diff --git a/cmake/modules/FindiViewHMD.cmake b/cmake/modules/FindiViewHMD.cmake deleted file mode 100644 index e408c92380..0000000000 --- a/cmake/modules/FindiViewHMD.cmake +++ /dev/null @@ -1,38 +0,0 @@ -# -# FindiViewHMD.cmake -# -# Try to find the SMI iViewHMD eye tracker library -# -# You must provide a IVIEWHMD_ROOT_DIR which contains 3rdParty, include, and libs directories -# -# Once done this will define -# -# IVIEWHMD_FOUND - system found iViewHMD -# IVIEWHMD_INCLUDE_DIRS - the iViewHMD include directory -# IVIEWHMD_LIBRARIES - link this to use iViewHMD -# -# Created on 27 Jul 2015 by David Rowe -# Copyright 2015 High Fidelity, Inc. -# - -if (WIN32) - - include("${MACRO_DIR}/HifiLibrarySearchHints.cmake") - hifi_library_search_hints("iViewHMD") - - find_path(IVIEWHMD_INCLUDE_DIRS iViewHMDAPI.h PATH_SUFFIXES include HINTS ${IVIEWHMD_SEARCH_DIRS}) - find_library(IVIEWHMD_LIBRARIES NAMES iViewHMDAPI PATH_SUFFIXES libs/x86 HINTS ${IVIEWHMD_SEARCH_DIRS}) - find_path(IVIEWHMD_API_DLL_PATH iViewHMDAPI.dll PATH_SUFFIXES libs/x86 HINTS ${IVIEWHMD_SEARCH_DIRS}) - list(APPEND IVIEWHMD_REQUIREMENTS IVIEWHMD_INCLUDE_DIRS IVIEWHMD_LIBRARIES IVIEWHMD_API_DLL_PATH) - - find_path(IVIEWHMD_DLL_PATH_3RD_PARTY libiViewNG.dll PATH_SUFFIXES 3rdParty HINTS ${IVIEWHMD_SEARCH_DIRS}) - list(APPEND IVIEWHMD_REQUIREMENTS IVIEWHMD_DLL_PATH_3RD_PARTY) - - include(FindPackageHandleStandardArgs) - find_package_handle_standard_args(IVIEWHMD DEFAULT_MSG ${IVIEWHMD_REQUIREMENTS}) - - add_paths_to_fixup_libs(${IVIEWHMD_API_DLL_PATH} ${IVIEWHMD_DLL_PATH_3RD_PARTY}) - - mark_as_advanced(IVIEWHMD_INCLUDE_DIRS IVIEWHMD_LIBRARIES IVIEWHMD_SEARCH_DIRS) - -endif() diff --git a/domain-server/resources/describe-settings.json b/domain-server/resources/describe-settings.json index 9cb4c2cab9..b854955953 100644 --- a/domain-server/resources/describe-settings.json +++ b/domain-server/resources/describe-settings.json @@ -1,5 +1,5 @@ { - "version": 2.3, + "version": 2.4, "settings": [ { "name": "metaverse", @@ -1705,6 +1705,114 @@ } ] }, + { + "name": "oauth", + "label": "OAuth", + "show_on_enable": true, + "settings": [ + { + "name": "enable", + "type": "checkbox", + "default": false, + "hidden": true + }, + { + "name": "admin-users", + "label": "Admin Users", + "type": "table", + "can_add_new_rows": true, + "help": "Any of these users can administer the domain.", + "numbered": false, + "backup": false, + "advanced": false, + "columns": [ + { + "name": "username", + "label": "Username", + "can_set": true + } + ] + }, + { + "name": "admin-roles", + "label": "Admin Roles", + "type": "table", + "can_add_new_rows": true, + "help": "Any user with any of these metaverse roles can administer the domain.", + "numbered": false, + "backup": false, + "advanced": true, + "columns": [ + { + "name": "role", + "label": "Role", + "can_set": true + } + ] + }, + { + "name": "client-id", + "label": "Client ID", + "help": "OAuth client ID.", + "default": "", + "advanced": true, + "backup": false + }, + { + "name": "client-secret", + "label": "Client Secret", + "help": "OAuth client secret.", + "type": "password", + "password_placeholder": "******", + "value-hidden": true, + "advanced": true, + "backup": false + }, + { + "name": "provider", + "label": "Provider", + "help": "OAuth provider URL.", + "default": "https://metaverse.highfidelity.com", + "advanced": true, + "backup": false + }, + { + "name": "hostname", + "label": "Hostname", + "help": "OAuth hostname.", + "default": "", + "advanced": true, + "backup": false + }, + { + "name": "key-passphrase", + "label": "SSL Private Key Passphrase", + "help": "SSL Private Key Passphrase", + "type": "password", + "password_placeholder": "******", + "value-hidden": true, + "advanced": true, + "backup": false + }, + { + "name": "cert-fingerprint", + "type": "hidden", + "readonly": true, + "advanced": true, + "backup": false + }, + { + "name": "cert", + "advanced": true, + "backup": false + }, + { + "name": "key", + "advanced": true, + "backup": false + } + ] + }, { "name": "automatic_content_archives", "label": "Automatic Content Archives", diff --git a/domain-server/resources/web/js/base-settings.js b/domain-server/resources/web/js/base-settings.js index bd96f636a8..295013878c 100644 --- a/domain-server/resources/web/js/base-settings.js +++ b/domain-server/resources/web/js/base-settings.js @@ -2,6 +2,9 @@ var DomainInfo = null; var viewHelpers = { getFormGroup: function(keypath, setting, values, isAdvanced) { + if (setting.hidden) { + return ""; + } form_group = "
" } - - form_group += "" + setting.help + "" + if (setting.help) { + form_group += "" + setting.help + "" + } } } @@ -114,12 +118,17 @@ function reloadSettings(callback) { data.descriptions.push(Settings.extraGroupsAtEnd[endGroupIndex]); } + data.descriptions = data.descriptions.map(function(x) { + x.hidden = x.hidden || (x.show_on_enable && data.values[x.name] && !data.values[x.name].enable); + return x; + }); + $('#panels').html(Settings.panelsTemplate(data)); Settings.data = data; Settings.initialValues = form2js('settings-form', ".", false, cleanupFormValues, true); - Settings.afterReloadActions(); + Settings.afterReloadActions(data); // setup any bootstrap switches $('.toggle-checkbox').bootstrapSwitch(); @@ -129,10 +138,14 @@ function reloadSettings(callback) { Settings.pendingChanges = 0; // call the callback now that settings are loaded - callback(true); + if (callback) { + callback(true); + } }).fail(function() { // call the failure object since settings load faild - callback(false) + if (callback) { + callback(false); + } }); } diff --git a/domain-server/resources/web/settings/js/settings.js b/domain-server/resources/web/settings/js/settings.js index 08d0550841..136d5b0ebc 100644 --- a/domain-server/resources/web/settings/js/settings.js +++ b/domain-server/resources/web/settings/js/settings.js @@ -18,7 +18,19 @@ $(document).ready(function(){ Settings.extraGroupsAtIndex = Settings.extraDomainGroupsAtIndex; var METAVERSE_URL = URLs.METAVERSE_URL; - Settings.afterReloadActions = function() { + var SSL_PRIVATE_KEY_FILE_ID = 'ssl-private-key-file'; + var SSL_PRIVATE_KEY_CONTENTS_ID = 'key-contents'; + var SSL_PRIVATE_KEY_CONTENTS_NAME = 'oauth.key-contents'; + var SSL_CERT_UPLOAD_ID = 'ssl-cert-button'; + var SSL_CERT_FILE_ID = 'ssl-cert-file'; + var SSL_CERT_FINGERPRINT_ID = 'cert-fingerprint'; + var SSL_CERT_FINGERPRINT_SPAN_ID = 'cert-fingerprint-span-id'; + var SSL_CERT_CONTENTS_ID = 'cert-contents'; + var SSL_CERT_CONTENTS_NAME = 'oauth.cert-contents'; + var SSL_PRIVATE_KEY_PATH = 'oauth.key'; + var SSL_CERT_PATH = 'oauth.cert'; + + Settings.afterReloadActions = function(data) { getMetaverseUrl(function(metaverse_url) { METAVERSE_URL = metaverse_url; @@ -32,6 +44,8 @@ $(document).ready(function(){ setupDomainNetworkingSettings(); // setupDomainLabelSetting(); + setupSettingsOAuth(data); + setupSettingsBackup(); if (domainIDIsSet()) { @@ -124,6 +138,48 @@ $(document).ready(function(){ } } + if (formJSON["oauth"]) { + var private_key = formJSON["oauth"]["key-contents"]; + var cert = formJSON["oauth"]["cert-contents"]; + var oauthErrors = ""; + if (private_key != undefined) { + var pattern = /-+BEGIN PRIVATE KEY-+[A-Za-z0-9+/\n=]*-+END PRIVATE KEY-+/m; + if (!pattern.test(private_key)) { + oauthErrors += "Private key must be in PEM format
"; + } + } + if (cert != undefined) { + var pattern = /-+BEGIN CERTIFICATE-+[A-Za-z0-9+/\n=]*-+END CERTIFICATE-+/m; + if (!pattern.test(cert)) { + oauthErrors += "Certificate must be in PEM format
"; + } + } + if ($('#oauth.panel').length) { + if (!$('input[name="oauth.client-id"]').val()) { + oauthErrors += "OAuth requires a client Id.
"; + } + if (!$('input[name="oauth.provider"]').val()) { + oauthErrors += "OAuth requires a provider.
"; + } + if (!$('input[name="oauth.hostname"]').val()) { + oauthErrors += "OAuth requires a hostname.
"; + } + if (!$('input[name="' + SSL_PRIVATE_KEY_PATH + '"]').val() && !$('input[name="' + SSL_PRIVATE_KEY_CONTENTS_NAME + '"]').val()) { + oauthErrors += "OAuth requires an SSL Private Key.
"; + } + if (!$('input[name="' + SSL_CERT_PATH + '"]').val() && !$('input[name="' + SSL_CERT_CONTENTS_NAME + '"]').val()) { + oauthErrors += "OAuth requires an SSL Certificate.
"; + } + if (!$("table[name='oauth.admin-users'] tr.value-row").length && + !$("table[name='oauth.admin-roles'] tr.value-row").length) { + oauthErrors += "OAuth must have at least one admin user or admin role.
"; + } + } + if (oauthErrors) { + bootbox.alert({ "message": oauthErrors, "title": "OAuth Configuration Error" }); + return false; + } + } postSettings(formJSON); }; @@ -1035,6 +1091,67 @@ $(document).ready(function(){ }); } + function setupSettingsOAuth(data) { + // construct the HTML needed for the settings backup panel + var html = "
"; + html += "
"; + html += ""; + html += ""; + html += ""; + html += "
"; + html += "
"; + html += ""; + html += "
Fingerprint:" + data.values.oauth["cert-fingerprint"] + "
"; + html += ""; + html += ""; + html += ""; + html += "
"; + + $('#oauth-advanced').append(html); + + $('#key-path-label').after($('[data-keypath="' + SSL_PRIVATE_KEY_PATH + '"]')); + $('#cert-path-label').after($('[data-keypath="' + SSL_CERT_PATH + '"]')); + $('[name="' + SSL_PRIVATE_KEY_PATH + '"]').val(data.values.oauth.key); + $('[name="' + SSL_CERT_PATH + '"]').val(data.values.oauth.cert); + + $('body').on('change input propertychange', '#' + SSL_PRIVATE_KEY_FILE_ID, function(e){ + var f = e.target.files[0]; + var reader = new FileReader(); + reader.onload = function(e) { + $('#' + SSL_PRIVATE_KEY_CONTENTS_ID).val(reader.result); + $('#' + SSL_PRIVATE_KEY_CONTENTS_ID).attr('data-changed', true); + $('[name="' + SSL_PRIVATE_KEY_PATH + '"]').val(''); + badgeForDifferences($('#' + SSL_PRIVATE_KEY_CONTENTS_ID)); + } + reader.readAsText(f); + }); + $('body').on('change input propertychange', '#' + SSL_CERT_FILE_ID, function(e){ + var f = e.target.files[0]; + var reader = new FileReader(); + reader.onload = function(e) { + $('#' + SSL_CERT_CONTENTS_ID).val(reader.result); + $('#' + SSL_CERT_CONTENTS_ID).attr('data-changed', true); + $('[name="' + SSL_CERT_PATH + '"]').val(''); + $('#' + SSL_CERT_FINGERPRINT_SPAN_ID).text(''); + badgeForDifferences($('#' + SSL_CERT_CONTENTS_ID)); + } + reader.readAsText(f); + }); + + $('body').on('change input propertychange', '[name="' + SSL_PRIVATE_KEY_PATH + '"]', function(e){ + $('#' + SSL_PRIVATE_KEY_FILE_ID).val(''); + $('#' + SSL_PRIVATE_KEY_CONTENTS_ID).val(''); + badgeForDifferences($('[name="' + SSL_PRIVATE_KEY_PATH + '"]').attr('data-changed', true)); + }); + + $('body').on('change input propertychange', '[name="' + SSL_CERT_PATH + '"]', function(e){ + $('#' + SSL_CERT_FILE_ID).val(''); + $('#' + SSL_CERT_CONTENTS_ID).val(''); + $('#' + SSL_CERT_FINGERPRINT_SPAN_ID).text(''); + badgeForDifferences($('[name="' + SSL_CERT_PATH + '"]').attr('data-changed', true)); + }); + } + var RESTORE_SETTINGS_UPLOAD_ID = 'restore-settings-button'; var RESTORE_SETTINGS_FILE_ID = 'restore-settings-file'; diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index 74ad014b53..307a43ee88 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -226,9 +226,10 @@ DomainServer::DomainServer(int argc, char* argv[]) : setupGroupCacheRefresh(); - // if we were given a certificate/private key or oauth credentials they must succeed - if (!(optionallyReadX509KeyAndCertificate() && optionallySetupOAuth())) { - return; + optionallySetupOAuth(); + + if (_oauthEnable) { + _oauthEnable = optionallyReadX509KeyAndCertificate(); } _settingsManager.apiRefreshGroupInformation(); @@ -447,8 +448,9 @@ QUuid DomainServer::getID() { } bool DomainServer::optionallyReadX509KeyAndCertificate() { - const QString X509_CERTIFICATE_OPTION = "cert"; - const QString X509_PRIVATE_KEY_OPTION = "key"; + const QString X509_CERTIFICATE_OPTION = "oauth.cert"; + const QString X509_PRIVATE_KEY_OPTION = "oauth.key"; + const QString X509_PRIVATE_KEY_PASSPHRASE_OPTION = "oauth.key-passphrase"; const QString X509_KEY_PASSPHRASE_ENV = "DOMAIN_SERVER_KEY_PASSPHRASE"; QString certPath = _settingsManager.valueForKeyPath(X509_CERTIFICATE_OPTION).toString(); @@ -459,7 +461,12 @@ bool DomainServer::optionallyReadX509KeyAndCertificate() { // this is used for Oauth callbacks when authorizing users against a data server // let's make sure we can load the key and certificate - QString keyPassphraseString = QProcessEnvironment::systemEnvironment().value(X509_KEY_PASSPHRASE_ENV); + QString keyPassphraseEnv = QProcessEnvironment::systemEnvironment().value(X509_KEY_PASSPHRASE_ENV); + QString keyPassphraseString = _settingsManager.valueForKeyPath(X509_PRIVATE_KEY_PASSPHRASE_OPTION).toString(); + + if (!keyPassphraseEnv.isEmpty()) { + keyPassphraseString = keyPassphraseEnv; + } qDebug() << "Reading certificate file at" << certPath << "for HTTPS."; qDebug() << "Reading key file at" << keyPath << "for HTTPS."; @@ -473,16 +480,15 @@ bool DomainServer::optionallyReadX509KeyAndCertificate() { QSslCertificate sslCertificate(&certFile); QSslKey privateKey(&keyFile, QSsl::Rsa, QSsl::Pem, QSsl::PrivateKey, keyPassphraseString.toUtf8()); + if (privateKey.isNull()) { + qCritical() << "SSL Private Key Not Loading. Bad password or key format?"; + } + _httpsManager.reset(new HTTPSManager(QHostAddress::AnyIPv4, DOMAIN_SERVER_HTTPS_PORT, sslCertificate, privateKey, QString(), this)); qDebug() << "TCP server listening for HTTPS connections on" << DOMAIN_SERVER_HTTPS_PORT; } else if (!certPath.isEmpty() || !keyPath.isEmpty()) { - static const QString MISSING_CERT_ERROR_MSG = "Missing certificate or private key. domain-server will now quit."; - static const int MISSING_CERT_ERROR_CODE = 3; - - QMetaObject::invokeMethod(this, "queuedQuit", Qt::QueuedConnection, - Q_ARG(QString, MISSING_CERT_ERROR_MSG), Q_ARG(int, MISSING_CERT_ERROR_CODE)); return false; } @@ -490,10 +496,12 @@ bool DomainServer::optionallyReadX509KeyAndCertificate() { } bool DomainServer::optionallySetupOAuth() { - const QString OAUTH_PROVIDER_URL_OPTION = "oauth-provider"; - const QString OAUTH_CLIENT_ID_OPTION = "oauth-client-id"; + const QString OAUTH_ENABLE_OPTION = "oauth.enable"; + const QString OAUTH_PROVIDER_URL_OPTION = "oauth.provider"; + const QString OAUTH_CLIENT_ID_OPTION = "oauth.client-id"; const QString OAUTH_CLIENT_SECRET_ENV = "DOMAIN_SERVER_CLIENT_SECRET"; - const QString REDIRECT_HOSTNAME_OPTION = "hostname"; + const QString OAUTH_CLIENT_SECRET_OPTION = "oauth.client-secret"; + const QString REDIRECT_HOSTNAME_OPTION = "oauth.hostname"; _oauthProviderURL = QUrl(_settingsManager.valueForKeyPath(OAUTH_PROVIDER_URL_OPTION).toString()); @@ -502,22 +510,24 @@ bool DomainServer::optionallySetupOAuth() { _oauthProviderURL = NetworkingConstants::METAVERSE_SERVER_URL(); } + _oauthClientSecret = QProcessEnvironment::systemEnvironment().value(OAUTH_CLIENT_SECRET_ENV); + if (_oauthClientSecret.isEmpty()) { + _oauthClientSecret = _settingsManager.valueForKeyPath(OAUTH_CLIENT_SECRET_OPTION).toString(); + } auto accountManager = DependencyManager::get(); accountManager->setAuthURL(_oauthProviderURL); _oauthClientID = _settingsManager.valueForKeyPath(OAUTH_CLIENT_ID_OPTION).toString(); - _oauthClientSecret = QProcessEnvironment::systemEnvironment().value(OAUTH_CLIENT_SECRET_ENV); _hostname = _settingsManager.valueForKeyPath(REDIRECT_HOSTNAME_OPTION).toString(); - if (!_oauthClientID.isEmpty()) { + _oauthEnable = _settingsManager.valueForKeyPath(OAUTH_ENABLE_OPTION).toBool(); + + if (_oauthEnable) { if (_oauthProviderURL.isEmpty() || _hostname.isEmpty() || _oauthClientID.isEmpty() || _oauthClientSecret.isEmpty()) { - static const QString MISSING_OAUTH_INFO_MSG = "Missing OAuth provider URL, hostname, client ID, or client secret. domain-server will now quit."; - static const int MISSING_OAUTH_INFO_ERROR_CODE = 4; - QMetaObject::invokeMethod(this, "queuedQuit", Qt::QueuedConnection, - Q_ARG(QString, MISSING_OAUTH_INFO_MSG), Q_ARG(int, MISSING_OAUTH_INFO_ERROR_CODE)); + _oauthEnable = false; return false; } else { qDebug() << "OAuth will be used to identify clients using provider at" << _oauthProviderURL.toString(); @@ -2693,8 +2703,8 @@ void DomainServer::profileRequestFinished() { std::pair DomainServer::isAuthenticatedRequest(HTTPConnection* connection) { static const QByteArray HTTP_COOKIE_HEADER_KEY = "Cookie"; - static const QString ADMIN_USERS_CONFIG_KEY = "admin-users"; - static const QString ADMIN_ROLES_CONFIG_KEY = "admin-roles"; + static const QString ADMIN_USERS_CONFIG_KEY = "oauth.admin-users"; + static const QString ADMIN_ROLES_CONFIG_KEY = "oauth.admin-roles"; static const QString BASIC_AUTH_USERNAME_KEY_PATH = "security.http_username"; static const QString BASIC_AUTH_PASSWORD_KEY_PATH = "security.http_password"; const QString COOKIE_UUID_REGEX_STRING = HIFI_SESSION_COOKIE_KEY + "=([\\d\\w-]+)($|;)"; @@ -2704,8 +2714,7 @@ std::pair DomainServer::isAuthenticatedRequest(HTTPConnection* c QVariant adminUsersVariant = _settingsManager.valueForKeyPath(ADMIN_USERS_CONFIG_KEY); QVariant adminRolesVariant = _settingsManager.valueForKeyPath(ADMIN_ROLES_CONFIG_KEY); - if (!_oauthProviderURL.isEmpty() - && (adminUsersVariant.isValid() || adminRolesVariant.isValid())) { + if (_oauthEnable) { QString cookieString = connection->requestHeader(HTTP_COOKIE_HEADER_KEY); QRegExp cookieUUIDRegex(COOKIE_UUID_REGEX_STRING); diff --git a/domain-server/src/DomainServer.h b/domain-server/src/DomainServer.h index 02362abd7b..5e8eee53fe 100644 --- a/domain-server/src/DomainServer.h +++ b/domain-server/src/DomainServer.h @@ -236,6 +236,7 @@ private: bool _isUsingDTLS { false }; + bool _oauthEnable { false }; QUrl _oauthProviderURL; QString _oauthClientID; QString _oauthClientSecret; diff --git a/domain-server/src/DomainServerSettingsManager.cpp b/domain-server/src/DomainServerSettingsManager.cpp index 17d473f02c..73d78a5c70 100644 --- a/domain-server/src/DomainServerSettingsManager.cpp +++ b/domain-server/src/DomainServerSettingsManager.cpp @@ -22,7 +22,9 @@ #include #include #include +#include #include +#include #include #include @@ -46,10 +48,14 @@ const QString DESCRIPTION_SETTINGS_KEY = "settings"; const QString SETTING_DEFAULT_KEY = "default"; const QString DESCRIPTION_NAME_KEY = "name"; const QString DESCRIPTION_GROUP_LABEL_KEY = "label"; +const QString DESCRIPTION_GROUP_SHOW_ON_ENABLE_KEY = "show_on_enable"; +const QString DESCRIPTION_ENABLE_KEY = "enable"; const QString DESCRIPTION_BACKUP_FLAG_KEY = "backup"; const QString SETTING_DESCRIPTION_TYPE_KEY = "type"; const QString DESCRIPTION_COLUMNS_KEY = "columns"; const QString CONTENT_SETTING_FLAG_KEY = "content_setting"; +static const QString SPLIT_MENU_GROUPS_DOMAIN_SETTINGS_KEY = "domain_settings"; +static const QString SPLIT_MENU_GROUPS_CONTENT_SETTINGS_KEY = "content_settings"; const QString SETTINGS_VIEWPOINT_KEY = "viewpoint"; @@ -136,6 +142,10 @@ void DomainServerSettingsManager::splitSettingsDescription() { settingsDropdownGroup[DESCRIPTION_GROUP_LABEL_KEY] = groupObject[DESCRIPTION_GROUP_LABEL_KEY]; + if (groupObject.contains(DESCRIPTION_GROUP_SHOW_ON_ENABLE_KEY)) { + settingsDropdownGroup[DESCRIPTION_GROUP_SHOW_ON_ENABLE_KEY] = groupObject[DESCRIPTION_GROUP_SHOW_ON_ENABLE_KEY]; + } + static const QString DESCRIPTION_GROUP_HTML_ID_KEY = "html_id"; if (groupObject.contains(DESCRIPTION_GROUP_HTML_ID_KEY)) { settingsDropdownGroup[DESCRIPTION_GROUP_HTML_ID_KEY] = groupObject[DESCRIPTION_GROUP_HTML_ID_KEY]; @@ -170,9 +180,6 @@ void DomainServerSettingsManager::splitSettingsDescription() { // populate the settings menu groups with what we've collected - static const QString SPLIT_MENU_GROUPS_DOMAIN_SETTINGS_KEY = "domain_settings"; - static const QString SPLIT_MENU_GROUPS_CONTENT_SETTINGS_KEY = "content_settings"; - _settingsMenuGroups[SPLIT_MENU_GROUPS_DOMAIN_SETTINGS_KEY] = domainSettingsMenuGroups; _settingsMenuGroups[SPLIT_MENU_GROUPS_CONTENT_SETTINGS_KEY] = contentSettingsMenuGroups; } @@ -448,6 +455,77 @@ void DomainServerSettingsManager::setupConfigMap(const QString& userConfigFilena packPermissions(); } + if (oldVersion < 2.4) { + // migrate oauth settings to their own group + const QString ADMIN_USERS = "admin-users"; + const QString OAUTH_ADMIN_USERS = "oauth.admin-users"; + const QString OAUTH_CLIENT_ID = "oauth.client-id"; + const QString ALT_ADMIN_USERS = "admin.users"; + const QString ADMIN_ROLES = "admin-roles"; + const QString OAUTH_ADMIN_ROLES = "oauth.admin-roles"; + const QString OAUTH_ENABLE = "oauth.enable"; + + QVector > conversionMap = { + {"key", "oauth.key"}, + {"cert", "oauth.cert"}, + {"hostname", "oauth.hostname"}, + {"oauth-client-id", "oauth.client-id"}, + {"oauth-provider", "oauth.provider"} + }; + + for (auto & conversion : conversionMap) { + QVariant* prevValue = _configMap.valueForKeyPath(conversion.first); + if (prevValue) { + auto newValue = _configMap.valueForKeyPath(conversion.second, true); + *newValue = *prevValue; + } + } + + QVariant* client_id = _configMap.valueForKeyPath(OAUTH_CLIENT_ID); + if (client_id) { + QVariant* oauthEnable = _configMap.valueForKeyPath(OAUTH_ENABLE, true); + + *oauthEnable = QVariant(true); + } + + QVariant* oldAdminUsers = _configMap.valueForKeyPath(ADMIN_USERS); + QVariant* newAdminUsers = _configMap.valueForKeyPath(OAUTH_ADMIN_USERS, true); + QVariantList adminUsers(newAdminUsers->toList()); + if (oldAdminUsers) { + QStringList adminUsersList = oldAdminUsers->toStringList(); + for (auto & user : adminUsersList) { + if (!adminUsers.contains(user)) { + adminUsers.append(user); + } + } + } + QVariant* altAdminUsers = _configMap.valueForKeyPath(ALT_ADMIN_USERS); + if (altAdminUsers) { + QStringList adminUsersList = altAdminUsers->toStringList(); + for (auto & user : adminUsersList) { + if (!adminUsers.contains(user)) { + adminUsers.append(user); + } + } + } + + *newAdminUsers = adminUsers; + + QVariant* oldAdminRoles = _configMap.valueForKeyPath(ADMIN_ROLES); + QVariant* newAdminRoles = _configMap.valueForKeyPath(OAUTH_ADMIN_ROLES, true); + QVariantList adminRoles(newAdminRoles->toList()); + if (oldAdminRoles) { + QStringList adminRoleList = oldAdminRoles->toStringList(); + for (auto & role : adminRoleList) { + if (!adminRoles.contains(role)) { + adminRoles.append(role); + } + } + } + + *newAdminRoles = adminRoles; + } + // write the current description version to our settings *versionVariant = _descriptionVersion; @@ -1185,7 +1263,24 @@ bool DomainServerSettingsManager::handleAuthenticatedHTTPRequest(HTTPConnection return true; } else if (url.path() == SETTINGS_MENU_GROUPS_PATH) { - connection->respond(HTTPConnection::StatusCode200, QJsonDocument(_settingsMenuGroups).toJson(), "application/json"); + + QJsonObject settings; + for (auto & key : _settingsMenuGroups.keys()) { + const QJsonArray& settingGroups = _settingsMenuGroups[key].toArray(); + QJsonArray groups; + foreach (const QJsonValue& group, settingGroups) { + QJsonObject groupObject = group.toObject(); + QVariant* enableKey = _configMap.valueForKeyPath(groupObject[DESCRIPTION_NAME_KEY].toString() + "." + DESCRIPTION_ENABLE_KEY); + + if (!groupObject.contains(DESCRIPTION_GROUP_SHOW_ON_ENABLE_KEY) + || (groupObject[DESCRIPTION_GROUP_SHOW_ON_ENABLE_KEY].toBool() && enableKey && enableKey->toBool() )) { + groups.append(groupObject); + } + } + settings[key] = groups; + } + + connection->respond(HTTPConnection::StatusCode200, QJsonDocument(settings).toJson(), "application/json"); return true; } else if (url.path() == SETTINGS_BACKUP_PATH) { @@ -1446,6 +1541,28 @@ QJsonObject DomainServerSettingsManager::settingsResponseObjectForType(const QSt } } + // add 'derived' values used primarily for UI + + const QString X509_CERTIFICATE_OPTION = "oauth.cert"; + + QString certPath = valueForKeyPath(X509_CERTIFICATE_OPTION).toString(); + if (!certPath.isEmpty()) { + // the user wants to use the following cert and key for HTTPS + // this is used for Oauth callbacks when authorizing users against a data server + // let's make sure we can load the key and certificate + + qDebug() << "Reading certificate file at" << certPath << "for HTTPS."; + + QFile certFile(certPath); + certFile.open(QIODevice::ReadOnly); + + QSslCertificate sslCertificate(&certFile); + QString digest = sslCertificate.digest().toHex(':'); + auto groupObject = responseObject["oauth"].toObject(); + groupObject["cert-fingerprint"] = digest; + responseObject["oauth"] = groupObject; + } + return responseObject; } @@ -1551,23 +1668,65 @@ QJsonObject DomainServerSettingsManager::settingDescriptionFromGroup(const QJson return QJsonObject(); } -bool DomainServerSettingsManager::recurseJSONObjectAndOverwriteSettings(const QJsonObject& postedObject, +bool DomainServerSettingsManager::recurseJSONObjectAndOverwriteSettings(const QJsonObject& postedSettingsObject, SettingsType settingsType) { // take a write lock since we're about to overwrite settings in the config map QWriteLocker locker(&_settingsLock); + QJsonObject postedObject(postedSettingsObject); + static const QString SECURITY_ROOT_KEY = "security"; static const QString AC_SUBNET_WHITELIST_KEY = "ac_subnet_whitelist"; static const QString BROADCASTING_KEY = "broadcasting"; static const QString WIZARD_KEY = "wizard"; static const QString DESCRIPTION_ROOT_KEY = "descriptors"; + static const QString OAUTH_ROOT_KEY = "oauth"; + static const QString OAUTH_KEY_CONTENTS = "key-contents"; + static const QString OAUTH_CERT_CONTENTS = "cert-contents"; + static const QString OAUTH_CERT_PATH = "cert"; + static const QString OAUTH_KEY_PASSPHRASE = "key-passphrase"; + static const QString OAUTH_KEY_PATH = "key"; auto& settingsVariant = _configMap.getConfig(); bool needRestart = false; auto& filteredDescriptionArray = settingsType == DomainSettings ? _domainSettingsDescription : _contentSettingsDescription; + auto oauthObject = postedObject[OAUTH_ROOT_KEY].toObject(); + if (oauthObject.contains(OAUTH_CERT_CONTENTS)) { + QSslCertificate cert(oauthObject[OAUTH_CERT_CONTENTS].toString().toUtf8()); + if (!cert.isNull()) { + static const QString CERT_FILE_NAME = "certificate.crt"; + auto certPath = PathUtils::getAppDataFilePath(CERT_FILE_NAME); + QFile file(certPath); + if (file.open(QFile::WriteOnly)) { + file.write(cert.toPem()); + file.close(); + } + oauthObject[OAUTH_CERT_PATH] = certPath; + } + oauthObject.remove(OAUTH_CERT_CONTENTS); + } + if (oauthObject.contains(OAUTH_KEY_CONTENTS)) { + QString keyPassphraseString = oauthObject[OAUTH_KEY_PASSPHRASE].toString(); + QSslKey key(oauthObject[OAUTH_KEY_CONTENTS].toString().toUtf8(), QSsl::Rsa, QSsl::Pem, QSsl::PrivateKey, keyPassphraseString.toUtf8()); + if (!key.isNull()) { + static const QString KEY_FILE_NAME = "certificate.key"; + auto keyPath = PathUtils::getAppDataFilePath(KEY_FILE_NAME); + QFile file(keyPath); + if (file.open(QFile::WriteOnly)) { + file.write(key.toPem()); + file.close(); + file.setPermissions(QFile::ReadOwner | QFile::WriteOwner); + } + oauthObject[OAUTH_KEY_PATH] = keyPath; + } + oauthObject.remove(OAUTH_KEY_CONTENTS); + } + + postedObject[OAUTH_ROOT_KEY] = oauthObject; + // Iterate on the setting groups foreach(const QString& rootKey, postedObject.keys()) { const QJsonValue& rootValue = postedObject[rootKey]; @@ -1752,6 +1911,8 @@ void DomainServerSettingsManager::persistToFile() { _configMap.loadConfig(); return; // defend against future code } + + QFile(settingsFilename).setPermissions(QFileDevice::ReadOwner | QFileDevice::WriteOwner); } QStringList DomainServerSettingsManager::getAllKnownGroupNames() { diff --git a/interface/external/iViewHMD/readme.txt b/interface/external/iViewHMD/readme.txt deleted file mode 100644 index 4b3d59349b..0000000000 --- a/interface/external/iViewHMD/readme.txt +++ /dev/null @@ -1,14 +0,0 @@ - -Instructions for adding SMI HMD Eye Tracking to Interface on Windows -David Rowe, 27 Jul 2015. - -1. Download and install the SMI HMD Eye Tracking software from http://update.smivision.com/iViewNG-HMD.exe. - -2. Copy the SDK folders (3rdParty, include, libs) from the SDK installation folder C:\Program Files (x86)\SMI\iViewNG-HMD\SDK - into the interface/externals/iViewHMD folder. This readme.txt should be there as well. - - You may optionally choose to copy the SDK folders to a location outside the repository (so you can re-use with different - checkouts and different projects). If so, set the ENV variable "HIFI_LIB_DIR" to a directory containing a subfolder - "iViewHMD" that contains the folders mentioned above. - -3. Clear your build directory, run cmake and build, and you should be all set. diff --git a/interface/resources/avatar/avatar-animation.json b/interface/resources/avatar/avatar-animation.json index 15ef447e61..9ab4d5d935 100644 --- a/interface/resources/avatar/avatar-animation.json +++ b/interface/resources/avatar/avatar-animation.json @@ -4600,6 +4600,10 @@ { "state": "strafeLeftHmd", "var": "isMovingLeftHmd" + }, + { + "state": "idle", + "var": "isNotSeated" } ] }, diff --git a/interface/resources/controllers/standard.json b/interface/resources/controllers/standard.json index 0a5bd12460..195f909942 100644 --- a/interface/resources/controllers/standard.json +++ b/interface/resources/controllers/standard.json @@ -162,7 +162,14 @@ { "from": "Standard.Head", "to": "Actions.Head" }, { "from": "Standard.LeftArm", "to": "Actions.LeftArm" }, { "from": "Standard.RightArm", "to": "Actions.RightArm" }, - + + { "from": "Standard.LeftEye", "to": "Actions.LeftEye" }, + { "from": "Standard.RightEye", "to": "Actions.RightEye" }, + + { "from": "Standard.LeftEyeBlink", "to": "Actions.LeftEyeBlink" }, + { "from": "Standard.RightEyeBlink", "to": "Actions.RightEyeBlink" }, + + { "from": "Standard.TrackedObject00", "to" : "Actions.TrackedObject00" }, { "from": "Standard.TrackedObject01", "to" : "Actions.TrackedObject01" }, { "from": "Standard.TrackedObject02", "to" : "Actions.TrackedObject02" }, diff --git a/interface/resources/controllers/standard_nomovement.json b/interface/resources/controllers/standard_nomovement.json index 015bc33056..602d3bb798 100644 --- a/interface/resources/controllers/standard_nomovement.json +++ b/interface/resources/controllers/standard_nomovement.json @@ -57,7 +57,13 @@ { "from": "Standard.Head", "to": "Actions.Head" }, { "from": "Standard.LeftArm", "to": "Actions.LeftArm" }, { "from": "Standard.RightArm", "to": "Actions.RightArm" }, - + + { "from": "Standard.LeftEye", "to": "Actions.LeftEye" }, + { "from": "Standard.RightEye", "to": "Actions.RightEye" }, + + { "from": "Standard.LeftEyeBlink", "to": "Actions.LeftEyeBlink" }, + { "from": "Standard.RightEyeBlink", "to": "Actions.RightEyeBlink" }, + { "from": "Standard.TrackedObject00", "to" : "Actions.TrackedObject00" }, { "from": "Standard.TrackedObject01", "to" : "Actions.TrackedObject01" }, { "from": "Standard.TrackedObject02", "to" : "Actions.TrackedObject02" }, diff --git a/interface/resources/controllers/vive.json b/interface/resources/controllers/vive.json index 730e1bcb58..b6fae1dd79 100644 --- a/interface/resources/controllers/vive.json +++ b/interface/resources/controllers/vive.json @@ -54,8 +54,52 @@ { "from": "Vive.RightApplicationMenu", "to": "Standard.RightSecondaryThumb" }, { "from": "Vive.LeftHand", "to": "Standard.LeftHand" }, + { "from": "Vive.LeftHandThumb1", "to": "Standard.LeftHandThumb1"}, + { "from": "Vive.LeftHandThumb2", "to": "Standard.LeftHandThumb2"}, + { "from": "Vive.LeftHandThumb3", "to": "Standard.LeftHandThumb3"}, + { "from": "Vive.LeftHandThumb4", "to": "Standard.LeftHandThumb4"}, + { "from": "Vive.LeftHandIndex1", "to": "Standard.LeftHandIndex1"}, + { "from": "Vive.LeftHandIndex2", "to": "Standard.LeftHandIndex2"}, + { "from": "Vive.LeftHandIndex3", "to": "Standard.LeftHandIndex3"}, + { "from": "Vive.LeftHandIndex4", "to": "Standard.LeftHandIndex4"}, + { "from": "Vive.LeftHandMiddle1", "to": "Standard.LeftHandMiddle1"}, + { "from": "Vive.LeftHandMiddle2", "to": "Standard.LeftHandMiddle2"}, + { "from": "Vive.LeftHandMiddle3", "to": "Standard.LeftHandMiddle3"}, + { "from": "Vive.LeftHandMiddle4", "to": "Standard.LeftHandMiddle4"}, + { "from": "Vive.LeftHandRing1", "to": "Standard.LeftHandRing1"}, + { "from": "Vive.LeftHandRing2", "to": "Standard.LeftHandRing2"}, + { "from": "Vive.LeftHandRing3", "to": "Standard.LeftHandRing3"}, + { "from": "Vive.LeftHandRing4", "to": "Standard.LeftHandRing4"}, + { "from": "Vive.LeftHandPinky1", "to": "Standard.LeftHandPinky1"}, + { "from": "Vive.LeftHandPinky2", "to": "Standard.LeftHandPinky2"}, + { "from": "Vive.LeftHandPinky3", "to": "Standard.LeftHandPinky3"}, + { "from": "Vive.LeftHandPinky4", "to": "Standard.LeftHandPinky4"}, { "from": "Vive.RightHand", "to": "Standard.RightHand" }, + { "from": "Vive.RightHandThumb1", "to": "Standard.RightHandThumb1"}, + { "from": "Vive.RightHandThumb2", "to": "Standard.RightHandThumb2"}, + { "from": "Vive.RightHandThumb3", "to": "Standard.RightHandThumb3"}, + { "from": "Vive.RightHandThumb4", "to": "Standard.RightHandThumb4"}, + { "from": "Vive.RightHandIndex1", "to": "Standard.RightHandIndex1"}, + { "from": "Vive.RightHandIndex2", "to": "Standard.RightHandIndex2"}, + { "from": "Vive.RightHandIndex3", "to": "Standard.RightHandIndex3"}, + { "from": "Vive.RightHandIndex4", "to": "Standard.RightHandIndex4"}, + { "from": "Vive.RightHandMiddle1", "to": "Standard.RightHandMiddle1"}, + { "from": "Vive.RightHandMiddle2", "to": "Standard.RightHandMiddle2"}, + { "from": "Vive.RightHandMiddle3", "to": "Standard.RightHandMiddle3"}, + { "from": "Vive.RightHandMiddle4", "to": "Standard.RightHandMiddle4"}, + { "from": "Vive.RightHandRing1", "to": "Standard.RightHandRing1"}, + { "from": "Vive.RightHandRing2", "to": "Standard.RightHandRing2"}, + { "from": "Vive.RightHandRing3", "to": "Standard.RightHandRing3"}, + { "from": "Vive.RightHandRing4", "to": "Standard.RightHandRing4"}, + { "from": "Vive.RightHandPinky1", "to": "Standard.RightHandPinky1"}, + { "from": "Vive.RightHandPinky2", "to": "Standard.RightHandPinky2"}, + { "from": "Vive.RightHandPinky3", "to": "Standard.RightHandPinky3"}, + { "from": "Vive.RightHandPinky4", "to": "Standard.RightHandPinky4"}, { "from": "Vive.Head", "to" : "Standard.Head" }, + { "from": "Vive.LeftEye", "to" : "Standard.LeftEye" }, + { "from": "Vive.RightEye", "to" : "Standard.RightEye" }, + { "from": "Vive.LeftEyeBlink", "to" : "Standard.LeftEyeBlink" }, + { "from": "Vive.RightEyeBlink", "to" : "Standard.RightEyeBlink" }, { "from": "Vive.LeftFoot", "to" : "Standard.LeftFoot", diff --git a/interface/resources/images/high-fidelity-banner.svg b/interface/resources/images/high-fidelity-banner.svg index d5666be0fa..19127e729c 100644 --- a/interface/resources/images/high-fidelity-banner.svg +++ b/interface/resources/images/high-fidelity-banner.svg @@ -1,17 +1 @@ - - - - - - - - - - - - - - - - - +Artboard 1 \ No newline at end of file diff --git a/interface/resources/qml/Web3DSurface.qml b/interface/resources/qml/Web3DSurface.qml index 32c19daf14..ff574ceaa5 100644 --- a/interface/resources/qml/Web3DSurface.qml +++ b/interface/resources/qml/Web3DSurface.qml @@ -12,20 +12,35 @@ import QtQuick 2.5 import "controls" as Controls -Controls.WebView { +Item { + id: root + anchors.fill: parent + property string url: "" + property string scriptUrl: null - // This is for JS/QML communication, which is unused in a Web3DOverlay, - // but not having this here results in spurious warnings about a - // missing signal - signal sendToScript(var message); + onUrlChanged: { + load(root.url, root.scriptUrl); + } - function onWebEventReceived(event) { - if (event.slice(0, 17) === "CLARA.IO DOWNLOAD") { - ApplicationInterface.addAssetToWorldFromURL(event.slice(18)); + onScriptUrlChanged: { + if (root.item) { + root.item.scriptUrl = root.scriptUrl; + } else { + load(root.url, root.scriptUrl); } } + property var item: null + + function load(url, scriptUrl) { + QmlSurface.load("./controls/WebView.qml", root, function(newItem) { + root.item = newItem + root.item.url = url + root.item.scriptUrl = scriptUrl + }) + } + Component.onCompleted: { - eventBridge.webEventReceived.connect(onWebEventReceived); + load(root.url, root.scriptUrl); } } diff --git a/interface/resources/qml/hifi/audio/Audio.qml b/interface/resources/qml/hifi/audio/Audio.qml index d3bf6aca0a..fccba12a8a 100644 --- a/interface/resources/qml/hifi/audio/Audio.qml +++ b/interface/resources/qml/hifi/audio/Audio.qml @@ -266,6 +266,7 @@ Rectangle { labelTextSize: 16; backgroundOnColor: "#E3E3E3"; checked: AudioScriptingInterface.warnWhenMuted; + visible: bar.currentIndex !== 0; onClicked: { AudioScriptingInterface.warnWhenMuted = checked; checked = Qt.binding(function() { return AudioScriptingInterface.warnWhenMuted; }); // restore binding @@ -277,8 +278,8 @@ Rectangle { id: audioLevelSwitch height: root.switchHeight; switchWidth: root.switchWidth; - anchors.top: warnMutedSwitch.bottom - anchors.topMargin: 24 + anchors.top: warnMutedSwitch.visible ? warnMutedSwitch.bottom : parent.top + anchors.topMargin: bar.currentIndex === 0 ? 0 : 24 anchors.left: parent.left labelTextOn: qsTr("Audio Level Meter"); labelTextSize: 16; diff --git a/interface/resources/qml/hifi/dialogs/TabletAboutDialog.qml b/interface/resources/qml/hifi/dialogs/TabletAboutDialog.qml index 213540b334..87f307b9bc 100644 --- a/interface/resources/qml/hifi/dialogs/TabletAboutDialog.qml +++ b/interface/resources/qml/hifi/dialogs/TabletAboutDialog.qml @@ -106,7 +106,7 @@ Rectangle { } RalewayRegular { color: "white" - text: "© 2018 High Fidelity. All rights reserved." + text: "© 2012 - 2019 High Fidelity, Inc.. All rights reserved." size: 14 } RalewayRegular { diff --git a/interface/resources/qml/hifi/simplifiedUI/settingsApp/vr/VR.qml b/interface/resources/qml/hifi/simplifiedUI/settingsApp/vr/VR.qml index 5f0fbe49d5..420ee11a05 100644 --- a/interface/resources/qml/hifi/simplifiedUI/settingsApp/vr/VR.qml +++ b/interface/resources/qml/hifi/simplifiedUI/settingsApp/vr/VR.qml @@ -71,7 +71,7 @@ Flickable { ColumnLayout { id: controlsContainer Layout.preferredWidth: parent.width - Layout.topMargin: 24 + Layout.topMargin: 24 spacing: 0 HifiStylesUit.GraphikSemiBold { @@ -154,6 +154,45 @@ Flickable { } } } + + ColumnLayout { + Layout.preferredWidth: parent.width + spacing: 0 + + HifiStylesUit.GraphikSemiBold { + text: "VR Rotation Mode" + Layout.preferredWidth: parent.width + height: paintedHeight + size: 22 + color: simplifiedUI.colors.text.white + } + + ColumnLayout { + width: parent.width + Layout.topMargin: simplifiedUI.margins.settings.settingsGroupTopMargin + spacing: simplifiedUI.margins.settings.spacingBetweenRadiobuttons + + ButtonGroup { id: rotationButtonGroup } + + SimplifiedControls.RadioButton { + text: "Snap Turn" + ButtonGroup.group: rotationButtonGroup + checked: MyAvatar.getSnapTurn() === true + onClicked: { + MyAvatar.setSnapTurn(true); + } + } + + SimplifiedControls.RadioButton { + text: "Smooth Turn" + ButtonGroup.group: rotationButtonGroup + checked: MyAvatar.getSnapTurn() === false + onClicked: { + MyAvatar.setSnapTurn(false); + } + } + } + } ColumnLayout { id: micControlsContainer diff --git a/interface/resources/qml/hifi/simplifiedUI/simplifiedControls/TextField.qml b/interface/resources/qml/hifi/simplifiedUI/simplifiedControls/TextField.qml index fd370be0ec..1dd3a80a52 100644 --- a/interface/resources/qml/hifi/simplifiedUI/simplifiedControls/TextField.qml +++ b/interface/resources/qml/hifi/simplifiedUI/simplifiedControls/TextField.qml @@ -22,6 +22,8 @@ TextField { } property string rightGlyph: "" + property alias bottomBorderVisible: bottomRectangle.visible + property alias backgroundColor: textFieldBackground.color color: simplifiedUI.colors.text.white font.family: "Graphik Medium" @@ -45,7 +47,9 @@ TextField { } } - background: Item { + background: Rectangle { + id: textFieldBackground + color: Qt.rgba(0, 0, 0, 0); anchors.fill: parent Rectangle { diff --git a/interface/resources/qml/hifi/simplifiedUI/topBar/SimplifiedTopBar.qml b/interface/resources/qml/hifi/simplifiedUI/topBar/SimplifiedTopBar.qml index d87431ea9c..5e82804cf6 100644 --- a/interface/resources/qml/hifi/simplifiedUI/topBar/SimplifiedTopBar.qml +++ b/interface/resources/qml/hifi/simplifiedUI/topBar/SimplifiedTopBar.qml @@ -9,6 +9,7 @@ // import QtQuick 2.10 +import hifi.simplifiedUI.simplifiedControls 1.0 as SimplifiedControls import "../simplifiedConstants" as SimplifiedConstants import "../inputDeviceButton" as InputDeviceButton import stylesUit 1.0 as HifiStylesUit @@ -157,7 +158,7 @@ Rectangle { Image { id: avatarButtonImage - source: "./images/defaultAvatar.svg" + source: "../images/defaultAvatar.svg" anchors.centerIn: parent width: 32 height: width @@ -350,6 +351,48 @@ Rectangle { } + TextMetrics { + id: goToTextFieldMetrics + font: goToTextField.font + text: goToTextField.longPlaceholderText + } + + + Item { + id: goToTextFieldContainer + anchors.left: statusButtonContainer.right + anchors.leftMargin: 12 + anchors.right: (hmdButtonContainer.visible ? hmdButtonContainer.left : helpButtonContainer.left) + anchors.rightMargin: 12 + anchors.verticalCenter: parent.verticalCenter + height: parent.height + + SimplifiedControls.TextField { + id: goToTextField + readonly property string shortPlaceholderText: "Jump to..." + readonly property string longPlaceholderText: "Type the name of a location to quickly jump there..." + anchors.centerIn: parent + width: Math.min(parent.width, 600) + height: parent.height - 11 + leftPadding: 8 + rightPadding: 8 + bottomBorderVisible: false + backgroundColor: "#313131" + placeholderText: width - leftPadding - rightPadding < goToTextFieldMetrics.width ? shortPlaceholderText : longPlaceholderText + clip: true + selectByMouse: true + autoScroll: true + onAccepted: { + if (goToTextField.length > 0) { + AddressManager.handleLookupString(goToTextField.text); + goToTextField.text = ""; + parent.forceActiveFocus(); + } + } + } + } + + Item { id: hmdButtonContainer diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index f5ba1bfd32..02cbdb1849 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -60,6 +60,7 @@ #include #include #include +#include #include #include #include @@ -154,7 +155,6 @@ #include #include #include -#include #include #include #include @@ -880,7 +880,6 @@ bool setupEssentials(int& argc, char** argv, bool runningMarkerExisted) { DependencyManager::set(); #endif - DependencyManager::set(); DependencyManager::set(); DependencyManager::set(); DependencyManager::set(); @@ -2005,12 +2004,6 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo connect(ddeTracker.data(), &FaceTracker::muteToggled, this, &Application::faceTrackerMuteToggled); #endif -#ifdef HAVE_IVIEWHMD - auto eyeTracker = DependencyManager::get(); - eyeTracker->init(); - setActiveEyeTracker(); -#endif - // If launched from Steam, let it handle updates const QString HIFI_NO_UPDATER_COMMAND_LINE_KEY = "--no-updater"; bool noUpdater = arguments().indexOf(HIFI_NO_UPDATER_COMMAND_LINE_KEY) != -1; @@ -2754,9 +2747,6 @@ void Application::cleanupBeforeQuit() { // Stop third party processes so that they're not left running in the event of a subsequent shutdown crash. #ifdef HAVE_DDE DependencyManager::get()->setEnabled(false); -#endif -#ifdef HAVE_IVIEWHMD - DependencyManager::get()->setEnabled(false, true); #endif AnimDebugDraw::getInstance().shutdown(); @@ -2831,9 +2821,6 @@ void Application::cleanupBeforeQuit() { #ifdef HAVE_DDE DependencyManager::destroy(); #endif -#ifdef HAVE_IVIEWHMD - DependencyManager::destroy(); -#endif DependencyManager::destroy(); // Must be destroyed before TabletScriptingInterface @@ -2842,7 +2829,7 @@ void Application::cleanupBeforeQuit() { DependencyManager::destroy(); DependencyManager::destroy(); DependencyManager::destroy(); - + DependencyManager::destroy(); _snapshotSoundInjector = nullptr; @@ -4359,14 +4346,14 @@ void Application::keyPressEvent(QKeyEvent* event) { _keyboardMouseDevice->keyReleaseEvent(event); } - bool isMeta = event->modifiers().testFlag(Qt::ControlModifier); + bool isControlOrCommand = event->modifiers().testFlag(Qt::ControlModifier); bool isOption = event->modifiers().testFlag(Qt::AltModifier); switch (event->key()) { case Qt::Key_4: case Qt::Key_5: case Qt::Key_6: case Qt::Key_7: - if (isMeta || isOption) { + if (isControlOrCommand || isOption) { unsigned int index = static_cast(event->key() - Qt::Key_1); auto displayPlugins = PluginManager::getInstance()->getDisplayPlugins(); if (index < displayPlugins.size()) { @@ -4387,7 +4374,8 @@ void Application::keyPressEvent(QKeyEvent* event) { } bool isShifted = event->modifiers().testFlag(Qt::ShiftModifier); - bool isMeta = event->modifiers().testFlag(Qt::ControlModifier); + bool isControlOrCommand = event->modifiers().testFlag(Qt::ControlModifier); + bool isMetaOrMacControl = event->modifiers().testFlag(Qt::MetaModifier); bool isOption = event->modifiers().testFlag(Qt::AltModifier); switch (event->key()) { case Qt::Key_Enter: @@ -4420,7 +4408,7 @@ void Application::keyPressEvent(QKeyEvent* event) { case Qt::Key_5: case Qt::Key_6: case Qt::Key_7: - if (isMeta || isOption) { + if (isControlOrCommand || isOption) { unsigned int index = static_cast(event->key() - Qt::Key_1); auto displayPlugins = PluginManager::getInstance()->getDisplayPlugins(); if (index < displayPlugins.size()) { @@ -4436,7 +4424,7 @@ void Application::keyPressEvent(QKeyEvent* event) { break; case Qt::Key_G: - if (isShifted && isMeta && Menu::getInstance() && Menu::getInstance()->getMenu("Developer")->isVisible()) { + if (isShifted && isControlOrCommand && isOption && isMetaOrMacControl) { static const QString HIFI_FRAMES_FOLDER_VAR = "HIFI_FRAMES_FOLDER"; static const QString GPU_FRAME_FOLDER = QProcessEnvironment::systemEnvironment().contains(HIFI_FRAMES_FOLDER_VAR) ? QProcessEnvironment::systemEnvironment().value(HIFI_FRAMES_FOLDER_VAR) @@ -4449,7 +4437,7 @@ void Application::keyPressEvent(QKeyEvent* event) { } break; case Qt::Key_X: - if (isShifted && isMeta) { + if (isShifted && isControlOrCommand) { auto offscreenUi = getOffscreenUI(); offscreenUi->togglePinned(); //offscreenUi->getSurfaceContext()->engine()->clearComponentCache(); @@ -4459,7 +4447,7 @@ void Application::keyPressEvent(QKeyEvent* event) { break; case Qt::Key_Y: - if (isShifted && isMeta) { + if (isShifted && isControlOrCommand) { getActiveDisplayPlugin()->cycleDebugOutput(); } break; @@ -4472,16 +4460,16 @@ void Application::keyPressEvent(QKeyEvent* event) { break; case Qt::Key_L: - if (isShifted && isMeta) { + if (isShifted && isControlOrCommand) { Menu::getInstance()->triggerOption(MenuOption::Log); - } else if (isMeta) { + } else if (isControlOrCommand) { auto dialogsManager = DependencyManager::get(); dialogsManager->toggleAddressBar(); } break; case Qt::Key_R: - if (isMeta && !event->isAutoRepeat()) { + if (isControlOrCommand && !event->isAutoRepeat()) { DependencyManager::get()->reloadAllScripts(); getOffscreenUI()->clearCache(); } @@ -4492,7 +4480,7 @@ void Application::keyPressEvent(QKeyEvent* event) { break; case Qt::Key_M: - if (isMeta) { + if (isControlOrCommand) { auto audioClient = DependencyManager::get(); audioClient->setMuted(!audioClient->isMuted()); QSharedPointer audioScriptingInterface = qSharedPointerDynamicCast(DependencyManager::get()); @@ -4503,13 +4491,13 @@ void Application::keyPressEvent(QKeyEvent* event) { break; case Qt::Key_S: - if (isShifted && isMeta && !isOption) { + if (isShifted && isControlOrCommand && !isOption) { Menu::getInstance()->triggerOption(MenuOption::SuppressShortTimings); } break; case Qt::Key_Apostrophe: { - if (isMeta) { + if (isControlOrCommand) { auto cursor = Cursor::Manager::instance().getCursor(); auto curIcon = cursor->getIcon(); if (curIcon == Cursor::Icon::DEFAULT) { @@ -4536,7 +4524,7 @@ void Application::keyPressEvent(QKeyEvent* event) { break; case Qt::Key_Plus: { - if (isMeta && event->modifiers().testFlag(Qt::KeypadModifier)) { + if (isControlOrCommand && event->modifiers().testFlag(Qt::KeypadModifier)) { auto& cursorManager = Cursor::Manager::instance(); cursorManager.setScale(cursorManager.getScale() * 1.1f); } else { @@ -4546,7 +4534,7 @@ void Application::keyPressEvent(QKeyEvent* event) { } case Qt::Key_Minus: { - if (isMeta && event->modifiers().testFlag(Qt::KeypadModifier)) { + if (isControlOrCommand && event->modifiers().testFlag(Qt::KeypadModifier)) { auto& cursorManager = Cursor::Manager::instance(); cursorManager.setScale(cursorManager.getScale() / 1.1f); } else { @@ -5340,35 +5328,6 @@ void Application::setActiveFaceTracker() const { #endif } -#ifdef HAVE_IVIEWHMD -void Application::setActiveEyeTracker() { - auto eyeTracker = DependencyManager::get(); - if (!eyeTracker->isInitialized()) { - return; - } - - bool isEyeTracking = Menu::getInstance()->isOptionChecked(MenuOption::SMIEyeTracking); - bool isSimulating = Menu::getInstance()->isOptionChecked(MenuOption::SimulateEyeTracking); - eyeTracker->setEnabled(isEyeTracking, isSimulating); - - Menu::getInstance()->getActionForOption(MenuOption::OnePointCalibration)->setEnabled(isEyeTracking && !isSimulating); - Menu::getInstance()->getActionForOption(MenuOption::ThreePointCalibration)->setEnabled(isEyeTracking && !isSimulating); - Menu::getInstance()->getActionForOption(MenuOption::FivePointCalibration)->setEnabled(isEyeTracking && !isSimulating); -} - -void Application::calibrateEyeTracker1Point() { - DependencyManager::get()->calibrate(1); -} - -void Application::calibrateEyeTracker3Points() { - DependencyManager::get()->calibrate(3); -} - -void Application::calibrateEyeTracker5Points() { - DependencyManager::get()->calibrate(5); -} -#endif - bool Application::exportEntities(const QString& filename, const QVector& entityIDs, const glm::vec3* givenOffset) { @@ -5842,8 +5801,8 @@ void Application::pushPostUpdateLambda(void* key, const std::function& f _postUpdateLambdas[key] = func; } -// Called during Application::update immediately before AvatarManager::updateMyAvatar, updating my data that is then sent to everyone. -// (Maybe this code should be moved there?) +// Called during Application::update immediately before AvatarManager::updateMyAvatar, updating my data that is then sent +// to everyone. // The principal result is to call updateLookAtTargetAvatar() and then setLookAtPosition(). // Note that it is called BEFORE we update position or joints based on sensors, etc. void Application::updateMyAvatarLookAtPosition() { @@ -5852,91 +5811,8 @@ void Application::updateMyAvatarLookAtPosition() { PerformanceWarning warn(showWarnings, "Application::updateMyAvatarLookAtPosition()"); auto myAvatar = getMyAvatar(); - myAvatar->updateLookAtTargetAvatar(); FaceTracker* faceTracker = getActiveFaceTracker(); - auto eyeTracker = DependencyManager::get(); - - bool isLookingAtSomeone = false; - bool isHMD = qApp->isHMDMode(); - glm::vec3 lookAtSpot; - if (eyeTracker->isTracking() && (isHMD || eyeTracker->isSimulating())) { - // Look at the point that the user is looking at. - glm::vec3 lookAtPosition = eyeTracker->getLookAtPosition(); - if (_myCamera.getMode() == CAMERA_MODE_MIRROR) { - lookAtPosition.x = -lookAtPosition.x; - } - if (isHMD) { - // TODO -- this code is probably wrong, getHeadPose() returns something in sensor frame, not avatar - glm::mat4 headPose = getActiveDisplayPlugin()->getHeadPose(); - glm::quat hmdRotation = glm::quat_cast(headPose); - lookAtSpot = _myCamera.getPosition() + myAvatar->getWorldOrientation() * (hmdRotation * lookAtPosition); - } else { - lookAtSpot = myAvatar->getHead()->getEyePosition() - + (myAvatar->getHead()->getFinalOrientationInWorldFrame() * lookAtPosition); - } - } else { - AvatarSharedPointer lookingAt = myAvatar->getLookAtTargetAvatar().lock(); - bool haveLookAtCandidate = lookingAt && myAvatar.get() != lookingAt.get(); - auto avatar = static_pointer_cast(lookingAt); - bool mutualLookAtSnappingEnabled = avatar && avatar->getLookAtSnappingEnabled() && myAvatar->getLookAtSnappingEnabled(); - if (haveLookAtCandidate && mutualLookAtSnappingEnabled) { - // If I am looking at someone else, look directly at one of their eyes - isLookingAtSomeone = true; - auto lookingAtHead = avatar->getHead(); - - const float MAXIMUM_FACE_ANGLE = 65.0f * RADIANS_PER_DEGREE; - glm::vec3 lookingAtFaceOrientation = lookingAtHead->getFinalOrientationInWorldFrame() * IDENTITY_FORWARD; - glm::vec3 fromLookingAtToMe = glm::normalize(myAvatar->getHead()->getEyePosition() - - lookingAtHead->getEyePosition()); - float faceAngle = glm::angle(lookingAtFaceOrientation, fromLookingAtToMe); - - if (faceAngle < MAXIMUM_FACE_ANGLE) { - // Randomly look back and forth between look targets - eyeContactTarget target = Menu::getInstance()->isOptionChecked(MenuOption::FixGaze) ? - LEFT_EYE : myAvatar->getEyeContactTarget(); - switch (target) { - case LEFT_EYE: - lookAtSpot = lookingAtHead->getLeftEyePosition(); - break; - case RIGHT_EYE: - lookAtSpot = lookingAtHead->getRightEyePosition(); - break; - case MOUTH: - lookAtSpot = lookingAtHead->getMouthPosition(); - break; - } - } else { - // Just look at their head (mid point between eyes) - lookAtSpot = lookingAtHead->getEyePosition(); - } - } else { - // I am not looking at anyone else, so just look forward - auto headPose = myAvatar->getControllerPoseInWorldFrame(controller::Action::HEAD); - if (headPose.isValid()) { - lookAtSpot = transformPoint(headPose.getMatrix(), glm::vec3(0.0f, 0.0f, TREE_SCALE)); - } else { - lookAtSpot = myAvatar->getHead()->getEyePosition() + - (myAvatar->getHead()->getFinalOrientationInWorldFrame() * glm::vec3(0.0f, 0.0f, -TREE_SCALE)); - } - } - - // Deflect the eyes a bit to match the detected gaze from the face tracker if active. - if (faceTracker && !faceTracker->isMuted()) { - float eyePitch = faceTracker->getEstimatedEyePitch(); - float eyeYaw = faceTracker->getEstimatedEyeYaw(); - const float GAZE_DEFLECTION_REDUCTION_DURING_EYE_CONTACT = 0.1f; - glm::vec3 origin = myAvatar->getHead()->getEyePosition(); - float deflection = faceTracker->getEyeDeflection(); - if (isLookingAtSomeone) { - deflection *= GAZE_DEFLECTION_REDUCTION_DURING_EYE_CONTACT; - } - lookAtSpot = origin + _myCamera.getOrientation() * glm::quat(glm::radians(glm::vec3( - eyePitch * deflection, eyeYaw * deflection, 0.0f))) * - glm::inverse(_myCamera.getOrientation()) * (lookAtSpot - origin); - } - } - - myAvatar->getHead()->setLookAtPosition(lookAtSpot); + myAvatar->updateLookAtPosition(faceTracker, _myCamera); } void Application::updateThreads(float deltaTime) { @@ -6513,7 +6389,10 @@ void Application::update(float deltaTime) { controller::Action::LEFT_UP_LEG, controller::Action::RIGHT_UP_LEG, controller::Action::LEFT_TOE_BASE, - controller::Action::RIGHT_TOE_BASE + controller::Action::RIGHT_TOE_BASE, + controller::Action::LEFT_EYE, + controller::Action::RIGHT_EYE + }; // copy controller poses from userInputMapper to myAvatar. @@ -7188,8 +7067,7 @@ void Application::resetSensors(bool andReload) { #ifdef HAVE_DDE DependencyManager::get()->reset(); #endif - - DependencyManager::get()->reset(); + _overlayConductor.centerUI(); getActiveDisplayPlugin()->resetSensors(); getMyAvatar()->reset(true, andReload); diff --git a/interface/src/Application.h b/interface/src/Application.h index cd867598c0..af2348d1e9 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -437,13 +437,6 @@ public slots: void sendWrongProtocolVersionsSignature(bool checked) { ::sendWrongProtocolVersionsSignature(checked); } #endif -#ifdef HAVE_IVIEWHMD - void setActiveEyeTracker(); - void calibrateEyeTracker1Point(); - void calibrateEyeTracker3Points(); - void calibrateEyeTracker5Points(); -#endif - static void showHelp(); void cycleCamera(); diff --git a/interface/src/Menu.cpp b/interface/src/Menu.cpp index 046a1bd892..89fec3c812 100644 --- a/interface/src/Menu.cpp +++ b/interface/src/Menu.cpp @@ -534,32 +534,18 @@ Menu::Menu() { addCheckableActionToQMenuAndActionHash(faceTrackingMenu, MenuOption::AutoMuteAudio, 0, false); #endif -#ifdef HAVE_IVIEWHMD - // Developer > Avatar > Eye Tracking - MenuWrapper* eyeTrackingMenu = avatarDebugMenu->addMenu("Eye Tracking"); - addCheckableActionToQMenuAndActionHash(eyeTrackingMenu, MenuOption::SMIEyeTracking, 0, false, - qApp, SLOT(setActiveEyeTracker())); - { - MenuWrapper* calibrateEyeTrackingMenu = eyeTrackingMenu->addMenu("Calibrate"); - addActionToQMenuAndActionHash(calibrateEyeTrackingMenu, MenuOption::OnePointCalibration, 0, - qApp, SLOT(calibrateEyeTracker1Point())); - addActionToQMenuAndActionHash(calibrateEyeTrackingMenu, MenuOption::ThreePointCalibration, 0, - qApp, SLOT(calibrateEyeTracker3Points())); - addActionToQMenuAndActionHash(calibrateEyeTrackingMenu, MenuOption::FivePointCalibration, 0, - qApp, SLOT(calibrateEyeTracker5Points())); - } - addCheckableActionToQMenuAndActionHash(eyeTrackingMenu, MenuOption::SimulateEyeTracking, 0, false, - qApp, SLOT(setActiveEyeTracker())); -#endif - action = addCheckableActionToQMenuAndActionHash(avatarDebugMenu, MenuOption::AvatarReceiveStats, 0, false); connect(action, &QAction::triggered, [this]{ Avatar::setShowReceiveStats(isOptionChecked(MenuOption::AvatarReceiveStats)); }); action = addCheckableActionToQMenuAndActionHash(avatarDebugMenu, MenuOption::ShowBoundingCollisionShapes, 0, false); connect(action, &QAction::triggered, [this]{ Avatar::setShowCollisionShapes(isOptionChecked(MenuOption::ShowBoundingCollisionShapes)); }); action = addCheckableActionToQMenuAndActionHash(avatarDebugMenu, MenuOption::ShowMyLookAtVectors, 0, false); connect(action, &QAction::triggered, [this]{ Avatar::setShowMyLookAtVectors(isOptionChecked(MenuOption::ShowMyLookAtVectors)); }); + action = addCheckableActionToQMenuAndActionHash(avatarDebugMenu, MenuOption::ShowMyLookAtTarget, 0, false); + connect(action, &QAction::triggered, [this]{ Avatar::setShowMyLookAtTarget(isOptionChecked(MenuOption::ShowMyLookAtTarget)); }); action = addCheckableActionToQMenuAndActionHash(avatarDebugMenu, MenuOption::ShowOtherLookAtVectors, 0, false); connect(action, &QAction::triggered, [this]{ Avatar::setShowOtherLookAtVectors(isOptionChecked(MenuOption::ShowOtherLookAtVectors)); }); + action = addCheckableActionToQMenuAndActionHash(avatarDebugMenu, MenuOption::ShowOtherLookAtTarget, 0, false); + connect(action, &QAction::triggered, [this]{ Avatar::setShowOtherLookAtTarget(isOptionChecked(MenuOption::ShowOtherLookAtTarget)); }); auto avatarManager = DependencyManager::get(); auto avatar = avatarManager->getMyAvatar(); @@ -725,45 +711,51 @@ Menu::Menu() { DependencyManager::get().data(), SLOT(setForceCoarsePicking(bool))); // Developer > Crash >>> - MenuWrapper* crashMenu = developerMenu->addMenu("Crash"); + bool result = false; + const QString HIFI_SHOW_DEVELOPER_CRASH_MENU("HIFI_SHOW_DEVELOPER_CRASH_MENU"); + result = QProcessEnvironment::systemEnvironment().contains(HIFI_SHOW_DEVELOPER_CRASH_MENU); + if (result) { + MenuWrapper* crashMenu = developerMenu->addMenu("Crash"); - // Developer > Crash > Display Crash Options - addCheckableActionToQMenuAndActionHash(crashMenu, MenuOption::DisplayCrashOptions, 0, true); + // Developer > Crash > Display Crash Options + addCheckableActionToQMenuAndActionHash(crashMenu, MenuOption::DisplayCrashOptions, 0, true); - addActionToQMenuAndActionHash(crashMenu, MenuOption::DeadlockInterface, 0, qApp, SLOT(deadlockApplication())); - addActionToQMenuAndActionHash(crashMenu, MenuOption::UnresponsiveInterface, 0, qApp, SLOT(unresponsiveApplication())); + addActionToQMenuAndActionHash(crashMenu, MenuOption::DeadlockInterface, 0, qApp, SLOT(deadlockApplication())); + addActionToQMenuAndActionHash(crashMenu, MenuOption::UnresponsiveInterface, 0, qApp, SLOT(unresponsiveApplication())); - action = addActionToQMenuAndActionHash(crashMenu, MenuOption::CrashPureVirtualFunction); - connect(action, &QAction::triggered, qApp, []() { crash::pureVirtualCall(); }); - action = addActionToQMenuAndActionHash(crashMenu, MenuOption::CrashPureVirtualFunctionThreaded); - connect(action, &QAction::triggered, qApp, []() { std::thread(crash::pureVirtualCall).join(); }); + action = addActionToQMenuAndActionHash(crashMenu, MenuOption::CrashPureVirtualFunction); + connect(action, &QAction::triggered, qApp, []() { crash::pureVirtualCall(); }); + action = addActionToQMenuAndActionHash(crashMenu, MenuOption::CrashPureVirtualFunctionThreaded); + connect(action, &QAction::triggered, qApp, []() { std::thread(crash::pureVirtualCall).join(); }); - action = addActionToQMenuAndActionHash(crashMenu, MenuOption::CrashDoubleFree); - connect(action, &QAction::triggered, qApp, []() { crash::doubleFree(); }); - action = addActionToQMenuAndActionHash(crashMenu, MenuOption::CrashDoubleFreeThreaded); - connect(action, &QAction::triggered, qApp, []() { std::thread(crash::doubleFree).join(); }); + action = addActionToQMenuAndActionHash(crashMenu, MenuOption::CrashDoubleFree); + connect(action, &QAction::triggered, qApp, []() { crash::doubleFree(); }); + action = addActionToQMenuAndActionHash(crashMenu, MenuOption::CrashDoubleFreeThreaded); + connect(action, &QAction::triggered, qApp, []() { std::thread(crash::doubleFree).join(); }); - action = addActionToQMenuAndActionHash(crashMenu, MenuOption::CrashAbort); - connect(action, &QAction::triggered, qApp, []() { crash::doAbort(); }); - action = addActionToQMenuAndActionHash(crashMenu, MenuOption::CrashAbortThreaded); - connect(action, &QAction::triggered, qApp, []() { std::thread(crash::doAbort).join(); }); + action = addActionToQMenuAndActionHash(crashMenu, MenuOption::CrashAbort); + connect(action, &QAction::triggered, qApp, []() { crash::doAbort(); }); + action = addActionToQMenuAndActionHash(crashMenu, MenuOption::CrashAbortThreaded); + connect(action, &QAction::triggered, qApp, []() { std::thread(crash::doAbort).join(); }); - action = addActionToQMenuAndActionHash(crashMenu, MenuOption::CrashNullDereference); - connect(action, &QAction::triggered, qApp, []() { crash::nullDeref(); }); - action = addActionToQMenuAndActionHash(crashMenu, MenuOption::CrashNullDereferenceThreaded); - connect(action, &QAction::triggered, qApp, []() { std::thread(crash::nullDeref).join(); }); + action = addActionToQMenuAndActionHash(crashMenu, MenuOption::CrashNullDereference); + connect(action, &QAction::triggered, qApp, []() { crash::nullDeref(); }); + action = addActionToQMenuAndActionHash(crashMenu, MenuOption::CrashNullDereferenceThreaded); + connect(action, &QAction::triggered, qApp, []() { std::thread(crash::nullDeref).join(); }); - action = addActionToQMenuAndActionHash(crashMenu, MenuOption::CrashOutOfBoundsVectorAccess); - connect(action, &QAction::triggered, qApp, []() { crash::outOfBoundsVectorCrash(); }); - action = addActionToQMenuAndActionHash(crashMenu, MenuOption::CrashOutOfBoundsVectorAccessThreaded); - connect(action, &QAction::triggered, qApp, []() { std::thread(crash::outOfBoundsVectorCrash).join(); }); + action = addActionToQMenuAndActionHash(crashMenu, MenuOption::CrashOutOfBoundsVectorAccess); + connect(action, &QAction::triggered, qApp, []() { crash::outOfBoundsVectorCrash(); }); + action = addActionToQMenuAndActionHash(crashMenu, MenuOption::CrashOutOfBoundsVectorAccessThreaded); + connect(action, &QAction::triggered, qApp, []() { std::thread(crash::outOfBoundsVectorCrash).join(); }); - action = addActionToQMenuAndActionHash(crashMenu, MenuOption::CrashNewFault); - connect(action, &QAction::triggered, qApp, []() { crash::newFault(); }); - action = addActionToQMenuAndActionHash(crashMenu, MenuOption::CrashNewFaultThreaded); - connect(action, &QAction::triggered, qApp, []() { std::thread(crash::newFault).join(); }); + action = addActionToQMenuAndActionHash(crashMenu, MenuOption::CrashNewFault); + connect(action, &QAction::triggered, qApp, []() { crash::newFault(); }); + action = addActionToQMenuAndActionHash(crashMenu, MenuOption::CrashNewFaultThreaded); + connect(action, &QAction::triggered, qApp, []() { std::thread(crash::newFault).join(); }); - addActionToQMenuAndActionHash(crashMenu, MenuOption::CrashOnShutdown, 0, qApp, SLOT(crashOnShutdown())); + addActionToQMenuAndActionHash(crashMenu, MenuOption::CrashOnShutdown, 0, qApp, SLOT(crashOnShutdown())); + } + // Developer > Show Statistics addCheckableActionToQMenuAndActionHash(developerMenu, MenuOption::Stats, 0, true); diff --git a/interface/src/Menu.h b/interface/src/Menu.h index 098e635578..0ba1159052 100644 --- a/interface/src/Menu.h +++ b/interface/src/Menu.h @@ -190,7 +190,9 @@ namespace MenuOption { const QString ShowBoundingCollisionShapes = "Show Bounding Collision Shapes"; const QString ShowDSConnectTable = "Show Domain Connection Timing"; const QString ShowMyLookAtVectors = "Show My Eye Vectors"; + const QString ShowMyLookAtTarget = "Show My Look-At Target"; const QString ShowOtherLookAtVectors = "Show Other Eye Vectors"; + const QString ShowOtherLookAtTarget = "Show Other Look-At Target"; const QString EnableLookAtSnapping = "Enable LookAt Snapping"; const QString ShowRealtimeEntityStats = "Show Realtime Entity Stats"; const QString SimulateEyeTracking = "Simulate"; diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index f20495ef31..dbd4644ef2 100644 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -776,6 +776,18 @@ void MyAvatar::update(float deltaTime) { emit energyChanged(currentEnergy); updateEyeContactTarget(deltaTime); + + // if we're getting eye rotations from a tracker, disable observer-side procedural eye motions + auto userInputMapper = DependencyManager::get(); + bool eyesTracked = + userInputMapper->getPoseState(controller::Action::LEFT_EYE).valid && + userInputMapper->getPoseState(controller::Action::RIGHT_EYE).valid; + + int leftEyeJointIndex = getJointIndex("LeftEye"); + int rightEyeJointIndex = getJointIndex("RightEye"); + bool eyesAreOverridden = getIsJointOverridden(leftEyeJointIndex) || getIsJointOverridden(rightEyeJointIndex); + + _headData->setHasProceduralEyeMovement(!(eyesTracked || eyesAreOverridden)); } void MyAvatar::updateEyeContactTarget(float deltaTime) { @@ -1465,8 +1477,50 @@ void MyAvatar::setEnableDebugDrawHandControllers(bool isEnabled) { _enableDebugDrawHandControllers = isEnabled; if (!isEnabled) { - DebugDraw::getInstance().removeMarker("leftHandController"); - DebugDraw::getInstance().removeMarker("rightHandController"); + DebugDraw::getInstance().removeMarker("LEFT_HAND"); + DebugDraw::getInstance().removeMarker("RIGHT_HAND"); + + DebugDraw::getInstance().removeMarker("LEFT_HAND_THUMB1"); + DebugDraw::getInstance().removeMarker("LEFT_HAND_THUMB2"); + DebugDraw::getInstance().removeMarker("LEFT_HAND_THUMB3"); + DebugDraw::getInstance().removeMarker("LEFT_HAND_THUMB4"); + DebugDraw::getInstance().removeMarker("LEFT_HAND_INDEX1"); + DebugDraw::getInstance().removeMarker("LEFT_HAND_INDEX2"); + DebugDraw::getInstance().removeMarker("LEFT_HAND_INDEX3"); + DebugDraw::getInstance().removeMarker("LEFT_HAND_INDEX4"); + DebugDraw::getInstance().removeMarker("LEFT_HAND_MIDDLE1"); + DebugDraw::getInstance().removeMarker("LEFT_HAND_MIDDLE2"); + DebugDraw::getInstance().removeMarker("LEFT_HAND_MIDDLE3"); + DebugDraw::getInstance().removeMarker("LEFT_HAND_MIDDLE4"); + DebugDraw::getInstance().removeMarker("LEFT_HAND_RING1"); + DebugDraw::getInstance().removeMarker("LEFT_HAND_RING2"); + DebugDraw::getInstance().removeMarker("LEFT_HAND_RING3"); + DebugDraw::getInstance().removeMarker("LEFT_HAND_RING4"); + DebugDraw::getInstance().removeMarker("LEFT_HAND_PINKY1"); + DebugDraw::getInstance().removeMarker("LEFT_HAND_PINKY2"); + DebugDraw::getInstance().removeMarker("LEFT_HAND_PINKY3"); + DebugDraw::getInstance().removeMarker("LEFT_HAND_PINKY4"); + + DebugDraw::getInstance().removeMarker("RIGHT_HAND_THUMB1"); + DebugDraw::getInstance().removeMarker("RIGHT_HAND_THUMB2"); + DebugDraw::getInstance().removeMarker("RIGHT_HAND_THUMB3"); + DebugDraw::getInstance().removeMarker("RIGHT_HAND_THUMB4"); + DebugDraw::getInstance().removeMarker("RIGHT_HAND_INDEX1"); + DebugDraw::getInstance().removeMarker("RIGHT_HAND_INDEX2"); + DebugDraw::getInstance().removeMarker("RIGHT_HAND_INDEX3"); + DebugDraw::getInstance().removeMarker("RIGHT_HAND_INDEX4"); + DebugDraw::getInstance().removeMarker("RIGHT_HAND_MIDDLE1"); + DebugDraw::getInstance().removeMarker("RIGHT_HAND_MIDDLE2"); + DebugDraw::getInstance().removeMarker("RIGHT_HAND_MIDDLE3"); + DebugDraw::getInstance().removeMarker("RIGHT_HAND_MIDDLE4"); + DebugDraw::getInstance().removeMarker("RIGHT_HAND_RING1"); + DebugDraw::getInstance().removeMarker("RIGHT_HAND_RING2"); + DebugDraw::getInstance().removeMarker("RIGHT_HAND_RING3"); + DebugDraw::getInstance().removeMarker("RIGHT_HAND_RING4"); + DebugDraw::getInstance().removeMarker("RIGHT_HAND_PINKY1"); + DebugDraw::getInstance().removeMarker("RIGHT_HAND_PINKY2"); + DebugDraw::getInstance().removeMarker("RIGHT_HAND_PINKY3"); + DebugDraw::getInstance().removeMarker("RIGHT_HAND_PINKY4"); } } @@ -3107,6 +3161,16 @@ void MyAvatar::animGraphLoaded() { disconnect(&(_skeletonModel->getRig()), SIGNAL(onLoadComplete()), this, SLOT(animGraphLoaded())); } +void MyAvatar::debugDrawPose(controller::Action action, const char* channelName, float size) { + auto pose = getControllerPoseInWorldFrame(action); + if (pose.isValid()) { + DebugDraw::getInstance().addMarker(channelName, pose.getRotation(), pose.getTranslation(), glm::vec4(1), size); + } else { + DebugDraw::getInstance().removeMarker(channelName); + } +} + + void MyAvatar::postUpdate(float deltaTime, const render::ScenePointer& scene) { Avatar::postUpdate(deltaTime, scene); @@ -3147,20 +3211,50 @@ void MyAvatar::postUpdate(float deltaTime, const render::ScenePointer& scene) { } if (_enableDebugDrawHandControllers) { - auto leftHandPose = getControllerPoseInWorldFrame(controller::Action::LEFT_HAND); - auto rightHandPose = getControllerPoseInWorldFrame(controller::Action::RIGHT_HAND); + debugDrawPose(controller::Action::LEFT_HAND, "LEFT_HAND", 1.0); + debugDrawPose(controller::Action::RIGHT_HAND, "RIGHT_HAND", 1.0); - if (leftHandPose.isValid()) { - DebugDraw::getInstance().addMarker("leftHandController", leftHandPose.getRotation(), leftHandPose.getTranslation(), glm::vec4(1)); - } else { - DebugDraw::getInstance().removeMarker("leftHandController"); - } + debugDrawPose(controller::Action::LEFT_HAND_THUMB1, "LEFT_HAND_THUMB1", 0.1f); + debugDrawPose(controller::Action::LEFT_HAND_THUMB2, "LEFT_HAND_THUMB2", 0.1f); + debugDrawPose(controller::Action::LEFT_HAND_THUMB3, "LEFT_HAND_THUMB3", 0.1f); + debugDrawPose(controller::Action::LEFT_HAND_THUMB4, "LEFT_HAND_THUMB4", 0.1f); + debugDrawPose(controller::Action::LEFT_HAND_INDEX1, "LEFT_HAND_INDEX1", 0.1f); + debugDrawPose(controller::Action::LEFT_HAND_INDEX2, "LEFT_HAND_INDEX2", 0.1f); + debugDrawPose(controller::Action::LEFT_HAND_INDEX3, "LEFT_HAND_INDEX3", 0.1f); + debugDrawPose(controller::Action::LEFT_HAND_INDEX4, "LEFT_HAND_INDEX4", 0.1f); + debugDrawPose(controller::Action::LEFT_HAND_MIDDLE1, "LEFT_HAND_MIDDLE1", 0.1f); + debugDrawPose(controller::Action::LEFT_HAND_MIDDLE2, "LEFT_HAND_MIDDLE2", 0.1f); + debugDrawPose(controller::Action::LEFT_HAND_MIDDLE3, "LEFT_HAND_MIDDLE3", 0.1f); + debugDrawPose(controller::Action::LEFT_HAND_MIDDLE4, "LEFT_HAND_MIDDLE4", 0.1f); + debugDrawPose(controller::Action::LEFT_HAND_RING1, "LEFT_HAND_RING1", 0.1f); + debugDrawPose(controller::Action::LEFT_HAND_RING2, "LEFT_HAND_RING2", 0.1f); + debugDrawPose(controller::Action::LEFT_HAND_RING3, "LEFT_HAND_RING3", 0.1f); + debugDrawPose(controller::Action::LEFT_HAND_RING4, "LEFT_HAND_RING4", 0.1f); + debugDrawPose(controller::Action::LEFT_HAND_PINKY1, "LEFT_HAND_PINKY1", 0.1f); + debugDrawPose(controller::Action::LEFT_HAND_PINKY2, "LEFT_HAND_PINKY2", 0.1f); + debugDrawPose(controller::Action::LEFT_HAND_PINKY3, "LEFT_HAND_PINKY3", 0.1f); + debugDrawPose(controller::Action::LEFT_HAND_PINKY4, "LEFT_HAND_PINKY4", 0.1f); - if (rightHandPose.isValid()) { - DebugDraw::getInstance().addMarker("rightHandController", rightHandPose.getRotation(), rightHandPose.getTranslation(), glm::vec4(1)); - } else { - DebugDraw::getInstance().removeMarker("rightHandController"); - } + debugDrawPose(controller::Action::RIGHT_HAND_THUMB1, "RIGHT_HAND_THUMB1", 0.1f); + debugDrawPose(controller::Action::RIGHT_HAND_THUMB2, "RIGHT_HAND_THUMB2", 0.1f); + debugDrawPose(controller::Action::RIGHT_HAND_THUMB3, "RIGHT_HAND_THUMB3", 0.1f); + debugDrawPose(controller::Action::RIGHT_HAND_THUMB4, "RIGHT_HAND_THUMB4", 0.1f); + debugDrawPose(controller::Action::RIGHT_HAND_INDEX1, "RIGHT_HAND_INDEX1", 0.1f); + debugDrawPose(controller::Action::RIGHT_HAND_INDEX2, "RIGHT_HAND_INDEX2", 0.1f); + debugDrawPose(controller::Action::RIGHT_HAND_INDEX3, "RIGHT_HAND_INDEX3", 0.1f); + debugDrawPose(controller::Action::RIGHT_HAND_INDEX4, "RIGHT_HAND_INDEX4", 0.1f); + debugDrawPose(controller::Action::RIGHT_HAND_MIDDLE1, "RIGHT_HAND_MIDDLE1", 0.1f); + debugDrawPose(controller::Action::RIGHT_HAND_MIDDLE2, "RIGHT_HAND_MIDDLE2", 0.1f); + debugDrawPose(controller::Action::RIGHT_HAND_MIDDLE3, "RIGHT_HAND_MIDDLE3", 0.1f); + debugDrawPose(controller::Action::RIGHT_HAND_MIDDLE4, "RIGHT_HAND_MIDDLE4", 0.1f); + debugDrawPose(controller::Action::RIGHT_HAND_RING1, "RIGHT_HAND_RING1", 0.1f); + debugDrawPose(controller::Action::RIGHT_HAND_RING2, "RIGHT_HAND_RING2", 0.1f); + debugDrawPose(controller::Action::RIGHT_HAND_RING3, "RIGHT_HAND_RING3", 0.1f); + debugDrawPose(controller::Action::RIGHT_HAND_RING4, "RIGHT_HAND_RING4", 0.1f); + debugDrawPose(controller::Action::RIGHT_HAND_PINKY1, "RIGHT_HAND_PINKY1", 0.1f); + debugDrawPose(controller::Action::RIGHT_HAND_PINKY2, "RIGHT_HAND_PINKY2", 0.1f); + debugDrawPose(controller::Action::RIGHT_HAND_PINKY3, "RIGHT_HAND_PINKY3", 0.1f); + debugDrawPose(controller::Action::RIGHT_HAND_PINKY4, "RIGHT_HAND_PINKY4", 0.1f); } DebugDraw::getInstance().updateMyAvatarPos(getWorldPosition()); @@ -4513,8 +4607,15 @@ float MyAvatar::getRawDriveKey(DriveKeys key) const { } void MyAvatar::relayDriveKeysToCharacterController() { - if (getDriveKey(TRANSLATE_Y) > 0.0f && (!qApp->isHMDMode() || (useAdvancedMovementControls() && getFlyingHMDPref()))) { - _characterController.jump(); + if (_endSitKeyPressComplete) { + if (getDriveKey(TRANSLATE_Y) > 0.0f && (!qApp->isHMDMode() || (useAdvancedMovementControls() && getFlyingHMDPref()))) { + _characterController.jump(); + } + } else { + // used to prevent character from jumping after endSit is called. + if (getDriveKey(TRANSLATE_Y) == 0.0f) { + _endSitKeyPressComplete = true; + } } } @@ -6397,15 +6498,17 @@ void MyAvatar::beginSit(const glm::vec3& position, const glm::quat& rotation) { return; } - _characterController.setSeated(true); - setCollisionsEnabled(false); - setHMDLeanRecenterEnabled(false); - // Disable movement - setSitDriveKeysStatus(false); - centerBody(); - int hipIndex = getJointIndex("Hips"); - clearPinOnJoint(hipIndex); - pinJoint(hipIndex, position, rotation); + if (!_characterController.getSeated()) { + _characterController.setSeated(true); + setCollisionsEnabled(false); + setHMDLeanRecenterEnabled(false); + // Disable movement + setSitDriveKeysStatus(false); + centerBody(); + int hipIndex = getJointIndex("Hips"); + clearPinOnJoint(hipIndex); + pinJoint(hipIndex, position, rotation); + } } void MyAvatar::endSit(const glm::vec3& position, const glm::quat& rotation) { @@ -6423,16 +6526,135 @@ void MyAvatar::endSit(const glm::vec3& position, const glm::quat& rotation) { slamPosition(position); setWorldOrientation(rotation); - // the jump key is used to exit the chair. We add a delay here to prevent - // the avatar from jumping right as they exit the chair. - float TIME_BEFORE_DRIVE_ENABLED_MS = 150.0f; - QTimer::singleShot(TIME_BEFORE_DRIVE_ENABLED_MS, [this]() { - // Enable movement again - setSitDriveKeysStatus(true); - }); + // used to prevent character from jumping after endSit is called. + _endSitKeyPressComplete = false; + + setSitDriveKeysStatus(true); } } +bool MyAvatar::getIsJointOverridden(int jointIndex) const { + // has this joint been set by a script? + return _skeletonModel->getIsJointOverridden(jointIndex); +} + +void MyAvatar::updateLookAtPosition(FaceTracker* faceTracker, Camera& myCamera) { + + updateLookAtTargetAvatar(); + + bool isLookingAtSomeone = false; + glm::vec3 lookAtSpot; + + const MyHead* myHead = getMyHead(); + + int leftEyeJointIndex = getJointIndex("LeftEye"); + int rightEyeJointIndex = getJointIndex("RightEye"); + bool eyesAreOverridden = getIsJointOverridden(leftEyeJointIndex) || + getIsJointOverridden(rightEyeJointIndex); + if (eyesAreOverridden) { + // A script has set the eye rotations, so use these to set lookAtSpot + glm::quat leftEyeRotation = getAbsoluteJointRotationInObjectFrame(leftEyeJointIndex); + glm::quat rightEyeRotation = getAbsoluteJointRotationInObjectFrame(rightEyeJointIndex); + glm::vec3 leftVec = getWorldOrientation() * leftEyeRotation * IDENTITY_FORWARD; + glm::vec3 rightVec = getWorldOrientation() * rightEyeRotation * IDENTITY_FORWARD; + glm::vec3 leftEyePosition = myHead->getLeftEyePosition(); + glm::vec3 rightEyePosition = myHead->getRightEyePosition(); + float t1, t2; + bool success = findClosestApproachOfLines(leftEyePosition, leftVec, rightEyePosition, rightVec, t1, t2); + if (success) { + glm::vec3 leftFocus = leftEyePosition + leftVec * t1; + glm::vec3 rightFocus = rightEyePosition + rightVec * t2; + lookAtSpot = (leftFocus + rightFocus) / 2.0f; // average + } else { + lookAtSpot = myHead->getEyePosition() + glm::normalize(leftVec) * 1000.0f; + } + } else { + controller::Pose leftEyePose = getControllerPoseInAvatarFrame(controller::Action::LEFT_EYE); + controller::Pose rightEyePose = getControllerPoseInAvatarFrame(controller::Action::RIGHT_EYE); + if (leftEyePose.isValid() && rightEyePose.isValid()) { + // an eye tracker is in use, set lookAtSpot from this + glm::vec3 leftVec = getWorldOrientation() * leftEyePose.rotation * glm::vec3(0.0f, 0.0f, -1.0f); + glm::vec3 rightVec = getWorldOrientation() * rightEyePose.rotation * glm::vec3(0.0f, 0.0f, -1.0f); + + glm::vec3 leftEyePosition = myHead->getLeftEyePosition(); + glm::vec3 rightEyePosition = myHead->getRightEyePosition(); + float t1, t2; + bool success = findClosestApproachOfLines(leftEyePosition, leftVec, rightEyePosition, rightVec, t1, t2); + if (success) { + glm::vec3 leftFocus = leftEyePosition + leftVec * t1; + glm::vec3 rightFocus = rightEyePosition + rightVec * t2; + lookAtSpot = (leftFocus + rightFocus) / 2.0f; // average + } else { + lookAtSpot = myHead->getEyePosition() + glm::normalize(leftVec) * 1000.0f; + } + } else { + // no script override, no eye tracker, so do procedural eye motion + AvatarSharedPointer lookingAt = getLookAtTargetAvatar().lock(); + bool haveLookAtCandidate = lookingAt && this != lookingAt.get(); + auto avatar = static_pointer_cast(lookingAt); + bool mutualLookAtSnappingEnabled = + avatar && avatar->getLookAtSnappingEnabled() && getLookAtSnappingEnabled(); + if (haveLookAtCandidate && mutualLookAtSnappingEnabled) { + // If I am looking at someone else, look directly at one of their eyes + isLookingAtSomeone = true; + auto lookingAtHead = avatar->getHead(); + + const float MAXIMUM_FACE_ANGLE = 65.0f * RADIANS_PER_DEGREE; + glm::vec3 lookingAtFaceOrientation = lookingAtHead->getFinalOrientationInWorldFrame() * IDENTITY_FORWARD; + glm::vec3 fromLookingAtToMe = glm::normalize(getHead()->getEyePosition() + - lookingAtHead->getEyePosition()); + float faceAngle = glm::angle(lookingAtFaceOrientation, fromLookingAtToMe); + + if (faceAngle < MAXIMUM_FACE_ANGLE) { + // Randomly look back and forth between look targets + eyeContactTarget target = Menu::getInstance()->isOptionChecked(MenuOption::FixGaze) ? + LEFT_EYE : getEyeContactTarget(); + switch (target) { + case LEFT_EYE: + lookAtSpot = lookingAtHead->getLeftEyePosition(); + break; + case RIGHT_EYE: + lookAtSpot = lookingAtHead->getRightEyePosition(); + break; + case MOUTH: + lookAtSpot = lookingAtHead->getMouthPosition(); + break; + } + } else { + // Just look at their head (mid point between eyes) + lookAtSpot = lookingAtHead->getEyePosition(); + } + } else { + // I am not looking at anyone else, so just look forward + auto headPose = getControllerPoseInWorldFrame(controller::Action::HEAD); + if (headPose.isValid()) { + lookAtSpot = transformPoint(headPose.getMatrix(), glm::vec3(0.0f, 0.0f, TREE_SCALE)); + } else { + lookAtSpot = myHead->getEyePosition() + + (getHead()->getFinalOrientationInWorldFrame() * glm::vec3(0.0f, 0.0f, -TREE_SCALE)); + } + } + + // Deflect the eyes a bit to match the detected gaze from the face tracker if active. + if (faceTracker && !faceTracker->isMuted()) { + float eyePitch = faceTracker->getEstimatedEyePitch(); + float eyeYaw = faceTracker->getEstimatedEyeYaw(); + const float GAZE_DEFLECTION_REDUCTION_DURING_EYE_CONTACT = 0.1f; + glm::vec3 origin = myHead->getEyePosition(); + float deflection = faceTracker->getEyeDeflection(); + if (isLookingAtSomeone) { + deflection *= GAZE_DEFLECTION_REDUCTION_DURING_EYE_CONTACT; + } + lookAtSpot = origin + myCamera.getOrientation() * glm::quat(glm::radians(glm::vec3( + eyePitch * deflection, eyeYaw * deflection, 0.0f))) * + glm::inverse(myCamera.getOrientation()) * (lookAtSpot - origin); + } + } + } + + getHead()->setLookAtPosition(lookAtSpot); +} + void MyAvatar::resetHeadLookAt() { if (_skeletonModelLoaded) { _skeletonModel->getRig().setDirectionalBlending(HEAD_BLENDING_NAME, glm::vec3(), diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index 0a74f9a16d..0adb0f5bf7 100644 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -29,10 +29,12 @@ #include #include #include +#include #include "AtRestDetector.h" #include "MyCharacterController.h" #include "RingBufferHistory.h" +#include "devices/DdeFaceTracker.h" class AvatarActionHold; class ModelItemID; @@ -1880,6 +1882,8 @@ public: bool getFlowActive() const; bool getNetworkGraphActive() const; + void updateLookAtPosition(FaceTracker* faceTracker, Camera& myCamera); + // sets the reaction enabled and triggered parameters of the passed in params // also clears internal reaction triggers void updateRigControllerParameters(Rig::ControllerParameters& params); @@ -1887,6 +1891,10 @@ public: // Don't substitute verify-fail: virtual const QUrl& getSkeletonModelURL() const override { return _skeletonModelURL; } + void debugDrawPose(controller::Action action, const char* channelName, float size); + + bool getIsJointOverridden(int jointIndex) const; + public slots: /**jsdoc @@ -2936,6 +2944,9 @@ private: int _reactionEnabledRefCounts[NUM_AVATAR_BEGIN_END_REACTIONS] { 0, 0, 0 }; mutable std::mutex _reactionLock; + + // used to prevent character from jumping after endSit is called. + bool _endSitKeyPressComplete { false }; }; QScriptValue audioListenModeToScriptValue(QScriptEngine* engine, const AudioListenerMode& audioListenerMode); diff --git a/interface/src/avatar/MyHead.cpp b/interface/src/avatar/MyHead.cpp index 9b05a26c76..e5c8b71ea2 100644 --- a/interface/src/avatar/MyHead.cpp +++ b/interface/src/avatar/MyHead.cpp @@ -15,7 +15,7 @@ #include #include #include -#include +#include #include "devices/DdeFaceTracker.h" #include "Application.h" @@ -46,18 +46,37 @@ void MyHead::simulate(float deltaTime) { auto player = DependencyManager::get(); // Only use face trackers when not playing back a recording. if (!player->isPlaying()) { - auto faceTracker = qApp->getActiveFaceTracker(); - const bool hasActualFaceTrackerConnected = faceTracker && !faceTracker->isMuted(); - _isFaceTrackerConnected = hasActualFaceTrackerConnected || _owningAvatar->getHasScriptedBlendshapes(); - if (_isFaceTrackerConnected) { - if (hasActualFaceTrackerConnected) { - _blendshapeCoefficients = faceTracker->getBlendshapeCoefficients(); - } - } + // TODO -- finish removing face-tracker specific code. To do this, add input channels for + // each blendshape-coefficient and update the various json files to relay them in a useful way. + // After that, input plugins can be used to drive the avatar's face, and the various "DDE" files + // can be ported into the plugin and removed. + // + // auto faceTracker = qApp->getActiveFaceTracker(); + // const bool hasActualFaceTrackerConnected = faceTracker && !faceTracker->isMuted(); + // _isFaceTrackerConnected = hasActualFaceTrackerConnected || _owningAvatar->getHasScriptedBlendshapes(); + // if (_isFaceTrackerConnected) { + // if (hasActualFaceTrackerConnected) { + // _blendshapeCoefficients = faceTracker->getBlendshapeCoefficients(); + // } + // } - auto eyeTracker = DependencyManager::get(); - _isEyeTrackerConnected = eyeTracker->isTracking(); - // if eye tracker is connected we should get the data here. + auto userInputMapper = DependencyManager::get(); + bool eyeLidsTracked = + userInputMapper->getActionStateValid(controller::Action::LEFT_EYE_BLINK) && + userInputMapper->getActionStateValid(controller::Action::RIGHT_EYE_BLINK); + setFaceTrackerConnected(eyeLidsTracked); + if (eyeLidsTracked) { + float leftEyeBlink = userInputMapper->getActionState(controller::Action::LEFT_EYE_BLINK); + float rightEyeBlink = userInputMapper->getActionState(controller::Action::RIGHT_EYE_BLINK); + _blendshapeCoefficients.resize(std::max(_blendshapeCoefficients.size(), 2)); + _blendshapeCoefficients[EYE_BLINK_INDICES[0]] = leftEyeBlink; + _blendshapeCoefficients[EYE_BLINK_INDICES[1]] = rightEyeBlink; + } else { + const float FULLY_OPEN = 0.0f; + _blendshapeCoefficients.resize(std::max(_blendshapeCoefficients.size(), 2)); + _blendshapeCoefficients[EYE_BLINK_INDICES[0]] = FULLY_OPEN; + _blendshapeCoefficients[EYE_BLINK_INDICES[1]] = FULLY_OPEN; + } } Parent::simulate(deltaTime); } diff --git a/interface/src/avatar/MySkeletonModel.cpp b/interface/src/avatar/MySkeletonModel.cpp index 0229eeee76..38065c8095 100755 --- a/interface/src/avatar/MySkeletonModel.cpp +++ b/interface/src/avatar/MySkeletonModel.cpp @@ -114,13 +114,12 @@ void MySkeletonModel::updateRig(float deltaTime, glm::mat4 parentTransform) { Head* head = _owningAvatar->getHead(); - // make sure lookAt is not too close to face (avoid crosseyes) - glm::vec3 lookAt = head->getLookAtPosition(); - glm::vec3 focusOffset = lookAt - _owningAvatar->getHead()->getEyePosition(); - float focusDistance = glm::length(focusOffset); - const float MIN_LOOK_AT_FOCUS_DISTANCE = 1.0f; - if (focusDistance < MIN_LOOK_AT_FOCUS_DISTANCE && focusDistance > EPSILON) { - lookAt = _owningAvatar->getHead()->getEyePosition() + (MIN_LOOK_AT_FOCUS_DISTANCE / focusDistance) * focusOffset; + bool eyePosesValid = !head->getHasProceduralEyeMovement(); + glm::vec3 lookAt; + if (eyePosesValid) { + lookAt = head->getLookAtPosition(); // don't apply no-crosseyes code when eyes are being tracked + } else { + lookAt = avoidCrossedEyes(head->getLookAtPosition()); } MyAvatar* myAvatar = static_cast(_owningAvatar); diff --git a/libraries/animation/src/AnimUtil.cpp b/libraries/animation/src/AnimUtil.cpp index 8830cb78b1..0a8b2fe286 100644 --- a/libraries/animation/src/AnimUtil.cpp +++ b/libraries/animation/src/AnimUtil.cpp @@ -54,12 +54,14 @@ void blend4(size_t numPoses, const AnimPose* a, const AnimPose* b, const AnimPos // additive blend void blendAdd(size_t numPoses, const AnimPose* a, const AnimPose* b, float alpha, AnimPose* result) { - const glm::quat identity = glm::quat(); + const glm::vec3 IDENTITY_SCALE = glm::vec3(1.0f); + const glm::quat IDENTITY_ROT = glm::quat(); + for (size_t i = 0; i < numPoses; i++) { const AnimPose& aPose = a[i]; const AnimPose& bPose = b[i]; - result[i].scale() = lerp(aPose.scale(), bPose.scale(), alpha); + result[i].scale() = aPose.scale() * lerp(IDENTITY_SCALE, bPose.scale(), alpha); // ensure that delta has the same "polarity" as the identity quat. // we don't need to do a full dot product, just sign of w is sufficient. @@ -67,7 +69,7 @@ void blendAdd(size_t numPoses, const AnimPose* a, const AnimPose* b, float alpha if (delta.w < 0.0f) { delta = -delta; } - delta = glm::lerp(identity, delta, alpha); + delta = glm::lerp(IDENTITY_ROT, delta, alpha); result[i].rot() = glm::normalize(aPose.rot() * delta); result[i].trans() = aPose.trans() + (alpha * bPose.trans()); } diff --git a/libraries/animation/src/Rig.cpp b/libraries/animation/src/Rig.cpp index 6dfcfc6994..d63c2d6a9b 100644 --- a/libraries/animation/src/Rig.cpp +++ b/libraries/animation/src/Rig.cpp @@ -715,7 +715,7 @@ void Rig::reset(const HFMModel& hfmModel) { } } -bool Rig::jointStatesEmpty() { +bool Rig::jointStatesEmpty() const { return _internalPoseSet._relativePoses.empty(); } @@ -878,6 +878,20 @@ void Rig::setJointRotation(int index, bool valid, const glm::quat& rotation, flo } } +bool Rig::getIsJointOverridden(int jointIndex) const { + if (QThread::currentThread() == thread()) { + if (isIndexValid(jointIndex)) { + return _internalPoseSet._overrideFlags[jointIndex]; + } + } else { + QReadLocker readLock(&_externalPoseSetLock); + if (jointIndex >= 0 && jointIndex < (int)_externalPoseSet._overrideFlags.size()) { + return _externalPoseSet._overrideFlags[jointIndex]; + } + } + return false; +} + bool Rig::getJointPositionInWorldFrame(int jointIndex, glm::vec3& position, glm::vec3 translation, glm::quat rotation) const { bool success { false }; glm::vec3 originalPosition = position; @@ -1958,8 +1972,7 @@ void Rig::updateReactions(const ControllerParameters& params) { bool isSeated = _state == RigRole::Seated; bool hipsEnabled = params.primaryControllerFlags[PrimaryControllerType_Hips] & (uint8_t)ControllerFlags::Enabled; - bool hipsEstimated = params.primaryControllerFlags[PrimaryControllerType_Hips] & (uint8_t)ControllerFlags::Estimated; - bool hmdMode = hipsEnabled && !hipsEstimated; + bool hmdMode = hipsEnabled; if ((reactionPlaying || isSeated) && !hmdMode) { // TODO: make this smooth. diff --git a/libraries/animation/src/Rig.h b/libraries/animation/src/Rig.h index 47f96221f9..51ff537d51 100644 --- a/libraries/animation/src/Rig.h +++ b/libraries/animation/src/Rig.h @@ -135,7 +135,7 @@ public: void initJointStates(const HFMModel& hfmModel, const glm::mat4& modelOffset); void reset(const HFMModel& hfmModel); - bool jointStatesEmpty(); + bool jointStatesEmpty() const; int getJointStateCount() const; int indexOfJoint(const QString& jointName) const; QString nameOfJoint(int jointIndex) const; @@ -163,6 +163,8 @@ public: void setJointTranslation(int index, bool valid, const glm::vec3& translation, float priority); void setJointRotation(int index, bool valid, const glm::quat& rotation, float priority); + bool getIsJointOverridden(int jointIndex) const; + // if translation and rotation is identity, position will be in rig space bool getJointPositionInWorldFrame(int jointIndex, glm::vec3& position, glm::vec3 translation, glm::quat rotation) const; diff --git a/libraries/audio/src/AudioInjector.cpp b/libraries/audio/src/AudioInjector.cpp index c09dba6190..2df766377f 100644 --- a/libraries/audio/src/AudioInjector.cpp +++ b/libraries/audio/src/AudioInjector.cpp @@ -210,7 +210,6 @@ qint64 writeStringToStream(const QString& string, QDataStream& stream) { int64_t AudioInjector::injectNextFrame() { if (stateHas(AudioInjectorState::NetworkInjectionFinished)) { - qCDebug(audio) << "AudioInjector::injectNextFrame called but AudioInjector has finished and was not restarted. Returning."; return NEXT_FRAME_DELTA_ERROR_OR_FINISHED; } diff --git a/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp b/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp index 7f363dd36f..b0f3934278 100644 --- a/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp +++ b/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp @@ -108,11 +108,21 @@ void Avatar::setShowMyLookAtVectors(bool showMine) { showMyLookAtVectors = showMine; } +static bool showMyLookAtTarget = false; +void Avatar::setShowMyLookAtTarget(bool showMine) { + showMyLookAtTarget = showMine; +} + static bool showOtherLookAtVectors = false; void Avatar::setShowOtherLookAtVectors(bool showOthers) { showOtherLookAtVectors = showOthers; } +static bool showOtherLookAtTarget = false; +void Avatar::setShowOtherLookAtTarget(bool showOthers) { + showOtherLookAtTarget = showOthers; +} + static bool showCollisionShapes = false; void Avatar::setShowCollisionShapes(bool render) { showCollisionShapes = render; @@ -711,6 +721,14 @@ void Avatar::updateRenderItem(render::Transaction& transaction) { void Avatar::postUpdate(float deltaTime, const render::ScenePointer& scene) { + if (isMyAvatar() ? showMyLookAtTarget : showOtherLookAtTarget) { + glm::vec3 lookAtTarget = getHead()->getLookAtPosition(); + DebugDraw::getInstance().addMarker(QString("look-at-") + getID().toString(), + glm::quat(), lookAtTarget, glm::vec4(1), 1.0f); + } else { + DebugDraw::getInstance().removeMarker(QString("look-at-") + getID().toString()); + } + if (isMyAvatar() ? showMyLookAtVectors : showOtherLookAtVectors) { const float EYE_RAY_LENGTH = 10.0; const glm::vec4 BLUE(0.0f, 0.0f, _lookAtSnappingEnabled ? 1.0f : 0.25f, 1.0f); diff --git a/libraries/avatars-renderer/src/avatars-renderer/Avatar.h b/libraries/avatars-renderer/src/avatars-renderer/Avatar.h index 7bb15ecbf7..b969449d5e 100644 --- a/libraries/avatars-renderer/src/avatars-renderer/Avatar.h +++ b/libraries/avatars-renderer/src/avatars-renderer/Avatar.h @@ -140,7 +140,9 @@ public: static void setShowAvatars(bool render); static void setShowReceiveStats(bool receiveStats); static void setShowMyLookAtVectors(bool showMine); + static void setShowMyLookAtTarget(bool showMine); static void setShowOtherLookAtVectors(bool showOthers); + static void setShowOtherLookAtTarget(bool showOthers); static void setShowCollisionShapes(bool render); static void setShowNamesAboveHeads(bool show); diff --git a/libraries/avatars-renderer/src/avatars-renderer/Head.cpp b/libraries/avatars-renderer/src/avatars-renderer/Head.cpp index a551793ab0..445184f5f8 100644 --- a/libraries/avatars-renderer/src/avatars-renderer/Head.cpp +++ b/libraries/avatars-renderer/src/avatars-renderer/Head.cpp @@ -17,7 +17,6 @@ #include #include #include -#include #include #include "Logging.h" @@ -58,7 +57,7 @@ void Head::simulate(float deltaTime) { _longTermAverageLoudness = glm::mix(_longTermAverageLoudness, _averageLoudness, glm::min(deltaTime / AUDIO_LONG_TERM_AVERAGING_SECS, 1.0f)); } - if (!_isEyeTrackerConnected) { + if (getHasProceduralEyeMovement()) { // Update eye saccades const float AVERAGE_MICROSACCADE_INTERVAL = 1.0f; const float AVERAGE_SACCADE_INTERVAL = 6.0f; @@ -82,6 +81,7 @@ void Head::simulate(float deltaTime) { const float FULLY_OPEN = 0.0f; const float FULLY_CLOSED = 1.0f; if (getHasProceduralBlinkFaceMovement()) { + // handle automatic blinks // Detect transition from talking to not; force blink after that and a delay bool forceBlink = false; const float TALKING_LOUDNESS = 150.0f; @@ -129,7 +129,7 @@ void Head::simulate(float deltaTime) { _leftEyeBlink = FULLY_OPEN; } - // use data to update fake Faceshift blendshape coefficients + // use data to update fake Faceshift blendshape coefficients if (getHasAudioEnabledFaceMovement()) { // Update audio attack data for facial animation (eyebrows and mouth) float audioAttackAveragingRate = (10.0f - deltaTime * NORMAL_HZ) / 10.0f; // --> 0.9 at 60 Hz @@ -152,7 +152,8 @@ void Head::simulate(float deltaTime) { _mouthTime = 0.0f; } - FaceTracker::updateFakeCoefficients(_leftEyeBlink, + FaceTracker::updateFakeCoefficients( + _leftEyeBlink, _rightEyeBlink, _browAudioLift, _audioJawOpen, @@ -162,6 +163,8 @@ void Head::simulate(float deltaTime) { _transientBlendshapeCoefficients); if (getHasProceduralEyeFaceMovement()) { + // This controls two things, the eye brow and the upper eye lid, it is driven by the vertical up/down angle of the + // eyes relative to the head. This is to try to help prevent sleepy eyes/crazy eyes. applyEyelidOffset(getOrientation()); } @@ -292,7 +295,7 @@ glm::quat Head::getFinalOrientationInLocalFrame() const { } // Everyone else's head keeps track of a lookAtPosition that everybody sees the same, and refers to where that head -// is looking in model space -- e.g., at someone's eyeball, or between their eyes, or mouth, etc. Everyon's Interface +// is looking in model space -- e.g., at someone's eyeball, or between their eyes, or mouth, etc. Everyone's Interface // will have the same value for the lookAtPosition of any given head. // // Everyone else's head also keeps track of a correctedLookAtPosition that may be different for the same head within diff --git a/libraries/avatars-renderer/src/avatars-renderer/SkeletonModel.cpp b/libraries/avatars-renderer/src/avatars-renderer/SkeletonModel.cpp index 40b65c54a1..e0fed08955 100644 --- a/libraries/avatars-renderer/src/avatars-renderer/SkeletonModel.cpp +++ b/libraries/avatars-renderer/src/avatars-renderer/SkeletonModel.cpp @@ -93,19 +93,30 @@ void SkeletonModel::initJointStates() { emit skeletonLoaded(); } +glm::vec3 SkeletonModel::avoidCrossedEyes(const glm::vec3& lookAt) { + // make sure lookAt is not too close to face (avoid crosseyes) + glm::vec3 focusOffset = lookAt - _owningAvatar->getHead()->getEyePosition(); + float focusDistance = glm::length(focusOffset); + const float MIN_LOOK_AT_FOCUS_DISTANCE = 1.0f; + if (focusDistance < MIN_LOOK_AT_FOCUS_DISTANCE && focusDistance > EPSILON) { + return _owningAvatar->getHead()->getEyePosition() + (MIN_LOOK_AT_FOCUS_DISTANCE / focusDistance) * focusOffset; + } else { + return lookAt; + } +} + // Called within Model::simulate call, below. void SkeletonModel::updateRig(float deltaTime, glm::mat4 parentTransform) { assert(!_owningAvatar->isMyAvatar()); Head* head = _owningAvatar->getHead(); - // make sure lookAt is not too close to face (avoid crosseyes) - glm::vec3 lookAt = head->getCorrectedLookAtPosition(); - glm::vec3 focusOffset = lookAt - _owningAvatar->getHead()->getEyePosition(); - float focusDistance = glm::length(focusOffset); - const float MIN_LOOK_AT_FOCUS_DISTANCE = 1.0f; - if (focusDistance < MIN_LOOK_AT_FOCUS_DISTANCE && focusDistance > EPSILON) { - lookAt = _owningAvatar->getHead()->getEyePosition() + (MIN_LOOK_AT_FOCUS_DISTANCE / focusDistance) * focusOffset; + bool eyePosesValid = !head->getHasProceduralEyeMovement(); + glm::vec3 lookAt; + if (eyePosesValid) { + lookAt = head->getLookAtPosition(); // don't apply no-crosseyes code etc when eyes are being tracked + } else { + lookAt = avoidCrossedEyes(head->getCorrectedLookAtPosition()); } // no need to call Model::updateRig() because otherAvatars get their joint state @@ -288,6 +299,15 @@ bool SkeletonModel::getEyeModelPositions(glm::vec3& firstEyePosition, glm::vec3& return false; } + +bool SkeletonModel::getIsJointOverridden(int jointIndex) const { + // has this joint been set by a script? + if (!isLoaded() || _rig.jointStatesEmpty()) { + return false; + } + return _rig.getIsJointOverridden(jointIndex); +} + bool SkeletonModel::getEyePositions(glm::vec3& firstEyePosition, glm::vec3& secondEyePosition) const { if (getEyeModelPositions(firstEyePosition, secondEyePosition)) { firstEyePosition = _translation + _rotation * firstEyePosition; @@ -352,4 +372,3 @@ bool SkeletonModel::hasSkeleton() { void SkeletonModel::onInvalidate() { } - diff --git a/libraries/avatars-renderer/src/avatars-renderer/SkeletonModel.h b/libraries/avatars-renderer/src/avatars-renderer/SkeletonModel.h index 99f6632306..6b0bd79f0b 100644 --- a/libraries/avatars-renderer/src/avatars-renderer/SkeletonModel.h +++ b/libraries/avatars-renderer/src/avatars-renderer/SkeletonModel.h @@ -37,9 +37,12 @@ public: void initJointStates() override; void simulate(float deltaTime, bool fullUpdate = true) override; + glm::vec3 avoidCrossedEyes(const glm::vec3& lookAt); void updateRig(float deltaTime, glm::mat4 parentTransform) override; void updateAttitude(const glm::quat& orientation); + bool getIsJointOverridden(int jointIndex) const; + /// Returns the index of the left hand joint, or -1 if not found. int getLeftHandJointIndex() const { return isActive() ? _rig.indexOfJoint("LeftHand") : -1; } diff --git a/libraries/avatars/src/AvatarData.cpp b/libraries/avatars/src/AvatarData.cpp index c03f9430be..a91154ff15 100755 --- a/libraries/avatars/src/AvatarData.cpp +++ b/libraries/avatars/src/AvatarData.cpp @@ -245,9 +245,10 @@ QByteArray AvatarData::toByteArrayStateful(AvatarDataDetail dataDetail, bool dro } QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSentTime, - const QVector& lastSentJointData, - AvatarDataPacket::SendStatus& sendStatus, bool dropFaceTracking, bool distanceAdjust, - glm::vec3 viewerPosition, QVector* sentJointDataOut, int maxDataSize, AvatarDataRate* outboundDataRateOut) const { + const QVector& lastSentJointData, AvatarDataPacket::SendStatus& sendStatus, + bool dropFaceTracking, bool distanceAdjust, glm::vec3 viewerPosition, + QVector* sentJointDataOut, + int maxDataSize, AvatarDataRate* outboundDataRateOut) const { bool cullSmallChanges = (dataDetail == CullSmallData); bool sendAll = (dataDetail == SendAllData); @@ -532,7 +533,7 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent setAtBit16(flags, IS_FACE_TRACKER_CONNECTED); } // eye tracker state - if (_headData->_isEyeTrackerConnected) { + if (!_headData->_hasProceduralEyeMovement) { setAtBit16(flags, IS_EYE_TRACKER_CONNECTED); } // referential state @@ -1150,7 +1151,7 @@ int AvatarData::parseDataFromBuffer(const QByteArray& buffer) { + (oneAtBit16(bitItems, HAND_STATE_FINGER_POINTING_BIT) ? IS_FINGER_POINTING_FLAG : 0); auto newFaceTrackerConnected = oneAtBit16(bitItems, IS_FACE_TRACKER_CONNECTED); - auto newEyeTrackerConnected = oneAtBit16(bitItems, IS_EYE_TRACKER_CONNECTED); + auto newHasntProceduralEyeMovement = oneAtBit16(bitItems, IS_EYE_TRACKER_CONNECTED); auto newHasAudioEnabledFaceMovement = oneAtBit16(bitItems, AUDIO_ENABLED_FACE_MOVEMENT); auto newHasProceduralEyeFaceMovement = oneAtBit16(bitItems, PROCEDURAL_EYE_FACE_MOVEMENT); @@ -1161,7 +1162,7 @@ int AvatarData::parseDataFromBuffer(const QByteArray& buffer) { bool keyStateChanged = (_keyState != newKeyState); bool handStateChanged = (_handState != newHandState); bool faceStateChanged = (_headData->_isFaceTrackerConnected != newFaceTrackerConnected); - bool eyeStateChanged = (_headData->_isEyeTrackerConnected != newEyeTrackerConnected); + bool eyeStateChanged = (_headData->_hasProceduralEyeMovement == newHasntProceduralEyeMovement); bool audioEnableFaceMovementChanged = (_headData->getHasAudioEnabledFaceMovement() != newHasAudioEnabledFaceMovement); bool proceduralEyeFaceMovementChanged = (_headData->getHasProceduralEyeFaceMovement() != newHasProceduralEyeFaceMovement); bool proceduralBlinkFaceMovementChanged = (_headData->getHasProceduralBlinkFaceMovement() != newHasProceduralBlinkFaceMovement); @@ -1174,7 +1175,7 @@ int AvatarData::parseDataFromBuffer(const QByteArray& buffer) { _keyState = newKeyState; _handState = newHandState; _headData->_isFaceTrackerConnected = newFaceTrackerConnected; - _headData->_isEyeTrackerConnected = newEyeTrackerConnected; + _headData->setHasProceduralEyeMovement(!newHasntProceduralEyeMovement); _headData->setHasAudioEnabledFaceMovement(newHasAudioEnabledFaceMovement); _headData->setHasProceduralEyeFaceMovement(newHasProceduralEyeFaceMovement); _headData->setHasProceduralBlinkFaceMovement(newHasProceduralBlinkFaceMovement); diff --git a/libraries/avatars/src/HeadData.cpp b/libraries/avatars/src/HeadData.cpp index 19f5efcd16..c86e534929 100644 --- a/libraries/avatars/src/HeadData.cpp +++ b/libraries/avatars/src/HeadData.cpp @@ -196,3 +196,40 @@ void HeadData::fromJson(const QJsonObject& json) { setHeadOrientation(quatFromJsonValue(json[JSON_AVATAR_HEAD_ROTATION])); } } + +bool HeadData::getHasProceduralEyeFaceMovement() const { + return _hasProceduralEyeFaceMovement; +} + +void HeadData::setHasProceduralEyeFaceMovement(bool hasProceduralEyeFaceMovement) { + _hasProceduralEyeFaceMovement = hasProceduralEyeFaceMovement; +} + +bool HeadData::getHasProceduralBlinkFaceMovement() const { + // return _hasProceduralBlinkFaceMovement; + return _hasProceduralBlinkFaceMovement && !_isFaceTrackerConnected; +} + +void HeadData::setHasProceduralBlinkFaceMovement(bool hasProceduralBlinkFaceMovement) { + _hasProceduralBlinkFaceMovement = hasProceduralBlinkFaceMovement; +} + +bool HeadData::getHasAudioEnabledFaceMovement() const { + return _hasAudioEnabledFaceMovement; +} + +void HeadData::setHasAudioEnabledFaceMovement(bool hasAudioEnabledFaceMovement) { + _hasAudioEnabledFaceMovement = hasAudioEnabledFaceMovement; +} + +bool HeadData::getHasProceduralEyeMovement() const { + return _hasProceduralEyeMovement; +} + +void HeadData::setHasProceduralEyeMovement(bool hasProceduralEyeMovement) { + _hasProceduralEyeMovement = hasProceduralEyeMovement; +} + +void HeadData::setFaceTrackerConnected(bool value) { + _isFaceTrackerConnected = value; +} diff --git a/libraries/avatars/src/HeadData.h b/libraries/avatars/src/HeadData.h index 6d211da2cd..dc5aaf2595 100644 --- a/libraries/avatars/src/HeadData.h +++ b/libraries/avatars/src/HeadData.h @@ -72,23 +72,17 @@ public: } bool lookAtPositionChangedSince(quint64 time) { return _lookAtPositionChanged >= time; } - bool getHasProceduralEyeFaceMovement() const { return _hasProceduralEyeFaceMovement; } + bool getHasProceduralEyeFaceMovement() const; + void setHasProceduralEyeFaceMovement(bool hasProceduralEyeFaceMovement); + bool getHasProceduralBlinkFaceMovement() const; + void setHasProceduralBlinkFaceMovement(bool hasProceduralBlinkFaceMovement); + bool getHasAudioEnabledFaceMovement() const; + void setHasAudioEnabledFaceMovement(bool hasAudioEnabledFaceMovement); + bool getHasProceduralEyeMovement() const; + void setHasProceduralEyeMovement(bool hasProceduralEyeMovement); - void setHasProceduralEyeFaceMovement(const bool hasProceduralEyeFaceMovement) { - _hasProceduralEyeFaceMovement = hasProceduralEyeFaceMovement; - } - - bool getHasProceduralBlinkFaceMovement() const { return _hasProceduralBlinkFaceMovement; } - - void setHasProceduralBlinkFaceMovement(const bool hasProceduralBlinkFaceMovement) { - _hasProceduralBlinkFaceMovement = hasProceduralBlinkFaceMovement; - } - - bool getHasAudioEnabledFaceMovement() const { return _hasAudioEnabledFaceMovement; } - - void setHasAudioEnabledFaceMovement(const bool hasAudioEnabledFaceMovement) { - _hasAudioEnabledFaceMovement = hasAudioEnabledFaceMovement; - } + void setFaceTrackerConnected(bool value); + bool getFaceTrackerConnected() const { return _isFaceTrackerConnected; } friend class AvatarData; @@ -107,8 +101,10 @@ protected: bool _hasAudioEnabledFaceMovement { true }; bool _hasProceduralBlinkFaceMovement { true }; bool _hasProceduralEyeFaceMovement { true }; + bool _hasProceduralEyeMovement { true }; + bool _isFaceTrackerConnected { false }; - bool _isEyeTrackerConnected { false }; + float _leftEyeBlink { 0.0f }; float _rightEyeBlink { 0.0f }; float _averageLoudness { 0.0f }; diff --git a/libraries/baking/src/ModelBaker.cpp b/libraries/baking/src/ModelBaker.cpp index 9d6a368e1c..70290fe283 100644 --- a/libraries/baking/src/ModelBaker.cpp +++ b/libraries/baking/src/ModelBaker.cpp @@ -246,6 +246,12 @@ void ModelBaker::bakeSourceCopy() { // Begin hfm baking baker.run(); + const auto& errors = baker.getDracoErrors(); + if (std::find(errors.cbegin(), errors.cend(), true) != errors.cend()) { + handleError("Failed to finalize the baking of a draco Geometry node from model " + _modelURL.toString()); + return; + } + _hfmModel = baker.getHFMModel(); _materialMapping = baker.getMaterialMapping(); dracoMeshes = baker.getDracoMeshes(); @@ -437,8 +443,7 @@ void ModelBaker::abort() { bool ModelBaker::buildDracoMeshNode(FBXNode& dracoMeshNode, const QByteArray& dracoMeshBytes, const std::vector& dracoMaterialList) { if (dracoMeshBytes.isEmpty()) { - handleError("Failed to finalize the baking of a draco Geometry node"); - return false; + handleWarning("Empty mesh detected in model: '" + _modelURL.toString() + "'. It will be included in the baked output."); } FBXNode dracoNode; diff --git a/libraries/baking/src/OBJBaker.h b/libraries/baking/src/OBJBaker.h index 9d0fe53e3c..55adec5786 100644 --- a/libraries/baking/src/OBJBaker.h +++ b/libraries/baking/src/OBJBaker.h @@ -28,7 +28,7 @@ protected: private: void createFBXNodeTree(FBXNode& rootNode, const hfm::Model::Pointer& hfmModel, const hifi::ByteArray& dracoMesh); - void setMaterialNodeProperties(FBXNode& materialNode, QString material, const hfm::Model::Pointer& hfmModel); + void setMaterialNodeProperties(FBXNode& materialNode, QString material, const hfm::Model::Pointer& hfmModel); NodeID nextNodeID() { return _nodeID++; } NodeID _nodeID { 0 }; diff --git a/libraries/controllers/src/controllers/Actions.cpp b/libraries/controllers/src/controllers/Actions.cpp index 9f9d92fed7..40011f2682 100644 --- a/libraries/controllers/src/controllers/Actions.cpp +++ b/libraries/controllers/src/controllers/Actions.cpp @@ -347,6 +347,10 @@ namespace controller { makePosePair(Action::HIPS, "Hips"), makePosePair(Action::SPINE2, "Spine2"), makePosePair(Action::HEAD, "Head"), + makePosePair(Action::LEFT_EYE, "LeftEye"), + makePosePair(Action::RIGHT_EYE, "RightEye"), + makeAxisPair(Action::LEFT_EYE_BLINK, "LeftEyeBlink"), + makeAxisPair(Action::RIGHT_EYE_BLINK, "RightEyeBlink"), makePosePair(Action::LEFT_HAND_THUMB1, "LeftHandThumb1"), makePosePair(Action::LEFT_HAND_THUMB2, "LeftHandThumb2"), diff --git a/libraries/controllers/src/controllers/Actions.h b/libraries/controllers/src/controllers/Actions.h index 3e99d8d147..f91d9f2522 100644 --- a/libraries/controllers/src/controllers/Actions.h +++ b/libraries/controllers/src/controllers/Actions.h @@ -181,6 +181,11 @@ enum class Action { TRACKED_OBJECT_15, SPRINT, + LEFT_EYE, + RIGHT_EYE, + LEFT_EYE_BLINK, + RIGHT_EYE_BLINK, + NUM_ACTIONS }; diff --git a/libraries/controllers/src/controllers/AxisValue.cpp b/libraries/controllers/src/controllers/AxisValue.cpp index 4b7913754c..0acbc6dbc7 100644 --- a/libraries/controllers/src/controllers/AxisValue.cpp +++ b/libraries/controllers/src/controllers/AxisValue.cpp @@ -12,10 +12,13 @@ namespace controller { - AxisValue::AxisValue(const float value, const quint64 timestamp) : - value(value), timestamp(timestamp) { } + AxisValue::AxisValue(const float value, const quint64 timestamp, bool valid) : + value(value), timestamp(timestamp), valid(valid) { + } bool AxisValue::operator==(const AxisValue& right) const { - return value == right.value && timestamp == right.timestamp; + return value == right.value && + timestamp == right.timestamp && + valid == right.valid; } } diff --git a/libraries/controllers/src/controllers/AxisValue.h b/libraries/controllers/src/controllers/AxisValue.h index e4bc20f7d2..ec356e1fb3 100644 --- a/libraries/controllers/src/controllers/AxisValue.h +++ b/libraries/controllers/src/controllers/AxisValue.h @@ -21,14 +21,14 @@ namespace controller { float value { 0.0f }; // The value can be timestamped to determine if consecutive identical values should be output (e.g., mouse movement). quint64 timestamp { 0 }; + bool valid { false }; AxisValue() {} - AxisValue(const float value, const quint64 timestamp); + AxisValue(const float value, const quint64 timestamp, bool valid = true); bool operator ==(const AxisValue& right) const; bool operator !=(const AxisValue& right) const { return !(*this == right); } }; - } #endif // hifi_controllers_AxisValue_h diff --git a/libraries/controllers/src/controllers/InputDevice.cpp b/libraries/controllers/src/controllers/InputDevice.cpp index dd430263fa..b28e7cfc82 100644 --- a/libraries/controllers/src/controllers/InputDevice.cpp +++ b/libraries/controllers/src/controllers/InputDevice.cpp @@ -77,13 +77,13 @@ namespace controller { return { getButton(channel), 0 }; case ChannelType::POSE: - return { getPose(channel).valid ? 1.0f : 0.0f, 0 }; + return { getPose(channel).valid ? 1.0f : 0.0f, 0, getPose(channel).valid }; default: break; } - return { 0.0f, 0 }; + return { 0.0f, 0, false }; } AxisValue InputDevice::getValue(const Input& input) const { diff --git a/libraries/controllers/src/controllers/StandardController.cpp b/libraries/controllers/src/controllers/StandardController.cpp index ece10ecca3..dbc92cc7e5 100644 --- a/libraries/controllers/src/controllers/StandardController.cpp +++ b/libraries/controllers/src/controllers/StandardController.cpp @@ -354,6 +354,10 @@ Input::NamedVector StandardController::getAvailableInputs() const { makePair(HIPS, "Hips"), makePair(SPINE2, "Spine2"), makePair(HEAD, "Head"), + makePair(LEFT_EYE, "LeftEye"), + makePair(RIGHT_EYE, "RightEye"), + makePair(LEFT_EYE_BLINK, "LeftEyeBlink"), + makePair(RIGHT_EYE_BLINK, "RightEyeBlink"), // Aliases, PlayStation style names makePair(LB, "L1"), diff --git a/libraries/controllers/src/controllers/StandardControls.h b/libraries/controllers/src/controllers/StandardControls.h index f521ab81cf..99d9246264 100644 --- a/libraries/controllers/src/controllers/StandardControls.h +++ b/libraries/controllers/src/controllers/StandardControls.h @@ -90,6 +90,8 @@ namespace controller { // Grips LEFT_GRIP, RIGHT_GRIP, + LEFT_EYE_BLINK, + RIGHT_EYE_BLINK, NUM_STANDARD_AXES, LZ = LT, RZ = RT @@ -174,6 +176,8 @@ namespace controller { TRACKED_OBJECT_13, TRACKED_OBJECT_14, TRACKED_OBJECT_15, + LEFT_EYE, + RIGHT_EYE, NUM_STANDARD_POSES }; diff --git a/libraries/controllers/src/controllers/UserInputMapper.cpp b/libraries/controllers/src/controllers/UserInputMapper.cpp index 33dc37312e..1eb1a9fa1a 100755 --- a/libraries/controllers/src/controllers/UserInputMapper.cpp +++ b/libraries/controllers/src/controllers/UserInputMapper.cpp @@ -256,6 +256,9 @@ void UserInputMapper::update(float deltaTime) { for (auto& channel : _actionStates) { channel = 0.0f; } + for (unsigned int i = 0; i < _actionStatesValid.size(); i++) { + _actionStatesValid[i] = true; + } for (auto& channel : _poseStates) { channel = Pose(); @@ -1233,5 +1236,17 @@ void UserInputMapper::disableMapping(const Mapping::Pointer& mapping) { } } +void UserInputMapper::setActionState(Action action, float value, bool valid) { + _actionStates[toInt(action)] = value; + _actionStatesValid[toInt(action)] = valid; +} + +void UserInputMapper::deltaActionState(Action action, float delta, bool valid) { + _actionStates[toInt(action)] += delta; + bool wasValid = _actionStatesValid[toInt(action)]; + _actionStatesValid[toInt(action)] = wasValid & valid; +} + + } diff --git a/libraries/controllers/src/controllers/UserInputMapper.h b/libraries/controllers/src/controllers/UserInputMapper.h index 2b3c947491..cd44f3226c 100644 --- a/libraries/controllers/src/controllers/UserInputMapper.h +++ b/libraries/controllers/src/controllers/UserInputMapper.h @@ -82,13 +82,14 @@ namespace controller { QString getActionName(Action action) const; QString getStandardPoseName(uint16_t pose); float getActionState(Action action) const { return _actionStates[toInt(action)]; } + bool getActionStateValid(Action action) const { return _actionStatesValid[toInt(action)]; } Pose getPoseState(Action action) const; int findAction(const QString& actionName) const; QVector getActionNames() const; Input inputFromAction(Action action) const { return getActionInputs()[toInt(action)].first; } - void setActionState(Action action, float value) { _actionStates[toInt(action)] = value; } - void deltaActionState(Action action, float delta) { _actionStates[toInt(action)] += delta; } + void setActionState(Action action, float value, bool valid = true); + void deltaActionState(Action action, float delta, bool valid = true); void setActionState(Action action, const Pose& value) { _poseStates[toInt(action)] = value; } bool triggerHapticPulse(float strength, float duration, controller::Hand hand); bool triggerHapticPulseOnDevice(uint16 deviceID, float strength, float duration, controller::Hand hand); @@ -146,6 +147,7 @@ namespace controller { std::vector _actionStates = std::vector(toInt(Action::NUM_ACTIONS), 0.0f); std::vector _actionScales = std::vector(toInt(Action::NUM_ACTIONS), 1.0f); std::vector _lastActionStates = std::vector(toInt(Action::NUM_ACTIONS), 0.0f); + std::vector _actionStatesValid = std::vector(toInt(Action::NUM_ACTIONS), false); std::vector _poseStates = std::vector(toInt(Action::NUM_ACTIONS)); std::vector _lastStandardStates = std::vector(); @@ -167,7 +169,7 @@ namespace controller { ConditionalPointer conditionalFor(const QJSValue& endpoint); ConditionalPointer conditionalFor(const QScriptValue& endpoint); ConditionalPointer conditionalFor(const Input& endpoint) const; - + MappingPointer parseMapping(const QJsonValue& json); RoutePointer parseRoute(const QJsonValue& value); EndpointPointer parseDestination(const QJsonValue& value); diff --git a/libraries/controllers/src/controllers/impl/Endpoint.h b/libraries/controllers/src/controllers/impl/Endpoint.h index bcf71f3094..692e427e16 100644 --- a/libraries/controllers/src/controllers/impl/Endpoint.h +++ b/libraries/controllers/src/controllers/impl/Endpoint.h @@ -100,7 +100,7 @@ namespace controller { _currentPose = value; } protected: - AxisValue _currentValue { 0.0f, 0 }; + AxisValue _currentValue { 0.0f, 0, false }; Pose _currentPose {}; }; diff --git a/libraries/controllers/src/controllers/impl/endpoints/ActionEndpoint.cpp b/libraries/controllers/src/controllers/impl/endpoints/ActionEndpoint.cpp index 58744c468c..4be23b0c29 100644 --- a/libraries/controllers/src/controllers/impl/endpoints/ActionEndpoint.cpp +++ b/libraries/controllers/src/controllers/impl/endpoints/ActionEndpoint.cpp @@ -26,7 +26,7 @@ void ActionEndpoint::apply(AxisValue newValue, const Pointer& source) { _currentValue.value += newValue.value; if (_input != Input::INVALID_INPUT) { - userInputMapper->deltaActionState(Action(_input.getChannel()), newValue.value); + userInputMapper->deltaActionState(Action(_input.getChannel()), newValue.value, newValue.valid); } } diff --git a/libraries/controllers/src/controllers/impl/endpoints/ActionEndpoint.h b/libraries/controllers/src/controllers/impl/endpoints/ActionEndpoint.h index 94da4663aa..7ab21031a7 100644 --- a/libraries/controllers/src/controllers/impl/endpoints/ActionEndpoint.h +++ b/libraries/controllers/src/controllers/impl/endpoints/ActionEndpoint.h @@ -32,7 +32,7 @@ public: virtual void reset() override; private: - AxisValue _currentValue { 0.0f, 0 }; + AxisValue _currentValue { 0.0f, 0, false }; Pose _currentPose{}; }; diff --git a/libraries/controllers/src/controllers/impl/endpoints/CompositeEndpoint.cpp b/libraries/controllers/src/controllers/impl/endpoints/CompositeEndpoint.cpp index f54c786a33..82a44bd7ae 100644 --- a/libraries/controllers/src/controllers/impl/endpoints/CompositeEndpoint.cpp +++ b/libraries/controllers/src/controllers/impl/endpoints/CompositeEndpoint.cpp @@ -27,7 +27,9 @@ bool CompositeEndpoint::readable() const { AxisValue CompositeEndpoint::peek() const { auto negative = first->peek(); auto positive = second->peek(); - auto result = AxisValue(positive.value - negative.value, std::max(positive.timestamp, negative.timestamp)); + auto result = AxisValue(positive.value - negative.value, + std::max(positive.timestamp, negative.timestamp), + negative.valid && positive.valid); return result; } @@ -35,7 +37,9 @@ AxisValue CompositeEndpoint::peek() const { AxisValue CompositeEndpoint::value() { auto negative = first->value(); auto positive = second->value(); - auto result = AxisValue(positive.value - negative.value, std::max(positive.timestamp, negative.timestamp)); + auto result = AxisValue(positive.value - negative.value, + std::max(positive.timestamp, negative.timestamp), + negative.valid && positive.valid); return result; } diff --git a/libraries/controllers/src/controllers/impl/endpoints/InputEndpoint.cpp b/libraries/controllers/src/controllers/impl/endpoints/InputEndpoint.cpp index 3755d860b6..db0aed47e1 100644 --- a/libraries/controllers/src/controllers/impl/endpoints/InputEndpoint.cpp +++ b/libraries/controllers/src/controllers/impl/endpoints/InputEndpoint.cpp @@ -16,7 +16,7 @@ using namespace controller; AxisValue InputEndpoint::peek() const { if (isPose()) { - return peekPose().valid ? AxisValue(1.0f, 0) : AxisValue(0.0f, 0); + return peekPose().valid ? AxisValue(1.0f, 0) : AxisValue(0.0f, 0, false); } auto userInputMapper = DependencyManager::get(); auto deviceProxy = userInputMapper->getDevice(_input); diff --git a/libraries/controllers/src/controllers/impl/endpoints/ScriptEndpoint.h b/libraries/controllers/src/controllers/impl/endpoints/ScriptEndpoint.h index e739ab0b01..1aa1746b24 100644 --- a/libraries/controllers/src/controllers/impl/endpoints/ScriptEndpoint.h +++ b/libraries/controllers/src/controllers/impl/endpoints/ScriptEndpoint.h @@ -41,7 +41,7 @@ protected: private: QScriptValue _callable; float _lastValueRead { 0.0f }; - AxisValue _lastValueWritten { 0.0f, 0 }; + AxisValue _lastValueWritten { 0.0f, 0, false }; bool _returnPose { false }; Pose _lastPoseRead; diff --git a/libraries/controllers/src/controllers/impl/filters/ClampFilter.h b/libraries/controllers/src/controllers/impl/filters/ClampFilter.h index 04684655c9..7ec8173c2a 100644 --- a/libraries/controllers/src/controllers/impl/filters/ClampFilter.h +++ b/libraries/controllers/src/controllers/impl/filters/ClampFilter.h @@ -19,7 +19,7 @@ class ClampFilter : public Filter { public: ClampFilter(float min = 0.0, float max = 1.0) : _min(min), _max(max) {}; virtual AxisValue apply(AxisValue value) const override { - return { glm::clamp(value.value, _min, _max), value.timestamp }; + return { glm::clamp(value.value, _min, _max), value.timestamp, value.valid }; } virtual Pose apply(Pose value) const override { return value; } diff --git a/libraries/controllers/src/controllers/impl/filters/ConstrainToIntegerFilter.h b/libraries/controllers/src/controllers/impl/filters/ConstrainToIntegerFilter.h index 2cce5f828d..3adcbbd008 100644 --- a/libraries/controllers/src/controllers/impl/filters/ConstrainToIntegerFilter.h +++ b/libraries/controllers/src/controllers/impl/filters/ConstrainToIntegerFilter.h @@ -20,7 +20,7 @@ public: ConstrainToIntegerFilter() = default; virtual AxisValue apply(AxisValue value) const override { - return { glm::sign(value.value), value.timestamp }; + return { glm::sign(value.value), value.timestamp, value.valid }; } virtual Pose apply(Pose value) const override { return value; } diff --git a/libraries/controllers/src/controllers/impl/filters/ConstrainToPositiveIntegerFilter.h b/libraries/controllers/src/controllers/impl/filters/ConstrainToPositiveIntegerFilter.h index 07dd6654f1..5dcc43b3f7 100644 --- a/libraries/controllers/src/controllers/impl/filters/ConstrainToPositiveIntegerFilter.h +++ b/libraries/controllers/src/controllers/impl/filters/ConstrainToPositiveIntegerFilter.h @@ -20,7 +20,7 @@ public: ConstrainToPositiveIntegerFilter() = default; virtual AxisValue apply(AxisValue value) const override { - return { (value.value <= 0.0f) ? 0.0f : 1.0f, value.timestamp }; + return { (value.value <= 0.0f) ? 0.0f : 1.0f, value.timestamp, value.valid }; } virtual Pose apply(Pose value) const override { return value; } diff --git a/libraries/controllers/src/controllers/impl/filters/DeadZoneFilter.cpp b/libraries/controllers/src/controllers/impl/filters/DeadZoneFilter.cpp index 84d3b9de60..4c396fee70 100644 --- a/libraries/controllers/src/controllers/impl/filters/DeadZoneFilter.cpp +++ b/libraries/controllers/src/controllers/impl/filters/DeadZoneFilter.cpp @@ -18,7 +18,7 @@ AxisValue DeadZoneFilter::apply(AxisValue value) const { if (magnitude < _min) { return { 0.0f, value.timestamp }; } - return { (magnitude - _min) * scale, value.timestamp }; + return { (magnitude - _min) * scale, value.timestamp, value.valid }; } bool DeadZoneFilter::parseParameters(const QJsonValue& parameters) { diff --git a/libraries/controllers/src/controllers/impl/filters/HysteresisFilter.cpp b/libraries/controllers/src/controllers/impl/filters/HysteresisFilter.cpp index 91e59a39b9..b7b573a98c 100644 --- a/libraries/controllers/src/controllers/impl/filters/HysteresisFilter.cpp +++ b/libraries/controllers/src/controllers/impl/filters/HysteresisFilter.cpp @@ -19,7 +19,6 @@ HysteresisFilter::HysteresisFilter(float min, float max) : _min(min), _max(max) } }; - AxisValue HysteresisFilter::apply(AxisValue value) const { if (_signaled) { if (value.value <= _min) { @@ -30,7 +29,7 @@ AxisValue HysteresisFilter::apply(AxisValue value) const { _signaled = true; } } - return { _signaled ? 1.0f : 0.0f, value.timestamp }; + return { _signaled ? 1.0f : 0.0f, value.timestamp, value.valid }; } bool HysteresisFilter::parseParameters(const QJsonValue& parameters) { diff --git a/libraries/controllers/src/controllers/impl/filters/NotFilter.cpp b/libraries/controllers/src/controllers/impl/filters/NotFilter.cpp index c0396857e5..dd181b7857 100644 --- a/libraries/controllers/src/controllers/impl/filters/NotFilter.cpp +++ b/libraries/controllers/src/controllers/impl/filters/NotFilter.cpp @@ -6,5 +6,5 @@ NotFilter::NotFilter() { } AxisValue NotFilter::apply(AxisValue value) const { - return { (value.value == 0.0f) ? 1.0f : 0.0f, value.timestamp }; + return { (value.value == 0.0f) ? 1.0f : 0.0f, value.timestamp, value.valid }; } diff --git a/libraries/controllers/src/controllers/impl/filters/PulseFilter.cpp b/libraries/controllers/src/controllers/impl/filters/PulseFilter.cpp index d37eb99ca9..353bf7dca9 100644 --- a/libraries/controllers/src/controllers/impl/filters/PulseFilter.cpp +++ b/libraries/controllers/src/controllers/impl/filters/PulseFilter.cpp @@ -29,7 +29,7 @@ AxisValue PulseFilter::apply(AxisValue value) const { _lastEmitTime = DEFAULT_LAST_EMIT_TIME; } - return { result, value.timestamp }; + return { result, value.timestamp, value.valid }; } bool PulseFilter::parseParameters(const QJsonValue& parameters) { diff --git a/libraries/controllers/src/controllers/impl/filters/ScaleFilter.h b/libraries/controllers/src/controllers/impl/filters/ScaleFilter.h index 3eb58e7f47..7c146d4e4a 100644 --- a/libraries/controllers/src/controllers/impl/filters/ScaleFilter.h +++ b/libraries/controllers/src/controllers/impl/filters/ScaleFilter.h @@ -23,7 +23,7 @@ public: ScaleFilter(float scale) : _scale(scale) {} virtual AxisValue apply(AxisValue value) const override { - return { value.value * _scale, value.timestamp }; + return { value.value * _scale, value.timestamp, value.valid }; } virtual Pose apply(Pose value) const override { diff --git a/libraries/entities-renderer/src/EntityTreeRenderer.cpp b/libraries/entities-renderer/src/EntityTreeRenderer.cpp index a8ef41643a..3837be5c9c 100644 --- a/libraries/entities-renderer/src/EntityTreeRenderer.cpp +++ b/libraries/entities-renderer/src/EntityTreeRenderer.cpp @@ -800,7 +800,7 @@ QUuid EntityTreeRenderer::mousePressEvent(QMouseEvent* event) { RayToEntityIntersectionResult rayPickResult = _getPrevRayPickResultOperator(_mouseRayPickID); EntityItemPointer entity; if (rayPickResult.intersects && (entity = getTree()->findEntityByID(rayPickResult.entityID))) { - if (!EntityTree::areEntityClicksCaptured()) { + if (!EntityTree::areEntityClicksCaptured() && event->button() == Qt::MouseButton::LeftButton) { auto properties = entity->getProperties(); QString urlString = properties.getHref(); QUrl url = QUrl(urlString, QUrl::StrictMode); diff --git a/libraries/entities-renderer/src/RenderableWebEntityItem.cpp b/libraries/entities-renderer/src/RenderableWebEntityItem.cpp index a1d24fe52e..b1feddfd47 100644 --- a/libraries/entities-renderer/src/RenderableWebEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableWebEntityItem.cpp @@ -24,6 +24,7 @@ #include #include #include +#include #include "EntitiesRendererLogging.h" #include @@ -180,14 +181,23 @@ void WebEntityRenderer::doRenderUpdateSynchronousTyped(const ScenePointer& scene } // This work must be done on the main thread + bool localSafeContext = entity->getLocalSafeContext(); if (!_webSurface) { + if (localSafeContext) { + ::hifi::scripting::setLocalAccessSafeThread(true); + } buildWebSurface(entity, newSourceURL); + ::hifi::scripting::setLocalAccessSafeThread(false); } if (_webSurface) { if (_webSurface->getRootItem()) { if (_contentType == ContentType::HtmlContent && _sourceURL != newSourceURL) { + if (localSafeContext) { + ::hifi::scripting::setLocalAccessSafeThread(true); + } _webSurface->getRootItem()->setProperty(URL_PROPERTY, newSourceURL); + ::hifi::scripting::setLocalAccessSafeThread(false); _sourceURL = newSourceURL; } else if (_contentType != ContentType::HtmlContent) { _sourceURL = newSourceURL; diff --git a/libraries/entities/src/EntitySimulation.cpp b/libraries/entities/src/EntitySimulation.cpp index b5e4fed0fd..9f81572a4a 100644 --- a/libraries/entities/src/EntitySimulation.cpp +++ b/libraries/entities/src/EntitySimulation.cpp @@ -242,11 +242,21 @@ void EntitySimulation::moveSimpleKinematics(uint64_t now) { entity->getMaximumAACube(ancestryIsKnown); bool hasAvatarAncestor = entity->hasAncestorOfType(NestableType::Avatar); - if (entity->isMovingRelativeToParent() && !entity->getPhysicsInfo() && ancestryIsKnown && !hasAvatarAncestor) { + bool isMoving = entity->isMovingRelativeToParent(); + if (isMoving && !entity->getPhysicsInfo() && ancestryIsKnown && !hasAvatarAncestor) { entity->simulate(now); + if (ancestryIsKnown && !hasAvatarAncestor) { + entity->updateQueryAACube(); + } _entitiesToSort.insert(entity); ++itemItr; } else { + if (!isMoving && ancestryIsKnown && !hasAvatarAncestor) { + // HACK: This catches most cases where the entity's QueryAACube (and spatial sorting in the EntityTree) + // would otherwise be out of date at conclusion of its "unowned" simpleKinematicMotion. + entity->updateQueryAACube(); + _entitiesToSort.insert(entity); + } // the entity is no longer non-physical-kinematic itemItr = _simpleKinematicEntities.erase(itemItr); } diff --git a/libraries/entities/src/WebEntityItem.cpp b/libraries/entities/src/WebEntityItem.cpp index 186a8fa8b4..a62f599e4c 100644 --- a/libraries/entities/src/WebEntityItem.cpp +++ b/libraries/entities/src/WebEntityItem.cpp @@ -15,6 +15,7 @@ #include #include +#include #include "EntitiesLogging.h" #include "EntityItemProperties.h" @@ -31,6 +32,9 @@ EntityItemPointer WebEntityItem::factory(const EntityItemID& entityID, const Ent } WebEntityItem::WebEntityItem(const EntityItemID& entityItemID) : EntityItem(entityItemID) { + // this initialzation of localSafeContext is reading a thread-local variable and that is depends on + // the ctor being executed on the same thread as the script, assuming it's being create by a script + _localSafeContext = hifi::scripting::isLocalAccessSafeThread(); _type = EntityTypes::Web; } @@ -241,6 +245,12 @@ glm::u8vec3 WebEntityItem::getColor() const { }); } +bool WebEntityItem::getLocalSafeContext() const { + return resultWithReadLock([&] { + return _localSafeContext; + }); +} + void WebEntityItem::setAlpha(float alpha) { withWriteLock([&] { _needsRenderUpdate |= _alpha != alpha; diff --git a/libraries/entities/src/WebEntityItem.h b/libraries/entities/src/WebEntityItem.h index bb1e527712..b61e2b124f 100644 --- a/libraries/entities/src/WebEntityItem.h +++ b/libraries/entities/src/WebEntityItem.h @@ -74,6 +74,8 @@ public: void setScriptURL(const QString& value); QString getScriptURL() const; + bool getLocalSafeContext() const; + static const uint8_t DEFAULT_MAX_FPS; void setMaxFPS(uint8_t value); uint8_t getMaxFPS() const; @@ -98,6 +100,7 @@ protected: uint8_t _maxFPS; WebInputMode _inputMode; bool _showKeyboardFocusHighlight; + bool _localSafeContext { false }; }; #endif // hifi_WebEntityItem_h diff --git a/libraries/hfm/src/hfm/HFM.h b/libraries/hfm/src/hfm/HFM.h index 484a10aa3b..888e562bca 100644 --- a/libraries/hfm/src/hfm/HFM.h +++ b/libraries/hfm/src/hfm/HFM.h @@ -56,7 +56,7 @@ const int MAX_NUM_PIXELS_FOR_FBX_TEXTURE = 2048 * 2048; using ShapeVertices = std::vector; // The version of the Draco mesh binary data itself. See also: FBX_DRACO_MESH_VERSION in FBX.h -static const int DRACO_MESH_VERSION = 2; +static const int DRACO_MESH_VERSION = 3; static const int DRACO_BEGIN_CUSTOM_HIFI_ATTRIBUTES = 1000; static const int DRACO_ATTRIBUTE_MATERIAL_ID = DRACO_BEGIN_CUSTOM_HIFI_ATTRIBUTES; diff --git a/libraries/model-baker/src/model-baker/Baker.cpp b/libraries/model-baker/src/model-baker/Baker.cpp index c896613df5..47a8db82b8 100644 --- a/libraries/model-baker/src/model-baker/Baker.cpp +++ b/libraries/model-baker/src/model-baker/Baker.cpp @@ -120,7 +120,7 @@ namespace baker { class BakerEngineBuilder { public: using Input = VaryingSet3; - using Output = VaryingSet4, std::vector>>; + using Output = VaryingSet5, std::vector, std::vector>>; using JobModel = Task::ModelIO; void build(JobModel& model, const Varying& input, Varying& output) { const auto& hfmModelIn = input.getN(0); @@ -168,7 +168,8 @@ namespace baker { const auto buildDracoMeshInputs = BuildDracoMeshTask::Input(meshesIn, normalsPerMesh, tangentsPerMesh).asVarying(); const auto buildDracoMeshOutputs = model.addJob("BuildDracoMesh", buildDracoMeshInputs); const auto dracoMeshes = buildDracoMeshOutputs.getN(0); - const auto materialList = buildDracoMeshOutputs.getN(1); + const auto dracoErrors = buildDracoMeshOutputs.getN(1); + const auto materialList = buildDracoMeshOutputs.getN(2); // Parse flow data const auto flowData = model.addJob("ParseFlowData", mapping); @@ -181,7 +182,7 @@ namespace baker { const auto buildModelInputs = BuildModelTask::Input(hfmModelIn, meshesOut, jointsOut, jointRotationOffsets, jointIndices, flowData).asVarying(); const auto hfmModelOut = model.addJob("BuildModel", buildModelInputs); - output = Output(hfmModelOut, materialMapping, dracoMeshes, materialList); + output = Output(hfmModelOut, materialMapping, dracoMeshes, dracoErrors, materialList); } }; @@ -212,7 +213,11 @@ namespace baker { return _engine->getOutput().get().get2(); } - std::vector> Baker::getDracoMaterialLists() const { + std::vector Baker::getDracoErrors() const { return _engine->getOutput().get().get3(); } + + std::vector> Baker::getDracoMaterialLists() const { + return _engine->getOutput().get().get4(); + } }; diff --git a/libraries/model-baker/src/model-baker/Baker.h b/libraries/model-baker/src/model-baker/Baker.h index 6f74cb646e..9780484fa4 100644 --- a/libraries/model-baker/src/model-baker/Baker.h +++ b/libraries/model-baker/src/model-baker/Baker.h @@ -33,6 +33,7 @@ namespace baker { hfm::Model::Pointer getHFMModel() const; MaterialMapping getMaterialMapping() const; const std::vector& getDracoMeshes() const; + std::vector getDracoErrors() const; // This is a ByteArray and not a std::string because the character sequence can contain the null character (particularly for FBX materials) std::vector> getDracoMaterialLists() const; diff --git a/libraries/model-baker/src/model-baker/BuildDracoMeshTask.cpp b/libraries/model-baker/src/model-baker/BuildDracoMeshTask.cpp index 25a45cefe5..12347c30b1 100644 --- a/libraries/model-baker/src/model-baker/BuildDracoMeshTask.cpp +++ b/libraries/model-baker/src/model-baker/BuildDracoMeshTask.cpp @@ -51,7 +51,7 @@ std::vector createMaterialList(const hfm::Mesh& mesh) { return materialList; } -std::unique_ptr createDracoMesh(const hfm::Mesh& mesh, const std::vector& normals, const std::vector& tangents, const std::vector& materialList) { +std::tuple, bool> createDracoMesh(const hfm::Mesh& mesh, const std::vector& normals, const std::vector& tangents, const std::vector& materialList) { Q_ASSERT(normals.size() == 0 || (int)normals.size() == mesh.vertices.size()); Q_ASSERT(mesh.colors.size() == 0 || mesh.colors.size() == mesh.vertices.size()); Q_ASSERT(mesh.texCoords.size() == 0 || mesh.texCoords.size() == mesh.vertices.size()); @@ -68,7 +68,7 @@ std::unique_ptr createDracoMesh(const hfm::Mesh& mesh, const std::v } if (numTriangles == 0) { - return std::unique_ptr(); + return std::make_tuple(std::unique_ptr(), false); } draco::TriangleSoupMeshBuilder meshBuilder; @@ -184,7 +184,7 @@ std::unique_ptr createDracoMesh(const hfm::Mesh& mesh, const std::v if (!dracoMesh) { qCWarning(model_baker) << "Failed to finalize the baking of a draco Geometry node"; - return std::unique_ptr(); + return std::make_tuple(std::unique_ptr(), true); } // we need to modify unique attribute IDs for custom attributes @@ -201,7 +201,7 @@ std::unique_ptr createDracoMesh(const hfm::Mesh& mesh, const std::v dracoMesh->attribute(originalIndexAttributeID)->set_unique_id(DRACO_ATTRIBUTE_ORIGINAL_INDEX); } - return dracoMesh; + return std::make_tuple(std::move(dracoMesh), false); } #endif // not Q_OS_ANDROID @@ -218,9 +218,13 @@ void BuildDracoMeshTask::run(const baker::BakeContextPointer& context, const Inp const auto& normalsPerMesh = input.get1(); const auto& tangentsPerMesh = input.get2(); auto& dracoBytesPerMesh = output.edit0(); - auto& materialLists = output.edit1(); + auto& dracoErrorsPerMesh = output.edit1(); + auto& materialLists = output.edit2(); dracoBytesPerMesh.reserve(meshes.size()); + // vector is an exception to the std::vector conventions as it is a bit field + // So a bool reference to an element doesn't work + dracoErrorsPerMesh.resize(meshes.size()); materialLists.reserve(meshes.size()); for (size_t i = 0; i < meshes.size(); i++) { const auto& mesh = meshes[i]; @@ -231,7 +235,10 @@ void BuildDracoMeshTask::run(const baker::BakeContextPointer& context, const Inp materialLists.push_back(createMaterialList(mesh)); const auto& materialList = materialLists.back(); - auto dracoMesh = createDracoMesh(mesh, normals, tangents, materialList); + bool dracoError; + std::unique_ptr dracoMesh; + std::tie(dracoMesh, dracoError) = createDracoMesh(mesh, normals, tangents, materialList); + dracoErrorsPerMesh[i] = dracoError; if (dracoMesh) { draco::Encoder encoder; diff --git a/libraries/model-baker/src/model-baker/BuildDracoMeshTask.h b/libraries/model-baker/src/model-baker/BuildDracoMeshTask.h index 0e33be3c41..ac9ad648ab 100644 --- a/libraries/model-baker/src/model-baker/BuildDracoMeshTask.h +++ b/libraries/model-baker/src/model-baker/BuildDracoMeshTask.h @@ -34,7 +34,7 @@ class BuildDracoMeshTask { public: using Config = BuildDracoMeshConfig; using Input = baker::VaryingSet3, baker::NormalsPerMesh, baker::TangentsPerMesh>; - using Output = baker::VaryingSet2, std::vector>>; + using Output = baker::VaryingSet3, std::vector, std::vector>>; using JobModel = baker::Job::ModelIO; void configure(const Config& config); diff --git a/libraries/networking/src/AssetClient.cpp b/libraries/networking/src/AssetClient.cpp index b9a3e6f61e..44f42caec2 100644 --- a/libraries/networking/src/AssetClient.cpp +++ b/libraries/networking/src/AssetClient.cpp @@ -85,6 +85,13 @@ namespace { const QString& CACHE_ERROR_MESSAGE{ "AssetClient::Error: %1 %2" }; } +/**jsdoc + * Cache status value returned by {@link Assets.getCacheStatus}. + * @typedef {object} Assets.GetCacheStatusResult + * @property {string} cacheDirectory - The path of the cache directory. + * @property {number} cacheSize - The current cache size, in bytes. + * @property {number} maximumCacheSize - The maximum cache size, in bytes. + */ MiniPromise::Promise AssetClient::cacheInfoRequestAsync(MiniPromise::Promise deferred) { if (!deferred) { deferred = makePromise(__FUNCTION__); // create on caller's thread @@ -106,6 +113,20 @@ MiniPromise::Promise AssetClient::cacheInfoRequestAsync(MiniPromise::Promise def return deferred; } +/**jsdoc + * Information on an asset in the cache. Value returned by {@link Assets.queryCacheMeta} and included in the data returned by + * {@link Assets.loadFromCache}. + * @typedef {object} Assets.CacheItemMetaData + * @property {object} [attributes] - The attributes that are stored with this cache item. Not used. + * @property {Date} [expirationDate] - The date and time when the meta data expires. An invalid date means "never expires". + * @property {boolean} isValid - true if the item specified in the URL is in the cache, false if + * it isn't. + * @property {Date} [lastModified] - The date and time when the meta data was last modified. + * @property {object} [rawHeaders] - The raw headers that are set in the meta data. Not used. + * @property {boolean} [saveToDisk] - true if the cache item is allowed to be store on disk, + * false if it isn't. + * @property {string} [url|metaDataURL] - The ATP URL of the cached item. + */ MiniPromise::Promise AssetClient::queryCacheMetaAsync(const QUrl& url, MiniPromise::Promise deferred) { if (QThread::currentThread() != thread()) { QMetaObject::invokeMethod(this, "queryCacheMetaAsync", Q_ARG(const QUrl&, url), Q_ARG(MiniPromise::Promise, deferred)); @@ -202,6 +223,24 @@ namespace { } } +/**jsdoc + * Last-modified and expiry times for a cache item. + * @typedef {object} Assets.SaveToCacheHeaders + * @property {string} [expires] - The date and time the cache value expires, in the format: + * "ddd, dd MMM yyyy HH:mm:ss". The default value is an invalid date, representing "never expires". + * @property {string} [last-modified] - The date and time the cache value was last modified, in the format: + * "ddd, dd MMM yyyy HH:mm:ss". The default value is the current date and time. + */ +/**jsdoc + * Information on saving asset data to the cache with {@link Assets.saveToCache}. + * @typedef {object} Assets.SaveToCacheResult + * @property {number} [byteLength] - The size of the cached data, in bytes. + * @property {Date} [expirationDate] - The date and time that the cache item expires. An invalid date means "never expires". + * @property {Date} [lastModified] - The date and time that the cache item was last modified. + * @property {string} [metaDataURL] - The URL associated with the cache item. + * @property {boolean} [success] - true if the save to cache request was successful. + * @property {string} [url] - The URL associated with the cache item. + */ MiniPromise::Promise AssetClient::saveToCacheAsync(const QUrl& url, const QByteArray& data, const QVariantMap& headers, MiniPromise::Promise deferred) { if (!deferred) { deferred = makePromise(__FUNCTION__); // create on caller's thread diff --git a/libraries/networking/src/BaseAssetScriptingInterface.cpp b/libraries/networking/src/BaseAssetScriptingInterface.cpp index b231339e51..2a98dbf3c3 100644 --- a/libraries/networking/src/BaseAssetScriptingInterface.cpp +++ b/libraries/networking/src/BaseAssetScriptingInterface.cpp @@ -68,6 +68,17 @@ Promise BaseAssetScriptingInterface::queryCacheMeta(const QUrl& url) { return assetClient()->queryCacheMetaAsync(url, makePromise(__FUNCTION__)); } +/**jsdoc + * Data and information returned by {@link Assets.loadFromCache}. + * @typedef {object} Assets.LoadFromCacheResult + * @property {number} [byteLength] - The number of bytes in the retrieved data. + * @property {string} [contentType] - The automatically detected MIME type of the content. + * @property {ArrayBuffer} data - The data bytes. + * @property {Assets.CacheItemMetaData} metadata - Information on the cache item. + * @property {string|object|ArrayBuffer} [response] - The content of the response. + * @property {Assets.ResponseType} responseType - The type of the content in response. + * @property {string} url - The URL of the cache item. + */ Promise BaseAssetScriptingInterface::loadFromCache(const QUrl& url, bool decompress, const QString& responseType) { QVariantMap metaData = { { "_type", "cache" }, diff --git a/libraries/networking/src/BaseAssetScriptingInterface.h b/libraries/networking/src/BaseAssetScriptingInterface.h index 497f627421..7d118e1979 100644 --- a/libraries/networking/src/BaseAssetScriptingInterface.h +++ b/libraries/networking/src/BaseAssetScriptingInterface.h @@ -24,6 +24,22 @@ class BaseAssetScriptingInterface : public QObject { Q_OBJECT public: + + /**jsdoc + *

Types of response that {@link Assets.decompressData}, {@link Assets.getAsset}, or {@link Assets.loadFromCache} may + * provide.

+ * + * + * + * + * + * + * + * + * + *
ValueDescription
"arraybuffer"A binary ArrayBuffer object.
"json"A parsed JSON object.
"text"UTF-8 decoded string value.
+ * @typedef {string} Assets.ResponseType + */ const QStringList RESPONSE_TYPES{ "text", "arraybuffer", "json" }; using Promise = MiniPromise::Promise; QSharedPointer assetClient(); @@ -33,51 +49,62 @@ public: public slots: /**jsdoc + * Checks whether a string is a valid path. Note: A valid path must start with a "/". * @function Assets.isValidPath - * @param {string} input - * @returns {boolean} + * @param {string} path - The path to check. + * @returns {boolean} true if the path is a valid path, false if it isn't. */ bool isValidPath(QString input) { return AssetUtils::isValidPath(input); } /**jsdoc + * Checks whether a string is a valid path and filename. Note: A valid path and filename must start with a "/" + * but must not end with a "/". * @function Assets.isValidFilePath - * @param {string} input - * @returns {boolean} + * @param {string} path - The path to check. + * @returns {boolean} true if the path is a valid file path, false if it isn't. */ bool isValidFilePath(QString input) { return AssetUtils::isValidFilePath(input); } /**jsdoc + * Gets the normalized ATP URL for a path or hash: ensures that it has "atp:" at the start. * @function Assets.getATPUrl - * @param {string} input - * @returns {string} + * @param {string} url - The URL to normalize. + * @returns {string} The normalized ATP URL. */ QUrl getATPUrl(QString input) { return AssetUtils::getATPUrl(input); } /**jsdoc + * Gets the SHA256 hexadecimal hash portion of an asset server URL. * @function Assets.extractAssetHash - * @param {string} input - * @returns {string} + * @param {string} url - The URL to get the SHA256 hexadecimal hash from. + * @returns {string} The SHA256 hexadecimal hash portion of the URL if present and valid, "" otherwise. */ QString extractAssetHash(QString input) { return AssetUtils::extractAssetHash(input); } /**jsdoc + * Checks whether a string is a valid SHA256 hexadecimal hash, i.e., 64 hexadecimal characters. * @function Assets.isValidHash - * @param {string} input - * @returns {boolean} + * @param {string} hash - The hash to check. + * @returns {boolean} true if the hash is a valid SHA256 hexadecimal string, false if it isn't. */ bool isValidHash(QString input) { return AssetUtils::isValidHash(input); } /**jsdoc + * Calculates the SHA256 hash of given data. * @function Assets.hashData - * @param {} data - * @returns {object} + * @param {string|ArrayBuffer} data - The data to calculate the hash of. + * @returns {ArrayBuffer} The SHA256 hash of the data. */ QByteArray hashData(const QByteArray& data) { return AssetUtils::hashData(data); } /**jsdoc + * Calculates the SHA256 hash of given data, in hexadecimal format. * @function Assets.hashDataHex - * @param {} data - * @returns {string} + * @param {string|ArrayBuffer} data - The data to calculate the hash of. + * @returns {string} The SHA256 hash of the data, in hexadecimal format. + * @example Calculate the hash of some text. + * var text = "Hello world!"; + * print("Hash: " + Assets.hashDataHex(text)); */ QString hashDataHex(const QByteArray& data) { return hashData(data).toHex(); } diff --git a/libraries/networking/src/udt/Socket.cpp b/libraries/networking/src/udt/Socket.cpp index 4c01517346..20cb30dbd8 100644 --- a/libraries/networking/src/udt/Socket.cpp +++ b/libraries/networking/src/udt/Socket.cpp @@ -546,7 +546,6 @@ void Socket::handleStateChanged(QAbstractSocket::SocketState socketState) { void Socket::handleRemoteAddressChange(HifiSockAddr previousAddress, HifiSockAddr currentAddress) { { Lock connectionsLock(_connectionsHashMutex); - _connectionsHash.erase(currentAddress); const auto connectionIter = _connectionsHash.find(previousAddress); if (connectionIter != _connectionsHash.end()) { @@ -554,18 +553,16 @@ void Socket::handleRemoteAddressChange(HifiSockAddr previousAddress, HifiSockAdd _connectionsHash.erase(connectionIter); connection->setDestinationAddress(currentAddress); _connectionsHash[currentAddress] = move(connection); - } - } + connectionsLock.unlock(); - { - Lock sequenceNumbersLock(_unreliableSequenceNumbersMutex); - _unreliableSequenceNumbers.erase(currentAddress); + Lock sequenceNumbersLock(_unreliableSequenceNumbersMutex); + const auto sequenceNumbersIter = _unreliableSequenceNumbers.find(previousAddress); + if (sequenceNumbersIter != _unreliableSequenceNumbers.end()) { + auto sequenceNumbers = sequenceNumbersIter->second; + _unreliableSequenceNumbers.erase(sequenceNumbersIter); + _unreliableSequenceNumbers[currentAddress] = sequenceNumbers; + } - const auto sequenceNumbersIter = _unreliableSequenceNumbers.find(previousAddress); - if (sequenceNumbersIter != _unreliableSequenceNumbers.end()) { - auto sequenceNumbers = sequenceNumbersIter->second; - _unreliableSequenceNumbers.erase(sequenceNumbersIter); - _unreliableSequenceNumbers[currentAddress] = sequenceNumbers; } } } diff --git a/libraries/physics/src/EntityMotionState.cpp b/libraries/physics/src/EntityMotionState.cpp index 68c8266e9f..e48f0603bd 100644 --- a/libraries/physics/src/EntityMotionState.cpp +++ b/libraries/physics/src/EntityMotionState.cpp @@ -84,48 +84,57 @@ EntityMotionState::~EntityMotionState() { } void EntityMotionState::updateServerPhysicsVariables() { - if (_ownershipState != EntityMotionState::OwnershipState::LocallyOwned) { - // only slam these values if we are NOT the simulation owner - Transform localTransform; - _entity->getLocalTransformAndVelocities(localTransform, _serverVelocity, _serverAngularVelocity); - _serverPosition = localTransform.getTranslation(); - _serverRotation = localTransform.getRotation(); - _serverAcceleration = _entity->getAcceleration(); - _serverActionData = _entity->getDynamicData(); - _lastStep = ObjectMotionState::getWorldSimulationStep(); - } + // only slam these values if we are NOT the simulation owner + Transform localTransform; + _entity->getLocalTransformAndVelocities(localTransform, _serverVelocity, _serverAngularVelocity); + _serverPosition = localTransform.getTranslation(); + _serverRotation = localTransform.getRotation(); + _serverAcceleration = _entity->getAcceleration(); + _serverActionData = _entity->getDynamicData(); + _lastStep = ObjectMotionState::getWorldSimulationStep(); } void EntityMotionState::handleDeactivation() { - if (_entity->getDirtyFlags() & (Simulation::DIRTY_TRANSFORM | Simulation::DIRTY_VELOCITIES)) { - // Some non-physical event (script-call or network-packet) has modified the entity's transform and/or velocities - // at the last minute before deactivation --> the values stored in _server* and _body are stale. - // We assume the EntityMotionState is the last to know, so we copy from EntityItem and let things sort themselves out. - Transform localTransform; - _entity->getLocalTransformAndVelocities(localTransform, _serverVelocity, _serverAngularVelocity); - _serverPosition = localTransform.getTranslation(); - _serverRotation = localTransform.getRotation(); - _serverAcceleration = _entity->getAcceleration(); - _serverActionData = _entity->getDynamicData(); - _lastStep = ObjectMotionState::getWorldSimulationStep(); - } else { - // copy _server data to entity - Transform localTransform = _entity->getLocalTransform(); - localTransform.setTranslation(_serverPosition); - localTransform.setRotation(_serverRotation); - _entity->setLocalTransformAndVelocities(localTransform, ENTITY_ITEM_ZERO_VEC3, ENTITY_ITEM_ZERO_VEC3); - // and also to RigidBody - btTransform worldTrans; - worldTrans.setOrigin(glmToBullet(_entity->getWorldPosition())); - worldTrans.setRotation(glmToBullet(_entity->getWorldOrientation())); - _body->setWorldTransform(worldTrans); - // no need to update velocities... should already be zero - } + if (_entity->getDirtyFlags() & (Simulation::DIRTY_TRANSFORM | Simulation::DIRTY_VELOCITIES)) { + // Some non-physical event (script-call or network-packet) has modified the entity's transform and/or + // velocities at the last minute before deactivation --> the values stored in _server* and _body are stale. + // We assume the EntityMotionState is the last to know, so we copy from EntityItem to _server* variables + // here but don't clear the flags --> the will body be set straight before next simulation step. + updateServerPhysicsVariables(); + } else if (_body->isStaticOrKinematicObject() && _ownershipState != EntityMotionState::OwnershipState::LocallyOwned) { + // To allow the ESS to move entities around in a kinematic way we had to remove the requirement that + // every moving+simulated entity has an authoritative simulation owner. As a result, we cannot rely + // on a final authoritative update of kinmatic objects prior to deactivation in the local simulation. + // For this case (unowned kinematic objects) we update the _server* variables for good measure but + // leave the entity and body alone. They should have been updated correctly in the last call to + // EntityMotionState::getWorldTransform(). + updateServerPhysicsVariables(); + } else { + // copy _server data to entity + Transform localTransform = _entity->getLocalTransform(); + localTransform.setTranslation(_serverPosition); + localTransform.setRotation(_serverRotation); + _entity->setLocalTransformAndVelocities(localTransform, ENTITY_ITEM_ZERO_VEC3, ENTITY_ITEM_ZERO_VEC3); + // and also to RigidBody + btTransform worldTrans; + worldTrans.setOrigin(glmToBullet(_entity->getWorldPosition())); + worldTrans.setRotation(glmToBullet(_entity->getWorldOrientation())); + _body->setWorldTransform(worldTrans); + // no need to update velocities... should already be zero + } + if (!isLocallyOwned()) { + // HACK: To allow the ESS to move entities around in a kinematic way we had to remove the requirement that + // every moving+simulated entity has an authoritative simulation owner. As a result, we cannot rely + // on a simulation owner to update the QueryAACube on the entity-server. + _entity->updateQueryAACube(); + } } // virtual void EntityMotionState::handleEasyChanges(uint32_t& flags) { - updateServerPhysicsVariables(); + if (_ownershipState != EntityMotionState::OwnershipState::LocallyOwned) { + updateServerPhysicsVariables(); + } ObjectMotionState::handleEasyChanges(flags); if (flags & Simulation::DIRTY_SIMULATOR_ID) { diff --git a/libraries/physics/src/PhysicalEntitySimulation.cpp b/libraries/physics/src/PhysicalEntitySimulation.cpp index 85c53af10a..43ced16119 100644 --- a/libraries/physics/src/PhysicalEntitySimulation.cpp +++ b/libraries/physics/src/PhysicalEntitySimulation.cpp @@ -528,6 +528,8 @@ void PhysicalEntitySimulation::handleChangedMotionStates(const VectorOfMotionSta addOwnership(entityState); } else if (entityState->shouldSendBid()) { addOwnershipBid(entityState); + } else { + entityState->getEntity()->updateQueryAACube(); } } } diff --git a/libraries/render-utils/src/AnimDebugDraw.cpp b/libraries/render-utils/src/AnimDebugDraw.cpp index bf528ee5f0..c4020cb4c4 100644 --- a/libraries/render-utils/src/AnimDebugDraw.cpp +++ b/libraries/render-utils/src/AnimDebugDraw.cpp @@ -393,7 +393,7 @@ void AnimDebugDraw::update() { glm::quat rot = std::get<0>(iter.second); glm::vec3 pos = std::get<1>(iter.second); glm::vec4 color = std::get<2>(iter.second); - const float radius = POSE_RADIUS; + const float radius = std::get<3>(iter.second) * POSE_RADIUS; addBone(AnimPose::identity, AnimPose(glm::vec3(1), rot, pos), radius, color, v); } @@ -402,7 +402,7 @@ void AnimDebugDraw::update() { glm::quat rot = std::get<0>(iter.second); glm::vec3 pos = std::get<1>(iter.second); glm::vec4 color = std::get<2>(iter.second); - const float radius = POSE_RADIUS; + const float radius = std::get<3>(iter.second) * POSE_RADIUS; addBone(myAvatarPose, AnimPose(glm::vec3(1), rot, pos), radius, color, v); } diff --git a/libraries/script-engine/src/AssetScriptingInterface.cpp b/libraries/script-engine/src/AssetScriptingInterface.cpp index 689b4b89ea..54b570e256 100644 --- a/libraries/script-engine/src/AssetScriptingInterface.cpp +++ b/libraries/script-engine/src/AssetScriptingInterface.cpp @@ -81,6 +81,11 @@ void AssetScriptingInterface::setMapping(QString path, QString hash, QScriptValu setMappingRequest->start(); } +/**jsdoc + * The success or failure of an {@link Assets.downloadData} call. + * @typedef {object} Assets.DownloadDataError + * @property {string} errorMessage - "" if the download was successful, otherwise a description of the error. + */ void AssetScriptingInterface::downloadData(QString urlString, QScriptValue callback) { // FIXME: historically this API method failed silently when given a non-atp prefixed // urlString (or if the AssetRequest failed). @@ -219,20 +224,31 @@ void AssetScriptingInterface::deleteAsset(QScriptValue options, QScriptValue sco } /**jsdoc - * @typedef {string} Assets.GetOptions.ResponseType - *

Available responseType values for use with @{link Assets.getAsset} and @{link Assets.loadFromCache} configuration option.

- * - * - * - * - * - * - * - * - * - *
responseTypetypeof response value
"text"contents returned as utf-8 decoded String value
"arraybuffer"contents as a binary ArrayBuffer object
"json"contents as a parsed JSON object
+ * Source and download options for {@link Assets.getAsset}. + * @typedef {object} Assets.GetOptions + * @property {boolean} [decompress=false] - true to gunzip decompress the downloaded data. Synonym: + * compressed. + * @property {Assets.ResponseType} [responseType="text"] - The desired result type. + * @property {string} url - The mapped path or hash to download. May have a leading "atp:". + */ +/**jsdoc + * Result value returned by {@link Assets.getAsset}. + * @typedef {object} Assets.GetResult + * @property {number} [byteLength] - The number of bytes in the downloaded content in response. + * @property {boolean} cached - true if the item was retrieved from the cache, false if it was + * downloaded. + * @property {string} [contentType] - The automatically detected MIME type of the content. + * @property {boolean} [decompressed] - true if the content was decompressed, false if it wasn't. + * @property {string} [hash] - The hash for the downloaded asset. + * @property {string} [hashURL] - The ATP URL of the hash file. + * @property {string} [path] - The path for the asset, if a path was requested. Otherwise, undefined. + * @property {string|object|ArrayBuffer} [response] - The downloaded content. + * @property {Assets.ResponseType} [responseType] - The type of the downloaded content in response. + * @property {string} [url] - The URL of the asset requested: the path with leading "atp:" if a path was + * requested, otherwise the requested URL. + * @property {boolean} [wasRedirected] - true if the downloaded data is the baked version of the asset, + * false if it isn't baked. */ - void AssetScriptingInterface::getAsset(QScriptValue options, QScriptValue scope, QScriptValue callback) { JS_VERIFY(options.isObject() || options.isString(), "expected request options Object or URL as first parameter"); @@ -283,6 +299,22 @@ void AssetScriptingInterface::getAsset(QScriptValue options, QScriptValue scope, } } +/**jsdoc + * Source options for {@link Assets.resolveAsset}. + * @typedef {object} Assets.ResolveOptions + * @property {string} url - The hash or path to resolve. May have a leading "atp:". + */ +/**jsdoc + * Result value returned by {@link Assets.resolveAsset}. + *

Note: If resolving a hash, a file of that hash need not be present on the asset server for the hash to resolve.

+ * @typedef {object} Assets.ResolveResult + * @property {string} [hash] - The hash of the asset. + * @property {string} [hashURL] - The url of the asset's hash file, with leading atp:. + * @property {string} [path] - The path to the asset. + * @property {string} [url] - The URL of the asset. + * @property {boolean} [wasRedirected] - true if the resolved data is for the baked version of the asset, + * false if it isn't. + */ void AssetScriptingInterface::resolveAsset(QScriptValue options, QScriptValue scope, QScriptValue callback) { const QString& URL{ "url" }; @@ -295,6 +327,21 @@ void AssetScriptingInterface::resolveAsset(QScriptValue options, QScriptValue sc jsPromiseReady(getAssetInfo(asset), scope, callback); } +/**jsdoc + * Content and decompression options for {@link Assets.decompressData}. + * @typedef {object} Assets.DecompressOptions + * @property {ArrayBuffer} data - The data to decompress. + * @property {Assets.ResponseType} [responseType=text] - The type of decompressed data to return. + */ +/**jsdoc + * Result value returned by {@link Assets.decompressData}. + * @typedef {object} Assets.DecompressResult + * @property {number} [byteLength] - The number of bytes in the decompressed data. + * @property {string} [contentType] - The MIME type of the decompressed data. + * @property {boolean} [decompressed] - true if the data is decompressed. + * @property {string|object|ArrayBuffer} [response] - The decompressed data. + * @property {Assets.ResponseType} [responseType] - The type of the decompressed data in response. + */ void AssetScriptingInterface::decompressData(QScriptValue options, QScriptValue scope, QScriptValue callback) { auto data = options.property("data"); QByteArray dataByteArray = qscriptvalue_cast(data); @@ -319,6 +366,23 @@ namespace { const int32_t DEFAULT_GZIP_COMPRESSION_LEVEL = -1; const int32_t MAX_GZIP_COMPRESSION_LEVEL = 9; } + +/**jsdoc + * Content and compression options for {@link Assets.compressData}. + * @typedef {object} Assets.CompressOptions + * @property {string|ArrayBuffer} data - The data to compress. + * @property {number} level - The compression level, range -19. -1 means + * use the default gzip compression level, 0 means no compression, and 9 means maximum + * compression. + */ +/**jsdoc + * Result value returned by {@link Assets.compressData}. + * @typedef {object} Assets.CompressResult + * @property {number} [byteLength] - The number of bytes in the compressed data. + * @property {boolean} [compressed] - true if the data is compressed. + * @property {string} [contentType] - The MIME type of the compressed data, i.e., "application/gzip". + * @property {ArrayBuffer} [data] - The compressed data. + */ void AssetScriptingInterface::compressData(QScriptValue options, QScriptValue scope, QScriptValue callback) { auto data = options.property("data").isValid() ? options.property("data") : options; QByteArray dataByteArray = data.isString() ? data.toString().toUtf8() : qscriptvalue_cast(data); @@ -327,6 +391,27 @@ void AssetScriptingInterface::compressData(QScriptValue options, QScriptValue sc jsPromiseReady(compressBytes(dataByteArray, level), scope, callback); } +/**jsdoc + * Content and upload options for {@link Assets.putAsset}. + * @typedef {object} Assets.PutOptions + * @property {boolean} [compress=false] - true to gzip compress the content for upload and storage, + * false to upload and store the data without gzip compression. Synonym: compressed. + * @property {string|ArrayBuffer} data - The content to upload. + * @property {string} [path] - A user-friendly path for the file in the asset server. May have a leading + * "atp:". IF not specified, no path-to-hash mapping is set. + *

Note: The asset server destroys any unmapped SHA256-named file at server restart. Either set the mapping path + * with this property or use {@link Assets.setMapping} to set a path-to-hash mapping for the uploaded file.

+ */ +/**jsdoc + * Result value returned by {@link Assets.putAsset}. + * @typedef {object} Assets.PutResult + * @property {number} [byteLength] - The number of bytes in the hash file stored on the asset server. + * @property {boolean} [compressed] - true if the content stored is gzip compressed. + * @property {string} [contentType] - "application/gzip" if the content stored is gzip compressed. + * @property {string} [hash] - The SHA256 hash of the content. + * @property {string} [url] - The atp: URL of the content: using the path if specified, otherwise the hash. + * @property {string} [path] - The uploaded content's mapped path, if specified. + */ void AssetScriptingInterface::putAsset(QScriptValue options, QScriptValue scope, QScriptValue callback) { auto compress = options.property("compress").toBool() || options.property("compressed").toBool(); auto data = options.isObject() ? options.property("data") : options; @@ -377,12 +462,27 @@ void AssetScriptingInterface::putAsset(QScriptValue options, QScriptValue scope, } } +/**jsdoc + * Source for {@link Assets.queryCacheMeta}. + * @typedef {object} Assets.QueryCacheMetaOptions + * @property {string} url - The URL of the cached asset to get information on. Must start with "atp:" or + * "cache:". + */ void AssetScriptingInterface::queryCacheMeta(QScriptValue options, QScriptValue scope, QScriptValue callback) { QString url = options.isString() ? options.toString() : options.property("url").toString(); JS_VERIFY(QUrl(url).isValid(), QString("Invalid URL '%1'").arg(url)); jsPromiseReady(Parent::queryCacheMeta(url), scope, callback); } +/**jsdoc + * Source and retrieval options for {@link Assets.loadFromCache}. + * @typedef {object} Assets.LoadFromCacheOptions + * @property {boolean} [decompress=false] - true to gunzip decompress the cached data. Synonym: + * compressed. + * @property {Assets.ResponseType} [responseType=text] - The desired result type. + * @property {string} url - The URL of the asset to load from cache. Must start with "atp:" or + * "cache:". + */ void AssetScriptingInterface::loadFromCache(QScriptValue options, QScriptValue scope, QScriptValue callback) { QString url, responseType; bool decompress = false; @@ -417,6 +517,14 @@ bool AssetScriptingInterface::canWriteCacheValue(const QUrl& url) { return true; } +/**jsdoc + * The data to save to the cache and cache options for {@link Assets.saveToCache}. + * @typedef {object} Assets.SaveToCacheOptions + * @property {string|ArrayBuffer} data - The data to save to the cache. + * @property {Assets.SaveToCacheHeaders} [headers] - The last-modified and expiry times for the cache item. + * @property {string} [url] - The URL to associate with the cache item. Must start with "atp:" or + * "cache:". If not specified, the URL is "atp:" followed by the SHA256 hash of the content. + */ void AssetScriptingInterface::saveToCache(QScriptValue options, QScriptValue scope, QScriptValue callback) { JS_VERIFY(options.isObject(), QString("expected options object as first parameter not: %1").arg(options.toVariant().typeName())); diff --git a/libraries/script-engine/src/AssetScriptingInterface.h b/libraries/script-engine/src/AssetScriptingInterface.h index 07d681ca88..5da3c51a08 100644 --- a/libraries/script-engine/src/AssetScriptingInterface.h +++ b/libraries/script-engine/src/AssetScriptingInterface.h @@ -25,7 +25,14 @@ #include /**jsdoc - * The Assets API allows you to communicate with the Asset Browser. + * The Assets API provides facilities for interacting with the domain's asset server and the client cache. + *

Assets are stored in the asset server in files with SHA256 names. These files are mapped to user-friendly URLs of the + * format: atp:/path/filename. The assets may optionally be baked, in which case a request for the original + * unbaked version of the asset is automatically redirected to the baked version. The asset data may optionally be stored as + * compressed.

+ *

The client cache can be access directly, using "atp:" or "cache:" URLs. Interface, avatar, and + * assignment client scripts can write to the cache. All script types can read from the cache.

+ * * @namespace Assets * * @hifi-interface @@ -41,251 +48,490 @@ public: AssetScriptingInterface(QObject* parent = nullptr); /**jsdoc - * Upload content to the connected domain's asset server. - * @function Assets.uploadData - * @static - * @param data {string} content to upload - * @param callback {Assets~uploadDataCallback} called when upload is complete + * Called when an {@link Assets.uploadData} call is complete. + * @callback Assets~uploadDataCallback + * @param {string} url - The raw URL of the file that the content is stored in, with atp: as the scheme and + * the SHA256 hash as the filename (with no extension). + * @param {string} hash - The SHA256 hash of the content. */ /**jsdoc - * Called when uploadData is complete - * @callback Assets~uploadDataCallback - * @param {string} url - * @param {string} hash + * Uploads content to the asset server, storing it in a SHA256-named file. + *

Note: The asset server destroys any unmapped SHA256-named file at server restart. Use {@link Assets.setMapping} to + * set a path-to-hash mapping for the new file.

+ * @function Assets.uploadData + * @param {string} data - The content to upload. + * @param {Assets~uploadDataCallback} callback - The function to call upon completion. + * @example Store a string in the asset server. + * Assets.uploadData("Hello world!", function (url, hash) { + * print("URL: " + url); // atp:0a1b...9g + * Assets.setMapping("/assetsExamples/helloWorld.txt", hash, function (error) { + * if (error) { + * print("ERROR: Could not set mapping!"); + * return; + * } + * }); + * }); */ Q_INVOKABLE void uploadData(QString data, QScriptValue callback); /**jsdoc - * Download data from the connected domain's asset server. - * @function Assets.downloadData - * @param url {string} URL of asset to download, must be ATP scheme URL. - * @param callback {Assets~downloadDataCallback} + * Called when an {@link Assets.downloadData} call is complete. + * @callback Assets~downloadDataCallback + * @param {string} data - The content that was downloaded. + * @param {Assets.DownloadDataError} error - The success or failure of the download. */ /**jsdoc - * Called when downloadData is complete - * @callback Assets~downloadDataCallback - * @param data {string} content that was downloaded + * Downloads content from the asset server, from a SHA256-named file. + * @function Assets.downloadData + * @param {string} url - The raw URL of asset to download: atp: followed by the assets's SHA256 hash. + * @param {Assets~downloadDataCallback} callback - The function to call upon completion. + * @example Store and retrieve a string from the asset server. + * var assetURL; + * + * // Store the string. + * Assets.uploadData("Hello world!", function (url, hash) { + * assetURL = url; + * print("url: " + assetURL); // atp:a0g89... + * Assets.setMapping("/assetsExamples/helloWorld.txt", hash, function (error) { + * if (error) { + * print("ERROR: Could not set mapping!"); + * return; + * } + * }); + * }); + * + * // Retrieve the string. + * Script.setTimeout(function () { + * Assets.downloadData(assetURL, function (data, error) { + * print("Downloaded data: " + data); + * print("Error: " + JSON.stringify(error)); + * }); + * }, 1000); */ - Q_INVOKABLE void downloadData(QString url, QScriptValue downloadComplete); + Q_INVOKABLE void downloadData(QString url, QScriptValue callback); /**jsdoc - * Sets up a path to hash mapping within the connected domain's asset server - * @function Assets.setMapping - * @param path {string} - * @param hash {string} - * @param callback {Assets~setMappingCallback} + * Called when an {@link Assets.setMapping} call is complete. + * @callback Assets~setMappingCallback + * @param {string} error - null if the path-to-hash mapping was set, otherwise a description of the error. */ /**jsdoc - * Called when setMapping is complete - * @callback Assets~setMappingCallback - * @param {string} error + * Sets a path-to-hash mapping within the asset server. + * @function Assets.setMapping + * @param {string} path - A user-friendly path for the file in the asset server, without leading "atp:". + * @param {string} hash - The hash in the asset server. + * @param {Assets~setMappingCallback} callback - The function to call upon completion. */ Q_INVOKABLE void setMapping(QString path, QString hash, QScriptValue callback); /**jsdoc - * Look up a path to hash mapping within the connected domain's asset server - * @function Assets.getMapping - * @param path {string} - * @param callback {Assets~getMappingCallback} + * Called when an {@link Assets.getMapping} call is complete. + * @callback Assets~getMappingCallback + * @param {string} error - null if the path was found, otherwise a description of the error. + * @param {string} hash - The hash value if the path was found, "" if it wasn't. */ /**jsdoc - * Called when getMapping is complete. - * @callback Assets~getMappingCallback - * @param assetID {string} hash value if found, else an empty string - * @param error {string} error description if the path could not be resolved; otherwise a null value. + * Gets the hash for a path within the asset server. The hash is for the unbaked or baked version of the + * asset, according to the asset server setting for the particular path. + * @function Assets.getMapping + * @param {string} path - The path to a file in the asset server to get the hash of. + * @param {Assets~getMappingCallback} callback - The function to call upon completion. + * @example Report the hash of an asset server item. + * var assetPath = Window.browseAssets(); + * if (assetPath) { + * var mapping = Assets.getMapping(assetPath, function (error, hash) { + * print("Asset: " + assetPath); + * print("- hash: " + hash); + * print("- error: " + error); + * }); + * } */ Q_INVOKABLE void getMapping(QString path, QScriptValue callback); /**jsdoc - * @function Assets.setBakingEnabled - * @param path {string} - * @param enabled {boolean} - * @param callback {} + * Called when an {@link Assets.setBakingEnabled} call is complete. + * @callback Assets~setBakingEnabledCallback + * @param {string} error - null if baking was successfully enabled or disabled, otherwise a description of the + * error. */ /**jsdoc - * Called when setBakingEnabled is complete. - * @callback Assets~setBakingEnabledCallback + * Sets whether or not to bake an asset in the asset server. + * @function Assets.setBakingEnabled + * @param {string} path - The path to a file in the asset server. + * @param {boolean} enabled - true to enable baking of the asset, false to disable. + * @param {Assets~setBakingEnabledCallback} callback - The function to call upon completion. */ + // Note: Second callback parameter not documented because it's always {}. Q_INVOKABLE void setBakingEnabled(QString path, bool enabled, QScriptValue callback); #if (PR_BUILD || DEV_BUILD) /** * This function is purely for development purposes, and not meant for use in a - * production context. It is not a public-facing API, so it should not contain jsdoc. + * production context. It is not a public-facing API, so it should not have JSDoc. */ Q_INVOKABLE void sendFakedHandshake(); #endif /**jsdoc - * Request Asset data from the ATP Server - * @function Assets.getAsset - * @param {URL|Assets.GetOptions} options An atp: style URL, hash, or relative mapped path; or an {@link Assets.GetOptions} object with request parameters - * @param {Assets~getAssetCallback} scope A scope callback function to receive (error, results) values - * @param {function} [callback=undefined] + * Details of a callback function. + * @typedef {object} Assets.CallbackDetails + * @property {object} scope - The scope that the callback function is defined in. This object is bound to + * this when the function is called. + * @property {Assets~compressDataCallback|Assets~decompressDataCallback|Assets~getAssetCallback + * |Assets~getCacheStatusCallback|Assets~loadFromCacheCallback|Assets~putAssetCallback|Assets~queryCacheMetaCallback + * |Assets~resolveAssetCallback|Assets~saveToCacheCallback} + * callback - The function to call upon completion. May be an inline function or a function identifier. If a function + * identifier, it must be a member of scope. */ /**jsdoc - * A set of properties that can be passed to {@link Assets.getAsset}. - * @typedef {object} Assets.GetOptions - * @property {string} [url] an "atp:" style URL, hash, or relative mapped path to fetch - * @property {string} [responseType=text] the desired reponse type (text | arraybuffer | json) - * @property {boolean} [decompress=false] whether to attempt gunzip decompression on the fetched data - * See: {@link Assets.putAsset} and its .compress=true option - */ - - /**jsdoc - * Called when Assets.getAsset is complete. + * Called when an {@link Assets.getAsset} call is complete. * @callback Assets~getAssetCallback - * @param {string} error - contains error message or null value if no error occured fetching the asset - * @param {Asset~getAssetResult} result - result object containing, on success containing asset metadata and contents + * @param {string} error - null if the content was downloaded, otherwise a description of the error. + * @param {Assets.GetResult} result - Information on and the content downloaded. */ - /**jsdoc - * Result value returned by {@link Assets.getAsset}. - * @typedef {object} Assets~getAssetResult - * @property {string} [url] the resolved "atp:" style URL for the fetched asset - * @property {string} [hash] the resolved hash for the fetched asset - * @property {string|ArrayBuffer|Object} [response] response data (possibly converted per .responseType value) - * @property {string} [responseType] response type (text | arraybuffer | json) - * @property {string} [contentType] detected asset mime-type (autodetected) - * @property {number} [byteLength] response data size in bytes - * @property {number} [decompressed] flag indicating whether data was decompressed + * Downloads content from the asset server. + * @function Assets.getAsset + * @param {string|Assets.GetOptions} source - What to download and download options. If a string, the mapped path or hash + * to download, optionally including a leading "atp:". + * @param {object|Assets.CallbackDetails|Assets~getAssetCallback} scopeOrCallback - If an object, then the scope that + * the callback function is defined in. This object is bound to this when the function is + * called. + *

Otherwise, the function to call upon completion. This may be an inline function or a function identifier.

+ * @param {Assets~getAssetCallback} [callback] - Used if scopeOrCallback specifies the scope. + *

The function to call upon completion. May be an inline function, a function identifier, or the name of a function + * in a string. If the name of a function or a function identifier, it must be a member of the scope specified by + * scopeOrCallback.

+ * @example Retrieve a string from the asset server. + * Assets.getAsset( + * { + * url: "/assetsExamples/helloWorld.txt", + * responseType: "text" + * }, + * function (error, result) { + * if (error) { + * print("ERROR: Data not downloaded"); + * } else { + * print("Data: " + result.response); + * } + * } + * ); */ - Q_INVOKABLE void getAsset(QScriptValue options, QScriptValue scope, QScriptValue callback = QScriptValue()); /**jsdoc - * Upload Asset data to the ATP Server + * Called when an {@link Assets.putAsset} call is complete. + * @callback Assets~putAssetCallback + * @param {string} error - null if the content was uploaded and any path-to-hash mapping set, otherwise a + * description of the error. + * @param {Assets.PutResult} result - Information on the content uploaded. + */ + /**jsdoc + * Uploads content to the asset server and sets a path-to-hash mapping. * @function Assets.putAsset - * @param {Assets.PutOptions} options A PutOptions object with upload parameters - * @param {Assets~putAssetCallback} scope[callback] A scoped callback function invoked with (error, results) - * @param {function} [callback=undefined] + * @param {string|Assets.PutOptions} options - The content to upload and upload options. If a string, the value of the + * string is uploaded but a path-to-hash mapping is not set. + * @param {object|Assets.CallbackDetails|Assets~putAssetCallback} scopeOrCallback - If an object, then the scope that + * the callback function is defined in. This object is bound to this when the function is + * called. + *

Otherwise, the function to call upon completion. This may be an inline function or a function identifier.

+ * @param {Assets~putAssetCallback} [callback] - Used if scopeOrCallback specifies the scope. + *

The function to call upon completion. May be an inline function, a function identifier, or the name of a function + * in a string. If the name of a function or a function identifier, it must be a member of the scope specified by + * scopeOrCallback.

+ * @example Store a string in the asset server. + * Assets.putAsset( + * { + * data: "Hello world!", + * path: "/assetsExamples/helloWorld.txt" + * }, + * function (error, result) { + * if (error) { + * print("ERROR: Data not uploaded or mapping not set"); + * } else { + * print("URL: " + result.url); // atp:/assetsExamples/helloWorld.txt + * } + * } + * ); */ - - /**jsdoc - * A set of properties that can be passed to {@link Assets.putAsset}. - * @typedef {object} Assets.PutOptions - * @property {ArrayBuffer|string} [data] byte buffer or string value representing the new asset's content - * @property {string} [path=null] ATP path mapping to automatically create (upon successful upload to hash) - * @property {boolean} [compress=false] whether to gzip compress data before uploading - */ - - /**jsdoc - * Called when Assets.putAsset is complete. - * @callback Assets~puttAssetCallback - * @param {string} error - contains error message (or null value if no error occured while uploading/mapping the new asset) - * @param {Asset~putAssetResult} result - result object containing error or result status of asset upload - */ - - /**jsdoc - * Result value returned by {@link Assets.putAsset}. - * @typedef {object} Assets~putAssetResult - * @property {string} [url] the resolved "atp:" style URL for the uploaded asset (based on .path if specified, otherwise on the resulting ATP hash) - * @property {string} [path] the uploaded asset's resulting ATP path (or undefined if no path mapping was assigned) - * @property {string} [hash] the uploaded asset's resulting ATP hash - * @property {boolean} [compressed] flag indicating whether the data was compressed before upload - * @property {number} [byteLength] flag indicating final byte size of the data uploaded to the ATP server - */ - Q_INVOKABLE void putAsset(QScriptValue options, QScriptValue scope, QScriptValue callback = QScriptValue()); /**jsdoc - * @function Assets.deleteAsset - * @param {} options - * @param {} scope - * @param {} [callback = ""] + * Called when an {@link Assets.deleteAsset} call is complete. + *

Not implemented: This type is not implemented yet.

+ * @callback Assets~deleteAssetCallback + * @param {string} error - null if the content was deleted, otherwise a description of the error. + * @param {Assets.DeleteResult} result - Information on the content deleted. */ - - Q_INVOKABLE void deleteAsset(QScriptValue options, QScriptValue scope, QScriptValue callback = QScriptValue()); - /**jsdoc - * @function Assets.resolveAsset - * @param {} options - * @param {} scope - * @param {} [callback = ""] + * Deletes content from the asset server. + *

Not implemented: This method is not implemented yet.

+ * @function Assets.deleteAsset + * @param {Assets.DeleteOptions} options - The content to delete and delete options. + * @param {object} scope - The scope that the callback function is defined in. + * @param {Assets~deleteAssetCallback} callback - The function to call upon completion. */ + Q_INVOKABLE void deleteAsset(QScriptValue options, QScriptValue scope, QScriptValue callback = QScriptValue()); + /**jsdoc + * Called when an {@link Assets.resolveAsset} call is complete. + * @callback Assets~resolveAssetCallback + * @param {string} error - null if the asset hash or path was resolved, otherwise a description of the error. + * @param {Assets.ResolveResult} result - Information on the hash or path resolved. + */ + /**jsdoc + * Resolves and returns information on a hash or a path in the asset server. + * @function Assets.resolveAsset + * @param {string|Assets.ResolveOptions} source - The hash or path to resolve if a string, otherwise an object specifying + * what to resolve. If a string, it may have a leading "atp:". + * @param {object|Assets.CallbackDetails|Assets~resolveAssetCallback} scopeOrCallback - If an object, then the scope that + * the callback function is defined in. This object is bound to this when the function is + * called. + *

Otherwise, the function to call upon completion. This may be an inline function or a function identifier.

+ * @param {Assets~resolveAssetCallback} [callback] - Used if scopeOrCallback specifies the scope. + *

The function to call upon completion. May be an inline function, a function identifier, or the name of a function + * in a string. If the name of a function or a function identifier, it must be a member of the scope specified by + * scopeOrCallback.

+ * @example Get the hash and URL for a path. + * Assets.resolveAsset( + * "/assetsExamples/helloWorld.txt", + * function (error, result) { + * if (error) { + * print("ERROR: " + error); + * } else { + * print("Hash: " + result.hash); + * print("URL: " + result.url); + * } + * } + * ); + */ Q_INVOKABLE void resolveAsset(QScriptValue options, QScriptValue scope, QScriptValue callback = QScriptValue()); /**jsdoc - * @function Assets.decompressData - * @param {} options - * @param {} scope - * @param {} [callback = ""] + * Called when an {@link Assets.decompressData} call is complete. + * @callback Assets~decompressDataCallback + * @param {string} error - null if the data was successfully compressed, otherwise a description of the error. + * @param {Assets.DecompressResult} result - Information on and the decompressed data. + */ + /**jsdoc + * Decompresses data in memory using gunzip. + * @function Assets.decompressData + * @param {Assets.DecompressOptions} source - What to decompress and decompression options. + * @param {object|Assets.CallbackDetails|Assets~decompressDataCallback} scopeOrCallback - If an object, then the scope that + * the callback function is defined in. This object is bound to this when the function is + * called. + *

Otherwise, the function to call upon completion. This may be an inline function or a function identifier.

+ * @param {Assets~decompressDataCallback} [callback] - Used if scopeOrCallback specifies the scope. + *

The function to call upon completion. May be an inline function, a function identifier, or the name of a function + * in a string. If the name of a function or a function identifier, it must be a member of the scope specified by + * scopeOrCallback.

*/ - Q_INVOKABLE void decompressData(QScriptValue options, QScriptValue scope, QScriptValue callback = QScriptValue()); /**jsdoc - * @function Assets.compressData - * @param {} options - * @param {} scope - * @param {} [callback = ""] + * Called when an {@link Assets.compressData} call is complete. + * @callback Assets~compressDataCallback + * @param {string} error - null if the data was successfully compressed, otherwise a description of the error. + * @param {Assets.CompressResult} result - Information on and the compressed data. + */ + /**jsdoc + * Compresses data in memory using gzip. + * @function Assets.compressData + * @param {string|ArrayBuffer|Assets.CompressOptions} source - What to compress and compression options. If a string or + * ArrayBuffer, the data to compress. + * @param {object|Assets.CallbackDetails|Assets~compressDataCallback} scopeOrCallback - If an object, then the scope that + * the callback function is defined in. This object is bound to this when the function is + * called. + *

Otherwise, the function to call upon completion. This may be an inline function or a function identifier.

+ * @param {Assets~compressDataCallback} [callback] - Used if scopeOrCallback specifies the scope. + *

The function to call upon completion. May be an inline function, a function identifier, or the name of a function + * in a string. If the name of a function or a function identifier, it must be a member of the scope specified by + * scopeOrCallback.

*/ - Q_INVOKABLE void compressData(QScriptValue options, QScriptValue scope, QScriptValue callback = QScriptValue()); /**jsdoc + * Initializes the cache if it isn't already initialized. * @function Assets.initializeCache - * @returns {boolean} + * @returns {boolean} true if the cache is initialized, false if it isn't. */ - Q_INVOKABLE bool initializeCache() { return Parent::initializeCache(); } /**jsdoc + * Checks whether the script can write to the cache. * @function Assets.canWriteCacheValue - * @param {string} url - * @returns {boolean} + * @param {string} url - Not used. + * @returns {boolean} true if the script is an Interface, avatar, or assignment client script, + * false if the script is a client entity or server entity script. + * @example Report whether the script can write to the cache. + * print("Can write to cache: " + Assets.canWriteCacheValue(null)); */ - Q_INVOKABLE bool canWriteCacheValue(const QUrl& url); /**jsdoc - * @function Assets.getCacheStatus - * @param {} scope - * @param {} [callback=undefined] + * Called when a {@link Assets.getCacheStatus} call is complete. + * @callback Assets~getCacheStatusCallback + * @param {string} error - null if the cache status was retrieved without error, otherwise a description of + * the error. + * @param {Assets.GetCacheStatusResult} result - Details of the current cache status. + */ + /**jsdoc + * Gets the current cache status. + * @function Assets.getCacheStatus + * @param {object|Assets.CallbackDetails|Assets~getCacheStatusCallback} scopeOrCallback - If an object, then the scope that + * the callback function is defined in. This object is bound to this when the function is + * called. + *

Otherwise, the function to call upon completion. This may be an inline function or a function identifier.

+ * @param {Assets~getCacheStatusCallback} [callback] - Used if scopeOrCallback specifies the scope. + *

The function to call upon completion. May be an inline function, a function identifier, or the name of a function + * in a string. If the name of a function or a function identifier, it must be a member of the scope specified by + * scopeOrCallback.

+ * @example Report the cache status. + * Assets.getCacheStatus(function (error, status) { + * print("Cache status"); + * print("- Error: " + error); + * print("- Status: " + JSON.stringify(status)); + * }); */ - Q_INVOKABLE void getCacheStatus(QScriptValue scope, QScriptValue callback = QScriptValue()) { jsPromiseReady(Parent::getCacheStatus(), scope, callback); } /**jsdoc - * @function Assets.queryCacheMeta - * @param {} options - * @param {} scope - * @param {} [callback=undefined] + * Called when {@link Assets.queryCacheMeta} is complete. + * @callback Assets~queryCacheMetaCallback + * @param {string} error - null if the URL has a valid cache entry, otherwise a description of the error. + * @param {Assets.CacheItemMetaData} result - Information on an asset in the cache. + */ + /**jsdoc + * Gets information about the status of an asset in the cache. + * @function Assets.queryCacheMeta + * @param {string|Assets.QueryCacheMetaOptions} path - The URL of the cached asset to get information on if a string, + * otherwise an object specifying the cached asset to get information on. The URL must start with "atp:" + * or "cache:". + * @param {object|Assets.CallbackDetails|Assets~queryCacheMetaCallback} scopeOrCallback - If an object, then the scope that + * the callback function is defined in. This object is bound to this when the function is + * called. + *

Otherwise, the function to call upon completion. This may be an inline function or a function identifier.

+ * @param {Assets~queryCacheMetaCallback} [callback] - Used if scopeOrCallback specifies the scope. + *

The function to call upon completion. May be an inline function, a function identifier, or the name of a function + * in a string. If the name of a function or a function identifier, it must be a member of the scope specified by + * scopeOrCallback.

+ * @example Report details of a string store in the cache. + * Assets.queryCacheMeta( + * "cache:/cacheExample/helloCache.txt", + * function (error, result) { + * if (error) { + * print("Error: " + error); + * } else { + * print("Success:"); + * print("- URL: " + result.url); + * print("- isValid: " + result.isValid); + * print("- saveToDisk: " + result.saveToDisk); + * print("- expirationDate: " + result.expirationDate); + * } + * } + * ); */ - Q_INVOKABLE void queryCacheMeta(QScriptValue options, QScriptValue scope, QScriptValue callback = QScriptValue()); /**jsdoc - * @function Assets.loadFromCache - * @param {} options - * @param {} scope - * @param {} [callback=undefined] + * Called when an {@link Assets.loadFromCache} call is complete. + * @callback Assets~loadFromCacheCallback + * @param {string} error - null if the cache item was successfully retrieved, otherwise a description of the + * error. + * @param {Assets.LoadFromCacheResult} result - Information on and the retrieved data. + */ + /**jsdoc + * Retrieves data from the cache directly, without downloading it. + * @function Assets.loadFromCache + * @param {string|Assets.LoadFromCacheOptions} options - The URL of the asset to load from the cache if a string, otherwise + * an object specifying the asset to load from the cache and load options. The URL must start with "atp:" + * or "cache:". + * @param {object|Assets.CallbackDetails|Assets~loadFromCacheCallback} scopeOrCallback - If an object, then the scope that + * the callback function is defined in. This object is bound to this when the function is + * called. + *

Otherwise, the function to call upon completion. This may be an inline function or a function identifier.

+ * @param {Assets~loadFromCacheCallback} [callback] - Used if scopeOrCallback specifies the scope. + *

The function to call upon completion. May be an inline function, a function identifier, or the name of a function + * in a string. If the name of a function or a function identifier, it must be a member of the scope specified by + * scopeOrCallback.

+ * @example Retrieve a string from the cache. + * Assets.loadFromCache( + * "cache:/cacheExample/helloCache.txt", + * function (error, result) { + * if (error) { + * print("Error: " + error); + * } else { + * print("Success:"); + * print("- Response: " + result.response); + * print("- Content type: " + result.contentType); + * print("- Number of bytes: " + result.byteLength); + * print("- Bytes: " + [].slice.call(new Uint8Array(result.data), 0, result.byteLength)); + * print("- URL: " + result.url); + * } + * } + * ); */ - Q_INVOKABLE void loadFromCache(QScriptValue options, QScriptValue scope, QScriptValue callback = QScriptValue()); /**jsdoc - * @function Assets.saveToCache - * @param {} options - * @param {} scope - * @param {} [callback=undefined] + * Called when an {@link Assets.saveToCache} call is complete. + * @callback Assets~saveToCacheCallback + * @param {string} error - null if the asset data was successfully saved to the cache, otherwise a description + * of the error. + * @param {Assets.SaveToCacheResult} result - Information on the cached data. + */ + /**jsdoc + * Saves asset data to the cache directly, without downloading it from a URL. + *

Note: Can only be used in Interface, avatar, and assignment client scripts.

+ * @function Assets.saveToCache + * @param {Assets.SaveToCacheOptions} options - The data to save to the cache and cache options. + * @param {object|Assets.CallbackDetails|Assets~saveToCacheCallback} scopeOrCallback - If an object, then the scope that + * the callback function is defined in. This object is bound to this when the function is + * called. + *

Otherwise, the function to call upon completion. This may be an inline function or a function identifier.

+ * @param {Assets~saveToCacheCallback} [callback] - Used if scopeOrCallback specifies the scope. + *

The function to call upon completion. May be an inline function, a function identifier, or the name of a function + * in a string. If the name of a function or a function identifier, it must be a member of the scope specified by + * scopeOrCallback.

+ * @example Save a string in the cache. + * Assets.saveToCache( + * { + * url: "cache:/cacheExample/helloCache.txt", + * data: "Hello cache" + * }, + * function (error, result) { + * if (error) { + * print("Error: " + error); + * } else { + * print("Success:"); + * print("- Bytes: " + result.byteLength); + * print("- URL: " + result.url); + * } + * } + * ); */ - Q_INVOKABLE void saveToCache(QScriptValue options, QScriptValue scope, QScriptValue callback = QScriptValue()); /**jsdoc + * Saves asset data to the cache directly, without downloading it from a URL. + *

Note: Can only be used in Interface, avatar, and assignment client scripts.

* @function Assets.saveToCache - * @param {} url - * @param {} data - * @param {} metadata - * @param {} scope - * @param {} [callback=undefined] + * @param {string} url - The URL to associate with the cache item. Must start with "atp:" or + * "cache:". + * @param {string|ArrayBuffer} data - The data to save to the cache. + * @param {Assets.SaveToCacheHeaders} headers - The last-modified and expiry times for the cache item. + * @param {object|Assets.CallbackDetails|Assets~saveToCacheCallback} scopeOrCallback - If an object, then the scope that + * the callback function is defined in. This object is bound to this when the function is + * called. + *

Otherwise, the function to call upon completion. This may be an inline function or a function identifier.

+ * @param {Assets~saveToCacheCallback} [callback] - Used if scopeOrCallback specifies the scope. + *

The function to call upon completion. May be an inline function, a function identifier, or the name of a function + * in a string. If the name of a function or a function identifier, it must be a member of the scope specified by + * scopeOrCallback.

*/ - Q_INVOKABLE void saveToCache(const QUrl& url, const QByteArray& data, const QVariantMap& metadata, QScriptValue scope, QScriptValue callback = QScriptValue()); protected: diff --git a/libraries/shared/src/DebugDraw.cpp b/libraries/shared/src/DebugDraw.cpp index 1b2418f7c7..2539a43672 100644 --- a/libraries/shared/src/DebugDraw.cpp +++ b/libraries/shared/src/DebugDraw.cpp @@ -31,9 +31,10 @@ void DebugDraw::drawRay(const glm::vec3& start, const glm::vec3& end, const glm: _rays.push_back(Ray(start, end, color)); } -void DebugDraw::addMarker(const QString& key, const glm::quat& rotation, const glm::vec3& position, const glm::vec4& color) { +void DebugDraw::addMarker(const QString& key, const glm::quat& rotation, const glm::vec3& position, + const glm::vec4& color, float size) { Lock lock(_mapMutex); - _markers[key] = MarkerInfo(rotation, position, color); + _markers[key] = MarkerInfo(rotation, position, color, size); } void DebugDraw::removeMarker(const QString& key) { @@ -41,9 +42,10 @@ void DebugDraw::removeMarker(const QString& key) { _markers.erase(key); } -void DebugDraw::addMyAvatarMarker(const QString& key, const glm::quat& rotation, const glm::vec3& position, const glm::vec4& color) { +void DebugDraw::addMyAvatarMarker(const QString& key, const glm::quat& rotation, const glm::vec3& position, + const glm::vec4& color, float size) { Lock lock(_mapMutex); - _myAvatarMarkers[key] = MarkerInfo(rotation, position, color); + _myAvatarMarkers[key] = MarkerInfo(rotation, position, color, size); } void DebugDraw::removeMyAvatarMarker(const QString& key) { @@ -83,4 +85,4 @@ void DebugDraw::drawRays(const std::vector>& lin auto point2 = translation + rotation * line.second; _rays.push_back(Ray(point1, point2, color)); } -} \ No newline at end of file +} diff --git a/libraries/shared/src/DebugDraw.h b/libraries/shared/src/DebugDraw.h index 9e3140ca9b..9db48b759b 100644 --- a/libraries/shared/src/DebugDraw.h +++ b/libraries/shared/src/DebugDraw.h @@ -95,19 +95,22 @@ public: * @param {Quat} rotation - The orientation of the marker in world coordinates. * @param {Vec3} position - The position of the market in world coordinates. * @param {Vec4} color - The color of the marker. + * @param {float} size - A float between 0.0 and 1.0 (10 cm) to control the size of the marker. * @example Briefly draw a debug marker in front of your avatar, in world coordinates. * var MARKER_NAME = "my marker"; * DebugDraw.addMarker( * MARKER_NAME, * Quat.ZERO, * Vec3.sum(MyAvatar.position, Vec3.multiplyQbyV(MyAvatar.orientation, { x: 0, y: 0, z: -5})), - * { red: 255, green: 0, blue: 0 } + * { red: 255, green: 0, blue: 0 }, + * 1.0 * ); * Script.setTimeout(function () { * DebugDraw.removeMarker(MARKER_NAME); * }, 5000); */ - Q_INVOKABLE void addMarker(const QString& key, const glm::quat& rotation, const glm::vec3& position, const glm::vec4& color); + Q_INVOKABLE void addMarker(const QString& key, const glm::quat& rotation, const glm::vec3& position, + const glm::vec4& color, float size = 1.0f); /**jsdoc * Removes a debug marker that was added in world coordinates. @@ -125,19 +128,22 @@ public: * @param {Quat} rotation - The orientation of the marker in avatar coordinates. * @param {Vec3} position - The position of the market in avatar coordinates. * @param {Vec4} color - color of the marker. + * @param {float} size - A float between 0.0 and 1.0 (10 cm) to control the size of the marker. * @example Briefly draw a debug marker in front of your avatar, in avatar coordinates. * var MARKER_NAME = "My avatar marker"; * DebugDraw.addMyAvatarMarker( * MARKER_NAME, * Quat.ZERO, * { x: 0, y: 0, z: -5 }, - * { red: 255, green: 0, blue: 0 } + * { red: 255, green: 0, blue: 0 }, + * 1.0 * ); * Script.setTimeout(function () { * DebugDraw.removeMyAvatarMarker(MARKER_NAME); * }, 5000); */ - Q_INVOKABLE void addMyAvatarMarker(const QString& key, const glm::quat& rotation, const glm::vec3& position, const glm::vec4& color); + Q_INVOKABLE void addMyAvatarMarker(const QString& key, const glm::quat& rotation, const glm::vec3& position, + const glm::vec4& color, float size = 1.0f); /**jsdoc * Removes a debug marker that was added in avatar coordinates. @@ -146,7 +152,7 @@ public: */ Q_INVOKABLE void removeMyAvatarMarker(const QString& key); - using MarkerInfo = std::tuple; + using MarkerInfo = std::tuple; using MarkerMap = std::map; using Ray = std::tuple; using Rays = std::vector; diff --git a/libraries/shared/src/GeometryUtil.cpp b/libraries/shared/src/GeometryUtil.cpp index b6fca03403..23d2f0f71b 100644 --- a/libraries/shared/src/GeometryUtil.cpp +++ b/libraries/shared/src/GeometryUtil.cpp @@ -547,6 +547,28 @@ bool doLineSegmentsIntersect(glm::vec2 r1p1, glm::vec2 r1p2, glm::vec2 r2p1, glm (d4 == 0 && isOnSegment(r1p1.x, r1p1.y, r1p2.x, r1p2.y, r2p2.x, r2p2.y)); } +bool findClosestApproachOfLines(glm::vec3 p1, glm::vec3 d1, glm::vec3 p2, glm::vec3 d2, + // return values... + float& t1, float& t2) { + // https://math.stackexchange.com/questions/1993953/closest-points-between-two-lines/1993990#1993990 + // https://en.wikipedia.org/wiki/Skew_lines#Nearest_Points + glm::vec3 n1 = glm::cross(d1, glm::cross(d2, d1)); + glm::vec3 n2 = glm::cross(d2, glm::cross(d1, d2)); + + float denom1 = glm::dot(d1, n2); + float denom2 = glm::dot(d2, n1); + + if (denom1 != 0.0f && denom2 != 0.0f) { + t1 = glm::dot((p2 - p1), n2) / denom1; + t2 = glm::dot((p1 - p2), n1) / denom2; + return true; + } else { + t1 = 0.0f; + t2 = 0.0f; + return false; + } +} + bool isOnSegment(float xi, float yi, float xj, float yj, float xk, float yk) { return (xi <= xk || xj <= xk) && (xk <= xi || xk <= xj) && (yi <= yk || yj <= yk) && (yk <= yi || yk <= yj); @@ -1813,4 +1835,4 @@ bool solve_quartic(float a, float b, float c, float d, glm::vec4& roots) { bool computeRealQuarticRoots(float a, float b, float c, float d, float e, glm::vec4& roots) { return solve_quartic(b / a, c / a, d / a, e / a, roots); -} \ No newline at end of file +} diff --git a/libraries/shared/src/GeometryUtil.h b/libraries/shared/src/GeometryUtil.h index 764eeb1500..d786d63980 100644 --- a/libraries/shared/src/GeometryUtil.h +++ b/libraries/shared/src/GeometryUtil.h @@ -150,6 +150,7 @@ int clipTriangleWithPlane(const Triangle& triangle, const Plane& plane, Triangle int clipTriangleWithPlanes(const Triangle& triangle, const Plane* planes, int planeCount, Triangle* clippedTriangles, int maxClippedTriangleCount); bool doLineSegmentsIntersect(glm::vec2 r1p1, glm::vec2 r1p2, glm::vec2 r2p1, glm::vec2 r2p2); +bool findClosestApproachOfLines(glm::vec3 p1, glm::vec3 d1, glm::vec3 p2, glm::vec3 d2, float& t1, float& t2); bool isOnSegment(float xi, float yi, float xj, float yj, float xk, float yk); int computeDirection(float xi, float yi, float xj, float yj, float xk, float yk); diff --git a/libraries/task/src/task/Config.h b/libraries/task/src/task/Config.h index 8accba9e1f..71d48c9a18 100644 --- a/libraries/task/src/task/Config.h +++ b/libraries/task/src/task/Config.h @@ -90,6 +90,17 @@ public: using Config = JobConfig; }; +/**jsdoc + * @namespace Workload + * + * @hifi-interface + * @hifi-client-entity + * @hifi-avatar + * + * @property {number} cpuRunTime - Read-only. + * @property {boolean} enabled + * @property {number} branch + */ // A default Config is always on; to create an enableable Config, use the ctor JobConfig(bool enabled) class JobConfig : public QObject { Q_OBJECT @@ -139,7 +150,7 @@ public: double getCPURunTime() const { return _msCPURunTime; } /**jsdoc - * @function Render.getConfig + * @function Workload.getConfig * @param {string} name * @returns {object} */ @@ -162,19 +173,19 @@ public: // Describe the node graph data connections of the associated Job/Task /**jsdoc - * @function JobConfig.isTask + * @function Workload.isTask * @returns {boolean} */ Q_INVOKABLE bool isTask() const { return _isTask; } /**jsdoc - * @function JobConfig.isSwitch + * @function Workload.isSwitch * @returns {boolean} */ Q_INVOKABLE bool isSwitch() const { return _isSwitch; } /**jsdoc - * @function JobConfig.getSubConfigs + * @function Workload.getSubConfigs * @returns {object[]} */ Q_INVOKABLE QObjectList getSubConfigs() const { @@ -187,13 +198,13 @@ public: } /**jsdoc - * @function JobConfig.getNumSubs + * @function Workload.getNumSubs * @returns {number} */ Q_INVOKABLE int getNumSubs() const { return getSubConfigs().size(); } /**jsdoc - * @function JobConfig.getSubConfig + * @function Workload.getSubConfig * @param {number} index * @returns {object} */ @@ -214,7 +225,7 @@ public slots: /**jsdoc * @function Workload.load - * @param {object} map + * @param {object} json */ void load(const QJsonObject& val) { qObjectFromJsonValue(val, *this); emit loaded(); } diff --git a/libraries/trackers/src/trackers/EyeTracker.cpp b/libraries/trackers/src/trackers/EyeTracker.cpp deleted file mode 100644 index a64b945c55..0000000000 --- a/libraries/trackers/src/trackers/EyeTracker.cpp +++ /dev/null @@ -1,307 +0,0 @@ -// -// Created by David Rowe on 27 Jul 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 "EyeTracker.h" - -#include -#include - -#include - -#include "Logging.h" -#include - -#ifdef HAVE_IVIEWHMD -char* HIGH_FIDELITY_EYE_TRACKER_CALIBRATION = "HighFidelityEyeTrackerCalibration"; -#endif - -#ifdef HAVE_IVIEWHMD -static void CALLBACK eyeTrackerCallback(smi_CallbackDataStruct* data) { - auto eyeTracker = DependencyManager::get(); - if (eyeTracker) { // Guard against a few callbacks that continue to be received after smi_quit(). - eyeTracker->processData(data); - } -} -#endif - -EyeTracker::~EyeTracker() { -#ifdef HAVE_IVIEWHMD - if (_isStreaming) { - int result = smi_quit(); - if (result != SMI_RET_SUCCESS) { - qCWarning(interfaceapp) << "Eye Tracker: Error terminating tracking:" << smiReturnValueToString(result); - } - } -#endif -} - -#ifdef HAVE_IVIEWHMD -void EyeTracker::processData(smi_CallbackDataStruct* data) { - _lastProcessDataTimestamp = usecTimestampNow(); - - if (!_isEnabled) { - return; - } - - if (data->type == SMI_SIMPLE_GAZE_SAMPLE) { - // Calculate the intersections of the left and right eye look-at vectors with a vertical plane along the monocular - // gaze direction. Average these positions to give the look-at point. - // If the eyes are parallel or diverged, gaze at a distant look-at point calculated the same as for non eye tracking. - // Line-plane intersection: https://en.wikipedia.org/wiki/Line%E2%80%93plane_intersection - - smi_SampleHMDStruct* sample = (smi_SampleHMDStruct*)data->result; - // The iViewHMD coordinate system has x and z axes reversed compared to Interface, i.e., wearing the HMD: - // - x is left - // - y is up - // - z is forwards - - // Plane - smi_Vec3d point = sample->gazeBasePoint; // mm - smi_Vec3d direction = sample->gazeDirection; - glm::vec3 planePoint = glm::vec3(-point.x, point.y, -point.z) / 1000.0f; - glm::vec3 planeNormal = glm::vec3(-direction.z, 0.0f, direction.x); - glm::vec3 monocularDirection = glm::vec3(-direction.x, direction.y, -direction.z); - - // Left eye - point = sample->left.gazeBasePoint; // mm - direction = sample->left.gazeDirection; - glm::vec3 leftLinePoint = glm::vec3(-point.x, point.y, -point.z) / 1000.0f; - glm::vec3 leftLineDirection = glm::vec3(-direction.x, direction.y, -direction.z); - - // Right eye - point = sample->right.gazeBasePoint; // mm - direction = sample->right.gazeDirection; - glm::vec3 rightLinePoint = glm::vec3(-point.x, point.y, -point.z) / 1000.0f; - glm::vec3 rightLineDirection = glm::vec3(-direction.x, direction.y, -direction.z); - - // Plane - line dot products - float leftLinePlaneDotProduct = glm::dot(leftLineDirection, planeNormal); - float rightLinePlaneDotProduct = glm::dot(rightLineDirection, planeNormal); - - // Gaze into distance if eyes are parallel or diverged; otherwise the look-at is the average of look-at points - glm::vec3 lookAtPosition; - if (abs(leftLinePlaneDotProduct) <= FLT_EPSILON || abs(rightLinePlaneDotProduct) <= FLT_EPSILON) { - lookAtPosition = monocularDirection * (float)TREE_SCALE; - } else { - float leftDistance = glm::dot(planePoint - leftLinePoint, planeNormal) / leftLinePlaneDotProduct; - float rightDistance = glm::dot(planePoint - rightLinePoint, planeNormal) / rightLinePlaneDotProduct; - if (leftDistance <= 0.0f || rightDistance <= 0.0f - || leftDistance > (float)TREE_SCALE || rightDistance > (float)TREE_SCALE) { - lookAtPosition = monocularDirection * (float)TREE_SCALE; - } else { - glm::vec3 leftIntersectionPoint = leftLinePoint + leftDistance * leftLineDirection; - glm::vec3 rightIntersectionPoint = rightLinePoint + rightDistance * rightLineDirection; - lookAtPosition = (leftIntersectionPoint + rightIntersectionPoint) / 2.0f; - } - } - - if (glm::isnan(lookAtPosition.x) || glm::isnan(lookAtPosition.y) || glm::isnan(lookAtPosition.z)) { - return; - } - - _lookAtPosition = lookAtPosition; - } -} -#endif - -void EyeTracker::init() { - if (_isInitialized) { - qCWarning(trackers) << "Eye Tracker: Already initialized"; - return; - } -} - -#ifdef HAVE_IVIEWHMD -int EyeTracker::startStreaming(bool simulate) { - return smi_startStreaming(simulate); // This call blocks execution. -} -#endif - -#ifdef HAVE_IVIEWHMD -void EyeTracker::onStreamStarted() { - if (!_isInitialized) { - return; - } - - int result = _startStreamingWatcher.result(); - _isStreaming = (result == SMI_RET_SUCCESS); - - if (result != SMI_RET_SUCCESS) { - qCWarning(interfaceapp) << "Eye Tracker: Error starting streaming:" << smiReturnValueToString(result); - // Display error dialog unless SMI SDK has already displayed an error message. - if (result != SMI_ERROR_HMD_NOT_SUPPORTED) { - OffscreenUi::asyncWarning(nullptr, "Eye Tracker Error", smiReturnValueToString(result)); - } - } else { - qCDebug(interfaceapp) << "Eye Tracker: Started streaming"; - } - - if (_isStreaming) { - // Automatically load calibration if one has been saved. - QString availableCalibrations = QString(smi_getAvailableCalibrations()); - if (availableCalibrations.contains(HIGH_FIDELITY_EYE_TRACKER_CALIBRATION)) { - result = smi_loadCalibration(HIGH_FIDELITY_EYE_TRACKER_CALIBRATION); - if (result != SMI_RET_SUCCESS) { - qCWarning(interfaceapp) << "Eye Tracker: Error loading calibration:" << smiReturnValueToString(result); - OffscreenUi::asyncWarning(nullptr, "Eye Tracker Error", "Error loading calibration" - + smiReturnValueToString(result)); - } else { - qCDebug(interfaceapp) << "Eye Tracker: Loaded calibration"; - } - } - } -} -#endif - -void EyeTracker::setEnabled(bool enabled, bool simulate) { - if (enabled && !_isInitialized) { -#ifdef HAVE_IVIEWHMD - int result = smi_setCallback(eyeTrackerCallback); - if (result != SMI_RET_SUCCESS) { - qCWarning(interfaceapp) << "Eye Tracker: Error setting callback:" << smiReturnValueToString(result); - OffscreenUi::asyncWarning(nullptr, "Eye Tracker Error", smiReturnValueToString(result)); - } else { - _isInitialized = true; - } - - connect(&_startStreamingWatcher, SIGNAL(finished()), this, SLOT(onStreamStarted())); -#endif - } - - if (!_isInitialized) { - return; - } - -#ifdef HAVE_IVIEWHMD - qCDebug(interfaceapp) << "Eye Tracker: Set enabled =" << enabled << ", simulate =" << simulate; - - // There is no smi_stopStreaming() method and after an smi_quit(), streaming cannot be restarted (at least not for - // simulated data). So keep streaming once started in case tracking is re-enabled after stopping. - - // Try to stop streaming if changing whether simulating or not. - if (enabled && _isStreaming && _isStreamSimulating != simulate) { - int result = smi_quit(); - if (result != SMI_RET_SUCCESS) { - qCWarning(interfaceapp) << "Eye Tracker: Error stopping streaming:" << smiReturnValueToString(result); - } - _isStreaming = false; - } - - if (enabled && !_isStreaming) { - // Start SMI streaming in a separate thread because it blocks. - QFuture future = QtConcurrent::run(this, &EyeTracker::startStreaming, simulate); - _startStreamingWatcher.setFuture(future); - _isStreamSimulating = simulate; - } - - _isEnabled = enabled; - _isSimulating = simulate; - -#endif -} - -void EyeTracker::reset() { - // Nothing to do. -} - -bool EyeTracker::isTracking() const { - static const quint64 ACTIVE_TIMEOUT_USECS = 2000000; // 2 secs - return _isEnabled && (usecTimestampNow() - _lastProcessDataTimestamp < ACTIVE_TIMEOUT_USECS); -} - -#ifdef HAVE_IVIEWHMD -void EyeTracker::calibrate(int points) { - - if (!_isStreaming) { - qCWarning(interfaceapp) << "Eye Tracker: Cannot calibrate because not streaming"; - return; - } - - smi_CalibrationHMDStruct* calibrationHMDStruct; - smi_createCalibrationHMDStruct(&calibrationHMDStruct); - - smi_CalibrationTypeEnum calibrationType; - switch (points) { - case 1: - calibrationType = SMI_ONE_POINT_CALIBRATION; - qCDebug(interfaceapp) << "Eye Tracker: One point calibration"; - break; - case 3: - calibrationType = SMI_THREE_POINT_CALIBRATION; - qCDebug(interfaceapp) << "Eye Tracker: Three point calibration"; - break; - case 5: - calibrationType = SMI_FIVE_POINT_CALIBRATION; - qCDebug(interfaceapp) << "Eye Tracker: Five point calibration"; - break; - default: - qCWarning(interfaceapp) << "Eye Tracker: Invalid calibration specified"; - return; - } - - calibrationHMDStruct->type = calibrationType; - calibrationHMDStruct->backgroundColor->blue = 0.5; - calibrationHMDStruct->backgroundColor->green = 0.5; - calibrationHMDStruct->backgroundColor->red = 0.5; - calibrationHMDStruct->foregroundColor->blue = 1.0; - calibrationHMDStruct->foregroundColor->green = 1.0; - calibrationHMDStruct->foregroundColor->red = 1.0; - - int result = smi_setupCalibration(calibrationHMDStruct); - if (result != SMI_RET_SUCCESS) { - qCWarning(interfaceapp) << "Eye Tracker: Error setting up calibration:" << smiReturnValueToString(result); - return; - } else { - result = smi_calibrate(); - if (result != SMI_RET_SUCCESS) { - qCWarning(interfaceapp) << "Eye Tracker: Error performing calibration:" << smiReturnValueToString(result); - } else { - result = smi_saveCalibration(HIGH_FIDELITY_EYE_TRACKER_CALIBRATION); - if (result != SMI_RET_SUCCESS) { - qCWarning(interfaceapp) << "Eye Tracker: Error saving calibration:" << smiReturnValueToString(result); - } - } - } - - if (result != SMI_RET_SUCCESS) { - OffscreenUi::asyncWarning(nullptr, "Eye Tracker Error", "Calibration error: " + smiReturnValueToString(result)); - } -} -#endif - -#ifdef HAVE_IVIEWHMD -QString EyeTracker::smiReturnValueToString(int value) { - switch (value) - { - case smi_ErrorReturnValue::SMI_ERROR_NO_CALLBACK_SET: - return "No callback set"; - case smi_ErrorReturnValue::SMI_ERROR_CONNECTING_TO_HMD: - return "Error connecting to HMD"; - case smi_ErrorReturnValue::SMI_ERROR_HMD_NOT_SUPPORTED: - return "HMD not supported"; - case smi_ErrorReturnValue::SMI_ERROR_NOT_IMPLEMENTED: - return "Not implmented"; - case smi_ErrorReturnValue::SMI_ERROR_INVALID_PARAMETER: - return "Invalid parameter"; - case smi_ErrorReturnValue::SMI_ERROR_EYECAMERAS_NOT_AVAILABLE: - return "Eye cameras not available"; - case smi_ErrorReturnValue::SMI_ERROR_OCULUS_RUNTIME_NOT_SUPPORTED: - return "Oculus runtime not supported"; - case smi_ErrorReturnValue::SMI_ERROR_FILE_NOT_FOUND: - return "File not found"; - case smi_ErrorReturnValue::SMI_ERROR_FILE_EMPTY: - return "File empty"; - case smi_ErrorReturnValue::SMI_ERROR_UNKNOWN: - return "Unknown error"; - default: - QString number; - number.setNum(value); - return number; - } -} -#endif diff --git a/libraries/trackers/src/trackers/EyeTracker.h b/libraries/trackers/src/trackers/EyeTracker.h deleted file mode 100644 index 9cf35d0f2a..0000000000 --- a/libraries/trackers/src/trackers/EyeTracker.h +++ /dev/null @@ -1,68 +0,0 @@ -// -// Created by David Rowe on 27 Jul 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 -// - -#ifndef hifi_EyeTracker_h -#define hifi_EyeTracker_h - -#include -#include - -#include - -#include -#ifdef HAVE_IVIEWHMD -#include -#endif - - -class EyeTracker : public QObject, public Dependency { - Q_OBJECT - SINGLETON_DEPENDENCY - -public: - ~EyeTracker(); - - void init(); - void setEnabled(bool enabled, bool simulate); - void reset(); - - bool isInitialized() const { return _isInitialized; } - bool isEnabled() const { return _isEnabled; } - bool isTracking() const; - bool isSimulating() const { return _isSimulating; } - - glm::vec3 getLookAtPosition() const { return _lookAtPosition; } // From mid eye point in head frame. - -#ifdef HAVE_IVIEWHMD - void processData(smi_CallbackDataStruct* data); - - void calibrate(int points); - - int startStreaming(bool simulate); - -private slots: - void onStreamStarted(); -#endif - -private: - QString smiReturnValueToString(int value); - - bool _isInitialized = false; - bool _isEnabled = false; - bool _isSimulating = false; - bool _isStreaming = false; - bool _isStreamSimulating = false; - - quint64 _lastProcessDataTimestamp; - - glm::vec3 _lookAtPosition; - - QFutureWatcher _startStreamingWatcher; -}; - -#endif // hifi_EyeTracker_h diff --git a/libraries/ui/src/ui/TabletScriptingInterface.cpp b/libraries/ui/src/ui/TabletScriptingInterface.cpp index c54f63690d..3465138e00 100644 --- a/libraries/ui/src/ui/TabletScriptingInterface.cpp +++ b/libraries/ui/src/ui/TabletScriptingInterface.cpp @@ -795,11 +795,25 @@ void TabletProxy::loadWebScreenOnTop(const QVariant& url) { } void TabletProxy::loadWebScreenOnTop(const QVariant& url, const QString& injectJavaScriptUrl) { + bool localSafeContext = hifi::scripting::isLocalAccessSafeThread(); if (QThread::currentThread() != thread()) { - QMetaObject::invokeMethod(this, "loadWebScreenOnTop", Q_ARG(QVariant, url), Q_ARG(QString, injectJavaScriptUrl)); + QMetaObject::invokeMethod(this, "loadHTMLSourceImpl", Q_ARG(QVariant, url), Q_ARG(QString, injectJavaScriptUrl), Q_ARG(bool, localSafeContext)); return; } + loadHTMLSourceImpl(url, injectJavaScriptUrl, localSafeContext); +} + + + +void TabletProxy::loadHTMLSourceImpl(const QVariant& url, const QString& injectJavaScriptUrl, bool localSafeContext) { + if (QThread::currentThread() != thread()) { + qCWarning(uiLogging) << __FUNCTION__ << "may not be called directly by scripts"; + return; + + } + + QObject* root = nullptr; if (!_toolbarMode && _qmlTabletRoot) { root = _qmlTabletRoot; @@ -808,22 +822,33 @@ void TabletProxy::loadWebScreenOnTop(const QVariant& url, const QString& injectJ } if (root) { + if (localSafeContext) { + hifi::scripting::setLocalAccessSafeThread(true); + } QMetaObject::invokeMethod(root, "loadQMLOnTop", Q_ARG(const QVariant&, QVariant(WEB_VIEW_SOURCE_URL))); QMetaObject::invokeMethod(root, "setShown", Q_ARG(const QVariant&, QVariant(true))); if (_toolbarMode && _desktopWindow) { QMetaObject::invokeMethod(root, "setResizable", Q_ARG(const QVariant&, QVariant(false))); } QMetaObject::invokeMethod(root, "loadWebOnTop", Q_ARG(const QVariant&, QVariant(url)), Q_ARG(const QVariant&, QVariant(injectJavaScriptUrl))); + hifi::scripting::setLocalAccessSafeThread(false); } _state = State::Web; } void TabletProxy::gotoWebScreen(const QString& url, const QString& injectedJavaScriptUrl, bool loadOtherBase) { + bool localSafeContext = hifi::scripting::isLocalAccessSafeThread(); if (QThread::currentThread() != thread()) { - QMetaObject::invokeMethod(this, "gotoWebScreen", Q_ARG(QString, url), Q_ARG(QString, injectedJavaScriptUrl), Q_ARG(bool, loadOtherBase)); + QMetaObject::invokeMethod(this, "loadHTMLSourceImpl", Q_ARG(QString, url), Q_ARG(QString, injectedJavaScriptUrl), Q_ARG(bool, loadOtherBase), Q_ARG(bool, localSafeContext)); return; } + + loadHTMLSourceImpl(url, injectedJavaScriptUrl, loadOtherBase, localSafeContext); +} + +void TabletProxy::loadHTMLSourceImpl(const QString& url, const QString& injectedJavaScriptUrl, bool loadOtherBase, bool localSafeContext) { + QObject* root = nullptr; if (!_toolbarMode && _qmlTabletRoot) { root = _qmlTabletRoot; @@ -832,6 +857,9 @@ void TabletProxy::gotoWebScreen(const QString& url, const QString& injectedJavaS } if (root) { + if (localSafeContext) { + hifi::scripting::setLocalAccessSafeThread(true); + } if (loadOtherBase) { QMetaObject::invokeMethod(root, "loadTabletWebBase", Q_ARG(const QVariant&, QVariant(url)), Q_ARG(const QVariant&, QVariant(injectedJavaScriptUrl))); } else { @@ -841,6 +869,8 @@ void TabletProxy::gotoWebScreen(const QString& url, const QString& injectedJavaS if (_toolbarMode && _desktopWindow) { QMetaObject::invokeMethod(root, "setResizable", Q_ARG(const QVariant&, QVariant(false))); } + + hifi::scripting::setLocalAccessSafeThread(false); _state = State::Web; _currentPathLoaded = QVariant(url); } else { diff --git a/libraries/ui/src/ui/TabletScriptingInterface.h b/libraries/ui/src/ui/TabletScriptingInterface.h index ba02ba25b0..9a5ff9efac 100644 --- a/libraries/ui/src/ui/TabletScriptingInterface.h +++ b/libraries/ui/src/ui/TabletScriptingInterface.h @@ -298,6 +298,10 @@ public: */ Q_INVOKABLE void loadQMLSourceImpl(const QVariant& path, bool resizable, bool localSafeContext); + Q_INVOKABLE void loadHTMLSourceImpl(const QVariant& url, const QString& injectJavaScriptUrl, bool localSafeContext); + + Q_INVOKABLE void loadHTMLSourceImpl(const QString& url, const QString& injectedJavaScriptUrl, bool loadOtherBase, bool localSafeContext); + // FIXME: This currently relies on a script initializing the tablet (hence the bool denoting success); // it should be initialized internally so it cannot fail diff --git a/plugins/openvr/src/OpenVrDisplayPlugin.h b/plugins/openvr/src/OpenVrDisplayPlugin.h index 25427c6dcd..9212a42639 100644 --- a/plugins/openvr/src/OpenVrDisplayPlugin.h +++ b/plugins/openvr/src/OpenVrDisplayPlugin.h @@ -76,6 +76,8 @@ public: int visionSqueezePerEye, float visionSqueezeGroundPlaneY, float visionSqueezeSpotlightSize) override; + glm::mat4 getSensorResetMatrix() const { return _sensorResetMat; } + protected: bool internalActivate() override; void internalDeactivate() override; diff --git a/scripts/simplifiedUI/simplifiedEmote/emojiApp/resources/sounds/emojiPopSound2.wav b/scripts/simplifiedUI/simplifiedEmote/emojiApp/resources/sounds/emojiPopSound2.wav deleted file mode 100644 index 3d917997a7..0000000000 Binary files a/scripts/simplifiedUI/simplifiedEmote/emojiApp/resources/sounds/emojiPopSound2.wav and /dev/null differ diff --git a/scripts/simplifiedUI/simplifiedEmote/emojiApp/simplifiedEmoji.js b/scripts/simplifiedUI/simplifiedEmote/emojiApp/simplifiedEmoji.js index 1b16843b09..d8db41e488 100644 --- a/scripts/simplifiedUI/simplifiedEmote/emojiApp/simplifiedEmoji.js +++ b/scripts/simplifiedUI/simplifiedEmote/emojiApp/simplifiedEmoji.js @@ -48,7 +48,6 @@ var UTF_CODE = 0; // Only plays a sound if it is downloaded. // Only plays one sound at a time. var emojiCreateSound = SoundCache.getSound(Script.resolvePath('resources/sounds/emojiPopSound1.wav')); -var emojiDestroySound = SoundCache.getSound(Script.resolvePath('resources/sounds/emojiPopSound2.wav')); var injector; var DEFAULT_VOLUME = 0.01; var local = false; @@ -326,9 +325,7 @@ function playPopAnimation() { if (popType === "in") { currentPopScale = MIN_POP_SCALE; } else { - // Start with the pop sound on the out currentPopScale = finalInPopScale ? finalInPopScale : MAX_POP_SCALE; - playSound(emojiDestroySound, DEFAULT_VOLUME, MyAvatar.position, true); } } diff --git a/scripts/simplifiedUI/simplifiedEmote/emojiApp/ui/qml/SimplifiedEmoji.qml b/scripts/simplifiedUI/simplifiedEmote/emojiApp/ui/qml/SimplifiedEmoji.qml index dd78b8fe09..a890be46f1 100644 --- a/scripts/simplifiedUI/simplifiedEmote/emojiApp/ui/qml/SimplifiedEmoji.qml +++ b/scripts/simplifiedUI/simplifiedEmote/emojiApp/ui/qml/SimplifiedEmoji.qml @@ -66,6 +66,7 @@ Rectangle { .forEach(function(item, index){ item.code = { utf: item.code[0] } item.keywords = { keywords: item.keywords } + item.shortName = item.shortName mainModel.append(item); filteredModel.append(item); }); @@ -73,6 +74,7 @@ Rectangle { .forEach(function(item, index){ item.code = { utf: item.name } item.keywords = { keywords: item.keywords } + item.shortName = item.name mainModel.append(item); filteredModel.append(item); }); @@ -230,7 +232,7 @@ Rectangle { anchors.top: parent.top anchors.left: parent.left anchors.right: parent.right - height: 200 + height: 160 clip: true color: simplifiedUI.colors.darkBackground @@ -248,8 +250,8 @@ Rectangle { Image { id: mainEmojiImage - width: 180 - height: 180 + width: emojiIndicatorContainer.width - 20 + height: emojiIndicatorContainer.height - 20 anchors.centerIn: parent source: "" fillMode: Image.PreserveAspectFit @@ -281,6 +283,64 @@ Rectangle { } + Item { + id: searchBarContainer + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: emojiIndicatorContainer.bottom + width: Math.min(parent.width, 420) + height: 48 + + SimplifiedControls.TextField { + id: emojiSearchTextField + readonly property string defaultPlaceholderText: "Search Emojis" + bottomBorderVisible: false + backgroundColor: "#313131" + placeholderText: emojiSearchTextField.defaultPlaceholderText + maximumLength: 100 + clip: true + selectByMouse: true + autoScroll: true + horizontalAlignment: TextInput.AlignHCenter + anchors.left: parent.left + anchors.leftMargin: 16 + anchors.right: parent.right + anchors.rightMargin: 16 + anchors.verticalCenter: parent.verticalCenter + onTextChanged: { + if (text.length === 0) { + root.filterEmoji(emojiSearchTextField.text); + } else { + waitForMoreInputTimer.restart(); + } + } + onAccepted: { + root.filterEmoji(emojiSearchTextField.text); + waitForMoreInputTimer.stop(); + if (filteredModel.count === 1) { + root.selectEmoji(filteredModel.get(0).code.utf); + } else { + grid.forceActiveFocus(); + } + } + + KeyNavigation.backtab: grid + KeyNavigation.tab: grid + } + + Timer { + id: waitForMoreInputTimer + repeat: false + running: false + triggeredOnStart: false + interval: 300 + + onTriggered: { + root.filterEmoji(emojiSearchTextField.text); + } + } + } + + function selectEmoji(code) { sendToScript({ "source": "SimplifiedEmoji.qml", @@ -294,13 +354,45 @@ Rectangle { Rectangle { id: emojiIconListContainer - anchors.top: emojiIndicatorContainer.bottom + anchors.top: searchBarContainer.bottom + anchors.topMargin: 10 anchors.left: parent.left anchors.right: parent.right - anchors.bottom: bottomContainer.top + anchors.bottom: parent.bottom + anchors.bottomMargin: 8 clip: true color: simplifiedUI.colors.darkBackground + Item { + id: helpGlyphContainer + anchors.left: parent.left + anchors.leftMargin: 4 + anchors.bottom: parent.bottom + anchors.bottomMargin: 2 + width: 22 + height: width + + HifiStylesUit.GraphikRegular { + text: "?" + anchors.fill: parent + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + color: simplifiedUI.colors.text.darkGrey + opacity: attributionMouseArea.containsMouse ? 1.0 : 0.8 + size: 22 + } + + MouseArea { + id: attributionMouseArea + hoverEnabled: enabled + anchors.fill: parent + propagateComposedEvents: false + onClicked: { + popupContainer.visible = true; + } + } + } + GridView { id: grid anchors.fill: parent @@ -319,13 +411,18 @@ Rectangle { anchors.fill: parent propagateComposedEvents: false onEntered: { - grid.currentIndex = index + grid.currentIndex = index; // don't allow a hover image change of the main emoji image if (root.isSelected) { return; } // Updates the selected image root.currentCode = model.code.utf; + // Ensures that the placeholder text is visible and updated + if (emojiSearchTextField.text === "") { + grid.forceActiveFocus(); + } + emojiSearchTextField.placeholderText = "::" + model.shortName + "::"; } onClicked: { root.selectEmoji(model.code.utf); @@ -379,88 +476,6 @@ Rectangle { } } - - Item { - id: bottomContainer - anchors.left: parent.left - anchors.right: parent.right - anchors.bottom: parent.bottom - height: 40 - - SimplifiedControls.TextField { - id: emojiSearchTextField - placeholderText: "Search" - maximumLength: 100 - clip: true - selectByMouse: true - autoScroll: true - anchors.left: parent.left - anchors.leftMargin: 16 - anchors.right: helpGlyphContainer.left - anchors.rightMargin: 16 - anchors.verticalCenter: parent.verticalCenter - onTextChanged: { - if (text.length === 0) { - root.filterEmoji(emojiSearchTextField.text); - } else { - waitForMoreInputTimer.restart(); - } - } - onAccepted: { - root.filterEmoji(emojiSearchTextField.text); - waitForMoreInputTimer.stop(); - if (filteredModel.count === 1) { - root.selectEmoji(filteredModel.get(0).code.utf); - } else { - grid.forceActiveFocus(); - } - } - - KeyNavigation.backtab: grid - KeyNavigation.tab: grid - } - - Timer { - id: waitForMoreInputTimer - repeat: false - running: false - triggeredOnStart: false - interval: 300 - - onTriggered: { - root.filterEmoji(emojiSearchTextField.text); - } - } - - Item { - id: helpGlyphContainer - anchors.right: parent.right - anchors.rightMargin: 8 - width: height - height: parent.height - - HifiStylesUit.GraphikRegular { - text: "?" - anchors.fill: parent - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - color: simplifiedUI.colors.text.darkGrey - opacity: attributionMouseArea.containsMouse ? 1.0 : 0.8 - size: 22 - } - - MouseArea { - id: attributionMouseArea - hoverEnabled: enabled - anchors.fill: parent - propagateComposedEvents: false - onClicked: { - popupContainer.visible = true; - } - } - } - } - function filterEmoji(filterText) { filteredModel.clear(); diff --git a/scripts/simplifiedUI/simplifiedEmote/ui/qml/SimplifiedEmoteIndicator.qml b/scripts/simplifiedUI/simplifiedEmote/ui/qml/SimplifiedEmoteIndicator.qml index 787ccadd62..bbd1d4d735 100644 --- a/scripts/simplifiedUI/simplifiedEmote/ui/qml/SimplifiedEmoteIndicator.qml +++ b/scripts/simplifiedUI/simplifiedEmote/ui/qml/SimplifiedEmoteIndicator.qml @@ -45,7 +45,8 @@ Rectangle { } Behavior on requestedWidth { - enabled: true + enabled: false // Set this to `true` once we have a different windowing system that better supports on-screen widgets + // like the Emote Indicator. SmoothedAnimation { duration: 220 } } diff --git a/tools/jsdoc/hifi-jsdoc-template/static/styles/jsdoc.css b/tools/jsdoc/hifi-jsdoc-template/static/styles/jsdoc.css index a33cff15e4..2386f88586 100644 --- a/tools/jsdoc/hifi-jsdoc-template/static/styles/jsdoc.css +++ b/tools/jsdoc/hifi-jsdoc-template/static/styles/jsdoc.css @@ -23,7 +23,7 @@ ********************************************************************/ * { - box-sizing: border-box + box-sizing: border-box; } html @@ -38,11 +38,12 @@ body font-weight: 400; color: #000000; letter-spacing: 0.5px; + font-size: 0.95rem; + line-height: 20px; } p { font-size: 0.95rem; - line-height: 20px; } section @@ -128,7 +129,6 @@ table { thead { border-color: #d8e1d9; background:#d8e1d9; - text-align: left; } table tr { @@ -146,6 +146,7 @@ td { article table thead tr th, article table tbody tr td, article table tbody tr td p { font-size: .89rem; line-height: 20px; + text-align: left; } article table thead tr th, article table tbody tr td { @@ -199,6 +200,7 @@ nav { padding-left: 20px; padding-right: 20px; box-sizing: border-box; + line-height: 12px; } nav::-webkit-scrollbar { @@ -378,12 +380,12 @@ nav > h2 > a { tt, code, kbd, samp { font-family: Consolas, Monaco, 'Andale Mono', monospace; - font-size: 0.9rem; + font-size: 1.05em; } .name, .signature { font-family: Consolas, Monaco, 'Andale Mono', monospace; - font-size: 0.9rem; + font-size: 1.05em; } img { @@ -422,7 +424,6 @@ header { display: block; text-align: center; font-size: 90%; - margin-top: -20px; } .variation { @@ -537,7 +538,7 @@ header { .prettyprint code { - font-size: 0.7rem; + font-size: 0.9em; line-height: 18px; display: block; padding: 4px 12px; diff --git a/tools/jsdoc/hifi-jsdoc-template/tmpl/container.tmpl b/tools/jsdoc/hifi-jsdoc-template/tmpl/container.tmpl index 2a5e863d6d..5c149fa434 100644 --- a/tools/jsdoc/hifi-jsdoc-template/tmpl/container.tmpl +++ b/tools/jsdoc/hifi-jsdoc-template/tmpl/container.tmpl @@ -92,35 +92,33 @@ -

Example 1? 's':'' ?>

- - +

Description

- -

Classes

- -

- - - -

Example 1? 's':'' ?>

+ +

Classes

+ +

+ + + +
diff --git a/tools/jsdoc/plugins/hifi.js b/tools/jsdoc/plugins/hifi.js index 67dafe5a16..07549530ce 100644 --- a/tools/jsdoc/plugins/hifi.js +++ b/tools/jsdoc/plugins/hifi.js @@ -125,7 +125,7 @@ exports.handlers = { if (rows.length > 0) { var availableIn = "

Supported Script Types: " + rows.join(" • ") + "

"; - e.doclet.description = (e.doclet.description ? e.doclet.description : "") + availableIn; + e.doclet.description = availableIn + (e.doclet.description ? e.doclet.description : ""); } }