diff --git a/cmake/externals/wasapi/CMakeLists.txt b/cmake/externals/wasapi/CMakeLists.txt index b085eefb0c..d4d4b42e10 100644 --- a/cmake/externals/wasapi/CMakeLists.txt +++ b/cmake/externals/wasapi/CMakeLists.txt @@ -6,8 +6,8 @@ if (WIN32) include(ExternalProject) ExternalProject_Add( ${EXTERNAL_NAME} - URL http://hifi-public.s3.amazonaws.com/dependencies/qtaudio_wasapi6.zip - URL_MD5 fcac808c1ba0b0f5b44ea06e2612ebab + URL http://hifi-public.s3.amazonaws.com/dependencies/qtaudio_wasapi7.zip + URL_MD5 bc2861e50852dd590cdc773a14a041a7 CONFIGURE_COMMAND "" BUILD_COMMAND "" INSTALL_COMMAND "" diff --git a/domain-server/resources/describe-settings.json b/domain-server/resources/describe-settings.json index 58b1df08c1..20d2711743 100644 --- a/domain-server/resources/describe-settings.json +++ b/domain-server/resources/describe-settings.json @@ -372,6 +372,13 @@ "help": "Password used for basic HTTP authentication. Leave this blank if you do not want to change it.", "value-hidden": true }, + { + "name": "verify_http_password", + "label": "Verify HTTP Password", + "type": "password", + "help": "Must match the password entered above for change to be saved.", + "value-hidden": true + }, { "name": "maximum_user_capacity", "label": "Maximum User Capacity", diff --git a/domain-server/resources/web/css/style.css b/domain-server/resources/web/css/style.css index ad426671a4..553f408e15 100644 --- a/domain-server/resources/web/css/style.css +++ b/domain-server/resources/web/css/style.css @@ -125,6 +125,10 @@ tr.new-row { background-color: #dff0d8; } +tr.invalid-input { + background-color: #f2dede; +} + .graphable-stat { text-align: center; color: #5286BC; diff --git a/domain-server/resources/web/settings/js/settings.js b/domain-server/resources/web/settings/js/settings.js index c31d6e2dfc..b04d55b9eb 100644 --- a/domain-server/resources/web/settings/js/settings.js +++ b/domain-server/resources/web/settings/js/settings.js @@ -38,14 +38,15 @@ var Settings = { DOMAIN_ID_SELECTOR: '[name="metaverse.id"]', ACCESS_TOKEN_SELECTOR: '[name="metaverse.access_token"]', PLACES_TABLE_ID: 'places-table', - FORM_ID: 'settings-form' + FORM_ID: 'settings-form', + INVALID_ROW_CLASS: 'invalid-input' }; var viewHelpers = { getFormGroup: function(keypath, setting, values, isAdvanced) { form_group = "
"; setting_value = _(values).valueForKeyPath(keypath); @@ -215,8 +216,8 @@ $(document).ready(function(){ sibling = sibling.next(); } - if (sibling.hasClass(Settings.ADD_DEL_BUTTONS_CLASS)) { - sibling.find('.' + Settings.ADD_ROW_BUTTON_CLASS).click(); + // for tables with categories we add the entry and setup the new row on enter + if (sibling.find("." + Settings.ADD_CATEGORY_BUTTON_CLASS).length) { sibling.find("." + Settings.ADD_CATEGORY_BUTTON_CLASS).click(); // set focus to the first input in the new row @@ -891,39 +892,152 @@ function reloadSettings(callback) { }); } +function validateInputs() { + // check if any new values are bad + var tables = $('table'); + + var inputsValid = true; + + var tables = $('table'); + + // clear any current invalid rows + $('tr.' + Settings.INVALID_ROW_CLASS).removeClass(Settings.INVALID_ROW_CLASS); + + function markParentRowInvalid(rowChild) { + $(rowChild).closest('tr').addClass(Settings.INVALID_ROW_CLASS); + } + + _.each(tables, function(table) { + var inputs = $(table).find('tr.' + Settings.NEW_ROW_CLASS + ':not([data-category]) input[data-changed="true"]'); + + var empty = false; + + _.each(inputs, function(input){ + var inputVal = $(input).val(); + + if (inputVal.length === 0) { + empty = true + + markParentRowInvalid(input); + return; + } + }); + + if (empty) { + showErrorMessage("Error", "Empty field(s)"); + inputsValid = false; + return + } + + // validate keys specificially for spaces and equality to an existing key + var newKeys = $(table).find('tr.' + Settings.NEW_ROW_CLASS + ' td.key'); + + var keyWithSpaces = false; + var duplicateKey = false; + + _.each(newKeys, function(keyCell) { + var keyVal = $(keyCell).children('input').val(); + + if (keyVal.indexOf(' ') !== -1) { + keyWithSpaces = true; + markParentRowInvalid(keyCell); + return; + } + + // make sure we don't have duplicate keys in the table + var otherKeys = $(table).find('td.key').not(keyCell); + _.each(otherKeys, function(otherKeyCell) { + var keyInput = $(otherKeyCell).children('input'); + + if (keyInput.length) { + if ($(keyInput).val() == keyVal) { + duplicateKey = true; + } + } else if ($(otherKeyCell).html() == keyVal) { + duplicateKey = true; + } + + if (duplicateKey) { + markParentRowInvalid(keyCell); + return; + } + }); + + }); + + if (keyWithSpaces) { + showErrorMessage("Error", "Key contains spaces"); + inputsValid = false; + return + } + + if (duplicateKey) { + showErrorMessage("Error", "Two keys cannot be identical"); + inputsValid = false; + return; + } + }); + + return inputsValid; +} var SETTINGS_ERROR_MESSAGE = "There was a problem saving domain settings. Please try again!"; function saveSettings() { - // disable any inputs not changed - $("input:not([data-changed])").each(function(){ - $(this).prop('disabled', true); - }); - // grab a JSON representation of the form via form2js - var formJSON = form2js('settings-form', ".", false, cleanupFormValues, true); + if (validateInputs()) { + // POST the form JSON to the domain-server settings.json endpoint so the settings are saved - // check if we've set the basic http password - if so convert it to base64 - if (formJSON["security"]) { - var password = formJSON["security"]["http_password"]; - if (password && password.length > 0) { - formJSON["security"]["http_password"] = sha256_digest(password); + // disable any inputs not changed + $("input:not([data-changed])").each(function(){ + $(this).prop('disabled', true); + }); + + // grab a JSON representation of the form via form2js + var formJSON = form2js('settings-form', ".", false, cleanupFormValues, true); + + // check if we've set the basic http password - if so convert it to base64 + if (formJSON["security"]) { + var password = formJSON["security"]["http_password"]; + if (password && password.length > 0) { + formJSON["security"]["http_password"] = sha256_digest(password); + } + } + + // verify that the password and confirmation match before saving + var canPost = true; + + if (formJSON["security"]) { + var password = formJSON["security"]["http_password"]; + var verify_password = formJSON["security"]["verify_http_password"]; + + if (password && password.length > 0) { + if (password != verify_password) { + bootbox.alert({"message": "Passwords must match!", "title":"Password Error"}); + canPost = false; + } else { + formJSON["security"]["http_password"] = sha256_digest(password); + delete formJSON["security"]["verify_http_password"]; + } + } + } + + console.log("----- SAVING ------"); + console.log(formJSON); + + // re-enable all inputs + $("input").each(function(){ + $(this).prop('disabled', false); + }); + + // remove focus from the button + $(this).blur(); + + if (canPost) { + // POST the form JSON to the domain-server settings.json endpoint so the settings are saved + postSettings(formJSON); } } - - console.log("----- SAVING ------"); - console.log(formJSON); - - // re-enable all inputs - $("input").each(function(){ - $(this).prop('disabled', false); - }); - - // remove focus from the button - $(this).blur(); - - // POST the form JSON to the domain-server settings.json endpoint so the settings are saved - postSettings(formJSON); } $('body').on('click', '.save-button', function(e){ @@ -1100,8 +1214,9 @@ function makeTable(setting, keypath, setting_value) { if (setting.can_add_new_categories) { html += makeTableCategoryInput(setting, numVisibleColumns); } + if (setting.can_add_new_rows || setting.can_add_new_categories) { - html += makeTableInputs(setting, {}, ""); + html += makeTableHiddenInputs(setting, {}, ""); } } html += "" @@ -1127,7 +1242,7 @@ function makeTableCategoryHeader(categoryKey, categoryValue, numVisibleColumns, return html; } -function makeTableInputs(setting, initialValues, categoryValue) { +function makeTableHiddenInputs(setting, initialValues, categoryValue) { var html = ""; @@ -1138,7 +1253,7 @@ function makeTableInputs(setting, initialValues, categoryValue) { if (setting.key) { html += "\ - \ + \ " } @@ -1147,14 +1262,14 @@ function makeTableInputs(setting, initialValues, categoryValue) { if (col.type === "checkbox") { html += "" + - "" + ""; } else { html += "" + - "" + ""; @@ -1234,49 +1349,17 @@ function addTableRow(row) { var columns = row.parent().children('.' + Settings.DATA_ROW_CLASS); + var input_clone = row.clone(); + if (!isArray) { - // Check key spaces - var key = row.children(".key").children("input").val() - if (key.indexOf(' ') !== -1) { - showErrorMessage("Error", "Key contains spaces") - return - } - // Check keys with the same name - var equals = false; - _.each(columns.children(".key"), function(element) { - if ($(element).text() === key) { - equals = true - return - } - }) - if (equals) { - showErrorMessage("Error", "Two keys cannot be identical") - return - } + // show the key input + var keyInput = row.children(".key").children("input"); } - // Check empty fields - var empty = false; - _.each(row.children('.' + Settings.DATA_COL_CLASS + ' input'), function(element) { - if ($(element).val().length === 0) { - empty = true - return - } - }) - - if (empty) { - showErrorMessage("Error", "Empty field(s)") - return - } - - var input_clone = row.clone() - // Change input row to data row - var table = row.parents("table") - var setting_name = table.attr("name") - var full_name = setting_name + "." + key - row.addClass(Settings.DATA_ROW_CLASS + " " + Settings.NEW_ROW_CLASS) - row.removeClass("inputs") + var table = row.parents("table"); + var setting_name = table.attr("name"); + row.addClass(Settings.DATA_ROW_CLASS + " " + Settings.NEW_ROW_CLASS); _.each(row.children(), function(element) { if ($(element).hasClass("numbered")) { @@ -1298,56 +1381,43 @@ function addTableRow(row) { anchor.addClass(Settings.DEL_ROW_SPAN_CLASSES) } else if ($(element).hasClass("key")) { var input = $(element).children("input") - $(element).html(input.val()) - input.remove() + input.show(); } else if ($(element).hasClass(Settings.DATA_COL_CLASS)) { - // Hide inputs - var input = $(element).find("input") - var isCheckbox = false; - var isTime = false; - if (input.hasClass("table-checkbox")) { - input = $(input).parent(); - isCheckbox = true; - } else if (input.hasClass("table-time")) { - input = $(input).parent(); - isTime = true; - } + // show inputs + var input = $(element).find("input"); + input.show(); - var val = input.val(); - if (isCheckbox) { - // don't hide the checkbox - val = $(input).find("input").is(':checked'); - } else if (isTime) { - // don't hide the time - } else { - input.attr("type", "hidden") - } + var isCheckbox = input.hasClass("table-checkbox"); if (isArray) { var row_index = row.siblings('.' + Settings.DATA_ROW_CLASS).length - var key = $(element).attr('name') + var key = $(element).attr('name'); // are there multiple columns or just one? // with multiple we have an array of Objects, with one we have an array of whatever the value type is var num_columns = row.children('.' + Settings.DATA_COL_CLASS).length if (isCheckbox) { - $(input).find("input").attr("name", setting_name + "[" + row_index + "]" + (num_columns > 1 ? "." + key : "")) + input.attr("name", setting_name + "[" + row_index + "]" + (num_columns > 1 ? "." + key : "")) } else { input.attr("name", setting_name + "[" + row_index + "]" + (num_columns > 1 ? "." + key : "")) } } else { - input.attr("name", full_name + "." + $(element).attr("name")) + // because the name of the setting in question requires the key + // setup a hook to change the HTML name of the element whenever the key changes + var colName = $(element).attr("name"); + keyInput.on('change', function(){ + input.attr("name", setting_name + "." + $(this).val() + "." + colName); + }); } if (isCheckbox) { $(input).find("input").attr("data-changed", "true"); } else { input.attr("data-changed", "true"); - $(element).append(val); } } else { - console.log("Unknown table element") + console.log("Unknown table element"); } }); @@ -1377,7 +1447,12 @@ function deleteTableRow($row) { $row.empty(); if (!isArray) { - $row.html(""); + if ($row.attr('name')) { + $row.html(""); + } else { + // for rows that didn't have a key, simply remove the row + $row.remove(); + } } else { if ($table.find('.' + Settings.DATA_ROW_CLASS + "[data-category='" + categoryName + "']").length <= 1) { // This is the last row of the category, so delete the header diff --git a/interface/resources/QtWebEngine/UIDelegates/Menu.qml b/interface/resources/QtWebEngine/UIDelegates/Menu.qml index 21c5e71394..5176d9d11e 100644 --- a/interface/resources/QtWebEngine/UIDelegates/Menu.qml +++ b/interface/resources/QtWebEngine/UIDelegates/Menu.qml @@ -33,6 +33,7 @@ Item { propagateComposedEvents: true acceptedButtons: "AllButtons" onClicked: { + menu.visible = false; menu.done(); mouse.accepted = false; } diff --git a/interface/resources/QtWebEngine/UIDelegates/MenuItem.qml b/interface/resources/QtWebEngine/UIDelegates/MenuItem.qml index 561a8926e1..1890fcb81d 100644 --- a/interface/resources/QtWebEngine/UIDelegates/MenuItem.qml +++ b/interface/resources/QtWebEngine/UIDelegates/MenuItem.qml @@ -32,6 +32,7 @@ Item { MouseArea { anchors.fill: parent onClicked: { + menu.visible = false; root.triggered(); menu.done(); } diff --git a/interface/resources/controllers/standard_navigation.json b/interface/resources/controllers/standard_navigation.json index 81096a7230..a557ba7b45 100644 --- a/interface/resources/controllers/standard_navigation.json +++ b/interface/resources/controllers/standard_navigation.json @@ -1,50 +1,12 @@ { - "name": "Standard to Action", - "when": "Application.NavigationFocused", + "name": "Standard to UI Navigation Action", "channels": [ { "from": "Standard.DU", "to": "Actions.UiNavVertical" }, { "from": "Standard.DD", "to": "Actions.UiNavVertical", "filters": "invert" }, { "from": "Standard.DL", "to": "Actions.UiNavLateral", "filters": "invert" }, { "from": "Standard.DR", "to": "Actions.UiNavLateral" }, { "from": "Standard.LB", "to": "Actions.UiNavGroup","filters": "invert" }, - { "from": "Standard.RB", "to": "Actions.UiNavGroup" }, - { "from": [ "Standard.A", "Standard.X" ], "to": "Actions.UiNavSelect" }, - { "from": [ "Standard.B", "Standard.Y" ], "to": "Actions.UiNavBack" }, - { "from": [ "Standard.RTClick", "Standard.LTClick" ], "to": "Actions.UiNavSelect" }, - { - "from": "Standard.LX", "to": "Actions.UiNavLateral", - "filters": [ - { "type": "deadZone", "min": 0.95 }, - "constrainToInteger", - { "type": "pulse", "interval": 0.4 } - ] - }, - { - "from": "Standard.LY", "to": "Actions.UiNavVertical", - "filters": [ - "invert", - { "type": "deadZone", "min": 0.95 }, - "constrainToInteger", - { "type": "pulse", "interval": 0.4 } - ] - }, - { - "from": "Standard.RX", "to": "Actions.UiNavLateral", - "filters": [ - { "type": "deadZone", "min": 0.95 }, - "constrainToInteger", - { "type": "pulse", "interval": 0.4 } - ] - }, - { - "from": "Standard.RY", "to": "Actions.UiNavVertical", - "filters": [ - "invert", - { "type": "deadZone", "min": 0.95 }, - "constrainToInteger", - { "type": "pulse", "interval": 0.4 } - ] - } + { "from": "Standard.RB", "to": "Actions.UiNavGroup" } ] } diff --git a/interface/resources/controllers/xbox.json b/interface/resources/controllers/xbox.json index 0b4a992fa7..08088f50d9 100644 --- a/interface/resources/controllers/xbox.json +++ b/interface/resources/controllers/xbox.json @@ -56,6 +56,9 @@ { "from": "GamePad.A", "to": "Standard.A" }, { "from": "GamePad.B", "to": "Standard.B" }, { "from": "GamePad.X", "to": "Standard.X" }, - { "from": "GamePad.Y", "to": "Standard.Y" } + { "from": "GamePad.Y", "to": "Standard.Y" }, + + { "from": [ "Standard.A", "Standard.X" ], "to": "Actions.UiNavSelect" }, + { "from": [ "Standard.B", "Standard.Y" ], "to": "Actions.UiNavBack" } ] } diff --git a/interface/resources/icons/circle.svg b/interface/resources/icons/circle.svg new file mode 100644 index 0000000000..b544955858 --- /dev/null +++ b/interface/resources/icons/circle.svg @@ -0,0 +1,63 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + diff --git a/interface/resources/icons/edit-icon.svg b/interface/resources/icons/edit-icon.svg new file mode 100644 index 0000000000..a4322c5a3c --- /dev/null +++ b/interface/resources/icons/edit-icon.svg @@ -0,0 +1,68 @@ + + + +image/svg+xml \ No newline at end of file diff --git a/interface/resources/icons/tablet-icons/bubble-i.svg b/interface/resources/icons/tablet-icons/bubble-i.svg new file mode 100644 index 0000000000..d7c8948e01 --- /dev/null +++ b/interface/resources/icons/tablet-icons/bubble-i.svg @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/interface/resources/icons/tablet-icons/edit-i.svg b/interface/resources/icons/tablet-icons/edit-i.svg new file mode 100644 index 0000000000..e430333597 --- /dev/null +++ b/interface/resources/icons/tablet-icons/edit-i.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + diff --git a/interface/resources/icons/tablet-icons/goto-i.svg b/interface/resources/icons/tablet-icons/goto-i.svg new file mode 100644 index 0000000000..911e346866 --- /dev/null +++ b/interface/resources/icons/tablet-icons/goto-i.svg @@ -0,0 +1,16 @@ + + + + + + + + + + diff --git a/interface/resources/icons/tablet-icons/help-i.svg b/interface/resources/icons/tablet-icons/help-i.svg new file mode 100644 index 0000000000..8d53e04d64 --- /dev/null +++ b/interface/resources/icons/tablet-icons/help-i.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + diff --git a/interface/resources/icons/tablet-icons/ignore-i.svg b/interface/resources/icons/tablet-icons/ignore-i.svg new file mode 100644 index 0000000000..3b73385574 --- /dev/null +++ b/interface/resources/icons/tablet-icons/ignore-i.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + diff --git a/interface/resources/icons/tablet-icons/ignore.svg b/interface/resources/icons/tablet-icons/ignore.svg new file mode 100644 index 0000000000..f315c5f249 --- /dev/null +++ b/interface/resources/icons/tablet-icons/ignore.svg @@ -0,0 +1,177 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/interface/resources/icons/tablet-icons/kick.svg b/interface/resources/icons/tablet-icons/kick.svg new file mode 100644 index 0000000000..1eed6e7f43 --- /dev/null +++ b/interface/resources/icons/tablet-icons/kick.svg @@ -0,0 +1,140 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/interface/resources/icons/tablet-icons/market-i.svg b/interface/resources/icons/tablet-icons/market-i.svg new file mode 100644 index 0000000000..bf9aa9335f --- /dev/null +++ b/interface/resources/icons/tablet-icons/market-i.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + diff --git a/interface/resources/icons/tablet-icons/menu-i.svg b/interface/resources/icons/tablet-icons/menu-i.svg new file mode 100644 index 0000000000..a7a7400ffd --- /dev/null +++ b/interface/resources/icons/tablet-icons/menu-i.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + diff --git a/interface/resources/icons/tablet-icons/mic-a.svg b/interface/resources/icons/tablet-icons/mic-a.svg new file mode 100644 index 0000000000..69feec7c17 --- /dev/null +++ b/interface/resources/icons/tablet-icons/mic-a.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + diff --git a/interface/resources/icons/tablet-icons/mic-i.svg b/interface/resources/icons/tablet-icons/mic-i.svg new file mode 100644 index 0000000000..c4eda55cbc --- /dev/null +++ b/interface/resources/icons/tablet-icons/mic-i.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + diff --git a/interface/resources/icons/tablet-icons/people-i.svg b/interface/resources/icons/tablet-icons/people-i.svg new file mode 100644 index 0000000000..8665dfc6f7 --- /dev/null +++ b/interface/resources/icons/tablet-icons/people-i.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + diff --git a/interface/resources/icons/tablet-icons/scripts-i.svg b/interface/resources/icons/tablet-icons/scripts-i.svg new file mode 100644 index 0000000000..647cf805ce --- /dev/null +++ b/interface/resources/icons/tablet-icons/scripts-i.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + diff --git a/interface/resources/icons/tablet-icons/snap-i.svg b/interface/resources/icons/tablet-icons/snap-i.svg new file mode 100644 index 0000000000..abafa1c3cf --- /dev/null +++ b/interface/resources/icons/tablet-icons/snap-i.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + diff --git a/interface/resources/icons/tablet-icons/switch-a.svg b/interface/resources/icons/tablet-icons/switch-a.svg new file mode 100644 index 0000000000..2e26d09e62 --- /dev/null +++ b/interface/resources/icons/tablet-icons/switch-a.svg @@ -0,0 +1,16 @@ + + + + + + + + + + diff --git a/interface/resources/icons/tablet-icons/switch-i.svg b/interface/resources/icons/tablet-icons/switch-i.svg new file mode 100644 index 0000000000..a6460f9c27 --- /dev/null +++ b/interface/resources/icons/tablet-icons/switch-i.svg @@ -0,0 +1,15 @@ + + + + + + + + + + diff --git a/interface/resources/icons/tablet-icons/users-i.svg b/interface/resources/icons/tablet-icons/users-i.svg new file mode 100644 index 0000000000..aa34dd35f9 --- /dev/null +++ b/interface/resources/icons/tablet-icons/users-i.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/interface/resources/icons/tablet-mute-icon.svg b/interface/resources/icons/tablet-mute-icon.svg new file mode 100644 index 0000000000..dc626845e6 --- /dev/null +++ b/interface/resources/icons/tablet-mute-icon.svg @@ -0,0 +1,18 @@ + + + + + + + + + + diff --git a/interface/resources/icons/tablet-unmute-icon.svg b/interface/resources/icons/tablet-unmute-icon.svg new file mode 100644 index 0000000000..34a33a3535 --- /dev/null +++ b/interface/resources/icons/tablet-unmute-icon.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + diff --git a/interface/resources/meshes/tablet-home-button.fbx b/interface/resources/meshes/tablet-home-button.fbx new file mode 100644 index 0000000000..df82d9122e Binary files /dev/null and b/interface/resources/meshes/tablet-home-button.fbx differ diff --git a/interface/resources/meshes/tablet-stylus-fat.fbx b/interface/resources/meshes/tablet-stylus-fat.fbx new file mode 100644 index 0000000000..9fbf471b5d Binary files /dev/null and b/interface/resources/meshes/tablet-stylus-fat.fbx differ diff --git a/interface/resources/meshes/tablet-stylus-skinny.fbx b/interface/resources/meshes/tablet-stylus-skinny.fbx new file mode 100644 index 0000000000..5313f96111 Binary files /dev/null and b/interface/resources/meshes/tablet-stylus-skinny.fbx differ diff --git a/interface/resources/meshes/tablet-with-home-button.fbx b/interface/resources/meshes/tablet-with-home-button.fbx new file mode 100644 index 0000000000..13ab8bab0c Binary files /dev/null and b/interface/resources/meshes/tablet-with-home-button.fbx differ diff --git a/interface/resources/qml/AddressBarDialog.qml b/interface/resources/qml/AddressBarDialog.qml index 7f9d638dc5..c19e16cd36 100644 --- a/interface/resources/qml/AddressBarDialog.qml +++ b/interface/resources/qml/AddressBarDialog.qml @@ -187,9 +187,6 @@ Window { ToolbarButton { id: homeButton imageURL: "../images/home.svg" - buttonState: 1 - defaultState: 1 - hoverState: 2 onClicked: { addressBarDialog.loadHome(); root.shown = false; @@ -204,9 +201,6 @@ Window { ToolbarButton { id: backArrow; imageURL: "../images/backward.svg"; - hoverState: addressBarDialog.backEnabled ? 2 : 0; - defaultState: addressBarDialog.backEnabled ? 1 : 0; - buttonState: addressBarDialog.backEnabled ? 1 : 0; onClicked: addressBarDialog.loadBack(); anchors { left: homeButton.right @@ -216,9 +210,6 @@ Window { ToolbarButton { id: forwardArrow; imageURL: "../images/forward.svg"; - hoverState: addressBarDialog.forwardEnabled ? 2 : 0; - defaultState: addressBarDialog.forwardEnabled ? 1 : 0; - buttonState: addressBarDialog.forwardEnabled ? 1 : 0; onClicked: addressBarDialog.loadForward(); anchors { left: backArrow.right diff --git a/interface/resources/qml/desktop/Desktop.qml b/interface/resources/qml/desktop/Desktop.qml index 66b59f0aea..cc64d0f2b4 100644 --- a/interface/resources/qml/desktop/Desktop.qml +++ b/interface/resources/qml/desktop/Desktop.qml @@ -12,7 +12,6 @@ import QtQuick 2.5 import QtQuick.Controls 1.4 import "../dialogs" -import "../menus" import "../js/Utils.js" as Utils // This is our primary 'desktop' object to which all VR dialogs and windows are childed. @@ -465,32 +464,7 @@ FocusScope { Component { id: fileDialogBuilder; FileDialog { } } function fileDialog(properties) { return fileDialogBuilder.createObject(desktop, properties); - } - - MenuMouseHandler { id: menuPopperUpper } - function popupMenu(point) { - menuPopperUpper.popup(desktop, rootMenu.items, point); - } - - function toggleMenu(point) { - menuPopperUpper.toggle(desktop, rootMenu.items, point); - } - - Keys.onEscapePressed: { - if (menuPopperUpper.closeLastMenu()) { - event.accepted = true; - return; - } - event.accepted = false; - } - - Keys.onLeftPressed: { - if (menuPopperUpper.closeLastMenu()) { - event.accepted = true; - return; - } - event.accepted = false; - } + } function unfocusWindows() { // First find the active focus item, and unfocus it, all the way diff --git a/interface/resources/qml/hifi/Desktop.qml b/interface/resources/qml/hifi/Desktop.qml index e20ecd70e1..4c81027211 100644 --- a/interface/resources/qml/hifi/Desktop.qml +++ b/interface/resources/qml/hifi/Desktop.qml @@ -48,14 +48,7 @@ OriginalDesktop.Desktop { // This used to create sysToolbar dynamically with a call to getToolbar() within onCompleted. // Beginning with QT 5.6, this stopped working, as anything added to toolbars too early got // wiped during startup. - Toolbar { - id: sysToolbar; - objectName: "com.highfidelity.interface.toolbar.system"; - anchors.horizontalCenter: settings.constrainToolbarToCenterX ? desktop.horizontalCenter : undefined; - // Literal 50 is overwritten by settings from previous session, and sysToolbar.x comes from settings when not constrained. - x: sysToolbar.x - y: 50 - } + Settings { id: settings; category: "toolbar"; @@ -65,7 +58,6 @@ OriginalDesktop.Desktop { settings.constrainToolbarToCenterX = constrain; } property var toolbars: (function (map) { // answer dictionary preloaded with sysToolbar - map[sysToolbar.objectName] = sysToolbar; return map; })({}); @@ -74,27 +66,6 @@ OriginalDesktop.Desktop { WebEngine.settings.javascriptCanAccessClipboard = false; WebEngine.settings.spatialNavigationEnabled = false; WebEngine.settings.localContentCanAccessRemoteUrls = true; - - [ // Allocate the standard buttons in the correct order. They will get images, etc., via scripts. - "hmdToggle", "mute", "pal", "bubble", "help", - "hudToggle", - "com.highfidelity.interface.system.editButton", "marketplace", "snapshot", "goto" - ].forEach(function (name) { - sysToolbar.addButton({objectName: name}); - }); - var toggleHudButton = sysToolbar.findButton("hudToggle"); - toggleHudButton.imageURL = "../../../icons/hud.svg"; - toggleHudButton.pinned = true; - sysToolbar.updatePinned(); // automatic when adding buttons only IFF button is pinned at creation. - - toggleHudButton.buttonState = Qt.binding(function(){ - return desktop.pinned ? 1 : 0 - }); - toggleHudButton.clicked.connect(function(){ - console.log("Clicked on hud button") - var overlayMenuItem = "Overlays" - MenuInterface.setIsOptionChecked(overlayMenuItem, !MenuInterface.isOptionChecked(overlayMenuItem)); - }); } // Accept a download through the webview diff --git a/interface/resources/qml/hifi/tablet/Tablet.qml b/interface/resources/qml/hifi/tablet/Tablet.qml new file mode 100644 index 0000000000..8f0b591da8 --- /dev/null +++ b/interface/resources/qml/hifi/tablet/Tablet.qml @@ -0,0 +1,278 @@ +import QtQuick 2.0 +import QtGraphicalEffects 1.0 +import "../../styles-uit" + +Item { + id: tablet + objectName: "tablet" + property double micLevel: 0.8 + property int rowIndex: 0 + property int columnIndex: 0 + property int count: (flowMain.children.length - 1) + + // called by C++ code to keep audio bar updated + function setMicLevel(newMicLevel) { + tablet.micLevel = newMicLevel; + } + + // used to look up a button by its uuid + function findButtonIndex(uuid) { + if (!uuid) { + return -1; + } + + for (var i in flowMain.children) { + var child = flowMain.children[i]; + if (child.uuid === uuid) { + return i; + } + } + return -1; + } + + // called by C++ code when a button should be added to the tablet + function addButtonProxy(properties) { + var component = Qt.createComponent("TabletButton.qml"); + var button = component.createObject(flowMain); + + // copy all properites to button + var keys = Object.keys(properties).forEach(function (key) { + button[key] = properties[key]; + }); + + // pass a reference to the tabletRoot object to the button. + button.tabletRoot = parent.parent; + return button; + } + + // called by C++ code when a button should be removed from the tablet + function removeButtonProxy(properties) { + var index = findButtonIndex(properties.uuid); + if (index < 0) { + console.log("Warning: Tablet.qml could not find button with uuid = " + properties.uuid); + } else { + flowMain.children[index].destroy(); + } + } + + Rectangle { + id: bgTopBar + height: 90 + gradient: Gradient { + GradientStop { + position: 0 + color: "#2b2b2b" + } + + GradientStop { + position: 1 + color: "#1e1e1e" + } + } + anchors.right: parent.right + anchors.rightMargin: 0 + anchors.left: parent.left + anchors.leftMargin: 0 + anchors.topMargin: 0 + anchors.top: parent.top + + + Image { + id: muteIcon + width: 40 + height: 40 + source: "../../../icons/tablet-mute-icon.svg" + anchors.verticalCenter: parent.verticalCenter + } + + Item { + id: item1 + width: 170 + height: 10 + anchors.left: parent.left + anchors.leftMargin: 50 + anchors.verticalCenter: parent.verticalCenter + Rectangle { + id: audioBarBase + color: "#333333" + radius: 5 + anchors.fill: parent + } + Rectangle { + id: audioBarMask + width: parent.width * tablet.micLevel + color: "#333333" + radius: 5 + anchors.bottom: parent.bottom + anchors.bottomMargin: 0 + anchors.top: parent.top + anchors.topMargin: 0 + anchors.left: parent.left + anchors.leftMargin: 0 + } + LinearGradient { + anchors.fill: audioBarMask + source: audioBarMask + start: Qt.point(0, 0) + end: Qt.point(170, 0) + gradient: Gradient { + GradientStop { + position: 0 + color: "#2c8e72" + } + GradientStop { + position: 0.8 + color: "#1fc6a6" + } + GradientStop { + position: 0.81 + color: "#ea4c5f" + } + GradientStop { + position: 1 + color: "#ea4c5f" + } + } + } + } + + RalewaySemiBold { + id: usernameText + text: tablet.parent.parent.username + anchors.verticalCenter: parent.verticalCenter + anchors.right: parent.right + anchors.rightMargin: 20 + horizontalAlignment: Text.AlignRight + font.pixelSize: 20 + color: "#afafaf" + } + } + + Rectangle { + id: bgMain + gradient: Gradient { + GradientStop { + position: 0 + color: "#2b2b2b" + + } + + GradientStop { + position: 1 + color: "#0f212e" + } + } + anchors.bottom: parent.bottom + anchors.bottomMargin: 0 + anchors.right: parent.right + anchors.rightMargin: 0 + anchors.left: parent.left + anchors.leftMargin: 0 + anchors.top: bgTopBar.bottom + anchors.topMargin: 0 + + Flickable { + id: flickable + width: parent.width + height: parent.height + contentWidth: parent.width + contentHeight: flowMain.childrenRect.height + flowMain.anchors.topMargin + flowMain.anchors.bottomMargin + flowMain.spacing + clip: true + Flow { + id: flowMain + spacing: 16 + anchors.right: parent.right + anchors.rightMargin: 30 + anchors.left: parent.left + anchors.leftMargin: 30 + anchors.bottom: parent.bottom + anchors.bottomMargin: 30 + anchors.top: parent.top + anchors.topMargin: 30 + } + } + } + + states: [ + State { + name: "muted state" + + PropertyChanges { + target: muteText + text: "UNMUTE" + } + + PropertyChanges { + target: muteIcon + source: "../../../icons/tablet-unmute-icon.svg" + } + + PropertyChanges { + target: tablet + micLevel: 0 + } + } + ] + + function setCurrentItemState(state) { + var index = rowIndex + columnIndex; + + if (index >= 0 && index <= count ) { + flowMain.children[index].state = state; + } + } + function nextItem() { + setCurrentItemState("base state"); + var nextColumnIndex = (columnIndex + 3 + 1) % 3; + var nextIndex = rowIndex + nextColumnIndex; + if(nextIndex <= count) { + columnIndex = nextColumnIndex; + }; + setCurrentItemState("hover state"); + } + + function previousItem() { + setCurrentItemState("base state"); + var prevIndex = (columnIndex + 3 - 1) % 3; + if((rowIndex + prevIndex) <= count){ + columnIndex = prevIndex; + } + setCurrentItemState("hover state"); + } + + function upItem() { + setCurrentItemState("base state"); + rowIndex = rowIndex - 3; + if (rowIndex < 0 ) { + rowIndex = (count - (count % 3)); + var index = rowIndex + columnIndex; + if(index > count) { + rowIndex = rowIndex - 3; + } + } + setCurrentItemState("hover state"); + } + + function downItem() { + setCurrentItemState("base state"); + rowIndex = rowIndex + 3; + var index = rowIndex + columnIndex; + if (index > count ) { + rowIndex = 0; + } + setCurrentItemState("hover state"); + } + + function selectItem() { + flowMain.children[rowIndex + columnIndex].clicked(); + if (tabletRoot) { + tabletRoot.playButtonClickSound(); + } + } + + Keys.onRightPressed: nextItem(); + Keys.onLeftPressed: previousItem(); + Keys.onDownPressed: downItem(); + Keys.onUpPressed: upItem(); + Keys.onReturnPressed: selectItem(); +} diff --git a/interface/resources/qml/hifi/tablet/TabletButton.qml b/interface/resources/qml/hifi/tablet/TabletButton.qml new file mode 100644 index 0000000000..c6c810d25e --- /dev/null +++ b/interface/resources/qml/hifi/tablet/TabletButton.qml @@ -0,0 +1,233 @@ +import QtQuick 2.0 +import QtGraphicalEffects 1.0 + +Item { + id: tabletButton + property var uuid; + property string text: "EDIT" + property string icon: "icons/edit-icon.svg" + property string activeText: tabletButton.text + property string activeIcon: tabletButton.icon + property bool isActive: false + property bool inDebugMode: false + property bool isEntered: false + property var tabletRoot; + width: 129 + height: 129 + + signal clicked() + + function changeProperty(key, value) { + tabletButton[key] = value; + } + + onIsActiveChanged: { + if (tabletButton.isEntered) { + tabletButton.state = (tabletButton.isActive) ? "hover active state" : "hover sate"; + } else { + tabletButton.state = (tabletButton.isActive) ? "active state" : "base sate"; + } + } + + Rectangle { + id: buttonBg + color: "#000000" + opacity: 0.1 + radius: 8 + anchors.right: parent.right + anchors.rightMargin: 0 + anchors.left: parent.left + anchors.leftMargin: 0 + anchors.bottom: parent.bottom + anchors.bottomMargin: 0 + anchors.top: parent.top + anchors.topMargin: 0 + } + + Rectangle { + id: buttonOutline + color: "#00000000" + opacity: 0.2 + radius: 8 + z: 1 + border.width: 2 + border.color: "#ffffff" + anchors.right: parent.right + anchors.rightMargin: 0 + anchors.left: parent.left + anchors.leftMargin: 0 + anchors.bottom: parent.bottom + anchors.bottomMargin: 0 + anchors.top: parent.top + anchors.topMargin: 0 + } + + DropShadow { + id: glow + visible: false + anchors.fill: parent + horizontalOffset: 0 + verticalOffset: 0 + color: "#ffffff" + radius: 20 + z: -1 + samples: 41 + source: buttonOutline + } + + function urlHelper(src) { + if (src.match(/\bhttp/)) { + return src; + } else { + return "../../../" + src; + } + } + + Image { + id: icon + width: 50 + height: 50 + visible: false + anchors.bottom: text.top + anchors.bottomMargin: 5 + anchors.horizontalCenter: parent.horizontalCenter + fillMode: Image.Stretch + source: tabletButton.urlHelper(tabletButton.icon) + } + + ColorOverlay { + id: iconColorOverlay + anchors.fill: icon + source: icon + color: "#ffffff" + } + + Text { + id: text + color: "#ffffff" + text: tabletButton.text + font.bold: true + font.pixelSize: 18 + anchors.bottom: parent.bottom + anchors.bottomMargin: 20 + anchors.horizontalCenter: parent.horizontalCenter + horizontalAlignment: Text.AlignHCenter + } + + MouseArea { + anchors.fill: parent + hoverEnabled: true + enabled: true + onClicked: { + console.log("Tablet Button Clicked!"); + if (tabletButton.inDebugMode) { + if (tabletButton.isActive) { + tabletButton.isActive = false; + } else { + tabletButton.isActive = true; + } + } + tabletButton.clicked(); + if (tabletRoot) { + tabletRoot.playButtonClickSound(); + } + } + onEntered: { + tabletButton.isEntered = true; + if (tabletButton.isActive) { + tabletButton.state = "hover active state"; + } else { + tabletButton.state = "hover state"; + } + } + onExited: { + tabletButton.isEntered = false; + if (tabletButton.isActive) { + tabletButton.state = "active state"; + } else { + tabletButton.state = "base state"; + } + } + } + + states: [ + State { + name: "hover state" + + PropertyChanges { + target: buttonOutline + border.color: "#1fc6a6" + opacity: 1 + } + + PropertyChanges { + target: glow + visible: true + } + }, + State { + name: "active state" + + PropertyChanges { + target: buttonOutline + border.color: "#1fc6a6" + opacity: 1 + } + + PropertyChanges { + target: buttonBg + color: "#1fc6a6" + opacity: 1 + } + + PropertyChanges { + target: text + color: "#333333" + text: tabletButton.activeText + } + + PropertyChanges { + target: iconColorOverlay + color: "#333333" + } + + PropertyChanges { + target: icon + source: tabletButton.urlHelper(tabletButton.activeIcon) + } + }, + State { + name: "hover active state" + + PropertyChanges { + target: glow + visible: true + } + + PropertyChanges { + target: buttonOutline + border.color: "#ffffff" + opacity: 1 + } + + PropertyChanges { + target: buttonBg + color: "#1fc6a6" + opacity: 1 + } + + PropertyChanges { + target: text + color: "#333333" + } + + PropertyChanges { + target: iconColorOverlay + color: "#333333" + } + + } + ] +} + + diff --git a/interface/resources/qml/hifi/tablet/TabletMenu.qml b/interface/resources/qml/hifi/tablet/TabletMenu.qml new file mode 100644 index 0000000000..a9ca1b884c --- /dev/null +++ b/interface/resources/qml/hifi/tablet/TabletMenu.qml @@ -0,0 +1,101 @@ +import QtQuick 2.5 +import QtGraphicalEffects 1.0 +import QtQuick.Controls 1.4 +import QtQml 2.2 +import "." +import "../../styles-uit" + +FocusScope { + id: tabletMenu + objectName: "tabletMenu" + + width: 480 + height: 720 + + property var rootMenu: Menu { objectName:"rootMenu" } + property var point: Qt.point(50, 50) + + TabletMouseHandler { id: menuPopperUpper } + + Rectangle { + id: bgNavBar + height: 90 + z: 1 + gradient: Gradient { + GradientStop { + position: 0 + color: "#2b2b2b" + } + + GradientStop { + position: 1 + color: "#1e1e1e" + } + } + anchors.right: parent.right + anchors.rightMargin: 0 + anchors.left: parent.left + anchors.leftMargin: 0 + anchors.topMargin: 0 + anchors.top: parent.top + + Image { + id: menuRootIcon + width: 40 + height: 40 + source: "../../../icons/tablet-icons/menu-i.svg" + anchors.verticalCenter: parent.verticalCenter + anchors.left: parent.left + anchors.leftMargin: 15 + + MouseArea { + anchors.fill: parent + hoverEnabled: true + onEntered: iconColorOverlay.color = "#1fc6a6"; + onExited: iconColorOverlay.color = "#ffffff"; + // navigate back to root level menu + onClicked: buildMenu(); + } + } + + ColorOverlay { + id: iconColorOverlay + anchors.fill: menuRootIcon + source: menuRootIcon + color: "#34a2c7" + } + + RalewayBold { + id: breadcrumbText + text: "Menu" + size: 26 + color: "#34a2c7" + anchors.verticalCenter: parent.verticalCenter + anchors.left: menuRootIcon.right + anchors.leftMargin: 15 + MouseArea { + anchors.fill: parent + hoverEnabled: true + onEntered: breadcrumbText.color = "#1fc6a6"; + onExited: breadcrumbText.color = "#34a2c7"; + // navigate back to parent level menu if there is one + onClicked: + if (breadcrumbText.text !== "Menu") { + menuPopperUpper.closeLastMenu(); + } + } + } + } + + function pop() { + menuPopperUpper.closeLastMenu(); + } + + function setRootMenu(menu) { + tabletMenu.rootMenu = menu + buildMenu() + } + function buildMenu() { + menuPopperUpper.popup(tabletMenu, rootMenu.items) + } +} diff --git a/interface/resources/qml/menus/VrMenuItem.qml b/interface/resources/qml/hifi/tablet/TabletMenuItem.qml similarity index 94% rename from interface/resources/qml/menus/VrMenuItem.qml rename to interface/resources/qml/hifi/tablet/TabletMenuItem.qml index 38d2b57c03..c9223650f8 100644 --- a/interface/resources/qml/menus/VrMenuItem.qml +++ b/interface/resources/qml/hifi/tablet/TabletMenuItem.qml @@ -12,8 +12,8 @@ import QtQuick 2.5 import QtQuick.Controls 1.4 import QtQuick.Controls.Styles 1.4 -import "../controls-uit" -import "../styles-uit" +import "../../controls-uit" +import "../../styles-uit" Item { id: root @@ -31,7 +31,7 @@ Item { // FIXME: Should use radio buttons if source.exclusiveGroup. anchors { left: parent.left - leftMargin: hifi.dimensions.menuPadding.x + leftMargin: hifi.dimensions.menuPadding.x + 15 top: label.top topMargin: 0 } @@ -50,7 +50,7 @@ Item { RalewaySemiBold { id: label - size: hifi.fontSizes.rootMenu + size: 20 font.capitalization: isSubMenu ? Font.MixedCase : Font.AllUppercase anchors.left: check.right anchors.verticalCenter: parent.verticalCenter @@ -103,7 +103,7 @@ Item { HiFiGlyphs { text: hifi.glyphs.disclosureExpand color: source.enabled ? hifi.colors.baseGrayShadow : hifi.colors.baseGrayShadow25 - size: 2 * hifi.fontSizes.rootMenuDisclosure + size: 70 anchors.verticalCenter: parent.verticalCenter anchors.right: parent.right horizontalAlignment: Text.AlignRight diff --git a/interface/resources/qml/menus/VrMenuView.qml b/interface/resources/qml/hifi/tablet/TabletMenuView.qml similarity index 74% rename from interface/resources/qml/menus/VrMenuView.qml rename to interface/resources/qml/hifi/tablet/TabletMenuView.qml index 5db13fc160..6411c1af02 100644 --- a/interface/resources/qml/menus/VrMenuView.qml +++ b/interface/resources/qml/hifi/tablet/TabletMenuView.qml @@ -12,13 +12,13 @@ import QtQuick 2.5 import QtQuick.Controls 1.4 import QtQuick.Controls.Styles 1.4 -import "../styles-uit" - +import "../../styles-uit" +import "." FocusScope { id: root implicitHeight: background.height implicitWidth: background.width - + objectName: "root" property alias currentItem: listView.currentItem property alias model: listView.model property bool isSubMenu: false @@ -32,15 +32,23 @@ FocusScope { radius: hifi.dimensions.borderRadius border.width: hifi.dimensions.borderWidth border.color: hifi.colors.lightGrayText80 - color: isSubMenu ? hifi.colors.faintGray : hifi.colors.faintGray80 + color: hifi.colors.faintGray + //color: isSubMenu ? hifi.colors.faintGray : hifi.colors.faintGray80 } + ListView { id: listView - x: 8; y: 8 - width: 128 - height: count * 32 + x: 0 + y: 0 + width: 480 + height: 720 + contentWidth: 480 + contentHeight: 720 + objectName: "menuList" + topMargin: hifi.dimensions.menuPadding.y + bottomMargin: hifi.dimensions.menuPadding.y onEnabledChanged: recalcSize(); onVisibleChanged: recalcSize(); onCountChanged: recalcSize(); @@ -57,7 +65,7 @@ FocusScope { color: hifi.colors.white } - delegate: VrMenuItem { + delegate: TabletMenuItem { text: name source: item onImplicitHeightChanged: listView.recalcSize() @@ -91,26 +99,30 @@ FocusScope { newHeight += currentItem.implicitHeight } } - newHeight += 2 * hifi.dimensions.menuPadding.y; // White space at top and bottom. + newHeight += hifi.dimensions.menuPadding.y * 2; // White space at top and bottom. if (maxWidth > width) { width = maxWidth; } - if (newHeight > height) { - height = newHeight + if (newHeight > contentHeight) { + contentHeight = newHeight; } currentIndex = originalIndex; } - - function previousItem() { currentIndex = (currentIndex + count - 1) % count; } - function nextItem() { currentIndex = (currentIndex + count + 1) % count; } - function selectCurrentItem() { if (currentIndex != -1) root.selected(currentItem.source); } - + Keys.onUpPressed: previousItem(); Keys.onDownPressed: nextItem(); Keys.onSpacePressed: selectCurrentItem(); Keys.onRightPressed: selectCurrentItem(); Keys.onReturnPressed: selectCurrentItem(); + Keys.onLeftPressed: previousPage(); } + + function previousItem() { listView.currentIndex = (listView.currentIndex + listView.count - 1) % listView.count; } + function nextItem() { listView.currentIndex = (listView.currentIndex + listView.count + 1) % listView.count; } + function selectCurrentItem() { if (listView.currentIndex != -1) root.selected(currentItem.source); } + function previousPage() { root.parent.pop(); } + + } diff --git a/interface/resources/qml/menus/MenuMouseHandler.qml b/interface/resources/qml/hifi/tablet/TabletMouseHandler.qml similarity index 77% rename from interface/resources/qml/menus/MenuMouseHandler.qml rename to interface/resources/qml/hifi/tablet/TabletMouseHandler.qml index 48574d41e5..32e34e279b 100644 --- a/interface/resources/qml/menus/MenuMouseHandler.qml +++ b/interface/resources/qml/hifi/tablet/TabletMouseHandler.qml @@ -16,11 +16,11 @@ import "." Item { id: root anchors.fill: parent - objectName: "MouseMenuHandlerItem" + objectName: "tabletMenuHandlerItem" MouseArea { id: menuRoot; - objectName: "MouseMenuHandlerMouseArea" + objectName: "tabletMenuHandlerMouseArea" anchors.fill: parent enabled: d.topMenu !== null onClicked: { @@ -34,7 +34,7 @@ Item { property var topMenu: null; property var modelMaker: Component { ListModel { } } property var menuViewMaker: Component { - VrMenuView { + TabletMenuView { id: subMenu onSelected: d.handleSelection(subMenu, currentItem, item) } @@ -54,7 +54,7 @@ Item { } function toModel(items) { - var result = modelMaker.createObject(desktop); + var result = modelMaker.createObject(tabletMenu); for (var i = 0; i < items.length; ++i) { var item = items[i]; if (!item.visible) continue; @@ -63,7 +63,9 @@ Item { result.append({"name": item.title, "item": item}) break; case MenuItemType.Item: - result.append({"name": item.text, "item": item}) + if (item.text !== "Users Online") { + result.append({"name": item.text, "item": item}) + } break; case MenuItemType.Separator: result.append({"name": "", "item": item}) @@ -80,10 +82,16 @@ Item { if (menuStack.length) { topMenu = menuStack[menuStack.length - 1]; topMenu.focus = true; + topMenu.forceActiveFocus(); + // show current menu level on nav bar + if (topMenu.objectName === "") { + breadcrumbText.text = "Menu"; + } else { + breadcrumbText.text = topMenu.objectName; + } } else { + breadcrumbText.text = "Menu"; topMenu = null; - offscreenFlags.navigationFocused = false; - menuRoot.enabled = false; } } @@ -91,7 +99,7 @@ Item { menuStack.push(newMenu); topMenu = newMenu; topMenu.focus = true; - offscreenFlags.navigationFocused = true; + topMenu.forceActiveFocus(); } function clearMenus() { @@ -118,12 +126,7 @@ Item { function buildMenu(items, targetPosition) { var model = toModel(items); // Menus must be childed to desktop for Z-ordering - var newMenu = menuViewMaker.createObject(desktop, { model: model, z: topMenu ? topMenu.z + 1 : desktop.zLevels.menu, isSubMenu: topMenu !== null }); - if (targetPosition) { - newMenu.x = targetPosition.x - newMenu.y = targetPosition.y - newMenu.height / 3 * 1 - } - clampMenuPosition(newMenu); + var newMenu = menuViewMaker.createObject(tabletMenu, { model: model, isSubMenu: topMenu !== null }); pushMenu(newMenu); return newMenu; } @@ -137,39 +140,36 @@ Item { case MenuItemType.Menu: var target = Qt.vector2d(topMenu.x, topMenu.y).plus(Qt.vector2d(selectedItem.x + 96, selectedItem.y)); buildMenu(item.items, target).objectName = item.title; + // show current menu level on nav bar + breadcrumbText.text = item.title; break; case MenuItemType.Item: console.log("Triggering " + item.text) // Don't block waiting for modal dialogs and such that the menu might open. delay.trigger(item); - clearMenus(); break; } } } - function popup(parent, items, point) { + function popup(parent, items) { d.clearMenus(); - menuRoot.enabled = true; d.buildMenu(items, point); } - function toggle(parent, items, point) { - if (d.topMenu) { - d.clearMenus(); - return; - } - popup(parent, items, point); - } - function closeLastMenu() { - if (d.menuStack.length) { + if (d.menuStack.length > 1) { d.popMenu(); return true; } return false; } + function previousItem() { d.topMenu.previousItem(); } + function nextItem() { d.topMenu.nextItem(); } + function selectCurrentItem() { d.topMenu.selectCurrentItem(); } + function previousPage() { d.topMenu.previousPage(); } + } diff --git a/interface/resources/qml/hifi/tablet/TabletRoot.qml b/interface/resources/qml/hifi/tablet/TabletRoot.qml new file mode 100644 index 0000000000..ded91a5eff --- /dev/null +++ b/interface/resources/qml/hifi/tablet/TabletRoot.qml @@ -0,0 +1,63 @@ +import QtQuick 2.0 +import Hifi 1.0 + +Item { + id: tabletRoot + objectName: "tabletRoot" + property string username: "Unknown user" + property var eventBridge; + + signal showDesktop(); + + function loadSource(url) { + loader.source = url; + } + + function loadWebUrl(url, injectedJavaScriptUrl) { + loader.item.url = url; + loader.item.scriptURL = injectedJavaScriptUrl; + } + + SoundEffect { + id: buttonClickSound + source: "../../../sounds/Gamemaster-Audio-button-click.wav" + } + + function playButtonClickSound() { + // Because of the asynchronous nature of initalization, it is possible for this function to be + // called before the C++ has set the globalPosition context variable. + if (typeof globalPosition !== 'undefined') { + buttonClickSound.play(globalPosition); + } + } + + function setUsername(newUsername) { + username = newUsername; + } + + Loader { + id: loader + objectName: "loader" + asynchronous: false + + width: parent.width + height: parent.height + + onLoaded: { + if (loader.item.hasOwnProperty("eventBridge")) { + loader.item.eventBridge = eventBridge; + + // Hook up callback for clara.io download from the marketplace. + eventBridge.webEventReceived.connect(function (event) { + if (event.slice(0, 17) === "CLARA.IO DOWNLOAD") { + ApplicationInterface.addAssetToWorldFromURL(event.slice(18)); + } + }); + } + loader.item.forceActiveFocus(); + } + } + + width: 480 + height: 720 +} diff --git a/interface/resources/qml/hifi/tablet/TabletWebView.qml b/interface/resources/qml/hifi/tablet/TabletWebView.qml new file mode 100644 index 0000000000..0f697d634e --- /dev/null +++ b/interface/resources/qml/hifi/tablet/TabletWebView.qml @@ -0,0 +1,10 @@ +import QtQuick 2.0 +import QtWebEngine 1.2 + +import "../../controls" as Controls + +Controls.WebView { + +} + + diff --git a/interface/resources/qml/hifi/toolbars/StateImage.qml b/interface/resources/qml/hifi/toolbars/StateImage.qml index 44eaa6f7fd..ee0778626d 100644 --- a/interface/resources/qml/hifi/toolbars/StateImage.qml +++ b/interface/resources/qml/hifi/toolbars/StateImage.qml @@ -6,7 +6,7 @@ Item { property alias alpha: image.opacity property var subImage; property int yOffset: 0 - property int buttonState: 0 + property int buttonState: 1 property real size: 50 width: size; height: size property bool pinned: false diff --git a/interface/resources/qml/hifi/toolbars/ToolbarButton.qml b/interface/resources/qml/hifi/toolbars/ToolbarButton.qml index bc035ca19c..91c992bf0d 100644 --- a/interface/resources/qml/hifi/toolbars/ToolbarButton.qml +++ b/interface/resources/qml/hifi/toolbars/ToolbarButton.qml @@ -3,11 +3,34 @@ import QtQuick.Controls 1.4 StateImage { id: button - property int hoverState: -1 - property int defaultState: -1 + property bool isActive: false + property bool isEntered: false + + property int imageOffOut: 1 + property int imageOffIn: 3 + property int imageOnOut: 0 + property int imageOnIn: 2 signal clicked() + function changeProperty(key, value) { + button[key] = value; + } + + function updateState() { + if (!button.isEntered && !button.isActive) { + buttonState = imageOffOut; + } else if (!button.isEntered && button.isActive) { + buttonState = imageOnOut; + } else if (button.isEntered && !button.isActive) { + buttonState = imageOffIn; + } else { + buttonState = imageOnIn; + } + } + + onIsActiveChanged: updateState(); + Timer { id: asyncClickSender interval: 10 @@ -22,14 +45,12 @@ StateImage { anchors.fill: parent onClicked: asyncClickSender.start(); onEntered: { - if (hoverState >= 0) { - buttonState = hoverState; - } + button.isEntered = true; + updateState(); } onExited: { - if (defaultState >= 0) { - buttonState = defaultState; - } + button.isEntered = false; + updateState(); } } } diff --git a/interface/resources/qml/styles-uit/HifiConstants.qml b/interface/resources/qml/styles-uit/HifiConstants.qml index 18bdd89799..e261e2198f 100644 --- a/interface/resources/qml/styles-uit/HifiConstants.qml +++ b/interface/resources/qml/styles-uit/HifiConstants.qml @@ -156,7 +156,7 @@ Item { readonly property real modalDialogTitleHeight: 40 readonly property real controlLineHeight: 28 // Height of spinbox control on 1920 x 1080 monitor readonly property real controlInterlineHeight: 21 // 75% of controlLineHeight - readonly property vector2d menuPadding: Qt.vector2d(14, 12) + readonly property vector2d menuPadding: Qt.vector2d(14, 102) readonly property real scrollbarBackgroundWidth: 18 readonly property real scrollbarHandleWidth: scrollbarBackgroundWidth - 2 } diff --git a/interface/resources/sounds/Gamemaster-Audio-button-click.wav b/interface/resources/sounds/Gamemaster-Audio-button-click.wav new file mode 100644 index 0000000000..7fe6feeff1 Binary files /dev/null and b/interface/resources/sounds/Gamemaster-Audio-button-click.wav differ diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 576241ddc2..0217136a0c 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -111,6 +111,7 @@ #include #include #include +#include #include #include #include @@ -492,6 +493,7 @@ bool setupEssentials(int& argc, char** argv) { DependencyManager::set(); DependencyManager::set(); DependencyManager::set(); + DependencyManager::set(); DependencyManager::set(); DependencyManager::set(); @@ -977,7 +979,8 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo connect(userInputMapper.data(), &UserInputMapper::actionEvent, [this](int action, float state) { using namespace controller; auto offscreenUi = DependencyManager::get(); - if (offscreenUi->navigationFocused()) { + auto tabletScriptingInterface = DependencyManager::get(); + { auto actionEnum = static_cast(action); int key = Qt::Key_unknown; static int lastKey = Qt::Key_unknown; @@ -1021,25 +1024,28 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo break; } - if (navAxis) { + auto window = tabletScriptingInterface->getTabletWindow(); + if (navAxis && window) { if (lastKey != Qt::Key_unknown) { QKeyEvent event(QEvent::KeyRelease, lastKey, Qt::NoModifier); - sendEvent(offscreenUi->getWindow(), &event); + sendEvent(window, &event); lastKey = Qt::Key_unknown; } if (key != Qt::Key_unknown) { QKeyEvent event(QEvent::KeyPress, key, Qt::NoModifier); - sendEvent(offscreenUi->getWindow(), &event); + sendEvent(window, &event); + tabletScriptingInterface->processEvent(&event); lastKey = key; } - } else if (key != Qt::Key_unknown) { + } else if (key != Qt::Key_unknown && window) { if (state) { QKeyEvent event(QEvent::KeyPress, key, Qt::NoModifier); - sendEvent(offscreenUi->getWindow(), &event); + sendEvent(window, &event); + tabletScriptingInterface->processEvent(&event); } else { QKeyEvent event(QEvent::KeyRelease, key, Qt::NoModifier); - sendEvent(offscreenUi->getWindow(), &event); + sendEvent(window, &event); } return; } @@ -1065,12 +1071,8 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo DependencyManager::get()->toggleMute(); } else if (action == controller::toInt(controller::Action::CYCLE_CAMERA)) { cycleCamera(); - } else if (action == controller::toInt(controller::Action::UI_NAV_SELECT)) { - if (!offscreenUi->navigationFocused()) { - toggleMenuUnderReticle(); - } } else if (action == controller::toInt(controller::Action::CONTEXT_MENU)) { - toggleMenuUnderReticle(); + toggleTabletUI(); } else if (action == controller::toInt(controller::Action::RETICLE_X)) { auto oldPos = getApplicationCompositor().getReticlePosition(); getApplicationCompositor().setReticlePosition({ oldPos.x + state, oldPos.y }); @@ -1589,15 +1591,17 @@ QString Application::getUserAgent() { return userAgent; } -void Application::toggleMenuUnderReticle() const { - // In HMD, if the menu is near the mouse but not under it, the reticle can be at a significantly - // different depth. When you focus on the menu, the cursor can appear to your crossed eyes as both - // on the menu and off. - // Even in 2D, it is arguable whether the user would want the menu to be to the side. - const float X_LEFT_SHIFT = 50.0; - auto offscreenUi = DependencyManager::get(); - auto reticlePosition = getApplicationCompositor().getReticlePosition(); - offscreenUi->toggleMenu(QPoint(reticlePosition.x - X_LEFT_SHIFT, reticlePosition.y)); +uint64_t lastTabletUIToggle { 0 }; +const uint64_t toggleTabletUILockout { 500000 }; +void Application::toggleTabletUI() const { + uint64_t now = usecTimestampNow(); + if (now - lastTabletUIToggle < toggleTabletUILockout) { + return; + } + lastTabletUIToggle = now; + + auto HMD = DependencyManager::get(); + HMD->toggleShouldShowTablet(); } void Application::checkChangeCursor() { @@ -2933,10 +2937,6 @@ void Application::keyPressEvent(QKeyEvent* event) { void Application::keyReleaseEvent(QKeyEvent* event) { - if (event->key() == Qt::Key_Alt && _altPressed && hasFocus()) { - toggleMenuUnderReticle(); - } - _keysPressed.remove(event->key()); _controllerScriptingInterface->emitKeyReleaseEvent(event); // send events to any registered scripts @@ -4161,9 +4161,11 @@ void Application::setKeyboardFocusOverlay(unsigned int overlayID) { } _lastAcceptedKeyPress = usecTimestampNow(); - auto size = overlay->getSize() * FOCUS_HIGHLIGHT_EXPANSION_FACTOR; - const float OVERLAY_DEPTH = 0.0105f; - setKeyboardFocusHighlight(overlay->getPosition(), overlay->getRotation(), glm::vec3(size.x, size.y, OVERLAY_DEPTH)); + if (overlay->getProperty("showKeyboardFocusHighlight").toBool()) { + auto size = overlay->getSize() * FOCUS_HIGHLIGHT_EXPANSION_FACTOR; + const float OVERLAY_DEPTH = 0.0105f; + setKeyboardFocusHighlight(overlay->getPosition(), overlay->getRotation(), glm::vec3(size.x, size.y, OVERLAY_DEPTH)); + } } } } @@ -4377,6 +4379,10 @@ void Application::update(float deltaTime) { PROFILE_RANGE_EX(simulation_physics, "HarvestChanges", 0xffffff00, (uint64_t)getActiveDisplayPlugin()->presentCount()); PerformanceTimer perfTimer("harvestChanges"); if (_physicsEngine->hasOutgoingChanges()) { + // grab the collision events BEFORE handleOutgoingChanges() because at this point + // we have a better idea of which objects we own or should own. + auto& collisionEvents = _physicsEngine->getCollisionEvents(); + getEntities()->getTree()->withWriteLock([&] { PerformanceTimer perfTimer("handleOutgoingChanges"); const VectorOfMotionStates& outgoingChanges = _physicsEngine->getOutgoingChanges(); @@ -4384,11 +4390,10 @@ void Application::update(float deltaTime) { avatarManager->handleOutgoingChanges(outgoingChanges); }); - auto collisionEvents = _physicsEngine->getCollisionEvents(); - avatarManager->handleCollisionEvents(collisionEvents); - if (!_aboutToQuit) { + // handleCollisionEvents() AFTER handleOutgoinChanges() PerformanceTimer perfTimer("entities"); + avatarManager->handleCollisionEvents(collisionEvents); // Collision events (and their scripts) must not be handled when we're locked, above. (That would risk // deadlock.) _entitySimulation->handleCollisionEvents(collisionEvents); diff --git a/interface/src/Application.h b/interface/src/Application.h index 49e30b13d1..3b89aa52f3 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -484,7 +484,7 @@ private: static void dragEnterEvent(QDragEnterEvent* event); void maybeToggleMenuVisible(QMouseEvent* event) const; - void toggleMenuUnderReticle() const; + void toggleTabletUI() const; MainWindow* _window; QElapsedTimer& _sessionRunTimer; diff --git a/interface/src/Menu.cpp b/interface/src/Menu.cpp index 4819220400..870d60fdf3 100644 --- a/interface/src/Menu.cpp +++ b/interface/src/Menu.cpp @@ -160,7 +160,7 @@ Menu::Menu() { audioIO.data(), SLOT(toggleMute())); // Audio > Show Level Meter - addCheckableActionToQMenuAndActionHash(audioMenu, MenuOption::AudioTools, 0, true); + addCheckableActionToQMenuAndActionHash(audioMenu, MenuOption::AudioTools, 0, false); // Avatar menu ---------------------------------- diff --git a/interface/src/avatar/Avatar.cpp b/interface/src/avatar/Avatar.cpp index 7ab1e7e8bd..9e6c524c2c 100644 --- a/interface/src/avatar/Avatar.cpp +++ b/interface/src/avatar/Avatar.cpp @@ -892,6 +892,16 @@ glm::quat Avatar::getAbsoluteJointRotationInObjectFrame(int index) const { Transform controllerRightHandTransform = Transform(getControllerRightHandMatrix()); return controllerRightHandTransform.getRotation(); } + case CAMERA_MATRIX_INDEX: { + glm::quat rotation; + if (_skeletonModel && _skeletonModel->isActive()) { + int headJointIndex = _skeletonModel->getFBXGeometry().headJointIndex; + if (headJointIndex >= 0) { + _skeletonModel->getAbsoluteJointRotationInRigFrame(headJointIndex, rotation); + } + } + return rotation; + } default: { glm::quat rotation; _skeletonModel->getAbsoluteJointRotationInRigFrame(index, rotation); @@ -918,6 +928,16 @@ glm::vec3 Avatar::getAbsoluteJointTranslationInObjectFrame(int index) const { Transform controllerRightHandTransform = Transform(getControllerRightHandMatrix()); return controllerRightHandTransform.getTranslation(); } + case CAMERA_MATRIX_INDEX: { + glm::vec3 translation; + if (_skeletonModel && _skeletonModel->isActive()) { + int headJointIndex = _skeletonModel->getFBXGeometry().headJointIndex; + if (headJointIndex >= 0) { + _skeletonModel->getAbsoluteJointTranslationInRigFrame(headJointIndex, translation); + } + } + return translation; + } default: { glm::vec3 translation; _skeletonModel->getAbsoluteJointTranslationInRigFrame(index, translation); diff --git a/interface/src/avatar/AvatarActionHold.cpp b/interface/src/avatar/AvatarActionHold.cpp index f1b2ee1440..7f58c86aec 100644 --- a/interface/src/avatar/AvatarActionHold.cpp +++ b/interface/src/avatar/AvatarActionHold.cpp @@ -128,32 +128,44 @@ bool AvatarActionHold::getTarget(float deltaTimeStep, glm::quat& rotation, glm:: glm::quat palmRotation; if (holdingAvatar->isMyAvatar()) { + std::shared_ptr myAvatar = avatarManager->getMyAvatar(); // fetch the hand controller pose controller::Pose pose; if (isRightHand) { - pose = avatarManager->getMyAvatar()->getRightHandControllerPoseInWorldFrame(); + pose = myAvatar->getRightHandControllerPoseInWorldFrame(); } else { - pose = avatarManager->getMyAvatar()->getLeftHandControllerPoseInWorldFrame(); + pose = myAvatar->getLeftHandControllerPoseInWorldFrame(); } if (pose.isValid()) { linearVelocity = pose.getVelocity(); angularVelocity = pose.getAngularVelocity(); - - if (isRightHand) { - pose = avatarManager->getMyAvatar()->getRightHandControllerPoseInAvatarFrame(); - } else { - pose = avatarManager->getMyAvatar()->getLeftHandControllerPoseInAvatarFrame(); - } } if (_ignoreIK && pose.isValid()) { + + // this position/rotation should be the same as the one in scripts/system/libraries/controllers.js + // otherwise things will do a little hop when you grab them. + + // if (isRightHand) { + // pose = myAvatar->getRightHandControllerPoseInAvatarFrame(); + // } else { + // pose = myAvatar->getLeftHandControllerPoseInAvatarFrame(); + // } + // glm::vec3 camRelPos = pose.getTranslation(); + // glm::quat camRelRot = pose.getRotation(); + + int camRelIndex = isRightHand ? + CAMERA_RELATIVE_CONTROLLER_RIGHTHAND_INDEX : + CAMERA_RELATIVE_CONTROLLER_LEFTHAND_INDEX; + glm::vec3 camRelPos = myAvatar->getAbsoluteJointTranslationInObjectFrame(camRelIndex); + glm::quat camRelRot = myAvatar->getAbsoluteJointRotationInObjectFrame(camRelIndex); + Transform avatarTransform; - auto myAvatar = DependencyManager::get()->getMyAvatar(); avatarTransform = myAvatar->getTransform(); - palmPosition = avatarTransform.transform(pose.getTranslation() / myAvatar->getDomainLimitedScale()); - palmRotation = avatarTransform.getRotation() * pose.getRotation(); + palmPosition = avatarTransform.transform(camRelPos / myAvatar->getDomainLimitedScale()); + palmRotation = avatarTransform.getRotation() * camRelRot; } else { glm::vec3 avatarRigidBodyPosition; glm::quat avatarRigidBodyRotation; @@ -223,6 +235,11 @@ void AvatarActionHold::doKinematicUpdate(float deltaTimeStep) { qDebug() << "AvatarActionHold::doKinematicUpdate -- no owning entity"; return; } + if (ownerEntity->getParentID() != QUuid()) { + // if the held entity has been given a parent, stop acting on it. + return; + } + void* physicsInfo = ownerEntity->getPhysicsInfo(); if (!physicsInfo) { qDebug() << "AvatarActionHold::doKinematicUpdate -- no owning physics info"; diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index fd2f113f2a..d4815b35c6 100644 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -797,8 +797,14 @@ void MyAvatar::saveData() { settings.beginWriteArray("avatarEntityData"); int avatarEntityIndex = 0; + auto hmdInterface = DependencyManager::get(); _avatarEntitiesLock.withReadLock([&] { for (auto entityID : _avatarEntityData.keys()) { + if (hmdInterface->getCurrentTableUIID() == entityID) { + // don't persist the tablet between domains / sessions + continue; + } + settings.setArrayIndex(avatarEntityIndex); settings.setValue("id", entityID); settings.setValue("properties", _avatarEntityData.value(entityID)); @@ -2388,6 +2394,13 @@ glm::quat MyAvatar::getAbsoluteJointRotationInObjectFrame(int index) const { glm::mat4 result = computeCameraRelativeHandControllerMatrix(controllerSensorMatrix); return glmExtractRotation(result); } + case CAMERA_MATRIX_INDEX: { + bool success; + Transform avatarTransform; + Transform::mult(avatarTransform, getParentTransform(success), getLocalTransform()); + glm::mat4 invAvatarMat = avatarTransform.getInverseMatrix(); + return glmExtractRotation(invAvatarMat * qApp->getCamera()->getTransform()); + } default: { return Avatar::getAbsoluteJointRotationInObjectFrame(index); } @@ -2414,6 +2427,13 @@ glm::vec3 MyAvatar::getAbsoluteJointTranslationInObjectFrame(int index) const { glm::mat4 result = computeCameraRelativeHandControllerMatrix(controllerSensorMatrix); return extractTranslation(result); } + case CAMERA_MATRIX_INDEX: { + bool success; + Transform avatarTransform; + Transform::mult(avatarTransform, getParentTransform(success), getLocalTransform()); + glm::mat4 invAvatarMat = avatarTransform.getInverseMatrix(); + return extractTranslation(invAvatarMat * qApp->getCamera()->getTransform()); + } default: { return Avatar::getAbsoluteJointTranslationInObjectFrame(index); } diff --git a/interface/src/scripting/HMDScriptingInterface.cpp b/interface/src/scripting/HMDScriptingInterface.cpp index 0a5a113526..2bca793d80 100644 --- a/interface/src/scripting/HMDScriptingInterface.cpp +++ b/interface/src/scripting/HMDScriptingInterface.cpp @@ -18,6 +18,7 @@ #include #include #include +#include #include "Application.h" @@ -76,6 +77,10 @@ bool HMDScriptingInterface::shouldShowHandControllers() const { return _showHandControllersCount > 0; } +void HMDScriptingInterface::closeTablet() { + _showTablet = false; +} + QScriptValue HMDScriptingInterface::getHUDLookAtPosition2D(QScriptContext* context, QScriptEngine* engine) { glm::vec3 hudIntersection; auto instance = DependencyManager::get(); diff --git a/interface/src/scripting/HMDScriptingInterface.h b/interface/src/scripting/HMDScriptingInterface.h index 52b16c5eed..f5744bb8d1 100644 --- a/interface/src/scripting/HMDScriptingInterface.h +++ b/interface/src/scripting/HMDScriptingInterface.h @@ -28,6 +28,10 @@ class HMDScriptingInterface : public AbstractHMDScriptingInterface, public Depen Q_PROPERTY(glm::vec3 position READ getPosition) Q_PROPERTY(glm::quat orientation READ getOrientation) Q_PROPERTY(bool mounted READ isMounted) + Q_PROPERTY(bool showTablet READ getShouldShowTablet) + Q_PROPERTY(QUuid tabletID READ getCurrentTableUIID WRITE setCurrentTabletUIID) + Q_PROPERTY(unsigned int homeButtonID READ getCurrentHomeButtonUUID WRITE setCurrentHomeButtonUUID) + public: Q_INVOKABLE glm::vec3 calculateRayUICollisionPoint(const glm::vec3& position, const glm::vec3& direction) const; @@ -53,11 +57,11 @@ public: Q_INVOKABLE void disableExtraLaser() const; - /// Suppress the activation of any on-screen keyboard so that a script operation will + /// Suppress the activation of any on-screen keyboard so that a script operation will /// not be interrupted by a keyboard popup /// Returns false if there is already an active keyboard displayed. /// Clients should re-enable the keyboard when the operation is complete and ensure - /// that they balance any call to suppressKeyboard() that returns true with a corresponding + /// that they balance any call to suppressKeyboard() that returns true with a corresponding /// call to unsuppressKeyboard() within a reasonable amount of time Q_INVOKABLE bool suppressKeyboard(); @@ -70,6 +74,8 @@ public: // rotate the overlay UI sphere so that it is centered about the the current HMD position and orientation Q_INVOKABLE void centerUI(); + Q_INVOKABLE void closeTablet(); + signals: bool shouldShowHandControllersChanged(); @@ -80,10 +86,25 @@ public: bool isMounted() const; + void toggleShouldShowTablet() { _showTablet = !_showTablet; } + void setShouldShowTablet(bool value) { _showTablet = value; } + bool getShouldShowTablet() const { return _showTablet; } + + void setCurrentTabletUIID(QUuid tabletID) { _tabletUIID = tabletID; } + QUuid getCurrentTableUIID() const { return _tabletUIID; } + + void setCurrentHomeButtonUUID(unsigned int homeButtonID) { _homeButtonID = homeButtonID; } + unsigned int getCurrentHomeButtonUUID() const { return _homeButtonID; } + private: + bool _showTablet { false }; + QUuid _tabletUIID; // this is the entityID of the WebEntity which is part of (a child of) the tablet-ui. + unsigned int _homeButtonID; + QUuid _tabletEntityID; + // Get the position of the HMD glm::vec3 getPosition() const; - + // Get the orientation of the HMD glm::quat getOrientation() const; diff --git a/interface/src/scripting/MenuScriptingInterface.cpp b/interface/src/scripting/MenuScriptingInterface.cpp index df75d331d6..cf186271d2 100644 --- a/interface/src/scripting/MenuScriptingInterface.cpp +++ b/interface/src/scripting/MenuScriptingInterface.cpp @@ -147,3 +147,14 @@ void MenuScriptingInterface::triggerOption(const QString& menuOption) { QMetaObject::invokeMethod(Menu::getInstance(), "triggerOption", Q_ARG(const QString&, menuOption)); } +void MenuScriptingInterface::closeInfoView(const QString& path) { + QMetaObject::invokeMethod(Menu::getInstance(), "closeInfoView", Q_ARG(const QString&, path)); +} + +bool MenuScriptingInterface::isInfoViewVisible(const QString& path) { + bool result; + QMetaObject::invokeMethod(Menu::getInstance(), "isInfoViewVisible", Qt::BlockingQueuedConnection, + Q_RETURN_ARG(bool, result), Q_ARG(const QString&, path)); + return result; +} + diff --git a/interface/src/scripting/MenuScriptingInterface.h b/interface/src/scripting/MenuScriptingInterface.h index b1c389f733..b9c29ccf08 100644 --- a/interface/src/scripting/MenuScriptingInterface.h +++ b/interface/src/scripting/MenuScriptingInterface.h @@ -182,6 +182,9 @@ public slots: */ void setMenuEnabled(const QString& menuName, bool isEnabled); + void closeInfoView(const QString& path); + bool isInfoViewVisible(const QString& path); + signals: /**jsdoc * This is a signal that is emitted when a menu item is clicked. diff --git a/interface/src/scripting/QmlWrapper.h b/interface/src/scripting/QmlWrapper.h new file mode 100644 index 0000000000..7dd319e445 --- /dev/null +++ b/interface/src/scripting/QmlWrapper.h @@ -0,0 +1,63 @@ +// +// Created by Anthony J. Thibault on 2016-12-12 +// Copyright 2013-2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_QmlWrapper_h +#define hifi_QmlWrapper_h + +#include +#include +#include + +class QmlWrapper : public QObject { + Q_OBJECT +public: + QmlWrapper(QObject* qmlObject, QObject* parent = nullptr) + : QObject(parent), _qmlObject(qmlObject) { + } + + Q_INVOKABLE void writeProperty(QString propertyName, QVariant propertyValue) { + auto offscreenUi = DependencyManager::get(); + offscreenUi->executeOnUiThread([=] { + _qmlObject->setProperty(propertyName.toStdString().c_str(), propertyValue); + }); + } + + Q_INVOKABLE void writeProperties(QVariant propertyMap) { + auto offscreenUi = DependencyManager::get(); + offscreenUi->executeOnUiThread([=] { + QVariantMap map = propertyMap.toMap(); + for (const QString& key : map.keys()) { + _qmlObject->setProperty(key.toStdString().c_str(), map[key]); + } + }); + } + + Q_INVOKABLE QVariant readProperty(const QString& propertyName) { + auto offscreenUi = DependencyManager::get(); + return offscreenUi->returnFromUiThread([&]()->QVariant { + return _qmlObject->property(propertyName.toStdString().c_str()); + }); + } + + Q_INVOKABLE QVariant readProperties(const QVariant& propertyList) { + auto offscreenUi = DependencyManager::get(); + return offscreenUi->returnFromUiThread([&]()->QVariant { + QVariantMap result; + for (const QVariant& property : propertyList.toList()) { + QString propertyString = property.toString(); + result.insert(propertyString, _qmlObject->property(propertyString.toStdString().c_str())); + } + return result; + }); + } + +protected: + QObject* _qmlObject{ nullptr }; +}; + +#endif \ No newline at end of file diff --git a/interface/src/scripting/ToolbarScriptingInterface.cpp b/interface/src/scripting/ToolbarScriptingInterface.cpp index 0cb314615a..2b4f64f35e 100644 --- a/interface/src/scripting/ToolbarScriptingInterface.cpp +++ b/interface/src/scripting/ToolbarScriptingInterface.cpp @@ -8,69 +8,44 @@ #include "ToolbarScriptingInterface.h" + #include #include - -class QmlWrapper : public QObject { - Q_OBJECT -public: - QmlWrapper(QObject* qmlObject, QObject* parent = nullptr) - : QObject(parent), _qmlObject(qmlObject) { - } - - Q_INVOKABLE void writeProperty(QString propertyName, QVariant propertyValue) { - auto offscreenUi = DependencyManager::get(); - offscreenUi->executeOnUiThread([=] { - _qmlObject->setProperty(propertyName.toStdString().c_str(), propertyValue); - }); - } - - Q_INVOKABLE void writeProperties(QVariant propertyMap) { - auto offscreenUi = DependencyManager::get(); - offscreenUi->executeOnUiThread([=] { - QVariantMap map = propertyMap.toMap(); - for (const QString& key : map.keys()) { - _qmlObject->setProperty(key.toStdString().c_str(), map[key]); - } - }); - } - - Q_INVOKABLE QVariant readProperty(const QString& propertyName) { - auto offscreenUi = DependencyManager::get(); - return offscreenUi->returnFromUiThread([&]()->QVariant { - return _qmlObject->property(propertyName.toStdString().c_str()); - }); - } - - Q_INVOKABLE QVariant readProperties(const QVariant& propertyList) { - auto offscreenUi = DependencyManager::get(); - return offscreenUi->returnFromUiThread([&]()->QVariant { - QVariantMap result; - for (const QVariant& property : propertyList.toList()) { - QString propertyString = property.toString(); - result.insert(propertyString, _qmlObject->property(propertyString.toStdString().c_str())); - } - return result; - }); - } - - -protected: - QObject* _qmlObject{ nullptr }; -}; - +#include "QmlWrapper.h" class ToolbarButtonProxy : public QmlWrapper { Q_OBJECT public: ToolbarButtonProxy(QObject* qmlObject, QObject* parent = nullptr) : QmlWrapper(qmlObject, parent) { + std::lock_guard guard(_mutex); + _qmlButton = qobject_cast(qmlObject); connect(qmlObject, SIGNAL(clicked()), this, SIGNAL(clicked())); } + Q_INVOKABLE void editProperties(QVariantMap properties) { + std::lock_guard guard(_mutex); + QVariantMap::const_iterator iter = properties.constBegin(); + while (iter != properties.constEnd()) { + _properties[iter.key()] = iter.value(); + if (_qmlButton) { + // [01/25 14:26:20] [WARNING] [default] QMetaObject::invokeMethod: No such method ToolbarButton_QMLTYPE_195::changeProperty(QVariant,QVariant) + + QMetaObject::invokeMethod(_qmlButton, "changeProperty", Qt::AutoConnection, + Q_ARG(QVariant, QVariant(iter.key())), Q_ARG(QVariant, iter.value())); + } + ++iter; + } + } + signals: void clicked(); + +protected: + mutable std::mutex _mutex; + QQuickItem* _qmlButton { nullptr }; + QVariantMap _properties; }; class ToolbarProxy : public QmlWrapper { diff --git a/interface/src/scripting/WindowScriptingInterface.cpp b/interface/src/scripting/WindowScriptingInterface.cpp index fbeddf41e0..52f7d723eb 100644 --- a/interface/src/scripting/WindowScriptingInterface.cpp +++ b/interface/src/scripting/WindowScriptingInterface.cpp @@ -58,6 +58,8 @@ WindowScriptingInterface::WindowScriptingInterface() { OffscreenUi::warning("Import SVO Error", "You need to be running edit.js to import entities."); } }); + + connect(qApp->getWindow(), &MainWindow::windowGeometryChanged, this, &WindowScriptingInterface::geometryChanged); } WindowScriptingInterface::~WindowScriptingInterface() { diff --git a/interface/src/scripting/WindowScriptingInterface.h b/interface/src/scripting/WindowScriptingInterface.h index 4652e00661..6cc6c7b715 100644 --- a/interface/src/scripting/WindowScriptingInterface.h +++ b/interface/src/scripting/WindowScriptingInterface.h @@ -76,6 +76,9 @@ signals: void messageBoxClosed(int id, int button); + // triggered when window size or position changes + void geometryChanged(QRect geometry); + private: QString getPreviousBrowseLocation() const; void setPreviousBrowseLocation(const QString& location); diff --git a/interface/src/ui/overlays/Base3DOverlay.cpp b/interface/src/ui/overlays/Base3DOverlay.cpp index 8f2149f02d..ff5177ed3a 100644 --- a/interface/src/ui/overlays/Base3DOverlay.cpp +++ b/interface/src/ui/overlays/Base3DOverlay.cpp @@ -26,7 +26,8 @@ Base3DOverlay::Base3DOverlay() : _isSolid(DEFAULT_IS_SOLID), _isDashedLine(DEFAULT_IS_DASHED_LINE), _ignoreRayIntersection(false), - _drawInFront(false) + _drawInFront(false), + _isAA(true) { } @@ -37,7 +38,8 @@ Base3DOverlay::Base3DOverlay(const Base3DOverlay* base3DOverlay) : _isSolid(base3DOverlay->_isSolid), _isDashedLine(base3DOverlay->_isDashedLine), _ignoreRayIntersection(base3DOverlay->_ignoreRayIntersection), - _drawInFront(base3DOverlay->_drawInFront) + _drawInFront(base3DOverlay->_drawInFront), + _isAA(base3DOverlay->_isAA) { setTransform(base3DOverlay->getTransform()); } @@ -175,6 +177,13 @@ void Base3DOverlay::setProperties(const QVariantMap& originalProperties) { needRenderItemUpdate = true; } + auto isAA = properties["isAA"]; + if (isAA.isValid()) { + bool value = isAA.toBool(); + setIsAA(value); + needRenderItemUpdate = true; + } + // Communicate changes to the renderItem if needed if (needRenderItemUpdate) { auto itemID = getRenderItemID(); @@ -224,6 +233,9 @@ QVariant Base3DOverlay::getProperty(const QString& property) { if (property == "parentJointIndex") { return getParentJointIndex(); } + if (property == "isAA") { + return _isAA; + } return Overlay::getProperty(property); } diff --git a/interface/src/ui/overlays/Base3DOverlay.h b/interface/src/ui/overlays/Base3DOverlay.h index 1860af4e85..18936df504 100644 --- a/interface/src/ui/overlays/Base3DOverlay.h +++ b/interface/src/ui/overlays/Base3DOverlay.h @@ -36,11 +36,14 @@ public: bool getIgnoreRayIntersection() const { return _ignoreRayIntersection; } bool getDrawInFront() const { return _drawInFront; } + virtual bool isAA() const { return _isAA; } + void setLineWidth(float lineWidth) { _lineWidth = lineWidth; } void setIsSolid(bool isSolid) { _isSolid = isSolid; } void setIsDashedLine(bool isDashedLine) { _isDashedLine = isDashedLine; } void setIgnoreRayIntersection(bool value) { _ignoreRayIntersection = value; } void setDrawInFront(bool value) { _drawInFront = value; } + void setIsAA(bool value) { _isAA = value; } virtual AABox getBounds() const override = 0; @@ -64,6 +67,7 @@ protected: bool _isDashedLine; bool _ignoreRayIntersection; bool _drawInFront; + bool _isAA; }; #endif // hifi_Base3DOverlay_h diff --git a/interface/src/ui/overlays/OverlaysPayload.cpp b/interface/src/ui/overlays/OverlaysPayload.cpp index 7cc74d60e0..277a86e93f 100644 --- a/interface/src/ui/overlays/OverlaysPayload.cpp +++ b/interface/src/ui/overlays/OverlaysPayload.cpp @@ -38,6 +38,9 @@ namespace render { if (std::static_pointer_cast(overlay)->getDrawInFront()) { builder.withLayered(); } + if (!std::static_pointer_cast(overlay)->isAA()) { + builder.withLayered(); + } if (overlay->getAlphaPulse() != 0.0f || overlay->getAlpha() != 1.0f) { builder.withTransparent(); } @@ -51,11 +54,17 @@ namespace render { } template <> int payloadGetLayer(const Overlay::Pointer& overlay) { // MAgic number while we are defining the layering mechanism: - const int LAYER_2D = 2; + const int LAYER_NO_AA = 3; + const int LAYER_2D = 2; const int LAYER_3D_FRONT = 1; const int LAYER_3D = 0; + if (overlay->is3D()) { - return (std::dynamic_pointer_cast(overlay)->getDrawInFront() ? LAYER_3D_FRONT : LAYER_3D); + auto overlay3D = std::dynamic_pointer_cast(overlay); + if (overlay3D->isAA()) + return (overlay3D->getDrawInFront() ? LAYER_3D_FRONT : LAYER_3D); + else + return LAYER_NO_AA; } else { return LAYER_2D; } diff --git a/interface/src/ui/overlays/Web3DOverlay.cpp b/interface/src/ui/overlays/Web3DOverlay.cpp index c987735367..f33ef24c0d 100644 --- a/interface/src/ui/overlays/Web3DOverlay.cpp +++ b/interface/src/ui/overlays/Web3DOverlay.cpp @@ -18,15 +18,17 @@ #include #include +#include +#include #include #include #include -#include +#include #include -#include #include +#include +#include #include - #include #include @@ -37,7 +39,7 @@ static const float OPAQUE_ALPHA_THRESHOLD = 0.99f; const QString Web3DOverlay::TYPE = "web3d"; const QString Web3DOverlay::QML = "Web3DOverlay.qml"; -Web3DOverlay::Web3DOverlay() : _dpi(DPI) { +Web3DOverlay::Web3DOverlay() : _dpi(DPI) { _touchDevice.setCapabilities(QTouchDevice::Position); _touchDevice.setType(QTouchDevice::TouchScreen); _touchDevice.setName("RenderableWebEntityItemTouchDevice"); @@ -51,13 +53,31 @@ Web3DOverlay::Web3DOverlay(const Web3DOverlay* Web3DOverlay) : _url(Web3DOverlay->_url), _scriptURL(Web3DOverlay->_scriptURL), _dpi(Web3DOverlay->_dpi), - _resolution(Web3DOverlay->_resolution) + _resolution(Web3DOverlay->_resolution), + _showKeyboardFocusHighlight(Web3DOverlay->_showKeyboardFocusHighlight) { _geometryId = DependencyManager::get()->allocateID(); } Web3DOverlay::~Web3DOverlay() { if (_webSurface) { + QQuickItem* rootItem = _webSurface->getRootItem(); + + if (rootItem && rootItem->objectName() == "tabletRoot") { + auto tabletScriptingInterface = DependencyManager::get(); + tabletScriptingInterface->setQmlTabletRoot("com.highfidelity.interface.tablet.system", nullptr, nullptr); + } + + // Fix for crash in QtWebEngineCore when rapidly switching domains + // Call stop on the QWebEngineView before destroying OffscreenQMLSurface. + if (rootItem) { + QObject* obj = rootItem->findChild("webEngineView"); + if (obj) { + // stop loading + QMetaObject::invokeMethod(obj, "stop"); + } + } + _webSurface->pause(); _webSurface->disconnect(_connection); @@ -92,15 +112,54 @@ Web3DOverlay::~Web3DOverlay() { } void Web3DOverlay::update(float deltatime) { - // FIXME: applyTransformTo causes tablet overlay to detach from tablet entity. - // Perhaps rather than deleting the following code it should be run only if isFacingAvatar() is true? - /* - if (usecTimestampNow() > _transformExpiry) { - Transform transform = getTransform(); - applyTransformTo(transform); - setTransform(transform); + if (_webSurface) { + // update globalPosition + _webSurface->getRootContext()->setContextProperty("globalPosition", vec3toVariant(getPosition())); } - */ +} + +QString Web3DOverlay::pickURL() { + QUrl sourceUrl(_url); + if (sourceUrl.scheme() == "http" || sourceUrl.scheme() == "https" || + _url.toLower().endsWith(".htm") || _url.toLower().endsWith(".html")) { + + _webSurface->setBaseUrl(QUrl::fromLocalFile(PathUtils::resourcesPath() + "/qml/")); + return "Web3DOverlay.qml"; + } else { + return QUrl::fromLocalFile(PathUtils::resourcesPath()).toString() + "/" + _url; + } +} + + +void Web3DOverlay::loadSourceURL() { + + QUrl sourceUrl(_url); + if (sourceUrl.scheme() == "http" || sourceUrl.scheme() == "https" || + _url.toLower().endsWith(".htm") || _url.toLower().endsWith(".html")) { + + _webSurface->setBaseUrl(QUrl::fromLocalFile(PathUtils::resourcesPath() + "/qml/")); + _webSurface->load("Web3DOverlay.qml"); + _webSurface->resume(); + _webSurface->getRootItem()->setProperty("url", _url); + _webSurface->getRootItem()->setProperty("scriptURL", _scriptURL); + _webSurface->getRootContext()->setContextProperty("ApplicationInterface", qApp); + + } else { + _webSurface->setBaseUrl(QUrl::fromLocalFile(PathUtils::resourcesPath())); + _webSurface->load(_url, [&](QQmlContext* context, QObject* obj) {}); + _webSurface->resume(); + + if (_webSurface->getRootItem() && _webSurface->getRootItem()->objectName() == "tabletRoot") { + auto tabletScriptingInterface = DependencyManager::get(); + auto flags = tabletScriptingInterface->getFlags(); + _webSurface->getRootContext()->setContextProperty("offscreenFlags", flags); + tabletScriptingInterface->setQmlTabletRoot("com.highfidelity.interface.tablet.system", _webSurface->getRootItem(), _webSurface.data()); + + // Override min fps for tablet UI, for silky smooth scrolling + _webSurface->setMaxFps(90); + } + } + _webSurface->getRootContext()->setContextProperty("globalPosition", vec3toVariant(getPosition())); } void Web3DOverlay::render(RenderArgs* args) { @@ -111,10 +170,11 @@ void Web3DOverlay::render(RenderArgs* args) { QOpenGLContext * currentContext = QOpenGLContext::currentContext(); QSurface * currentSurface = currentContext->surface(); if (!_webSurface) { - _webSurface = DependencyManager::get()->acquire(QML); + _webSurface = DependencyManager::get()->acquire(pickURL()); _webSurface->setMaxFps(10); // FIXME, the max FPS could be better managed by being dynamic (based on the number of current surfaces // and the current rendering load) + loadSourceURL(); _webSurface->resume(); _webSurface->resize(QSize(_resolution.x, _resolution.y)); _webSurface->getRootItem()->setProperty("url", _url); @@ -143,7 +203,7 @@ void Web3DOverlay::render(RenderArgs* args) { point.setPos(windowPoint); QList touchPoints; touchPoints.push_back(point); - QTouchEvent* touchEvent = new QTouchEvent(QEvent::TouchEnd, nullptr, Qt::NoModifier, Qt::TouchPointReleased, + QTouchEvent* touchEvent = new QTouchEvent(QEvent::TouchEnd, nullptr, Qt::NoModifier, Qt::TouchPointReleased, touchPoints); touchEvent->setWindow(_webSurface->getWindow()); touchEvent->setDevice(&_touchDevice); @@ -160,7 +220,7 @@ void Web3DOverlay::render(RenderArgs* args) { vec4 color(toGlm(getColor()), getAlpha()); Transform transform = getTransform(); - + // FIXME: applyTransformTo causes tablet overlay to detach from tablet entity. // Perhaps rather than deleting the following code it should be run only if isFacingAvatar() is true? /* @@ -189,9 +249,9 @@ void Web3DOverlay::render(RenderArgs* args) { batch.setModelTransform(transform); auto geometryCache = DependencyManager::get(); if (color.a < OPAQUE_ALPHA_THRESHOLD) { - geometryCache->bindTransparentWebBrowserProgram(batch); + geometryCache->bindTransparentWebBrowserProgram(batch, _isAA); } else { - geometryCache->bindOpaqueWebBrowserProgram(batch); + geometryCache->bindOpaqueWebBrowserProgram(batch, _isAA); } geometryCache->renderQuad(batch, halfSize * -1.0f, halfSize, vec2(0), vec2(1), color, _geometryId); batch.setResourceTexture(0, args->_whiteTexture); // restore default white color after me @@ -230,7 +290,7 @@ void Web3DOverlay::handlePointerEvent(const PointerEvent& event) { if (event.getType() == PointerEvent::Move) { // Forward a mouse move event to the Web surface. - QMouseEvent* mouseEvent = new QMouseEvent(QEvent::MouseMove, windowPoint, windowPoint, windowPoint, Qt::NoButton, + QMouseEvent* mouseEvent = new QMouseEvent(QEvent::MouseMove, windowPoint, windowPoint, windowPoint, Qt::NoButton, Qt::NoButton, Qt::NoModifier); QCoreApplication::postEvent(_webSurface->getWindow(), mouseEvent); } @@ -309,6 +369,11 @@ void Web3DOverlay::setProperties(const QVariantMap& properties) { if (dpi.isValid()) { _dpi = dpi.toFloat(); } + + auto showKeyboardFocusHighlight = properties["showKeyboardFocusHighlight"]; + if (showKeyboardFocusHighlight.isValid()) { + _showKeyboardFocusHighlight = showKeyboardFocusHighlight.toBool(); + } } QVariant Web3DOverlay::getProperty(const QString& property) { @@ -324,6 +389,9 @@ QVariant Web3DOverlay::getProperty(const QString& property) { if (property == "dpi") { return _dpi; } + if (property == "showKeyboardFocusHighlight") { + return _showKeyboardFocusHighlight; + } return Billboard3DOverlay::getProperty(property); } @@ -331,7 +399,7 @@ void Web3DOverlay::setURL(const QString& url) { _url = url; if (_webSurface) { AbstractViewStateInterface::instance()->postLambdaEvent([this, url] { - _webSurface->getRootItem()->setProperty("url", url); + loadSourceURL(); }); } } diff --git a/interface/src/ui/overlays/Web3DOverlay.h b/interface/src/ui/overlays/Web3DOverlay.h index c389f5e6f9..2b9686919d 100644 --- a/interface/src/ui/overlays/Web3DOverlay.h +++ b/interface/src/ui/overlays/Web3DOverlay.h @@ -29,6 +29,8 @@ public: Web3DOverlay(const Web3DOverlay* Web3DOverlay); virtual ~Web3DOverlay(); + QString pickURL(); + void loadSourceURL(); virtual void render(RenderArgs* args) override; virtual const render::ShapeKey getShapeKey() override; @@ -47,7 +49,7 @@ public: glm::vec2 getSize(); - virtual bool findRayIntersection(const glm::vec3& origin, const glm::vec3& direction, float& distance, + virtual bool findRayIntersection(const glm::vec3& origin, const glm::vec3& direction, float& distance, BoxFace& face, glm::vec3& surfaceNormal) override; virtual Web3DOverlay* createClone() const override; @@ -68,6 +70,7 @@ private: float _dpi; vec2 _resolution{ 640, 480 }; int _geometryId { 0 }; + bool _showKeyboardFocusHighlight{ true }; bool _pressed{ false }; QTouchDevice _touchDevice; diff --git a/libraries/audio-client/src/AudioClient.cpp b/libraries/audio-client/src/AudioClient.cpp index 1e3dc11338..2e532d67bf 100644 --- a/libraries/audio-client/src/AudioClient.cpp +++ b/libraries/audio-client/src/AudioClient.cpp @@ -112,6 +112,42 @@ private: bool _quit { false }; }; +void AudioInjectorsThread::prepare() { + _audio->prepareLocalAudioInjectors(); +} + +static void channelUpmix(int16_t* source, int16_t* dest, int numSamples, int numExtraChannels) { + for (int i = 0; i < numSamples/2; i++) { + + // read 2 samples + int16_t left = *source++; + int16_t right = *source++; + + // write 2 + N samples + *dest++ = left; + *dest++ = right; + for (int n = 0; n < numExtraChannels; n++) { + *dest++ = 0; + } + } +} + +static void channelDownmix(int16_t* source, int16_t* dest, int numSamples) { + for (int i = 0; i < numSamples/2; i++) { + + // read 2 samples + int16_t left = *source++; + int16_t right = *source++; + + // write 1 sample + *dest++ = (int16_t)((left + right) / 2); + } +} + +static inline float convertToFloat(int16_t sample) { + return (float)sample * (1 / 32768.0f); +} + AudioClient::AudioClient() : AbstractAudioInterface(), _gate(this), @@ -127,6 +163,7 @@ AudioClient::AudioClient() : _loopbackAudioOutput(NULL), _loopbackOutputDevice(NULL), _inputRingBuffer(0), + _localInjectorsStream(0), _receivedAudioStream(RECEIVED_AUDIO_STREAM_CAPACITY_FRAMES), _isStereoInput(false), _outputStarveDetectionStartTimeMsec(0), @@ -144,13 +181,18 @@ AudioClient::AudioClient() : _reverbOptions(&_scriptReverbOptions), _inputToNetworkResampler(NULL), _networkToOutputResampler(NULL), + _localToOutputResampler(NULL), + _localAudioThread(this), _audioLimiter(AudioConstants::SAMPLE_RATE, OUTPUT_CHANNEL_COUNT), _outgoingAvatarAudioSequenceNumber(0), - _audioOutputIODevice(_receivedAudioStream, this), + _audioOutputIODevice(_localInjectorsStream, _receivedAudioStream, this), _stats(&_receivedAudioStream), _inputGate(), _positionGetter(DEFAULT_POSITION_GETTER), _orientationGetter(DEFAULT_ORIENTATION_GETTER) { + // avoid putting a lock in the device callback + assert(_localSamplesAvailable.is_lock_free()); + // deprecate legacy settings { Setting::Handle::Deprecated("maxFramesOverDesired", InboundAudioStream::MAX_FRAMES_OVER_DESIRED); @@ -176,6 +218,10 @@ AudioClient::AudioClient() : _checkDevicesThread->setPriority(QThread::LowPriority); _checkDevicesThread->start(); + // start a thread to process local injectors + _localAudioThread.setObjectName("LocalAudio Thread"); + _localAudioThread.start(); + configureReverb(); auto& packetReceiver = DependencyManager::get()->getPacketReceiver(); @@ -213,6 +259,7 @@ void AudioClient::reset() { _stats.reset(); _sourceReverb.reset(); _listenerReverb.reset(); + _localReverb.reset(); } void AudioClient::audioMixerKilled() { @@ -365,7 +412,7 @@ QAudioDeviceInfo defaultAudioDeviceForMode(QAudio::Mode mode) { CoUninitialize(); } - qCDebug(audioclient) << "DEBUG [" << deviceName << "] [" << getNamedAudioDeviceForMode(mode, deviceName).deviceName() << "]"; + qCDebug(audioclient) << "[" << deviceName << "] [" << getNamedAudioDeviceForMode(mode, deviceName).deviceName() << "]"; return getNamedAudioDeviceForMode(mode, deviceName); #endif @@ -387,12 +434,12 @@ bool nativeFormatForAudioDevice(const QAudioDeviceInfo& audioDevice, audioFormat.setByteOrder(QAudioFormat::LittleEndian); if (!audioDevice.isFormatSupported(audioFormat)) { - qCDebug(audioclient) << "WARNING: The native format is" << audioFormat << "but isFormatSupported() failed."; + qCWarning(audioclient) << "The native format is" << audioFormat << "but isFormatSupported() failed."; return false; } // converting to/from this rate must produce an integral number of samples if (audioFormat.sampleRate() * AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL % AudioConstants::SAMPLE_RATE != 0) { - qCDebug(audioclient) << "WARNING: The native sample rate [" << audioFormat.sampleRate() << "] is not supported."; + qCWarning(audioclient) << "The native sample rate [" << audioFormat.sampleRate() << "] is not supported."; return false; } return true; @@ -726,12 +773,12 @@ QVector AudioClient::getDeviceNames(QAudio::Mode mode) { } bool AudioClient::switchInputToAudioDevice(const QString& inputDeviceName) { - qCDebug(audioclient) << "DEBUG [" << inputDeviceName << "] [" << getNamedAudioDeviceForMode(QAudio::AudioInput, inputDeviceName).deviceName() << "]"; + qCDebug(audioclient) << "[" << inputDeviceName << "] [" << getNamedAudioDeviceForMode(QAudio::AudioInput, inputDeviceName).deviceName() << "]"; return switchInputToAudioDevice(getNamedAudioDeviceForMode(QAudio::AudioInput, inputDeviceName)); } bool AudioClient::switchOutputToAudioDevice(const QString& outputDeviceName) { - qCDebug(audioclient) << "DEBUG [" << outputDeviceName << "] [" << getNamedAudioDeviceForMode(QAudio::AudioOutput, outputDeviceName).deviceName() << "]"; + qCDebug(audioclient) << "[" << outputDeviceName << "] [" << getNamedAudioDeviceForMode(QAudio::AudioOutput, outputDeviceName).deviceName() << "]"; return switchOutputToAudioDevice(getNamedAudioDeviceForMode(QAudio::AudioOutput, outputDeviceName)); } @@ -762,6 +809,7 @@ void AudioClient::configureReverb() { p.wetDryMix = _reverbOptions->getWetDryMix(); _listenerReverb.setParameters(&p); + _localReverb.setParameters(&p); // used only for adding self-reverb to loopback audio p.sampleRate = _outputFormat.sampleRate(); @@ -808,6 +856,7 @@ void AudioClient::setReverb(bool reverb) { if (!_reverb) { _sourceReverb.reset(); _listenerReverb.reset(); + _localReverb.reset(); } } @@ -841,36 +890,6 @@ void AudioClient::setReverbOptions(const AudioEffectOptions* options) { } } -static void channelUpmix(int16_t* source, int16_t* dest, int numSamples, int numExtraChannels) { - - for (int i = 0; i < numSamples/2; i++) { - - // read 2 samples - int16_t left = *source++; - int16_t right = *source++; - - // write 2 + N samples - *dest++ = left; - *dest++ = right; - for (int n = 0; n < numExtraChannels; n++) { - *dest++ = 0; - } - } -} - -static void channelDownmix(int16_t* source, int16_t* dest, int numSamples) { - - for (int i = 0; i < numSamples/2; i++) { - - // read 2 samples - int16_t left = *source++; - int16_t right = *source++; - - // write 1 sample - *dest++ = (int16_t)((left + right) / 2); - } -} - void AudioClient::handleLocalEchoAndReverb(QByteArray& inputByteArray) { // If there is server echo, reverb will be applied to the recieved audio stream so no need to have it here. bool hasReverb = _reverb || _receivedAudioStream.hasReverb(); @@ -1082,14 +1101,78 @@ void AudioClient::handleRecordedAudioInput(const QByteArray& audio) { PacketType::MicrophoneAudioWithEcho, _selectedCodecName); } -void AudioClient::mixLocalAudioInjectors(float* mixBuffer) { +void AudioClient::prepareLocalAudioInjectors() { + if (_outputPeriod == 0) { + return; + } + + int bufferCapacity = _localInjectorsStream.getSampleCapacity(); + if (_localToOutputResampler) { + // avoid overwriting the buffer, + // instead of failing on writes because the buffer is used as a lock-free pipe + bufferCapacity -= + _localToOutputResampler->getMaxOutput(AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL) * + AudioConstants::STEREO; + bufferCapacity += 1; + } + + int samplesNeeded = std::numeric_limits::max(); + while (samplesNeeded > 0) { + // lock for every write to avoid locking out the device callback + // this lock is intentional - the buffer is only lock-free in its use in the device callback + Lock lock(_localAudioMutex); + + samplesNeeded = bufferCapacity - _localSamplesAvailable.load(std::memory_order_relaxed); + if (samplesNeeded <= 0) { + break; + } + + // get a network frame of local injectors' audio + if (!mixLocalAudioInjectors(_localMixBuffer)) { + break; + } + + // reverb + if (_reverb) { + _localReverb.render(_localMixBuffer, _localMixBuffer, AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL); + } + + int samples; + if (_localToOutputResampler) { + // resample to output sample rate + int frames = _localToOutputResampler->render(_localMixBuffer, _localOutputMixBuffer, + AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL); + + // write to local injectors' ring buffer + samples = frames * AudioConstants::STEREO; + _localInjectorsStream.writeSamples(_localOutputMixBuffer, samples); + + } else { + // write to local injectors' ring buffer + samples = AudioConstants::NETWORK_FRAME_SAMPLES_STEREO; + _localInjectorsStream.writeSamples(_localMixBuffer, + AudioConstants::NETWORK_FRAME_SAMPLES_STEREO); + } + + _localSamplesAvailable.fetch_add(samples, std::memory_order_release); + samplesNeeded -= samples; + } +} + +bool AudioClient::mixLocalAudioInjectors(float* mixBuffer) { QVector injectorsToRemove; // lock the injector vector Lock lock(_injectorsMutex); - for (AudioInjector* injector : getActiveLocalAudioInjectors()) { + if (_activeLocalAudioInjectors.size() == 0) { + return false; + } + + memset(mixBuffer, 0, AudioConstants::NETWORK_FRAME_SAMPLES_STEREO * sizeof(float)); + + for (AudioInjector* injector : _activeLocalAudioInjectors) { if (injector->getLocalBuffer()) { static const int HRTF_DATASET_INDEX = 1; @@ -1098,8 +1181,8 @@ void AudioClient::mixLocalAudioInjectors(float* mixBuffer) { qint64 bytesToRead = numChannels * AudioConstants::NETWORK_FRAME_BYTES_PER_CHANNEL; // get one frame from the injector - memset(_scratchBuffer, 0, bytesToRead); - if (0 < injector->getLocalBuffer()->readData((char*)_scratchBuffer, bytesToRead)) { + memset(_localScratchBuffer, 0, bytesToRead); + if (0 < injector->getLocalBuffer()->readData((char*)_localScratchBuffer, bytesToRead)) { if (injector->isAmbisonic()) { @@ -1119,7 +1202,7 @@ void AudioClient::mixLocalAudioInjectors(float* mixBuffer) { float qz = relativeOrientation.y; // Ambisonic gets spatialized into mixBuffer - injector->getLocalFOA().render(_scratchBuffer, mixBuffer, HRTF_DATASET_INDEX, + injector->getLocalFOA().render(_localScratchBuffer, mixBuffer, HRTF_DATASET_INDEX, qw, qx, qy, qz, gain, AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL); } else if (injector->isStereo()) { @@ -1127,7 +1210,7 @@ void AudioClient::mixLocalAudioInjectors(float* mixBuffer) { // stereo gets directly mixed into mixBuffer float gain = injector->getVolume(); for (int i = 0; i < AudioConstants::NETWORK_FRAME_SAMPLES_STEREO; i++) { - mixBuffer[i] += (float)_scratchBuffer[i] * (1/32768.0f) * gain; + mixBuffer[i] += convertToFloat(_localScratchBuffer[i]) * gain; } } else { @@ -1136,10 +1219,10 @@ void AudioClient::mixLocalAudioInjectors(float* mixBuffer) { glm::vec3 relativePosition = injector->getPosition() - _positionGetter(); float distance = glm::max(glm::length(relativePosition), EPSILON); float gain = gainForSource(distance, injector->getVolume()); - float azimuth = azimuthForSource(relativePosition); + float azimuth = azimuthForSource(relativePosition); // mono gets spatialized into mixBuffer - injector->getLocalHRTF().render(_scratchBuffer, mixBuffer, HRTF_DATASET_INDEX, + injector->getLocalHRTF().render(_localScratchBuffer, mixBuffer, HRTF_DATASET_INDEX, azimuth, distance, gain, AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL); } @@ -1160,8 +1243,10 @@ void AudioClient::mixLocalAudioInjectors(float* mixBuffer) { for (AudioInjector* injector : injectorsToRemove) { qCDebug(audioclient) << "removing injector"; - getActiveLocalAudioInjectors().removeOne(injector); + _activeLocalAudioInjectors.removeOne(injector); } + + return true; } void AudioClient::processReceivedSamples(const QByteArray& decodedBuffer, QByteArray& outputBuffer) { @@ -1172,33 +1257,24 @@ void AudioClient::processReceivedSamples(const QByteArray& decodedBuffer, QByteA outputBuffer.resize(_outputFrameSize * AudioConstants::SAMPLE_SIZE); int16_t* outputSamples = reinterpret_cast(outputBuffer.data()); - // convert network audio to float - for (int i = 0; i < AudioConstants::NETWORK_FRAME_SAMPLES_STEREO; i++) { - _mixBuffer[i] = (float)decodedSamples[i] * (1/32768.0f); - } - - // mix in active injectors - if (getActiveLocalAudioInjectors().size() > 0) { - mixLocalAudioInjectors(_mixBuffer); - } + bool hasReverb = _reverb || _receivedAudioStream.hasReverb(); // apply stereo reverb - bool hasReverb = _reverb || _receivedAudioStream.hasReverb(); if (hasReverb) { updateReverbOptions(); - _listenerReverb.render(_mixBuffer, _mixBuffer, AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL); + int16_t* reverbSamples = _networkToOutputResampler ? _networkScratchBuffer : outputSamples; + _listenerReverb.render(decodedSamples, reverbSamples, AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL); } + // resample to output sample rate if (_networkToOutputResampler) { + const int16_t* inputSamples = hasReverb ? _networkScratchBuffer : decodedSamples; + _networkToOutputResampler->render(inputSamples, outputSamples, AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL); + } - // resample to output sample rate - _audioLimiter.render(_mixBuffer, _scratchBuffer, AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL); - _networkToOutputResampler->render(_scratchBuffer, outputSamples, AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL); - - } else { - - // no resampling needed - _audioLimiter.render(_mixBuffer, outputSamples, AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL); + // if no transformations were applied, we still need to copy the buffer + if (!hasReverb && !_networkToOutputResampler) { + memcpy(outputSamples, decodedSamples, decodedBuffer.size()); } } @@ -1381,6 +1457,9 @@ void AudioClient::outputNotify() { bool AudioClient::switchOutputToAudioDevice(const QAudioDeviceInfo& outputDeviceInfo) { bool supportedFormat = false; + Lock lock(_localAudioMutex); + _localSamplesAvailable.exchange(0, std::memory_order_release); + // cleanup any previously initialized device if (_audioOutput) { _audioOutput->stop(); @@ -1391,12 +1470,24 @@ bool AudioClient::switchOutputToAudioDevice(const QAudioDeviceInfo& outputDevice _loopbackOutputDevice = NULL; delete _loopbackAudioOutput; _loopbackAudioOutput = NULL; + + delete[] _outputMixBuffer; + _outputMixBuffer = NULL; + + delete[] _outputScratchBuffer; + _outputScratchBuffer = NULL; + + delete[] _localOutputMixBuffer; + _localOutputMixBuffer = NULL; } if (_networkToOutputResampler) { // if we were using an input to network resampler, delete it here delete _networkToOutputResampler; _networkToOutputResampler = NULL; + + delete _localToOutputResampler; + _localToOutputResampler = NULL; } if (!outputDeviceInfo.isNull()) { @@ -1416,6 +1507,7 @@ bool AudioClient::switchOutputToAudioDevice(const QAudioDeviceInfo& outputDevice assert(_outputFormat.sampleSize() == 16); _networkToOutputResampler = new AudioSRC(_desiredOutputFormat.sampleRate(), _outputFormat.sampleRate(), OUTPUT_CHANNEL_COUNT); + _localToOutputResampler = new AudioSRC(_desiredOutputFormat.sampleRate(), _outputFormat.sampleRate(), OUTPUT_CHANNEL_COUNT); } else { qCDebug(audioclient) << "No resampling required for network output to match actual output format."; @@ -1441,6 +1533,14 @@ bool AudioClient::switchOutputToAudioDevice(const QAudioDeviceInfo& outputDevice _audioOutput->start(&_audioOutputIODevice); lock.unlock(); + int periodSampleSize = _audioOutput->periodSize() / AudioConstants::SAMPLE_SIZE; + // device callback is not restricted to periodSampleSize, so double the mix/scratch buffer sizes + _outputPeriod = periodSampleSize * 2; + _outputMixBuffer = new float[_outputPeriod]; + _outputScratchBuffer = new int16_t[_outputPeriod]; + _localOutputMixBuffer = new float[_outputPeriod]; + _localInjectorsStream.resizeForFrameSize(_outputPeriod * 2); + qCDebug(audioclient) << "Output Buffer capacity in frames: " << _audioOutput->bufferSize() / AudioConstants::SAMPLE_SIZE / (float)deviceFrameSize << "requested bytes:" << requestedSize << "actual bytes:" << _audioOutput->bufferSize() << "os default:" << osDefaultBufferSize << "period size:" << _audioOutput->periodSize(); @@ -1550,26 +1650,61 @@ qint64 AudioClient::AudioOutputIODevice::readData(char * data, qint64 maxSize) { // samples requested from OUTPUT_CHANNEL_COUNT int deviceChannelCount = _audio->_outputFormat.channelCount(); int samplesRequested = (int)(maxSize / AudioConstants::SAMPLE_SIZE) * OUTPUT_CHANNEL_COUNT / deviceChannelCount; + // restrict samplesRequested to the size of our mix/scratch buffers + samplesRequested = std::min(samplesRequested, _audio->_outputPeriod); - int samplesPopped; - int bytesWritten; + int16_t* scratchBuffer = _audio->_outputScratchBuffer; + float* mixBuffer = _audio->_outputMixBuffer; - if ((samplesPopped = _receivedAudioStream.popSamples(samplesRequested, false)) > 0) { - qCDebug(audiostream, "Read %d samples from buffer (%d available)", samplesPopped, _receivedAudioStream.getSamplesAvailable()); + int networkSamplesPopped; + if ((networkSamplesPopped = _receivedAudioStream.popSamples(samplesRequested, false)) > 0) { + qCDebug(audiostream, "Read %d samples from buffer (%d available, %d requested)", networkSamplesPopped, _receivedAudioStream.getSamplesAvailable(), samplesRequested); AudioRingBuffer::ConstIterator lastPopOutput = _receivedAudioStream.getLastPopOutput(); + lastPopOutput.readSamples(scratchBuffer, networkSamplesPopped); - // if required, upmix or downmix to deviceChannelCount - if (deviceChannelCount == OUTPUT_CHANNEL_COUNT) { - lastPopOutput.readSamples((int16_t*)data, samplesPopped); - } else if (deviceChannelCount > OUTPUT_CHANNEL_COUNT) { - lastPopOutput.readSamplesWithUpmix((int16_t*)data, samplesPopped, deviceChannelCount - OUTPUT_CHANNEL_COUNT); - } else { - lastPopOutput.readSamplesWithDownmix((int16_t*)data, samplesPopped); + for (int i = 0; i < networkSamplesPopped; i++) { + mixBuffer[i] = convertToFloat(scratchBuffer[i]); } - bytesWritten = (samplesPopped * AudioConstants::SAMPLE_SIZE) * deviceChannelCount / OUTPUT_CHANNEL_COUNT; + + samplesRequested = networkSamplesPopped; + } + + int injectorSamplesPopped = 0; + { + Lock lock(_audio->_localAudioMutex); + bool append = networkSamplesPopped > 0; + samplesRequested = std::min(samplesRequested, _audio->_localSamplesAvailable.load(std::memory_order_acquire)); + if ((injectorSamplesPopped = _localInjectorsStream.appendSamples(mixBuffer, samplesRequested, append)) > 0) { + _audio->_localSamplesAvailable.fetch_sub(injectorSamplesPopped, std::memory_order_release); + qCDebug(audiostream, "Read %d samples from injectors (%d available, %d requested)", injectorSamplesPopped, _localInjectorsStream.samplesAvailable(), samplesRequested); + } + } + + // prepare injectors for the next callback + QMetaObject::invokeMethod(&_audio->_localAudioThread, "prepare", Qt::QueuedConnection); + + int samplesPopped = std::max(networkSamplesPopped, injectorSamplesPopped); + int framesPopped = samplesPopped / AudioConstants::STEREO; + int bytesWritten; + if (samplesPopped > 0) { + if (deviceChannelCount == OUTPUT_CHANNEL_COUNT) { + // limit the audio + _audio->_audioLimiter.render(mixBuffer, (int16_t*)data, framesPopped); + } else { + _audio->_audioLimiter.render(mixBuffer, scratchBuffer, framesPopped); + + // upmix or downmix to deviceChannelCount + if (deviceChannelCount > OUTPUT_CHANNEL_COUNT) { + int extraChannels = deviceChannelCount - OUTPUT_CHANNEL_COUNT; + channelUpmix(scratchBuffer, (int16_t*)data, samplesPopped, extraChannels); + } else { + channelDownmix(scratchBuffer, (int16_t*)data, samplesPopped); + } + } + + bytesWritten = framesPopped * AudioConstants::SAMPLE_SIZE * deviceChannelCount; } else { // nothing on network, don't grab anything from injectors, and just return 0s - // this will flood the log: qCDebug(audioclient, "empty/partial network buffer"); memset(data, 0, maxSize); bytesWritten = maxSize; } diff --git a/libraries/audio-client/src/AudioClient.h b/libraries/audio-client/src/AudioClient.h index 123da35319..699ba71ef7 100644 --- a/libraries/audio-client/src/AudioClient.h +++ b/libraries/audio-client/src/AudioClient.h @@ -69,9 +69,24 @@ class QIODevice; class Transform; class NLPacket; +class AudioInjectorsThread : public QThread { + Q_OBJECT + +public: + AudioInjectorsThread(AudioClient* audio) : _audio(audio) {} + +public slots : + void prepare(); + +private: + AudioClient* _audio; +}; + class AudioClient : public AbstractAudioInterface, public Dependency { Q_OBJECT SINGLETON_DEPENDENCY + + using LocalInjectorsStream = AudioMixRingBuffer; public: static const int MIN_BUFFER_FRAMES; static const int MAX_BUFFER_FRAMES; @@ -84,8 +99,10 @@ public: class AudioOutputIODevice : public QIODevice { public: - AudioOutputIODevice(MixedProcessedAudioStream& receivedAudioStream, AudioClient* audio) : - _receivedAudioStream(receivedAudioStream), _audio(audio), _unfulfilledReads(0) {}; + AudioOutputIODevice(LocalInjectorsStream& localInjectorsStream, MixedProcessedAudioStream& receivedAudioStream, + AudioClient* audio) : + _localInjectorsStream(localInjectorsStream), _receivedAudioStream(receivedAudioStream), + _audio(audio), _unfulfilledReads(0) {} void start() { open(QIODevice::ReadOnly | QIODevice::Unbuffered); } void stop() { close(); } @@ -93,6 +110,7 @@ public: qint64 writeData(const char * data, qint64 maxSize) override { return 0; } int getRecentUnfulfilledReads() { int unfulfilledReads = _unfulfilledReads; _unfulfilledReads = 0; return unfulfilledReads; } private: + LocalInjectorsStream& _localInjectorsStream; MixedProcessedAudioStream& _receivedAudioStream; AudioClient* _audio; int _unfulfilledReads; @@ -129,8 +147,6 @@ public: Q_INVOKABLE void setAvatarBoundingBoxParameters(glm::vec3 corner, glm::vec3 scale); - QVector& getActiveLocalAudioInjectors() { return _activeLocalAudioInjectors; } - void checkDevices(); static const float CALLBACK_ACCELERATOR_RATIO; @@ -171,6 +187,7 @@ public slots: int setOutputBufferSize(int numFrames, bool persist = true); + void prepareLocalAudioInjectors(); bool outputLocalInjector(AudioInjector* injector) override; bool shouldLoopbackInjectors() override { return _shouldEchoToServer; } @@ -218,7 +235,7 @@ protected: private: void outputFormatChanged(); - void mixLocalAudioInjectors(float* mixBuffer); + bool mixLocalAudioInjectors(float* mixBuffer); float azimuthForSource(const glm::vec3& relativePosition); float gainForSource(float distance, float volume); @@ -262,6 +279,10 @@ private: QAudioOutput* _loopbackAudioOutput; QIODevice* _loopbackOutputDevice; AudioRingBuffer _inputRingBuffer; + LocalInjectorsStream _localInjectorsStream; + // In order to use _localInjectorsStream as a lock-free pipe, + // use it with a single producer/consumer, and track available samples + std::atomic _localSamplesAvailable { 0 }; MixedProcessedAudioStream _receivedAudioStream; bool _isStereoInput; @@ -292,14 +313,28 @@ private: AudioEffectOptions* _reverbOptions; AudioReverb _sourceReverb { AudioConstants::SAMPLE_RATE }; AudioReverb _listenerReverb { AudioConstants::SAMPLE_RATE }; + AudioReverb _localReverb { AudioConstants::SAMPLE_RATE }; // possible streams needed for resample AudioSRC* _inputToNetworkResampler; AudioSRC* _networkToOutputResampler; + AudioSRC* _localToOutputResampler; + + // for network audio (used by network audio thread) + int16_t _networkScratchBuffer[AudioConstants::NETWORK_FRAME_SAMPLES_AMBISONIC]; + + // for local audio (used by audio injectors thread) + float _localMixBuffer[AudioConstants::NETWORK_FRAME_SAMPLES_STEREO]; + int16_t _localScratchBuffer[AudioConstants::NETWORK_FRAME_SAMPLES_AMBISONIC]; + float* _localOutputMixBuffer { NULL }; + AudioInjectorsThread _localAudioThread; + Mutex _localAudioMutex; + + // for output audio (used by this thread) + int _outputPeriod { 0 }; + float* _outputMixBuffer { NULL }; + int16_t* _outputScratchBuffer { NULL }; - // for local hrtf-ing - float _mixBuffer[AudioConstants::NETWORK_FRAME_SAMPLES_STEREO]; - int16_t _scratchBuffer[AudioConstants::NETWORK_FRAME_SAMPLES_AMBISONIC]; AudioLimiter _audioLimiter; // Adds Reverb diff --git a/libraries/audio/src/AudioRingBuffer.cpp b/libraries/audio/src/AudioRingBuffer.cpp index 260c682cde..59b3e874d7 100644 --- a/libraries/audio/src/AudioRingBuffer.cpp +++ b/libraries/audio/src/AudioRingBuffer.cpp @@ -26,46 +26,51 @@ static const QString RING_BUFFER_OVERFLOW_DEBUG { "AudioRingBuffer::writeData has overflown the buffer. Overwriting old data." }; static const QString DROPPED_SILENT_DEBUG { "AudioRingBuffer::addSilentSamples dropping silent samples to prevent overflow." }; -AudioRingBuffer::AudioRingBuffer(int numFrameSamples, int numFramesCapacity) : +template +AudioRingBufferTemplate::AudioRingBufferTemplate(int numFrameSamples, int numFramesCapacity) : _numFrameSamples(numFrameSamples), _frameCapacity(numFramesCapacity), _sampleCapacity(numFrameSamples * numFramesCapacity), _bufferLength(numFrameSamples * (numFramesCapacity + 1)) { if (numFrameSamples) { - _buffer = new int16_t[_bufferLength]; - memset(_buffer, 0, _bufferLength * sizeof(int16_t)); + _buffer = new Sample[_bufferLength]; + memset(_buffer, 0, _bufferLength * SampleSize); _nextOutput = _buffer; _endOfLastWrite = _buffer; } static QString repeatedOverflowMessage = LogHandler::getInstance().addRepeatedMessageRegex(RING_BUFFER_OVERFLOW_DEBUG); static QString repeatedDroppedMessage = LogHandler::getInstance().addRepeatedMessageRegex(DROPPED_SILENT_DEBUG); -}; +} -AudioRingBuffer::~AudioRingBuffer() { +template +AudioRingBufferTemplate::~AudioRingBufferTemplate() { delete[] _buffer; } -void AudioRingBuffer::clear() { +template +void AudioRingBufferTemplate::clear() { _endOfLastWrite = _buffer; _nextOutput = _buffer; } -void AudioRingBuffer::reset() { +template +void AudioRingBufferTemplate::reset() { clear(); _overflowCount = 0; } -void AudioRingBuffer::resizeForFrameSize(int numFrameSamples) { +template +void AudioRingBufferTemplate::resizeForFrameSize(int numFrameSamples) { delete[] _buffer; _numFrameSamples = numFrameSamples; _sampleCapacity = numFrameSamples * _frameCapacity; _bufferLength = numFrameSamples * (_frameCapacity + 1); if (numFrameSamples) { - _buffer = new int16_t[_bufferLength]; - memset(_buffer, 0, _bufferLength * sizeof(int16_t)); + _buffer = new Sample[_bufferLength]; + memset(_buffer, 0, _bufferLength * SampleSize); } else { _buffer = nullptr; } @@ -73,17 +78,29 @@ void AudioRingBuffer::resizeForFrameSize(int numFrameSamples) { reset(); } -int AudioRingBuffer::readSamples(int16_t* destination, int maxSamples) { - return readData((char*)destination, maxSamples * sizeof(int16_t)) / sizeof(int16_t); +template +int AudioRingBufferTemplate::readSamples(Sample* destination, int maxSamples) { + return readData((char*)destination, maxSamples * SampleSize) / SampleSize; } -int AudioRingBuffer::writeSamples(const int16_t* source, int maxSamples) { - return writeData((char*)source, maxSamples * sizeof(int16_t)) / sizeof(int16_t); +template +int AudioRingBufferTemplate::appendSamples(Sample* destination, int maxSamples, bool append) { + if (append) { + return appendData((char*)destination, maxSamples * SampleSize) / SampleSize; + } else { + return readData((char*)destination, maxSamples * SampleSize) / SampleSize; + } } -int AudioRingBuffer::readData(char *data, int maxSize) { +template +int AudioRingBufferTemplate::writeSamples(const Sample* source, int maxSamples) { + return writeData((char*)source, maxSamples * SampleSize) / SampleSize; +} + +template +int AudioRingBufferTemplate::readData(char *data, int maxSize) { // only copy up to the number of samples we have available - int maxSamples = maxSize / sizeof(int16_t); + int maxSamples = maxSize / SampleSize; int numReadSamples = std::min(maxSamples, samplesAvailable()); if (_nextOutput + numReadSamples > _buffer + _bufferLength) { @@ -91,22 +108,56 @@ int AudioRingBuffer::readData(char *data, int maxSize) { int numSamplesToEnd = (_buffer + _bufferLength) - _nextOutput; // read to the end of the buffer - memcpy(data, _nextOutput, numSamplesToEnd * sizeof(int16_t)); + memcpy(data, _nextOutput, numSamplesToEnd * SampleSize); // read the rest from the beginning of the buffer - memcpy(data + (numSamplesToEnd * sizeof(int16_t)), _buffer, (numReadSamples - numSamplesToEnd) * sizeof(int16_t)); + memcpy(data + (numSamplesToEnd * SampleSize), _buffer, (numReadSamples - numSamplesToEnd) * SampleSize); } else { - memcpy(data, _nextOutput, numReadSamples * sizeof(int16_t)); + memcpy(data, _nextOutput, numReadSamples * SampleSize); } shiftReadPosition(numReadSamples); - return numReadSamples * sizeof(int16_t); + return numReadSamples * SampleSize; } -int AudioRingBuffer::writeData(const char* data, int maxSize) { +template +int AudioRingBufferTemplate::appendData(char *data, int maxSize) { + // only copy up to the number of samples we have available + int maxSamples = maxSize / SampleSize; + int numReadSamples = std::min(maxSamples, samplesAvailable()); + + Sample* dest = reinterpret_cast(data); + Sample* output = _nextOutput; + if (_nextOutput + numReadSamples > _buffer + _bufferLength) { + // we're going to need to do two reads to get this data, it wraps around the edge + int numSamplesToEnd = (_buffer + _bufferLength) - _nextOutput; + + // read to the end of the buffer + for (int i = 0; i < numSamplesToEnd; i++) { + *dest++ += *output++; + } + + // read the rest from the beginning of the buffer + output = _buffer; + for (int i = 0; i < (numReadSamples - numSamplesToEnd); i++) { + *dest++ += *output++; + } + } else { + for (int i = 0; i < numReadSamples; i++) { + *dest++ += *output++; + } + } + + shiftReadPosition(numReadSamples); + + return numReadSamples * SampleSize; +} + +template +int AudioRingBufferTemplate::writeData(const char* data, int maxSize) { // only copy up to the number of samples we have capacity for - int maxSamples = maxSize / sizeof(int16_t); + int maxSamples = maxSize / SampleSize; int numWriteSamples = std::min(maxSamples, _sampleCapacity); int samplesRoomFor = _sampleCapacity - samplesAvailable(); @@ -124,20 +175,21 @@ int AudioRingBuffer::writeData(const char* data, int maxSize) { int numSamplesToEnd = (_buffer + _bufferLength) - _endOfLastWrite; // write to the end of the buffer - memcpy(_endOfLastWrite, data, numSamplesToEnd * sizeof(int16_t)); + memcpy(_endOfLastWrite, data, numSamplesToEnd * SampleSize); // write the rest to the beginning of the buffer - memcpy(_buffer, data + (numSamplesToEnd * sizeof(int16_t)), (numWriteSamples - numSamplesToEnd) * sizeof(int16_t)); + memcpy(_buffer, data + (numSamplesToEnd * SampleSize), (numWriteSamples - numSamplesToEnd) * SampleSize); } else { - memcpy(_endOfLastWrite, data, numWriteSamples * sizeof(int16_t)); + memcpy(_endOfLastWrite, data, numWriteSamples * SampleSize); } _endOfLastWrite = shiftedPositionAccomodatingWrap(_endOfLastWrite, numWriteSamples); - return numWriteSamples * sizeof(int16_t); + return numWriteSamples * SampleSize; } -int AudioRingBuffer::samplesAvailable() const { +template +int AudioRingBufferTemplate::samplesAvailable() const { if (!_endOfLastWrite) { return 0; } @@ -149,31 +201,8 @@ int AudioRingBuffer::samplesAvailable() const { return sampleDifference; } -int AudioRingBuffer::addSilentSamples(int silentSamples) { - // NOTE: This implementation is nearly identical to writeData save for s/memcpy/memset, refer to comments there - int numWriteSamples = std::min(silentSamples, _sampleCapacity); - int samplesRoomFor = _sampleCapacity - samplesAvailable(); - - if (numWriteSamples > samplesRoomFor) { - numWriteSamples = samplesRoomFor; - - qCDebug(audio) << qPrintable(DROPPED_SILENT_DEBUG); - } - - if (_endOfLastWrite + numWriteSamples > _buffer + _bufferLength) { - int numSamplesToEnd = (_buffer + _bufferLength) - _endOfLastWrite; - memset(_endOfLastWrite, 0, numSamplesToEnd * sizeof(int16_t)); - memset(_buffer, 0, (numWriteSamples - numSamplesToEnd) * sizeof(int16_t)); - } else { - memset(_endOfLastWrite, 0, numWriteSamples * sizeof(int16_t)); - } - - _endOfLastWrite = shiftedPositionAccomodatingWrap(_endOfLastWrite, numWriteSamples); - - return numWriteSamples; -} - -int16_t* AudioRingBuffer::shiftedPositionAccomodatingWrap(int16_t* position, int numSamplesShift) const { +template +typename AudioRingBufferTemplate::Sample* AudioRingBufferTemplate::shiftedPositionAccomodatingWrap(Sample* position, int numSamplesShift) const { // NOTE: It is possible to shift out-of-bounds if (|numSamplesShift| > 2 * _bufferLength), but this should not occur if (numSamplesShift > 0 && position + numSamplesShift >= _buffer + _bufferLength) { // this shift will wrap the position around to the beginning of the ring @@ -186,11 +215,37 @@ int16_t* AudioRingBuffer::shiftedPositionAccomodatingWrap(int16_t* position, int } } -float AudioRingBuffer::getFrameLoudness(const int16_t* frameStart) const { +template +int AudioRingBufferTemplate::addSilentSamples(int silentSamples) { + // NOTE: This implementation is nearly identical to writeData save for s/memcpy/memset, refer to comments there + int numWriteSamples = std::min(silentSamples, _sampleCapacity); + int samplesRoomFor = _sampleCapacity - samplesAvailable(); + + if (numWriteSamples > samplesRoomFor) { + numWriteSamples = samplesRoomFor; + + qCDebug(audio) << qPrintable(DROPPED_SILENT_DEBUG); + } + + if (_endOfLastWrite + numWriteSamples > _buffer + _bufferLength) { + int numSamplesToEnd = (_buffer + _bufferLength) - _endOfLastWrite; + memset(_endOfLastWrite, 0, numSamplesToEnd * SampleSize); + memset(_buffer, 0, (numWriteSamples - numSamplesToEnd) * SampleSize); + } else { + memset(_endOfLastWrite, 0, numWriteSamples * SampleSize); + } + + _endOfLastWrite = shiftedPositionAccomodatingWrap(_endOfLastWrite, numWriteSamples); + + return numWriteSamples; +} + +template +float AudioRingBufferTemplate::getFrameLoudness(const Sample* frameStart) const { // FIXME: This is a bad measure of loudness - normal estimation uses sqrt(sum(x*x)) float loudness = 0.0f; - const int16_t* sampleAt = frameStart; - const int16_t* bufferLastAt = _buffer + _bufferLength - 1; + const Sample* sampleAt = frameStart; + const Sample* bufferLastAt = _buffer + _bufferLength - 1; for (int i = 0; i < _numFrameSamples; ++i) { loudness += (float) std::abs(*sampleAt); @@ -203,14 +258,16 @@ float AudioRingBuffer::getFrameLoudness(const int16_t* frameStart) const { return loudness; } -float AudioRingBuffer::getFrameLoudness(ConstIterator frameStart) const { +template +float AudioRingBufferTemplate::getFrameLoudness(ConstIterator frameStart) const { if (frameStart.isNull()) { return 0.0f; } return getFrameLoudness(&(*frameStart)); } -int AudioRingBuffer::writeSamples(ConstIterator source, int maxSamples) { +template +int AudioRingBufferTemplate::writeSamples(ConstIterator source, int maxSamples) { int samplesToCopy = std::min(maxSamples, _sampleCapacity); int samplesRoomFor = _sampleCapacity - samplesAvailable(); if (samplesToCopy > samplesRoomFor) { @@ -221,7 +278,7 @@ int AudioRingBuffer::writeSamples(ConstIterator source, int maxSamples) { qCDebug(audio) << qPrintable(RING_BUFFER_OVERFLOW_DEBUG); } - int16_t* bufferLast = _buffer + _bufferLength - 1; + Sample* bufferLast = _buffer + _bufferLength - 1; for (int i = 0; i < samplesToCopy; i++) { *_endOfLastWrite = *source; _endOfLastWrite = (_endOfLastWrite == bufferLast) ? _buffer : _endOfLastWrite + 1; @@ -231,7 +288,8 @@ int AudioRingBuffer::writeSamples(ConstIterator source, int maxSamples) { return samplesToCopy; } -int AudioRingBuffer::writeSamplesWithFade(ConstIterator source, int maxSamples, float fade) { +template +int AudioRingBufferTemplate::writeSamplesWithFade(ConstIterator source, int maxSamples, float fade) { int samplesToCopy = std::min(maxSamples, _sampleCapacity); int samplesRoomFor = _sampleCapacity - samplesAvailable(); if (samplesToCopy > samplesRoomFor) { @@ -242,12 +300,16 @@ int AudioRingBuffer::writeSamplesWithFade(ConstIterator source, int maxSamples, qCDebug(audio) << qPrintable(RING_BUFFER_OVERFLOW_DEBUG); } - int16_t* bufferLast = _buffer + _bufferLength - 1; + Sample* bufferLast = _buffer + _bufferLength - 1; for (int i = 0; i < samplesToCopy; i++) { - *_endOfLastWrite = (int16_t)((float)(*source) * fade); + *_endOfLastWrite = (Sample)((float)(*source) * fade); _endOfLastWrite = (_endOfLastWrite == bufferLast) ? _buffer : _endOfLastWrite + 1; ++source; } return samplesToCopy; } + +// explicit instantiations for scratch/mix buffers +template class AudioRingBufferTemplate; +template class AudioRingBufferTemplate; diff --git a/libraries/audio/src/AudioRingBuffer.h b/libraries/audio/src/AudioRingBuffer.h index 29e7a9e998..bb32df19a2 100644 --- a/libraries/audio/src/AudioRingBuffer.h +++ b/libraries/audio/src/AudioRingBuffer.h @@ -21,15 +21,19 @@ const int DEFAULT_RING_BUFFER_FRAME_CAPACITY = 10; -class AudioRingBuffer { +template +class AudioRingBufferTemplate { + using Sample = T; + static const int SampleSize = sizeof(Sample); + public: - AudioRingBuffer(int numFrameSamples, int numFramesCapacity = DEFAULT_RING_BUFFER_FRAME_CAPACITY); - ~AudioRingBuffer(); + AudioRingBufferTemplate(int numFrameSamples, int numFramesCapacity = DEFAULT_RING_BUFFER_FRAME_CAPACITY); + ~AudioRingBufferTemplate(); // disallow copying - AudioRingBuffer(const AudioRingBuffer&) = delete; - AudioRingBuffer(AudioRingBuffer&&) = delete; - AudioRingBuffer& operator=(const AudioRingBuffer&) = delete; + AudioRingBufferTemplate(const AudioRingBufferTemplate&) = delete; + AudioRingBufferTemplate(AudioRingBufferTemplate&&) = delete; + AudioRingBufferTemplate& operator=(const AudioRingBufferTemplate&) = delete; /// Invalidate any data in the buffer void clear(); @@ -41,13 +45,27 @@ public: // FIXME: discards any data in the buffer void resizeForFrameSize(int numFrameSamples); + // Reading and writing to the buffer uses minimal shared data, such that + // in cases that avoid overwriting the buffer, a single producer/consumer + // may use this as a lock-free pipe (see audio-client/src/AudioClient.cpp). + // IMPORTANT: Avoid changes to the implementation that touch shared data unless you can + // maintain this behavior. + /// Read up to maxSamples into destination (will only read up to samplesAvailable()) /// Returns number of read samples - int readSamples(int16_t* destination, int maxSamples); + int readSamples(Sample* destination, int maxSamples); + + /// Append up to maxSamples into destination (will only read up to samplesAvailable()) + /// If append == false, behaves as readSamples + /// Returns number of appended samples + int appendSamples(Sample* destination, int maxSamples, bool append = true); + + /// Skip up to maxSamples (will only skip up to samplesAvailable()) + void skipSamples(int maxSamples) { shiftReadPosition(std::min(maxSamples, samplesAvailable())); } /// Write up to maxSamples from source (will only write up to sample capacity) /// Returns number of written samples - int writeSamples(const int16_t* source, int maxSamples); + int writeSamples(const Sample* source, int maxSamples); /// Write up to maxSamples silent samples (will only write until other data exists in the buffer) /// This method will not overwrite existing data in the buffer, instead dropping silent samples that would overflow @@ -58,13 +76,17 @@ public: /// Returns number of read bytes int readData(char* destination, int maxSize); + /// Append up to maxSize into destination + /// Returns number of read bytes + int appendData(char* destination, int maxSize); + /// Write up to maxSize from source /// Returns number of written bytes int writeData(const char* source, int maxSize); /// Returns a reference to the index-th sample offset from the current read sample - int16_t& operator[](const int index) { return *shiftedPositionAccomodatingWrap(_nextOutput, index); } - const int16_t& operator[] (const int index) const { return *shiftedPositionAccomodatingWrap(_nextOutput, index); } + Sample& operator[](const int index) { return *shiftedPositionAccomodatingWrap(_nextOutput, index); } + const Sample& operator[] (const int index) const { return *shiftedPositionAccomodatingWrap(_nextOutput, index); } /// Essentially discards the next numSamples from the ring buffer /// NOTE: This is not checked - it is possible to shift past written data @@ -84,41 +106,104 @@ public: class ConstIterator { public: - ConstIterator(); - ConstIterator(int16_t* bufferFirst, int capacity, int16_t* at); + ConstIterator() : + _bufferLength(0), + _bufferFirst(NULL), + _bufferLast(NULL), + _at(NULL) {} + ConstIterator(Sample* bufferFirst, int capacity, Sample* at) : + _bufferLength(capacity), + _bufferFirst(bufferFirst), + _bufferLast(bufferFirst + capacity - 1), + _at(at) {} ConstIterator(const ConstIterator& rhs) = default; bool isNull() const { return _at == NULL; } bool operator==(const ConstIterator& rhs) { return _at == rhs._at; } bool operator!=(const ConstIterator& rhs) { return _at != rhs._at; } - const int16_t& operator*() { return *_at; } + const Sample& operator*() { return *_at; } - ConstIterator& operator=(const ConstIterator& rhs); - ConstIterator& operator++(); - ConstIterator operator++(int); - ConstIterator& operator--(); - ConstIterator operator--(int); - const int16_t& operator[] (int i); - ConstIterator operator+(int i); - ConstIterator operator-(int i); + ConstIterator& operator=(const ConstIterator& rhs) { + _bufferLength = rhs._bufferLength; + _bufferFirst = rhs._bufferFirst; + _bufferLast = rhs._bufferLast; + _at = rhs._at; + return *this; + } + ConstIterator& operator++() { + _at = (_at == _bufferLast) ? _bufferFirst : _at + 1; + return *this; + } + ConstIterator operator++(int) { + ConstIterator tmp(*this); + ++(*this); + return tmp; + } + ConstIterator& operator--() { + _at = (_at == _bufferFirst) ? _bufferLast : _at - 1; + return *this; + } + ConstIterator operator--(int) { + ConstIterator tmp(*this); + --(*this); + return tmp; + } + const Sample& operator[] (int i) { + return *atShiftedBy(i); + } + ConstIterator operator+(int i) { + return ConstIterator(_bufferFirst, _bufferLength, atShiftedBy(i)); + } + ConstIterator operator-(int i) { + return ConstIterator(_bufferFirst, _bufferLength, atShiftedBy(-i)); + } + + void readSamples(Sample* dest, int numSamples) { + auto samplesToEnd = _bufferLast - _at + 1; + + if (samplesToEnd >= numSamples) { + memcpy(dest, _at, numSamples * SampleSize); + _at += numSamples; + } else { + auto samplesFromStart = numSamples - samplesToEnd; + memcpy(dest, _at, samplesToEnd * SampleSize); + memcpy(dest + samplesToEnd, _bufferFirst, samplesFromStart * SampleSize); + + _at = _bufferFirst + samplesFromStart; + } + } + void readSamplesWithFade(Sample* dest, int numSamples, float fade) { + Sample* at = _at; + for (int i = 0; i < numSamples; i++) { + *dest = (float)*at * fade; + ++dest; + at = (at == _bufferLast) ? _bufferFirst : at + 1; + } + } - void readSamples(int16_t* dest, int numSamples); - void readSamplesWithFade(int16_t* dest, int numSamples, float fade); - void readSamplesWithUpmix(int16_t* dest, int numSamples, int numExtraChannels); - void readSamplesWithDownmix(int16_t* dest, int numSamples); private: - int16_t* atShiftedBy(int i); + Sample* atShiftedBy(int i) { + i = (_at - _bufferFirst + i) % _bufferLength; + if (i < 0) { + i += _bufferLength; + } + return _bufferFirst + i; + } int _bufferLength; - int16_t* _bufferFirst; - int16_t* _bufferLast; - int16_t* _at; + Sample* _bufferFirst; + Sample* _bufferLast; + Sample* _at; }; - ConstIterator nextOutput() const; - ConstIterator lastFrameWritten() const; + ConstIterator nextOutput() const { + return ConstIterator(_buffer, _bufferLength, _nextOutput); + } + ConstIterator lastFrameWritten() const { + return ConstIterator(_buffer, _bufferLength, _endOfLastWrite) - _numFrameSamples; + } int writeSamples(ConstIterator source, int maxSamples); int writeSamplesWithFade(ConstIterator source, int maxSamples, float fade); @@ -126,8 +211,8 @@ public: float getFrameLoudness(ConstIterator frameStart) const; protected: - int16_t* shiftedPositionAccomodatingWrap(int16_t* position, int numSamplesShift) const; - float getFrameLoudness(const int16_t* frameStart) const; + Sample* shiftedPositionAccomodatingWrap(Sample* position, int numSamplesShift) const; + float getFrameLoudness(const Sample* frameStart) const; int _numFrameSamples; int _frameCapacity; @@ -135,138 +220,13 @@ protected: int _bufferLength; // actual _buffer length (_sampleCapacity + 1) int _overflowCount{ 0 }; // times the ring buffer has overwritten data - int16_t* _nextOutput{ nullptr }; - int16_t* _endOfLastWrite{ nullptr }; - int16_t* _buffer{ nullptr }; + Sample* _nextOutput{ nullptr }; + Sample* _endOfLastWrite{ nullptr }; + Sample* _buffer{ nullptr }; }; -// inline the iterator: -inline AudioRingBuffer::ConstIterator::ConstIterator() : - _bufferLength(0), - _bufferFirst(NULL), - _bufferLast(NULL), - _at(NULL) {} - -inline AudioRingBuffer::ConstIterator::ConstIterator(int16_t* bufferFirst, int capacity, int16_t* at) : - _bufferLength(capacity), - _bufferFirst(bufferFirst), - _bufferLast(bufferFirst + capacity - 1), - _at(at) {} - -inline AudioRingBuffer::ConstIterator& AudioRingBuffer::ConstIterator::operator=(const ConstIterator& rhs) { - _bufferLength = rhs._bufferLength; - _bufferFirst = rhs._bufferFirst; - _bufferLast = rhs._bufferLast; - _at = rhs._at; - return *this; -} - -inline AudioRingBuffer::ConstIterator& AudioRingBuffer::ConstIterator::operator++() { - _at = (_at == _bufferLast) ? _bufferFirst : _at + 1; - return *this; -} - -inline AudioRingBuffer::ConstIterator AudioRingBuffer::ConstIterator::operator++(int) { - ConstIterator tmp(*this); - ++(*this); - return tmp; -} - -inline AudioRingBuffer::ConstIterator& AudioRingBuffer::ConstIterator::operator--() { - _at = (_at == _bufferFirst) ? _bufferLast : _at - 1; - return *this; -} - -inline AudioRingBuffer::ConstIterator AudioRingBuffer::ConstIterator::operator--(int) { - ConstIterator tmp(*this); - --(*this); - return tmp; -} - -inline const int16_t& AudioRingBuffer::ConstIterator::operator[] (int i) { - return *atShiftedBy(i); -} - -inline AudioRingBuffer::ConstIterator AudioRingBuffer::ConstIterator::operator+(int i) { - return ConstIterator(_bufferFirst, _bufferLength, atShiftedBy(i)); -} - -inline AudioRingBuffer::ConstIterator AudioRingBuffer::ConstIterator::operator-(int i) { - return ConstIterator(_bufferFirst, _bufferLength, atShiftedBy(-i)); -} - -inline int16_t* AudioRingBuffer::ConstIterator::atShiftedBy(int i) { - i = (_at - _bufferFirst + i) % _bufferLength; - if (i < 0) { - i += _bufferLength; - } - return _bufferFirst + i; -} - -inline void AudioRingBuffer::ConstIterator::readSamples(int16_t* dest, int numSamples) { - auto samplesToEnd = _bufferLast - _at + 1; - - if (samplesToEnd >= numSamples) { - memcpy(dest, _at, numSamples * sizeof(int16_t)); - _at += numSamples; - } else { - auto samplesFromStart = numSamples - samplesToEnd; - memcpy(dest, _at, samplesToEnd * sizeof(int16_t)); - memcpy(dest + samplesToEnd, _bufferFirst, samplesFromStart * sizeof(int16_t)); - - _at = _bufferFirst + samplesFromStart; - } -} - -inline void AudioRingBuffer::ConstIterator::readSamplesWithFade(int16_t* dest, int numSamples, float fade) { - int16_t* at = _at; - for (int i = 0; i < numSamples; i++) { - *dest = (float)*at * fade; - ++dest; - at = (at == _bufferLast) ? _bufferFirst : at + 1; - } -} - -inline void AudioRingBuffer::ConstIterator::readSamplesWithUpmix(int16_t* dest, int numSamples, int numExtraChannels) { - int16_t* at = _at; - for (int i = 0; i < numSamples/2; i++) { - - // read 2 samples - int16_t left = *at; - at = (at == _bufferLast) ? _bufferFirst : at + 1; - int16_t right = *at; - at = (at == _bufferLast) ? _bufferFirst : at + 1; - - // write 2 + N samples - *dest++ = left; - *dest++ = right; - for (int n = 0; n < numExtraChannels; n++) { - *dest++ = 0; - } - } -} - -inline void AudioRingBuffer::ConstIterator::readSamplesWithDownmix(int16_t* dest, int numSamples) { - int16_t* at = _at; - for (int i = 0; i < numSamples/2; i++) { - - // read 2 samples - int16_t left = *at; - at = (at == _bufferLast) ? _bufferFirst : at + 1; - int16_t right = *at; - at = (at == _bufferLast) ? _bufferFirst : at + 1; - - // write 1 sample - *dest++ = (int16_t)((left + right) / 2); - } -} - -inline AudioRingBuffer::ConstIterator AudioRingBuffer::nextOutput() const { - return ConstIterator(_buffer, _bufferLength, _nextOutput); -} - -inline AudioRingBuffer::ConstIterator AudioRingBuffer::lastFrameWritten() const { - return ConstIterator(_buffer, _bufferLength, _endOfLastWrite) - _numFrameSamples; -} +// expose explicit instantiations for scratch/mix buffers +using AudioRingBuffer = AudioRingBufferTemplate; +using AudioMixRingBuffer = AudioRingBufferTemplate; #endif // hifi_AudioRingBuffer_h diff --git a/libraries/avatars/src/AvatarData.cpp b/libraries/avatars/src/AvatarData.cpp index f594787f80..c35572a415 100644 --- a/libraries/avatars/src/AvatarData.cpp +++ b/libraries/avatars/src/AvatarData.cpp @@ -1304,6 +1304,9 @@ int AvatarData::getFauxJointIndex(const QString& name) const { if (name == "_CAMERA_RELATIVE_CONTROLLER_RIGHTHAND") { return CAMERA_RELATIVE_CONTROLLER_RIGHTHAND_INDEX; } + if (name == "_CAMERA_MATRIX") { + return CAMERA_MATRIX_INDEX; + } return -1; } diff --git a/libraries/avatars/src/AvatarData.h b/libraries/avatars/src/AvatarData.h index 5604e41f63..5d989b0eee 100644 --- a/libraries/avatars/src/AvatarData.h +++ b/libraries/avatars/src/AvatarData.h @@ -765,5 +765,6 @@ const int CONTROLLER_RIGHTHAND_INDEX = 65533; // -3 const int CONTROLLER_LEFTHAND_INDEX = 65532; // -4 const int CAMERA_RELATIVE_CONTROLLER_RIGHTHAND_INDEX = 65531; // -5 const int CAMERA_RELATIVE_CONTROLLER_LEFTHAND_INDEX = 65530; // -6 +const int CAMERA_MATRIX_INDEX = 65529; // -7 #endif // hifi_AvatarData_h diff --git a/libraries/entities-renderer/src/EntityTreeRenderer.cpp b/libraries/entities-renderer/src/EntityTreeRenderer.cpp index d277fd540f..60bb29f85f 100644 --- a/libraries/entities-renderer/src/EntityTreeRenderer.cpp +++ b/libraries/entities-renderer/src/EntityTreeRenderer.cpp @@ -956,68 +956,29 @@ void EntityTreeRenderer::checkAndCallPreload(const EntityItemID& entityID, const } } -bool EntityTreeRenderer::isCollisionOwner(const QUuid& myNodeID, EntityTreePointer entityTree, - const EntityItemID& id, const Collision& collision) { - EntityItemPointer entity = entityTree->findEntityByEntityItemID(id); - if (!entity) { - return false; - } - QUuid simulatorID = entity->getSimulatorID(); - if (simulatorID.isNull()) { - // Can be null if it has never moved since being created or coming out of persistence. - // However, for there to be a collission, one of the two objects must be moving. - const EntityItemID& otherID = (id == collision.idA) ? collision.idB : collision.idA; - EntityItemPointer otherEntity = entityTree->findEntityByEntityItemID(otherID); - if (!otherEntity) { - return false; - } - simulatorID = otherEntity->getSimulatorID(); - } - - if (simulatorID.isNull() || (simulatorID != myNodeID)) { - return false; - } - - return true; -} - -void EntityTreeRenderer::playEntityCollisionSound(const QUuid& myNodeID, EntityTreePointer entityTree, - const EntityItemID& id, const Collision& collision) { - - if (!isCollisionOwner(myNodeID, entityTree, id, collision)) { - return; - } - - SharedSoundPointer collisionSound; - float mass = 1.0; // value doesn't get used, but set it so compiler is quiet - AACube minAACube; - bool success = false; - _tree->withReadLock([&] { - EntityItemPointer entity = entityTree->findEntityByEntityItemID(id); - if (entity) { - collisionSound = entity->getCollisionSound(); - mass = entity->computeMass(); - minAACube = entity->getMinimumAACube(success); - } - }); - if (!success) { - return; - } +void EntityTreeRenderer::playEntityCollisionSound(EntityItemPointer entity, const Collision& collision) { + assert((bool)entity); + SharedSoundPointer collisionSound = entity->getCollisionSound(); if (!collisionSound) { return; } + bool success = false; + AACube minAACube = entity->getMinimumAACube(success); + if (!success) { + return; + } + float mass = entity->computeMass(); - const float COLLISION_PENETRATION_TO_VELOCITY = 50; // as a subsitute for RELATIVE entity->getVelocity() + const float COLLISION_PENETRATION_TO_VELOCITY = 50.0f; // as a subsitute for RELATIVE entity->getVelocity() // The collision.penetration is a pretty good indicator of changed velocity AFTER the initial contact, // but that first contact depends on exactly where we hit in the physics step. // We can get a more consistent initial-contact energy reading by using the changed velocity. // Note that velocityChange is not a good indicator for continuing collisions, because it does not distinguish // between bounce and sliding along a surface. - const float linearVelocity = (collision.type == CONTACT_EVENT_TYPE_START) ? - glm::length(collision.velocityChange) : - glm::length(collision.penetration) * COLLISION_PENETRATION_TO_VELOCITY; - const float energy = mass * linearVelocity * linearVelocity / 2.0f; - const glm::vec3 position = collision.contactPoint; + const float speedSquared = (collision.type == CONTACT_EVENT_TYPE_START) ? + glm::length2(collision.velocityChange) : + glm::length2(collision.penetration) * COLLISION_PENETRATION_TO_VELOCITY; + const float energy = mass * speedSquared / 2.0f; const float COLLISION_ENERGY_AT_FULL_VOLUME = (collision.type == CONTACT_EVENT_TYPE_START) ? 150.0f : 5.0f; const float COLLISION_MINIMUM_VOLUME = 0.005f; const float energyFactorOfFull = fmin(1.0f, energy / COLLISION_ENERGY_AT_FULL_VOLUME); @@ -1031,7 +992,7 @@ void EntityTreeRenderer::playEntityCollisionSound(const QUuid& myNodeID, EntityT // Shift the pitch down by ln(1 + (size / COLLISION_SIZE_FOR_STANDARD_PITCH)) / ln(2) const float COLLISION_SIZE_FOR_STANDARD_PITCH = 0.2f; const float stretchFactor = log(1.0f + (minAACube.getLargestDimension() / COLLISION_SIZE_FOR_STANDARD_PITCH)) / log(2); - AudioInjector::playSound(collisionSound, volume, stretchFactor, position); + AudioInjector::playSound(collisionSound, volume, stretchFactor, collision.contactPoint); } void EntityTreeRenderer::entityCollisionWithEntity(const EntityItemID& idA, const EntityItemID& idB, @@ -1041,30 +1002,28 @@ void EntityTreeRenderer::entityCollisionWithEntity(const EntityItemID& idA, cons if (!_tree || _shuttingDown) { return; } - // Don't respond to small continuous contacts. - const float COLLISION_MINUMUM_PENETRATION = 0.002f; - if ((collision.type == CONTACT_EVENT_TYPE_CONTINUE) && (glm::length(collision.penetration) < COLLISION_MINUMUM_PENETRATION)) { - return; - } - // See if we should play sounds EntityTreePointer entityTree = std::static_pointer_cast(_tree); const QUuid& myNodeID = DependencyManager::get()->getSessionUUID(); - playEntityCollisionSound(myNodeID, entityTree, idA, collision); - playEntityCollisionSound(myNodeID, entityTree, idB, collision); - // And now the entity scripts - if (isCollisionOwner(myNodeID, entityTree, idA, collision)) { + // trigger scripted collision sounds and events for locally owned objects + EntityItemPointer entityA = entityTree->findEntityByEntityItemID(idA); + if ((bool)entityA && myNodeID == entityA->getSimulatorID()) { + playEntityCollisionSound(entityA, collision); emit collisionWithEntity(idA, idB, collision); if (_entitiesScriptEngine) { _entitiesScriptEngine->callEntityScriptMethod(idA, "collisionWithEntity", idB, collision); } } - - if (isCollisionOwner(myNodeID, entityTree, idB, collision)) { - emit collisionWithEntity(idB, idA, collision); + EntityItemPointer entityB = entityTree->findEntityByEntityItemID(idB); + if ((bool)entityB && myNodeID == entityB->getSimulatorID()) { + playEntityCollisionSound(entityB, collision); + // since we're swapping A and B we need to send the inverted collision + Collision invertedCollision(collision); + invertedCollision.invert(); + emit collisionWithEntity(idB, idA, invertedCollision); if (_entitiesScriptEngine) { - _entitiesScriptEngine->callEntityScriptMethod(idB, "collisionWithEntity", idA, collision); + _entitiesScriptEngine->callEntityScriptMethod(idB, "collisionWithEntity", idA, invertedCollision); } } } diff --git a/libraries/entities-renderer/src/EntityTreeRenderer.h b/libraries/entities-renderer/src/EntityTreeRenderer.h index 8c021ad184..29d463b915 100644 --- a/libraries/entities-renderer/src/EntityTreeRenderer.h +++ b/libraries/entities-renderer/src/EntityTreeRenderer.h @@ -170,11 +170,7 @@ private: bool _wantScripts; QSharedPointer _entitiesScriptEngine; - bool isCollisionOwner(const QUuid& myNodeID, EntityTreePointer entityTree, - const EntityItemID& id, const Collision& collision); - - void playEntityCollisionSound(const QUuid& myNodeID, EntityTreePointer entityTree, - const EntityItemID& id, const Collision& collision); + static void playEntityCollisionSound(EntityItemPointer entity, const Collision& collision); bool _lastPointerEventValid; PointerEvent _lastPointerEvent; diff --git a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp index b947c5283d..bc8c7c222e 100644 --- a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp @@ -505,6 +505,7 @@ ModelPointer RenderableModelEntityItem::getModel(QSharedPointerallocateModel(getModelURL(), renderer->getEntityLoadingPriority(*this)); + _model->setSpatiallyNestableOverride(shared_from_this()); _needsInitialSimulation = true; // If we need to change URLs, update it *after rendering* (to avoid access violations) } else if (QUrl(getModelURL()) != _model->getURL()) { @@ -1189,8 +1190,7 @@ void RenderableModelEntityItem::locationChanged(bool tellPhysics) { PerformanceTimer pertTimer("locationChanged"); EntityItem::locationChanged(tellPhysics); if (_model && _model->isActive()) { - _model->setRotation(getRotation()); - _model->setTranslation(getPosition()); + _model->updateRenderItems(); void* key = (void*)this; std::weak_ptr weakSelf = diff --git a/libraries/entities-renderer/src/RenderableWebEntityItem.cpp b/libraries/entities-renderer/src/RenderableWebEntityItem.cpp index e4d4b222fe..972c23d534 100644 --- a/libraries/entities-renderer/src/RenderableWebEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableWebEntityItem.cpp @@ -18,12 +18,12 @@ #include #include -#include #include #include #include #include #include +#include #include "EntityTreeRenderer.h" #include "EntitiesRendererLogging.h" @@ -46,6 +46,7 @@ EntityItemPointer RenderableWebEntityItem::factory(const EntityItemID& entityID, RenderableWebEntityItem::RenderableWebEntityItem(const EntityItemID& entityItemID) : WebEntityItem(entityItemID) { + qCDebug(entities) << "Created web entity " << getID(); _touchDevice.setCapabilities(QTouchDevice::Position); @@ -57,7 +58,9 @@ RenderableWebEntityItem::RenderableWebEntityItem(const EntityItemID& entityItemI RenderableWebEntityItem::~RenderableWebEntityItem() { destroyWebSurface(); + qCDebug(entities) << "Destroyed web entity " << getID(); + auto geometryCache = DependencyManager::get(); if (geometryCache) { geometryCache->releaseID(_geometryId); @@ -78,18 +81,19 @@ bool RenderableWebEntityItem::buildWebSurface(QSharedPointer QString createGlobalEventBridgeStr = QTextStream(&createGlobalEventBridgeFile).readAll(); // concatenate these js files - javaScriptToInject = webChannelStr + createGlobalEventBridgeStr; + _javaScriptToInject = webChannelStr + createGlobalEventBridgeStr; } else { qCWarning(entitiesrenderer) << "unable to find qwebchannel.js or createGlobalEventBridge.js"; } // Save the original GL context, because creating a QML surface will create a new context - QOpenGLContext * currentContext = QOpenGLContext::currentContext(); + QOpenGLContext* currentContext = QOpenGLContext::currentContext(); if (!currentContext) { return false; } ++_currentWebCount; + qCDebug(entities) << "Building web surface: " << getID() << ", #" << _currentWebCount << ", url = " << _sourceUrl; QSurface * currentSurface = currentContext->surface(); @@ -113,13 +117,12 @@ bool RenderableWebEntityItem::buildWebSurface(QSharedPointer // The lifetime of the QML surface MUST be managed by the main thread // Additionally, we MUST use local variables copied by value, rather than - // member variables, since they would implicitly refer to a this that + // member variables, since they would implicitly refer to a this that // is no longer valid _webSurface->create(currentContext); - _webSurface->setBaseUrl(QUrl::fromLocalFile(PathUtils::resourcesPath() + "/qml/controls/")); - _webSurface->load("WebView.qml", [&](QQmlContext* context, QObject* obj) { - context->setContextProperty("eventBridgeJavaScriptToInject", QVariant(javaScriptToInject)); - }); + + loadSourceURL(); + _webSurface->resume(); _webSurface->getRootItem()->setProperty("url", _sourceUrl); _webSurface->getRootContext()->setContextProperty("desktop", QVariant()); @@ -156,7 +159,8 @@ bool RenderableWebEntityItem::buildWebSurface(QSharedPointer point.setPos(windowPoint); QList touchPoints; touchPoints.push_back(point); - QTouchEvent* touchEvent = new QTouchEvent(QEvent::TouchEnd, nullptr, Qt::NoModifier, Qt::TouchPointReleased, touchPoints); + QTouchEvent* touchEvent = new QTouchEvent(QEvent::TouchEnd, nullptr, + Qt::NoModifier, Qt::TouchPointReleased, touchPoints); touchEvent->setWindow(_webSurface->getWindow()); touchEvent->setDevice(&_touchDevice); touchEvent->setTarget(_webSurface->getRootItem()); @@ -237,14 +241,42 @@ void RenderableWebEntityItem::render(RenderArgs* args) { batch._glColor4f(1.0f, 1.0f, 1.0f, fadeRatio); + const bool IS_AA = true; if (fadeRatio < OPAQUE_ALPHA_THRESHOLD) { - DependencyManager::get()->bindTransparentWebBrowserProgram(batch); + DependencyManager::get()->bindTransparentWebBrowserProgram(batch, IS_AA); } else { - DependencyManager::get()->bindOpaqueWebBrowserProgram(batch); + DependencyManager::get()->bindOpaqueWebBrowserProgram(batch, IS_AA); } DependencyManager::get()->renderQuad(batch, topLeft, bottomRight, texMin, texMax, glm::vec4(1.0f, 1.0f, 1.0f, fadeRatio), _geometryId); } +void RenderableWebEntityItem::loadSourceURL() { + QUrl sourceUrl(_sourceUrl); + if (sourceUrl.scheme() == "http" || sourceUrl.scheme() == "https" || + _sourceUrl.toLower().endsWith(".htm") || _sourceUrl.toLower().endsWith(".html")) { + _contentType = htmlContent; + _webSurface->setBaseUrl(QUrl::fromLocalFile(PathUtils::resourcesPath() + "qml/controls/")); + _webSurface->load("WebView.qml", [&](QQmlContext* context, QObject* obj) { + context->setContextProperty("eventBridgeJavaScriptToInject", QVariant(_javaScriptToInject)); + }); + _webSurface->getRootItem()->setProperty("url", _sourceUrl); + _webSurface->getRootContext()->setContextProperty("desktop", QVariant()); + + } else { + _contentType = qmlContent; + _webSurface->setBaseUrl(QUrl::fromLocalFile(PathUtils::resourcesPath())); + _webSurface->load(_sourceUrl, [&](QQmlContext* context, QObject* obj) {}); + + if (_webSurface->getRootItem() && _webSurface->getRootItem()->objectName() == "tabletRoot") { + auto tabletScriptingInterface = DependencyManager::get(); + tabletScriptingInterface->setQmlTabletRoot("com.highfidelity.interface.tablet.system", + _webSurface->getRootItem(), _webSurface.data()); + } + } + _webSurface->getRootContext()->setContextProperty("globalPosition", vec3toVariant(getPosition())); +} + + void RenderableWebEntityItem::setSourceUrl(const QString& value) { auto valueBeforeSuperclassSet = _sourceUrl; @@ -254,7 +286,10 @@ void RenderableWebEntityItem::setSourceUrl(const QString& value) { qCDebug(entities) << "Changing web entity source URL to " << _sourceUrl; AbstractViewStateInterface::instance()->postLambdaEvent([this] { - _webSurface->getRootItem()->setProperty("url", _sourceUrl); + loadSourceURL(); + if (_contentType == htmlContent) { + _webSurface->getRootItem()->setProperty("url", _sourceUrl); + } }); } } @@ -337,6 +372,14 @@ void RenderableWebEntityItem::destroyWebSurface() { --_currentWebCount; QQuickItem* rootItem = _webSurface->getRootItem(); + + if (rootItem && rootItem->objectName() == "tabletRoot") { + auto tabletScriptingInterface = DependencyManager::get(); + tabletScriptingInterface->setQmlTabletRoot("com.highfidelity.interface.tablet.system", nullptr, nullptr); + } + + // Fix for crash in QtWebEngineCore when rapidly switching domains + // Call stop on the QWebEngineView before destroying OffscreenQMLSurface. if (rootItem) { QObject* obj = rootItem->findChild("webEngineView"); if (obj) { @@ -363,6 +406,12 @@ void RenderableWebEntityItem::destroyWebSurface() { } void RenderableWebEntityItem::update(const quint64& now) { + + if (_webSurface) { + // update globalPosition + _webSurface->getRootContext()->setContextProperty("globalPosition", vec3toVariant(getPosition())); + } + auto interval = now - _lastRenderTime; if (interval > MAX_NO_RENDER_INTERVAL) { destroyWebSurface(); @@ -374,6 +423,13 @@ bool RenderableWebEntityItem::isTransparent() { return fadeRatio < OPAQUE_ALPHA_THRESHOLD; } +QObject* RenderableWebEntityItem::getRootItem() { + if (_webSurface) { + return dynamic_cast(_webSurface->getRootItem()); + } + return nullptr; +} + void RenderableWebEntityItem::emitScriptEvent(const QVariant& message) { if (_webSurface) { _webSurface->emitScriptEvent(message); diff --git a/libraries/entities-renderer/src/RenderableWebEntityItem.h b/libraries/entities-renderer/src/RenderableWebEntityItem.h index a5445d6915..e47e6bdfd3 100644 --- a/libraries/entities-renderer/src/RenderableWebEntityItem.h +++ b/libraries/entities-renderer/src/RenderableWebEntityItem.h @@ -16,6 +16,7 @@ #include #include +#include #include "RenderableEntityItem.h" @@ -33,6 +34,7 @@ public: ~RenderableWebEntityItem(); virtual void render(RenderArgs* args) override; + void loadSourceURL(); virtual void setSourceUrl(const QString& value) override; virtual bool wantsHandControllerPointerEvents() const override { return true; } @@ -51,6 +53,10 @@ public: virtual bool isTransparent() override; + public: + + virtual QObject* getRootItem() override; + private: bool buildWebSurface(QSharedPointer renderer); void destroyWebSurface(); @@ -67,6 +73,14 @@ private: QMetaObject::Connection _mouseReleaseConnection; QMetaObject::Connection _mouseMoveConnection; QMetaObject::Connection _hoverLeaveConnection; + + QString _javaScriptToInject; + + enum contentType { + htmlContent, + qmlContent + }; + contentType _contentType; int _geometryId { 0 }; }; diff --git a/libraries/entities/src/EntityItem.cpp b/libraries/entities/src/EntityItem.cpp index 64bc9fbd5a..3c10d0382c 100644 --- a/libraries/entities/src/EntityItem.cpp +++ b/libraries/entities/src/EntityItem.cpp @@ -688,6 +688,13 @@ int EntityItem::readEntityDataFromBuffer(const unsigned char* data, int bytesLef somethingChanged = true; _simulationOwner.clearCurrentOwner(); } + } else if (newSimOwner.matchesValidID(myNodeID) && !_hasBidOnSimulation) { + // entity-server tells us that we have simulation ownership while we never requested this for this EntityItem, + // this could happen when the user reloads the cache and entity tree. + _dirtyFlags |= Simulation::DIRTY_SIMULATOR_ID; + somethingChanged = true; + _simulationOwner.clearCurrentOwner(); + weOwnSimulation = false; } else if (_simulationOwner.set(newSimOwner)) { _dirtyFlags |= Simulation::DIRTY_SIMULATOR_ID; somethingChanged = true; @@ -1278,7 +1285,7 @@ void EntityItem::grabSimulationOwnership() { auto nodeList = DependencyManager::get(); if (_simulationOwner.matchesValidID(nodeList->getSessionUUID())) { // we already own it - _simulationOwner.promotePriority(SCRIPT_POKE_SIMULATION_PRIORITY); + _simulationOwner.promotePriority(SCRIPT_GRAB_SIMULATION_PRIORITY); } else { // we don't own it yet _simulationOwner.setPendingPriority(SCRIPT_GRAB_SIMULATION_PRIORITY, usecTimestampNow()); @@ -1327,7 +1334,7 @@ bool EntityItem::setProperties(const EntityItemProperties& properties) { SET_ENTITY_PROPERTY_FROM_PROPERTIES(href, setHref); SET_ENTITY_PROPERTY_FROM_PROPERTIES(description, setDescription); SET_ENTITY_PROPERTY_FROM_PROPERTIES(actionData, setActionData); - SET_ENTITY_PROPERTY_FROM_PROPERTIES(parentID, setParentID); + SET_ENTITY_PROPERTY_FROM_PROPERTIES(parentID, updateParentID); SET_ENTITY_PROPERTY_FROM_PROPERTIES(parentJointIndex, setParentJointIndex); SET_ENTITY_PROPERTY_FROM_PROPERTIES(queryAACube, setQueryAACube); @@ -1584,6 +1591,14 @@ void EntityItem::updatePosition(const glm::vec3& value) { } } +void EntityItem::updateParentID(const QUuid& value) { + if (_parentID != value) { + setParentID(value); + _dirtyFlags |= Simulation::DIRTY_MOTION_TYPE; // children are forced to be kinematic + _dirtyFlags |= Simulation::DIRTY_COLLISION_GROUP; // may need to not collide with own avatar + } +} + void EntityItem::updatePositionFromNetwork(const glm::vec3& value) { if (shouldSuppressLocationEdits()) { return; @@ -1792,7 +1807,6 @@ void EntityItem::updateCreated(uint64_t value) { } void EntityItem::computeCollisionGroupAndFinalMask(int16_t& group, int16_t& mask) const { - // TODO: detect attachment status and adopt group of wearer if (_collisionless) { group = BULLET_COLLISION_GROUP_COLLISIONLESS; mask = 0; @@ -1806,10 +1820,33 @@ void EntityItem::computeCollisionGroupAndFinalMask(int16_t& group, int16_t& mask } uint8_t userMask = getCollisionMask(); + if (userMask & USER_COLLISION_GROUP_MY_AVATAR) { + // if this entity is a descendant of MyAvatar, don't collide with MyAvatar. This avoids the + // "bootstrapping" problem where you can shoot yourself across the room by grabbing something + // and holding it against your own avatar. + QUuid ancestorID = findAncestorOfType(NestableType::Avatar); + if (!ancestorID.isNull() && ancestorID == Physics::getSessionUUID()) { + userMask &= ~USER_COLLISION_GROUP_MY_AVATAR; + } + } + if (userMask & USER_COLLISION_GROUP_MY_AVATAR) { + // also, don't bootstrap our own avatar with a hold action + QList holdActions = getActionsOfType(ACTION_TYPE_HOLD); + QList::const_iterator i = holdActions.begin(); + while (i != holdActions.end()) { + EntityActionPointer action = *i; + if (action->isMine()) { + userMask &= ~USER_COLLISION_GROUP_MY_AVATAR; + break; + } + i++; + } + } + if ((bool)(userMask & USER_COLLISION_GROUP_MY_AVATAR) != (bool)(userMask & USER_COLLISION_GROUP_OTHER_AVATAR)) { // asymmetric avatar collision mask bits - if (!getSimulatorID().isNull() && (!getSimulatorID().isNull()) && getSimulatorID() != Physics::getSessionUUID()) { + if (!getSimulatorID().isNull() && getSimulatorID() != Physics::getSessionUUID()) { // someone else owns the simulation, so we toggle the avatar bits (swap interpretation) userMask ^= USER_COLLISION_MASK_AVATARS | ~userMask; } @@ -1859,6 +1896,10 @@ void EntityItem::setPendingOwnershipPriority(quint8 priority, const quint64& tim _simulationOwner.setPendingPriority(priority, timestamp); } +void EntityItem::rememberHasSimulationOwnershipBid() const { + _hasBidOnSimulation = true; +} + QString EntityItem::actionsToDebugString() { QString result; QVector serializedActions; @@ -1908,6 +1949,7 @@ bool EntityItem::addActionInternal(EntitySimulationPointer simulation, EntityAct if (success) { _allActionsDataCache = newDataCache; _dirtyFlags |= Simulation::DIRTY_PHYSICS_ACTIVATION; + _dirtyFlags |= Simulation::DIRTY_COLLISION_GROUP; // may need to not collide with own avatar } else { qCDebug(entities) << "EntityItem::addActionInternal -- serializeActions failed"; } @@ -1930,6 +1972,7 @@ bool EntityItem::updateAction(EntitySimulationPointer simulation, const QUuid& a action->setIsMine(true); serializeActions(success, _allActionsDataCache); _dirtyFlags |= Simulation::DIRTY_PHYSICS_ACTIVATION; + _dirtyFlags |= Simulation::DIRTY_COLLISION_GROUP; // may need to not collide with own avatar } else { qCDebug(entities) << "EntityItem::updateAction failed"; } @@ -1967,6 +2010,7 @@ bool EntityItem::removeActionInternal(const QUuid& actionID, EntitySimulationPoi bool success = true; serializeActions(success, _allActionsDataCache); _dirtyFlags |= Simulation::DIRTY_PHYSICS_ACTIVATION; + _dirtyFlags |= Simulation::DIRTY_COLLISION_GROUP; // may need to not collide with own avatar setActionDataNeedsTransmit(true); return success; } @@ -1987,6 +2031,7 @@ bool EntityItem::clearActions(EntitySimulationPointer simulation) { _actionsToRemove.clear(); _allActionsDataCache.clear(); _dirtyFlags |= Simulation::DIRTY_PHYSICS_ACTIVATION; + _dirtyFlags |= Simulation::DIRTY_COLLISION_GROUP; // may need to not collide with own avatar }); return true; } @@ -2193,7 +2238,7 @@ bool EntityItem::shouldSuppressLocationEdits() const { return false; } -QList EntityItem::getActionsOfType(EntityActionType typeToGet) { +QList EntityItem::getActionsOfType(EntityActionType typeToGet) const { QList result; QHash::const_iterator i = _objectActions.begin(); diff --git a/libraries/entities/src/EntityItem.h b/libraries/entities/src/EntityItem.h index b0389cf99f..e69195d53d 100644 --- a/libraries/entities/src/EntityItem.h +++ b/libraries/entities/src/EntityItem.h @@ -321,6 +321,7 @@ public: void updateSimulationOwner(const SimulationOwner& owner); void clearSimulationOwnership(); void setPendingOwnershipPriority(quint8 priority, const quint64& timestamp); + void rememberHasSimulationOwnershipBid() const; const QString& getMarketplaceID() const { return _marketplaceID; } void setMarketplaceID(const QString& value) { _marketplaceID = value; } @@ -343,6 +344,7 @@ public: // updateFoo() methods to be used when changes need to be accumulated in the _dirtyFlags virtual void updateRegistrationPoint(const glm::vec3& value); void updatePosition(const glm::vec3& value); + void updateParentID(const QUuid& value); void updatePositionFromNetwork(const glm::vec3& value); void updateDimensions(const glm::vec3& value); void updateRotation(const glm::quat& rotation); @@ -419,7 +421,7 @@ public: const QUuid& getSourceUUID() const { return _sourceUUID; } bool matchesSourceUUID(const QUuid& sourceUUID) const { return _sourceUUID == sourceUUID; } - QList getActionsOfType(EntityActionType typeToGet); + QList getActionsOfType(EntityActionType typeToGet) const; // these are in the frame of this object virtual glm::quat getAbsoluteJointRotationInObjectFrame(int index) const override { return glm::quat(); } @@ -496,16 +498,16 @@ protected: mutable AABox _cachedAABox; mutable AACube _maxAACube; mutable AACube _minAACube; - mutable bool _recalcAABox = true; - mutable bool _recalcMinAACube = true; - mutable bool _recalcMaxAACube = true; + mutable bool _recalcAABox { true }; + mutable bool _recalcMinAACube { true }; + mutable bool _recalcMaxAACube { true }; float _localRenderAlpha; - float _density = ENTITY_ITEM_DEFAULT_DENSITY; // kg/m^3 + float _density { ENTITY_ITEM_DEFAULT_DENSITY }; // kg/m^3 // NOTE: _volumeMultiplier is used to allow some mass properties code exist in the EntityItem base class // rather than in all of the derived classes. If we ever collapse these classes to one we could do it a // different way. - float _volumeMultiplier = 1.0f; + float _volumeMultiplier { 1.0f }; glm::vec3 _gravity; glm::vec3 _acceleration; float _damping; @@ -515,7 +517,7 @@ protected: QString _script; /// the value of the script property QString _loadedScript; /// the value of _script when the last preload signal was sent - quint64 _scriptTimestamp{ ENTITY_ITEM_DEFAULT_SCRIPT_TIMESTAMP }; /// the script loaded property used for forced reload + quint64 _scriptTimestamp { ENTITY_ITEM_DEFAULT_SCRIPT_TIMESTAMP }; /// the script loaded property used for forced reload QString _serverScripts; /// keep track of time when _serverScripts property was last changed @@ -523,7 +525,7 @@ protected: /// the value of _scriptTimestamp when the last preload signal was sent // NOTE: on construction we want this to be different from _scriptTimestamp so we intentionally bump it - quint64 _loadedScriptTimestamp{ ENTITY_ITEM_DEFAULT_SCRIPT_TIMESTAMP + 1 }; + quint64 _loadedScriptTimestamp { ENTITY_ITEM_DEFAULT_SCRIPT_TIMESTAMP + 1 }; QString _collisionSoundURL; SharedSoundPointer _collisionSound; @@ -561,8 +563,8 @@ protected: uint32_t _dirtyFlags; // things that have changed from EXTERNAL changes (via script or packet) but NOT from simulation // these backpointers are only ever set/cleared by friends: - EntityTreeElementPointer _element = nullptr; // set by EntityTreeElement - void* _physicsInfo = nullptr; // set by EntitySimulation + EntityTreeElementPointer _element { nullptr }; // set by EntityTreeElement + void* _physicsInfo { nullptr }; // set by EntitySimulation bool _simulated; // set by EntitySimulation bool addActionInternal(EntitySimulationPointer simulation, EntityActionPointer action); @@ -579,12 +581,15 @@ protected: // are used to keep track of and work around this situation. void checkWaitingToRemove(EntitySimulationPointer simulation = nullptr); mutable QSet _actionsToRemove; - mutable bool _actionDataDirty = false; - mutable bool _actionDataNeedsTransmit = false; + mutable bool _actionDataDirty { false }; + mutable bool _actionDataNeedsTransmit { false }; // _previouslyDeletedActions is used to avoid an action being re-added due to server round-trip lag static quint64 _rememberDeletedActionTime; mutable QHash _previouslyDeletedActions; + // per entity keep state if it ever bid on simulation, so that we can ignore false simulation ownership + mutable bool _hasBidOnSimulation { false }; + QUuid _sourceUUID; /// the server node UUID we came from bool _clientOnly { false }; @@ -593,7 +598,7 @@ protected: // physics related changes from the network to suppress any duplicates and make // sure redundant applications are idempotent glm::vec3 _lastUpdatedPositionValue; - glm::quat _lastUpdatedRotationValue; + glm::quat _lastUpdatedRotationValue; glm::vec3 _lastUpdatedVelocityValue; glm::vec3 _lastUpdatedAngularVelocityValue; glm::vec3 _lastUpdatedAccelerationValue; diff --git a/libraries/entities/src/EntityScriptingInterface.cpp b/libraries/entities/src/EntityScriptingInterface.cpp index 26754335a0..85c3fc74f6 100644 --- a/libraries/entities/src/EntityScriptingInterface.cpp +++ b/libraries/entities/src/EntityScriptingInterface.cpp @@ -25,6 +25,7 @@ #include "QVariantGLM.h" #include "SimulationOwner.h" #include "ZoneEntityItem.h" +#include "WebEntityItem.h" #include @@ -230,6 +231,7 @@ QUuid EntityScriptingInterface::addEntity(const EntityItemProperties& properties // and make note of it now, so we can act on it right away. propertiesWithSimID.setSimulationOwner(myNodeID, SCRIPT_POKE_SIMULATION_PRIORITY); entity->setSimulationOwner(myNodeID, SCRIPT_POKE_SIMULATION_PRIORITY); + entity->rememberHasSimulationOwnershipBid(); } entity->setLastBroadcast(usecTimestampNow()); @@ -443,6 +445,7 @@ QUuid EntityScriptingInterface::editEntity(QUuid id, const EntityItemProperties& // we make a bid for simulation ownership properties.setSimulationOwner(myNodeID, SCRIPT_POKE_SIMULATION_PRIORITY); entity->pokeSimulationOwnership(); + entity->rememberHasSimulationOwnershipBid(); } } if (properties.parentRelatedPropertyChanged() && entity->computePuffedQueryAACube()) { @@ -1366,14 +1369,16 @@ bool EntityScriptingInterface::isChildOfParent(QUuid childID, QUuid parentID) { _entityTree->withReadLock([&] { EntityItemPointer parent = _entityTree->findEntityByEntityItemID(parentID); - parent->forEachDescendant([&](SpatiallyNestablePointer descendant) { - if(descendant->getID() == childID) { - isChild = true; - return; - } - }); + if (parent) { + parent->forEachDescendant([&](SpatiallyNestablePointer descendant) { + if (descendant->getID() == childID) { + isChild = true; + return; + } + }); + } }); - + return isChild; } @@ -1397,7 +1402,8 @@ QVector EntityScriptingInterface::getChildrenIDsOfJoint(const QUuid& pare return; } parent->forEachChild([&](SpatiallyNestablePointer child) { - if (child->getParentJointIndex() == jointIndex) { + if (child->getParentJointIndex() == jointIndex && + child->getNestableType() != NestableType::Overlay) { result.push_back(child->getID()); } }); @@ -1492,3 +1498,12 @@ void EntityScriptingInterface::setCostMultiplier(float value) { costMultiplier = value; } +QObject* EntityScriptingInterface::getWebViewRoot(const QUuid& entityID) { + if (auto entity = checkForTreeEntityAndTypeMatch(entityID, EntityTypes::Web)) { + auto webEntity = std::dynamic_pointer_cast(entity); + QObject* root = webEntity->getRootItem(); + return root; + } else { + return nullptr; + } +} diff --git a/libraries/entities/src/EntityScriptingInterface.h b/libraries/entities/src/EntityScriptingInterface.h index 12fef968f9..0353fa08a8 100644 --- a/libraries/entities/src/EntityScriptingInterface.h +++ b/libraries/entities/src/EntityScriptingInterface.h @@ -285,6 +285,8 @@ public slots: Q_INVOKABLE void emitScriptEvent(const EntityItemID& entityID, const QVariant& message); + Q_INVOKABLE QObject* getWebViewRoot(const QUuid& entityID); + signals: void collisionWithEntity(const EntityItemID& idA, const EntityItemID& idB, const Collision& collision); diff --git a/libraries/entities/src/EntityTree.cpp b/libraries/entities/src/EntityTree.cpp index e75c5442b2..848a4ad96e 100644 --- a/libraries/entities/src/EntityTree.cpp +++ b/libraries/entities/src/EntityTree.cpp @@ -64,7 +64,7 @@ EntityTree::~EntityTree() { } void EntityTree::setEntityScriptSourceWhitelist(const QString& entityScriptSourceWhitelist) { - _entityScriptSourceWhitelist = entityScriptSourceWhitelist.split(','); + _entityScriptSourceWhitelist = entityScriptSourceWhitelist.split(',', QString::SkipEmptyParts); } @@ -1111,7 +1111,15 @@ int EntityTree::processEditPacketData(ReceivedMessage& message, const unsigned c endUpdate = usecTimestampNow(); _totalUpdates++; } else if (message.getType() == PacketType::EntityAdd) { - if (senderNode->getCanRez() || senderNode->getCanRezTmp()) { + bool failedAdd = !allowed; + if (!allowed) { + qCDebug(entities) << "Filtered entity add. ID:" << entityItemID; + } else if (!senderNode->getCanRez() && !senderNode->getCanRezTmp()) { + failedAdd = true; + qCDebug(entities) << "User without 'rez rights' [" << senderNode->getUUID() + << "] attempted to add an entity ID:" << entityItemID; + + } else { // this is a new entity... assign a new entityID properties.setCreated(properties.getLastEdited()); properties.setLastEditedBy(senderNode->getUUID()); @@ -1126,7 +1134,7 @@ int EntityTree::processEditPacketData(ReceivedMessage& message, const unsigned c startLogging = usecTimestampNow(); if (wantEditLogging()) { qCDebug(entities) << "User [" << senderNode->getUUID() << "] added entity. ID:" - << newEntity->getEntityItemID(); + << newEntity->getEntityItemID(); qCDebug(entities) << " properties:" << properties; } if (wantTerseEditLogging()) { @@ -1136,10 +1144,14 @@ int EntityTree::processEditPacketData(ReceivedMessage& message, const unsigned c } endLogging = usecTimestampNow(); + } else { + failedAdd = true; + qCDebug(entities) << "Add entity failed ID:" << entityItemID; } - } else { - qCDebug(entities) << "User without 'rez rights' [" << senderNode->getUUID() - << "] attempted to add an entity."; + } + if (failedAdd) { // Let client know it failed, so that they don't have an entity that no one else sees. + QWriteLocker locker(&_recentlyDeletedEntitiesLock); + _recentlyDeletedEntityItemIDs.insert(usecTimestampNow(), entityItemID); } } else { static QString repeatedMessage = @@ -1560,6 +1572,8 @@ bool EntityTree::sendEntitiesOperation(OctreeElementPointer element, void* extra return args->map->value(oldID); } EntityItemID newID = QUuid::createUuid(); + args->map->insert(oldID, newID); + EntityItemProperties properties = item->getProperties(); EntityItemID oldParentID = properties.getParentID(); if (oldParentID.isInvalidID()) { // no parent @@ -1575,6 +1589,43 @@ bool EntityTree::sendEntitiesOperation(OctreeElementPointer element, void* extra } } + if (!properties.getXNNeighborID().isInvalidID()) { + auto neighborEntity = args->ourTree->findEntityByEntityItemID(properties.getXNNeighborID()); + if (neighborEntity) { + properties.setXNNeighborID(getMapped(neighborEntity)); + } + } + if (!properties.getXPNeighborID().isInvalidID()) { + auto neighborEntity = args->ourTree->findEntityByEntityItemID(properties.getXPNeighborID()); + if (neighborEntity) { + properties.setXPNeighborID(getMapped(neighborEntity)); + } + } + if (!properties.getYNNeighborID().isInvalidID()) { + auto neighborEntity = args->ourTree->findEntityByEntityItemID(properties.getYNNeighborID()); + if (neighborEntity) { + properties.setYNNeighborID(getMapped(neighborEntity)); + } + } + if (!properties.getYPNeighborID().isInvalidID()) { + auto neighborEntity = args->ourTree->findEntityByEntityItemID(properties.getYPNeighborID()); + if (neighborEntity) { + properties.setYPNeighborID(getMapped(neighborEntity)); + } + } + if (!properties.getZNNeighborID().isInvalidID()) { + auto neighborEntity = args->ourTree->findEntityByEntityItemID(properties.getZNNeighborID()); + if (neighborEntity) { + properties.setZNNeighborID(getMapped(neighborEntity)); + } + } + if (!properties.getZPNeighborID().isInvalidID()) { + auto neighborEntity = args->ourTree->findEntityByEntityItemID(properties.getZPNeighborID()); + if (neighborEntity) { + properties.setZPNeighborID(getMapped(neighborEntity)); + } + } + // set creation time to "now" for imported entities properties.setCreated(usecTimestampNow()); @@ -1592,7 +1643,6 @@ bool EntityTree::sendEntitiesOperation(OctreeElementPointer element, void* extra args->otherTree->addEntity(newID, properties); }); } - args->map->insert(oldID, newID); return newID; }; diff --git a/libraries/entities/src/WebEntityItem.h b/libraries/entities/src/WebEntityItem.h index 202a8fb93f..19a7b577fe 100644 --- a/libraries/entities/src/WebEntityItem.h +++ b/libraries/entities/src/WebEntityItem.h @@ -59,6 +59,8 @@ public: void setDPI(uint16_t value); uint16_t getDPI() const; + virtual QObject* getRootItem() { return nullptr; } + protected: QString _sourceUrl; uint16_t _dpi; diff --git a/libraries/gl/src/gl/OffscreenQmlSurface.cpp b/libraries/gl/src/gl/OffscreenQmlSurface.cpp index 8559872aba..8af115ebcb 100644 --- a/libraries/gl/src/gl/OffscreenQmlSurface.cpp +++ b/libraries/gl/src/gl/OffscreenQmlSurface.cpp @@ -749,8 +749,12 @@ void OffscreenQmlSurface::resume() { _paused = false; _render = true; - getRootItem()->setProperty("eventBridge", QVariant::fromValue(this)); - getRootContext()->setContextProperty("webEntity", this); + if (getRootItem()) { + getRootItem()->setProperty("eventBridge", QVariant::fromValue(this)); + } + if (getRootContext()) { + getRootContext()->setContextProperty("webEntity", this); + } } bool OffscreenQmlSurface::isPaused() const { diff --git a/libraries/model/src/model/TextureMap.cpp b/libraries/model/src/model/TextureMap.cpp index 6ae7d6f339..9f62c9abbc 100755 --- a/libraries/model/src/model/TextureMap.cpp +++ b/libraries/model/src/model/TextureMap.cpp @@ -52,6 +52,7 @@ std::atomic DECIMATED_TEXTURE_COUNT { 0 }; std::atomic RECTIFIED_TEXTURE_COUNT { 0 }; QImage processSourceImage(const QImage& srcImage, bool cubemap) { + PROFILE_RANGE(resource_parse, "processSourceImage"); const uvec2 srcImageSize = toGlm(srcImage.size()); uvec2 targetSize = srcImageSize; @@ -109,6 +110,7 @@ void TextureMap::setLightmapOffsetScale(float offset, float scale) { } const QImage TextureUsage::process2DImageColor(const QImage& srcImage, bool& validAlpha, bool& alphaAsMask) { + PROFILE_RANGE(resource_parse, "process2DImageColor"); QImage image = processSourceImage(srcImage, false); validAlpha = false; alphaAsMask = true; @@ -197,10 +199,11 @@ const QImage& image, bool isLinear, bool doCompress) { } } -#define CPU_MIPMAPS 0 +#define CPU_MIPMAPS 1 void generateMips(gpu::Texture* texture, QImage& image, gpu::Element formatMip) { #if CPU_MIPMAPS + PROFILE_RANGE(resource_parse, "generateMips"); auto numMips = texture->evalNumMips(); for (uint16 level = 1; level < numMips; ++level) { QSize mipSize(texture->evalMipWidth(level), texture->evalMipHeight(level)); @@ -214,6 +217,7 @@ void generateMips(gpu::Texture* texture, QImage& image, gpu::Element formatMip) void generateFaceMips(gpu::Texture* texture, QImage& image, gpu::Element formatMip, uint8 face) { #if CPU_MIPMAPS + PROFILE_RANGE(resource_parse, "generateFaceMips"); auto numMips = texture->evalNumMips(); for (uint16 level = 1; level < numMips; ++level) { QSize mipSize(texture->evalMipWidth(level), texture->evalMipHeight(level)); @@ -226,6 +230,7 @@ void generateFaceMips(gpu::Texture* texture, QImage& image, gpu::Element formatM } gpu::Texture* TextureUsage::process2DTextureColorFromImage(const QImage& srcImage, const std::string& srcImageName, bool isLinear, bool doCompress, bool generateMips) { + PROFILE_RANGE(resource_parse, "process2DTextureColorFromImage"); bool validAlpha = false; bool alphaAsMask = true; QImage image = process2DImageColor(srcImage, validAlpha, alphaAsMask); diff --git a/libraries/octree/src/Octree.cpp b/libraries/octree/src/Octree.cpp index d2d7aba517..58910c66bd 100644 --- a/libraries/octree/src/Octree.cpp +++ b/libraries/octree/src/Octree.cpp @@ -1084,17 +1084,17 @@ int Octree::encodeTreeBitstreamRecursion(OctreeElementPointer element, params.stopReason = EncodeBitstreamParams::WAS_IN_VIEW; return bytesAtThisLevel; } + } - // If we're not in delta sending mode, and we weren't asked to do a force send, and the voxel hasn't changed, - // then we can also bail early and save bits - if (!params.forceSendScene && !params.deltaView && - !element->hasChangedSince(params.lastQuerySent - CHANGE_FUDGE)) { - if (params.stats) { - params.stats->skippedNoChange(element); - } - params.stopReason = EncodeBitstreamParams::NO_CHANGE; - return bytesAtThisLevel; + // If we're not in delta sending mode, and we weren't asked to do a force send, and the octree element hasn't changed, + // then we can also bail early and save bits + if (!params.forceSendScene && !params.deltaView && + !element->hasChangedSince(params.lastQuerySent - CHANGE_FUDGE)) { + if (params.stats) { + params.stats->skippedNoChange(element); } + params.stopReason = EncodeBitstreamParams::NO_CHANGE; + return bytesAtThisLevel; } bool keepDiggingDeeper = true; // Assuming we're in view we have a great work ethic, we're always ready for more! diff --git a/libraries/physics/src/ContactInfo.cpp b/libraries/physics/src/ContactInfo.cpp index c2ea6e8671..085f746a73 100644 --- a/libraries/physics/src/ContactInfo.cpp +++ b/libraries/physics/src/ContactInfo.cpp @@ -13,15 +13,25 @@ void ContactInfo::update(uint32_t currentStep, const btManifoldPoint& p) { _lastStep = currentStep; - ++_numSteps; positionWorldOnB = p.m_positionWorldOnB; normalWorldOnB = p.m_normalWorldOnB; distance = p.m_distance1; -} +} + +const uint32_t STEPS_BETWEEN_CONTINUE_EVENTS = 9; ContactEventType ContactInfo::computeType(uint32_t thisStep) { - if (_lastStep != thisStep) { - return CONTACT_EVENT_TYPE_END; + if (_continueExpiry == 0) { + _continueExpiry = thisStep + STEPS_BETWEEN_CONTINUE_EVENTS; + return CONTACT_EVENT_TYPE_START; } - return (_numSteps == 1) ? CONTACT_EVENT_TYPE_START : CONTACT_EVENT_TYPE_CONTINUE; + return (_lastStep == thisStep) ? CONTACT_EVENT_TYPE_CONTINUE : CONTACT_EVENT_TYPE_END; +} + +bool ContactInfo::readyForContinue(uint32_t thisStep) { + if (thisStep > _continueExpiry) { + _continueExpiry = thisStep + STEPS_BETWEEN_CONTINUE_EVENTS; + return true; + } + return false; } diff --git a/libraries/physics/src/ContactInfo.h b/libraries/physics/src/ContactInfo.h index 11c908a414..8d05f73b61 100644 --- a/libraries/physics/src/ContactInfo.h +++ b/libraries/physics/src/ContactInfo.h @@ -19,20 +19,22 @@ class ContactInfo { -public: +public: void update(uint32_t currentStep, const btManifoldPoint& p); ContactEventType computeType(uint32_t thisStep); const btVector3& getPositionWorldOnB() const { return positionWorldOnB; } btVector3 getPositionWorldOnA() const { return positionWorldOnB + normalWorldOnB * distance; } + bool readyForContinue(uint32_t thisStep); + btVector3 positionWorldOnB; btVector3 normalWorldOnB; btScalar distance; private: - uint32_t _lastStep = 0; - uint32_t _numSteps = 0; -}; + uint32_t _lastStep { 0 }; + uint32_t _continueExpiry { 0 }; +}; #endif // hifi_ContactEvent_h diff --git a/libraries/physics/src/EntityMotionState.cpp b/libraries/physics/src/EntityMotionState.cpp index 1833d0aba4..02cee9a03a 100644 --- a/libraries/physics/src/EntityMotionState.cpp +++ b/libraries/physics/src/EntityMotionState.cpp @@ -582,6 +582,8 @@ void EntityMotionState::sendUpdate(OctreeEditPacketSender* packetSender, uint32_ _nextOwnershipBid = now + USECS_BETWEEN_OWNERSHIP_BIDS; // copy _outgoingPriority into pendingPriority... _entity->setPendingOwnershipPriority(_outgoingPriority, now); + // don't forget to remember that we have made a bid + _entity->rememberHasSimulationOwnershipBid(); // ...then reset _outgoingPriority in preparation for the next frame _outgoingPriority = 0; } else if (_outgoingPriority != _entity->getSimulationPriority()) { @@ -762,6 +764,11 @@ void EntityMotionState::computeCollisionGroupAndMask(int16_t& group, int16_t& ma _entity->computeCollisionGroupAndFinalMask(group, mask); } +bool EntityMotionState::shouldBeLocallyOwned() const { + return (_outgoingPriority > VOLUNTEER_SIMULATION_PRIORITY && _outgoingPriority > _entity->getSimulationPriority()) || + _entity->getSimulatorID() == Physics::getSessionUUID(); +} + void EntityMotionState::upgradeOutgoingPriority(uint8_t priority) { _outgoingPriority = glm::max(_outgoingPriority, priority); } diff --git a/libraries/physics/src/EntityMotionState.h b/libraries/physics/src/EntityMotionState.h index 194d82805f..feac47d8ec 100644 --- a/libraries/physics/src/EntityMotionState.h +++ b/libraries/physics/src/EntityMotionState.h @@ -78,6 +78,8 @@ public: virtual void computeCollisionGroupAndMask(int16_t& group, int16_t& mask) const override; + bool shouldBeLocallyOwned() const override; + friend class PhysicalEntitySimulation; protected: diff --git a/libraries/physics/src/ObjectAction.cpp b/libraries/physics/src/ObjectAction.cpp index 140de3a972..95448ad029 100644 --- a/libraries/physics/src/ObjectAction.cpp +++ b/libraries/physics/src/ObjectAction.cpp @@ -133,6 +133,7 @@ QVariantMap ObjectAction::getArguments() { arguments["::no-motion-state"] = true; } } + arguments["isMine"] = isMine(); }); return arguments; } diff --git a/libraries/physics/src/ObjectMotionState.h b/libraries/physics/src/ObjectMotionState.h index a7894998a8..1d258560c3 100644 --- a/libraries/physics/src/ObjectMotionState.h +++ b/libraries/physics/src/ObjectMotionState.h @@ -146,6 +146,8 @@ public: void dirtyInternalKinematicChanges() { _hasInternalKinematicChanges = true; } void clearInternalKinematicChanges() { _hasInternalKinematicChanges = false; } + virtual bool shouldBeLocallyOwned() const { return false; } + friend class PhysicsEngine; protected: diff --git a/libraries/physics/src/PhysicsEngine.cpp b/libraries/physics/src/PhysicsEngine.cpp index ba002d925c..f57be4eab3 100644 --- a/libraries/physics/src/PhysicsEngine.cpp +++ b/libraries/physics/src/PhysicsEngine.cpp @@ -270,7 +270,7 @@ void PhysicsEngine::stepSimulation() { } auto onSubStep = [this]() { - updateContactMap(); + this->updateContactMap(); }; int numSubsteps = _dynamicsWorld->stepSimulationWithSubstepCallback(timeStep, PHYSICS_ENGINE_MAX_NUM_SUBSTEPS, @@ -393,7 +393,6 @@ void PhysicsEngine::updateContactMap() { } const CollisionEvents& PhysicsEngine::getCollisionEvents() { - const uint32_t CONTINUE_EVENT_FILTER_FREQUENCY = 10; _collisionEvents.clear(); // scan known contacts and trigger events @@ -402,28 +401,42 @@ const CollisionEvents& PhysicsEngine::getCollisionEvents() { while (contactItr != _contactMap.end()) { ContactInfo& contact = contactItr->second; ContactEventType type = contact.computeType(_numContactFrames); - if(type != CONTACT_EVENT_TYPE_CONTINUE || _numSubsteps % CONTINUE_EVENT_FILTER_FREQUENCY == 0) { + const btScalar SIGNIFICANT_DEPTH = -0.002f; // penetrations have negative distance + if (type != CONTACT_EVENT_TYPE_CONTINUE || + (contact.distance < SIGNIFICANT_DEPTH && + contact.readyForContinue(_numContactFrames))) { ObjectMotionState* motionStateA = static_cast(contactItr->first._a); ObjectMotionState* motionStateB = static_cast(contactItr->first._b); - glm::vec3 velocityChange = (motionStateA ? motionStateA->getObjectLinearVelocityChange() : glm::vec3(0.0f)) + - (motionStateB ? motionStateB->getObjectLinearVelocityChange() : glm::vec3(0.0f)); - if (motionStateA) { + // NOTE: the MyAvatar RigidBody is the only object in the simulation that does NOT have a MotionState + // which means should we ever want to report ALL collision events against the avatar we can + // modify the logic below. + // + // We only create events when at least one of the objects is (or should be) owned in the local simulation. + if (motionStateA && (motionStateA->shouldBeLocallyOwned())) { QUuid idA = motionStateA->getObjectID(); QUuid idB; if (motionStateB) { idB = motionStateB->getObjectID(); } glm::vec3 position = bulletToGLM(contact.getPositionWorldOnB()) + _originOffset; + glm::vec3 velocityChange = motionStateA->getObjectLinearVelocityChange() + + (motionStateB ? motionStateB->getObjectLinearVelocityChange() : glm::vec3(0.0f)); glm::vec3 penetration = bulletToGLM(contact.distance * contact.normalWorldOnB); _collisionEvents.push_back(Collision(type, idA, idB, position, penetration, velocityChange)); - } else if (motionStateB) { + } else if (motionStateB && (motionStateB->shouldBeLocallyOwned())) { QUuid idB = motionStateB->getObjectID(); + QUuid idA; + if (motionStateA) { + idA = motionStateA->getObjectID(); + } glm::vec3 position = bulletToGLM(contact.getPositionWorldOnA()) + _originOffset; + glm::vec3 velocityChange = motionStateB->getObjectLinearVelocityChange() + + (motionStateA ? motionStateA->getObjectLinearVelocityChange() : glm::vec3(0.0f)); // NOTE: we're flipping the order of A and B (so that the first objectID is never NULL) - // hence we must negate the penetration. + // hence we negate the penetration (because penetration always points from B to A). glm::vec3 penetration = - bulletToGLM(contact.distance * contact.normalWorldOnB); - _collisionEvents.push_back(Collision(type, idB, QUuid(), position, penetration, velocityChange)); + _collisionEvents.push_back(Collision(type, idB, idA, position, penetration, velocityChange)); } } diff --git a/libraries/render-utils/src/GeometryCache.cpp b/libraries/render-utils/src/GeometryCache.cpp index a19f1844f0..c277b9be64 100644 --- a/libraries/render-utils/src/GeometryCache.cpp +++ b/libraries/render-utils/src/GeometryCache.cpp @@ -36,7 +36,9 @@ #include "simple_textured_frag.h" #include "simple_textured_unlit_frag.h" #include "simple_opaque_web_browser_frag.h" +#include "simple_opaque_web_browser_overlay_frag.h" #include "simple_transparent_web_browser_frag.h" +#include "simple_transparent_web_browser_overlay_frag.h" #include "glowLine_vert.h" #include "glowLine_frag.h" @@ -1760,66 +1762,61 @@ inline bool operator==(const SimpleProgramKey& a, const SimpleProgramKey& b) { return a.getRaw() == b.getRaw(); } -void GeometryCache::bindOpaqueWebBrowserProgram(gpu::Batch& batch) { - batch.setPipeline(getOpaqueWebBrowserProgram()); +static void buildWebShader(const std::string& vertShaderText, const std::string& fragShaderText, bool blendEnable, + gpu::ShaderPointer& shaderPointerOut, gpu::PipelinePointer& pipelinePointerOut) { + auto VS = gpu::Shader::createVertex(vertShaderText); + auto PS = gpu::Shader::createPixel(fragShaderText); + + shaderPointerOut = gpu::Shader::createProgram(VS, PS); + + gpu::Shader::BindingSet slotBindings; + slotBindings.insert(gpu::Shader::Binding(std::string("normalFittingMap"), render::ShapePipeline::Slot::MAP::NORMAL_FITTING)); + gpu::Shader::makeProgram(*shaderPointerOut, slotBindings); + auto state = std::make_shared(); + state->setCullMode(gpu::State::CULL_NONE); + state->setDepthTest(true, true, gpu::LESS_EQUAL); + state->setBlendFunction(blendEnable, + gpu::State::SRC_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::INV_SRC_ALPHA, + gpu::State::FACTOR_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::ONE); + + pipelinePointerOut = gpu::Pipeline::create(shaderPointerOut, state); +} + +void GeometryCache::bindOpaqueWebBrowserProgram(gpu::Batch& batch, bool isAA) { + batch.setPipeline(getOpaqueWebBrowserProgram(isAA)); // Set a default normal map batch.setResourceTexture(render::ShapePipeline::Slot::MAP::NORMAL_FITTING, DependencyManager::get()->getNormalFittingTexture()); } -gpu::PipelinePointer GeometryCache::getOpaqueWebBrowserProgram() { +gpu::PipelinePointer GeometryCache::getOpaqueWebBrowserProgram(bool isAA) { static std::once_flag once; std::call_once(once, [&]() { - auto VS = gpu::Shader::createVertex(std::string(simple_vert)); - auto PS = gpu::Shader::createPixel(std::string(simple_opaque_web_browser_frag)); - - _simpleOpaqueWebBrowserShader = gpu::Shader::createProgram(VS, PS); - - gpu::Shader::BindingSet slotBindings; - slotBindings.insert(gpu::Shader::Binding(std::string("normalFittingMap"), render::ShapePipeline::Slot::MAP::NORMAL_FITTING)); - gpu::Shader::makeProgram(*_simpleOpaqueWebBrowserShader, slotBindings); - auto state = std::make_shared(); - state->setCullMode(gpu::State::CULL_NONE); - state->setDepthTest(true, true, gpu::LESS_EQUAL); - state->setBlendFunction(false, - gpu::State::SRC_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::INV_SRC_ALPHA, - gpu::State::FACTOR_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::ONE); - - _simpleOpaqueWebBrowserPipeline = gpu::Pipeline::create(_simpleOpaqueWebBrowserShader, state); + const bool BLEND_ENABLE = false; + buildWebShader(simple_vert, simple_opaque_web_browser_frag, BLEND_ENABLE, _simpleOpaqueWebBrowserShader, _simpleOpaqueWebBrowserPipeline); + buildWebShader(simple_vert, simple_opaque_web_browser_overlay_frag, BLEND_ENABLE, _simpleOpaqueWebBrowserOverlayShader, _simpleOpaqueWebBrowserOverlayPipeline); }); - return _simpleOpaqueWebBrowserPipeline; + return isAA ? _simpleOpaqueWebBrowserPipeline : _simpleOpaqueWebBrowserOverlayPipeline; } -void GeometryCache::bindTransparentWebBrowserProgram(gpu::Batch& batch) { - batch.setPipeline(getTransparentWebBrowserProgram()); +void GeometryCache::bindTransparentWebBrowserProgram(gpu::Batch& batch, bool isAA) { + batch.setPipeline(getTransparentWebBrowserProgram(isAA)); // Set a default normal map batch.setResourceTexture(render::ShapePipeline::Slot::MAP::NORMAL_FITTING, DependencyManager::get()->getNormalFittingTexture()); } -gpu::PipelinePointer GeometryCache::getTransparentWebBrowserProgram() { +gpu::PipelinePointer GeometryCache::getTransparentWebBrowserProgram(bool isAA) { static std::once_flag once; std::call_once(once, [&]() { - auto VS = gpu::Shader::createVertex(std::string(simple_vert)); - auto PS = gpu::Shader::createPixel(std::string(simple_transparent_web_browser_frag)); - _simpleTransparentWebBrowserShader = gpu::Shader::createProgram(VS, PS); - - gpu::Shader::BindingSet slotBindings; - slotBindings.insert(gpu::Shader::Binding(std::string("normalFittingMap"), render::ShapePipeline::Slot::MAP::NORMAL_FITTING)); - gpu::Shader::makeProgram(*_simpleTransparentWebBrowserShader, slotBindings); - auto state = std::make_shared(); - state->setCullMode(gpu::State::CULL_NONE); - state->setDepthTest(true, true, gpu::LESS_EQUAL); - state->setBlendFunction(true, - gpu::State::SRC_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::INV_SRC_ALPHA, - gpu::State::FACTOR_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::ONE); - - _simpleTransparentWebBrowserPipeline = gpu::Pipeline::create(_simpleTransparentWebBrowserShader, state); + const bool BLEND_ENABLE = true; + buildWebShader(simple_vert, simple_transparent_web_browser_frag, BLEND_ENABLE, _simpleTransparentWebBrowserShader, _simpleTransparentWebBrowserPipeline); + buildWebShader(simple_vert, simple_transparent_web_browser_overlay_frag, BLEND_ENABLE, _simpleTransparentWebBrowserOverlayShader, _simpleTransparentWebBrowserOverlayPipeline); }); - return _simpleTransparentWebBrowserPipeline; + return isAA ? _simpleTransparentWebBrowserPipeline : _simpleTransparentWebBrowserOverlayPipeline; } void GeometryCache::bindSimpleProgram(gpu::Batch& batch, bool textured, bool transparent, bool culled, bool unlit, bool depthBiased) { diff --git a/libraries/render-utils/src/GeometryCache.h b/libraries/render-utils/src/GeometryCache.h index 84dfd8ccc3..e0a610a095 100644 --- a/libraries/render-utils/src/GeometryCache.h +++ b/libraries/render-utils/src/GeometryCache.h @@ -158,11 +158,11 @@ public: gpu::PipelinePointer getSimplePipeline(bool textured = false, bool transparent = false, bool culled = true, bool unlit = false, bool depthBias = false); - void bindOpaqueWebBrowserProgram(gpu::Batch& batch); - gpu::PipelinePointer getOpaqueWebBrowserProgram(); + void bindOpaqueWebBrowserProgram(gpu::Batch& batch, bool isAA); + gpu::PipelinePointer getOpaqueWebBrowserProgram(bool isAA); - void bindTransparentWebBrowserProgram(gpu::Batch& batch); - gpu::PipelinePointer getTransparentWebBrowserProgram(); + void bindTransparentWebBrowserProgram(gpu::Batch& batch, bool isAA); + gpu::PipelinePointer getTransparentWebBrowserProgram(bool isAA); render::ShapePipelinePointer getOpaqueShapePipeline() { return GeometryCache::_simpleOpaquePipeline; } render::ShapePipelinePointer getTransparentShapePipeline() { return GeometryCache::_simpleTransparentPipeline; } @@ -420,15 +420,21 @@ private: gpu::ShaderPointer _unlitShader; static render::ShapePipelinePointer _simpleOpaquePipeline; static render::ShapePipelinePointer _simpleTransparentPipeline; + static render::ShapePipelinePointer _simpleOpaqueOverlayPipeline; + static render::ShapePipelinePointer _simpleTransparentOverlayPipeline; static render::ShapePipelinePointer _simpleWirePipeline; gpu::PipelinePointer _glowLinePipeline; QHash _simplePrograms; gpu::ShaderPointer _simpleOpaqueWebBrowserShader; gpu::PipelinePointer _simpleOpaqueWebBrowserPipeline; - gpu::ShaderPointer _simpleTransparentWebBrowserShader; gpu::PipelinePointer _simpleTransparentWebBrowserPipeline; + + gpu::ShaderPointer _simpleOpaqueWebBrowserOverlayShader; + gpu::PipelinePointer _simpleOpaqueWebBrowserOverlayPipeline; + gpu::ShaderPointer _simpleTransparentWebBrowserOverlayShader; + gpu::PipelinePointer _simpleTransparentWebBrowserOverlayPipeline; }; #endif // hifi_GeometryCache_h diff --git a/libraries/render-utils/src/LightingModel.cpp b/libraries/render-utils/src/LightingModel.cpp index 5a251fc5e9..47af83da36 100644 --- a/libraries/render-utils/src/LightingModel.cpp +++ b/libraries/render-utils/src/LightingModel.cpp @@ -92,6 +92,15 @@ bool LightingModel::isAlbedoEnabled() const { return (bool)_parametersBuffer.get().enableAlbedo; } +void LightingModel::setMaterialTexturing(bool enable) { + if (enable != isMaterialTexturingEnabled()) { + _parametersBuffer.edit().enableMaterialTexturing = (float)enable; + } +} +bool LightingModel::isMaterialTexturingEnabled() const { + return (bool)_parametersBuffer.get().enableMaterialTexturing; +} + void LightingModel::setAmbientLight(bool enable) { if (enable != isAmbientLightEnabled()) { _parametersBuffer.edit().enableAmbientLight = (float)enable; @@ -150,6 +159,8 @@ void MakeLightingModel::configure(const Config& config) { _lightingModel->setSpecular(config.enableSpecular); _lightingModel->setAlbedo(config.enableAlbedo); + _lightingModel->setMaterialTexturing(config.enableMaterialTexturing); + _lightingModel->setAmbientLight(config.enableAmbientLight); _lightingModel->setDirectionalLight(config.enableDirectionalLight); _lightingModel->setPointLight(config.enablePointLight); @@ -160,5 +171,8 @@ void MakeLightingModel::configure(const Config& config) { void MakeLightingModel::run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext, LightingModelPointer& lightingModel) { - lightingModel = _lightingModel; + lightingModel = _lightingModel; + + // make sure the enableTexturing flag of the render ARgs is in sync + renderContext->args->_enableTexturing = _lightingModel->isMaterialTexturingEnabled(); } \ No newline at end of file diff --git a/libraries/render-utils/src/LightingModel.h b/libraries/render-utils/src/LightingModel.h index 8f3ee9b7d6..45514654f2 100644 --- a/libraries/render-utils/src/LightingModel.h +++ b/libraries/render-utils/src/LightingModel.h @@ -49,6 +49,8 @@ public: void setAlbedo(bool enable); bool isAlbedoEnabled() const; + void setMaterialTexturing(bool enable); + bool isMaterialTexturingEnabled() const; void setAmbientLight(bool enable); bool isAmbientLightEnabled() const; @@ -88,9 +90,12 @@ protected: float enableSpotLight{ 1.0f }; float showLightContour{ 0.0f }; // false by default + float enableObscurance{ 1.0f }; - glm::vec2 spares{ 0.0f }; + float enableMaterialTexturing { 1.0f }; + + float spares{ 0.0f }; Parameters() {} }; @@ -117,6 +122,8 @@ class MakeLightingModelConfig : public render::Job::Config { Q_PROPERTY(bool enableSpecular MEMBER enableSpecular NOTIFY dirty) Q_PROPERTY(bool enableAlbedo MEMBER enableAlbedo NOTIFY dirty) + Q_PROPERTY(bool enableMaterialTexturing MEMBER enableMaterialTexturing NOTIFY dirty) + Q_PROPERTY(bool enableAmbientLight MEMBER enableAmbientLight NOTIFY dirty) Q_PROPERTY(bool enableDirectionalLight MEMBER enableDirectionalLight NOTIFY dirty) Q_PROPERTY(bool enablePointLight MEMBER enablePointLight NOTIFY dirty) @@ -136,13 +143,16 @@ public: bool enableScattering{ true }; bool enableDiffuse{ true }; bool enableSpecular{ true }; + bool enableAlbedo{ true }; + bool enableMaterialTexturing { true }; bool enableAmbientLight{ true }; bool enableDirectionalLight{ true }; bool enablePointLight{ true }; bool enableSpotLight{ true }; + bool showLightContour { false }; // false by default signals: diff --git a/libraries/render-utils/src/MeshPartPayload.cpp b/libraries/render-utils/src/MeshPartPayload.cpp index fa180a654a..4cb4e2a316 100644 --- a/libraries/render-utils/src/MeshPartPayload.cpp +++ b/libraries/render-utils/src/MeshPartPayload.cpp @@ -129,7 +129,7 @@ void MeshPartPayload::bindMesh(gpu::Batch& batch) const { } } -void MeshPartPayload::bindMaterial(gpu::Batch& batch, const ShapePipeline::LocationsPointer locations) const { +void MeshPartPayload::bindMaterial(gpu::Batch& batch, const ShapePipeline::LocationsPointer locations, bool enableTextures) const { if (!_drawMaterial) { return; } @@ -147,6 +147,17 @@ void MeshPartPayload::bindMaterial(gpu::Batch& batch, const ShapePipeline::Locat numUnlit++; } + if (!enableTextures) { + batch.setResourceTexture(ShapePipeline::Slot::ALBEDO, textureCache->getWhiteTexture()); + batch.setResourceTexture(ShapePipeline::Slot::MAP::ROUGHNESS, textureCache->getWhiteTexture()); + batch.setResourceTexture(ShapePipeline::Slot::MAP::NORMAL, textureCache->getBlueTexture()); + batch.setResourceTexture(ShapePipeline::Slot::MAP::METALLIC, textureCache->getBlackTexture()); + batch.setResourceTexture(ShapePipeline::Slot::MAP::OCCLUSION, textureCache->getWhiteTexture()); + batch.setResourceTexture(ShapePipeline::Slot::MAP::SCATTERING, textureCache->getWhiteTexture()); + batch.setResourceTexture(ShapePipeline::Slot::MAP::EMISSIVE_LIGHTMAP, textureCache->getBlackTexture()); + return; + } + // Albedo if (materialKey.isAlbedoMap()) { auto itr = textureMaps.find(model::MaterialKey::ALBEDO_MAP); @@ -271,7 +282,7 @@ void MeshPartPayload::render(RenderArgs* args) const { bindMesh(batch); // apply material properties - bindMaterial(batch, locations); + bindMaterial(batch, locations, args->_enableTexturing); if (args) { args->_details._materialSwitches++; @@ -363,12 +374,7 @@ void ModelMeshPartPayload::updateTransformForSkinnedMesh(const Transform& transf _transform = transform; if (clusterMatrices.size() > 0) { - _worldBound = AABox(); - for (auto& clusterMatrix : clusterMatrices) { - AABox clusterBound = _localBound; - clusterBound.transform(clusterMatrix); - _worldBound += clusterBound; - } + _worldBound = _adjustedLocalBound; _worldBound.transform(_transform); if (clusterMatrices.size() == 1) { _transform = _transform.worldTransform(Transform(clusterMatrices[0])); @@ -588,7 +594,7 @@ void ModelMeshPartPayload::render(RenderArgs* args) const { bindMesh(batch); // apply material properties - bindMaterial(batch, locations); + bindMaterial(batch, locations, args->_enableTexturing); args->_details._materialSwitches++; @@ -601,3 +607,15 @@ void ModelMeshPartPayload::render(RenderArgs* args) const { const int INDICES_PER_TRIANGLE = 3; args->_details._trianglesRendered += _drawPart._numIndices / INDICES_PER_TRIANGLE; } + +void ModelMeshPartPayload::computeAdjustedLocalBound(const QVector& clusterMatrices) { + _adjustedLocalBound = _localBound; + if (clusterMatrices.size() > 0) { + _adjustedLocalBound.transform(clusterMatrices[0]); + for (int i = 1; i < clusterMatrices.size(); ++i) { + AABox clusterBound = _localBound; + clusterBound.transform(clusterMatrices[i]); + _adjustedLocalBound += clusterBound; + } + } +} diff --git a/libraries/render-utils/src/MeshPartPayload.h b/libraries/render-utils/src/MeshPartPayload.h index 7d0aeab2bd..c585c95025 100644 --- a/libraries/render-utils/src/MeshPartPayload.h +++ b/libraries/render-utils/src/MeshPartPayload.h @@ -1,5 +1,5 @@ // -// ModelMeshPartPayload.h +// MeshPartPayload.h // interface/src/renderer // // Created by Sam Gateau on 10/3/15. @@ -51,7 +51,7 @@ public: // ModelMeshPartPayload functions to perform render void drawCall(gpu::Batch& batch) const; virtual void bindMesh(gpu::Batch& batch) const; - virtual void bindMaterial(gpu::Batch& batch, const render::ShapePipeline::LocationsPointer locations) const; + virtual void bindMaterial(gpu::Batch& batch, const render::ShapePipeline::LocationsPointer locations, bool enableTextures) const; virtual void bindTransform(gpu::Batch& batch, const render::ShapePipeline::LocationsPointer locations, RenderArgs::RenderMode renderMode) const; // Payload resource cached values @@ -61,6 +61,7 @@ public: bool _hasColorAttrib { false }; model::Box _localBound; + model::Box _adjustedLocalBound; mutable model::Box _worldBound; std::shared_ptr _drawMesh; @@ -105,6 +106,8 @@ public: void initCache(); + void computeAdjustedLocalBound(const QVector& clusterMatrices); + Model* _model; int _meshIndex; diff --git a/libraries/render-utils/src/Model.cpp b/libraries/render-utils/src/Model.cpp index 1e83a874dc..018a7e6954 100644 --- a/libraries/render-utils/src/Model.cpp +++ b/libraries/render-utils/src/Model.cpp @@ -133,6 +133,29 @@ void Model::setRotation(const glm::quat& rotation) { updateRenderItems(); } +void Model::setSpatiallyNestableOverride(SpatiallyNestablePointer override) { + _spatiallyNestableOverride = override; + updateRenderItems(); +} + +Transform Model::getTransform() const { + SpatiallyNestablePointer spatiallyNestableOverride = _spatiallyNestableOverride.lock(); + if (spatiallyNestableOverride) { + bool success; + Transform transform = spatiallyNestableOverride->getTransform(success); + if (success) { + transform.setScale(getScale()); + return transform; + } + } + + Transform transform; + transform.setScale(getScale()); + transform.setTranslation(getTranslation()); + transform.setRotation(getRotation()); + return transform; +} + void Model::setScale(const glm::vec3& scale) { setScaleInternal(scale); // if anyone sets scale manually, then we are no longer scaled to fit @@ -214,21 +237,17 @@ void Model::updateRenderItems() { render::ScenePointer scene = AbstractViewStateInterface::instance()->getMain3DScene(); - Transform modelTransform; - modelTransform.setTranslation(self->_translation); - modelTransform.setRotation(self->_rotation); - - Transform scaledModelTransform(modelTransform); - scaledModelTransform.setScale(scale); - uint32_t deleteGeometryCounter = self->_deleteGeometryCounter; render::PendingChanges pendingChanges; foreach (auto itemID, self->_modelMeshRenderItems.keys()) { - pendingChanges.updateItem(itemID, [modelTransform, deleteGeometryCounter](ModelMeshPartPayload& data) { + pendingChanges.updateItem(itemID, [deleteGeometryCounter](ModelMeshPartPayload& data) { if (data._model && data._model->isLoaded()) { // Ensure the model geometry was not reset between frames if (deleteGeometryCounter == data._model->_deleteGeometryCounter) { + Transform modelTransform = data._model->getTransform(); + modelTransform.setScale(glm::vec3(1.0f)); + // lazy update of cluster matrices used for rendering. We need to update them here, so we can correctly update the bounding box. data._model->updateClusterMatrices(); @@ -243,10 +262,11 @@ void Model::updateRenderItems() { // collision mesh does not share the same unit scale as the FBX file's mesh: only apply offset Transform collisionMeshOffset; collisionMeshOffset.setIdentity(); + Transform modelTransform = self->getTransform(); foreach (auto itemID, self->_collisionRenderItems.keys()) { - pendingChanges.updateItem(itemID, [scaledModelTransform, collisionMeshOffset](MeshPartPayload& data) { + pendingChanges.updateItem(itemID, [modelTransform, collisionMeshOffset](MeshPartPayload& data) { // update the model transform for this render item. - data.updateTransform(scaledModelTransform, collisionMeshOffset); + data.updateTransform(modelTransform, collisionMeshOffset); }); } @@ -1129,6 +1149,8 @@ void Model::simulate(float deltaTime, bool fullUpdate) { // update the world space transforms for all joints glm::mat4 parentTransform = glm::scale(_scale) * glm::translate(_offset); updateRig(deltaTime, parentTransform); + + computeMeshPartLocalBounds(); } } @@ -1138,6 +1160,14 @@ void Model::updateRig(float deltaTime, glm::mat4 parentTransform) { _rig->updateAnimations(deltaTime, parentTransform); } +void Model::computeMeshPartLocalBounds() { + for (auto& part : _modelMeshRenderItemsSet) { + assert(part->_meshIndex < _modelMeshRenderItemsSet.size()); + const Model::MeshState& state = _meshStates.at(part->_meshIndex); + part->computeAdjustedLocalBound(state.clusterMatrices); + } +} + // virtual void Model::updateClusterMatrices() { PerformanceTimer perfTimer("Model::updateClusterMatrices"); @@ -1314,6 +1344,7 @@ void Model::createVisibleRenderItemSet() { shapeID++; } } + computeMeshPartLocalBounds(); } void Model::createCollisionRenderItemSet() { diff --git a/libraries/render-utils/src/Model.h b/libraries/render-utils/src/Model.h index 3f673b0250..49890bfb04 100644 --- a/libraries/render-utils/src/Model.h +++ b/libraries/render-utils/src/Model.h @@ -27,6 +27,7 @@ #include #include #include +#include #include "GeometryCache.h" #include "TextureCache.h" @@ -204,10 +205,13 @@ public: void setTranslation(const glm::vec3& translation); void setRotation(const glm::quat& rotation); + void setSpatiallyNestableOverride(SpatiallyNestablePointer ptr); const glm::vec3& getTranslation() const { return _translation; } const glm::quat& getRotation() const { return _rotation; } + Transform getTransform() const; + void setScale(const glm::vec3& scale); const glm::vec3& getScale() const { return _scale; } @@ -240,7 +244,6 @@ public: public: QVector clusterMatrices; gpu::BufferPointer clusterBuffer; - }; const MeshState& getMeshState(int index) { return _meshStates.at(index); } @@ -292,6 +295,9 @@ protected: glm::vec3 _translation; glm::quat _rotation; glm::vec3 _scale; + + SpatiallyNestableWeakPointer _spatiallyNestableOverride; + glm::vec3 _offset; static float FAKE_DIMENSION_PLACEHOLDER; @@ -312,6 +318,7 @@ protected: void scaleToFit(); void snapToRegistrationPoint(); + void computeMeshPartLocalBounds(); virtual void updateRig(float deltaTime, glm::mat4 parentTransform); /// Restores the indexed joint to its default position. diff --git a/libraries/render-utils/src/RenderDeferredTask.cpp b/libraries/render-utils/src/RenderDeferredTask.cpp index e0192b5f85..55a9c8b9e4 100644 --- a/libraries/render-utils/src/RenderDeferredTask.cpp +++ b/libraries/render-utils/src/RenderDeferredTask.cpp @@ -63,6 +63,10 @@ RenderDeferredTask::RenderDeferredTask(RenderFetchCullSortTask::Output items) { const auto background = items[RenderFetchCullSortTask::BACKGROUND]; const auto spatialSelection = items[RenderFetchCullSortTask::SPATIAL_SELECTION]; + // Filter the non antialiaased overlays + const int LAYER_NO_AA = 3; + const auto nonAAOverlays = addJob("Filter2DWebOverlays", overlayOpaques, LAYER_NO_AA); + // Prepare deferred, generate the shared Deferred Frame Transform const auto deferredFrameTransform = addJob("DeferredFrameTransform"); const auto lightingModel = addJob("LightingModel"); @@ -195,6 +199,10 @@ RenderDeferredTask::RenderDeferredTask(RenderFetchCullSortTask::Output items) { // AA job to be revisited addJob("Antialiasing", primaryFramebuffer); + // Draw 2DWeb non AA + const auto nonAAOverlaysInputs = DrawOverlay3D::Inputs(nonAAOverlays, lightingModel).hasVarying(); + addJob("Draw2DWebSurfaces", nonAAOverlaysInputs, false); + addJob("ToneAndPostRangeTimer", toneAndPostRangeTimer); // Blit! diff --git a/libraries/render-utils/src/simple_opaque_web_browser.slf b/libraries/render-utils/src/simple_opaque_web_browser.slf index 2921d6aea0..3acf104b55 100644 --- a/libraries/render-utils/src/simple_opaque_web_browser.slf +++ b/libraries/render-utils/src/simple_opaque_web_browser.slf @@ -2,7 +2,7 @@ <$VERSION_HEADER$> // Generated on <$_SCRIBE_DATE$> // -// simple_opaque_web_browser.frag +// simple_opaque_web_browser.slf // fragment shader // // Created by Anthony Thibault on 7/25/16. diff --git a/libraries/render-utils/src/simple_opaque_web_browser_overlay.slf b/libraries/render-utils/src/simple_opaque_web_browser_overlay.slf new file mode 100644 index 0000000000..6d4d35591f --- /dev/null +++ b/libraries/render-utils/src/simple_opaque_web_browser_overlay.slf @@ -0,0 +1,30 @@ +<@include gpu/Config.slh@> +<$VERSION_HEADER$> +// Generated on <$_SCRIBE_DATE$> +// +// simple_opaque_web_browser_overlay.slf +// fragment shader +// +// Created by Anthony Thibault on 1/30/17. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// +// Same as simple_opaque_web_browser.slf except frame buffer is sRGB, so colorToLinearRGBA is not necessary. + +<@include gpu/Color.slh@> +<@include DeferredBufferWrite.slh@> + +// the albedo texture +uniform sampler2D originalTexture; + +// the interpolated normal +in vec3 _normal; +in vec4 _color; +in vec2 _texCoord0; + +void main(void) { + vec4 texel = texture(originalTexture, _texCoord0.st); + _fragColor0 = vec4(_color.rgb * texel.rgb, 1.0); +} diff --git a/libraries/render-utils/src/simple_transparent_web_browser.slf b/libraries/render-utils/src/simple_transparent_web_browser.slf index b7606985e6..19079f5d92 100644 --- a/libraries/render-utils/src/simple_transparent_web_browser.slf +++ b/libraries/render-utils/src/simple_transparent_web_browser.slf @@ -2,7 +2,7 @@ <$VERSION_HEADER$> // Generated on <$_SCRIBE_DATE$> // -// simple_transparent_web_browser.frag +// simple_transparent_web_browser.slf // fragment shader // // Created by Anthony Thibault on 7/25/16. @@ -33,4 +33,3 @@ void main(void) { DEFAULT_FRESNEL, DEFAULT_ROUGHNESS); } - diff --git a/libraries/render-utils/src/simple_transparent_web_browser_overlay.slf b/libraries/render-utils/src/simple_transparent_web_browser_overlay.slf new file mode 100644 index 0000000000..af52389d5b --- /dev/null +++ b/libraries/render-utils/src/simple_transparent_web_browser_overlay.slf @@ -0,0 +1,31 @@ +<@include gpu/Config.slh@> +<$VERSION_HEADER$> +// Generated on <$_SCRIBE_DATE$> +// +// simple_transparent_web_browser_overlay.slf +// fragment shader +// +// Created by Anthony Thibault on 1/30/17. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// +// Same as simple_transparent_web_browser.slf except frame buffer is sRGB, So colorToLinearRGBA is not necessary. +// + +<@include gpu/Color.slh@> +<@include DeferredBufferWrite.slh@> + +// the albedo texture +uniform sampler2D originalTexture; + +// the interpolated normal +in vec3 _normal; +in vec4 _color; +in vec2 _texCoord0; + +void main(void) { + vec4 texel = texture(originalTexture, _texCoord0.st); + _fragColor0 = vec4(_color.rgb * texel.rgb, _color.a); +} diff --git a/libraries/render/src/render/CullTask.cpp b/libraries/render/src/render/CullTask.cpp index e27895352f..42f95f458f 100644 --- a/libraries/render/src/render/CullTask.cpp +++ b/libraries/render/src/render/CullTask.cpp @@ -306,3 +306,19 @@ void CullSpatialSelection::run(const SceneContextPointer& sceneContext, const Re std::static_pointer_cast(renderContext->jobConfig)->numItems = (int)outItems.size(); } + + +void FilterItemLayer::run(const SceneContextPointer& sceneContext, const RenderContextPointer& renderContext, const ItemBounds& inItems, ItemBounds& outItems) { + auto& scene = sceneContext->_scene; + + // Clear previous values + outItems.clear(); + + // For each item, filter it into one bucket + for (auto itemBound : inItems) { + auto& item = scene->getItem(itemBound.id); + if (item.getLayer() == _keepLayer) { + outItems.emplace_back(itemBound); + } + } +} \ No newline at end of file diff --git a/libraries/render/src/render/CullTask.h b/libraries/render/src/render/CullTask.h index 1a709ed102..f613faa2e6 100644 --- a/libraries/render/src/render/CullTask.h +++ b/libraries/render/src/render/CullTask.h @@ -175,6 +175,19 @@ namespace render { } } }; + + class FilterItemLayer { + public: + using JobModel = Job::ModelIO; + + FilterItemLayer() {} + FilterItemLayer(int keepLayer) : + _keepLayer(keepLayer) {} + + int _keepLayer { 0 }; + + void run(const SceneContextPointer& sceneContext, const RenderContextPointer& renderContext, const ItemBounds& inItems, ItemBounds& outItems); + }; } #endif // hifi_render_CullTask_h; \ No newline at end of file diff --git a/libraries/script-engine/src/ScriptEngine.cpp b/libraries/script-engine/src/ScriptEngine.cpp index b823ce858b..2191d45d45 100644 --- a/libraries/script-engine/src/ScriptEngine.cpp +++ b/libraries/script-engine/src/ScriptEngine.cpp @@ -62,6 +62,7 @@ #include "WebSocketClass.h" #include "RecordingScriptingInterface.h" #include "ScriptEngines.h" +#include "TabletScriptingInterface.h" #include "MIDIEvent.h" @@ -582,6 +583,7 @@ void ScriptEngine::init() { // constants globalObject().setProperty("TREE_SCALE", newVariant(QVariant(TREE_SCALE))); + registerGlobalObject("Tablet", DependencyManager::get().data()); registerGlobalObject("Assets", &_assetScriptingInterface); registerGlobalObject("Resources", DependencyManager::get().data()); } diff --git a/libraries/script-engine/src/SoundEffect.cpp b/libraries/script-engine/src/SoundEffect.cpp new file mode 100644 index 0000000000..6833bb1f31 --- /dev/null +++ b/libraries/script-engine/src/SoundEffect.cpp @@ -0,0 +1,39 @@ + +#include "SoundEffect.h" + +#include +#include + +SoundEffect::~SoundEffect() { + if (_sound) { + _sound->deleteLater(); + } + if (_injector) { + // stop will cause the AudioInjector to delete itself. + _injector->stop(); + } +} + +QUrl SoundEffect::getSource() const { + return _url; +} + +void SoundEffect::setSource(QUrl url) { + _url = url; + _sound = DependencyManager::get()->getSound(_url); +} + +void SoundEffect::play(QVariant position) { + AudioInjectorOptions options; + options.position = vec3FromVariant(position); + options.localOnly = true; + if (_injector) { + _injector->setOptions(options); + _injector->restart(); + } else { + QByteArray samples = _sound->getByteArray(); + _injector = AudioInjector::playSound(samples, options); + } +} + +#include "SoundEffect.moc" diff --git a/libraries/script-engine/src/SoundEffect.h b/libraries/script-engine/src/SoundEffect.h new file mode 100644 index 0000000000..5d2a5095c1 --- /dev/null +++ b/libraries/script-engine/src/SoundEffect.h @@ -0,0 +1,39 @@ +// +// Created by Anthony J. Thibault on 2017-01-30 +// Copyright 2013-2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_SoundEffect_h +#define hifi_SoundEffect_h + +#include +#include + +#include + +class AudioInjector; + +// SoundEffect object, exposed to qml only, not interface JavaScript. +// This is used to play spatial sound effects on tablets/web entities from within QML. + +class SoundEffect : public QQuickItem { + Q_OBJECT + Q_PROPERTY(QUrl source READ getSource WRITE setSource) +public: + + virtual ~SoundEffect(); + + QUrl getSource() const; + void setSource(QUrl url); + + Q_INVOKABLE void play(QVariant position); +protected: + QUrl _url; + SharedSoundPointer _sound; + AudioInjector* _injector { nullptr }; +}; + +#endif // hifi_SoundEffect_h diff --git a/libraries/script-engine/src/TabletScriptingInterface.cpp b/libraries/script-engine/src/TabletScriptingInterface.cpp new file mode 100644 index 0000000000..d73cb980f6 --- /dev/null +++ b/libraries/script-engine/src/TabletScriptingInterface.cpp @@ -0,0 +1,420 @@ +// +// Created by Anthony J. Thibault on 2016-12-12 +// Copyright 2013-2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include "TabletScriptingInterface.h" + +#include + +#include +#include +#include +#include "ScriptEngineLogging.h" +#include "DependencyManager.h" +#include "OffscreenUi.h" +#include "SoundEffect.h" + +TabletScriptingInterface::TabletScriptingInterface() { + qmlRegisterType("Hifi", 1, 0, "SoundEffect"); +} + +QObject* TabletScriptingInterface::getTablet(const QString& tabletId) { + + std::lock_guard guard(_mutex); + + // look up tabletId in the map. + auto iter = _tabletProxies.find(tabletId); + if (iter != _tabletProxies.end()) { + // tablet already exists, just return it. + return iter->second.data(); + } else { + // allocate a new tablet, add it to the map then return it. + auto tabletProxy = QSharedPointer(new TabletProxy(tabletId)); + _tabletProxies[tabletId] = tabletProxy; + return tabletProxy.data(); + } +} + +void TabletScriptingInterface::setQmlTabletRoot(QString tabletId, QQuickItem* qmlTabletRoot, QObject* qmlOffscreenSurface) { + TabletProxy* tablet = qobject_cast(getTablet(tabletId)); + if (tablet) { + tablet->setQmlTabletRoot(qmlTabletRoot, qmlOffscreenSurface); + } else { + qCWarning(scriptengine) << "TabletScriptingInterface::setupTablet() bad tablet object"; + } +} + +QQuickWindow* TabletScriptingInterface::getTabletWindow() { + TabletProxy* tablet = qobject_cast(getTablet("com.highfidelity.interface.tablet.system")); + QObject* qmlSurface = tablet->getTabletSurface(); + OffscreenQmlSurface* surface = dynamic_cast(qmlSurface); + + if (!surface) { + return nullptr; + } + QQuickWindow* window = surface->getWindow(); + return window; +} + +void TabletScriptingInterface::processMenuEvents(QObject* object, const QKeyEvent* event) { + switch (event->key()) { + case Qt::Key_Down: + QMetaObject::invokeMethod(object, "nextItem"); + break; + + case Qt::Key_Up: + QMetaObject::invokeMethod(object, "previousItem"); + break; + + case Qt::Key_Left: + QMetaObject::invokeMethod(object, "previousPage"); + break; + + case Qt::Key_Right: + QMetaObject::invokeMethod(object, "selectCurrentItem"); + break; + + case Qt::Key_Return: + QMetaObject::invokeMethod(object, "selectCurrentItem"); + break; + + default: + break; + } +} + +void TabletScriptingInterface::processTabletEvents(QObject* object, const QKeyEvent* event) { + switch (event->key()) { + case Qt::Key_Down: + QMetaObject::invokeMethod(object, "downItem"); + break; + + case Qt::Key_Up: + QMetaObject::invokeMethod(object, "upItem"); + break; + + case Qt::Key_Left: + QMetaObject::invokeMethod(object, "previousItem"); + break; + + case Qt::Key_Right: + QMetaObject::invokeMethod(object, "nextItem"); + break; + + case Qt::Key_Return: + QMetaObject::invokeMethod(object, "selectItem"); + break; + + default: + break; + } +} + + +void TabletScriptingInterface::processEvent(const QKeyEvent* event) { + TabletProxy* tablet = qobject_cast(getTablet("com.highfidelity.interface.tablet.system")); + QObject* qmlTablet = tablet->getQmlTablet(); + QObject* qmlMenu = tablet->getQmlMenu(); + + if (qmlTablet) { + processTabletEvents(qmlTablet, event); + } else if (qmlMenu) { + processMenuEvents(qmlMenu, event); + } +} + +QObject* TabletScriptingInterface::getFlags() +{ + auto offscreenUi = DependencyManager::get(); + return offscreenUi->getFlags(); +} + +// +// TabletProxy +// + +static const char* TABLET_SOURCE_URL = "Tablet.qml"; +static const char* WEB_VIEW_SOURCE_URL = "TabletWebView.qml"; +static const char* VRMENU_SOURCE_URL = "TabletMenu.qml"; + +TabletProxy::TabletProxy(QString name) : _name(name) { + ; +} + +static void addButtonProxyToQmlTablet(QQuickItem* qmlTablet, TabletButtonProxy* buttonProxy) { + QVariant resultVar; + Qt::ConnectionType connectionType = Qt::AutoConnection; + if (QThread::currentThread() != qmlTablet->thread()) { + connectionType = Qt::BlockingQueuedConnection; + } + bool hasResult = QMetaObject::invokeMethod(qmlTablet, "addButtonProxy", connectionType, + Q_RETURN_ARG(QVariant, resultVar), Q_ARG(QVariant, buttonProxy->getProperties())); + if (!hasResult) { + qCWarning(scriptengine) << "TabletScriptingInterface addButtonProxyToQmlTablet has no result"; + return; + } + + QObject* qmlButton = qvariant_cast(resultVar); + if (!qmlButton) { + qCWarning(scriptengine) << "TabletScriptingInterface addButtonProxyToQmlTablet result not a QObject"; + return; + } + QObject::connect(qmlButton, SIGNAL(clicked()), buttonProxy, SLOT(clickedSlot())); + buttonProxy->setQmlButton(qobject_cast(qmlButton)); +} + +static QString getUsername() { + QString username = "Unknown user"; + auto accountManager = DependencyManager::get(); + if (accountManager->isLoggedIn()) { + return accountManager->getAccountInfo().getUsername(); + } else { + return "Unknown user"; + } +} + +void TabletProxy::setQmlTabletRoot(QQuickItem* qmlTabletRoot, QObject* qmlOffscreenSurface) { + std::lock_guard guard(_mutex); + _qmlOffscreenSurface = qmlOffscreenSurface; + _qmlTabletRoot = qmlTabletRoot; + if (_qmlTabletRoot && _qmlOffscreenSurface) { + QObject::connect(_qmlOffscreenSurface, SIGNAL(webEventReceived(QVariant)), this, SIGNAL(webEventReceived(QVariant))); + gotoHomeScreen(); + + QMetaObject::invokeMethod(_qmlTabletRoot, "setUsername", Q_ARG(const QVariant&, QVariant(getUsername()))); + + // hook up username changed signal. + auto accountManager = DependencyManager::get(); + QObject::connect(accountManager.data(), &AccountManager::profileChanged, [this]() { + if (_qmlTabletRoot) { + QMetaObject::invokeMethod(_qmlTabletRoot, "setUsername", Q_ARG(const QVariant&, QVariant(getUsername()))); + } + }); + } else { + removeButtonsFromHomeScreen(); + _state = State::Uninitialized; + } +} + +void TabletProxy::gotoMenuScreen() { + if (_qmlTabletRoot) { + if (_state != State::Menu) { + removeButtonsFromHomeScreen(); + auto loader = _qmlTabletRoot->findChild("loader"); + QObject::connect(loader, SIGNAL(loaded()), this, SLOT(addButtonsToMenuScreen()), Qt::DirectConnection); + QMetaObject::invokeMethod(_qmlTabletRoot, "loadSource", Q_ARG(const QVariant&, QVariant(VRMENU_SOURCE_URL))); + _state = State::Menu; + } + } +} + +void TabletProxy::gotoHomeScreen() { + if (_qmlTabletRoot) { + if (_state != State::Home) { + auto loader = _qmlTabletRoot->findChild("loader"); + QObject::connect(loader, SIGNAL(loaded()), this, SLOT(addButtonsToHomeScreen()), Qt::DirectConnection); + QMetaObject::invokeMethod(_qmlTabletRoot, "loadSource", Q_ARG(const QVariant&, QVariant(TABLET_SOURCE_URL))); + QMetaObject::invokeMethod(_qmlTabletRoot, "playButtonClickSound"); + _state = State::Home; + } + } +} + +void TabletProxy::gotoWebScreen(const QString& url) { + gotoWebScreen(url, ""); +} + +void TabletProxy::gotoWebScreen(const QString& url, const QString& injectedJavaScriptUrl) { + if (_qmlTabletRoot) { + if (_state == State::Home) { + removeButtonsFromHomeScreen(); + } + if (_state != State::Web) { + QMetaObject::invokeMethod(_qmlTabletRoot, "loadSource", Q_ARG(const QVariant&, QVariant(WEB_VIEW_SOURCE_URL))); + _state = State::Web; + } + QMetaObject::invokeMethod(_qmlTabletRoot, "loadWebUrl", Q_ARG(const QVariant&, QVariant(url)), + Q_ARG(const QVariant&, QVariant(injectedJavaScriptUrl))); + } +} + +QObject* TabletProxy::addButton(const QVariant& properties) { + auto tabletButtonProxy = QSharedPointer(new TabletButtonProxy(properties.toMap())); + std::lock_guard guard(_mutex); + _tabletButtonProxies.push_back(tabletButtonProxy); + if (_qmlTabletRoot) { + auto tablet = getQmlTablet(); + if (tablet) { + addButtonProxyToQmlTablet(tablet, tabletButtonProxy.data()); + } else { + qCCritical(scriptengine) << "Could not find tablet in TabletRoot.qml"; + } + } + return tabletButtonProxy.data(); +} + +bool TabletProxy::onHomeScreen() { + return _state == State::Home; +} + +void TabletProxy::removeButton(QObject* tabletButtonProxy) { + std::lock_guard guard(_mutex); + + auto tablet = getQmlTablet(); + if (!tablet) { + qCCritical(scriptengine) << "Could not find tablet in TabletRoot.qml"; + } + + auto iter = std::find(_tabletButtonProxies.begin(), _tabletButtonProxies.end(), tabletButtonProxy); + if (iter != _tabletButtonProxies.end()) { + if (_qmlTabletRoot) { + (*iter)->setQmlButton(nullptr); + if (tablet) { + QMetaObject::invokeMethod(tablet, "removeButtonProxy", Qt::AutoConnection, Q_ARG(QVariant, (*iter)->getProperties())); + } + } + _tabletButtonProxies.erase(iter); + } else { + qCWarning(scriptengine) << "TabletProxy::removeButton() could not find button " << tabletButtonProxy; + } +} + +void TabletProxy::updateAudioBar(const double micLevel) { + auto tablet = getQmlTablet(); + if (!tablet) { + //qCCritical(scriptengine) << "Could not find tablet in TabletRoot.qml"; + } else { + QMetaObject::invokeMethod(tablet, "setMicLevel", Qt::AutoConnection, Q_ARG(QVariant, QVariant(micLevel))); + } +} + +void TabletProxy::emitScriptEvent(QVariant msg) { + if (_qmlOffscreenSurface) { + QMetaObject::invokeMethod(_qmlOffscreenSurface, "emitScriptEvent", Qt::AutoConnection, Q_ARG(QVariant, msg)); + } +} + +void TabletProxy::addButtonsToHomeScreen() { + auto tablet = getQmlTablet(); + if (!tablet) { + return; + } + + auto tabletScriptingInterface = DependencyManager::get(); + for (auto& buttonProxy : _tabletButtonProxies) { + addButtonProxyToQmlTablet(tablet, buttonProxy.data()); + } + auto loader = _qmlTabletRoot->findChild("loader"); + QObject::disconnect(loader, SIGNAL(loaded()), this, SLOT(addButtonsToHomeScreen())); +} + +QObject* TabletProxy::getTabletSurface() { + return _qmlOffscreenSurface; +} + +void TabletProxy::addButtonsToMenuScreen() { + if (!_qmlTabletRoot) { + return; + } + + auto loader = _qmlTabletRoot->findChild("loader"); + if (!loader) { + return; + } + + QQuickItem* VrMenu = loader->findChild("tabletMenu"); + if (!VrMenu) { + return; + } + + auto offscreenUi = DependencyManager::get(); + QObject* menu = offscreenUi->getRootMenu(); + QMetaObject::invokeMethod(VrMenu, "setRootMenu", Qt::AutoConnection, Q_ARG(QVariant, QVariant::fromValue(menu))); +} + +void TabletProxy::removeButtonsFromHomeScreen() { + auto tabletScriptingInterface = DependencyManager::get(); + for (auto& buttonProxy : _tabletButtonProxies) { + buttonProxy->setQmlButton(nullptr); + } +} + +QQuickItem* TabletProxy::getQmlTablet() const { + if (!_qmlTabletRoot) { + return nullptr; + } + + auto loader = _qmlTabletRoot->findChild("loader"); + if (!loader) { + return nullptr; + } + + auto tablet = loader->findChild("tablet"); + if (!tablet) { + return nullptr; + } + + return tablet; +} + +QQuickItem* TabletProxy::getQmlMenu() const { + if (!_qmlTabletRoot) { + return nullptr; + } + + auto loader = _qmlTabletRoot->findChild("loader"); + if (!loader) { + return nullptr; + } + + QQuickItem* VrMenu = loader->findChild("tabletMenu"); + if (!VrMenu) { + return nullptr; + } + + QQuickItem* menuList = VrMenu->findChild("tabletMenuHandlerItem"); + if (!menuList) { + return nullptr; + } + return menuList; +} + +// +// TabletButtonProxy +// + +const QString UUID_KEY = "uuid"; + +TabletButtonProxy::TabletButtonProxy(const QVariantMap& properties) : _uuid(QUuid::createUuid()), _properties(properties) { + // this is used to uniquely identify this button. + _properties[UUID_KEY] = _uuid; +} + +void TabletButtonProxy::setQmlButton(QQuickItem* qmlButton) { + std::lock_guard guard(_mutex); + _qmlButton = qmlButton; +} + +QVariantMap TabletButtonProxy::getProperties() const { + std::lock_guard guard(_mutex); + return _properties; +} + +void TabletButtonProxy::editProperties(QVariantMap properties) { + std::lock_guard guard(_mutex); + QVariantMap::const_iterator iter = properties.constBegin(); + while (iter != properties.constEnd()) { + _properties[iter.key()] = iter.value(); + if (_qmlButton) { + QMetaObject::invokeMethod(_qmlButton, "changeProperty", Qt::AutoConnection, Q_ARG(QVariant, QVariant(iter.key())), Q_ARG(QVariant, iter.value())); + } + ++iter; + } +} + +#include "TabletScriptingInterface.moc" + diff --git a/libraries/script-engine/src/TabletScriptingInterface.h b/libraries/script-engine/src/TabletScriptingInterface.h new file mode 100644 index 0000000000..0b7829c7fb --- /dev/null +++ b/libraries/script-engine/src/TabletScriptingInterface.h @@ -0,0 +1,211 @@ +// +// Created by Anthony J. Thibault on 2016-12-12 +// Copyright 2013-2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_TabletScriptingInterface_h +#define hifi_TabletScriptingInterface_h + +#include +#include + +#include +#include +#include +#include +#include + +#include +#include +#include + +#include + +class TabletProxy; +class TabletButtonProxy; + +/**jsdoc + * @namespace Tablet + */ +class TabletScriptingInterface : public QObject, public Dependency { + Q_OBJECT +public: + TabletScriptingInterface(); + + /**jsdoc + * Creates or retruns a new TabletProxy and returns it. + * @function Tablet.getTablet + * @param name {String} tablet name + * @return {TabletProxy} tablet instance + */ + Q_INVOKABLE QObject* getTablet(const QString& tabletId); + + void setQmlTabletRoot(QString tabletId, QQuickItem* qmlTabletRoot, QObject* qmlOffscreenSurface); + + void processEvent(const QKeyEvent* event); + + QQuickWindow* getTabletWindow(); + + QObject* getFlags(); + +private: + void processMenuEvents(QObject* object, const QKeyEvent* event); + void processTabletEvents(QObject* object, const QKeyEvent* event); + +protected: + std::mutex _mutex; + std::map> _tabletProxies; +}; + +/**jsdoc + * @class TabletProxy + * @property name {string} READ_ONLY: name of this tablet + */ +class TabletProxy : public QObject { + Q_OBJECT + Q_PROPERTY(QString name READ getName) +public: + TabletProxy(QString name); + + void setQmlTabletRoot(QQuickItem* qmlTabletRoot, QObject* qmlOffscreenSurface); + + Q_INVOKABLE void gotoMenuScreen(); + + /**jsdoc + * transition to the home screen + * @function TabletProxy#gotoHomeScreen + */ + Q_INVOKABLE void gotoHomeScreen(); + + /**jsdoc + * show the specified web url on the tablet. + * @function TabletProxy#gotoWebScreen + * @param url {string} url of web page. + * @param [injectedJavaScriptUrl] {string} optional url to an additional JS script to inject into the web page. + */ + Q_INVOKABLE void gotoWebScreen(const QString& url); + Q_INVOKABLE void gotoWebScreen(const QString& url, const QString& injectedJavaScriptUrl); + + /**jsdoc + * Creates a new button, adds it to this and returns it. + * @function TabletProxy#addButton + * @param properties {Object} button properties UI_TABLET_HACK: enumerate these when we figure out what they should be! + * @returns {TabletButtonProxy} + */ + Q_INVOKABLE QObject* addButton(const QVariant& properties); + + /**jsdoc + * removes button from the tablet + * @function TabletProxy.removeButton + * @param tabletButtonProxy {TabletButtonProxy} button to be removed + */ + Q_INVOKABLE void removeButton(QObject* tabletButtonProxy); + + /**jsdoc + * Updates the audio bar in tablet to reflect latest mic level + * @function TabletProxy#updateAudioBar + * @param micLevel {double} mic level value between 0 and 1 + */ + Q_INVOKABLE void updateAudioBar(const double micLevel); + + QString getName() const { return _name; } + + /**jsdoc + * Used to send an event to the html/js embedded in the tablet + * @function TabletProxy#emitScriptEvent + * @param msg {object|string} + */ + Q_INVOKABLE void emitScriptEvent(QVariant msg); + + Q_INVOKABLE bool onHomeScreen(); + + QObject* getTabletSurface(); + + QQuickItem* getQmlTablet() const; + + QQuickItem* getQmlMenu() const; + +signals: + /**jsdoc + * Signaled when this tablet receives an event from the html/js embedded in the tablet + * @function TabletProxy#webEventReceived + * @param msg {object|string} + * @returns {Signal} + */ + void webEventReceived(QVariant msg); + +private slots: + void addButtonsToHomeScreen(); + void addButtonsToMenuScreen(); +protected: + void removeButtonsFromHomeScreen(); + + QString _name; + std::mutex _mutex; + std::vector> _tabletButtonProxies; + QQuickItem* _qmlTabletRoot { nullptr }; + QObject* _qmlOffscreenSurface { nullptr }; + + enum class State { Uninitialized, Home, Web, Menu }; + State _state { State::Uninitialized }; +}; + +/**jsdoc + * @class TabletButtonProxy + * @property uuid {QUuid} READ_ONLY: uniquely identifies this button + */ +class TabletButtonProxy : public QObject { + Q_OBJECT + Q_PROPERTY(QUuid uuid READ getUuid) +public: + TabletButtonProxy(const QVariantMap& properties); + + void setQmlButton(QQuickItem* qmlButton); + + QUuid getUuid() const { return _uuid; } + + /**jsdoc + * Returns the current value of this button's properties + * @function TabletButtonProxy#getProperties + * @returns {ButtonProperties} + */ + Q_INVOKABLE QVariantMap getProperties() const; + + /**jsdoc + * Replace the values of some of this button's properties + * @function TabletButtonProxy#editProperties + * @param {ButtonProperties} properties - set of properties to change + */ + Q_INVOKABLE void editProperties(QVariantMap properties); + +public slots: + void clickedSlot() { emit clicked(); } + +signals: + /**jsdoc + * Signaled when this button has been clicked on by the user. + * @function TabletButtonProxy#clicked + * @returns {Signal} + */ + void clicked(); + +protected: + QUuid _uuid; + mutable std::mutex _mutex; + QQuickItem* _qmlButton { nullptr }; + QVariantMap _properties; +}; + +/**jsdoc + * @typedef TabletButtonProxy.ButtonProperties + * @property {string} text - button caption + * @property {string} icon - url to button icon. (50 x 50) + * @property {string} activeText - button caption when button is active + * @property {string} activeIcon - url to button icon used when button is active. (50 x 50) + * @property {string} isActive - true when button is active. + */ + +#endif // hifi_TabletScriptingInterface_h diff --git a/libraries/shared/src/CPUDetect.h b/libraries/shared/src/CPUDetect.h index c9d2eb649b..ea6d23d8d6 100644 --- a/libraries/shared/src/CPUDetect.h +++ b/libraries/shared/src/CPUDetect.h @@ -134,7 +134,7 @@ static inline bool cpuSupportsAVX() { result = true; } } - return result; + return result; } static inline bool cpuSupportsAVX2() { @@ -143,11 +143,18 @@ static inline bool cpuSupportsAVX2() { bool result = false; if (cpuSupportsAVX()) { - if (__get_cpuid(0x7, &eax, &ebx, &ecx, &edx) && ((ebx & MASK_AVX2) == MASK_AVX2)) { - result = true; + // Work around a bug where __get_cpuid(0x7) returns wrong values on older GCC + // https://gcc.gnu.org/bugzilla/show_bug.cgi?id=77756 + if (__get_cpuid(0x0, &eax, &ebx, &ecx, &edx) && (eax >= 0x7)) { + + __cpuid_count(0x7, 0x0, eax, ebx, ecx, edx); + + if ((ebx & MASK_AVX2) == MASK_AVX2) { + result = true; + } } } - return result; + return result; } #else diff --git a/libraries/shared/src/RegisteredMetaTypes.cpp b/libraries/shared/src/RegisteredMetaTypes.cpp index 984529c4ba..7f12d6cc00 100644 --- a/libraries/shared/src/RegisteredMetaTypes.cpp +++ b/libraries/shared/src/RegisteredMetaTypes.cpp @@ -742,6 +742,12 @@ void collisionFromScriptValue(const QScriptValue &object, Collision& collision) // TODO: implement this when we know what it means to accept collision events from JS } +void Collision::invert() { + std::swap(idA, idB); + contactPoint += penetration; + penetration *= -1.0f; +} + QScriptValue quuidToScriptValue(QScriptEngine* engine, const QUuid& uuid) { if (uuid.isNull()) { return QScriptValue::NullValue; diff --git a/libraries/shared/src/RegisteredMetaTypes.h b/libraries/shared/src/RegisteredMetaTypes.h index 2aefd3aa47..498a8b3b3a 100644 --- a/libraries/shared/src/RegisteredMetaTypes.h +++ b/libraries/shared/src/RegisteredMetaTypes.h @@ -142,11 +142,13 @@ public: const glm::vec3& cPenetration, const glm::vec3& velocityChange) : type(cType), idA(cIdA), idB(cIdB), contactPoint(cPoint), penetration(cPenetration), velocityChange(velocityChange) { } + void invert(); // swap A and B + ContactEventType type; QUuid idA; QUuid idB; - glm::vec3 contactPoint; - glm::vec3 penetration; + glm::vec3 contactPoint; // on B in world-frame + glm::vec3 penetration; // from B towards A in world-frame glm::vec3 velocityChange; }; Q_DECLARE_METATYPE(Collision) diff --git a/libraries/shared/src/RenderArgs.h b/libraries/shared/src/RenderArgs.h index 851e065f20..b2c05b0548 100644 --- a/libraries/shared/src/RenderArgs.h +++ b/libraries/shared/src/RenderArgs.h @@ -122,6 +122,7 @@ public: gpu::Batch* _batch = nullptr; std::shared_ptr _whiteTexture; + bool _enableTexturing { true }; RenderDetails _details; }; diff --git a/libraries/shared/src/SpatiallyNestable.cpp b/libraries/shared/src/SpatiallyNestable.cpp index 7ecb0f7409..35e574bf06 100644 --- a/libraries/shared/src/SpatiallyNestable.cpp +++ b/libraries/shared/src/SpatiallyNestable.cpp @@ -1032,7 +1032,7 @@ AACube SpatiallyNestable::getQueryAACube() const { return result; } -bool SpatiallyNestable::hasAncestorOfType(NestableType nestableType) { +bool SpatiallyNestable::hasAncestorOfType(NestableType nestableType) const { bool success; SpatiallyNestablePointer parent = getParentPointer(success); if (!success || !parent) { @@ -1046,6 +1046,20 @@ bool SpatiallyNestable::hasAncestorOfType(NestableType nestableType) { return parent->hasAncestorOfType(nestableType); } +const QUuid SpatiallyNestable::findAncestorOfType(NestableType nestableType) const { + bool success; + SpatiallyNestablePointer parent = getParentPointer(success); + if (!success || !parent) { + return QUuid(); + } + + if (parent->_nestableType == nestableType) { + return parent->getID(); + } + + return parent->findAncestorOfType(nestableType); +} + void SpatiallyNestable::getLocalTransformAndVelocities( Transform& transform, glm::vec3& velocity, diff --git a/libraries/shared/src/SpatiallyNestable.h b/libraries/shared/src/SpatiallyNestable.h index 6f56a108bd..cd59fb30a0 100644 --- a/libraries/shared/src/SpatiallyNestable.h +++ b/libraries/shared/src/SpatiallyNestable.h @@ -165,7 +165,8 @@ public: bool isParentIDValid() const { bool success = false; getParentPointer(success); return success; } virtual SpatialParentTree* getParentTree() const { return nullptr; } - bool hasAncestorOfType(NestableType nestableType); + bool hasAncestorOfType(NestableType nestableType) const; + const QUuid findAncestorOfType(NestableType nestableType) const; SpatiallyNestablePointer getParentPointer(bool& success) const; static SpatiallyNestablePointer findByID(QUuid id, bool& success); diff --git a/libraries/ui/src/OffscreenUi.cpp b/libraries/ui/src/OffscreenUi.cpp index 60d80c6b35..7724a409f0 100644 --- a/libraries/ui/src/OffscreenUi.cpp +++ b/libraries/ui/src/OffscreenUi.cpp @@ -130,6 +130,15 @@ void OffscreenUi::hide(const QString& name) { } } +bool OffscreenUi::isVisible(const QString& name) { + QQuickItem* item = getRootItem()->findChild(name); + if (item) { + return QQmlProperty(item, OFFSCREEN_VISIBILITY_PROPERTY).read().toBool(); + } else { + return false; + } +} + class ModalDialogListener : public QObject { Q_OBJECT friend class OffscreenUi; @@ -524,6 +533,10 @@ QQuickItem* OffscreenUi::getDesktop() { return _desktop; } +QObject* OffscreenUi::getRootMenu() { + return getRootItem()->findChild("rootMenu"); +} + QQuickItem* OffscreenUi::getToolWindow() { return _toolWindow; } @@ -533,11 +546,6 @@ void OffscreenUi::unfocusWindows() { Q_ASSERT(invokeResult); } -void OffscreenUi::toggleMenu(const QPoint& screenPosition) { // caller should already have mapped using getReticlePosition - emit showDesktop(); // we really only want to do this if you're showing the menu, but for now this works - QMetaObject::invokeMethod(_desktop, "toggleMenu", Q_ARG(QVariant, screenPosition)); -} - class FileDialogListener : public ModalDialogListener { Q_OBJECT diff --git a/libraries/ui/src/OffscreenUi.h b/libraries/ui/src/OffscreenUi.h index 3ab4fa0758..5813d0bfd2 100644 --- a/libraries/ui/src/OffscreenUi.h +++ b/libraries/ui/src/OffscreenUi.h @@ -40,17 +40,17 @@ public: void createDesktop(const QUrl& url); void show(const QUrl& url, const QString& name, std::function f = [](QQmlContext*, QObject*) {}); void hide(const QString& name); + bool isVisible(const QString& name); void toggle(const QUrl& url, const QString& name, std::function f = [](QQmlContext*, QObject*) {}); bool shouldSwallowShortcut(QEvent* event); bool navigationFocused(); void setNavigationFocused(bool focused); void unfocusWindows(); - void toggleMenu(const QPoint& screenCoordinates); // Setting pinned to true will hide all overlay elements on the desktop that don't have a pinned flag void setPinned(bool pinned = true); - + void togglePinned(); void setConstrainToolbarToCenterX(bool constrained); @@ -59,7 +59,7 @@ public: QObject* getFlags(); QQuickItem* getDesktop(); QQuickItem* getToolWindow(); - + QObject* getRootMenu(); enum Icon { ICON_NONE = 0, ICON_QUESTION, diff --git a/libraries/ui/src/ui/Menu.cpp b/libraries/ui/src/ui/Menu.cpp index ba24adfc3f..f68fff0204 100644 --- a/libraries/ui/src/ui/Menu.cpp +++ b/libraries/ui/src/ui/Menu.cpp @@ -256,6 +256,16 @@ bool Menu::isOptionChecked(const QString& menuOption) const { return false; } +void Menu::closeInfoView(const QString& path) { + auto offscreenUi = DependencyManager::get(); + offscreenUi->hide(path); +} + +bool Menu::isInfoViewVisible(const QString& path) { + auto offscreenUi = DependencyManager::get(); + return offscreenUi->isVisible(path); +} + void Menu::triggerOption(const QString& menuOption) { QAction* action = _actionHash.value(menuOption); if (action) { diff --git a/libraries/ui/src/ui/Menu.h b/libraries/ui/src/ui/Menu.h index 2711fc5921..9839bd1eb6 100644 --- a/libraries/ui/src/ui/Menu.h +++ b/libraries/ui/src/ui/Menu.h @@ -114,6 +114,9 @@ public slots: void toggleDeveloperMenus(); void toggleAdvancedMenus(); + + bool isInfoViewVisible(const QString& path); + void closeInfoView(const QString& path); void triggerOption(const QString& menuOption); diff --git a/script-archive/entity-server-filter-example.js b/script-archive/entity-server-filter-example.js index 4d4f7273f1..ad44bf1583 100644 --- a/script-archive/entity-server-filter-example.js +++ b/script-archive/entity-server-filter-example.js @@ -1,54 +1,106 @@ function filter(p) { - /* block comments are ok, but not double-slash end-of-line-comments */ - + /******************************************************/ + /* General Filter Comments + /* + - Custom filters must be named "filter" and must be global + - Block comments are ok, but not double-slash end-of-line-comments + - Certain JavaScript functions are not available, like Math.sign(), as they are undefined in QT's non-conforming JS + - HiFi's scripting interface is unavailable here. That means you can't call, for example, Users.*() + */ + /******************************************************/ + + /******************************************************/ + /* Simple Filter Examples + /******************************************************/ /* Simple example: if someone specifies name, add an 'x' to it. Note that print is ok to use. */ if (p.name) {p.name += 'x'; print('fixme name', p. name);} + /* This example clamps y. A better filter would probably zero y component of velocity and acceleration. */ if (p.position) {p.position.y = Math.min(1, p.position.y); print('fixme p.y', p.position.y);} - /* Can also reject altogether */ + + /* Can also reject new properties altogether by returning false */ if (p.userData) { return false; } + /* Reject if modifications made to Model properties */ if (p.modelURL || p.compoundShapeURL || p.shape || p.shapeType || p.url || p.fps || p.currentFrame || p.running || p.loop || p.firstFrame || p.lastFrame || p.hold || p.textures || p.xTextureURL || p.yTextureURL || p.zTextureURL) { return false; } + + /******************************************************/ + /* Physical Property Filter Examples + /* + NOTES about filtering physical properties: + - For now, ensure you always supply a new value for the filtered physical property + (instead of simply removing the property) + - Ensure you always specify a slightly different value for physical properties every + time your filter returns. Look to "var nearZero" below for an example). + This is necessary because Interface checks if a physical property has changed + when deciding whether to apply or reject the server's physical properties. + If a physical property's value doesn't change, Interface will reject the server's property value, + and Bullet will continue simulating the entity with stale physical properties. + Ensure that this value is not changed by such a small amount such that new values + fall within floating point precision boundaries. If you accidentally do this, prepare for many + hours of frustrating debugging :). + */ + /******************************************************/ /* Clamp velocity to maxVelocity units/second. Zeroing each component of acceleration keeps us from slamming.*/ - var maxVelocity = 5; if (p.velocity) { + var maxVelocity = 5; + /* Random near-zero value used as "zero" to prevent two sequential updates from being + exactly the same (which would cause them to be ignored) */ + var nearZero = 0.0001 * Math.random() + 0.001; + function sign(val) { + if (val > 0) { + return 1; + } else if (val < 0) { + return -1; + } else { + return 0; + } + } if (Math.abs(p.velocity.x) > maxVelocity) { - p.velocity.x = Math.sign(p.velocity.x) * maxVelocity; - p.acceleration.x = 0; + p.velocity.x = sign(p.velocity.x) * (maxVelocity + nearZero); + p.acceleration.x = nearZero; } if (Math.abs(p.velocity.y) > maxVelocity) { - p.velocity.y = Math.sign(p.velocity.y) * maxVelocity; - p.acceleration.y = 0; + p.velocity.y = sign(p.velocity.y) * (maxVelocity + nearZero); + p.acceleration.y = nearZero; } if (Math.abs(p.velocity.z) > maxVelocity) { - p.velocity.z = Math.sign(p.velocity.z) * maxVelocity; - p.acceleration.z = 0; + p.velocity.z = sign(p.velocity.z) * (maxVelocity + nearZero); + p.acceleration.z = nearZero; } } + /* Define an axis-aligned zone in which entities are not allowed to enter. */ /* This example zone corresponds to an area to the right of the spawnpoint in your Sandbox. It's an area near the big rock to the right. If an entity enters the zone, it'll move behind the rock.*/ - var boxMin = {x: 25.5, y: -0.48, z: -9.9}; - var boxMax = {x: 31.1, y: 4, z: -3.79}; - var zero = {x: 0.0, y: 0.0, z: 0.0}; - if (p.position) { + /* Random near-zero value used as "zero" to prevent two sequential updates from being + exactly the same (which would cause them to be ignored) */ + var nearZero = 0.0001 * Math.random() + 0.001; + /* Define the points that create the "NO ENTITIES ALLOWED" box */ + var boxMin = {x: 25.5, y: -0.48, z: -9.9}; + var boxMax = {x: 31.1, y: 4, z: -3.79}; + /* Define the point that you want entites that enter the box to appear */ + var resetPoint = {x: 29.5, y: 0.37 + nearZero, z: -2}; var x = p.position.x; var y = p.position.y; var z = p.position.z; if ((x > boxMin.x && x < boxMax.x) && (y > boxMin.y && y < boxMax.y) && (z > boxMin.z && z < boxMax.z)) { - /* Move it to the origin of the zone */ - p.position = boxMin; - p.velocity = zero; - p.acceleration = zero; + p.position = resetPoint; + if (p.velocity) { + p.velocity = {x: 0, y: nearZero, z: 0}; + } + if (p.acceleration) { + p.acceleration = {x: 0, y: nearZero, z: 0}; + } } } diff --git a/scripts/defaultScripts.js b/scripts/defaultScripts.js index e9868bd38d..bb96fe96cf 100644 --- a/scripts/defaultScripts.js +++ b/scripts/defaultScripts.js @@ -14,13 +14,17 @@ var DEFAULT_SCRIPTS = [ "system/progress.js", "system/away.js", - "system/users.js", "system/mute.js", - "system/goto.js", "system/hmd.js", + "system/menu.js", + "system/bubble.js", + "system/snapshot.js", + "system/help.js", + "system/pal.js", //"system/mod.js", // older UX, if you prefer + "system/goto.js", "system/marketplaces/marketplaces.js", "system/edit.js", - "system/pal.js", //"system/mod.js", // older UX, if you prefer + "system/users.js", "system/selectAudioDevice.js", "system/notifications.js", "system/controllers/controllerDisplayManager.js", @@ -32,9 +36,7 @@ var DEFAULT_SCRIPTS = [ "system/controllers/toggleAdvancedMovementForHandControllers.js", "system/dialTone.js", "system/firstPersonHMD.js", - "system/snapshot.js", - "system/help.js", - "system/bubble.js" + "system/tablet-ui/tabletUI.js" ]; // add a menu item for debugging diff --git a/scripts/developer/tests/tabletEventBridgeTest.js b/scripts/developer/tests/tabletEventBridgeTest.js new file mode 100644 index 0000000000..1fa935bef2 --- /dev/null +++ b/scripts/developer/tests/tabletEventBridgeTest.js @@ -0,0 +1,81 @@ +// +// tabletEventBridgeTest.js +// +// Created by Anthony J. Thibault on 2016-12-15 +// Copyright 2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +// Adds a button to the tablet that will switch to a web page. +// This web page contains buttons that will use the event bridge to trigger sounds. + +/* globals Tablet */ + + +var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); +var tabletButton = tablet.addButton({ + text: "SOUNDS" +}); + +var WEB_BRIDGE_TEST_HTML = "https://s3.amazonaws.com/hifi-public/tony/webBridgeTest.html?2"; + +var TROMBONE_URL = "https://s3.amazonaws.com/hifi-public/tony/audio/sad-trombone.wav"; +var tromboneSound = SoundCache.getSound(TROMBONE_URL); +var tromboneInjector; + +var SCREAM_URL = "https://s3.amazonaws.com/hifi-public/tony/audio/wilhelm-scream.wav"; +var screamSound = SoundCache.getSound(SCREAM_URL); +var screamInjector; + +tabletButton.clicked.connect(function () { + tablet.gotoWebScreen(WEB_BRIDGE_TEST_HTML); +}); + +// hook up to the event bridge +tablet.webEventReceived.connect(function (msg) { + Script.print("HIFI: recv web event = " + JSON.stringify(msg)); + if (msg === "button-1-play") { + + // play sad trombone + if (tromboneSound.downloaded) { + if (tromboneInjector) { + tromboneInjector.restart(); + } else { + tromboneInjector = Audio.playSound(tromboneSound, { position: MyAvatar.position, + volume: 1.0, + loop: false }); + } + } + + // wait until sound is finished then send a done event + Script.setTimeout(function () { + tablet.emitScriptEvent("button-1-done"); + }, 3500); + } + + if (msg === "button-2-play") { + + // play scream + if (screamSound.downloaded) { + if (screamInjector) { + screamInjector.restart(); + } else { + screamInjector = Audio.playSound(screamSound, { position: MyAvatar.position, + volume: 1.0, + loop: false }); + } + } + + // wait until sound is finished then send a done event + Script.setTimeout(function () { + tablet.emitScriptEvent("button-2-done"); + }, 1000); + } +}); + +Script.scriptEnding.connect(function () { + tablet.removeButton(tabletButton); +}); + diff --git a/scripts/developer/tests/tabletTest.js b/scripts/developer/tests/tabletTest.js new file mode 100644 index 0000000000..438d0a4b99 --- /dev/null +++ b/scripts/developer/tests/tabletTest.js @@ -0,0 +1,45 @@ +// +// tabletTest.js +// +// Created by Anthony J. Thibault on 2016-12-15 +// Copyright 2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +// Adds a BAM! button to the tablet ui. + +var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); +var button = tablet.addButton({ + text: "BAM!!!" +}); + +var TEST_BUTTON_COUNT = 10; +for (var i = 0; i < TEST_BUTTON_COUNT; i++) { + tablet.addButton({ + text: "TEST_" + i, + inDebugMode: true + }); +} + +// change the name and isActive state every second... +var names = ["BAM!", "BAM!!", "BAM!!!", "BAM!!!!"]; +var nameIndex = 0; +Script.setInterval(function () { + nameIndex = (nameIndex + 1) % names.length; + button.editProperties({ + isActive: (nameIndex & 0x1) == 0, + text: names[nameIndex] + }); +}, 1000); + +button.clicked.connect(function () { + print("AJT: BAM!!! CLICK from JS!"); + var url = "https://news.ycombinator.com/"; + tablet.gotoWebScreen(url); +}); + +Script.scriptEnding.connect(function () { + tablet.removeButton(button); +}); diff --git a/scripts/developer/utilities/render/deferredLighting.qml b/scripts/developer/utilities/render/deferredLighting.qml index 26dbc1f2bc..0ac4cbc5b5 100644 --- a/scripts/developer/utilities/render/deferredLighting.qml +++ b/scripts/developer/utilities/render/deferredLighting.qml @@ -25,6 +25,7 @@ Column { "Lightmap:LightingModel:enableLightmap", "Background:LightingModel:enableBackground", "ssao:AmbientOcclusion:enabled", + "Textures:LightingModel:enableMaterialTexturing", ] CheckBox { text: modelData.split(":")[0] diff --git a/scripts/system/bubble.js b/scripts/system/bubble.js index 2f7286872e..b5134e096b 100644 --- a/scripts/system/bubble.js +++ b/scripts/system/bubble.js @@ -15,8 +15,7 @@ (function () { // BEGIN LOCAL_SCOPE - // grab the toolbar - var toolbar = Toolbars.getToolbar("com.highfidelity.interface.toolbar.system"); + var button; // Used for animating and disappearing the bubble var bubbleOverlayTimestamp; // Used for flashing the HUD button upon activation @@ -91,9 +90,7 @@ // Used to set the state of the bubble HUD button function writeButtonProperties(parameter) { - button.writeProperty('buttonState', parameter ? 0 : 1); - button.writeProperty('defaultState', parameter ? 0 : 1); - button.writeProperty('hoverState', parameter ? 2 : 3); + button.editProperties({isActive: parameter}); } // The bubble script's update function @@ -166,13 +163,23 @@ } } - // Setup the bubble button and add it to the toolbar - var button = toolbar.addButton({ - objectName: 'bubble', - imageURL: buttonImageURL(), - visible: true, - alpha: 0.9 - }); + // Setup the bubble button + var buttonName = "BUBBLE"; + if (Settings.getValue("HUDUIEnabled")) { + var toolbar = Toolbars.getToolbar("com.highfidelity.interface.toolbar.system"); + button = toolbar.addButton({ + objectName: 'bubble', + imageURL: buttonImageURL(), + visible: true, + alpha: 0.9 + }); + } else { + var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); + button = tablet.addButton({ + icon: "icons/tablet-icons/bubble-i.svg", + text: buttonName + }); + } onBubbleToggled(); button.clicked.connect(Users.toggleIgnoreRadius); @@ -181,8 +188,13 @@ // Cleanup the toolbar button and overlays when script is stopped Script.scriptEnding.connect(function () { - toolbar.removeButton('bubble'); button.clicked.disconnect(Users.toggleIgnoreRadius); + if (tablet) { + tablet.removeButton(button); + } + if (toolbar) { + toolbar.removeButton('bubble'); + } Users.ignoreRadiusEnabledChanged.disconnect(onBubbleToggled); Users.enteredIgnoreRadius.disconnect(enteredIgnoreRadius); Overlays.deleteOverlay(bubbleOverlay); diff --git a/scripts/system/controllers/grab.js b/scripts/system/controllers/grab.js index 38a07c469d..e495ccc67b 100644 --- a/scripts/system/controllers/grab.js +++ b/scripts/system/controllers/grab.js @@ -14,9 +14,12 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // +/* global MyAvatar, Entities, Script, Camera, Vec3, Reticle, Overlays, getEntityCustomData, Messages, Quat, Controller */ + + (function() { // BEGIN LOCAL_SCOPE -Script.include("../libraries/utils.js"); +Script.include("/~/system/libraries/utils.js"); var MAX_SOLID_ANGLE = 0.01; // objects that appear smaller than this can't be grabbed var ZERO_VEC3 = { @@ -31,11 +34,6 @@ var IDENTITY_QUAT = { w: 0 }; var GRABBABLE_DATA_KEY = "grabbableKey"; // shared with handControllerGrab.js -var GRAB_USER_DATA_KEY = "grabKey"; // shared with handControllerGrab.js - -var MSECS_PER_SEC = 1000.0; -var HEART_BEAT_INTERVAL = 5 * MSECS_PER_SEC; -var HEART_BEAT_TIMEOUT = 15 * MSECS_PER_SEC; var DEFAULT_GRABBABLE_DATA = { grabbable: true, @@ -72,7 +70,7 @@ function entityIsGrabbedByOther(entityID) { for (var actionIndex = 0; actionIndex < actionIDs.length; actionIndex++) { var actionID = actionIDs[actionIndex]; var actionArguments = Entities.getActionArguments(entityID, actionID); - var tag = actionArguments["tag"]; + var tag = actionArguments.tag; if (tag == getTag()) { // we see a grab-*uuid* shaped tag, but it's our tag, so that's okay. continue; @@ -153,14 +151,14 @@ Mouse.prototype.startDrag = function(position) { y: position.y }; this.startRotateDrag(); -} +}; Mouse.prototype.updateDrag = function(position) { this.current = { x: position.x, y: position.y }; -} +}; Mouse.prototype.startRotateDrag = function() { this.previous = { @@ -172,7 +170,7 @@ Mouse.prototype.startRotateDrag = function() { y: this.current.y }; this.cursorRestore = Reticle.getPosition(); -} +}; Mouse.prototype.getDrag = function() { var delta = { @@ -184,7 +182,7 @@ Mouse.prototype.getDrag = function() { y: this.current.y }; return delta; -} +}; Mouse.prototype.restoreRotateCursor = function() { Reticle.setPosition(this.cursorRestore); @@ -192,7 +190,7 @@ Mouse.prototype.restoreRotateCursor = function() { x: this.rotateStart.x, y: this.rotateStart.y }; -} +}; var mouse = new Mouse(); @@ -216,13 +214,13 @@ Beacon.prototype.enable = function() { Overlays.editOverlay(this.overlayID, { visible: true }); -} +}; Beacon.prototype.disable = function() { Overlays.editOverlay(this.overlayID, { visible: false }); -} +}; Beacon.prototype.updatePosition = function(position) { Overlays.editOverlay(this.overlayID, { @@ -238,7 +236,7 @@ Beacon.prototype.updatePosition = function(position) { z: position.z } }); -} +}; var beacon = new Beacon(); @@ -260,7 +258,7 @@ function Grabber() { this.planeNormal = ZERO_VEC3; // maxDistance is a function of the size of the object. - this.maxDistance; + this.maxDistance = 0; // mode defines the degrees of freedom of the grab target positions // relative to startPosition options include: @@ -279,8 +277,8 @@ function Grabber() { z: 0 }; - this.targetPosition; - this.targetRotation; + this.targetPosition = null; + this.targetRotation = null; this.liftKey = false; // SHIFT this.rotateKey = false; // CONTROL @@ -316,7 +314,7 @@ Grabber.prototype.computeNewGrabPlane = function() { var xzOffset = Vec3.subtract(this.pointOnPlane, Camera.getPosition()); xzOffset.y = 0; this.xzDistanceToGrab = Vec3.length(xzOffset); -} +}; Grabber.prototype.pressEvent = function(event) { if (isInEditMode()) { @@ -351,10 +349,8 @@ Grabber.prototype.pressEvent = function(event) { mouse.startDrag(event); - this.lastHeartBeat = 0; - var clickedEntity = pickResults.entityID; - var entityProperties = Entities.getEntityProperties(clickedEntity) + var entityProperties = Entities.getEntityProperties(clickedEntity); this.startPosition = entityProperties.position; this.lastRotation = entityProperties.rotation; var cameraPosition = Camera.getPosition(); @@ -367,7 +363,7 @@ Grabber.prototype.pressEvent = function(event) { return; } - this.activateEntity(clickedEntity, entityProperties); + // this.activateEntity(clickedEntity, entityProperties); this.isGrabbing = true; this.entityID = clickedEntity; @@ -403,10 +399,9 @@ Grabber.prototype.pressEvent = function(event) { grabbedEntity: this.entityID })); - // TODO: play sounds again when we aren't leaking AudioInjector threads //Audio.playSound(grabSound, { position: entityProperties.position, volume: VOLUME }); -} +}; Grabber.prototype.releaseEvent = function(event) { if (event.isLeftButton!==true ||event.isRightButton===true || event.isMiddleButton===true) { @@ -414,9 +409,11 @@ Grabber.prototype.releaseEvent = function(event) { } if (this.isGrabbing) { - this.deactivateEntity(this.entityID); - this.isGrabbing = false - Entities.deleteAction(this.entityID, this.actionID); + // this.deactivateEntity(this.entityID); + this.isGrabbing = false; + if (this.actionID) { + Entities.deleteAction(this.entityID, this.actionID); + } this.actionID = null; beacon.disable(); @@ -430,32 +427,17 @@ Grabber.prototype.releaseEvent = function(event) { joint: "mouse" })); - // TODO: play sounds again when we aren't leaking AudioInjector threads //Audio.playSound(releaseSound, { position: entityProperties.position, volume: VOLUME }); } -} - - -Grabber.prototype.heartBeat = function(entityID) { - var now = Date.now(); - if (now - this.lastHeartBeat > HEART_BEAT_INTERVAL) { - var data = getEntityCustomData(GRAB_USER_DATA_KEY, entityID, {}); - data["heartBeat"] = now; - setEntityCustomData(GRAB_USER_DATA_KEY, entityID, data); - this.lastHeartBeat = now; - } }; - Grabber.prototype.moveEvent = function(event) { if (!this.isGrabbing) { return; } mouse.updateDrag(event); - this.heartBeat(this.entityID); - // see if something added/restored gravity var entityProperties = Entities.getEntityProperties(this.entityID); if (Vec3.length(entityProperties.gravity) !== 0.0) { @@ -484,8 +466,8 @@ Grabber.prototype.moveEvent = function(event) { //var qZero = this.lastRotation; this.lastRotation = Quat.multiply(deltaQ, this.lastRotation); - var distanceToCamera = Vec3.length(Vec3.subtract(this.currentPosition, cameraPosition)); - var angularTimeScale = distanceGrabTimescale(this.mass, distanceToCamera); + var distanceToCameraR = Vec3.length(Vec3.subtract(this.currentPosition, cameraPosition)); + var angularTimeScale = distanceGrabTimescale(this.mass, distanceToCameraR); actionArgs = { targetRotation: this.lastRotation, @@ -525,8 +507,8 @@ Grabber.prototype.moveEvent = function(event) { } this.targetPosition = Vec3.subtract(newPointOnPlane, this.offset); - var distanceToCamera = Vec3.length(Vec3.subtract(this.targetPosition, cameraPosition)); - var linearTimeScale = distanceGrabTimescale(this.mass, distanceToCamera); + var distanceToCameraL = Vec3.length(Vec3.subtract(this.targetPosition, cameraPosition)); + var linearTimeScale = distanceGrabTimescale(this.mass, distanceToCameraL); actionArgs = { targetPosition: this.targetPosition, @@ -535,7 +517,6 @@ Grabber.prototype.moveEvent = function(event) { ttl: ACTION_TTL }; - beacon.updatePosition(this.targetPosition); } @@ -546,7 +527,7 @@ Grabber.prototype.moveEvent = function(event) { } else { Entities.updateAction(this.entityID, this.actionID, actionArgs); } -} +}; Grabber.prototype.keyReleaseEvent = function(event) { if (event.text === "SHIFT") { @@ -556,7 +537,7 @@ Grabber.prototype.keyReleaseEvent = function(event) { this.rotateKey = false; } this.computeNewGrabPlane(); -} +}; Grabber.prototype.keyPressEvent = function(event) { if (event.text === "SHIFT") { @@ -566,49 +547,8 @@ Grabber.prototype.keyPressEvent = function(event) { this.rotateKey = true; } this.computeNewGrabPlane(); -} - -Grabber.prototype.activateEntity = function(entityID, grabbedProperties) { - var grabbableData = getEntityCustomData(GRABBABLE_DATA_KEY, entityID, DEFAULT_GRABBABLE_DATA); - var invertSolidWhileHeld = grabbableData["invertSolidWhileHeld"]; - var data = getEntityCustomData(GRAB_USER_DATA_KEY, entityID, {}); - data["activated"] = true; - data["avatarId"] = MyAvatar.sessionUUID; - data["refCount"] = data["refCount"] ? data["refCount"] + 1 : 1; - // zero gravity and set collisionless to true, but in a way that lets us put them back, after all grabs are done - if (data["refCount"] == 1) { - data["gravity"] = grabbedProperties.gravity; - data["collisionless"] = grabbedProperties.collisionless; - data["dynamic"] = grabbedProperties.dynamic; - var whileHeldProperties = {gravity: {x:0, y:0, z:0}}; - if (invertSolidWhileHeld) { - whileHeldProperties["collisionless"] = ! grabbedProperties.collisionless; - } - Entities.editEntity(entityID, whileHeldProperties); - } - setEntityCustomData(GRAB_USER_DATA_KEY, entityID, data); }; -Grabber.prototype.deactivateEntity = function(entityID) { - var data = getEntityCustomData(GRAB_USER_DATA_KEY, entityID, {}); - if (data && data["refCount"]) { - data["refCount"] = data["refCount"] - 1; - if (data["refCount"] < 1) { - Entities.editEntity(entityID, { - gravity: data["gravity"], - collisionless: data["collisionless"], - dynamic: data["dynamic"] - }); - data = null; - } - } else { - data = null; - } - setEntityCustomData(GRAB_USER_DATA_KEY, entityID, data); -}; - - - var grabber = new Grabber(); function pressEvent(event) { diff --git a/scripts/system/controllers/handControllerGrab.js b/scripts/system/controllers/handControllerGrab.js index 6608b9cc92..972d95e9e9 100644 --- a/scripts/system/controllers/handControllerGrab.js +++ b/scripts/system/controllers/handControllerGrab.js @@ -12,15 +12,16 @@ // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -/* global setEntityCustomData, getEntityCustomData, flatten, Xform, Script, Quat, Vec3, MyAvatar, Entities, Overlays, Settings, - Reticle, Controller, Camera, Messages, Mat4, getControllerWorldLocation, getGrabPointSphereOffset, setGrabCommunications */ +/* global getEntityCustomData, flatten, Xform, Script, Quat, Vec3, MyAvatar, Entities, Overlays, Settings, + Reticle, Controller, Camera, Messages, Mat4, getControllerWorldLocation, getGrabPointSphereOffset, setGrabCommunications, + Menu, HMD */ /* eslint indent: ["error", 4, { "outerIIFEBody": 0 }] */ (function() { // BEGIN LOCAL_SCOPE -Script.include("../libraries/utils.js"); -Script.include("../libraries/Xform.js"); -Script.include("../libraries/controllers.js"); +Script.include("/~/system/libraries/utils.js"); +Script.include("/~/system/libraries/Xform.js"); +Script.include("/~/system/libraries/controllers.js"); // // add lines where the hand ray picking is happening @@ -40,8 +41,6 @@ var TRIGGER_SMOOTH_RATIO = 0.1; // Time averaging of trigger - 0.0 disables smo var TRIGGER_OFF_VALUE = 0.1; var TRIGGER_ON_VALUE = TRIGGER_OFF_VALUE + 0.05; // Squeezed just enough to activate search or near grab -var COLLIDE_WITH_AV_AFTER_RELEASE_DELAY = 0.25; // seconds - var BUMPER_ON_VALUE = 0.5; var THUMB_ON_VALUE = 0.5; @@ -61,6 +60,11 @@ var PICK_WITH_HAND_RAY = true; var EQUIP_SPHERE_SCALE_FACTOR = 0.65; +var WEB_DISPLAY_STYLUS_DISTANCE = 0.5; +var WEB_STYLUS_LENGTH = 0.2; +var WEB_TOUCH_Y_OFFSET = 0.05; // how far forward (or back with a negative number) to slide stylus in hand +var WEB_TOUCH_TOO_CLOSE = 0.03; // if the stylus is pushed far though the web surface, don't consider it touching +var WEB_TOUCH_Y_TOUCH_DEADZONE_SIZE = 0.01; // // distant manipulation @@ -168,7 +172,6 @@ var GRABBABLE_PROPERTIES = [ ]; var GRABBABLE_DATA_KEY = "grabbableKey"; // shared with grab.js -var GRAB_USER_DATA_KEY = "grabKey"; // shared with grab.js var DEFAULT_GRABBABLE_DATA = { disableReleaseVelocity: false @@ -181,6 +184,15 @@ var blacklist = []; var FORBIDDEN_GRAB_NAMES = ["Grab Debug Entity", "grab pointer"]; var FORBIDDEN_GRAB_TYPES = ["Unknown", "Light", "PolyLine", "Zone"]; +var holdEnabled = true; +var nearGrabEnabled = true; +var farGrabEnabled = true; +var myAvatarScalingEnabled = true; +var objectScalingEnabled = true; +var mostRecentSearchingHand = RIGHT_HAND; +var DEFAULT_SPHERE_MODEL_URL = "http://hifi-content.s3.amazonaws.com/alan/dev/equip-Fresnel-3.fbx"; +var HARDWARE_MOUSE_ID = 0; // Value reserved for hardware mouse. + // states for the state machine var STATE_OFF = 0; var STATE_SEARCHING = 1; @@ -189,34 +201,13 @@ var STATE_NEAR_GRABBING = 3; var STATE_NEAR_TRIGGER = 4; var STATE_FAR_TRIGGER = 5; var STATE_HOLD = 6; -var STATE_ENTITY_TOUCHING = 7; -var STATE_OVERLAY_TOUCHING = 8; - -var holdEnabled = true; -var nearGrabEnabled = true; -var farGrabEnabled = true; -var myAvatarScalingEnabled = true; -var objectScalingEnabled = true; - -// "collidesWith" is specified by comma-separated list of group names -// the possible group names are: static, dynamic, kinematic, myAvatar, otherAvatar -var COLLIDES_WITH_WHILE_GRABBED = "dynamic,otherAvatar"; - -var HEART_BEAT_INTERVAL = 5 * MSECS_PER_SEC; -var HEART_BEAT_TIMEOUT = 15 * MSECS_PER_SEC; - -var delayedDeactivateFunc; -var delayedDeactivateTimeout; -var delayedDeactivateEntityID; +var STATE_ENTITY_STYLUS_TOUCHING = 7; +var STATE_ENTITY_LASER_TOUCHING = 8; +var STATE_OVERLAY_STYLUS_TOUCHING = 9; +var STATE_OVERLAY_LASER_TOUCHING = 10; var CONTROLLER_STATE_MACHINE = {}; -var mostRecentSearchingHand = RIGHT_HAND; - -var DEFAULT_SPHERE_MODEL_URL = "http://hifi-content.s3.amazonaws.com/alan/dev/equip-Fresnel-3.fbx"; - -var HARDWARE_MOUSE_ID = 0; // Value reserved for hardware mouse. - CONTROLLER_STATE_MACHINE[STATE_OFF] = { name: "off", enterMethod: "offEnter", @@ -252,18 +243,21 @@ CONTROLLER_STATE_MACHINE[STATE_FAR_TRIGGER] = { enterMethod: "farTriggerEnter", updateMethod: "farTrigger" }; -CONTROLLER_STATE_MACHINE[STATE_ENTITY_TOUCHING] = { +CONTROLLER_STATE_MACHINE[STATE_ENTITY_STYLUS_TOUCHING] = { name: "entityTouching", enterMethod: "entityTouchingEnter", exitMethod: "entityTouchingExit", updateMethod: "entityTouching" }; -CONTROLLER_STATE_MACHINE[STATE_OVERLAY_TOUCHING] = { +CONTROLLER_STATE_MACHINE[STATE_ENTITY_LASER_TOUCHING] = CONTROLLER_STATE_MACHINE[STATE_ENTITY_STYLUS_TOUCHING]; +CONTROLLER_STATE_MACHINE[STATE_OVERLAY_STYLUS_TOUCHING] = { name: "overlayTouching", enterMethod: "overlayTouchingEnter", exitMethod: "overlayTouchingExit", updateMethod: "overlayTouching" }; +CONTROLLER_STATE_MACHINE[STATE_OVERLAY_LASER_TOUCHING] = CONTROLLER_STATE_MACHINE[STATE_OVERLAY_STYLUS_TOUCHING]; + function distanceBetweenPointAndEntityBoundingBox(point, entityProps) { var entityXform = new Xform(entityProps.rotation, entityProps.position); @@ -284,10 +278,6 @@ function distanceBetweenPointAndEntityBoundingBox(point, entityProps) { return Vec3.distance(v, localPoint); } -function angleBetween(a, b) { - return Math.acos(Vec3.dot(Vec3.normalize(a), Vec3.normalize(b))); -} - function projectOntoXYPlane(worldPos, position, rotation, dimensions, registrationPoint) { var invRot = Quat.inverse(rotation); var localPos = Vec3.multiplyQbyV(invRot, Vec3.subtract(worldPos, position)); @@ -315,9 +305,11 @@ function projectOntoOverlayXYPlane(overlayID, worldPos) { var resolution = Overlays.getProperty(overlayID, "resolution"); resolution.z = 1; // Circumvent divide-by-zero. var scale = Overlays.getProperty(overlayID, "dimensions"); + scale.z = 0.01; // overlay dimensions are 2D, not 3D. dimensions = Vec3.multiplyVbyV(Vec3.multiply(resolution, INCHES_TO_METERS / dpi), scale); } else { dimensions = Overlays.getProperty(overlayID, "dimensions"); + dimensions.z = 0.01; // overlay dimensions are 2D, not 3D. } return projectOntoXYPlane(worldPos, position, rotation, dimensions, DEFAULT_REGISTRATION_POINT); @@ -479,17 +471,6 @@ function storeAttachPointForHotspotInSettings(hotspot, hand, offsetPosition, off setAttachPointSettings(attachPointSettings); } -function removeMyAvatarFromCollidesWith(origCollidesWith) { - var collidesWithSplit = origCollidesWith.split(","); - // remove myAvatar from the array - for (var i = collidesWithSplit.length - 1; i >= 0; i--) { - if (collidesWithSplit[i] === "myAvatar") { - collidesWithSplit.splice(i, 1); - } - } - return collidesWithSplit.join(); -} - // If another script is managing the reticle (as is done by HandControllerPointer), we should not be setting it here, // and we should not be showing lasers when someone else is using the Reticle to indicate a 2D minor mode. var EXTERNALLY_MANAGED_2D_MINOR_MODE = true; @@ -747,6 +728,21 @@ function MyController(hand) { this.hand = hand; this.autoUnequipCounter = 0; this.grabPointIntersectsEntity = false; + this.stylus = null; + this.homeButtonTouched = false; + + // Until there is some reliable way to keep track of a "stack" of parentIDs, we'll have problems + // when more than one avatar does parenting grabs on things. This script tries to work + // around this with two associative arrays: previousParentID and previousParentJointIndex. If + // (1) avatar-A does a parenting grab on something, and then (2) avatar-B takes it, and (3) avatar-A + // releases it and then (4) avatar-B releases it, then avatar-B will set the parent back to + // avatar-A's hand. Avatar-A is no longer grabbing it, so it will end up triggering avatar-A's + // checkForUnexpectedChildren which will put it back to wherever it was when avatar-A initially grabbed it. + // this will work most of the time, unless avatar-A crashes or logs out while avatar-B is grabbing the + // entity. This can also happen when a single avatar passes something from hand to hand. + this.previousParentID = {}; + this.previousParentJointIndex = {}; + this.previouslyUnhooked = {}; this.shouldScale = false; @@ -806,6 +802,10 @@ function MyController(hand) { this.equipOverlayInfoSetMap = {}; + this.tabletStabbed = false; + this.tabletStabbedPos2D = null; + this.tabletStabbedPos3D = null; + var _this = this; var suppressedIn2D = [STATE_OFF, STATE_SEARCHING]; @@ -817,13 +817,19 @@ function MyController(hand) { this.update = function(deltaTime, timestamp) { this.updateSmoothedTrigger(); - // If both trigger and grip buttons squeezed and nothing is held, rescale my avatar! - if (this.hand === RIGHT_HAND && this.state === STATE_SEARCHING && this.getOtherHandController().state === STATE_SEARCHING) { + if (this.hand === RIGHT_HAND && this.state === STATE_SEARCHING && + this.getOtherHandController().state === STATE_SEARCHING) { this.maybeScaleMyAvatar(); } if (this.ignoreInput()) { + + // Most hand input is disabled, because we are interacting with the 2d hud. + // However, we still should check for collisions of the stylus with the web overlay. + var controllerLocation = getControllerWorldLocation(this.handToController(), true); + this.processStylus(controllerLocation.position); + this.turnOffVisualizations(); return; } @@ -847,14 +853,17 @@ function MyController(hand) { }; this.setState = function(newState, reason) { - if (isInEditMode() && newState !== STATE_OFF && newState !== STATE_SEARCHING) { + if (isInEditMode() && (newState !== STATE_OFF && + newState !== STATE_SEARCHING && + newState !== STATE_OVERLAY_STYLUS_TOUCHING)) { return; } setGrabCommunications((newState === STATE_DISTANCE_HOLDING) || (newState === STATE_NEAR_GRABBING)); if (WANT_DEBUG || WANT_DEBUG_STATE) { var oldStateName = stateToName(this.state); var newStateName = stateToName(newState); - print("STATE (" + this.hand + "): " + newStateName + " <-- " + oldStateName + ", reason = " + reason); + print("STATE (" + this.hand + "): " + this.state + "-" + newStateName + + " <-- " + oldStateName + ", reason = " + reason); } // exit the old state @@ -948,6 +957,46 @@ function MyController(hand) { } }; + this.showStylus = function() { + if (this.stylus) { + return; + } + if (!MyAvatar.sessionUUID) { + return; + } + + var stylusProperties = { + url: Script.resourcesPath() + "meshes/tablet-stylus-fat.fbx", + localPosition: Vec3.sum({ x: 0.0, + y: WEB_TOUCH_Y_OFFSET, + z: 0.0 }, + getGrabPointSphereOffset(this.handToController())), + localRotation: Quat.fromVec3Degrees({ x: -90, y: 0, z: 0 }), + dimensions: { x: 0.01, y: 0.01, z: WEB_STYLUS_LENGTH }, + solid: true, + visible: true, + ignoreRayIntersection: true, + drawInFront: false, + parentID: MyAvatar.sessionUUID, + parentJointIndex: MyAvatar.getJointIndex(this.hand === RIGHT_HAND ? + "_CAMERA_RELATIVE_CONTROLLER_RIGHTHAND" : + "_CAMERA_RELATIVE_CONTROLLER_LEFTHAND") + }; + this.stylus = Overlays.addOverlay("model", stylusProperties); + }; + + this.hideStylus = function() { + if (!this.stylus) { + return; + } + Overlays.deleteOverlay(this.stylus); + this.stylus = null; + if (this.stylusTip) { + Overlays.deleteOverlay(this.stylusTip); + this.stylusTip = null; + } + }; + this.overlayLineOn = function(closePoint, farPoint, color) { if (this.overlayLine === null) { var lineProperties = { @@ -1113,8 +1162,43 @@ function MyController(hand) { return _this.rawThumbValue < THUMB_ON_VALUE; }; + this.processStylus = function(worldHandPosition) { + // see if the hand is near a tablet or web-entity + var candidateEntities = Entities.findEntities(worldHandPosition, WEB_DISPLAY_STYLUS_DISTANCE); + entityPropertiesCache.addEntities(candidateEntities); + var nearWeb = false; + for (var i = 0; i < candidateEntities.length; i++) { + var props = entityPropertiesCache.getProps(candidateEntities[i]); + if (props && (props.type == "Web" || this.isTablet(candidateEntities[i]))) { + nearWeb = true; + break; + } + } + + if (nearWeb) { + this.showStylus(); + var rayPickInfo = this.calcRayPickInfo(this.hand); + if (rayPickInfo.distance < WEB_STYLUS_LENGTH / 2.0 + WEB_TOUCH_Y_OFFSET && + rayPickInfo.distance > WEB_STYLUS_LENGTH / 2.0 + WEB_TOUCH_TOO_CLOSE) { + this.handleStylusOnHomeButton(rayPickInfo); + if (this.handleStylusOnWebEntity(rayPickInfo)) { + return; + } + if (this.handleStylusOnWebOverlay(rayPickInfo)) { + return; + } + } else { + this.homeButtonTouched = false; + } + } else { + this.hideStylus(); + } + }; + this.off = function(deltaTime, timestamp) { + this.checkForUnexpectedChildren(); + if (this.triggerSmoothedReleased() && this.secondaryReleased()) { this.waitForTriggerRelease = false; } @@ -1157,6 +1241,44 @@ function MyController(hand) { this.grabPointIntersectsEntity = false; this.grabPointSphereOff(); } + + this.processStylus(worldHandPosition); + }; + + this.handleStylusOnHomeButton = function(rayPickInfo) { + if (rayPickInfo.overlayID) { + var homeButton = rayPickInfo.overlayID; + var hmdHomeButton = HMD.homeButtonID; + if (homeButton === hmdHomeButton) { + if (this.homeButtonTouched === false) { + this.homeButtonTouched = true; + Controller.triggerHapticPulse(1, 20, this.hand); + Messages.sendLocalMessage("home", homeButton); + } + } else { + this.homeButtonTouched = false; + } + } else { + this.homeButtonTouched = false; + } + }; + + this.handleLaserOnHomeButton = function(rayPickInfo) { + if (rayPickInfo.overlayID && this.triggerSmoothedGrab()) { + var homeButton = rayPickInfo.overlayID; + var hmdHomeButton = HMD.homeButtonID; + if (homeButton === hmdHomeButton) { + if (this.homeButtonTouched === false) { + this.homeButtonTouched = true; + Controller.triggerHapticPulse(1, 20, this.hand); + Messages.sendLocalMessage("home", homeButton); + } + } else { + this.homeButtonTouched = false; + } + } else { + this.homeButtonTouched = false; + } }; this.clearEquipHaptics = function() { @@ -1176,11 +1298,6 @@ function MyController(hand) { this.prevPotentialEquipHotspot = potentialEquipHotspot; }; - this.heartBeatIsStale = function(data) { - var now = Date.now(); - return data.heartBeat === undefined || now - data.heartBeat > HEART_BEAT_TIMEOUT; - }; - // Performs ray pick test from the hand controller into the world // @param {number} which hand to use, RIGHT_HAND or LEFT_HAND // @returns {object} returns object with two keys entityID and distance @@ -1225,7 +1342,8 @@ function MyController(hand) { searchRay: pickRay, distance: Vec3.distance(pickRay.origin, intersection.intersection), intersection: intersection.intersection, - normal: intersection.surfaceNormal + normal: intersection.surfaceNormal, + properties: intersection.properties }; } else { return result; @@ -1295,14 +1413,16 @@ function MyController(hand) { this.hotspotIsEquippable = function(hotspot) { var props = entityPropertiesCache.getProps(hotspot.entityID); - var grabProps = entityPropertiesCache.getGrabProps(hotspot.entityID); var debug = (WANT_DEBUG_SEARCH_NAME && props.name === WANT_DEBUG_SEARCH_NAME); - var refCount = ("refCount" in grabProps) ? grabProps.refCount : 0; var okToEquipFromOtherHand = ((this.getOtherHandController().state == STATE_NEAR_GRABBING || - this.getOtherHandController().state == STATE_DISTANCE_HOLDING) && - this.getOtherHandController().grabbedEntity == hotspot.entityID); - if (refCount > 0 && !this.heartBeatIsStale(grabProps) && !okToEquipFromOtherHand) { + this.getOtherHandController().state == STATE_DISTANCE_HOLDING) && + this.getOtherHandController().grabbedEntity == hotspot.entityID); + var hasParent = true; + if (props.parentID === NULL_UUID) { + hasParent = false; + } + if ((hasParent || entityHasActions(hotspot.entityID)) && !okToEquipFromOtherHand) { if (debug) { print("equip is skipping '" + props.name + "': grabbed by someone else"); } @@ -1314,26 +1434,13 @@ function MyController(hand) { this.entityIsGrabbable = function(entityID) { var grabbableProps = entityPropertiesCache.getGrabbableProps(entityID); - var grabProps = entityPropertiesCache.getGrabProps(entityID); var props = entityPropertiesCache.getProps(entityID); - var physical = propsArePhysical(props); - var grabbable = false; - var debug = (WANT_DEBUG_SEARCH_NAME && props.name === WANT_DEBUG_SEARCH_NAME); - var refCount = ("refCount" in grabProps) ? grabProps.refCount : 0; - - if (physical) { - // physical things default to grabbable - grabbable = true; - } else { - // non-physical things default to non-grabbable unless they are already grabbed - if (refCount > 0) { - grabbable = true; - } else { - grabbable = false; - } + if (!props) { + return false; } - - if (grabbableProps.hasOwnProperty("grabbable") && refCount === 0) { + var debug = (WANT_DEBUG_SEARCH_NAME && props.name === WANT_DEBUG_SEARCH_NAME); + var grabbable = propsArePhysical(props); + if (grabbableProps.hasOwnProperty("grabbable")) { grabbable = grabbableProps.grabbable; } @@ -1471,12 +1578,12 @@ function MyController(hand) { this.grabbedEntity = null; this.grabbedOverlay = null; this.isInitialGrab = false; - this.shouldResetParentOnRelease = false; this.preparingHoldRelease = false; - this.checkForStrayChildren(); + this.checkForUnexpectedChildren(); if ((this.triggerSmoothedReleased() && this.secondaryReleased())) { + this.grabbedEntity = null; this.setState(STATE_OFF, "trigger released"); return; } @@ -1541,17 +1648,6 @@ function MyController(hand) { } else { // If near something grabbable, grab it! if ((this.triggerSmoothedGrab() || this.secondarySqueezed()) && nearGrabEnabled) { - var props = entityPropertiesCache.getProps(entity); - var grabProps = entityPropertiesCache.getGrabProps(entity); - var refCount = grabProps.refCount ? grabProps.refCount : 0; - if (refCount >= 1) { - // if another person is holding the object, remember to restore the - // parent info, when we are finished grabbing it. - this.shouldResetParentOnRelease = true; - this.previousParentID = props.parentID; - this.previousParentJointIndex = props.parentJointIndex; - } - this.setState(STATE_NEAR_GRABBING, "near grab '" + name + "'"); return; } else { @@ -1560,62 +1656,14 @@ function MyController(hand) { } } - var pointerEvent; - if (rayPickInfo.entityID && Entities.wantsHandControllerPointerEvents(rayPickInfo.entityID)) { - entity = rayPickInfo.entityID; - name = entityPropertiesCache.getProps(entity).name; - - if (Entities.keyboardFocusEntity != entity) { - Overlays.keyboardFocusOverlay = 0; - Entities.keyboardFocusEntity = entity; - - pointerEvent = { - type: "Move", - id: this.hand + 1, // 0 is reserved for hardware mouse - pos2D: projectOntoEntityXYPlane(entity, rayPickInfo.intersection), - pos3D: rayPickInfo.intersection, - normal: rayPickInfo.normal, - direction: rayPickInfo.searchRay.direction, - button: "None" - }; - - this.hoverEntity = entity; - Entities.sendHoverEnterEntity(entity, pointerEvent); - } - - // send mouse events for button highlights and tooltips. - if (this.hand == mostRecentSearchingHand || (this.hand !== mostRecentSearchingHand && - this.getOtherHandController().state !== STATE_SEARCHING && - this.getOtherHandController().state !== STATE_ENTITY_TOUCHING && - this.getOtherHandController().state !== STATE_OVERLAY_TOUCHING)) { - - // most recently searching hand has priority over other hand, for the purposes of button highlighting. - pointerEvent = { - type: "Move", - id: this.hand + 1, // 0 is reserved for hardware mouse - pos2D: projectOntoEntityXYPlane(entity, rayPickInfo.intersection), - pos3D: rayPickInfo.intersection, - normal: rayPickInfo.normal, - direction: rayPickInfo.searchRay.direction, - button: "None" - }; - - Entities.sendMouseMoveOnEntity(entity, pointerEvent); - Entities.sendHoverOverEntity(entity, pointerEvent); - } - - if (this.triggerSmoothedGrab() && !isEditing()) { - this.grabbedEntity = entity; - this.setState(STATE_ENTITY_TOUCHING, "begin touching entity '" + name + "'"); + if (rayPickInfo.distance >= WEB_STYLUS_LENGTH / 2.0 + WEB_TOUCH_Y_OFFSET) { + this.handleLaserOnHomeButton(rayPickInfo); + if (this.handleLaserOnWebEntity(rayPickInfo)) { + return; + } + if (this.handleLaserOnWebOverlay(rayPickInfo)) { return; } - } else if (this.hoverEntity) { - pointerEvent = { - type: "Move", - id: this.hand + 1 - }; - Entities.sendHoverLeaveEntity(this.hoverEntity, pointerEvent); - this.hoverEntity = null; } if (rayPickInfo.entityID) { @@ -1640,6 +1688,228 @@ function MyController(hand) { } } + this.updateEquipHaptics(potentialEquipHotspot, handPosition); + + var nearEquipHotspots = this.chooseNearEquipHotspots(candidateEntities, EQUIP_HOTSPOT_RENDER_RADIUS); + equipHotspotBuddy.updateHotspots(nearEquipHotspots, timestamp); + if (potentialEquipHotspot) { + equipHotspotBuddy.highlightHotspot(potentialEquipHotspot); + } + + if (farGrabEnabled && farSearching) { + this.searchIndicatorOn(rayPickInfo.searchRay); + } + Reticle.setVisible(false); + }; + + this.isTablet = function (entityID) { + if (entityID === HMD.tabletID) { // XXX what's a better way to know this? + return true; + } + return false; + }; + + this.handleStylusOnWebEntity = function (rayPickInfo) { + var pointerEvent; + + if (rayPickInfo.entityID && Entities.wantsHandControllerPointerEvents(rayPickInfo.entityID)) { + var entity = rayPickInfo.entityID; + var name = entityPropertiesCache.getProps(entity).name; + + if (Entities.keyboardFocusEntity != entity) { + Overlays.keyboardFocusOverlay = 0; + Entities.keyboardFocusEntity = entity; + + pointerEvent = { + type: "Move", + id: this.hand + 1, // 0 is reserved for hardware mouse + pos2D: projectOntoEntityXYPlane(entity, rayPickInfo.intersection), + pos3D: rayPickInfo.intersection, + normal: rayPickInfo.normal, + direction: rayPickInfo.searchRay.direction, + button: "None" + }; + + this.hoverEntity = entity; + Entities.sendHoverEnterEntity(entity, pointerEvent); + } + + // send mouse events for button highlights and tooltips. + if (this.hand == mostRecentSearchingHand || + (this.hand !== mostRecentSearchingHand && + this.getOtherHandController().state !== STATE_SEARCHING && + this.getOtherHandController().state !== STATE_ENTITY_STYLUS_TOUCHING && + this.getOtherHandController().state !== STATE_ENTITY_LASER_TOUCHING && + this.getOtherHandController().state !== STATE_OVERLAY_STYLUS_TOUCHING && + this.getOtherHandController().state !== STATE_OVERLAY_LASER_TOUCHING)) { + + // most recently searching hand has priority over other hand, for the purposes of button highlighting. + pointerEvent = { + type: "Move", + id: this.hand + 1, // 0 is reserved for hardware mouse + pos2D: projectOntoEntityXYPlane(entity, rayPickInfo.intersection), + pos3D: rayPickInfo.intersection, + normal: rayPickInfo.normal, + direction: rayPickInfo.searchRay.direction, + button: "None" + }; + + Entities.sendMouseMoveOnEntity(entity, pointerEvent); + Entities.sendHoverOverEntity(entity, pointerEvent); + } + + + this.grabbedEntity = entity; + this.setState(STATE_ENTITY_STYLUS_TOUCHING, "begin touching entity '" + name + "'"); + return true; + + } else if (this.hoverEntity) { + pointerEvent = { + type: "Move", + id: this.hand + 1 + }; + Entities.sendHoverLeaveEntity(this.hoverEntity, pointerEvent); + this.hoverEntity = null; + } + + return false; + }; + + this.handleStylusOnWebOverlay = function (rayPickInfo) { + var pointerEvent; + if (rayPickInfo.overlayID) { + var overlay = rayPickInfo.overlayID; + + if (!this.homeButtonTouched) { + Controller.triggerHapticPulse(1, 20, this.hand); + } + + if (Overlays.keyboardFocusOverlay != overlay) { + Entities.keyboardFocusEntity = null; + Overlays.keyboardFocusOverlay = overlay; + + pointerEvent = { + type: "Move", + id: HARDWARE_MOUSE_ID, + pos2D: projectOntoOverlayXYPlane(overlay, rayPickInfo.intersection), + pos3D: rayPickInfo.intersection, + normal: rayPickInfo.normal, + direction: rayPickInfo.searchRay.direction, + button: "None" + }; + + this.hoverOverlay = overlay; + Overlays.sendHoverEnterOverlay(overlay, pointerEvent); + } + + // Send mouse events for button highlights and tooltips. + if (this.hand == mostRecentSearchingHand || + (this.hand !== mostRecentSearchingHand && + this.getOtherHandController().state !== STATE_SEARCHING && + this.getOtherHandController().state !== STATE_ENTITY_STYLUS_TOUCHING && + this.getOtherHandController().state !== STATE_ENTITY_LASER_TOUCHING && + this.getOtherHandController().state !== STATE_OVERLAY_STYLUS_TOUCHING && + this.getOtherHandController().state !== STATE_OVERLAY_LASER_TOUCHING)) { + + // most recently searching hand has priority over other hand, for the purposes of button highlighting. + pointerEvent = { + type: "Move", + id: HARDWARE_MOUSE_ID, + pos2D: projectOntoOverlayXYPlane(overlay, rayPickInfo.intersection), + pos3D: rayPickInfo.intersection, + normal: rayPickInfo.normal, + direction: rayPickInfo.searchRay.direction, + button: "None" + }; + + Overlays.sendMouseMoveOnOverlay(overlay, pointerEvent); + Overlays.sendHoverOverOverlay(overlay, pointerEvent); + } + + this.grabbedOverlay = overlay; + this.setState(STATE_OVERLAY_STYLUS_TOUCHING, "begin touching overlay '" + overlay + "'"); + return true; + + } else if (this.hoverOverlay) { + pointerEvent = { + type: "Move", + id: HARDWARE_MOUSE_ID + }; + Overlays.sendHoverLeaveOverlay(this.hoverOverlay, pointerEvent); + this.hoverOverlay = null; + } + + return false; + }; + + this.handleLaserOnWebEntity = function(rayPickInfo) { + var pointerEvent; + if (rayPickInfo.entityID && Entities.wantsHandControllerPointerEvents(rayPickInfo.entityID)) { + var entity = rayPickInfo.entityID; + var props = entityPropertiesCache.getProps(entity); + var name = props.name; + + if (Entities.keyboardFocusEntity != entity) { + Overlays.keyboardFocusOverlay = 0; + Entities.keyboardFocusEntity = entity; + + pointerEvent = { + type: "Move", + id: this.hand + 1, // 0 is reserved for hardware mouse + pos2D: projectOntoEntityXYPlane(entity, rayPickInfo.intersection), + pos3D: rayPickInfo.intersection, + normal: rayPickInfo.normal, + direction: rayPickInfo.searchRay.direction, + button: "None" + }; + + this.hoverEntity = entity; + Entities.sendHoverEnterEntity(entity, pointerEvent); + } + + // send mouse events for button highlights and tooltips. + if (this.hand == mostRecentSearchingHand || + (this.hand !== mostRecentSearchingHand && + this.getOtherHandController().state !== STATE_SEARCHING && + this.getOtherHandController().state !== STATE_ENTITY_STYLUS_TOUCHING && + this.getOtherHandController().state !== STATE_ENTITY_LASER_TOUCHING && + this.getOtherHandController().state !== STATE_OVERLAY_STYLUS_TOUCHING && + this.getOtherHandController().state !== STATE_OVERLAY_LASER_TOUCHING)) { + + // most recently searching hand has priority over other hand, for the purposes of button highlighting. + pointerEvent = { + type: "Move", + id: this.hand + 1, // 0 is reserved for hardware mouse + pos2D: projectOntoEntityXYPlane(entity, rayPickInfo.intersection), + pos3D: rayPickInfo.intersection, + normal: rayPickInfo.normal, + direction: rayPickInfo.searchRay.direction, + button: "None" + }; + + Entities.sendMouseMoveOnEntity(entity, pointerEvent); + Entities.sendHoverOverEntity(entity, pointerEvent); + } + + if (this.triggerSmoothedGrab() && (!isEditing() || this.isTablet(entity))) { + this.grabbedEntity = entity; + this.setState(STATE_ENTITY_LASER_TOUCHING, "begin touching entity '" + name + "'"); + return true; + } + } else if (this.hoverEntity) { + pointerEvent = { + type: "Move", + id: this.hand + 1 + }; + Entities.sendHoverLeaveEntity(this.hoverEntity, pointerEvent); + this.hoverEntity = null; + } + + return false; + }; + + this.handleLaserOnWebOverlay = function(rayPickInfo) { + var pointerEvent; var overlay; if (rayPickInfo.overlayID) { @@ -1664,10 +1934,13 @@ function MyController(hand) { } // Send mouse events for button highlights and tooltips. - if (this.hand == mostRecentSearchingHand || (this.hand !== mostRecentSearchingHand && - this.getOtherHandController().state !== STATE_SEARCHING && - this.getOtherHandController().state !== STATE_ENTITY_TOUCHING && - this.getOtherHandController().state !== STATE_OVERLAY_TOUCHING)) { + if (this.hand == mostRecentSearchingHand || + (this.hand !== mostRecentSearchingHand && + this.getOtherHandController().state !== STATE_SEARCHING && + this.getOtherHandController().state !== STATE_ENTITY_STYLUS_TOUCHING && + this.getOtherHandController().state !== STATE_ENTITY_LASER_TOUCHING && + this.getOtherHandController().state !== STATE_OVERLAY_STYLUS_TOUCHING && + this.getOtherHandController().state !== STATE_OVERLAY_LASER_TOUCHING)) { // most recently searching hand has priority over other hand, for the purposes of button highlighting. pointerEvent = { @@ -1684,10 +1957,10 @@ function MyController(hand) { Overlays.sendHoverOverOverlay(overlay, pointerEvent); } - if (this.triggerSmoothedGrab() && !isEditing()) { + if (this.triggerSmoothedGrab()) { this.grabbedOverlay = overlay; - this.setState(STATE_OVERLAY_TOUCHING, "begin touching overlay '" + overlay + "'"); - return; + this.setState(STATE_OVERLAY_LASER_TOUCHING, "begin touching overlay '" + overlay + "'"); + return true; } } else if (this.hoverOverlay) { @@ -1699,18 +1972,7 @@ function MyController(hand) { this.hoverOverlay = null; } - this.updateEquipHaptics(potentialEquipHotspot, handPosition); - - var nearEquipHotspots = this.chooseNearEquipHotspots(candidateEntities, EQUIP_HOTSPOT_RENDER_RADIUS); - equipHotspotBuddy.updateHotspots(nearEquipHotspots, timestamp); - if (potentialEquipHotspot) { - equipHotspotBuddy.highlightHotspot(potentialEquipHotspot); - } - - if (farGrabEnabled && farSearching) { - this.searchIndicatorOn(rayPickInfo.searchRay); - } - Reticle.setVisible(false); + return false; }; this.distanceGrabTimescale = function(mass, distance) { @@ -1778,36 +2040,36 @@ function MyController(hand) { this.actionTimeout = now + (ACTION_TTL * MSECS_PER_SEC); if (this.actionID !== null) { - this.activateEntity(this.grabbedEntity, grabbedProperties, false, true); this.callEntityMethodOnGrabbed("startDistanceGrab"); } Controller.triggerHapticPulse(HAPTIC_PULSE_STRENGTH, HAPTIC_PULSE_DURATION, this.hand); - this.turnOffVisualizations(); - this.previousRoomControllerPosition = roomControllerPosition; }; + this.ensureDynamic = function() { + // if we distance hold something and keep it very still before releasing it, it ends up + // non-dynamic in bullet. If it's too still, give it a little bounce so it will fall. + var props = Entities.getEntityProperties(this.grabbedEntity, ["velocity", "dynamic", "parentID"]); + if (props.dynamic && props.parentID == NULL_UUID) { + var velocity = props.velocity; + if (Vec3.length(velocity) < 0.05) { // see EntityMotionState.cpp DYNAMIC_LINEAR_VELOCITY_THRESHOLD + velocity = { x: 0.0, y: 0.2, z:0.0 }; + Entities.editEntity(this.grabbedEntity, { velocity: velocity }); + } + } + }; + this.distanceHolding = function(deltaTime, timestamp) { if (!this.triggerClicked) { this.callEntityMethodOnGrabbed("releaseGrab"); - - // if we distance hold something and keep it very still before releasing it, it ends up - // non-dynamic in bullet. If it's too still, give it a little bounce so it will fall. - var velocity = Entities.getEntityProperties(this.grabbedEntity, ["velocity"]).velocity; - if (Vec3.length(velocity) < 0.05) { // see EntityMotionState.cpp DYNAMIC_LINEAR_VELOCITY_THRESHOLD - velocity = { x: 0.0, y: 0.2, z:0.0 }; - Entities.editEntity(this.grabbedEntity, { velocity: velocity }); - } - + this.ensureDynamic(); this.setState(STATE_OFF, "trigger released"); return; } - this.heartBeat(this.grabbedEntity); - var controllerLocation = getControllerWorldLocation(this.handToController(), true); var worldControllerPosition = controllerLocation.position; var worldControllerRotation = controllerLocation.orientation; @@ -1840,8 +2102,6 @@ function MyController(hand) { disableMoveWithHead: false }; - var handControllerData = getEntityCustomData('handControllerKey', this.grabbedEntity, defaultMoveWithHeadData); - // Update radialVelocity var lastVelocity = Vec3.multiply(worldHandDelta, 1.0 / deltaObjectTime); var delta = Vec3.normalize(Vec3.subtract(grabbedProperties.position, worldControllerPosition)); @@ -1872,6 +2132,7 @@ function MyController(hand) { newTargetPosition = Vec3.sum(newTargetPosition, worldControllerPosition); var objectToAvatar = Vec3.subtract(this.currentObjectPosition, MyAvatar.position); + var handControllerData = getEntityCustomData('handControllerKey', this.grabbedEntity, defaultMoveWithHeadData); if (handControllerData.disableMoveWithHead !== true) { // mix in head motion if (MOVE_WITH_HEAD) { @@ -2012,19 +2273,11 @@ function MyController(hand) { this.grabbedEntity = saveGrabbedID; } - var otherHandController = this.getOtherHandController(); - if (otherHandController.grabbedEntity == this.grabbedEntity && - (otherHandController.state == STATE_NEAR_GRABBING || otherHandController.state == STATE_DISTANCE_HOLDING)) { - otherHandController.setState(STATE_OFF, "other hand grabbed this entity"); - } - var grabbedProperties = Entities.getEntityProperties(this.grabbedEntity, GRABBABLE_PROPERTIES); - this.activateEntity(this.grabbedEntity, grabbedProperties, false, false); - - var grabbableData = getEntityCustomData(GRABBABLE_DATA_KEY, this.grabbedEntity, DEFAULT_GRABBABLE_DATA); if (FORCE_IGNORE_IK) { this.ignoreIK = true; } else { + var grabbableData = getEntityCustomData(GRABBABLE_DATA_KEY, this.grabbedEntity, DEFAULT_GRABBABLE_DATA); this.ignoreIK = (grabbableData.ignoreIK !== undefined) ? grabbableData.ignoreIK : true; } @@ -2062,14 +2315,10 @@ function MyController(hand) { var currentObjectPosition = grabbedProperties.position; var offset = Vec3.subtract(currentObjectPosition, handPosition); this.offsetPosition = Vec3.multiplyQbyV(Quat.inverse(Quat.multiply(handRotation, this.offsetRotation)), offset); - if (this.temporaryPositionOffset) { - this.offsetPosition = this.temporaryPositionOffset; - // hasPresetPosition = true; - } } var isPhysical = propsArePhysical(grabbedProperties) || entityHasActions(this.grabbedEntity); - if (isPhysical && this.state == STATE_NEAR_GRABBING) { + if (isPhysical && this.state == STATE_NEAR_GRABBING && grabbedProperties.parentID === NULL_UUID) { // grab entity via action if (!this.setupHoldAction()) { return; @@ -2103,6 +2352,15 @@ function MyController(hand) { } Entities.editEntity(this.grabbedEntity, reparentProps); + if (this.thisHandIsParent(grabbedProperties)) { + // this should never happen, but if it does, don't set previous parent to be this hand. + // this.previousParentID[this.grabbedEntity] = NULL; + // this.previousParentJointIndex[this.grabbedEntity] = -1; + } else { + this.previousParentID[this.grabbedEntity] = grabbedProperties.parentID; + this.previousParentJointIndex[this.grabbedEntity] = grabbedProperties.parentJointIndex; + } + Messages.sendMessage('Hifi-Object-Manipulation', JSON.stringify({ action: 'equip', grabbedEntity: this.grabbedEntity, @@ -2111,17 +2369,9 @@ function MyController(hand) { } Entities.editEntity(this.grabbedEntity, { - velocity: { - x: 0, - y: 0, - z: 0 - }, - angularVelocity: { - x: 0, - y: 0, - z: 0 - }, - dynamic: false + velocity: { x: 0, y: 0, z: 0 }, + angularVelocity: { x: 0, y: 0, z: 0 }, + // dynamic: false }); if (this.state == STATE_NEAR_GRABBING) { @@ -2164,8 +2414,8 @@ function MyController(hand) { // we have an equipped object and the secondary trigger was released // short-circuit the other checks and release it this.preparingHoldRelease = false; - - this.release(); + this.callEntityMethodOnGrabbed("releaseEquip"); + this.setState(STATE_OFF, "equipping ended via secondary press"); return; } @@ -2205,9 +2455,7 @@ function MyController(hand) { this.prevDropDetected = dropDetected; } - this.heartBeat(this.grabbedEntity); - - var props = Entities.getEntityProperties(this.grabbedEntity, ["localPosition", "parentID", + var props = Entities.getEntityProperties(this.grabbedEntity, ["localPosition", "parentID", "parentJointIndex", "position", "rotation", "dimensions", "registrationPoint"]); if (!props.position) { @@ -2217,6 +2465,15 @@ function MyController(hand) { return; } + if (this.state == STATE_NEAR_GRABBING && this.actionID === null && !this.thisHandIsParent(props)) { + // someone took it from us or otherwise edited the parentID. end the grab. We don't do this + // for equipped things so that they can be adjusted while equipped. + this.callEntityMethodOnGrabbed("releaseGrab"); + this.grabbedEntity = null; + this.setState(STATE_OFF, "someone took it"); + return; + } + var now = Date.now(); if (this.state == STATE_HOLD && now - this.lastUnequipCheckTime > MSECS_PER_SEC * CHECK_TOO_FAR_UNEQUIP_TIME) { this.lastUnequipCheckTime = now; @@ -2321,8 +2578,11 @@ function MyController(hand) { if (!this.shouldScale) { // If both secondary triggers squeezed, and the non-holding hand is empty, start scaling - if (this.secondarySqueezed() && this.getOtherHandController().secondarySqueezed() && this.getOtherHandController().state === STATE_OFF) { - this.scalingStartDistance = Vec3.length(Vec3.subtract(this.getHandPosition(), this.getOtherHandController().getHandPosition())); + if (this.secondarySqueezed() && + this.getOtherHandController().secondarySqueezed() && + this.getOtherHandController().state === STATE_OFF) { + this.scalingStartDistance = Vec3.length(Vec3.subtract(this.getHandPosition(), + this.getOtherHandController().getHandPosition())); this.scalingStartDimensions = props.dimensions; this.shouldScale = true; } @@ -2330,12 +2590,13 @@ function MyController(hand) { this.shouldScale = false; } if (this.shouldScale) { - var scalingCurrentDistance = Vec3.length(Vec3.subtract(this.getHandPosition(), this.getOtherHandController().getHandPosition())); + var scalingCurrentDistance = Vec3.length(Vec3.subtract(this.getHandPosition(), + this.getOtherHandController().getHandPosition())); var currentRescale = scalingCurrentDistance / this.scalingStartDistance; var newDimensions = Vec3.multiply(currentRescale, this.scalingStartDimensions); - Entities.editEntity(this.grabbedEntity, { dimensions: newDimensions }) + Entities.editEntity(this.grabbedEntity, { dimensions: newDimensions }); } - } + }; this.maybeScaleMyAvatar = function() { if (!myAvatarScalingEnabled) { @@ -2345,7 +2606,8 @@ function MyController(hand) { if (!this.shouldScale) { // If both secondary triggers squeezed, start scaling if (this.secondarySqueezed() && this.getOtherHandController().secondarySqueezed()) { - this.scalingStartDistance = Vec3.length(Vec3.subtract(this.getHandPosition(), this.getOtherHandController().getHandPosition())); + this.scalingStartDistance = Vec3.length(Vec3.subtract(this.getHandPosition(), + this.getOtherHandController().getHandPosition())); this.scalingStartAvatarScale = MyAvatar.scale; this.shouldScale = true; } @@ -2353,11 +2615,12 @@ function MyController(hand) { this.shouldScale = false; } if (this.shouldScale) { - var scalingCurrentDistance = Vec3.length(Vec3.subtract(this.getHandPosition(), this.getOtherHandController().getHandPosition())); + var scalingCurrentDistance = Vec3.length(Vec3.subtract(this.getHandPosition(), + this.getOtherHandController().getHandPosition())); var newAvatarScale = (scalingCurrentDistance / this.scalingStartDistance) * this.scalingStartAvatarScale; MyAvatar.scale = newAvatarScale; } - } + }; this.nearTriggerEnter = function() { this.clearEquipHaptics(); @@ -2375,6 +2638,7 @@ function MyController(hand) { this.nearTrigger = function(deltaTime, timestamp) { if (this.triggerSmoothedReleased()) { this.callEntityMethodOnGrabbed("stopNearTrigger"); + this.grabbedEntity = null; this.setState(STATE_OFF, "trigger released"); return; } @@ -2384,6 +2648,7 @@ function MyController(hand) { this.farTrigger = function(deltaTime, timestamp) { if (this.triggerSmoothedReleased()) { this.callEntityMethodOnGrabbed("stopFarTrigger"); + this.grabbedEntity = null; this.setState(STATE_OFF, "trigger released"); return; } @@ -2400,6 +2665,7 @@ function MyController(hand) { this.lastPickTime = now; if (intersection.entityID != this.grabbedEntity) { this.callEntityMethodOnGrabbed("stopFarTrigger"); + this.grabbedEntity = null; this.setState(STATE_OFF, "laser moved off of entity"); return; } @@ -2442,6 +2708,11 @@ function MyController(hand) { this.touchingEnterPointerEvent = pointerEvent; this.touchingEnterPointerEvent.button = "None"; this.deadspotExpired = false; + + var LASER_PRESS_TO_MOVE_DEADSPOT_ANGLE = 0.026; // radians ~ 1.2 degrees + var STYLUS_PRESS_TO_MOVE_DEADSPOT_ANGLE = 0.314; // radians ~ 18 degrees + var theta = this.state === STATE_ENTITY_STYLUS_TOUCHING ? STYLUS_PRESS_TO_MOVE_DEADSPOT_ANGLE : LASER_PRESS_TO_MOVE_DEADSPOT_ANGLE; + this.deadspotRadius = Math.tan(theta) * intersectInfo.distance; // dead spot radius in meters } }; @@ -2472,6 +2743,8 @@ function MyController(hand) { Entities.sendClickReleaseOnEntity(this.grabbedEntity, pointerEvent); Entities.sendHoverLeaveEntity(this.grabbedEntity, pointerEvent); } + this.grabbedEntity = null; + this.grabbedOverlay = null; }; this.entityTouching = function(dt) { @@ -2480,7 +2753,7 @@ function MyController(hand) { entityPropertiesCache.addEntity(this.grabbedEntity); - if (!this.triggerSmoothedGrab()) { + if (this.state == STATE_ENTITY_LASER_TOUCHING && !this.triggerSmoothedGrab()) { this.setState(STATE_OFF, "released trigger"); return; } @@ -2490,6 +2763,12 @@ function MyController(hand) { getControllerWorldLocation(this.handToController(), true)); if (intersectInfo) { + if (this.state == STATE_ENTITY_STYLUS_TOUCHING && + intersectInfo.distance > WEB_STYLUS_LENGTH / 2.0 + WEB_TOUCH_Y_OFFSET) { + this.setState(STATE_OFF, "pulled away from web entity"); + return; + } + if (Entities.keyboardFocusEntity != this.grabbedEntity) { Overlays.keyboardFocusOverlay = 0; Entities.keyboardFocusEntity = this.grabbedEntity; @@ -2506,21 +2785,21 @@ function MyController(hand) { isPrimaryHeld: true }; - var POINTER_PRESS_TO_MOVE_DELAY = 0.15; // seconds - var POINTER_PRESS_TO_MOVE_DEADSPOT_ANGLE = 0.05; // radians ~ 3 degrees + var POINTER_PRESS_TO_MOVE_DELAY = 0.25; // seconds if (this.deadspotExpired || this.touchingEnterTimer > POINTER_PRESS_TO_MOVE_DELAY || - angleBetween(pointerEvent.direction, this.touchingEnterPointerEvent.direction) > POINTER_PRESS_TO_MOVE_DEADSPOT_ANGLE) { + Vec3.distance(intersectInfo.point, this.touchingEnterPointerEvent.pos3D) > this.deadspotRadius) { Entities.sendMouseMoveOnEntity(this.grabbedEntity, pointerEvent); Entities.sendHoldingClickOnEntity(this.grabbedEntity, pointerEvent); this.deadspotExpired = true; } this.intersectionDistance = intersectInfo.distance; - if (farGrabEnabled) { + if (this.state == STATE_ENTITY_LASER_TOUCHING) { this.searchIndicatorOn(intersectInfo.searchRay); } Reticle.setVisible(false); } else { + this.grabbedEntity = null; this.setState(STATE_OFF, "grabbed entity was destroyed"); return; } @@ -2542,13 +2821,17 @@ function MyController(hand) { isPrimaryHeld: true }; - Overlays.sendMousePressOnOverlay(this.grabbedOverlay, pointerEvent); this.touchingEnterTimer = 0; this.touchingEnterPointerEvent = pointerEvent; this.touchingEnterPointerEvent.button = "None"; this.deadspotExpired = false; + + var LASER_PRESS_TO_MOVE_DEADSPOT_ANGLE = 0.026; // radians ~ 1.2 degrees + var STYLUS_PRESS_TO_MOVE_DEADSPOT_ANGLE = 0.314; // radians ~ 18 degrees + var theta = this.state === STATE_OVERLAY_STYLUS_TOUCHING ? STYLUS_PRESS_TO_MOVE_DEADSPOT_ANGLE : LASER_PRESS_TO_MOVE_DEADSPOT_ANGLE; + this.deadspotRadius = Math.tan(theta) * intersectInfo.distance; // dead spot radius in meters } }; @@ -2558,12 +2841,29 @@ function MyController(hand) { handLaserIntersectOverlay(this.grabbedOverlay, getControllerWorldLocation(this.handToController(), true)); if (intersectInfo) { var pointerEvent; + + var pos2D; + var pos3D; + if (this.tabletStabbed) { + // Some people like to jam the stylus a long ways into the tablet when clicking on a button. + // They almost always move out of the deadzone when they do this. We detect if the stylus + // has gone far through the tablet and suppress any further faux mouse events until the + // stylus is withdrawn. Once it has withdrawn, we do a release click wherever the stylus was + // when it was pushed into the tablet. + this.tabletStabbed = false; + pos2D = this.tabletStabbedPos2D; + pos3D = this.tabletStabbedPos3D; + } else { + pos2D = projectOntoOverlayXYPlane(this.grabbedOverlay, intersectInfo.point); + pos3D = intersectInfo.point; + } + if (this.deadspotExpired) { pointerEvent = { type: "Release", id: HARDWARE_MOUSE_ID, - pos2D: projectOntoOverlayXYPlane(this.grabbedOverlay, intersectInfo.point), - pos3D: intersectInfo.point, + pos2D: pos2D, + pos3D: pos3D, normal: intersectInfo.normal, direction: intersectInfo.searchRay.direction, button: "Primary" @@ -2578,12 +2878,19 @@ function MyController(hand) { Overlays.sendMouseReleaseOnOverlay(this.grabbedOverlay, pointerEvent); Overlays.sendHoverLeaveOverlay(this.grabbedOverlay, pointerEvent); } + this.grabbedEntity = null; + this.grabbedOverlay = null; }; this.overlayTouching = function (dt) { this.touchingEnterTimer += dt; - if (!this.triggerSmoothedGrab()) { + if (this.state == STATE_OVERLAY_STYLUS_TOUCHING && this.triggerSmoothedSqueezed()) { + this.setState(STATE_OFF, "trigger squeezed"); + return; + } + + if (this.state == STATE_OVERLAY_LASER_TOUCHING && !this.triggerSmoothedGrab()) { this.setState(STATE_OFF, "released trigger"); return; } @@ -2593,6 +2900,29 @@ function MyController(hand) { handLaserIntersectOverlay(this.grabbedOverlay, getControllerWorldLocation(this.handToController(), true)); if (intersectInfo) { + if (this.state == STATE_OVERLAY_STYLUS_TOUCHING && + intersectInfo.distance > WEB_STYLUS_LENGTH / 2.0 + WEB_TOUCH_Y_OFFSET + WEB_TOUCH_Y_TOUCH_DEADZONE_SIZE) { + this.grabbedEntity = null; + this.setState(STATE_OFF, "pulled away from overlay"); + return; + } + + var pos2D = projectOntoOverlayXYPlane(this.grabbedOverlay, intersectInfo.point); + var pos3D = intersectInfo.point; + + if (this.state == STATE_OVERLAY_STYLUS_TOUCHING && + !this.deadspotExpired && + intersectInfo.distance < WEB_STYLUS_LENGTH / 2.0 + WEB_TOUCH_TOO_CLOSE) { + // they've stabbed the tablet, don't send events until they pull back + this.tabletStabbed = true; + this.tabletStabbedPos2D = pos2D; + this.tabletStabbedPos3D = pos3D; + return; + } + if (this.tabletStabbed) { + return; + } + if (Overlays.keyboardFocusOverlay != this.grabbedOverlay) { Entities.keyboardFocusEntity = null; Overlays.keyboardFocusOverlay = this.grabbedOverlay; @@ -2601,28 +2931,28 @@ function MyController(hand) { var pointerEvent = { type: "Move", id: HARDWARE_MOUSE_ID, - pos2D: projectOntoOverlayXYPlane(this.grabbedOverlay, intersectInfo.point), - pos3D: intersectInfo.point, + pos2D: pos2D, + pos3D: pos3D, normal: intersectInfo.normal, direction: intersectInfo.searchRay.direction, button: "NoButtons", isPrimaryHeld: true }; - var POINTER_PRESS_TO_MOVE_DELAY = 0.15; // seconds - var POINTER_PRESS_TO_MOVE_DEADSPOT_ANGLE = 0.05; // radians ~ 3 degrees + var POINTER_PRESS_TO_MOVE_DELAY = 0.25; // seconds if (this.deadspotExpired || this.touchingEnterTimer > POINTER_PRESS_TO_MOVE_DELAY || - angleBetween(pointerEvent.direction, this.touchingEnterPointerEvent.direction) > POINTER_PRESS_TO_MOVE_DEADSPOT_ANGLE) { + Vec3.distance(intersectInfo.point, this.touchingEnterPointerEvent.pos3D) > this.deadspotRadius) { Overlays.sendMouseMoveOnOverlay(this.grabbedOverlay, pointerEvent); this.deadspotExpired = true; } this.intersectionDistance = intersectInfo.distance; - if (farGrabEnabled) { + if (this.state == STATE_OVERLAY_LASER_TOUCHING) { this.searchIndicatorOn(intersectInfo.searchRay); } Reticle.setVisible(false); } else { + this.grabbedEntity = null; this.setState(STATE_OFF, "grabbed overlay was destroyed"); return; } @@ -2631,35 +2961,34 @@ function MyController(hand) { this.release = function() { this.turnOffVisualizations(); - var noVelocity = false; if (this.grabbedEntity !== null) { if (this.state === STATE_HOLD) { this.callEntityMethodOnGrabbed("releaseEquip"); } - // Make a small release haptic pulse if we really were holding something + // Make a small release haptic pulse if we really were holding something Controller.triggerHapticPulse(HAPTIC_PULSE_STRENGTH, HAPTIC_PULSE_DURATION, this.hand); - - // If this looks like the release after adjusting something still held in the other hand, print the position - // and rotation of the held thing to help content creators set the userData. - var grabData = getEntityCustomData(GRAB_USER_DATA_KEY, this.grabbedEntity, {}); - this.printNewOffsets = (grabData.refCount > 1); - if (this.actionID !== null) { Entities.deleteAction(this.grabbedEntity, this.actionID); - // sometimes we want things to stay right where they are when we let go. - var releaseVelocityData = getEntityCustomData(GRABBABLE_DATA_KEY, this.grabbedEntity, DEFAULT_GRABBABLE_DATA); - if (releaseVelocityData.disableReleaseVelocity === true || - // this next line allowed both: - // (1) far-grab, pull to self, near grab, then throw - // (2) equip something physical and adjust it with a other-hand grab without the thing drifting - grabData.refCount > 1) { - noVelocity = true; + } else { + // no action, so it's a parenting grab + if (this.previousParentID[this.grabbedEntity] === NULL_UUID) { + Entities.editEntity(this.grabbedEntity, { + parentID: this.previousParentID[this.grabbedEntity], + parentJointIndex: this.previousParentJointIndex[this.grabbedEntity] + }); + this.ensureDynamic(); + } else { + // we're putting this back as a child of some other parent, so zero its velocity + Entities.editEntity(this.grabbedEntity, { + parentID: this.previousParentID[this.grabbedEntity], + parentJointIndex: this.previousParentJointIndex[this.grabbedEntity], + velocity: {x: 0, y: 0, z: 0}, + angularVelocity: {x: 0, y: 0, z: 0} + }); } } - this.deactivateEntity(this.grabbedEntity, noVelocity); - Messages.sendMessage('Hifi-Object-Manipulation', JSON.stringify({ action: 'release', grabbedEntity: this.grabbedEntity, @@ -2682,252 +3011,81 @@ function MyController(hand) { this.grabPointSphereOff(); }; - this.heartBeat = function(entityID) { - var now = Date.now(); - if (now - this.lastHeartBeat > HEART_BEAT_INTERVAL) { - var data = getEntityCustomData(GRAB_USER_DATA_KEY, entityID, {}); - data.heartBeat = now; - setEntityCustomData(GRAB_USER_DATA_KEY, entityID, data); - this.lastHeartBeat = now; - } - }; - - this.resetAbandonedGrab = function(entityID) { - print("cleaning up abandoned grab on " + entityID); - var data = getEntityCustomData(GRAB_USER_DATA_KEY, entityID, {}); - data.refCount = 1; - setEntityCustomData(GRAB_USER_DATA_KEY, entityID, data); - this.deactivateEntity(entityID, false); - }; - - this.activateEntity = function(entityID, grabbedProperties, wasLoaded, collideWithStatic) { - this.autoUnequipCounter = 0; - - if (this.entityActivated) { - return; - } - this.entityActivated = true; - - if (delayedDeactivateTimeout && delayedDeactivateEntityID == entityID) { - // we have a timeout waiting to set collisions with myAvatar back on (so that when something - // is thrown it doesn't collide with the avatar's capsule the moment it's released). We've - // regrabbed the entity before the timeout fired, so cancel the timeout, run the function now - // and adjust the grabbedProperties. This will make the saved set of properties (the ones that - // get re-instated after all the grabs have been released) be correct. - Script.clearTimeout(delayedDeactivateTimeout); - delayedDeactivateTimeout = null; - grabbedProperties.collidesWith = delayedDeactivateFunc(); + this.thisHandIsParent = function(props) { + if (props.parentID != MyAvatar.sessionUUID) { + return false; } - var data = getEntityCustomData(GRAB_USER_DATA_KEY, entityID, {}); - var now = Date.now(); - - if (wasLoaded) { - data.refCount = 1; - } else { - data.refCount = data.refCount ? data.refCount + 1 : 1; - - // zero gravity and set ignoreForCollisions in a way that lets us put them back, after all grabs are done - if (data.refCount == 1) { - data.heartBeat = now; - this.lastHeartBeat = now; - - this.isInitialGrab = true; - data.gravity = grabbedProperties.gravity; - data.collidesWith = grabbedProperties.collidesWith; - data.collisionless = grabbedProperties.collisionless; - data.dynamic = grabbedProperties.dynamic; - data.parentID = wasLoaded ? NULL_UUID : grabbedProperties.parentID; - data.parentJointIndex = grabbedProperties.parentJointIndex; - - var whileHeldProperties = { - gravity: { x: 0, y: 0, z: 0 }, - "collidesWith": collideWithStatic ? - COLLIDES_WITH_WHILE_GRABBED + ",static" : - COLLIDES_WITH_WHILE_GRABBED - }; - Entities.editEntity(entityID, whileHeldProperties); - } else if (data.refCount > 1) { - if (this.heartBeatIsStale(data)) { - // this entity has userData suggesting it is grabbed, but nobody is updating the hearbeat. - // deactivate it before grabbing. - this.resetAbandonedGrab(entityID); - grabbedProperties = Entities.getEntityProperties(this.grabbedEntity, GRABBABLE_PROPERTIES); - return this.activateEntity(entityID, grabbedProperties, wasLoaded, false); - } - - this.isInitialGrab = false; - // if an object is being grabbed by more than one person (or the same person twice, but nevermind), switch - // the collision groups so that it wont collide with "other" avatars. This avoids a situation where two - // people are holding something and one of them will be able (if the other releases at the right time) to - // bootstrap themselves with the held object. This happens because the meaning of "otherAvatar" in - // the collision mask hinges on who the physics simulation owner is. - Entities.editEntity(entityID, { - // "collidesWith": removeAvatarsFromCollidesWith(grabbedProperties.collidesWith) - collisionless: true - }); - } - } - setEntityCustomData(GRAB_USER_DATA_KEY, entityID, data); - return data; - }; - - this.checkForStrayChildren = function() { - // sometimes things can get parented to a hand and this script is unaware. Search for such entities and - // unhook them. var handJointIndex = MyAvatar.getJointIndex(this.hand === RIGHT_HAND ? "RightHand" : "LeftHand"); - var children = Entities.getChildrenIDsOfJoint(MyAvatar.sessionUUID, handJointIndex); + if (props.parentJointIndex == handJointIndex) { + return true; + } + var controllerJointIndex = MyAvatar.getJointIndex(this.hand === RIGHT_HAND ? "_CONTROLLER_RIGHTHAND" : "_CONTROLLER_LEFTHAND"); - children.concat(Entities.getChildrenIDsOfJoint(MyAvatar.sessionUUID, controllerJointIndex)); + if (props.parentJointIndex == controllerJointIndex) { + return true; + } + + var controllerCRJointIndex = MyAvatar.getJointIndex(this.hand === RIGHT_HAND ? + "_CAMERA_RELATIVE_CONTROLLER_RIGHTHAND" : + "_CAMERA_RELATIVE_CONTROLLER_LEFTHAND"); + if (props.parentJointIndex == controllerCRJointIndex) { + return true; + } + + return false; + }; + + this.checkForUnexpectedChildren = function() { + var _this = this; + // sometimes things can get parented to a hand and this script is unaware. Search for such entities and + // unhook them. + + // find children of avatar's hand joint + var handJointIndex = MyAvatar.getJointIndex(this.hand === RIGHT_HAND ? "RightHand" : "LeftHand"); + var children = Entities.getChildrenIDsOfJoint(MyAvatar.sessionUUID, handJointIndex); + // find children of faux controller joint + var controllerJointIndex = MyAvatar.getJointIndex(this.hand === RIGHT_HAND ? + "_CONTROLLER_RIGHTHAND" : + "_CONTROLLER_LEFTHAND"); + children = children.concat(Entities.getChildrenIDsOfJoint(MyAvatar.sessionUUID, controllerJointIndex)); + // find children of faux camera-relative controller joint + var controllerCRJointIndex = MyAvatar.getJointIndex(this.hand === RIGHT_HAND ? + "_CAMERA_RELATIVE_CONTROLLER_RIGHTHAND" : + "_CAMERA_RELATIVE_CONTROLLER_LEFTHAND"); + children = children.concat(Entities.getChildrenIDsOfJoint(MyAvatar.sessionUUID, controllerCRJointIndex)); + children.forEach(function(childID) { - print("disconnecting stray child of hand: (" + _this.hand + ") " + childID); - Entities.editEntity(childID, { - parentID: NULL_UUID - }); - }); - }; + if (childID !== _this.stylus) { + // we appear to be holding something and this script isn't in a state that would be holding something. + // unhook it. if we previously took note of this entity's parent, put it back where it was. This + // works around some problems that happen when more than one hand or avatar is passing something around. + print("disconnecting stray child of hand: (" + _this.hand + ") " + childID); + if (_this.previousParentID[childID]) { + var previousParentID = _this.previousParentID[childID]; + var previousParentJointIndex = _this.previousParentJointIndex[childID]; - this.delayedDeactivateEntity = function(entityID, collidesWith) { - // If, before the grab started, the held entity collided with myAvatar, we do the deactivation in - // two parts. Most of it is done in deactivateEntity(), but the final collidesWith and refcount - // are delayed a bit. This keeps thrown things from colliding with the avatar's capsule so often. - // The refcount is handled in this delayed fashion so things don't get confused if someone else - // grabs the entity before the timeout fires. - Entities.editEntity(entityID, { - collidesWith: collidesWith - }); - var data = getEntityCustomData(GRAB_USER_DATA_KEY, entityID, {}); - if (data && data.refCount) { - data.refCount = data.refCount - 1; - if (data.refCount < 1) { - data = null; - } - } else { - data = null; - } - - setEntityCustomData(GRAB_USER_DATA_KEY, entityID, data); - }; - - this.deactivateEntity = function(entityID, noVelocity, delayed) { - var deactiveProps; - - if (!this.entityActivated) { - return; - } - this.entityActivated = false; - - var data = getEntityCustomData(GRAB_USER_DATA_KEY, entityID, {}); - var doDelayedDeactivate = false; - if (data && data.refCount) { - data.refCount = data.refCount - 1; - if (data.refCount < 1) { - deactiveProps = { - gravity: data.gravity, - // don't set collidesWith myAvatar back right away, because thrown things tend to bounce off the - // avatar's capsule. - collidesWith: removeMyAvatarFromCollidesWith(data.collidesWith), - collisionless: data.collisionless, - dynamic: data.dynamic, - parentID: data.parentID, - parentJointIndex: data.parentJointIndex - }; - - doDelayedDeactivate = (data.collidesWith.indexOf("myAvatar") >= 0); - - if (doDelayedDeactivate) { - var delayedCollidesWith = data.collidesWith; - var delayedEntityID = entityID; - delayedDeactivateFunc = function() { - // set collidesWith back to original value a bit later than the rest - delayedDeactivateTimeout = null; - _this.delayedDeactivateEntity(delayedEntityID, delayedCollidesWith); - return delayedCollidesWith; - }; - delayedDeactivateTimeout = - Script.setTimeout(delayedDeactivateFunc, COLLIDE_WITH_AV_AFTER_RELEASE_DELAY * MSECS_PER_SEC); - delayedDeactivateEntityID = entityID; - } - - // things that are held by parenting and dropped with no velocity will end up as "static" in bullet. If - // it looks like the dropped thing should fall, give it a little velocity. - var props = Entities.getEntityProperties(entityID, ["parentID", "velocity", "dynamic", "shapeType"]); - var parentID = props.parentID; - - if (!noVelocity && - parentID == MyAvatar.sessionUUID && - Vec3.length(data.gravity) > 0.0 && - data.dynamic && - data.parentID == NULL_UUID && - !data.collisionless) { - deactiveProps.velocity = this.currentVelocity; - } - if (noVelocity) { - deactiveProps.velocity = { - x: 0.0, - y: 0.0, - z: 0.0 - }; - deactiveProps.angularVelocity = { - x: 0.0, - y: 0.0, - z: 0.0 - }; - } - - Entities.editEntity(entityID, deactiveProps); - data = null; - } else if (this.shouldResetParentOnRelease) { - // we parent-grabbed this from another parent grab. try to put it back where we found it. - deactiveProps = { - parentID: this.previousParentID, - parentJointIndex: this.previousParentJointIndex, - velocity: { - x: 0.0, - y: 0.0, - z: 0.0 - }, - angularVelocity: { - x: 0.0, - y: 0.0, - z: 0.0 + // The main flaw with keeping track of previous parantage in individual scripts is: + // (1) A grabs something (2) B takes it from A (3) A takes it from B (4) A releases it + // now A and B will take turns passing it back to the other. Detect this and stop the loop here... + var UNHOOK_LOOP_DETECT_MS = 200; + var now = Date.now(); + if (_this.previouslyUnhooked[childID]) { + if (now - _this.previouslyUnhooked[childID] < UNHOOK_LOOP_DETECT_MS) { + previousParentID = NULL_UUID; + previousParentJointIndex = -1; + } } - }; - Entities.editEntity(entityID, deactiveProps); + _this.previouslyUnhooked[childID] = now; - if (this.printNewOffsets) { - var grabbedProperties = Entities.getEntityProperties(this.grabbedEntity, ["localPosition", "localRotation"]); - if (grabbedProperties && grabbedProperties.localPosition && grabbedProperties.localRotation) { - print((this.hand === RIGHT_HAND ? '"LeftHand"' : '"RightHand"') + ":" + - '[{"x":' + grabbedProperties.localPosition.x + ', "y":' + grabbedProperties.localPosition.y + - ', "z":' + grabbedProperties.localPosition.z + '}, {"x":' + grabbedProperties.localRotation.x + - ', "y":' + grabbedProperties.localRotation.y + ', "z":' + grabbedProperties.localRotation.z + - ', "w":' + grabbedProperties.localRotation.w + '}]'); - } + Entities.editEntity(childID, { parentID: previousParentID, parentJointIndex: previousParentJointIndex }); + } else { + Entities.editEntity(childID, { parentID: NULL_UUID }); } - } else if (noVelocity) { - Entities.editEntity(entityID, { - velocity: { - x: 0.0, - y: 0.0, - z: 0.0 - }, - angularVelocity: { - x: 0.0, - y: 0.0, - z: 0.0 - }, - dynamic: data.dynamic - }); } - } else { - data = null; - } - if (!doDelayedDeactivate) { - setEntityCustomData(GRAB_USER_DATA_KEY, entityID, data); - } + }); }; this.getOtherHandController = function() { @@ -3084,7 +3242,7 @@ var handleHandMessages = function(channel, message, sender) { Messages.messageReceived.connect(handleHandMessages); var TARGET_UPDATE_HZ = 60; // 50hz good enough, but we're using update -var BASIC_TIMER_INTERVAL_MS = 1000 / TARGET_UPDATE_HZ; +var BASIC_TIMER_INTERVAL_MS = 1000 / TARGET_UPDATE_HZ; var lastInterval = Date.now(); var intervalCount = 0; @@ -3095,7 +3253,7 @@ var veryhighVarianceCount = 0; var updateTotalWork = 0; var UPDATE_PERFORMANCE_DEBUGGING = false; - + function updateWrapper(){ intervalCount++; @@ -3127,11 +3285,13 @@ function updateWrapper(){ if (intervalCount == 100) { if (UPDATE_PERFORMANCE_DEBUGGING) { - print("handControllerGrab.js -- For " + intervalCount + " samples average= " + totalDelta/intervalCount + " ms" - + " average variance:" + totalVariance/intervalCount + " ms" - + " high variance count:" + highVarianceCount + " [ " + (highVarianceCount/intervalCount) * 100 + "% ] " - + " VERY high variance count:" + veryhighVarianceCount + " [ " + (veryhighVarianceCount/intervalCount) * 100 + "% ] " - + " average work:" + updateTotalWork/intervalCount + " ms"); + print("handControllerGrab.js -- For " + intervalCount + " samples average= " + + totalDelta/intervalCount + " ms" + + " average variance:" + totalVariance/intervalCount + " ms" + + " high variance count:" + highVarianceCount + " [ " + (highVarianceCount/intervalCount) * 100 + "% ] " + + " VERY high variance count:" + veryhighVarianceCount + + " [ " + (veryhighVarianceCount/intervalCount) * 100 + "% ] " + + " average work:" + updateTotalWork/intervalCount + " ms"); } intervalCount = 0; diff --git a/scripts/system/controllers/handControllerPointer.js b/scripts/system/controllers/handControllerPointer.js index 2c732b4cdc..6bebbf0498 100644 --- a/scripts/system/controllers/handControllerPointer.js +++ b/scripts/system/controllers/handControllerPointer.js @@ -436,18 +436,22 @@ clickMapping.from(function () { return wantsMenu; }).to(Controller.Actions.Conte clickMapping.from(Controller.Standard.RightSecondaryThumb).peek().to(function (clicked) { if (clicked) { activeHudPoint2d(Controller.Standard.RightHand); + Messages.sendLocalMessage("toggleHand", Controller.Standard.RightHand); } wantsMenu = clicked; }); clickMapping.from(Controller.Standard.LeftSecondaryThumb).peek().to(function (clicked) { if (clicked) { activeHudPoint2d(Controller.Standard.LeftHand); + Messages.sendLocalMessage("toggleHand", Controller.Standard.LeftHand); } wantsMenu = clicked; }); clickMapping.from(Controller.Standard.Start).peek().to(function (clicked) { if (clicked) { activeHudPoint2dGamePad(); + var noHands = -1; + Messages.sendLocalMessage("toggleHand", noHands); } wantsMenu = clicked; @@ -459,6 +463,8 @@ clickMapping.from(Controller.Hardware.Keyboard.RightMouseClicked).peek().to(func // We don't want the system code to always do this for us, because, e.g., we do not want to get a mouseMove // after the Left/RightSecondaryThumb gives us a context menu. Only from the mouse. Script.setTimeout(function () { + var noHands = -1; + Messages.sendLocalMessage("toggleHand", noHands); Reticle.setPosition(Reticle.position); }, 0); }); diff --git a/scripts/system/edit.js b/scripts/system/edit.js index e1ed4ead65..d49f7ad3c5 100644 --- a/scripts/system/edit.js +++ b/scripts/system/edit.js @@ -170,8 +170,9 @@ var toolBar = (function () { var EDIT_SETTING = "io.highfidelity.isEditting"; // for communication with other scripts var that = {}, toolBar, - systemToolbar, - activeButton; + activeButton = null, + systemToolbar = null, + tablet = null; function createNewEntity(properties) { Settings.setValue(EDIT_SETTING, false); @@ -196,7 +197,12 @@ var toolBar = (function () { function cleanup() { that.setActive(false); - systemToolbar.removeButton(EDIT_TOGGLE_BUTTON); + if (tablet) { + tablet.removeButton(activeButton); + } + if (systemToolbar) { + systemToolbar.removeButton(EDIT_TOGGLE_BUTTON); + } } function addButton(name, image, handler) { @@ -230,16 +236,24 @@ var toolBar = (function () { } }); - systemToolbar = Toolbars.getToolbar(SYSTEM_TOOLBAR); - activeButton = systemToolbar.addButton({ - objectName: EDIT_TOGGLE_BUTTON, - imageURL: TOOLS_PATH + "edit.svg", - visible: true, - alpha: 0.9, - buttonState: 1, - hoverState: 3, - defaultState: 1 - }); + + if (Settings.getValue("HUDUIEnabled")) { + systemToolbar = Toolbars.getToolbar(SYSTEM_TOOLBAR); + activeButton = systemToolbar.addButton({ + objectName: EDIT_TOGGLE_BUTTON, + imageURL: TOOLS_PATH + "edit.svg", + visible: true, + alpha: 0.9, + defaultState: 1 + }); + } else { + tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); + activeButton = tablet.addButton({ + icon: "icons/tablet-icons/edit-i.svg", + text: "EDIT" + }); + } + activeButton.clicked.connect(function() { that.toggle(); }); @@ -439,9 +453,7 @@ var toolBar = (function () { that.toggle = function () { that.setActive(!isActive); - activeButton.writeProperty("buttonState", isActive ? 0 : 1); - activeButton.writeProperty("defaultState", isActive ? 0 : 1); - activeButton.writeProperty("hoverState", isActive ? 2 : 3); + activeButton.editProperties({isActive: isActive}); }; that.setActive = function (active) { @@ -483,7 +495,6 @@ var toolBar = (function () { toolBar.writeProperty("shown", false); toolBar.writeProperty("shown", true); } - // toolBar.selectTool(activeButton, isActive); lightOverlayManager.setVisible(isActive && Menu.isOptionChecked(MENU_SHOW_LIGHTS_IN_EDIT_MODE)); Entities.setDrawZoneBoundaries(isActive && Menu.isOptionChecked(MENU_SHOW_ZONES_IN_EDIT_MODE)); }; @@ -703,7 +714,9 @@ function mouseClickEvent(event) { toolBar.setActive(true); var pickRay = result.pickRay; var foundEntity = result.entityID; - + if (foundEntity === HMD.tabletID) { + return; + } properties = Entities.getEntityProperties(foundEntity); if (isLocked(properties)) { if (wantDebug) { diff --git a/scripts/system/goto.js b/scripts/system/goto.js index 9116142293..95bd05ae73 100644 --- a/scripts/system/goto.js +++ b/scripts/system/goto.js @@ -10,36 +10,50 @@ // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // +/* globals Tablet, Toolbars, Script, HMD, DialogsManager */ (function() { // BEGIN LOCAL_SCOPE -var toolBar = Toolbars.getToolbar("com.highfidelity.interface.toolbar.system"); - - -var button = toolBar.addButton({ - objectName: "goto", - imageURL: Script.resolvePath("assets/images/tools/directory.svg"), - visible: true, - buttonState: 1, - defaultState: 1, - hoverState: 3, - alpha: 0.9, -}); +var button; +var buttonName = "GOTO"; +var toolBar = null; +var tablet = null; function onAddressBarShown(visible) { - button.writeProperty('buttonState', visible ? 0 : 1); - button.writeProperty('defaultState', visible ? 0 : 1); - button.writeProperty('hoverState', visible ? 2 : 3); + button.editProperties({isActive: visible}); } + function onClicked(){ DialogsManager.toggleAddressBar(); } + +if (Settings.getValue("HUDUIEnabled")) { + toolBar = Toolbars.getToolbar("com.highfidelity.interface.toolbar.system"); + button = toolBar.addButton({ + objectName: buttonName, + imageURL: Script.resolvePath("assets/images/tools/directory.svg"), + visible: true, + alpha: 0.9 + }); +} else { + tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); + button = tablet.addButton({ + icon: "icons/tablet-icons/goto-i.svg", + text: buttonName + }); +} + button.clicked.connect(onClicked); DialogsManager.addressBarShown.connect(onAddressBarShown); Script.scriptEnding.connect(function () { - toolBar.removeButton("goto"); button.clicked.disconnect(onClicked); + if (tablet) { + tablet.removeButton(button); + } + if (toolBar) { + toolBar.removeButton(buttonName); + } DialogsManager.addressBarShown.disconnect(onAddressBarShown); }); diff --git a/scripts/system/help.js b/scripts/system/help.js index e79ed0444c..7813780da3 100644 --- a/scripts/system/help.js +++ b/scripts/system/help.js @@ -10,32 +10,77 @@ // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // +/* globals Tablet, Toolbars, Script, HMD, Controller, Menu */ (function() { // BEGIN LOCAL_SCOPE - var toolBar = Toolbars.getToolbar("com.highfidelity.interface.toolbar.system"); - var buttonName = "help"; // matching location reserved in Desktop.qml - var button = toolBar.addButton({ - objectName: buttonName, - imageURL: Script.resolvePath("assets/images/tools/help.svg"), - visible: true, - hoverState: 2, - defaultState: 1, - buttonState: 1, - alpha: 0.9 - }); + var button; + var buttonName = "HELP"; + var toolBar = null; + var tablet = null; + if (Settings.getValue("HUDUIEnabled")) { + toolBar = Toolbars.getToolbar("com.highfidelity.interface.toolbar.system"); + button = toolBar.addButton({ + objectName: buttonName, + imageURL: Script.resolvePath("assets/images/tools/help.svg"), + visible: true, + alpha: 0.9 + }); + } else { + tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); + button = tablet.addButton({ + icon: "icons/tablet-icons/help-i.svg", + text: buttonName + }); + } + var enabled = false; + function onClicked() { + // Similar logic to Application::showHelp() + var defaultTab = "kbm"; + var handControllerName = "vive"; + if (HMD.active) { + if ("Vive" in Controller.Hardware) { + defaultTab = "handControllers"; + handControllerName = "vive"; + } else if ("OculusTouch" in Controller.Hardware) { + defaultTab = "handControllers"; + handControllerName = "oculus"; + } + } else if ("SDL2" in Controller.Hardware) { + defaultTab = "gamepad"; + } - // TODO: make button state reflect whether the window is opened or closed (independently from us). - - function onClicked(){ - Menu.triggerOption('Help...') + if (enabled) { + Menu.closeInfoView('InfoView_html/help.html'); + enabled = !enabled; + button.editProperties({isActive: enabled}); + } else { + Menu.triggerOption('Help...'); + enabled = !enabled; + button.editProperties({isActive: enabled}); + } } button.clicked.connect(onClicked); + var POLL_RATE = 500; + var interval = Script.setInterval(function () { + var visible = Menu.isInfoViewVisible('InfoView_html/help.html'); + if (visible !== enabled) { + enabled = visible; + button.editProperties({isActive: enabled}); + } + }, POLL_RATE); + Script.scriptEnding.connect(function () { - toolBar.removeButton(buttonName); button.clicked.disconnect(onClicked); + Script.clearInterval(interval); + if (tablet) { + tablet.removeButton(button); + } + if (toolBar) { + toolBar.removeButton(buttonName); + } }); }()); // END LOCAL_SCOPE diff --git a/scripts/system/hmd.js b/scripts/system/hmd.js index 5dd06de8eb..61667d6ecc 100644 --- a/scripts/system/hmd.js +++ b/scripts/system/hmd.js @@ -10,6 +10,7 @@ // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // +/*globals HMD, Toolbars, Script, Menu, Tablet, Camera */ (function() { // BEGIN LOCAL_SCOPE @@ -35,15 +36,32 @@ function updateControllerDisplay() { } } -var toolBar = Toolbars.getToolbar("com.highfidelity.interface.toolbar.system"); var button; +var toolBar = null; +var tablet = null; + +if (Settings.getValue("HUDUIEnabled")) { + toolBar = Toolbars.getToolbar("com.highfidelity.interface.toolbar.system"); +} else { + tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); +} + // Independent and Entity mode make people sick. Third Person and Mirror have traps that we need to work through. // Disable them in hmd. var desktopOnlyViews = ['Mirror', 'Independent Mode', 'Entity Mode']; function onHmdChanged(isHmd) { - button.writeProperty('buttonState', isHmd ? 0 : 1); - button.writeProperty('defaultState', isHmd ? 0 : 1); - button.writeProperty('hoverState', isHmd ? 2 : 3); + //TODO change button icon when the hmd changes + if (isHmd) { + button.editProperties({ + icon: "icons/tablet-icons/switch-a.svg", + text: "DESKTOP" + }); + } else { + button.editProperties({ + icon: "icons/tablet-icons/switch-i.svg", + text: "VR" + }); + } desktopOnlyViews.forEach(function (view) { Menu.setMenuEnabled("View>" + view, !isHmd); }); @@ -54,14 +72,19 @@ function onClicked(){ Menu.setIsOptionChecked(isDesktop ? headset : desktopMenuItemName, true); } if (headset) { - button = toolBar.addButton({ - objectName: "hmdToggle", - imageURL: Script.resolvePath("assets/images/tools/switch.svg"), - visible: true, - hoverState: 2, - defaultState: 0, - alpha: 0.9 - }); + if (Settings.getValue("HUDUIEnabled")) { + button = toolBar.addButton({ + objectName: "hmdToggle", + imageURL: Script.resolvePath("assets/images/tools/switch.svg"), + visible: true, + alpha: 0.9 + }); + } else { + button = tablet.addButton({ + icon: "icons/tablet-icons/switch-a.svg", + text: "SWITCH" + }); + } onHmdChanged(HMD.active); button.clicked.connect(onClicked); @@ -69,8 +92,13 @@ if (headset) { Camera.modeUpdated.connect(updateControllerDisplay); Script.scriptEnding.connect(function () { - toolBar.removeButton("hmdToggle"); button.clicked.disconnect(onClicked); + if (tablet) { + tablet.removeButton(button); + } + if (toolBar) { + toolBar.removeButton("hmdToggle"); + } HMD.displayModeChanged.disconnect(onHmdChanged); Camera.modeUpdated.disconnect(updateControllerDisplay); }); diff --git a/scripts/system/libraries/WebTablet.js b/scripts/system/libraries/WebTablet.js index 72be0e583e..75ca2e514f 100644 --- a/scripts/system/libraries/WebTablet.js +++ b/scripts/system/libraries/WebTablet.js @@ -7,84 +7,211 @@ // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // +/* global getControllerWorldLocation, setEntityCustomData, Tablet, WebTablet:true, HMD, Settings, Script, + Vec3, Quat, MyAvatar, Entities, Overlays, Camera, Messages, Xform */ + +Script.include(Script.resolvePath("../libraries/utils.js")); +Script.include(Script.resolvePath("../libraries/controllers.js")); +Script.include(Script.resolvePath("../libraries/Xform.js")); -var RAD_TO_DEG = 180 / Math.PI; var X_AXIS = {x: 1, y: 0, z: 0}; var Y_AXIS = {x: 0, y: 1, z: 0}; -var DEFAULT_DPI = 30; -var DEFAULT_WIDTH = 0.5; +var DEFAULT_DPI = 34; +var DEFAULT_WIDTH = 0.4375; +var DEFAULT_VERTICAL_FIELD_OF_VIEW = 45; // degrees +var SENSOR_TO_ROOM_MATRIX = -2; +var CAMERA_MATRIX = -7; +var ROT_Y_180 = {x: 0, y: 1, z: 0, w: 0}; +var TABLET_TEXTURE_RESOLUTION = { x: 480, y: 706 }; +var INCHES_TO_METERS = 1 / 39.3701; +var NO_HANDS = -1; -var TABLET_URL = "https://s3.amazonaws.com/hifi-public/tony/tablet.fbx"; +var TABLET_URL = Script.resourcesPath() + "meshes/tablet-with-home-button.fbx"; +// will need to be recaclulated if dimensions of fbx model change. +var TABLET_NATURAL_DIMENSIONS = {x: 33.797, y: 50.129, z: 2.269}; +var HOME_BUTTON_TEXTURE = "http://hifi-content.s3.amazonaws.com/alan/dev/tablet-with-home-button.fbx/tablet-with-home-button.fbm/button-close.png"; +var TABLET_MODEL_PATH = "http://hifi-content.s3.amazonaws.com/alan/dev/tablet-with-home-button.fbx"; // returns object with two fields: // * position - position in front of the user // * rotation - rotation of entity so it faces the user. -function calcSpawnInfo() { - var front; - var pitchBackRotation = Quat.angleAxis(20.0, X_AXIS); - if (HMD.active) { - front = Quat.getFront(HMD.orientation); - var yawOnlyRotation = Quat.angleAxis(Math.atan2(front.x, front.z) * RAD_TO_DEG, Y_AXIS); +function calcSpawnInfo(hand, height) { + var finalPosition; + + var headPos = (HMD.active && Camera.mode === "first person") ? HMD.position : Camera.position; + var headRot = (HMD.active && Camera.mode === "first person") ? HMD.orientation : Camera.orientation; + + if (HMD.active && hand !== NO_HANDS) { + var handController = getControllerWorldLocation(hand, true); + var controllerPosition = handController.position; + + // compute the angle of the chord with length (height / 2) + var theta = Math.asin(height / (2 * Vec3.distance(headPos, controllerPosition))); + + // then we can use this angle to rotate the vector between the HMD position and the center of the tablet. + // this vector, u, will become our new look at direction. + var d = Vec3.normalize(Vec3.subtract(headPos, controllerPosition)); + var w = Vec3.normalize(Vec3.cross(Y_AXIS, d)); + var q = Quat.angleAxis(theta * (180 / Math.PI), w); + var u = Vec3.multiplyQbyV(q, d); + + // use u to compute a full lookAt quaternion. + var lookAtRot = Quat.lookAt(controllerPosition, Vec3.sum(controllerPosition, u), Y_AXIS); + + // adjust the tablet position by a small amount. + var yDisplacement = (height / 2) + 0.1; + var zDisplacement = 0.05; + var tabletOffset = Vec3.multiplyQbyV(lookAtRot, {x: 0, y: yDisplacement, z: zDisplacement}); + finalPosition = Vec3.sum(controllerPosition, tabletOffset); return { - position: Vec3.sum(Vec3.sum(HMD.position, Vec3.multiply(0.6, front)), Vec3.multiply(-0.5, Y_AXIS)), - rotation: Quat.multiply(yawOnlyRotation, pitchBackRotation) + position: finalPosition, + rotation: lookAtRot }; } else { - front = Quat.getFront(MyAvatar.orientation); + var front = Quat.getFront(headRot); + finalPosition = Vec3.sum(headPos, Vec3.multiply(0.6, front)); + var orientation = Quat.lookAt({x: 0, y: 0, z: 0}, front, {x: 0, y: 1, z: 0}); return { - position: Vec3.sum(Vec3.sum(MyAvatar.position, Vec3.multiply(0.6, front)), {x: 0, y: 0.6, z: 0}), - rotation: Quat.multiply(MyAvatar.orientation, pitchBackRotation) + position: finalPosition, + rotation: Quat.multiply(orientation, {x: 0, y: 1, z: 0, w: 0}) }; } } -// ctor -WebTablet = function (url, width, dpi, clientOnly) { +/** + * WebTablet + * @param url [string] url of content to show on the tablet. + * @param width [number] width in meters of the tablet model + * @param dpi [number] dpi of web surface used to show the content. + * @param hand [number] -1 indicates no hand, Controller.Standard.RightHand or Controller.Standard.LeftHand + * @param clientOnly [bool] true indicates tablet model is only visible to client. + */ +WebTablet = function (url, width, dpi, hand, clientOnly) { - var ASPECT = 4.0 / 3.0; - var WIDTH = width || DEFAULT_WIDTH; - var TABLET_HEIGHT_SCALE = 640 / 680; // Screen size of tablet entity isn't quite the desired aspect. - var HEIGHT = WIDTH * ASPECT * TABLET_HEIGHT_SCALE; - var DEPTH = 0.025; - var DPI = dpi || DEFAULT_DPI; - var SENSOR_TO_ROOM_MATRIX = -2; + var _this = this; - var spawnInfo = calcSpawnInfo(); - var tabletEntityPosition = spawnInfo.position; - var tabletEntityRotation = spawnInfo.rotation; + // scale factor of natural tablet dimensions. + this.width = width || DEFAULT_WIDTH; + var tabletScaleFactor = this.width / TABLET_NATURAL_DIMENSIONS.x; + this.height = TABLET_NATURAL_DIMENSIONS.y * tabletScaleFactor; + this.depth = TABLET_NATURAL_DIMENSIONS.z * tabletScaleFactor; + this.dpi = dpi || DEFAULT_DPI; - this.tabletEntityID = Entities.addEntity({ - name: "tablet", + var tabletProperties = { + name: "WebTablet Tablet", type: "Model", - modelURL: TABLET_URL, - position: tabletEntityPosition, - rotation: tabletEntityRotation, + modelURL: TABLET_MODEL_PATH, userData: JSON.stringify({ "grabbableKey": {"grabbable": true} }), - dimensions: {x: WIDTH, y: HEIGHT, z: DEPTH}, - parentID: MyAvatar.sessionUUID, - parentJointIndex: SENSOR_TO_ROOM_MATRIX - }, clientOnly); + dimensions: {x: this.width, y: this.height, z: this.depth}, + parentID: MyAvatar.sessionUUID + }; - var WEB_OVERLAY_Z_OFFSET = -0.01; + // compute position, rotation & parentJointIndex of the tablet + this.calculateTabletAttachmentProperties(hand, tabletProperties); - var webOverlayRotation = Quat.multiply(spawnInfo.rotation, Quat.angleAxis(180, Y_AXIS)); - var webOverlayPosition = Vec3.sum(spawnInfo.position, Vec3.multiply(WEB_OVERLAY_Z_OFFSET, Quat.getFront(webOverlayRotation))); + this.cleanUpOldTablets(); + this.tabletEntityID = Entities.addEntity(tabletProperties, clientOnly); + + if (this.webOverlayID) { + Overlays.deleteOverlay(this.webOverlayID); + } + + var WEB_ENTITY_Z_OFFSET = (this.depth / 2); + var WEB_ENTITY_Y_OFFSET = 0.004; this.webOverlayID = Overlays.addOverlay("web3d", { + name: "WebTablet Web", url: url, - position: webOverlayPosition, - rotation: webOverlayRotation, - resolution: { x: 480, y: 640 }, - dpi: DPI, + localPosition: { x: 0, y: WEB_ENTITY_Y_OFFSET, z: -WEB_ENTITY_Z_OFFSET }, + localRotation: Quat.angleAxis(180, Y_AXIS), + resolution: TABLET_TEXTURE_RESOLUTION, + dpi: this.dpi, color: { red: 255, green: 255, blue: 255 }, alpha: 1.0, parentID: this.tabletEntityID, + parentJointIndex: -1, + showKeyboardFocusHighlight: false, + isAA: HMD.active + }); + + var HOME_BUTTON_Y_OFFSET = (this.height / 2) - 0.035; + this.homeButtonEntity = Overlays.addOverlay("sphere", { + name: "homeButton", + localPosition: {x: 0.0, y: -HOME_BUTTON_Y_OFFSET, z: -0.01}, + localRotation: Quat.angleAxis(0, Y_AXIS), + dimensions: { x: 0.04, y: 0.04, z: 0.02}, + alpha: 0.0, + visible: true, + drawInFront: false, + parentID: this.tabletEntityID, parentJointIndex: -1 }); + this.receive = function (channel, senderID, senderUUID, localOnly) { + if (_this.homeButtonEntity === parseInt(senderID)) { + var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); + var onHomeScreen = tablet.onHomeScreen(); + if (onHomeScreen) { + HMD.closeTablet(); + } else { + tablet.gotoHomeScreen(); + _this.setHomeButtonTexture(); + } + } + }; + this.state = "idle"; + + this.getRoot = function() { + return Entities.getWebViewRoot(_this.tabletEntityID); + }; + + this.getLocation = function() { + return Entities.getEntityProperties(_this.tabletEntityID, ["localPosition", "localRotation"]); + }; + this.clicked = false; + + this.myOnHmdChanged = function () { + _this.onHmdChanged(); + }; + HMD.displayModeChanged.connect(this.myOnHmdChanged); + + this.myMousePressEvent = function (event) { + _this.mousePressEvent(event); + }; + + this.myMouseMoveEvent = function (event) { + _this.mouseMoveEvent(event); + }; + + this.myMouseReleaseEvent = function (event) { + _this.mouseReleaseEvent(event); + }; + + Controller.mousePressEvent.connect(this.myMousePressEvent); + Controller.mouseMoveEvent.connect(this.myMouseMoveEvent); + Controller.mouseReleaseEvent.connect(this.myMouseReleaseEvent); + + this.dragging = false; + this.initialLocalIntersectionPoint = {x: 0, y: 0, z: 0}; + this.initialLocalPosition = {x: 0, y: 0, z: 0}; + + this.myGeometryChanged = function (geometry) { + _this.geometryChanged(geometry); + }; + Window.geometryChanged.connect(this.myGeometryChanged); + + this.myCameraModeChanged = function(newMode) { + _this.cameraModeChanged(newMode); + }; + Camera.modeUpdated.connect(this.myCameraModeChanged); +}; + +WebTablet.prototype.setHomeButtonTexture = function() { + print(this.homeButtonEntity); + Entities.editEntity(this.tabletEntityID, {textures: JSON.stringify({"tex.close": HOME_BUTTON_TEXTURE})}); }; WebTablet.prototype.setURL = function (url) { @@ -102,12 +229,119 @@ WebTablet.prototype.getOverlayObject = function () { WebTablet.prototype.destroy = function () { Overlays.deleteOverlay(this.webOverlayID); Entities.deleteEntity(this.tabletEntityID); + Overlays.deleteOverlay(this.homeButtonEntity); + HMD.displayModeChanged.disconnect(this.myOnHmdChanged); + + Controller.mousePressEvent.disconnect(this.myMousePressEvent); + Controller.mouseMoveEvent.disconnect(this.myMouseMoveEvent); + Controller.mouseReleaseEvent.disconnect(this.myMouseReleaseEvent); + + Window.geometryChanged.disconnect(this.myGeometryChanged); + Camera.modeUpdated.disconnect(this.myCameraModeChanged); +}; + +WebTablet.prototype.geometryChanged = function (geometry) { + if (!HMD.active) { + var tabletProperties = {}; + // compute position, rotation & parentJointIndex of the tablet + this.calculateTabletAttachmentProperties(NO_HANDS, tabletProperties); + Entities.editEntity(this.tabletEntityID, tabletProperties); + } +}; + +// calclulate the appropriate position of the tablet in world space, such that it fits in the center of the screen. +// with a bit of padding on the top and bottom. +WebTablet.prototype.calculateWorldAttitudeRelativeToCamera = function () { + var fov = (Settings.getValue('fieldOfView') || DEFAULT_VERTICAL_FIELD_OF_VIEW) * (Math.PI / 180); + var MAX_PADDING_FACTOR = 2.2; + var PADDING_FACTOR = Math.min(Window.innerHeight / TABLET_TEXTURE_RESOLUTION.y, MAX_PADDING_FACTOR); + var TABLET_HEIGHT = (TABLET_TEXTURE_RESOLUTION.y / this.dpi) * INCHES_TO_METERS; + var WEB_ENTITY_Z_OFFSET = (this.depth / 2); + var dist = (PADDING_FACTOR * TABLET_HEIGHT) / (2 * Math.tan(fov / 2)) - WEB_ENTITY_Z_OFFSET; + return { + position: Vec3.sum(Camera.position, Vec3.multiply(dist, Quat.getFront(Camera.orientation))), + rotation: Quat.multiply(Camera.orientation, ROT_Y_180) + }; +}; + +// compute position, rotation & parentJointIndex of the tablet +WebTablet.prototype.calculateTabletAttachmentProperties = function (hand, tabletProperties) { + if (HMD.active) { + // in HMD mode, the tablet should be relative to the sensor to world matrix. + tabletProperties.parentJointIndex = SENSOR_TO_ROOM_MATRIX; + + // compute the appropriate position of the tablet, near the hand controller that was used to spawn it. + var spawnInfo = calcSpawnInfo(hand, this.height); + tabletProperties.position = spawnInfo.position; + tabletProperties.rotation = spawnInfo.rotation; + } else { + // in desktop mode, the tablet should be relative to the camera + tabletProperties.parentJointIndex = CAMERA_MATRIX; + + // compute the appropriate postion of the tablet such that it fits in the center of the screen nicely. + var attitude = this.calculateWorldAttitudeRelativeToCamera(); + tabletProperties.position = attitude.position; + tabletProperties.rotation = attitude.rotation; + } +}; + +WebTablet.prototype.onHmdChanged = function () { + + if (HMD.active) { + Controller.mousePressEvent.disconnect(this.myMousePressEvent); + Controller.mouseMoveEvent.disconnect(this.myMouseMoveEvent); + Controller.mouseReleaseEvent.disconnect(this.myMouseReleaseEvent); + } else { + Controller.mousePressEvent.connect(this.myMousePressEvent); + Controller.mouseMoveEvent.connect(this.myMouseMoveEvent); + Controller.mouseReleaseEvent.connect(this.myMouseReleaseEvent); + } + + var tabletProperties = {}; + // compute position, rotation & parentJointIndex of the tablet + this.calculateTabletAttachmentProperties(NO_HANDS, tabletProperties); + Entities.editEntity(this.tabletEntityID, tabletProperties); + + // Full scene FXAA should be disabled on the overlay when the tablet in desktop mode. + // This should make the text more readable. + Overlays.editOverlay(this.webOverlayID, { isAA: HMD.active }); }; WebTablet.prototype.pickle = function () { return JSON.stringify({ webOverlayID: this.webOverlayID, tabletEntityID: this.tabletEntityID }); }; +WebTablet.prototype.register = function() { + Messages.subscribe("home"); + Messages.messageReceived.connect(this.receive); +}; + +WebTablet.prototype.cleanUpOldTabletsOnJoint = function(jointIndex) { + var children = Entities.getChildrenIDsOfJoint(MyAvatar.sessionUUID, jointIndex); + print("cleanup " + children); + children.forEach(function(childID) { + var props = Entities.getEntityProperties(childID, ["name"]); + if (props.name === "WebTablet Tablet") { + print("cleaning up " + props.name); + Entities.deleteEntity(childID); + } else { + print("not cleaning up " + props.name); + } + }); +}; + +WebTablet.prototype.cleanUpOldTablets = function() { + this.cleanUpOldTabletsOnJoint(-1); + this.cleanUpOldTabletsOnJoint(SENSOR_TO_ROOM_MATRIX); + this.cleanUpOldTabletsOnJoint(CAMERA_MATRIX); + this.cleanUpOldTabletsOnJoint(65529); +}; + +WebTablet.prototype.unregister = function() { + Messages.unsubscribe("home"); + Messages.messageReceived.disconnect(this.receive); +}; + WebTablet.unpickle = function (string) { if (!string) { return; @@ -116,3 +350,82 @@ WebTablet.unpickle = function (string) { tablet.__proto__ = WebTablet.prototype; return tablet; }; + +WebTablet.prototype.getPosition = function () { + return Overlays.getProperty(this.webOverlayID, "position"); +}; + +WebTablet.prototype.mousePressEvent = function (event) { + var pickRay = Camera.computePickRay(event.x, event.y); + var entityPickResults = Entities.findRayIntersection(pickRay, true); // non-accurate picking + if (entityPickResults.intersects && entityPickResults.entityID === this.tabletEntityID) { + var overlayPickResults = Overlays.findRayIntersection(pickRay); + if (overlayPickResults.intersects && overlayPickResults.overlayID === HMD.homeButtonID) { + var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); + var onHomeScreen = tablet.onHomeScreen(); + if (onHomeScreen) { + HMD.closeTablet(); + } else { + tablet.gotoHomeScreen(); + this.setHomeButtonTexture(); + } + } else if (!HMD.active && (!overlayPickResults.intersects || !overlayPickResults.overlayID === this.webOverlayID)) { + this.dragging = true; + var invCameraXform = new Xform(Camera.orientation, Camera.position).inv(); + this.initialLocalIntersectionPoint = invCameraXform.xformPoint(entityPickResults.intersection); + this.initialLocalPosition = Entities.getEntityProperties(this.tabletEntityID, ["localPosition"]).localPosition; + } + } +}; + +WebTablet.prototype.cameraModeChanged = function (newMode) { + // reposition the tablet. + // This allows HMD.position to reflect the new camera mode. + if (HMD.active) { + var self = this; + var tabletProperties = {}; + // compute position, rotation & parentJointIndex of the tablet + self.calculateTabletAttachmentProperties(NO_HANDS, tabletProperties); + Entities.editEntity(self.tabletEntityID, tabletProperties); + } +}; + +function rayIntersectPlane(planePosition, planeNormal, rayStart, rayDirection) { + var rayDirectionDotPlaneNormal = Vec3.dot(rayDirection, planeNormal); + if (rayDirectionDotPlaneNormal > 0.00001 || rayDirectionDotPlaneNormal < -0.00001) { + var rayStartDotPlaneNormal = Vec3.dot(Vec3.subtract(planePosition, rayStart), planeNormal); + var distance = rayStartDotPlaneNormal / rayDirectionDotPlaneNormal; + return {hit: true, distance: distance}; + } else { + // ray is parallel to the plane + return {hit: false, distance: 0}; + } +} + +WebTablet.prototype.mouseMoveEvent = function (event) { + if (this.dragging) { + var pickRay = Camera.computePickRay(event.x, event.y); + + // transform pickRay into camera local coordinates + var invCameraXform = new Xform(Camera.orientation, Camera.position).inv(); + var localPickRay = { + origin: invCameraXform.xformPoint(pickRay.origin), + direction: invCameraXform.xformVector(pickRay.direction) + }; + + var NORMAL = {x: 0, y: 0, z: -1}; + var result = rayIntersectPlane(this.initialLocalIntersectionPoint, NORMAL, localPickRay.origin, localPickRay.direction); + if (result.hit) { + var localIntersectionPoint = Vec3.sum(localPickRay.origin, Vec3.multiply(localPickRay.direction, result.distance)); + var localOffset = Vec3.subtract(localIntersectionPoint, this.initialLocalIntersectionPoint); + var localPosition = Vec3.sum(this.initialLocalPosition, localOffset); + Entities.editEntity(this.tabletEntityID, { + localPosition: localPosition + }); + } + } +}; + +WebTablet.prototype.mouseReleaseEvent = function (event) { + this.dragging = false; +}; diff --git a/scripts/system/libraries/entitySelectionTool.js b/scripts/system/libraries/entitySelectionTool.js index 7fe29bec67..2932417d25 100644 --- a/scripts/system/libraries/entitySelectionTool.js +++ b/scripts/system/libraries/entitySelectionTool.js @@ -1032,9 +1032,16 @@ SelectionDisplay = (function() { var pickRay = controllerComputePickRay(); if (pickRay) { var entityIntersection = Entities.findRayIntersection(pickRay, true); + + var overlayIntersection = Overlays.findRayIntersection(pickRay); if (entityIntersection.intersects && (!overlayIntersection.intersects || (entityIntersection.distance < overlayIntersection.distance))) { + + if (HMD.tabletID == entityIntersection.entityID) { + return; + } + selectionManager.setSelections([entityIntersection.entityID]); } } diff --git a/scripts/system/marketplaces/marketplace.js b/scripts/system/marketplaces/marketplace.js index 894dae7eac..f365ca5d4c 100644 --- a/scripts/system/marketplaces/marketplace.js +++ b/scripts/system/marketplaces/marketplace.js @@ -42,19 +42,11 @@ function shouldShowWebTablet() { } function showMarketplace(marketplaceID) { - if (shouldShowWebTablet()) { - updateButtonState(true); - marketplaceWebTablet = new WebTablet("https://metaverse.highfidelity.com/marketplace", null, null, true); - Settings.setValue(persistenceKey, marketplaceWebTablet.pickle()); - } else { - var url = MARKETPLACE_URL; - if (marketplaceID) { - url = url + "/items/" + marketplaceID; - } - marketplaceWindow.setURL(url); - marketplaceWindow.setVisible(true); + var url = MARKETPLACE_URL; + if (marketplaceID) { + url = url + "/items/" + marketplaceID; } - + tablet.gotoWebScreen(url); marketplaceVisible = true; UserActivityLogger.openedMarketplace(); } @@ -64,20 +56,20 @@ function hideTablet(tablet) { return; } updateButtonState(false); + tablet.unregister(); tablet.destroy(); marketplaceWebTablet = null; Settings.setValue(persistenceKey, ""); } function clearOldTablet() { // If there was a tablet from previous domain or session, kill it and let it be recreated - var tablet = WebTablet.unpickle(Settings.getValue(persistenceKey, "")); - hideTablet(tablet); + } function hideMarketplace() { if (marketplaceWindow.visible) { marketplaceWindow.setVisible(false); marketplaceWindow.setURL("about:blank"); } else if (marketplaceWebTablet) { - hideTablet(marketplaceWebTablet); + } marketplaceVisible = false; } @@ -90,21 +82,15 @@ function toggleMarketplace() { } } -var toolBar = Toolbars.getToolbar("com.highfidelity.interface.toolbar.system"); +var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); -var browseExamplesButton = toolBar.addButton({ - imageURL: toolIconUrl + "market.svg", - objectName: "marketplace", - buttonState: 1, - defaultState: 1, - hoverState: 3, - alpha: 0.9 +var browseExamplesButton = tablet.addButton({ + icon: "icons/tablet-icons/market-i.svg", + text: "MARKET" }); function updateButtonState(visible) { - browseExamplesButton.writeProperty('buttonState', visible ? 0 : 1); - browseExamplesButton.writeProperty('defaultState', visible ? 0 : 1); - browseExamplesButton.writeProperty('hoverState', visible ? 2 : 3); + } function onMarketplaceWindowVisibilityChanged() { updateButtonState(marketplaceWindow.visible); @@ -123,8 +109,8 @@ clearOldTablet(); // Run once at startup, in case there's anything laying around // but the HUD version stays around, so lets do the same. Script.scriptEnding.connect(function () { - toolBar.removeButton("marketplace"); browseExamplesButton.clicked.disconnect(onClick); + tablet.removeButton(browseExamplesButton); marketplaceWindow.visibleChanged.disconnect(onMarketplaceWindowVisibilityChanged); }); diff --git a/scripts/system/marketplaces/marketplaces.js b/scripts/system/marketplaces/marketplaces.js index d5530e7db2..8cb13cf27e 100644 --- a/scripts/system/marketplaces/marketplaces.js +++ b/scripts/system/marketplaces/marketplaces.js @@ -8,17 +8,18 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // +/* global Tablet, Script, HMD, Toolbars, UserActivityLogger, Entities */ +/* eslint indent: ["error", 4, { "outerIIFEBody": 0 }] */ + (function() { // BEGIN LOCAL_SCOPE -/* global WebTablet */ Script.include("../libraries/WebTablet.js"); -var toolIconUrl = Script.resolvePath("../assets/images/tools/"); - var MARKETPLACE_URL = "https://metaverse.highfidelity.com/marketplace"; var MARKETPLACE_URL_INITIAL = MARKETPLACE_URL + "?"; // Append "?" to signal injected script that it's the initial page. var MARKETPLACES_URL = Script.resolvePath("../html/marketplaces.html"); var MARKETPLACES_INJECT_SCRIPT_URL = Script.resolvePath("../html/js/marketplacesInject.js"); +var HOME_BUTTON_TEXTURE = "http://hifi-content.s3.amazonaws.com/alan/dev/tablet-with-home-button.fbx/tablet-with-home-button.fbm/button-root.png"; // Event bridge messages. var CLARA_IO_DOWNLOAD = "CLARA.IO DOWNLOAD"; @@ -30,6 +31,8 @@ var QUERY_CAN_WRITE_ASSETS = "QUERY_CAN_WRITE_ASSETS"; var CAN_WRITE_ASSETS = "CAN_WRITE_ASSETS"; var WARN_USER_NO_PERMISSIONS = "WARN_USER_NO_PERMISSIONS"; +var marketplaceWindow = null; + var CLARA_DOWNLOAD_TITLE = "Preparing Download"; var messageBox = null; var isDownloadBeingCancelled = false; @@ -39,194 +42,119 @@ var NO_BUTTON = 0; // QMessageBox::NoButton var NO_PERMISSIONS_ERROR_MESSAGE = "Cannot download model because you can't write to \nthe domain's Asset Server."; -var marketplaceWindow = new OverlayWebWindow({ - title: "Marketplace", - source: "about:blank", - width: 900, - height: 700, - visible: false -}); -marketplaceWindow.setScriptURL(MARKETPLACES_INJECT_SCRIPT_URL); - -function onWebEventReceived(message) { - if (message === GOTO_DIRECTORY) { - var url = MARKETPLACES_URL; - if (marketplaceWindow.visible) { - marketplaceWindow.setURL(url); - } - if (marketplaceWebTablet) { - marketplaceWebTablet.setURL(url); - } - return; - } - if (message === QUERY_CAN_WRITE_ASSETS) { - var canWriteAssets = CAN_WRITE_ASSETS + " " + Entities.canWriteAssets(); - if (marketplaceWindow.visible) { - marketplaceWindow.emitScriptEvent(canWriteAssets); - } - if (marketplaceWebTablet) { - marketplaceWebTablet.getOverlayObject().emitScriptEvent(canWriteAssets); - } - return; - } - if (message === WARN_USER_NO_PERMISSIONS) { - Window.alert(NO_PERMISSIONS_ERROR_MESSAGE); - return; - } - - if (message.slice(0, CLARA_IO_STATUS.length) === CLARA_IO_STATUS) { - if (isDownloadBeingCancelled) { - return; - } - - var text = message.slice(CLARA_IO_STATUS.length); - if (messageBox === null) { - messageBox = Window.openMessageBox(CLARA_DOWNLOAD_TITLE, text, CANCEL_BUTTON, NO_BUTTON); - } else { - Window.updateMessageBox(messageBox, CLARA_DOWNLOAD_TITLE, text, CANCEL_BUTTON, NO_BUTTON); - } - return; - } - - if (message.slice(0, CLARA_IO_DOWNLOAD.length) === CLARA_IO_DOWNLOAD) { - if (messageBox !== null) { - Window.closeMessageBox(messageBox); - messageBox = null; - } - return; - } - - if (message === CLARA_IO_CANCELLED_DOWNLOAD) { - isDownloadBeingCancelled = false; - } -} - -marketplaceWindow.webEventReceived.connect(onWebEventReceived); - function onMessageBoxClosed(id, button) { if (id === messageBox && button === CANCEL_BUTTON) { isDownloadBeingCancelled = true; messageBox = null; - marketplaceWindow.emitScriptEvent(CLARA_IO_CANCEL_DOWNLOAD); + tablet.emitScriptEvent(CLARA_IO_CANCEL_DOWNLOAD); } } Window.messageBoxClosed.connect(onMessageBoxClosed); -var toolHeight = 50; -var toolWidth = 50; -var TOOLBAR_MARGIN_Y = 0; -var marketplaceVisible = false; -var marketplaceWebTablet; - -// We persist clientOnly data in the .ini file, and reconstitute it on restart. -// To keep things consistent, we pickle the tablet data in Settings, and kill any existing such on restart and domain change. -var persistenceKey = "io.highfidelity.lastDomainTablet"; - -function shouldShowWebTablet() { - var rightPose = Controller.getPoseValue(Controller.Standard.RightHand); - var leftPose = Controller.getPoseValue(Controller.Standard.LeftHand); - var hasHydra = !!Controller.Hardware.Hydra; - return HMD.active && (leftPose.valid || rightPose.valid || hasHydra); -} - function showMarketplace() { - if (shouldShowWebTablet()) { - updateButtonState(true); - marketplaceWebTablet = new WebTablet(MARKETPLACE_URL_INITIAL, null, null, true); - Settings.setValue(persistenceKey, marketplaceWebTablet.pickle()); - marketplaceWebTablet.setScriptURL(MARKETPLACES_INJECT_SCRIPT_URL); - marketplaceWebTablet.getOverlayObject().webEventReceived.connect(onWebEventReceived); + UserActivityLogger.openedMarketplace(); + + if (tablet) { + tablet.gotoWebScreen(MARKETPLACE_URL_INITIAL, MARKETPLACES_INJECT_SCRIPT_URL); + tablet.webEventReceived.connect(function (message) { + if (message === GOTO_DIRECTORY) { + tablet.gotoWebScreen(MARKETPLACES_URL); + } + + if (message === QUERY_CAN_WRITE_ASSETS) { + tablet.emitScriptEvent(CAN_WRITE_ASSETS + " " + Entities.canWriteAssets()); + } + + if (message === WARN_USER_NO_PERMISSIONS) { + Window.alert(NO_PERMISSIONS_ERROR_MESSAGE); + } + + if (message.slice(0, CLARA_IO_STATUS.length) === CLARA_IO_STATUS) { + if (isDownloadBeingCancelled) { + return; + } + + var text = message.slice(CLARA_IO_STATUS.length); + if (messageBox === null) { + messageBox = Window.openMessageBox(CLARA_DOWNLOAD_TITLE, text, CANCEL_BUTTON, NO_BUTTON); + } else { + Window.updateMessageBox(messageBox, CLARA_DOWNLOAD_TITLE, text, CANCEL_BUTTON, NO_BUTTON); + } + return; + } + + if (message.slice(0, CLARA_IO_DOWNLOAD.length) === CLARA_IO_DOWNLOAD) { + if (messageBox !== null) { + Window.closeMessageBox(messageBox); + messageBox = null; + } + return; + } + + if (message === CLARA_IO_CANCELLED_DOWNLOAD) { + isDownloadBeingCancelled = false; + } + }); } else { marketplaceWindow.setURL(MARKETPLACE_URL_INITIAL); marketplaceWindow.setVisible(true); + marketplaceVisible = true; } - - marketplaceVisible = true; - UserActivityLogger.openedMarketplace(); } -function hideTablet(tablet) { - if (!tablet) { - return; - } - updateButtonState(false); - tablet.destroy(); - marketplaceWebTablet = null; - Settings.setValue(persistenceKey, ""); -} -function clearOldTablet() { // If there was a tablet from previous domain or session, kill it and let it be recreated - var tablet = WebTablet.unpickle(Settings.getValue(persistenceKey, "")); - hideTablet(tablet); -} -function hideMarketplace() { - if (marketplaceWindow.visible) { - marketplaceWindow.setVisible(false); - marketplaceWindow.setURL("about:blank"); - } else if (marketplaceWebTablet) { - hideTablet(marketplaceWebTablet); - } - marketplaceVisible = false; -} -marketplaceWindow.closed.connect(function () { - marketplaceWindow.setURL("about:blank"); -}); - function toggleMarketplace() { - if (marketplaceVisible) { - hideMarketplace(); - } else { - showMarketplace(); - } + var entity = HMD.tabletID; + Entities.editEntity(entity, {textures: JSON.stringify({"tex.close": HOME_BUTTON_TEXTURE})}); + showMarketplace(); } -var toolBar = Toolbars.getToolbar("com.highfidelity.interface.toolbar.system"); - -var browseExamplesButton = toolBar.addButton({ - imageURL: toolIconUrl + "market.svg", - objectName: "marketplace", - buttonState: 1, - defaultState: 1, - hoverState: 3, - alpha: 0.9 -}); - -function updateButtonState(visible) { - browseExamplesButton.writeProperty('buttonState', visible ? 0 : 1); - browseExamplesButton.writeProperty('defaultState', visible ? 0 : 1); - browseExamplesButton.writeProperty('hoverState', visible ? 2 : 3); -} -function onMarketplaceWindowVisibilityChanged() { - updateButtonState(marketplaceWindow.visible); - marketplaceVisible = marketplaceWindow.visible; +var tablet = null; +var toolBar = null; +var marketplaceButton = null; +if (Settings.getValue("HUDUIEnabled")) { + marketplaceWindow = new OverlayWebWindow({ + title: "Marketplace", + source: "about:blank", + width: 900, + height: 700, + visible: false + }); + marketplaceWindow.setScriptURL(MARKETPLACES_INJECT_SCRIPT_URL); + toolBar = Toolbars.getToolbar("com.highfidelity.interface.toolbar.system"); + var toolIconUrl = Script.resolvePath("../assets/images/tools/"); + marketplaceButton = toolBar.addButton({ + imageURL: toolIconUrl + "market.svg", + objectName: "marketplace", + alpha: 0.9 + }); +} else { + tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); + marketplaceButton = tablet.addButton({ + icon: "icons/tablet-icons/market-i.svg", + text: "MARKET" + }); } function onCanWriteAssetsChanged() { var message = CAN_WRITE_ASSETS + " " + Entities.canWriteAssets(); - if (marketplaceWindow.visible) { - marketplaceWindow.emitScriptEvent(message); - } - if (marketplaceWebTablet) { - marketplaceWebTablet.getOverlayObject().emitScriptEvent(message); - } + tablet.emitScriptEvent(message); } function onClick() { toggleMarketplace(); } -browseExamplesButton.clicked.connect(onClick); -marketplaceWindow.visibleChanged.connect(onMarketplaceWindowVisibilityChanged); +marketplaceButton.clicked.connect(onClick); Entities.canWriteAssetsChanged.connect(onCanWriteAssetsChanged); -clearOldTablet(); // Run once at startup, in case there's anything laying around from a crash. -// We could also optionally do something like Window.domainChanged.connect(function () {Script.setTimeout(clearOldTablet, 2000)}), -// but the HUD version stays around, so lets do the same. - Script.scriptEnding.connect(function () { - toolBar.removeButton("marketplace"); - browseExamplesButton.clicked.disconnect(onClick); - marketplaceWindow.visibleChanged.disconnect(onMarketplaceWindowVisibilityChanged); + if (toolBar) { + toolBar.removeButton("marketplace"); + } + if (tablet) { + tablet.removeButton(marketplaceButton); + } Entities.canWriteAssetsChanged.disconnect(onCanWriteAssetsChanged); }); diff --git a/scripts/system/menu.js b/scripts/system/menu.js new file mode 100644 index 0000000000..1b67a7995c --- /dev/null +++ b/scripts/system/menu.js @@ -0,0 +1,33 @@ +// +// menu.js +// scripts/system/ +// +// Created by Dante Ruiz on 5 Jun 2017 +// Copyright 2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +var HOME_BUTTON_TEXTURE = "http://hifi-content.s3.amazonaws.com/alan/dev/tablet-with-home-button.fbx/tablet-with-home-button.fbm/button-root.png"; +(function() { + var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); + var button = tablet.addButton({ + icon: "icons/tablet-icons/menu-i.svg", + text: "MENU" + }); + + + function onClicked() { + var entity = HMD.tabletID; + Entities.editEntity(entity, {textures: JSON.stringify({"tex.close": HOME_BUTTON_TEXTURE})}); + tablet.gotoMenuScreen(); + } + + button.clicked.connect(onClicked); + + Script.scriptEnding.connect(function () { + button.clicked.disconnect(onClicked); + tablet.removeButton(button); + }) +}()); \ No newline at end of file diff --git a/scripts/system/mod.js b/scripts/system/mod.js index 7e5cc5d2a5..a3b4974f8d 100644 --- a/scripts/system/mod.js +++ b/scripts/system/mod.js @@ -18,29 +18,24 @@ Script.include("/~/system/libraries/controllers.js"); // grab the toolbar -var toolbar = Toolbars.getToolbar("com.highfidelity.interface.toolbar.system"); +var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); var ASSETS_PATH = Script.resolvePath("assets"); var TOOLS_PATH = Script.resolvePath("assets/images/tools/"); function buttonImageURL() { - return TOOLS_PATH + (Users.canKick ? 'kick.svg' : 'ignore.svg'); + return (Users.canKick ? "kick.svg" : "ignore.svg"); } // setup the mod button and add it to the toolbar -var button = toolbar.addButton({ - objectName: 'mod', - imageURL: buttonImageURL(), - visible: true, - buttonState: 1, - defaultState: 1, - hoverState: 3, - alpha: 0.9 +var button = tablet.addButton({ + icon: "icons/tablet-icons/ignore-i.svg", + text: "KICK" }); // if this user's kick permissions change, change the state of the button in the HUD Users.canKickChanged.connect(function(canKick){ - button.writeProperty('imageURL', buttonImageURL()); + button.editProperties({text: buttonImageURL()}); }); var isShowingOverlays = false; @@ -69,9 +64,7 @@ function buttonClicked(){ isShowingOverlays = true; } - button.writeProperty('buttonState', isShowingOverlays ? 0 : 1); - button.writeProperty('defaultState', isShowingOverlays ? 0 : 1); - button.writeProperty('hoverState', isShowingOverlays ? 2 : 3); + } button.clicked.connect(buttonClicked); @@ -251,7 +244,7 @@ triggerMapping.enable(); // cleanup the toolbar button and overlays when script is stopped Script.scriptEnding.connect(function() { - toolbar.removeButton('mod'); + tablet.removeButton(button); removeOverlays(); triggerMapping.disable(); }); diff --git a/scripts/system/mute.js b/scripts/system/mute.js index 722ed65b3d..fff40eb883 100644 --- a/scripts/system/mute.js +++ b/scripts/system/mute.js @@ -13,39 +13,50 @@ (function() { // BEGIN LOCAL_SCOPE -var toolBar = Toolbars.getToolbar("com.highfidelity.interface.toolbar.system"); - -var button = toolBar.addButton({ - objectName: "mute", - imageURL: Script.resolvePath("assets/images/tools/mic.svg"), - visible: true, - buttonState: 1, - defaultState: 1, - hoverState: 3, - alpha: 0.9 -}); +var button; +var buttonName = "MUTE"; +var toolBar = null; +var tablet = null; function onMuteToggled() { - // We could just toggle state, but we're less likely to get out of wack if we read the AudioDevice. - // muted => button "on" state => 1. go figure. - var state = AudioDevice.getMuted() ? 0 : 1; - var hoverState = AudioDevice.getMuted() ? 2 : 3; - button.writeProperty('buttonState', state); - button.writeProperty('defaultState', state); - button.writeProperty('hoverState', hoverState); + button.editProperties({isActive: AudioDevice.getMuted()}); } -onMuteToggled(); function onClicked(){ var menuItem = "Mute Microphone"; Menu.setIsOptionChecked(menuItem, !Menu.isOptionChecked(menuItem)); } + +if (Settings.getValue("HUDUIEnabled")) { + toolBar = Toolbars.getToolbar("com.highfidelity.interface.toolbar.system"); + button = toolBar.addButton({ + objectName: buttonName, + imageURL: Script.resolvePath("assets/images/tools/mic.svg"), + visible: true, + alpha: 0.9 + }); +} else { + tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); + button = tablet.addButton({ + icon: "icons/tablet-icons/mic-a.svg", + text: buttonName, + activeIcon: "icons/tablet-icons/mic-i.svg", + activeText: "UNMUTE" + }); +} +onMuteToggled(); + button.clicked.connect(onClicked); AudioDevice.muteToggled.connect(onMuteToggled); Script.scriptEnding.connect(function () { - toolBar.removeButton("mute"); button.clicked.disconnect(onClicked); AudioDevice.muteToggled.disconnect(onMuteToggled); + if (tablet) { + tablet.removeButton(button); + } + if (toolBar) { + toolBar.removeButton(buttonName); + } }); }()); // END LOCAL_SCOPE diff --git a/scripts/system/pal.js b/scripts/system/pal.js index f148ad5fdb..800a1a8c42 100644 --- a/scripts/system/pal.js +++ b/scripts/system/pal.js @@ -474,6 +474,58 @@ triggerMapping.from(Controller.Standard.RTClick).peek().to(makeClickHandler(Cont triggerMapping.from(Controller.Standard.LTClick).peek().to(makeClickHandler(Controller.Standard.LeftHand)); triggerPressMapping.from(Controller.Standard.RT).peek().to(makePressHandler(Controller.Standard.RightHand)); triggerPressMapping.from(Controller.Standard.LT).peek().to(makePressHandler(Controller.Standard.LeftHand)); +// +// Manage the connection between the button and the window. +// +var button; +var buttonName = "PEOPLE"; +var tablet = null; +var toolBar = null; +if (Settings.getValue("HUDUIEnabled")) { + toolBar = Toolbars.getToolbar("com.highfidelity.interface.toolbar.system"); + button = toolBar.addButton({ + objectName: buttonName, + imageURL: Script.resolvePath("assets/images/tools/people.svg"), + visible: true, + alpha: 0.9 + }); +} else { + tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); + button = tablet.addButton({ + text: buttonName, + icon: "icons/tablet-icons/people-i.svg" + }); +} +var isWired = false; +function off() { + if (isWired) { // It is not ok to disconnect these twice, hence guard. + Script.update.disconnect(updateOverlays); + Controller.mousePressEvent.disconnect(handleMouseEvent); + Controller.mouseMoveEvent.disconnect(handleMouseMoveEvent); + isWired = false; + } + triggerMapping.disable(); // It's ok if we disable twice. + triggerPressMapping.disable(); // see above + removeOverlays(); + Users.requestsDomainListData = false; +} +function onClicked() { + if (!pal.visible) { + Users.requestsDomainListData = true; + populateUserList(); + pal.raise(); + isWired = true; + Script.update.connect(updateOverlays); + Controller.mousePressEvent.connect(handleMouseEvent); + Controller.mouseMoveEvent.connect(handleMouseMoveEvent); + triggerMapping.enable(); + triggerPressMapping.enable(); + } else { + off(); + } + pal.setVisible(!pal.visible); +} + // // Message from other scripts, such as edit.js // @@ -554,62 +606,6 @@ function createAudioInterval() { }, AUDIO_LEVEL_UPDATE_INTERVAL_MS); } -// -// Manage the connection between the button and the window. -// -var toolBar = Toolbars.getToolbar("com.highfidelity.interface.toolbar.system"); -var buttonName = "pal"; -var button = toolBar.addButton({ - objectName: buttonName, - imageURL: Script.resolvePath("assets/images/tools/people.svg"), - visible: true, - hoverState: 2, - defaultState: 1, - buttonState: 1, - alpha: 0.9 -}); - -var isWired = false; -var palOpenedAt; - -function off() { - if (isWired) { // It is not ok to disconnect these twice, hence guard. - Script.update.disconnect(updateOverlays); - Controller.mousePressEvent.disconnect(handleMouseEvent); - Controller.mouseMoveEvent.disconnect(handleMouseMoveEvent); - isWired = false; - } - triggerMapping.disable(); // It's ok if we disable twice. - triggerPressMapping.disable(); // see above - removeOverlays(); - Users.requestsDomainListData = false; - if (palOpenedAt) { - var duration = new Date().getTime() - palOpenedAt; - UserActivityLogger.palOpened(duration / 1000.0); - palOpenedAt = 0; // just a falsy number is good enough. - } - if (audioInterval) { - Script.clearInterval(audioInterval); - } -} -function onClicked() { - if (!pal.visible) { - Users.requestsDomainListData = true; - populateUserList(); - pal.raise(); - isWired = true; - Script.update.connect(updateOverlays); - Controller.mousePressEvent.connect(handleMouseEvent); - Controller.mouseMoveEvent.connect(handleMouseMoveEvent); - triggerMapping.enable(); - triggerPressMapping.enable(); - createAudioInterval(); - palOpenedAt = new Date().getTime(); - } else { - off(); - } - pal.setVisible(!pal.visible); -} function avatarDisconnected(nodeID) { // remove from the pal list pal.sendToQml({method: 'avatarDisconnected', params: [nodeID]}); @@ -618,9 +614,7 @@ function avatarDisconnected(nodeID) { // Button state. // function onVisibleChanged() { - button.writeProperty('buttonState', pal.visible ? 0 : 1); - button.writeProperty('defaultState', pal.visible ? 0 : 1); - button.writeProperty('hoverState', pal.visible ? 2 : 3); + button.editProperties({isActive: pal.visible}); } button.clicked.connect(onClicked); pal.visibleChanged.connect(onVisibleChanged); @@ -642,7 +636,12 @@ Window.domainConnectionRefused.connect(clearLocalQMLDataAndClosePAL); // Script.scriptEnding.connect(function () { button.clicked.disconnect(onClicked); - toolBar.removeButton(buttonName); + if (tablet) { + tablet.removeButton(button); + } + if (toolBar) { + toolBar.removeButton(buttonName); + } pal.visibleChanged.disconnect(onVisibleChanged); pal.closed.disconnect(off); Users.usernameFromIDReply.disconnect(usernameFromIDReply); diff --git a/scripts/system/snapshot.js b/scripts/system/snapshot.js index d79a6e46cb..ed22b60242 100644 --- a/scripts/system/snapshot.js +++ b/scripts/system/snapshot.js @@ -7,23 +7,38 @@ // Distributed under the Apache License, Version 2.0 // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // +/* globals Tablet, Toolbars, Script, HMD, Settings, DialogsManager, Menu, Reticle, OverlayWebWindow, Desktop, Account, MyAvatar */ (function() { // BEGIN LOCAL_SCOPE var SNAPSHOT_DELAY = 500; // 500ms -var toolBar = Toolbars.getToolbar("com.highfidelity.interface.toolbar.system"); +var FINISH_SOUND_DELAY = 350; var resetOverlays; var reticleVisible; var clearOverlayWhenMoving; -var button = toolBar.addButton({ - objectName: "snapshot", - imageURL: Script.resolvePath("assets/images/tools/snap.svg"), - visible: true, - buttonState: 1, - defaultState: 1, - hoverState: 2, - alpha: 0.9, -}); + +var button; +var buttonName = "SNAP"; +var tablet = null; +var toolBar = null; + +var buttonConnected = false; + +if (Settings.getValue("HUDUIEnabled")) { + toolBar = Toolbars.getToolbar("com.highfidelity.interface.toolbar.system"); + button = toolBar.addButton({ + objectName: buttonName, + imageURL: Script.resolvePath("assets/images/tools/snap.svg"), + visible: true, + alpha: 0.9, + }); +} else { + tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); + button = tablet.addButton({ + icon: "icons/tablet-icons/snap-i.svg", + text: buttonName + }); +} function shouldOpenFeedAfterShare() { var persisted = Settings.getValue('openFeedAfterShare', true); // might answer true, false, "true", or "false" @@ -55,10 +70,10 @@ function confirmShare(data) { Desktop.show("hifi/dialogs/GeneralPreferencesDialog.qml", "GeneralPreferencesDialog"); break; case 'setOpenFeedFalse': - Settings.setValue('openFeedAfterShare', false) + Settings.setValue('openFeedAfterShare', false); break; case 'setOpenFeedTrue': - Settings.setValue('openFeedAfterShare', true) + Settings.setValue('openFeedAfterShare', true); break; default: dialog.webEventReceived.disconnect(onMessage); @@ -116,23 +131,19 @@ function onClicked() { reticleVisible = Reticle.visible; Reticle.visible = false; Window.snapshotTaken.connect(resetButtons); - - button.writeProperty("buttonState", 0); - button.writeProperty("defaultState", 0); - button.writeProperty("hoverState", 2); // hide overlays if they are on if (resetOverlays) { Menu.setIsOptionChecked("Overlays", false); } - - // hide hud - toolBar.writeProperty("visible", false); // take snapshot (with no notification) Script.setTimeout(function () { - Window.takeSnapshot(false, true, 1.91); - }, SNAPSHOT_DELAY); + HMD.closeTablet(); + Script.setTimeout(function () { + Window.takeSnapshot(false, true, 1.91); + }, SNAPSHOT_DELAY); + }, FINISH_SOUND_DELAY); } function isDomainOpen(id) { @@ -160,7 +171,7 @@ function resetButtons(pathStillSnapshot, pathAnimatedSnapshot, notify) { // If we ARE taking an animated snapshot, we've already re-enabled the HUD by this point. if (pathAnimatedSnapshot === "") { // show hud - toolBar.writeProperty("visible", true); + Reticle.visible = reticleVisible; // show overlays if they were on if (resetOverlays) { @@ -168,12 +179,11 @@ function resetButtons(pathStillSnapshot, pathAnimatedSnapshot, notify) { } } else { // Allow the user to click the snapshot HUD button again - button.clicked.connect(onClicked); + if (!buttonConnected) { + button.clicked.connect(onClicked); + buttonConnected = true; + } } - // update button states - button.writeProperty("buttonState", 1); - button.writeProperty("defaultState", 1); - button.writeProperty("hoverState", 3); Window.snapshotTaken.disconnect(resetButtons); // A Snapshot Review dialog might be left open indefinitely after taking the picture, @@ -197,15 +207,10 @@ function resetButtons(pathStillSnapshot, pathAnimatedSnapshot, notify) { function processingGif() { // show hud - toolBar.writeProperty("visible", true); Reticle.visible = reticleVisible; - // update button states - button.writeProperty("buttonState", 0); - button.writeProperty("defaultState", 0); - button.writeProperty("hoverState", 2); - // Don't allow the user to click the snapshot button yet button.clicked.disconnect(onClicked); + buttonConnected = false; // show overlays if they were on if (resetOverlays) { Menu.setIsOptionChecked("Overlays", true); @@ -213,12 +218,19 @@ function processingGif() { } button.clicked.connect(onClicked); +buttonConnected = true; Window.snapshotShared.connect(snapshotShared); Window.processingGif.connect(processingGif); Script.scriptEnding.connect(function () { - toolBar.removeButton("snapshot"); button.clicked.disconnect(onClicked); + buttonConnected = false; + if (tablet) { + tablet.removeButton(button); + } + if (toolBar) { + toolBar.removeButton(buttonName); + } Window.snapshotShared.disconnect(snapshotShared); Window.processingGif.disconnect(processingGif); }); diff --git a/scripts/system/tablet-ui/tabletUI.js b/scripts/system/tablet-ui/tabletUI.js new file mode 100644 index 0000000000..942534e3b6 --- /dev/null +++ b/scripts/system/tablet-ui/tabletUI.js @@ -0,0 +1,113 @@ +"use strict"; + +// +// tabletUI.js +// +// scripts/system/tablet-ui/ +// +// Created by Seth Alves 2016-9-29 +// Copyright 2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +/* global Script, HMD, WebTablet, UIWebTablet */ + +(function() { // BEGIN LOCAL_SCOPE + var tabletShown = false; + var tabletLocation = null; + var activeHand = null; + + Script.include("../libraries/WebTablet.js"); + + function showTabletUI() { + tabletShown = true; + print("show tablet-ui"); + UIWebTablet = new WebTablet("qml/hifi/tablet/TabletRoot.qml", null, null, activeHand, true); + UIWebTablet.register(); + HMD.tabletID = UIWebTablet.tabletEntityID; + HMD.homeButtonID = UIWebTablet.homeButtonEntity; + } + + function hideTabletUI() { + tabletShown = false; + print("hide tablet-ui"); + if (UIWebTablet) { + if (UIWebTablet.onClose) { + UIWebTablet.onClose(); + } + + tabletLocation = UIWebTablet.getLocation(); + UIWebTablet.unregister(); + UIWebTablet.destroy(); + UIWebTablet = null; + HMD.tabletID = null; + HMD.homeButtonID = null; + } + } + + function updateShowTablet() { + if (tabletShown) { + var currentMicLevel = getMicLevel(); + var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); + tablet.updateAudioBar(currentMicLevel); + } + + if (tabletShown && UIWebTablet && Overlays.getOverlayType(UIWebTablet.webOverlayID) != "web3d") { + // when we switch domains, the tablet entity gets destroyed and recreated. this causes + // the overlay to be deleted, but not recreated. If the overlay is deleted for this or any + // other reason, close the tablet. + hideTabletUI(); + HMD.closeTablet(); + } else if (HMD.showTablet && !tabletShown) { + showTabletUI(); + } else if (!HMD.showTablet && tabletShown) { + hideTabletUI(); + } + } + + function toggleHand(channel, hand, senderUUID, localOnly) { + if (channel === "toggleHand") { + activeHand = JSON.parse(hand); + } + } + + Messages.subscribe("toggleHand"); + Messages.messageReceived.connect(toggleHand); + + Script.setInterval(updateShowTablet, 100); + + // Initialise variables used to calculate audio level + var accumulatedLevel = 0.0; + // Note: Might have to tweak the following two based on the rate we're getting the data + var AVERAGING_RATIO = 0.05; + var MIC_LEVEL_UPDATE_INTERVAL_MS = 100; + + // Calculate microphone level with the same scaling equation (log scale, exponentially averaged) in AvatarInputs and pal.js + function getMicLevel() { + var LOUDNESS_FLOOR = 11.0; + var LOUDNESS_SCALE = 2.8 / 5.0; + var LOG2 = Math.log(2.0); + var micLevel = 0.0; + accumulatedLevel = AVERAGING_RATIO * accumulatedLevel + (1 - AVERAGING_RATIO) * (MyAvatar.audioLoudness); + // Convert to log base 2 + var logLevel = Math.log(accumulatedLevel + 1) / LOG2; + + if (logLevel <= LOUDNESS_FLOOR) { + micLevel = logLevel / LOUDNESS_FLOOR * LOUDNESS_SCALE; + } else { + micLevel = (logLevel - (LOUDNESS_FLOOR - 1.0)) * LOUDNESS_SCALE; + } + if (micLevel > 1.0) { + micLevel = 1.0; + } + return micLevel; + } + + Script.scriptEnding.connect(function () { + Entities.deleteEntity(HMD.tabletID); + HMD.tabletID = null; + HMD.homeButtonID = null; + }); +}()); // END LOCAL_SCOPE diff --git a/scripts/system/users.js b/scripts/system/users.js index 8c52240aa9..009c446ff3 100644 --- a/scripts/system/users.js +++ b/scripts/system/users.js @@ -10,8 +10,39 @@ // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // +/*globals HMD, Toolbars, Script, Menu, Overlays, Tablet, Controller, Settings, OverlayWebWindow, Account, GlobalServices */ (function() { // BEGIN LOCAL_SCOPE +var button; +var buttonName = "USERS"; +var toolBar = null; +var tablet = null; + +var MENU_ITEM = "Users Online"; + +if (Settings.getValue("HUDUIEnabled")) { + toolBar = Toolbars.getToolbar("com.highfidelity.interface.toolbar.system"); + button = toolBar.addButton({ + objectName: buttonName, + imageURL: Script.resolvePath("assets/images/tools/people.svg"), + visible: true, + alpha: 0.9 + }); +} else { + tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); + button = tablet.addButton({ + icon: "icons/tablet-icons/users-i.svg", + text: "USERS", + isActive: Menu.isOptionChecked(MENU_ITEM) + }); +} + + +function onClicked() { + Menu.setIsOptionChecked(MENU_ITEM, !Menu.isOptionChecked(MENU_ITEM)); + button.editProperties({isActive: Menu.isOptionChecked(MENU_ITEM)}); +} +button.clicked.connect(onClicked); // resolve these paths immediately var MIN_MAX_BUTTON_SVG = Script.resolvePath("assets/images/tools/min-max-toggle.svg"); @@ -429,11 +460,11 @@ var usersWindow = (function () { } // Reserve space for title, friends button, and option controls - nonUsersHeight = WINDOW_MARGIN + windowLineHeight - + (shouldShowFriendsButton() ? FRIENDS_BUTTON_SPACER + FRIENDS_BUTTON_HEIGHT : 0) - + DISPLAY_SPACER - + windowLineHeight + VISIBILITY_SPACER - + windowLineHeight + WINDOW_BASE_MARGIN; + nonUsersHeight = WINDOW_MARGIN + windowLineHeight + + (shouldShowFriendsButton() ? FRIENDS_BUTTON_SPACER + FRIENDS_BUTTON_HEIGHT : 0) + + DISPLAY_SPACER + + windowLineHeight + VISIBILITY_SPACER + + windowLineHeight + WINDOW_BASE_MARGIN; // Limit window to height of viewport above window position minus VU meter and mirror if displayed windowHeight = linesOfUsers.length * windowLineHeight - windowLineSpacing + nonUsersHeight; @@ -491,8 +522,8 @@ var usersWindow = (function () { x: scrollbarBackgroundPosition.x, y: scrollbarBackgroundPosition.y }); - scrollbarBarPosition.y = scrollbarBackgroundPosition.y + 1 - + scrollbarValue * (scrollbarBackgroundHeight - scrollbarBarHeight - 2); + scrollbarBarPosition.y = scrollbarBackgroundPosition.y + 1 + + scrollbarValue * (scrollbarBackgroundHeight - scrollbarBarHeight - 2); Overlays.editOverlay(scrollbarBar, { x: scrollbarBackgroundPosition.x + 1, y: scrollbarBarPosition.y @@ -500,10 +531,10 @@ var usersWindow = (function () { x = windowLeft + WINDOW_MARGIN; - y = windowPosition.y - - DISPLAY_SPACER - - windowLineHeight - VISIBILITY_SPACER - - windowLineHeight - WINDOW_BASE_MARGIN; + y = windowPosition.y - + DISPLAY_SPACER - + windowLineHeight - VISIBILITY_SPACER - + windowLineHeight - WINDOW_BASE_MARGIN; if (shouldShowFriendsButton()) { y -= FRIENDS_BUTTON_HEIGHT; Overlays.editOverlay(friendsButton, { @@ -798,8 +829,8 @@ var usersWindow = (function () { userClicked = firstUserToDisplay + lineClicked; - if (0 <= userClicked && userClicked < linesOfUsers.length && 0 <= overlayX - && overlayX <= usersOnline[linesOfUsers[userClicked]].textWidth) { + if (0 <= userClicked && userClicked < linesOfUsers.length && 0 <= overlayX && + overlayX <= usersOnline[linesOfUsers[userClicked]].textWidth) { //print("Go to " + usersOnline[linesOfUsers[userClicked]].username); location.goToUser(usersOnline[linesOfUsers[userClicked]].username); } @@ -872,12 +903,12 @@ var usersWindow = (function () { } if (isMovingScrollbar) { - if (scrollbarBackgroundPosition.x - WINDOW_MARGIN <= event.x - && event.x <= scrollbarBackgroundPosition.x + SCROLLBAR_BACKGROUND_WIDTH + WINDOW_MARGIN - && scrollbarBackgroundPosition.y - WINDOW_MARGIN <= event.y - && event.y <= scrollbarBackgroundPosition.y + scrollbarBackgroundHeight + WINDOW_MARGIN) { - scrollbarValue = (event.y - scrollbarBarClickedAt * scrollbarBarHeight - scrollbarBackgroundPosition.y) - / (scrollbarBackgroundHeight - scrollbarBarHeight - 2); + if (scrollbarBackgroundPosition.x - WINDOW_MARGIN <= event.x && + event.x <= scrollbarBackgroundPosition.x + SCROLLBAR_BACKGROUND_WIDTH + WINDOW_MARGIN && + scrollbarBackgroundPosition.y - WINDOW_MARGIN <= event.y && + event.y <= scrollbarBackgroundPosition.y + scrollbarBackgroundHeight + WINDOW_MARGIN) { + scrollbarValue = (event.y - scrollbarBarClickedAt * scrollbarBarHeight - scrollbarBackgroundPosition.y) / + (scrollbarBackgroundHeight - scrollbarBarHeight - 2); scrollbarValue = Math.min(Math.max(scrollbarValue, 0.0), 1.0); firstUserToDisplay = Math.floor(scrollbarValue * (linesOfUsers.length - numUsersToDisplay)); updateOverlayPositions(); @@ -905,13 +936,13 @@ var usersWindow = (function () { isVisible = isBorderVisible; if (isVisible) { - isVisible = windowPosition.x - WINDOW_BORDER_LEFT_MARGIN <= event.x - && event.x <= windowPosition.x - WINDOW_BORDER_LEFT_MARGIN + WINDOW_BORDER_WIDTH - && windowPosition.y - windowHeight - WINDOW_BORDER_TOP_MARGIN <= event.y - && event.y <= windowPosition.y + WINDOW_BORDER_BOTTOM_MARGIN; + isVisible = windowPosition.x - WINDOW_BORDER_LEFT_MARGIN <= event.x && + event.x <= windowPosition.x - WINDOW_BORDER_LEFT_MARGIN + WINDOW_BORDER_WIDTH && + windowPosition.y - windowHeight - WINDOW_BORDER_TOP_MARGIN <= event.y && + event.y <= windowPosition.y + WINDOW_BORDER_BOTTOM_MARGIN; } else { - isVisible = windowPosition.x <= event.x && event.x <= windowPosition.x + WINDOW_WIDTH - && windowPosition.y - windowHeight <= event.y && event.y <= windowPosition.y; + isVisible = windowPosition.x <= event.x && event.x <= windowPosition.x + WINDOW_WIDTH && + windowPosition.y - windowHeight <= event.y && event.y <= windowPosition.y; } if (isVisible !== isBorderVisible) { isBorderVisible = isVisible; @@ -938,10 +969,10 @@ var usersWindow = (function () { if (isMovingWindow) { // Save offset of bottom of window to nearest edge of the window. - offset.x = (windowPosition.x + WINDOW_WIDTH / 2 < viewport.x / 2) - ? windowPosition.x : windowPosition.x - viewport.x; - offset.y = (windowPosition.y < viewport.y / 2) - ? windowPosition.y : windowPosition.y - viewport.y; + offset.x = (windowPosition.x + WINDOW_WIDTH / 2 < viewport.x / 2) ? + windowPosition.x : windowPosition.x - viewport.x; + offset.y = (windowPosition.y < viewport.y / 2) ? + windowPosition.y : windowPosition.y - viewport.y; Settings.setValue(SETTING_USERS_WINDOW_OFFSET, JSON.stringify(offset)); isMovingWindow = false; } @@ -962,8 +993,8 @@ var usersWindow = (function () { isMirrorDisplay = Menu.isOptionChecked(MIRROR_MENU_ITEM); isFullscreenMirror = Menu.isOptionChecked(FULLSCREEN_MIRROR_MENU_ITEM); - if (viewport.y !== oldViewport.y || isMirrorDisplay !== oldIsMirrorDisplay - || isFullscreenMirror !== oldIsFullscreenMirror) { + if (viewport.y !== oldViewport.y || isMirrorDisplay !== oldIsMirrorDisplay || + isFullscreenMirror !== oldIsFullscreenMirror) { calculateWindowHeight(); updateUsersDisplay(); } @@ -1234,4 +1265,16 @@ var usersWindow = (function () { Script.scriptEnding.connect(tearDown); }()); +function cleanup () { + //remove tablet button + button.clicked.disconnect(onClicked); + if (tablet) { + tablet.removeButton(button); + } + if (toolBar) { + toolBar.removeButton(buttonName); + } +} +Script.scriptEnding.connect(cleanup); + }()); // END LOCAL_SCOPE diff --git a/unpublishedScripts/marketplace/teaLight/teaLight.js b/unpublishedScripts/marketplace/teaLight/teaLight.js new file mode 100644 index 0000000000..85b0a3c310 --- /dev/null +++ b/unpublishedScripts/marketplace/teaLight/teaLight.js @@ -0,0 +1,21 @@ +(function() { + var MINIMUM_LIGHT_INTENSITY = 100.0; + var MAXIMUM_LIGHT_INTENSITY = 125.0; + + // Return a random number between `low` (inclusive) and `high` (exclusive) + function randFloat(low, high) { + return low + Math.random() * (high - low); + } + + var self = this; + this.preload = function(entityID) { + self.intervalID = Script.setInterval(function() { + Entities.editEntity(entityID, { + intensity: randFloat(MINIMUM_LIGHT_INTENSITY, MAXIMUM_LIGHT_INTENSITY) + }); + }, 100); + }; + this.unload = function() { + Script.clearInterval(self.intervalID); + } +}); diff --git a/unpublishedScripts/steam.js b/unpublishedScripts/steam.js index a80f0072ac..8ff3d83d86 100644 --- a/unpublishedScripts/steam.js +++ b/unpublishedScripts/steam.js @@ -16,9 +16,6 @@ var steamInviteButton = toolBar.addButton({ objectName: "steamInvite", imageURL: Script.resolvePath("assets/images/tools/steam-invite.svg"), visible: Steam.isRunning, - buttonState: 1, - defaultState: 1, - hoverState: 3, alpha: 0.9 }); @@ -28,4 +25,4 @@ steamInviteButton.clicked.connect(function(){ Script.scriptEnding.connect(function () { toolBar.removeButton("steamInvite"); -}); \ No newline at end of file +});