diff --git a/BUILD.md b/BUILD.md index 9c56574cbb..547b79cb08 100644 --- a/BUILD.md +++ b/BUILD.md @@ -1,7 +1,7 @@ ###Dependencies -* [cmake](http://www.cmake.org/cmake/resources/software.html) ~> 3.3.2 -* [Qt](http://www.qt.io/download-open-source) ~> 5.6.1 +* [cmake](https://cmake.org/download/) ~> 3.3.2 +* [Qt](https://www.qt.io/download-open-source) ~> 5.6.1 * [OpenSSL](https://www.openssl.org/community/binaries.html) * IMPORTANT: Use the latest available version of OpenSSL to avoid security vulnerabilities. * [VHACD](https://github.com/virneo/v-hacd)(clone this repository)(Optional) @@ -9,18 +9,17 @@ ####CMake External Project Dependencies * [boostconfig](https://github.com/boostorg/config) ~> 1.58 -* [Bullet Physics Engine](https://code.google.com/p/bullet/downloads/list) ~> 2.82 -* [Faceshift](http://www.faceshift.com/) ~> 4.3 +* [Bullet Physics Engine](https://github.com/bulletphysics/bullet3/releases) ~> 2.83 * [GLEW](http://glew.sourceforge.net/) -* [glm](http://glm.g-truc.net/0.9.5/index.html) ~> 0.9.5.4 +* [glm](https://glm.g-truc.net/0.9.5/index.html) ~> 0.9.5.4 * [gverb](https://github.com/highfidelity/gverb) * [Oculus SDK](https://developer.oculus.com/downloads/) ~> 0.6 (Win32) / 0.5 (Mac / Linux) * [oglplus](http://oglplus.org/) ~> 0.63 * [OpenVR](https://github.com/ValveSoftware/openvr) ~> 0.91 (Win32 only) * [Polyvox](http://www.volumesoffun.com/) ~> 0.2.1 -* [QuaZip](http://sourceforge.net/projects/quazip/files/quazip/) ~> 0.7.1 +* [QuaZip](https://sourceforge.net/projects/quazip/files/quazip/) ~> 0.7.1 * [SDL2](https://www.libsdl.org/download-2.0.php) ~> 2.0.3 -* [soxr](http://soxr.sourceforge.net) ~> 0.1.1 +* [soxr](https://sourceforge.net/p/soxr/wiki/Home/) ~> 0.1.1 * [Intel Threading Building Blocks](https://www.threadingbuildingblocks.org/) ~> 4.3 * [Sixense](http://sixense.com/) ~> 071615 * [zlib](http://www.zlib.net/) ~> 1.28 (Win32 only) diff --git a/BUILD_OSX.md b/BUILD_OSX.md index 55d4276aa0..980263cbbc 100644 --- a/BUILD_OSX.md +++ b/BUILD_OSX.md @@ -1,7 +1,7 @@ Please read the [general build guide](BUILD.md) for information on dependencies required for all platforms. Only OS X specific instructions are found in this file. ###Homebrew -[Homebrew](http://brew.sh/) is an excellent package manager for OS X. It makes install of some High Fidelity dependencies very simple. +[Homebrew](https://brew.sh/) is an excellent package manager for OS X. It makes install of some High Fidelity dependencies very simple. brew tap homebrew/versions brew install cmake openssl @@ -18,11 +18,11 @@ Note that this uses the version from the homebrew formula at the time of this wr ###Qt You can use the online installer or the offline installer. -* [Download the online installer](http://www.qt.io/download-open-source/#section-2) +* [Download the online installer](https://www.qt.io/download-open-source/#section-2) * When it asks you to select components, select the following: * Qt > Qt 5.6 -* [Download the offline installer](http://download.qt.io/official_releases/qt/5.6/5.6.1-1/qt-opensource-mac-x64-clang-5.6.1-1.dmg) +* [Download the offline installer](https://download.qt.io/official_releases/qt/5.6/5.6.1-1/qt-opensource-mac-x64-clang-5.6.1-1.dmg) Once Qt is installed, you need to manually configure the following: * Set the QT_CMAKE_PREFIX_PATH environment variable to your `Qt5.6.1/5.6/clang_64/lib/cmake/` directory. diff --git a/BUILD_WIN.md b/BUILD_WIN.md index b8adaad8d1..45373d3093 100644 --- a/BUILD_WIN.md +++ b/BUILD_WIN.md @@ -33,8 +33,8 @@ You can use the online installer or the offline installer. If you use the offlin * Qt > Qt 5.6.1 > **msvc2013 64-bit** * Download the offline installer, 32- or 64-bit to match your build preference: - * [32-bit](http://download.qt.io/official_releases/qt/5.6/5.6.1-1/qt-opensource-windows-x86-msvc2013-5.6.1-1.exe) - * [64-bit](http://download.qt.io/official_releases/qt/5.6/5.6.1-1/qt-opensource-windows-x86-msvc2013_64-5.6.1-1.exe) + * [32-bit](https://download.qt.io/official_releases/qt/5.6/5.6.1-1/qt-opensource-windows-x86-msvc2013-5.6.1-1.exe) + * [64-bit](https://download.qt.io/official_releases/qt/5.6/5.6.1-1/qt-opensource-windows-x86-msvc2013_64-5.6.1-1.exe) Once Qt is installed, you need to manually configure the following: * Set the QT_CMAKE_PREFIX_PATH environment variable to your `Qt\5.6.1\msvc2013\lib\cmake` or `Qt\5.6.1\msvc2013_64\lib\cmake` directory. @@ -72,7 +72,7 @@ Your system may already have several versions of the OpenSSL DLL's (ssleay32.dll QSslSocket: cannot resolve SSL_CTX_set_next_proto_select_cb QSslSocket: cannot resolve SSL_get0_next_proto_negotiated -To prevent these problems, install OpenSSL yourself. Download one of the following binary packages [from this website](http://slproweb.com/products/Win32OpenSSL.html): +To prevent these problems, install OpenSSL yourself. Download one of the following binary packages [from this website](https://slproweb.com/products/Win32OpenSSL.html): * Win32 OpenSSL v1.0.1q * Win64 OpenSSL v1.0.1q diff --git a/README.md b/README.md index 44bfb94634..00e7cbc45b 100644 --- a/README.md +++ b/README.md @@ -11,11 +11,11 @@ We're hiring! We're looking for skilled developers; send your resume to hiring@highfidelity.com ##### Chat with us -Come chat with us in [our Gitter](http://gitter.im/highfidelity/hifi) if you have any questions or just want to say hi! +Come chat with us in [our Gitter](https://gitter.im/highfidelity/hifi) if you have any questions or just want to say hi! Documentation ========= -Documentation is available at [docs.highfidelity.com](http://docs.highfidelity.com), if something is missing, please suggest it via a new job on Worklist (add to the hifi-docs project). +Documentation is available at [docs.highfidelity.com](https://docs.highfidelity.com), if something is missing, please suggest it via a new job on Worklist (add to the hifi-docs project). Build Instructions ========= diff --git a/interface/resources/fonts/hifi-glyphs.ttf b/interface/resources/fonts/hifi-glyphs.ttf old mode 100644 new mode 100755 index c139a196d0..138d7f3dda Binary files a/interface/resources/fonts/hifi-glyphs.ttf and b/interface/resources/fonts/hifi-glyphs.ttf differ diff --git a/interface/resources/qml/hifi/NameCard.qml b/interface/resources/qml/hifi/NameCard.qml index 846f1bec3c..020a85b46d 100644 --- a/interface/resources/qml/hifi/NameCard.qml +++ b/interface/resources/qml/hifi/NameCard.qml @@ -31,6 +31,7 @@ Item { property real displayNameTextPixelSize: 18 property int usernameTextHeight: 12 property real audioLevel: 0.0 + property real avgAudioLevel: 0.0 property bool isMyCard: false property bool selected: false property bool isAdmin: false @@ -55,7 +56,7 @@ Item { id: textContainer // Size width: parent.width - /*avatarImage.width - parent.spacing - */parent.anchors.leftMargin - parent.anchors.rightMargin - height: childrenRect.height + height: selected || isMyCard ? childrenRect.height : childrenRect.height - 15 anchors.verticalCenter: parent.verticalCenter // DisplayName field for my card @@ -273,6 +274,7 @@ Item { // Style radius: 4 color: "#c5c5c5" + visible: isMyCard || selected // Rectangle for the zero-gain point on the VU meter Rectangle { id: vuMeterZeroGain @@ -303,6 +305,7 @@ Item { id: vuMeterBase // Anchors anchors.fill: parent + visible: isMyCard || selected // Style color: parent.color radius: parent.radius @@ -310,6 +313,7 @@ Item { // Rectangle for the VU meter audio level Rectangle { id: vuMeterLevel + visible: isMyCard || selected // Size width: (thisNameCard.audioLevel) * parent.width // Style diff --git a/interface/resources/qml/hifi/Pal.qml b/interface/resources/qml/hifi/Pal.qml index 7ff4e8a4b1..28384f9c1c 100644 --- a/interface/resources/qml/hifi/Pal.qml +++ b/interface/resources/qml/hifi/Pal.qml @@ -13,6 +13,7 @@ import QtQuick 2.5 import QtQuick.Controls 1.4 +import QtGraphicalEffects 1.0 import Qt.labs.settings 1.0 import "../styles-uit" import "../controls-uit" as HifiControls @@ -33,7 +34,7 @@ Rectangle { 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, admin: true}) // valid dummy until set + 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 @@ -57,6 +58,8 @@ Rectangle { category: "pal" property bool filtered: false property int nearDistance: 30 + property int sortIndicatorColumn: 1 + property int sortIndicatorOrder: Qt.AscendingOrder } function refreshWithFilter() { // We should just be able to set settings.filtered to filter.checked, but see #3249, so send to .js for saving. @@ -96,6 +99,7 @@ Rectangle { displayName: myData.displayName userName: myData.userName audioLevel: myData.audioLevel + avgAudioLevel: myData.avgAudioLevel isMyCard: true // Size width: minNameCardWidth @@ -190,8 +194,24 @@ Rectangle { centerHeaderText: true sortIndicatorVisible: true headerVisible: true - onSortIndicatorColumnChanged: sortModel() - onSortIndicatorOrderChanged: sortModel() + 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 @@ -201,13 +221,6 @@ Rectangle { movable: false resizable: false } - TableViewColumn { - role: "personalMute" - title: "MUTE" - width: actionButtonWidth - movable: false - resizable: false - } TableViewColumn { role: "ignore" title: "IGNORE" @@ -238,7 +251,7 @@ Rectangle { // This Rectangle refers to each Row in the table. rowDelegate: Rectangle { // The only way I know to specify a row height. // Size - height: rowHeight + height: styleData.selected ? rowHeight : rowHeight - 15 color: styleData.selected ? hifi.colors.orangeHighlight : styleData.alternate ? hifi.colors.tableRowLightEven : hifi.colors.tableRowLightOdd @@ -249,6 +262,8 @@ Rectangle { 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 { @@ -257,7 +272,8 @@ Rectangle { displayName: styleData.value userName: model ? model.userName : "" audioLevel: model ? model.audioLevel : 0.0 - visible: !isCheckBox && !isButton + avgAudioLevel: model ? model.avgAudioLevel : 0.0 + visible: !isCheckBox && !isButton && !isAvgAudio uuid: model ? model.sessionId : "" selected: styleData.selected isAdmin: model && model.admin @@ -267,6 +283,33 @@ Rectangle { // Anchors 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]; + } + 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 @@ -296,6 +339,7 @@ Rectangle { } 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 @@ -311,7 +355,7 @@ Rectangle { visible: isButton anchors.centerIn: parent width: 32 - height: 24 + height: 32 onClicked: { Users[styleData.role](model.sessionId) UserActivityLogger["palAction"](styleData.role, model.sessionId) @@ -363,7 +407,7 @@ Rectangle { anchors.left: table.left anchors.top: table.top anchors.topMargin: 1 - anchors.leftMargin: nameCardWidth/2 + displayNameHeaderMetrics.width/2 + 6 + anchors.leftMargin: actionButtonWidth + nameCardWidth/2 + displayNameHeaderMetrics.width/2 + 6 RalewayRegular { id: helpText text: "[?]" @@ -537,16 +581,21 @@ Rectangle { break; case 'updateAudioLevel': for (var userId in message.params) { - var audioLevel = message.params[userId]; + var audioLevel = message.params[userId][0]; + var avgAudioLevel = message.params[userId][1]; // If the userId is 0, we're updating "myData". if (userId == 0) { myData.audioLevel = audioLevel; myCard.audioLevel = audioLevel; // Defensive programming + myData.avgAudioLevel = avgAudioLevel; + myCard.avgAudioLevel = avgAudioLevel; } else { var userIndex = findSessionIndex(userId); if (userIndex != -1) { userModel.setProperty(userIndex, "audioLevel", audioLevel); userModelData[userIndex].audioLevel = audioLevel; // Defensive programming + userModel.setProperty(userIndex, "avgAudioLevel", avgAudioLevel); + userModelData[userIndex].avgAudioLevel = avgAudioLevel; } } } diff --git a/interface/resources/qml/styles-uit/HifiConstants.qml b/interface/resources/qml/styles-uit/HifiConstants.qml index e261e2198f..031e80283e 100644 --- a/interface/resources/qml/styles-uit/HifiConstants.qml +++ b/interface/resources/qml/styles-uit/HifiConstants.qml @@ -318,5 +318,15 @@ Item { readonly property string deg: "\\" readonly property string px: "|" readonly property string editPencil: "\ue00d" + readonly property string vol_0: "\ue00e" + readonly property string vol_1: "\ue00f" + readonly property string vol_2: "\ue010" + readonly property string vol_3: "\ue011" + readonly property string vol_4: "\ue012" + readonly property string vol_x_0: "\ue013" + readonly property string vol_x_1: "\ue014" + readonly property string vol_x_2: "\ue015" + readonly property string vol_x_3: "\ue016" + readonly property string vol_x_4: "\ue017" } } diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index d48fe19a99..745a25ee65 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -2918,10 +2918,12 @@ void Application::keyPressEvent(QKeyEvent* event) { } break; case Qt::Key_P: { - bool isFirstPersonChecked = Menu::getInstance()->isOptionChecked(MenuOption::FirstPerson); - Menu::getInstance()->setIsOptionChecked(MenuOption::FirstPerson, !isFirstPersonChecked); - Menu::getInstance()->setIsOptionChecked(MenuOption::ThirdPerson, isFirstPersonChecked); - cameraMenuChanged(); + if (!(isShifted || isMeta || isOption)) { + bool isFirstPersonChecked = Menu::getInstance()->isOptionChecked(MenuOption::FirstPerson); + Menu::getInstance()->setIsOptionChecked(MenuOption::FirstPerson, !isFirstPersonChecked); + Menu::getInstance()->setIsOptionChecked(MenuOption::ThirdPerson, isFirstPersonChecked); + cameraMenuChanged(); + } break; } diff --git a/interface/src/Menu.cpp b/interface/src/Menu.cpp index acf97ad5f7..c131367aee 100644 --- a/interface/src/Menu.cpp +++ b/interface/src/Menu.cpp @@ -577,7 +577,7 @@ Menu::Menu() { nodeList.data(), SLOT(toggleSendNewerDSConnectVersion(bool))); #endif - + // Developer >> Tests >>> MenuWrapper* testMenu = developerMenu->addMenu("Tests"); addActionToQMenuAndActionHash(testMenu, MenuOption::RunClientScriptTests, 0, dialogsManager.data(), SLOT(showTestingResults())); @@ -628,9 +628,9 @@ Menu::Menu() { auto scope = DependencyManager::get(); MenuWrapper* audioScopeMenu = audioDebugMenu->addMenu("Audio Scope"); - addCheckableActionToQMenuAndActionHash(audioScopeMenu, MenuOption::AudioScope, Qt::CTRL | Qt::Key_P, false, + addCheckableActionToQMenuAndActionHash(audioScopeMenu, MenuOption::AudioScope, Qt::CTRL | Qt::Key_F2, false, scope.data(), SLOT(toggle())); - addCheckableActionToQMenuAndActionHash(audioScopeMenu, MenuOption::AudioScopePause, Qt::CTRL | Qt::SHIFT | Qt::Key_P, false, + addCheckableActionToQMenuAndActionHash(audioScopeMenu, MenuOption::AudioScopePause, Qt::CTRL | Qt::SHIFT | Qt::Key_F2, false, scope.data(), SLOT(togglePause())); addDisabledActionAndSeparator(audioScopeMenu, "Display Frames"); diff --git a/interface/src/avatar/Avatar.cpp b/interface/src/avatar/Avatar.cpp index 6e1f44f5ac..626719b42e 100644 --- a/interface/src/avatar/Avatar.cpp +++ b/interface/src/avatar/Avatar.cpp @@ -348,6 +348,8 @@ void Avatar::simulate(float deltaTime, bool inView) { PROFILE_RANGE(simulation, "updateJoints"); if (inView && _hasNewJointData) { _skeletonModel->getRig()->copyJointsFromJointData(_jointData); + glm::mat4 rootTransform = glm::scale(_skeletonModel->getScale()) * glm::translate(_skeletonModel->getOffset()); + _skeletonModel->getRig()->computeExternalPoses(rootTransform); _jointDataSimulationRate.increment(); _skeletonModel->simulate(deltaTime, true); diff --git a/interface/src/ui/overlays/Line3DOverlay.cpp b/interface/src/ui/overlays/Line3DOverlay.cpp index 23668bcc25..e1ea06c599 100644 --- a/interface/src/ui/overlays/Line3DOverlay.cpp +++ b/interface/src/ui/overlays/Line3DOverlay.cpp @@ -23,10 +23,15 @@ Line3DOverlay::Line3DOverlay() : Line3DOverlay::Line3DOverlay(const Line3DOverlay* line3DOverlay) : Base3DOverlay(line3DOverlay), - _start(line3DOverlay->_start), - _end(line3DOverlay->_end), _geometryCacheID(DependencyManager::get()->allocateID()) { + setParentID(line3DOverlay->getParentID()); + setParentJointIndex(line3DOverlay->getParentJointIndex()); + setLocalTransform(line3DOverlay->getLocalTransform()); + _direction = line3DOverlay->getDirection(); + _length = line3DOverlay->getLength(); + _endParentID = line3DOverlay->getEndParentID(); + _endParentJointIndex = line3DOverlay->getEndJointIndex(); } Line3DOverlay::~Line3DOverlay() { @@ -37,17 +42,23 @@ Line3DOverlay::~Line3DOverlay() { } glm::vec3 Line3DOverlay::getStart() const { - bool success; - glm::vec3 worldStart = localToWorld(_start, getParentID(), getParentJointIndex(), success); - if (!success) { - qDebug() << "Line3DOverlay::getStart failed"; - } - return worldStart; + return getPosition(); } glm::vec3 Line3DOverlay::getEnd() const { bool success; - glm::vec3 worldEnd = localToWorld(_end, getParentID(), getParentJointIndex(), success); + glm::vec3 localEnd; + glm::vec3 worldEnd; + + if (_endParentID != QUuid()) { + glm::vec3 localOffset = _direction * _length; + bool success; + worldEnd = localToWorld(localOffset, _endParentID, _endParentJointIndex, success); + return worldEnd; + } + + localEnd = getLocalEnd(); + worldEnd = localToWorld(localEnd, getParentID(), getParentJointIndex(), success); if (!success) { qDebug() << "Line3DOverlay::getEnd failed"; } @@ -55,27 +66,55 @@ glm::vec3 Line3DOverlay::getEnd() const { } void Line3DOverlay::setStart(const glm::vec3& start) { - bool success; - _start = worldToLocal(start, getParentID(), getParentJointIndex(), success); - if (!success) { - qDebug() << "Line3DOverlay::setStart failed"; - } + setPosition(start); } void Line3DOverlay::setEnd(const glm::vec3& end) { bool success; - _end = worldToLocal(end, getParentID(), getParentJointIndex(), success); + glm::vec3 localStart; + glm::vec3 localEnd; + glm::vec3 offset; + + if (_endParentID != QUuid()) { + offset = worldToLocal(end, _endParentID, _endParentJointIndex, success); + } else { + localStart = getLocalStart(); + localEnd = worldToLocal(end, getParentID(), getParentJointIndex(), success); + offset = localEnd - localStart; + } if (!success) { qDebug() << "Line3DOverlay::setEnd failed"; + return; + } + + _length = glm::length(offset); + if (_length > 0.0f) { + _direction = glm::normalize(offset); + } else { + _direction = glm::vec3(0.0f); + } +} + +void Line3DOverlay::setLocalEnd(const glm::vec3& localEnd) { + glm::vec3 offset; + if (_endParentID != QUuid()) { + offset = localEnd; + } else { + glm::vec3 localStart = getLocalStart(); + offset = localEnd - localStart; + } + _length = glm::length(offset); + if (_length > 0.0f) { + _direction = glm::normalize(offset); + } else { + _direction = glm::vec3(0.0f); } } AABox Line3DOverlay::getBounds() const { auto extents = Extents{}; - extents.addPoint(_start); - extents.addPoint(_end); - extents.transform(getTransform()); - + extents.addPoint(getStart()); + extents.addPoint(getEnd()); return AABox(extents); } @@ -90,18 +129,20 @@ void Line3DOverlay::render(RenderArgs* args) { glm::vec4 colorv4(color.red / MAX_COLOR, color.green / MAX_COLOR, color.blue / MAX_COLOR, alpha); auto batch = args->_batch; if (batch) { - batch->setModelTransform(getTransform()); + batch->setModelTransform(Transform()); + glm::vec3 start = getStart(); + glm::vec3 end = getEnd(); auto geometryCache = DependencyManager::get(); if (getIsDashedLine()) { // TODO: add support for color to renderDashedLine() geometryCache->bindSimpleProgram(*batch, false, false, false, true, true); - geometryCache->renderDashedLine(*batch, _start, _end, colorv4, _geometryCacheID); + geometryCache->renderDashedLine(*batch, start, end, colorv4, _geometryCacheID); } else if (_glow > 0.0f) { - geometryCache->renderGlowLine(*batch, _start, _end, colorv4, _glow, _glowWidth, _geometryCacheID); + geometryCache->renderGlowLine(*batch, start, end, colorv4, _glow, _glowWidth, _geometryCacheID); } else { geometryCache->bindSimpleProgram(*batch, false, false, false, true, true); - geometryCache->renderLine(*batch, _start, _end, colorv4, _geometryCacheID); + geometryCache->renderLine(*batch, start, end, colorv4, _geometryCacheID); } } } @@ -116,6 +157,10 @@ const render::ShapeKey Line3DOverlay::getShapeKey() { void Line3DOverlay::setProperties(const QVariantMap& originalProperties) { QVariantMap properties = originalProperties; + glm::vec3 newStart(0.0f); + bool newStartSet { false }; + glm::vec3 newEnd(0.0f); + bool newEndSet { false }; auto start = properties["start"]; // if "start" property was not there, check to see if they included aliases: startPoint @@ -123,30 +168,57 @@ void Line3DOverlay::setProperties(const QVariantMap& originalProperties) { start = properties["startPoint"]; } if (start.isValid()) { - setStart(vec3FromVariant(start)); + newStart = vec3FromVariant(start); + newStartSet = true; } properties.remove("start"); // so that Base3DOverlay doesn't respond to it - auto localStart = properties["localStart"]; - if (localStart.isValid()) { - _start = vec3FromVariant(localStart); - } - properties.remove("localStart"); // so that Base3DOverlay doesn't respond to it - auto end = properties["end"]; // if "end" property was not there, check to see if they included aliases: endPoint if (!end.isValid()) { end = properties["endPoint"]; } if (end.isValid()) { - setEnd(vec3FromVariant(end)); + newEnd = vec3FromVariant(end); + newEndSet = true; + } + properties.remove("end"); // so that Base3DOverlay doesn't respond to it + + auto length = properties["length"]; + if (length.isValid()) { + _length = length.toFloat(); + } + + Base3DOverlay::setProperties(properties); + + auto endParentIDProp = properties["endParentID"]; + if (endParentIDProp.isValid()) { + _endParentID = QUuid(endParentIDProp.toString()); + } + auto endParentJointIndexProp = properties["endParentJointIndex"]; + if (endParentJointIndexProp.isValid()) { + _endParentJointIndex = endParentJointIndexProp.toInt(); + } + + auto localStart = properties["localStart"]; + if (localStart.isValid()) { + glm::vec3 tmpLocalEnd = getLocalEnd(); + setLocalStart(vec3FromVariant(localStart)); + setLocalEnd(tmpLocalEnd); } auto localEnd = properties["localEnd"]; if (localEnd.isValid()) { - _end = vec3FromVariant(localEnd); + setLocalEnd(vec3FromVariant(localEnd)); + } + + // these are saved until after Base3DOverlay::setProperties so parenting infomation can be set, first + if (newStartSet) { + setStart(newStart); + } + if (newEndSet) { + setEnd(newEnd); } - properties.remove("localEnd"); // so that Base3DOverlay doesn't respond to it auto glow = properties["glow"]; if (glow.isValid()) { @@ -161,7 +233,6 @@ void Line3DOverlay::setProperties(const QVariantMap& originalProperties) { setGlow(glowWidth.toFloat()); } - Base3DOverlay::setProperties(properties); } QVariant Line3DOverlay::getProperty(const QString& property) { @@ -171,6 +242,15 @@ QVariant Line3DOverlay::getProperty(const QString& property) { if (property == "end" || property == "endPoint" || property == "p2") { return vec3toVariant(getEnd()); } + if (property == "localStart") { + return vec3toVariant(getLocalStart()); + } + if (property == "localEnd") { + return vec3toVariant(getLocalEnd()); + } + if (property == "length") { + return QVariant(getLength()); + } return Base3DOverlay::getProperty(property); } diff --git a/interface/src/ui/overlays/Line3DOverlay.h b/interface/src/ui/overlays/Line3DOverlay.h index b4e2ba8168..aceecff6b2 100644 --- a/interface/src/ui/overlays/Line3DOverlay.h +++ b/interface/src/ui/overlays/Line3DOverlay.h @@ -15,7 +15,7 @@ class Line3DOverlay : public Base3DOverlay { Q_OBJECT - + public: static QString const TYPE; virtual QString getType() const override { return TYPE; } @@ -37,6 +37,9 @@ public: void setStart(const glm::vec3& start); void setEnd(const glm::vec3& end); + void setLocalStart(const glm::vec3& localStart) { setLocalPosition(localStart); } + void setLocalEnd(const glm::vec3& localEnd); + void setGlow(const float& glow) { _glow = glow; } void setGlowWidth(const float& glowWidth) { _glowWidth = glowWidth; } @@ -47,13 +50,26 @@ public: virtual void locationChanged(bool tellPhysics = true) override; -protected: - glm::vec3 _start; - glm::vec3 _end; + glm::vec3 getDirection() const { return _direction; } + float getLength() const { return _length; } + glm::vec3 getLocalStart() const { return getLocalPosition(); } + glm::vec3 getLocalEnd() const { return getLocalStart() + _direction * _length; } + QUuid getEndParentID() const { return _endParentID; } + quint16 getEndJointIndex() const { return _endParentJointIndex; } + +private: + QUuid _endParentID; + quint16 _endParentJointIndex { INVALID_JOINT_INDEX }; + + // _direction and _length are in the parent's frame. If _endParentID is set, they are + // relative to that. Otherwise, they are relative to the local-start-position (which is the + // same as localPosition) + glm::vec3 _direction; // in parent frame + float _length { 1.0 }; // in parent frame + float _glow { 0.0 }; float _glowWidth { 0.0 }; int _geometryCacheID; }; - #endif // hifi_Line3DOverlay_h diff --git a/libraries/animation/src/Rig.cpp b/libraries/animation/src/Rig.cpp index 84e34adec7..b70d28fc30 100644 --- a/libraries/animation/src/Rig.cpp +++ b/libraries/animation/src/Rig.cpp @@ -1346,8 +1346,13 @@ void Rig::copyJointsFromJointData(const QVector& jointDataVec) { _internalPoseSet._relativePoses[i].trans() = relativeDefaultPoses[i].trans(); } } +} + +void Rig::computeExternalPoses(const glm::mat4& modelOffsetMat) { + _modelOffset = AnimPose(modelOffsetMat); + _geometryToRigTransform = _modelOffset * _geometryOffset; + _rigToGeometryTransform = glm::inverse(_geometryToRigTransform); - // build absolute poses and copy to externalPoseSet buildAbsoluteRigPoses(_internalPoseSet._relativePoses, _internalPoseSet._absolutePoses); QWriteLocker writeLock(&_externalPoseSetLock); _externalPoseSet = _internalPoseSet; diff --git a/libraries/animation/src/Rig.h b/libraries/animation/src/Rig.h index f1c87d0d3e..b2cc877460 100644 --- a/libraries/animation/src/Rig.h +++ b/libraries/animation/src/Rig.h @@ -210,6 +210,7 @@ public: void copyJointsIntoJointData(QVector& jointDataVec) const; void copyJointsFromJointData(const QVector& jointDataVec); + void computeExternalPoses(const glm::mat4& modelOffsetMat); void computeAvatarBoundingCapsule(const FBXGeometry& geometry, float& radiusOut, float& heightOut, glm::vec3& offsetOut) const; diff --git a/libraries/entities-renderer/src/RenderableWebEntityItem.cpp b/libraries/entities-renderer/src/RenderableWebEntityItem.cpp index b3978b9356..c4ae0db1aa 100644 --- a/libraries/entities-renderer/src/RenderableWebEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableWebEntityItem.cpp @@ -37,6 +37,8 @@ static uint64_t MAX_NO_RENDER_INTERVAL = 30 * USECS_PER_SECOND; static int MAX_WINDOW_SIZE = 4096; static float OPAQUE_ALPHA_THRESHOLD = 0.99f; +static int DEFAULT_MAX_FPS = 10; +static int YOUTUBE_MAX_FPS = 30; EntityItemPointer RenderableWebEntityItem::factory(const EntityItemID& entityID, const EntityItemProperties& properties) { EntityItemPointer entity{ new RenderableWebEntityItem(entityID) }; @@ -113,7 +115,7 @@ bool RenderableWebEntityItem::buildWebSurface(QSharedPointer // FIXME, the max FPS could be better managed by being dynamic (based on the number of current surfaces // and the current rendering load) - _webSurface->setMaxFps(10); + _webSurface->setMaxFps(DEFAULT_MAX_FPS); // The lifetime of the QML surface MUST be managed by the main thread // Additionally, we MUST use local variables copied by value, rather than @@ -256,9 +258,18 @@ void RenderableWebEntityItem::loadSourceURL() { _sourceUrl.toLower().endsWith(".htm") || _sourceUrl.toLower().endsWith(".html")) { _contentType = htmlContent; _webSurface->setBaseUrl(QUrl::fromLocalFile(PathUtils::resourcesPath() + "qml/controls/")); + + // We special case YouTube URLs since we know they are videos that we should play with at least 30 FPS. + if (sourceUrl.host().endsWith("youtube.com", Qt::CaseInsensitive)) { + _webSurface->setMaxFps(YOUTUBE_MAX_FPS); + } else { + _webSurface->setMaxFps(DEFAULT_MAX_FPS); + } + _webSurface->load("WebView.qml", [&](QQmlContext* context, QObject* obj) { context->setContextProperty("eventBridgeJavaScriptToInject", QVariant(_javaScriptToInject)); }); + _webSurface->getRootItem()->setProperty("url", _sourceUrl); _webSurface->getRootContext()->setContextProperty("desktop", QVariant()); diff --git a/libraries/render-utils/src/RenderDeferredTask.cpp b/libraries/render-utils/src/RenderDeferredTask.cpp index d1a7080eca..44cd51f245 100644 --- a/libraries/render-utils/src/RenderDeferredTask.cpp +++ b/libraries/render-utils/src/RenderDeferredTask.cpp @@ -160,13 +160,14 @@ RenderDeferredTask::RenderDeferredTask(RenderFetchCullSortTask::Output items) { addJob("DrawOverlay3DOpaque", overlayOpaquesInputs, true); addJob("DrawOverlay3DTransparent", overlayTransparentsInputs, false); - // Debugging stages { // Bounds do not draw on stencil buffer, so they must come last addJob("DrawMetaBounds", metas); + addJob("DrawOverlayOpaqueBounds", overlayOpaques); + addJob("DrawOverlayTransparentBounds", overlayTransparents); // Debugging Deferred buffer job const auto debugFramebuffers = render::Varying(DebugDeferredBuffer::Inputs(deferredFramebuffer, linearDepthTarget, surfaceGeometryFramebuffer, ambientOcclusionFramebuffer)); diff --git a/scripts/system/controllers/handControllerGrab.js b/scripts/system/controllers/handControllerGrab.js index d313d1cfa1..c584e777e3 100644 --- a/scripts/system/controllers/handControllerGrab.js +++ b/scripts/system/controllers/handControllerGrab.js @@ -205,14 +205,15 @@ var HARDWARE_MOUSE_ID = 0; // Value reserved for hardware mouse. var STATE_OFF = 0; var STATE_SEARCHING = 1; var STATE_DISTANCE_HOLDING = 2; -var STATE_NEAR_GRABBING = 3; -var STATE_NEAR_TRIGGER = 4; -var STATE_FAR_TRIGGER = 5; -var STATE_HOLD = 6; -var STATE_ENTITY_STYLUS_TOUCHING = 7; -var STATE_ENTITY_LASER_TOUCHING = 8; -var STATE_OVERLAY_STYLUS_TOUCHING = 9; -var STATE_OVERLAY_LASER_TOUCHING = 10; +var STATE_DISTANCE_ROTATING = 3; +var STATE_NEAR_GRABBING = 4; +var STATE_NEAR_TRIGGER = 5; +var STATE_FAR_TRIGGER = 6; +var STATE_HOLD = 7; +var STATE_ENTITY_STYLUS_TOUCHING = 8; +var STATE_ENTITY_LASER_TOUCHING = 9; +var STATE_OVERLAY_STYLUS_TOUCHING = 10; +var STATE_OVERLAY_LASER_TOUCHING = 11; var CONTROLLER_STATE_MACHINE = {}; @@ -231,6 +232,11 @@ CONTROLLER_STATE_MACHINE[STATE_DISTANCE_HOLDING] = { enterMethod: "distanceHoldingEnter", updateMethod: "distanceHolding" }; +CONTROLLER_STATE_MACHINE[STATE_DISTANCE_ROTATING] = { + name: "distance_rotating", + enterMethod: "distanceRotatingEnter", + updateMethod: "distanceRotating" +}; CONTROLLER_STATE_MACHINE[STATE_NEAR_GRABBING] = { name: "near_grabbing", enterMethod: "nearGrabbingEnter", @@ -266,6 +272,27 @@ CONTROLLER_STATE_MACHINE[STATE_OVERLAY_STYLUS_TOUCHING] = { }; CONTROLLER_STATE_MACHINE[STATE_OVERLAY_LASER_TOUCHING] = CONTROLLER_STATE_MACHINE[STATE_OVERLAY_STYLUS_TOUCHING]; +// 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]; + } + } + } + } + return to; + }; +} function distanceBetweenPointAndEntityBoundingBox(point, entityProps) { var entityXform = new Xform(entityProps.rotation, entityProps.position); @@ -740,6 +767,10 @@ function MyController(hand) { this.stylus = null; this.homeButtonTouched = false; + this.controllerJointIndex = MyAvatar.getJointIndex(this.hand === RIGHT_HAND ? + "_CONTROLLER_RIGHTHAND" : + "_CONTROLLER_LEFTHAND"); + // Until there is some reliable way to keep track of a "stack" of parentIDs, we'll have problems // when more than one avatar does parenting grabs on things. This script tries to work // around this with two associative arrays: previousParentID and previousParentJointIndex. If @@ -791,9 +822,6 @@ function MyController(hand) { // for visualizations this.overlayLine = null; - - // for lights - this.overlayLine = null; this.searchSphere = null; this.waitForTriggerRelease = false; @@ -869,7 +897,8 @@ function MyController(hand) { newState !== STATE_OVERLAY_LASER_TOUCHING)) { return; } - setGrabCommunications((newState === STATE_DISTANCE_HOLDING) || (newState === STATE_NEAR_GRABBING)); + setGrabCommunications((newState === STATE_DISTANCE_HOLDING) || (newState === STATE_DISTANCE_ROTATING) + || (newState === STATE_NEAR_GRABBING)); if (WANT_DEBUG || WANT_DEBUG_STATE) { var oldStateName = stateToName(this.state); var newStateName = stateToName(newState); @@ -920,9 +949,7 @@ function MyController(hand) { ignoreRayIntersection: true, drawInFront: false, parentID: AVATAR_SELF_ID, - parentJointIndex: MyAvatar.getJointIndex(this.hand === RIGHT_HAND ? - "_CONTROLLER_RIGHTHAND" : - "_CONTROLLER_LEFTHAND") + parentJointIndex: this.controllerJointIndex }); } }; @@ -1007,32 +1034,38 @@ function MyController(hand) { } }; - this.overlayLineOn = function(closePoint, farPoint, color) { + this.overlayLineOn = function(closePoint, farPoint, color, farParentID) { if (this.overlayLine === null) { var lineProperties = { name: "line", glow: 1.0, - start: closePoint, - end: farPoint, - color: color, - ignoreRayIntersection: true, // always ignore this - drawInFront: true, // Even when burried inside of something, show it. - visible: true, - alpha: 1 - }; - this.overlayLine = Overlays.addOverlay("line3d", lineProperties); - - } else { - Overlays.editOverlay(this.overlayLine, { lineWidth: 5, start: closePoint, end: farPoint, color: color, - visible: true, ignoreRayIntersection: true, // always ignore this drawInFront: true, // Even when burried inside of something, show it. - alpha: 1 - }); + visible: true, + alpha: 1, + parentID: AVATAR_SELF_ID, + parentJointIndex: this.controllerJointIndex, + endParentID: farParentID + }; + this.overlayLine = Overlays.addOverlay("line3d", lineProperties); + + } else { + if (farParentID && farParentID != NULL_UUID) { + Overlays.editOverlay(this.overlayLine, { + color: color, + endParentID: farParentID + }); + } else { + Overlays.editOverlay(this.overlayLine, { + length: Vec3.distance(farPoint, closePoint), + color: color, + endParentID: farParentID + }); + } } }; @@ -1439,9 +1472,10 @@ function MyController(hand) { var props = entityPropertiesCache.getProps(hotspot.entityID); var debug = (WANT_DEBUG_SEARCH_NAME && props.name === WANT_DEBUG_SEARCH_NAME); - var okToEquipFromOtherHand = ((this.getOtherHandController().state == STATE_NEAR_GRABBING || - this.getOtherHandController().state == STATE_DISTANCE_HOLDING) && - this.getOtherHandController().grabbedThingID == hotspot.entityID); + var otherHandControllerState = this.getOtherHandController().state; + var okToEquipFromOtherHand = ((otherHandControllerState === STATE_NEAR_GRABBING + || otherHandControllerState === STATE_DISTANCE_HOLDING || otherHandControllerState === STATE_DISTANCE_ROTATING) + && this.getOtherHandController().grabbedThingID === hotspot.entityID); var hasParent = true; if (props.parentID === NULL_UUID) { hasParent = false; @@ -1455,7 +1489,18 @@ function MyController(hand) { return true; }; + this.entityIsCloneable = function(entityID) { + var entityProps = entityPropertiesCache.getGrabbableProps(entityID); + var props = entityPropertiesCache.getProps(entityID); + if (!props) { + return false; + } + if (entityProps.hasOwnProperty("cloneable")) { + return entityProps.cloneable; + } + return false; + } this.entityIsGrabbable = function(entityID) { var grabbableProps = entityPropertiesCache.getGrabbableProps(entityID); var props = entityPropertiesCache.getProps(entityID); @@ -1535,7 +1580,7 @@ function MyController(hand) { this.entityIsNearGrabbable = function(entityID, handPosition, maxDistance) { - if (!this.entityIsGrabbable(entityID)) { + if (!this.entityIsCloneable(entityID) && !this.entityIsGrabbable(entityID)) { return false; } @@ -1731,7 +1776,11 @@ function MyController(hand) { this.grabbedThingID = entity; this.grabbedIsOverlay = false; this.grabbedDistance = rayPickInfo.distance; + if (this.getOtherHandController().state === STATE_DISTANCE_HOLDING) { + this.setState(STATE_DISTANCE_ROTATING, "distance rotate '" + name + "'"); + } else { this.setState(STATE_DISTANCE_HOLDING, "distance hold '" + name + "'"); + } return; } else { // potentialFarGrabEntity = entity; @@ -2036,6 +2085,19 @@ function MyController(hand) { return (dimensions.x * dimensions.y * dimensions.z) * density; }; + this.ensureDynamic = function () { + // if we distance hold something and keep it very still before releasing it, it ends up + // non-dynamic in bullet. If it's too still, give it a little bounce so it will fall. + var props = Entities.getEntityProperties(this.grabbedThingID, ["velocity", "dynamic", "parentID"]); + if (props.dynamic && props.parentID == NULL_UUID) { + var velocity = props.velocity; + if (Vec3.length(velocity) < 0.05) { // see EntityMotionState.cpp DYNAMIC_LINEAR_VELOCITY_THRESHOLD + velocity = { x: 0.0, y: 0.2, z: 0.0 }; + Entities.editEntity(this.grabbedThingID, { velocity: velocity }); + } + } + }; + this.distanceHoldingEnter = function() { this.clearEquipHaptics(); this.grabPointSphereOff(); @@ -2102,25 +2164,20 @@ function MyController(hand) { this.previousRoomControllerPosition = roomControllerPosition; }; - this.ensureDynamic = function() { - // if we distance hold something and keep it very still before releasing it, it ends up - // non-dynamic in bullet. If it's too still, give it a little bounce so it will fall. - var props = Entities.getEntityProperties(this.grabbedThingID, ["velocity", "dynamic", "parentID"]); - if (props.dynamic && props.parentID == NULL_UUID) { - var velocity = props.velocity; - if (Vec3.length(velocity) < 0.05) { // see EntityMotionState.cpp DYNAMIC_LINEAR_VELOCITY_THRESHOLD - velocity = { x: 0.0, y: 0.2, z:0.0 }; - Entities.editEntity(this.grabbedThingID, { velocity: velocity }); - } - } - }; - this.distanceHolding = function(deltaTime, timestamp) { if (!this.triggerClicked) { this.callEntityMethodOnGrabbed("releaseGrab"); this.ensureDynamic(); this.setState(STATE_OFF, "trigger released"); + if (this.getOtherHandController().state === STATE_DISTANCE_ROTATING) { + this.getOtherHandController().setState(STATE_SEARCHING, "trigger released on holding controller"); + // 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 + // rotating controller start distance holding the entity if it happens to be pointing at the entity. + } return; } @@ -2209,11 +2266,13 @@ function MyController(hand) { } this.maybeScale(grabbedProperties); + // visualizations - var rayPickInfo = this.calcRayPickInfo(this.hand); - - this.overlayLineOn(rayPickInfo.searchRay.origin, Vec3.subtract(grabbedProperties.position, this.offsetPosition), COLORS_GRAB_DISTANCE_HOLD); + this.overlayLineOn(rayPickInfo.searchRay.origin, + Vec3.subtract(grabbedProperties.position, this.offsetPosition), + COLORS_GRAB_DISTANCE_HOLD, + this.grabbedThingID); var distanceToObject = Vec3.length(Vec3.subtract(MyAvatar.position, this.currentObjectPosition)); var success = Entities.updateAction(this.grabbedThingID, this.actionID, { @@ -2232,6 +2291,64 @@ function MyController(hand) { this.previousRoomControllerPosition = roomControllerPosition; }; + this.distanceRotatingEnter = function() { + this.clearEquipHaptics(); + this.grabPointSphereOff(); + + var controllerLocation = getControllerWorldLocation(this.handToController(), true); + var worldControllerPosition = controllerLocation.position; + var worldControllerRotation = controllerLocation.orientation; + + var grabbedProperties = Entities.getEntityProperties(this.grabbedThingID, GRABBABLE_PROPERTIES); + this.currentObjectPosition = grabbedProperties.position; + this.grabRadius = this.grabbedDistance; + + // Offset between controller vector at the grab radius and the entity position. + var targetPosition = Vec3.multiply(this.grabRadius, Quat.getUp(worldControllerRotation)); + targetPosition = Vec3.sum(targetPosition, worldControllerPosition); + this.offsetPosition = Vec3.subtract(this.currentObjectPosition, targetPosition); + + // Initial controller rotation. + this.previousWorldControllerRotation = worldControllerRotation; + + Controller.triggerHapticPulse(HAPTIC_PULSE_STRENGTH, HAPTIC_PULSE_DURATION, this.hand); + this.turnOffVisualizations(); + }; + + this.distanceRotating = function(deltaTime, timestamp) { + + if (!this.triggerClicked) { + this.callEntityMethodOnGrabbed("releaseGrab"); + this.ensureDynamic(); + this.setState(STATE_OFF, "trigger released"); + return; + } + + var grabbedProperties = Entities.getEntityProperties(this.grabbedThingID, GRABBABLE_PROPERTIES); + + // Delta rotation of grabbing controller since last update. + var worldControllerRotation = getControllerWorldLocation(this.handToController(), true).orientation; + var controllerRotationDelta = Quat.multiply(worldControllerRotation, Quat.inverse(this.previousWorldControllerRotation)); + + // Rotate entity by twice the delta rotation. + controllerRotationDelta = Quat.multiply(controllerRotationDelta, controllerRotationDelta); + + // Perform the rotation in the translation controller's action update. + this.getOtherHandController().currentObjectRotation = Quat.multiply(controllerRotationDelta, + this.getOtherHandController().currentObjectRotation); + + // Rotate about the translation controller's target position. + this.offsetPosition = Vec3.multiplyQbyV(controllerRotationDelta, this.offsetPosition); + this.getOtherHandController().offsetPosition = Vec3.multiplyQbyV(controllerRotationDelta, + this.getOtherHandController().offsetPosition); + + var rayPickInfo = this.calcRayPickInfo(this.hand); + this.overlayLineOn(rayPickInfo.searchRay.origin, Vec3.subtract(grabbedProperties.position, this.offsetPosition), + COLORS_GRAB_DISTANCE_HOLD, this.grabbedThingID); + + this.previousWorldControllerRotation = worldControllerRotation; + } + this.setupHoldAction = function() { this.actionID = Entities.addAction("hold", this.grabbedThingID, { hand: this.hand === RIGHT_HAND ? "right" : "left", @@ -2385,6 +2502,9 @@ function MyController(hand) { this.offsetPosition = Vec3.multiplyQbyV(Quat.inverse(Quat.multiply(handRotation, this.offsetRotation)), offset); } + // This boolean is used to check if the object that is grabbed has just been cloned + // It is only set true, if the object that is grabbed creates a new clone. + var isClone = false; var isPhysical = propsArePhysical(grabbedProperties) || (!this.grabbedIsOverlay && entityHasActions(this.grabbedThingID)); if (isPhysical && this.state == STATE_NEAR_GRABBING && grabbedProperties.parentID === NULL_UUID) { @@ -2402,9 +2522,7 @@ function MyController(hand) { this.actionID = null; var handJointIndex; if (this.ignoreIK) { - handJointIndex = MyAvatar.getJointIndex(this.hand === RIGHT_HAND ? - "_CONTROLLER_RIGHTHAND" : - "_CONTROLLER_LEFTHAND"); + handJointIndex = this.controllerJointIndex; } else { handJointIndex = MyAvatar.getJointIndex(this.hand === RIGHT_HAND ? "RightHand" : "LeftHand"); } @@ -2423,6 +2541,54 @@ function MyController(hand) { if (this.grabbedIsOverlay) { Overlays.editOverlay(this.grabbedThingID, reparentProps); } else { + if (grabbedProperties.userData.length > 0) { + try{ + 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 count = 0; + worldEntities.forEach(function(item) { + var item = Entities.getEntityProperties(item, ["name"]); + if (item.name === grabbedProperties.name) { + count++; + } + }) + var cloneableProps = Entities.getEntityProperties(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; + delete cUserData.grabbableKey.cloneLimit; + delete cProperties.id + + cProperties.dynamic = dynamic; + cProperties.locked = false; + cUserData.grabbableKey.triggerable = true; + cUserData.grabbableKey.grabbable = true; + cProperties.lifetime = lifetime; + cProperties.userData = JSON.stringify(cUserData); + var cloneID = Entities.addEntity(cProperties); + this.grabbedThingID = cloneID; + grabbedProperties = Entities.getEntityProperties(cloneID); + } + }catch(e) {} + } Entities.editEntity(this.grabbedThingID, reparentProps); } @@ -2434,7 +2600,6 @@ function MyController(hand) { this.previousParentID[this.grabbedThingID] = grabbedProperties.parentID; this.previousParentJointIndex[this.grabbedThingID] = grabbedProperties.parentJointIndex; } - Messages.sendMessage('Hifi-Object-Manipulation', JSON.stringify({ action: 'equip', grabbedEntity: this.grabbedThingID, @@ -2450,22 +2615,37 @@ function MyController(hand) { }); } - if (this.state == STATE_NEAR_GRABBING) { - this.callEntityMethodOnGrabbed("startNearGrab"); - } else { // this.state == STATE_HOLD - this.callEntityMethodOnGrabbed("startEquip"); + var _this = this; + /* + * Setting context for function that is either called via timer or directly, depending if + * if the object in question is a clone. If it is a clone, we need to make sure that the intial equipment event + * is called correctly, as these just freshly created entity may not have completely initialized. + */ + var grabEquipCheck = function () { + if (_this.state == STATE_NEAR_GRABBING) { + _this.callEntityMethodOnGrabbed("startNearGrab"); + } else { // this.state == STATE_HOLD + _this.callEntityMethodOnGrabbed("startEquip"); + } + + _this.currentHandControllerTipPosition = + (_this.hand === RIGHT_HAND) ? MyAvatar.rightHandTipPosition : MyAvatar.leftHandTipPosition; + _this.currentObjectTime = Date.now(); + + _this.currentObjectPosition = grabbedProperties.position; + _this.currentObjectRotation = grabbedProperties.rotation; + _this.currentVelocity = ZERO_VEC; + _this.currentAngularVelocity = ZERO_VEC; + + _this.prevDropDetected = false; } - this.currentHandControllerTipPosition = - (this.hand === RIGHT_HAND) ? MyAvatar.rightHandTipPosition : MyAvatar.leftHandTipPosition; - this.currentObjectTime = Date.now(); - - this.currentObjectPosition = grabbedProperties.position; - this.currentObjectRotation = grabbedProperties.rotation; - this.currentVelocity = ZERO_VEC; - this.currentAngularVelocity = ZERO_VEC; - - this.prevDropDetected = false; + if (isClone) { + // 100 ms seems to be sufficient time to force the check even occur after the object has been initialized. + Script.setTimeout(grabEquipCheck, 100); + } else { + grabEquipCheck(); + } }; this.nearGrabbing = function(deltaTime, timestamp) { @@ -3149,9 +3329,7 @@ function MyController(hand) { return true; } - var controllerJointIndex = MyAvatar.getJointIndex(this.hand === RIGHT_HAND ? - "_CONTROLLER_RIGHTHAND" : - "_CONTROLLER_LEFTHAND"); + var controllerJointIndex = this.controllerJointIndex; if (props.parentJointIndex == controllerJointIndex) { return true; } @@ -3177,9 +3355,7 @@ function MyController(hand) { children = children.concat(Entities.getChildrenIDsOfJoint(AVATAR_SELF_ID, handJointIndex)); // find children of faux controller joint - var controllerJointIndex = MyAvatar.getJointIndex(this.hand === RIGHT_HAND ? - "_CONTROLLER_RIGHTHAND" : - "_CONTROLLER_LEFTHAND"); + var controllerJointIndex = this.controllerJointIndex; children = children.concat(Entities.getChildrenIDsOfJoint(MyAvatar.sessionUUID, controllerJointIndex)); children = children.concat(Entities.getChildrenIDsOfJoint(AVATAR_SELF_ID, controllerJointIndex)); @@ -3191,7 +3367,8 @@ function MyController(hand) { children = children.concat(Entities.getChildrenIDsOfJoint(AVATAR_SELF_ID, controllerCRJointIndex)); children.forEach(function(childID) { - if (childID !== _this.stylus) { + if (childID !== _this.stylus && + childID !== _this.overlayLine) { // we appear to be holding something and this script isn't in a state that would be holding something. // unhook it. if we previously took note of this entity's parent, put it back where it was. This // works around some problems that happen when more than one hand or avatar is passing something around. @@ -3287,6 +3464,7 @@ Messages.subscribe('Hifi-Hand-Disabler'); Messages.subscribe('Hifi-Hand-Grab'); Messages.subscribe('Hifi-Hand-RayPick-Blacklist'); Messages.subscribe('Hifi-Object-Manipulation'); +Messages.subscribe('Hifi-Hand-Drop'); var handleHandMessages = function(channel, message, sender) { var data; @@ -3372,6 +3550,15 @@ var handleHandMessages = function(channel, message, sender) { } catch (e) { print("WARNING: handControllerGrab.js -- error parsing Hifi-Hand-RayPick-Blacklist message: " + message); } + } else if (channel === 'Hifi-Hand-Drop') { + if (message === 'left') { + leftController.release(); + } else if (message === 'right') { + rightController.release(); + } else if (message === 'both') { + leftController.release(); + rightController.release(); + } } } }; diff --git a/scripts/system/edit.js b/scripts/system/edit.js index ad3af3a659..a440fec1ac 100644 --- a/scripts/system/edit.js +++ b/scripts/system/edit.js @@ -56,6 +56,7 @@ selectionManager.addEventListener(function () { lightOverlayManager.updatePositions(); }); +const KEY_P = 80; //Key code for letter p used for Parenting hotkey. var DEGREES_TO_RADIANS = Math.PI / 180.0; var RADIANS_TO_DEGREES = 180.0 / Math.PI; var epsilon = 0.001; @@ -843,7 +844,6 @@ function setupModelMenus() { }); modelMenuAddedDelete = true; } - Menu.addMenuItem({ menuName: "Edit", menuItemName: "Entity List...", @@ -851,11 +851,25 @@ function setupModelMenus() { afterItem: "Entities", grouping: "Advanced" }); + + Menu.addMenuItem({ + menuName: "Edit", + menuItemName: "Parent Entity to Last", + afterItem: "Entity List...", + grouping: "Advanced" + }); + + Menu.addMenuItem({ + menuName: "Edit", + menuItemName: "Unparent Entity", + afterItem: "Parent Entity to Last", + grouping: "Advanced" + }); Menu.addMenuItem({ menuName: "Edit", menuItemName: "Allow Selecting of Large Models", shortcutKey: "CTRL+META+L", - afterItem: "Entity List...", + afterItem: "Unparent Entity", isCheckable: true, isChecked: true, grouping: "Advanced" @@ -958,6 +972,8 @@ function cleanupModelMenus() { Menu.removeMenuItem("Edit", "Delete"); } + Menu.removeMenuItem("Edit", "Parent Entity to Last"); + Menu.removeMenuItem("Edit", "Unparent Entity"); Menu.removeMenuItem("Edit", "Entity List..."); Menu.removeMenuItem("Edit", "Allow Selecting of Large Models"); Menu.removeMenuItem("Edit", "Allow Selecting of Small Models"); @@ -990,6 +1006,9 @@ Script.scriptEnding.connect(function () { Overlays.deleteOverlay(importingSVOImageOverlay); Overlays.deleteOverlay(importingSVOTextOverlay); + + Controller.keyReleaseEvent.disconnect(keyReleaseEvent); + Controller.keyPressEvent.disconnect(keyPressEvent); }); var lastOrientation = null; @@ -1101,7 +1120,68 @@ function recursiveDelete(entities, childrenList) { Entities.deleteEntity(entityID); } } +function unparentSelectedEntities() { + if (SelectionManager.hasSelection()) { + var selectedEntities = selectionManager.selections; + var parentCheck = false; + if (selectedEntities.length < 1) { + Window.notifyEditError("You must have an entity selected inorder to unparent it."); + return; + } + selectedEntities.forEach(function (id, index) { + var parentId = Entities.getEntityProperties(id, ["parentID"]).parentID; + if (parentId !== null && parentId.length > 0 && parentId !== "{00000000-0000-0000-0000-000000000000}") { + parentCheck = true; + } + Entities.editEntity(id, {parentID: null}) + return true; + }); + if (parentCheck) { + if (selectedEntities.length > 1) { + Window.notify("Entities unparented"); + } else { + Window.notify("Entity unparented"); + } + } else { + if (selectedEntities.length > 1) { + Window.notify("Selected Entities have no parents"); + } else { + Window.notify("Selected Entity does not have a parent"); + } + } + } else { + Window.notifyEditError("You have nothing selected to unparent"); + } +} +function parentSelectedEntities() { + if (SelectionManager.hasSelection()) { + var selectedEntities = selectionManager.selections; + if (selectedEntities.length <= 1) { + Window.notifyEditError("You must have multiple entities selected in order to parent them"); + return; + } + var parentCheck = false; + var lastEntityId = selectedEntities[selectedEntities.length-1]; + selectedEntities.forEach(function (id, index) { + if (lastEntityId !== id) { + var parentId = Entities.getEntityProperties(id, ["parentID"]).parentID; + if (parentId !== lastEntityId) { + parentCheck = true; + } + Entities.editEntity(id, {parentID: lastEntityId}) + } + }); + + if(parentCheck) { + Window.notify("Entities parented"); + }else { + Window.notify("Entities are already parented to last"); + } + } else { + Window.notifyEditError("You have nothing selected to parent"); + } +} function deleteSelectedEntities() { if (SelectionManager.hasSelection()) { selectedParticleEntity = 0; @@ -1164,6 +1244,10 @@ function handeMenuEvent(menuItem) { Entities.setLightsArePickable(Menu.isOptionChecked("Allow Selecting of Lights")); } else if (menuItem === "Delete") { deleteSelectedEntities(); + } else if (menuItem === "Parent Entity to Last") { + parentSelectedEntities(); + } else if (menuItem === "Unparent Entity") { + unparentSelectedEntities(); } else if (menuItem === "Export Entities") { if (!selectionManager.hasSelection()) { Window.notifyEditError("No entities have been selected."); @@ -1289,13 +1373,12 @@ Window.svoImportRequested.connect(importSVO); Menu.menuItemEvent.connect(handeMenuEvent); -Controller.keyPressEvent.connect(function (event) { +var keyPressEvent = function (event) { if (isActive) { cameraManager.keyPressEvent(event); } -}); - -Controller.keyReleaseEvent.connect(function (event) { +}; +var keyReleaseEvent = function (event) { if (isActive) { cameraManager.keyReleaseEvent(event); } @@ -1329,8 +1412,16 @@ Controller.keyReleaseEvent.connect(function (event) { }); grid.setPosition(newPosition); } + } else if (event.key === KEY_P && event.isControl && !event.isAutoRepeat ) { + if (event.isShifted) { + unparentSelectedEntities(); + } else { + parentSelectedEntities(); + } } -}); +}; +Controller.keyReleaseEvent.connect(keyReleaseEvent); +Controller.keyPressEvent.connect(keyPressEvent); function recursiveAdd(newParentID, parentData) { var children = parentData.children; @@ -1580,6 +1671,10 @@ var PropertiesTool = function (opts) { } pushCommandForSelections(); selectionManager._update(); + } else if(data.type === 'parent') { + parentSelectedEntities(); + } else if(data.type === 'unparent') { + unparentSelectedEntities(); } else if(data.type === 'saveUserData'){ //the event bridge and json parsing handle our avatar id string differently. var actualID = data.id.split('"')[1]; @@ -1837,6 +1932,9 @@ var PopupMenu = function () { for (var i = 0; i < overlays.length; i++) { Overlays.deleteOverlay(overlays[i]); } + Controller.mousePressEvent.disconnect(self.mousePressEvent); + Controller.mouseMoveEvent.disconnect(self.mouseMoveEvent); + Controller.mouseReleaseEvent.disconnect(self.mouseReleaseEvent); } Controller.mousePressEvent.connect(self.mousePressEvent); @@ -1864,7 +1962,11 @@ var particleExplorerTool = new ParticleExplorerTool(); var selectedParticleEntity = 0; entityListTool.webView.webEventReceived.connect(function (data) { data = JSON.parse(data); - if (data.type === "selectionUpdate") { + if(data.type === 'parent') { + parentSelectedEntities(); + } else if(data.type === 'unparent') { + unparentSelectedEntities(); + } else if (data.type === "selectionUpdate") { var ids = data.entityIds; if (ids.length === 1) { if (Entities.getEntityProperties(ids[0], "type").type === "ParticleEffect") { diff --git a/scripts/system/html/entityList.html b/scripts/system/html/entityList.html index 197d8f550a..3cb79353f9 100644 --- a/scripts/system/html/entityList.html +++ b/scripts/system/html/entityList.html @@ -89,6 +89,7 @@ +
No entities found in view within a 100 meter radius. Try moving to a different location and refreshing.
diff --git a/scripts/system/html/entityProperties.html b/scripts/system/html/entityProperties.html index b11127b26c..5022dbd6a6 100644 --- a/scripts/system/html/entityProperties.html +++ b/scripts/system/html/entityProperties.html @@ -61,7 +61,7 @@ - +

@@ -295,12 +295,29 @@
+
+ + +
+
diff --git a/scripts/system/html/js/entityList.js b/scripts/system/html/js/entityList.js index 1af9c1e1d6..914fc6525e 100644 --- a/scripts/system/html/js/entityList.js +++ b/scripts/system/html/js/entityList.js @@ -19,6 +19,7 @@ const VISIBLE_GLYPH = ""; const TRANSPARENCY_GLYPH = ""; const SCRIPT_GLYPH = "k"; const DELETE = 46; // Key code for the delete key. +const KEY_P = 80; // Key code for letter p used for Parenting hotkey. const MAX_ITEMS = Number.MAX_VALUE; // Used to set the max length of the list of discovered entities. debugPrint = function (message) { @@ -26,7 +27,7 @@ debugPrint = function (message) { }; function loaded() { - openEventBridge(function() { + openEventBridge(function() { entityList = new List('entity-list', { valueNames: ['name', 'type', 'url', 'locked', 'visible'], page: MAX_ITEMS}); entityList.clear(); elEntityTable = document.getElementById("entity-table"); @@ -48,7 +49,7 @@ function loaded() { elNoEntitiesInView = document.getElementById("no-entities-in-view"); elNoEntitiesRadius = document.getElementById("no-entities-radius"); elEntityTableScroll = document.getElementById("entity-table-scroll"); - + document.getElementById("entity-name").onclick = function() { setSortColumn('name'); }; @@ -90,7 +91,7 @@ function loaded() { selection = selection.concat(selectedEntities); } else if (clickEvent.shiftKey && selectedEntities.length > 0) { var previousItemFound = -1; - var clickedItemFound = -1; + var clickedItemFound = -1; for (var entity in entityList.visibleItems) { if (clickedItemFound === -1 && this.dataset.entityId == entityList.visibleItems[entity].values().id) { clickedItemFound = entity; @@ -113,11 +114,11 @@ function loaded() { selection = selection.concat(betweenItems, selectedEntities); } } - + selectedEntities = selection; - + this.className = 'selected'; - + EventBridge.emitWebEvent(JSON.stringify({ type: "selectionUpdate", focus: false, @@ -126,7 +127,7 @@ function loaded() { refreshFooter(); } - + function onRowDoubleClicked() { EventBridge.emitWebEvent(JSON.stringify({ type: "selectionUpdate", @@ -134,7 +135,7 @@ function loaded() { entityIds: [this.dataset.entityId], })); } - + const BYTES_PER_MEGABYTE = 1024 * 1024; function decimalMegabytes(number) { @@ -173,7 +174,7 @@ function loaded() { currentElement.onclick = onRowClicked; currentElement.ondblclick = onRowDoubleClicked; }); - + if (refreshEntityListTimer) { clearTimeout(refreshEntityListTimer); } @@ -183,13 +184,13 @@ function loaded() { item.values({ name: name, url: filename, locked: locked, visible: visible }); } } - + function clearEntities() { entities = {}; entityList.clear(); refreshFooter(); } - + var elSortOrder = { name: document.querySelector('#entity-name .sort-order'), type: document.querySelector('#entity-type .sort-order'), @@ -215,12 +216,12 @@ function loaded() { entityList.sort(currentSortColumn, { order: currentSortOrder }); } setSortColumn('type'); - + function refreshEntities() { clearEntities(); EventBridge.emitWebEvent(JSON.stringify({ type: 'refresh' })); } - + function refreshFooter() { if (selectedEntities.length > 1) { elFooter.firstChild.nodeValue = selectedEntities.length + " entities selected"; @@ -239,7 +240,7 @@ function loaded() { entityList.search(elFilter.value); refreshFooter(); } - + function updateSelectedEntities(selectedIDs) { var notFound = false; for (var id in entities) { @@ -262,7 +263,7 @@ function loaded() { return notFound; } - + elRefresh.onclick = function() { refreshEntities(); } @@ -282,7 +283,7 @@ function loaded() { EventBridge.emitWebEvent(JSON.stringify({ type: 'delete' })); refreshEntities(); } - + document.addEventListener("keydown", function (keyDownEvent) { if (keyDownEvent.target.nodeName === "INPUT") { return; @@ -292,8 +293,15 @@ function loaded() { EventBridge.emitWebEvent(JSON.stringify({ type: 'delete' })); refreshEntities(); } + if (keyDownEvent.keyCode === KEY_P && keyDownEvent.ctrlKey) { + if (keyDownEvent.shiftKey) { + EventBridge.emitWebEvent(JSON.stringify({ type: 'unparent' })); + } else { + EventBridge.emitWebEvent(JSON.stringify({ type: 'parent' })); + } + } }, false); - + var isFilterInView = false; var FILTER_IN_VIEW_ATTRIBUTE = "pressed"; elNoEntitiesInView.style.display = "none"; @@ -320,7 +328,7 @@ function loaded() { if (window.EventBridge !== undefined) { EventBridge.scriptEventReceived.connect(function(data) { data = JSON.parse(data); - + if (data.type === "clearEntityList") { clearEntities(); } else if (data.type == "selectionUpdate") { @@ -426,4 +434,3 @@ function loaded() { event.preventDefault(); }, false); } - diff --git a/scripts/system/html/js/entityProperties.js b/scripts/system/html/js/entityProperties.js index 8879c0f34e..3280e1f196 100644 --- a/scripts/system/html/js/entityProperties.js +++ b/scripts/system/html/js/entityProperties.js @@ -24,9 +24,10 @@ var ICON_FOR_TYPE = { } var EDITOR_TIMEOUT_DURATION = 1500; - +const KEY_P = 80; //Key code for letter p used for Parenting hotkey. var colorPickers = []; var lastEntityID = null; + debugPrint = function(message) { EventBridge.emitWebEvent( JSON.stringify({ @@ -273,7 +274,7 @@ function updateCheckedSubProperty(propertyName, propertyValue, subPropertyElemen propertyValue += subPropertyString + ','; } } else { - // We've unchecked, so remove + // We've unchecked, so remove propertyValue = propertyValue.replace(subPropertyString + ",", ""); } @@ -323,13 +324,9 @@ function setUserDataFromEditor(noUpdate) { }) ); } - } - - } - -function userDataChanger(groupName, keyName, checkBoxElement, userDataElement, defaultValue) { +function multiDataUpdater(groupName, updateKeyPair, userDataElement, defaults) { var properties = {}; var parsedData = {}; try { @@ -339,17 +336,31 @@ function userDataChanger(groupName, keyName, checkBoxElement, userDataElement, d } else { parsedData = JSON.parse(userDataElement.value); } - } catch (e) {} if (!(groupName in parsedData)) { parsedData[groupName] = {} } - delete parsedData[groupName][keyName]; - if (checkBoxElement.checked !== defaultValue) { - parsedData[groupName][keyName] = checkBoxElement.checked; - } - + var keys = Object.keys(updateKeyPair); + keys.forEach(function (key) { + delete parsedData[groupName][key]; + if (updateKeyPair[key] !== null && updateKeyPair[key] !== "null") { + if (updateKeyPair[key] instanceof Element) { + if(updateKeyPair[key].type === "checkbox") { + if (updateKeyPair[key].checked !== defaults[key]) { + parsedData[groupName][key] = updateKeyPair[key].checked; + } + } else { + var val = isNaN(updateKeyPair[key].value) ? updateKeyPair[key].value : parseInt(updateKeyPair[key].value); + if (val !== defaults[key]) { + parsedData[groupName][key] = val; + } + } + } else { + parsedData[groupName][key] = updateKeyPair[key]; + } + } + }); if (Object.keys(parsedData[groupName]).length == 0) { delete parsedData[groupName]; } @@ -368,6 +379,12 @@ function userDataChanger(groupName, keyName, checkBoxElement, userDataElement, d properties: properties, }) ); +} +function userDataChanger(groupName, keyName, values, userDataElement, defaultValue) { + var val = {}, def = {}; + val[keyName] = values; + def[keyName] = defaultValue; + multiDataUpdater(groupName, val, userDataElement, def); }; function setTextareaScrolling(element) { @@ -521,6 +538,7 @@ function unbindAllInputs() { function loaded() { openEventBridge(function() { + var allSections = []; var elID = document.getElementById("property-id"); var elType = document.getElementById("property-type"); @@ -584,6 +602,13 @@ function loaded() { var elCollisionSoundURL = document.getElementById("property-collision-sound-url"); var elGrabbable = document.getElementById("property-grabbable"); + + var elCloneable = document.getElementById("property-cloneable"); + var elCloneableDynamic = document.getElementById("property-cloneable-dynamic"); + var elCloneableGroup = document.getElementById("group-cloneable-group"); + var elCloneableLifetime = document.getElementById("property-cloneable-lifetime"); + var elCloneableLimit = document.getElementById("property-cloneable-limit"); + var elWantsTrigger = document.getElementById("property-wants-trigger"); var elIgnoreIK = document.getElementById("property-ignore-ik"); @@ -780,7 +805,7 @@ function loaded() { if (lastEntityID !== '"' + properties.id + '"' && lastEntityID !== null && editor !== null) { saveJSONUserData(true); } - //the event bridge and json parsing handle our avatar id string differently. + //the event bridge and json parsing handle our avatar id string differently. lastEntityID = '"' + properties.id + '"'; elID.innerHTML = properties.id; @@ -847,8 +872,16 @@ function loaded() { elCollideOtherAvatar.checked = properties.collidesWith.indexOf("otherAvatar") > -1; elGrabbable.checked = properties.dynamic; + elWantsTrigger.checked = false; elIgnoreIK.checked = true; + + elCloneable.checked = false; + elCloneableDynamic.checked = false; + elCloneableGroup.style.display = elCloneable.checked ? "block": "none"; + elCloneableLimit.value = 10; + elCloneableLifetime.value = 300; + var parsedUserData = {} try { parsedUserData = JSON.parse(properties.userData); @@ -863,8 +896,25 @@ function loaded() { if ("ignoreIK" in parsedUserData["grabbableKey"]) { elIgnoreIK.checked = parsedUserData["grabbableKey"].ignoreIK; } + 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) { + if ("cloneLifetime" in parsedUserData["grabbableKey"]) { + elCloneableLifetime.value = parsedUserData["grabbableKey"].cloneLifetime ? parsedUserData["grabbableKey"].cloneLifetime : 300; + } + if ("cloneLimit" in parsedUserData["grabbableKey"]) { + elCloneableLimit.value = parsedUserData["grabbableKey"].cloneLimit ? parsedUserData["grabbableKey"].cloneLimit : 10; + } + } + } } - } catch (e) {} + } catch (e) { + } elCollisionSoundURL.value = properties.collisionSoundURL; elLifetime.value = properties.lifetime; @@ -1154,8 +1204,38 @@ function loaded() { }); elGrabbable.addEventListener('change', function() { + if(elCloneable.checked) { + elGrabbable.checked = false; + } userDataChanger("grabbableKey", "grabbable", elGrabbable, elUserData, properties.dynamic); }); + elCloneableDynamic.addEventListener('change', function (event){ + userDataChanger("grabbableKey", "cloneDynamic", event.target, elUserData, -1); + }); + elCloneable.addEventListener('change', function (event) { + var checked = event.target.checked; + if (checked) { + multiDataUpdater("grabbableKey", + {cloneLifetime: elCloneableLifetime, cloneLimit: elCloneableLimit, cloneDynamic: elCloneableDynamic, cloneable: event.target}, + elUserData, {}); + elCloneableGroup.style.display = "block"; + EventBridge.emitWebEvent( + '{"id":' + lastEntityID + ', "type":"update", "properties":{"dynamic":false, "grabbable": false}}' + ); + } else { + multiDataUpdater("grabbableKey", + {cloneLifetime: null, cloneLimit: null, cloneDynamic: null, cloneable: false}, + elUserData, {}); + elCloneableGroup.style.display = "none"; + } + }); + + var numberListener = function (event) { + userDataChanger("grabbableKey", event.target.getAttribute("data-user-data-type"), parseInt(event.target.value), elUserData, false); + }; + elCloneableLifetime.addEventListener('change', numberListener); + elCloneableLimit.addEventListener('change', numberListener); + elWantsTrigger.addEventListener('change', function() { userDataChanger("grabbableKey", "wantsTrigger", elWantsTrigger, elUserData, false); }); @@ -1390,7 +1470,7 @@ function loaded() { elZoneFlyingAllowed.addEventListener('change', createEmitCheckedPropertyUpdateFunction('flyingAllowed')); elZoneGhostingAllowed.addEventListener('change', createEmitCheckedPropertyUpdateFunction('ghostingAllowed')); elZoneFilterURL.addEventListener('change', createEmitTextPropertyUpdateFunction('filterURL')); - + var voxelVolumeSizeChangeFunction = createEmitVec3PropertyUpdateFunction( 'voxelVolumeSize', elVoxelVolumeSizeX, elVoxelVolumeSizeY, elVoxelVolumeSizeZ); elVoxelVolumeSizeX.addEventListener('change', voxelVolumeSizeChangeFunction); @@ -1441,7 +1521,15 @@ function loaded() { })); }); - + document.addEventListener("keydown", function (keyDown) { + if (keyDown.keyCode === KEY_P && keyDown.ctrlKey) { + if (keyDown.shiftKey) { + EventBridge.emitWebEvent(JSON.stringify({ type: 'unparent' })); + } else { + EventBridge.emitWebEvent(JSON.stringify({ type: 'parent' })); + } + } + }); window.onblur = function() { // Fake a change event var ev = document.createEvent("HTMLEvents"); diff --git a/scripts/system/html/js/gridControls.js b/scripts/system/html/js/gridControls.js index a245ed4cda..be4271788e 100644 --- a/scripts/system/html/js/gridControls.js +++ b/scripts/system/html/js/gridControls.js @@ -6,6 +6,8 @@ // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +const KEY_P = 80; //Key code for letter p used for Parenting hotkey. + function loaded() { openEventBridge(function() { elPosY = document.getElementById("horiz-y"); @@ -131,10 +133,17 @@ function loaded() { EventBridge.emitWebEvent(JSON.stringify({ type: 'init' })); }); - + document.addEventListener("keydown", function (keyDown) { + if (keyDown.keyCode === KEY_P && keyDown.ctrlKey) { + if (keyDown.shiftKey) { + EventBridge.emitWebEvent(JSON.stringify({ type: 'unparent' })); + } else { + EventBridge.emitWebEvent(JSON.stringify({ type: 'parent' })); + } + } + }) // Disable right-click context menu which is not visible in the HMD and makes it seem like the app has locked document.addEventListener("contextmenu", function (event) { event.preventDefault(); }, false); } - diff --git a/scripts/system/libraries/entitySelectionTool.js b/scripts/system/libraries/entitySelectionTool.js index 9c1626caf4..d68a525458 100644 --- a/scripts/system/libraries/entitySelectionTool.js +++ b/scripts/system/libraries/entitySelectionTool.js @@ -1170,14 +1170,14 @@ SelectionDisplay = (function() { // determine which bottom corner we are closest to /*------------------------------ example: - + BRF +--------+ BLF | | | | BRN +--------+ BLN - + * - + ------------------------------*/ var cameraPosition = Camera.getPosition(); @@ -2189,8 +2189,12 @@ SelectionDisplay = (function() { offset = Vec3.multiplyQbyV(properties.rotation, offset); var boxPosition = Vec3.sum(properties.position, offset); + var color = {red: 255, green: 128, blue: 0}; + if (i >= selectionManager.selections.length - 1) color = {red: 255, green: 255, blue: 64}; + Overlays.editOverlay(selectionBoxes[i], { position: boxPosition, + color: color, rotation: properties.rotation, dimensions: properties.dimensions, visible: true, @@ -2395,7 +2399,7 @@ SelectionDisplay = (function() { if (wantDebug) { print("Start Elevation: " + translateXZTool.startingElevation + ", elevation: " + elevation); } - if ((translateXZTool.startingElevation > 0.0 && elevation < MIN_ELEVATION) || + if ((translateXZTool.startingElevation > 0.0 && elevation < MIN_ELEVATION) || (translateXZTool.startingElevation < 0.0 && elevation > -MIN_ELEVATION)) { if (wantDebug) { print("too close to horizon!"); @@ -3857,7 +3861,7 @@ SelectionDisplay = (function() { }; that.mousePressEvent = function(event) { - var wantDebug = false; + var wantDebug = false; if (!event.isLeftButton && !that.triggered) { // if another mouse button than left is pressed ignore it return false; @@ -3889,7 +3893,7 @@ SelectionDisplay = (function() { if (result.intersects) { - + if (wantDebug) { print("something intersects... "); print(" result.overlayID:" + result.overlayID + "[" + overlayNames[result.overlayID] + "]"); @@ -3989,7 +3993,7 @@ SelectionDisplay = (function() { if (wantDebug) { print("rotate handle case..."); } - + // After testing our stretch handles, then check out rotate handles Overlays.editOverlay(yawHandle, { @@ -4211,7 +4215,7 @@ SelectionDisplay = (function() { case selectionBox: activeTool = translateXZTool; translateXZTool.pickPlanePosition = result.intersection; - translateXZTool.greatestDimension = Math.max(Math.max(SelectionManager.worldDimensions.x, SelectionManager.worldDimensions.y), + translateXZTool.greatestDimension = Math.max(Math.max(SelectionManager.worldDimensions.x, SelectionManager.worldDimensions.y), SelectionManager.worldDimensions.z); if (wantDebug) { print("longest dimension: " + translateXZTool.greatestDimension); @@ -4220,7 +4224,7 @@ SelectionDisplay = (function() { translateXZTool.startingElevation = translateXZTool.elevation(pickRay.origin, translateXZTool.pickPlanePosition); print(" starting elevation: " + translateXZTool.startingElevation); } - + mode = translateXZTool.mode; activeTool.onBegin(event); somethingClicked = 'selectionBox'; diff --git a/scripts/system/notifications.js b/scripts/system/notifications.js index 3ae071c7e3..b2ebb1fd46 100644 --- a/scripts/system/notifications.js +++ b/scripts/system/notifications.js @@ -521,6 +521,9 @@ function onEditError(msg) { createNotification(wordWrap(msg), NotificationType.EDIT_ERROR); } +function onNotify(msg) { + createNotification(wordWrap(msg), NotificationType.UNKNOWN); // Needs a generic notification system for user feedback, thus using this +} function onSnapshotTaken(pathStillSnapshot, pathAnimatedSnapshot, notify) { if (notify) { @@ -637,6 +640,7 @@ Window.domainConnectionRefused.connect(onDomainConnectionRefused); Window.snapshotTaken.connect(onSnapshotTaken); Window.processingGif.connect(processingGif); Window.notifyEditError = onEditError; +Window.notify = onNotify; setup(); diff --git a/scripts/system/pal.js b/scripts/system/pal.js index 106f226a33..70b2739c96 100644 --- a/scripts/system/pal.js +++ b/scripts/system/pal.js @@ -206,6 +206,17 @@ HighlightedEntity.updateOverlays = function updateHighlightedEntities() { }); }; +/* this contains current gain for a given node (by session id). More efficient than + * querying it, plus there isn't a getGain function so why write one */ +var sessionGains = {}; +function convertDbToLinear(decibels) { + // +20db = 10x, 0dB = 1x, -10dB = 0.1x, etc... + // but, your perception is that something 2x as loud is +10db + // so we go from -60 to +20 or 1/64x to 4x. For now, we can + // maybe scale the signal this way?? + return Math.pow(2, decibels/10.0); +} + function fromQml(message) { // messages are {method, params}, like json-rpc. See also sendToQml. var data; switch (message.method) { @@ -311,6 +322,7 @@ function populateUserList(selectData) { userName: '', sessionId: id || '', audioLevel: 0.0, + avgAudioLevel: 0.0, admin: false, personalMute: !!id && Users.getPersonalMuteStatus(id), // expects proper boolean, not null ignore: !!id && Users.getIgnoreStatus(id) // ditto @@ -604,41 +616,54 @@ function receiveMessage(channel, messageString, senderID) { } } - var AVERAGING_RATIO = 0.05; var LOUDNESS_FLOOR = 11.0; var LOUDNESS_SCALE = 2.8 / 5.0; var LOG2 = Math.log(2.0); +var AUDIO_PEAK_DECAY = 0.02; var myData = {}; // we're not includied in ExtendedOverlay.get. +function scaleAudio(val) { + var audioLevel = 0.0; + if (val <= LOUDNESS_FLOOR) { + audioLevel = val / LOUDNESS_FLOOR * LOUDNESS_SCALE; + } else { + audioLevel = (val -(LOUDNESS_FLOOR -1 )) * LOUDNESS_SCALE; + } + if (audioLevel > 1.0) { + audioLevel = 1; + } + return audioLevel; +} + function getAudioLevel(id) { // the VU meter should work similarly to the one in AvatarInputs: log scale, exponentially averaged // But of course it gets the data at a different rate, so we tweak the averaging ratio and frequency // of updating (the latter for efficiency too). var avatar = AvatarList.getAvatar(id); var audioLevel = 0.0; + var avgAudioLevel = 0.0; var data = id ? ExtendedOverlay.get(id) : myData; - if (!data) { - return audioLevel; - } + if (data) { - // we will do exponential moving average by taking some the last loudness and averaging - data.accumulatedLevel = AVERAGING_RATIO * (data.accumulatedLevel || 0) + (1 - AVERAGING_RATIO) * (avatar.audioLoudness); + // we will do exponential moving average by taking some the last loudness and averaging + data.accumulatedLevel = AVERAGING_RATIO * (data.accumulatedLevel || 0) + (1 - AVERAGING_RATIO) * (avatar.audioLoudness); - // add 1 to insure we don't go log() and hit -infinity. Math.log is - // natural log, so to get log base 2, just divide by ln(2). - var logLevel = Math.log(data.accumulatedLevel + 1) / LOG2; + // add 1 to insure we don't go log() and hit -infinity. Math.log is + // natural log, so to get log base 2, just divide by ln(2). + audioLevel = scaleAudio(Math.log(data.accumulatedLevel + 1) / LOG2); - if (logLevel <= LOUDNESS_FLOOR) { - audioLevel = logLevel / LOUDNESS_FLOOR * LOUDNESS_SCALE; - } else { - audioLevel = (logLevel - (LOUDNESS_FLOOR - 1.0)) * LOUDNESS_SCALE; + // decay avgAudioLevel + avgAudioLevel = Math.max((1-AUDIO_PEAK_DECAY) * (data.avgAudioLevel || 0), audioLevel); + + data.avgAudioLevel = avgAudioLevel; + data.audioLevel = audioLevel; + + // now scale for the gain. Also, asked to boost the low end, so one simple way is + // to take sqrt of the value. Lets try that, see how it feels. + avgAudioLevel = Math.min(1.0, Math.sqrt(avgAudioLevel *(sessionGains[id] || 0.75))); } - if (audioLevel > 1.0) { - audioLevel = 1; - } - data.audioLevel = audioLevel; - return audioLevel; + return [audioLevel, avgAudioLevel]; } function createAudioInterval(interval) {