merging with master

This commit is contained in:
sam 2017-03-05 12:57:13 -08:00
commit cffd838d1d
41 changed files with 1352 additions and 385 deletions

View file

@ -1,7 +1,7 @@
###Dependencies ###Dependencies
* [cmake](http://www.cmake.org/cmake/resources/software.html) ~> 3.3.2 * [cmake](https://cmake.org/download/) ~> 3.3.2
* [Qt](http://www.qt.io/download-open-source) ~> 5.6.1 * [Qt](https://www.qt.io/download-open-source) ~> 5.6.1
* [OpenSSL](https://www.openssl.org/community/binaries.html) * [OpenSSL](https://www.openssl.org/community/binaries.html)
* IMPORTANT: Use the latest available version of OpenSSL to avoid security vulnerabilities. * IMPORTANT: Use the latest available version of OpenSSL to avoid security vulnerabilities.
* [VHACD](https://github.com/virneo/v-hacd)(clone this repository)(Optional) * [VHACD](https://github.com/virneo/v-hacd)(clone this repository)(Optional)
@ -9,18 +9,17 @@
####CMake External Project Dependencies ####CMake External Project Dependencies
* [boostconfig](https://github.com/boostorg/config) ~> 1.58 * [boostconfig](https://github.com/boostorg/config) ~> 1.58
* [Bullet Physics Engine](https://code.google.com/p/bullet/downloads/list) ~> 2.82 * [Bullet Physics Engine](https://github.com/bulletphysics/bullet3/releases) ~> 2.83
* [Faceshift](http://www.faceshift.com/) ~> 4.3
* [GLEW](http://glew.sourceforge.net/) * [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) * [gverb](https://github.com/highfidelity/gverb)
* [Oculus SDK](https://developer.oculus.com/downloads/) ~> 0.6 (Win32) / 0.5 (Mac / Linux) * [Oculus SDK](https://developer.oculus.com/downloads/) ~> 0.6 (Win32) / 0.5 (Mac / Linux)
* [oglplus](http://oglplus.org/) ~> 0.63 * [oglplus](http://oglplus.org/) ~> 0.63
* [OpenVR](https://github.com/ValveSoftware/openvr) ~> 0.91 (Win32 only) * [OpenVR](https://github.com/ValveSoftware/openvr) ~> 0.91 (Win32 only)
* [Polyvox](http://www.volumesoffun.com/) ~> 0.2.1 * [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 * [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 * [Intel Threading Building Blocks](https://www.threadingbuildingblocks.org/) ~> 4.3
* [Sixense](http://sixense.com/) ~> 071615 * [Sixense](http://sixense.com/) ~> 071615
* [zlib](http://www.zlib.net/) ~> 1.28 (Win32 only) * [zlib](http://www.zlib.net/) ~> 1.28 (Win32 only)

View file

@ -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. 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
[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 tap homebrew/versions
brew install cmake openssl 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 ###Qt
You can use the online installer or the offline installer. 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: * When it asks you to select components, select the following:
* Qt > Qt 5.6 * 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: 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. * Set the QT_CMAKE_PREFIX_PATH environment variable to your `Qt5.6.1/5.6/clang_64/lib/cmake/` directory.

View file

@ -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** * Qt > Qt 5.6.1 > **msvc2013 64-bit**
* Download the offline installer, 32- or 64-bit to match your build preference: * 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) * [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](http://download.qt.io/official_releases/qt/5.6/5.6.1-1/qt-opensource-windows-x86-msvc2013_64-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: 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. * 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_CTX_set_next_proto_select_cb
QSslSocket: cannot resolve SSL_get0_next_proto_negotiated 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 * Win32 OpenSSL v1.0.1q
* Win64 OpenSSL v1.0.1q * Win64 OpenSSL v1.0.1q

View file

@ -11,11 +11,11 @@ We're hiring! We're looking for skilled developers;
send your resume to hiring@highfidelity.com send your resume to hiring@highfidelity.com
##### Chat with us ##### 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
========= =========
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 Build Instructions
========= =========

View file

@ -43,7 +43,6 @@
#include <WebSocketServerClass.h> #include <WebSocketServerClass.h>
#include <EntityScriptingInterface.h> // TODO: consider moving to scriptengine.h #include <EntityScriptingInterface.h> // TODO: consider moving to scriptengine.h
#include "avatars/ScriptableAvatar.h"
#include "entities/AssignmentParentFinder.h" #include "entities/AssignmentParentFinder.h"
#include "RecordingScriptingInterface.h" #include "RecordingScriptingInterface.h"
#include "AbstractAudioInterface.h" #include "AbstractAudioInterface.h"
@ -88,9 +87,9 @@ void Agent::playAvatarSound(SharedSoundPointer sound) {
QMetaObject::invokeMethod(this, "playAvatarSound", Q_ARG(SharedSoundPointer, sound)); QMetaObject::invokeMethod(this, "playAvatarSound", Q_ARG(SharedSoundPointer, sound));
return; return;
} else { } else {
// TODO: seems to add occasional artifact in tests. I believe it is // TODO: seems to add occasional artifact in tests. I believe it is
// correct to do this, but need to figure out for sure, so commenting this // correct to do this, but need to figure out for sure, so commenting this
// out until I verify. // out until I verify.
// _numAvatarSoundSentBytes = 0; // _numAvatarSoundSentBytes = 0;
setAvatarSound(sound); setAvatarSound(sound);
} }
@ -105,7 +104,7 @@ void Agent::handleOctreePacket(QSharedPointer<ReceivedMessage> message, SharedNo
if (message->getSize() > statsMessageLength) { if (message->getSize() > statsMessageLength) {
// pull out the piggybacked packet and create a new QSharedPointer<NLPacket> for it // pull out the piggybacked packet and create a new QSharedPointer<NLPacket> for it
int piggyBackedSizeWithHeader = message->getSize() - statsMessageLength; int piggyBackedSizeWithHeader = message->getSize() - statsMessageLength;
auto buffer = std::unique_ptr<char[]>(new char[piggyBackedSizeWithHeader]); auto buffer = std::unique_ptr<char[]>(new char[piggyBackedSizeWithHeader]);
memcpy(buffer.get(), message->getRawMessage() + statsMessageLength, piggyBackedSizeWithHeader); memcpy(buffer.get(), message->getRawMessage() + statsMessageLength, piggyBackedSizeWithHeader);
@ -284,7 +283,7 @@ void Agent::selectAudioFormat(const QString& selectedCodecName) {
for (auto& plugin : codecPlugins) { for (auto& plugin : codecPlugins) {
if (_selectedCodecName == plugin->getName()) { if (_selectedCodecName == plugin->getName()) {
_codec = plugin; _codec = plugin;
_receivedAudioStream.setupCodec(plugin, _selectedCodecName, AudioConstants::STEREO); _receivedAudioStream.setupCodec(plugin, _selectedCodecName, AudioConstants::STEREO);
_encoder = plugin->createEncoder(AudioConstants::SAMPLE_RATE, AudioConstants::MONO); _encoder = plugin->createEncoder(AudioConstants::SAMPLE_RATE, AudioConstants::MONO);
qDebug() << "Selected Codec Plugin:" << _codec.get(); qDebug() << "Selected Codec Plugin:" << _codec.get();
break; break;
@ -380,6 +379,8 @@ void Agent::executeScript() {
audioTransform.setTranslation(scriptedAvatar->getPosition()); audioTransform.setTranslation(scriptedAvatar->getPosition());
audioTransform.setRotation(headOrientation); audioTransform.setRotation(headOrientation);
computeLoudness(&audio, scriptedAvatar);
QByteArray encodedBuffer; QByteArray encodedBuffer;
if (_encoder) { if (_encoder) {
_encoder->encode(audio, encodedBuffer); _encoder->encode(audio, encodedBuffer);
@ -424,16 +425,16 @@ void Agent::executeScript() {
entityScriptingInterface->setEntityTree(_entityViewer.getTree()); entityScriptingInterface->setEntityTree(_entityViewer.getTree());
DependencyManager::set<AssignmentParentFinder>(_entityViewer.getTree()); DependencyManager::set<AssignmentParentFinder>(_entityViewer.getTree());
// 100Hz timer for audio // 100Hz timer for audio
AvatarAudioTimer* audioTimerWorker = new AvatarAudioTimer(); AvatarAudioTimer* audioTimerWorker = new AvatarAudioTimer();
audioTimerWorker->moveToThread(&_avatarAudioTimerThread); audioTimerWorker->moveToThread(&_avatarAudioTimerThread);
connect(audioTimerWorker, &AvatarAudioTimer::avatarTick, this, &Agent::processAgentAvatarAudio); connect(audioTimerWorker, &AvatarAudioTimer::avatarTick, this, &Agent::processAgentAvatarAudio);
connect(this, &Agent::startAvatarAudioTimer, audioTimerWorker, &AvatarAudioTimer::start); connect(this, &Agent::startAvatarAudioTimer, audioTimerWorker, &AvatarAudioTimer::start);
connect(this, &Agent::stopAvatarAudioTimer, audioTimerWorker, &AvatarAudioTimer::stop); connect(this, &Agent::stopAvatarAudioTimer, audioTimerWorker, &AvatarAudioTimer::stop);
connect(&_avatarAudioTimerThread, &QThread::finished, audioTimerWorker, &QObject::deleteLater); connect(&_avatarAudioTimerThread, &QThread::finished, audioTimerWorker, &QObject::deleteLater);
_avatarAudioTimerThread.start(); _avatarAudioTimerThread.start();
// Agents should run at 45hz // Agents should run at 45hz
static const int AVATAR_DATA_HZ = 45; static const int AVATAR_DATA_HZ = 45;
static const int AVATAR_DATA_IN_MSECS = MSECS_PER_SECOND / AVATAR_DATA_HZ; static const int AVATAR_DATA_IN_MSECS = MSECS_PER_SECOND / AVATAR_DATA_HZ;
@ -456,14 +457,14 @@ QUuid Agent::getSessionUUID() const {
return DependencyManager::get<NodeList>()->getSessionUUID(); return DependencyManager::get<NodeList>()->getSessionUUID();
} }
void Agent::setIsListeningToAudioStream(bool isListeningToAudioStream) { void Agent::setIsListeningToAudioStream(bool isListeningToAudioStream) {
// this must happen on Agent's main thread // this must happen on Agent's main thread
if (QThread::currentThread() != thread()) { if (QThread::currentThread() != thread()) {
QMetaObject::invokeMethod(this, "setIsListeningToAudioStream", Q_ARG(bool, isListeningToAudioStream)); QMetaObject::invokeMethod(this, "setIsListeningToAudioStream", Q_ARG(bool, isListeningToAudioStream));
return; return;
} }
if (_isListeningToAudioStream) { if (_isListeningToAudioStream) {
// have to tell just the audio mixer to KillAvatar. // have to tell just the audio mixer to KillAvatar.
auto nodeList = DependencyManager::get<NodeList>(); auto nodeList = DependencyManager::get<NodeList>();
nodeList->eachMatchingNode( nodeList->eachMatchingNode(
@ -479,7 +480,7 @@ void Agent::setIsListeningToAudioStream(bool isListeningToAudioStream) {
}); });
} }
_isListeningToAudioStream = isListeningToAudioStream; _isListeningToAudioStream = isListeningToAudioStream;
} }
void Agent::setIsAvatar(bool isAvatar) { void Agent::setIsAvatar(bool isAvatar) {
@ -560,6 +561,7 @@ void Agent::processAgentAvatar() {
nodeList->broadcastToNodes(std::move(avatarPacket), NodeSet() << NodeType::AvatarMixer); nodeList->broadcastToNodes(std::move(avatarPacket), NodeSet() << NodeType::AvatarMixer);
} }
} }
void Agent::encodeFrameOfZeros(QByteArray& encodedZeros) { void Agent::encodeFrameOfZeros(QByteArray& encodedZeros) {
_flushEncoder = false; _flushEncoder = false;
static const QByteArray zeros(AudioConstants::NETWORK_FRAME_BYTES_PER_CHANNEL, 0); static const QByteArray zeros(AudioConstants::NETWORK_FRAME_BYTES_PER_CHANNEL, 0);
@ -570,6 +572,22 @@ void Agent::encodeFrameOfZeros(QByteArray& encodedZeros) {
} }
} }
void Agent::computeLoudness(const QByteArray* decodedBuffer, QSharedPointer<ScriptableAvatar> scriptableAvatar) {
float loudness = 0.0f;
if (decodedBuffer) {
auto soundData = reinterpret_cast<const int16_t*>(decodedBuffer->constData());
int numFrames = decodedBuffer->size() / sizeof(int16_t);
// now iterate and come up with average
if (numFrames > 0) {
for(int i = 0; i < numFrames; i++) {
loudness += (float) std::abs(soundData[i]);
}
loudness /= numFrames;
}
}
scriptableAvatar->setAudioLoudness(loudness);
}
void Agent::processAgentAvatarAudio() { void Agent::processAgentAvatarAudio() {
auto recordingInterface = DependencyManager::get<RecordingScriptingInterface>(); auto recordingInterface = DependencyManager::get<RecordingScriptingInterface>();
bool isPlayingRecording = recordingInterface->isPlaying(); bool isPlayingRecording = recordingInterface->isPlaying();
@ -619,6 +637,7 @@ void Agent::processAgentAvatarAudio() {
audioPacket->seek(sizeof(quint16)); audioPacket->seek(sizeof(quint16));
if (silentFrame) { if (silentFrame) {
if (!_isListeningToAudioStream) { if (!_isListeningToAudioStream) {
// if we have a silent frame and we're not listening then just send nothing and break out of here // if we have a silent frame and we're not listening then just send nothing and break out of here
return; return;
@ -626,7 +645,7 @@ void Agent::processAgentAvatarAudio() {
// write the codec // write the codec
audioPacket->writeString(_selectedCodecName); audioPacket->writeString(_selectedCodecName);
// write the number of silent samples so the audio-mixer can uphold timing // write the number of silent samples so the audio-mixer can uphold timing
audioPacket->writePrimitive(numAvailableSamples); audioPacket->writePrimitive(numAvailableSamples);
@ -636,8 +655,11 @@ void Agent::processAgentAvatarAudio() {
audioPacket->writePrimitive(headOrientation); audioPacket->writePrimitive(headOrientation);
audioPacket->writePrimitive(scriptedAvatar->getPosition()); audioPacket->writePrimitive(scriptedAvatar->getPosition());
audioPacket->writePrimitive(glm::vec3(0)); audioPacket->writePrimitive(glm::vec3(0));
// no matter what, the loudness should be set to 0
computeLoudness(nullptr, scriptedAvatar);
} else if (nextSoundOutput) { } else if (nextSoundOutput) {
// write the codec // write the codec
audioPacket->writeString(_selectedCodecName); audioPacket->writeString(_selectedCodecName);
@ -654,6 +676,8 @@ void Agent::processAgentAvatarAudio() {
QByteArray encodedBuffer; QByteArray encodedBuffer;
if (_flushEncoder) { if (_flushEncoder) {
encodeFrameOfZeros(encodedBuffer); encodeFrameOfZeros(encodedBuffer);
// loudness is 0
computeLoudness(nullptr, scriptedAvatar);
} else { } else {
QByteArray decodedBuffer(reinterpret_cast<const char*>(nextSoundOutput), numAvailableSamples*sizeof(int16_t)); QByteArray decodedBuffer(reinterpret_cast<const char*>(nextSoundOutput), numAvailableSamples*sizeof(int16_t));
if (_encoder) { if (_encoder) {
@ -662,10 +686,15 @@ void Agent::processAgentAvatarAudio() {
} else { } else {
encodedBuffer = decodedBuffer; encodedBuffer = decodedBuffer;
} }
computeLoudness(&decodedBuffer, scriptedAvatar);
} }
audioPacket->write(encodedBuffer.constData(), encodedBuffer.size()); audioPacket->write(encodedBuffer.constData(), encodedBuffer.size());
} }
// we should never have both nextSoundOutput being null and silentFrame being false, but lets
// assert on it in case things above change in a bad way
assert(nextSoundOutput || silentFrame);
// write audio packet to AudioMixer nodes // write audio packet to AudioMixer nodes
auto nodeList = DependencyManager::get<NodeList>(); auto nodeList = DependencyManager::get<NodeList>();
nodeList->eachNode([this, &nodeList, &audioPacket](const SharedNodePointer& node) { nodeList->eachNode([this, &nodeList, &audioPacket](const SharedNodePointer& node) {

View file

@ -30,6 +30,7 @@
#include <plugins/CodecPlugin.h> #include <plugins/CodecPlugin.h>
#include "MixedAudioStream.h" #include "MixedAudioStream.h"
#include "avatars/ScriptableAvatar.h"
class Agent : public ThreadedAssignment { class Agent : public ThreadedAssignment {
Q_OBJECT Q_OBJECT
@ -68,10 +69,10 @@ private slots:
void handleAudioPacket(QSharedPointer<ReceivedMessage> message); void handleAudioPacket(QSharedPointer<ReceivedMessage> message);
void handleOctreePacket(QSharedPointer<ReceivedMessage> message, SharedNodePointer senderNode); void handleOctreePacket(QSharedPointer<ReceivedMessage> message, SharedNodePointer senderNode);
void handleJurisdictionPacket(QSharedPointer<ReceivedMessage> message, SharedNodePointer senderNode); void handleJurisdictionPacket(QSharedPointer<ReceivedMessage> message, SharedNodePointer senderNode);
void handleSelectedAudioFormat(QSharedPointer<ReceivedMessage> message); void handleSelectedAudioFormat(QSharedPointer<ReceivedMessage> message);
void nodeActivated(SharedNodePointer activatedNode); void nodeActivated(SharedNodePointer activatedNode);
void processAgentAvatar(); void processAgentAvatar();
void processAgentAvatarAudio(); void processAgentAvatarAudio();
@ -82,6 +83,7 @@ private:
void negotiateAudioFormat(); void negotiateAudioFormat();
void selectAudioFormat(const QString& selectedCodecName); void selectAudioFormat(const QString& selectedCodecName);
void encodeFrameOfZeros(QByteArray& encodedZeros); void encodeFrameOfZeros(QByteArray& encodedZeros);
void computeLoudness(const QByteArray* decodedBuffer, QSharedPointer<ScriptableAvatar>);
std::unique_ptr<ScriptEngine> _scriptEngine; std::unique_ptr<ScriptEngine> _scriptEngine;
EntityEditPacketSender _entityEditSender; EntityEditPacketSender _entityEditSender;
@ -103,10 +105,10 @@ private:
bool _isAvatar = false; bool _isAvatar = false;
QTimer* _avatarIdentityTimer = nullptr; QTimer* _avatarIdentityTimer = nullptr;
QHash<QUuid, quint16> _outgoingScriptAudioSequenceNumbers; QHash<QUuid, quint16> _outgoingScriptAudioSequenceNumbers;
CodecPluginPointer _codec; CodecPluginPointer _codec;
QString _selectedCodecName; QString _selectedCodecName;
Encoder* _encoder { nullptr }; Encoder* _encoder { nullptr };
QThread _avatarAudioTimerThread; QThread _avatarAudioTimerThread;
bool _flushEncoder { false }; bool _flushEncoder { false };
}; };

BIN
interface/resources/fonts/hifi-glyphs.ttf Normal file → Executable file

Binary file not shown.

View file

@ -31,6 +31,7 @@ Item {
property real displayNameTextPixelSize: 18 property real displayNameTextPixelSize: 18
property int usernameTextHeight: 12 property int usernameTextHeight: 12
property real audioLevel: 0.0 property real audioLevel: 0.0
property real avgAudioLevel: 0.0
property bool isMyCard: false property bool isMyCard: false
property bool selected: false property bool selected: false
property bool isAdmin: false property bool isAdmin: false
@ -55,7 +56,7 @@ Item {
id: textContainer id: textContainer
// Size // Size
width: parent.width - /*avatarImage.width - parent.spacing - */parent.anchors.leftMargin - parent.anchors.rightMargin 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 anchors.verticalCenter: parent.verticalCenter
// DisplayName field for my card // DisplayName field for my card
@ -273,6 +274,7 @@ Item {
// Style // Style
radius: 4 radius: 4
color: "#c5c5c5" color: "#c5c5c5"
visible: isMyCard || selected
// Rectangle for the zero-gain point on the VU meter // Rectangle for the zero-gain point on the VU meter
Rectangle { Rectangle {
id: vuMeterZeroGain id: vuMeterZeroGain
@ -303,6 +305,7 @@ Item {
id: vuMeterBase id: vuMeterBase
// Anchors // Anchors
anchors.fill: parent anchors.fill: parent
visible: isMyCard || selected
// Style // Style
color: parent.color color: parent.color
radius: parent.radius radius: parent.radius
@ -310,6 +313,7 @@ Item {
// Rectangle for the VU meter audio level // Rectangle for the VU meter audio level
Rectangle { Rectangle {
id: vuMeterLevel id: vuMeterLevel
visible: isMyCard || selected
// Size // Size
width: (thisNameCard.audioLevel) * parent.width width: (thisNameCard.audioLevel) * parent.width
// Style // Style
@ -335,7 +339,7 @@ Item {
} }
} }
// Per-Avatar Gain Slider // Per-Avatar Gain Slider
Slider { Slider {
id: gainSlider id: gainSlider
// Size // Size
@ -345,7 +349,7 @@ Item {
anchors.verticalCenter: nameCardVUMeter.verticalCenter anchors.verticalCenter: nameCardVUMeter.verticalCenter
// Properties // Properties
visible: !isMyCard && selected visible: !isMyCard && selected
value: pal.gainSliderValueDB[uuid] ? pal.gainSliderValueDB[uuid] : 0.0 value: Users.getAvatarGain(uuid)
minimumValue: -60.0 minimumValue: -60.0
maximumValue: 20.0 maximumValue: 20.0
stepSize: 5 stepSize: 5
@ -369,7 +373,7 @@ Item {
mouse.accepted = false mouse.accepted = false
} }
onReleased: { onReleased: {
// the above mouse.accepted seems to make this // the above mouse.accepted seems to make this
// never get called, nonetheless... // never get called, nonetheless...
mouse.accepted = false mouse.accepted = false
} }
@ -393,14 +397,9 @@ Item {
} }
function updateGainFromQML(avatarUuid, sliderValue, isReleased) { function updateGainFromQML(avatarUuid, sliderValue, isReleased) {
if (isReleased || pal.gainSliderValueDB[avatarUuid] !== sliderValue) { Users.setAvatarGain(avatarUuid, sliderValue);
pal.gainSliderValueDB[avatarUuid] = sliderValue; if (isReleased) {
var data = { UserActivityLogger.palAction("avatar_gain_changed", avatarUuid);
sessionId: avatarUuid,
gain: sliderValue,
isReleased: isReleased
};
pal.sendToScript({method: 'updateGain', params: data});
} }
} }
} }

View file

@ -2,7 +2,7 @@
// Pal.qml // Pal.qml
// qml/hifi // qml/hifi
// //
// People Action List // People Action List
// //
// Created by Howard Stearns on 12/12/2016 // Created by Howard Stearns on 12/12/2016
// Copyright 2016 High Fidelity, Inc. // Copyright 2016 High Fidelity, Inc.
@ -13,6 +13,7 @@
import QtQuick 2.5 import QtQuick 2.5
import QtQuick.Controls 1.4 import QtQuick.Controls 1.4
import QtGraphicalEffects 1.0
import Qt.labs.settings 1.0 import Qt.labs.settings 1.0
import "../styles-uit" import "../styles-uit"
import "../controls-uit" as HifiControls import "../controls-uit" as HifiControls
@ -33,13 +34,10 @@ Rectangle {
property int actionButtonAllowance: actionButtonWidth * 2 property int actionButtonAllowance: actionButtonWidth * 2
property int minNameCardWidth: palContainer.width - (actionButtonAllowance * 2) - 4 - hifi.dimensions.scrollbarBackgroundWidth property int minNameCardWidth: palContainer.width - (actionButtonAllowance * 2) - 4 - hifi.dimensions.scrollbarBackgroundWidth
property int nameCardWidth: minNameCardWidth + (iAmAdmin ? 0 : actionButtonAllowance) 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 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 var userModelData: [] // This simple list is essentially a mirror of the userModel listModel without all the extra complexities.
property bool iAmAdmin: false property bool iAmAdmin: false
// Keep a local list of per-avatar gainSliderValueDBs. Far faster than fetching this data from the server.
// NOTE: if another script modifies the per-avatar gain, this value won't be accurate!
property var gainSliderValueDB: ({});
HifiConstants { id: hifi } HifiConstants { id: hifi }
@ -60,6 +58,8 @@ Rectangle {
category: "pal" category: "pal"
property bool filtered: false property bool filtered: false
property int nearDistance: 30 property int nearDistance: 30
property int sortIndicatorColumn: 1
property int sortIndicatorOrder: Qt.AscendingOrder
} }
function refreshWithFilter() { function refreshWithFilter() {
// We should just be able to set settings.filtered to filter.checked, but see #3249, so send to .js for saving. // We should just be able to set settings.filtered to filter.checked, but see #3249, so send to .js for saving.
@ -99,6 +99,7 @@ Rectangle {
displayName: myData.displayName displayName: myData.displayName
userName: myData.userName userName: myData.userName
audioLevel: myData.audioLevel audioLevel: myData.audioLevel
avgAudioLevel: myData.avgAudioLevel
isMyCard: true isMyCard: true
// Size // Size
width: minNameCardWidth width: minNameCardWidth
@ -193,8 +194,24 @@ Rectangle {
centerHeaderText: true centerHeaderText: true
sortIndicatorVisible: true sortIndicatorVisible: true
headerVisible: true headerVisible: true
onSortIndicatorColumnChanged: sortModel() sortIndicatorColumn: settings.sortIndicatorColumn
onSortIndicatorOrderChanged: sortModel() 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 { TableViewColumn {
id: displayNameHeader id: displayNameHeader
@ -204,13 +221,6 @@ Rectangle {
movable: false movable: false
resizable: false resizable: false
} }
TableViewColumn {
role: "personalMute"
title: "MUTE"
width: actionButtonWidth
movable: false
resizable: false
}
TableViewColumn { TableViewColumn {
role: "ignore" role: "ignore"
title: "IGNORE" title: "IGNORE"
@ -241,7 +251,7 @@ Rectangle {
// This Rectangle refers to each Row in the table. // This Rectangle refers to each Row in the table.
rowDelegate: Rectangle { // The only way I know to specify a row height. rowDelegate: Rectangle { // The only way I know to specify a row height.
// Size // Size
height: rowHeight height: styleData.selected ? rowHeight : rowHeight - 15
color: styleData.selected color: styleData.selected
? hifi.colors.orangeHighlight ? hifi.colors.orangeHighlight
: styleData.alternate ? hifi.colors.tableRowLightEven : hifi.colors.tableRowLightOdd : styleData.alternate ? hifi.colors.tableRowLightEven : hifi.colors.tableRowLightOdd
@ -252,6 +262,8 @@ Rectangle {
id: itemCell id: itemCell
property bool isCheckBox: styleData.role === "personalMute" || styleData.role === "ignore" property bool isCheckBox: styleData.role === "personalMute" || styleData.role === "ignore"
property bool isButton: styleData.role === "mute" || styleData.role === "kick" 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 // This NameCard refers to the cell that contains an avatar's
// DisplayName and UserName // DisplayName and UserName
NameCard { NameCard {
@ -260,7 +272,8 @@ Rectangle {
displayName: styleData.value displayName: styleData.value
userName: model ? model.userName : "" userName: model ? model.userName : ""
audioLevel: model ? model.audioLevel : 0.0 audioLevel: model ? model.audioLevel : 0.0
visible: !isCheckBox && !isButton avgAudioLevel: model ? model.avgAudioLevel : 0.0
visible: !isCheckBox && !isButton && !isAvgAudio
uuid: model ? model.sessionId : "" uuid: model ? model.sessionId : ""
selected: styleData.selected selected: styleData.selected
isAdmin: model && model.admin isAdmin: model && model.admin
@ -270,7 +283,34 @@ Rectangle {
// Anchors // 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];
}
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) // 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 // 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. // will appear in the "hovered" state. Hovering over the checkbox will fix it.
@ -299,6 +339,7 @@ Rectangle {
} else { } else {
delete ignored[model.sessionId] delete ignored[model.sessionId]
} }
avgAudioVolume.glyph = avgAudioVolume.getGlyph()
} }
// http://doc.qt.io/qt-5/qtqml-syntax-propertybinding.html#creating-property-bindings-from-javascript // 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 // I'm using an explicit binding here because clicking a checkbox breaks the implicit binding as set by
@ -306,7 +347,7 @@ Rectangle {
checked = Qt.binding(function() { return (model[styleData.role])}) checked = Qt.binding(function() { return (model[styleData.role])})
} }
} }
// This Button belongs in the columns that contain the stateless action buttons ("Silence" & "Ban" for now) // This Button belongs in the columns that contain the stateless action buttons ("Silence" & "Ban" for now)
HifiControls.Button { HifiControls.Button {
id: actionButton id: actionButton
@ -314,7 +355,7 @@ Rectangle {
visible: isButton visible: isButton
anchors.centerIn: parent anchors.centerIn: parent
width: 32 width: 32
height: 24 height: 32
onClicked: { onClicked: {
Users[styleData.role](model.sessionId) Users[styleData.role](model.sessionId)
UserActivityLogger["palAction"](styleData.role, model.sessionId) UserActivityLogger["palAction"](styleData.role, model.sessionId)
@ -366,7 +407,7 @@ Rectangle {
anchors.left: table.left anchors.left: table.left
anchors.top: table.top anchors.top: table.top
anchors.topMargin: 1 anchors.topMargin: 1
anchors.leftMargin: nameCardWidth/2 + displayNameHeaderMetrics.width/2 + 6 anchors.leftMargin: actionButtonWidth + nameCardWidth/2 + displayNameHeaderMetrics.width/2 + 6
RalewayRegular { RalewayRegular {
id: helpText id: helpText
text: "[?]" text: "[?]"
@ -538,25 +579,29 @@ Rectangle {
} }
} }
break; break;
case 'updateAudioLevel': case 'updateAudioLevel':
for (var userId in message.params) { 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 the userId is 0, we're updating "myData".
if (userId == 0) { if (userId == 0) {
myData.audioLevel = audioLevel; myData.audioLevel = audioLevel;
myCard.audioLevel = audioLevel; // Defensive programming myCard.audioLevel = audioLevel; // Defensive programming
myData.avgAudioLevel = avgAudioLevel;
myCard.avgAudioLevel = avgAudioLevel;
} else { } else {
var userIndex = findSessionIndex(userId); var userIndex = findSessionIndex(userId);
if (userIndex != -1) { if (userIndex != -1) {
userModel.setProperty(userIndex, "audioLevel", audioLevel); userModel.setProperty(userIndex, "audioLevel", audioLevel);
userModelData[userIndex].audioLevel = audioLevel; // Defensive programming userModelData[userIndex].audioLevel = audioLevel; // Defensive programming
userModel.setProperty(userIndex, "avgAudioLevel", avgAudioLevel);
userModelData[userIndex].avgAudioLevel = avgAudioLevel;
} }
} }
} }
break; break;
case 'clearLocalQMLData': case 'clearLocalQMLData':
ignored = {}; ignored = {};
gainSliderValueDB = {};
break; break;
case 'avatarDisconnected': case 'avatarDisconnected':
var sessionID = message.params[0]; var sessionID = message.params[0];

View file

@ -318,5 +318,15 @@ Item {
readonly property string deg: "\\" readonly property string deg: "\\"
readonly property string px: "|" readonly property string px: "|"
readonly property string editPencil: "\ue00d" 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"
} }
} }

View file

@ -549,6 +549,7 @@ const float DEFAULT_DESKTOP_TABLET_SCALE_PERCENT = 75.0f;
const bool DEFAULT_DESKTOP_TABLET_BECOMES_TOOLBAR = true; const bool DEFAULT_DESKTOP_TABLET_BECOMES_TOOLBAR = true;
const bool DEFAULT_HMD_TABLET_BECOMES_TOOLBAR = false; const bool DEFAULT_HMD_TABLET_BECOMES_TOOLBAR = false;
const bool DEFAULT_TABLET_VISIBLE_TO_OTHERS = false; const bool DEFAULT_TABLET_VISIBLE_TO_OTHERS = false;
const bool DEFAULT_PREFER_AVATAR_FINGER_OVER_STYLUS = false;
Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bool runServer, QString runServerPathOption) : Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bool runServer, QString runServerPathOption) :
QApplication(argc, argv), QApplication(argc, argv),
@ -572,6 +573,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo
_desktopTabletBecomesToolbarSetting("desktopTabletBecomesToolbar", DEFAULT_DESKTOP_TABLET_BECOMES_TOOLBAR), _desktopTabletBecomesToolbarSetting("desktopTabletBecomesToolbar", DEFAULT_DESKTOP_TABLET_BECOMES_TOOLBAR),
_hmdTabletBecomesToolbarSetting("hmdTabletBecomesToolbar", DEFAULT_HMD_TABLET_BECOMES_TOOLBAR), _hmdTabletBecomesToolbarSetting("hmdTabletBecomesToolbar", DEFAULT_HMD_TABLET_BECOMES_TOOLBAR),
_tabletVisibleToOthersSetting("tabletVisibleToOthers", DEFAULT_TABLET_VISIBLE_TO_OTHERS), _tabletVisibleToOthersSetting("tabletVisibleToOthers", DEFAULT_TABLET_VISIBLE_TO_OTHERS),
_preferAvatarFingerOverStylusSetting("preferAvatarFingerOverStylus", DEFAULT_PREFER_AVATAR_FINGER_OVER_STYLUS),
_constrainToolbarPosition("toolbar/constrainToolbarToCenterX", true), _constrainToolbarPosition("toolbar/constrainToolbarToCenterX", true),
_scaleMirror(1.0f), _scaleMirror(1.0f),
_rotateMirror(0.0f), _rotateMirror(0.0f),
@ -2362,6 +2364,10 @@ void Application::setTabletVisibleToOthersSetting(bool value) {
updateSystemTabletMode(); updateSystemTabletMode();
} }
void Application::setPreferAvatarFingerOverStylus(bool value) {
_preferAvatarFingerOverStylusSetting.set(value);
}
void Application::setSettingConstrainToolbarPosition(bool setting) { void Application::setSettingConstrainToolbarPosition(bool setting) {
_constrainToolbarPosition.set(setting); _constrainToolbarPosition.set(setting);
DependencyManager::get<OffscreenUi>()->setConstrainToolbarToCenterX(setting); DependencyManager::get<OffscreenUi>()->setConstrainToolbarToCenterX(setting);
@ -2918,10 +2924,12 @@ void Application::keyPressEvent(QKeyEvent* event) {
} }
break; break;
case Qt::Key_P: { case Qt::Key_P: {
bool isFirstPersonChecked = Menu::getInstance()->isOptionChecked(MenuOption::FirstPerson); if (!(isShifted || isMeta || isOption)) {
Menu::getInstance()->setIsOptionChecked(MenuOption::FirstPerson, !isFirstPersonChecked); bool isFirstPersonChecked = Menu::getInstance()->isOptionChecked(MenuOption::FirstPerson);
Menu::getInstance()->setIsOptionChecked(MenuOption::ThirdPerson, isFirstPersonChecked); Menu::getInstance()->setIsOptionChecked(MenuOption::FirstPerson, !isFirstPersonChecked);
cameraMenuChanged(); Menu::getInstance()->setIsOptionChecked(MenuOption::ThirdPerson, isFirstPersonChecked);
cameraMenuChanged();
}
break; break;
} }

View file

@ -220,6 +220,8 @@ public:
void setHmdTabletBecomesToolbarSetting(bool value); void setHmdTabletBecomesToolbarSetting(bool value);
bool getTabletVisibleToOthersSetting() { return _tabletVisibleToOthersSetting.get(); } bool getTabletVisibleToOthersSetting() { return _tabletVisibleToOthersSetting.get(); }
void setTabletVisibleToOthersSetting(bool value); void setTabletVisibleToOthersSetting(bool value);
bool getPreferAvatarFingerOverStylus() { return _preferAvatarFingerOverStylusSetting.get(); }
void setPreferAvatarFingerOverStylus(bool value);
float getSettingConstrainToolbarPosition() { return _constrainToolbarPosition.get(); } float getSettingConstrainToolbarPosition() { return _constrainToolbarPosition.get(); }
void setSettingConstrainToolbarPosition(bool setting); void setSettingConstrainToolbarPosition(bool setting);
@ -565,6 +567,7 @@ private:
Setting::Handle<bool> _desktopTabletBecomesToolbarSetting; Setting::Handle<bool> _desktopTabletBecomesToolbarSetting;
Setting::Handle<bool> _hmdTabletBecomesToolbarSetting; Setting::Handle<bool> _hmdTabletBecomesToolbarSetting;
Setting::Handle<bool> _tabletVisibleToOthersSetting; Setting::Handle<bool> _tabletVisibleToOthersSetting;
Setting::Handle<bool> _preferAvatarFingerOverStylusSetting;
Setting::Handle<bool> _constrainToolbarPosition; Setting::Handle<bool> _constrainToolbarPosition;
float _scaleMirror; float _scaleMirror;

View file

@ -577,7 +577,7 @@ Menu::Menu() {
nodeList.data(), SLOT(toggleSendNewerDSConnectVersion(bool))); nodeList.data(), SLOT(toggleSendNewerDSConnectVersion(bool)));
#endif #endif
// Developer >> Tests >>> // Developer >> Tests >>>
MenuWrapper* testMenu = developerMenu->addMenu("Tests"); MenuWrapper* testMenu = developerMenu->addMenu("Tests");
addActionToQMenuAndActionHash(testMenu, MenuOption::RunClientScriptTests, 0, dialogsManager.data(), SLOT(showTestingResults())); addActionToQMenuAndActionHash(testMenu, MenuOption::RunClientScriptTests, 0, dialogsManager.data(), SLOT(showTestingResults()));
@ -628,9 +628,9 @@ Menu::Menu() {
auto scope = DependencyManager::get<AudioScope>(); auto scope = DependencyManager::get<AudioScope>();
MenuWrapper* audioScopeMenu = audioDebugMenu->addMenu("Audio Scope"); 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())); 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())); scope.data(), SLOT(togglePause()));
addDisabledActionAndSeparator(audioScopeMenu, "Display Frames"); addDisabledActionAndSeparator(audioScopeMenu, "Display Frames");

View file

@ -348,6 +348,8 @@ void Avatar::simulate(float deltaTime, bool inView) {
PROFILE_RANGE(simulation, "updateJoints"); PROFILE_RANGE(simulation, "updateJoints");
if (inView && _hasNewJointData) { if (inView && _hasNewJointData) {
_skeletonModel->getRig()->copyJointsFromJointData(_jointData); _skeletonModel->getRig()->copyJointsFromJointData(_jointData);
glm::mat4 rootTransform = glm::scale(_skeletonModel->getScale()) * glm::translate(_skeletonModel->getOffset());
_skeletonModel->getRig()->computeExternalPoses(rootTransform);
_jointDataSimulationRate.increment(); _jointDataSimulationRate.increment();
_skeletonModel->simulate(deltaTime, true); _skeletonModel->simulate(deltaTime, true);

View file

@ -107,6 +107,12 @@ void setupPreferences() {
auto setter = [](bool value) { qApp->setTabletVisibleToOthersSetting(value); }; auto setter = [](bool value) { qApp->setTabletVisibleToOthersSetting(value); };
preferences->addPreference(new CheckPreference(UI_CATEGORY, "Tablet Is Visible To Others", getter, setter)); preferences->addPreference(new CheckPreference(UI_CATEGORY, "Tablet Is Visible To Others", getter, setter));
} }
{
auto getter = []()->bool { return qApp->getPreferAvatarFingerOverStylus(); };
auto setter = [](bool value) { qApp->setPreferAvatarFingerOverStylus(value); };
preferences->addPreference(new CheckPreference(UI_CATEGORY, "Prefer Avatar Finger Over Stylus", getter, setter));
}
// Snapshots // Snapshots
static const QString SNAPSHOTS { "Snapshots" }; static const QString SNAPSHOTS { "Snapshots" };
{ {

View file

@ -23,10 +23,15 @@ Line3DOverlay::Line3DOverlay() :
Line3DOverlay::Line3DOverlay(const Line3DOverlay* line3DOverlay) : Line3DOverlay::Line3DOverlay(const Line3DOverlay* line3DOverlay) :
Base3DOverlay(line3DOverlay), Base3DOverlay(line3DOverlay),
_start(line3DOverlay->_start),
_end(line3DOverlay->_end),
_geometryCacheID(DependencyManager::get<GeometryCache>()->allocateID()) _geometryCacheID(DependencyManager::get<GeometryCache>()->allocateID())
{ {
setParentID(line3DOverlay->getParentID());
setParentJointIndex(line3DOverlay->getParentJointIndex());
setLocalTransform(line3DOverlay->getLocalTransform());
_direction = line3DOverlay->getDirection();
_length = line3DOverlay->getLength();
_endParentID = line3DOverlay->getEndParentID();
_endParentJointIndex = line3DOverlay->getEndJointIndex();
} }
Line3DOverlay::~Line3DOverlay() { Line3DOverlay::~Line3DOverlay() {
@ -37,17 +42,23 @@ Line3DOverlay::~Line3DOverlay() {
} }
glm::vec3 Line3DOverlay::getStart() const { glm::vec3 Line3DOverlay::getStart() const {
bool success; return getPosition();
glm::vec3 worldStart = localToWorld(_start, getParentID(), getParentJointIndex(), success);
if (!success) {
qDebug() << "Line3DOverlay::getStart failed";
}
return worldStart;
} }
glm::vec3 Line3DOverlay::getEnd() const { glm::vec3 Line3DOverlay::getEnd() const {
bool success; 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) { if (!success) {
qDebug() << "Line3DOverlay::getEnd failed"; qDebug() << "Line3DOverlay::getEnd failed";
} }
@ -55,27 +66,55 @@ glm::vec3 Line3DOverlay::getEnd() const {
} }
void Line3DOverlay::setStart(const glm::vec3& start) { void Line3DOverlay::setStart(const glm::vec3& start) {
bool success; setPosition(start);
_start = worldToLocal(start, getParentID(), getParentJointIndex(), success);
if (!success) {
qDebug() << "Line3DOverlay::setStart failed";
}
} }
void Line3DOverlay::setEnd(const glm::vec3& end) { void Line3DOverlay::setEnd(const glm::vec3& end) {
bool success; 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) { if (!success) {
qDebug() << "Line3DOverlay::setEnd failed"; 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 { AABox Line3DOverlay::getBounds() const {
auto extents = Extents{}; auto extents = Extents{};
extents.addPoint(_start); extents.addPoint(getStart());
extents.addPoint(_end); extents.addPoint(getEnd());
extents.transform(getTransform());
return AABox(extents); 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); glm::vec4 colorv4(color.red / MAX_COLOR, color.green / MAX_COLOR, color.blue / MAX_COLOR, alpha);
auto batch = args->_batch; auto batch = args->_batch;
if (batch) { if (batch) {
batch->setModelTransform(getTransform()); batch->setModelTransform(Transform());
glm::vec3 start = getStart();
glm::vec3 end = getEnd();
auto geometryCache = DependencyManager::get<GeometryCache>(); auto geometryCache = DependencyManager::get<GeometryCache>();
if (getIsDashedLine()) { if (getIsDashedLine()) {
// TODO: add support for color to renderDashedLine() // TODO: add support for color to renderDashedLine()
geometryCache->bindSimpleProgram(*batch, false, false, false, true, true); 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) { } else if (_glow > 0.0f) {
geometryCache->renderGlowLine(*batch, _start, _end, colorv4, _glow, _glowWidth, _geometryCacheID); geometryCache->renderGlowLine(*batch, start, end, colorv4, _glow, _glowWidth, _geometryCacheID);
} else { } else {
geometryCache->bindSimpleProgram(*batch, false, false, false, true, true); 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) { void Line3DOverlay::setProperties(const QVariantMap& originalProperties) {
QVariantMap properties = originalProperties; QVariantMap properties = originalProperties;
glm::vec3 newStart(0.0f);
bool newStartSet { false };
glm::vec3 newEnd(0.0f);
bool newEndSet { false };
auto start = properties["start"]; auto start = properties["start"];
// if "start" property was not there, check to see if they included aliases: startPoint // 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"]; start = properties["startPoint"];
} }
if (start.isValid()) { if (start.isValid()) {
setStart(vec3FromVariant(start)); newStart = vec3FromVariant(start);
newStartSet = true;
} }
properties.remove("start"); // so that Base3DOverlay doesn't respond to it 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"]; auto end = properties["end"];
// if "end" property was not there, check to see if they included aliases: endPoint // if "end" property was not there, check to see if they included aliases: endPoint
if (!end.isValid()) { if (!end.isValid()) {
end = properties["endPoint"]; end = properties["endPoint"];
} }
if (end.isValid()) { 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"]; auto localEnd = properties["localEnd"];
if (localEnd.isValid()) { 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"]; auto glow = properties["glow"];
if (glow.isValid()) { if (glow.isValid()) {
@ -161,7 +233,6 @@ void Line3DOverlay::setProperties(const QVariantMap& originalProperties) {
setGlow(glowWidth.toFloat()); setGlow(glowWidth.toFloat());
} }
Base3DOverlay::setProperties(properties);
} }
QVariant Line3DOverlay::getProperty(const QString& property) { QVariant Line3DOverlay::getProperty(const QString& property) {
@ -171,6 +242,15 @@ QVariant Line3DOverlay::getProperty(const QString& property) {
if (property == "end" || property == "endPoint" || property == "p2") { if (property == "end" || property == "endPoint" || property == "p2") {
return vec3toVariant(getEnd()); 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); return Base3DOverlay::getProperty(property);
} }

View file

@ -15,7 +15,7 @@
class Line3DOverlay : public Base3DOverlay { class Line3DOverlay : public Base3DOverlay {
Q_OBJECT Q_OBJECT
public: public:
static QString const TYPE; static QString const TYPE;
virtual QString getType() const override { return TYPE; } virtual QString getType() const override { return TYPE; }
@ -37,6 +37,9 @@ public:
void setStart(const glm::vec3& start); void setStart(const glm::vec3& start);
void setEnd(const glm::vec3& end); 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 setGlow(const float& glow) { _glow = glow; }
void setGlowWidth(const float& glowWidth) { _glowWidth = glowWidth; } void setGlowWidth(const float& glowWidth) { _glowWidth = glowWidth; }
@ -47,13 +50,26 @@ public:
virtual void locationChanged(bool tellPhysics = true) override; virtual void locationChanged(bool tellPhysics = true) override;
protected: glm::vec3 getDirection() const { return _direction; }
glm::vec3 _start; float getLength() const { return _length; }
glm::vec3 _end; 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 _glow { 0.0 };
float _glowWidth { 0.0 }; float _glowWidth { 0.0 };
int _geometryCacheID; int _geometryCacheID;
}; };
#endif // hifi_Line3DOverlay_h #endif // hifi_Line3DOverlay_h

View file

@ -198,18 +198,27 @@ void Web3DOverlay::render(RenderArgs* args) {
_webSurface->getRootItem()->setProperty("scriptURL", _scriptURL); _webSurface->getRootItem()->setProperty("scriptURL", _scriptURL);
currentContext->makeCurrent(currentSurface); currentContext->makeCurrent(currentSurface);
auto selfOverlayID = getOverlayID();
std::weak_ptr<Web3DOverlay> weakSelf = std::dynamic_pointer_cast<Web3DOverlay>(qApp->getOverlays().getOverlay(selfOverlayID));
auto forwardPointerEvent = [=](OverlayID overlayID, const PointerEvent& event) { auto forwardPointerEvent = [=](OverlayID overlayID, const PointerEvent& event) {
if (overlayID == getOverlayID()) { auto self = weakSelf.lock();
handlePointerEvent(event); if (!self) {
return;
}
if (overlayID == selfOverlayID) {
self->handlePointerEvent(event);
} }
}; };
_mousePressConnection = connect(&(qApp->getOverlays()), &Overlays::mousePressOnOverlay, forwardPointerEvent); _mousePressConnection = connect(&(qApp->getOverlays()), &Overlays::mousePressOnOverlay, this, forwardPointerEvent, Qt::DirectConnection);
_mouseReleaseConnection = connect(&(qApp->getOverlays()), &Overlays::mouseReleaseOnOverlay, forwardPointerEvent); _mouseReleaseConnection = connect(&(qApp->getOverlays()), &Overlays::mouseReleaseOnOverlay, this, forwardPointerEvent, Qt::DirectConnection);
_mouseMoveConnection = connect(&(qApp->getOverlays()), &Overlays::mouseMoveOnOverlay, forwardPointerEvent); _mouseMoveConnection = connect(&(qApp->getOverlays()), &Overlays::mouseMoveOnOverlay, this, forwardPointerEvent, Qt::DirectConnection);
_hoverLeaveConnection = connect(&(qApp->getOverlays()), &Overlays::hoverLeaveOverlay, _hoverLeaveConnection = connect(&(qApp->getOverlays()), &Overlays::hoverLeaveOverlay, this, [=](OverlayID overlayID, const PointerEvent& event) {
[=](OverlayID overlayID, const PointerEvent& event) { auto self = weakSelf.lock();
if (this->_pressed && this->getOverlayID() == overlayID) { if (!self) {
return;
}
if (self->_pressed && overlayID == selfOverlayID) {
// If the user mouses off the overlay while the button is down, simulate a touch end. // If the user mouses off the overlay while the button is down, simulate a touch end.
QTouchEvent::TouchPoint point; QTouchEvent::TouchPoint point;
point.setId(event.getID()); point.setId(event.getID());
@ -222,12 +231,12 @@ void Web3DOverlay::render(RenderArgs* args) {
touchPoints.push_back(point); touchPoints.push_back(point);
QTouchEvent* touchEvent = new QTouchEvent(QEvent::TouchEnd, nullptr, Qt::NoModifier, Qt::TouchPointReleased, QTouchEvent* touchEvent = new QTouchEvent(QEvent::TouchEnd, nullptr, Qt::NoModifier, Qt::TouchPointReleased,
touchPoints); touchPoints);
touchEvent->setWindow(_webSurface->getWindow()); touchEvent->setWindow(self->_webSurface->getWindow());
touchEvent->setDevice(&_touchDevice); touchEvent->setDevice(&_touchDevice);
touchEvent->setTarget(_webSurface->getRootItem()); touchEvent->setTarget(self->_webSurface->getRootItem());
QCoreApplication::postEvent(_webSurface->getWindow(), touchEvent); QCoreApplication::postEvent(self->_webSurface->getWindow(), touchEvent);
} }
}); }, Qt::DirectConnection);
_emitScriptEventConnection = connect(this, &Web3DOverlay::scriptEventReceived, _webSurface.data(), &OffscreenQmlSurface::emitScriptEvent); _emitScriptEventConnection = connect(this, &Web3DOverlay::scriptEventReceived, _webSurface.data(), &OffscreenQmlSurface::emitScriptEvent);
_webEventReceivedConnection = connect(_webSurface.data(), &OffscreenQmlSurface::webEventReceived, this, &Web3DOverlay::webEventReceived); _webEventReceivedConnection = connect(_webSurface.data(), &OffscreenQmlSurface::webEventReceived, this, &Web3DOverlay::webEventReceived);

View file

@ -1346,8 +1346,13 @@ void Rig::copyJointsFromJointData(const QVector<JointData>& jointDataVec) {
_internalPoseSet._relativePoses[i].trans() = relativeDefaultPoses[i].trans(); _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); buildAbsoluteRigPoses(_internalPoseSet._relativePoses, _internalPoseSet._absolutePoses);
QWriteLocker writeLock(&_externalPoseSetLock); QWriteLocker writeLock(&_externalPoseSetLock);
_externalPoseSet = _internalPoseSet; _externalPoseSet = _internalPoseSet;

View file

@ -210,6 +210,7 @@ public:
void copyJointsIntoJointData(QVector<JointData>& jointDataVec) const; void copyJointsIntoJointData(QVector<JointData>& jointDataVec) const;
void copyJointsFromJointData(const QVector<JointData>& jointDataVec); void copyJointsFromJointData(const QVector<JointData>& jointDataVec);
void computeExternalPoses(const glm::mat4& modelOffsetMat);
void computeAvatarBoundingCapsule(const FBXGeometry& geometry, float& radiusOut, float& heightOut, glm::vec3& offsetOut) const; void computeAvatarBoundingCapsule(const FBXGeometry& geometry, float& radiusOut, float& heightOut, glm::vec3& offsetOut) const;

View file

@ -37,6 +37,8 @@ static uint64_t MAX_NO_RENDER_INTERVAL = 30 * USECS_PER_SECOND;
static int MAX_WINDOW_SIZE = 4096; static int MAX_WINDOW_SIZE = 4096;
static float OPAQUE_ALPHA_THRESHOLD = 0.99f; 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 RenderableWebEntityItem::factory(const EntityItemID& entityID, const EntityItemProperties& properties) {
EntityItemPointer entity{ new RenderableWebEntityItem(entityID) }; EntityItemPointer entity{ new RenderableWebEntityItem(entityID) };
@ -113,7 +115,7 @@ bool RenderableWebEntityItem::buildWebSurface(QSharedPointer<EntityTreeRenderer>
// FIXME, the max FPS could be better managed by being dynamic (based on the number of current surfaces // FIXME, the max FPS could be better managed by being dynamic (based on the number of current surfaces
// and the current rendering load) // 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 // The lifetime of the QML surface MUST be managed by the main thread
// Additionally, we MUST use local variables copied by value, rather than // 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")) { _sourceUrl.toLower().endsWith(".htm") || _sourceUrl.toLower().endsWith(".html")) {
_contentType = htmlContent; _contentType = htmlContent;
_webSurface->setBaseUrl(QUrl::fromLocalFile(PathUtils::resourcesPath() + "qml/controls/")); _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) { _webSurface->load("WebView.qml", [&](QQmlContext* context, QObject* obj) {
context->setContextProperty("eventBridgeJavaScriptToInject", QVariant(_javaScriptToInject)); context->setContextProperty("eventBridgeJavaScriptToInject", QVariant(_javaScriptToInject));
}); });
_webSurface->getRootItem()->setProperty("url", _sourceUrl); _webSurface->getRootItem()->setProperty("url", _sourceUrl);
_webSurface->getRootContext()->setContextProperty("desktop", QVariant()); _webSurface->getRootContext()->setContextProperty("desktop", QVariant());

View file

@ -49,7 +49,7 @@ NodeList::NodeList(char newOwnerType, int socketListenPort, int dtlsListenPort)
setCustomDeleter([](Dependency* dependency){ setCustomDeleter([](Dependency* dependency){
static_cast<NodeList*>(dependency)->deleteLater(); static_cast<NodeList*>(dependency)->deleteLater();
}); });
auto addressManager = DependencyManager::get<AddressManager>(); auto addressManager = DependencyManager::get<AddressManager>();
// handle domain change signals from AddressManager // handle domain change signals from AddressManager
@ -85,8 +85,8 @@ NodeList::NodeList(char newOwnerType, int socketListenPort, int dtlsListenPort)
connect(&_domainHandler, &DomainHandler::icePeerSocketsReceived, this, &NodeList::pingPunchForDomainServer); connect(&_domainHandler, &DomainHandler::icePeerSocketsReceived, this, &NodeList::pingPunchForDomainServer);
auto accountManager = DependencyManager::get<AccountManager>(); auto accountManager = DependencyManager::get<AccountManager>();
// assume that we may need to send a new DS check in anytime a new keypair is generated // assume that we may need to send a new DS check in anytime a new keypair is generated
connect(accountManager.data(), &AccountManager::newKeypair, this, &NodeList::sendDomainServerCheckIn); connect(accountManager.data(), &AccountManager::newKeypair, this, &NodeList::sendDomainServerCheckIn);
// clear out NodeList when login is finished // clear out NodeList when login is finished
@ -101,7 +101,7 @@ NodeList::NodeList(char newOwnerType, int socketListenPort, int dtlsListenPort)
// anytime we get a new node we may need to re-send our set of ignored node IDs to it // anytime we get a new node we may need to re-send our set of ignored node IDs to it
connect(this, &LimitedNodeList::nodeActivated, this, &NodeList::maybeSendIgnoreSetToNode); connect(this, &LimitedNodeList::nodeActivated, this, &NodeList::maybeSendIgnoreSetToNode);
// setup our timer to send keepalive pings (it's started and stopped on domain connect/disconnect) // setup our timer to send keepalive pings (it's started and stopped on domain connect/disconnect)
_keepAlivePingTimer.setInterval(KEEPALIVE_PING_INTERVAL_MS); // 1s, Qt::CoarseTimer acceptable _keepAlivePingTimer.setInterval(KEEPALIVE_PING_INTERVAL_MS); // 1s, Qt::CoarseTimer acceptable
connect(&_keepAlivePingTimer, &QTimer::timeout, this, &NodeList::sendKeepAlivePings); connect(&_keepAlivePingTimer, &QTimer::timeout, this, &NodeList::sendKeepAlivePings);
@ -161,11 +161,11 @@ qint64 NodeList::sendStatsToDomainServer(QJsonObject statsObject) {
void NodeList::timePingReply(ReceivedMessage& message, const SharedNodePointer& sendingNode) { void NodeList::timePingReply(ReceivedMessage& message, const SharedNodePointer& sendingNode) {
PingType_t pingType; PingType_t pingType;
quint64 ourOriginalTime, othersReplyTime; quint64 ourOriginalTime, othersReplyTime;
message.seek(0); message.seek(0);
message.readPrimitive(&pingType); message.readPrimitive(&pingType);
message.readPrimitive(&ourOriginalTime); message.readPrimitive(&ourOriginalTime);
message.readPrimitive(&othersReplyTime); message.readPrimitive(&othersReplyTime);
@ -199,7 +199,7 @@ void NodeList::timePingReply(ReceivedMessage& message, const SharedNodePointer&
} }
void NodeList::processPingPacket(QSharedPointer<ReceivedMessage> message, SharedNodePointer sendingNode) { void NodeList::processPingPacket(QSharedPointer<ReceivedMessage> message, SharedNodePointer sendingNode) {
// send back a reply // send back a reply
auto replyPacket = constructPingReplyPacket(*message); auto replyPacket = constructPingReplyPacket(*message);
const HifiSockAddr& senderSockAddr = message->getSenderSockAddr(); const HifiSockAddr& senderSockAddr = message->getSenderSockAddr();
@ -252,6 +252,11 @@ void NodeList::reset() {
_personalMutedNodeIDs.clear(); _personalMutedNodeIDs.clear();
_personalMutedSetLock.unlock(); _personalMutedSetLock.unlock();
// lock and clear out set of avatarGains
_avatarGainMapLock.lockForWrite();
_avatarGainMap.clear();
_avatarGainMapLock.unlock();
// refresh the owner UUID to the NULL UUID // refresh the owner UUID to the NULL UUID
setSessionUUID(QUuid()); setSessionUUID(QUuid());
@ -329,7 +334,7 @@ void NodeList::sendDomainServerCheckIn() {
} }
auto domainPacket = NLPacket::create(domainPacketType); auto domainPacket = NLPacket::create(domainPacketType);
QDataStream packetStream(domainPacket.get()); QDataStream packetStream(domainPacket.get());
if (domainPacketType == PacketType::DomainConnectRequest) { if (domainPacketType == PacketType::DomainConnectRequest) {
@ -488,7 +493,7 @@ void NodeList::processDomainServerPathResponse(QSharedPointer<ReceivedMessage> m
qCDebug(networking) << "Could not read query path from DomainServerPathQueryResponse. Bailing."; qCDebug(networking) << "Could not read query path from DomainServerPathQueryResponse. Bailing.";
return; return;
} }
QString pathQuery = QString::fromUtf8(message->getRawMessage() + message->getPosition(), numPathBytes); QString pathQuery = QString::fromUtf8(message->getRawMessage() + message->getPosition(), numPathBytes);
message->seek(message->getPosition() + numPathBytes); message->seek(message->getPosition() + numPathBytes);
@ -500,10 +505,10 @@ void NodeList::processDomainServerPathResponse(QSharedPointer<ReceivedMessage> m
qCDebug(networking) << "Could not read resulting viewpoint from DomainServerPathQueryReponse. Bailing"; qCDebug(networking) << "Could not read resulting viewpoint from DomainServerPathQueryReponse. Bailing";
return; return;
} }
// pull the viewpoint from the packet // pull the viewpoint from the packet
QString viewpoint = QString::fromUtf8(message->getRawMessage() + message->getPosition(), numViewpointBytes); QString viewpoint = QString::fromUtf8(message->getRawMessage() + message->getPosition(), numViewpointBytes);
// Hand it off to the AddressManager so it can handle it as a relative viewpoint // Hand it off to the AddressManager so it can handle it as a relative viewpoint
if (DependencyManager::get<AddressManager>()->goToViewpointForPath(viewpoint, pathQuery)) { if (DependencyManager::get<AddressManager>()->goToViewpointForPath(viewpoint, pathQuery)) {
qCDebug(networking) << "Going to viewpoint" << viewpoint << "which was the lookup result for path" << pathQuery; qCDebug(networking) << "Going to viewpoint" << viewpoint << "which was the lookup result for path" << pathQuery;
@ -664,16 +669,16 @@ void NodeList::parseNodeFromPacketStream(QDataStream& packetStream) {
} }
void NodeList::sendAssignment(Assignment& assignment) { void NodeList::sendAssignment(Assignment& assignment) {
PacketType assignmentPacketType = assignment.getCommand() == Assignment::CreateCommand PacketType assignmentPacketType = assignment.getCommand() == Assignment::CreateCommand
? PacketType::CreateAssignment ? PacketType::CreateAssignment
: PacketType::RequestAssignment; : PacketType::RequestAssignment;
auto assignmentPacket = NLPacket::create(assignmentPacketType); auto assignmentPacket = NLPacket::create(assignmentPacketType);
QDataStream packetStream(assignmentPacket.get()); QDataStream packetStream(assignmentPacket.get());
packetStream << assignment; packetStream << assignment;
sendPacket(std::move(assignmentPacket), _assignmentServerSocket); sendPacket(std::move(assignmentPacket), _assignmentServerSocket);
} }
@ -833,7 +838,7 @@ void NodeList::ignoreNodeBySessionID(const QUuid& nodeID, bool ignoreEnabled) {
_ignoredNodeIDs.insert(nodeID); _ignoredNodeIDs.insert(nodeID);
} }
{ {
QReadLocker personalMutedSetLocker{ &_personalMutedSetLock }; // read lock for insert QReadLocker personalMutedSetLocker{ &_personalMutedSetLock }; // read lock for insert
// add this nodeID to our set of personal muted IDs // add this nodeID to our set of personal muted IDs
_personalMutedNodeIDs.insert(nodeID); _personalMutedNodeIDs.insert(nodeID);
} }
@ -896,7 +901,7 @@ void NodeList::personalMuteNodeBySessionID(const QUuid& nodeID, bool muteEnabled
if (muteEnabled) { if (muteEnabled) {
QReadLocker personalMutedSetLocker{ &_personalMutedSetLock }; // read lock for insert QReadLocker personalMutedSetLocker{ &_personalMutedSetLock }; // read lock for insert
// add this nodeID to our set of personal muted IDs // add this nodeID to our set of personal muted IDs
_personalMutedNodeIDs.insert(nodeID); _personalMutedNodeIDs.insert(nodeID);
} else { } else {
@ -981,7 +986,7 @@ void NodeList::setAvatarGain(const QUuid& nodeID, float gain) {
if (audioMixer) { if (audioMixer) {
// setup the packet // setup the packet
auto setAvatarGainPacket = NLPacket::create(PacketType::PerAvatarGainSet, NUM_BYTES_RFC4122_UUID + sizeof(float), true); auto setAvatarGainPacket = NLPacket::create(PacketType::PerAvatarGainSet, NUM_BYTES_RFC4122_UUID + sizeof(float), true);
// write the node ID to the packet // write the node ID to the packet
setAvatarGainPacket->write(nodeID.toRfc4122()); setAvatarGainPacket->write(nodeID.toRfc4122());
// We need to convert the gain in dB (from the script) to an amplitude before packing it. // We need to convert the gain in dB (from the script) to an amplitude before packing it.
@ -990,6 +995,9 @@ void NodeList::setAvatarGain(const QUuid& nodeID, float gain) {
qCDebug(networking) << "Sending Set Avatar Gain packet UUID: " << uuidStringWithoutCurlyBraces(nodeID) << "Gain:" << gain; qCDebug(networking) << "Sending Set Avatar Gain packet UUID: " << uuidStringWithoutCurlyBraces(nodeID) << "Gain:" << gain;
sendPacket(std::move(setAvatarGainPacket), *audioMixer); sendPacket(std::move(setAvatarGainPacket), *audioMixer);
QWriteLocker{ &_avatarGainMapLock };
_avatarGainMap[nodeID] = gain;
} else { } else {
qWarning() << "Couldn't find audio mixer to send set gain request"; qWarning() << "Couldn't find audio mixer to send set gain request";
} }
@ -998,6 +1006,15 @@ void NodeList::setAvatarGain(const QUuid& nodeID, float gain) {
} }
} }
float NodeList::getAvatarGain(const QUuid& nodeID) {
QReadLocker{ &_avatarGainMapLock };
auto it = _avatarGainMap.find(nodeID);
if (it != _avatarGainMap.cend()) {
return it->second;
}
return 0.0f;
}
void NodeList::kickNodeBySessionID(const QUuid& nodeID) { void NodeList::kickNodeBySessionID(const QUuid& nodeID) {
// send a request to domain-server to kick the node with the given session ID // send a request to domain-server to kick the node with the given session ID
// the domain-server will handle the persistence of the kick (via username or IP) // the domain-server will handle the persistence of the kick (via username or IP)
@ -1036,7 +1053,7 @@ void NodeList::muteNodeBySessionID(const QUuid& nodeID) {
mutePacket->write(nodeID.toRfc4122()); mutePacket->write(nodeID.toRfc4122());
qCDebug(networking) << "Sending packet to mute node" << uuidStringWithoutCurlyBraces(nodeID); qCDebug(networking) << "Sending packet to mute node" << uuidStringWithoutCurlyBraces(nodeID);
sendPacket(std::move(mutePacket), *audioMixer); sendPacket(std::move(mutePacket), *audioMixer);
} else { } else {
qWarning() << "Couldn't find audio mixer to send node mute request"; qWarning() << "Couldn't find audio mixer to send node mute request";

View file

@ -68,7 +68,7 @@ public:
void setAssignmentServerSocket(const HifiSockAddr& serverSocket) { _assignmentServerSocket = serverSocket; } void setAssignmentServerSocket(const HifiSockAddr& serverSocket) { _assignmentServerSocket = serverSocket; }
void sendAssignment(Assignment& assignment); void sendAssignment(Assignment& assignment);
void setIsShuttingDown(bool isShuttingDown) { _isShuttingDown = isShuttingDown; } void setIsShuttingDown(bool isShuttingDown) { _isShuttingDown = isShuttingDown; }
void ignoreNodesInRadius(bool enabled = true); void ignoreNodesInRadius(bool enabled = true);
@ -83,6 +83,7 @@ public:
void personalMuteNodeBySessionID(const QUuid& nodeID, bool muteEnabled); void personalMuteNodeBySessionID(const QUuid& nodeID, bool muteEnabled);
bool isPersonalMutingNode(const QUuid& nodeID) const; bool isPersonalMutingNode(const QUuid& nodeID) const;
void setAvatarGain(const QUuid& nodeID, float gain); void setAvatarGain(const QUuid& nodeID, float gain);
float getAvatarGain(const QUuid& nodeID);
void kickNodeBySessionID(const QUuid& nodeID); void kickNodeBySessionID(const QUuid& nodeID);
void muteNodeBySessionID(const QUuid& nodeID); void muteNodeBySessionID(const QUuid& nodeID);
@ -103,7 +104,7 @@ public slots:
void processDomainServerPathResponse(QSharedPointer<ReceivedMessage> message); void processDomainServerPathResponse(QSharedPointer<ReceivedMessage> message);
void processDomainServerConnectionTokenPacket(QSharedPointer<ReceivedMessage> message); void processDomainServerConnectionTokenPacket(QSharedPointer<ReceivedMessage> message);
void processPingPacket(QSharedPointer<ReceivedMessage> message, SharedNodePointer sendingNode); void processPingPacket(QSharedPointer<ReceivedMessage> message, SharedNodePointer sendingNode);
void processPingReplyPacket(QSharedPointer<ReceivedMessage> message, SharedNodePointer sendingNode); void processPingReplyPacket(QSharedPointer<ReceivedMessage> message, SharedNodePointer sendingNode);
@ -131,11 +132,11 @@ private slots:
void handleNodePingTimeout(); void handleNodePingTimeout();
void pingPunchForDomainServer(); void pingPunchForDomainServer();
void sendKeepAlivePings(); void sendKeepAlivePings();
void maybeSendIgnoreSetToNode(SharedNodePointer node); void maybeSendIgnoreSetToNode(SharedNodePointer node);
private: private:
NodeList() : LimitedNodeList(INVALID_PORT, INVALID_PORT) { assert(false); } // Not implemented, needed for DependencyManager templates compile NodeList() : LimitedNodeList(INVALID_PORT, INVALID_PORT) { assert(false); } // Not implemented, needed for DependencyManager templates compile
NodeList(char ownerType, int socketListenPort = INVALID_PORT, int dtlsListenPort = INVALID_PORT); NodeList(char ownerType, int socketListenPort = INVALID_PORT, int dtlsListenPort = INVALID_PORT);
@ -148,7 +149,7 @@ private:
void timePingReply(ReceivedMessage& message, const SharedNodePointer& sendingNode); void timePingReply(ReceivedMessage& message, const SharedNodePointer& sendingNode);
void sendDSPathQuery(const QString& newPath); void sendDSPathQuery(const QString& newPath);
void parseNodeFromPacketStream(QDataStream& packetStream); void parseNodeFromPacketStream(QDataStream& packetStream);
void pingPunchForInactiveNode(const SharedNodePointer& node); void pingPunchForInactiveNode(const SharedNodePointer& node);
@ -170,6 +171,8 @@ private:
tbb::concurrent_unordered_set<QUuid, UUIDHasher> _ignoredNodeIDs; tbb::concurrent_unordered_set<QUuid, UUIDHasher> _ignoredNodeIDs;
mutable QReadWriteLock _personalMutedSetLock; mutable QReadWriteLock _personalMutedSetLock;
tbb::concurrent_unordered_set<QUuid, UUIDHasher> _personalMutedNodeIDs; tbb::concurrent_unordered_set<QUuid, UUIDHasher> _personalMutedNodeIDs;
mutable QReadWriteLock _avatarGainMapLock;
tbb::concurrent_unordered_map<QUuid, float, UUIDHasher> _avatarGainMap;
void sendIgnoreRadiusStateToNode(const SharedNodePointer& destinationNode); void sendIgnoreRadiusStateToNode(const SharedNodePointer& destinationNode);
Setting::Handle<bool> _ignoreRadiusEnabled { "IgnoreRadiusEnabled", true }; Setting::Handle<bool> _ignoreRadiusEnabled { "IgnoreRadiusEnabled", true };

View file

@ -19,6 +19,16 @@
#include <QObject> #include <QObject>
#include <QString> #include <QString>
/**jsdoc
* A Quaternion
*
* @typedef Quat
* @property {float} x imaginary component i.
* @property {float} y imaginary component j.
* @property {float} z imaginary component k.
* @property {float} w real component.
*/
/// Scriptable interface a Quaternion helper class object. Used exclusively in the JavaScript API /// Scriptable interface a Quaternion helper class object. Used exclusively in the JavaScript API
class Quat : public QObject { class Quat : public QObject {
Q_OBJECT Q_OBJECT

View file

@ -34,6 +34,7 @@
#include <AudioConstants.h> #include <AudioConstants.h>
#include <AudioEffectOptions.h> #include <AudioEffectOptions.h>
#include <AvatarData.h> #include <AvatarData.h>
#include <DebugDraw.h>
#include <EntityScriptingInterface.h> #include <EntityScriptingInterface.h>
#include <MessagesClient.h> #include <MessagesClient.h>
#include <NetworkAccessManager.h> #include <NetworkAccessManager.h>
@ -630,6 +631,8 @@ void ScriptEngine::init() {
registerGlobalObject("Tablet", DependencyManager::get<TabletScriptingInterface>().data()); registerGlobalObject("Tablet", DependencyManager::get<TabletScriptingInterface>().data());
registerGlobalObject("Assets", &_assetScriptingInterface); registerGlobalObject("Assets", &_assetScriptingInterface);
registerGlobalObject("Resources", DependencyManager::get<ResourceScriptingInterface>().data()); registerGlobalObject("Resources", DependencyManager::get<ResourceScriptingInterface>().data());
registerGlobalObject("DebugDraw", &DebugDraw::getInstance());
} }
void ScriptEngine::registerValue(const QString& valueName, QScriptValue value) { void ScriptEngine::registerValue(const QString& valueName, QScriptValue value) {

View file

@ -47,6 +47,10 @@ void UsersScriptingInterface::setAvatarGain(const QUuid& nodeID, float gain) {
DependencyManager::get<NodeList>()->setAvatarGain(nodeID, gain); DependencyManager::get<NodeList>()->setAvatarGain(nodeID, gain);
} }
float UsersScriptingInterface::getAvatarGain(const QUuid& nodeID) {
return DependencyManager::get<NodeList>()->getAvatarGain(nodeID);
}
void UsersScriptingInterface::kick(const QUuid& nodeID) { void UsersScriptingInterface::kick(const QUuid& nodeID) {
// ask the NodeList to kick the user with the given session ID // ask the NodeList to kick the user with the given session ID
DependencyManager::get<NodeList>()->kickNodeBySessionID(nodeID); DependencyManager::get<NodeList>()->kickNodeBySessionID(nodeID);
@ -88,4 +92,4 @@ bool UsersScriptingInterface::getRequestsDomainListData() {
} }
void UsersScriptingInterface::setRequestsDomainListData(bool isRequesting) { void UsersScriptingInterface::setRequestsDomainListData(bool isRequesting) {
DependencyManager::get<NodeList>()->setRequestsDomainListData(isRequesting); DependencyManager::get<NodeList>()->setRequestsDomainListData(isRequesting);
} }

View file

@ -70,6 +70,14 @@ public slots:
*/ */
void setAvatarGain(const QUuid& nodeID, float gain); void setAvatarGain(const QUuid& nodeID, float gain);
/**jsdoc
* Gets an avatar's gain for you and you only.
* @function Users.getAvatarGain
* @param {nodeID} nodeID The node or session ID of the user whose gain you want to get.
* @return {float} gain (in dB)
*/
float getAvatarGain(const QUuid& nodeID);
/**jsdoc /**jsdoc
* Kick another user. * Kick another user.
* @function Users.kick * @function Users.kick

View file

@ -37,6 +37,15 @@
* @property {float} z Z-coordinate of the vector. * @property {float} z Z-coordinate of the vector.
*/ */
/**jsdoc
* A 4-dimensional vector.
*
* @typedef Vec4
* @property {float} x X-coordinate of the vector.
* @property {float} y Y-coordinate of the vector.
* @property {float} z Z-coordinate of the vector.
* @property {float} w W-coordinate of the vector.
*/
/// Scriptable interface a Vec3ernion helper class object. Used exclusively in the JavaScript API /// Scriptable interface a Vec3ernion helper class object. Used exclusively in the JavaScript API
class Vec3 : public QObject { class Vec3 : public QObject {

View file

@ -10,6 +10,8 @@
#include "DebugDraw.h" #include "DebugDraw.h"
#include "SharedUtil.h" #include "SharedUtil.h"
using Lock = std::unique_lock<std::mutex>;
DebugDraw& DebugDraw::getInstance() { DebugDraw& DebugDraw::getInstance() {
static DebugDraw* instance = globalInstance<DebugDraw>("com.highfidelity.DebugDraw"); static DebugDraw* instance = globalInstance<DebugDraw>("com.highfidelity.DebugDraw");
return *instance; return *instance;
@ -25,22 +27,50 @@ DebugDraw::~DebugDraw() {
// world space line, drawn only once // world space line, drawn only once
void DebugDraw::drawRay(const glm::vec3& start, const glm::vec3& end, const glm::vec4& color) { void DebugDraw::drawRay(const glm::vec3& start, const glm::vec3& end, const glm::vec4& color) {
Lock lock(_mapMutex);
_rays.push_back(Ray(start, end, color)); _rays.push_back(Ray(start, end, color));
} }
void DebugDraw::addMarker(const std::string& key, const glm::quat& rotation, const glm::vec3& position, const glm::vec4& color) { void DebugDraw::addMarker(const QString& key, const glm::quat& rotation, const glm::vec3& position, const glm::vec4& color) {
Lock lock(_mapMutex);
_markers[key] = MarkerInfo(rotation, position, color); _markers[key] = MarkerInfo(rotation, position, color);
} }
void DebugDraw::removeMarker(const std::string& key) { void DebugDraw::removeMarker(const QString& key) {
Lock lock(_mapMutex);
_markers.erase(key); _markers.erase(key);
} }
void DebugDraw::addMyAvatarMarker(const std::string& key, const glm::quat& rotation, const glm::vec3& position, const glm::vec4& color) { void DebugDraw::addMyAvatarMarker(const QString& key, const glm::quat& rotation, const glm::vec3& position, const glm::vec4& color) {
Lock lock(_mapMutex);
_myAvatarMarkers[key] = MarkerInfo(rotation, position, color); _myAvatarMarkers[key] = MarkerInfo(rotation, position, color);
} }
void DebugDraw::removeMyAvatarMarker(const std::string& key) { void DebugDraw::removeMyAvatarMarker(const QString& key) {
Lock lock(_mapMutex);
_myAvatarMarkers.erase(key); _myAvatarMarkers.erase(key);
} }
//
// accessors used by renderer
//
DebugDraw::MarkerMap DebugDraw::getMarkerMap() const {
Lock lock(_mapMutex);
return _markers;
}
DebugDraw::MarkerMap DebugDraw::getMyAvatarMarkerMap() const {
Lock lock(_mapMutex);
return _myAvatarMarkers;
}
DebugDraw::Rays DebugDraw::getRays() const {
Lock lock(_mapMutex);
return _rays;
}
void DebugDraw::clearRays() {
Lock lock(_mapMutex);
_rays.clear();
}

View file

@ -10,6 +10,7 @@
#ifndef hifi_DebugDraw_h #ifndef hifi_DebugDraw_h
#define hifi_DebugDraw_h #define hifi_DebugDraw_h
#include <mutex>
#include <unordered_map> #include <unordered_map>
#include <tuple> #include <tuple>
#include <string> #include <string>
@ -17,26 +18,69 @@
#include <glm/glm.hpp> #include <glm/glm.hpp>
#include <glm/gtc/quaternion.hpp> #include <glm/gtc/quaternion.hpp>
class DebugDraw { #include <QObject>
#include <QString>
/**jsdoc
* Helper functions to render ephemeral debug markers and lines.
* DebugDraw markers and lines are only visible locally, they are not visible by other users.
* @namespace DebugDraw
*/
class DebugDraw : public QObject {
Q_OBJECT
public: public:
static DebugDraw& getInstance(); static DebugDraw& getInstance();
DebugDraw(); DebugDraw();
~DebugDraw(); ~DebugDraw();
// world space line, drawn only once /**jsdoc
void drawRay(const glm::vec3& start, const glm::vec3& end, const glm::vec4& color); * Draws a line in world space, but it will only be visible for a single frame.
* @function DebugDraw.drawRay
* @param {Vec3} start - start position of line in world space.
* @param {Vec3} end - end position of line in world space.
* @param {Vec4} color - color of line, each component should be in the zero to one range. x = red, y = blue, z = green, w = alpha.
*/
Q_INVOKABLE void drawRay(const glm::vec3& start, const glm::vec3& end, const glm::vec4& color);
// world space maker, marker drawn every frame until it is removed. /**jsdoc
void addMarker(const std::string& key, const glm::quat& rotation, const glm::vec3& position, const glm::vec4& color); * Adds a debug marker to the world. This marker will be drawn every frame until it is removed with DebugDraw.removeMarker.
void removeMarker(const std::string& key); * This can be called repeatedly to change the position of the marker.
* @function DebugDraw.addMarker
* @param {string} key - name to uniquely identify this marker, later used for DebugDraw.removeMarker.
* @param {Quat} rotation - start position of line in world space.
* @param {Vec3} position - position of the marker in world space.
* @param {Vec4} color - color of the marker.
*/
Q_INVOKABLE void addMarker(const QString& key, const glm::quat& rotation, const glm::vec3& position, const glm::vec4& color);
// myAvatar relative marker, maker is drawn every frame until it is removed. /**jsdoc
void addMyAvatarMarker(const std::string& key, const glm::quat& rotation, const glm::vec3& position, const glm::vec4& color); * Removes debug marker from the world. Once a marker is removed, it will no longer be visible.
void removeMyAvatarMarker(const std::string& key); * @function DebugDraw.removeMarker
* @param {string} key - name of marker to remove.
*/
Q_INVOKABLE void removeMarker(const QString& key);
/**jsdoc
* Adds a debug marker to the world, this marker will be drawn every frame until it is removed with DebugDraw.removeMyAvatarMarker.
* This can be called repeatedly to change the position of the marker.
* @function DebugDraw.addMyAvatarMarker
* @param {string} key - name to uniquely identify this marker, later used for DebugDraw.removeMyAvatarMarker.
* @param {Quat} rotation - start position of line in avatar space.
* @param {Vec3} position - position of the marker in avatar space.
* @param {Vec4} color - color of the marker.
*/
Q_INVOKABLE void addMyAvatarMarker(const QString& key, const glm::quat& rotation, const glm::vec3& position, const glm::vec4& color);
/**jsdoc
* Removes debug marker from the world. Once a marker is removed, it will no longer be visible
* @function DebugDraw.removeMyAvatarMarker
* @param {string} key - name of marker to remove.
*/
Q_INVOKABLE void removeMyAvatarMarker(const QString& key);
using MarkerInfo = std::tuple<glm::quat, glm::vec3, glm::vec4>; using MarkerInfo = std::tuple<glm::quat, glm::vec3, glm::vec4>;
using MarkerMap = std::unordered_map<std::string, MarkerInfo>; using MarkerMap = std::map<QString, MarkerInfo>;
using Ray = std::tuple<glm::vec3, glm::vec3, glm::vec4>; using Ray = std::tuple<glm::vec3, glm::vec3, glm::vec4>;
using Rays = std::vector<Ray>; using Rays = std::vector<Ray>;
@ -44,16 +88,17 @@ public:
// accessors used by renderer // accessors used by renderer
// //
const MarkerMap& getMarkerMap() const { return _markers; } MarkerMap getMarkerMap() const;
const MarkerMap& getMyAvatarMarkerMap() const { return _myAvatarMarkers; } MarkerMap getMyAvatarMarkerMap() const;
void updateMyAvatarPos(const glm::vec3& pos) { _myAvatarPos = pos; } void updateMyAvatarPos(const glm::vec3& pos) { _myAvatarPos = pos; }
const glm::vec3& getMyAvatarPos() const { return _myAvatarPos; } const glm::vec3& getMyAvatarPos() const { return _myAvatarPos; }
void updateMyAvatarRot(const glm::quat& rot) { _myAvatarRot = rot; } void updateMyAvatarRot(const glm::quat& rot) { _myAvatarRot = rot; }
const glm::quat& getMyAvatarRot() const { return _myAvatarRot; } const glm::quat& getMyAvatarRot() const { return _myAvatarRot; }
const Rays getRays() const { return _rays; } Rays getRays() const;
void clearRays() { _rays.clear(); } void clearRays();
protected: protected:
mutable std::mutex _mapMutex;
MarkerMap _markers; MarkerMap _markers;
MarkerMap _myAvatarMarkers; MarkerMap _myAvatarMarkers;
glm::quat _myAvatarRot; glm::quat _myAvatarRot;

View file

@ -74,6 +74,10 @@ var WEB_TOUCH_Y_OFFSET = 0.05; // how far forward (or back with a negative numbe
var WEB_TOUCH_TOO_CLOSE = 0.03; // if the stylus is pushed far though the web surface, don't consider it touching var WEB_TOUCH_TOO_CLOSE = 0.03; // if the stylus is pushed far though the web surface, don't consider it touching
var WEB_TOUCH_Y_TOUCH_DEADZONE_SIZE = 0.01; var WEB_TOUCH_Y_TOUCH_DEADZONE_SIZE = 0.01;
var FINGER_TOUCH_Y_OFFSET = -0.02;
var FINGER_TOUCH_MIN = -0.01 - FINGER_TOUCH_Y_OFFSET;
var FINGER_TOUCH_MAX = 0.01 - FINGER_TOUCH_Y_OFFSET;
// //
// distant manipulation // distant manipulation
// //
@ -205,14 +209,15 @@ var HARDWARE_MOUSE_ID = 0; // Value reserved for hardware mouse.
var STATE_OFF = 0; var STATE_OFF = 0;
var STATE_SEARCHING = 1; var STATE_SEARCHING = 1;
var STATE_DISTANCE_HOLDING = 2; var STATE_DISTANCE_HOLDING = 2;
var STATE_NEAR_GRABBING = 3; var STATE_DISTANCE_ROTATING = 3;
var STATE_NEAR_TRIGGER = 4; var STATE_NEAR_GRABBING = 4;
var STATE_FAR_TRIGGER = 5; var STATE_NEAR_TRIGGER = 5;
var STATE_HOLD = 6; var STATE_FAR_TRIGGER = 6;
var STATE_ENTITY_STYLUS_TOUCHING = 7; var STATE_HOLD = 7;
var STATE_ENTITY_LASER_TOUCHING = 8; var STATE_ENTITY_STYLUS_TOUCHING = 8;
var STATE_OVERLAY_STYLUS_TOUCHING = 9; var STATE_ENTITY_LASER_TOUCHING = 9;
var STATE_OVERLAY_LASER_TOUCHING = 10; var STATE_OVERLAY_STYLUS_TOUCHING = 10;
var STATE_OVERLAY_LASER_TOUCHING = 11;
var CONTROLLER_STATE_MACHINE = {}; var CONTROLLER_STATE_MACHINE = {};
@ -231,6 +236,11 @@ CONTROLLER_STATE_MACHINE[STATE_DISTANCE_HOLDING] = {
enterMethod: "distanceHoldingEnter", enterMethod: "distanceHoldingEnter",
updateMethod: "distanceHolding" updateMethod: "distanceHolding"
}; };
CONTROLLER_STATE_MACHINE[STATE_DISTANCE_ROTATING] = {
name: "distance_rotating",
enterMethod: "distanceRotatingEnter",
updateMethod: "distanceRotating"
};
CONTROLLER_STATE_MACHINE[STATE_NEAR_GRABBING] = { CONTROLLER_STATE_MACHINE[STATE_NEAR_GRABBING] = {
name: "near_grabbing", name: "near_grabbing",
enterMethod: "nearGrabbingEnter", enterMethod: "nearGrabbingEnter",
@ -252,20 +262,73 @@ CONTROLLER_STATE_MACHINE[STATE_FAR_TRIGGER] = {
updateMethod: "farTrigger" updateMethod: "farTrigger"
}; };
CONTROLLER_STATE_MACHINE[STATE_ENTITY_STYLUS_TOUCHING] = { CONTROLLER_STATE_MACHINE[STATE_ENTITY_STYLUS_TOUCHING] = {
name: "entityTouching", name: "entityStylusTouching",
enterMethod: "entityTouchingEnter",
exitMethod: "entityTouchingExit",
updateMethod: "entityTouching"
};
CONTROLLER_STATE_MACHINE[STATE_ENTITY_LASER_TOUCHING] = {
name: "entityLaserTouching",
enterMethod: "entityTouchingEnter", enterMethod: "entityTouchingEnter",
exitMethod: "entityTouchingExit", exitMethod: "entityTouchingExit",
updateMethod: "entityTouching" updateMethod: "entityTouching"
}; };
CONTROLLER_STATE_MACHINE[STATE_ENTITY_LASER_TOUCHING] = CONTROLLER_STATE_MACHINE[STATE_ENTITY_STYLUS_TOUCHING];
CONTROLLER_STATE_MACHINE[STATE_OVERLAY_STYLUS_TOUCHING] = { CONTROLLER_STATE_MACHINE[STATE_OVERLAY_STYLUS_TOUCHING] = {
name: "overlayTouching", name: "overlayStylusTouching",
enterMethod: "overlayTouchingEnter",
exitMethod: "overlayTouchingExit",
updateMethod: "overlayTouching"
};
CONTROLLER_STATE_MACHINE[STATE_OVERLAY_LASER_TOUCHING] = {
name: "overlayLaserTouching",
enterMethod: "overlayTouchingEnter", enterMethod: "overlayTouchingEnter",
exitMethod: "overlayTouchingExit", exitMethod: "overlayTouchingExit",
updateMethod: "overlayTouching" updateMethod: "overlayTouching"
}; };
CONTROLLER_STATE_MACHINE[STATE_OVERLAY_LASER_TOUCHING] = CONTROLLER_STATE_MACHINE[STATE_OVERLAY_STYLUS_TOUCHING];
function getFingerWorldLocation(hand) {
var fingerJointName = (hand === RIGHT_HAND) ? "RightHandIndex4" : "LeftHandIndex4";
var fingerJointIndex = MyAvatar.getJointIndex(fingerJointName);
var fingerPosition = MyAvatar.getAbsoluteJointTranslationInObjectFrame(fingerJointIndex);
var fingerRotation = MyAvatar.getAbsoluteJointRotationInObjectFrame(fingerJointIndex);
var worldFingerRotation = Quat.multiply(MyAvatar.orientation, fingerRotation);
var worldFingerPosition = Vec3.sum(MyAvatar.position, Vec3.multiplyQbyV(MyAvatar.orientation, fingerPosition));
// local y offset.
var localYOffset = Vec3.multiplyQbyV(worldFingerRotation, {x: 0, y: FINGER_TOUCH_Y_OFFSET, z: 0});
var offsetWorldFingerPosition = Vec3.sum(worldFingerPosition, localYOffset);
return {
position: offsetWorldFingerPosition,
orientation: worldFingerRotation,
rotation: worldFingerRotation,
valid: true
};
}
// 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) { function distanceBetweenPointAndEntityBoundingBox(point, entityProps) {
var entityXform = new Xform(entityProps.rotation, entityProps.position); var entityXform = new Xform(entityProps.rotation, entityProps.position);
@ -347,6 +410,7 @@ function handLaserIntersectItem(position, rotation, start) {
direction: rayDirection, direction: rayDirection,
length: PICK_MAX_DISTANCE length: PICK_MAX_DISTANCE
}; };
return intersectionInfo; return intersectionInfo;
} else { } else {
// entity has been destroyed? or is no longer in cache // entity has been destroyed? or is no longer in cache
@ -413,16 +477,18 @@ function entityIsGrabbedByOther(entityID) {
var actionID = actionIDs[actionIndex]; var actionID = actionIDs[actionIndex];
var actionArguments = Entities.getActionArguments(entityID, actionID); var actionArguments = Entities.getActionArguments(entityID, actionID);
var tag = actionArguments.tag; var tag = actionArguments.tag;
if (tag == getTag()) { if (tag === getTag()) {
// we see a grab-*uuid* shaped tag, but it's our tag, so that's okay. // we see a grab-*uuid* shaped tag, but it's our tag, so that's okay.
continue; continue;
} }
if (tag.slice(0, 5) == "grab-") { var GRAB_PREFIX_LENGTH = 5;
var UUID_LENGTH = 38;
if (tag && tag.slice(0, GRAB_PREFIX_LENGTH) == "grab-") {
// we see a grab-*uuid* shaped tag and it's not ours, so someone else is grabbing it. // we see a grab-*uuid* shaped tag and it's not ours, so someone else is grabbing it.
return true; return tag.slice(GRAB_PREFIX_LENGTH, GRAB_PREFIX_LENGTH + UUID_LENGTH - 1);
} }
} }
return false; return null;
} }
function propsArePhysical(props) { function propsArePhysical(props) {
@ -740,6 +806,10 @@ function MyController(hand) {
this.stylus = null; this.stylus = null;
this.homeButtonTouched = false; 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 // 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 // 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 // around this with two associative arrays: previousParentID and previousParentJointIndex. If
@ -791,10 +861,10 @@ function MyController(hand) {
// for visualizations // for visualizations
this.overlayLine = null; this.overlayLine = null;
// for lights
this.overlayLine = null;
this.searchSphere = null; this.searchSphere = null;
this.otherGrabbingLine = null;
this.otherGrabbingUUID = null;
this.waitForTriggerRelease = false; this.waitForTriggerRelease = false;
@ -816,6 +886,8 @@ function MyController(hand) {
this.tabletStabbedPos2D = null; this.tabletStabbedPos2D = null;
this.tabletStabbedPos3D = null; this.tabletStabbedPos3D = null;
this.useFingerInsteadOfStylus = false;
var _this = this; var _this = this;
var suppressedIn2D = [STATE_OFF, STATE_SEARCHING]; var suppressedIn2D = [STATE_OFF, STATE_SEARCHING];
@ -829,10 +901,22 @@ function MyController(hand) {
this.updateSmoothedTrigger(); this.updateSmoothedTrigger();
this.maybeScaleMyAvatar(); this.maybeScaleMyAvatar();
var DEFAULT_USE_FINGER_AS_STYLUS = false;
var USE_FINGER_AS_STYLUS = Settings.getValue("preferAvatarFingerOverStylus");
if (USE_FINGER_AS_STYLUS === "") {
USE_FINGER_AS_STYLUS = DEFAULT_USE_FINGER_AS_STYLUS;
}
if (USE_FINGER_AS_STYLUS && MyAvatar.getJointIndex("LeftHandIndex4") !== -1) {
this.useFingerInsteadOfStylus = true;
} else {
this.useFingerInsteadOfStylus = false;
}
if (this.ignoreInput()) { if (this.ignoreInput()) {
// Most hand input is disabled, because we are interacting with the 2d hud. // Most hand input is disabled, because we are interacting with the 2d hud.
// However, we still should check for collisions of the stylus with the web overlay. // However, we still should check for collisions of the stylus with the web overlay.
var controllerLocation = getControllerWorldLocation(this.handToController(), true); var controllerLocation = getControllerWorldLocation(this.handToController(), true);
this.processStylus(controllerLocation.position); this.processStylus(controllerLocation.position);
@ -869,7 +953,8 @@ function MyController(hand) {
newState !== STATE_OVERLAY_LASER_TOUCHING)) { newState !== STATE_OVERLAY_LASER_TOUCHING)) {
return; 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) { if (WANT_DEBUG || WANT_DEBUG_STATE) {
var oldStateName = stateToName(this.state); var oldStateName = stateToName(this.state);
var newStateName = stateToName(newState); var newStateName = stateToName(newState);
@ -920,9 +1005,7 @@ function MyController(hand) {
ignoreRayIntersection: true, ignoreRayIntersection: true,
drawInFront: false, drawInFront: false,
parentID: AVATAR_SELF_ID, parentID: AVATAR_SELF_ID,
parentJointIndex: MyAvatar.getJointIndex(this.hand === RIGHT_HAND ? parentJointIndex: this.controllerJointIndex
"_CONTROLLER_RIGHTHAND" :
"_CONTROLLER_LEFTHAND")
}); });
} }
}; };
@ -1007,32 +1090,38 @@ function MyController(hand) {
} }
}; };
this.overlayLineOn = function(closePoint, farPoint, color) { this.overlayLineOn = function(closePoint, farPoint, color, farParentID) {
if (this.overlayLine === null) { if (this.overlayLine === null) {
var lineProperties = { var lineProperties = {
name: "line", name: "line",
glow: 1.0, 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, lineWidth: 5,
start: closePoint, start: closePoint,
end: farPoint, end: farPoint,
color: color, color: color,
visible: true,
ignoreRayIntersection: true, // always ignore this ignoreRayIntersection: true, // always ignore this
drawInFront: true, // Even when burried inside of something, show it. 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
});
}
} }
}; };
@ -1061,6 +1150,29 @@ function MyController(hand) {
} }
}; };
this.otherGrabbingLineOn = function(avatarPosition, entityPosition, color) {
if (this.otherGrabbingLine === null) {
var lineProperties = {
lineWidth: 5,
start: avatarPosition,
end: entityPosition,
color: color,
glow: 1.0,
ignoreRayIntersection: true,
drawInFront: true,
visible: true,
alpha: 1
};
this.otherGrabbingLine = Overlays.addOverlay("line3d", lineProperties);
} else {
Overlays.editOverlay(this.otherGrabbingLine, {
start: avatarPosition,
end: entityPosition,
color: color
});
}
};
this.evalLightWorldTransform = function(modelPos, modelRot) { this.evalLightWorldTransform = function(modelPos, modelRot) {
var MODEL_LIGHT_POSITION = { var MODEL_LIGHT_POSITION = {
@ -1104,14 +1216,20 @@ function MyController(hand) {
} }
}; };
this.turnOffVisualizations = function() { this.otherGrabbingLineOff = function() {
if (this.otherGrabbingLine !== null) {
Overlays.deleteOverlay(this.otherGrabbingLine);
}
this.otherGrabbingLine = null;
};
this.turnOffVisualizations = function() {
this.overlayLineOff(); this.overlayLineOff();
this.grabPointSphereOff(); this.grabPointSphereOff();
this.lineOff(); this.lineOff();
this.searchSphereOff(); this.searchSphereOff();
this.otherGrabbingLineOff();
restore2DMode(); restore2DMode();
}; };
this.triggerPress = function(value) { this.triggerPress = function(value) {
@ -1174,30 +1292,54 @@ function MyController(hand) {
}; };
this.processStylus = function(worldHandPosition) { this.processStylus = function(worldHandPosition) {
// see if the hand is near a tablet or web-entity
var candidateEntities = Entities.findEntities(worldHandPosition, WEB_DISPLAY_STYLUS_DISTANCE); var performRayTest = false;
entityPropertiesCache.addEntities(candidateEntities); if (this.useFingerInsteadOfStylus) {
var nearWeb = false; this.hideStylus();
for (var i = 0; i < candidateEntities.length; i++) { performRayTest = true;
var props = entityPropertiesCache.getProps(candidateEntities[i]); } else {
if (props && (props.type == "Web" || this.isTablet(candidateEntities[i]))) { var i;
nearWeb = true;
break; // see if the hand is near a tablet or web-entity
var candidateEntities = Entities.findEntities(worldHandPosition, WEB_DISPLAY_STYLUS_DISTANCE);
entityPropertiesCache.addEntities(candidateEntities);
for (i = 0; i < candidateEntities.length; i++) {
var props = entityPropertiesCache.getProps(candidateEntities[i]);
if (props && (props.type == "Web" || this.isTablet(candidateEntities[i]))) {
performRayTest = true;
break;
}
}
if (!performRayTest) {
var candidateOverlays = Overlays.findOverlays(worldHandPosition, WEB_DISPLAY_STYLUS_DISTANCE);
for (i = 0; i < candidateOverlays.length; i++) {
if (this.isTablet(candidateOverlays[i])) {
performRayTest = true;
break;
}
}
}
if (performRayTest) {
this.showStylus();
} else {
this.hideStylus();
} }
} }
var candidateOverlays = Overlays.findOverlays(worldHandPosition, WEB_DISPLAY_STYLUS_DISTANCE); if (performRayTest) {
for (var j = 0; j < candidateOverlays.length; j++) { var rayPickInfo = this.calcRayPickInfo(this.hand, this.useFingerInsteadOfStylus);
if (this.isTablet(candidateOverlays[j])) { var max, min;
nearWeb = true; if (this.useFingerInsteadOfStylus) {
max = FINGER_TOUCH_MAX;
min = FINGER_TOUCH_MIN;
} else {
max = WEB_STYLUS_LENGTH / 2.0 + WEB_TOUCH_Y_OFFSET;
min = WEB_STYLUS_LENGTH / 2.0 + WEB_TOUCH_TOO_CLOSE;
} }
}
if (nearWeb) { if (rayPickInfo.distance < max && rayPickInfo.distance > min) {
this.showStylus();
var rayPickInfo = this.calcRayPickInfo(this.hand);
if (rayPickInfo.distance < WEB_STYLUS_LENGTH / 2.0 + WEB_TOUCH_Y_OFFSET &&
rayPickInfo.distance > WEB_STYLUS_LENGTH / 2.0 + WEB_TOUCH_TOO_CLOSE) {
this.handleStylusOnHomeButton(rayPickInfo); this.handleStylusOnHomeButton(rayPickInfo);
if (this.handleStylusOnWebEntity(rayPickInfo)) { if (this.handleStylusOnWebEntity(rayPickInfo)) {
return; return;
@ -1206,10 +1348,8 @@ function MyController(hand) {
return; return;
} }
} else { } else {
this.homeButtonTouched = false; this.homeButtonTouched = false;
} }
} else {
this.hideStylus();
} }
}; };
@ -1324,10 +1464,17 @@ function MyController(hand) {
// Performs ray pick test from the hand controller into the world // Performs ray pick test from the hand controller into the world
// @param {number} which hand to use, RIGHT_HAND or LEFT_HAND // @param {number} which hand to use, RIGHT_HAND or LEFT_HAND
// @param {bool} if true use the world position/orientation of the index finger to cast the ray from.
// @returns {object} returns object with two keys entityID and distance // @returns {object} returns object with two keys entityID and distance
// //
this.calcRayPickInfo = function(hand) { this.calcRayPickInfo = function(hand, useFingerInsteadOfController) {
var controllerLocation = getControllerWorldLocation(this.handToController(), true);
var controllerLocation;
if (useFingerInsteadOfController) {
controllerLocation = getFingerWorldLocation(hand);
} else {
controllerLocation = getControllerWorldLocation(this.handToController(), true);
}
var worldHandPosition = controllerLocation.position; var worldHandPosition = controllerLocation.position;
var worldHandRotation = controllerLocation.orientation; var worldHandRotation = controllerLocation.orientation;
@ -1439,9 +1586,10 @@ function MyController(hand) {
var props = entityPropertiesCache.getProps(hotspot.entityID); var props = entityPropertiesCache.getProps(hotspot.entityID);
var debug = (WANT_DEBUG_SEARCH_NAME && props.name === WANT_DEBUG_SEARCH_NAME); var debug = (WANT_DEBUG_SEARCH_NAME && props.name === WANT_DEBUG_SEARCH_NAME);
var okToEquipFromOtherHand = ((this.getOtherHandController().state == STATE_NEAR_GRABBING || var otherHandControllerState = this.getOtherHandController().state;
this.getOtherHandController().state == STATE_DISTANCE_HOLDING) && var okToEquipFromOtherHand = ((otherHandControllerState === STATE_NEAR_GRABBING
this.getOtherHandController().grabbedThingID == hotspot.entityID); || otherHandControllerState === STATE_DISTANCE_HOLDING || otherHandControllerState === STATE_DISTANCE_ROTATING)
&& this.getOtherHandController().grabbedThingID === hotspot.entityID);
var hasParent = true; var hasParent = true;
if (props.parentID === NULL_UUID) { if (props.parentID === NULL_UUID) {
hasParent = false; hasParent = false;
@ -1455,7 +1603,18 @@ function MyController(hand) {
return true; 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) { this.entityIsGrabbable = function(entityID) {
var grabbableProps = entityPropertiesCache.getGrabbableProps(entityID); var grabbableProps = entityPropertiesCache.getGrabbableProps(entityID);
var props = entityPropertiesCache.getProps(entityID); var props = entityPropertiesCache.getProps(entityID);
@ -1522,7 +1681,8 @@ function MyController(hand) {
return false; return false;
} }
if (entityIsGrabbedByOther(entityID)) { this.otherGrabbingUUID = entityIsGrabbedByOther(entityID);
if (this.otherGrabbingUUID !== null) {
// don't distance grab something that is already grabbed. // don't distance grab something that is already grabbed.
if (debug) { if (debug) {
print("distance grab is skipping '" + props.name + "': already grabbed by another."); print("distance grab is skipping '" + props.name + "': already grabbed by another.");
@ -1535,7 +1695,7 @@ function MyController(hand) {
this.entityIsNearGrabbable = function(entityID, handPosition, maxDistance) { this.entityIsNearGrabbable = function(entityID, handPosition, maxDistance) {
if (!this.entityIsGrabbable(entityID)) { if (!this.entityIsCloneable(entityID) && !this.entityIsGrabbable(entityID)) {
return false; return false;
} }
@ -1726,17 +1886,40 @@ function MyController(hand) {
} else { } else {
// potentialFarTriggerEntity = entity; // potentialFarTriggerEntity = entity;
} }
this.otherGrabbingLineOff();
} else if (this.entityIsDistanceGrabbable(rayPickInfo.entityID, handPosition)) { } else if (this.entityIsDistanceGrabbable(rayPickInfo.entityID, handPosition)) {
if (this.triggerSmoothedGrab() && !isEditing() && farGrabEnabled && farSearching) { if (this.triggerSmoothedGrab() && !isEditing() && farGrabEnabled && farSearching) {
this.grabbedThingID = entity; this.grabbedThingID = entity;
this.grabbedIsOverlay = false; this.grabbedIsOverlay = false;
this.grabbedDistance = rayPickInfo.distance; 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 + "'"); this.setState(STATE_DISTANCE_HOLDING, "distance hold '" + name + "'");
}
return; return;
} else { } else {
// potentialFarGrabEntity = entity; // potentialFarGrabEntity = entity;
} }
this.otherGrabbingLineOff();
} else if (this.otherGrabbingUUID !== null) {
if (this.triggerSmoothedGrab() && !isEditing() && farGrabEnabled && farSearching) {
var avatar = AvatarList.getAvatar(this.otherGrabbingUUID);
var IN_FRONT_OF_AVATAR = { x: 0, y: 0.2, z: 0.4 }; // Up from hips and in front of avatar.
var startPosition = Vec3.sum(avatar.position, Vec3.multiplyQbyV(avatar.rotation, IN_FRONT_OF_AVATAR));
var finishPisition = Vec3.sum(rayPickInfo.properties.position, // Entity's centroid.
Vec3.multiplyQbyV(rayPickInfo.properties.rotation ,
Vec3.multiplyVbyV(rayPickInfo.properties.dimensions,
Vec3.subtract(DEFAULT_REGISTRATION_POINT, rayPickInfo.properties.registrationPoint))));
this.otherGrabbingLineOn(startPosition, finishPisition, COLORS_GRAB_DISTANCE_HOLD);
} else {
this.otherGrabbingLineOff();
}
} else {
this.otherGrabbingLineOff();
} }
} else {
this.otherGrabbingLineOff();
} }
this.updateEquipHaptics(potentialEquipHotspot, handPosition); this.updateEquipHaptics(potentialEquipHotspot, handPosition);
@ -2036,6 +2219,19 @@ function MyController(hand) {
return (dimensions.x * dimensions.y * dimensions.z) * density; 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.distanceHoldingEnter = function() {
this.clearEquipHaptics(); this.clearEquipHaptics();
this.grabPointSphereOff(); this.grabPointSphereOff();
@ -2102,25 +2298,20 @@ function MyController(hand) {
this.previousRoomControllerPosition = roomControllerPosition; 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) { this.distanceHolding = function(deltaTime, timestamp) {
if (!this.triggerClicked) { if (!this.triggerClicked) {
this.callEntityMethodOnGrabbed("releaseGrab"); this.callEntityMethodOnGrabbed("releaseGrab");
this.ensureDynamic(); this.ensureDynamic();
this.setState(STATE_OFF, "trigger released"); 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; return;
} }
@ -2209,11 +2400,13 @@ function MyController(hand) {
} }
this.maybeScale(grabbedProperties); this.maybeScale(grabbedProperties);
// visualizations // visualizations
var rayPickInfo = this.calcRayPickInfo(this.hand); var rayPickInfo = this.calcRayPickInfo(this.hand);
this.overlayLineOn(rayPickInfo.searchRay.origin,
this.overlayLineOn(rayPickInfo.searchRay.origin, Vec3.subtract(grabbedProperties.position, this.offsetPosition), COLORS_GRAB_DISTANCE_HOLD); Vec3.subtract(grabbedProperties.position, this.offsetPosition),
COLORS_GRAB_DISTANCE_HOLD,
this.grabbedThingID);
var distanceToObject = Vec3.length(Vec3.subtract(MyAvatar.position, this.currentObjectPosition)); var distanceToObject = Vec3.length(Vec3.subtract(MyAvatar.position, this.currentObjectPosition));
var success = Entities.updateAction(this.grabbedThingID, this.actionID, { var success = Entities.updateAction(this.grabbedThingID, this.actionID, {
@ -2232,6 +2425,64 @@ function MyController(hand) {
this.previousRoomControllerPosition = roomControllerPosition; 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.setupHoldAction = function() {
this.actionID = Entities.addAction("hold", this.grabbedThingID, { this.actionID = Entities.addAction("hold", this.grabbedThingID, {
hand: this.hand === RIGHT_HAND ? "right" : "left", hand: this.hand === RIGHT_HAND ? "right" : "left",
@ -2314,6 +2565,7 @@ function MyController(hand) {
this.lineOff(); this.lineOff();
this.overlayLineOff(); this.overlayLineOff();
this.searchSphereOff(); this.searchSphereOff();
this.otherGrabbingLineOff();
this.dropGestureReset(); this.dropGestureReset();
this.clearEquipHaptics(); this.clearEquipHaptics();
@ -2385,6 +2637,9 @@ function MyController(hand) {
this.offsetPosition = Vec3.multiplyQbyV(Quat.inverse(Quat.multiply(handRotation, this.offsetRotation)), offset); 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) || var isPhysical = propsArePhysical(grabbedProperties) ||
(!this.grabbedIsOverlay && entityHasActions(this.grabbedThingID)); (!this.grabbedIsOverlay && entityHasActions(this.grabbedThingID));
if (isPhysical && this.state == STATE_NEAR_GRABBING && grabbedProperties.parentID === NULL_UUID) { if (isPhysical && this.state == STATE_NEAR_GRABBING && grabbedProperties.parentID === NULL_UUID) {
@ -2402,9 +2657,7 @@ function MyController(hand) {
this.actionID = null; this.actionID = null;
var handJointIndex; var handJointIndex;
if (this.ignoreIK) { if (this.ignoreIK) {
handJointIndex = MyAvatar.getJointIndex(this.hand === RIGHT_HAND ? handJointIndex = this.controllerJointIndex;
"_CONTROLLER_RIGHTHAND" :
"_CONTROLLER_LEFTHAND");
} else { } else {
handJointIndex = MyAvatar.getJointIndex(this.hand === RIGHT_HAND ? "RightHand" : "LeftHand"); handJointIndex = MyAvatar.getJointIndex(this.hand === RIGHT_HAND ? "RightHand" : "LeftHand");
} }
@ -2423,6 +2676,54 @@ function MyController(hand) {
if (this.grabbedIsOverlay) { if (this.grabbedIsOverlay) {
Overlays.editOverlay(this.grabbedThingID, reparentProps); Overlays.editOverlay(this.grabbedThingID, reparentProps);
} else { } 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); Entities.editEntity(this.grabbedThingID, reparentProps);
} }
@ -2434,7 +2735,6 @@ function MyController(hand) {
this.previousParentID[this.grabbedThingID] = grabbedProperties.parentID; this.previousParentID[this.grabbedThingID] = grabbedProperties.parentID;
this.previousParentJointIndex[this.grabbedThingID] = grabbedProperties.parentJointIndex; this.previousParentJointIndex[this.grabbedThingID] = grabbedProperties.parentJointIndex;
} }
Messages.sendMessage('Hifi-Object-Manipulation', JSON.stringify({ Messages.sendMessage('Hifi-Object-Manipulation', JSON.stringify({
action: 'equip', action: 'equip',
grabbedEntity: this.grabbedThingID, grabbedEntity: this.grabbedThingID,
@ -2450,22 +2750,37 @@ function MyController(hand) {
}); });
} }
if (this.state == STATE_NEAR_GRABBING) { var _this = this;
this.callEntityMethodOnGrabbed("startNearGrab"); /*
} else { // this.state == STATE_HOLD * Setting context for function that is either called via timer or directly, depending if
this.callEntityMethodOnGrabbed("startEquip"); * 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 = if (isClone) {
(this.hand === RIGHT_HAND) ? MyAvatar.rightHandTipPosition : MyAvatar.leftHandTipPosition; // 100 ms seems to be sufficient time to force the check even occur after the object has been initialized.
this.currentObjectTime = Date.now(); Script.setTimeout(grabEquipCheck, 100);
} else {
this.currentObjectPosition = grabbedProperties.position; grabEquipCheck();
this.currentObjectRotation = grabbedProperties.rotation; }
this.currentVelocity = ZERO_VEC;
this.currentAngularVelocity = ZERO_VEC;
this.prevDropDetected = false;
}; };
this.nearGrabbing = function(deltaTime, timestamp) { this.nearGrabbing = function(deltaTime, timestamp) {
@ -2783,8 +3098,13 @@ function MyController(hand) {
this.entityTouchingEnter = function() { this.entityTouchingEnter = function() {
// test for intersection between controller laser and web entity plane. // test for intersection between controller laser and web entity plane.
var intersectInfo = handLaserIntersectEntity(this.grabbedThingID, var controllerLocation;
getControllerWorldLocation(this.handToController(), true)); if (this.useFingerInsteadOfStylus && this.state === STATE_ENTITY_STYLUS_TOUCHING) {
controllerLocation = getFingerWorldLocation(this.hand);
} else {
controllerLocation = getControllerWorldLocation(this.handToController(), true);
}
var intersectInfo = handLaserIntersectEntity(this.grabbedThingID, controllerLocation);
if (intersectInfo) { if (intersectInfo) {
var pointerEvent = { var pointerEvent = {
type: "Press", type: "Press",
@ -2820,8 +3140,13 @@ function MyController(hand) {
this.entityTouchingExit = function() { this.entityTouchingExit = function() {
// test for intersection between controller laser and web entity plane. // test for intersection between controller laser and web entity plane.
var intersectInfo = handLaserIntersectEntity(this.grabbedThingID, var controllerLocation;
getControllerWorldLocation(this.handToController(), true)); if (this.useFingerInsteadOfStylus && this.state === STATE_ENTITY_STYLUS_TOUCHING) {
controllerLocation = getFingerWorldLocation(this.hand);
} else {
controllerLocation = getControllerWorldLocation(this.handToController(), true);
}
var intersectInfo = handLaserIntersectEntity(this.grabbedThingID, controllerLocation);
if (intersectInfo) { if (intersectInfo) {
var pointerEvent; var pointerEvent;
if (this.deadspotExpired) { if (this.deadspotExpired) {
@ -2861,12 +3186,24 @@ function MyController(hand) {
} }
// test for intersection between controller laser and web entity plane. // test for intersection between controller laser and web entity plane.
var intersectInfo = handLaserIntersectEntity(this.grabbedThingID, var controllerLocation;
getControllerWorldLocation(this.handToController(), true)); if (this.useFingerInsteadOfStylus && this.state === STATE_ENTITY_STYLUS_TOUCHING) {
controllerLocation = getFingerWorldLocation(this.hand);
} else {
controllerLocation = getControllerWorldLocation(this.handToController(), true);
}
var intersectInfo = handLaserIntersectEntity(this.grabbedThingID, controllerLocation);
if (intersectInfo) { if (intersectInfo) {
var max;
if (this.useFingerInsteadOfStylus && this.state === STATE_ENTITY_STYLUS_TOUCHING) {
max = FINGER_TOUCH_MAX;
} else {
max = WEB_STYLUS_LENGTH / 2.0 + WEB_TOUCH_Y_OFFSET;
}
if (this.state == STATE_ENTITY_STYLUS_TOUCHING && if (this.state == STATE_ENTITY_STYLUS_TOUCHING &&
intersectInfo.distance > WEB_STYLUS_LENGTH / 2.0 + WEB_TOUCH_Y_OFFSET) { intersectInfo.distance > max) {
this.setState(STATE_OFF, "pulled away from web entity"); this.setState(STATE_OFF, "pulled away from web entity");
return; return;
} }
@ -2909,8 +3246,13 @@ function MyController(hand) {
this.overlayTouchingEnter = function () { this.overlayTouchingEnter = function () {
// Test for intersection between controller laser and Web overlay plane. // Test for intersection between controller laser and Web overlay plane.
var intersectInfo = var controllerLocation;
handLaserIntersectOverlay(this.grabbedOverlay, getControllerWorldLocation(this.handToController(), true)); if (this.useFingerInsteadOfStylus && this.state === STATE_OVERLAY_STYLUS_TOUCHING) {
controllerLocation = getFingerWorldLocation(this.hand);
} else {
controllerLocation = getControllerWorldLocation(this.handToController(), true);
}
var intersectInfo = handLaserIntersectOverlay(this.grabbedOverlay, controllerLocation);
if (intersectInfo) { if (intersectInfo) {
var pointerEvent = { var pointerEvent = {
type: "Press", type: "Press",
@ -2945,8 +3287,13 @@ function MyController(hand) {
this.overlayTouchingExit = function () { this.overlayTouchingExit = function () {
// Test for intersection between controller laser and Web overlay plane. // Test for intersection between controller laser and Web overlay plane.
var intersectInfo = var controllerLocation;
handLaserIntersectOverlay(this.grabbedOverlay, getControllerWorldLocation(this.handToController(), true)); if (this.useFingerInsteadOfStylus && this.state === STATE_OVERLAY_STYLUS_TOUCHING) {
controllerLocation = getFingerWorldLocation(this.hand);
} else {
controllerLocation = getControllerWorldLocation(this.handToController(), true);
}
var intersectInfo = handLaserIntersectOverlay(this.grabbedOverlay, controllerLocation);
if (intersectInfo) { if (intersectInfo) {
var pointerEvent; var pointerEvent;
@ -3003,12 +3350,25 @@ function MyController(hand) {
} }
// Test for intersection between controller laser and Web overlay plane. // Test for intersection between controller laser and Web overlay plane.
var intersectInfo = var controllerLocation;
handLaserIntersectOverlay(this.grabbedOverlay, getControllerWorldLocation(this.handToController(), true)); if (this.useFingerInsteadOfStylus && this.state === STATE_OVERLAY_STYLUS_TOUCHING) {
controllerLocation = getFingerWorldLocation(this.hand);
} else {
controllerLocation = getControllerWorldLocation(this.handToController(), true);
}
var intersectInfo = handLaserIntersectOverlay(this.grabbedOverlay, controllerLocation);
if (intersectInfo) { if (intersectInfo) {
if (this.state == STATE_OVERLAY_STYLUS_TOUCHING && var max, min;
intersectInfo.distance > WEB_STYLUS_LENGTH / 2.0 + WEB_TOUCH_Y_OFFSET + WEB_TOUCH_Y_TOUCH_DEADZONE_SIZE) { if (this.useFingerInsteadOfStylus && this.state === STATE_OVERLAY_STYLUS_TOUCHING) {
max = FINGER_TOUCH_MAX;
min = FINGER_TOUCH_MIN;
} else {
max = WEB_STYLUS_LENGTH / 2.0 + WEB_TOUCH_Y_OFFSET + WEB_TOUCH_Y_TOUCH_DEADZONE_SIZE;
min = WEB_STYLUS_LENGTH / 2.0 + WEB_TOUCH_TOO_CLOSE;
}
if (this.state == STATE_OVERLAY_STYLUS_TOUCHING && intersectInfo.distance > max) {
this.grabbedThingID = null; this.grabbedThingID = null;
this.setState(STATE_OFF, "pulled away from overlay"); this.setState(STATE_OFF, "pulled away from overlay");
return; return;
@ -3019,7 +3379,7 @@ function MyController(hand) {
if (this.state == STATE_OVERLAY_STYLUS_TOUCHING && if (this.state == STATE_OVERLAY_STYLUS_TOUCHING &&
!this.tabletStabbed && !this.tabletStabbed &&
intersectInfo.distance < WEB_STYLUS_LENGTH / 2.0 + WEB_TOUCH_TOO_CLOSE) { intersectInfo.distance < min) {
// they've stabbed the tablet, don't send events until they pull back // they've stabbed the tablet, don't send events until they pull back
this.tabletStabbed = true; this.tabletStabbed = true;
this.tabletStabbedPos2D = pos2D; this.tabletStabbedPos2D = pos2D;
@ -3149,9 +3509,7 @@ function MyController(hand) {
return true; return true;
} }
var controllerJointIndex = MyAvatar.getJointIndex(this.hand === RIGHT_HAND ? var controllerJointIndex = this.controllerJointIndex;
"_CONTROLLER_RIGHTHAND" :
"_CONTROLLER_LEFTHAND");
if (props.parentJointIndex == controllerJointIndex) { if (props.parentJointIndex == controllerJointIndex) {
return true; return true;
} }
@ -3177,9 +3535,7 @@ function MyController(hand) {
children = children.concat(Entities.getChildrenIDsOfJoint(AVATAR_SELF_ID, handJointIndex)); children = children.concat(Entities.getChildrenIDsOfJoint(AVATAR_SELF_ID, handJointIndex));
// find children of faux controller joint // find children of faux controller joint
var controllerJointIndex = MyAvatar.getJointIndex(this.hand === RIGHT_HAND ? var controllerJointIndex = this.controllerJointIndex;
"_CONTROLLER_RIGHTHAND" :
"_CONTROLLER_LEFTHAND");
children = children.concat(Entities.getChildrenIDsOfJoint(MyAvatar.sessionUUID, controllerJointIndex)); children = children.concat(Entities.getChildrenIDsOfJoint(MyAvatar.sessionUUID, controllerJointIndex));
children = children.concat(Entities.getChildrenIDsOfJoint(AVATAR_SELF_ID, controllerJointIndex)); children = children.concat(Entities.getChildrenIDsOfJoint(AVATAR_SELF_ID, controllerJointIndex));
@ -3191,7 +3547,8 @@ function MyController(hand) {
children = children.concat(Entities.getChildrenIDsOfJoint(AVATAR_SELF_ID, controllerCRJointIndex)); children = children.concat(Entities.getChildrenIDsOfJoint(AVATAR_SELF_ID, controllerCRJointIndex));
children.forEach(function(childID) { 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. // 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 // 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. // works around some problems that happen when more than one hand or avatar is passing something around.
@ -3287,6 +3644,7 @@ Messages.subscribe('Hifi-Hand-Disabler');
Messages.subscribe('Hifi-Hand-Grab'); Messages.subscribe('Hifi-Hand-Grab');
Messages.subscribe('Hifi-Hand-RayPick-Blacklist'); Messages.subscribe('Hifi-Hand-RayPick-Blacklist');
Messages.subscribe('Hifi-Object-Manipulation'); Messages.subscribe('Hifi-Object-Manipulation');
Messages.subscribe('Hifi-Hand-Drop');
var handleHandMessages = function(channel, message, sender) { var handleHandMessages = function(channel, message, sender) {
var data; var data;
@ -3372,6 +3730,15 @@ var handleHandMessages = function(channel, message, sender) {
} catch (e) { } catch (e) {
print("WARNING: handControllerGrab.js -- error parsing Hifi-Hand-RayPick-Blacklist message: " + message); 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();
}
} }
} }
}; };

View file

@ -56,6 +56,7 @@ selectionManager.addEventListener(function () {
lightOverlayManager.updatePositions(); lightOverlayManager.updatePositions();
}); });
const KEY_P = 80; //Key code for letter p used for Parenting hotkey.
var DEGREES_TO_RADIANS = Math.PI / 180.0; var DEGREES_TO_RADIANS = Math.PI / 180.0;
var RADIANS_TO_DEGREES = 180.0 / Math.PI; var RADIANS_TO_DEGREES = 180.0 / Math.PI;
var epsilon = 0.001; var epsilon = 0.001;
@ -843,7 +844,6 @@ function setupModelMenus() {
}); });
modelMenuAddedDelete = true; modelMenuAddedDelete = true;
} }
Menu.addMenuItem({ Menu.addMenuItem({
menuName: "Edit", menuName: "Edit",
menuItemName: "Entity List...", menuItemName: "Entity List...",
@ -851,11 +851,25 @@ function setupModelMenus() {
afterItem: "Entities", afterItem: "Entities",
grouping: "Advanced" 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({ Menu.addMenuItem({
menuName: "Edit", menuName: "Edit",
menuItemName: "Allow Selecting of Large Models", menuItemName: "Allow Selecting of Large Models",
shortcutKey: "CTRL+META+L", shortcutKey: "CTRL+META+L",
afterItem: "Entity List...", afterItem: "Unparent Entity",
isCheckable: true, isCheckable: true,
isChecked: true, isChecked: true,
grouping: "Advanced" grouping: "Advanced"
@ -958,6 +972,8 @@ function cleanupModelMenus() {
Menu.removeMenuItem("Edit", "Delete"); Menu.removeMenuItem("Edit", "Delete");
} }
Menu.removeMenuItem("Edit", "Parent Entity to Last");
Menu.removeMenuItem("Edit", "Unparent Entity");
Menu.removeMenuItem("Edit", "Entity List..."); Menu.removeMenuItem("Edit", "Entity List...");
Menu.removeMenuItem("Edit", "Allow Selecting of Large Models"); Menu.removeMenuItem("Edit", "Allow Selecting of Large Models");
Menu.removeMenuItem("Edit", "Allow Selecting of Small Models"); Menu.removeMenuItem("Edit", "Allow Selecting of Small Models");
@ -990,6 +1006,9 @@ Script.scriptEnding.connect(function () {
Overlays.deleteOverlay(importingSVOImageOverlay); Overlays.deleteOverlay(importingSVOImageOverlay);
Overlays.deleteOverlay(importingSVOTextOverlay); Overlays.deleteOverlay(importingSVOTextOverlay);
Controller.keyReleaseEvent.disconnect(keyReleaseEvent);
Controller.keyPressEvent.disconnect(keyPressEvent);
}); });
var lastOrientation = null; var lastOrientation = null;
@ -1101,7 +1120,68 @@ function recursiveDelete(entities, childrenList) {
Entities.deleteEntity(entityID); 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() { function deleteSelectedEntities() {
if (SelectionManager.hasSelection()) { if (SelectionManager.hasSelection()) {
selectedParticleEntity = 0; selectedParticleEntity = 0;
@ -1164,6 +1244,10 @@ function handeMenuEvent(menuItem) {
Entities.setLightsArePickable(Menu.isOptionChecked("Allow Selecting of Lights")); Entities.setLightsArePickable(Menu.isOptionChecked("Allow Selecting of Lights"));
} else if (menuItem === "Delete") { } else if (menuItem === "Delete") {
deleteSelectedEntities(); deleteSelectedEntities();
} else if (menuItem === "Parent Entity to Last") {
parentSelectedEntities();
} else if (menuItem === "Unparent Entity") {
unparentSelectedEntities();
} else if (menuItem === "Export Entities") { } else if (menuItem === "Export Entities") {
if (!selectionManager.hasSelection()) { if (!selectionManager.hasSelection()) {
Window.notifyEditError("No entities have been selected."); Window.notifyEditError("No entities have been selected.");
@ -1289,13 +1373,12 @@ Window.svoImportRequested.connect(importSVO);
Menu.menuItemEvent.connect(handeMenuEvent); Menu.menuItemEvent.connect(handeMenuEvent);
Controller.keyPressEvent.connect(function (event) { var keyPressEvent = function (event) {
if (isActive) { if (isActive) {
cameraManager.keyPressEvent(event); cameraManager.keyPressEvent(event);
} }
}); };
var keyReleaseEvent = function (event) {
Controller.keyReleaseEvent.connect(function (event) {
if (isActive) { if (isActive) {
cameraManager.keyReleaseEvent(event); cameraManager.keyReleaseEvent(event);
} }
@ -1329,8 +1412,16 @@ Controller.keyReleaseEvent.connect(function (event) {
}); });
grid.setPosition(newPosition); 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) { function recursiveAdd(newParentID, parentData) {
var children = parentData.children; var children = parentData.children;
@ -1580,6 +1671,10 @@ var PropertiesTool = function (opts) {
} }
pushCommandForSelections(); pushCommandForSelections();
selectionManager._update(); selectionManager._update();
} else if(data.type === 'parent') {
parentSelectedEntities();
} else if(data.type === 'unparent') {
unparentSelectedEntities();
} else if(data.type === 'saveUserData'){ } else if(data.type === 'saveUserData'){
//the event bridge and json parsing handle our avatar id string differently. //the event bridge and json parsing handle our avatar id string differently.
var actualID = data.id.split('"')[1]; var actualID = data.id.split('"')[1];
@ -1837,6 +1932,9 @@ var PopupMenu = function () {
for (var i = 0; i < overlays.length; i++) { for (var i = 0; i < overlays.length; i++) {
Overlays.deleteOverlay(overlays[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); Controller.mousePressEvent.connect(self.mousePressEvent);
@ -1864,7 +1962,11 @@ var particleExplorerTool = new ParticleExplorerTool();
var selectedParticleEntity = 0; var selectedParticleEntity = 0;
entityListTool.webView.webEventReceived.connect(function (data) { entityListTool.webView.webEventReceived.connect(function (data) {
data = JSON.parse(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; var ids = data.entityIds;
if (ids.length === 1) { if (ids.length === 1) {
if (Entities.getEntityProperties(ids[0], "type").type === "ParticleEffect") { if (Entities.getEntityProperties(ids[0], "type").type === "ParticleEffect") {

View file

@ -89,6 +89,7 @@
</tr> </tr>
</tfoot> </tfoot>
</table> </table>
<div id="no-entities"> <div id="no-entities">
No entities found <span id="no-entities-in-view">in view</span> within a <span id="no-entities-radius">100</span> meter radius. Try moving to a different location and refreshing. No entities found <span id="no-entities-in-view">in view</span> within a <span id="no-entities-radius">100</span> meter radius. Try moving to a different location and refreshing.
</div> </div>

View file

@ -61,7 +61,7 @@
<label for="property-description">Description</label> <label for="property-description">Description</label>
<input type="text" id="property-description"> <input type="text" id="property-description">
</div> </div>
<div class="property textarea"> <div class="property textarea">
<label for="property-user-data">User data</label> <label for="property-user-data">User data</label>
<br> <br>
@ -295,12 +295,29 @@
<input type="checkbox" id="property-wants-trigger"> <input type="checkbox" id="property-wants-trigger">
<label for="property-wants-trigger">Triggerable</label> <label for="property-wants-trigger">Triggerable</label>
</div> </div>
<div class="property checkbox">
<input type="checkbox" id="property-cloneable">
<label for="property-cloneable">Cloneable</label>
</div>
<div class="property checkbox"> <div class="property checkbox">
<input type="checkbox" id="property-ignore-ik"> <input type="checkbox" id="property-ignore-ik">
<label for="property-ignore-ik">Ignore inverse kinematics</label> <label for="property-ignore-ik">Ignore inverse kinematics</label>
</div> </div>
</div> </div>
</div> </div>
<div class="column" id="group-cloneable-group" style="display:none;">
<div class="sub-section-header">
<span>Cloneable Settings</span>
</div>
<div class="cloneable-group property gen">
<div><label>Clone Lifetime</label><input type="number" data-user-data-type="cloneLifetime" id="property-cloneable-lifetime"></div>
<div><label>Clone Limit</label><input type="number" data-user-data-type="cloneLimit" id="property-cloneable-limit"></div>
<div class="property checkbox">
<input type="checkbox" id="property-cloneable-dynamic">
<label for="property-cloneable-dynamic">Clone Dynamic</label>
</div>
</div>
</div>
</div> </div>
<hr class="behavior-group" /> <hr class="behavior-group" />
<div class="behavior-group property url "> <div class="behavior-group property url ">

View file

@ -19,6 +19,7 @@ const VISIBLE_GLYPH = "&#xe007;";
const TRANSPARENCY_GLYPH = "&#xe00b;"; const TRANSPARENCY_GLYPH = "&#xe00b;";
const SCRIPT_GLYPH = "k"; const SCRIPT_GLYPH = "k";
const DELETE = 46; // Key code for the delete key. 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. const MAX_ITEMS = Number.MAX_VALUE; // Used to set the max length of the list of discovered entities.
debugPrint = function (message) { debugPrint = function (message) {
@ -26,7 +27,7 @@ debugPrint = function (message) {
}; };
function loaded() { function loaded() {
openEventBridge(function() { openEventBridge(function() {
entityList = new List('entity-list', { valueNames: ['name', 'type', 'url', 'locked', 'visible'], page: MAX_ITEMS}); entityList = new List('entity-list', { valueNames: ['name', 'type', 'url', 'locked', 'visible'], page: MAX_ITEMS});
entityList.clear(); entityList.clear();
elEntityTable = document.getElementById("entity-table"); elEntityTable = document.getElementById("entity-table");
@ -48,7 +49,7 @@ function loaded() {
elNoEntitiesInView = document.getElementById("no-entities-in-view"); elNoEntitiesInView = document.getElementById("no-entities-in-view");
elNoEntitiesRadius = document.getElementById("no-entities-radius"); elNoEntitiesRadius = document.getElementById("no-entities-radius");
elEntityTableScroll = document.getElementById("entity-table-scroll"); elEntityTableScroll = document.getElementById("entity-table-scroll");
document.getElementById("entity-name").onclick = function() { document.getElementById("entity-name").onclick = function() {
setSortColumn('name'); setSortColumn('name');
}; };
@ -90,7 +91,7 @@ function loaded() {
selection = selection.concat(selectedEntities); selection = selection.concat(selectedEntities);
} else if (clickEvent.shiftKey && selectedEntities.length > 0) { } else if (clickEvent.shiftKey && selectedEntities.length > 0) {
var previousItemFound = -1; var previousItemFound = -1;
var clickedItemFound = -1; var clickedItemFound = -1;
for (var entity in entityList.visibleItems) { for (var entity in entityList.visibleItems) {
if (clickedItemFound === -1 && this.dataset.entityId == entityList.visibleItems[entity].values().id) { if (clickedItemFound === -1 && this.dataset.entityId == entityList.visibleItems[entity].values().id) {
clickedItemFound = entity; clickedItemFound = entity;
@ -113,11 +114,11 @@ function loaded() {
selection = selection.concat(betweenItems, selectedEntities); selection = selection.concat(betweenItems, selectedEntities);
} }
} }
selectedEntities = selection; selectedEntities = selection;
this.className = 'selected'; this.className = 'selected';
EventBridge.emitWebEvent(JSON.stringify({ EventBridge.emitWebEvent(JSON.stringify({
type: "selectionUpdate", type: "selectionUpdate",
focus: false, focus: false,
@ -126,7 +127,7 @@ function loaded() {
refreshFooter(); refreshFooter();
} }
function onRowDoubleClicked() { function onRowDoubleClicked() {
EventBridge.emitWebEvent(JSON.stringify({ EventBridge.emitWebEvent(JSON.stringify({
type: "selectionUpdate", type: "selectionUpdate",
@ -134,7 +135,7 @@ function loaded() {
entityIds: [this.dataset.entityId], entityIds: [this.dataset.entityId],
})); }));
} }
const BYTES_PER_MEGABYTE = 1024 * 1024; const BYTES_PER_MEGABYTE = 1024 * 1024;
function decimalMegabytes(number) { function decimalMegabytes(number) {
@ -173,7 +174,7 @@ function loaded() {
currentElement.onclick = onRowClicked; currentElement.onclick = onRowClicked;
currentElement.ondblclick = onRowDoubleClicked; currentElement.ondblclick = onRowDoubleClicked;
}); });
if (refreshEntityListTimer) { if (refreshEntityListTimer) {
clearTimeout(refreshEntityListTimer); clearTimeout(refreshEntityListTimer);
} }
@ -183,13 +184,13 @@ function loaded() {
item.values({ name: name, url: filename, locked: locked, visible: visible }); item.values({ name: name, url: filename, locked: locked, visible: visible });
} }
} }
function clearEntities() { function clearEntities() {
entities = {}; entities = {};
entityList.clear(); entityList.clear();
refreshFooter(); refreshFooter();
} }
var elSortOrder = { var elSortOrder = {
name: document.querySelector('#entity-name .sort-order'), name: document.querySelector('#entity-name .sort-order'),
type: document.querySelector('#entity-type .sort-order'), type: document.querySelector('#entity-type .sort-order'),
@ -215,12 +216,12 @@ function loaded() {
entityList.sort(currentSortColumn, { order: currentSortOrder }); entityList.sort(currentSortColumn, { order: currentSortOrder });
} }
setSortColumn('type'); setSortColumn('type');
function refreshEntities() { function refreshEntities() {
clearEntities(); clearEntities();
EventBridge.emitWebEvent(JSON.stringify({ type: 'refresh' })); EventBridge.emitWebEvent(JSON.stringify({ type: 'refresh' }));
} }
function refreshFooter() { function refreshFooter() {
if (selectedEntities.length > 1) { if (selectedEntities.length > 1) {
elFooter.firstChild.nodeValue = selectedEntities.length + " entities selected"; elFooter.firstChild.nodeValue = selectedEntities.length + " entities selected";
@ -239,7 +240,7 @@ function loaded() {
entityList.search(elFilter.value); entityList.search(elFilter.value);
refreshFooter(); refreshFooter();
} }
function updateSelectedEntities(selectedIDs) { function updateSelectedEntities(selectedIDs) {
var notFound = false; var notFound = false;
for (var id in entities) { for (var id in entities) {
@ -262,7 +263,7 @@ function loaded() {
return notFound; return notFound;
} }
elRefresh.onclick = function() { elRefresh.onclick = function() {
refreshEntities(); refreshEntities();
} }
@ -282,7 +283,7 @@ function loaded() {
EventBridge.emitWebEvent(JSON.stringify({ type: 'delete' })); EventBridge.emitWebEvent(JSON.stringify({ type: 'delete' }));
refreshEntities(); refreshEntities();
} }
document.addEventListener("keydown", function (keyDownEvent) { document.addEventListener("keydown", function (keyDownEvent) {
if (keyDownEvent.target.nodeName === "INPUT") { if (keyDownEvent.target.nodeName === "INPUT") {
return; return;
@ -292,8 +293,15 @@ function loaded() {
EventBridge.emitWebEvent(JSON.stringify({ type: 'delete' })); EventBridge.emitWebEvent(JSON.stringify({ type: 'delete' }));
refreshEntities(); 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); }, false);
var isFilterInView = false; var isFilterInView = false;
var FILTER_IN_VIEW_ATTRIBUTE = "pressed"; var FILTER_IN_VIEW_ATTRIBUTE = "pressed";
elNoEntitiesInView.style.display = "none"; elNoEntitiesInView.style.display = "none";
@ -320,7 +328,7 @@ function loaded() {
if (window.EventBridge !== undefined) { if (window.EventBridge !== undefined) {
EventBridge.scriptEventReceived.connect(function(data) { EventBridge.scriptEventReceived.connect(function(data) {
data = JSON.parse(data); data = JSON.parse(data);
if (data.type === "clearEntityList") { if (data.type === "clearEntityList") {
clearEntities(); clearEntities();
} else if (data.type == "selectionUpdate") { } else if (data.type == "selectionUpdate") {
@ -426,4 +434,3 @@ function loaded() {
event.preventDefault(); event.preventDefault();
}, false); }, false);
} }

View file

@ -24,9 +24,10 @@ var ICON_FOR_TYPE = {
} }
var EDITOR_TIMEOUT_DURATION = 1500; var EDITOR_TIMEOUT_DURATION = 1500;
const KEY_P = 80; //Key code for letter p used for Parenting hotkey.
var colorPickers = []; var colorPickers = [];
var lastEntityID = null; var lastEntityID = null;
debugPrint = function(message) { debugPrint = function(message) {
EventBridge.emitWebEvent( EventBridge.emitWebEvent(
JSON.stringify({ JSON.stringify({
@ -273,7 +274,7 @@ function updateCheckedSubProperty(propertyName, propertyValue, subPropertyElemen
propertyValue += subPropertyString + ','; propertyValue += subPropertyString + ',';
} }
} else { } else {
// We've unchecked, so remove // We've unchecked, so remove
propertyValue = propertyValue.replace(subPropertyString + ",", ""); propertyValue = propertyValue.replace(subPropertyString + ",", "");
} }
@ -323,13 +324,9 @@ function setUserDataFromEditor(noUpdate) {
}) })
); );
} }
} }
} }
function multiDataUpdater(groupName, updateKeyPair, userDataElement, defaults) {
function userDataChanger(groupName, keyName, checkBoxElement, userDataElement, defaultValue) {
var properties = {}; var properties = {};
var parsedData = {}; var parsedData = {};
try { try {
@ -339,17 +336,31 @@ function userDataChanger(groupName, keyName, checkBoxElement, userDataElement, d
} else { } else {
parsedData = JSON.parse(userDataElement.value); parsedData = JSON.parse(userDataElement.value);
} }
} catch (e) {} } catch (e) {}
if (!(groupName in parsedData)) { if (!(groupName in parsedData)) {
parsedData[groupName] = {} parsedData[groupName] = {}
} }
delete parsedData[groupName][keyName]; var keys = Object.keys(updateKeyPair);
if (checkBoxElement.checked !== defaultValue) { keys.forEach(function (key) {
parsedData[groupName][keyName] = checkBoxElement.checked; 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) { if (Object.keys(parsedData[groupName]).length == 0) {
delete parsedData[groupName]; delete parsedData[groupName];
} }
@ -368,6 +379,12 @@ function userDataChanger(groupName, keyName, checkBoxElement, userDataElement, d
properties: properties, 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) { function setTextareaScrolling(element) {
@ -521,6 +538,7 @@ function unbindAllInputs() {
function loaded() { function loaded() {
openEventBridge(function() { openEventBridge(function() {
var allSections = []; var allSections = [];
var elID = document.getElementById("property-id"); var elID = document.getElementById("property-id");
var elType = document.getElementById("property-type"); var elType = document.getElementById("property-type");
@ -584,6 +602,13 @@ function loaded() {
var elCollisionSoundURL = document.getElementById("property-collision-sound-url"); var elCollisionSoundURL = document.getElementById("property-collision-sound-url");
var elGrabbable = document.getElementById("property-grabbable"); 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 elWantsTrigger = document.getElementById("property-wants-trigger");
var elIgnoreIK = document.getElementById("property-ignore-ik"); var elIgnoreIK = document.getElementById("property-ignore-ik");
@ -780,7 +805,7 @@ function loaded() {
if (lastEntityID !== '"' + properties.id + '"' && lastEntityID !== null && editor !== null) { if (lastEntityID !== '"' + properties.id + '"' && lastEntityID !== null && editor !== null) {
saveJSONUserData(true); 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 + '"'; lastEntityID = '"' + properties.id + '"';
elID.innerHTML = properties.id; elID.innerHTML = properties.id;
@ -847,8 +872,16 @@ function loaded() {
elCollideOtherAvatar.checked = properties.collidesWith.indexOf("otherAvatar") > -1; elCollideOtherAvatar.checked = properties.collidesWith.indexOf("otherAvatar") > -1;
elGrabbable.checked = properties.dynamic; elGrabbable.checked = properties.dynamic;
elWantsTrigger.checked = false; elWantsTrigger.checked = false;
elIgnoreIK.checked = true; elIgnoreIK.checked = true;
elCloneable.checked = false;
elCloneableDynamic.checked = false;
elCloneableGroup.style.display = elCloneable.checked ? "block": "none";
elCloneableLimit.value = 10;
elCloneableLifetime.value = 300;
var parsedUserData = {} var parsedUserData = {}
try { try {
parsedUserData = JSON.parse(properties.userData); parsedUserData = JSON.parse(properties.userData);
@ -863,8 +896,25 @@ function loaded() {
if ("ignoreIK" in parsedUserData["grabbableKey"]) { if ("ignoreIK" in parsedUserData["grabbableKey"]) {
elIgnoreIK.checked = parsedUserData["grabbableKey"].ignoreIK; 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; elCollisionSoundURL.value = properties.collisionSoundURL;
elLifetime.value = properties.lifetime; elLifetime.value = properties.lifetime;
@ -1154,8 +1204,38 @@ function loaded() {
}); });
elGrabbable.addEventListener('change', function() { elGrabbable.addEventListener('change', function() {
if(elCloneable.checked) {
elGrabbable.checked = false;
}
userDataChanger("grabbableKey", "grabbable", elGrabbable, elUserData, properties.dynamic); 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() { elWantsTrigger.addEventListener('change', function() {
userDataChanger("grabbableKey", "wantsTrigger", elWantsTrigger, elUserData, false); userDataChanger("grabbableKey", "wantsTrigger", elWantsTrigger, elUserData, false);
}); });
@ -1390,7 +1470,7 @@ function loaded() {
elZoneFlyingAllowed.addEventListener('change', createEmitCheckedPropertyUpdateFunction('flyingAllowed')); elZoneFlyingAllowed.addEventListener('change', createEmitCheckedPropertyUpdateFunction('flyingAllowed'));
elZoneGhostingAllowed.addEventListener('change', createEmitCheckedPropertyUpdateFunction('ghostingAllowed')); elZoneGhostingAllowed.addEventListener('change', createEmitCheckedPropertyUpdateFunction('ghostingAllowed'));
elZoneFilterURL.addEventListener('change', createEmitTextPropertyUpdateFunction('filterURL')); elZoneFilterURL.addEventListener('change', createEmitTextPropertyUpdateFunction('filterURL'));
var voxelVolumeSizeChangeFunction = createEmitVec3PropertyUpdateFunction( var voxelVolumeSizeChangeFunction = createEmitVec3PropertyUpdateFunction(
'voxelVolumeSize', elVoxelVolumeSizeX, elVoxelVolumeSizeY, elVoxelVolumeSizeZ); 'voxelVolumeSize', elVoxelVolumeSizeX, elVoxelVolumeSizeY, elVoxelVolumeSizeZ);
elVoxelVolumeSizeX.addEventListener('change', voxelVolumeSizeChangeFunction); 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() { window.onblur = function() {
// Fake a change event // Fake a change event
var ev = document.createEvent("HTMLEvents"); var ev = document.createEvent("HTMLEvents");

View file

@ -6,6 +6,8 @@
// Distributed under the Apache License, Version 2.0. // Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // 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() { function loaded() {
openEventBridge(function() { openEventBridge(function() {
elPosY = document.getElementById("horiz-y"); elPosY = document.getElementById("horiz-y");
@ -131,10 +133,17 @@ function loaded() {
EventBridge.emitWebEvent(JSON.stringify({ type: 'init' })); 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 // 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) { document.addEventListener("contextmenu", function (event) {
event.preventDefault(); event.preventDefault();
}, false); }, false);
} }

View file

@ -1170,14 +1170,14 @@ SelectionDisplay = (function() {
// determine which bottom corner we are closest to // determine which bottom corner we are closest to
/*------------------------------ /*------------------------------
example: example:
BRF +--------+ BLF BRF +--------+ BLF
| | | |
| | | |
BRN +--------+ BLN BRN +--------+ BLN
* *
------------------------------*/ ------------------------------*/
var cameraPosition = Camera.getPosition(); var cameraPosition = Camera.getPosition();
@ -2189,8 +2189,12 @@ SelectionDisplay = (function() {
offset = Vec3.multiplyQbyV(properties.rotation, offset); offset = Vec3.multiplyQbyV(properties.rotation, offset);
var boxPosition = Vec3.sum(properties.position, 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], { Overlays.editOverlay(selectionBoxes[i], {
position: boxPosition, position: boxPosition,
color: color,
rotation: properties.rotation, rotation: properties.rotation,
dimensions: properties.dimensions, dimensions: properties.dimensions,
visible: true, visible: true,
@ -2395,7 +2399,7 @@ SelectionDisplay = (function() {
if (wantDebug) { if (wantDebug) {
print("Start Elevation: " + translateXZTool.startingElevation + ", elevation: " + elevation); 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)) { (translateXZTool.startingElevation < 0.0 && elevation > -MIN_ELEVATION)) {
if (wantDebug) { if (wantDebug) {
print("too close to horizon!"); print("too close to horizon!");
@ -3857,7 +3861,7 @@ SelectionDisplay = (function() {
}; };
that.mousePressEvent = function(event) { that.mousePressEvent = function(event) {
var wantDebug = false; var wantDebug = false;
if (!event.isLeftButton && !that.triggered) { if (!event.isLeftButton && !that.triggered) {
// if another mouse button than left is pressed ignore it // if another mouse button than left is pressed ignore it
return false; return false;
@ -3889,7 +3893,7 @@ SelectionDisplay = (function() {
if (result.intersects) { if (result.intersects) {
if (wantDebug) { if (wantDebug) {
print("something intersects... "); print("something intersects... ");
print(" result.overlayID:" + result.overlayID + "[" + overlayNames[result.overlayID] + "]"); print(" result.overlayID:" + result.overlayID + "[" + overlayNames[result.overlayID] + "]");
@ -3989,7 +3993,7 @@ SelectionDisplay = (function() {
if (wantDebug) { if (wantDebug) {
print("rotate handle case..."); print("rotate handle case...");
} }
// After testing our stretch handles, then check out rotate handles // After testing our stretch handles, then check out rotate handles
Overlays.editOverlay(yawHandle, { Overlays.editOverlay(yawHandle, {
@ -4211,7 +4215,7 @@ SelectionDisplay = (function() {
case selectionBox: case selectionBox:
activeTool = translateXZTool; activeTool = translateXZTool;
translateXZTool.pickPlanePosition = result.intersection; 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); SelectionManager.worldDimensions.z);
if (wantDebug) { if (wantDebug) {
print("longest dimension: " + translateXZTool.greatestDimension); print("longest dimension: " + translateXZTool.greatestDimension);
@ -4220,7 +4224,7 @@ SelectionDisplay = (function() {
translateXZTool.startingElevation = translateXZTool.elevation(pickRay.origin, translateXZTool.pickPlanePosition); translateXZTool.startingElevation = translateXZTool.elevation(pickRay.origin, translateXZTool.pickPlanePosition);
print(" starting elevation: " + translateXZTool.startingElevation); print(" starting elevation: " + translateXZTool.startingElevation);
} }
mode = translateXZTool.mode; mode = translateXZTool.mode;
activeTool.onBegin(event); activeTool.onBegin(event);
somethingClicked = 'selectionBox'; somethingClicked = 'selectionBox';

View file

@ -521,6 +521,9 @@ function onEditError(msg) {
createNotification(wordWrap(msg), NotificationType.EDIT_ERROR); 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) { function onSnapshotTaken(pathStillSnapshot, pathAnimatedSnapshot, notify) {
if (notify) { if (notify) {
@ -637,6 +640,7 @@ Window.domainConnectionRefused.connect(onDomainConnectionRefused);
Window.snapshotTaken.connect(onSnapshotTaken); Window.snapshotTaken.connect(onSnapshotTaken);
Window.processingGif.connect(processingGif); Window.processingGif.connect(processingGif);
Window.notifyEditError = onEditError; Window.notifyEditError = onEditError;
Window.notify = onNotify;
setup(); setup();

View file

@ -207,6 +207,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. function fromQml(message) { // messages are {method, params}, like json-rpc. See also sendToQml.
var data; var data;
switch (message.method) { switch (message.method) {
@ -246,18 +257,6 @@ function fromQml(message) { // messages are {method, params}, like json-rpc. See
populateUserList(message.params.selected); populateUserList(message.params.selected);
UserActivityLogger.palAction("refresh", ""); UserActivityLogger.palAction("refresh", "");
break; break;
case 'updateGain':
data = message.params;
if (data['isReleased']) {
// isReleased=true happens once at the end of a cycle of dragging
// the slider about, but with same gain as last isReleased=false so
// we don't set the gain in that case, and only here do we want to
// send an analytic event.
UserActivityLogger.palAction("avatar_gain_changed", data['sessionId']);
} else {
Users.setAvatarGain(data['sessionId'], data['gain']);
}
break;
case 'displayNameUpdate': case 'displayNameUpdate':
if (MyAvatar.displayName !== message.params) { if (MyAvatar.displayName !== message.params) {
MyAvatar.displayName = message.params; MyAvatar.displayName = message.params;
@ -324,6 +323,7 @@ function populateUserList(selectData) {
userName: '', userName: '',
sessionId: id || '', sessionId: id || '',
audioLevel: 0.0, audioLevel: 0.0,
avgAudioLevel: 0.0,
admin: false, admin: false,
personalMute: !!id && Users.getPersonalMuteStatus(id), // expects proper boolean, not null personalMute: !!id && Users.getPersonalMuteStatus(id), // expects proper boolean, not null
ignore: !!id && Users.getIgnoreStatus(id) // ditto ignore: !!id && Users.getIgnoreStatus(id) // ditto
@ -617,41 +617,54 @@ function receiveMessage(channel, messageString, senderID) {
} }
} }
var AVERAGING_RATIO = 0.05; var AVERAGING_RATIO = 0.05;
var LOUDNESS_FLOOR = 11.0; var LOUDNESS_FLOOR = 11.0;
var LOUDNESS_SCALE = 2.8 / 5.0; var LOUDNESS_SCALE = 2.8 / 5.0;
var LOG2 = Math.log(2.0); var LOG2 = Math.log(2.0);
var AUDIO_PEAK_DECAY = 0.02;
var myData = {}; // we're not includied in ExtendedOverlay.get. 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) { function getAudioLevel(id) {
// the VU meter should work similarly to the one in AvatarInputs: log scale, exponentially averaged // 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 // 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). // of updating (the latter for efficiency too).
var avatar = AvatarList.getAvatar(id); var avatar = AvatarList.getAvatar(id);
var audioLevel = 0.0; var audioLevel = 0.0;
var avgAudioLevel = 0.0;
var data = id ? ExtendedOverlay.get(id) : myData; var data = id ? ExtendedOverlay.get(id) : myData;
if (!data) { if (data) {
return audioLevel;
}
// we will do exponential moving average by taking some the last loudness and averaging // 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); 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 // 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). // natural log, so to get log base 2, just divide by ln(2).
var logLevel = Math.log(data.accumulatedLevel + 1) / LOG2; audioLevel = scaleAudio(Math.log(data.accumulatedLevel + 1) / LOG2);
if (logLevel <= LOUDNESS_FLOOR) { // decay avgAudioLevel
audioLevel = logLevel / LOUDNESS_FLOOR * LOUDNESS_SCALE; avgAudioLevel = Math.max((1-AUDIO_PEAK_DECAY) * (data.avgAudioLevel || 0), audioLevel);
} else {
audioLevel = (logLevel - (LOUDNESS_FLOOR - 1.0)) * LOUDNESS_SCALE; 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) { return [audioLevel, avgAudioLevel];
audioLevel = 1;
}
data.audioLevel = audioLevel;
return audioLevel;
} }
function createAudioInterval(interval) { function createAudioInterval(interval) {

View file

@ -21,6 +21,7 @@ exports.handlers = {
'../../libraries/networking/src', '../../libraries/networking/src',
'../../libraries/animation/src', '../../libraries/animation/src',
'../../libraries/entities/src', '../../libraries/entities/src',
'../../libraries/shared/src'
]; ];
var exts = ['.h', '.cpp']; var exts = ['.h', '.cpp'];