mirror of
https://thingvellir.net/git/overte
synced 2025-03-27 23:52:03 +01:00
Merge remote-tracking branch 'upstream/master' into HEAD
This commit is contained in:
commit
e94f803b84
63 changed files with 1989 additions and 1259 deletions
|
@ -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})
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
32
cmake/externals/LibOVR/CMakeLists.txt
vendored
32
cmake/externals/LibOVR/CMakeLists.txt
vendored
|
@ -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(
|
||||
|
|
14
cmake/externals/LibOVR/LibOVRCMakeLists.txt
vendored
Normal file
14
cmake/externals/LibOVR/LibOVRCMakeLists.txt
vendored
Normal 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)
|
|
@ -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)
|
||||
|
|
|
@ -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 "UNMUTE" 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 "UNMUTE" 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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
@ -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
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -59,6 +59,8 @@ public:
|
|||
|
||||
float presentRate() const override;
|
||||
|
||||
void resetPresentRate() override;
|
||||
|
||||
float newFramePresentRate() const override;
|
||||
|
||||
float droppedFrameRate() const override;
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
287
libraries/script-engine/src/BaseScriptEngine.cpp
Normal file
287
libraries/script-engine/src/BaseScriptEngine.cpp
Normal 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
|
||||
|
67
libraries/script-engine/src/BaseScriptEngine.h
Normal file
67
libraries/script-engine/src/BaseScriptEngine.h
Normal 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
|
|
@ -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()) {
|
||||
|
|
|
@ -188,6 +188,8 @@ void ScriptCache::scriptContentAvailable(int maxRetries) {
|
|||
|
||||
}
|
||||
}
|
||||
} else {
|
||||
qCWarning(scriptengine) << "Warning: scriptContentAvailable for inactive url: " << url;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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 };
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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 {
|
||||
|
|
76
libraries/shared/src/TriangleSet.cpp
Normal file
76
libraries/shared/src/TriangleSet.cpp
Normal 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;
|
||||
}
|
||||
|
41
libraries/shared/src/TriangleSet.h
Normal file
41
libraries/shared/src/TriangleSet.h
Normal 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;
|
||||
};
|
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
21
scripts/developer/tests/entityServerStampedeTest-entity.js
Normal file
21
scripts/developer/tests/entityServerStampedeTest-entity.js
Normal 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 },
|
||||
});
|
||||
},
|
||||
};
|
||||
})
|
33
scripts/developer/tests/entityServerStampedeTest.js
Normal file
33
scripts/developer/tests/entityServerStampedeTest.js
Normal 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()));
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
(function() {
|
||||
throw new Error(Script.resolvePath(''));
|
||||
})
|
21
scripts/developer/tests/entityStampedeTest-entity.js
Normal file
21
scripts/developer/tests/entityStampedeTest-entity.js
Normal 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 },
|
||||
});
|
||||
},
|
||||
};
|
||||
})
|
32
scripts/developer/tests/entityStampedeTest.js
Normal file
32
scripts/developer/tests/entityStampedeTest.js
Normal 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()));
|
||||
}
|
6
scripts/developer/tests/unit_tests/scriptTests/error.js
Normal file
6
scripts/developer/tests/unit_tests/scriptTests/error.js
Normal file
|
@ -0,0 +1,6 @@
|
|||
afterError = false;
|
||||
throw new Error('error.js');
|
||||
afterError = true;
|
||||
|
||||
(1,eval)('this').$finishes.push(Script.resolvePath(''));
|
||||
|
|
@ -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(''));
|
|
@ -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(''));
|
|
@ -0,0 +1,5 @@
|
|||
afterError = false;
|
||||
throw new Error('nested/error.js');
|
||||
afterError = true;
|
||||
|
||||
(1,eval)("this").$finishes.push(Script.resolvePath(''));
|
|
@ -0,0 +1,5 @@
|
|||
Script.include('sibling.js');
|
||||
inner = {
|
||||
lib: "nested/lib.js",
|
||||
};
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
sibling = {
|
||||
lib: "nested/sibling",
|
||||
};
|
|
@ -0,0 +1,3 @@
|
|||
function() {
|
||||
// intentional SyntaxError...
|
||||
|
|
@ -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(''));
|
|
@ -0,0 +1,5 @@
|
|||
Script.include('./nested/lib.js');
|
||||
outer = {
|
||||
inner: inner.lib,
|
||||
sibling: sibling.lib,
|
||||
};
|
125
scripts/developer/tests/unit_tests/scriptUnitTests.js
Normal file
125
scripts/developer/tests/unit_tests/scriptUnitTests.js
Normal 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();
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
|
Loading…
Reference in a new issue