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