Merge master

This commit is contained in:
luiscuenca 2019-09-19 13:21:49 -07:00
commit 279e25ca70
No known key found for this signature in database
GPG key ID: 2387ECD129A6961D
100 changed files with 2158 additions and 1198 deletions

View file

@ -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()

View file

@ -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",

View file

@ -2,6 +2,9 @@ var DomainInfo = null;
var viewHelpers = {
getFormGroup: function(keypath, setting, values, isAdvanced) {
if (setting.hidden) {
return "";
}
form_group = "<div class='form-group " +
(isAdvanced ? Settings.ADVANCED_CLASS : "") + " " +
(setting.deprecated ? Settings.DEPRECATED_CLASS : "" ) + "' " +
@ -82,8 +85,9 @@ var viewHelpers = {
"placeholder='" + (_.has(setting, 'placeholder') ? setting.placeholder : "") +
"' value='" + (_.has(setting, 'password_placeholder') ? setting.password_placeholder : setting_value) + "'/>"
}
form_group += "<span class='help-block'>" + setting.help + "</span>"
if (setting.help) {
form_group += "<span class='help-block'>" + setting.help + "</span>"
}
}
}
@ -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);
}
});
}

View file

@ -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<BR/>";
}
}
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<BR/>";
}
}
if ($('#oauth.panel').length) {
if (!$('input[name="oauth.client-id"]').val()) {
oauthErrors += "OAuth requires a client Id.<BR/>";
}
if (!$('input[name="oauth.provider"]').val()) {
oauthErrors += "OAuth requires a provider.<BR/>";
}
if (!$('input[name="oauth.hostname"]').val()) {
oauthErrors += "OAuth requires a hostname.<BR/>";
}
if (!$('input[name="' + SSL_PRIVATE_KEY_PATH + '"]').val() && !$('input[name="' + SSL_PRIVATE_KEY_CONTENTS_NAME + '"]').val()) {
oauthErrors += "OAuth requires an SSL Private Key.<BR/>";
}
if (!$('input[name="' + SSL_CERT_PATH + '"]').val() && !$('input[name="' + SSL_CERT_CONTENTS_NAME + '"]').val()) {
oauthErrors += "OAuth requires an SSL Certificate.<BR/>";
}
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.<BR/>";
}
}
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 = "<div class='form-group undefined'>";
html += "<label class='control-label'>SSL Private Key</label><BR/>";
html += "<label id='key-path-label'class='control-label'>Path</label>";
html += "<input id='" + SSL_PRIVATE_KEY_FILE_ID + "' type='file' accept='.key'/>";
html += "<input id='" + SSL_PRIVATE_KEY_CONTENTS_ID + "' name='" + SSL_PRIVATE_KEY_CONTENTS_NAME + "' type='hidden'/>";
html += "</div>";
html += "<div class='form-group undefined'>";
html += "<label class='control-label'>SSL Cert</label>";
html += "<div id='cert-fingerprint'><b>Fingerprint:</b><span id='" + SSL_CERT_FINGERPRINT_SPAN_ID + "'>" + data.values.oauth["cert-fingerprint"] + "</span></div>";
html += "<label id='cert-path-label' class='control-label'>Path</label>";
html += "<input id='" + SSL_CERT_FILE_ID + "' type='file' accept='.cer,.crt'/>";
html += "<input id='" + SSL_CERT_CONTENTS_ID + "' name='" + SSL_CERT_CONTENTS_NAME + "' type='hidden'/>";
html += "</div>";
$('#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';

View file

@ -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>();
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<bool, QString> 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<bool, QString> 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);

View file

@ -236,6 +236,7 @@ private:
bool _isUsingDTLS { false };
bool _oauthEnable { false };
QUrl _oauthProviderURL;
QString _oauthClientID;
QString _oauthClientSecret;

View file

@ -22,7 +22,9 @@
#include <QtCore/QThread>
#include <QtCore/QUrl>
#include <QtCore/QUrlQuery>
#include <QtNetwork/QSslKey>
#include <QSaveFile>
#include <QPair>
#include <AccountManager.h>
#include <Assignment.h>
@ -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<QPair<const char*, const char*> > 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() {

View file

@ -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.

View file

@ -4600,6 +4600,10 @@
{
"state": "strafeLeftHmd",
"var": "isMovingLeftHmd"
},
{
"state": "idle",
"var": "isNotSeated"
}
]
},

View file

@ -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" },

View file

@ -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" },

View file

@ -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",

View file

@ -1,17 +1 @@
<svg width="360" height="85" viewBox="0 0 360 85" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M124 30V59H117.974V46.3455H108.026V59H102V30H108.026V41.1899H117.974V30H124Z" fill="#00B4F0"/>
<path d="M135 59H129V30H135V59Z" fill="#00B4F0"/>
<path d="M151.829 34.9471C150.406 34.9471 149.211 35.0608 148.187 35.3451C147.22 35.6294 146.423 36.0843 145.797 36.7098C145.171 37.3353 144.772 38.2451 144.488 39.3255C144.203 40.4059 144.089 41.7706 144.089 43.3627V45.9216C144.089 47.6275 144.203 48.9922 144.431 50.0726C144.659 51.1529 145.057 52.0059 145.626 52.5745C146.138 53.2 146.878 53.598 147.732 53.8255C148.585 54.0529 149.667 54.1667 150.919 54.1667C151.26 54.1667 151.659 54.1667 152.057 54.1098C152.455 54.1098 152.854 54.0529 153.309 53.9961V46.3765H149.78L150.35 41.7137H159V57.749C157.919 58.1471 156.553 58.4314 154.902 58.6588C153.309 58.8863 151.602 59 149.894 59C147.732 59 145.911 58.7157 144.431 58.2039C142.894 57.6922 141.699 56.8961 140.732 55.8157C139.764 54.7353 139.081 53.4275 138.626 51.7784C138.171 50.1863 138 48.2529 138 46.0922V43.1922C138 41.202 138.228 39.4392 138.626 37.7902C139.081 36.1412 139.821 34.7765 140.846 33.6392C141.87 32.502 143.236 31.5922 144.886 30.9667C146.537 30.3412 148.642 30 151.146 30C152.512 30 153.878 30.1137 155.187 30.3412C156.496 30.5686 157.577 30.8529 158.431 31.1373L157.463 35.6863C156.724 35.4588 155.87 35.2882 154.959 35.1176C154.049 35.0039 153.024 34.9471 151.829 34.9471Z" fill="#00B4F0"/>
<path d="M187 30V59H180.959V46.3455H171.041V59H165V30H171.041V41.1899H181.016V30H187Z" fill="#00B4F0"/>
<path d="M218 30L217.366 35.0384H207.597V41.0727H216.627L215.993 46.1111H207.597V59H202V30H218Z" fill="#00B4F0"/>
<path d="M229 59H223V30H229V59Z" fill="#00B4F0"/>
<path d="M233 30H242.404C244.842 30 246.873 30.3508 248.499 30.994C250.124 31.6371 251.401 32.5726 252.388 33.6835C253.375 34.8528 254.013 36.1976 254.42 37.8347C254.826 39.4718 255 41.2258 255 43.0968V45.8448C255 47.7742 254.826 49.5282 254.42 51.1653C254.013 52.8024 253.317 54.1472 252.388 55.3165C251.401 56.4859 250.124 57.3629 248.499 58.006C246.873 58.6492 244.842 59 242.404 59H233.058V30H233ZM239.095 53.9133H241.765C242.926 53.9133 243.913 53.7964 244.842 53.504C245.712 53.2117 246.467 52.744 247.047 52.1008C247.628 51.4577 248.034 50.5806 248.325 49.5282C248.615 48.4758 248.731 47.1311 248.731 45.494V43.3306C248.731 41.6935 248.615 40.3488 248.325 39.2964C248.034 38.2439 247.628 37.3669 247.047 36.7238C246.467 36.0806 245.77 35.6129 244.842 35.379C243.971 35.1452 242.926 34.9698 241.765 34.9698H239.095V53.9133Z" fill="#00B4F0"/>
<path d="M275.945 30L275.34 34.8528H264.832V41.1673H274.735L274.184 46.0202H264.887V54.1472H276L275.395 59H259V30H275.945Z" fill="#00B4F0"/>
<path d="M286.215 30V53.9616H298L297.296 59H280V30H286.215Z" fill="#00B4F0"/>
<path d="M307 59H302V30H307V59Z" fill="#00B4F0"/>
<path d="M334 30L333.307 35.0384H325.563V59H319.437V35.0384H311L311.693 30H334Z" fill="#00B4F0"/>
<path d="M347.617 41.3071L353.633 30H360L350.537 46.3455V59H344.346V46.3455L335 30H341.776L347.617 41.3071Z" fill="#00B4F0"/>
<path d="M42.1132 85C36.4528 85 30.9057 83.856 25.7547 81.6824C20.717 79.5659 16.2453 76.4771 12.3962 72.5875C8.54717 68.6978 5.49057 64.1218 3.39623 59.0882C1.13208 53.7685 -1.07963e-07 48.22 -1.07963e-07 42.4428C-1.07963e-07 36.7227 1.13208 31.1171 3.28302 25.9118C5.37736 20.821 8.43396 16.3022 12.283 12.4125C16.1321 8.52288 20.6604 5.43405 25.6415 3.31763C30.8491 1.08681 36.3396 1.09101e-07 42 1.09101e-07C47.6604 1.09101e-07 53.2076 1.14401 58.3585 3.31763C63.3962 5.43405 67.8679 8.52288 71.717 12.4125C75.566 16.3022 78.6227 20.8782 80.717 25.9118C82.9245 31.1743 84 36.7227 84 42.4428C84 48.1628 82.8679 53.7685 80.717 58.9737C78.6227 64.0646 75.566 68.5834 71.717 72.4731C67.8679 76.3627 63.3396 79.4515 58.3585 81.568C53.3208 83.856 47.7736 85 42.1132 85ZM42.1132 4.34724C21.3396 4.34724 4.41509 21.4502 4.41509 42.4428C4.41509 63.4354 21.3396 80.5384 42.1132 80.5384C62.8868 80.5384 79.8113 63.4354 79.8113 42.4428C79.8113 21.4502 62.8868 4.34724 42.1132 4.34724Z" fill="#00B4F0"/>
<path d="M54.8 60.7368V30.5158C56.6857 29.8789 58 28.0842 58 26C58 23.3947 55.8857 21.2526 53.3143 21.2526C50.7429 21.2526 48.6286 23.3947 48.6286 26C48.6286 28.0263 49.8286 29.7053 51.6 30.4579V44.5263L32.1714 35.379V24.2632C34.0571 23.6263 35.3714 21.8316 35.3714 19.7474C35.3714 17.1421 33.2571 15 30.6857 15C28.1143 15 26 17.1421 26 19.7474C26 21.7737 27.2 23.4526 28.9714 24.2053V54.6C27.2571 55.2947 26 57.0316 26 59.0579C26 61.6632 28.1143 63.8053 30.6857 63.8053C33.2571 63.8053 35.3714 61.6632 35.3714 59.0579C35.3714 56.9737 34.0571 55.1789 32.1714 54.5421V39.2L51.6 48.3474V60.7947C49.8857 61.4895 48.6286 63.2263 48.6286 65.2526C48.6286 67.8579 50.7429 70 53.3143 70C55.8857 70 58 67.8579 58 65.2526C58 63.1105 56.6857 61.3737 54.8 60.7368Z" fill="#00B4F0"/>
</svg>
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 360 120.9"><defs><style>.cls-1{fill:#fff;}</style></defs><title>Artboard 1</title><polygon class="cls-1" points="28.69 60.97 28.69 81.59 10.31 81.59 10.31 60.97 0 60.97 0 108.64 10.31 108.64 10.31 89.81 28.69 89.81 28.69 108.64 39 108.64 39 60.97 28.69 60.97"/><path class="cls-1" d="M45,75.17h9.27v33.47H45Zm-.6-9.57a5.31,5.31,0,1,1,5.53,5.08h-.3a5,5,0,0,1-5.23-4.93V65.6"/><path class="cls-1" d="M96.84,61h9.26V80.55c1.65-3.29,5.38-6.13,10.91-6.13,6.58,0,11.21,4,11.21,12.85v21.22H119V88.62c0-4.49-1.79-6.73-5.82-6.73s-7,2.54-7,7.47v19.28H96.84Z"/><polygon class="cls-1" points="145.41 60.97 174.69 60.97 174.69 69.04 155.72 69.04 155.72 83.09 173.05 83.09 173.05 90.86 155.72 90.86 155.72 108.64 145.41 108.64 145.41 60.97"/><path class="cls-1" d="M178.88,75.17h9.26v33.47h-9.26Zm-.75-9.57a5.32,5.32,0,0,1,5.08-5.53,5.25,5.25,0,0,1,5.53,5.09,5.31,5.31,0,0,1-5.08,5.52h-.3a5,5,0,0,1-5.23-4.93V65.6"/><path class="cls-1" d="M216.84,92.05v-.44c0-6.73-3-10-8.07-10S200.7,85,200.7,91.76v.44c0,6.73,3.14,9.87,7.77,9.87,4.93,0,8.37-3.14,8.37-10m-25.56.3v-.44c0-11.06,6.28-17.49,14.65-17.49,5.38,0,8.67,2.39,10.61,5.68V61h9.26v47.67h-9.26v-5.53a12.19,12.19,0,0,1-10.76,6.28c-8.07,0-14.5-5.83-14.5-17"/><path class="cls-1" d="M82.94,91v-.45c0-5.68-3-9.12-8.07-9.12s-8.07,3.74-8.07,9.27v.45c0,5.53,3.29,9,7.92,9,4.78,0,8.22-3.43,8.22-9.11M57.53,109.39H66.8c.6,3,2.69,4.78,7.47,4.78,5.68,0,8.52-3,8.52-8.22v-5.08A12.69,12.69,0,0,1,72,107.15c-8.07,0-14.65-6.13-14.65-16v-.45c0-9.56,6.43-16.29,14.65-16.29,5.38,0,8.67,2.39,10.61,5.68V75.17h9.27V106c0,9.87-6.58,15-17.64,15s-15.69-4.64-16.74-11.51"/><path class="cls-1" d="M253.45,88.32c-.3-5.08-2.84-7.47-7.32-7.47-4.19,0-7,2.69-7.62,7.47Zm-24.51,4v-.44c0-10.61,7.47-17.34,17.19-17.34,8.66,0,16.29,5.08,16.29,17v2.54H238.36c.3,5.53,3.28,8.81,8.36,8.81,4.34,0,6.43-1.79,7-4.63h8.82c-1.05,7.17-6.88,11.21-16,11.21-10.32-.15-17.64-6.58-17.64-17.19"/><rect class="cls-1" x="265.85" y="60.97" width="9.27" height="47.67"/><path class="cls-1" d="M280.2,75.17h9.26v33.47H280.2Zm-.75-9.57A5.31,5.31,0,1,1,285,70.68h-.3a5,5,0,0,1-5.23-4.93V65.6"/><path class="cls-1" d="M294.25,98.63V67.85h9.26V75h7v6.57h-7V97.73c0,2.69,1.35,4,3.74,4a10,10,0,0,0,3.58-.6v7.17a18,18,0,0,1-5.67.9c-7.18-.15-10.91-3.74-10.91-10.61"/><polygon class="cls-1" points="338.03 75.02 330.56 95.19 322.49 75.02 312.48 75.02 325.78 105.06 319.95 119.85 328.92 119.85 347 75.02 338.03 75.02"/><path class="cls-1" d="M336.54,31.68,323.09,25.4V18.08a3.52,3.52,0,0,0,2.39-4.18,3.41,3.41,0,0,0-6.58,1.79,3.5,3.5,0,0,0,2.4,2.39V38.41A3.4,3.4,0,1,0,323.09,45a3.4,3.4,0,0,0,0-6.57V27.65l13.45,6.27v8.67a3.41,3.41,0,0,0,1.79,6.58A3.46,3.46,0,0,0,340.72,45a3.5,3.5,0,0,0-2.39-2.39V22.27a3.53,3.53,0,0,0,2.39-4.19,3.41,3.41,0,1,0-6.57,1.8,3.5,3.5,0,0,0,2.39,2.39Z"/><path class="cls-1" d="M329.81,1.94a28.25,28.25,0,1,0,28.25,28.25h0A28.33,28.33,0,0,0,329.81,1.94m0,58.58A30.26,30.26,0,1,1,360,30.19h0a30.33,30.33,0,0,1-30.19,30.33"/></svg>

Before

Width:  |  Height:  |  Size: 4.8 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

View file

@ -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);
}
}

View file

@ -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;

View file

@ -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 {

View file

@ -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

View file

@ -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 {

View file

@ -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

View file

@ -60,6 +60,7 @@
#include <shared/QtHelpers.h>
#include <shared/PlatformHelper.h>
#include <shared/GlobalAppProperties.h>
#include <GeometryUtil.h>
#include <StatTracker.h>
#include <Trace.h>
#include <ResourceScriptingInterface.h>
@ -154,7 +155,6 @@
#include <display-plugins/CompositorHelper.h>
#include <display-plugins/hmd/HmdDisplayPlugin.h>
#include <display-plugins/RefreshRateController.h>
#include <trackers/EyeTracker.h>
#include <avatars-renderer/ScriptAvatar.h>
#include <RenderableEntityItem.h>
#include <RenderableTextEntityItem.h>
@ -880,7 +880,6 @@ bool setupEssentials(int& argc, char** argv, bool runningMarkerExisted) {
DependencyManager::set<DdeFaceTracker>();
#endif
DependencyManager::set<EyeTracker>();
DependencyManager::set<AudioClient>();
DependencyManager::set<AudioScope>();
DependencyManager::set<DeferredLightingEffect>();
@ -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>();
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<DdeFaceTracker>()->setEnabled(false);
#endif
#ifdef HAVE_IVIEWHMD
DependencyManager::get<EyeTracker>()->setEnabled(false, true);
#endif
AnimDebugDraw::getInstance().shutdown();
@ -2831,9 +2821,6 @@ void Application::cleanupBeforeQuit() {
#ifdef HAVE_DDE
DependencyManager::destroy<DdeFaceTracker>();
#endif
#ifdef HAVE_IVIEWHMD
DependencyManager::destroy<EyeTracker>();
#endif
DependencyManager::destroy<ContextOverlayInterface>(); // Must be destroyed before TabletScriptingInterface
@ -2842,7 +2829,7 @@ void Application::cleanupBeforeQuit() {
DependencyManager::destroy<TabletScriptingInterface>();
DependencyManager::destroy<ToolbarScriptingInterface>();
DependencyManager::destroy<OffscreenUi>();
DependencyManager::destroy<OffscreenQmlSurfaceCache>();
_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<unsigned int>(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<unsigned int>(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>();
dialogsManager->toggleAddressBar();
}
break;
case Qt::Key_R:
if (isMeta && !event->isAutoRepeat()) {
if (isControlOrCommand && !event->isAutoRepeat()) {
DependencyManager::get<ScriptEngines>()->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>();
audioClient->setMuted(!audioClient->isMuted());
QSharedPointer<scripting::Audio> audioScriptingInterface = qSharedPointerDynamicCast<scripting::Audio>(DependencyManager::get<AudioScriptingInterface>());
@ -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<EyeTracker>();
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<EyeTracker>()->calibrate(1);
}
void Application::calibrateEyeTracker3Points() {
DependencyManager::get<EyeTracker>()->calibrate(3);
}
void Application::calibrateEyeTracker5Points() {
DependencyManager::get<EyeTracker>()->calibrate(5);
}
#endif
bool Application::exportEntities(const QString& filename,
const QVector<QUuid>& entityIDs,
const glm::vec3* givenOffset) {
@ -5842,8 +5801,8 @@ void Application::pushPostUpdateLambda(void* key, const std::function<void()>& 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<EyeTracker>();
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<Avatar>(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<DdeFaceTracker>()->reset();
#endif
DependencyManager::get<EyeTracker>()->reset();
_overlayConductor.centerUI();
getActiveDisplayPlugin()->resetSensors();
getMyAvatar()->reset(true, andReload);

View file

@ -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();

View file

@ -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<AvatarManager>();
auto avatar = avatarManager->getMyAvatar();
@ -725,45 +711,51 @@ Menu::Menu() {
DependencyManager::get<PickManager>().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);

View file

@ -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";

View file

@ -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<UserInputMapper>();
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<Avatar>(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(),

View file

@ -29,10 +29,12 @@
#include <ScriptEngine.h>
#include <SettingHandle.h>
#include <Sound.h>
#include <shared/Camera.h>
#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);

View file

@ -15,7 +15,7 @@
#include <recording/Deck.h>
#include <Rig.h>
#include <trackers/FaceTracker.h>
#include <trackers/EyeTracker.h>
#include <FaceshiftConstants.h>
#include "devices/DdeFaceTracker.h"
#include "Application.h"
@ -46,18 +46,37 @@ void MyHead::simulate(float deltaTime) {
auto player = DependencyManager::get<recording::Deck>();
// 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<EyeTracker>();
_isEyeTrackerConnected = eyeTracker->isTracking();
// if eye tracker is connected we should get the data here.
auto userInputMapper = DependencyManager::get<UserInputMapper>();
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);
}

View file

@ -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<MyAvatar*>(_owningAvatar);

View file

@ -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());
}

View file

@ -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.

View file

@ -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;

View file

@ -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;
}

View file

@ -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);

View file

@ -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);

View file

@ -17,7 +17,6 @@
#include <DependencyManager.h>
#include <GeometryUtil.h>
#include <trackers/FaceTracker.h>
#include <trackers/EyeTracker.h>
#include <Rig.h>
#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

View file

@ -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() {
}

View file

@ -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; }

View file

@ -245,9 +245,10 @@ QByteArray AvatarData::toByteArrayStateful(AvatarDataDetail dataDetail, bool dro
}
QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSentTime,
const QVector<JointData>& lastSentJointData,
AvatarDataPacket::SendStatus& sendStatus, bool dropFaceTracking, bool distanceAdjust,
glm::vec3 viewerPosition, QVector<JointData>* sentJointDataOut, int maxDataSize, AvatarDataRate* outboundDataRateOut) const {
const QVector<JointData>& lastSentJointData, AvatarDataPacket::SendStatus& sendStatus,
bool dropFaceTracking, bool distanceAdjust, glm::vec3 viewerPosition,
QVector<JointData>* 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);

View file

@ -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;
}

View file

@ -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 };

View file

@ -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<hifi::ByteArray>& 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;

View file

@ -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 };

View file

@ -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"),

View file

@ -181,6 +181,11 @@ enum class Action {
TRACKED_OBJECT_15,
SPRINT,
LEFT_EYE,
RIGHT_EYE,
LEFT_EYE_BLINK,
RIGHT_EYE_BLINK,
NUM_ACTIONS
};

View file

@ -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;
}
}

View file

@ -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

View file

@ -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 {

View file

@ -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"),

View file

@ -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
};

View file

@ -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;
}
}

View file

@ -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<QString> 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<float> _actionStates = std::vector<float>(toInt(Action::NUM_ACTIONS), 0.0f);
std::vector<float> _actionScales = std::vector<float>(toInt(Action::NUM_ACTIONS), 1.0f);
std::vector<float> _lastActionStates = std::vector<float>(toInt(Action::NUM_ACTIONS), 0.0f);
std::vector<bool> _actionStatesValid = std::vector<bool>(toInt(Action::NUM_ACTIONS), false);
std::vector<Pose> _poseStates = std::vector<Pose>(toInt(Action::NUM_ACTIONS));
std::vector<AxisValue> _lastStandardStates = std::vector<AxisValue>();
@ -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);

View file

@ -100,7 +100,7 @@ namespace controller {
_currentPose = value;
}
protected:
AxisValue _currentValue { 0.0f, 0 };
AxisValue _currentValue { 0.0f, 0, false };
Pose _currentPose {};
};

View file

@ -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);
}
}

View file

@ -32,7 +32,7 @@ public:
virtual void reset() override;
private:
AxisValue _currentValue { 0.0f, 0 };
AxisValue _currentValue { 0.0f, 0, false };
Pose _currentPose{};
};

View file

@ -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;
}

View file

@ -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<UserInputMapper>();
auto deviceProxy = userInputMapper->getDevice(_input);

View file

@ -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;

View file

@ -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; }

View file

@ -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; }

View file

@ -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; }

View file

@ -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) {

View file

@ -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) {

View file

@ -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 };
}

View file

@ -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) {

View file

@ -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 {

View file

@ -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);

View file

@ -24,6 +24,7 @@
#include <ui/OffscreenQmlSurface.h>
#include <ui/TabletScriptingInterface.h>
#include <EntityScriptingInterface.h>
#include <shared/LocalFileAccessGate.h>
#include "EntitiesRendererLogging.h"
#include <NetworkingConstants.h>
@ -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;

View file

@ -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);
}

View file

@ -15,6 +15,7 @@
#include <ByteCountCoding.h>
#include <GeometryUtil.h>
#include <shared/LocalFileAccessGate.h>
#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<bool>([&] {
return _localSafeContext;
});
}
void WebEntityItem::setAlpha(float alpha) {
withWriteLock([&] {
_needsRenderUpdate |= _alpha != alpha;

View file

@ -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

View file

@ -56,7 +56,7 @@ const int MAX_NUM_PIXELS_FOR_FBX_TEXTURE = 2048 * 2048;
using ShapeVertices = std::vector<glm::vec3>;
// 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;

View file

@ -120,7 +120,7 @@ namespace baker {
class BakerEngineBuilder {
public:
using Input = VaryingSet3<hfm::Model::Pointer, hifi::VariantHash, hifi::URL>;
using Output = VaryingSet4<hfm::Model::Pointer, MaterialMapping, std::vector<hifi::ByteArray>, std::vector<std::vector<hifi::ByteArray>>>;
using Output = VaryingSet5<hfm::Model::Pointer, MaterialMapping, std::vector<hifi::ByteArray>, std::vector<bool>, std::vector<std::vector<hifi::ByteArray>>>;
using JobModel = Task::ModelIO<BakerEngineBuilder, Input, Output>;
void build(JobModel& model, const Varying& input, Varying& output) {
const auto& hfmModelIn = input.getN<Input>(0);
@ -168,7 +168,8 @@ namespace baker {
const auto buildDracoMeshInputs = BuildDracoMeshTask::Input(meshesIn, normalsPerMesh, tangentsPerMesh).asVarying();
const auto buildDracoMeshOutputs = model.addJob<BuildDracoMeshTask>("BuildDracoMesh", buildDracoMeshInputs);
const auto dracoMeshes = buildDracoMeshOutputs.getN<BuildDracoMeshTask::Output>(0);
const auto materialList = buildDracoMeshOutputs.getN<BuildDracoMeshTask::Output>(1);
const auto dracoErrors = buildDracoMeshOutputs.getN<BuildDracoMeshTask::Output>(1);
const auto materialList = buildDracoMeshOutputs.getN<BuildDracoMeshTask::Output>(2);
// Parse flow data
const auto flowData = model.addJob<ParseFlowDataTask>("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<BuildModelTask>("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<BakerEngineBuilder::Output>().get2();
}
std::vector<std::vector<hifi::ByteArray>> Baker::getDracoMaterialLists() const {
std::vector<bool> Baker::getDracoErrors() const {
return _engine->getOutput().get<BakerEngineBuilder::Output>().get3();
}
std::vector<std::vector<hifi::ByteArray>> Baker::getDracoMaterialLists() const {
return _engine->getOutput().get<BakerEngineBuilder::Output>().get4();
}
};

View file

@ -33,6 +33,7 @@ namespace baker {
hfm::Model::Pointer getHFMModel() const;
MaterialMapping getMaterialMapping() const;
const std::vector<hifi::ByteArray>& getDracoMeshes() const;
std::vector<bool> 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<std::vector<hifi::ByteArray>> getDracoMaterialLists() const;

View file

@ -51,7 +51,7 @@ std::vector<hifi::ByteArray> createMaterialList(const hfm::Mesh& mesh) {
return materialList;
}
std::unique_ptr<draco::Mesh> createDracoMesh(const hfm::Mesh& mesh, const std::vector<glm::vec3>& normals, const std::vector<glm::vec3>& tangents, const std::vector<hifi::ByteArray>& materialList) {
std::tuple<std::unique_ptr<draco::Mesh>, bool> createDracoMesh(const hfm::Mesh& mesh, const std::vector<glm::vec3>& normals, const std::vector<glm::vec3>& tangents, const std::vector<hifi::ByteArray>& 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<draco::Mesh> createDracoMesh(const hfm::Mesh& mesh, const std::v
}
if (numTriangles == 0) {
return std::unique_ptr<draco::Mesh>();
return std::make_tuple(std::unique_ptr<draco::Mesh>(), false);
}
draco::TriangleSoupMeshBuilder meshBuilder;
@ -184,7 +184,7 @@ std::unique_ptr<draco::Mesh> 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<draco::Mesh>();
return std::make_tuple(std::unique_ptr<draco::Mesh>(), true);
}
// we need to modify unique attribute IDs for custom attributes
@ -201,7 +201,7 @@ std::unique_ptr<draco::Mesh> 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<bool> 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<draco::Mesh> dracoMesh;
std::tie(dracoMesh, dracoError) = createDracoMesh(mesh, normals, tangents, materialList);
dracoErrorsPerMesh[i] = dracoError;
if (dracoMesh) {
draco::Encoder encoder;

View file

@ -34,7 +34,7 @@ class BuildDracoMeshTask {
public:
using Config = BuildDracoMeshConfig;
using Input = baker::VaryingSet3<std::vector<hfm::Mesh>, baker::NormalsPerMesh, baker::TangentsPerMesh>;
using Output = baker::VaryingSet2<std::vector<hifi::ByteArray>, std::vector<std::vector<hifi::ByteArray>>>;
using Output = baker::VaryingSet3<std::vector<hifi::ByteArray>, std::vector<bool>, std::vector<std::vector<hifi::ByteArray>>>;
using JobModel = baker::Job::ModelIO<BuildDracoMeshTask, Input, Output, Config>;
void configure(const Config& config);

View file

@ -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. <em>Not used.</em>
* @property {Date} [expirationDate] - The date and time when the meta data expires. An invalid date means "never expires".
* @property {boolean} isValid - <code>true</code> if the item specified in the URL is in the cache, <code>false</code> 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. <em>Not used.</em>
* @property {boolean} [saveToDisk] - <code>true</code> if the cache item is allowed to be store on disk,
* <code>false</code> 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:
* <code>"ddd, dd MMM yyyy HH:mm:ss"</code>. 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:
* <code>"ddd, dd MMM yyyy HH:mm:ss"</code>. 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] - <code>true</code> 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

View file

@ -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 <code>response</code>.
* @property {string} url - The URL of the cache item.
*/
Promise BaseAssetScriptingInterface::loadFromCache(const QUrl& url, bool decompress, const QString& responseType) {
QVariantMap metaData = {
{ "_type", "cache" },

View file

@ -24,6 +24,22 @@
class BaseAssetScriptingInterface : public QObject {
Q_OBJECT
public:
/**jsdoc
* <p>Types of response that {@link Assets.decompressData}, {@link Assets.getAsset}, or {@link Assets.loadFromCache} may
* provide.</p>
* <table>
* <thead>
* <tr><th>Value</th><th>Description</th></tr>
* </thead>
* <tbody>
* <tr><td><code>"arraybuffer"</code></td><td>A binary <code>ArrayBuffer</code> object.</td></tr>
* <tr><td><code>"json"</code></td><td>A parsed <code>JSON</code> object.</td></tr>
* <tr><td><code>"text"</code></td><td>UTF-8 decoded <code>string</code> value.</td></tr>
* </tbody>
* </table>
* @typedef {string} Assets.ResponseType
*/
const QStringList RESPONSE_TYPES{ "text", "arraybuffer", "json" };
using Promise = MiniPromise::Promise;
QSharedPointer<AssetClient> 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 <code>"/"</code>.
* @function Assets.isValidPath
* @param {string} input
* @returns {boolean}
* @param {string} path - The path to check.
* @returns {boolean} <code>true</code> if the path is a valid path, <code>false</code> 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 <code>"/"</code>
* but must not end with a <code>"/"</code>.
* @function Assets.isValidFilePath
* @param {string} input
* @returns {boolean}
* @param {string} path - The path to check.
* @returns {boolean} <code>true</code> if the path is a valid file path, <code>false</code> 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 <code>"atp:"</code> 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, <code>""</code> 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} <code>true</code> if the hash is a valid SHA256 hexadecimal string, <code>false</code> 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 <code>data</code>.
*/
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 <code>data</code>, in hexadecimal format.
* @example <caption>Calculate the hash of some text.</caption>
* var text = "Hello world!";
* print("Hash: " + Assets.hashDataHex(text));
*/
QString hashDataHex(const QByteArray& data) { return hashData(data).toHex(); }

View file

@ -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;
}
}
}

View file

@ -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) {

View file

@ -528,6 +528,8 @@ void PhysicalEntitySimulation::handleChangedMotionStates(const VectorOfMotionSta
addOwnership(entityState);
} else if (entityState->shouldSendBid()) {
addOwnershipBid(entityState);
} else {
entityState->getEntity()->updateQueryAACube();
}
}
}

View file

@ -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);
}

View file

@ -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 - <code>""</code> 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
* <p>Available <code>responseType</code> values for use with @{link Assets.getAsset} and @{link Assets.loadFromCache} configuration option. </p>
* <table>
* <thead>
* <tr><th>responseType</th><th>typeof response value</th></tr>
* </thead>
* <tbody>
* <tr><td><code>"text"</code></td><td>contents returned as utf-8 decoded <code>String</code> value</td></tr>
* <tr><td><code>"arraybuffer"</code></td><td>contents as a binary <code>ArrayBuffer</code> object</td></tr>
* <tr><td><code>"json"</code></td><td>contents as a parsed <code>JSON</code> object</td></tr>
* </tbody>
* </table>
* Source and download options for {@link Assets.getAsset}.
* @typedef {object} Assets.GetOptions
* @property {boolean} [decompress=false] - <code>true</code> to gunzip decompress the downloaded data. Synonym:
* <code>compressed</code>.
* @property {Assets.ResponseType} [responseType="text"] - The desired result type.
* @property {string} url - The mapped path or hash to download. May have a leading <code>"atp:"</code>.
*/
/**jsdoc
* Result value returned by {@link Assets.getAsset}.
* @typedef {object} Assets.GetResult
* @property {number} [byteLength] - The number of bytes in the downloaded content in <code>response</code>.
* @property {boolean} cached - <code>true</code> if the item was retrieved from the cache, <code>false</code> if it was
* downloaded.
* @property {string} [contentType] - The automatically detected MIME type of the content.
* @property {boolean} [decompressed] - <code>true</code> if the content was decompressed, <code>false</code> 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, <code>undefined</code>.
* @property {string|object|ArrayBuffer} [response] - The downloaded content.
* @property {Assets.ResponseType} [responseType] - The type of the downloaded content in <code>response</code>.
* @property {string} [url] - The URL of the asset requested: the path with leading <code>"atp:"</code> if a path was
* requested, otherwise the requested URL.
* @property {boolean} [wasRedirected] - <code>true</code> if the downloaded data is the baked version of the asset,
* <code>false</code> 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 <code>"atp:"</code>.
*/
/**jsdoc
* Result value returned by {@link Assets.resolveAsset}.
* <p>Note: If resolving a hash, a file of that hash need not be present on the asset server for the hash to resolve.</p>
* @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 <code>atp:</code>.
* @property {string} [path] - The path to the asset.
* @property {string} [url] - The URL of the asset.
* @property {boolean} [wasRedirected] - <code>true</code> if the resolved data is for the baked version of the asset,
* <code>false</code> 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] - <code>true</code> if the data is decompressed.
* @property {string|object|ArrayBuffer} [response] - The decompressed data.
* @property {Assets.ResponseType} [responseType] - The type of the decompressed data in <code>response</code>.
*/
void AssetScriptingInterface::decompressData(QScriptValue options, QScriptValue scope, QScriptValue callback) {
auto data = options.property("data");
QByteArray dataByteArray = qscriptvalue_cast<QByteArray>(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 <code>-1</code> &ndash; <code>9</code>. <code>-1</code> means
* use the default gzip compression level, <code>0</code> means no compression, and <code>9</code> 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] - <code>true</code> if the data is compressed.
* @property {string} [contentType] - The MIME type of the compressed data, i.e., <code>"application/gzip"</code>.
* @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<QByteArray>(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] - <code>true</code> to gzip compress the content for upload and storage,
* <code>false</code> to upload and store the data without gzip compression. Synonym: <code>compressed</code>.
* @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
* <code>"atp:"</code>. IF not specified, no path-to-hash mapping is set.
* <p>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.</p>
*/
/**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] - <code>true</code> if the content stored is gzip compressed.
* @property {string} [contentType] - <code>"application/gzip"</code> if the content stored is gzip compressed.
* @property {string} [hash] - The SHA256 hash of the content.
* @property {string} [url] - The <code>atp:</code> 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 <code>"atp:"</code> or
* <code>"cache:"</code>.
*/
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] - <code>true</code> to gunzip decompress the cached data. Synonym:
* <code>compressed</code>.
* @property {Assets.ResponseType} [responseType=text] - The desired result type.
* @property {string} url - The URL of the asset to load from cache. Must start with <code>"atp:"</code> or
* <code>"cache:"</code>.
*/
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 <code>"atp:"</code> or
* <code>"cache:"</code>. If not specified, the URL is <code>"atp:"</code> 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()));

View file

@ -25,7 +25,14 @@
#include <QtNetwork/QNetworkDiskCache>
/**jsdoc
* The Assets API allows you to communicate with the Asset Browser.
* The <code>Assets</code> API provides facilities for interacting with the domain's asset server and the client cache.
* <p>Assets are stored in the asset server in files with SHA256 names. These files are mapped to user-friendly URLs of the
* format: <code>atp:/path/filename</code>. 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.</p>
* <p>The client cache can be access directly, using <code>"atp:"</code> or <code>"cache:"</code> URLs. Interface, avatar, and
* assignment client scripts can write to the cache. All script types can read from the cache.</p>
*
* @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 <code>atp:</code> 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.
* <p>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.</p>
* @function Assets.uploadData
* @param {string} data - The content to upload.
* @param {Assets~uploadDataCallback} callback - The function to call upon completion.
* @example <caption>Store a string in the asset server.</caption>
* 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: <code>atp:</code> followed by the assets's SHA256 hash.
* @param {Assets~downloadDataCallback} callback - The function to call upon completion.
* @example <caption>Store and retrieve a string from the asset server.</caption>
* 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 - <code>null</code> 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 <code>"atp:"</code>.
* @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 - <code>null</code> if the path was found, otherwise a description of the error.
* @param {string} hash - The hash value if the path was found, <code>""</code> 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 <caption>Report the hash of an asset server item.</caption>
* 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 - <code>null</code> 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 - <code>true</code> to enable baking of the asset, <code>false</code> 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 <code>callback</code> function is defined in. This object is bound to
* <code>this</code> 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 <code>scope</code>.
*/
/**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 - <code>null</code> 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 <code>"atp:"</code>.
* @param {object|Assets.CallbackDetails|Assets~getAssetCallback} scopeOrCallback - If an object, then the scope that
* the <code>callback</code> function is defined in. This object is bound to <code>this</code> when the function is
* called.
* <p>Otherwise, the function to call upon completion. This may be an inline function or a function identifier.</p>
* @param {Assets~getAssetCallback} [callback] - Used if <code>scopeOrCallback</code> specifies the scope.
* <p>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
* <code>scopeOrCallback</code>.</p>
* @example <caption>Retrieve a string from the asset server.</caption>
* 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 - <code>null</code> 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 <code>callback</code> function is defined in. This object is bound to <code>this</code> when the function is
* called.
* <p>Otherwise, the function to call upon completion. This may be an inline function or a function identifier.</p>
* @param {Assets~putAssetCallback} [callback] - Used if <code>scopeOrCallback</code> specifies the scope.
* <p>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
* <code>scopeOrCallback</code>.</p>
* @example <caption>Store a string in the asset server.</caption>
* 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.
* <p class="important">Not implemented: This type is not implemented yet.</p>
* @callback Assets~deleteAssetCallback
* @param {string} error - <code>null</code> 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.
* <p class="important">Not implemented: This method is not implemented yet.</p>
* @function Assets.deleteAsset
* @param {Assets.DeleteOptions} options - The content to delete and delete options.
* @param {object} scope - The scope that the <code>callback</code> 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 - <code>null</code> 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 <code>"atp:"</code>.
* @param {object|Assets.CallbackDetails|Assets~resolveAssetCallback} scopeOrCallback - If an object, then the scope that
* the <code>callback</code> function is defined in. This object is bound to <code>this</code> when the function is
* called.
* <p>Otherwise, the function to call upon completion. This may be an inline function or a function identifier.</p>
* @param {Assets~resolveAssetCallback} [callback] - Used if <code>scopeOrCallback</code> specifies the scope.
* <p>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
* <code>scopeOrCallback</code>.</p>
* @example <caption>Get the hash and URL for a path.</caption>
* 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 - <code>null</code> 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 <code>callback</code> function is defined in. This object is bound to <code>this</code> when the function is
* called.
* <p>Otherwise, the function to call upon completion. This may be an inline function or a function identifier.</p>
* @param {Assets~decompressDataCallback} [callback] - Used if <code>scopeOrCallback</code> specifies the scope.
* <p>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
* <code>scopeOrCallback</code>.</p>
*/
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 - <code>null</code> 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 <code>callback</code> function is defined in. This object is bound to <code>this</code> when the function is
* called.
* <p>Otherwise, the function to call upon completion. This may be an inline function or a function identifier.</p>
* @param {Assets~compressDataCallback} [callback] - Used if <code>scopeOrCallback</code> specifies the scope.
* <p>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
* <code>scopeOrCallback</code>.</p>
*/
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} <code>true</code> if the cache is initialized, <code>false</code> 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 - <em>Not used.</em>
* @returns {boolean} <code>true</code> if the script is an Interface, avatar, or assignment client script,
* <code>false</code> if the script is a client entity or server entity script.
* @example <caption>Report whether the script can write to the cache.</caption>
* 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 - <code>null</code> 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 <code>callback</code> function is defined in. This object is bound to <code>this</code> when the function is
* called.
* <p>Otherwise, the function to call upon completion. This may be an inline function or a function identifier.</p>
* @param {Assets~getCacheStatusCallback} [callback] - Used if <code>scopeOrCallback</code> specifies the scope.
* <p>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
* <code>scopeOrCallback</code>.</p>
* @example <caption>Report the cache status.</caption>
* 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 - <code>null</code> 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 <code>"atp:"</code>
* or <code>"cache:"</code>.
* @param {object|Assets.CallbackDetails|Assets~queryCacheMetaCallback} scopeOrCallback - If an object, then the scope that
* the <code>callback</code> function is defined in. This object is bound to <code>this</code> when the function is
* called.
* <p>Otherwise, the function to call upon completion. This may be an inline function or a function identifier.</p>
* @param {Assets~queryCacheMetaCallback} [callback] - Used if <code>scopeOrCallback</code> specifies the scope.
* <p>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
* <code>scopeOrCallback</code>.</p>
* @example <caption>Report details of a string store in the cache.</caption>
* 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 - <code>null</code> 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 <code>"atp:"</code>
* or <code>"cache:"</code>.
* @param {object|Assets.CallbackDetails|Assets~loadFromCacheCallback} scopeOrCallback - If an object, then the scope that
* the <code>callback</code> function is defined in. This object is bound to <code>this</code> when the function is
* called.
* <p>Otherwise, the function to call upon completion. This may be an inline function or a function identifier.</p>
* @param {Assets~loadFromCacheCallback} [callback] - Used if <code>scopeOrCallback</code> specifies the scope.
* <p>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
* <code>scopeOrCallback</code>.</p>
* @example <caption>Retrieve a string from the cache.</caption>
* 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 - <code>null</code> 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.
* <p>Note: Can only be used in Interface, avatar, and assignment client scripts.</p>
* @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 <code>callback</code> function is defined in. This object is bound to <code>this</code> when the function is
* called.
* <p>Otherwise, the function to call upon completion. This may be an inline function or a function identifier.</p>
* @param {Assets~saveToCacheCallback} [callback] - Used if <code>scopeOrCallback</code> specifies the scope.
* <p>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
* <code>scopeOrCallback</code>.</p>
* @example <caption>Save a string in the cache.</caption>
* 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.
* <p>Note: Can only be used in Interface, avatar, and assignment client scripts.</p>
* @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 <code>"atp:"</code> or
* <code>"cache:"</code>.
* @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 <code>callback</code> function is defined in. This object is bound to <code>this</code> when the function is
* called.
* <p>Otherwise, the function to call upon completion. This may be an inline function or a function identifier.</p>
* @param {Assets~saveToCacheCallback} [callback] - Used if <code>scopeOrCallback</code> specifies the scope.
* <p>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
* <code>scopeOrCallback</code>.</p>
*/
Q_INVOKABLE void saveToCache(const QUrl& url, const QByteArray& data, const QVariantMap& metadata,
QScriptValue scope, QScriptValue callback = QScriptValue());
protected:

View file

@ -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<std::pair<glm::vec3, glm::vec3>>& lin
auto point2 = translation + rotation * line.second;
_rays.push_back(Ray(point1, point2, color));
}
}
}

View file

@ -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 <caption>Briefly draw a debug marker in front of your avatar, in world coordinates.</caption>
* 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 <caption>Briefly draw a debug marker in front of your avatar, in avatar coordinates.</caption>
* 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<glm::quat, glm::vec3, glm::vec4>;
using MarkerInfo = std::tuple<glm::quat, glm::vec3, glm::vec4, float>;
using MarkerMap = std::map<QString, MarkerInfo>;
using Ray = std::tuple<glm::vec3, glm::vec3, glm::vec4>;
using Rays = std::vector<Ray>;

View file

@ -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);
}
}

View file

@ -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);

View file

@ -90,6 +90,17 @@ public:
using Config = JobConfig;
};
/**jsdoc
* @namespace Workload
*
* @hifi-interface
* @hifi-client-entity
* @hifi-avatar
*
* @property {number} cpuRunTime - <em>Read-only.</em>
* @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(); }

View file

@ -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 <QFuture>
#include <QtConcurrent/QtConcurrentRun>
#include <SharedUtil.h>
#include "Logging.h"
#include <OctreeConstants.h>
#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<EyeTracker>();
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<int> 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

View file

@ -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 <QObject>
#include <QFutureWatcher>
#include <glm/glm.hpp>
#include <DependencyManager.h>
#ifdef HAVE_IVIEWHMD
#include <iViewHMDAPI.h>
#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<int> _startStreamingWatcher;
};
#endif // hifi_EyeTracker_h

View file

@ -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 {

View file

@ -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

View file

@ -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;

View file

@ -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);
}
}

View file

@ -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();

View file

@ -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 }
}

View file

@ -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;

View file

@ -92,35 +92,33 @@
</tr>
</tbody>
</table>
<?js= self.partial('details.tmpl', doc) ?>
<?js if (doc.examples && doc.examples.length) { ?>
<h3>Example<?js= doc.examples.length > 1? 's':'' ?></h3>
<?js= self.partial('examples.tmpl', doc.examples) ?>
<?js } ?>
<?js } ?>
<?js } else { ?>
<h3>Description</h3>
<?js if (doc.description) { ?>
<p><?js= doc.description ?></p>
<?js } ?>
<?js
var classes = self.find({kind: 'class', memberof: doc.longname});
if (!isGlobalPage && classes && classes.length) {
?>
<h3 id="#class">Classes</h3>
<?js classes.forEach(function(c) { ?>
<p><?js= self.linkto(c.longname, c.name) ?></p>
<?js }); ?>
<?js } ?>
<?js= self.partial('details.tmpl', doc) ?>
<?js if (doc.examples && doc.examples.length) { ?>
<h3>Example<?js= doc.examples.length > 1? 's':'' ?></h3>
<?js= self.partial('examples.tmpl', doc.examples) ?>
<?js } ?>
<?js
var classes = self.find({kind: 'class', memberof: doc.longname});
if (!isGlobalPage && classes && classes.length) {
?>
<h3 id="#class">Classes</h3>
<?js classes.forEach(function(c) { ?>
<p><?js= self.linkto(c.longname, c.name) ?></p>
<?js }); ?>
<?js } ?>
<?js= self.partial('details.tmpl', doc) ?>
<?js } ?>
</div>

View file

@ -125,7 +125,7 @@ exports.handlers = {
if (rows.length > 0) {
var availableIn = "<p class='availableIn'><b>Supported Script Types:</b> " + rows.join(" &bull; ") + "</p>";
e.doclet.description = (e.doclet.description ? e.doclet.description : "") + availableIn;
e.doclet.description = availableIn + (e.doclet.description ? e.doclet.description : "");
}
}