Merge remote-tracking branch 'upstream/master' into HEAD

This commit is contained in:
Brad Davis 2017-03-08 09:03:21 -08:00
commit e94f803b84
63 changed files with 1989 additions and 1259 deletions

View file

@ -206,6 +206,17 @@ foreach(CUSTOM_MACRO ${HIFI_CUSTOM_MACROS})
include(${CUSTOM_MACRO})
endforeach()
file(GLOB_RECURSE JS_SRC scripts/*.js)
add_custom_target(js SOURCES ${JS_SRC})
if (UNIX)
install(
DIRECTORY "${CMAKE_SOURCE_DIR}/scripts"
DESTINATION ${CMAKE_CURRENT_BINARY_DIR}/interface
COMPONENT ${CLIENT_COMPONENT}
)
endif()
if (ANDROID)
file(GLOB ANDROID_CUSTOM_MACROS "cmake/android/*.cmake")
foreach(CUSTOM_MACRO ${ANDROID_CUSTOM_MACROS})

View file

@ -16,7 +16,7 @@ Contributing
git checkout -b new_branch_name
```
4. Code
* Follow the [coding standard](https://readme.highfidelity.com/v1.0/docs/coding-standard)
* Follow the [coding standard](https://wiki.highfidelity.com/wiki/Coding_Standards)
5. Commit
* Use [well formed commit messages](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html)
6. Update your branch

View file

@ -394,7 +394,7 @@ void EntityScriptServer::selectAudioFormat(const QString& selectedCodecName) {
}
void EntityScriptServer::resetEntitiesScriptEngine() {
auto engineName = QString("Entities %1").arg(++_entitiesScriptEngineCount);
auto engineName = QString("about:Entities %1").arg(++_entitiesScriptEngineCount);
auto newEngine = QSharedPointer<ScriptEngine>(new ScriptEngine(ScriptEngine::ENTITY_SERVER_SCRIPT, NO_SCRIPT, engineName));
auto webSocketServerConstructorValue = newEngine->newFunction(WebSocketServerClass::constructor);
@ -477,7 +477,7 @@ void EntityScriptServer::checkAndCallPreload(const EntityItemID& entityID, const
if (!scriptUrl.isEmpty()) {
scriptUrl = ResourceManager::normalizeURL(scriptUrl);
qCDebug(entity_script_server) << "Loading entity server script" << scriptUrl << "for" << entityID;
ScriptEngine::loadEntityScript(_entitiesScriptEngine, entityID, scriptUrl, reload);
_entitiesScriptEngine->loadEntityScript(entityID, scriptUrl, reload);
}
}
}

View file

@ -12,35 +12,29 @@ string(TOUPPER ${EXTERNAL_NAME} EXTERNAL_NAME_UPPER)
# 0.5 public
# URL http://static.oculus.com/sdk-downloads/ovr_sdk_win_0.5.0.1.zip
# URL_MD5 d3fc4c02db9be5ff08af4ef4c97b32f9
# 1.3 public
# URL http://hifi-public.s3.amazonaws.com/dependencies/ovr_sdk_win_1.3.0_public.zip
# URL_MD5 4d26faba0c1f35ff80bf674c96ed9259
if (WIN32)
ExternalProject_Add(
${EXTERNAL_NAME}
URL https://hifi-public.s3.amazonaws.com/dependencies/ovr_sdk_win_1.8.0_public.zip
URL_MD5 bea17e04acc1dd8cf7cabefa1b28cc3c
CONFIGURE_COMMAND ""
BUILD_COMMAND ""
INSTALL_COMMAND ""
URL https://static.oculus.com/sdk-downloads/1.11.0/Public/1486063832/ovr_sdk_win_1.11.0_public.zip
URL_MD5 ea484403757cbfdfa743b6577fb1f9d2
CMAKE_ARGS -DCMAKE_INSTALL_PREFIX:PATH=<INSTALL_DIR>
PATCH_COMMAND ${CMAKE_COMMAND} -E copy "${CMAKE_CURRENT_SOURCE_DIR}/LibOVRCMakeLists.txt" <SOURCE_DIR>/CMakeLists.txt
LOG_DOWNLOAD 1
)
ExternalProject_Get_Property(${EXTERNAL_NAME} SOURCE_DIR)
message("LIBOVR dir ${SOURCE_DIR}")
set(LIBOVR_DIR ${SOURCE_DIR}/LibOVR)
if ("${CMAKE_SIZEOF_VOID_P}" EQUAL "8")
set(LIBOVR_LIB_DIR ${LIBOVR_DIR}/Lib/Windows/x64/Release/VS2013 CACHE TYPE INTERNAL)
else()
set(LIBOVR_LIB_DIR ${LIBOVR_DIR}/Lib/Windows/Win32/Release/VS2013 CACHE TYPE INTERNAL)
endif()
ExternalProject_Get_Property(${EXTERNAL_NAME} INSTALL_DIR)
set(LIBOVR_DIR ${INSTALL_DIR})
set(${EXTERNAL_NAME_UPPER}_INCLUDE_DIRS ${LIBOVR_DIR}/Include CACHE TYPE INTERNAL)
message("LIBOVR include dir ${${EXTERNAL_NAME_UPPER}_INCLUDE_DIRS}")
set(${EXTERNAL_NAME_UPPER}_LIBRARIES ${LIBOVR_LIB_DIR}/LibOVR.lib CACHE TYPE INTERNAL)
set(${EXTERNAL_NAME_UPPER}_LIBRARY_DEBUG ${LIBOVR_DIR}/Lib/LibOVRd.lib CACHE TYPE INTERNAL)
set(${EXTERNAL_NAME_UPPER}_LIBRARY_RELEASE ${LIBOVR_DIR}/Lib/LibOVR.lib CACHE TYPE INTERNAL)
include(SelectLibraryConfigurations)
select_library_configurations(LIBOVR)
set(${EXTERNAL_NAME_UPPER}_LIBRARIES ${${EXTERNAL_NAME_UPPER}_LIBRARIES} CACHE TYPE INTERNAL)
message("Libs ${EXTERNAL_NAME_UPPER}_LIBRARIES ${${EXTERNAL_NAME_UPPER}_LIBRARIES}")
elseif(APPLE)
ExternalProject_Add(

View file

@ -0,0 +1,14 @@
cmake_minimum_required(VERSION 3.2)
project(LibOVR)
include_directories(LibOVR/Include LibOVR/Src)
file(GLOB HEADER_FILES LibOVR/Include/*.h)
file(GLOB EXTRA_HEADER_FILES LibOVR/Include/Extras/*.h)
file(GLOB_RECURSE SOURCE_FILES LibOVR/Src/*.c LibOVR/Src/*.cpp)
set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -DOVR_BUILD_DEBUG")
add_library(LibOVR STATIC ${SOURCE_FILES} ${HEADER_FILES} ${EXTRA_HEADER_FILES})
set_target_properties(LibOVR PROPERTIES DEBUG_POSTFIX "d")
install(TARGETS LibOVR DESTINATION Lib)
install(FILES ${HEADER_FILES} DESTINATION Include)
install(FILES ${EXTRA_HEADER_FILES} DESTINATION Include/Extras)

View file

@ -63,6 +63,17 @@ qt5_wrap_ui(QT_UI_HEADERS "${QT_UI_FILES}")
# add them to the interface source files
set(INTERFACE_SRCS ${INTERFACE_SRCS} "${QT_UI_HEADERS}" "${QT_RESOURCES}")
file(GLOB_RECURSE QML_SRC resources/qml/*.qml resources/qml/*.js)
add_custom_target(qml SOURCES ${QML_SRC})
if (UNIX)
install(
DIRECTORY "${CMAKE_SOURCE_DIR}/interface/resources/qml"
DESTINATION ${CMAKE_CURRENT_BINARY_DIR}/resources
COMPONENT ${CLIENT_COMPONENT}
)
endif()
# translation disabled until we strip out the line numbers
# set(QM ${TARGET_NAME}_en.qm)
# set(TS ${TARGET_NAME}_en.ts)

View file

@ -21,467 +21,478 @@ import "../controls-uit" as HifiControls
// references HMD, Users, UserActivityLogger from root context
Rectangle {
id: pal
id: pal;
// Size
width: parent.width
height: parent.height
width: parent.width;
height: parent.height;
// Style
color: "#E3E3E3"
color: "#E3E3E3";
// Properties
property int myCardHeight: 90
property int rowHeight: 70
property int actionButtonWidth: 55
property int actionButtonAllowance: actionButtonWidth * 2
property int minNameCardWidth: palContainer.width - (actionButtonAllowance * 2) - 4 - hifi.dimensions.scrollbarBackgroundWidth
property int nameCardWidth: minNameCardWidth + (iAmAdmin ? 0 : actionButtonAllowance)
property var myData: ({displayName: "", userName: "", audioLevel: 0.0, avgAudioLevel: 0.0, admin: true}) // valid dummy until set
property int myCardHeight: 90;
property int rowHeight: 70;
property int actionButtonWidth: 55;
property int actionButtonAllowance: actionButtonWidth * 2;
property int minNameCardWidth: palContainer.width - (actionButtonAllowance * 2) - 4 - hifi.dimensions.scrollbarBackgroundWidth;
property int nameCardWidth: minNameCardWidth + (iAmAdmin ? 0 : actionButtonAllowance);
property var myData: ({displayName: "", userName: "", audioLevel: 0.0, avgAudioLevel: 0.0, admin: true}); // valid dummy until set
property var ignored: ({}); // Keep a local list of ignored avatars & their data. Necessary because HashMap is slow to respond after ignoring.
property var userModelData: [] // This simple list is essentially a mirror of the userModel listModel without all the extra complexities.
property bool iAmAdmin: false
property var userModelData: []; // This simple list is essentially a mirror of the userModel listModel without all the extra complexities.
property bool iAmAdmin: false;
HifiConstants { id: hifi }
HifiConstants { id: hifi; }
// The letterbox used for popup messages
LetterboxMessage {
id: letterboxMessage
z: 999 // Force the popup on top of everything else
id: letterboxMessage;
z: 999; // Force the popup on top of everything else
}
function letterbox(headerGlyph, headerText, message) {
letterboxMessage.headerGlyph = headerGlyph
letterboxMessage.headerText = headerText
letterboxMessage.text = message
letterboxMessage.visible = true
letterboxMessage.popupRadius = 0
letterboxMessage.headerGlyph = headerGlyph;
letterboxMessage.headerText = headerText;
letterboxMessage.text = message;
letterboxMessage.visible = true;
letterboxMessage.popupRadius = 0;
}
Settings {
id: settings
category: "pal"
property bool filtered: false
property int nearDistance: 30
property int sortIndicatorColumn: 1
property int sortIndicatorOrder: Qt.AscendingOrder
id: settings;
category: "pal";
property bool filtered: false;
property int nearDistance: 30;
property int sortIndicatorColumn: 1;
property int sortIndicatorOrder: Qt.AscendingOrder;
}
function getSelectedSessionIDs() {
var sessionIDs = [];
table.selection.forEach(function (userIndex) {
sessionIDs.push(userModelData[userIndex].sessionId);
});
return sessionIDs;
}
function refreshWithFilter() {
// We should just be able to set settings.filtered to filter.checked, but see #3249, so send to .js for saving.
pal.sendToScript({method: 'refresh', params: {filter: filter.checked && {distance: settings.nearDistance}}});
var userIds = getSelectedSessionIDs();
var params = {filter: filter.checked && {distance: settings.nearDistance}};
if (userIds.length > 0) {
params.selected = [[userIds[0]], true, true];
}
pal.sendToScript({method: 'refresh', params: params});
}
// This is the container for the PAL
Rectangle {
property bool punctuationMode: false
id: palContainer
property bool punctuationMode: false;
id: palContainer;
// Size
width: pal.width - 10
height: pal.height - 10
width: pal.width - 10;
height: pal.height - 10;
// Style
color: pal.color
color: pal.color;
// Anchors
anchors.centerIn: pal
anchors.centerIn: pal;
// Properties
radius: hifi.dimensions.borderRadius
radius: hifi.dimensions.borderRadius;
// This contains the current user's NameCard and will contain other information in the future
Rectangle {
id: myInfo
// Size
width: palContainer.width
height: myCardHeight
// Style
color: pal.color
// Anchors
anchors.top: palContainer.top
// Properties
radius: hifi.dimensions.borderRadius
// This NameCard refers to the current user's NameCard (the one above the table)
NameCard {
id: myCard
// Properties
displayName: myData.displayName
userName: myData.userName
audioLevel: myData.audioLevel
avgAudioLevel: myData.avgAudioLevel
isMyCard: true
// This contains the current user's NameCard and will contain other information in the future
Rectangle {
id: myInfo;
// Size
width: minNameCardWidth
height: parent.height
// Anchors
anchors.left: parent.left
}
Row {
HifiControls.CheckBox {
id: filter
checked: settings.filtered
text: "in view"
boxSize: reload.height * 0.70
onCheckedChanged: refreshWithFilter()
}
HifiControls.GlyphButton {
id: reload
glyph: hifi.glyphs.reload
width: reload.height
onClicked: refreshWithFilter()
}
spacing: 50
anchors {
right: parent.right
top: parent.top
topMargin: 10
}
}
}
// Rectangles used to cover up rounded edges on bottom of MyInfo Rectangle
Rectangle {
color: pal.color
width: palContainer.width
height: 10
anchors.top: myInfo.bottom
anchors.left: parent.left
}
Rectangle {
color: pal.color
width: palContainer.width
height: 10
anchors.bottom: table.top
anchors.left: parent.left
}
// Rectangle that houses "ADMIN" string
Rectangle {
id: adminTab
// Size
width: 2*actionButtonWidth + hifi.dimensions.scrollbarBackgroundWidth + 2
height: 40
// Anchors
anchors.bottom: myInfo.bottom
anchors.bottomMargin: -10
anchors.right: myInfo.right
// Properties
visible: iAmAdmin
// Style
color: hifi.colors.tableRowLightEven
radius: hifi.dimensions.borderRadius
border.color: hifi.colors.lightGrayText
border.width: 2
// "ADMIN" text
RalewaySemiBold {
id: adminTabText
text: "ADMIN"
// Text size
size: hifi.fontSizes.tableHeading + 2
// Anchors
anchors.top: parent.top
anchors.topMargin: 8
anchors.left: parent.left
anchors.right: parent.right
anchors.rightMargin: hifi.dimensions.scrollbarBackgroundWidth
width: palContainer.width;
height: myCardHeight;
// Style
font.capitalization: Font.AllUppercase
color: hifi.colors.redHighlight
// Alignment
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignTop
}
}
// This TableView refers to the table (below the current user's NameCard)
HifiControls.Table {
id: table
// Size
height: palContainer.height - myInfo.height - 4
width: palContainer.width - 4
// Anchors
anchors.left: parent.left
anchors.top: myInfo.bottom
// Properties
centerHeaderText: true
sortIndicatorVisible: true
headerVisible: true
sortIndicatorColumn: settings.sortIndicatorColumn
sortIndicatorOrder: settings.sortIndicatorOrder
onSortIndicatorColumnChanged: {
settings.sortIndicatorColumn = sortIndicatorColumn
sortModel()
}
onSortIndicatorOrderChanged: {
settings.sortIndicatorOrder = sortIndicatorOrder
sortModel()
}
TableViewColumn {
role: "avgAudioLevel"
title: "LOUD"
width: actionButtonWidth
movable: false
resizable: false
}
TableViewColumn {
id: displayNameHeader
role: "displayName"
title: table.rowCount + (table.rowCount === 1 ? " NAME" : " NAMES")
width: nameCardWidth
movable: false
resizable: false
}
TableViewColumn {
role: "ignore"
title: "IGNORE"
width: actionButtonWidth
movable: false
resizable: false
}
TableViewColumn {
visible: iAmAdmin
role: "mute"
title: "SILENCE"
width: actionButtonWidth
movable: false
resizable: false
}
TableViewColumn {
visible: iAmAdmin
role: "kick"
title: "BAN"
width: actionButtonWidth
movable: false
resizable: false
}
model: ListModel {
id: userModel
}
// This Rectangle refers to each Row in the table.
rowDelegate: Rectangle { // The only way I know to specify a row height.
// Size
height: styleData.selected ? rowHeight : rowHeight - 15
color: styleData.selected
? hifi.colors.orangeHighlight
: styleData.alternate ? hifi.colors.tableRowLightEven : hifi.colors.tableRowLightOdd
}
// This Item refers to the contents of each Cell
itemDelegate: Item {
id: itemCell
property bool isCheckBox: styleData.role === "personalMute" || styleData.role === "ignore"
property bool isButton: styleData.role === "mute" || styleData.role === "kick"
property bool isAvgAudio: styleData.role === "avgAudioLevel"
// This NameCard refers to the cell that contains an avatar's
// DisplayName and UserName
color: pal.color;
// Anchors
anchors.top: palContainer.top;
// Properties
radius: hifi.dimensions.borderRadius;
// This NameCard refers to the current user's NameCard (the one above the table)
NameCard {
id: nameCard
id: myCard;
// Properties
displayName: styleData.value
userName: model ? model.userName : ""
audioLevel: model ? model.audioLevel : 0.0
avgAudioLevel: model ? model.avgAudioLevel : 0.0
visible: !isCheckBox && !isButton && !isAvgAudio
uuid: model ? model.sessionId : ""
selected: styleData.selected
isAdmin: model && model.admin
displayName: myData.displayName;
userName: myData.userName;
audioLevel: myData.audioLevel;
avgAudioLevel: myData.avgAudioLevel;
isMyCard: true;
// Size
width: nameCardWidth
height: parent.height
width: minNameCardWidth;
height: parent.height;
// Anchors
anchors.left: parent.left
anchors.left: parent.left;
}
HifiControls.GlyphButton {
function getGlyph() {
var fileName = "vol_";
if (model["personalMute"]) {
fileName += "x_";
}
fileName += (4.0*(model ? model.avgAudioLevel : 0.0)).toFixed(0);
return hifi.glyphs[fileName];
Row {
HifiControls.CheckBox {
id: filter;
checked: settings.filtered;
text: "in view";
boxSize: reload.height * 0.70;
onCheckedChanged: refreshWithFilter();
}
id: avgAudioVolume
visible: isAvgAudio
glyph: getGlyph()
width: 32
size: height
anchors.verticalCenter: parent.verticalCenter
anchors.horizontalCenter: parent.horizontalCenter
onClicked: {
// cannot change mute status when ignoring
if (!model["ignore"]) {
var newValue = !model["personalMute"];
userModel.setProperty(model.userIndex, "personalMute", newValue)
userModelData[model.userIndex]["personalMute"] = newValue // Defensive programming
Users["personalMute"](model.sessionId, newValue)
UserActivityLogger["palAction"](newValue ? "personalMute" : "un-personalMute", model.sessionId)
}
HifiControls.GlyphButton {
id: reload;
glyph: hifi.glyphs.reload;
width: reload.height;
onClicked: refreshWithFilter();
}
spacing: 50;
anchors {
right: parent.right;
top: parent.top;
topMargin: 10;
}
}
}
// Rectangles used to cover up rounded edges on bottom of MyInfo Rectangle
Rectangle {
color: pal.color;
width: palContainer.width;
height: 10;
anchors.top: myInfo.bottom;
anchors.left: parent.left;
}
Rectangle {
color: pal.color;
width: palContainer.width;
height: 10;
anchors.bottom: table.top;
anchors.left: parent.left;
}
// Rectangle that houses "ADMIN" string
Rectangle {
id: adminTab;
// Size
width: 2*actionButtonWidth + hifi.dimensions.scrollbarBackgroundWidth + 2;
height: 40;
// Anchors
anchors.bottom: myInfo.bottom;
anchors.bottomMargin: -10;
anchors.right: myInfo.right;
// Properties
visible: iAmAdmin;
// Style
color: hifi.colors.tableRowLightEven;
radius: hifi.dimensions.borderRadius;
border.color: hifi.colors.lightGrayText;
border.width: 2;
// "ADMIN" text
RalewaySemiBold {
id: adminTabText;
text: "ADMIN";
// Text size
size: hifi.fontSizes.tableHeading + 2;
// Anchors
anchors.top: parent.top;
anchors.topMargin: 8;
anchors.left: parent.left;
anchors.right: parent.right;
anchors.rightMargin: hifi.dimensions.scrollbarBackgroundWidth;
// Style
font.capitalization: Font.AllUppercase;
color: hifi.colors.redHighlight;
// Alignment
horizontalAlignment: Text.AlignHCenter;
verticalAlignment: Text.AlignTop;
}
}
// This TableView refers to the table (below the current user's NameCard)
HifiControls.Table {
id: table;
// Size
height: palContainer.height - myInfo.height - 4;
width: palContainer.width - 4;
// Anchors
anchors.left: parent.left;
anchors.top: myInfo.bottom;
// Properties
centerHeaderText: true;
sortIndicatorVisible: true;
headerVisible: true;
sortIndicatorColumn: settings.sortIndicatorColumn;
sortIndicatorOrder: settings.sortIndicatorOrder;
onSortIndicatorColumnChanged: {
settings.sortIndicatorColumn = sortIndicatorColumn;
sortModel();
}
onSortIndicatorOrderChanged: {
settings.sortIndicatorOrder = sortIndicatorOrder;
sortModel();
}
// This CheckBox belongs in the columns that contain the stateful action buttons ("Mute" & "Ignore" for now)
// KNOWN BUG with the Checkboxes: When clicking in the center of the sorting header, the checkbox
// will appear in the "hovered" state. Hovering over the checkbox will fix it.
// Clicking on the sides of the sorting header doesn't cause this problem.
// I'm guessing this is a QT bug and not anything I can fix. I spent too long trying to work around it...
// I'm just going to leave the minor visual bug in.
HifiControls.CheckBox {
id: actionCheckBox
visible: isCheckBox
anchors.centerIn: parent
checked: model ? model[styleData.role] : false
// If this is a "Personal Mute" checkbox, disable the checkbox if the "Ignore" checkbox is checked.
enabled: !(styleData.role === "personalMute" && (model ? model["ignore"] : true))
boxSize: 24
onClicked: {
var newValue = !model[styleData.role]
userModel.setProperty(model.userIndex, styleData.role, newValue)
userModelData[model.userIndex][styleData.role] = newValue // Defensive programming
Users[styleData.role](model.sessionId, newValue)
UserActivityLogger["palAction"](newValue ? styleData.role : "un-" + styleData.role, model.sessionId)
if (styleData.role === "ignore") {
userModel.setProperty(model.userIndex, "personalMute", newValue)
userModelData[model.userIndex]["personalMute"] = newValue // Defensive programming
if (newValue) {
ignored[model.sessionId] = userModelData[model.userIndex]
} else {
delete ignored[model.sessionId]
}
avgAudioVolume.glyph = avgAudioVolume.getGlyph()
}
// http://doc.qt.io/qt-5/qtqml-syntax-propertybinding.html#creating-property-bindings-from-javascript
// I'm using an explicit binding here because clicking a checkbox breaks the implicit binding as set by
// "checked:" statement above.
checked = Qt.binding(function() { return (model[styleData.role])})
}
TableViewColumn {
role: "avgAudioLevel";
title: "LOUD";
width: actionButtonWidth;
movable: false;
resizable: false;
}
// This Button belongs in the columns that contain the stateless action buttons ("Silence" & "Ban" for now)
HifiControls.Button {
id: actionButton
color: 2 // Red
visible: isButton
anchors.centerIn: parent
width: 32
height: 32
onClicked: {
Users[styleData.role](model.sessionId)
UserActivityLogger["palAction"](styleData.role, model.sessionId)
if (styleData.role === "kick") {
// Just for now, while we cannot undo "Ban":
userModel.remove(model.userIndex)
delete userModelData[model.userIndex] // Defensive programming
sortModel()
}
}
// muted/error glyphs
HiFiGlyphs {
text: (styleData.role === "kick") ? hifi.glyphs.error : hifi.glyphs.muted
TableViewColumn {
id: displayNameHeader;
role: "displayName";
title: table.rowCount + (table.rowCount === 1 ? " NAME" : " NAMES");
width: nameCardWidth;
movable: false;
resizable: false;
}
TableViewColumn {
role: "ignore";
title: "IGNORE";
width: actionButtonWidth;
movable: false;
resizable: false;
}
TableViewColumn {
visible: iAmAdmin;
role: "mute";
title: "SILENCE";
width: actionButtonWidth;
movable: false;
resizable: false;
}
TableViewColumn {
visible: iAmAdmin;
role: "kick";
title: "BAN";
width: actionButtonWidth;
movable: false;
resizable: false;
}
model: ListModel {
id: userModel;
}
// This Rectangle refers to each Row in the table.
rowDelegate: Rectangle { // The only way I know to specify a row height.
// Size
height: styleData.selected ? rowHeight : rowHeight - 15;
color: styleData.selected
? hifi.colors.orangeHighlight
: styleData.alternate ? hifi.colors.tableRowLightEven : hifi.colors.tableRowLightOdd;
}
// This Item refers to the contents of each Cell
itemDelegate: Item {
id: itemCell;
property bool isCheckBox: styleData.role === "personalMute" || styleData.role === "ignore";
property bool isButton: styleData.role === "mute" || styleData.role === "kick";
property bool isAvgAudio: styleData.role === "avgAudioLevel";
// This NameCard refers to the cell that contains an avatar's
// DisplayName and UserName
NameCard {
id: nameCard;
// Properties
displayName: styleData.value;
userName: model ? model.userName : "";
audioLevel: model ? model.audioLevel : 0.0;
avgAudioLevel: model ? model.avgAudioLevel : 0.0;
visible: !isCheckBox && !isButton && !isAvgAudio;
uuid: model ? model.sessionId : "";
selected: styleData.selected;
isAdmin: model && model.admin;
// Size
size: parent.height*1.3
width: nameCardWidth;
height: parent.height;
// Anchors
anchors.fill: parent
// Style
horizontalAlignment: Text.AlignHCenter
color: enabled ? hifi.buttons.textColor[actionButton.color]
: hifi.buttons.disabledTextColor[actionButton.colorScheme]
anchors.left: parent.left;
}
HifiControls.GlyphButton {
function getGlyph() {
var fileName = "vol_";
if (model && model.personalMute) {
fileName += "x_";
}
fileName += (4.0*(model ? model.avgAudioLevel : 0.0)).toFixed(0);
return hifi.glyphs[fileName];
}
id: avgAudioVolume;
visible: isAvgAudio;
glyph: getGlyph();
width: 32;
size: height;
anchors.verticalCenter: parent.verticalCenter;
anchors.horizontalCenter: parent.horizontalCenter;
onClicked: {
// cannot change mute status when ignoring
if (!model["ignore"]) {
var newValue = !model["personalMute"];
userModel.setProperty(model.userIndex, "personalMute", newValue);
userModelData[model.userIndex]["personalMute"] = newValue; // Defensive programming
Users["personalMute"](model.sessionId, newValue);
UserActivityLogger["palAction"](newValue ? "personalMute" : "un-personalMute", model.sessionId);
}
}
}
// This CheckBox belongs in the columns that contain the stateful action buttons ("Mute" & "Ignore" for now)
// KNOWN BUG with the Checkboxes: When clicking in the center of the sorting header, the checkbox
// will appear in the "hovered" state. Hovering over the checkbox will fix it.
// Clicking on the sides of the sorting header doesn't cause this problem.
// I'm guessing this is a QT bug and not anything I can fix. I spent too long trying to work around it...
// I'm just going to leave the minor visual bug in.
HifiControls.CheckBox {
id: actionCheckBox;
visible: isCheckBox;
anchors.centerIn: parent;
checked: model ? model[styleData.role] : false;
// If this is a "Personal Mute" checkbox, disable the checkbox if the "Ignore" checkbox is checked.
enabled: !(styleData.role === "personalMute" && (model ? model["ignore"] : true));
boxSize: 24;
onClicked: {
var newValue = !model[styleData.role];
userModel.setProperty(model.userIndex, styleData.role, newValue);
userModelData[model.userIndex][styleData.role] = newValue; // Defensive programming
Users[styleData.role](model.sessionId, newValue);
UserActivityLogger["palAction"](newValue ? styleData.role : "un-" + styleData.role, model.sessionId);
if (styleData.role === "ignore") {
userModel.setProperty(model.userIndex, "personalMute", newValue);
userModelData[model.userIndex]["personalMute"] = newValue; // Defensive programming
if (newValue) {
ignored[model.sessionId] = userModelData[model.userIndex];
} else {
delete ignored[model.sessionId];
}
avgAudioVolume.glyph = avgAudioVolume.getGlyph();
}
// http://doc.qt.io/qt-5/qtqml-syntax-propertybinding.html#creating-property-bindings-from-javascript
// I'm using an explicit binding here because clicking a checkbox breaks the implicit binding as set by
// "checked:" statement above.
checked = Qt.binding(function() { return (model[styleData.role])});
}
}
// This Button belongs in the columns that contain the stateless action buttons ("Silence" & "Ban" for now)
HifiControls.Button {
id: actionButton;
color: 2; // Red
visible: isButton;
anchors.centerIn: parent;
width: 32;
height: 32;
onClicked: {
Users[styleData.role](model.sessionId);
UserActivityLogger["palAction"](styleData.role, model.sessionId);
if (styleData.role === "kick") {
userModelData.splice(model.userIndex, 1);
userModel.remove(model.userIndex); // after changing userModelData, b/c ListModel can frob the data
}
}
// muted/error glyphs
HiFiGlyphs {
text: (styleData.role === "kick") ? hifi.glyphs.error : hifi.glyphs.muted;
// Size
size: parent.height*1.3;
// Anchors
anchors.fill: parent;
// Style
horizontalAlignment: Text.AlignHCenter;
color: enabled ? hifi.buttons.textColor[actionButton.color]
: hifi.buttons.disabledTextColor[actionButton.colorScheme];
}
}
}
}
}
// Separator between user and admin functions
Rectangle {
// Size
width: 2
height: table.height
// Anchors
anchors.left: adminTab.left
anchors.top: table.top
// Properties
visible: iAmAdmin
color: hifi.colors.lightGrayText
}
TextMetrics {
id: displayNameHeaderMetrics
text: displayNameHeader.title
// font: displayNameHeader.font // was this always undefined? giving error now...
}
// This Rectangle refers to the [?] popup button next to "NAMES"
Rectangle {
color: hifi.colors.tableBackgroundLight
width: 20
height: hifi.dimensions.tableHeaderHeight - 2
anchors.left: table.left
anchors.top: table.top
anchors.topMargin: 1
anchors.leftMargin: actionButtonWidth + nameCardWidth/2 + displayNameHeaderMetrics.width/2 + 6
RalewayRegular {
id: helpText
text: "[?]"
size: hifi.fontSizes.tableHeading + 2
font.capitalization: Font.AllUppercase
color: hifi.colors.darkGray
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
anchors.fill: parent
// Separator between user and admin functions
Rectangle {
// Size
width: 2;
height: table.height;
// Anchors
anchors.left: adminTab.left;
anchors.top: table.top;
// Properties
visible: iAmAdmin;
color: hifi.colors.lightGrayText;
}
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.LeftButton
hoverEnabled: true
onClicked: letterbox(hifi.glyphs.question,
"Display Names",
"Bold names in the list are <b>avatar display names</b>.<br>" +
"If a display name isn't set, a unique <b>session display name</b> is assigned." +
"<br><br>Administrators of this domain can also see the <b>username</b> or <b>machine ID</b> associated with each avatar present.")
onEntered: helpText.color = hifi.colors.baseGrayHighlight
onExited: helpText.color = hifi.colors.darkGray
TextMetrics {
id: displayNameHeaderMetrics;
text: displayNameHeader.title;
// font: displayNameHeader.font // was this always undefined? giving error now...
}
}
// This Rectangle refers to the [?] popup button next to "ADMIN"
Rectangle {
visible: iAmAdmin
color: adminTab.color
width: 20
height: 28
anchors.right: adminTab.right
anchors.rightMargin: 10 + hifi.dimensions.scrollbarBackgroundWidth
anchors.top: adminTab.top
anchors.topMargin: 2
RalewayRegular {
id: adminHelpText
text: "[?]"
size: hifi.fontSizes.tableHeading + 2
font.capitalization: Font.AllUppercase
color: hifi.colors.redHighlight
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
anchors.fill: parent
// This Rectangle refers to the [?] popup button next to "NAMES"
Rectangle {
color: hifi.colors.tableBackgroundLight;
width: 20;
height: hifi.dimensions.tableHeaderHeight - 2;
anchors.left: table.left;
anchors.top: table.top;
anchors.topMargin: 1;
anchors.leftMargin: actionButtonWidth + nameCardWidth/2 + displayNameHeaderMetrics.width/2 + 6;
RalewayRegular {
id: helpText;
text: "[?]";
size: hifi.fontSizes.tableHeading + 2;
font.capitalization: Font.AllUppercase;
color: hifi.colors.darkGray;
horizontalAlignment: Text.AlignHCenter;
verticalAlignment: Text.AlignVCenter;
anchors.fill: parent;
}
MouseArea {
anchors.fill: parent;
acceptedButtons: Qt.LeftButton;
hoverEnabled: true;
onClicked: letterbox(hifi.glyphs.question,
"Display Names",
"Bold names in the list are <b>avatar display names</b>.<br>" +
"If a display name isn't set, a unique <b>session display name</b> is assigned." +
"<br><br>Administrators of this domain can also see the <b>username</b> or <b>machine ID</b> associated with each avatar present.");
onEntered: helpText.color = hifi.colors.baseGrayHighlight;
onExited: helpText.color = hifi.colors.darkGray;
}
}
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.LeftButton
hoverEnabled: true
onClicked: letterbox(hifi.glyphs.question,
"Admin Actions",
"<b>Silence</b> mutes a user's microphone. Silenced users can unmute themselves by clicking &quot;UNMUTE&quot; on their toolbar.<br><br>" +
"<b>Ban</b> removes a user from this domain and prevents them from returning. Admins can un-ban users from the Sandbox Domain Settings page.")
onEntered: adminHelpText.color = "#94132e"
onExited: adminHelpText.color = hifi.colors.redHighlight
// This Rectangle refers to the [?] popup button next to "ADMIN"
Rectangle {
visible: iAmAdmin;
color: adminTab.color;
width: 20;
height: 28;
anchors.right: adminTab.right;
anchors.rightMargin: 10 + hifi.dimensions.scrollbarBackgroundWidth;
anchors.top: adminTab.top;
anchors.topMargin: 2;
RalewayRegular {
id: adminHelpText;
text: "[?]";
size: hifi.fontSizes.tableHeading + 2;
font.capitalization: Font.AllUppercase;
color: hifi.colors.redHighlight;
horizontalAlignment: Text.AlignHCenter;
verticalAlignment: Text.AlignVCenter;
anchors.fill: parent;
}
MouseArea {
anchors.fill: parent;
acceptedButtons: Qt.LeftButton;
hoverEnabled: true;
onClicked: letterbox(hifi.glyphs.question,
"Admin Actions",
"<b>Silence</b> mutes a user's microphone. Silenced users can unmute themselves by clicking &quot;UNMUTE&quot; on their toolbar.<br><br>" +
"<b>Ban</b> removes a user from this domain and prevents them from returning. Admins can un-ban users from the Sandbox Domain Settings page.");
onEntered: adminHelpText.color = "#94132e";
onExited: adminHelpText.color = hifi.colors.redHighlight;
}
}
}
HifiControls.Keyboard {
id: keyboard
raised: myCard.currentlyEditingDisplayName && HMD.active
numeric: parent.punctuationMode
anchors {
bottom: parent.bottom
left: parent.left
right: parent.right
HifiControls.Keyboard {
id: keyboard;
raised: myCard.currentlyEditingDisplayName && HMD.active;
numeric: parent.punctuationMode;
anchors {
bottom: parent.bottom;
left: parent.left;
right: parent.right;
}
}
}
}
// Timer used when selecting table rows that aren't yet present in the model
// (i.e. when selecting avatars using edit.js or sphere overlays)
Timer {
property bool selected // Selected or deselected?
property int userIndex // The userIndex of the avatar we want to select
id: selectionTimer
property bool selected; // Selected or deselected?
property int userIndex; // The userIndex of the avatar we want to select
id: selectionTimer;
onTriggered: {
if (selected) {
table.selection.clear(); // for now, no multi-select
@ -612,9 +623,12 @@ Rectangle {
}
}
function sortModel() {
var sortProperty = table.getColumn(table.sortIndicatorColumn).role;
var column = table.getColumn(table.sortIndicatorColumn);
var sortProperty = column ? column.role : "displayName";
var before = (table.sortIndicatorOrder === Qt.AscendingOrder) ? -1 : 1;
var after = -1 * before;
// get selection(s) before sorting
var selectedIDs = getSelectedSessionIDs();
userModelData.sort(function (a, b) {
var aValue = a[sortProperty].toString().toLowerCase(), bValue = b[sortProperty].toString().toLowerCase();
switch (true) {
@ -627,6 +641,7 @@ Rectangle {
userModel.clear();
var userIndex = 0;
var newSelectedIndexes = [];
userModelData.forEach(function (datum) {
function init(property) {
if (datum[property] === undefined) {
@ -636,7 +651,14 @@ Rectangle {
['personalMute', 'ignore', 'mute', 'kick'].forEach(init);
datum.userIndex = userIndex++;
userModel.append(datum);
if (selectedIDs.indexOf(datum.sessionId) != -1) {
newSelectedIndexes.push(datum.userIndex);
}
});
if (newSelectedIndexes.length > 0) {
table.selection.select(newSelectedIndexes);
table.positionViewAtRow(newSelectedIndexes[0], ListView.Beginning);
}
}
signal sendToScript(var message);
function noticeSelection() {
@ -647,7 +669,7 @@ Rectangle {
pal.sendToScript({method: 'selected', params: userIds});
}
Connections {
target: table.selection
onSelectionChanged: pal.noticeSelection()
target: table.selection;
onSelectionChanged: pal.noticeSelection();
}
}

View file

@ -4236,12 +4236,6 @@ void Application::updateDialogs(float deltaTime) const {
PerformanceWarning warn(showWarnings, "Application::updateDialogs()");
auto dialogsManager = DependencyManager::get<DialogsManager>();
// Update bandwidth dialog, if any
BandwidthDialog* bandwidthDialog = dialogsManager->getBandwidthDialog();
if (bandwidthDialog) {
bandwidthDialog->update();
}
QPointer<OctreeStatsDialog> octreeStatsDialog = dialogsManager->getOctreeStatsDialog();
if (octreeStatsDialog) {
octreeStatsDialog->update();

View file

@ -51,6 +51,7 @@
#include <RunningMarker.h>
#include "avatar/MyAvatar.h"
#include "BandwidthRecorder.h"
#include "Bookmarks.h"
#include "Camera.h"
#include "ConnectionMonitor.h"
@ -61,7 +62,6 @@
#include "scripting/ControllerScriptingInterface.h"
#include "scripting/DialogsManagerScriptingInterface.h"
#include "ui/ApplicationOverlay.h"
#include "ui/BandwidthDialog.h"
#include "ui/EntityScriptServerLogDialog.h"
#include "ui/LodToolsDialog.h"
#include "ui/LogDialog.h"

View file

@ -566,8 +566,6 @@ Menu::Menu() {
dialogsManager.data(), SLOT(toggleDiskCacheEditor()));
addActionToQMenuAndActionHash(networkMenu, MenuOption::ShowDSConnectTable, 0,
dialogsManager.data(), SLOT(showDomainConnectionDialog()));
addActionToQMenuAndActionHash(networkMenu, MenuOption::BandwidthDetails, 0,
dialogsManager.data(), SLOT(bandwidthDetails()));
#if (PR_BUILD || DEV_BUILD)
addCheckableActionToQMenuAndActionHash(networkMenu, MenuOption::SendWrongProtocolVersion, 0, false,

View file

@ -49,7 +49,6 @@ namespace MenuOption {
const QString AutoMuteAudio = "Auto Mute Microphone";
const QString AvatarReceiveStats = "Show Receive Stats";
const QString Back = "Back";
const QString BandwidthDetails = "Bandwidth Details";
const QString BinaryEyelidControl = "Binary Eyelid Control";
const QString BookmarkLocation = "Bookmark Location";
const QString Bookmarks = "Bookmarks";

View file

@ -1,135 +0,0 @@
//
// BandwidthDialog.cpp
// interface/src/ui
//
// Created by Tobias Schwinger on 6/21/13.
// Copyright 2013 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 <cstdio>
#include "BandwidthRecorder.h"
#include "ui/BandwidthDialog.h"
#include <QFormLayout>
#include <QDialogButtonBox>
#include <QPalette>
#include <QColor>
BandwidthChannelDisplay::BandwidthChannelDisplay(QVector<NodeType_t> nodeTypesToFollow,
QFormLayout* form,
char const* const caption, char const* unitCaption,
const float unitScale, unsigned colorRGBA) :
_nodeTypesToFollow(nodeTypesToFollow),
_caption(caption),
_unitCaption(unitCaption),
_unitScale(unitScale),
_colorRGBA(colorRGBA)
{
_label = new QLabel();
_label->setAlignment(Qt::AlignRight);
QPalette palette = _label->palette();
unsigned rgb = colorRGBA >> 8;
rgb = ((rgb & 0xfefefeu) >> 1) + ((rgb & 0xf8f8f8) >> 3);
palette.setColor(QPalette::WindowText, QColor::fromRgb(rgb));
_label->setPalette(palette);
form->addRow(QString(" ") + _caption + " Bandwidth In/Out:", _label);
}
void BandwidthChannelDisplay::bandwidthAverageUpdated() {
float inTotal = 0.;
float outTotal = 0.;
QSharedPointer<BandwidthRecorder> bandwidthRecorder = DependencyManager::get<BandwidthRecorder>();
for (int i = 0; i < _nodeTypesToFollow.size(); ++i) {
inTotal += bandwidthRecorder->getAverageInputKilobitsPerSecond(_nodeTypesToFollow.at(i));
outTotal += bandwidthRecorder->getAverageOutputKilobitsPerSecond(_nodeTypesToFollow.at(i));
}
_strBuf =
QString("").setNum((int) (inTotal * _unitScale)) + "/" +
QString("").setNum((int) (outTotal * _unitScale)) + " " + _unitCaption;
}
void BandwidthChannelDisplay::paint() {
_label->setText(_strBuf);
}
BandwidthDialog::BandwidthDialog(QWidget* parent) :
QDialog(parent, Qt::Window | Qt::WindowCloseButtonHint | Qt::WindowStaysOnTopHint) {
this->setWindowTitle("Bandwidth Details");
// Create layout
QFormLayout* form = new QFormLayout();
form->setSizeConstraint(QLayout::SetFixedSize);
this->QDialog::setLayout(form);
QSharedPointer<BandwidthRecorder> bandwidthRecorder = DependencyManager::get<BandwidthRecorder>();
_allChannelDisplays[0] = _audioChannelDisplay =
new BandwidthChannelDisplay({NodeType::AudioMixer}, form, "Audio", "Kbps", 1.0, COLOR0);
_allChannelDisplays[1] = _avatarsChannelDisplay =
new BandwidthChannelDisplay({NodeType::Agent, NodeType::AvatarMixer}, form, "Avatars", "Kbps", 1.0, COLOR1);
_allChannelDisplays[2] = _octreeChannelDisplay =
new BandwidthChannelDisplay({NodeType::EntityServer}, form, "Octree", "Kbps", 1.0, COLOR2);
_allChannelDisplays[3] = _octreeChannelDisplay =
new BandwidthChannelDisplay({NodeType::DomainServer}, form, "Domain", "Kbps", 1.0, COLOR2);
_allChannelDisplays[4] = _otherChannelDisplay =
new BandwidthChannelDisplay({NodeType::Unassigned}, form, "Other", "Kbps", 1.0, COLOR2);
_allChannelDisplays[5] = _totalChannelDisplay =
new BandwidthChannelDisplay({
NodeType::DomainServer, NodeType::EntityServer,
NodeType::AudioMixer, NodeType::Agent,
NodeType::AvatarMixer, NodeType::Unassigned
}, form, "Total", "Kbps", 1.0, COLOR2);
connect(averageUpdateTimer, SIGNAL(timeout()), this, SLOT(updateTimerTimeout()));
averageUpdateTimer->start(1000);
}
BandwidthDialog::~BandwidthDialog() {
for (unsigned int i = 0; i < _CHANNELCOUNT; i++) {
delete _allChannelDisplays[i];
}
}
void BandwidthDialog::updateTimerTimeout() {
for (unsigned int i = 0; i < _CHANNELCOUNT; i++) {
_allChannelDisplays[i]->bandwidthAverageUpdated();
}
}
void BandwidthDialog::paintEvent(QPaintEvent* event) {
for (unsigned int i=0; i<_CHANNELCOUNT; i++)
_allChannelDisplays[i]->paint();
this->QDialog::paintEvent(event);
}
void BandwidthDialog::reject() {
// Just regularly close upon ESC
this->QDialog::close();
}
void BandwidthDialog::closeEvent(QCloseEvent* event) {
this->QDialog::closeEvent(event);
emit closed();
}

View file

@ -1,94 +0,0 @@
//
// BandwidthDialog.h
// interface/src/ui
//
// Created by Tobias Schwinger on 6/21/13.
// Copyright 2013 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_BandwidthDialog_h
#define hifi_BandwidthDialog_h
#include <QDialog>
#include <QLabel>
#include <QFormLayout>
#include <QVector>
#include <QTimer>
#include "Node.h"
#include "BandwidthRecorder.h"
const unsigned int COLOR0 = 0x33cc99ff;
const unsigned int COLOR1 = 0xffef40c0;
const unsigned int COLOR2 = 0xd0d0d0a0;
class BandwidthChannelDisplay : public QObject {
Q_OBJECT
public:
BandwidthChannelDisplay(QVector<NodeType_t> nodeTypesToFollow,
QFormLayout* form,
char const* const caption, char const* unitCaption, float unitScale, unsigned colorRGBA);
void paint();
private:
QVector<NodeType_t> _nodeTypesToFollow;
QLabel* _label;
QString _strBuf;
char const* const _caption;
char const* _unitCaption;
float const _unitScale;
unsigned _colorRGBA;
public slots:
void bandwidthAverageUpdated();
};
class BandwidthDialog : public QDialog {
Q_OBJECT
public:
BandwidthDialog(QWidget* parent);
~BandwidthDialog();
void paintEvent(QPaintEvent*) override;
private:
BandwidthChannelDisplay* _audioChannelDisplay;
BandwidthChannelDisplay* _avatarsChannelDisplay;
BandwidthChannelDisplay* _octreeChannelDisplay;
BandwidthChannelDisplay* _domainChannelDisplay;
BandwidthChannelDisplay* _otherChannelDisplay;
BandwidthChannelDisplay* _totalChannelDisplay; // sums of all the other channels
static const unsigned int _CHANNELCOUNT = 6;
BandwidthChannelDisplay* _allChannelDisplays[_CHANNELCOUNT];
signals:
void closed();
public slots:
void reject() override;
void updateTimerTimeout();
protected:
// Emits a 'closed' signal when this dialog is closed.
void closeEvent(QCloseEvent*) override;
private:
QTimer* averageUpdateTimer = new QTimer(this);
};
#endif // hifi_BandwidthDialog_h

View file

@ -19,7 +19,6 @@
#include <PathUtils.h>
#include "AddressBarDialog.h"
#include "BandwidthDialog.h"
#include "CachesSizeDialog.h"
#include "ConnectionFailureDialog.h"
#include "DiskCacheEditor.h"
@ -108,20 +107,6 @@ void DialogsManager::cachesSizeDialog() {
_cachesSizeDialog->raise();
}
void DialogsManager::bandwidthDetails() {
if (! _bandwidthDialog) {
_bandwidthDialog = new BandwidthDialog(qApp->getWindow());
connect(_bandwidthDialog, SIGNAL(closed()), _bandwidthDialog, SLOT(deleteLater()));
if (_hmdToolsDialog) {
_hmdToolsDialog->watchWindow(_bandwidthDialog->windowHandle());
}
_bandwidthDialog->show();
}
_bandwidthDialog->raise();
}
void DialogsManager::lodTools() {
if (!_lodToolsDialog) {
maybeCreateDialog(_lodToolsDialog);

View file

@ -21,7 +21,6 @@
class AnimationsDialog;
class AttachmentsDialog;
class BandwidthDialog;
class CachesSizeDialog;
class DiskCacheEditor;
class LodToolsDialog;
@ -36,7 +35,6 @@ class DialogsManager : public QObject, public Dependency {
SINGLETON_DEPENDENCY
public:
QPointer<BandwidthDialog> getBandwidthDialog() const { return _bandwidthDialog; }
QPointer<HMDToolsDialog> getHMDToolsDialog() const { return _hmdToolsDialog; }
QPointer<LodToolsDialog> getLodToolsDialog() const { return _lodToolsDialog; }
QPointer<OctreeStatsDialog> getOctreeStatsDialog() const { return _octreeStatsDialog; }
@ -53,7 +51,6 @@ public slots:
void showLoginDialog();
void octreeStatsDetails();
void cachesSizeDialog();
void bandwidthDetails();
void lodTools();
void hmdTools(bool showTools);
void showScriptEditor();
@ -79,7 +76,6 @@ private:
QPointer<AnimationsDialog> _animationsDialog;
QPointer<AttachmentsDialog> _attachmentsDialog;
QPointer<BandwidthDialog> _bandwidthDialog;
QPointer<CachesSizeDialog> _cachesSizeDialog;
QPointer<DiskCacheEditor> _diskCacheEditor;
QPointer<QMessageBox> _ircInfoBox;

View file

@ -79,9 +79,6 @@ HMDToolsDialog::HMDToolsDialog(QWidget* parent) :
// what screens we're allowed on
watchWindow(windowHandle());
auto dialogsManager = DependencyManager::get<DialogsManager>();
if (dialogsManager->getBandwidthDialog()) {
watchWindow(dialogsManager->getBandwidthDialog()->windowHandle());
}
if (dialogsManager->getOctreeStatsDialog()) {
watchWindow(dialogsManager->getOctreeStatsDialog()->windowHandle());
}

View file

@ -488,13 +488,7 @@ const AnimPoseVec& AnimInverseKinematics::overlay(const AnimVariantMap& animVars
// measure new _hipsOffset for next frame
// by looking for discrepancies between where a targeted endEffector is
// and where it wants to be (after IK solutions are done)
// use weighted average between HMD and other targets
float HMD_WEIGHT = 10.0f;
float OTHER_WEIGHT = 1.0f;
float totalWeight = 0.0f;
glm::vec3 additionalHipsOffset = Vectors::ZERO;
glm::vec3 newHipsOffset = Vectors::ZERO;
for (auto& target: targets) {
int targetIndex = target.getIndex();
if (targetIndex == _headIndex && _headIndex != -1) {
@ -505,42 +499,34 @@ const AnimPoseVec& AnimInverseKinematics::overlay(const AnimVariantMap& animVars
glm::vec3 under = _skeleton->getAbsolutePose(_headIndex, underPoses).trans();
glm::vec3 actual = _skeleton->getAbsolutePose(_headIndex, _relativePoses).trans();
const float HEAD_OFFSET_SLAVE_FACTOR = 0.65f;
additionalHipsOffset += (OTHER_WEIGHT * HEAD_OFFSET_SLAVE_FACTOR) * (under- actual);
totalWeight += OTHER_WEIGHT;
newHipsOffset += HEAD_OFFSET_SLAVE_FACTOR * (actual - under);
} else if (target.getType() == IKTarget::Type::HmdHead) {
// we want to shift the hips to bring the head to its designated position
glm::vec3 actual = _skeleton->getAbsolutePose(_headIndex, _relativePoses).trans();
glm::vec3 thisOffset = target.getTranslation() - actual;
glm::vec3 futureHipsOffset = _hipsOffset + thisOffset;
if (glm::length(glm::vec2(futureHipsOffset.x, futureHipsOffset.z)) < _maxHipsOffsetLength) {
// it is imperative to shift the hips and bring the head to its designated position
// so we slam newHipsOffset here and ignore all other targets
additionalHipsOffset = futureHipsOffset - _hipsOffset;
totalWeight = 0.0f;
break;
} else {
additionalHipsOffset += HMD_WEIGHT * (target.getTranslation() - actual);
totalWeight += HMD_WEIGHT;
}
_hipsOffset += target.getTranslation() - actual;
// and ignore all other targets
newHipsOffset = _hipsOffset;
break;
} else if (target.getType() == IKTarget::Type::RotationAndPosition) {
glm::vec3 actualPosition = _skeleton->getAbsolutePose(targetIndex, _relativePoses).trans();
glm::vec3 targetPosition = target.getTranslation();
newHipsOffset += targetPosition - actualPosition;
// Add downward pressure on the hips
newHipsOffset *= 0.95f;
newHipsOffset -= 1.0f;
}
} else if (target.getType() == IKTarget::Type::RotationAndPosition) {
glm::vec3 actualPosition = _skeleton->getAbsolutePose(targetIndex, _relativePoses).trans();
glm::vec3 targetPosition = target.getTranslation();
additionalHipsOffset += OTHER_WEIGHT * (targetPosition - actualPosition);
totalWeight += OTHER_WEIGHT;
newHipsOffset += targetPosition - actualPosition;
}
}
if (totalWeight > 1.0f) {
additionalHipsOffset /= totalWeight;
}
// Add downward pressure on the hips
additionalHipsOffset *= 0.95f;
additionalHipsOffset -= 1.0f;
// smooth transitions by relaxing _hipsOffset toward the new value
const float HIPS_OFFSET_SLAVE_TIMESCALE = 0.10f;
float tau = dt < HIPS_OFFSET_SLAVE_TIMESCALE ? dt / HIPS_OFFSET_SLAVE_TIMESCALE : 1.0f;
_hipsOffset += additionalHipsOffset * tau;
_hipsOffset += (newHipsOffset - _hipsOffset) * tau;
// clamp the hips offset
float hipsOffsetLength = glm::length(_hipsOffset);

View file

@ -656,6 +656,11 @@ float OpenGLDisplayPlugin::presentRate() const {
return _presentRate.rate();
}
void OpenGLDisplayPlugin::resetPresentRate() {
// FIXME
// _presentRate = RateCounter<100>();
}
float OpenGLDisplayPlugin::renderRate() const {
return _renderRate.rate();
}

View file

@ -59,6 +59,8 @@ public:
float presentRate() const override;
void resetPresentRate() override;
float newFramePresentRate() const override;
float droppedFrameRate() const override;

View file

@ -101,7 +101,7 @@ void EntityTreeRenderer::resetEntitiesScriptEngine() {
// Keep a ref to oldEngine until newEngine is ready so EntityScriptingInterface has something to use
auto oldEngine = _entitiesScriptEngine;
auto newEngine = new ScriptEngine(ScriptEngine::ENTITY_CLIENT_SCRIPT, NO_SCRIPT, QString("Entities %1").arg(++_entitiesScriptEngineCount));
auto newEngine = new ScriptEngine(ScriptEngine::ENTITY_CLIENT_SCRIPT, NO_SCRIPT, QString("about:Entities %1").arg(++_entitiesScriptEngineCount));
_entitiesScriptEngine = QSharedPointer<ScriptEngine>(newEngine, entitiesScriptEngineDeleter);
_scriptingServices->registerScriptEngineWithApplicationServices(_entitiesScriptEngine.data());
@ -148,7 +148,7 @@ void EntityTreeRenderer::reloadEntityScripts() {
_entitiesScriptEngine->unloadAllEntityScripts();
foreach(auto entity, _entitiesInScene) {
if (!entity->getScript().isEmpty()) {
ScriptEngine::loadEntityScript(_entitiesScriptEngine, entity->getEntityItemID(), entity->getScript(), true);
_entitiesScriptEngine->loadEntityScript(entity->getEntityItemID(), entity->getScript(), true);
}
}
}
@ -955,7 +955,7 @@ void EntityTreeRenderer::checkAndCallPreload(const EntityItemID& entityID, const
}
if (shouldLoad && !scriptUrl.isEmpty()) {
scriptUrl = ResourceManager::normalizeURL(scriptUrl);
ScriptEngine::loadEntityScript(_entitiesScriptEngine, entityID, scriptUrl, reload);
_entitiesScriptEngine->loadEntityScript(entityID, scriptUrl, reload);
entity->scriptHasPreloaded();
}
}

View file

@ -418,6 +418,12 @@ void RenderableModelEntityItem::render(RenderArgs* args) {
// Enqueue updates for the next frame
if (_model) {
#ifdef WANT_EXTRA_RENDER_DEBUGGING
// debugging...
gpu::Batch& batch = *args->_batch;
_model->renderDebugMeshBoxes(batch);
#endif
render::ScenePointer scene = AbstractViewStateInterface::instance()->getMain3DScene();
// FIXME: this seems like it could be optimized if we tracked our last known visible state in

View file

@ -163,7 +163,7 @@ void ShapeEntityItem::appendSubclassData(OctreePacketData* packetData, EncodeBit
// This value specifes how the shape should be treated by physics calculations.
// For now, all polys will act as spheres
ShapeType ShapeEntityItem::getShapeType() const {
return (_shape == entity::Shape::Cube) ? SHAPE_TYPE_BOX : SHAPE_TYPE_SPHERE;
return (_shape == entity::Shape::Cube) ? SHAPE_TYPE_BOX : SHAPE_TYPE_ELLIPSOID;
}
void ShapeEntityItem::setColor(const rgbColor& value) {

View file

@ -300,7 +300,7 @@ public:
QHash<QString, FBXMaterial> materials;
glm::mat4 offset;
glm::mat4 offset; // This includes offset, rotation, and scale as specified by the FST file
int leftEyeJointIndex = -1;
int rightEyeJointIndex = -1;

View file

@ -256,9 +256,20 @@ const btCollisionShape* ShapeFactory::createShapeFromInfo(const ShapeInfo& info)
}
break;
case SHAPE_TYPE_SPHERE: {
glm::vec3 halfExtents = info.getHalfExtents();
float radius = glm::max(halfExtents.x, glm::max(halfExtents.y, halfExtents.z));
shape = new btSphereShape(radius);
}
break;
case SHAPE_TYPE_ELLIPSOID: {
glm::vec3 halfExtents = info.getHalfExtents();
float radius = halfExtents.x;
if (radius == halfExtents.y && radius == halfExtents.z) {
const float MIN_RADIUS = 0.001f;
const float MIN_RELATIVE_SPHERICAL_ERROR = 0.001f;
if (radius > MIN_RADIUS
&& fabsf(radius - halfExtents.y) / radius < MIN_RELATIVE_SPHERICAL_ERROR
&& fabsf(radius - halfExtents.z) / radius < MIN_RELATIVE_SPHERICAL_ERROR) {
// close enough to true sphere
shape = new btSphereShape(radius);
} else {
ShapeInfo::PointList points;

View file

@ -139,7 +139,7 @@ public:
/// By default, all HMDs are stereo
virtual bool isStereo() const { return isHmd(); }
virtual bool isThrottled() const { return false; }
virtual float getTargetFrameRate() const { return 0.0f; }
virtual float getTargetFrameRate() const { return 1.0f; }
virtual bool hasAsyncReprojection() const { return false; }
/// Returns a boolean value indicating whether the display is currently visible
@ -189,6 +189,11 @@ public:
virtual float renderRate() const { return -1.0f; }
// Rate at which we present to the display device
virtual float presentRate() const { return -1.0f; }
// Reset the present rate tracking (useful for if the target frame rate changes as in ASW for Oculus)
virtual void resetPresentRate() {}
// Return the present rate as fraction of the target present rate (hopefully 0.0 and 1.0)
virtual float normalizedPresentRate() const { return presentRate() / getTargetFrameRate(); }
// Rate at which old frames are presented to the device display
virtual float stutterRate() const { return -1.0f; }
// Rate at which new frames are being presented to the display device

View file

@ -96,9 +96,6 @@ Model::Model(RigPointer rig, QObject* parent, SpatiallyNestable* spatiallyNestab
_isVisible(true),
_blendNumber(0),
_appliedBlendNumber(0),
_calculatedMeshPartBoxesValid(false),
_calculatedMeshBoxesValid(false),
_calculatedMeshTrianglesValid(false),
_isWireframe(false),
_rig(rig)
{
@ -360,53 +357,43 @@ bool Model::findRayIntersectionAgainstSubMeshes(const glm::vec3& origin, const g
// we can use the AABox's ray intersection by mapping our origin and direction into the model frame
// and testing intersection there.
if (modelFrameBox.findRayIntersection(modelFrameOrigin, modelFrameDirection, distance, face, surfaceNormal)) {
QMutexLocker locker(&_mutex);
float bestDistance = std::numeric_limits<float>::max();
float distanceToSubMesh;
BoxFace subMeshFace;
glm::vec3 subMeshSurfaceNormal;
int subMeshIndex = 0;
const FBXGeometry& geometry = getFBXGeometry();
// If we hit the models box, then consider the submeshes...
_mutex.lock();
if (!_calculatedMeshBoxesValid || (pickAgainstTriangles && !_calculatedMeshTrianglesValid)) {
recalculateMeshBoxes(pickAgainstTriangles);
if (!_triangleSetsValid) {
calculateTriangleSets();
}
for (const auto& subMeshBox : _calculatedMeshBoxes) {
glm::mat4 meshToModelMatrix = glm::scale(_scale) * glm::translate(_offset);
glm::mat4 meshToWorldMatrix = createMatFromQuatAndPos(_rotation, _translation) * meshToModelMatrix;
glm::mat4 worldToMeshMatrix = glm::inverse(meshToWorldMatrix);
if (subMeshBox.findRayIntersection(origin, direction, distanceToSubMesh, subMeshFace, subMeshSurfaceNormal)) {
if (distanceToSubMesh < bestDistance) {
if (pickAgainstTriangles) {
// check our triangles here....
const QVector<Triangle>& meshTriangles = _calculatedMeshTriangles[subMeshIndex];
for(const auto& triangle : meshTriangles) {
float thisTriangleDistance;
if (findRayTriangleIntersection(origin, direction, triangle, thisTriangleDistance)) {
if (thisTriangleDistance < bestDistance) {
bestDistance = thisTriangleDistance;
intersectedSomething = true;
face = subMeshFace;
surfaceNormal = triangle.getNormal();
extraInfo = geometry.getModelNameOfMesh(subMeshIndex);
}
}
}
} else {
// this is the non-triangle picking case...
bestDistance = distanceToSubMesh;
intersectedSomething = true;
face = subMeshFace;
surfaceNormal = subMeshSurfaceNormal;
extraInfo = geometry.getModelNameOfMesh(subMeshIndex);
}
glm::vec3 meshFrameOrigin = glm::vec3(worldToMeshMatrix * glm::vec4(origin, 1.0f));
glm::vec3 meshFrameDirection = glm::vec3(worldToMeshMatrix * glm::vec4(direction, 0.0f));
for (const auto& triangleSet : _modelSpaceMeshTriangleSets) {
float triangleSetDistance = 0.0f;
BoxFace triangleSetFace;
glm::vec3 triangleSetNormal;
if (triangleSet.findRayIntersection(meshFrameOrigin, meshFrameDirection, triangleSetDistance, triangleSetFace, triangleSetNormal, pickAgainstTriangles)) {
glm::vec3 meshIntersectionPoint = meshFrameOrigin + (meshFrameDirection * triangleSetDistance);
glm::vec3 worldIntersectionPoint = glm::vec3(meshToWorldMatrix * glm::vec4(meshIntersectionPoint, 1.0f));
float worldDistance = glm::distance(origin, worldIntersectionPoint);
if (worldDistance < bestDistance) {
bestDistance = worldDistance;
intersectedSomething = true;
face = triangleSetFace;
surfaceNormal = glm::vec3(meshToWorldMatrix * glm::vec4(triangleSetNormal, 0.0f));
extraInfo = geometry.getModelNameOfMesh(subMeshIndex);
}
}
subMeshIndex++;
}
_mutex.unlock();
if (intersectedSomething) {
distance = bestDistance;
@ -442,172 +429,104 @@ bool Model::convexHullContains(glm::vec3 point) {
// we can use the AABox's contains() by mapping our point into the model frame
// and testing there.
if (modelFrameBox.contains(modelFramePoint)){
_mutex.lock();
if (!_calculatedMeshTrianglesValid) {
recalculateMeshBoxes(true);
QMutexLocker locker(&_mutex);
if (!_triangleSetsValid) {
calculateTriangleSets();
}
// If we are inside the models box, then consider the submeshes...
int subMeshIndex = 0;
foreach(const AABox& subMeshBox, _calculatedMeshBoxes) {
if (subMeshBox.contains(point)) {
bool insideMesh = true;
// To be inside the sub mesh, we need to be behind every triangles' planes
const QVector<Triangle>& meshTriangles = _calculatedMeshTriangles[subMeshIndex];
foreach (const Triangle& triangle, meshTriangles) {
if (!isPointBehindTrianglesPlane(point, triangle.v0, triangle.v1, triangle.v2)) {
// it's not behind at least one so we bail
insideMesh = false;
break;
}
glm::mat4 meshToModelMatrix = glm::scale(_scale) * glm::translate(_offset);
glm::mat4 meshToWorldMatrix = createMatFromQuatAndPos(_rotation, _translation) * meshToModelMatrix;
glm::mat4 worldToMeshMatrix = glm::inverse(meshToWorldMatrix);
glm::vec3 meshFramePoint = glm::vec3(worldToMeshMatrix * glm::vec4(point, 1.0f));
}
if (insideMesh) {
for (const auto& triangleSet : _modelSpaceMeshTriangleSets) {
const AABox& box = triangleSet.getBounds();
if (box.contains(meshFramePoint)) {
if (triangleSet.convexHullContains(meshFramePoint)) {
// It's inside this mesh, return true.
_mutex.unlock();
return true;
}
}
subMeshIndex++;
}
_mutex.unlock();
}
// It wasn't in any mesh, return false.
return false;
}
// TODO: we seem to call this too often when things haven't actually changed... look into optimizing this
// Any script might trigger findRayIntersectionAgainstSubMeshes (and maybe convexHullContains), so these
// can occur multiple times. In addition, rendering does it's own ray picking in order to decide which
// entity-scripts to call. I think it would be best to do the picking once-per-frame (in cpu, or gpu if possible)
// and then the calls use the most recent such result.
void Model::recalculateMeshBoxes(bool pickAgainstTriangles) {
void Model::calculateTriangleSets() {
PROFILE_RANGE(render, __FUNCTION__);
bool calculatedMeshTrianglesNeeded = pickAgainstTriangles && !_calculatedMeshTrianglesValid;
if (!_calculatedMeshBoxesValid || calculatedMeshTrianglesNeeded || (!_calculatedMeshPartBoxesValid && pickAgainstTriangles) ) {
const FBXGeometry& geometry = getFBXGeometry();
int numberOfMeshes = geometry.meshes.size();
_calculatedMeshBoxes.resize(numberOfMeshes);
_calculatedMeshTriangles.clear();
_calculatedMeshTriangles.resize(numberOfMeshes);
_calculatedMeshPartBoxes.clear();
for (int i = 0; i < numberOfMeshes; i++) {
const FBXMesh& mesh = geometry.meshes.at(i);
Extents scaledMeshExtents = calculateScaledOffsetExtents(mesh.meshExtents, _translation, _rotation);
const FBXGeometry& geometry = getFBXGeometry();
int numberOfMeshes = geometry.meshes.size();
_calculatedMeshBoxes[i] = AABox(scaledMeshExtents);
_triangleSetsValid = true;
_modelSpaceMeshTriangleSets.clear();
_modelSpaceMeshTriangleSets.resize(numberOfMeshes);
if (pickAgainstTriangles) {
QVector<Triangle> thisMeshTriangles;
for (int j = 0; j < mesh.parts.size(); j++) {
const FBXMeshPart& part = mesh.parts.at(j);
for (int i = 0; i < numberOfMeshes; i++) {
const FBXMesh& mesh = geometry.meshes.at(i);
bool atLeastOnePointInBounds = false;
AABox thisPartBounds;
for (int j = 0; j < mesh.parts.size(); j++) {
const FBXMeshPart& part = mesh.parts.at(j);
const int INDICES_PER_TRIANGLE = 3;
const int INDICES_PER_QUAD = 4;
const int INDICES_PER_TRIANGLE = 3;
const int INDICES_PER_QUAD = 4;
const int TRIANGLES_PER_QUAD = 2;
if (part.quadIndices.size() > 0) {
int numberOfQuads = part.quadIndices.size() / INDICES_PER_QUAD;
int vIndex = 0;
for (int q = 0; q < numberOfQuads; q++) {
int i0 = part.quadIndices[vIndex++];
int i1 = part.quadIndices[vIndex++];
int i2 = part.quadIndices[vIndex++];
int i3 = part.quadIndices[vIndex++];
// tell our triangleSet how many triangles to expect.
int numberOfQuads = part.quadIndices.size() / INDICES_PER_QUAD;
int numberOfTris = part.triangleIndices.size() / INDICES_PER_TRIANGLE;
int totalTriangles = (numberOfQuads * TRIANGLES_PER_QUAD) + numberOfTris;
_modelSpaceMeshTriangleSets[i].reserve(totalTriangles);
glm::vec3 mv0 = glm::vec3(mesh.modelTransform * glm::vec4(mesh.vertices[i0], 1.0f));
glm::vec3 mv1 = glm::vec3(mesh.modelTransform * glm::vec4(mesh.vertices[i1], 1.0f));
glm::vec3 mv2 = glm::vec3(mesh.modelTransform * glm::vec4(mesh.vertices[i2], 1.0f));
glm::vec3 mv3 = glm::vec3(mesh.modelTransform * glm::vec4(mesh.vertices[i3], 1.0f));
auto meshTransform = getFBXGeometry().offset * mesh.modelTransform;
// track the mesh parts in model space
if (!atLeastOnePointInBounds) {
thisPartBounds.setBox(mv0, 0.0f);
atLeastOnePointInBounds = true;
} else {
thisPartBounds += mv0;
}
thisPartBounds += mv1;
thisPartBounds += mv2;
thisPartBounds += mv3;
if (part.quadIndices.size() > 0) {
int vIndex = 0;
for (int q = 0; q < numberOfQuads; q++) {
int i0 = part.quadIndices[vIndex++];
int i1 = part.quadIndices[vIndex++];
int i2 = part.quadIndices[vIndex++];
int i3 = part.quadIndices[vIndex++];
glm::vec3 v0 = calculateScaledOffsetPoint(mv0);
glm::vec3 v1 = calculateScaledOffsetPoint(mv1);
glm::vec3 v2 = calculateScaledOffsetPoint(mv2);
glm::vec3 v3 = calculateScaledOffsetPoint(mv3);
// track the model space version... these points will be transformed by the FST's offset,
// which includes the scaling, rotation, and translation specified by the FST/FBX,
// this can't change at runtime, so we can safely store these in our TriangleSet
glm::vec3 v0 = glm::vec3(meshTransform * glm::vec4(mesh.vertices[i0], 1.0f));
glm::vec3 v1 = glm::vec3(meshTransform * glm::vec4(mesh.vertices[i1], 1.0f));
glm::vec3 v2 = glm::vec3(meshTransform * glm::vec4(mesh.vertices[i2], 1.0f));
glm::vec3 v3 = glm::vec3(meshTransform * glm::vec4(mesh.vertices[i3], 1.0f));
// Sam's recommended triangle slices
Triangle tri1 = { v0, v1, v3 };
Triangle tri2 = { v1, v2, v3 };
// NOTE: Random guy on the internet's recommended triangle slices
//Triangle tri1 = { v0, v1, v2 };
//Triangle tri2 = { v2, v3, v0 };
thisMeshTriangles.push_back(tri1);
thisMeshTriangles.push_back(tri2);
}
}
if (part.triangleIndices.size() > 0) {
int numberOfTris = part.triangleIndices.size() / INDICES_PER_TRIANGLE;
int vIndex = 0;
for (int t = 0; t < numberOfTris; t++) {
int i0 = part.triangleIndices[vIndex++];
int i1 = part.triangleIndices[vIndex++];
int i2 = part.triangleIndices[vIndex++];
glm::vec3 mv0 = glm::vec3(mesh.modelTransform * glm::vec4(mesh.vertices[i0], 1.0f));
glm::vec3 mv1 = glm::vec3(mesh.modelTransform * glm::vec4(mesh.vertices[i1], 1.0f));
glm::vec3 mv2 = glm::vec3(mesh.modelTransform * glm::vec4(mesh.vertices[i2], 1.0f));
// track the mesh parts in model space
if (!atLeastOnePointInBounds) {
thisPartBounds.setBox(mv0, 0.0f);
atLeastOnePointInBounds = true;
} else {
thisPartBounds += mv0;
}
thisPartBounds += mv1;
thisPartBounds += mv2;
glm::vec3 v0 = calculateScaledOffsetPoint(mv0);
glm::vec3 v1 = calculateScaledOffsetPoint(mv1);
glm::vec3 v2 = calculateScaledOffsetPoint(mv2);
Triangle tri = { v0, v1, v2 };
thisMeshTriangles.push_back(tri);
}
}
_calculatedMeshPartBoxes[QPair<int,int>(i, j)] = thisPartBounds;
Triangle tri1 = { v0, v1, v3 };
Triangle tri2 = { v1, v2, v3 };
_modelSpaceMeshTriangleSets[i].insert(tri1);
_modelSpaceMeshTriangleSets[i].insert(tri2);
}
}
if (part.triangleIndices.size() > 0) {
int vIndex = 0;
for (int t = 0; t < numberOfTris; t++) {
int i0 = part.triangleIndices[vIndex++];
int i1 = part.triangleIndices[vIndex++];
int i2 = part.triangleIndices[vIndex++];
// track the model space version... these points will be transformed by the FST's offset,
// which includes the scaling, rotation, and translation specified by the FST/FBX,
// this can't change at runtime, so we can safely store these in our TriangleSet
glm::vec3 v0 = glm::vec3(meshTransform * glm::vec4(mesh.vertices[i0], 1.0f));
glm::vec3 v1 = glm::vec3(meshTransform * glm::vec4(mesh.vertices[i1], 1.0f));
glm::vec3 v2 = glm::vec3(meshTransform * glm::vec4(mesh.vertices[i2], 1.0f));
Triangle tri = { v0, v1, v2 };
_modelSpaceMeshTriangleSets[i].insert(tri);
}
_calculatedMeshTriangles[i] = thisMeshTriangles;
_calculatedMeshPartBoxesValid = true;
}
}
_calculatedMeshBoxesValid = true;
_calculatedMeshTrianglesValid = pickAgainstTriangles;
}
}
void Model::renderSetup(RenderArgs* args) {
// set up dilated textures on first render after load/simulate
const FBXGeometry& geometry = getFBXGeometry();
if (_dilatedTextures.isEmpty()) {
foreach (const FBXMesh& mesh, geometry.meshes) {
QVector<QSharedPointer<Texture> > dilated;
dilated.resize(mesh.parts.size());
_dilatedTextures.append(dilated);
}
}
if (!_addedToScene && isLoaded()) {
createRenderItemSet();
}
}
@ -723,7 +642,17 @@ void Model::removeFromScene(std::shared_ptr<render::Scene> scene, render::Pendin
void Model::renderDebugMeshBoxes(gpu::Batch& batch) {
int colorNdx = 0;
_mutex.lock();
foreach(AABox box, _calculatedMeshBoxes) {
glm::mat4 meshToModelMatrix = glm::scale(_scale) * glm::translate(_offset);
glm::mat4 meshToWorldMatrix = createMatFromQuatAndPos(_rotation, _translation) * meshToModelMatrix;
Transform meshToWorld(meshToWorldMatrix);
batch.setModelTransform(meshToWorld);
DependencyManager::get<GeometryCache>()->bindSimpleProgram(batch, false, false, false, true, true);
for(const auto& triangleSet : _modelSpaceMeshTriangleSets) {
auto box = triangleSet.getBounds();
if (_debugMeshBoxesID == GeometryCache::UNKNOWN_ID) {
_debugMeshBoxesID = DependencyManager::get<GeometryCache>()->allocateID();
}
@ -755,8 +684,8 @@ void Model::renderDebugMeshBoxes(gpu::Batch& batch) {
points << blf << tlf;
glm::vec4 color[] = {
{ 1.0f, 0.0f, 0.0f, 1.0f }, // red
{ 0.0f, 1.0f, 0.0f, 1.0f }, // green
{ 1.0f, 0.0f, 0.0f, 1.0f }, // red
{ 0.0f, 0.0f, 1.0f, 1.0f }, // blue
{ 1.0f, 0.0f, 1.0f, 1.0f }, // purple
{ 1.0f, 1.0f, 0.0f, 1.0f }, // yellow
@ -814,37 +743,6 @@ Extents Model::getUnscaledMeshExtents() const {
return scaledExtents;
}
Extents Model::calculateScaledOffsetExtents(const Extents& extents,
glm::vec3 modelPosition, glm::quat modelOrientation) const {
// we need to include any fst scaling, translation, and rotation, which is captured in the offset matrix
glm::vec3 minimum = glm::vec3(getFBXGeometry().offset * glm::vec4(extents.minimum, 1.0f));
glm::vec3 maximum = glm::vec3(getFBXGeometry().offset * glm::vec4(extents.maximum, 1.0f));
Extents scaledOffsetExtents = { ((minimum + _offset) * _scale),
((maximum + _offset) * _scale) };
Extents rotatedExtents = scaledOffsetExtents.getRotated(modelOrientation);
Extents translatedExtents = { rotatedExtents.minimum + modelPosition,
rotatedExtents.maximum + modelPosition };
return translatedExtents;
}
/// Returns the world space equivalent of some box in model space.
AABox Model::calculateScaledOffsetAABox(const AABox& box, glm::vec3 modelPosition, glm::quat modelOrientation) const {
return AABox(calculateScaledOffsetExtents(Extents(box), modelPosition, modelOrientation));
}
glm::vec3 Model::calculateScaledOffsetPoint(const glm::vec3& point) const {
// we need to include any fst scaling, translation, and rotation, which is captured in the offset matrix
glm::vec3 offsetPoint = glm::vec3(getFBXGeometry().offset * glm::vec4(point, 1.0f));
glm::vec3 scaledPoint = ((offsetPoint + _offset) * _scale);
glm::vec3 rotatedPoint = _rotation * scaledPoint;
glm::vec3 translatedPoint = rotatedPoint + _translation;
return translatedPoint;
}
void Model::clearJointState(int index) {
_rig->clearJointState(index);
}
@ -1126,12 +1024,6 @@ void Model::simulate(float deltaTime, bool fullUpdate) {
|| (_snapModelToRegistrationPoint && !_snappedToRegistrationPoint);
if (isActive() && fullUpdate) {
// NOTE: This is overly aggressive and we are invalidating the MeshBoxes when in fact they may not be invalid
// they really only become invalid if something about the transform to world space has changed. This is
// not too bad at this point, because it doesn't impact rendering. However it does slow down ray picking
// because ray picking needs valid boxes to work
_calculatedMeshBoxesValid = false;
_calculatedMeshTrianglesValid = false;
onInvalidate();
// check for scale to fit

View file

@ -28,6 +28,7 @@
#include <render/Scene.h>
#include <Transform.h>
#include <SpatiallyNestable.h>
#include <TriangleSet.h>
#include "GeometryCache.h"
#include "TextureCache.h"
@ -95,7 +96,6 @@ public:
render::PendingChanges& pendingChanges,
render::Item::Status::Getters& statusGetters);
void removeFromScene(std::shared_ptr<render::Scene> scene, render::PendingChanges& pendingChanges);
void renderSetup(RenderArgs* args);
bool isRenderable() const;
bool isVisible() const { return _isVisible; }
@ -250,6 +250,9 @@ public:
uint32_t getGeometryCounter() const { return _deleteGeometryCounter; }
const QMap<render::ItemID, render::PayloadPointer>& getRenderItems() const { return _modelMeshRenderItems; }
void renderDebugMeshBoxes(gpu::Batch& batch);
public slots:
void loadURLFinished(bool success);
@ -266,15 +269,6 @@ protected:
/// Returns the unscaled extents of the model's mesh
Extents getUnscaledMeshExtents() const;
/// Returns the scaled equivalent of some extents in model space.
Extents calculateScaledOffsetExtents(const Extents& extents, glm::vec3 modelPosition, glm::quat modelOrientation) const;
/// Returns the world space equivalent of some box in model space.
AABox calculateScaledOffsetAABox(const AABox& box, glm::vec3 modelPosition, glm::quat modelOrientation) const;
/// Returns the scaled equivalent of a point in model space.
glm::vec3 calculateScaledOffsetPoint(const glm::vec3& point) const;
/// Clear the joint states
void clearJointState(int index);
@ -293,9 +287,13 @@ protected:
SpatiallyNestable* _spatiallyNestableOverride;
glm::vec3 _translation;
glm::vec3 _translation; // this is the translation in world coordinates to the model's registration point
glm::quat _rotation;
glm::vec3 _scale;
// For entity models this is the translation for the minimum extent of the model (in original mesh coordinate space)
// to the model's registration point. For avatar models this is the translation from the avatar's hips, as determined
// by the default pose, to the origin.
glm::vec3 _offset;
static float FAKE_DIMENSION_PLACEHOLDER;
@ -331,14 +329,13 @@ protected:
/// Allow sub classes to force invalidating the bboxes
void invalidCalculatedMeshBoxes() {
_calculatedMeshBoxesValid = false;
_calculatedMeshPartBoxesValid = false;
_calculatedMeshTrianglesValid = false;
_triangleSetsValid = false;
}
// hook for derived classes to be notified when setUrl invalidates the current model.
virtual void onInvalidate() {};
protected:
virtual void deleteGeometry();
@ -357,17 +354,12 @@ protected:
int _blendNumber;
int _appliedBlendNumber;
QHash<QPair<int,int>, AABox> _calculatedMeshPartBoxes; // world coordinate AABoxes for all sub mesh part boxes
bool _calculatedMeshPartBoxesValid;
QVector<AABox> _calculatedMeshBoxes; // world coordinate AABoxes for all sub mesh boxes
bool _calculatedMeshBoxesValid;
QVector< QVector<Triangle> > _calculatedMeshTriangles; // world coordinate triangles for all sub meshes
bool _calculatedMeshTrianglesValid;
QMutex _mutex;
void recalculateMeshBoxes(bool pickAgainstTriangles = false);
bool _triangleSetsValid { false };
void calculateTriangleSets();
QVector<TriangleSet> _modelSpaceMeshTriangleSets; // model space triangles for all sub meshes
void createRenderItemSet();
virtual void createVisibleRenderItemSet();
@ -376,7 +368,6 @@ protected:
bool _isWireframe;
// debug rendering support
void renderDebugMeshBoxes(gpu::Batch& batch);
int _debugMeshBoxesID = GeometryCache::UNKNOWN_ID;

View file

@ -165,6 +165,9 @@ void initDeferredPipelines(render::ShapePlumber& plumber) {
addPipeline(
Key::Builder().withMaterial(),
modelVertex, modelPixel);
addPipeline(
Key::Builder(),
modelVertex, modelPixel);
addPipeline(
Key::Builder().withMaterial().withUnlit(),
modelVertex, modelUnlitPixel);

View file

@ -0,0 +1,287 @@
//
// BaseScriptEngine.cpp
// libraries/script-engine/src
//
// Created by Timothy Dedischew on 02/01/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
//
#include "BaseScriptEngine.h"
#include <QtCore/QString>
#include <QtCore/QThread>
#include <QtCore/QUrl>
#include <QtScript/QScriptValue>
#include <QtScript/QScriptValueIterator>
#include <QtScript/QScriptContextInfo>
#include "ScriptEngineLogging.h"
#include "Profile.h"
const QString BaseScriptEngine::_SETTINGS_ENABLE_EXTENDED_EXCEPTIONS {
"com.highfidelity.experimental.enableExtendedJSExceptions"
};
const QString BaseScriptEngine::SCRIPT_EXCEPTION_FORMAT { "[%0] %1 in %2:%3" };
const QString BaseScriptEngine::SCRIPT_BACKTRACE_SEP { "\n " };
// engine-aware JS Error copier and factory
QScriptValue BaseScriptEngine::makeError(const QScriptValue& _other, const QString& type) {
auto other = _other;
if (other.isString()) {
other = newObject();
other.setProperty("message", _other.toString());
}
auto proto = globalObject().property(type);
if (!proto.isFunction()) {
proto = globalObject().property(other.prototype().property("constructor").property("name").toString());
}
if (!proto.isFunction()) {
#ifdef DEBUG_JS_EXCEPTIONS
qCDebug(scriptengine) << "BaseScriptEngine::makeError -- couldn't find constructor for" << type << " -- using Error instead";
#endif
proto = globalObject().property("Error");
}
if (other.engine() != this) {
// JS Objects are parented to a specific script engine instance
// -- this effectively ~clones it locally by routing through a QVariant and back
other = toScriptValue(other.toVariant());
}
// ~ var err = new Error(other.message)
auto err = proto.construct(QScriptValueList({other.property("message")}));
// transfer over any existing properties
QScriptValueIterator it(other);
while (it.hasNext()) {
it.next();
err.setProperty(it.name(), it.value());
}
return err;
}
// check syntax and when there are issues returns an actual "SyntaxError" with the details
QScriptValue BaseScriptEngine::lintScript(const QString& sourceCode, const QString& fileName, const int lineNumber) {
const auto syntaxCheck = checkSyntax(sourceCode);
if (syntaxCheck.state() != QScriptSyntaxCheckResult::Valid) {
auto err = globalObject().property("SyntaxError")
.construct(QScriptValueList({syntaxCheck.errorMessage()}));
err.setProperty("fileName", fileName);
err.setProperty("lineNumber", syntaxCheck.errorLineNumber());
err.setProperty("expressionBeginOffset", syntaxCheck.errorColumnNumber());
err.setProperty("stack", currentContext()->backtrace().join(SCRIPT_BACKTRACE_SEP));
{
const auto error = syntaxCheck.errorMessage();
const auto line = QString::number(syntaxCheck.errorLineNumber());
const auto column = QString::number(syntaxCheck.errorColumnNumber());
// for compatibility with legacy reporting
const auto message = QString("[SyntaxError] %1 in %2:%3(%4)").arg(error, fileName, line, column);
err.setProperty("formatted", message);
}
return err;
}
return undefinedValue();
}
// this pulls from the best available information to create a detailed snapshot of the current exception
QScriptValue BaseScriptEngine::cloneUncaughtException(const QString& extraDetail) {
if (!hasUncaughtException()) {
return QScriptValue();
}
auto exception = uncaughtException();
// ensure the error object is engine-local
auto err = makeError(exception);
// not sure why Qt does't offer uncaughtExceptionFileName -- but the line number
// on its own is often useless/wrong if arbitrarily married to a filename.
// when the error object already has this info, it seems to be the most reliable
auto fileName = exception.property("fileName").toString();
auto lineNumber = exception.property("lineNumber").toInt32();
// the backtrace, on the other hand, seems most reliable taken from uncaughtExceptionBacktrace
auto backtrace = uncaughtExceptionBacktrace();
if (backtrace.isEmpty()) {
// fallback to the error object
backtrace = exception.property("stack").toString().split(SCRIPT_BACKTRACE_SEP);
}
// the ad hoc "detail" property can be used now to embed additional clues
auto detail = exception.property("detail").toString();
if (detail.isEmpty()) {
detail = extraDetail;
} else if (!extraDetail.isEmpty()) {
detail += "(" + extraDetail + ")";
}
if (lineNumber <= 0) {
lineNumber = uncaughtExceptionLineNumber();
}
if (fileName.isEmpty()) {
// climb the stack frames looking for something useful to display
for (auto c = currentContext(); c && fileName.isEmpty(); c = c->parentContext()) {
QScriptContextInfo info { c };
if (!info.fileName().isEmpty()) {
// take fileName:lineNumber as a pair
fileName = info.fileName();
lineNumber = info.lineNumber();
if (backtrace.isEmpty()) {
backtrace = c->backtrace();
}
break;
}
}
}
err.setProperty("fileName", fileName);
err.setProperty("lineNumber", lineNumber );
err.setProperty("detail", detail);
err.setProperty("stack", backtrace.join(SCRIPT_BACKTRACE_SEP));
#ifdef DEBUG_JS_EXCEPTIONS
err.setProperty("_fileName", exception.property("fileName").toString());
err.setProperty("_stack", uncaughtExceptionBacktrace().join(SCRIPT_BACKTRACE_SEP));
err.setProperty("_lineNumber", uncaughtExceptionLineNumber());
#endif
return err;
}
QString BaseScriptEngine::formatException(const QScriptValue& exception) {
QString note { "UncaughtException" };
QString result;
if (!exception.isObject()) {
return result;
}
const auto message = exception.toString();
const auto fileName = exception.property("fileName").toString();
const auto lineNumber = exception.property("lineNumber").toString();
const auto stacktrace = exception.property("stack").toString();
if (_enableExtendedJSExceptions.get()) {
// This setting toggles display of the hints now being added during the loading process.
// Example difference:
// [UncaughtExceptions] Error: Can't find variable: foobar in atp:/myentity.js\n...
// [UncaughtException (construct {1eb5d3fa-23b1-411c-af83-163af7220e3f})] Error: Can't find variable: foobar in atp:/myentity.js\n...
if (exception.property("detail").isValid()) {
note += " " + exception.property("detail").toString();
}
}
result = QString(SCRIPT_EXCEPTION_FORMAT).arg(note, message, fileName, lineNumber);
if (!stacktrace.isEmpty()) {
result += QString("\n[Backtrace]%1%2").arg(SCRIPT_BACKTRACE_SEP).arg(stacktrace);
}
return result;
}
QScriptValue BaseScriptEngine::evaluateInClosure(const QScriptValue& closure, const QScriptProgram& program) {
PROFILE_RANGE(script, "evaluateInClosure");
if (QThread::currentThread() != thread()) {
qCCritical(scriptengine) << "*** CRITICAL *** ScriptEngine::evaluateInClosure() is meant to be called from engine thread only.";
// note: a recursive mutex might be needed around below code if this method ever becomes Q_INVOKABLE
return QScriptValue();
}
const auto fileName = program.fileName();
const auto shortName = QUrl(fileName).fileName();
QScriptValue result;
QScriptValue oldGlobal;
auto global = closure.property("global");
if (global.isObject()) {
#ifdef DEBUG_JS
qCDebug(scriptengine) << " setting global = closure.global" << shortName;
#endif
oldGlobal = globalObject();
setGlobalObject(global);
}
auto context = pushContext();
auto thiz = closure.property("this");
if (thiz.isObject()) {
#ifdef DEBUG_JS
qCDebug(scriptengine) << " setting this = closure.this" << shortName;
#endif
context->setThisObject(thiz);
}
context->pushScope(closure);
#ifdef DEBUG_JS
qCDebug(scriptengine) << QString("[%1] evaluateInClosure %2").arg(isEvaluating()).arg(shortName);
#endif
{
result = BaseScriptEngine::evaluate(program);
if (hasUncaughtException()) {
auto err = cloneUncaughtException(__FUNCTION__);
#ifdef DEBUG_JS_EXCEPTIONS
qCWarning(scriptengine) << __FUNCTION__ << "---------- hasCaught:" << err.toString() << result.toString();
err.setProperty("_result", result);
#endif
result = err;
}
}
#ifdef DEBUG_JS
qCDebug(scriptengine) << QString("[%1] //evaluateInClosure %2").arg(isEvaluating()).arg(shortName);
#endif
popContext();
if (oldGlobal.isValid()) {
#ifdef DEBUG_JS
qCDebug(scriptengine) << " restoring global" << shortName;
#endif
setGlobalObject(oldGlobal);
}
return result;
}
// Lambda
QScriptValue BaseScriptEngine::newLambdaFunction(std::function<QScriptValue(QScriptContext *, QScriptEngine*)> operation, const QScriptValue& data, const QScriptEngine::ValueOwnership& ownership) {
auto lambda = new Lambda(this, operation, data);
auto object = newQObject(lambda, ownership);
auto call = object.property("call");
call.setPrototype(object); // context->callee().prototype() === Lambda QObject
call.setData(data); // context->callee().data() will === data param
return call;
}
QString Lambda::toString() const {
return QString("[Lambda%1]").arg(data.isValid() ? " " + data.toString() : data.toString());
}
Lambda::~Lambda() {
#ifdef DEBUG_JS_LAMBDA_FUNCS
qDebug() << "~Lambda" << "this" << this;
#endif
}
Lambda::Lambda(QScriptEngine *engine, std::function<QScriptValue(QScriptContext *, QScriptEngine*)> operation, QScriptValue data)
: engine(engine), operation(operation), data(data) {
#ifdef DEBUG_JS_LAMBDA_FUNCS
qDebug() << "Lambda" << data.toString();
#endif
}
QScriptValue Lambda::call() {
return operation(engine->currentContext(), engine);
}
#ifdef DEBUG_JS
void BaseScriptEngine::_debugDump(const QString& header, const QScriptValue& object, const QString& footer) {
if (!header.isEmpty()) {
qCDebug(scriptengine) << header;
}
if (!object.isObject()) {
qCDebug(scriptengine) << "(!isObject)" << object.toVariant().toString() << object.toString();
return;
}
QScriptValueIterator it(object);
while (it.hasNext()) {
it.next();
qCDebug(scriptengine) << it.name() << ":" << it.value().toString();
}
if (!footer.isEmpty()) {
qCDebug(scriptengine) << footer;
}
}
#endif

View file

@ -0,0 +1,67 @@
//
// BaseScriptEngine.h
// libraries/script-engine/src
//
// Created by Timothy Dedischew on 02/01/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
//
#ifndef hifi_BaseScriptEngine_h
#define hifi_BaseScriptEngine_h
#include <functional>
#include <QtCore/QDebug>
#include <QtScript/QScriptEngine>
#include "SettingHandle.h"
// common base class for extending QScriptEngine itself
class BaseScriptEngine : public QScriptEngine {
Q_OBJECT
public:
static const QString SCRIPT_EXCEPTION_FORMAT;
static const QString SCRIPT_BACKTRACE_SEP;
BaseScriptEngine() {}
Q_INVOKABLE QScriptValue evaluateInClosure(const QScriptValue& locals, const QScriptProgram& program);
Q_INVOKABLE QScriptValue lintScript(const QString& sourceCode, const QString& fileName, const int lineNumber = 1);
Q_INVOKABLE QScriptValue makeError(const QScriptValue& other = QScriptValue(), const QString& type = "Error");
Q_INVOKABLE QString formatException(const QScriptValue& exception);
QScriptValue cloneUncaughtException(const QString& detail = QString());
signals:
void unhandledException(const QScriptValue& exception);
protected:
void _emitUnhandledException(const QScriptValue& exception);
QScriptValue newLambdaFunction(std::function<QScriptValue(QScriptContext *context, QScriptEngine* engine)> operation, const QScriptValue& data = QScriptValue(), const QScriptEngine::ValueOwnership& ownership = QScriptEngine::AutoOwnership);
static const QString _SETTINGS_ENABLE_EXTENDED_EXCEPTIONS;
Setting::Handle<bool> _enableExtendedJSExceptions { _SETTINGS_ENABLE_EXTENDED_EXCEPTIONS, true };
#ifdef DEBUG_JS
static void _debugDump(const QString& header, const QScriptValue& object, const QString& footer = QString());
#endif
};
// Lambda helps create callable QScriptValues out of std::functions:
// (just meant for use from within the script engine itself)
class Lambda : public QObject {
Q_OBJECT
public:
Lambda(QScriptEngine *engine, std::function<QScriptValue(QScriptContext *context, QScriptEngine* engine)> operation, QScriptValue data);
~Lambda();
public slots:
QScriptValue call();
QString toString() const;
private:
QScriptEngine* engine;
std::function<QScriptValue(QScriptContext *context, QScriptEngine* engine)> operation;
QScriptValue data;
};
#endif // hifi_BaseScriptEngine_h

View file

@ -66,7 +66,7 @@ void BatchLoader::start(int maxRetries) {
qCDebug(scriptengine) << "Loaded: " << url;
} else {
_data.insert(url, QString());
qCDebug(scriptengine) << "Could not load: " << url;
qCDebug(scriptengine) << "Could not load: " << url << status;
}
if (!_finished && _urls.size() == _data.size()) {

View file

@ -188,6 +188,8 @@ void ScriptCache::scriptContentAvailable(int maxRetries) {
}
}
} else {
qCWarning(scriptengine) << "Warning: scriptContentAvailable for inactive url: " << url;
}
}

View file

@ -43,6 +43,9 @@ class ScriptCache : public QObject, public Dependency {
public:
static const QString STATUS_INLINE;
static const QString STATUS_CACHED;
static bool isSuccessStatus(const QString& status) {
return status == "Success" || status == STATUS_INLINE || status == STATUS_CACHED;
}
void clearCache();
Q_INVOKABLE void clearATPScriptsFromCache();

View file

@ -50,6 +50,7 @@
#include "ArrayBufferViewClass.h"
#include "BatchLoader.h"
#include "BaseScriptEngine.h"
#include "DataViewClass.h"
#include "EventTypes.h"
#include "FileScriptingInterface.h" // unzip project
@ -69,9 +70,12 @@
#include "MIDIEvent.h"
const QString BaseScriptEngine::SCRIPT_EXCEPTION_FORMAT { "[UncaughtException] %1 in %2:%3" };
static const QScriptEngine::QObjectWrapOptions DEFAULT_QOBJECT_WRAP_OPTIONS =
QScriptEngine::ExcludeDeleteLater | QScriptEngine::ExcludeChildObjects;
static const QScriptValue::PropertyFlags READONLY_PROP_FLAGS { QScriptValue::ReadOnly | QScriptValue::Undeletable };
static const QScriptValue::PropertyFlags READONLY_HIDDEN_PROP_FLAGS { READONLY_PROP_FLAGS | QScriptValue::SkipInEnumeration };
static const bool HIFI_AUTOREFRESH_FILE_SCRIPTS { true };
@ -90,13 +94,9 @@ static QScriptValue debugPrint(QScriptContext* context, QScriptEngine* engine){
}
qCDebug(scriptengineScript).noquote() << "script:print()<<" << message; // noquote() so that \n is treated as newline
message = message.replace("\\", "\\\\")
.replace("\n", "\\n")
.replace("\r", "\\r")
.replace("'", "\\'");
// FIXME - this approach neeeds revisiting. print() comes here, which ends up doing an evaluate?
engine->evaluate("Script.print('" + message + "')");
// FIXME - this approach neeeds revisiting. print() comes here, which ends up calling Script.print?
engine->globalObject().property("Script").property("print")
.call(engine->nullValue(), QScriptValueList({ message }));
return QScriptValue();
}
@ -140,52 +140,15 @@ QString encodeEntityIdIntoEntityUrl(const QString& url, const QString& entityID)
return url + " [EntityID:" + entityID + "]";
}
QString BaseScriptEngine::lintScript(const QString& sourceCode, const QString& fileName, const int lineNumber) {
const auto syntaxCheck = checkSyntax(sourceCode);
if (syntaxCheck.state() != syntaxCheck.Valid) {
const auto error = syntaxCheck.errorMessage();
const auto line = QString::number(syntaxCheck.errorLineNumber());
const auto column = QString::number(syntaxCheck.errorColumnNumber());
const auto message = QString("[SyntaxError] %1 in %2:%3(%4)").arg(error, fileName, line, column);
return message;
}
return QString();
}
QString BaseScriptEngine::formatUncaughtException(const QString& overrideFileName) {
QString message;
if (hasUncaughtException()) {
const auto error = uncaughtException();
const auto backtrace = uncaughtExceptionBacktrace();
const auto exception = error.toString();
auto filename = overrideFileName;
if (filename.isEmpty()) {
QScriptContextInfo ctx { currentContext() };
filename = ctx.fileName();
}
const auto line = QString::number(uncaughtExceptionLineNumber());
message = QString(SCRIPT_EXCEPTION_FORMAT).arg(exception, overrideFileName, line);
if (!backtrace.empty()) {
static const auto lineSeparator = "\n ";
message += QString("\n[Backtrace]%1%2").arg(lineSeparator, backtrace.join(lineSeparator));
}
}
return message;
}
QString ScriptEngine::reportUncaughtException(const QString& overrideFileName) {
QString message;
if (!hasUncaughtException()) {
return message;
}
message = formatUncaughtException(overrideFileName.isEmpty() ? _fileNameString : overrideFileName);
QString ScriptEngine::logException(const QScriptValue& exception) {
auto message = formatException(exception);
scriptErrorMessage(qPrintable(message));
return message;
}
int ScriptEngine::processLevelMaxRetries { ScriptRequest::MAX_RETRIES };
ScriptEngine::ScriptEngine(Context context, const QString& scriptContents, const QString& fileNameString) :
BaseScriptEngine(),
_context(context),
_scriptContents(scriptContents),
_timerFunctionMap(),
@ -195,16 +158,30 @@ ScriptEngine::ScriptEngine(Context context, const QString& scriptContents, const
DependencyManager::get<ScriptEngines>()->addScriptEngine(this);
connect(this, &QScriptEngine::signalHandlerException, this, [this](const QScriptValue& exception) {
reportUncaughtException();
clearExceptions();
});
if (hasUncaughtException()) {
// the engine's uncaughtException() seems to produce much better stack traces here
emit unhandledException(cloneUncaughtException("signalHandlerException"));
clearExceptions();
} else {
// ... but may not always be available -- so if needed we fallback to the passed exception
emit unhandledException(exception);
}
}, Qt::DirectConnection);
setProcessEventsInterval(MSECS_PER_SECOND);
if (isEntityServerScript()) {
qCDebug(scriptengine) << "isEntityServerScript() -- limiting maxRetries to 1";
processLevelMaxRetries = 1;
}
qCDebug(scriptengine) << getContext() << "processLevelMaxRetries =" << processLevelMaxRetries;
// this is where all unhandled exceptions end up getting logged
connect(this, &BaseScriptEngine::unhandledException, this, [this](const QScriptValue& err) {
auto output = err.engine() == this ? err : makeError(err);
if (!output.property("detail").isValid()) {
output.setProperty("detail", "UnhandledException");
}
logException(output);
});
}
QString ScriptEngine::getContext() const {
@ -224,13 +201,22 @@ QString ScriptEngine::getContext() const {
}
ScriptEngine::~ScriptEngine() {
// FIXME: are these scriptInfoMessage/scriptWarningMessage segfaulting anybody else at app shutdown?
#if !defined(Q_OS_LINUX)
scriptInfoMessage("Script Engine shutting down:" + getFilename());
#else
qCDebug(scriptengine) << "~ScriptEngine()" << this;
#endif
auto scriptEngines = DependencyManager::get<ScriptEngines>();
if (scriptEngines) {
scriptEngines->removeScriptEngine(this);
} else {
#if !defined(Q_OS_LINUX)
scriptWarningMessage("Script destroyed after ScriptEngines!");
#else
qCWarning(scriptengine) << ("Script destroyed after ScriptEngines!");
#endif
}
}
@ -320,9 +306,12 @@ void ScriptEngine::runDebuggable() {
}
}
_lastUpdate = now;
// Debug and clear exceptions
if (hasUncaughtException()) {
reportUncaughtException();
// only clear exceptions if we are not in the middle of evaluating
if (!isEvaluating() && hasUncaughtException()) {
qCWarning(scriptengine) << __FUNCTION__ << "---------- UNCAUGHT EXCEPTION --------";
qCWarning(scriptengine) << "runDebuggable" << uncaughtException().toString();
logException(__FUNCTION__);
clearExceptions();
}
});
@ -357,10 +346,9 @@ void ScriptEngine::runInThread() {
workerThread->start();
}
void ScriptEngine::executeOnScriptThread(std::function<void()> function, bool blocking ) {
void ScriptEngine::executeOnScriptThread(std::function<void()> function, const Qt::ConnectionType& type ) {
if (QThread::currentThread() != thread()) {
QMetaObject::invokeMethod(this, "executeOnScriptThread", blocking ? Qt::BlockingQueuedConnection : Qt::QueuedConnection,
Q_ARG(std::function<void()>, function));
QMetaObject::invokeMethod(this, "executeOnScriptThread", type, Q_ARG(std::function<void()>, function));
return;
}
@ -559,6 +547,7 @@ void ScriptEngine::init() {
qCWarning(scriptengine) << "deletingEntity while entity script is still running!" << entityID;
}
_entityScripts.remove(entityID);
emit entityScriptDetailsUpdated();
}
});
@ -591,8 +580,7 @@ void ScriptEngine::init() {
QScriptValue webSocketConstructorValue = newFunction(WebSocketClass::constructor);
globalObject().setProperty("WebSocket", webSocketConstructorValue);
QScriptValue printConstructorValue = newFunction(debugPrint);
globalObject().setProperty("print", printConstructorValue);
globalObject().setProperty("print", newFunction(debugPrint));
QScriptValue audioEffectOptionsConstructorValue = newFunction(AudioEffectOptions::constructor);
globalObject().setProperty("AudioEffectOptions", audioEffectOptionsConstructorValue);
@ -606,6 +594,7 @@ void ScriptEngine::init() {
qScriptRegisterMetaType(this, wscReadyStateToScriptValue, wscReadyStateFromScriptValue);
registerGlobalObject("Script", this);
registerGlobalObject("Audio", &AudioScriptingInterface::getInstance());
registerGlobalObject("Entities", entityScriptingInterface.data());
registerGlobalObject("Quat", &_quatLibrary);
@ -874,7 +863,6 @@ void ScriptEngine::addEventHandler(const EntityItemID& entityID, const QString&
handlersForEvent << handlerData; // Note that the same handler can be added many times. See removeEntityEventHandler().
}
QScriptValue ScriptEngine::evaluate(const QString& sourceCode, const QString& fileName, int lineNumber) {
if (DependencyManager::get<ScriptEngines>()->isStopped()) {
return QScriptValue(); // bail early
@ -896,23 +884,30 @@ QScriptValue ScriptEngine::evaluate(const QString& sourceCode, const QString& fi
// Check syntax
auto syntaxError = lintScript(sourceCode, fileName);
if (syntaxError.isError()) {
if (isEvaluating()) {
currentContext()->throwValue(syntaxError);
} else {
syntaxError.setProperty("detail", "evaluate");
emit unhandledException(syntaxError);
}
return syntaxError;
}
QScriptProgram program { sourceCode, fileName, lineNumber };
if (!syntaxError.isEmpty() || program.isNull()) {
scriptErrorMessage(qPrintable(syntaxError));
return QScriptValue();
if (program.isNull()) {
// can this happen?
auto err = makeError("could not create QScriptProgram for " + fileName);
emit unhandledException(err);
return err;
}
++_evaluatesPending;
auto result = BaseScriptEngine::evaluate(program);
--_evaluatesPending;
if (hasUncaughtException()) {
result = uncaughtException();
reportUncaughtException(program.fileName());
emit evaluationFinished(result, true);
clearExceptions();
} else {
emit evaluationFinished(result, false);
QScriptValue result;
{
result = BaseScriptEngine::evaluate(program);
if (!isEvaluating() && hasUncaughtException()) {
emit unhandledException(cloneUncaughtException(__FUNCTION__));
clearExceptions();
}
}
return result;
}
@ -933,8 +928,13 @@ void ScriptEngine::run() {
_isRunning = true;
emit runningStateChanged();
QScriptValue result = evaluate(_scriptContents, _fileNameString);
{
evaluate(_scriptContents, _fileNameString);
if (!isEvaluating() && hasUncaughtException()) {
emit unhandledException(cloneUncaughtException(__FUNCTION__));
clearExceptions();
}
}
#ifdef _WIN32
// VS13 does not sleep_until unless it uses the system_clock, see:
// https://www.reddit.com/r/cpp_questions/comments/3o71ic/sleep_until_not_working_with_a_time_pointsteady/
@ -1061,13 +1061,14 @@ void ScriptEngine::run() {
}
_lastUpdate = now;
// Debug and clear exceptions
if (hasUncaughtException()) {
reportUncaughtException();
// only clear exceptions if we are not in the middle of evaluating
if (!isEvaluating() && hasUncaughtException()) {
qCWarning(scriptengine) << __FUNCTION__ << "---------- UNCAUGHT EXCEPTION --------";
qCWarning(scriptengine) << "runInThread" << uncaughtException().toString();
emit unhandledException(cloneUncaughtException(__FUNCTION__));
clearExceptions();
}
}
scriptInfoMessage("Script Engine stopping:" + getFilename());
stopAllTimers(); // make sure all our timers are stopped if the script is ending
@ -1100,9 +1101,11 @@ void ScriptEngine::run() {
// we want to only call it in our own run "shutdown" processing.
void ScriptEngine::stopAllTimers() {
QMutableHashIterator<QTimer*, CallbackData> i(_timerFunctionMap);
int j {0};
while (i.hasNext()) {
i.next();
QTimer* timer = i.key();
qCDebug(scriptengine) << getFilename() << "stopAllTimers[" << j++ << "]";
stopTimer(timer);
}
}
@ -1197,11 +1200,11 @@ void ScriptEngine::timerFired() {
auto postTimer = p_high_resolution_clock::now();
auto elapsed = (postTimer - preTimer);
_totalTimerExecution += std::chrono::duration_cast<std::chrono::microseconds>(elapsed);
} else {
qCWarning(scriptengine) << "timerFired -- invalid function" << timerData.function.toVariant().toString();
}
}
QObject* ScriptEngine::setupTimerWithInterval(const QScriptValue& function, int intervalMS, bool isSingleShot) {
// create the timer, add it to the map, and start it
QTimer* newTimer = new QTimer(this);
@ -1218,7 +1221,7 @@ QObject* ScriptEngine::setupTimerWithInterval(const QScriptValue& function, int
// make sure the timer stops when the script does
connect(this, &ScriptEngine::scriptEnding, newTimer, &QTimer::stop);
CallbackData timerData = {function, currentEntityIdentifier, currentSandboxURL };
CallbackData timerData = { function, currentEntityIdentifier, currentSandboxURL };
_timerFunctionMap.insert(newTimer, timerData);
newTimer->start(intervalMS);
@ -1248,33 +1251,44 @@ void ScriptEngine::stopTimer(QTimer *timer) {
timer->stop();
_timerFunctionMap.remove(timer);
delete timer;
} else {
qCDebug(scriptengine) << "stopTimer -- not in _timerFunctionMap" << timer;
}
}
QUrl ScriptEngine::resolvePath(const QString& include) const {
QUrl url(include);
// first lets check to see if it's already a full URL
if (!url.scheme().isEmpty()) {
// first lets check to see if it's already a full URL -- or a Windows path like "c:/"
if (include.startsWith("/") || url.scheme().length() == 1) {
url = QUrl::fromLocalFile(include);
}
if (!url.isRelative()) {
return expandScriptUrl(url);
}
QScriptContextInfo contextInfo { currentContext()->parentContext() };
// we apparently weren't a fully qualified url, so, let's assume we're relative
// to the original URL of our script
QUrl parentURL = contextInfo.fileName();
if (parentURL.isEmpty()) {
if (_parentURL.isEmpty()) {
parentURL = QUrl(_fileNameString);
} else {
parentURL = QUrl(_parentURL);
}
// to the first absolute URL in the JS scope chain
QUrl parentURL;
auto context = currentContext();
do {
QScriptContextInfo contextInfo { context };
parentURL = QUrl(contextInfo.fileName());
context = context->parentContext();
} while (parentURL.isRelative() && context);
if (parentURL.isRelative()) {
// fallback to the "include" parent (if defined, this will already be absolute)
parentURL = QUrl(_parentURL);
}
// if the parent URL's scheme is empty, then this is probably a local file...
if (parentURL.scheme().isEmpty()) {
parentURL = QUrl::fromLocalFile(_fileNameString);
if (parentURL.isRelative()) {
// fallback to the original script engine URL
parentURL = QUrl(_fileNameString);
// if still relative and path-like, then this is probably a local file...
if (parentURL.isRelative() && url.path().contains("/")) {
parentURL = QUrl::fromLocalFile(_fileNameString);
}
}
// at this point we should have a legitimate fully qualified URL for our parent
@ -1301,22 +1315,7 @@ void ScriptEngine::include(const QStringList& includeFiles, QScriptValue callbac
return; // bail early
}
QList<QUrl> urls;
bool knowsSensitivity = false;
Qt::CaseSensitivity sensitivity { Qt::CaseSensitive };
auto getSensitivity = [&]() {
if (!knowsSensitivity) {
QString path = currentSandboxURL.path();
QFileInfo upperFI(path.toUpper());
QFileInfo lowerFI(path.toLower());
sensitivity = (upperFI == lowerFI) ? Qt::CaseInsensitive : Qt::CaseSensitive;
knowsSensitivity = true;
}
return sensitivity;
};
// Guard against meaningless query and fragment parts.
// Do NOT use PreferLocalFile as its behavior is unpredictable (e.g., on defaultScriptsLocation())
const auto strippingFlags = QUrl::RemoveFilename | QUrl::RemoveQuery | QUrl::RemoveFragment;
for (QString includeFile : includeFiles) {
QString file = ResourceManager::normalizeURL(includeFile);
QUrl thisURL;
@ -1333,10 +1332,8 @@ void ScriptEngine::include(const QStringList& includeFiles, QScriptValue callbac
thisURL = resolvePath(file);
}
if (!isStandardLibrary && !currentSandboxURL.isEmpty() && (thisURL.scheme() == "file") &&
(currentSandboxURL.scheme() != "file" ||
!thisURL.toString(strippingFlags).startsWith(currentSandboxURL.toString(strippingFlags), getSensitivity()))) {
bool disallowOutsideFiles = thisURL.isLocalFile() && !isStandardLibrary && !currentSandboxURL.isLocalFile();
if (disallowOutsideFiles && !PathUtils::isDescendantOf(thisURL, currentSandboxURL)) {
scriptWarningMessage("Script.include() ignoring file path" + thisURL.toString()
+ "outside of original entity script" + currentSandboxURL.toString());
} else {
@ -1373,6 +1370,10 @@ void ScriptEngine::include(const QStringList& includeFiles, QScriptValue callbac
};
doWithEnvironment(capturedEntityIdentifier, capturedSandboxURL, operation);
if (hasUncaughtException()) {
emit unhandledException(cloneUncaughtException(__FUNCTION__));
clearExceptions();
}
} else {
scriptWarningMessage("Script.include() skipping evaluation of previously included url:" + url.toString());
}
@ -1474,21 +1475,6 @@ int ScriptEngine::getNumRunningEntityScripts() const {
return sum;
}
QString ScriptEngine::getEntityScriptStatus(const EntityItemID& entityID) {
if (_entityScripts.contains(entityID))
return EntityScriptStatus_::valueToKey(_entityScripts[entityID].status).toLower();
return QString();
}
bool ScriptEngine::getEntityScriptDetails(const EntityItemID& entityID, EntityScriptDetails &details) const {
auto it = _entityScripts.constFind(entityID);
if (it == _entityScripts.constEnd()) {
return false;
}
details = it.value();
return true;
}
void ScriptEngine::setEntityScriptDetails(const EntityItemID& entityID, const EntityScriptDetails& details) {
_entityScripts[entityID] = details;
emit entityScriptDetailsUpdated();
@ -1501,31 +1487,175 @@ void ScriptEngine::updateEntityScriptStatus(const EntityItemID& entityID, const
emit entityScriptDetailsUpdated();
}
// since all of these operations can be asynch we will always do the actual work in the response handler
// for the download
void ScriptEngine::loadEntityScript(QWeakPointer<ScriptEngine> theEngine, const EntityItemID& entityID, const QString& entityScript, bool forceRedownload) {
auto engine = theEngine.data();
engine->executeOnScriptThread([=]{
EntityScriptDetails details = engine->_entityScripts[entityID];
if (details.status == EntityScriptStatus::PENDING || details.status == EntityScriptStatus::UNLOADED) {
engine->updateEntityScriptStatus(entityID, EntityScriptStatus::LOADING, QThread::currentThread()->objectName());
}
});
bool ScriptEngine::getEntityScriptDetails(const EntityItemID& entityID, EntityScriptDetails &details) const {
auto it = _entityScripts.constFind(entityID);
if (it == _entityScripts.constEnd()) {
return false;
}
details = it.value();
return true;
}
// NOTE: If the script content is not currently in the cache, the LAMBDA here will be called on the Main Thread
// which means we're guaranteed that it's not the correct thread for the ScriptEngine. This means
// when we get into entityScriptContentAvailable() we will likely invokeMethod() to get it over
// to the "Entities" ScriptEngine thread.
DependencyManager::get<ScriptCache>()->getScriptContents(entityScript, [theEngine, entityID](const QString& scriptOrURL, const QString& contents, bool isURL, bool success, const QString &status) {
QSharedPointer<ScriptEngine> strongEngine = theEngine.toStrongRef();
if (strongEngine) {
#ifdef THREAD_DEBUGGING
qCDebug(scriptengine) << "ScriptEngine::entityScriptContentAvailable() IN LAMBDA contentAvailable on thread ["
<< QThread::currentThread() << "] expected thread [" << strongEngine->thread() << "]";
#endif
strongEngine->entityScriptContentAvailable(entityID, scriptOrURL, contents, isURL, success, status);
const static EntityItemID BAD_SCRIPT_UUID_PLACEHOLDER { "{20170224-dead-face-0000-cee000021114}" };
void ScriptEngine::processDeferredEntityLoads(const QString& entityScript, const EntityItemID& leaderID) {
QList<DeferredLoadEntity> retryLoads;
QMutableListIterator<DeferredLoadEntity> i(_deferredEntityLoads);
while (i.hasNext()) {
auto retry = i.next();
if (retry.entityScript == entityScript) {
retryLoads << retry;
i.remove();
}
}, forceRedownload, processLevelMaxRetries);
}
foreach(DeferredLoadEntity retry, retryLoads) {
// check whether entity was since been deleted
if (!_entityScripts.contains(retry.entityID)) {
qCDebug(scriptengine) << "processDeferredEntityLoads -- entity details gone (entity deleted?)"
<< retry.entityID;
continue;
}
// check whether entity has since been unloaded or otherwise errored-out
auto details = _entityScripts[retry.entityID];
if (details.status != EntityScriptStatus::PENDING) {
qCDebug(scriptengine) << "processDeferredEntityLoads -- entity status no longer PENDING; "
<< retry.entityID << details.status;
continue;
}
// propagate leader's failure reasons to the pending entity
const auto leaderDetails = _entityScripts[leaderID];
if (leaderDetails.status != EntityScriptStatus::RUNNING) {
qCDebug(scriptengine) << QString("... pending load of %1 cancelled (leader: %2 status: %3)")
.arg(retry.entityID.toString()).arg(leaderID.toString()).arg(leaderDetails.status);
auto extraDetail = QString("\n(propagated from %1)").arg(leaderID.toString());
if (leaderDetails.status == EntityScriptStatus::ERROR_LOADING_SCRIPT ||
leaderDetails.status == EntityScriptStatus::ERROR_RUNNING_SCRIPT) {
// propagate same error so User doesn't have to hunt down stampede's leader
updateEntityScriptStatus(retry.entityID, leaderDetails.status, leaderDetails.errorInfo + extraDetail);
} else {
// the leader Entity somehow ended up in some other state (rapid-fire delete or unload could cause)
updateEntityScriptStatus(retry.entityID, EntityScriptStatus::ERROR_LOADING_SCRIPT,
"A previous Entity failed to load using this script URL; reload to try again." + extraDetail);
}
continue;
}
if (_occupiedScriptURLs.contains(retry.entityScript)) {
qCWarning(scriptengine) << "--- SHOULD NOT HAPPEN -- recursive call into processDeferredEntityLoads" << retry.entityScript;
continue;
}
// if we made it here then the leading entity was successful so proceed with normal load
loadEntityScript(retry.entityID, retry.entityScript, false);
}
}
void ScriptEngine::loadEntityScript(const EntityItemID& entityID, const QString& entityScript, bool forceRedownload) {
if (QThread::currentThread() != thread()) {
QMetaObject::invokeMethod(this, "loadEntityScript",
Q_ARG(const EntityItemID&, entityID),
Q_ARG(const QString&, entityScript),
Q_ARG(bool, forceRedownload)
);
return;
}
PROFILE_RANGE(script, __FUNCTION__);
if (isStopping() || DependencyManager::get<ScriptEngines>()->isStopped()) {
qCDebug(scriptengine) << "loadEntityScript.start " << entityScript << entityID.toString()
<< " but isStopping==" << isStopping()
<< " || engines->isStopped==" << DependencyManager::get<ScriptEngines>()->isStopped();
return;
}
if (!_entityScripts.contains(entityID)) {
// make sure EntityScriptDetails has an entry for this UUID right away
// (which allows bailing from the loading/provisioning process early if the Entity gets deleted mid-flight)
updateEntityScriptStatus(entityID, EntityScriptStatus::PENDING, "...pending...");
}
// This "occupied" approach allows multiple Entities to boot from the same script URL while still taking
// full advantage of cacheable require modules. This only affects Entities literally coming in back-to-back
// before the first one has time to finish loading.
if (_occupiedScriptURLs.contains(entityScript)) {
auto currentEntityID = _occupiedScriptURLs[entityScript];
if (currentEntityID == BAD_SCRIPT_UUID_PLACEHOLDER) {
if (forceRedownload) {
// script was previously marked unusable, but we're reloading so reset it
_occupiedScriptURLs.remove(entityScript);
} else {
// since not reloading, assume that the exact same input would produce the exact same output again
// note: this state gets reset with "reload all scripts," leaving/returning to a Domain, clear cache, etc.
#ifdef DEBUG_ENTITY_STATES
qCDebug(scriptengine) << QString("loadEntityScript.cancelled entity: %1 script: %2 (previous script failure)")
.arg(entityID.toString()).arg(entityScript);
#endif
updateEntityScriptStatus(entityID, EntityScriptStatus::ERROR_LOADING_SCRIPT,
"A previous Entity failed to load using this script URL; reload to try again.");
return;
}
} else {
// another entity is busy loading from this script URL so wait for them to finish
#ifdef DEBUG_ENTITY_STATES
qCDebug(scriptengine) << QString("loadEntityScript.deferring[%0] entity: %1 script: %2 (waiting on %3)")
.arg(_deferredEntityLoads.size()).arg(entityID.toString()).arg(entityScript).arg(currentEntityID.toString());
#endif
_deferredEntityLoads.push_back({ entityID, entityScript });
return;
}
}
// the scriptURL slot is available; flag as in-use
_occupiedScriptURLs[entityScript] = entityID;
#ifdef DEBUG_ENTITY_STATES
auto previousStatus = _entityScripts.contains(entityID) ? _entityScripts[entityID].status : EntityScriptStatus::PENDING;
qCDebug(scriptengine) << "loadEntityScript.LOADING: " << entityScript << entityID.toString()
<< "(previous: " << previousStatus << ")";
#endif
EntityScriptDetails newDetails;
newDetails.scriptText = entityScript;
newDetails.status = EntityScriptStatus::LOADING;
newDetails.definingSandboxURL = currentSandboxURL;
setEntityScriptDetails(entityID, newDetails);
auto scriptCache = DependencyManager::get<ScriptCache>();
// note: see EntityTreeRenderer.cpp for shared pointer lifecycle management
QWeakPointer<ScriptEngine> weakRef(sharedFromThis());
scriptCache->getScriptContents(entityScript,
[this, weakRef, entityScript, entityID](const QString& url, const QString& contents, bool isURL, bool success, const QString& status) {
QSharedPointer<ScriptEngine> strongRef(weakRef);
if (!strongRef) {
qCWarning(scriptengine) << "loadEntityScript.contentAvailable -- ScriptEngine was deleted during getScriptContents!!";
return;
}
if (isStopping()) {
#ifdef DEBUG_ENTITY_STATES
qCDebug(scriptengine) << "loadEntityScript.contentAvailable -- stopping";
#endif
return;
}
executeOnScriptThread([=]{
#ifdef DEBUG_ENTITY_STATES
qCDebug(scriptengine) << "loadEntityScript.contentAvailable" << status << QUrl(url).fileName() << entityID.toString();
#endif
if (!isStopping() && _entityScripts.contains(entityID)) {
entityScriptContentAvailable(entityID, url, contents, isURL, success, status);
} else {
#ifdef DEBUG_ENTITY_STATES
qCDebug(scriptengine) << "loadEntityScript.contentAvailable -- aborting";
#endif
}
// recheck whether us since may have been set to BAD_SCRIPT_UUID_PLACEHOLDER in entityScriptContentAvailable
if (_occupiedScriptURLs.contains(entityScript) && _occupiedScriptURLs[entityScript] == entityID) {
_occupiedScriptURLs.remove(entityScript);
}
});
}, forceRedownload);
}
// since all of these operations can be asynch we will always do the actual work in the response handler
@ -1555,25 +1685,51 @@ void ScriptEngine::entityScriptContentAvailable(const EntityItemID& entityID, co
auto scriptCache = DependencyManager::get<ScriptCache>();
bool isFileUrl = isURL && scriptOrURL.startsWith("file://");
auto fileName = isURL ? scriptOrURL : "EmbeddedEntityScript";
auto fileName = isURL ? scriptOrURL : "about:EmbeddedEntityScript";
const EntityScriptDetails &oldDetails = _entityScripts[entityID];
const QString entityScript = oldDetails.scriptText;
EntityScriptDetails newDetails;
newDetails.scriptText = scriptOrURL;
if (!success) {
newDetails.status = EntityScriptStatus::ERROR_LOADING_SCRIPT;
newDetails.errorInfo = "Failed to load script (" + status + ")";
// If an error happens below, we want to update newDetails with the new status info
// and also abort any pending Entity loads that are waiting on the exact same script URL.
auto setError = [&](const QString &errorInfo, const EntityScriptStatus& status) {
newDetails.errorInfo = errorInfo;
newDetails.status = status;
setEntityScriptDetails(entityID, newDetails);
#ifdef DEBUG_ENTITY_STATES
qCDebug(scriptengine) << "entityScriptContentAvailable -- flagging " << entityScript << " as BAD_SCRIPT_UUID_PLACEHOLDER";
#endif
// flag the original entityScript as unusuable
_occupiedScriptURLs[entityScript] = BAD_SCRIPT_UUID_PLACEHOLDER;
processDeferredEntityLoads(entityScript, entityID);
};
// NETWORK / FILESYSTEM ERRORS
if (!success) {
setError("Failed to load script (" + status + ")", EntityScriptStatus::ERROR_LOADING_SCRIPT);
return;
}
// SYNTAX ERRORS
auto syntaxError = lintScript(contents, fileName);
if (syntaxError.isError()) {
auto message = syntaxError.property("formatted").toString();
if (message.isEmpty()) {
message = syntaxError.toString();
}
setError(QString("Bad syntax (%1)").arg(message), EntityScriptStatus::ERROR_RUNNING_SCRIPT);
syntaxError.setProperty("detail", entityID.toString());
emit unhandledException(syntaxError);
return;
}
QScriptProgram program { contents, fileName };
if (!syntaxError.isNull() || program.isNull()) {
newDetails.status = EntityScriptStatus::ERROR_RUNNING_SCRIPT;
newDetails.errorInfo = QString("Bad syntax (%1)").arg(syntaxError);
setEntityScriptDetails(entityID, newDetails);
qCDebug(scriptengine) << newDetails.errorInfo << scriptOrURL;
if (program.isNull()) {
setError("Bad program (isNull)", EntityScriptStatus::ERROR_RUNNING_SCRIPT);
emit unhandledException(makeError("program.isNull"));
return; // done processing script
}
@ -1581,10 +1737,11 @@ void ScriptEngine::entityScriptContentAvailable(const EntityItemID& entityID, co
setParentURL(scriptOrURL);
}
// SANITY/PERFORMANCE CHECK USING SANDBOX
const int SANDBOX_TIMEOUT = 0.25 * MSECS_PER_SECOND;
BaseScriptEngine sandbox;
sandbox.setProcessEventsInterval(SANDBOX_TIMEOUT);
QScriptValue testConstructor;
QScriptValue testConstructor, exception;
{
QTimer timeout;
timeout.setSingleShot(true);
@ -1598,18 +1755,26 @@ void ScriptEngine::entityScriptContentAvailable(const EntityItemID& entityID, co
context->throwError(QString("Timed out (entity constructors are limited to %1ms)").arg(SANDBOX_TIMEOUT));
}
});
testConstructor = sandbox.evaluate(program);
if (sandbox.hasUncaughtException()) {
exception = sandbox.cloneUncaughtException(QString("(preflight %1)").arg(entityID.toString()));
sandbox.clearExceptions();
} else if (testConstructor.isError()) {
exception = testConstructor;
}
}
QString exceptionMessage = sandbox.formatUncaughtException(program.fileName());
if (!exceptionMessage.isNull()) {
newDetails.status = EntityScriptStatus::ERROR_RUNNING_SCRIPT;
newDetails.errorInfo = exceptionMessage;
setEntityScriptDetails(entityID, newDetails);
qCDebug(scriptengine) << "----- ScriptEngine::entityScriptContentAvailable -- hadUncaughtExceptions (" << scriptOrURL << ")";
if (exception.isError()) {
// create a local copy using makeError to decouple from the sandbox engine
exception = makeError(exception);
setError(formatException(exception), EntityScriptStatus::ERROR_RUNNING_SCRIPT);
emit unhandledException(exception);
return;
}
// CONSTRUCTOR VIABILITY
if (!testConstructor.isFunction()) {
QString testConstructorType = QString(testConstructor.toVariant().typeName());
if (testConstructorType == "") {
@ -1620,32 +1785,48 @@ void ScriptEngine::entityScriptContentAvailable(const EntityItemID& entityID, co
if (testConstructorValue.size() > maxTestConstructorValueSize) {
testConstructorValue = testConstructorValue.mid(0, maxTestConstructorValueSize) + "...";
}
scriptErrorMessage("Error -- ScriptEngine::loadEntityScript() entity:" + entityID.toString()
+ "failed to load entity script -- expected a function, got " + testConstructorType
+ "," + testConstructorValue
+ "," + scriptOrURL);
auto message = QString("failed to load entity script -- expected a function, got %1, %2")
.arg(testConstructorType).arg(testConstructorValue);
newDetails.status = EntityScriptStatus::ERROR_RUNNING_SCRIPT;
newDetails.errorInfo = "Could not find constructor";
setEntityScriptDetails(entityID, newDetails);
auto err = makeError(message);
err.setProperty("fileName", scriptOrURL);
err.setProperty("detail", "(constructor " + entityID.toString() + ")");
qCDebug(scriptengine) << "----- ScriptEngine::entityScriptContentAvailable -- failed to run (" << scriptOrURL << ")";
setError("Could not find constructor (" + testConstructorType + ")", EntityScriptStatus::ERROR_RUNNING_SCRIPT);
emit unhandledException(err);
return; // done processing script
}
// (this feeds into refreshFileScript)
int64_t lastModified = 0;
if (isFileUrl) {
QString file = QUrl(scriptOrURL).toLocalFile();
lastModified = (quint64)QFileInfo(file).lastModified().toMSecsSinceEpoch();
}
// THE ACTUAL EVALUATION AND CONSTRUCTION
QScriptValue entityScriptConstructor, entityScriptObject;
QUrl sandboxURL = currentSandboxURL.isEmpty() ? scriptOrURL : currentSandboxURL;
auto initialization = [&]{
entityScriptConstructor = evaluate(contents, fileName);
entityScriptObject = entityScriptConstructor.construct();
if (hasUncaughtException()) {
entityScriptObject = cloneUncaughtException("(construct " + entityID.toString() + ")");
clearExceptions();
}
};
doWithEnvironment(entityID, sandboxURL, initialization);
if (entityScriptObject.isError()) {
auto exception = entityScriptObject;
setError(formatException(exception), EntityScriptStatus::ERROR_RUNNING_SCRIPT);
emit unhandledException(exception);
return;
}
// ... AND WE HAVE LIFTOFF
newDetails.status = EntityScriptStatus::RUNNING;
newDetails.scriptObject = entityScriptObject;
newDetails.lastModified = lastModified;
@ -1658,6 +1839,9 @@ void ScriptEngine::entityScriptContentAvailable(const EntityItemID& entityID, co
// if we got this far, then call the preload method
callEntityScriptMethod(entityID, "preload");
_occupiedScriptURLs.remove(entityScript);
processDeferredEntityLoads(entityScript, entityID);
}
void ScriptEngine::unloadEntityScript(const EntityItemID& entityID) {
@ -1677,13 +1861,25 @@ void ScriptEngine::unloadEntityScript(const EntityItemID& entityID) {
#endif
if (_entityScripts.contains(entityID)) {
const EntityScriptDetails &oldDetails = _entityScripts[entityID];
if (isEntityScriptRunning(entityID)) {
callEntityScriptMethod(entityID, "unload");
} else {
qCDebug(scriptengine) << "unload called while !running" << entityID << oldDetails.status;
}
if (oldDetails.status != EntityScriptStatus::UNLOADED) {
EntityScriptDetails newDetails;
newDetails.status = EntityScriptStatus::UNLOADED;
newDetails.lastModified = QDateTime::currentMSecsSinceEpoch();
// keep scriptText populated for the current need to "debouce" duplicate calls to unloadEntityScript
newDetails.scriptText = oldDetails.scriptText;
setEntityScriptDetails(entityID, newDetails);
}
EntityScriptDetails newDetails;
newDetails.status = EntityScriptStatus::UNLOADED;
setEntityScriptDetails(entityID, newDetails);
stopAllTimersForEntityScript(entityID);
{
// FIXME: shouldn't have to do this here, but currently something seems to be firing unloads moments after firing initial load requests
processDeferredEntityLoads(oldDetails.scriptText, entityID);
}
}
}
@ -1704,15 +1900,14 @@ void ScriptEngine::unloadAllEntityScripts() {
}
_entityScripts.clear();
emit entityScriptDetailsUpdated();
_occupiedScriptURLs.clear();
#ifdef DEBUG_ENGINE_STATE
qCDebug(scriptengine) << "---- CURRENT STATE OF ENGINE: --------------------------";
QScriptValueIterator it(globalObject());
while (it.hasNext()) {
it.next();
qCDebug(scriptengine) << it.name() << ":" << it.value().toString();
}
qCDebug(scriptengine) << "--------------------------------------------------------";
_debugDump(
"---- CURRENT STATE OF ENGINE: --------------------------",
globalObject(),
"--------------------------------------------------------"
);
#endif // DEBUG_ENGINE_STATE
}
@ -1734,17 +1929,7 @@ void ScriptEngine::refreshFileScript(const EntityItemID& entityID) {
auto lastModified = QFileInfo(filePath).lastModified().toMSecsSinceEpoch();
if (lastModified > details.lastModified) {
scriptInfoMessage("Reloading modified script " + details.scriptText);
QFile file(filePath);
file.open(QIODevice::ReadOnly);
QString scriptContents = QTextStream(&file).readAll();
this->unloadEntityScript(entityID);
this->entityScriptContentAvailable(entityID, details.scriptText, scriptContents, true, true, "Success");
if (!isEntityScriptRunning(entityID)) {
scriptWarningMessage("Reload script " + details.scriptText + " failed");
} else {
details = _entityScripts[entityID];
}
loadEntityScript(entityID, details.scriptText, true);
}
}
recurseGuard = false;
@ -1768,14 +1953,14 @@ void ScriptEngine::doWithEnvironment(const EntityItemID& entityID, const QUrl& s
#else
operation();
#endif
if (hasUncaughtException()) {
reportUncaughtException();
if (!isEvaluating() && hasUncaughtException()) {
emit unhandledException(cloneUncaughtException(!entityID.isNull() ? entityID.toString() : __FUNCTION__));
clearExceptions();
}
currentEntityIdentifier = oldIdentifier;
currentSandboxURL = oldSandboxURL;
}
void ScriptEngine::callWithEnvironment(const EntityItemID& entityID, const QUrl& sandboxURL, QScriptValue function, QScriptValue thisObject, QScriptValueList args) {
auto operation = [&]() {
function.call(thisObject, args);
@ -1850,7 +2035,6 @@ void ScriptEngine::callEntityScriptMethod(const EntityItemID& entityID, const QS
}
}
void ScriptEngine::callEntityScriptMethod(const EntityItemID& entityID, const QString& methodName, const EntityItemID& otherID, const Collision& collision) {
if (QThread::currentThread() != thread()) {
#ifdef THREAD_DEBUGGING
@ -1885,3 +2069,4 @@ void ScriptEngine::callEntityScriptMethod(const EntityItemID& entityID, const QS
}
}
}

View file

@ -35,6 +35,7 @@
#include "ArrayBufferClass.h"
#include "AssetScriptingInterface.h"
#include "AudioScriptingInterface.h"
#include "BaseScriptEngine.h"
#include "Quat.h"
#include "Mat4.h"
#include "ScriptCache.h"
@ -54,6 +55,13 @@ public:
QUrl definingSandboxURL;
};
class DeferredLoadEntity {
public:
EntityItemID entityID;
QString entityScript;
//bool forceRedownload;
};
typedef QList<CallbackData> CallbackList;
typedef QHash<QString, CallbackList> RegisteredEventHandlers;
@ -67,18 +75,10 @@ public:
QString scriptText { "" };
QScriptValue scriptObject { QScriptValue() };
int64_t lastModified { 0 };
QUrl definingSandboxURL { QUrl() };
QUrl definingSandboxURL { QUrl("about:EntityScript") };
};
// common base class with just QScriptEngine-dependent helper methods
class BaseScriptEngine : public QScriptEngine {
public:
static const QString SCRIPT_EXCEPTION_FORMAT;
QString lintScript(const QString& sourceCode, const QString& fileName, const int lineNumber = 1);
QString formatUncaughtException(const QString& overrideFileName = QString());
};
class ScriptEngine : public BaseScriptEngine, public EntitiesScriptEngineProvider {
class ScriptEngine : public BaseScriptEngine, public EntitiesScriptEngineProvider, public QEnableSharedFromThis<ScriptEngine> {
Q_OBJECT
Q_PROPERTY(QString context READ getContext)
public:
@ -91,14 +91,13 @@ public:
};
static int processLevelMaxRetries;
ScriptEngine(Context context, const QString& scriptContents = NO_SCRIPT, const QString& fileNameString = QString(""));
ScriptEngine(Context context, const QString& scriptContents = NO_SCRIPT, const QString& fileNameString = QString("about:ScriptEngine"));
~ScriptEngine();
/// run the script in a dedicated thread. This will have the side effect of evalulating
/// the current script contents and calling run(). Callers will likely want to register the script with external
/// services before calling this.
void runInThread();
Q_INVOKABLE void executeOnScriptThread(std::function<void()> function, bool blocking = false);
void runDebuggable();
@ -162,16 +161,16 @@ public:
Q_INVOKABLE QObject* setTimeout(const QScriptValue& function, int timeoutMS);
Q_INVOKABLE void clearInterval(QObject* timer) { stopTimer(reinterpret_cast<QTimer*>(timer)); }
Q_INVOKABLE void clearTimeout(QObject* timer) { stopTimer(reinterpret_cast<QTimer*>(timer)); }
Q_INVOKABLE void print(const QString& message);
Q_INVOKABLE QUrl resolvePath(const QString& path) const;
Q_INVOKABLE QUrl resourcesPath() const;
// Entity Script Related methods
Q_INVOKABLE QString getEntityScriptStatus(const EntityItemID& entityID);
Q_INVOKABLE bool isEntityScriptRunning(const EntityItemID& entityID) {
return _entityScripts.contains(entityID) && _entityScripts[entityID].status == EntityScriptStatus::RUNNING;
}
static void loadEntityScript(QWeakPointer<ScriptEngine> theEngine, const EntityItemID& entityID, const QString& entityScript, bool forceRedownload);
Q_INVOKABLE void loadEntityScript(const EntityItemID& entityID, const QString& entityScript, bool forceRedownload);
Q_INVOKABLE void unloadEntityScript(const EntityItemID& entityID); // will call unload method
Q_INVOKABLE void unloadAllEntityScripts();
Q_INVOKABLE void callEntityScriptMethod(const EntityItemID& entityID, const QString& methodName,
@ -212,7 +211,6 @@ public:
bool getEntityScriptDetails(const EntityItemID& entityID, EntityScriptDetails &details) const;
public slots:
int evaluatePending() const { return _evaluatesPending; }
void callAnimationStateHandler(QScriptValue callback, AnimVariantMap parameters, QStringList names, bool useNames, AnimVariantResultHandler resultHandler);
void updateMemoryCost(const qint64&);
@ -228,7 +226,6 @@ signals:
void warningMessage(const QString& message);
void infoMessage(const QString& message);
void runningStateChanged();
void evaluationFinished(QScriptValue result, bool isException);
void loadScript(const QString& scriptName, bool isUserLoaded);
void reloadScript(const QString& scriptName, bool isUserLoaded);
void doneRunning();
@ -239,8 +236,9 @@ signals:
protected:
void init();
Q_INVOKABLE void executeOnScriptThread(std::function<void()> function, const Qt::ConnectionType& type = Qt::QueuedConnection );
QString reportUncaughtException(const QString& overrideFileName = QString());
QString logException(const QScriptValue& exception);
void timerFired();
void stopAllTimers();
void stopAllTimersForEntityScript(const EntityItemID& entityID);
@ -248,6 +246,7 @@ protected:
void updateEntityScriptStatus(const EntityItemID& entityID, const EntityScriptStatus& status, const QString& errorInfo = QString());
void setEntityScriptDetails(const EntityItemID& entityID, const EntityScriptDetails& details);
void setParentURL(const QString& parentURL) { _parentURL = parentURL; }
void processDeferredEntityLoads(const QString& entityScript, const EntityItemID& leaderID);
QObject* setupTimerWithInterval(const QScriptValue& function, int intervalMS, bool isSingleShot);
void stopTimer(QTimer* timer);
@ -262,17 +261,18 @@ protected:
void callWithEnvironment(const EntityItemID& entityID, const QUrl& sandboxURL, QScriptValue function, QScriptValue thisObject, QScriptValueList args);
Context _context;
QString _scriptContents;
QString _parentURL;
std::atomic<bool> _isFinished { false };
std::atomic<bool> _isRunning { false };
std::atomic<bool> _isStopping { false };
int _evaluatesPending { 0 };
bool _isInitialized { false };
QHash<QTimer*, CallbackData> _timerFunctionMap;
QSet<QUrl> _includedURLs;
QHash<EntityItemID, EntityScriptDetails> _entityScripts;
QHash<QString, EntityItemID> _occupiedScriptURLs;
QList<DeferredLoadEntity> _deferredEntityLoads;
bool _isThreaded { false };
QScriptEngineDebugger* _debugger { nullptr };
bool _debuggable { false };

View file

@ -364,25 +364,43 @@ QStringList ScriptEngines::getRunningScripts() {
}
void ScriptEngines::stopAllScripts(bool restart) {
QVector<QString> toReload;
QReadLocker lock(&_scriptEnginesHashLock);
for (QHash<QUrl, ScriptEngine*>::const_iterator it = _scriptEnginesHash.constBegin();
it != _scriptEnginesHash.constEnd(); it++) {
ScriptEngine *scriptEngine = it.value();
// skip already stopped scripts
if (it.value()->isFinished() || it.value()->isStopping()) {
if (scriptEngine->isFinished() || scriptEngine->isStopping()) {
continue;
}
// queue user scripts if restarting
if (restart && it.value()->isUserLoaded()) {
connect(it.value(), &ScriptEngine::finished, this, [this](QString scriptName, ScriptEngine* engine) {
reloadScript(scriptName);
});
if (restart && scriptEngine->isUserLoaded()) {
toReload << it.key().toString();
}
// stop all scripts
it.value()->stop(true);
qCDebug(scriptengine) << "stopping script..." << it.key();
scriptEngine->stop();
}
// wait for engines to stop (ie: providing time for .scriptEnding cleanup handlers to run) before
// triggering reload of any Client scripts / Entity scripts
QTimer::singleShot(500, this, [=]() {
for(const auto &scriptName : toReload) {
auto scriptEngine = getScriptEngine(scriptName);
if (scriptEngine && !scriptEngine->isFinished()) {
qCDebug(scriptengine) << "waiting on script:" << scriptName;
scriptEngine->waitTillDoneRunning();
qCDebug(scriptengine) << "done waiting on script:" << scriptName;
}
qCDebug(scriptengine) << "reloading script..." << scriptName;
reloadScript(scriptName);
}
if (restart) {
qCDebug(scriptengine) << "stopAllScripts -- emitting scriptsReloading";
emit scriptsReloading();
}
});
}
bool ScriptEngines::stopScript(const QString& rawScriptURL, bool restart) {
@ -421,9 +439,10 @@ void ScriptEngines::setScriptsLocation(const QString& scriptsLocation) {
}
void ScriptEngines::reloadAllScripts() {
qCDebug(scriptengine) << "reloadAllScripts -- clearing caches";
DependencyManager::get<ScriptCache>()->clearCache();
DependencyManager::get<OffscreenUi>()->clearCache();
emit scriptsReloading();
qCDebug(scriptengine) << "reloadAllScripts -- stopping all scripts";
stopAllScripts(true);
}
@ -456,7 +475,7 @@ ScriptEngine* ScriptEngines::loadScript(const QUrl& scriptFilename, bool isUserL
return scriptEngine;
}
scriptEngine = new ScriptEngine(_context, NO_SCRIPT, "");
scriptEngine = new ScriptEngine(_context, NO_SCRIPT, "about:" + scriptFilename.fileName());
scriptEngine->setUserLoaded(isUserLoaded);
connect(scriptEngine, &ScriptEngine::doneRunning, this, [scriptEngine] {
scriptEngine->deleteLater();

View file

@ -109,6 +109,8 @@ public:
bool isInvalid() const { return _corner == INFINITY_VECTOR; }
void clear() { _corner = INFINITY_VECTOR; _scale = glm::vec3(0.0f); }
private:
glm::vec3 getClosestPointOnFace(const glm::vec3& point, BoxFace face) const;
glm::vec3 getClosestPointOnFace(const glm::vec4& origin, const glm::vec4& direction, BoxFace face) const;

View file

@ -18,7 +18,7 @@
#include <QUrl>
#include "PathUtils.h"
#include <QtCore/QStandardPaths>
#include <mutex> // std::once
const QString& PathUtils::resourcesPath() {
#ifdef Q_OS_MAC
@ -82,3 +82,28 @@ QUrl defaultScriptsLocation() {
QFileInfo fileInfo(path);
return QUrl::fromLocalFile(fileInfo.canonicalFilePath());
}
QString PathUtils::stripFilename(const QUrl& url) {
// Guard against meaningless query and fragment parts.
// Do NOT use PreferLocalFile as its behavior is unpredictable (e.g., on defaultScriptsLocation())
return url.toString(QUrl::RemoveFilename | QUrl::RemoveQuery | QUrl::RemoveFragment);
}
Qt::CaseSensitivity PathUtils::getFSCaseSensitivity() {
static Qt::CaseSensitivity sensitivity { Qt::CaseSensitive };
static std::once_flag once;
std::call_once(once, [&] {
QString path = defaultScriptsLocation().toLocalFile();
QFileInfo upperFI(path.toUpper());
QFileInfo lowerFI(path.toLower());
sensitivity = (upperFI == lowerFI) ? Qt::CaseInsensitive : Qt::CaseSensitive;
});
return sensitivity;
}
bool PathUtils::isDescendantOf(const QUrl& childURL, const QUrl& parentURL) {
QString child = stripFilename(childURL);
QString parent = stripFilename(parentURL);
return child.startsWith(parent, PathUtils::getFSCaseSensitivity());
}

View file

@ -28,6 +28,11 @@ class PathUtils : public QObject, public Dependency {
public:
static const QString& resourcesPath();
static QString getRootDataDirectory();
static Qt::CaseSensitivity getFSCaseSensitivity();
static QString stripFilename(const QUrl& url);
// note: this is FS-case-sensitive version of parentURL.isParentOf(childURL)
static bool isDescendantOf(const QUrl& childURL, const QUrl& parentURL);
};
QString fileNameWithoutExtension(const QString& fileName, const QVector<QString> possibleExtensions);

View file

@ -45,7 +45,8 @@ enum ShapeType {
SHAPE_TYPE_COMPOUND,
SHAPE_TYPE_SIMPLE_HULL,
SHAPE_TYPE_SIMPLE_COMPOUND,
SHAPE_TYPE_STATIC_MESH
SHAPE_TYPE_STATIC_MESH,
SHAPE_TYPE_ELLIPSOID
};
class ShapeInfo {

View file

@ -0,0 +1,76 @@
//
// TriangleSet.cpp
// libraries/entities/src
//
// Created by Brad Hefta-Gaub on 3/2/2017.
// 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
//
#include "GLMHelpers.h"
#include "TriangleSet.h"
void TriangleSet::insert(const Triangle& t) {
_triangles.push_back(t);
_bounds += t.v0;
_bounds += t.v1;
_bounds += t.v2;
}
void TriangleSet::clear() {
_triangles.clear();
_bounds.clear();
}
// Determine of the given ray (origin/direction) in model space intersects with any triangles
// in the set. If an intersection occurs, the distance and surface normal will be provided.
bool TriangleSet::findRayIntersection(const glm::vec3& origin, const glm::vec3& direction,
float& distance, BoxFace& face, glm::vec3& surfaceNormal, bool precision) const {
bool intersectedSomething = false;
float boxDistance = std::numeric_limits<float>::max();
float bestDistance = std::numeric_limits<float>::max();
if (_bounds.findRayIntersection(origin, direction, boxDistance, face, surfaceNormal)) {
if (precision) {
for (const auto& triangle : _triangles) {
float thisTriangleDistance;
if (findRayTriangleIntersection(origin, direction, triangle, thisTriangleDistance)) {
if (thisTriangleDistance < bestDistance) {
bestDistance = thisTriangleDistance;
intersectedSomething = true;
surfaceNormal = triangle.getNormal();
distance = bestDistance;
}
}
}
} else {
intersectedSomething = true;
distance = boxDistance;
}
}
return intersectedSomething;
}
bool TriangleSet::convexHullContains(const glm::vec3& point) const {
if (!_bounds.contains(point)) {
return false;
}
bool insideMesh = true; // optimistic
for (const auto& triangle : _triangles) {
if (!isPointBehindTrianglesPlane(point, triangle.v0, triangle.v1, triangle.v2)) {
// it's not behind at least one so we bail
insideMesh = false;
break;
}
}
return insideMesh;
}

View file

@ -0,0 +1,41 @@
//
// TriangleSet.h
// libraries/entities/src
//
// Created by Brad Hefta-Gaub on 3/2/2017.
// 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
//
#include <vector>
#include "AABox.h"
#include "GeometryUtil.h"
class TriangleSet {
public:
void reserve(size_t size) { _triangles.reserve(size); } // reserve space in the datastructure for size number of triangles
size_t size() const { return _triangles.size(); }
const Triangle& getTriangle(size_t t) const { return _triangles[t]; }
void insert(const Triangle& t);
void clear();
// Determine if the given ray (origin/direction) in model space intersects with any triangles in the set. If an
// intersection occurs, the distance and surface normal will be provided.
bool findRayIntersection(const glm::vec3& origin, const glm::vec3& direction,
float& distance, BoxFace& face, glm::vec3& surfaceNormal, bool precision) const;
// Determine if a point is "inside" all the triangles of a convex hull. It is the responsibility of the caller to
// determine that the triangle set is indeed a convex hull. If the triangles added to this set are not in fact a
// convex hull, the result of this method is meaningless and undetermined.
bool convexHullContains(const glm::vec3& point) const;
const AABox& getBounds() const { return _bounds; }
private:
std::vector<Triangle> _triangles;
AABox _bounds;
};

View file

@ -28,6 +28,12 @@ OculusDisplayPlugin::OculusDisplayPlugin() {
_compositorDroppedFrames.store(0);
}
float OculusDisplayPlugin::getTargetFrameRate() const {
if (_aswActive) {
return _hmdDesc.DisplayRefreshRate / 2.0f;
}
return _hmdDesc.DisplayRefreshRate;
}
bool OculusDisplayPlugin::internalActivate() {
bool result = Parent::internalActivate();
@ -185,8 +191,6 @@ void OculusDisplayPlugin::hmdPresent() {
}
}
if (!OVR_SUCCESS(result)) {
logWarning("Failed to present");
}
@ -195,12 +199,20 @@ void OculusDisplayPlugin::hmdPresent() {
static int appDroppedFrames = 0;
ovrPerfStats perfStats;
ovr_GetPerfStats(_session, &perfStats);
bool shouldResetPresentRate = false;
for (int i = 0; i < perfStats.FrameStatsCount; ++i) {
const auto& frameStats = perfStats.FrameStats[i];
int delta = frameStats.CompositorDroppedFrameCount - compositorDroppedFrames;
_stutterRate.increment(delta);
compositorDroppedFrames = frameStats.CompositorDroppedFrameCount;
appDroppedFrames = frameStats.AppDroppedFrameCount;
bool newAswState = ovrTrue == frameStats.AswIsActive;
if (_aswActive.exchange(newAswState) != newAswState) {
shouldResetPresentRate = true;
}
}
if (shouldResetPresentRate) {
resetPresentRate();
}
_appDroppedFrames.store(appDroppedFrames);
_compositorDroppedFrames.store(compositorDroppedFrames);
@ -212,6 +224,7 @@ void OculusDisplayPlugin::hmdPresent() {
QJsonObject OculusDisplayPlugin::getHardwareStats() const {
QJsonObject hardwareStats;
hardwareStats["asw_active"] = _aswActive.load();
hardwareStats["app_dropped_frame_count"] = _appDroppedFrames.load();
hardwareStats["compositor_dropped_frame_count"] = _compositorDroppedFrames.load();
hardwareStats["long_render_count"] = _longRenders.load();

View file

@ -20,7 +20,8 @@ public:
QString getPreferredAudioInDevice() const override;
QString getPreferredAudioOutDevice() const override;
float getTargetFrameRate() const override;
virtual QJsonObject getHardwareStats() const;
protected:
@ -39,6 +40,7 @@ private:
gpu::FramebufferPointer _outputFramebuffer;
bool _customized { false };
std::atomic_bool _aswActive;
std::atomic_int _compositorDroppedFrames;
std::atomic_int _appDroppedFrames;
std::atomic_int _longSubmits;

View file

@ -88,7 +88,11 @@ ovrSession acquireOculusSession() {
}
if (!session) {
if (!OVR_SUCCESS(ovr_Initialize(nullptr))) {
ovrInitParams initParams {
ovrInit_RequestVersion | ovrInit_MixedRendering, OVR_MINOR_VERSION, nullptr, 0, 0
};
if (!OVR_SUCCESS(ovr_Initialize(&initParams))) {
logWarning("Failed to initialize Oculus SDK");
return session;
}

View file

@ -0,0 +1,21 @@
(function() {
return {
preload: function(uuid) {
var props = Entities.getEntityProperties(uuid);
var shape = props.shape === 'Sphere' ? 'Hexagon' : 'Sphere';
Entities.editEntity(uuid, {
shape: shape,
color: { red: 0xff, green: 0xff, blue: 0xff },
});
this.name = props.name;
print("preload", this.name);
},
unload: function(uuid) {
print("unload", this.name);
Entities.editEntity(uuid, {
color: { red: 0x0f, green: 0x0f, blue: 0xff },
});
},
};
})

View file

@ -0,0 +1,33 @@
var NUM_ENTITIES = 100;
var RADIUS = 2;
var DIV = NUM_ENTITIES / Math.PI / 2;
var PASS_SCRIPT_URL = Script.resolvePath('entityServerStampedeTest-entity.js');
var FAIL_SCRIPT_URL = Script.resolvePath('entityStampedeTest-entity-fail.js');
var origin = Vec3.sum(MyAvatar.position, Vec3.multiply(5, Quat.getFront(MyAvatar.orientation)));
origin.y += HMD.eyeHeight;
var uuids = [];
Script.scriptEnding.connect(function() {
uuids.forEach(function(id) {
Entities.deleteEntity(id);
});
});
for (var i=0; i < NUM_ENTITIES; i++) {
var failGroup = i % 2;
uuids.push(Entities.addEntity({
type: 'Shape',
shape: failGroup ? 'Sphere' : 'Icosahedron',
name: 'SERVER-entityStampedeTest-' + i,
lifetime: 120,
position: Vec3.sum(origin, Vec3.multiplyQbyV(
MyAvatar.orientation, { x: Math.sin(i / DIV) * RADIUS, y: Math.cos(i / DIV) * RADIUS, z: 0 }
)),
serverScripts: (failGroup ? FAIL_SCRIPT_URL : PASS_SCRIPT_URL) + Settings.getValue('cache_buster'),
dimensions: Vec3.HALF,
color: { red: 0, green: 0, blue: 0 },
}, !Entities.serversExist()));
}

View file

@ -0,0 +1,3 @@
(function() {
throw new Error(Script.resolvePath(''));
})

View file

@ -0,0 +1,21 @@
(function() {
return {
preload: function(uuid) {
var props = Entities.getEntityProperties(uuid);
var shape = props.shape === 'Sphere' ? 'Cube' : 'Sphere';
Entities.editEntity(uuid, {
shape: shape,
color: { red: 0xff, green: 0xff, blue: 0xff },
});
this.name = props.name;
print("preload", this.name);
},
unload: function(uuid) {
print("unload", this.name);
Entities.editEntity(uuid, {
color: { red: 0xff, green: 0x0f, blue: 0x0f },
});
},
};
})

View file

@ -0,0 +1,32 @@
var NUM_ENTITIES = 100;
var RADIUS = 2;
var DIV = NUM_ENTITIES / Math.PI / 2;
var PASS_SCRIPT_URL = Script.resolvePath('').replace('.js', '-entity.js');
var FAIL_SCRIPT_URL = Script.resolvePath('').replace('.js', '-entity-fail.js');
var origin = Vec3.sum(MyAvatar.position, Vec3.multiply(5, Quat.getFront(MyAvatar.orientation)));
origin.y += HMD.eyeHeight;
var uuids = [];
Script.scriptEnding.connect(function() {
uuids.forEach(function(id) {
Entities.deleteEntity(id);
});
});
for (var i=0; i < NUM_ENTITIES; i++) {
var failGroup = i % 2;
uuids.push(Entities.addEntity({
type: 'Shape',
shape: failGroup ? 'Sphere' : 'Icosahedron',
name: 'entityStampedeTest-' + i,
lifetime: 120,
position: Vec3.sum(origin, Vec3.multiplyQbyV(
MyAvatar.orientation, { x: Math.sin(i / DIV) * RADIUS, y: Math.cos(i / DIV) * RADIUS, z: 0 }
)),
script: (failGroup ? FAIL_SCRIPT_URL : PASS_SCRIPT_URL) + Settings.getValue('cache_buster'),
dimensions: Vec3.HALF,
color: { red: 0, green: 0, blue: 0 },
}, !Entities.serversExist()));
}

View file

@ -0,0 +1,6 @@
afterError = false;
throw new Error('error.js');
afterError = true;
(1,eval)('this').$finishes.push(Script.resolvePath(''));

View file

@ -0,0 +1,10 @@
afterError = false;
outer = null;
Script.include('./nested/error.js?' + Settings.getValue('cache_buster'));
outer = {
inner: inner.lib,
sibling: sibling.lib,
};
afterError = true;
(1,eval)("this").$finishes.push(Script.resolvePath(''));

View file

@ -0,0 +1,10 @@
afterError = false;
outer = null;
Script.include('./nested/syntax-error.js?' + Settings.getValue('cache_buster'));
outer = {
inner: inner.lib,
sibling: sibling.lib,
};
afterError = true;
(1,eval)("this").$finishes.push(Script.resolvePath(''));

View file

@ -0,0 +1,5 @@
afterError = false;
throw new Error('nested/error.js');
afterError = true;
(1,eval)("this").$finishes.push(Script.resolvePath(''));

View file

@ -0,0 +1,5 @@
Script.include('sibling.js');
inner = {
lib: "nested/lib.js",
};

View file

@ -0,0 +1,3 @@
sibling = {
lib: "nested/sibling",
};

View file

@ -0,0 +1,3 @@
function() {
// intentional SyntaxError...

View file

@ -0,0 +1,11 @@
afterError = false;
outer = null;
Script.include('./nested/lib.js');
Undefined_symbol;
outer = {
inner: inner.lib,
sibling: sibling.lib,
};
afterError = true;
(1,eval)("this").$finishes.push(Script.resolvePath(''));

View file

@ -0,0 +1,5 @@
Script.include('./nested/lib.js');
outer = {
inner: inner.lib,
sibling: sibling.lib,
};

View file

@ -0,0 +1,125 @@
/* eslint-env jasmine */
instrument_testrunner();
describe('Script', function () {
// get the current filename without calling Script.resolvePath('')
try {
throw new Error('stack');
} catch(e) {
var filename = e.fileName;
var dirname = filename.split('/').slice(0, -1).join('/') + '/';
var parentdir = dirname.split('/').slice(0, -2).join('/') + '/';
}
// characterization tests
// initially these are just to capture how the app works currently
var testCases = {
'': filename,
'.': dirname,
'..': parentdir,
'about:Entities 1': '',
'Entities 1': dirname + 'Entities 1',
'./file.js': dirname + 'file.js',
'c:/temp/': 'file:///c:/temp/',
'c:/temp': 'file:///c:/temp',
'/temp/': 'file:///temp/',
'c:/': 'file:///c:/',
'c:': 'file:///c:',
'file:///~/libraries/a.js': 'file:///~/libraries/a.js',
'/temp/tested/../file.js': 'file:///temp/tested/../file.js',
'/~/libraries/utils.js': 'file:///~/libraries/utils.js',
'/temp/file.js': 'file:///temp/file.js',
'/~/': 'file:///~/',
};
describe('resolvePath', function () {
Object.keys(testCases).forEach(function(input) {
it('(' + JSON.stringify(input) + ')', function () {
expect(Script.resolvePath(input)).toEqual(testCases[input]);
});
});
});
describe('include', function () {
var old_cache_buster;
var cache_buster = '#' + +new Date;
beforeAll(function() {
old_cache_buster = Settings.getValue('cache_buster');
Settings.setValue('cache_buster', cache_buster);
});
afterAll(function() {
Settings.setValue('cache_buster', old_cache_buster);
});
beforeEach(function() {
vec3toStr = undefined;
});
it('file:///~/system/libraries/utils.js' + cache_buster, function() {
Script.include('file:///~/system/libraries/utils.js' + cache_buster);
expect(vec3toStr).toEqual(jasmine.any(Function));
});
it('nested' + cache_buster, function() {
Script.include('./scriptTests/top-level.js' + cache_buster);
expect(outer).toEqual(jasmine.any(Object));
expect(inner).toEqual(jasmine.any(Object));
expect(sibling).toEqual(jasmine.any(Object));
expect(outer.inner).toEqual(inner.lib);
expect(outer.sibling).toEqual(sibling.lib);
});
describe('errors' + cache_buster, function() {
var finishes, oldFinishes;
beforeAll(function() {
oldFinishes = (1,eval)('this').$finishes;
});
afterAll(function() {
(1,eval)('this').$finishes = oldFinishes;
});
beforeEach(function() {
finishes = (1,eval)('this').$finishes = [];
});
it('error', function() {
// a thrown Error in top-level include aborts that include, but does not throw the error back to JS
expect(function() {
Script.include('./scriptTests/error.js' + cache_buster);
}).not.toThrowError("error.js");
expect(finishes.length).toBe(0);
});
it('top-level-error', function() {
// an organice Error in a top-level include aborts that include, but does not throw the error
expect(function() {
Script.include('./scriptTests/top-level-error.js' + cache_buster);
}).not.toThrowError(/Undefined_symbol/);
expect(finishes.length).toBe(0);
});
it('nested', function() {
// a thrown Error in a nested include aborts the nested include, but does not abort the top-level script
expect(function() {
Script.include('./scriptTests/nested-error.js' + cache_buster);
}).not.toThrowError("nested/error.js");
expect(finishes.length).toBe(1);
});
it('nested-syntax-error', function() {
// a SyntaxError in a nested include breaks only that include (the main script should finish unimpeded)
expect(function() {
Script.include('./scriptTests/nested-syntax-error.js' + cache_buster);
}).not.toThrowError(/SyntaxEror/);
expect(finishes.length).toBe(1);
});
});
});
});
// enable scriptUnitTests to be loaded directly
function run() {}
function instrument_testrunner() {
if (typeof describe === 'undefined') {
print('instrumenting jasmine', Script.resolvePath(''));
Script.include('../../libraries/jasmine/jasmine.js');
Script.include('../../libraries/jasmine/hifi-boot.js');
jasmine.getEnv().addReporter({ jasmineDone: Script.stop });
run = function() {
print('executing jasmine', Script.resolvePath(''));
jasmine.getEnv().execute();
};
}
}
run();

View file

@ -310,24 +310,24 @@ function getFingerWorldLocation(hand) {
// Object assign polyfill
if (typeof Object.assign != 'function') {
Object.assign = function(target, varArgs) {
'use strict';
if (target == null) {
throw new TypeError('Cannot convert undefined or null to object');
}
var to = Object(target);
for (var index = 1; index < arguments.length; index++) {
var nextSource = arguments[index];
if (nextSource != null) {
for (var nextKey in nextSource) {
if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) {
to[nextKey] = nextSource[nextKey];
}
Object.assign = function(target, varArgs) {
'use strict';
if (target == null) {
throw new TypeError('Cannot convert undefined or null to object');
}
}
}
return to;
};
var to = Object(target);
for (var index = 1; index < arguments.length; index++) {
var nextSource = arguments[index];
if (nextSource != null) {
for (var nextKey in nextSource) {
if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) {
to[nextKey] = nextSource[nextKey];
}
}
}
}
return to;
};
}
function distanceBetweenPointAndEntityBoundingBox(point, entityProps) {
@ -1604,16 +1604,16 @@ function MyController(hand) {
return true;
};
this.entityIsCloneable = function(entityID) {
var entityProps = entityPropertiesCache.getGrabbableProps(entityID);
var props = entityPropertiesCache.getProps(entityID);
if (!props) {
return false;
}
var entityProps = entityPropertiesCache.getGrabbableProps(entityID);
var props = entityPropertiesCache.getProps(entityID);
if (!props) {
return false;
}
if (entityProps.hasOwnProperty("cloneable")) {
return entityProps.cloneable;
}
return false;
if (entityProps.hasOwnProperty("cloneable")) {
return entityProps.cloneable;
}
return false;
}
this.entityIsGrabbable = function(entityID) {
var grabbableProps = entityPropertiesCache.getGrabbableProps(entityID);
@ -2309,7 +2309,7 @@ function MyController(hand) {
// Can't set state of other controller to STATE_DISTANCE_HOLDING because then either:
// (a) The entity would jump to line up with the formerly rotating controller's orientation, or
// (b) The grab beam would need an orientation offset to the controller's true orientation.
// Neither of these options is good, so instead set STATE_SEARCHING and subsequently let the formerly distance
// Neither of these options is good, so instead set STATE_SEARCHING and subsequently let the formerly distance
// rotating controller start distance holding the entity if it happens to be pointing at the entity.
}
return;
@ -2681,31 +2681,29 @@ function MyController(hand) {
var userData = JSON.parse(grabbedProperties.userData);
var grabInfo = userData.grabbableKey;
if (grabInfo && grabInfo.cloneable) {
// Check if
var worldEntities = Entities.findEntitiesInBox(Vec3.subtract(MyAvatar.position, {x:25,y:25, z:25}), {x:50, y: 50, z: 50})
var worldEntities = Entities.findEntities(MyAvatar.position, 50);
var count = 0;
worldEntities.forEach(function(item) {
var item = Entities.getEntityProperties(item, ["name"]);
if (item.name === grabbedProperties.name) {
if (item.name.indexOf('-clone-' + grabbedProperties.id) !== -1) {
count++;
}
})
var limit = grabInfo.cloneLimit ? grabInfo.cloneLimit : 0;
if (count >= limit && limit !== 0) {
delete limit;
return;
}
var cloneableProps = Entities.getEntityProperties(grabbedProperties.id);
cloneableProps.name = cloneableProps.name + '-clone-' + grabbedProperties.id;
var lifetime = grabInfo.cloneLifetime ? grabInfo.cloneLifetime : 300;
var limit = grabInfo.cloneLimit ? grabInfo.cloneLimit : 10;
var dynamic = grabInfo.cloneDynamic ? grabInfo.cloneDynamic : false;
var cUserData = Object.assign({}, userData);
var cProperties = Object.assign({}, cloneableProps);
isClone = true;
if (count > limit) {
delete cloneableProps;
delete lifetime;
delete cUserData;
delete cProperties;
return;
}
delete cUserData.grabbableKey.cloneLifetime;
delete cUserData.grabbableKey.cloneable;
delete cUserData.grabbableKey.cloneDynamic;

View file

@ -879,7 +879,7 @@ function loaded() {
elCloneable.checked = false;
elCloneableDynamic.checked = false;
elCloneableGroup.style.display = elCloneable.checked ? "block": "none";
elCloneableLimit.value = 10;
elCloneableLimit.value = 0;
elCloneableLifetime.value = 300;
var parsedUserData = {}
@ -899,8 +899,6 @@ function loaded() {
if ("cloneable" in parsedUserData["grabbableKey"]) {
elCloneable.checked = parsedUserData["grabbableKey"].cloneable;
elCloneableGroup.style.display = elCloneable.checked ? "block": "none";
elCloneableLimit.value = elCloneable.checked ? 10: 0;
elCloneableLifetime.value = elCloneable.checked ? 300: 0;
elCloneableDynamic.checked = parsedUserData["grabbableKey"].cloneDynamic ? parsedUserData["grabbableKey"].cloneDynamic : properties.dynamic;
elDynamic.checked = elCloneable.checked ? false: properties.dynamic;
if (elCloneable.checked) {
@ -908,7 +906,7 @@ function loaded() {
elCloneableLifetime.value = parsedUserData["grabbableKey"].cloneLifetime ? parsedUserData["grabbableKey"].cloneLifetime : 300;
}
if ("cloneLimit" in parsedUserData["grabbableKey"]) {
elCloneableLimit.value = parsedUserData["grabbableKey"].cloneLimit ? parsedUserData["grabbableKey"].cloneLimit : 10;
elCloneableLimit.value = parsedUserData["grabbableKey"].cloneLimit ? parsedUserData["grabbableKey"].cloneLimit : 0;
}
}
}

View file

@ -51,7 +51,9 @@ const OUTPUT_DEVICE_SETTING = "audio_output_device";
var selectedInputMenu = "";
var selectedOutputMenu = "";
var audioDevicesList = [];
function setupAudioMenus() {
removeAudioMenus();
Menu.addSeparator("Audio", "Input Audio Device");
var inputDeviceSetting = Settings.getValue(INPUT_DEVICE_SETTING);
@ -67,11 +69,12 @@ function setupAudioMenus() {
var thisDeviceSelected = (inputDevices[i] == selectedInputDevice);
var menuItem = "Use " + inputDevices[i] + " for Input";
Menu.addMenuItem({
menuName: "Audio",
menuItemName: menuItem,
isCheckable: true,
isChecked: thisDeviceSelected
});
menuName: "Audio",
menuItemName: menuItem,
isCheckable: true,
isChecked: thisDeviceSelected
});
audioDevicesList.push(menuItem);
if (thisDeviceSelected) {
selectedInputMenu = menuItem;
}
@ -97,12 +100,24 @@ function setupAudioMenus() {
isCheckable: true,
isChecked: thisDeviceSelected
});
audioDevicesList.push(menuItem);
if (thisDeviceSelected) {
selectedOutputMenu = menuItem;
}
}
}
function removeAudioMenus() {
Menu.removeSeparator("Audio", "Input Audio Device");
Menu.removeSeparator("Audio", "Output Audio Device");
for (var index = 0; index < audioDevicesList.length; index++) {
Menu.removeMenuItem("Audio", audioDevicesList[index]);
}
audioDevicesList = [];
}
function onDevicechanged() {
print("audio devices changed, removing Audio > Devices menu...");
Menu.removeMenu("Audio > Devices");
@ -218,6 +233,7 @@ Script.update.connect(checkHMDAudio);
Script.scriptEnding.connect(function () {
restoreAudio();
removeAudioMenus();
Menu.menuItemEvent.disconnect(menuItemEvent);
Script.update.disconnect(checkHMDAudio);
});