diff --git a/.eslintrc.js b/.eslintrc.js index c708decc51..9635142d1a 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -17,6 +17,7 @@ module.exports = { "Clipboard": false, "Controller": false, "DialogsManager": false, + "DebugDraw": false, "Entities": false, "FaceTracker": false, "GlobalServices": false, diff --git a/BUILD_WIN.md b/BUILD_WIN.md index 45373d3093..e37bf27503 100644 --- a/BUILD_WIN.md +++ b/BUILD_WIN.md @@ -1,104 +1,81 @@ -Please read the [general build guide](BUILD.md) for information on dependencies required for all platforms. Only Windows specific instructions are found in this file. +This is a stand-alone guide for creating your first High Fidelity build for Windows 64-bit. -Interface can be built as 32 or 64 bit. +###Step 1. Installing Visual Studio 2013 -###Visual Studio 2013 +If you don't already have the Community or Professional edition of Visual Studio 2013, download and install [Visual Studio Community 2013](https://www.visualstudio.com/en-us/news/releasenotes/vs2013-community-vs). You do not need to install any of the optional components when going through the installer. -You can use the Community or Professional editions of Visual Studio 2013. +Note: Newer versions of Visual Studio are not yet compatible. -You can start a Visual Studio 2013 command prompt using the shortcut provided in the Visual Studio Tools folder installed as part of Visual Studio 2013. +###Step 2. Installing CMake -Or you can start a regular command prompt and then run: +Download and install the CMake 3.8.0-rc2 "win64-x64 Installer" from the [CMake Website](https://cmake.org/download/). Make sure "Add CMake to system PATH for all users" is checked when going through the installer. - "%VS120COMNTOOLS%\vsvars32.bat" +###Step 3. Installing Qt -####Windows SDK 8.1 +Download and install the [Qt 5.6.1 Installer](https://download.qt.io/official_releases/qt/5.6/5.6.1-1/qt-opensource-windows-x86-msvc2013_64-5.6.1-1.exe). Please note that the download file is large (850MB) and may take some time. -If using Visual Studio 2013 and building as a Visual Studio 2013 project you need the Windows 8 SDK which you should already have as part of installing Visual Studio 2013. You should be able to see it at `C:\Program Files (x86)\Windows Kits\8.1\Lib\winv6.3\um\x86`. +Make sure to select all components when going through the installer. -####nmake +###Step 4. Setting Qt Environment Variable -Some of the external projects may require nmake to compile and install. If it is not installed at the location listed below, please ensure that it is in your PATH so CMake can find it when required. +Go to "Control Panel > System > Advanced System Settings > Environment Variables > New..." (or search “Environment Variables” in Start Search). +* Set "Variable name": QT_CMAKE_PREFIX_PATH +* Set "Variable value": `C:\Qt\Qt5.6.1\5.6\msvc2013_64\lib\cmake` -We expect nmake.exe to be located at the following path. +###Step 5. Installing OpenSSL - C:\Program Files (x86)\Microsoft Visual Studio 12.0\VC\bin +Download and install the "Win64 OpenSSL v1.0.2k" Installer from [this website](https://slproweb.com/products/Win32OpenSSL.html). -###Qt -You can use the online installer or the offline installer. If you use the offline installer, be sure to select the "OpenGL" version. - -* [Download the online installer](http://www.qt.io/download-open-source/#section-2) - * When it asks you to select components, ONLY select one of the following, 32- or 64-bit to match your build preference: - * Qt > Qt 5.6.1 > **msvc2013 32-bit** - * Qt > Qt 5.6.1 > **msvc2013 64-bit** - -* Download the offline installer, 32- or 64-bit to match your build preference: - * [32-bit](https://download.qt.io/official_releases/qt/5.6/5.6.1-1/qt-opensource-windows-x86-msvc2013-5.6.1-1.exe) - * [64-bit](https://download.qt.io/official_releases/qt/5.6/5.6.1-1/qt-opensource-windows-x86-msvc2013_64-5.6.1-1.exe) - -Once Qt is installed, you need to manually configure the following: -* Set the QT_CMAKE_PREFIX_PATH environment variable to your `Qt\5.6.1\msvc2013\lib\cmake` or `Qt\5.6.1\msvc2013_64\lib\cmake` directory. - * You can set an environment variable from Control Panel > System > Advanced System Settings > Environment Variables > New - -###External Libraries - -All libraries should be 32- or 64-bit to match your build preference. - -CMake will need to know where the headers and libraries for required external dependencies are. - -We use CMake's `fixup_bundle` to find the DLLs all of our executable targets require, and then copy them beside the executable in a post-build step. If `fixup_bundle` is having problems finding a DLL, you can fix it manually on your end by adding the folder containing that DLL to your path. Let us know which DLL CMake had trouble finding, as it is possible a tweak to our CMake files is required. - -The recommended route for CMake to find the external dependencies is to place all of the dependencies in one folder and set one ENV variable - HIFI_LIB_DIR. That ENV variable should point to a directory with the following structure: - - root_lib_dir - -> openssl - -> bin - -> include - -> lib - -For many of the external libraries where precompiled binaries are readily available you should be able to simply copy the extracted folder that you get from the download links provided at the top of the guide. Otherwise you may need to build from source and install the built product to this directory. The `root_lib_dir` in the above example can be wherever you choose on your system - as long as the environment variable HIFI_LIB_DIR is set to it. From here on, whenever you see %HIFI_LIB_DIR% you should substitute the directory that you chose. - -####OpenSSL - -Qt will use OpenSSL if it's available, but it doesn't install it, so you must install it separately. - -Your system may already have several versions of the OpenSSL DLL's (ssleay32.dll, libeay32.dll) lying around, but they may be the wrong version. If these DLL's are in the PATH then QT will try to use them, and if they're the wrong version then you will see the following errors in the console: - - QSslSocket: cannot resolve TLSv1_1_client_method - QSslSocket: cannot resolve TLSv1_2_client_method - QSslSocket: cannot resolve TLSv1_1_server_method - QSslSocket: cannot resolve TLSv1_2_server_method - QSslSocket: cannot resolve SSL_select_next_proto - QSslSocket: cannot resolve SSL_CTX_set_next_proto_select_cb - QSslSocket: cannot resolve SSL_get0_next_proto_negotiated - -To prevent these problems, install OpenSSL yourself. Download one of the following binary packages [from this website](https://slproweb.com/products/Win32OpenSSL.html): -* Win32 OpenSSL v1.0.1q -* Win64 OpenSSL v1.0.1q - -Install OpenSSL into the Windows system directory, to make sure that Qt uses the version that you've just installed, and not some other version. - -###Build High Fidelity using Visual Studio -Follow the same build steps from the CMake section of [BUILD.md](BUILD.md), but pass a different generator to CMake. - -For 32-bit builds: - - cmake .. -G "Visual Studio 12" - -For 64-bit builds: +###Step 6. Running CMake to Generate Build Files +Run Command Prompt from Start and run the following commands: + cd "%HIFI_DIR%" + mkdir build + cd build cmake .. -G "Visual Studio 12 Win64" + +Where %HIFI_DIR% is the directory for the highfidelity repository. -Open %HIFI_DIR%\build\hifi.sln and compile. +###Step 7. Making a Build -###Running Interface -If you need to debug Interface, you can run interface from within Visual Studio (see the section below). You can also run Interface by launching it from command line or File Explorer from %HIFI_DIR%\build\interface\Debug\interface.exe +Open '%HIFI_DIR%\build\hifi.sln' using Visual Studio. -###Debugging Interface -* In the Solution Explorer, right click interface and click Set as StartUp Project -* Set the "Working Directory" for the Interface debugging sessions to the Debug output directory so that your application can load resources. Do this: right click interface and click Properties, choose Debugging from Configuration Properties, set Working Directory to .\Debug -* Now you can run and debug interface through Visual Studio +Change the Solution Configuration (next to the green play button) from "Debug" to "Release" for best performance. -For better performance when running debug builds, set the environment variable ```_NO_DEBUG_HEAP``` to ```1``` +Run Build > Build Solution. + +###Step 8. Testing Interface + +Create another environment variable (see Step #4) +* Set "Variable name": _NO_DEBUG_HEAP +* Set "Variable value": 1 + +In Visual Studio, right+click "interface" under the Apps folder in Solution Explorer and select "Set as Startup Project". Run Debug > Start Debugging. + +Now, you should have a full build of High Fidelity and be able to run the Interface using Visual Studio. Please check our [Docs](https://wiki.highfidelity.com/wiki/Main_Page) for more information regarding the programming workflow. + +Note: You can also run Interface by launching it from command line or File Explorer from %HIFI_DIR%\build\interface\Release\interface.exe + +###Troubleshooting + +For any problems after Step #6, first try this: +* Delete your locally cloned copy of the highfidelity repository +* Restart your computer +* Redownload the [repository](https://github.com/highfidelity/hifi) +* Restart directions from Step #6 + +####CMake gives you the same error message repeatedly after the build fails + +Remove `CMakeCache.txt` found in the '%HIFI_DIR%\build' directory + +####nmake cannot be found + +Make sure nmake.exe is located at the following path: + C:\Program Files (x86)\Microsoft Visual Studio 12.0\VC\bin + +If not, add the directory where nmake is located to the PATH environment variable. + +####Qt is throwing an error + +Make sure you have the correct version (5.6.1-1) installed and 'QT_CMAKE_PREFIX_PATH' environment variable is set correctly. -http://preshing.com/20110717/the-windows-heap-is-slow-when-launched-from-the-debugger/ diff --git a/CMakeLists.txt b/CMakeLists.txt index 1ab7e55343..aa2c353453 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -30,6 +30,8 @@ project(hifi) add_definitions(-DGLM_FORCE_RADIANS) set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -DDEBUG") +find_package( Threads ) + if (WIN32) add_definitions(-DNOMINMAX -D_CRT_SECURE_NO_WARNINGS) @@ -206,6 +208,17 @@ foreach(CUSTOM_MACRO ${HIFI_CUSTOM_MACROS}) include(${CUSTOM_MACRO}) endforeach() +file(GLOB_RECURSE JS_SRC scripts/*.js) +add_custom_target(js SOURCES ${JS_SRC}) + +if (UNIX) + install( + DIRECTORY "${CMAKE_SOURCE_DIR}/scripts" + DESTINATION ${CMAKE_CURRENT_BINARY_DIR}/interface + COMPONENT ${CLIENT_COMPONENT} + ) +endif() + if (ANDROID) file(GLOB ANDROID_CUSTOM_MACROS "cmake/android/*.cmake") foreach(CUSTOM_MACRO ${ANDROID_CUSTOM_MACROS}) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 86ea351609..a0d867ade9 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -16,7 +16,7 @@ Contributing git checkout -b new_branch_name ``` 4. Code - * Follow the [coding standard](https://readme.highfidelity.com/v1.0/docs/coding-standard) + * Follow the [coding standard](https://wiki.highfidelity.com/wiki/Coding_Standards) 5. Commit * Use [well formed commit messages](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html) 6. Update your branch diff --git a/README.md b/README.md index 00e7cbc45b..6294981e9a 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,8 @@ Documentation ========= 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). +There is also detailed [documentation on our coding standards](https://wiki.highfidelity.com/wiki/Coding_Standards). + Build Instructions ========= All information required to build is found in the [build guide](BUILD.md). diff --git a/assignment-client/src/Agent.cpp b/assignment-client/src/Agent.cpp index 355e47be46..e28c04379f 100644 --- a/assignment-client/src/Agent.cpp +++ b/assignment-client/src/Agent.cpp @@ -43,7 +43,6 @@ #include #include // TODO: consider moving to scriptengine.h -#include "avatars/ScriptableAvatar.h" #include "entities/AssignmentParentFinder.h" #include "RecordingScriptingInterface.h" #include "AbstractAudioInterface.h" @@ -63,6 +62,7 @@ Agent::Agent(ReceivedMessage& message) : DependencyManager::set(); DependencyManager::set(); + DependencyManager::set(); DependencyManager::set(); DependencyManager::set(); DependencyManager::set(); @@ -88,9 +88,9 @@ void Agent::playAvatarSound(SharedSoundPointer sound) { QMetaObject::invokeMethod(this, "playAvatarSound", Q_ARG(SharedSoundPointer, sound)); return; } 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 - // out until I verify. + // out until I verify. // _numAvatarSoundSentBytes = 0; setAvatarSound(sound); } @@ -105,7 +105,7 @@ void Agent::handleOctreePacket(QSharedPointer message, SharedNo if (message->getSize() > statsMessageLength) { // pull out the piggybacked packet and create a new QSharedPointer for it int piggyBackedSizeWithHeader = message->getSize() - statsMessageLength; - + auto buffer = std::unique_ptr(new char[piggyBackedSizeWithHeader]); memcpy(buffer.get(), message->getRawMessage() + statsMessageLength, piggyBackedSizeWithHeader); @@ -284,7 +284,7 @@ void Agent::selectAudioFormat(const QString& selectedCodecName) { for (auto& plugin : codecPlugins) { if (_selectedCodecName == plugin->getName()) { _codec = plugin; - _receivedAudioStream.setupCodec(plugin, _selectedCodecName, AudioConstants::STEREO); + _receivedAudioStream.setupCodec(plugin, _selectedCodecName, AudioConstants::STEREO); _encoder = plugin->createEncoder(AudioConstants::SAMPLE_RATE, AudioConstants::MONO); qDebug() << "Selected Codec Plugin:" << _codec.get(); break; @@ -336,6 +336,10 @@ void Agent::executeScript() { // call model URL setters with empty URLs so our avatar, if user, will have the default models scriptedAvatar->setSkeletonModelURL(QUrl()); + // force lazy initialization of the head data for the scripted avatar + // since it is referenced below by computeLoudness and getAudioLoudness + scriptedAvatar->getHeadOrientation(); + // give this AvatarData object to the script engine _scriptEngine->registerGlobalObject("Avatar", scriptedAvatar.data()); @@ -372,10 +376,25 @@ void Agent::executeScript() { using namespace recording; static const FrameType AUDIO_FRAME_TYPE = Frame::registerFrameType(AudioConstants::getAudioFrameName()); Frame::registerFrameHandler(AUDIO_FRAME_TYPE, [this, &scriptedAvatar](Frame::ConstPointer frame) { - const QByteArray& audio = frame->data; static quint16 audioSequenceNumber{ 0 }; - Transform audioTransform; + QByteArray audio(frame->data); + + if (_isNoiseGateEnabled) { + static int numSamples = AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL; + _noiseGate.gateSamples(reinterpret_cast(audio.data()), numSamples); + } + + computeLoudness(&audio, scriptedAvatar); + + // the codec needs a flush frame before sending silent packets, so + // do not send one if the gate closed in this block (eventually this can be crossfaded). + auto packetType = PacketType::MicrophoneAudioNoEcho; + if (scriptedAvatar->getAudioLoudness() == 0.0f && !_noiseGate.closedInLastBlock()) { + packetType = PacketType::SilentAudioFrame; + } + + Transform audioTransform; auto headOrientation = scriptedAvatar->getHeadOrientation(); audioTransform.setTranslation(scriptedAvatar->getPosition()); audioTransform.setRotation(headOrientation); @@ -386,9 +405,10 @@ void Agent::executeScript() { } else { encodedBuffer = audio; } + AbstractAudioInterface::emitAudioPacket(encodedBuffer.data(), encodedBuffer.size(), audioSequenceNumber, audioTransform, scriptedAvatar->getPosition(), glm::vec3(0), - PacketType::MicrophoneAudioNoEcho, _selectedCodecName); + packetType, _selectedCodecName); }); auto avatarHashMap = DependencyManager::set(); @@ -403,6 +423,7 @@ void Agent::executeScript() { _scriptEngine->registerGlobalObject("Agent", this); _scriptEngine->registerGlobalObject("SoundCache", DependencyManager::get().data()); + _scriptEngine->registerGlobalObject("AnimationCache", DependencyManager::get().data()); QScriptValue webSocketServerConstructorValue = _scriptEngine->newFunction(WebSocketServerClass::constructor); _scriptEngine->globalObject().setProperty("WebSocketServer", webSocketServerConstructorValue); @@ -424,16 +445,16 @@ void Agent::executeScript() { entityScriptingInterface->setEntityTree(_entityViewer.getTree()); DependencyManager::set(_entityViewer.getTree()); - + // 100Hz timer for audio AvatarAudioTimer* audioTimerWorker = new AvatarAudioTimer(); audioTimerWorker->moveToThread(&_avatarAudioTimerThread); connect(audioTimerWorker, &AvatarAudioTimer::avatarTick, this, &Agent::processAgentAvatarAudio); connect(this, &Agent::startAvatarAudioTimer, audioTimerWorker, &AvatarAudioTimer::start); connect(this, &Agent::stopAvatarAudioTimer, audioTimerWorker, &AvatarAudioTimer::stop); - connect(&_avatarAudioTimerThread, &QThread::finished, audioTimerWorker, &QObject::deleteLater); + connect(&_avatarAudioTimerThread, &QThread::finished, audioTimerWorker, &QObject::deleteLater); _avatarAudioTimerThread.start(); - + // Agents should run at 45hz static const int AVATAR_DATA_HZ = 45; static const int AVATAR_DATA_IN_MSECS = MSECS_PER_SECOND / AVATAR_DATA_HZ; @@ -456,14 +477,14 @@ QUuid Agent::getSessionUUID() const { return DependencyManager::get()->getSessionUUID(); } -void Agent::setIsListeningToAudioStream(bool isListeningToAudioStream) { +void Agent::setIsListeningToAudioStream(bool isListeningToAudioStream) { // this must happen on Agent's main thread if (QThread::currentThread() != thread()) { QMetaObject::invokeMethod(this, "setIsListeningToAudioStream", Q_ARG(bool, isListeningToAudioStream)); return; } 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->eachMatchingNode( @@ -479,7 +500,15 @@ void Agent::setIsListeningToAudioStream(bool isListeningToAudioStream) { }); } - _isListeningToAudioStream = isListeningToAudioStream; + _isListeningToAudioStream = isListeningToAudioStream; +} + +void Agent::setIsNoiseGateEnabled(bool isNoiseGateEnabled) { + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(this, "setIsNoiseGateEnabled", Q_ARG(bool, isNoiseGateEnabled)); + return; + } + _isNoiseGateEnabled = isNoiseGateEnabled; } void Agent::setIsAvatar(bool isAvatar) { @@ -560,6 +589,7 @@ void Agent::processAgentAvatar() { nodeList->broadcastToNodes(std::move(avatarPacket), NodeSet() << NodeType::AvatarMixer); } } + void Agent::encodeFrameOfZeros(QByteArray& encodedZeros) { _flushEncoder = false; static const QByteArray zeros(AudioConstants::NETWORK_FRAME_BYTES_PER_CHANNEL, 0); @@ -570,6 +600,22 @@ void Agent::encodeFrameOfZeros(QByteArray& encodedZeros) { } } +void Agent::computeLoudness(const QByteArray* decodedBuffer, QSharedPointer scriptableAvatar) { + float loudness = 0.0f; + if (decodedBuffer) { + auto soundData = reinterpret_cast(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() { auto recordingInterface = DependencyManager::get(); bool isPlayingRecording = recordingInterface->isPlaying(); @@ -619,6 +665,7 @@ void Agent::processAgentAvatarAudio() { audioPacket->seek(sizeof(quint16)); if (silentFrame) { + if (!_isListeningToAudioStream) { // if we have a silent frame and we're not listening then just send nothing and break out of here return; @@ -626,7 +673,7 @@ void Agent::processAgentAvatarAudio() { // write the codec audioPacket->writeString(_selectedCodecName); - + // write the number of silent samples so the audio-mixer can uphold timing audioPacket->writePrimitive(numAvailableSamples); @@ -636,8 +683,11 @@ void Agent::processAgentAvatarAudio() { audioPacket->writePrimitive(headOrientation); audioPacket->writePrimitive(scriptedAvatar->getPosition()); audioPacket->writePrimitive(glm::vec3(0)); + + // no matter what, the loudness should be set to 0 + computeLoudness(nullptr, scriptedAvatar); } else if (nextSoundOutput) { - + // write the codec audioPacket->writeString(_selectedCodecName); @@ -654,6 +704,8 @@ void Agent::processAgentAvatarAudio() { QByteArray encodedBuffer; if (_flushEncoder) { encodeFrameOfZeros(encodedBuffer); + // loudness is 0 + computeLoudness(nullptr, scriptedAvatar); } else { QByteArray decodedBuffer(reinterpret_cast(nextSoundOutput), numAvailableSamples*sizeof(int16_t)); if (_encoder) { @@ -662,10 +714,15 @@ void Agent::processAgentAvatarAudio() { } else { encodedBuffer = decodedBuffer; } + computeLoudness(&decodedBuffer, scriptedAvatar); } 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 auto nodeList = DependencyManager::get(); nodeList->eachNode([this, &nodeList, &audioPacket](const SharedNodePointer& node) { diff --git a/assignment-client/src/Agent.h b/assignment-client/src/Agent.h index c9b1707101..620ac8e047 100644 --- a/assignment-client/src/Agent.h +++ b/assignment-client/src/Agent.h @@ -29,7 +29,9 @@ #include +#include "AudioNoiseGate.h" #include "MixedAudioStream.h" +#include "avatars/ScriptableAvatar.h" class Agent : public ThreadedAssignment { Q_OBJECT @@ -37,6 +39,7 @@ class Agent : public ThreadedAssignment { Q_PROPERTY(bool isAvatar READ isAvatar WRITE setIsAvatar) Q_PROPERTY(bool isPlayingAvatarSound READ isPlayingAvatarSound) Q_PROPERTY(bool isListeningToAudioStream READ isListeningToAudioStream WRITE setIsListeningToAudioStream) + Q_PROPERTY(bool isNoiseGateEnabled READ isNoiseGateEnabled WRITE setIsNoiseGateEnabled) Q_PROPERTY(float lastReceivedAudioLoudness READ getLastReceivedAudioLoudness) Q_PROPERTY(QUuid sessionUUID READ getSessionUUID) @@ -51,6 +54,9 @@ public: bool isListeningToAudioStream() const { return _isListeningToAudioStream; } void setIsListeningToAudioStream(bool isListeningToAudioStream); + bool isNoiseGateEnabled() const { return _isNoiseGateEnabled; } + void setIsNoiseGateEnabled(bool isNoiseGateEnabled); + float getLastReceivedAudioLoudness() const { return _lastReceivedAudioLoudness; } QUuid getSessionUUID() const; @@ -68,10 +74,10 @@ private slots: void handleAudioPacket(QSharedPointer message); void handleOctreePacket(QSharedPointer message, SharedNodePointer senderNode); void handleJurisdictionPacket(QSharedPointer message, SharedNodePointer senderNode); - void handleSelectedAudioFormat(QSharedPointer message); + void handleSelectedAudioFormat(QSharedPointer message); void nodeActivated(SharedNodePointer activatedNode); - + void processAgentAvatar(); void processAgentAvatarAudio(); @@ -82,6 +88,7 @@ private: void negotiateAudioFormat(); void selectAudioFormat(const QString& selectedCodecName); void encodeFrameOfZeros(QByteArray& encodedZeros); + void computeLoudness(const QByteArray* decodedBuffer, QSharedPointer); std::unique_ptr _scriptEngine; EntityEditPacketSender _entityEditSender; @@ -103,10 +110,13 @@ private: bool _isAvatar = false; QTimer* _avatarIdentityTimer = nullptr; QHash _outgoingScriptAudioSequenceNumbers; - + + AudioNoiseGate _noiseGate; + bool _isNoiseGateEnabled { false }; + CodecPluginPointer _codec; QString _selectedCodecName; - Encoder* _encoder { nullptr }; + Encoder* _encoder { nullptr }; QThread _avatarAudioTimerThread; bool _flushEncoder { false }; }; diff --git a/assignment-client/src/assets/AssetServer.cpp b/assignment-client/src/assets/AssetServer.cpp index 82dd23a9de..3886ff8d92 100644 --- a/assignment-client/src/assets/AssetServer.cpp +++ b/assignment-client/src/assets/AssetServer.cpp @@ -24,7 +24,7 @@ #include #include -#include +#include #include "NetworkLogging.h" #include "NodeType.h" @@ -162,7 +162,7 @@ void AssetServer::completeSetup() { if (assetsPath.isRelative()) { // if the domain settings passed us a relative path, make an absolute path that is relative to the // default data directory - absoluteFilePath = ServerPathUtils::getDataFilePath("assets/" + assetsPathString); + absoluteFilePath = PathUtils::getAppDataFilePath("assets/" + assetsPathString); } _resourcesDirectory = QDir(absoluteFilePath); diff --git a/assignment-client/src/audio/AudioMixer.cpp b/assignment-client/src/audio/AudioMixer.cpp index 2583e15760..b95c429b2d 100644 --- a/assignment-client/src/audio/AudioMixer.cpp +++ b/assignment-client/src/audio/AudioMixer.cpp @@ -241,6 +241,7 @@ void AudioMixer::sendStatsPacket() { statsObject["avg_streams_per_frame"] = (float)_stats.sumStreams / (float)_numStatFrames; statsObject["avg_listeners_per_frame"] = (float)_stats.sumListeners / (float)_numStatFrames; + statsObject["avg_listeners_(silent)_per_frame"] = (float)_stats.sumListenersSilent / (float)_numStatFrames; statsObject["silent_packets_per_frame"] = (float)_numSilentPackets / (float)_numStatFrames; diff --git a/assignment-client/src/audio/AudioMixerSlave.cpp b/assignment-client/src/audio/AudioMixerSlave.cpp index 6b53de89c2..d01d961e33 100644 --- a/assignment-client/src/audio/AudioMixerSlave.cpp +++ b/assignment-client/src/audio/AudioMixerSlave.cpp @@ -106,6 +106,7 @@ void AudioMixerSlave::mix(const SharedNodePointer& node) { sendMixPacket(node, *data, encodedBuffer); } else { + ++stats.sumListenersSilent; sendSilentPacket(node, *data); } @@ -221,17 +222,19 @@ bool AudioMixerSlave::prepareMix(const SharedNodePointer& listener) { stats.mixTime += mixTime.count(); #endif - // use the per listener AudioLimiter to render the mixed data... - listenerData->audioLimiter.render(_mixSamples, _bufferSamples, AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL); - - // check for silent audio after the peak limiter has converted the samples + // check for silent audio before limiting + // limiting uses a dither and can only guarantee abs(sample) <= 1 bool hasAudio = false; for (int i = 0; i < AudioConstants::NETWORK_FRAME_SAMPLES_STEREO; ++i) { - if (_bufferSamples[i] != 0) { + if (_mixSamples[i] != 0.0f) { hasAudio = true; break; } } + + // use the per listener AudioLimiter to render the mixed data + listenerData->audioLimiter.render(_mixSamples, _bufferSamples, AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL); + return hasAudio; } diff --git a/assignment-client/src/audio/AudioMixerStats.cpp b/assignment-client/src/audio/AudioMixerStats.cpp index a831210871..4cfdd55167 100644 --- a/assignment-client/src/audio/AudioMixerStats.cpp +++ b/assignment-client/src/audio/AudioMixerStats.cpp @@ -14,6 +14,7 @@ void AudioMixerStats::reset() { sumStreams = 0; sumListeners = 0; + sumListenersSilent = 0; totalMixes = 0; hrtfRenders = 0; hrtfSilentRenders = 0; @@ -28,6 +29,7 @@ void AudioMixerStats::reset() { void AudioMixerStats::accumulate(const AudioMixerStats& otherStats) { sumStreams += otherStats.sumStreams; sumListeners += otherStats.sumListeners; + sumListenersSilent += otherStats.sumListenersSilent; totalMixes += otherStats.totalMixes; hrtfRenders += otherStats.hrtfRenders; hrtfSilentRenders += otherStats.hrtfSilentRenders; diff --git a/assignment-client/src/audio/AudioMixerStats.h b/assignment-client/src/audio/AudioMixerStats.h index 77ac8b985d..f4ba9db769 100644 --- a/assignment-client/src/audio/AudioMixerStats.h +++ b/assignment-client/src/audio/AudioMixerStats.h @@ -19,6 +19,7 @@ struct AudioMixerStats { int sumStreams { 0 }; int sumListeners { 0 }; + int sumListenersSilent { 0 }; int totalMixes { 0 }; diff --git a/assignment-client/src/avatars/AvatarMixer.cpp b/assignment-client/src/avatars/AvatarMixer.cpp index 0f6863f9ae..f3da74ce5e 100644 --- a/assignment-client/src/avatars/AvatarMixer.cpp +++ b/assignment-client/src/avatars/AvatarMixer.cpp @@ -37,7 +37,6 @@ const QString AVATAR_MIXER_LOGGING_NAME = "avatar-mixer"; // FIXME - what we'd actually like to do is send to users at ~50% of their present rate down to 30hz. Assume 90 for now. const int AVATAR_MIXER_BROADCAST_FRAMES_PER_SECOND = 45; -const unsigned int AVATAR_DATA_SEND_INTERVAL_MSECS = (1.0f / (float) AVATAR_MIXER_BROADCAST_FRAMES_PER_SECOND) * 1000; AvatarMixer::AvatarMixer(ReceivedMessage& message) : ThreadedAssignment(message) @@ -188,7 +187,7 @@ void AvatarMixer::start() { // NOTE: nodeData->getAvatar() might be side effected, must be called when access to node/nodeData -// is guarenteed to not be accessed by other thread +// is guaranteed to not be accessed by other thread void AvatarMixer::manageDisplayName(const SharedNodePointer& node) { AvatarMixerClientData* nodeData = reinterpret_cast(node->getLinkedData()); if (nodeData && nodeData->getAvatarSessionDisplayNameMustChange()) { @@ -201,7 +200,7 @@ void AvatarMixer::manageDisplayName(const SharedNodePointer& node) { QString baseName = avatar.getDisplayName().trimmed(); const QRegularExpression curses { "fuck|shit|damn|cock|cunt" }; // POC. We may eventually want something much more elaborate (subscription?). baseName = baseName.replace(curses, "*"); // Replace rather than remove, so that people have a clue that the person's a jerk. - const QRegularExpression trailingDigits { "\\s*_\\d+$" }; // whitespace "_123" + const QRegularExpression trailingDigits { "\\s*(_\\d+\\s*)?(\\s*\\n[^$]*)?$" }; // trailing whitespace "_123" and any subsequent lines baseName = baseName.remove(trailingDigits); if (baseName.isEmpty()) { baseName = "anonymous"; @@ -365,6 +364,28 @@ void AvatarMixer::handleRequestsDomainListDataPacket(QSharedPointerreadPrimitive(&isRequesting); nodeData->setRequestsDomainListData(isRequesting); qCDebug(avatars) << "node" << nodeData->getNodeID() << "requestsDomainListData" << isRequesting; + + // If we just opened the PAL... + if (isRequesting) { + // For each node in the NodeList... + auto nodeList = DependencyManager::get(); + nodeList->eachMatchingNode( + // Discover the valid nodes we're ignoring... + [&](const SharedNodePointer& node)->bool { + if (node->getUUID() != senderNode->getUUID() && + (nodeData->isRadiusIgnoring(node->getUUID()) || + senderNode->isIgnoringNodeWithID(node->getUUID()))) { + return true; + } + return false; + }, + // ...For those nodes, reset the lastBroadcastTime to 0 + // so that the AvatarMixer will send Identity data to us + [&](const SharedNodePointer& node) { + nodeData->setLastBroadcastTime(node->getUUID(), 0); + } + ); + } } } auto end = usecTimestampNow(); @@ -409,7 +430,34 @@ void AvatarMixer::handleKillAvatarPacket(QSharedPointer message void AvatarMixer::handleNodeIgnoreRequestPacket(QSharedPointer message, SharedNodePointer senderNode) { auto start = usecTimestampNow(); - senderNode->parseIgnoreRequestMessage(message); + auto nodeList = DependencyManager::get(); + AvatarMixerClientData* nodeData = reinterpret_cast(senderNode->getLinkedData()); + bool addToIgnore; + message->readPrimitive(&addToIgnore); + while (message->getBytesLeftToRead()) { + // parse out the UUID being ignored from the packet + QUuid ignoredUUID = QUuid::fromRfc4122(message->readWithoutCopy(NUM_BYTES_RFC4122_UUID)); + + if (nodeList->nodeWithUUID(ignoredUUID)) { + // Reset the lastBroadcastTime for the ignored avatar to 0 + // so the AvatarMixer knows it'll have to send identity data about the ignored avatar + // to the ignorer if the ignorer unignores. + nodeData->setLastBroadcastTime(ignoredUUID, 0); + + // Reset the lastBroadcastTime for the ignorer (FROM THE PERSPECTIVE OF THE IGNORED) to 0 + // so the AvatarMixer knows it'll have to send identity data about the ignorer + // to the ignored if the ignorer unignores. + auto ignoredNode = nodeList->nodeWithUUID(ignoredUUID); + AvatarMixerClientData* ignoredNodeData = reinterpret_cast(ignoredNode->getLinkedData()); + ignoredNodeData->setLastBroadcastTime(senderNode->getUUID(), 0); + } + + if (addToIgnore) { + senderNode->addIgnoredNode(ignoredUUID); + } else { + senderNode->removeIgnoredNode(ignoredUUID); + } + } auto end = usecTimestampNow(); _handleNodeIgnoreRequestPacketElapsedTime += (end - start); } diff --git a/assignment-client/src/avatars/AvatarMixerClientData.cpp b/assignment-client/src/avatars/AvatarMixerClientData.cpp index 717e14535f..15a7f50fa3 100644 --- a/assignment-client/src/avatars/AvatarMixerClientData.cpp +++ b/assignment-client/src/avatars/AvatarMixerClientData.cpp @@ -65,15 +65,6 @@ int AvatarMixerClientData::parseData(ReceivedMessage& message) { // compute the offset to the data payload return _avatar->parseDataFromBuffer(message.readWithoutCopy(message.getBytesLeftToRead())); } - -bool AvatarMixerClientData::checkAndSetHasReceivedFirstPacketsFrom(const QUuid& uuid) { - if (_hasReceivedFirstPacketsFrom.find(uuid) == _hasReceivedFirstPacketsFrom.end()) { - _hasReceivedFirstPacketsFrom.insert(uuid); - return false; - } - return true; -} - uint64_t AvatarMixerClientData::getLastBroadcastTime(const QUuid& nodeUUID) const { // return the matching PacketSequenceNumber, or the default if we don't have it auto nodeMatch = _lastBroadcastTimes.find(nodeUUID); @@ -102,8 +93,8 @@ void AvatarMixerClientData::ignoreOther(SharedNodePointer self, SharedNodePointe } else { killPacket->writePrimitive(KillAvatarReason::YourAvatarEnteredTheirBubble); } + setLastBroadcastTime(other->getUUID(), 0); DependencyManager::get()->sendUnreliablePacket(*killPacket, *self); - _hasReceivedFirstPacketsFrom.erase(other->getUUID()); } } diff --git a/assignment-client/src/avatars/AvatarMixerClientData.h b/assignment-client/src/avatars/AvatarMixerClientData.h index 51b8d692e2..1449005246 100644 --- a/assignment-client/src/avatars/AvatarMixerClientData.h +++ b/assignment-client/src/avatars/AvatarMixerClientData.h @@ -45,8 +45,6 @@ public: const AvatarData* getConstAvatarData() const { return _avatar.get(); } AvatarSharedPointer getAvatarSharedPointer() const { return _avatar; } - bool checkAndSetHasReceivedFirstPacketsFrom(const QUuid& uuid); - uint16_t getLastBroadcastSequenceNumber(const QUuid& nodeUUID) const; void setLastBroadcastSequenceNumber(const QUuid& nodeUUID, uint16_t sequenceNumber) { _lastBroadcastSequenceNumbers[nodeUUID] = sequenceNumber; } @@ -63,8 +61,8 @@ public: uint16_t getLastReceivedSequenceNumber() const { return _lastReceivedSequenceNumber; } - HRCTime getIdentityChangeTimestamp() const { return _identityChangeTimestamp; } - void flagIdentityChange() { _identityChangeTimestamp = p_high_resolution_clock::now(); } + uint64_t getIdentityChangeTimestamp() const { return _identityChangeTimestamp; } + void flagIdentityChange() { _identityChangeTimestamp = usecTimestampNow(); } bool getAvatarSessionDisplayNameMustChange() const { return _avatarSessionDisplayNameMustChange; } void setAvatarSessionDisplayNameMustChange(bool set = true) { _avatarSessionDisplayNameMustChange = set; } @@ -139,7 +137,6 @@ private: uint16_t _lastReceivedSequenceNumber { 0 }; std::unordered_map _lastBroadcastSequenceNumbers; - std::unordered_set _hasReceivedFirstPacketsFrom; std::unordered_map _lastBroadcastTimes; // this is a map of the last time we encoded an "other" avatar for @@ -147,7 +144,7 @@ private: std::unordered_map _lastOtherAvatarEncodeTime; std::unordered_map> _lastOtherAvatarSentJoints; - HRCTime _identityChangeTimestamp; + uint64_t _identityChangeTimestamp; bool _avatarSessionDisplayNameMustChange{ false }; int _numAvatarsSentLastFrame = 0; diff --git a/assignment-client/src/avatars/AvatarMixerSlave.cpp b/assignment-client/src/avatars/AvatarMixerSlave.cpp index 49b4b1ced4..05de209e81 100644 --- a/assignment-client/src/avatars/AvatarMixerSlave.cpp +++ b/assignment-client/src/avatars/AvatarMixerSlave.cpp @@ -80,16 +80,6 @@ int AvatarMixerSlave::sendIdentityPacket(const AvatarMixerClientData* nodeData, static const int AVATAR_MIXER_BROADCAST_FRAMES_PER_SECOND = 45; -// FIXME - There is some old logic (unchanged as of 2/17/17) that randomly decides to send an identity -// packet. That logic had the following comment about the constants it uses... -// -// An 80% chance of sending a identity packet within a 5 second interval. -// assuming 60 htz update rate. -// -// Assuming the calculation of the constant is in fact correct for 80% and 60hz and 5 seconds (an assumption -// that I have not verified) then the constant is definitely wrong now, since we send at 45hz. -const float IDENTITY_SEND_PROBABILITY = 1.0f / 187.0f; - void AvatarMixerSlave::broadcastAvatarData(const SharedNodePointer& node) { quint64 start = usecTimestampNow(); @@ -137,14 +127,18 @@ void AvatarMixerSlave::broadcastAvatarData(const SharedNodePointer& node) { // keep track of the number of other avatar frames skipped int numAvatarsWithSkippedFrames = 0; - // When this is true, the AvatarMixer will send Avatar data to a client about avatars that are not in the view frustrum - bool getsOutOfView = nodeData->getRequestsDomainListData(); - - // When this is true, the AvatarMixer will send Avatar data to a client about avatars that they've ignored - bool getsIgnoredByMe = getsOutOfView; + // When this is true, the AvatarMixer will send Avatar data to a client + // about avatars they've ignored or that are out of view + bool PALIsOpen = nodeData->getRequestsDomainListData(); // When this is true, the AvatarMixer will send Avatar data to a client about avatars that have ignored them - bool getsAnyIgnored = getsIgnoredByMe && node->getCanKick(); + bool getsAnyIgnored = PALIsOpen && node->getCanKick(); + + if (PALIsOpen) { + // Increase minimumBytesPerAvatar if the PAL is open + minimumBytesPerAvatar += sizeof(AvatarDataPacket::AvatarGlobalPosition) + + sizeof(AvatarDataPacket::AudioLoudness); + } // setup a PacketList for the avatarPackets auto avatarPacketList = NLPacketList::create(PacketType::BulkAvatarData); @@ -222,13 +216,14 @@ void AvatarMixerSlave::broadcastAvatarData(const SharedNodePointer& node) { // or that has ignored the viewing node if (!avatarNode->getLinkedData() || avatarNode->getUUID() == node->getUUID() - || (node->isIgnoringNodeWithID(avatarNode->getUUID()) && !getsIgnoredByMe) + || (node->isIgnoringNodeWithID(avatarNode->getUUID()) && !PALIsOpen) || (avatarNode->isIgnoringNodeWithID(node->getUUID()) && !getsAnyIgnored)) { shouldIgnore = true; } else { // Check to see if the space bubble is enabled - if (node->isIgnoreRadiusEnabled() || avatarNode->isIgnoreRadiusEnabled()) { + // Don't bother with these checks if the other avatar has their bubble enabled and we're gettingAnyIgnored + if (node->isIgnoreRadiusEnabled() || (avatarNode->isIgnoreRadiusEnabled() && !getsAnyIgnored)) { // Define the scale of the box for the current other node glm::vec3 otherNodeBoxScale = (avatarNodeData->getPosition() - avatarNodeData->getGlobalBoundingBoxCorner()) * 2.0f; @@ -306,16 +301,9 @@ void AvatarMixerSlave::broadcastAvatarData(const SharedNodePointer& node) { const AvatarMixerClientData* otherNodeData = reinterpret_cast(otherNode->getLinkedData()); - // make sure we send out identity packets to and from new arrivals. - bool forceSend = !nodeData->checkAndSetHasReceivedFirstPacketsFrom(otherNode->getUUID()); - - // FIXME - this clause seems suspicious "... || otherNodeData->getIdentityChangeTimestamp() > _lastFrameTimestamp ..." - if (!overBudget - && otherNodeData->getIdentityChangeTimestamp().time_since_epoch().count() > 0 - && (forceSend - || otherNodeData->getIdentityChangeTimestamp() > _lastFrameTimestamp - || distribution(generator) < IDENTITY_SEND_PROBABILITY)) { - + // If the time that the mixer sent AVATAR DATA about Avatar B to Avatar A is BEFORE OR EQUAL TO + // the time that Avatar B flagged an IDENTITY DATA change, send IDENTITY DATA about Avatar B to Avatar A. + if (nodeData->getLastBroadcastTime(otherNode->getUUID()) <= otherNodeData->getIdentityChangeTimestamp()) { identityBytesSent += sendIdentityPacket(otherNodeData, node); } @@ -335,9 +323,9 @@ void AvatarMixerSlave::broadcastAvatarData(const SharedNodePointer& node) { if (overBudget) { overBudgetAvatars++; _stats.overBudgetAvatars++; - detail = AvatarData::NoData; - } else if (!isInView && !getsOutOfView) { - detail = AvatarData::NoData; + detail = PALIsOpen ? AvatarData::PALMinimum : AvatarData::NoData; + } else if (!isInView) { + detail = PALIsOpen ? AvatarData::PALMinimum : AvatarData::NoData; nodeData->incrementAvatarOutOfView(); } else { detail = distribution(generator) < AVATAR_SEND_FULL_UPDATE_RATIO diff --git a/assignment-client/src/avatars/AvatarMixerSlavePool.h b/assignment-client/src/avatars/AvatarMixerSlavePool.h index 6bef0515bb..e6ac2a1f4e 100644 --- a/assignment-client/src/avatars/AvatarMixerSlavePool.h +++ b/assignment-client/src/avatars/AvatarMixerSlavePool.h @@ -49,7 +49,7 @@ private: bool _stop { false }; }; -// Slave pool for audio mixers +// Slave pool for avatar mixers // AvatarMixerSlavePool is not thread-safe! It should be instantiated and used from a single thread. class AvatarMixerSlavePool { using Queue = tbb::concurrent_queue; diff --git a/assignment-client/src/avatars/ScriptableAvatar.cpp b/assignment-client/src/avatars/ScriptableAvatar.cpp index 516bf7a1e3..57456b00c3 100644 --- a/assignment-client/src/avatars/ScriptableAvatar.cpp +++ b/assignment-client/src/avatars/ScriptableAvatar.cpp @@ -9,11 +9,15 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // +#include #include +#include #include +#include #include "ScriptableAvatar.h" + QByteArray ScriptableAvatar::toByteArrayStateful(AvatarDataDetail dataDetail) { _globalPosition = getPosition(); return AvatarData::toByteArrayStateful(dataDetail); @@ -57,6 +61,14 @@ void ScriptableAvatar::setSkeletonModelURL(const QUrl& skeletonModelURL) { _animSkeleton.reset(); AvatarData::setSkeletonModelURL(skeletonModelURL); } + +static AnimPose composeAnimPose(const FBXJoint& fbxJoint, const glm::quat rotation, const glm::vec3 translation) { + glm::mat4 translationMat = glm::translate(translation); + glm::mat4 rotationMat = glm::mat4_cast(fbxJoint.preRotation * rotation * fbxJoint.postRotation); + glm::mat4 finalMat = translationMat * fbxJoint.preTransform * rotationMat * fbxJoint.postTransform; + return AnimPose(finalMat); +} + void ScriptableAvatar::update(float deltatime) { if (_bind.isNull() && !_skeletonFBXURL.isEmpty()) { // AvatarData will parse the .fst, but not get the .fbx skeleton. _bind = DependencyManager::get()->getAnimation(_skeletonFBXURL); @@ -81,32 +93,42 @@ void ScriptableAvatar::update(float deltatime) { if (_jointData.size() != nJoints) { _jointData.resize(nJoints); } - + const int frameCount = _animation->getFrames().size(); const FBXAnimationFrame& floorFrame = _animation->getFrames().at((int)glm::floor(currentFrame) % frameCount); const FBXAnimationFrame& ceilFrame = _animation->getFrames().at((int)glm::ceil(currentFrame) % frameCount); const float frameFraction = glm::fract(currentFrame); std::vector poses = _animSkeleton->getRelativeDefaultPoses(); - + + const float UNIT_SCALE = 0.01f; + for (int i = 0; i < animationJointNames.size(); i++) { const QString& name = animationJointNames[i]; // As long as we need the model preRotations anyway, let's get the jointIndex from the bind skeleton rather than // trusting the .fst (which is sometimes not updated to match changes to .fbx). int mapping = _bind->getGeometry().getJointIndex(name); if (mapping != -1 && !_maskedJoints.contains(name)) { - // Eventually, this should probably deal with post rotations and translations, too. - poses[mapping].rot() = modelJoints[mapping].preRotation * - safeMix(floorFrame.rotations.at(i), ceilFrame.rotations.at(i), frameFraction);; + + AnimPose floorPose = composeAnimPose(modelJoints[mapping], floorFrame.rotations[i], floorFrame.translations[i] * UNIT_SCALE); + AnimPose ceilPose = composeAnimPose(modelJoints[mapping], ceilFrame.rotations[i], floorFrame.translations[i] * UNIT_SCALE); + blend(1, &floorPose, &ceilPose, frameFraction, &poses[mapping]); } } - _animSkeleton->convertRelativePosesToAbsolute(poses); + + std::vector absPoses = poses; + _animSkeleton->convertRelativePosesToAbsolute(absPoses); for (int i = 0; i < nJoints; i++) { JointData& data = _jointData[i]; - AnimPose& pose = poses[i]; - if (data.rotation != pose.rot()) { - data.rotation = pose.rot(); + AnimPose& absPose = absPoses[i]; + if (data.rotation != absPose.rot()) { + data.rotation = absPose.rot(); data.rotationSet = true; } + AnimPose& relPose = poses[i]; + if (data.translation != relPose.trans()) { + data.translation = relPose.trans(); + data.translationSet = true; + } } } else { diff --git a/assignment-client/src/octree/OctreeServer.cpp b/assignment-client/src/octree/OctreeServer.cpp index 2eee2ee229..f2dbe5d1d2 100644 --- a/assignment-client/src/octree/OctreeServer.cpp +++ b/assignment-client/src/octree/OctreeServer.cpp @@ -29,7 +29,7 @@ #include "OctreeQueryNode.h" #include "OctreeServerConsts.h" #include -#include +#include #include int OctreeServer::_clientCount = 0; @@ -279,8 +279,7 @@ OctreeServer::~OctreeServer() { void OctreeServer::initHTTPManager(int port) { // setup the embedded web server - - QString documentRoot = QString("%1/web").arg(ServerPathUtils::getDataDirectory()); + QString documentRoot = QString("%1/web").arg(PathUtils::getAppDataPath()); // setup an httpManager with us as the request handler and the parent _httpManager = new HTTPManager(QHostAddress::AnyIPv4, port, documentRoot, this, this); @@ -1179,7 +1178,7 @@ void OctreeServer::domainSettingsRequestComplete() { if (persistPath.isRelative()) { // if the domain settings passed us a relative path, make an absolute path that is relative to the // default data directory - persistAbsoluteFilePath = QDir(ServerPathUtils::getDataFilePath("entities/")).absoluteFilePath(_persistFilePath); + persistAbsoluteFilePath = QDir(PathUtils::getAppDataFilePath("entities/")).absoluteFilePath(_persistFilePath); } static const QString ENTITY_PERSIST_EXTENSION = ".json.gz"; @@ -1245,7 +1244,7 @@ void OctreeServer::domainSettingsRequestComplete() { QDir backupDirectory { _backupDirectoryPath }; QString absoluteBackupDirectory; if (backupDirectory.isRelative()) { - absoluteBackupDirectory = QDir(ServerPathUtils::getDataFilePath("entities/")).absoluteFilePath(_backupDirectoryPath); + absoluteBackupDirectory = QDir(PathUtils::getAppDataFilePath("entities/")).absoluteFilePath(_backupDirectoryPath); absoluteBackupDirectory = QDir(absoluteBackupDirectory).absolutePath(); } else { absoluteBackupDirectory = backupDirectory.absolutePath(); diff --git a/assignment-client/src/scripts/EntityScriptServer.cpp b/assignment-client/src/scripts/EntityScriptServer.cpp index 5f99dd68bc..954c25a342 100644 --- a/assignment-client/src/scripts/EntityScriptServer.cpp +++ b/assignment-client/src/scripts/EntityScriptServer.cpp @@ -58,6 +58,8 @@ EntityScriptServer::EntityScriptServer(ReceivedMessage& message) : ThreadedAssig DependencyManager::registerInheritance(); + DependencyManager::set(); + DependencyManager::set(); DependencyManager::set(); DependencyManager::set(); @@ -324,7 +326,26 @@ void EntityScriptServer::nodeActivated(SharedNodePointer activatedNode) { void EntityScriptServer::nodeKilled(SharedNodePointer killedNode) { switch (killedNode->getType()) { case NodeType::EntityServer: { - clear(); + // Before we clear, make sure this was our only entity server. + // Otherwise we're assuming that we have "trading" entity servers + // (an old one going away and a new one coming onboard) + // and that we shouldn't clear here because we're still doing work. + bool hasAnotherEntityServer = false; + auto nodeList = DependencyManager::get(); + + nodeList->eachNodeBreakable([&hasAnotherEntityServer, &killedNode](const SharedNodePointer& node){ + if (node->getType() == NodeType::EntityServer && node->getUUID() != killedNode->getUUID()) { + // we're talking to > 1 entity servers, we know we won't clear + hasAnotherEntityServer = true; + return false; + } + + return true; + }); + + if (!hasAnotherEntityServer) { + clear(); + } break; } @@ -394,8 +415,9 @@ void EntityScriptServer::selectAudioFormat(const QString& selectedCodecName) { } void EntityScriptServer::resetEntitiesScriptEngine() { - auto engineName = QString("Entities %1").arg(++_entitiesScriptEngineCount); - auto newEngine = QSharedPointer(new ScriptEngine(ScriptEngine::ENTITY_SERVER_SCRIPT, NO_SCRIPT, engineName)); + auto engineName = QString("about:Entities %1").arg(++_entitiesScriptEngineCount); + auto newEngine = QSharedPointer(new ScriptEngine(ScriptEngine::ENTITY_SERVER_SCRIPT, NO_SCRIPT, engineName), + &ScriptEngine::deleteLater); auto webSocketServerConstructorValue = newEngine->newFunction(WebSocketServerClass::constructor); newEngine->globalObject().setProperty("WebSocketServer", webSocketServerConstructorValue); @@ -455,13 +477,13 @@ void EntityScriptServer::addingEntity(const EntityItemID& entityID) { void EntityScriptServer::deletingEntity(const EntityItemID& entityID) { if (_entityViewer.getTree() && !_shuttingDown && _entitiesScriptEngine) { - _entitiesScriptEngine->unloadEntityScript(entityID); + _entitiesScriptEngine->unloadEntityScript(entityID, true); } } void EntityScriptServer::entityServerScriptChanging(const EntityItemID& entityID, const bool reload) { if (_entityViewer.getTree() && !_shuttingDown) { - _entitiesScriptEngine->unloadEntityScript(entityID); + _entitiesScriptEngine->unloadEntityScript(entityID, true); checkAndCallPreload(entityID, reload); } } @@ -477,7 +499,7 @@ void EntityScriptServer::checkAndCallPreload(const EntityItemID& entityID, const if (!scriptUrl.isEmpty()) { scriptUrl = ResourceManager::normalizeURL(scriptUrl); qCDebug(entity_script_server) << "Loading entity server script" << scriptUrl << "for" << entityID; - ScriptEngine::loadEntityScript(_entitiesScriptEngine, entityID, scriptUrl, reload); + _entitiesScriptEngine->loadEntityScript(entityID, scriptUrl, reload); } } } diff --git a/cmake/externals/LibOVR/CMakeLists.txt b/cmake/externals/LibOVR/CMakeLists.txt index 54a4a47929..c98aa8a04a 100644 --- a/cmake/externals/LibOVR/CMakeLists.txt +++ b/cmake/externals/LibOVR/CMakeLists.txt @@ -12,35 +12,29 @@ string(TOUPPER ${EXTERNAL_NAME} EXTERNAL_NAME_UPPER) # 0.5 public # URL http://static.oculus.com/sdk-downloads/ovr_sdk_win_0.5.0.1.zip # URL_MD5 d3fc4c02db9be5ff08af4ef4c97b32f9 -# 1.3 public -# URL http://hifi-public.s3.amazonaws.com/dependencies/ovr_sdk_win_1.3.0_public.zip -# URL_MD5 4d26faba0c1f35ff80bf674c96ed9259 if (WIN32) ExternalProject_Add( ${EXTERNAL_NAME} - URL https://hifi-public.s3.amazonaws.com/dependencies/ovr_sdk_win_1.8.0_public.zip - URL_MD5 bea17e04acc1dd8cf7cabefa1b28cc3c - CONFIGURE_COMMAND "" - BUILD_COMMAND "" - INSTALL_COMMAND "" + URL https://static.oculus.com/sdk-downloads/1.11.0/Public/1486063832/ovr_sdk_win_1.11.0_public.zip + URL_MD5 ea484403757cbfdfa743b6577fb1f9d2 + CMAKE_ARGS -DCMAKE_INSTALL_PREFIX:PATH= + PATCH_COMMAND ${CMAKE_COMMAND} -E copy "${CMAKE_CURRENT_SOURCE_DIR}/LibOVRCMakeLists.txt" /CMakeLists.txt LOG_DOWNLOAD 1 ) ExternalProject_Get_Property(${EXTERNAL_NAME} SOURCE_DIR) - message("LIBOVR dir ${SOURCE_DIR}") - set(LIBOVR_DIR ${SOURCE_DIR}/LibOVR) - if ("${CMAKE_SIZEOF_VOID_P}" EQUAL "8") - set(LIBOVR_LIB_DIR ${LIBOVR_DIR}/Lib/Windows/x64/Release/VS2013 CACHE TYPE INTERNAL) - else() - set(LIBOVR_LIB_DIR ${LIBOVR_DIR}/Lib/Windows/Win32/Release/VS2013 CACHE TYPE INTERNAL) - endif() - + ExternalProject_Get_Property(${EXTERNAL_NAME} INSTALL_DIR) + set(LIBOVR_DIR ${INSTALL_DIR}) set(${EXTERNAL_NAME_UPPER}_INCLUDE_DIRS ${LIBOVR_DIR}/Include CACHE TYPE INTERNAL) - message("LIBOVR include dir ${${EXTERNAL_NAME_UPPER}_INCLUDE_DIRS}") - set(${EXTERNAL_NAME_UPPER}_LIBRARIES ${LIBOVR_LIB_DIR}/LibOVR.lib CACHE TYPE INTERNAL) - + set(${EXTERNAL_NAME_UPPER}_LIBRARY_DEBUG ${LIBOVR_DIR}/Lib/LibOVRd.lib CACHE TYPE INTERNAL) + set(${EXTERNAL_NAME_UPPER}_LIBRARY_RELEASE ${LIBOVR_DIR}/Lib/LibOVR.lib CACHE TYPE INTERNAL) + include(SelectLibraryConfigurations) + select_library_configurations(LIBOVR) + set(${EXTERNAL_NAME_UPPER}_LIBRARIES ${${EXTERNAL_NAME_UPPER}_LIBRARIES} CACHE TYPE INTERNAL) + message("Libs ${EXTERNAL_NAME_UPPER}_LIBRARIES ${${EXTERNAL_NAME_UPPER}_LIBRARIES}") + elseif(APPLE) ExternalProject_Add( diff --git a/cmake/externals/LibOVR/LibOVRCMakeLists.txt b/cmake/externals/LibOVR/LibOVRCMakeLists.txt new file mode 100644 index 0000000000..556533f0c2 --- /dev/null +++ b/cmake/externals/LibOVR/LibOVRCMakeLists.txt @@ -0,0 +1,14 @@ +cmake_minimum_required(VERSION 3.2) +project(LibOVR) + +include_directories(LibOVR/Include LibOVR/Src) +file(GLOB HEADER_FILES LibOVR/Include/*.h) +file(GLOB EXTRA_HEADER_FILES LibOVR/Include/Extras/*.h) +file(GLOB_RECURSE SOURCE_FILES LibOVR/Src/*.c LibOVR/Src/*.cpp) +set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -DOVR_BUILD_DEBUG") +add_library(LibOVR STATIC ${SOURCE_FILES} ${HEADER_FILES} ${EXTRA_HEADER_FILES}) +set_target_properties(LibOVR PROPERTIES DEBUG_POSTFIX "d") + +install(TARGETS LibOVR DESTINATION Lib) +install(FILES ${HEADER_FILES} DESTINATION Include) +install(FILES ${EXTRA_HEADER_FILES} DESTINATION Include/Extras) \ No newline at end of file diff --git a/cmake/externals/bullet/CMakeLists.txt b/cmake/externals/bullet/CMakeLists.txt index 125432002c..317e3302d9 100644 --- a/cmake/externals/bullet/CMakeLists.txt +++ b/cmake/externals/bullet/CMakeLists.txt @@ -66,11 +66,15 @@ if (DEFINED BULLET_LIB_EXT) list(GET _LIB_PAIR 0 _LIB_VAR_NAME) list(GET _LIB_PAIR 1 _LIB_NAME) - set(${EXTERNAL_NAME_UPPER}_${_LIB_VAR_NAME}_RELEASE ${BULLET_LIB_DIR}/${LIB_PREFIX}${_LIB_NAME}.${BULLET_LIB_EXT} CACHE FILEPATH "${_LIB_NAME} release library location") + if (WIN32) + # on windows, we might end up with a library that ends with RelWithDebInfo if Visual Studio is building for that configuration + set(${EXTERNAL_NAME_UPPER}_${_LIB_VAR_NAME}_RELEASE "${BULLET_LIB_DIR}/${LIB_PREFIX}${_LIB_NAME}$<$:_RelWithDebugInfo>$<$:_MinsizeRel>.${BULLET_LIB_EXT}" CACHE FILEPATH "${_LIB_NAME} release library location") + set(${EXTERNAL_NAME_UPPER}_${_LIB_VAR_NAME}_DEBUG ${BULLET_LIB_DIR}/${LIB_PREFIX}${_LIB_NAME}_Debug.${BULLET_LIB_EXT} CACHE FILEPATH "${_LIB_NAME} debug library location") else () + set(${EXTERNAL_NAME_UPPER}_${_LIB_VAR_NAME}_RELEASE ${BULLET_LIB_DIR}/${LIB_PREFIX}${_LIB_NAME}.${BULLET_LIB_EXT} CACHE FILEPATH "${_LIB_NAME} release library location") set(${EXTERNAL_NAME_UPPER}_${_LIB_VAR_NAME}_DEBUG "" CACHE FILEPATH "${_LIB_NAME} debug library location") endif () endforeach() diff --git a/cmake/externals/faceshift/CMakeLists.txt b/cmake/externals/faceshift/CMakeLists.txt index 28fbffec34..c4f2055435 100644 --- a/cmake/externals/faceshift/CMakeLists.txt +++ b/cmake/externals/faceshift/CMakeLists.txt @@ -27,6 +27,10 @@ set(LIBRARY_RELEASE_PATH "lib/Release") if (WIN32) set(LIBRARY_PREFIX "") set(LIBRARY_EXT "lib") + # use selected configuration in release path when building on Windows + set(LIBRARY_RELEASE_PATH "$<$:build/RelWithDebInfo>") + set(LIBRARY_RELEASE_PATH "${LIBRARY_RELEASE_PATH}$<$:build/MinSizeRel>") + set(LIBRARY_RELEASE_PATH "${LIBRARY_RELEASE_PATH}$<$,$>:lib/Release>") elseif (APPLE) set(LIBRARY_EXT "a") set(LIBRARY_PREFIX "lib") diff --git a/cmake/externals/openvr/CMakeLists.txt b/cmake/externals/openvr/CMakeLists.txt index 19a9dd1f15..cb4aafcf8b 100644 --- a/cmake/externals/openvr/CMakeLists.txt +++ b/cmake/externals/openvr/CMakeLists.txt @@ -7,8 +7,8 @@ string(TOUPPER ${EXTERNAL_NAME} EXTERNAL_NAME_UPPER) ExternalProject_Add( ${EXTERNAL_NAME} - URL https://github.com/ValveSoftware/openvr/archive/v1.0.3.zip - URL_MD5 b484b12901917cc739e40389583c8b0d + URL https://github.com/ValveSoftware/openvr/archive/v1.0.6.zip + URL_MD5 f6892cd3a3078f505d03b4297f5a1951 CONFIGURE_COMMAND "" BUILD_COMMAND "" INSTALL_COMMAND "" diff --git a/cmake/externals/polyvox/CMakeLists.txt b/cmake/externals/polyvox/CMakeLists.txt index 3740e26762..c799b45e78 100644 --- a/cmake/externals/polyvox/CMakeLists.txt +++ b/cmake/externals/polyvox/CMakeLists.txt @@ -19,7 +19,7 @@ ExternalProject_Get_Property(${EXTERNAL_NAME} INSTALL_DIR) if (APPLE) set(INSTALL_NAME_LIBRARY_DIR ${INSTALL_DIR}/lib) - + ExternalProject_Add_Step( ${EXTERNAL_NAME} change-install-name-debug @@ -29,7 +29,7 @@ if (APPLE) WORKING_DIRECTORY LOG 1 ) - + ExternalProject_Add_Step( ${EXTERNAL_NAME} change-install-name-release @@ -59,7 +59,13 @@ endif () if (WIN32) set(${EXTERNAL_NAME_UPPER}_CORE_LIBRARY_DEBUG ${INSTALL_DIR}/PolyVoxCore/lib/Debug/PolyVoxCore.lib CACHE FILEPATH "polyvox core library") - set(${EXTERNAL_NAME_UPPER}_CORE_LIBRARY_RELEASE ${INSTALL_DIR}/PolyVoxCore/lib/Release/PolyVoxCore.lib CACHE FILEPATH "polyvox core library") + + # use generator expression to ensure the correct library is found when building different configurations in VS + set(_LIB_FOLDER "$<$:PolyVoxCore/lib/RelWithDebInfo>") + set(_LIB_FOLDER "${_LIB_FOLDER}$<$:build/library/PolyVoxCore/MinSizeRel>") + set(_LIB_FOLDER "${_LIB_FOLDER}$<$,$>:PolyVoxCore/lib/Release>") + + set(${EXTERNAL_NAME_UPPER}_CORE_LIBRARY_RELEASE "${INSTALL_DIR}/${_LIB_FOLDER}/PolyVoxCore.lib" CACHE FILEPATH "polyvox core library") # set(${EXTERNAL_NAME_UPPER}_UTIL_LIBRARY ${INSTALL_DIR}/PolyVoxUtil/lib/PolyVoxUtil.lib CACHE FILEPATH "polyvox util library") elseif (APPLE) set(${EXTERNAL_NAME_UPPER}_CORE_LIBRARY_DEBUG ${INSTALL_DIR}/lib/Debug/libPolyVoxCore.dylib CACHE FILEPATH "polyvox core library") diff --git a/cmake/externals/vhacd/CMakeLists.txt b/cmake/externals/vhacd/CMakeLists.txt index efe6ed0381..11afa255f1 100644 --- a/cmake/externals/vhacd/CMakeLists.txt +++ b/cmake/externals/vhacd/CMakeLists.txt @@ -8,7 +8,7 @@ include(ExternalProject) ExternalProject_Add( ${EXTERNAL_NAME} URL http://hifi-public.s3.amazonaws.com/dependencies/v-hacd-master.zip - URL_MD5 3bfc94f8dd3dfbfe8f4dc088f4820b3e + URL_MD5 3bfc94f8dd3dfbfe8f4dc088f4820b3e CMAKE_ARGS ${ANDROID_CMAKE_ARGS} -DCMAKE_INSTALL_PREFIX:PATH= BINARY_DIR ${EXTERNAL_PROJECT_PREFIX}/build LOG_DOWNLOAD 1 @@ -25,7 +25,13 @@ string(TOUPPER ${EXTERNAL_NAME} EXTERNAL_NAME_UPPER) if (WIN32) set(${EXTERNAL_NAME_UPPER}_LIBRARY_DEBUG ${INSTALL_DIR}/lib/Debug/VHACD_LIB.lib CACHE FILEPATH "Path to V-HACD debug library") - set(${EXTERNAL_NAME_UPPER}_LIBRARY_RELEASE ${INSTALL_DIR}/lib/Release/VHACD_LIB.lib CACHE FILEPATH "Path to V-HACD release library") + + # use generator expression to ensure the correct library is found when building different configurations in VS + set(_LIB_FOLDER "$<$:build/src/VHACD_Lib/RelWithDebInfo>") + set(_LIB_FOLDER "${_LIB_FOLDER}$<$:build/src/VHACD_Lib/MinSizeRel>") + set(_LIB_FOLDER "${_LIB_FOLDER}$<$,$>:lib/Release>") + + set(${EXTERNAL_NAME_UPPER}_LIBRARY_RELEASE ${INSTALL_DIR}/${_LIB_FOLDER}/VHACD_LIB.lib CACHE FILEPATH "Path to V-HACD release library") else () set(${EXTERNAL_NAME_UPPER}_LIBRARY_DEBUG "" CACHE FILEPATH "Path to V-HACD debug library") set(${EXTERNAL_NAME_UPPER}_LIBRARY_RELEASE ${INSTALL_DIR}/lib/libVHACD.a CACHE FILEPATH "Path to V-HACD release library") diff --git a/cmake/macros/LinkHifiLibraries.cmake b/cmake/macros/LinkHifiLibraries.cmake index 3767dc7131..de4ff23863 100644 --- a/cmake/macros/LinkHifiLibraries.cmake +++ b/cmake/macros/LinkHifiLibraries.cmake @@ -21,7 +21,7 @@ macro(LINK_HIFI_LIBRARIES) include_directories("${HIFI_LIBRARY_DIR}/${HIFI_LIBRARY}/src") include_directories("${CMAKE_BINARY_DIR}/libraries/${HIFI_LIBRARY}/shaders") - add_dependencies(${TARGET_NAME} ${HIFI_LIBRARY}) + #add_dependencies(${TARGET_NAME} ${HIFI_LIBRARY}) # link the actual library - it is static so don't bubble it up target_link_libraries(${TARGET_NAME} ${HIFI_LIBRARY}) diff --git a/cmake/macros/PackageLibrariesForDeployment.cmake b/cmake/macros/PackageLibrariesForDeployment.cmake index 795e3642a5..d324776572 100644 --- a/cmake/macros/PackageLibrariesForDeployment.cmake +++ b/cmake/macros/PackageLibrariesForDeployment.cmake @@ -24,9 +24,9 @@ macro(PACKAGE_LIBRARIES_FOR_DEPLOYMENT) TARGET ${TARGET_NAME} POST_BUILD COMMAND ${CMAKE_COMMAND} - -DBUNDLE_EXECUTABLE=$ - -DBUNDLE_PLUGIN_DIR=$/${PLUGIN_PATH} - -P ${CMAKE_CURRENT_BINARY_DIR}/FixupBundlePostBuild.cmake + -DBUNDLE_EXECUTABLE="$" + -DBUNDLE_PLUGIN_DIR="$/${PLUGIN_PATH}" + -P "${CMAKE_CURRENT_BINARY_DIR}/FixupBundlePostBuild.cmake" ) find_program(WINDEPLOYQT_COMMAND windeployqt PATHS ${QT_DIR}/bin NO_DEFAULT_PATH) @@ -39,27 +39,27 @@ macro(PACKAGE_LIBRARIES_FOR_DEPLOYMENT) add_custom_command( TARGET ${TARGET_NAME} POST_BUILD - COMMAND CMD /C "SET PATH=%PATH%;${QT_DIR}/bin && ${WINDEPLOYQT_COMMAND} ${EXTRA_DEPLOY_OPTIONS} $<$,$,$>:--release> $" + COMMAND CMD /C "SET PATH=%PATH%;${QT_DIR}/bin && ${WINDEPLOYQT_COMMAND} ${EXTRA_DEPLOY_OPTIONS} $<$,$,$>:--release> \"$\"" ) - set(QTAUDIO_PATH $/audio) - set(QTAUDIO_WIN7_PATH $/audioWin7/audio) - set(QTAUDIO_WIN8_PATH $/audioWin8/audio) + set(QTAUDIO_PATH "$/audio") + set(QTAUDIO_WIN7_PATH "$/audioWin7/audio") + set(QTAUDIO_WIN8_PATH "$/audioWin8/audio") # copy qtaudio_wasapi.dll and qtaudio_windows.dll in the correct directories for runtime selection add_custom_command( TARGET ${TARGET_NAME} POST_BUILD - COMMAND ${CMAKE_COMMAND} -E make_directory ${QTAUDIO_WIN7_PATH} - COMMAND ${CMAKE_COMMAND} -E make_directory ${QTAUDIO_WIN8_PATH} + COMMAND ${CMAKE_COMMAND} -E make_directory "${QTAUDIO_WIN7_PATH}" + COMMAND ${CMAKE_COMMAND} -E make_directory "${QTAUDIO_WIN8_PATH}" # copy release DLLs - COMMAND if exist ${QTAUDIO_PATH}/qtaudio_windows.dll ( ${CMAKE_COMMAND} -E copy ${QTAUDIO_PATH}/qtaudio_windows.dll ${QTAUDIO_WIN7_PATH} ) - COMMAND if exist ${QTAUDIO_PATH}/qtaudio_windows.dll ( ${CMAKE_COMMAND} -E copy ${WASAPI_DLL_PATH}/qtaudio_wasapi.dll ${QTAUDIO_WIN8_PATH} ) + COMMAND if exist "${QTAUDIO_PATH}/qtaudio_windows.dll" ( ${CMAKE_COMMAND} -E copy "${QTAUDIO_PATH}/qtaudio_windows.dll" "${QTAUDIO_WIN7_PATH}" ) + COMMAND if exist "${QTAUDIO_PATH}/qtaudio_windows.dll" ( ${CMAKE_COMMAND} -E copy "${WASAPI_DLL_PATH}/qtaudio_wasapi.dll" "${QTAUDIO_WIN8_PATH}" ) # copy debug DLLs - COMMAND if exist ${QTAUDIO_PATH}/qtaudio_windowsd.dll ( ${CMAKE_COMMAND} -E copy ${QTAUDIO_PATH}/qtaudio_windowsd.dll ${QTAUDIO_WIN7_PATH} ) - COMMAND if exist ${QTAUDIO_PATH}/qtaudio_windowsd.dll ( ${CMAKE_COMMAND} -E copy ${WASAPI_DLL_PATH}/qtaudio_wasapid.dll ${QTAUDIO_WIN8_PATH} ) + COMMAND if exist "${QTAUDIO_PATH}/qtaudio_windowsd.dll" ( ${CMAKE_COMMAND} -E copy "${QTAUDIO_PATH}/qtaudio_windowsd.dll" "${QTAUDIO_WIN7_PATH}" ) + COMMAND if exist "${QTAUDIO_PATH}/qtaudio_windowsd.dll" ( ${CMAKE_COMMAND} -E copy "${WASAPI_DLL_PATH}/qtaudio_wasapid.dll" "${QTAUDIO_WIN8_PATH}" ) # remove directory - COMMAND ${CMAKE_COMMAND} -E remove_directory ${QTAUDIO_PATH} + COMMAND ${CMAKE_COMMAND} -E remove_directory "${QTAUDIO_PATH}" ) endif () diff --git a/cmake/macros/SetupHifiPlugin.cmake b/cmake/macros/SetupHifiPlugin.cmake index 0db91cb9e6..7e56ea3db2 100644 --- a/cmake/macros/SetupHifiPlugin.cmake +++ b/cmake/macros/SetupHifiPlugin.cmake @@ -9,6 +9,7 @@ macro(SETUP_HIFI_PLUGIN) set(${TARGET_NAME}_SHARED 1) setup_hifi_library(${ARGV}) add_dependencies(interface ${TARGET_NAME}) + target_link_libraries(${TARGET_NAME} ${CMAKE_THREAD_LIBS_INIT}) set_target_properties(${TARGET_NAME} PROPERTIES FOLDER "Plugins") if (APPLE) diff --git a/cmake/macros/SetupHifiProject.cmake b/cmake/macros/SetupHifiProject.cmake index 8695063556..8759c949f3 100644 --- a/cmake/macros/SetupHifiProject.cmake +++ b/cmake/macros/SetupHifiProject.cmake @@ -43,6 +43,7 @@ macro(SETUP_HIFI_PROJECT) foreach(QT_MODULE ${${TARGET_NAME}_DEPENDENCY_QT_MODULES}) target_link_libraries(${TARGET_NAME} Qt5::${QT_MODULE}) endforeach() + target_link_libraries(${TARGET_NAME} ${CMAKE_THREAD_LIBS_INIT}) target_glm() diff --git a/cmake/macros/SetupHifiTestCase.cmake b/cmake/macros/SetupHifiTestCase.cmake index 38239d6e97..6c7d38e19c 100644 --- a/cmake/macros/SetupHifiTestCase.cmake +++ b/cmake/macros/SetupHifiTestCase.cmake @@ -108,6 +108,7 @@ macro(SETUP_HIFI_TESTCASE) foreach(QT_MODULE ${${TARGET_NAME}_DEPENDENCY_QT_MODULES}) target_link_libraries(${TARGET_NAME} Qt5::${QT_MODULE}) endforeach() + target_link_libraries(${TARGET_NAME} ${CMAKE_THREAD_LIBS_INIT}) set_target_properties(${TARGET_NAME} PROPERTIES FOLDER "hidden/test-executables") diff --git a/cmake/macros/SymlinkOrCopyDirectoryBesideTarget.cmake b/cmake/macros/SymlinkOrCopyDirectoryBesideTarget.cmake index 37a7a9caa0..9ae47aad82 100644 --- a/cmake/macros/SymlinkOrCopyDirectoryBesideTarget.cmake +++ b/cmake/macros/SymlinkOrCopyDirectoryBesideTarget.cmake @@ -14,7 +14,7 @@ macro(SYMLINK_OR_COPY_DIRECTORY_BESIDE_TARGET _SHOULD_SYMLINK _DIRECTORY _DESTIN # remove the current directory add_custom_command( TARGET ${TARGET_NAME} POST_BUILD - COMMAND "${CMAKE_COMMAND}" -E remove_directory $/${_DESTINATION} + COMMAND "${CMAKE_COMMAND}" -E remove_directory "$/${_DESTINATION}" ) if (${_SHOULD_SYMLINK}) @@ -48,8 +48,8 @@ macro(SYMLINK_OR_COPY_DIRECTORY_BESIDE_TARGET _SHOULD_SYMLINK _DIRECTORY _DESTIN # copy the directory add_custom_command( TARGET ${TARGET_NAME} POST_BUILD - COMMAND ${CMAKE_COMMAND} -E copy_directory ${_DIRECTORY} - $/${_DESTINATION} + COMMAND ${CMAKE_COMMAND} -E copy_directory "${_DIRECTORY}" + "$/${_DESTINATION}" ) endif () # glob everything in this directory - add a custom command to copy any files diff --git a/cmake/modules/FindKinect.cmake b/cmake/modules/FindKinect.cmake index 7607de1f44..895c6ebe8a 100644 --- a/cmake/modules/FindKinect.cmake +++ b/cmake/modules/FindKinect.cmake @@ -21,7 +21,7 @@ include("${MACRO_DIR}/HifiLibrarySearchHints.cmake") hifi_library_search_hints("kinect") -find_path(KINECT_INCLUDE_DIRS Kinect.h PATH_SUFFIXES inc HINTS $ENV{KINECT_ROOT_DIR}) +find_path(KINECT_INCLUDE_DIRS Kinect.h PATH_SUFFIXES inc HINTS $ENV{KINECT_ROOT_DIR} $ENV{KINECTSDK20_DIR}) if (WIN32) @@ -35,7 +35,7 @@ if (WIN32) KINECT_LIBRARY_RELEASE Kinect20 PATH_SUFFIXES "Lib/${ARCH_DIR}" "lib" HINTS ${KINECT_SEARCH_DIRS} - PATH $ENV{KINECT_ROOT_DIR}) + PATHS $ENV{KINECT_ROOT_DIR} $ENV{KINECTSDK20_DIR}) set(KINECT_LIBRARIES ${KINECT_LIBRARY}) diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index c741c22b83..620b11d8ad 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -38,7 +38,7 @@ #include #include #include -#include +#include #include #include "DomainServerNodeData.h" @@ -1618,7 +1618,7 @@ QJsonObject DomainServer::jsonObjectForNode(const SharedNodePointer& node) { QDir pathForAssignmentScriptsDirectory() { static const QString SCRIPTS_DIRECTORY_NAME = "/scripts/"; - QDir directory(ServerPathUtils::getDataDirectory() + SCRIPTS_DIRECTORY_NAME); + QDir directory(PathUtils::getAppDataPath() + SCRIPTS_DIRECTORY_NAME); if (!directory.exists()) { directory.mkpath("."); qInfo() << "Created path to " << directory.path(); diff --git a/domain-server/src/DomainServerSettingsManager.cpp b/domain-server/src/DomainServerSettingsManager.cpp index 661a6213b8..d6b57b450a 100644 --- a/domain-server/src/DomainServerSettingsManager.cpp +++ b/domain-server/src/DomainServerSettingsManager.cpp @@ -246,10 +246,13 @@ void DomainServerSettingsManager::setupConfigMap(const QStringList& argumentList _agentPermissions[editorKey]->set(NodePermissions::Permission::canAdjustLocks); } - QList> permissionsSets; - permissionsSets << _standardAgentPermissions.get() << _agentPermissions.get(); + std::list> permissionsSets{ + _standardAgentPermissions.get(), + _agentPermissions.get() + }; foreach (auto permissionsSet, permissionsSets) { - foreach (NodePermissionsKey userKey, permissionsSet.keys()) { + for (auto entry : permissionsSet) { + const auto& userKey = entry.first; if (onlyEditorsAreRezzers) { if (permissionsSet[userKey]->can(NodePermissions::Permission::canAdjustLocks)) { permissionsSet[userKey]->set(NodePermissions::Permission::canRezPermanentEntities); @@ -300,7 +303,6 @@ void DomainServerSettingsManager::setupConfigMap(const QStringList& argumentList } QVariantMap& DomainServerSettingsManager::getDescriptorsMap() { - static const QString DESCRIPTORS{ "descriptors" }; auto& settingsMap = getSettingsMap(); @@ -1355,18 +1357,12 @@ QStringList DomainServerSettingsManager::getAllKnownGroupNames() { // extract all the group names from the group-permissions and group-forbiddens settings QSet result; - QHashIterator i(_groupPermissions.get()); - while (i.hasNext()) { - i.next(); - NodePermissionsKey key = i.key(); - result += key.first; + for (const auto& entry : _groupPermissions.get()) { + result += entry.first.first; } - QHashIterator j(_groupForbiddens.get()); - while (j.hasNext()) { - j.next(); - NodePermissionsKey key = j.key(); - result += key.first; + for (const auto& entry : _groupForbiddens.get()) { + result += entry.first.first; } return result.toList(); @@ -1377,20 +1373,17 @@ bool DomainServerSettingsManager::setGroupID(const QString& groupName, const QUu _groupIDs[groupName.toLower()] = groupID; _groupNames[groupID] = groupName; - QHashIterator i(_groupPermissions.get()); - while (i.hasNext()) { - i.next(); - NodePermissionsPointer perms = i.value(); + + for (const auto& entry : _groupPermissions.get()) { + auto& perms = entry.second; if (perms->getID().toLower() == groupName.toLower() && !perms->isGroup()) { changed = true; perms->setGroupID(groupID); } } - QHashIterator j(_groupForbiddens.get()); - while (j.hasNext()) { - j.next(); - NodePermissionsPointer perms = j.value(); + for (const auto& entry : _groupForbiddens.get()) { + auto& perms = entry.second; if (perms->getID().toLower() == groupName.toLower() && !perms->isGroup()) { changed = true; perms->setGroupID(groupID); diff --git a/interface/CMakeLists.txt b/interface/CMakeLists.txt index 868a2cf933..726aa7ef84 100644 --- a/interface/CMakeLists.txt +++ b/interface/CMakeLists.txt @@ -63,6 +63,17 @@ qt5_wrap_ui(QT_UI_HEADERS "${QT_UI_FILES}") # add them to the interface source files set(INTERFACE_SRCS ${INTERFACE_SRCS} "${QT_UI_HEADERS}" "${QT_RESOURCES}") +file(GLOB_RECURSE QML_SRC resources/qml/*.qml resources/qml/*.js) +add_custom_target(qml SOURCES ${QML_SRC}) + +if (UNIX) + install( + DIRECTORY "${CMAKE_SOURCE_DIR}/interface/resources/qml" + DESTINATION ${CMAKE_CURRENT_BINARY_DIR}/resources + COMPONENT ${CLIENT_COMPONENT} + ) +endif() + # translation disabled until we strip out the line numbers # set(QM ${TARGET_NAME}_en.qm) # set(TS ${TARGET_NAME}_en.ts) @@ -178,7 +189,7 @@ endif() # link required hifi libraries link_hifi_libraries( - shared octree gpu gl gpu-gl procedural model render + shared octree ktx gpu gl gpu-gl procedural model render recording fbx networking model-networking entities avatars audio audio-client animation script-engine physics render-utils entities-renderer ui auto-updater @@ -277,7 +288,7 @@ if (APPLE) add_custom_command(TARGET ${TARGET_NAME} POST_BUILD COMMAND "${CMAKE_COMMAND}" -E copy_directory "${CMAKE_SOURCE_DIR}/scripts" - $/../Resources/scripts + "$/../Resources/scripts" ) # call the fixup_interface macro to add required bundling commands for installation @@ -288,10 +299,10 @@ else (APPLE) add_custom_command(TARGET ${TARGET_NAME} POST_BUILD COMMAND "${CMAKE_COMMAND}" -E copy_directory "${PROJECT_SOURCE_DIR}/resources" - $/resources + "$/resources" COMMAND "${CMAKE_COMMAND}" -E copy_directory "${CMAKE_SOURCE_DIR}/scripts" - $/scripts + "$/scripts" ) # link target to external libraries @@ -326,7 +337,7 @@ endif() add_bugsplat() if (WIN32) - set(EXTRA_DEPLOY_OPTIONS "--qmldir ${PROJECT_SOURCE_DIR}/resources/qml") + set(EXTRA_DEPLOY_OPTIONS "--qmldir \"${PROJECT_SOURCE_DIR}/resources/qml\"") set(TARGET_INSTALL_DIR ${INTERFACE_INSTALL_DIR}) set(TARGET_INSTALL_COMPONENT ${CLIENT_COMPONENT}) diff --git a/interface/resources/QtWebEngine/UIDelegates/Menu.qml b/interface/resources/QtWebEngine/UIDelegates/Menu.qml index 5176d9d11e..1bbbbd6cbe 100644 --- a/interface/resources/QtWebEngine/UIDelegates/Menu.qml +++ b/interface/resources/QtWebEngine/UIDelegates/Menu.qml @@ -1,7 +1,6 @@ import QtQuick 2.5 import QtQuick.Controls 1.4 as Controls -import "../../qml/menus" import "../../qml/controls-uit" import "../../qml/styles-uit" diff --git a/interface/resources/controllers/kinect.json b/interface/resources/controllers/kinect.json index 96a504030c..eb1d301af9 100644 --- a/interface/resources/controllers/kinect.json +++ b/interface/resources/controllers/kinect.json @@ -2,6 +2,8 @@ "name": "Kinect to Standard", "channels": [ { "from": "Kinect.LeftHand", "to": "Standard.LeftHand" }, - { "from": "Kinect.RightHand", "to": "Standard.RightHand" } + { "from": "Kinect.RightHand", "to": "Standard.RightHand" }, + { "from": "Kinect.LeftFoot", "to": "Standard.LeftFoot" }, + { "from": "Kinect.RightFoot", "to": "Standard.RightFoot" } ] } diff --git a/interface/resources/controllers/standard.json b/interface/resources/controllers/standard.json index 04a3f560b6..53285ea974 100644 --- a/interface/resources/controllers/standard.json +++ b/interface/resources/controllers/standard.json @@ -2,7 +2,27 @@ "name": "Standard to Action", "channels": [ { "from": "Standard.LY", "to": "Actions.TranslateZ" }, - { "from": "Standard.LX", "to": "Actions.TranslateX" }, + + { "from": "Standard.LX", + "when": [ + "Application.InHMD", "!Application.AdvancedMovement", + "Application.SnapTurn", "!Standard.RX" + ], + "to": "Actions.StepYaw", + "filters": + [ + { "type": "deadZone", "min": 0.15 }, + "constrainToInteger", + { "type": "pulse", "interval": 0.25 }, + { "type": "scale", "scale": 22.5 } + ] + }, + { "from": "Standard.LX", "to": "Actions.TranslateX", + "when": [ "Application.AdvancedMovement" ] + }, + { "from": "Standard.LX", "to": "Actions.Yaw", + "when": [ "!Application.AdvancedMovement", "!Application.SnapTurn" ] + }, { "from": "Standard.RX", "when": [ "Application.InHMD", "Application.SnapTurn" ], @@ -15,29 +35,32 @@ { "type": "scale", "scale": 22.5 } ] }, + { "from": "Standard.RX", "to": "Actions.Yaw", + "when": [ "!Application.SnapTurn" ] + }, - { "from": "Standard.RX", "to": "Actions.Yaw" }, - { "from": "Standard.RY", - "when": "Application.Grounded", - "to": "Actions.Up", - "filters": + { "from": "Standard.RY", + "when": "Application.Grounded", + "to": "Actions.Up", + "filters": [ { "type": "deadZone", "min": 0.6 }, "invert" ] - }, + }, - { "from": "Standard.RY", "to": "Actions.Up", "filters": "invert"}, + { "from": "Standard.RY", "to": "Actions.Up", "filters": "invert"}, { "from": "Standard.Back", "to": "Actions.CycleCamera" }, { "from": "Standard.Start", "to": "Actions.ContextMenu" }, - { "from": "Standard.LT", "to": "Actions.LeftHandClick" }, + { "from": "Standard.LT", "to": "Actions.LeftHandClick" }, { "from": "Standard.RT", "to": "Actions.RightHandClick" }, - { "from": "Standard.LeftHand", "to": "Actions.LeftHand" }, - { "from": "Standard.RightHand", "to": "Actions.RightHand" } + { "from": "Standard.LeftHand", "to": "Actions.LeftHand" }, + { "from": "Standard.RightHand", "to": "Actions.RightHand" }, + + { "from": "Standard.LeftFoot", "to": "Actions.LeftFoot" }, + { "from": "Standard.RightFoot", "to": "Actions.RightFoot" } ] } - - diff --git a/interface/resources/fonts/hifi-glyphs.ttf b/interface/resources/fonts/hifi-glyphs.ttf old mode 100755 new mode 100644 diff --git a/interface/resources/html/img/controls-help-oculus.png b/interface/resources/html/img/controls-help-oculus.png index 0bd0a656de..8887bc9ab1 100644 Binary files a/interface/resources/html/img/controls-help-oculus.png and b/interface/resources/html/img/controls-help-oculus.png differ diff --git a/interface/resources/html/img/devices.png b/interface/resources/html/img/devices.png deleted file mode 100644 index fc4231e96e..0000000000 Binary files a/interface/resources/html/img/devices.png and /dev/null differ diff --git a/interface/resources/html/img/models.png b/interface/resources/html/img/models.png deleted file mode 100644 index b09c36011d..0000000000 Binary files a/interface/resources/html/img/models.png and /dev/null differ diff --git a/interface/resources/html/img/move.png b/interface/resources/html/img/move.png deleted file mode 100644 index 4444ebbec5..0000000000 Binary files a/interface/resources/html/img/move.png and /dev/null differ diff --git a/interface/resources/html/img/run-script.png b/interface/resources/html/img/run-script.png deleted file mode 100644 index 941b8ee9f1..0000000000 Binary files a/interface/resources/html/img/run-script.png and /dev/null differ diff --git a/interface/resources/html/img/tablet-help-gamepad.jpg b/interface/resources/html/img/tablet-help-gamepad.jpg new file mode 100644 index 0000000000..5abb38d66b Binary files /dev/null and b/interface/resources/html/img/tablet-help-gamepad.jpg differ diff --git a/interface/resources/html/img/tablet-help-keyboard.jpg b/interface/resources/html/img/tablet-help-keyboard.jpg new file mode 100644 index 0000000000..40c6017561 Binary files /dev/null and b/interface/resources/html/img/tablet-help-keyboard.jpg differ diff --git a/interface/resources/html/img/tablet-help-oculus.jpg b/interface/resources/html/img/tablet-help-oculus.jpg new file mode 100644 index 0000000000..b4f54396e0 Binary files /dev/null and b/interface/resources/html/img/tablet-help-oculus.jpg differ diff --git a/interface/resources/html/img/tablet-help-vive.jpg b/interface/resources/html/img/tablet-help-vive.jpg new file mode 100644 index 0000000000..98e57eef47 Binary files /dev/null and b/interface/resources/html/img/tablet-help-vive.jpg differ diff --git a/interface/resources/html/img/talk.png b/interface/resources/html/img/talk.png deleted file mode 100644 index 682e034ab0..0000000000 Binary files a/interface/resources/html/img/talk.png and /dev/null differ diff --git a/interface/resources/html/img/write-script.png b/interface/resources/html/img/write-script.png deleted file mode 100644 index dae97e59b1..0000000000 Binary files a/interface/resources/html/img/write-script.png and /dev/null differ diff --git a/interface/resources/html/interface-welcome.html b/interface/resources/html/interface-welcome.html deleted file mode 100644 index c803ef864b..0000000000 --- a/interface/resources/html/interface-welcome.html +++ /dev/null @@ -1,187 +0,0 @@ - - - - - - Welcome to Interface - - - - - -
-
-

Move around

- Move around -

- Move around with WASD & fly
- up or down with E & C.
- Cmnd/Ctrl+G will send you
- home. Hitting Enter will let you
- teleport to a user or location. -

-
-
-

Listen & talk

- Talk -

- Use your best headphones
- and microphone for high
- fidelity audio. -

-
-
-

Connect devices

- Connect devices -

- Have an Oculus Rift, a Razer
- Hydra, or a PrimeSense 3D
- camera? We support them all. -

-
-
-

Run a script

- Run a script -

- Cmnd/Cntrl+J will launch a
- Running Scripts dialog to help
- manage your scripts and search
- for new ones to run. -

-
-
-

Script something

- Write a script -

- Write a script; we're always
- adding new features.
- Cmnd/Cntrl+J will launch a
- Running Scripts dialog to help
- manage your scripts. -

-
-
-

Import models

- Import models -

- Use the edit.js script to
- add FBX models in-world. You
- can use grids and fine tune
- placement-related parameters
- with ease. -

-
-
-
-

Read the docs

-

- We are writing documentation on
- just about everything. Please,
- devour all we've written and make
- suggestions where necessary.
- Documentation is always at
- docs.highfidelity.com -

-
-
-
- - - - - diff --git a/interface/resources/html/tabletHelp.html b/interface/resources/html/tabletHelp.html new file mode 100644 index 0000000000..cbd7ffcfe7 --- /dev/null +++ b/interface/resources/html/tabletHelp.html @@ -0,0 +1,157 @@ + + + + + + Welcome to Interface + + + + + + +
+ + + +
+ + + diff --git a/interface/resources/icons/connection.svg b/interface/resources/icons/connection.svg new file mode 100644 index 0000000000..05b23abf9a --- /dev/null +++ b/interface/resources/icons/connection.svg @@ -0,0 +1,48 @@ + + + + + + + + diff --git a/interface/resources/icons/create-icons/20-text-01.svg b/interface/resources/icons/create-icons/20-text-01.svg new file mode 100644 index 0000000000..337f3b70e3 --- /dev/null +++ b/interface/resources/icons/create-icons/20-text-01.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + diff --git a/interface/resources/icons/create-icons/21-cube-01.svg b/interface/resources/icons/create-icons/21-cube-01.svg new file mode 100644 index 0000000000..21a980ca35 --- /dev/null +++ b/interface/resources/icons/create-icons/21-cube-01.svg @@ -0,0 +1,17 @@ + + + + + + + + diff --git a/interface/resources/icons/create-icons/22-sphere-01.svg b/interface/resources/icons/create-icons/22-sphere-01.svg new file mode 100644 index 0000000000..5080a16e78 --- /dev/null +++ b/interface/resources/icons/create-icons/22-sphere-01.svg @@ -0,0 +1,21 @@ + + + + + + + + diff --git a/interface/resources/icons/create-icons/23-zone-01.svg b/interface/resources/icons/create-icons/23-zone-01.svg new file mode 100644 index 0000000000..5428257893 --- /dev/null +++ b/interface/resources/icons/create-icons/23-zone-01.svg @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + diff --git a/interface/resources/icons/create-icons/24-light-01.svg b/interface/resources/icons/create-icons/24-light-01.svg new file mode 100644 index 0000000000..028ea22793 --- /dev/null +++ b/interface/resources/icons/create-icons/24-light-01.svg @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + diff --git a/interface/resources/icons/create-icons/25-web-1-01.svg b/interface/resources/icons/create-icons/25-web-1-01.svg new file mode 100644 index 0000000000..4f0eccc11e --- /dev/null +++ b/interface/resources/icons/create-icons/25-web-1-01.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + diff --git a/interface/resources/icons/create-icons/90-particles-01.svg b/interface/resources/icons/create-icons/90-particles-01.svg new file mode 100644 index 0000000000..5e0105d7cd --- /dev/null +++ b/interface/resources/icons/create-icons/90-particles-01.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + diff --git a/interface/resources/icons/create-icons/94-model-01.svg b/interface/resources/icons/create-icons/94-model-01.svg new file mode 100644 index 0000000000..5d8c4c5eca --- /dev/null +++ b/interface/resources/icons/create-icons/94-model-01.svg @@ -0,0 +1,20 @@ + + + + + diff --git a/interface/resources/icons/load-script.svg b/interface/resources/icons/load-script.svg deleted file mode 100644 index 21be61c321..0000000000 --- a/interface/resources/icons/load-script.svg +++ /dev/null @@ -1,125 +0,0 @@ - - - - - - - - - - image/svg+xml - - - - - T.Hofmeister - - - - - - - - - - - - - - - - - - - - - - diff --git a/interface/resources/icons/new-script.svg b/interface/resources/icons/new-script.svg deleted file mode 100644 index f68fcfa967..0000000000 --- a/interface/resources/icons/new-script.svg +++ /dev/null @@ -1,129 +0,0 @@ - - - - - - - - - - image/svg+xml - - - - - T.Hofmeister - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/interface/resources/icons/profilePicLoading.gif b/interface/resources/icons/profilePicLoading.gif new file mode 100644 index 0000000000..4500f4dda3 Binary files /dev/null and b/interface/resources/icons/profilePicLoading.gif differ diff --git a/interface/resources/icons/save-script.svg b/interface/resources/icons/save-script.svg deleted file mode 100644 index 04d41b8302..0000000000 --- a/interface/resources/icons/save-script.svg +++ /dev/null @@ -1,674 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - image/svg+xml - - - - - T.Hofmeister - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/interface/resources/icons/start-script.svg b/interface/resources/icons/start-script.svg deleted file mode 100644 index 994eb61efe..0000000000 --- a/interface/resources/icons/start-script.svg +++ /dev/null @@ -1,550 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - image/svg+xml - - - - - Maximillian Merlin - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/interface/resources/icons/stop-script.svg b/interface/resources/icons/stop-script.svg deleted file mode 100644 index 31cdcee749..0000000000 --- a/interface/resources/icons/stop-script.svg +++ /dev/null @@ -1,163 +0,0 @@ - - - - - - - - - - image/svg+xml - - - - - Maximillian Merlin - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/interface/resources/icons/tablet-icons/scope-auto.svg b/interface/resources/icons/tablet-icons/scope-auto.svg new file mode 100644 index 0000000000..85ef3f0e38 --- /dev/null +++ b/interface/resources/icons/tablet-icons/scope-auto.svg @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/interface/resources/icons/tablet-icons/scope-pause.svg b/interface/resources/icons/tablet-icons/scope-pause.svg new file mode 100644 index 0000000000..3fe74fcc9f --- /dev/null +++ b/interface/resources/icons/tablet-icons/scope-pause.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + diff --git a/interface/resources/icons/tablet-icons/scope-play.svg b/interface/resources/icons/tablet-icons/scope-play.svg new file mode 100644 index 0000000000..56d90ef38a --- /dev/null +++ b/interface/resources/icons/tablet-icons/scope-play.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + diff --git a/interface/resources/images/sphere-01.svg b/interface/resources/images/sphere-01.svg new file mode 100644 index 0000000000..975199c8da --- /dev/null +++ b/interface/resources/images/sphere-01.svg @@ -0,0 +1,108 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/interface/resources/qml/AssetServer.qml b/interface/resources/qml/AssetServer.qml index cf61a2ae4a..c85fd5b379 100644 --- a/interface/resources/qml/AssetServer.qml +++ b/interface/resources/qml/AssetServer.qml @@ -177,7 +177,7 @@ ScrollingWindow { SHAPE_TYPE_STATIC_MESH ], checkStateOnDisable: false, - warningOnDisable: "Models with automatic collisions set to 'Exact' cannot be dynamic" + warningOnDisable: "Models with 'Exact' automatic collisions cannot be dynamic" } }); @@ -206,7 +206,7 @@ ScrollingWindow { print("Error: model cannot be both static mesh and dynamic. This should never happen."); } else if (url) { var name = assetProxyModel.data(treeView.selection.currentIndex); - var addPosition = Vec3.sum(MyAvatar.position, Vec3.multiply(2, Quat.getFront(MyAvatar.orientation))); + var addPosition = Vec3.sum(MyAvatar.position, Vec3.multiply(2, Quat.getForward(MyAvatar.orientation))); var gravity; if (dynamic) { // Create a vector <0, -10, 0>. { x: 0, y: -10, z: 0 } won't work because Qt is dumb and this is a diff --git a/interface/resources/qml/AvatarInputs.qml b/interface/resources/qml/AvatarInputs.qml index 384504aaa0..28f3c0c7b9 100644 --- a/interface/resources/qml/AvatarInputs.qml +++ b/interface/resources/qml/AvatarInputs.qml @@ -15,12 +15,11 @@ import Qt.labs.settings 1.0 Hifi.AvatarInputs { id: root objectName: "AvatarInputs" - width: mirrorWidth - height: controls.height + mirror.height + width: rootWidth + height: controls.height x: 10; y: 5 - readonly property int mirrorHeight: 215 - readonly property int mirrorWidth: 265 + readonly property int rootWidth: 265 readonly property int iconSize: 24 readonly property int iconPadding: 5 @@ -39,61 +38,15 @@ Hifi.AvatarInputs { anchors.fill: parent } - Item { - id: mirror - width: root.mirrorWidth - height: root.mirrorVisible ? root.mirrorHeight : 0 - visible: root.mirrorVisible - anchors.left: parent.left - clip: true - - Image { - id: closeMirror - visible: hover.containsMouse - width: root.iconSize - height: root.iconSize - anchors.top: parent.top - anchors.topMargin: root.iconPadding - anchors.left: parent.left - anchors.leftMargin: root.iconPadding - source: "../images/close.svg" - MouseArea { - anchors.fill: parent - onClicked: { - root.closeMirror(); - } - } - } - - Image { - id: zoomIn - visible: hover.containsMouse - width: root.iconSize - height: root.iconSize - anchors.bottom: parent.bottom - anchors.bottomMargin: root.iconPadding - anchors.left: parent.left - anchors.leftMargin: root.iconPadding - source: root.mirrorZoomed ? "../images/minus.svg" : "../images/plus.svg" - MouseArea { - anchors.fill: parent - onClicked: { - root.toggleZoom(); - } - } - } - } - Item { id: controls - width: root.mirrorWidth + width: root.rootWidth height: 44 visible: root.showAudioTools - anchors.top: mirror.bottom Rectangle { anchors.fill: parent - color: root.mirrorVisible ? (root.audioClipping ? "red" : "#696969") : "#00000000" + color: "#00000000" Item { id: audioMeter diff --git a/interface/resources/qml/Browser.qml b/interface/resources/qml/Browser.qml index bd98e42709..47fb610469 100644 --- a/interface/resources/qml/Browser.qml +++ b/interface/resources/qml/Browser.qml @@ -2,6 +2,7 @@ import QtQuick 2.5 import QtQuick.Controls 1.2 import QtWebChannel 1.0 import QtWebEngine 1.2 +import FileTypeProfile 1.0 import "controls-uit" import "styles" as HifiStyles @@ -33,6 +34,10 @@ ScrollingWindow { addressBar.text = webview.url } + function setProfile(profile) { + webview.profile = profile; + } + function showPermissionsBar(){ permissionsContainer.visible=true; } @@ -212,6 +217,11 @@ ScrollingWindow { WebChannel.id: "eventBridgeWrapper" property var eventBridge; } + + profile: FileTypeProfile { + id: webviewProfile + storageName: "qmlWebEngine" + } webChannel.registeredObjects: [eventBridgeWrapper] diff --git a/interface/resources/qml/Stats.qml b/interface/resources/qml/Stats.qml index 58d589b667..d6459afd08 100644 --- a/interface/resources/qml/Stats.qml +++ b/interface/resources/qml/Stats.qml @@ -130,7 +130,7 @@ Item { id: pingCol spacing: 4; x: 4; y: 4; StatText { - text: "Audio ping: " + root.audioPing + text: "Audio ping/loss: " + root.audioPing + "/" + root.audioPacketLoss + "%" } StatText { text: "Avatar ping: " + root.avatarPing @@ -181,6 +181,31 @@ Item { root.avatarMixerOutPps + "pps, " + root.myAvatarSendRate.toFixed(2) + "hz"; } + StatText { + visible: root.expanded; + text: "Audio Mixer In: " + root.audioMixerInKbps + " kbps, " + + root.audioMixerInPps + "pps"; + } + StatText { + visible: root.expanded; + text: "Audio In Audio: " + root.audioAudioInboundPPS + " pps, " + + "Silent: " + root.audioSilentInboundPPS + " pps"; + } + StatText { + visible: root.expanded; + text: "Audio Mixer Out: " + root.audioMixerOutKbps + " kbps, " + + root.audioMixerOutPps + "pps"; + } + StatText { + visible: root.expanded; + text: "Audio Out Mic: " + root.audioOutboundPPS + " pps, " + + "Silent: " + root.audioSilentOutboundPPS + " pps"; + } + StatText { + visible: root.expanded; + text: "Audio Codec: " + root.audioCodec + " Noise Gate: " + + root.audioNoiseGate; + } StatText { visible: root.expanded; text: "Downloads: " + root.downloads + "/" + root.downloadLimit + @@ -241,7 +266,7 @@ Item { text: "GPU Textures: "; } StatText { - text: " Sparse Enabled: " + (0 == root.gpuSparseTextureEnabled ? "false" : "true"); + text: " Pressure State: " + root.gpuTextureMemoryPressureState; } StatText { text: " Count: " + root.gpuTextures; @@ -253,14 +278,10 @@ Item { text: " Decimated: " + root.decimatedTextureCount; } StatText { - text: " Sparse Count: " + root.gpuTexturesSparse; - visible: 0 != root.gpuSparseTextureEnabled; + text: " Pending Transfer: " + root.texturePendingTransfers + " MB"; } StatText { - text: " Virtual Memory: " + root.gpuTextureVirtualMemory + " MB"; - } - StatText { - text: " Commited Memory: " + root.gpuTextureMemory + " MB"; + text: " Resource Memory: " + root.gpuTextureMemory + " MB"; } StatText { text: " Framebuffer Memory: " + root.gpuTextureFramebufferMemory + " MB"; diff --git a/interface/resources/qml/TabletBrowser.qml b/interface/resources/qml/TabletBrowser.qml new file mode 100644 index 0000000000..74318a165e --- /dev/null +++ b/interface/resources/qml/TabletBrowser.qml @@ -0,0 +1,134 @@ +import QtQuick 2.5 +import QtQuick.Controls 1.4 +import QtWebChannel 1.0 +import QtWebEngine 1.2 + +import "controls" +import "styles" as HifiStyles +import "styles-uit" +import "windows" +import HFTabletWebEngineProfile 1.0 + +Item { + id: root + HifiConstants { id: hifi } + HifiStyles.HifiConstants { id: hifistyles } + //width: parent.width + height: 600 + property variant permissionsBar: {'securityOrigin':'none','feature':'none'} + property alias url: webview.url + property WebEngineView webView: webview + property alias eventBridge: eventBridgeWrapper.eventBridge + property bool canGoBack: webview.canGoBack + property bool canGoForward: webview.canGoForward + + + signal loadingChanged(int status) + + x: 0 + y: 0 + + function setProfile(profile) { + webview.profile = profile; + } + + QtObject { + id: eventBridgeWrapper + WebChannel.id: "eventBridgeWrapper" + property var eventBridge; + } + + WebEngineView { + id: webview + objectName: "webEngineView" + x: 0 + y: 0 + width: parent.width + height: keyboardEnabled && keyboardRaised ? parent.height - keyboard.height : parent.height + + profile: HFTabletWebEngineProfile { + id: webviewTabletProfile + storageName: "qmlTabletWebEngine" + } + + property string userScriptUrl: "" + + // creates a global EventBridge object. + WebEngineScript { + id: createGlobalEventBridge + sourceCode: eventBridgeJavaScriptToInject + injectionPoint: WebEngineScript.DocumentCreation + worldId: WebEngineScript.MainWorld + } + + // detects when to raise and lower virtual keyboard + WebEngineScript { + id: raiseAndLowerKeyboard + injectionPoint: WebEngineScript.Deferred + sourceUrl: resourceDirectoryUrl + "/html/raiseAndLowerKeyboard.js" + worldId: WebEngineScript.MainWorld + } + + // User script. + WebEngineScript { + id: userScript + sourceUrl: webview.userScriptUrl + injectionPoint: WebEngineScript.DocumentReady // DOM ready but page load may not be finished. + worldId: WebEngineScript.MainWorld + } + + userScripts: [ createGlobalEventBridge, raiseAndLowerKeyboard, userScript ] + + property string newUrl: "" + + webChannel.registeredObjects: [eventBridgeWrapper] + + Component.onCompleted: { + // Ensure the JS from the web-engine makes it to our logging + webview.javaScriptConsoleMessage.connect(function(level, message, lineNumber, sourceID) { + console.log("Web Entity JS message: " + sourceID + " " + lineNumber + " " + message); + }); + + webview.profile.httpUserAgent = "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Mobile Safari/537.36"; + web.address = url; + } + + onFeaturePermissionRequested: { + grantFeaturePermission(securityOrigin, feature, true); + } + + onLoadingChanged: { + keyboardRaised = false; + punctuationMode = false; + keyboard.resetShiftMode(false); + + // Required to support clicking on "hifi://" links + if (WebEngineView.LoadStartedStatus == loadRequest.status) { + urlAppend(loadRequest.url.toString()) + var url = loadRequest.url.toString(); + if (urlHandler.canHandleUrl(url)) { + if (urlHandler.handleUrl(url)) { + root.stop(); + } + } + } + } + + onNewViewRequested: { + request.openIn(webView); + } + } + + Keys.onPressed: { + switch(event.key) { + case Qt.Key_L: + if (event.modifiers == Qt.ControlModifier) { + event.accepted = true + addressBar.selectAll() + addressBar.forceActiveFocus() + } + break; + } + } + +} diff --git a/interface/resources/qml/TabletLoginDialog/CompleteProfileBody.qml b/interface/resources/qml/TabletLoginDialog/CompleteProfileBody.qml new file mode 100644 index 0000000000..6024563bcf --- /dev/null +++ b/interface/resources/qml/TabletLoginDialog/CompleteProfileBody.qml @@ -0,0 +1,124 @@ +// +// CompleteProfileBody.qml +// +// Created by Clement on 7/18/16 +// Copyright 2015 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +import Hifi 1.0 +import QtQuick 2.4 +import QtQuick.Controls.Styles 1.4 as OriginalStyles + +import "../controls-uit" +import "../styles-uit" + +Item { + id: completeProfileBody + clip: true + + QtObject { + id: d + function resize() {} + } + + Row { + id: buttons + anchors { + top: parent.top + horizontalCenter: parent.horizontalCenter + margins: 0 + topMargin: 2 * hifi.dimensions.contentSpacing.y + } + spacing: hifi.dimensions.contentSpacing.x + onHeightChanged: d.resize(); onWidthChanged: d.resize(); + + Button { + anchors.verticalCenter: parent.verticalCenter + width: 200 + + text: qsTr("Create your profile") + color: hifi.buttons.blue + + onClicked: loginDialog.createAccountFromStream() + } + + Button { + anchors.verticalCenter: parent.verticalCenter + + text: qsTr("Cancel") + + onClicked: bodyLoader.popup() + } + } + + ShortcutText { + id: additionalTextContainer + anchors { + top: buttons.bottom + horizontalCenter: parent.horizontalCenter + margins: 0 + topMargin: hifi.dimensions.contentSpacing.y + } + + text: "Already have a High Fidelity profile? Link to an existing profile here." + + wrapMode: Text.WordWrap + lineHeight: 2 + lineHeightMode: Text.ProportionalHeight + horizontalAlignment: Text.AlignHCenter + + onLinkActivated: { + bodyLoader.setSource("LinkAccountBody.qml") + } + } + + InfoItem { + id: termsContainer + anchors { + top: additionalTextContainer.bottom + left: parent.left + margins: 0 + topMargin: 2 * hifi.dimensions.contentSpacing.y + } + + text: qsTr("By creating this user profile, you agree to High Fidelity's Terms of Service") + wrapMode: Text.WordWrap + color: hifi.colors.baseGrayHighlight + lineHeight: 1 + lineHeightMode: Text.ProportionalHeight + horizontalAlignment: Text.AlignHCenter + + onLinkActivated: loginDialog.openUrl(link) + } + + Component.onCompleted: { + loginDialogRoot.title = qsTr("Complete Your Profile") + loginDialogRoot.iconText = "<" + d.resize(); + } + + Connections { + target: loginDialog + onHandleCreateCompleted: { + console.log("Create Succeeded") + + loginDialog.loginThroughSteam() + } + onHandleCreateFailed: { + console.log("Create Failed: " + error) + + bodyLoadersetSource("UsernameCollisionBody.qml") + } + onHandleLoginCompleted: { + console.log("Login Succeeded") + + bodyLoader.setSource("WelcomeBody.qml", { "welcomeBack" : false }) + } + onHandleLoginFailed: { + console.log("Login Failed") + } + } +} diff --git a/interface/resources/qml/TabletLoginDialog/LinkAccountBody.qml b/interface/resources/qml/TabletLoginDialog/LinkAccountBody.qml new file mode 100644 index 0000000000..8010a34250 --- /dev/null +++ b/interface/resources/qml/TabletLoginDialog/LinkAccountBody.qml @@ -0,0 +1,296 @@ +// +// LinkAccountBody.qml +// +// Created by Clement on 7/18/16 +// Copyright 2015 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +import Hifi 1.0 +import QtQuick 2.4 +import QtQuick.Controls 1.4 +import QtQuick.Controls.Styles 1.4 as OriginalStyles + +import "../controls-uit" +import "../styles-uit" + +Item { + id: linkAccountBody + clip: true + height: parent.height + width: parent.width + property bool failAfterSignUp: false + + function login() { + mainTextContainer.visible = false + toggleLoading(true) + loginDialog.login(usernameField.text, passwordField.text) + } + + property bool keyboardEnabled: false + property bool keyboardRaised: false + property bool punctuationMode: false + + onKeyboardRaisedChanged: d.resize(); + + QtObject { + id: d + function resize() {} + } + + function toggleLoading(isLoading) { + linkAccountSpinner.visible = isLoading + form.visible = !isLoading + + if (loginDialog.isSteamRunning()) { + additionalInformation.visible = !isLoading + } + + leftButton.visible = !isLoading + buttons.visible = !isLoading + } + + BusyIndicator { + id: linkAccountSpinner + + anchors { + top: parent.top + horizontalCenter: parent.horizontalCenter + topMargin: hifi.dimensions.contentSpacing.y + } + + visible: false + running: true + + width: 48 + height: 48 + } + + ShortcutText { + id: mainTextContainer + anchors { + top: parent.top + left: parent.left + margins: 0 + topMargin: hifi.dimensions.contentSpacing.y + } + + visible: false + + text: qsTr("Username or password incorrect.") + wrapMode: Text.WordWrap + color: hifi.colors.redAccent + lineHeight: 1 + lineHeightMode: Text.ProportionalHeight + horizontalAlignment: Text.AlignHCenter + } + + Column { + id: form + anchors { + top: mainTextContainer.bottom + left: parent.left + margins: 0 + topMargin: 2 * hifi.dimensions.contentSpacing.y + } + spacing: 2 * hifi.dimensions.contentSpacing.y + + Row { + spacing: hifi.dimensions.contentSpacing.x + + TextField { + id: usernameField + anchors { + verticalCenter: parent.verticalCenter + } + width: 350 + + label: "Username or Email" + } + + ShortcutText { + anchors { + verticalCenter: parent.verticalCenter + } + + text: "Forgot Username?" + + verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignHCenter + linkColor: hifi.colors.blueAccent + + onLinkActivated: loginDialog.openUrl(link) + } + } + Row { + spacing: hifi.dimensions.contentSpacing.x + + TextField { + id: passwordField + anchors { + verticalCenter: parent.verticalCenter + } + width: 350 + + label: "Password" + echoMode: TextInput.Password + } + + ShortcutText { + anchors { + verticalCenter: parent.verticalCenter + } + + text: "Forgot Password?" + + verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignHCenter + linkColor: hifi.colors.blueAccent + + onLinkActivated: loginDialog.openUrl(link) + } + } + + } + + InfoItem { + id: additionalInformation + anchors { + top: form.bottom + left: parent.left + margins: 0 + topMargin: hifi.dimensions.contentSpacing.y + } + + visible: loginDialog.isSteamRunning() + + text: qsTr("Your steam account informations will not be exposed to other users.") + wrapMode: Text.WordWrap + color: hifi.colors.baseGrayHighlight + lineHeight: 1 + lineHeightMode: Text.ProportionalHeight + horizontalAlignment: Text.AlignHCenter + } + + // Override ScrollingWindow's keyboard that would be at very bottom of dialog. + Keyboard { + raised: keyboardEnabled && keyboardRaised + numeric: punctuationMode + anchors { + left: parent.left + right: parent.right + bottom: buttons.top + bottomMargin: keyboardRaised ? 2 * hifi.dimensions.contentSpacing.y : 0 + } + } + + Row { + id: leftButton + anchors { + left: parent.left + bottom: parent.bottom + bottomMargin: hifi.dimensions.contentSpacing.y + } + + spacing: hifi.dimensions.contentSpacing.x + onHeightChanged: d.resize(); onWidthChanged: d.resize(); + + Button { + anchors.verticalCenter: parent.verticalCenter + + text: qsTr("Sign Up") + visible: !loginDialog.isSteamRunning() + + onClicked: { + bodyLoader.setSource("SignUpBody.qml") + } + } + } + + Row { + id: buttons + anchors { + right: parent.right + bottom: parent.bottom + bottomMargin: hifi.dimensions.contentSpacing.y + } + spacing: hifi.dimensions.contentSpacing.x + onHeightChanged: d.resize(); onWidthChanged: d.resize(); + + Button { + id: linkAccountButton + anchors.verticalCenter: parent.verticalCenter + width: 200 + + text: qsTr(loginDialog.isSteamRunning() ? "Link Account" : "Login") + color: hifi.buttons.blue + + onClicked: linkAccountBody.login() + } + + Button { + anchors.verticalCenter: parent.verticalCenter + text: qsTr("Cancel") + onClicked: { + bodyLoader.popup() + } + } + } + + Component.onCompleted: { + loginDialogRoot.title = qsTr("Sign Into High Fidelity") + loginDialogRoot.iconText = "<" + keyboardEnabled = HMD.active; + d.resize(); + + if (failAfterSignUp) { + mainTextContainer.text = "Account created successfully." + mainTextContainer.visible = true + } + + usernameField.forceActiveFocus(); + } + + Connections { + target: loginDialog + onHandleLoginCompleted: { + console.log("Login Succeeded, linking steam account") + + if (loginDialog.isSteamRunning()) { + loginDialog.linkSteam() + } else { + bodyLoader.setSource("WelcomeBody.qml", { "welcomeBack" : true }) + } + } + onHandleLoginFailed: { + console.log("Login Failed") + mainTextContainer.visible = true + toggleLoading(false) + } + onHandleLinkCompleted: { + console.log("Link Succeeded") + + bodyLoader.setSource("WelcomeBody.qml", { "welcomeBack" : true }) + } + onHandleLinkFailed: { + console.log("Link Failed") + toggleLoading(false) + } + } + + Keys.onPressed: { + if (!visible) { + return + } + + switch (event.key) { + case Qt.Key_Enter: + case Qt.Key_Return: + event.accepted = true + linkAccountBody.login() + break + } + } +} diff --git a/interface/resources/qml/TabletLoginDialog/SignInBody.qml b/interface/resources/qml/TabletLoginDialog/SignInBody.qml new file mode 100644 index 0000000000..9cdf69c7bc --- /dev/null +++ b/interface/resources/qml/TabletLoginDialog/SignInBody.qml @@ -0,0 +1,109 @@ +// +// SignInBody.qml +// +// Created by Clement on 7/18/16 +// Copyright 2015 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +import Hifi 1.0 +import QtQuick 2.4 +import QtQuick.Controls.Styles 1.4 as OriginalStyles + +import "../controls-uit" +import "../styles-uit" + +Item { + id: signInBody + clip: true + + property bool required: false + + function login() { + console.log("Trying to log in") + loginDialog.loginThroughSteam() + } + + function cancel() { + bodyLoader.popup() + } + + QtObject { + id: d + function resize() {} + } + + InfoItem { + id: mainTextContainer + anchors { + top: parent.top + horizontalCenter: parent.horizontalCenter + margins: 0 + topMargin: hifi.dimensions.contentSpacing.y + } + + text: required ? qsTr("This domain's owner requires that you sign in:") + : qsTr("Sign in to access your user account:") + wrapMode: Text.WordWrap + color: hifi.colors.baseGrayHighlight + lineHeight: 2 + lineHeightMode: Text.ProportionalHeight + horizontalAlignment: Text.AlignHCenter + } + + Row { + id: buttons + anchors { + top: mainTextContainer.bottom + horizontalCenter: parent.horizontalCenter + margins: 0 + topMargin: 2 * hifi.dimensions.contentSpacing.y + } + spacing: hifi.dimensions.contentSpacing.x + onHeightChanged: d.resize(); onWidthChanged: d.resize(); + + Button { + anchors.verticalCenter: parent.verticalCenter + + width: undefined // invalidate so that the image's size sets the width + height: undefined // invalidate so that the image's size sets the height + focus: true + + style: OriginalStyles.ButtonStyle { + background: Image { + id: buttonImage + source: "../../images/steam-sign-in.png" + } + } + onClicked: signInBody.login() + } + Button { + anchors.verticalCenter: parent.verticalCenter + + text: qsTr("Cancel"); + + onClicked: signInBody.cancel() + } + } + + Component.onCompleted: { + loginDialogRoot.title = required ? qsTr("Sign In Required") + : qsTr("Sign In") + loginDialogRoot.iconText = "" + d.resize(); + } + + Connections { + target: loginDialog + onHandleLoginCompleted: { + console.log("Login Succeeded") + bodyLoader.setSource("WelcomeBody.qml", { "welcomeBack" : true }) + } + onHandleLoginFailed: { + console.log("Login Failed") + bodyLoader.setSource("CompleteProfileBody.qml") + } + } +} diff --git a/interface/resources/qml/TabletLoginDialog/SignUpBody.qml b/interface/resources/qml/TabletLoginDialog/SignUpBody.qml new file mode 100644 index 0000000000..2cfc0e736a --- /dev/null +++ b/interface/resources/qml/TabletLoginDialog/SignUpBody.qml @@ -0,0 +1,276 @@ +// +// SignUpBody.qml +// +// Created by Stephen Birarda on 7 Dec 2016 +// Copyright 2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +import Hifi 1.0 +import QtQuick 2.4 +import QtQuick.Controls 1.4 +import QtQuick.Controls.Styles 1.4 as OriginalStyles + +import "../controls-uit" +import "../styles-uit" + +Item { + id: signupBody + clip: true +// height: parent.height +// width: parent.width + + function signup() { + mainTextContainer.visible = false + toggleLoading(true) + loginDialog.signup(emailField.text, usernameField.text, passwordField.text) + } + + property bool keyboardEnabled: false + property bool keyboardRaised: false + property bool punctuationMode: false + + onKeyboardRaisedChanged: d.resize(); + + QtObject { + id: d + function resize() {} + } + + function toggleLoading(isLoading) { + linkAccountSpinner.visible = isLoading + form.visible = !isLoading + + leftButton.visible = !isLoading + buttons.visible = !isLoading + } + + BusyIndicator { + id: linkAccountSpinner + + anchors { + top: parent.top + horizontalCenter: parent.horizontalCenter + topMargin: hifi.dimensions.contentSpacing.y + } + + visible: false + running: true + + width: 48 + height: 48 + } + + ShortcutText { + id: mainTextContainer + anchors { + top: parent.top + left: parent.left + margins: 0 + topMargin: hifi.dimensions.contentSpacing.y + } + + visible: false + + text: qsTr("There was an unknown error while creating your account.") + wrapMode: Text.WordWrap + color: hifi.colors.redAccent + horizontalAlignment: Text.AlignLeft + } + + Column { + id: form + anchors { + top: mainTextContainer.bottom + left: parent.left + margins: 0 + topMargin: 2 * hifi.dimensions.contentSpacing.y + } + spacing: 2 * hifi.dimensions.contentSpacing.y + + Row { + spacing: hifi.dimensions.contentSpacing.x + + TextField { + id: emailField + anchors { + verticalCenter: parent.verticalCenter + } + width: 300 + + label: "Email" + } + } + + Row { + spacing: hifi.dimensions.contentSpacing.x + + TextField { + id: usernameField + anchors { + verticalCenter: parent.verticalCenter + } + width: 300 + + label: "Username" + } + + ShortcutText { + anchors { + verticalCenter: parent.verticalCenter + } + + text: qsTr("No spaces / special chars.") + + verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignHCenter + + color: hifi.colors.blueAccent + } + } + + Row { + spacing: hifi.dimensions.contentSpacing.x + + TextField { + id: passwordField + anchors { + verticalCenter: parent.verticalCenter + } + width: 300 + + label: "Password" + echoMode: TextInput.Password + } + + ShortcutText { + anchors { + verticalCenter: parent.verticalCenter + } + + text: qsTr("At least 6 characters") + + verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignHCenter + + color: hifi.colors.blueAccent + } + } + + } + + // Override ScrollingWindow's keyboard that would be at very bottom of dialog. + Keyboard { + raised: keyboardEnabled && keyboardRaised + numeric: punctuationMode + anchors { + left: parent.left + right: parent.right + bottom: buttons.top + bottomMargin: keyboardRaised ? 2 * hifi.dimensions.contentSpacing.y : 0 + } + } + + Row { + id: leftButton + anchors { + left: parent.left + bottom: parent.bottom + bottomMargin: hifi.dimensions.contentSpacing.y + } + + spacing: hifi.dimensions.contentSpacing.x + onHeightChanged: d.resize(); onWidthChanged: d.resize(); + + Button { + anchors.verticalCenter: parent.verticalCenter + + text: qsTr("Existing User") + + onClicked: { + bodyLoader.setSource("LinkAccountBody.qml") + } + } + } + + Row { + id: buttons + anchors { + right: parent.right + bottom: parent.bottom + bottomMargin: hifi.dimensions.contentSpacing.y + } + spacing: hifi.dimensions.contentSpacing.x + onHeightChanged: d.resize(); onWidthChanged: d.resize(); + + Button { + id: linkAccountButton + anchors.verticalCenter: parent.verticalCenter + width: 200 + + text: qsTr("Sign Up") + color: hifi.buttons.blue + + onClicked: signupBody.signup() + } + + Button { + anchors.verticalCenter: parent.verticalCenter + + text: qsTr("Cancel") + + onClicked: bodyLoader.popup() + } + } + + Component.onCompleted: { + loginDialogRoot.title = qsTr("Create an Account") + loginDialogRoot.iconText = "<" + keyboardEnabled = HMD.active; + d.resize(); + + emailField.forceActiveFocus(); + } + + Connections { + target: loginDialog + onHandleSignupCompleted: { + console.log("Sign Up Succeeded"); + + // now that we have an account, login with that username and password + loginDialog.login(usernameField.text, passwordField.text) + } + onHandleSignupFailed: { + console.log("Sign Up Failed") + toggleLoading(false) + + mainTextContainer.text = errorString + mainTextContainer.visible = true + + d.resize(); + } + onHandleLoginCompleted: { + bodyLoader.setSource("WelcomeBody.qml", { "welcomeBack": false }) + } + onHandleLoginFailed: { + // we failed to login, show the LoginDialog so the user will try again + bodyLoader.setSource("LinkAccountBody.qml", { "failAfterSignUp": true }) + } + } + + Keys.onPressed: { + if (!visible) { + return + } + + switch (event.key) { + case Qt.Key_Enter: + case Qt.Key_Return: + event.accepted = true + signupBody.signup() + break + } + } +} diff --git a/interface/resources/qml/TabletLoginDialog/UsernameCollisionBody.qml b/interface/resources/qml/TabletLoginDialog/UsernameCollisionBody.qml new file mode 100644 index 0000000000..9e5b01d339 --- /dev/null +++ b/interface/resources/qml/TabletLoginDialog/UsernameCollisionBody.qml @@ -0,0 +1,157 @@ +// +// UsernameCollisionBody.qml +// +// Created by Clement on 7/18/16 +// Copyright 2015 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +import Hifi 1.0 +import QtQuick 2.4 +import QtQuick.Controls 1.4 +import QtQuick.Controls.Styles 1.4 as OriginalStyles + +import "../controls-uit" +import "../styles-uit" + +Item { + id: usernameCollisionBody + clip: true + width: parent.width + height: parent.height + + function create() { + mainTextContainer.visible = false + loginDialog.createAccountFromStream(textField.text) + } + + + property bool keyboardEnabled: false + property bool keyboardRaised: false + property bool punctuationMode: false + + onKeyboardRaisedChanged: d.resize(); + + QtObject { + id: d + function resize() {} + } + + ShortcutText { + id: mainTextContainer + anchors { + top: parent.top + left: parent.left + margins: 0 + topMargin: hifi.dimensions.contentSpacing.y + } + + text: qsTr("Your Steam username is not available.") + wrapMode: Text.WordWrap + color: hifi.colors.redAccent + lineHeight: 1 + lineHeightMode: Text.ProportionalHeight + horizontalAlignment: Text.AlignHCenter + } + + + TextField { + id: textField + anchors { + top: mainTextContainer.bottom + left: parent.left + margins: 0 + topMargin: hifi.dimensions.contentSpacing.y + } + width: 250 + + placeholderText: "Choose your own" + } + + // Override ScrollingWindow's keyboard that would be at very bottom of dialog. + Keyboard { + raised: keyboardEnabled && keyboardRaised + numeric: punctuationMode + anchors { + left: parent.left + right: parent.right + bottom: buttons.top + bottomMargin: keyboardRaised ? 2 * hifi.dimensions.contentSpacing.y : 0 + } + } + + Row { + id: buttons + anchors { + bottom: parent.bottom + right: parent.right + margins: 0 + topMargin: hifi.dimensions.contentSpacing.y + } + spacing: hifi.dimensions.contentSpacing.x + onHeightChanged: d.resize(); onWidthChanged: d.resize(); + + Button { + anchors.verticalCenter: parent.verticalCenter + width: 200 + + text: qsTr("Create your profile") + color: hifi.buttons.blue + + onClicked: usernameCollisionBody.create() + } + + Button { + anchors.verticalCenter: parent.verticalCenter + + text: qsTr("Cancel") + onClicked: bodyLoader.popup() + } + } + + Component.onCompleted: { + loginDialogRoot.title = qsTr("Complete Your Profile") + loginDialogRoot.iconText = "<" + keyboardEnabled = HMD.active; + d.resize(); + } + + Connections { + target: loginDialog + onHandleCreateCompleted: { + console.log("Create Succeeded") + + loginDialog.loginThroughSteam() + } + onHandleCreateFailed: { + console.log("Create Failed: " + error) + + mainTextContainer.visible = true + mainTextContainer.text = "\"" + textField.text + qsTr("\" is invalid or already taken.") + } + onHandleLoginCompleted: { + console.log("Login Succeeded") + + bodyLoader.setSource("WelcomeBody.qml", { "welcomeBack" : false }) + } + onHandleLoginFailed: { + console.log("Login Failed") + } + } + + Keys.onPressed: { + if (!visible) { + return + } + + switch (event.key) { + case Qt.Key_Enter: + case Qt.Key_Return: + event.accepted = true + usernameCollisionBody.create() + break + } + } +} diff --git a/interface/resources/qml/TabletLoginDialog/WelcomeBody.qml b/interface/resources/qml/TabletLoginDialog/WelcomeBody.qml new file mode 100644 index 0000000000..5ec259ca96 --- /dev/null +++ b/interface/resources/qml/TabletLoginDialog/WelcomeBody.qml @@ -0,0 +1,79 @@ +// +// WelcomeBody.qml +// +// Created by Clement on 7/18/16 +// Copyright 2015 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +import Hifi 1.0 +import QtQuick 2.4 + +import "../controls-uit" +import "../styles-uit" + +Item { + id: welcomeBody + clip: true + + property bool welcomeBack: false + + function setTitle() { + loginDialogRoot.title = (welcomeBack ? qsTr("Welcome back ") : qsTr("Welcome ")) + Account.username + qsTr("!") + loginDialogRoot.iconText = "" + d.resize(); + } + + QtObject { + id: d + function resize() {} + } + + InfoItem { + id: mainTextContainer + anchors { + top: parent.top + horizontalCenter: parent.horizontalCenter + margins: 0 + topMargin: hifi.dimensions.contentSpacing.y + } + + text: qsTr("You are now signed into High Fidelity") + wrapMode: Text.WordWrap + color: hifi.colors.baseGrayHighlight + lineHeight: 2 + lineHeightMode: Text.ProportionalHeight + horizontalAlignment: Text.AlignHCenter + } + + Row { + id: buttons + anchors { + top: mainTextContainer.bottom + horizontalCenter: parent.horizontalCenter + margins: 0 + topMargin: 2 * hifi.dimensions.contentSpacing.y + } + spacing: hifi.dimensions.contentSpacing.x + onHeightChanged: d.resize(); onWidthChanged: d.resize(); + + Button { + anchors.verticalCenter: parent.verticalCenter + + text: qsTr("Close"); + + onClicked: bodyLoader.popup() + } + } + + Component.onCompleted: { + welcomeBody.setTitle() + } + + Connections { + target: Account + onUsernameChanged: welcomeBody.setTitle() + } +} diff --git a/interface/resources/qml/controls-uit/AttachmentsTable.qml b/interface/resources/qml/controls-uit/AttachmentsTable.qml index 7d0280b72d..8ee9909ab8 100644 --- a/interface/resources/qml/controls-uit/AttachmentsTable.qml +++ b/interface/resources/qml/controls-uit/AttachmentsTable.qml @@ -120,7 +120,7 @@ TableView { } rowDelegate: Rectangle { - height: (styleData.selected ? 1.2 : 1) * hifi.dimensions.tableRowHeight + height: hifi.dimensions.tableRowHeight color: styleData.selected ? hifi.colors.primaryHighlight : tableView.isLightColorScheme diff --git a/interface/resources/qml/controls-uit/BaseWebView.qml b/interface/resources/qml/controls-uit/BaseWebView.qml index 763e6530fb..9c22a8ff8c 100644 --- a/interface/resources/qml/controls-uit/BaseWebView.qml +++ b/interface/resources/qml/controls-uit/BaseWebView.qml @@ -15,7 +15,7 @@ import HFWebEngineProfile 1.0 WebEngineView { id: root - profile: desktop.browserProfile + // profile: desktop.browserProfile Component.onCompleted: { console.log("Connecting JS messaging to Hifi Logging") diff --git a/interface/resources/qml/controls-uit/CheckBox.qml b/interface/resources/qml/controls-uit/CheckBox.qml index 09a0e04148..916a7d4889 100644 --- a/interface/resources/qml/controls-uit/CheckBox.qml +++ b/interface/resources/qml/controls-uit/CheckBox.qml @@ -19,11 +19,12 @@ Original.CheckBox { property int colorScheme: hifi.colorSchemes.light readonly property bool isLightColorScheme: colorScheme == hifi.colorSchemes.light - + property bool isRedCheck: false property int boxSize: 14 readonly property int boxRadius: 3 readonly property int checkSize: Math.max(boxSize - 8, 10) readonly property int checkRadius: 2 + activeFocusOnPress: true style: CheckBoxStyle { indicator: Rectangle { @@ -35,6 +36,7 @@ Original.CheckBox { border.color: pressed || hovered ? hifi.colors.checkboxCheckedBorder : (checkBox.isLightColorScheme ? hifi.colors.checkboxLightFinish : hifi.colors.checkboxDarkFinish) + gradient: Gradient { GradientStop { position: 0.2 @@ -66,9 +68,9 @@ Original.CheckBox { height: checkSize radius: checkRadius anchors.centerIn: parent - color: hifi.colors.checkboxChecked + color: isRedCheck ? hifi.colors.checkboxCheckedRed : hifi.colors.checkboxChecked border.width: 2 - border.color: hifi.colors.checkboxCheckedBorder + border.color: isRedCheck? hifi.colors.checkboxCheckedBorderRed : hifi.colors.checkboxCheckedBorder visible: checked && !pressed || !checked && pressed } @@ -88,7 +90,7 @@ Original.CheckBox { label: Label { text: control.text colorScheme: checkBox.colorScheme - x: checkBox.boxSize / 2 + x: 2 wrapMode: Text.Wrap enabled: checkBox.enabled } diff --git a/interface/resources/qml/controls-uit/ComboBox.qml b/interface/resources/qml/controls-uit/ComboBox.qml index 573fed4ef9..be6a439e57 100644 --- a/interface/resources/qml/controls-uit/ComboBox.qml +++ b/interface/resources/qml/controls-uit/ComboBox.qml @@ -14,7 +14,6 @@ import QtQuick.Controls.Styles 1.4 import "../styles-uit" import "../controls-uit" as HifiControls -import "." as VrControls FocusScope { id: root @@ -25,6 +24,7 @@ FocusScope { readonly property alias currentText: comboBox.currentText; property alias currentIndex: comboBox.currentIndex; + property int dropdownHeight: 480 property int colorScheme: hifi.colorSchemes.light readonly property bool isLightColorScheme: colorScheme == hifi.colorSchemes.light property string label: "" @@ -32,6 +32,8 @@ FocusScope { readonly property ComboBox control: comboBox + property bool isDesktop: true + signal accepted(); implicitHeight: comboBox.height; @@ -119,11 +121,17 @@ FocusScope { } function showList() { - var r = desktop.mapFromItem(root, 0, 0, root.width, root.height); + var r; + if (isDesktop) { + r = desktop.mapFromItem(root, 0, 0, root.width, root.height); + } else { + r = mapFromItem(root, 0, 0, root.width, root.height); + } var y = r.y + r.height; var bottom = y + scrollView.height; - if (bottom > desktop.height) { - y -= bottom - desktop.height + 8; + var height = isDesktop ? desktop.height : tabletRoot.height; + if (bottom > height) { + y -= bottom - height + 8; } scrollView.x = r.x; scrollView.y = y; @@ -141,9 +149,9 @@ FocusScope { FocusScope { id: popup - parent: desktop + parent: isDesktop ? desktop : root anchors.fill: parent - z: desktop.zLevels.menu + z: isDesktop ? desktop.zLevels.menu : 12 visible: false focus: true @@ -166,7 +174,7 @@ FocusScope { ScrollView { id: scrollView - height: 480 + height: root.dropdownHeight width: root.width + 4 property bool hoverEnabled: false; @@ -178,18 +186,18 @@ FocusScope { visible: false } scrollBarBackground: Rectangle{ - implicitWidth: 14 + implicitWidth: 20 color: hifi.colors.baseGray } handle: Rectangle { - implicitWidth: 8 + implicitWidth: 16 anchors.left: parent.left anchors.leftMargin: 3 anchors.top: parent.top anchors.bottom: parent.bottom - radius: 3 + radius: 6 color: hifi.colors.lightGrayText } } @@ -208,7 +216,7 @@ FocusScope { anchors.leftMargin: hifi.dimensions.textPadding anchors.verticalCenter: parent.verticalCenter id: popupText - text: listView.model[index] ? listView.model[index] : "" + text: listView.model[index] ? listView.model[index] : (listView.model.get(index).text ? listView.model.get(index).text : "") size: hifi.fontSizes.textFieldInput color: hifi.colors.baseGray } @@ -233,4 +241,8 @@ FocusScope { anchors.bottomMargin: 4 visible: label != "" } + + Component.onCompleted: { + isDesktop = (typeof desktop !== "undefined"); + } } diff --git a/interface/resources/qml/controls-uit/Slider.qml b/interface/resources/qml/controls-uit/Slider.qml index cf59e1d989..39831546e1 100644 --- a/interface/resources/qml/controls-uit/Slider.qml +++ b/interface/resources/qml/controls-uit/Slider.qml @@ -36,7 +36,7 @@ Slider { Rectangle { width: parent.height - 2 - height: slider.value * slider.width - 1 + height: slider.value * (slider.width/(slider.maximumValue - slider.minimumValue)) - 1 radius: height / 2 anchors { top: parent.top diff --git a/interface/resources/qml/controls-uit/Table.qml b/interface/resources/qml/controls-uit/Table.qml index c7e0809b29..11d1920f95 100644 --- a/interface/resources/qml/controls-uit/Table.qml +++ b/interface/resources/qml/controls-uit/Table.qml @@ -48,11 +48,12 @@ TableView { HiFiGlyphs { id: titleSort text: sortIndicatorOrder == Qt.AscendingOrder ? hifi.glyphs.caratUp : hifi.glyphs.caratDn - color: hifi.colors.baseGrayHighlight + color: hifi.colors.darkGray + opacity: 0.6; size: hifi.fontSizes.tableHeadingIcon anchors { left: titleText.right - leftMargin: -hifi.fontSizes.tableHeadingIcon / 3 - (centerHeaderText ? 3 : 0) + leftMargin: -hifi.fontSizes.tableHeadingIcon / 3 - (centerHeaderText ? 5 : 0) right: parent.right rightMargin: hifi.dimensions.tablePadding verticalCenter: titleText.verticalCenter @@ -89,7 +90,6 @@ TableView { Rectangle { color: "#00000000" anchors { fill: parent; margins: -2 } - radius: hifi.dimensions.borderRadius border.color: isLightColorScheme ? hifi.colors.lightGrayText : hifi.colors.baseGrayHighlight border.width: 2 } diff --git a/interface/resources/qml/controls-uit/TabletComboBox.qml b/interface/resources/qml/controls-uit/TabletComboBox.qml deleted file mode 100644 index e5dec315e5..0000000000 --- a/interface/resources/qml/controls-uit/TabletComboBox.qml +++ /dev/null @@ -1,211 +0,0 @@ -// -// ComboBox.qml -// -// Created by Dante Ruiz on 13 Feb 2017 -// Copyright 2016 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -import QtQuick 2.5 -import QtQuick.Controls 1.4 -import QtQuick.Controls.Styles 1.4 - -import "../styles-uit" -import "../controls-uit" as HifiControls -import "." as VrControls - -FocusScope { - id: root - HifiConstants { id: hifi } - - property alias model: comboBox.model; - property alias comboBox: comboBox - readonly property alias currentText: comboBox.currentText; - property alias currentIndex: comboBox.currentIndex; - - property int colorScheme: hifi.colorSchemes.light - readonly property bool isLightColorScheme: colorScheme == hifi.colorSchemes.light - property string label: "" - property real controlHeight: height + (comboBoxLabel.visible ? comboBoxLabel.height + comboBoxLabel.anchors.bottomMargin : 0) - - readonly property ComboBox control: comboBox - - signal accepted(); - - implicitHeight: comboBox.height; - focus: true - - Rectangle { - id: background - gradient: Gradient { - GradientStop { - position: 0.2 - color: popup.visible - ? (isLightColorScheme ? hifi.colors.dropDownPressedLight : hifi.colors.dropDownPressedDark) - : (isLightColorScheme ? hifi.colors.dropDownLightStart : hifi.colors.dropDownDarkStart) - } - GradientStop { - position: 1.0 - color: popup.visible - ? (isLightColorScheme ? hifi.colors.dropDownPressedLight : hifi.colors.dropDownPressedDark) - : (isLightColorScheme ? hifi.colors.dropDownLightFinish : hifi.colors.dropDownDarkFinish) - } - } - anchors.fill: parent - } - - SystemPalette { id: palette } - - ComboBox { - id: comboBox - anchors.fill: parent - visible: false - height: hifi.fontSizes.textFieldInput + 13 // Match height of TextField control. - } - - FiraSansSemiBold { - id: textField - anchors { - left: parent.left - leftMargin: hifi.dimensions.textPadding - right: dropIcon.left - verticalCenter: parent.verticalCenter - } - size: hifi.fontSizes.textFieldInput - text: comboBox.currentText - elide: Text.ElideRight - color: controlHover.containsMouse || popup.visible ? hifi.colors.baseGray : (isLightColorScheme ? hifi.colors.lightGray : hifi.colors.lightGrayText ) - } - - Item { - id: dropIcon - anchors { right: parent.right; verticalCenter: parent.verticalCenter } - height: background.height - width: height - Rectangle { - width: 1 - height: parent.height - anchors.top: parent.top - anchors.left: parent.left - color: isLightColorScheme ? hifi.colors.faintGray : hifi.colors.baseGray - } - HiFiGlyphs { - anchors { - top: parent.top - topMargin: -11 - horizontalCenter: parent.horizontalCenter - } - size: hifi.dimensions.spinnerSize - text: hifi.glyphs.caratDn - color: controlHover.containsMouse || popup.visible ? hifi.colors.baseGray : (isLightColorScheme ? hifi.colors.lightGray : hifi.colors.lightGrayText) - } - } - - MouseArea { - id: controlHover - hoverEnabled: true - anchors.fill: parent - onClicked: toggleList(); - } - - function toggleList() { - if (popup.visible) { - hideList(); - } else { - showList(); - } - } - - function showList() { - var r = 20//desktop.mapFromItem(root, 0, 0, root.width, root.height); - var y = 200; - var bottom = 0 + scrollView.height; - if (bottom > 720) { - y -= bottom - 720 + 8; - } - scrollView.x = 0; - scrollView.y = 0; - popup.visible = true; - popup.forceActiveFocus(); - listView.currentIndex = root.currentIndex; - scrollView.hoverEnabled = true; - } - - function hideList() { - popup.visible = false; - scrollView.hoverEnabled = false; - root.accepted(); - } - - FocusScope { - id: popup - parent: parent - anchors.fill: parent - visible: false - focus: true - - MouseArea { - anchors.fill: parent - onClicked: hideList(); - } - - function previousItem() { listView.currentIndex = (listView.currentIndex + listView.count - 1) % listView.count; } - function nextItem() { listView.currentIndex = (listView.currentIndex + listView.count + 1) % listView.count; } - function selectCurrentItem() { root.currentIndex = listView.currentIndex; hideList(); } - function selectSpecificItem(index) { root.currentIndex = index; hideList(); } - - Keys.onUpPressed: previousItem(); - Keys.onDownPressed: nextItem(); - Keys.onSpacePressed: selectCurrentItem(); - Keys.onRightPressed: selectCurrentItem(); - Keys.onReturnPressed: selectCurrentItem(); - Keys.onEscapePressed: hideList(); - - ScrollView { - id: scrollView - height: 480 - width: root.width + 4 - property bool hoverEnabled: false; - - ListView { - id: listView - height: textField.height * count * 1.4 - model: root.model - delegate: Rectangle { - width: root.width + 4 - height: popupText.implicitHeight * 1.4 - color: (listView.currentIndex === index) ? hifi.colors.primaryHighlight : - (isLightColorScheme ? hifi.colors.dropDownPressedLight : hifi.colors.dropDownPressedDark) - FiraSansSemiBold { - anchors.left: parent.left - anchors.leftMargin: hifi.dimensions.textPadding - anchors.verticalCenter: parent.verticalCenter - id: popupText - text: listView.model[index] ? listView.model[index] : "" - size: hifi.fontSizes.textFieldInput - color: hifi.colors.baseGray - } - MouseArea { - id: popupHover - anchors.fill: parent; - hoverEnabled: scrollView.hoverEnabled; - onEntered: listView.currentIndex = index; - onClicked: popup.selectSpecificItem(index); - } - } - } - } - } - - HifiControls.Label { - id: comboBoxLabel - text: root.label - colorScheme: root.colorScheme - anchors.left: parent.left - anchors.bottom: parent.top - anchors.bottomMargin: 4 - visible: label != "" - } -} diff --git a/interface/resources/qml/controls-uit/TabletHeader.qml b/interface/resources/qml/controls-uit/TabletHeader.qml new file mode 100644 index 0000000000..17530f81ea --- /dev/null +++ b/interface/resources/qml/controls-uit/TabletHeader.qml @@ -0,0 +1,35 @@ +// +// TabletHeader.qml +// +// Created by David Rowe on 11 Mar 2017. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or https://www.apache.org/licenses/LICENSE-2.0.html +// + +import QtQuick 2.5 +import QtQuick.Controls 1.4 + +import "../styles-uit" + +Rectangle { + + property string title: "" + + HifiConstants { id: hifi } + + height: hifi.dimensions.tabletMenuHeader + z: 100 + + color: hifi.colors.darkGray + + RalewayBold { + text: title + size: 26 + color: hifi.colors.white + anchors.verticalCenter: parent.verticalCenter + anchors.left: parent.left + anchors.leftMargin: hifi.dimensions.contentMargin.x + } +} diff --git a/interface/resources/qml/controls/TabletWebView.qml b/interface/resources/qml/controls/TabletWebView.qml new file mode 100644 index 0000000000..ee26a32a85 --- /dev/null +++ b/interface/resources/qml/controls/TabletWebView.qml @@ -0,0 +1,197 @@ +import QtQuick 2.5 +import QtQuick.Controls 1.4 +import QtWebEngine 1.2 +import QtWebChannel 1.0 +import "../controls-uit" as HiFiControls +import "../styles" as HifiStyles +import "../styles-uit" +import HFWebEngineProfile 1.0 +import HFTabletWebEngineProfile 1.0 +import "../" +Item { + id: web + width: parent.width + height: parent.height + property var parentStackItem: null + property int headerHeight: 38 + property string url + property string address: url //for compatibility + property string scriptURL + property alias eventBridge: eventBridgeWrapper.eventBridge + property bool keyboardEnabled: HMD.active + property bool keyboardRaised: false + property bool punctuationMode: false + property bool isDesktop: false + property WebEngineView view: loader.currentView + + + property int currentPage: -1 // used as a model for repeater + property alias pagesModel: pagesModel + + Row { + id: buttons + HifiConstants { id: hifi } + HifiStyles.HifiConstants { id: hifistyles } + height: headerHeight + spacing: 4 + anchors.top: parent.top + anchors.topMargin: 8 + anchors.left: parent.left + anchors.leftMargin: 8 + HiFiGlyphs { + id: back; + enabled: currentPage > 0 + text: hifi.glyphs.backward + color: enabled ? hifistyles.colors.text : hifistyles.colors.disabledText + size: 48 + MouseArea { anchors.fill: parent; onClicked: goBack() } + } + + HiFiGlyphs { + id: forward; + enabled: currentPage < pagesModel.count - 1 + text: hifi.glyphs.forward + color: enabled ? hifistyles.colors.text : hifistyles.colors.disabledText + size: 48 + MouseArea { anchors.fill: parent; onClicked: goForward() } + } + + HiFiGlyphs { + id: reload; + enabled: view != null; + text: (view !== null && view.loading) ? hifi.glyphs.close : hifi.glyphs.reload + color: enabled ? hifistyles.colors.text : hifistyles.colors.disabledText + size: 48 + MouseArea { anchors.fill: parent; onClicked: reloadPage(); } + } + + } + + TextField { + id: addressBar + height: 30 + anchors.right: parent.right + anchors.rightMargin: 8 + anchors.left: buttons.right + anchors.leftMargin: 0 + anchors.verticalCenter: buttons.verticalCenter + focus: true + text: address + Component.onCompleted: ScriptDiscoveryService.scriptsModelFilter.filterRegExp = new RegExp("^.*$", "i") + + Keys.onPressed: { + switch (event.key) { + case Qt.Key_Enter: + case Qt.Key_Return: + event.accepted = true; + if (text.indexOf("http") != 0) { + text = "http://" + text; + } + //root.hidePermissionsBar(); + web.keyboardRaised = false; + gotoPage(text); + break; + + + } + } + } + + ListModel { + id: pagesModel + onCountChanged: { + currentPage = count - 1 + } + } + + function goBack() { + if (currentPage > 0) { + currentPage--; + } + } + + function goForward() { + if (currentPage < pagesModel.count - 1) { + currentPage++; + } + } + + function gotoPage(url) { + urlAppend(url) + } + + function reloadPage() { + view.reloadAndBypassCache() + view.setActiveFocusOnPress(true); + view.setEnabled(true); + } + + function urlAppend(url) { + var lurl = decodeURIComponent(url) + if (lurl[lurl.length - 1] !== "/") + lurl = lurl + "/" + if (currentPage === -1 || pagesModel.get(currentPage).webUrl !== lurl) { + pagesModel.append({webUrl: lurl}) + } + } + + onCurrentPageChanged: { + if (currentPage >= 0 && currentPage < pagesModel.count && loader.item !== null) { + loader.item.url = pagesModel.get(currentPage).webUrl + web.url = loader.item.url + web.address = loader.item.url + } + } + + onUrlChanged: { + gotoPage(url) + } + + QtObject { + id: eventBridgeWrapper + WebChannel.id: "eventBridgeWrapper" + property var eventBridge; + } + + Loader { + id: loader + + property WebEngineView currentView: null + + width: parent.width + height: parent.height - web.headerHeight + asynchronous: true + anchors.top: buttons.bottom + active: false + source: "../TabletBrowser.qml" + onStatusChanged: { + if (loader.status === Loader.Ready) { + currentView = item.webView + item.webView.userScriptUrl = web.scriptURL + if (currentPage >= 0) { + //we got something to load already + item.url = pagesModel.get(currentPage).webUrl + web.address = loader.item.url + } + } + } + } + + Component.onCompleted: { + web.isDesktop = (typeof desktop !== "undefined"); + address = url; + loader.active = true + } + + Keys.onPressed: { + switch(event.key) { + case Qt.Key_L: + if (event.modifiers == Qt.ControlModifier) { + event.accepted = true + addressBar.selectAll() + addressBar.forceActiveFocus() + } + break; + } + } +} diff --git a/interface/resources/qml/controls/WebEntityView.qml b/interface/resources/qml/controls/WebEntityView.qml new file mode 100644 index 0000000000..a3d5fe903b --- /dev/null +++ b/interface/resources/qml/controls/WebEntityView.qml @@ -0,0 +1,22 @@ +// +// WebEntityView.qml +// +// Created by Kunal Gosar on 16 March 2017 +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +import QtQuick 2.5 +import "." +import FileTypeProfile 1.0 + +WebView { + viewProfile: FileTypeProfile { + id: webviewProfile + storageName: "qmlWebEngine" + } + + urlTag: "noDownload=true"; +} diff --git a/interface/resources/qml/controls/WebView.qml b/interface/resources/qml/controls/WebView.qml index ae96590e97..52f277520f 100644 --- a/interface/resources/qml/controls/WebView.qml +++ b/interface/resources/qml/controls/WebView.qml @@ -8,6 +8,9 @@ Item { property alias url: root.url property alias scriptURL: root.userScriptUrl property alias eventBridge: eventBridgeWrapper.eventBridge + property alias canGoBack: root.canGoBack; + property var goBack: root.goBack; + property alias urlTag: root.urlTag property bool keyboardEnabled: true // FIXME - Keyboard HMD only: Default to false property bool keyboardRaised: false property bool punctuationMode: false @@ -25,6 +28,8 @@ Item { WebChannel.id: "eventBridgeWrapper" property var eventBridge; } + + property alias viewProfile: root.profile WebEngineView { id: root @@ -64,6 +69,8 @@ Item { injectionPoint: WebEngineScript.DocumentReady // DOM ready but page load may not be finished. worldId: WebEngineScript.MainWorld } + + property string urlTag: "noDownload=false"; userScripts: [ createGlobalEventBridge, raiseAndLowerKeyboard, userScript ] @@ -92,6 +99,7 @@ Item { // Required to support clicking on "hifi://" links if (WebEngineView.LoadStartedStatus == loadRequest.status) { var url = loadRequest.url.toString(); + url = (url.indexOf("?") >= 0) ? url + urlTag : url + "?" + urlTag; if (urlHandler.canHandleUrl(url)) { if (urlHandler.handleUrl(url)) { root.stop(); @@ -101,11 +109,11 @@ Item { } onNewViewRequested:{ - // desktop is not defined for web-entities - if (desktop) { - var component = Qt.createComponent("../Browser.qml"); - var newWindow = component.createObject(desktop); - request.openIn(newWindow.webView); + // desktop is not defined for web-entities or tablet + if (typeof desktop !== "undefined") { + desktop.openBrowserWindow(request, profile); + } else { + console.log("onNewViewRequested: desktop not defined"); } } } diff --git a/interface/resources/qml/desktop/Desktop.qml b/interface/resources/qml/desktop/Desktop.qml index cc64d0f2b4..d8aedf6666 100644 --- a/interface/resources/qml/desktop/Desktop.qml +++ b/interface/resources/qml/desktop/Desktop.qml @@ -490,6 +490,13 @@ FocusScope { desktop.forceActiveFocus(); } + function openBrowserWindow(request, profile) { + var component = Qt.createComponent("../Browser.qml"); + var newWindow = component.createObject(desktop); + newWindow.webView.profile = profile; + request.openIn(newWindow.webView); + } + FocusHack { id: focusHack; } Rectangle { diff --git a/interface/resources/qml/dialogs/CustomQueryDialog.qml b/interface/resources/qml/dialogs/CustomQueryDialog.qml index 97f55d087b..4d6fe74bca 100644 --- a/interface/resources/qml/dialogs/CustomQueryDialog.qml +++ b/interface/resources/qml/dialogs/CustomQueryDialog.qml @@ -107,10 +107,10 @@ ModalWindow { QtObject { id: d; - readonly property int minWidth: 480; - readonly property int maxWdith: 1280; - readonly property int minHeight: 120; - readonly property int maxHeight: 720; + readonly property int minWidth: 480 + readonly property int maxWdith: 1280 + readonly property int minHeight: 120 + readonly property int maxHeight: 720 function resize() { var targetWidth = Math.max(titleWidth, pane.width); @@ -259,6 +259,7 @@ ModalWindow { visible: Boolean(root.warning); text: hifi.glyphs.alert; size: hifi.dimensions.controlLineHeight; + width: 20 // Line up with checkbox. } } diff --git a/interface/resources/qml/dialogs/TabletConnectionFailureDialog.qml b/interface/resources/qml/dialogs/TabletConnectionFailureDialog.qml new file mode 100644 index 0000000000..6d2ff36550 --- /dev/null +++ b/interface/resources/qml/dialogs/TabletConnectionFailureDialog.qml @@ -0,0 +1,32 @@ +// +// TabletConnectionFailureDialog.qml +// +// Created by Vlad Stelmahovsky on 29 Mar 2017 +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +import Hifi 1.0 +import QtQuick 2.5 +import QtQuick.Controls 1.4 +import QtQuick.Dialogs 1.2 as OriginalDialogs + +Item { + Component.onCompleted: { + var object = tabletRoot.messageBox({ + icon: OriginalDialogs.StandardIcon.Warning, + buttons: OriginalDialogs.StandardButton.Ok, + defaultButton: OriginalDialogs.StandardButton.NoButton, + title: "No Connection", + text: "Unable to connect to this domain. Click the 'GO TO' button on the toolbar to visit another domain." + }); + object.selected.connect(function(button) { + if (button === OriginalDialogs.StandardButton.Ok) { + var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); + tablet.gotoHomeScreen() + } + }); + } +} diff --git a/interface/resources/qml/dialogs/TabletCustomQueryDialog.qml b/interface/resources/qml/dialogs/TabletCustomQueryDialog.qml new file mode 100644 index 0000000000..7965006b8b --- /dev/null +++ b/interface/resources/qml/dialogs/TabletCustomQueryDialog.qml @@ -0,0 +1,355 @@ +// +// TabletCustomQueryDialog.qml +// +// Created by Vlad Stelmahovsky on 3/27/17 +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +import QtQuick 2.5 +import QtQuick.Controls 1.4 +import QtQuick.Dialogs 1.2 as OriginalDialogs + +import "../controls-uit" +import "../styles-uit" +import "../windows" + +TabletModalWindow { + id: root; + HifiConstants { id: hifi; } + + anchors.fill: parent + width: parent.width + height: parent.height + + title: "" + visible: true; + property bool keyboardOverride: true + + signal selected(var result); + signal canceled(); + + property int icon: hifi.icons.none; + property string iconText: ""; + property int iconSize: 35; + onIconChanged: updateIcon(); + + property var textInput; + property var comboBox; + property var checkBox; + onTextInputChanged: { + if (textInput && textInput.text !== undefined) { + textField.text = textInput.text; + } + } + onComboBoxChanged: { + if (comboBox && comboBox.index !== undefined) { + comboBoxField.currentIndex = comboBox.index; + } + } + onCheckBoxChanged: { + if (checkBox && checkBox.checked !== undefined) { + checkBoxField.checked = checkBox.checked; + } + } + + property bool keyboardEnabled: false + property bool keyboardRaised: false + property bool punctuationMode: false + onKeyboardRaisedChanged: d.resize(); + + property var warning: ""; + property var result; + + property var implicitCheckState: null; + + property int titleWidth: 0; + onTitleWidthChanged: d.resize(); + + MouseArea { + width: parent.width + height: parent.height + } + + function updateIcon() { + if (!root) { + return; + } + iconText = hifi.glyphForIcon(root.icon); + } + + function updateCheckbox() { + if (checkBox.disableForItems) { + var currentItemInDisableList = false; + for (var i in checkBox.disableForItems) { + if (comboBoxField.currentIndex === checkBox.disableForItems[i]) { + currentItemInDisableList = true; + break; + } + } + + if (currentItemInDisableList) { + checkBoxField.enabled = false; + if (checkBox.checkStateOnDisable !== null && checkBox.checkStateOnDisable !== undefined) { + root.implicitCheckState = checkBoxField.checked; + checkBoxField.checked = checkBox.checkStateOnDisable; + } + root.warning = checkBox.warningOnDisable; + } else { + checkBoxField.enabled = true; + if (root.implicitCheckState !== null) { + checkBoxField.checked = root.implicitCheckState; + root.implicitCheckState = null; + } + root.warning = ""; + } + } + } + + TabletModalFrame { + id: modalWindowItem + width: parent.width - 6 + height: 240 + anchors { + verticalCenter: parent.verticalCenter + horizontalCenter: parent.horizontalCenter + } + + MouseArea { + // Clicking dialog background defocuses active control. + anchors.fill: parent + onClicked: parent.forceActiveFocus(); + } + + QtObject { + id: d; + readonly property int minWidth: 470 + readonly property int maxWidth: 470 + readonly property int minHeight: 120 + readonly property int maxHeight: 720 + + function resize() { + var targetWidth = Math.max(titleWidth, 470); + var targetHeight = (textField.visible ? textField.controlHeight + hifi.dimensions.contentSpacing.y : 0) + + (extraInputs.visible ? extraInputs.height + hifi.dimensions.contentSpacing.y : 0) + + (buttons.height + 3 * hifi.dimensions.contentSpacing.y) + + ((keyboardEnabled && keyboardRaised) ? (keyboard.raisedHeight + hifi.dimensions.contentSpacing.y) : 0); + + root.width = (targetWidth < d.minWidth) ? d.minWidth : ((targetWidth > d.maxWdith) ? d.maxWidth : targetWidth); + modalWindowItem.height = (targetHeight < d.minHeight) ? d.minHeight : ((targetHeight > d.maxHeight) ? + d.maxHeight : targetHeight); + if (checkBoxField.visible && comboBoxField.visible) { + checkBoxField.width = extraInputs.width / 2; + comboBoxField.width = extraInputs.width / 2; + } else if (!checkBoxField.visible && comboBoxField.visible) { + comboBoxField.width = extraInputs.width; + } + } + } + + Item { + anchors { + top: parent.top; + bottom: extraInputs.visible ? extraInputs.top : buttons.top; + left: parent.left; + right: parent.right; + leftMargin: 12 + rightMargin: 12 + } + + // FIXME make a text field type that can be bound to a history for autocompletion + TextField { + id: textField; + label: root.textInput.label; + focus: root.textInput ? true : false; + visible: root.textInput ? true : false; + anchors { + left: parent.left; + right: parent.right; + bottom: keyboard.top; + bottomMargin: hifi.dimensions.contentSpacing.y; + } + } + + property alias keyboardOverride: root.keyboardOverride + property alias keyboardRaised: root.keyboardRaised + property alias punctuationMode: root.punctuationMode + Keyboard { + id: keyboard + raised: keyboardEnabled && keyboardRaised + numeric: punctuationMode + anchors { + left: parent.left + right: parent.right + bottom: parent.bottom + bottomMargin: raised ? hifi.dimensions.contentSpacing.y : 0 + } + } + } + + Item { + id: extraInputs; + visible: Boolean(root.checkBox || root.comboBox); + anchors { + left: parent.left; + right: parent.right; + bottom: buttons.top; + bottomMargin: hifi.dimensions.contentSpacing.y; + leftMargin: 12 + rightMargin: 12 + } + height: comboBoxField.controlHeight; + onHeightChanged: d.resize(); + onWidthChanged: d.resize(); + z: 20 + + CheckBox { + id: checkBoxField; + text: root.checkBox.label; + focus: Boolean(root.checkBox); + visible: Boolean(root.checkBox); + anchors { + left: parent.left; + bottom: parent.bottom; + leftMargin: 6; // Magic number to align with warning icon + bottomMargin: 6; + } + } + + ComboBox { + id: comboBoxField; + label: root.comboBox.label; + focus: Boolean(root.comboBox); + visible: Boolean(root.comboBox); + anchors { + right: parent.right; + bottom: parent.bottom; + } + model: root.comboBox ? root.comboBox.items : []; + onAccepted: { + updateCheckbox(); + focus = true; + } + } + } + + Row { + id: buttons; + focus: true; + spacing: hifi.dimensions.contentSpacing.x; + layoutDirection: Qt.RightToLeft; + onHeightChanged: d.resize(); + onWidthChanged: { + d.resize(); + resizeWarningText(); + } + z: 10 + + anchors { + bottom: parent.bottom; + left: parent.left; + right: parent.right; + bottomMargin: hifi.dimensions.contentSpacing.y; + leftMargin: 12 + rightMargin: 12 + } + + function resizeWarningText() { + var rowWidth = buttons.width; + var buttonsWidth = acceptButton.width + cancelButton.width + hifi.dimensions.contentSpacing.x * 2; + var warningIconWidth = warningIcon.width + hifi.dimensions.contentSpacing.x; + warningText.width = rowWidth - buttonsWidth - warningIconWidth; + } + + Button { + id: cancelButton; + action: cancelAction; + } + + Button { + id: acceptButton; + action: acceptAction; + } + + Text { + id: warningText; + visible: Boolean(root.warning); + text: root.warning; + wrapMode: Text.WordWrap; + font.italic: true; + maximumLineCount: 2; + } + + HiFiGlyphs { + id: warningIcon; + visible: Boolean(root.warning); + text: hifi.glyphs.alert; + size: hifi.dimensions.controlLineHeight; + width: 20 // Line up with checkbox. + } + } + + Action { + id: cancelAction; + text: qsTr("Cancel"); + shortcut: Qt.Key_Escape; + onTriggered: { + root.result = null; + root.canceled(); + root.destroy(); + } + } + + Action { + id: acceptAction; + text: qsTr("Add"); + shortcut: Qt.Key_Return; + onTriggered: { + var result = {}; + if (textInput) { + result.textInput = textField.text; + } + if (comboBox) { + result.comboBox = comboBoxField.currentIndex; + result.comboBoxText = comboBoxField.currentText; + } + if (checkBox) { + result.checkBox = checkBoxField.enabled ? checkBoxField.checked : null; + } + root.result = JSON.stringify(result); + root.selected(root.result); + root.destroy(); + } + } + } + + Keys.onPressed: { + if (!visible) { + return; + } + + switch (event.key) { + case Qt.Key_Escape: + case Qt.Key_Back: + cancelAction.trigger(); + event.accepted = true; + break; + + case Qt.Key_Return: + case Qt.Key_Enter: + acceptAction.trigger(); + event.accepted = true; + break; + } + } + + Component.onCompleted: { + keyboardEnabled = HMD.active; + updateIcon(); + updateCheckbox(); + d.resize(); + textField.forceActiveFocus(); + } +} diff --git a/interface/resources/qml/dialogs/TabletFileDialog.qml b/interface/resources/qml/dialogs/TabletFileDialog.qml new file mode 100644 index 0000000000..5e33663436 --- /dev/null +++ b/interface/resources/qml/dialogs/TabletFileDialog.qml @@ -0,0 +1,782 @@ +// +// FileDialog.qml +// +// Created by Dante Ruiz on 23 Feb 2017 +// Copyright 2015 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +import QtQuick 2.5 +import QtQuick.Controls 1.4 +import Qt.labs.folderlistmodel 2.1 +import Qt.labs.settings 1.0 +import QtQuick.Controls.Styles 1.4 +import QtQuick.Dialogs 1.2 as OriginalDialogs + +import ".." +import "../controls-uit" +import "../styles-uit" +import "../windows" + +import "fileDialog" + +//FIXME implement shortcuts for favorite location +TabletModalWindow { + id: root + anchors.fill: parent + width: parent.width + height: parent.height + HifiConstants { id: hifi } + + Settings { + category: "FileDialog" + property alias width: root.width + property alias height: root.height + property alias x: root.x + property alias y: root.y + } + + + // Set from OffscreenUi::getOpenFile() + property alias caption: root.title; + // Set from OffscreenUi::getOpenFile() + property alias dir: fileTableModel.folder; + // Set from OffscreenUi::getOpenFile() + property alias filter: selectionType.filtersString; + // Set from OffscreenUi::getOpenFile() + property int options; // <-- FIXME unused + + property string iconText: root.title !== "" ? hifi.glyphs.scriptUpload : "" + property int iconSize: 40 + + property bool selectDirectory: false; + property bool showHidden: false; + // FIXME implement + property bool multiSelect: false; + property bool saveDialog: false; + property var helper: fileDialogHelper + property alias model: fileTableView.model + property var drives: helper.drives() + + property int titleWidth: 0 + + signal selectedFile(var file); + signal canceled(); + + Component.onCompleted: { + fileDialogItem.keyboardEnabled = HMD.active; + + // HACK: The following lines force the model to initialize properly such that the go-up button + // works properly from the initial screen. + var initialFolder = folderListModel.folder; + fileTableModel.folder = helper.pathToUrl(drives[0]); + fileTableModel.folder = initialFolder; + + iconText = root.title !== "" ? hifi.glyphs.scriptUpload : ""; + + // Clear selection when click on external frame. + //frameClicked.connect(function() { d.clearSelection(); }); + + if (selectDirectory) { + currentSelection.text = d.capitalizeDrive(helper.urlToPath(initialFolder)); + d.currentSelectionIsFolder = true; + d.currentSelectionUrl = initialFolder; + } + + helper.contentsChanged.connect(function() { + if (folderListModel) { + // Make folderListModel refresh. + var save = folderListModel.folder; + folderListModel.folder = ""; + folderListModel.folder = save; + } + }); + + fileTableView.forceActiveFocus(); + } + + TabletModalFrame { + id: fileDialogItem + width: parent.width - 6 + height: parent.height - 6 + + anchors { + horizontalCenter: root.horizontalCenter + verticalCenter: root.verticalCenter + } + + property bool keyboardEnabled: false + property bool keyboardRaised: false + property bool punctuationMode: false + + MouseArea { + // Clear selection when click on internal unused area. + anchors.fill: parent + onClicked: { + d.clearSelection(); + } + } + + Row { + id: navControls + anchors { + top: parent.top + topMargin: (fileDialogItem.hasTitle ? (fileDialogItem.frameMarginTop + hifi.dimensions.modalDialogMargin.y) : hifi.dimension.modalDialogMargin.y) + left: parent.left + leftMargin: hifi.dimensions.contentSpacing.x + } + spacing: hifi.dimensions.contentSpacing.x + + GlyphButton { + id: upButton + glyph: hifi.glyphs.levelUp + width: height + size: 30 + enabled: fileTableModel.parentFolder && fileTableModel.parentFolder !== "" + onClicked: d.navigateUp(); + } + + GlyphButton { + id: homeButton + property var destination: helper.home(); + glyph: hifi.glyphs.home + size: 28 + width: height + enabled: d.homeDestination ? true : false + onClicked: d.navigateHome(); + } + } + + ComboBox { + id: pathSelector + z: 10 + anchors { + top: parent.top + topMargin: (fileDialogItem.hasTitle ? (fileDialogItem.frameMarginTop + hifi.dimensions.modalDialogMargin.y) : hifi.dimension.modalDialogMargin.y) + left: navControls.right + leftMargin: hifi.dimensions.contentSpacing.x + right: parent.right + } + + property var lastValidFolder: helper.urlToPath(fileTableModel.folder) + + function calculatePathChoices(folder) { + var folders = folder.split("/"), + choices = [], + i, length; + + if (folders[folders.length - 1] === "") { + folders.pop(); + } + + choices.push(folders[0]); + + for (i = 1, length = folders.length; i < length; i++) { + choices.push(choices[i - 1] + "/" + folders[i]); + } + + if (folders[0] === "") { + // Special handling for OSX root dir. + choices[0] = "/"; + } + + choices.reverse(); + + if (drives && drives.length > 1) { + choices.push("This PC"); + } + + if (choices.length > 0) { + pathSelector.model = choices; + } + } + + onLastValidFolderChanged: { + var folder = d.capitalizeDrive(lastValidFolder); + calculatePathChoices(folder); + } + + onCurrentTextChanged: { + var folder = currentText; + + if (/^[a-zA-z]:$/.test(folder)) { + folder = "file:///" + folder + "/"; + } else if (folder === "This PC") { + folder = "file:///"; + } else { + folder = helper.pathToUrl(folder); + } + + if (helper.urlToPath(folder).toLowerCase() !== helper.urlToPath(fileTableModel.folder).toLowerCase()) { + if (root.selectDirectory) { + currentSelection.text = currentText !== "This PC" ? currentText : ""; + d.currentSelectionUrl = helper.pathToUrl(currentText); + } + fileTableModel.folder = folder; + fileTableView.forceActiveFocus(); + } + } + } + + QtObject { + id: d + property var currentSelectionUrl; + readonly property string currentSelectionPath: helper.urlToPath(currentSelectionUrl); + property bool currentSelectionIsFolder; + property var backStack: [] + property var tableViewConnection: Connections { target: fileTableView; onCurrentRowChanged: d.update(); } + property var modelConnection: Connections { target: fileTableModel; onFolderChanged: d.update(); } + property var homeDestination: helper.home(); + + function capitalizeDrive(path) { + // Consistently capitalize drive letter for Windows. + if (/[a-zA-Z]:/.test(path)) { + return path.charAt(0).toUpperCase() + path.slice(1); + } + return path; + } + + function update() { + var row = fileTableView.currentRow; + + if (row === -1) { + if (!root.selectDirectory) { + currentSelection.text = ""; + currentSelectionIsFolder = false; + } + return; + } + + currentSelectionUrl = helper.pathToUrl(fileTableView.model.get(row).filePath); + currentSelectionIsFolder = fileTableView.model.isFolder(row); + if (root.selectDirectory || !currentSelectionIsFolder) { + currentSelection.text = capitalizeDrive(helper.urlToPath(currentSelectionUrl)); + } else { + currentSelection.text = ""; + } + } + + function navigateUp() { + if (fileTableModel.parentFolder && fileTableModel.parentFolder !== "") { + fileTableModel.folder = fileTableModel.parentFolder; + return true; + } + } + + function navigateHome() { + fileTableModel.folder = homeDestination; + return true; + } + + function clearSelection() { + fileTableView.selection.clear(); + fileTableView.currentRow = -1; + update(); + } + } + + FolderListModel { + id: folderListModel + nameFilters: selectionType.currentFilter + showDirsFirst: true + showDotAndDotDot: false + showFiles: !root.selectDirectory + Component.onCompleted: { + showFiles = !root.selectDirectory + } + + onFolderChanged: { + fileTableModel.update(); // Update once the data from the folder change is available. + } + + function getItem(index, field) { + return get(index, field); + } + } + + ListModel { + // Emulates FolderListModel but contains drive data. + id: driveListModel + + property int count: 1 + + Component.onCompleted: initialize(); + + function initialize() { + var drive, + i; + + count = drives.length; + + for (i = 0; i < count; i++) { + drive = drives[i].slice(0, -1); // Remove trailing "/". + append({ + fileName: drive, + fileModified: new Date(0), + fileSize: 0, + filePath: drive + "/", + fileIsDir: true, + fileNameSort: drive.toLowerCase() + }); + } + } + + function getItem(index, field) { + return get(index)[field]; + } + } + + ListModel { + id: fileTableModel + + // FolderListModel has a couple of problems: + // 1) Files and directories sort case-sensitively: https://bugreports.qt.io/browse/QTBUG-48757 + // 2) Cannot browse up to the "computer" level to view Windows drives: https://bugreports.qt.io/browse/QTBUG-42901 + // + // To solve these problems an intermediary ListModel is used that implements proper sorting and can be populated with + // drive information when viewing at the computer level. + + property var folder + property int sortOrder: Qt.AscendingOrder + property int sortColumn: 0 + property var model: folderListModel + property string parentFolder: calculateParentFolder(); + + readonly property string rootFolder: "file:///" + + function calculateParentFolder() { + if (model === folderListModel) { + if (folderListModel.parentFolder.toString() === "" && driveListModel.count > 1) { + return rootFolder; + } else { + return folderListModel.parentFolder; + } + } else { + return ""; + } + } + + onFolderChanged: { + if (folder === rootFolder) { + model = driveListModel; + helper.monitorDirectory(""); + update(); + } else { + var needsUpdate = model === driveListModel && folder === folderListModel.folder; + + model = folderListModel; + folderListModel.folder = folder; + helper.monitorDirectory(helper.urlToPath(folder)); + + if (needsUpdate) { + update(); + } + } + } + + function isFolder(row) { + if (row === -1) { + return false; + } + return get(row).fileIsDir; + } + + function update() { + var dataFields = ["fileName", "fileModified", "fileSize"], + sortFields = ["fileNameSort", "fileModified", "fileSize"], + dataField = dataFields[sortColumn], + sortField = sortFields[sortColumn], + sortValue, + fileName, + fileIsDir, + comparisonFunction, + lower, + middle, + upper, + rows = 0, + i; + + clear(); + + comparisonFunction = sortOrder === Qt.AscendingOrder + ? function(a, b) { return a < b; } + : function(a, b) { return a > b; } + + for (i = 0; i < model.count; i++) { + fileName = model.getItem(i, "fileName"); + fileIsDir = model.getItem(i, "fileIsDir"); + + sortValue = model.getItem(i, dataField); + if (dataField === "fileName") { + // Directories first by prefixing a "*". + // Case-insensitive. + sortValue = (fileIsDir ? "*" : "") + sortValue.toLowerCase(); + } + + lower = 0; + upper = rows; + while (lower < upper) { + middle = Math.floor((lower + upper) / 2); + var lessThan; + if (comparisonFunction(sortValue, get(middle)[sortField])) { + lessThan = true; + upper = middle; + } else { + lessThan = false; + lower = middle + 1; + } + } + + insert(lower, { + fileName: fileName, + fileModified: (fileIsDir ? new Date(0) : model.getItem(i, "fileModified")), + fileSize: model.getItem(i, "fileSize"), + filePath: model.getItem(i, "filePath"), + fileIsDir: fileIsDir, + fileNameSort: (fileIsDir ? "*" : "") + fileName.toLowerCase() + }); + + rows++; + } + + d.clearSelection(); + } + } + + Table { + id: fileTableView + colorScheme: hifi.colorSchemes.light + anchors { + top: navControls.bottom + topMargin: hifi.dimensions.contentSpacing.y + left: parent.left + right: parent.right + bottom: currentSelection.top + bottomMargin: hifi.dimensions.contentSpacing.y + currentSelection.controlHeight - currentSelection.height + } + headerVisible: !selectDirectory + onClicked: navigateToRow(row); + onDoubleClicked: navigateToRow(row); + focus: true + Keys.onReturnPressed: navigateToCurrentRow(); + Keys.onEnterPressed: navigateToCurrentRow(); + + sortIndicatorColumn: 0 + sortIndicatorOrder: Qt.AscendingOrder + sortIndicatorVisible: true + + model: fileTableModel + + function updateSort() { + model.sortOrder = sortIndicatorOrder; + model.sortColumn = sortIndicatorColumn; + model.update(); + } + + onSortIndicatorColumnChanged: { updateSort(); } + + onSortIndicatorOrderChanged: { updateSort(); } + + itemDelegate: Item { + clip: true + + //FontLoader { id: firaSansSemiBold; source: "../../fonts/FiraSans-SemiBold.ttf"; } + //FontLoader { id: firaSansRegular; source: "../../fonts/FiraSans-Regular.ttf"; } + + FiraSansSemiBold { + text: getText(); + elide: styleData.elideMode + anchors { + left: parent.left + leftMargin: hifi.dimensions.tablePadding + right: parent.right + rightMargin: hifi.dimensions.tablePadding + verticalCenter: parent.verticalCenter + } + size: hifi.fontSizes.tableText + color: hifi.colors.baseGrayHighlight + //font.family: (styleData.row !== -1 && fileTableView.model.get(styleData.row).fileIsDir) + //? firaSansSemiBold.name : firaSansRegular.name + + function getText() { + if (styleData.row === -1) { + return styleData.value; + } + + switch (styleData.column) { + case 1: return fileTableView.model.get(styleData.row).fileIsDir ? "" : styleData.value; + case 2: return fileTableView.model.get(styleData.row).fileIsDir ? "" : formatSize(styleData.value); + default: return styleData.value; + } + } + function formatSize(size) { + var suffixes = [ "bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB" ]; + var suffixIndex = 0 + while ((size / 1024.0) > 1.1) { + size /= 1024.0; + ++suffixIndex; + } + + size = Math.round(size*1000)/1000; + size = size.toLocaleString() + + return size + " " + suffixes[suffixIndex]; + } + } + } + + TableViewColumn { + id: fileNameColumn + role: "fileName" + title: "Name" + width: (selectDirectory ? 1.0 : 0.5) * fileTableView.width + movable: false + resizable: true + } + TableViewColumn { + id: fileMofifiedColumn + role: "fileModified" + title: "Date" + width: 0.3 * fileTableView.width + movable: false + resizable: true + visible: !selectDirectory + } + TableViewColumn { + role: "fileSize" + title: "Size" + width: fileTableView.width - fileNameColumn.width - fileMofifiedColumn.width + movable: false + resizable: true + visible: !selectDirectory + } + + function navigateToRow(row) { + currentRow = row; + navigateToCurrentRow(); + } + + function navigateToCurrentRow() { + var row = fileTableView.currentRow + var isFolder = model.isFolder(row); + var file = model.get(row).filePath; + if (isFolder) { + fileTableView.model.folder = helper.pathToUrl(file); + } else { + okAction.trigger(); + } + } + + property string prefix: "" + + function addToPrefix(event) { + if (!event.text || event.text === "") { + return false; + } + var newPrefix = prefix + event.text.toLowerCase(); + var matchedIndex = -1; + for (var i = 0; i < model.count; ++i) { + var name = model.get(i).fileName.toLowerCase(); + if (0 === name.indexOf(newPrefix)) { + matchedIndex = i; + break; + } + } + + if (matchedIndex !== -1) { + fileTableView.selection.clear(); + fileTableView.selection.select(matchedIndex); + fileTableView.currentRow = matchedIndex; + fileTableView.prefix = newPrefix; + } + prefixClearTimer.restart(); + return true; + } + + Timer { + id: prefixClearTimer + interval: 1000 + repeat: false + running: false + onTriggered: fileTableView.prefix = ""; + } + + Keys.onPressed: { + switch (event.key) { + case Qt.Key_Backspace: + case Qt.Key_Tab: + case Qt.Key_Backtab: + event.accepted = false; + break; + + default: + if (addToPrefix(event)) { + event.accepted = true + } else { + event.accepted = false; + } + break; + } + } + } + + TextField { + id: currentSelection + label: selectDirectory ? "Directory:" : "File name:" + anchors { + left: parent.left + right: selectionType.visible ? selectionType.left: parent.right + rightMargin: hifi.dimensions.contentSpacing.x + leftMargin: hifi.dimensions.contentSpacing.x + bottom: keyboard.top + bottomMargin: hifi.dimensions.contentSpacing.y + } + readOnly: !root.saveDialog + activeFocusOnTab: !readOnly + onActiveFocusChanged: if (activeFocus) { selectAll(); } + onAccepted: okAction.trigger(); + } + + FileTypeSelection { + id: selectionType + anchors { + top: currentSelection.top + left: buttonRow.left + right: parent.right + } + visible: !selectDirectory && filtersCount > 1 + KeyNavigation.left: fileTableView + KeyNavigation.right: openButton + } + + Keyboard { + id: keyboard + raised: parent.keyboardEnabled && parent.keyboardRaised + numeric: parent.punctuationMode + anchors { + left: parent.left + right: parent.right + bottom: buttonRow.top + bottomMargin: visible ? hifi.dimensions.contentSpacing.y : 0 + } + } + + Row { + id: buttonRow + anchors { + right: parent.right + bottom: parent.bottom + } + spacing: hifi.dimensions.contentSpacing.y + + Button { + id: openButton + color: hifi.buttons.blue + action: okAction + Keys.onReturnPressed: okAction.trigger() + KeyNavigation.up: selectionType + KeyNavigation.left: selectionType + KeyNavigation.right: cancelButton + } + + Button { + id: cancelButton + action: cancelAction + KeyNavigation.up: selectionType + KeyNavigation.left: openButton + KeyNavigation.right: fileTableView.contentItem + Keys.onReturnPressed: { canceled(); root.enabled = false } + } + } + + Action { + id: okAction + text: currentSelection.text ? (root.selectDirectory && fileTableView.currentRow === -1 ? "Choose" : (root.saveDialog ? "Save" : "Open")) : "Open" + enabled: currentSelection.text || !root.selectDirectory && d.currentSelectionIsFolder ? true : false + onTriggered: { + if (!root.selectDirectory && !d.currentSelectionIsFolder + || root.selectDirectory && fileTableView.currentRow === -1) { + okActionTimer.start(); + } else { + fileTableView.navigateToCurrentRow(); + } + } + } + + Timer { + id: okActionTimer + interval: 50 + running: false + repeat: false + onTriggered: { + if (!root.saveDialog) { + selectedFile(d.currentSelectionUrl); + root.destroy(); + return; + } + + // Handle the ambiguity between different cases + // * typed name (with or without extension) + // * full path vs relative vs filename only + var selection = helper.saveHelper(currentSelection.text, root.dir, selectionType.currentFilter); + + if (!selection) { + desktop.messageBox({ icon: OriginalDialogs.StandardIcon.Warning, text: "Unable to parse selection" }) + return; + } + + if (helper.urlIsDir(selection)) { + root.dir = selection; + currentSelection.text = ""; + return; + } + + // Check if the file is a valid target + if (!helper.urlIsWritable(selection)) { + desktop.messageBox({ + icon: OriginalDialogs.StandardIcon.Warning, + text: "Unable to write to location " + selection + }) + return; + } + + if (helper.urlExists(selection)) { + var messageBox = desktop.messageBox({ + icon: OriginalDialogs.StandardIcon.Question, + buttons: OriginalDialogs.StandardButton.Yes | OriginalDialogs.StandardButton.No, + text: "Do you wish to overwrite " + selection + "?", + }); + var result = messageBox.exec(); + if (OriginalDialogs.StandardButton.Yes !== result) { + return; + } + } + + console.log("Selecting " + selection) + selectedFile(selection); + root.destroy(); + } + } + + Action { + id: cancelAction + text: "Cancel" + onTriggered: { canceled();root.destroy(); } + } + } + + Keys.onPressed: { + switch (event.key) { + case Qt.Key_Backspace: + event.accepted = d.navigateUp(); + break; + + case Qt.Key_Home: + event.accepted = d.navigateHome(); + break; + + } + } +} diff --git a/interface/resources/qml/dialogs/TabletLoginDialog.qml b/interface/resources/qml/dialogs/TabletLoginDialog.qml new file mode 100644 index 0000000000..78e5edebb5 --- /dev/null +++ b/interface/resources/qml/dialogs/TabletLoginDialog.qml @@ -0,0 +1,113 @@ +// +// TabletLoginDialog.qml +// +// Created by Vlad Stelmahovsky on 15 Mar 2017 +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +import Hifi 1.0 +import QtQuick 2.5 +import QtQuick.Controls 1.4 + +import "../controls-uit" +import "../styles-uit" +import "../windows" + +TabletModalWindow { + id: loginDialogRoot + objectName: "LoginDialog" + + property var eventBridge; + signal sendToScript(var message); + property bool isHMD: false + + color: hifi.colors.baseGray + + property int colorScheme: hifi.colorSchemes.dark + property int titleWidth: 0 + property string iconText: "" + property int icon: hifi.icons.none + property int iconSize: 35 + MouseArea { + width: parent.width + height: parent.height + } + + property bool keyboardOverride: true + onIconChanged: updateIcon(); + + property var items; + property string label: "" + + onTitleWidthChanged: d.resize(); + + property bool keyboardEnabled: false + property bool keyboardRaised: false + property bool punctuationMode: false + + onKeyboardRaisedChanged: d.resize(); + + signal canceled(); + + function updateIcon() { + if (!root) { + return; + } + iconText = hifi.glyphForIcon(root.icon); + } + + property alias bodyLoader: bodyLoader + property alias loginDialog: loginDialog + property alias hifi: hifi + + HifiConstants { id: hifi } + + onCanceled: { + loginDialogRoot.Stack.view.pop() + } + + LoginDialog { + id: loginDialog + width: parent.width + height: parent.height + StackView { + id: bodyLoader + property var item: currentItem + property var props + property string source: "" + + onCurrentItemChanged: { + //cleanup source for future usage + source = "" + } + + function setSource(src, props) { + source = "../TabletLoginDialog/" + src + bodyLoader.props = props + } + function popup() { + bodyLoader.pop() + + //check if last screen, if yes, dialog is popped out + if (depth === 1) + loginDialogRoot.canceled() + } + + anchors.fill: parent + anchors.margins: 10 + onSourceChanged: { + if (source !== "") { + bodyLoader.push(Qt.resolvedUrl(source), props) + } + } + Component.onCompleted: { + setSource(loginDialog.isSteamRunning() ? + "SignInBody.qml" : + "LinkAccountBody.qml") + } + } + } +} diff --git a/interface/resources/qml/dialogs/TabletMessageBox.qml b/interface/resources/qml/dialogs/TabletMessageBox.qml new file mode 100644 index 0000000000..f8876b1ec8 --- /dev/null +++ b/interface/resources/qml/dialogs/TabletMessageBox.qml @@ -0,0 +1,249 @@ +// +// MessageDialog.qml +// +// Created by Bradley Austin Davis on 15 Jan 2016 +// Copyright 2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +import QtQuick 2.5 +import QtQuick.Controls 1.4 +import QtQuick.Dialogs 1.2 as OriginalDialogs + +import "../controls-uit" +import "../styles-uit" +import "../windows" + +import "messageDialog" + +TabletModalWindow { + id: root + HifiConstants { id: hifi } + visible: true + + signal selected(int button); + + MouseArea { + id: mouse; + anchors.fill: parent + } + + function click(button) { + clickedButton = button; + selected(button); + destroy(); + } + + function exec() { + return OffscreenUi.waitForMessageBoxResult(root); + } + + property alias detailedText: detailedText.text + property alias text: mainTextContainer.text + property alias informativeText: informativeTextContainer.text + property int buttons: OriginalDialogs.StandardButton.Ok + property int icon: OriginalDialogs.StandardIcon.NoIcon + property string iconText: "" + property int iconSize: 50 + onIconChanged: updateIcon(); + property int defaultButton: OriginalDialogs.StandardButton.NoButton; + property int clickedButton: OriginalDialogs.StandardButton.NoButton; + focus: defaultButton === OriginalDialogs.StandardButton.NoButton + + property int titleWidth: 0 + onTitleWidthChanged: d.resize(); + + function updateIcon() { + if (!root) { + return; + } + iconText = hifi.glyphForIcon(root.icon); + } + + TabletModalFrame { + id: messageBox + clip: true + anchors.verticalCenter: parent.verticalCenter + anchors.horizontalCenter: parent.horizontalCenter + width: parent.width - 6 + height: 300 + + QtObject { + id: d + readonly property int minWidth: 200 + readonly property int maxWidth: 1280 + readonly property int minHeight: 120 + readonly property int maxHeight: 720 + + function resize() { + var targetWidth = Math.max(titleWidth, mainTextContainer.contentWidth) + var targetHeight = mainTextContainer.height + 3 * hifi.dimensions.contentSpacing.y + + (informativeTextContainer.text != "" ? informativeTextContainer.contentHeight + 3 * hifi.dimensions.contentSpacing.y : 0) + + buttons.height + + (details.implicitHeight + hifi.dimensions.contentSpacing.y) + messageBox.frameMarginTop + messageBox.height = (targetHeight < d.minHeight) ? d.minHeight: ((targetHeight > d.maxHeight) ? d.maxHeight : targetHeight) + } + } + + RalewaySemiBold { + id: mainTextContainer + onTextChanged: d.resize(); + wrapMode: Text.WordWrap + size: hifi.fontSizes.sectionName + color: hifi.colors.baseGrayHighlight + width: parent.width - 6 + anchors { + top: parent.top + horizontalCenter: parent.horizontalCenter + margins: 0 + topMargin: hifi.dimensions.contentSpacing.y + messageBox.frameMarginTop + } + maximumLineCount: 30 + elide: Text.ElideLeft + lineHeight: 2 + lineHeightMode: Text.ProportionalHeight + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + } + + RalewaySemiBold { + id: informativeTextContainer + onTextChanged: d.resize(); + wrapMode: Text.WordWrap + size: hifi.fontSizes.sectionName + color: hifi.colors.baseGrayHighlight + anchors { + top: mainTextContainer.bottom + left: parent.left + right: parent.right + margins: 0 + topMargin: text != "" ? hifi.dimensions.contentSpacing.y : 0 + } + } + + Flow { + id: buttons + focus: true + spacing: hifi.dimensions.contentSpacing.x + onHeightChanged: d.resize(); onWidthChanged: d.resize(); + layoutDirection: Qt.RightToLeft + anchors { + top: informativeTextContainer.text == "" ? mainTextContainer.bottom : informativeTextContainer.bottom + horizontalCenter: parent.horizontalCenter + margins: 0 + topMargin: 2 * hifi.dimensions.contentSpacing.y + } + MessageDialogButton { dialog: root; text: qsTr("Close"); button: OriginalDialogs.StandardButton.Close; } + MessageDialogButton { dialog: root; text: qsTr("Abort"); button: OriginalDialogs.StandardButton.Abort; } + MessageDialogButton { dialog: root; text: qsTr("Cancel"); button: OriginalDialogs.StandardButton.Cancel; } + MessageDialogButton { dialog: root; text: qsTr("Restore Defaults"); button: OriginalDialogs.StandardButton.RestoreDefaults; } + MessageDialogButton { dialog: root; text: qsTr("Reset"); button: OriginalDialogs.StandardButton.Reset; } + MessageDialogButton { dialog: root; text: qsTr("Discard"); button: OriginalDialogs.StandardButton.Discard; } + MessageDialogButton { dialog: root; text: qsTr("No to All"); button: OriginalDialogs.StandardButton.NoToAll; } + MessageDialogButton { dialog: root; text: qsTr("No"); button: OriginalDialogs.StandardButton.No; } + MessageDialogButton { dialog: root; text: qsTr("Yes to All"); button: OriginalDialogs.StandardButton.YesToAll; } + MessageDialogButton { dialog: root; text: qsTr("Yes"); button: OriginalDialogs.StandardButton.Yes; } + MessageDialogButton { dialog: root; text: qsTr("Apply"); button: OriginalDialogs.StandardButton.Apply; } + MessageDialogButton { dialog: root; text: qsTr("Ignore"); button: OriginalDialogs.StandardButton.Ignore; } + MessageDialogButton { dialog: root; text: qsTr("Retry"); button: OriginalDialogs.StandardButton.Retry; } + MessageDialogButton { dialog: root; text: qsTr("Save All"); button: OriginalDialogs.StandardButton.SaveAll; } + MessageDialogButton { dialog: root; text: qsTr("Save"); button: OriginalDialogs.StandardButton.Save; } + MessageDialogButton { dialog: root; text: qsTr("Open"); button: OriginalDialogs.StandardButton.Open; } + MessageDialogButton { dialog: root; text: qsTr("OK"); button: OriginalDialogs.StandardButton.Ok; } + + Button { + id: moreButton + text: qsTr("Show Details...") + width: 160 + onClicked: { content.state = (content.state === "" ? "expanded" : "") } + visible: detailedText && detailedText.length > 0 + } + MessageDialogButton { dialog: root; text: qsTr("Help"); button: OriginalDialogs.StandardButton.Help; } + } + + Item { + id: details + width: parent.width + implicitHeight: detailedText.implicitHeight + height: 0 + clip: true + anchors { + top: buttons.bottom + left: parent.left; + right: parent.right; + margins: 0 + topMargin: hifi.dimensions.contentSpacing.y + } + Flickable { + id: flickable + contentHeight: detailedText.height + anchors.fill: parent + anchors.topMargin: hifi.dimensions.contentSpacing.x + anchors.bottomMargin: hifi.dimensions.contentSpacing.y + TextEdit { + id: detailedText + size: hifi.fontSizes.menuItem + color: hifi.colors.baseGrayHighlight + width: details.width + wrapMode: Text.WordWrap + readOnly: true + selectByMouse: true + anchors.margins: 0 + } + } + } + + states: [ + State { + name: "expanded" + PropertyChanges { target: root; anchors.fill: undefined } + PropertyChanges { target: details; height: 120 } + PropertyChanges { target: moreButton; text: qsTr("Hide Details") } + } + ] + + Component.onCompleted: { + updateIcon(); + d.resize(); + } + onStateChanged: d.resize() + } + + Keys.onPressed: { + if (!visible) { + return + } + + if (event.modifiers === Qt.ControlModifier) + switch (event.key) { + case Qt.Key_A: + event.accepted = true + detailedText.selectAll() + break + case Qt.Key_C: + event.accepted = true + detailedText.copy() + break + case Qt.Key_Period: + if (Qt.platform.os === "osx") { + event.accepted = true + content.reject() + } + break + } else switch (event.key) { + case Qt.Key_Escape: + case Qt.Key_Back: + event.accepted = true + root.click(OriginalDialogs.StandardButton.Cancel) + break + + case Qt.Key_Enter: + case Qt.Key_Return: + event.accepted = true + root.click(root.defaultButton) + break + } + } +} diff --git a/interface/resources/qml/dialogs/TabletQueryDialog.qml b/interface/resources/qml/dialogs/TabletQueryDialog.qml new file mode 100644 index 0000000000..e21677c12c --- /dev/null +++ b/interface/resources/qml/dialogs/TabletQueryDialog.qml @@ -0,0 +1,206 @@ +// +// QueryDialog.qml +// +// Created by Bradley Austin Davis on 22 Jan 2016 +// Copyright 2015 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +import QtQuick 2.5 +import QtQuick.Controls 1.4 +import QtQuick.Dialogs 1.2 as OriginalDialogs + +import "../controls-uit" +import "../styles-uit" +import "../windows" + +TabletModalWindow { + id: root + HifiConstants { id: hifi } + signal selected(var result); + signal canceled(); + layer.enabled: true + property int icon: hifi.icons.none + property string iconText: "" + property int iconSize: 35 + + MouseArea { + width: parent.width + height: parent.height + } + + property bool keyboardOverride: true + onIconChanged: updateIcon(); + + property var items; + property string label: "" + property var result; + property alias current: textResult.text + + // For text boxes + property alias placeholderText: textResult.placeholderText + + // For combo boxes + property bool editable: true; + + property int titleWidth: 0 + onTitleWidthChanged: d.resize(); + + property bool keyboardEnabled: false + property bool keyboardRaised: false + property bool punctuationMode: false + + onKeyboardRaisedChanged: d.resize(); + + function updateIcon() { + if (!root) { + return; + } + iconText = hifi.glyphForIcon(root.icon); + } + + TabletModalFrame { + id: modalWindowItem + width: parent.width - 12 + height: 240 + anchors { + verticalCenter: parent.verticalCenter + horizontalCenter: parent.horizontalCenter + } + + QtObject { + id: d + readonly property int minWidth: 470 + readonly property int maxWidth: 470 + readonly property int minHeight: 120 + readonly property int maxHeight: 720 + + function resize() { + var targetWidth = Math.max(titleWidth, 470) + var targetHeight = (items ? comboBox.controlHeight : textResult.controlHeight) + 5 * hifi.dimensions.contentSpacing.y + buttons.height + modalWindowItem.width = (targetWidth < d.minWidth) ? d.minWidth : ((targetWidth > d.maxWdith) ? d.maxWidth : targetWidth); + modalWindowItem.height = ((targetHeight < d.minHeight) ? d.minHeight : ((targetHeight > d.maxHeight) ? d.maxHeight : targetHeight)) + ((keyboardEnabled && keyboardRaised) ? (keyboard.raisedHeight + 2 * hifi.dimensions.contentSpacing.y) : 0) + modalWindowItem.frameMarginTop + } + } + + Item { + anchors { + top: parent.top + bottom: keyboard.top; + left: parent.left; + right: parent.right; + margins: 0 + bottomMargin: 2 * hifi.dimensions.contentSpacing.y + } + + // FIXME make a text field type that can be bound to a history for autocompletion + TextField { + id: textResult + label: root.label + focus: items ? false : true + visible: items ? false : true + anchors { + left: parent.left; + right: parent.right; + bottom: parent.bottom + leftMargin: 5 + } + } + + ComboBox { + id: comboBox + label: root.label + focus: true + visible: items ? true : false + anchors { + left: parent.left + right: parent.right + bottom: parent.bottom + rightMargin: 5 + } + model: items ? items : [] + } + } + + property alias keyboardOverride: root.keyboardOverride + property alias keyboardRaised: root.keyboardRaised + property alias punctuationMode: root.punctuationMode + + Keyboard { + id: keyboard + raised: keyboardEnabled && keyboardRaised + numeric: punctuationMode + anchors { + left: parent.left + right: parent.right + bottom: buttons.top + bottomMargin: raised ? 2 * hifi.dimensions.contentSpacing.y : 0 + } + } + + Flow { + id: buttons + focus: true + spacing: hifi.dimensions.contentSpacing.x + onHeightChanged: d.resize(); onWidthChanged: d.resize(); + layoutDirection: Qt.RightToLeft + anchors { + bottom: parent.bottom + right: parent.right + margins: 0 + bottomMargin: hifi.dimensions.contentSpacing.y + } + Button { action: cancelAction } + Button { action: acceptAction } + } + + Action { + id: cancelAction + text: qsTr("Cancel") + shortcut: Qt.Key_Escape + onTriggered: { + root.canceled(); + root.destroy(); + } + } + Action { + id: acceptAction + text: qsTr("OK") + shortcut: Qt.Key_Return + onTriggered: { + root.result = items ? comboBox.currentText : textResult.text + root.selected(root.result); + root.destroy(); + } + } + } + + Keys.onPressed: { + if (!visible) { + return + } + + switch (event.key) { + case Qt.Key_Escape: + case Qt.Key_Back: + cancelAction.trigger() + event.accepted = true; + break; + + case Qt.Key_Return: + case Qt.Key_Enter: + acceptAction.trigger() + event.accepted = true; + break; + } + } + + Component.onCompleted: { + keyboardEnabled = HMD.active; + updateIcon(); + d.resize(); + textResult.forceActiveFocus(); + } +} diff --git a/interface/resources/qml/dialogs/preferences/AvatarPreference.qml b/interface/resources/qml/dialogs/preferences/AvatarPreference.qml index 0c5c5bf630..7ae4fe6761 100644 --- a/interface/resources/qml/dialogs/preferences/AvatarPreference.qml +++ b/interface/resources/qml/dialogs/preferences/AvatarPreference.qml @@ -10,8 +10,8 @@ import QtQuick 2.5 -import "../../dialogs" import "../../controls-uit" +import "../../hifi/tablet/tabletWindows/preferences" Preference { id: root @@ -82,11 +82,25 @@ Preference { verticalCenter: dataTextField.verticalCenter } onClicked: { - // Load dialog via OffscreenUi so that JavaScript EventBridge is available. - root.browser = OffscreenUi.load("dialogs/preferences/AvatarBrowser.qml"); - root.browser.windowDestroyed.connect(function(){ - root.browser = null; - }); + if (typeof desktop !== "undefined") { + // Load dialog via OffscreenUi so that JavaScript EventBridge is available. + root.browser = OffscreenUi.load("dialogs/preferences/AvatarBrowser.qml"); + root.browser.windowDestroyed.connect(function(){ + root.browser = null; + }); + } else { + root.browser = tabletAvatarBrowserBuilder.createObject(tabletRoot); + + // Make dialog modal. + tabletRoot.openModal = root.browser; + } + } + } + + Component { + id: tabletAvatarBrowserBuilder; + TabletAvatarBrowser { + eventBridge: tabletRoot.eventBridge } } } diff --git a/interface/resources/qml/hifi/Audio.qml b/interface/resources/qml/hifi/Audio.qml new file mode 100644 index 0000000000..d0c3122100 --- /dev/null +++ b/interface/resources/qml/hifi/Audio.qml @@ -0,0 +1,253 @@ +// +// Audio.qml +// qml/hifi +// +// Audio setup +// +// Created by Vlad Stelmahovsky on 03/22/2017 +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +import QtQuick 2.5 +import QtQuick.Controls 1.4 +import QtGraphicalEffects 1.0 + +import "../styles-uit" +import "../controls-uit" as HifiControls + +import "components" + +Rectangle { + id: audio; + + //put info text here + property alias infoText: infoArea.text + + color: "#404040"; + + HifiConstants { id: hifi; } + objectName: "AudioWindow" + + property var eventBridge; + property string title: "Audio Options" + signal sendToScript(var message); + + //set models after Components is shown + Component.onCompleted: { + refreshTimer.start() + refreshTimerOutput.start() + } + + Component { + id: separator + LinearGradient { + start: Qt.point(0, 0) + end: Qt.point(0, 4) + gradient: Gradient { + GradientStop { position: 0.0; color: "#303030" } + GradientStop { position: 0.33; color: "#252525" } // Equivalent of darkGray0 over baseGray background. + GradientStop { position: 0.5; color: "#303030" } + GradientStop { position: 0.6; color: "#454a49" } + GradientStop { position: 1.0; color: "#454a49" } + } + cached: true + } + } + + Column { + anchors { left: parent.left; right: parent.right } + spacing: 8 + + RalewayRegular { + anchors { left: parent.left; right: parent.right; leftMargin: 30 } + height: 45 + size: 20 + color: "white" + text: audio.title + } + + Loader { + width: parent.width + height: 5 + sourceComponent: separator + } + + //connections required to syncronize with Menu + Connections { + target: AudioDevice + onMuteToggled: { + audioMute.checkbox.checked = AudioDevice.getMuted() + } + } + + Connections { + target: AvatarInputs + onShowAudioToolsChanged: { + audioTools.checkbox.checked = showAudioTools + } + } + + AudioCheckbox { + id: audioMute + width: parent.width + anchors { left: parent.left; right: parent.right; leftMargin: 30 } + checkbox.checked: AudioDevice.muted + text.text: qsTr("Mute microphone") + onCheckBoxClicked: { + AudioDevice.muted = checked + } + } + + AudioCheckbox { + id: audioTools + width: parent.width + anchors { left: parent.left; right: parent.right; leftMargin: 30 } + checkbox.checked: AvatarInputs.showAudioTools + text.text: qsTr("Show audio level meter") + onCheckBoxClicked: { + AvatarInputs.showAudioTools = checked + } + } + + Loader { + width: parent.width + height: 5 + sourceComponent: separator + } + + Row { + anchors { left: parent.left; right: parent.right; leftMargin: 30 } + height: 40 + spacing: 8 + + HiFiGlyphs { + text: hifi.glyphs.mic + color: hifi.colors.primaryHighlight + anchors.verticalCenter: parent.verticalCenter + size: 32 + } + RalewayRegular { + anchors.verticalCenter: parent.verticalCenter + size: 16 + color: "#AFAFAF" + text: qsTr("CHOOSE INPUT DEVICE") + } + } + + ListView { + Timer { + id: refreshTimer + interval: 1 + repeat: false + onTriggered: { + //refresh model + inputAudioListView.model = undefined + inputAudioListView.model = AudioDevice.inputAudioDevices + } + } + id: inputAudioListView + anchors { left: parent.left; right: parent.right; leftMargin: 70 } + height: 125 + spacing: 16 + clip: true + snapMode: ListView.SnapToItem + delegate: AudioCheckbox { + width: parent.width + checkbox.checked: (modelData === AudioDevice.getInputDevice()) + text.text: modelData + onCheckBoxClicked: { + if (checked) { + AudioDevice.setInputDevice(modelData) + refreshTimer.start() + } + } + } + } + + Loader { + width: parent.width + height: 5 + sourceComponent: separator + } + + Row { + anchors { left: parent.left; right: parent.right; leftMargin: 30 } + height: 40 + spacing: 8 + + HiFiGlyphs { + text: hifi.glyphs.unmuted + color: hifi.colors.primaryHighlight + anchors.verticalCenter: parent.verticalCenter + size: 32 + } + RalewayRegular { + anchors.verticalCenter: parent.verticalCenter + size: 16 + color: "#AFAFAF" + text: qsTr("CHOOSE OUTPUT DEVICE") + } + } + ListView { + id: outputAudioListView + Timer { + id: refreshTimerOutput + interval: 1 + repeat: false + onTriggered: { + //refresh model + outputAudioListView.model = undefined + outputAudioListView.model = AudioDevice.outputAudioDevices + } + } + anchors { left: parent.left; right: parent.right; leftMargin: 70 } + height: 250 + spacing: 16 + clip: true + snapMode: ListView.SnapToItem + delegate: AudioCheckbox { + width: parent.width + checkbox.checked: (modelData === AudioDevice.getOutputDevice()) + text.text: modelData + onCheckBoxClicked: { + if (checked) { + AudioDevice.setOutputDevice(modelData) + refreshTimerOutput.start() + } + } + } + } + + Loader { + id: lastSeparator + width: parent.width + height: 6 + sourceComponent: separator + } + + Row { + anchors { left: parent.left; right: parent.right; leftMargin: 30 } + height: 40 + spacing: 8 + + HiFiGlyphs { + id: infoSign + text: hifi.glyphs.info + color: "#AFAFAF" + anchors.verticalCenter: parent.verticalCenter + size: 60 + } + RalewayRegular { + id: infoArea + width: parent.width - infoSign.implicitWidth - parent.spacing - 10 + wrapMode: Text.WordWrap + anchors.verticalCenter: parent.verticalCenter + size: 12 + color: hifi.colors.baseGrayHighlight + } + } + } +} diff --git a/interface/resources/qml/hifi/Card.qml b/interface/resources/qml/hifi/Card.qml index f6f7e88d0c..b72901fbdf 100644 --- a/interface/resources/qml/hifi/Card.qml +++ b/interface/resources/qml/hifi/Card.qml @@ -80,7 +80,7 @@ Rectangle { id: lobby; visible: !hasGif || (animation.status !== Image.Ready); width: parent.width - (isConcurrency ? 0 : (2 * smallMargin)); - height: parent.height - messageHeight - (isConcurrency ? 0 : smallMargin); + height: parent.height - (isConcurrency ? 0 : smallMargin); source: thumbnail || defaultThumbnail; fillMode: Image.PreserveAspectCrop; anchors { @@ -129,7 +129,7 @@ Rectangle { property int dropSamples: 9; property int dropSpread: 0; DropShadow { - visible: showPlace && (desktop ? desktop.gradientsSupported : false) + visible: true; source: place; anchors.fill: place; horizontalOffset: dropHorizontalOffset; @@ -139,12 +139,12 @@ Rectangle { color: hifi.colors.black; spread: dropSpread; } - RalewaySemiBold { + RalewayLight { id: place; visible: showPlace; text: placeName; color: hifi.colors.white; - size: textSize; + size: 38; elide: Text.ElideRight; // requires constrained width anchors { top: parent.top; @@ -153,44 +153,57 @@ Rectangle { margins: textPadding; } } - Row { - FiraSansRegular { - id: users; - visible: isConcurrency; - text: onlineUsers; - size: textSize; - color: messageColor; - anchors.verticalCenter: message.verticalCenter; + Rectangle { + id: rectRow + z: 1 + width: message.width + (users.visible ? users.width + bottomRow.spacing : 0) + + (icon.visible ? icon.width + bottomRow.spacing: 0) + bottomRow.spacing; + height: messageHeight + 1; + radius: 25 + + anchors { + bottom: parent.bottom + left: parent.left + leftMargin: textPadding + bottomMargin: textPadding } - Image { - id: icon; - source: "../../images/snap-icon.svg" - width: 40; - height: 40; - visible: action === 'snapshot'; - } - RalewayRegular { - id: message; - text: isConcurrency ? ((onlineUsers === 1) ? "person" : "people") : (drillDownToPlace ? "snapshots" : ("by " + userName)); - size: textSizeSmall; - color: messageColor; - elide: Text.ElideRight; // requires a width to be specified` - width: root.width - textPadding - - (users.visible ? users.width + parent.spacing : 0) - - (icon.visible ? icon.width + parent.spacing : 0) - - (actionIcon.width + (2 * smallMargin)); + + Row { + id: bottomRow + FiraSansRegular { + id: users; + visible: isConcurrency; + text: onlineUsers; + size: textSize; + color: messageColor; + anchors.verticalCenter: message.verticalCenter; + } + Image { + id: icon; + source: "../../images/snap-icon.svg" + width: 40; + height: 40; + visible: action === 'snapshot'; + } + RalewayRegular { + id: message; + text: isConcurrency ? ((onlineUsers === 1) ? "person" : "people") : (drillDownToPlace ? "snapshots" : ("by " + userName)); + size: textSizeSmall; + color: messageColor; + elide: Text.ElideRight; // requires a width to be specified` + anchors { + bottom: parent.bottom; + bottomMargin: parent.spacing; + } + } + spacing: textPadding; + height: messageHeight; anchors { bottom: parent.bottom; - bottomMargin: parent.spacing; + left: parent.left; + leftMargin: 4 } } - spacing: textPadding; - height: messageHeight; - anchors { - bottom: parent.bottom; - left: parent.left; - leftMargin: textPadding; - } } // These two can be supplied to provide hover behavior. // For example, AddressBarDialog provides functions that set the current list view item @@ -205,24 +218,37 @@ Rectangle { onEntered: hoverThunk(); onExited: unhoverThunk(); } - StateImage { - id: actionIcon; - imageURL: "../../images/info-icon-2-state.svg"; - size: 32; - buttonState: messageArea.containsMouse ? 1 : 0; + Rectangle { + id: rectIcon + z: 1 + width: 32 + height: 32 + radius: 15 anchors { bottom: parent.bottom; right: parent.right; - margins: smallMargin; + bottomMargin: textPadding; + rightMargin: textPadding; + } + + StateImage { + id: actionIcon; + imageURL: "../../images/info-icon-2-state.svg"; + size: 32; + buttonState: messageArea.containsMouse ? 1 : 0; + anchors { + bottom: parent.bottom; + right: parent.right; + //margins: smallMargin; + } } } + MouseArea { id: messageArea; - width: parent.width; - height: messageHeight; - anchors { - top: lobby.bottom; - } + width: rectIcon.width; + height: rectIcon.height; + anchors.fill: rectIcon acceptedButtons: Qt.LeftButton; onClicked: goFunction(drillDownToPlace ? ("/places/" + placeName) : ("/user_stories/" + storyId)); hoverEnabled: true; diff --git a/interface/resources/qml/hifi/ComboDialog.qml b/interface/resources/qml/hifi/ComboDialog.qml new file mode 100644 index 0000000000..e328805d74 --- /dev/null +++ b/interface/resources/qml/hifi/ComboDialog.qml @@ -0,0 +1,180 @@ +// +// ComboDialog.qml +// qml/hifi +// +// Created by Zach Fox on 3/31/2017 +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +import QtQuick 2.5 +import QtQuick.Controls 1.4 +import "../styles-uit" +import "../controls-uit" + +Item { + property var dialogTitleText : ""; + property var optionTitleText: ""; + property var optionBodyText: ""; + property var optionValues: []; + property var selectedOptionIndex: 0; + property var callbackFunction; + property int dialogWidth; + property int dialogHeight; + property int comboOptionTextSize: 18; + FontLoader { id: ralewayRegular; source: "../../fonts/Raleway-Regular.ttf"; } + FontLoader { id: ralewaySemiBold; source: "../../fonts/Raleway-SemiBold.ttf"; } + visible: false; + id: combo; + anchors.fill: parent; + onVisibleChanged: { + populateComboListViewModel(); + } + + MouseArea { + anchors.fill: parent + acceptedButtons: Qt.LeftButton + onClicked: { + combo.visible = false; + } + } + + Rectangle { + id: dialogBackground; + anchors.fill: parent; + color: "black"; + opacity: 0.5; + } + + Rectangle { + id: dialogContainer; + color: "white"; + anchors.centerIn: dialogBackground; + width: combo.dialogWidth; + height: combo.dialogHeight; + + RalewayRegular { + id: dialogTitle; + text: combo.dialogTitleText; + anchors.top: parent.top; + anchors.topMargin: 20; + anchors.left: parent.left; + anchors.leftMargin: 20; + size: 24; + color: 'black'; + horizontalAlignment: Text.AlignLeft; + verticalAlignment: Text.AlignTop; + } + + HiFiGlyphs { + id: closeGlyphButton; + text: hifi.glyphs.close; + size: 32; + anchors.verticalCenter: dialogTitle.verticalCenter; + anchors.right: parent.right; + anchors.rightMargin: 20; + MouseArea { + anchors.fill: closeGlyphButton; + hoverEnabled: true; + onEntered: { + parent.text = hifi.glyphs.closeInverted; + } + onExited: { + parent.text = hifi.glyphs.close; + } + onClicked: { + combo.visible = false; + } + } + } + + + ListModel { + id: comboListViewModel; + } + + ListView { + id: comboListView; + anchors.top: dialogTitle.bottom; + anchors.topMargin: 20; + anchors.bottom: parent.bottom; + anchors.left: parent.left; + anchors.right: parent.right; + clip: true; + model: comboListViewModel; + delegate: comboListViewDelegate; + + Component { + id: comboListViewDelegate; + Rectangle { + id: comboListViewItemContainer; + // Size + height: optionTitle.height + optionBody.height + 20; + width: dialogContainer.width; + color: selectedOptionIndex === index ? '#cee6ff' : 'white'; + Rectangle { + id: comboOptionSelected; + color: selectedOptionIndex == index ? hifi.colors.blueAccent : 'white'; + anchors.left: parent.left; + anchors.leftMargin: 20; + anchors.top: parent.top; + anchors.topMargin: 20; + width: 25; + height: width; + radius: width; + border.width: 3; + border.color: selectedOptionIndex === index ? hifi.colors.blueHighlight: hifi.colors.lightGrayText; + } + + + RalewaySemiBold { + id: optionTitle; + text: titleText; + anchors.top: parent.top; + anchors.topMargin: 7; + anchors.left: comboOptionSelected.right; + anchors.leftMargin: 10; + anchors.right: parent.right; + anchors.rightMargin: 10; + height: 30; + size: comboOptionTextSize; + wrapMode: Text.WordWrap; + } + + RalewayRegular { + id: optionBody; + text: bodyText; + anchors.top: optionTitle.bottom; + anchors.left: comboOptionSelected.right; + anchors.leftMargin: 25; + anchors.right: parent.right; + anchors.rightMargin: 10; + size: comboOptionTextSize; + wrapMode: Text.WordWrap; + } + + MouseArea { + anchors.fill: parent + acceptedButtons: Qt.LeftButton + hoverEnabled: true; + onEntered: comboListViewItemContainer.color = hifi.colors.blueHighlight + onExited: comboListViewItemContainer.color = selectedOptionIndex === index ? '#cee6ff' : 'white'; + onClicked: { + callbackFunction(optionValue); + combo.visible = false; + } + } + } + } + } + } + + function populateComboListViewModel() { + comboListViewModel.clear(); + optionTitleText.forEach(function(titleText, index) { + comboListViewModel.insert(index, {"titleText": titleText, "bodyText": optionBodyText[index], "optionValue": optionValues[index]}); + }); + } +} diff --git a/interface/resources/qml/hifi/Desktop.qml b/interface/resources/qml/hifi/Desktop.qml index 3e6e5b6764..7857eda3c2 100644 --- a/interface/resources/qml/hifi/Desktop.qml +++ b/interface/resources/qml/hifi/Desktop.qml @@ -55,7 +55,7 @@ OriginalDesktop.Desktop { // Literal 50 is overwritten by settings from previous session, and sysToolbar.x comes from settings when not constrained. x: sysToolbar.x y: 50 - shown: false + shown: true } Settings { diff --git a/interface/resources/qml/hifi/LetterboxMessage.qml b/interface/resources/qml/hifi/LetterboxMessage.qml index e50d1de547..754876b2c1 100644 --- a/interface/resources/qml/hifi/LetterboxMessage.qml +++ b/interface/resources/qml/hifi/LetterboxMessage.qml @@ -85,6 +85,28 @@ Item { wrapMode: Text.WordWrap textFormat: Text.StyledText } + HiFiGlyphs { + id: closeGlyphButton + text: hifi.glyphs.close + size: headerTextPixelSize + anchors.top: parent.top + anchors.topMargin: -20 + anchors.right: parent.right + anchors.rightMargin: -25 + MouseArea { + anchors.fill: closeGlyphButton + hoverEnabled: true + onEntered: { + parent.text = hifi.glyphs.closeInverted; + } + onExited: { + parent.text = hifi.glyphs.close; + } + onClicked: { + letterbox.visible = false; + } + } + } } // Popup Text Text { diff --git a/interface/resources/qml/hifi/NameCard.qml b/interface/resources/qml/hifi/NameCard.qml index 020a85b46d..6be85f2ea6 100644 --- a/interface/resources/qml/hifi/NameCard.qml +++ b/interface/resources/qml/hifi/NameCard.qml @@ -14,384 +14,543 @@ import QtQuick.Controls 1.4 import QtQuick.Controls.Styles 1.4 import QtGraphicalEffects 1.0 import "../styles-uit" +import "../controls-uit" as HifiControls +import "toolbars" + +// references Users, UserActivityLogger, MyAvatar, Vec3, Quat, AddressManager from root context Item { id: thisNameCard - // Anchors - anchors { - verticalCenter: parent.verticalCenter - leftMargin: 10 - rightMargin: 10 - } + // Size + width: isMyCard ? pal.myCardWidth - anchors.leftMargin : pal.nearbyNameCardWidth; + height: isMyCard ? pal.myCardHeight : pal.rowHeight; + anchors.left: parent.left + anchors.leftMargin: 5 + anchors.top: parent.top; // Properties + property string profileUrl: ""; + property string defaultBaseUrl: AddressManager.metaverseServerUrl; + property string connectionStatus : "" property string uuid: "" property string displayName: "" property string userName: "" property real displayNameTextPixelSize: 18 - property int usernameTextHeight: 12 + property int usernameTextPixelSize: 14 property real audioLevel: 0.0 property real avgAudioLevel: 0.0 property bool isMyCard: false property bool selected: false property bool isAdmin: false - property bool currentlyEditingDisplayName: false - - /* User image commented out for now - will probably be re-introduced later. - Column { + property bool isPresent: true + property string placeName: "" + property string profilePicBorderColor: (connectionStatus == "connection" ? hifi.colors.indigoAccent : (connectionStatus == "friend" ? hifi.colors.greenHighlight : "transparent")) + property alias avImage: avatarImage + Item { id: avatarImage + visible: profileUrl !== "" && userName !== ""; // Size - height: parent.height - width: height + height: isMyCard ? 70 : 42; + width: visible ? height : 0; + anchors.top: parent.top; + anchors.topMargin: isMyCard ? 0 : 8; + anchors.left: parent.left + clip: true Image { id: userImage - source: "../../icons/defaultNameCardUser.png" + source: profileUrl !== "" ? ((0 === profileUrl.indexOf("http")) ? profileUrl : (defaultBaseUrl + profileUrl)) : ""; + mipmap: true; // Anchors - width: parent.width - height: parent.height + anchors.fill: parent + layer.enabled: true + layer.effect: OpacityMask { + maskSource: Item { + width: userImage.width; + height: userImage.height; + Rectangle { + anchors.centerIn: parent; + width: userImage.width; // This works because userImage is square + height: width; + radius: width; + } + } + } + } + AnimatedImage { + source: "../../icons/profilePicLoading.gif" + anchors.fill: parent; + visible: userImage.status != Image.Ready; } } - */ - Item { - id: textContainer + + // Colored border around avatarImage + Rectangle { + id: avatarImageBorder; + visible: avatarImage.visible; + anchors.verticalCenter: avatarImage.verticalCenter; + anchors.horizontalCenter: avatarImage.horizontalCenter; + width: avatarImage.width + border.width; + height: avatarImage.height + border.width; + color: "transparent" + radius: avatarImage.height; + border.color: profilePicBorderColor; + border.width: 4; + } + + // DisplayName field for my card + Rectangle { + id: myDisplayName + visible: isMyCard // Size - width: parent.width - /*avatarImage.width - parent.spacing - */parent.anchors.leftMargin - parent.anchors.rightMargin - height: selected || isMyCard ? childrenRect.height : childrenRect.height - 15 - anchors.verticalCenter: parent.verticalCenter - - // DisplayName field for my card - Rectangle { - id: myDisplayName - visible: isMyCard - // Size - width: parent.width + 70 - height: 35 - // Anchors - anchors.top: parent.top - anchors.left: parent.left - anchors.leftMargin: -10 - // Style - color: hifi.colors.textFieldLightBackground - border.color: hifi.colors.blueHighlight - border.width: 0 - TextInput { - id: myDisplayNameText - // Properties - text: thisNameCard.displayName - maximumLength: 256 - clip: true - // Size - width: parent.width - height: parent.height - // Anchors - anchors.verticalCenter: parent.verticalCenter - anchors.left: parent.left - anchors.leftMargin: 10 - anchors.right: parent.right - anchors.rightMargin: editGlyph.width + editGlyph.anchors.rightMargin - // Style - color: hifi.colors.darkGray - FontLoader { id: firaSansSemiBold; source: "../../fonts/FiraSans-SemiBold.ttf"; } - font.family: firaSansSemiBold.name - font.pixelSize: displayNameTextPixelSize - selectionColor: hifi.colors.blueHighlight - selectedTextColor: "black" - // Text Positioning - verticalAlignment: TextInput.AlignVCenter - horizontalAlignment: TextInput.AlignLeft - // Signals - onEditingFinished: { - pal.sendToScript({method: 'displayNameUpdate', params: text}) - cursorPosition = 0 - focus = false - myDisplayName.border.width = 0 - color = hifi.colors.darkGray - currentlyEditingDisplayName = false - } - } - MouseArea { - anchors.fill: parent - acceptedButtons: Qt.LeftButton - hoverEnabled: true - onClicked: { - myDisplayName.border.width = 1 - myDisplayNameText.focus ? myDisplayNameText.cursorPosition = myDisplayNameText.positionAt(mouseX, mouseY, TextInput.CursorOnCharacter) : myDisplayNameText.selectAll(); - myDisplayNameText.focus = true - myDisplayNameText.color = "black" - currentlyEditingDisplayName = true - } - onDoubleClicked: { - myDisplayNameText.selectAll(); - myDisplayNameText.focus = true; - currentlyEditingDisplayName = true - } - onEntered: myDisplayName.color = hifi.colors.lightGrayText - onExited: myDisplayName.color = hifi.colors.textFieldLightBackground - } - // Edit pencil glyph - HiFiGlyphs { - id: editGlyph - text: hifi.glyphs.editPencil - // Text Size - size: displayNameTextPixelSize*1.5 - // Anchors - anchors.right: parent.right - anchors.rightMargin: 5 - anchors.verticalCenter: parent.verticalCenter - // Style - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - color: hifi.colors.baseGray - } - } - // Spacer for DisplayName for my card - Item { - id: myDisplayNameSpacer - width: 1 - height: 4 - // Anchors - anchors.top: myDisplayName.bottom - } - // DisplayName container for others' cards - Item { - id: displayNameContainer - visible: !isMyCard - // Size - width: parent.width - height: displayNameTextPixelSize + 4 - // Anchors - anchors.top: parent.top - anchors.left: parent.left - // DisplayName Text for others' cards - FiraSansSemiBold { - id: displayNameText - // Properties - text: thisNameCard.displayName - elide: Text.ElideRight - // Size - width: isAdmin ? Math.min(displayNameTextMetrics.tightBoundingRect.width + 8, parent.width - adminLabelText.width - adminLabelQuestionMark.width + 8) : parent.width - // Anchors - anchors.top: parent.top - anchors.left: parent.left - // Text Size - size: displayNameTextPixelSize - // Text Positioning - verticalAlignment: Text.AlignVCenter - // Style - color: hifi.colors.darkGray - } - TextMetrics { - id: displayNameTextMetrics - font: displayNameText.font - text: displayNameText.text - } - // "ADMIN" label for other users' cards - RalewaySemiBold { - id: adminLabelText - visible: isAdmin - text: "ADMIN" - // Text size - size: displayNameText.size - 4 - // Anchors - anchors.verticalCenter: parent.verticalCenter - anchors.left: displayNameText.right - // Style - font.capitalization: Font.AllUppercase - color: hifi.colors.redHighlight - // Alignment - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignTop - } - // This Rectangle refers to the [?] popup button next to "ADMIN" - Item { - id: adminLabelQuestionMark - visible: isAdmin - // Size - width: 20 - height: displayNameText.height - // Anchors - anchors.verticalCenter: parent.verticalCenter - anchors.left: adminLabelText.right - RalewayRegular { - id: adminLabelQuestionMarkText - text: "[?]" - size: adminLabelText.size - font.capitalization: Font.AllUppercase - color: hifi.colors.redHighlight - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - anchors.fill: parent - } - MouseArea { - anchors.fill: parent - acceptedButtons: Qt.LeftButton - hoverEnabled: true - onClicked: letterbox(hifi.glyphs.question, - "Domain Admin", - "This user is an admin on this domain. Admins can Silence and Ban other users at their discretion - so be extra nice!") - onEntered: adminLabelQuestionMarkText.color = "#94132e" - onExited: adminLabelQuestionMarkText.color = hifi.colors.redHighlight - } - } - } - - // UserName Text - FiraSansRegular { - id: userNameText + width: parent.width - avatarImage.width - anchors.leftMargin - anchors.rightMargin*2; + height: 40 + // Anchors + anchors.top: avatarImage.top + anchors.left: avatarImage.right + anchors.leftMargin: avatarImage.visible ? 5 : 0; + anchors.rightMargin: 5; + // Style + color: hifi.colors.textFieldLightBackground + border.color: hifi.colors.blueHighlight + border.width: 0 + TextInput { + id: myDisplayNameText // Properties - text: thisNameCard.userName - elide: Text.ElideRight - visible: thisNameCard.displayName + text: thisNameCard.displayName + maximumLength: 256 + clip: true // Size width: parent.width + height: parent.height // Anchors - anchors.top: isMyCard ? myDisplayNameSpacer.bottom : displayNameContainer.bottom - // Text Size - size: thisNameCard.usernameTextHeight - // Text Positioning - verticalAlignment: Text.AlignVCenter + anchors.verticalCenter: parent.verticalCenter + anchors.left: parent.left + anchors.leftMargin: 10 + anchors.right: parent.right + anchors.rightMargin: editGlyph.width + editGlyph.anchors.rightMargin // Style + color: hifi.colors.darkGray + FontLoader { id: firaSansSemiBold; source: "../../fonts/FiraSans-SemiBold.ttf"; } + font.family: firaSansSemiBold.name + font.pixelSize: displayNameTextPixelSize + selectionColor: hifi.colors.blueAccent + selectedTextColor: "black" + // Text Positioning + verticalAlignment: TextInput.AlignVCenter + horizontalAlignment: TextInput.AlignLeft + autoScroll: false; + // Signals + onEditingFinished: { + if (MyAvatar.displayName !== text) { + MyAvatar.displayName = text; + UserActivityLogger.palAction("display_name_change", text); + } + cursorPosition = 0 + focus = false + myDisplayName.border.width = 0 + color = hifi.colors.darkGray + pal.currentlyEditingDisplayName = false + autoScroll = false; + } + } + MouseArea { + anchors.fill: parent + hoverEnabled: true + onClicked: { + myDisplayName.border.width = 1 + myDisplayNameText.focus ? myDisplayNameText.cursorPosition = myDisplayNameText.positionAt(mouseX, mouseY, TextInput.CursorOnCharacter) : myDisplayNameText.selectAll(); + myDisplayNameText.focus = true + myDisplayNameText.color = "black" + pal.currentlyEditingDisplayName = true + myDisplayNameText.autoScroll = true; + } + onDoubleClicked: { + myDisplayNameText.selectAll(); + myDisplayNameText.focus = true; + pal.currentlyEditingDisplayName = true + myDisplayNameText.autoScroll = true; + } + onEntered: myDisplayName.color = hifi.colors.lightGrayText; + onExited: myDisplayName.color = hifi.colors.textFieldLightBackground; + } + // Edit pencil glyph + HiFiGlyphs { + id: editGlyph + text: hifi.glyphs.editPencil + // Text Size + size: displayNameTextPixelSize*1.5 + // Anchors + anchors.right: parent.right + anchors.rightMargin: 5 + anchors.verticalCenter: parent.verticalCenter + // Style + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter color: hifi.colors.baseGray } - - // Spacer - Item { - id: userNameSpacer - height: 4 - width: parent.width - // Anchors - anchors.top: userNameText.bottom - } - - // VU Meter - Rectangle { - id: nameCardVUMeter - // Size - width: isMyCard ? myDisplayName.width - 70 : ((gainSlider.value - gainSlider.minimumValue)/(gainSlider.maximumValue - gainSlider.minimumValue)) * parent.width - height: 8 - // Anchors - anchors.top: userNameSpacer.bottom - // Style - radius: 4 - color: "#c5c5c5" - visible: isMyCard || selected - // Rectangle for the zero-gain point on the VU meter - Rectangle { - id: vuMeterZeroGain - visible: gainSlider.visible - // Size - width: 4 - height: 18 - // Style - color: hifi.colors.darkGray - // Anchors - anchors.verticalCenter: parent.verticalCenter - anchors.left: parent.left - anchors.leftMargin: (-gainSlider.minimumValue)/(gainSlider.maximumValue - gainSlider.minimumValue) * gainSlider.width - 4 - } - // Rectangle for the VU meter line - Rectangle { - id: vuMeterLine - width: gainSlider.width - visible: gainSlider.visible - // Style - color: vuMeterBase.color - radius: nameCardVUMeter.radius - height: nameCardVUMeter.height / 2 - anchors.verticalCenter: nameCardVUMeter.verticalCenter - } - // Rectangle for the VU meter base - Rectangle { - id: vuMeterBase - // Anchors - anchors.fill: parent - visible: isMyCard || selected - // Style - color: parent.color - radius: parent.radius - } - // Rectangle for the VU meter audio level - Rectangle { - id: vuMeterLevel - visible: isMyCard || selected - // Size - width: (thisNameCard.audioLevel) * parent.width - // Style - color: parent.color - radius: parent.radius - // Anchors - anchors.bottom: parent.bottom - anchors.top: parent.top - anchors.left: parent.left - } - // Gradient for the VU meter audio level - LinearGradient { - anchors.fill: vuMeterLevel - source: vuMeterLevel - start: Qt.point(0, 0) - end: Qt.point(parent.width, 0) - gradient: Gradient { - GradientStop { position: 0.0; color: "#2c8e72" } - GradientStop { position: 0.9; color: "#1fc6a6" } - GradientStop { position: 0.91; color: "#ea4c5f" } - GradientStop { position: 1.0; color: "#ea4c5f" } - } - } - } - - // Per-Avatar Gain Slider - Slider { - id: gainSlider - // Size - width: parent.width - height: 14 - // Anchors - anchors.verticalCenter: nameCardVUMeter.verticalCenter + } + // DisplayName container for others' cards + Item { + id: displayNameContainer + visible: !isMyCard && pal.activeTab !== "connectionsTab" + // Size + width: parent.width - anchors.leftMargin - avatarImage.width - anchors.leftMargin; + height: displayNameTextPixelSize + 4 + // Anchors + anchors.top: avatarImage.top; + anchors.left: avatarImage.right + anchors.leftMargin: avatarImage.visible ? 5 : 0; + // DisplayName Text for others' cards + FiraSansSemiBold { + id: displayNameText // Properties - visible: !isMyCard && selected - value: Users.getAvatarGain(uuid) - minimumValue: -60.0 - maximumValue: 20.0 - stepSize: 5 - updateValueWhileDragging: true - onValueChanged: updateGainFromQML(uuid, value, false) - onPressedChanged: { - if (!pressed) { - updateGainFromQML(uuid, value, true) + text: thisNameCard.displayName + elide: Text.ElideRight + // Size + width: isAdmin ? Math.min(displayNameTextMetrics.tightBoundingRect.width + 8, parent.width - adminLabelText.width - adminLabelQuestionMark.width + 8) : parent.width + // Anchors + anchors.top: parent.top + anchors.left: parent.left + // Text Size + size: displayNameTextPixelSize + // Text Positioning + verticalAlignment: Text.AlignTop + // Style + color: hifi.colors.darkGray; + MouseArea { + anchors.fill: parent + enabled: selected && pal.activeTab == "nearbyTab" && thisNameCard.userName !== "" && isPresent; + hoverEnabled: enabled + onClicked: { + goToUserInDomain(thisNameCard.uuid); + UserActivityLogger.palAction("go_to_user_in_domain", thisNameCard.uuid); } + onEntered: { + displayNameText.color = hifi.colors.blueHighlight; + userNameText.color = hifi.colors.blueHighlight; + } + onExited: { + displayNameText.color = hifi.colors.darkGray + userNameText.color = hifi.colors.blueAccent; + } + } + } + TextMetrics { + id: displayNameTextMetrics + font: displayNameText.font + text: displayNameText.text + } + // "ADMIN" label for other users' cards + RalewaySemiBold { + id: adminLabelText + visible: isAdmin + text: "ADMIN" + // Text size + size: displayNameText.size - 4 + // Anchors + anchors.verticalCenter: parent.verticalCenter + anchors.left: displayNameText.right + // Style + font.capitalization: Font.AllUppercase + color: hifi.colors.redHighlight + // Alignment + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignTop + } + // This Rectangle refers to the [?] popup button next to "ADMIN" + Item { + id: adminLabelQuestionMark + visible: isAdmin + // Size + width: 20 + height: displayNameText.height + // Anchors + anchors.verticalCenter: parent.verticalCenter + anchors.left: adminLabelText.right + RalewayRegular { + id: adminLabelQuestionMarkText + text: "[?]" + size: adminLabelText.size + font.capitalization: Font.AllUppercase + color: hifi.colors.redHighlight + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + anchors.fill: parent } MouseArea { anchors.fill: parent - onWheel: { - // Do nothing. - } - onDoubleClicked: { - gainSlider.value = 0.0 - } - onPressed: { - // Pass through to Slider - mouse.accepted = false - } - onReleased: { - // the above mouse.accepted seems to make this - // never get called, nonetheless... - mouse.accepted = false - } + enabled: isPresent + hoverEnabled: enabled + onClicked: letterbox(hifi.glyphs.question, + "Domain Admin", + "This user is an admin on this domain. Admins can Silence and Ban other users at their discretion - so be extra nice!") + onEntered: adminLabelQuestionMarkText.color = "#94132e" + onExited: adminLabelQuestionMarkText.color = hifi.colors.redHighlight } - style: SliderStyle { - groove: Rectangle { - color: "#c5c5c5" - implicitWidth: gainSlider.width - implicitHeight: 4 - radius: 2 - opacity: 0 - } - handle: Rectangle { - anchors.centerIn: parent - color: (control.pressed || control.hovered) ? "#00b4ef" : "#8F8F8F" - implicitWidth: 10 - implicitHeight: 16 - } + } + } + + // UserName Text + FiraSansRegular { + id: userNameText + // Properties + text: thisNameCard.userName === "Unknown user" ? "not logged in" : thisNameCard.userName; + elide: Text.ElideRight + visible: thisNameCard.userName !== ""; + // Size + width: parent.width + height: usernameTextPixelSize + 4 + // Anchors + anchors.top: isMyCard ? myDisplayName.bottom : pal.activeTab == "nearbyTab" ? displayNameContainer.bottom : undefined //(parent.height - displayNameTextPixelSize/2)); + anchors.verticalCenter: pal.activeTab == "connectionsTab" && !isMyCard ? avatarImage.verticalCenter : undefined + anchors.left: avatarImage.right; + anchors.leftMargin: avatarImage.visible ? 5 : 0; + anchors.rightMargin: 5; + // Text Size + size: pal.activeTab == "nearbyTab" || isMyCard ? usernameTextPixelSize : displayNameTextPixelSize; + // Text Positioning + verticalAlignment: Text.AlignVCenter; + // Style + color: hifi.colors.blueAccent; + MouseArea { + anchors.fill: parent + enabled: selected && pal.activeTab == "nearbyTab" && thisNameCard.userName !== "" && isPresent; + hoverEnabled: enabled + onClicked: { + goToUserInDomain(thisNameCard.uuid); + UserActivityLogger.palAction("go_to_user_in_domain", thisNameCard.uuid); + } + onEntered: { + displayNameText.color = hifi.colors.blueHighlight; + userNameText.color = hifi.colors.blueHighlight; + } + onExited: { + displayNameText.color = hifi.colors.darkGray; + userNameText.color = hifi.colors.blueAccent; + } + } + } + StateImage { + id: nameCardConnectionInfoImage + visible: selected && !isMyCard && pal.activeTab == "connectionsTab" + imageURL: "../../images/info-icon-2-state.svg" // PLACEHOLDER!!! + size: 32; + buttonState: 0; + anchors.left: avatarImage.right + anchors.bottom: parent.bottom + } + MouseArea { + anchors.fill:nameCardConnectionInfoImage + enabled: selected + hoverEnabled: true + onClicked: { + userInfoViewer.url = defaultBaseUrl + "/users/" + userName; + userInfoViewer.visible = true; + } + onEntered: { + nameCardConnectionInfoImage.buttonState = 1; + } + onExited: { + nameCardConnectionInfoImage.buttonState = 0; + } + } + FiraSansRegular { + id: nameCardConnectionInfoText + visible: selected && !isMyCard && pal.activeTab == "connectionsTab" && !isMyCard + width: parent.width + height: displayNameTextPixelSize + size: displayNameTextPixelSize - 4 + anchors.left: nameCardConnectionInfoImage.right + anchors.verticalCenter: nameCardConnectionInfoImage.verticalCenter + anchors.leftMargin: 5 + verticalAlignment: Text.AlignVCenter + text: "Info" + color: hifi.colors.baseGray + } + HiFiGlyphs { + id: nameCardRemoveConnectionImage + visible: selected && !isMyCard && pal.activeTab == "connectionsTab" + text: hifi.glyphs.close + size: 28; + x: 120 + anchors.verticalCenter: nameCardConnectionInfoImage.verticalCenter + } + MouseArea { + anchors.fill:nameCardRemoveConnectionImage + enabled: selected + hoverEnabled: true + onClicked: { + // send message to pal.js to forgetConnection + pal.sendToScript({method: 'removeConnection', params: thisNameCard.userName}); + } + onEntered: { + nameCardRemoveConnectionImage.text = hifi.glyphs.closeInverted; + } + onExited: { + nameCardRemoveConnectionImage.text = hifi.glyphs.close; + } + } + FiraSansRegular { + id: nameCardRemoveConnectionText + visible: selected && !isMyCard && pal.activeTab == "connectionsTab" && !isMyCard + width: parent.width + height: displayNameTextPixelSize + size: displayNameTextPixelSize - 4 + anchors.left: nameCardRemoveConnectionImage.right + anchors.verticalCenter: nameCardRemoveConnectionImage.verticalCenter + anchors.leftMargin: 5 + verticalAlignment: Text.AlignVCenter + text: "Forget" + color: hifi.colors.baseGray + } + HifiControls.Button { + id: visitConnectionButton + visible: selected && !isMyCard && pal.activeTab == "connectionsTab" && !isMyCard + text: "Visit" + enabled: thisNameCard.placeName !== "" + anchors.verticalCenter: nameCardRemoveConnectionImage.verticalCenter + x: 240 + onClicked: { + AddressManager.goToUser(thisNameCard.userName); + UserActivityLogger.palAction("go_to_user", thisNameCard.userName); + } + } + + // VU Meter + Rectangle { + id: nameCardVUMeter + // Size + width: isMyCard ? myDisplayName.width - 20 : ((gainSlider.value - gainSlider.minimumValue)/(gainSlider.maximumValue - gainSlider.minimumValue)) * (gainSlider.width); + height: 8 + // Anchors + anchors.bottom: isMyCard ? avatarImage.bottom : parent.bottom; + anchors.bottomMargin: isMyCard ? 0 : height; + anchors.left: isMyCard ? userNameText.left : parent.left; + // Style + radius: 4 + color: "#c5c5c5" + visible: (isMyCard || (selected && pal.activeTab == "nearbyTab")) && isPresent + // Rectangle for the zero-gain point on the VU meter + Rectangle { + id: vuMeterZeroGain + visible: gainSlider.visible + // Size + width: 4 + height: 18 + // Style + color: hifi.colors.darkGray + // Anchors + anchors.verticalCenter: parent.verticalCenter + anchors.left: parent.left + anchors.leftMargin: (-gainSlider.minimumValue)/(gainSlider.maximumValue - gainSlider.minimumValue) * gainSlider.width - 4 + } + // Rectangle for the VU meter line + Rectangle { + id: vuMeterLine + width: gainSlider.width + visible: gainSlider.visible + // Style + color: vuMeterBase.color + radius: nameCardVUMeter.radius + height: nameCardVUMeter.height / 2 + anchors.verticalCenter: nameCardVUMeter.verticalCenter + } + // Rectangle for the VU meter base + Rectangle { + id: vuMeterBase + // Anchors + anchors.fill: parent + visible: isMyCard || selected + // Style + color: parent.color + radius: parent.radius + } + // Rectangle for the VU meter audio level + Rectangle { + id: vuMeterLevel + visible: isMyCard || selected + // Size + width: (thisNameCard.audioLevel) * parent.width + // Style + color: parent.color + radius: parent.radius + // Anchors + anchors.bottom: parent.bottom + anchors.top: parent.top + anchors.left: parent.left + } + // Gradient for the VU meter audio level + LinearGradient { + anchors.fill: vuMeterLevel + source: vuMeterLevel + start: Qt.point(0, 0) + end: Qt.point(parent.width, 0) + gradient: Gradient { + GradientStop { position: 0.0; color: "#2c8e72" } + GradientStop { position: 0.9; color: "#1fc6a6" } + GradientStop { position: 0.91; color: "#ea4c5f" } + GradientStop { position: 1.0; color: "#ea4c5f" } + } + } + } + + // Per-Avatar Gain Slider + Slider { + id: gainSlider + // Size + width: thisNameCard.width; + height: 14 + // Anchors + anchors.verticalCenter: nameCardVUMeter.verticalCenter; + anchors.left: nameCardVUMeter.left; + // Properties + visible: !isMyCard && selected && pal.activeTab == "nearbyTab" && isPresent; + value: Users.getAvatarGain(uuid) + minimumValue: -60.0 + maximumValue: 20.0 + stepSize: 5 + updateValueWhileDragging: true + onValueChanged: { + if (uuid !== "") { + updateGainFromQML(uuid, value, false); + } + } + onPressedChanged: { + if (!pressed) { + updateGainFromQML(uuid, value, true) + } + } + MouseArea { + anchors.fill: parent + onWheel: { + // Do nothing. + } + onDoubleClicked: { + gainSlider.value = 0.0 + } + onPressed: { + // Pass through to Slider + mouse.accepted = false + } + onReleased: { + // the above mouse.accepted seems to make this + // never get called, nonetheless... + mouse.accepted = false + } + } + style: SliderStyle { + groove: Rectangle { + color: "#c5c5c5" + implicitWidth: gainSlider.width + implicitHeight: 4 + radius: 2 + opacity: 0 + } + handle: Rectangle { + anchors.centerIn: parent + color: (control.pressed || control.hovered) ? "#00b4ef" : "#8F8F8F" + implicitWidth: 10 + implicitHeight: 16 } } } @@ -402,4 +561,22 @@ Item { UserActivityLogger.palAction("avatar_gain_changed", avatarUuid); } } + + // Function body by Howard Stearns 2017-01-08 + function goToUserInDomain(avatarUuid) { + var avatar = AvatarList.getAvatar(avatarUuid); + if (!avatar) { + console.log("This avatar is no longer present. goToUserInDomain() failed."); + return; + } + var vector = Vec3.subtract(avatar.position, MyAvatar.position); + var distance = Vec3.length(vector); + var target = Vec3.multiply(Vec3.normalize(vector), distance - 2.0); + // FIXME: We would like the avatar to recompute the avatar's "maybe fly" test at the new position, so that if high enough up, + // the avatar goes into fly mode rather than falling. However, that is not exposed to Javascript right now. + // FIXME: it would be nice if this used the same teleport steps and smoothing as in the teleport.js script. + // Note, however, that this script allows teleporting to a person in the air, while teleport.js is going to a grounded target. + MyAvatar.orientation = Quat.lookAtSimple(MyAvatar.position, avatar.position); + MyAvatar.position = Vec3.sum(MyAvatar.position, target); + } } diff --git a/interface/resources/qml/hifi/Pal.qml b/interface/resources/qml/hifi/Pal.qml index 28384f9c1c..d785a8582c 100644 --- a/interface/resources/qml/hifi/Pal.qml +++ b/interface/resources/qml/hifi/Pal.qml @@ -16,485 +16,1191 @@ import QtQuick.Controls 1.4 import QtGraphicalEffects 1.0 import Qt.labs.settings 1.0 import "../styles-uit" -import "../controls-uit" as HifiControls +import "../controls-uit" as HifiControlsUit +import "../controls" as HifiControls // references HMD, Users, UserActivityLogger from root context Rectangle { - id: pal + id: pal; // Size - width: parent.width - height: parent.height + width: parent.width; + height: parent.height; // Style - color: "#E3E3E3" + color: "#E3E3E3"; // Properties - property int myCardHeight: 90 - property int rowHeight: 70 - property int actionButtonWidth: 55 - property int actionButtonAllowance: actionButtonWidth * 2 - property int minNameCardWidth: palContainer.width - (actionButtonAllowance * 2) - 4 - hifi.dimensions.scrollbarBackgroundWidth - property int nameCardWidth: minNameCardWidth + (iAmAdmin ? 0 : actionButtonAllowance) - property var myData: ({displayName: "", userName: "", audioLevel: 0.0, avgAudioLevel: 0.0, admin: true}) // valid dummy until set + property int myCardWidth: width - upperRightInfoContainer.width; + property int myCardHeight: 80; + property int rowHeight: 60; + property int actionButtonWidth: 55; + property int locationColumnWidth: 170; + property int nearbyNameCardWidth: nearbyTable.width - (iAmAdmin ? (actionButtonWidth * 4) : (actionButtonWidth * 2)) - 4 - hifi.dimensions.scrollbarBackgroundWidth; + property int connectionsNameCardWidth: connectionsTable.width - locationColumnWidth - actionButtonWidth - 4 - hifi.dimensions.scrollbarBackgroundWidth; + property var myData: ({profileUrl: "", displayName: "", userName: "", audioLevel: 0.0, avgAudioLevel: 0.0, admin: true, placeName: "", connection: "", isPresent: true}); // valid dummy until set property var ignored: ({}); // Keep a local list of ignored avatars & their data. Necessary because HashMap is slow to respond after ignoring. - property var userModelData: [] // This simple list is essentially a mirror of the userModel listModel without all the extra complexities. - property bool iAmAdmin: false + property var nearbyUserModelData: []; // This simple list is essentially a mirror of the nearbyUserModel listModel without all the extra complexities. + property var connectionsUserModelData: []; // This simple list is essentially a mirror of the connectionsUserModel listModel without all the extra complexities. + property bool iAmAdmin: false; + property var activeTab: "nearbyTab"; + property bool currentlyEditingDisplayName: false + property bool punctuationMode: false; - HifiConstants { id: hifi } + HifiConstants { id: hifi; } // The letterbox used for popup messages LetterboxMessage { - id: letterboxMessage - z: 999 // Force the popup on top of everything else + id: letterboxMessage; + z: 999; // Force the popup on top of everything else + } + Connections { + target: GlobalServices + onMyUsernameChanged: { + myData.userName = Account.username; + myDataChanged(); // Setting a property within an object isn't enough to update dependencies. This will do it. + } + } + // The ComboDialog used for setting availability + ComboDialog { + id: comboDialog; + z: 999; // Force the ComboDialog on top of everything else + dialogWidth: parent.width - 50; + dialogHeight: parent.height - 100; } function letterbox(headerGlyph, headerText, message) { - letterboxMessage.headerGlyph = headerGlyph - letterboxMessage.headerText = headerText - letterboxMessage.text = message - letterboxMessage.visible = true - letterboxMessage.popupRadius = 0 + letterboxMessage.headerGlyph = headerGlyph; + letterboxMessage.headerText = headerText; + letterboxMessage.text = message; + letterboxMessage.visible = true; + letterboxMessage.popupRadius = 0; + } + function popupComboDialogCallback(availability) { + GlobalServices.findableBy = availability; + UserActivityLogger.palAction("set_availability", availability); + print('Setting availability:', JSON.stringify(GlobalServices.findableBy)); + } + function popupComboDialog(dialogTitleText, optionTitleText, optionBodyText, optionValues) { + comboDialog.callbackFunction = popupComboDialogCallback; + comboDialog.dialogTitleText = dialogTitleText; + comboDialog.optionTitleText = optionTitleText; + comboDialog.optionBodyText = optionBodyText; + comboDialog.optionValues = optionValues; + comboDialog.selectedOptionIndex = ['all', 'connections', 'friends', 'none'].indexOf(GlobalServices.findableBy); + comboDialog.populateComboListViewModel(); + comboDialog.visible = true; } Settings { - id: settings - category: "pal" - property bool filtered: false - property int nearDistance: 30 - property int sortIndicatorColumn: 1 - property int sortIndicatorOrder: Qt.AscendingOrder + id: settings; + category: "pal"; + property bool filtered: false; + property int nearDistance: 30; + property int nearbySortIndicatorColumn: 1; + property int nearbySortIndicatorOrder: Qt.AscendingOrder; + property int connectionsSortIndicatorColumn: 0; + property int connectionsSortIndicatorOrder: Qt.AscendingOrder; } - function refreshWithFilter() { - // We should just be able to set settings.filtered to filter.checked, but see #3249, so send to .js for saving. - pal.sendToScript({method: 'refresh', params: {filter: filter.checked && {distance: settings.nearDistance}}}); + function getSelectedNearbySessionIDs() { + var sessionIDs = []; + nearbyTable.selection.forEach(function (userIndex) { + var datum = nearbyUserModelData[userIndex]; + if (datum) { // Might have been filtered out + sessionIDs.push(datum.sessionId); + } + }); + return sessionIDs; + } + function getSelectedConnectionsUserNames() { + var userNames = []; + connectionsTable.selection.forEach(function (userIndex) { + var datum = connectionsUserModelData[userIndex]; + if (datum) { + userNames.push(datum.userName); + } + }); + return userNames; + } + function refreshNearbyWithFilter() { + // We should just be able to set settings.filtered to inViewCheckbox.checked, but see #3249, so send to .js for saving. + var userIds = getSelectedNearbySessionIDs(); + var params = {filter: inViewCheckbox.checked && {distance: settings.nearDistance}}; + if (userIds.length > 0) { + params.selected = [[userIds[0]], true, true]; + } + pal.sendToScript({method: 'refreshNearby', params: params}); } - // This is the container for the PAL - Rectangle { - property bool punctuationMode: false - id: palContainer - // Size - width: pal.width - 10 - height: pal.height - 10 - // Style - color: pal.color + Item { + id: palTabContainer; // Anchors - anchors.centerIn: pal - // Properties - radius: hifi.dimensions.borderRadius + anchors { + top: myInfo.bottom; + bottom: parent.bottom; + left: parent.left; + right: parent.right; + } + Rectangle { + id: tabSelectorContainer; + // Anchors + anchors { + top: parent.top; + horizontalCenter: parent.horizontalCenter; + } + width: parent.width; + height: 50; + Rectangle { + id: nearbyTabSelector; + // Anchors + anchors { + top: parent.top; + left: parent.left; + } + width: parent.width/2; + height: parent.height; + color: activeTab == "nearbyTab" ? "white" : "#CCCCCC"; + MouseArea { + anchors.fill: parent; + onClicked: { + if (activeTab != "nearbyTab") { + refreshNearbyWithFilter(); + } + activeTab = "nearbyTab"; + connectionsHelpText.color = hifi.colors.baseGray; + } + } + + // "NEARBY" Text Container + Item { + id: nearbyTabSelectorTextContainer; + anchors.fill: parent; + anchors.leftMargin: 15; + // "NEARBY" text + RalewaySemiBold { + id: nearbyTabSelectorText; + text: "NEARBY"; + // Text size + size: hifi.fontSizes.tabularData; + // Anchors + anchors.fill: parent; + // Style + font.capitalization: Font.AllUppercase; + color: activeTab === "nearbyTab" ? hifi.colors.blueAccent : hifi.colors.baseGray; + // Alignment + horizontalAlignment: Text.AlignHLeft; + verticalAlignment: Text.AlignVCenter; + } + // "In View" Checkbox + HifiControlsUit.CheckBox { + id: inViewCheckbox; + visible: activeTab == "nearbyTab"; + anchors.right: reloadNearbyContainer.left; + anchors.rightMargin: 25; + anchors.verticalCenter: parent.verticalCenter; + checked: settings.filtered; + text: "in view"; + boxSize: 24; + onCheckedChanged: refreshNearbyWithFilter(); + } + // Refresh button + Rectangle { + id: reloadNearbyContainer + visible: activeTab == "nearbyTab"; + anchors.verticalCenter: parent.verticalCenter; + anchors.right: parent.right; + anchors.rightMargin: 6; + height: reloadNearby.height; + width: height; + HifiControlsUit.GlyphButton { + id: reloadNearby; + width: reloadNearby.height; + glyph: hifi.glyphs.reload; + onClicked: { + refreshNearbyWithFilter(); + } + } + } + } + } + Rectangle { + id: connectionsTabSelector; + // Anchors + anchors { + top: parent.top; + left: nearbyTabSelector.right; + } + width: parent.width/2; + height: parent.height; + color: activeTab == "connectionsTab" ? "white" : "#CCCCCC"; + MouseArea { + anchors.fill: parent; + onClicked: { + if (activeTab != "connectionsTab") { + connectionsLoading.visible = false; + connectionsLoading.visible = true; + pal.sendToScript({method: 'refreshConnections'}); + } + activeTab = "connectionsTab"; + connectionsHelpText.color = hifi.colors.blueAccent; + } + } + + // "CONNECTIONS" Text Container + Item { + id: connectionsTabSelectorTextContainer; + anchors.fill: parent; + anchors.leftMargin: 15; + // Refresh button + Rectangle { + visible: activeTab == "connectionsTab"; + anchors.verticalCenter: parent.verticalCenter; + anchors.right: parent.right; + anchors.rightMargin: 6; + height: reloadConnections.height; + width: height; + HifiControlsUit.GlyphButton { + id: reloadConnections; + width: reloadConnections.height; + glyph: hifi.glyphs.reload; + onClicked: { + connectionsLoading.visible = false; + connectionsLoading.visible = true; + pal.sendToScript({method: 'refreshConnections'}); + } + } + } + // "CONNECTIONS" text + RalewaySemiBold { + id: connectionsTabSelectorText; + text: "CONNECTIONS"; + // Text size + size: hifi.fontSizes.tabularData; + // Anchors + anchors.fill: parent; + // Style + font.capitalization: Font.AllUppercase; + color: activeTab === "connectionsTab" ? hifi.colors.blueAccent : hifi.colors.baseGray; + // Alignment + horizontalAlignment: Text.AlignHLeft; + verticalAlignment: Text.AlignVCenter; + } + TextMetrics { + id: connectionsTabSelectorTextMetrics; + text: connectionsTabSelectorText.text; + } + + // This Rectangle refers to the [?] popup button next to "CONNECTIONS" + Rectangle { + color: connectionsTabSelector.color; + width: 20; + height: connectionsTabSelectorText.height - 2; + anchors.left: connectionsTabSelectorTextContainer.left; + anchors.top: connectionsTabSelectorTextContainer.top; + anchors.topMargin: 1; + anchors.leftMargin: connectionsTabSelectorTextMetrics.width + 42; + RalewayRegular { + id: connectionsHelpText; + text: "[?]"; + size: connectionsTabSelectorText.size + 6; + font.capitalization: Font.AllUppercase; + color: connectionsTabSelectorText.color; + horizontalAlignment: Text.AlignHCenter; + verticalAlignment: Text.AlignVCenter; + anchors.fill: parent; + } + MouseArea { + anchors.fill: parent; + hoverEnabled: true; + onClicked: letterbox(hifi.glyphs.question, + "Connections and Friends", + "Purple borders around profile pictures represent Connections.
" + + "When your availability is set to Everyone, Connections can see your username and location.

" + + "Green borders around profile pictures represent Friends.
" + + "When your availability is set to Friends, only Friends can see your username and location."); + onEntered: connectionsHelpText.color = hifi.colors.blueHighlight; + onExited: connectionsHelpText.color = hifi.colors.blueAccent; + } + } + } + } + } + + /***************************************** + NEARBY TAB + *****************************************/ + Rectangle { + id: nearbyTab; + // Anchors + anchors { + top: tabSelectorContainer.bottom; + topMargin: 12 + (iAmAdmin ? -adminTab.anchors.topMargin : 0); + bottom: parent.bottom; + bottomMargin: 12; + horizontalCenter: parent.horizontalCenter; + } + width: parent.width - 12; + visible: activeTab == "nearbyTab"; + + // Rectangle that houses "ADMIN" string + Rectangle { + id: adminTab; + // Size + width: 2*actionButtonWidth + hifi.dimensions.scrollbarBackgroundWidth + 6; + height: 40; + // Anchors + anchors.top: parent.top; + anchors.topMargin: -30; + anchors.right: parent.right; + // Properties + visible: iAmAdmin; + // Style + color: hifi.colors.tableRowLightEven; + border.color: hifi.colors.lightGrayText; + border.width: 2; + // "ADMIN" text + RalewaySemiBold { + id: adminTabText; + text: "ADMIN"; + // Text size + size: hifi.fontSizes.tableHeading + 2; + // Anchors + anchors.top: parent.top; + anchors.topMargin: 8; + anchors.left: parent.left; + anchors.right: parent.right; + anchors.rightMargin: hifi.dimensions.scrollbarBackgroundWidth; + // Style + font.capitalization: Font.AllUppercase; + color: hifi.colors.redHighlight; + // Alignment + horizontalAlignment: Text.AlignHCenter; + verticalAlignment: Text.AlignTop; + } + } + // This TableView refers to the Nearby Table (on the "Nearby" tab below the current user's NameCard) + HifiControlsUit.Table { + id: nearbyTable; + flickableItem.interactive: true; + // Anchors + anchors.fill: parent; + // Properties + centerHeaderText: true; + sortIndicatorVisible: true; + headerVisible: true; + sortIndicatorColumn: settings.nearbySortIndicatorColumn; + sortIndicatorOrder: settings.nearbySortIndicatorOrder; + onSortIndicatorColumnChanged: { + settings.nearbySortIndicatorColumn = sortIndicatorColumn; + sortModel(); + } + onSortIndicatorOrderChanged: { + settings.nearbySortIndicatorOrder = sortIndicatorOrder; + sortModel(); + } + + TableViewColumn { + role: "avgAudioLevel"; + title: "LOUD"; + width: actionButtonWidth; + movable: false; + resizable: false; + } + + TableViewColumn { + id: displayNameHeader; + role: "displayName"; + title: nearbyTable.rowCount + (nearbyTable.rowCount === 1 ? " NAME" : " NAMES"); + width: nearbyNameCardWidth; + movable: false; + resizable: false; + } + TableViewColumn { + role: "ignore"; + title: "IGNORE"; + width: actionButtonWidth; + movable: false; + resizable: false; + } + TableViewColumn { + visible: iAmAdmin; + role: "mute"; + title: "SILENCE"; + width: actionButtonWidth; + movable: false; + resizable: false; + } + TableViewColumn { + visible: iAmAdmin; + role: "kick"; + title: "BAN"; + width: actionButtonWidth; + movable: false; + resizable: false; + } + model: ListModel { + id: nearbyUserModel; + } + + // This Rectangle refers to each Row in the nearbyTable. + rowDelegate: Rectangle { // The only way I know to specify a row height. + // Size + height: rowHeight + (styleData.selected ? 15 : 0); + color: nearbyRowColor(styleData.selected, styleData.alternate); + } + + // This Item refers to the contents of each Cell + itemDelegate: Item { + id: itemCell; + property bool isCheckBox: styleData.role === "personalMute" || styleData.role === "ignore"; + property bool isButton: styleData.role === "mute" || styleData.role === "kick"; + property bool isAvgAudio: styleData.role === "avgAudioLevel"; + opacity: !isButton ? (model && model.isPresent ? 1.0 : 0.4) : 1.0; // Admin actions shouldn't turn gray + + // This NameCard refers to the cell that contains an avatar's + // DisplayName and UserName + NameCard { + id: nameCard; + // Properties + profileUrl: (model && model.profileUrl) || ""; + displayName: styleData.value; + userName: model ? model.userName : ""; + connectionStatus: model ? model.connection : ""; + audioLevel: model ? model.audioLevel : 0.0; + avgAudioLevel: model ? model.avgAudioLevel : 0.0; + visible: !isCheckBox && !isButton && !isAvgAudio; + uuid: model ? model.sessionId : ""; + selected: styleData.selected; + isAdmin: model && model.admin; + isPresent: model && model.isPresent; + // Size + width: nearbyNameCardWidth; + height: parent.height; + // Anchors + anchors.left: parent.left; + } + HifiControlsUit.GlyphButton { + function getGlyph() { + var fileName = "vol_"; + if (model && model.personalMute) { + fileName += "x_"; + } + fileName += (4.0*(model ? model.avgAudioLevel : 0.0)).toFixed(0); + return hifi.glyphs[fileName]; + } + id: avgAudioVolume; + visible: isAvgAudio; + glyph: getGlyph(); + width: 32; + size: height; + anchors.verticalCenter: parent.verticalCenter; + anchors.horizontalCenter: parent.horizontalCenter; + enabled: (model ? !model["ignore"] && model["isPresent"] : true); + onClicked: { + // cannot change mute status when ignoring + if (!model["ignore"]) { + var newValue = !model["personalMute"]; + nearbyUserModel.setProperty(model.userIndex, "personalMute", newValue); + nearbyUserModelData[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 ("Ignore" for now) + // KNOWN BUG with the Checkboxes: When clicking in the center of the sorting header, the checkbox + // will appear in the "hovered" state. Hovering over the checkbox will fix it. + // Clicking on the sides of the sorting header doesn't cause this problem. + // I'm guessing this is a QT bug and not anything I can fix. I spent too long trying to work around it... + // I'm just going to leave the minor visual bug in. + HifiControlsUit.CheckBox { + id: actionCheckBox; + visible: isCheckBox; + anchors.centerIn: parent; + checked: model ? model[styleData.role] : false; + // If this is an "Ignore" checkbox, disable the checkbox if user isn't present. + enabled: styleData.role === "ignore" ? (model ? model["isPresent"] : true) : true; + boxSize: 24; + isRedCheck: true + onClicked: { + var newValue = !model[styleData.role]; + nearbyUserModel.setProperty(model.userIndex, styleData.role, newValue); + nearbyUserModelData[model.userIndex][styleData.role] = newValue; // Defensive programming + Users[styleData.role](model.sessionId, newValue); + UserActivityLogger["palAction"](newValue ? styleData.role : "un-" + styleData.role, model.sessionId); + if (styleData.role === "ignore") { + nearbyUserModel.setProperty(model.userIndex, "personalMute", newValue); + nearbyUserModelData[model.userIndex]["personalMute"] = newValue; // Defensive programming + if (newValue) { + ignored[model.sessionId] = nearbyUserModelData[model.userIndex]; + } else { + delete ignored[model.sessionId]; + } + avgAudioVolume.glyph = avgAudioVolume.getGlyph(); + } + // http://doc.qt.io/qt-5/qtqml-syntax-propertybinding.html#creating-property-bindings-from-javascript + // I'm using an explicit binding here because clicking a checkbox breaks the implicit binding as set by + // "checked:" statement above. + checked = Qt.binding(function() { return (model[styleData.role])}); + } + } + + // This Button belongs in the columns that contain the stateless action buttons ("Silence" & "Ban" for now) + HifiControlsUit.Button { + id: actionButton; + color: 2; // Red + visible: isButton; + anchors.centerIn: parent; + width: 32; + height: 32; + onClicked: { + Users[styleData.role](model.sessionId); + UserActivityLogger["palAction"](styleData.role, model.sessionId); + if (styleData.role === "kick") { + nearbyUserModelData.splice(model.userIndex, 1); + nearbyUserModel.remove(model.userIndex); // after changing nearbyUserModelData, b/c ListModel can frob the data + } + } + // muted/error glyphs + HiFiGlyphs { + text: (styleData.role === "kick") ? hifi.glyphs.error : hifi.glyphs.muted; + // Size + size: parent.height*1.3; + // Anchors + anchors.fill: parent; + // Style + horizontalAlignment: Text.AlignHCenter; + color: enabled ? hifi.buttons.textColor[actionButton.color] + : hifi.buttons.disabledTextColor[actionButton.colorScheme]; + } + } + } + } + + // Separator between user and admin functions + Rectangle { + // Size + width: 2; + height: nearbyTable.height; + // Anchors + anchors.left: adminTab.left; + anchors.top: nearbyTable.top; + // Properties + visible: iAmAdmin; + color: hifi.colors.lightGrayText; + } + TextMetrics { + id: displayNameHeaderMetrics; + text: displayNameHeader.title; + // font: displayNameHeader.font // was this always undefined? giving error now... + } + // This Rectangle refers to the [?] popup button next to "NAMES" + Rectangle { + color: hifi.colors.tableBackgroundLight; + width: 20; + height: hifi.dimensions.tableHeaderHeight - 2; + anchors.left: nearbyTable.left; + anchors.top: nearbyTable.top; + anchors.topMargin: 1; + anchors.leftMargin: actionButtonWidth + nearbyNameCardWidth/2 + displayNameHeaderMetrics.width/2 + 6; + RalewayRegular { + id: helpText; + text: "[?]"; + size: hifi.fontSizes.tableHeading + 2; + font.capitalization: Font.AllUppercase; + color: hifi.colors.darkGray; + horizontalAlignment: Text.AlignHCenter; + verticalAlignment: Text.AlignVCenter; + anchors.fill: parent; + } + MouseArea { + anchors.fill: parent; + hoverEnabled: true; + onClicked: letterbox(hifi.glyphs.question, + "Display Names", + "Bold names in the list are avatar display names.
" + + "Purple borders around profile pictures are connections.
" + + "Green borders around profile pictures are friends.
" + + "Others can find you and see your username according to your availability settings.
" + + "If you can see someone's username, you can GoTo them by selecting them in the PAL, then clicking their name.
" + + "
If someone's display name isn't set, a unique session display name is assigned to them.
" + + "
Administrators of this domain can also see the username or machine ID associated with each avatar present."); + onEntered: helpText.color = hifi.colors.baseGrayHighlight; + onExited: helpText.color = hifi.colors.darkGray; + } + } + // This Rectangle refers to the [?] popup button next to "ADMIN" + Rectangle { + visible: iAmAdmin; + color: adminTab.color; + width: 20; + height: 28; + anchors.right: adminTab.right; + anchors.rightMargin: 12 + hifi.dimensions.scrollbarBackgroundWidth; + anchors.top: adminTab.top; + anchors.topMargin: 2; + RalewayRegular { + id: adminHelpText; + text: "[?]"; + size: hifi.fontSizes.tableHeading + 2; + font.capitalization: Font.AllUppercase; + color: hifi.colors.redHighlight; + horizontalAlignment: Text.AlignHCenter; + verticalAlignment: Text.AlignVCenter; + anchors.fill: parent; + } + MouseArea { + anchors.fill: parent; + hoverEnabled: true; + onClicked: letterbox(hifi.glyphs.question, + "Admin Actions", + "Silence mutes a user's microphone. Silenced users can unmute themselves by clicking "UNMUTE" on their toolbar.

" + + "Ban removes a user from this domain and prevents them from returning. Admins can un-ban users from the Sandbox Domain Settings page."); + onEntered: adminHelpText.color = "#94132e"; + onExited: adminHelpText.color = hifi.colors.redHighlight; + } + } + } // "Nearby" Tab + + + /***************************************** + CONNECTIONS TAB + *****************************************/ + Rectangle { + id: connectionsTab; + color: "white"; + // Anchors + anchors { + top: tabSelectorContainer.bottom; + topMargin: 12; + bottom: parent.bottom; + bottomMargin: 12; + horizontalCenter: parent.horizontalCenter; + } + width: parent.width - 12; + visible: activeTab == "connectionsTab"; + + AnimatedImage { + id: connectionsLoading; + source: "../../icons/profilePicLoading.gif" + width: 120; + height: width; + anchors.top: parent.top; + anchors.topMargin: 185; + anchors.horizontalCenter: parent.horizontalCenter; + visible: true; + onVisibleChanged: { + if (visible) { + connectionsTimeoutTimer.start(); + } else { + connectionsTimeoutTimer.stop(); + connectionsRefreshProblemText.visible = false; + } + } + } + + // "This is taking too long..." text + FiraSansSemiBold { + id: connectionsRefreshProblemText + // Properties + text: "This is taking longer than normal.\nIf you get stuck, try refreshing the Connections tab."; + // Anchors + anchors.top: connectionsLoading.bottom; + anchors.topMargin: 10; + anchors.left: parent.left; + anchors.bottom: parent.bottom; + width: parent.width; + // Text Size + size: 16; + // Text Positioning + verticalAlignment: Text.AlignTop; + horizontalAlignment: Text.AlignHCenter; + wrapMode: Text.WordWrap; + // Style + color: hifi.colors.darkGray; + } + + // This TableView refers to the Connections Table (on the "Connections" tab below the current user's NameCard) + HifiControlsUit.Table { + id: connectionsTable; + flickableItem.interactive: true; + visible: !connectionsLoading.visible; + // Anchors + anchors.fill: parent; + // Properties + centerHeaderText: true; + sortIndicatorVisible: true; + headerVisible: true; + sortIndicatorColumn: settings.connectionsSortIndicatorColumn; + sortIndicatorOrder: settings.connectionsSortIndicatorOrder; + onSortIndicatorColumnChanged: { + settings.connectionsSortIndicatorColumn = sortIndicatorColumn; + sortConnectionsModel(); + } + onSortIndicatorOrderChanged: { + settings.connectionsSortIndicatorOrder = sortIndicatorOrder; + sortConnectionsModel(); + } + + TableViewColumn { + id: connectionsUserNameHeader; + role: "userName"; + title: connectionsTable.rowCount + (connectionsTable.rowCount === 1 ? " NAME" : " NAMES"); + width: connectionsNameCardWidth; + movable: false; + resizable: false; + } + TableViewColumn { + role: "placeName"; + title: "LOCATION"; + width: locationColumnWidth; + movable: false; + resizable: false; + } + TableViewColumn { + role: "connection"; + title: "FRIEND"; + width: actionButtonWidth; + movable: false; + resizable: false; + } + + model: ListModel { + id: connectionsUserModel; + } + + // This Rectangle refers to each Row in the connectionsTable. + rowDelegate: Rectangle { + // Size + height: rowHeight + (styleData.selected ? 15 : 0); + color: connectionsRowColor(styleData.selected, styleData.alternate); + } + + // This Item refers to the contents of each Cell + itemDelegate: Item { + id: connectionsItemCell; + + // This NameCard refers to the cell that contains a connection's UserName + NameCard { + id: connectionsNameCard; + // Properties + visible: styleData.role === "userName"; + profileUrl: (model && model.profileUrl) || ""; + displayName: ""; + userName: model ? model.userName : ""; + placeName: model ? model.placeName : "" + connectionStatus : model ? model.connection : ""; + selected: styleData.selected; + // Size + width: connectionsNameCardWidth; + height: parent.height; + // Anchors + anchors.left: parent.left; + } + + // LOCATION data + FiraSansRegular { + id: connectionsLocationData + // Properties + visible: styleData.role === "placeName"; + text: (model && model.placeName) || ""; + elide: Text.ElideRight; + // Size + width: parent.width; + // you would think that this would work: + // anchors.verticalCenter: connectionsNameCard.avImage.verticalCenter + // but no! you cannot anchor to a non-sibling or parent. So I will + // align with the friends checkbox, where I did the manual alignment + anchors.verticalCenter: friendsCheckBox.verticalCenter + // Text Size + size: 16; + // Text Positioning + verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignHCenter + // Style + color: hifi.colors.blueAccent; + font.underline: true; + MouseArea { + anchors.fill: parent + hoverEnabled: enabled + enabled: connectionsNameCard.selected && pal.activeTab == "connectionsTab" + onClicked: { + AddressManager.goToUser(model.userName); + UserActivityLogger.palAction("go_to_user", model.userName); + } + onEntered: connectionsLocationData.color = hifi.colors.blueHighlight; + onExited: connectionsLocationData.color = hifi.colors.blueAccent; + } + } + + // "Friends" checkbox + HifiControlsUit.CheckBox { + id: friendsCheckBox; + visible: styleData.role === "connection" && model && model.userName !== myData.userName; + // you would think that this would work: + // anchors.verticalCenter: connectionsNameCard.avImage.verticalCenter + // but no! you cannot anchor to a non-sibling or parent. So: + x: parent.width/2 - boxSize/2; + y: connectionsNameCard.avImage.y + connectionsNameCard.avImage.height/2 - boxSize/2; + checked: model && (model.connection === "friend"); + boxSize: 24; + onClicked: { + var newValue = model.connection !== "friend"; + connectionsUserModel.setProperty(model.userIndex, styleData.role, newValue); + connectionsUserModelData[model.userIndex][styleData.role] = newValue; // Defensive programming + pal.sendToScript({method: newValue ? 'addFriend' : 'removeFriend', params: model.userName}); + + UserActivityLogger["palAction"](newValue ? styleData.role : "un-" + styleData.role, model.sessionId); + } + } + } + } + + // "Make a Connection" instructions + Rectangle { + id: connectionInstructions; + visible: connectionsTable.rowCount === 0 && !connectionsLoading.visible; + anchors.fill: connectionsTable; + anchors.topMargin: hifi.dimensions.tableHeaderHeight; + color: "white"; + + RalewayRegular { + id: makeAConnectionText; + // Properties + text: "Make a Connection"; + // Anchors + anchors.top: parent.top; + anchors.topMargin: 60; + anchors.left: parent.left; + anchors.right: parent.right; + // Text Size + size: 24; + // Text Positioning + verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignHCenter; + // Style + color: hifi.colors.darkGray; + } + + Image { + id: connectionImage; + source: "../../icons/connection.svg"; + width: 150; + height: 150; + mipmap: true; + // Anchors + anchors.top: makeAConnectionText.bottom; + anchors.topMargin: 15; + anchors.horizontalCenter: parent.horizontalCenter; + } + + FontLoader { id: ralewayRegular; source: "../../fonts/Raleway-Regular.ttf"; } + Text { + id: connectionHelpText; + // Anchors + anchors.top: connectionImage.bottom; + anchors.topMargin: 15; + anchors.left: parent.left + anchors.leftMargin: 40; + anchors.right: parent.right + anchors.rightMargin: 10; + // Text alignment + verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignHLeft + // Style + font.pixelSize: 18; + font.family: ralewayRegular.name + color: hifi.colors.darkGray + wrapMode: Text.WordWrap + textFormat: Text.StyledText; + // Text + text: HMD.isMounted ? + "When you meet someone you want to remember later, you can connect with a handshake:

" + + "1. Put your hand out onto their hand and squeeze your controller's grip button on its side.
" + + "2. Once the other person puts their hand onto yours, you'll see your connection form.
" + + "3. After about 3 seconds, you're connected!" + : + "When you meet someone you want to remember later, you can connect with a handshake:

" + + "1. Press and hold the 'x' key to extend your arm.
" + + "2. Once the other person puts their hand onto yours, you'll see your connection form.
" + + "3. After about 3 seconds, you're connected!"; + } + + } + } // "Connections" Tab + } // palTabContainer // This contains the current user's NameCard and will contain other information in the future Rectangle { - id: myInfo + id: myInfo; // Size - width: palContainer.width - height: myCardHeight + width: pal.width; + height: myCardHeight; // Style - color: pal.color + color: pal.color; // Anchors - anchors.top: palContainer.top - // Properties - radius: hifi.dimensions.borderRadius - // This NameCard refers to the current user's NameCard (the one above the table) + anchors.top: pal.top; + anchors.topMargin: 10; + anchors.left: pal.left; + // This NameCard refers to the current user's NameCard (the one above the nearbyTable) NameCard { - id: myCard + id: myCard; // Properties - displayName: myData.displayName - userName: myData.userName - audioLevel: myData.audioLevel - avgAudioLevel: myData.avgAudioLevel - isMyCard: true + profileUrl: myData.profileUrl; + displayName: myData.displayName; + userName: myData.userName; + audioLevel: myData.audioLevel; + avgAudioLevel: myData.avgAudioLevel; + isMyCard: true; + isPresent: true; // Size - width: minNameCardWidth - height: parent.height - // Anchors - anchors.left: parent.left - } - Row { - HifiControls.CheckBox { - id: filter - checked: settings.filtered - text: "in view" - boxSize: reload.height * 0.70 - onCheckedChanged: refreshWithFilter() - } - HifiControls.GlyphButton { - id: reload - glyph: hifi.glyphs.reload - width: reload.height - onClicked: refreshWithFilter() - } - spacing: 50 - anchors { - right: parent.right - top: parent.top - topMargin: 10 - } - } - } - // Rectangles used to cover up rounded edges on bottom of MyInfo Rectangle - Rectangle { - color: pal.color - width: palContainer.width - height: 10 - anchors.top: myInfo.bottom - anchors.left: parent.left - } - Rectangle { - color: pal.color - width: palContainer.width - height: 10 - anchors.bottom: table.top - anchors.left: parent.left - } - // Rectangle that houses "ADMIN" string - Rectangle { - id: adminTab - // Size - width: 2*actionButtonWidth + hifi.dimensions.scrollbarBackgroundWidth + 2 - height: 40 - // Anchors - anchors.bottom: myInfo.bottom - anchors.bottomMargin: -10 - anchors.right: myInfo.right - // Properties - visible: iAmAdmin - // Style - color: hifi.colors.tableRowLightEven - radius: hifi.dimensions.borderRadius - border.color: hifi.colors.lightGrayText - border.width: 2 - // "ADMIN" text - RalewaySemiBold { - id: adminTabText - text: "ADMIN" - // Text size - size: hifi.fontSizes.tableHeading + 2 + width: myCardWidth; + height: parent.height; // Anchors anchors.top: parent.top - anchors.topMargin: 8 - anchors.left: parent.left - anchors.right: parent.right - anchors.rightMargin: hifi.dimensions.scrollbarBackgroundWidth - // Style - font.capitalization: Font.AllUppercase - color: hifi.colors.redHighlight - // Alignment - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignTop - } - } - // This TableView refers to the table (below the current user's NameCard) - HifiControls.Table { - id: table - // Size - height: palContainer.height - myInfo.height - 4 - width: palContainer.width - 4 - // Anchors - anchors.left: parent.left - anchors.top: myInfo.bottom - // Properties - centerHeaderText: true - sortIndicatorVisible: true - headerVisible: true - sortIndicatorColumn: settings.sortIndicatorColumn - sortIndicatorOrder: settings.sortIndicatorOrder - onSortIndicatorColumnChanged: { - settings.sortIndicatorColumn = sortIndicatorColumn - sortModel() - } - onSortIndicatorOrderChanged: { - settings.sortIndicatorOrder = sortIndicatorOrder - sortModel() + anchors.left: parent.left; } + Item { + id: upperRightInfoContainer; + width: 200; + height: parent.height; + anchors.top: parent.top; + anchors.right: parent.right; - TableViewColumn { - role: "avgAudioLevel" - title: "LOUD" - width: actionButtonWidth - movable: false - resizable: false - } - - TableViewColumn { - id: displayNameHeader - role: "displayName" - title: table.rowCount + (table.rowCount === 1 ? " NAME" : " NAMES") - width: nameCardWidth - movable: false - resizable: false - } - TableViewColumn { - role: "ignore" - title: "IGNORE" - width: actionButtonWidth - movable: false - resizable: false - } - TableViewColumn { - visible: iAmAdmin - role: "mute" - title: "SILENCE" - width: actionButtonWidth - movable: false - resizable: false - } - TableViewColumn { - visible: iAmAdmin - role: "kick" - title: "BAN" - width: actionButtonWidth - movable: false - resizable: false - } - model: ListModel { - id: userModel - } - - // This Rectangle refers to each Row in the table. - rowDelegate: Rectangle { // The only way I know to specify a row height. - // Size - height: styleData.selected ? rowHeight : rowHeight - 15 - color: styleData.selected - ? hifi.colors.orangeHighlight - : styleData.alternate ? hifi.colors.tableRowLightEven : hifi.colors.tableRowLightOdd - } - - // This Item refers to the contents of each Cell - itemDelegate: Item { - id: itemCell - property bool isCheckBox: styleData.role === "personalMute" || styleData.role === "ignore" - property bool isButton: styleData.role === "mute" || styleData.role === "kick" - property bool isAvgAudio: styleData.role === "avgAudioLevel" - - // This NameCard refers to the cell that contains an avatar's - // DisplayName and UserName - NameCard { - id: nameCard - // Properties - displayName: styleData.value - userName: model ? model.userName : "" - audioLevel: model ? model.audioLevel : 0.0 - avgAudioLevel: model ? model.avgAudioLevel : 0.0 - visible: !isCheckBox && !isButton && !isAvgAudio - uuid: model ? model.sessionId : "" - selected: styleData.selected - isAdmin: model && model.admin - // Size - width: nameCardWidth - height: parent.height + RalewayRegular { + id: availabilityText; + text: "set availability"; + // Text size + size: hifi.fontSizes.tabularData; // Anchors - anchors.left: parent.left + anchors.left: parent.left; + // Style + color: hifi.colors.baseGrayHighlight; + // Alignment + horizontalAlignment: Text.AlignLeft; + verticalAlignment: Text.AlignTop; } - 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]; + Rectangle { + property var availabilityStrings: ["Everyone", "Friends and Connections", "Friends Only", "Appear Offline"]; + id: availabilityComboBox; + color: hifi.colors.textFieldLightBackground + // Anchors + anchors.top: availabilityText.bottom; + anchors.horizontalCenter: parent.horizontalCenter; + // Size + width: parent.width; + height: 40; + function determineAvailabilityIndex() { + return ['all', 'connections', 'friends', 'none'].indexOf(GlobalServices.findableBy); } - id: avgAudioVolume - visible: isAvgAudio - glyph: getGlyph() - width: 32 - size: height - anchors.verticalCenter: parent.verticalCenter - anchors.horizontalCenter: parent.horizontalCenter - onClicked: { - // cannot change mute status when ignoring - if (!model["ignore"]) { - var newValue = !model["personalMute"]; - userModel.setProperty(model.userIndex, "personalMute", newValue) - userModelData[model.userIndex]["personalMute"] = newValue // Defensive programming - Users["personalMute"](model.sessionId, newValue) - UserActivityLogger["palAction"](newValue ? "personalMute" : "un-personalMute", model.sessionId) - } - } - } - // This CheckBox belongs in the columns that contain the stateful action buttons ("Mute" & "Ignore" for now) - // KNOWN BUG with the Checkboxes: When clicking in the center of the sorting header, the checkbox - // will appear in the "hovered" state. Hovering over the checkbox will fix it. - // Clicking on the sides of the sorting header doesn't cause this problem. - // I'm guessing this is a QT bug and not anything I can fix. I spent too long trying to work around it... - // I'm just going to leave the minor visual bug in. - HifiControls.CheckBox { - id: actionCheckBox - visible: isCheckBox - anchors.centerIn: parent - checked: model ? model[styleData.role] : false - // If this is a "Personal Mute" checkbox, disable the checkbox if the "Ignore" checkbox is checked. - enabled: !(styleData.role === "personalMute" && (model ? model["ignore"] : true)) - boxSize: 24 - onClicked: { - var newValue = !model[styleData.role] - userModel.setProperty(model.userIndex, styleData.role, newValue) - userModelData[model.userIndex][styleData.role] = newValue // Defensive programming - Users[styleData.role](model.sessionId, newValue) - UserActivityLogger["palAction"](newValue ? styleData.role : "un-" + styleData.role, model.sessionId) - if (styleData.role === "ignore") { - userModel.setProperty(model.userIndex, "personalMute", newValue) - userModelData[model.userIndex]["personalMute"] = newValue // Defensive programming - if (newValue) { - ignored[model.sessionId] = userModelData[model.userIndex] - } else { - delete ignored[model.sessionId] + function determineAvailabilityString() { + return availabilityStrings[determineAvailabilityIndex()]; + } + RalewayRegular { + text: myData.userName === "Unknown user" ? "Login to Set" : availabilityComboBox.determineAvailabilityString(); + anchors.fill: parent; + anchors.leftMargin: 10; + horizontalAlignment: Text.AlignLeft; + size: 16; + } + MouseArea { + anchors.fill: parent; + enabled: myData.userName !== "Unknown user"; + hoverEnabled: true; + onClicked: { + popupComboDialog("Set your availability:", + availabilityComboBox.availabilityStrings, + ["Your username will be visible in everyone's 'Nearby' list.\nAnyone will be able to jump to your location from within the 'Nearby' list.", + "Your location will be visible in the 'Connections' list only for those with whom you are connected or friends.\nThey will be able to jump to your location if the domain allows.", + "Your location will be visible in the 'Connections' list only for those with whom you are friends.\nThey will be able to jump to your location if the domain allows.", + "Your location will not be visible in the 'Connections' list of any other users. Only domain admins will be able to see your username in the 'Nearby' list."], + ["all", "connections", "friends", "none"]); + } + onEntered: availabilityComboBox.color = hifi.colors.lightGrayText; + onExited: availabilityComboBox.color = hifi.colors.textFieldLightBackground; + } + } + } + } + + HifiControlsUit.Keyboard { + id: keyboard; + raised: currentlyEditingDisplayName && HMD.mounted; + numeric: parent.punctuationMode; + anchors { + bottom: parent.bottom; + left: parent.left; + right: parent.right; + } + } // Keyboard + + Item { + id: webViewContainer; + anchors.fill: parent; + + Rectangle { + id: navigationContainer; + visible: userInfoViewer.visible; + height: 60; + anchors { + top: parent.top; + left: parent.left; + right: parent.right; + } + color: hifi.colors.faintGray; + + Item { + id: backButton + anchors { + top: parent.top; + left: parent.left; + } + height: parent.height - addressBar.height; + width: parent.width/2; + + FiraSansSemiBold { + // Properties + text: "BACK"; + elide: Text.ElideRight; + // Anchors + anchors.fill: parent; + // Text Size + size: 16; + // Text Positioning + verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignHCenter; + // Style + color: backButtonMouseArea.containsMouse || !userInfoViewer.canGoBack ? hifi.colors.lightGray : hifi.colors.darkGray; + MouseArea { + id: backButtonMouseArea; + anchors.fill: parent + hoverEnabled: enabled + onClicked: { + if (userInfoViewer.canGoBack) { + userInfoViewer.goBack(); + } + } } - avgAudioVolume.glyph = avgAudioVolume.getGlyph() } - // http://doc.qt.io/qt-5/qtqml-syntax-propertybinding.html#creating-property-bindings-from-javascript - // I'm using an explicit binding here because clicking a checkbox breaks the implicit binding as set by - // "checked:" statement above. - checked = Qt.binding(function() { return (model[styleData.role])}) + } + + Item { + id: closeButtonContainer + anchors { + top: parent.top; + right: parent.right; + } + height: parent.height - addressBar.height; + width: parent.width/2; + + FiraSansSemiBold { + id: closeButton; + // Properties + text: "CLOSE"; + elide: Text.ElideRight; + // Anchors + anchors.fill: parent; + // Text Size + size: 16; + // Text Positioning + verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignHCenter; + // Style + color: hifi.colors.redHighlight; + MouseArea { + anchors.fill: parent + hoverEnabled: enabled + onClicked: userInfoViewer.visible = false; + onEntered: closeButton.color = hifi.colors.redAccent; + onExited: closeButton.color = hifi.colors.redHighlight; + } + } + } + + Item { + id: addressBar + anchors { + top: closeButtonContainer.bottom; + left: parent.left; + right: parent.right; + } + height: 30; + width: parent.width; + + FiraSansRegular { + // Properties + text: userInfoViewer.url; + elide: Text.ElideRight; + // Anchors + anchors.fill: parent; + anchors.leftMargin: 5; + // Text Size + size: 14; + // Text Positioning + verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignLeft; + // Style + color: hifi.colors.lightGray; + } } } - // This Button belongs in the columns that contain the stateless action buttons ("Silence" & "Ban" for now) - HifiControls.Button { - id: actionButton - color: 2 // Red - visible: isButton - anchors.centerIn: parent - width: 32 - height: 32 - onClicked: { - Users[styleData.role](model.sessionId) - UserActivityLogger["palAction"](styleData.role, model.sessionId) - if (styleData.role === "kick") { - // Just for now, while we cannot undo "Ban": - userModel.remove(model.userIndex) - delete userModelData[model.userIndex] // Defensive programming - sortModel() - } - } - // muted/error glyphs - HiFiGlyphs { - text: (styleData.role === "kick") ? hifi.glyphs.error : hifi.glyphs.muted - // Size - size: parent.height*1.3 - // Anchors - anchors.fill: parent - // Style - horizontalAlignment: Text.AlignHCenter - color: enabled ? hifi.buttons.textColor[actionButton.color] - : hifi.buttons.disabledTextColor[actionButton.colorScheme] + Rectangle { + id: webViewBackground; + color: "white"; + visible: userInfoViewer.visible; + anchors { + top: navigationContainer.bottom; + bottom: parent.bottom; + left: parent.left; + right: parent.right; } } - } - } - // Separator between user and admin functions - Rectangle { - // Size - width: 2 - height: table.height - // Anchors - anchors.left: adminTab.left - anchors.top: table.top - // Properties - visible: iAmAdmin - color: hifi.colors.lightGrayText - } - TextMetrics { - id: displayNameHeaderMetrics - text: displayNameHeader.title - // font: displayNameHeader.font // was this always undefined? giving error now... - } - // This Rectangle refers to the [?] popup button next to "NAMES" - Rectangle { - color: hifi.colors.tableBackgroundLight - width: 20 - height: hifi.dimensions.tableHeaderHeight - 2 - anchors.left: table.left - anchors.top: table.top - anchors.topMargin: 1 - anchors.leftMargin: actionButtonWidth + nameCardWidth/2 + displayNameHeaderMetrics.width/2 + 6 - RalewayRegular { - id: helpText - text: "[?]" - size: hifi.fontSizes.tableHeading + 2 - font.capitalization: Font.AllUppercase - color: hifi.colors.darkGray - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - anchors.fill: parent + HifiControls.WebView { + id: userInfoViewer; + anchors { + top: navigationContainer.bottom; + bottom: parent.bottom; + left: parent.left; + right: parent.right; + } + visible: false; + } } - MouseArea { - anchors.fill: parent - acceptedButtons: Qt.LeftButton - hoverEnabled: true - onClicked: letterbox(hifi.glyphs.question, - "Display Names", - "Bold names in the list are avatar display names.
" + - "If a display name isn't set, a unique session display name is assigned." + - "

Administrators of this domain can also see the username or machine ID associated with each avatar present.") - onEntered: helpText.color = hifi.colors.baseGrayHighlight - onExited: helpText.color = hifi.colors.darkGray - } - } - // This Rectangle refers to the [?] popup button next to "ADMIN" - Rectangle { - visible: iAmAdmin - color: adminTab.color - width: 20 - height: 28 - anchors.right: adminTab.right - anchors.rightMargin: 10 + hifi.dimensions.scrollbarBackgroundWidth - anchors.top: adminTab.top - anchors.topMargin: 2 - RalewayRegular { - id: adminHelpText - text: "[?]" - size: hifi.fontSizes.tableHeading + 2 - font.capitalization: Font.AllUppercase - color: hifi.colors.redHighlight - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - anchors.fill: parent - } - MouseArea { - anchors.fill: parent - acceptedButtons: Qt.LeftButton - hoverEnabled: true - onClicked: letterbox(hifi.glyphs.question, - "Admin Actions", - "Silence mutes a user's microphone. Silenced users can unmute themselves by clicking "UNMUTE" on their toolbar.

" + - "Ban removes a user from this domain and prevents them from returning. Admins can un-ban users from the Sandbox Domain Settings page.") - onEntered: adminHelpText.color = "#94132e" - onExited: adminHelpText.color = hifi.colors.redHighlight - } - } - HifiControls.Keyboard { - id: keyboard - raised: myCard.currentlyEditingDisplayName && HMD.active - numeric: parent.punctuationMode - anchors { - bottom: parent.bottom - left: parent.left - right: parent.right - } - } - } - - // Timer used when selecting table rows that aren't yet present in the model + // Timer used when selecting nearbyTable rows that aren't yet present in the model // (i.e. when selecting avatars using edit.js or sphere overlays) Timer { - property bool selected // Selected or deselected? - property int userIndex // The userIndex of the avatar we want to select - id: selectionTimer + property bool selected; // Selected or deselected? + property int userIndex; // The userIndex of the avatar we want to select + id: selectionTimer; onTriggered: { if (selected) { - table.selection.clear(); // for now, no multi-select - table.selection.select(userIndex); - table.positionViewAtRow(userIndex, ListView.Beginning); + nearbyTable.selection.clear(); // for now, no multi-select + nearbyTable.selection.select(userIndex); + nearbyTable.positionViewAtRow(userIndex, ListView.Beginning); } else { - table.selection.deselect(userIndex); + nearbyTable.selection.deselect(userIndex); } } } - function findSessionIndex(sessionId, optionalData) { // no findIndex in .qml - var data = optionalData || userModelData, length = data.length; + // Timer used when refreshing the Connections tab + Timer { + id: connectionsTimeoutTimer; + interval: 3000; // 3 seconds + onTriggered: { + connectionsRefreshProblemText.visible = true; + } + } + + function nearbyRowColor(selected, alternate) { + return selected ? hifi.colors.orangeHighlight : alternate ? hifi.colors.tableRowLightEven : hifi.colors.tableRowLightOdd; + } + function connectionsRowColor(selected, alternate) { + return selected ? hifi.colors.lightBlueHighlight : alternate ? hifi.colors.tableRowLightEven : hifi.colors.tableRowLightOdd; + } + function findNearbySessionIndex(sessionId, optionalData) { // no findIndex in .qml + var data = optionalData || nearbyUserModelData, length = data.length; for (var i = 0; i < length; i++) { if (data[i].sessionId === sessionId) { return i; @@ -504,10 +1210,10 @@ Rectangle { } function fromScript(message) { switch (message.method) { - case 'users': + case 'nearbyUsers': var data = message.params; var index = -1; - index = findSessionIndex('', data); + index = findNearbySessionIndex('', data); if (index !== -1) { iAmAdmin = Users.canKick; myData = data[index]; @@ -515,68 +1221,78 @@ Rectangle { } else { console.log("This user's data was not found in the user list. PAL will not function properly."); } - userModelData = data; + nearbyUserModelData = data; for (var ignoredID in ignored) { - index = findSessionIndex(ignoredID); + index = findNearbySessionIndex(ignoredID); if (index === -1) { // Add back any missing ignored to the PAL, because they sometimes take a moment to show up. - userModelData.push(ignored[ignoredID]); + nearbyUserModelData.push(ignored[ignoredID]); } else { // Already appears in PAL; update properties of existing element in model data - userModelData[index] = ignored[ignoredID]; + nearbyUserModelData[index] = ignored[ignoredID]; } } sortModel(); + reloadNearby.color = 0; + break; + case 'connections': + var data = message.params; + console.log('Got connection data: ', JSON.stringify(data)); + connectionsUserModelData = data; + sortConnectionsModel(); + connectionsLoading.visible = false; + connectionsRefreshProblemText.visible = false; break; case 'select': var sessionIds = message.params[0]; var selected = message.params[1]; var alreadyRefreshed = message.params[2]; - var userIndex = findSessionIndex(sessionIds[0]); + var userIndex = findNearbySessionIndex(sessionIds[0]); if (sessionIds.length > 1) { letterbox("", "", 'Only one user can be selected at a time.'); } else if (userIndex < 0) { // If we've already refreshed the PAL and the avatar still isn't present in the model... if (alreadyRefreshed === true) { - letterbox('', '', 'The last editor of this object is either you or not among this list of users.'); + letterbox('', '', 'The user you attempted to select is no longer available.'); } else { pal.sendToScript({method: 'refresh', params: {selected: message.params}}); } } else { // If we've already refreshed the PAL and found the avatar in the model if (alreadyRefreshed === true) { - // Wait a little bit before trying to actually select the avatar in the table + // Wait a little bit before trying to actually select the avatar in the nearbyTable selectionTimer.interval = 250; } else { // If we've found the avatar in the model and didn't need to refresh, - // select the avatar in the table immediately + // select the avatar in the nearbyTable immediately selectionTimer.interval = 0; } selectionTimer.selected = selected; selectionTimer.userIndex = userIndex; selectionTimer.start(); } + // in any case make sure we are in the nearby tab + activeTab="nearbyTab"; break; // Received an "updateUsername()" request from the JS case 'updateUsername': - // The User ID (UUID) is the first parameter in the message. - var userId = message.params[0]; - // The text that goes in the userName field is the second parameter in the message. - var userName = message.params[1]; - var admin = message.params[2]; - // If the userId is empty, we're updating "myData". - if (!userId) { - myData.userName = userName; - myCard.userName = userName; // Defensive programming - } else { - // Get the index in userModel and userModelData associated with the passed UUID - var userIndex = findSessionIndex(userId); - if (userIndex != -1) { - // Set the userName appropriately - userModel.setProperty(userIndex, "userName", userName); - userModelData[userIndex].userName = userName; // Defensive programming - // Set the admin status appropriately - userModel.setProperty(userIndex, "admin", admin); - userModelData[userIndex].admin = admin; // Defensive programming + // Get the connection status + var connectionStatus = message.params.connection; + // If the connection status isn't "self"... + if (connectionStatus !== "self") { + // Get the index in nearbyUserModel and nearbyUserModelData associated with the passed UUID + var userIndex = findNearbySessionIndex(message.params.sessionId); + if (userIndex !== -1) { + ['userName', 'admin', 'connection', 'profileUrl', 'placeName'].forEach(function (name) { + var value = message.params[name]; + if (value === undefined) { + return; + } + nearbyUserModel.setProperty(userIndex, name, value); + nearbyUserModelData[userIndex][name] = value; // for refill after sort + }); } + } else if (message.params.profileUrl) { + myData.profileUrl = message.params.profileUrl; + myCard.profileUrl = message.params.profileUrl; } break; case 'updateAudioLevel': @@ -590,12 +1306,12 @@ Rectangle { myData.avgAudioLevel = avgAudioLevel; myCard.avgAudioLevel = avgAudioLevel; } else { - var userIndex = findSessionIndex(userId); + var userIndex = findNearbySessionIndex(userId); if (userIndex != -1) { - userModel.setProperty(userIndex, "audioLevel", audioLevel); - userModelData[userIndex].audioLevel = audioLevel; // Defensive programming - userModel.setProperty(userIndex, "avgAudioLevel", avgAudioLevel); - userModelData[userIndex].avgAudioLevel = avgAudioLevel; + nearbyUserModel.setProperty(userIndex, "audioLevel", audioLevel); + nearbyUserModelData[userIndex].audioLevel = audioLevel; // Defensive programming + nearbyUserModel.setProperty(userIndex, "avgAudioLevel", avgAudioLevel); + nearbyUserModelData[userIndex].avgAudioLevel = avgAudioLevel; } } } @@ -607,15 +1323,35 @@ Rectangle { var sessionID = message.params[0]; delete ignored[sessionID]; break; + case 'palIsStale': + var sessionID = message.params[0]; + var reason = message.params[1]; + var userIndex = findNearbySessionIndex(sessionID); + if (userIndex != -1) { + if (!nearbyUserModelData[userIndex].ignore) { + if (reason !== 'avatarAdded') { + nearbyUserModel.setProperty(userIndex, "isPresent", false); + nearbyUserModelData[userIndex].isPresent = false; + nearbyTable.selection.deselect(userIndex); + } + reloadNearby.color = 2; + } + } else { + reloadNearby.color = 2; + } + break; default: console.log('Unrecognized message:', JSON.stringify(message)); } } function sortModel() { - var sortProperty = table.getColumn(table.sortIndicatorColumn).role; - var before = (table.sortIndicatorOrder === Qt.AscendingOrder) ? -1 : 1; + var column = nearbyTable.getColumn(nearbyTable.sortIndicatorColumn); + var sortProperty = column ? column.role : "displayName"; + var before = (nearbyTable.sortIndicatorOrder === Qt.AscendingOrder) ? -1 : 1; var after = -1 * before; - userModelData.sort(function (a, b) { + // get selection(s) before sorting + var selectedIDs = getSelectedNearbySessionIDs(); + nearbyUserModelData.sort(function (a, b) { var aValue = a[sortProperty].toString().toLowerCase(), bValue = b[sortProperty].toString().toLowerCase(); switch (true) { case (aValue < bValue): return before; @@ -623,31 +1359,77 @@ Rectangle { default: return 0; } }); - table.selection.clear(); + nearbyTable.selection.clear(); - userModel.clear(); + nearbyUserModel.clear(); var userIndex = 0; - userModelData.forEach(function (datum) { + var newSelectedIndexes = []; + nearbyUserModelData.forEach(function (datum) { function init(property) { if (datum[property] === undefined) { - datum[property] = false; + // These properties must have values of type 'string'. + if (property === 'userName' || property === 'profileUrl' || property === 'placeName' || property === 'connection') { + datum[property] = ""; + // All other properties must have values of type 'bool'. + } else { + datum[property] = false; + } } } - ['personalMute', 'ignore', 'mute', 'kick'].forEach(init); + ['personalMute', 'ignore', 'mute', 'kick', 'admin', 'userName', 'profileUrl', 'placeName', 'connection'].forEach(init); datum.userIndex = userIndex++; - userModel.append(datum); + nearbyUserModel.append(datum); + if (selectedIDs.indexOf(datum.sessionId) != -1) { + newSelectedIndexes.push(datum.userIndex); + } }); + if (newSelectedIndexes.length > 0) { + nearbyTable.selection.select(newSelectedIndexes); + nearbyTable.positionViewAtRow(newSelectedIndexes[0], ListView.Beginning); + } + } + function sortConnectionsModel() { + var column = connectionsTable.getColumn(connectionsTable.sortIndicatorColumn); + var sortProperty = column ? column.role : "userName"; + var before = (connectionsTable.sortIndicatorOrder === Qt.AscendingOrder) ? -1 : 1; + var after = -1 * before; + // get selection(s) before sorting + var selectedIDs = getSelectedConnectionsUserNames(); + connectionsUserModelData.sort(function (a, b) { + var aValue = a[sortProperty].toString().toLowerCase(), bValue = b[sortProperty].toString().toLowerCase(); + switch (true) { + case (aValue < bValue): return before; + case (aValue > bValue): return after; + default: return 0; + } + }); + connectionsTable.selection.clear(); + + connectionsUserModel.clear(); + var userIndex = 0; + var newSelectedIndexes = []; + connectionsUserModelData.forEach(function (datum) { + datum.userIndex = userIndex++; + connectionsUserModel.append(datum); + if (selectedIDs.indexOf(datum.sessionId) != -1) { + newSelectedIndexes.push(datum.userIndex); + } + }); + if (newSelectedIndexes.length > 0) { + connectionsTable.selection.select(newSelectedIndexes); + connectionsTable.positionViewAtRow(newSelectedIndexes[0], ListView.Beginning); + } } signal sendToScript(var message); function noticeSelection() { var userIds = []; - table.selection.forEach(function (userIndex) { - userIds.push(userModelData[userIndex].sessionId); + nearbyTable.selection.forEach(function (userIndex) { + userIds.push(nearbyUserModelData[userIndex].sessionId); }); pal.sendToScript({method: 'selected', params: userIds}); } Connections { - target: table.selection - onSelectionChanged: pal.noticeSelection() + target: nearbyTable.selection; + onSelectionChanged: pal.noticeSelection(); } } diff --git a/interface/resources/qml/hifi/TabletTextButton.qml b/interface/resources/qml/hifi/TabletTextButton.qml new file mode 100644 index 0000000000..12e53eb217 --- /dev/null +++ b/interface/resources/qml/hifi/TabletTextButton.qml @@ -0,0 +1,58 @@ +// +// TabletTextButton.qml +// +// Created by Dante Ruiz on 2017/3/23 +// Copyright 2015 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +import Hifi 1.0 +import QtQuick 2.4 +import "../styles-uit" + +Rectangle { + property alias text: label.text + property alias pixelSize: label.font.pixelSize; + property bool selected: false + property int spacing: 2 + property var action: function () {} + property string highlightColor: hifi.colors.blueHighlight; + width: label.width + 64 + height: 32 + color: hifi.colors.white + HifiConstants { id: hifi } + RalewaySemiBold { + id: label; + color: hifi.colors.blueHighlight; + font.pixelSize: 15; + anchors { + horizontalCenter: parent.horizontalCenter; + verticalCenter: parent.verticalCenter; + } + } + + + Rectangle { + id: indicator + width: parent.width + height: 3 + anchors { + left: parent.left + right: parent.right + bottom: parent.bottom + } + color: hifi.colors.blueHighlight + visible: parent.selected + } + + MouseArea { + id: clickArea; + anchors.fill: parent; + acceptedButtons: Qt.LeftButton; + onClicked: action(parent); + hoverEnabled: true; + } +} + diff --git a/interface/resources/qml/hifi/components/AudioCheckbox.qml b/interface/resources/qml/hifi/components/AudioCheckbox.qml new file mode 100644 index 0000000000..a8e0441e0a --- /dev/null +++ b/interface/resources/qml/hifi/components/AudioCheckbox.qml @@ -0,0 +1,29 @@ +import QtQuick 2.5 +import QtQuick.Controls 1.4 + +import "../../styles-uit" +import "../../controls-uit" as HifiControls + +Row { + id: row + spacing: 16 + property alias checkbox: cb + property alias text: txt + signal checkBoxClicked(bool checked) + + HifiControls.CheckBox { + id: cb + boxSize: 20 + colorScheme: hifi.colorSchemes.dark + anchors.verticalCenter: parent.verticalCenter + onClicked: checkBoxClicked(cb.checked) + } + RalewayBold { + id: txt + wrapMode: Text.WordWrap + width: parent.width - cb.boxSize - 30 + anchors.verticalCenter: parent.verticalCenter + size: 16 + color: "white" + } +} diff --git a/interface/resources/qml/hifi/dialogs/AttachmentsDialog.qml b/interface/resources/qml/hifi/dialogs/AttachmentsDialog.qml index 27d225b58e..12e8de3bfc 100644 --- a/interface/resources/qml/hifi/dialogs/AttachmentsDialog.qml +++ b/interface/resources/qml/hifi/dialogs/AttachmentsDialog.qml @@ -1,13 +1,20 @@ +// +// AttachmentsDialog.qml +// +// Created by David Rowe on 9 Mar 2017. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or https://www.apache.org/licenses/LICENSE-2.0.html +// + import QtQuick 2.5 import QtQuick.Controls 1.4 -import QtQuick.Dialogs 1.2 as OriginalDialogs import Qt.labs.settings 1.0 -import QtQuick.Controls.Styles 1.4 import "../../styles-uit" -import "../../controls-uit" as HifiControls import "../../windows" -import "attachments" +import "content" ScrollingWindow { id: root @@ -21,9 +28,6 @@ ScrollingWindow { HifiConstants { id: hifi } - readonly property var originalAttachments: MyAvatar.getAttachmentsVariant(); - property var attachments: []; - property var settings: Settings { category: "AttachmentsDialog" property alias x: root.x @@ -32,198 +36,9 @@ ScrollingWindow { property alias height: root.height } - Component.onCompleted: { - for (var i = 0; i < originalAttachments.length; ++i) { - var attachment = originalAttachments[i]; - root.attachments.push(attachment); - listView.model.append({}); - } + function closeDialog() { + root.destroy(); } - Column { - width: pane.contentWidth - - Rectangle { - width: parent.width - height: root.height - (keyboardEnabled && keyboardRaised ? 200 : 0) - radius: 4 - color: hifi.colors.baseGray - - Rectangle { - id: attachmentsBackground - anchors { - left: parent.left; right: parent.right; top: parent.top; bottom: newAttachmentButton.top; - margins: hifi.dimensions.contentMargin.x - bottomMargin: hifi.dimensions.contentSpacing.y - } - color: hifi.colors.baseGrayShadow - radius: 4 - - ScrollView { - id: scrollView - anchors.fill: parent - anchors.margins: 4 - - style: ScrollViewStyle { - - padding { - top: 0 - right: 0 - bottom: 0 - } - - decrementControl: Item { - visible: false - } - incrementControl: Item { - visible: false - } - scrollBarBackground: Rectangle{ - implicitWidth: 14 - color: hifi.colors.baseGray - radius: 4 - Rectangle { - // Make top left corner of scrollbar appear square - width: 8 - height: 4 - color: hifi.colors.baseGray - anchors.top: parent.top - anchors.horizontalCenter: parent.left - } - - } - handle: - Rectangle { - implicitWidth: 8 - anchors { - left: parent.left - leftMargin: 3 - top: parent.top - topMargin: 3 - bottom: parent.bottom - bottomMargin: 4 - } - radius: 4 - color: hifi.colors.lightGrayText - } - } - - ListView { - id: listView - model: ListModel {} - delegate: Item { - id: attachmentDelegate - implicitHeight: attachmentView.height + 8; - implicitWidth: attachmentView.width - Attachment { - id: attachmentView - width: scrollView.width - attachment: root.attachments[index] - onDeleteAttachment: { - attachments.splice(index, 1); - listView.model.remove(index, 1); - } - onUpdateAttachment: MyAvatar.setAttachmentsVariant(attachments); - } - } - onCountChanged: MyAvatar.setAttachmentsVariant(attachments); - } - - function scrollBy(delta) { - flickableItem.contentY += delta; - } - } - } - - HifiControls.Button { - id: newAttachmentButton - anchors { - left: parent.left - right: parent.right - bottom: buttonRow.top - margins: hifi.dimensions.contentMargin.x; - topMargin: hifi.dimensions.contentSpacing.y - bottomMargin: hifi.dimensions.contentSpacing.y - } - text: "New Attachment" - color: hifi.buttons.black - colorScheme: hifi.colorSchemes.dark - onClicked: { - var template = { - modelUrl: "", - translation: { x: 0, y: 0, z: 0 }, - rotation: { x: 0, y: 0, z: 0 }, - scale: 1, - jointName: MyAvatar.jointNames[0], - soft: false - }; - attachments.push(template); - listView.model.append({}); - MyAvatar.setAttachmentsVariant(attachments); - } - } - - Row { - id: buttonRow - spacing: 8 - anchors { - right: parent.right - bottom: parent.bottom - margins: hifi.dimensions.contentMargin.x - topMargin: hifi.dimensions.contentSpacing.y - bottomMargin: hifi.dimensions.contentSpacing.y - } - HifiControls.Button { - action: okAction - color: hifi.buttons.black - colorScheme: hifi.colorSchemes.dark - } - HifiControls.Button { - action: cancelAction - color: hifi.buttons.black - colorScheme: hifi.colorSchemes.dark - } - } - - Action { - id: cancelAction - text: "Cancel" - onTriggered: { - MyAvatar.setAttachmentsVariant(originalAttachments); - root.destroy() - } - } - - Action { - id: okAction - text: "OK" - onTriggered: { - for (var i = 0; i < attachments.length; ++i) { - console.log("Attachment " + i + ": " + attachments[i]); - } - - MyAvatar.setAttachmentsVariant(attachments); - root.destroy() - } - } - } - } - - onKeyboardRaisedChanged: { - if (keyboardEnabled && keyboardRaised) { - // Scroll to item with focus if necessary. - var footerHeight = newAttachmentButton.height + buttonRow.height + 3 * hifi.dimensions.contentSpacing.y; - var delta = activator.mouseY - - (activator.height + activator.y - 200 - footerHeight - hifi.dimensions.controlLineHeight); - - if (delta > 0) { - scrollView.scrollBy(delta); - } else { - // HACK: Work around for case where are 100% scrolled; stops window from erroneously scrolling to 100% when show keyboard. - scrollView.scrollBy(-1); - scrollView.scrollBy(1); - } - } - } + AttachmentsContent { } } - diff --git a/interface/resources/qml/hifi/dialogs/GeneralPreferencesDialog.qml b/interface/resources/qml/hifi/dialogs/GeneralPreferencesDialog.qml index 9be1c30e55..44cae95696 100644 --- a/interface/resources/qml/hifi/dialogs/GeneralPreferencesDialog.qml +++ b/interface/resources/qml/hifi/dialogs/GeneralPreferencesDialog.qml @@ -1,5 +1,5 @@ // -// PreferencesDialog.qml +// GeneralPreferencesDialog.qml // // Created by Bradley Austin Davis on 24 Jan 2016 // Copyright 2015 High Fidelity, Inc. diff --git a/interface/resources/qml/hifi/dialogs/ModelBrowserDialog.qml b/interface/resources/qml/hifi/dialogs/ModelBrowserDialog.qml index 7a63c0604c..c427052904 100644 --- a/interface/resources/qml/hifi/dialogs/ModelBrowserDialog.qml +++ b/interface/resources/qml/hifi/dialogs/ModelBrowserDialog.qml @@ -1,92 +1,36 @@ +// +// ModelBrowserDialog.qml +// +// Created by David Rowe on 11 Mar 2017. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or https://www.apache.org/licenses/LICENSE-2.0.html +// + import QtQuick 2.5 import QtQuick.Controls 1.4 -import QtQuick.XmlListModel 2.0 -import QtQuick.Controls.Styles 1.4 import "../../windows" -import "../../js/Utils.js" as Utils -import "../models" - -import "../../styles-uit" -import "../../controls-uit" as HifiControls -import "../../windows" +import "content" ScrollingWindow { id: root + objectName: "ModelBrowserDialog" + title: "Attachment Model" resizable: true width: 600 height: 480 closable: false - property var result; + //HifiConstants { id: hifi } - signal selected(var modelUrl); - signal canceled(); + property var result - HifiConstants {id: hifi} + signal selected(var modelUrl) + signal canceled() - Column { - width: pane.contentWidth - - Rectangle { - width: parent.width - height: root.height - (keyboardEnabled && keyboardRaised ? 200 : 0) - radius: 4 - color: hifi.colors.baseGray - - HifiControls.TextField { - id: filterEdit - anchors { left: parent.left; right: parent.right; top: parent.top ; margins: 8} - placeholderText: "filter" - onTextChanged: tableView.model.filter = text - colorScheme: hifi.colorSchemes.dark - } - - HifiControls.AttachmentsTable { - id: tableView - anchors { left: parent.left; right: parent.right; top: filterEdit.bottom; bottom: buttonRow.top; margins: 8; } - colorScheme: hifi.colorSchemes.dark - onCurrentRowChanged: { - if (currentRow == -1) { - root.result = null; - return; - } - result = model.baseUrl + "/" + model.get(tableView.currentRow).key; - } - } - - Row { - id: buttonRow - spacing: 8 - anchors { right: parent.right; rightMargin: 8; bottom: parent.bottom; bottomMargin: 8; } - HifiControls.Button { action: acceptAction ; color: hifi.buttons.black; colorScheme: hifi.colorSchemes.dark } - HifiControls.Button { action: cancelAction ; color: hifi.buttons.black; colorScheme: hifi.colorSchemes.dark } - } - - Action { - id: acceptAction - text: qsTr("OK") - enabled: root.result ? true : false - shortcut: Qt.Key_Return - onTriggered: { - root.selected(root.result); - root.destroy(); - } - } - - Action { - id: cancelAction - text: qsTr("Cancel") - shortcut: Qt.Key_Escape - onTriggered: { - root.canceled(); - root.destroy(); - } - } - } + ModelBrowserContent { + id: modelBrowserContent } } - - - - diff --git a/interface/resources/qml/hifi/dialogs/TabletAssetServer.qml b/interface/resources/qml/hifi/dialogs/TabletAssetServer.qml new file mode 100644 index 0000000000..85f8a2f59e --- /dev/null +++ b/interface/resources/qml/hifi/dialogs/TabletAssetServer.qml @@ -0,0 +1,616 @@ +// +// TabletAssetServer.qml +// +// Created by Vlad Stelmahovsky on 3/3/17 +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +import QtQuick 2.5 +import QtQuick.Controls 1.4 +import QtQuick.Dialogs 1.2 as OriginalDialogs +import Qt.labs.settings 1.0 + +import "../../styles-uit" +import "../../controls-uit" as HifiControls +import "../../windows" + +Rectangle { + id: root + objectName: "AssetServer" + + property string title: "Asset Browser" + property bool keyboardRaised: false + + property var eventBridge; + signal sendToScript(var message); + property bool isHMD: false + + color: hifi.colors.baseGray + + property int colorScheme: hifi.colorSchemes.dark + + HifiConstants { id: hifi } + + property var scripts: ScriptDiscoveryService; + property var assetProxyModel: Assets.proxyModel; + property var assetMappingsModel: Assets.mappingModel; + property var currentDirectory; + + Settings { + category: "Overlay.AssetServer" + property alias directory: root.currentDirectory + } + + Component.onCompleted: { + isHMD = HMD.active; + ApplicationInterface.uploadRequest.connect(uploadClicked); + assetMappingsModel.errorGettingMappings.connect(handleGetMappingsError); + reload(); + } + + function doDeleteFile(path) { + console.log("Deleting " + path); + + Assets.deleteMappings(path, function(err) { + if (err) { + console.log("Asset browser - error deleting path: ", path, err); + + box = errorMessageBox("There was an error deleting:\n" + path + "\n" + err); + box.selected.connect(reload); + } else { + console.log("Asset browser - finished deleting path: ", path); + reload(); + } + }); + + } + + function doRenameFile(oldPath, newPath) { + + if (newPath[0] !== "/") { + newPath = "/" + newPath; + } + + if (oldPath[oldPath.length - 1] === '/' && newPath[newPath.length - 1] != '/') { + // this is a folder rename but the user neglected to add a trailing slash when providing a new path + newPath = newPath + "/"; + } + + if (Assets.isKnownFolder(newPath)) { + box = errorMessageBox("Cannot overwrite existing directory."); + box.selected.connect(reload); + } + + console.log("Asset browser - renaming " + oldPath + " to " + newPath); + + Assets.renameMapping(oldPath, newPath, function(err) { + if (err) { + console.log("Asset browser - error renaming: ", oldPath, "=>", newPath, " - error ", err); + box = errorMessageBox("There was an error renaming:\n" + oldPath + " to " + newPath + "\n" + err); + box.selected.connect(reload); + } else { + console.log("Asset browser - finished rename: ", oldPath, "=>", newPath); + } + + reload(); + }); + } + + function fileExists(path) { + return Assets.isKnownMapping(path); + } + + function askForOverwrite(path, callback) { + var object = tabletRoot.messageBox({ + icon: hifi.icons.question, + buttons: OriginalDialogs.StandardButton.Yes | OriginalDialogs.StandardButton.No, + defaultButton: OriginalDialogs.StandardButton.No, + title: "Overwrite File", + text: path + "\n" + "This file already exists. Do you want to overwrite it?" + }); + object.selected.connect(function(button) { + if (button === OriginalDialogs.StandardButton.Yes) { + callback(); + } + }); + } + + function canAddToWorld(path) { + var supportedExtensions = [/\.fbx\b/i, /\.obj\b/i]; + + return supportedExtensions.reduce(function(total, current) { + return total | new RegExp(current).test(path); + }, false); + } + + function clear() { + Assets.mappingModel.clear(); + } + + function reload() { + Assets.mappingModel.refresh(); + treeView.selection.clear(); + } + + function handleGetMappingsError(errorString) { + errorMessageBox( + "There was a problem retreiving the list of assets from your Asset Server.\n" + + errorString + ); + } + + function addToWorld() { + var defaultURL = assetProxyModel.data(treeView.selection.currentIndex, 0x103); + + if (!defaultURL || !canAddToWorld(defaultURL)) { + return; + } + + var SHAPE_TYPE_NONE = 0; + var SHAPE_TYPE_SIMPLE_HULL = 1; + var SHAPE_TYPE_SIMPLE_COMPOUND = 2; + var SHAPE_TYPE_STATIC_MESH = 3; + + var SHAPE_TYPES = []; + SHAPE_TYPES[SHAPE_TYPE_NONE] = "No Collision"; + SHAPE_TYPES[SHAPE_TYPE_SIMPLE_HULL] = "Basic - Whole model"; + SHAPE_TYPES[SHAPE_TYPE_SIMPLE_COMPOUND] = "Good - Sub-meshes"; + SHAPE_TYPES[SHAPE_TYPE_STATIC_MESH] = "Exact - All polygons"; + + var SHAPE_TYPE_DEFAULT = SHAPE_TYPE_STATIC_MESH; + var DYNAMIC_DEFAULT = false; + var prompt = tabletRoot.customInputDialog({ + textInput: { + label: "Model URL", + text: defaultURL + }, + comboBox: { + label: "Automatic Collisions", + index: SHAPE_TYPE_DEFAULT, + items: SHAPE_TYPES + }, + checkBox: { + label: "Dynamic", + checked: DYNAMIC_DEFAULT, + disableForItems: [ + SHAPE_TYPE_STATIC_MESH + ], + checkStateOnDisable: false, + warningOnDisable: "Models with 'Exact' automatic collisions cannot be dynamic" + } + }); + + prompt.selected.connect(function (jsonResult) { + if (jsonResult) { + var result = JSON.parse(jsonResult); + var url = result.textInput.trim(); + var shapeType; + switch (result.comboBox) { + case SHAPE_TYPE_SIMPLE_HULL: + shapeType = "simple-hull"; + break; + case SHAPE_TYPE_SIMPLE_COMPOUND: + shapeType = "simple-compound"; + break; + case SHAPE_TYPE_STATIC_MESH: + shapeType = "static-mesh"; + break; + default: + shapeType = "none"; + } + + var dynamic = result.checkBox !== null ? result.checkBox : DYNAMIC_DEFAULT; + if (shapeType === "static-mesh" && dynamic) { + // The prompt should prevent this case + print("Error: model cannot be both static mesh and dynamic. This should never happen."); + } else if (url) { + var name = assetProxyModel.data(treeView.selection.currentIndex); + var addPosition = Vec3.sum(MyAvatar.position, Vec3.multiply(2, Quat.getFront(MyAvatar.orientation))); + var gravity; + if (dynamic) { + // Create a vector <0, -10, 0>. { x: 0, y: -10, z: 0 } won't work because Qt is dumb and this is a + // different scripting engine from QTScript. + gravity = Vec3.multiply(Vec3.fromPolar(Math.PI / 2, 0), 10); + } else { + gravity = Vec3.multiply(Vec3.fromPolar(Math.PI / 2, 0), 0); + } + + print("Asset browser - adding asset " + url + " (" + name + ") to world."); + + // Entities.addEntity doesn't work from QML, so we use this. + Entities.addModelEntity(name, url, shapeType, dynamic, addPosition, gravity); + } + } + }); + } + + function copyURLToClipboard(index) { + if (!index) { + index = treeView.selection.currentIndex; + } + + var url = assetProxyModel.data(treeView.selection.currentIndex, 0x103); + if (!url) { + return; + } + Window.copyToClipboard(url); + } + + function renameEl(index, data) { + if (!index) { + return false; + } + + var path = assetProxyModel.data(index, 0x100); + if (!path) { + return false; + } + + var destinationPath = path.split('/'); + destinationPath[destinationPath.length - (path[path.length - 1] === '/' ? 2 : 1)] = data; + destinationPath = destinationPath.join('/').trim(); + + if (path === destinationPath) { + return; + } + if (!fileExists(destinationPath)) { + doRenameFile(path, destinationPath); + } + } + function renameFile(index) { + if (!index) { + index = treeView.selection.currentIndex; + } + + var path = assetProxyModel.data(index, 0x100); + if (!path) { + return; + } + + var object = tabletRoot.inputDialog({ + label: "Enter new path:", + current: path, + placeholderText: "Enter path here" + }); + object.selected.connect(function(destinationPath) { + destinationPath = destinationPath.trim(); + + if (path === destinationPath) { + return; + } + if (fileExists(destinationPath)) { + askForOverwrite(destinationPath, function() { + doRenameFile(path, destinationPath); + }); + } else { + doRenameFile(path, destinationPath); + } + }); + } + function deleteFile(index) { + if (!index) { + index = treeView.selection.currentIndex; + } + var path = assetProxyModel.data(index, 0x100); + if (!path) { + return; + } + + var isFolder = assetProxyModel.data(treeView.selection.currentIndex, 0x101); + var typeString = isFolder ? 'folder' : 'file'; + + var object = tabletRoot.messageBox({ + icon: hifi.icons.question, + buttons: OriginalDialogs.StandardButton.Yes + OriginalDialogs.StandardButton.No, + defaultButton: OriginalDialogs.StandardButton.Yes, + title: "Delete", + text: "You are about to delete the following " + typeString + ":\n" + path + "\nDo you want to continue?" + }); + object.selected.connect(function(button) { + if (button === OriginalDialogs.StandardButton.Yes) { + doDeleteFile(path); + } + }); + } + + Timer { + id: doUploadTimer + property var url + property bool isConnected: false + interval: 5 + repeat: false + running: false + } + + property bool uploadOpen: false; + Timer { + id: timer + } + function uploadClicked(fileUrl) { + if (uploadOpen) { + return; + } + uploadOpen = true; + + function doUpload(url, dropping) { + var fileUrl = fileDialogHelper.urlToPath(url); + + var path = assetProxyModel.data(treeView.selection.currentIndex, 0x100); + var directory = path ? path.slice(0, path.lastIndexOf('/') + 1) : "/"; + var filename = fileUrl.slice(fileUrl.lastIndexOf('/') + 1); + + Assets.uploadFile(fileUrl, directory + filename, + function() { + // Upload started + uploadSpinner.visible = true; + uploadButton.enabled = false; + uploadProgressLabel.text = "In progress..."; + }, + function(err, path) { + print(err, path); + if (err === "") { + uploadProgressLabel.text = "Upload Complete"; + timer.interval = 1000; + timer.repeat = false; + timer.triggered.connect(function() { + uploadSpinner.visible = false; + uploadButton.enabled = true; + uploadOpen = false; + }); + timer.start(); + console.log("Asset Browser - finished uploading: ", fileUrl); + reload(); + } else { + uploadSpinner.visible = false; + uploadButton.enabled = true; + uploadOpen = false; + + if (err !== -1) { + console.log("Asset Browser - error uploading: ", fileUrl, " - error ", err); + var box = errorMessageBox("There was an error uploading:\n" + fileUrl + "\n" + err); + box.selected.connect(reload); + } + } + }, dropping); + } + + function initiateUpload(url) { + doUpload(doUploadTimer.url, false); + } + + if (fileUrl) { + doUpload(fileUrl, true); + } else { + var browser = tabletRoot.fileDialog({ + selectDirectory: false, + dir: currentDirectory + }); + + browser.canceled.connect(function() { + uploadOpen = false; + }); + + browser.selectedFile.connect(function(url) { + currentDirectory = browser.dir; + + // Initiate upload from a timer so that file browser dialog can close beforehand. + doUploadTimer.url = url; + if (!doUploadTimer.isConnected) { + doUploadTimer.triggered.connect(function() { initiateUpload(); }); + doUploadTimer.isConnected = true; + } + doUploadTimer.start(); + }); + } + } + + function errorMessageBox(message) { + return tabletRoot.messageBox({ + icon: hifi.icons.warning, + defaultButton: OriginalDialogs.StandardButton.Ok, + title: "Error", + text: message + }); + } + + Column { + width: parent.width + spacing: 10 + + HifiControls.TabletContentSection { + id: assetDirectory + name: "Asset Directory" + isFirst: true + + HifiControls.VerticalSpacer {} + + Row { + id: buttonRow + width: parent.width + height: 30 + spacing: hifi.dimensions.contentSpacing.x + + HifiControls.GlyphButton { + glyph: hifi.glyphs.reload + color: hifi.buttons.black + colorScheme: root.colorScheme + width: hifi.dimensions.controlLineHeight + + onClicked: root.reload() + } + + HifiControls.Button { + text: "Add To World" + color: hifi.buttons.black + colorScheme: root.colorScheme + width: 120 + + enabled: canAddToWorld(assetProxyModel.data(treeView.selection.currentIndex, 0x100)) + + onClicked: root.addToWorld() + } + + HifiControls.Button { + text: "Rename" + color: hifi.buttons.black + colorScheme: root.colorScheme + width: 80 + + onClicked: root.renameFile() + enabled: treeView.selection.hasSelection + } + + HifiControls.Button { + id: deleteButton + + text: "Delete" + color: hifi.buttons.red + colorScheme: root.colorScheme + width: 80 + + onClicked: root.deleteFile() + enabled: treeView.selection.hasSelection + } + } + + Menu { + id: contextMenu + title: "Edit" + property var url: "" + property var currentIndex: null + + MenuItem { + text: "Copy URL" + onTriggered: { + copyURLToClipboard(contextMenu.currentIndex); + } + } + + MenuItem { + text: "Rename" + onTriggered: { + renameFile(contextMenu.currentIndex); + } + } + + MenuItem { + text: "Delete" + onTriggered: { + deleteFile(contextMenu.currentIndex); + } + } + } + + } + HifiControls.Tree { + id: treeView + height: 430 + anchors.leftMargin: hifi.dimensions.contentMargin.x + 2 // Extra for border + anchors.rightMargin: hifi.dimensions.contentMargin.x + 2 // Extra for border + anchors.left: parent.left + anchors.right: parent.right + + treeModel: assetProxyModel + canEdit: true + colorScheme: root.colorScheme + + modifyEl: renameEl + + MouseArea { + propagateComposedEvents: true + anchors.fill: parent + acceptedButtons: Qt.RightButton + onClicked: { + if (!HMD.active) { // Popup only displays properly on desktop + var index = treeView.indexAt(mouse.x, mouse.y); + treeView.selection.setCurrentIndex(index, 0x0002); + contextMenu.currentIndex = index; + contextMenu.popup(); + } + } + } + } + + + HifiControls.TabletContentSection { + id: uploadSection + name: "Upload A File" + spacing: hifi.dimensions.contentSpacing.y + //anchors.bottom: parent.bottom + height: 65 + anchors.left: parent.left + anchors.right: parent.right + + Item { + height: parent.height + width: parent.width + HifiControls.Button { + id: uploadButton + anchors.right: parent.right + + text: "Choose File" + color: hifi.buttons.blue + colorScheme: root.colorScheme + height: 30 + width: 155 + + onClicked: uploadClickedTimer.running = true + + // For some reason trigginer an API that enters + // an internal event loop directly from the button clicked + // trigger below causes the appliction to behave oddly. + // Most likely because the button onClicked handling is never + // completed until the function returns. + // FIXME find a better way of handling the input dialogs that + // doesn't trigger this. + Timer { + id: uploadClickedTimer + interval: 5 + repeat: false + running: false + onTriggered: uploadClicked(); + } + } + + Item { + id: uploadSpinner + visible: false + anchors.top: parent.top + anchors.left: parent.left + width: 40 + height: 32 + + Image { + id: image + width: 24 + height: 24 + source: "../../../images/Loading-Outer-Ring.png" + RotationAnimation on rotation { + loops: Animation.Infinite + from: 0 + to: 360 + duration: 2000 + } + } + Image { + width: 24 + height: 24 + source: "../../../images/Loading-Inner-H.png" + } + HifiControls.Label { + id: uploadProgressLabel + anchors.left: image.right + anchors.leftMargin: 10 + anchors.verticalCenter: image.verticalCenter + text: "In progress..." + colorScheme: root.colorScheme + } + } + } + } + } +} + diff --git a/interface/resources/qml/hifi/dialogs/TabletDCDialog.qml b/interface/resources/qml/hifi/dialogs/TabletDCDialog.qml new file mode 100644 index 0000000000..b33b957e15 --- /dev/null +++ b/interface/resources/qml/hifi/dialogs/TabletDCDialog.qml @@ -0,0 +1,160 @@ +// +// TabletDCDialog.qml +// +// Created by Vlad Stelmahovsky on 3/15/17 +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +import QtQuick 2.5 +import QtQuick.Controls 1.4 +import Qt.labs.settings 1.0 + +import "../../styles-uit" +import "../../controls-uit" as HifiControls +import "../../windows" + +Rectangle { + id: root + objectName: "DCConectionTiming" + + property var eventBridge; + signal sendToScript(var message); + property bool isHMD: false + + color: hifi.colors.baseGray + + property int colorScheme: hifi.colorSchemes.dark + + HifiConstants { id: hifi } + + Component.onCompleted: DCModel.refresh() + + Row { + id: header + anchors.top: parent.top + anchors.topMargin: hifi.dimensions.tabletMenuHeader + anchors.leftMargin: 5 + anchors.rightMargin: 5 + anchors.left: parent.left + anchors.right: parent.right + + HifiControls.Label { + id: nameButton + text: qsTr("Name") + size: 15 + color: "white" + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + height: 40 + width: 175 + } + HifiControls.Label { + id: tsButton + text: qsTr("Timestamp\n(ms)") + size: 15 + color: "white" + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + height: 40 + width: 125 + } + HifiControls.Label { + id: deltaButton + text: qsTr("Delta\n(ms)") + size: 15 + color: "white" + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + height: 40 + width: 80 + } + HifiControls.Label { + id: elapseButton + text: qsTr("Time elapsed\n(ms)") + size: 15 + color: "white" + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + height: 40 + width: 80 + } + } + + ListView { + anchors.leftMargin: 5 + anchors.rightMargin: 5 + anchors.left: parent.left + anchors.right: parent.right + anchors.top: header.bottom + anchors.topMargin: 5 + anchors.bottom: refreshButton.top + anchors.bottomMargin: 10 + clip: true + snapMode: ListView.SnapToItem + + model: DCModel + + delegate: Rectangle { + anchors.left: parent.left + anchors.right: parent.right + height: 30 + color: index % 2 === 0 ? hifi.colors.baseGray : hifi.colors.lightGray + Row { + anchors.fill: parent + spacing: 5 + HifiControls.Label { + size: 15 + text: name + color: "white" + anchors.verticalCenter: parent.verticalCenter + colorScheme: root.colorScheme + width: nameButton.width + } + HifiControls.Label { + size: 15 + text: timestamp + color: "white" + anchors.verticalCenter: parent.verticalCenter + colorScheme: root.colorScheme + horizontalAlignment: Text.AlignHCenter + width: tsButton.width + } + HifiControls.Label { + size: 15 + text: delta + color: "white" + anchors.verticalCenter: parent.verticalCenter + colorScheme: root.colorScheme + horizontalAlignment: Text.AlignHCenter + width: deltaButton.width + } + HifiControls.Label { + size: 15 + text: timeelapsed + color: "white" + anchors.verticalCenter: parent.verticalCenter + colorScheme: root.colorScheme + horizontalAlignment: Text.AlignHCenter + width: elapseButton.width + } + } + } + } + + HifiControls.Button { + id: refreshButton + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + text: qsTr("Refresh") + color: hifi.buttons.blue + colorScheme: root.colorScheme + height: 30 + onClicked: { + DCModel.refresh() + } + } +} diff --git a/interface/resources/qml/hifi/dialogs/TabletDebugWindow.qml b/interface/resources/qml/hifi/dialogs/TabletDebugWindow.qml new file mode 100644 index 0000000000..d4bbe0af04 --- /dev/null +++ b/interface/resources/qml/hifi/dialogs/TabletDebugWindow.qml @@ -0,0 +1,78 @@ +// +// TabletDebugWindow.qml +// +// Vlad Stelmahovsky, created on 20/03/2017. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +import QtQuick 2.5 +import QtQuick.Controls 1.4 +import Hifi 1.0 as Hifi + +import "../../styles-uit" +import "../../controls-uit" as HifiControls + +Rectangle { + id: root + + objectName: "DebugWindow" + property var eventBridge; + + property var title: "Debug Window" + property bool isHMD: false + + color: hifi.colors.baseGray + + HifiConstants { id: hifi } + + signal sendToScript(var message); + property int colorScheme: hifi.colorSchemes.dark + + property var channel; + property var scripts: ScriptDiscoveryService; + + function fromScript(message) { + var MAX_LINE_COUNT = 2000; + var TRIM_LINES = 500; + if (textArea.lineCount > MAX_LINE_COUNT) { + var lines = textArea.text.split('\n'); + lines.splice(0, TRIM_LINES); + textArea.text = lines.join('\n'); + } + textArea.append(message); + } + + function getFormattedDate() { + var date = new Date(); + return date.getMonth() + "/" + date.getDate() + " " + date.getHours() + ":" + date.getMinutes() + ":" + date.getSeconds(); + } + + function sendToLogWindow(type, message, scriptFileName) { + var typeFormatted = ""; + if (type) { + typeFormatted = type + " - "; + } + fromScript("[" + getFormattedDate() + "] " + "[" + scriptFileName + "] " + typeFormatted + message); + } + + Connections { + target: ScriptDiscoveryService + onPrintedMessage: sendToLogWindow("", message, engineName); + onWarningMessage: sendToLogWindow("WARNING", message, engineName); + onErrorMessage: sendToLogWindow("ERROR", message, engineName); + onInfoMessage: sendToLogWindow("INFO", message, engineName); + } + + TextArea { + id: textArea + width: parent.width + height: parent.height + backgroundVisible: false + textColor: hifi.colors.white + text:"" + } + +} diff --git a/interface/resources/qml/hifi/dialogs/TabletEntityStatistics.qml b/interface/resources/qml/hifi/dialogs/TabletEntityStatistics.qml new file mode 100644 index 0000000000..35ee58be0c --- /dev/null +++ b/interface/resources/qml/hifi/dialogs/TabletEntityStatistics.qml @@ -0,0 +1,244 @@ +// +// TabletEntityStatistics.qml +// +// Created by Vlad Stelmahovsky on 3/11/17 +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +import QtQuick 2.5 +import QtQuick.Controls 1.4 +import Qt.labs.settings 1.0 + +import "../../styles-uit" +import "../../controls-uit" as HifiControls +import "../../windows" + +Rectangle { + id: root + objectName: "EntityStatistics" + + property var eventBridge; + signal sendToScript(var message); + property bool isHMD: false + + color: hifi.colors.baseGray + + property int colorScheme: hifi.colorSchemes.dark + + HifiConstants { id: hifi } + + Component.onCompleted: { + OctreeStats.startUpdates() + } + + Component.onDestruction: { + OctreeStats.stopUpdates() + } + + Flickable { + id: scrollView + width: parent.width + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.bottomMargin: hifi.dimensions.tabletMenuHeader + contentWidth: column.implicitWidth + contentHeight: column.implicitHeight + boundsBehavior: Flickable.StopAtBounds + + Column { + id: column + anchors.margins: 10 + anchors.left: parent.left + anchors.right: parent.right + y: hifi.dimensions.tabletMenuHeader //-bgNavBar + spacing: 20 + + TabletEntityStatisticsItem { + anchors.left: parent.left + anchors.right: parent.right + titleText: qsTr("Elements on Servers:") + text: OctreeStats.serverElements + colorScheme: root.colorScheme + color: OctreeStats.getColor() + } + + TabletEntityStatisticsItem { + anchors.left: parent.left + anchors.right: parent.right + titleText: qsTr("Local Elements:") + text: OctreeStats.localElements + colorScheme: root.colorScheme + color: OctreeStats.getColor() + } + + TabletEntityStatisticsItem { + anchors.left: parent.left + anchors.right: parent.right + titleText: qsTr("Elements Memory:") + text: OctreeStats.localElementsMemory + colorScheme: root.colorScheme + color: OctreeStats.getColor() + } + + TabletEntityStatisticsItem { + anchors.left: parent.left + anchors.right: parent.right + titleText: qsTr("Sending Mode:") + text: OctreeStats.sendingMode + colorScheme: root.colorScheme + color: OctreeStats.getColor() + } + + TabletEntityStatisticsItem { + anchors.left: parent.left + anchors.right: parent.right + titleText: qsTr("Incoming Entity Packets:") + text: OctreeStats.processedPackets + colorScheme: root.colorScheme + color: OctreeStats.getColor() + } + + TabletEntityStatisticsItem { + anchors.left: parent.left + anchors.right: parent.right + titleText: qsTr("Processed Packets Elements:") + text: OctreeStats.processedPacketsElements + colorScheme: root.colorScheme + color: OctreeStats.getColor() + } + + TabletEntityStatisticsItem { + anchors.left: parent.left + anchors.right: parent.right + titleText: qsTr("Processed Packets Entities:") + text: OctreeStats.processedPacketsEntities + colorScheme: root.colorScheme + color: OctreeStats.getColor() + } + + TabletEntityStatisticsItem { + anchors.left: parent.left + anchors.right: parent.right + titleText: qsTr("Processed Packets Timing:") + text: OctreeStats.processedPacketsTiming + colorScheme: root.colorScheme + color: OctreeStats.getColor() + } + + TabletEntityStatisticsItem { + anchors.left: parent.left + anchors.right: parent.right + titleText: qsTr("Outbound Entity Packets:") + text: OctreeStats.outboundEditPackets + colorScheme: root.colorScheme + color: OctreeStats.getColor() + } + + TabletEntityStatisticsItem { + anchors.left: parent.left + anchors.right: parent.right + titleText: qsTr("Entity Update Time:") + text: OctreeStats.entityUpdateTime + colorScheme: root.colorScheme + color: OctreeStats.getColor() + } + + TabletEntityStatisticsItem { + anchors.left: parent.left + anchors.right: parent.right + titleText: qsTr("Entity Updates:") + text: OctreeStats.entityUpdates + colorScheme: root.colorScheme + color: OctreeStats.getColor() + } + + Repeater { + model: OctreeStats.serversNum + + delegate: Column { + id: serverColumn + width: scrollView.width - 10 + x: 5 + spacing: 5 + + state: "less" + + TabletEntityStatisticsItem { + id: serverStats + width: parent.width + titleText: qsTr("Entity Server ") + (index+1) + ":" + colorScheme: root.colorScheme + color: OctreeStats.getColor() + } + + Row { + id: buttonsRow + width: parent.width + height: 30 + spacing: 10 + + HifiControls.Button { + id: moreButton + color: hifi.buttons.blue + colorScheme: root.colorScheme + width: parent.width / 2 - 10 + height: 30 + onClicked: { + if (serverColumn.state === "less") { + serverColumn.state = "more" + } else if (serverColumn.state === "more") { + serverColumn.state = "most" + } else { + serverColumn.state = "more" + } + } + } + HifiControls.Button { + id: mostButton + color: hifi.buttons.blue + colorScheme: root.colorScheme + height: 30 + width: parent.width / 2 - 10 + onClicked: { + if (serverColumn.state === "less") { + serverColumn.state = "most" + } else if (serverColumn.state === "more") { + serverColumn.state = "less" + } else { + serverColumn.state = "less" + } + } + + } + } + states: [ + State { + name: "less" + PropertyChanges { target: moreButton; text: qsTr("more..."); } + PropertyChanges { target: mostButton; text: qsTr("most..."); } + PropertyChanges { target: serverStats; text: OctreeStats.servers[index*3]; } + }, + State { + name: "more" + PropertyChanges { target: moreButton; text: qsTr("most..."); } + PropertyChanges { target: mostButton; text: qsTr("less..."); } + PropertyChanges { target: serverStats; text: OctreeStats.servers[index*3] + + OctreeStats.servers[index*3 + 1]; } + }, + State { + name: "most" + PropertyChanges { target: moreButton; text: qsTr("less..."); } + PropertyChanges { target: mostButton; text: qsTr("least..."); } + PropertyChanges { target: serverStats; text: OctreeStats.servers[index*3] + + OctreeStats.servers[index*3 + 1] + + OctreeStats.servers[index*3 + 2]; } + } + ] + } //servers column + } //repeater + } //column + } //flickable +} diff --git a/interface/resources/qml/hifi/dialogs/TabletEntityStatisticsItem.qml b/interface/resources/qml/hifi/dialogs/TabletEntityStatisticsItem.qml new file mode 100644 index 0000000000..894a4c1813 --- /dev/null +++ b/interface/resources/qml/hifi/dialogs/TabletEntityStatisticsItem.qml @@ -0,0 +1,49 @@ +// +// TabletEntityStatistics.qml +// +// Created by Vlad Stelmahovsky on 3/11/17 +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +import QtQuick 2.5 +import QtQuick.Controls 1.4 +import Qt.labs.settings 1.0 + +import "../../styles-uit" +import "../../controls-uit" as HifiControls + +Column { + id: root + property int colorScheme: hifi.colorSchemes.dark + + property alias titleText: titleLabel.text + property alias text: valueLabel.text + property alias color: valueLabel.color + + HifiConstants { id: hifi } + + anchors.left: parent.left + anchors.right: parent.right + spacing: 10 + + HifiControls.Label { + id: titleLabel + size: 20 + anchors.left: parent.left + anchors.right: parent.right + colorScheme: root.colorScheme + } + + RalewaySemiBold { + id: valueLabel + anchors.left: parent.left + anchors.right: parent.right + wrapMode: Text.WordWrap + size: 16 + color: enabled ? (root.colorScheme == hifi.colorSchemes.light ? hifi.colors.lightGray : hifi.colors.lightGrayText) + : (root.colorScheme == hifi.colorSchemes.light ? hifi.colors.lightGrayText : hifi.colors.baseGrayHighlight); + } +} diff --git a/interface/resources/qml/hifi/dialogs/TabletLODTools.qml b/interface/resources/qml/hifi/dialogs/TabletLODTools.qml new file mode 100644 index 0000000000..26e9759e0d --- /dev/null +++ b/interface/resources/qml/hifi/dialogs/TabletLODTools.qml @@ -0,0 +1,119 @@ +// +// TabletLODTools.qml +// +// Created by Vlad Stelmahovsky on 3/11/17 +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +import QtQuick 2.5 +import QtQuick.Controls 1.4 +import Qt.labs.settings 1.0 + +import "../../styles-uit" +import "../../controls-uit" as HifiControls +import "../../windows" + +Rectangle { + id: root + objectName: "LODTools" + + property var eventBridge; + signal sendToScript(var message); + property bool isHMD: false + + color: hifi.colors.baseGray + + property int colorScheme: hifi.colorSchemes.dark + + HifiConstants { id: hifi } + + readonly property real treeScale: 32768; // ~20 miles.. This is the number of meters of the 0.0 to 1.0 voxel universe + readonly property real halfTreeScale: treeScale / 2; + + // This controls the LOD. Larger number will make smaller voxels visible at greater distance. + readonly property real defaultOctreeSizeScale: treeScale * 400.0 + + Column { + anchors.margins: 10 + anchors.left: parent.left + anchors.right: parent.right + y: hifi.dimensions.tabletMenuHeader //-bgNavBar + spacing: 20 + + HifiControls.Label { + size: 20 + anchors.left: parent.left + anchors.right: parent.right + text: qsTr("You can see...") + colorScheme: root.colorScheme + } + HifiControls.Label { + id: whatYouCanSeeLabel + color: "red" + size: 20 + anchors.left: parent.left + anchors.right: parent.right + colorScheme: root.colorScheme + } + Row { + anchors.left: parent.left + anchors.right: parent.right + spacing: 10 + + HifiControls.Label { + size: 20 + text: qsTr("Manually Adjust Level of Detail:") + anchors.verticalCenter: parent.verticalCenter + colorScheme: root.colorScheme + } + + HifiControls.CheckBox { + id: adjustCheckbox + boxSize: 20 + anchors.verticalCenter: parent.verticalCenter + onCheckedChanged: LODManager.setAutomaticLODAdjust(!checked); + } + } + + HifiControls.Label { + size: 20 + anchors.left: parent.left + anchors.right: parent.right + text: qsTr("Level of Detail:") + colorScheme: root.colorScheme + } + HifiControls.Slider { + id: slider + enabled: adjustCheckbox.checked + anchors.left: parent.left + anchors.right: parent.right + minimumValue: 5 + maximumValue: 2000 + value: LODManager.getOctreeSizeScale() / treeScale + tickmarksEnabled: false + onValueChanged: { + LODManager.setOctreeSizeScale(value * treeScale); + whatYouCanSeeLabel.text = LODManager.getLODFeedbackText() + } + } + + HifiControls.Button { + id: uploadButton + anchors.left: parent.left + anchors.right: parent.right + text: qsTr("Reset") + color: hifi.buttons.blue + colorScheme: root.colorScheme + height: 30 + onClicked: { + slider.value = defaultOctreeSizeScale/treeScale + adjustCheckbox.checked = false + LODManager.setAutomaticLODAdjust(adjustCheckbox.checked); + } + } + } +} + diff --git a/interface/resources/qml/hifi/dialogs/TabletRunningScripts.qml b/interface/resources/qml/hifi/dialogs/TabletRunningScripts.qml new file mode 100644 index 0000000000..d826b40ad1 --- /dev/null +++ b/interface/resources/qml/hifi/dialogs/TabletRunningScripts.qml @@ -0,0 +1,375 @@ +// +// RunningScripts.qml +// +// Created by Bradley Austin Davis on 12 Jan 2016 +// Copyright 2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +import QtQuick 2.5 +import QtQuick.Controls 1.4 +import QtQuick.Dialogs 1.2 as OriginalDialogs +import Qt.labs.settings 1.0 + +import "../../styles-uit" +import "../../controls-uit" as HifiControls +import "../../windows" + +Rectangle { + id: root + objectName: "RunningScripts" + property string title: "Running Scripts" + HifiConstants { id: hifi } + signal sendToScript(var message); + property var eventBridge; + property var scripts: ScriptDiscoveryService; + property var scriptsModel: scripts.scriptsModelFilter + property var runningScriptsModel: ListModel { } + property bool isHMD: false + + color: hifi.colors.baseGray + + Connections { + target: ScriptDiscoveryService + onScriptCountChanged: updateRunningScripts(); + } + + Component.onCompleted: { + isHMD = HMD.active; + updateRunningScripts(); + } + + function updateRunningScripts() { + var runningScripts = ScriptDiscoveryService.getRunning(); + runningScriptsModel.clear() + for (var i = 0; i < runningScripts.length; ++i) { + runningScriptsModel.append(runningScripts[i]); + } + } + + function loadScript(script) { + console.log("Load script " + script); + scripts.loadOneScript(script); + } + + function reloadScript(script) { + console.log("Reload script " + script); + scripts.stopScript(script, true); + } + + function stopScript(script) { + console.log("Stop script " + script); + scripts.stopScript(script); + } + + function reloadAll() { + console.log("Reload all scripts"); + scripts.reloadAllScripts(); + } + + function loadDefaults() { + console.log("Load default scripts"); + scripts.loadOneScript(scripts.defaultScriptsPath + "/defaultScripts.js"); + } + + function stopAll() { + console.log("Stop all scripts"); + scripts.stopAllScripts(); + } + + Flickable { + id: flickable + width: tabletRoot.width + height: parent.height - (keyboard.raised ? keyboard.raisedHeight : 0) + contentWidth: column.width + contentHeight: column.childrenRect.height + clip: true + + Column { + id: column + width: parent.width + HifiControls.TabletContentSection { + id: firstSection + name: "Currently Running" + isFirst: true + + HifiControls.VerticalSpacer {} + + Row { + spacing: hifi.dimensions.contentSpacing.x + + HifiControls.Button { + text: "Reload All" + color: hifi.buttons.black + onClicked: reloadAll() + } + + HifiControls.Button { + text: "Remove All" + color: hifi.buttons.red + onClicked: stopAll() + } + } + + HifiControls.VerticalSpacer { + height: hifi.dimensions.controlInterlineHeight + 2 // Add space for border + } + + HifiControls.Table { + model: runningScriptsModel + id: table + height: 185 + width: parent.width + colorScheme: hifi.colorSchemes.dark + expandSelectedRow: true + + itemDelegate: Item { + anchors { + left: parent ? parent.left : undefined + leftMargin: hifi.dimensions.tablePadding + right: parent ? parent.right : undefined + rightMargin: hifi.dimensions.tablePadding + } + + FiraSansSemiBold { + id: textItem + text: styleData.value + size: hifi.fontSizes.tableText + color: table.colorScheme == hifi.colorSchemes.light + ? (styleData.selected ? hifi.colors.black : hifi.colors.baseGrayHighlight) + : (styleData.selected ? hifi.colors.black : hifi.colors.lightGrayText) + anchors { + left: parent.left + right: parent.right + top: parent.top + topMargin: 3 + } + + HiFiGlyphs { + id: reloadButton + text: hifi.glyphs.reloadSmall + color: reloadButtonArea.pressed ? hifi.colors.white : parent.color + anchors { + top: parent.top + right: stopButton.left + verticalCenter: parent.verticalCenter + } + MouseArea { + id: reloadButtonArea + anchors { fill: parent; margins: -2 } + onClicked: reloadScript(model.url) + } + } + + HiFiGlyphs { + id: stopButton + text: hifi.glyphs.closeSmall + color: stopButtonArea.pressed ? hifi.colors.white : parent.color + anchors { + top: parent.top + right: parent.right + verticalCenter: parent.verticalCenter + } + MouseArea { + id: stopButtonArea + anchors { fill: parent; margins: -2 } + onClicked: stopScript(model.url) + } + } + + } + + FiraSansSemiBold { + text: runningScriptsModel.get(styleData.row) ? runningScriptsModel.get(styleData.row).url : "" + elide: Text.ElideMiddle + size: hifi.fontSizes.tableText + color: table.colorScheme == hifi.colorSchemes.light + ? (styleData.selected ? hifi.colors.black : hifi.colors.lightGray) + : (styleData.selected ? hifi.colors.black : hifi.colors.lightGrayText) + anchors { + top: textItem.bottom + left: parent.left + right: parent.right + } + visible: styleData.selected + } + } + + TableViewColumn { + role: "name" + } + } + + HifiControls.VerticalSpacer { + height: hifi.dimensions.controlInterlineHeight + 2 // Add space for border + } + } + + HifiControls.TabletContentSection { + name: "Load Scripts" + + HifiControls.VerticalSpacer {} + + Row { + spacing: hifi.dimensions.contentSpacing.x + + HifiControls.Button { + text: "from URL" + color: hifi.buttons.black + height: 26 + onClicked: fromUrlTimer.running = true + + // For some reason trigginer an API that enters + // an internal event loop directly from the button clicked + // trigger below causes the appliction to behave oddly. + // Most likely because the button onClicked handling is never + // completed until the function returns. + // FIXME find a better way of handling the input dialogs that + // doesn't trigger this. + Timer { + id: fromUrlTimer + interval: 5 + repeat: false + running: false + onTriggered: ApplicationInterface.loadScriptURLDialog(); + } + } + + HifiControls.Button { + text: "from Disk" + color: hifi.buttons.black + height: 26 + onClicked: fromDiskTimer.running = true + + Timer { + id: fromDiskTimer + interval: 5 + repeat: false + running: false + onTriggered: ApplicationInterface.loadDialog(); + } + } + + HifiControls.Button { + text: "Load Defaults" + color: hifi.buttons.black + height: 26 + onClicked: loadDefaults() + } + } + + HifiControls.VerticalSpacer {} + + HifiControls.TextField { + id: filterEdit + isSearchField: true + anchors.left: parent.left + anchors.right: parent.right + colorScheme: hifi.colorSchemes.dark + placeholderText: "Filter" + onTextChanged: scriptsModel.filterRegExp = new RegExp("^.*" + text + ".*$", "i") + Component.onCompleted: scriptsModel.filterRegExp = new RegExp("^.*$", "i") + onActiveFocusChanged: { + // raise the keyboard + keyboard.raised = activeFocus; + + // scroll to the bottom of the content area. + if (activeFocus) { + flickable.contentY = (flickable.contentHeight - flickable.height); + } + } + } + + HifiControls.VerticalSpacer { + height: hifi.dimensions.controlInterlineHeight + 2 // Add space for border + } + + HifiControls.Tree { + id: treeView + height: 155 + treeModel: scriptsModel + colorScheme: hifi.colorSchemes.dark + anchors.left: parent.left + anchors.right: parent.right + } + + HifiControls.VerticalSpacer { + height: hifi.dimensions.controlInterlineHeight + 2 // Add space for border + } + + HifiControls.TextField { + id: selectedScript + anchors.left: parent.left + anchors.right: parent.right + anchors.rightMargin: loadButton.width + hifi.dimensions.contentSpacing.x + + colorScheme: hifi.colorSchemes.dark + readOnly: true + + Connections { + target: treeView + onCurrentIndexChanged: { + var path = scriptsModel.data(treeView.currentIndex, 0x100) + if (path) { + selectedScript.text = path + } else { + selectedScript.text = "" + } + } + } + } + + Item { + // Take the loadButton out of the column flow. + id: loadButtonContainer + anchors.top: selectedScript.top + anchors.right: parent.right + + HifiControls.Button { + id: loadButton + anchors.right: parent.right + + text: "Load" + color: hifi.buttons.blue + enabled: selectedScript.text != "" + onClicked: root.loadScript(selectedScript.text) + } + } + + HifiControls.VerticalSpacer { + height: hifi.dimensions.controlInterlineHeight - (!isHMD ? 3 : 0) + } + + HifiControls.TextAction { + id: directoryButton + icon: hifi.glyphs.script + iconSize: 24 + text: "Reveal Scripts Folder" + onClicked: fileDialogHelper.openDirectory(scripts.defaultScriptsPath) + colorScheme: hifi.colorSchemes.dark + anchors.left: parent.left + visible: !isHMD + } + + HifiControls.VerticalSpacer { + height: hifi.dimensions.controlInterlineHeight - 3 + visible: !isHMD + } + } + } + } + + HifiControls.Keyboard { + id: keyboard + raised: false + numeric: false + anchors { + bottom: parent.bottom + left: parent.left + right: parent.right + } + } +} + diff --git a/interface/resources/qml/hifi/dialogs/attachments/Attachment.qml b/interface/resources/qml/hifi/dialogs/attachments/Attachment.qml index 6d371741ea..0e98f79216 100644 --- a/interface/resources/qml/hifi/dialogs/attachments/Attachment.qml +++ b/interface/resources/qml/hifi/dialogs/attachments/Attachment.qml @@ -6,6 +6,7 @@ import Qt.labs.settings 1.0 import "." import ".." +import "../../tablet" import "../../../styles-uit" import "../../../controls-uit" as HifiControls import "../../../windows" @@ -17,10 +18,24 @@ Item { HifiConstants { id: hifi } + signal selectAttachment(); signal deleteAttachment(var attachment); signal updateAttachment(); property bool completed: false; + function doSelectAttachment(control, focus) { + if (focus) { + selectAttachment(); + + // Refocus control after possibly changing focus to attachment. + if (control.setControlFocus !== undefined) { + control.setControlFocus(); + } else { + control.focus = true; + } + } + } + Rectangle { color: hifi.colors.baseGray; anchors.fill: parent; radius: 4 } Component.onCompleted: { @@ -50,6 +65,7 @@ Item { updateAttachment(); } } + onFocusChanged: doSelectAttachment(this, focus); } HifiControls.Button { id: modelChooserButton; @@ -61,17 +77,37 @@ Item { id: modelBrowserBuilder; ModelBrowserDialog {} } + Component { + id: tabletModelBrowserBuilder; + TabletModelBrowserDialog {} + } onClicked: { - var browser = modelBrowserBuilder.createObject(desktop); - browser.selected.connect(function(newModelUrl){ - modelUrl.text = newModelUrl; - }) + var browser; + if (typeof desktop !== "undefined") { + browser = modelBrowserBuilder.createObject(desktop); + browser.selected.connect(function(newModelUrl){ + modelUrl.text = newModelUrl; + }); + } else { + browser = tabletModelBrowserBuilder.createObject(tabletRoot); + browser.selected.connect(function(newModelUrl){ + modelUrl.text = newModelUrl; + tabletRoot.openModal = null; + }); + browser.canceled.connect(function() { + tabletRoot.openModal = null; + }); + + // Make dialog modal. + tabletRoot.openModal = browser; + } } } } Item { + z: 1000 height: jointChooser.height + jointLabel.height + 4 anchors { left: parent.left; right: parent.right; } HifiControls.Label { @@ -82,6 +118,7 @@ Item { } HifiControls.ComboBox { id: jointChooser; + dropdownHeight: (typeof desktop !== "undefined") ? 480 : 206 anchors { bottom: parent.bottom; left: parent.left; right: parent.right } colorScheme: hifi.colorSchemes.dark currentIndex: attachment ? model.indexOf(attachment.jointName) : -1 @@ -91,6 +128,7 @@ Item { updateAttachment(); } } + onFocusChanged: doSelectAttachment(this, focus); } } @@ -108,6 +146,7 @@ Item { updateAttachment(); } } + onControlFocusChanged: doSelectAttachment(this, controlFocus); } } @@ -125,6 +164,7 @@ Item { updateAttachment(); } } + onControlFocusChanged: doSelectAttachment(this, controlFocus); } } @@ -153,6 +193,7 @@ Item { updateAttachment(); } } + onFocusChanged: doSelectAttachment(this, focus); } } @@ -178,6 +219,7 @@ Item { updateAttachment(); } } + onFocusChanged: doSelectAttachment(this, focus); } } } diff --git a/interface/resources/qml/hifi/dialogs/attachments/Vector3.qml b/interface/resources/qml/hifi/dialogs/attachments/Vector3.qml index 3d109cc2a5..29f2c0ebf4 100644 --- a/interface/resources/qml/hifi/dialogs/attachments/Vector3.qml +++ b/interface/resources/qml/hifi/dialogs/attachments/Vector3.qml @@ -15,9 +15,37 @@ Item { property real stepSize: 1 property real maximumValue: 99 property real minimumValue: 0 + property bool controlFocus: false; // True if one of the ordinate controls has focus. + property var controlFocusControl: undefined signal valueChanged(); + function setControlFocus() { + if (controlFocusControl) { + controlFocusControl.focus = true; + // The controlFocus value is updated via onFocusChanged. + } + } + + function setFocus(control, focus) { + if (focus) { + controlFocusControl = control; + setControlFocusTrue.start(); // After any subsequent false from previous control. + } else { + controlFocus = false; + } + } + + Timer { + id: setControlFocusTrue + interval: 50 + repeat: false + running: false + onTriggered: { + controlFocus = true; + } + } + HifiConstants { id: hifi } HifiControls.SpinBox { @@ -38,6 +66,7 @@ Item { root.valueChanged(); } } + onFocusChanged: setFocus(this, focus); } HifiControls.SpinBox { @@ -58,6 +87,7 @@ Item { root.valueChanged(); } } + onFocusChanged: setFocus(this, focus); } HifiControls.SpinBox { @@ -78,6 +108,6 @@ Item { root.valueChanged(); } } + onFocusChanged: setFocus(this, focus); } } - diff --git a/interface/resources/qml/hifi/dialogs/content/AttachmentsContent.qml b/interface/resources/qml/hifi/dialogs/content/AttachmentsContent.qml new file mode 100644 index 0000000000..4adb485c2b --- /dev/null +++ b/interface/resources/qml/hifi/dialogs/content/AttachmentsContent.qml @@ -0,0 +1,260 @@ +import QtQuick 2.5 +import QtQuick.Controls 1.4 +import QtQuick.Dialogs 1.2 as OriginalDialogs +import QtQuick.Controls.Styles 1.4 + +import "../../../styles-uit" +import "../../../controls-uit" as HifiControls +import "../../../windows" +import "../attachments" + +Item { + id: content + + readonly property var originalAttachments: MyAvatar.getAttachmentsVariant(); + property var attachments: []; + + Component.onCompleted: { + for (var i = 0; i < originalAttachments.length; ++i) { + var attachment = originalAttachments[i]; + content.attachments.push(attachment); + listView.model.append({}); + } + } + + Column { + width: pane.width + + Rectangle { + width: parent.width + height: root.height - (keyboardEnabled && keyboardRaised ? 200 : 0) + color: hifi.colors.baseGray + + Rectangle { + id: attachmentsBackground + anchors { + left: parent.left; right: parent.right; top: parent.top; bottom: newAttachmentButton.top; + margins: hifi.dimensions.contentMargin.x + bottomMargin: hifi.dimensions.contentSpacing.y + } + color: hifi.colors.baseGrayShadow + radius: 4 + + ListView { + id: listView + anchors { + top: parent.top + left: parent.left + right: scrollBar.left + bottom: parent.bottom + margins: 4 + } + clip: true + snapMode: ListView.SnapToItem + + model: ListModel {} + delegate: Item { + id: attachmentDelegate + implicitHeight: attachmentView.height + 8; + implicitWidth: attachmentView.width + + MouseArea { + // User can click on whitespace to select item. + anchors.fill: parent + propagateComposedEvents: true + onClicked: { + listView.currentIndex = index; + attachmentsBackground.forceActiveFocus(); // Unfocus from any control. + mouse.accepted = false; + } + } + + Attachment { + id: attachmentView + width: listView.width + attachment: content.attachments[index] + onSelectAttachment: { + listView.currentIndex = index; + } + onDeleteAttachment: { + attachments.splice(index, 1); + listView.model.remove(index, 1); + } + onUpdateAttachment: MyAvatar.setAttachmentsVariant(attachments); + } + } + + onCountChanged: MyAvatar.setAttachmentsVariant(attachments); + + /* + // DEBUG + highlight: Rectangle { color: "#40ffff00" } + highlightFollowsCurrentItem: true + */ + + onHeightChanged: { + // Keyboard has been raised / lowered. + positionViewAtIndex(listView.currentIndex, ListView.SnapPosition); + } + + onCurrentIndexChanged: { + if (!yScrollTimer.running) { + scrollSlider.y = currentIndex * (scrollBar.height - scrollSlider.height) / (listView.count - 1); + } + } + + onContentYChanged: { + // User may have dragged content up/down. + yScrollTimer.restart(); + } + + Timer { + id: yScrollTimer + interval: 200 + repeat: false + running: false + onTriggered: { + var index = (listView.count - 1) * listView.contentY / (listView.contentHeight - scrollBar.height); + index = Math.round(index); + listView.currentIndex = index; + scrollSlider.y = index * (scrollBar.height - scrollSlider.height) / (listView.count - 1); + } + } + } + + Rectangle { + id: scrollBar + + property bool scrolling: listView.contentHeight > listView.height + + anchors { + top: parent.top + right: parent.right + bottom: parent.bottom + topMargin: 4 + bottomMargin: 4 + } + width: scrolling ? 18 : 0 + radius: attachmentsBackground.radius + color: hifi.colors.baseGrayShadow + + MouseArea { + anchors.fill: parent + + onClicked: { + var index = listView.currentIndex; + index = index + (mouse.y <= scrollSlider.y ? -1 : 1); + if (index < 0) { + index = 0; + } + if (index > listView.count - 1) { + index = listView.count - 1; + } + listView.currentIndex = index; + } + } + + Rectangle { + id: scrollSlider + anchors { + right: parent.right + rightMargin: 3 + } + width: 16 + height: (listView.height / listView.contentHeight) * listView.height + radius: width / 2 + color: hifi.colors.lightGray + + visible: scrollBar.scrolling; + + onYChanged: { + var index = y * (listView.count - 1) / (scrollBar.height - scrollSlider.height); + index = Math.round(index); + listView.currentIndex = index; + } + + MouseArea { + anchors.fill: parent + drag.target: scrollSlider + drag.axis: Drag.YAxis + drag.minimumY: 0 + drag.maximumY: scrollBar.height - scrollSlider.height + } + } + } + } + + HifiControls.Button { + id: newAttachmentButton + anchors { + left: parent.left + right: parent.right + bottom: buttonRow.top + margins: hifi.dimensions.contentMargin.x; + topMargin: hifi.dimensions.contentSpacing.y + bottomMargin: hifi.dimensions.contentSpacing.y + } + text: "New Attachment" + color: hifi.buttons.black + colorScheme: hifi.colorSchemes.dark + onClicked: { + var template = { + modelUrl: "", + translation: { x: 0, y: 0, z: 0 }, + rotation: { x: 0, y: 0, z: 0 }, + scale: 1, + jointName: MyAvatar.jointNames[0], + soft: false + }; + attachments.push(template); + listView.model.append({}); + MyAvatar.setAttachmentsVariant(attachments); + } + } + + Row { + id: buttonRow + spacing: 8 + anchors { + right: parent.right + bottom: parent.bottom + margins: hifi.dimensions.contentMargin.x + topMargin: hifi.dimensions.contentSpacing.y + bottomMargin: hifi.dimensions.contentSpacing.y + } + HifiControls.Button { + action: okAction + color: hifi.buttons.black + colorScheme: hifi.colorSchemes.dark + } + HifiControls.Button { + action: cancelAction + color: hifi.buttons.black + colorScheme: hifi.colorSchemes.dark + } + } + + Action { + id: cancelAction + text: "Cancel" + onTriggered: { + MyAvatar.setAttachmentsVariant(originalAttachments); + closeDialog(); + } + } + + Action { + id: okAction + text: "OK" + onTriggered: { + for (var i = 0; i < attachments.length; ++i) { + console.log("Attachment " + i + ": " + attachments[i]); + } + + MyAvatar.setAttachmentsVariant(attachments); + closeDialog(); + } + } + } + } +} diff --git a/interface/resources/qml/hifi/dialogs/content/ModelBrowserContent.qml b/interface/resources/qml/hifi/dialogs/content/ModelBrowserContent.qml new file mode 100644 index 0000000000..50fca94ff1 --- /dev/null +++ b/interface/resources/qml/hifi/dialogs/content/ModelBrowserContent.qml @@ -0,0 +1,64 @@ +import QtQuick 2.5 +import QtQuick.Controls 1.4 + +import "../../../controls-uit" as HifiControls + +Column { + width: pane.contentWidth + + Rectangle { + width: parent.width + height: root.height - (keyboardEnabled && keyboardRaised ? 200 : 0) + color: hifi.colors.baseGray + + HifiControls.TextField { + id: filterEdit + anchors { left: parent.left; right: parent.right; top: parent.top ; margins: 8} + placeholderText: "filter" + onTextChanged: tableView.model.filter = text + colorScheme: hifi.colorSchemes.dark + } + + HifiControls.AttachmentsTable { + id: tableView + anchors { left: parent.left; right: parent.right; top: filterEdit.bottom; bottom: buttonRow.top; margins: 8; } + colorScheme: hifi.colorSchemes.dark + onCurrentRowChanged: { + if (currentRow == -1) { + root.result = null; + return; + } + result = model.baseUrl + "/" + model.get(tableView.currentRow).key; + } + } + + Row { + id: buttonRow + spacing: 8 + anchors { right: parent.right; rightMargin: 8; bottom: parent.bottom; bottomMargin: 8; } + HifiControls.Button { action: acceptAction ; color: hifi.buttons.black; colorScheme: hifi.colorSchemes.dark } + HifiControls.Button { action: cancelAction ; color: hifi.buttons.black; colorScheme: hifi.colorSchemes.dark } + } + + Action { + id: acceptAction + text: qsTr("OK") + enabled: root.result ? true : false + shortcut: Qt.Key_Return + onTriggered: { + root.selected(root.result); + root.destroy(); + } + } + + Action { + id: cancelAction + text: qsTr("Cancel") + shortcut: Qt.Key_Escape + onTriggered: { + root.canceled(); + root.destroy(); + } + } + } +} diff --git a/interface/resources/qml/hifi/tablet/Edit.qml b/interface/resources/qml/hifi/tablet/Edit.qml new file mode 100644 index 0000000000..4abe698fbc --- /dev/null +++ b/interface/resources/qml/hifi/tablet/Edit.qml @@ -0,0 +1,299 @@ +import QtQuick 2.5 +import QtQuick.Controls 1.4 +import QtWebEngine 1.1 +import QtWebChannel 1.0 +import QtQuick.Controls.Styles 1.4 +import "../../controls" +import "../toolbars" +import HFWebEngineProfile 1.0 +import QtGraphicalEffects 1.0 +import "../../controls-uit" as HifiControls +import "../../styles-uit" + +StackView { + id: editRoot + objectName: "stack" + initialItem: editBasePage + + property var eventBridge; + signal sendToScript(var message); + + HifiConstants { id: hifi } + + function pushSource(path) { + editRoot.push(Qt.resolvedUrl(path)); + editRoot.currentItem.eventBridge = editRoot.eventBridge; + editRoot.currentItem.sendToScript.connect(editRoot.sendToScript); + } + + function popSource() { + editRoot.pop(); + } + + + Component { + id: editBasePage + TabView { + id: editTabView + // anchors.fill: parent + height: 60 + + Tab { + title: "CREATE" + active: true + enabled: true + property string originalUrl: "" + + Rectangle { + color: "#404040" + + Text { + color: "#ffffff" + text: "Choose an Entity Type to Create:" + font.pixelSize: 14 + font.bold: true + anchors.top: parent.top + anchors.topMargin: 28 + anchors.left: parent.left + anchors.leftMargin: 28 + } + + Flow { + id: createEntitiesFlow + spacing: 35 + anchors.right: parent.right + anchors.rightMargin: 55 + anchors.left: parent.left + anchors.leftMargin: 55 + anchors.top: parent.top + anchors.topMargin: 70 + + + NewEntityButton { + icon: "icons/create-icons/94-model-01.svg" + text: "MODEL" + onClicked: { + editRoot.sendToScript({ + method: "newEntityButtonClicked", params: { buttonName: "newModelButton" } + }); + editTabView.currentIndex = 2 + } + } + + NewEntityButton { + icon: "icons/create-icons/21-cube-01.svg" + text: "CUBE" + onClicked: { + editRoot.sendToScript({ + method: "newEntityButtonClicked", params: { buttonName: "newCubeButton" } + }); + editTabView.currentIndex = 2 + } + } + + NewEntityButton { + icon: "icons/create-icons/22-sphere-01.svg" + text: "SPHERE" + onClicked: { + editRoot.sendToScript({ + method: "newEntityButtonClicked", params: { buttonName: "newSphereButton" } + }); + editTabView.currentIndex = 2 + } + } + + NewEntityButton { + icon: "icons/create-icons/24-light-01.svg" + text: "LIGHT" + onClicked: { + editRoot.sendToScript({ + method: "newEntityButtonClicked", params: { buttonName: "newLightButton" } + }); + editTabView.currentIndex = 2 + } + } + + NewEntityButton { + icon: "icons/create-icons/20-text-01.svg" + text: "TEXT" + onClicked: { + editRoot.sendToScript({ + method: "newEntityButtonClicked", params: { buttonName: "newTextButton" } + }); + editTabView.currentIndex = 2 + } + } + + NewEntityButton { + icon: "icons/create-icons/25-web-1-01.svg" + text: "WEB" + onClicked: { + editRoot.sendToScript({ + method: "newEntityButtonClicked", params: { buttonName: "newWebButton" } + }); + editTabView.currentIndex = 2 + } + } + + NewEntityButton { + icon: "icons/create-icons/23-zone-01.svg" + text: "ZONE" + onClicked: { + editRoot.sendToScript({ + method: "newEntityButtonClicked", params: { buttonName: "newZoneButton" } + }); + editTabView.currentIndex = 2 + } + } + + NewEntityButton { + icon: "icons/create-icons/90-particles-01.svg" + text: "PARTICLE" + onClicked: { + editRoot.sendToScript({ + method: "newEntityButtonClicked", params: { buttonName: "newParticleButton" } + }); + editTabView.currentIndex = 2 + } + } + } + + HifiControls.Button { + id: assetServerButton + text: "Open This Domain's Asset Server" + color: hifi.buttons.black + colorScheme: hifi.colorSchemes.dark + anchors.right: parent.right + anchors.rightMargin: 55 + anchors.left: parent.left + anchors.leftMargin: 55 + anchors.top: createEntitiesFlow.bottom + anchors.topMargin: 35 + onClicked: { + editRoot.sendToScript({ + method: "newEntityButtonClicked", params: { buttonName: "openAssetBrowserButton" } + }); + } + } + + HifiControls.Button { + text: "Import Entities (.json)" + color: hifi.buttons.black + colorScheme: hifi.colorSchemes.dark + anchors.right: parent.right + anchors.rightMargin: 55 + anchors.left: parent.left + anchors.leftMargin: 55 + anchors.top: assetServerButton.bottom + anchors.topMargin: 20 + onClicked: { + editRoot.sendToScript({ + method: "newEntityButtonClicked", params: { buttonName: "importEntitiesButton" } + }); + } + } + } + } + + Tab { + title: "LIST" + active: true + enabled: true + property string originalUrl: "" + + WebView { + id: entityListToolWebView + url: "../../../../../scripts/system/html/entityList.html" + eventBridge: editRoot.eventBridge + anchors.fill: parent + enabled: true + } + } + + Tab { + title: "PROPERTIES" + active: true + enabled: true + property string originalUrl: "" + + WebView { + id: entityPropertiesWebView + url: "../../../../../scripts/system/html/entityProperties.html" + eventBridge: editRoot.eventBridge + anchors.fill: parent + enabled: true + } + } + + Tab { + title: "GRID" + active: true + enabled: true + property string originalUrl: "" + + WebView { + id: gridControlsWebView + url: "../../../../../scripts/system/html/gridControls.html" + eventBridge: editRoot.eventBridge + anchors.fill: parent + enabled: true + } + } + + Tab { + title: "P" + active: true + enabled: true + property string originalUrl: "" + + WebView { + id: particleExplorerWebView + url: "../../../../../scripts/system/particle_explorer/particleExplorer.html" + eventBridge: editRoot.eventBridge + anchors.fill: parent + enabled: true + } + } + + + style: TabViewStyle { + frameOverlap: 1 + tab: Rectangle { + color: styleData.selected ? "#404040" :"black" + implicitWidth: text.width + 42 + implicitHeight: 40 + Text { + id: text + anchors.centerIn: parent + text: styleData.title + font.pixelSize: 16 + font.bold: true + color: styleData.selected ? "white" : "white" + property string glyphtext: "" + HiFiGlyphs { + anchors.centerIn: parent + size: 30 + color: "#ffffff" + text: text.glyphtext + } + Component.onCompleted: if (styleData.title == "P") { + text.text = " "; + text.glyphtext = "\ue004"; + } + } + } + tabBar: Rectangle { + color: "black" + anchors.right: parent.right + anchors.rightMargin: 0 + anchors.left: parent.left + anchors.leftMargin: 0 + anchors.bottom: parent.bottom + anchors.bottomMargin: 0 + anchors.top: parent.top + anchors.topMargin: 0 + } + } + } + } +} diff --git a/interface/resources/qml/hifi/tablet/NewEntityButton.qml b/interface/resources/qml/hifi/tablet/NewEntityButton.qml new file mode 100644 index 0000000000..e5684fa791 --- /dev/null +++ b/interface/resources/qml/hifi/tablet/NewEntityButton.qml @@ -0,0 +1,160 @@ +import QtQuick 2.0 +import QtGraphicalEffects 1.0 + +Item { + id: newEntityButton + property var uuid; + property string text: "ENTITY" + property string icon: "icons/edit-icon.svg" + property string activeText: newEntityButton.text + property string activeIcon: newEntityButton.icon + property bool isActive: false + property bool inDebugMode: false + property bool isEntered: false + property double sortOrder: 100 + property int stableOrder: 0 + property var tabletRoot; + width: 100 + height: 100 + + signal clicked() + + function changeProperty(key, value) { + tabletButton[key] = value; + } + + onIsActiveChanged: { + if (tabletButton.isEntered) { + tabletButton.state = (tabletButton.isActive) ? "hover active state" : "hover sate"; + } else { + tabletButton.state = (tabletButton.isActive) ? "active state" : "base sate"; + } + } + + Rectangle { + id: buttonBg + color: "#1c1c1c" + opacity: 1 + radius: 8 + anchors.right: parent.right + anchors.rightMargin: 0 + anchors.left: parent.left + anchors.leftMargin: 0 + anchors.bottom: parent.bottom + anchors.bottomMargin: 0 + anchors.top: parent.top + anchors.topMargin: 0 + } + + function urlHelper(src) { + if (src.match(/\bhttp/)) { + return src; + } else { + return "../../../" + src; + } + } + + Rectangle { + id: buttonOutline + color: "#00000000" + opacity: 0 + radius: 8 + z: 1 + border.width: 2 + border.color: "#ffffff" + anchors.right: parent.right + anchors.rightMargin: 0 + anchors.left: parent.left + anchors.leftMargin: 0 + anchors.bottom: parent.bottom + anchors.bottomMargin: 0 + anchors.top: parent.top + anchors.topMargin: 0 + } + + DropShadow { + id: glow + visible: false + anchors.fill: parent + horizontalOffset: 0 + verticalOffset: 0 + color: "#ffffff" + radius: 20 + z: -1 + samples: 41 + source: buttonOutline + } + + + Image { + id: icon + width: 50 + height: 50 + visible: false + anchors.bottom: text.top + anchors.bottomMargin: 5 + anchors.horizontalCenter: parent.horizontalCenter + fillMode: Image.Stretch + source: newEntityButton.urlHelper(newEntityButton.icon) + } + + ColorOverlay { + id: iconColorOverlay + anchors.fill: icon + source: icon + color: "#ffffff" + } + + Text { + id: text + color: "#ffffff" + text: newEntityButton.text + font.bold: true + font.pixelSize: 16 + anchors.bottom: parent.bottom + anchors.bottomMargin: 12 + anchors.horizontalCenter: parent.horizontalCenter + horizontalAlignment: Text.AlignHCenter + } + + MouseArea { + anchors.fill: parent + hoverEnabled: true + enabled: true + onClicked: { + newEntityButton.clicked(); + } + onEntered: { + newEntityButton.state = "hover state"; + } + onExited: { + newEntityButton.state = "base state"; + } + } + + states: [ + State { + name: "hover state" + + PropertyChanges { + target: buttonOutline + opacity: 1 + } + + PropertyChanges { + target: glow + visible: true + } + }, + State { + name: "base state" + + PropertyChanges { + target: glow + visible: false + } + } + ] +} + + diff --git a/interface/resources/qml/hifi/tablet/NewModelDialog.qml b/interface/resources/qml/hifi/tablet/NewModelDialog.qml new file mode 100644 index 0000000000..5dbb733872 --- /dev/null +++ b/interface/resources/qml/hifi/tablet/NewModelDialog.qml @@ -0,0 +1,199 @@ +// +// NewModelDialog.qml +// qml/hifi +// +// Created by Seth Alves on 2017-2-10 +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +import QtQuick 2.5 +import QtQuick.Controls 1.4 +import "../../styles-uit" +import "../../controls-uit" + +Rectangle { + id: newModelDialog + // width: parent.width + // height: parent.height + HifiConstants { id: hifi } + color: hifi.colors.baseGray; + property var eventBridge; + signal sendToScript(var message); + property bool keyboardEnabled: false + property bool punctuationMode: false + property bool keyboardRasied: false + + Item { + id: column1 + anchors.rightMargin: 10 + anchors.leftMargin: 10 + anchors.bottomMargin: 10 + anchors.topMargin: 10 + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: keyboard.top + + Text { + id: text1 + text: qsTr("Model URL") + color: "#ffffff" + font.pixelSize: 12 + } + + TextInput { + id: modelURL + height: 20 + text: qsTr("") + color: "white" + anchors.top: text1.bottom + anchors.topMargin: 5 + anchors.left: parent.left + anchors.leftMargin: 0 + anchors.right: parent.right + anchors.rightMargin: 0 + font.pixelSize: 12 + + onAccepted: { + newModelDialog.keyboardEnabled = false; + } + + MouseArea { + anchors.fill: parent + onClicked: { + newModelDialog.keyboardEnabled = HMD.active + parent.focus = true; + parent.forceActiveFocus() + } + } + } + + Rectangle { + id: textInputBox + color: "white" + anchors.fill: modelURL + opacity: 0.1 + } + + Row { + id: row1 + height: 400 + spacing: 30 + anchors.top: modelURL.top + anchors.topMargin: 25 + anchors.left: parent.left + anchors.leftMargin: 0 + anchors.right: parent.right + anchors.rightMargin: 0 + + Column { + id: column2 + width: 200 + height: 400 + spacing: 10 + + CheckBox { + id: dynamic + text: qsTr("Dynamic") + + } + + Row { + id: row2 + width: 200 + height: 400 + spacing: 20 + + Image { + id: image1 + width: 30 + height: 30 + source: "qrc:/qtquickplugin/images/template_image.png" + } + + Text { + id: text2 + width: 160 + color: "#ffffff" + text: qsTr("Models with automatic collisions set to 'Exact' cannot be dynamic") + wrapMode: Text.WordWrap + font.pixelSize: 12 + } + } + } + + Column { + id: column3 + height: 400 + spacing: 10 + + Text { + id: text3 + text: qsTr("Automatic Collisions") + color: "#ffffff" + font.pixelSize: 12 + } + + ComboBox { + id: collisionType + width: 200 + z: 100 + transformOrigin: Item.Center + model: ["No Collision", + "Basic - Whole model", + "Good - Sub-meshes", + "Exact - All polygons"] + } + + Row { + id: row3 + width: 200 + height: 400 + spacing: 5 + + anchors { + rightMargin: 15 + } + Button { + id: button1 + text: qsTr("Add") + z: -1 + onClicked: { + newModelDialog.sendToScript({ + method: "newModelDialogAdd", + params: { + textInput: modelURL.text, + checkBox: dynamic.checked, + comboBox: collisionType.currentIndex + } + }); + } + } + + Button { + id: button2 + z: -1 + text: qsTr("Cancel") + onClicked: { + newModelDialog.sendToScript({method: "newModelDialogCancel"}) + } + } + } + } + } + } + + Keyboard { + id: keyboard + raised: parent.keyboardEnabled + numeric: parent.punctuationMode + anchors { + bottom: parent.bottom + left: parent.left + right: parent.right + } + } +} diff --git a/interface/resources/qml/hifi/tablet/Tablet.qml b/interface/resources/qml/hifi/tablet/Tablet.qml index 3fb70f9cca..8ad6339d88 100644 --- a/interface/resources/qml/hifi/tablet/Tablet.qml +++ b/interface/resources/qml/hifi/tablet/Tablet.qml @@ -202,7 +202,7 @@ Item { RalewaySemiBold { id: usernameText - text: tablet.parent.parent.username + text: tabletRoot.username anchors.verticalCenter: parent.verticalCenter anchors.right: parent.right anchors.rightMargin: 20 diff --git a/interface/resources/qml/hifi/tablet/TabletAddressDialog.qml b/interface/resources/qml/hifi/tablet/TabletAddressDialog.qml index 952a1f7faa..39892f27a4 100644 --- a/interface/resources/qml/hifi/tablet/TabletAddressDialog.qml +++ b/interface/resources/qml/hifi/tablet/TabletAddressDialog.qml @@ -1,7 +1,7 @@ // // TabletAddressDialog.qml // -// Created by Dante Ruiz on 2016/07/16 +// Created by Dante Ruiz on 2017/03/16 // Copyright 2015 High Fidelity, Inc. // // Distributed under the Apache License, Version 2.0. @@ -9,7 +9,8 @@ // import Hifi 1.0 -import QtQuick 2.4 +import QtQuick 2.5 +import QtQuick.Controls 1.4 import QtGraphicalEffects 1.0 import "../../controls" import "../../styles" @@ -19,25 +20,38 @@ import "../toolbars" import "../../styles-uit" as HifiStyles import "../../controls-uit" as HifiControls -Item { +StackView { id: root HifiConstants { id: hifi } HifiStyles.HifiConstants { id: hifiStyleConstants } - - width: parent.width - height: parent.height - + initialItem: addressBarDialog + width: parent !== null ? parent.width : undefined + height: parent !== null ? parent.height : undefined + property var eventBridge; property var allStories: []; - property int cardWidth: 370; + property int cardWidth: 460; property int cardHeight: 320; property string metaverseBase: addressBarDialog.metaverseServerUrl + "/api/v1/"; + property var tablet: null; + property bool isDesktop: false; + Component { id: tabletStoryCard; TabletStoryCard {} } Component.onCompleted: { + root.currentItem.focus = true; + root.currentItem.forceActiveFocus(); + addressLine.focus = true; + addressLine.forceActiveFocus(); fillDestinations(); - updateLocationText(); + updateLocationText(false); root.parentChanged.connect(center); center(); + isDesktop = (typeof desktop !== "undefined"); + tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); + + if (desktop) { + root.title = "GOTO"; + } } Component.onDestruction: { root.parentChanged.disconnect(center); @@ -54,10 +68,14 @@ Item { } function goCard(targetString) { if (0 !== targetString.indexOf('hifi://')) { + var card = tabletStoryCard.createObject(); + card.setUrl(addressBarDialog.metaverseServerUrl + targetString); + card.eventBridge = root.eventBridge; + root.push(card); return; } - addressLine.text = targetString; - toggleOrGo(true); + location.text = targetString; + toggleOrGo(true, targetString); clearAddressLineTimer.start(); } @@ -83,38 +101,158 @@ Item { onMetaverseServerUrlChanged: updateLocationTextTimer.start(); Rectangle { - id: topBar - height: 90 - gradient: Gradient { - GradientStop { - position: 0 - color: "#2b2b2b" - - } + id: navBar + width: 480 + height: 70 + color: hifiStyleConstants.colors.white + anchors { + top: parent.top + right: parent.right + rightMargin: 0 + left: parent.left + leftMargin: 0 + } - GradientStop { - position: 1 - color: "#1e1e1e" + ToolbarButton { + id: homeButton + imageURL: "../../../images/home.svg" + onClicked: { + addressBarDialog.loadHome(); + tabletRoot.shown = false; + tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); + tablet.gotoHomeScreen(); + } + anchors { + left: parent.left + verticalCenter: parent.verticalCenter } } + ToolbarButton { + id: backArrow; + imageURL: "../../../images/backward.svg"; + onClicked: addressBarDialog.loadBack(); + anchors { + left: homeButton.right + verticalCenter: parent.verticalCenter + } + } + ToolbarButton { + id: forwardArrow; + imageURL: "../../../images/forward.svg"; + onClicked: addressBarDialog.loadForward(); + anchors { + left: backArrow.right + verticalCenter: parent.verticalCenter + } + } + } + + Rectangle { + id: addressBar + width: 480 + height: 70 + color: hifiStyleConstants.colors.white + anchors { + top: navBar.bottom + right: parent.right + rightMargin: 16 + left: parent.left + leftMargin: 16 + } + + property int inputAreaHeight: 70 + property int inputAreaStep: (height - inputAreaHeight) / 2 + + HifiStyles.RalewayLight { + id: notice; + font.pixelSize: hifi.fonts.pixelSize * 0.50; + anchors { + top: parent.top + topMargin: parent.inputAreaStep + 12 + left: addressLine.left + right: addressLine.right + } + } + HifiStyles.FiraSansRegular { + id: location; + font.pixelSize: addressLine.font.pixelSize; + color: "gray"; + clip: true; + anchors.fill: addressLine; + visible: addressLine.text.length === 0 + } + + TextInput { + id: addressLine + focus: true + anchors { + bottom: parent.bottom + left: parent.left + right: parent.right + leftMargin: 16 + rightMargin: 16 + topMargin: parent.inputAreaStep + (2 * hifi.layout.spacing) + bottomMargin: parent.inputAreaStep + } + font.pixelSize: hifi.fonts.pixelSize * 0.75 + cursorVisible: false + onTextChanged: { + filterChoicesByText(); + updateLocationText(text.length > 0); + if (!isCursorVisible && text.length > 0) { + isCursorVisible = true; + cursorVisible = true; + } + } + onAccepted: { + addressBarDialog.keyboardEnabled = false; + } + onActiveFocusChanged: { + cursorVisible = isCursorVisible && focus; + } + MouseArea { + // If user clicks in address bar show cursor to indicate ability to enter address. + anchors.fill: parent + onClicked: { + isCursorVisible = true; + parent.cursorVisible = true; + parent.focus = true; + parent.forceActiveFocus(); + addressBarDialog.keyboardEnabled = HMD.active + tabletRoot.playButtonClickSound(); + } + } + } + + Rectangle { + anchors.fill: addressLine + color: hifiStyleConstants.colors.lightGray + opacity: 0.1 + } + } + Rectangle { + id: topBar + height: 37 + color: hifiStyleConstants.colors.white anchors.right: parent.right anchors.rightMargin: 0 anchors.left: parent.left anchors.leftMargin: 0 anchors.topMargin: 0 - anchors.top: parent.top + anchors.top: addressBar.bottom Row { id: thing - spacing: 2 * hifi.layout.spacing + spacing: 5 * hifi.layout.spacing anchors { top: parent.top; left: parent.left + leftMargin: 25 } - TextButton { + TabletTextButton { id: allTab; text: "ALL"; property string includeActions: 'snapshot, concurrency'; @@ -122,7 +260,7 @@ Item { action: tabSelect; } - TextButton { + TabletTextButton { id: placeTab; text: "PLACES"; property string includeActions: 'concurrency'; @@ -131,7 +269,7 @@ Item { } - TextButton { + TabletTextButton { id: snapTab; text: "SNAP"; property string includeActions: 'snapshot'; @@ -144,21 +282,8 @@ Item { Rectangle { id: bgMain - gradient: Gradient { - GradientStop { - position: 0 - color: "#2b2b2b" - - } - - GradientStop { - position: 1 - color: "#0f212e" - } - } - - - anchors.bottom: backgroundImage.top + color: hifiStyleConstants.colors.white + anchors.bottom: parent.keyboardEnabled ? keyboard.top : parent.bottom anchors.bottomMargin: 0 anchors.right: parent.right anchors.rightMargin: 0 @@ -172,7 +297,7 @@ Item { ListView { id: scroll - property int stackedCardShadowHeight: 10; + property int stackedCardShadowHeight: 0; clip: true spacing: 14 anchors { @@ -180,8 +305,9 @@ Item { top: parent.top left: parent.left right: parent.right - leftMargin: 50 + leftMargin: 10 } + model: suggestions orientation: ListView.Vertical @@ -210,109 +336,7 @@ Item { } } - Rectangle { - id: backgroundImage - width: 480 - height: 70 - - gradient: Gradient { - GradientStop { - position: 0 - color: "#c2ced8" - - } - - GradientStop { - position: 1 - color: "#c2ced8" - } - } - - anchors { - bottom: parent.keyboardEnabled ? keyboard.top : parent.bottom - right: parent.right - left: parent.left - } - - - ToolbarButton { - id: homeButton - imageURL: "../../../images/home.svg" - onClicked: { - addressBarDialog.loadHome(); - root.shown = false; - } - anchors { - left: parent.left - leftMargin: homeButton.width / 2 - verticalCenter: parent.verticalCenter - } - } - property int inputAreaHeight: 70 - property int inputAreaStep: (height - inputAreaHeight) / 2 - - HifiStyles.RalewayLight { - id: notice; - font.pixelSize: hifi.fonts.pixelSize * 0.50; - anchors { - top: parent.top - topMargin: parent.inputAreaStep + 12 - left: addressLine.left - right: addressLine.right - } - } - HifiStyles.FiraSansRegular { - id: location; - font.pixelSize: addressLine.font.pixelSize; - color: "gray"; - clip: true; - anchors.fill: addressLine; - visible: addressLine.text.length === 0 - } - - TextInput { - id: addressLine - focus: true - anchors { - bottom: parent.bottom - left: homeButton.right - right: parent.right - leftMargin: homeButton.width - rightMargin: homeButton.width / 2 - topMargin: parent.inputAreaStep + (2 * hifi.layout.spacing) - bottomMargin: parent.inputAreaStep - } - font.pixelSize: hifi.fonts.pixelSize * 0.75 - cursorVisible: false - onTextChanged: { - filterChoicesByText(); - updateLocationText(text.length > 0); - if (!isCursorVisible && text.length > 0) { - isCursorVisible = true; - cursorVisible = true; - } - } - onAccepted: { - addressBarDialog.keyboardEnabled = false; - } - onActiveFocusChanged: { - cursorVisible = isCursorVisible && focus; - } - MouseArea { - // If user clicks in address bar show cursor to indicate ability to enter address. - anchors.fill: parent - onClicked: { - isCursorVisible = true; - //parent.cursorVisible = true; - parent.forceActiveFocus(); - addressBarDialog.keyboardEnabled = HMD.active - tabletRoot.playButtonClickSound(); - } - } - } - } - - Timer { + Timer { // Delay updating location text a bit to avoid flicker of content and so that connection status is valid. id: updateLocationTextTimer running: false @@ -529,18 +553,29 @@ Item { } } - function toggleOrGo(fromSuggestions) { + function toggleOrGo(fromSuggestions, address) { + if (address !== undefined && address !== "") { + addressBarDialog.loadAddress(address, fromSuggestions) + } + if (addressLine.text !== "") { addressBarDialog.loadAddress(addressLine.text, fromSuggestions) } - root.shown = false; + + if (isDesktop) { + tablet.gotoHomeScreen(); + } else { + HMD.closeTablet(); + } + + tabletRoot.shown = false; } Keys.onPressed: { switch (event.key) { case Qt.Key_Escape: case Qt.Key_Back: - root.shown = false + tabletRoot.shown = false clearAddressLineTimer.start(); event.accepted = true break diff --git a/interface/resources/qml/hifi/tablet/TabletAttachmentsDialog.qml b/interface/resources/qml/hifi/tablet/TabletAttachmentsDialog.qml new file mode 100644 index 0000000000..634c9d41ec --- /dev/null +++ b/interface/resources/qml/hifi/tablet/TabletAttachmentsDialog.qml @@ -0,0 +1,105 @@ +// +// TabletAttachmentsDialog.qml +// +// Created by David Rowe on 9 Mar 2017. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or https://www.apache.org/licenses/LICENSE-2.0.html +// + +import QtQuick 2.5 +import QtQuick.Controls 1.4 + +import "../../controls-uit" as HifiControls +import "../../styles-uit" +import "../dialogs/content" + +Item { + id: root + objectName: "AttachmentsDialog" + + property string title: "Avatar Attachments" + + property bool keyboardEnabled: false + property bool keyboardRaised: false + property bool punctuationMode: false + + property var eventBridge; + signal sendToScript(var message); + + anchors.fill: parent + + HifiConstants { id: hifi } + + Rectangle { + id: pane // Surrogate for ScrollingWindow's pane. + anchors.fill: parent + } + + function closeDialog() { + Tablet.getTablet("com.highfidelity.interface.tablet.system").gotoHomeScreen(); + } + + anchors.topMargin: hifi.dimensions.tabletMenuHeader // Space for header. + + HifiControls.TabletHeader { + id: header + title: root.title + + anchors { + left: parent.left + right: parent.right + bottom: parent.top + } + } + + AttachmentsContent { + id: attachments + + anchors { + top: header.bottom + left: parent.left + right: parent.right + bottom: keyboard.top + } + + MouseArea { + // Defocuses any current control so that the keyboard gets hidden. + id: defocuser + anchors.fill: parent + propagateComposedEvents: true + acceptedButtons: Qt.AllButtons + onPressed: { + parent.forceActiveFocus(); + mouse.accepted = false; + } + } + } + + HifiControls.Keyboard { + id: keyboard + raised: parent.keyboardEnabled && parent.keyboardRaised + numeric: parent.punctuationMode + anchors { + left: parent.left + right: parent.right + bottom: parent.bottom + } + } + + MouseArea { + id: activator + anchors.fill: parent + propagateComposedEvents: true + enabled: true + acceptedButtons: Qt.AllButtons + onPressed: { + mouse.accepted = false; + } + } + + Component.onCompleted: { + keyboardEnabled = HMD.active; + } +} diff --git a/interface/resources/qml/hifi/tablet/TabletAudioPreferences.qml b/interface/resources/qml/hifi/tablet/TabletAudioPreferences.qml new file mode 100644 index 0000000000..b21bc238ac --- /dev/null +++ b/interface/resources/qml/hifi/tablet/TabletAudioPreferences.qml @@ -0,0 +1,38 @@ +// +// TabletAudioPreferences.qml +// +// Created by Davd Rowe on 7 Mar 2017. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +import QtQuick 2.5 +import QtQuick.Controls 1.4 +import "tabletWindows" +import "../../dialogs" + +StackView { + id: profileRoot + initialItem: root + objectName: "stack" + property string title: "Audio Settings" + + property var eventBridge; + signal sendToScript(var message); + + function pushSource(path) { + profileRoot.push(Qt.reslovedUrl(path)); + } + + function popSource() { + profileRoot.pop(); + } + + TabletPreferencesDialog { + id: root + objectName: "TabletAudioPreferences" + showCategories: ["Audio"] + } +} diff --git a/interface/resources/qml/hifi/tablet/TabletAvatarPreferences.qml b/interface/resources/qml/hifi/tablet/TabletAvatarPreferences.qml new file mode 100644 index 0000000000..75973f32ae --- /dev/null +++ b/interface/resources/qml/hifi/tablet/TabletAvatarPreferences.qml @@ -0,0 +1,38 @@ +// +// TabletAvatarPreferences.qml +// +// Created by Davd Rowe on 2 Mar 2017. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +import QtQuick 2.5 +import QtQuick.Controls 1.4 +import "tabletWindows" +import "../../dialogs" + +StackView { + id: profileRoot + initialItem: root + objectName: "stack" + property string title: "Avatar Settings" + + property var eventBridge; + signal sendToScript(var message); + + function pushSource(path) { + profileRoot.push(Qt.reslovedUrl(path)); + } + + function popSource() { + profileRoot.pop(); + } + + TabletPreferencesDialog { + id: root + objectName: "TabletAvatarPreferences" + showCategories: ["Avatar Basics", "Avatar Tuning", "Avatar Camera"] + } +} diff --git a/interface/resources/qml/hifi/tablet/TabletGeneralSettings.qml b/interface/resources/qml/hifi/tablet/TabletGeneralPreferences.qml similarity index 68% rename from interface/resources/qml/hifi/tablet/TabletGeneralSettings.qml rename to interface/resources/qml/hifi/tablet/TabletGeneralPreferences.qml index b445e6a463..6fc29ac920 100644 --- a/interface/resources/qml/hifi/tablet/TabletGeneralSettings.qml +++ b/interface/resources/qml/hifi/tablet/TabletGeneralPreferences.qml @@ -1,44 +1,38 @@ // -// TabletGeneralSettings.qml -// scripts/system/ +// TabletGeneralPreferences.qml // // Created by Dante Ruiz on 9 Feb 2017 -// Copyright 2016 High Fidelity, Inc. +// Copyright 2017 High Fidelity, Inc. // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // import QtQuick 2.5 +import QtQuick.Controls 1.4 import "tabletWindows" import "../../dialogs" -import QtQuick.Controls 1.4 -import QtQuick.Controls.Styles 1.4 -import QtGraphicalEffects 1.0 StackView { id: profileRoot initialItem: root objectName: "stack" - + property string title: "General Settings" + property var eventBridge; signal sendToScript(var message); function pushSource(path) { - editRoot.push(Qt.reslovedUrl(path)); + profileRoot.push(Qt.reslovedUrl(path)); } function popSource() { - - } - - TabletPreferencesDialog { - id: root - objectName: "GeneralPreferencesDialog" - width: parent.width - height: parent.height - showCategories: ["UI", "Snapshots", "Scripts", "Privacy", "Octree", "HMD", "Sixense Controllers", "Perception Neuron", "Kinect"] - + profileRoot.pop(); } + TabletPreferencesDialog { + id: root + objectName: "TabletGeneralPreferences" + showCategories: ["UI", "Snapshots", "Scripts", "Privacy", "Octree", "HMD", "Sixense Controllers", "Perception Neuron", "Kinect"] + } } diff --git a/interface/resources/qml/hifi/tablet/TabletGraphicsPreferences.qml b/interface/resources/qml/hifi/tablet/TabletGraphicsPreferences.qml new file mode 100644 index 0000000000..67c466f991 --- /dev/null +++ b/interface/resources/qml/hifi/tablet/TabletGraphicsPreferences.qml @@ -0,0 +1,38 @@ +// +// TabletGraphicsPreferences.qml +// +// Created by Vlad Stelmahovsky on 12 Mar 2017. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +import QtQuick 2.5 +import QtQuick.Controls 1.4 +import "tabletWindows" +import "../../dialogs" + +StackView { + id: profileRoot + initialItem: root + objectName: "stack" + property string title: "Graphics Settings" + + property var eventBridge; + signal sendToScript(var message); + + function pushSource(path) { + profileRoot.push(Qt.reslovedUrl(path)); + } + + function popSource() { + profileRoot.pop(); + } + + TabletPreferencesDialog { + id: root + objectName: "TabletGraphicsPreferences" + showCategories: ["Graphics"] + } +} diff --git a/interface/resources/qml/hifi/tablet/TabletLodPreferences.qml b/interface/resources/qml/hifi/tablet/TabletLodPreferences.qml new file mode 100644 index 0000000000..f61f6f8c4e --- /dev/null +++ b/interface/resources/qml/hifi/tablet/TabletLodPreferences.qml @@ -0,0 +1,38 @@ +// +// TabletLodPreferences.qml +// +// Created by Vlad Stelmahovsky on 11 Mar 2017. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +import QtQuick 2.5 +import QtQuick.Controls 1.4 +import "tabletWindows" +import "../../dialogs" + +StackView { + id: profileRoot + initialItem: root + objectName: "stack" + property string title: "LOD Settings" + + property var eventBridge; + signal sendToScript(var message); + + function pushSource(path) { + profileRoot.push(Qt.reslovedUrl(path)); + } + + function popSource() { + profileRoot.pop(); + } + + TabletPreferencesDialog { + id: root + objectName: "TabletLodPreferences" + showCategories: ["Level of Detail Tuning"] + } +} diff --git a/interface/resources/qml/hifi/tablet/TabletMenu.qml b/interface/resources/qml/hifi/tablet/TabletMenu.qml index e0deab64b6..af36f72c82 100644 --- a/interface/resources/qml/hifi/tablet/TabletMenu.qml +++ b/interface/resources/qml/hifi/tablet/TabletMenu.qml @@ -2,8 +2,14 @@ import QtQuick 2.5 import QtGraphicalEffects 1.0 import QtQuick.Controls 1.4 import QtQml 2.2 +import QtWebChannel 1.0 +import QtWebEngine 1.1 +import HFWebEngineProfile 1.0 + + import "." import "../../styles-uit" +import "../../controls" FocusScope { id: tabletMenu @@ -13,10 +19,11 @@ FocusScope { height: 720 property var rootMenu: Menu { objectName:"rootMenu" } - property var point: Qt.point(50, 50) + property var point: Qt.point(50, 50); + TabletMenuStack { id: menuPopperUpper } property string subMenu: "" - - TabletMouseHandler { id: menuPopperUpper } + property var eventBridge; + signal sendToScript(var message); Rectangle { id: bgNavBar @@ -53,10 +60,11 @@ FocusScope { anchors.fill: parent hoverEnabled: true onEntered: iconColorOverlay.color = "#1fc6a6"; - onExited: iconColorOverlay.color = "#ffffff"; + onExited: iconColorOverlay.color = "#34a2c7"; // navigate back to root level menu onClicked: { buildMenu(); + breadcrumbText.text = "Menu"; tabletRoot.playButtonClickSound(); } } @@ -97,6 +105,7 @@ FocusScope { menuPopperUpper.closeLastMenu(); } + function setRootMenu(rootMenu, subMenu) { tabletMenu.subMenu = subMenu; tabletMenu.rootMenu = rootMenu; @@ -116,12 +125,12 @@ FocusScope { } subMenu = ""; // Continue with full menu after initially displaying submenu. if (found) { - menuPopperUpper.popup(tabletMenu, rootMenu.items[index].items); + menuPopperUpper.popup(rootMenu.items[index].items); return; } } // Otherwise build whole menu. - menuPopperUpper.popup(tabletMenu, rootMenu.items); + menuPopperUpper.popup(rootMenu.items); } } diff --git a/interface/resources/qml/hifi/tablet/TabletMenuItem.qml b/interface/resources/qml/hifi/tablet/TabletMenuItem.qml index c9223650f8..25f672e7a9 100644 --- a/interface/resources/qml/hifi/tablet/TabletMenuItem.qml +++ b/interface/resources/qml/hifi/tablet/TabletMenuItem.qml @@ -32,8 +32,7 @@ Item { anchors { left: parent.left leftMargin: hifi.dimensions.menuPadding.x + 15 - top: label.top - topMargin: 0 + verticalCenter: label.verticalCenter } width: 20 visible: source.visible && source.type === 1 && source.checkable @@ -51,6 +50,8 @@ Item { RalewaySemiBold { id: label size: 20 + //wrap will work only if width is set + width: parent.width - (check.width + check.anchors.leftMargin) - tail.width font.capitalization: isSubMenu ? Font.MixedCase : Font.AllUppercase anchors.left: check.right anchors.verticalCenter: parent.verticalCenter @@ -58,6 +59,7 @@ Item { color: source.enabled ? hifi.colors.baseGrayShadow : hifi.colors.baseGrayShadow50 enabled: source.visible && (source.type !== 0 ? source.enabled : false) visible: source.visible + wrapMode: Text.WordWrap } Item { diff --git a/interface/resources/qml/hifi/tablet/TabletMouseHandler.qml b/interface/resources/qml/hifi/tablet/TabletMenuStack.qml similarity index 79% rename from interface/resources/qml/hifi/tablet/TabletMouseHandler.qml rename to interface/resources/qml/hifi/tablet/TabletMenuStack.qml index 17a00eccde..bacc11228e 100644 --- a/interface/resources/qml/hifi/tablet/TabletMouseHandler.qml +++ b/interface/resources/qml/hifi/tablet/TabletMenuStack.qml @@ -1,7 +1,7 @@ // // MessageDialog.qml // -// Created by Bradley Austin Davis on 18 Jan 2016 +// Created by Dante Ruiz on 13 Feb 2017 // Copyright 2016 High Fidelity, Inc. // // Distributed under the Apache License, Version 2.0. @@ -18,18 +18,12 @@ Item { anchors.fill: parent objectName: "tabletMenuHandlerItem" - MouseArea { - id: menuRoot; - objectName: "tabletMenuHandlerMouseArea" + StackView { anchors.fill: parent - enabled: d.topMenu !== null - onClicked: { - d.clearMenus(); - } - } - - QtObject { id: d + objectName: "stack" + initialItem: topMenu + property var menuStack: [] property var topMenu: null; property var modelMaker: Component { ListModel { } } @@ -53,6 +47,24 @@ Item { } } + function pushSource(path) { + d.push(Qt.resolvedUrl(path)); + d.currentItem.eventBridge = tabletMenu.eventBridge + d.currentItem.sendToScript.connect(tabletMenu.sendToScript); + d.currentItem.focus = true; + d.currentItem.forceActiveFocus(); + breadcrumbText.text = d.currentItem.title; + if (typeof bgNavBar !== "undefined") { + d.currentItem.y = bgNavBar.height; + d.currentItem.height -= bgNavBar.height; + } + } + + function popSource() { + console.log("trying to pop page"); + d.pop(); + } + function toModel(items) { var result = modelMaker.createObject(tabletMenu); for (var i = 0; i < items.length; ++i) { @@ -76,22 +88,18 @@ Item { } function popMenu() { - if (menuStack.length) { - menuStack.pop().destroy(); + if (d.depth) { + d.pop(); } - if (menuStack.length) { - topMenu = menuStack[menuStack.length - 1]; + if (d.depth) { + topMenu = d.currentItem; topMenu.focus = true; topMenu.forceActiveFocus(); // show current menu level on nav bar - if (topMenu.objectName === "") { + if (topMenu.objectName === "" || d.depth === 1) { breadcrumbText.text = "Menu"; } else { - if (menuStack.length === 1) { - breadcrumbText.text = "Menu"; - } else { - breadcrumbText.text = topMenu.objectName; - } + breadcrumbText.text = topMenu.objectName; } } else { breadcrumbText.text = "Menu"; @@ -100,16 +108,14 @@ Item { } function pushMenu(newMenu) { - menuStack.push(newMenu); + d.push({ item:newMenu, destroyOnPop: true}); topMenu = newMenu; topMenu.focus = true; topMenu.forceActiveFocus(); } function clearMenus() { - while (menuStack.length) { - popMenu() - } + d.clear() } function clampMenuPosition(menu) { @@ -127,7 +133,7 @@ Item { } } - function buildMenu(items, targetPosition) { + function buildMenu(items) { var model = toModel(items); // Menus must be childed to desktop for Z-ordering var newMenu = menuViewMaker.createObject(tabletMenu, { model: model, isSubMenu: topMenu !== null }); @@ -158,13 +164,13 @@ Item { } - function popup(parent, items) { + function popup(items) { d.clearMenus(); - d.buildMenu(items, point); + d.buildMenu(items); } function closeLastMenu() { - if (d.menuStack.length > 1) { + if (d.depth > 1) { d.popMenu(); return true; } diff --git a/interface/resources/qml/hifi/tablet/TabletModelBrowserDialog.qml b/interface/resources/qml/hifi/tablet/TabletModelBrowserDialog.qml new file mode 100644 index 0000000000..60bd7a88e0 --- /dev/null +++ b/interface/resources/qml/hifi/tablet/TabletModelBrowserDialog.qml @@ -0,0 +1,87 @@ +// +// TabletModelBrowserDialog.qml +// +// Created by David Rowe on 11 Mar 2017. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or https://www.apache.org/licenses/LICENSE-2.0.html +// + +import QtQuick 2.5 +import QtQuick.Controls 1.4 + +import "../../controls-uit" as HifiControls +import "../../styles-uit" +import "../dialogs/content" + +Item { + id: root + objectName: "ModelBrowserDialog" + + property string title: "Attachment Model" + + property var result + + signal selected(var modelUrl) + signal canceled() + + property bool keyboardEnabled: false + property bool keyboardRaised: false + property bool punctuationMode: false + + anchors.fill: parent + + Rectangle { + id: pane // Surrogate for ScrollingWindow's pane. + anchors.fill: parent + } + + anchors.topMargin: hifi.dimensions.tabletMenuHeader // Space for header. + + HifiControls.TabletHeader { + id: header + title: parent.title + + anchors { + left: parent.left + right: parent.right + bottom: parent.top + } + } + + ModelBrowserContent { + anchors { + top: header.bottom + left: parent.left + right: parent.right + bottom: keyboard.top + } + } + + HifiControls.Keyboard { + id: keyboard + raised: parent.keyboardEnabled && parent.keyboardRaised + numeric: parent.punctuationMode + anchors { + left: parent.left + right: parent.right + bottom: parent.bottom + } + } + + MouseArea { + id: activator + anchors.fill: parent + propagateComposedEvents: true + enabled: true + acceptedButtons: Qt.AllButtons + onPressed: { + mouse.accepted = false; + } + } + + Component.onCompleted: { + keyboardEnabled = HMD.active; + } +} diff --git a/interface/resources/qml/hifi/tablet/TabletNetworkingPreferences.qml b/interface/resources/qml/hifi/tablet/TabletNetworkingPreferences.qml new file mode 100644 index 0000000000..db47c78c48 --- /dev/null +++ b/interface/resources/qml/hifi/tablet/TabletNetworkingPreferences.qml @@ -0,0 +1,38 @@ +// +// TabletNetworkingPreferences.qml +// +// Created by Davd Rowe on 7 Mar 2017. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +import QtQuick 2.5 +import QtQuick.Controls 1.4 +import "tabletWindows" +import "../../dialogs" + +StackView { + id: profileRoot + initialItem: root + objectName: "stack" + property var title: "Networking Settings" + + property var eventBridge; + signal sendToScript(var message); + + function pushSource(path) { + profileRoot.push(Qt.reslovedUrl(path)); + } + + function popSource() { + profileRoot.pop(); + } + + TabletPreferencesDialog { + id: root + objectName: "TabletNetworkingPreferences" + showCategories: ["Networking"] + } +} diff --git a/interface/resources/qml/hifi/tablet/TabletRoot.qml b/interface/resources/qml/hifi/tablet/TabletRoot.qml index 1fb31e5619..f5144e698f 100644 --- a/interface/resources/qml/hifi/tablet/TabletRoot.qml +++ b/interface/resources/qml/hifi/tablet/TabletRoot.qml @@ -1,26 +1,60 @@ import QtQuick 2.0 import Hifi 1.0 +import QtQuick.Controls 1.4 +import "../../dialogs" Item { id: tabletRoot objectName: "tabletRoot" property string username: "Unknown user" property var eventBridge; - property var rootMenu; + property var openModal: null; + property var openMessage: null; property string subMenu: "" - signal showDesktop(); + property bool shown: true function setOption(value) { option = value; } + Component { id: inputDialogBuilder; TabletQueryDialog { } } + function inputDialog(properties) { + openModal = inputDialogBuilder.createObject(tabletRoot, properties); + return openModal; + } + Component { id: messageBoxBuilder; TabletMessageBox { } } + function messageBox(properties) { + openMessage = messageBoxBuilder.createObject(tabletRoot, properties); + return openMessage; + } + + Component { id: customInputDialogBuilder; TabletCustomQueryDialog { } } + function customInputDialog(properties) { + openModal = customInputDialogBuilder.createObject(tabletRoot, properties); + return openModal; + } + + Component { id: fileDialogBuilder; TabletFileDialog { } } + function fileDialog(properties) { + openModal = fileDialogBuilder.createObject(tabletRoot, properties); + return openModal; + } + function setMenuProperties(rootMenu, subMenu) { tabletRoot.rootMenu = rootMenu; tabletRoot.subMenu = subMenu; } + function isDialogOpen() { + if (openMessage !== null || openModal !== null) { + return true; + } + + return false; + } + function loadSource(url) { loader.source = ""; // make sure we load the qml fresh each time. loader.source = url; @@ -68,6 +102,7 @@ Item { objectName: "loader" asynchronous: false + width: parent.width height: parent.height @@ -89,6 +124,12 @@ Item { loader.item.setRootMenu(tabletRoot.rootMenu, tabletRoot.subMenu); } loader.item.forceActiveFocus(); + + if (openModal) { + openModal.canceled(); + openModal.destroy(); + openModal = null; + } } } diff --git a/interface/resources/qml/hifi/tablet/TabletStoryCard.qml b/interface/resources/qml/hifi/tablet/TabletStoryCard.qml new file mode 100644 index 0000000000..1d57c8a083 --- /dev/null +++ b/interface/resources/qml/hifi/tablet/TabletStoryCard.qml @@ -0,0 +1,46 @@ +// +// TabletAddressDialog.qml +// +// Created by Dante Ruiz on 2017/04/24 +// Copyright 2015 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +import Hifi 1.0 +import QtQuick 2.4 +import QtGraphicalEffects 1.0 +import "../../controls" +import "../../styles" +import "../../windows" +import "../" +import "../toolbars" +import "../../styles-uit" as HifiStyles +import "../../controls-uit" as HifiControlsUit +import "../../controls" as HifiControls + + +Rectangle { + id: cardRoot + HifiStyles.HifiConstants { id: hifi } + width: parent.width + height: parent.height + property string address: "" + property alias eventBridge: webview.eventBridge + function setUrl(url) { + cardRoot.address = url; + webview.url = url; + } + + HifiControls.TabletWebView { + id: webview + parentStackItem: root + anchors { + top: parent.top + right: parent.right + left: parent.left + bottom: parent.bottom + } + } +} diff --git a/interface/resources/qml/hifi/tablet/tabletWindows/TabletFileDialog.qml b/interface/resources/qml/hifi/tablet/tabletWindows/TabletFileDialog.qml index a3e94152b8..26e35c4dcf 100644 --- a/interface/resources/qml/hifi/tablet/tabletWindows/TabletFileDialog.qml +++ b/interface/resources/qml/hifi/tablet/tabletWindows/TabletFileDialog.qml @@ -145,7 +145,7 @@ Item { } } - TabletComboBox { + ComboBox { id: pathSelector anchors { top: parent.top diff --git a/interface/resources/qml/hifi/tablet/tabletWindows/TabletPreferencesDialog.qml b/interface/resources/qml/hifi/tablet/tabletWindows/TabletPreferencesDialog.qml index 7d214237a3..c96099a78a 100644 --- a/interface/resources/qml/hifi/tablet/tabletWindows/TabletPreferencesDialog.qml +++ b/interface/resources/qml/hifi/tablet/tabletWindows/TabletPreferencesDialog.qml @@ -21,18 +21,28 @@ import "../../../controls-uit" as HifiControls Item { id: dialog - width: 480 - height: 720 + width: parent.width + height: parent.height HifiConstants { id: hifi } property var sections: [] property var showCategories: [] + + property bool keyboardEnabled: false + property bool keyboardRaised: false + property bool punctuationMode: false + + property var tablet; function saveAll() { + dialog.forceActiveFocus(); // Accept any text box edits in progress. + for (var i = 0; i < sections.length; ++i) { var section = sections[i]; section.saveAll(); } + + closeDialog(); } function restoreAll() { @@ -40,29 +50,25 @@ Item { var section = sections[i]; section.restoreAll(); } + + closeDialog(); } - + + function closeDialog() { + Tablet.getTablet("com.highfidelity.interface.tablet.system").gotoHomeScreen(); + } + Rectangle { id: main - height: parent.height - 40 anchors { top: parent.top bottom: footer.top left: parent.left right: parent.right } - gradient: Gradient { - GradientStop { - position: 0 - color: "#2b2b2b" - - } - - GradientStop { - position: 1 - color: "#0f212e" - } - } + + color: hifi.colors.baseGray + Flickable { id: scrollView width: parent.width @@ -110,9 +116,7 @@ Item { } scrollView.contentHeight = scrollView.getSectionsHeight(); - } - Column { id: prefControls @@ -131,32 +135,39 @@ Item { } } + MouseArea { + // Defocuses the current control so that the HMD keyboard gets hidden. + // Created under the footer so that the non-button part of the footer can defocus a control. + id: mouseArea + anchors { + top: parent.top + left: parent.left + right: parent.right + bottom: keyboard.top + } + propagateComposedEvents: true + acceptedButtons: Qt.AllButtons + onPressed: { + parent.forceActiveFocus(); + mouse.accepted = false; + } + } + Rectangle { id: footer height: 40 anchors { - top: main.bottom - bottom: parent.bottom + bottom: keyboard.top left: parent.left right: parent.right } - gradient: Gradient { - GradientStop { - position: 0 - color: "#2b2b2b" - - } - GradientStop { - position: 1 - color: "#0f212e" - } - } + color: hifi.colors.baseGray Row { anchors { - top: parent,top + verticalCenter: parent.verticalCenter right: parent.right rightMargin: hifi.dimensions.contentMargin.x } @@ -165,15 +176,39 @@ Item { HifiControls.Button { text: "Save changes" color: hifi.buttons.blue - onClicked: root.saveAll() + onClicked: dialog.saveAll() } HifiControls.Button { text: "Cancel" color: hifi.buttons.white - onClicked: root.restoreAll() + onClicked: dialog.restoreAll() + } + } + } + + HifiControls.Keyboard { + id: keyboard + raised: parent.keyboardEnabled && parent.keyboardRaised + numeric: parent.punctuationMode + anchors { + bottom: parent.bottom + left: parent.left + right: parent.right + } + } + + Component.onCompleted: { + tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); + keyboardEnabled = HMD.active; + } + + onKeyboardRaisedChanged: { + if (keyboardEnabled && keyboardRaised) { + var delta = mouseArea.mouseY - (dialog.height - footer.height - keyboard.raisedHeight -hifi.dimensions.controlLineHeight); + if (delta > 0) { + scrollView.contentY += delta; } } } - } diff --git a/interface/resources/qml/hifi/tablet/tabletWindows/preferences/Section.qml b/interface/resources/qml/hifi/tablet/tabletWindows/preferences/Section.qml index 3d6dfa10ce..9dd0956000 100644 --- a/interface/resources/qml/hifi/tablet/tabletWindows/preferences/Section.qml +++ b/interface/resources/qml/hifi/tablet/tabletWindows/preferences/Section.qml @@ -89,14 +89,17 @@ Preference { if (categoryPreferences) { console.log("Category " + root.name + " with " + categoryPreferences.length + " preferences"); for (var j = 0; j < categoryPreferences.length; ++j) { - buildPreference(categoryPreferences[j]); + //provide component position within column + //lowest numbers on top + buildPreference(categoryPreferences[j], j); } } } - function buildPreference(preference) { + function buildPreference(preference, itemNum) { console.log("\tPreference type " + preference.type + " name " + preference.name) var builder; + var zpos; switch (preference.type) { case Preference.Editable: checkBoxCount = 0; @@ -136,11 +139,14 @@ Preference { case Preference.ComboBox: checkBoxCount = 0; builder = comboBoxBuilder; + //make sure that combo boxes sitting higher will have higher z coordinate + //to be not overlapped when drop down is active + zpos = root.z + 1000 - itemNum break; }; if (builder) { - preferences.push(builder.createObject(contentContainer, { preference: preference, isFirstCheckBox: (checkBoxCount === 1) })); + preferences.push(builder.createObject(contentContainer, { preference: preference, isFirstCheckBox: (checkBoxCount === 1) , z: zpos})); } } } diff --git a/interface/resources/qml/hifi/tablet/tabletWindows/preferences/TabletAvatarBrowser.qml b/interface/resources/qml/hifi/tablet/tabletWindows/preferences/TabletAvatarBrowser.qml new file mode 100644 index 0000000000..029cf7d46b --- /dev/null +++ b/interface/resources/qml/hifi/tablet/tabletWindows/preferences/TabletAvatarBrowser.qml @@ -0,0 +1,116 @@ +// +// TabletAvatarBrowser.qml +// +// Created by David Rowe on 14 Mar 2017 +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +import QtQuick 2.5 +import QtQuick.Controls 1.4 +import QtWebChannel 1.0 +import QtWebEngine 1.2 + +import "../../../../windows" +import "../../../../controls-uit" +import "../../../../styles-uit" + +Item { + id: root + objectName: "ModelBrowserDialog" + + property string title: "Attachment Model" + + property bool keyboardEnabled: false + property bool keyboardRaised: false + property bool punctuationMode: false + + property alias eventBridge: eventBridgeWrapper.eventBridge + + anchors.fill: parent + + BaseWebView { + id: webview + url: "https://metaverse.highfidelity.com/marketplace?category=avatars" + focus: true + + anchors { + top: parent.top + left: parent.left + right: parent.right + bottom: footer.top + } + + QtObject { + id: eventBridgeWrapper + WebChannel.id: "eventBridgeWrapper" + property var eventBridge; + } + + webChannel.registeredObjects: [eventBridgeWrapper] + + // Create a global EventBridge object for raiseAndLowerKeyboard. + WebEngineScript { + id: createGlobalEventBridge + sourceCode: eventBridgeJavaScriptToInject + injectionPoint: WebEngineScript.DocumentCreation + worldId: WebEngineScript.MainWorld + } + + // Detect when may want to raise and lower keyboard. + WebEngineScript { + id: raiseAndLowerKeyboard + injectionPoint: WebEngineScript.Deferred + sourceUrl: resourceDirectoryUrl + "html/raiseAndLowerKeyboard.js" + worldId: WebEngineScript.MainWorld + } + + userScripts: [ createGlobalEventBridge, raiseAndLowerKeyboard ] + } + + Rectangle { + id: footer + height: 40 + + anchors { + left: parent.left + right: parent.right + bottom: keyboard.top + } + + color: hifi.colors.baseGray + + Row { + anchors { + verticalCenter: parent.verticalCenter + right: parent.right + rightMargin: hifi.dimensions.contentMargin.x + } + + Button { + text: "Cancel" + color: hifi.buttons.white + onClicked: root.destroy(); + } + } + } + + Keyboard { + id: keyboard + + raised: parent.keyboardEnabled && parent.keyboardRaised + numeric: parent.punctuationMode + + anchors { + left: parent.left + right: parent.right + bottom: parent.bottom + } + } + + Component.onCompleted: { + keyboardEnabled = HMD.active; + } +} diff --git a/interface/resources/qml/hifi/toolbars/StateImage.qml b/interface/resources/qml/hifi/toolbars/StateImage.qml index e0389c5e02..ebf1544f2b 100644 --- a/interface/resources/qml/hifi/toolbars/StateImage.qml +++ b/interface/resources/qml/hifi/toolbars/StateImage.qml @@ -12,7 +12,10 @@ Item { property bool pinned: false clip: true - function updateYOffset() { yOffset = size * buttonState; } + function updateYOffset() { + //make sure offset not set outside image + yOffset = (size * buttonState >= image.height) ? image.height - size : size * buttonState + } onButtonStateChanged: updateYOffset(); Component.onCompleted: { diff --git a/interface/resources/qml/hifi/toolbars/Toolbar.qml b/interface/resources/qml/hifi/toolbars/Toolbar.qml index c0d984e822..9015c026b0 100644 --- a/interface/resources/qml/hifi/toolbars/Toolbar.qml +++ b/interface/resources/qml/hifi/toolbars/Toolbar.qml @@ -49,7 +49,6 @@ Window { id: content implicitHeight: horizontal ? row.height : column.height implicitWidth: horizontal ? row.width : column.width - property bool wasVisibleBeforeBeingPinned: false Row { id: row @@ -62,18 +61,6 @@ Window { } Component { id: toolbarButtonBuilder; ToolbarButton { } } - - Connections { - target: desktop - onPinnedChanged: { - if (desktop.pinned) { - content.wasVisibleBeforeBeingPinned = window.visible; - window.visible = false; - } else { - window.visible = content.wasVisibleBeforeBeingPinned; - } - } - } } @@ -120,8 +107,6 @@ Window { function addButton(properties) { properties = properties || {} - unpinnedAlpha = 1; - // If a name is specified, then check if there's an existing button with that name // and return it if so. This will allow multiple clients to listen to a single button, // and allow scripts to be idempotent so they don't duplicate buttons if they're reloaded @@ -138,10 +123,11 @@ Window { buttons.push(result); result.opacity = 1; - updatePinned(); sortButtons(); + shown = true; + return result; } @@ -151,24 +137,12 @@ Window { console.warn("Tried to remove non-existent button " + name); return; } + buttons[index].destroy(); buttons.splice(index, 1); - updatePinned(); if (buttons.length === 0) { - unpinnedAlpha = 0; + shown = false; } } - - function updatePinned() { - var newPinned = false; - for (var i in buttons) { - var child = buttons[i]; - if (child.pinned) { - newPinned = true; - break; - } - } - pinned = newPinned; - } } diff --git a/interface/resources/qml/styles-uit/FiraSansSemiBold.qml b/interface/resources/qml/styles-uit/FiraSansSemiBold.qml index ddbeff7d90..b3f3324090 100644 --- a/interface/resources/qml/styles-uit/FiraSansSemiBold.qml +++ b/interface/resources/qml/styles-uit/FiraSansSemiBold.qml @@ -14,7 +14,7 @@ import QtQuick.Controls.Styles 1.4 Text { id: root - FontLoader { id: firaSansSemiBold; source: "../../fonts/FiraSans-SemiBold.ttf"; } + FontLoader { id: firaSansSemiBold; source: pathToFonts + "fonts/FiraSans-SemiBold.ttf"; } property real size: 32 font.pixelSize: size verticalAlignment: Text.AlignVCenter diff --git a/interface/resources/qml/styles-uit/HiFiGlyphs.qml b/interface/resources/qml/styles-uit/HiFiGlyphs.qml index d0dae746be..cbd6fa1d68 100644 --- a/interface/resources/qml/styles-uit/HiFiGlyphs.qml +++ b/interface/resources/qml/styles-uit/HiFiGlyphs.qml @@ -14,7 +14,7 @@ import QtQuick.Controls.Styles 1.4 Text { id: root - FontLoader { id: hiFiGlyphs; source: "../../fonts/hifi-glyphs.ttf"; } + FontLoader { id: hiFiGlyphs; source: pathToFonts + "fonts/hifi-glyphs.ttf"; } property int size: 32 font.pixelSize: size width: size diff --git a/interface/resources/qml/styles-uit/HifiConstants.qml b/interface/resources/qml/styles-uit/HifiConstants.qml index 031e80283e..7b6efbd573 100644 --- a/interface/resources/qml/styles-uit/HifiConstants.qml +++ b/interface/resources/qml/styles-uit/HifiConstants.qml @@ -70,6 +70,10 @@ Item { readonly property color indigoAccent: "#9495FF" readonly property color magentaHighlight: "#EF93D1" readonly property color magentaAccent: "#A2277C" + readonly property color checkboxCheckedRed: "#FF0000" + readonly property color checkboxCheckedBorderRed: "#D00000" + readonly property color lightBlueHighlight: "#d6f6ff" + // Semitransparent readonly property color darkGray30: "#4d121212" readonly property color darkGray0: "#00121212" @@ -159,6 +163,7 @@ Item { readonly property vector2d menuPadding: Qt.vector2d(14, 102) readonly property real scrollbarBackgroundWidth: 18 readonly property real scrollbarHandleWidth: scrollbarBackgroundWidth - 2 + readonly property real tabletMenuHeader: 90 } Item { @@ -171,7 +176,7 @@ Item { readonly property real textFieldInputLabel: dimensions.largeScreen ? 13 : 9 readonly property real textFieldSearchIcon: dimensions.largeScreen ? 30 : 24 readonly property real tableHeading: dimensions.largeScreen ? 12 : 10 - readonly property real tableHeadingIcon: dimensions.largeScreen ? 40 : 33 + readonly property real tableHeadingIcon: dimensions.largeScreen ? 60 : 33 readonly property real tableText: dimensions.largeScreen ? 15 : 12 readonly property real buttonLabel: dimensions.largeScreen ? 13 : 9 readonly property real iconButton: dimensions.largeScreen ? 13 : 9 diff --git a/interface/resources/qml/styles-uit/RalewayBold.qml b/interface/resources/qml/styles-uit/RalewayBold.qml index 97a6a4c208..433fdb7ae6 100644 --- a/interface/resources/qml/styles-uit/RalewayBold.qml +++ b/interface/resources/qml/styles-uit/RalewayBold.qml @@ -14,7 +14,7 @@ import QtQuick.Controls.Styles 1.4 Text { id: root - FontLoader { id: ralewayBold; source: "../../fonts/Raleway-Bold.ttf"; } + FontLoader { id: ralewayBold; source: pathToFonts + "fonts/Raleway-Bold.ttf"; } property real size: 32 font.pixelSize: size verticalAlignment: Text.AlignVCenter diff --git a/interface/resources/qml/styles-uit/RalewayRegular.qml b/interface/resources/qml/styles-uit/RalewayRegular.qml index 1ed5f122dc..2cffeeb59d 100644 --- a/interface/resources/qml/styles-uit/RalewayRegular.qml +++ b/interface/resources/qml/styles-uit/RalewayRegular.qml @@ -14,7 +14,7 @@ import QtQuick.Controls.Styles 1.4 Text { id: root - FontLoader { id: ralewayRegular; source: "../../fonts/Raleway-Regular.ttf"; } + FontLoader { id: ralewayRegular; source: pathToFonts + "fonts/Raleway-Regular.ttf"; } property real size: 32 font.pixelSize: size verticalAlignment: Text.AlignVCenter diff --git a/interface/resources/qml/styles-uit/RalewaySemiBold.qml b/interface/resources/qml/styles-uit/RalewaySemiBold.qml index 3c36a872a4..b6c79e02a4 100644 --- a/interface/resources/qml/styles-uit/RalewaySemiBold.qml +++ b/interface/resources/qml/styles-uit/RalewaySemiBold.qml @@ -14,7 +14,7 @@ import QtQuick.Controls.Styles 1.4 Text { id: root - FontLoader { id: ralewaySemiBold; source: "../../fonts/Raleway-SemiBold.ttf"; } + FontLoader { id: ralewaySemiBold; source: pathToFonts + "fonts/Raleway-SemiBold.ttf"; } property real size: 32 font.pixelSize: size verticalAlignment: Text.AlignVCenter diff --git a/interface/resources/qml/windows/TabletModalFrame.qml b/interface/resources/qml/windows/TabletModalFrame.qml new file mode 100644 index 0000000000..550eec8357 --- /dev/null +++ b/interface/resources/qml/windows/TabletModalFrame.qml @@ -0,0 +1,89 @@ +// +// ModalFrame.qml +// +// Created by Bradley Austin Davis on 15 Jan 2016 +// Copyright 2015 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +import QtQuick 2.5 + +import "." +import "../controls-uit" +import "../styles-uit" + + +Rectangle { + HifiConstants { id: hifi } + + id: frameContent + + readonly property bool hasTitle: root.title != "" + + readonly property int frameMarginLeft: hifi.dimensions.modalDialogMargin.x + readonly property int frameMarginRight: hifi.dimensions.modalDialogMargin.x + readonly property int frameMarginTop: hifi.dimensions.modalDialogMargin.y + (frameContent.hasTitle ? hifi.dimensions.modalDialogTitleHeight + 10 : 0) + readonly property int frameMarginBottom: hifi.dimensions.modalDialogMargin.y + + border { + width: hifi.dimensions.borderWidth + color: hifi.colors.lightGrayText80 + } + + radius: hifi.dimensions.borderRadius + color: hifi.colors.faintGray + Item { + id: frameTitle + visible: frameContent.hasTitle + + anchors { + horizontalCenter: parent.horizontalCenter + verticalCenter: parent.verticalCenter + fill: parent + topMargin: frameMarginTop + leftMargin: frameMarginLeft + rightMargin: frameMarginRight + //bottomMargin: frameMarginBottom + } + + Item { + width: title.width + (icon.text !== "" ? icon.width + hifi.dimensions.contentSpacing.x : 20) + + onWidthChanged: root.titleWidth = width + + HiFiGlyphs { + id: icon + text: root.iconText ? root.iconText : "" + size: root.iconSize ? root.iconSize : 30 + color: hifi.colors.lightGray + visible: true + anchors.verticalCenter: title.verticalCenter + anchors.leftMargin: 50 + anchors.left: parent.left + } + + RalewayRegular { + id: title + text: root.title + elide: Text.ElideRight + color: hifi.colors.baseGrayHighlight + size: hifi.fontSizes.overlayTitle + y: -hifi.dimensions.modalDialogTitleHeight + anchors.rightMargin: -50 + anchors.right: parent.right + //anchors.horizontalCenter: parent.horizontalCenter + } + } + + Rectangle { + anchors.left: parent.left + anchors.right: parent.right + height: 1 + color: hifi.colors.lightGray + } + + } + +} diff --git a/interface/resources/qml/windows/TabletModalWindow.qml b/interface/resources/qml/windows/TabletModalWindow.qml new file mode 100644 index 0000000000..05f192f7a7 --- /dev/null +++ b/interface/resources/qml/windows/TabletModalWindow.qml @@ -0,0 +1,22 @@ +// +// ModalWindow.qml +// +// Created by Bradley Austin Davis on 22 Jan 2016 +// Copyright 2015 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// +import QtQuick 2.5 +import QtQuick.Controls 1.4 +import QtQuick.Dialogs 1.2 as OriginalDialogs +import "." + +Rectangle { + id: modalWindow + layer.enabled: true + property var title: "Modal" + width: tabletRoot.width + height: tabletRoot.height + color: "#80000000" +} diff --git a/interface/resources/qml/windows/Window.qml b/interface/resources/qml/windows/Window.qml index 20216ed7ae..a0ef73290a 100644 --- a/interface/resources/qml/windows/Window.qml +++ b/interface/resources/qml/windows/Window.qml @@ -313,6 +313,6 @@ Fadable { } } - onMouseEntered: console.log("Mouse entered " + window) - onMouseExited: console.log("Mouse exited " + window) + // onMouseEntered: console.log("Mouse entered " + window) + // onMouseExited: console.log("Mouse exited " + window) } diff --git a/interface/resources/styles/log_dialog.qss b/interface/resources/styles/log_dialog.qss index 1fc4df0717..d3ae4e0a00 100644 --- a/interface/resources/styles/log_dialog.qss +++ b/interface/resources/styles/log_dialog.qss @@ -1,6 +1,6 @@ QPlainTextEdit { - font-family: Inconsolata, Lucida Console, Andale Mono, Monaco; + font-family: Inconsolata, Consolas, Courier New, monospace; font-size: 16px; padding-left: 28px; padding-top: 7px; @@ -11,7 +11,7 @@ QPlainTextEdit { } QLineEdit { - font-family: Inconsolata, Lucida Console, Andale Mono, Monaco; + font-family: Inconsolata, Consolas, Courier New, monospace; padding-left: 7px; background-color: #CCCCCC; border-width: 0; diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index d48fe19a99..7cf42a7759 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -141,6 +141,8 @@ #include "LODManager.h" #include "ModelPackager.h" #include "networking/HFWebEngineProfile.h" +#include "networking/HFTabletWebEngineProfile.h" +#include "networking/FileTypeProfile.h" #include "scripting/TestScriptingInterface.h" #include "scripting/AccountScriptingInterface.h" #include "scripting/AssetMappingsScriptingInterface.h" @@ -171,12 +173,17 @@ #include "ui/Stats.h" #include "ui/UpdateDialog.h" #include "ui/overlays/Overlays.h" +#include "ui/DomainConnectionModel.h" #include "Util.h" #include "InterfaceParentFinder.h" +#include "ui/OctreeStatsProvider.h" #include "FrameTimingsScriptingInterface.h" #include #include +#include +#include +#include // On Windows PC, NVidia Optimus laptop, we want to enable NVIDIA GPU // FIXME seems to be broken. @@ -212,21 +219,14 @@ static const QString FST_EXTENSION = ".fst"; static const QString FBX_EXTENSION = ".fbx"; static const QString OBJ_EXTENSION = ".obj"; static const QString AVA_JSON_EXTENSION = ".ava.json"; +static const QString WEB_VIEW_TAG = "noDownload=true"; -static const int MIRROR_VIEW_TOP_PADDING = 5; -static const int MIRROR_VIEW_LEFT_PADDING = 10; -static const int MIRROR_VIEW_WIDTH = 265; -static const int MIRROR_VIEW_HEIGHT = 215; static const float MIRROR_FULLSCREEN_DISTANCE = 0.389f; -static const float MIRROR_REARVIEW_DISTANCE = 0.722f; -static const float MIRROR_REARVIEW_BODY_DISTANCE = 2.56f; -static const float MIRROR_FIELD_OF_VIEW = 30.0f; static const quint64 TOO_LONG_SINCE_LAST_SEND_DOWNSTREAM_AUDIO_STATS = 1 * USECS_PER_SECOND; -static const QString INFO_WELCOME_PATH = "html/interface-welcome.html"; static const QString INFO_EDIT_ENTITIES_PATH = "html/edit-commands.html"; -static const QString INFO_HELP_PATH = "html/help.html"; +static const QString INFO_HELP_PATH = "../../../html/tabletHelp.html"; static const unsigned int THROTTLED_SIM_FRAMERATE = 15; static const int THROTTLED_SIM_FRAME_PERIOD_MS = MSECS_PER_SECOND / THROTTLED_SIM_FRAMERATE; @@ -423,6 +423,7 @@ static const QString STATE_CAMERA_THIRD_PERSON = "CameraThirdPerson"; static const QString STATE_CAMERA_ENTITY = "CameraEntity"; static const QString STATE_CAMERA_INDEPENDENT = "CameraIndependent"; static const QString STATE_SNAP_TURN = "SnapTurn"; +static const QString STATE_ADVANCED_MOVEMENT_CONTROLS = "AdvancedMovement"; static const QString STATE_GROUNDED = "Grounded"; static const QString STATE_NAV_FOCUSED = "NavigationFocused"; @@ -499,6 +500,8 @@ bool setupEssentials(int& argc, char** argv) { DependencyManager::set(); DependencyManager::set(); DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); #if defined(Q_OS_MAC) || defined(Q_OS_WIN) DependencyManager::set(); @@ -513,7 +516,7 @@ bool setupEssentials(int& argc, char** argv) { DependencyManager::set(); controller::StateController::setStateVariables({ { STATE_IN_HMD, STATE_CAMERA_FULL_SCREEN_MIRROR, STATE_CAMERA_FIRST_PERSON, STATE_CAMERA_THIRD_PERSON, STATE_CAMERA_ENTITY, STATE_CAMERA_INDEPENDENT, - STATE_SNAP_TURN, STATE_GROUNDED, STATE_NAV_FOCUSED } }); + STATE_SNAP_TURN, STATE_ADVANCED_MOVEMENT_CONTROLS, STATE_GROUNDED, STATE_NAV_FOCUSED } }); DependencyManager::set(); DependencyManager::set(); DependencyManager::set(); @@ -522,6 +525,9 @@ bool setupEssentials(int& argc, char** argv) { DependencyManager::set(); DependencyManager::set(); DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(nullptr, qApp->getOcteeSceneStats()); + return previousSessionCrashed; } @@ -548,7 +554,7 @@ const float DEFAULT_HMD_TABLET_SCALE_PERCENT = 100.0f; const float DEFAULT_DESKTOP_TABLET_SCALE_PERCENT = 75.0f; const bool DEFAULT_DESKTOP_TABLET_BECOMES_TOOLBAR = true; const bool DEFAULT_HMD_TABLET_BECOMES_TOOLBAR = 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) : QApplication(argc, argv), @@ -564,14 +570,13 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo _entityClipboardRenderer(false, this, this), _entityClipboard(new EntityTree()), _lastQueriedTime(usecTimestampNow()), - _mirrorViewRect(QRect(MIRROR_VIEW_LEFT_PADDING, MIRROR_VIEW_TOP_PADDING, MIRROR_VIEW_WIDTH, MIRROR_VIEW_HEIGHT)), _previousScriptLocation("LastScriptLocation", DESKTOP_LOCATION), _fieldOfView("fieldOfView", DEFAULT_FIELD_OF_VIEW_DEGREES), _hmdTabletScale("hmdTabletScale", DEFAULT_HMD_TABLET_SCALE_PERCENT), _desktopTabletScale("desktopTabletScale", DEFAULT_DESKTOP_TABLET_SCALE_PERCENT), _desktopTabletBecomesToolbarSetting("desktopTabletBecomesToolbar", DEFAULT_DESKTOP_TABLET_BECOMES_TOOLBAR), _hmdTabletBecomesToolbarSetting("hmdTabletBecomesToolbar", DEFAULT_HMD_TABLET_BECOMES_TOOLBAR), - _tabletVisibleToOthersSetting("tabletVisibleToOthers", DEFAULT_TABLET_VISIBLE_TO_OTHERS), + _preferAvatarFingerOverStylusSetting("preferAvatarFingerOverStylus", DEFAULT_PREFER_AVATAR_FINGER_OVER_STYLUS), _constrainToolbarPosition("toolbar/constrainToolbarToCenterX", true), _scaleMirror(1.0f), _rotateMirror(0.0f), @@ -606,6 +611,8 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo } } + // make sure the debug draw singleton is initialized on the main thread. + DebugDraw::getInstance().removeMarker(""); _runningMarker.startRunningMarker(); @@ -742,23 +749,24 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo } }); - auto& audioScriptingInterface = AudioScriptingInterface::getInstance(); + auto audioScriptingInterface = DependencyManager::set(); connect(audioThread, &QThread::started, audioIO.data(), &AudioClient::start); connect(audioIO.data(), &AudioClient::destroyed, audioThread, &QThread::quit); connect(audioThread, &QThread::finished, audioThread, &QThread::deleteLater); connect(audioIO.data(), &AudioClient::muteToggled, this, &Application::audioMuteToggled); - connect(audioIO.data(), &AudioClient::mutedByMixer, &audioScriptingInterface, &AudioScriptingInterface::mutedByMixer); - connect(audioIO.data(), &AudioClient::receivedFirstPacket, &audioScriptingInterface, &AudioScriptingInterface::receivedFirstPacket); - connect(audioIO.data(), &AudioClient::disconnected, &audioScriptingInterface, &AudioScriptingInterface::disconnected); + connect(audioIO.data(), &AudioClient::mutedByMixer, audioScriptingInterface.data(), &AudioScriptingInterface::mutedByMixer); + connect(audioIO.data(), &AudioClient::receivedFirstPacket, audioScriptingInterface.data(), &AudioScriptingInterface::receivedFirstPacket); + connect(audioIO.data(), &AudioClient::disconnected, audioScriptingInterface.data(), &AudioScriptingInterface::disconnected); connect(audioIO.data(), &AudioClient::muteEnvironmentRequested, [](glm::vec3 position, float radius) { auto audioClient = DependencyManager::get(); + auto audioScriptingInterface = DependencyManager::get(); auto myAvatarPosition = DependencyManager::get()->getMyAvatar()->getPosition(); float distance = glm::distance(myAvatarPosition, position); bool shouldMute = !audioClient->isMuted() && (distance < radius); if (shouldMute) { audioClient->toggleMute(); - AudioScriptingInterface::getInstance().environmentMuted(); + audioScriptingInterface->environmentMuted(); } }); @@ -843,6 +851,9 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo connect(this, &QCoreApplication::aboutToQuit, addressManager.data(), &AddressManager::storeCurrentAddress); connect(this, &Application::activeDisplayPluginChanged, this, &Application::updateThreadPoolCount); + connect(this, &Application::activeDisplayPluginChanged, this, [](){ + qApp->setProperty(hifi::properties::HMD, qApp->isHMDMode()); + }); connect(this, &Application::activeDisplayPluginChanged, this, &Application::updateSystemTabletMode); // Save avatar location immediately after a teleport. @@ -1125,6 +1136,10 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo _applicationStateDevice->setInputVariant(STATE_SNAP_TURN, []() -> float { return qApp->getMyAvatar()->getSnapTurn() ? 1 : 0; }); + _applicationStateDevice->setInputVariant(STATE_ADVANCED_MOVEMENT_CONTROLS, []() -> float { + return qApp->getMyAvatar()->useAdvancedMovementControls() ? 1 : 0; + }); + _applicationStateDevice->setInputVariant(STATE_GROUNDED, []() -> float { return qApp->getMyAvatar()->getCharacterController()->onGround() ? 1 : 0; }); @@ -1179,7 +1194,11 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo // set the local loopback interface for local sounds AudioInjector::setLocalAudioInterface(audioIO.data()); - AudioScriptingInterface::getInstance().setLocalAudioInterface(audioIO.data()); + audioScriptingInterface->setLocalAudioInterface(audioIO.data()); + connect(audioIO.data(), &AudioClient::noiseGateOpened, audioScriptingInterface.data(), &AudioScriptingInterface::noiseGateOpened); + connect(audioIO.data(), &AudioClient::noiseGateClosed, audioScriptingInterface.data(), &AudioScriptingInterface::noiseGateClosed); + connect(audioIO.data(), &AudioClient::inputReceived, audioScriptingInterface.data(), &AudioScriptingInterface::inputReceived); + this->installEventFilter(this); @@ -1437,7 +1456,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo scriptEngines->loadScript(testScript, false); } else { // Get sandbox content set version, if available - auto acDirPath = PathUtils::getRootDataDirectory() + BuildInfo::MODIFIED_ORGANIZATION + "/assignment-client/"; + auto acDirPath = PathUtils::getAppDataPath() + "../../" + BuildInfo::MODIFIED_ORGANIZATION + "/assignment-client/"; auto contentVersionPath = acDirPath + "content-version.txt"; qCDebug(interfaceapp) << "Checking " << contentVersionPath << " for content version"; auto contentVersion = 0; @@ -1615,17 +1634,15 @@ QString Application::getUserAgent() { return userAgent; } -uint64_t lastTabletUIToggle { 0 }; -const uint64_t toggleTabletUILockout { 500000 }; void Application::toggleTabletUI() const { - uint64_t now = usecTimestampNow(); - if (now - lastTabletUIToggle < toggleTabletUILockout) { - return; + auto tabletScriptingInterface = DependencyManager::get(); + auto hmd = DependencyManager::get(); + TabletProxy* tablet = dynamic_cast(tabletScriptingInterface->getTablet("com.highfidelity.interface.tablet.system")); + bool messageOpen = tablet->isMessageDialogOpen(); + if (!messageOpen || (messageOpen && !hmd->getShouldShowTablet())) { + auto HMD = DependencyManager::get(); + HMD->toggleShouldShowTablet(); } - lastTabletUIToggle = now; - - auto HMD = DependencyManager::get(); - HMD->toggleShouldShowTablet(); } void Application::checkChangeCursor() { @@ -1798,6 +1815,7 @@ Application::~Application() { DependencyManager::destroy(); DependencyManager::destroy(); DependencyManager::destroy(); + DependencyManager::destroy(); ResourceManager::cleanup(); @@ -1917,6 +1935,8 @@ void Application::initializeUi() { qmlRegisterType("Hifi", 1, 0, "Preference"); qmlRegisterType("HFWebEngineProfile", 1, 0, "HFWebEngineProfile"); + qmlRegisterType("HFTabletWebEngineProfile", 1, 0, "HFTabletWebEngineProfile"); + qmlRegisterType("FileTypeProfile", 1, 0, "FileTypeProfile"); auto offscreenUi = DependencyManager::get(); offscreenUi->create(_glWidget->qglContext()); @@ -1943,8 +1963,10 @@ void Application::initializeUi() { // For some reason there is already an "Application" object in the QML context, // though I can't find it. Hence, "ApplicationInterface" rootContext->setContextProperty("ApplicationInterface", this); - rootContext->setContextProperty("Audio", &AudioScriptingInterface::getInstance()); + rootContext->setContextProperty("Audio", DependencyManager::get().data()); rootContext->setContextProperty("AudioStats", DependencyManager::get()->getStats().data()); + rootContext->setContextProperty("AudioScope", DependencyManager::get().data()); + rootContext->setContextProperty("Controller", DependencyManager::get().data()); rootContext->setContextProperty("Entities", DependencyManager::get().data()); _fileDownload = new FileScriptingInterface(engine); @@ -1957,12 +1979,13 @@ void Application::initializeUi() { rootContext->setContextProperty("AddressManager", DependencyManager::get().data()); rootContext->setContextProperty("FrameTimings", &_frameTimingsScriptingInterface); rootContext->setContextProperty("Rates", new RatesScriptingInterface(this)); + rootContext->setContextProperty("pathToFonts", "../../"); rootContext->setContextProperty("TREE_SCALE", TREE_SCALE); rootContext->setContextProperty("Quat", new Quat()); rootContext->setContextProperty("Vec3", new Vec3()); rootContext->setContextProperty("Uuid", new ScriptUUID()); - rootContext->setContextProperty("Assets", new AssetMappingsScriptingInterface()); + rootContext->setContextProperty("Assets", DependencyManager::get().data()); rootContext->setContextProperty("AvatarList", DependencyManager::get().data()); rootContext->setContextProperty("Users", DependencyManager::get().data()); @@ -1990,6 +2013,7 @@ void Application::initializeUi() { rootContext->setContextProperty("SoundCache", DependencyManager::get().data()); rootContext->setContextProperty("Account", AccountScriptingInterface::getInstance()); + rootContext->setContextProperty("Tablet", DependencyManager::get().data()); rootContext->setContextProperty("DialogsManager", _dialogsManagerScriptingInterface); rootContext->setContextProperty("GlobalServices", GlobalServicesScriptingInterface::getInstance()); rootContext->setContextProperty("FaceTracker", DependencyManager::get().data()); @@ -2109,21 +2133,6 @@ void Application::paintGL() { batch.resetStages(); }); - auto inputs = AvatarInputs::getInstance(); - if (inputs->mirrorVisible()) { - PerformanceTimer perfTimer("Mirror"); - - renderArgs._renderMode = RenderArgs::MIRROR_RENDER_MODE; - renderArgs._blitFramebuffer = DependencyManager::get()->getSelfieFramebuffer(); - - _mirrorViewRect.moveTo(inputs->x(), inputs->y()); - - renderRearViewMirror(&renderArgs, _mirrorViewRect, inputs->mirrorZoomed()); - - renderArgs._blitFramebuffer.reset(); - renderArgs._renderMode = RenderArgs::DEFAULT_RENDER_MODE; - } - { PerformanceTimer perfTimer("renderOverlay"); // NOTE: There is no batch associated with this renderArgs @@ -2138,7 +2147,7 @@ void Application::paintGL() { PerformanceTimer perfTimer("CameraUpdates"); auto myAvatar = getMyAvatar(); - boomOffset = myAvatar->getScale() * myAvatar->getBoomLength() * -IDENTITY_FRONT; + boomOffset = myAvatar->getScale() * myAvatar->getBoomLength() * -IDENTITY_FORWARD; if (_myCamera.getMode() == CAMERA_MODE_FIRST_PERSON || _myCamera.getMode() == CAMERA_MODE_THIRD_PERSON) { Menu::getInstance()->setIsOptionChecked(MenuOption::FirstPerson, myAvatar->getBoomLength() <= MyAvatar::ZOOM_MIN); @@ -2357,9 +2366,8 @@ void Application::setHmdTabletBecomesToolbarSetting(bool value) { updateSystemTabletMode(); } -void Application::setTabletVisibleToOthersSetting(bool value) { - _tabletVisibleToOthersSetting.set(value); - updateSystemTabletMode(); +void Application::setPreferAvatarFingerOverStylus(bool value) { + _preferAvatarFingerOverStylusSetting.set(value); } void Application::setSettingConstrainToolbarPosition(bool setting) { @@ -2367,10 +2375,6 @@ void Application::setSettingConstrainToolbarPosition(bool setting) { DependencyManager::get()->setConstrainToolbarToCenterX(setting); } -void Application::aboutApp() { - InfoView::show(INFO_WELCOME_PATH); -} - void Application::showHelp() { static const QString HAND_CONTROLLER_NAME_VIVE = "vive"; static const QString HAND_CONTROLLER_NAME_OCULUS_TOUCH = "oculus"; @@ -2395,8 +2399,10 @@ void Application::showHelp() { QUrlQuery queryString; queryString.addQueryItem("handControllerName", handControllerName); queryString.addQueryItem("defaultTab", defaultTab); - - InfoView::show(INFO_HELP_PATH, false, queryString.toString()); + auto tabletScriptingInterface = DependencyManager::get(); + TabletProxy* tablet = dynamic_cast(tabletScriptingInterface->getTablet("com.highfidelity.interface.tablet.system")); + tablet->gotoWebScreen(INFO_HELP_PATH + "?" + queryString.toString()); + //InfoView::show(INFO_HELP_PATH, false, queryString.toString()); } void Application::resizeEvent(QResizeEvent* event) { @@ -2752,8 +2758,6 @@ void Application::keyPressEvent(QKeyEvent* event) { case Qt::Key_S: if (isShifted && isMeta && !isOption) { Menu::getInstance()->triggerOption(MenuOption::SuppressShortTimings); - } else if (isOption && !isShifted && !isMeta) { - Menu::getInstance()->triggerOption(MenuOption::ScriptEditor); } else if (!isOption && !isShifted && isMeta) { takeSnapshot(true); } @@ -2872,57 +2876,57 @@ void Application::keyPressEvent(QKeyEvent* event) { break; #endif - case Qt::Key_H: - if (isShifted) { - Menu::getInstance()->triggerOption(MenuOption::MiniMirror); - } else { - // whenever switching to/from full screen mirror from the keyboard, remember - // the state you were in before full screen mirror, and return to that. - auto previousMode = _myCamera.getMode(); - if (previousMode != CAMERA_MODE_MIRROR) { - switch (previousMode) { - case CAMERA_MODE_FIRST_PERSON: - _returnFromFullScreenMirrorTo = MenuOption::FirstPerson; - break; - case CAMERA_MODE_THIRD_PERSON: - _returnFromFullScreenMirrorTo = MenuOption::ThirdPerson; - break; + case Qt::Key_H: { + // whenever switching to/from full screen mirror from the keyboard, remember + // the state you were in before full screen mirror, and return to that. + auto previousMode = _myCamera.getMode(); + if (previousMode != CAMERA_MODE_MIRROR) { + switch (previousMode) { + case CAMERA_MODE_FIRST_PERSON: + _returnFromFullScreenMirrorTo = MenuOption::FirstPerson; + break; + case CAMERA_MODE_THIRD_PERSON: + _returnFromFullScreenMirrorTo = MenuOption::ThirdPerson; + break; - // FIXME - it's not clear that these modes make sense to return to... - case CAMERA_MODE_INDEPENDENT: - _returnFromFullScreenMirrorTo = MenuOption::IndependentMode; - break; - case CAMERA_MODE_ENTITY: - _returnFromFullScreenMirrorTo = MenuOption::CameraEntityMode; - break; + // FIXME - it's not clear that these modes make sense to return to... + case CAMERA_MODE_INDEPENDENT: + _returnFromFullScreenMirrorTo = MenuOption::IndependentMode; + break; + case CAMERA_MODE_ENTITY: + _returnFromFullScreenMirrorTo = MenuOption::CameraEntityMode; + break; - default: - _returnFromFullScreenMirrorTo = MenuOption::ThirdPerson; - break; - } + default: + _returnFromFullScreenMirrorTo = MenuOption::ThirdPerson; + break; } + } - bool isMirrorChecked = Menu::getInstance()->isOptionChecked(MenuOption::FullscreenMirror); - Menu::getInstance()->setIsOptionChecked(MenuOption::FullscreenMirror, !isMirrorChecked); - if (isMirrorChecked) { + bool isMirrorChecked = Menu::getInstance()->isOptionChecked(MenuOption::FullscreenMirror); + Menu::getInstance()->setIsOptionChecked(MenuOption::FullscreenMirror, !isMirrorChecked); + if (isMirrorChecked) { - // if we got here without coming in from a non-Full Screen mirror case, then our - // _returnFromFullScreenMirrorTo is unknown. In that case we'll go to the old - // behavior of returning to ThirdPerson - if (_returnFromFullScreenMirrorTo.isEmpty()) { - _returnFromFullScreenMirrorTo = MenuOption::ThirdPerson; - } - Menu::getInstance()->setIsOptionChecked(_returnFromFullScreenMirrorTo, true); + // if we got here without coming in from a non-Full Screen mirror case, then our + // _returnFromFullScreenMirrorTo is unknown. In that case we'll go to the old + // behavior of returning to ThirdPerson + if (_returnFromFullScreenMirrorTo.isEmpty()) { + _returnFromFullScreenMirrorTo = MenuOption::ThirdPerson; } + Menu::getInstance()->setIsOptionChecked(_returnFromFullScreenMirrorTo, true); + } + cameraMenuChanged(); + break; + } + + case Qt::Key_P: { + if (!(isShifted || isMeta || isOption)) { + bool isFirstPersonChecked = Menu::getInstance()->isOptionChecked(MenuOption::FirstPerson); + Menu::getInstance()->setIsOptionChecked(MenuOption::FirstPerson, !isFirstPersonChecked); + Menu::getInstance()->setIsOptionChecked(MenuOption::ThirdPerson, isFirstPersonChecked); cameraMenuChanged(); } break; - case Qt::Key_P: { - bool isFirstPersonChecked = Menu::getInstance()->isOptionChecked(MenuOption::FirstPerson); - Menu::getInstance()->setIsOptionChecked(MenuOption::FirstPerson, !isFirstPersonChecked); - Menu::getInstance()->setIsOptionChecked(MenuOption::ThirdPerson, isFirstPersonChecked); - cameraMenuChanged(); - break; } case Qt::Key_Slash: @@ -3166,7 +3170,23 @@ void Application::mousePressEvent(QMouseEvent* event) { } } -void Application::mouseDoublePressEvent(QMouseEvent* event) const { +void Application::mouseDoublePressEvent(QMouseEvent* event) { + auto offscreenUi = DependencyManager::get(); + auto eventPosition = getApplicationCompositor().getMouseEventPosition(event); + QPointF transformedPos = offscreenUi->mapToVirtualScreen(eventPosition, _glWidget); + QMouseEvent mappedEvent(event->type(), + transformedPos, + event->screenPos(), event->button(), + event->buttons(), event->modifiers()); + + if (!_aboutToQuit) { + getOverlays().mouseDoublePressEvent(&mappedEvent); + if (!_controllerScriptingInterface->areEntityClicksCaptured()) { + getEntities()->mouseDoublePressEvent(&mappedEvent); + } + } + + // if one of our scripts have asked to capture this event, then stop processing it if (_controllerScriptingInterface->isMouseCaptured()) { return; @@ -3770,7 +3790,6 @@ void Application::loadSettings() { } getMyAvatar()->loadData(); - _settingsLoaded = true; } @@ -3813,8 +3832,6 @@ void Application::init() { DependencyManager::get()->init(); _myCamera.setMode(CAMERA_MODE_FIRST_PERSON); - _mirrorCamera.setMode(CAMERA_MODE_MIRROR); - _timerStart.start(); _lastTimeUpdated.start(); @@ -3949,7 +3966,7 @@ void Application::updateMyAvatarLookAtPosition() { auto lookingAtHead = static_pointer_cast(lookingAt)->getHead(); const float MAXIMUM_FACE_ANGLE = 65.0f * RADIANS_PER_DEGREE; - glm::vec3 lookingAtFaceOrientation = lookingAtHead->getFinalOrientationInWorldFrame() * IDENTITY_FRONT; + glm::vec3 lookingAtFaceOrientation = lookingAtHead->getFinalOrientationInWorldFrame() * IDENTITY_FORWARD; glm::vec3 fromLookingAtToMe = glm::normalize(myAvatar->getHead()->getEyePosition() - lookingAtHead->getEyePosition()); float faceAngle = glm::angle(lookingAtFaceOrientation, fromLookingAtToMe); @@ -4228,12 +4245,6 @@ void Application::updateDialogs(float deltaTime) const { PerformanceWarning warn(showWarnings, "Application::updateDialogs()"); auto dialogsManager = DependencyManager::get(); - // Update bandwidth dialog, if any - BandwidthDialog* bandwidthDialog = dialogsManager->getBandwidthDialog(); - if (bandwidthDialog) { - bandwidthDialog->update(); - } - QPointer octreeStatsDialog = dialogsManager->getOctreeStatsDialog(); if (octreeStatsDialog) { octreeStatsDialog->update(); @@ -4357,16 +4368,16 @@ void Application::update(float deltaTime) { myAvatar->clearDriveKeys(); if (_myCamera.getMode() != CAMERA_MODE_INDEPENDENT) { if (!_controllerScriptingInterface->areActionsCaptured()) { - myAvatar->setDriveKeys(TRANSLATE_Z, -1.0f * userInputMapper->getActionState(controller::Action::TRANSLATE_Z)); - myAvatar->setDriveKeys(TRANSLATE_Y, userInputMapper->getActionState(controller::Action::TRANSLATE_Y)); - myAvatar->setDriveKeys(TRANSLATE_X, userInputMapper->getActionState(controller::Action::TRANSLATE_X)); + myAvatar->setDriveKey(MyAvatar::TRANSLATE_Z, -1.0f * userInputMapper->getActionState(controller::Action::TRANSLATE_Z)); + myAvatar->setDriveKey(MyAvatar::TRANSLATE_Y, userInputMapper->getActionState(controller::Action::TRANSLATE_Y)); + myAvatar->setDriveKey(MyAvatar::TRANSLATE_X, userInputMapper->getActionState(controller::Action::TRANSLATE_X)); if (deltaTime > FLT_EPSILON) { - myAvatar->setDriveKeys(PITCH, -1.0f * userInputMapper->getActionState(controller::Action::PITCH)); - myAvatar->setDriveKeys(YAW, -1.0f * userInputMapper->getActionState(controller::Action::YAW)); - myAvatar->setDriveKeys(STEP_YAW, -1.0f * userInputMapper->getActionState(controller::Action::STEP_YAW)); + myAvatar->setDriveKey(MyAvatar::PITCH, -1.0f * userInputMapper->getActionState(controller::Action::PITCH)); + myAvatar->setDriveKey(MyAvatar::YAW, -1.0f * userInputMapper->getActionState(controller::Action::YAW)); + myAvatar->setDriveKey(MyAvatar::STEP_YAW, -1.0f * userInputMapper->getActionState(controller::Action::STEP_YAW)); } } - myAvatar->setDriveKeys(ZOOM, userInputMapper->getActionState(controller::Action::TRANSLATE_CAMERA_Z)); + myAvatar->setDriveKey(MyAvatar::ZOOM, userInputMapper->getActionState(controller::Action::TRANSLATE_CAMERA_Z)); } controller::Pose leftHandPose = userInputMapper->getPoseState(controller::Action::LEFT_HAND); @@ -4376,6 +4387,10 @@ void Application::update(float deltaTime) { auto avatarToSensorMatrix = worldToSensorMatrix * myAvatarMatrix; myAvatar->setHandControllerPosesInSensorFrame(leftHandPose.transform(avatarToSensorMatrix), rightHandPose.transform(avatarToSensorMatrix)); + controller::Pose leftFootPose = userInputMapper->getPoseState(controller::Action::LEFT_FOOT); + controller::Pose rightFootPose = userInputMapper->getPoseState(controller::Action::RIGHT_FOOT); + myAvatar->setFootControllerPosesInSensorFrame(leftFootPose.transform(avatarToSensorMatrix), rightFootPose.transform(avatarToSensorMatrix)); + updateThreads(deltaTime); // If running non-threaded, then give the threads some time to process... updateDialogs(deltaTime); // update various stats dialogs if present @@ -4437,9 +4452,12 @@ void Application::update(float deltaTime) { getEntities()->getTree()->withWriteLock([&] { PerformanceTimer perfTimer("handleOutgoingChanges"); - const VectorOfMotionStates& outgoingChanges = _physicsEngine->getOutgoingChanges(); - _entitySimulation->handleOutgoingChanges(outgoingChanges); - avatarManager->handleOutgoingChanges(outgoingChanges); + const VectorOfMotionStates& deactivations = _physicsEngine->getDeactivatedMotionStates(); + _entitySimulation->handleDeactivatedMotionStates(deactivations); + + const VectorOfMotionStates& outgoingChanges = _physicsEngine->getChangedMotionStates(); + _entitySimulation->handleChangedMotionStates(outgoingChanges); + avatarManager->handleChangedMotionStates(outgoingChanges); }); if (!_aboutToQuit) { @@ -4557,6 +4575,8 @@ void Application::update(float deltaTime) { } AnimDebugDraw::getInstance().update(); + + DependencyManager::get()->update(); } void Application::sendAvatarViewFrustum() { @@ -5020,7 +5040,7 @@ void Application::displaySide(RenderArgs* renderArgs, Camera& theCamera, bool se // TODO fix shadows and make them use the GPU library // The pending changes collecting the changes here - render::PendingChanges pendingChanges; + render::Transaction transaction; // FIXME: Move this out of here!, Background / skybox should be driven by the enityt content just like the other entities // Background rendering decision @@ -5028,7 +5048,7 @@ void Application::displaySide(RenderArgs* renderArgs, Camera& theCamera, bool se auto backgroundRenderData = make_shared(); auto backgroundRenderPayload = make_shared(backgroundRenderData); BackgroundRenderData::_item = _main3DScene->allocateID(); - pendingChanges.resetItem(BackgroundRenderData::_item, backgroundRenderPayload); + transaction.resetItem(BackgroundRenderData::_item, backgroundRenderPayload); } // Assuming nothing get's rendered through that @@ -5046,7 +5066,7 @@ void Application::displaySide(RenderArgs* renderArgs, Camera& theCamera, bool se static_cast(RenderArgs::RENDER_DEBUG_HULLS)); } renderArgs->_debugFlags = renderDebugFlags; - //ViveControllerManager::getInstance().updateRendering(renderArgs, _main3DScene, pendingChanges); + //ViveControllerManager::getInstance().updateRendering(renderArgs, _main3DScene, transaction); } } @@ -5058,9 +5078,9 @@ void Application::displaySide(RenderArgs* renderArgs, Camera& theCamera, bool se WorldBoxRenderData::_item = _main3DScene->allocateID(); - pendingChanges.resetItem(WorldBoxRenderData::_item, worldBoxRenderPayload); + transaction.resetItem(WorldBoxRenderData::_item, worldBoxRenderPayload); } else { - pendingChanges.updateItem(WorldBoxRenderData::_item, + transaction.updateItem(WorldBoxRenderData::_item, [](WorldBoxRenderData& payload) { payload._val++; }); @@ -5073,10 +5093,10 @@ void Application::displaySide(RenderArgs* renderArgs, Camera& theCamera, bool se } { - PerformanceTimer perfTimer("SceneProcessPendingChanges"); - _main3DScene->enqueuePendingChanges(pendingChanges); + PerformanceTimer perfTimer("SceneProcessTransaction"); + _main3DScene->enqueueTransaction(transaction); - _main3DScene->processPendingChangesQueue(); + _main3DScene->processTransactionQueue(); } // For now every frame pass the renderContext @@ -5096,58 +5116,6 @@ void Application::displaySide(RenderArgs* renderArgs, Camera& theCamera, bool se activeRenderingThread = nullptr; } -void Application::renderRearViewMirror(RenderArgs* renderArgs, const QRect& region, bool isZoomed) { - auto originalViewport = renderArgs->_viewport; - // Grab current viewport to reset it at the end - - float aspect = (float)region.width() / region.height(); - float fov = MIRROR_FIELD_OF_VIEW; - - auto myAvatar = getMyAvatar(); - - // bool eyeRelativeCamera = false; - if (!isZoomed) { - _mirrorCamera.setPosition(myAvatar->getChestPosition() + - myAvatar->getOrientation() * glm::vec3(0.0f, 0.0f, -1.0f) * MIRROR_REARVIEW_BODY_DISTANCE * myAvatar->getScale()); - - } else { // HEAD zoom level - // FIXME note that the positioning of the camera relative to the avatar can suffer limited - // precision as the user's position moves further away from the origin. Thus at - // /1e7,1e7,1e7 (well outside the buildable volume) the mirror camera veers and sways - // wildly as you rotate your avatar because the floating point values are becoming - // larger, squeezing out the available digits of precision you have available at the - // human scale for camera positioning. - - // Previously there was a hack to correct this using the mechanism of repositioning - // the avatar at the origin of the world for the purposes of rendering the mirror, - // but it resulted in failing to render the avatar's head model in the mirror view - // when in first person mode. Presumably this was because of some missed culling logic - // that was not accounted for in the hack. - - // This was removed in commit 71e59cfa88c6563749594e25494102fe01db38e9 but could be further - // investigated in order to adapt the technique while fixing the head rendering issue, - // but the complexity of the hack suggests that a better approach - _mirrorCamera.setPosition(myAvatar->getDefaultEyePosition() + - myAvatar->getOrientation() * glm::vec3(0.0f, 0.0f, -1.0f) * MIRROR_REARVIEW_DISTANCE * myAvatar->getScale()); - } - _mirrorCamera.setProjection(glm::perspective(glm::radians(fov), aspect, DEFAULT_NEAR_CLIP, DEFAULT_FAR_CLIP)); - _mirrorCamera.setOrientation(myAvatar->getWorldAlignedOrientation() * glm::quat(glm::vec3(0.0f, PI, 0.0f))); - - - // set the bounds of rear mirror view - // the region is in device independent coordinates; must convert to device - float ratio = (float)QApplication::desktop()->windowHandle()->devicePixelRatio() * getRenderResolutionScale(); - int width = region.width() * ratio; - int height = region.height() * ratio; - gpu::Vec4i viewport = gpu::Vec4i(0, 0, width, height); - renderArgs->_viewport = viewport; - - // render rear mirror view - displaySide(renderArgs, _mirrorCamera, true); - - renderArgs->_viewport = originalViewport; -} - void Application::resetSensors(bool andReload) { DependencyManager::get()->reset(); DependencyManager::get()->reset(); @@ -5184,6 +5152,7 @@ void Application::updateWindowTitle() const { #endif _window->setWindowTitle(title); } + void Application::clearDomainOctreeDetails() { // if we're about to quit, we really don't need to do any of these things... @@ -5213,6 +5182,12 @@ void Application::clearDomainOctreeDetails() { skyStage->setBackgroundMode(model::SunSkyStage::SKY_DEFAULT); _recentlyClearedDomain = true; + + DependencyManager::get()->clearOtherAvatars(); + DependencyManager::get()->clearUnusedResources(); + DependencyManager::get()->clearUnusedResources(); + DependencyManager::get()->clearUnusedResources(); + DependencyManager::get()->clearUnusedResources(); } void Application::domainChanged(const QString& domainHostname) { @@ -5470,8 +5445,7 @@ void Application::registerScriptEngineWithApplicationServices(ScriptEngine* scri scriptEngine->registerGlobalObject("Rates", new RatesScriptingInterface(this)); // hook our avatar and avatar hash map object into this script engine - scriptEngine->registerGlobalObject("MyAvatar", getMyAvatar().get()); - qScriptRegisterMetaType(scriptEngine, audioListenModeToScriptValue, audioListenModeFromScriptValue); + getMyAvatar()->registerMetaTypes(scriptEngine); scriptEngine->registerGlobalObject("AvatarList", DependencyManager::get().data()); @@ -5512,6 +5486,7 @@ void Application::registerScriptEngineWithApplicationServices(ScriptEngine* scri scriptEngine->registerGlobalObject("Settings", SettingsScriptingInterface::getInstance()); scriptEngine->registerGlobalObject("AudioDevice", AudioDeviceScriptingInterface::getInstance()); scriptEngine->registerGlobalObject("AudioStats", DependencyManager::get()->getStats().data()); + scriptEngine->registerGlobalObject("AudioScope", DependencyManager::get().data()); // Caches scriptEngine->registerGlobalObject("AnimationCache", DependencyManager::get().data()); @@ -5548,6 +5523,8 @@ void Application::registerScriptEngineWithApplicationServices(ScriptEngine* scri scriptEngine->registerGlobalObject("UserActivityLogger", DependencyManager::get().data()); scriptEngine->registerGlobalObject("Users", DependencyManager::get().data()); + scriptEngine->registerGlobalObject("LimitlessSpeechRecognition", DependencyManager::get().data()); + if (auto steamClient = PluginManager::getInstance()->getSteamClientPlugin()) { scriptEngine->registerGlobalObject("Steam", new SteamScriptingInterface(scriptEngine, steamClient.get())); } @@ -5572,7 +5549,9 @@ void Application::registerScriptEngineWithApplicationServices(ScriptEngine* scri bool Application::canAcceptURL(const QString& urlString) const { QUrl url(urlString); - if (urlString.startsWith(HIFI_URL_SCHEME)) { + if (url.query().contains(WEB_VIEW_TAG)) { + return false; + } else if (urlString.startsWith(HIFI_URL_SCHEME)) { return true; } QHashIterator i(_acceptedExtensions); @@ -5680,7 +5659,9 @@ bool Application::askToLoadScript(const QString& scriptFilenameOrURL) { QUrl scriptURL { scriptFilenameOrURL }; if (scriptURL.host().endsWith(MARKETPLACE_CDN_HOSTNAME)) { - shortName = shortName.mid(shortName.lastIndexOf('/') + 1); + int startIndex = shortName.lastIndexOf('/') + 1; + int endIndex = shortName.lastIndexOf('?'); + shortName = shortName.mid(startIndex, endIndex - startIndex); } QString message = "Would you like to run this script:\n" + shortName; @@ -5778,9 +5759,26 @@ bool Application::displayAvatarAttachmentConfirmationDialog(const QString& name) } } -void Application::toggleRunningScriptsWidget() const { - static const QUrl url("hifi/dialogs/RunningScripts.qml"); - DependencyManager::get()->show(url, "RunningScripts"); +void Application::toggleRunningScriptsWidget() const { + auto scriptEngines = DependencyManager::get(); + bool scriptsRunning = !scriptEngines->getRunningScripts().isEmpty(); + auto tabletScriptingInterface = DependencyManager::get(); + auto tablet = dynamic_cast(tabletScriptingInterface->getTablet("com.highfidelity.interface.tablet.system")); + + if (tablet->getToolbarMode() || false == scriptsRunning) { + static const QUrl url("hifi/dialogs/RunningScripts.qml"); + DependencyManager::get()->show(url, "RunningScripts"); + } else { + auto hmd = DependencyManager::get(); + if (!hmd->getShouldShowTablet() && !isHMDMode()) { + static const QUrl url("hifi/dialogs/RunningScripts.qml"); + DependencyManager::get()->show(url, "RunningScripts"); + } else { + static const QUrl url("../../hifi/dialogs/TabletRunningScripts.qml"); + tablet->pushOntoStack(url); + } + } + //DependencyManager::get()->show(url, "RunningScripts"); //if (_runningScriptsWidget->isVisible()) { // if (_runningScriptsWidget->hasFocus()) { // _runningScriptsWidget->hide(); @@ -5795,6 +5793,12 @@ void Application::toggleRunningScriptsWidget() const { //} } +void Application::showScriptLogs() { + auto scriptEngines = DependencyManager::get(); + QUrl defaultScriptsLoc = defaultScriptsLocation(); + defaultScriptsLoc.setPath(defaultScriptsLoc.path() + "developer/debugging/debugWindow.js"); + scriptEngines->loadScript(defaultScriptsLoc.toString()); +} void Application::showAssetServerWidget(QString filePath) { if (!DependencyManager::get()->getThisNodeCanWriteAssets()) { @@ -5807,7 +5811,20 @@ void Application::showAssetServerWidget(QString filePath) { emit uploadRequest(filePath); } }; - DependencyManager::get()->show(url, "AssetServer", startUpload); + auto tabletScriptingInterface = DependencyManager::get(); + auto tablet = dynamic_cast(tabletScriptingInterface->getTablet("com.highfidelity.interface.tablet.system")); + auto hmd = DependencyManager::get(); + if (tablet->getToolbarMode()) { + DependencyManager::get()->show(url, "AssetServer", startUpload); + } else { + if (!hmd->getShouldShowTablet() && !isHMDMode()) { + DependencyManager::get()->show(url, "AssetServer", startUpload); + } else { + static const QUrl url("../../hifi/dialogs/TabletAssetServer.qml"); + tablet->pushOntoStack(url); + } + } + startUpload(nullptr, nullptr); } @@ -5830,6 +5847,21 @@ void Application::addAssetToWorldFromURL(QString url) { request->send(); } +void Application::showDialog(const QString& desktopURL, const QString& tabletURL, const QString& name) const { + auto tabletScriptingInterface = DependencyManager::get(); + auto tablet = dynamic_cast(tabletScriptingInterface->getTablet("com.highfidelity.interface.tablet.system")); + auto hmd = DependencyManager::get(); + if (tablet->getToolbarMode()) { + DependencyManager::get()->show(desktopURL, name); + } else { + tablet->pushOntoStack(tabletURL); + if (!hmd->getShouldShowTablet() && !isHMDMode()) { + hmd->openTablet(); + } + + } +} + void Application::addAssetToWorldFromURLRequestFinished() { auto request = qobject_cast(sender()); auto url = request->getUrl().toString(); @@ -6311,6 +6343,41 @@ void Application::loadScriptURLDialog() const { } } +void Application::loadLODToolsDialog() { + auto tabletScriptingInterface = DependencyManager::get(); + auto tablet = dynamic_cast(tabletScriptingInterface->getTablet("com.highfidelity.interface.tablet.system")); + if (tablet->getToolbarMode() || (!tablet->getTabletRoot() && !isHMDMode())) { + auto dialogsManager = DependencyManager::get(); + dialogsManager->lodTools(); + } else { + tablet->pushOntoStack("../../hifi/dialogs/TabletLODTools.qml"); + } + +} + + +void Application::loadEntityStatisticsDialog() { + auto tabletScriptingInterface = DependencyManager::get(); + auto tablet = dynamic_cast(tabletScriptingInterface->getTablet("com.highfidelity.interface.tablet.system")); + if (tablet->getToolbarMode() || (!tablet->getTabletRoot() && !isHMDMode())) { + auto dialogsManager = DependencyManager::get(); + dialogsManager->octreeStatsDetails(); + } else { + tablet->pushOntoStack("../../hifi/dialogs/TabletEntityStatistics.qml"); + } +} + +void Application::loadDomainConnectionDialog() { + auto tabletScriptingInterface = DependencyManager::get(); + auto tablet = dynamic_cast(tabletScriptingInterface->getTablet("com.highfidelity.interface.tablet.system")); + if (tablet->getToolbarMode() || (!tablet->getTabletRoot() && !isHMDMode())) { + auto dialogsManager = DependencyManager::get(); + dialogsManager->showDomainConnectionDialog(); + } else { + tablet->pushOntoStack("../../hifi/dialogs/TabletDCDialog.qml"); + } +} + void Application::toggleLogDialog() { if (! _logDialog) { _logDialog = new LogDialog(nullptr, getLogger()); @@ -6897,6 +6964,7 @@ void Application::updateThreadPoolCount() const { } void Application::updateSystemTabletMode() { + qApp->setProperty(hifi::properties::HMD, isHMDMode()); if (isHMDMode()) { DependencyManager::get()->setToolbarMode(getHmdTabletBecomesToolbarSetting()); } else { diff --git a/interface/src/Application.h b/interface/src/Application.h index 13c1458aee..44c0f127c2 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -51,6 +51,7 @@ #include #include "avatar/MyAvatar.h" +#include "BandwidthRecorder.h" #include "Bookmarks.h" #include "Camera.h" #include "ConnectionMonitor.h" @@ -61,7 +62,6 @@ #include "scripting/ControllerScriptingInterface.h" #include "scripting/DialogsManagerScriptingInterface.h" #include "ui/ApplicationOverlay.h" -#include "ui/BandwidthDialog.h" #include "ui/EntityScriptServerLogDialog.h" #include "ui/LodToolsDialog.h" #include "ui/LogDialog.h" @@ -72,6 +72,8 @@ #include #include +#include + class OffscreenGLCanvas; class GLCanvas; @@ -218,8 +220,8 @@ public: void setDesktopTabletBecomesToolbarSetting(bool value); bool getHmdTabletBecomesToolbarSetting() { return _hmdTabletBecomesToolbarSetting.get(); } void setHmdTabletBecomesToolbarSetting(bool value); - bool getTabletVisibleToOthersSetting() { return _tabletVisibleToOthersSetting.get(); } - void setTabletVisibleToOthersSetting(bool value); + bool getPreferAvatarFingerOverStylus() { return _preferAvatarFingerOverStylusSetting.get(); } + void setPreferAvatarFingerOverStylus(bool value); float getSettingConstrainToolbarPosition() { return _constrainToolbarPosition.get(); } void setSettingConstrainToolbarPosition(bool setting); @@ -274,8 +276,6 @@ public: virtual void pushPostUpdateLambda(void* key, std::function func) override; - const QRect& getMirrorViewRect() const { return _mirrorViewRect; } - void updateMyAvatarLookAtPosition(); float getAvatarSimrate() const { return _avatarSimCounter.rate(); } @@ -331,6 +331,8 @@ public slots: void toggleRunningScriptsWidget() const; Q_INVOKABLE void showAssetServerWidget(QString filePath = ""); + void showDialog(const QString& desktopURL, const QString& tabletURL, const QString& name) const; + // FIXME: Move addAssetToWorld* methods to own class? void addAssetToWorldFromURL(QString url); void addAssetToWorldFromURLRequestFinished(); @@ -366,7 +368,6 @@ public slots: void calibrateEyeTracker5Points(); #endif - void aboutApp(); static void showHelp(); void cycleCamera(); @@ -399,6 +400,10 @@ public slots: void addAssetToWorldMessageClose(); Q_INVOKABLE void toggleMuteAudio(); + void loadLODToolsDialog(); + void loadEntityStatisticsDialog(); + void loadDomainConnectionDialog(); + void showScriptLogs(); private slots: void showDesktop(); @@ -492,7 +497,7 @@ private: void mouseMoveEvent(QMouseEvent* event); void mousePressEvent(QMouseEvent* event); - void mouseDoublePressEvent(QMouseEvent* event) const; + void mouseDoublePressEvent(QMouseEvent* event); void mouseReleaseEvent(QMouseEvent* event); void touchBeginEvent(QTouchEvent* event); @@ -555,8 +560,6 @@ private: int _avatarSimsPerSecondReport {0}; quint64 _lastAvatarSimsPerSecondUpdate {0}; Camera _myCamera; // My view onto the world - Camera _mirrorCamera; // Camera for mirror view - QRect _mirrorViewRect; Setting::Handle _previousScriptLocation; Setting::Handle _fieldOfView; @@ -564,7 +567,7 @@ private: Setting::Handle _desktopTabletScale; Setting::Handle _desktopTabletBecomesToolbarSetting; Setting::Handle _hmdTabletBecomesToolbarSetting; - Setting::Handle _tabletVisibleToOthersSetting; + Setting::Handle _preferAvatarFingerOverStylusSetting; Setting::Handle _constrainToolbarPosition; float _scaleMirror; diff --git a/interface/src/DiscoverabilityManager.cpp b/interface/src/DiscoverabilityManager.cpp index 8fcc1e5477..98bfa9c0c7 100644 --- a/interface/src/DiscoverabilityManager.cpp +++ b/interface/src/DiscoverabilityManager.cpp @@ -40,9 +40,10 @@ void DiscoverabilityManager::updateLocation() { auto accountManager = DependencyManager::get(); auto addressManager = DependencyManager::get(); auto& domainHandler = DependencyManager::get()->getDomainHandler(); + bool discoverable = (_mode.get() != Discoverability::None); - if (_mode.get() != Discoverability::None && accountManager->isLoggedIn()) { + if (accountManager->isLoggedIn()) { // construct a QJsonObject given the user's current address information QJsonObject rootObject; @@ -50,34 +51,40 @@ void DiscoverabilityManager::updateLocation() { QString pathString = addressManager->currentPath(); - const QString PATH_KEY_IN_LOCATION = "path"; - locationObject.insert(PATH_KEY_IN_LOCATION, pathString); - const QString CONNECTED_KEY_IN_LOCATION = "connected"; - locationObject.insert(CONNECTED_KEY_IN_LOCATION, domainHandler.isConnected()); + locationObject.insert(CONNECTED_KEY_IN_LOCATION, discoverable && domainHandler.isConnected()); - if (!addressManager->getRootPlaceID().isNull()) { - const QString PLACE_ID_KEY_IN_LOCATION = "place_id"; - locationObject.insert(PLACE_ID_KEY_IN_LOCATION, - uuidStringWithoutCurlyBraces(addressManager->getRootPlaceID())); + if (discoverable || _lastLocationObject.isEmpty()) { // Don't consider changes to these as update-worthy if we're not discoverable. + const QString PATH_KEY_IN_LOCATION = "path"; + locationObject.insert(PATH_KEY_IN_LOCATION, pathString); + + if (!addressManager->getRootPlaceID().isNull()) { + const QString PLACE_ID_KEY_IN_LOCATION = "place_id"; + locationObject.insert(PLACE_ID_KEY_IN_LOCATION, + uuidStringWithoutCurlyBraces(addressManager->getRootPlaceID())); + } + + if (!domainHandler.getUUID().isNull()) { + const QString DOMAIN_ID_KEY_IN_LOCATION = "domain_id"; + locationObject.insert(DOMAIN_ID_KEY_IN_LOCATION, + uuidStringWithoutCurlyBraces(domainHandler.getUUID())); + } + + // in case the place/domain isn't in the database, we send the network address and port + auto& domainSockAddr = domainHandler.getSockAddr(); + const QString NETWORK_ADDRESS_KEY_IN_LOCATION = "network_address"; + locationObject.insert(NETWORK_ADDRESS_KEY_IN_LOCATION, domainSockAddr.getAddress().toString()); + + const QString NETWORK_ADDRESS_PORT_IN_LOCATION = "network_port"; + locationObject.insert(NETWORK_ADDRESS_PORT_IN_LOCATION, domainSockAddr.getPort()); + + const QString NODE_ID_IN_LOCATION = "node_id"; + const int UUID_REAL_LENGTH = 36; + locationObject.insert(NODE_ID_IN_LOCATION, DependencyManager::get()->getSessionUUID().toString().mid(1, UUID_REAL_LENGTH)); } - if (!domainHandler.getUUID().isNull()) { - const QString DOMAIN_ID_KEY_IN_LOCATION = "domain_id"; - locationObject.insert(DOMAIN_ID_KEY_IN_LOCATION, - uuidStringWithoutCurlyBraces(domainHandler.getUUID())); - } - - // in case the place/domain isn't in the database, we send the network address and port - auto& domainSockAddr = domainHandler.getSockAddr(); - const QString NETWORK_ADRESS_KEY_IN_LOCATION = "network_address"; - locationObject.insert(NETWORK_ADRESS_KEY_IN_LOCATION, domainSockAddr.getAddress().toString()); - - const QString NETWORK_ADDRESS_PORT_IN_LOCATION = "network_port"; - locationObject.insert(NETWORK_ADDRESS_PORT_IN_LOCATION, domainSockAddr.getPort()); - - const QString FRIENDS_ONLY_KEY_IN_LOCATION = "friends_only"; - locationObject.insert(FRIENDS_ONLY_KEY_IN_LOCATION, (_mode.get() == Discoverability::Friends)); + const QString AVAILABILITY_KEY_IN_LOCATION = "availability"; + locationObject.insert(AVAILABILITY_KEY_IN_LOCATION, findableByString(static_cast(_mode.get()))); JSONCallbackParameters callbackParameters; callbackParameters.jsonCallbackReceiver = this; @@ -139,19 +146,29 @@ void DiscoverabilityManager::setDiscoverabilityMode(Discoverability::Mode discov // update the setting to the new value _mode.set(static_cast(discoverabilityMode)); - - if (static_cast(_mode.get()) == Discoverability::None) { - // if we just got set to no discoverability, make sure that we delete our location in DB - removeLocation(); - } else { - // we have a discoverability mode that says we should send a location, do that right away - updateLocation(); - } + updateLocation(); // update right away emit discoverabilityModeChanged(discoverabilityMode); } } + +QString DiscoverabilityManager::findableByString(Discoverability::Mode discoverabilityMode) { + if (discoverabilityMode == Discoverability::None) { + return "none"; + } else if (discoverabilityMode == Discoverability::Friends) { + return "friends"; + } else if (discoverabilityMode == Discoverability::Connections) { + return "connections"; + } else if (discoverabilityMode == Discoverability::All) { + return "all"; + } else { + qDebug() << "GlobalServices findableByString called with an unrecognized value."; + return ""; + } +} + + void DiscoverabilityManager::setVisibility() { Menu* menu = Menu::getInstance(); diff --git a/interface/src/DiscoverabilityManager.h b/interface/src/DiscoverabilityManager.h index 196b0cdf81..96190b25d9 100644 --- a/interface/src/DiscoverabilityManager.h +++ b/interface/src/DiscoverabilityManager.h @@ -19,6 +19,7 @@ namespace Discoverability { enum Mode { None, Friends, + Connections, All }; } @@ -42,6 +43,9 @@ public slots: signals: void discoverabilityModeChanged(Discoverability::Mode discoverabilityMode); +public: + static QString findableByString(Discoverability::Mode discoverabilityMode); + private slots: void handleHeartbeatResponse(QNetworkReply& requestReply); diff --git a/interface/src/Menu.cpp b/interface/src/Menu.cpp index acf97ad5f7..23d689e339 100644 --- a/interface/src/Menu.cpp +++ b/interface/src/Menu.cpp @@ -74,9 +74,6 @@ Menu::Menu() { // File > Help addActionToQMenuAndActionHash(fileMenu, MenuOption::Help, 0, qApp, SLOT(showHelp())); - // File > About - addActionToQMenuAndActionHash(fileMenu, MenuOption::AboutApp, 0, qApp, SLOT(aboutApp()), QAction::AboutRole); - // File > Quit addActionToQMenuAndActionHash(fileMenu, MenuOption::Quit, Qt::CTRL | Qt::Key_Q, qApp, SLOT(quit()), QAction::QuitRole); @@ -120,11 +117,6 @@ Menu::Menu() { scriptEngines.data(), SLOT(reloadAllScripts()), QAction::NoRole, UNSPECIFIED_POSITION, "Advanced"); - // Edit > Scripts Editor... [advanced] - addActionToQMenuAndActionHash(editMenu, MenuOption::ScriptEditor, Qt::ALT | Qt::Key_S, - dialogsManager.data(), SLOT(showScriptEditor()), - QAction::NoRole, UNSPECIFIED_POSITION, "Advanced"); - // Edit > Console... [advanced] addActionToQMenuAndActionHash(editMenu, MenuOption::Console, Qt::CTRL | Qt::ALT | Qt::Key_J, DependencyManager::get().data(), @@ -171,10 +163,10 @@ Menu::Menu() { // Avatar > Attachments... auto action = addActionToQMenuAndActionHash(avatarMenu, MenuOption::Attachments); connect(action, &QAction::triggered, [] { - DependencyManager::get()->show(QString("hifi/dialogs/AttachmentsDialog.qml"), "AttachmentsDialog"); + qApp->showDialog(QString("hifi/dialogs/AttachmentsDialog.qml"), + QString("../../hifi/tablet/TabletAttachmentsDialog.qml"), "AttachmentsDialog"); }); - // Avatar > Size MenuWrapper* avatarSizeMenu = avatarMenu->addMenu("Size"); @@ -249,9 +241,6 @@ Menu::Menu() { viewMenu->addSeparator(); - // View > Mini Mirror - addCheckableActionToQMenuAndActionHash(viewMenu, MenuOption::MiniMirror, 0, false); - // View > Center Player In View addCheckableActionToQMenuAndActionHash(viewMenu, MenuOption::CenterPlayerInView, 0, true, qApp, SLOT(rotationModeChanged()), @@ -294,19 +283,22 @@ Menu::Menu() { // Settings > General... action = addActionToQMenuAndActionHash(settingsMenu, MenuOption::Preferences, Qt::CTRL | Qt::Key_Comma, nullptr, nullptr, QAction::PreferencesRole); connect(action, &QAction::triggered, [] { - DependencyManager::get()->toggle(QString("hifi/dialogs/GeneralPreferencesDialog.qml"), "GeneralPreferencesDialog"); + qApp->showDialog(QString("hifi/dialogs/GeneralPreferencesDialog.qml"), + QString("../../hifi/tablet/TabletGeneralPreferences.qml"), "GeneralPreferencesDialog"); }); // Settings > Avatar... action = addActionToQMenuAndActionHash(settingsMenu, "Avatar..."); connect(action, &QAction::triggered, [] { - DependencyManager::get()->toggle(QString("hifi/dialogs/AvatarPreferencesDialog.qml"), "AvatarPreferencesDialog"); + qApp->showDialog(QString("hifi/dialogs/AvatarPreferencesDialog.qml"), + QString("../../hifi/tablet/TabletAvatarPreferences.qml"), "AvatarPreferencesDialog"); }); // Settings > LOD... action = addActionToQMenuAndActionHash(settingsMenu, "LOD..."); connect(action, &QAction::triggered, [] { - DependencyManager::get()->toggle(QString("hifi/dialogs/LodPreferencesDialog.qml"), "LodPreferencesDialog"); + qApp->showDialog(QString("hifi/dialogs/LodPreferencesDialog.qml"), + QString("../../hifi/tablet/TabletLodPreferences.qml"), "LodPreferencesDialog"); }); // Settings > Control with Speech [advanced] @@ -327,7 +319,8 @@ Menu::Menu() { // Developer > Graphics... action = addActionToQMenuAndActionHash(developerMenu, "Graphics..."); connect(action, &QAction::triggered, [] { - DependencyManager::get()->toggle(QString("hifi/dialogs/GraphicsPreferencesDialog.qml"), "GraphicsPreferencesDialog"); + qApp->showDialog(QString("hifi/dialogs/GraphicsPreferencesDialog.qml"), + QString("../../hifi/tablet/TabletGraphicsPreferences.qml"), "GraphicsPreferencesDialog"); }); // Developer > Render >>> @@ -406,7 +399,8 @@ Menu::Menu() { // Developer > Render > LOD Tools - addActionToQMenuAndActionHash(renderOptionsMenu, MenuOption::LodTools, 0, dialogsManager.data(), SLOT(lodTools())); + addActionToQMenuAndActionHash(renderOptionsMenu, MenuOption::LodTools, 0, + qApp, SLOT(loadLODToolsDialog())); // HACK enable texture decimation { @@ -417,6 +411,9 @@ Menu::Menu() { } // Developer > Assets >>> + // Menu item is not currently needed but code should be kept in case it proves useful again at some stage. +//#define WANT_ASSET_MIGRATION +#ifdef WANT_ASSET_MIGRATION MenuWrapper* assetDeveloperMenu = developerMenu->addMenu("Assets"); auto& atpMigrator = ATPAssetMigrator::getInstance(); atpMigrator.setDialogParent(this); @@ -424,6 +421,7 @@ Menu::Menu() { addActionToQMenuAndActionHash(assetDeveloperMenu, MenuOption::AssetMigration, 0, &atpMigrator, SLOT(loadEntityServerFile())); +#endif // Developer > Avatar >>> MenuWrapper* avatarDebugMenu = developerMenu->addMenu("Avatar"); @@ -517,6 +515,8 @@ Menu::Menu() { avatar.get(), SLOT(setEnableInverseKinematics(bool))); addCheckableActionToQMenuAndActionHash(avatarDebugMenu, MenuOption::RenderSensorToWorldMatrix, 0, false, avatar.get(), SLOT(setEnableDebugDrawSensorToWorldMatrix(bool))); + addCheckableActionToQMenuAndActionHash(avatarDebugMenu, MenuOption::RenderIKTargets, 0, false, + avatar.get(), SLOT(setEnableDebugDrawIKTargets(bool))); addCheckableActionToQMenuAndActionHash(avatarDebugMenu, MenuOption::ActionMotorControl, Qt::CTRL | Qt::SHIFT | Qt::Key_K, true, avatar.get(), SLOT(updateMotionBehaviorFromMenu()), @@ -542,32 +542,30 @@ Menu::Menu() { // Developer > Entities >>> MenuWrapper* entitiesOptionsMenu = developerMenu->addMenu("Entities"); + addActionToQMenuAndActionHash(entitiesOptionsMenu, MenuOption::OctreeStats, 0, - dialogsManager.data(), SLOT(octreeStatsDetails())); + qApp, SLOT(loadEntityStatisticsDialog())); + addCheckableActionToQMenuAndActionHash(entitiesOptionsMenu, MenuOption::ShowRealtimeEntityStats); // Developer > Network >>> MenuWrapper* networkMenu = developerMenu->addMenu("Network"); action = addActionToQMenuAndActionHash(networkMenu, MenuOption::Networking); connect(action, &QAction::triggered, [] { - DependencyManager::get()->toggle(QUrl("hifi/dialogs/NetworkingPreferencesDialog.qml"), - "NetworkingPreferencesDialog"); + qApp->showDialog(QString("hifi/dialogs/NetworkingPreferencesDialog.qml"), + QString("../../hifi/tablet/TabletNetworkingPreferences.qml"), "NetworkingPreferencesDialog"); }); addActionToQMenuAndActionHash(networkMenu, MenuOption::ReloadContent, 0, qApp, SLOT(reloadResourceCaches())); + addActionToQMenuAndActionHash(networkMenu, MenuOption::ClearDiskCache, 0, + DependencyManager::get().data(), SLOT(clearCache())); addCheckableActionToQMenuAndActionHash(networkMenu, MenuOption::DisableActivityLogger, 0, false, &UserActivityLogger::getInstance(), SLOT(disable(bool))); - addActionToQMenuAndActionHash(networkMenu, MenuOption::CachesSize, 0, - dialogsManager.data(), SLOT(cachesSizeDialog())); - addActionToQMenuAndActionHash(networkMenu, MenuOption::DiskCacheEditor, 0, - dialogsManager.data(), SLOT(toggleDiskCacheEditor())); addActionToQMenuAndActionHash(networkMenu, MenuOption::ShowDSConnectTable, 0, - dialogsManager.data(), SLOT(showDomainConnectionDialog())); - addActionToQMenuAndActionHash(networkMenu, MenuOption::BandwidthDetails, 0, - dialogsManager.data(), SLOT(bandwidthDetails())); + qApp, SLOT(loadDomainConnectionDialog())); #if (PR_BUILD || DEV_BUILD) addCheckableActionToQMenuAndActionHash(networkMenu, MenuOption::SendWrongProtocolVersion, 0, false, @@ -577,7 +575,7 @@ Menu::Menu() { nodeList.data(), SLOT(toggleSendNewerDSConnectVersion(bool))); #endif - + // Developer >> Tests >>> MenuWrapper* testMenu = developerMenu->addMenu("Tests"); addActionToQMenuAndActionHash(testMenu, MenuOption::RunClientScriptTests, 0, dialogsManager.data(), SLOT(showTestingResults())); @@ -614,7 +612,8 @@ Menu::Menu() { action = addActionToQMenuAndActionHash(audioDebugMenu, "Buffers..."); connect(action, &QAction::triggered, [] { - DependencyManager::get()->toggle(QString("hifi/dialogs/AudioPreferencesDialog.qml"), "AudioPreferencesDialog"); + qApp->showDialog(QString("hifi/dialogs/AudioPreferencesDialog.qml"), + QString("../../hifi/tablet/TabletAudioPreferences.qml"), "AudioPreferencesDialog"); }); addCheckableActionToQMenuAndActionHash(audioDebugMenu, MenuOption::AudioNoiseReduction, 0, true, @@ -628,9 +627,9 @@ Menu::Menu() { auto scope = DependencyManager::get(); MenuWrapper* audioScopeMenu = audioDebugMenu->addMenu("Audio Scope"); - addCheckableActionToQMenuAndActionHash(audioScopeMenu, MenuOption::AudioScope, Qt::CTRL | Qt::Key_P, false, + addCheckableActionToQMenuAndActionHash(audioScopeMenu, MenuOption::AudioScope, Qt::CTRL | Qt::Key_F2, false, scope.data(), SLOT(toggle())); - addCheckableActionToQMenuAndActionHash(audioScopeMenu, MenuOption::AudioScopePause, Qt::CTRL | Qt::SHIFT | Qt::Key_P, false, + addCheckableActionToQMenuAndActionHash(audioScopeMenu, MenuOption::AudioScopePause, Qt::CTRL | Qt::SHIFT | Qt::Key_F2, false, scope.data(), SLOT(togglePause())); addDisabledActionAndSeparator(audioScopeMenu, "Display Frames"); @@ -711,14 +710,8 @@ Menu::Menu() { }); essLogAction->setEnabled(nodeList->getThisNodeCanRez()); - action = addActionToQMenuAndActionHash(developerMenu, "Script Log (HMD friendly)..."); - connect(action, &QAction::triggered, [] { - auto scriptEngines = DependencyManager::get(); - QUrl defaultScriptsLoc = defaultScriptsLocation(); - defaultScriptsLoc.setPath(defaultScriptsLoc.path() + "developer/debugging/debugWindow.js"); - scriptEngines->loadScript(defaultScriptsLoc.toString()); - }); - + action = addActionToQMenuAndActionHash(developerMenu, "Script Log (HMD friendly)...", Qt::NoButton, + qApp, SLOT(showScriptLogs())); // Developer > Stats addCheckableActionToQMenuAndActionHash(developerMenu, MenuOption::Stats); diff --git a/interface/src/Menu.h b/interface/src/Menu.h index 1b2564735b..14b2f4aeaa 100644 --- a/interface/src/Menu.h +++ b/interface/src/Menu.h @@ -26,7 +26,6 @@ public: }; namespace MenuOption { - const QString AboutApp = "About Interface"; const QString AddRemoveFriends = "Add/Remove Friends..."; const QString AddressBar = "Show Address Bar"; const QString Animations = "Animations..."; @@ -49,15 +48,14 @@ namespace MenuOption { const QString AutoMuteAudio = "Auto Mute Microphone"; const QString AvatarReceiveStats = "Show Receive Stats"; const QString Back = "Back"; - const QString BandwidthDetails = "Bandwidth Details"; const QString BinaryEyelidControl = "Binary Eyelid Control"; const QString BookmarkLocation = "Bookmark Location"; const QString Bookmarks = "Bookmarks"; - const QString CachesSize = "RAM Caches Size"; const QString CalibrateCamera = "Calibrate Camera"; const QString CameraEntityMode = "Entity Mode"; const QString CenterPlayerInView = "Center Player In View"; const QString Chat = "Chat..."; + const QString ClearDiskCache = "Clear Disk Cache"; const QString Collisions = "Collisions"; const QString Connexion = "Activate 3D Connexion Devices"; const QString Console = "Console..."; @@ -84,7 +82,6 @@ namespace MenuOption { const QString DisableActivityLogger = "Disable Activity Logger"; const QString DisableEyelidAdjustment = "Disable Eyelid Adjustment"; const QString DisableLightEntities = "Disable Light Entities"; - const QString DiskCacheEditor = "Disk Cache Editor"; const QString DisplayCrashOptions = "Display Crash Options"; const QString DisplayHandTargets = "Show Hand Targets"; const QString DisplayModelBounds = "Display Model Bounds"; @@ -125,7 +122,6 @@ namespace MenuOption { const QString LogExtraTimings = "Log Extra Timing Details"; const QString LowVelocityFilter = "Low Velocity Filter"; const QString MeshVisible = "Draw Mesh"; - const QString MiniMirror = "Mini Mirror"; const QString MuteAudio = "Mute Microphone"; const QString MuteEnvironment = "Mute Environment"; const QString MuteFaceTracking = "Mute Face Tracking"; @@ -165,12 +161,12 @@ namespace MenuOption { const QString RenderResolutionThird = "1/3"; const QString RenderResolutionQuarter = "1/4"; const QString RenderSensorToWorldMatrix = "Show SensorToWorld Matrix"; + const QString RenderIKTargets = "Show IK Targets"; const QString ResetAvatarSize = "Reset Avatar Size"; const QString ResetSensors = "Reset Sensors"; const QString RunningScripts = "Running Scripts..."; const QString RunClientScriptTests = "Run Client Script Tests"; const QString RunTimingTests = "Run Timing Tests"; - const QString ScriptEditor = "Script Editor..."; const QString ScriptedMotorControl = "Enable Scripted Motor Control"; const QString SendWrongDSConnectVersion = "Send wrong DS connect version"; const QString SendWrongProtocolVersion = "Send wrong protocol version"; diff --git a/interface/src/audio/AudioScope.cpp b/interface/src/audio/AudioScope.cpp index 346fbd11f4..cf9984e32b 100644 --- a/interface/src/audio/AudioScope.cpp +++ b/interface/src/audio/AudioScope.cpp @@ -52,12 +52,14 @@ AudioScope::AudioScope() : connect(audioIO.data(), &AudioClient::inputReceived, this, &AudioScope::addInputToScope); } -void AudioScope::toggle() { - _isEnabled = !_isEnabled; - if (_isEnabled) { - allocateScope(); - } else { - freeScope(); +void AudioScope::setVisible(bool visible) { + if (_isEnabled != visible) { + _isEnabled = visible; + if (_isEnabled) { + allocateScope(); + } else { + freeScope(); + } } } diff --git a/interface/src/audio/AudioScope.h b/interface/src/audio/AudioScope.h index 0b716d7666..615bdaf17f 100644 --- a/interface/src/audio/AudioScope.h +++ b/interface/src/audio/AudioScope.h @@ -34,8 +34,14 @@ public: void render(RenderArgs* renderArgs, int width, int height); public slots: - void toggle(); + void toggle() { setVisible(!_isEnabled); } + void setVisible(bool visible); + bool getVisible() const { return _isEnabled; } + void togglePause() { _isPaused = !_isPaused; } + void setPause(bool paused) { _isPaused = paused; } + bool getPause() { return _isPaused; } + void selectAudioScopeFiveFrames(); void selectAudioScopeTwentyFrames(); void selectAudioScopeFiftyFrames(); @@ -74,7 +80,6 @@ private: int _inputID; int _outputLeftID; int _outputRightD; - }; #endif // hifi_AudioScope_h diff --git a/interface/src/avatar/Avatar.cpp b/interface/src/avatar/Avatar.cpp index 6e1f44f5ac..b6ec45b308 100644 --- a/interface/src/avatar/Avatar.cpp +++ b/interface/src/avatar/Avatar.cpp @@ -348,6 +348,8 @@ void Avatar::simulate(float deltaTime, bool inView) { PROFILE_RANGE(simulation, "updateJoints"); if (inView && _hasNewJointData) { _skeletonModel->getRig()->copyJointsFromJointData(_jointData); + glm::mat4 rootTransform = glm::scale(_skeletonModel->getScale()) * glm::translate(_skeletonModel->getOffset()); + _skeletonModel->getRig()->computeExternalPoses(rootTransform); _jointDataSimulationRate.increment(); _skeletonModel->simulate(deltaTime, true); @@ -474,34 +476,34 @@ static TextRenderer3D* textRenderer(TextRendererType type) { return displayNameRenderer; } -bool Avatar::addToScene(AvatarSharedPointer self, std::shared_ptr scene, render::PendingChanges& pendingChanges) { +bool Avatar::addToScene(AvatarSharedPointer self, std::shared_ptr scene, render::Transaction& transaction) { auto avatarPayload = new render::Payload(self); auto avatarPayloadPointer = Avatar::PayloadPointer(avatarPayload); _renderItemID = scene->allocateID(); - pendingChanges.resetItem(_renderItemID, avatarPayloadPointer); - _skeletonModel->addToScene(scene, pendingChanges); + transaction.resetItem(_renderItemID, avatarPayloadPointer); + _skeletonModel->addToScene(scene, transaction); for (auto& attachmentModel : _attachmentModels) { - attachmentModel->addToScene(scene, pendingChanges); + attachmentModel->addToScene(scene, transaction); } _inScene = true; return true; } -void Avatar::removeFromScene(AvatarSharedPointer self, std::shared_ptr scene, render::PendingChanges& pendingChanges) { - pendingChanges.removeItem(_renderItemID); +void Avatar::removeFromScene(AvatarSharedPointer self, std::shared_ptr scene, render::Transaction& transaction) { + transaction.removeItem(_renderItemID); render::Item::clearID(_renderItemID); - _skeletonModel->removeFromScene(scene, pendingChanges); + _skeletonModel->removeFromScene(scene, transaction); for (auto& attachmentModel : _attachmentModels) { - attachmentModel->removeFromScene(scene, pendingChanges); + attachmentModel->removeFromScene(scene, transaction); } _inScene = false; } -void Avatar::updateRenderItem(render::PendingChanges& pendingChanges) { +void Avatar::updateRenderItem(render::Transaction& transaction) { if (render::Item::isValidID(_renderItemID)) { - pendingChanges.updateItem>(_renderItemID, [](render::Payload& p) {}); + transaction.updateItem>(_renderItemID, [](render::Payload& p) {}); } } @@ -678,24 +680,24 @@ void Avatar::fixupModelsInScene() { // check to see if when we added our models to the scene they were ready, if they were not ready, then // fix them up in the scene render::ScenePointer scene = qApp->getMain3DScene(); - render::PendingChanges pendingChanges; + render::Transaction transaction; if (_skeletonModel->isRenderable() && _skeletonModel->needsFixupInScene()) { - _skeletonModel->removeFromScene(scene, pendingChanges); - _skeletonModel->addToScene(scene, pendingChanges); + _skeletonModel->removeFromScene(scene, transaction); + _skeletonModel->addToScene(scene, transaction); } for (auto attachmentModel : _attachmentModels) { if (attachmentModel->isRenderable() && attachmentModel->needsFixupInScene()) { - attachmentModel->removeFromScene(scene, pendingChanges); - attachmentModel->addToScene(scene, pendingChanges); + attachmentModel->removeFromScene(scene, transaction); + attachmentModel->addToScene(scene, transaction); } } for (auto attachmentModelToRemove : _attachmentsToRemove) { - attachmentModelToRemove->removeFromScene(scene, pendingChanges); + attachmentModelToRemove->removeFromScene(scene, transaction); } _attachmentsToDelete.insert(_attachmentsToDelete.end(), _attachmentsToRemove.begin(), _attachmentsToRemove.end()); _attachmentsToRemove.clear(); - scene->enqueuePendingChanges(pendingChanges); + scene->enqueueTransaction(transaction); } bool Avatar::shouldRenderHead(const RenderArgs* renderArgs) const { @@ -1393,17 +1395,41 @@ void Avatar::setParentJointIndex(quint16 parentJointIndex) { } } +QList Avatar::getSkeleton() { + SkeletonModelPointer skeletonModel = _skeletonModel; + if (skeletonModel) { + RigPointer rig = skeletonModel->getRig(); + if (rig) { + AnimSkeleton::ConstPointer skeleton = rig->getAnimSkeleton(); + if (skeleton) { + QList list; + list.reserve(skeleton->getNumJoints()); + for (int i = 0; i < skeleton->getNumJoints(); i++) { + QVariantMap obj; + obj["name"] = skeleton->getJointName(i); + obj["index"] = i; + obj["parentIndex"] = skeleton->getParentIndex(i); + list.push_back(obj); + } + return list; + } + } + } + + return QList(); +} + void Avatar::addToScene(AvatarSharedPointer myHandle) { render::ScenePointer scene = qApp->getMain3DScene(); if (scene) { - render::PendingChanges pendingChanges; + render::Transaction transaction; auto nodelist = DependencyManager::get(); if (DependencyManager::get()->shouldRenderAvatars() && !nodelist->isIgnoringNode(getSessionUUID()) && !nodelist->isRadiusIgnoringNode(getSessionUUID())) { - addToScene(myHandle, scene, pendingChanges); + addToScene(myHandle, scene, transaction); } - scene->enqueuePendingChanges(pendingChanges); + scene->enqueueTransaction(transaction); } else { qCWarning(interfaceapp) << "AvatarManager::addAvatar() : Unexpected null scene, possibly during application shutdown"; } diff --git a/interface/src/avatar/Avatar.h b/interface/src/avatar/Avatar.h index ca4dbd2af8..7fadf96e11 100644 --- a/interface/src/avatar/Avatar.h +++ b/interface/src/avatar/Avatar.h @@ -55,6 +55,16 @@ class Texture; class Avatar : public AvatarData { Q_OBJECT + + /**jsdoc + * An avatar is representation of yourself or another user. The Avatar API can be used to query or manipulate the avatar of a user. + * NOTE: Avatar extends AvatarData, see those namespace for more properties/methods. + * + * @namespace Avatar + * @augments AvatarData + * + * @property skeletonOffset {Vec3} can be used to apply a translation offset between the avatar's position and the registration point of the 3d model. + */ Q_PROPERTY(glm::vec3 skeletonOffset READ getSkeletonOffset WRITE setSkeletonOffset) public: @@ -72,12 +82,12 @@ public: virtual void render(RenderArgs* renderArgs, const glm::vec3& cameraPosition); bool addToScene(AvatarSharedPointer self, std::shared_ptr scene, - render::PendingChanges& pendingChanges); + render::Transaction& transaction); void removeFromScene(AvatarSharedPointer self, std::shared_ptr scene, - render::PendingChanges& pendingChanges); + render::Transaction& transaction); - void updateRenderItem(render::PendingChanges& pendingChanges); + void updateRenderItem(render::Transaction& transaction); virtual void postUpdate(float deltaTime); @@ -169,6 +179,21 @@ public: Q_INVOKABLE virtual quint16 getParentJointIndex() const override { return SpatiallyNestable::getParentJointIndex(); } Q_INVOKABLE virtual void setParentJointIndex(quint16 parentJointIndex) override; + /**jsdoc + * Information about a single joint in an Avatar's skeleton hierarchy. + * @typedef Avatar.SkeletonJoint + * @property {string} name - name of joint + * @property {number} index - joint index + * @property {number} parentIndex - index of this joint's parent (-1 if no parent) + */ + + /**jsdoc + * Returns an array of joints, where each joint is an object containing name, index and parentIndex fields. + * @function Avatar.getSkeleton + * @returns {Avatar.SkeletonJoint[]} returns a list of information about each joint in this avatar's skeleton. + */ + Q_INVOKABLE QList getSkeleton(); + // NOT thread safe, must be called on main thread. glm::vec3 getUncachedLeftPalmPosition() const; glm::quat getUncachedLeftPalmRotation() const; @@ -236,7 +261,6 @@ protected: glm::vec3 getBodyRightDirection() const { return getOrientation() * IDENTITY_RIGHT; } glm::vec3 getBodyUpDirection() const { return getOrientation() * IDENTITY_UP; } - glm::vec3 getBodyFrontDirection() const { return getOrientation() * IDENTITY_FRONT; } glm::quat computeRotationFromBodyToWorldUp(float proportion = 1.0f) const; void measureMotionDerivatives(float deltaTime); diff --git a/interface/src/avatar/AvatarManager.cpp b/interface/src/avatar/AvatarManager.cpp index 7417f73102..48914908c6 100644 --- a/interface/src/avatar/AvatarManager.cpp +++ b/interface/src/avatar/AvatarManager.cpp @@ -104,11 +104,11 @@ void AvatarManager::init() { this, &AvatarManager::updateAvatarRenderStatus, Qt::QueuedConnection); render::ScenePointer scene = qApp->getMain3DScene(); - render::PendingChanges pendingChanges; + render::Transaction transaction; if (DependencyManager::get()->shouldRenderAvatars()) { - _myAvatar->addToScene(_myAvatar, scene, pendingChanges); + _myAvatar->addToScene(_myAvatar, scene, transaction); } - scene->enqueuePendingChanges(pendingChanges); + scene->enqueueTransaction(transaction); } void AvatarManager::updateMyAvatar(float deltaTime) { @@ -192,7 +192,7 @@ void AvatarManager::updateOtherAvatars(float deltaTime) { return false; }); - render::PendingChanges pendingChanges; + render::Transaction transaction; uint64_t startTime = usecTimestampNow(); const uint64_t UPDATE_BUDGET = 2000; // usec uint64_t updateExpiry = startTime + UPDATE_BUDGET; @@ -228,7 +228,7 @@ void AvatarManager::updateOtherAvatars(float deltaTime) { numAvatarsUpdated++; } avatar->simulate(deltaTime, inView); - avatar->updateRenderItem(pendingChanges); + avatar->updateRenderItem(transaction); avatar->setLastRenderUpdateTime(startTime); } else { // we've spent our full time budget --> bail on the rest of the avatar updates @@ -262,7 +262,7 @@ void AvatarManager::updateOtherAvatars(float deltaTime) { _avatarSimulationTime = (float)(usecTimestampNow() - startTime) / (float)USECS_PER_MSEC; _numAvatarsUpdated = numAvatarsUpdated; _numAvatarsNotUpdated = numAVatarsNotUpdated; - qApp->getMain3DScene()->enqueuePendingChanges(pendingChanges); + qApp->getMain3DScene()->enqueueTransaction(transaction); simulateAvatarFades(deltaTime); } @@ -283,13 +283,13 @@ void AvatarManager::simulateAvatarFades(float deltaTime) { const float MIN_FADE_SCALE = MIN_AVATAR_SCALE; render::ScenePointer scene = qApp->getMain3DScene(); - render::PendingChanges pendingChanges; + render::Transaction transaction; while (fadingIterator != _avatarFades.end()) { auto avatar = std::static_pointer_cast(*fadingIterator); avatar->setTargetScale(avatar->getUniformScale() * SHRINK_RATE); avatar->animateScaleChanges(deltaTime); if (avatar->getTargetScale() <= MIN_FADE_SCALE) { - avatar->removeFromScene(*fadingIterator, scene, pendingChanges); + avatar->removeFromScene(*fadingIterator, scene, transaction); // only remove from _avatarFades if we're sure its motionState has been removed from PhysicsEngine if (_motionStatesToRemoveFromPhysics.empty()) { fadingIterator = _avatarFades.erase(fadingIterator); @@ -302,7 +302,7 @@ void AvatarManager::simulateAvatarFades(float deltaTime) { ++fadingIterator; } } - scene->enqueuePendingChanges(pendingChanges); + scene->enqueueTransaction(transaction); } AvatarSharedPointer AvatarManager::newSharedAvatar() { @@ -329,7 +329,7 @@ void AvatarManager::removeAvatar(const QUuid& sessionUUID, KillAvatarReason remo } void AvatarManager::handleRemovedAvatar(const AvatarSharedPointer& removedAvatar, KillAvatarReason removalReason) { - AvatarHashMap::handleRemovedAvatar(removedAvatar); + AvatarHashMap::handleRemovedAvatar(removedAvatar, removalReason); // removedAvatar is a shared pointer to an AvatarData but we need to get to the derived Avatar // class in this context so we can call methods that don't exist at the base class. @@ -424,7 +424,7 @@ void AvatarManager::getObjectsToChange(VectorOfMotionStates& result) { } } -void AvatarManager::handleOutgoingChanges(const VectorOfMotionStates& motionStates) { +void AvatarManager::handleChangedMotionStates(const VectorOfMotionStates& motionStates) { // TODO: extract the MyAvatar results once we use a MotionState for it. } @@ -479,17 +479,17 @@ void AvatarManager::updateAvatarRenderStatus(bool shouldRenderAvatars) { for (auto avatarData : _avatarHash) { auto avatar = std::static_pointer_cast(avatarData); render::ScenePointer scene = qApp->getMain3DScene(); - render::PendingChanges pendingChanges; - avatar->addToScene(avatar, scene, pendingChanges); - scene->enqueuePendingChanges(pendingChanges); + render::Transaction transaction; + avatar->addToScene(avatar, scene, transaction); + scene->enqueueTransaction(transaction); } } else { for (auto avatarData : _avatarHash) { auto avatar = std::static_pointer_cast(avatarData); render::ScenePointer scene = qApp->getMain3DScene(); - render::PendingChanges pendingChanges; - avatar->removeFromScene(avatar, scene, pendingChanges); - scene->enqueuePendingChanges(pendingChanges); + render::Transaction transaction; + avatar->removeFromScene(avatar, scene, transaction); + scene->enqueueTransaction(transaction); } } } diff --git a/interface/src/avatar/AvatarManager.h b/interface/src/avatar/AvatarManager.h index e1f5a3b411..b94f9e6a96 100644 --- a/interface/src/avatar/AvatarManager.h +++ b/interface/src/avatar/AvatarManager.h @@ -70,7 +70,7 @@ public: void getObjectsToRemoveFromPhysics(VectorOfMotionStates& motionStates); void getObjectsToAddToPhysics(VectorOfMotionStates& motionStates); void getObjectsToChange(VectorOfMotionStates& motionStates); - void handleOutgoingChanges(const VectorOfMotionStates& motionStates); + void handleChangedMotionStates(const VectorOfMotionStates& motionStates); void handleCollisionEvents(const CollisionEvents& collisionEvents); Q_INVOKABLE float getAvatarDataRate(const QUuid& sessionID, const QString& rateName = QString("")) const; diff --git a/interface/src/avatar/CauterizedMeshPartPayload.cpp b/interface/src/avatar/CauterizedMeshPartPayload.cpp index c8ec90dcee..c11f92083b 100644 --- a/interface/src/avatar/CauterizedMeshPartPayload.cpp +++ b/interface/src/avatar/CauterizedMeshPartPayload.cpp @@ -20,55 +20,28 @@ using namespace render; CauterizedMeshPartPayload::CauterizedMeshPartPayload(Model* model, int meshIndex, int partIndex, int shapeIndex, const Transform& transform, const Transform& offsetTransform) : ModelMeshPartPayload(model, meshIndex, partIndex, shapeIndex, transform, offsetTransform) {} -void CauterizedMeshPartPayload::updateTransformForSkinnedCauterizedMesh(const Transform& transform, - const QVector& clusterMatrices, - const QVector& cauterizedClusterMatrices) { - _transform = transform; - _cauterizedTransform = transform; - - if (clusterMatrices.size() > 0) { - _worldBound = AABox(); - for (auto& clusterMatrix : clusterMatrices) { - AABox clusterBound = _localBound; - clusterBound.transform(clusterMatrix); - _worldBound += clusterBound; - } - - _worldBound.transform(transform); - if (clusterMatrices.size() == 1) { - _transform = _transform.worldTransform(Transform(clusterMatrices[0])); - if (cauterizedClusterMatrices.size() != 0) { - _cauterizedTransform = _cauterizedTransform.worldTransform(Transform(cauterizedClusterMatrices[0])); - } else { - _cauterizedTransform = _transform; - } - } - } else { - _worldBound = _localBound; - _worldBound.transform(_drawTransform); - } +void CauterizedMeshPartPayload::updateTransformForCauterizedMesh( + const Transform& renderTransform, + const gpu::BufferPointer& buffer) { + _cauterizedTransform = renderTransform; + _cauterizedClusterBuffer = buffer; } void CauterizedMeshPartPayload::bindTransform(gpu::Batch& batch, const render::ShapePipeline::LocationsPointer locations, RenderArgs::RenderMode renderMode) const { // Still relying on the raw data from the model - const Model::MeshState& state = _model->getMeshState(_meshIndex); SkeletonModel* skeleton = static_cast(_model); bool useCauterizedMesh = (renderMode != RenderArgs::RenderMode::SHADOW_RENDER_MODE) && skeleton->getEnableCauterization(); - if (state.clusterBuffer) { - if (useCauterizedMesh) { - const Model::MeshState& cState = skeleton->getCauterizeMeshState(_meshIndex); - batch.setUniformBuffer(ShapePipeline::Slot::BUFFER::SKINNING, cState.clusterBuffer); - } else { - batch.setUniformBuffer(ShapePipeline::Slot::BUFFER::SKINNING, state.clusterBuffer); + if (useCauterizedMesh) { + if (_cauterizedClusterBuffer) { + batch.setUniformBuffer(ShapePipeline::Slot::BUFFER::SKINNING, _cauterizedClusterBuffer); + } + batch.setModelTransform(_cauterizedTransform); + } else { + if (_clusterBuffer) { + batch.setUniformBuffer(ShapePipeline::Slot::BUFFER::SKINNING, _clusterBuffer); } batch.setModelTransform(_transform); - } else { - if (useCauterizedMesh) { - batch.setModelTransform(_cauterizedTransform); - } else { - batch.setModelTransform(_transform); - } } } diff --git a/interface/src/avatar/CauterizedMeshPartPayload.h b/interface/src/avatar/CauterizedMeshPartPayload.h index f4319ead6f..dc88e950c1 100644 --- a/interface/src/avatar/CauterizedMeshPartPayload.h +++ b/interface/src/avatar/CauterizedMeshPartPayload.h @@ -17,12 +17,13 @@ class CauterizedMeshPartPayload : public ModelMeshPartPayload { public: CauterizedMeshPartPayload(Model* model, int meshIndex, int partIndex, int shapeIndex, const Transform& transform, const Transform& offsetTransform); - void updateTransformForSkinnedCauterizedMesh(const Transform& transform, - const QVector& clusterMatrices, - const QVector& cauterizedClusterMatrices); + + void updateTransformForCauterizedMesh(const Transform& renderTransform, const gpu::BufferPointer& buffer); void bindTransform(gpu::Batch& batch, const render::ShapePipeline::LocationsPointer locations, RenderArgs::RenderMode renderMode) const override; + private: + gpu::BufferPointer _cauterizedClusterBuffer; Transform _cauterizedTransform; }; diff --git a/interface/src/avatar/CauterizedModel.cpp b/interface/src/avatar/CauterizedModel.cpp index 0c3d863649..c7d25d3151 100644 --- a/interface/src/avatar/CauterizedModel.cpp +++ b/interface/src/avatar/CauterizedModel.cpp @@ -26,8 +26,8 @@ CauterizedModel::~CauterizedModel() { } void CauterizedModel::deleteGeometry() { - Model::deleteGeometry(); - _cauterizeMeshStates.clear(); + Model::deleteGeometry(); + _cauterizeMeshStates.clear(); } bool CauterizedModel::updateGeometry() { @@ -41,7 +41,7 @@ bool CauterizedModel::updateGeometry() { _cauterizeMeshStates.append(state); } } - return needsFullUpdate; + return needsFullUpdate; } void CauterizedModel::createVisibleRenderItemSet() { @@ -56,9 +56,9 @@ void CauterizedModel::createVisibleRenderItemSet() { } // We should not have any existing renderItems if we enter this section of code - Q_ASSERT(_modelMeshRenderItemsSet.isEmpty()); + Q_ASSERT(_modelMeshRenderItems.isEmpty()); - _modelMeshRenderItemsSet.clear(); + _modelMeshRenderItems.clear(); Transform transform; transform.setTranslation(_translation); @@ -81,18 +81,18 @@ void CauterizedModel::createVisibleRenderItemSet() { int numParts = (int)mesh->getNumParts(); for (int partIndex = 0; partIndex < numParts; partIndex++) { auto ptr = std::make_shared(this, i, partIndex, shapeID, transform, offset); - _modelMeshRenderItemsSet << std::static_pointer_cast(ptr); + _modelMeshRenderItems << std::static_pointer_cast(ptr); shapeID++; } } } else { - Model::createVisibleRenderItemSet(); + Model::createVisibleRenderItemSet(); } } void CauterizedModel::createCollisionRenderItemSet() { // Temporary HACK: use base class method for now - Model::createCollisionRenderItemSet(); + Model::createCollisionRenderItemSet(); } void CauterizedModel::updateClusterMatrices() { @@ -110,13 +110,7 @@ void CauterizedModel::updateClusterMatrices() { for (int j = 0; j < mesh.clusters.size(); j++) { const FBXCluster& cluster = mesh.clusters.at(j); auto jointMatrix = _rig->getJointTransform(cluster.jointIndex); -#if (GLM_ARCH & GLM_ARCH_SSE2) && !(defined Q_OS_MAC) - glm::mat4 out, inverseBindMatrix = cluster.inverseBindMatrix; - glm_mat4_mul((glm_vec4*)&jointMatrix, (glm_vec4*)&inverseBindMatrix, (glm_vec4*)&out); - state.clusterMatrices[j] = out; -#else - state.clusterMatrices[j] = jointMatrix * cluster.inverseBindMatrix; -#endif + glm_mat4u_mul(jointMatrix, cluster.inverseBindMatrix, state.clusterMatrices[j]); } // Once computed the cluster matrices, update the buffer(s) @@ -128,8 +122,8 @@ void CauterizedModel::updateClusterMatrices() { state.clusterBuffer->setSubData(0, state.clusterMatrices.size() * sizeof(glm::mat4), (const gpu::Byte*) state.clusterMatrices.constData()); } - } - } + } + } // as an optimization, don't build cautrizedClusterMatrices if the boneSet is empty. if (!_cauterizeBoneSet.empty()) { @@ -149,13 +143,7 @@ void CauterizedModel::updateClusterMatrices() { if (_cauterizeBoneSet.find(cluster.jointIndex) != _cauterizeBoneSet.end()) { jointMatrix = cauterizeMatrix; } -#if (GLM_ARCH & GLM_ARCH_SSE2) && !(defined Q_OS_MAC) - glm::mat4 out, inverseBindMatrix = cluster.inverseBindMatrix; - glm_mat4_mul((glm_vec4*)&jointMatrix, (glm_vec4*)&inverseBindMatrix, (glm_vec4*)&out); - state.clusterMatrices[j] = out; -#else - state.clusterMatrices[j] = jointMatrix * cluster.inverseBindMatrix; -#endif + glm_mat4u_mul(jointMatrix, cluster.inverseBindMatrix, state.clusterMatrices[j]); } if (!_cauterizeBoneSet.empty() && (state.clusterMatrices.size() > 1)) { @@ -203,6 +191,9 @@ void CauterizedModel::updateRenderItems() { return; } + // lazy update of cluster matrices used for rendering. We need to update them here, so we can correctly update the bounding box. + self->updateClusterMatrices(); + render::ScenePointer scene = AbstractViewStateInterface::instance()->getMain3DScene(); Transform modelTransform; @@ -214,28 +205,35 @@ void CauterizedModel::updateRenderItems() { uint32_t deleteGeometryCounter = self->getGeometryCounter(); - render::PendingChanges pendingChanges; + render::Transaction transaction; QList keys = self->getRenderItems().keys(); foreach (auto itemID, keys) { - pendingChanges.updateItem(itemID, [modelTransform, deleteGeometryCounter](CauterizedMeshPartPayload& data) { + transaction.updateItem(itemID, [modelTransform, deleteGeometryCounter](CauterizedMeshPartPayload& data) { if (data._model && data._model->isLoaded()) { // Ensure the model geometry was not reset between frames if (deleteGeometryCounter == data._model->getGeometryCounter()) { - // lazy update of cluster matrices used for rendering. We need to update them here, so we can correctly update the bounding box. - data._model->updateClusterMatrices(); - - // update the model transform and bounding box for this render item. + // this stuff identical to what happens in regular Model const Model::MeshState& state = data._model->getMeshState(data._meshIndex); + Transform renderTransform = modelTransform; + if (state.clusterMatrices.size() == 1) { + renderTransform = modelTransform.worldTransform(Transform(state.clusterMatrices[0])); + } + data.updateTransformForSkinnedMesh(renderTransform, modelTransform, state.clusterBuffer); + + // this stuff for cauterized mesh CauterizedModel* cModel = static_cast(data._model); - assert(data._meshIndex < cModel->_cauterizeMeshStates.size()); - const Model::MeshState& cState = cModel->_cauterizeMeshStates.at(data._meshIndex); - data.updateTransformForSkinnedCauterizedMesh(modelTransform, state.clusterMatrices, cState.clusterMatrices); + const Model::MeshState& cState = cModel->getCauterizeMeshState(data._meshIndex); + renderTransform = modelTransform; + if (cState.clusterMatrices.size() == 1) { + renderTransform = modelTransform.worldTransform(Transform(cState.clusterMatrices[0])); + } + data.updateTransformForCauterizedMesh(renderTransform, cState.clusterBuffer); } } }); } - scene->enqueuePendingChanges(pendingChanges); + scene->enqueueTransaction(transaction); }); } else { Model::updateRenderItems(); diff --git a/interface/src/avatar/Head.cpp b/interface/src/avatar/Head.cpp index d7bf2b79bf..4e748037a4 100644 --- a/interface/src/avatar/Head.cpp +++ b/interface/src/avatar/Head.cpp @@ -71,6 +71,8 @@ void Head::reset() { } void Head::simulate(float deltaTime, bool isMine) { + const float NORMAL_HZ = 60.0f; // the update rate the constant values were tuned for + // Update audio trailing average for rendering facial animations const float AUDIO_AVERAGING_SECS = 0.05f; const float AUDIO_LONG_TERM_AVERAGING_SECS = 30.0f; @@ -94,7 +96,7 @@ void Head::simulate(float deltaTime, bool isMine) { if (typeid(*faceTracker) == typeid(DdeFaceTracker)) { if (Menu::getInstance()->isOptionChecked(MenuOption::UseAudioForMouth)) { - calculateMouthShapes(); + calculateMouthShapes(deltaTime); const int JAW_OPEN_BLENDSHAPE = 21; const int MMMM_BLENDSHAPE = 34; @@ -116,7 +118,7 @@ void Head::simulate(float deltaTime, bool isMine) { _isEyeTrackerConnected = eyeTracker->isTracking(); } } - + if (!_isFaceTrackerConnected) { if (!_isEyeTrackerConnected) { @@ -144,22 +146,23 @@ void Head::simulate(float deltaTime, bool isMine) { _timeWithoutTalking += deltaTime; if ((_averageLoudness - _longTermAverageLoudness) > TALKING_LOUDNESS) { _timeWithoutTalking = 0.0f; - + } else if (_timeWithoutTalking < BLINK_AFTER_TALKING && _timeWithoutTalking >= BLINK_AFTER_TALKING) { forceBlink = true; } - + // Update audio attack data for facial animation (eyebrows and mouth) - const float AUDIO_ATTACK_AVERAGING_RATE = 0.9f; - _audioAttack = AUDIO_ATTACK_AVERAGING_RATE * _audioAttack + (1.0f - AUDIO_ATTACK_AVERAGING_RATE) * fabs((_audioLoudness - _longTermAverageLoudness) - _lastLoudness); + float audioAttackAveragingRate = (10.0f - deltaTime * NORMAL_HZ) / 10.0f; // --> 0.9 at 60 Hz + _audioAttack = audioAttackAveragingRate * _audioAttack + + (1.0f - audioAttackAveragingRate) * fabs((_audioLoudness - _longTermAverageLoudness) - _lastLoudness); _lastLoudness = (_audioLoudness - _longTermAverageLoudness); - + const float BROW_LIFT_THRESHOLD = 100.0f; if (_audioAttack > BROW_LIFT_THRESHOLD) { _browAudioLift += sqrtf(_audioAttack) * 0.01f; } _browAudioLift = glm::clamp(_browAudioLift *= 0.7f, 0.0f, 1.0f); - + const float BLINK_SPEED = 10.0f; const float BLINK_SPEED_VARIABILITY = 1.0f; const float BLINK_START_VARIABILITY = 0.25f; @@ -182,23 +185,23 @@ void Head::simulate(float deltaTime, bool isMine) { } else { _leftEyeBlink = glm::clamp(_leftEyeBlink + _leftEyeBlinkVelocity * deltaTime, FULLY_OPEN, FULLY_CLOSED); _rightEyeBlink = glm::clamp(_rightEyeBlink + _rightEyeBlinkVelocity * deltaTime, FULLY_OPEN, FULLY_CLOSED); - + if (_leftEyeBlink == FULLY_CLOSED) { _leftEyeBlinkVelocity = -BLINK_SPEED; - + } else if (_leftEyeBlink == FULLY_OPEN) { _leftEyeBlinkVelocity = 0.0f; } if (_rightEyeBlink == FULLY_CLOSED) { _rightEyeBlinkVelocity = -BLINK_SPEED; - + } else if (_rightEyeBlink == FULLY_OPEN) { _rightEyeBlinkVelocity = 0.0f; } } - + // use data to update fake Faceshift blendshape coefficients - calculateMouthShapes(); + calculateMouthShapes(deltaTime); DependencyManager::get()->updateFakeCoefficients(_leftEyeBlink, _rightEyeBlink, _browAudioLift, @@ -216,7 +219,7 @@ void Head::simulate(float deltaTime, bool isMine) { if (Menu::getInstance()->isOptionChecked(MenuOption::FixGaze)) { // if debug menu turns off, use no saccade _saccade = glm::vec3(); } - + _leftEyePosition = _rightEyePosition = getPosition(); _eyePosition = getPosition(); @@ -230,7 +233,7 @@ void Head::simulate(float deltaTime, bool isMine) { _eyePosition = calculateAverageEyePosition(); } -void Head::calculateMouthShapes() { +void Head::calculateMouthShapes(float deltaTime) { const float JAW_OPEN_SCALE = 0.015f; const float JAW_OPEN_RATE = 0.9f; const float JAW_CLOSE_RATE = 0.90f; @@ -242,20 +245,24 @@ void Head::calculateMouthShapes() { const float SMILE_SPEED = 1.0f; const float FUNNEL_SPEED = 2.335f; const float STOP_GAIN = 5.0f; + const float NORMAL_HZ = 60.0f; // the update rate the constant values were tuned for + + float deltaTimeRatio = deltaTime / (1.0f / NORMAL_HZ); // From the change in loudness, decide how much to open or close the jaw float audioDelta = sqrtf(glm::max(_averageLoudness - _longTermAverageLoudness, 0.0f)) * JAW_OPEN_SCALE; if (audioDelta > _audioJawOpen) { - _audioJawOpen += (audioDelta - _audioJawOpen) * JAW_OPEN_RATE; + _audioJawOpen += (audioDelta - _audioJawOpen) * JAW_OPEN_RATE * deltaTimeRatio; } else { - _audioJawOpen *= JAW_CLOSE_RATE; + _audioJawOpen *= powf(JAW_CLOSE_RATE, deltaTimeRatio); } _audioJawOpen = glm::clamp(_audioJawOpen, 0.0f, 1.0f); - _trailingAudioJawOpen = glm::mix(_trailingAudioJawOpen, _audioJawOpen, 0.99f); + float trailingAudioJawOpenRatio = (100.0f - deltaTime * NORMAL_HZ) / 100.0f; // --> 0.99 at 60 Hz + _trailingAudioJawOpen = glm::mix(_trailingAudioJawOpen, _audioJawOpen, trailingAudioJawOpenRatio); - // Advance time at a rate proportional to loudness, and move the mouth shapes through + // Advance time at a rate proportional to loudness, and move the mouth shapes through // a cycle at differing speeds to create a continuous random blend of shapes. - _mouthTime += sqrtf(_averageLoudness) * TIMESTEP_CONSTANT; + _mouthTime += sqrtf(_averageLoudness) * TIMESTEP_CONSTANT * deltaTimeRatio; _mouth2 = (sinf(_mouthTime * MMMM_SPEED) + 1.0f) * MMMM_POWER * glm::min(1.0f, _trailingAudioJawOpen * STOP_GAIN); _mouth3 = (sinf(_mouthTime * FUNNEL_SPEED) + 1.0f) * FUNNEL_POWER * glm::min(1.0f, _trailingAudioJawOpen * STOP_GAIN); _mouth4 = (sinf(_mouthTime * SMILE_SPEED) + 1.0f) * SMILE_POWER * glm::min(1.0f, _trailingAudioJawOpen * STOP_GAIN); @@ -268,7 +275,7 @@ void Head::applyEyelidOffset(glm::quat headOrientation) { return; } - glm::quat eyeRotation = rotationBetween(headOrientation * IDENTITY_FRONT, getLookAtPosition() - _eyePosition); + glm::quat eyeRotation = rotationBetween(headOrientation * IDENTITY_FORWARD, getLookAtPosition() - _eyePosition); eyeRotation = eyeRotation * glm::angleAxis(safeEulerAngles(headOrientation).y, IDENTITY_UP); // Rotation w.r.t. head float eyePitch = safeEulerAngles(eyeRotation).x; @@ -321,7 +328,7 @@ glm::quat Head::getFinalOrientationInLocalFrame() const { // // Everyone else's head also keeps track of a correctedLookAtPosition that may be different for the same head within // different Interfaces. If that head is not looking at me, the correctedLookAtPosition is the same as the lookAtPosition. -// However, if that head is looking at me, then I will attempt to adjust the lookAtPosition by the difference between +// However, if that head is looking at me, then I will attempt to adjust the lookAtPosition by the difference between // my (singular) eye position and my actual camera position. This adjustment is used on their eyeballs during rendering // (and also on any lookAt vector display for that head, during rendering). Note that: // 1. this adjustment can be made directly to the other head's eyeball joints, because we won't be send their joint information to others. @@ -375,7 +382,7 @@ glm::quat Head::getCameraOrientation() const { glm::quat Head::getEyeRotation(const glm::vec3& eyePosition) const { glm::quat orientation = getOrientation(); glm::vec3 lookAtDelta = _lookAtPosition - eyePosition; - return rotationBetween(orientation * IDENTITY_FRONT, lookAtDelta + glm::length(lookAtDelta) * _saccade) * orientation; + return rotationBetween(orientation * IDENTITY_FORWARD, lookAtDelta + glm::length(lookAtDelta) * _saccade) * orientation; } void Head::setFinalPitch(float finalPitch) { diff --git a/interface/src/avatar/Head.h b/interface/src/avatar/Head.h index 3d25c79087..fd20e709f5 100644 --- a/interface/src/avatar/Head.h +++ b/interface/src/avatar/Head.h @@ -58,14 +58,14 @@ public: const glm::vec3& getSaccade() const { return _saccade; } glm::vec3 getRightDirection() const { return getOrientation() * IDENTITY_RIGHT; } glm::vec3 getUpDirection() const { return getOrientation() * IDENTITY_UP; } - glm::vec3 getFrontDirection() const { return getOrientation() * IDENTITY_FRONT; } + glm::vec3 getForwardDirection() const { return getOrientation() * IDENTITY_FORWARD; } glm::quat getEyeRotation(const glm::vec3& eyePosition) const; const glm::vec3& getRightEyePosition() const { return _rightEyePosition; } const glm::vec3& getLeftEyePosition() const { return _leftEyePosition; } - glm::vec3 getRightEarPosition() const { return _rightEyePosition + (getRightDirection() * EYE_EAR_GAP) + (getFrontDirection() * -EYE_EAR_GAP); } - glm::vec3 getLeftEarPosition() const { return _leftEyePosition + (getRightDirection() * -EYE_EAR_GAP) + (getFrontDirection() * -EYE_EAR_GAP); } + glm::vec3 getRightEarPosition() const { return _rightEyePosition + (getRightDirection() * EYE_EAR_GAP) + (getForwardDirection() * -EYE_EAR_GAP); } + glm::vec3 getLeftEarPosition() const { return _leftEyePosition + (getRightDirection() * -EYE_EAR_GAP) + (getForwardDirection() * -EYE_EAR_GAP); } glm::vec3 getMouthPosition() const { return _eyePosition - getUpDirection() * glm::length(_rightEyePosition - _leftEyePosition); } bool getReturnToCenter() const { return _returnHeadToCenter; } // Do you want head to try to return to center (depends on interface detected) @@ -138,7 +138,7 @@ private: int _rightEyeLookAtID; // private methods - void calculateMouthShapes(); + void calculateMouthShapes(float timeRatio); void applyEyelidOffset(glm::quat headOrientation); }; diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 969268c549..d1edf9d44e 100644 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -104,6 +104,7 @@ MyAvatar::MyAvatar(RigPointer rig) : _eyeContactTarget(LEFT_EYE), _realWorldFieldOfView("realWorldFieldOfView", DEFAULT_REAL_WORLD_FIELD_OF_VIEW_DEGREES), + _useAdvancedMovementControls("advancedMovementForHandControllersIsChecked", false), _hmdSensorMatrix(), _hmdSensorOrientation(), _hmdSensorPosition(), @@ -119,9 +120,7 @@ MyAvatar::MyAvatar(RigPointer rig) : using namespace recording; _skeletonModel->flagAsCauterized(); - for (int i = 0; i < MAX_DRIVE_KEYS; i++) { - _driveKeys[i] = 0.0f; - } + clearDriveKeys(); // Necessary to select the correct slot using SlotType = void(MyAvatar::*)(const glm::vec3&, bool, const glm::quat&, bool); @@ -154,9 +153,12 @@ MyAvatar::MyAvatar(RigPointer rig) : if (recordingInterface->getPlayFromCurrentLocation()) { setRecordingBasis(); } + _wasCharacterControllerEnabled = _characterController.isEnabled(); + _characterController.setEnabled(false); } else { clearRecordingBasis(); useFullAvatarURL(_fullAvatarURLFromPreferences, _fullAvatarModelName); + _characterController.setEnabled(_wasCharacterControllerEnabled); } auto audioIO = DependencyManager::get(); @@ -227,6 +229,21 @@ MyAvatar::~MyAvatar() { _lookAtTargetAvatar.reset(); } +void MyAvatar::registerMetaTypes(QScriptEngine* engine) { + QScriptValue value = engine->newQObject(this, QScriptEngine::QtOwnership, QScriptEngine::ExcludeDeleteLater | QScriptEngine::ExcludeChildObjects); + engine->globalObject().setProperty("MyAvatar", value); + + QScriptValue driveKeys = engine->newObject(); + auto metaEnum = QMetaEnum::fromType(); + for (int i = 0; i < MAX_DRIVE_KEYS; ++i) { + driveKeys.setProperty(metaEnum.key(i), metaEnum.value(i)); + } + engine->globalObject().setProperty("DriveKeys", driveKeys); + + qScriptRegisterMetaType(engine, audioListenModeToScriptValue, audioListenModeFromScriptValue); + qScriptRegisterMetaType(engine, driveKeysToScriptValue, driveKeysFromScriptValue); +} + void MyAvatar::setOrientationVar(const QVariant& newOrientationVar) { Avatar::setOrientation(quatFromVariant(newOrientationVar)); } @@ -459,7 +476,7 @@ void MyAvatar::simulate(float deltaTime) { // When there are no step values, we zero out the last step pulse. // This allows a user to do faster snapping by tapping a control for (int i = STEP_TRANSLATE_X; !stepAction && i <= STEP_YAW; ++i) { - if (_driveKeys[i] != 0.0f) { + if (getDriveKey((DriveKeys)i) != 0.0f) { stepAction = true; } } @@ -474,6 +491,11 @@ void MyAvatar::simulate(float deltaTime) { { PerformanceTimer perfTimer("skeleton"); + + if (_rig) { + _rig->setEnableDebugDrawIKTargets(_enableDebugDrawIKTargets); + } + _skeletonModel->simulate(deltaTime); } @@ -899,6 +921,10 @@ void MyAvatar::setEnableDebugDrawSensorToWorldMatrix(bool isEnabled) { } } +void MyAvatar::setEnableDebugDrawIKTargets(bool isEnabled) { + _enableDebugDrawIKTargets = isEnabled; +} + void MyAvatar::setEnableMeshVisible(bool isEnabled) { render::ScenePointer scene = qApp->getMain3DScene(); _skeletonModel->setVisibleInScene(isEnabled, scene); @@ -1051,7 +1077,7 @@ void MyAvatar::updateLookAtTargetAvatar() { _lookAtTargetAvatar.reset(); _targetAvatarPosition = glm::vec3(0.0f); - glm::vec3 lookForward = getHead()->getFinalOrientationInWorldFrame() * IDENTITY_FRONT; + glm::vec3 lookForward = getHead()->getFinalOrientationInWorldFrame() * IDENTITY_FORWARD; glm::vec3 cameraPosition = qApp->getCamera()->getPosition(); float smallestAngleTo = glm::radians(DEFAULT_FIELD_OF_VIEW_DEGREES) / 2.0f; @@ -1319,6 +1345,45 @@ controller::Pose MyAvatar::getRightHandControllerPoseInAvatarFrame() const { return getRightHandControllerPoseInWorldFrame().transform(invAvatarMatrix); } +void MyAvatar::setFootControllerPosesInSensorFrame(const controller::Pose& left, const controller::Pose& right) { + if (controller::InputDevice::getLowVelocityFilter()) { + auto oldLeftPose = getLeftFootControllerPoseInSensorFrame(); + auto oldRightPose = getRightFootControllerPoseInSensorFrame(); + _leftFootControllerPoseInSensorFrameCache.set(applyLowVelocityFilter(oldLeftPose, left)); + _rightFootControllerPoseInSensorFrameCache.set(applyLowVelocityFilter(oldRightPose, right)); + } else { + _leftFootControllerPoseInSensorFrameCache.set(left); + _rightFootControllerPoseInSensorFrameCache.set(right); + } +} + +controller::Pose MyAvatar::getLeftFootControllerPoseInSensorFrame() const { + return _leftFootControllerPoseInSensorFrameCache.get(); +} + +controller::Pose MyAvatar::getRightFootControllerPoseInSensorFrame() const { + return _rightFootControllerPoseInSensorFrameCache.get(); +} + +controller::Pose MyAvatar::getLeftFootControllerPoseInWorldFrame() const { + return _leftFootControllerPoseInSensorFrameCache.get().transform(getSensorToWorldMatrix()); +} + +controller::Pose MyAvatar::getRightFootControllerPoseInWorldFrame() const { + return _rightFootControllerPoseInSensorFrameCache.get().transform(getSensorToWorldMatrix()); +} + +controller::Pose MyAvatar::getLeftFootControllerPoseInAvatarFrame() const { + glm::mat4 invAvatarMatrix = glm::inverse(createMatFromQuatAndPos(getOrientation(), getPosition())); + return getLeftFootControllerPoseInWorldFrame().transform(invAvatarMatrix); +} + +controller::Pose MyAvatar::getRightFootControllerPoseInAvatarFrame() const { + glm::mat4 invAvatarMatrix = glm::inverse(createMatFromQuatAndPos(getOrientation(), getPosition())); + return getRightFootControllerPoseInWorldFrame().transform(invAvatarMatrix); +} + + void MyAvatar::updateMotors() { _characterController.clearMotors(); glm::quat motorRotation; @@ -1652,7 +1717,7 @@ bool MyAvatar::shouldRenderHead(const RenderArgs* renderArgs) const { void MyAvatar::updateOrientation(float deltaTime) { // Smoothly rotate body with arrow keys - float targetSpeed = _driveKeys[YAW] * _yawSpeed; + float targetSpeed = getDriveKey(YAW) * _yawSpeed; if (targetSpeed != 0.0f) { const float ROTATION_RAMP_TIMESCALE = 0.1f; float blend = deltaTime / ROTATION_RAMP_TIMESCALE; @@ -1681,8 +1746,8 @@ void MyAvatar::updateOrientation(float deltaTime) { // Comfort Mode: If you press any of the left/right rotation drive keys or input, you'll // get an instantaneous 15 degree turn. If you keep holding the key down you'll get another // snap turn every half second. - if (_driveKeys[STEP_YAW] != 0.0f) { - totalBodyYaw += _driveKeys[STEP_YAW]; + if (getDriveKey(STEP_YAW) != 0.0f) { + totalBodyYaw += getDriveKey(STEP_YAW); } // use head/HMD orientation to turn while flying @@ -1719,7 +1784,7 @@ void MyAvatar::updateOrientation(float deltaTime) { // update body orientation by movement inputs setOrientation(getOrientation() * glm::quat(glm::radians(glm::vec3(0.0f, totalBodyYaw, 0.0f)))); - getHead()->setBasePitch(getHead()->getBasePitch() + _driveKeys[PITCH] * _pitchSpeed * deltaTime); + getHead()->setBasePitch(getHead()->getBasePitch() + getDriveKey(PITCH) * _pitchSpeed * deltaTime); if (qApp->isHMDMode()) { glm::quat orientation = glm::quat_cast(getSensorToWorldMatrix()) * getHMDSensorOrientation(); @@ -1753,14 +1818,14 @@ void MyAvatar::updateActionMotor(float deltaTime) { } // compute action input - glm::vec3 front = (_driveKeys[TRANSLATE_Z]) * IDENTITY_FRONT; - glm::vec3 right = (_driveKeys[TRANSLATE_X]) * IDENTITY_RIGHT; + glm::vec3 forward = (getDriveKey(TRANSLATE_Z)) * IDENTITY_FORWARD; + glm::vec3 right = (getDriveKey(TRANSLATE_X)) * IDENTITY_RIGHT; - glm::vec3 direction = front + right; + glm::vec3 direction = forward + right; CharacterController::State state = _characterController.getState(); if (state == CharacterController::State::Hover) { // we're flying --> support vertical motion - glm::vec3 up = (_driveKeys[TRANSLATE_Y]) * IDENTITY_UP; + glm::vec3 up = (getDriveKey(TRANSLATE_Y)) * IDENTITY_UP; direction += up; } @@ -1799,7 +1864,7 @@ void MyAvatar::updateActionMotor(float deltaTime) { _actionMotorVelocity = MAX_WALKING_SPEED * direction; } - float boomChange = _driveKeys[ZOOM]; + float boomChange = getDriveKey(ZOOM); _boomLength += 2.0f * _boomLength * boomChange + boomChange * boomChange; _boomLength = glm::clamp(_boomLength, ZOOM_MIN, ZOOM_MAX); } @@ -1830,11 +1895,11 @@ void MyAvatar::updatePosition(float deltaTime) { } // capture the head rotation, in sensor space, when the user first indicates they would like to move/fly. - if (!_hoverReferenceCameraFacingIsCaptured && (fabs(_driveKeys[TRANSLATE_Z]) > 0.1f || fabs(_driveKeys[TRANSLATE_X]) > 0.1f)) { + if (!_hoverReferenceCameraFacingIsCaptured && (fabs(getDriveKey(TRANSLATE_Z)) > 0.1f || fabs(getDriveKey(TRANSLATE_X)) > 0.1f)) { _hoverReferenceCameraFacingIsCaptured = true; // transform the camera facing vector into sensor space. _hoverReferenceCameraFacing = transformVectorFast(glm::inverse(_sensorToWorldMatrix), getHead()->getCameraOrientation() * Vectors::UNIT_Z); - } else if (_hoverReferenceCameraFacingIsCaptured && (fabs(_driveKeys[TRANSLATE_Z]) <= 0.1f && fabs(_driveKeys[TRANSLATE_X]) <= 0.1f)) { + } else if (_hoverReferenceCameraFacingIsCaptured && (fabs(getDriveKey(TRANSLATE_Z)) <= 0.1f && fabs(getDriveKey(TRANSLATE_X)) <= 0.1f)) { _hoverReferenceCameraFacingIsCaptured = false; } } @@ -2036,7 +2101,7 @@ void MyAvatar::goToLocation(const glm::vec3& newPosition, // move the user a couple units away const float DISTANCE_TO_USER = 2.0f; - _goToPosition = newPosition - quatOrientation * IDENTITY_FRONT * DISTANCE_TO_USER; + _goToPosition = newPosition - quatOrientation * IDENTITY_FORWARD * DISTANCE_TO_USER; } _goToOrientation = quatOrientation; @@ -2090,17 +2155,61 @@ bool MyAvatar::getCharacterControllerEnabled() { } void MyAvatar::clearDriveKeys() { - for (int i = 0; i < MAX_DRIVE_KEYS; ++i) { - _driveKeys[i] = 0.0f; + _driveKeys.fill(0.0f); +} + +void MyAvatar::setDriveKey(DriveKeys key, float val) { + try { + _driveKeys.at(key) = val; + } catch (const std::exception&) { + qCCritical(interfaceapp) << Q_FUNC_INFO << ": Index out of bounds"; + } +} + +float MyAvatar::getDriveKey(DriveKeys key) const { + return isDriveKeyDisabled(key) ? 0.0f : getRawDriveKey(key); +} + +float MyAvatar::getRawDriveKey(DriveKeys key) const { + try { + return _driveKeys.at(key); + } catch (const std::exception&) { + qCCritical(interfaceapp) << Q_FUNC_INFO << ": Index out of bounds"; + return 0.0f; } } void MyAvatar::relayDriveKeysToCharacterController() { - if (_driveKeys[TRANSLATE_Y] > 0.0f) { + if (getDriveKey(TRANSLATE_Y) > 0.0f) { _characterController.jump(); } } +void MyAvatar::disableDriveKey(DriveKeys key) { + try { + _disabledDriveKeys.set(key); + } catch (const std::exception&) { + qCCritical(interfaceapp) << Q_FUNC_INFO << ": Index out of bounds"; + } +} + +void MyAvatar::enableDriveKey(DriveKeys key) { + try { + _disabledDriveKeys.reset(key); + } catch (const std::exception&) { + qCCritical(interfaceapp) << Q_FUNC_INFO << ": Index out of bounds"; + } +} + +bool MyAvatar::isDriveKeyDisabled(DriveKeys key) const { + try { + return _disabledDriveKeys.test(key); + } catch (const std::exception&) { + qCCritical(interfaceapp) << Q_FUNC_INFO << ": Index out of bounds"; + return true; + } +} + glm::vec3 MyAvatar::getWorldBodyPosition() const { return transformPoint(_sensorToWorldMatrix, extractTranslation(_bodySensorMatrix)); } @@ -2186,7 +2295,15 @@ QScriptValue audioListenModeToScriptValue(QScriptEngine* engine, const AudioList } void audioListenModeFromScriptValue(const QScriptValue& object, AudioListenerMode& audioListenerMode) { - audioListenerMode = (AudioListenerMode)object.toUInt16(); + audioListenerMode = static_cast(object.toUInt16()); +} + +QScriptValue driveKeysToScriptValue(QScriptEngine* engine, const MyAvatar::DriveKeys& driveKeys) { + return driveKeys; +} + +void driveKeysFromScriptValue(const QScriptValue& object, MyAvatar::DriveKeys& driveKeys) { + driveKeys = static_cast(object.toUInt16()); } @@ -2379,7 +2496,7 @@ bool MyAvatar::didTeleport() { } bool MyAvatar::hasDriveInput() const { - return fabsf(_driveKeys[TRANSLATE_X]) > 0.0f || fabsf(_driveKeys[TRANSLATE_Y]) > 0.0f || fabsf(_driveKeys[TRANSLATE_Z]) > 0.0f; + return fabsf(getDriveKey(TRANSLATE_X)) > 0.0f || fabsf(getDriveKey(TRANSLATE_Y)) > 0.0f || fabsf(getDriveKey(TRANSLATE_Z)) > 0.0f; } void MyAvatar::setAway(bool value) { @@ -2495,7 +2612,7 @@ bool MyAvatar::pinJoint(int index, const glm::vec3& position, const glm::quat& o return false; } - setPosition(position); + slamPosition(position); setOrientation(orientation); _rig->setMaxHipsOffsetLength(0.05f); diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index 3cc665b533..097d3a1059 100644 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -12,6 +12,8 @@ #ifndef hifi_MyAvatar_h #define hifi_MyAvatar_h +#include + #include #include @@ -29,20 +31,6 @@ class AvatarActionHold; class ModelItemID; -enum DriveKeys { - TRANSLATE_X = 0, - TRANSLATE_Y, - TRANSLATE_Z, - YAW, - STEP_TRANSLATE_X, - STEP_TRANSLATE_Y, - STEP_TRANSLATE_Z, - STEP_YAW, - PITCH, - ZOOM, - MAX_DRIVE_KEYS -}; - enum eyeContactTarget { LEFT_EYE, RIGHT_EYE, @@ -58,6 +46,60 @@ Q_DECLARE_METATYPE(AudioListenerMode); class MyAvatar : public Avatar { Q_OBJECT + + /**jsdoc + * Your avatar is your in-world representation of you. The MyAvatar API is used to manipulate the avatar. + * For example, using the MyAvatar API you can customize the avatar's appearance, run custom avatar animations, + * change the avatar's position within the domain, or manage the avatar's collisions with other objects. + * NOTE: MyAvatar extends Avatar and AvatarData, see those namespace for more properties/methods. + * + * @namespace MyAvatar + * @augments Avatar + * @property shouldRenderLocally {bool} Set it to true if you would like to see MyAvatar in your local interface, + * and false if you would not like to see MyAvatar in your local interface. + * @property motorVelocity {Vec3} Can be used to move the avatar with this velocity. + * @property motorTimescale {float} Specifies how quickly the avatar should accelerate to meet the motorVelocity, + * smaller values will result in higher acceleration. + * @property motorReferenceFrame {string} Reference frame of the motorVelocity, must be one of the following: "avatar", "camera", "world" + * @property collisionSoundURL {string} Specifies the sound to play when the avatar experiences a collision. + * You can provide a mono or stereo 16-bit WAV file running at either 24 Khz or 48 Khz. + * The latter is downsampled by the audio mixer, so all audio effectively plays back at a 24 Khz sample rate. + * 48 Khz RAW files are also supported. + * @property audioListenerMode {number} When hearing spatialized audio this determines where the listener placed. + * Should be one of the following values: + * MyAvatar.audioListenerModeHead - the listener located at the avatar's head. + * MyAvatar.audioListenerModeCamera - the listener is relative to the camera. + * MyAvatar.audioListenerModeCustom - the listener is at a custom location specified by the MyAvatar.customListenPosition + * and MyAvatar.customListenOrientation properties. + * @property customListenPosition {Vec3} If MyAvatar.audioListenerMode == MyAvatar.audioListenerModeHead, then this determines the position + * of audio spatialization listener. + * @property customListenOreintation {Quat} If MyAvatar.audioListenerMode == MyAvatar.audioListenerModeHead, then this determines the orientation + * of the audio spatialization listener. + * @property audioListenerModeHead {number} READ-ONLY. When passed to MyAvatar.audioListenerMode, it will set the audio listener + * around the avatar's head. + * @property audioListenerModeCamera {number} READ-ONLY. When passed to MyAvatar.audioListenerMode, it will set the audio listener + * around the camera. + * @property audioListenerModeCustom {number} READ-ONLY. When passed to MyAvatar.audioListenerMode, it will set the audio listener + * around the value specified by MyAvatar.customListenPosition and MyAvatar.customListenOrientation. + * @property leftHandPosition {Vec3} READ-ONLY. The desired position of the left wrist in avatar space, determined by the hand controllers. + * Note: only valid if hand controllers are in use. + * @property rightHandPosition {Vec3} READ-ONLY. The desired position of the right wrist in avatar space, determined by the hand controllers. + * Note: only valid if hand controllers are in use. + * @property leftHandTipPosition {Vec3} READ-ONLY. A position 30 cm offset from MyAvatar.leftHandPosition + * @property rightHandTipPosition {Vec3} READ-ONLY. A position 30 cm offset from MyAvatar.rightHandPosition + * @property leftHandPose {Pose} READ-ONLY. Returns full pose (translation, orientation, velocity & angularVelocity) of the desired + * wrist position, determined by the hand controllers. + * @property rightHandPose {Pose} READ-ONLY. Returns full pose (translation, orientation, velocity & angularVelocity) of the desired + * wrist position, determined by the hand controllers. + * @property leftHandTipPose {Pose} READ-ONLY. Returns a pose offset 30 cm from MyAvatar.leftHandPose + * @property rightHandTipPose {Pose} READ-ONLY. Returns a pose offset 30 cm from MyAvatar.rightHandPose + * @property hmdLeanRecenterEnabled {bool} This can be used disable the hmd lean recenter behavior. This behavior is what causes your avatar + * to follow your HMD as you walk around the room, in room scale VR. Disabling this is useful if you desire to pin the avatar to a fixed location. + * @property characterControllerEnabled {bool} This can be used to disable collisions between the avatar and the world. + * @property useAdvancedMovementControls {bool} Stores the user preference only, does not change user mappings, this is done in the defaultScript + * "scripts/system/controllers/toggleAdvancedMovementForHandControllers.js". + */ + Q_PROPERTY(bool shouldRenderLocally READ getShouldRenderLocally WRITE setShouldRenderLocally) Q_PROPERTY(glm::vec3 motorVelocity READ getScriptedMotorVelocity WRITE setScriptedMotorVelocity) Q_PROPERTY(float motorTimescale READ getScriptedMotorTimescale WRITE setScriptedMotorTimescale) @@ -86,11 +128,29 @@ class MyAvatar : public Avatar { Q_PROPERTY(bool hmdLeanRecenterEnabled READ getHMDLeanRecenterEnabled WRITE setHMDLeanRecenterEnabled) Q_PROPERTY(bool characterControllerEnabled READ getCharacterControllerEnabled WRITE setCharacterControllerEnabled) + Q_PROPERTY(bool useAdvancedMovementControls READ useAdvancedMovementControls WRITE setUseAdvancedMovementControls) public: + enum DriveKeys { + TRANSLATE_X = 0, + TRANSLATE_Y, + TRANSLATE_Z, + YAW, + STEP_TRANSLATE_X, + STEP_TRANSLATE_Y, + STEP_TRANSLATE_Z, + STEP_YAW, + PITCH, + ZOOM, + MAX_DRIVE_KEYS + }; + Q_ENUM(DriveKeys) + explicit MyAvatar(RigPointer rig); ~MyAvatar(); + void registerMetaTypes(QScriptEngine* engine); + virtual void simulateAttachments(float deltaTime) override; AudioListenerMode getAudioListenerModeHead() const { return FROM_HEAD; } @@ -100,7 +160,19 @@ public: void reset(bool andRecenter = false, bool andReload = true, bool andHead = true); Q_INVOKABLE void resetSensorsAndBody(); + + /**jsdoc + * Moves and orients the avatar, such that it is directly underneath the HMD, with toes pointed forward. + * @function MyAvatar.centerBody + */ Q_INVOKABLE void centerBody(); // thread-safe + + + /**jsdoc + * The internal inverse-kinematics system maintains a record of which joints are "locked". Sometimes it is useful to forget this history, to prevent + * contorted joints. + * @function MyAvatar.clearIKJointLimitHistory + */ Q_INVOKABLE void clearIKJointLimitHistory(); // thread-safe void update(float deltaTime); @@ -131,23 +203,109 @@ public: void setRealWorldFieldOfView(float realWorldFov) { _realWorldFieldOfView.set(realWorldFov); } + /**jsdoc + * The default position in world coordinates of the point directly between the avatar's eyes + * @function MyAvatar.getDefaultEyePosition + * @example This example gets the default eye position and prints it to the debug log. + * var defaultEyePosition = MyAvatar.getDefaultEyePosition(); + * print (JSON.stringify(defaultEyePosition)); + * @returns {Vec3} Position between the avatar's eyes. + */ Q_INVOKABLE glm::vec3 getDefaultEyePosition() const; float getRealWorldFieldOfView() { return _realWorldFieldOfView.get(); } - // Interrupt the current animation with a custom animation. + /**jsdoc + * The avatar animation system includes a set of default animations along with rules for how those animations are blended + * together with procedural data (such as look at vectors, hand sensors etc.). overrideAnimation() is used to completely + * override all motion from the default animation system (including inverse kinematics for hand and head controllers) and + * play a specified animation. To end this animation and restore the default animations, use MyAvatar.restoreAnimation. + * @function MyAvatar.overrideAnimation + * @example Play a clapping animation on your avatar for three seconds. + * // Clap your hands for 3 seconds then restore animation back to the avatar. + * var ANIM_URL = "https://s3.amazonaws.com/hifi-public/animations/ClapAnimations/ClapHands_Standing.fbx"; + * MyAvatar.overrideAnimation(ANIM_URL, 30, true, 0, 53); + * Script.setTimeout(function () { + * MyAvatar.restoreAnimation(); + * }, 3000); + * @param url {string} The URL to the animation file. Animation files need to be .FBX format, but only need to contain the avatar skeleton and animation data. + * @param fps {number} The frames per second (FPS) rate for the animation playback. 30 FPS is normal speed. + * @param loop {bool} Set to true if the animation should loop. + * @param firstFrame {number} The frame the animation should start at. + * @param lastFrame {number} The frame the animation should end at. + */ Q_INVOKABLE void overrideAnimation(const QString& url, float fps, bool loop, float firstFrame, float lastFrame); - // Stop the animation that was started with overrideAnimation and go back to the standard animation. + /**jsdoc + * The avatar animation system includes a set of default animations along with rules for how those animations are blended together with + * procedural data (such as look at vectors, hand sensors etc.). Playing your own custom animations will override the default animations. + * restoreAnimation() is used to restore all motion from the default animation system including inverse kinematics for hand and head + * controllers. If you aren't currently playing an override animation, this function will have no effect. + * @function MyAvatar.restoreAnimation + * @example Play a clapping animation on your avatar for three seconds. + * // Clap your hands for 3 seconds then restore animation back to the avatar. + * var ANIM_URL = "https://s3.amazonaws.com/hifi-public/animations/ClapAnimations/ClapHands_Standing.fbx"; + * MyAvatar.overrideAnimation(ANIM_URL, 30, true, 0, 53); + * Script.setTimeout(function () { + * MyAvatar.restoreAnimation(); + * }, 3000); + */ Q_INVOKABLE void restoreAnimation(); - // Returns a list of all clips that are available + /**jsdoc + * Each avatar has an avatar-animation.json file that defines which animations are used and how they are blended together with procedural data + * (such as look at vectors, hand sensors etc.). Each animation specified in the avatar-animation.json file is known as an animation role. + * Animation roles map to easily understandable actions that the avatar can perform, such as "idleStand", "idleTalk", or "walkFwd." + * getAnimationRoles() is used get the list of animation roles defined in the avatar-animation.json. + * @function MyAvatar.getAnimatationRoles + * @example This example prints the list of animation roles defined in the avatar's avatar-animation.json file to the debug log. + * var roles = MyAvatar.getAnimationRoles(); + * print("Animation Roles:"); + * for (var i = 0; i < roles.length; i++) { + * print(roles[i]); + * } + * @returns {string[]} Array of role strings + */ Q_INVOKABLE QStringList getAnimationRoles(); - // Replace an existing standard role animation with a custom one. + /**jsdoc + * Each avatar has an avatar-animation.json file that defines a set of animation roles. Animation roles map to easily understandable actions + * that the avatar can perform, such as "idleStand", "idleTalk", or "walkFwd". To get the full list of roles, use getAnimationRoles(). + * For each role, the avatar-animation.json defines when the animation is used, the animation clip (.FBX) used, and how animations are blended + * together with procedural data (such as look at vectors, hand sensors etc.). + * overrideRoleAnimation() is used to change the animation clip (.FBX) associated with a specified animation role. + * Note: Hand roles only affect the hand. Other 'main' roles, like 'idleStand', 'idleTalk', 'takeoffStand' are full body. + * @function MyAvatar.overrideRoleAnimation + * @example The default avatar-animation.json defines an "idleStand" animation role. This role specifies that when the avatar is not moving, + * an animation clip of the avatar idling with hands hanging at its side will be used. It also specifies that when the avatar moves, the animation + * will smoothly blend to the walking animation used by the "walkFwd" animation role. + * In this example, the "idleStand" role animation clip has been replaced with a clapping animation clip. Now instead of standing with its arms + * hanging at its sides when it is not moving, the avatar will stand and clap its hands. Note that just as it did before, as soon as the avatar + * starts to move, the animation will smoothly blend into the walk animation used by the "walkFwd" animation role. + * // An animation of the avatar clapping its hands while standing + * var ANIM_URL = "https://s3.amazonaws.com/hifi-public/animations/ClapAnimations/ClapHands_Standing.fbx"; + * MyAvatar.overrideRoleAnimation("idleStand", ANIM_URL, 30, true, 0, 53); + * // To restore the default animation, use MyAvatar.restoreRoleAnimation(). + * @param role {string} The animation role to override + * @param url {string} The URL to the animation file. Animation files need to be .FBX format, but only need to contain the avatar skeleton and animation data. + * @param fps {number} The frames per second (FPS) rate for the animation playback. 30 FPS is normal speed. + * @param loop {bool} Set to true if the animation should loop + * @param firstFrame {number} The frame the animation should start at + * @param lastFrame {number} The frame the animation should end at + */ Q_INVOKABLE void overrideRoleAnimation(const QString& role, const QString& url, float fps, bool loop, float firstFrame, float lastFrame); - // remove an animation role override and return to the standard animation. + /**jsdoc + * Each avatar has an avatar-animation.json file that defines a set of animation roles. Animation roles map to easily understandable actions that + * the avatar can perform, such as "idleStand", "idleTalk", or "walkFwd". To get the full list of roles, use getAnimationRoles(). For each role, + * the avatar-animation.json defines when the animation is used, the animation clip (.FBX) used, and how animations are blended together with + * procedural data (such as look at vectors, hand sensors etc.). You can change the animation clip (.FBX) associated with a specified animation + * role using overrideRoleAnimation(). + * restoreRoleAnimation() is used to restore a specified animation role's default animation clip. If you have not specified an override animation + * for the specified role, this function will have no effect. + * @function MyAvatar.restoreRoleAnimation + * @param rule {string} The animation role clip to restore + */ Q_INVOKABLE void restoreRoleAnimation(const QString& role); // Adds handler(animStateDictionaryIn) => animStateDictionaryOut, which will be invoked just before each animGraph state update. @@ -171,6 +329,10 @@ public: Q_INVOKABLE void setHMDLeanRecenterEnabled(bool value) { _hmdLeanRecenterEnabled = value; } Q_INVOKABLE bool getHMDLeanRecenterEnabled() const { return _hmdLeanRecenterEnabled; } + bool useAdvancedMovementControls() const { return _useAdvancedMovementControls.get(); } + void setUseAdvancedMovementControls(bool useAdvancedMovementControls) + { _useAdvancedMovementControls.set(useAdvancedMovementControls); } + // get/set avatar data void saveData(); void loadData(); @@ -180,9 +342,15 @@ public: // Set what driving keys are being pressed to control thrust levels void clearDriveKeys(); - void setDriveKeys(int key, float val) { _driveKeys[key] = val; }; + void setDriveKey(DriveKeys key, float val); + float getDriveKey(DriveKeys key) const; + Q_INVOKABLE float getRawDriveKey(DriveKeys key) const; void relayDriveKeysToCharacterController(); + Q_INVOKABLE void disableDriveKey(DriveKeys key); + Q_INVOKABLE void enableDriveKey(DriveKeys key); + Q_INVOKABLE bool isDriveKeyDisabled(DriveKeys key) const; + eyeContactTarget getEyeContactTarget(); Q_INVOKABLE glm::vec3 getTrackedHeadPosition() const { return _trackedHeadPosition; } @@ -277,6 +445,14 @@ public: controller::Pose getLeftHandControllerPoseInAvatarFrame() const; controller::Pose getRightHandControllerPoseInAvatarFrame() const; + void setFootControllerPosesInSensorFrame(const controller::Pose& left, const controller::Pose& right); + controller::Pose getLeftFootControllerPoseInSensorFrame() const; + controller::Pose getRightFootControllerPoseInSensorFrame() const; + controller::Pose getLeftFootControllerPoseInWorldFrame() const; + controller::Pose getRightFootControllerPoseInWorldFrame() const; + controller::Pose getLeftFootControllerPoseInAvatarFrame() const; + controller::Pose getRightFootControllerPoseInAvatarFrame() const; + bool hasDriveInput() const; Q_INVOKABLE void setCharacterControllerEnabled(bool enabled); @@ -314,6 +490,7 @@ public slots: void setEnableDebugDrawPosition(bool isEnabled); void setEnableDebugDrawHandControllers(bool isEnabled); void setEnableDebugDrawSensorToWorldMatrix(bool isEnabled); + void setEnableDebugDrawIKTargets(bool isEnabled); bool getEnableMeshVisible() const { return _skeletonModel->isVisible(); } void setEnableMeshVisible(bool isEnabled); void setUseAnimPreAndPostRotations(bool isEnabled); @@ -352,7 +529,6 @@ private: virtual bool shouldRenderHead(const RenderArgs* renderArgs) const override; void setShouldRenderLocally(bool shouldRender) { _shouldRender = shouldRender; setEnableMeshVisible(shouldRender); } bool getShouldRenderLocally() const { return _shouldRender; } - bool getDriveKeys(int key) { return _driveKeys[key] != 0.0f; }; bool isMyAvatar() const override { return true; } virtual int parseDataFromBuffer(const QByteArray& buffer) override; virtual glm::vec3 getSkeletonPosition() const override; @@ -388,7 +564,9 @@ private: void clampScaleChangeToDomainLimits(float desiredScale); glm::mat4 computeCameraRelativeHandControllerMatrix(const glm::mat4& controllerSensorMatrix) const; - float _driveKeys[MAX_DRIVE_KEYS]; + std::array _driveKeys; + std::bitset _disabledDriveKeys; + bool _wasPushing; bool _isPushing; bool _isBeingPushed; @@ -411,6 +589,7 @@ private: SharedSoundPointer _collisionSound; MyCharacterController _characterController; + bool _wasCharacterControllerEnabled { true }; AvatarWeakPointer _lookAtTargetAvatar; glm::vec3 _targetAvatarPosition; @@ -423,6 +602,7 @@ private: glm::vec3 _trackedHeadPosition; Setting::Handle _realWorldFieldOfView; + Setting::Handle _useAdvancedMovementControls; // private methods void updateOrientation(float deltaTime); @@ -497,6 +677,7 @@ private: bool _enableDebugDrawAnimPose { false }; bool _enableDebugDrawHandControllers { false }; bool _enableDebugDrawSensorToWorldMatrix { false }; + bool _enableDebugDrawIKTargets { false }; AudioListenerMode _audioListenerMode; glm::vec3 _customListenPosition; @@ -511,6 +692,9 @@ private: ThreadSafeValueCache _leftHandControllerPoseInSensorFrameCache { controller::Pose() }; ThreadSafeValueCache _rightHandControllerPoseInSensorFrameCache { controller::Pose() }; + ThreadSafeValueCache _leftFootControllerPoseInSensorFrameCache{ controller::Pose() }; + ThreadSafeValueCache _rightFootControllerPoseInSensorFrameCache{ controller::Pose() }; + bool _hmdLeanRecenterEnabled = true; AnimPose _prePhysicsRoomPose; @@ -540,4 +724,7 @@ private: QScriptValue audioListenModeToScriptValue(QScriptEngine* engine, const AudioListenerMode& audioListenerMode); void audioListenModeFromScriptValue(const QScriptValue& object, AudioListenerMode& audioListenerMode); +QScriptValue driveKeysToScriptValue(QScriptEngine* engine, const MyAvatar::DriveKeys& driveKeys); +void driveKeysFromScriptValue(const QScriptValue& object, MyAvatar::DriveKeys& driveKeys); + #endif // hifi_MyAvatar_h diff --git a/interface/src/avatar/SkeletonModel.cpp b/interface/src/avatar/SkeletonModel.cpp index 88590a6f69..d7dd93cedf 100644 --- a/interface/src/avatar/SkeletonModel.cpp +++ b/interface/src/avatar/SkeletonModel.cpp @@ -132,31 +132,49 @@ void SkeletonModel::updateRig(float deltaTime, glm::mat4 parentTransform) { _rig->updateFromHeadParameters(headParams, deltaTime); - Rig::HandParameters handParams; + Rig::HandAndFeetParameters handAndFeetParams; auto leftPose = myAvatar->getLeftHandControllerPoseInAvatarFrame(); if (leftPose.isValid()) { - handParams.isLeftEnabled = true; - handParams.leftPosition = Quaternions::Y_180 * leftPose.getTranslation(); - handParams.leftOrientation = Quaternions::Y_180 * leftPose.getRotation(); + handAndFeetParams.isLeftEnabled = true; + handAndFeetParams.leftPosition = Quaternions::Y_180 * leftPose.getTranslation(); + handAndFeetParams.leftOrientation = Quaternions::Y_180 * leftPose.getRotation(); } else { - handParams.isLeftEnabled = false; + handAndFeetParams.isLeftEnabled = false; } auto rightPose = myAvatar->getRightHandControllerPoseInAvatarFrame(); if (rightPose.isValid()) { - handParams.isRightEnabled = true; - handParams.rightPosition = Quaternions::Y_180 * rightPose.getTranslation(); - handParams.rightOrientation = Quaternions::Y_180 * rightPose.getRotation(); + handAndFeetParams.isRightEnabled = true; + handAndFeetParams.rightPosition = Quaternions::Y_180 * rightPose.getTranslation(); + handAndFeetParams.rightOrientation = Quaternions::Y_180 * rightPose.getRotation(); } else { - handParams.isRightEnabled = false; + handAndFeetParams.isRightEnabled = false; } - handParams.bodyCapsuleRadius = myAvatar->getCharacterController()->getCapsuleRadius(); - handParams.bodyCapsuleHalfHeight = myAvatar->getCharacterController()->getCapsuleHalfHeight(); - handParams.bodyCapsuleLocalOffset = myAvatar->getCharacterController()->getCapsuleLocalOffset(); + auto leftFootPose = myAvatar->getLeftFootControllerPoseInAvatarFrame(); + if (leftFootPose.isValid()) { + handAndFeetParams.isLeftFootEnabled = true; + handAndFeetParams.leftFootPosition = Quaternions::Y_180 * leftFootPose.getTranslation(); + handAndFeetParams.leftFootOrientation = Quaternions::Y_180 * leftFootPose.getRotation(); + } else { + handAndFeetParams.isLeftFootEnabled = false; + } - _rig->updateFromHandParameters(handParams, deltaTime); + auto rightFootPose = myAvatar->getRightFootControllerPoseInAvatarFrame(); + if (rightFootPose.isValid()) { + handAndFeetParams.isRightFootEnabled = true; + handAndFeetParams.rightFootPosition = Quaternions::Y_180 * rightFootPose.getTranslation(); + handAndFeetParams.rightFootOrientation = Quaternions::Y_180 * rightFootPose.getRotation(); + } else { + handAndFeetParams.isRightFootEnabled = false; + } + + handAndFeetParams.bodyCapsuleRadius = myAvatar->getCharacterController()->getCapsuleRadius(); + handAndFeetParams.bodyCapsuleHalfHeight = myAvatar->getCharacterController()->getCapsuleHalfHeight(); + handAndFeetParams.bodyCapsuleLocalOffset = myAvatar->getCharacterController()->getCapsuleLocalOffset(); + + _rig->updateFromHandAndFeetParameters(handAndFeetParams, deltaTime); Rig::CharacterControllerState ccState = convertCharacterControllerState(myAvatar->getCharacterController()->getState()); @@ -179,7 +197,9 @@ void SkeletonModel::updateRig(float deltaTime, glm::mat4 parentTransform) { _rig->updateFromEyeParameters(eyeParams); } else { - Model::updateRig(deltaTime, parentTransform); + // no need to call Model::updateRig() because otherAvatars get their joint state + // copied directly from AvtarData::_jointData (there are no Rig animations to blend) + _needsUpdateClusterMatrices = true; // This is a little more work than we really want. // diff --git a/interface/src/avatar/SoftAttachmentModel.cpp b/interface/src/avatar/SoftAttachmentModel.cpp index 6ed54afb27..0521f7a893 100644 --- a/interface/src/avatar/SoftAttachmentModel.cpp +++ b/interface/src/avatar/SoftAttachmentModel.cpp @@ -60,13 +60,7 @@ void SoftAttachmentModel::updateClusterMatrices() { } else { jointMatrix = _rig->getJointTransform(cluster.jointIndex); } -#if (GLM_ARCH & GLM_ARCH_SSE2) && !(defined Q_OS_MAC) - glm::mat4 out, inverseBindMatrix = cluster.inverseBindMatrix; - glm_mat4_mul((glm_vec4*)&jointMatrix, (glm_vec4*)&inverseBindMatrix, (glm_vec4*)&out); - state.clusterMatrices[j] = out; -#else - state.clusterMatrices[j] = jointMatrix * cluster.inverseBindMatrix; -#endif + glm_mat4u_mul(jointMatrix, cluster.inverseBindMatrix, state.clusterMatrices[j]); } // Once computed the cluster matrices, update the buffer(s) diff --git a/interface/src/networking/FileTypeProfile.cpp b/interface/src/networking/FileTypeProfile.cpp new file mode 100644 index 0000000000..6fcd8df669 --- /dev/null +++ b/interface/src/networking/FileTypeProfile.cpp @@ -0,0 +1,26 @@ +// +// FileTypeProfile.cpp +// interface/src/networking +// +// Created by Kunal Gosar on 2017-03-10. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include "FileTypeProfile.h" + +#include "FileTypeRequestInterceptor.h" + +static const QString QML_WEB_ENGINE_STORAGE_NAME = "qmlWebEngine"; + +FileTypeProfile::FileTypeProfile(QObject* parent) : + QQuickWebEngineProfile(parent) +{ + static const QString WEB_ENGINE_USER_AGENT = "Chrome/48.0 (HighFidelityInterface)"; + setHttpUserAgent(WEB_ENGINE_USER_AGENT); + + auto requestInterceptor = new FileTypeRequestInterceptor(this); + setRequestInterceptor(requestInterceptor); +} diff --git a/interface/src/networking/FileTypeProfile.h b/interface/src/networking/FileTypeProfile.h new file mode 100644 index 0000000000..f922fd66de --- /dev/null +++ b/interface/src/networking/FileTypeProfile.h @@ -0,0 +1,25 @@ +// +// FileTypeProfile.h +// interface/src/networking +// +// Created by Kunal Gosar on 2017-03-10. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#pragma once + +#ifndef hifi_FileTypeProfile_h +#define hifi_FileTypeProfile_h + +#include + +class FileTypeProfile : public QQuickWebEngineProfile { +public: + FileTypeProfile(QObject* parent = Q_NULLPTR); +}; + + +#endif // hifi_FileTypeProfile_h diff --git a/interface/src/networking/FileTypeRequestInterceptor.cpp b/interface/src/networking/FileTypeRequestInterceptor.cpp new file mode 100644 index 0000000000..91bacd46a6 --- /dev/null +++ b/interface/src/networking/FileTypeRequestInterceptor.cpp @@ -0,0 +1,21 @@ +// +// FileTypeRequestInterceptor.cpp +// interface/src/networking +// +// Created by Kunal Gosar on 2017-03-10. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include "FileTypeRequestInterceptor.h" + +#include + +#include "RequestFilters.h" + +void FileTypeRequestInterceptor::interceptRequest(QWebEngineUrlRequestInfo& info) { + RequestFilters::interceptHFWebEngineRequest(info); + RequestFilters::interceptFileType(info); +} diff --git a/interface/src/networking/FileTypeRequestInterceptor.h b/interface/src/networking/FileTypeRequestInterceptor.h new file mode 100644 index 0000000000..be971daf7a --- /dev/null +++ b/interface/src/networking/FileTypeRequestInterceptor.h @@ -0,0 +1,26 @@ +// +// FileTypeRequestInterceptor.h +// interface/src/networking +// +// Created by Kunal Gosar on 2017-03-10. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#pragma once + +#ifndef hifi_FileTypeRequestInterceptor_h +#define hifi_FileTypeRequestInterceptor_h + +#include + +class FileTypeRequestInterceptor : public QWebEngineUrlRequestInterceptor { +public: + FileTypeRequestInterceptor(QObject* parent) : QWebEngineUrlRequestInterceptor(parent) {}; + + virtual void interceptRequest(QWebEngineUrlRequestInfo& info) override; +}; + +#endif // hifi_FileTypeRequestInterceptor_h diff --git a/interface/src/networking/HFTabletWebEngineProfile.cpp b/interface/src/networking/HFTabletWebEngineProfile.cpp new file mode 100644 index 0000000000..46634299bb --- /dev/null +++ b/interface/src/networking/HFTabletWebEngineProfile.cpp @@ -0,0 +1,26 @@ +// +// HFTabletWebEngineProfile.h +// interface/src/networking +// +// Created by Dante Ruiz on 2017-03-31. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include "HFTabletWebEngineProfile.h" +#include "HFTabletWebEngineRequestInterceptor.h" + +static const QString QML_WEB_ENGINE_NAME = "qmlTabletWebEngine"; + +HFTabletWebEngineProfile::HFTabletWebEngineProfile(QObject* parent) : QQuickWebEngineProfile(parent) { + + static const QString WEB_ENGINE_USER_AGENT = "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Mobile Safari/537.36"; + + setHttpUserAgent(WEB_ENGINE_USER_AGENT); + + auto requestInterceptor = new HFTabletWebEngineRequestInterceptor(this); + setRequestInterceptor(requestInterceptor); +} + diff --git a/interface/src/networking/HFTabletWebEngineProfile.h b/interface/src/networking/HFTabletWebEngineProfile.h new file mode 100644 index 0000000000..406cb1a19a --- /dev/null +++ b/interface/src/networking/HFTabletWebEngineProfile.h @@ -0,0 +1,23 @@ +// +// HFTabletWebEngineProfile.h +// interface/src/networking +// +// Created by Dante Ruiz on 2017-03-31. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + + +#ifndef hifi_HFTabletWebEngineProfile_h +#define hifi_HFTabletWebEngineProfile_h + +#include + +class HFTabletWebEngineProfile : public QQuickWebEngineProfile { +public: + HFTabletWebEngineProfile(QObject* parent = Q_NULLPTR); +}; + +#endif // hifi_HFTabletWebEngineProfile_h diff --git a/interface/src/networking/HFTabletWebEngineRequestInterceptor.cpp b/interface/src/networking/HFTabletWebEngineRequestInterceptor.cpp new file mode 100644 index 0000000000..7282fb5e3d --- /dev/null +++ b/interface/src/networking/HFTabletWebEngineRequestInterceptor.cpp @@ -0,0 +1,42 @@ +// +// HFTabletWebEngineRequestInterceptor.cpp +// interface/src/networking +// +// Created by Dante Ruiz on 2017-3-31. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include "HFTabletWebEngineRequestInterceptor.h" +#include +#include + +bool isTabletAuthableHighFidelityURL(const QUrl& url) { + static const QStringList HF_HOSTS = { + "highfidelity.com", "highfidelity.io", + "metaverse.highfidelity.com", "metaverse.highfidelity.io" + }; + + return url.scheme() == "https" && HF_HOSTS.contains(url.host()); +} + +void HFTabletWebEngineRequestInterceptor::interceptRequest(QWebEngineUrlRequestInfo& info) { + // check if this is a request to a highfidelity URL + if (isTabletAuthableHighFidelityURL(info.requestUrl())) { + // if we have an access token, add it to the right HTTP header for authorization + auto accountManager = DependencyManager::get(); + + if (accountManager->hasValidAccessToken()) { + static const QString OAUTH_AUTHORIZATION_HEADER = "Authorization"; + + QString bearerTokenString = "Bearer " + accountManager->getAccountInfo().getAccessToken().token; + info.setHttpHeader(OAUTH_AUTHORIZATION_HEADER.toLocal8Bit(), bearerTokenString.toLocal8Bit()); + } + } + + static const QString USER_AGENT = "User-Agent"; + QString tokenString = "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Mobile Safari/537.36"; + info.setHttpHeader(USER_AGENT.toLocal8Bit(), tokenString.toLocal8Bit()); +} diff --git a/interface/src/networking/HFTabletWebEngineRequestInterceptor.h b/interface/src/networking/HFTabletWebEngineRequestInterceptor.h new file mode 100644 index 0000000000..e38549937e --- /dev/null +++ b/interface/src/networking/HFTabletWebEngineRequestInterceptor.h @@ -0,0 +1,24 @@ +// +// HFTabletWebEngineRequestInterceptor.h +// interface/src/networking +// +// Created by Dante Ruiz on 2017-3-31. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_HFTabletWebEngineRequestInterceptor_h +#define hifi_HFTabletWebEngineRequestInterceptor_h + +#include + +class HFTabletWebEngineRequestInterceptor : public QWebEngineUrlRequestInterceptor { +public: + HFTabletWebEngineRequestInterceptor(QObject* parent) : QWebEngineUrlRequestInterceptor(parent) {}; + + virtual void interceptRequest(QWebEngineUrlRequestInfo& info) override; +}; + +#endif // hifi_HFWebEngineRequestInterceptor_h diff --git a/interface/src/networking/HFWebEngineRequestInterceptor.cpp b/interface/src/networking/HFWebEngineRequestInterceptor.cpp index 9c3f0b232e..59897d427f 100644 --- a/interface/src/networking/HFWebEngineRequestInterceptor.cpp +++ b/interface/src/networking/HFWebEngineRequestInterceptor.cpp @@ -14,27 +14,8 @@ #include #include - -bool isAuthableHighFidelityURL(const QUrl& url) { - static const QStringList HF_HOSTS = { - "highfidelity.com", "highfidelity.io", - "metaverse.highfidelity.com", "metaverse.highfidelity.io" - }; - - return url.scheme() == "https" && HF_HOSTS.contains(url.host()); -} +#include "RequestFilters.h" void HFWebEngineRequestInterceptor::interceptRequest(QWebEngineUrlRequestInfo& info) { - // check if this is a request to a highfidelity URL - if (isAuthableHighFidelityURL(info.requestUrl())) { - // if we have an access token, add it to the right HTTP header for authorization - auto accountManager = DependencyManager::get(); - - if (accountManager->hasValidAccessToken()) { - static const QString OAUTH_AUTHORIZATION_HEADER = "Authorization"; - - QString bearerTokenString = "Bearer " + accountManager->getAccountInfo().getAccessToken().token; - info.setHttpHeader(OAUTH_AUTHORIZATION_HEADER.toLocal8Bit(), bearerTokenString.toLocal8Bit()); - } - } + RequestFilters::interceptHFWebEngineRequest(info); } diff --git a/interface/src/networking/RequestFilters.cpp b/interface/src/networking/RequestFilters.cpp new file mode 100644 index 0000000000..fedde94f15 --- /dev/null +++ b/interface/src/networking/RequestFilters.cpp @@ -0,0 +1,65 @@ +// +// RequestFilters.cpp +// interface/src/networking +// +// Created by Kunal Gosar on 2017-03-10. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include "RequestFilters.h" +#include "NetworkingConstants.h" + +#include + +#include + +namespace { + + bool isAuthableHighFidelityURL(const QUrl& url) { + static const QStringList HF_HOSTS = { + "highfidelity.com", "highfidelity.io", + "metaverse.highfidelity.com", "metaverse.highfidelity.io" + }; + const auto& scheme = url.scheme(); + const auto& host = url.host(); + + return (scheme == "https" && HF_HOSTS.contains(host)) || + ((scheme == NetworkingConstants::METAVERSE_SERVER_URL.scheme()) && (host == NetworkingConstants::METAVERSE_SERVER_URL.host())); + } + + bool isScript(const QString filename) { + return filename.endsWith(".js", Qt::CaseInsensitive); + } + + bool isJSON(const QString filename) { + return filename.endsWith(".json", Qt::CaseInsensitive); + } + +} + +void RequestFilters::interceptHFWebEngineRequest(QWebEngineUrlRequestInfo& info) { + // check if this is a request to a highfidelity URL + if (isAuthableHighFidelityURL(info.requestUrl())) { + // if we have an access token, add it to the right HTTP header for authorization + auto accountManager = DependencyManager::get(); + + if (accountManager->hasValidAccessToken()) { + static const QString OAUTH_AUTHORIZATION_HEADER = "Authorization"; + + QString bearerTokenString = "Bearer " + accountManager->getAccountInfo().getAccessToken().token; + info.setHttpHeader(OAUTH_AUTHORIZATION_HEADER.toLocal8Bit(), bearerTokenString.toLocal8Bit()); + } + } +} + +void RequestFilters::interceptFileType(QWebEngineUrlRequestInfo& info) { + QString filename = info.requestUrl().fileName(); + if (isScript(filename) || isJSON(filename)) { + static const QString CONTENT_HEADER = "Accept"; + static const QString TYPE_VALUE = "text/plain,text/html"; + info.setHttpHeader(CONTENT_HEADER.toLocal8Bit(), TYPE_VALUE.toLocal8Bit()); + } +} diff --git a/interface/src/networking/RequestFilters.h b/interface/src/networking/RequestFilters.h new file mode 100644 index 0000000000..0d7d66e155 --- /dev/null +++ b/interface/src/networking/RequestFilters.h @@ -0,0 +1,28 @@ +// +// RequestFilters.h +// interface/src/networking +// +// Created by Kunal Gosar on 2017-03-10. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#pragma once + +#ifndef hifi_RequestFilters_h +#define hifi_RequestFilters_h + +#include +#include + +class RequestFilters : public QObject { + Q_OBJECT + +public: + static void interceptHFWebEngineRequest(QWebEngineUrlRequestInfo& info); + static void interceptFileType(QWebEngineUrlRequestInfo& info); +}; + +#endif // hifi_RequestFilters_h diff --git a/interface/src/scripting/AssetMappingsScriptingInterface.h b/interface/src/scripting/AssetMappingsScriptingInterface.h index 459f01b512..b7fcea2491 100644 --- a/interface/src/scripting/AssetMappingsScriptingInterface.h +++ b/interface/src/scripting/AssetMappingsScriptingInterface.h @@ -20,6 +20,8 @@ #include #include +#include "DependencyManager.h" + class AssetMappingModel : public QStandardItemModel { Q_OBJECT @@ -39,10 +41,12 @@ private: QHash _pathToItemMap; }; -Q_DECLARE_METATYPE(AssetMappingModel*); +Q_DECLARE_METATYPE(AssetMappingModel*) -class AssetMappingsScriptingInterface : public QObject { +class AssetMappingsScriptingInterface : public QObject, public Dependency { Q_OBJECT + SINGLETON_DEPENDENCY + Q_PROPERTY(AssetMappingModel* mappingModel READ getAssetMappingModel CONSTANT) Q_PROPERTY(QAbstractProxyModel* proxyModel READ getProxyModel CONSTANT) public: diff --git a/interface/src/scripting/AudioDeviceScriptingInterface.cpp b/interface/src/scripting/AudioDeviceScriptingInterface.cpp index c4dc58f16b..cbb08c0af0 100644 --- a/interface/src/scripting/AudioDeviceScriptingInterface.cpp +++ b/interface/src/scripting/AudioDeviceScriptingInterface.cpp @@ -18,6 +18,21 @@ AudioDeviceScriptingInterface* AudioDeviceScriptingInterface::getInstance() { return &sharedInstance; } +QStringList AudioDeviceScriptingInterface::inputAudioDevices() const +{ + return DependencyManager::get()->getDeviceNames(QAudio::AudioInput).toList();; +} + +QStringList AudioDeviceScriptingInterface::outputAudioDevices() const +{ + return DependencyManager::get()->getDeviceNames(QAudio::AudioOutput).toList();; +} + +bool AudioDeviceScriptingInterface::muted() +{ + return getMuted(); +} + AudioDeviceScriptingInterface::AudioDeviceScriptingInterface() { connect(DependencyManager::get().data(), &AudioClient::muteToggled, this, &AudioDeviceScriptingInterface::muteToggled); @@ -31,7 +46,6 @@ bool AudioDeviceScriptingInterface::setInputDevice(const QString& deviceName) { Qt::BlockingQueuedConnection, Q_RETURN_ARG(bool, result), Q_ARG(const QString&, deviceName)); - return result; } @@ -41,7 +55,6 @@ bool AudioDeviceScriptingInterface::setOutputDevice(const QString& deviceName) { Qt::BlockingQueuedConnection, Q_RETURN_ARG(bool, result), Q_ARG(const QString&, deviceName)); - return result; } @@ -69,7 +82,6 @@ QVector AudioDeviceScriptingInterface::getOutputDevices() { return DependencyManager::get()->getDeviceNames(QAudio::AudioOutput); } - float AudioDeviceScriptingInterface::getInputVolume() { return DependencyManager::get()->getInputVolume(); } @@ -90,6 +102,17 @@ void AudioDeviceScriptingInterface::toggleMute() { DependencyManager::get()->toggleMute(); } +void AudioDeviceScriptingInterface::setMuted(bool muted) +{ + bool lMuted = getMuted(); + if (lMuted == muted) + return; + + toggleMute(); + lMuted = getMuted(); + emit mutedChanged(lMuted); +} + bool AudioDeviceScriptingInterface::getMuted() { return DependencyManager::get()->isMuted(); } diff --git a/interface/src/scripting/AudioDeviceScriptingInterface.h b/interface/src/scripting/AudioDeviceScriptingInterface.h index 149de9bf56..4d1d47dcba 100644 --- a/interface/src/scripting/AudioDeviceScriptingInterface.h +++ b/interface/src/scripting/AudioDeviceScriptingInterface.h @@ -20,9 +20,18 @@ class AudioEffectOptions; class AudioDeviceScriptingInterface : public QObject { Q_OBJECT + + Q_PROPERTY(QStringList inputAudioDevices READ inputAudioDevices NOTIFY inputAudioDevicesChanged) + Q_PROPERTY(QStringList outputAudioDevices READ outputAudioDevices NOTIFY outputAudioDevicesChanged) + Q_PROPERTY(bool muted READ muted WRITE setMuted NOTIFY mutedChanged) + public: static AudioDeviceScriptingInterface* getInstance(); + QStringList inputAudioDevices() const; + QStringList outputAudioDevices() const; + bool muted(); + public slots: bool setInputDevice(const QString& deviceName); bool setOutputDevice(const QString& deviceName); @@ -44,12 +53,17 @@ public slots: bool getMuted(); void toggleMute(); + void setMuted(bool muted); + private: AudioDeviceScriptingInterface(); signals: void muteToggled(); void deviceChanged(); + void mutedChanged(bool muted); + void inputAudioDevicesChanged(QStringList inputAudioDevices); + void outputAudioDevicesChanged(QStringList outputAudioDevices); }; #endif // hifi_AudioDeviceScriptingInterface_h diff --git a/interface/src/scripting/GlobalServicesScriptingInterface.cpp b/interface/src/scripting/GlobalServicesScriptingInterface.cpp index d7e5bae3f8..f4a5ffb39c 100644 --- a/interface/src/scripting/GlobalServicesScriptingInterface.cpp +++ b/interface/src/scripting/GlobalServicesScriptingInterface.cpp @@ -53,33 +53,19 @@ void GlobalServicesScriptingInterface::loggedOut() { emit GlobalServicesScriptingInterface::disconnected(QString("logout")); } - -QString GlobalServicesScriptingInterface::findableByString(Discoverability::Mode discoverabilityMode) const { - if (discoverabilityMode == Discoverability::None) { - return "none"; - } else if (discoverabilityMode == Discoverability::Friends) { - return "friends"; - } else if (discoverabilityMode == Discoverability::All) { - return "all"; - } else { - qDebug() << "GlobalServices findableByString called with an unrecognized value."; - return ""; - } -} - - QString GlobalServicesScriptingInterface::getFindableBy() const { auto discoverabilityManager = DependencyManager::get(); - return findableByString(discoverabilityManager->getDiscoverabilityMode()); + return DiscoverabilityManager::findableByString(discoverabilityManager->getDiscoverabilityMode()); } void GlobalServicesScriptingInterface::setFindableBy(const QString& discoverabilityMode) { auto discoverabilityManager = DependencyManager::get(); - if (discoverabilityMode.toLower() == "none") { discoverabilityManager->setDiscoverabilityMode(Discoverability::None); } else if (discoverabilityMode.toLower() == "friends") { discoverabilityManager->setDiscoverabilityMode(Discoverability::Friends); + } else if (discoverabilityMode.toLower() == "connections") { + discoverabilityManager->setDiscoverabilityMode(Discoverability::Connections); } else if (discoverabilityMode.toLower() == "all") { discoverabilityManager->setDiscoverabilityMode(Discoverability::All); } else { @@ -88,7 +74,7 @@ void GlobalServicesScriptingInterface::setFindableBy(const QString& discoverabil } void GlobalServicesScriptingInterface::discoverabilityModeChanged(Discoverability::Mode discoverabilityMode) { - emit findableByChanged(findableByString(discoverabilityMode)); + emit findableByChanged(DiscoverabilityManager::findableByString(discoverabilityMode)); } DownloadInfoResult::DownloadInfoResult() : diff --git a/interface/src/scripting/GlobalServicesScriptingInterface.h b/interface/src/scripting/GlobalServicesScriptingInterface.h index 11d8735187..63294fc656 100644 --- a/interface/src/scripting/GlobalServicesScriptingInterface.h +++ b/interface/src/scripting/GlobalServicesScriptingInterface.h @@ -18,6 +18,7 @@ #include #include #include +#include class DownloadInfoResult { public: @@ -35,7 +36,7 @@ class GlobalServicesScriptingInterface : public QObject { Q_OBJECT Q_PROPERTY(QString username READ getUsername) - Q_PROPERTY(QString findableBy READ getFindableBy WRITE setFindableBy) + Q_PROPERTY(QString findableBy READ getFindableBy WRITE setFindableBy NOTIFY findableByChanged) public: static GlobalServicesScriptingInterface* getInstance(); @@ -65,8 +66,6 @@ private: GlobalServicesScriptingInterface(); ~GlobalServicesScriptingInterface(); - QString findableByString(Discoverability::Mode discoverabilityMode) const; - bool _downloading; }; diff --git a/interface/src/scripting/HMDScriptingInterface.cpp b/interface/src/scripting/HMDScriptingInterface.cpp index 2bca793d80..e2fed40a6d 100644 --- a/interface/src/scripting/HMDScriptingInterface.cpp +++ b/interface/src/scripting/HMDScriptingInterface.cpp @@ -81,6 +81,10 @@ void HMDScriptingInterface::closeTablet() { _showTablet = false; } +void HMDScriptingInterface::openTablet() { + _showTablet = true; +} + QScriptValue HMDScriptingInterface::getHUDLookAtPosition2D(QScriptContext* context, QScriptEngine* engine) { glm::vec3 hudIntersection; auto instance = DependencyManager::get(); @@ -131,7 +135,7 @@ glm::quat HMDScriptingInterface::getOrientation() const { return glm::quat(); } -bool HMDScriptingInterface::isMounted() const{ +bool HMDScriptingInterface::isMounted() const { auto displayPlugin = qApp->getActiveDisplayPlugin(); return (displayPlugin->isHmd() && displayPlugin->isDisplayVisible()); } diff --git a/interface/src/scripting/HMDScriptingInterface.h b/interface/src/scripting/HMDScriptingInterface.h index d895d5da4c..7ecafdcbcb 100644 --- a/interface/src/scripting/HMDScriptingInterface.h +++ b/interface/src/scripting/HMDScriptingInterface.h @@ -27,7 +27,7 @@ class HMDScriptingInterface : public AbstractHMDScriptingInterface, public Depen Q_OBJECT Q_PROPERTY(glm::vec3 position READ getPosition) Q_PROPERTY(glm::quat orientation READ getOrientation) - Q_PROPERTY(bool mounted READ isMounted) + Q_PROPERTY(bool mounted READ isMounted NOTIFY mountedChanged) Q_PROPERTY(bool showTablet READ getShouldShowTablet) Q_PROPERTY(QUuid tabletID READ getCurrentTabletFrameID WRITE setCurrentTabletFrameID) Q_PROPERTY(QUuid homeButtonID READ getCurrentHomeButtonID WRITE setCurrentHomeButtonID) @@ -76,8 +76,11 @@ public: Q_INVOKABLE void closeTablet(); + Q_INVOKABLE void openTablet(); + signals: bool shouldShowHandControllersChanged(); + void mountedChanged(); public: HMDScriptingInterface(); diff --git a/interface/src/scripting/LimitlessConnection.cpp b/interface/src/scripting/LimitlessConnection.cpp new file mode 100644 index 0000000000..b9f4eacd4b --- /dev/null +++ b/interface/src/scripting/LimitlessConnection.cpp @@ -0,0 +1,91 @@ +#include +#include +#include +#include +#include "LimitlessConnection.h" +#include "LimitlessVoiceRecognitionScriptingInterface.h" + +LimitlessConnection::LimitlessConnection() : + _streamingAudioForTranscription(false) +{ +} + +void LimitlessConnection::startListening(QString authCode) { + _transcribeServerSocket.reset(new QTcpSocket(this)); + connect(_transcribeServerSocket.get(), &QTcpSocket::readyRead, this, + &LimitlessConnection::transcriptionReceived); + connect(_transcribeServerSocket.get(), &QTcpSocket::disconnected, this, [this](){stopListening();}); + + static const auto host = "gserv_devel.studiolimitless.com"; + _transcribeServerSocket->connectToHost(host, 1407); + _transcribeServerSocket->waitForConnected(); + QString requestHeader = QString::asprintf("Authorization: %s\r\nfs: %i\r\n", + authCode.toLocal8Bit().data(), AudioConstants::SAMPLE_RATE); + qCDebug(interfaceapp) << "Sending Limitless Audio Stream Request: " << requestHeader; + _transcribeServerSocket->write(requestHeader.toLocal8Bit()); + _transcribeServerSocket->waitForBytesWritten(); +} + +void LimitlessConnection::stopListening() { + emit onFinishedSpeaking(_currentTranscription); + _streamingAudioForTranscription = false; + _currentTranscription = ""; + if (!isConnected()) + return; + _transcribeServerSocket->close(); + disconnect(_transcribeServerSocket.get(), &QTcpSocket::readyRead, this, + &LimitlessConnection::transcriptionReceived); + _transcribeServerSocket.release()->deleteLater(); + disconnect(DependencyManager::get().data(), &AudioClient::inputReceived, this, + &LimitlessConnection::audioInputReceived); + qCDebug(interfaceapp) << "Connection to Limitless Voice Server closed."; +} + +void LimitlessConnection::audioInputReceived(const QByteArray& inputSamples) { + if (isConnected()) { + _transcribeServerSocket->write(inputSamples.data(), inputSamples.size()); + _transcribeServerSocket->waitForBytesWritten(); + } +} + +void LimitlessConnection::transcriptionReceived() { + while (_transcribeServerSocket && _transcribeServerSocket->bytesAvailable() > 0) { + const QByteArray data = _transcribeServerSocket->readAll(); + _serverDataBuffer.append(data); + int begin = _serverDataBuffer.indexOf('<'); + int end = _serverDataBuffer.indexOf('>'); + while (begin > -1 && end > -1) { + const int len = end - begin; + const QByteArray serverMessage = _serverDataBuffer.mid(begin+1, len-1); + if (serverMessage.contains("1407")) { + qCDebug(interfaceapp) << "Limitless Speech Server denied the request."; + // Don't spam the server with further false requests please. + DependencyManager::get()->setListeningToVoice(true); + stopListening(); + return; + } else if (serverMessage.contains("1408")) { + qCDebug(interfaceapp) << "Limitless Audio request authenticated!"; + _serverDataBuffer.clear(); + connect(DependencyManager::get().data(), &AudioClient::inputReceived, this, + &LimitlessConnection::audioInputReceived); + return; + } + QJsonObject json = QJsonDocument::fromJson(serverMessage.data()).object(); + _serverDataBuffer.remove(begin, len+1); + _currentTranscription = json["alternatives"].toArray()[0].toObject()["transcript"].toString(); + emit onReceivedTranscription(_currentTranscription); + if (json["isFinal"] == true) { + qCDebug(interfaceapp) << "Final transcription: " << _currentTranscription; + stopListening(); + return; + } + begin = _serverDataBuffer.indexOf('<'); + end = _serverDataBuffer.indexOf('>'); + } + } +} + +bool LimitlessConnection::isConnected() const { + return _transcribeServerSocket.get() && _transcribeServerSocket->isWritable() + && _transcribeServerSocket->state() != QAbstractSocket::SocketState::UnconnectedState; +} diff --git a/interface/src/scripting/LimitlessConnection.h b/interface/src/scripting/LimitlessConnection.h new file mode 100644 index 0000000000..ee049aff8e --- /dev/null +++ b/interface/src/scripting/LimitlessConnection.h @@ -0,0 +1,44 @@ +// +// SpeechRecognitionScriptingInterface.h +// interface/src/scripting +// +// Created by Trevor Berninger on 3/24/17. +// Copyright 2017 Limitless ltd. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_LimitlessConnection_h +#define hifi_LimitlessConnection_h + +#include +#include +#include + +class LimitlessConnection : public QObject { + Q_OBJECT +public: + LimitlessConnection(); + + Q_INVOKABLE void startListening(QString authCode); + Q_INVOKABLE void stopListening(); + + std::atomic _streamingAudioForTranscription; + +signals: + void onReceivedTranscription(QString speech); + void onFinishedSpeaking(QString speech); + +private: + void transcriptionReceived(); + void audioInputReceived(const QByteArray& inputSamples); + + bool isConnected() const; + + std::unique_ptr _transcribeServerSocket; + QByteArray _serverDataBuffer; + QString _currentTranscription; +}; + +#endif //hifi_LimitlessConnection_h diff --git a/interface/src/scripting/LimitlessVoiceRecognitionScriptingInterface.cpp b/interface/src/scripting/LimitlessVoiceRecognitionScriptingInterface.cpp new file mode 100644 index 0000000000..1352630f84 --- /dev/null +++ b/interface/src/scripting/LimitlessVoiceRecognitionScriptingInterface.cpp @@ -0,0 +1,64 @@ +// +// SpeechRecognitionScriptingInterface.h +// interface/src/scripting +// +// Created by Trevor Berninger on 3/20/17. +// Copyright 2017 Limitless ltd. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include +#include +#include +#include "LimitlessVoiceRecognitionScriptingInterface.h" + +const float LimitlessVoiceRecognitionScriptingInterface::_audioLevelThreshold = 0.33f; +const int LimitlessVoiceRecognitionScriptingInterface::_voiceTimeoutDuration = 2000; + +LimitlessVoiceRecognitionScriptingInterface::LimitlessVoiceRecognitionScriptingInterface() : + _shouldStartListeningForVoice(false) +{ + _voiceTimer.setSingleShot(true); + connect(&_voiceTimer, &QTimer::timeout, this, &LimitlessVoiceRecognitionScriptingInterface::voiceTimeout); + connect(&_connection, &LimitlessConnection::onReceivedTranscription, this, [this](QString transcription){emit onReceivedTranscription(transcription);}); + connect(&_connection, &LimitlessConnection::onFinishedSpeaking, this, [this](QString transcription){emit onFinishedSpeaking(transcription);}); + _connection.moveToThread(&_connectionThread); + _connectionThread.setObjectName("Limitless Connection"); + _connectionThread.start(); +} + +void LimitlessVoiceRecognitionScriptingInterface::update() { + const float audioLevel = AvatarInputs::getInstance()->loudnessToAudioLevel(DependencyManager::get()->getAudioAverageInputLoudness()); + + if (_shouldStartListeningForVoice) { + if (_connection._streamingAudioForTranscription) { + if (audioLevel > _audioLevelThreshold) { + if (_voiceTimer.isActive()) { + _voiceTimer.stop(); + } + } else if (!_voiceTimer.isActive()){ + _voiceTimer.start(_voiceTimeoutDuration); + } + } else if (audioLevel > _audioLevelThreshold) { + // to make sure invoke doesn't get called twice before the method actually gets called + _connection._streamingAudioForTranscription = true; + QMetaObject::invokeMethod(&_connection, "startListening", Q_ARG(QString, authCode)); + } + } +} + +void LimitlessVoiceRecognitionScriptingInterface::setListeningToVoice(bool listening) { + _shouldStartListeningForVoice = listening; +} + +void LimitlessVoiceRecognitionScriptingInterface::setAuthKey(QString key) { + authCode = key; +} + +void LimitlessVoiceRecognitionScriptingInterface::voiceTimeout() { + if (_connection._streamingAudioForTranscription) { + QMetaObject::invokeMethod(&_connection, "stopListening"); + } +} diff --git a/interface/src/scripting/LimitlessVoiceRecognitionScriptingInterface.h b/interface/src/scripting/LimitlessVoiceRecognitionScriptingInterface.h new file mode 100644 index 0000000000..d1b1139695 --- /dev/null +++ b/interface/src/scripting/LimitlessVoiceRecognitionScriptingInterface.h @@ -0,0 +1,50 @@ +// +// SpeechRecognitionScriptingInterface.h +// interface/src/scripting +// +// Created by Trevor Berninger on 3/20/17. +// Copyright 2017 Limitless ltd. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_SpeechRecognitionScriptingInterface_h +#define hifi_SpeechRecognitionScriptingInterface_h + +#include +#include +#include +#include "LimitlessConnection.h" + +class LimitlessVoiceRecognitionScriptingInterface : public QObject, public Dependency { + Q_OBJECT +public: + LimitlessVoiceRecognitionScriptingInterface(); + + void update(); + + QString authCode; + +public slots: + void setListeningToVoice(bool listening); + void setAuthKey(QString key); + +signals: + void onReceivedTranscription(QString speech); + void onFinishedSpeaking(QString speech); + +private: + + bool _shouldStartListeningForVoice; + static const float _audioLevelThreshold; + static const int _voiceTimeoutDuration; + + QTimer _voiceTimer; + QThread _connectionThread; + LimitlessConnection _connection; + + void voiceTimeout(); +}; + +#endif //hifi_SpeechRecognitionScriptingInterface_h diff --git a/interface/src/scripting/WindowScriptingInterface.cpp b/interface/src/scripting/WindowScriptingInterface.cpp index 9c1aedf7a0..39c2f2e402 100644 --- a/interface/src/scripting/WindowScriptingInterface.cpp +++ b/interface/src/scripting/WindowScriptingInterface.cpp @@ -235,6 +235,14 @@ void WindowScriptingInterface::shareSnapshot(const QString& path, const QUrl& hr qApp->shareSnapshot(path, href); } +void WindowScriptingInterface::makeConnection(bool success, const QString& userNameOrError) { + if (success) { + emit connectionAdded(userNameOrError); + } else { + emit connectionError(userNameOrError); + } +} + bool WindowScriptingInterface::isPhysicsEnabled() { return qApp->isPhysicsEnabled(); } @@ -255,7 +263,7 @@ int WindowScriptingInterface::openMessageBox(QString title, QString text, int bu } int WindowScriptingInterface::createMessageBox(QString title, QString text, int buttons, int defaultButton) { - auto messageBox = DependencyManager::get()->createMessageBox(OffscreenUi::ICON_INFORMATION, title, text, + auto messageBox = DependencyManager::get()->createMessageBox(OffscreenUi::ICON_INFORMATION, title, text, static_cast>(buttons), static_cast(defaultButton)); connect(messageBox, SIGNAL(selected(int)), this, SLOT(onMessageBoxSelected(int))); diff --git a/interface/src/scripting/WindowScriptingInterface.h b/interface/src/scripting/WindowScriptingInterface.h index 60d24d50df..b7bed7d85f 100644 --- a/interface/src/scripting/WindowScriptingInterface.h +++ b/interface/src/scripting/WindowScriptingInterface.h @@ -56,6 +56,7 @@ public slots: void showAssetServer(const QString& upload = ""); void copyToClipboard(const QString& text); void takeSnapshot(bool notify = true, bool includeAnimated = false, float aspectRatio = 0.0f); + void makeConnection(bool success, const QString& userNameOrError); void shareSnapshot(const QString& path, const QUrl& href = QUrl("")); bool isPhysicsEnabled(); @@ -74,6 +75,9 @@ signals: void snapshotShared(const QString& error); void processingGif(); + void connectionAdded(const QString& connectionName); + void connectionError(const QString& errorString); + void messageBoxClosed(int id, int button); // triggered when window size or position changes diff --git a/interface/src/ui/ApplicationOverlay.cpp b/interface/src/ui/ApplicationOverlay.cpp index 364dff52a3..7239e49d89 100644 --- a/interface/src/ui/ApplicationOverlay.cpp +++ b/interface/src/ui/ApplicationOverlay.cpp @@ -13,7 +13,6 @@ #include #include -#include #include #include #include @@ -42,7 +41,6 @@ ApplicationOverlay::ApplicationOverlay() _domainStatusBorder = geometryCache->allocateID(); _magnifierBorder = geometryCache->allocateID(); _qmlGeometryId = geometryCache->allocateID(); - _rearViewGeometryId = geometryCache->allocateID(); } ApplicationOverlay::~ApplicationOverlay() { @@ -51,7 +49,6 @@ ApplicationOverlay::~ApplicationOverlay() { geometryCache->releaseID(_domainStatusBorder); geometryCache->releaseID(_magnifierBorder); geometryCache->releaseID(_qmlGeometryId); - geometryCache->releaseID(_rearViewGeometryId); } } @@ -86,10 +83,8 @@ void ApplicationOverlay::renderOverlay(RenderArgs* renderArgs) { // Now render the overlay components together into a single texture renderDomainConnectionStatusBorder(renderArgs); // renders the connected domain line renderAudioScope(renderArgs); // audio scope in the very back - NOTE: this is the debug audio scope, not the VU meter - renderRearView(renderArgs); // renders the mirror view selfie renderOverlays(renderArgs); // renders Scripts Overlay and AudioScope renderQmlUi(renderArgs); // renders a unit quad with the QML UI texture, and the text overlays from scripts - renderStatsAndLogs(renderArgs); // currently renders nothing }); renderArgs->_batch = nullptr; // so future users of renderArgs don't try to use our batch @@ -99,7 +94,7 @@ void ApplicationOverlay::renderQmlUi(RenderArgs* renderArgs) { PROFILE_RANGE(app, __FUNCTION__); if (!_uiTexture) { - _uiTexture = gpu::TexturePointer(gpu::Texture::createExternal2D(OffscreenQmlSurface::getDiscardLambda())); + _uiTexture = gpu::TexturePointer(gpu::Texture::createExternal(OffscreenQmlSurface::getDiscardLambda())); _uiTexture->setSource(__FUNCTION__); } // Once we move UI rendering and screen rendering to different @@ -163,66 +158,6 @@ void ApplicationOverlay::renderOverlays(RenderArgs* renderArgs) { qApp->getOverlays().renderHUD(renderArgs); } -void ApplicationOverlay::renderRearViewToFbo(RenderArgs* renderArgs) { -} - -void ApplicationOverlay::renderRearView(RenderArgs* renderArgs) { - if (!qApp->isHMDMode() && Menu::getInstance()->isOptionChecked(MenuOption::MiniMirror) && - !Menu::getInstance()->isOptionChecked(MenuOption::FullscreenMirror)) { - gpu::Batch& batch = *renderArgs->_batch; - - auto geometryCache = DependencyManager::get(); - - auto framebuffer = DependencyManager::get(); - auto selfieTexture = framebuffer->getSelfieFramebuffer()->getRenderBuffer(0); - - int width = renderArgs->_viewport.z; - int height = renderArgs->_viewport.w; - mat4 legacyProjection = glm::ortho(0, width, height, 0, ORTHO_NEAR_CLIP, ORTHO_FAR_CLIP); - batch.setProjectionTransform(legacyProjection); - batch.setModelTransform(Transform()); - batch.resetViewTransform(); - - float screenRatio = ((float)qApp->getDevicePixelRatio()); - float renderRatio = ((float)qApp->getRenderResolutionScale()); - - auto viewport = qApp->getMirrorViewRect(); - glm::vec2 bottomLeft(viewport.left(), viewport.top() + viewport.height()); - glm::vec2 topRight(viewport.left() + viewport.width(), viewport.top()); - bottomLeft *= screenRatio; - topRight *= screenRatio; - glm::vec2 texCoordMinCorner(0.0f, 0.0f); - glm::vec2 texCoordMaxCorner(viewport.width() * renderRatio / float(selfieTexture->getWidth()), viewport.height() * renderRatio / float(selfieTexture->getHeight())); - - batch.setResourceTexture(0, selfieTexture); - float alpha = DependencyManager::get()->getDesktop()->property("unpinnedAlpha").toFloat(); - geometryCache->renderQuad(batch, bottomLeft, topRight, texCoordMinCorner, texCoordMaxCorner, glm::vec4(1.0f, 1.0f, 1.0f, alpha), _rearViewGeometryId); - - batch.setResourceTexture(0, renderArgs->_whiteTexture); - } -} - -void ApplicationOverlay::renderStatsAndLogs(RenderArgs* renderArgs) { - - // Display stats and log text onscreen - - // Determine whether to compute timing details - - /* - // Show on-screen msec timer - if (Menu::getInstance()->isOptionChecked(MenuOption::FrameTimer)) { - auto canvasSize = qApp->getCanvasSize(); - quint64 mSecsNow = floor(usecTimestampNow() / 1000.0 + 0.5); - QString frameTimer = QString("%1\n").arg((int)(mSecsNow % 1000)); - int timerBottom = - (Menu::getInstance()->isOptionChecked(MenuOption::Stats)) - ? 80 : 20; - drawText(canvasSize.x - 100, canvasSize.y - timerBottom, - 0.30f, 0.0f, 0, frameTimer.toUtf8().constData(), WHITE_TEXT); - } - */ -} - void ApplicationOverlay::renderDomainConnectionStatusBorder(RenderArgs* renderArgs) { auto geometryCache = DependencyManager::get(); static std::once_flag once; @@ -272,13 +207,13 @@ void ApplicationOverlay::buildFramebufferObject() { auto width = uiSize.x; auto height = uiSize.y; if (!_overlayFramebuffer->getDepthStencilBuffer()) { - auto overlayDepthTexture = gpu::TexturePointer(gpu::Texture::create2D(DEPTH_FORMAT, width, height, DEFAULT_SAMPLER)); + auto overlayDepthTexture = gpu::TexturePointer(gpu::Texture::createRenderBuffer(DEPTH_FORMAT, width, height, gpu::Texture::SINGLE_MIP, DEFAULT_SAMPLER)); _overlayFramebuffer->setDepthStencilBuffer(overlayDepthTexture, DEPTH_FORMAT); } if (!_overlayFramebuffer->getRenderBuffer(0)) { const gpu::Sampler OVERLAY_SAMPLER(gpu::Sampler::FILTER_MIN_MAG_LINEAR, gpu::Sampler::WRAP_CLAMP); - auto colorBuffer = gpu::TexturePointer(gpu::Texture::create2D(COLOR_FORMAT, width, height, OVERLAY_SAMPLER)); + auto colorBuffer = gpu::TexturePointer(gpu::Texture::createRenderBuffer(COLOR_FORMAT, width, height, gpu::Texture::SINGLE_MIP, OVERLAY_SAMPLER)); _overlayFramebuffer->setRenderBuffer(0, colorBuffer); } } diff --git a/interface/src/ui/ApplicationOverlay.h b/interface/src/ui/ApplicationOverlay.h index 7ace5ee885..af4d8779d4 100644 --- a/interface/src/ui/ApplicationOverlay.h +++ b/interface/src/ui/ApplicationOverlay.h @@ -31,8 +31,6 @@ public: private: void renderStatsAndLogs(RenderArgs* renderArgs); void renderDomainConnectionStatusBorder(RenderArgs* renderArgs); - void renderRearViewToFbo(RenderArgs* renderArgs); - void renderRearView(RenderArgs* renderArgs); void renderQmlUi(RenderArgs* renderArgs); void renderAudioScope(RenderArgs* renderArgs); void renderOverlays(RenderArgs* renderArgs); @@ -51,7 +49,6 @@ private: gpu::TexturePointer _overlayColorTexture; gpu::FramebufferPointer _overlayFramebuffer; int _qmlGeometryId { 0 }; - int _rearViewGeometryId { 0 }; }; #endif // hifi_ApplicationOverlay_h diff --git a/interface/src/ui/AvatarInputs.cpp b/interface/src/ui/AvatarInputs.cpp index b09289c78a..341915e57f 100644 --- a/interface/src/ui/AvatarInputs.cpp +++ b/interface/src/ui/AvatarInputs.cpp @@ -20,10 +20,6 @@ HIFI_QML_DEF(AvatarInputs) static AvatarInputs* INSTANCE{ nullptr }; -static const char SETTINGS_GROUP_NAME[] = "Rear View Tools"; -static const char ZOOM_LEVEL_SETTINGS[] = "ZoomLevel"; - -static Setting::Handle rearViewZoomLevel(QStringList() << SETTINGS_GROUP_NAME << ZOOM_LEVEL_SETTINGS, 0); AvatarInputs* AvatarInputs::getInstance() { if (!INSTANCE) { @@ -36,8 +32,6 @@ AvatarInputs* AvatarInputs::getInstance() { AvatarInputs::AvatarInputs(QQuickItem* parent) : QQuickItem(parent) { INSTANCE = this; - int zoomSetting = rearViewZoomLevel.get(); - _mirrorZoomed = zoomSetting == 0; } #define AI_UPDATE(name, src) \ @@ -49,34 +43,32 @@ AvatarInputs::AvatarInputs(QQuickItem* parent) : QQuickItem(parent) { } \ } +#define AI_UPDATE_WRITABLE(name, src) \ + { \ + auto val = src; \ + if (_##name != val) { \ + _##name = val; \ + qDebug() << "AvatarInputs" << val; \ + emit name##Changed(val); \ + } \ + } + #define AI_UPDATE_FLOAT(name, src, epsilon) \ { \ float val = src; \ - if (fabs(_##name - val) >= epsilon) { \ + if (fabsf(_##name - val) >= epsilon) { \ _##name = val; \ emit name##Changed(); \ } \ } -void AvatarInputs::update() { - if (!Menu::getInstance()) { - return; - } - AI_UPDATE(mirrorVisible, Menu::getInstance()->isOptionChecked(MenuOption::MiniMirror) && !qApp->isHMDMode() - && !Menu::getInstance()->isOptionChecked(MenuOption::FullscreenMirror)); - AI_UPDATE(cameraEnabled, !Menu::getInstance()->isOptionChecked(MenuOption::NoFaceTracking)); - AI_UPDATE(cameraMuted, Menu::getInstance()->isOptionChecked(MenuOption::MuteFaceTracking)); - AI_UPDATE(isHMD, qApp->isHMDMode()); - AI_UPDATE(showAudioTools, Menu::getInstance()->isOptionChecked(MenuOption::AudioTools)); - - auto audioIO = DependencyManager::get(); +float AvatarInputs::loudnessToAudioLevel(float loudness) { const float AUDIO_METER_AVERAGING = 0.5; const float LOG2 = log(2.0f); const float METER_LOUDNESS_SCALE = 2.8f / 5.0f; const float LOG2_LOUDNESS_FLOOR = 11.0f; float audioLevel = 0.0f; - auto audio = DependencyManager::get(); - float loudness = audio->getLastInputLoudness() + 1.0f; + loudness += 1.0f; _trailingAudioLoudness = AUDIO_METER_AVERAGING * _trailingAudioLoudness + (1.0f - AUDIO_METER_AVERAGING) * loudness; @@ -90,7 +82,25 @@ void AvatarInputs::update() { if (audioLevel > 1.0f) { audioLevel = 1.0; } - AI_UPDATE_FLOAT(audioLevel, audioLevel, 0.01); + return audioLevel; +} + +void AvatarInputs::update() { + if (!Menu::getInstance()) { + return; + } + + AI_UPDATE(cameraEnabled, !Menu::getInstance()->isOptionChecked(MenuOption::NoFaceTracking)); + AI_UPDATE(cameraMuted, Menu::getInstance()->isOptionChecked(MenuOption::MuteFaceTracking)); + AI_UPDATE(isHMD, qApp->isHMDMode()); + + AI_UPDATE_WRITABLE(showAudioTools, Menu::getInstance()->isOptionChecked(MenuOption::AudioTools)); + + auto audioIO = DependencyManager::get(); + + const float audioLevel = loudnessToAudioLevel(DependencyManager::get()->getLastInputLoudness()); + + AI_UPDATE_FLOAT(audioLevel, audioLevel, 0.01f); AI_UPDATE(audioClipping, ((audioIO->getTimeSinceLastClip() > 0.0f) && (audioIO->getTimeSinceLastClip() < 1.0f))); AI_UPDATE(audioMuted, audioIO->isMuted()); @@ -108,6 +118,14 @@ void AvatarInputs::update() { //iconColor = PULSE_MIN + (PULSE_MAX - PULSE_MIN) * pulseFactor; } +void AvatarInputs::setShowAudioTools(bool showAudioTools) { + if (_showAudioTools == showAudioTools) + return; + + Menu::getInstance()->setIsOptionChecked(MenuOption::AudioTools, showAudioTools); + update(); +} + void AvatarInputs::toggleCameraMute() { FaceTracker* faceTracker = qApp->getSelectedFaceTracker(); if (faceTracker) { @@ -122,15 +140,3 @@ void AvatarInputs::toggleAudioMute() { void AvatarInputs::resetSensors() { qApp->resetSensors(); } - -void AvatarInputs::toggleZoom() { - _mirrorZoomed = !_mirrorZoomed; - rearViewZoomLevel.set(_mirrorZoomed ? 0 : 1); - emit mirrorZoomedChanged(); -} - -void AvatarInputs::closeMirror() { - if (Menu::getInstance()->isOptionChecked(MenuOption::MiniMirror)) { - Menu::getInstance()->triggerOption(MenuOption::MiniMirror); - } -} diff --git a/interface/src/ui/AvatarInputs.h b/interface/src/ui/AvatarInputs.h index 85570ecd3c..34b2cbca8b 100644 --- a/interface/src/ui/AvatarInputs.h +++ b/interface/src/ui/AvatarInputs.h @@ -28,15 +28,19 @@ class AvatarInputs : public QQuickItem { AI_PROPERTY(bool, audioMuted, false) AI_PROPERTY(bool, audioClipping, false) AI_PROPERTY(float, audioLevel, 0) - AI_PROPERTY(bool, mirrorVisible, false) - AI_PROPERTY(bool, mirrorZoomed, true) AI_PROPERTY(bool, isHMD, false) - AI_PROPERTY(bool, showAudioTools, true) + + Q_PROPERTY(bool showAudioTools READ showAudioTools WRITE setShowAudioTools NOTIFY showAudioToolsChanged) public: static AvatarInputs* getInstance(); + float loudnessToAudioLevel(float loudness); AvatarInputs(QQuickItem* parent = nullptr); void update(); + bool showAudioTools() const { return _showAudioTools; } + +public slots: + void setShowAudioTools(bool showAudioTools); signals: void cameraEnabledChanged(); @@ -44,20 +48,17 @@ signals: void audioMutedChanged(); void audioClippingChanged(); void audioLevelChanged(); - void mirrorVisibleChanged(); - void mirrorZoomedChanged(); void isHMDChanged(); - void showAudioToolsChanged(); + void showAudioToolsChanged(bool showAudioTools); protected: Q_INVOKABLE void resetSensors(); Q_INVOKABLE void toggleCameraMute(); Q_INVOKABLE void toggleAudioMute(); - Q_INVOKABLE void toggleZoom(); - Q_INVOKABLE void closeMirror(); private: float _trailingAudioLoudness{ 0 }; + bool _showAudioTools { false }; }; #endif // hifi_AvatarInputs_h diff --git a/interface/src/ui/BandwidthDialog.cpp b/interface/src/ui/BandwidthDialog.cpp deleted file mode 100644 index f07c844894..0000000000 --- a/interface/src/ui/BandwidthDialog.cpp +++ /dev/null @@ -1,135 +0,0 @@ -// -// BandwidthDialog.cpp -// interface/src/ui -// -// Created by Tobias Schwinger on 6/21/13. -// Copyright 2013 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -#include - -#include "BandwidthRecorder.h" -#include "ui/BandwidthDialog.h" - -#include -#include - -#include -#include - - -BandwidthChannelDisplay::BandwidthChannelDisplay(QVector nodeTypesToFollow, - QFormLayout* form, - char const* const caption, char const* unitCaption, - const float unitScale, unsigned colorRGBA) : - _nodeTypesToFollow(nodeTypesToFollow), - _caption(caption), - _unitCaption(unitCaption), - _unitScale(unitScale), - _colorRGBA(colorRGBA) -{ - _label = new QLabel(); - _label->setAlignment(Qt::AlignRight); - - QPalette palette = _label->palette(); - unsigned rgb = colorRGBA >> 8; - rgb = ((rgb & 0xfefefeu) >> 1) + ((rgb & 0xf8f8f8) >> 3); - palette.setColor(QPalette::WindowText, QColor::fromRgb(rgb)); - _label->setPalette(palette); - - form->addRow(QString(" ") + _caption + " Bandwidth In/Out:", _label); -} - - - -void BandwidthChannelDisplay::bandwidthAverageUpdated() { - float inTotal = 0.; - float outTotal = 0.; - - QSharedPointer bandwidthRecorder = DependencyManager::get(); - - for (int i = 0; i < _nodeTypesToFollow.size(); ++i) { - inTotal += bandwidthRecorder->getAverageInputKilobitsPerSecond(_nodeTypesToFollow.at(i)); - outTotal += bandwidthRecorder->getAverageOutputKilobitsPerSecond(_nodeTypesToFollow.at(i)); - } - - _strBuf = - QString("").setNum((int) (inTotal * _unitScale)) + "/" + - QString("").setNum((int) (outTotal * _unitScale)) + " " + _unitCaption; -} - - -void BandwidthChannelDisplay::paint() { - _label->setText(_strBuf); -} - - -BandwidthDialog::BandwidthDialog(QWidget* parent) : - QDialog(parent, Qt::Window | Qt::WindowCloseButtonHint | Qt::WindowStaysOnTopHint) { - - this->setWindowTitle("Bandwidth Details"); - - // Create layout - QFormLayout* form = new QFormLayout(); - form->setSizeConstraint(QLayout::SetFixedSize); - this->QDialog::setLayout(form); - - QSharedPointer bandwidthRecorder = DependencyManager::get(); - - _allChannelDisplays[0] = _audioChannelDisplay = - new BandwidthChannelDisplay({NodeType::AudioMixer}, form, "Audio", "Kbps", 1.0, COLOR0); - _allChannelDisplays[1] = _avatarsChannelDisplay = - new BandwidthChannelDisplay({NodeType::Agent, NodeType::AvatarMixer}, form, "Avatars", "Kbps", 1.0, COLOR1); - _allChannelDisplays[2] = _octreeChannelDisplay = - new BandwidthChannelDisplay({NodeType::EntityServer}, form, "Octree", "Kbps", 1.0, COLOR2); - _allChannelDisplays[3] = _octreeChannelDisplay = - new BandwidthChannelDisplay({NodeType::DomainServer}, form, "Domain", "Kbps", 1.0, COLOR2); - _allChannelDisplays[4] = _otherChannelDisplay = - new BandwidthChannelDisplay({NodeType::Unassigned}, form, "Other", "Kbps", 1.0, COLOR2); - _allChannelDisplays[5] = _totalChannelDisplay = - new BandwidthChannelDisplay({ - NodeType::DomainServer, NodeType::EntityServer, - NodeType::AudioMixer, NodeType::Agent, - NodeType::AvatarMixer, NodeType::Unassigned - }, form, "Total", "Kbps", 1.0, COLOR2); - - connect(averageUpdateTimer, SIGNAL(timeout()), this, SLOT(updateTimerTimeout())); - averageUpdateTimer->start(1000); -} - - -BandwidthDialog::~BandwidthDialog() { - for (unsigned int i = 0; i < _CHANNELCOUNT; i++) { - delete _allChannelDisplays[i]; - } -} - - -void BandwidthDialog::updateTimerTimeout() { - for (unsigned int i = 0; i < _CHANNELCOUNT; i++) { - _allChannelDisplays[i]->bandwidthAverageUpdated(); - } -} - - -void BandwidthDialog::paintEvent(QPaintEvent* event) { - for (unsigned int i=0; i<_CHANNELCOUNT; i++) - _allChannelDisplays[i]->paint(); - this->QDialog::paintEvent(event); -} - -void BandwidthDialog::reject() { - - // Just regularly close upon ESC - this->QDialog::close(); -} - -void BandwidthDialog::closeEvent(QCloseEvent* event) { - - this->QDialog::closeEvent(event); - emit closed(); -} - diff --git a/interface/src/ui/BandwidthDialog.h b/interface/src/ui/BandwidthDialog.h deleted file mode 100644 index a53cc21030..0000000000 --- a/interface/src/ui/BandwidthDialog.h +++ /dev/null @@ -1,94 +0,0 @@ -// -// BandwidthDialog.h -// interface/src/ui -// -// Created by Tobias Schwinger on 6/21/13. -// Copyright 2013 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -#ifndef hifi_BandwidthDialog_h -#define hifi_BandwidthDialog_h - -#include -#include -#include -#include -#include - -#include "Node.h" -#include "BandwidthRecorder.h" - - -const unsigned int COLOR0 = 0x33cc99ff; -const unsigned int COLOR1 = 0xffef40c0; -const unsigned int COLOR2 = 0xd0d0d0a0; - - -class BandwidthChannelDisplay : public QObject { - Q_OBJECT - - public: - BandwidthChannelDisplay(QVector nodeTypesToFollow, - QFormLayout* form, - char const* const caption, char const* unitCaption, float unitScale, unsigned colorRGBA); - void paint(); - - private: - QVector _nodeTypesToFollow; - QLabel* _label; - QString _strBuf; - char const* const _caption; - char const* _unitCaption; - float const _unitScale; - unsigned _colorRGBA; - - - public slots: - void bandwidthAverageUpdated(); -}; - - -class BandwidthDialog : public QDialog { - Q_OBJECT -public: - BandwidthDialog(QWidget* parent); - ~BandwidthDialog(); - - void paintEvent(QPaintEvent*) override; - -private: - BandwidthChannelDisplay* _audioChannelDisplay; - BandwidthChannelDisplay* _avatarsChannelDisplay; - BandwidthChannelDisplay* _octreeChannelDisplay; - BandwidthChannelDisplay* _domainChannelDisplay; - BandwidthChannelDisplay* _otherChannelDisplay; - BandwidthChannelDisplay* _totalChannelDisplay; // sums of all the other channels - - static const unsigned int _CHANNELCOUNT = 6; - BandwidthChannelDisplay* _allChannelDisplays[_CHANNELCOUNT]; - - -signals: - - void closed(); - -public slots: - - void reject() override; - void updateTimerTimeout(); - - -protected: - - // Emits a 'closed' signal when this dialog is closed. - void closeEvent(QCloseEvent*) override; - -private: - QTimer* averageUpdateTimer = new QTimer(this); - -}; - -#endif // hifi_BandwidthDialog_h diff --git a/interface/src/ui/BaseLogDialog.cpp b/interface/src/ui/BaseLogDialog.cpp index 7e0027e0a8..571d3ac403 100644 --- a/interface/src/ui/BaseLogDialog.cpp +++ b/interface/src/ui/BaseLogDialog.cpp @@ -28,17 +28,23 @@ const int SEARCH_BUTTON_LEFT = 25; const int SEARCH_BUTTON_WIDTH = 20; const int SEARCH_TOGGLE_BUTTON_WIDTH = 50; const int SEARCH_TEXT_WIDTH = 240; +const int TIME_STAMP_LENGTH = 16; +const int FONT_WEIGHT = 75; const QColor HIGHLIGHT_COLOR = QColor("#3366CC"); +const QColor BOLD_COLOR = QColor("#445c8c"); +const QString BOLD_PATTERN = "\\[\\d*\\/.*:\\d*:\\d*\\]"; -class KeywordHighlighter : public QSyntaxHighlighter { +class Highlighter : public QSyntaxHighlighter { public: - KeywordHighlighter(QTextDocument* parent = nullptr); + Highlighter(QTextDocument* parent = nullptr); + void setBold(int indexToBold); QString keyword; protected: void highlightBlock(const QString& text) override; private: + QTextCharFormat boldFormat; QTextCharFormat keywordFormat; }; @@ -89,7 +95,7 @@ void BaseLogDialog::initControls() { _leftPad += SEARCH_TOGGLE_BUTTON_WIDTH + BUTTON_MARGIN; _searchPrevButton->show(); connect(_searchPrevButton, SIGNAL(clicked()), SLOT(toggleSearchPrev())); - + _searchNextButton = new QPushButton(this); _searchNextButton->setObjectName("searchNextButton"); _searchNextButton->setGeometry(_leftPad, ELEMENT_MARGIN, SEARCH_TOGGLE_BUTTON_WIDTH, ELEMENT_HEIGHT); @@ -101,9 +107,8 @@ void BaseLogDialog::initControls() { _logTextBox = new QPlainTextEdit(this); _logTextBox->setReadOnly(true); _logTextBox->show(); - _highlighter = new KeywordHighlighter(_logTextBox->document()); + _highlighter = new Highlighter(_logTextBox->document()); connect(_logTextBox, SIGNAL(selectionChanged()), SLOT(updateSelection())); - } void BaseLogDialog::showEvent(QShowEvent* event) { @@ -116,7 +121,9 @@ void BaseLogDialog::resizeEvent(QResizeEvent* event) { void BaseLogDialog::appendLogLine(QString logLine) { if (logLine.contains(_searchTerm, Qt::CaseInsensitive)) { + int indexToBold = _logTextBox->document()->characterCount(); _logTextBox->appendPlainText(logLine.trimmed()); + _highlighter->setBold(indexToBold); } } @@ -128,7 +135,7 @@ void BaseLogDialog::handleSearchTextChanged(QString searchText) { if (searchText.isEmpty()) { return; } - + QTextCursor cursor = _logTextBox->textCursor(); if (cursor.hasSelection()) { QString selectedTerm = cursor.selectedText(); @@ -136,16 +143,16 @@ void BaseLogDialog::handleSearchTextChanged(QString searchText) { return; } } - + cursor.setPosition(0, QTextCursor::MoveAnchor); _logTextBox->setTextCursor(cursor); bool foundTerm = _logTextBox->find(searchText); - + if (!foundTerm) { cursor.movePosition(QTextCursor::End, QTextCursor::MoveAnchor); _logTextBox->setTextCursor(cursor); } - + _searchTerm = searchText; _highlighter->keyword = searchText; _highlighter->rehighlight(); @@ -175,6 +182,7 @@ void BaseLogDialog::showLogData() { _logTextBox->clear(); _logTextBox->appendPlainText(getCurrentLog()); _logTextBox->ensureCursorVisible(); + _highlighter->rehighlight(); } void BaseLogDialog::updateSelection() { @@ -187,16 +195,28 @@ void BaseLogDialog::updateSelection() { } } -KeywordHighlighter::KeywordHighlighter(QTextDocument* parent) : QSyntaxHighlighter(parent) { +Highlighter::Highlighter(QTextDocument* parent) : QSyntaxHighlighter(parent) { + boldFormat.setFontWeight(FONT_WEIGHT); + boldFormat.setForeground(BOLD_COLOR); keywordFormat.setForeground(HIGHLIGHT_COLOR); } -void KeywordHighlighter::highlightBlock(const QString& text) { +void Highlighter::highlightBlock(const QString& text) { + QRegExp expression(BOLD_PATTERN); + + int index = text.indexOf(expression, 0); + + while (index >= 0) { + int length = expression.matchedLength(); + setFormat(index, length, boldFormat); + index = text.indexOf(expression, index + length); + } + if (keyword.isNull() || keyword.isEmpty()) { return; } - int index = text.indexOf(keyword, 0, Qt::CaseInsensitive); + index = text.indexOf(keyword, 0, Qt::CaseInsensitive); int length = keyword.length(); while (index >= 0) { @@ -204,3 +224,7 @@ void KeywordHighlighter::highlightBlock(const QString& text) { index = text.indexOf(keyword, index + length, Qt::CaseInsensitive); } } + +void Highlighter::setBold(int indexToBold) { + setFormat(indexToBold, TIME_STAMP_LENGTH, boldFormat); +} diff --git a/interface/src/ui/BaseLogDialog.h b/interface/src/ui/BaseLogDialog.h index d097010bae..e18d23937f 100644 --- a/interface/src/ui/BaseLogDialog.h +++ b/interface/src/ui/BaseLogDialog.h @@ -23,7 +23,7 @@ const int BUTTON_MARGIN = 8; class QPushButton; class QLineEdit; class QPlainTextEdit; -class KeywordHighlighter; +class Highlighter; class BaseLogDialog : public QDialog { Q_OBJECT @@ -56,7 +56,7 @@ private: QPushButton* _searchPrevButton { nullptr }; QPushButton* _searchNextButton { nullptr }; QString _searchTerm; - KeywordHighlighter* _highlighter { nullptr }; + Highlighter* _highlighter { nullptr }; void initControls(); void showLogData(); diff --git a/interface/src/ui/CachesSizeDialog.cpp b/interface/src/ui/CachesSizeDialog.cpp deleted file mode 100644 index 935a6d126e..0000000000 --- a/interface/src/ui/CachesSizeDialog.cpp +++ /dev/null @@ -1,84 +0,0 @@ -// -// CachesSizeDialog.cpp -// -// -// Created by Clement on 1/12/15. -// Copyright 2015 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -#include -#include -#include - -#include -#include -#include -#include -#include - -#include "CachesSizeDialog.h" - - -QDoubleSpinBox* createDoubleSpinBox(QWidget* parent) { - QDoubleSpinBox* box = new QDoubleSpinBox(parent); - box->setDecimals(0); - box->setRange(MIN_UNUSED_MAX_SIZE / BYTES_PER_MEGABYTES, MAX_UNUSED_MAX_SIZE / BYTES_PER_MEGABYTES); - - return box; -} - -CachesSizeDialog::CachesSizeDialog(QWidget* parent) : - QDialog(parent, Qt::Window | Qt::WindowCloseButtonHint) -{ - setWindowTitle("Caches Size"); - - // Create layouter - QFormLayout* form = new QFormLayout(this); - setLayout(form); - - form->addRow("Animations cache size (MB):", _animations = createDoubleSpinBox(this)); - form->addRow("Geometries cache size (MB):", _geometries = createDoubleSpinBox(this)); - form->addRow("Sounds cache size (MB):", _sounds = createDoubleSpinBox(this)); - form->addRow("Textures cache size (MB):", _textures = createDoubleSpinBox(this)); - - resetClicked(true); - - // Add a button to reset - QPushButton* confirmButton = new QPushButton("Confirm", this); - QPushButton* resetButton = new QPushButton("Reset", this); - form->addRow(confirmButton, resetButton); - connect(confirmButton, SIGNAL(clicked(bool)), this, SLOT(confirmClicked(bool))); - connect(resetButton, SIGNAL(clicked(bool)), this, SLOT(resetClicked(bool))); -} - -void CachesSizeDialog::confirmClicked(bool checked) { - DependencyManager::get()->setUnusedResourceCacheSize(_animations->value() * BYTES_PER_MEGABYTES); - DependencyManager::get()->setUnusedResourceCacheSize(_geometries->value() * BYTES_PER_MEGABYTES); - DependencyManager::get()->setUnusedResourceCacheSize(_sounds->value() * BYTES_PER_MEGABYTES); - // Disabling the texture cache because it's a liability in cases where we're overcommiting GPU memory -#if 0 - DependencyManager::get()->setUnusedResourceCacheSize(_textures->value() * BYTES_PER_MEGABYTES); -#endif - - QDialog::close(); -} - -void CachesSizeDialog::resetClicked(bool checked) { - _animations->setValue(DependencyManager::get()->getUnusedResourceCacheSize() / BYTES_PER_MEGABYTES); - _geometries->setValue(DependencyManager::get()->getUnusedResourceCacheSize() / BYTES_PER_MEGABYTES); - _sounds->setValue(DependencyManager::get()->getUnusedResourceCacheSize() / BYTES_PER_MEGABYTES); - _textures->setValue(DependencyManager::get()->getUnusedResourceCacheSize() / BYTES_PER_MEGABYTES); -} - -void CachesSizeDialog::reject() { - // Just regularly close upon ESC - QDialog::close(); -} - -void CachesSizeDialog::closeEvent(QCloseEvent* event) { - QDialog::closeEvent(event); - emit closed(); -} diff --git a/interface/src/ui/CachesSizeDialog.h b/interface/src/ui/CachesSizeDialog.h deleted file mode 100644 index 025d0f2bac..0000000000 --- a/interface/src/ui/CachesSizeDialog.h +++ /dev/null @@ -1,45 +0,0 @@ -// -// CachesSizeDialog.h -// -// -// Created by Clement on 1/12/15. -// Copyright 2015 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -#ifndef hifi_CachesSizeDialog_h -#define hifi_CachesSizeDialog_h - -#include - -class QDoubleSpinBox; - -class CachesSizeDialog : public QDialog { - Q_OBJECT -public: - // Sets up the UI - CachesSizeDialog(QWidget* parent); - -signals: - void closed(); - -public slots: - void reject() override; - void confirmClicked(bool checked); - void resetClicked(bool checked); - -protected: - // Emits a 'closed' signal when this dialog is closed. - void closeEvent(QCloseEvent* event) override; - -private: - QDoubleSpinBox* _animations = nullptr; - QDoubleSpinBox* _geometries = nullptr; - QDoubleSpinBox* _scripts = nullptr; - QDoubleSpinBox* _sounds = nullptr; - QDoubleSpinBox* _textures = nullptr; -}; - -#endif // hifi_CachesSizeDialog_h diff --git a/interface/src/ui/DialogsManager.cpp b/interface/src/ui/DialogsManager.cpp index 03c71d8573..a95ac8d91f 100644 --- a/interface/src/ui/DialogsManager.cpp +++ b/interface/src/ui/DialogsManager.cpp @@ -19,19 +19,19 @@ #include #include "AddressBarDialog.h" -#include "BandwidthDialog.h" -#include "CachesSizeDialog.h" #include "ConnectionFailureDialog.h" -#include "DiskCacheEditor.h" #include "DomainConnectionDialog.h" #include "HMDToolsDialog.h" #include "LodToolsDialog.h" #include "LoginDialog.h" #include "OctreeStatsDialog.h" #include "PreferencesDialog.h" -#include "ScriptEditorWindow.h" #include "UpdateDialog.h" +#include "TabletScriptingInterface.h" +#include "scripting/HMDScriptingInterface.h" + +static const QVariant TABLET_ADDRESS_DIALOG = "TabletAddressDialog.qml"; template void DialogsManager::maybeCreateDialog(QPointer& member) { if (!member) { @@ -47,12 +47,48 @@ void DialogsManager::maybeCreateDialog(QPointer& member) { } void DialogsManager::toggleAddressBar() { - AddressBarDialog::toggle(); - emit addressBarToggled(); + auto hmd = DependencyManager::get(); + auto tabletScriptingInterface = DependencyManager::get(); + auto tablet = dynamic_cast(tabletScriptingInterface->getTablet("com.highfidelity.interface.tablet.system")); + if (tablet->getToolbarMode()) { + if (tablet->isPathLoaded(TABLET_ADDRESS_DIALOG)) { + tablet->gotoHomeScreen(); + emit addressBarToggled(); + } else { + tablet->loadQMLSource(TABLET_ADDRESS_DIALOG); + emit addressBarToggled(); + } + } else { + if (hmd->getShouldShowTablet()) { + if (tablet->isPathLoaded(TABLET_ADDRESS_DIALOG) && _closeAddressBar) { + tablet->gotoHomeScreen(); + hmd->closeTablet(); + _closeAddressBar = false; + emit addressBarToggled(); + } else { + tablet->loadQMLSource(TABLET_ADDRESS_DIALOG); + _closeAddressBar = true; + emit addressBarToggled(); + } + } else { + tablet->loadQMLSource(TABLET_ADDRESS_DIALOG); + hmd->openTablet(); + _closeAddressBar = true; + emit addressBarToggled(); + } + + } } void DialogsManager::showAddressBar() { - AddressBarDialog::show(); + auto hmd = DependencyManager::get(); + auto tabletScriptingInterface = DependencyManager::get(); + auto tablet = dynamic_cast(tabletScriptingInterface->getTablet("com.highfidelity.interface.tablet.system")); + tablet->loadQMLSource(TABLET_ADDRESS_DIALOG); + + if (!hmd->getShouldShowTablet()) { + hmd->openTablet(); + } } void DialogsManager::showFeed() { @@ -61,16 +97,26 @@ void DialogsManager::showFeed() { } void DialogsManager::setDomainConnectionFailureVisibility(bool visible) { - if (visible) { - ConnectionFailureDialog::show(); - } else { - ConnectionFailureDialog::hide(); - } -} + qDebug() << "DialogsManager::setDomainConnectionFailureVisibility: visible" << visible; + auto tabletScriptingInterface = DependencyManager::get(); + auto tablet = dynamic_cast(tabletScriptingInterface->getTablet("com.highfidelity.interface.tablet.system")); -void DialogsManager::toggleDiskCacheEditor() { - maybeCreateDialog(_diskCacheEditor); - _diskCacheEditor->toggle(); + if (tablet->getToolbarMode()) { + if (visible) { + ConnectionFailureDialog::show(); + } else { + ConnectionFailureDialog::hide(); + } + } else { + static const QUrl url("../../dialogs/TabletConnectionFailureDialog.qml"); + auto hmd = DependencyManager::get(); + if (visible) { + tablet->initialScreen(url); + if (!hmd->getShouldShowTablet()) { + hmd->openTablet(); + } + } + } } void DialogsManager::toggleLoginDialog() { @@ -78,7 +124,7 @@ void DialogsManager::toggleLoginDialog() { } void DialogsManager::showLoginDialog() { - LoginDialog::show(); + LoginDialog::showWithSelection(); } void DialogsManager::showUpdateDialog() { @@ -98,30 +144,6 @@ void DialogsManager::octreeStatsDetails() { _octreeStatsDialog->raise(); } -void DialogsManager::cachesSizeDialog() { - if (!_cachesSizeDialog) { - maybeCreateDialog(_cachesSizeDialog); - - connect(_cachesSizeDialog, SIGNAL(closed()), _cachesSizeDialog, SLOT(deleteLater())); - _cachesSizeDialog->show(); - } - _cachesSizeDialog->raise(); -} - -void DialogsManager::bandwidthDetails() { - if (! _bandwidthDialog) { - _bandwidthDialog = new BandwidthDialog(qApp->getWindow()); - connect(_bandwidthDialog, SIGNAL(closed()), _bandwidthDialog, SLOT(deleteLater())); - - if (_hmdToolsDialog) { - _hmdToolsDialog->watchWindow(_bandwidthDialog->windowHandle()); - } - - _bandwidthDialog->show(); - } - _bandwidthDialog->raise(); -} - void DialogsManager::lodTools() { if (!_lodToolsDialog) { maybeCreateDialog(_lodToolsDialog); @@ -152,12 +174,6 @@ void DialogsManager::hmdToolsClosed() { } } -void DialogsManager::showScriptEditor() { - maybeCreateDialog(_scriptEditor); - _scriptEditor->show(); - _scriptEditor->raise(); -} - void DialogsManager::showTestingResults() { if (!_testingDialog) { _testingDialog = new TestingDialog(qApp->getWindow()); diff --git a/interface/src/ui/DialogsManager.h b/interface/src/ui/DialogsManager.h index c02c1fc2c3..24b9078baf 100644 --- a/interface/src/ui/DialogsManager.h +++ b/interface/src/ui/DialogsManager.h @@ -21,9 +21,7 @@ class AnimationsDialog; class AttachmentsDialog; -class BandwidthDialog; class CachesSizeDialog; -class DiskCacheEditor; class LodToolsDialog; class OctreeStatsDialog; class ScriptEditorWindow; @@ -36,7 +34,6 @@ class DialogsManager : public QObject, public Dependency { SINGLETON_DEPENDENCY public: - QPointer getBandwidthDialog() const { return _bandwidthDialog; } QPointer getHMDToolsDialog() const { return _hmdToolsDialog; } QPointer getLodToolsDialog() const { return _lodToolsDialog; } QPointer getOctreeStatsDialog() const { return _octreeStatsDialog; } @@ -48,15 +45,11 @@ public slots: void showAddressBar(); void showFeed(); void setDomainConnectionFailureVisibility(bool visible); - void toggleDiskCacheEditor(); void toggleLoginDialog(); void showLoginDialog(); void octreeStatsDetails(); - void cachesSizeDialog(); - void bandwidthDetails(); void lodTools(); void hmdTools(bool showTools); - void showScriptEditor(); void showDomainConnectionDialog(); void showTestingResults(); @@ -79,16 +72,14 @@ private: QPointer _animationsDialog; QPointer _attachmentsDialog; - QPointer _bandwidthDialog; QPointer _cachesSizeDialog; - QPointer _diskCacheEditor; QPointer _ircInfoBox; QPointer _hmdToolsDialog; QPointer _lodToolsDialog; QPointer _octreeStatsDialog; - QPointer _scriptEditor; QPointer _testingDialog; QPointer _domainConnectionDialog; + bool _closeAddressBar { false }; }; #endif // hifi_DialogsManager_h diff --git a/interface/src/ui/DiskCacheEditor.cpp b/interface/src/ui/DiskCacheEditor.cpp deleted file mode 100644 index 1a7be8642b..0000000000 --- a/interface/src/ui/DiskCacheEditor.cpp +++ /dev/null @@ -1,146 +0,0 @@ -// -// DiskCacheEditor.cpp -// -// -// Created by Clement on 3/4/15. -// Copyright 2015 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -#include "DiskCacheEditor.h" - -#include -#include -#include -#include -#include -#include -#include - -#include - -#include "OffscreenUi.h" - -DiskCacheEditor::DiskCacheEditor(QWidget* parent) : QObject(parent) { -} - -QWindow* DiskCacheEditor::windowHandle() { - return (_dialog) ? _dialog->windowHandle() : nullptr; -} - -void DiskCacheEditor::toggle() { - if (!_dialog) { - makeDialog(); - } - - if (!_dialog->isActiveWindow()) { - _dialog->show(); - _dialog->raise(); - _dialog->activateWindow(); - } else { - _dialog->close(); - } -} - -void DiskCacheEditor::makeDialog() { - _dialog = new QDialog(static_cast(parent())); - Q_CHECK_PTR(_dialog); - _dialog->setAttribute(Qt::WA_DeleteOnClose); - _dialog->setWindowTitle("Disk Cache Editor"); - - QGridLayout* layout = new QGridLayout(_dialog); - Q_CHECK_PTR(layout); - _dialog->setLayout(layout); - - - QLabel* path = new QLabel("Path : ", _dialog); - Q_CHECK_PTR(path); - path->setAlignment(Qt::AlignRight); - layout->addWidget(path, 0, 0); - - QLabel* size = new QLabel("Current Size : ", _dialog); - Q_CHECK_PTR(size); - size->setAlignment(Qt::AlignRight); - layout->addWidget(size, 1, 0); - - QLabel* maxSize = new QLabel("Max Size : ", _dialog); - Q_CHECK_PTR(maxSize); - maxSize->setAlignment(Qt::AlignRight); - layout->addWidget(maxSize, 2, 0); - - - _path = new QLabel(_dialog); - Q_CHECK_PTR(_path); - _path->setAlignment(Qt::AlignLeft); - layout->addWidget(_path, 0, 1, 1, 3); - - _size = new QLabel(_dialog); - Q_CHECK_PTR(_size); - _size->setAlignment(Qt::AlignLeft); - layout->addWidget(_size, 1, 1, 1, 3); - - _maxSize = new QLabel(_dialog); - Q_CHECK_PTR(_maxSize); - _maxSize->setAlignment(Qt::AlignLeft); - layout->addWidget(_maxSize, 2, 1, 1, 3); - - refresh(); - - - static const int REFRESH_INTERVAL = 100; // msec - _refreshTimer = new QTimer(_dialog); - _refreshTimer->setInterval(REFRESH_INTERVAL); // Qt::CoarseTimer acceptable, no need for real time accuracy - _refreshTimer->setSingleShot(false); - QObject::connect(_refreshTimer.data(), &QTimer::timeout, this, &DiskCacheEditor::refresh); - _refreshTimer->start(); - - QPushButton* clearCacheButton = new QPushButton(_dialog); - Q_CHECK_PTR(clearCacheButton); - clearCacheButton->setText("Clear"); - clearCacheButton->setToolTip("Erases the entire content of the disk cache."); - connect(clearCacheButton, SIGNAL(clicked()), SLOT(clear())); - layout->addWidget(clearCacheButton, 3, 3); -} - -void DiskCacheEditor::refresh() { - DependencyManager::get()->cacheInfoRequest(this, "cacheInfoCallback"); -} - -void DiskCacheEditor::cacheInfoCallback(QString cacheDirectory, qint64 cacheSize, qint64 maximumCacheSize) { - static const auto stringify = [](qint64 number) { - static const QStringList UNITS = QStringList() << "B" << "KB" << "MB" << "GB"; - static const qint64 CHUNK = 1024; - QString unit; - int i = 0; - for (i = 0; i < 4; ++i) { - if (number / CHUNK > 0) { - number /= CHUNK; - } else { - break; - } - } - return QString("%0 %1").arg(number).arg(UNITS[i]); - }; - - if (_path) { - _path->setText(cacheDirectory); - } - if (_size) { - _size->setText(stringify(cacheSize)); - } - if (_maxSize) { - _maxSize->setText(stringify(maximumCacheSize)); - } -} - -void DiskCacheEditor::clear() { - auto buttonClicked = OffscreenUi::question(_dialog, "Clearing disk cache", - "You are about to erase all the content of the disk cache, " - "are you sure you want to do that?", - QMessageBox::Ok | QMessageBox::Cancel); - if (buttonClicked == QMessageBox::Ok) { - DependencyManager::get()->clearCache(); - } -} diff --git a/interface/src/ui/DiskCacheEditor.h b/interface/src/ui/DiskCacheEditor.h deleted file mode 100644 index 3f8fa1a883..0000000000 --- a/interface/src/ui/DiskCacheEditor.h +++ /dev/null @@ -1,49 +0,0 @@ -// -// DiskCacheEditor.h -// -// -// Created by Clement on 3/4/15. -// Copyright 2015 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -#ifndef hifi_DiskCacheEditor_h -#define hifi_DiskCacheEditor_h - -#include -#include - -class QDialog; -class QLabel; -class QWindow; -class QTimer; - -class DiskCacheEditor : public QObject { - Q_OBJECT - -public: - DiskCacheEditor(QWidget* parent = nullptr); - - QWindow* windowHandle(); - -public slots: - void toggle(); - -private slots: - void refresh(); - void cacheInfoCallback(QString cacheDirectory, qint64 cacheSize, qint64 maximumCacheSize); - void clear(); - -private: - void makeDialog(); - - QPointer _dialog; - QPointer _path; - QPointer _size; - QPointer _maxSize; - QPointer _refreshTimer; -}; - -#endif // hifi_DiskCacheEditor_h \ No newline at end of file diff --git a/interface/src/ui/DomainConnectionModel.cpp b/interface/src/ui/DomainConnectionModel.cpp new file mode 100644 index 0000000000..b9e4c1348e --- /dev/null +++ b/interface/src/ui/DomainConnectionModel.cpp @@ -0,0 +1,101 @@ +// +// DomainConnectionModel.cpp +// +// Created by Vlad Stelmahovsky +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include "DomainConnectionModel.h" +#include + +#include +#include + +Q_LOGGING_CATEGORY(dcmodel, "hifi.dcmodel") + +DomainConnectionModel::DomainConnectionModel(QAbstractItemModel* parent) : + QAbstractItemModel(parent) +{} + +DomainConnectionModel::~DomainConnectionModel() { +} + +QVariant DomainConnectionModel::data(const QModelIndex& index, int role) const { + //sanity + const QMap × = + DependencyManager::get()->getLastConnectionTimes(); + + if (!index.isValid() || index.row() >= times.size()) + return QVariant(); + + // setup our data with the values from the NodeList + quint64 firstStepTime = times.firstKey() / USECS_PER_MSEC; + quint64 timestamp = times.keys().at(index.row()); + + quint64 stepTime = timestamp / USECS_PER_MSEC; + quint64 delta = 0;//(stepTime - lastStepTime); + quint64 elapsed = 0;//stepTime - firstStepTime; + + if (index.row() > 0) { + quint64 prevstepTime = times.keys().at(index.row() - 1) / USECS_PER_MSEC; + delta = (stepTime - prevstepTime); + elapsed = stepTime - firstStepTime; + } + + if (role == Qt::DisplayRole || role == DisplayNameRole) { + const QMetaObject &nodeListMeta = NodeList::staticMetaObject; + QMetaEnum stepEnum = nodeListMeta.enumerator(nodeListMeta.indexOfEnumerator("ConnectionStep")); + int stepIndex = (int) times.value(timestamp); + return stepEnum.valueToKey(stepIndex); + } else if (role == DeltaRole) { + return delta; + } else if (role == TimestampRole) { + return stepTime; + } else if (role == TimeElapsedRole) { + return elapsed; + } + return QVariant(); +} + +int DomainConnectionModel::rowCount(const QModelIndex& parent) const { + Q_UNUSED(parent) + const QMap × = + DependencyManager::get()->getLastConnectionTimes(); + return times.size(); +} + +QHash DomainConnectionModel::roleNames() const { + QHash roles; + roles.insert(DisplayNameRole, "name"); + roles.insert(TimestampRole, "timestamp"); + roles.insert(DeltaRole, "delta"); + roles.insert(TimeElapsedRole, "timeelapsed"); + return roles; +} + +QModelIndex DomainConnectionModel::index(int row, int column, const QModelIndex &parent) const +{ + Q_UNUSED(parent) + return createIndex(row, column); +} + +QModelIndex DomainConnectionModel::parent(const QModelIndex &child) const +{ + Q_UNUSED(child) + return QModelIndex(); +} + +int DomainConnectionModel::columnCount(const QModelIndex &parent) const +{ + Q_UNUSED(parent) + return 1; +} + +void DomainConnectionModel::refresh() { + //inform view that we want refresh data + beginResetModel(); + endResetModel(); +} \ No newline at end of file diff --git a/interface/src/ui/DomainConnectionModel.h b/interface/src/ui/DomainConnectionModel.h new file mode 100644 index 0000000000..11ecb23bd2 --- /dev/null +++ b/interface/src/ui/DomainConnectionModel.h @@ -0,0 +1,47 @@ +// +// DomainConnectionModel.h +// +// Created by Vlad Stelmahovsky +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// +#pragma once + +#ifndef hifi_DomainConnectionModel_h +#define hifi_DomainConnectionModel_h + +#include +#include + +class DomainConnectionModel : public QAbstractItemModel, public Dependency { + Q_OBJECT + SINGLETON_DEPENDENCY +public: + DomainConnectionModel(QAbstractItemModel* parent = nullptr); + ~DomainConnectionModel(); + QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; + int rowCount(const QModelIndex& parent = QModelIndex()) const override; + QHash roleNames() const override; + + QModelIndex index(int row, int column, const QModelIndex& parent) const override; + QModelIndex parent(const QModelIndex& child) const override; + int columnCount(const QModelIndex& parent = QModelIndex()) const override; + + enum Roles { + DisplayNameRole = Qt::UserRole, + TimestampRole, + DeltaRole, + TimeElapsedRole + }; + +public slots: + void refresh(); + +protected: + +private: +}; + +#endif // hifi_DomainConnectionModel_h diff --git a/interface/src/ui/HMDToolsDialog.cpp b/interface/src/ui/HMDToolsDialog.cpp index a596403948..55c321723e 100644 --- a/interface/src/ui/HMDToolsDialog.cpp +++ b/interface/src/ui/HMDToolsDialog.cpp @@ -79,9 +79,6 @@ HMDToolsDialog::HMDToolsDialog(QWidget* parent) : // what screens we're allowed on watchWindow(windowHandle()); auto dialogsManager = DependencyManager::get(); - if (dialogsManager->getBandwidthDialog()) { - watchWindow(dialogsManager->getBandwidthDialog()->windowHandle()); - } if (dialogsManager->getOctreeStatsDialog()) { watchWindow(dialogsManager->getOctreeStatsDialog()->windowHandle()); } diff --git a/interface/src/ui/JSConsole.cpp b/interface/src/ui/JSConsole.cpp index 5d197f5ddc..7700874d9a 100644 --- a/interface/src/ui/JSConsole.cpp +++ b/interface/src/ui/JSConsole.cpp @@ -76,8 +76,8 @@ void JSConsole::setScriptEngine(ScriptEngine* scriptEngine) { return; } if (_scriptEngine != NULL) { - disconnect(_scriptEngine, SIGNAL(printedMessage(const QString&)), this, SLOT(handlePrint(const QString&))); - disconnect(_scriptEngine, SIGNAL(errorMessage(const QString&)), this, SLOT(handleError(const QString&))); + disconnect(_scriptEngine, &ScriptEngine::printedMessage, this, &JSConsole::handlePrint); + disconnect(_scriptEngine, &ScriptEngine::errorMessage, this, &JSConsole::handleError); if (_ownScriptEngine) { _scriptEngine->deleteLater(); } @@ -87,8 +87,8 @@ void JSConsole::setScriptEngine(ScriptEngine* scriptEngine) { _ownScriptEngine = scriptEngine == NULL; _scriptEngine = _ownScriptEngine ? DependencyManager::get()->loadScript(QString(), false) : scriptEngine; - connect(_scriptEngine, SIGNAL(printedMessage(const QString&)), this, SLOT(handlePrint(const QString&))); - connect(_scriptEngine, SIGNAL(errorMessage(const QString&)), this, SLOT(handleError(const QString&))); + connect(_scriptEngine, &ScriptEngine::printedMessage, this, &JSConsole::handlePrint); + connect(_scriptEngine, &ScriptEngine::errorMessage, this, &JSConsole::handleError); } void JSConsole::executeCommand(const QString& command) { @@ -134,11 +134,13 @@ void JSConsole::commandFinished() { resetCurrentCommandHistory(); } -void JSConsole::handleError(const QString& message) { +void JSConsole::handleError(const QString& scriptName, const QString& message) { + Q_UNUSED(scriptName); appendMessage(GUTTER_ERROR, "" + message.toHtmlEscaped() + ""); } -void JSConsole::handlePrint(const QString& message) { +void JSConsole::handlePrint(const QString& scriptName, const QString& message) { + Q_UNUSED(scriptName); appendMessage("", message); } diff --git a/interface/src/ui/JSConsole.h b/interface/src/ui/JSConsole.h index 47878fea99..d5f5aff301 100644 --- a/interface/src/ui/JSConsole.h +++ b/interface/src/ui/JSConsole.h @@ -47,8 +47,8 @@ protected: protected slots: void scrollToBottom(); void resizeTextInput(); - void handlePrint(const QString& message); - void handleError(const QString& message); + void handlePrint(const QString& scriptName, const QString& message); + void handleError(const QString& scriptName, const QString& message); void commandFinished(); private: diff --git a/interface/src/ui/LoginDialog.cpp b/interface/src/ui/LoginDialog.cpp index e333bb1b88..10783afd23 100644 --- a/interface/src/ui/LoginDialog.cpp +++ b/interface/src/ui/LoginDialog.cpp @@ -24,6 +24,10 @@ #include "DependencyManager.h" #include "Menu.h" +#include "Application.h" +#include "TabletScriptingInterface.h" +#include "scripting/HMDScriptingInterface.h" + HIFI_QML_DEF(LoginDialog) LoginDialog::LoginDialog(QQuickItem *parent) : OffscreenQmlDialog(parent) { @@ -31,7 +35,24 @@ LoginDialog::LoginDialog(QQuickItem *parent) : OffscreenQmlDialog(parent) { connect(accountManager.data(), &AccountManager::loginComplete, this, &LoginDialog::handleLoginCompleted); connect(accountManager.data(), &AccountManager::loginFailed, - this, &LoginDialog::handleLoginFailed); + this, &LoginDialog::handleLoginFailed); +} + +void LoginDialog::showWithSelection() +{ + auto tabletScriptingInterface = DependencyManager::get(); + auto tablet = dynamic_cast(tabletScriptingInterface->getTablet("com.highfidelity.interface.tablet.system")); + auto hmd = DependencyManager::get(); + + if (tablet->getToolbarMode()) { + LoginDialog::show(); + } else { + static const QUrl url("../../dialogs/TabletLoginDialog.qml"); + tablet->initialScreen(url); + if (!hmd->getShouldShowTablet()) { + hmd->openTablet(); + } + } } void LoginDialog::toggleAction() { @@ -51,7 +72,7 @@ void LoginDialog::toggleAction() { // change the menu item to login loginAction->setText("Login / Sign Up"); connection = connect(loginAction, &QAction::triggered, [] { - LoginDialog::show(); + LoginDialog::showWithSelection(); }); } } @@ -141,9 +162,23 @@ void LoginDialog::createAccountFromStream(QString username) { } void LoginDialog::openUrl(const QString& url) const { + + auto tabletScriptingInterface = DependencyManager::get(); + auto tablet = dynamic_cast(tabletScriptingInterface->getTablet("com.highfidelity.interface.tablet.system")); + auto hmd = DependencyManager::get(); auto offscreenUi = DependencyManager::get(); - auto browser = offscreenUi->load("Browser.qml"); - browser->setProperty("url", url); + + if (tablet->getToolbarMode()) { + auto browser = offscreenUi->load("Browser.qml"); + browser->setProperty("url", url); + } else { + if (!hmd->getShouldShowTablet() && !qApp->isHMDMode()) { + auto browser = offscreenUi->load("Browser.qml"); + browser->setProperty("url", url); + } else { + tablet->gotoWebScreen(url); + } + } } void LoginDialog::linkCompleted(QNetworkReply& reply) { diff --git a/interface/src/ui/LoginDialog.h b/interface/src/ui/LoginDialog.h index ce6075793b..5ebf866fbd 100644 --- a/interface/src/ui/LoginDialog.h +++ b/interface/src/ui/LoginDialog.h @@ -27,6 +27,7 @@ public: LoginDialog(QQuickItem* parent = nullptr); + static void showWithSelection(); signals: void handleLoginCompleted(); void handleLoginFailed(); diff --git a/interface/src/ui/OctreeStatsProvider.cpp b/interface/src/ui/OctreeStatsProvider.cpp new file mode 100644 index 0000000000..5f40b9916d --- /dev/null +++ b/interface/src/ui/OctreeStatsProvider.cpp @@ -0,0 +1,377 @@ +// +// OctreeStatsProvider.cpp +// interface/src/ui +// +// Created by Vlad Stelmahovsky on 3/12/17. +// Copyright 2013 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include "Application.h" + +#include "../octree/OctreePacketProcessor.h" +#include "ui/OctreeStatsProvider.h" + +OctreeStatsProvider::OctreeStatsProvider(QObject* parent, NodeToOctreeSceneStats* model) : + QObject(parent), + _model(model) + , _statCount(0) + , _averageUpdatesPerSecond(SAMPLES_PER_SECOND) +{ + //schedule updates + connect(&_updateTimer, &QTimer::timeout, this, &OctreeStatsProvider::updateOctreeStatsData); + _updateTimer.setInterval(100); + //timer will be rescheduled on each new timeout + _updateTimer.setSingleShot(true); +} + +/* + * Start updates statistics +*/ +void OctreeStatsProvider::startUpdates() { + _updateTimer.start(); +} + +/* + * Stop updates statistics +*/ +void OctreeStatsProvider::stopUpdates() { + _updateTimer.stop(); +} + +QColor OctreeStatsProvider::getColor() const { + static int statIndex = 1; + static quint32 rotatingColors[] = { GREENISH, YELLOWISH, GREYISH }; + quint32 colorRGBA = rotatingColors[statIndex % (sizeof(rotatingColors)/sizeof(rotatingColors[0]))]; + quint32 rgb = colorRGBA >> 8; + const quint32 colorpart1 = 0xfefefeu; + const quint32 colorpart2 = 0xf8f8f8; + rgb = ((rgb & colorpart1) >> 1) + ((rgb & colorpart2) >> 3); + statIndex++; + return QColor::fromRgb(rgb); +} + +OctreeStatsProvider::~OctreeStatsProvider() { + _updateTimer.stop(); +} + +int OctreeStatsProvider::serversNum() const { + return m_serversNum; +} + +void OctreeStatsProvider::updateOctreeStatsData() { + + // Processed Entities Related stats + auto entities = qApp->getEntities(); + auto entitiesTree = entities->getTree(); + + // Do this ever paint event... even if we don't update + auto totalTrackedEdits = entitiesTree->getTotalTrackedEdits(); + + const quint64 SAMPLING_WINDOW = USECS_PER_SECOND / SAMPLES_PER_SECOND; + quint64 now = usecTimestampNow(); + quint64 sinceLastWindow = now - _lastWindowAt; + auto editsInLastWindow = totalTrackedEdits - _lastKnownTrackedEdits; + float sinceLastWindowInSeconds = (float)sinceLastWindow / (float)USECS_PER_SECOND; + float recentUpdatesPerSecond = (float)editsInLastWindow / sinceLastWindowInSeconds; + if (sinceLastWindow > SAMPLING_WINDOW) { + _averageUpdatesPerSecond.updateAverage(recentUpdatesPerSecond); + _lastWindowAt = now; + _lastKnownTrackedEdits = totalTrackedEdits; + } + + // Only refresh our stats every once in a while, unless asked for realtime + quint64 REFRESH_AFTER = Menu::getInstance()->isOptionChecked(MenuOption::ShowRealtimeEntityStats) ? 0 : USECS_PER_SECOND; + quint64 sinceLastRefresh = now - _lastRefresh; + if (sinceLastRefresh < REFRESH_AFTER) { + _updateTimer.start((REFRESH_AFTER - sinceLastRefresh)/1000); + return; + } + // Only refresh our stats every once in a while, unless asked for realtime + //if no realtime, then update once per second. Otherwise consider 60FPS update, ie 16ms interval + //int updateinterval = Menu::getInstance()->isOptionChecked(MenuOption::ShowRealtimeEntityStats) ? 16 : 1000; + _updateTimer.start(REFRESH_AFTER/1000); + + const int FLOATING_POINT_PRECISION = 3; + + m_localElementsMemory = QString("Elements RAM: %1MB").arg(OctreeElement::getTotalMemoryUsage() / 1000000.0f, 5, 'f', 4); + emit localElementsMemoryChanged(m_localElementsMemory); + + // Local Elements + m_localElements = QString("Total: %1 / Internal: %2 / Leaves: %3"). + arg(OctreeElement::getNodeCount()). + arg(OctreeElement::getInternalNodeCount()). + arg(OctreeElement::getLeafNodeCount()); + emit localElementsChanged(m_localElements); + + // iterate all the current octree stats, and list their sending modes, total their octree elements, etc... + int serverCount = 0; + int movingServerCount = 0; + unsigned long totalNodes = 0; + unsigned long totalInternal = 0; + unsigned long totalLeaves = 0; + + m_sendingMode.clear(); + NodeToOctreeSceneStats* sceneStats = qApp->getOcteeSceneStats(); + sceneStats->withReadLock([&] { + for (NodeToOctreeSceneStatsIterator i = sceneStats->begin(); i != sceneStats->end(); i++) { + //const QUuid& uuid = i->first; + OctreeSceneStats& stats = i->second; + serverCount++; + + // calculate server node totals + totalNodes += stats.getTotalElements(); + totalInternal += stats.getTotalInternal(); + totalLeaves += stats.getTotalLeaves(); + + // Sending mode + if (serverCount > 1) { + m_sendingMode += ","; + } + if (stats.isMoving()) { + m_sendingMode += "M"; + movingServerCount++; + } else { + m_sendingMode += "S"; + } + if (stats.isFullScene()) { + m_sendingMode += "F"; + } else { + m_sendingMode += "p"; + } + } + }); + m_sendingMode += QString(" - %1 servers").arg(serverCount); + if (movingServerCount > 0) { + m_sendingMode += " "; + } else { + m_sendingMode += " "; + } + + emit sendingModeChanged(m_sendingMode); + + // Server Elements + m_serverElements = QString("Total: %1 / Internal: %2 / Leaves: %3"). + arg(totalNodes).arg(totalInternal).arg(totalLeaves); + emit serverElementsChanged(m_serverElements); + + + // Processed Packets Elements + auto averageElementsPerPacket = entities->getAverageElementsPerPacket(); + auto averageEntitiesPerPacket = entities->getAverageEntitiesPerPacket(); + + auto averageElementsPerSecond = entities->getAverageElementsPerSecond(); + auto averageEntitiesPerSecond = entities->getAverageEntitiesPerSecond(); + + auto averageWaitLockPerPacket = entities->getAverageWaitLockPerPacket(); + auto averageUncompressPerPacket = entities->getAverageUncompressPerPacket(); + auto averageReadBitstreamPerPacket = entities->getAverageReadBitstreamPerPacket(); + + const OctreePacketProcessor& entitiesPacketProcessor = qApp->getOctreePacketProcessor(); + + auto incomingPacketsDepth = entitiesPacketProcessor.packetsToProcessCount(); + auto incomingPPS = entitiesPacketProcessor.getIncomingPPS(); + auto processedPPS = entitiesPacketProcessor.getProcessedPPS(); + auto treeProcessedPPS = entities->getAveragePacketsPerSecond(); + + m_processedPackets = QString("Queue Size: %1 Packets / Network IN: %2 PPS / Queue OUT: %3 PPS / Tree IN: %4 PPS") + .arg(incomingPacketsDepth) + .arg(incomingPPS, 5, 'f', FLOATING_POINT_PRECISION) + .arg(processedPPS, 5, 'f', FLOATING_POINT_PRECISION) + .arg(treeProcessedPPS, 5, 'f', FLOATING_POINT_PRECISION); + emit processedPacketsChanged(m_processedPackets); + + m_processedPacketsElements = QString("%1 per packet / %2 per second") + .arg(averageElementsPerPacket, 5, 'f', FLOATING_POINT_PRECISION) + .arg(averageElementsPerSecond, 5, 'f', FLOATING_POINT_PRECISION); + emit processedPacketsElementsChanged(m_processedPacketsElements); + + m_processedPacketsEntities = QString("%1 per packet / %2 per second") + .arg(averageEntitiesPerPacket, 5, 'f', FLOATING_POINT_PRECISION) + .arg(averageEntitiesPerSecond, 5, 'f', FLOATING_POINT_PRECISION); + emit processedPacketsEntitiesChanged(m_processedPacketsEntities); + + m_processedPacketsTiming = QString("Lock Wait: %1 (usecs) / Uncompress: %2 (usecs) / Process: %3 (usecs)") + .arg(averageWaitLockPerPacket) + .arg(averageUncompressPerPacket) + .arg(averageReadBitstreamPerPacket); + emit processedPacketsTimingChanged(m_processedPacketsTiming); + + auto entitiesEditPacketSender = qApp->getEntityEditPacketSender(); + auto outboundPacketsDepth = entitiesEditPacketSender->packetsToSendCount(); + auto outboundQueuedPPS = entitiesEditPacketSender->getLifetimePPSQueued(); + auto outboundSentPPS = entitiesEditPacketSender->getLifetimePPS(); + + m_outboundEditPackets = QString("Queue Size: %1 packets / Queued IN: %2 PPS / Sent OUT: %3 PPS") + .arg(outboundPacketsDepth) + .arg(outboundQueuedPPS, 5, 'f', FLOATING_POINT_PRECISION) + .arg(outboundSentPPS, 5, 'f', FLOATING_POINT_PRECISION); + emit outboundEditPacketsChanged(m_outboundEditPackets); + + // Entity Edits update time + auto averageEditDelta = entitiesTree->getAverageEditDeltas(); + auto maxEditDelta = entitiesTree->getMaxEditDelta(); + + m_entityUpdateTime = QString("Average: %1 (usecs) / Max: %2 (usecs)") + .arg(averageEditDelta) + .arg(maxEditDelta); + emit entityUpdateTimeChanged(m_entityUpdateTime); + + // Entity Edits + auto bytesPerEdit = entitiesTree->getAverageEditBytes(); + + auto updatesPerSecond = _averageUpdatesPerSecond.getAverage(); + if (updatesPerSecond < 1) { + updatesPerSecond = 0; // we don't really care about small updates per second so suppress those + } + + m_entityUpdates = QString("%1 updates per second / %2 total updates / Average Size: %3 bytes") + .arg(updatesPerSecond, 5, 'f', FLOATING_POINT_PRECISION) + .arg(totalTrackedEdits) + .arg(bytesPerEdit); + emit entityUpdatesChanged(m_entityUpdates); + + updateOctreeServers(); +} + +void OctreeStatsProvider::updateOctreeServers() { + int serverCount = 0; + + showOctreeServersOfType(serverCount, NodeType::EntityServer, "Entity", + qApp->getEntityServerJurisdictions()); + if (m_serversNum != serverCount) { + m_serversNum = serverCount; + emit serversNumChanged(m_serversNum); + } +} + +void OctreeStatsProvider::showOctreeServersOfType(int& serverCount, NodeType_t serverType, const char* serverTypeName, + NodeToJurisdictionMap& serverJurisdictions) { + + m_servers.clear(); + + auto nodeList = DependencyManager::get(); + nodeList->eachNode([&](const SharedNodePointer& node) { + + // only send to the NodeTypes that are NodeType_t_VOXEL_SERVER + if (node->getType() == serverType) { + serverCount++; + + QString lesserDetails; + QString moreDetails; + QString mostDetails; + + if (node->getActiveSocket()) { + lesserDetails += "active "; + } else { + lesserDetails += "inactive "; + } + + QUuid nodeUUID = node->getUUID(); + + // lookup our nodeUUID in the jurisdiction map, if it's missing then we're + // missing at least one jurisdiction + serverJurisdictions.withReadLock([&] { + if (serverJurisdictions.find(nodeUUID) == serverJurisdictions.end()) { + lesserDetails += " unknown jurisdiction "; + return; + } + const JurisdictionMap& map = serverJurisdictions[nodeUUID]; + + auto rootCode = map.getRootOctalCode(); + + if (rootCode) { + QString rootCodeHex = octalCodeToHexString(rootCode.get()); + + VoxelPositionSize rootDetails; + voxelDetailsForCode(rootCode.get(), rootDetails); + AACube serverBounds(glm::vec3(rootDetails.x, rootDetails.y, rootDetails.z), rootDetails.s); + lesserDetails += QString(" jurisdiction: %1 [%2, %3, %4: %5]") + .arg(rootCodeHex) + .arg(rootDetails.x) + .arg(rootDetails.y) + .arg(rootDetails.z) + .arg(rootDetails.s); + } else { + lesserDetails += " jurisdiction has no rootCode"; + } // root code + }); + + // now lookup stats details for this server... + NodeToOctreeSceneStats* sceneStats = qApp->getOcteeSceneStats(); + sceneStats->withReadLock([&] { + if (sceneStats->find(nodeUUID) != sceneStats->end()) { + OctreeSceneStats& stats = sceneStats->at(nodeUUID); + + float lastFullEncode = stats.getLastFullTotalEncodeTime() / USECS_PER_MSEC; + float lastFullSend = stats.getLastFullElapsedTime() / USECS_PER_MSEC; + float lastFullSendInSeconds = stats.getLastFullElapsedTime() / USECS_PER_SECOND; + float lastFullPackets = stats.getLastFullTotalPackets(); + float lastFullPPS = lastFullPackets; + if (lastFullSendInSeconds > 0) { + lastFullPPS = lastFullPackets / lastFullSendInSeconds; + } + + mostDetails += QString("

Last Full Scene... Encode: %1 ms Send: %2 ms Packets: %3 Bytes: %4 Rate: %5 PPS") + .arg(lastFullEncode) + .arg(lastFullSend) + .arg(lastFullPackets) + .arg(stats.getLastFullTotalBytes()) + .arg(lastFullPPS); + + for (int i = 0; i < OctreeSceneStats::ITEM_COUNT; i++) { + OctreeSceneStats::Item item = (OctreeSceneStats::Item)(i); + OctreeSceneStats::ItemInfo& itemInfo = stats.getItemInfo(item); + mostDetails += QString("
%1 %2") + .arg(itemInfo.caption).arg(stats.getItemValue(item)); + } + + moreDetails += "
Node UUID: " +nodeUUID.toString() + " "; + + moreDetails += QString("
Elements: %1 total %2 internal %3 leaves ") + .arg(stats.getTotalElements()) + .arg(stats.getTotalInternal()) + .arg(stats.getTotalLeaves()); + + const SequenceNumberStats& seqStats = stats.getIncomingOctreeSequenceNumberStats(); + qint64 clockSkewInUsecs = node->getClockSkewUsec(); + qint64 clockSkewInMS = clockSkewInUsecs / (qint64)USECS_PER_MSEC; + + moreDetails += QString("
Incoming Packets: %1/ Lost: %2/ Recovered: %3") + .arg(stats.getIncomingPackets()) + .arg(seqStats.getLost()) + .arg(seqStats.getRecovered()); + + moreDetails += QString("
Out of Order: %1/ Early: %2/ Late: %3/ Unreasonable: %4") + .arg(seqStats.getOutOfOrder()) + .arg(seqStats.getEarly()) + .arg(seqStats.getLate()) + .arg(seqStats.getUnreasonable()); + + moreDetails += QString("
Average Flight Time: %1 msecs") + .arg(stats.getIncomingFlightTimeAverage()); + + moreDetails += QString("
Average Ping Time: %1 msecs") + .arg(node->getPingMs()); + + moreDetails += QString("
Average Clock Skew: %1 msecs [%2]") + .arg(clockSkewInMS) + .arg(formatUsecTime(clockSkewInUsecs)); + + + moreDetails += QString("
Incoming Bytes: %1 Wasted Bytes: %2") + .arg(stats.getIncomingBytes()) + .arg(stats.getIncomingWastedBytes()); + } + }); + m_servers.append(lesserDetails); + m_servers.append(moreDetails); + m_servers.append(mostDetails); + } // is VOXEL_SERVER + }); + emit serversChanged(m_servers); +} + + diff --git a/interface/src/ui/OctreeStatsProvider.h b/interface/src/ui/OctreeStatsProvider.h new file mode 100644 index 0000000000..c919ca102f --- /dev/null +++ b/interface/src/ui/OctreeStatsProvider.h @@ -0,0 +1,154 @@ +// +// OctreeStatsProvider.h +// interface/src/ui +// +// Created by Vlad Stelmahovsky on 3/12/17. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_OctreeStatsProvider_h +#define hifi_OctreeStatsProvider_h + +#include +#include +#include + +#include "DependencyManager.h" + +#define MAX_STATS 100 +#define MAX_VOXEL_SERVERS 50 +#define DEFAULT_COLOR 0 + +class OctreeStatsProvider : public QObject, public Dependency { + Q_OBJECT + SINGLETON_DEPENDENCY + + Q_PROPERTY(int serversNum READ serversNum NOTIFY serversNumChanged) + Q_PROPERTY(QString serverElements READ serverElements NOTIFY serverElementsChanged) + Q_PROPERTY(QString localElements READ localElements NOTIFY localElementsChanged) + Q_PROPERTY(QString localElementsMemory READ localElementsMemory NOTIFY localElementsMemoryChanged) + Q_PROPERTY(QString sendingMode READ sendingMode NOTIFY sendingModeChanged) + Q_PROPERTY(QString processedPackets READ processedPackets NOTIFY processedPacketsChanged) + Q_PROPERTY(QString processedPacketsElements READ processedPacketsElements NOTIFY processedPacketsElementsChanged) + Q_PROPERTY(QString processedPacketsEntities READ processedPacketsEntities NOTIFY processedPacketsEntitiesChanged) + Q_PROPERTY(QString processedPacketsTiming READ processedPacketsTiming NOTIFY processedPacketsTimingChanged) + Q_PROPERTY(QString outboundEditPackets READ outboundEditPackets NOTIFY outboundEditPacketsChanged) + Q_PROPERTY(QString entityUpdateTime READ entityUpdateTime NOTIFY entityUpdateTimeChanged) + Q_PROPERTY(QString entityUpdates READ entityUpdates NOTIFY entityUpdatesChanged) + + Q_PROPERTY(QStringList servers READ servers NOTIFY serversChanged) + +public: + OctreeStatsProvider(QObject* parent, NodeToOctreeSceneStats* model); + ~OctreeStatsProvider(); + + int serversNum() const; + + QString serverElements() const { + return m_serverElements; + } + + QString localElements() const { + return m_localElements; + } + + QString localElementsMemory() const { + return m_localElementsMemory; + } + + QString sendingMode() const { + return m_sendingMode; + } + + QString processedPackets() const { + return m_processedPackets; + } + + QString processedPacketsElements() const { + return m_processedPacketsElements; + } + + QString processedPacketsEntities() const { + return m_processedPacketsEntities; + } + + QString processedPacketsTiming() const { + return m_processedPacketsTiming; + } + + QString outboundEditPackets() const { + return m_outboundEditPackets; + } + + QString entityUpdateTime() const { + return m_entityUpdateTime; + } + + QString entityUpdates() const { + return m_entityUpdates; + } + + QStringList servers() const { + return m_servers; + } + +signals: + + void serversNumChanged(int serversNum); + void serverElementsChanged(const QString &serverElements); + void localElementsChanged(const QString &localElements); + void sendingModeChanged(const QString &sendingMode); + void processedPacketsChanged(const QString &processedPackets); + void localElementsMemoryChanged(const QString &localElementsMemory); + void processedPacketsElementsChanged(const QString &processedPacketsElements); + void processedPacketsEntitiesChanged(const QString &processedPacketsEntities); + void processedPacketsTimingChanged(const QString &processedPacketsTiming); + void outboundEditPacketsChanged(const QString &outboundEditPackets); + void entityUpdateTimeChanged(const QString &entityUpdateTime); + void entityUpdatesChanged(const QString &entityUpdates); + + void serversChanged(const QStringList &servers); + +public slots: + void startUpdates(); + void stopUpdates(); + QColor getColor() const; + +private slots: + void updateOctreeStatsData(); +protected: + void updateOctreeServers(); + void showOctreeServersOfType(int& serverNumber, NodeType_t serverType, + const char* serverTypeName, NodeToJurisdictionMap& serverJurisdictions); + +private: + NodeToOctreeSceneStats* _model; + int _statCount; + + const int SAMPLES_PER_SECOND = 10; + SimpleMovingAverage _averageUpdatesPerSecond; + quint64 _lastWindowAt = usecTimestampNow(); + quint64 _lastKnownTrackedEdits = 0; + + quint64 _lastRefresh = 0; + + QTimer _updateTimer; + int m_serversNum {0}; + QString m_serverElements; + QString m_localElements; + QString m_localElementsMemory; + QString m_sendingMode; + QString m_processedPackets; + QString m_processedPacketsElements; + QString m_processedPacketsEntities; + QString m_processedPacketsTiming; + QString m_outboundEditPackets; + QString m_entityUpdateTime; + QString m_entityUpdates; + QStringList m_servers; +}; + +#endif // hifi_OctreeStatsProvider_h diff --git a/interface/src/ui/PreferencesDialog.cpp b/interface/src/ui/PreferencesDialog.cpp index d291510556..a12d9020ae 100644 --- a/interface/src/ui/PreferencesDialog.cpp +++ b/interface/src/ui/PreferencesDialog.cpp @@ -103,10 +103,11 @@ void setupPreferences() { preferences->addPreference(new CheckPreference(UI_CATEGORY, "HMD Tablet Becomes Toolbar", getter, setter)); } { - auto getter = []()->bool { return qApp->getTabletVisibleToOthersSetting(); }; - auto setter = [](bool value) { qApp->setTabletVisibleToOthersSetting(value); }; - 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 static const QString SNAPSHOTS { "Snapshots" }; { diff --git a/interface/src/ui/ScriptEditBox.cpp b/interface/src/ui/ScriptEditBox.cpp deleted file mode 100644 index 2aea225b17..0000000000 --- a/interface/src/ui/ScriptEditBox.cpp +++ /dev/null @@ -1,111 +0,0 @@ -// -// ScriptEditBox.cpp -// interface/src/ui -// -// Created by Thijs Wenker on 4/30/14. -// Copyright 2014 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -#include "ScriptEditBox.h" - -#include -#include - -#include "ScriptLineNumberArea.h" - -ScriptEditBox::ScriptEditBox(QWidget* parent) : - QPlainTextEdit(parent) -{ - _scriptLineNumberArea = new ScriptLineNumberArea(this); - - connect(this, &ScriptEditBox::blockCountChanged, this, &ScriptEditBox::updateLineNumberAreaWidth); - connect(this, &ScriptEditBox::updateRequest, this, &ScriptEditBox::updateLineNumberArea); - connect(this, &ScriptEditBox::cursorPositionChanged, this, &ScriptEditBox::highlightCurrentLine); - - updateLineNumberAreaWidth(0); - highlightCurrentLine(); -} - -int ScriptEditBox::lineNumberAreaWidth() { - int digits = 1; - const int SPACER_PIXELS = 3; - const int BASE_TEN = 10; - int max = qMax(1, blockCount()); - while (max >= BASE_TEN) { - max /= BASE_TEN; - digits++; - } - return SPACER_PIXELS + fontMetrics().width(QLatin1Char('H')) * digits; -} - -void ScriptEditBox::updateLineNumberAreaWidth(int blockCount) { - setViewportMargins(lineNumberAreaWidth(), 0, 0, 0); -} - -void ScriptEditBox::updateLineNumberArea(const QRect& rect, int deltaY) { - if (deltaY) { - _scriptLineNumberArea->scroll(0, deltaY); - } else { - _scriptLineNumberArea->update(0, rect.y(), _scriptLineNumberArea->width(), rect.height()); - } - - if (rect.contains(viewport()->rect())) { - updateLineNumberAreaWidth(0); - } -} - -void ScriptEditBox::resizeEvent(QResizeEvent* event) { - QPlainTextEdit::resizeEvent(event); - - QRect localContentsRect = contentsRect(); - _scriptLineNumberArea->setGeometry(QRect(localContentsRect.left(), localContentsRect.top(), lineNumberAreaWidth(), - localContentsRect.height())); -} - -void ScriptEditBox::highlightCurrentLine() { - QList extraSelections; - - if (!isReadOnly()) { - QTextEdit::ExtraSelection selection; - - QColor lineColor = QColor(Qt::gray).lighter(); - - selection.format.setBackground(lineColor); - selection.format.setProperty(QTextFormat::FullWidthSelection, true); - selection.cursor = textCursor(); - selection.cursor.clearSelection(); - extraSelections.append(selection); - } - - setExtraSelections(extraSelections); -} - -void ScriptEditBox::lineNumberAreaPaintEvent(QPaintEvent* event) -{ - QPainter painter(_scriptLineNumberArea); - painter.fillRect(event->rect(), Qt::lightGray); - QTextBlock block = firstVisibleBlock(); - int blockNumber = block.blockNumber(); - int top = (int) blockBoundingGeometry(block).translated(contentOffset()).top(); - int bottom = top + (int) blockBoundingRect(block).height(); - - while (block.isValid() && top <= event->rect().bottom()) { - if (block.isVisible() && bottom >= event->rect().top()) { - QFont font = painter.font(); - font.setBold(this->textCursor().blockNumber() == block.blockNumber()); - painter.setFont(font); - QString number = QString::number(blockNumber + 1); - painter.setPen(Qt::black); - painter.drawText(0, top, _scriptLineNumberArea->width(), fontMetrics().height(), - Qt::AlignRight, number); - } - - block = block.next(); - top = bottom; - bottom = top + (int) blockBoundingRect(block).height(); - blockNumber++; - } -} diff --git a/interface/src/ui/ScriptEditBox.h b/interface/src/ui/ScriptEditBox.h deleted file mode 100644 index 0b037db16a..0000000000 --- a/interface/src/ui/ScriptEditBox.h +++ /dev/null @@ -1,38 +0,0 @@ -// -// ScriptEditBox.h -// interface/src/ui -// -// Created by Thijs Wenker on 4/30/14. -// Copyright 2014 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -#ifndef hifi_ScriptEditBox_h -#define hifi_ScriptEditBox_h - -#include - -class ScriptEditBox : public QPlainTextEdit { - Q_OBJECT - -public: - ScriptEditBox(QWidget* parent = NULL); - - void lineNumberAreaPaintEvent(QPaintEvent* event); - int lineNumberAreaWidth(); - -protected: - void resizeEvent(QResizeEvent* event) override; - -private slots: - void updateLineNumberAreaWidth(int blockCount); - void highlightCurrentLine(); - void updateLineNumberArea(const QRect& rect, int deltaY); - -private: - QWidget* _scriptLineNumberArea; -}; - -#endif // hifi_ScriptEditBox_h diff --git a/interface/src/ui/ScriptEditorWidget.cpp b/interface/src/ui/ScriptEditorWidget.cpp deleted file mode 100644 index ada6b11355..0000000000 --- a/interface/src/ui/ScriptEditorWidget.cpp +++ /dev/null @@ -1,256 +0,0 @@ -// -// ScriptEditorWidget.cpp -// interface/src/ui -// -// Created by Thijs Wenker on 4/14/14. -// Copyright 2014 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -#include "ui_scriptEditorWidget.h" -#include "ScriptEditorWidget.h" -#include "ScriptEditorWindow.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include - -#include "Application.h" -#include "ScriptHighlighting.h" - -ScriptEditorWidget::ScriptEditorWidget() : - _scriptEditorWidgetUI(new Ui::ScriptEditorWidget), - _scriptEngine(NULL), - _isRestarting(false), - _isReloading(false) -{ - setAttribute(Qt::WA_DeleteOnClose); - - _scriptEditorWidgetUI->setupUi(this); - - connect(_scriptEditorWidgetUI->scriptEdit->document(), &QTextDocument::modificationChanged, this, - &ScriptEditorWidget::scriptModified); - connect(_scriptEditorWidgetUI->scriptEdit->document(), &QTextDocument::contentsChanged, this, - &ScriptEditorWidget::onScriptModified); - - // remove the title bar (see the Qt docs on setTitleBarWidget) - setTitleBarWidget(new QWidget()); - QFontMetrics fm(_scriptEditorWidgetUI->scriptEdit->font()); - _scriptEditorWidgetUI->scriptEdit->setTabStopWidth(fm.width('0') * 4); - // We create a new ScriptHighligting QObject and provide it with a parent so this is NOT a memory leak. - new ScriptHighlighting(_scriptEditorWidgetUI->scriptEdit->document()); - QTimer::singleShot(0, _scriptEditorWidgetUI->scriptEdit, SLOT(setFocus())); - - _console = new JSConsole(this); - _console->setFixedHeight(CONSOLE_HEIGHT); - _scriptEditorWidgetUI->verticalLayout->addWidget(_console); - connect(_scriptEditorWidgetUI->clearButton, &QPushButton::clicked, _console, &JSConsole::clear); -} - -ScriptEditorWidget::~ScriptEditorWidget() { - delete _scriptEditorWidgetUI; - delete _console; -} - -void ScriptEditorWidget::onScriptModified() { - if(_scriptEditorWidgetUI->onTheFlyCheckBox->isChecked() && isModified() && isRunning() && !_isReloading) { - _isRestarting = true; - setRunning(false); - // Script is restarted once current script instance finishes. - } -} - -void ScriptEditorWidget::onScriptFinished(const QString& scriptPath) { - _scriptEngine = NULL; - _console->setScriptEngine(NULL); - if (_isRestarting) { - _isRestarting = false; - setRunning(true); - } -} - -bool ScriptEditorWidget::isModified() { - return _scriptEditorWidgetUI->scriptEdit->document()->isModified(); -} - -bool ScriptEditorWidget::isRunning() { - return (_scriptEngine != NULL) ? _scriptEngine->isRunning() : false; -} - -bool ScriptEditorWidget::setRunning(bool run) { - if (run && isModified() && !save()) { - return false; - } - - if (_scriptEngine != NULL) { - disconnect(_scriptEngine, &ScriptEngine::runningStateChanged, this, &ScriptEditorWidget::runningStateChanged); - disconnect(_scriptEngine, &ScriptEngine::update, this, &ScriptEditorWidget::onScriptModified); - disconnect(_scriptEngine, &ScriptEngine::finished, this, &ScriptEditorWidget::onScriptFinished); - } - - auto scriptEngines = DependencyManager::get(); - if (run) { - const QString& scriptURLString = QUrl(_currentScript).toString(); - // Reload script so that an out of date copy is not retrieved from the cache - _scriptEngine = scriptEngines->loadScript(scriptURLString, true, true, false, true); - connect(_scriptEngine, &ScriptEngine::runningStateChanged, this, &ScriptEditorWidget::runningStateChanged); - connect(_scriptEngine, &ScriptEngine::update, this, &ScriptEditorWidget::onScriptModified); - connect(_scriptEngine, &ScriptEngine::finished, this, &ScriptEditorWidget::onScriptFinished); - } else { - connect(_scriptEngine, &ScriptEngine::finished, this, &ScriptEditorWidget::onScriptFinished); - const QString& scriptURLString = QUrl(_currentScript).toString(); - scriptEngines->stopScript(scriptURLString); - _scriptEngine = NULL; - } - _console->setScriptEngine(_scriptEngine); - return true; -} - -bool ScriptEditorWidget::saveFile(const QString &scriptPath) { - QFile file(scriptPath); - if (!file.open(QFile::WriteOnly | QFile::Text)) { - OffscreenUi::warning(this, tr("Interface"), tr("Cannot write script %1:\n%2.").arg(scriptPath) - .arg(file.errorString())); - return false; - } - - QTextStream out(&file); - out << _scriptEditorWidgetUI->scriptEdit->toPlainText(); - file.close(); - - setScriptFile(scriptPath); - return true; -} - -void ScriptEditorWidget::loadFile(const QString& scriptPath) { - QUrl url(scriptPath); - - // if the scheme length is one or lower, maybe they typed in a file, let's try - const int WINDOWS_DRIVE_LETTER_SIZE = 1; - if (url.scheme().size() <= WINDOWS_DRIVE_LETTER_SIZE) { - QFile file(scriptPath); - if (!file.open(QFile::ReadOnly | QFile::Text)) { - OffscreenUi::warning(this, tr("Interface"), tr("Cannot read script %1:\n%2.").arg(scriptPath) - .arg(file.errorString())); - return; - } - QTextStream in(&file); - _scriptEditorWidgetUI->scriptEdit->setPlainText(in.readAll()); - file.close(); - setScriptFile(scriptPath); - - if (_scriptEngine != NULL) { - disconnect(_scriptEngine, &ScriptEngine::runningStateChanged, this, &ScriptEditorWidget::runningStateChanged); - disconnect(_scriptEngine, &ScriptEngine::update, this, &ScriptEditorWidget::onScriptModified); - disconnect(_scriptEngine, &ScriptEngine::finished, this, &ScriptEditorWidget::onScriptFinished); - } - } else { - QNetworkAccessManager& networkAccessManager = NetworkAccessManager::getInstance(); - QNetworkRequest networkRequest = QNetworkRequest(url); - networkRequest.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true); - networkRequest.setHeader(QNetworkRequest::UserAgentHeader, HIGH_FIDELITY_USER_AGENT); - QNetworkReply* reply = networkAccessManager.get(networkRequest); - qDebug() << "Downloading included script at" << scriptPath; - QEventLoop loop; - QObject::connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit); - loop.exec(); - _scriptEditorWidgetUI->scriptEdit->setPlainText(reply->readAll()); - delete reply; - - if (!saveAs()) { - static_cast(this->parent()->parent()->parent())->terminateCurrentTab(); - } - } - const QString& scriptURLString = QUrl(_currentScript).toString(); - _scriptEngine = DependencyManager::get()->getScriptEngine(scriptURLString); - if (_scriptEngine != NULL) { - connect(_scriptEngine, &ScriptEngine::runningStateChanged, this, &ScriptEditorWidget::runningStateChanged); - connect(_scriptEngine, &ScriptEngine::update, this, &ScriptEditorWidget::onScriptModified); - connect(_scriptEngine, &ScriptEngine::finished, this, &ScriptEditorWidget::onScriptFinished); - } - _console->setScriptEngine(_scriptEngine); -} - -bool ScriptEditorWidget::save() { - return _currentScript.isEmpty() ? saveAs() : saveFile(_currentScript); -} - -bool ScriptEditorWidget::saveAs() { - auto scriptEngines = DependencyManager::get(); - QString fileName = QFileDialog::getSaveFileName(this, tr("Save script"), - qApp->getPreviousScriptLocation(), - tr("JavaScript Files (*.js)")); - if (!fileName.isEmpty()) { - qApp->setPreviousScriptLocation(fileName); - return saveFile(fileName); - } else { - return false; - } -} - -void ScriptEditorWidget::setScriptFile(const QString& scriptPath) { - _currentScript = scriptPath; - _currentScriptModified = QFileInfo(_currentScript).lastModified(); - _scriptEditorWidgetUI->scriptEdit->document()->setModified(false); - setWindowModified(false); - - emit scriptnameChanged(); -} - -bool ScriptEditorWidget::questionSave() { - if (_scriptEditorWidgetUI->scriptEdit->document()->isModified()) { - QMessageBox::StandardButton button = OffscreenUi::warning(this, tr("Interface"), - tr("The script has been modified.\nDo you want to save your changes?"), QMessageBox::Save | QMessageBox::Discard | - QMessageBox::Cancel, QMessageBox::Save); - return button == QMessageBox::Save ? save() : (button == QMessageBox::Discard); - } - return true; -} - -void ScriptEditorWidget::onWindowActivated() { - if (!_isReloading) { - _isReloading = true; - - QDateTime fileStamp = QFileInfo(_currentScript).lastModified(); - if (fileStamp > _currentScriptModified) { - bool doReload = false; - auto window = static_cast(this->parent()->parent()->parent()); - window->inModalDialog = true; - if (window->autoReloadScripts() - || OffscreenUi::question(this, tr("Reload Script"), - tr("The following file has been modified outside of the Interface editor:") + "\n" + _currentScript + "\n" - + (isModified() - ? tr("Do you want to reload it and lose the changes you've made in the Interface editor?") - : tr("Do you want to reload it?")), - QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) { - doReload = true; - } - window->inModalDialog = false; - if (doReload) { - loadFile(_currentScript); - if (_scriptEditorWidgetUI->onTheFlyCheckBox->isChecked() && isRunning()) { - _isRestarting = true; - setRunning(false); - // Script is restarted once current script instance finishes. - } - } else { - _currentScriptModified = fileStamp; // Asked and answered. Don't ask again until the external file is changed again. - } - } - _isReloading = false; - } -} diff --git a/interface/src/ui/ScriptEditorWidget.h b/interface/src/ui/ScriptEditorWidget.h deleted file mode 100644 index f53fd7b718..0000000000 --- a/interface/src/ui/ScriptEditorWidget.h +++ /dev/null @@ -1,64 +0,0 @@ -// -// ScriptEditorWidget.h -// interface/src/ui -// -// Created by Thijs Wenker on 4/14/14. -// Copyright 2014 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -#ifndef hifi_ScriptEditorWidget_h -#define hifi_ScriptEditorWidget_h - -#include - -#include "JSConsole.h" -#include "ScriptEngine.h" - -namespace Ui { - class ScriptEditorWidget; -} - -class ScriptEditorWidget : public QDockWidget { - Q_OBJECT - -public: - ScriptEditorWidget(); - ~ScriptEditorWidget(); - - bool isModified(); - bool isRunning(); - bool setRunning(bool run); - bool saveFile(const QString& scriptPath); - void loadFile(const QString& scriptPath); - void setScriptFile(const QString& scriptPath); - bool save(); - bool saveAs(); - bool questionSave(); - const QString getScriptName() const { return _currentScript; }; - -signals: - void runningStateChanged(); - void scriptnameChanged(); - void scriptModified(); - -public slots: - void onWindowActivated(); - -private slots: - void onScriptModified(); - void onScriptFinished(const QString& scriptName); - -private: - JSConsole* _console; - Ui::ScriptEditorWidget* _scriptEditorWidgetUI; - ScriptEngine* _scriptEngine; - QString _currentScript; - QDateTime _currentScriptModified; - bool _isRestarting; - bool _isReloading; -}; - -#endif // hifi_ScriptEditorWidget_h diff --git a/interface/src/ui/ScriptEditorWindow.cpp b/interface/src/ui/ScriptEditorWindow.cpp deleted file mode 100644 index 58abd23979..0000000000 --- a/interface/src/ui/ScriptEditorWindow.cpp +++ /dev/null @@ -1,259 +0,0 @@ -// -// ScriptEditorWindow.cpp -// interface/src/ui -// -// Created by Thijs Wenker on 4/14/14. -// Copyright 2014 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -#include - -#include "ui_scriptEditorWindow.h" -#include "ScriptEditorWindow.h" -#include "ScriptEditorWidget.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include "Application.h" -#include "PathUtils.h" - -ScriptEditorWindow::ScriptEditorWindow(QWidget* parent) : - QWidget(parent), - _ScriptEditorWindowUI(new Ui::ScriptEditorWindow), - _loadMenu(new QMenu), - _saveMenu(new QMenu) -{ - setAttribute(Qt::WA_DeleteOnClose); - - _ScriptEditorWindowUI->setupUi(this); - - this->setWindowFlags(Qt::Tool); - addScriptEditorWidget("New script"); - connect(_loadMenu, &QMenu::aboutToShow, this, &ScriptEditorWindow::loadMenuAboutToShow); - _ScriptEditorWindowUI->loadButton->setMenu(_loadMenu); - - _saveMenu->addAction("Save as..", this, SLOT(saveScriptAsClicked()), Qt::CTRL | Qt::SHIFT | Qt::Key_S); - - _ScriptEditorWindowUI->saveButton->setMenu(_saveMenu); - - connect(new QShortcut(QKeySequence("Ctrl+N"), this), &QShortcut::activated, this, &ScriptEditorWindow::newScriptClicked); - connect(new QShortcut(QKeySequence("Ctrl+S"), this), &QShortcut::activated, this,&ScriptEditorWindow::saveScriptClicked); - connect(new QShortcut(QKeySequence("Ctrl+O"), this), &QShortcut::activated, this, &ScriptEditorWindow::loadScriptClicked); - connect(new QShortcut(QKeySequence("F5"), this), &QShortcut::activated, this, &ScriptEditorWindow::toggleRunScriptClicked); - - _ScriptEditorWindowUI->loadButton->setIcon(QIcon(QPixmap(PathUtils::resourcesPath() + "icons/load-script.svg"))); - _ScriptEditorWindowUI->newButton->setIcon(QIcon(QPixmap(PathUtils::resourcesPath() + "icons/new-script.svg"))); - _ScriptEditorWindowUI->saveButton->setIcon(QIcon(QPixmap(PathUtils::resourcesPath() + "icons/save-script.svg"))); - _ScriptEditorWindowUI->toggleRunButton->setIcon(QIcon(QPixmap(PathUtils::resourcesPath() + "icons/start-script.svg"))); -} - -ScriptEditorWindow::~ScriptEditorWindow() { - delete _ScriptEditorWindowUI; -} - -void ScriptEditorWindow::setRunningState(bool run) { - if (_ScriptEditorWindowUI->tabWidget->currentIndex() != -1) { - static_cast(_ScriptEditorWindowUI->tabWidget->currentWidget())->setRunning(run); - } - this->updateButtons(); -} - -void ScriptEditorWindow::updateButtons() { - bool isRunning = _ScriptEditorWindowUI->tabWidget->currentIndex() != -1 && - static_cast(_ScriptEditorWindowUI->tabWidget->currentWidget())->isRunning(); - _ScriptEditorWindowUI->toggleRunButton->setEnabled(_ScriptEditorWindowUI->tabWidget->currentIndex() != -1); - _ScriptEditorWindowUI->toggleRunButton->setIcon(QIcon(QPixmap(PathUtils::resourcesPath() + ((isRunning ? - "icons/stop-script.svg" : "icons/start-script.svg"))))); -} - -void ScriptEditorWindow::loadScriptMenu(const QString& scriptName) { - addScriptEditorWidget("loading...")->loadFile(scriptName); - updateButtons(); -} - -void ScriptEditorWindow::loadScriptClicked() { - QString scriptName = QFileDialog::getOpenFileName(this, tr("Interface"), - qApp->getPreviousScriptLocation(), - tr("JavaScript Files (*.js)")); - if (!scriptName.isEmpty()) { - qApp->setPreviousScriptLocation(scriptName); - addScriptEditorWidget("loading...")->loadFile(scriptName); - updateButtons(); - } -} - -void ScriptEditorWindow::loadMenuAboutToShow() { - _loadMenu->clear(); - QStringList runningScripts = DependencyManager::get()->getRunningScripts(); - if (runningScripts.count() > 0) { - QSignalMapper* signalMapper = new QSignalMapper(this); - foreach (const QString& runningScript, runningScripts) { - QAction* runningScriptAction = new QAction(runningScript, _loadMenu); - connect(runningScriptAction, SIGNAL(triggered()), signalMapper, SLOT(map())); - signalMapper->setMapping(runningScriptAction, runningScript); - _loadMenu->addAction(runningScriptAction); - } - connect(signalMapper, SIGNAL(mapped(const QString &)), this, SLOT(loadScriptMenu(const QString&))); - } else { - QAction* naAction = new QAction("(no running scripts)", _loadMenu); - naAction->setDisabled(true); - _loadMenu->addAction(naAction); - } -} - -void ScriptEditorWindow::newScriptClicked() { - addScriptEditorWidget(QString("New script")); -} - -void ScriptEditorWindow::toggleRunScriptClicked() { - this->setRunningState(!(_ScriptEditorWindowUI->tabWidget->currentIndex() !=-1 - && static_cast(_ScriptEditorWindowUI->tabWidget->currentWidget())->isRunning())); -} - -void ScriptEditorWindow::saveScriptClicked() { - if (_ScriptEditorWindowUI->tabWidget->currentIndex() != -1) { - ScriptEditorWidget* currentScriptWidget = static_cast(_ScriptEditorWindowUI->tabWidget - ->currentWidget()); - currentScriptWidget->save(); - } -} - -void ScriptEditorWindow::saveScriptAsClicked() { - if (_ScriptEditorWindowUI->tabWidget->currentIndex() != -1) { - ScriptEditorWidget* currentScriptWidget = static_cast(_ScriptEditorWindowUI->tabWidget - ->currentWidget()); - currentScriptWidget->saveAs(); - } -} - -ScriptEditorWidget* ScriptEditorWindow::addScriptEditorWidget(QString title) { - ScriptEditorWidget* newScriptEditorWidget = new ScriptEditorWidget(); - connect(newScriptEditorWidget, &ScriptEditorWidget::scriptnameChanged, this, &ScriptEditorWindow::updateScriptNameOrStatus); - connect(newScriptEditorWidget, &ScriptEditorWidget::scriptModified, this, &ScriptEditorWindow::updateScriptNameOrStatus); - connect(newScriptEditorWidget, &ScriptEditorWidget::runningStateChanged, this, &ScriptEditorWindow::updateButtons); - connect(this, &ScriptEditorWindow::windowActivated, newScriptEditorWidget, &ScriptEditorWidget::onWindowActivated); - _ScriptEditorWindowUI->tabWidget->addTab(newScriptEditorWidget, title); - _ScriptEditorWindowUI->tabWidget->setCurrentWidget(newScriptEditorWidget); - newScriptEditorWidget->setUpdatesEnabled(true); - newScriptEditorWidget->adjustSize(); - return newScriptEditorWidget; -} - -void ScriptEditorWindow::tabSwitched(int tabIndex) { - this->updateButtons(); - if (_ScriptEditorWindowUI->tabWidget->currentIndex() != -1) { - ScriptEditorWidget* currentScriptWidget = static_cast(_ScriptEditorWindowUI->tabWidget - ->currentWidget()); - QString modifiedStar = (currentScriptWidget->isModified() ? "*" : ""); - if (currentScriptWidget->getScriptName().length() > 0) { - this->setWindowTitle("Script Editor [" + currentScriptWidget->getScriptName() + modifiedStar + "]"); - } else { - this->setWindowTitle("Script Editor [New script" + modifiedStar + "]"); - } - } else { - this->setWindowTitle("Script Editor"); - } -} - -void ScriptEditorWindow::tabCloseRequested(int tabIndex) { - if (ignoreCloseForModal(nullptr)) { - return; - } - ScriptEditorWidget* closingScriptWidget = static_cast(_ScriptEditorWindowUI->tabWidget - ->widget(tabIndex)); - if(closingScriptWidget->questionSave()) { - _ScriptEditorWindowUI->tabWidget->removeTab(tabIndex); - } -} - -// If this operating system window causes a qml overlay modal dialog (which might not even be seen by the user), closing this window -// will crash the code that was waiting on the dialog result. So that code whousl set inModalDialog to true while the question is up. -// This code will not be necessary when switch out all operating system windows for qml overlays. -bool ScriptEditorWindow::ignoreCloseForModal(QCloseEvent* event) { - if (!inModalDialog) { - return false; - } - // Deliberately not using OffscreenUi, so that the dialog is seen. - QMessageBox::information(this, tr("Interface"), tr("There is a modal dialog that must be answered before closing."), - QMessageBox::Discard, QMessageBox::Discard); - if (event) { - event->ignore(); // don't close - } - return true; -} - -void ScriptEditorWindow::closeEvent(QCloseEvent *event) { - if (ignoreCloseForModal(event)) { - return; - } - bool unsaved_docs_warning = false; - for (int i = 0; i < _ScriptEditorWindowUI->tabWidget->count(); i++){ - if(static_cast(_ScriptEditorWindowUI->tabWidget->widget(i))->isModified()){ - unsaved_docs_warning = true; - break; - } - } - - if (!unsaved_docs_warning || QMessageBox::warning(this, tr("Interface"), - tr("There are some unsaved scripts, are you sure you want to close the editor? Changes will be lost!"), - QMessageBox::Discard | QMessageBox::Cancel, QMessageBox::Cancel) == QMessageBox::Discard) { - event->accept(); - } else { - event->ignore(); - } -} - -void ScriptEditorWindow::updateScriptNameOrStatus() { - ScriptEditorWidget* source = static_cast(QObject::sender()); - QString modifiedStar = (source->isModified()? "*" : ""); - if (source->getScriptName().length() > 0) { - for (int i = 0; i < _ScriptEditorWindowUI->tabWidget->count(); i++){ - if (_ScriptEditorWindowUI->tabWidget->widget(i) == source) { - _ScriptEditorWindowUI->tabWidget->setTabText(i, modifiedStar + QFileInfo(source->getScriptName()).fileName()); - _ScriptEditorWindowUI->tabWidget->setTabToolTip(i, source->getScriptName()); - } - } - } - - if (_ScriptEditorWindowUI->tabWidget->currentWidget() == source) { - if (source->getScriptName().length() > 0) { - this->setWindowTitle("Script Editor [" + source->getScriptName() + modifiedStar + "]"); - } else { - this->setWindowTitle("Script Editor [New script" + modifiedStar + "]"); - } - } -} - -void ScriptEditorWindow::terminateCurrentTab() { - if (_ScriptEditorWindowUI->tabWidget->currentIndex() != -1) { - _ScriptEditorWindowUI->tabWidget->removeTab(_ScriptEditorWindowUI->tabWidget->currentIndex()); - this->raise(); - } -} - -bool ScriptEditorWindow::autoReloadScripts() { - return _ScriptEditorWindowUI->autoReloadCheckBox->isChecked(); -} - -bool ScriptEditorWindow::event(QEvent* event) { - if (event->type() == QEvent::WindowActivate) { - emit windowActivated(); - } - return QWidget::event(event); -} - diff --git a/interface/src/ui/ScriptEditorWindow.h b/interface/src/ui/ScriptEditorWindow.h deleted file mode 100644 index af9863d136..0000000000 --- a/interface/src/ui/ScriptEditorWindow.h +++ /dev/null @@ -1,64 +0,0 @@ -// -// ScriptEditorWindow.h -// interface/src/ui -// -// Created by Thijs Wenker on 4/14/14. -// Copyright 2014 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -#ifndef hifi_ScriptEditorWindow_h -#define hifi_ScriptEditorWindow_h - -#include "ScriptEditorWidget.h" - -namespace Ui { - class ScriptEditorWindow; -} - -class ScriptEditorWindow : public QWidget { - Q_OBJECT - -public: - ScriptEditorWindow(QWidget* parent = nullptr); - ~ScriptEditorWindow(); - - void terminateCurrentTab(); - bool autoReloadScripts(); - - bool inModalDialog { false }; - bool ignoreCloseForModal(QCloseEvent* event); - -signals: - void windowActivated(); - -protected: - void closeEvent(QCloseEvent* event) override; - virtual bool event(QEvent* event) override; - -private: - Ui::ScriptEditorWindow* _ScriptEditorWindowUI; - QMenu* _loadMenu; - QMenu* _saveMenu; - - ScriptEditorWidget* addScriptEditorWidget(QString title); - void setRunningState(bool run); - void setScriptName(const QString& scriptName); - -private slots: - void loadScriptMenu(const QString& scriptName); - void loadScriptClicked(); - void newScriptClicked(); - void toggleRunScriptClicked(); - void saveScriptClicked(); - void saveScriptAsClicked(); - void loadMenuAboutToShow(); - void tabSwitched(int tabIndex); - void tabCloseRequested(int tabIndex); - void updateScriptNameOrStatus(); - void updateButtons(); -}; - -#endif // hifi_ScriptEditorWindow_h diff --git a/interface/src/ui/ScriptLineNumberArea.cpp b/interface/src/ui/ScriptLineNumberArea.cpp deleted file mode 100644 index 6d7e9185ea..0000000000 --- a/interface/src/ui/ScriptLineNumberArea.cpp +++ /dev/null @@ -1,28 +0,0 @@ -// -// ScriptLineNumberArea.cpp -// interface/src/ui -// -// Created by Thijs Wenker on 4/30/14. -// Copyright 2014 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -#include "ScriptLineNumberArea.h" - -#include "ScriptEditBox.h" - -ScriptLineNumberArea::ScriptLineNumberArea(ScriptEditBox* scriptEditBox) : - QWidget(scriptEditBox) -{ - _scriptEditBox = scriptEditBox; -} - -QSize ScriptLineNumberArea::sizeHint() const { - return QSize(_scriptEditBox->lineNumberAreaWidth(), 0); -} - -void ScriptLineNumberArea::paintEvent(QPaintEvent* event) { - _scriptEditBox->lineNumberAreaPaintEvent(event); -} diff --git a/interface/src/ui/ScriptLineNumberArea.h b/interface/src/ui/ScriptLineNumberArea.h deleted file mode 100644 index 77de8244ce..0000000000 --- a/interface/src/ui/ScriptLineNumberArea.h +++ /dev/null @@ -1,32 +0,0 @@ -// -// ScriptLineNumberArea.h -// interface/src/ui -// -// Created by Thijs Wenker on 4/30/14. -// Copyright 2014 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -#ifndef hifi_ScriptLineNumberArea_h -#define hifi_ScriptLineNumberArea_h - -#include - -class ScriptEditBox; - -class ScriptLineNumberArea : public QWidget { - -public: - ScriptLineNumberArea(ScriptEditBox* scriptEditBox); - QSize sizeHint() const override; - -protected: - void paintEvent(QPaintEvent* event) override; - -private: - ScriptEditBox* _scriptEditBox; -}; - -#endif // hifi_ScriptLineNumberArea_h diff --git a/interface/src/ui/ScriptsTableWidget.cpp b/interface/src/ui/ScriptsTableWidget.cpp deleted file mode 100644 index 7b4f9e6b1f..0000000000 --- a/interface/src/ui/ScriptsTableWidget.cpp +++ /dev/null @@ -1,49 +0,0 @@ -// -// ScriptsTableWidget.cpp -// interface -// -// Created by Mohammed Nafees on 04/03/2014. -// Copyright (c) 2014 High Fidelity, Inc. All rights reserved. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -#include -#include -#include -#include - -#include "ScriptsTableWidget.h" - -ScriptsTableWidget::ScriptsTableWidget(QWidget* parent) : - QTableWidget(parent) { - verticalHeader()->setVisible(false); - horizontalHeader()->setVisible(false); - setShowGrid(false); - setSelectionMode(QAbstractItemView::NoSelection); - setEditTriggers(QAbstractItemView::NoEditTriggers); - setStyleSheet("QTableWidget { border: none; background: transparent; color: #333333; } QToolTip { color: #000000; background: #f9f6e4; padding: 2px; }"); - setToolTipDuration(200); - setWordWrap(true); - setGeometry(0, 0, parent->width(), parent->height()); -} - -void ScriptsTableWidget::paintEvent(QPaintEvent* event) { - QPainter painter(viewport()); - painter.setPen(QColor::fromRgb(225, 225, 225)); // #e1e1e1 - - int y = 0; - for (int i = 0; i < rowCount(); i++) { - painter.drawLine(5, rowHeight(i) + y, width(), rowHeight(i) + y); - y += rowHeight(i); - } - painter.end(); - - QTableWidget::paintEvent(event); -} - -void ScriptsTableWidget::keyPressEvent(QKeyEvent* event) { - // Ignore keys so they will propagate correctly - event->ignore(); -} diff --git a/interface/src/ui/ScriptsTableWidget.h b/interface/src/ui/ScriptsTableWidget.h deleted file mode 100644 index f5e3407e97..0000000000 --- a/interface/src/ui/ScriptsTableWidget.h +++ /dev/null @@ -1,28 +0,0 @@ -// -// ScriptsTableWidget.h -// interface -// -// Created by Mohammed Nafees on 04/03/2014. -// Copyright (c) 2014 High Fidelity, Inc. All rights reserved. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -#ifndef hifi__ScriptsTableWidget_h -#define hifi__ScriptsTableWidget_h - -#include -#include - -class ScriptsTableWidget : public QTableWidget { - Q_OBJECT -public: - explicit ScriptsTableWidget(QWidget* parent); - -protected: - virtual void paintEvent(QPaintEvent* event) override; - virtual void keyPressEvent(QKeyEvent* event) override; -}; - -#endif // hifi__ScriptsTableWidget_h diff --git a/interface/src/ui/Stats.cpp b/interface/src/ui/Stats.cpp index 1075bbdaa4..dc612b0129 100644 --- a/interface/src/ui/Stats.cpp +++ b/interface/src/ui/Stats.cpp @@ -38,6 +38,8 @@ using namespace std; static Stats* INSTANCE{ nullptr }; +QString getTextureMemoryPressureModeString(); + Stats* Stats::getInstance() { if (!INSTANCE) { Stats::registerType(); @@ -124,7 +126,7 @@ void Stats::updateStats(bool force) { STAT_UPDATE(updatedAvatarCount, avatarManager->getNumAvatarsUpdated()); STAT_UPDATE(notUpdatedAvatarCount, avatarManager->getNumAvatarsNotUpdated()); STAT_UPDATE(serverCount, (int)nodeList->size()); - STAT_UPDATE(framerate, qApp->getFps()); + STAT_UPDATE_FLOAT(framerate, qApp->getFps(), 0.1f); if (qApp->getActiveDisplayPlugin()) { auto displayPlugin = qApp->getActiveDisplayPlugin(); auto stats = displayPlugin->getHardwareStats(); @@ -132,11 +134,11 @@ void Stats::updateStats(bool force) { STAT_UPDATE(longrenders, stats["long_render_count"].toInt()); STAT_UPDATE(longsubmits, stats["long_submit_count"].toInt()); STAT_UPDATE(longframes, stats["long_frame_count"].toInt()); - STAT_UPDATE(renderrate, displayPlugin->renderRate()); - STAT_UPDATE(presentrate, displayPlugin->presentRate()); - STAT_UPDATE(presentnewrate, displayPlugin->newFramePresentRate()); - STAT_UPDATE(presentdroprate, displayPlugin->droppedFrameRate()); - STAT_UPDATE(stutterrate, displayPlugin->stutterRate()); + STAT_UPDATE_FLOAT(renderrate, displayPlugin->renderRate(), 0.1f); + STAT_UPDATE_FLOAT(presentrate, displayPlugin->presentRate(), 0.1f); + STAT_UPDATE_FLOAT(presentnewrate, displayPlugin->newFramePresentRate(), 0.1f); + STAT_UPDATE_FLOAT(presentdroprate, displayPlugin->droppedFrameRate(), 0.1f); + STAT_UPDATE_FLOAT(stutterrate, displayPlugin->stutterRate(), 0.1f); } else { STAT_UPDATE(appdropped, -1); STAT_UPDATE(longrenders, -1); @@ -149,8 +151,8 @@ void Stats::updateStats(bool force) { STAT_UPDATE(avatarSimrate, (int)qApp->getAvatarSimrate()); auto bandwidthRecorder = DependencyManager::get(); - STAT_UPDATE(packetInCount, bandwidthRecorder->getCachedTotalAverageInputPacketsPerSecond()); - STAT_UPDATE(packetOutCount, bandwidthRecorder->getCachedTotalAverageOutputPacketsPerSecond()); + STAT_UPDATE(packetInCount, (int)bandwidthRecorder->getCachedTotalAverageInputPacketsPerSecond()); + STAT_UPDATE(packetOutCount, (int)bandwidthRecorder->getCachedTotalAverageOutputPacketsPerSecond()); STAT_UPDATE_FLOAT(mbpsIn, (float)bandwidthRecorder->getCachedTotalAverageInputKilobitsPerSecond() / 1000.0f, 0.01f); STAT_UPDATE_FLOAT(mbpsOut, (float)bandwidthRecorder->getCachedTotalAverageOutputKilobitsPerSecond() / 1000.0f, 0.01f); @@ -162,7 +164,11 @@ void Stats::updateStats(bool force) { SharedNodePointer avatarMixerNode = nodeList->soloNodeOfType(NodeType::AvatarMixer); SharedNodePointer assetServerNode = nodeList->soloNodeOfType(NodeType::AssetServer); SharedNodePointer messageMixerNode = nodeList->soloNodeOfType(NodeType::MessagesMixer); - STAT_UPDATE(audioPing, audioMixerNode ? audioMixerNode->getPingMs() : -1); + STAT_UPDATE(audioPing, audioMixerNode ? audioMixerNode->getPingMs() : -1); + const int mixerLossRate = (int)roundf(_audioStats->data()->getMixerStream()->lossRateWindow() * 100.0f); + const int clientLossRate = (int)roundf(_audioStats->data()->getClientStream()->lossRateWindow() * 100.0f); + const int largestLossRate = mixerLossRate > clientLossRate ? mixerLossRate : clientLossRate; + STAT_UPDATE(audioPacketLoss, audioMixerNode ? largestLossRate : -1); STAT_UPDATE(avatarPing, avatarMixerNode ? avatarMixerNode->getPingMs() : -1); STAT_UPDATE(assetPing, assetServerNode ? assetServerNode->getPingMs() : -1); STAT_UPDATE(messagePing, messageMixerNode ? messageMixerNode->getPingMs() : -1); @@ -194,30 +200,51 @@ void Stats::updateStats(bool force) { if (_expanded || force) { SharedNodePointer avatarMixer = nodeList->soloNodeOfType(NodeType::AvatarMixer); if (avatarMixer) { - STAT_UPDATE(avatarMixerInKbps, roundf(bandwidthRecorder->getAverageInputKilobitsPerSecond(NodeType::AvatarMixer))); - STAT_UPDATE(avatarMixerInPps, roundf(bandwidthRecorder->getAverageInputPacketsPerSecond(NodeType::AvatarMixer))); - STAT_UPDATE(avatarMixerOutKbps, roundf(bandwidthRecorder->getAverageOutputKilobitsPerSecond(NodeType::AvatarMixer))); - STAT_UPDATE(avatarMixerOutPps, roundf(bandwidthRecorder->getAverageOutputPacketsPerSecond(NodeType::AvatarMixer))); - STAT_UPDATE(myAvatarSendRate, avatarManager->getMyAvatarSendRate()); + STAT_UPDATE(avatarMixerInKbps, (int)roundf(bandwidthRecorder->getAverageInputKilobitsPerSecond(NodeType::AvatarMixer))); + STAT_UPDATE(avatarMixerInPps, (int)roundf(bandwidthRecorder->getAverageInputPacketsPerSecond(NodeType::AvatarMixer))); + STAT_UPDATE(avatarMixerOutKbps, (int)roundf(bandwidthRecorder->getAverageOutputKilobitsPerSecond(NodeType::AvatarMixer))); + STAT_UPDATE(avatarMixerOutPps, (int)roundf(bandwidthRecorder->getAverageOutputPacketsPerSecond(NodeType::AvatarMixer))); } else { STAT_UPDATE(avatarMixerInKbps, -1); STAT_UPDATE(avatarMixerInPps, -1); STAT_UPDATE(avatarMixerOutKbps, -1); STAT_UPDATE(avatarMixerOutPps, -1); - STAT_UPDATE(myAvatarSendRate, avatarManager->getMyAvatarSendRate()); } + STAT_UPDATE_FLOAT(myAvatarSendRate, avatarManager->getMyAvatarSendRate(), 0.1f); + SharedNodePointer audioMixerNode = nodeList->soloNodeOfType(NodeType::AudioMixer); + auto audioClient = DependencyManager::get(); if (audioMixerNode || force) { - STAT_UPDATE(audioMixerKbps, roundf( + STAT_UPDATE(audioMixerKbps, (int)roundf( bandwidthRecorder->getAverageInputKilobitsPerSecond(NodeType::AudioMixer) + bandwidthRecorder->getAverageOutputKilobitsPerSecond(NodeType::AudioMixer))); - STAT_UPDATE(audioMixerPps, roundf( + STAT_UPDATE(audioMixerPps, (int)roundf( bandwidthRecorder->getAverageInputPacketsPerSecond(NodeType::AudioMixer) + bandwidthRecorder->getAverageOutputPacketsPerSecond(NodeType::AudioMixer))); + + STAT_UPDATE(audioMixerInKbps, (int)roundf(bandwidthRecorder->getAverageInputKilobitsPerSecond(NodeType::AudioMixer))); + STAT_UPDATE(audioMixerInPps, (int)roundf(bandwidthRecorder->getAverageInputPacketsPerSecond(NodeType::AudioMixer))); + STAT_UPDATE(audioMixerOutKbps, (int)roundf(bandwidthRecorder->getAverageOutputKilobitsPerSecond(NodeType::AudioMixer))); + STAT_UPDATE(audioMixerOutPps, (int)roundf(bandwidthRecorder->getAverageOutputPacketsPerSecond(NodeType::AudioMixer))); + STAT_UPDATE(audioAudioInboundPPS, (int)audioClient->getAudioInboundPPS()); + STAT_UPDATE(audioSilentInboundPPS, (int)audioClient->getSilentInboundPPS()); + STAT_UPDATE(audioOutboundPPS, (int)audioClient->getAudioOutboundPPS()); + STAT_UPDATE(audioSilentOutboundPPS, (int)audioClient->getSilentOutboundPPS()); } else { STAT_UPDATE(audioMixerKbps, -1); STAT_UPDATE(audioMixerPps, -1); + STAT_UPDATE(audioMixerInKbps, -1); + STAT_UPDATE(audioMixerInPps, -1); + STAT_UPDATE(audioMixerOutKbps, -1); + STAT_UPDATE(audioMixerOutPps, -1); + STAT_UPDATE(audioOutboundPPS, -1); + STAT_UPDATE(audioSilentOutboundPPS, -1); + STAT_UPDATE(audioAudioInboundPPS, -1); + STAT_UPDATE(audioSilentInboundPPS, -1); } + STAT_UPDATE(audioCodec, audioClient->getSelectedAudioFormat()); + STAT_UPDATE(audioNoiseGate, audioClient->getNoiseGateOpen() ? "Open" : "Closed"); + auto loadingRequests = ResourceCache::getLoadingRequests(); STAT_UPDATE(downloads, loadingRequests.size()); @@ -319,10 +346,12 @@ void Stats::updateStats(bool force) { STAT_UPDATE(glContextSwapchainMemory, (int)BYTES_TO_MB(gl::Context::getSwapchainMemoryUsage())); STAT_UPDATE(qmlTextureMemory, (int)BYTES_TO_MB(OffscreenQmlSurface::getUsedTextureMemory())); + STAT_UPDATE(texturePendingTransfers, (int)BYTES_TO_MB(gpu::Texture::getTextureTransferPendingSize())); STAT_UPDATE(gpuTextureMemory, (int)BYTES_TO_MB(gpu::Texture::getTextureGPUMemoryUsage())); STAT_UPDATE(gpuTextureVirtualMemory, (int)BYTES_TO_MB(gpu::Texture::getTextureGPUVirtualMemoryUsage())); STAT_UPDATE(gpuTextureFramebufferMemory, (int)BYTES_TO_MB(gpu::Texture::getTextureGPUFramebufferMemoryUsage())); STAT_UPDATE(gpuTextureSparseMemory, (int)BYTES_TO_MB(gpu::Texture::getTextureGPUSparseMemoryUsage())); + STAT_UPDATE(gpuTextureMemoryPressureState, getTextureMemoryPressureModeString()); STAT_UPDATE(gpuSparseTextureEnabled, gpuContext->getBackend()->isTextureManagementSparseEnabled() ? 1 : 0); STAT_UPDATE(gpuFreeMemory, (int)BYTES_TO_MB(gpu::Context::getFreeGPUMemory())); STAT_UPDATE(rectifiedTextureCount, (int)RECTIFIED_TEXTURE_COUNT.load()); diff --git a/interface/src/ui/Stats.h b/interface/src/ui/Stats.h index 6be084100c..85cf2caab9 100644 --- a/interface/src/ui/Stats.h +++ b/interface/src/ui/Stats.h @@ -70,8 +70,21 @@ class Stats : public QQuickItem { STATS_PROPERTY(int, avatarMixerOutKbps, 0) STATS_PROPERTY(int, avatarMixerOutPps, 0) STATS_PROPERTY(float, myAvatarSendRate, 0) + + STATS_PROPERTY(int, audioMixerInKbps, 0) + STATS_PROPERTY(int, audioMixerInPps, 0) + STATS_PROPERTY(int, audioMixerOutKbps, 0) + STATS_PROPERTY(int, audioMixerOutPps, 0) STATS_PROPERTY(int, audioMixerKbps, 0) STATS_PROPERTY(int, audioMixerPps, 0) + STATS_PROPERTY(int, audioOutboundPPS, 0) + STATS_PROPERTY(int, audioSilentOutboundPPS, 0) + STATS_PROPERTY(int, audioAudioInboundPPS, 0) + STATS_PROPERTY(int, audioSilentInboundPPS, 0) + STATS_PROPERTY(int, audioPacketLoss, 0) + STATS_PROPERTY(QString, audioCodec, QString()) + STATS_PROPERTY(QString, audioNoiseGate, QString()) + STATS_PROPERTY(int, downloads, 0) STATS_PROPERTY(int, downloadLimit, 0) STATS_PROPERTY(int, downloadsPending, 0) @@ -105,11 +118,13 @@ class Stats : public QQuickItem { STATS_PROPERTY(int, gpuTexturesSparse, 0) STATS_PROPERTY(int, glContextSwapchainMemory, 0) STATS_PROPERTY(int, qmlTextureMemory, 0) + STATS_PROPERTY(int, texturePendingTransfers, 0) STATS_PROPERTY(int, gpuTextureMemory, 0) STATS_PROPERTY(int, gpuTextureVirtualMemory, 0) STATS_PROPERTY(int, gpuTextureFramebufferMemory, 0) STATS_PROPERTY(int, gpuTextureSparseMemory, 0) STATS_PROPERTY(int, gpuSparseTextureEnabled, 0) + STATS_PROPERTY(QString, gpuTextureMemoryPressureState, QString()) STATS_PROPERTY(int, gpuFreeMemory, 0) STATS_PROPERTY(float, gpuFrameTime, 0) STATS_PROPERTY(float, batchFrameTime, 0) @@ -180,8 +195,20 @@ signals: void avatarMixerOutKbpsChanged(); void avatarMixerOutPpsChanged(); void myAvatarSendRateChanged(); + void audioMixerInKbpsChanged(); + void audioMixerInPpsChanged(); + void audioMixerOutKbpsChanged(); + void audioMixerOutPpsChanged(); void audioMixerKbpsChanged(); void audioMixerPpsChanged(); + void audioOutboundPPSChanged(); + void audioSilentOutboundPPSChanged(); + void audioAudioInboundPPSChanged(); + void audioSilentInboundPPSChanged(); + void audioPacketLossChanged(); + void audioCodecChanged(); + void audioNoiseGateChanged(); + void downloadsChanged(); void downloadLimitChanged(); void downloadsPendingChanged(); @@ -209,6 +236,7 @@ signals: void timingStatsChanged(); void glContextSwapchainMemoryChanged(); void qmlTextureMemoryChanged(); + void texturePendingTransfersChanged(); void gpuBuffersChanged(); void gpuBufferMemoryChanged(); void gpuTexturesChanged(); @@ -217,6 +245,7 @@ signals: void gpuTextureVirtualMemoryChanged(); void gpuTextureFramebufferMemoryChanged(); void gpuTextureSparseMemoryChanged(); + void gpuTextureMemoryPressureStateChanged(); void gpuSparseTextureEnabledChanged(); void gpuFreeMemoryChanged(); void gpuFrameTimeChanged(); @@ -236,4 +265,3 @@ private: }; #endif // hifi_Stats_h - diff --git a/interface/src/ui/overlays/Base3DOverlay.cpp b/interface/src/ui/overlays/Base3DOverlay.cpp index 70b1fa4b71..d7057c6faa 100644 --- a/interface/src/ui/overlays/Base3DOverlay.cpp +++ b/interface/src/ui/overlays/Base3DOverlay.cpp @@ -199,9 +199,9 @@ void Base3DOverlay::setProperties(const QVariantMap& originalProperties) { auto itemID = getRenderItemID(); if (render::Item::isValidID(itemID)) { render::ScenePointer scene = qApp->getMain3DScene(); - render::PendingChanges pendingChanges; - pendingChanges.updateItem(itemID); - scene->enqueuePendingChanges(pendingChanges); + render::Transaction transaction; + transaction.updateItem(itemID); + scene->enqueueTransaction(transaction); } } } @@ -264,9 +264,9 @@ void Base3DOverlay::locationChanged(bool tellPhysics) { auto itemID = getRenderItemID(); if (render::Item::isValidID(itemID)) { render::ScenePointer scene = qApp->getMain3DScene(); - render::PendingChanges pendingChanges; - pendingChanges.updateItem(itemID); - scene->enqueuePendingChanges(pendingChanges); + render::Transaction transaction; + transaction.updateItem(itemID); + scene->enqueueTransaction(transaction); } } diff --git a/interface/src/ui/overlays/Line3DOverlay.cpp b/interface/src/ui/overlays/Line3DOverlay.cpp index 23668bcc25..900c79fc3f 100644 --- a/interface/src/ui/overlays/Line3DOverlay.cpp +++ b/interface/src/ui/overlays/Line3DOverlay.cpp @@ -23,10 +23,15 @@ Line3DOverlay::Line3DOverlay() : Line3DOverlay::Line3DOverlay(const Line3DOverlay* line3DOverlay) : Base3DOverlay(line3DOverlay), - _start(line3DOverlay->_start), - _end(line3DOverlay->_end), _geometryCacheID(DependencyManager::get()->allocateID()) { + setParentID(line3DOverlay->getParentID()); + setParentJointIndex(line3DOverlay->getParentJointIndex()); + setLocalTransform(line3DOverlay->getLocalTransform()); + _direction = line3DOverlay->getDirection(); + _length = line3DOverlay->getLength(); + _endParentID = line3DOverlay->getEndParentID(); + _endParentJointIndex = line3DOverlay->getEndJointIndex(); } Line3DOverlay::~Line3DOverlay() { @@ -37,17 +42,23 @@ Line3DOverlay::~Line3DOverlay() { } glm::vec3 Line3DOverlay::getStart() const { - bool success; - glm::vec3 worldStart = localToWorld(_start, getParentID(), getParentJointIndex(), success); - if (!success) { - qDebug() << "Line3DOverlay::getStart failed"; - } - return worldStart; + return getPosition(); } glm::vec3 Line3DOverlay::getEnd() const { bool success; - glm::vec3 worldEnd = localToWorld(_end, getParentID(), getParentJointIndex(), success); + glm::vec3 localEnd; + glm::vec3 worldEnd; + + if (_endParentID != QUuid()) { + glm::vec3 localOffset = _direction * _length; + bool success; + worldEnd = localToWorld(localOffset, _endParentID, _endParentJointIndex, success); + return worldEnd; + } + + localEnd = getLocalEnd(); + worldEnd = localToWorld(localEnd, getParentID(), getParentJointIndex(), success); if (!success) { qDebug() << "Line3DOverlay::getEnd failed"; } @@ -55,27 +66,55 @@ glm::vec3 Line3DOverlay::getEnd() const { } void Line3DOverlay::setStart(const glm::vec3& start) { - bool success; - _start = worldToLocal(start, getParentID(), getParentJointIndex(), success); - if (!success) { - qDebug() << "Line3DOverlay::setStart failed"; - } + setPosition(start); } void Line3DOverlay::setEnd(const glm::vec3& end) { bool success; - _end = worldToLocal(end, getParentID(), getParentJointIndex(), success); + glm::vec3 localStart; + glm::vec3 localEnd; + glm::vec3 offset; + + if (_endParentID != QUuid()) { + offset = worldToLocal(end, _endParentID, _endParentJointIndex, success); + } else { + localStart = getLocalStart(); + localEnd = worldToLocal(end, getParentID(), getParentJointIndex(), success); + offset = localEnd - localStart; + } if (!success) { qDebug() << "Line3DOverlay::setEnd failed"; + return; + } + + _length = glm::length(offset); + if (_length > 0.0f) { + _direction = glm::normalize(offset); + } else { + _direction = glm::vec3(0.0f); + } +} + +void Line3DOverlay::setLocalEnd(const glm::vec3& localEnd) { + glm::vec3 offset; + if (_endParentID != QUuid()) { + offset = localEnd; + } else { + glm::vec3 localStart = getLocalStart(); + offset = localEnd - localStart; + } + _length = glm::length(offset); + if (_length > 0.0f) { + _direction = glm::normalize(offset); + } else { + _direction = glm::vec3(0.0f); } } AABox Line3DOverlay::getBounds() const { auto extents = Extents{}; - extents.addPoint(_start); - extents.addPoint(_end); - extents.transform(getTransform()); - + extents.addPoint(getStart()); + extents.addPoint(getEnd()); return AABox(extents); } @@ -90,18 +129,20 @@ void Line3DOverlay::render(RenderArgs* args) { glm::vec4 colorv4(color.red / MAX_COLOR, color.green / MAX_COLOR, color.blue / MAX_COLOR, alpha); auto batch = args->_batch; if (batch) { - batch->setModelTransform(getTransform()); + batch->setModelTransform(Transform()); + glm::vec3 start = getStart(); + glm::vec3 end = getEnd(); auto geometryCache = DependencyManager::get(); if (getIsDashedLine()) { // TODO: add support for color to renderDashedLine() geometryCache->bindSimpleProgram(*batch, false, false, false, true, true); - geometryCache->renderDashedLine(*batch, _start, _end, colorv4, _geometryCacheID); + geometryCache->renderDashedLine(*batch, start, end, colorv4, _geometryCacheID); } else if (_glow > 0.0f) { - geometryCache->renderGlowLine(*batch, _start, _end, colorv4, _glow, _glowWidth, _geometryCacheID); + geometryCache->renderGlowLine(*batch, start, end, colorv4, _glow, _glowWidth, _geometryCacheID); } else { geometryCache->bindSimpleProgram(*batch, false, false, false, true, true); - geometryCache->renderLine(*batch, _start, _end, colorv4, _geometryCacheID); + geometryCache->renderLine(*batch, start, end, colorv4, _geometryCacheID); } } } @@ -116,6 +157,10 @@ const render::ShapeKey Line3DOverlay::getShapeKey() { void Line3DOverlay::setProperties(const QVariantMap& originalProperties) { QVariantMap properties = originalProperties; + glm::vec3 newStart(0.0f); + bool newStartSet { false }; + glm::vec3 newEnd(0.0f); + bool newEndSet { false }; auto start = properties["start"]; // if "start" property was not there, check to see if they included aliases: startPoint @@ -123,30 +168,57 @@ void Line3DOverlay::setProperties(const QVariantMap& originalProperties) { start = properties["startPoint"]; } if (start.isValid()) { - setStart(vec3FromVariant(start)); + newStart = vec3FromVariant(start); + newStartSet = true; } properties.remove("start"); // so that Base3DOverlay doesn't respond to it - auto localStart = properties["localStart"]; - if (localStart.isValid()) { - _start = vec3FromVariant(localStart); - } - properties.remove("localStart"); // so that Base3DOverlay doesn't respond to it - auto end = properties["end"]; // if "end" property was not there, check to see if they included aliases: endPoint if (!end.isValid()) { end = properties["endPoint"]; } if (end.isValid()) { - setEnd(vec3FromVariant(end)); + newEnd = vec3FromVariant(end); + newEndSet = true; + } + properties.remove("end"); // so that Base3DOverlay doesn't respond to it + + auto length = properties["length"]; + if (length.isValid()) { + _length = length.toFloat(); + } + + Base3DOverlay::setProperties(properties); + + auto endParentIDProp = properties["endParentID"]; + if (endParentIDProp.isValid()) { + _endParentID = QUuid(endParentIDProp.toString()); + } + auto endParentJointIndexProp = properties["endParentJointIndex"]; + if (endParentJointIndexProp.isValid()) { + _endParentJointIndex = endParentJointIndexProp.toInt(); + } + + auto localStart = properties["localStart"]; + if (localStart.isValid()) { + glm::vec3 tmpLocalEnd = getLocalEnd(); + setLocalStart(vec3FromVariant(localStart)); + setLocalEnd(tmpLocalEnd); } auto localEnd = properties["localEnd"]; if (localEnd.isValid()) { - _end = vec3FromVariant(localEnd); + setLocalEnd(vec3FromVariant(localEnd)); + } + + // these are saved until after Base3DOverlay::setProperties so parenting infomation can be set, first + if (newStartSet) { + setStart(newStart); + } + if (newEndSet) { + setEnd(newEnd); } - properties.remove("localEnd"); // so that Base3DOverlay doesn't respond to it auto glow = properties["glow"]; if (glow.isValid()) { @@ -161,7 +233,6 @@ void Line3DOverlay::setProperties(const QVariantMap& originalProperties) { setGlow(glowWidth.toFloat()); } - Base3DOverlay::setProperties(properties); } QVariant Line3DOverlay::getProperty(const QString& property) { @@ -171,6 +242,15 @@ QVariant Line3DOverlay::getProperty(const QString& property) { if (property == "end" || property == "endPoint" || property == "p2") { return vec3toVariant(getEnd()); } + if (property == "localStart") { + return vec3toVariant(getLocalStart()); + } + if (property == "localEnd") { + return vec3toVariant(getLocalEnd()); + } + if (property == "length") { + return QVariant(getLength()); + } return Base3DOverlay::getProperty(property); } @@ -178,8 +258,3 @@ QVariant Line3DOverlay::getProperty(const QString& property) { Line3DOverlay* Line3DOverlay::createClone() const { return new Line3DOverlay(this); } - - -void Line3DOverlay::locationChanged(bool tellPhysics) { - // do nothing -} diff --git a/interface/src/ui/overlays/Line3DOverlay.h b/interface/src/ui/overlays/Line3DOverlay.h index b4e2ba8168..c9ceac55a9 100644 --- a/interface/src/ui/overlays/Line3DOverlay.h +++ b/interface/src/ui/overlays/Line3DOverlay.h @@ -15,7 +15,7 @@ class Line3DOverlay : public Base3DOverlay { Q_OBJECT - + public: static QString const TYPE; virtual QString getType() const override { return TYPE; } @@ -37,6 +37,9 @@ public: void setStart(const glm::vec3& start); void setEnd(const glm::vec3& end); + void setLocalStart(const glm::vec3& localStart) { setLocalPosition(localStart); } + void setLocalEnd(const glm::vec3& localEnd); + void setGlow(const float& glow) { _glow = glow; } void setGlowWidth(const float& glowWidth) { _glowWidth = glowWidth; } @@ -45,15 +48,26 @@ public: virtual Line3DOverlay* createClone() const override; - virtual void locationChanged(bool tellPhysics = true) override; + glm::vec3 getDirection() const { return _direction; } + float getLength() const { return _length; } + glm::vec3 getLocalStart() const { return getLocalPosition(); } + glm::vec3 getLocalEnd() const { return getLocalStart() + _direction * _length; } + QUuid getEndParentID() const { return _endParentID; } + quint16 getEndJointIndex() const { return _endParentJointIndex; } + +private: + QUuid _endParentID; + quint16 _endParentJointIndex { INVALID_JOINT_INDEX }; + + // _direction and _length are in the parent's frame. If _endParentID is set, they are + // relative to that. Otherwise, they are relative to the local-start-position (which is the + // same as localPosition) + glm::vec3 _direction; // in parent frame + float _length { 1.0 }; // in parent frame -protected: - glm::vec3 _start; - glm::vec3 _end; float _glow { 0.0 }; float _glowWidth { 0.0 }; int _geometryCacheID; }; - #endif // hifi_Line3DOverlay_h diff --git a/interface/src/ui/overlays/ModelOverlay.cpp b/interface/src/ui/overlays/ModelOverlay.cpp index a0f7c4e824..98ca7110ae 100644 --- a/interface/src/ui/overlays/ModelOverlay.cpp +++ b/interface/src/ui/overlays/ModelOverlay.cpp @@ -58,15 +58,15 @@ void ModelOverlay::update(float deltatime) { _isLoaded = _model->isActive(); } -bool ModelOverlay::addToScene(Overlay::Pointer overlay, std::shared_ptr scene, render::PendingChanges& pendingChanges) { - Volume3DOverlay::addToScene(overlay, scene, pendingChanges); - _model->addToScene(scene, pendingChanges); +bool ModelOverlay::addToScene(Overlay::Pointer overlay, std::shared_ptr scene, render::Transaction& transaction) { + Volume3DOverlay::addToScene(overlay, scene, transaction); + _model->addToScene(scene, transaction); return true; } -void ModelOverlay::removeFromScene(Overlay::Pointer overlay, std::shared_ptr scene, render::PendingChanges& pendingChanges) { - Volume3DOverlay::removeFromScene(overlay, scene, pendingChanges); - _model->removeFromScene(scene, pendingChanges); +void ModelOverlay::removeFromScene(Overlay::Pointer overlay, std::shared_ptr scene, render::Transaction& transaction) { + Volume3DOverlay::removeFromScene(overlay, scene, transaction); + _model->removeFromScene(scene, transaction); } void ModelOverlay::render(RenderArgs* args) { @@ -74,16 +74,16 @@ void ModelOverlay::render(RenderArgs* args) { // check to see if when we added our model to the scene they were ready, if they were not ready, then // fix them up in the scene render::ScenePointer scene = qApp->getMain3DScene(); - render::PendingChanges pendingChanges; + render::Transaction transaction; if (_model->needsFixupInScene()) { - _model->removeFromScene(scene, pendingChanges); - _model->addToScene(scene, pendingChanges); + _model->removeFromScene(scene, transaction); + _model->addToScene(scene, transaction); } _model->setVisibleInScene(_visible, scene); _model->setLayeredInFront(getDrawInFront(), scene); - scene->enqueuePendingChanges(pendingChanges); + scene->enqueueTransaction(transaction); } void ModelOverlay::setProperties(const QVariantMap& properties) { diff --git a/interface/src/ui/overlays/ModelOverlay.h b/interface/src/ui/overlays/ModelOverlay.h index d5f709c2db..245688156f 100644 --- a/interface/src/ui/overlays/ModelOverlay.h +++ b/interface/src/ui/overlays/ModelOverlay.h @@ -36,8 +36,8 @@ public: virtual ModelOverlay* createClone() const override; - virtual bool addToScene(Overlay::Pointer overlay, std::shared_ptr scene, render::PendingChanges& pendingChanges) override; - virtual void removeFromScene(Overlay::Pointer overlay, std::shared_ptr scene, render::PendingChanges& pendingChanges) override; + virtual bool addToScene(Overlay::Pointer overlay, std::shared_ptr scene, render::Transaction& transaction) override; + virtual void removeFromScene(Overlay::Pointer overlay, std::shared_ptr scene, render::Transaction& transaction) override; void locationChanged(bool tellPhysics) override; diff --git a/interface/src/ui/overlays/Overlay.cpp b/interface/src/ui/overlays/Overlay.cpp index 0ad2c94241..e3004bd9c6 100644 --- a/interface/src/ui/overlays/Overlay.cpp +++ b/interface/src/ui/overlays/Overlay.cpp @@ -100,6 +100,9 @@ void Overlay::setProperties(const QVariantMap& properties) { } QVariant Overlay::getProperty(const QString& property) { + if (property == "type") { + return QVariant(getType()); + } if (property == "color") { return xColorToVariant(_color); } @@ -193,14 +196,14 @@ float Overlay::updatePulse() { return _pulse; } -bool Overlay::addToScene(Overlay::Pointer overlay, std::shared_ptr scene, render::PendingChanges& pendingChanges) { +bool Overlay::addToScene(Overlay::Pointer overlay, std::shared_ptr scene, render::Transaction& transaction) { _renderItemID = scene->allocateID(); - pendingChanges.resetItem(_renderItemID, std::make_shared(overlay)); + transaction.resetItem(_renderItemID, std::make_shared(overlay)); return true; } -void Overlay::removeFromScene(Overlay::Pointer overlay, std::shared_ptr scene, render::PendingChanges& pendingChanges) { - pendingChanges.removeItem(_renderItemID); +void Overlay::removeFromScene(Overlay::Pointer overlay, std::shared_ptr scene, render::Transaction& transaction) { + transaction.removeItem(_renderItemID); render::Item::clearID(_renderItemID); } diff --git a/interface/src/ui/overlays/Overlay.h b/interface/src/ui/overlays/Overlay.h index 9ad4f0ba70..9849c71a1f 100644 --- a/interface/src/ui/overlays/Overlay.h +++ b/interface/src/ui/overlays/Overlay.h @@ -48,8 +48,8 @@ public: virtual AABox getBounds() const = 0; virtual bool supportsGetProperty() const { return true; } - virtual bool addToScene(Overlay::Pointer overlay, std::shared_ptr scene, render::PendingChanges& pendingChanges); - virtual void removeFromScene(Overlay::Pointer overlay, std::shared_ptr scene, render::PendingChanges& pendingChanges); + virtual bool addToScene(Overlay::Pointer overlay, std::shared_ptr scene, render::Transaction& transaction); + virtual void removeFromScene(Overlay::Pointer overlay, std::shared_ptr scene, render::Transaction& transaction); virtual const render::ShapeKey getShapeKey() { return render::ShapeKey::Builder::ownPipeline(); } diff --git a/interface/src/ui/overlays/Overlays.cpp b/interface/src/ui/overlays/Overlays.cpp index ad7fbd6cc2..61a283b88c 100644 --- a/interface/src/ui/overlays/Overlays.cpp +++ b/interface/src/ui/overlays/Overlays.cpp @@ -78,7 +78,7 @@ void Overlays::update(float deltatime) { void Overlays::cleanupOverlaysToDelete() { if (!_overlaysToDelete.isEmpty()) { render::ScenePointer scene = qApp->getMain3DScene(); - render::PendingChanges pendingChanges; + render::Transaction transaction; { QWriteLocker lock(&_deleteLock); @@ -88,13 +88,13 @@ void Overlays::cleanupOverlaysToDelete() { auto itemID = overlay->getRenderItemID(); if (render::Item::isValidID(itemID)) { - overlay->removeFromScene(overlay, scene, pendingChanges); + overlay->removeFromScene(overlay, scene, transaction); } } while (!_overlaysToDelete.isEmpty()); } - if (pendingChanges._removedItems.size() > 0) { - scene->enqueuePendingChanges(pendingChanges); + if (transaction._removedItems.size() > 0) { + scene->enqueueTransaction(transaction); } } } @@ -197,9 +197,9 @@ OverlayID Overlays::addOverlay(Overlay::Pointer overlay) { _overlaysWorld[thisID] = overlay; render::ScenePointer scene = qApp->getMain3DScene(); - render::PendingChanges pendingChanges; - overlay->addToScene(overlay, scene, pendingChanges); - scene->enqueuePendingChanges(pendingChanges); + render::Transaction transaction; + overlay->addToScene(overlay, scene, transaction); + scene->enqueueTransaction(transaction); } else { _overlaysHUD[thisID] = overlay; } @@ -431,7 +431,9 @@ RayToOverlayIntersectionResult Overlays::findRayIntersectionInternal(const PickR if (thisOverlay->findRayIntersectionExtraInfo(ray.origin, ray.direction, thisDistance, thisFace, thisSurfaceNormal, thisExtraInfo)) { bool isDrawInFront = thisOverlay->getDrawInFront(); - if (thisDistance < bestDistance && (!bestIsFront || isDrawInFront)) { + if ((bestIsFront && isDrawInFront && thisDistance < bestDistance) + || (!bestIsFront && (isDrawInFront || thisDistance < bestDistance))) { + bestIsFront = isDrawInFront; bestDistance = thisDistance; result.intersects = true; @@ -711,10 +713,9 @@ PointerEvent Overlays::calculatePointerEvent(Overlay::Pointer overlay, PickRay r auto dimensions = thisOverlay->getSize(); glm::vec2 pos2D = projectOntoOverlayXYPlane(position, rotation, dimensions, ray, rayPickResult); - PointerEvent pointerEvent(eventType, MOUSE_POINTER_ID, - pos2D, rayPickResult.intersection, - rayPickResult.surfaceNormal, ray.direction, - toPointerButton(*event), toPointerButtons(*event)); + + PointerEvent pointerEvent(eventType, MOUSE_POINTER_ID, pos2D, rayPickResult.intersection, rayPickResult.surfaceNormal, + ray.direction, toPointerButton(*event), toPointerButtons(*event), event->modifiers()); return pointerEvent; } @@ -769,6 +770,26 @@ bool Overlays::mousePressEvent(QMouseEvent* event) { return false; } +bool Overlays::mouseDoublePressEvent(QMouseEvent* event) { + PerformanceTimer perfTimer("Overlays::mouseDoublePressEvent"); + + PickRay ray = qApp->computePickRay(event->x(), event->y()); + RayToOverlayIntersectionResult rayPickResult = findRayIntersectionForMouseEvent(ray); + if (rayPickResult.intersects) { + _currentClickingOnOverlayID = rayPickResult.overlayID; + + // Only Web overlays can have focus. + auto thisOverlay = std::dynamic_pointer_cast(getOverlay(_currentClickingOnOverlayID)); + if (thisOverlay) { + auto pointerEvent = calculatePointerEvent(thisOverlay, ray, rayPickResult, event, PointerEvent::Press); + emit mouseDoublePressOnOverlay(_currentClickingOnOverlayID, pointerEvent); + return true; + } + } + emit mouseDoublePressOffOverlay(); + return false; +} + bool Overlays::mouseReleaseEvent(QMouseEvent* event) { PerformanceTimer perfTimer("Overlays::mouseReleaseEvent"); diff --git a/interface/src/ui/overlays/Overlays.h b/interface/src/ui/overlays/Overlays.h index 5c22e46880..a1d4be8376 100644 --- a/interface/src/ui/overlays/Overlays.h +++ b/interface/src/ui/overlays/Overlays.h @@ -101,6 +101,7 @@ public: OverlayID addOverlay(Overlay::Pointer overlay); bool mousePressEvent(QMouseEvent* event); + bool mouseDoublePressEvent(QMouseEvent* event); bool mouseReleaseEvent(QMouseEvent* event); bool mouseMoveEvent(QMouseEvent* event); @@ -212,7 +213,7 @@ public slots: * @function Overlays.findOverlays * @param {Vec3} center the point to search from. * @param {float} radius search radius - * @return {List of Overlays.OverlayID} list of overlays withing the radius + * @return {Overlays.OverlayID[]} list of overlays withing the radius */ QVector findOverlays(const glm::vec3& center, float radius) const; @@ -300,9 +301,11 @@ signals: void panelDeleted(OverlayID id); void mousePressOnOverlay(OverlayID overlayID, const PointerEvent& event); + void mouseDoublePressOnOverlay(OverlayID overlayID, const PointerEvent& event); void mouseReleaseOnOverlay(OverlayID overlayID, const PointerEvent& event); void mouseMoveOnOverlay(OverlayID overlayID, const PointerEvent& event); void mousePressOffOverlay(); + void mouseDoublePressOffOverlay(); void hoverEnterOverlay(OverlayID overlayID, const PointerEvent& event); void hoverOverOverlay(OverlayID overlayID, const PointerEvent& event); diff --git a/interface/src/ui/overlays/OverlaysPayload.cpp b/interface/src/ui/overlays/OverlaysPayload.cpp index aa06741638..f1beeea7f5 100644 --- a/interface/src/ui/overlays/OverlaysPayload.cpp +++ b/interface/src/ui/overlays/OverlaysPayload.cpp @@ -62,7 +62,11 @@ namespace render { if (overlay->is3D()) { auto overlay3D = std::dynamic_pointer_cast(overlay); if (overlay3D->isAA()) - return (overlay3D->getDrawInFront() ? LAYER_3D_FRONT : LAYER_3D); + if (overlay3D->getDrawInFront()) { + return LAYER_3D_FRONT; + } else { + return LAYER_3D; + } else return LAYER_NO_AA; } else { diff --git a/interface/src/ui/overlays/Web3DOverlay.cpp b/interface/src/ui/overlays/Web3DOverlay.cpp index bfc37ccf60..502006c862 100644 --- a/interface/src/ui/overlays/Web3DOverlay.cpp +++ b/interface/src/ui/overlays/Web3DOverlay.cpp @@ -17,6 +17,7 @@ #include #include #include +#include #include #include @@ -37,8 +38,19 @@ #include #include "scripting/AccountScriptingInterface.h" #include "scripting/HMDScriptingInterface.h" +#include "scripting/AssetMappingsScriptingInterface.h" #include +#include #include "FileDialogHelper.h" +#include "avatar/AvatarManager.h" +#include "AudioClient.h" +#include "LODManager.h" +#include "ui/OctreeStatsProvider.h" +#include "ui/DomainConnectionModel.h" +#include "scripting/AudioDeviceScriptingInterface.h" +#include "ui/AvatarInputs.h" +#include "avatar/AvatarManager.h" +#include "scripting/GlobalServicesScriptingInterface.h" static const float DPI = 30.47f; static const float INCHES_TO_METERS = 1.0f / 39.3701f; @@ -161,6 +173,10 @@ void Web3DOverlay::loadSourceURL() { _webSurface->getRootContext()->setContextProperty("HMD", DependencyManager::get().data()); _webSurface->getRootContext()->setContextProperty("UserActivityLogger", DependencyManager::get().data()); _webSurface->getRootContext()->setContextProperty("Preferences", DependencyManager::get().data()); + _webSurface->getRootContext()->setContextProperty("Vec3", new Vec3()); + _webSurface->getRootContext()->setContextProperty("Quat", new Quat()); + _webSurface->getRootContext()->setContextProperty("MyAvatar", DependencyManager::get()->getMyAvatar().get()); + _webSurface->getRootContext()->setContextProperty("Entities", DependencyManager::get().data()); if (_webSurface->getRootItem() && _webSurface->getRootItem()->objectName() == "tabletRoot") { auto tabletScriptingInterface = DependencyManager::get(); @@ -168,17 +184,43 @@ void Web3DOverlay::loadSourceURL() { _webSurface->getRootContext()->setContextProperty("offscreenFlags", flags); _webSurface->getRootContext()->setContextProperty("AddressManager", DependencyManager::get().data()); _webSurface->getRootContext()->setContextProperty("Account", AccountScriptingInterface::getInstance()); + _webSurface->getRootContext()->setContextProperty("AudioStats", DependencyManager::get()->getStats().data()); _webSurface->getRootContext()->setContextProperty("HMD", DependencyManager::get().data()); _webSurface->getRootContext()->setContextProperty("fileDialogHelper", new FileDialogHelper()); + _webSurface->getRootContext()->setContextProperty("MyAvatar", DependencyManager::get()->getMyAvatar().get()); + _webSurface->getRootContext()->setContextProperty("ScriptDiscoveryService", DependencyManager::get().data()); + _webSurface->getRootContext()->setContextProperty("Tablet", DependencyManager::get().data()); + _webSurface->getRootContext()->setContextProperty("Assets", DependencyManager::get().data()); + _webSurface->getRootContext()->setContextProperty("LODManager", DependencyManager::get().data()); + _webSurface->getRootContext()->setContextProperty("OctreeStats", DependencyManager::get().data()); + _webSurface->getRootContext()->setContextProperty("DCModel", DependencyManager::get().data()); + _webSurface->getRootContext()->setContextProperty("AudioDevice", AudioDeviceScriptingInterface::getInstance()); + _webSurface->getRootContext()->setContextProperty("AvatarInputs", AvatarInputs::getInstance()); + _webSurface->getRootContext()->setContextProperty("GlobalServices", GlobalServicesScriptingInterface::getInstance()); + _webSurface->getRootContext()->setContextProperty("AvatarList", DependencyManager::get().data()); + + _webSurface->getRootContext()->setContextProperty("pathToFonts", "../../"); tabletScriptingInterface->setQmlTabletRoot("com.highfidelity.interface.tablet.system", _webSurface->getRootItem(), _webSurface.data()); + // mark the TabletProxy object as cpp ownership. + QObject* tablet = tabletScriptingInterface->getTablet("com.highfidelity.interface.tablet.system"); + _webSurface->getRootContext()->engine()->setObjectOwnership(tablet, QQmlEngine::CppOwnership); + // Override min fps for tablet UI, for silky smooth scrolling - _webSurface->setMaxFps(90); + setMaxFPS(90); } } _webSurface->getRootContext()->setContextProperty("globalPosition", vec3toVariant(getPosition())); } +void Web3DOverlay::setMaxFPS(uint8_t maxFPS) { + _desiredMaxFPS = maxFPS; + if (_webSurface) { + _webSurface->setMaxFps(_desiredMaxFPS); + _currentMaxFPS = _desiredMaxFPS; + } +} + void Web3DOverlay::render(RenderArgs* args) { if (!_visible || !getParentVisible()) { return; @@ -188,9 +230,11 @@ void Web3DOverlay::render(RenderArgs* args) { QSurface * currentSurface = currentContext->surface(); if (!_webSurface) { _webSurface = DependencyManager::get()->acquire(pickURL()); - _webSurface->setMaxFps(10); // FIXME, the max FPS could be better managed by being dynamic (based on the number of current surfaces // and the current rendering load) + if (_currentMaxFPS != _desiredMaxFPS) { + setMaxFPS(_desiredMaxFPS); + } loadSourceURL(); _webSurface->resume(); _webSurface->resize(QSize(_resolution.x, _resolution.y)); @@ -198,39 +242,41 @@ void Web3DOverlay::render(RenderArgs* args) { _webSurface->getRootItem()->setProperty("scriptURL", _scriptURL); currentContext->makeCurrent(currentSurface); + auto selfOverlayID = getOverlayID(); + std::weak_ptr weakSelf = std::dynamic_pointer_cast(qApp->getOverlays().getOverlay(selfOverlayID)); auto forwardPointerEvent = [=](OverlayID overlayID, const PointerEvent& event) { - if (overlayID == getOverlayID()) { - handlePointerEvent(event); + auto self = weakSelf.lock(); + if (self && overlayID == selfOverlayID) { + self->handlePointerEvent(event); } }; - _mousePressConnection = connect(&(qApp->getOverlays()), &Overlays::mousePressOnOverlay, forwardPointerEvent); - _mouseReleaseConnection = connect(&(qApp->getOverlays()), &Overlays::mouseReleaseOnOverlay, forwardPointerEvent); - _mouseMoveConnection = connect(&(qApp->getOverlays()), &Overlays::mouseMoveOnOverlay, forwardPointerEvent); - _hoverLeaveConnection = connect(&(qApp->getOverlays()), &Overlays::hoverLeaveOverlay, - [=](OverlayID overlayID, const PointerEvent& event) { - if (this->_pressed && this->getOverlayID() == overlayID) { - // If the user mouses off the overlay while the button is down, simulate a touch end. - QTouchEvent::TouchPoint point; - point.setId(event.getID()); - point.setState(Qt::TouchPointReleased); - glm::vec2 windowPos = event.getPos2D() * (METERS_TO_INCHES * _dpi); - QPointF windowPoint(windowPos.x, windowPos.y); - point.setScenePos(windowPoint); - point.setPos(windowPoint); - QList touchPoints; - touchPoints.push_back(point); - QTouchEvent* touchEvent = new QTouchEvent(QEvent::TouchEnd, nullptr, Qt::NoModifier, Qt::TouchPointReleased, - touchPoints); - touchEvent->setWindow(_webSurface->getWindow()); - touchEvent->setDevice(&_touchDevice); - touchEvent->setTarget(_webSurface->getRootItem()); - QCoreApplication::postEvent(_webSurface->getWindow(), touchEvent); + _mousePressConnection = connect(&(qApp->getOverlays()), &Overlays::mousePressOnOverlay, this, forwardPointerEvent, Qt::DirectConnection); + _mouseReleaseConnection = connect(&(qApp->getOverlays()), &Overlays::mouseReleaseOnOverlay, this, forwardPointerEvent, Qt::DirectConnection); + _mouseMoveConnection = connect(&(qApp->getOverlays()), &Overlays::mouseMoveOnOverlay, this, forwardPointerEvent, Qt::DirectConnection); + _hoverLeaveConnection = connect(&(qApp->getOverlays()), &Overlays::hoverLeaveOverlay, this, [=](OverlayID overlayID, const PointerEvent& event) { + auto self = weakSelf.lock(); + if (!self) { + return; } - }); + if (self->_pressed && overlayID == selfOverlayID) { + PointerEvent endEvent(PointerEvent::Release, event.getID(), event.getPos2D(), event.getPos3D(), event.getNormal(), event.getDirection(), + event.getButton(), event.getButtons(), event.getKeyboardModifiers()); + forwardPointerEvent(overlayID, event); + } + }, Qt::DirectConnection); _emitScriptEventConnection = connect(this, &Web3DOverlay::scriptEventReceived, _webSurface.data(), &OffscreenQmlSurface::emitScriptEvent); _webEventReceivedConnection = connect(_webSurface.data(), &OffscreenQmlSurface::webEventReceived, this, &Web3DOverlay::webEventReceived); + } else { + if (_currentMaxFPS != _desiredMaxFPS) { + setMaxFPS(_desiredMaxFPS); + } + } + + if (_mayNeedResize) { + _mayNeedResize = false; + _webSurface->resize(QSize(_resolution.x, _resolution.y)); } vec2 halfSize = getSize() / 2.0f; @@ -251,7 +297,7 @@ void Web3DOverlay::render(RenderArgs* args) { if (!_texture) { auto webSurface = _webSurface; - _texture = gpu::TexturePointer(gpu::Texture::createExternal2D(OffscreenQmlSurface::getDiscardLambda())); + _texture = gpu::TexturePointer(gpu::Texture::createExternal(OffscreenQmlSurface::getDiscardLambda())); _texture->setSource(__FUNCTION__); } OffscreenQmlSurface::TextureAndFence newTextureAndFence; @@ -298,6 +344,14 @@ void Web3DOverlay::setProxyWindow(QWindow* proxyWindow) { } void Web3DOverlay::handlePointerEvent(const PointerEvent& event) { + if (_inputMode == Touch) { + handlePointerEventAsTouch(event); + } else { + handlePointerEventAsMouse(event); + } +} + +void Web3DOverlay::handlePointerEventAsTouch(const PointerEvent& event) { if (!_webSurface) { return; } @@ -305,35 +359,63 @@ void Web3DOverlay::handlePointerEvent(const PointerEvent& event) { glm::vec2 windowPos = event.getPos2D() * (METERS_TO_INCHES * _dpi); QPointF windowPoint(windowPos.x, windowPos.y); - if (event.getType() == PointerEvent::Move) { - // Forward a mouse move event to the Web surface. - QMouseEvent* mouseEvent = new QMouseEvent(QEvent::MouseMove, windowPoint, windowPoint, windowPoint, Qt::NoButton, - Qt::NoButton, Qt::NoModifier); - QCoreApplication::postEvent(_webSurface->getWindow(), mouseEvent); - } - - if (event.getType() == PointerEvent::Press) { + if (event.getType() == PointerEvent::Press && event.getButton() == PointerEvent::PrimaryButton) { this->_pressed = true; - } else if (event.getType() == PointerEvent::Release) { + } else if (event.getType() == PointerEvent::Release && event.getButton() == PointerEvent::PrimaryButton) { this->_pressed = false; } - QEvent::Type type; + QEvent::Type touchType; Qt::TouchPointState touchPointState; + QEvent::Type mouseType; + + Qt::MouseButton button = Qt::NoButton; + Qt::MouseButtons buttons = Qt::NoButton; + if (event.getButton() == PointerEvent::PrimaryButton) { + button = Qt::LeftButton; + } + if (event.getButtons() & PointerEvent::PrimaryButton) { + buttons |= Qt::LeftButton; + } + switch (event.getType()) { case PointerEvent::Press: - type = QEvent::TouchBegin; + touchType = QEvent::TouchBegin; touchPointState = Qt::TouchPointPressed; + mouseType = QEvent::MouseButtonPress; break; case PointerEvent::Release: - type = QEvent::TouchEnd; + touchType = QEvent::TouchEnd; touchPointState = Qt::TouchPointReleased; + mouseType = QEvent::MouseButtonRelease; break; case PointerEvent::Move: - default: - type = QEvent::TouchUpdate; + touchType = QEvent::TouchUpdate; touchPointState = Qt::TouchPointMoved; + mouseType = QEvent::MouseMove; + + if (((event.getButtons() & PointerEvent::PrimaryButton) > 0) != this->_pressed) { + // Mouse was pressed/released while off the overlay; convert touch and mouse events to press/release to reflect + // current mouse/touch status. + this->_pressed = !this->_pressed; + if (this->_pressed) { + touchType = QEvent::TouchBegin; + touchPointState = Qt::TouchPointPressed; + mouseType = QEvent::MouseButtonPress; + + } else { + touchType = QEvent::TouchEnd; + touchPointState = Qt::TouchPointReleased; + mouseType = QEvent::MouseButtonRelease; + + } + button = Qt::LeftButton; + buttons |= Qt::LeftButton; + } + break; + default: + return; } QTouchEvent::TouchPoint point; @@ -344,14 +426,63 @@ void Web3DOverlay::handlePointerEvent(const PointerEvent& event) { QList touchPoints; touchPoints.push_back(point); - QTouchEvent* touchEvent = new QTouchEvent(type); + QTouchEvent* touchEvent = new QTouchEvent(touchType, &_touchDevice, event.getKeyboardModifiers()); touchEvent->setWindow(_webSurface->getWindow()); - touchEvent->setDevice(&_touchDevice); touchEvent->setTarget(_webSurface->getRootItem()); touchEvent->setTouchPoints(touchPoints); touchEvent->setTouchPointStates(touchPointState); QCoreApplication::postEvent(_webSurface->getWindow(), touchEvent); + + // Send mouse events to the Web surface so that HTML dialog elements work with mouse press and hover. + // FIXME: Scroll bar dragging is a bit unstable in the tablet (content can jump up and down at times). + // This may be improved in Qt 5.8. Release notes: "Cleaned up touch and mouse event delivery". + + QMouseEvent* mouseEvent = new QMouseEvent(mouseType, windowPoint, windowPoint, windowPoint, button, buttons, Qt::NoModifier); + QCoreApplication::postEvent(_webSurface->getWindow(), mouseEvent); +} + +void Web3DOverlay::handlePointerEventAsMouse(const PointerEvent& event) { + if (!_webSurface) { + return; + } + + glm::vec2 windowPos = event.getPos2D() * (METERS_TO_INCHES * _dpi); + QPointF windowPoint(windowPos.x, windowPos.y); + + if (event.getType() == PointerEvent::Press) { + this->_pressed = true; + } else if (event.getType() == PointerEvent::Release) { + this->_pressed = false; + } + + Qt::MouseButtons buttons = Qt::NoButton; + if (event.getButtons() & PointerEvent::PrimaryButton) { + buttons |= Qt::LeftButton; + } + + Qt::MouseButton button = Qt::NoButton; + if (event.getButton() == PointerEvent::PrimaryButton) { + button = Qt::LeftButton; + } + + QEvent::Type type; + switch (event.getType()) { + case PointerEvent::Press: + type = QEvent::MouseButtonPress; + break; + case PointerEvent::Release: + type = QEvent::MouseButtonRelease; + break; + case PointerEvent::Move: + type = QEvent::MouseMove; + break; + default: + return; + } + + QMouseEvent* mouseEvent = new QMouseEvent(type, windowPoint, windowPoint, windowPoint, button, buttons, Qt::NoModifier); + QCoreApplication::postEvent(_webSurface->getWindow(), mouseEvent); } void Web3DOverlay::setProperties(const QVariantMap& properties) { @@ -387,10 +518,27 @@ void Web3DOverlay::setProperties(const QVariantMap& properties) { _dpi = dpi.toFloat(); } + auto maxFPS = properties["maxFPS"]; + if (maxFPS.isValid()) { + _desiredMaxFPS = maxFPS.toInt(); + } + auto showKeyboardFocusHighlight = properties["showKeyboardFocusHighlight"]; if (showKeyboardFocusHighlight.isValid()) { _showKeyboardFocusHighlight = showKeyboardFocusHighlight.toBool(); } + + auto inputModeValue = properties["inputMode"]; + if (inputModeValue.isValid()) { + QString inputModeStr = inputModeValue.toString(); + if (inputModeStr == "Mouse") { + _inputMode = Mouse; + } else { + _inputMode = Touch; + } + } + + _mayNeedResize = true; } QVariant Web3DOverlay::getProperty(const QString& property) { @@ -406,9 +554,20 @@ QVariant Web3DOverlay::getProperty(const QString& property) { if (property == "dpi") { return _dpi; } + if (property == "maxFPS") { + return _desiredMaxFPS; + } if (property == "showKeyboardFocusHighlight") { return _showKeyboardFocusHighlight; } + + if (property == "inputMode") { + if (_inputMode == Mouse) { + return QVariant("Mouse"); + } else { + return QVariant("Touch"); + } + } return Billboard3DOverlay::getProperty(property); } diff --git a/interface/src/ui/overlays/Web3DOverlay.h b/interface/src/ui/overlays/Web3DOverlay.h index 2b9686919d..1e3706ed25 100644 --- a/interface/src/ui/overlays/Web3DOverlay.h +++ b/interface/src/ui/overlays/Web3DOverlay.h @@ -31,6 +31,7 @@ public: QString pickURL(); void loadSourceURL(); + void setMaxFPS(uint8_t maxFPS); virtual void render(RenderArgs* args) override; virtual const render::ShapeKey getShapeKey() override; @@ -39,6 +40,8 @@ public: QObject* getEventHandler(); void setProxyWindow(QWindow* proxyWindow); void handlePointerEvent(const PointerEvent& event); + void handlePointerEventAsTouch(const PointerEvent& event); + void handlePointerEventAsMouse(const PointerEvent& event); // setters void setURL(const QString& url); @@ -54,6 +57,11 @@ public: virtual Web3DOverlay* createClone() const override; + enum InputMode { + Touch, + Mouse + }; + public slots: void emitScriptEvent(const QVariant& scriptMessage); @@ -62,6 +70,7 @@ signals: void webEventReceived(const QVariant& message); private: + InputMode _inputMode { Touch }; QSharedPointer _webSurface; QMetaObject::Connection _connection; gpu::TexturePointer _texture; @@ -75,6 +84,11 @@ private: bool _pressed{ false }; QTouchDevice _touchDevice; + uint8_t _desiredMaxFPS { 10 }; + uint8_t _currentMaxFPS { 0 }; + + bool _mayNeedResize { false }; + QMetaObject::Connection _mousePressConnection; QMetaObject::Connection _mouseReleaseConnection; QMetaObject::Connection _mouseMoveConnection; diff --git a/interface/ui/scriptEditorWidget.ui b/interface/ui/scriptEditorWidget.ui deleted file mode 100644 index e2e538a595..0000000000 --- a/interface/ui/scriptEditorWidget.ui +++ /dev/null @@ -1,142 +0,0 @@ - - - ScriptEditorWidget - - - - 0 - 0 - 691 - 549 - - - - - 0 - 0 - - - - - 690 - 328 - - - - font-family: Helvetica, Arial, sans-serif; - - - QDockWidget::DockWidgetFloatable|QDockWidget::DockWidgetMovable - - - Qt::NoDockWidgetArea - - - Edit Script - - - - - 0 - - - 0 - - - 0 - - - 0 - - - 0 - - - - - - Courier - -1 - 50 - false - false - - - - font: 16px "Courier"; - - - - - - - 0 - - - 0 - - - 0 - - - - - - 0 - 0 - - - - font: 13px "Helvetica","Arial","sans-serif"; - - - Debug Log: - - - - - - - - Helvetica,Arial,sans-serif - -1 - 50 - false - false - - - - font: 13px "Helvetica","Arial","sans-serif"; - - - Run on the fly (Careful: Any valid change made to the code will run immediately) - - - - - - - Clear - - - - 16 - 16 - - - - - - - - - - - - ScriptEditBox - QTextEdit -
ui/ScriptEditBox.h
-
-
- -
diff --git a/interface/ui/scriptEditorWindow.ui b/interface/ui/scriptEditorWindow.ui deleted file mode 100644 index 1e50aaef0b..0000000000 --- a/interface/ui/scriptEditorWindow.ui +++ /dev/null @@ -1,324 +0,0 @@ - - - ScriptEditorWindow - - - Qt::NonModal - - - - 0 - 0 - 780 - 717 - - - - - 400 - 250 - - - - Script Editor - - - font-family: Helvetica, Arial, sans-serif; - - - - 0 - - - 0 - - - 0 - - - 0 - - - 0 - - - - - 3 - - - QLayout::SetNoConstraint - - - 0 - - - 0 - - - - - New Script (Ctrl+N) - - - New - - - - 32 - 32 - - - - - - - - - 30 - 0 - - - - - 25 - 0 - - - - Load Script (Ctrl+O) - - - Load - - - - 32 - 32 - - - - false - - - QToolButton::MenuButtonPopup - - - Qt::ToolButtonIconOnly - - - - - - - - 30 - 0 - - - - - 32 - 0 - - - - Qt::NoFocus - - - Qt::NoContextMenu - - - Save Script (Ctrl+S) - - - Save - - - - 32 - 32 - - - - 316 - - - QToolButton::MenuButtonPopup - - - - - - - Toggle Run Script (F5) - - - Run/Stop - - - - 32 - 32 - - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - font: 13px "Helvetica","Arial","sans-serif"; - - - Automatically reload externally changed files - - - - - - - - - true - - - - 250 - 80 - - - - QTabWidget::West - - - QTabWidget::Triangular - - - -1 - - - Qt::ElideNone - - - true - - - true - - - - - - - - - saveButton - clicked() - ScriptEditorWindow - saveScriptClicked() - - - 236 - 10 - - - 199 - 264 - - - - - toggleRunButton - clicked() - ScriptEditorWindow - toggleRunScriptClicked() - - - 330 - 10 - - - 199 - 264 - - - - - newButton - clicked() - ScriptEditorWindow - newScriptClicked() - - - 58 - 10 - - - 199 - 264 - - - - - loadButton - clicked() - ScriptEditorWindow - loadScriptClicked() - - - 85 - 10 - - - 199 - 264 - - - - - tabWidget - currentChanged(int) - ScriptEditorWindow - tabSwitched(int) - - - 352 - 360 - - - 352 - 340 - - - - - tabWidget - tabCloseRequested(int) - ScriptEditorWindow - tabCloseRequested(int) - - - 352 - 360 - - - 352 - 340 - - - - - diff --git a/libraries/animation/src/AnimBlendLinear.cpp b/libraries/animation/src/AnimBlendLinear.cpp index 52c440a14e..936126bf52 100644 --- a/libraries/animation/src/AnimBlendLinear.cpp +++ b/libraries/animation/src/AnimBlendLinear.cpp @@ -24,7 +24,7 @@ AnimBlendLinear::~AnimBlendLinear() { } -const AnimPoseVec& AnimBlendLinear::evaluate(const AnimVariantMap& animVars, float dt, Triggers& triggersOut) { +const AnimPoseVec& AnimBlendLinear::evaluate(const AnimVariantMap& animVars, const AnimContext& context, float dt, Triggers& triggersOut) { _alpha = animVars.lookup(_alphaVar, _alpha); @@ -33,7 +33,7 @@ const AnimPoseVec& AnimBlendLinear::evaluate(const AnimVariantMap& animVars, flo pose = AnimPose::identity; } } else if (_children.size() == 1) { - _poses = _children[0]->evaluate(animVars, dt, triggersOut); + _poses = _children[0]->evaluate(animVars, context, dt, triggersOut); } else { float clampedAlpha = glm::clamp(_alpha, 0.0f, (float)(_children.size() - 1)); @@ -41,7 +41,7 @@ const AnimPoseVec& AnimBlendLinear::evaluate(const AnimVariantMap& animVars, flo size_t nextPoseIndex = glm::ceil(clampedAlpha); float alpha = glm::fract(clampedAlpha); - evaluateAndBlendChildren(animVars, triggersOut, alpha, prevPoseIndex, nextPoseIndex, dt); + evaluateAndBlendChildren(animVars, context, triggersOut, alpha, prevPoseIndex, nextPoseIndex, dt); } return _poses; } @@ -51,15 +51,15 @@ const AnimPoseVec& AnimBlendLinear::getPosesInternal() const { return _poses; } -void AnimBlendLinear::evaluateAndBlendChildren(const AnimVariantMap& animVars, Triggers& triggersOut, float alpha, +void AnimBlendLinear::evaluateAndBlendChildren(const AnimVariantMap& animVars, const AnimContext& context, Triggers& triggersOut, float alpha, size_t prevPoseIndex, size_t nextPoseIndex, float dt) { if (prevPoseIndex == nextPoseIndex) { // this can happen if alpha is on an integer boundary - _poses = _children[prevPoseIndex]->evaluate(animVars, dt, triggersOut); + _poses = _children[prevPoseIndex]->evaluate(animVars, context, dt, triggersOut); } else { // need to eval and blend between two children. - auto prevPoses = _children[prevPoseIndex]->evaluate(animVars, dt, triggersOut); - auto nextPoses = _children[nextPoseIndex]->evaluate(animVars, dt, triggersOut); + auto prevPoses = _children[prevPoseIndex]->evaluate(animVars, context, dt, triggersOut); + auto nextPoses = _children[nextPoseIndex]->evaluate(animVars, context, dt, triggersOut); if (prevPoses.size() > 0 && prevPoses.size() == nextPoses.size()) { _poses.resize(prevPoses.size()); diff --git a/libraries/animation/src/AnimBlendLinear.h b/libraries/animation/src/AnimBlendLinear.h index 2478f9b473..0dae6aabdb 100644 --- a/libraries/animation/src/AnimBlendLinear.h +++ b/libraries/animation/src/AnimBlendLinear.h @@ -30,7 +30,7 @@ public: AnimBlendLinear(const QString& id, float alpha); virtual ~AnimBlendLinear() override; - virtual const AnimPoseVec& evaluate(const AnimVariantMap& animVars, float dt, Triggers& triggersOut) override; + virtual const AnimPoseVec& evaluate(const AnimVariantMap& animVars, const AnimContext& context, float dt, Triggers& triggersOut) override; void setAlphaVar(const QString& alphaVar) { _alphaVar = alphaVar; } @@ -38,7 +38,7 @@ protected: // for AnimDebugDraw rendering virtual const AnimPoseVec& getPosesInternal() const override; - void evaluateAndBlendChildren(const AnimVariantMap& animVars, Triggers& triggersOut, float alpha, + void evaluateAndBlendChildren(const AnimVariantMap& animVars, const AnimContext& context, Triggers& triggersOut, float alpha, size_t prevPoseIndex, size_t nextPoseIndex, float dt); AnimPoseVec _poses; diff --git a/libraries/animation/src/AnimBlendLinearMove.cpp b/libraries/animation/src/AnimBlendLinearMove.cpp index 609b464512..40fbb5a6f7 100644 --- a/libraries/animation/src/AnimBlendLinearMove.cpp +++ b/libraries/animation/src/AnimBlendLinearMove.cpp @@ -26,7 +26,7 @@ AnimBlendLinearMove::~AnimBlendLinearMove() { } -const AnimPoseVec& AnimBlendLinearMove::evaluate(const AnimVariantMap& animVars, float dt, Triggers& triggersOut) { +const AnimPoseVec& AnimBlendLinearMove::evaluate(const AnimVariantMap& animVars, const AnimContext& context, float dt, Triggers& triggersOut) { assert(_children.size() == _characteristicSpeeds.size()); @@ -43,7 +43,7 @@ const AnimPoseVec& AnimBlendLinearMove::evaluate(const AnimVariantMap& animVars, const int nextPoseIndex = 0; float prevDeltaTime, nextDeltaTime; setFrameAndPhase(dt, alpha, prevPoseIndex, nextPoseIndex, &prevDeltaTime, &nextDeltaTime, triggersOut); - evaluateAndBlendChildren(animVars, triggersOut, alpha, prevPoseIndex, nextPoseIndex, prevDeltaTime, nextDeltaTime); + evaluateAndBlendChildren(animVars, context, triggersOut, alpha, prevPoseIndex, nextPoseIndex, prevDeltaTime, nextDeltaTime); } else { auto clampedAlpha = glm::clamp(_alpha, 0.0f, (float)(_children.size() - 1)); @@ -52,7 +52,7 @@ const AnimPoseVec& AnimBlendLinearMove::evaluate(const AnimVariantMap& animVars, auto alpha = glm::fract(clampedAlpha); float prevDeltaTime, nextDeltaTime; setFrameAndPhase(dt, alpha, prevPoseIndex, nextPoseIndex, &prevDeltaTime, &nextDeltaTime, triggersOut); - evaluateAndBlendChildren(animVars, triggersOut, alpha, prevPoseIndex, nextPoseIndex, prevDeltaTime, nextDeltaTime); + evaluateAndBlendChildren(animVars, context, triggersOut, alpha, prevPoseIndex, nextPoseIndex, prevDeltaTime, nextDeltaTime); } return _poses; } @@ -62,16 +62,16 @@ const AnimPoseVec& AnimBlendLinearMove::getPosesInternal() const { return _poses; } -void AnimBlendLinearMove::evaluateAndBlendChildren(const AnimVariantMap& animVars, Triggers& triggersOut, float alpha, +void AnimBlendLinearMove::evaluateAndBlendChildren(const AnimVariantMap& animVars, const AnimContext& context, Triggers& triggersOut, float alpha, size_t prevPoseIndex, size_t nextPoseIndex, float prevDeltaTime, float nextDeltaTime) { if (prevPoseIndex == nextPoseIndex) { // this can happen if alpha is on an integer boundary - _poses = _children[prevPoseIndex]->evaluate(animVars, prevDeltaTime, triggersOut); + _poses = _children[prevPoseIndex]->evaluate(animVars, context, prevDeltaTime, triggersOut); } else { // need to eval and blend between two children. - auto prevPoses = _children[prevPoseIndex]->evaluate(animVars, prevDeltaTime, triggersOut); - auto nextPoses = _children[nextPoseIndex]->evaluate(animVars, nextDeltaTime, triggersOut); + auto prevPoses = _children[prevPoseIndex]->evaluate(animVars, context, prevDeltaTime, triggersOut); + auto nextPoses = _children[nextPoseIndex]->evaluate(animVars, context, nextDeltaTime, triggersOut); if (prevPoses.size() > 0 && prevPoses.size() == nextPoses.size()) { _poses.resize(prevPoses.size()); diff --git a/libraries/animation/src/AnimBlendLinearMove.h b/libraries/animation/src/AnimBlendLinearMove.h index 4e04ce29cb..083858f873 100644 --- a/libraries/animation/src/AnimBlendLinearMove.h +++ b/libraries/animation/src/AnimBlendLinearMove.h @@ -39,7 +39,7 @@ public: AnimBlendLinearMove(const QString& id, float alpha, float desiredSpeed, const std::vector& characteristicSpeeds); virtual ~AnimBlendLinearMove() override; - virtual const AnimPoseVec& evaluate(const AnimVariantMap& animVars, float dt, Triggers& triggersOut) override; + virtual const AnimPoseVec& evaluate(const AnimVariantMap& animVars, const AnimContext& context, float dt, Triggers& triggersOut) override; void setAlphaVar(const QString& alphaVar) { _alphaVar = alphaVar; } void setDesiredSpeedVar(const QString& desiredSpeedVar) { _desiredSpeedVar = desiredSpeedVar; } @@ -48,7 +48,7 @@ protected: // for AnimDebugDraw rendering virtual const AnimPoseVec& getPosesInternal() const override; - void evaluateAndBlendChildren(const AnimVariantMap& animVars, Triggers& triggersOut, float alpha, + void evaluateAndBlendChildren(const AnimVariantMap& animVars, const AnimContext& context, Triggers& triggersOut, float alpha, size_t prevPoseIndex, size_t nextPoseIndex, float prevDeltaTime, float nextDeltaTime); diff --git a/libraries/animation/src/AnimClip.cpp b/libraries/animation/src/AnimClip.cpp index cb1d058576..1118e21c91 100644 --- a/libraries/animation/src/AnimClip.cpp +++ b/libraries/animation/src/AnimClip.cpp @@ -31,7 +31,7 @@ AnimClip::~AnimClip() { } -const AnimPoseVec& AnimClip::evaluate(const AnimVariantMap& animVars, float dt, Triggers& triggersOut) { +const AnimPoseVec& AnimClip::evaluate(const AnimVariantMap& animVars, const AnimContext& context, float dt, Triggers& triggersOut) { // lookup parameters from animVars, using current instance variables as defaults. _startFrame = animVars.lookup(_startFrameVar, _startFrame); diff --git a/libraries/animation/src/AnimClip.h b/libraries/animation/src/AnimClip.h index 7989f6d172..c7e7ebf3ee 100644 --- a/libraries/animation/src/AnimClip.h +++ b/libraries/animation/src/AnimClip.h @@ -30,7 +30,7 @@ public: AnimClip(const QString& id, const QString& url, float startFrame, float endFrame, float timeScale, bool loopFlag, bool mirrorFlag); virtual ~AnimClip() override; - virtual const AnimPoseVec& evaluate(const AnimVariantMap& animVars, float dt, Triggers& triggersOut) override; + virtual const AnimPoseVec& evaluate(const AnimVariantMap& animVars, const AnimContext& context, float dt, Triggers& triggersOut) override; void setStartFrameVar(const QString& startFrameVar) { _startFrameVar = startFrameVar; } void setEndFrameVar(const QString& endFrameVar) { _endFrameVar = endFrameVar; } diff --git a/libraries/animation/src/AnimContext.cpp b/libraries/animation/src/AnimContext.cpp new file mode 100644 index 0000000000..c8d3e7bcda --- /dev/null +++ b/libraries/animation/src/AnimContext.cpp @@ -0,0 +1,16 @@ +// +// AnimContext.cpp +// +// Created by Anthony J. Thibault on 9/19/16. +// Copyright (c) 2016 High Fidelity, Inc. All rights reserved. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include "AnimContext.h" + +AnimContext::AnimContext(bool enableDebugDrawIKTargets, const glm::mat4& geometryToRigMatrix) : + _enableDebugDrawIKTargets(enableDebugDrawIKTargets), + _geometryToRigMatrix(geometryToRigMatrix) { +} diff --git a/libraries/animation/src/AnimContext.h b/libraries/animation/src/AnimContext.h new file mode 100644 index 0000000000..3170911e14 --- /dev/null +++ b/libraries/animation/src/AnimContext.h @@ -0,0 +1,30 @@ +// +// AnimContext.h +// +// Created by Anthony J. Thibault on 9/19/16. +// Copyright (c) 2016 High Fidelity, Inc. All rights reserved. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_AnimContext_h +#define hifi_AnimContext_h + +#include +#include + +class AnimContext { +public: + AnimContext(bool enableDebugDrawIKTargets, const glm::mat4& geometryToRigMatrix); + + bool getEnableDebugDrawIKTargets() const { return _enableDebugDrawIKTargets; } + const glm::mat4& getGeometryToRigMatrix() const { return _geometryToRigMatrix; } + +protected: + + bool _enableDebugDrawIKTargets { false }; + glm::mat4 _geometryToRigMatrix; +}; + +#endif // hifi_AnimContext_h diff --git a/libraries/animation/src/AnimInverseKinematics.cpp b/libraries/animation/src/AnimInverseKinematics.cpp index 173af3fdf6..2c9376d591 100644 --- a/libraries/animation/src/AnimInverseKinematics.cpp +++ b/libraries/animation/src/AnimInverseKinematics.cpp @@ -14,6 +14,8 @@ #include #include #include +#include +#include "Rig.h" #include "ElbowConstraint.h" #include "SwingTwistConstraint.h" @@ -378,14 +380,14 @@ int AnimInverseKinematics::solveTargetWithCCD(const IKTarget& target, AnimPoseVe } //virtual -const AnimPoseVec& AnimInverseKinematics::evaluate(const AnimVariantMap& animVars, float dt, AnimNode::Triggers& triggersOut) { +const AnimPoseVec& AnimInverseKinematics::evaluate(const AnimVariantMap& animVars, const AnimContext& context, float dt, AnimNode::Triggers& triggersOut) { // don't call this function, call overlay() instead assert(false); return _relativePoses; } //virtual -const AnimPoseVec& AnimInverseKinematics::overlay(const AnimVariantMap& animVars, float dt, Triggers& triggersOut, const AnimPoseVec& underPoses) { +const AnimPoseVec& AnimInverseKinematics::overlay(const AnimVariantMap& animVars, const AnimContext& context, float dt, Triggers& triggersOut, const AnimPoseVec& underPoses) { const float MAX_OVERLAY_DT = 1.0f / 30.0f; // what to clamp delta-time to in AnimInverseKinematics::overlay if (dt > MAX_OVERLAY_DT) { @@ -439,6 +441,28 @@ const AnimPoseVec& AnimInverseKinematics::overlay(const AnimVariantMap& animVars computeTargets(animVars, targets, underPoses); } + // debug render ik targets + if (context.getEnableDebugDrawIKTargets()) { + const vec4 WHITE(1.0f); + glm::mat4 rigToAvatarMat = createMatFromQuatAndPos(Quaternions::Y_180, glm::vec3()); + + for (auto& target : targets) { + glm::mat4 geomTargetMat = createMatFromQuatAndPos(target.getRotation(), target.getTranslation()); + glm::mat4 avatarTargetMat = rigToAvatarMat * context.getGeometryToRigMatrix() * geomTargetMat; + + QString name = QString("ikTarget%1").arg(target.getIndex()); + DebugDraw::getInstance().addMyAvatarMarker(name, glmExtractRotation(avatarTargetMat), extractTranslation(avatarTargetMat), WHITE); + } + } else if (context.getEnableDebugDrawIKTargets() != _previousEnableDebugIKTargets) { + // remove markers if they were added last frame. + for (auto& target : targets) { + QString name = QString("ikTarget%1").arg(target.getIndex()); + DebugDraw::getInstance().removeMyAvatarMarker(name); + } + } + + _previousEnableDebugIKTargets = context.getEnableDebugDrawIKTargets(); + if (targets.empty()) { // no IK targets but still need to enforce constraints std::map::iterator constraintItr = _constraints.begin(); @@ -488,13 +512,7 @@ const AnimPoseVec& AnimInverseKinematics::overlay(const AnimVariantMap& animVars // measure new _hipsOffset for next frame // by looking for discrepancies between where a targeted endEffector is // and where it wants to be (after IK solutions are done) - - // use weighted average between HMD and other targets - float HMD_WEIGHT = 10.0f; - float OTHER_WEIGHT = 1.0f; - float totalWeight = 0.0f; - - glm::vec3 additionalHipsOffset = Vectors::ZERO; + glm::vec3 newHipsOffset = Vectors::ZERO; for (auto& target: targets) { int targetIndex = target.getIndex(); if (targetIndex == _headIndex && _headIndex != -1) { @@ -505,42 +523,34 @@ const AnimPoseVec& AnimInverseKinematics::overlay(const AnimVariantMap& animVars glm::vec3 under = _skeleton->getAbsolutePose(_headIndex, underPoses).trans(); glm::vec3 actual = _skeleton->getAbsolutePose(_headIndex, _relativePoses).trans(); const float HEAD_OFFSET_SLAVE_FACTOR = 0.65f; - additionalHipsOffset += (OTHER_WEIGHT * HEAD_OFFSET_SLAVE_FACTOR) * (under- actual); - totalWeight += OTHER_WEIGHT; + newHipsOffset += HEAD_OFFSET_SLAVE_FACTOR * (actual - under); } else if (target.getType() == IKTarget::Type::HmdHead) { + // we want to shift the hips to bring the head to its designated position glm::vec3 actual = _skeleton->getAbsolutePose(_headIndex, _relativePoses).trans(); - glm::vec3 thisOffset = target.getTranslation() - actual; - glm::vec3 futureHipsOffset = _hipsOffset + thisOffset; - if (glm::length(glm::vec2(futureHipsOffset.x, futureHipsOffset.z)) < _maxHipsOffsetLength) { - // it is imperative to shift the hips and bring the head to its designated position - // so we slam newHipsOffset here and ignore all other targets - additionalHipsOffset = futureHipsOffset - _hipsOffset; - totalWeight = 0.0f; - break; - } else { - additionalHipsOffset += HMD_WEIGHT * (target.getTranslation() - actual); - totalWeight += HMD_WEIGHT; - } + _hipsOffset += target.getTranslation() - actual; + // and ignore all other targets + newHipsOffset = _hipsOffset; + break; + } else if (target.getType() == IKTarget::Type::RotationAndPosition) { + glm::vec3 actualPosition = _skeleton->getAbsolutePose(targetIndex, _relativePoses).trans(); + glm::vec3 targetPosition = target.getTranslation(); + newHipsOffset += targetPosition - actualPosition; + + // Add downward pressure on the hips + newHipsOffset *= 0.95f; + newHipsOffset -= 1.0f; } } else if (target.getType() == IKTarget::Type::RotationAndPosition) { glm::vec3 actualPosition = _skeleton->getAbsolutePose(targetIndex, _relativePoses).trans(); glm::vec3 targetPosition = target.getTranslation(); - additionalHipsOffset += OTHER_WEIGHT * (targetPosition - actualPosition); - totalWeight += OTHER_WEIGHT; + newHipsOffset += targetPosition - actualPosition; } } - if (totalWeight > 1.0f) { - additionalHipsOffset /= totalWeight; - } - - // Add downward pressure on the hips - additionalHipsOffset *= 0.95f; - additionalHipsOffset -= 1.0f; // smooth transitions by relaxing _hipsOffset toward the new value const float HIPS_OFFSET_SLAVE_TIMESCALE = 0.10f; float tau = dt < HIPS_OFFSET_SLAVE_TIMESCALE ? dt / HIPS_OFFSET_SLAVE_TIMESCALE : 1.0f; - _hipsOffset += additionalHipsOffset * tau; + _hipsOffset += (newHipsOffset - _hipsOffset) * tau; // clamp the hips offset float hipsOffsetLength = glm::length(_hipsOffset); diff --git a/libraries/animation/src/AnimInverseKinematics.h b/libraries/animation/src/AnimInverseKinematics.h index 892a5616b2..366e5f765e 100644 --- a/libraries/animation/src/AnimInverseKinematics.h +++ b/libraries/animation/src/AnimInverseKinematics.h @@ -34,8 +34,8 @@ public: void setTargetVars(const QString& jointName, const QString& positionVar, const QString& rotationVar, const QString& typeVar); - virtual const AnimPoseVec& evaluate(const AnimVariantMap& animVars, float dt, AnimNode::Triggers& triggersOut) override; - virtual const AnimPoseVec& overlay(const AnimVariantMap& animVars, float dt, Triggers& triggersOut, const AnimPoseVec& underPoses) override; + virtual const AnimPoseVec& evaluate(const AnimVariantMap& animVars, const AnimContext& context, float dt, AnimNode::Triggers& triggersOut) override; + virtual const AnimPoseVec& overlay(const AnimVariantMap& animVars, const AnimContext& context, float dt, Triggers& triggersOut, const AnimPoseVec& underPoses) override; void clearIKJointLimitHistory(); @@ -97,6 +97,7 @@ protected: int _maxTargetIndex { 0 }; float _maxErrorOnLastSolve { FLT_MAX }; + bool _previousEnableDebugIKTargets { false }; }; #endif // hifi_AnimInverseKinematics_h diff --git a/libraries/animation/src/AnimManipulator.cpp b/libraries/animation/src/AnimManipulator.cpp index f2bd2d983a..111501898a 100644 --- a/libraries/animation/src/AnimManipulator.cpp +++ b/libraries/animation/src/AnimManipulator.cpp @@ -22,11 +22,11 @@ AnimManipulator::~AnimManipulator() { } -const AnimPoseVec& AnimManipulator::evaluate(const AnimVariantMap& animVars, float dt, Triggers& triggersOut) { - return overlay(animVars, dt, triggersOut, _skeleton->getRelativeBindPoses()); +const AnimPoseVec& AnimManipulator::evaluate(const AnimVariantMap& animVars, const AnimContext& context, float dt, Triggers& triggersOut) { + return overlay(animVars, context, dt, triggersOut, _skeleton->getRelativeBindPoses()); } -const AnimPoseVec& AnimManipulator::overlay(const AnimVariantMap& animVars, float dt, Triggers& triggersOut, const AnimPoseVec& underPoses) { +const AnimPoseVec& AnimManipulator::overlay(const AnimVariantMap& animVars, const AnimContext& context, float dt, Triggers& triggersOut, const AnimPoseVec& underPoses) { _alpha = animVars.lookup(_alphaVar, _alpha); _poses = underPoses; diff --git a/libraries/animation/src/AnimManipulator.h b/libraries/animation/src/AnimManipulator.h index 8534b9c269..26f50a7dd9 100644 --- a/libraries/animation/src/AnimManipulator.h +++ b/libraries/animation/src/AnimManipulator.h @@ -22,8 +22,8 @@ public: AnimManipulator(const QString& id, float alpha); virtual ~AnimManipulator() override; - virtual const AnimPoseVec& evaluate(const AnimVariantMap& animVars, float dt, Triggers& triggersOut) override; - virtual const AnimPoseVec& overlay(const AnimVariantMap& animVars, float dt, Triggers& triggersOut, const AnimPoseVec& underPoses) override; + virtual const AnimPoseVec& evaluate(const AnimVariantMap& animVars, const AnimContext& context, float dt, Triggers& triggersOut) override; + virtual const AnimPoseVec& overlay(const AnimVariantMap& animVars, const AnimContext& context, float dt, Triggers& triggersOut, const AnimPoseVec& underPoses) override; void setAlphaVar(const QString& alphaVar) { _alphaVar = alphaVar; } diff --git a/libraries/animation/src/AnimNode.h b/libraries/animation/src/AnimNode.h index 23f2e1c7b3..10db38f42e 100644 --- a/libraries/animation/src/AnimNode.h +++ b/libraries/animation/src/AnimNode.h @@ -20,6 +20,7 @@ #include "AnimSkeleton.h" #include "AnimVariant.h" +#include "AnimContext.h" class QJsonObject; @@ -72,9 +73,10 @@ public: AnimSkeleton::ConstPointer getSkeleton() const { return _skeleton; } - virtual const AnimPoseVec& evaluate(const AnimVariantMap& animVars, float dt, Triggers& triggersOut) = 0; - virtual const AnimPoseVec& overlay(const AnimVariantMap& animVars, float dt, Triggers& triggersOut, const AnimPoseVec& underPoses) { - return evaluate(animVars, dt, triggersOut); + virtual const AnimPoseVec& evaluate(const AnimVariantMap& animVars, const AnimContext& context, float dt, Triggers& triggersOut) = 0; + virtual const AnimPoseVec& overlay(const AnimVariantMap& animVars, const AnimContext& context, float dt, Triggers& triggersOut, + const AnimPoseVec& underPoses) { + return evaluate(animVars, context, dt, triggersOut); } void setCurrentFrame(float frame); diff --git a/libraries/animation/src/AnimOverlay.cpp b/libraries/animation/src/AnimOverlay.cpp index 8f60b972ce..dbc635af66 100644 --- a/libraries/animation/src/AnimOverlay.cpp +++ b/libraries/animation/src/AnimOverlay.cpp @@ -39,7 +39,7 @@ void AnimOverlay::buildBoneSet(BoneSet boneSet) { } } -const AnimPoseVec& AnimOverlay::evaluate(const AnimVariantMap& animVars, float dt, Triggers& triggersOut) { +const AnimPoseVec& AnimOverlay::evaluate(const AnimVariantMap& animVars, const AnimContext& context, float dt, Triggers& triggersOut) { // lookup parameters from animVars, using current instance variables as defaults. // NOTE: switching bonesets can be an expensive operation, let's try to avoid it. @@ -51,8 +51,8 @@ const AnimPoseVec& AnimOverlay::evaluate(const AnimVariantMap& animVars, float d _alpha = animVars.lookup(_alphaVar, _alpha); if (_children.size() >= 2) { - auto& underPoses = _children[1]->evaluate(animVars, dt, triggersOut); - auto& overPoses = _children[0]->overlay(animVars, dt, triggersOut, underPoses); + auto& underPoses = _children[1]->evaluate(animVars, context, dt, triggersOut); + auto& overPoses = _children[0]->overlay(animVars, context, dt, triggersOut, underPoses); if (underPoses.size() > 0 && underPoses.size() == overPoses.size()) { _poses.resize(underPoses.size()); diff --git a/libraries/animation/src/AnimOverlay.h b/libraries/animation/src/AnimOverlay.h index eda8847d40..2f34c07309 100644 --- a/libraries/animation/src/AnimOverlay.h +++ b/libraries/animation/src/AnimOverlay.h @@ -43,7 +43,7 @@ public: AnimOverlay(const QString& id, BoneSet boneSet, float alpha); virtual ~AnimOverlay() override; - virtual const AnimPoseVec& evaluate(const AnimVariantMap& animVars, float dt, Triggers& triggersOut) override; + virtual const AnimPoseVec& evaluate(const AnimVariantMap& animVars, const AnimContext& context, float dt, Triggers& triggersOut) override; void setBoneSetVar(const QString& boneSetVar) { _boneSetVar = boneSetVar; } void setAlphaVar(const QString& alphaVar) { _alphaVar = alphaVar; } diff --git a/libraries/animation/src/AnimPose.cpp b/libraries/animation/src/AnimPose.cpp index 5638cacabc..e1c8528e0b 100644 --- a/libraries/animation/src/AnimPose.cpp +++ b/libraries/animation/src/AnimPose.cpp @@ -50,15 +50,9 @@ glm::vec3 AnimPose::xformVector(const glm::vec3& rhs) const { } AnimPose AnimPose::operator*(const AnimPose& rhs) const { -#if (GLM_ARCH & GLM_ARCH_SSE2) && !(defined Q_OS_MAC) glm::mat4 result; - glm::mat4 lhsMat = *this; - glm::mat4 rhsMat = rhs; - glm_mat4_mul((glm_vec4*)&lhsMat, (glm_vec4*)&rhsMat, (glm_vec4*)&result); + glm_mat4u_mul(*this, rhs, result); return AnimPose(result); -#else - return AnimPose(static_cast(*this) * static_cast(rhs)); -#endif } AnimPose AnimPose::inverse() const { diff --git a/libraries/animation/src/AnimStateMachine.cpp b/libraries/animation/src/AnimStateMachine.cpp index 41d8a94b0a..4e86b92c0b 100644 --- a/libraries/animation/src/AnimStateMachine.cpp +++ b/libraries/animation/src/AnimStateMachine.cpp @@ -21,7 +21,7 @@ AnimStateMachine::~AnimStateMachine() { } -const AnimPoseVec& AnimStateMachine::evaluate(const AnimVariantMap& animVars, float dt, Triggers& triggersOut) { +const AnimPoseVec& AnimStateMachine::evaluate(const AnimVariantMap& animVars, const AnimContext& context, float dt, Triggers& triggersOut) { QString desiredStateID = animVars.lookup(_currentStateVar, _currentState->getID()); if (_currentState->getID() != desiredStateID) { @@ -29,7 +29,7 @@ const AnimPoseVec& AnimStateMachine::evaluate(const AnimVariantMap& animVars, fl bool foundState = false; for (auto& state : _states) { if (state->getID() == desiredStateID) { - switchState(animVars, state); + switchState(animVars, context, state); foundState = true; break; } @@ -42,7 +42,7 @@ const AnimPoseVec& AnimStateMachine::evaluate(const AnimVariantMap& animVars, fl // evaluate currentState transitions auto desiredState = evaluateTransitions(animVars); if (desiredState != _currentState) { - switchState(animVars, desiredState); + switchState(animVars, context, desiredState); } assert(_currentState); @@ -62,7 +62,7 @@ const AnimPoseVec& AnimStateMachine::evaluate(const AnimVariantMap& animVars, fl } else if (_interpType == InterpType::SnapshotPrev) { // interp between the prev snapshot and evaluated next target. // this is useful for interping into a blend - localNextPoses = currentStateNode->evaluate(animVars, dt, triggersOut); + localNextPoses = currentStateNode->evaluate(animVars, context, dt, triggersOut); prevPoses = &_prevPoses; nextPoses = &localNextPoses; } else { @@ -79,7 +79,7 @@ const AnimPoseVec& AnimStateMachine::evaluate(const AnimVariantMap& animVars, fl } } if (!_duringInterp) { - _poses = currentStateNode->evaluate(animVars, dt, triggersOut); + _poses = currentStateNode->evaluate(animVars, context, dt, triggersOut); } return _poses; } @@ -92,7 +92,7 @@ void AnimStateMachine::addState(State::Pointer state) { _states.push_back(state); } -void AnimStateMachine::switchState(const AnimVariantMap& animVars, State::Pointer desiredState) { +void AnimStateMachine::switchState(const AnimVariantMap& animVars, const AnimContext& context, State::Pointer desiredState) { const float FRAMES_PER_SECOND = 30.0f; @@ -114,7 +114,7 @@ void AnimStateMachine::switchState(const AnimVariantMap& animVars, State::Pointe _prevPoses = _poses; // snapshot next pose at the target frame. nextStateNode->setCurrentFrame(desiredState->_interpTarget); - _nextPoses = nextStateNode->evaluate(animVars, dt, triggers); + _nextPoses = nextStateNode->evaluate(animVars, context, dt, triggers); } else if (_interpType == InterpType::SnapshotPrev) { // snapshot previoius pose _prevPoses = _poses; diff --git a/libraries/animation/src/AnimStateMachine.h b/libraries/animation/src/AnimStateMachine.h index d92b94d1b5..711326a9ae 100644 --- a/libraries/animation/src/AnimStateMachine.h +++ b/libraries/animation/src/AnimStateMachine.h @@ -113,7 +113,7 @@ public: explicit AnimStateMachine(const QString& id); virtual ~AnimStateMachine() override; - virtual const AnimPoseVec& evaluate(const AnimVariantMap& animVars, float dt, Triggers& triggersOut) override; + virtual const AnimPoseVec& evaluate(const AnimVariantMap& animVars, const AnimContext& context, float dt, Triggers& triggersOut) override; void setCurrentStateVar(QString& currentStateVar) { _currentStateVar = currentStateVar; } @@ -123,7 +123,7 @@ protected: void addState(State::Pointer state); - void switchState(const AnimVariantMap& animVars, State::Pointer desiredState); + void switchState(const AnimVariantMap& animVars, const AnimContext& context, State::Pointer desiredState); State::Pointer evaluateTransitions(const AnimVariantMap& animVars) const; // for AnimDebugDraw rendering diff --git a/libraries/animation/src/AnimationCache.cpp b/libraries/animation/src/AnimationCache.cpp index 6594482085..23c9d1d0b5 100644 --- a/libraries/animation/src/AnimationCache.cpp +++ b/libraries/animation/src/AnimationCache.cpp @@ -74,7 +74,7 @@ void AnimationReader::run() { // Parse the FBX directly from the QNetworkReply FBXGeometry::Pointer fbxgeo; if (_url.path().toLower().endsWith(".fbx")) { - fbxgeo.reset(readFBX(_data, QVariantHash(), _url.path())); + fbxgeo.reset(readFBX(_data, QVariantHash(), _url)); } else { QString errorStr("usupported format"); emit onError(299, errorStr); diff --git a/libraries/animation/src/Rig.cpp b/libraries/animation/src/Rig.cpp index 84e34adec7..fb0867e2de 100644 --- a/libraries/animation/src/Rig.cpp +++ b/libraries/animation/src/Rig.cpp @@ -558,15 +558,15 @@ static const std::vector LATERAL_SPEEDS = { 0.2f, 0.65f }; // m/s void Rig::computeMotionAnimationState(float deltaTime, const glm::vec3& worldPosition, const glm::vec3& worldVelocity, const glm::quat& worldRotation, CharacterControllerState ccState) { - glm::vec3 front = worldRotation * IDENTITY_FRONT; + glm::vec3 forward = worldRotation * IDENTITY_FORWARD; glm::vec3 workingVelocity = worldVelocity; { glm::vec3 localVel = glm::inverse(worldRotation) * workingVelocity; - float forwardSpeed = glm::dot(localVel, IDENTITY_FRONT); + float forwardSpeed = glm::dot(localVel, IDENTITY_FORWARD); float lateralSpeed = glm::dot(localVel, IDENTITY_RIGHT); - float turningSpeed = glm::orientedAngle(front, _lastFront, IDENTITY_UP) / deltaTime; + float turningSpeed = glm::orientedAngle(forward, _lastForward, IDENTITY_UP) / deltaTime; // filter speeds using a simple moving average. _averageForwardSpeed.updateAverage(forwardSpeed); @@ -852,7 +852,7 @@ void Rig::computeMotionAnimationState(float deltaTime, const glm::vec3& worldPos _lastEnableInverseKinematics = _enableInverseKinematics; } - _lastFront = front; + _lastForward = forward; _lastPosition = worldPosition; _lastVelocity = workingVelocity; } @@ -950,9 +950,11 @@ void Rig::updateAnimations(float deltaTime, glm::mat4 rootTransform) { updateAnimationStateHandlers(); _animVars.setRigToGeometryTransform(_rigToGeometryTransform); + AnimContext context(_enableDebugDrawIKTargets, getGeometryToRigTransform()); + // evaluate the animation AnimNode::Triggers triggersOut; - _internalPoseSet._relativePoses = _animNode->evaluate(_animVars, deltaTime, triggersOut); + _internalPoseSet._relativePoses = _animNode->evaluate(_animVars, context, deltaTime, triggersOut); if ((int)_internalPoseSet._relativePoses.size() != _animSkeleton->getNumJoints()) { // animations haven't fully loaded yet. _internalPoseSet._relativePoses = _animSkeleton->getRelativeDefaultPoses(); @@ -1144,9 +1146,8 @@ void Rig::updateEyeJoint(int index, const glm::vec3& modelTranslation, const glm } } -void Rig::updateFromHandParameters(const HandParameters& params, float dt) { +void Rig::updateFromHandAndFeetParameters(const HandAndFeetParameters& params, float dt) { if (_animSkeleton && _animNode) { - const float HAND_RADIUS = 0.05f; int hipsIndex = indexOfJoint("Hips"); glm::vec3 hipsTrans; @@ -1195,6 +1196,27 @@ void Rig::updateFromHandParameters(const HandParameters& params, float dt) { _animVars.unset("rightHandRotation"); _animVars.set("rightHandType", (int)IKTarget::Type::HipsRelativeRotationAndPosition); } + + if (params.isLeftFootEnabled) { + _animVars.set("leftFootPosition", params.leftFootPosition); + _animVars.set("leftFootRotation", params.leftFootOrientation); + _animVars.set("leftFootType", (int)IKTarget::Type::RotationAndPosition); + } else { + _animVars.unset("leftFootPosition"); + _animVars.unset("leftFootRotation"); + _animVars.set("leftFootType", (int)IKTarget::Type::RotationAndPosition); + } + + if (params.isRightFootEnabled) { + _animVars.set("rightFootPosition", params.rightFootPosition); + _animVars.set("rightFootRotation", params.rightFootOrientation); + _animVars.set("rightFootType", (int)IKTarget::Type::RotationAndPosition); + } else { + _animVars.unset("rightFootPosition"); + _animVars.unset("rightFootRotation"); + _animVars.set("rightFootType", (int)IKTarget::Type::RotationAndPosition); + } + } } @@ -1310,17 +1332,18 @@ void Rig::copyJointsFromJointData(const QVector& jointDataVec) { if (!_animSkeleton) { return; } - if (jointDataVec.size() != (int)_internalPoseSet._relativePoses.size()) { - // animations haven't fully loaded yet. - _internalPoseSet._relativePoses = _animSkeleton->getRelativeDefaultPoses(); + int numJoints = jointDataVec.size(); + const AnimPoseVec& absoluteDefaultPoses = _animSkeleton->getAbsoluteDefaultPoses(); + if (numJoints != (int)absoluteDefaultPoses.size()) { + // jointData is incompatible + return; } // make a vector of rotations in absolute-geometry-frame - const AnimPoseVec& absoluteDefaultPoses = _animSkeleton->getAbsoluteDefaultPoses(); std::vector rotations; - rotations.reserve(absoluteDefaultPoses.size()); + rotations.reserve(numJoints); const glm::quat rigToGeometryRot(glmExtractRotation(_rigToGeometryTransform)); - for (int i = 0; i < jointDataVec.size(); i++) { + for (int i = 0; i < numJoints; i++) { const JointData& data = jointDataVec.at(i); if (data.rotationSet) { // JointData rotations are in absolute rig-frame so we rotate them to absolute geometry-frame @@ -1334,8 +1357,11 @@ void Rig::copyJointsFromJointData(const QVector& jointDataVec) { _animSkeleton->convertAbsoluteRotationsToRelative(rotations); // store new relative poses + if (numJoints != (int)_internalPoseSet._relativePoses.size()) { + _internalPoseSet._relativePoses = _animSkeleton->getRelativeDefaultPoses(); + } const AnimPoseVec& relativeDefaultPoses = _animSkeleton->getRelativeDefaultPoses(); - for (int i = 0; i < jointDataVec.size(); i++) { + for (int i = 0; i < numJoints; i++) { const JointData& data = jointDataVec.at(i); _internalPoseSet._relativePoses[i].scale() = Vectors::ONE; _internalPoseSet._relativePoses[i].rot() = rotations[i]; @@ -1346,8 +1372,13 @@ void Rig::copyJointsFromJointData(const QVector& jointDataVec) { _internalPoseSet._relativePoses[i].trans() = relativeDefaultPoses[i].trans(); } } +} + +void Rig::computeExternalPoses(const glm::mat4& modelOffsetMat) { + _modelOffset = AnimPose(modelOffsetMat); + _geometryToRigTransform = _modelOffset * _geometryOffset; + _rigToGeometryTransform = glm::inverse(_geometryToRigTransform); - // build absolute poses and copy to externalPoseSet buildAbsoluteRigPoses(_internalPoseSet._relativePoses, _internalPoseSet._absolutePoses); QWriteLocker writeLock(&_externalPoseSetLock); _externalPoseSet = _internalPoseSet; @@ -1418,9 +1449,10 @@ void Rig::computeAvatarBoundingCapsule( // call overlay twice: once to verify AnimPoseVec joints and again to do the IK AnimNode::Triggers triggersOut; + AnimContext context(false, glm::mat4()); float dt = 1.0f; // the value of this does not matter - ikNode.overlay(animVars, dt, triggersOut, _animSkeleton->getRelativeBindPoses()); - AnimPoseVec finalPoses = ikNode.overlay(animVars, dt, triggersOut, _animSkeleton->getRelativeBindPoses()); + ikNode.overlay(animVars, context, dt, triggersOut, _animSkeleton->getRelativeBindPoses()); + AnimPoseVec finalPoses = ikNode.overlay(animVars, context, dt, triggersOut, _animSkeleton->getRelativeBindPoses()); // convert relative poses to absolute _animSkeleton->convertRelativePosesToAbsolute(finalPoses); diff --git a/libraries/animation/src/Rig.h b/libraries/animation/src/Rig.h index f1c87d0d3e..2cd20c2704 100644 --- a/libraries/animation/src/Rig.h +++ b/libraries/animation/src/Rig.h @@ -60,7 +60,7 @@ public: int rightEyeJointIndex = -1; }; - struct HandParameters { + struct HandAndFeetParameters { bool isLeftEnabled; bool isRightEnabled; float bodyCapsuleRadius; @@ -70,6 +70,13 @@ public: glm::quat leftOrientation = glm::quat(); // rig space (z forward) glm::vec3 rightPosition = glm::vec3(); // rig space glm::quat rightOrientation = glm::quat(); // rig space (z forward) + + bool isLeftFootEnabled; + bool isRightFootEnabled; + glm::vec3 leftFootPosition = glm::vec3(); // rig space + glm::quat leftFootOrientation = glm::quat(); // rig space (z forward) + glm::vec3 rightFootPosition = glm::vec3(); // rig space + glm::quat rightFootOrientation = glm::quat(); // rig space (z forward) }; enum class CharacterControllerState { @@ -185,7 +192,7 @@ public: void updateFromHeadParameters(const HeadParameters& params, float dt); void updateFromEyeParameters(const EyeParameters& params); - void updateFromHandParameters(const HandParameters& params, float dt); + void updateFromHandAndFeetParameters(const HandAndFeetParameters& params, float dt); void initAnimGraph(const QUrl& url); @@ -210,6 +217,7 @@ public: void copyJointsIntoJointData(QVector& jointDataVec) const; void copyJointsFromJointData(const QVector& jointDataVec); + void computeExternalPoses(const glm::mat4& modelOffsetMat); void computeAvatarBoundingCapsule(const FBXGeometry& geometry, float& radiusOut, float& heightOut, glm::vec3& offsetOut) const; @@ -218,6 +226,8 @@ public: const glm::mat4& getGeometryToRigTransform() const { return _geometryToRigTransform; } + void setEnableDebugDrawIKTargets(bool enableDebugDrawIKTargets) { _enableDebugDrawIKTargets = enableDebugDrawIKTargets; } + signals: void onLoadComplete(); @@ -266,7 +276,7 @@ protected: int _rightElbowJointIndex { -1 }; int _rightShoulderJointIndex { -1 }; - glm::vec3 _lastFront; + glm::vec3 _lastForward; glm::vec3 _lastPosition; glm::vec3 _lastVelocity; @@ -323,7 +333,8 @@ protected: mutable uint32_t _jointNameWarningCount { 0 }; float _maxHipsOffsetLength { 1.0f }; - float _maxErrorOnLastSolve { 0.0f }; + + bool _enableDebugDrawIKTargets { false }; private: QMap _stateHandlers; diff --git a/libraries/audio-client/src/AudioClient.cpp b/libraries/audio-client/src/AudioClient.cpp index bd141cfb12..4a2de0a64b 100644 --- a/libraries/audio-client/src/AudioClient.cpp +++ b/libraries/audio-client/src/AudioClient.cpp @@ -160,7 +160,7 @@ AudioClient::AudioClient() : _loopbackAudioOutput(NULL), _loopbackOutputDevice(NULL), _inputRingBuffer(0), - _localInjectorsStream(0), + _localInjectorsStream(0, 1), _receivedAudioStream(RECEIVED_AUDIO_STREAM_CAPACITY_FRAMES), _isStereoInput(false), _outputStarveDetectionStartTimeMsec(0), @@ -184,7 +184,6 @@ AudioClient::AudioClient() : _outgoingAvatarAudioSequenceNumber(0), _audioOutputIODevice(_localInjectorsStream, _receivedAudioStream, this), _stats(&_receivedAudioStream), - _inputGate(), _positionGetter(DEFAULT_POSITION_GETTER), _orientationGetter(DEFAULT_ORIENTATION_GETTER) { // avoid putting a lock in the device callback @@ -608,6 +607,13 @@ void AudioClient::handleAudioEnvironmentDataPacket(QSharedPointer message) { + + if (message->getType() == PacketType::SilentAudioFrame) { + _silentInbound.increment(); + } else { + _audioInbound.increment(); + } + auto nodeList = DependencyManager::get(); nodeList->flagTimeForConnectionStep(LimitedNodeList::ConnectionStep::ReceiveFirstAudioPacket); @@ -964,14 +970,87 @@ void AudioClient::handleLocalEchoAndReverb(QByteArray& inputByteArray) { } } -void AudioClient::handleAudioInput() { +void AudioClient::handleAudioInput(QByteArray& audioBuffer) { + if (_muted) { + _lastInputLoudness = 0.0f; + _timeSinceLastClip = 0.0f; + } else { + int16_t* samples = reinterpret_cast(audioBuffer.data()); + int numSamples = audioBuffer.size() / sizeof(AudioConstants::SAMPLE_SIZE); + bool didClip = false; + + bool shouldRemoveDCOffset = !_isPlayingBackRecording && !_isStereoInput; + if (shouldRemoveDCOffset) { + _noiseGate.removeDCOffset(samples, numSamples); + } + + bool shouldNoiseGate = (_isPlayingBackRecording || !_isStereoInput) && _isNoiseGateEnabled; + if (shouldNoiseGate) { + _noiseGate.gateSamples(samples, numSamples); + _lastInputLoudness = _noiseGate.getLastLoudness(); + didClip = _noiseGate.clippedInLastBlock(); + } else { + float loudness = 0.0f; + for (int i = 0; i < numSamples; ++i) { + int16_t sample = std::abs(samples[i]); + loudness += (float)sample; + didClip = didClip || + (sample > (AudioConstants::MAX_SAMPLE_VALUE * AudioNoiseGate::CLIPPING_THRESHOLD)); + } + _lastInputLoudness = fabs(loudness / numSamples); + } + + if (didClip) { + _timeSinceLastClip = 0.0f; + } else if (_timeSinceLastClip >= 0.0f) { + _timeSinceLastClip += (float)numSamples / (float)AudioConstants::SAMPLE_RATE; + } + + emit inputReceived({ audioBuffer.data(), numSamples }); + + if (_noiseGate.openedInLastBlock()) { + emit noiseGateOpened(); + } else if (_noiseGate.closedInLastBlock()) { + emit noiseGateClosed(); + } + } + + // the codec needs a flush frame before sending silent packets, so + // do not send one if the gate closed in this block (eventually this can be crossfaded). + auto packetType = _shouldEchoToServer ? + PacketType::MicrophoneAudioWithEcho : PacketType::MicrophoneAudioNoEcho; + if (_lastInputLoudness == 0.0f && !_noiseGate.closedInLastBlock()) { + packetType = PacketType::SilentAudioFrame; + _silentOutbound.increment(); + } else { + _audioOutbound.increment(); + } + + Transform audioTransform; + audioTransform.setTranslation(_positionGetter()); + audioTransform.setRotation(_orientationGetter()); + + QByteArray encodedBuffer; + if (_encoder) { + _encoder->encode(audioBuffer, encodedBuffer); + } else { + encodedBuffer = audioBuffer; + } + + emitAudioPacket(encodedBuffer.data(), encodedBuffer.size(), _outgoingAvatarAudioSequenceNumber, + audioTransform, avatarBoundingBoxCorner, avatarBoundingBoxScale, + packetType, _selectedCodecName); + _stats.sentPacket(); +} + +void AudioClient::handleMicAudioInput() { if (!_inputDevice || _isPlayingBackRecording) { return; } // input samples required to produce exactly NETWORK_FRAME_SAMPLES of output - const int inputSamplesRequired = (_inputToNetworkResampler ? - _inputToNetworkResampler->getMinInput(AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL) : + const int inputSamplesRequired = (_inputToNetworkResampler ? + _inputToNetworkResampler->getMinInput(AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL) : AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL) * _inputFormat.channelCount(); const auto inputAudioSamples = std::unique_ptr(new int16_t[inputSamplesRequired]); @@ -994,107 +1073,27 @@ void AudioClient::handleAudioInput() { static int16_t networkAudioSamples[AudioConstants::NETWORK_FRAME_SAMPLES_STEREO]; while (_inputRingBuffer.samplesAvailable() >= inputSamplesRequired) { - - if (!_muted) { - - - // Increment the time since the last clip - if (_timeSinceLastClip >= 0.0f) { - _timeSinceLastClip += (float)numNetworkSamples / (float)AudioConstants::SAMPLE_RATE; - } - + if (_muted) { + _inputRingBuffer.shiftReadPosition(inputSamplesRequired); + } else { _inputRingBuffer.readSamples(inputAudioSamples.get(), inputSamplesRequired); possibleResampling(_inputToNetworkResampler, inputAudioSamples.get(), networkAudioSamples, inputSamplesRequired, numNetworkSamples, _inputFormat.channelCount(), _desiredInputFormat.channelCount()); - - // Remove DC offset - if (!_isStereoInput) { - _inputGate.removeDCOffset(networkAudioSamples, numNetworkSamples); - } - - // only impose the noise gate and perform tone injection if we are sending mono audio - if (!_isStereoInput && _isNoiseGateEnabled) { - _inputGate.gateSamples(networkAudioSamples, numNetworkSamples); - - // if we performed the noise gate we can get values from it instead of enumerating the samples again - _lastInputLoudness = _inputGate.getLastLoudness(); - - if (_inputGate.clippedInLastFrame()) { - _timeSinceLastClip = 0.0f; - } - } else { - float loudness = 0.0f; - - for (int i = 0; i < numNetworkSamples; i++) { - int thisSample = std::abs(networkAudioSamples[i]); - loudness += (float)thisSample; - - if (thisSample > (AudioConstants::MAX_SAMPLE_VALUE * AudioNoiseGate::CLIPPING_THRESHOLD)) { - _timeSinceLastClip = 0.0f; - } - } - - _lastInputLoudness = fabs(loudness / numNetworkSamples); - } - - emit inputReceived({ reinterpret_cast(networkAudioSamples), numNetworkBytes }); - - } else { - // our input loudness is 0, since we're muted - _lastInputLoudness = 0; - _timeSinceLastClip = 0.0f; - - _inputRingBuffer.shiftReadPosition(inputSamplesRequired); } - - auto packetType = _shouldEchoToServer ? - PacketType::MicrophoneAudioWithEcho : PacketType::MicrophoneAudioNoEcho; - - if (_lastInputLoudness == 0) { - packetType = PacketType::SilentAudioFrame; - } - Transform audioTransform; - audioTransform.setTranslation(_positionGetter()); - audioTransform.setRotation(_orientationGetter()); - // FIXME find a way to properly handle both playback audio and user audio concurrently - - QByteArray decodedBuffer(reinterpret_cast(networkAudioSamples), numNetworkBytes); - QByteArray encodedBuffer; - if (_encoder) { - _encoder->encode(decodedBuffer, encodedBuffer); - } else { - encodedBuffer = decodedBuffer; - } - - emitAudioPacket(encodedBuffer.constData(), encodedBuffer.size(), _outgoingAvatarAudioSequenceNumber, - audioTransform, avatarBoundingBoxCorner, avatarBoundingBoxScale, - packetType, _selectedCodecName); - _stats.sentPacket(); - int bytesInInputRingBuffer = _inputRingBuffer.samplesAvailable() * AudioConstants::SAMPLE_SIZE; float msecsInInputRingBuffer = bytesInInputRingBuffer / (float)(_inputFormat.bytesForDuration(USECS_PER_MSEC)); _stats.updateInputMsUnplayed(msecsInInputRingBuffer); + + QByteArray audioBuffer(reinterpret_cast(networkAudioSamples), numNetworkBytes); + handleAudioInput(audioBuffer); } } void AudioClient::handleRecordedAudioInput(const QByteArray& audio) { - Transform audioTransform; - audioTransform.setTranslation(_positionGetter()); - audioTransform.setRotation(_orientationGetter()); - - QByteArray encodedBuffer; - if (_encoder) { - _encoder->encode(audio, encodedBuffer); - } else { - encodedBuffer = audio; - } - - // FIXME check a flag to see if we should echo audio? - emitAudioPacket(encodedBuffer.data(), encodedBuffer.size(), _outgoingAvatarAudioSequenceNumber, - audioTransform, avatarBoundingBoxCorner, avatarBoundingBoxScale, - PacketType::MicrophoneAudioWithEcho, _selectedCodecName); + QByteArray audioBuffer(audio); + handleAudioInput(audioBuffer); } void AudioClient::prepareLocalAudioInjectors() { @@ -1408,7 +1407,7 @@ bool AudioClient::switchInputToAudioDevice(const QAudioDeviceInfo& inputDeviceIn lock.unlock(); if (_inputDevice) { - connect(_inputDevice, SIGNAL(readyRead()), this, SLOT(handleAudioInput())); + connect(_inputDevice, SIGNAL(readyRead()), this, SLOT(handleMicAudioInput())); supportedFormat = true; } else { qCDebug(audioclient) << "Error starting audio input -" << _audioInput->error(); @@ -1514,12 +1513,39 @@ bool AudioClient::switchOutputToAudioDevice(const QAudioDeviceInfo& outputDevice // setup our general output device for audio-mixer audio _audioOutput = new QAudioOutput(outputDeviceInfo, _outputFormat, this); - int osDefaultBufferSize = _audioOutput->bufferSize(); int deviceChannelCount = _outputFormat.channelCount(); - int deviceFrameSize = (AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL * deviceChannelCount * _outputFormat.sampleRate()) / _desiredOutputFormat.sampleRate(); - int requestedSize = _sessionOutputBufferSizeFrames * deviceFrameSize * AudioConstants::SAMPLE_SIZE; + int frameSize = (AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL * deviceChannelCount * _outputFormat.sampleRate()) / _desiredOutputFormat.sampleRate(); + int requestedSize = _sessionOutputBufferSizeFrames * frameSize * AudioConstants::SAMPLE_SIZE; _audioOutput->setBufferSize(requestedSize); + // initialize mix buffers on the _audioOutput thread to avoid races + connect(_audioOutput, &QAudioOutput::stateChanged, [&, frameSize, requestedSize](QAudio::State state) { + if (state == QAudio::ActiveState) { + // restrict device callback to _outputPeriod samples + _outputPeriod = (_audioOutput->periodSize() / AudioConstants::SAMPLE_SIZE) * 2; + _outputMixBuffer = new float[_outputPeriod]; + _outputScratchBuffer = new int16_t[_outputPeriod]; + + // size local output mix buffer based on resampled network frame size + _networkPeriod = _localToOutputResampler->getMaxOutput(AudioConstants::NETWORK_FRAME_SAMPLES_STEREO); + _localOutputMixBuffer = new float[_networkPeriod]; + int localPeriod = _outputPeriod * 2; + _localInjectorsStream.resizeForFrameSize(localPeriod); + + int bufferSize = _audioOutput->bufferSize(); + int bufferSamples = bufferSize / AudioConstants::SAMPLE_SIZE; + int bufferFrames = bufferSamples / (float)frameSize; + qCDebug(audioclient) << "frame (samples):" << frameSize; + qCDebug(audioclient) << "buffer (frames):" << bufferFrames; + qCDebug(audioclient) << "buffer (samples):" << bufferSamples; + qCDebug(audioclient) << "buffer (bytes):" << bufferSize; + qCDebug(audioclient) << "requested (bytes):" << requestedSize; + qCDebug(audioclient) << "period (samples):" << _outputPeriod; + qCDebug(audioclient) << "local buffer (samples):" << localPeriod; + + disconnect(_audioOutput, &QAudioOutput::stateChanged, 0, 0); + } + }); connect(_audioOutput, &QAudioOutput::notify, this, &AudioClient::outputNotify); _audioOutputIODevice.start(); @@ -1529,18 +1555,6 @@ bool AudioClient::switchOutputToAudioDevice(const QAudioDeviceInfo& outputDevice _audioOutput->start(&_audioOutputIODevice); lock.unlock(); - int periodSampleSize = _audioOutput->periodSize() / AudioConstants::SAMPLE_SIZE; - // device callback is not restricted to periodSampleSize, so double the mix/scratch buffer sizes - _outputPeriod = periodSampleSize * 2; - _outputMixBuffer = new float[_outputPeriod]; - _outputScratchBuffer = new int16_t[_outputPeriod]; - _localOutputMixBuffer = new float[_outputPeriod]; - _localInjectorsStream.resizeForFrameSize(_outputPeriod * 2); - - qCDebug(audioclient) << "Output Buffer capacity in frames: " << _audioOutput->bufferSize() / AudioConstants::SAMPLE_SIZE / (float)deviceFrameSize << - "requested bytes:" << requestedSize << "actual bytes:" << _audioOutput->bufferSize() << - "os default:" << osDefaultBufferSize << "period size:" << _audioOutput->periodSize(); - // setup a loopback audio output device _loopbackAudioOutput = new QAudioOutput(outputDeviceInfo, _outputFormat, this); diff --git a/libraries/audio-client/src/AudioClient.h b/libraries/audio-client/src/AudioClient.h index 5619051eaf..139749e8e8 100644 --- a/libraries/audio-client/src/AudioClient.h +++ b/libraries/audio-client/src/AudioClient.h @@ -45,11 +45,13 @@ #include #include #include +#include + +#include #include #include "AudioIOStats.h" -#include "AudioNoiseGate.h" #ifdef _WIN32 #pragma warning( push ) @@ -121,10 +123,17 @@ public: void negotiateAudioFormat(); void selectAudioFormat(const QString& selectedCodecName); + Q_INVOKABLE QString getSelectedAudioFormat() const { return _selectedCodecName; } + Q_INVOKABLE bool getNoiseGateOpen() const { return _noiseGate.isOpen(); } + Q_INVOKABLE float getSilentInboundPPS() const { return _silentInbound.rate(); } + Q_INVOKABLE float getAudioInboundPPS() const { return _audioInbound.rate(); } + Q_INVOKABLE float getSilentOutboundPPS() const { return _silentOutbound.rate(); } + Q_INVOKABLE float getAudioOutboundPPS() const { return _audioOutbound.rate(); } + const MixedProcessedAudioStream& getReceivedAudioStream() const { return _receivedAudioStream; } MixedProcessedAudioStream& getReceivedAudioStream() { return _receivedAudioStream; } - float getLastInputLoudness() const { return glm::max(_lastInputLoudness - _inputGate.getMeasuredFloor(), 0.0f); } + float getLastInputLoudness() const { return glm::max(_lastInputLoudness - _noiseGate.getMeasuredFloor(), 0.0f); } float getTimeSinceLastClip() const { return _timeSinceLastClip; } float getAudioAverageInputLoudness() const { return _lastInputLoudness; } @@ -171,7 +180,7 @@ public slots: void handleMismatchAudioFormat(SharedNodePointer node, const QString& currentCodec, const QString& recievedCodec); void sendDownstreamAudioStatsPacket() { _stats.publish(); } - void handleAudioInput(); + void handleMicAudioInput(); void handleRecordedAudioInput(const QByteArray& audio); void reset(); void audioMixerKilled(); @@ -218,6 +227,8 @@ signals: void inputReceived(const QByteArray& inputSamples); void outputBytesToNetwork(int numBytes); void inputBytesFromNetwork(int numBytes); + void noiseGateOpened(); + void noiseGateClosed(); void changeDevice(const QAudioDeviceInfo& outputDeviceInfo); void deviceChanged(); @@ -239,6 +250,7 @@ protected: private: void outputFormatChanged(); + void handleAudioInput(QByteArray& audioBuffer); bool mixLocalAudioInjectors(float* mixBuffer); float azimuthForSource(const glm::vec3& relativePosition); float gainForSource(float distance, float volume); @@ -328,6 +340,7 @@ private: int16_t _networkScratchBuffer[AudioConstants::NETWORK_FRAME_SAMPLES_AMBISONIC]; // for local audio (used by audio injectors thread) + int _networkPeriod { 0 }; float _localMixBuffer[AudioConstants::NETWORK_FRAME_SAMPLES_STEREO]; int16_t _localScratchBuffer[AudioConstants::NETWORK_FRAME_SAMPLES_AMBISONIC]; float* _localOutputMixBuffer { NULL }; @@ -360,7 +373,7 @@ private: AudioIOStats _stats; - AudioNoiseGate _inputGate; + AudioNoiseGate _noiseGate; AudioPositionGetter _positionGetter; AudioOrientationGetter _orientationGetter; @@ -382,6 +395,11 @@ private: Encoder* _encoder { nullptr }; // for outbound mic stream QThread* _checkDevicesThread { nullptr }; + + RateCounter<> _silentOutbound; + RateCounter<> _audioOutbound; + RateCounter<> _silentInbound; + RateCounter<> _audioInbound; }; diff --git a/libraries/audio/src/AudioInjector.h b/libraries/audio/src/AudioInjector.h index 2abc445034..7d57708738 100644 --- a/libraries/audio/src/AudioInjector.h +++ b/libraries/audio/src/AudioInjector.h @@ -121,5 +121,6 @@ private: friend class AudioInjectorManager; }; +Q_DECLARE_METATYPE(AudioInjector*) #endif // hifi_AudioInjector_h diff --git a/libraries/audio-client/src/AudioNoiseGate.cpp b/libraries/audio/src/AudioNoiseGate.cpp similarity index 57% rename from libraries/audio-client/src/AudioNoiseGate.cpp rename to libraries/audio/src/AudioNoiseGate.cpp index 8766a20cdf..604897af8a 100644 --- a/libraries/audio-client/src/AudioNoiseGate.cpp +++ b/libraries/audio/src/AudioNoiseGate.cpp @@ -1,6 +1,6 @@ // // AudioNoiseGate.cpp -// interface/src/audio +// libraries/audio // // Created by Stephen Birarda on 2014-12-16. // Copyright 2014 High Fidelity, Inc. @@ -9,35 +9,29 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // +#include "AudioNoiseGate.h" + #include #include -#include - -#include "AudioNoiseGate.h" +#include "AudioConstants.h" const float AudioNoiseGate::CLIPPING_THRESHOLD = 0.90f; AudioNoiseGate::AudioNoiseGate() : - _inputFrameCounter(0), _lastLoudness(0.0f), - _quietestFrame(std::numeric_limits::max()), - _loudestFrame(0.0f), - _didClipInLastFrame(false), + _didClipInLastBlock(false), _dcOffset(0.0f), _measuredFloor(0.0f), _sampleCounter(0), _isOpen(false), - _framesToClose(0) -{ - -} + _blocksToClose(0) {} void AudioNoiseGate::removeDCOffset(int16_t* samples, int numSamples) { // // DC Offset correction // - // Measure the DC offset over a trailing number of frames, and remove it from the input signal. + // Measure the DC offset over a trailing number of blocks, and remove it from the input signal. // This causes the noise background measurements and server muting to be more accurate. Many off-board // ADC's have a noticeable DC offset. // @@ -51,14 +45,13 @@ void AudioNoiseGate::removeDCOffset(int16_t* samples, int numSamples) { // Update measured DC offset measuredDcOffset /= numSamples; if (_dcOffset == 0.0f) { - // On first frame, copy over measured offset + // On first block, copy over measured offset _dcOffset = measuredDcOffset; } else { _dcOffset = DC_OFFSET_AVERAGING * _dcOffset + (1.0f - DC_OFFSET_AVERAGING) * measuredDcOffset; } } - void AudioNoiseGate::gateSamples(int16_t* samples, int numSamples) { // // Impose Noise Gate @@ -70,88 +63,102 @@ void AudioNoiseGate::gateSamples(int16_t* samples, int numSamples) { // // NOISE_GATE_HEIGHT: How loud you have to speak relative to noise background to open the gate. // Make this value lower for more sensitivity and less rejection of noise. - // NOISE_GATE_WIDTH: The number of samples in an audio frame for which the height must be exceeded + // NOISE_GATE_WIDTH: The number of samples in an audio block for which the height must be exceeded // to open the gate. - // NOISE_GATE_CLOSE_FRAME_DELAY: Once the noise is below the gate height for the frame, how many frames + // NOISE_GATE_CLOSE_BLOCK_DELAY: Once the noise is below the gate height for the block, how many blocks // will we wait before closing the gate. - // NOISE_GATE_FRAMES_TO_AVERAGE: How many audio frames should we average together to compute noise floor. + // NOISE_GATE_BLOCKS_TO_AVERAGE: How many audio blocks should we average together to compute noise floor. // More means better rejection but also can reject continuous things like singing. - // NUMBER_OF_NOISE_SAMPLE_FRAMES: How often should we re-evaluate the noise floor? - - + // NUMBER_OF_NOISE_SAMPLE_BLOCKS: How often should we re-evaluate the noise floor? + float loudness = 0; int thisSample = 0; int samplesOverNoiseGate = 0; - + const float NOISE_GATE_HEIGHT = 7.0f; const int NOISE_GATE_WIDTH = 5; - const int NOISE_GATE_CLOSE_FRAME_DELAY = 5; - const int NOISE_GATE_FRAMES_TO_AVERAGE = 5; + const int NOISE_GATE_CLOSE_BLOCK_DELAY = 5; + const int NOISE_GATE_BLOCKS_TO_AVERAGE = 5; // Check clipping, and check if should open noise gate - _didClipInLastFrame = false; - + _didClipInLastBlock = false; + for (int i = 0; i < numSamples; i++) { thisSample = std::abs(samples[i]); if (thisSample >= ((float) AudioConstants::MAX_SAMPLE_VALUE * CLIPPING_THRESHOLD)) { - _didClipInLastFrame = true; + _didClipInLastBlock = true; } - + loudness += thisSample; // Noise Reduction: Count peaks above the average loudness if (thisSample > (_measuredFloor * NOISE_GATE_HEIGHT)) { samplesOverNoiseGate++; } } - + _lastLoudness = fabs(loudness / numSamples); - - if (_quietestFrame > _lastLoudness) { - _quietestFrame = _lastLoudness; - } - if (_loudestFrame < _lastLoudness) { - _loudestFrame = _lastLoudness; - } - - const int FRAMES_FOR_NOISE_DETECTION = 400; - if (_inputFrameCounter++ > FRAMES_FOR_NOISE_DETECTION) { - _quietestFrame = std::numeric_limits::max(); - _loudestFrame = 0.0f; - _inputFrameCounter = 0; - } - + // If Noise Gate is enabled, check and turn the gate on and off - float averageOfAllSampleFrames = 0.0f; - _sampleFrames[_sampleCounter++] = _lastLoudness; - if (_sampleCounter == NUMBER_OF_NOISE_SAMPLE_FRAMES) { + float averageOfAllSampleBlocks = 0.0f; + _sampleBlocks[_sampleCounter++] = _lastLoudness; + if (_sampleCounter == NUMBER_OF_NOISE_SAMPLE_BLOCKS) { float smallestSample = std::numeric_limits::max(); - for (int i = 0; i <= NUMBER_OF_NOISE_SAMPLE_FRAMES - NOISE_GATE_FRAMES_TO_AVERAGE; i += NOISE_GATE_FRAMES_TO_AVERAGE) { + for (int i = 0; i <= NUMBER_OF_NOISE_SAMPLE_BLOCKS - NOISE_GATE_BLOCKS_TO_AVERAGE; i += NOISE_GATE_BLOCKS_TO_AVERAGE) { float thisAverage = 0.0f; - for (int j = i; j < i + NOISE_GATE_FRAMES_TO_AVERAGE; j++) { - thisAverage += _sampleFrames[j]; - averageOfAllSampleFrames += _sampleFrames[j]; + for (int j = i; j < i + NOISE_GATE_BLOCKS_TO_AVERAGE; j++) { + thisAverage += _sampleBlocks[j]; + averageOfAllSampleBlocks += _sampleBlocks[j]; } - thisAverage /= NOISE_GATE_FRAMES_TO_AVERAGE; - + thisAverage /= NOISE_GATE_BLOCKS_TO_AVERAGE; + if (thisAverage < smallestSample) { smallestSample = thisAverage; } } - averageOfAllSampleFrames /= NUMBER_OF_NOISE_SAMPLE_FRAMES; + averageOfAllSampleBlocks /= NUMBER_OF_NOISE_SAMPLE_BLOCKS; _measuredFloor = smallestSample; _sampleCounter = 0; - + } + + _closedInLastBlock = false; + _openedInLastBlock = false; + if (samplesOverNoiseGate > NOISE_GATE_WIDTH) { + _openedInLastBlock = !_isOpen; _isOpen = true; - _framesToClose = NOISE_GATE_CLOSE_FRAME_DELAY; + _blocksToClose = NOISE_GATE_CLOSE_BLOCK_DELAY; } else { - if (--_framesToClose == 0) { + if (--_blocksToClose == 0) { + _closedInLastBlock = _isOpen; _isOpen = false; } } if (!_isOpen) { - memset(samples, 0, numSamples * sizeof(int16_t)); + // First block after being closed gets faded to silence, we fade across + // the entire block on fading out. All subsequent blocks are muted by being slammed + // to zeros + if (_closedInLastBlock) { + float fadeSlope = (1.0f / numSamples); + for (int i = 0; i < numSamples; i++) { + float fadedSample = (1.0f - ((float)i * fadeSlope)) * (float)samples[i]; + samples[i] = (int16_t)fadedSample; + } + } else { + memset(samples, 0, numSamples * sizeof(int16_t)); + } _lastLoudness = 0; } + + if (_openedInLastBlock) { + // would be nice to do a little crossfade from silence, but we only want to fade + // across the first 1/10th of the block, because we don't want to miss early + // transients. + int fadeSamples = numSamples / 10; // fade over 1/10th of the samples + float fadeSlope = (1.0f / fadeSamples); + for (int i = 0; i < fadeSamples; i++) { + float fadedSample = (float)i * fadeSlope * (float)samples[i]; + samples[i] = (int16_t)fadedSample; + } + } } diff --git a/libraries/audio-client/src/AudioNoiseGate.h b/libraries/audio/src/AudioNoiseGate.h similarity index 60% rename from libraries/audio-client/src/AudioNoiseGate.h rename to libraries/audio/src/AudioNoiseGate.h index 8cb1155938..8430f120e5 100644 --- a/libraries/audio-client/src/AudioNoiseGate.h +++ b/libraries/audio/src/AudioNoiseGate.h @@ -1,6 +1,6 @@ // // AudioNoiseGate.h -// interface/src/audio +// libraries/audio // // Created by Stephen Birarda on 2014-12-16. // Copyright 2014 High Fidelity, Inc. @@ -14,33 +14,35 @@ #include -const int NUMBER_OF_NOISE_SAMPLE_FRAMES = 300; +const int NUMBER_OF_NOISE_SAMPLE_BLOCKS = 300; class AudioNoiseGate { public: AudioNoiseGate(); - + void gateSamples(int16_t* samples, int numSamples); void removeDCOffset(int16_t* samples, int numSamples); - - bool clippedInLastFrame() const { return _didClipInLastFrame; } + + bool clippedInLastBlock() const { return _didClipInLastBlock; } + bool closedInLastBlock() const { return _closedInLastBlock; } + bool openedInLastBlock() const { return _openedInLastBlock; } + bool isOpen() const { return _isOpen; } float getMeasuredFloor() const { return _measuredFloor; } float getLastLoudness() const { return _lastLoudness; } - + static const float CLIPPING_THRESHOLD; - + private: - int _inputFrameCounter; float _lastLoudness; - float _quietestFrame; - float _loudestFrame; - bool _didClipInLastFrame; + bool _didClipInLastBlock; float _dcOffset; float _measuredFloor; - float _sampleFrames[NUMBER_OF_NOISE_SAMPLE_FRAMES]; + float _sampleBlocks[NUMBER_OF_NOISE_SAMPLE_BLOCKS]; int _sampleCounter; bool _isOpen; - int _framesToClose; + bool _closedInLastBlock { false }; + bool _openedInLastBlock { false }; + int _blocksToClose; }; -#endif // hifi_AudioNoiseGate_h \ No newline at end of file +#endif // hifi_AudioNoiseGate_h diff --git a/libraries/audio/src/InboundAudioStream.cpp b/libraries/audio/src/InboundAudioStream.cpp index 57c344adaf..88ec7e7bc0 100644 --- a/libraries/audio/src/InboundAudioStream.cpp +++ b/libraries/audio/src/InboundAudioStream.cpp @@ -136,9 +136,10 @@ int InboundAudioStream::parseData(ReceivedMessage& message) { break; } case SequenceNumberStats::Early: { - // Packet is early; write droppable silent samples for each of the skipped packets. - // NOTE: we assume that each dropped packet contains the same number of samples - // as the packet we just received. + // Packet is early. Treat the packets as if all the packets between the last + // OnTime packet and this packet were lost. If we're using a codec this will + // also result in allowing the codec to interpolate lost data. Then + // fall through to the "on time" logic to actually handle this packet int packetsDropped = arrivalInfo._seqDiffFromExpected; lostAudioData(packetsDropped); @@ -147,7 +148,8 @@ int InboundAudioStream::parseData(ReceivedMessage& message) { case SequenceNumberStats::OnTime: { // Packet is on time; parse its data to the ringbuffer if (message.getType() == PacketType::SilentAudioFrame) { - // FIXME - Some codecs need to know about these silent frames... and can produce better output + // If we recieved a SilentAudioFrame from our sender, we might want to drop + // some of the samples in order to catch up to our desired jitter buffer size. writeDroppableSilentFrames(networkFrames); } else { // note: PCM and no codec are identical @@ -158,7 +160,12 @@ int InboundAudioStream::parseData(ReceivedMessage& message) { parseAudioData(message.getType(), afterProperties); } else { qDebug(audio) << "Codec mismatch: expected" << _selectedCodecName << "got" << codecInPacket << "writing silence"; - writeDroppableSilentFrames(networkFrames); + + // Since the data in the stream is using a codec that we aren't prepared for, + // we need to let the codec know that we don't have data for it, this will + // allow the codec to interpolate missing data and produce a fade to silence. + lostAudioData(1); + // inform others of the mismatch auto sendingNode = DependencyManager::get()->nodeWithUUID(message.getSourceID()); emit mismatchedAudioCodec(sendingNode, _selectedCodecName, codecInPacket); @@ -240,6 +247,25 @@ int InboundAudioStream::parseAudioData(PacketType type, const QByteArray& packet int InboundAudioStream::writeDroppableSilentFrames(int silentFrames) { + // We can't guarentee that all clients have faded the stream down + // to silence and encoded that silence before sending us a + // SilentAudioFrame. If the encoder has truncated the stream it will + // leave the decoder holding some unknown loud state. To handle this + // case we will call the decoder's lostFrame() method, which indicates + // that it should interpolate from its last known state down toward + // silence. + if (_decoder) { + // FIXME - We could potentially use the output from the codec, in which + // case we might get a cleaner fade toward silence. NOTE: The below logic + // attempts to catch up in the event that the jitter buffers have grown. + // The better long term fix is to use the output from the decode, detect + // when it actually reaches silence, and then delete the silent portions + // of the jitter buffers. Or petentially do a cross fade from the decode + // output to silence. + QByteArray decodedBuffer; + _decoder->lostFrame(decodedBuffer); + } + // calculate how many silent frames we should drop. int silentSamples = silentFrames * _numChannels; int samplesPerFrame = _ringBuffer.getNumFrameSamples(); diff --git a/libraries/avatars/src/AvatarData.cpp b/libraries/avatars/src/AvatarData.cpp index 8025c680ca..a1ea103edb 100644 --- a/libraries/avatars/src/AvatarData.cpp +++ b/libraries/avatars/src/AvatarData.cpp @@ -47,9 +47,6 @@ quint64 DEFAULT_FILTERED_LOG_EXPIRY = 2 * USECS_PER_SECOND; using namespace std; -const glm::vec3 DEFAULT_LOCAL_AABOX_CORNER(-0.5f); -const glm::vec3 DEFAULT_LOCAL_AABOX_SCALE(1.0f); - const QString AvatarData::FRAME_NAME = "com.highfidelity.recording.AvatarData"; static const int TRANSLATION_COMPRESSION_RADIX = 12; @@ -186,6 +183,7 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent bool cullSmallChanges = (dataDetail == CullSmallData); bool sendAll = (dataDetail == SendAllData); bool sendMinimum = (dataDetail == MinimumData); + bool sendPALMinimum = (dataDetail == PALMinimum); lazyInitHeadData(); @@ -222,24 +220,41 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent auto parentID = getParentID(); bool hasAvatarGlobalPosition = true; // always include global position - bool hasAvatarOrientation = sendAll || rotationChangedSince(lastSentTime); - bool hasAvatarBoundingBox = sendAll || avatarBoundingBoxChangedSince(lastSentTime); - bool hasAvatarScale = sendAll || avatarScaleChangedSince(lastSentTime); - bool hasLookAtPosition = sendAll || lookAtPositionChangedSince(lastSentTime); - bool hasAudioLoudness = sendAll || audioLoudnessChangedSince(lastSentTime); - bool hasSensorToWorldMatrix = sendAll || sensorToWorldMatrixChangedSince(lastSentTime); - bool hasAdditionalFlags = sendAll || additionalFlagsChangedSince(lastSentTime); + bool hasAvatarOrientation = false; + bool hasAvatarBoundingBox = false; + bool hasAvatarScale = false; + bool hasLookAtPosition = false; + bool hasAudioLoudness = false; + bool hasSensorToWorldMatrix = false; + bool hasAdditionalFlags = false; // local position, and parent info only apply to avatars that are parented. The local position // and the parent info can change independently though, so we track their "changed since" // separately - bool hasParentInfo = sendAll || parentInfoChangedSince(lastSentTime); - bool hasAvatarLocalPosition = hasParent() && (sendAll || - tranlationChangedSince(lastSentTime) || - parentInfoChangedSince(lastSentTime)); + bool hasParentInfo = false; + bool hasAvatarLocalPosition = false; - bool hasFaceTrackerInfo = !dropFaceTracking && hasFaceTracker() && (sendAll || faceTrackerInfoChangedSince(lastSentTime)); - bool hasJointData = sendAll || !sendMinimum; + bool hasFaceTrackerInfo = false; + bool hasJointData = false; + + if (sendPALMinimum) { + hasAudioLoudness = true; + } else { + hasAvatarOrientation = sendAll || rotationChangedSince(lastSentTime); + hasAvatarBoundingBox = sendAll || avatarBoundingBoxChangedSince(lastSentTime); + hasAvatarScale = sendAll || avatarScaleChangedSince(lastSentTime); + hasLookAtPosition = sendAll || lookAtPositionChangedSince(lastSentTime); + hasAudioLoudness = sendAll || audioLoudnessChangedSince(lastSentTime); + hasSensorToWorldMatrix = sendAll || sensorToWorldMatrixChangedSince(lastSentTime); + hasAdditionalFlags = sendAll || additionalFlagsChangedSince(lastSentTime); + hasParentInfo = sendAll || parentInfoChangedSince(lastSentTime); + hasAvatarLocalPosition = hasParent() && (sendAll || + tranlationChangedSince(lastSentTime) || + parentInfoChangedSince(lastSentTime)); + + hasFaceTrackerInfo = !dropFaceTracking && hasFaceTracker() && (sendAll || faceTrackerInfoChangedSince(lastSentTime)); + hasJointData = sendAll || !sendMinimum; + } // Leading flags, to indicate how much data is actually included in the packet... AvatarDataPacket::HasFlags packetStateFlags = @@ -1480,6 +1495,9 @@ void AvatarData::processAvatarIdentity(const Identity& identity, bool& identityC setAvatarEntityData(identity.avatarEntityData); identityChanged = true; } + // flag this avatar as non-stale by updating _averageBytesReceived + const int BOGUS_NUM_BYTES = 1; + _averageBytesReceived.updateAverage(BOGUS_NUM_BYTES); } QByteArray AvatarData::identityByteArray() const { @@ -1982,6 +2000,11 @@ void AvatarData::fromJson(const QJsonObject& json, bool useFrameSkeleton) { } } + auto currentBasis = getRecordingBasis(); + if (!currentBasis) { + currentBasis = std::make_shared(Transform::fromJson(json[JSON_AVATAR_BASIS])); + } + if (json.contains(JSON_AVATAR_RELATIVE)) { // During playback you can either have the recording basis set to the avatar current state // meaning that all playback is relative to this avatars starting position, or @@ -1990,15 +2013,14 @@ void AvatarData::fromJson(const QJsonObject& json, bool useFrameSkeleton) { // The first is more useful for playing back recordings on your own avatar, while // the latter is more useful for playing back other avatars within your scene. - auto currentBasis = getRecordingBasis(); - if (!currentBasis) { - currentBasis = std::make_shared(Transform::fromJson(json[JSON_AVATAR_BASIS])); - } - auto relativeTransform = Transform::fromJson(json[JSON_AVATAR_RELATIVE]); auto worldTransform = currentBasis->worldTransform(relativeTransform); setPosition(worldTransform.getTranslation()); setOrientation(worldTransform.getRotation()); + } else { + // We still set the position in the case that there is no movement. + setPosition(currentBasis->getTranslation()); + setOrientation(currentBasis->getRotation()); } if (json.contains(JSON_AVATAR_SCALE)) { diff --git a/libraries/avatars/src/AvatarData.h b/libraries/avatars/src/AvatarData.h index c2240f400f..1327798a0a 100644 --- a/libraries/avatars/src/AvatarData.h +++ b/libraries/avatars/src/AvatarData.h @@ -340,7 +340,7 @@ class AvatarData : public QObject, public SpatiallyNestable { Q_PROPERTY(float audioLoudness READ getAudioLoudness WRITE setAudioLoudness) Q_PROPERTY(float audioAverageLoudness READ getAudioAverageLoudness WRITE setAudioAverageLoudness) - Q_PROPERTY(QString displayName READ getDisplayName WRITE setDisplayName) + Q_PROPERTY(QString displayName READ getDisplayName WRITE setDisplayName NOTIFY displayNameChanged) // sessionDisplayName is sanitized, defaulted version displayName that is defined by the AvatarMixer rather than by Interface clients. // The result is unique among all avatars present at the time. Q_PROPERTY(QString sessionDisplayName READ getSessionDisplayName WRITE setSessionDisplayName) @@ -376,6 +376,7 @@ public: typedef enum { NoData, + PALMinimum, MinimumData, CullSmallData, IncludeSmallData, @@ -613,6 +614,9 @@ public: +signals: + void displayNameChanged(); + public slots: void sendAvatarDataPacket(); void sendIdentityPacket(); diff --git a/libraries/avatars/src/HeadData.cpp b/libraries/avatars/src/HeadData.cpp index 72516d9740..bf8593f1d9 100644 --- a/libraries/avatars/src/HeadData.cpp +++ b/libraries/avatars/src/HeadData.cpp @@ -65,8 +65,8 @@ glm::quat HeadData::getOrientation() const { void HeadData::setOrientation(const glm::quat& orientation) { // rotate body about vertical axis glm::quat bodyOrientation = _owningAvatar->getOrientation(); - glm::vec3 newFront = glm::inverse(bodyOrientation) * (orientation * IDENTITY_FRONT); - bodyOrientation = bodyOrientation * glm::angleAxis(atan2f(-newFront.x, -newFront.z), glm::vec3(0.0f, 1.0f, 0.0f)); + glm::vec3 newForward = glm::inverse(bodyOrientation) * (orientation * IDENTITY_FORWARD); + bodyOrientation = bodyOrientation * glm::angleAxis(atan2f(-newForward.x, -newForward.z), glm::vec3(0.0f, 1.0f, 0.0f)); _owningAvatar->setOrientation(bodyOrientation); // the rest goes to the head diff --git a/libraries/controllers/src/controllers/Actions.cpp b/libraries/controllers/src/controllers/Actions.cpp index 79ff4ecbf8..300fa684d9 100644 --- a/libraries/controllers/src/controllers/Actions.cpp +++ b/libraries/controllers/src/controllers/Actions.cpp @@ -51,6 +51,8 @@ namespace controller { makePosePair(Action::LEFT_HAND, "LeftHand"), makePosePair(Action::RIGHT_HAND, "RightHand"), + makePosePair(Action::LEFT_FOOT, "LeftFoot"), + makePosePair(Action::RIGHT_FOOT, "RightFoot"), makeButtonPair(Action::LEFT_HAND_CLICK, "LeftHandClick"), makeButtonPair(Action::RIGHT_HAND_CLICK, "RightHandClick"), diff --git a/libraries/controllers/src/controllers/Actions.h b/libraries/controllers/src/controllers/Actions.h index 724d17d951..edf3dee07a 100644 --- a/libraries/controllers/src/controllers/Actions.h +++ b/libraries/controllers/src/controllers/Actions.h @@ -42,6 +42,8 @@ enum class Action { LEFT_HAND = NUM_COMBINED_AXES, RIGHT_HAND, + LEFT_FOOT, + RIGHT_FOOT, LEFT_HAND_CLICK, RIGHT_HAND_CLICK, diff --git a/libraries/controllers/src/controllers/StandardController.cpp b/libraries/controllers/src/controllers/StandardController.cpp index 02ae5706b7..cc90ee7b49 100644 --- a/libraries/controllers/src/controllers/StandardController.cpp +++ b/libraries/controllers/src/controllers/StandardController.cpp @@ -102,6 +102,8 @@ Input::NamedVector StandardController::getAvailableInputs() const { // Poses makePair(LEFT_HAND, "LeftHand"), makePair(RIGHT_HAND, "RightHand"), + makePair(LEFT_FOOT, "LeftFoot"), + makePair(RIGHT_FOOT, "RightFoot"), // Aliases, PlayStation style names makePair(LB, "L1"), diff --git a/libraries/controllers/src/controllers/StandardControls.h b/libraries/controllers/src/controllers/StandardControls.h index c21d8a2f6e..f521ab81cf 100644 --- a/libraries/controllers/src/controllers/StandardControls.h +++ b/libraries/controllers/src/controllers/StandardControls.h @@ -158,6 +158,22 @@ namespace controller { LEFT_HAND_PINKY2, LEFT_HAND_PINKY3, LEFT_HAND_PINKY4, + TRACKED_OBJECT_00, + TRACKED_OBJECT_01, + TRACKED_OBJECT_02, + TRACKED_OBJECT_03, + TRACKED_OBJECT_04, + TRACKED_OBJECT_05, + TRACKED_OBJECT_06, + TRACKED_OBJECT_07, + TRACKED_OBJECT_08, + TRACKED_OBJECT_09, + TRACKED_OBJECT_10, + TRACKED_OBJECT_11, + TRACKED_OBJECT_12, + TRACKED_OBJECT_13, + TRACKED_OBJECT_14, + TRACKED_OBJECT_15, NUM_STANDARD_POSES }; diff --git a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp index cf6b39812a..f5d335adea 100644 --- a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp @@ -355,14 +355,17 @@ void OpenGLDisplayPlugin::customizeContext() { if ((image.width() > 0) && (image.height() > 0)) { cursorData.texture.reset( - gpu::Texture::create2D( - gpu::Element(gpu::VEC4, gpu::NUINT8, gpu::RGBA), - image.width(), image.height(), - gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR))); + gpu::Texture::createStrict( + gpu::Element(gpu::VEC4, gpu::NUINT8, gpu::RGBA), + image.width(), image.height(), + gpu::Texture::MAX_NUM_MIPS, + gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR))); cursorData.texture->setSource("cursor texture"); auto usage = gpu::Texture::Usage::Builder().withColor().withAlpha(); cursorData.texture->setUsage(usage.build()); - cursorData.texture->assignStoredMip(0, gpu::Element(gpu::VEC4, gpu::NUINT8, gpu::RGBA), image.byteCount(), image.constBits()); + cursorData.texture->setStoredMipFormat(gpu::Element(gpu::VEC4, gpu::NUINT8, gpu::RGBA)); + cursorData.texture->assignStoredMip(0, image.byteCount(), image.constBits()); + cursorData.texture->setAutoGenerateMips(true); } } } @@ -568,10 +571,15 @@ void OpenGLDisplayPlugin::compositeLayers() { compositeScene(); } + +#ifdef HIFI_ENABLE_NSIGHT_DEBUG + if (false) // do not compositeoverlay if running nsight debug +#endif { PROFILE_RANGE_EX(render_detail, "compositeOverlay", 0xff0077ff, (uint64_t)presentCount()) compositeOverlay(); } + auto compositorHelper = DependencyManager::get(); if (compositorHelper->getReticleVisible()) { PROFILE_RANGE_EX(render_detail, "compositePointer", 0xff0077ff, (uint64_t)presentCount()) @@ -654,6 +662,11 @@ float OpenGLDisplayPlugin::presentRate() const { return _presentRate.rate(); } +void OpenGLDisplayPlugin::resetPresentRate() { + // FIXME + // _presentRate = RateCounter<100>(); +} + float OpenGLDisplayPlugin::renderRate() const { return _renderRate.rate(); } diff --git a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.h b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.h index f4efc0267b..e1eea5de6c 100644 --- a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.h +++ b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.h @@ -59,6 +59,8 @@ public: float presentRate() const override; + void resetPresentRate() override; + float newFramePresentRate() const override; float droppedFrameRate() const override; diff --git a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp index a8b8ba3618..52c689ec00 100644 --- a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp @@ -296,33 +296,33 @@ void HmdDisplayPlugin::internalPresent() { image = image.convertToFormat(QImage::Format_RGBA8888); if (!_previewTexture) { _previewTexture.reset( - gpu::Texture::create2D( + gpu::Texture::createStrict( gpu::Element(gpu::VEC4, gpu::NUINT8, gpu::RGBA), image.width(), image.height(), + gpu::Texture::MAX_NUM_MIPS, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR))); _previewTexture->setSource("HMD Preview Texture"); _previewTexture->setUsage(gpu::Texture::Usage::Builder().withColor().build()); - _previewTexture->assignStoredMip(0, gpu::Element(gpu::VEC4, gpu::NUINT8, gpu::RGBA), image.byteCount(), image.constBits()); - _previewTexture->autoGenerateMips(-1); + _previewTexture->setStoredMipFormat(gpu::Element(gpu::VEC4, gpu::NUINT8, gpu::RGBA)); + _previewTexture->assignStoredMip(0, image.byteCount(), image.constBits()); + _previewTexture->setAutoGenerateMips(true); } - if (getGLBackend()->isTextureReady(_previewTexture)) { - auto viewport = getViewportForSourceSize(uvec2(_previewTexture->getDimensions())); + auto viewport = getViewportForSourceSize(uvec2(_previewTexture->getDimensions())); - render([&](gpu::Batch& batch) { - batch.enableStereo(false); - batch.resetViewTransform(); - batch.setFramebuffer(gpu::FramebufferPointer()); - batch.clearColorFramebuffer(gpu::Framebuffer::BUFFER_COLOR0, vec4(0)); - batch.setStateScissorRect(viewport); - batch.setViewportTransform(viewport); - batch.setResourceTexture(0, _previewTexture); - batch.setPipeline(_presentPipeline); - batch.draw(gpu::TRIANGLE_STRIP, 4); - }); - _clearPreviewFlag = false; - swapBuffers(); - } + render([&](gpu::Batch& batch) { + batch.enableStereo(false); + batch.resetViewTransform(); + batch.setFramebuffer(gpu::FramebufferPointer()); + batch.clearColorFramebuffer(gpu::Framebuffer::BUFFER_COLOR0, vec4(0)); + batch.setStateScissorRect(viewport); + batch.setViewportTransform(viewport); + batch.setResourceTexture(0, _previewTexture); + batch.setPipeline(_presentPipeline); + batch.draw(gpu::TRIANGLE_STRIP, 4); + }); + _clearPreviewFlag = false; + swapBuffers(); } postPreview(); diff --git a/libraries/entities-renderer/src/EntityTreeRenderer.cpp b/libraries/entities-renderer/src/EntityTreeRenderer.cpp index 55a7221f5d..fd5e07918e 100644 --- a/libraries/entities-renderer/src/EntityTreeRenderer.cpp +++ b/libraries/entities-renderer/src/EntityTreeRenderer.cpp @@ -101,7 +101,7 @@ void EntityTreeRenderer::resetEntitiesScriptEngine() { // Keep a ref to oldEngine until newEngine is ready so EntityScriptingInterface has something to use auto oldEngine = _entitiesScriptEngine; - auto newEngine = new ScriptEngine(ScriptEngine::ENTITY_CLIENT_SCRIPT, NO_SCRIPT, QString("Entities %1").arg(++_entitiesScriptEngineCount)); + auto newEngine = new ScriptEngine(ScriptEngine::ENTITY_CLIENT_SCRIPT, NO_SCRIPT, QString("about:Entities %1").arg(++_entitiesScriptEngineCount)); _entitiesScriptEngine = QSharedPointer(newEngine, entitiesScriptEngineDeleter); _scriptingServices->registerScriptEngineWithApplicationServices(_entitiesScriptEngine.data()); @@ -127,11 +127,11 @@ void EntityTreeRenderer::clear() { // remove all entities from the scene auto scene = _viewState->getMain3DScene(); if (scene) { - render::PendingChanges pendingChanges; + render::Transaction transaction; foreach(auto entity, _entitiesInScene) { - entity->removeFromScene(entity, scene, pendingChanges); + entity->removeFromScene(entity, scene, transaction); } - scene->enqueuePendingChanges(pendingChanges); + scene->enqueueTransaction(transaction); } else { qCWarning(entitiesrenderer) << "EntitityTreeRenderer::clear(), Unexpected null scene, possibly during application shutdown"; } @@ -146,9 +146,10 @@ void EntityTreeRenderer::clear() { void EntityTreeRenderer::reloadEntityScripts() { _entitiesScriptEngine->unloadAllEntityScripts(); + _entitiesScriptEngine->resetModuleCache(); foreach(auto entity, _entitiesInScene) { if (!entity->getScript().isEmpty()) { - ScriptEngine::loadEntityScript(_entitiesScriptEngine, entity->getEntityItemID(), entity->getScript(), true); + _entitiesScriptEngine->loadEntityScript(entity->getEntityItemID(), entity->getScript(), true); } } } @@ -607,7 +608,6 @@ RayToEntityIntersectionResult EntityTreeRenderer::findRayIntersectionWorker(cons (void**)&intersectedEntity, lockType, &result.accurate); if (result.intersects && intersectedEntity) { result.entityID = intersectedEntity->getEntityItemID(); - result.properties = intersectedEntity->getProperties(); result.intersection = ray.origin + (ray.direction * result.distance); result.entity = intersectedEntity; } @@ -703,7 +703,9 @@ void EntityTreeRenderer::mousePressEvent(QMouseEvent* event) { if (rayPickResult.intersects) { //qCDebug(entitiesrenderer) << "mousePressEvent over entity:" << rayPickResult.entityID; - QString urlString = rayPickResult.properties.getHref(); + auto entity = getTree()->findEntityByEntityItemID(rayPickResult.entityID); + auto properties = entity->getProperties(); + QString urlString = properties.getHref(); QUrl url = QUrl(urlString, QUrl::StrictMode); if (url.isValid() && !url.isEmpty()){ DependencyManager::get()->handleLookupString(urlString); @@ -713,7 +715,8 @@ void EntityTreeRenderer::mousePressEvent(QMouseEvent* event) { PointerEvent pointerEvent(PointerEvent::Press, MOUSE_POINTER_ID, pos2D, rayPickResult.intersection, rayPickResult.surfaceNormal, ray.direction, - toPointerButton(*event), toPointerButtons(*event)); + toPointerButton(*event), toPointerButtons(*event), + Qt::NoModifier); // TODO -- check for modifier keys? emit mousePressOnEntity(rayPickResult.entityID, pointerEvent); @@ -735,6 +738,46 @@ void EntityTreeRenderer::mousePressEvent(QMouseEvent* event) { } } +void EntityTreeRenderer::mouseDoublePressEvent(QMouseEvent* event) { + // If we don't have a tree, or we're in the process of shutting down, then don't + // process these events. + if (!_tree || _shuttingDown) { + return; + } + PerformanceTimer perfTimer("EntityTreeRenderer::mouseDoublePressEvent"); + PickRay ray = _viewState->computePickRay(event->x(), event->y()); + + bool precisionPicking = !_dontDoPrecisionPicking; + RayToEntityIntersectionResult rayPickResult = findRayIntersectionWorker(ray, Octree::Lock, precisionPicking); + if (rayPickResult.intersects) { + //qCDebug(entitiesrenderer) << "mouseDoublePressEvent over entity:" << rayPickResult.entityID; + + glm::vec2 pos2D = projectOntoEntityXYPlane(rayPickResult.entity, ray, rayPickResult); + PointerEvent pointerEvent(PointerEvent::Press, MOUSE_POINTER_ID, + pos2D, rayPickResult.intersection, + rayPickResult.surfaceNormal, ray.direction, + toPointerButton(*event), toPointerButtons(*event), Qt::NoModifier); + + emit mouseDoublePressOnEntity(rayPickResult.entityID, pointerEvent); + + if (_entitiesScriptEngine) { + _entitiesScriptEngine->callEntityScriptMethod(rayPickResult.entityID, "mouseDoublePressOnEntity", pointerEvent); + } + + _currentClickingOnEntityID = rayPickResult.entityID; + emit clickDownOnEntity(_currentClickingOnEntityID, pointerEvent); + if (_entitiesScriptEngine) { + _entitiesScriptEngine->callEntityScriptMethod(_currentClickingOnEntityID, "doubleclickOnEntity", pointerEvent); + } + + _lastPointerEvent = pointerEvent; + _lastPointerEventValid = true; + + } else { + emit mouseDoublePressOffEntity(); + } +} + void EntityTreeRenderer::mouseReleaseEvent(QMouseEvent* event) { // If we don't have a tree, or we're in the process of shutting down, then don't // process these events. @@ -753,7 +796,8 @@ void EntityTreeRenderer::mouseReleaseEvent(QMouseEvent* event) { PointerEvent pointerEvent(PointerEvent::Release, MOUSE_POINTER_ID, pos2D, rayPickResult.intersection, rayPickResult.surfaceNormal, ray.direction, - toPointerButton(*event), toPointerButtons(*event)); + toPointerButton(*event), toPointerButtons(*event), + Qt::NoModifier); // TODO -- check for modifier keys? emit mouseReleaseOnEntity(rayPickResult.entityID, pointerEvent); if (_entitiesScriptEngine) { @@ -773,7 +817,8 @@ void EntityTreeRenderer::mouseReleaseEvent(QMouseEvent* event) { PointerEvent pointerEvent(PointerEvent::Release, MOUSE_POINTER_ID, pos2D, rayPickResult.intersection, rayPickResult.surfaceNormal, ray.direction, - toPointerButton(*event), toPointerButtons(*event)); + toPointerButton(*event), toPointerButtons(*event), + Qt::NoModifier); // TODO -- check for modifier keys? emit clickReleaseOnEntity(_currentClickingOnEntityID, pointerEvent); if (_entitiesScriptEngine) { @@ -803,7 +848,8 @@ void EntityTreeRenderer::mouseMoveEvent(QMouseEvent* event) { PointerEvent pointerEvent(PointerEvent::Move, MOUSE_POINTER_ID, pos2D, rayPickResult.intersection, rayPickResult.surfaceNormal, ray.direction, - toPointerButton(*event), toPointerButtons(*event)); + toPointerButton(*event), toPointerButtons(*event), + Qt::NoModifier); // TODO -- check for modifier keys? emit mouseMoveOnEntity(rayPickResult.entityID, pointerEvent); @@ -823,7 +869,8 @@ void EntityTreeRenderer::mouseMoveEvent(QMouseEvent* event) { PointerEvent pointerEvent(PointerEvent::Move, MOUSE_POINTER_ID, pos2D, rayPickResult.intersection, rayPickResult.surfaceNormal, ray.direction, - toPointerButton(*event), toPointerButtons(*event)); + toPointerButton(*event), toPointerButtons(*event), + Qt::NoModifier); // TODO -- check for modifier keys? emit hoverLeaveEntity(_currentHoverOverEntityID, pointerEvent); if (_entitiesScriptEngine) { @@ -864,7 +911,8 @@ void EntityTreeRenderer::mouseMoveEvent(QMouseEvent* event) { PointerEvent pointerEvent(PointerEvent::Move, MOUSE_POINTER_ID, pos2D, rayPickResult.intersection, rayPickResult.surfaceNormal, ray.direction, - toPointerButton(*event), toPointerButtons(*event)); + toPointerButton(*event), toPointerButtons(*event), + Qt::NoModifier); // TODO -- check for modifier keys? emit hoverLeaveEntity(_currentHoverOverEntityID, pointerEvent); if (_entitiesScriptEngine) { @@ -883,7 +931,8 @@ void EntityTreeRenderer::mouseMoveEvent(QMouseEvent* event) { PointerEvent pointerEvent(PointerEvent::Move, MOUSE_POINTER_ID, pos2D, rayPickResult.intersection, rayPickResult.surfaceNormal, ray.direction, - toPointerButton(*event), toPointerButtons(*event)); + toPointerButton(*event), toPointerButtons(*event), + Qt::NoModifier); // TODO -- check for modifier keys? emit holdingClickOnEntity(_currentClickingOnEntityID, pointerEvent); if (_entitiesScriptEngine) { @@ -894,7 +943,7 @@ void EntityTreeRenderer::mouseMoveEvent(QMouseEvent* event) { void EntityTreeRenderer::deletingEntity(const EntityItemID& entityID) { if (_tree && !_shuttingDown && _entitiesScriptEngine) { - _entitiesScriptEngine->unloadEntityScript(entityID); + _entitiesScriptEngine->unloadEntityScript(entityID, true); } forceRecheckEntities(); // reset our state to force checking our inside/outsideness of entities @@ -902,11 +951,11 @@ void EntityTreeRenderer::deletingEntity(const EntityItemID& entityID) { // here's where we remove the entity payload from the scene if (_entitiesInScene.contains(entityID)) { auto entity = _entitiesInScene.take(entityID); - render::PendingChanges pendingChanges; + render::Transaction transaction; auto scene = _viewState->getMain3DScene(); if (scene) { - entity->removeFromScene(entity, scene, pendingChanges); - scene->enqueuePendingChanges(pendingChanges); + entity->removeFromScene(entity, scene, transaction); + scene->enqueueTransaction(transaction); } else { qCWarning(entitiesrenderer) << "EntityTreeRenderer::deletingEntity(), Unexpected null scene, possibly during application shutdown"; } @@ -924,13 +973,13 @@ void EntityTreeRenderer::addingEntity(const EntityItemID& entityID) { void EntityTreeRenderer::addEntityToScene(EntityItemPointer entity) { // here's where we add the entity payload to the scene - render::PendingChanges pendingChanges; + render::Transaction transaction; auto scene = _viewState->getMain3DScene(); if (scene) { - if (entity->addToScene(entity, scene, pendingChanges)) { + if (entity->addToScene(entity, scene, transaction)) { _entitiesInScene.insert(entity->getEntityItemID(), entity); } - scene->enqueuePendingChanges(pendingChanges); + scene->enqueueTransaction(transaction); } else { qCWarning(entitiesrenderer) << "EntityTreeRenderer::addEntityToScene(), Unexpected null scene, possibly during application shutdown"; } @@ -949,13 +998,13 @@ void EntityTreeRenderer::checkAndCallPreload(const EntityItemID& entityID, const } bool shouldLoad = entity->shouldPreloadScript() && _entitiesScriptEngine; QString scriptUrl = entity->getScript(); - if ((unloadFirst && shouldLoad) || scriptUrl.isEmpty()) { + if (shouldLoad && (unloadFirst || scriptUrl.isEmpty())) { _entitiesScriptEngine->unloadEntityScript(entityID); entity->scriptHasUnloaded(); } if (shouldLoad && !scriptUrl.isEmpty()) { scriptUrl = ResourceManager::normalizeURL(scriptUrl); - ScriptEngine::loadEntityScript(_entitiesScriptEngine, entityID, scriptUrl, reload); + _entitiesScriptEngine->loadEntityScript(entityID, scriptUrl, reload); entity->scriptHasPreloaded(); } } @@ -996,7 +1045,7 @@ void EntityTreeRenderer::playEntityCollisionSound(EntityItemPointer entity, cons // Shift the pitch down by ln(1 + (size / COLLISION_SIZE_FOR_STANDARD_PITCH)) / ln(2) const float COLLISION_SIZE_FOR_STANDARD_PITCH = 0.2f; - const float stretchFactor = log(1.0f + (minAACube.getLargestDimension() / COLLISION_SIZE_FOR_STANDARD_PITCH)) / log(2); + const float stretchFactor = logf(1.0f + (minAACube.getLargestDimension() / COLLISION_SIZE_FOR_STANDARD_PITCH)) / logf(2.0f); AudioInjector::playSound(collisionSound, volume, stretchFactor, collision.contactPoint); } diff --git a/libraries/entities-renderer/src/EntityTreeRenderer.h b/libraries/entities-renderer/src/EntityTreeRenderer.h index c11738c459..753f25310c 100644 --- a/libraries/entities-renderer/src/EntityTreeRenderer.h +++ b/libraries/entities-renderer/src/EntityTreeRenderer.h @@ -90,6 +90,7 @@ public: // event handles which may generate entity related events void mouseReleaseEvent(QMouseEvent* event); void mousePressEvent(QMouseEvent* event); + void mouseDoublePressEvent(QMouseEvent* event); void mouseMoveEvent(QMouseEvent* event); /// connect our signals to anEntityScriptingInterface for firing of events related clicking, @@ -103,9 +104,11 @@ public: signals: void mousePressOnEntity(const EntityItemID& entityItemID, const PointerEvent& event); + void mouseDoublePressOnEntity(const EntityItemID& entityItemID, const PointerEvent& event); void mouseMoveOnEntity(const EntityItemID& entityItemID, const PointerEvent& event); void mouseReleaseOnEntity(const EntityItemID& entityItemID, const PointerEvent& event); void mousePressOffEntity(); + void mouseDoublePressOffEntity(); void clickDownOnEntity(const EntityItemID& entityItemID, const PointerEvent& event); void holdingClickOnEntity(const EntityItemID& entityItemID, const PointerEvent& event); diff --git a/libraries/entities-renderer/src/RenderableEntityItem.h b/libraries/entities-renderer/src/RenderableEntityItem.h index 98271ddbbb..2f053924ab 100644 --- a/libraries/entities-renderer/src/RenderableEntityItem.h +++ b/libraries/entities-renderer/src/RenderableEntityItem.h @@ -54,7 +54,7 @@ namespace render { // Mixin class for implementing basic single item rendering class SimpleRenderableEntityItem { public: - bool addToScene(EntityItemPointer self, std::shared_ptr scene, render::PendingChanges& pendingChanges) { + bool addToScene(EntityItemPointer self, std::shared_ptr scene, render::Transaction& transaction) { _myItem = scene->allocateID(); auto renderData = std::make_shared(self, _myItem); @@ -64,13 +64,13 @@ public: makeEntityItemStatusGetters(self, statusGetters); renderPayload->addStatusGetters(statusGetters); - pendingChanges.resetItem(_myItem, renderPayload); + transaction.resetItem(_myItem, renderPayload); return true; } - void removeFromScene(EntityItemPointer self, std::shared_ptr scene, render::PendingChanges& pendingChanges) { - pendingChanges.removeItem(_myItem); + void removeFromScene(EntityItemPointer self, std::shared_ptr scene, render::Transaction& transaction) { + transaction.removeItem(_myItem); render::Item::clearID(_myItem); } @@ -79,14 +79,14 @@ public: return; } - render::PendingChanges pendingChanges; + render::Transaction transaction; render::ScenePointer scene = AbstractViewStateInterface::instance()->getMain3DScene(); if (scene) { - pendingChanges.updateItem(_myItem, [](RenderableEntityItemProxy& data) { + transaction.updateItem(_myItem, [](RenderableEntityItemProxy& data) { }); - scene->enqueuePendingChanges(pendingChanges); + scene->enqueueTransaction(transaction); } else { qCWarning(entitiesrenderer) << "SimpleRenderableEntityItem::notifyChanged(), Unexpected null scene, possibly during application shutdown"; } @@ -99,8 +99,8 @@ private: #define SIMPLE_RENDERABLE() \ public: \ - virtual bool addToScene(EntityItemPointer self, std::shared_ptr scene, render::PendingChanges& pendingChanges) override { return _renderHelper.addToScene(self, scene, pendingChanges); } \ - virtual void removeFromScene(EntityItemPointer self, std::shared_ptr scene, render::PendingChanges& pendingChanges) override { _renderHelper.removeFromScene(self, scene, pendingChanges); } \ + virtual bool addToScene(EntityItemPointer self, std::shared_ptr scene, render::Transaction& transaction) override { return _renderHelper.addToScene(self, scene, transaction); } \ + virtual void removeFromScene(EntityItemPointer self, std::shared_ptr scene, render::Transaction& transaction) override { _renderHelper.removeFromScene(self, scene, transaction); } \ virtual void locationChanged(bool tellPhysics = true) override { EntityItem::locationChanged(tellPhysics); _renderHelper.notifyChanged(); } \ virtual void dimensionsChanged() override { EntityItem::dimensionsChanged(); _renderHelper.notifyChanged(); } \ void checkFading() { \ diff --git a/libraries/entities-renderer/src/RenderableLightEntityItem.cpp b/libraries/entities-renderer/src/RenderableLightEntityItem.cpp index a7fcbf53ae..0b8592e678 100644 --- a/libraries/entities-renderer/src/RenderableLightEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableLightEntityItem.cpp @@ -27,7 +27,7 @@ RenderableLightEntityItem::RenderableLightEntityItem(const EntityItemID& entityI { } -bool RenderableLightEntityItem::addToScene(EntityItemPointer self, std::shared_ptr scene, render::PendingChanges& pendingChanges) { +bool RenderableLightEntityItem::addToScene(EntityItemPointer self, std::shared_ptr scene, render::Transaction& transaction) { _myItem = scene->allocateID(); auto renderItem = std::make_shared(); @@ -39,7 +39,7 @@ bool RenderableLightEntityItem::addToScene(EntityItemPointer self, std::shared_p makeEntityItemStatusGetters(self, statusGetters); renderPayload->addStatusGetters(statusGetters); - pendingChanges.resetItem(_myItem, renderPayload); + transaction.resetItem(_myItem, renderPayload); return true; } @@ -51,8 +51,8 @@ void RenderableLightEntityItem::somethingChangedNotification() { LightEntityItem::somethingChangedNotification(); } -void RenderableLightEntityItem::removeFromScene(EntityItemPointer self, std::shared_ptr scene, render::PendingChanges& pendingChanges) { - pendingChanges.removeItem(_myItem); +void RenderableLightEntityItem::removeFromScene(EntityItemPointer self, std::shared_ptr scene, render::Transaction& transaction) { + transaction.removeItem(_myItem); render::Item::clearID(_myItem); } @@ -81,12 +81,12 @@ void RenderableLightEntityItem::notifyChanged() { return; } - render::PendingChanges pendingChanges; + render::Transaction transaction; render::ScenePointer scene = AbstractViewStateInterface::instance()->getMain3DScene(); - updateLightFromEntity(pendingChanges); + updateLightFromEntity(transaction); - scene->enqueuePendingChanges(pendingChanges); + scene->enqueueTransaction(transaction); } bool RenderableLightEntityItem::findDetailedRayIntersection(const glm::vec3& origin, const glm::vec3& direction, @@ -103,13 +103,13 @@ bool RenderableLightEntityItem::findDetailedRayIntersection(const glm::vec3& ori } -void RenderableLightEntityItem::updateLightFromEntity(render::PendingChanges& pendingChanges) { +void RenderableLightEntityItem::updateLightFromEntity(render::Transaction& transaction) { if (!render::Item::isValidID(_myItem)) { return; } - pendingChanges.updateItem(_myItem, [&](LightPayload& data) { + transaction.updateItem(_myItem, [&](LightPayload& data) { updateRenderItemFromEntity(data); }); } diff --git a/libraries/entities-renderer/src/RenderableLightEntityItem.h b/libraries/entities-renderer/src/RenderableLightEntityItem.h index 36ba0d6311..42a5dbc91a 100644 --- a/libraries/entities-renderer/src/RenderableLightEntityItem.h +++ b/libraries/entities-renderer/src/RenderableLightEntityItem.h @@ -28,12 +28,12 @@ public: BoxFace& face, glm::vec3& surfaceNormal, void** intersectedObject, bool precisionPicking) const override; - void updateLightFromEntity(render::PendingChanges& pendingChanges); + void updateLightFromEntity(render::Transaction& transaction); - virtual bool addToScene(EntityItemPointer self, std::shared_ptr scene, render::PendingChanges& pendingChanges) override; + virtual bool addToScene(EntityItemPointer self, std::shared_ptr scene, render::Transaction& transaction) override; virtual void somethingChangedNotification() override; - virtual void removeFromScene(EntityItemPointer self, std::shared_ptr scene, render::PendingChanges& pendingChanges) override; + virtual void removeFromScene(EntityItemPointer self, std::shared_ptr scene, render::Transaction& transaction) override; virtual void locationChanged(bool tellPhysics = true) override; diff --git a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp index e6902228c5..91a4fc2ff9 100644 --- a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp @@ -228,20 +228,20 @@ namespace render { } bool RenderableModelEntityItem::addToScene(EntityItemPointer self, std::shared_ptr scene, - render::PendingChanges& pendingChanges) { + render::Transaction& transaction) { _myMetaItem = scene->allocateID(); auto renderData = std::make_shared(self); auto renderPayload = std::make_shared(renderData); - pendingChanges.resetItem(_myMetaItem, renderPayload); + transaction.resetItem(_myMetaItem, renderPayload); if (_model) { render::Item::Status::Getters statusGetters; makeEntityItemStatusGetters(getThisPointer(), statusGetters); // note: we don't mind if the model fails to add, we'll retry (in render()) until it succeeds - _model->addToScene(scene, pendingChanges, statusGetters); + _model->addToScene(scene, transaction, statusGetters); } // we've successfully added _myMetaItem so we always return true @@ -249,11 +249,11 @@ bool RenderableModelEntityItem::addToScene(EntityItemPointer self, std::shared_p } void RenderableModelEntityItem::removeFromScene(EntityItemPointer self, std::shared_ptr scene, - render::PendingChanges& pendingChanges) { - pendingChanges.removeItem(_myMetaItem); + render::Transaction& transaction) { + transaction.removeItem(_myMetaItem); render::Item::clearID(_myMetaItem); if (_model) { - _model->removeFromScene(scene, pendingChanges); + _model->removeFromScene(scene, transaction); } } @@ -277,10 +277,11 @@ bool RenderableModelEntityItem::getAnimationFrame() { return false; } - if (_animation && _animation->isLoaded()) { + auto animation = getAnimation(); + if (animation && animation->isLoaded()) { - const QVector& frames = _animation->getFramesReference(); // NOTE: getFrames() is too heavy - auto& fbxJoints = _animation->getGeometry().joints; + const QVector& frames = animation->getFramesReference(); // NOTE: getFrames() is too heavy + auto& fbxJoints = animation->getGeometry().joints; int frameCount = frames.size(); if (frameCount > 0) { @@ -371,103 +372,7 @@ void RenderableModelEntityItem::render(RenderArgs* args) { _model->updateRenderItems(); } - if (hasModel()) { - // Prepare the current frame - { - if (!_model || _needsModelReload) { - // TODO: this getModel() appears to be about 3% of model render time. We should optimize - PerformanceTimer perfTimer("getModel"); - auto renderer = qSharedPointerCast(args->_renderer); - getModel(renderer); - - // Remap textures immediately after loading to avoid flicker - remapTextures(); - } - - if (_model) { - if (hasRenderAnimation()) { - if (!jointsMapped()) { - QStringList modelJointNames = _model->getJointNames(); - mapJoints(modelJointNames); - } - } - - _jointDataLock.withWriteLock([&] { - getAnimationFrame(); - - // relay any inbound joint changes from scripts/animation/network to the model/rig - for (int index = 0; index < _localJointRotations.size(); index++) { - if (_localJointRotationsDirty[index]) { - glm::quat rotation = _localJointRotations[index]; - _model->setJointRotation(index, true, rotation, 1.0f); - _localJointRotationsDirty[index] = false; - } - } - for (int index = 0; index < _localJointTranslations.size(); index++) { - if (_localJointTranslationsDirty[index]) { - glm::vec3 translation = _localJointTranslations[index]; - _model->setJointTranslation(index, true, translation, 1.0f); - _localJointTranslationsDirty[index] = false; - } - } - }); - updateModelBounds(); - } - } - - // Enqueue updates for the next frame - if (_model) { - - render::ScenePointer scene = AbstractViewStateInterface::instance()->getMain3DScene(); - - // FIXME: this seems like it could be optimized if we tracked our last known visible state in - // the renderable item. As it stands now the model checks it's visible/invisible state - // so most of the time we don't do anything in this function. - _model->setVisibleInScene(getVisible(), scene); - - // Remap textures for the next frame to avoid flicker - remapTextures(); - - // update whether the model should be showing collision mesh (this may flag for fixupInScene) - bool showingCollisionGeometry = (bool)(args->_debugFlags & (int)RenderArgs::RENDER_DEBUG_HULLS); - if (showingCollisionGeometry != _showCollisionGeometry) { - ShapeType type = getShapeType(); - _showCollisionGeometry = showingCollisionGeometry; - if (_showCollisionGeometry && type != SHAPE_TYPE_STATIC_MESH && type != SHAPE_TYPE_NONE) { - // NOTE: it is OK if _collisionMeshKey is nullptr - model::MeshPointer mesh = collisionMeshCache.getMesh(_collisionMeshKey); - // NOTE: the model will render the collisionGeometry if it has one - _model->setCollisionMesh(mesh); - } else { - // release mesh - if (_collisionMeshKey) { - collisionMeshCache.releaseMesh(_collisionMeshKey); - } - // clear model's collision geometry - model::MeshPointer mesh = nullptr; - _model->setCollisionMesh(mesh); - } - } - - if (_model->needsFixupInScene()) { - render::PendingChanges pendingChanges; - - _model->removeFromScene(scene, pendingChanges); - - render::Item::Status::Getters statusGetters; - makeEntityItemStatusGetters(getThisPointer(), statusGetters); - _model->addToScene(scene, pendingChanges, statusGetters); - - scene->enqueuePendingChanges(pendingChanges); - } - - auto& currentURL = getParsedModelURL(); - if (currentURL != _model->getURL()) { - // Defer setting the url to the render thread - getModel(_myRenderer); - } - } - } else { + if (!hasModel() || (_model && _model->didVisualGeometryRequestFail())) { static glm::vec4 greenColor(0.0f, 1.0f, 0.0f, 1.0f); gpu::Batch& batch = *args->_batch; bool success; @@ -476,6 +381,109 @@ void RenderableModelEntityItem::render(RenderArgs* args) { batch.setModelTransform(shapeTransform); // we want to include the scale as well DependencyManager::get()->renderWireCubeInstance(batch, greenColor); } + return; + } + + // Prepare the current frame + { + if (!_model || _needsModelReload) { + // TODO: this getModel() appears to be about 3% of model render time. We should optimize + PerformanceTimer perfTimer("getModel"); + auto renderer = qSharedPointerCast(args->_renderer); + getModel(renderer); + + // Remap textures immediately after loading to avoid flicker + remapTextures(); + } + + if (_model) { + if (hasRenderAnimation()) { + if (!jointsMapped()) { + QStringList modelJointNames = _model->getJointNames(); + mapJoints(modelJointNames); + } + } + + _jointDataLock.withWriteLock([&] { + getAnimationFrame(); + + // relay any inbound joint changes from scripts/animation/network to the model/rig + for (int index = 0; index < _localJointRotations.size(); index++) { + if (_localJointRotationsDirty[index]) { + glm::quat rotation = _localJointRotations[index]; + _model->setJointRotation(index, true, rotation, 1.0f); + _localJointRotationsDirty[index] = false; + } + } + for (int index = 0; index < _localJointTranslations.size(); index++) { + if (_localJointTranslationsDirty[index]) { + glm::vec3 translation = _localJointTranslations[index]; + _model->setJointTranslation(index, true, translation, 1.0f); + _localJointTranslationsDirty[index] = false; + } + } + }); + updateModelBounds(); + } + } + + // Enqueue updates for the next frame + if (_model) { + +#ifdef WANT_EXTRA_RENDER_DEBUGGING + // debugging... + gpu::Batch& batch = *args->_batch; + _model->renderDebugMeshBoxes(batch); +#endif + + render::ScenePointer scene = AbstractViewStateInterface::instance()->getMain3DScene(); + + // FIXME: this seems like it could be optimized if we tracked our last known visible state in + // the renderable item. As it stands now the model checks it's visible/invisible state + // so most of the time we don't do anything in this function. + _model->setVisibleInScene(getVisible(), scene); + + // Remap textures for the next frame to avoid flicker + remapTextures(); + + // update whether the model should be showing collision mesh (this may flag for fixupInScene) + bool showingCollisionGeometry = (bool)(args->_debugFlags & (int)RenderArgs::RENDER_DEBUG_HULLS); + if (showingCollisionGeometry != _showCollisionGeometry) { + ShapeType type = getShapeType(); + _showCollisionGeometry = showingCollisionGeometry; + if (_showCollisionGeometry && type != SHAPE_TYPE_STATIC_MESH && type != SHAPE_TYPE_NONE) { + // NOTE: it is OK if _collisionMeshKey is nullptr + model::MeshPointer mesh = collisionMeshCache.getMesh(_collisionMeshKey); + // NOTE: the model will render the collisionGeometry if it has one + _model->setCollisionMesh(mesh); + } else { + // release mesh + if (_collisionMeshKey) { + collisionMeshCache.releaseMesh(_collisionMeshKey); + } + // clear model's collision geometry + model::MeshPointer mesh = nullptr; + _model->setCollisionMesh(mesh); + } + } + + if (_model->needsFixupInScene()) { + render::Transaction transaction; + + _model->removeFromScene(scene, transaction); + + render::Item::Status::Getters statusGetters; + makeEntityItemStatusGetters(getThisPointer(), statusGetters); + _model->addToScene(scene, transaction, statusGetters); + + scene->enqueueTransaction(transaction); + } + + auto& currentURL = getParsedModelURL(); + if (currentURL != _model->getURL()) { + // Defer setting the url to the render thread + getModel(_myRenderer); + } } } @@ -518,9 +526,9 @@ ModelPointer RenderableModelEntityItem::getModel(QSharedPointergetMain3DScene(); - render::PendingChanges pendingChanges; - _model->removeFromScene(scene, pendingChanges); - scene->enqueuePendingChanges(pendingChanges); + render::Transaction transaction; + _model->removeFromScene(scene, transaction); + scene->enqueueTransaction(transaction); // release interest _myRenderer->releaseModel(_model); @@ -559,7 +567,7 @@ void RenderableModelEntityItem::update(const quint64& now) { } // make a copy of the animation properites - _renderAnimationProperties = _animationProperties; + _renderAnimationProperties = getAnimationProperties(); ModelEntityItem::update(now); } @@ -581,6 +589,10 @@ EntityItemProperties RenderableModelEntityItem::getProperties(EntityPropertyFlag return properties; } +bool RenderableModelEntityItem::supportsDetailedRayIntersection() const { + return _model && _model->isLoaded(); +} + bool RenderableModelEntityItem::findDetailedRayIntersection(const glm::vec3& origin, const glm::vec3& direction, bool& keepSearching, OctreeElementPointer& element, float& distance, BoxFace& face, glm::vec3& surfaceNormal, void** intersectedObject, bool precisionPicking) const { @@ -597,11 +609,11 @@ bool RenderableModelEntityItem::findDetailedRayIntersection(const glm::vec3& ori void RenderableModelEntityItem::setShapeType(ShapeType type) { ModelEntityItem::setShapeType(type); - if (_shapeType == SHAPE_TYPE_COMPOUND) { - if (!_compoundShapeResource && !_compoundShapeURL.isEmpty()) { + if (getShapeType() == SHAPE_TYPE_COMPOUND) { + if (!_compoundShapeResource && !getCompoundShapeURL().isEmpty()) { _compoundShapeResource = DependencyManager::get()->getGeometryResource(getCompoundShapeURL()); } - } else if (_compoundShapeResource && !_compoundShapeURL.isEmpty()) { + } else if (_compoundShapeResource && !getCompoundShapeURL().isEmpty()) { // the compoundURL has been set but the shapeType does not agree _compoundShapeResource.reset(); } @@ -616,7 +628,7 @@ void RenderableModelEntityItem::setCompoundShapeURL(const QString& url) { if (tree) { QMetaObject::invokeMethod(tree.get(), "callLoader", Qt::QueuedConnection, Q_ARG(EntityItemID, getID())); } - if (_shapeType == SHAPE_TYPE_COMPOUND) { + if (getShapeType() == SHAPE_TYPE_COMPOUND) { _compoundShapeResource = DependencyManager::get()->getGeometryResource(url); } } @@ -626,7 +638,7 @@ bool RenderableModelEntityItem::isReadyToComputeShape() { ShapeType type = getShapeType(); if (type == SHAPE_TYPE_COMPOUND) { - if (!_model || _compoundShapeURL.isEmpty()) { + if (!_model || getCompoundShapeURL().isEmpty()) { EntityTreePointer tree = getTree(); if (tree) { QMetaObject::invokeMethod(tree.get(), "callLoader", Qt::QueuedConnection, Q_ARG(EntityItemID, getID())); @@ -648,8 +660,8 @@ bool RenderableModelEntityItem::isReadyToComputeShape() { doInitialModelSimulation(); } return true; - } else if (!_compoundShapeURL.isEmpty()) { - _compoundShapeResource = DependencyManager::get()->getGeometryResource(_compoundShapeURL); + } else if (!getCompoundShapeURL().isEmpty()) { + _compoundShapeResource = DependencyManager::get()->getGeometryResource(getCompoundShapeURL()); } } @@ -764,7 +776,7 @@ void RenderableModelEntityItem::computeShapeInfo(ShapeInfo& shapeInfo) { pointCollection[i][j] = scaleToFit * (pointCollection[i][j] + _model->getOffset()) - registrationOffset; } } - shapeInfo.setParams(type, dimensions, _compoundShapeURL); + shapeInfo.setParams(type, dimensions, getCompoundShapeURL()); } else if (type >= SHAPE_TYPE_SIMPLE_HULL && type <= SHAPE_TYPE_STATIC_MESH) { // should never fall in here when model not fully loaded assert(_model && _model->isLoaded()); @@ -801,6 +813,13 @@ void RenderableModelEntityItem::computeShapeInfo(ShapeInfo& shapeInfo) { auto& meshes = _model->getGeometry()->getMeshes(); int32_t numMeshes = (int32_t)(meshes.size()); + const int MAX_ALLOWED_MESH_COUNT = 1000; + if (numMeshes > MAX_ALLOWED_MESH_COUNT) { + // too many will cause the deadlock timer to throw... + shapeInfo.setParams(SHAPE_TYPE_BOX, 0.5f * dimensions); + return; + } + ShapeInfo::PointCollection& pointCollection = shapeInfo.getPointCollection(); pointCollection.clear(); if (type == SHAPE_TYPE_SIMPLE_COMPOUND) { @@ -983,7 +1002,7 @@ void RenderableModelEntityItem::computeShapeInfo(ShapeInfo& shapeInfo) { } } - shapeInfo.setParams(type, 0.5f * dimensions, _modelURL); + shapeInfo.setParams(type, 0.5f * dimensions, getModelURL()); } else { ModelEntityItem::computeShapeInfo(shapeInfo); shapeInfo.setParams(type, 0.5f * dimensions); @@ -1208,10 +1227,10 @@ void RenderableModelEntityItem::locationChanged(bool tellPhysics) { } render::ScenePointer scene = AbstractViewStateInterface::instance()->getMain3DScene(); - render::PendingChanges pendingChanges; + render::Transaction transaction; - pendingChanges.updateItem(myMetaItem); - scene->enqueuePendingChanges(pendingChanges); + transaction.updateItem(myMetaItem); + scene->enqueueTransaction(transaction); }); } } diff --git a/libraries/entities-renderer/src/RenderableModelEntityItem.h b/libraries/entities-renderer/src/RenderableModelEntityItem.h index bac2118326..c770e85089 100644 --- a/libraries/entities-renderer/src/RenderableModelEntityItem.h +++ b/libraries/entities-renderer/src/RenderableModelEntityItem.h @@ -40,13 +40,13 @@ public: void doInitialModelSimulation(); - virtual bool addToScene(EntityItemPointer self, std::shared_ptr scene, render::PendingChanges& pendingChanges) override; - virtual void removeFromScene(EntityItemPointer self, std::shared_ptr scene, render::PendingChanges& pendingChanges) override; + virtual bool addToScene(EntityItemPointer self, std::shared_ptr scene, render::Transaction& transaction) override; + virtual void removeFromScene(EntityItemPointer self, std::shared_ptr scene, render::Transaction& transaction) override; void updateModelBounds(); virtual void render(RenderArgs* args) override; - virtual bool supportsDetailedRayIntersection() const override { return true; } + virtual bool supportsDetailedRayIntersection() const override; virtual bool findDetailedRayIntersection(const glm::vec3& origin, const glm::vec3& direction, bool& keepSearching, OctreeElementPointer& element, float& distance, BoxFace& face, glm::vec3& surfaceNormal, diff --git a/libraries/entities-renderer/src/RenderableParticleEffectEntityItem.cpp b/libraries/entities-renderer/src/RenderableParticleEffectEntityItem.cpp index 86c3f5ff35..fdf9ba888f 100644 --- a/libraries/entities-renderer/src/RenderableParticleEffectEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableParticleEffectEntityItem.cpp @@ -162,7 +162,7 @@ RenderableParticleEffectEntityItem::RenderableParticleEffectEntityItem(const Ent bool RenderableParticleEffectEntityItem::addToScene(EntityItemPointer self, render::ScenePointer scene, - render::PendingChanges& pendingChanges) { + render::Transaction& transaction) { _scene = scene; _renderItemId = _scene->allocateID(); auto particlePayloadData = std::make_shared(); @@ -171,14 +171,14 @@ bool RenderableParticleEffectEntityItem::addToScene(EntityItemPointer self, render::Item::Status::Getters statusGetters; makeEntityItemStatusGetters(getThisPointer(), statusGetters); renderPayload->addStatusGetters(statusGetters); - pendingChanges.resetItem(_renderItemId, renderPayload); + transaction.resetItem(_renderItemId, renderPayload); return true; } void RenderableParticleEffectEntityItem::removeFromScene(EntityItemPointer self, render::ScenePointer scene, - render::PendingChanges& pendingChanges) { - pendingChanges.removeItem(_renderItemId); + render::Transaction& transaction) { + transaction.removeItem(_renderItemId); _scene = nullptr; render::Item::clearID(_renderItemId); }; @@ -206,12 +206,12 @@ void RenderableParticleEffectEntityItem::updateRenderItem() { return; } if (!getVisible()) { - render::PendingChanges pendingChanges; - pendingChanges.updateItem(_renderItemId, [](ParticlePayloadData& payload) { + render::Transaction transaction; + transaction.updateItem(_renderItemId, [](ParticlePayloadData& payload) { payload.setVisibleFlag(false); }); - _scene->enqueuePendingChanges(pendingChanges); + _scene->enqueueTransaction(transaction); return; } @@ -253,8 +253,8 @@ void RenderableParticleEffectEntityItem::updateRenderItem() { } - render::PendingChanges pendingChanges; - pendingChanges.updateItem(_renderItemId, [=](ParticlePayloadData& payload) { + render::Transaction transaction; + transaction.updateItem(_renderItemId, [=](ParticlePayloadData& payload) { payload.setVisibleFlag(true); // Update particle uniforms @@ -282,7 +282,7 @@ void RenderableParticleEffectEntityItem::updateRenderItem() { } }); - _scene->enqueuePendingChanges(pendingChanges); + _scene->enqueueTransaction(transaction); } void RenderableParticleEffectEntityItem::createPipelines() { @@ -318,9 +318,9 @@ void RenderableParticleEffectEntityItem::notifyBoundChanged() { if (!render::Item::isValidID(_renderItemId)) { return; } - render::PendingChanges pendingChanges; - pendingChanges.updateItem(_renderItemId, [](ParticlePayloadData& payload) { + render::Transaction transaction; + transaction.updateItem(_renderItemId, [](ParticlePayloadData& payload) { }); - _scene->enqueuePendingChanges(pendingChanges); + _scene->enqueueTransaction(transaction); } \ No newline at end of file diff --git a/libraries/entities-renderer/src/RenderableParticleEffectEntityItem.h b/libraries/entities-renderer/src/RenderableParticleEffectEntityItem.h index a36c3640d6..9b31c92b07 100644 --- a/libraries/entities-renderer/src/RenderableParticleEffectEntityItem.h +++ b/libraries/entities-renderer/src/RenderableParticleEffectEntityItem.h @@ -25,8 +25,8 @@ public: void updateRenderItem(); - virtual bool addToScene(EntityItemPointer self, render::ScenePointer scene, render::PendingChanges& pendingChanges) override; - virtual void removeFromScene(EntityItemPointer self, render::ScenePointer scene, render::PendingChanges& pendingChanges) override; + virtual bool addToScene(EntityItemPointer self, render::ScenePointer scene, render::Transaction& transaction) override; + virtual void removeFromScene(EntityItemPointer self, render::ScenePointer scene, render::Transaction& transaction) override; protected: virtual void locationChanged(bool tellPhysics = true) override { EntityItem::locationChanged(tellPhysics); notifyBoundChanged(); } diff --git a/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.cpp b/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.cpp index 7359a548fc..ee3b068af9 100644 --- a/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.cpp @@ -14,6 +14,7 @@ #include #include #include +#include "ModelScriptingInterface.h" #if defined(__GNUC__) && !defined(__clang__) #pragma GCC diagnostic push @@ -53,6 +54,8 @@ #include "PhysicalEntitySimulation.h" gpu::PipelinePointer RenderablePolyVoxEntityItem::_pipeline = nullptr; +gpu::PipelinePointer RenderablePolyVoxEntityItem::_wireframePipeline = nullptr; + const float MARCHING_CUBE_COLLISION_HULL_OFFSET = 0.5; @@ -73,7 +76,7 @@ const float MARCHING_CUBE_COLLISION_HULL_OFFSET = 0.5; _meshDirty In RenderablePolyVoxEntityItem::render, these flags are checked and changes are propagated along the chain. - decompressVolumeData() is called to decompress _voxelData into _volData. getMesh() is called to invoke the + decompressVolumeData() is called to decompress _voxelData into _volData. recomputeMesh() is called to invoke the polyVox surface extractor to create _mesh (as well as set Simulation _dirtyFlags). Because Simulation::DIRTY_SHAPE is set, isReadyToComputeShape() gets called and _shape is created either from _volData or _shape, depending on the surface style. @@ -81,7 +84,7 @@ const float MARCHING_CUBE_COLLISION_HULL_OFFSET = 0.5; When a script changes _volData, compressVolumeDataAndSendEditPacket is called to update _voxelData and to send a packet to the entity-server. - decompressVolumeData, getMesh, computeShapeInfoWorker, and compressVolumeDataAndSendEditPacket are too expensive + decompressVolumeData, recomputeMesh, computeShapeInfoWorker, and compressVolumeDataAndSendEditPacket are too expensive to run on a thread that has other things to do. These use QtConcurrent::run to spawn a thread. As each thread finishes, it adjusts the dirty flags so that the next call to render() will kick off the next step. @@ -401,6 +404,9 @@ bool RenderablePolyVoxEntityItem::setSphere(glm::vec3 centerWorldCoords, float r float smallestDimensionSize = voxelSize.x; smallestDimensionSize = glm::min(smallestDimensionSize, voxelSize.y); smallestDimensionSize = glm::min(smallestDimensionSize, voxelSize.z); + if (smallestDimensionSize <= 0.0f) { + return false; + } glm::vec3 maxRadiusInVoxelCoords = glm::vec3(radiusWorldCoords / smallestDimensionSize); glm::vec3 centerInVoxelCoords = wtvMatrix * glm::vec4(centerWorldCoords, 1.0f); @@ -411,21 +417,33 @@ bool RenderablePolyVoxEntityItem::setSphere(glm::vec3 centerWorldCoords, float r glm::ivec3 lowI = glm::clamp(low, glm::vec3(0.0f), _voxelVolumeSize); glm::ivec3 highI = glm::clamp(high, glm::vec3(0.0f), _voxelVolumeSize); + glm::vec3 radials(radiusWorldCoords / voxelSize.x, + radiusWorldCoords / voxelSize.y, + radiusWorldCoords / voxelSize.z); + // This three-level for loop iterates over every voxel in the volume that might be in the sphere withWriteLock([&] { for (int z = lowI.z; z < highI.z; z++) { for (int y = lowI.y; y < highI.y; y++) { for (int x = lowI.x; x < highI.x; x++) { - // Store our current position as a vector... - glm::vec4 pos(x + 0.5f, y + 0.5f, z + 0.5f, 1.0); // consider voxels cenetered on their coordinates - // convert to world coordinates - glm::vec3 worldPos = glm::vec3(vtwMatrix * pos); - // compute how far the current position is from the center of the volume - float fDistToCenter = glm::distance(worldPos, centerWorldCoords); - // If the current voxel is less than 'radius' units from the center then we set its value - if (fDistToCenter <= radiusWorldCoords) { + + // set voxels whose bounding-box touches the sphere + AABox voxelBox(glm::vec3(x - 0.5f, y - 0.5f, z - 0.5f), glm::vec3(1.0f, 1.0f, 1.0f)); + if (voxelBox.touchesAAEllipsoid(centerInVoxelCoords, radials)) { result |= setVoxelInternal(x, y, z, toValue); } + + // TODO -- this version only sets voxels which have centers inside the sphere. which is best? + // // Store our current position as a vector... + // glm::vec4 pos(x + 0.5f, y + 0.5f, z + 0.5f, 1.0); // consider voxels cenetered on their coordinates + // // convert to world coordinates + // glm::vec3 worldPos = glm::vec3(vtwMatrix * pos); + // // compute how far the current position is from the center of the volume + // float fDistToCenter = glm::distance(worldPos, centerWorldCoords); + // // If the current voxel is less than 'radius' units from the center then we set its value + // if (fDistToCenter <= radiusWorldCoords) { + // result |= setVoxelInternal(x, y, z, toValue); + // } } } } @@ -642,32 +660,29 @@ void RenderablePolyVoxEntityItem::computeShapeInfo(ShapeInfo& info) { }); } -void RenderablePolyVoxEntityItem::setXTextureURL(QString xTextureURL) { - if (xTextureURL != _xTextureURL) { +void RenderablePolyVoxEntityItem::setXTextureURL(const QString& xTextureURL) { + if (xTextureURL != getXTextureURL()) { _xTexture.clear(); PolyVoxEntityItem::setXTextureURL(xTextureURL); } } -void RenderablePolyVoxEntityItem::setYTextureURL(QString yTextureURL) { - if (yTextureURL != _yTextureURL) { +void RenderablePolyVoxEntityItem::setYTextureURL(const QString& yTextureURL) { + if (yTextureURL != getYTextureURL()) { _yTexture.clear(); PolyVoxEntityItem::setYTextureURL(yTextureURL); } } -void RenderablePolyVoxEntityItem::setZTextureURL(QString zTextureURL) { - if (zTextureURL != _zTextureURL) { +void RenderablePolyVoxEntityItem::setZTextureURL(const QString& zTextureURL) { + if (zTextureURL != getZTextureURL()) { _zTexture.clear(); PolyVoxEntityItem::setZTextureURL(zTextureURL); } } -void RenderablePolyVoxEntityItem::render(RenderArgs* args) { - PerformanceTimer perfTimer("RenderablePolyVoxEntityItem::render"); - assert(getType() == EntityTypes::PolyVox); - Q_ASSERT(args->_batch); +bool RenderablePolyVoxEntityItem::updateDependents() { bool voxelDataDirty; bool volDataDirty; withWriteLock([&] { @@ -677,12 +692,27 @@ void RenderablePolyVoxEntityItem::render(RenderArgs* args) { _voxelDataDirty = false; } else if (_volDataDirty) { _volDataDirty = false; + } else { + _meshReady = true; } }); if (voxelDataDirty) { decompressVolumeData(); } else if (volDataDirty) { - getMesh(); + recomputeMesh(); + } + + return !volDataDirty; +} + + +void RenderablePolyVoxEntityItem::render(RenderArgs* args) { + PerformanceTimer perfTimer("RenderablePolyVoxEntityItem::render"); + assert(getType() == EntityTypes::PolyVox); + Q_ASSERT(args->_batch); + + if (_voxelDataDirty || _volDataDirty) { + updateDependents(); } model::MeshPointer mesh; @@ -696,7 +726,7 @@ void RenderablePolyVoxEntityItem::render(RenderArgs* args) { !mesh->getIndexBuffer()._buffer) { return; } - + if (!_pipeline) { gpu::ShaderPointer vertexShader = gpu::Shader::createVertex(std::string(polyvox_vert)); gpu::ShaderPointer pixelShader = gpu::Shader::createPixel(std::string(polyvox_frag)); @@ -715,6 +745,13 @@ void RenderablePolyVoxEntityItem::render(RenderArgs* args) { state->setDepthTest(true, true, gpu::LESS_EQUAL); _pipeline = gpu::Pipeline::create(program, state); + + auto wireframeState = std::make_shared(); + wireframeState->setCullMode(gpu::State::CULL_BACK); + wireframeState->setDepthTest(true, true, gpu::LESS_EQUAL); + wireframeState->setFillMode(gpu::State::FILL_LINE); + + _wireframePipeline = gpu::Pipeline::create(program, wireframeState); } if (!_vertexFormat) { @@ -725,7 +762,11 @@ void RenderablePolyVoxEntityItem::render(RenderArgs* args) { } gpu::Batch& batch = *args->_batch; - batch.setPipeline(_pipeline); + + // Pick correct Pipeline + bool wireframe = (render::ShapeKey(args->_globalShapeKey).isWireframe()); + auto pipeline = (wireframe ? _wireframePipeline : _pipeline); + batch.setPipeline(pipeline); Transform transform(voxelToWorldMatrix()); batch.setModelTransform(transform); @@ -734,6 +775,12 @@ void RenderablePolyVoxEntityItem::render(RenderArgs* args) { 0, sizeof(PolyVox::PositionMaterialNormal)); + // TODO -- should we be setting this? + // batch.setInputBuffer(gpu::Stream::NORMAL, mesh->getVertexBuffer()._buffer, + // 12, + // sizeof(PolyVox::PositionMaterialNormal)); + + batch.setIndexBuffer(gpu::UINT32, mesh->getIndexBuffer()._buffer, 0); if (!_xTextureURL.isEmpty() && !_xTexture) { @@ -762,7 +809,7 @@ void RenderablePolyVoxEntityItem::render(RenderArgs* args) { batch.setResourceTexture(2, DependencyManager::get()->getWhiteTexture()); } - int voxelVolumeSizeLocation = _pipeline->getProgram()->getUniforms().findLocation("voxelVolumeSize"); + int voxelVolumeSizeLocation = pipeline->getProgram()->getUniforms().findLocation("voxelVolumeSize"); batch._glUniform3f(voxelVolumeSizeLocation, voxelVolumeSize.x, voxelVolumeSize.y, voxelVolumeSize.z); batch.drawIndexed(gpu::TRIANGLES, (gpu::uint32)mesh->getNumIndices(), 0); @@ -770,7 +817,7 @@ void RenderablePolyVoxEntityItem::render(RenderArgs* args) { bool RenderablePolyVoxEntityItem::addToScene(EntityItemPointer self, std::shared_ptr scene, - render::PendingChanges& pendingChanges) { + render::Transaction& transaction) { _myItem = scene->allocateID(); auto renderItem = std::make_shared(getThisPointer()); @@ -781,15 +828,15 @@ bool RenderablePolyVoxEntityItem::addToScene(EntityItemPointer self, makeEntityItemStatusGetters(getThisPointer(), statusGetters); renderPayload->addStatusGetters(statusGetters); - pendingChanges.resetItem(_myItem, renderPayload); + transaction.resetItem(_myItem, renderPayload); return true; } void RenderablePolyVoxEntityItem::removeFromScene(EntityItemPointer self, std::shared_ptr scene, - render::PendingChanges& pendingChanges) { - pendingChanges.removeItem(_myItem); + render::Transaction& transaction) { + transaction.removeItem(_myItem); render::Item::clearID(_myItem); } @@ -1199,7 +1246,7 @@ void RenderablePolyVoxEntityItem::copyUpperEdgesFromNeighbors() { } } -void RenderablePolyVoxEntityItem::getMesh() { +void RenderablePolyVoxEntityItem::recomputeMesh() { // use _volData to make a renderable mesh PolyVoxSurfaceStyle voxelSurfaceStyle; withReadLock([&] { @@ -1252,29 +1299,41 @@ void RenderablePolyVoxEntityItem::getMesh() { auto indexBuffer = std::make_shared(vecIndices.size() * sizeof(uint32_t), (gpu::Byte*)vecIndices.data()); auto indexBufferPtr = gpu::BufferPointer(indexBuffer); - gpu::BufferView indexBufferView(indexBufferPtr, gpu::Element(gpu::SCALAR, gpu::UINT32, gpu::RAW)); + gpu::BufferView indexBufferView(indexBufferPtr, gpu::Element(gpu::SCALAR, gpu::UINT32, gpu::INDEX)); mesh->setIndexBuffer(indexBufferView); - const std::vector& vecVertices = polyVoxMesh.getVertices(); + const std::vector& vecVertices = polyVoxMesh.getRawVertexData(); auto vertexBuffer = std::make_shared(vecVertices.size() * sizeof(PolyVox::PositionMaterialNormal), (gpu::Byte*)vecVertices.data()); auto vertexBufferPtr = gpu::BufferPointer(vertexBuffer); gpu::BufferView vertexBufferView(vertexBufferPtr, 0, vertexBufferPtr->getSize(), sizeof(PolyVox::PositionMaterialNormal), - gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::RAW)); + gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::XYZ)); mesh->setVertexBuffer(vertexBufferView); + + + // TODO -- use 3-byte normals rather than 3-float normals mesh->addAttribute(gpu::Stream::NORMAL, - gpu::BufferView(vertexBufferPtr, sizeof(float) * 3, - vertexBufferPtr->getSize() , + gpu::BufferView(vertexBufferPtr, + sizeof(float) * 3, // polyvox mesh is packed: position, normal, material + vertexBufferPtr->getSize(), sizeof(PolyVox::PositionMaterialNormal), - gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::RAW))); + gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::XYZ))); + + std::vector parts; + parts.emplace_back(model::Mesh::Part((model::Index)0, // startIndex + (model::Index)vecIndices.size(), // numIndices + (model::Index)0, // baseVertex + model::Mesh::TRIANGLES)); // topology + mesh->setPartBuffer(gpu::BufferView(new gpu::Buffer(parts.size() * sizeof(model::Mesh::Part), + (gpu::Byte*) parts.data()), gpu::Element::PART_DRAWCALL)); entity->setMesh(mesh); }); } void RenderablePolyVoxEntityItem::setMesh(model::MeshPointer mesh) { - // this catches the payload from getMesh + // this catches the payload from recomputeMesh bool neighborsNeedUpdate; withWriteLock([&] { if (!_collisionless) { @@ -1282,7 +1341,7 @@ void RenderablePolyVoxEntityItem::setMesh(model::MeshPointer mesh) { } _mesh = mesh; _meshDirty = true; - _meshInitialized = true; + _meshReady = true; neighborsNeedUpdate = _neighborsNeedUpdate; _neighborsNeedUpdate = false; }); @@ -1294,7 +1353,7 @@ void RenderablePolyVoxEntityItem::setMesh(model::MeshPointer mesh) { void RenderablePolyVoxEntityItem::computeShapeInfoWorker() { // this creates a collision-shape for the physics engine. The shape comes from // _volData for cubic extractors and from _mesh for marching-cube extractors - if (!_meshInitialized) { + if (!_meshReady) { return; } @@ -1531,7 +1590,6 @@ std::shared_ptr RenderablePolyVoxEntityItem::getZPN return std::dynamic_pointer_cast(_zPNeighbor.lock()); } - void RenderablePolyVoxEntityItem::bonkNeighbors() { // flag neighbors to the negative of this entity as needing to rebake their meshes. cacheNeighbors(); @@ -1551,15 +1609,43 @@ void RenderablePolyVoxEntityItem::bonkNeighbors() { } } - void RenderablePolyVoxEntityItem::locationChanged(bool tellPhysics) { EntityItem::locationChanged(tellPhysics); if (!_pipeline || !render::Item::isValidID(_myItem)) { return; } render::ScenePointer scene = AbstractViewStateInterface::instance()->getMain3DScene(); - render::PendingChanges pendingChanges; - pendingChanges.updateItem(_myItem, [](PolyVoxPayload& payload) {}); + render::Transaction transaction; + transaction.updateItem(_myItem, [](PolyVoxPayload& payload) {}); - scene->enqueuePendingChanges(pendingChanges); + scene->enqueueTransaction(transaction); +} + +bool RenderablePolyVoxEntityItem::getMeshes(MeshProxyList& result) { + if (!updateDependents()) { + return false; + } + + bool success = false; + MeshProxy* meshProxy = nullptr; + glm::mat4 transform = voxelToLocalMatrix(); + withReadLock([&] { + gpu::BufferView::Index numVertices = (gpu::BufferView::Index)_mesh->getNumVertices(); + if (!_meshReady) { + // we aren't ready to return a mesh. the caller will have to try again later. + success = false; + } else if (numVertices == 0) { + // we are ready, but there are no triangles in the mesh. + success = true; + } else { + success = true; + // the mesh will be in voxel-space. transform it into object-space + meshProxy = new MeshProxy( + _mesh->map([=](glm::vec3 position){ return glm::vec3(transform * glm::vec4(position, 1.0f)); }, + [=](glm::vec3 normal){ return glm::normalize(glm::vec3(transform * glm::vec4(normal, 0.0f))); }, + [&](uint32_t index){ return index; })); + result << meshProxy; + } + }); + return success; } diff --git a/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.h b/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.h index 45842c2fb9..c0bc8ba610 100644 --- a/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.h +++ b/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.h @@ -61,10 +61,12 @@ public: virtual uint8_t getVoxel(int x, int y, int z) override; virtual bool setVoxel(int x, int y, int z, uint8_t toValue) override; + int getOnCount() const override { return _onCount; } + void render(RenderArgs* args) override; virtual bool supportsDetailedRayIntersection() const override { return true; } virtual bool findDetailedRayIntersection(const glm::vec3& origin, const glm::vec3& direction, - bool& keepSearching, OctreeElementPointer& element, float& distance, + bool& keepSearching, OctreeElementPointer& element, float& distance, BoxFace& face, glm::vec3& surfaceNormal, void** intersectedObject, bool precisionPicking) const override; @@ -99,16 +101,16 @@ public: virtual bool setAll(uint8_t toValue) override; virtual bool setCuboid(const glm::vec3& lowPosition, const glm::vec3& cuboidSize, int toValue) override; - virtual void setXTextureURL(QString xTextureURL) override; - virtual void setYTextureURL(QString yTextureURL) override; - virtual void setZTextureURL(QString zTextureURL) override; + virtual void setXTextureURL(const QString& xTextureURL) override; + virtual void setYTextureURL(const QString& yTextureURL) override; + virtual void setZTextureURL(const QString& zTextureURL) override; virtual bool addToScene(EntityItemPointer self, std::shared_ptr scene, - render::PendingChanges& pendingChanges) override; + render::Transaction& transaction) override; virtual void removeFromScene(EntityItemPointer self, std::shared_ptr scene, - render::PendingChanges& pendingChanges) override; + render::Transaction& transaction) override; virtual void setXNNeighborID(const EntityItemID& xNNeighborID) override; virtual void setYNNeighborID(const EntityItemID& yNNeighborID) override; @@ -139,11 +141,13 @@ public: uint8_t getVoxelInternal(int x, int y, int z) const; bool setVoxelInternal(int x, int y, int z, uint8_t toValue); - void setVolDataDirty() { withWriteLock([&] { _volDataDirty = true; }); } + void setVolDataDirty() { withWriteLock([&] { _volDataDirty = true; _meshReady = false; }); } // Transparent polyvox didn't seem to be working so disable for now bool isTransparent() override { return false; } + bool getMeshes(MeshProxyList& result) override; + protected: virtual void locationChanged(bool tellPhysics = true) override; @@ -154,7 +158,7 @@ private: model::MeshPointer _mesh; gpu::Stream::FormatPointer _vertexFormat; bool _meshDirty { true }; // does collision-shape need to be recomputed? - bool _meshInitialized { false }; + bool _meshReady { false }; NetworkTexturePointer _xTexture; NetworkTexturePointer _yTexture; @@ -163,11 +167,12 @@ private: const int MATERIAL_GPU_SLOT = 3; render::ItemID _myItem{ render::Item::INVALID_ITEM_ID }; static gpu::PipelinePointer _pipeline; + static gpu::PipelinePointer _wireframePipeline; ShapeInfo _shapeInfo; PolyVox::SimpleVolume* _volData = nullptr; - bool _volDataDirty = false; // does getMesh need to be called? + bool _volDataDirty = false; // does recomputeMesh need to be called? int _onCount; // how many non-zero voxels are in _volData bool _neighborsNeedUpdate { false }; @@ -178,7 +183,7 @@ private: // these are run off the main thread void decompressVolumeData(); void compressVolumeDataAndSendEditPacket(); - virtual void getMesh() override; // recompute mesh + virtual void recomputeMesh() override; // recompute mesh void computeShapeInfoWorker(); // these are cached lookups of _xNNeighborID, _yNNeighborID, _zNNeighborID, _xPNeighborID, _yPNeighborID, _zPNeighborID @@ -191,6 +196,7 @@ private: void cacheNeighbors(); void copyUpperEdgesFromNeighbors(); void bonkNeighbors(); + bool updateDependents(); }; bool inUserBounds(const PolyVox::SimpleVolume* vol, PolyVoxEntityItem::PolyVoxSurfaceStyle surfaceStyle, diff --git a/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp b/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp index c3e097382c..1ad60bf7c6 100644 --- a/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp @@ -114,13 +114,22 @@ void RenderableShapeEntityItem::render(RenderArgs* args) { auto outColor = _procedural->getColor(color); outColor.a *= _procedural->isFading() ? Interpolate::calculateFadeRatio(_procedural->getFadeStartTime()) : 1.0f; batch._glColor4f(outColor.r, outColor.g, outColor.b, outColor.a); - DependencyManager::get()->renderShape(batch, MAPPING[_shape]); + if (render::ShapeKey(args->_globalShapeKey).isWireframe()) { + DependencyManager::get()->renderWireShape(batch, MAPPING[_shape]); + } else { + DependencyManager::get()->renderShape(batch, MAPPING[_shape]); + } } else { // FIXME, support instanced multi-shape rendering using multidraw indirect color.a *= _isFading ? Interpolate::calculateFadeRatio(_fadeStartTime) : 1.0f; auto geometryCache = DependencyManager::get(); auto pipeline = color.a < 1.0f ? geometryCache->getTransparentShapePipeline() : geometryCache->getOpaqueShapePipeline(); - geometryCache->renderSolidShapeInstance(batch, MAPPING[_shape], color, pipeline); + + if (render::ShapeKey(args->_globalShapeKey).isWireframe()) { + geometryCache->renderWireShapeInstance(batch, MAPPING[_shape], color, pipeline); + } else { + geometryCache->renderSolidShapeInstance(batch, MAPPING[_shape], color, pipeline); + } } static const auto triCount = DependencyManager::get()->getShapeTriangleCount(MAPPING[_shape]); diff --git a/libraries/entities-renderer/src/RenderableTextEntityItem.cpp b/libraries/entities-renderer/src/RenderableTextEntityItem.cpp index 9c98e699f1..a58ed8bbff 100644 --- a/libraries/entities-renderer/src/RenderableTextEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableTextEntityItem.cpp @@ -77,14 +77,16 @@ void RenderableTextEntityItem::render(RenderArgs* args) { geometryCache->bindSimpleProgram(batch, false, transparent, false, false, true); geometryCache->renderQuad(batch, minCorner, maxCorner, backgroundColor, _geometryID); - float scale = _lineHeight / _textRenderer->getFontSize(); + float lineheight = getLineHeight(); + float scale = lineheight / _textRenderer->getFontSize(); transformToTopLeft.setScale(scale); // Scale to have the correct line height batch.setModelTransform(transformToTopLeft); - float leftMargin = 0.1f * _lineHeight, topMargin = 0.1f * _lineHeight; + float leftMargin = 0.1f * lineheight, topMargin = 0.1f * lineheight; glm::vec2 bounds = glm::vec2(dimensions.x - 2.0f * leftMargin, dimensions.y - 2.0f * topMargin); - _textRenderer->draw(batch, leftMargin / scale, -topMargin / scale, _text, textColor, bounds / scale); + auto text = getText(); + _textRenderer->draw(batch, leftMargin / scale, -topMargin / scale, text, textColor, bounds / scale); } diff --git a/libraries/entities-renderer/src/RenderableWebEntityItem.cpp b/libraries/entities-renderer/src/RenderableWebEntityItem.cpp index d7d7013f59..109c4cbfe9 100644 --- a/libraries/entities-renderer/src/RenderableWebEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableWebEntityItem.cpp @@ -216,7 +216,7 @@ void RenderableWebEntityItem::render(RenderArgs* args) { if (!_texture) { auto webSurface = _webSurface; - _texture = gpu::TexturePointer(gpu::Texture::createExternal2D(OffscreenQmlSurface::getDiscardLambda())); + _texture = gpu::TexturePointer(gpu::Texture::createExternal(OffscreenQmlSurface::getDiscardLambda())); _texture->setSource(__FUNCTION__); } OffscreenQmlSurface::TextureAndFence newTextureAndFence; @@ -266,7 +266,7 @@ void RenderableWebEntityItem::loadSourceURL() { _webSurface->setMaxFps(DEFAULT_MAX_FPS); } - _webSurface->load("WebView.qml", [&](QQmlContext* context, QObject* obj) { + _webSurface->load("WebEntityView.qml", [&](QQmlContext* context, QObject* obj) { context->setContextProperty("eventBridgeJavaScriptToInject", QVariant(_javaScriptToInject)); }); diff --git a/libraries/entities-renderer/src/RenderableZoneEntityItem.cpp b/libraries/entities-renderer/src/RenderableZoneEntityItem.cpp index 0215ce4d07..c9a45465c2 100644 --- a/libraries/entities-renderer/src/RenderableZoneEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableZoneEntityItem.cpp @@ -118,13 +118,13 @@ void RenderableZoneEntityItem::render(RenderArgs* args) { // check to see if when we added our models to the scene they were ready, if they were not ready, then // fix them up in the scene render::ScenePointer scene = AbstractViewStateInterface::instance()->getMain3DScene(); - render::PendingChanges pendingChanges; - _model->removeFromScene(scene, pendingChanges); + render::Transaction transaction; + _model->removeFromScene(scene, transaction); render::Item::Status::Getters statusGetters; makeEntityItemStatusGetters(getThisPointer(), statusGetters); - _model->addToScene(scene, pendingChanges); + _model->addToScene(scene, transaction); - scene->enqueuePendingChanges(pendingChanges); + scene->enqueueTransaction(transaction); _model->setVisibleInScene(getVisible(), scene); } @@ -164,9 +164,9 @@ void RenderableZoneEntityItem::render(RenderArgs* args) { _model && !_model->needsFixupInScene()) { // If the model is in the scene but doesn't need to be, remove it. render::ScenePointer scene = AbstractViewStateInterface::instance()->getMain3DScene(); - render::PendingChanges pendingChanges; - _model->removeFromScene(scene, pendingChanges); - scene->enqueuePendingChanges(pendingChanges); + render::Transaction transaction; + _model->removeFromScene(scene, transaction); + scene->enqueueTransaction(transaction); } } @@ -218,7 +218,7 @@ namespace render { } bool RenderableZoneEntityItem::addToScene(EntityItemPointer self, std::shared_ptr scene, - render::PendingChanges& pendingChanges) { + render::Transaction& transaction) { _myMetaItem = scene->allocateID(); auto renderData = std::make_shared(self); @@ -228,16 +228,16 @@ bool RenderableZoneEntityItem::addToScene(EntityItemPointer self, std::shared_pt makeEntityItemStatusGetters(getThisPointer(), statusGetters); renderPayload->addStatusGetters(statusGetters); - pendingChanges.resetItem(_myMetaItem, renderPayload); + transaction.resetItem(_myMetaItem, renderPayload); return true; } void RenderableZoneEntityItem::removeFromScene(EntityItemPointer self, std::shared_ptr scene, - render::PendingChanges& pendingChanges) { - pendingChanges.removeItem(_myMetaItem); + render::Transaction& transaction) { + transaction.removeItem(_myMetaItem); render::Item::clearID(_myMetaItem); if (_model) { - _model->removeFromScene(scene, pendingChanges); + _model->removeFromScene(scene, transaction); } } @@ -246,13 +246,13 @@ void RenderableZoneEntityItem::notifyBoundChanged() { if (!render::Item::isValidID(_myMetaItem)) { return; } - render::PendingChanges pendingChanges; + render::Transaction transaction; render::ScenePointer scene = AbstractViewStateInterface::instance()->getMain3DScene(); if (scene) { - pendingChanges.updateItem(_myMetaItem, [](RenderableZoneEntityItemMeta& data) { + transaction.updateItem(_myMetaItem, [](RenderableZoneEntityItemMeta& data) { }); - scene->enqueuePendingChanges(pendingChanges); + scene->enqueueTransaction(transaction); } else { qCWarning(entitiesrenderer) << "RenderableZoneEntityItem::notifyBoundChanged(), Unexpected null scene, possibly during application shutdown"; } diff --git a/libraries/entities-renderer/src/RenderableZoneEntityItem.h b/libraries/entities-renderer/src/RenderableZoneEntityItem.h index 58aa951e64..bbb7ed0c01 100644 --- a/libraries/entities-renderer/src/RenderableZoneEntityItem.h +++ b/libraries/entities-renderer/src/RenderableZoneEntityItem.h @@ -38,8 +38,8 @@ public: virtual void render(RenderArgs* args) override; virtual bool contains(const glm::vec3& point) const override; - virtual bool addToScene(EntityItemPointer self, std::shared_ptr scene, render::PendingChanges& pendingChanges) override; - virtual void removeFromScene(EntityItemPointer self, std::shared_ptr scene, render::PendingChanges& pendingChanges) override; + virtual bool addToScene(EntityItemPointer self, std::shared_ptr scene, render::Transaction& transaction) override; + virtual void removeFromScene(EntityItemPointer self, std::shared_ptr scene, render::Transaction& transaction) override; private: virtual void locationChanged(bool tellPhysics = true) override { EntityItem::locationChanged(tellPhysics); notifyBoundChanged(); } diff --git a/libraries/entities-renderer/src/polyvox.slf b/libraries/entities-renderer/src/polyvox.slf index bebefa9434..56f6f31d71 100644 --- a/libraries/entities-renderer/src/polyvox.slf +++ b/libraries/entities-renderer/src/polyvox.slf @@ -11,7 +11,6 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -<@include gpu/Inputs.slh@> <@include model/Material.slh@> <@include DeferredBufferWrite.slh@> diff --git a/libraries/entities/CMakeLists.txt b/libraries/entities/CMakeLists.txt index 1230fe8146..b2ae0f0ab7 100644 --- a/libraries/entities/CMakeLists.txt +++ b/libraries/entities/CMakeLists.txt @@ -1,6 +1,6 @@ set(TARGET_NAME entities) setup_hifi_library(Network Script) -link_hifi_libraries(avatars shared audio octree model fbx networking animation) +link_hifi_libraries(avatars shared audio octree model model-networking fbx networking animation) target_bullet() diff --git a/libraries/entities/src/EntitiesScriptEngineProvider.h b/libraries/entities/src/EntitiesScriptEngineProvider.h index 69bf73e688..d87dd105c2 100644 --- a/libraries/entities/src/EntitiesScriptEngineProvider.h +++ b/libraries/entities/src/EntitiesScriptEngineProvider.h @@ -15,11 +15,13 @@ #define hifi_EntitiesScriptEngineProvider_h #include +#include #include "EntityItemID.h" class EntitiesScriptEngineProvider { public: virtual void callEntityScriptMethod(const EntityItemID& entityID, const QString& methodName, const QStringList& params = QStringList()) = 0; + virtual QFuture getLocalEntityScriptDetails(const EntityItemID& entityID) = 0; }; -#endif // hifi_EntitiesScriptEngineProvider_h \ No newline at end of file +#endif // hifi_EntitiesScriptEngineProvider_h diff --git a/libraries/entities/src/EntityEditPacketSender.cpp b/libraries/entities/src/EntityEditPacketSender.cpp index 00f85f5078..7845b0d5e3 100644 --- a/libraries/entities/src/EntityEditPacketSender.cpp +++ b/libraries/entities/src/EntityEditPacketSender.cpp @@ -60,8 +60,10 @@ void EntityEditPacketSender::queueEditAvatarEntityMessage(PacketType type, // the ID of the parent/avatar changes from session to session. use a special UUID to indicate the avatar QJsonObject jsonObject = jsonProperties.object(); - if (QUuid(jsonObject["parentID"].toString()) == _myAvatar->getID()) { - jsonObject["parentID"] = AVATAR_SELF_ID.toString(); + if (jsonObject.contains("parentID")) { + if (QUuid(jsonObject["parentID"].toString()) == _myAvatar->getID()) { + jsonObject["parentID"] = AVATAR_SELF_ID.toString(); + } } jsonProperties = QJsonDocument(jsonObject); diff --git a/libraries/entities/src/EntityItem.cpp b/libraries/entities/src/EntityItem.cpp index 3ef1648fae..3f732e26cb 100644 --- a/libraries/entities/src/EntityItem.cpp +++ b/libraries/entities/src/EntityItem.cpp @@ -655,13 +655,11 @@ int EntityItem::readEntityDataFromBuffer(const unsigned char* data, int bytesLef // pack SimulationOwner and terse update properties near each other - // NOTE: the server is authoritative for changes to simOwnerID so we always unpack ownership data // even when we would otherwise ignore the rest of the packet. bool filterRejection = false; if (propertyFlags.getHasProperty(PROP_SIMULATION_OWNER)) { - QByteArray simOwnerData; int bytes = OctreePacketData::unpackDataFromBytes(dataAt, simOwnerData); SimulationOwner newSimOwner; @@ -685,7 +683,7 @@ int EntityItem::readEntityDataFromBuffer(const unsigned char* data, int bytesLef // However, for now, when the server uses a newer time than what we sent, listen to what we're told. if (overwriteLocalData) weOwnSimulation = false; } else if (_simulationOwner.set(newSimOwner)) { - _dirtyFlags |= Simulation::DIRTY_SIMULATOR_ID; + markDirtyFlags(Simulation::DIRTY_SIMULATOR_ID); somethingChanged = true; // recompute weOwnSimulation for later weOwnSimulation = _simulationOwner.matchesValidID(myNodeID); @@ -697,19 +695,19 @@ int EntityItem::readEntityDataFromBuffer(const unsigned char* data, int bytesLef weOwnSimulation = true; if (!_simulationOwner.isNull()) { // someone else really did own it - _dirtyFlags |= Simulation::DIRTY_SIMULATOR_ID; + markDirtyFlags(Simulation::DIRTY_SIMULATOR_ID); somethingChanged = true; _simulationOwner.clearCurrentOwner(); } } else if (newSimOwner.matchesValidID(myNodeID) && !_hasBidOnSimulation) { // entity-server tells us that we have simulation ownership while we never requested this for this EntityItem, // this could happen when the user reloads the cache and entity tree. - _dirtyFlags |= Simulation::DIRTY_SIMULATOR_ID; + markDirtyFlags(Simulation::DIRTY_SIMULATOR_ID); somethingChanged = true; _simulationOwner.clearCurrentOwner(); weOwnSimulation = false; } else if (_simulationOwner.set(newSimOwner)) { - _dirtyFlags |= Simulation::DIRTY_SIMULATOR_ID; + markDirtyFlags(Simulation::DIRTY_SIMULATOR_ID); somethingChanged = true; // recompute weOwnSimulation for later weOwnSimulation = _simulationOwner.matchesValidID(myNodeID); @@ -911,19 +909,23 @@ void EntityItem::adjustEditPacketForClockSkew(QByteArray& buffer, qint64 clockSk float EntityItem::computeMass() const { glm::vec3 dimensions = getDimensions(); - return _density * _volumeMultiplier * dimensions.x * dimensions.y * dimensions.z; + return getDensity() * _volumeMultiplier * dimensions.x * dimensions.y * dimensions.z; } void EntityItem::setDensity(float density) { - _density = glm::max(glm::min(density, ENTITY_ITEM_MAX_DENSITY), ENTITY_ITEM_MIN_DENSITY); + withWriteLock([&] { + _density = glm::max(glm::min(density, ENTITY_ITEM_MAX_DENSITY), ENTITY_ITEM_MIN_DENSITY); + }); } void EntityItem::updateDensity(float density) { float clampedDensity = glm::max(glm::min(density, ENTITY_ITEM_MAX_DENSITY), ENTITY_ITEM_MIN_DENSITY); - if (_density != clampedDensity) { - _density = clampedDensity; - _dirtyFlags |= Simulation::DIRTY_MASS; - } + withWriteLock([&] { + if (_density != clampedDensity) { + _density = clampedDensity; + _dirtyFlags |= Simulation::DIRTY_MASS; + } + }); } void EntityItem::setMass(float mass) { @@ -943,10 +945,12 @@ void EntityItem::setMass(float mass) { } else { newDensity = glm::max(glm::min(mass / volume, ENTITY_ITEM_MAX_DENSITY), ENTITY_ITEM_MIN_DENSITY); } - if (_density != newDensity) { - _density = newDensity; - _dirtyFlags |= Simulation::DIRTY_MASS; - } + withWriteLock([&] { + if (_density != newDensity) { + _density = newDensity; + _dirtyFlags |= Simulation::DIRTY_MASS; + } + }); } void EntityItem::setHref(QString value) { @@ -954,32 +958,47 @@ void EntityItem::setHref(QString value) { if (! (value.toLower().startsWith("hifi://")) ) { return; } - _href = value; + withWriteLock([&] { + _href = value; + }); } void EntityItem::setCollisionSoundURL(const QString& value) { - if (_collisionSoundURL != value) { - _collisionSoundURL = value; - + bool modified = false; + withWriteLock([&] { + if (_collisionSoundURL != value) { + _collisionSoundURL = value; + modified = true; + } + }); + if (modified) { if (auto myTree = getTree()) { - myTree->notifyNewCollisionSoundURL(_collisionSoundURL, getEntityItemID()); + myTree->notifyNewCollisionSoundURL(value, getEntityItemID()); } } } SharedSoundPointer EntityItem::getCollisionSound() { - if (!_collisionSound) { - _collisionSound = DependencyManager::get()->getSound(_collisionSoundURL); + SharedSoundPointer result; + withReadLock([&] { + result = _collisionSound; + }); + + if (!result) { + result = DependencyManager::get()->getSound(_collisionSoundURL); + withWriteLock([&] { + _collisionSound = result; + }); } - return _collisionSound; + return result; } void EntityItem::simulate(const quint64& now) { - if (_lastSimulated == 0) { - _lastSimulated = now; + if (getLastSimulated() == 0) { + setLastSimulated(now); } - float timeElapsed = (float)(now - _lastSimulated) / (float)(USECS_PER_SECOND); + float timeElapsed = (float)(now - getLastSimulated()) / (float)(USECS_PER_SECOND); #ifdef WANT_DEBUG qCDebug(entities) << "********** EntityItem::simulate()"; @@ -1023,10 +1042,10 @@ void EntityItem::simulate(const quint64& now) { if (!stepKinematicMotion(timeElapsed)) { // this entity is no longer moving // flag it to transition from KINEMATIC to STATIC - _dirtyFlags |= Simulation::DIRTY_MOTION_TYPE; + markDirtyFlags(Simulation::DIRTY_MOTION_TYPE); setAcceleration(Vectors::ZERO); } - _lastSimulated = now; + setLastSimulated(now); } bool EntityItem::stepKinematicMotion(float timeElapsed) { @@ -1058,9 +1077,10 @@ bool EntityItem::stepKinematicMotion(float timeElapsed) { timeElapsed = glm::min(timeElapsed, MAX_TIME_ELAPSED); if (isSpinning) { + float angularDamping = getAngularDamping(); // angular damping - if (_angularDamping > 0.0f) { - angularVelocity *= powf(1.0f - _angularDamping, timeElapsed); + if (angularDamping > 0.0f) { + angularVelocity *= powf(1.0f - angularDamping, timeElapsed); } const float MIN_KINEMATIC_ANGULAR_SPEED_SQUARED = @@ -1088,15 +1108,17 @@ bool EntityItem::stepKinematicMotion(float timeElapsed) { glm::vec3 deltaVelocity = Vectors::ZERO; // linear damping - if (_damping > 0.0f) { - deltaVelocity = (powf(1.0f - _damping, timeElapsed) - 1.0f) * linearVelocity; + float damping = getDamping(); + if (damping > 0.0f) { + deltaVelocity = (powf(1.0f - damping, timeElapsed) - 1.0f) * linearVelocity; } const float MIN_KINEMATIC_LINEAR_ACCELERATION_SQUARED = 1.0e-4f; // 0.01 m/sec^2 - if (glm::length2(_acceleration) > MIN_KINEMATIC_LINEAR_ACCELERATION_SQUARED) { + vec3 acceleration = getAcceleration(); + if (glm::length2(acceleration) > MIN_KINEMATIC_LINEAR_ACCELERATION_SQUARED) { // yes acceleration // acceleration is in world-frame but we need it in local-frame - glm::vec3 linearAcceleration = _acceleration; + glm::vec3 linearAcceleration = acceleration; bool success; Transform parentTransform = getParentTransform(success); if (success) { @@ -1182,7 +1204,7 @@ bool EntityItem::lifetimeHasExpired() const { } quint64 EntityItem::getExpiry() const { - return _created + (quint64)(_lifetime * (float)USECS_PER_SECOND); + return getCreated() + (quint64)(getLifetime() * (float)USECS_PER_SECOND); } EntityItemProperties EntityItem::getProperties(EntityPropertyFlags desiredProperties) const { @@ -1191,10 +1213,10 @@ EntityItemProperties EntityItem::getProperties(EntityPropertyFlags desiredProper EntityItemProperties properties(propertyFlags); properties._id = getID(); properties._idSet = true; - properties._created = _created; - properties._lastEdited = _lastEdited; - properties.setClientOnly(_clientOnly); - properties.setOwningAvatarID(_owningAvatarID); + properties._created = getCreated(); + properties._lastEdited = getLastEdited(); + properties.setClientOnly(getClientOnly()); + properties.setOwningAvatarID(getOwningAvatarID()); properties._type = getType(); @@ -1261,7 +1283,7 @@ void EntityItem::getAllTerseUpdateProperties(EntityItemProperties& properties) c properties._angularVelocity = getLocalAngularVelocity(); } if (!properties._accelerationChanged) { - properties._acceleration = _acceleration; + properties._acceleration = getAcceleration(); } properties._positionChanged = true; @@ -1272,7 +1294,7 @@ void EntityItem::getAllTerseUpdateProperties(EntityItemProperties& properties) c } void EntityItem::pokeSimulationOwnership() { - _dirtyFlags |= Simulation::DIRTY_SIMULATION_OWNERSHIP_FOR_POKE; + markDirtyFlags(Simulation::DIRTY_SIMULATION_OWNERSHIP_FOR_POKE); auto nodeList = DependencyManager::get(); if (_simulationOwner.matchesValidID(nodeList->getSessionUUID())) { // we already own it @@ -1284,7 +1306,7 @@ void EntityItem::pokeSimulationOwnership() { } void EntityItem::grabSimulationOwnership() { - _dirtyFlags |= Simulation::DIRTY_SIMULATION_OWNERSHIP_FOR_GRAB; + markDirtyFlags(Simulation::DIRTY_SIMULATION_OWNERSHIP_FOR_GRAB); auto nodeList = DependencyManager::get(); if (_simulationOwner.matchesValidID(nodeList->getSessionUUID())) { // we already own it @@ -1577,18 +1599,18 @@ float EntityItem::getVolumeEstimate() const { void EntityItem::updateRegistrationPoint(const glm::vec3& value) { if (value != _registrationPoint) { setRegistrationPoint(value); - _dirtyFlags |= Simulation::DIRTY_SHAPE; + markDirtyFlags(Simulation::DIRTY_SHAPE); } } void EntityItem::updatePosition(const glm::vec3& value) { if (getLocalPosition() != value) { setLocalPosition(value); - _dirtyFlags |= Simulation::DIRTY_POSITION; + markDirtyFlags(Simulation::DIRTY_POSITION); forEachDescendant([&](SpatiallyNestablePointer object) { if (object->getNestableType() == NestableType::Entity) { EntityItemPointer entity = std::static_pointer_cast(object); - entity->_dirtyFlags |= Simulation::DIRTY_POSITION; + entity->markDirtyFlags(Simulation::DIRTY_POSITION); } }); } @@ -1597,8 +1619,9 @@ void EntityItem::updatePosition(const glm::vec3& value) { void EntityItem::updateParentID(const QUuid& value) { if (getParentID() != value) { setParentID(value); - _dirtyFlags |= Simulation::DIRTY_MOTION_TYPE; // children are forced to be kinematic - _dirtyFlags |= Simulation::DIRTY_COLLISION_GROUP; // may need to not collide with own avatar + // children are forced to be kinematic + // may need to not collide with own avatar + markDirtyFlags(Simulation::DIRTY_MOTION_TYPE | Simulation::DIRTY_COLLISION_GROUP); } } @@ -1612,7 +1635,7 @@ void EntityItem::updatePositionFromNetwork(const glm::vec3& value) { void EntityItem::updateDimensions(const glm::vec3& value) { if (getDimensions() != value) { setDimensions(value); - _dirtyFlags |= (Simulation::DIRTY_SHAPE | Simulation::DIRTY_MASS); + markDirtyFlags(Simulation::DIRTY_SHAPE | Simulation::DIRTY_MASS); } } @@ -1623,8 +1646,7 @@ void EntityItem::updateRotation(const glm::quat& rotation) { forEachDescendant([&](SpatiallyNestablePointer object) { if (object->getNestableType() == NestableType::Entity) { EntityItemPointer entity = std::static_pointer_cast(object); - entity->_dirtyFlags |= Simulation::DIRTY_ROTATION; - entity->_dirtyFlags |= Simulation::DIRTY_POSITION; + entity->markDirtyFlags(Simulation::DIRTY_ROTATION | Simulation::DIRTY_POSITION); } }); } @@ -1779,20 +1801,26 @@ void EntityItem::updateRestitution(float value) { void EntityItem::updateFriction(float value) { float clampedValue = glm::max(glm::min(ENTITY_ITEM_MAX_FRICTION, value), ENTITY_ITEM_MIN_FRICTION); - if (_friction != clampedValue) { - _friction = clampedValue; - _dirtyFlags |= Simulation::DIRTY_MATERIAL; - } + withWriteLock([&] { + if (_friction != clampedValue) { + _friction = clampedValue; + _dirtyFlags |= Simulation::DIRTY_MATERIAL; + } + }); } void EntityItem::setRestitution(float value) { float clampedValue = glm::max(glm::min(ENTITY_ITEM_MAX_RESTITUTION, value), ENTITY_ITEM_MIN_RESTITUTION); - _restitution = clampedValue; + withWriteLock([&] { + _restitution = clampedValue; + }); } void EntityItem::setFriction(float value) { float clampedValue = glm::max(glm::min(ENTITY_ITEM_MAX_FRICTION, value), ENTITY_ITEM_MIN_FRICTION); - _friction = clampedValue; + withWriteLock([&] { + _friction = clampedValue; + }); } void EntityItem::updateLifetime(float value) { @@ -1879,12 +1907,13 @@ void EntityItem::setSimulationOwner(const SimulationOwner& owner) { } void EntityItem::updateSimulationOwner(const SimulationOwner& owner) { + // NOTE: this method only used by EntityServer. The Interface uses special code in readEntityDataFromBuffer(). if (wantTerseEditLogging() && _simulationOwner != owner) { qCDebug(entities) << "sim ownership for" << getDebugName() << "is now" << owner; } if (_simulationOwner.set(owner)) { - _dirtyFlags |= Simulation::DIRTY_SIMULATOR_ID; + markDirtyFlags(Simulation::DIRTY_SIMULATOR_ID); } } @@ -1894,9 +1923,10 @@ void EntityItem::clearSimulationOwnership() { } _simulationOwner.clear(); - // don't bother setting the DIRTY_SIMULATOR_ID flag because clearSimulationOwnership() - // is only ever called on the entity-server and the flags are only used client-side - //_dirtyFlags |= Simulation::DIRTY_SIMULATOR_ID; + // don't bother setting the DIRTY_SIMULATOR_ID flag because: + // (a) when entity-server calls clearSimulationOwnership() the dirty-flags are meaningless (only used by interface) + // (b) the interface only calls clearSimulationOwnership() in a context that already knows best about dirty flags + //markDirtyFlags(Simulation::DIRTY_SIMULATOR_ID); } @@ -2103,7 +2133,7 @@ void EntityItem::deserializeActionsInternal() { static QString repeatedMessage = LogHandler::getInstance().addRepeatedMessageRegex(".*action creation failed for.*"); qCDebug(entities) << "EntityItem::deserializeActionsInternal -- action creation failed for" - << getID() << getName(); + << getID() << _name; // getName(); removeActionInternal(actionID, nullptr); } } @@ -2327,3 +2357,443 @@ bool EntityItem::matchesJSONFilters(const QJsonObject& jsonFilters) const { // the json filter syntax did not match what we expected, return a match return true; } + +quint64 EntityItem::getLastSimulated() const { + quint64 result; + withReadLock([&] { + result = _lastSimulated; + }); + return result; +} + +void EntityItem::setLastSimulated(quint64 now) { + withWriteLock([&] { + _lastSimulated = now; + }); +} + +quint64 EntityItem::getLastEdited() const { + quint64 result; + withReadLock([&] { + result = _lastEdited; + }); + return result; +} + +void EntityItem::setLastEdited(quint64 lastEdited) { + withWriteLock([&] { + _lastEdited = _lastUpdated = lastEdited; + _changedOnServer = glm::max(lastEdited, _changedOnServer); + }); +} + +quint64 EntityItem::getLastBroadcast() const { + quint64 result; + withReadLock([&] { + result = _lastBroadcast; + }); + return result; +} + +void EntityItem::setLastBroadcast(quint64 lastBroadcast) { + withWriteLock([&] { + _lastBroadcast = lastBroadcast; + }); +} + +void EntityItem::markAsChangedOnServer() { + withWriteLock([&] { + _changedOnServer = usecTimestampNow(); + }); +} + +quint64 EntityItem::getLastChangedOnServer() const { + quint64 result; + withReadLock([&] { + result = _changedOnServer; + }); + return result; +} + +void EntityItem::update(const quint64& now) { + withWriteLock([&] { + _lastUpdated = now; + }); +} + +quint64 EntityItem::getLastUpdated() const { + quint64 result; + withReadLock([&] { + result = _lastUpdated; + }); + return result; +} + +void EntityItem::requiresRecalcBoxes() { + withWriteLock([&] { + _recalcAABox = true; + _recalcMinAACube = true; + _recalcMaxAACube = true; + }); +} + +QString EntityItem::getHref() const { + QString result; + withReadLock([&] { + result = _href; + }); + return result; +} + +QString EntityItem::getDescription() const { + QString result; + withReadLock([&] { + result = _description; + }); + return result; +} + +void EntityItem::setDescription(const QString& value) { + withWriteLock([&] { + _description = value; + }); +} + +float EntityItem::getLocalRenderAlpha() const { + float result; + withReadLock([&] { + result = _localRenderAlpha; + }); + return result; +} + +void EntityItem::setLocalRenderAlpha(float localRenderAlpha) { + withWriteLock([&] { + _localRenderAlpha = localRenderAlpha; + }); +} + +glm::vec3 EntityItem::getGravity() const { + glm::vec3 result; + withReadLock([&] { + result = _gravity; + }); + return result; +} + +void EntityItem::setGravity(const glm::vec3& value) { + withWriteLock([&] { + _gravity = value; + }); +} + +glm::vec3 EntityItem::getAcceleration() const { + glm::vec3 result; + withReadLock([&] { + result = _acceleration; + }); + return result; +} + +void EntityItem::setAcceleration(const glm::vec3& value) { + withWriteLock([&] { + _acceleration = value; + }); +} + +float EntityItem::getDamping() const { + float result; + withReadLock([&] { + result = _damping; + }); + return result; +} +void EntityItem::setDamping(float value) { + withWriteLock([&] { + _damping = value; + }); +} + +float EntityItem::getRestitution() const { + float result; + withReadLock([&] { + result = _restitution; + }); + return result; +} + +float EntityItem::getFriction() const { + float result; + withReadLock([&] { + result = _friction; + }); + return result; +} + +// lifetime related properties. +float EntityItem::getLifetime() const { + float result; + withReadLock([&] { + result = _lifetime; + }); + return result; +} + +void EntityItem::setLifetime(float value) { + withWriteLock([&] { + _lifetime = value; + }); +} + +quint64 EntityItem::getCreated() const { + quint64 result; + withReadLock([&] { + result = _created; + }); + return result; +} + +void EntityItem::setCreated(quint64 value) { + withWriteLock([&] { + _created = value; + }); +} + +QString EntityItem::getScript() const { + QString result; + withReadLock([&] { + result = _script; + }); + return result; +} + +void EntityItem::setScript(const QString& value) { + withWriteLock([&] { + _script = value; + }); +} + +quint64 EntityItem::getScriptTimestamp() const { + quint64 result; + withReadLock([&] { + result = _scriptTimestamp; + }); + return result; +} + +void EntityItem::setScriptTimestamp(const quint64 value) { + withWriteLock([&] { + _scriptTimestamp = value; + }); +} + +QString EntityItem::getServerScripts() const { + QString result; + withReadLock([&] { + result = _serverScripts; + }); + return result; +} + +void EntityItem::setServerScripts(const QString& serverScripts) { + withWriteLock([&] { + _serverScripts = serverScripts; + _serverScriptsChangedTimestamp = usecTimestampNow(); + }); +} + +QString EntityItem::getCollisionSoundURL() const { + QString result; + withReadLock([&] { + result = _collisionSoundURL; + }); + return result; +} + +void EntityItem::setCollisionSound(SharedSoundPointer sound) { + withWriteLock([&] { + _collisionSound = sound; + }); +} + +glm::vec3 EntityItem::getRegistrationPoint() const { + glm::vec3 result; + withReadLock([&] { + result = _registrationPoint; + }); + return result; +} + +void EntityItem::setRegistrationPoint(const glm::vec3& value) { + withWriteLock([&] { + _registrationPoint = glm::clamp(value, 0.0f, 1.0f); + }); + dimensionsChanged(); // Registration Point affects the bounding box +} + +float EntityItem::getAngularDamping() const { + float result; + withReadLock([&] { + result = _angularDamping; + }); + return result; +} + +void EntityItem::setAngularDamping(float value) { + withWriteLock([&] { + _angularDamping = value; + }); +} + +QString EntityItem::getName() const { + QString result; + withReadLock([&] { + result = _name; + }); + return result; +} + +void EntityItem::setName(const QString& value) { + withWriteLock([&] { + _name = value; + }); +} + +QString EntityItem::getDebugName() { + QString result = getName(); + if (result.isEmpty()) { + result = getID().toString(); + } + return result; +} + +bool EntityItem::getVisible() const { + bool result; + withReadLock([&] { + result = _visible; + }); + return result; +} + +void EntityItem::setVisible(bool value) { + withWriteLock([&] { + _visible = value; + }); +} + +bool EntityItem::getCollisionless() const { + bool result; + withReadLock([&] { + result = _collisionless; + }); + return result; +} + +void EntityItem::setCollisionless(bool value) { + withWriteLock([&] { + _collisionless = value; + }); +} + +uint8_t EntityItem::getCollisionMask() const { + uint8_t result; + withReadLock([&] { + result = _collisionMask; + }); + return result; +} + +void EntityItem::setCollisionMask(uint8_t value) { + withWriteLock([&] { + _collisionMask = value; + }); +} + +bool EntityItem::getDynamic() const { + if (SHAPE_TYPE_STATIC_MESH == getShapeType()) { + return false; + } + bool result; + withReadLock([&] { + result = _dynamic; + }); + return result; +} + +void EntityItem::setDynamic(bool value) { + withWriteLock([&] { + _dynamic = value; + }); +} + +bool EntityItem::getLocked() const { + bool result; + withReadLock([&] { + result = _locked; + }); + return result; +} + +void EntityItem::setLocked(bool value) { + withWriteLock([&] { + _locked = value; + }); +} + +QString EntityItem::getUserData() const { + QString result; + withReadLock([&] { + result = _userData; + }); + return result; +} + +void EntityItem::setUserData(const QString& value) { + withWriteLock([&] { + _userData = value; + }); +} + +QString EntityItem::getMarketplaceID() const { + QString result; + withReadLock([&] { + result = _marketplaceID; + }); + return result; +} + +void EntityItem::setMarketplaceID(const QString& value) { + withWriteLock([&] { + _marketplaceID = value; + }); +} + +uint32_t EntityItem::getDirtyFlags() const { + uint32_t result; + withReadLock([&] { + result = _dirtyFlags; + }); + return result; +} + + +void EntityItem::markDirtyFlags(uint32_t mask) { + withWriteLock([&] { + _dirtyFlags |= mask; + }); +} + +void EntityItem::clearDirtyFlags(uint32_t mask) { + withWriteLock([&] { + _dirtyFlags &= ~mask; + }); +} + +float EntityItem::getDensity() const { + float result; + withReadLock([&] { + result = _density; + }); + return result; +} + diff --git a/libraries/entities/src/EntityItem.h b/libraries/entities/src/EntityItem.h index 163b4d9e45..766ebfb4dc 100644 --- a/libraries/entities/src/EntityItem.h +++ b/libraries/entities/src/EntityItem.h @@ -53,7 +53,7 @@ using EntityTreeElementExtraEncodeDataPointer = std::shared_ptr scene, - render::PendingChanges& pendingChanges) { return false; } // by default entity items don't add to scene + render::Transaction& transaction) { return false; } // by default entity items don't add to scene virtual void removeFromScene(EntityItemPointer self, std::shared_ptr scene, - render::PendingChanges& pendingChanges) { } // by default entity items don't add to scene + render::Transaction& transaction) { } // by default entity items don't add to scene virtual void render(RenderArgs* args) { } // by default entity items don't know how to render static int expectedBytes(); @@ -161,8 +162,8 @@ public: static void adjustEditPacketForClockSkew(QByteArray& buffer, qint64 clockSkew); // perform update - virtual void update(const quint64& now) { _lastUpdated = now; } - quint64 getLastUpdated() const { return _lastUpdated; } + virtual void update(const quint64& now); + quint64 getLastUpdated() const; // perform linear extrapolation for SimpleEntitySimulation void simulate(const quint64& now); @@ -186,63 +187,63 @@ public: const Transform getTransformToCenter(bool& success) const; - inline void requiresRecalcBoxes() { _recalcAABox = true; _recalcMinAACube = true; _recalcMaxAACube = true; } + inline void requiresRecalcBoxes(); // Hyperlink related getters and setters - QString getHref() const { return _href; } + QString getHref() const; void setHref(QString value); - QString getDescription() const { return _description; } - void setDescription(QString value) { _description = value; } + QString getDescription() const; + void setDescription(const QString& value); /// Dimensions in meters (0.0 - TREE_SCALE) inline const glm::vec3 getDimensions() const { return getScale(); } virtual void setDimensions(const glm::vec3& value); - float getLocalRenderAlpha() const { return _localRenderAlpha; } - void setLocalRenderAlpha(float localRenderAlpha) { _localRenderAlpha = localRenderAlpha; } + float getLocalRenderAlpha() const; + void setLocalRenderAlpha(float localRenderAlpha); void setDensity(float density); float computeMass() const; void setMass(float mass); - float getDensity() const { return _density; } + float getDensity() const; bool hasVelocity() const { return getVelocity() != ENTITY_ITEM_ZERO_VEC3; } bool hasLocalVelocity() const { return getLocalVelocity() != ENTITY_ITEM_ZERO_VEC3; } - const glm::vec3& getGravity() const { return _gravity; } /// get gravity in meters - void setGravity(const glm::vec3& value) { _gravity = value; } /// gravity in meters - bool hasGravity() const { return _gravity != ENTITY_ITEM_ZERO_VEC3; } + glm::vec3 getGravity() const; /// get gravity in meters + void setGravity(const glm::vec3& value); /// gravity in meters + bool hasGravity() const { return getGravity() != ENTITY_ITEM_ZERO_VEC3; } - const glm::vec3& getAcceleration() const { return _acceleration; } /// get acceleration in meters/second/second - void setAcceleration(const glm::vec3& value) { _acceleration = value; } /// acceleration in meters/second/second - bool hasAcceleration() const { return _acceleration != ENTITY_ITEM_ZERO_VEC3; } + glm::vec3 getAcceleration() const; /// get acceleration in meters/second/second + void setAcceleration(const glm::vec3& value); /// acceleration in meters/second/second + bool hasAcceleration() const { return getAcceleration() != ENTITY_ITEM_ZERO_VEC3; } - float getDamping() const { return _damping; } - void setDamping(float value) { _damping = value; } + float getDamping() const; + void setDamping(float value); - float getRestitution() const { return _restitution; } + float getRestitution() const; void setRestitution(float value); - float getFriction() const { return _friction; } + float getFriction() const; void setFriction(float value); // lifetime related properties. - float getLifetime() const { return _lifetime; } /// get the lifetime in seconds for the entity - void setLifetime(float value) { _lifetime = value; } /// set the lifetime in seconds for the entity + float getLifetime() const; /// get the lifetime in seconds for the entity + void setLifetime(float value); /// set the lifetime in seconds for the entity - quint64 getCreated() const { return _created; } /// get the created-time in useconds for the entity - void setCreated(quint64 value) { _created = value; } /// set the created-time in useconds for the entity + quint64 getCreated() const; /// get the created-time in useconds for the entity + void setCreated(quint64 value); /// set the created-time in useconds for the entity /// is this entity immortal, in that it has no lifetime set, and will exist until manually deleted - bool isImmortal() const { return _lifetime == ENTITY_ITEM_IMMORTAL_LIFETIME; } + bool isImmortal() const { return getLifetime() == ENTITY_ITEM_IMMORTAL_LIFETIME; } /// is this entity mortal, in that it has a lifetime set, and will automatically be deleted when that lifetime expires - bool isMortal() const { return _lifetime != ENTITY_ITEM_IMMORTAL_LIFETIME; } + bool isMortal() const { return getLifetime() != ENTITY_ITEM_IMMORTAL_LIFETIME; } /// age of this entity in seconds - float getAge() const { return (float)(usecTimestampNow() - _created) / (float)USECS_PER_SECOND; } + float getAge() const { return (float)(usecTimestampNow() - getCreated()) / (float)USECS_PER_SECOND; } bool lifetimeHasExpired() const; quint64 getExpiry() const; @@ -254,63 +255,61 @@ public: using SpatiallyNestable::getQueryAACube; virtual AACube getQueryAACube(bool& success) const override; - QString getScript() const { return _script; } - void setScript(const QString& value) { _script = value; } + QString getScript() const; + void setScript(const QString& value); - quint64 getScriptTimestamp() const { return _scriptTimestamp; } - void setScriptTimestamp(const quint64 value) { _scriptTimestamp = value; } + quint64 getScriptTimestamp() const; + void setScriptTimestamp(const quint64 value); - QString getServerScripts() const { return _serverScripts; } - void setServerScripts(const QString& serverScripts) - { _serverScripts = serverScripts; _serverScriptsChangedTimestamp = usecTimestampNow(); } + QString getServerScripts() const; + void setServerScripts(const QString& serverScripts); - const QString& getCollisionSoundURL() const { return _collisionSoundURL; } + QString getCollisionSoundURL() const; void setCollisionSoundURL(const QString& value); SharedSoundPointer getCollisionSound(); - void setCollisionSound(SharedSoundPointer sound) { _collisionSound = sound; } + void setCollisionSound(SharedSoundPointer sound); - const glm::vec3& getRegistrationPoint() const { return _registrationPoint; } /// registration point as ratio of entity + glm::vec3 getRegistrationPoint() const; /// registration point as ratio of entity /// registration point as ratio of entity - void setRegistrationPoint(const glm::vec3& value) { - _registrationPoint = glm::clamp(value, 0.0f, 1.0f); dimensionsChanged(); // Registration Point affects the bounding box - } + void setRegistrationPoint(const glm::vec3& value); bool hasAngularVelocity() const { return getAngularVelocity() != ENTITY_ITEM_ZERO_VEC3; } bool hasLocalAngularVelocity() const { return getLocalAngularVelocity() != ENTITY_ITEM_ZERO_VEC3; } - float getAngularDamping() const { return _angularDamping; } - void setAngularDamping(float value) { _angularDamping = value; } + float getAngularDamping() const; + void setAngularDamping(float value); - QString getName() const { return _name; } - void setName(const QString& value) { _name = value; } - QString getDebugName() { return _name != "" ? _name : getID().toString(); } + QString getName() const; + void setName(const QString& value); + QString getDebugName(); - bool getVisible() const { return _visible; } - void setVisible(bool value) { _visible = value; } - bool isVisible() const { return _visible; } - bool isInvisible() const { return !_visible; } + bool getVisible() const; + void setVisible(bool value); + inline bool isVisible() const { return getVisible(); } + inline bool isInvisible() const { return !getVisible(); } - bool getCollisionless() const { return _collisionless; } - void setCollisionless(bool value) { _collisionless = value; } + bool getCollisionless() const; + void setCollisionless(bool value); - uint8_t getCollisionMask() const { return _collisionMask; } - void setCollisionMask(uint8_t value) { _collisionMask = value; } + uint8_t getCollisionMask() const; + void setCollisionMask(uint8_t value); void computeCollisionGroupAndFinalMask(int16_t& group, int16_t& mask) const; - bool getDynamic() const { return SHAPE_TYPE_STATIC_MESH == getShapeType() ? false : _dynamic; } - void setDynamic(bool value) { _dynamic = value; } + bool getDynamic() const; + void setDynamic(bool value); virtual bool shouldBePhysical() const { return false; } - bool getLocked() const { return _locked; } - void setLocked(bool value) { _locked = value; } + bool getLocked() const; + void setLocked(bool value); - const QString& getUserData() const { return _userData; } - virtual void setUserData(const QString& value) { _userData = value; } + QString getUserData() const; + virtual void setUserData(const QString& value); + // FIXME not thread safe? const SimulationOwner& getSimulationOwner() const { return _simulationOwner; } void setSimulationOwner(const QUuid& id, quint8 priority); void setSimulationOwner(const SimulationOwner& owner); @@ -323,8 +322,8 @@ public: void setPendingOwnershipPriority(quint8 priority, const quint64& timestamp); void rememberHasSimulationOwnershipBid() const; - const QString& getMarketplaceID() const { return _marketplaceID; } - void setMarketplaceID(const QString& value) { _marketplaceID = value; } + QString getMarketplaceID() const; + void setMarketplaceID(const QString& value); // TODO: get rid of users of getRadius()... float getRadius() const; @@ -367,8 +366,9 @@ public: void updateCreated(uint64_t value); virtual void setShapeType(ShapeType type) { /* do nothing */ } - uint32_t getDirtyFlags() const { return _dirtyFlags; } - void clearDirtyFlags(uint32_t mask = 0xffffffff) { _dirtyFlags &= ~mask; } + uint32_t getDirtyFlags() const; + void markDirtyFlags(uint32_t mask); + void clearDirtyFlags(uint32_t mask = 0xffffffff); bool isMoving() const; bool isMovingRelativeToParent() const; @@ -470,9 +470,11 @@ public: QUuid getLastEditedBy() const { return _lastEditedBy; } void setLastEditedBy(QUuid value) { _lastEditedBy = value; } - + bool matchesJSONFilters(const QJsonObject& jsonFilters) const; + virtual bool getMeshes(MeshProxyList& result) { return true; } + protected: void setSimulated(bool simulated) { _simulated = simulated; } diff --git a/libraries/entities/src/EntityItemProperties.cpp b/libraries/entities/src/EntityItemProperties.cpp index ea81df3801..1ed020e592 100644 --- a/libraries/entities/src/EntityItemProperties.cpp +++ b/libraries/entities/src/EntityItemProperties.cpp @@ -49,13 +49,6 @@ EntityItemProperties::EntityItemProperties(EntityPropertyFlags desiredProperties } -void EntityItemProperties::setSittingPoints(const QVector& sittingPoints) { - _sittingPoints.clear(); - foreach (SittingPoint sitPoint, sittingPoints) { - _sittingPoints.append(sitPoint); - } -} - void EntityItemProperties::calculateNaturalPosition(const glm::vec3& min, const glm::vec3& max) { glm::vec3 halfDimension = (max - min) / 2.0f; _naturalPosition = max - halfDimension; @@ -546,20 +539,6 @@ QScriptValue EntityItemProperties::copyToScriptValue(QScriptEngine* engine, bool COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_TEXTURES, textures); } - // Sitting properties support - if (!skipDefaults && !strictSemantics) { - QScriptValue sittingPoints = engine->newObject(); - for (int i = 0; i < _sittingPoints.size(); ++i) { - QScriptValue sittingPoint = engine->newObject(); - sittingPoint.setProperty("name", _sittingPoints.at(i).name); - sittingPoint.setProperty("position", vec3toScriptValue(engine, _sittingPoints.at(i).position)); - sittingPoint.setProperty("rotation", quatToScriptValue(engine, _sittingPoints.at(i).rotation)); - sittingPoints.setProperty(i, sittingPoint); - } - sittingPoints.setProperty("length", _sittingPoints.size()); - COPY_PROPERTY_TO_QSCRIPTVALUE_GETTER_ALWAYS(sittingPoints, sittingPoints); // gettable, but not settable - } - if (!skipDefaults && !strictSemantics) { AABox aaBox = getAABox(); QScriptValue boundingBox = engine->newObject(); diff --git a/libraries/entities/src/EntityItemProperties.h b/libraries/entities/src/EntityItemProperties.h index 419740e4ea..590298e102 100644 --- a/libraries/entities/src/EntityItemProperties.h +++ b/libraries/entities/src/EntityItemProperties.h @@ -22,7 +22,6 @@ #include #include -#include // for SittingPoint #include #include #include @@ -255,8 +254,6 @@ public: void clearID() { _id = UNKNOWN_ENTITY_ID; _idSet = false; } void markAllChanged(); - void setSittingPoints(const QVector& sittingPoints); - const glm::vec3& getNaturalDimensions() const { return _naturalDimensions; } void setNaturalDimensions(const glm::vec3& value) { _naturalDimensions = value; } @@ -325,7 +322,6 @@ private: // NOTE: The following are pseudo client only properties. They are only used in clients which can access // properties of model geometry. But these properties are not serialized like other properties. - QVector _sittingPoints; QVariantMap _textureNames; glm::vec3 _naturalDimensions; glm::vec3 _naturalPosition; diff --git a/libraries/entities/src/EntityScriptingInterface.cpp b/libraries/entities/src/EntityScriptingInterface.cpp index 540eba4511..2c332e8d05 100644 --- a/libraries/entities/src/EntityScriptingInterface.cpp +++ b/libraries/entities/src/EntityScriptingInterface.cpp @@ -8,12 +8,20 @@ // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // + +#include +#include + #include "EntityScriptingInterface.h" +#include +#include + #include "EntityItemID.h" #include #include #include +#include #include "EntitiesLogging.h" #include "EntityActionFactoryInterface.h" @@ -289,13 +297,11 @@ EntityItemProperties EntityScriptingInterface::getEntityProperties(QUuid identit results = entity->getProperties(desiredProperties); - // TODO: improve sitting points and naturalDimensions in the future, - // for now we've included the old sitting points model behavior for entity types that are models - // we've also added this hack for setting natural dimensions of models + // TODO: improve naturalDimensions in the future, + // for now we've added this hack for setting natural dimensions of models if (entity->getType() == EntityTypes::Model) { const FBXGeometry* geometry = _entityTree->getGeometryForEntity(entity); if (geometry) { - results.setSittingPoints(geometry->sittingPoints); Extents meshExtents = geometry->getUnscaledMeshExtents(); results.setNaturalDimensions(meshExtents.maximum - meshExtents.minimum); results.calculateNaturalPosition(meshExtents.minimum, meshExtents.maximum); @@ -668,7 +674,6 @@ RayToEntityIntersectionResult EntityScriptingInterface::findRayIntersectionWorke (void**)&intersectedEntity, lockType, &result.accurate); if (result.intersects && intersectedEntity) { result.entityID = intersectedEntity->getEntityItemID(); - result.properties = intersectedEntity->getProperties(); result.intersection = ray.origin + (ray.direction * result.distance); } } @@ -680,6 +685,118 @@ bool EntityScriptingInterface::reloadServerScripts(QUuid entityID) { return client->reloadServerScript(entityID); } +bool EntityPropertyMetadataRequest::script(EntityItemID entityID, QScriptValue handler) { + using LocalScriptStatusRequest = QFutureWatcher; + + LocalScriptStatusRequest* request = new LocalScriptStatusRequest; + QObject::connect(request, &LocalScriptStatusRequest::finished, _engine, [=]() mutable { + auto details = request->result().toMap(); + QScriptValue err, result; + if (details.contains("isError")) { + if (!details.contains("message")) { + details["message"] = details["errorInfo"]; + } + err = _engine->makeError(_engine->toScriptValue(details)); + } else { + details["success"] = true; + result = _engine->toScriptValue(details); + } + callScopedHandlerObject(handler, err, result); + request->deleteLater(); + }); + auto entityScriptingInterface = DependencyManager::get(); + entityScriptingInterface->withEntitiesScriptEngine([&](EntitiesScriptEngineProvider* entitiesScriptEngine) { + if (entitiesScriptEngine) { + request->setFuture(entitiesScriptEngine->getLocalEntityScriptDetails(entityID)); + } + }); + if (!request->isStarted()) { + request->deleteLater(); + callScopedHandlerObject(handler, _engine->makeError("Entities Scripting Provider unavailable", "InternalError"), QScriptValue()); + return false; + } + return true; +} + +bool EntityPropertyMetadataRequest::serverScripts(EntityItemID entityID, QScriptValue handler) { + auto client = DependencyManager::get(); + auto request = client->createScriptStatusRequest(entityID); + QPointer engine = _engine; + QObject::connect(request, &GetScriptStatusRequest::finished, _engine, [=](GetScriptStatusRequest* request) mutable { + auto engine = _engine; + if (!engine) { + qCDebug(entities) << __FUNCTION__ << " -- engine destroyed while inflight" << entityID; + return; + } + QVariantMap details; + details["success"] = request->getResponseReceived(); + details["isRunning"] = request->getIsRunning(); + details["status"] = EntityScriptStatus_::valueToKey(request->getStatus()).toLower(); + details["errorInfo"] = request->getErrorInfo(); + + QScriptValue err, result; + if (!details["success"].toBool()) { + if (!details.contains("message") && details.contains("errorInfo")) { + details["message"] = details["errorInfo"]; + } + if (details["message"].toString().isEmpty()) { + details["message"] = "entity server script details not found"; + } + err = engine->makeError(engine->toScriptValue(details)); + } else { + result = engine->toScriptValue(details); + } + callScopedHandlerObject(handler, err, result); + request->deleteLater(); + }); + request->start(); + return true; +} + +bool EntityScriptingInterface::queryPropertyMetadata(QUuid entityID, QScriptValue property, QScriptValue scopeOrCallback, QScriptValue methodOrName) { + auto name = property.toString(); + auto handler = makeScopedHandlerObject(scopeOrCallback, methodOrName); + QPointer engine = dynamic_cast(handler.engine()); + if (!engine) { + qCDebug(entities) << "queryPropertyMetadata without detectable engine" << entityID << name; + return false; + } +#ifdef DEBUG_ENGINE_STATE + connect(engine, &QObject::destroyed, this, [=]() { + qDebug() << "queryPropertyMetadata -- engine destroyed!" << (!engine ? "nullptr" : "engine"); + }); +#endif + if (!handler.property("callback").isFunction()) { + qDebug() << "!handler.callback.isFunction" << engine; + engine->raiseException(engine->makeError("callback is not a function", "TypeError")); + return false; + } + + // NOTE: this approach is a work-in-progress and for now just meant to work 100% correctly and provide + // some initial structure for organizing metadata adapters around. + + // The extra layer of indirection is *essential* because in real world conditions errors are often introduced + // by accident and sometimes without exact memory of "what just changed." + + // Here the scripter only needs to know an entityID and a property name -- which means all scripters can + // level this method when stuck in dead-end scenarios or to learn more about "magic" Entity properties + // like .script that work in terms of side-effects. + + // This is an async callback pattern -- so if needed C++ can easily throttle or restrict queries later. + + EntityPropertyMetadataRequest request(engine); + + if (name == "script") { + return request.script(entityID, handler); + } else if (name == "serverScripts") { + return request.serverScripts(entityID, handler); + } else { + engine->raiseException(engine->makeError("metadata for property " + name + " is not yet queryable")); + engine->maybeEmitUncaughtException(__FUNCTION__); + return false; + } +} + bool EntityScriptingInterface::getServerScriptStatus(QUuid entityID, QScriptValue callback) { auto client = DependencyManager::get(); auto request = client->createScriptStatusRequest(entityID); @@ -721,7 +838,6 @@ RayToEntityIntersectionResult::RayToEntityIntersectionResult() : intersects(false), accurate(true), // assume it's accurate entityID(), - properties(), distance(0), face(), entity(NULL) @@ -737,9 +853,6 @@ QScriptValue RayToEntityIntersectionResultToScriptValue(QScriptEngine* engine, c QScriptValue entityItemValue = EntityItemIDtoScriptValue(engine, value.entityID); obj.setProperty("entityID", entityItemValue); - QScriptValue propertiesValue = EntityItemPropertiesToScriptValue(engine, value.properties); - obj.setProperty("properties", propertiesValue); - obj.setProperty("distance", value.distance); QString faceName = ""; @@ -785,10 +898,6 @@ void RayToEntityIntersectionResultFromScriptValue(const QScriptValue& object, Ra QScriptValue entityIDValue = object.property("entityID"); // EntityItemIDfromScriptValue(entityIDValue, value.entityID); quuidFromScriptValue(entityIDValue, value.entityID); - QScriptValue entityPropertiesValue = object.property("properties"); - if (entityPropertiesValue.isValid()) { - EntityItemPropertiesFromScriptValueHonorReadOnly(entityPropertiesValue, value.properties); - } value.distance = object.property("distance").toVariant().toFloat(); QString faceName = object.property("face").toVariant().toString(); @@ -815,8 +924,7 @@ void RayToEntityIntersectionResultFromScriptValue(const QScriptValue& object, Ra } } -bool EntityScriptingInterface::setVoxels(QUuid entityID, - std::function actor) { +bool EntityScriptingInterface::polyVoxWorker(QUuid entityID, std::function actor) { PROFILE_RANGE(script_entities, __FUNCTION__); if (!_entityTree) { @@ -882,11 +990,9 @@ bool EntityScriptingInterface::setPoints(QUuid entityID, std::function(_entityTree->findEntityByEntityItemID(entityID)); + if (!entity) { + qCDebug(entities) << "EntityScriptingInterface::getMeshes no entity with ID" << entityID; + QScriptValueList args { callback.engine()->undefinedValue(), false }; + callback.call(QScriptValue(), args); + return; + } + + MeshProxyList result; + bool success = entity->getMeshes(result); + + if (success) { + QScriptValue resultAsScriptValue = meshesToScriptValue(callback.engine(), result); + QScriptValueList args { resultAsScriptValue, true }; + callback.call(QScriptValue(), args); + } else { + QScriptValueList args { callback.engine()->undefinedValue(), false }; + callback.call(QScriptValue(), args); + } +} + +glm::mat4 EntityScriptingInterface::getEntityTransform(const QUuid& entityID) { + glm::mat4 result; + if (_entityTree) { + _entityTree->withReadLock([&] { + EntityItemPointer entity = _entityTree->findEntityByEntityItemID(EntityItemID(entityID)); + if (entity) { + glm::mat4 translation = glm::translate(entity->getPosition()); + glm::mat4 rotation = glm::mat4_cast(entity->getRotation()); + glm::mat4 registration = glm::translate(ENTITY_ITEM_DEFAULT_REGISTRATION_POINT - + entity->getRegistrationPoint()); + result = translation * rotation * registration; + } + }); + } + return result; +} + +glm::mat4 EntityScriptingInterface::getEntityLocalTransform(const QUuid& entityID) { + glm::mat4 result; + if (_entityTree) { + _entityTree->withReadLock([&] { + EntityItemPointer entity = _entityTree->findEntityByEntityItemID(EntityItemID(entityID)); + if (entity) { + glm::mat4 translation = glm::translate(entity->getLocalPosition()); + glm::mat4 rotation = glm::mat4_cast(entity->getLocalOrientation()); + glm::mat4 registration = glm::translate(ENTITY_ITEM_DEFAULT_REGISTRATION_POINT - + entity->getRegistrationPoint()); + result = translation * rotation * registration; + } + }); + } + return result; +} diff --git a/libraries/entities/src/EntityScriptingInterface.h b/libraries/entities/src/EntityScriptingInterface.h index e9f0637830..b25764790e 100644 --- a/libraries/entities/src/EntityScriptingInterface.h +++ b/libraries/entities/src/EntityScriptingInterface.h @@ -34,7 +34,23 @@ #include "EntitiesScriptEngineProvider.h" #include "EntityItemProperties.h" +#include "BaseScriptEngine.h" + class EntityTree; +class MeshProxy; + +// helper factory to compose standardized, async metadata queries for "magic" Entity properties +// like .script and .serverScripts. This is used for automated testing of core scripting features +// as well as to provide early adopters a self-discoverable, consistent way to diagnose common +// problems with their own Entity scripts. +class EntityPropertyMetadataRequest { +public: + EntityPropertyMetadataRequest(BaseScriptEngine* engine) : _engine(engine) {}; + bool script(EntityItemID entityID, QScriptValue handler); + bool serverScripts(EntityItemID entityID, QScriptValue handler); +private: + QPointer _engine; +}; class RayToEntityIntersectionResult { public: @@ -42,7 +58,6 @@ public: bool intersects; bool accurate; QUuid entityID; - EntityItemProperties properties; float distance; BoxFace face; glm::vec3 intersection; @@ -67,6 +82,7 @@ class EntityScriptingInterface : public OctreeScriptingInterface, public Depende Q_PROPERTY(float costMultiplier READ getCostMultiplier WRITE setCostMultiplier) Q_PROPERTY(QUuid keyboardFocusEntity READ getKeyboardFocusEntity WRITE setKeyboardFocusEntity) + friend EntityPropertyMetadataRequest; public: EntityScriptingInterface(bool bidOnSimulationOwnership); @@ -211,6 +227,26 @@ public slots: Q_INVOKABLE RayToEntityIntersectionResult findRayIntersectionBlocking(const PickRay& ray, bool precisionPicking = false, const QScriptValue& entityIdsToInclude = QScriptValue(), const QScriptValue& entityIdsToDiscard = QScriptValue()); Q_INVOKABLE bool reloadServerScripts(QUuid entityID); + + /**jsdoc + * Query additional metadata for "magic" Entity properties like `script` and `serverScripts`. + * + * @function Entities.queryPropertyMetadata + * @param {EntityID} entityID The ID of the entity. + * @param {string} property The name of the property extended metadata is wanted for. + * @param {ResultCallback} callback Executes callback(err, result) with the query results. + */ + /**jsdoc + * Query additional metadata for "magic" Entity properties like `script` and `serverScripts`. + * + * @function Entities.queryPropertyMetadata + * @param {EntityID} entityID The ID of the entity. + * @param {string} property The name of the property extended metadata is wanted for. + * @param {Object} thisObject The scoping "this" context that callback will be executed within. + * @param {ResultCallback} callbackOrMethodName Executes thisObject[callbackOrMethodName](err, result) with the query results. + */ + Q_INVOKABLE bool queryPropertyMetadata(QUuid entityID, QScriptValue property, QScriptValue scopeOrCallback, QScriptValue methodOrName = QScriptValue()); + Q_INVOKABLE bool getServerScriptStatus(QUuid entityID, QScriptValue callback); Q_INVOKABLE void setLightsArePickable(bool value); @@ -293,6 +329,27 @@ public slots: const glm::vec3& start, const glm::vec3& end, float radius); + Q_INVOKABLE void getMeshes(QUuid entityID, QScriptValue callback); + + /**jsdoc + * Returns object to world transform, excluding scale + * + * @function Entities.getEntityTransform + * @param {EntityID} entityID The ID of the entity whose transform is to be returned + * @return {Mat4} Entity's object to world transform, excluding scale + */ + Q_INVOKABLE glm::mat4 getEntityTransform(const QUuid& entityID); + + + /**jsdoc + * Returns object to world transform, excluding scale + * + * @function Entities.getEntityLocalTransform + * @param {EntityID} entityID The ID of the entity whose local transform is to be returned + * @return {Mat4} Entity's object to parent transform, excluding scale + */ + Q_INVOKABLE glm::mat4 getEntityLocalTransform(const QUuid& entityID); + signals: void collisionWithEntity(const EntityItemID& idA, const EntityItemID& idB, const Collision& collision); @@ -323,9 +380,14 @@ signals: void webEventReceived(const EntityItemID& entityItemID, const QVariant& message); +protected: + void withEntitiesScriptEngine(std::function function) { + std::lock_guard lock(_entitiesScriptEngineLock); + function(_entitiesScriptEngine); + }; private: bool actionWorker(const QUuid& entityID, std::function actor); - bool setVoxels(QUuid entityID, std::function actor); + bool polyVoxWorker(QUuid entityID, std::function actor); bool setPoints(QUuid entityID, std::function actor); void queueEntityMessage(PacketType packetType, EntityItemID entityID, const EntityItemProperties& properties); diff --git a/libraries/entities/src/EntityTree.cpp b/libraries/entities/src/EntityTree.cpp index d7471474a6..f544a4e5c7 100644 --- a/libraries/entities/src/EntityTree.cpp +++ b/libraries/entities/src/EntityTree.cpp @@ -182,6 +182,7 @@ bool EntityTree::updateEntityWithElement(EntityItemPointer entity, const EntityI if (!wantsLocked) { EntityItemProperties tempProperties; tempProperties.setLocked(wantsLocked); + tempProperties.setLastEdited(properties.getLastEdited()); bool success; AACube queryCube = entity->getQueryAACube(success); diff --git a/libraries/entities/src/LightEntityItem.cpp b/libraries/entities/src/LightEntityItem.cpp index e09822f028..753ff1d3c8 100644 --- a/libraries/entities/src/LightEntityItem.cpp +++ b/libraries/entities/src/LightEntityItem.cpp @@ -69,38 +69,59 @@ EntityItemProperties LightEntityItem::getProperties(EntityPropertyFlags desiredP } void LightEntityItem::setFalloffRadius(float value) { - _falloffRadius = glm::max(value, 0.0f); - _lightPropertiesChanged = true; + value = glm::max(value, 0.0f); + if (value == getFalloffRadius()) { + return; + } + withWriteLock([&] { + _falloffRadius = value; + _lightPropertiesChanged = true; + }); } void LightEntityItem::setIsSpotlight(bool value) { - if (value != _isSpotlight) { - _isSpotlight = value; - - glm::vec3 dimensions = getDimensions(); - if (_isSpotlight) { - const float length = dimensions.z; - const float width = length * glm::sin(glm::radians(_cutoff)); - setDimensions(glm::vec3(width, width, length)); - } else { - float maxDimension = glm::compMax(dimensions); - setDimensions(glm::vec3(maxDimension, maxDimension, maxDimension)); - } - _lightPropertiesChanged = true; + if (value == getIsSpotlight()) { + return; } + + glm::vec3 dimensions = getDimensions(); + glm::vec3 newDimensions; + if (value) { + const float length = dimensions.z; + const float width = length * glm::sin(glm::radians(getCutoff())); + newDimensions = glm::vec3(width, width, length); + } else { + newDimensions = glm::vec3(glm::compMax(dimensions)); + } + + withWriteLock([&] { + _isSpotlight = value; + _lightPropertiesChanged = true; + }); + setDimensions(newDimensions); } void LightEntityItem::setCutoff(float value) { - _cutoff = glm::clamp(value, 0.0f, 90.0f); + value = glm::clamp(value, 0.0f, 90.0f); + if (value == getCutoff()) { + return; + } - if (_isSpotlight) { + withWriteLock([&] { + _cutoff = value; + }); + + if (getIsSpotlight()) { // If we are a spotlight, adjusting the cutoff will affect the area we encapsulate, // so update the dimensions to reflect this. const float length = getDimensions().z; const float width = length * glm::sin(glm::radians(_cutoff)); setDimensions(glm::vec3(width, width, length)); } - _lightPropertiesChanged = true; + + withWriteLock([&] { + _lightPropertiesChanged = true; + }); } bool LightEntityItem::setProperties(const EntityItemProperties& properties) { @@ -205,5 +226,86 @@ void LightEntityItem::appendSubclassData(OctreePacketData* packetData, EncodeBit void LightEntityItem::somethingChangedNotification() { EntityItem::somethingChangedNotification(); - _lightPropertiesChanged = false; + withWriteLock([&] { + _lightPropertiesChanged = false; + }); } + +const rgbColor& LightEntityItem::getColor() const { + return _color; +} + +xColor LightEntityItem::getXColor() const { + xColor color = { _color[RED_INDEX], _color[GREEN_INDEX], _color[BLUE_INDEX] }; return color; +} + +void LightEntityItem::setColor(const rgbColor& value) { + withWriteLock([&] { + memcpy(_color, value, sizeof(_color)); + _lightPropertiesChanged = true; + }); +} + +void LightEntityItem::setColor(const xColor& value) { + withWriteLock([&] { + _color[RED_INDEX] = value.red; + _color[GREEN_INDEX] = value.green; + _color[BLUE_INDEX] = value.blue; + _lightPropertiesChanged = true; + }); +} + +bool LightEntityItem::getIsSpotlight() const { + bool result; + withReadLock([&] { + result = _isSpotlight; + }); + return result; +} + +float LightEntityItem::getIntensity() const { + float result; + withReadLock([&] { + result = _intensity; + }); + return result; +} + +void LightEntityItem::setIntensity(float value) { + withWriteLock([&] { + _intensity = value; + _lightPropertiesChanged = true; + }); +} + +float LightEntityItem::getFalloffRadius() const { + float result; + withReadLock([&] { + result = _falloffRadius; + }); + return result; +} + +float LightEntityItem::getExponent() const { + float result; + withReadLock([&] { + result = _exponent; + }); + return result; +} + +void LightEntityItem::setExponent(float value) { + withWriteLock([&] { + _exponent = value; + _lightPropertiesChanged = true; + }); +} + +float LightEntityItem::getCutoff() const { + float result; + withReadLock([&] { + result = _cutoff; + }); + return result; +} + diff --git a/libraries/entities/src/LightEntityItem.h b/libraries/entities/src/LightEntityItem.h index 3444b11cae..b3cadcf0c7 100644 --- a/libraries/entities/src/LightEntityItem.h +++ b/libraries/entities/src/LightEntityItem.h @@ -57,47 +57,33 @@ public: EntityPropertyFlags& propertyFlags, bool overwriteLocalData, bool& somethingChanged) override; - const rgbColor& getColor() const { return _color; } - xColor getXColor() const { - xColor color = { _color[RED_INDEX], _color[GREEN_INDEX], _color[BLUE_INDEX] }; return color; - } + const rgbColor& getColor() const; + xColor getXColor() const; - void setColor(const rgbColor& value) { memcpy(_color, value, sizeof(_color)); } - void setColor(const xColor& value) { - _color[RED_INDEX] = value.red; - _color[GREEN_INDEX] = value.green; - _color[BLUE_INDEX] = value.blue; - _lightPropertiesChanged = true; - } + void setColor(const rgbColor& value); + void setColor(const xColor& value); - bool getIsSpotlight() const { return _isSpotlight; } + bool getIsSpotlight() const; void setIsSpotlight(bool value); void setIgnoredColor(const rgbColor& value) { } void setIgnoredAttenuation(float value) { } - float getIntensity() const { return _intensity; } - void setIntensity(float value) { - _intensity = value; - _lightPropertiesChanged = true; - } - - float getFalloffRadius() const { return _falloffRadius; } + float getIntensity() const; + void setIntensity(float value); + float getFalloffRadius() const; void setFalloffRadius(float value); - float getExponent() const { return _exponent; } - void setExponent(float value) { - _exponent = value; - _lightPropertiesChanged = true; - } + float getExponent() const; + void setExponent(float value); - float getCutoff() const { return _cutoff; } + float getCutoff() const; void setCutoff(float value); static bool getLightsArePickable() { return _lightsArePickable; } static void setLightsArePickable(bool value) { _lightsArePickable = value; } -protected: +private: // properties of a light @@ -108,6 +94,7 @@ protected: float _exponent { DEFAULT_EXPONENT }; float _cutoff { DEFAULT_CUTOFF }; +protected: // Dirty flag turn true when either light properties is changing values. // This gets back to false in the somethingChangedNotification() call // Which is called after a setProperties() or a readEntitySubClassFromBUfferCall on the entity. diff --git a/libraries/entities/src/LineEntityItem.cpp b/libraries/entities/src/LineEntityItem.cpp index 8ace665616..58cdb1cd7b 100644 --- a/libraries/entities/src/LineEntityItem.cpp +++ b/libraries/entities/src/LineEntityItem.cpp @@ -34,8 +34,8 @@ EntityItemPointer LineEntityItem::factory(const EntityItemID& entityID, const En LineEntityItem::LineEntityItem(const EntityItemID& entityItemID) : EntityItem(entityItemID), _lineWidth(DEFAULT_LINE_WIDTH), - _pointsChanged(true), - _points(QVector(0)) + _points(QVector(0)), + _pointsChanged(true) { _type = EntityTypes::Line; } @@ -88,8 +88,10 @@ bool LineEntityItem::appendPoint(const glm::vec3& point) { qCDebug(entities) << "Point is outside entity's bounding box"; return false; } - _points << point; - _pointsChanged = true; + withWriteLock([&] { + _points << point; + _pointsChanged = true; + }); return true; } @@ -105,8 +107,11 @@ bool LineEntityItem::setLinePoints(const QVector& points) { return false; } } - _points = points; - _pointsChanged = true; + + withWriteLock([&] { + _points = points; + _pointsChanged = true; + }); return true; } @@ -159,3 +164,51 @@ void LineEntityItem::debugDump() const { qCDebug(entities) << " getLastEdited:" << debugTime(getLastEdited(), now); } + +const rgbColor& LineEntityItem::getColor() const { + return _color; +} + +xColor LineEntityItem::getXColor() const { + xColor result; + withReadLock([&] { + result = { _color[RED_INDEX], _color[GREEN_INDEX], _color[BLUE_INDEX] }; + }); + return result; +} + +void LineEntityItem::setColor(const rgbColor& value) { + withWriteLock([&] { + memcpy(_color, value, sizeof(_color)); + }); +} + +void LineEntityItem::setColor(const xColor& value) { + withWriteLock([&] { + _color[RED_INDEX] = value.red; + _color[GREEN_INDEX] = value.green; + _color[BLUE_INDEX] = value.blue; + }); +} + +void LineEntityItem::setLineWidth(float lineWidth) { + withWriteLock([&] { + _lineWidth = lineWidth; + }); +} + +float LineEntityItem::getLineWidth() const { + float result; + withReadLock([&] { + result = _lineWidth; + }); + return result; +} + +QVector LineEntityItem::getLinePoints() const { + QVector result; + withReadLock([&] { + result = _points; + }); + return result; +} diff --git a/libraries/entities/src/LineEntityItem.h b/libraries/entities/src/LineEntityItem.h index 8629c94eb4..bb8ae8a21a 100644 --- a/libraries/entities/src/LineEntityItem.h +++ b/libraries/entities/src/LineEntityItem.h @@ -42,23 +42,19 @@ class LineEntityItem : public EntityItem { EntityPropertyFlags& propertyFlags, bool overwriteLocalData, bool& somethingChanged) override; - const rgbColor& getColor() const { return _color; } - xColor getXColor() const { xColor color = { _color[RED_INDEX], _color[GREEN_INDEX], _color[BLUE_INDEX] }; return color; } + const rgbColor& getColor() const; + xColor getXColor() const; - void setColor(const rgbColor& value) { memcpy(_color, value, sizeof(_color)); } - void setColor(const xColor& value) { - _color[RED_INDEX] = value.red; - _color[GREEN_INDEX] = value.green; - _color[BLUE_INDEX] = value.blue; - } + void setColor(const rgbColor& value); + void setColor(const xColor& value); - void setLineWidth(float lineWidth){ _lineWidth = lineWidth; } - float getLineWidth() const{ return _lineWidth; } + void setLineWidth(float lineWidth); + float getLineWidth() const; bool setLinePoints(const QVector& points); bool appendPoint(const glm::vec3& point); - const QVector& getLinePoints() const{ return _points; } + QVector getLinePoints() const; virtual ShapeType getShapeType() const override { return SHAPE_TYPE_NONE; } @@ -74,11 +70,12 @@ class LineEntityItem : public EntityItem { static const float DEFAULT_LINE_WIDTH; static const int MAX_POINTS_PER_LINE; - protected: + private: rgbColor _color; float _lineWidth; - bool _pointsChanged; QVector _points; +protected: + bool _pointsChanged; }; #endif // hifi_LineEntityItem_h diff --git a/libraries/entities/src/ParticleEffectEntityItem.cpp b/libraries/entities/src/ParticleEffectEntityItem.cpp index 140522b00e..80f7f3b6b8 100644 --- a/libraries/entities/src/ParticleEffectEntityItem.cpp +++ b/libraries/entities/src/ParticleEffectEntityItem.cpp @@ -733,3 +733,20 @@ void ParticleEffectEntityItem::setMaxParticles(quint32 maxParticles) { _timeUntilNextEmit = 0.0f; } } + +QString ParticleEffectEntityItem::getTextures() const { + QString result; + withReadLock([&] { + result = _textures; + }); + return result; +} + +void ParticleEffectEntityItem::setTextures(const QString& textures) { + withWriteLock([&] { + if (_textures != textures) { + _textures = textures; + _texturesChangedFlag = true; + } + }); +} diff --git a/libraries/entities/src/ParticleEffectEntityItem.h b/libraries/entities/src/ParticleEffectEntityItem.h index 6c6596e7a2..4d053e5a45 100644 --- a/libraries/entities/src/ParticleEffectEntityItem.h +++ b/libraries/entities/src/ParticleEffectEntityItem.h @@ -205,13 +205,8 @@ public: void computeAndUpdateDimensions(); static const QString DEFAULT_TEXTURES; - const QString& getTextures() const { return _textures; } - void setTextures(const QString& textures) { - if (_textures != textures) { - _textures = textures; - _texturesChangedFlag = true; - } - } + QString getTextures() const; + void setTextures(const QString& textures); static const bool DEFAULT_EMITTER_SHOULD_TRAIL; bool getEmitterShouldTrail() const { return _emitterShouldTrail; } diff --git a/libraries/entities/src/PolyLineEntityItem.cpp b/libraries/entities/src/PolyLineEntityItem.cpp index 7abafad627..9fe8ac0c8f 100644 --- a/libraries/entities/src/PolyLineEntityItem.cpp +++ b/libraries/entities/src/PolyLineEntityItem.cpp @@ -104,14 +104,18 @@ bool PolyLineEntityItem::appendPoint(const glm::vec3& point) { bool PolyLineEntityItem::setStrokeWidths(const QVector& strokeWidths) { - _strokeWidths = strokeWidths; - _strokeWidthsChanged = true; + withWriteLock([&] { + _strokeWidths = strokeWidths; + _strokeWidthsChanged = true; + }); return true; } bool PolyLineEntityItem::setNormals(const QVector& normals) { - _normals = normals; - _normalsChanged = true; + withWriteLock([&] { + _normals = normals; + _normalsChanged = true; + }); return true; } @@ -119,35 +123,39 @@ bool PolyLineEntityItem::setLinePoints(const QVector& points) { if (points.size() > MAX_POINTS_PER_LINE) { return false; } - if (points.size() != _points.size()) { - _pointsChanged = true; - } - //Check to see if points actually changed. If they haven't, return before doing anything else - else if (points.size() == _points.size()) { - //same number of points, so now compare every point - for (int i = 0; i < points.size(); i++) { - if (points.at(i) != _points.at(i)){ - _pointsChanged = true; - break; + bool result = false; + withWriteLock([&] { + //Check to see if points actually changed. If they haven't, return before doing anything else + if (points.size() != _points.size()) { + _pointsChanged = true; + } else if (points.size() == _points.size()) { + //same number of points, so now compare every point + for (int i = 0; i < points.size(); i++) { + if (points.at(i) != _points.at(i)) { + _pointsChanged = true; + break; + } } } - } - if (!_pointsChanged) { - return false; - } - - for (int i = 0; i < points.size(); i++) { - glm::vec3 point = points.at(i); - glm::vec3 halfBox = getDimensions() * 0.5f; - if ((point.x < -halfBox.x || point.x > halfBox.x) || - (point.y < -halfBox.y || point.y > halfBox.y) || - (point.z < -halfBox.z || point.z > halfBox.z)) { - qCDebug(entities) << "Point is outside entity's bounding box"; - return false; + if (!_pointsChanged) { + return; } - } - _points = points; - return true; + + for (int i = 0; i < points.size(); i++) { + glm::vec3 point = points.at(i); + glm::vec3 halfBox = getDimensions() * 0.5f; + if ((point.x < -halfBox.x || point.x > halfBox.x) || + (point.y < -halfBox.y || point.y > halfBox.y) || + (point.z < -halfBox.z || point.z > halfBox.z)) { + qCDebug(entities) << "Point is outside entity's bounding box"; + return; + } + } + _points = points; + result = true; + }); + + return result; } int PolyLineEntityItem::readEntitySubclassDataFromBuffer(const unsigned char* data, int bytesLeftToRead, @@ -210,3 +218,45 @@ void PolyLineEntityItem::debugDump() const { qCDebug(entities) << " getLastEdited:" << debugTime(getLastEdited(), now); } + + +QVector PolyLineEntityItem::getLinePoints() const { + QVector result; + withReadLock([&] { + result = _points; + }); + return result; +} + +QVector PolyLineEntityItem::getNormals() const { + QVector result; + withReadLock([&] { + result = _normals; + }); + return result; +} + +QVector PolyLineEntityItem::getStrokeWidths() const { + QVector result; + withReadLock([&] { + result = _strokeWidths; + }); + return result; +} + +QString PolyLineEntityItem::getTextures() const { + QString result; + withReadLock([&] { + result = _textures; + }); + return result; +} + +void PolyLineEntityItem::setTextures(const QString& textures) { + withWriteLock([&] { + if (_textures != textures) { + _textures = textures; + _texturesChangedFlag = true; + } + }); +} diff --git a/libraries/entities/src/PolyLineEntityItem.h b/libraries/entities/src/PolyLineEntityItem.h index 5f9f9124cf..0ee9a638de 100644 --- a/libraries/entities/src/PolyLineEntityItem.h +++ b/libraries/entities/src/PolyLineEntityItem.h @@ -59,21 +59,16 @@ class PolyLineEntityItem : public EntityItem { bool setLinePoints(const QVector& points); bool appendPoint(const glm::vec3& point); - const QVector& getLinePoints() const{ return _points; } + QVector getLinePoints() const; bool setNormals(const QVector& normals); - const QVector& getNormals() const{ return _normals; } + QVector getNormals() const; bool setStrokeWidths(const QVector& strokeWidths); - const QVector& getStrokeWidths() const{ return _strokeWidths; } + QVector getStrokeWidths() const; - const QString& getTextures() const { return _textures; } - void setTextures(const QString& textures) { - if (_textures != textures) { - _textures = textures; - _texturesChangedFlag = true; - } - } + QString getTextures() const; + void setTextures(const QString& textures); virtual bool needsToCallUpdate() const override { return true; } diff --git a/libraries/entities/src/PolyVoxEntityItem.cpp b/libraries/entities/src/PolyVoxEntityItem.cpp index 2a374c1d17..711c3b5625 100644 --- a/libraries/entities/src/PolyVoxEntityItem.cpp +++ b/libraries/entities/src/PolyVoxEntityItem.cpp @@ -242,3 +242,129 @@ const QByteArray PolyVoxEntityItem::getVoxelData() const { }); return voxelDataCopy; } + + +void PolyVoxEntityItem::setXTextureURL(const QString& xTextureURL) { + withWriteLock([&] { + _xTextureURL = xTextureURL; + }); +} + +QString PolyVoxEntityItem::getXTextureURL() const { + QString result; + withReadLock([&] { + result = _xTextureURL; + }); + return result; +} + +void PolyVoxEntityItem::setYTextureURL(const QString& yTextureURL) { + withWriteLock([&] { + _yTextureURL = yTextureURL; + }); +} + +QString PolyVoxEntityItem::getYTextureURL() const { + QString result; + withReadLock([&] { + result = _yTextureURL; + }); + return result; +} + +void PolyVoxEntityItem::setZTextureURL(const QString& zTextureURL) { + withWriteLock([&] { + _zTextureURL = zTextureURL; + }); +} +QString PolyVoxEntityItem::getZTextureURL() const { + QString result; + withReadLock([&] { + result = _zTextureURL; + }); + return result; +} + +void PolyVoxEntityItem::setXNNeighborID(const EntityItemID& xNNeighborID) { + withWriteLock([&] { + _xNNeighborID = xNNeighborID; + }); +} + +EntityItemID PolyVoxEntityItem::getXNNeighborID() const { + EntityItemID result; + withReadLock([&] { + result = _xNNeighborID; + }); + return result; +} + +void PolyVoxEntityItem::setYNNeighborID(const EntityItemID& yNNeighborID) { + withWriteLock([&] { + _yNNeighborID = yNNeighborID; + }); +} + +EntityItemID PolyVoxEntityItem::getYNNeighborID() const { + EntityItemID result; + withReadLock([&] { + result = _yNNeighborID; + }); + return result; +} + +void PolyVoxEntityItem::setZNNeighborID(const EntityItemID& zNNeighborID) { + withWriteLock([&] { + _zNNeighborID = zNNeighborID; + }); +} + +EntityItemID PolyVoxEntityItem::getZNNeighborID() const { + EntityItemID result; + withReadLock([&] { + result = _zNNeighborID; + }); + return result; +} + +void PolyVoxEntityItem::setXPNeighborID(const EntityItemID& xPNeighborID) { + withWriteLock([&] { + _xPNeighborID = xPNeighborID; + }); +} + +EntityItemID PolyVoxEntityItem::getXPNeighborID() const { + EntityItemID result; + withReadLock([&] { + result = _xPNeighborID; + }); + return result; +} + +void PolyVoxEntityItem::setYPNeighborID(const EntityItemID& yPNeighborID) { + withWriteLock([&] { + _yPNeighborID = yPNeighborID; + }); +} + +EntityItemID PolyVoxEntityItem::getYPNeighborID() const { + EntityItemID result; + withReadLock([&] { + result = _yPNeighborID; + }); + return result; +} + +void PolyVoxEntityItem::setZPNeighborID(const EntityItemID& zPNeighborID) { + withWriteLock([&] { + _zPNeighborID = zPNeighborID; + }); +} + +EntityItemID PolyVoxEntityItem::getZPNeighborID() const { + EntityItemID result; + withReadLock([&] { + result = _zPNeighborID; + }); + return result; +} diff --git a/libraries/entities/src/PolyVoxEntityItem.h b/libraries/entities/src/PolyVoxEntityItem.h index 910d8eff88..05b5cb33a6 100644 --- a/libraries/entities/src/PolyVoxEntityItem.h +++ b/libraries/entities/src/PolyVoxEntityItem.h @@ -57,6 +57,8 @@ class PolyVoxEntityItem : public EntityItem { virtual void setVoxelData(QByteArray voxelData); virtual const QByteArray getVoxelData() const; + virtual int getOnCount() const { return 0; } + enum PolyVoxSurfaceStyle { SURFACE_MARCHING_CUBES, SURFACE_CUBIC, @@ -97,41 +99,41 @@ class PolyVoxEntityItem : public EntityItem { static QByteArray makeEmptyVoxelData(quint16 voxelXSize = 16, quint16 voxelYSize = 16, quint16 voxelZSize = 16); static const QString DEFAULT_X_TEXTURE_URL; - virtual void setXTextureURL(QString xTextureURL) { _xTextureURL = xTextureURL; } - virtual const QString& getXTextureURL() const { return _xTextureURL; } + virtual void setXTextureURL(const QString& xTextureURL); + QString getXTextureURL() const; static const QString DEFAULT_Y_TEXTURE_URL; - virtual void setYTextureURL(QString yTextureURL) { _yTextureURL = yTextureURL; } - virtual const QString& getYTextureURL() const { return _yTextureURL; } + virtual void setYTextureURL(const QString& yTextureURL); + QString getYTextureURL() const; static const QString DEFAULT_Z_TEXTURE_URL; - virtual void setZTextureURL(QString zTextureURL) { _zTextureURL = zTextureURL; } - virtual const QString& getZTextureURL() const { return _zTextureURL; } + virtual void setZTextureURL(const QString& zTextureURL); + QString getZTextureURL() const; - virtual void setXNNeighborID(const EntityItemID& xNNeighborID) { _xNNeighborID = xNNeighborID; } - void setXNNeighborID(const QString& xNNeighborID) { setXNNeighborID(QUuid(xNNeighborID)); } - virtual const EntityItemID& getXNNeighborID() const { return _xNNeighborID; } - virtual void setYNNeighborID(const EntityItemID& yNNeighborID) { _yNNeighborID = yNNeighborID; } - void setYNNeighborID(const QString& yNNeighborID) { setYNNeighborID(QUuid(yNNeighborID)); } - virtual const EntityItemID& getYNNeighborID() const { return _yNNeighborID; } - virtual void setZNNeighborID(const EntityItemID& zNNeighborID) { _zNNeighborID = zNNeighborID; } - void setZNNeighborID(const QString& zNNeighborID) { setZNNeighborID(QUuid(zNNeighborID)); } - virtual const EntityItemID& getZNNeighborID() const { return _zNNeighborID; } + virtual void setXNNeighborID(const EntityItemID& xNNeighborID); + void setXNNeighborID(const QString& xNNeighborID); + virtual EntityItemID getXNNeighborID() const; + virtual void setYNNeighborID(const EntityItemID& yNNeighborID); + void setYNNeighborID(const QString& yNNeighborID); + virtual EntityItemID getYNNeighborID() const; + virtual void setZNNeighborID(const EntityItemID& zNNeighborID); + void setZNNeighborID(const QString& zNNeighborID); + virtual EntityItemID getZNNeighborID() const; - virtual void setXPNeighborID(const EntityItemID& xPNeighborID) { _xPNeighborID = xPNeighborID; } - void setXPNeighborID(const QString& xPNeighborID) { setXPNeighborID(QUuid(xPNeighborID)); } - virtual const EntityItemID& getXPNeighborID() const { return _xPNeighborID; } - virtual void setYPNeighborID(const EntityItemID& yPNeighborID) { _yPNeighborID = yPNeighborID; } - void setYPNeighborID(const QString& yPNeighborID) { setYPNeighborID(QUuid(yPNeighborID)); } - virtual const EntityItemID& getYPNeighborID() const { return _yPNeighborID; } - virtual void setZPNeighborID(const EntityItemID& zPNeighborID) { _zPNeighborID = zPNeighborID; } - void setZPNeighborID(const QString& zPNeighborID) { setZPNeighborID(QUuid(zPNeighborID)); } - virtual const EntityItemID& getZPNeighborID() const { return _zPNeighborID; } + virtual void setXPNeighborID(const EntityItemID& xPNeighborID); + void setXPNeighborID(const QString& xPNeighborID); + virtual EntityItemID getXPNeighborID() const; + virtual void setYPNeighborID(const EntityItemID& yPNeighborID); + void setYPNeighborID(const QString& yPNeighborID); + virtual EntityItemID getYPNeighborID() const; + virtual void setZPNeighborID(const EntityItemID& zPNeighborID); + void setZPNeighborID(const QString& zPNeighborID); + virtual EntityItemID getZPNeighborID() const; virtual void rebakeMesh() {}; void setVoxelDataDirty(bool value) { withWriteLock([&] { _voxelDataDirty = value; }); } - virtual void getMesh() {}; // recompute mesh + virtual void recomputeMesh() {}; protected: glm::vec3 _voxelVolumeSize; // this is always 3 bytes diff --git a/libraries/entities/src/PropertyGroup.h b/libraries/entities/src/PropertyGroup.h index 38b1e5f599..f45d19f5eb 100644 --- a/libraries/entities/src/PropertyGroup.h +++ b/libraries/entities/src/PropertyGroup.h @@ -14,9 +14,11 @@ #include -//#include "EntityItemProperties.h" +#include + #include "EntityPropertyFlags.h" + class EntityItemProperties; class EncodeBitstreamParams; class OctreePacketData; @@ -24,31 +26,6 @@ class EntityTreeElementExtraEncodeData; class ReadBitstreamToTreeParams; using EntityTreeElementExtraEncodeDataPointer = std::shared_ptr; -#include - -/* -#include - -#include -#include - -#include -#include -#include - -#include -#include // for SittingPoint -#include -#include -#include - -#include "EntityItemID.h" -#include "PropertyGroupMacros.h" -#include "EntityTypes.h" -*/ - -//typedef PropertyFlags EntityPropertyFlags; - class PropertyGroup { public: diff --git a/libraries/entities/src/ShapeEntityItem.cpp b/libraries/entities/src/ShapeEntityItem.cpp index eaef4c5c0e..345d9e54ab 100644 --- a/libraries/entities/src/ShapeEntityItem.cpp +++ b/libraries/entities/src/ShapeEntityItem.cpp @@ -163,7 +163,7 @@ void ShapeEntityItem::appendSubclassData(OctreePacketData* packetData, EncodeBit // This value specifes how the shape should be treated by physics calculations. // For now, all polys will act as spheres ShapeType ShapeEntityItem::getShapeType() const { - return (_shape == entity::Shape::Cube) ? SHAPE_TYPE_BOX : SHAPE_TYPE_SPHERE; + return (_shape == entity::Shape::Cube) ? SHAPE_TYPE_BOX : SHAPE_TYPE_ELLIPSOID; } void ShapeEntityItem::setColor(const rgbColor& value) { diff --git a/libraries/entities/src/TextEntityItem.cpp b/libraries/entities/src/TextEntityItem.cpp index fbb0bdc9cf..c6b857f6e7 100644 --- a/libraries/entities/src/TextEntityItem.cpp +++ b/libraries/entities/src/TextEntityItem.cpp @@ -141,3 +141,98 @@ bool TextEntityItem::findDetailedRayIntersection(const glm::vec3& origin, const // FIXME - should set face and surfaceNormal return findRayRectangleIntersection(origin, direction, rotation, position, xyDimensions, distance); } + +void TextEntityItem::setText(const QString& value) { + withWriteLock([&] { + _text = value; + }); +} + +QString TextEntityItem::getText() const { + QString result; + withReadLock([&] { + result = _text; + }); + return result; +} + +void TextEntityItem::setLineHeight(float value) { + withWriteLock([&] { + _lineHeight = value; + }); +} + +float TextEntityItem::getLineHeight() const { + float result; + withReadLock([&] { + result = _lineHeight; + }); + return result; +} + +const rgbColor& TextEntityItem::getTextColor() const { + return _textColor; +} + +const rgbColor& TextEntityItem::getBackgroundColor() const { + return _backgroundColor; +} + +xColor TextEntityItem::getTextColorX() const { + xColor result; + withReadLock([&] { + result = { _textColor[RED_INDEX], _textColor[GREEN_INDEX], _textColor[BLUE_INDEX] }; + }); + return result; +} + +void TextEntityItem::setTextColor(const rgbColor& value) { + withWriteLock([&] { + memcpy(_textColor, value, sizeof(_textColor)); + }); +} + +void TextEntityItem::setTextColor(const xColor& value) { + withWriteLock([&] { + _textColor[RED_INDEX] = value.red; + _textColor[GREEN_INDEX] = value.green; + _textColor[BLUE_INDEX] = value.blue; + }); +} + +xColor TextEntityItem::getBackgroundColorX() const { + xColor result; + withReadLock([&] { + result = { _backgroundColor[RED_INDEX], _backgroundColor[GREEN_INDEX], _backgroundColor[BLUE_INDEX] }; + }); + return result; +} + +void TextEntityItem::setBackgroundColor(const rgbColor& value) { + withWriteLock([&] { + memcpy(_backgroundColor, value, sizeof(_backgroundColor)); + }); +} + +void TextEntityItem::setBackgroundColor(const xColor& value) { + withWriteLock([&] { + _backgroundColor[RED_INDEX] = value.red; + _backgroundColor[GREEN_INDEX] = value.green; + _backgroundColor[BLUE_INDEX] = value.blue; + }); +} + +bool TextEntityItem::getFaceCamera() const { + bool result; + withReadLock([&] { + result = _faceCamera; + }); + return result; +} + +void TextEntityItem::setFaceCamera(bool value) { + withWriteLock([&] { + _faceCamera = value; + }); +} + diff --git a/libraries/entities/src/TextEntityItem.h b/libraries/entities/src/TextEntityItem.h index 633aa96bfa..ee421d567a 100644 --- a/libraries/entities/src/TextEntityItem.h +++ b/libraries/entities/src/TextEntityItem.h @@ -53,40 +53,34 @@ public: void** intersectedObject, bool precisionPicking) const override; static const QString DEFAULT_TEXT; - void setText(const QString& value) { _text = value; } - const QString& getText() const { return _text; } + void setText(const QString& value); + QString getText() const; static const float DEFAULT_LINE_HEIGHT; - void setLineHeight(float value) { _lineHeight = value; } - float getLineHeight() const { return _lineHeight; } + void setLineHeight(float value); + float getLineHeight() const; static const xColor DEFAULT_TEXT_COLOR; - const rgbColor& getTextColor() const { return _textColor; } - xColor getTextColorX() const { xColor color = { _textColor[RED_INDEX], _textColor[GREEN_INDEX], _textColor[BLUE_INDEX] }; return color; } + // FIXME should not return a reference because of thread safety, but can't return an array + const rgbColor& getTextColor() const; + xColor getTextColorX() const; - void setTextColor(const rgbColor& value) { memcpy(_textColor, value, sizeof(_textColor)); } - void setTextColor(const xColor& value) { - _textColor[RED_INDEX] = value.red; - _textColor[GREEN_INDEX] = value.green; - _textColor[BLUE_INDEX] = value.blue; - } + void setTextColor(const rgbColor& value); + void setTextColor(const xColor& value); static const xColor DEFAULT_BACKGROUND_COLOR; - const rgbColor& getBackgroundColor() const { return _backgroundColor; } - xColor getBackgroundColorX() const { xColor color = { _backgroundColor[RED_INDEX], _backgroundColor[GREEN_INDEX], _backgroundColor[BLUE_INDEX] }; return color; } + // FIXME should not return a reference because of thread safety, but can't return an array + const rgbColor& getBackgroundColor() const; + xColor getBackgroundColorX() const; - void setBackgroundColor(const rgbColor& value) { memcpy(_backgroundColor, value, sizeof(_backgroundColor)); } - void setBackgroundColor(const xColor& value) { - _backgroundColor[RED_INDEX] = value.red; - _backgroundColor[GREEN_INDEX] = value.green; - _backgroundColor[BLUE_INDEX] = value.blue; - } + void setBackgroundColor(const rgbColor& value); + void setBackgroundColor(const xColor& value); static const bool DEFAULT_FACE_CAMERA; - bool getFaceCamera() const { return _faceCamera; } - void setFaceCamera(bool value) { _faceCamera = value; } + bool getFaceCamera() const; + void setFaceCamera(bool value); -protected: +private: QString _text; float _lineHeight; rgbColor _textColor; diff --git a/libraries/entities/src/WebEntityItem.cpp b/libraries/entities/src/WebEntityItem.cpp index 182d58ba36..5b060cc702 100644 --- a/libraries/entities/src/WebEntityItem.cpp +++ b/libraries/entities/src/WebEntityItem.cpp @@ -124,18 +124,26 @@ bool WebEntityItem::findDetailedRayIntersection(const glm::vec3& origin, const g } void WebEntityItem::setSourceUrl(const QString& value) { - if (_sourceUrl != value) { - auto newURL = QUrl::fromUserInput(value); + withWriteLock([&] { + if (_sourceUrl != value) { + auto newURL = QUrl::fromUserInput(value); - if (newURL.isValid()) { - _sourceUrl = newURL.toDisplayString(); - } else { - qCDebug(entities) << "Clearing web entity source URL since" << value << "cannot be parsed to a valid URL."; + if (newURL.isValid()) { + _sourceUrl = newURL.toDisplayString(); + } else { + qCDebug(entities) << "Clearing web entity source URL since" << value << "cannot be parsed to a valid URL."; + } } - } + }); } -const QString& WebEntityItem::getSourceUrl() const { return _sourceUrl; } +QString WebEntityItem::getSourceUrl() const { + QString result; + withReadLock([&] { + result = _sourceUrl; + }); + return result; +} void WebEntityItem::setDPI(uint16_t value) { _dpi = value; diff --git a/libraries/entities/src/WebEntityItem.h b/libraries/entities/src/WebEntityItem.h index 19a7b577fe..5cd081687f 100644 --- a/libraries/entities/src/WebEntityItem.h +++ b/libraries/entities/src/WebEntityItem.h @@ -52,7 +52,7 @@ public: void** intersectedObject, bool precisionPicking) const override; virtual void setSourceUrl(const QString& value); - const QString& getSourceUrl() const; + QString getSourceUrl() const; virtual bool wantsHandControllerPointerEvents() const override { return true; } diff --git a/libraries/entities/src/ZoneEntityItem.cpp b/libraries/entities/src/ZoneEntityItem.cpp index 37b3be99a3..26d566a795 100644 --- a/libraries/entities/src/ZoneEntityItem.cpp +++ b/libraries/entities/src/ZoneEntityItem.cpp @@ -208,10 +208,12 @@ ShapeType ZoneEntityItem::getShapeType() const { } void ZoneEntityItem::setCompoundShapeURL(const QString& url) { - _compoundShapeURL = url; - if (_compoundShapeURL.isEmpty() && _shapeType == SHAPE_TYPE_COMPOUND) { - _shapeType = DEFAULT_SHAPE_TYPE; - } + withWriteLock([&] { + _compoundShapeURL = url; + if (_compoundShapeURL.isEmpty() && _shapeType == SHAPE_TYPE_COMPOUND) { + _shapeType = DEFAULT_SHAPE_TYPE; + } + }); } bool ZoneEntityItem::findDetailedRayIntersection(const glm::vec3& origin, const glm::vec3& direction, @@ -223,7 +225,9 @@ bool ZoneEntityItem::findDetailedRayIntersection(const glm::vec3& origin, const } void ZoneEntityItem::setFilterURL(QString url) { - _filterURL = url; + withWriteLock([&] { + _filterURL = url; + }); if (DependencyManager::isSet()) { auto entityEditFilters = DependencyManager::get(); qCDebug(entities) << "adding filter " << url << "for zone" << getEntityItemID(); @@ -231,3 +235,22 @@ void ZoneEntityItem::setFilterURL(QString url) { } } +QString ZoneEntityItem::getFilterURL() const { + QString result; + withReadLock([&] { + result = _filterURL; + }); + return result; +} + +bool ZoneEntityItem::hasCompoundShapeURL() const { + return !getCompoundShapeURL().isEmpty(); +} + +QString ZoneEntityItem::getCompoundShapeURL() const { + QString result; + withReadLock([&] { + result = _compoundShapeURL; + }); + return result; +} diff --git a/libraries/entities/src/ZoneEntityItem.h b/libraries/entities/src/ZoneEntityItem.h index 2bef95e452..dfc18aff80 100644 --- a/libraries/entities/src/ZoneEntityItem.h +++ b/libraries/entities/src/ZoneEntityItem.h @@ -58,8 +58,8 @@ public: void setShapeType(ShapeType type) override { _shapeType = type; } virtual ShapeType getShapeType() const override; - virtual bool hasCompoundShapeURL() const { return !_compoundShapeURL.isEmpty(); } - const QString getCompoundShapeURL() const { return _compoundShapeURL; } + virtual bool hasCompoundShapeURL() const; + QString getCompoundShapeURL() const; virtual void setCompoundShapeURL(const QString& url); const KeyLightPropertyGroup& getKeyLightProperties() const { return _keyLightProperties; } @@ -74,7 +74,7 @@ public: void setFlyingAllowed(bool value) { _flyingAllowed = value; } bool getGhostingAllowed() const { return _ghostingAllowed; } void setGhostingAllowed(bool value) { _ghostingAllowed = value; } - QString getFilterURL() const { return _filterURL; } + QString getFilterURL() const; void setFilterURL(const QString url); virtual bool supportsDetailedRayIntersection() const override { return true; } diff --git a/libraries/fbx/src/FBXReader.cpp b/libraries/fbx/src/FBXReader.cpp index fcaef90527..bcd2be3384 100644 --- a/libraries/fbx/src/FBXReader.cpp +++ b/libraries/fbx/src/FBXReader.cpp @@ -376,10 +376,10 @@ public: }; bool checkMaterialsHaveTextures(const QHash& materials, - const QHash& textureFilenames, const QMultiMap& _connectionChildMap) { + const QHash& textureFilepaths, const QMultiMap& _connectionChildMap) { foreach (const QString& materialID, materials.keys()) { foreach (const QString& childID, _connectionChildMap.values(materialID)) { - if (textureFilenames.contains(childID)) { + if (textureFilepaths.contains(childID)) { return true; } } @@ -443,21 +443,48 @@ FBXLight extractLight(const FBXNode& object) { return light; } -QByteArray fileOnUrl(const QByteArray& filepath, const QString& url) { - QString path = QFileInfo(url).path(); - QByteArray filename = filepath; - QFileInfo checkFile(path + "/" + filepath); +QByteArray fixedTextureFilepath(QByteArray fbxRelativeFilepath, QUrl url) { + // first setup a QFileInfo for the passed relative filepath, with backslashes replaced by forward slashes + auto fileInfo = QFileInfo { fbxRelativeFilepath.replace("\\", "/") }; - // check if the file exists at the RelativeFilename - if (!(checkFile.exists() && checkFile.isFile())) { - // if not, assume it is in the fbx directory - filename = filename.mid(filename.lastIndexOf('/') + 1); +#ifndef Q_OS_WIN + // it turns out that absolute windows paths starting with drive letters look like relative paths to QFileInfo on UNIX + // so we add a check for that here to work around it + bool isRelative = fbxRelativeFilepath[1] != ':' && fileInfo.isRelative(); +#else + bool isRelative = fileInfo.isRelative(); +#endif + + if (isRelative) { + // the RelativeFilename pulled from the FBX is already correctly relative + // so simply return this as the filepath to use + return fbxRelativeFilepath; + } else { + // the RelativeFilename pulled from the FBX is an absolute path + + // use the URL to figure out where the FBX is being loaded from + auto filename = fileInfo.fileName(); + + if (url.isLocalFile()) { + // the FBX is being loaded from the local filesystem + + if (fileInfo.exists() && fileInfo.isFile()) { + // found a file at the absolute path in the FBX, return that path + return fbxRelativeFilepath; + } else { + // didn't find a file at the absolute path, assume it is right beside the FBX + // return just the filename as the relative path + return filename.toUtf8(); + } + } else { + // this is a remote file, meaning we can't really do anything with the absolute path to the texture + // so assume it will be right beside the fbx + return filename.toUtf8(); + } } - - return filename; } -FBXGeometry* FBXReader::extractFBXGeometry(const QVariantHash& mapping, const QString& url) { +FBXGeometry* FBXReader::extractFBXGeometry(const QVariantHash& mapping, const QUrl& url) { const FBXNode& node = _fbxNode; QMap meshes; QHash modelIDsToNames; @@ -833,11 +860,9 @@ FBXGeometry* FBXReader::extractFBXGeometry(const QVariantHash& mapping, const QS const int MODEL_UV_SCALING_MIN_SIZE = 2; const int CROPPING_MIN_SIZE = 4; if (subobject.name == "RelativeFilename" && subobject.properties.length() >= RELATIVE_FILENAME_MIN_SIZE) { - QByteArray filename = subobject.properties.at(0).toByteArray(); - QByteArray filepath = filename.replace('\\', '/'); - filename = fileOnUrl(filepath, url); + auto filepath = fixedTextureFilepath(subobject.properties.at(0).toByteArray(), url); + _textureFilepaths.insert(getID(object.properties), filepath); - _textureFilenames.insert(getID(object.properties), filename); } else if (subobject.name == "TextureName" && subobject.properties.length() >= TEXTURE_NAME_MIN_SIZE) { // trim the name from the timestamp QString name = QString(subobject.properties.at(0).toByteArray()); @@ -930,7 +955,7 @@ FBXGeometry* FBXReader::extractFBXGeometry(const QVariantHash& mapping, const QS QByteArray content; foreach (const FBXNode& subobject, object.children) { if (subobject.name == "RelativeFilename") { - filepath= subobject.properties.at(0).toByteArray(); + filepath = subobject.properties.at(0).toByteArray(); filepath = filepath.replace('\\', '/'); } else if (subobject.name == "Content" && !subobject.properties.isEmpty()) { @@ -1468,6 +1493,9 @@ FBXGeometry* FBXReader::extractFBXGeometry(const QVariantHash& mapping, const QS // Create the Material Library consolidateFBXMaterials(mapping); + // We can't allow the scaling of a given image to different sizes, because the hash used for the KTX cache is based on the original image + // Allowing scaling of the same image to different sizes would cause different KTX files to target the same cache key +#if 0 // HACK: until we get proper LOD management we're going to cap model textures // according to how many unique textures the model uses: // 1 - 8 textures --> 2048 @@ -1481,6 +1509,7 @@ FBXGeometry* FBXReader::extractFBXGeometry(const QVariantHash& mapping, const QS int numTextures = uniqueTextures.size(); const int MAX_NUM_TEXTURES_AT_MAX_RESOLUTION = 8; int maxWidth = sqrt(MAX_NUM_PIXELS_FOR_FBX_TEXTURE); + if (numTextures > MAX_NUM_TEXTURES_AT_MAX_RESOLUTION) { int numTextureThreshold = MAX_NUM_TEXTURES_AT_MAX_RESOLUTION; const int MIN_MIP_TEXTURE_WIDTH = 64; @@ -1494,11 +1523,11 @@ FBXGeometry* FBXReader::extractFBXGeometry(const QVariantHash& mapping, const QS material.setMaxNumPixelsPerTexture(maxWidth * maxWidth); } } - +#endif geometry.materials = _fbxMaterials; // see if any materials have texture children - bool materialsHaveTextures = checkMaterialsHaveTextures(_fbxMaterials, _textureFilenames, _connectionChildMap); + bool materialsHaveTextures = checkMaterialsHaveTextures(_fbxMaterials, _textureFilepaths, _connectionChildMap); for (QMap::iterator it = meshes.begin(); it != meshes.end(); it++) { ExtractedMesh& extracted = it.value(); @@ -1543,7 +1572,7 @@ FBXGeometry* FBXReader::extractFBXGeometry(const QVariantHash& mapping, const QS materialIndex++; - } else if (_textureFilenames.contains(childID)) { + } else if (_textureFilepaths.contains(childID)) { FBXTexture texture = getTexture(childID); for (int j = 0; j < extracted.partMaterialTextures.size(); j++) { int partTexture = extracted.partMaterialTextures.at(j).second; @@ -1627,13 +1656,15 @@ FBXGeometry* FBXReader::extractFBXGeometry(const QVariantHash& mapping, const QS // whether we're skinned depends on how many clusters are attached const FBXCluster& firstFBXCluster = extracted.mesh.clusters.at(0); - int maxJointIndex = firstFBXCluster.jointIndex; glm::mat4 inverseModelTransform = glm::inverse(modelTransform); if (clusterIDs.size() > 1) { // this is a multi-mesh joint - extracted.mesh.clusterIndices.resize(extracted.mesh.vertices.size()); - extracted.mesh.clusterWeights.resize(extracted.mesh.vertices.size()); - float maxWeight = 0.0f; + const int WEIGHTS_PER_VERTEX = 4; + int numClusterIndices = extracted.mesh.vertices.size() * WEIGHTS_PER_VERTEX; + extracted.mesh.clusterIndices.fill(0, numClusterIndices); + QVector weightAccumulators; + weightAccumulators.fill(0.0f, numClusterIndices); + for (int i = 0; i < clusterIDs.size(); i++) { QString clusterID = clusterIDs.at(i); const Cluster& cluster = clusters[clusterID]; @@ -1658,61 +1689,69 @@ FBXGeometry* FBXReader::extractFBXGeometry(const QVariantHash& mapping, const QS glm::mat4 meshToJoint = glm::inverse(joint.bindTransform) * modelTransform; ShapeVertices& points = shapeVertices.at(jointIndex); - float totalWeight = 0.0f; for (int j = 0; j < cluster.indices.size(); j++) { int oldIndex = cluster.indices.at(j); float weight = cluster.weights.at(j); - totalWeight += weight; for (QMultiHash::const_iterator it = extracted.newIndices.constFind(oldIndex); it != extracted.newIndices.end() && it.key() == oldIndex; it++) { + int newIndex = it.value(); // remember vertices with at least 1/4 weight const float EXPANSION_WEIGHT_THRESHOLD = 0.99f; if (weight > EXPANSION_WEIGHT_THRESHOLD) { // transform to joint-frame and save for later - const glm::mat4 vertexTransform = meshToJoint * glm::translate(extracted.mesh.vertices.at(it.value())); + const glm::mat4 vertexTransform = meshToJoint * glm::translate(extracted.mesh.vertices.at(newIndex)); points.push_back(extractTranslation(vertexTransform) * clusterScale); } // look for an unused slot in the weights vector - glm::vec4& weights = extracted.mesh.clusterWeights[it.value()]; + int weightIndex = newIndex * WEIGHTS_PER_VERTEX; int lowestIndex = -1; float lowestWeight = FLT_MAX; int k = 0; - for (; k < 4; k++) { - if (weights[k] == 0.0f) { - extracted.mesh.clusterIndices[it.value()][k] = i; - weights[k] = weight; + for (; k < WEIGHTS_PER_VERTEX; k++) { + if (weightAccumulators[weightIndex + k] == 0.0f) { + extracted.mesh.clusterIndices[weightIndex + k] = i; + weightAccumulators[weightIndex + k] = weight; break; } - if (weights[k] < lowestWeight) { + if (weightAccumulators[weightIndex + k] < lowestWeight) { lowestIndex = k; - lowestWeight = weights[k]; + lowestWeight = weightAccumulators[weightIndex + k]; } } - if (k == 4 && weight > lowestWeight) { + if (k == WEIGHTS_PER_VERTEX && weight > lowestWeight) { // no space for an additional weight; we must replace the lowest - weights[lowestIndex] = weight; - extracted.mesh.clusterIndices[it.value()][lowestIndex] = i; + weightAccumulators[weightIndex + lowestIndex] = weight; + extracted.mesh.clusterIndices[weightIndex + lowestIndex] = i; } } } - if (totalWeight > maxWeight) { - maxWeight = totalWeight; - maxJointIndex = jointIndex; - } } - // normalize the weights if they don't add up to one - for (int i = 0; i < extracted.mesh.clusterWeights.size(); i++) { - glm::vec4& weights = extracted.mesh.clusterWeights[i]; - float total = weights.x + weights.y + weights.z + weights.w; - if (total != 1.0f && total != 0.0f) { - weights /= total; + + // now that we've accumulated the most relevant weights for each vertex + // normalize and compress to 8-bits + extracted.mesh.clusterWeights.fill(0, numClusterIndices); + int numVertices = extracted.mesh.vertices.size(); + for (int i = 0; i < numVertices; ++i) { + int j = i * WEIGHTS_PER_VERTEX; + + // normalize weights into uint8_t + float totalWeight = weightAccumulators[j]; + for (int k = j + 1; k < j + WEIGHTS_PER_VERTEX; ++k) { + totalWeight += weightAccumulators[k]; + } + if (totalWeight > 0.0f) { + const float ALMOST_HALF = 0.499f; + float weightScalingFactor = (float)(UINT8_MAX) / totalWeight; + for (int k = j; k < j + WEIGHTS_PER_VERTEX; ++k) { + extracted.mesh.clusterWeights[k] = (uint8_t)(weightScalingFactor * weightAccumulators[k] + ALMOST_HALF); + } } } } else { // this is a single-mesh joint - int jointIndex = maxJointIndex; + int jointIndex = firstFBXCluster.jointIndex; FBXJoint& joint = geometry.joints[jointIndex]; // transform cluster vertices to joint-frame and save for later @@ -1732,18 +1771,8 @@ FBXGeometry* FBXReader::extractFBXGeometry(const QVariantHash& mapping, const QS } } } - extracted.mesh.isEye = (maxJointIndex == geometry.leftEyeJointIndex || maxJointIndex == geometry.rightEyeJointIndex); - buildModelMesh(extracted.mesh, url); - if (extracted.mesh.isEye) { - if (maxJointIndex == geometry.leftEyeJointIndex) { - geometry.leftEyeSize = extracted.mesh.meshExtents.largestDimension() * offsetScale; - } else { - geometry.rightEyeSize = extracted.mesh.meshExtents.largestDimension() * offsetScale; - } - } - geometry.meshes.append(extracted.mesh); int meshIndex = geometry.meshes.size() - 1; meshIDsToMeshIndices.insert(it.key(), meshIndex); @@ -1795,19 +1824,6 @@ FBXGeometry* FBXReader::extractFBXGeometry(const QVariantHash& mapping, const QS } geometry.palmDirection = parseVec3(mapping.value("palmDirection", "0, -1, 0").toString()); - // Add sitting points - QVariantHash sittingPoints = mapping.value("sit").toHash(); - for (QVariantHash::const_iterator it = sittingPoints.constBegin(); it != sittingPoints.constEnd(); it++) { - SittingPoint sittingPoint; - sittingPoint.name = it.key(); - - QVariantList properties = it->toList(); - sittingPoint.position = parseVec3(properties.at(0).toString()); - sittingPoint.rotation = glm::quat(glm::radians(parseVec3(properties.at(1).toString()))); - - geometry.sittingPoints.append(sittingPoint); - } - // attempt to map any meshes to a named model for (QHash::const_iterator m = meshIDsToMeshIndices.constBegin(); m != meshIDsToMeshIndices.constEnd(); m++) { @@ -1827,13 +1843,13 @@ FBXGeometry* FBXReader::extractFBXGeometry(const QVariantHash& mapping, const QS return geometryPtr; } -FBXGeometry* readFBX(const QByteArray& model, const QVariantHash& mapping, const QString& url, bool loadLightmaps, float lightmapLevel) { +FBXGeometry* readFBX(const QByteArray& model, const QVariantHash& mapping, const QUrl& url, bool loadLightmaps, float lightmapLevel) { QBuffer buffer(const_cast(&model)); buffer.open(QIODevice::ReadOnly); return readFBX(&buffer, mapping, url, loadLightmaps, lightmapLevel); } -FBXGeometry* readFBX(QIODevice* device, const QVariantHash& mapping, const QString& url, bool loadLightmaps, float lightmapLevel) { +FBXGeometry* readFBX(QIODevice* device, const QVariantHash& mapping, const QUrl& url, bool loadLightmaps, float lightmapLevel) { FBXReader reader; reader._fbxNode = FBXReader::parseFBX(device); reader._loadLightmaps = loadLightmaps; diff --git a/libraries/fbx/src/FBXReader.h b/libraries/fbx/src/FBXReader.h index 5910b8d312..dd746322e9 100644 --- a/libraries/fbx/src/FBXReader.h +++ b/libraries/fbx/src/FBXReader.h @@ -202,7 +202,7 @@ public: /// A single mesh (with optional blendshapes) extracted from an FBX document. class FBXMesh { public: - + QVector parts; QVector vertices; @@ -211,16 +211,14 @@ public: QVector colors; QVector texCoords; QVector texCoords1; - QVector clusterIndices; - QVector clusterWeights; - + QVector clusterIndices; + QVector clusterWeights; + QVector clusters; Extents meshExtents; glm::mat4 modelTransform; - bool isEye; - QVector blendshapes; unsigned int meshIndex; // the order the meshes appeared in the object file @@ -265,30 +263,12 @@ public: Q_DECLARE_METATYPE(FBXAnimationFrame) Q_DECLARE_METATYPE(QVector) -/// A point where an avatar can sit -class SittingPoint { -public: - QString name; - glm::vec3 position; // relative postion - glm::quat rotation; // relative orientation -}; - -inline bool operator==(const SittingPoint& lhs, const SittingPoint& rhs) -{ - return (lhs.name == rhs.name) && (lhs.position == rhs.position) && (lhs.rotation == rhs.rotation); -} - -inline bool operator!=(const SittingPoint& lhs, const SittingPoint& rhs) -{ - return (lhs.name != rhs.name) || (lhs.position != rhs.position) || (lhs.rotation != rhs.rotation); -} - /// A set of meshes extracted from an FBX document. class FBXGeometry { public: using Pointer = std::shared_ptr; - QString originalURL; + QUrl originalURL; QString author; QString applicationName; ///< the name of the application that generated the model @@ -300,7 +280,7 @@ public: QHash materials; - glm::mat4 offset; + glm::mat4 offset; // This includes offset, rotation, and scale as specified by the FST file int leftEyeJointIndex = -1; int rightEyeJointIndex = -1; @@ -320,8 +300,6 @@ public: glm::vec3 palmDirection; - QVector sittingPoints; - glm::vec3 neckPivot; Extents bindExtents; @@ -352,11 +330,11 @@ Q_DECLARE_METATYPE(FBXGeometry::Pointer) /// Reads FBX geometry from the supplied model and mapping data. /// \exception QString if an error occurs in parsing -FBXGeometry* readFBX(const QByteArray& model, const QVariantHash& mapping, const QString& url = "", bool loadLightmaps = true, float lightmapLevel = 1.0f); +FBXGeometry* readFBX(const QByteArray& model, const QVariantHash& mapping, const QUrl& url = QUrl(), bool loadLightmaps = true, float lightmapLevel = 1.0f); /// Reads FBX geometry from the supplied model and mapping data. /// \exception QString if an error occurs in parsing -FBXGeometry* readFBX(QIODevice* device, const QVariantHash& mapping, const QString& url = "", bool loadLightmaps = true, float lightmapLevel = 1.0f); +FBXGeometry* readFBX(QIODevice* device, const QVariantHash& mapping, const QUrl& url = QUrl(), bool loadLightmaps = true, float lightmapLevel = 1.0f); class TextureParam { public: @@ -424,19 +402,17 @@ public: FBXNode _fbxNode; static FBXNode parseFBX(QIODevice* device); - FBXGeometry* extractFBXGeometry(const QVariantHash& mapping, const QString& url); + FBXGeometry* extractFBXGeometry(const QVariantHash& mapping, const QUrl& url); ExtractedMesh extractMesh(const FBXNode& object, unsigned int& meshIndex); QHash meshes; - static void buildModelMesh(FBXMesh& extractedMesh, const QString& url); + static void buildModelMesh(FBXMesh& extractedMesh, const QUrl& url); FBXTexture getTexture(const QString& textureID); QHash _textureNames; // Hashes the original RelativeFilename of textures QHash _textureFilepaths; - // Hashes the place to look for textures, in case they are not inlined - QHash _textureFilenames; // Hashes texture content by filepath, in case they are inlined QHash _textureContent; QHash _textureParams; diff --git a/libraries/fbx/src/FBXReader_Material.cpp b/libraries/fbx/src/FBXReader_Material.cpp index ca2ec557b4..2f63d894fd 100644 --- a/libraries/fbx/src/FBXReader_Material.cpp +++ b/libraries/fbx/src/FBXReader_Material.cpp @@ -85,12 +85,7 @@ FBXTexture FBXReader::getTexture(const QString& textureID) { FBXTexture texture; const QByteArray& filepath = _textureFilepaths.value(textureID); texture.content = _textureContent.value(filepath); - - if (texture.content.isEmpty()) { // the content is not inlined - texture.filename = _textureFilenames.value(textureID); - } else { // use supplied filepath for inlined content - texture.filename = filepath; - } + texture.filename = filepath; texture.name = _textureNames.value(textureID); texture.transform.setIdentity(); @@ -155,7 +150,7 @@ void FBXReader::consolidateFBXMaterials(const QVariantHash& mapping) { // FBX files generated by 3DSMax have an intermediate texture parent, apparently foreach(const QString& childTextureID, _connectionChildMap.values(diffuseTextureID)) { - if (_textureFilenames.contains(childTextureID)) { + if (_textureFilepaths.contains(childTextureID)) { diffuseTexture = getTexture(diffuseTextureID); } } diff --git a/libraries/fbx/src/FBXReader_Mesh.cpp b/libraries/fbx/src/FBXReader_Mesh.cpp index 8eb31b0731..a6d70408ae 100644 --- a/libraries/fbx/src/FBXReader_Mesh.cpp +++ b/libraries/fbx/src/FBXReader_Mesh.cpp @@ -388,7 +388,7 @@ ExtractedMesh FBXReader::extractMesh(const FBXNode& object, unsigned int& meshIn return data.extracted; } -void FBXReader::buildModelMesh(FBXMesh& extractedMesh, const QString& url) { +void FBXReader::buildModelMesh(FBXMesh& extractedMesh, const QUrl& url) { static QString repeatedMessage = LogHandler::getInstance().addRepeatedMessageRegex("buildModelMesh failed -- .*"); unsigned int totalSourceIndices = 0; @@ -422,8 +422,13 @@ void FBXReader::buildModelMesh(FBXMesh& extractedMesh, const QString& url) { int colorsSize = fbxMesh.colors.size() * sizeof(glm::vec3); int texCoordsSize = fbxMesh.texCoords.size() * sizeof(glm::vec2); int texCoords1Size = fbxMesh.texCoords1.size() * sizeof(glm::vec2); - int clusterIndicesSize = fbxMesh.clusterIndices.size() * sizeof(glm::vec4); - int clusterWeightsSize = fbxMesh.clusterWeights.size() * sizeof(glm::vec4); + + int clusterIndicesSize = fbxMesh.clusterIndices.size() * sizeof(uint8_t); + if (fbxMesh.clusters.size() > UINT8_MAX) { + // we need 16 bits instead of just 8 for clusterIndices + clusterIndicesSize *= 2; + } + int clusterWeightsSize = fbxMesh.clusterWeights.size() * sizeof(uint8_t); int normalsOffset = 0; int tangentsOffset = normalsOffset + normalsSize; @@ -442,7 +447,20 @@ void FBXReader::buildModelMesh(FBXMesh& extractedMesh, const QString& url) { attribBuffer->setSubData(colorsOffset, colorsSize, (gpu::Byte*) fbxMesh.colors.constData()); attribBuffer->setSubData(texCoordsOffset, texCoordsSize, (gpu::Byte*) fbxMesh.texCoords.constData()); attribBuffer->setSubData(texCoords1Offset, texCoords1Size, (gpu::Byte*) fbxMesh.texCoords1.constData()); - attribBuffer->setSubData(clusterIndicesOffset, clusterIndicesSize, (gpu::Byte*) fbxMesh.clusterIndices.constData()); + + if (fbxMesh.clusters.size() < UINT8_MAX) { + // yay! we can fit the clusterIndices within 8-bits + int32_t numIndices = fbxMesh.clusterIndices.size(); + QVector clusterIndices; + clusterIndices.resize(numIndices); + for (int32_t i = 0; i < numIndices; ++i) { + assert(fbxMesh.clusterIndices[i] <= UINT8_MAX); + clusterIndices[i] = (uint8_t)(fbxMesh.clusterIndices[i]); + } + attribBuffer->setSubData(clusterIndicesOffset, clusterIndicesSize, (gpu::Byte*) clusterIndices.constData()); + } else { + attribBuffer->setSubData(clusterIndicesOffset, clusterIndicesSize, (gpu::Byte*) fbxMesh.clusterIndices.constData()); + } attribBuffer->setSubData(clusterWeightsOffset, clusterWeightsSize, (gpu::Byte*) fbxMesh.clusterWeights.constData()); if (normalsSize) { @@ -476,14 +494,20 @@ void FBXReader::buildModelMesh(FBXMesh& extractedMesh, const QString& url) { } if (clusterIndicesSize) { - mesh->addAttribute(gpu::Stream::SKIN_CLUSTER_INDEX, - model::BufferView(attribBuffer, clusterIndicesOffset, clusterIndicesSize, - gpu::Element(gpu::VEC4, gpu::FLOAT, gpu::XYZW))); + if (fbxMesh.clusters.size() < UINT8_MAX) { + mesh->addAttribute(gpu::Stream::SKIN_CLUSTER_INDEX, + model::BufferView(attribBuffer, clusterIndicesOffset, clusterIndicesSize, + gpu::Element(gpu::VEC4, gpu::UINT8, gpu::XYZW))); + } else { + mesh->addAttribute(gpu::Stream::SKIN_CLUSTER_INDEX, + model::BufferView(attribBuffer, clusterIndicesOffset, clusterIndicesSize, + gpu::Element(gpu::VEC4, gpu::UINT16, gpu::XYZW))); + } } if (clusterWeightsSize) { mesh->addAttribute(gpu::Stream::SKIN_CLUSTER_WEIGHT, model::BufferView(attribBuffer, clusterWeightsOffset, clusterWeightsSize, - gpu::Element(gpu::VEC4, gpu::FLOAT, gpu::XYZW))); + gpu::Element(gpu::VEC4, gpu::NUINT8, gpu::XYZW))); } diff --git a/libraries/fbx/src/FBXReader_Node.cpp b/libraries/fbx/src/FBXReader_Node.cpp index d814f58dab..d987f885eb 100644 --- a/libraries/fbx/src/FBXReader_Node.cpp +++ b/libraries/fbx/src/FBXReader_Node.cpp @@ -54,7 +54,8 @@ template QVariant readBinaryArray(QDataStream& in, int& position) { in.readRawData(compressed.data() + sizeof(quint32), compressedLength); position += compressedLength; arrayData = qUncompress(compressed); - if (arrayData.isEmpty() || arrayData.size() != (sizeof(T) * arrayLength)) { // answers empty byte array if corrupt + if (arrayData.isEmpty() || + (unsigned int)arrayData.size() != (sizeof(T) * arrayLength)) { // answers empty byte array if corrupt throw QString("corrupt fbx file"); } } else { diff --git a/libraries/fbx/src/OBJReader.cpp b/libraries/fbx/src/OBJReader.cpp index 73cf7a520e..c99b847722 100644 --- a/libraries/fbx/src/OBJReader.cpp +++ b/libraries/fbx/src/OBJReader.cpp @@ -267,7 +267,7 @@ void OBJReader::parseMaterialLibrary(QIODevice* device) { } if (token == "map_Kd") { currentMaterial.diffuseTextureFilename = filename; - } else { + } else if( token == "map_Ks" ) { currentMaterial.specularTextureFilename = filename; } } @@ -294,7 +294,7 @@ QNetworkReply* OBJReader::request(QUrl& url, bool isTest) { QObject::connect(netReply, SIGNAL(finished()), &loop, SLOT(quit())); loop.exec(); // Nothing is going to happen on this whole run thread until we get this static const int WAIT_TIMEOUT_MS = 500; - while (qApp && !aboutToQuit && !netReply->isReadable()) { + while (!aboutToQuit && qApp && !netReply->isReadable()) { netReply->waitForReadyRead(WAIT_TIMEOUT_MS); // so we might as well block this thread waiting for the response, rather than } QObject::disconnect(connection); @@ -440,105 +440,142 @@ FBXGeometry* OBJReader::readOBJ(QByteArray& model, const QVariantHash& mapping, // add a new meshPart to the geometry's single mesh. while (parseOBJGroup(tokenizer, mapping, geometry, scaleGuess)) {} - FBXMesh& mesh = geometry.meshes[0]; - mesh.meshIndex = 0; + FBXMesh& mesh = geometry.meshes[0]; + mesh.meshIndex = 0; - geometry.joints.resize(1); - geometry.joints[0].isFree = false; - geometry.joints[0].parentIndex = -1; - geometry.joints[0].distanceToParent = 0; - geometry.joints[0].translation = glm::vec3(0, 0, 0); - geometry.joints[0].rotationMin = glm::vec3(0, 0, 0); - geometry.joints[0].rotationMax = glm::vec3(0, 0, 0); - geometry.joints[0].name = "OBJ"; - geometry.joints[0].isSkeletonJoint = true; + geometry.joints.resize(1); + geometry.joints[0].isFree = false; + geometry.joints[0].parentIndex = -1; + geometry.joints[0].distanceToParent = 0; + geometry.joints[0].translation = glm::vec3(0, 0, 0); + geometry.joints[0].rotationMin = glm::vec3(0, 0, 0); + geometry.joints[0].rotationMax = glm::vec3(0, 0, 0); + geometry.joints[0].name = "OBJ"; + geometry.joints[0].isSkeletonJoint = true; - geometry.jointIndices["x"] = 1; + geometry.jointIndices["x"] = 1; - FBXCluster cluster; - cluster.jointIndex = 0; - cluster.inverseBindMatrix = glm::mat4(1, 0, 0, 0, - 0, 1, 0, 0, - 0, 0, 1, 0, - 0, 0, 0, 1); - mesh.clusters.append(cluster); + FBXCluster cluster; + cluster.jointIndex = 0; + cluster.inverseBindMatrix = glm::mat4(1, 0, 0, 0, + 0, 1, 0, 0, + 0, 0, 1, 0, + 0, 0, 0, 1); + mesh.clusters.append(cluster); - for (int i = 0, meshPartCount = 0; i < mesh.parts.count(); i++, meshPartCount++) { - FBXMeshPart& meshPart = mesh.parts[i]; - FaceGroup faceGroup = faceGroups[meshPartCount]; + QMap materialMeshIdMap; + QVector fbxMeshParts; + for (int i = 0, meshPartCount = 0; i < mesh.parts.count(); i++, meshPartCount++) { + FBXMeshPart& meshPart = mesh.parts[i]; + FaceGroup faceGroup = faceGroups[meshPartCount]; bool specifiesUV = false; - foreach(OBJFace face, faceGroup) { - glm::vec3 v0 = checked_at(vertices, face.vertexIndices[0]); - glm::vec3 v1 = checked_at(vertices, face.vertexIndices[1]); - glm::vec3 v2 = checked_at(vertices, face.vertexIndices[2]); - meshPart.triangleIndices.append(mesh.vertices.count()); // not face.vertexIndices into vertices - mesh.vertices << v0; - meshPart.triangleIndices.append(mesh.vertices.count()); - mesh.vertices << v1; - meshPart.triangleIndices.append(mesh.vertices.count()); - mesh.vertices << v2; + foreach(OBJFace face, faceGroup) { + // Go through all of the OBJ faces and determine the number of different materials necessary (each different material will be a unique mesh). + // NOTE (trent/mittens 3/30/17): this seems hardcore wasteful and is slowed down a bit by iterating through the face group twice, but it's the best way I've thought of to hack multi-material support in an OBJ into this pipeline. + if (!materialMeshIdMap.contains(face.materialName)) { + // Create a new FBXMesh for this material mapping. + materialMeshIdMap.insert(face.materialName, materialMeshIdMap.count()); - glm::vec3 n0, n1, n2; - if (face.normalIndices.count()) { - n0 = checked_at(normals, face.normalIndices[0]); - n1 = checked_at(normals, face.normalIndices[1]); - n2 = checked_at(normals, face.normalIndices[2]); - } else { // generate normals from triangle plane if not provided - n0 = n1 = n2 = glm::cross(v1 - v0, v2 - v0); - } - mesh.normals << n0 << n1 << n2; - if (face.textureUVIndices.count()) { - specifiesUV = true; - mesh.texCoords << - checked_at(textureUVs, face.textureUVIndices[0]) << - checked_at(textureUVs, face.textureUVIndices[1]) << - checked_at(textureUVs, face.textureUVIndices[2]); - } else { - glm::vec2 corner(0.0f, 1.0f); - mesh.texCoords << corner << corner << corner; - } - } - // All the faces in the same group will have the same name and material. - OBJFace leadFace = faceGroup[0]; - QString groupMaterialName = leadFace.materialName; - if (groupMaterialName.isEmpty() && specifiesUV) { - #ifdef WANT_DEBUG - qCDebug(modelformat) << "OBJ Reader WARNING: " << url - << " needs a texture that isn't specified. Using default mechanism."; - #endif - groupMaterialName = SMART_DEFAULT_MATERIAL_NAME; - } - if (!groupMaterialName.isEmpty()) { - OBJMaterial& material = materials[groupMaterialName]; - if (specifiesUV) { - material.userSpecifiesUV = true; // Note might not be true in a later usage. - } - if (specifiesUV || (0 != groupMaterialName.compare("none", Qt::CaseInsensitive))) { - // Blender has a convention that a material named "None" isn't really used (or defined). - material.used = true; - needsMaterialLibrary = groupMaterialName != SMART_DEFAULT_MATERIAL_NAME; - } - materials[groupMaterialName] = material; - meshPart.materialID = groupMaterialName; - } + fbxMeshParts.append(FBXMeshPart()); + FBXMeshPart& meshPartNew = fbxMeshParts.last(); + meshPartNew.quadIndices = QVector(meshPart.quadIndices); // Copy over quad indices [NOTE (trent/mittens, 4/3/17): Likely unnecessary since they go unused anyway]. + meshPartNew.quadTrianglesIndices = QVector(meshPart.quadTrianglesIndices); // Copy over quad triangulated indices [NOTE (trent/mittens, 4/3/17): Likely unnecessary since they go unused anyway]. + meshPartNew.triangleIndices = QVector(meshPart.triangleIndices); // Copy over triangle indices. + // Do some of the material logic (which previously lived below) now. + // All the faces in the same group will have the same name and material. + QString groupMaterialName = face.materialName; + if (groupMaterialName.isEmpty() && specifiesUV) { +#ifdef WANT_DEBUG + qCDebug(modelformat) << "OBJ Reader WARNING: " << url + << " needs a texture that isn't specified. Using default mechanism."; +#endif + groupMaterialName = SMART_DEFAULT_MATERIAL_NAME; + } + if (!groupMaterialName.isEmpty()) { + OBJMaterial& material = materials[groupMaterialName]; + if (specifiesUV) { + material.userSpecifiesUV = true; // Note might not be true in a later usage. + } + if (specifiesUV || (groupMaterialName.compare("none", Qt::CaseInsensitive) != 0)) { + // Blender has a convention that a material named "None" isn't really used (or defined). + material.used = true; + needsMaterialLibrary = groupMaterialName != SMART_DEFAULT_MATERIAL_NAME; + } + materials[groupMaterialName] = material; + meshPartNew.materialID = groupMaterialName; + } + } + } + } + + // clean up old mesh parts. + int unmodifiedMeshPartCount = mesh.parts.count(); + mesh.parts.clear(); + mesh.parts = QVector(fbxMeshParts); + + for (int i = 0, meshPartCount = 0; i < unmodifiedMeshPartCount; i++, meshPartCount++) { + FaceGroup faceGroup = faceGroups[meshPartCount]; + + // Now that each mesh has been created with its own unique material mappings, fill them with data (vertex data is duplicated, face data is not). + foreach(OBJFace face, faceGroup) { + FBXMeshPart& meshPart = mesh.parts[materialMeshIdMap[face.materialName]]; + + glm::vec3 v0 = checked_at(vertices, face.vertexIndices[0]); + glm::vec3 v1 = checked_at(vertices, face.vertexIndices[1]); + glm::vec3 v2 = checked_at(vertices, face.vertexIndices[2]); + + // Scale the vertices if the OBJ file scale is specified as non-one. + if (scaleGuess != 1.0f) { + v0 *= scaleGuess; + v1 *= scaleGuess; + v2 *= scaleGuess; + } + + // Add the vertices. + meshPart.triangleIndices.append(mesh.vertices.count()); // not face.vertexIndices into vertices + mesh.vertices << v0; + meshPart.triangleIndices.append(mesh.vertices.count()); + mesh.vertices << v1; + meshPart.triangleIndices.append(mesh.vertices.count()); + mesh.vertices << v2; + + glm::vec3 n0, n1, n2; + if (face.normalIndices.count()) { + n0 = checked_at(normals, face.normalIndices[0]); + n1 = checked_at(normals, face.normalIndices[1]); + n2 = checked_at(normals, face.normalIndices[2]); + } else { + // generate normals from triangle plane if not provided + n0 = n1 = n2 = glm::cross(v1 - v0, v2 - v0); + } + + mesh.normals.append(n0); + mesh.normals.append(n1); + mesh.normals.append(n2); + + if (face.textureUVIndices.count()) { + mesh.texCoords << + checked_at(textureUVs, face.textureUVIndices[0]) << + checked_at(textureUVs, face.textureUVIndices[1]) << + checked_at(textureUVs, face.textureUVIndices[2]); + } else { + glm::vec2 corner(0.0f, 1.0f); + mesh.texCoords << corner << corner << corner; + } + } } - // if we got a hint about units, scale all the points - if (scaleGuess != 1.0f) { - for (int i = 0; i < mesh.vertices.size(); i++) { - mesh.vertices[i] *= scaleGuess; - } - } + mesh.meshExtents.reset(); + foreach(const glm::vec3& vertex, mesh.vertices) { + mesh.meshExtents.addPoint(vertex); + geometry.meshExtents.addPoint(vertex); + } - mesh.meshExtents.reset(); - foreach (const glm::vec3& vertex, mesh.vertices) { - mesh.meshExtents.addPoint(vertex); - geometry.meshExtents.addPoint(vertex); - } + // Build the single mesh. + FBXReader::buildModelMesh(mesh, url.toString()); - FBXReader::buildModelMesh(mesh, url.toString()); - // fbxDebugDump(geometry); + // fbxDebugDump(geometry); } catch(const std::exception& e) { qCDebug(modelformat) << "OBJ reader fail: " << e.what(); } @@ -546,6 +583,7 @@ FBXGeometry* OBJReader::readOBJ(QByteArray& model, const QVariantHash& mapping, QString queryPart = _url.query(); bool suppressMaterialsHack = queryPart.contains("hifiusemat"); // If this appears in query string, don't fetch mtl even if used. OBJMaterial& preDefinedMaterial = materials[SMART_DEFAULT_MATERIAL_NAME]; + preDefinedMaterial.used = true; if (suppressMaterialsHack) { needsMaterialLibrary = preDefinedMaterial.userSpecifiesUV = false; // I said it was a hack... } @@ -594,8 +632,8 @@ FBXGeometry* OBJReader::readOBJ(QByteArray& model, const QVariantHash& mapping, } foreach (QString materialID, materials.keys()) { - OBJMaterial& objMaterial = materials[materialID]; - if (!objMaterial.used) { + OBJMaterial& objMaterial = materials[materialID]; + if (!objMaterial.used) { continue; } geometry.materials[materialID] = FBXMaterial(objMaterial.diffuseColor, @@ -611,6 +649,9 @@ FBXGeometry* OBJReader::readOBJ(QByteArray& model, const QVariantHash& mapping, if (!objMaterial.diffuseTextureFilename.isEmpty()) { fbxMaterial.albedoTexture.filename = objMaterial.diffuseTextureFilename; } + if (!objMaterial.specularTextureFilename.isEmpty()) { + fbxMaterial.specularTexture.filename = objMaterial.specularTextureFilename; + } modelMaterial->setEmissive(fbxMaterial.emissiveColor); modelMaterial->setAlbedo(fbxMaterial.diffuseColor); diff --git a/libraries/fbx/src/OBJReader.h b/libraries/fbx/src/OBJReader.h index 200f11548d..b4a48c570e 100644 --- a/libraries/fbx/src/OBJReader.h +++ b/libraries/fbx/src/OBJReader.h @@ -58,7 +58,7 @@ public: QByteArray specularTextureFilename; bool used { false }; bool userSpecifiesUV { false }; - OBJMaterial() : shininess(96.0f), opacity(1.0f), diffuseColor(1.0f), specularColor(1.0f) {} + OBJMaterial() : shininess(0.0f), opacity(1.0f), diffuseColor(0.9f), specularColor(0.9f) {} }; class OBJReader: public QObject { // QObject so we can make network requests. diff --git a/libraries/fbx/src/OBJWriter.cpp b/libraries/fbx/src/OBJWriter.cpp new file mode 100644 index 0000000000..034263eb53 --- /dev/null +++ b/libraries/fbx/src/OBJWriter.cpp @@ -0,0 +1,176 @@ +// +// OBJWriter.cpp +// libraries/fbx/src/ +// +// Created by Seth Alves on 2017-1-27. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include +#include +#include "model/Geometry.h" +#include "OBJWriter.h" +#include "ModelFormatLogging.h" + +static QString formatFloat(double n) { + // limit precision to 6, but don't output trailing zeros. + QString s = QString::number(n, 'f', 6); + while (s.endsWith("0")) { + s.remove(s.size() - 1, 1); + } + if (s.endsWith(".")) { + s.remove(s.size() - 1, 1); + } + + // check for non-numbers. if we get NaN or inf or scientific notation, just return 0 + for (int i = 0; i < s.length(); i++) { + auto c = s.at(i).toLatin1(); + if (c != '-' && + c != '.' && + (c < '0' || c > '9')) { + qCDebug(modelformat) << "OBJWriter zeroing bad vertex coordinate:" << s << "because of" << c; + return QString("0"); + } + } + + return s; +} + +bool writeOBJToTextStream(QTextStream& out, QList meshes) { + int attributeTypeNormal = gpu::Stream::InputSlot::NORMAL; // libraries/gpu/src/gpu/Stream.h + + // each mesh's vertices are numbered from zero. We're combining all their vertices into one list here, + // so keep track of the start index for each mesh. + QList meshVertexStartOffset; + QList meshNormalStartOffset; + int currentVertexStartOffset = 0; + int currentNormalStartOffset = 0; + + // write out vertices + foreach (const MeshPointer& mesh, meshes) { + meshVertexStartOffset.append(currentVertexStartOffset); + const gpu::BufferView& vertexBuffer = mesh->getVertexBuffer(); + int vertexCount = 0; + gpu::BufferView::Iterator vertexItr = vertexBuffer.cbegin(); + while (vertexItr != vertexBuffer.cend()) { + glm::vec3 v = *vertexItr; + out << "v "; + out << formatFloat(v[0]) << " "; + out << formatFloat(v[1]) << " "; + out << formatFloat(v[2]) << "\n"; + vertexItr++; + vertexCount++; + } + currentVertexStartOffset += vertexCount; + } + out << "\n"; + + // write out normals + bool haveNormals = true; + foreach (const MeshPointer& mesh, meshes) { + meshNormalStartOffset.append(currentNormalStartOffset); + const gpu::BufferView& normalsBufferView = mesh->getAttributeBuffer(attributeTypeNormal); + gpu::BufferView::Index numNormals = (gpu::BufferView::Index)normalsBufferView.getNumElements(); + for (gpu::BufferView::Index i = 0; i < numNormals; i++) { + glm::vec3 normal = normalsBufferView.get(i); + out << "vn "; + out << formatFloat(normal[0]) << " "; + out << formatFloat(normal[1]) << " "; + out << formatFloat(normal[2]) << "\n"; + } + currentNormalStartOffset += numNormals; + } + out << "\n"; + + // write out faces + int nth = 0; + foreach (const MeshPointer& mesh, meshes) { + currentVertexStartOffset = meshVertexStartOffset.takeFirst(); + currentNormalStartOffset = meshNormalStartOffset.takeFirst(); + + const gpu::BufferView& partBuffer = mesh->getPartBuffer(); + const gpu::BufferView& indexBuffer = mesh->getIndexBuffer(); + + model::Index partCount = (model::Index)mesh->getNumParts(); + for (int partIndex = 0; partIndex < partCount; partIndex++) { + const model::Mesh::Part& part = partBuffer.get(partIndex); + + out << "g part-" << nth++ << "\n"; + + // model::Mesh::TRIANGLES + // TODO -- handle other formats + gpu::BufferView::Iterator indexItr = indexBuffer.cbegin(); + indexItr += part._startIndex; + + int indexCount = 0; + while (indexItr != indexBuffer.cend() && indexCount < part._numIndices) { + uint32_t index0 = *indexItr; + indexItr++; + indexCount++; + if (indexItr == indexBuffer.cend() || indexCount >= part._numIndices) { + qCDebug(modelformat) << "OBJWriter -- index buffer length isn't multiple of 3"; + break; + } + uint32_t index1 = *indexItr; + indexItr++; + indexCount++; + if (indexItr == indexBuffer.cend() || indexCount >= part._numIndices) { + qCDebug(modelformat) << "OBJWriter -- index buffer length isn't multiple of 3"; + break; + } + uint32_t index2 = *indexItr; + indexItr++; + indexCount++; + + out << "f "; + if (haveNormals) { + out << currentVertexStartOffset + index0 + 1 << "//" << currentVertexStartOffset + index0 + 1 << " "; + out << currentVertexStartOffset + index1 + 1 << "//" << currentVertexStartOffset + index1 + 1 << " "; + out << currentVertexStartOffset + index2 + 1 << "//" << currentVertexStartOffset + index2 + 1 << "\n"; + } else { + out << currentVertexStartOffset + index0 + 1 << " "; + out << currentVertexStartOffset + index1 + 1 << " "; + out << currentVertexStartOffset + index2 + 1 << "\n"; + } + } + out << "\n"; + } + } + + return true; +} + +bool writeOBJToFile(QString path, QList meshes) { + if (QFileInfo(path).exists() && !QFile::remove(path)) { + qCDebug(modelformat) << "OBJ writer failed, file exists:" << path; + return false; + } + + QFile file(path); + if (!file.open(QIODevice::WriteOnly)) { + qCDebug(modelformat) << "OBJ writer failed to open output file:" << path; + return false; + } + + QTextStream outStream(&file); + + bool success; + success = writeOBJToTextStream(outStream, meshes); + + file.close(); + return success; +} + +QString writeOBJToString(QList meshes) { + QString result; + QTextStream outStream(&result, QIODevice::ReadWrite); + bool success; + success = writeOBJToTextStream(outStream, meshes); + if (success) { + return result; + } + return QString(""); +} diff --git a/libraries/fbx/src/OBJWriter.h b/libraries/fbx/src/OBJWriter.h new file mode 100644 index 0000000000..b6e20e1ae6 --- /dev/null +++ b/libraries/fbx/src/OBJWriter.h @@ -0,0 +1,26 @@ +// +// OBJWriter.h +// libraries/fbx/src/ +// +// Created by Seth Alves on 2017-1-27. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_objwriter_h +#define hifi_objwriter_h + + +#include +#include +#include + +using MeshPointer = std::shared_ptr; + +bool writeOBJToTextStream(QTextStream& out, QList meshes); +bool writeOBJToFile(QString path, QList meshes); +QString writeOBJToString(QList meshes); + +#endif // hifi_objwriter_h diff --git a/libraries/gl/src/gl/Config.h b/libraries/gl/src/gl/Config.h index 7947bd45df..9efae96f2a 100644 --- a/libraries/gl/src/gl/Config.h +++ b/libraries/gl/src/gl/Config.h @@ -28,6 +28,9 @@ #include +// Uncomment this define and recompile to be able to avoid code path preventing to be able to run nsight graphics debug +//#define HIFI_ENABLE_NSIGHT_DEBUG 1 + #endif #endif // hifi_gpu_GPUConfig_h diff --git a/libraries/gl/src/gl/OffscreenGLCanvas.cpp b/libraries/gl/src/gl/OffscreenGLCanvas.cpp index e54846196b..3f1d629638 100644 --- a/libraries/gl/src/gl/OffscreenGLCanvas.cpp +++ b/libraries/gl/src/gl/OffscreenGLCanvas.cpp @@ -59,7 +59,6 @@ bool OffscreenGLCanvas::create(QOpenGLContext* sharedContext) { bool OffscreenGLCanvas::makeCurrent() { bool result = _context->makeCurrent(_offscreenSurface); - Q_ASSERT(result); std::call_once(_reportOnce, [this]{ qCDebug(glLogging) << "GL Version: " << QString((const char*) glGetString(GL_VERSION)); qCDebug(glLogging) << "GL Shader Language Version: " << QString((const char*) glGetString(GL_SHADING_LANGUAGE_VERSION)); diff --git a/libraries/gl/src/gl/OffscreenQmlSurface.cpp b/libraries/gl/src/gl/OffscreenQmlSurface.cpp index 447b9d56aa..3bbd26e010 100644 --- a/libraries/gl/src/gl/OffscreenQmlSurface.cpp +++ b/libraries/gl/src/gl/OffscreenQmlSurface.cpp @@ -32,6 +32,7 @@ #include #include #include +#include #include "OffscreenGLCanvas.h" #include "GLHelpers.h" @@ -277,6 +278,9 @@ void OffscreenQmlSurface::cleanup() { } void OffscreenQmlSurface::render() { +#ifdef HIFI_ENABLE_NSIGHT_DEBUG + return; +#endif if (_paused) { return; } @@ -433,6 +437,7 @@ void OffscreenQmlSurface::create(QOpenGLContext* shareContext) { auto rootContext = getRootContext(); rootContext->setContextProperty("urlHandler", new UrlHandler()); rootContext->setContextProperty("resourceDirectoryUrl", QUrl::fromLocalFile(PathUtils::resourcesPath())); + rootContext->setContextProperty("pathToFonts", "../../"); } static uvec2 clampSize(const uvec2& size, uint32_t maxDimension) { @@ -574,7 +579,9 @@ QObject* OffscreenQmlSurface::finishQmlLoad(std::functionsetObjectOwnership(this, QQmlEngine::CppOwnership); newObject->setProperty("eventBridge", QVariant::fromValue(this)); + newContext->setContextProperty("eventBridgeJavaScriptToInject", QVariant(javaScriptToInject)); f(newContext, newObject); @@ -911,20 +918,23 @@ void OffscreenQmlSurface::setKeyboardRaised(QObject* object, bool raised, bool n return; } - QQuickItem* item = dynamic_cast(object); - while (item) { - // Numeric value may be set in parameter from HTML UI; for QML UI, detect numeric fields here. - numeric = numeric || QString(item->metaObject()->className()).left(7) == "SpinBox"; + // if HMD is being worn, allow keyboard to open. allow it to close, HMD or not. + if (!raised || qApp->property(hifi::properties::HMD).toBool()) { + QQuickItem* item = dynamic_cast(object); + while (item) { + // Numeric value may be set in parameter from HTML UI; for QML UI, detect numeric fields here. + numeric = numeric || QString(item->metaObject()->className()).left(7) == "SpinBox"; - if (item->property("keyboardRaised").isValid()) { - // FIXME - HMD only: Possibly set value of "keyboardEnabled" per isHMDMode() for use in WebView.qml. - if (item->property("punctuationMode").isValid()) { - item->setProperty("punctuationMode", QVariant(numeric)); + if (item->property("keyboardRaised").isValid()) { + // FIXME - HMD only: Possibly set value of "keyboardEnabled" per isHMDMode() for use in WebView.qml. + if (item->property("punctuationMode").isValid()) { + item->setProperty("punctuationMode", QVariant(numeric)); + } + item->setProperty("keyboardRaised", QVariant(raised)); + return; } - item->setProperty("keyboardRaised", QVariant(raised)); - return; + item = dynamic_cast(item->parentItem()); } - item = dynamic_cast(item->parentItem()); } } diff --git a/libraries/gpu-gl/CMakeLists.txt b/libraries/gpu-gl/CMakeLists.txt index 65df5ed9dc..3e3853532a 100644 --- a/libraries/gpu-gl/CMakeLists.txt +++ b/libraries/gpu-gl/CMakeLists.txt @@ -1,6 +1,9 @@ set(TARGET_NAME gpu-gl) setup_hifi_library() link_hifi_libraries(shared gl gpu) +if (UNIX) + target_link_libraries(${TARGET_NAME} pthread) +endif(UNIX) GroupSources("src") target_opengl() diff --git a/libraries/gpu-gl/src/gpu/gl/GLBackend.cpp b/libraries/gpu-gl/src/gpu/gl/GLBackend.cpp index c51f468908..0800c27839 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLBackend.cpp +++ b/libraries/gpu-gl/src/gpu/gl/GLBackend.cpp @@ -62,8 +62,6 @@ BackendPointer GLBackend::createBackend() { INSTANCE = result.get(); void* voidInstance = &(*result); qApp->setProperty(hifi::properties::gl::BACKEND, QVariant::fromValue(voidInstance)); - - gl::GLTexture::initTextureTransferHelper(); return result; } @@ -209,7 +207,7 @@ void GLBackend::renderPassTransfer(const Batch& batch) { } } - { // Sync all the buffers + { // Sync all the transform states PROFILE_RANGE(render_gpu_gl_detail, "syncCPUTransform"); _transform._cameras.clear(); _transform._cameraOffsets.clear(); @@ -277,7 +275,7 @@ void GLBackend::renderPassDraw(const Batch& batch) { updateInput(); updateTransform(batch); updatePipeline(); - + CommandCall call = _commandCalls[(*command)]; (this->*(call))(batch, *offset); break; @@ -623,6 +621,7 @@ void GLBackend::queueLambda(const std::function lambda) const { } void GLBackend::recycle() const { + PROFILE_RANGE(render_gpu_gl, __FUNCTION__) { std::list> lamdbasTrash; { @@ -745,10 +744,6 @@ void GLBackend::recycle() const { glDeleteQueries((GLsizei)ids.size(), ids.data()); } } - -#ifndef THREADED_TEXTURE_TRANSFER - gl::GLTexture::_textureTransferHelper->process(); -#endif } void GLBackend::setCameraCorrection(const Mat4& correction) { diff --git a/libraries/gpu-gl/src/gpu/gl/GLBackend.h b/libraries/gpu-gl/src/gpu/gl/GLBackend.h index 950ac65a3f..76c950ec2b 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLBackend.h +++ b/libraries/gpu-gl/src/gpu/gl/GLBackend.h @@ -187,10 +187,15 @@ public: virtual void do_setStateScissorRect(const Batch& batch, size_t paramOffset) final; virtual GLuint getFramebufferID(const FramebufferPointer& framebuffer) = 0; - virtual GLuint getTextureID(const TexturePointer& texture, bool needTransfer = true) = 0; + virtual GLuint getTextureID(const TexturePointer& texture) final; virtual GLuint getBufferID(const Buffer& buffer) = 0; virtual GLuint getQueryID(const QueryPointer& query) = 0; - virtual bool isTextureReady(const TexturePointer& texture); + + virtual GLFramebuffer* syncGPUObject(const Framebuffer& framebuffer) = 0; + virtual GLBuffer* syncGPUObject(const Buffer& buffer) = 0; + virtual GLTexture* syncGPUObject(const TexturePointer& texture); + virtual GLQuery* syncGPUObject(const Query& query) = 0; + //virtual bool isTextureReady(const TexturePointer& texture); virtual void releaseBuffer(GLuint id, Size size) const; virtual void releaseExternalTexture(GLuint id, const Texture::ExternalRecycler& recycler) const; @@ -206,10 +211,6 @@ public: protected: void recycle() const override; - virtual GLFramebuffer* syncGPUObject(const Framebuffer& framebuffer) = 0; - virtual GLBuffer* syncGPUObject(const Buffer& buffer) = 0; - virtual GLTexture* syncGPUObject(const TexturePointer& texture, bool sync = true) = 0; - virtual GLQuery* syncGPUObject(const Query& query) = 0; static const size_t INVALID_OFFSET = (size_t)-1; bool _inRenderTransferPass { false }; diff --git a/libraries/gpu-gl/src/gpu/gl/GLBackendPipeline.cpp b/libraries/gpu-gl/src/gpu/gl/GLBackendPipeline.cpp index 8aab6abaa9..1dad72dbc1 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLBackendPipeline.cpp +++ b/libraries/gpu-gl/src/gpu/gl/GLBackendPipeline.cpp @@ -229,7 +229,7 @@ void GLBackend::do_setResourceTexture(const Batch& batch, size_t paramOffset) { _resource._textures[slot] = resourceTexture; - _stats._RSAmountTextureMemoryBounded += object->size(); + _stats._RSAmountTextureMemoryBounded += (int) object->size(); } else { releaseResourceTexture(slot); diff --git a/libraries/gpu-gl/src/gpu/gl/GLBackendTexture.cpp b/libraries/gpu-gl/src/gpu/gl/GLBackendTexture.cpp index f51eac0e33..ca4e328612 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLBackendTexture.cpp +++ b/libraries/gpu-gl/src/gpu/gl/GLBackendTexture.cpp @@ -14,12 +14,56 @@ using namespace gpu; using namespace gpu::gl; -bool GLBackend::isTextureReady(const TexturePointer& texture) { - // DO not transfer the texture, this call is expected for rendering texture - GLTexture* object = syncGPUObject(texture, true); - return object && object->isReady(); + +GLuint GLBackend::getTextureID(const TexturePointer& texture) { + GLTexture* object = syncGPUObject(texture); + + if (!object) { + return 0; + } + + return object->_id; } +GLTexture* GLBackend::syncGPUObject(const TexturePointer& texturePointer) { + const Texture& texture = *texturePointer; + // Special case external textures + if (TextureUsageType::EXTERNAL == texture.getUsageType()) { + Texture::ExternalUpdates updates = texture.getUpdates(); + if (!updates.empty()) { + Texture::ExternalRecycler recycler = texture.getExternalRecycler(); + Q_ASSERT(recycler); + // Discard any superfluous updates + while (updates.size() > 1) { + const auto& update = updates.front(); + // Superfluous updates will never have been read, but we want to ensure the previous + // writes to them are complete before they're written again, so return them with the + // same fences they arrived with. This can happen on any thread because no GL context + // work is involved + recycler(update.first, update.second); + updates.pop_front(); + } + + // The last texture remaining is the one we'll use to create the GLTexture + const auto& update = updates.front(); + // Check for a fence, and if it exists, inject a wait into the command stream, then destroy the fence + if (update.second) { + GLsync fence = static_cast(update.second); + glWaitSync(fence, 0, GL_TIMEOUT_IGNORED); + glDeleteSync(fence); + } + + // Create the new texture object (replaces any previous texture object) + new GLExternalTexture(shared_from_this(), texture, update.first); + } + + // Return the texture object (if any) associated with the texture, without extensive logic + // (external textures are + return Backend::getGPUObject(texture); + } + + return nullptr; +} void GLBackend::do_generateTextureMips(const Batch& batch, size_t paramOffset) { TexturePointer resourceTexture = batch._textures.get(batch._params[paramOffset + 0]._uint); @@ -28,7 +72,7 @@ void GLBackend::do_generateTextureMips(const Batch& batch, size_t paramOffset) { } // DO not transfer the texture, this call is expected for rendering texture - GLTexture* object = syncGPUObject(resourceTexture, false); + GLTexture* object = syncGPUObject(resourceTexture); if (!object) { return; } diff --git a/libraries/gpu-gl/src/gpu/gl/GLFramebuffer.cpp b/libraries/gpu-gl/src/gpu/gl/GLFramebuffer.cpp index 85cf069062..2ac7e9d060 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLFramebuffer.cpp +++ b/libraries/gpu-gl/src/gpu/gl/GLFramebuffer.cpp @@ -21,13 +21,12 @@ GLFramebuffer::~GLFramebuffer() { } } -bool GLFramebuffer::checkStatus(GLenum target) const { - bool result = false; +bool GLFramebuffer::checkStatus() const { switch (_status) { case GL_FRAMEBUFFER_COMPLETE: // Success ! - result = true; - break; + return true; + case GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT: qCWarning(gpugllogging) << "GLFramebuffer::syncGPUObject : Framebuffer not valid, GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT."; break; @@ -44,5 +43,5 @@ bool GLFramebuffer::checkStatus(GLenum target) const { qCWarning(gpugllogging) << "GLFramebuffer::syncGPUObject : Framebuffer not valid, GL_FRAMEBUFFER_UNSUPPORTED."; break; } - return result; + return false; } diff --git a/libraries/gpu-gl/src/gpu/gl/GLFramebuffer.h b/libraries/gpu-gl/src/gpu/gl/GLFramebuffer.h index 9b4f9703fc..c0633cfdef 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLFramebuffer.h +++ b/libraries/gpu-gl/src/gpu/gl/GLFramebuffer.h @@ -64,7 +64,7 @@ public: protected: GLenum _status { GL_FRAMEBUFFER_COMPLETE }; virtual void update() = 0; - bool checkStatus(GLenum target) const; + bool checkStatus() const; GLFramebuffer(const std::weak_ptr& backend, const Framebuffer& framebuffer, GLuint id) : GLObject(backend, framebuffer, id) {} ~GLFramebuffer(); diff --git a/libraries/gpu-gl/src/gpu/gl/GLTexelFormat.cpp b/libraries/gpu-gl/src/gpu/gl/GLTexelFormat.cpp index bd945cbaaa..7e26e65e02 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLTexelFormat.cpp +++ b/libraries/gpu-gl/src/gpu/gl/GLTexelFormat.cpp @@ -17,6 +17,7 @@ GLenum GLTexelFormat::evalGLTexelFormatInternal(const gpu::Element& dstFormat) { switch (dstFormat.getDimension()) { case gpu::SCALAR: { switch (dstFormat.getSemantic()) { + case gpu::RED: case gpu::RGB: case gpu::RGBA: case gpu::SRGB: @@ -262,6 +263,7 @@ GLTexelFormat GLTexelFormat::evalGLTexelFormat(const Element& dstFormat, const E texel.type = ELEMENT_TYPE_TO_GL[dstFormat.getType()]; switch (dstFormat.getSemantic()) { + case gpu::RED: case gpu::RGB: case gpu::RGBA: texel.internalFormat = GL_R8; @@ -272,8 +274,10 @@ GLTexelFormat GLTexelFormat::evalGLTexelFormat(const Element& dstFormat, const E break; case gpu::DEPTH: + texel.format = GL_DEPTH_COMPONENT; texel.internalFormat = GL_DEPTH_COMPONENT32; break; + case gpu::DEPTH_STENCIL: texel.type = GL_UNSIGNED_INT_24_8; texel.format = GL_DEPTH_STENCIL; @@ -403,6 +407,7 @@ GLTexelFormat GLTexelFormat::evalGLTexelFormat(const Element& dstFormat, const E texel.internalFormat = GL_COMPRESSED_RED_RGTC1; break; } + case gpu::RED: case gpu::RGB: case gpu::RGBA: case gpu::SRGB: diff --git a/libraries/gpu-gl/src/gpu/gl/GLTexture.cpp b/libraries/gpu-gl/src/gpu/gl/GLTexture.cpp index 1e0dd08ae1..65c5788227 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLTexture.cpp +++ b/libraries/gpu-gl/src/gpu/gl/GLTexture.cpp @@ -8,17 +8,13 @@ #include "GLTexture.h" -#include - -#include "GLTextureTransfer.h" #include "GLBackend.h" using namespace gpu; using namespace gpu::gl; -std::shared_ptr GLTexture::_textureTransferHelper; -const GLenum GLTexture::CUBE_FACE_LAYOUT[6] = { +const GLenum GLTexture::CUBE_FACE_LAYOUT[GLTexture::TEXTURE_CUBE_NUM_FACES] = { GL_TEXTURE_CUBE_MAP_POSITIVE_X, GL_TEXTURE_CUBE_MAP_NEGATIVE_X, GL_TEXTURE_CUBE_MAP_POSITIVE_Y, GL_TEXTURE_CUBE_MAP_NEGATIVE_Y, GL_TEXTURE_CUBE_MAP_POSITIVE_Z, GL_TEXTURE_CUBE_MAP_NEGATIVE_Z @@ -67,6 +63,17 @@ GLenum GLTexture::getGLTextureType(const Texture& texture) { } +uint8_t GLTexture::getFaceCount(GLenum target) { + switch (target) { + case GL_TEXTURE_2D: + return TEXTURE_2D_NUM_FACES; + case GL_TEXTURE_CUBE_MAP: + return TEXTURE_CUBE_NUM_FACES; + default: + Q_UNREACHABLE(); + break; + } +} const std::vector& GLTexture::getFaceTargets(GLenum target) { static std::vector cubeFaceTargets { GL_TEXTURE_CUBE_MAP_POSITIVE_X, GL_TEXTURE_CUBE_MAP_NEGATIVE_X, @@ -89,216 +96,34 @@ const std::vector& GLTexture::getFaceTargets(GLenum target) { return faceTargets; } -// Default texture memory = GPU total memory - 2GB -#define GPU_MEMORY_RESERVE_BYTES MB_TO_BYTES(2048) -// Minimum texture memory = 1GB -#define TEXTURE_MEMORY_MIN_BYTES MB_TO_BYTES(1024) - - -float GLTexture::getMemoryPressure() { - // Check for an explicit memory limit - auto availableTextureMemory = Texture::getAllowedGPUMemoryUsage(); - - - // If no memory limit has been set, use a percentage of the total dedicated memory - if (!availableTextureMemory) { -#if 0 - auto totalMemory = getDedicatedMemory(); - if ((GPU_MEMORY_RESERVE_BYTES + TEXTURE_MEMORY_MIN_BYTES) > totalMemory) { - availableTextureMemory = TEXTURE_MEMORY_MIN_BYTES; - } else { - availableTextureMemory = totalMemory - GPU_MEMORY_RESERVE_BYTES; - } -#else - // Hardcode texture limit for sparse textures at 1 GB for now - availableTextureMemory = TEXTURE_MEMORY_MIN_BYTES; -#endif - } - - // Return the consumed texture memory divided by the available texture memory. - auto consumedGpuMemory = Context::getTextureGPUMemoryUsage() - Context::getTextureGPUFramebufferMemoryUsage(); - float memoryPressure = (float)consumedGpuMemory / (float)availableTextureMemory; - static Context::Size lastConsumedGpuMemory = 0; - if (memoryPressure > 1.0f && lastConsumedGpuMemory != consumedGpuMemory) { - lastConsumedGpuMemory = consumedGpuMemory; - qCDebug(gpugllogging) << "Exceeded max allowed texture memory: " << consumedGpuMemory << " / " << availableTextureMemory; - } - return memoryPressure; -} - - -// Create the texture and allocate storage -GLTexture::GLTexture(const std::weak_ptr& backend, const Texture& texture, GLuint id, bool transferrable) : - GLObject(backend, texture, id), - _external(false), - _source(texture.source()), - _storageStamp(texture.getStamp()), - _target(getGLTextureType(texture)), - _internalFormat(gl::GLTexelFormat::evalGLTexelFormatInternal(texture.getTexelFormat())), - _maxMip(texture.maxMip()), - _minMip(texture.minMip()), - _virtualSize(texture.evalTotalSize()), - _transferrable(transferrable) -{ - auto strongBackend = _backend.lock(); - strongBackend->recycle(); - Backend::incrementTextureGPUCount(); - Backend::updateTextureGPUVirtualMemoryUsage(0, _virtualSize); - Backend::setGPUObject(texture, this); -} - GLTexture::GLTexture(const std::weak_ptr& backend, const Texture& texture, GLuint id) : GLObject(backend, texture, id), - _external(true), _source(texture.source()), - _storageStamp(0), - _target(getGLTextureType(texture)), - _internalFormat(GL_RGBA8), - // FIXME force mips to 0? - _maxMip(texture.maxMip()), - _minMip(texture.minMip()), - _virtualSize(0), - _transferrable(false) + _target(getGLTextureType(texture)) { Backend::setGPUObject(texture, this); - - // FIXME Is this necessary? - //withPreservedTexture([this] { - // syncSampler(); - // if (_gpuObject.isAutogenerateMips()) { - // generateMips(); - // } - //}); } GLTexture::~GLTexture() { + auto backend = _backend.lock(); + if (backend && _id) { + backend->releaseTexture(_id, 0); + } +} + + +GLExternalTexture::GLExternalTexture(const std::weak_ptr& backend, const Texture& texture, GLuint id) + : Parent(backend, texture, id) { } + +GLExternalTexture::~GLExternalTexture() { auto backend = _backend.lock(); if (backend) { - if (_external) { - auto recycler = _gpuObject.getExternalRecycler(); - if (recycler) { - backend->releaseExternalTexture(_id, recycler); - } else { - qWarning() << "No recycler available for texture " << _id << " possible leak"; - } - } else if (_id) { - // WARNING! Sparse textures do not use this code path. See GL45BackendTexture for - // the GL45Texture destructor for doing any required work tracking GPU stats - backend->releaseTexture(_id, _size); + auto recycler = _gpuObject.getExternalRecycler(); + if (recycler) { + backend->releaseExternalTexture(_id, recycler); + } else { + qWarning() << "No recycler available for texture " << _id << " possible leak"; } - - if (!_external && !_transferrable) { - Backend::updateTextureGPUFramebufferMemoryUsage(_size, 0); - } - } - Backend::updateTextureGPUVirtualMemoryUsage(_virtualSize, 0); -} - -void GLTexture::createTexture() { - withPreservedTexture([&] { - allocateStorage(); - (void)CHECK_GL_ERROR(); - syncSampler(); - (void)CHECK_GL_ERROR(); - }); -} - -void GLTexture::withPreservedTexture(std::function f) const { - GLint boundTex = -1; - switch (_target) { - case GL_TEXTURE_2D: - glGetIntegerv(GL_TEXTURE_BINDING_2D, &boundTex); - break; - - case GL_TEXTURE_CUBE_MAP: - glGetIntegerv(GL_TEXTURE_BINDING_CUBE_MAP, &boundTex); - break; - - default: - qFatal("Unsupported texture type"); - } - (void)CHECK_GL_ERROR(); - - glBindTexture(_target, _texture); - f(); - glBindTexture(_target, boundTex); - (void)CHECK_GL_ERROR(); -} - -void GLTexture::setSize(GLuint size) const { - if (!_external && !_transferrable) { - Backend::updateTextureGPUFramebufferMemoryUsage(_size, size); - } - Backend::updateTextureGPUMemoryUsage(_size, size); - const_cast(_size) = size; -} - -bool GLTexture::isInvalid() const { - return _storageStamp < _gpuObject.getStamp(); -} - -bool GLTexture::isOutdated() const { - return GLSyncState::Idle == _syncState && _contentStamp < _gpuObject.getDataStamp(); -} - -bool GLTexture::isReady() const { - // If we have an invalid texture, we're never ready - if (isInvalid()) { - return false; - } - - auto syncState = _syncState.load(); - if (isOutdated() || Idle != syncState) { - return false; - } - - return true; -} - - -// Do any post-transfer operations that might be required on the main context / rendering thread -void GLTexture::postTransfer() { - setSyncState(GLSyncState::Idle); - ++_transferCount; - - // At this point the mip pixels have been loaded, we can notify the gpu texture to abandon it's memory - switch (_gpuObject.getType()) { - case Texture::TEX_2D: - for (uint16_t i = 0; i < Sampler::MAX_MIP_LEVEL; ++i) { - if (_gpuObject.isStoredMipFaceAvailable(i)) { - _gpuObject.notifyMipFaceGPULoaded(i); - } - } - break; - - case Texture::TEX_CUBE: - // transfer pixels from each faces - for (uint8_t f = 0; f < CUBE_NUM_FACES; f++) { - for (uint16_t i = 0; i < Sampler::MAX_MIP_LEVEL; ++i) { - if (_gpuObject.isStoredMipFaceAvailable(i, f)) { - _gpuObject.notifyMipFaceGPULoaded(i, f); - } - } - } - break; - - default: - qCWarning(gpugllogging) << __FUNCTION__ << " case for Texture Type " << _gpuObject.getType() << " not supported"; - break; + const_cast(_id) = 0; } } - -void GLTexture::initTextureTransferHelper() { - _textureTransferHelper = std::make_shared(); -} - -void GLTexture::startTransfer() { - createTexture(); -} - -void GLTexture::finishTransfer() { - if (_gpuObject.isAutogenerateMips()) { - generateMips(); - } -} - diff --git a/libraries/gpu-gl/src/gpu/gl/GLTexture.h b/libraries/gpu-gl/src/gpu/gl/GLTexture.h index 0f75a6fe51..b47aa3e8dd 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLTexture.h +++ b/libraries/gpu-gl/src/gpu/gl/GLTexture.h @@ -9,7 +9,6 @@ #define hifi_gpu_gl_GLTexture_h #include "GLShared.h" -#include "GLTextureTransfer.h" #include "GLBackend.h" #include "GLTexelFormat.h" @@ -20,210 +19,48 @@ struct GLFilterMode { GLint magFilter; }; - class GLTexture : public GLObject { + using Parent = GLObject; + friend class GLBackend; public: static const uint16_t INVALID_MIP { (uint16_t)-1 }; static const uint8_t INVALID_FACE { (uint8_t)-1 }; - static void initTextureTransferHelper(); - static std::shared_ptr _textureTransferHelper; - - template - static GLTexture* sync(GLBackend& backend, const TexturePointer& texturePointer, bool needTransfer) { - const Texture& texture = *texturePointer; - - // Special case external textures - if (texture.getUsage().isExternal()) { - Texture::ExternalUpdates updates = texture.getUpdates(); - if (!updates.empty()) { - Texture::ExternalRecycler recycler = texture.getExternalRecycler(); - Q_ASSERT(recycler); - // Discard any superfluous updates - while (updates.size() > 1) { - const auto& update = updates.front(); - // Superfluous updates will never have been read, but we want to ensure the previous - // writes to them are complete before they're written again, so return them with the - // same fences they arrived with. This can happen on any thread because no GL context - // work is involved - recycler(update.first, update.second); - updates.pop_front(); - } - - // The last texture remaining is the one we'll use to create the GLTexture - const auto& update = updates.front(); - // Check for a fence, and if it exists, inject a wait into the command stream, then destroy the fence - if (update.second) { - GLsync fence = static_cast(update.second); - glWaitSync(fence, 0, GL_TIMEOUT_IGNORED); - glDeleteSync(fence); - } - - // Create the new texture object (replaces any previous texture object) - new GLTextureType(backend.shared_from_this(), texture, update.first); - } - - // Return the texture object (if any) associated with the texture, without extensive logic - // (external textures are - return Backend::getGPUObject(texture); - } - - if (!texture.isDefined()) { - // NO texture definition yet so let's avoid thinking - return nullptr; - } - - // If the object hasn't been created, or the object definition is out of date, drop and re-create - GLTexture* object = Backend::getGPUObject(texture); - - // Create the texture if need be (force re-creation if the storage stamp changes - // for easier use of immutable storage) - if (!object || object->isInvalid()) { - // This automatically any previous texture - object = new GLTextureType(backend.shared_from_this(), texture, needTransfer); - if (!object->_transferrable) { - object->createTexture(); - object->_contentStamp = texture.getDataStamp(); - object->updateSize(); - object->postTransfer(); - } - } - - // Object maybe doens't neet to be tranasferred after creation - if (!object->_transferrable) { - return object; - } - - // If we just did a transfer, return the object after doing post-transfer work - if (GLSyncState::Transferred == object->getSyncState()) { - object->postTransfer(); - } - - if (object->isOutdated()) { - // Object might be outdated, if so, start the transfer - // (outdated objects that are already in transfer will have reported 'true' for ready() - _textureTransferHelper->transferTexture(texturePointer); - return nullptr; - } - - if (!object->isReady()) { - return nullptr; - } - - ((GLTexture*)object)->updateMips(); - - return object; - } - - template - static GLuint getId(GLBackend& backend, const TexturePointer& texture, bool shouldSync) { - if (!texture) { - return 0; - } - GLTexture* object { nullptr }; - if (shouldSync) { - object = sync(backend, texture, shouldSync); - } else { - object = Backend::getGPUObject(*texture); - } - - if (!object) { - return 0; - } - - if (!shouldSync) { - return object->_id; - } - - // Don't return textures that are in transfer state - if ((object->getSyncState() != GLSyncState::Idle) || - // Don't return transferrable textures that have never completed transfer - (!object->_transferrable || 0 != object->_transferCount)) { - return 0; - } - - return object->_id; - } - ~GLTexture(); - // Is this texture generated outside the GPU library? - const bool _external; const GLuint& _texture { _id }; const std::string _source; - const Stamp _storageStamp; const GLenum _target; - const GLenum _internalFormat; - const uint16 _maxMip; - uint16 _minMip; - const GLuint _virtualSize; // theoretical size as expected - Stamp _contentStamp { 0 }; - const bool _transferrable; - Size _transferCount { 0 }; - GLuint size() const { return _size; } - GLSyncState getSyncState() const { return _syncState; } - // Is the storage out of date relative to the gpu texture? - bool isInvalid() const; + static const std::vector& getFaceTargets(GLenum textureType); + static uint8_t getFaceCount(GLenum textureType); + static GLenum getGLTextureType(const Texture& texture); - // Is the content out of date relative to the gpu texture? - bool isOutdated() const; - - // Is the texture in a state where it can be rendered with no work? - bool isReady() const; - - // Execute any post-move operations that must occur only on the main thread - virtual void postTransfer(); - - uint16 usedMipLevels() const { return (_maxMip - _minMip) + 1; } - - static const size_t CUBE_NUM_FACES = 6; - static const GLenum CUBE_FACE_LAYOUT[6]; + static const uint8_t TEXTURE_2D_NUM_FACES = 1; + static const uint8_t TEXTURE_CUBE_NUM_FACES = 6; + static const GLenum CUBE_FACE_LAYOUT[TEXTURE_CUBE_NUM_FACES]; static const GLFilterMode FILTER_MODES[Sampler::NUM_FILTERS]; static const GLenum WRAP_MODES[Sampler::NUM_WRAP_MODES]; - // Return a floating point value indicating how much of the allowed - // texture memory we are currently consuming. A value of 0 indicates - // no texture memory usage, while a value of 1 indicates all available / allowed memory - // is consumed. A value above 1 indicates that there is a problem. - static float getMemoryPressure(); protected: - - static const std::vector& getFaceTargets(GLenum textureType); - - static GLenum getGLTextureType(const Texture& texture); - - - const GLuint _size { 0 }; // true size as reported by the gl api - std::atomic _syncState { GLSyncState::Idle }; - - GLTexture(const std::weak_ptr& backend, const Texture& texture, GLuint id, bool transferrable); - GLTexture(const std::weak_ptr& backend, const Texture& texture, GLuint id); - - void setSyncState(GLSyncState syncState) { _syncState = syncState; } - - void createTexture(); - - virtual void updateMips() {} - virtual void allocateStorage() const = 0; - virtual void updateSize() const = 0; - virtual void syncSampler() const = 0; + virtual Size size() const = 0; virtual void generateMips() const = 0; - virtual void withPreservedTexture(std::function f) const; -protected: - void setSize(GLuint size) const; - - virtual void startTransfer(); - // Returns true if this is the last block required to complete transfer - virtual bool continueTransfer() { return false; } - virtual void finishTransfer(); - -private: - friend class GLTextureTransferHelper; - friend class GLBackend; + GLTexture(const std::weak_ptr& backend, const Texture& texture, GLuint id); }; +class GLExternalTexture : public GLTexture { + using Parent = GLTexture; + friend class GLBackend; +public: + ~GLExternalTexture(); +protected: + GLExternalTexture(const std::weak_ptr& backend, const Texture& texture, GLuint id); + void generateMips() const override {} + Size size() const override { return 0; } +}; + + } } #endif diff --git a/libraries/gpu-gl/src/gpu/gl/GLTextureTransfer.cpp b/libraries/gpu-gl/src/gpu/gl/GLTextureTransfer.cpp deleted file mode 100644 index 9dac2986e3..0000000000 --- a/libraries/gpu-gl/src/gpu/gl/GLTextureTransfer.cpp +++ /dev/null @@ -1,208 +0,0 @@ -// -// Created by Bradley Austin Davis on 2016/04/03 -// Copyright 2013-2016 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// -#include "GLTextureTransfer.h" - -#include -#include - -#include - -#include "GLShared.h" -#include "GLTexture.h" - -#ifdef HAVE_NSIGHT -#include "nvToolsExt.h" -std::unordered_map _map; -#endif - - -#ifdef TEXTURE_TRANSFER_PBOS -#define TEXTURE_TRANSFER_BLOCK_SIZE (64 * 1024) -#define TEXTURE_TRANSFER_PBO_COUNT 128 -#endif - -using namespace gpu; -using namespace gpu::gl; - -GLTextureTransferHelper::GLTextureTransferHelper() { -#ifdef THREADED_TEXTURE_TRANSFER - setObjectName("TextureTransferThread"); - _context.create(); - initialize(true, QThread::LowPriority); - // Clean shutdown on UNIX, otherwise _canvas is freed early - connect(qApp, &QCoreApplication::aboutToQuit, [&] { terminate(); }); -#else - initialize(false, QThread::LowPriority); -#endif -} - -GLTextureTransferHelper::~GLTextureTransferHelper() { -#ifdef THREADED_TEXTURE_TRANSFER - if (isStillRunning()) { - terminate(); - } -#else - terminate(); -#endif -} - -void GLTextureTransferHelper::transferTexture(const gpu::TexturePointer& texturePointer) { - GLTexture* object = Backend::getGPUObject(*texturePointer); - - Backend::incrementTextureGPUTransferCount(); - object->setSyncState(GLSyncState::Pending); - Lock lock(_mutex); - _pendingTextures.push_back(texturePointer); -} - -void GLTextureTransferHelper::setup() { -#ifdef THREADED_TEXTURE_TRANSFER - _context.makeCurrent(); - -#ifdef TEXTURE_TRANSFER_FORCE_DRAW - // FIXME don't use opengl 4.5 DSA functionality without verifying it's present - glCreateRenderbuffers(1, &_drawRenderbuffer); - glNamedRenderbufferStorage(_drawRenderbuffer, GL_RGBA8, 128, 128); - glCreateFramebuffers(1, &_drawFramebuffer); - glNamedFramebufferRenderbuffer(_drawFramebuffer, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, _drawRenderbuffer); - glCreateFramebuffers(1, &_readFramebuffer); -#endif - -#ifdef TEXTURE_TRANSFER_PBOS - std::array pbos; - glCreateBuffers(TEXTURE_TRANSFER_PBO_COUNT, &pbos[0]); - for (uint32_t i = 0; i < TEXTURE_TRANSFER_PBO_COUNT; ++i) { - TextureTransferBlock newBlock; - newBlock._pbo = pbos[i]; - glNamedBufferStorage(newBlock._pbo, TEXTURE_TRANSFER_BLOCK_SIZE, 0, GL_MAP_WRITE_BIT | GL_MAP_PERSISTENT_BIT | GL_MAP_COHERENT_BIT); - newBlock._mapped = glMapNamedBufferRange(newBlock._pbo, 0, TEXTURE_TRANSFER_BLOCK_SIZE, GL_MAP_WRITE_BIT | GL_MAP_PERSISTENT_BIT | GL_MAP_COHERENT_BIT); - _readyQueue.push(newBlock); - } -#endif -#endif -} - -void GLTextureTransferHelper::shutdown() { -#ifdef THREADED_TEXTURE_TRANSFER - _context.makeCurrent(); -#endif - -#ifdef TEXTURE_TRANSFER_FORCE_DRAW - glNamedFramebufferRenderbuffer(_drawFramebuffer, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, 0); - glDeleteFramebuffers(1, &_drawFramebuffer); - _drawFramebuffer = 0; - glDeleteFramebuffers(1, &_readFramebuffer); - _readFramebuffer = 0; - - glNamedFramebufferTexture(_readFramebuffer, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, 0); - glDeleteRenderbuffers(1, &_drawRenderbuffer); - _drawRenderbuffer = 0; -#endif -} - -void GLTextureTransferHelper::queueExecution(VoidLambda lambda) { - Lock lock(_mutex); - _pendingCommands.push_back(lambda); -} - -#define MAX_TRANSFERS_PER_PASS 2 - -bool GLTextureTransferHelper::process() { - // Take any new textures or commands off the queue - VoidLambdaList pendingCommands; - TextureList newTransferTextures; - { - Lock lock(_mutex); - newTransferTextures.swap(_pendingTextures); - pendingCommands.swap(_pendingCommands); - } - - if (!pendingCommands.empty()) { - for (auto command : pendingCommands) { - command(); - } - glFlush(); - } - - if (!newTransferTextures.empty()) { - for (auto& texturePointer : newTransferTextures) { -#ifdef HAVE_NSIGHT - _map[texturePointer] = nvtxRangeStart("TextureTansfer"); -#endif - GLTexture* object = Backend::getGPUObject(*texturePointer); - object->startTransfer(); - _transferringTextures.push_back(texturePointer); - _textureIterator = _transferringTextures.begin(); - } - _transferringTextures.sort([](const gpu::TexturePointer& a, const gpu::TexturePointer& b)->bool { - return a->getSize() < b->getSize(); - }); - } - - // No transfers in progress, sleep - if (_transferringTextures.empty()) { -#ifdef THREADED_TEXTURE_TRANSFER - QThread::usleep(1); -#endif - return true; - } - PROFILE_COUNTER_IF_CHANGED(render_gpu_gl, "transferringTextures", int, (int) _transferringTextures.size()) - - static auto lastReport = usecTimestampNow(); - auto now = usecTimestampNow(); - auto lastReportInterval = now - lastReport; - if (lastReportInterval > USECS_PER_SECOND * 4) { - lastReport = now; - qCDebug(gpulogging) << "Texture list " << _transferringTextures.size(); - } - - size_t transferCount = 0; - for (_textureIterator = _transferringTextures.begin(); _textureIterator != _transferringTextures.end();) { - if (++transferCount > MAX_TRANSFERS_PER_PASS) { - break; - } - auto texture = *_textureIterator; - GLTexture* gltexture = Backend::getGPUObject(*texture); - if (gltexture->continueTransfer()) { - ++_textureIterator; - continue; - } - - gltexture->finishTransfer(); - -#ifdef TEXTURE_TRANSFER_FORCE_DRAW - // FIXME force a draw on the texture transfer thread before passing the texture to the main thread for use -#endif - -#ifdef THREADED_TEXTURE_TRANSFER - clientWait(); -#endif - gltexture->_contentStamp = gltexture->_gpuObject.getDataStamp(); - gltexture->updateSize(); - gltexture->setSyncState(gpu::gl::GLSyncState::Transferred); - Backend::decrementTextureGPUTransferCount(); -#ifdef HAVE_NSIGHT - // Mark the texture as transferred - nvtxRangeEnd(_map[texture]); - _map.erase(texture); -#endif - _textureIterator = _transferringTextures.erase(_textureIterator); - } - -#ifdef THREADED_TEXTURE_TRANSFER - if (!_transferringTextures.empty()) { - // Don't saturate the GPU - clientWait(); - } else { - // Don't saturate the CPU - QThread::msleep(1); - } -#endif - - return true; -} diff --git a/libraries/gpu-gl/src/gpu/gl/GLTextureTransfer.h b/libraries/gpu-gl/src/gpu/gl/GLTextureTransfer.h deleted file mode 100644 index a23c282fd4..0000000000 --- a/libraries/gpu-gl/src/gpu/gl/GLTextureTransfer.h +++ /dev/null @@ -1,78 +0,0 @@ -// -// Created by Bradley Austin Davis on 2016/04/03 -// Copyright 2013-2016 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// -#ifndef hifi_gpu_gl_GLTextureTransfer_h -#define hifi_gpu_gl_GLTextureTransfer_h - -#include -#include - -#include - -#include - -#include "GLShared.h" - -#ifdef Q_OS_WIN -#define THREADED_TEXTURE_TRANSFER -#endif - -#ifdef THREADED_TEXTURE_TRANSFER -// FIXME when sparse textures are enabled, it's harder to force a draw on the transfer thread -// also, the current draw code is implicitly using OpenGL 4.5 functionality -//#define TEXTURE_TRANSFER_FORCE_DRAW -// FIXME PBO's increase the complexity and don't seem to work reliably -//#define TEXTURE_TRANSFER_PBOS -#endif - -namespace gpu { namespace gl { - -using TextureList = std::list; -using TextureListIterator = TextureList::iterator; - -class GLTextureTransferHelper : public GenericThread { -public: - using VoidLambda = std::function; - using VoidLambdaList = std::list; - using Pointer = std::shared_ptr; - GLTextureTransferHelper(); - ~GLTextureTransferHelper(); - void transferTexture(const gpu::TexturePointer& texturePointer); - void queueExecution(VoidLambda lambda); - - void setup() override; - void shutdown() override; - bool process() override; - -private: -#ifdef THREADED_TEXTURE_TRANSFER - ::gl::OffscreenContext _context; -#endif - -#ifdef TEXTURE_TRANSFER_FORCE_DRAW - // Framebuffers / renderbuffers for forcing access to the texture on the transfer thread - GLuint _drawRenderbuffer { 0 }; - GLuint _drawFramebuffer { 0 }; - GLuint _readFramebuffer { 0 }; -#endif - - // A mutex for protecting items access on the render and transfer threads - Mutex _mutex; - // Commands that have been submitted for execution on the texture transfer thread - VoidLambdaList _pendingCommands; - // Textures that have been submitted for transfer - TextureList _pendingTextures; - // Textures currently in the transfer process - // Only used on the transfer thread - TextureList _transferringTextures; - TextureListIterator _textureIterator; - -}; - -} } - -#endif \ No newline at end of file diff --git a/libraries/gpu-gl/src/gpu/gl41/GL41Backend.h b/libraries/gpu-gl/src/gpu/gl41/GL41Backend.h index 72e2f5a804..93d65b74dd 100644 --- a/libraries/gpu-gl/src/gpu/gl41/GL41Backend.h +++ b/libraries/gpu-gl/src/gpu/gl41/GL41Backend.h @@ -40,20 +40,59 @@ public: class GL41Texture : public GLTexture { using Parent = GLTexture; - GLuint allocate(); - public: - GL41Texture(const std::weak_ptr& backend, const Texture& buffer, GLuint externalId); - GL41Texture(const std::weak_ptr& backend, const Texture& buffer, bool transferrable); - + friend class GL41Backend; + static GLuint allocate(const Texture& texture); protected: - void transferMip(uint16_t mipLevel, uint8_t face) const; - void startTransfer() override; - void allocateStorage() const override; - void updateSize() const override; - void syncSampler() const override; + GL41Texture(const std::weak_ptr& backend, const Texture& texture); void generateMips() const override; + void copyMipFaceFromTexture(uint16_t sourceMip, uint16_t targetMip, uint8_t face) const; + void copyMipFaceLinesFromTexture(uint16_t mip, uint8_t face, const uvec3& size, uint32_t yOffset, GLenum format, GLenum type, const void* sourcePointer) const; + virtual void syncSampler() const; + + void withPreservedTexture(std::function f) const; }; + // + // Textures that have fixed allocation sizes and cannot be managed at runtime + // + + class GL41FixedAllocationTexture : public GL41Texture { + using Parent = GL41Texture; + friend class GL41Backend; + + public: + GL41FixedAllocationTexture(const std::weak_ptr& backend, const Texture& texture); + ~GL41FixedAllocationTexture(); + + protected: + Size size() const override { return _size; } + void allocateStorage() const; + void syncSampler() const override; + const Size _size { 0 }; + }; + + class GL41AttachmentTexture : public GL41FixedAllocationTexture { + using Parent = GL41FixedAllocationTexture; + friend class GL41Backend; + protected: + GL41AttachmentTexture(const std::weak_ptr& backend, const Texture& texture); + ~GL41AttachmentTexture(); + }; + + class GL41StrictResourceTexture : public GL41FixedAllocationTexture { + using Parent = GL41FixedAllocationTexture; + friend class GL41Backend; + protected: + GL41StrictResourceTexture(const std::weak_ptr& backend, const Texture& texture); + }; + + class GL41ResourceTexture : public GL41FixedAllocationTexture { + using Parent = GL41FixedAllocationTexture; + friend class GL41Backend; + protected: + GL41ResourceTexture(const std::weak_ptr& backend, const Texture& texture); + ~GL41ResourceTexture(); + }; protected: GLuint getFramebufferID(const FramebufferPointer& framebuffer) override; @@ -62,8 +101,7 @@ protected: GLuint getBufferID(const Buffer& buffer) override; GLBuffer* syncGPUObject(const Buffer& buffer) override; - GLuint getTextureID(const TexturePointer& texture, bool needTransfer = true) override; - GLTexture* syncGPUObject(const TexturePointer& texture, bool sync = true) override; + GLTexture* syncGPUObject(const TexturePointer& texture) override; GLuint getQueryID(const QueryPointer& query) override; GLQuery* syncGPUObject(const Query& query) override; diff --git a/libraries/gpu-gl/src/gpu/gl41/GL41BackendInput.cpp b/libraries/gpu-gl/src/gpu/gl41/GL41BackendInput.cpp index 638841b170..51a24563f3 100644 --- a/libraries/gpu-gl/src/gpu/gl41/GL41BackendInput.cpp +++ b/libraries/gpu-gl/src/gpu/gl41/GL41BackendInput.cpp @@ -99,8 +99,13 @@ void GL41Backend::updateInput() { GLboolean isNormalized = attrib._element.isNormalized(); for (size_t locNum = 0; locNum < locationCount; ++locNum) { - glVertexAttribPointer(slot + (GLuint)locNum, count, type, isNormalized, stride, - reinterpret_cast(pointer + perLocationStride * (GLuint)locNum)); + if (attrib._element.isInteger()) { + glVertexAttribIPointer(slot + (GLuint)locNum, count, type, stride, + reinterpret_cast(pointer + perLocationStride * (GLuint)locNum)); + } else { + glVertexAttribPointer(slot + (GLuint)locNum, count, type, isNormalized, stride, + reinterpret_cast(pointer + perLocationStride * (GLuint)locNum)); + } #ifdef GPU_STEREO_DRAWCALL_INSTANCED glVertexAttribDivisor(slot + (GLuint)locNum, attrib._frequency * (isStereo() ? 2 : 1)); #else diff --git a/libraries/gpu-gl/src/gpu/gl41/GL41BackendOutput.cpp b/libraries/gpu-gl/src/gpu/gl41/GL41BackendOutput.cpp index 6d11a52035..195b155bf3 100644 --- a/libraries/gpu-gl/src/gpu/gl41/GL41BackendOutput.cpp +++ b/libraries/gpu-gl/src/gpu/gl41/GL41BackendOutput.cpp @@ -53,10 +53,12 @@ public: GL_COLOR_ATTACHMENT15 }; int unit = 0; + auto backend = _backend.lock(); for (auto& b : _gpuObject.getRenderBuffers()) { surface = b._texture; if (surface) { - gltexture = gl::GLTexture::sync(*_backend.lock().get(), surface, false); // Grab the gltexture and don't transfer + Q_ASSERT(TextureUsageType::RENDERBUFFER == surface->getUsageType()); + gltexture = backend->syncGPUObject(surface); } else { gltexture = nullptr; } @@ -81,9 +83,11 @@ public: } if (_gpuObject.getDepthStamp() != _depthStamp) { + auto backend = _backend.lock(); auto surface = _gpuObject.getDepthStencilBuffer(); if (_gpuObject.hasDepthStencil() && surface) { - gltexture = gl::GLTexture::sync(*_backend.lock().get(), surface, false); // Grab the gltexture and don't transfer + Q_ASSERT(TextureUsageType::RENDERBUFFER == surface->getUsageType()); + gltexture = backend->syncGPUObject(surface); } if (gltexture) { @@ -110,7 +114,7 @@ public: glBindFramebuffer(GL_DRAW_FRAMEBUFFER, currentFBO); } - checkStatus(GL_DRAW_FRAMEBUFFER); + checkStatus(); } diff --git a/libraries/gpu-gl/src/gpu/gl41/GL41BackendTexture.cpp b/libraries/gpu-gl/src/gpu/gl41/GL41BackendTexture.cpp index 65c45111db..2056085091 100644 --- a/libraries/gpu-gl/src/gpu/gl41/GL41BackendTexture.cpp +++ b/libraries/gpu-gl/src/gpu/gl41/GL41BackendTexture.cpp @@ -19,31 +19,72 @@ using namespace gpu; using namespace gpu::gl; using namespace gpu::gl41; -using GL41TexelFormat = GLTexelFormat; +GLTexture* GL41Backend::syncGPUObject(const TexturePointer& texturePointer) { + if (!texturePointer) { + return nullptr; + } + + const Texture& texture = *texturePointer; + if (TextureUsageType::EXTERNAL == texture.getUsageType()) { + return Parent::syncGPUObject(texturePointer); + } + + if (!texture.isDefined()) { + // NO texture definition yet so let's avoid thinking + return nullptr; + } + + GL41Texture* object = Backend::getGPUObject(texture); + if (!object) { + switch (texture.getUsageType()) { + case TextureUsageType::RENDERBUFFER: + object = new GL41AttachmentTexture(shared_from_this(), texture); + break; + + case TextureUsageType::STRICT_RESOURCE: + qCDebug(gpugllogging) << "Strict texture " << texture.source().c_str(); + object = new GL41StrictResourceTexture(shared_from_this(), texture); + break; + + case TextureUsageType::RESOURCE: { + qCDebug(gpugllogging) << "variable / Strict texture " << texture.source().c_str(); + object = new GL41ResourceTexture(shared_from_this(), texture); + break; + } + + default: + Q_UNREACHABLE(); + } + } + + return object; +} + using GL41Texture = GL41Backend::GL41Texture; -GLuint GL41Texture::allocate() { - Backend::incrementTextureGPUCount(); +GL41Texture::GL41Texture(const std::weak_ptr& backend, const Texture& texture) + : GLTexture(backend, texture, allocate(texture)) { + incrementTextureGPUCount(); +} + +GLuint GL41Texture::allocate(const Texture& texture) { GLuint result; glGenTextures(1, &result); return result; } -GLuint GL41Backend::getTextureID(const TexturePointer& texture, bool transfer) { - return GL41Texture::getId(*this, texture, transfer); + +void GL41Texture::withPreservedTexture(std::function f) const { + const GLint TRANSFER_TEXTURE_UNIT = 32; + glActiveTexture(GL_TEXTURE0 + TRANSFER_TEXTURE_UNIT); + glBindTexture(_target, _texture); + (void)CHECK_GL_ERROR(); + + f(); + glBindTexture(_target, 0); + (void)CHECK_GL_ERROR(); } -GLTexture* GL41Backend::syncGPUObject(const TexturePointer& texture, bool transfer) { - return GL41Texture::sync(*this, texture, transfer); -} - -GL41Texture::GL41Texture(const std::weak_ptr& backend, const Texture& texture, GLuint externalId) - : GLTexture(backend, texture, externalId) { -} - -GL41Texture::GL41Texture(const std::weak_ptr& backend, const Texture& texture, bool transferrable) - : GLTexture(backend, texture, allocate(), transferrable) { -} void GL41Texture::generateMips() const { withPreservedTexture([&] { @@ -52,95 +93,35 @@ void GL41Texture::generateMips() const { (void)CHECK_GL_ERROR(); } -void GL41Texture::allocateStorage() const { - GLTexelFormat texelFormat = GLTexelFormat::evalGLTexelFormat(_gpuObject.getTexelFormat()); - glTexParameteri(_target, GL_TEXTURE_BASE_LEVEL, 0); - (void)CHECK_GL_ERROR(); - glTexParameteri(_target, GL_TEXTURE_MAX_LEVEL, _maxMip - _minMip); - (void)CHECK_GL_ERROR(); - if (GLEW_VERSION_4_2 && !_gpuObject.getTexelFormat().isCompressed()) { - // Get the dimensions, accounting for the downgrade level - Vec3u dimensions = _gpuObject.evalMipDimensions(_minMip); - glTexStorage2D(_target, usedMipLevels(), texelFormat.internalFormat, dimensions.x, dimensions.y); - (void)CHECK_GL_ERROR(); +void GL41Texture::copyMipFaceLinesFromTexture(uint16_t mip, uint8_t face, const uvec3& size, uint32_t yOffset, GLenum format, GLenum type, const void* sourcePointer) const { + if (GL_TEXTURE_2D == _target) { + glTexSubImage2D(_target, mip, 0, yOffset, size.x, size.y, format, type, sourcePointer); + } else if (GL_TEXTURE_CUBE_MAP == _target) { + auto target = GLTexture::CUBE_FACE_LAYOUT[face]; + glTexSubImage2D(target, mip, 0, yOffset, size.x, size.y, format, type, sourcePointer); } else { - for (uint16_t l = _minMip; l <= _maxMip; l++) { - // Get the mip level dimensions, accounting for the downgrade level - Vec3u dimensions = _gpuObject.evalMipDimensions(l); - for (GLenum target : getFaceTargets(_target)) { - glTexImage2D(target, l - _minMip, texelFormat.internalFormat, dimensions.x, dimensions.y, 0, texelFormat.format, texelFormat.type, NULL); - (void)CHECK_GL_ERROR(); - } - } + assert(false); } + (void)CHECK_GL_ERROR(); } -void GL41Texture::updateSize() const { - setSize(_virtualSize); - if (!_id) { +void GL41Texture::copyMipFaceFromTexture(uint16_t sourceMip, uint16_t targetMip, uint8_t face) const { + if (!_gpuObject.isStoredMipFaceAvailable(sourceMip)) { return; } - - if (_gpuObject.getTexelFormat().isCompressed()) { - GLenum proxyType = GL_TEXTURE_2D; - GLuint numFaces = 1; - if (_gpuObject.getType() == gpu::Texture::TEX_CUBE) { - proxyType = CUBE_FACE_LAYOUT[0]; - numFaces = (GLuint)CUBE_NUM_FACES; - } - GLint gpuSize{ 0 }; - glGetTexLevelParameteriv(proxyType, 0, GL_TEXTURE_COMPRESSED, &gpuSize); - (void)CHECK_GL_ERROR(); - - if (gpuSize) { - for (GLuint level = _minMip; level < _maxMip; level++) { - GLint levelSize{ 0 }; - glGetTexLevelParameteriv(proxyType, level, GL_TEXTURE_COMPRESSED_IMAGE_SIZE, &levelSize); - levelSize *= numFaces; - - if (levelSize <= 0) { - break; - } - gpuSize += levelSize; - } - (void)CHECK_GL_ERROR(); - setSize(gpuSize); - return; - } - } -} - -// Move content bits from the CPU to the GPU for a given mip / face -void GL41Texture::transferMip(uint16_t mipLevel, uint8_t face) const { - auto mip = _gpuObject.accessStoredMipFace(mipLevel, face); - GLTexelFormat texelFormat = GLTexelFormat::evalGLTexelFormat(_gpuObject.getTexelFormat(), mip->getFormat()); - //GLenum target = getFaceTargets()[face]; - GLenum target = _target == GL_TEXTURE_2D ? GL_TEXTURE_2D : CUBE_FACE_LAYOUT[face]; - auto size = _gpuObject.evalMipDimensions(mipLevel); - glTexSubImage2D(target, mipLevel, 0, 0, size.x, size.y, texelFormat.format, texelFormat.type, mip->readData()); - (void)CHECK_GL_ERROR(); -} - -void GL41Texture::startTransfer() { - PROFILE_RANGE(render_gpu_gl, __FUNCTION__); - Parent::startTransfer(); - - glBindTexture(_target, _id); - (void)CHECK_GL_ERROR(); - - // transfer pixels from each faces - uint8_t numFaces = (Texture::TEX_CUBE == _gpuObject.getType()) ? CUBE_NUM_FACES : 1; - for (uint8_t f = 0; f < numFaces; f++) { - for (uint16_t i = 0; i < Sampler::MAX_MIP_LEVEL; ++i) { - if (_gpuObject.isStoredMipFaceAvailable(i, f)) { - transferMip(i, f); - } - } + auto size = _gpuObject.evalMipDimensions(sourceMip); + auto mipData = _gpuObject.accessStoredMipFace(sourceMip, face); + if (mipData) { + GLTexelFormat texelFormat = GLTexelFormat::evalGLTexelFormat(_gpuObject.getTexelFormat(), _gpuObject.getStoredMipFormat()); + copyMipFaceLinesFromTexture(targetMip, face, size, 0, texelFormat.format, texelFormat.type, mipData->readData()); + } else { + qCDebug(gpugllogging) << "Missing mipData level=" << sourceMip << " face=" << (int)face << " for texture " << _gpuObject.source().c_str(); } } -void GL41Backend::GL41Texture::syncSampler() const { +void GL41Texture::syncSampler() const { const Sampler& sampler = _gpuObject.getSampler(); + const auto& fm = FILTER_MODES[sampler.getFilter()]; glTexParameteri(_target, GL_TEXTURE_MIN_FILTER, fm.minFilter); glTexParameteri(_target, GL_TEXTURE_MAG_FILTER, fm.magFilter); @@ -158,8 +139,106 @@ void GL41Backend::GL41Texture::syncSampler() const { glTexParameterfv(_target, GL_TEXTURE_BORDER_COLOR, (const float*)&sampler.getBorderColor()); glTexParameteri(_target, GL_TEXTURE_BASE_LEVEL, (uint16)sampler.getMipOffset()); + glTexParameterf(_target, GL_TEXTURE_MIN_LOD, (float)sampler.getMinMip()); glTexParameterf(_target, GL_TEXTURE_MAX_LOD, (sampler.getMaxMip() == Sampler::MAX_MIP_LEVEL ? 1000.f : sampler.getMaxMip())); + glTexParameterf(_target, GL_TEXTURE_MAX_ANISOTROPY_EXT, sampler.getMaxAnisotropy()); } +using GL41FixedAllocationTexture = GL41Backend::GL41FixedAllocationTexture; + +GL41FixedAllocationTexture::GL41FixedAllocationTexture(const std::weak_ptr& backend, const Texture& texture) : GL41Texture(backend, texture), _size(texture.evalTotalSize()) { + withPreservedTexture([&] { + allocateStorage(); + syncSampler(); + }); +} + +GL41FixedAllocationTexture::~GL41FixedAllocationTexture() { +} + +void GL41FixedAllocationTexture::allocateStorage() const { + const GLTexelFormat texelFormat = GLTexelFormat::evalGLTexelFormat(_gpuObject.getTexelFormat()); + const auto numMips = _gpuObject.getNumMips(); + + // glTextureStorage2D(_id, mips, texelFormat.internalFormat, dimensions.x, dimensions.y); + for (GLint level = 0; level < numMips; level++) { + Vec3u dimensions = _gpuObject.evalMipDimensions(level); + for (GLenum target : getFaceTargets(_target)) { + glTexImage2D(target, level, texelFormat.internalFormat, dimensions.x, dimensions.y, 0, texelFormat.format, texelFormat.type, nullptr); + } + } + + glTexParameteri(_target, GL_TEXTURE_BASE_LEVEL, 0); + glTexParameteri(_target, GL_TEXTURE_MAX_LEVEL, numMips - 1); +} + +void GL41FixedAllocationTexture::syncSampler() const { + Parent::syncSampler(); + const Sampler& sampler = _gpuObject.getSampler(); + auto baseMip = std::max(sampler.getMipOffset(), sampler.getMinMip()); + + glTexParameteri(_target, GL_TEXTURE_BASE_LEVEL, baseMip); + glTexParameterf(_target, GL_TEXTURE_MIN_LOD, (float)sampler.getMinMip()); + glTexParameterf(_target, GL_TEXTURE_MAX_LOD, (sampler.getMaxMip() == Sampler::MAX_MIP_LEVEL ? 1000.0f : sampler.getMaxMip())); +} + +// Renderbuffer attachment textures +using GL41AttachmentTexture = GL41Backend::GL41AttachmentTexture; + +GL41AttachmentTexture::GL41AttachmentTexture(const std::weak_ptr& backend, const Texture& texture) : GL41FixedAllocationTexture(backend, texture) { + Backend::updateTextureGPUFramebufferMemoryUsage(0, size()); +} + +GL41AttachmentTexture::~GL41AttachmentTexture() { + Backend::updateTextureGPUFramebufferMemoryUsage(size(), 0); +} + +// Strict resource textures +using GL41StrictResourceTexture = GL41Backend::GL41StrictResourceTexture; + +GL41StrictResourceTexture::GL41StrictResourceTexture(const std::weak_ptr& backend, const Texture& texture) : GL41FixedAllocationTexture(backend, texture) { + withPreservedTexture([&] { + + auto mipLevels = _gpuObject.getNumMips(); + for (uint16_t sourceMip = 0; sourceMip < mipLevels; sourceMip++) { + uint16_t targetMip = sourceMip; + size_t maxFace = GLTexture::getFaceCount(_target); + for (uint8_t face = 0; face < maxFace; face++) { + copyMipFaceFromTexture(sourceMip, targetMip, face); + } + } + }); + + if (texture.isAutogenerateMips()) { + generateMips(); + } +} + +// resource textures +using GL41ResourceTexture = GL41Backend::GL41ResourceTexture; + +GL41ResourceTexture::GL41ResourceTexture(const std::weak_ptr& backend, const Texture& texture) : GL41FixedAllocationTexture(backend, texture) { + Backend::updateTextureGPUMemoryUsage(0, size()); + + withPreservedTexture([&] { + + auto mipLevels = _gpuObject.getNumMips(); + for (uint16_t sourceMip = 0; sourceMip < mipLevels; sourceMip++) { + uint16_t targetMip = sourceMip; + size_t maxFace = GLTexture::getFaceCount(_target); + for (uint8_t face = 0; face < maxFace; face++) { + copyMipFaceFromTexture(sourceMip, targetMip, face); + } + } + }); + + if (texture.isAutogenerateMips()) { + generateMips(); + } +} + +GL41ResourceTexture::~GL41ResourceTexture() { + Backend::updateTextureGPUMemoryUsage(size(), 0); +} diff --git a/libraries/gpu-gl/src/gpu/gl41/GL41BackendTransform.cpp b/libraries/gpu-gl/src/gpu/gl41/GL41BackendTransform.cpp index aa0a83689c..526bac9ec8 100644 --- a/libraries/gpu-gl/src/gpu/gl41/GL41BackendTransform.cpp +++ b/libraries/gpu-gl/src/gpu/gl41/GL41BackendTransform.cpp @@ -95,7 +95,6 @@ void GL41Backend::updateTransform(const Batch& batch) { } else { if (!_transform._enabledDrawcallInfoBuffer) { glEnableVertexAttribArray(gpu::Stream::DRAW_CALL_INFO); // Make sure attrib array is enabled - glBindBuffer(GL_ARRAY_BUFFER, _transform._drawCallInfoBuffer); #ifdef GPU_STEREO_DRAWCALL_INSTANCED glVertexAttribDivisor(gpu::Stream::DRAW_CALL_INFO, (isStereo() ? 2 : 1)); #else @@ -103,6 +102,7 @@ void GL41Backend::updateTransform(const Batch& batch) { #endif _transform._enabledDrawcallInfoBuffer = true; } + glBindBuffer(GL_ARRAY_BUFFER, _transform._drawCallInfoBuffer); glVertexAttribIPointer(gpu::Stream::DRAW_CALL_INFO, 2, GL_UNSIGNED_SHORT, 0, _transform._drawCallInfoOffsets[batch._currentNamedCall]); } diff --git a/libraries/gpu-gl/src/gpu/gl45/GL45Backend.cpp b/libraries/gpu-gl/src/gpu/gl45/GL45Backend.cpp index d7dde8b7d6..12c4b818f7 100644 --- a/libraries/gpu-gl/src/gpu/gl45/GL45Backend.cpp +++ b/libraries/gpu-gl/src/gpu/gl45/GL45Backend.cpp @@ -18,6 +18,12 @@ Q_LOGGING_CATEGORY(gpugl45logging, "hifi.gpu.gl45") using namespace gpu; using namespace gpu::gl45; +void GL45Backend::recycle() const { + Parent::recycle(); + GL45VariableAllocationTexture::manageMemory(); + GL45VariableAllocationTexture::_frameTexturesCreated = 0; +} + void GL45Backend::do_draw(const Batch& batch, size_t paramOffset) { Primitive primitiveType = (Primitive)batch._params[paramOffset + 2]._uint; GLenum mode = gl::PRIMITIVE_TO_GL[primitiveType]; @@ -163,8 +169,3 @@ void GL45Backend::do_multiDrawIndexedIndirect(const Batch& batch, size_t paramOf _stats._DSNumAPIDrawcalls++; (void)CHECK_GL_ERROR(); } - -void GL45Backend::recycle() const { - Parent::recycle(); - derezTextures(); -} diff --git a/libraries/gpu-gl/src/gpu/gl45/GL45Backend.h b/libraries/gpu-gl/src/gpu/gl45/GL45Backend.h index 2242bba5d9..ca8028aff6 100644 --- a/libraries/gpu-gl/src/gpu/gl45/GL45Backend.h +++ b/libraries/gpu-gl/src/gpu/gl45/GL45Backend.h @@ -8,17 +8,21 @@ // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // +#pragma once #ifndef hifi_gpu_45_GL45Backend_h #define hifi_gpu_45_GL45Backend_h #include "../gl/GLBackend.h" #include "../gl/GLTexture.h" +#include #define INCREMENTAL_TRANSFER 0 +#define THREADED_TEXTURE_BUFFERING 1 namespace gpu { namespace gl45 { using namespace gpu::gl; +using TextureWeakPointer = std::weak_ptr; class GL45Backend : public GLBackend { using Parent = GLBackend; @@ -31,60 +35,219 @@ public: class GL45Texture : public GLTexture { using Parent = GLTexture; + friend class GL45Backend; static GLuint allocate(const Texture& texture); + protected: + GL45Texture(const std::weak_ptr& backend, const Texture& texture); + void generateMips() const override; + void copyMipFaceFromTexture(uint16_t sourceMip, uint16_t targetMip, uint8_t face) const; + void copyMipFaceLinesFromTexture(uint16_t mip, uint8_t face, const uvec3& size, uint32_t yOffset, GLenum format, GLenum type, const void* sourcePointer) const; + virtual void syncSampler() const; + }; + + // + // Textures that have fixed allocation sizes and cannot be managed at runtime + // + + class GL45FixedAllocationTexture : public GL45Texture { + using Parent = GL45Texture; + friend class GL45Backend; + + public: + GL45FixedAllocationTexture(const std::weak_ptr& backend, const Texture& texture); + ~GL45FixedAllocationTexture(); + + protected: + Size size() const override { return _size; } + void allocateStorage() const; + void syncSampler() const override; + const Size _size { 0 }; + }; + + class GL45AttachmentTexture : public GL45FixedAllocationTexture { + using Parent = GL45FixedAllocationTexture; + friend class GL45Backend; + protected: + GL45AttachmentTexture(const std::weak_ptr& backend, const Texture& texture); + ~GL45AttachmentTexture(); + }; + + class GL45StrictResourceTexture : public GL45FixedAllocationTexture { + using Parent = GL45FixedAllocationTexture; + friend class GL45Backend; + protected: + GL45StrictResourceTexture(const std::weak_ptr& backend, const Texture& texture); + }; + + // + // Textures that can be managed at runtime to increase or decrease their memory load + // + + class GL45VariableAllocationTexture : public GL45Texture { + using Parent = GL45Texture; + friend class GL45Backend; + using PromoteLambda = std::function; + + public: + enum class MemoryPressureState { + Idle, + Transfer, + Oversubscribed, + Undersubscribed, + }; + + using QueuePair = std::pair; + struct QueuePairLess { + bool operator()(const QueuePair& a, const QueuePair& b) { + return a.second < b.second; + } + }; + using WorkQueue = std::priority_queue, QueuePairLess>; + + class TransferJob { + using VoidLambda = std::function; + using VoidLambdaQueue = std::queue; + using ThreadPointer = std::shared_ptr; + const GL45VariableAllocationTexture& _parent; + // Holds the contents to transfer to the GPU in CPU memory + std::vector _buffer; + // Indicates if a transfer from backing storage to interal storage has started + bool _bufferingStarted { false }; + bool _bufferingCompleted { false }; + VoidLambda _transferLambda; + VoidLambda _bufferingLambda; +#if THREADED_TEXTURE_BUFFERING + static Mutex _mutex; + static VoidLambdaQueue _bufferLambdaQueue; + static ThreadPointer _bufferThread; + static std::atomic _shutdownBufferingThread; + static void bufferLoop(); +#endif + + public: + TransferJob(const TransferJob& other) = delete; + TransferJob(const GL45VariableAllocationTexture& parent, std::function transferLambda); + TransferJob(const GL45VariableAllocationTexture& parent, uint16_t sourceMip, uint16_t targetMip, uint8_t face, uint32_t lines = 0, uint32_t lineOffset = 0); + ~TransferJob(); + bool tryTransfer(); + +#if THREADED_TEXTURE_BUFFERING + static void startTransferLoop(); + static void stopTransferLoop(); +#endif + + private: + size_t _transferSize { 0 }; +#if THREADED_TEXTURE_BUFFERING + void startBuffering(); +#endif + void transfer(); + }; + + using TransferQueue = std::queue>; + static MemoryPressureState _memoryPressureState; + protected: + static size_t _frameTexturesCreated; + static std::atomic _memoryPressureStateStale; + static std::list _memoryManagedTextures; + static WorkQueue _transferQueue; + static WorkQueue _promoteQueue; + static WorkQueue _demoteQueue; + static TexturePointer _currentTransferTexture; + static const uvec3 INITIAL_MIP_TRANSFER_DIMENSIONS; + + + static void updateMemoryPressure(); + static void processWorkQueues(); + static void addMemoryManagedTexture(const TexturePointer& texturePointer); + static void addToWorkQueue(const TexturePointer& texture); + static WorkQueue& getActiveWorkQueue(); + + static void manageMemory(); + + protected: + GL45VariableAllocationTexture(const std::weak_ptr& backend, const Texture& texture); + ~GL45VariableAllocationTexture(); + //bool canPromoteNoAllocate() const { return _allocatedMip < _populatedMip; } + bool canPromote() const { return _allocatedMip > 0; } + bool canDemote() const { return _allocatedMip < _maxAllocatedMip; } + bool hasPendingTransfers() const { return _populatedMip > _allocatedMip; } + void executeNextTransfer(const TexturePointer& currentTexture); + Size size() const override { return _size; } + virtual void populateTransferQueue() = 0; + virtual void promote() = 0; + virtual void demote() = 0; + + // The allocated mip level, relative to the number of mips in the gpu::Texture object + // The relationship between a given glMip to the original gpu::Texture mip is always + // glMip + _allocatedMip + uint16 _allocatedMip { 0 }; + // The populated mip level, relative to the number of mips in the gpu::Texture object + // This must always be >= the allocated mip + uint16 _populatedMip { 0 }; + // The highest (lowest resolution) mip that we will support, relative to the number + // of mips in the gpu::Texture object + uint16 _maxAllocatedMip { 0 }; + Size _size { 0 }; + // Contains a series of lambdas that when executed will transfer data to the GPU, modify + // the _populatedMip and update the sampler in order to fully populate the allocated texture + // until _populatedMip == _allocatedMip + TransferQueue _pendingTransfers; + }; + + class GL45ResourceTexture : public GL45VariableAllocationTexture { + using Parent = GL45VariableAllocationTexture; + friend class GL45Backend; + protected: + GL45ResourceTexture(const std::weak_ptr& backend, const Texture& texture); + + void syncSampler() const override; + void promote() override; + void demote() override; + void populateTransferQueue() override; + + void allocateStorage(uint16 mip); + void copyMipsFromTexture(); + }; + +#if 0 + class GL45SparseResourceTexture : public GL45VariableAllocationTexture { + using Parent = GL45VariableAllocationTexture; + friend class GL45Backend; + using TextureTypeFormat = std::pair; + using PageDimensions = std::vector; + using PageDimensionsMap = std::map; + static PageDimensionsMap pageDimensionsByFormat; + static Mutex pageDimensionsMutex; + + static bool isSparseEligible(const Texture& texture); + static PageDimensions getPageDimensionsForFormat(const TextureTypeFormat& typeFormat); + static PageDimensions getPageDimensionsForFormat(GLenum type, GLenum format); static const uint32_t DEFAULT_PAGE_DIMENSION = 128; static const uint32_t DEFAULT_MAX_SPARSE_LEVEL = 0xFFFF; - public: - GL45Texture(const std::weak_ptr& backend, const Texture& texture, GLuint externalId); - GL45Texture(const std::weak_ptr& backend, const Texture& texture, bool transferrable); - ~GL45Texture(); - - void postTransfer() override; - - struct SparseInfo { - SparseInfo(GL45Texture& texture); - void maybeMakeSparse(); - void update(); - uvec3 getPageCounts(const uvec3& dimensions) const; - uint32_t getPageCount(const uvec3& dimensions) const; - uint32_t getSize() const; - - GL45Texture& texture; - bool sparse { false }; - uvec3 pageDimensions { DEFAULT_PAGE_DIMENSION }; - GLuint maxSparseLevel { DEFAULT_MAX_SPARSE_LEVEL }; - uint32_t allocatedPages { 0 }; - uint32_t maxPages { 0 }; - uint32_t pageBytes { 0 }; - GLint pageDimensionsIndex { 0 }; - }; - protected: - void updateMips() override; - void stripToMip(uint16_t newMinMip); - void startTransfer() override; - bool continueTransfer() override; - void finishTransfer() override; - void incrementalTransfer(const uvec3& size, const gpu::Texture::PixelsPointer& mip, std::function f) const; - void transferMip(uint16_t mipLevel, uint8_t face = 0) const; - void allocateMip(uint16_t mipLevel, uint8_t face = 0) const; - void allocateStorage() const override; - void updateSize() const override; - void syncSampler() const override; - void generateMips() const override; - void withPreservedTexture(std::function f) const override; - void derez(); + GL45SparseResourceTexture(const std::weak_ptr& backend, const Texture& texture); + ~GL45SparseResourceTexture(); + uint32 size() const override { return _allocatedPages * _pageBytes; } + void promote() override; + void demote() override; - SparseInfo _sparseInfo; - uint16_t _mipOffset { 0 }; - friend class GL45Backend; + private: + uvec3 getPageCounts(const uvec3& dimensions) const; + uint32_t getPageCount(const uvec3& dimensions) const; + + uint32_t _allocatedPages { 0 }; + uint32_t _pageBytes { 0 }; + uvec3 _pageDimensions { DEFAULT_PAGE_DIMENSION }; + GLuint _maxSparseLevel { DEFAULT_MAX_SPARSE_LEVEL }; }; +#endif protected: + void recycle() const override; - void derezTextures() const; GLuint getFramebufferID(const FramebufferPointer& framebuffer) override; GLFramebuffer* syncGPUObject(const Framebuffer& framebuffer) override; @@ -92,8 +255,7 @@ protected: GLuint getBufferID(const Buffer& buffer) override; GLBuffer* syncGPUObject(const Buffer& buffer) override; - GLuint getTextureID(const TexturePointer& texture, bool needTransfer = true) override; - GLTexture* syncGPUObject(const TexturePointer& texture, bool sync = true) override; + GLTexture* syncGPUObject(const TexturePointer& texture) override; GLuint getQueryID(const QueryPointer& query) override; GLQuery* syncGPUObject(const Query& query) override; @@ -126,5 +288,5 @@ protected: Q_DECLARE_LOGGING_CATEGORY(gpugl45logging) - #endif + diff --git a/libraries/gpu-gl/src/gpu/gl45/GL45BackendInput.cpp b/libraries/gpu-gl/src/gpu/gl45/GL45BackendInput.cpp index 01bd2d7bce..ece62e15f1 100644 --- a/libraries/gpu-gl/src/gpu/gl45/GL45BackendInput.cpp +++ b/libraries/gpu-gl/src/gpu/gl45/GL45BackendInput.cpp @@ -61,8 +61,11 @@ void GL45Backend::updateInput() { _input._attributeActivation.set(attriNum); glEnableVertexAttribArray(attriNum); } - glVertexAttribFormat(attriNum, count, type, isNormalized, offset + locNum * perLocationSize); - // TODO: Support properly the IAttrib version + if (attrib._element.isInteger()) { + glVertexAttribIFormat(attriNum, count, type, offset + locNum * perLocationSize); + } else { + glVertexAttribFormat(attriNum, count, type, isNormalized, offset + locNum * perLocationSize); + } glVertexAttribBinding(attriNum, attrib._channel); } diff --git a/libraries/gpu-gl/src/gpu/gl45/GL45BackendOutput.cpp b/libraries/gpu-gl/src/gpu/gl45/GL45BackendOutput.cpp index c5b84b7deb..9648af9b21 100644 --- a/libraries/gpu-gl/src/gpu/gl45/GL45BackendOutput.cpp +++ b/libraries/gpu-gl/src/gpu/gl45/GL45BackendOutput.cpp @@ -49,10 +49,12 @@ public: GL_COLOR_ATTACHMENT15 }; int unit = 0; + auto backend = _backend.lock(); for (auto& b : _gpuObject.getRenderBuffers()) { surface = b._texture; if (surface) { - gltexture = gl::GLTexture::sync(*_backend.lock().get(), surface, false); // Grab the gltexture and don't transfer + Q_ASSERT(TextureUsageType::RENDERBUFFER == surface->getUsageType()); + gltexture = backend->syncGPUObject(surface); } else { gltexture = nullptr; } @@ -78,8 +80,10 @@ public: if (_gpuObject.getDepthStamp() != _depthStamp) { auto surface = _gpuObject.getDepthStencilBuffer(); + auto backend = _backend.lock(); if (_gpuObject.hasDepthStencil() && surface) { - gltexture = gl::GLTexture::sync(*_backend.lock().get(), surface, false); // Grab the gltexture and don't transfer + Q_ASSERT(TextureUsageType::RENDERBUFFER == surface->getUsageType()); + gltexture = backend->syncGPUObject(surface); } if (gltexture) { @@ -102,7 +106,7 @@ public: _status = glCheckNamedFramebufferStatus(_id, GL_DRAW_FRAMEBUFFER); // restore the current framebuffer - checkStatus(GL_DRAW_FRAMEBUFFER); + checkStatus(); } diff --git a/libraries/gpu-gl/src/gpu/gl45/GL45BackendTexture.cpp b/libraries/gpu-gl/src/gpu/gl45/GL45BackendTexture.cpp index 6948a045a2..ab4153c04c 100644 --- a/libraries/gpu-gl/src/gpu/gl45/GL45BackendTexture.cpp +++ b/libraries/gpu-gl/src/gpu/gl45/GL45BackendTexture.cpp @@ -8,153 +8,81 @@ // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -#include "GL45Backend.h" +#include "GL45Backend.h" #include +#include #include #include #include #include #include -#include +#include #include "../gl/GLTexelFormat.h" using namespace gpu; using namespace gpu::gl; using namespace gpu::gl45; -// Allocate 1 MB of buffer space for paged transfers -#define DEFAULT_PAGE_BUFFER_SIZE (1024*1024) -#define DEFAULT_GL_PIXEL_ALIGNMENT 4 - -using GL45Texture = GL45Backend::GL45Texture; - -static std::map> texturesByMipCounts; -static Mutex texturesByMipCountsMutex; -using TextureTypeFormat = std::pair; -std::map> sparsePageDimensionsByFormat; -Mutex sparsePageDimensionsByFormatMutex; - -static std::vector getPageDimensionsForFormat(const TextureTypeFormat& typeFormat) { - { - Lock lock(sparsePageDimensionsByFormatMutex); - if (sparsePageDimensionsByFormat.count(typeFormat)) { - return sparsePageDimensionsByFormat[typeFormat]; - } - } - GLint count = 0; - glGetInternalformativ(typeFormat.first, typeFormat.second, GL_NUM_VIRTUAL_PAGE_SIZES_ARB, 1, &count); - - std::vector result; - if (count > 0) { - std::vector x, y, z; - x.resize(count); - glGetInternalformativ(typeFormat.first, typeFormat.second, GL_VIRTUAL_PAGE_SIZE_X_ARB, 1, &x[0]); - y.resize(count); - glGetInternalformativ(typeFormat.first, typeFormat.second, GL_VIRTUAL_PAGE_SIZE_Y_ARB, 1, &y[0]); - z.resize(count); - glGetInternalformativ(typeFormat.first, typeFormat.second, GL_VIRTUAL_PAGE_SIZE_Z_ARB, 1, &z[0]); - - result.resize(count); - for (GLint i = 0; i < count; ++i) { - result[i] = uvec3(x[i], y[i], z[i]); - } - } - - { - Lock lock(sparsePageDimensionsByFormatMutex); - if (0 == sparsePageDimensionsByFormat.count(typeFormat)) { - sparsePageDimensionsByFormat[typeFormat] = result; - } - } - - return result; -} - -static std::vector getPageDimensionsForFormat(GLenum target, GLenum format) { - return getPageDimensionsForFormat({ target, format }); -} - -GLTexture* GL45Backend::syncGPUObject(const TexturePointer& texture, bool transfer) { - return GL45Texture::sync(*this, texture, transfer); -} - -using SparseInfo = GL45Backend::GL45Texture::SparseInfo; - -SparseInfo::SparseInfo(GL45Texture& texture) - : texture(texture) { -} - -void SparseInfo::maybeMakeSparse() { - // Don't enable sparse for objects with explicitly managed mip levels - if (!texture._gpuObject.isAutogenerateMips()) { - return; - } - return; - - const uvec3 dimensions = texture._gpuObject.getDimensions(); - auto allowedPageDimensions = getPageDimensionsForFormat(texture._target, texture._internalFormat); - // In order to enable sparse the texture size must be an integer multiple of the page size - for (size_t i = 0; i < allowedPageDimensions.size(); ++i) { - pageDimensionsIndex = (uint32_t) i; - pageDimensions = allowedPageDimensions[i]; - // Is this texture an integer multiple of page dimensions? - if (uvec3(0) == (dimensions % pageDimensions)) { - qCDebug(gpugl45logging) << "Enabling sparse for texture " << texture._source.c_str(); - sparse = true; - break; - } - } - - if (sparse) { - glTextureParameteri(texture._id, GL_TEXTURE_SPARSE_ARB, GL_TRUE); - glTextureParameteri(texture._id, GL_VIRTUAL_PAGE_SIZE_INDEX_ARB, pageDimensionsIndex); - } else { - qCDebug(gpugl45logging) << "Size " << dimensions.x << " x " << dimensions.y << - " is not supported by any sparse page size for texture" << texture._source.c_str(); - } -} - #define SPARSE_PAGE_SIZE_OVERHEAD_ESTIMATE 1.3f +#define MAX_RESOURCE_TEXTURES_PER_FRAME 2 -// This can only be called after we've established our storage size -void SparseInfo::update() { - if (!sparse) { - return; +GLTexture* GL45Backend::syncGPUObject(const TexturePointer& texturePointer) { + if (!texturePointer) { + return nullptr; } - glGetTextureParameterIuiv(texture._id, GL_NUM_SPARSE_LEVELS_ARB, &maxSparseLevel); - pageBytes = texture._gpuObject.getTexelFormat().getSize(); - pageBytes *= pageDimensions.x * pageDimensions.y * pageDimensions.z; - // Testing with a simple texture allocating app shows an estimated 20% GPU memory overhead for - // sparse textures as compared to non-sparse, so we acount for that here. - pageBytes = (uint32_t)(pageBytes * SPARSE_PAGE_SIZE_OVERHEAD_ESTIMATE); - for (uint16_t mipLevel = 0; mipLevel <= maxSparseLevel; ++mipLevel) { - auto mipDimensions = texture._gpuObject.evalMipDimensions(mipLevel); - auto mipPageCount = getPageCount(mipDimensions); - maxPages += mipPageCount; + const Texture& texture = *texturePointer; + if (TextureUsageType::EXTERNAL == texture.getUsageType()) { + return Parent::syncGPUObject(texturePointer); } - if (texture._target == GL_TEXTURE_CUBE_MAP) { - maxPages *= GLTexture::CUBE_NUM_FACES; + + if (!texture.isDefined()) { + // NO texture definition yet so let's avoid thinking + return nullptr; } -} -uvec3 SparseInfo::getPageCounts(const uvec3& dimensions) const { - auto result = (dimensions / pageDimensions) + - glm::clamp(dimensions % pageDimensions, glm::uvec3(0), glm::uvec3(1)); - return result; -} + GL45Texture* object = Backend::getGPUObject(texture); + if (!object) { + switch (texture.getUsageType()) { + case TextureUsageType::RENDERBUFFER: + object = new GL45AttachmentTexture(shared_from_this(), texture); + break; -uint32_t SparseInfo::getPageCount(const uvec3& dimensions) const { - auto pageCounts = getPageCounts(dimensions); - return pageCounts.x * pageCounts.y * pageCounts.z; -} + case TextureUsageType::STRICT_RESOURCE: + qCDebug(gpugllogging) << "Strict texture " << texture.source().c_str(); + object = new GL45StrictResourceTexture(shared_from_this(), texture); + break; + case TextureUsageType::RESOURCE: { + if (GL45VariableAllocationTexture::_frameTexturesCreated < MAX_RESOURCE_TEXTURES_PER_FRAME) { +#if 0 + if (isTextureManagementSparseEnabled() && GL45Texture::isSparseEligible(texture)) { + object = new GL45SparseResourceTexture(shared_from_this(), texture); + } else { + object = new GL45ResourceTexture(shared_from_this(), texture); + } +#else + object = new GL45ResourceTexture(shared_from_this(), texture); +#endif + GL45VariableAllocationTexture::addMemoryManagedTexture(texturePointer); + } else { + auto fallback = texturePointer->getFallbackTexture(); + if (fallback) { + object = static_cast(syncGPUObject(fallback)); + } + } + break; + } -uint32_t SparseInfo::getSize() const { - return allocatedPages * pageBytes; + default: + Q_UNREACHABLE(); + } + } + + return object; } void GL45Backend::initTextureManagementStage() { @@ -171,6 +99,12 @@ void GL45Backend::initTextureManagementStage() { } } +using GL45Texture = GL45Backend::GL45Texture; + +GL45Texture::GL45Texture(const std::weak_ptr& backend, const Texture& texture) + : GLTexture(backend, texture, allocate(texture)) { + incrementTextureGPUCount(); +} GLuint GL45Texture::allocate(const Texture& texture) { GLuint result; @@ -178,164 +112,43 @@ GLuint GL45Texture::allocate(const Texture& texture) { return result; } -GLuint GL45Backend::getTextureID(const TexturePointer& texture, bool transfer) { - return GL45Texture::getId(*this, texture, transfer); -} - -GL45Texture::GL45Texture(const std::weak_ptr& backend, const Texture& texture, GLuint externalId) - : GLTexture(backend, texture, externalId), _sparseInfo(*this) -{ -} - -GL45Texture::GL45Texture(const std::weak_ptr& backend, const Texture& texture, bool transferrable) - : GLTexture(backend, texture, allocate(texture), transferrable), _sparseInfo(*this) - { - - auto theBackend = _backend.lock(); - if (_transferrable && theBackend && theBackend->isTextureManagementSparseEnabled()) { - _sparseInfo.maybeMakeSparse(); - if (_sparseInfo.sparse) { - Backend::incrementTextureGPUSparseCount(); - } - } -} - -GL45Texture::~GL45Texture() { - // Remove this texture from the candidate list of derezzable textures - if (_transferrable) { - auto mipLevels = usedMipLevels(); - Lock lock(texturesByMipCountsMutex); - if (texturesByMipCounts.count(mipLevels)) { - auto& textures = texturesByMipCounts[mipLevels]; - textures.erase(this); - if (textures.empty()) { - texturesByMipCounts.erase(mipLevels); - } - } - } - - if (_sparseInfo.sparse) { - Backend::decrementTextureGPUSparseCount(); - - // Experimenation suggests that allocating sparse textures on one context/thread and deallocating - // them on another is buggy. So for sparse textures we need to queue a lambda with the deallocation - // callls to the transfer thread - auto id = _id; - // Set the class _id to 0 so we don't try to double delete - const_cast(_id) = 0; - std::list> destructionFunctions; - - uint8_t maxFace = (uint8_t)((_target == GL_TEXTURE_CUBE_MAP) ? GLTexture::CUBE_NUM_FACES : 1); - auto maxSparseMip = std::min(_maxMip, _sparseInfo.maxSparseLevel); - for (uint16_t mipLevel = _minMip; mipLevel <= maxSparseMip; ++mipLevel) { - auto mipDimensions = _gpuObject.evalMipDimensions(mipLevel); - destructionFunctions.push_back([id, maxFace, mipLevel, mipDimensions] { - glTexturePageCommitmentEXT(id, mipLevel, 0, 0, 0, mipDimensions.x, mipDimensions.y, maxFace, GL_FALSE); - }); - - auto deallocatedPages = _sparseInfo.getPageCount(mipDimensions) * maxFace; - assert(deallocatedPages <= _sparseInfo.allocatedPages); - _sparseInfo.allocatedPages -= deallocatedPages; - } - - if (0 != _sparseInfo.allocatedPages) { - qCWarning(gpugl45logging) << "Allocated pages remaining " << _id << " " << _sparseInfo.allocatedPages; - } - - auto size = _size; - const_cast(_size) = 0; - _textureTransferHelper->queueExecution([id, size, destructionFunctions] { - for (auto function : destructionFunctions) { - function(); - } - glDeleteTextures(1, &id); - Backend::decrementTextureGPUCount(); - Backend::updateTextureGPUMemoryUsage(size, 0); - Backend::updateTextureGPUSparseMemoryUsage(size, 0); - }); - } -} - -void GL45Texture::withPreservedTexture(std::function f) const { - f(); -} - void GL45Texture::generateMips() const { glGenerateTextureMipmap(_id); (void)CHECK_GL_ERROR(); } -void GL45Texture::allocateStorage() const { - if (_gpuObject.getTexelFormat().isCompressed()) { - qFatal("Compressed textures not yet supported"); +void GL45Texture::copyMipFaceLinesFromTexture(uint16_t mip, uint8_t face, const uvec3& size, uint32_t yOffset, GLenum format, GLenum type, const void* sourcePointer) const { + if (GL_TEXTURE_2D == _target) { + glTextureSubImage2D(_id, mip, 0, yOffset, size.x, size.y, format, type, sourcePointer); + } else if (GL_TEXTURE_CUBE_MAP == _target) { + // DSA ARB does not work on AMD, so use EXT + // unless EXT is not available on the driver + if (glTextureSubImage2DEXT) { + auto target = GLTexture::CUBE_FACE_LAYOUT[face]; + glTextureSubImage2DEXT(_id, target, mip, 0, yOffset, size.x, size.y, format, type, sourcePointer); + } else { + glTextureSubImage3D(_id, mip, 0, yOffset, face, size.x, size.y, 1, format, type, sourcePointer); + } + } else { + Q_ASSERT(false); } - glTextureParameteri(_id, GL_TEXTURE_BASE_LEVEL, 0); - glTextureParameteri(_id, GL_TEXTURE_MAX_LEVEL, _maxMip - _minMip); - // Get the dimensions, accounting for the downgrade level - Vec3u dimensions = _gpuObject.evalMipDimensions(_minMip + _mipOffset); - glTextureStorage2D(_id, usedMipLevels(), _internalFormat, dimensions.x, dimensions.y); (void)CHECK_GL_ERROR(); } -void GL45Texture::updateSize() const { - if (_gpuObject.getTexelFormat().isCompressed()) { - qFatal("Compressed textures not yet supported"); +void GL45Texture::copyMipFaceFromTexture(uint16_t sourceMip, uint16_t targetMip, uint8_t face) const { + if (!_gpuObject.isStoredMipFaceAvailable(sourceMip)) { + return; } - - if (_transferrable && _sparseInfo.sparse) { - auto size = _sparseInfo.getSize(); - Backend::updateTextureGPUSparseMemoryUsage(_size, size); - setSize(size); + auto size = _gpuObject.evalMipDimensions(sourceMip); + auto mipData = _gpuObject.accessStoredMipFace(sourceMip, face); + if (mipData) { + GLTexelFormat texelFormat = GLTexelFormat::evalGLTexelFormat(_gpuObject.getTexelFormat(), _gpuObject.getStoredMipFormat()); + copyMipFaceLinesFromTexture(targetMip, face, size, 0, texelFormat.format, texelFormat.type, mipData->readData()); } else { - setSize(_gpuObject.evalTotalSize(_mipOffset)); + qCDebug(gpugllogging) << "Missing mipData level=" << sourceMip << " face=" << (int)face << " for texture " << _gpuObject.source().c_str(); } } -void GL45Texture::startTransfer() { - Parent::startTransfer(); - _sparseInfo.update(); -} - -bool GL45Texture::continueTransfer() { - PROFILE_RANGE(render_gpu_gl, "continueTransfer") - size_t maxFace = GL_TEXTURE_CUBE_MAP == _target ? CUBE_NUM_FACES : 1; - for (uint8_t face = 0; face < maxFace; ++face) { - for (uint16_t mipLevel = _minMip; mipLevel <= _maxMip; ++mipLevel) { - auto size = _gpuObject.evalMipDimensions(mipLevel); - if (_sparseInfo.sparse && mipLevel <= _sparseInfo.maxSparseLevel) { - glTexturePageCommitmentEXT(_id, mipLevel, 0, 0, face, size.x, size.y, 1, GL_TRUE); - _sparseInfo.allocatedPages += _sparseInfo.getPageCount(size); - } - if (_gpuObject.isStoredMipFaceAvailable(mipLevel, face)) { - PROFILE_RANGE_EX(render_gpu_gl, "texSubImage", 0x0000ffff, (size.x * size.y * maxFace / 1024)); - - auto mip = _gpuObject.accessStoredMipFace(mipLevel, face); - GLTexelFormat texelFormat = GLTexelFormat::evalGLTexelFormat(_gpuObject.getTexelFormat(), mip->getFormat()); - if (GL_TEXTURE_2D == _target) { - glTextureSubImage2D(_id, mipLevel, 0, 0, size.x, size.y, texelFormat.format, texelFormat.type, mip->readData()); - } else if (GL_TEXTURE_CUBE_MAP == _target) { - // DSA ARB does not work on AMD, so use EXT - // unless EXT is not available on the driver - if (glTextureSubImage2DEXT) { - auto target = CUBE_FACE_LAYOUT[face]; - glTextureSubImage2DEXT(_id, target, mipLevel, 0, 0, size.x, size.y, texelFormat.format, texelFormat.type, mip->readData()); - } else { - glTextureSubImage3D(_id, mipLevel, 0, 0, face, size.x, size.y, 1, texelFormat.format, texelFormat.type, mip->readData()); - } - } else { - Q_ASSERT(false); - } - (void)CHECK_GL_ERROR(); - } - } - } - return false; -} - -void GL45Texture::finishTransfer() { - Parent::finishTransfer(); -} - void GL45Texture::syncSampler() const { const Sampler& sampler = _gpuObject.getSampler(); @@ -353,163 +166,69 @@ void GL45Texture::syncSampler() const { glTextureParameteri(_id, GL_TEXTURE_WRAP_S, WRAP_MODES[sampler.getWrapModeU()]); glTextureParameteri(_id, GL_TEXTURE_WRAP_T, WRAP_MODES[sampler.getWrapModeV()]); glTextureParameteri(_id, GL_TEXTURE_WRAP_R, WRAP_MODES[sampler.getWrapModeW()]); + + glTextureParameterf(_id, GL_TEXTURE_MAX_ANISOTROPY_EXT, sampler.getMaxAnisotropy()); glTextureParameterfv(_id, GL_TEXTURE_BORDER_COLOR, (const float*)&sampler.getBorderColor()); - // FIXME account for mip offsets here - auto baseMip = std::max(sampler.getMipOffset(), _minMip); + + glTextureParameterf(_id, GL_TEXTURE_MIN_LOD, sampler.getMinMip()); + glTextureParameterf(_id, GL_TEXTURE_MAX_LOD, (sampler.getMaxMip() == Sampler::MAX_MIP_LEVEL ? 1000.f : sampler.getMaxMip())); +} + +using GL45FixedAllocationTexture = GL45Backend::GL45FixedAllocationTexture; + +GL45FixedAllocationTexture::GL45FixedAllocationTexture(const std::weak_ptr& backend, const Texture& texture) : GL45Texture(backend, texture), _size(texture.evalTotalSize()) { + allocateStorage(); + syncSampler(); +} + +GL45FixedAllocationTexture::~GL45FixedAllocationTexture() { +} + +void GL45FixedAllocationTexture::allocateStorage() const { + const GLTexelFormat texelFormat = GLTexelFormat::evalGLTexelFormat(_gpuObject.getTexelFormat()); + const auto dimensions = _gpuObject.getDimensions(); + const auto mips = _gpuObject.getNumMips(); + + glTextureStorage2D(_id, mips, texelFormat.internalFormat, dimensions.x, dimensions.y); + + glTextureParameteri(_id, GL_TEXTURE_BASE_LEVEL, 0); + glTextureParameteri(_id, GL_TEXTURE_MAX_LEVEL, mips - 1); +} + +void GL45FixedAllocationTexture::syncSampler() const { + Parent::syncSampler(); + const Sampler& sampler = _gpuObject.getSampler(); + auto baseMip = std::max(sampler.getMipOffset(), sampler.getMinMip()); glTextureParameteri(_id, GL_TEXTURE_BASE_LEVEL, baseMip); glTextureParameterf(_id, GL_TEXTURE_MIN_LOD, (float)sampler.getMinMip()); - glTextureParameterf(_id, GL_TEXTURE_MAX_LOD, (sampler.getMaxMip() == Sampler::MAX_MIP_LEVEL ? 1000.f : sampler.getMaxMip() - _mipOffset)); - glTextureParameterf(_id, GL_TEXTURE_MAX_ANISOTROPY_EXT, sampler.getMaxAnisotropy()); + glTextureParameterf(_id, GL_TEXTURE_MAX_LOD, (sampler.getMaxMip() == Sampler::MAX_MIP_LEVEL ? 1000.f : sampler.getMaxMip())); } -void GL45Texture::postTransfer() { - Parent::postTransfer(); - auto mipLevels = usedMipLevels(); - if (_transferrable && mipLevels > 1 && _minMip < _sparseInfo.maxSparseLevel) { - Lock lock(texturesByMipCountsMutex); - texturesByMipCounts[mipLevels].insert(this); - } +// Renderbuffer attachment textures +using GL45AttachmentTexture = GL45Backend::GL45AttachmentTexture; + +GL45AttachmentTexture::GL45AttachmentTexture(const std::weak_ptr& backend, const Texture& texture) : GL45FixedAllocationTexture(backend, texture) { + Backend::updateTextureGPUFramebufferMemoryUsage(0, size()); } -void GL45Texture::stripToMip(uint16_t newMinMip) { - if (newMinMip < _minMip) { - qCWarning(gpugl45logging) << "Cannot decrease the min mip"; - return; - } +GL45AttachmentTexture::~GL45AttachmentTexture() { + Backend::updateTextureGPUFramebufferMemoryUsage(size(), 0); +} - if (_sparseInfo.sparse && newMinMip > _sparseInfo.maxSparseLevel) { - qCWarning(gpugl45logging) << "Cannot increase the min mip into the mip tail"; - return; - } +// Strict resource textures +using GL45StrictResourceTexture = GL45Backend::GL45StrictResourceTexture; - PROFILE_RANGE(render_gpu_gl, "GL45Texture::stripToMip"); - - auto mipLevels = usedMipLevels(); - { - Lock lock(texturesByMipCountsMutex); - assert(0 != texturesByMipCounts.count(mipLevels)); - assert(0 != texturesByMipCounts[mipLevels].count(this)); - texturesByMipCounts[mipLevels].erase(this); - if (texturesByMipCounts[mipLevels].empty()) { - texturesByMipCounts.erase(mipLevels); +GL45StrictResourceTexture::GL45StrictResourceTexture(const std::weak_ptr& backend, const Texture& texture) : GL45FixedAllocationTexture(backend, texture) { + auto mipLevels = _gpuObject.getNumMips(); + for (uint16_t sourceMip = 0; sourceMip < mipLevels; ++sourceMip) { + uint16_t targetMip = sourceMip; + size_t maxFace = GLTexture::getFaceCount(_target); + for (uint8_t face = 0; face < maxFace; ++face) { + copyMipFaceFromTexture(sourceMip, targetMip, face); } } - - // If we weren't generating mips before, we need to now that we're stripping down mip levels. - if (!_gpuObject.isAutogenerateMips()) { - qCDebug(gpugl45logging) << "Force mip generation for texture"; - glGenerateTextureMipmap(_id); - } - - - uint8_t maxFace = (uint8_t)((_target == GL_TEXTURE_CUBE_MAP) ? GLTexture::CUBE_NUM_FACES : 1); - if (_sparseInfo.sparse) { - for (uint16_t mip = _minMip; mip < newMinMip; ++mip) { - auto id = _id; - auto mipDimensions = _gpuObject.evalMipDimensions(mip); - _textureTransferHelper->queueExecution([id, mip, mipDimensions, maxFace] { - glTexturePageCommitmentEXT(id, mip, 0, 0, 0, mipDimensions.x, mipDimensions.y, maxFace, GL_FALSE); - }); - - auto deallocatedPages = _sparseInfo.getPageCount(mipDimensions) * maxFace; - assert(deallocatedPages < _sparseInfo.allocatedPages); - _sparseInfo.allocatedPages -= deallocatedPages; - } - _minMip = newMinMip; - } else { - GLuint oldId = _id; - // Find the distance between the old min mip and the new one - uint16 mipDelta = newMinMip - _minMip; - _mipOffset += mipDelta; - const_cast(_maxMip) -= mipDelta; - auto newLevels = usedMipLevels(); - - // Create and setup the new texture (allocate) - { - Vec3u newDimensions = _gpuObject.evalMipDimensions(_mipOffset); - PROFILE_RANGE_EX(render_gpu_gl, "Re-Allocate", 0xff0000ff, (newDimensions.x * newDimensions.y)); - - glCreateTextures(_target, 1, &const_cast(_id)); - glTextureParameteri(_id, GL_TEXTURE_BASE_LEVEL, 0); - glTextureParameteri(_id, GL_TEXTURE_MAX_LEVEL, _maxMip - _minMip); - glTextureStorage2D(_id, newLevels, _internalFormat, newDimensions.x, newDimensions.y); - } - - // Copy the contents of the old texture to the new - { - PROFILE_RANGE(render_gpu_gl, "Blit"); - // Preferred path only available in 4.3 - for (uint16 targetMip = _minMip; targetMip <= _maxMip; ++targetMip) { - uint16 sourceMip = targetMip + mipDelta; - Vec3u mipDimensions = _gpuObject.evalMipDimensions(targetMip + _mipOffset); - for (GLenum target : getFaceTargets(_target)) { - glCopyImageSubData( - oldId, target, sourceMip, 0, 0, 0, - _id, target, targetMip, 0, 0, 0, - mipDimensions.x, mipDimensions.y, 1 - ); - (void)CHECK_GL_ERROR(); - } - } - - glDeleteTextures(1, &oldId); - } - } - - // Re-sync the sampler to force access to the new mip level - syncSampler(); - updateSize(); - - // Re-insert into the texture-by-mips map if appropriate - mipLevels = usedMipLevels(); - if (mipLevels > 1 && (!_sparseInfo.sparse || _minMip < _sparseInfo.maxSparseLevel)) { - Lock lock(texturesByMipCountsMutex); - texturesByMipCounts[mipLevels].insert(this); + if (texture.isAutogenerateMips()) { + generateMips(); } } -void GL45Texture::updateMips() { - if (!_sparseInfo.sparse) { - return; - } - auto newMinMip = std::min(_gpuObject.minMip(), _sparseInfo.maxSparseLevel); - if (_minMip < newMinMip) { - stripToMip(newMinMip); - } -} - -void GL45Texture::derez() { - if (_sparseInfo.sparse) { - assert(_minMip < _sparseInfo.maxSparseLevel); - } - assert(_minMip < _maxMip); - assert(_transferrable); - stripToMip(_minMip + 1); -} - -void GL45Backend::derezTextures() const { - if (GLTexture::getMemoryPressure() < 1.0f) { - return; - } - - Lock lock(texturesByMipCountsMutex); - if (texturesByMipCounts.empty()) { - // No available textures to derez - return; - } - - auto mipLevel = texturesByMipCounts.rbegin()->first; - if (mipLevel <= 1) { - // No mips available to remove - return; - } - - GL45Texture* targetTexture = nullptr; - { - auto& textures = texturesByMipCounts[mipLevel]; - assert(!textures.empty()); - targetTexture = *textures.begin(); - } - lock.unlock(); - targetTexture->derez(); -} diff --git a/libraries/gpu-gl/src/gpu/gl45/GL45BackendVariableTexture.cpp b/libraries/gpu-gl/src/gpu/gl45/GL45BackendVariableTexture.cpp new file mode 100644 index 0000000000..a614d62221 --- /dev/null +++ b/libraries/gpu-gl/src/gpu/gl45/GL45BackendVariableTexture.cpp @@ -0,0 +1,1035 @@ +// +// GL45BackendTexture.cpp +// libraries/gpu/src/gpu +// +// Created by Sam Gateau on 1/19/2015. +// Copyright 2014 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include "GL45Backend.h" +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include "../gl/GLTexelFormat.h" + +using namespace gpu; +using namespace gpu::gl; +using namespace gpu::gl45; + +// Variable sized textures +using GL45VariableAllocationTexture = GL45Backend::GL45VariableAllocationTexture; +using MemoryPressureState = GL45VariableAllocationTexture::MemoryPressureState; +using WorkQueue = GL45VariableAllocationTexture::WorkQueue; + +std::list GL45VariableAllocationTexture::_memoryManagedTextures; +MemoryPressureState GL45VariableAllocationTexture::_memoryPressureState = MemoryPressureState::Idle; +std::atomic GL45VariableAllocationTexture::_memoryPressureStateStale { false }; +const uvec3 GL45VariableAllocationTexture::INITIAL_MIP_TRANSFER_DIMENSIONS { 64, 64, 1 }; +WorkQueue GL45VariableAllocationTexture::_transferQueue; +WorkQueue GL45VariableAllocationTexture::_promoteQueue; +WorkQueue GL45VariableAllocationTexture::_demoteQueue; +TexturePointer GL45VariableAllocationTexture::_currentTransferTexture; + +#define OVERSUBSCRIBED_PRESSURE_VALUE 0.95f +#define UNDERSUBSCRIBED_PRESSURE_VALUE 0.85f +#define DEFAULT_ALLOWED_TEXTURE_MEMORY_MB ((size_t)1024) + +static const size_t DEFAULT_ALLOWED_TEXTURE_MEMORY = MB_TO_BYTES(DEFAULT_ALLOWED_TEXTURE_MEMORY_MB); + +using TransferJob = GL45VariableAllocationTexture::TransferJob; + +static const uvec3 MAX_TRANSFER_DIMENSIONS { 1024, 1024, 1 }; +static const size_t MAX_TRANSFER_SIZE = MAX_TRANSFER_DIMENSIONS.x * MAX_TRANSFER_DIMENSIONS.y * 4; + +#if THREADED_TEXTURE_BUFFERING +std::shared_ptr TransferJob::_bufferThread { nullptr }; +std::atomic TransferJob::_shutdownBufferingThread { false }; +Mutex TransferJob::_mutex; +TransferJob::VoidLambdaQueue TransferJob::_bufferLambdaQueue; + +void TransferJob::startTransferLoop() { + if (_bufferThread) { + return; + } + _shutdownBufferingThread = false; + _bufferThread = std::make_shared([] { + TransferJob::bufferLoop(); + }); +} + +void TransferJob::stopTransferLoop() { + if (!_bufferThread) { + return; + } + _shutdownBufferingThread = true; + _bufferThread->join(); + _bufferThread.reset(); + _shutdownBufferingThread = false; +} +#endif + +TransferJob::TransferJob(const GL45VariableAllocationTexture& parent, uint16_t sourceMip, uint16_t targetMip, uint8_t face, uint32_t lines, uint32_t lineOffset) + : _parent(parent) { + + auto transferDimensions = _parent._gpuObject.evalMipDimensions(sourceMip); + GLenum format; + GLenum type; + GLTexelFormat texelFormat = GLTexelFormat::evalGLTexelFormat(_parent._gpuObject.getTexelFormat(), _parent._gpuObject.getStoredMipFormat()); + format = texelFormat.format; + type = texelFormat.type; + auto mipSize = _parent._gpuObject.getStoredMipFaceSize(sourceMip, face); + + + if (0 == lines) { + _transferSize = mipSize; + _bufferingLambda = [=] { + auto mipData = _parent._gpuObject.accessStoredMipFace(sourceMip, face); + _buffer.resize(_transferSize); + memcpy(&_buffer[0], mipData->readData(), _transferSize); + _bufferingCompleted = true; + }; + + } else { + transferDimensions.y = lines; + auto dimensions = _parent._gpuObject.evalMipDimensions(sourceMip); + auto bytesPerLine = (uint32_t)mipSize / dimensions.y; + auto sourceOffset = bytesPerLine * lineOffset; + _transferSize = bytesPerLine * lines; + _bufferingLambda = [=] { + auto mipData = _parent._gpuObject.accessStoredMipFace(sourceMip, face); + _buffer.resize(_transferSize); + memcpy(&_buffer[0], mipData->readData() + sourceOffset, _transferSize); + _bufferingCompleted = true; + }; + } + + Backend::updateTextureTransferPendingSize(0, _transferSize); + + _transferLambda = [=] { + _parent.copyMipFaceLinesFromTexture(targetMip, face, transferDimensions, lineOffset, format, type, _buffer.data()); + std::vector emptyVector; + _buffer.swap(emptyVector); + }; +} + +TransferJob::TransferJob(const GL45VariableAllocationTexture& parent, std::function transferLambda) + : _parent(parent), _bufferingCompleted(true), _transferLambda(transferLambda) { +} + +TransferJob::~TransferJob() { + Backend::updateTextureTransferPendingSize(_transferSize, 0); +} + + +bool TransferJob::tryTransfer() { + // Disable threaded texture transfer for now +#if THREADED_TEXTURE_BUFFERING + // Are we ready to transfer + if (_bufferingCompleted) { + _transferLambda(); + return true; + } + + startBuffering(); + return false; +#else + if (!_bufferingCompleted) { + _bufferingLambda(); + _bufferingCompleted = true; + } + _transferLambda(); + return true; +#endif +} + +#if THREADED_TEXTURE_BUFFERING + +void TransferJob::startBuffering() { + if (_bufferingStarted) { + return; + } + _bufferingStarted = true; + { + Lock lock(_mutex); + _bufferLambdaQueue.push(_bufferingLambda); + } +} + +void TransferJob::bufferLoop() { + while (!_shutdownBufferingThread) { + VoidLambdaQueue workingQueue; + { + Lock lock(_mutex); + _bufferLambdaQueue.swap(workingQueue); + } + + if (workingQueue.empty()) { + QThread::msleep(5); + continue; + } + + while (!workingQueue.empty()) { + workingQueue.front()(); + workingQueue.pop(); + } + } +} +#endif + + +void GL45VariableAllocationTexture::addMemoryManagedTexture(const TexturePointer& texturePointer) { + _memoryManagedTextures.push_back(texturePointer); + addToWorkQueue(texturePointer); +} + +void GL45VariableAllocationTexture::addToWorkQueue(const TexturePointer& texturePointer) { + GL45VariableAllocationTexture* object = Backend::getGPUObject(*texturePointer); + switch (_memoryPressureState) { + case MemoryPressureState::Oversubscribed: + if (object->canDemote()) { + // Demote largest first + _demoteQueue.push({ texturePointer, (float)object->size() }); + } + break; + + case MemoryPressureState::Undersubscribed: + if (object->canPromote()) { + // Promote smallest first + _promoteQueue.push({ texturePointer, 1.0f / (float)object->size() }); + } + break; + + case MemoryPressureState::Transfer: + if (object->hasPendingTransfers()) { + // Transfer priority given to smaller mips first + _transferQueue.push({ texturePointer, 1.0f / (float)object->_gpuObject.evalMipSize(object->_populatedMip) }); + } + break; + + case MemoryPressureState::Idle: + break; + + default: + Q_UNREACHABLE(); + } +} + +WorkQueue& GL45VariableAllocationTexture::getActiveWorkQueue() { + static WorkQueue empty; + switch (_memoryPressureState) { + case MemoryPressureState::Oversubscribed: + return _demoteQueue; + + case MemoryPressureState::Undersubscribed: + return _promoteQueue; + + case MemoryPressureState::Transfer: + return _transferQueue; + + default: + break; + } + Q_UNREACHABLE(); + return empty; +} + +// FIXME hack for stats display +QString getTextureMemoryPressureModeString() { + switch (GL45VariableAllocationTexture::_memoryPressureState) { + case MemoryPressureState::Oversubscribed: + return "Oversubscribed"; + + case MemoryPressureState::Undersubscribed: + return "Undersubscribed"; + + case MemoryPressureState::Transfer: + return "Transfer"; + + case MemoryPressureState::Idle: + return "Idle"; + } + Q_UNREACHABLE(); + return "Unknown"; +} + +void GL45VariableAllocationTexture::updateMemoryPressure() { + static size_t lastAllowedMemoryAllocation = gpu::Texture::getAllowedGPUMemoryUsage(); + + size_t allowedMemoryAllocation = gpu::Texture::getAllowedGPUMemoryUsage(); + if (0 == allowedMemoryAllocation) { + allowedMemoryAllocation = DEFAULT_ALLOWED_TEXTURE_MEMORY; + } + + // If the user explicitly changed the allowed memory usage, we need to mark ourselves stale + // so that we react + if (allowedMemoryAllocation != lastAllowedMemoryAllocation) { + _memoryPressureStateStale = true; + lastAllowedMemoryAllocation = allowedMemoryAllocation; + } + + if (!_memoryPressureStateStale.exchange(false)) { + return; + } + + PROFILE_RANGE(render_gpu_gl, __FUNCTION__); + + // Clear any defunct textures (weak pointers that no longer have a valid texture) + _memoryManagedTextures.remove_if([&](const TextureWeakPointer& weakPointer) { + return weakPointer.expired(); + }); + + // Convert weak pointers to strong. This new list may still contain nulls if a texture was + // deleted on another thread between the previous line and this one + std::vector strongTextures; { + strongTextures.reserve(_memoryManagedTextures.size()); + std::transform( + _memoryManagedTextures.begin(), _memoryManagedTextures.end(), + std::back_inserter(strongTextures), + [](const TextureWeakPointer& p) { return p.lock(); }); + } + + size_t totalVariableMemoryAllocation = 0; + size_t idealMemoryAllocation = 0; + bool canDemote = false; + bool canPromote = false; + bool hasTransfers = false; + for (const auto& texture : strongTextures) { + // Race conditions can still leave nulls in the list, so we need to check + if (!texture) { + continue; + } + GL45VariableAllocationTexture* object = Backend::getGPUObject(*texture); + // Track how much the texture thinks it should be using + idealMemoryAllocation += texture->evalTotalSize(); + // Track how much we're actually using + totalVariableMemoryAllocation += object->size(); + canDemote |= object->canDemote(); + canPromote |= object->canPromote(); + hasTransfers |= object->hasPendingTransfers(); + } + + size_t unallocated = idealMemoryAllocation - totalVariableMemoryAllocation; + float pressure = (float)totalVariableMemoryAllocation / (float)allowedMemoryAllocation; + + auto newState = MemoryPressureState::Idle; + if (pressure > OVERSUBSCRIBED_PRESSURE_VALUE && canDemote) { + newState = MemoryPressureState::Oversubscribed; + } else if (pressure < UNDERSUBSCRIBED_PRESSURE_VALUE && unallocated != 0 && canPromote) { + newState = MemoryPressureState::Undersubscribed; + } else if (hasTransfers) { + newState = MemoryPressureState::Transfer; + } + + if (newState != _memoryPressureState) { +#if THREADED_TEXTURE_BUFFERING + if (MemoryPressureState::Transfer == _memoryPressureState) { + TransferJob::stopTransferLoop(); + } + _memoryPressureState = newState; + if (MemoryPressureState::Transfer == _memoryPressureState) { + TransferJob::startTransferLoop(); + } +#else + _memoryPressureState = newState; +#endif + // Clear the existing queue + _transferQueue = WorkQueue(); + _promoteQueue = WorkQueue(); + _demoteQueue = WorkQueue(); + + // Populate the existing textures into the queue + for (const auto& texture : strongTextures) { + addToWorkQueue(texture); + } + } +} + +void GL45VariableAllocationTexture::processWorkQueues() { + if (MemoryPressureState::Idle == _memoryPressureState) { + return; + } + + auto& workQueue = getActiveWorkQueue(); + PROFILE_RANGE(render_gpu_gl, __FUNCTION__); + while (!workQueue.empty()) { + auto workTarget = workQueue.top(); + workQueue.pop(); + auto texture = workTarget.first.lock(); + if (!texture) { + continue; + } + + // Grab the first item off the demote queue + GL45VariableAllocationTexture* object = Backend::getGPUObject(*texture); + if (MemoryPressureState::Oversubscribed == _memoryPressureState) { + if (!object->canDemote()) { + continue; + } + object->demote(); + } else if (MemoryPressureState::Undersubscribed == _memoryPressureState) { + if (!object->canPromote()) { + continue; + } + object->promote(); + } else if (MemoryPressureState::Transfer == _memoryPressureState) { + if (!object->hasPendingTransfers()) { + continue; + } + object->executeNextTransfer(texture); + } else { + Q_UNREACHABLE(); + } + + // Reinject into the queue if more work to be done + addToWorkQueue(texture); + break; + } + + if (workQueue.empty()) { + _memoryPressureStateStale = true; + } +} + +void GL45VariableAllocationTexture::manageMemory() { + PROFILE_RANGE(render_gpu_gl, __FUNCTION__); + updateMemoryPressure(); + processWorkQueues(); +} + +size_t GL45VariableAllocationTexture::_frameTexturesCreated { 0 }; + +GL45VariableAllocationTexture::GL45VariableAllocationTexture(const std::weak_ptr& backend, const Texture& texture) : GL45Texture(backend, texture) { + ++_frameTexturesCreated; +} + +GL45VariableAllocationTexture::~GL45VariableAllocationTexture() { + _memoryPressureStateStale = true; + Backend::updateTextureGPUMemoryUsage(_size, 0); +} + +void GL45VariableAllocationTexture::executeNextTransfer(const TexturePointer& currentTexture) { + if (_populatedMip <= _allocatedMip) { + return; + } + + if (_pendingTransfers.empty()) { + populateTransferQueue(); + } + + if (!_pendingTransfers.empty()) { + // Keeping hold of a strong pointer during the transfer ensures that the transfer thread cannot try to access a destroyed texture + _currentTransferTexture = currentTexture; + if (_pendingTransfers.front()->tryTransfer()) { + _pendingTransfers.pop(); + _currentTransferTexture.reset(); + } + } +} + +// Managed size resource textures +using GL45ResourceTexture = GL45Backend::GL45ResourceTexture; + +GL45ResourceTexture::GL45ResourceTexture(const std::weak_ptr& backend, const Texture& texture) : GL45VariableAllocationTexture(backend, texture) { + auto mipLevels = texture.getNumMips(); + _allocatedMip = mipLevels; + uvec3 mipDimensions; + for (uint16_t mip = 0; mip < mipLevels; ++mip) { + if (glm::all(glm::lessThanEqual(texture.evalMipDimensions(mip), INITIAL_MIP_TRANSFER_DIMENSIONS))) { + _maxAllocatedMip = _populatedMip = mip; + break; + } + } + + uint16_t allocatedMip = _populatedMip - std::min(_populatedMip, 2); + allocateStorage(allocatedMip); + _memoryPressureStateStale = true; + copyMipsFromTexture(); + syncSampler(); + +} + +void GL45ResourceTexture::allocateStorage(uint16 allocatedMip) { + _allocatedMip = allocatedMip; + const GLTexelFormat texelFormat = GLTexelFormat::evalGLTexelFormat(_gpuObject.getTexelFormat()); + const auto dimensions = _gpuObject.evalMipDimensions(_allocatedMip); + const auto totalMips = _gpuObject.getNumMips(); + const auto mips = totalMips - _allocatedMip; + glTextureStorage2D(_id, mips, texelFormat.internalFormat, dimensions.x, dimensions.y); + auto mipLevels = _gpuObject.getNumMips(); + _size = 0; + for (uint16_t mip = _allocatedMip; mip < mipLevels; ++mip) { + _size += _gpuObject.evalMipSize(mip); + } + Backend::updateTextureGPUMemoryUsage(0, _size); + +} + +void GL45ResourceTexture::copyMipsFromTexture() { + auto mipLevels = _gpuObject.getNumMips(); + size_t maxFace = GLTexture::getFaceCount(_target); + for (uint16_t sourceMip = _populatedMip; sourceMip < mipLevels; ++sourceMip) { + uint16_t targetMip = sourceMip - _allocatedMip; + for (uint8_t face = 0; face < maxFace; ++face) { + copyMipFaceFromTexture(sourceMip, targetMip, face); + } + } +} + +void GL45ResourceTexture::syncSampler() const { + Parent::syncSampler(); + glTextureParameteri(_id, GL_TEXTURE_BASE_LEVEL, _populatedMip - _allocatedMip); +} + +void GL45ResourceTexture::promote() { + PROFILE_RANGE(render_gpu_gl, __FUNCTION__); + Q_ASSERT(_allocatedMip > 0); + GLuint oldId = _id; + auto oldSize = _size; + // create new texture + const_cast(_id) = allocate(_gpuObject); + uint16_t oldAllocatedMip = _allocatedMip; + // allocate storage for new level + allocateStorage(_allocatedMip - std::min(_allocatedMip, 2)); + uint16_t mips = _gpuObject.getNumMips(); + // copy pre-existing mips + for (uint16_t mip = _populatedMip; mip < mips; ++mip) { + auto mipDimensions = _gpuObject.evalMipDimensions(mip); + uint16_t targetMip = mip - _allocatedMip; + uint16_t sourceMip = mip - oldAllocatedMip; + auto faces = getFaceCount(_target); + for (uint8_t face = 0; face < faces; ++face) { + glCopyImageSubData( + oldId, _target, sourceMip, 0, 0, face, + _id, _target, targetMip, 0, 0, face, + mipDimensions.x, mipDimensions.y, 1 + ); + (void)CHECK_GL_ERROR(); + } + } + // destroy the old texture + glDeleteTextures(1, &oldId); + // update the memory usage + Backend::updateTextureGPUMemoryUsage(oldSize, 0); + _memoryPressureStateStale = true; + syncSampler(); + populateTransferQueue(); +} + +void GL45ResourceTexture::demote() { + PROFILE_RANGE(render_gpu_gl, __FUNCTION__); + Q_ASSERT(_allocatedMip < _maxAllocatedMip); + auto oldId = _id; + auto oldSize = _size; + const_cast(_id) = allocate(_gpuObject); + allocateStorage(_allocatedMip + 1); + _populatedMip = std::max(_populatedMip, _allocatedMip); + uint16_t mips = _gpuObject.getNumMips(); + // copy pre-existing mips + for (uint16_t mip = _populatedMip; mip < mips; ++mip) { + auto mipDimensions = _gpuObject.evalMipDimensions(mip); + uint16_t targetMip = mip - _allocatedMip; + uint16_t sourceMip = targetMip + 1; + auto faces = getFaceCount(_target); + for (uint8_t face = 0; face < faces; ++face) { + glCopyImageSubData( + oldId, _target, sourceMip, 0, 0, face, + _id, _target, targetMip, 0, 0, face, + mipDimensions.x, mipDimensions.y, 1 + ); + (void)CHECK_GL_ERROR(); + } + } + // destroy the old texture + glDeleteTextures(1, &oldId); + // update the memory usage + Backend::updateTextureGPUMemoryUsage(oldSize, 0); + _memoryPressureStateStale = true; + syncSampler(); + populateTransferQueue(); +} + + +void GL45ResourceTexture::populateTransferQueue() { + PROFILE_RANGE(render_gpu_gl, __FUNCTION__); + if (_populatedMip <= _allocatedMip) { + return; + } + _pendingTransfers = TransferQueue(); + + const uint8_t maxFace = GLTexture::getFaceCount(_target); + uint16_t sourceMip = _populatedMip; + do { + --sourceMip; + auto targetMip = sourceMip - _allocatedMip; + auto mipDimensions = _gpuObject.evalMipDimensions(sourceMip); + for (uint8_t face = 0; face < maxFace; ++face) { + if (!_gpuObject.isStoredMipFaceAvailable(sourceMip, face)) { + continue; + } + + // If the mip is less than the max transfer size, then just do it in one transfer + if (glm::all(glm::lessThanEqual(mipDimensions, MAX_TRANSFER_DIMENSIONS))) { + // Can the mip be transferred in one go + _pendingTransfers.emplace(new TransferJob(*this, sourceMip, targetMip, face)); + continue; + } + + // break down the transfers into chunks so that no single transfer is + // consuming more than X bandwidth + auto mipSize = _gpuObject.getStoredMipFaceSize(sourceMip, face); + const auto lines = mipDimensions.y; + auto bytesPerLine = mipSize / lines; + Q_ASSERT(0 == (mipSize % lines)); + uint32_t linesPerTransfer = (uint32_t)(MAX_TRANSFER_SIZE / bytesPerLine); + uint32_t lineOffset = 0; + while (lineOffset < lines) { + uint32_t linesToCopy = std::min(lines - lineOffset, linesPerTransfer); + _pendingTransfers.emplace(new TransferJob(*this, sourceMip, targetMip, face, linesToCopy, lineOffset)); + lineOffset += linesToCopy; + } + } + + // queue up the sampler and populated mip change for after the transfer has completed + _pendingTransfers.emplace(new TransferJob(*this, [=] { + _populatedMip = sourceMip; + syncSampler(); + })); + } while (sourceMip != _allocatedMip); +} + +// Sparsely allocated, managed size resource textures +#if 0 +#define SPARSE_PAGE_SIZE_OVERHEAD_ESTIMATE 1.3f + +using GL45SparseResourceTexture = GL45Backend::GL45SparseResourceTexture; + +GL45Texture::PageDimensionsMap GL45Texture::pageDimensionsByFormat; +Mutex GL45Texture::pageDimensionsMutex; + +GL45Texture::PageDimensions GL45Texture::getPageDimensionsForFormat(const TextureTypeFormat& typeFormat) { + { + Lock lock(pageDimensionsMutex); + if (pageDimensionsByFormat.count(typeFormat)) { + return pageDimensionsByFormat[typeFormat]; + } + } + + GLint count = 0; + glGetInternalformativ(typeFormat.first, typeFormat.second, GL_NUM_VIRTUAL_PAGE_SIZES_ARB, 1, &count); + + std::vector result; + if (count > 0) { + std::vector x, y, z; + x.resize(count); + glGetInternalformativ(typeFormat.first, typeFormat.second, GL_VIRTUAL_PAGE_SIZE_X_ARB, 1, &x[0]); + y.resize(count); + glGetInternalformativ(typeFormat.first, typeFormat.second, GL_VIRTUAL_PAGE_SIZE_Y_ARB, 1, &y[0]); + z.resize(count); + glGetInternalformativ(typeFormat.first, typeFormat.second, GL_VIRTUAL_PAGE_SIZE_Z_ARB, 1, &z[0]); + + result.resize(count); + for (GLint i = 0; i < count; ++i) { + result[i] = uvec3(x[i], y[i], z[i]); + } + } + + { + Lock lock(pageDimensionsMutex); + if (0 == pageDimensionsByFormat.count(typeFormat)) { + pageDimensionsByFormat[typeFormat] = result; + } + } + + return result; +} + +GL45Texture::PageDimensions GL45Texture::getPageDimensionsForFormat(GLenum target, GLenum format) { + return getPageDimensionsForFormat({ target, format }); +} +bool GL45Texture::isSparseEligible(const Texture& texture) { + Q_ASSERT(TextureUsageType::RESOURCE == texture.getUsageType()); + + // Disabling sparse for the momemnt + return false; + + const auto allowedPageDimensions = getPageDimensionsForFormat(getGLTextureType(texture), + gl::GLTexelFormat::evalGLTexelFormatInternal(texture.getTexelFormat())); + const auto textureDimensions = texture.getDimensions(); + for (const auto& pageDimensions : allowedPageDimensions) { + if (uvec3(0) == (textureDimensions % pageDimensions)) { + return true; + } + } + + return false; +} + + +GL45SparseResourceTexture::GL45SparseResourceTexture(const std::weak_ptr& backend, const Texture& texture) : GL45VariableAllocationTexture(backend, texture) { + const GLTexelFormat texelFormat = GLTexelFormat::evalGLTexelFormat(_gpuObject.getTexelFormat()); + const uvec3 dimensions = _gpuObject.getDimensions(); + auto allowedPageDimensions = getPageDimensionsForFormat(_target, texelFormat.internalFormat); + uint32_t pageDimensionsIndex = 0; + // In order to enable sparse the texture size must be an integer multiple of the page size + for (size_t i = 0; i < allowedPageDimensions.size(); ++i) { + pageDimensionsIndex = (uint32_t)i; + _pageDimensions = allowedPageDimensions[i]; + // Is this texture an integer multiple of page dimensions? + if (uvec3(0) == (dimensions % _pageDimensions)) { + qCDebug(gpugl45logging) << "Enabling sparse for texture " << _gpuObject.source().c_str(); + break; + } + } + glTextureParameteri(_id, GL_TEXTURE_SPARSE_ARB, GL_TRUE); + glTextureParameteri(_id, GL_VIRTUAL_PAGE_SIZE_INDEX_ARB, pageDimensionsIndex); + glGetTextureParameterIuiv(_id, GL_NUM_SPARSE_LEVELS_ARB, &_maxSparseLevel); + + _pageBytes = _gpuObject.getTexelFormat().getSize(); + _pageBytes *= _pageDimensions.x * _pageDimensions.y * _pageDimensions.z; + // Testing with a simple texture allocating app shows an estimated 20% GPU memory overhead for + // sparse textures as compared to non-sparse, so we acount for that here. + _pageBytes = (uint32_t)(_pageBytes * SPARSE_PAGE_SIZE_OVERHEAD_ESTIMATE); + + //allocateStorage(); + syncSampler(); +} + +GL45SparseResourceTexture::~GL45SparseResourceTexture() { + Backend::updateTextureGPUVirtualMemoryUsage(size(), 0); +} + +uvec3 GL45SparseResourceTexture::getPageCounts(const uvec3& dimensions) const { + auto result = (dimensions / _pageDimensions) + + glm::clamp(dimensions % _pageDimensions, glm::uvec3(0), glm::uvec3(1)); + return result; +} + +uint32_t GL45SparseResourceTexture::getPageCount(const uvec3& dimensions) const { + auto pageCounts = getPageCounts(dimensions); + return pageCounts.x * pageCounts.y * pageCounts.z; +} + +void GL45SparseResourceTexture::promote() { +} + +void GL45SparseResourceTexture::demote() { +} + +SparseInfo::SparseInfo(GL45Texture& texture) + : texture(texture) { +} + +void SparseInfo::maybeMakeSparse() { + // Don't enable sparse for objects with explicitly managed mip levels + if (!texture._gpuObject.isAutogenerateMips()) { + return; + } + + const uvec3 dimensions = texture._gpuObject.getDimensions(); + auto allowedPageDimensions = getPageDimensionsForFormat(texture._target, texture._internalFormat); + // In order to enable sparse the texture size must be an integer multiple of the page size + for (size_t i = 0; i < allowedPageDimensions.size(); ++i) { + pageDimensionsIndex = (uint32_t)i; + pageDimensions = allowedPageDimensions[i]; + // Is this texture an integer multiple of page dimensions? + if (uvec3(0) == (dimensions % pageDimensions)) { + qCDebug(gpugl45logging) << "Enabling sparse for texture " << texture._source.c_str(); + sparse = true; + break; + } + } + + if (sparse) { + glTextureParameteri(texture._id, GL_TEXTURE_SPARSE_ARB, GL_TRUE); + glTextureParameteri(texture._id, GL_VIRTUAL_PAGE_SIZE_INDEX_ARB, pageDimensionsIndex); + } else { + qCDebug(gpugl45logging) << "Size " << dimensions.x << " x " << dimensions.y << + " is not supported by any sparse page size for texture" << texture._source.c_str(); + } +} + + +// This can only be called after we've established our storage size +void SparseInfo::update() { + if (!sparse) { + return; + } + glGetTextureParameterIuiv(texture._id, GL_NUM_SPARSE_LEVELS_ARB, &maxSparseLevel); + + for (uint16_t mipLevel = 0; mipLevel <= maxSparseLevel; ++mipLevel) { + auto mipDimensions = texture._gpuObject.evalMipDimensions(mipLevel); + auto mipPageCount = getPageCount(mipDimensions); + maxPages += mipPageCount; + } + if (texture._target == GL_TEXTURE_CUBE_MAP) { + maxPages *= GLTexture::CUBE_NUM_FACES; + } +} + + +void SparseInfo::allocateToMip(uint16_t targetMip) { + // Not sparse, do nothing + if (!sparse) { + return; + } + + if (allocatedMip == INVALID_MIP) { + allocatedMip = maxSparseLevel + 1; + } + + // Don't try to allocate below the maximum sparse level + if (targetMip > maxSparseLevel) { + targetMip = maxSparseLevel; + } + + // Already allocated this level + if (allocatedMip <= targetMip) { + return; + } + + uint32_t maxFace = (uint32_t)(GL_TEXTURE_CUBE_MAP == texture._target ? CUBE_NUM_FACES : 1); + for (uint16_t mip = targetMip; mip < allocatedMip; ++mip) { + auto size = texture._gpuObject.evalMipDimensions(mip); + glTexturePageCommitmentEXT(texture._id, mip, 0, 0, 0, size.x, size.y, maxFace, GL_TRUE); + allocatedPages += getPageCount(size); + } + allocatedMip = targetMip; +} + +uint32_t SparseInfo::getSize() const { + return allocatedPages * pageBytes; +} +using SparseInfo = GL45Backend::GL45Texture::SparseInfo; + +void GL45Texture::updateSize() const { + if (_gpuObject.getTexelFormat().isCompressed()) { + qFatal("Compressed textures not yet supported"); + } + + if (_transferrable && _sparseInfo.sparse) { + auto size = _sparseInfo.getSize(); + Backend::updateTextureGPUSparseMemoryUsage(_size, size); + setSize(size); + } else { + setSize(_gpuObject.evalTotalSize(_mipOffset)); + } +} + +void GL45Texture::startTransfer() { + Parent::startTransfer(); + _sparseInfo.update(); + _populatedMip = _maxMip + 1; +} + +bool GL45Texture::continueTransfer() { + size_t maxFace = GL_TEXTURE_CUBE_MAP == _target ? CUBE_NUM_FACES : 1; + if (_populatedMip == _minMip) { + return false; + } + + uint16_t targetMip = _populatedMip - 1; + while (targetMip > 0 && !_gpuObject.isStoredMipFaceAvailable(targetMip)) { + --targetMip; + } + + _sparseInfo.allocateToMip(targetMip); + for (uint8_t face = 0; face < maxFace; ++face) { + auto size = _gpuObject.evalMipDimensions(targetMip); + if (_gpuObject.isStoredMipFaceAvailable(targetMip, face)) { + auto mip = _gpuObject.accessStoredMipFace(targetMip, face); + GLTexelFormat texelFormat = GLTexelFormat::evalGLTexelFormat(_gpuObject.getTexelFormat(), mip->getFormat()); + if (GL_TEXTURE_2D == _target) { + glTextureSubImage2D(_id, targetMip, 0, 0, size.x, size.y, texelFormat.format, texelFormat.type, mip->readData()); + } else if (GL_TEXTURE_CUBE_MAP == _target) { + // DSA ARB does not work on AMD, so use EXT + // unless EXT is not available on the driver + if (glTextureSubImage2DEXT) { + auto target = CUBE_FACE_LAYOUT[face]; + glTextureSubImage2DEXT(_id, target, targetMip, 0, 0, size.x, size.y, texelFormat.format, texelFormat.type, mip->readData()); + } else { + glTextureSubImage3D(_id, targetMip, 0, 0, face, size.x, size.y, 1, texelFormat.format, texelFormat.type, mip->readData()); + } + } else { + Q_ASSERT(false); + } + (void)CHECK_GL_ERROR(); + break; + } + } + _populatedMip = targetMip; + return _populatedMip != _minMip; +} + +void GL45Texture::finishTransfer() { + Parent::finishTransfer(); +} + +void GL45Texture::postTransfer() { + Parent::postTransfer(); +} + +void GL45Texture::stripToMip(uint16_t newMinMip) { + if (newMinMip < _minMip) { + qCWarning(gpugl45logging) << "Cannot decrease the min mip"; + return; + } + + if (_sparseInfo.sparse && newMinMip > _sparseInfo.maxSparseLevel) { + qCWarning(gpugl45logging) << "Cannot increase the min mip into the mip tail"; + return; + } + + // If we weren't generating mips before, we need to now that we're stripping down mip levels. + if (!_gpuObject.isAutogenerateMips()) { + qCDebug(gpugl45logging) << "Force mip generation for texture"; + glGenerateTextureMipmap(_id); + } + + + uint8_t maxFace = (uint8_t)((_target == GL_TEXTURE_CUBE_MAP) ? GLTexture::CUBE_NUM_FACES : 1); + if (_sparseInfo.sparse) { + for (uint16_t mip = _minMip; mip < newMinMip; ++mip) { + auto id = _id; + auto mipDimensions = _gpuObject.evalMipDimensions(mip); + glTexturePageCommitmentEXT(id, mip, 0, 0, 0, mipDimensions.x, mipDimensions.y, maxFace, GL_FALSE); + auto deallocatedPages = _sparseInfo.getPageCount(mipDimensions) * maxFace; + assert(deallocatedPages < _sparseInfo.allocatedPages); + _sparseInfo.allocatedPages -= deallocatedPages; + } + _minMip = newMinMip; + } else { + GLuint oldId = _id; + // Find the distance between the old min mip and the new one + uint16 mipDelta = newMinMip - _minMip; + _mipOffset += mipDelta; + const_cast(_maxMip) -= mipDelta; + auto newLevels = usedMipLevels(); + + // Create and setup the new texture (allocate) + glCreateTextures(_target, 1, &const_cast(_id)); + glTextureParameteri(_id, GL_TEXTURE_BASE_LEVEL, 0); + glTextureParameteri(_id, GL_TEXTURE_MAX_LEVEL, _maxMip - _minMip); + Vec3u newDimensions = _gpuObject.evalMipDimensions(_mipOffset); + glTextureStorage2D(_id, newLevels, _internalFormat, newDimensions.x, newDimensions.y); + + // Copy the contents of the old texture to the new + GLuint fbo { 0 }; + glCreateFramebuffers(1, &fbo); + glBindFramebuffer(GL_READ_FRAMEBUFFER, fbo); + for (uint16 targetMip = _minMip; targetMip <= _maxMip; ++targetMip) { + uint16 sourceMip = targetMip + mipDelta; + Vec3u mipDimensions = _gpuObject.evalMipDimensions(targetMip + _mipOffset); + for (GLenum target : getFaceTargets(_target)) { + glFramebufferTexture2D(GL_READ_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, target, oldId, sourceMip); + (void)CHECK_GL_ERROR(); + glCopyTextureSubImage2D(_id, targetMip, 0, 0, 0, 0, mipDimensions.x, mipDimensions.y); + (void)CHECK_GL_ERROR(); + } + } + glBindFramebuffer(GL_READ_FRAMEBUFFER, 0); + glDeleteFramebuffers(1, &fbo); + glDeleteTextures(1, &oldId); + } + + // Re-sync the sampler to force access to the new mip level + syncSampler(); + updateSize(); +} + +bool GL45Texture::derezable() const { + if (_external) { + return false; + } + auto maxMinMip = _sparseInfo.sparse ? _sparseInfo.maxSparseLevel : _maxMip; + return _transferrable && (_targetMinMip < maxMinMip); +} + +size_t GL45Texture::getMipByteCount(uint16_t mip) const { + if (!_sparseInfo.sparse) { + return Parent::getMipByteCount(mip); + } + + auto dimensions = _gpuObject.evalMipDimensions(_targetMinMip); + return _sparseInfo.getPageCount(dimensions) * _sparseInfo.pageBytes; +} + +std::pair GL45Texture::preDerez() { + assert(!_sparseInfo.sparse || _targetMinMip < _sparseInfo.maxSparseLevel); + size_t freedMemory = getMipByteCount(_targetMinMip); + bool liveMip = _populatedMip != INVALID_MIP && _populatedMip <= _targetMinMip; + ++_targetMinMip; + return { freedMemory, liveMip }; +} + +void GL45Texture::derez() { + if (_sparseInfo.sparse) { + assert(_minMip < _sparseInfo.maxSparseLevel); + } + assert(_minMip < _maxMip); + assert(_transferrable); + stripToMip(_minMip + 1); +} + +size_t GL45Texture::getCurrentGpuSize() const { + if (!_sparseInfo.sparse) { + return Parent::getCurrentGpuSize(); + } + + return _sparseInfo.getSize(); +} + +size_t GL45Texture::getTargetGpuSize() const { + if (!_sparseInfo.sparse) { + return Parent::getTargetGpuSize(); + } + + size_t result = 0; + for (auto mip = _targetMinMip; mip <= _sparseInfo.maxSparseLevel; ++mip) { + result += (_sparseInfo.pageBytes * _sparseInfo.getPageCount(_gpuObject.evalMipDimensions(mip))); + } + + return result; +} + +GL45Texture::~GL45Texture() { + if (_sparseInfo.sparse) { + uint8_t maxFace = (uint8_t)((_target == GL_TEXTURE_CUBE_MAP) ? GLTexture::CUBE_NUM_FACES : 1); + auto maxSparseMip = std::min(_maxMip, _sparseInfo.maxSparseLevel); + for (uint16_t mipLevel = _minMip; mipLevel <= maxSparseMip; ++mipLevel) { + auto mipDimensions = _gpuObject.evalMipDimensions(mipLevel); + glTexturePageCommitmentEXT(_texture, mipLevel, 0, 0, 0, mipDimensions.x, mipDimensions.y, maxFace, GL_FALSE); + auto deallocatedPages = _sparseInfo.getPageCount(mipDimensions) * maxFace; + assert(deallocatedPages <= _sparseInfo.allocatedPages); + _sparseInfo.allocatedPages -= deallocatedPages; + } + + if (0 != _sparseInfo.allocatedPages) { + qCWarning(gpugl45logging) << "Allocated pages remaining " << _id << " " << _sparseInfo.allocatedPages; + } + Backend::decrementTextureGPUSparseCount(); + } +} +GL45Texture::GL45Texture(const std::weak_ptr& backend, const Texture& texture) + : GLTexture(backend, texture, allocate(texture)), _sparseInfo(*this), _targetMinMip(_minMip) +{ + + auto theBackend = _backend.lock(); + if (_transferrable && theBackend && theBackend->isTextureManagementSparseEnabled()) { + _sparseInfo.maybeMakeSparse(); + if (_sparseInfo.sparse) { + Backend::incrementTextureGPUSparseCount(); + } + } +} +#endif diff --git a/libraries/gpu/CMakeLists.txt b/libraries/gpu/CMakeLists.txt index 384c5709ee..207431d8c7 100644 --- a/libraries/gpu/CMakeLists.txt +++ b/libraries/gpu/CMakeLists.txt @@ -1,6 +1,6 @@ set(TARGET_NAME gpu) autoscribe_shader_lib(gpu) setup_hifi_library() -link_hifi_libraries(shared) +link_hifi_libraries(shared ktx) target_nsight() diff --git a/libraries/gpu/src/gpu/Batch.cpp b/libraries/gpu/src/gpu/Batch.cpp index c15da61800..f822da129b 100644 --- a/libraries/gpu/src/gpu/Batch.cpp +++ b/libraries/gpu/src/gpu/Batch.cpp @@ -292,15 +292,8 @@ void Batch::setUniformBuffer(uint32 slot, const BufferView& view) { setUniformBuffer(slot, view._buffer, view._offset, view._size); } - void Batch::setResourceTexture(uint32 slot, const TexturePointer& texture) { - if (texture && texture->getUsage().isExternal()) { - auto recycler = texture->getExternalRecycler(); - Q_ASSERT(recycler); - } - ADD_COMMAND(setResourceTexture); - _params.emplace_back(_textures.cache(texture)); _params.emplace_back(slot); } diff --git a/libraries/gpu/src/gpu/Buffer.h b/libraries/gpu/src/gpu/Buffer.h index 2507e8e0a6..290b84bef0 100644 --- a/libraries/gpu/src/gpu/Buffer.h +++ b/libraries/gpu/src/gpu/Buffer.h @@ -198,7 +198,7 @@ public: BufferView(const BufferPointer& buffer, Size offset, Size size, const Element& element = DEFAULT_ELEMENT); BufferView(const BufferPointer& buffer, Size offset, Size size, uint16 stride, const Element& element = DEFAULT_ELEMENT); - Size getNumElements() const { return _size / _element.getSize(); } + Size getNumElements() const { return (_size - _offset) / _stride; } //Template iterator with random access on the buffer sysmem template diff --git a/libraries/gpu/src/gpu/Context.cpp b/libraries/gpu/src/gpu/Context.cpp index 78b472bdae..0030b2fa88 100644 --- a/libraries/gpu/src/gpu/Context.cpp +++ b/libraries/gpu/src/gpu/Context.cpp @@ -241,6 +241,7 @@ std::atomic Context::_bufferGPUMemoryUsage { 0 }; std::atomic Context::_textureGPUCount{ 0 }; std::atomic Context::_textureGPUSparseCount { 0 }; +std::atomic Context::_textureTransferPendingSize { 0 }; std::atomic Context::_textureGPUMemoryUsage { 0 }; std::atomic Context::_textureGPUVirtualMemoryUsage { 0 }; std::atomic Context::_textureGPUFramebufferMemoryUsage { 0 }; @@ -264,7 +265,7 @@ void Context::incrementBufferGPUCount() { auto total = ++_bufferGPUCount; if (total > max.load()) { max = total; - qCDebug(gpulogging) << "New max GPU buffers " << total; + // qCDebug(gpulogging) << "New max GPU buffers " << total; } } void Context::decrementBufferGPUCount() { @@ -298,7 +299,7 @@ void Context::incrementTextureGPUCount() { auto total = ++_textureGPUCount; if (total > max.load()) { max = total; - qCDebug(gpulogging) << "New max GPU textures " << total; + // qCDebug(gpulogging) << "New max GPU textures " << total; } } void Context::decrementTextureGPUCount() { @@ -310,13 +311,24 @@ void Context::incrementTextureGPUSparseCount() { auto total = ++_textureGPUSparseCount; if (total > max.load()) { max = total; - qCDebug(gpulogging) << "New max GPU textures " << total; + // qCDebug(gpulogging) << "New max GPU textures " << total; } } void Context::decrementTextureGPUSparseCount() { --_textureGPUSparseCount; } +void Context::updateTextureTransferPendingSize(Size prevObjectSize, Size newObjectSize) { + if (prevObjectSize == newObjectSize) { + return; + } + if (newObjectSize > prevObjectSize) { + _textureTransferPendingSize.fetch_add(newObjectSize - prevObjectSize); + } else { + _textureTransferPendingSize.fetch_sub(prevObjectSize - newObjectSize); + } +} + void Context::updateTextureGPUMemoryUsage(Size prevObjectSize, Size newObjectSize) { if (prevObjectSize == newObjectSize) { return; @@ -366,7 +378,7 @@ void Context::incrementTextureGPUTransferCount() { auto total = ++_textureGPUTransferCount; if (total > max.load()) { max = total; - qCDebug(gpulogging) << "New max GPU textures transfers" << total; + // qCDebug(gpulogging) << "New max GPU textures transfers" << total; } } @@ -390,6 +402,10 @@ uint32_t Context::getTextureGPUSparseCount() { return _textureGPUSparseCount.load(); } +Context::Size Context::getTextureTransferPendingSize() { + return _textureTransferPendingSize.load(); +} + Context::Size Context::getTextureGPUMemoryUsage() { return _textureGPUMemoryUsage.load(); } @@ -419,6 +435,7 @@ void Backend::incrementTextureGPUCount() { Context::incrementTextureGPUCount(); void Backend::decrementTextureGPUCount() { Context::decrementTextureGPUCount(); } void Backend::incrementTextureGPUSparseCount() { Context::incrementTextureGPUSparseCount(); } void Backend::decrementTextureGPUSparseCount() { Context::decrementTextureGPUSparseCount(); } +void Backend::updateTextureTransferPendingSize(Resource::Size prevObjectSize, Resource::Size newObjectSize) { Context::updateTextureTransferPendingSize(prevObjectSize, newObjectSize); } void Backend::updateTextureGPUMemoryUsage(Resource::Size prevObjectSize, Resource::Size newObjectSize) { Context::updateTextureGPUMemoryUsage(prevObjectSize, newObjectSize); } void Backend::updateTextureGPUVirtualMemoryUsage(Resource::Size prevObjectSize, Resource::Size newObjectSize) { Context::updateTextureGPUVirtualMemoryUsage(prevObjectSize, newObjectSize); } void Backend::updateTextureGPUFramebufferMemoryUsage(Resource::Size prevObjectSize, Resource::Size newObjectSize) { Context::updateTextureGPUFramebufferMemoryUsage(prevObjectSize, newObjectSize); } diff --git a/libraries/gpu/src/gpu/Context.h b/libraries/gpu/src/gpu/Context.h index 01c841992d..102c754cd7 100644 --- a/libraries/gpu/src/gpu/Context.h +++ b/libraries/gpu/src/gpu/Context.h @@ -101,6 +101,7 @@ public: static void decrementTextureGPUCount(); static void incrementTextureGPUSparseCount(); static void decrementTextureGPUSparseCount(); + static void updateTextureTransferPendingSize(Resource::Size prevObjectSize, Resource::Size newObjectSize); static void updateTextureGPUMemoryUsage(Resource::Size prevObjectSize, Resource::Size newObjectSize); static void updateTextureGPUSparseMemoryUsage(Resource::Size prevObjectSize, Resource::Size newObjectSize); static void updateTextureGPUVirtualMemoryUsage(Resource::Size prevObjectSize, Resource::Size newObjectSize); @@ -220,6 +221,7 @@ public: static uint32_t getTextureGPUSparseCount(); static Size getFreeGPUMemory(); static Size getUsedGPUMemory(); + static Size getTextureTransferPendingSize(); static Size getTextureGPUMemoryUsage(); static Size getTextureGPUVirtualMemoryUsage(); static Size getTextureGPUFramebufferMemoryUsage(); @@ -263,6 +265,7 @@ protected: static void decrementTextureGPUCount(); static void incrementTextureGPUSparseCount(); static void decrementTextureGPUSparseCount(); + static void updateTextureTransferPendingSize(Size prevObjectSize, Size newObjectSize); static void updateTextureGPUMemoryUsage(Size prevObjectSize, Size newObjectSize); static void updateTextureGPUSparseMemoryUsage(Size prevObjectSize, Size newObjectSize); static void updateTextureGPUVirtualMemoryUsage(Size prevObjectSize, Size newObjectSize); @@ -279,6 +282,7 @@ protected: static std::atomic _textureGPUCount; static std::atomic _textureGPUSparseCount; + static std::atomic _textureTransferPendingSize; static std::atomic _textureGPUMemoryUsage; static std::atomic _textureGPUSparseMemoryUsage; static std::atomic _textureGPUVirtualMemoryUsage; diff --git a/libraries/gpu/src/gpu/Format.cpp b/libraries/gpu/src/gpu/Format.cpp index 2a8185bf94..de202911e3 100644 --- a/libraries/gpu/src/gpu/Format.cpp +++ b/libraries/gpu/src/gpu/Format.cpp @@ -10,8 +10,15 @@ using namespace gpu; +const Element Element::COLOR_R_8 { SCALAR, NUINT8, RED }; +const Element Element::COLOR_SR_8 { SCALAR, NUINT8, SRED }; + const Element Element::COLOR_RGBA_32{ VEC4, NUINT8, RGBA }; const Element Element::COLOR_SRGBA_32{ VEC4, NUINT8, SRGBA }; + +const Element Element::COLOR_BGRA_32{ VEC4, NUINT8, BGRA }; +const Element Element::COLOR_SBGRA_32{ VEC4, NUINT8, SBGRA }; + const Element Element::COLOR_R11G11B10{ SCALAR, FLOAT, R11G11B10 }; const Element Element::VEC4F_COLOR_RGBA{ VEC4, FLOAT, RGBA }; const Element Element::VEC2F_UV{ VEC2, FLOAT, UV }; diff --git a/libraries/gpu/src/gpu/Format.h b/libraries/gpu/src/gpu/Format.h index 13809a41e6..4114ccb15c 100644 --- a/libraries/gpu/src/gpu/Format.h +++ b/libraries/gpu/src/gpu/Format.h @@ -75,12 +75,12 @@ static const bool TYPE_IS_INTEGER[NUM_TYPES] = { true, // Normalized values - true, - true, - true, - true, - true, - true + false, + false, + false, + false, + false, + false }; // Dimension of an Element @@ -133,6 +133,7 @@ static const int SCALAR_COUNT[NUM_DIMENSIONS] = { enum Semantic { RAW = 0, // used as RAW memory + RED, RGB, RGBA, BGRA, @@ -149,6 +150,7 @@ enum Semantic { STENCIL, // Stencil only buffer DEPTH_STENCIL, // Depth Stencil buffer + SRED, SRGB, SRGBA, SBGRA, @@ -227,8 +229,12 @@ public: return getRaw() != right.getRaw(); } + static const Element COLOR_R_8; + static const Element COLOR_SR_8; static const Element COLOR_RGBA_32; static const Element COLOR_SRGBA_32; + static const Element COLOR_BGRA_32; + static const Element COLOR_SBGRA_32; static const Element COLOR_R11G11B10; static const Element VEC4F_COLOR_RGBA; static const Element VEC2F_UV; diff --git a/libraries/gpu/src/gpu/Framebuffer.cpp b/libraries/gpu/src/gpu/Framebuffer.cpp index e8ccfce3b2..b49c681889 100755 --- a/libraries/gpu/src/gpu/Framebuffer.cpp +++ b/libraries/gpu/src/gpu/Framebuffer.cpp @@ -32,7 +32,7 @@ Framebuffer* Framebuffer::create(const std::string& name) { Framebuffer* Framebuffer::create(const std::string& name, const Format& colorBufferFormat, uint16 width, uint16 height) { auto framebuffer = Framebuffer::create(name); - auto colorTexture = TexturePointer(Texture::create2D(colorBufferFormat, width, height, Sampler(Sampler::FILTER_MIN_MAG_POINT))); + auto colorTexture = TexturePointer(Texture::createRenderBuffer(colorBufferFormat, width, height, Texture::SINGLE_MIP, Sampler(Sampler::FILTER_MIN_MAG_POINT))); colorTexture->setSource("Framebuffer::colorTexture"); framebuffer->setRenderBuffer(0, colorTexture); @@ -43,8 +43,8 @@ Framebuffer* Framebuffer::create(const std::string& name, const Format& colorBuf Framebuffer* Framebuffer::create(const std::string& name, const Format& colorBufferFormat, const Format& depthStencilBufferFormat, uint16 width, uint16 height) { auto framebuffer = Framebuffer::create(name); - auto colorTexture = TexturePointer(Texture::create2D(colorBufferFormat, width, height, Sampler(Sampler::FILTER_MIN_MAG_POINT))); - auto depthTexture = TexturePointer(Texture::create2D(depthStencilBufferFormat, width, height, Sampler(Sampler::FILTER_MIN_MAG_POINT))); + auto colorTexture = TexturePointer(Texture::createRenderBuffer(colorBufferFormat, width, height, Texture::SINGLE_MIP, Sampler(Sampler::FILTER_MIN_MAG_POINT))); + auto depthTexture = TexturePointer(Texture::createRenderBuffer(depthStencilBufferFormat, width, height, Texture::SINGLE_MIP, Sampler(Sampler::FILTER_MIN_MAG_POINT))); framebuffer->setRenderBuffer(0, colorTexture); framebuffer->setDepthStencilBuffer(depthTexture, depthStencilBufferFormat); @@ -55,7 +55,7 @@ Framebuffer* Framebuffer::createShadowmap(uint16 width) { auto framebuffer = Framebuffer::create("Shadowmap"); auto depthFormat = Element(gpu::SCALAR, gpu::FLOAT, gpu::DEPTH); // Depth32 texel format - auto depthTexture = TexturePointer(Texture::create2D(depthFormat, width, width)); + auto depthTexture = TexturePointer(Texture::createRenderBuffer(depthFormat, width, width)); Sampler::Desc samplerDesc; samplerDesc._borderColor = glm::vec4(1.0f); samplerDesc._wrapModeU = Sampler::WRAP_BORDER; @@ -143,6 +143,8 @@ int Framebuffer::setRenderBuffer(uint32 slot, const TexturePointer& texture, uin return -1; } + Q_ASSERT(!texture || TextureUsageType::RENDERBUFFER == texture->getUsageType()); + // Check for the slot if (slot >= getMaxNumRenderBuffers()) { return -1; @@ -222,6 +224,8 @@ bool Framebuffer::setDepthStencilBuffer(const TexturePointer& texture, const For return false; } + Q_ASSERT(!texture || TextureUsageType::RENDERBUFFER == texture->getUsageType()); + // Check for the compatibility of size if (texture) { if (!validateTargetCompatibility(*texture)) { diff --git a/libraries/gpu/src/gpu/Inputs.slh b/libraries/gpu/src/gpu/Inputs.slh index ce5e4227eb..843d1059f2 100644 --- a/libraries/gpu/src/gpu/Inputs.slh +++ b/libraries/gpu/src/gpu/Inputs.slh @@ -15,7 +15,7 @@ layout(location = 1) in vec4 inNormal; layout(location = 2) in vec4 inColor; layout(location = 3) in vec4 inTexCoord0; layout(location = 4) in vec4 inTangent; -layout(location = 5) in vec4 inSkinClusterIndex; +layout(location = 5) in ivec4 inSkinClusterIndex; layout(location = 6) in vec4 inSkinClusterWeight; layout(location = 7) in vec4 inTexCoord1; <@endif@> diff --git a/libraries/gpu/src/gpu/Texture.cpp b/libraries/gpu/src/gpu/Texture.cpp index 5b0c4c876a..1e65972114 100755 --- a/libraries/gpu/src/gpu/Texture.cpp +++ b/libraries/gpu/src/gpu/Texture.cpp @@ -19,6 +19,7 @@ #include #include +#include #include #include "GPULogging.h" @@ -88,6 +89,10 @@ uint32_t Texture::getTextureGPUSparseCount() { return Context::getTextureGPUSparseCount(); } +Texture::Size Texture::getTextureTransferPendingSize() { + return Context::getTextureTransferPendingSize(); +} + Texture::Size Texture::getTextureGPUMemoryUsage() { return Context::getTextureGPUMemoryUsage(); } @@ -120,62 +125,23 @@ void Texture::setAllowedGPUMemoryUsage(Size size) { uint8 Texture::NUM_FACES_PER_TYPE[NUM_TYPES] = { 1, 1, 1, 6 }; -Texture::Pixels::Pixels(const Element& format, Size size, const Byte* bytes) : - _format(format), - _sysmem(size, bytes), - _isGPULoaded(false) { - Texture::updateTextureCPUMemoryUsage(0, _sysmem.getSize()); -} +using Storage = Texture::Storage; +using PixelsPointer = Texture::PixelsPointer; +using MemoryStorage = Texture::MemoryStorage; -Texture::Pixels::~Pixels() { - Texture::updateTextureCPUMemoryUsage(_sysmem.getSize(), 0); -} - -Texture::Size Texture::Pixels::resize(Size pSize) { - auto prevSize = _sysmem.getSize(); - auto newSize = _sysmem.resize(pSize); - Texture::updateTextureCPUMemoryUsage(prevSize, newSize); - return newSize; -} - -Texture::Size Texture::Pixels::setData(const Element& format, Size size, const Byte* bytes ) { - _format = format; - auto prevSize = _sysmem.getSize(); - auto newSize = _sysmem.setData(size, bytes); - Texture::updateTextureCPUMemoryUsage(prevSize, newSize); - _isGPULoaded = false; - return newSize; -} - -void Texture::Pixels::notifyGPULoaded() { - _isGPULoaded = true; - auto prevSize = _sysmem.getSize(); - auto newSize = _sysmem.resize(0); - Texture::updateTextureCPUMemoryUsage(prevSize, newSize); -} - -void Texture::Storage::assignTexture(Texture* texture) { +void Storage::assignTexture(Texture* texture) { _texture = texture; if (_texture) { _type = _texture->getType(); } } -void Texture::Storage::reset() { +void MemoryStorage::reset() { _mips.clear(); bumpStamp(); } -Texture::PixelsPointer Texture::Storage::editMipFace(uint16 level, uint8 face) { - if (level < _mips.size()) { - assert(face < _mips[level].size()); - bumpStamp(); - return _mips[level][face]; - } - return PixelsPointer(); -} - -const Texture::PixelsPointer Texture::Storage::getMipFace(uint16 level, uint8 face) const { +PixelsPointer MemoryStorage::getMipFace(uint16 level, uint8 face) const { if (level < _mips.size()) { assert(face < _mips[level].size()); return _mips[level][face]; @@ -183,20 +149,22 @@ const Texture::PixelsPointer Texture::Storage::getMipFace(uint16 level, uint8 fa return PixelsPointer(); } -void Texture::Storage::notifyMipFaceGPULoaded(uint16 level, uint8 face) const { + +Size MemoryStorage::getMipFaceSize(uint16 level, uint8 face) const { PixelsPointer mipFace = getMipFace(level, face); - // Free the mips if (mipFace) { - mipFace->notifyGPULoaded(); + return mipFace->getSize(); + } else { + return 0; } } -bool Texture::Storage::isMipAvailable(uint16 level, uint8 face) const { +bool MemoryStorage::isMipAvailable(uint16 level, uint8 face) const { PixelsPointer mipFace = getMipFace(level, face); return (mipFace && mipFace->getSize()); } -bool Texture::Storage::allocateMip(uint16 level) { +bool MemoryStorage::allocateMip(uint16 level) { bool changed = false; if (level >= _mips.size()) { _mips.resize(level+1, std::vector(Texture::NUM_FACES_PER_TYPE[getType()])); @@ -206,7 +174,6 @@ bool Texture::Storage::allocateMip(uint16 level) { auto& mip = _mips[level]; for (auto& face : mip) { if (!face) { - face = std::make_shared(); changed = true; } } @@ -216,7 +183,7 @@ bool Texture::Storage::allocateMip(uint16 level) { return changed; } -bool Texture::Storage::assignMipData(uint16 level, const Element& format, Size size, const Byte* bytes) { +void MemoryStorage::assignMipData(uint16 level, const storage::StoragePointer& storagePointer) { allocateMip(level); auto& mip = _mips[level]; @@ -225,84 +192,80 @@ bool Texture::Storage::assignMipData(uint16 level, const Element& format, Size s // The bytes assigned here are supposed to contain all the faces bytes of the mip. // For tex1D, 2D, 3D there is only one face // For Cube, we expect the 6 faces in the order X+, X-, Y+, Y-, Z+, Z- - auto sizePerFace = size / mip.size(); - auto faceBytes = bytes; - Size allocated = 0; + auto sizePerFace = storagePointer->size() / mip.size(); + size_t offset = 0; for (auto& face : mip) { - allocated += face->setData(format, sizePerFace, faceBytes); - faceBytes += sizePerFace; + face = storagePointer->createView(sizePerFace, offset); + offset += sizePerFace; } bumpStamp(); - - return allocated == size; } -bool Texture::Storage::assignMipFaceData(uint16 level, const Element& format, Size size, const Byte* bytes, uint8 face) { - +void Texture::MemoryStorage::assignMipFaceData(uint16 level, uint8 face, const storage::StoragePointer& storagePointer) { allocateMip(level); - auto mip = _mips[level]; - Size allocated = 0; + auto& mip = _mips[level]; if (face < mip.size()) { - auto mipFace = mip[face]; - allocated += mipFace->setData(format, size, bytes); + mip[face] = storagePointer; bumpStamp(); } - - return allocated == size; } -Texture* Texture::createExternal2D(const ExternalRecycler& recycler, const Sampler& sampler) { - Texture* tex = new Texture(); +Texture* Texture::createExternal(const ExternalRecycler& recycler, const Sampler& sampler) { + Texture* tex = new Texture(TextureUsageType::EXTERNAL); tex->_type = TEX_2D; - tex->_maxMip = 0; + tex->_maxMipLevel = 0; tex->_sampler = sampler; - tex->setUsage(Usage::Builder().withExternal().withColor()); tex->setExternalRecycler(recycler); return tex; } -Texture* Texture::create1D(const Element& texelFormat, uint16 width, const Sampler& sampler) { - return create(TEX_1D, texelFormat, width, 1, 1, 1, 1, sampler); +Texture* Texture::createRenderBuffer(const Element& texelFormat, uint16 width, uint16 height, uint16 numMips, const Sampler& sampler) { + return create(TextureUsageType::RENDERBUFFER, TEX_2D, texelFormat, width, height, 1, 1, 0, numMips, sampler); } -Texture* Texture::create2D(const Element& texelFormat, uint16 width, uint16 height, const Sampler& sampler) { - return create(TEX_2D, texelFormat, width, height, 1, 1, 1, sampler); +Texture* Texture::create1D(const Element& texelFormat, uint16 width, uint16 numMips, const Sampler& sampler) { + return create(TextureUsageType::RESOURCE, TEX_1D, texelFormat, width, 1, 1, 1, 0, numMips, sampler); } -Texture* Texture::create3D(const Element& texelFormat, uint16 width, uint16 height, uint16 depth, const Sampler& sampler) { - return create(TEX_3D, texelFormat, width, height, depth, 1, 1, sampler); +Texture* Texture::create2D(const Element& texelFormat, uint16 width, uint16 height, uint16 numMips, const Sampler& sampler) { + return create(TextureUsageType::RESOURCE, TEX_2D, texelFormat, width, height, 1, 1, 0, numMips, sampler); } -Texture* Texture::createCube(const Element& texelFormat, uint16 width, const Sampler& sampler) { - return create(TEX_CUBE, texelFormat, width, width, 1, 1, 1, sampler); +Texture* Texture::createStrict(const Element& texelFormat, uint16 width, uint16 height, uint16 numMips, const Sampler& sampler) { + return create(TextureUsageType::STRICT_RESOURCE, TEX_2D, texelFormat, width, height, 1, 1, 0, numMips, sampler); } -Texture* Texture::create(Type type, const Element& texelFormat, uint16 width, uint16 height, uint16 depth, uint16 numSamples, uint16 numSlices, const Sampler& sampler) +Texture* Texture::create3D(const Element& texelFormat, uint16 width, uint16 height, uint16 depth, uint16 numMips, const Sampler& sampler) { + return create(TextureUsageType::RESOURCE, TEX_3D, texelFormat, width, height, depth, 1, 0, numMips, sampler); +} + +Texture* Texture::createCube(const Element& texelFormat, uint16 width, uint16 numMips, const Sampler& sampler) { + return create(TextureUsageType::RESOURCE, TEX_CUBE, texelFormat, width, width, 1, 1, 0, numMips, sampler); +} + +Texture* Texture::create(TextureUsageType usageType, Type type, const Element& texelFormat, uint16 width, uint16 height, uint16 depth, uint16 numSamples, uint16 numSlices, uint16 numMips, const Sampler& sampler) { - Texture* tex = new Texture(); - tex->_storage.reset(new Storage()); + Texture* tex = new Texture(usageType); + tex->_storage.reset(new MemoryStorage()); tex->_type = type; tex->_storage->assignTexture(tex); - tex->_maxMip = 0; - tex->resize(type, texelFormat, width, height, depth, numSamples, numSlices); + tex->resize(type, texelFormat, width, height, depth, numSamples, numSlices, numMips); tex->_sampler = sampler; return tex; } -Texture::Texture(): - Resource() -{ +Texture::Texture(TextureUsageType usageType) : + Resource(), _usageType(usageType) { _textureCPUCount++; } -Texture::~Texture() -{ +Texture::~Texture() { _textureCPUCount--; - if (getUsage().isExternal()) { + if (_usageType == TextureUsageType::EXTERNAL) { Texture::ExternalUpdates externalUpdates; { Lock lock(_externalMutex); @@ -320,8 +283,8 @@ Texture::~Texture() } } -Texture::Size Texture::resize(Type type, const Element& texelFormat, uint16 width, uint16 height, uint16 depth, uint16 numSamples, uint16 numSlices) { - if (width && height && depth && numSamples && numSlices) { +Texture::Size Texture::resize(Type type, const Element& texelFormat, uint16 width, uint16 height, uint16 depth, uint16 numSamples, uint16 numSlices, uint16 numMips) { + if (width && height && depth && numSamples) { bool changed = false; if ( _type != type) { @@ -355,9 +318,19 @@ Texture::Size Texture::resize(Type type, const Element& texelFormat, uint16 widt _depth = depth; changed = true; } - + + if ((_maxMipLevel + 1) != numMips) { + _maxMipLevel = safeNumMips(numMips) - 1; + changed = true; + } + + if (texelFormat != _texelFormat) { + _texelFormat = texelFormat; + changed = true; + } + // Evaluate the new size with the new format - uint32_t size = NUM_FACES_PER_TYPE[_type] *_width * _height * _depth * _numSamples * texelFormat.getSize(); + Size size = NUM_FACES_PER_TYPE[_type] * _height * _depth * evalPaddedSize(_numSamples * _width * _texelFormat.getSize()); // If size change then we need to reset if (changed || (size != getSize())) { @@ -366,12 +339,6 @@ Texture::Size Texture::resize(Type type, const Element& texelFormat, uint16 widt _stamp++; } - // TexelFormat might have change, but it's mostly interpretation - if (texelFormat != _texelFormat) { - _texelFormat = texelFormat; - _stamp++; - } - // Here the Texture has been fully defined from the gpu point of view (size and format) _defined = true; } else { @@ -381,23 +348,6 @@ Texture::Size Texture::resize(Type type, const Element& texelFormat, uint16 widt return _size; } -Texture::Size Texture::resize1D(uint16 width, uint16 numSamples) { - return resize(TEX_1D, getTexelFormat(), width, 1, 1, numSamples, 1); -} -Texture::Size Texture::resize2D(uint16 width, uint16 height, uint16 numSamples) { - return resize(TEX_2D, getTexelFormat(), width, height, 1, numSamples, 1); -} -Texture::Size Texture::resize3D(uint16 width, uint16 height, uint16 depth, uint16 numSamples) { - return resize(TEX_3D, getTexelFormat(), width, height, depth, numSamples, 1); -} -Texture::Size Texture::resizeCube(uint16 width, uint16 numSamples) { - return resize(TEX_CUBE, getTexelFormat(), width, 1, 1, numSamples, 1); -} - -Texture::Size Texture::reformat(const Element& texelFormat) { - return resize(_type, texelFormat, getWidth(), getHeight(), getDepth(), getNumSamples(), getNumSlices()); -} - bool Texture::isColorRenderTarget() const { return (_texelFormat.getSemantic() == gpu::RGBA); } @@ -406,7 +356,7 @@ bool Texture::isDepthStencilRenderTarget() const { return (_texelFormat.getSemantic() == gpu::DEPTH) || (_texelFormat.getSemantic() == gpu::DEPTH_STENCIL); } -uint16 Texture::evalDimNumMips(uint16 size) { +uint16 Texture::evalDimMaxNumMips(uint16 size) { double largerDim = size; double val = log(largerDim)/log(2.0); return 1 + (uint16) val; @@ -414,7 +364,7 @@ uint16 Texture::evalDimNumMips(uint16 size) { static const double LOG_2 = log(2.0); -uint16 Texture::evalNumMips(const Vec3u& dimensions) { +uint16 Texture::evalMaxNumMips(const Vec3u& dimensions) { double largerDim = glm::compMax(dimensions); double val = log(largerDim) / LOG_2; return 1 + (uint16)val; @@ -422,136 +372,134 @@ uint16 Texture::evalNumMips(const Vec3u& dimensions) { // The number mips that the texture could have if all existed // = log2(max(width, height, depth)) -uint16 Texture::evalNumMips() const { - return evalNumMips({ _width, _height, _depth }); +uint16 Texture::evalMaxNumMips() const { + return evalMaxNumMips({ _width, _height, _depth }); } -bool Texture::assignStoredMip(uint16 level, const Element& format, Size size, const Byte* bytes) { +// Check a num of mips requested against the maximum possible specified +// if passing -1 then answer the max +// simply does (askedNumMips == 0 ? maxNumMips : (numstd::min(askedNumMips, maxNumMips)) +uint16 Texture::safeNumMips(uint16 askedNumMips, uint16 maxNumMips) { + if (askedNumMips > 0) { + return std::min(askedNumMips, maxNumMips); + } else { + return maxNumMips; + } +} + +// Same but applied to this texture's num max mips from evalNumMips() +uint16 Texture::safeNumMips(uint16 askedNumMips) const { + return safeNumMips(askedNumMips, evalMaxNumMips()); +} + +Size Texture::evalTotalSize(uint16 startingMip) const { + Size size = 0; + uint16 minMipLevel = std::max(getMinMip(), startingMip); + uint16 maxMipLevel = getMaxMip(); + for (uint16 level = minMipLevel; level <= maxMipLevel; level++) { + size += evalMipSize(level); + } + return size * getNumSlices(); +} + +void Texture::setStoredMipFormat(const Element& format) { + _storage->setFormat(format); +} + +const Element& Texture::getStoredMipFormat() const { + return _storage->getFormat(); +} + +void Texture::assignStoredMip(uint16 level, Size size, const Byte* bytes) { + storage::StoragePointer storage = std::make_shared(size, bytes); + assignStoredMip(level, storage); +} + +void Texture::assignStoredMipFace(uint16 level, uint8 face, Size size, const Byte* bytes) { + storage::StoragePointer storage = std::make_shared(size, bytes); + assignStoredMipFace(level, face, storage); +} + +void Texture::assignStoredMip(uint16 level, storage::StoragePointer& storage) { // Check that level accessed make sense if (level != 0) { if (_autoGenerateMips) { - return false; + return; } - if (level >= evalNumMips()) { - return false; + if (level >= getNumMips()) { + return; } } // THen check that the mem texture passed make sense with its format - Size expectedSize = evalStoredMipSize(level, format); - if (size == expectedSize) { - _storage->assignMipData(level, format, size, bytes); - _maxMip = std::max(_maxMip, level); + Size expectedSize = evalStoredMipSize(level, getStoredMipFormat()); + auto size = storage->size(); + if (storage->size() == expectedSize) { + _storage->assignMipData(level, storage); _stamp++; - return true; } else if (size > expectedSize) { // NOTE: We are facing this case sometime because apparently QImage (from where we get the bits) is generating images // and alligning the line of pixels to 32 bits. // We should probably consider something a bit more smart to get the correct result but for now (UI elements) // it seems to work... - _storage->assignMipData(level, format, size, bytes); - _maxMip = std::max(_maxMip, level); + _storage->assignMipData(level, storage); _stamp++; - return true; } - - return false; } - -bool Texture::assignStoredMipFace(uint16 level, const Element& format, Size size, const Byte* bytes, uint8 face) { +void Texture::assignStoredMipFace(uint16 level, uint8 face, storage::StoragePointer& storage) { // Check that level accessed make sense if (level != 0) { if (_autoGenerateMips) { - return false; + return; } - if (level >= evalNumMips()) { - return false; + if (level >= getNumMips()) { + return; } } // THen check that the mem texture passed make sense with its format - Size expectedSize = evalStoredMipFaceSize(level, format); + Size expectedSize = evalStoredMipFaceSize(level, getStoredMipFormat()); + auto size = storage->size(); if (size == expectedSize) { - _storage->assignMipFaceData(level, format, size, bytes, face); + _storage->assignMipFaceData(level, face, storage); _stamp++; - return true; } else if (size > expectedSize) { // NOTE: We are facing this case sometime because apparently QImage (from where we get the bits) is generating images // and alligning the line of pixels to 32 bits. // We should probably consider something a bit more smart to get the correct result but for now (UI elements) // it seems to work... - _storage->assignMipFaceData(level, format, size, bytes, face); + _storage->assignMipFaceData(level, face, storage); _stamp++; - return true; } - - return false; } -uint16 Texture::autoGenerateMips(uint16 maxMip) { +void Texture::setAutoGenerateMips(bool enable) { bool changed = false; if (!_autoGenerateMips) { changed = true; _autoGenerateMips = true; } - auto newMaxMip = std::min((uint16)(evalNumMips() - 1), maxMip); - if (newMaxMip != _maxMip) { - changed = true; - _maxMip = newMaxMip;; - } - if (changed) { _stamp++; } - - return _maxMip; } -uint16 Texture::getStoredMipWidth(uint16 level) const { +Size Texture::getStoredMipSize(uint16 level) const { PixelsPointer mipFace = accessStoredMipFace(level); + Size size = 0; if (mipFace && mipFace->getSize()) { - return evalMipWidth(level); + for (int face = 0; face < getNumFaces(); face++) { + size += getStoredMipFaceSize(level, face); + } } - return 0; + return size; } -uint16 Texture::getStoredMipHeight(uint16 level) const { - PixelsPointer mip = accessStoredMipFace(level); - if (mip && mip->getSize()) { - return evalMipHeight(level); - } - return 0; -} - -uint16 Texture::getStoredMipDepth(uint16 level) const { - PixelsPointer mipFace = accessStoredMipFace(level); - if (mipFace && mipFace->getSize()) { - return evalMipDepth(level); - } - return 0; -} - -uint32 Texture::getStoredMipNumTexels(uint16 level) const { - PixelsPointer mipFace = accessStoredMipFace(level); - if (mipFace && mipFace->getSize()) { - return evalMipWidth(level) * evalMipHeight(level) * evalMipDepth(level); - } - return 0; -} - -uint32 Texture::getStoredMipSize(uint16 level) const { - PixelsPointer mipFace = accessStoredMipFace(level); - if (mipFace && mipFace->getSize()) { - return evalMipWidth(level) * evalMipHeight(level) * evalMipDepth(level) * getTexelFormat().getSize(); - } - return 0; -} - -gpu::Resource::Size Texture::getStoredSize() const { - auto size = 0; - for (int level = 0; level < evalNumMips(); ++level) { +Size Texture::getStoredSize() const { + Size size = 0; + for (int level = 0; level < getNumMips(); level++) { size += getStoredMipSize(level); } return size; @@ -794,7 +742,16 @@ bool sphericalHarmonicsFromTexture(const gpu::Texture& cubeTexture, std::vector< for(int face=0; face < gpu::Texture::NUM_CUBE_FACES; face++) { PROFILE_RANGE(render_gpu, "ProcessFace"); - auto numComponents = cubeTexture.accessStoredMipFace(0,face)->getFormat().getScalarCount(); + auto mipFormat = cubeTexture.getStoredMipFormat(); + auto numComponents = mipFormat.getScalarCount(); + int roffset { 0 }; + int goffset { 1 }; + int boffset { 2 }; + if ((mipFormat.getSemantic() == gpu::BGRA) || (mipFormat.getSemantic() == gpu::SBGRA)) { + roffset = 2; + boffset = 0; + } + auto data = cubeTexture.accessStoredMipFace(0,face)->readData(); if (data == nullptr) { continue; @@ -882,9 +839,9 @@ bool sphericalHarmonicsFromTexture(const gpu::Texture& cubeTexture, std::vector< for (int i = 0; i < stride; ++i) { for (int j = 0; j < stride; ++j) { int k = (int)(x + i - halfStride + (y + j - halfStride) * width) * numComponents; - red += ColorUtils::sRGB8ToLinearFloat(data[k]); - green += ColorUtils::sRGB8ToLinearFloat(data[k + 1]); - blue += ColorUtils::sRGB8ToLinearFloat(data[k + 2]); + red += ColorUtils::sRGB8ToLinearFloat(data[k + roffset]); + green += ColorUtils::sRGB8ToLinearFloat(data[k + goffset]); + blue += ColorUtils::sRGB8ToLinearFloat(data[k + boffset]); } } glm::vec3 clr(red, green, blue); @@ -911,8 +868,6 @@ bool sphericalHarmonicsFromTexture(const gpu::Texture& cubeTexture, std::vector< // save result for(uint i=0; i < sqOrder; i++) { - // gamma Correct - // output[i] = linearTosRGB(glm::vec3(resultR[i], resultG[i], resultB[i])); output[i] = glm::vec3(resultR[i], resultG[i], resultB[i]); } @@ -962,7 +917,7 @@ bool TextureSource::isDefined() const { bool Texture::setMinMip(uint16 newMinMip) { uint16 oldMinMip = _minMip; - _minMip = std::min(std::max(_minMip, newMinMip), _maxMip); + _minMip = std::min(std::max(_minMip, newMinMip), getMaxMip()); return oldMinMip != _minMip; } @@ -1001,3 +956,7 @@ Texture::ExternalUpdates Texture::getUpdates() const { } return result; } + +void Texture::setStorage(std::unique_ptr& newStorage) { + _storage.swap(newStorage); +} diff --git a/libraries/gpu/src/gpu/Texture.h b/libraries/gpu/src/gpu/Texture.h index 856bd4983d..eab02141f0 100755 --- a/libraries/gpu/src/gpu/Texture.h +++ b/libraries/gpu/src/gpu/Texture.h @@ -17,9 +17,19 @@ #include #include +#include + #include "Forward.h" #include "Resource.h" +namespace ktx { + class KTX; + using KTXUniquePointer = std::unique_ptr; + struct KTXDescriptor; + using KTXDescriptorPointer = std::unique_ptr; + struct Header; +} + namespace gpu { // THe spherical harmonics is a nice tool for cubemap, so if required, the irradiance SH can be automatically generated @@ -135,10 +145,18 @@ public: uint8 getMinMip() const { return _desc._minMip; } uint8 getMaxMip() const { return _desc._maxMip; } + const Desc& getDesc() const { return _desc; } protected: Desc _desc; }; +enum class TextureUsageType { + RENDERBUFFER, // Used as attachments to a framebuffer + RESOURCE, // Resource textures, like materials... subject to memory manipulation + STRICT_RESOURCE, // Resource textures not subject to manipulation, like the normal fitting texture + EXTERNAL, +}; + class Texture : public Resource { static std::atomic _textureCPUCount; static std::atomic _textureCPUMemoryUsage; @@ -147,10 +165,12 @@ class Texture : public Resource { static void updateTextureCPUMemoryUsage(Size prevObjectSize, Size newObjectSize); public: + static const uint32_t CUBE_FACE_COUNT { 6 }; static uint32_t getTextureCPUCount(); static Size getTextureCPUMemoryUsage(); static uint32_t getTextureGPUCount(); static uint32_t getTextureGPUSparseCount(); + static Size getTextureTransferPendingSize(); static Size getTextureGPUMemoryUsage(); static Size getTextureGPUVirtualMemoryUsage(); static Size getTextureGPUFramebufferMemoryUsage(); @@ -173,9 +193,9 @@ public: NORMAL, // Texture is a normal map ALPHA, // Texture has an alpha channel ALPHA_MASK, // Texture alpha channel is a Mask 0/1 - EXTERNAL, NUM_FLAGS, }; + typedef std::bitset Flags; // The key is the Flags @@ -199,7 +219,6 @@ public: Builder& withNormal() { _flags.set(NORMAL); return (*this); } Builder& withAlpha() { _flags.set(ALPHA); return (*this); } Builder& withAlphaMask() { _flags.set(ALPHA_MASK); return (*this); } - Builder& withExternal() { _flags.set(EXTERNAL); return (*this); } }; Usage(const Builder& builder) : Usage(builder._flags) {} @@ -208,39 +227,12 @@ public: bool isAlpha() const { return _flags[ALPHA]; } bool isAlphaMask() const { return _flags[ALPHA_MASK]; } - bool isExternal() const { return _flags[EXTERNAL]; } - bool operator==(const Usage& usage) { return (_flags == usage._flags); } bool operator!=(const Usage& usage) { return (_flags != usage._flags); } }; - class Pixels { - public: - Pixels() {} - Pixels(const Pixels& pixels) = default; - Pixels(const Element& format, Size size, const Byte* bytes); - ~Pixels(); - - const Byte* readData() const { return _sysmem.readData(); } - Size getSize() const { return _sysmem.getSize(); } - Size resize(Size pSize); - Size setData(const Element& format, Size size, const Byte* bytes ); - - const Element& getFormat() const { return _format; } - - void notifyGPULoaded(); - - protected: - Element _format; - Sysmem _sysmem; - bool _isGPULoaded; - - friend class Texture; - }; - typedef std::shared_ptr< Pixels > PixelsPointer; - - enum Type { + enum Type : uint8 { TEX_1D = 0, TEX_2D, TEX_3D, @@ -261,46 +253,93 @@ public: NUM_CUBE_FACES, // Not a valid vace index }; + // Lines of pixels are padded to be a multiple of "PACKING_SIZE" which is 4 bytes + static const uint32 PACKING_SIZE = 4; + static uint8 evalPaddingNumBytes(Size byteSize) { return (uint8) (3 - (byteSize + 3) % PACKING_SIZE); } + static Size evalPaddedSize(Size byteSize) { return byteSize + (Size) evalPaddingNumBytes(byteSize); } + + + using PixelsPointer = storage::StoragePointer; class Storage { public: Storage() {} virtual ~Storage() {} - virtual void reset(); - virtual PixelsPointer editMipFace(uint16 level, uint8 face = 0); - virtual const PixelsPointer getMipFace(uint16 level, uint8 face = 0) const; - virtual bool allocateMip(uint16 level); - virtual bool assignMipData(uint16 level, const Element& format, Size size, const Byte* bytes); - virtual bool assignMipFaceData(uint16 level, const Element& format, Size size, const Byte* bytes, uint8 face); - virtual bool isMipAvailable(uint16 level, uint8 face = 0) const; + virtual void reset() = 0; + virtual PixelsPointer getMipFace(uint16 level, uint8 face = 0) const = 0; + virtual Size getMipFaceSize(uint16 level, uint8 face = 0) const = 0; + virtual void assignMipData(uint16 level, const storage::StoragePointer& storage) = 0; + virtual void assignMipFaceData(uint16 level, uint8 face, const storage::StoragePointer& storage) = 0; + virtual bool isMipAvailable(uint16 level, uint8 face = 0) const = 0; Texture::Type getType() const { return _type; } - + Stamp getStamp() const { return _stamp; } Stamp bumpStamp() { return ++_stamp; } - protected: - Stamp _stamp = 0; - Texture* _texture = nullptr; // Points to the parent texture (not owned) - Texture::Type _type = Texture::TEX_2D; // The type of texture is needed to know the number of faces to expect - std::vector> _mips; // an array of mips, each mip is an array of faces + void setFormat(const Element& format) { _format = format; } + const Element& getFormat() const { return _format; } + + private: + Stamp _stamp { 0 }; + Element _format; + Texture::Type _type { Texture::TEX_2D }; // The type of texture is needed to know the number of faces to expect + Texture* _texture { nullptr }; // Points to the parent texture (not owned) virtual void assignTexture(Texture* tex); // Texture storage is pointing to ONE corrresponding Texture. const Texture* getTexture() const { return _texture; } - friend class Texture; - - // THis should be only called by the Texture from the Backend to notify the storage that the specified mip face pixels - // have been uploaded to the GPU memory. IT is possible for the storage to free the system memory then - virtual void notifyMipFaceGPULoaded(uint16 level, uint8 face) const; }; - - static Texture* create1D(const Element& texelFormat, uint16 width, const Sampler& sampler = Sampler()); - static Texture* create2D(const Element& texelFormat, uint16 width, uint16 height, const Sampler& sampler = Sampler()); - static Texture* create3D(const Element& texelFormat, uint16 width, uint16 height, uint16 depth, const Sampler& sampler = Sampler()); - static Texture* createCube(const Element& texelFormat, uint16 width, const Sampler& sampler = Sampler()); - static Texture* createExternal2D(const ExternalRecycler& recycler, const Sampler& sampler = Sampler()); + class MemoryStorage : public Storage { + public: + void reset() override; + PixelsPointer getMipFace(uint16 level, uint8 face = 0) const override; + Size getMipFaceSize(uint16 level, uint8 face = 0) const override; + void assignMipData(uint16 level, const storage::StoragePointer& storage) override; + void assignMipFaceData(uint16 level, uint8 face, const storage::StoragePointer& storage) override; + bool isMipAvailable(uint16 level, uint8 face = 0) const override; - Texture(); + protected: + bool allocateMip(uint16 level); + std::vector> _mips; // an array of mips, each mip is an array of faces + }; + + class KtxStorage : public Storage { + public: + KtxStorage(const std::string& filename); + PixelsPointer getMipFace(uint16 level, uint8 face = 0) const override; + Size getMipFaceSize(uint16 level, uint8 face = 0) const override; + // By convention, all mip levels and faces MUST be populated when using KTX backing + bool isMipAvailable(uint16 level, uint8 face = 0) const override { return true; } + + void assignMipData(uint16 level, const storage::StoragePointer& storage) override { + throw std::runtime_error("Invalid call"); + } + + void assignMipFaceData(uint16 level, uint8 face, const storage::StoragePointer& storage) override { + throw std::runtime_error("Invalid call"); + } + void reset() override { } + + protected: + std::string _filename; + ktx::KTXDescriptorPointer _ktxDescriptor; + friend class Texture; + }; + + static const uint16 MAX_NUM_MIPS = 0; + static const uint16 SINGLE_MIP = 1; + static Texture* create1D(const Element& texelFormat, uint16 width, uint16 numMips = SINGLE_MIP, const Sampler& sampler = Sampler()); + static Texture* create2D(const Element& texelFormat, uint16 width, uint16 height, uint16 numMips = SINGLE_MIP, const Sampler& sampler = Sampler()); + static Texture* create3D(const Element& texelFormat, uint16 width, uint16 height, uint16 depth, uint16 numMips = SINGLE_MIP, const Sampler& sampler = Sampler()); + static Texture* createCube(const Element& texelFormat, uint16 width, uint16 numMips = 1, const Sampler& sampler = Sampler()); + static Texture* createRenderBuffer(const Element& texelFormat, uint16 width, uint16 height, uint16 numMips = SINGLE_MIP, const Sampler& sampler = Sampler()); + static Texture* createStrict(const Element& texelFormat, uint16 width, uint16 height, uint16 numMips = SINGLE_MIP, const Sampler& sampler = Sampler()); + static Texture* createExternal(const ExternalRecycler& recycler, const Sampler& sampler = Sampler()); + + // After the texture has been created, it should be defined + bool isDefined() const { return _defined; } + + Texture(TextureUsageType usageType); Texture(const Texture& buf); // deep copy of the sysmem texture Texture& operator=(const Texture& buf); // deep copy of the sysmem texture ~Texture(); @@ -309,146 +348,132 @@ public: Stamp getDataStamp() const { return _storage->getStamp(); } // The theoretical size in bytes of data stored in the texture + // For the master (level) first level of mip Size getSize() const override { return _size; } - // The actual size in bytes of data stored in the texture - Size getStoredSize() const; - - // Resize, unless auto mips mode would destroy all the sub mips - Size resize1D(uint16 width, uint16 numSamples); - Size resize2D(uint16 width, uint16 height, uint16 numSamples); - Size resize3D(uint16 width, uint16 height, uint16 depth, uint16 numSamples); - Size resizeCube(uint16 width, uint16 numSamples); - - // Reformat, unless auto mips mode would destroy all the sub mips - Size reformat(const Element& texelFormat); - // Size and format Type getType() const { return _type; } + TextureUsageType getUsageType() const { return _usageType; } bool isColorRenderTarget() const; bool isDepthStencilRenderTarget() const; const Element& getTexelFormat() const { return _texelFormat; } - bool hasBorder() const { return false; } Vec3u getDimensions() const { return Vec3u(_width, _height, _depth); } uint16 getWidth() const { return _width; } uint16 getHeight() const { return _height; } uint16 getDepth() const { return _depth; } - uint32 getRowPitch() const { return getWidth() * getTexelFormat().getSize(); } - // The number of faces is mostly used for cube map, and maybe for stereo ? otherwise it's 1 // For cube maps, this means the pixels of the different faces are supposed to be packed back to back in a mip // as if the height was NUM_FACES time bigger. static uint8 NUM_FACES_PER_TYPE[NUM_TYPES]; uint8 getNumFaces() const { return NUM_FACES_PER_TYPE[getType()]; } - uint32 getNumTexels() const { return _width * _height * _depth * getNumFaces(); } + // The texture is an array if the _numSlices is not 0. + // otherwise, if _numSLices is 0, then the texture is NOT an array + // The number of slices returned is 1 at the minimum (if not an array) or the actual _numSlices. + bool isArray() const { return _numSlices > 0; } + uint16 getNumSlices() const { return (isArray() ? _numSlices : 1); } - uint16 getNumSlices() const { return _numSlices; } uint16 getNumSamples() const { return _numSamples; } - - // NumSamples can only have certain values based on the hw static uint16 evalNumSamplesUsed(uint16 numSamplesTried); + // max mip is in the range [ 0 if no sub mips, log2(max(width, height, depth))] + // It is defined at creation time (immutable) + uint16 getMaxMip() const { return _maxMipLevel; } + uint16 getNumMips() const { return _maxMipLevel + 1; } + // Mips size evaluation // The number mips that a dimension could haves // = 1 + log2(size) - static uint16 evalDimNumMips(uint16 size); + static uint16 evalDimMaxNumMips(uint16 size); // The number mips that the texture could have if all existed // = 1 + log2(max(width, height, depth)) - uint16 evalNumMips() const; + uint16 evalMaxNumMips() const; + static uint16 evalMaxNumMips(const Vec3u& dimensions); - static uint16 evalNumMips(const Vec3u& dimensions); + // Check a num of mips requested against the maximum possible specified + // if passing -1 then answer the max + // simply does (askedNumMips == -1 ? maxMips : (numstd::min(askedNumMips, max)) + static uint16 safeNumMips(uint16 askedNumMips, uint16 maxMips); + + // Same but applied to this texture's num max mips from evalNumMips() + uint16 safeNumMips(uint16 askedNumMips) const; // Eval the size that the mips level SHOULD have // not the one stored in the Texture - static const uint MIN_DIMENSION = 1; Vec3u evalMipDimensions(uint16 level) const; uint16 evalMipWidth(uint16 level) const { return std::max(_width >> level, 1); } uint16 evalMipHeight(uint16 level) const { return std::max(_height >> level, 1); } uint16 evalMipDepth(uint16 level) const { return std::max(_depth >> level, 1); } + // The size of a face is a multiple of the padded line = (width * texelFormat_size + alignment padding) + Size evalMipLineSize(uint16 level) const { return evalPaddedSize(evalMipWidth(level) * getTexelFormat().getSize()); } + // Size for each face of a mip at a particular level uint32 evalMipFaceNumTexels(uint16 level) const { return evalMipWidth(level) * evalMipHeight(level) * evalMipDepth(level); } - uint32 evalMipFaceSize(uint16 level) const { return evalMipFaceNumTexels(level) * getTexelFormat().getSize(); } + Size evalMipFaceSize(uint16 level) const { return evalMipLineSize(level) * evalMipHeight(level) * evalMipDepth(level); } // Total size for the mip uint32 evalMipNumTexels(uint16 level) const { return evalMipFaceNumTexels(level) * getNumFaces(); } - uint32 evalMipSize(uint16 level) const { return evalMipNumTexels(level) * getTexelFormat().getSize(); } + Size evalMipSize(uint16 level) const { return evalMipFaceSize(level) * getNumFaces(); } - uint32 evalStoredMipFaceSize(uint16 level, const Element& format) const { return evalMipFaceNumTexels(level) * format.getSize(); } - uint32 evalStoredMipSize(uint16 level, const Element& format) const { return evalMipNumTexels(level) * format.getSize(); } + // Total size for all the mips of the texture + Size evalTotalSize(uint16 startingMip = 0) const; - uint32 evalTotalSize(uint16 startingMip = 0) const { - uint32 size = 0; - uint16 minMipLevel = std::max(minMip(), startingMip); - uint16 maxMipLevel = maxMip(); - for (uint16 l = minMipLevel; l <= maxMipLevel; l++) { - size += evalMipSize(l); - } - return size * getNumSlices(); - } - - // max mip is in the range [ 0 if no sub mips, log2(max(width, height, depth))] - // if autoGenerateMip is on => will provide the maxMIp level specified - // else provide the deepest mip level provided through assignMip - uint16 maxMip() const { return _maxMip; } - - uint16 minMip() const { return _minMip; } - - uint16 mipLevels() const { return _maxMip + 1; } - - uint16 usedMipLevels() const { return (_maxMip - _minMip) + 1; } + // Compute the theorical size of the texture elements storage depending on the specified format + Size evalStoredMipLineSize(uint16 level, const Element& format) const { return evalPaddedSize(evalMipWidth(level) * format.getSize()); } + Size evalStoredMipFaceSize(uint16 level, const Element& format) const { return evalMipFaceNumTexels(level) * format.getSize(); } + Size evalStoredMipSize(uint16 level, const Element& format) const { return evalMipNumTexels(level) * format.getSize(); } + // For convenience assign a source name const std::string& source() const { return _source; } void setSource(const std::string& source) { _source = source; } + + // Potentially change the minimum mip (mostly for debugging purpose) bool setMinMip(uint16 newMinMip); bool incremementMinMip(uint16 count = 1); + uint16 getMinMip() const { return _minMip; } + uint16 usedMipLevels() const { return (getNumMips() - _minMip); } - // Generate the mips automatically - // But the sysmem version is not available + // Generate the sub mips automatically for the texture + // If the storage version is not available (from CPU memory) // Only works for the standard formats - // Specify the maximum Mip level available - // 0 is the default one - // 1 is the first level - // ... - // nbMips - 1 is the last mip level - // - // If -1 then all the mips are generated - // - // Return the totalnumber of mips that will be available - uint16 autoGenerateMips(uint16 maxMip); + void setAutoGenerateMips(bool enable); bool isAutogenerateMips() const { return _autoGenerateMips; } // Managing Storage and mips + // Mip storage format is constant across all mips + void setStoredMipFormat(const Element& format); + const Element& getStoredMipFormat() const; + // Manually allocate the mips down until the specified maxMip // this is just allocating the sysmem version of it // in case autoGen is on, this doesn't allocate // Explicitely assign mip data for a certain level // If Bytes is NULL then simply allocate the space so mip sysmem can be accessed - bool assignStoredMip(uint16 level, const Element& format, Size size, const Byte* bytes); - bool assignStoredMipFace(uint16 level, const Element& format, Size size, const Byte* bytes, uint8 face); + void assignStoredMip(uint16 level, Size size, const Byte* bytes); + void assignStoredMipFace(uint16 level, uint8 face, Size size, const Byte* bytes); - // Access the the sub mips - bool isStoredMipFaceAvailable(uint16 level, uint8 face = 0) const { return _storage->isMipAvailable(level, face); } + void assignStoredMip(uint16 level, storage::StoragePointer& storage); + void assignStoredMipFace(uint16 level, uint8 face, storage::StoragePointer& storage); + + // Access the stored mips and faces const PixelsPointer accessStoredMipFace(uint16 level, uint8 face = 0) const { return _storage->getMipFace(level, face); } + bool isStoredMipFaceAvailable(uint16 level, uint8 face = 0) const { return _storage->isMipAvailable(level, face); } + Size getStoredMipFaceSize(uint16 level, uint8 face = 0) const { return _storage->getMipFaceSize(level, face); } + Size getStoredMipSize(uint16 level) const; + Size getStoredSize() const; - // access sizes for the stored mips - uint16 getStoredMipWidth(uint16 level) const; - uint16 getStoredMipHeight(uint16 level) const; - uint16 getStoredMipDepth(uint16 level) const; - uint32 getStoredMipNumTexels(uint16 level) const; - uint32 getStoredMipSize(uint16 level) const; - - bool isDefined() const { return _defined; } + void setStorage(std::unique_ptr& newStorage); + void setKtxBacking(const std::string& filename); // Usage is a a set of flags providing Semantic about the usage of the Texture. void setUsage(const Usage& usage) { _usage = usage; } @@ -464,8 +489,8 @@ public: const Sampler& getSampler() const { return _sampler; } Stamp getSamplerStamp() const { return _samplerStamp; } - // Only callable by the Backend - void notifyMipFaceGPULoaded(uint16 level, uint8 face = 0) const { return _storage->notifyMipFaceGPULoaded(level, face); } + void setFallbackTexture(const TexturePointer& fallback) { _fallback = fallback; } + TexturePointer getFallbackTexture() const { return _fallback.lock(); } void setExternalTexture(uint32 externalId, void* externalFence); void setExternalRecycler(const ExternalRecycler& recycler); @@ -475,36 +500,50 @@ public: ExternalUpdates getUpdates() const; + // Textures can be serialized directly to ktx data file, here is how + static ktx::KTXUniquePointer serialize(const Texture& texture); + static Texture* unserialize(const std::string& ktxFile, TextureUsageType usageType = TextureUsageType::RESOURCE, Usage usage = Usage(), const Sampler::Desc& sampler = Sampler::Desc()); + static bool evalKTXFormat(const Element& mipFormat, const Element& texelFormat, ktx::Header& header); + static bool evalTextureFormat(const ktx::Header& header, Element& mipFormat, Element& texelFormat); + protected: + const TextureUsageType _usageType; + // Should only be accessed internally or by the backend sync function mutable Mutex _externalMutex; mutable std::list _externalUpdates; ExternalRecycler _externalRecycler; + std::weak_ptr _fallback; // Not strictly necessary, but incredibly useful for debugging std::string _source; std::unique_ptr< Storage > _storage; - Stamp _stamp = 0; + Stamp _stamp { 0 }; Sampler _sampler; - Stamp _samplerStamp; + Stamp _samplerStamp { 0 }; - uint32 _size = 0; + Size _size { 0 }; Element _texelFormat; - uint16 _width = 1; - uint16 _height = 1; - uint16 _depth = 1; + uint16 _width { 1 }; + uint16 _height { 1 }; + uint16 _depth { 1 }; - uint16 _numSamples = 1; - uint16 _numSlices = 1; + uint16 _numSamples { 1 }; + + // if _numSlices is 0, the texture is not an "Array", the getNumSlices reported is 1 + uint16 _numSlices { 0 }; + + // valid _maxMipLevel is in the range [ 0 if no sub mips, log2(max(width, height, depth) ] + // The num of mips returned is _maxMipLevel + 1 + uint16 _maxMipLevel { 0 }; - uint16 _maxMip { 0 }; uint16 _minMip { 0 }; - Type _type = TEX_1D; + Type _type { TEX_1D }; Usage _usage; @@ -513,9 +552,9 @@ protected: bool _isIrradianceValid = false; bool _defined = false; - static Texture* create(Type type, const Element& texelFormat, uint16 width, uint16 height, uint16 depth, uint16 numSamples, uint16 numSlices, const Sampler& sampler); + static Texture* create(TextureUsageType usageType, Type type, const Element& texelFormat, uint16 width, uint16 height, uint16 depth, uint16 numSamples, uint16 numSlices, uint16 numMips, const Sampler& sampler); - Size resize(Type type, const Element& texelFormat, uint16 width, uint16 height, uint16 depth, uint16 numSamples, uint16 numSlices); + Size resize(Type type, const Element& texelFormat, uint16 width, uint16 height, uint16 depth, uint16 numSamples, uint16 numSlices, uint16 numMips); }; typedef std::shared_ptr TexturePointer; diff --git a/libraries/gpu/src/gpu/Texture_ktx.cpp b/libraries/gpu/src/gpu/Texture_ktx.cpp new file mode 100644 index 0000000000..28de0c70eb --- /dev/null +++ b/libraries/gpu/src/gpu/Texture_ktx.cpp @@ -0,0 +1,302 @@ +// +// Texture_ktx.cpp +// libraries/gpu/src/gpu +// +// Created by Sam Gateau on 2/16/2017. +// Copyright 2014 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + + +#include "Texture.h" + +#include +using namespace gpu; + +using PixelsPointer = Texture::PixelsPointer; +using KtxStorage = Texture::KtxStorage; + +struct GPUKTXPayload { + Sampler::Desc _samplerDesc; + Texture::Usage _usage; + TextureUsageType _usageType; + + static std::string KEY; + static bool isGPUKTX(const ktx::KeyValue& val) { + return (val._key.compare(KEY) == 0); + } + + static bool findInKeyValues(const ktx::KeyValues& keyValues, GPUKTXPayload& payload) { + auto found = std::find_if(keyValues.begin(), keyValues.end(), isGPUKTX); + if (found != keyValues.end()) { + if ((*found)._value.size() == sizeof(GPUKTXPayload)) { + memcpy(&payload, (*found)._value.data(), sizeof(GPUKTXPayload)); + return true; + } + } + return false; + } +}; + +std::string GPUKTXPayload::KEY { "hifi.gpu" }; + +KtxStorage::KtxStorage(const std::string& filename) : _filename(filename) { + { + ktx::StoragePointer storage { new storage::FileStorage(_filename.c_str()) }; + auto ktxPointer = ktx::KTX::create(storage); + _ktxDescriptor.reset(new ktx::KTXDescriptor(ktxPointer->toDescriptor())); + } + + // now that we know the ktx, let's get the header info to configure this Texture::Storage: + Format mipFormat = Format::COLOR_BGRA_32; + Format texelFormat = Format::COLOR_SRGBA_32; + if (Texture::evalTextureFormat(_ktxDescriptor->header, mipFormat, texelFormat)) { + _format = mipFormat; + } +} + +PixelsPointer KtxStorage::getMipFace(uint16 level, uint8 face) const { + storage::StoragePointer result; + auto faceOffset = _ktxDescriptor->getMipFaceTexelsOffset(level, face); + auto faceSize = _ktxDescriptor->getMipFaceTexelsSize(level, face); + if (faceSize != 0 && faceOffset != 0) { + result = std::make_shared(_filename.c_str())->createView(faceSize, faceOffset)->toMemoryStorage(); + } + return result; +} + +Size KtxStorage::getMipFaceSize(uint16 level, uint8 face) const { + return _ktxDescriptor->getMipFaceTexelsSize(level, face); +} + +void Texture::setKtxBacking(const std::string& filename) { + // Check the KTX file for validity before using it as backing storage + { + ktx::StoragePointer storage { new storage::FileStorage(filename.c_str()) }; + auto ktxPointer = ktx::KTX::create(storage); + if (!ktxPointer) { + return; + } + } + + auto newBacking = std::unique_ptr(new KtxStorage(filename)); + setStorage(newBacking); +} + +ktx::KTXUniquePointer Texture::serialize(const Texture& texture) { + ktx::Header header; + + // From texture format to ktx format description + auto texelFormat = texture.getTexelFormat(); + auto mipFormat = texture.getStoredMipFormat(); + + if (!Texture::evalKTXFormat(mipFormat, texelFormat, header)) { + return nullptr; + } + + // Set Dimensions + uint32_t numFaces = 1; + switch (texture.getType()) { + case TEX_1D: { + if (texture.isArray()) { + header.set1DArray(texture.getWidth(), texture.getNumSlices()); + } else { + header.set1D(texture.getWidth()); + } + break; + } + case TEX_2D: { + if (texture.isArray()) { + header.set2DArray(texture.getWidth(), texture.getHeight(), texture.getNumSlices()); + } else { + header.set2D(texture.getWidth(), texture.getHeight()); + } + break; + } + case TEX_3D: { + if (texture.isArray()) { + header.set3DArray(texture.getWidth(), texture.getHeight(), texture.getDepth(), texture.getNumSlices()); + } else { + header.set3D(texture.getWidth(), texture.getHeight(), texture.getDepth()); + } + break; + } + case TEX_CUBE: { + if (texture.isArray()) { + header.setCubeArray(texture.getWidth(), texture.getHeight(), texture.getNumSlices()); + } else { + header.setCube(texture.getWidth(), texture.getHeight()); + } + numFaces = Texture::CUBE_FACE_COUNT; + break; + } + default: + return nullptr; + } + + // Number level of mips coming + header.numberOfMipmapLevels = texture.getNumMips(); + + ktx::Images images; + for (uint32_t level = 0; level < header.numberOfMipmapLevels; level++) { + auto mip = texture.accessStoredMipFace(level); + if (mip) { + if (numFaces == 1) { + images.emplace_back(ktx::Image((uint32_t)mip->getSize(), 0, mip->readData())); + } else { + ktx::Image::FaceBytes cubeFaces(Texture::CUBE_FACE_COUNT); + cubeFaces[0] = mip->readData(); + for (uint32_t face = 1; face < Texture::CUBE_FACE_COUNT; face++) { + cubeFaces[face] = texture.accessStoredMipFace(level, face)->readData(); + } + images.emplace_back(ktx::Image((uint32_t)mip->getSize(), 0, cubeFaces)); + } + } + } + + GPUKTXPayload keyval; + keyval._samplerDesc = texture.getSampler().getDesc(); + keyval._usage = texture.getUsage(); + keyval._usageType = texture.getUsageType(); + ktx::KeyValues keyValues; + keyValues.emplace_back(ktx::KeyValue(GPUKTXPayload::KEY, sizeof(GPUKTXPayload), (ktx::Byte*) &keyval)); + + auto ktxBuffer = ktx::KTX::create(header, images, keyValues); +#if 0 + auto expectedMipCount = texture.evalNumMips(); + assert(expectedMipCount == ktxBuffer->_images.size()); + assert(expectedMipCount == header.numberOfMipmapLevels); + + assert(0 == memcmp(&header, ktxBuffer->getHeader(), sizeof(ktx::Header))); + assert(ktxBuffer->_images.size() == images.size()); + auto start = ktxBuffer->_storage->data(); + for (size_t i = 0; i < images.size(); ++i) { + auto expected = images[i]; + auto actual = ktxBuffer->_images[i]; + assert(expected._padding == actual._padding); + assert(expected._numFaces == actual._numFaces); + assert(expected._imageSize == actual._imageSize); + assert(expected._faceSize == actual._faceSize); + assert(actual._faceBytes.size() == actual._numFaces); + for (uint32_t face = 0; face < expected._numFaces; ++face) { + auto expectedFace = expected._faceBytes[face]; + auto actualFace = actual._faceBytes[face]; + auto offset = actualFace - start; + assert(offset % 4 == 0); + assert(expectedFace != actualFace); + assert(0 == memcmp(expectedFace, actualFace, expected._faceSize)); + } + } +#endif + return ktxBuffer; +} + +Texture* Texture::unserialize(const std::string& ktxfile, TextureUsageType usageType, Usage usage, const Sampler::Desc& sampler) { + std::unique_ptr ktxPointer = ktx::KTX::create(ktx::StoragePointer { new storage::FileStorage(ktxfile.c_str()) }); + if (!ktxPointer) { + return nullptr; + } + + ktx::KTXDescriptor descriptor { ktxPointer->toDescriptor() }; + const auto& header = descriptor.header; + + Format mipFormat = Format::COLOR_BGRA_32; + Format texelFormat = Format::COLOR_SRGBA_32; + + if (!Texture::evalTextureFormat(header, mipFormat, texelFormat)) { + return nullptr; + } + + // Find Texture Type based on dimensions + Type type = TEX_1D; + if (header.pixelWidth == 0) { + return nullptr; + } else if (header.pixelHeight == 0) { + type = TEX_1D; + } else if (header.pixelDepth == 0) { + if (header.numberOfFaces == ktx::NUM_CUBEMAPFACES) { + type = TEX_CUBE; + } else { + type = TEX_2D; + } + } else { + type = TEX_3D; + } + + + // If found, use the + GPUKTXPayload gpuktxKeyValue; + bool isGPUKTXPayload = GPUKTXPayload::findInKeyValues(descriptor.keyValues, gpuktxKeyValue); + + auto tex = Texture::create( (isGPUKTXPayload ? gpuktxKeyValue._usageType : usageType), + type, + texelFormat, + header.getPixelWidth(), + header.getPixelHeight(), + header.getPixelDepth(), + 1, // num Samples + header.getNumberOfSlices(), + header.getNumberOfLevels(), + (isGPUKTXPayload ? gpuktxKeyValue._samplerDesc : sampler)); + + tex->setUsage((isGPUKTXPayload ? gpuktxKeyValue._usage : usage)); + + // Assing the mips availables + tex->setStoredMipFormat(mipFormat); + tex->setKtxBacking(ktxfile); + return tex; +} + +bool Texture::evalKTXFormat(const Element& mipFormat, const Element& texelFormat, ktx::Header& header) { + if (texelFormat == Format::COLOR_RGBA_32 && mipFormat == Format::COLOR_BGRA_32) { + header.setUncompressed(ktx::GLType::UNSIGNED_BYTE, 1, ktx::GLFormat::BGRA, ktx::GLInternalFormat_Uncompressed::RGBA8, ktx::GLBaseInternalFormat::RGBA); + } else if (texelFormat == Format::COLOR_RGBA_32 && mipFormat == Format::COLOR_RGBA_32) { + header.setUncompressed(ktx::GLType::UNSIGNED_BYTE, 1, ktx::GLFormat::RGBA, ktx::GLInternalFormat_Uncompressed::RGBA8, ktx::GLBaseInternalFormat::RGBA); + } else if (texelFormat == Format::COLOR_SRGBA_32 && mipFormat == Format::COLOR_SBGRA_32) { + header.setUncompressed(ktx::GLType::UNSIGNED_BYTE, 1, ktx::GLFormat::BGRA, ktx::GLInternalFormat_Uncompressed::SRGB8_ALPHA8, ktx::GLBaseInternalFormat::RGBA); + } else if (texelFormat == Format::COLOR_SRGBA_32 && mipFormat == Format::COLOR_SRGBA_32) { + header.setUncompressed(ktx::GLType::UNSIGNED_BYTE, 1, ktx::GLFormat::RGBA, ktx::GLInternalFormat_Uncompressed::SRGB8_ALPHA8, ktx::GLBaseInternalFormat::RGBA); + } else if (texelFormat == Format::COLOR_R_8 && mipFormat == Format::COLOR_R_8) { + header.setUncompressed(ktx::GLType::UNSIGNED_BYTE, 1, ktx::GLFormat::RED, ktx::GLInternalFormat_Uncompressed::R8, ktx::GLBaseInternalFormat::RED); + } else { + return false; + } + + return true; +} + +bool Texture::evalTextureFormat(const ktx::Header& header, Element& mipFormat, Element& texelFormat) { + if (header.getGLFormat() == ktx::GLFormat::BGRA && header.getGLType() == ktx::GLType::UNSIGNED_BYTE && header.getTypeSize() == 1) { + if (header.getGLInternaFormat_Uncompressed() == ktx::GLInternalFormat_Uncompressed::RGBA8) { + mipFormat = Format::COLOR_BGRA_32; + texelFormat = Format::COLOR_RGBA_32; + } else if (header.getGLInternaFormat_Uncompressed() == ktx::GLInternalFormat_Uncompressed::SRGB8_ALPHA8) { + mipFormat = Format::COLOR_SBGRA_32; + texelFormat = Format::COLOR_SRGBA_32; + } else { + return false; + } + } else if (header.getGLFormat() == ktx::GLFormat::RGBA && header.getGLType() == ktx::GLType::UNSIGNED_BYTE && header.getTypeSize() == 1) { + if (header.getGLInternaFormat_Uncompressed() == ktx::GLInternalFormat_Uncompressed::RGBA8) { + mipFormat = Format::COLOR_RGBA_32; + texelFormat = Format::COLOR_RGBA_32; + } else if (header.getGLInternaFormat_Uncompressed() == ktx::GLInternalFormat_Uncompressed::SRGB8_ALPHA8) { + mipFormat = Format::COLOR_SRGBA_32; + texelFormat = Format::COLOR_SRGBA_32; + } else { + return false; + } + } else if (header.getGLFormat() == ktx::GLFormat::RED && header.getGLType() == ktx::GLType::UNSIGNED_BYTE && header.getTypeSize() == 1) { + mipFormat = Format::COLOR_R_8; + if (header.getGLInternaFormat_Uncompressed() == ktx::GLInternalFormat_Uncompressed::R8) { + texelFormat = Format::COLOR_R_8; + } else { + return false; + } + } else { + return false; + } + return true; +} diff --git a/libraries/input-plugins/src/input-plugins/KeyboardMouseDevice.cpp b/libraries/input-plugins/src/input-plugins/KeyboardMouseDevice.cpp index ddb2f482a1..b5a2fc6b3c 100755 --- a/libraries/input-plugins/src/input-plugins/KeyboardMouseDevice.cpp +++ b/libraries/input-plugins/src/input-plugins/KeyboardMouseDevice.cpp @@ -25,6 +25,7 @@ void KeyboardMouseDevice::pluginUpdate(float deltaTime, const controller::InputC auto userInputMapper = DependencyManager::get(); userInputMapper->withLock([&, this]() { _inputDevice->update(deltaTime, inputCalibrationData); + eraseMouseClicked(); _inputDevice->_axisStateMap[MOUSE_AXIS_X] = _lastCursor.x(); _inputDevice->_axisStateMap[MOUSE_AXIS_Y] = _lastCursor.y(); @@ -78,8 +79,6 @@ void KeyboardMouseDevice::mousePressEvent(QMouseEvent* event) { _mousePressPos = event->pos(); _clickDeadspotActive = true; - - eraseMouseClicked(); } void KeyboardMouseDevice::mouseReleaseEvent(QMouseEvent* event) { @@ -122,7 +121,6 @@ void KeyboardMouseDevice::mouseMoveEvent(QMouseEvent* event) { const int CLICK_EVENT_DEADSPOT = 6; // pixels if (_clickDeadspotActive && (_mousePressPos - currentPos).manhattanLength() > CLICK_EVENT_DEADSPOT) { - eraseMouseClicked(); _clickDeadspotActive = false; } } diff --git a/libraries/ktx/CMakeLists.txt b/libraries/ktx/CMakeLists.txt new file mode 100644 index 0000000000..404660b247 --- /dev/null +++ b/libraries/ktx/CMakeLists.txt @@ -0,0 +1,3 @@ +set(TARGET_NAME ktx) +setup_hifi_library() +link_hifi_libraries() \ No newline at end of file diff --git a/libraries/ktx/src/ktx/KTX.cpp b/libraries/ktx/src/ktx/KTX.cpp new file mode 100644 index 0000000000..6fca39788b --- /dev/null +++ b/libraries/ktx/src/ktx/KTX.cpp @@ -0,0 +1,212 @@ +// +// KTX.cpp +// ktx/src/ktx +// +// Created by Zach Pomerantz on 2/08/2017. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include "KTX.h" + +#include //min max and more + +using namespace ktx; + +uint32_t Header::evalPadding(size_t byteSize) { + //auto padding = byteSize % PACKING_SIZE; + // return (uint32_t) (padding ? PACKING_SIZE - padding : 0); + return (uint32_t) (3 - (byteSize + 3) % PACKING_SIZE);// padding ? PACKING_SIZE - padding : 0); +} + + +const Header::Identifier ktx::Header::IDENTIFIER {{ + 0xAB, 0x4B, 0x54, 0x58, 0x20, 0x31, 0x31, 0xBB, 0x0D, 0x0A, 0x1A, 0x0A +}}; + +Header::Header() { + memcpy(identifier, IDENTIFIER.data(), IDENTIFIER_LENGTH); +} + +uint32_t Header::evalMaxDimension() const { + return std::max(getPixelWidth(), std::max(getPixelHeight(), getPixelDepth())); +} + +uint32_t Header::evalPixelWidth(uint32_t level) const { + return std::max(getPixelWidth() >> level, 1U); +} +uint32_t Header::evalPixelHeight(uint32_t level) const { + return std::max(getPixelHeight() >> level, 1U); +} +uint32_t Header::evalPixelDepth(uint32_t level) const { + return std::max(getPixelDepth() >> level, 1U); +} + +size_t Header::evalPixelSize() const { + return glTypeSize; // Really we should generate the size from the FOrmat etc +} + +size_t Header::evalRowSize(uint32_t level) const { + auto pixWidth = evalPixelWidth(level); + auto pixSize = evalPixelSize(); + auto netSize = pixWidth * pixSize; + auto padding = evalPadding(netSize); + return netSize + padding; +} +size_t Header::evalFaceSize(uint32_t level) const { + auto pixHeight = evalPixelHeight(level); + auto pixDepth = evalPixelDepth(level); + auto rowSize = evalRowSize(level); + return pixDepth * pixHeight * rowSize; +} +size_t Header::evalImageSize(uint32_t level) const { + auto faceSize = evalFaceSize(level); + if (numberOfFaces == NUM_CUBEMAPFACES && numberOfArrayElements == 0) { + return faceSize; + } else { + return (getNumberOfSlices() * numberOfFaces * faceSize); + } +} + + +KeyValue::KeyValue(const std::string& key, uint32_t valueByteSize, const Byte* value) : + _byteSize((uint32_t) key.size() + 1 + valueByteSize), // keyString size + '\0' ending char + the value size + _key(key), + _value(valueByteSize) +{ + if (_value.size() && value) { + memcpy(_value.data(), value, valueByteSize); + } +} + +KeyValue::KeyValue(const std::string& key, const std::string& value) : + KeyValue(key, (uint32_t) value.size(), (const Byte*) value.data()) +{ + +} + +uint32_t KeyValue::serializedByteSize() const { + return (uint32_t) (sizeof(uint32_t) + _byteSize + Header::evalPadding(_byteSize)); +} + +uint32_t KeyValue::serializedKeyValuesByteSize(const KeyValues& keyValues) { + uint32_t keyValuesSize = 0; + for (auto& keyval : keyValues) { + keyValuesSize += keyval.serializedByteSize(); + } + return (keyValuesSize + Header::evalPadding(keyValuesSize)); +} + + +KTX::KTX() { +} + +KTX::~KTX() { +} + +void KTX::resetStorage(const StoragePointer& storage) { + _storage = storage; + if (_storage->size() >= sizeof(Header)) { + memcpy(&_header, _storage->data(), sizeof(Header)); + } +} + +const Header& KTX::getHeader() const { + return _header; +} + + +size_t KTX::getKeyValueDataSize() const { + return _header.bytesOfKeyValueData; +} + +size_t KTX::getTexelsDataSize() const { + if (!_storage) { + return 0; + } + return (_storage->data() + _storage->size()) - getTexelsData(); +} + +const Byte* KTX::getKeyValueData() const { + if (!_storage) { + return nullptr; + } + return (_storage->data() + sizeof(Header)); +} + +const Byte* KTX::getTexelsData() const { + if (!_storage) { + return nullptr; + } + return (_storage->data() + sizeof(Header) + getKeyValueDataSize()); +} + +storage::StoragePointer KTX::getMipFaceTexelsData(uint16_t mip, uint8_t face) const { + storage::StoragePointer result; + if (mip < _images.size()) { + const auto& faces = _images[mip]; + if (face < faces._numFaces) { + auto faceOffset = faces._faceBytes[face] - _storage->data(); + auto faceSize = faces._faceSize; + result = _storage->createView(faceSize, faceOffset); + } + } + return result; +} + +size_t KTXDescriptor::getMipFaceTexelsSize(uint16_t mip, uint8_t face) const { + size_t result { 0 }; + if (mip < images.size()) { + const auto& faces = images[mip]; + if (face < faces._numFaces) { + result = faces._faceSize; + } + } + return result; +} + +size_t KTXDescriptor::getMipFaceTexelsOffset(uint16_t mip, uint8_t face) const { + size_t result { 0 }; + if (mip < images.size()) { + const auto& faces = images[mip]; + if (face < faces._numFaces) { + result = faces._faceOffsets[face]; + } + } + return result; +} + +ImageDescriptor Image::toImageDescriptor(const Byte* baseAddress) const { + FaceOffsets offsets; + offsets.resize(_faceBytes.size()); + for (size_t face = 0; face < _numFaces; ++face) { + offsets[face] = _faceBytes[face] - baseAddress; + } + // Note, implicit cast of *this to const ImageHeader& + return ImageDescriptor(*this, offsets); +} + +Image ImageDescriptor::toImage(const ktx::StoragePointer& storage) const { + FaceBytes faces; + faces.resize(_faceOffsets.size()); + for (size_t face = 0; face < _numFaces; ++face) { + faces[face] = storage->data() + _faceOffsets[face]; + } + // Note, implicit cast of *this to const ImageHeader& + return Image(*this, faces); +} + +KTXDescriptor KTX::toDescriptor() const { + ImageDescriptors newDescriptors; + auto storageStart = _storage ? _storage->data() : nullptr; + for (size_t i = 0; i < _images.size(); ++i) { + newDescriptors.emplace_back(_images[i].toImageDescriptor(storageStart)); + } + return { this->_header, this->_keyValues, newDescriptors }; +} + +KTX::KTX(const StoragePointer& storage, const Header& header, const KeyValues& keyValues, const Images& images) + : _header(header), _storage(storage), _keyValues(keyValues), _images(images) { +} \ No newline at end of file diff --git a/libraries/ktx/src/ktx/KTX.h b/libraries/ktx/src/ktx/KTX.h new file mode 100644 index 0000000000..f09986991a --- /dev/null +++ b/libraries/ktx/src/ktx/KTX.h @@ -0,0 +1,525 @@ +// +// KTX.h +// ktx/src/ktx +// +// Created by Zach Pomerantz on 2/08/2017. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// +#pragma once +#ifndef hifi_ktx_KTX_h +#define hifi_ktx_KTX_h + +#include +#include +#include +#include +#include +#include +#include + +#include + +/* KTX Spec: + +Byte[12] identifier +UInt32 endianness +UInt32 glType +UInt32 glTypeSize +UInt32 glFormat +Uint32 glInternalFormat +Uint32 glBaseInternalFormat +UInt32 pixelWidth +UInt32 pixelHeight +UInt32 pixelDepth +UInt32 numberOfArrayElements +UInt32 numberOfFaces +UInt32 numberOfMipmapLevels +UInt32 bytesOfKeyValueData + +for each keyValuePair that fits in bytesOfKeyValueData + UInt32 keyAndValueByteSize + Byte keyAndValue[keyAndValueByteSize] + Byte valuePadding[3 - ((keyAndValueByteSize + 3) % 4)] +end + +for each mipmap_level in numberOfMipmapLevels* + UInt32 imageSize; + for each array_element in numberOfArrayElements* + for each face in numberOfFaces + for each z_slice in pixelDepth* + for each row or row_of_blocks in pixelHeight* + for each pixel or block_of_pixels in pixelWidth + Byte data[format-specific-number-of-bytes]** + end + end + end + Byte cubePadding[0-3] + end + end + Byte mipPadding[3 - ((imageSize + 3) % 4)] +end + +* Replace with 1 if this field is 0. + +** Uncompressed texture data matches a GL_UNPACK_ALIGNMENT of 4. +*/ + + + +namespace ktx { + const uint32_t PACKING_SIZE { sizeof(uint32_t) }; + using Byte = uint8_t; + + enum class GLType : uint32_t { + COMPRESSED_TYPE = 0, + + // GL 4.4 Table 8.2 + UNSIGNED_BYTE = 0x1401, + BYTE = 0x1400, + UNSIGNED_SHORT = 0x1403, + SHORT = 0x1402, + UNSIGNED_INT = 0x1405, + INT = 0x1404, + HALF_FLOAT = 0x140B, + FLOAT = 0x1406, + UNSIGNED_BYTE_3_3_2 = 0x8032, + UNSIGNED_BYTE_2_3_3_REV = 0x8362, + UNSIGNED_SHORT_5_6_5 = 0x8363, + UNSIGNED_SHORT_5_6_5_REV = 0x8364, + UNSIGNED_SHORT_4_4_4_4 = 0x8033, + UNSIGNED_SHORT_4_4_4_4_REV = 0x8365, + UNSIGNED_SHORT_5_5_5_1 = 0x8034, + UNSIGNED_SHORT_1_5_5_5_REV = 0x8366, + UNSIGNED_INT_8_8_8_8 = 0x8035, + UNSIGNED_INT_8_8_8_8_REV = 0x8367, + UNSIGNED_INT_10_10_10_2 = 0x8036, + UNSIGNED_INT_2_10_10_10_REV = 0x8368, + UNSIGNED_INT_24_8 = 0x84FA, + UNSIGNED_INT_10F_11F_11F_REV = 0x8C3B, + UNSIGNED_INT_5_9_9_9_REV = 0x8C3E, + FLOAT_32_UNSIGNED_INT_24_8_REV = 0x8DAD, + + NUM_GLTYPES = 25, + }; + + enum class GLFormat : uint32_t { + COMPRESSED_FORMAT = 0, + + // GL 4.4 Table 8.3 + STENCIL_INDEX = 0x1901, + DEPTH_COMPONENT = 0x1902, + DEPTH_STENCIL = 0x84F9, + + RED = 0x1903, + GREEN = 0x1904, + BLUE = 0x1905, + RG = 0x8227, + RGB = 0x1907, + RGBA = 0x1908, + BGR = 0x80E0, + BGRA = 0x80E1, + + RG_INTEGER = 0x8228, + RED_INTEGER = 0x8D94, + GREEN_INTEGER = 0x8D95, + BLUE_INTEGER = 0x8D96, + RGB_INTEGER = 0x8D98, + RGBA_INTEGER = 0x8D99, + BGR_INTEGER = 0x8D9A, + BGRA_INTEGER = 0x8D9B, + + NUM_GLFORMATS = 20, + }; + + enum class GLInternalFormat_Uncompressed : uint32_t { + // GL 4.4 Table 8.12 + R8 = 0x8229, + R8_SNORM = 0x8F94, + + R16 = 0x822A, + R16_SNORM = 0x8F98, + + RG8 = 0x822B, + RG8_SNORM = 0x8F95, + + RG16 = 0x822C, + RG16_SNORM = 0x8F99, + + R3_G3_B2 = 0x2A10, + RGB4 = 0x804F, + RGB5 = 0x8050, + RGB565 = 0x8D62, + + RGB8 = 0x8051, + RGB8_SNORM = 0x8F96, + RGB10 = 0x8052, + RGB12 = 0x8053, + + RGB16 = 0x8054, + RGB16_SNORM = 0x8F9A, + + RGBA2 = 0x8055, + RGBA4 = 0x8056, + RGB5_A1 = 0x8057, + RGBA8 = 0x8058, + RGBA8_SNORM = 0x8F97, + + RGB10_A2 = 0x8059, + RGB10_A2UI = 0x906F, + + RGBA12 = 0x805A, + RGBA16 = 0x805B, + RGBA16_SNORM = 0x8F9B, + + SRGB8 = 0x8C41, + SRGB8_ALPHA8 = 0x8C43, + + R16F = 0x822D, + RG16F = 0x822F, + RGB16F = 0x881B, + RGBA16F = 0x881A, + + R32F = 0x822E, + RG32F = 0x8230, + RGB32F = 0x8815, + RGBA32F = 0x8814, + + R11F_G11F_B10F = 0x8C3A, + RGB9_E5 = 0x8C3D, + + + R8I = 0x8231, + R8UI = 0x8232, + R16I = 0x8233, + R16UI = 0x8234, + R32I = 0x8235, + R32UI = 0x8236, + RG8I = 0x8237, + RG8UI = 0x8238, + RG16I = 0x8239, + RG16UI = 0x823A, + RG32I = 0x823B, + RG32UI = 0x823C, + + RGB8I = 0x8D8F, + RGB8UI = 0x8D7D, + RGB16I = 0x8D89, + RGB16UI = 0x8D77, + + RGB32I = 0x8D83, + RGB32UI = 0x8D71, + RGBA8I = 0x8D8E, + RGBA8UI = 0x8D7C, + RGBA16I = 0x8D88, + RGBA16UI = 0x8D76, + RGBA32I = 0x8D82, + + RGBA32UI = 0x8D70, + + // GL 4.4 Table 8.13 + DEPTH_COMPONENT16 = 0x81A5, + DEPTH_COMPONENT24 = 0x81A6, + DEPTH_COMPONENT32 = 0x81A7, + + DEPTH_COMPONENT32F = 0x8CAC, + DEPTH24_STENCIL8 = 0x88F0, + DEPTH32F_STENCIL8 = 0x8CAD, + + STENCIL_INDEX1 = 0x8D46, + STENCIL_INDEX4 = 0x8D47, + STENCIL_INDEX8 = 0x8D48, + STENCIL_INDEX16 = 0x8D49, + + NUM_UNCOMPRESSED_GLINTERNALFORMATS = 74, + }; + + enum class GLInternalFormat_Compressed : uint32_t { + // GL 4.4 Table 8.14 + COMPRESSED_RED = 0x8225, + COMPRESSED_RG = 0x8226, + COMPRESSED_RGB = 0x84ED, + COMPRESSED_RGBA = 0x84EE, + + COMPRESSED_SRGB = 0x8C48, + COMPRESSED_SRGB_ALPHA = 0x8C49, + + COMPRESSED_RED_RGTC1 = 0x8DBB, + COMPRESSED_SIGNED_RED_RGTC1 = 0x8DBC, + COMPRESSED_RG_RGTC2 = 0x8DBD, + COMPRESSED_SIGNED_RG_RGTC2 = 0x8DBE, + + COMPRESSED_RGBA_BPTC_UNORM = 0x8E8C, + COMPRESSED_SRGB_ALPHA_BPTC_UNORM = 0x8E8D, + COMPRESSED_RGB_BPTC_SIGNED_FLOAT = 0x8E8E, + COMPRESSED_RGB_BPTC_UNSIGNED_FLOAT = 0x8E8F, + + COMPRESSED_RGB8_ETC2 = 0x9274, + COMPRESSED_SRGB8_ETC2 = 0x9275, + COMPRESSED_RGB8_PUNCHTHROUGH_ALPHA1_ETC2 = 0x9276, + COMPRESSED_SRGB8_PUNCHTHROUGH_ALPHA1_ETC2 = 0x9277, + COMPRESSED_RGBA8_ETC2_EAC = 0x9278, + COMPRESSED_SRGB8_ALPHA8_ETC2_EAC = 0x9279, + + COMPRESSED_R11_EAC = 0x9270, + COMPRESSED_SIGNED_R11_EAC = 0x9271, + COMPRESSED_RG11_EAC = 0x9272, + COMPRESSED_SIGNED_RG11_EAC = 0x9273, + + NUM_COMPRESSED_GLINTERNALFORMATS = 24, + }; + + enum class GLBaseInternalFormat : uint32_t { + // GL 4.4 Table 8.11 + DEPTH_COMPONENT = 0x1902, + DEPTH_STENCIL = 0x84F9, + RED = 0x1903, + RG = 0x8227, + RGB = 0x1907, + RGBA = 0x1908, + STENCIL_INDEX = 0x1901, + + NUM_GLBASEINTERNALFORMATS = 7, + }; + + enum CubeMapFace { + POS_X = 0, + NEG_X = 1, + POS_Y = 2, + NEG_Y = 3, + POS_Z = 4, + NEG_Z = 5, + NUM_CUBEMAPFACES = 6, + }; + + using Storage = storage::Storage; + using StoragePointer = std::shared_ptr; + + // Header + struct Header { + static const size_t IDENTIFIER_LENGTH = 12; + using Identifier = std::array; + static const Identifier IDENTIFIER; + + static const uint32_t ENDIAN_TEST = 0x04030201; + static const uint32_t REVERSE_ENDIAN_TEST = 0x01020304; + + static uint32_t evalPadding(size_t byteSize); + + Header(); + + Byte identifier[IDENTIFIER_LENGTH]; + uint32_t endianness { ENDIAN_TEST }; + + uint32_t glType; + uint32_t glTypeSize { 0 }; + uint32_t glFormat; + uint32_t glInternalFormat; + uint32_t glBaseInternalFormat; + + uint32_t pixelWidth { 1 }; + uint32_t pixelHeight { 0 }; + uint32_t pixelDepth { 0 }; + uint32_t numberOfArrayElements { 0 }; + uint32_t numberOfFaces { 1 }; + uint32_t numberOfMipmapLevels { 1 }; + + uint32_t bytesOfKeyValueData { 0 }; + + uint32_t getPixelWidth() const { return (pixelWidth ? pixelWidth : 1); } + uint32_t getPixelHeight() const { return (pixelHeight ? pixelHeight : 1); } + uint32_t getPixelDepth() const { return (pixelDepth ? pixelDepth : 1); } + uint32_t getNumberOfSlices() const { return (numberOfArrayElements ? numberOfArrayElements : 1); } + uint32_t getNumberOfLevels() const { return (numberOfMipmapLevels ? numberOfMipmapLevels : 1); } + + uint32_t evalMaxDimension() const; + uint32_t evalPixelWidth(uint32_t level) const; + uint32_t evalPixelHeight(uint32_t level) const; + uint32_t evalPixelDepth(uint32_t level) const; + + size_t evalPixelSize() const; + size_t evalRowSize(uint32_t level) const; + size_t evalFaceSize(uint32_t level) const; + size_t evalImageSize(uint32_t level) const; + + void setUncompressed(GLType type, uint32_t typeSize, GLFormat format, GLInternalFormat_Uncompressed internalFormat, GLBaseInternalFormat baseInternalFormat) { + glType = (uint32_t) type; + glTypeSize = typeSize; + glFormat = (uint32_t) format; + glInternalFormat = (uint32_t) internalFormat; + glBaseInternalFormat = (uint32_t) baseInternalFormat; + } + void setCompressed(GLInternalFormat_Compressed internalFormat, GLBaseInternalFormat baseInternalFormat) { + glType = (uint32_t) GLType::COMPRESSED_TYPE; + glTypeSize = 1; + glFormat = (uint32_t) GLFormat::COMPRESSED_FORMAT; + glInternalFormat = (uint32_t) internalFormat; + glBaseInternalFormat = (uint32_t) baseInternalFormat; + } + + GLType getGLType() const { return (GLType)glType; } + uint32_t getTypeSize() const { return glTypeSize; } + GLFormat getGLFormat() const { return (GLFormat)glFormat; } + GLInternalFormat_Uncompressed getGLInternaFormat_Uncompressed() const { return (GLInternalFormat_Uncompressed)glInternalFormat; } + GLInternalFormat_Compressed getGLInternaFormat_Compressed() const { return (GLInternalFormat_Compressed)glInternalFormat; } + GLBaseInternalFormat getGLBaseInternalFormat() const { return (GLBaseInternalFormat)glBaseInternalFormat; } + + + void setDimensions(uint32_t width, uint32_t height = 0, uint32_t depth = 0, uint32_t numSlices = 0, uint32_t numFaces = 1) { + pixelWidth = (width > 0 ? width : 1); + pixelHeight = height; + pixelDepth = depth; + numberOfArrayElements = numSlices; + numberOfFaces = ((numFaces == 1) || (numFaces == NUM_CUBEMAPFACES) ? numFaces : 1); + } + void set1D(uint32_t width) { setDimensions(width); } + void set1DArray(uint32_t width, uint32_t numSlices) { setDimensions(width, 0, 0, (numSlices > 0 ? numSlices : 1)); } + void set2D(uint32_t width, uint32_t height) { setDimensions(width, height); } + void set2DArray(uint32_t width, uint32_t height, uint32_t numSlices) { setDimensions(width, height, 0, (numSlices > 0 ? numSlices : 1)); } + void set3D(uint32_t width, uint32_t height, uint32_t depth) { setDimensions(width, height, depth); } + void set3DArray(uint32_t width, uint32_t height, uint32_t depth, uint32_t numSlices) { setDimensions(width, height, depth, (numSlices > 0 ? numSlices : 1)); } + void setCube(uint32_t width, uint32_t height) { setDimensions(width, height, 0, 0, NUM_CUBEMAPFACES); } + void setCubeArray(uint32_t width, uint32_t height, uint32_t numSlices) { setDimensions(width, height, 0, (numSlices > 0 ? numSlices : 1), NUM_CUBEMAPFACES); } + + }; + + // Key Values + struct KeyValue { + uint32_t _byteSize { 0 }; + std::string _key; + std::vector _value; + + + KeyValue(const std::string& key, uint32_t valueByteSize, const Byte* value); + + KeyValue(const std::string& key, const std::string& value); + + uint32_t serializedByteSize() const; + + static KeyValue parseSerializedKeyAndValue(uint32_t srcSize, const Byte* srcBytes); + static uint32_t writeSerializedKeyAndValue(Byte* destBytes, uint32_t destByteSize, const KeyValue& keyval); + + using KeyValues = std::list; + static uint32_t serializedKeyValuesByteSize(const KeyValues& keyValues); + + }; + using KeyValues = KeyValue::KeyValues; + + struct ImageHeader { + using FaceOffsets = std::vector; + using FaceBytes = std::vector; + const uint32_t _numFaces; + const uint32_t _imageSize; + const uint32_t _faceSize; + const uint32_t _padding; + ImageHeader(bool cube, uint32_t imageSize, uint32_t padding) : + _numFaces(cube ? NUM_CUBEMAPFACES : 1), + _imageSize(imageSize * _numFaces), + _faceSize(imageSize), + _padding(padding) { + } + }; + + struct Image; + + struct ImageDescriptor : public ImageHeader { + const FaceOffsets _faceOffsets; + ImageDescriptor(const ImageHeader& header, const FaceOffsets& offsets) : ImageHeader(header), _faceOffsets(offsets) {} + Image toImage(const ktx::StoragePointer& storage) const; + }; + + using ImageDescriptors = std::vector; + + struct Image : public ImageHeader { + FaceBytes _faceBytes; + Image(const ImageHeader& header, const FaceBytes& faces) : ImageHeader(header), _faceBytes(faces) {} + Image(uint32_t imageSize, uint32_t padding, const Byte* bytes) : + ImageHeader(false, imageSize, padding), + _faceBytes(1, bytes) {} + Image(uint32_t pageSize, uint32_t padding, const FaceBytes& cubeFaceBytes) : + ImageHeader(true, pageSize, padding) + { + if (cubeFaceBytes.size() == NUM_CUBEMAPFACES) { + _faceBytes = cubeFaceBytes; + } + } + + ImageDescriptor toImageDescriptor(const Byte* baseAddress) const; + }; + + using Images = std::vector; + + class KTX; + + // A KTX descriptor is a lightweight container for all the information about a serialized KTX file, but without the + // actual image / face data available. + struct KTXDescriptor { + KTXDescriptor(const Header& header, const KeyValues& keyValues, const ImageDescriptors& imageDescriptors) : header(header), keyValues(keyValues), images(imageDescriptors) {} + const Header header; + const KeyValues keyValues; + const ImageDescriptors images; + size_t getMipFaceTexelsSize(uint16_t mip = 0, uint8_t face = 0) const; + size_t getMipFaceTexelsOffset(uint16_t mip = 0, uint8_t face = 0) const; + }; + + class KTX { + void resetStorage(const StoragePointer& src); + + KTX(); + KTX(const StoragePointer& storage, const Header& header, const KeyValues& keyValues, const Images& images); + public: + ~KTX(); + + // Define a KTX object manually to write it somewhere (in a file on disk?) + // This path allocate the Storage where to store header, keyvalues and copy mips + // Then COPY all the data + static std::unique_ptr create(const Header& header, const Images& images, const KeyValues& keyValues = KeyValues()); + + // Instead of creating a full Copy of the src data in a KTX object, the write serialization can be performed with the + // following two functions + // size_t sizeNeeded = KTX::evalStorageSize(header, images); + // + // //allocate a buffer of size "sizeNeeded" or map a file with enough capacity + // Byte* destBytes = new Byte[sizeNeeded]; + // + // // THen perform the writing of the src data to the destinnation buffer + // write(destBytes, sizeNeeded, header, images); + // + // This is exactly what is done in the create function + static size_t evalStorageSize(const Header& header, const Images& images, const KeyValues& keyValues = KeyValues()); + static size_t write(Byte* destBytes, size_t destByteSize, const Header& header, const Images& images, const KeyValues& keyValues = KeyValues()); + static size_t writeKeyValues(Byte* destBytes, size_t destByteSize, const KeyValues& keyValues); + static Images writeImages(Byte* destBytes, size_t destByteSize, const Images& images); + + // Parse a block of memory and create a KTX object from it + static std::unique_ptr create(const StoragePointer& src); + + static bool checkHeaderFromStorage(size_t srcSize, const Byte* srcBytes); + static KeyValues parseKeyValues(size_t srcSize, const Byte* srcBytes); + static Images parseImages(const Header& header, size_t srcSize, const Byte* srcBytes); + + // Access raw pointers to the main sections of the KTX + const Header& getHeader() const; + + const Byte* getKeyValueData() const; + const Byte* getTexelsData() const; + storage::StoragePointer getMipFaceTexelsData(uint16_t mip = 0, uint8_t face = 0) const; + const StoragePointer& getStorage() const { return _storage; } + + KTXDescriptor toDescriptor() const; + size_t getKeyValueDataSize() const; + size_t getTexelsDataSize() const; + + Header _header; + StoragePointer _storage; + KeyValues _keyValues; + Images _images; + + friend struct KTXDescriptor; + }; + +} + +#endif // hifi_ktx_KTX_h diff --git a/libraries/ktx/src/ktx/Reader.cpp b/libraries/ktx/src/ktx/Reader.cpp new file mode 100644 index 0000000000..bf72faeba5 --- /dev/null +++ b/libraries/ktx/src/ktx/Reader.cpp @@ -0,0 +1,195 @@ +// +// Reader.cpp +// ktx/src/ktx +// +// Created by Zach Pomerantz on 2/08/2017. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// +#include "KTX.h" + +#include +#include +#include + +#ifndef _MSC_VER +#define NOEXCEPT noexcept +#else +#define NOEXCEPT +#endif + +namespace ktx { + class ReaderException: public std::exception { + public: + ReaderException(const std::string& explanation) : _explanation("KTX deserialization error: " + explanation) {} + const char* what() const NOEXCEPT override { return _explanation.c_str(); } + private: + const std::string _explanation; + }; + + bool checkEndianness(uint32_t endianness, bool& matching) { + switch (endianness) { + case Header::ENDIAN_TEST: { + matching = true; + return true; + } + break; + case Header::REVERSE_ENDIAN_TEST: + { + matching = false; + return true; + } + break; + default: + throw ReaderException("endianness field has invalid value"); + return false; + } + } + + bool checkIdentifier(const Byte* identifier) { + if (!(0 == memcmp(identifier, Header::IDENTIFIER.data(), Header::IDENTIFIER_LENGTH))) { + throw ReaderException("identifier field invalid"); + return false; + } + return true; + } + + bool KTX::checkHeaderFromStorage(size_t srcSize, const Byte* srcBytes) { + try { + // validation + if (srcSize < sizeof(Header)) { + throw ReaderException("length is too short for header"); + } + const Header* header = reinterpret_cast(srcBytes); + + checkIdentifier(header->identifier); + + bool endianMatch { true }; + checkEndianness(header->endianness, endianMatch); + + // TODO: endian conversion if !endianMatch - for now, this is for local use and is unnecessary + + + // TODO: calculated bytesOfTexData + if (srcSize < (sizeof(Header) + header->bytesOfKeyValueData)) { + throw ReaderException("length is too short for metadata"); + } + + size_t bytesOfTexData = 0; + if (srcSize < (sizeof(Header) + header->bytesOfKeyValueData + bytesOfTexData)) { + + throw ReaderException("length is too short for data"); + } + + return true; + } + catch (const ReaderException& e) { + qWarning() << e.what(); + return false; + } + } + + KeyValue KeyValue::parseSerializedKeyAndValue(uint32_t srcSize, const Byte* srcBytes) { + uint32_t keyAndValueByteSize; + memcpy(&keyAndValueByteSize, srcBytes, sizeof(uint32_t)); + if (keyAndValueByteSize + sizeof(uint32_t) > srcSize) { + throw ReaderException("invalid key-value size"); + } + auto keyValueBytes = srcBytes + sizeof(uint32_t); + + // find the first null character \0 and extract the key + uint32_t keyLength = 0; + while (reinterpret_cast(keyValueBytes)[++keyLength] != '\0') { + if (keyLength == keyAndValueByteSize) { + // key must be null-terminated, and there must be space for the value + throw ReaderException("invalid key-value " + std::string(reinterpret_cast(keyValueBytes), keyLength)); + } + } + uint32_t valueStartOffset = keyLength + 1; + + // parse the key-value + return KeyValue(std::string(reinterpret_cast(keyValueBytes), keyLength), + keyAndValueByteSize - valueStartOffset, keyValueBytes + valueStartOffset); + } + + KeyValues KTX::parseKeyValues(size_t srcSize, const Byte* srcBytes) { + KeyValues keyValues; + try { + auto src = srcBytes; + uint32_t length = (uint32_t) srcSize; + uint32_t offset = 0; + while (offset < length) { + auto keyValue = KeyValue::parseSerializedKeyAndValue(length - offset, src); + keyValues.emplace_back(keyValue); + + // advance offset/src + offset += keyValue.serializedByteSize(); + src += keyValue.serializedByteSize(); + } + } + catch (const ReaderException& e) { + qWarning() << e.what(); + } + return keyValues; + } + + Images KTX::parseImages(const Header& header, size_t srcSize, const Byte* srcBytes) { + Images images; + auto currentPtr = srcBytes; + auto numFaces = header.numberOfFaces; + + // Keep identifying new mip as long as we can at list query the next imageSize + while ((currentPtr - srcBytes) + sizeof(uint32_t) <= (srcSize)) { + + // Grab the imageSize coming up + size_t imageSize = *reinterpret_cast(currentPtr); + currentPtr += sizeof(uint32_t); + + // If enough data ahead then capture the pointer + if ((currentPtr - srcBytes) + imageSize <= (srcSize)) { + auto padding = Header::evalPadding(imageSize); + + if (numFaces == NUM_CUBEMAPFACES) { + size_t faceSize = imageSize / NUM_CUBEMAPFACES; + Image::FaceBytes faces(NUM_CUBEMAPFACES); + for (uint32_t face = 0; face < NUM_CUBEMAPFACES; face++) { + faces[face] = currentPtr; + currentPtr += faceSize; + } + images.emplace_back(Image((uint32_t) faceSize, padding, faces)); + currentPtr += padding; + } else { + images.emplace_back(Image((uint32_t) imageSize, padding, currentPtr)); + currentPtr += imageSize + padding; + } + } else { + break; + } + } + + return images; + } + + std::unique_ptr KTX::create(const StoragePointer& src) { + if (!src) { + return nullptr; + } + + if (!checkHeaderFromStorage(src->size(), src->data())) { + return nullptr; + } + + std::unique_ptr result(new KTX()); + result->resetStorage(src); + + // read metadata + result->_keyValues = parseKeyValues(result->getHeader().bytesOfKeyValueData, result->getKeyValueData()); + + // populate image table + result->_images = parseImages(result->getHeader(), result->getTexelsDataSize(), result->getTexelsData()); + + return result; + } +} diff --git a/libraries/ktx/src/ktx/Writer.cpp b/libraries/ktx/src/ktx/Writer.cpp new file mode 100644 index 0000000000..25b363d31b --- /dev/null +++ b/libraries/ktx/src/ktx/Writer.cpp @@ -0,0 +1,171 @@ +// +// Writer.cpp +// ktx/src/ktx +// +// Created by Zach Pomerantz on 2/08/2017. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// +#include "KTX.h" + + +#include +#include +#ifndef _MSC_VER +#define NOEXCEPT noexcept +#else +#define NOEXCEPT +#endif + +namespace ktx { + + class WriterException : public std::exception { + public: + WriterException(const std::string& explanation) : _explanation("KTX serialization error: " + explanation) {} + const char* what() const NOEXCEPT override { return _explanation.c_str(); } + private: + const std::string _explanation; + }; + + std::unique_ptr KTX::create(const Header& header, const Images& images, const KeyValues& keyValues) { + StoragePointer storagePointer; + { + auto storageSize = ktx::KTX::evalStorageSize(header, images, keyValues); + auto memoryStorage = new storage::MemoryStorage(storageSize); + ktx::KTX::write(memoryStorage->data(), memoryStorage->size(), header, images, keyValues); + storagePointer.reset(memoryStorage); + } + return create(storagePointer); + } + + size_t KTX::evalStorageSize(const Header& header, const Images& images, const KeyValues& keyValues) { + size_t storageSize = sizeof(Header); + + if (!keyValues.empty()) { + size_t keyValuesSize = KeyValue::serializedKeyValuesByteSize(keyValues); + storageSize += keyValuesSize; + } + + auto numMips = header.getNumberOfLevels(); + for (uint32_t l = 0; l < numMips; l++) { + if (images.size() > l) { + storageSize += sizeof(uint32_t); + storageSize += images[l]._imageSize; + storageSize += Header::evalPadding(images[l]._imageSize); + } + } + return storageSize; + } + + size_t KTX::write(Byte* destBytes, size_t destByteSize, const Header& header, const Images& srcImages, const KeyValues& keyValues) { + // Check again that we have enough destination capacity + if (!destBytes || (destByteSize < evalStorageSize(header, srcImages, keyValues))) { + return 0; + } + + auto currentDestPtr = destBytes; + // Header + auto destHeader = reinterpret_cast(currentDestPtr); + memcpy(currentDestPtr, &header, sizeof(Header)); + currentDestPtr += sizeof(Header); + + // KeyValues + if (!keyValues.empty()) { + destHeader->bytesOfKeyValueData = (uint32_t) writeKeyValues(currentDestPtr, destByteSize - sizeof(Header), keyValues); + } else { + // Make sure the header contains the right bytesOfKeyValueData size + destHeader->bytesOfKeyValueData = 0; + } + currentDestPtr += destHeader->bytesOfKeyValueData; + + // Images + auto destImages = writeImages(currentDestPtr, destByteSize - sizeof(Header) - destHeader->bytesOfKeyValueData, srcImages); + // We chould check here that the amoutn of dest IMages generated is the same as the source + + return destByteSize; + } + + uint32_t KeyValue::writeSerializedKeyAndValue(Byte* destBytes, uint32_t destByteSize, const KeyValue& keyval) { + uint32_t keyvalSize = keyval.serializedByteSize(); + if (keyvalSize > destByteSize) { + throw WriterException("invalid key-value size"); + } + + *((uint32_t*) destBytes) = keyval._byteSize; + + auto dest = destBytes + sizeof(uint32_t); + + auto keySize = keyval._key.size() + 1; // Add 1 for the '\0' character at the end of the string + memcpy(dest, keyval._key.data(), keySize); + dest += keySize; + + memcpy(dest, keyval._value.data(), keyval._value.size()); + + return keyvalSize; + } + + size_t KTX::writeKeyValues(Byte* destBytes, size_t destByteSize, const KeyValues& keyValues) { + size_t writtenByteSize = 0; + try { + auto dest = destBytes; + for (auto& keyval : keyValues) { + size_t keyvalSize = KeyValue::writeSerializedKeyAndValue(dest, (uint32_t) (destByteSize - writtenByteSize), keyval); + writtenByteSize += keyvalSize; + dest += keyvalSize; + } + } + catch (const WriterException& e) { + qWarning() << e.what(); + } + return writtenByteSize; + } + + Images KTX::writeImages(Byte* destBytes, size_t destByteSize, const Images& srcImages) { + Images destImages; + auto imagesDataPtr = destBytes; + if (!imagesDataPtr) { + return destImages; + } + auto allocatedImagesDataSize = destByteSize; + size_t currentDataSize = 0; + auto currentPtr = imagesDataPtr; + + for (uint32_t l = 0; l < srcImages.size(); l++) { + if (currentDataSize + sizeof(uint32_t) < allocatedImagesDataSize) { + size_t imageSize = srcImages[l]._imageSize; + *(reinterpret_cast (currentPtr)) = (uint32_t) imageSize; + currentPtr += sizeof(uint32_t); + currentDataSize += sizeof(uint32_t); + + // If enough data ahead then capture the copy source pointer + if (currentDataSize + imageSize <= (allocatedImagesDataSize)) { + auto padding = Header::evalPadding(imageSize); + + // Single face vs cubes + if (srcImages[l]._numFaces == 1) { + memcpy(currentPtr, srcImages[l]._faceBytes[0], imageSize); + destImages.emplace_back(Image((uint32_t) imageSize, padding, currentPtr)); + currentPtr += imageSize; + } else { + Image::FaceBytes faceBytes(NUM_CUBEMAPFACES); + auto faceSize = srcImages[l]._faceSize; + for (int face = 0; face < NUM_CUBEMAPFACES; face++) { + memcpy(currentPtr, srcImages[l]._faceBytes[face], faceSize); + faceBytes[face] = currentPtr; + currentPtr += faceSize; + } + destImages.emplace_back(Image(faceSize, padding, faceBytes)); + } + + currentPtr += padding; + currentDataSize += imageSize + padding; + } + } + } + + return destImages; + } + +} diff --git a/libraries/model-networking/CMakeLists.txt b/libraries/model-networking/CMakeLists.txt index ed8cd7b5f9..00aa17ff57 100644 --- a/libraries/model-networking/CMakeLists.txt +++ b/libraries/model-networking/CMakeLists.txt @@ -1,4 +1,4 @@ set(TARGET_NAME model-networking) setup_hifi_library() -link_hifi_libraries(shared networking model fbx) +link_hifi_libraries(shared networking model fbx ktx) diff --git a/libraries/model-networking/src/model-networking/KTXCache.cpp b/libraries/model-networking/src/model-networking/KTXCache.cpp new file mode 100644 index 0000000000..8ec1c4e41c --- /dev/null +++ b/libraries/model-networking/src/model-networking/KTXCache.cpp @@ -0,0 +1,40 @@ +// +// KTXCache.cpp +// libraries/model-networking/src +// +// Created by Zach Pomerantz on 2/22/2017. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include "KTXCache.h" + +#include + +using File = cache::File; +using FilePointer = cache::FilePointer; + +KTXCache::KTXCache(const std::string& dir, const std::string& ext) : + FileCache(dir, ext) { + initialize(); +} + +KTXFilePointer KTXCache::writeFile(const char* data, Metadata&& metadata) { + FilePointer file = FileCache::writeFile(data, std::move(metadata)); + return std::static_pointer_cast(file); +} + +KTXFilePointer KTXCache::getFile(const Key& key) { + return std::static_pointer_cast(FileCache::getFile(key)); +} + +std::unique_ptr KTXCache::createFile(Metadata&& metadata, const std::string& filepath) { + qCInfo(file_cache) << "Wrote KTX" << metadata.key.c_str(); + return std::unique_ptr(new KTXFile(std::move(metadata), filepath)); +} + +KTXFile::KTXFile(Metadata&& metadata, const std::string& filepath) : + cache::File(std::move(metadata), filepath) {} + diff --git a/libraries/model-networking/src/model-networking/KTXCache.h b/libraries/model-networking/src/model-networking/KTXCache.h new file mode 100644 index 0000000000..bbf7ceadea --- /dev/null +++ b/libraries/model-networking/src/model-networking/KTXCache.h @@ -0,0 +1,48 @@ +// +// KTXCache.h +// libraries/model-networking/src +// +// Created by Zach Pomerantz 2/22/2017. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_KTXCache_h +#define hifi_KTXCache_h + +#include + +#include + +namespace ktx { + class KTX; +} + +class KTXFile; +using KTXFilePointer = std::shared_ptr; + +class KTXCache : public cache::FileCache { + Q_OBJECT + +public: + KTXCache(const std::string& dir, const std::string& ext); + + KTXFilePointer writeFile(const char* data, Metadata&& metadata); + KTXFilePointer getFile(const Key& key); + +protected: + std::unique_ptr createFile(Metadata&& metadata, const std::string& filepath) override final; +}; + +class KTXFile : public cache::File { + Q_OBJECT + +protected: + friend class KTXCache; + + KTXFile(Metadata&& metadata, const std::string& filepath); +}; + +#endif // hifi_KTXCache_h diff --git a/libraries/model-networking/src/model-networking/MeshFace.cpp b/libraries/model-networking/src/model-networking/MeshFace.cpp new file mode 100644 index 0000000000..8092d36aa3 --- /dev/null +++ b/libraries/model-networking/src/model-networking/MeshFace.cpp @@ -0,0 +1,44 @@ +// +// MeshFace.cpp +// libraries/model/src/model/ +// +// Created by Seth Alves on 2017-3-23 +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include + +#include "MeshFace.h" + + +QScriptValue meshFaceToScriptValue(QScriptEngine* engine, const MeshFace &meshFace) { + QScriptValue obj = engine->newObject(); + obj.setProperty("vertices", qVectorIntToScriptValue(engine, meshFace.vertexIndices)); + return obj; +} + +void meshFaceFromScriptValue(const QScriptValue &object, MeshFace& meshFaceResult) { + qVectorIntFromScriptValue(object.property("vertices"), meshFaceResult.vertexIndices); +} + +QScriptValue qVectorMeshFaceToScriptValue(QScriptEngine* engine, const QVector& vector) { + QScriptValue array = engine->newArray(); + for (int i = 0; i < vector.size(); i++) { + array.setProperty(i, meshFaceToScriptValue(engine, vector.at(i))); + } + return array; +} + +void qVectorMeshFaceFromScriptValue(const QScriptValue& array, QVector& result) { + int length = array.property("length").toInteger(); + result.clear(); + + for (int i = 0; i < length; i++) { + MeshFace meshFace = MeshFace(); + meshFaceFromScriptValue(array.property(i), meshFace); + result << meshFace; + } +} diff --git a/libraries/model-networking/src/model-networking/MeshFace.h b/libraries/model-networking/src/model-networking/MeshFace.h new file mode 100644 index 0000000000..3b81b372c3 --- /dev/null +++ b/libraries/model-networking/src/model-networking/MeshFace.h @@ -0,0 +1,43 @@ +// +// MeshFace.h +// libraries/model/src/model/ +// +// Created by Seth Alves on 2017-3-23 +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_MeshFace_h +#define hifi_MeshFace_h + +#include +#include +#include + +#include + +using MeshPointer = std::shared_ptr; + +class MeshFace { + +public: + MeshFace() {} + ~MeshFace() {} + + QVector vertexIndices; + // TODO -- material... +}; + +Q_DECLARE_METATYPE(MeshFace) +Q_DECLARE_METATYPE(QVector) + +QScriptValue meshFaceToScriptValue(QScriptEngine* engine, const MeshFace &meshFace); +void meshFaceFromScriptValue(const QScriptValue &object, MeshFace& meshFaceResult); +QScriptValue qVectorMeshFaceToScriptValue(QScriptEngine* engine, const QVector& vector); +void qVectorMeshFaceFromScriptValue(const QScriptValue& array, QVector& result); + + + +#endif // hifi_MeshFace_h diff --git a/libraries/model-networking/src/model-networking/MeshProxy.cpp b/libraries/model-networking/src/model-networking/MeshProxy.cpp new file mode 100644 index 0000000000..1b6fa43c82 --- /dev/null +++ b/libraries/model-networking/src/model-networking/MeshProxy.cpp @@ -0,0 +1,48 @@ +// +// MeshProxy.cpp +// libraries/model/src/model/ +// +// Created by Seth Alves on 2017-3-22. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include "MeshProxy.h" + + +QScriptValue meshToScriptValue(QScriptEngine* engine, MeshProxy* const &in) { + return engine->newQObject(in, QScriptEngine::QtOwnership, + QScriptEngine::ExcludeDeleteLater | QScriptEngine::ExcludeChildObjects); +} + +void meshFromScriptValue(const QScriptValue& value, MeshProxy* &out) { + out = qobject_cast(value.toQObject()); +} + +QScriptValue meshesToScriptValue(QScriptEngine* engine, const MeshProxyList &in) { + // QScriptValueList result; + QScriptValue result = engine->newArray(); + int i = 0; + foreach (MeshProxy* const meshProxy, in) { + result.setProperty(i++, meshToScriptValue(engine, meshProxy)); + } + return result; +} + +void meshesFromScriptValue(const QScriptValue& value, MeshProxyList &out) { + QScriptValueIterator itr(value); + + qDebug() << "in meshesFromScriptValue, value.length =" << value.property("length").toInt32(); + + while(itr.hasNext()) { + itr.next(); + MeshProxy* meshProxy = qscriptvalue_cast(itr.value()); + if (meshProxy) { + out.append(meshProxy); + } else { + qDebug() << "null meshProxy"; + } + } +} diff --git a/libraries/model-networking/src/model-networking/MeshProxy.h b/libraries/model-networking/src/model-networking/MeshProxy.h new file mode 100644 index 0000000000..c5b25b7895 --- /dev/null +++ b/libraries/model-networking/src/model-networking/MeshProxy.h @@ -0,0 +1,52 @@ +// +// MeshProxy.h +// libraries/model/src/model/ +// +// Created by Seth Alves on 2017-1-27. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_MeshProxy_h +#define hifi_MeshProxy_h + +#include +#include +#include + +#include + +using MeshPointer = std::shared_ptr; + +class MeshProxy : public QObject { + Q_OBJECT + +public: + MeshProxy(MeshPointer mesh) : _mesh(mesh) {} + ~MeshProxy() {} + + MeshPointer getMeshPointer() const { return _mesh; } + + Q_INVOKABLE int getNumVertices() const { return (int)_mesh->getNumVertices(); } + Q_INVOKABLE glm::vec3 getPos3(int index) const { return _mesh->getPos3(index); } + + +protected: + MeshPointer _mesh; +}; + +Q_DECLARE_METATYPE(MeshProxy*); + +class MeshProxyList : public QList {}; // typedef and using fight with the Qt macros/templates, do this instead +Q_DECLARE_METATYPE(MeshProxyList); + + +QScriptValue meshToScriptValue(QScriptEngine* engine, MeshProxy* const &in); +void meshFromScriptValue(const QScriptValue& value, MeshProxy* &out); + +QScriptValue meshesToScriptValue(QScriptEngine* engine, const MeshProxyList &in); +void meshesFromScriptValue(const QScriptValue& value, MeshProxyList &out); + +#endif // hifi_MeshProxy_h diff --git a/libraries/model-networking/src/model-networking/ModelCache.cpp b/libraries/model-networking/src/model-networking/ModelCache.cpp index dd3193073d..142ea74af4 100644 --- a/libraries/model-networking/src/model-networking/ModelCache.cpp +++ b/libraries/model-networking/src/model-networking/ModelCache.cpp @@ -173,7 +173,7 @@ void GeometryReader::run() { FBXGeometry::Pointer fbxGeometry; if (_url.path().toLower().endsWith(".fbx")) { - fbxGeometry.reset(readFBX(_data, _mapping, _url.path())); + fbxGeometry.reset(readFBX(_data, _mapping, _url)); if (fbxGeometry->meshes.size() == 0 && fbxGeometry->joints.size() == 0) { throw QString("empty geometry, possibly due to an unsupported FBX version"); } diff --git a/libraries/model-networking/src/model-networking/TextureCache.cpp b/libraries/model-networking/src/model-networking/TextureCache.cpp index f371207981..98b03eba1e 100644 --- a/libraries/model-networking/src/model-networking/TextureCache.cpp +++ b/libraries/model-networking/src/model-networking/TextureCache.cpp @@ -18,27 +18,37 @@ #include #include #include + +#if DEBUG_DUMP_TEXTURE_LOADS #include #include +#endif #include #include #include +#include + #include #include #include -#include #include "ModelNetworkingLogging.h" #include #include Q_LOGGING_CATEGORY(trace_resource_parse_image, "trace.resource.parse.image") +Q_LOGGING_CATEGORY(trace_resource_parse_image_raw, "trace.resource.parse.image.raw") +Q_LOGGING_CATEGORY(trace_resource_parse_image_ktx, "trace.resource.parse.image.ktx") -TextureCache::TextureCache() { +const std::string TextureCache::KTX_DIRNAME { "ktx_cache" }; +const std::string TextureCache::KTX_EXT { "ktx" }; + +TextureCache::TextureCache() : + _ktxCache(KTX_DIRNAME, KTX_EXT) { setUnusedResourceCacheSize(0); setObjectName("TextureCache"); @@ -61,7 +71,7 @@ TextureCache::~TextureCache() { // this list taken from Ken Perlin's Improved Noise reference implementation (orig. in Java) at // http://mrl.nyu.edu/~perlin/noise/ -const int permutation[256] = +const int permutation[256] = { 151, 160, 137, 91, 90, 15, 131, 13, 201, 95, 96, 53, 194, 233, 7, 225, 140, 36, 103, 30, 69, 142, 8, 99, 37, 240, 21, 10, 23, 190, 6, 148, @@ -108,7 +118,8 @@ const gpu::TexturePointer& TextureCache::getPermutationNormalTexture() { } _permutationNormalTexture = gpu::TexturePointer(gpu::Texture::create2D(gpu::Element(gpu::VEC3, gpu::NUINT8, gpu::RGB), 256, 2)); - _permutationNormalTexture->assignStoredMip(0, _blueTexture->getTexelFormat(), sizeof(data), data); + _permutationNormalTexture->setStoredMipFormat(_permutationNormalTexture->getTexelFormat()); + _permutationNormalTexture->assignStoredMip(0, sizeof(data), data); } return _permutationNormalTexture; } @@ -120,48 +131,44 @@ const unsigned char OPAQUE_BLACK[] = { 0x00, 0x00, 0x00, 0xFF }; const gpu::TexturePointer& TextureCache::getWhiteTexture() { if (!_whiteTexture) { - _whiteTexture = gpu::TexturePointer(gpu::Texture::create2D(gpu::Element::COLOR_RGBA_32, 1, 1)); + _whiteTexture = gpu::TexturePointer(gpu::Texture::createStrict(gpu::Element::COLOR_RGBA_32, 1, 1)); _whiteTexture->setSource("TextureCache::_whiteTexture"); - _whiteTexture->assignStoredMip(0, _whiteTexture->getTexelFormat(), sizeof(OPAQUE_WHITE), OPAQUE_WHITE); + _whiteTexture->setStoredMipFormat(_whiteTexture->getTexelFormat()); + _whiteTexture->assignStoredMip(0, sizeof(OPAQUE_WHITE), OPAQUE_WHITE); } return _whiteTexture; } const gpu::TexturePointer& TextureCache::getGrayTexture() { if (!_grayTexture) { - _grayTexture = gpu::TexturePointer(gpu::Texture::create2D(gpu::Element::COLOR_RGBA_32, 1, 1)); + _grayTexture = gpu::TexturePointer(gpu::Texture::createStrict(gpu::Element::COLOR_RGBA_32, 1, 1)); _grayTexture->setSource("TextureCache::_grayTexture"); - _grayTexture->assignStoredMip(0, _grayTexture->getTexelFormat(), sizeof(OPAQUE_GRAY), OPAQUE_GRAY); + _grayTexture->setStoredMipFormat(_grayTexture->getTexelFormat()); + _grayTexture->assignStoredMip(0, sizeof(OPAQUE_GRAY), OPAQUE_GRAY); } return _grayTexture; } const gpu::TexturePointer& TextureCache::getBlueTexture() { if (!_blueTexture) { - _blueTexture = gpu::TexturePointer(gpu::Texture::create2D(gpu::Element::COLOR_RGBA_32, 1, 1)); + _blueTexture = gpu::TexturePointer(gpu::Texture::createStrict(gpu::Element::COLOR_RGBA_32, 1, 1)); _blueTexture->setSource("TextureCache::_blueTexture"); - _blueTexture->assignStoredMip(0, _blueTexture->getTexelFormat(), sizeof(OPAQUE_BLUE), OPAQUE_BLUE); + _blueTexture->setStoredMipFormat(_blueTexture->getTexelFormat()); + _blueTexture->assignStoredMip(0, sizeof(OPAQUE_BLUE), OPAQUE_BLUE); } return _blueTexture; } const gpu::TexturePointer& TextureCache::getBlackTexture() { if (!_blackTexture) { - _blackTexture = gpu::TexturePointer(gpu::Texture::create2D(gpu::Element::COLOR_RGBA_32, 1, 1)); + _blackTexture = gpu::TexturePointer(gpu::Texture::createStrict(gpu::Element::COLOR_RGBA_32, 1, 1)); _blackTexture->setSource("TextureCache::_blackTexture"); - _blackTexture->assignStoredMip(0, _blackTexture->getTexelFormat(), sizeof(OPAQUE_BLACK), OPAQUE_BLACK); + _blackTexture->setStoredMipFormat(_blackTexture->getTexelFormat()); + _blackTexture->assignStoredMip(0, sizeof(OPAQUE_BLACK), OPAQUE_BLACK); } return _blackTexture; } - -const gpu::TexturePointer& TextureCache::getNormalFittingTexture() { - if (!_normalFittingTexture) { - _normalFittingTexture = getImageTexture(PathUtils::resourcesPath() + "images/normalFittingScale.dds"); - } - return _normalFittingTexture; -} - /// Extra data for creating textures. class TextureExtra { public: @@ -181,6 +188,72 @@ NetworkTexturePointer TextureCache::getTexture(const QUrl& url, Type type, const return ResourceCache::getResource(url, QUrl(), &extra).staticCast(); } +gpu::TexturePointer TextureCache::getTextureByHash(const std::string& hash) { + std::weak_ptr weakPointer; + { + std::unique_lock lock(_texturesByHashesMutex); + weakPointer = _texturesByHashes[hash]; + } + auto result = weakPointer.lock(); + if (result) { + qCWarning(modelnetworking) << "QQQ Returning live texture for hash " << hash.c_str(); + } + return result; +} + +gpu::TexturePointer TextureCache::cacheTextureByHash(const std::string& hash, const gpu::TexturePointer& texture) { + gpu::TexturePointer result; + { + std::unique_lock lock(_texturesByHashesMutex); + result = _texturesByHashes[hash].lock(); + if (!result) { + _texturesByHashes[hash] = texture; + result = texture; + } else { + qCWarning(modelnetworking) << "QQQ Swapping out texture with previous live texture in hash " << hash.c_str(); + } + } + return result; +} + + +gpu::TexturePointer getFallbackTextureForType(NetworkTexture::Type type) { + gpu::TexturePointer result; + auto textureCache = DependencyManager::get(); + // Since this can be called on a background thread, there's a chance that the cache + // will be destroyed by the time we request it + if (!textureCache) { + return result; + } + switch (type) { + case NetworkTexture::DEFAULT_TEXTURE: + case NetworkTexture::ALBEDO_TEXTURE: + case NetworkTexture::ROUGHNESS_TEXTURE: + case NetworkTexture::OCCLUSION_TEXTURE: + result = textureCache->getWhiteTexture(); + break; + + case NetworkTexture::NORMAL_TEXTURE: + result = textureCache->getBlueTexture(); + break; + + case NetworkTexture::EMISSIVE_TEXTURE: + case NetworkTexture::LIGHTMAP_TEXTURE: + result = textureCache->getBlackTexture(); + break; + + case NetworkTexture::BUMP_TEXTURE: + case NetworkTexture::SPECULAR_TEXTURE: + case NetworkTexture::GLOSS_TEXTURE: + case NetworkTexture::CUBE_TEXTURE: + case NetworkTexture::CUSTOM_TEXTURE: + case NetworkTexture::STRICT_TEXTURE: + default: + break; + } + return result; +} + NetworkTexture::TextureLoaderFunc getTextureLoaderForType(NetworkTexture::Type type, const QVariantMap& options = QVariantMap()) { @@ -227,11 +300,16 @@ NetworkTexture::TextureLoaderFunc getTextureLoaderForType(NetworkTexture::Type t return model::TextureUsage::createMetallicTextureFromImage; break; } + case Type::STRICT_TEXTURE: { + return model::TextureUsage::createStrict2DTextureFromImage; + break; + } case Type::CUSTOM_TEXTURE: { Q_ASSERT(false); return NetworkTexture::TextureLoaderFunc(); break; } + case Type::DEFAULT_TEXTURE: default: { return model::TextureUsage::create2DTextureFromImage; @@ -253,8 +331,8 @@ QSharedPointer TextureCache::createResource(const QUrl& url, const QSh auto type = textureExtra ? textureExtra->type : Type::DEFAULT_TEXTURE; auto content = textureExtra ? textureExtra->content : QByteArray(); auto maxNumPixels = textureExtra ? textureExtra->maxNumPixels : ABSOLUTE_MAX_TEXTURE_NUM_PIXELS; - return QSharedPointer(new NetworkTexture(url, type, content, maxNumPixels), - &Resource::deleter); + NetworkTexture* texture = new NetworkTexture(url, type, content, maxNumPixels); + return QSharedPointer(texture, &Resource::deleter); } NetworkTexture::NetworkTexture(const QUrl& url, Type type, const QByteArray& content, int maxNumPixels) : @@ -268,7 +346,6 @@ NetworkTexture::NetworkTexture(const QUrl& url, Type type, const QByteArray& con _loaded = true; } - std::string theName = url.toString().toStdString(); // if we have content, load it after we have our self pointer if (!content.isEmpty()) { _startedLoading = true; @@ -276,12 +353,6 @@ NetworkTexture::NetworkTexture(const QUrl& url, Type type, const QByteArray& con } } -NetworkTexture::NetworkTexture(const QUrl& url, const TextureLoaderFunc& textureLoader, const QByteArray& content) : - NetworkTexture(url, CUSTOM_TEXTURE, content, ABSOLUTE_MAX_TEXTURE_NUM_PIXELS) -{ - _textureLoader = textureLoader; -} - NetworkTexture::TextureLoaderFunc NetworkTexture::getTextureLoader() const { if (_type == CUSTOM_TEXTURE) { return _textureLoader; @@ -289,149 +360,6 @@ NetworkTexture::TextureLoaderFunc NetworkTexture::getTextureLoader() const { return getTextureLoaderForType(_type); } - -class ImageReader : public QRunnable { -public: - - ImageReader(const QWeakPointer& resource, const QByteArray& data, - const QUrl& url = QUrl(), int maxNumPixels = ABSOLUTE_MAX_TEXTURE_NUM_PIXELS); - - virtual void run() override; - -private: - static void listSupportedImageFormats(); - - QWeakPointer _resource; - QUrl _url; - QByteArray _content; - int _maxNumPixels; -}; - -void NetworkTexture::downloadFinished(const QByteArray& data) { - // send the reader off to the thread pool - QThreadPool::globalInstance()->start(new ImageReader(_self, data, _url)); -} - -void NetworkTexture::loadContent(const QByteArray& content) { - QThreadPool::globalInstance()->start(new ImageReader(_self, content, _url, _maxNumPixels)); -} - -ImageReader::ImageReader(const QWeakPointer& resource, const QByteArray& data, - const QUrl& url, int maxNumPixels) : - _resource(resource), - _url(url), - _content(data), - _maxNumPixels(maxNumPixels) -{ -#if DEBUG_DUMP_TEXTURE_LOADS - static auto start = usecTimestampNow() / USECS_PER_MSEC; - auto now = usecTimestampNow() / USECS_PER_MSEC - start; - QString urlStr = _url.toString(); - auto dot = urlStr.lastIndexOf("."); - QString outFileName = QString(QCryptographicHash::hash(urlStr.toLocal8Bit(), QCryptographicHash::Md5).toHex()) + urlStr.right(urlStr.length() - dot); - QFile loadRecord("h:/textures/loads.txt"); - loadRecord.open(QFile::Text | QFile::Append | QFile::ReadWrite); - loadRecord.write(QString("%1 %2\n").arg(now).arg(outFileName).toLocal8Bit()); - outFileName = "h:/textures/" + outFileName; - QFileInfo outInfo(outFileName); - if (!outInfo.exists()) { - QFile outFile(outFileName); - outFile.open(QFile::WriteOnly | QFile::Truncate); - outFile.write(data); - outFile.close(); - } -#endif - DependencyManager::get()->incrementStat("PendingProcessing"); -} - -void ImageReader::listSupportedImageFormats() { - static std::once_flag once; - std::call_once(once, []{ - auto supportedFormats = QImageReader::supportedImageFormats(); - qCDebug(modelnetworking) << "List of supported Image formats:" << supportedFormats.join(", "); - }); -} - -void ImageReader::run() { - DependencyManager::get()->decrementStat("PendingProcessing"); - - CounterStat counter("Processing"); - - PROFILE_RANGE_EX(resource_parse_image, __FUNCTION__, 0xffff0000, 0, { { "url", _url.toString() } }); - auto originalPriority = QThread::currentThread()->priority(); - if (originalPriority == QThread::InheritPriority) { - originalPriority = QThread::NormalPriority; - } - QThread::currentThread()->setPriority(QThread::LowPriority); - Finally restorePriority([originalPriority]{ - QThread::currentThread()->setPriority(originalPriority); - }); - - if (!_resource.data()) { - qCWarning(modelnetworking) << "Abandoning load of" << _url << "; could not get strong ref"; - return; - } - listSupportedImageFormats(); - - // Help the QImage loader by extracting the image file format from the url filename ext. - // Some tga are not created properly without it. - auto filename = _url.fileName().toStdString(); - auto filenameExtension = filename.substr(filename.find_last_of('.') + 1); - QImage image = QImage::fromData(_content, filenameExtension.c_str()); - - // Note that QImage.format is the pixel format which is different from the "format" of the image file... - auto imageFormat = image.format(); - int imageWidth = image.width(); - int imageHeight = image.height(); - - if (imageWidth == 0 || imageHeight == 0 || imageFormat == QImage::Format_Invalid) { - if (filenameExtension.empty()) { - qCDebug(modelnetworking) << "QImage failed to create from content, no file extension:" << _url; - } else { - qCDebug(modelnetworking) << "QImage failed to create from content" << _url; - } - return; - } - - if (imageWidth * imageHeight > _maxNumPixels) { - float scaleFactor = sqrtf(_maxNumPixels / (float)(imageWidth * imageHeight)); - int originalWidth = imageWidth; - int originalHeight = imageHeight; - imageWidth = (int)(scaleFactor * (float)imageWidth + 0.5f); - imageHeight = (int)(scaleFactor * (float)imageHeight + 0.5f); - QImage newImage = image.scaled(QSize(imageWidth, imageHeight), Qt::IgnoreAspectRatio, Qt::SmoothTransformation); - image.swap(newImage); - qCDebug(modelnetworking) << "Downscale image" << _url - << "from" << originalWidth << "x" << originalHeight - << "to" << imageWidth << "x" << imageHeight; - } - - gpu::TexturePointer texture = nullptr; - { - // Double-check the resource still exists between long operations. - auto resource = _resource.toStrongRef(); - if (!resource) { - qCWarning(modelnetworking) << "Abandoning load of" << _url << "; could not get strong ref"; - return; - } - - auto url = _url.toString().toStdString(); - - PROFILE_RANGE_EX(resource_parse_image, __FUNCTION__, 0xffffff00, 0); - texture.reset(resource.dynamicCast()->getTextureLoader()(image, url)); - } - - // Ensure the resource has not been deleted - auto resource = _resource.toStrongRef(); - if (!resource) { - qCWarning(modelnetworking) << "Abandoning load of" << _url << "; could not get strong ref"; - } else { - QMetaObject::invokeMethod(resource.data(), "setImage", - Q_ARG(gpu::TexturePointer, texture), - Q_ARG(int, imageWidth), Q_ARG(int, imageHeight)); - } -} - void NetworkTexture::setImage(gpu::TexturePointer texture, int originalWidth, int originalHeight) { _originalWidth = originalWidth; @@ -454,3 +382,222 @@ void NetworkTexture::setImage(gpu::TexturePointer texture, int originalWidth, emit networkTextureCreated(qWeakPointerCast (_self)); } + +gpu::TexturePointer NetworkTexture::getFallbackTexture() const { + if (_type == CUSTOM_TEXTURE) { + return gpu::TexturePointer(); + } + return getFallbackTextureForType(_type); +} + +class Reader : public QRunnable { +public: + Reader(const QWeakPointer& resource, const QUrl& url); + void run() override final; + virtual void read() = 0; + +protected: + QWeakPointer _resource; + QUrl _url; +}; + +class ImageReader : public Reader { +public: + ImageReader(const QWeakPointer& resource, const QUrl& url, + const QByteArray& data, const std::string& hash, int maxNumPixels); + void read() override final; + +private: + static void listSupportedImageFormats(); + + QByteArray _content; + std::string _hash; + int _maxNumPixels; +}; + +void NetworkTexture::downloadFinished(const QByteArray& data) { + loadContent(data); +} + +void NetworkTexture::loadContent(const QByteArray& content) { + // Hash the source image to for KTX caching + std::string hash; + { + QCryptographicHash hasher(QCryptographicHash::Md5); + hasher.addData(content); + hash = hasher.result().toHex().toStdString(); + } + + auto textureCache = static_cast(_cache.data()); + + if (textureCache != nullptr) { + // If we already have a live texture with the same hash, use it + auto texture = textureCache->getTextureByHash(hash); + + // If there is no live texture, check if there's an existing KTX file + if (!texture) { + KTXFilePointer ktxFile = textureCache->_ktxCache.getFile(hash); + if (ktxFile) { + texture.reset(gpu::Texture::unserialize(ktxFile->getFilepath())); + if (texture) { + texture = textureCache->cacheTextureByHash(hash, texture); + } + } + } + + // If we found the texture either because it's in use or via KTX deserialization, + // set the image and return immediately. + if (texture) { + setImage(texture, texture->getWidth(), texture->getHeight()); + return; + } + } + + // We failed to find an existing live or KTX texture, so trigger an image reader + QThreadPool::globalInstance()->start(new ImageReader(_self, _url, content, hash, _maxNumPixels)); +} + +Reader::Reader(const QWeakPointer& resource, const QUrl& url) : + _resource(resource), _url(url) { + DependencyManager::get()->incrementStat("PendingProcessing"); +} + +void Reader::run() { + PROFILE_RANGE_EX(resource_parse_image, __FUNCTION__, 0xffff0000, 0, { { "url", _url.toString() } }); + DependencyManager::get()->decrementStat("PendingProcessing"); + CounterStat counter("Processing"); + + auto originalPriority = QThread::currentThread()->priority(); + if (originalPriority == QThread::InheritPriority) { + originalPriority = QThread::NormalPriority; + } + QThread::currentThread()->setPriority(QThread::LowPriority); + Finally restorePriority([originalPriority]{ QThread::currentThread()->setPriority(originalPriority); }); + + if (!_resource.data()) { + qCWarning(modelnetworking) << "Abandoning load of" << _url << "; could not get strong ref"; + return; + } + + read(); +} + +ImageReader::ImageReader(const QWeakPointer& resource, const QUrl& url, + const QByteArray& data, const std::string& hash, int maxNumPixels) : + Reader(resource, url), _content(data), _hash(hash), _maxNumPixels(maxNumPixels) { + listSupportedImageFormats(); + +#if DEBUG_DUMP_TEXTURE_LOADS + static auto start = usecTimestampNow() / USECS_PER_MSEC; + auto now = usecTimestampNow() / USECS_PER_MSEC - start; + QString urlStr = _url.toString(); + auto dot = urlStr.lastIndexOf("."); + QString outFileName = QString(QCryptographicHash::hash(urlStr.toLocal8Bit(), QCryptographicHash::Md5).toHex()) + urlStr.right(urlStr.length() - dot); + QFile loadRecord("h:/textures/loads.txt"); + loadRecord.open(QFile::Text | QFile::Append | QFile::ReadWrite); + loadRecord.write(QString("%1 %2\n").arg(now).arg(outFileName).toLocal8Bit()); + outFileName = "h:/textures/" + outFileName; + QFileInfo outInfo(outFileName); + if (!outInfo.exists()) { + QFile outFile(outFileName); + outFile.open(QFile::WriteOnly | QFile::Truncate); + outFile.write(data); + outFile.close(); + } +#endif +} + +void ImageReader::listSupportedImageFormats() { + static std::once_flag once; + std::call_once(once, []{ + auto supportedFormats = QImageReader::supportedImageFormats(); + qCDebug(modelnetworking) << "List of supported Image formats:" << supportedFormats.join(", "); + }); +} + +void ImageReader::read() { + // Help the QImage loader by extracting the image file format from the url filename ext. + // Some tga are not created properly without it. + auto filename = _url.fileName().toStdString(); + auto filenameExtension = filename.substr(filename.find_last_of('.') + 1); + QImage image = QImage::fromData(_content, filenameExtension.c_str()); + int imageWidth = image.width(); + int imageHeight = image.height(); + + // Validate that the image loaded + if (imageWidth == 0 || imageHeight == 0 || image.format() == QImage::Format_Invalid) { + QString reason(filenameExtension.empty() ? "" : "(no file extension)"); + qCWarning(modelnetworking) << "Failed to load" << _url << reason; + return; + } + + // Validate the image is less than _maxNumPixels, and downscale if necessary + if (imageWidth * imageHeight > _maxNumPixels) { + float scaleFactor = sqrtf(_maxNumPixels / (float)(imageWidth * imageHeight)); + int originalWidth = imageWidth; + int originalHeight = imageHeight; + imageWidth = (int)(scaleFactor * (float)imageWidth + 0.5f); + imageHeight = (int)(scaleFactor * (float)imageHeight + 0.5f); + QImage newImage = image.scaled(QSize(imageWidth, imageHeight), Qt::IgnoreAspectRatio, Qt::SmoothTransformation); + image.swap(newImage); + qCDebug(modelnetworking).nospace() << "Downscaled " << _url << " (" << + QSize(originalWidth, originalHeight) << " to " << + QSize(imageWidth, imageHeight) << ")"; + } + + gpu::TexturePointer texture = nullptr; + { + auto resource = _resource.lock(); // to ensure the resource is still needed + if (!resource) { + qCDebug(modelnetworking) << _url << "loading stopped; resource out of scope"; + return; + } + + auto url = _url.toString().toStdString(); + + PROFILE_RANGE_EX(resource_parse_image_raw, __FUNCTION__, 0xffff0000, 0); + // Load the image into a gpu::Texture + auto networkTexture = resource.staticCast(); + texture.reset(networkTexture->getTextureLoader()(image, url)); + texture->setSource(url); + if (texture) { + texture->setFallbackTexture(networkTexture->getFallbackTexture()); + } + + auto textureCache = DependencyManager::get(); + // Save the image into a KTXFile + auto memKtx = gpu::Texture::serialize(*texture); + if (!memKtx) { + qCWarning(modelnetworking) << "Unable to serialize texture to KTX " << _url; + } + + if (memKtx && textureCache) { + const char* data = reinterpret_cast(memKtx->_storage->data()); + size_t length = memKtx->_storage->size(); + KTXFilePointer file; + auto& ktxCache = textureCache->_ktxCache; + if (!memKtx || !(file = ktxCache.writeFile(data, KTXCache::Metadata(_hash, length)))) { + qCWarning(modelnetworking) << _url << "file cache failed"; + } else { + resource.staticCast()->_file = file; + texture->setKtxBacking(file->getFilepath()); + } + } + + // We replace the texture with the one stored in the cache. This deals with the possible race condition of two different + // images with the same hash being loaded concurrently. Only one of them will make it into the cache by hash first and will + // be the winner + if (textureCache) { + texture = textureCache->cacheTextureByHash(_hash, texture); + } + } + + auto resource = _resource.lock(); // to ensure the resource is still needed + if (resource) { + QMetaObject::invokeMethod(resource.data(), "setImage", + Q_ARG(gpu::TexturePointer, texture), + Q_ARG(int, imageWidth), Q_ARG(int, imageHeight)); + } else { + qCDebug(modelnetworking) << _url << "loading stopped; resource out of scope"; + } +} diff --git a/libraries/model-networking/src/model-networking/TextureCache.h b/libraries/model-networking/src/model-networking/TextureCache.h index cb509490c6..6005cc1226 100644 --- a/libraries/model-networking/src/model-networking/TextureCache.h +++ b/libraries/model-networking/src/model-networking/TextureCache.h @@ -23,6 +23,8 @@ #include #include +#include "KTXCache.h" + const int ABSOLUTE_MAX_TEXTURE_NUM_PIXELS = 8192 * 8192; namespace gpu { @@ -43,6 +45,7 @@ class NetworkTexture : public Resource, public Texture { public: enum Type { DEFAULT_TEXTURE, + STRICT_TEXTURE, ALBEDO_TEXTURE, NORMAL_TEXTURE, BUMP_TEXTURE, @@ -63,7 +66,6 @@ public: using TextureLoaderFunc = std::function; NetworkTexture(const QUrl& url, Type type, const QByteArray& content, int maxNumPixels); - NetworkTexture(const QUrl& url, const TextureLoaderFunc& textureLoader, const QByteArray& content); QString getType() const override { return "NetworkTexture"; } @@ -74,12 +76,12 @@ public: Type getTextureType() const { return _type; } TextureLoaderFunc getTextureLoader() const; + gpu::TexturePointer getFallbackTexture() const; signals: void networkTextureCreated(const QWeakPointer& self); protected: - virtual bool isCacheable() const override { return _loaded; } virtual void downloadFinished(const QByteArray& data) override; @@ -88,8 +90,12 @@ protected: Q_INVOKABLE void setImage(gpu::TexturePointer texture, int originalWidth, int originalHeight); private: + friend class KTXReader; + friend class ImageReader; + Type _type; TextureLoaderFunc _textureLoader { [](const QImage&, const std::string&){ return nullptr; } }; + KTXFilePointer _file; int _originalWidth { 0 }; int _originalHeight { 0 }; int _width { 0 }; @@ -124,9 +130,6 @@ public: /// Returns the a black texture (useful for a default). const gpu::TexturePointer& getBlackTexture(); - // Returns a map used to compress the normals through a fitting scale algorithm - const gpu::TexturePointer& getNormalFittingTexture(); - /// Returns a texture version of an image file static gpu::TexturePointer getImageTexture(const QString& path, Type type = Type::DEFAULT_TEXTURE, QVariantMap options = QVariantMap()); @@ -134,6 +137,10 @@ public: NetworkTexturePointer getTexture(const QUrl& url, Type type = Type::DEFAULT_TEXTURE, const QByteArray& content = QByteArray(), int maxNumPixels = ABSOLUTE_MAX_TEXTURE_NUM_PIXELS); + + gpu::TexturePointer getTextureByHash(const std::string& hash); + gpu::TexturePointer cacheTextureByHash(const std::string& hash, const gpu::TexturePointer& texture); + protected: // Overload ResourceCache::prefetch to allow specifying texture type for loads Q_INVOKABLE ScriptableResource* prefetch(const QUrl& url, int type, int maxNumPixels = ABSOLUTE_MAX_TEXTURE_NUM_PIXELS); @@ -142,16 +149,25 @@ protected: const void* extra) override; private: + friend class ImageReader; + friend class NetworkTexture; + friend class DilatableNetworkTexture; + TextureCache(); virtual ~TextureCache(); - friend class DilatableNetworkTexture; + + static const std::string KTX_DIRNAME; + static const std::string KTX_EXT; + KTXCache _ktxCache; + // Map from image hashes to texture weak pointers + std::unordered_map> _texturesByHashes; + std::mutex _texturesByHashesMutex; gpu::TexturePointer _permutationNormalTexture; gpu::TexturePointer _whiteTexture; gpu::TexturePointer _grayTexture; gpu::TexturePointer _blueTexture; gpu::TexturePointer _blackTexture; - gpu::TexturePointer _normalFittingTexture; }; #endif // hifi_TextureCache_h diff --git a/libraries/model/CMakeLists.txt b/libraries/model/CMakeLists.txt index 63f632e484..021aa3d027 100755 --- a/libraries/model/CMakeLists.txt +++ b/libraries/model/CMakeLists.txt @@ -1,5 +1,5 @@ set(TARGET_NAME model) AUTOSCRIBE_SHADER_LIB(gpu model) setup_hifi_library() -link_hifi_libraries(shared gpu) +link_hifi_libraries(shared ktx gpu) diff --git a/libraries/model/src/model/Geometry.cpp b/libraries/model/src/model/Geometry.cpp index 2bb6cfa436..16608ab63e 100755 --- a/libraries/model/src/model/Geometry.cpp +++ b/libraries/model/src/model/Geometry.cpp @@ -117,7 +117,7 @@ Box Mesh::evalPartsBound(int partStart, int partEnd) const { auto partItEnd = _partBuffer.cbegin() + partEnd; for (;part != partItEnd; part++) { - + Box partBound; auto index = _indexBuffer.cbegin() + (*part)._startIndex; auto endIndex = index + (*part)._numIndices; @@ -134,6 +134,114 @@ Box Mesh::evalPartsBound(int partStart, int partEnd) const { return totalBound; } + +model::MeshPointer Mesh::map(std::function vertexFunc, + std::function normalFunc, + std::function indexFunc) { + // vertex data + const gpu::BufferView& vertexBufferView = getVertexBuffer(); + gpu::BufferView::Index numVertices = (gpu::BufferView::Index)getNumVertices(); + gpu::Resource::Size vertexSize = numVertices * sizeof(glm::vec3); + unsigned char* resultVertexData = new unsigned char[vertexSize]; + unsigned char* vertexDataCursor = resultVertexData; + + for (gpu::BufferView::Index i = 0; i < numVertices; i++) { + glm::vec3 pos = vertexFunc(vertexBufferView.get(i)); + memcpy(vertexDataCursor, &pos, sizeof(pos)); + vertexDataCursor += sizeof(pos); + } + + // normal data + int attributeTypeNormal = gpu::Stream::InputSlot::NORMAL; // libraries/gpu/src/gpu/Stream.h + const gpu::BufferView& normalsBufferView = getAttributeBuffer(attributeTypeNormal); + gpu::BufferView::Index numNormals = (gpu::BufferView::Index)normalsBufferView.getNumElements(); + gpu::Resource::Size normalSize = numNormals * sizeof(glm::vec3); + unsigned char* resultNormalData = new unsigned char[normalSize]; + unsigned char* normalDataCursor = resultNormalData; + + for (gpu::BufferView::Index i = 0; i < numNormals; i++) { + glm::vec3 normal = normalFunc(normalsBufferView.get(i)); + memcpy(normalDataCursor, &normal, sizeof(normal)); + normalDataCursor += sizeof(normal); + } + // TODO -- other attributes + + // face data + const gpu::BufferView& indexBufferView = getIndexBuffer(); + gpu::BufferView::Index numIndexes = (gpu::BufferView::Index)getNumIndices(); + gpu::Resource::Size indexSize = numIndexes * sizeof(uint32_t); + unsigned char* resultIndexData = new unsigned char[indexSize]; + unsigned char* indexDataCursor = resultIndexData; + + for (gpu::BufferView::Index i = 0; i < numIndexes; i++) { + uint32_t index = indexFunc(indexBufferView.get(i)); + memcpy(indexDataCursor, &index, sizeof(index)); + indexDataCursor += sizeof(index); + } + + model::MeshPointer result(new model::Mesh()); + + gpu::Element vertexElement = gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::XYZ); + gpu::Buffer* resultVertexBuffer = new gpu::Buffer(vertexSize, resultVertexData); + gpu::BufferPointer resultVertexBufferPointer(resultVertexBuffer); + gpu::BufferView resultVertexBufferView(resultVertexBufferPointer, vertexElement); + result->setVertexBuffer(resultVertexBufferView); + + gpu::Element normalElement = gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::XYZ); + gpu::Buffer* resultNormalsBuffer = new gpu::Buffer(normalSize, resultNormalData); + gpu::BufferPointer resultNormalsBufferPointer(resultNormalsBuffer); + gpu::BufferView resultNormalsBufferView(resultNormalsBufferPointer, normalElement); + result->addAttribute(attributeTypeNormal, resultNormalsBufferView); + + gpu::Element indexElement = gpu::Element(gpu::SCALAR, gpu::UINT32, gpu::RAW); + gpu::Buffer* resultIndexesBuffer = new gpu::Buffer(indexSize, resultIndexData); + gpu::BufferPointer resultIndexesBufferPointer(resultIndexesBuffer); + gpu::BufferView resultIndexesBufferView(resultIndexesBufferPointer, indexElement); + result->setIndexBuffer(resultIndexesBufferView); + + + // TODO -- shouldn't assume just one part + + std::vector parts; + parts.emplace_back(model::Mesh::Part((model::Index)0, // startIndex + (model::Index)result->getNumIndices(), // numIndices + (model::Index)0, // baseVertex + model::Mesh::TRIANGLES)); // topology + result->setPartBuffer(gpu::BufferView(new gpu::Buffer(parts.size() * sizeof(model::Mesh::Part), + (gpu::Byte*) parts.data()), gpu::Element::PART_DRAWCALL)); + + return result; +} + + +void Mesh::forEach(std::function vertexFunc, + std::function normalFunc, + std::function indexFunc) { + // vertex data + const gpu::BufferView& vertexBufferView = getVertexBuffer(); + gpu::BufferView::Index numVertices = (gpu::BufferView::Index)getNumVertices(); + for (gpu::BufferView::Index i = 0; i < numVertices; i++) { + vertexFunc(vertexBufferView.get(i)); + } + + // normal data + int attributeTypeNormal = gpu::Stream::InputSlot::NORMAL; // libraries/gpu/src/gpu/Stream.h + const gpu::BufferView& normalsBufferView = getAttributeBuffer(attributeTypeNormal); + gpu::BufferView::Index numNormals = (gpu::BufferView::Index)normalsBufferView.getNumElements(); + for (gpu::BufferView::Index i = 0; i < numNormals; i++) { + normalFunc(normalsBufferView.get(i)); + } + // TODO -- other attributes + + // face data + const gpu::BufferView& indexBufferView = getIndexBuffer(); + gpu::BufferView::Index numIndexes = (gpu::BufferView::Index)getNumIndices(); + for (gpu::BufferView::Index i = 0; i < numIndexes; i++) { + indexFunc(indexBufferView.get(i)); + } +} + + Geometry::Geometry() { } @@ -148,4 +256,3 @@ Geometry::~Geometry() { void Geometry::setMesh(const MeshPointer& mesh) { _mesh = mesh; } - diff --git a/libraries/model/src/model/Geometry.h b/libraries/model/src/model/Geometry.h index 4256f0be03..7ba3e83407 100755 --- a/libraries/model/src/model/Geometry.h +++ b/libraries/model/src/model/Geometry.h @@ -25,6 +25,10 @@ typedef AABox Box; typedef std::vector< Box > Boxes; typedef glm::vec3 Vec3; +class Mesh; +using MeshPointer = std::shared_ptr< Mesh >; + + class Mesh { public: const static Index PRIMITIVE_RESTART_INDEX = -1; @@ -114,6 +118,15 @@ public: static gpu::Primitive topologyToPrimitive(Topology topo) { return static_cast(topo); } + // create a copy of this mesh after passing its vertices, normals, and indexes though the provided functions + MeshPointer map(std::function vertexFunc, + std::function normalFunc, + std::function indexFunc); + + void forEach(std::function vertexFunc, + std::function normalFunc, + std::function indexFunc); + protected: gpu::Stream::FormatPointer _vertexFormat; @@ -130,7 +143,6 @@ protected: void evalVertexStream(); }; -using MeshPointer = std::shared_ptr< Mesh >; class Geometry { diff --git a/libraries/model/src/model/Light.cpp b/libraries/model/src/model/Light.cpp index 4ac0573cf6..11b13606b8 100755 --- a/libraries/model/src/model/Light.cpp +++ b/libraries/model/src/model/Light.cpp @@ -148,7 +148,7 @@ void Light::setAmbientSpherePreset(gpu::SphericalHarmonics::Preset preset) { void Light::setAmbientMap(gpu::TexturePointer ambientMap) { _ambientMap = ambientMap; if (ambientMap) { - setAmbientMapNumMips(_ambientMap->evalNumMips()); + setAmbientMapNumMips(_ambientMap->getNumMips()); } else { setAmbientMapNumMips(0); } diff --git a/libraries/model/src/model/TextureMap.cpp b/libraries/model/src/model/TextureMap.cpp index 7ac8083d9c..e619a2d70f 100755 --- a/libraries/model/src/model/TextureMap.cpp +++ b/libraries/model/src/model/TextureMap.cpp @@ -10,10 +10,15 @@ // #include "TextureMap.h" +#include + #include #include #include - +#include +#include +#include +#include #include #include "ModelLogging.h" @@ -149,7 +154,7 @@ const QImage TextureUsage::process2DImageColor(const QImage& srcImage, bool& val return image; } -void TextureUsage::defineColorTexelFormats(gpu::Element& formatGPU, gpu::Element& formatMip, +void TextureUsage::defineColorTexelFormats(gpu::Element& formatGPU, gpu::Element& formatMip, const QImage& image, bool isLinear, bool doCompress) { #ifdef COMPRESS_TEXTURES @@ -202,40 +207,41 @@ const QImage& image, bool isLinear, bool doCompress) { #define CPU_MIPMAPS 1 -void generateMips(gpu::Texture* texture, QImage& image, gpu::Element formatMip, bool fastResize) { +void generateMips(gpu::Texture* texture, QImage& image, bool fastResize) { #if CPU_MIPMAPS PROFILE_RANGE(resource_parse, "generateMips"); - auto numMips = texture->evalNumMips(); + auto numMips = texture->getNumMips(); for (uint16 level = 1; level < numMips; ++level) { QSize mipSize(texture->evalMipWidth(level), texture->evalMipHeight(level)); if (fastResize) { image = image.scaled(mipSize); - texture->assignStoredMip(level, formatMip, image.byteCount(), image.constBits()); + texture->assignStoredMip(level, image.byteCount(), image.constBits()); } else { QImage mipImage = image.scaled(mipSize, Qt::IgnoreAspectRatio, Qt::SmoothTransformation); - texture->assignStoredMip(level, formatMip, mipImage.byteCount(), mipImage.constBits()); + texture->assignStoredMip(level, mipImage.byteCount(), mipImage.constBits()); } } + #else texture->autoGenerateMips(-1); #endif } -void generateFaceMips(gpu::Texture* texture, QImage& image, gpu::Element formatMip, uint8 face) { +void generateFaceMips(gpu::Texture* texture, QImage& image, uint8 face) { #if CPU_MIPMAPS PROFILE_RANGE(resource_parse, "generateFaceMips"); - auto numMips = texture->evalNumMips(); + auto numMips = texture->getNumMips(); for (uint16 level = 1; level < numMips; ++level) { QSize mipSize(texture->evalMipWidth(level), texture->evalMipHeight(level)); QImage mipImage = image.scaled(mipSize, Qt::IgnoreAspectRatio, Qt::SmoothTransformation); - texture->assignStoredMipFace(level, formatMip, mipImage.byteCount(), mipImage.constBits(), face); + texture->assignStoredMipFace(level, face, mipImage.byteCount(), mipImage.constBits()); } #else texture->autoGenerateMips(-1); #endif } -gpu::Texture* TextureUsage::process2DTextureColorFromImage(const QImage& srcImage, const std::string& srcImageName, bool isLinear, bool doCompress, bool generateMips) { +gpu::Texture* TextureUsage::process2DTextureColorFromImage(const QImage& srcImage, const std::string& srcImageName, bool isLinear, bool doCompress, bool generateMips, bool isStrict) { PROFILE_RANGE(resource_parse, "process2DTextureColorFromImage"); bool validAlpha = false; bool alphaAsMask = true; @@ -248,7 +254,11 @@ gpu::Texture* TextureUsage::process2DTextureColorFromImage(const QImage& srcImag gpu::Element formatMip; defineColorTexelFormats(formatGPU, formatMip, image, isLinear, doCompress); - theTexture = (gpu::Texture::create2D(formatGPU, image.width(), image.height(), gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR))); + if (isStrict) { + theTexture = (gpu::Texture::createStrict(formatGPU, image.width(), image.height(), gpu::Texture::MAX_NUM_MIPS, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR))); + } else { + theTexture = (gpu::Texture::create2D(formatGPU, image.width(), image.height(), gpu::Texture::MAX_NUM_MIPS, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR))); + } theTexture->setSource(srcImageName); auto usage = gpu::Texture::Usage::Builder().withColor(); if (validAlpha) { @@ -258,22 +268,26 @@ gpu::Texture* TextureUsage::process2DTextureColorFromImage(const QImage& srcImag } } theTexture->setUsage(usage.build()); - - theTexture->assignStoredMip(0, formatMip, image.byteCount(), image.constBits()); + theTexture->setStoredMipFormat(formatMip); + theTexture->assignStoredMip(0, image.byteCount(), image.constBits()); if (generateMips) { - ::generateMips(theTexture, image, formatMip, false); + ::generateMips(theTexture, image, false); } + theTexture->setSource(srcImageName); } return theTexture; } +gpu::Texture* TextureUsage::createStrict2DTextureFromImage(const QImage& srcImage, const std::string& srcImageName) { + return process2DTextureColorFromImage(srcImage, srcImageName, false, false, true, true); +} + gpu::Texture* TextureUsage::create2DTextureFromImage(const QImage& srcImage, const std::string& srcImageName) { return process2DTextureColorFromImage(srcImage, srcImageName, false, false, true); } - gpu::Texture* TextureUsage::createAlbedoTextureFromImage(const QImage& srcImage, const std::string& srcImageName) { return process2DTextureColorFromImage(srcImage, srcImageName, false, true, true); } @@ -291,21 +305,25 @@ gpu::Texture* TextureUsage::createNormalTextureFromNormalImage(const QImage& src PROFILE_RANGE(resource_parse, "createNormalTextureFromNormalImage"); QImage image = processSourceImage(srcImage, false); - // Make sure the normal map source image is RGBA32 - if (image.format() != QImage::Format_RGBA8888) { - image = image.convertToFormat(QImage::Format_RGBA8888); + // Make sure the normal map source image is ARGB32 + if (image.format() != QImage::Format_ARGB32) { + image = image.convertToFormat(QImage::Format_ARGB32); } + gpu::Texture* theTexture = nullptr; if ((image.width() > 0) && (image.height() > 0)) { - gpu::Element formatGPU = gpu::Element(gpu::VEC4, gpu::NUINT8, gpu::RGBA); - gpu::Element formatMip = gpu::Element(gpu::VEC4, gpu::NUINT8, gpu::RGBA); + gpu::Element formatMip = gpu::Element::COLOR_BGRA_32; + gpu::Element formatGPU = gpu::Element::COLOR_RGBA_32; + + theTexture = (gpu::Texture::create2D(formatGPU, image.width(), image.height(), gpu::Texture::MAX_NUM_MIPS, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR))); + theTexture->setSource(srcImageName); + theTexture->setStoredMipFormat(formatMip); + theTexture->assignStoredMip(0, image.byteCount(), image.constBits()); + generateMips(theTexture, image, true); - theTexture = (gpu::Texture::create2D(formatGPU, image.width(), image.height(), gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR))); theTexture->setSource(srcImageName); - theTexture->assignStoredMip(0, formatMip, image.byteCount(), image.constBits()); - generateMips(theTexture, image, formatMip, true); } return theTexture; @@ -327,8 +345,8 @@ gpu::Texture* TextureUsage::createNormalTextureFromBumpImage(const QImage& srcIm PROFILE_RANGE(resource_parse, "createNormalTextureFromBumpImage"); QImage image = processSourceImage(srcImage, false); - if (image.format() != QImage::Format_RGB888) { - image = image.convertToFormat(QImage::Format_RGB888); + if (image.format() != QImage::Format_Grayscale8) { + image = image.convertToFormat(QImage::Format_Grayscale8); } // PR 5540 by AlessandroSigna integrated here as a specialized TextureLoader for bumpmaps @@ -336,16 +354,17 @@ gpu::Texture* TextureUsage::createNormalTextureFromBumpImage(const QImage& srcIm const double pStrength = 2.0; int width = image.width(); int height = image.height(); - QImage result(width, height, QImage::Format_RGB888); - + + QImage result(width, height, QImage::Format_ARGB32); + for (int i = 0; i < width; i++) { const int iNextClamped = clampPixelCoordinate(i + 1, width - 1); const int iPrevClamped = clampPixelCoordinate(i - 1, width - 1); - + for (int j = 0; j < height; j++) { const int jNextClamped = clampPixelCoordinate(j + 1, height - 1); const int jPrevClamped = clampPixelCoordinate(j - 1, height - 1); - + // surrounding pixels const QRgb topLeft = image.pixel(iPrevClamped, jPrevClamped); const QRgb top = image.pixel(iPrevClamped, j); @@ -355,7 +374,7 @@ gpu::Texture* TextureUsage::createNormalTextureFromBumpImage(const QImage& srcIm const QRgb bottom = image.pixel(iNextClamped, j); const QRgb bottomLeft = image.pixel(iNextClamped, jPrevClamped); const QRgb left = image.pixel(i, jPrevClamped); - + // take their gray intensities // since it's a grayscale image, the value of each component RGB is the same const double tl = qRed(topLeft); @@ -366,29 +385,35 @@ gpu::Texture* TextureUsage::createNormalTextureFromBumpImage(const QImage& srcIm const double b = qRed(bottom); const double bl = qRed(bottomLeft); const double l = qRed(left); - + // apply the sobel filter const double dX = (tr + pStrength * r + br) - (tl + pStrength * l + bl); const double dY = (bl + pStrength * b + br) - (tl + pStrength * t + tr); const double dZ = RGBA_MAX / pStrength; - + glm::vec3 v(dX, dY, dZ); glm::normalize(v); - + // convert to rgb from the value obtained computing the filter - QRgb qRgbValue = qRgba(mapComponent(v.x), mapComponent(v.y), mapComponent(v.z), 1.0); + QRgb qRgbValue = qRgba(mapComponent(v.z), mapComponent(v.y), mapComponent(v.x), 1.0); result.setPixel(i, j, qRgbValue); } } gpu::Texture* theTexture = nullptr; - if ((image.width() > 0) && (image.height() > 0)) { - gpu::Element formatGPU = gpu::Element(gpu::VEC3, gpu::NUINT8, gpu::RGB); - gpu::Element formatMip = gpu::Element(gpu::VEC3, gpu::NUINT8, gpu::RGB); + if ((result.width() > 0) && (result.height() > 0)) { + + gpu::Element formatMip = gpu::Element::COLOR_BGRA_32; + gpu::Element formatGPU = gpu::Element::COLOR_RGBA_32; + + + theTexture = (gpu::Texture::create2D(formatGPU, result.width(), result.height(), gpu::Texture::MAX_NUM_MIPS, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR))); + theTexture->setSource(srcImageName); + theTexture->setStoredMipFormat(formatMip); + theTexture->assignStoredMip(0, result.byteCount(), result.constBits()); + generateMips(theTexture, result, true); - theTexture = (gpu::Texture::create2D(formatGPU, image.width(), image.height(), gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR))); theTexture->setSource(srcImageName); - theTexture->assignStoredMip(0, formatMip, image.byteCount(), image.constBits()); } return theTexture; @@ -414,16 +439,17 @@ gpu::Texture* TextureUsage::createRoughnessTextureFromImage(const QImage& srcIma #ifdef COMPRESS_TEXTURES gpu::Element formatGPU = gpu::Element(gpu::SCALAR, gpu::NUINT8, gpu::COMPRESSED_R); #else - gpu::Element formatGPU = gpu::Element(gpu::SCALAR, gpu::NUINT8, gpu::RGB); + gpu::Element formatGPU = gpu::Element::COLOR_R_8; #endif - gpu::Element formatMip = gpu::Element(gpu::SCALAR, gpu::NUINT8, gpu::RGB); + gpu::Element formatMip = gpu::Element::COLOR_R_8; - theTexture = (gpu::Texture::create2D(formatGPU, image.width(), image.height(), gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR))); + theTexture = (gpu::Texture::create2D(formatGPU, image.width(), image.height(), gpu::Texture::MAX_NUM_MIPS, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR))); theTexture->setSource(srcImageName); - theTexture->assignStoredMip(0, formatMip, image.byteCount(), image.constBits()); - generateMips(theTexture, image, formatMip, true); + theTexture->setStoredMipFormat(formatMip); + theTexture->assignStoredMip(0, image.byteCount(), image.constBits()); + generateMips(theTexture, image, true); - // FIXME queue for transfer to GPU and block on completion + theTexture->setSource(srcImageName); } return theTexture; @@ -444,27 +470,28 @@ gpu::Texture* TextureUsage::createRoughnessTextureFromGlossImage(const QImage& s // Gloss turned into Rough image.invertPixels(QImage::InvertRgba); - + image = image.convertToFormat(QImage::Format_Grayscale8); - + gpu::Texture* theTexture = nullptr; if ((image.width() > 0) && (image.height() > 0)) { - + #ifdef COMPRESS_TEXTURES gpu::Element formatGPU = gpu::Element(gpu::SCALAR, gpu::NUINT8, gpu::COMPRESSED_R); #else - gpu::Element formatGPU = gpu::Element(gpu::SCALAR, gpu::NUINT8, gpu::RGB); + gpu::Element formatGPU = gpu::Element::COLOR_R_8; #endif - gpu::Element formatMip = gpu::Element(gpu::SCALAR, gpu::NUINT8, gpu::RGB); + gpu::Element formatMip = gpu::Element::COLOR_R_8; + + theTexture = (gpu::Texture::create2D(formatGPU, image.width(), image.height(), gpu::Texture::MAX_NUM_MIPS, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR))); + theTexture->setSource(srcImageName); + theTexture->setStoredMipFormat(formatMip); + theTexture->assignStoredMip(0, image.byteCount(), image.constBits()); + generateMips(theTexture, image, true); - theTexture = (gpu::Texture::create2D(formatGPU, image.width(), image.height(), gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR))); theTexture->setSource(srcImageName); - theTexture->assignStoredMip(0, formatMip, image.byteCount(), image.constBits()); - generateMips(theTexture, image, formatMip, true); - - // FIXME queue for transfer to GPU and block on completion } - + return theTexture; } @@ -489,16 +516,17 @@ gpu::Texture* TextureUsage::createMetallicTextureFromImage(const QImage& srcImag #ifdef COMPRESS_TEXTURES gpu::Element formatGPU = gpu::Element(gpu::SCALAR, gpu::NUINT8, gpu::COMPRESSED_R); #else - gpu::Element formatGPU = gpu::Element(gpu::SCALAR, gpu::NUINT8, gpu::RGB); + gpu::Element formatGPU = gpu::Element::COLOR_R_8; #endif - gpu::Element formatMip = gpu::Element(gpu::SCALAR, gpu::NUINT8, gpu::RGB); + gpu::Element formatMip = gpu::Element::COLOR_R_8; - theTexture = (gpu::Texture::create2D(formatGPU, image.width(), image.height(), gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR))); + theTexture = (gpu::Texture::create2D(formatGPU, image.width(), image.height(), gpu::Texture::MAX_NUM_MIPS, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR))); theTexture->setSource(srcImageName); - theTexture->assignStoredMip(0, formatMip, image.byteCount(), image.constBits()); - generateMips(theTexture, image, formatMip, true); + theTexture->setStoredMipFormat(formatMip); + theTexture->assignStoredMip(0, image.byteCount(), image.constBits()); + generateMips(theTexture, image, true); - // FIXME queue for transfer to GPU and block on completion + theTexture->setSource(srcImageName); } return theTexture; @@ -521,18 +549,18 @@ public: int _y = 0; bool _horizontalMirror = false; bool _verticalMirror = false; - + Face() {} Face(int x, int y, bool horizontalMirror, bool verticalMirror) : _x(x), _y(y), _horizontalMirror(horizontalMirror), _verticalMirror(verticalMirror) {} }; - + Face _faceXPos; Face _faceXNeg; Face _faceYPos; Face _faceYNeg; Face _faceZPos; Face _faceZNeg; - + CubeLayout(int wr, int hr, Face fXP, Face fXN, Face fYP, Face fYN, Face fZP, Face fZN) : _type(FLAT), _widthRatio(wr), @@ -775,7 +803,7 @@ gpu::Texture* TextureUsage::processCubeTextureColorFromImage(const QImage& srcIm defineColorTexelFormats(formatGPU, formatMip, image, isLinear, doCompress); // Find the layout of the cubemap in the 2D image - // Use the original image size since processSourceImage may have altered the size / aspect ratio + // Use the original image size since processSourceImage may have altered the size / aspect ratio int foundLayout = CubeLayout::findLayout(srcImage.width(), srcImage.height()); std::vector faces; @@ -808,27 +836,25 @@ gpu::Texture* TextureUsage::processCubeTextureColorFromImage(const QImage& srcIm // If the 6 faces have been created go on and define the true Texture if (faces.size() == gpu::Texture::NUM_FACES_PER_TYPE[gpu::Texture::TEX_CUBE]) { - theTexture = gpu::Texture::createCube(formatGPU, faces[0].width(), gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR, gpu::Sampler::WRAP_CLAMP)); + theTexture = gpu::Texture::createCube(formatGPU, faces[0].width(), gpu::Texture::MAX_NUM_MIPS, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR, gpu::Sampler::WRAP_CLAMP)); theTexture->setSource(srcImageName); + theTexture->setStoredMipFormat(formatMip); int f = 0; for (auto& face : faces) { - theTexture->assignStoredMipFace(0, formatMip, face.byteCount(), face.constBits(), f); + theTexture->assignStoredMipFace(0, f, face.byteCount(), face.constBits()); if (generateMips) { - generateFaceMips(theTexture, face, formatMip, f); + generateFaceMips(theTexture, face, f); } f++; } - if (generateMips) { - PROFILE_RANGE(resource_parse, "generateMips"); - theTexture->autoGenerateMips(-1); - } - // Generate irradiance while we are at it if (generateIrradiance) { PROFILE_RANGE(resource_parse, "generateIrradiance"); theTexture->generateIrradiance(); } + + theTexture->setSource(srcImageName); } } diff --git a/libraries/model/src/model/TextureMap.h b/libraries/model/src/model/TextureMap.h index 220ee57a97..a4bb861502 100755 --- a/libraries/model/src/model/TextureMap.h +++ b/libraries/model/src/model/TextureMap.h @@ -32,6 +32,7 @@ public: int _environmentUsage = 0; static gpu::Texture* create2DTextureFromImage(const QImage& image, const std::string& srcImageName); + static gpu::Texture* createStrict2DTextureFromImage(const QImage& image, const std::string& srcImageName); static gpu::Texture* createAlbedoTextureFromImage(const QImage& image, const std::string& srcImageName); static gpu::Texture* createEmissiveTextureFromImage(const QImage& image, const std::string& srcImageName); static gpu::Texture* createNormalTextureFromNormalImage(const QImage& image, const std::string& srcImageName); @@ -47,7 +48,7 @@ public: static const QImage process2DImageColor(const QImage& srcImage, bool& validAlpha, bool& alphaAsMask); static void defineColorTexelFormats(gpu::Element& formatGPU, gpu::Element& formatMip, const QImage& srcImage, bool isLinear, bool doCompress); - static gpu::Texture* process2DTextureColorFromImage(const QImage& srcImage, const std::string& srcImageName, bool isLinear, bool doCompress, bool generateMips); + static gpu::Texture* process2DTextureColorFromImage(const QImage& srcImage, const std::string& srcImageName, bool isLinear, bool doCompress, bool generateMips, bool isStrict = false); static gpu::Texture* processCubeTextureColorFromImage(const QImage& srcImage, const std::string& srcImageName, bool isLinear, bool doCompress, bool generateMips, bool generateIrradiance); }; diff --git a/libraries/networking/src/AddressManager.h b/libraries/networking/src/AddressManager.h index c7d283ad02..83eedfc82f 100644 --- a/libraries/networking/src/AddressManager.h +++ b/libraries/networking/src/AddressManager.h @@ -41,7 +41,7 @@ class AddressManager : public QObject, public Dependency { Q_PROPERTY(QString pathname READ currentPath) Q_PROPERTY(QString placename READ getPlaceName) Q_PROPERTY(QString domainId READ getDomainId) - Q_PROPERTY(QUrl metaverseServerUrl READ getMetaverseServerUrl) + Q_PROPERTY(QUrl metaverseServerUrl READ getMetaverseServerUrl NOTIFY metaverseServerUrlChanged) public: Q_INVOKABLE QString protocolVersion(); using PositionGetter = std::function; @@ -123,6 +123,8 @@ signals: void goBackPossible(bool isPossible); void goForwardPossible(bool isPossible); + void metaverseServerUrlChanged(); + protected: AddressManager(); private slots: diff --git a/libraries/networking/src/Assignment.cpp b/libraries/networking/src/Assignment.cpp index 9efad15398..27d4a31ccf 100644 --- a/libraries/networking/src/Assignment.cpp +++ b/libraries/networking/src/Assignment.cpp @@ -12,7 +12,6 @@ #include "udt/PacketHeaders.h" #include "SharedUtil.h" #include "UUID.h" -#include "ServerPathUtils.h" #include diff --git a/libraries/networking/src/FileCache.cpp b/libraries/networking/src/FileCache.cpp new file mode 100644 index 0000000000..0a859d511b --- /dev/null +++ b/libraries/networking/src/FileCache.cpp @@ -0,0 +1,243 @@ +// +// FileCache.cpp +// libraries/model-networking/src +// +// Created by Zach Pomerantz on 2/21/2017. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include "FileCache.h" + +#include +#include +#include +#include + +#include + +#include + +Q_LOGGING_CATEGORY(file_cache, "hifi.file_cache", QtWarningMsg) + +using namespace cache; + +static const std::string MANIFEST_NAME = "manifest"; + +static const size_t BYTES_PER_MEGABYTES = 1024 * 1024; +static const size_t BYTES_PER_GIGABYTES = 1024 * BYTES_PER_MEGABYTES; +const size_t FileCache::DEFAULT_UNUSED_MAX_SIZE = 5 * BYTES_PER_GIGABYTES; // 5GB +const size_t FileCache::MAX_UNUSED_MAX_SIZE = 100 * BYTES_PER_GIGABYTES; // 100GB +const size_t FileCache::DEFAULT_OFFLINE_MAX_SIZE = 2 * BYTES_PER_GIGABYTES; // 2GB + +void FileCache::setUnusedFileCacheSize(size_t unusedFilesMaxSize) { + _unusedFilesMaxSize = std::min(unusedFilesMaxSize, MAX_UNUSED_MAX_SIZE); + reserve(0); + emit dirty(); +} + +void FileCache::setOfflineFileCacheSize(size_t offlineFilesMaxSize) { + _offlineFilesMaxSize = std::min(offlineFilesMaxSize, MAX_UNUSED_MAX_SIZE); +} + +FileCache::FileCache(const std::string& dirname, const std::string& ext, QObject* parent) : + QObject(parent), + _ext(ext), + _dirname(dirname), + _dirpath(PathUtils::getAppLocalDataFilePath(dirname.c_str()).toStdString()) {} + +FileCache::~FileCache() { + clear(); +} + +void fileDeleter(File* file) { + file->deleter(); +} + +void FileCache::initialize() { + QDir dir(_dirpath.c_str()); + + if (dir.exists()) { + auto nameFilters = QStringList(("*." + _ext).c_str()); + auto filters = QDir::Filters(QDir::NoDotAndDotDot | QDir::Files); + auto sort = QDir::SortFlags(QDir::Time); + auto files = dir.entryList(nameFilters, filters, sort); + + // load persisted files + foreach(QString filename, files) { + const Key key = filename.section('.', 0, 0).toStdString(); + const std::string filepath = dir.filePath(filename).toStdString(); + const size_t length = QFileInfo(filepath.c_str()).size(); + addFile(Metadata(key, length), filepath); + } + + qCDebug(file_cache, "[%s] Initialized %s", _dirname.c_str(), _dirpath.c_str()); + } else { + dir.mkpath(_dirpath.c_str()); + qCDebug(file_cache, "[%s] Created %s", _dirname.c_str(), _dirpath.c_str()); + } + + _initialized = true; +} + +FilePointer FileCache::addFile(Metadata&& metadata, const std::string& filepath) { + FilePointer file(createFile(std::move(metadata), filepath).release(), &fileDeleter); + if (file) { + _numTotalFiles += 1; + _totalFilesSize += file->getLength(); + file->_cache = this; + emit dirty(); + + Lock lock(_filesMutex); + _files[file->getKey()] = file; + } + return file; +} + +FilePointer FileCache::writeFile(const char* data, File::Metadata&& metadata) { + assert(_initialized); + + std::string filepath = getFilepath(metadata.key); + + Lock lock(_filesMutex); + + // if file already exists, return it + FilePointer file = getFile(metadata.key); + if (file) { + qCWarning(file_cache, "[%s] Attempted to overwrite %s", _dirname.c_str(), metadata.key.c_str()); + return file; + } + + // write the new file + FILE* saveFile = fopen(filepath.c_str(), "wb"); + if (saveFile != nullptr && fwrite(data, metadata.length, 1, saveFile) && fclose(saveFile) == 0) { + file = addFile(std::move(metadata), filepath); + } else { + qCWarning(file_cache, "[%s] Failed to write %s (%s)", _dirname.c_str(), metadata.key.c_str(), strerror(errno)); + errno = 0; + } + + return file; +} + +FilePointer FileCache::getFile(const Key& key) { + assert(_initialized); + + FilePointer file; + + Lock lock(_filesMutex); + + // check if file exists + const auto it = _files.find(key); + if (it != _files.cend()) { + file = it->second.lock(); + if (file) { + // if it exists, it is active - remove it from the cache + removeUnusedFile(file); + qCDebug(file_cache, "[%s] Found %s", _dirname.c_str(), key.c_str()); + emit dirty(); + } else { + // if not, remove the weak_ptr + _files.erase(it); + } + } + + return file; +} + +std::string FileCache::getFilepath(const Key& key) { + return _dirpath + '/' + key + '.' + _ext; +} + +void FileCache::addUnusedFile(const FilePointer file) { + { + Lock lock(_filesMutex); + _files[file->getKey()] = file; + } + + reserve(file->getLength()); + file->_LRUKey = ++_lastLRUKey; + + { + Lock lock(_unusedFilesMutex); + _unusedFiles.insert({ file->_LRUKey, file }); + _numUnusedFiles += 1; + _unusedFilesSize += file->getLength(); + } + + emit dirty(); +} + +void FileCache::removeUnusedFile(const FilePointer file) { + Lock lock(_unusedFilesMutex); + const auto it = _unusedFiles.find(file->_LRUKey); + if (it != _unusedFiles.cend()) { + _unusedFiles.erase(it); + _numUnusedFiles -= 1; + _unusedFilesSize -= file->getLength(); + } +} + +void FileCache::reserve(size_t length) { + Lock unusedLock(_unusedFilesMutex); + while (!_unusedFiles.empty() && + _unusedFilesSize + length > _unusedFilesMaxSize) { + auto it = _unusedFiles.begin(); + auto file = it->second; + auto length = file->getLength(); + + unusedLock.unlock(); + { + file->_cache = nullptr; + Lock lock(_filesMutex); + _files.erase(file->getKey()); + } + unusedLock.lock(); + + _unusedFiles.erase(it); + _numTotalFiles -= 1; + _numUnusedFiles -= 1; + _totalFilesSize -= length; + _unusedFilesSize -= length; + } +} + +void FileCache::clear() { + Lock unusedFilesLock(_unusedFilesMutex); + for (const auto& pair : _unusedFiles) { + auto& file = pair.second; + file->_cache = nullptr; + + if (_totalFilesSize > _offlineFilesMaxSize) { + _totalFilesSize -= file->getLength(); + } else { + file->_shouldPersist = true; + qCDebug(file_cache, "[%s] Persisting %s", _dirname.c_str(), file->getKey().c_str()); + } + } + _unusedFiles.clear(); +} + +void File::deleter() { + if (_cache) { + FilePointer self(this, &fileDeleter); + _cache->addUnusedFile(self); + } else { + deleteLater(); + } +} + +File::File(Metadata&& metadata, const std::string& filepath) : + _key(std::move(metadata.key)), + _length(metadata.length), + _filepath(filepath) {} + +File::~File() { + QFile file(getFilepath().c_str()); + if (file.exists() && !_shouldPersist) { + qCInfo(file_cache, "Unlinked %s", getFilepath().c_str()); + file.remove(); + } +} diff --git a/libraries/networking/src/FileCache.h b/libraries/networking/src/FileCache.h new file mode 100644 index 0000000000..f77db555bc --- /dev/null +++ b/libraries/networking/src/FileCache.h @@ -0,0 +1,158 @@ +// +// FileCache.h +// libraries/networking/src +// +// Created by Zach Pomerantz on 2/21/2017. +// Copyright 2017 High Fidelity, Inc. // // Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_FileCache_h +#define hifi_FileCache_h + +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +Q_DECLARE_LOGGING_CATEGORY(file_cache) + +namespace cache { + +class File; +using FilePointer = std::shared_ptr; + +class FileCache : public QObject { + Q_OBJECT + Q_PROPERTY(size_t numTotal READ getNumTotalFiles NOTIFY dirty) + Q_PROPERTY(size_t numCached READ getNumCachedFiles NOTIFY dirty) + Q_PROPERTY(size_t sizeTotal READ getSizeTotalFiles NOTIFY dirty) + Q_PROPERTY(size_t sizeCached READ getSizeCachedFiles NOTIFY dirty) + + static const size_t DEFAULT_UNUSED_MAX_SIZE; + static const size_t MAX_UNUSED_MAX_SIZE; + static const size_t DEFAULT_OFFLINE_MAX_SIZE; + +public: + size_t getNumTotalFiles() const { return _numTotalFiles; } + size_t getNumCachedFiles() const { return _numUnusedFiles; } + size_t getSizeTotalFiles() const { return _totalFilesSize; } + size_t getSizeCachedFiles() const { return _unusedFilesSize; } + + void setUnusedFileCacheSize(size_t unusedFilesMaxSize); + size_t getUnusedFileCacheSize() const { return _unusedFilesSize; } + + void setOfflineFileCacheSize(size_t offlineFilesMaxSize); + + // initialize FileCache with a directory name (not a path, ex.: "temp_jpgs") and an ext (ex.: "jpg") + FileCache(const std::string& dirname, const std::string& ext, QObject* parent = nullptr); + virtual ~FileCache(); + + using Key = std::string; + struct Metadata { + Metadata(const Key& key, size_t length) : + key(key), length(length) {} + Key key; + size_t length; + }; + + // derived classes should implement a setter/getter, for example, for a FileCache backing a network cache: + // + // DerivedFilePointer writeFile(const char* data, DerivedMetadata&& metadata) { + // return writeFile(data, std::forward(metadata)); + // } + // + // DerivedFilePointer getFile(const QUrl& url) { + // auto key = lookup_hash_for(url); // assuming hashing url in create/evictedFile overrides + // return getFile(key); + // } + +signals: + void dirty(); + +protected: + /// must be called after construction to create the cache on the fs and restore persisted files + void initialize(); + + FilePointer writeFile(const char* data, Metadata&& metadata); + FilePointer getFile(const Key& key); + + /// create a file + virtual std::unique_ptr createFile(Metadata&& metadata, const std::string& filepath) = 0; + +private: + using Mutex = std::recursive_mutex; + using Lock = std::unique_lock; + + friend class File; + + std::string getFilepath(const Key& key); + + FilePointer addFile(Metadata&& metadata, const std::string& filepath); + void addUnusedFile(const FilePointer file); + void removeUnusedFile(const FilePointer file); + void reserve(size_t length); + void clear(); + + std::atomic _numTotalFiles { 0 }; + std::atomic _numUnusedFiles { 0 }; + std::atomic _totalFilesSize { 0 }; + std::atomic _unusedFilesSize { 0 }; + + std::string _ext; + std::string _dirname; + std::string _dirpath; + bool _initialized { false }; + + std::unordered_map> _files; + Mutex _filesMutex; + + std::map _unusedFiles; + Mutex _unusedFilesMutex; + size_t _unusedFilesMaxSize { DEFAULT_UNUSED_MAX_SIZE }; + int _lastLRUKey { 0 }; + + size_t _offlineFilesMaxSize { DEFAULT_OFFLINE_MAX_SIZE }; +}; + +class File : public QObject { + Q_OBJECT + +public: + using Key = FileCache::Key; + using Metadata = FileCache::Metadata; + + Key getKey() const { return _key; } + size_t getLength() const { return _length; } + std::string getFilepath() const { return _filepath; } + + virtual ~File(); + /// overrides should call File::deleter to maintain caching behavior + virtual void deleter(); + +protected: + /// when constructed, the file has already been created/written + File(Metadata&& metadata, const std::string& filepath); + +private: + friend class FileCache; + + const Key _key; + const size_t _length; + const std::string _filepath; + + FileCache* _cache; + int _LRUKey { 0 }; + + bool _shouldPersist { false }; +}; + +} + +#endif // hifi_FileCache_h diff --git a/libraries/networking/src/NodePermissions.h b/libraries/networking/src/NodePermissions.h index 5d2755f9b5..6fa005e360 100644 --- a/libraries/networking/src/NodePermissions.h +++ b/libraries/networking/src/NodePermissions.h @@ -13,18 +13,31 @@ #define hifi_NodePermissions_h #include +#include #include #include #include #include - +#include +#include #include "GroupRank.h" class NodePermissions; using NodePermissionsPointer = std::shared_ptr; -using NodePermissionsKey = QPair; // name, rankID +using NodePermissionsKey = std::pair; // name, rankID using NodePermissionsKeyList = QList>; +namespace std { + template<> + struct hash { + size_t operator()(const NodePermissionsKey& key) const { + size_t result = qHash(key.first); + result <<= 32; + result |= qHash(key.second); + return result; + } + }; +} class NodePermissions { public: @@ -100,27 +113,40 @@ public: NodePermissionsMap() { } NodePermissionsPointer& operator[](const NodePermissionsKey& key) { NodePermissionsKey dataKey(key.first.toLower(), key.second); - if (!_data.contains(dataKey)) { + if (0 == _data.count(dataKey)) { _data[dataKey] = NodePermissionsPointer(new NodePermissions(key)); } return _data[dataKey]; } NodePermissionsPointer operator[](const NodePermissionsKey& key) const { - return _data.value(NodePermissionsKey(key.first.toLower(), key.second)); + NodePermissionsPointer result; + auto itr = _data.find(NodePermissionsKey(key.first.toLower(), key.second)); + if (_data.end() != itr) { + result = itr->second; + } + return result; } bool contains(const NodePermissionsKey& key) const { - return _data.contains(NodePermissionsKey(key.first.toLower(), key.second)); + return 0 != _data.count(NodePermissionsKey(key.first.toLower(), key.second)); } - bool contains(const QString& keyFirst, QUuid keySecond) const { - return _data.contains(NodePermissionsKey(keyFirst.toLower(), keySecond)); + bool contains(const QString& keyFirst, const QUuid& keySecond) const { + return 0 != _data.count(NodePermissionsKey(keyFirst.toLower(), keySecond)); } - QList keys() const { return _data.keys(); } - QHash get() { return _data; } + + QList keys() const { + QList result; + for (const auto& entry : _data) { + result.push_back(entry.first); + } + return result; + } + + const std::unordered_map& get() { return _data; } void clear() { _data.clear(); } - void remove(const NodePermissionsKey& key) { _data.remove(key); } + void remove(const NodePermissionsKey& key) { _data.erase(key); } private: - QHash _data; + std::unordered_map _data; }; diff --git a/libraries/networking/src/ResourceCache.cpp b/libraries/networking/src/ResourceCache.cpp index d95c6f140f..0396e0ed94 100644 --- a/libraries/networking/src/ResourceCache.cpp +++ b/libraries/networking/src/ResourceCache.cpp @@ -221,7 +221,7 @@ ResourceCache::ResourceCache(QObject* parent) : QObject(parent) { } ResourceCache::~ResourceCache() { - clearUnusedResource(); + clearUnusedResources(); } void ResourceCache::clearATPAssets() { @@ -265,7 +265,7 @@ void ResourceCache::clearATPAssets() { void ResourceCache::refreshAll() { // Clear all unused resources so we don't have to reload them - clearUnusedResource(); + clearUnusedResources(); resetResourceCounters(); QHash> resources; @@ -418,7 +418,7 @@ void ResourceCache::reserveUnusedResource(qint64 resourceSize) { } } -void ResourceCache::clearUnusedResource() { +void ResourceCache::clearUnusedResources() { // the unused resources may themselves reference resources that will be added to the unused // list on destruction, so keep clearing until there are no references left QWriteLocker locker(&_unusedResourcesLock); diff --git a/libraries/networking/src/ResourceCache.h b/libraries/networking/src/ResourceCache.h index a369416ebe..8f1f1baed2 100644 --- a/libraries/networking/src/ResourceCache.h +++ b/libraries/networking/src/ResourceCache.h @@ -249,6 +249,7 @@ public: void refreshAll(); void refresh(const QUrl& url); + void clearUnusedResources(); signals: void dirty(); @@ -298,7 +299,7 @@ protected: void addUnusedResource(const QSharedPointer& resource); void removeUnusedResource(const QSharedPointer& resource); - + /// Attempt to load a resource if requests are below the limit, otherwise queue the resource for loading /// \return true if the resource began loading, otherwise false if the resource is in the pending queue static bool attemptRequest(QSharedPointer resource); @@ -309,7 +310,6 @@ private: friend class Resource; void reserveUnusedResource(qint64 resourceSize); - void clearUnusedResource(); void resetResourceCounters(); void removeResource(const QUrl& url, qint64 size = 0); diff --git a/libraries/networking/src/UserActivityLoggerScriptingInterface.cpp b/libraries/networking/src/UserActivityLoggerScriptingInterface.cpp index c8a7b61aa7..9c29e87f16 100644 --- a/libraries/networking/src/UserActivityLoggerScriptingInterface.cpp +++ b/libraries/networking/src/UserActivityLoggerScriptingInterface.cpp @@ -61,6 +61,16 @@ void UserActivityLoggerScriptingInterface::palOpened(float secondsOpened) { }); } +void UserActivityLoggerScriptingInterface::makeUserConnection(QString otherID, bool success, QString detailsString) { + QJsonObject payload; + payload["otherUser"] = otherID; + payload["success"] = success; + if (detailsString.length() > 0) { + payload["details"] = detailsString; + } + logAction("makeUserConnection", payload); +} + void UserActivityLoggerScriptingInterface::logAction(QString action, QJsonObject details) { QMetaObject::invokeMethod(&UserActivityLogger::getInstance(), "logAction", Q_ARG(QString, action), diff --git a/libraries/networking/src/UserActivityLoggerScriptingInterface.h b/libraries/networking/src/UserActivityLoggerScriptingInterface.h index cf38450891..b68c7beb95 100644 --- a/libraries/networking/src/UserActivityLoggerScriptingInterface.h +++ b/libraries/networking/src/UserActivityLoggerScriptingInterface.h @@ -29,6 +29,7 @@ public: float tutorialElapsedTime, QString tutorialRunID = "", int tutorialVersion = 0, QString controllerType = ""); Q_INVOKABLE void palAction(QString action, QString target); Q_INVOKABLE void palOpened(float secondsOpen); + Q_INVOKABLE void makeUserConnection(QString otherUser, bool success, QString details=""); private: void logAction(QString action, QJsonObject details = {}); }; diff --git a/libraries/networking/src/udt/PacketQueue.cpp b/libraries/networking/src/udt/PacketQueue.cpp index bb20982ca4..9560f2f187 100644 --- a/libraries/networking/src/udt/PacketQueue.cpp +++ b/libraries/networking/src/udt/PacketQueue.cpp @@ -15,6 +15,10 @@ using namespace udt; +PacketQueue::PacketQueue() { + _channels.emplace_back(new std::list()); +} + MessageNumber PacketQueue::getNextMessageNumber() { static const MessageNumber MAX_MESSAGE_NUMBER = MessageNumber(1) << MESSAGE_NUMBER_SIZE; _currentMessageNumber = (_currentMessageNumber + 1) % MAX_MESSAGE_NUMBER; @@ -24,7 +28,7 @@ MessageNumber PacketQueue::getNextMessageNumber() { bool PacketQueue::isEmpty() const { LockGuard locker(_packetsLock); // Only the main channel and it is empty - return (_channels.size() == 1) && _channels.front().empty(); + return (_channels.size() == 1) && _channels.front()->empty(); } PacketQueue::PacketPointer PacketQueue::takePacket() { @@ -34,19 +38,19 @@ PacketQueue::PacketPointer PacketQueue::takePacket() { } // Find next non empty channel - if (_channels[nextIndex()].empty()) { + if (_channels[nextIndex()]->empty()) { nextIndex(); } auto& channel = _channels[_currentIndex]; - Q_ASSERT(!channel.empty()); + Q_ASSERT(!channel->empty()); // Take front packet - auto packet = std::move(channel.front()); - channel.pop_front(); + auto packet = std::move(channel->front()); + channel->pop_front(); // Remove now empty channel (Don't remove the main channel) - if (channel.empty() && _currentIndex != 0) { - channel.swap(_channels.back()); + if (channel->empty() && _currentIndex != 0) { + channel->swap(*_channels.back()); _channels.pop_back(); --_currentIndex; } @@ -61,7 +65,7 @@ unsigned int PacketQueue::nextIndex() { void PacketQueue::queuePacket(PacketPointer packet) { LockGuard locker(_packetsLock); - _channels.front().push_back(std::move(packet)); + _channels.front()->push_back(std::move(packet)); } void PacketQueue::queuePacketList(PacketListPointer packetList) { @@ -70,5 +74,6 @@ void PacketQueue::queuePacketList(PacketListPointer packetList) { } LockGuard locker(_packetsLock); - _channels.push_back(std::move(packetList->_packets)); + _channels.emplace_back(new std::list()); + _channels.back()->swap(packetList->_packets); } diff --git a/libraries/networking/src/udt/PacketQueue.h b/libraries/networking/src/udt/PacketQueue.h index 69784fd8db..2b3d3a4b5b 100644 --- a/libraries/networking/src/udt/PacketQueue.h +++ b/libraries/networking/src/udt/PacketQueue.h @@ -30,10 +30,11 @@ class PacketQueue { using LockGuard = std::lock_guard; using PacketPointer = std::unique_ptr; using PacketListPointer = std::unique_ptr; - using Channel = std::list; + using Channel = std::unique_ptr>; using Channels = std::vector; public: + PacketQueue(); void queuePacket(PacketPointer packet); void queuePacketList(PacketListPointer packetList); @@ -49,7 +50,7 @@ private: MessageNumber _currentMessageNumber { 0 }; mutable Mutex _packetsLock; // Protects the packets to be sent. - Channels _channels = Channels(1); // One channel per packet list + Main channel + Channels _channels; // One channel per packet list + Main channel unsigned int _currentIndex { 0 }; }; diff --git a/libraries/octree/src/OctreeQuery.cpp b/libraries/octree/src/OctreeQuery.cpp index a639eccaba..7d9fc7d08c 100644 --- a/libraries/octree/src/OctreeQuery.cpp +++ b/libraries/octree/src/OctreeQuery.cpp @@ -142,6 +142,6 @@ int OctreeQuery::parseData(ReceivedMessage& message) { } glm::vec3 OctreeQuery::calculateCameraDirection() const { - glm::vec3 direction = glm::vec3(_cameraOrientation * glm::vec4(IDENTITY_FRONT, 0.0f)); + glm::vec3 direction = glm::vec3(_cameraOrientation * glm::vec4(IDENTITY_FORWARD, 0.0f)); return direction; } diff --git a/libraries/physics/src/EntityMotionState.cpp b/libraries/physics/src/EntityMotionState.cpp index c175a836cc..d383f4c199 100644 --- a/libraries/physics/src/EntityMotionState.cpp +++ b/libraries/physics/src/EntityMotionState.cpp @@ -97,6 +97,21 @@ void EntityMotionState::updateServerPhysicsVariables() { _serverActionData = _entity->getActionData(); } +void EntityMotionState::handleDeactivation() { + // copy _server data to entity + bool success; + _entity->setPosition(_serverPosition, success, false); + _entity->setOrientation(_serverRotation, success, false); + _entity->setVelocity(ENTITY_ITEM_ZERO_VEC3); + _entity->setAngularVelocity(ENTITY_ITEM_ZERO_VEC3); + // and also to RigidBody + btTransform worldTrans; + worldTrans.setOrigin(glmToBullet(_serverPosition)); + worldTrans.setRotation(glmToBullet(_serverRotation)); + _body->setWorldTransform(worldTrans); + // no need to update velocities... should already be zero +} + // virtual void EntityMotionState::handleEasyChanges(uint32_t& flags) { assert(entityTreeIsLocked()); @@ -111,6 +126,8 @@ void EntityMotionState::handleEasyChanges(uint32_t& flags) { flags &= ~Simulation::DIRTY_PHYSICS_ACTIVATION; _body->setActivationState(WANTS_DEACTIVATION); _outgoingPriority = 0; + const float ACTIVATION_EXPIRY = 3.0f; // something larger than the 2.0 hard coded in Bullet + _body->setDeactivationTime(ACTIVATION_EXPIRY); } else { // disowned object is still moving --> start timer for ownership bid // TODO? put a delay in here proportional to distance from object? @@ -221,12 +238,9 @@ void EntityMotionState::getWorldTransform(btTransform& worldTrans) const { } // This callback is invoked by the physics simulation at the end of each simulation step... -// iff the corresponding RigidBody is DYNAMIC and has moved. +// iff the corresponding RigidBody is DYNAMIC and ACTIVE. void EntityMotionState::setWorldTransform(const btTransform& worldTrans) { - if (!_entity) { - return; - } - + assert(_entity); assert(entityTreeIsLocked()); measureBodyAcceleration(); bool positionSuccess; diff --git a/libraries/physics/src/EntityMotionState.h b/libraries/physics/src/EntityMotionState.h index feac47d8ec..380edf3927 100644 --- a/libraries/physics/src/EntityMotionState.h +++ b/libraries/physics/src/EntityMotionState.h @@ -29,6 +29,7 @@ public: virtual ~EntityMotionState(); void updateServerPhysicsVariables(); + void handleDeactivation(); virtual void handleEasyChanges(uint32_t& flags) override; virtual bool handleHardAndEasyChanges(uint32_t& flags, PhysicsEngine* engine) override; diff --git a/libraries/physics/src/PhysicalEntitySimulation.cpp b/libraries/physics/src/PhysicalEntitySimulation.cpp index 903b160a5e..6f5b474810 100644 --- a/libraries/physics/src/PhysicalEntitySimulation.cpp +++ b/libraries/physics/src/PhysicalEntitySimulation.cpp @@ -208,6 +208,7 @@ void PhysicalEntitySimulation::getObjectsToAddToPhysics(VectorOfMotionStates& re assert(!entity->getPhysicsInfo()); if (entity->isDead()) { prepareEntityForDelete(entity); + entityItr = _entitiesToAddToPhysics.erase(entityItr); } else if (!entity->shouldBePhysical()) { // this entity should no longer be on the internal _entitiesToAddToPhysics entityItr = _entitiesToAddToPhysics.erase(entityItr); @@ -259,13 +260,27 @@ void PhysicalEntitySimulation::getObjectsToChange(VectorOfMotionStates& result) _pendingChanges.clear(); } -void PhysicalEntitySimulation::handleOutgoingChanges(const VectorOfMotionStates& motionStates) { +void PhysicalEntitySimulation::handleDeactivatedMotionStates(const VectorOfMotionStates& motionStates) { + for (auto stateItr : motionStates) { + ObjectMotionState* state = &(*stateItr); + assert(state); + if (state->getType() == MOTIONSTATE_TYPE_ENTITY) { + EntityMotionState* entityState = static_cast(state); + entityState->handleDeactivation(); + EntityItemPointer entity = entityState->getEntity(); + _entitiesToSort.insert(entity); + } + } +} + +void PhysicalEntitySimulation::handleChangedMotionStates(const VectorOfMotionStates& motionStates) { QMutexLocker lock(&_mutex); // walk the motionStates looking for those that correspond to entities for (auto stateItr : motionStates) { ObjectMotionState* state = &(*stateItr); - if (state && state->getType() == MOTIONSTATE_TYPE_ENTITY) { + assert(state); + if (state->getType() == MOTIONSTATE_TYPE_ENTITY) { EntityMotionState* entityState = static_cast(state); EntityItemPointer entity = entityState->getEntity(); assert(entity.get()); diff --git a/libraries/physics/src/PhysicalEntitySimulation.h b/libraries/physics/src/PhysicalEntitySimulation.h index af5def9775..5f6185add3 100644 --- a/libraries/physics/src/PhysicalEntitySimulation.h +++ b/libraries/physics/src/PhysicalEntitySimulation.h @@ -56,7 +56,8 @@ public: void setObjectsToChange(const VectorOfMotionStates& objectsToChange); void getObjectsToChange(VectorOfMotionStates& result); - void handleOutgoingChanges(const VectorOfMotionStates& motionStates); + void handleDeactivatedMotionStates(const VectorOfMotionStates& motionStates); + void handleChangedMotionStates(const VectorOfMotionStates& motionStates); void handleCollisionEvents(const CollisionEvents& collisionEvents); EntityEditPacketSender* getPacketSender() { return _entityPacketSender; } @@ -67,7 +68,7 @@ private: SetOfEntities _entitiesToAddToPhysics; SetOfEntityMotionStates _pendingChanges; // EntityMotionStates already in PhysicsEngine that need their physics changed - SetOfEntityMotionStates _outgoingChanges; // EntityMotionStates for which we need to send updates to entity-server + SetOfEntityMotionStates _outgoingChanges; // EntityMotionStates for which we may need to send updates to entity-server SetOfMotionStates _physicalObjects; // MotionStates of entities in PhysicsEngine diff --git a/libraries/physics/src/PhysicsEngine.cpp b/libraries/physics/src/PhysicsEngine.cpp index f57be4eab3..a8a8e6acfd 100644 --- a/libraries/physics/src/PhysicsEngine.cpp +++ b/libraries/physics/src/PhysicsEngine.cpp @@ -143,12 +143,35 @@ void PhysicsEngine::addObjectToDynamicsWorld(ObjectMotionState* motionState) { } void PhysicsEngine::removeObjects(const VectorOfMotionStates& objects) { - // first bump and prune contacts for all objects in the list + // bump and prune contacts for all objects in the list for (auto object : objects) { bumpAndPruneContacts(object); } - // then remove them + if (_activeStaticBodies.size() > 0) { + // _activeStaticBodies was not cleared last frame. + // The only way to get here is if a static object were moved but we did not actually step the simulation last + // frame (because the framerate is faster than our physics simulation rate). When this happens we must scan + // _activeStaticBodies for objects that were recently deleted so we don't try to access a dangling pointer. + for (auto object : objects) { + btRigidBody* body = object->getRigidBody(); + + std::vector::reverse_iterator itr = _activeStaticBodies.rbegin(); + while (itr != _activeStaticBodies.rend()) { + if (body == *itr) { + if (*itr != *(_activeStaticBodies.rbegin())) { + // swap with rbegin + *itr = *(_activeStaticBodies.rbegin()); + } + _activeStaticBodies.pop_back(); + break; + } + ++itr; + } + } + } + + // remove bodies for (auto object : objects) { btRigidBody* body = object->getRigidBody(); if (body) { @@ -449,7 +472,7 @@ const CollisionEvents& PhysicsEngine::getCollisionEvents() { return _collisionEvents; } -const VectorOfMotionStates& PhysicsEngine::getOutgoingChanges() { +const VectorOfMotionStates& PhysicsEngine::getChangedMotionStates() { BT_PROFILE("copyOutgoingChanges"); // Bullet will not deactivate static objects (it doesn't expect them to be active) // so we must deactivate them ourselves diff --git a/libraries/physics/src/PhysicsEngine.h b/libraries/physics/src/PhysicsEngine.h index bbafbb06b6..b2ebe58f08 100644 --- a/libraries/physics/src/PhysicsEngine.h +++ b/libraries/physics/src/PhysicsEngine.h @@ -65,7 +65,8 @@ public: bool hasOutgoingChanges() const { return _hasOutgoingChanges; } /// \return reference to list of changed MotionStates. The list is only valid until beginning of next simulation loop. - const VectorOfMotionStates& getOutgoingChanges(); + const VectorOfMotionStates& getChangedMotionStates(); + const VectorOfMotionStates& getDeactivatedMotionStates() const { return _dynamicsWorld->getDeactivatedMotionStates(); } /// \return reference to list of Collision events. The list is only valid until beginning of next simulation loop. const CollisionEvents& getCollisionEvents(); diff --git a/libraries/physics/src/ShapeFactory.cpp b/libraries/physics/src/ShapeFactory.cpp index 100dab0fd1..35e050024a 100644 --- a/libraries/physics/src/ShapeFactory.cpp +++ b/libraries/physics/src/ShapeFactory.cpp @@ -256,9 +256,20 @@ const btCollisionShape* ShapeFactory::createShapeFromInfo(const ShapeInfo& info) } break; case SHAPE_TYPE_SPHERE: { + glm::vec3 halfExtents = info.getHalfExtents(); + float radius = glm::max(halfExtents.x, glm::max(halfExtents.y, halfExtents.z)); + shape = new btSphereShape(radius); + } + break; + case SHAPE_TYPE_ELLIPSOID: { glm::vec3 halfExtents = info.getHalfExtents(); float radius = halfExtents.x; - if (radius == halfExtents.y && radius == halfExtents.z) { + const float MIN_RADIUS = 0.001f; + const float MIN_RELATIVE_SPHERICAL_ERROR = 0.001f; + if (radius > MIN_RADIUS + && fabsf(radius - halfExtents.y) / radius < MIN_RELATIVE_SPHERICAL_ERROR + && fabsf(radius - halfExtents.z) / radius < MIN_RELATIVE_SPHERICAL_ERROR) { + // close enough to true sphere shape = new btSphereShape(radius); } else { ShapeInfo::PointList points; diff --git a/libraries/physics/src/ThreadSafeDynamicsWorld.cpp b/libraries/physics/src/ThreadSafeDynamicsWorld.cpp index 5fe99f137c..24cfbc2609 100644 --- a/libraries/physics/src/ThreadSafeDynamicsWorld.cpp +++ b/libraries/physics/src/ThreadSafeDynamicsWorld.cpp @@ -120,30 +120,41 @@ void ThreadSafeDynamicsWorld::synchronizeMotionState(btRigidBody* body) { void ThreadSafeDynamicsWorld::synchronizeMotionStates() { BT_PROFILE("synchronizeMotionStates"); _changedMotionStates.clear(); + + // NOTE: m_synchronizeAllMotionStates is 'false' by default for optimization. + // See PhysicsEngine::init() where we call _dynamicsWorld->setForceUpdateAllAabbs(false) if (m_synchronizeAllMotionStates) { //iterate over all collision objects for (int i=0;igetMotionState()) { - synchronizeMotionState(body); - _changedMotionStates.push_back(static_cast(body->getMotionState())); - } + if (body && body->getMotionState()) { + synchronizeMotionState(body); + _changedMotionStates.push_back(static_cast(body->getMotionState())); } } } else { //iterate over all active rigid bodies + // TODO? if this becomes a performance bottleneck we could derive our own SimulationIslandManager + // that remembers a list of objects deactivated last step + _activeStates.clear(); + _deactivatedStates.clear(); for (int i=0;iisActive()) { - if (body->getMotionState()) { + ObjectMotionState* motionState = static_cast(body->getMotionState()); + if (motionState) { + if (body->isActive()) { synchronizeMotionState(body); - _changedMotionStates.push_back(static_cast(body->getMotionState())); + _changedMotionStates.push_back(motionState); + _activeStates.insert(motionState); + } else if (_lastActiveStates.find(motionState) != _lastActiveStates.end()) { + // this object was active last frame but is no longer + _deactivatedStates.push_back(motionState); } } } } + _activeStates.swap(_lastActiveStates); } void ThreadSafeDynamicsWorld::saveKinematicState(btScalar timeStep) { diff --git a/libraries/physics/src/ThreadSafeDynamicsWorld.h b/libraries/physics/src/ThreadSafeDynamicsWorld.h index 68062d8d29..b4fcca8cdb 100644 --- a/libraries/physics/src/ThreadSafeDynamicsWorld.h +++ b/libraries/physics/src/ThreadSafeDynamicsWorld.h @@ -49,12 +49,16 @@ public: float getLocalTimeAccumulation() const { return m_localTime; } const VectorOfMotionStates& getChangedMotionStates() const { return _changedMotionStates; } + const VectorOfMotionStates& getDeactivatedMotionStates() const { return _deactivatedStates; } private: // call this instead of non-virtual btDiscreteDynamicsWorld::synchronizeSingleMotionState() void synchronizeMotionState(btRigidBody* body); VectorOfMotionStates _changedMotionStates; + VectorOfMotionStates _deactivatedStates; + SetOfMotionStates _activeStates; + SetOfMotionStates _lastActiveStates; }; #endif // hifi_ThreadSafeDynamicsWorld_h diff --git a/libraries/plugins/src/plugins/DisplayPlugin.h b/libraries/plugins/src/plugins/DisplayPlugin.h index 2491aed817..754c919fd4 100644 --- a/libraries/plugins/src/plugins/DisplayPlugin.h +++ b/libraries/plugins/src/plugins/DisplayPlugin.h @@ -139,7 +139,7 @@ public: /// By default, all HMDs are stereo virtual bool isStereo() const { return isHmd(); } virtual bool isThrottled() const { return false; } - virtual float getTargetFrameRate() const { return 0.0f; } + virtual float getTargetFrameRate() const { return 1.0f; } virtual bool hasAsyncReprojection() const { return false; } /// Returns a boolean value indicating whether the display is currently visible @@ -189,6 +189,11 @@ public: virtual float renderRate() const { return -1.0f; } // Rate at which we present to the display device virtual float presentRate() const { return -1.0f; } + // Reset the present rate tracking (useful for if the target frame rate changes as in ASW for Oculus) + virtual void resetPresentRate() {} + // Return the present rate as fraction of the target present rate (hopefully 0.0 and 1.0) + virtual float normalizedPresentRate() const { return presentRate() / getTargetFrameRate(); } + // Rate at which old frames are presented to the device display virtual float stutterRate() const { return -1.0f; } // Rate at which new frames are being presented to the display device diff --git a/libraries/procedural/src/procedural/Procedural.cpp b/libraries/procedural/src/procedural/Procedural.cpp index ac6163c227..e4ce3c691a 100644 --- a/libraries/procedural/src/procedural/Procedural.cpp +++ b/libraries/procedural/src/procedural/Procedural.cpp @@ -325,7 +325,7 @@ void Procedural::prepare(gpu::Batch& batch, const glm::vec3& position, const glm auto gpuTexture = _channels[i]->getGPUTexture(); if (gpuTexture) { gpuTexture->setSampler(sampler); - gpuTexture->autoGenerateMips(-1); + gpuTexture->setAutoGenerateMips(true); } batch.setResourceTexture((gpu::uint32)i, gpuTexture); } diff --git a/libraries/recording/src/recording/Deck.cpp b/libraries/recording/src/recording/Deck.cpp index 61eb86c91f..186516e01c 100644 --- a/libraries/recording/src/recording/Deck.cpp +++ b/libraries/recording/src/recording/Deck.cpp @@ -33,6 +33,7 @@ void Deck::queueClip(ClipPointer clip, float timeOffset) { // FIXME disabling multiple clips for now _clips.clear(); + _length = 0.0f; // if the time offset is not zero, wrap in an OffsetClip if (timeOffset != 0.0f) { @@ -153,8 +154,8 @@ void Deck::processFrames() { // if doing relative movement emit looped(); } else { - // otherwise pause playback - pause(); + // otherwise stop playback + stop(); } return; } diff --git a/libraries/render-utils/CMakeLists.txt b/libraries/render-utils/CMakeLists.txt index ecafb8f565..3bf389973a 100644 --- a/libraries/render-utils/CMakeLists.txt +++ b/libraries/render-utils/CMakeLists.txt @@ -3,7 +3,7 @@ AUTOSCRIBE_SHADER_LIB(gpu model render) # pull in the resources.qrc file qt5_add_resources(QT_RESOURCES_FILE "${CMAKE_CURRENT_SOURCE_DIR}/res/fonts/fonts.qrc") setup_hifi_library(Widgets OpenGL Network Qml Quick Script) -link_hifi_libraries(shared gpu model model-networking render animation fbx entities) +link_hifi_libraries(shared ktx gpu model model-networking render animation fbx entities) if (NOT ANDROID) target_nsight() diff --git a/libraries/render-utils/src/AmbientOcclusionEffect.cpp b/libraries/render-utils/src/AmbientOcclusionEffect.cpp index 4a8f4d1dbc..678d8b1baf 100644 --- a/libraries/render-utils/src/AmbientOcclusionEffect.cpp +++ b/libraries/render-utils/src/AmbientOcclusionEffect.cpp @@ -73,12 +73,12 @@ void AmbientOcclusionFramebuffer::allocate() { auto width = _frameSize.x; auto height = _frameSize.y; - - _occlusionTexture = gpu::TexturePointer(gpu::Texture::create2D(gpu::Element::COLOR_RGBA_32, width, height, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_LINEAR_MIP_POINT))); + + _occlusionTexture = gpu::TexturePointer(gpu::Texture::createRenderBuffer(gpu::Element::COLOR_RGBA_32, width, height, gpu::Texture::SINGLE_MIP, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_LINEAR_MIP_POINT))); _occlusionFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create("occlusion")); _occlusionFramebuffer->setRenderBuffer(0, _occlusionTexture); - - _occlusionBlurredTexture = gpu::TexturePointer(gpu::Texture::create2D(gpu::Element::COLOR_RGBA_32, width, height, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_LINEAR_MIP_POINT))); + + _occlusionBlurredTexture = gpu::TexturePointer(gpu::Texture::createRenderBuffer(gpu::Element::COLOR_RGBA_32, width, height, gpu::Texture::SINGLE_MIP, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_LINEAR_MIP_POINT))); _occlusionBlurredFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create("occlusionBlurred")); _occlusionBlurredFramebuffer->setRenderBuffer(0, _occlusionBlurredTexture); } diff --git a/libraries/render-utils/src/AnimDebugDraw.cpp b/libraries/render-utils/src/AnimDebugDraw.cpp index c8746d5c60..162745e76f 100644 --- a/libraries/render-utils/src/AnimDebugDraw.cpp +++ b/libraries/render-utils/src/AnimDebugDraw.cpp @@ -114,9 +114,9 @@ AnimDebugDraw::AnimDebugDraw() : render::ScenePointer scene = AbstractViewStateInterface::instance()->getMain3DScene(); if (scene) { _itemID = scene->allocateID(); - render::PendingChanges pendingChanges; - pendingChanges.resetItem(_itemID, _animDebugDrawPayload); - scene->enqueuePendingChanges(pendingChanges); + render::Transaction transaction; + transaction.resetItem(_itemID, _animDebugDrawPayload); + scene->enqueueTransaction(transaction); } // HACK: add red, green and blue axis at (1,1,1) @@ -142,9 +142,9 @@ void AnimDebugDraw::shutdown() { // remove renderItem from main 3d scene. render::ScenePointer scene = AbstractViewStateInterface::instance()->getMain3DScene(); if (scene && _itemID) { - render::PendingChanges pendingChanges; - pendingChanges.removeItem(_itemID); - scene->enqueuePendingChanges(pendingChanges); + render::Transaction transaction; + transaction.removeItem(_itemID); + scene->enqueueTransaction(transaction); } } @@ -317,8 +317,8 @@ void AnimDebugDraw::update() { return; } - render::PendingChanges pendingChanges; - pendingChanges.updateItem(_itemID, [&](AnimDebugDrawData& data) { + render::Transaction transaction; + transaction.updateItem(_itemID, [&](AnimDebugDrawData& data) { const size_t VERTICES_PER_BONE = (6 + (NUM_CIRCLE_SLICES * 2) * 3); const size_t VERTICES_PER_LINK = 8 * 2; @@ -346,7 +346,9 @@ void AnimDebugDraw::update() { numVerts += (int)markerMap.size() * VERTICES_PER_BONE; auto myAvatarMarkerMap = DebugDraw::getInstance().getMyAvatarMarkerMap(); numVerts += (int)myAvatarMarkerMap.size() * VERTICES_PER_BONE; - numVerts += (int)DebugDraw::getInstance().getRays().size() * VERTICES_PER_RAY; + auto rays = DebugDraw::getInstance().getRays(); + DebugDraw::getInstance().clearRays(); + numVerts += (int)rays.size() * VERTICES_PER_RAY; // allocate verts! std::vector vertices; @@ -398,10 +400,9 @@ void AnimDebugDraw::update() { } // draw rays from shared DebugDraw singleton - for (auto& iter : DebugDraw::getInstance().getRays()) { + for (auto& iter : rays) { addLine(std::get<0>(iter), std::get<1>(iter), std::get<2>(iter), v); } - DebugDraw::getInstance().clearRays(); data._vertexBuffer->resize(sizeof(AnimDebugDrawData::Vertex) * numVerts); data._vertexBuffer->setSubData(0, vertices); @@ -421,5 +422,5 @@ void AnimDebugDraw::update() { data._indexBuffer->setSubData(i, (uint16_t)i);; } }); - scene->enqueuePendingChanges(pendingChanges); + scene->enqueueTransaction(transaction); } diff --git a/libraries/render-utils/src/AntialiasingEffect.cpp b/libraries/render-utils/src/AntialiasingEffect.cpp index 2941197e6d..f11d62acbe 100644 --- a/libraries/render-utils/src/AntialiasingEffect.cpp +++ b/libraries/render-utils/src/AntialiasingEffect.cpp @@ -52,7 +52,7 @@ const gpu::PipelinePointer& Antialiasing::getAntialiasingPipeline() { _antialiasingBuffer = gpu::FramebufferPointer(gpu::Framebuffer::create("antialiasing")); auto format = gpu::Element::COLOR_SRGBA_32; // DependencyManager::get()->getLightingTexture()->getTexelFormat(); auto defaultSampler = gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_POINT); - _antialiasingTexture = gpu::TexturePointer(gpu::Texture::create2D(format, width, height, defaultSampler)); + _antialiasingTexture = gpu::TexturePointer(gpu::Texture::createRenderBuffer(format, width, height, gpu::Texture::SINGLE_MIP, defaultSampler)); _antialiasingBuffer->setRenderBuffer(0, _antialiasingTexture); } diff --git a/libraries/render-utils/src/DebugDeferredBuffer.cpp b/libraries/render-utils/src/DebugDeferredBuffer.cpp index d0e2859e90..e534628c83 100644 --- a/libraries/render-utils/src/DebugDeferredBuffer.cpp +++ b/libraries/render-utils/src/DebugDeferredBuffer.cpp @@ -76,6 +76,7 @@ static const std::string DEFAULT_ROUGHNESS_SHADER { "vec4 getFragmentColor() {" " DeferredFragment frag = unpackDeferredFragmentNoPosition(uv);" " return vec4(vec3(pow(frag.roughness, 1.0 / 2.2)), 1.0);" + // " return vec4(vec3(pow(colorRamp(frag.roughness), vec3(1.0 / 2.2))), 1.0);" " }" }; static const std::string DEFAULT_NORMAL_SHADER { @@ -87,8 +88,9 @@ static const std::string DEFAULT_NORMAL_SHADER { static const std::string DEFAULT_OCCLUSION_SHADER{ "vec4 getFragmentColor() {" - " DeferredFragment frag = unpackDeferredFragmentNoPosition(uv);" - " return vec4(vec3(pow(frag.obscurance, 1.0 / 2.2)), 1.0);" + // " DeferredFragment frag = unpackDeferredFragmentNoPosition(uv);" + // " return vec4(vec3(pow(frag.obscurance, 1.0 / 2.2)), 1.0);" + " return vec4(vec3(pow(texture(specularMap, uv).a, 1.0 / 2.2)), 1.0);" " }" }; @@ -193,6 +195,18 @@ static const std::string DEFAULT_DIFFUSED_NORMAL_CURVATURE_SHADER{ " }" }; +static const std::string DEFAULT_CURVATURE_OCCLUSION_SHADER{ + "vec4 getFragmentColor() {" + " vec4 midNormalCurvature;" + " vec4 lowNormalCurvature;" + " unpackMidLowNormalCurvature(uv, midNormalCurvature, lowNormalCurvature);" + " float ambientOcclusion = curvatureAO(lowNormalCurvature.a * 20.0f) * 0.5f;" + " float ambientOcclusionHF = curvatureAO(midNormalCurvature.a * 8.0f) * 0.5f;" + " ambientOcclusion = min(ambientOcclusion, ambientOcclusionHF);" + " return vec4(vec3(ambientOcclusion), 1.0);" + " }" +}; + static const std::string DEFAULT_DEBUG_SCATTERING_SHADER{ "vec4 getFragmentColor() {" " return vec4(pow(vec3(texture(scatteringMap, uv).xyz), vec3(1.0 / 2.2)), 1.0);" @@ -202,7 +216,7 @@ static const std::string DEFAULT_DEBUG_SCATTERING_SHADER{ static const std::string DEFAULT_AMBIENT_OCCLUSION_SHADER{ "vec4 getFragmentColor() {" - " return vec4(vec3(texture(obscuranceMap, uv).xyz), 1.0);" + " return vec4(vec3(texture(obscuranceMap, uv).x), 1.0);" // When drawing color " return vec4(vec3(texture(occlusionMap, uv).xyz), 1.0);" // when drawing normal" return vec4(normalize(texture(occlusionMap, uv).xyz * 2.0 - vec3(1.0)), 1.0);" " }" @@ -287,6 +301,8 @@ std::string DebugDeferredBuffer::getShaderSourceCode(Mode mode, std::string cust return DEFAULT_DIFFUSED_CURVATURE_SHADER; case DiffusedNormalCurvatureMode: return DEFAULT_DIFFUSED_NORMAL_CURVATURE_SHADER; + case CurvatureOcclusionMode: + return DEFAULT_CURVATURE_OCCLUSION_SHADER; case ScatteringDebugMode: return DEFAULT_DEBUG_SCATTERING_SHADER; case AmbientOcclusionMode: diff --git a/libraries/render-utils/src/DebugDeferredBuffer.h b/libraries/render-utils/src/DebugDeferredBuffer.h index eb1a541d2e..be775e052f 100644 --- a/libraries/render-utils/src/DebugDeferredBuffer.h +++ b/libraries/render-utils/src/DebugDeferredBuffer.h @@ -72,6 +72,7 @@ protected: NormalCurvatureMode, DiffusedCurvatureMode, DiffusedNormalCurvatureMode, + CurvatureOcclusionMode, ScatteringDebugMode, AmbientOcclusionMode, AmbientOcclusionBlurredMode, diff --git a/libraries/render-utils/src/DeferredBuffer.slh b/libraries/render-utils/src/DeferredBuffer.slh index a4b69bd70e..a13c2ec5d1 100644 --- a/libraries/render-utils/src/DeferredBuffer.slh +++ b/libraries/render-utils/src/DeferredBuffer.slh @@ -65,25 +65,4 @@ float packUnlit() { return FRAG_PACK_UNLIT; } - - <@endif@> diff --git a/libraries/render-utils/src/DeferredBufferRead.slh b/libraries/render-utils/src/DeferredBufferRead.slh index 7c81b2c142..315de30fea 100644 --- a/libraries/render-utils/src/DeferredBufferRead.slh +++ b/libraries/render-utils/src/DeferredBufferRead.slh @@ -67,10 +67,8 @@ DeferredFragment unpackDeferredFragmentNoPosition(vec2 texcoord) { frag.scattering = 0.0; unpackModeMetallic(diffuseVal.w, frag.mode, frag.metallic); - //frag.emissive = specularVal.xyz; frag.obscurance = min(specularVal.w, frag.obscurance); - if (frag.mode == FRAG_MODE_SCATTERING) { frag.scattering = specularVal.x; } diff --git a/libraries/render-utils/src/DeferredFramebuffer.cpp b/libraries/render-utils/src/DeferredFramebuffer.cpp index e8783e0e0d..5d345f0851 100644 --- a/libraries/render-utils/src/DeferredFramebuffer.cpp +++ b/libraries/render-utils/src/DeferredFramebuffer.cpp @@ -53,9 +53,9 @@ void DeferredFramebuffer::allocate() { auto defaultSampler = gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_POINT); - _deferredColorTexture = gpu::TexturePointer(gpu::Texture::create2D(colorFormat, width, height, defaultSampler)); - _deferredNormalTexture = gpu::TexturePointer(gpu::Texture::create2D(linearFormat, width, height, defaultSampler)); - _deferredSpecularTexture = gpu::TexturePointer(gpu::Texture::create2D(colorFormat, width, height, defaultSampler)); + _deferredColorTexture = gpu::TexturePointer(gpu::Texture::createRenderBuffer(colorFormat, width, height, gpu::Texture::SINGLE_MIP, defaultSampler)); + _deferredNormalTexture = gpu::TexturePointer(gpu::Texture::createRenderBuffer(linearFormat, width, height, gpu::Texture::SINGLE_MIP, defaultSampler)); + _deferredSpecularTexture = gpu::TexturePointer(gpu::Texture::createRenderBuffer(linearFormat, width, height, gpu::Texture::SINGLE_MIP, defaultSampler)); _deferredFramebuffer->setRenderBuffer(0, _deferredColorTexture); _deferredFramebuffer->setRenderBuffer(1, _deferredNormalTexture); @@ -65,7 +65,7 @@ void DeferredFramebuffer::allocate() { auto depthFormat = gpu::Element(gpu::SCALAR, gpu::UINT32, gpu::DEPTH_STENCIL); // Depth24_Stencil8 texel format if (!_primaryDepthTexture) { - _primaryDepthTexture = gpu::TexturePointer(gpu::Texture::create2D(depthFormat, width, height, defaultSampler)); + _primaryDepthTexture = gpu::TexturePointer(gpu::Texture::createRenderBuffer(depthFormat, width, height, gpu::Texture::SINGLE_MIP, defaultSampler)); } _deferredFramebuffer->setDepthStencilBuffer(_primaryDepthTexture, depthFormat); @@ -75,7 +75,7 @@ void DeferredFramebuffer::allocate() { auto smoothSampler = gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR); - _lightingTexture = gpu::TexturePointer(gpu::Texture::create2D(gpu::Element(gpu::SCALAR, gpu::FLOAT, gpu::R11G11B10), width, height, defaultSampler)); + _lightingTexture = gpu::TexturePointer(gpu::Texture::createRenderBuffer(gpu::Element(gpu::SCALAR, gpu::FLOAT, gpu::R11G11B10), width, height, gpu::Texture::SINGLE_MIP, defaultSampler)); _lightingFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create("lighting")); _lightingFramebuffer->setRenderBuffer(0, _lightingTexture); _lightingFramebuffer->setDepthStencilBuffer(_primaryDepthTexture, depthFormat); diff --git a/libraries/render-utils/src/DeferredLightingEffect.cpp b/libraries/render-utils/src/DeferredLightingEffect.cpp index 6f1152ac16..dc1822c0f5 100644 --- a/libraries/render-utils/src/DeferredLightingEffect.cpp +++ b/libraries/render-utils/src/DeferredLightingEffect.cpp @@ -496,14 +496,14 @@ void PreparePrimaryFramebuffer::run(const SceneContextPointer& sceneContext, con auto colorFormat = gpu::Element::COLOR_SRGBA_32; auto defaultSampler = gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_POINT); - auto primaryColorTexture = gpu::TexturePointer(gpu::Texture::create2D(colorFormat, frameSize.x, frameSize.y, defaultSampler)); + auto primaryColorTexture = gpu::TexturePointer(gpu::Texture::createRenderBuffer(colorFormat, frameSize.x, frameSize.y, gpu::Texture::SINGLE_MIP, defaultSampler)); _primaryFramebuffer->setRenderBuffer(0, primaryColorTexture); auto depthFormat = gpu::Element(gpu::SCALAR, gpu::UINT32, gpu::DEPTH_STENCIL); // Depth24_Stencil8 texel format - auto primaryDepthTexture = gpu::TexturePointer(gpu::Texture::create2D(depthFormat, frameSize.x, frameSize.y, defaultSampler)); + auto primaryDepthTexture = gpu::TexturePointer(gpu::Texture::createRenderBuffer(depthFormat, frameSize.x, frameSize.y, gpu::Texture::SINGLE_MIP, defaultSampler)); _primaryFramebuffer->setDepthStencilBuffer(primaryDepthTexture, depthFormat); } diff --git a/libraries/render-utils/src/FramebufferCache.cpp b/libraries/render-utils/src/FramebufferCache.cpp index 27429595b4..72b3c2ceb4 100644 --- a/libraries/render-utils/src/FramebufferCache.cpp +++ b/libraries/render-utils/src/FramebufferCache.cpp @@ -21,7 +21,6 @@ void FramebufferCache::setFrameBufferSize(QSize frameBufferSize) { //If the size changed, we need to delete our FBOs if (_frameBufferSize != frameBufferSize) { _frameBufferSize = frameBufferSize; - _selfieFramebuffer.reset(); { std::unique_lock lock(_mutex); _cachedFramebuffers.clear(); @@ -30,16 +29,8 @@ void FramebufferCache::setFrameBufferSize(QSize frameBufferSize) { } void FramebufferCache::createPrimaryFramebuffer() { - auto colorFormat = gpu::Element::COLOR_SRGBA_32; - auto width = _frameBufferSize.width(); - auto height = _frameBufferSize.height(); - auto defaultSampler = gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_POINT); - _selfieFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create("selfie")); - auto tex = gpu::TexturePointer(gpu::Texture::create2D(colorFormat, width * 0.5, height * 0.5, defaultSampler)); - _selfieFramebuffer->setRenderBuffer(0, tex); - auto smoothSampler = gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR); } @@ -60,10 +51,3 @@ void FramebufferCache::releaseFramebuffer(const gpu::FramebufferPointer& framebu _cachedFramebuffers.push_back(framebuffer); } } - -gpu::FramebufferPointer FramebufferCache::getSelfieFramebuffer() { - if (!_selfieFramebuffer) { - createPrimaryFramebuffer(); - } - return _selfieFramebuffer; -} diff --git a/libraries/render-utils/src/FramebufferCache.h b/libraries/render-utils/src/FramebufferCache.h index f74d224a61..8065357615 100644 --- a/libraries/render-utils/src/FramebufferCache.h +++ b/libraries/render-utils/src/FramebufferCache.h @@ -27,9 +27,6 @@ public: void setFrameBufferSize(QSize frameBufferSize); const QSize& getFrameBufferSize() const { return _frameBufferSize; } - /// Returns the framebuffer object used to render selfie maps; - gpu::FramebufferPointer getSelfieFramebuffer(); - /// Returns a free framebuffer with a single color attachment for temp or intra-frame operations gpu::FramebufferPointer getFramebuffer(); @@ -42,8 +39,6 @@ private: gpu::FramebufferPointer _shadowFramebuffer; - gpu::FramebufferPointer _selfieFramebuffer; - QSize _frameBufferSize{ 100, 100 }; std::mutex _mutex; diff --git a/libraries/render-utils/src/GeometryCache.cpp b/libraries/render-utils/src/GeometryCache.cpp index c277b9be64..e0dee7b953 100644 --- a/libraries/render-utils/src/GeometryCache.cpp +++ b/libraries/render-utils/src/GeometryCache.cpp @@ -414,8 +414,6 @@ _nextID(0) { // Set the defaults needed for a simple program batch.setResourceTexture(render::ShapePipeline::Slot::MAP::ALBEDO, DependencyManager::get()->getWhiteTexture()); - batch.setResourceTexture(render::ShapePipeline::Slot::MAP::NORMAL_FITTING, - DependencyManager::get()->getNormalFittingTexture()); } ); GeometryCache::_simpleTransparentPipeline = @@ -424,8 +422,6 @@ _nextID(0) { // Set the defaults needed for a simple program batch.setResourceTexture(render::ShapePipeline::Slot::MAP::ALBEDO, DependencyManager::get()->getWhiteTexture()); - batch.setResourceTexture(render::ShapePipeline::Slot::MAP::NORMAL_FITTING, - DependencyManager::get()->getNormalFittingTexture()); } ); GeometryCache::_simpleWirePipeline = @@ -1770,7 +1766,6 @@ static void buildWebShader(const std::string& vertShaderText, const std::string& shaderPointerOut = gpu::Shader::createProgram(VS, PS); gpu::Shader::BindingSet slotBindings; - slotBindings.insert(gpu::Shader::Binding(std::string("normalFittingMap"), render::ShapePipeline::Slot::MAP::NORMAL_FITTING)); gpu::Shader::makeProgram(*shaderPointerOut, slotBindings); auto state = std::make_shared(); state->setCullMode(gpu::State::CULL_NONE); @@ -1784,9 +1779,6 @@ static void buildWebShader(const std::string& vertShaderText, const std::string& void GeometryCache::bindOpaqueWebBrowserProgram(gpu::Batch& batch, bool isAA) { batch.setPipeline(getOpaqueWebBrowserProgram(isAA)); - // Set a default normal map - batch.setResourceTexture(render::ShapePipeline::Slot::MAP::NORMAL_FITTING, - DependencyManager::get()->getNormalFittingTexture()); } gpu::PipelinePointer GeometryCache::getOpaqueWebBrowserProgram(bool isAA) { @@ -1802,9 +1794,6 @@ gpu::PipelinePointer GeometryCache::getOpaqueWebBrowserProgram(bool isAA) { void GeometryCache::bindTransparentWebBrowserProgram(gpu::Batch& batch, bool isAA) { batch.setPipeline(getTransparentWebBrowserProgram(isAA)); - // Set a default normal map - batch.setResourceTexture(render::ShapePipeline::Slot::MAP::NORMAL_FITTING, - DependencyManager::get()->getNormalFittingTexture()); } gpu::PipelinePointer GeometryCache::getTransparentWebBrowserProgram(bool isAA) { @@ -1827,9 +1816,6 @@ void GeometryCache::bindSimpleProgram(gpu::Batch& batch, bool textured, bool tra batch.setResourceTexture(render::ShapePipeline::Slot::MAP::ALBEDO, DependencyManager::get()->getWhiteTexture()); } - // Set a default normal map - batch.setResourceTexture(render::ShapePipeline::Slot::MAP::NORMAL_FITTING, - DependencyManager::get()->getNormalFittingTexture()); } gpu::PipelinePointer GeometryCache::getSimplePipeline(bool textured, bool transparent, bool culled, bool unlit, bool depthBiased) { @@ -1846,7 +1832,6 @@ gpu::PipelinePointer GeometryCache::getSimplePipeline(bool textured, bool transp _unlitShader = gpu::Shader::createProgram(VS, PSUnlit); gpu::Shader::BindingSet slotBindings; - slotBindings.insert(gpu::Shader::Binding(std::string("normalFittingMap"), render::ShapePipeline::Slot::MAP::NORMAL_FITTING)); gpu::Shader::makeProgram(*_simpleShader, slotBindings); gpu::Shader::makeProgram(*_unlitShader, slotBindings); }); diff --git a/libraries/render-utils/src/LightAmbient.slh b/libraries/render-utils/src/LightAmbient.slh index 15e23015cb..5f74b46d3e 100644 --- a/libraries/render-utils/src/LightAmbient.slh +++ b/libraries/render-utils/src/LightAmbient.slh @@ -30,9 +30,8 @@ vec3 fresnelSchlickAmbient(vec3 fresnelColor, vec3 lightDir, vec3 halfDir, float <$declareSkyboxMap()$> <@endif@> -vec3 evalAmbientSpecularIrradiance(LightAmbient ambient, vec3 fragEyeDir, vec3 fragNormal, float roughness, vec3 fresnel) { +vec3 evalAmbientSpecularIrradiance(LightAmbient ambient, vec3 fragEyeDir, vec3 fragNormal, float roughness) { vec3 direction = -reflect(fragEyeDir, fragNormal); - vec3 ambientFresnel = fresnelSchlickAmbient(fresnel, fragEyeDir, fragNormal, 1.0 - roughness); vec3 specularLight; <@if supportIfAmbientMapElseAmbientSphere@> if (getLightHasAmbientMap(ambient)) @@ -40,7 +39,7 @@ vec3 evalAmbientSpecularIrradiance(LightAmbient ambient, vec3 fragEyeDir, vec3 f <@if supportAmbientMap@> { float levels = getLightAmbientMapNumMips(ambient); - float lod = min(floor((roughness)* levels), levels); + float lod = min(((roughness)* levels), levels); specularLight = evalSkyboxLight(direction, lod).xyz; } <@endif@> @@ -53,7 +52,7 @@ vec3 evalAmbientSpecularIrradiance(LightAmbient ambient, vec3 fragEyeDir, vec3 f } <@endif@> - return specularLight * ambientFresnel; + return specularLight; } <@endfunc@> @@ -63,7 +62,7 @@ vec3 evalAmbientSpecularIrradiance(LightAmbient ambient, vec3 fragEyeDir, vec3 f <@if supportScattering@> float curvatureAO(in float k) { - return 1.0f - (0.0022f * k * k) + (0.0776f * k) + 0.7369; + return 1.0f - (0.0022f * k * k) + (0.0776f * k) + 0.7369f; } <@endif@> @@ -74,21 +73,22 @@ void evalLightingAmbient(out vec3 diffuse, out vec3 specular, LightAmbient ambie <@endif@> ) { + // Fresnel + vec3 ambientFresnel = fresnelSchlickAmbient(fresnel, eyeDir, normal, 1.0 - roughness); + // Diffuse from ambient - diffuse = (1.0 - metallic) * sphericalHarmonics_evalSphericalLight(getLightAmbientSphere(ambient), normal).xyz; + diffuse = (1.0 - metallic) * (vec3(1.0) - ambientFresnel) * sphericalHarmonics_evalSphericalLight(getLightAmbientSphere(ambient), normal).xyz; // Specular highlight from ambient - specular = evalAmbientSpecularIrradiance(ambient, eyeDir, normal, roughness, fresnel) * obscurance * getLightAmbientIntensity(ambient); - + specular = evalAmbientSpecularIrradiance(ambient, eyeDir, normal, roughness) * ambientFresnel; <@if supportScattering@> - float ambientOcclusion = curvatureAO(lowNormalCurvature.w * 20.0f) * 0.5f; - float ambientOcclusionHF = curvatureAO(midNormalCurvature.w * 8.0f) * 0.5f; - ambientOcclusion = min(ambientOcclusion, ambientOcclusionHF); - - obscurance = min(obscurance, ambientOcclusion); - if (scattering * isScatteringEnabled() > 0.0) { + float ambientOcclusion = curvatureAO(lowNormalCurvature.w * 20.0f) * 0.5f; + float ambientOcclusionHF = curvatureAO(midNormalCurvature.w * 8.0f) * 0.5f; + ambientOcclusion = min(ambientOcclusion, ambientOcclusionHF); + + obscurance = min(obscurance, ambientOcclusion); // Diffuse from ambient diffuse = sphericalHarmonics_evalSphericalLight(getLightAmbientSphere(ambient), lowNormalCurvature.xyz).xyz; diff --git a/libraries/render-utils/src/LightStage.cpp b/libraries/render-utils/src/LightStage.cpp index 66a9797d3c..dd6a046dea 100644 --- a/libraries/render-utils/src/LightStage.cpp +++ b/libraries/render-utils/src/LightStage.cpp @@ -27,9 +27,9 @@ void LightStage::Shadow::setKeylightFrustum(const ViewFrustum& viewFrustum, floa const auto& direction = glm::normalize(_light->getDirection()); glm::quat orientation; if (direction == IDENTITY_UP) { - orientation = glm::quat(glm::mat3(-IDENTITY_RIGHT, IDENTITY_FRONT, -IDENTITY_UP)); + orientation = glm::quat(glm::mat3(-IDENTITY_RIGHT, IDENTITY_FORWARD, -IDENTITY_UP)); } else if (direction == -IDENTITY_UP) { - orientation = glm::quat(glm::mat3(IDENTITY_RIGHT, IDENTITY_FRONT, IDENTITY_UP)); + orientation = glm::quat(glm::mat3(IDENTITY_RIGHT, IDENTITY_FORWARD, IDENTITY_UP)); } else { auto side = glm::normalize(glm::cross(direction, IDENTITY_UP)); auto up = glm::normalize(glm::cross(side, direction)); diff --git a/libraries/render-utils/src/LightingModel.cpp b/libraries/render-utils/src/LightingModel.cpp index 47af83da36..bd321bad95 100644 --- a/libraries/render-utils/src/LightingModel.cpp +++ b/libraries/render-utils/src/LightingModel.cpp @@ -133,6 +133,7 @@ void LightingModel::setSpotLight(bool enable) { bool LightingModel::isSpotLightEnabled() const { return (bool)_parametersBuffer.get().enableSpotLight; } + void LightingModel::setShowLightContour(bool enable) { if (enable != isShowLightContourEnabled()) { _parametersBuffer.edit().showLightContour = (float)enable; @@ -142,6 +143,14 @@ bool LightingModel::isShowLightContourEnabled() const { return (bool)_parametersBuffer.get().showLightContour; } +void LightingModel::setWireframe(bool enable) { + if (enable != isWireframeEnabled()) { + _parametersBuffer.edit().enableWireframe = (float)enable; + } +} +bool LightingModel::isWireframeEnabled() const { + return (bool)_parametersBuffer.get().enableWireframe; +} MakeLightingModel::MakeLightingModel() { _lightingModel = std::make_shared(); } @@ -167,6 +176,7 @@ void MakeLightingModel::configure(const Config& config) { _lightingModel->setSpotLight(config.enableSpotLight); _lightingModel->setShowLightContour(config.showLightContour); + _lightingModel->setWireframe(config.enableWireframe); } void MakeLightingModel::run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext, LightingModelPointer& lightingModel) { diff --git a/libraries/render-utils/src/LightingModel.h b/libraries/render-utils/src/LightingModel.h index 45514654f2..c1189d5160 100644 --- a/libraries/render-utils/src/LightingModel.h +++ b/libraries/render-utils/src/LightingModel.h @@ -64,6 +64,9 @@ public: void setShowLightContour(bool enable); bool isShowLightContourEnabled() const; + void setWireframe(bool enable); + bool isWireframeEnabled() const; + UniformBufferView getParametersBuffer() const { return _parametersBuffer; } protected: @@ -89,13 +92,12 @@ protected: float enablePointLight{ 1.0f }; float enableSpotLight{ 1.0f }; - float showLightContour{ 0.0f }; // false by default + float showLightContour { 0.0f }; // false by default float enableObscurance{ 1.0f }; float enableMaterialTexturing { 1.0f }; - - float spares{ 0.0f }; + float enableWireframe { 0.0f }; // false by default Parameters() {} }; @@ -129,6 +131,7 @@ class MakeLightingModelConfig : public render::Job::Config { Q_PROPERTY(bool enablePointLight MEMBER enablePointLight NOTIFY dirty) Q_PROPERTY(bool enableSpotLight MEMBER enableSpotLight NOTIFY dirty) + Q_PROPERTY(bool enableWireframe MEMBER enableWireframe NOTIFY dirty) Q_PROPERTY(bool showLightContour MEMBER showLightContour NOTIFY dirty) public: @@ -152,9 +155,10 @@ public: bool enablePointLight{ true }; bool enableSpotLight{ true }; - bool showLightContour { false }; // false by default + bool enableWireframe { false }; // false by default + signals: void dirty(); }; diff --git a/libraries/render-utils/src/LightingModel.slh b/libraries/render-utils/src/LightingModel.slh index 74285aa6a9..209a1f38d6 100644 --- a/libraries/render-utils/src/LightingModel.slh +++ b/libraries/render-utils/src/LightingModel.slh @@ -17,7 +17,7 @@ struct LightingModel { vec4 _UnlitEmissiveLightmapBackground; vec4 _ScatteringDiffuseSpecularAlbedo; vec4 _AmbientDirectionalPointSpot; - vec4 _ShowContourObscuranceSpare2; + vec4 _ShowContourObscuranceWireframe; }; uniform lightingModelBuffer{ @@ -37,7 +37,7 @@ float isBackgroundEnabled() { return lightingModel._UnlitEmissiveLightmapBackground.w; } float isObscuranceEnabled() { - return lightingModel._ShowContourObscuranceSpare2.y; + return lightingModel._ShowContourObscuranceWireframe.y; } float isScatteringEnabled() { @@ -67,9 +67,12 @@ float isSpotEnabled() { } float isShowLightContour() { - return lightingModel._ShowContourObscuranceSpare2.x; + return lightingModel._ShowContourObscuranceWireframe.x; } +float isWireframeEnabled() { + return lightingModel._ShowContourObscuranceWireframe.z; +} <@endfunc@> <$declareLightingModel()$> diff --git a/libraries/render-utils/src/MaterialTextures.slh b/libraries/render-utils/src/MaterialTextures.slh index 6d2ad23c21..7b73896cc5 100644 --- a/libraries/render-utils/src/MaterialTextures.slh +++ b/libraries/render-utils/src/MaterialTextures.slh @@ -64,7 +64,7 @@ float fetchRoughnessMap(vec2 uv) { uniform sampler2D normalMap; vec3 fetchNormalMap(vec2 uv) { // unpack normal, swizzle to get into hifi tangent space with Y axis pointing out - return normalize(texture(normalMap, uv).xzy -vec3(0.5, 0.5, 0.5)); + return normalize(texture(normalMap, uv).rbg -vec3(0.5, 0.5, 0.5)); } <@endif@> diff --git a/libraries/render-utils/src/MeshPartPayload.cpp b/libraries/render-utils/src/MeshPartPayload.cpp index 4cb4e2a316..51ce0fffa7 100644 --- a/libraries/render-utils/src/MeshPartPayload.cpp +++ b/libraries/render-utils/src/MeshPartPayload.cpp @@ -97,6 +97,8 @@ ShapeKey MeshPartPayload::getShapeKey() const { } ShapeKey::Builder builder; + builder.withMaterial(); + if (drawMaterialKey.isTranslucent()) { builder.withTranslucent(); } @@ -166,8 +168,6 @@ void MeshPartPayload::bindMaterial(gpu::Batch& batch, const ShapePipeline::Locat } else { batch.setResourceTexture(ShapePipeline::Slot::ALBEDO, textureCache->getGrayTexture()); } - } else { - batch.setResourceTexture(ShapePipeline::Slot::ALBEDO, textureCache->getWhiteTexture()); } // Roughness map @@ -180,8 +180,6 @@ void MeshPartPayload::bindMaterial(gpu::Batch& batch, const ShapePipeline::Locat } else { batch.setResourceTexture(ShapePipeline::Slot::MAP::ROUGHNESS, textureCache->getWhiteTexture()); } - } else { - batch.setResourceTexture(ShapePipeline::Slot::MAP::ROUGHNESS, textureCache->getWhiteTexture()); } // Normal map @@ -194,8 +192,6 @@ void MeshPartPayload::bindMaterial(gpu::Batch& batch, const ShapePipeline::Locat } else { batch.setResourceTexture(ShapePipeline::Slot::MAP::NORMAL, textureCache->getBlueTexture()); } - } else { - batch.setResourceTexture(ShapePipeline::Slot::MAP::NORMAL, nullptr); } // Metallic map @@ -208,8 +204,6 @@ void MeshPartPayload::bindMaterial(gpu::Batch& batch, const ShapePipeline::Locat } else { batch.setResourceTexture(ShapePipeline::Slot::MAP::METALLIC, textureCache->getBlackTexture()); } - } else { - batch.setResourceTexture(ShapePipeline::Slot::MAP::METALLIC, nullptr); } // Occlusion map @@ -222,8 +216,6 @@ void MeshPartPayload::bindMaterial(gpu::Batch& batch, const ShapePipeline::Locat } else { batch.setResourceTexture(ShapePipeline::Slot::MAP::OCCLUSION, textureCache->getWhiteTexture()); } - } else { - batch.setResourceTexture(ShapePipeline::Slot::MAP::OCCLUSION, nullptr); } // Scattering map @@ -236,8 +228,6 @@ void MeshPartPayload::bindMaterial(gpu::Batch& batch, const ShapePipeline::Locat } else { batch.setResourceTexture(ShapePipeline::Slot::MAP::SCATTERING, textureCache->getWhiteTexture()); } - } else { - batch.setResourceTexture(ShapePipeline::Slot::MAP::SCATTERING, nullptr); } // Emissive / Lightmap @@ -257,8 +247,6 @@ void MeshPartPayload::bindMaterial(gpu::Batch& batch, const ShapePipeline::Locat } else { batch.setResourceTexture(ShapePipeline::Slot::MAP::EMISSIVE_LIGHTMAP, textureCache->getBlackTexture()); } - } else { - batch.setResourceTexture(ShapePipeline::Slot::MAP::EMISSIVE_LIGHTMAP, nullptr); } } @@ -370,19 +358,12 @@ void ModelMeshPartPayload::notifyLocationChanged() { } -void ModelMeshPartPayload::updateTransformForSkinnedMesh(const Transform& transform, const QVector& clusterMatrices) { - _transform = transform; - - if (clusterMatrices.size() > 0) { - _worldBound = _adjustedLocalBound; - _worldBound.transform(_transform); - if (clusterMatrices.size() == 1) { - _transform = _transform.worldTransform(Transform(clusterMatrices[0])); - } - } else { - _worldBound = _localBound; - _worldBound.transform(_transform); - } +void ModelMeshPartPayload::updateTransformForSkinnedMesh(const Transform& renderTransform, const Transform& boundTransform, + const gpu::BufferPointer& buffer) { + _transform = renderTransform; + _worldBound = _adjustedLocalBound; + _worldBound.transform(boundTransform); + _clusterBuffer = buffer; } ItemKey ModelMeshPartPayload::getKey() const { @@ -478,6 +459,8 @@ ShapeKey ModelMeshPartPayload::getShapeKey() const { } ShapeKey::Builder builder; + builder.withMaterial(); + if (isTranslucent || _fadeState != FADE_COMPLETE) { builder.withTranslucent(); } @@ -528,9 +511,8 @@ void ModelMeshPartPayload::bindMesh(gpu::Batch& batch) const { void ModelMeshPartPayload::bindTransform(gpu::Batch& batch, const ShapePipeline::LocationsPointer locations, RenderArgs::RenderMode renderMode) const { // Still relying on the raw data from the model - const Model::MeshState& state = _model->getMeshState(_meshIndex); - if (state.clusterBuffer) { - batch.setUniformBuffer(ShapePipeline::Slot::BUFFER::SKINNING, state.clusterBuffer); + if (_clusterBuffer) { + batch.setUniformBuffer(ShapePipeline::Slot::BUFFER::SKINNING, _clusterBuffer); } batch.setModelTransform(_transform); } @@ -586,8 +568,6 @@ void ModelMeshPartPayload::render(RenderArgs* args) const { auto locations = args->_pipeline->locations; assert(locations); - // Bind the model transform and the skinCLusterMatrices if needed - _model->updateClusterMatrices(); bindTransform(batch, locations, args->_renderMode); //Bind the index buffer and vertex buffer and Blend shapes if needed diff --git a/libraries/render-utils/src/MeshPartPayload.h b/libraries/render-utils/src/MeshPartPayload.h index c585c95025..ef74011c40 100644 --- a/libraries/render-utils/src/MeshPartPayload.h +++ b/libraries/render-utils/src/MeshPartPayload.h @@ -89,8 +89,9 @@ public: typedef Payload::DataPointer Pointer; void notifyLocationChanged() override; - void updateTransformForSkinnedMesh(const Transform& transform, - const QVector& clusterMatrices); + void updateTransformForSkinnedMesh(const Transform& renderTransform, + const Transform& boundTransform, + const gpu::BufferPointer& buffer); float computeFadeAlpha() const; @@ -108,6 +109,7 @@ public: void computeAdjustedLocalBound(const QVector& clusterMatrices); + gpu::BufferPointer _clusterBuffer; Model* _model; int _meshIndex; diff --git a/libraries/render-utils/src/Model.cpp b/libraries/render-utils/src/Model.cpp index adfffe2614..c274d69af6 100644 --- a/libraries/render-utils/src/Model.cpp +++ b/libraries/render-utils/src/Model.cpp @@ -96,9 +96,6 @@ Model::Model(RigPointer rig, QObject* parent, SpatiallyNestable* spatiallyNestab _isVisible(true), _blendNumber(0), _appliedBlendNumber(0), - _calculatedMeshPartBoxesValid(false), - _calculatedMeshBoxesValid(false), - _calculatedMeshTrianglesValid(false), _isWireframe(false), _rig(rig) { @@ -179,11 +176,11 @@ void Model::setOffset(const glm::vec3& offset) { } void Model::calculateTextureInfo() { - if (!_hasCalculatedTextureInfo && isLoaded() && getGeometry()->areTexturesLoaded() && !_modelMeshRenderItems.isEmpty()) { + if (!_hasCalculatedTextureInfo && isLoaded() && getGeometry()->areTexturesLoaded() && !_modelMeshRenderItemsMap.isEmpty()) { size_t textureSize = 0; int textureCount = 0; bool allTexturesLoaded = true; - foreach(auto renderItem, _modelMeshRenderItemsSet) { + foreach(auto renderItem, _modelMeshRenderItems) { auto meshPart = renderItem.get(); textureSize += meshPart->getMaterialTextureSize(); textureCount += meshPart->getMaterialTextureCount(); @@ -230,25 +227,29 @@ void Model::updateRenderItems() { return; } + // lazy update of cluster matrices used for rendering. + // We need to update them here so we can correctly update the bounding box. + self->updateClusterMatrices(); + render::ScenePointer scene = AbstractViewStateInterface::instance()->getMain3DScene(); uint32_t deleteGeometryCounter = self->_deleteGeometryCounter; - render::PendingChanges pendingChanges; - foreach (auto itemID, self->_modelMeshRenderItems.keys()) { - pendingChanges.updateItem(itemID, [deleteGeometryCounter](ModelMeshPartPayload& data) { + render::Transaction transaction; + foreach (auto itemID, self->_modelMeshRenderItemsMap.keys()) { + transaction.updateItem(itemID, [deleteGeometryCounter](ModelMeshPartPayload& data) { if (data._model && data._model->isLoaded()) { // Ensure the model geometry was not reset between frames if (deleteGeometryCounter == data._model->_deleteGeometryCounter) { Transform modelTransform = data._model->getTransform(); modelTransform.setScale(glm::vec3(1.0f)); - // lazy update of cluster matrices used for rendering. We need to update them here, so we can correctly update the bounding box. - data._model->updateClusterMatrices(); - - // update the model transform and bounding box for this render item. - const Model::MeshState& state = data._model->_meshStates.at(data._meshIndex); - data.updateTransformForSkinnedMesh(modelTransform, state.clusterMatrices); + const Model::MeshState& state = data._model->getMeshState(data._meshIndex); + Transform renderTransform = modelTransform; + if (state.clusterMatrices.size() == 1) { + renderTransform = modelTransform.worldTransform(Transform(state.clusterMatrices[0])); + } + data.updateTransformForSkinnedMesh(renderTransform, modelTransform, state.clusterBuffer); } } }); @@ -258,14 +259,14 @@ void Model::updateRenderItems() { Transform collisionMeshOffset; collisionMeshOffset.setIdentity(); Transform modelTransform = self->getTransform(); - foreach (auto itemID, self->_collisionRenderItems.keys()) { - pendingChanges.updateItem(itemID, [modelTransform, collisionMeshOffset](MeshPartPayload& data) { + foreach(auto itemID, self->_collisionRenderItemsMap.keys()) { + transaction.updateItem(itemID, [modelTransform, collisionMeshOffset](MeshPartPayload& data) { // update the model transform for this render item. data.updateTransform(modelTransform, collisionMeshOffset); }); } - scene->enqueuePendingChanges(pendingChanges); + scene->enqueueTransaction(transaction); }); } @@ -360,53 +361,43 @@ bool Model::findRayIntersectionAgainstSubMeshes(const glm::vec3& origin, const g // we can use the AABox's ray intersection by mapping our origin and direction into the model frame // and testing intersection there. if (modelFrameBox.findRayIntersection(modelFrameOrigin, modelFrameDirection, distance, face, surfaceNormal)) { + QMutexLocker locker(&_mutex); + float bestDistance = std::numeric_limits::max(); - - float distanceToSubMesh; - BoxFace subMeshFace; - glm::vec3 subMeshSurfaceNormal; int subMeshIndex = 0; - const FBXGeometry& geometry = getFBXGeometry(); - // If we hit the models box, then consider the submeshes... - _mutex.lock(); - if (!_calculatedMeshBoxesValid || (pickAgainstTriangles && !_calculatedMeshTrianglesValid)) { - recalculateMeshBoxes(pickAgainstTriangles); + if (!_triangleSetsValid) { + calculateTriangleSets(); } - for (const auto& subMeshBox : _calculatedMeshBoxes) { + glm::mat4 meshToModelMatrix = glm::scale(_scale) * glm::translate(_offset); + glm::mat4 meshToWorldMatrix = createMatFromQuatAndPos(_rotation, _translation) * meshToModelMatrix; + glm::mat4 worldToMeshMatrix = glm::inverse(meshToWorldMatrix); - if (subMeshBox.findRayIntersection(origin, direction, distanceToSubMesh, subMeshFace, subMeshSurfaceNormal)) { - if (distanceToSubMesh < bestDistance) { - if (pickAgainstTriangles) { - // check our triangles here.... - const QVector& meshTriangles = _calculatedMeshTriangles[subMeshIndex]; - for(const auto& triangle : meshTriangles) { - float thisTriangleDistance; - if (findRayTriangleIntersection(origin, direction, triangle, thisTriangleDistance)) { - if (thisTriangleDistance < bestDistance) { - bestDistance = thisTriangleDistance; - intersectedSomething = true; - face = subMeshFace; - surfaceNormal = triangle.getNormal(); - extraInfo = geometry.getModelNameOfMesh(subMeshIndex); - } - } - } - } else { - // this is the non-triangle picking case... - bestDistance = distanceToSubMesh; - intersectedSomething = true; - face = subMeshFace; - surfaceNormal = subMeshSurfaceNormal; - extraInfo = geometry.getModelNameOfMesh(subMeshIndex); - } + glm::vec3 meshFrameOrigin = glm::vec3(worldToMeshMatrix * glm::vec4(origin, 1.0f)); + glm::vec3 meshFrameDirection = glm::vec3(worldToMeshMatrix * glm::vec4(direction, 0.0f)); + + for (const auto& triangleSet : _modelSpaceMeshTriangleSets) { + float triangleSetDistance = 0.0f; + BoxFace triangleSetFace; + glm::vec3 triangleSetNormal; + if (triangleSet.findRayIntersection(meshFrameOrigin, meshFrameDirection, triangleSetDistance, triangleSetFace, triangleSetNormal, pickAgainstTriangles)) { + + glm::vec3 meshIntersectionPoint = meshFrameOrigin + (meshFrameDirection * triangleSetDistance); + glm::vec3 worldIntersectionPoint = glm::vec3(meshToWorldMatrix * glm::vec4(meshIntersectionPoint, 1.0f)); + float worldDistance = glm::distance(origin, worldIntersectionPoint); + + if (worldDistance < bestDistance) { + bestDistance = worldDistance; + intersectedSomething = true; + face = triangleSetFace; + surfaceNormal = glm::vec3(meshToWorldMatrix * glm::vec4(triangleSetNormal, 0.0f)); + extraInfo = geometry.getModelNameOfMesh(subMeshIndex); } } subMeshIndex++; } - _mutex.unlock(); if (intersectedSomething) { distance = bestDistance; @@ -442,172 +433,104 @@ bool Model::convexHullContains(glm::vec3 point) { // we can use the AABox's contains() by mapping our point into the model frame // and testing there. if (modelFrameBox.contains(modelFramePoint)){ - _mutex.lock(); - if (!_calculatedMeshTrianglesValid) { - recalculateMeshBoxes(true); + QMutexLocker locker(&_mutex); + + if (!_triangleSetsValid) { + calculateTriangleSets(); } // If we are inside the models box, then consider the submeshes... - int subMeshIndex = 0; - foreach(const AABox& subMeshBox, _calculatedMeshBoxes) { - if (subMeshBox.contains(point)) { - bool insideMesh = true; - // To be inside the sub mesh, we need to be behind every triangles' planes - const QVector& meshTriangles = _calculatedMeshTriangles[subMeshIndex]; - foreach (const Triangle& triangle, meshTriangles) { - if (!isPointBehindTrianglesPlane(point, triangle.v0, triangle.v1, triangle.v2)) { - // it's not behind at least one so we bail - insideMesh = false; - break; - } + glm::mat4 meshToModelMatrix = glm::scale(_scale) * glm::translate(_offset); + glm::mat4 meshToWorldMatrix = createMatFromQuatAndPos(_rotation, _translation) * meshToModelMatrix; + glm::mat4 worldToMeshMatrix = glm::inverse(meshToWorldMatrix); + glm::vec3 meshFramePoint = glm::vec3(worldToMeshMatrix * glm::vec4(point, 1.0f)); - } - if (insideMesh) { + for (const auto& triangleSet : _modelSpaceMeshTriangleSets) { + const AABox& box = triangleSet.getBounds(); + if (box.contains(meshFramePoint)) { + if (triangleSet.convexHullContains(meshFramePoint)) { // It's inside this mesh, return true. - _mutex.unlock(); return true; } } - subMeshIndex++; } - _mutex.unlock(); + + } // It wasn't in any mesh, return false. return false; } -// TODO: we seem to call this too often when things haven't actually changed... look into optimizing this -// Any script might trigger findRayIntersectionAgainstSubMeshes (and maybe convexHullContains), so these -// can occur multiple times. In addition, rendering does it's own ray picking in order to decide which -// entity-scripts to call. I think it would be best to do the picking once-per-frame (in cpu, or gpu if possible) -// and then the calls use the most recent such result. -void Model::recalculateMeshBoxes(bool pickAgainstTriangles) { +void Model::calculateTriangleSets() { PROFILE_RANGE(render, __FUNCTION__); - bool calculatedMeshTrianglesNeeded = pickAgainstTriangles && !_calculatedMeshTrianglesValid; - if (!_calculatedMeshBoxesValid || calculatedMeshTrianglesNeeded || (!_calculatedMeshPartBoxesValid && pickAgainstTriangles) ) { - const FBXGeometry& geometry = getFBXGeometry(); - int numberOfMeshes = geometry.meshes.size(); - _calculatedMeshBoxes.resize(numberOfMeshes); - _calculatedMeshTriangles.clear(); - _calculatedMeshTriangles.resize(numberOfMeshes); - _calculatedMeshPartBoxes.clear(); - for (int i = 0; i < numberOfMeshes; i++) { - const FBXMesh& mesh = geometry.meshes.at(i); - Extents scaledMeshExtents = calculateScaledOffsetExtents(mesh.meshExtents, _translation, _rotation); + const FBXGeometry& geometry = getFBXGeometry(); + int numberOfMeshes = geometry.meshes.size(); - _calculatedMeshBoxes[i] = AABox(scaledMeshExtents); + _triangleSetsValid = true; + _modelSpaceMeshTriangleSets.clear(); + _modelSpaceMeshTriangleSets.resize(numberOfMeshes); - if (pickAgainstTriangles) { - QVector thisMeshTriangles; - for (int j = 0; j < mesh.parts.size(); j++) { - const FBXMeshPart& part = mesh.parts.at(j); + for (int i = 0; i < numberOfMeshes; i++) { + const FBXMesh& mesh = geometry.meshes.at(i); - bool atLeastOnePointInBounds = false; - AABox thisPartBounds; + for (int j = 0; j < mesh.parts.size(); j++) { + const FBXMeshPart& part = mesh.parts.at(j); - const int INDICES_PER_TRIANGLE = 3; - const int INDICES_PER_QUAD = 4; + const int INDICES_PER_TRIANGLE = 3; + const int INDICES_PER_QUAD = 4; + const int TRIANGLES_PER_QUAD = 2; - if (part.quadIndices.size() > 0) { - int numberOfQuads = part.quadIndices.size() / INDICES_PER_QUAD; - int vIndex = 0; - for (int q = 0; q < numberOfQuads; q++) { - int i0 = part.quadIndices[vIndex++]; - int i1 = part.quadIndices[vIndex++]; - int i2 = part.quadIndices[vIndex++]; - int i3 = part.quadIndices[vIndex++]; + // tell our triangleSet how many triangles to expect. + int numberOfQuads = part.quadIndices.size() / INDICES_PER_QUAD; + int numberOfTris = part.triangleIndices.size() / INDICES_PER_TRIANGLE; + int totalTriangles = (numberOfQuads * TRIANGLES_PER_QUAD) + numberOfTris; + _modelSpaceMeshTriangleSets[i].reserve(totalTriangles); - glm::vec3 mv0 = glm::vec3(mesh.modelTransform * glm::vec4(mesh.vertices[i0], 1.0f)); - glm::vec3 mv1 = glm::vec3(mesh.modelTransform * glm::vec4(mesh.vertices[i1], 1.0f)); - glm::vec3 mv2 = glm::vec3(mesh.modelTransform * glm::vec4(mesh.vertices[i2], 1.0f)); - glm::vec3 mv3 = glm::vec3(mesh.modelTransform * glm::vec4(mesh.vertices[i3], 1.0f)); + auto meshTransform = getFBXGeometry().offset * mesh.modelTransform; - // track the mesh parts in model space - if (!atLeastOnePointInBounds) { - thisPartBounds.setBox(mv0, 0.0f); - atLeastOnePointInBounds = true; - } else { - thisPartBounds += mv0; - } - thisPartBounds += mv1; - thisPartBounds += mv2; - thisPartBounds += mv3; + if (part.quadIndices.size() > 0) { + int vIndex = 0; + for (int q = 0; q < numberOfQuads; q++) { + int i0 = part.quadIndices[vIndex++]; + int i1 = part.quadIndices[vIndex++]; + int i2 = part.quadIndices[vIndex++]; + int i3 = part.quadIndices[vIndex++]; - glm::vec3 v0 = calculateScaledOffsetPoint(mv0); - glm::vec3 v1 = calculateScaledOffsetPoint(mv1); - glm::vec3 v2 = calculateScaledOffsetPoint(mv2); - glm::vec3 v3 = calculateScaledOffsetPoint(mv3); + // track the model space version... these points will be transformed by the FST's offset, + // which includes the scaling, rotation, and translation specified by the FST/FBX, + // this can't change at runtime, so we can safely store these in our TriangleSet + glm::vec3 v0 = glm::vec3(meshTransform * glm::vec4(mesh.vertices[i0], 1.0f)); + glm::vec3 v1 = glm::vec3(meshTransform * glm::vec4(mesh.vertices[i1], 1.0f)); + glm::vec3 v2 = glm::vec3(meshTransform * glm::vec4(mesh.vertices[i2], 1.0f)); + glm::vec3 v3 = glm::vec3(meshTransform * glm::vec4(mesh.vertices[i3], 1.0f)); - // Sam's recommended triangle slices - Triangle tri1 = { v0, v1, v3 }; - Triangle tri2 = { v1, v2, v3 }; - - // NOTE: Random guy on the internet's recommended triangle slices - //Triangle tri1 = { v0, v1, v2 }; - //Triangle tri2 = { v2, v3, v0 }; - - thisMeshTriangles.push_back(tri1); - thisMeshTriangles.push_back(tri2); - - } - } - - if (part.triangleIndices.size() > 0) { - int numberOfTris = part.triangleIndices.size() / INDICES_PER_TRIANGLE; - int vIndex = 0; - for (int t = 0; t < numberOfTris; t++) { - int i0 = part.triangleIndices[vIndex++]; - int i1 = part.triangleIndices[vIndex++]; - int i2 = part.triangleIndices[vIndex++]; - - glm::vec3 mv0 = glm::vec3(mesh.modelTransform * glm::vec4(mesh.vertices[i0], 1.0f)); - glm::vec3 mv1 = glm::vec3(mesh.modelTransform * glm::vec4(mesh.vertices[i1], 1.0f)); - glm::vec3 mv2 = glm::vec3(mesh.modelTransform * glm::vec4(mesh.vertices[i2], 1.0f)); - - // track the mesh parts in model space - if (!atLeastOnePointInBounds) { - thisPartBounds.setBox(mv0, 0.0f); - atLeastOnePointInBounds = true; - } else { - thisPartBounds += mv0; - } - thisPartBounds += mv1; - thisPartBounds += mv2; - - glm::vec3 v0 = calculateScaledOffsetPoint(mv0); - glm::vec3 v1 = calculateScaledOffsetPoint(mv1); - glm::vec3 v2 = calculateScaledOffsetPoint(mv2); - - Triangle tri = { v0, v1, v2 }; - - thisMeshTriangles.push_back(tri); - } - } - _calculatedMeshPartBoxes[QPair(i, j)] = thisPartBounds; + Triangle tri1 = { v0, v1, v3 }; + Triangle tri2 = { v1, v2, v3 }; + _modelSpaceMeshTriangleSets[i].insert(tri1); + _modelSpaceMeshTriangleSets[i].insert(tri2); + } + } + + if (part.triangleIndices.size() > 0) { + int vIndex = 0; + for (int t = 0; t < numberOfTris; t++) { + int i0 = part.triangleIndices[vIndex++]; + int i1 = part.triangleIndices[vIndex++]; + int i2 = part.triangleIndices[vIndex++]; + + // track the model space version... these points will be transformed by the FST's offset, + // which includes the scaling, rotation, and translation specified by the FST/FBX, + // this can't change at runtime, so we can safely store these in our TriangleSet + glm::vec3 v0 = glm::vec3(meshTransform * glm::vec4(mesh.vertices[i0], 1.0f)); + glm::vec3 v1 = glm::vec3(meshTransform * glm::vec4(mesh.vertices[i1], 1.0f)); + glm::vec3 v2 = glm::vec3(meshTransform * glm::vec4(mesh.vertices[i2], 1.0f)); + + Triangle tri = { v0, v1, v2 }; + _modelSpaceMeshTriangleSets[i].insert(tri); } - _calculatedMeshTriangles[i] = thisMeshTriangles; - _calculatedMeshPartBoxesValid = true; } } - _calculatedMeshBoxesValid = true; - _calculatedMeshTrianglesValid = pickAgainstTriangles; - } -} - -void Model::renderSetup(RenderArgs* args) { - // set up dilated textures on first render after load/simulate - const FBXGeometry& geometry = getFBXGeometry(); - if (_dilatedTextures.isEmpty()) { - foreach (const FBXMesh& mesh, geometry.meshes) { - QVector > dilated; - dilated.resize(mesh.parts.size()); - _dilatedTextures.append(dilated); - } - } - - if (!_addedToScene && isLoaded()) { - createRenderItemSet(); } } @@ -615,14 +538,14 @@ void Model::setVisibleInScene(bool newValue, std::shared_ptr scen if (_isVisible != newValue) { _isVisible = newValue; - render::PendingChanges pendingChanges; - foreach (auto item, _modelMeshRenderItems.keys()) { - pendingChanges.resetItem(item, _modelMeshRenderItems[item]); + render::Transaction transaction; + foreach (auto item, _modelMeshRenderItemsMap.keys()) { + transaction.resetItem(item, _modelMeshRenderItemsMap[item]); } - foreach (auto item, _collisionRenderItems.keys()) { - pendingChanges.resetItem(item, _collisionRenderItems[item]); + foreach(auto item, _collisionRenderItemsMap.keys()) { + transaction.resetItem(item, _collisionRenderItemsMap[item]); } - scene->enqueuePendingChanges(pendingChanges); + scene->enqueueTransaction(transaction); } } @@ -631,19 +554,19 @@ void Model::setLayeredInFront(bool layered, std::shared_ptr scene if (_isLayeredInFront != layered) { _isLayeredInFront = layered; - render::PendingChanges pendingChanges; - foreach(auto item, _modelMeshRenderItems.keys()) { - pendingChanges.resetItem(item, _modelMeshRenderItems[item]); + render::Transaction transaction; + foreach(auto item, _modelMeshRenderItemsMap.keys()) { + transaction.resetItem(item, _modelMeshRenderItemsMap[item]); } - foreach(auto item, _collisionRenderItems.keys()) { - pendingChanges.resetItem(item, _collisionRenderItems[item]); + foreach(auto item, _collisionRenderItemsMap.keys()) { + transaction.resetItem(item, _collisionRenderItemsMap[item]); } - scene->enqueuePendingChanges(pendingChanges); + scene->enqueueTransaction(transaction); } } bool Model::addToScene(std::shared_ptr scene, - render::PendingChanges& pendingChanges, + render::Transaction& transaction, render::Item::Status::Getters& statusGetters) { bool readyToRender = _collisionGeometry || isLoaded(); if (!_addedToScene && readyToRender) { @@ -653,39 +576,39 @@ bool Model::addToScene(std::shared_ptr scene, bool somethingAdded = false; if (_collisionGeometry) { if (_collisionRenderItems.empty()) { - foreach (auto renderItem, _collisionRenderItemsSet) { + foreach (auto renderItem, _collisionRenderItems) { auto item = scene->allocateID(); auto renderPayload = std::make_shared(renderItem); - if (statusGetters.size()) { + if (_collisionRenderItems.empty() && statusGetters.size()) { renderPayload->addStatusGetters(statusGetters); } - pendingChanges.resetItem(item, renderPayload); - _collisionRenderItems.insert(item, renderPayload); + transaction.resetItem(item, renderPayload); + _collisionRenderItemsMap.insert(item, renderPayload); } somethingAdded = !_collisionRenderItems.empty(); } } else { - if (_modelMeshRenderItems.empty()) { + if (_modelMeshRenderItemsMap.empty()) { bool hasTransparent = false; size_t verticesCount = 0; - foreach(auto renderItem, _modelMeshRenderItemsSet) { + foreach(auto renderItem, _modelMeshRenderItems) { auto item = scene->allocateID(); auto renderPayload = std::make_shared(renderItem); - if (statusGetters.size()) { + if (_modelMeshRenderItemsMap.empty() && statusGetters.size()) { renderPayload->addStatusGetters(statusGetters); } - pendingChanges.resetItem(item, renderPayload); + transaction.resetItem(item, renderPayload); hasTransparent = hasTransparent || renderItem.get()->getShapeKey().isTranslucent(); verticesCount += renderItem.get()->getVerticesCount(); - _modelMeshRenderItems.insert(item, renderPayload); + _modelMeshRenderItemsMap.insert(item, renderPayload); _modelMeshRenderItemIDs.emplace_back(item); } - somethingAdded = !_modelMeshRenderItems.empty(); + somethingAdded = !_modelMeshRenderItemsMap.empty(); _renderInfoVertexCount = verticesCount; - _renderInfoDrawCalls = _modelMeshRenderItems.count(); + _renderInfoDrawCalls = _modelMeshRenderItemsMap.count(); _renderInfoHasTransparent = hasTransparent; } } @@ -699,19 +622,19 @@ bool Model::addToScene(std::shared_ptr scene, return somethingAdded; } -void Model::removeFromScene(std::shared_ptr scene, render::PendingChanges& pendingChanges) { - foreach (auto item, _modelMeshRenderItems.keys()) { - pendingChanges.removeItem(item); +void Model::removeFromScene(std::shared_ptr scene, render::Transaction& transaction) { + foreach (auto item, _modelMeshRenderItemsMap.keys()) { + transaction.removeItem(item); } _modelMeshRenderItemIDs.clear(); + _modelMeshRenderItemsMap.clear(); _modelMeshRenderItems.clear(); - _modelMeshRenderItemsSet.clear(); - foreach (auto item, _collisionRenderItems.keys()) { - pendingChanges.removeItem(item); + foreach(auto item, _collisionRenderItemsMap.keys()) { + transaction.removeItem(item); } _collisionRenderItems.clear(); - _collisionRenderItemsSet.clear(); + _collisionRenderItems.clear(); _addedToScene = false; _renderInfoVertexCount = 0; @@ -723,7 +646,17 @@ void Model::removeFromScene(std::shared_ptr scene, render::Pendin void Model::renderDebugMeshBoxes(gpu::Batch& batch) { int colorNdx = 0; _mutex.lock(); - foreach(AABox box, _calculatedMeshBoxes) { + + glm::mat4 meshToModelMatrix = glm::scale(_scale) * glm::translate(_offset); + glm::mat4 meshToWorldMatrix = createMatFromQuatAndPos(_rotation, _translation) * meshToModelMatrix; + Transform meshToWorld(meshToWorldMatrix); + batch.setModelTransform(meshToWorld); + + DependencyManager::get()->bindSimpleProgram(batch, false, false, false, true, true); + + for(const auto& triangleSet : _modelSpaceMeshTriangleSets) { + auto box = triangleSet.getBounds(); + if (_debugMeshBoxesID == GeometryCache::UNKNOWN_ID) { _debugMeshBoxesID = DependencyManager::get()->allocateID(); } @@ -755,8 +688,8 @@ void Model::renderDebugMeshBoxes(gpu::Batch& batch) { points << blf << tlf; glm::vec4 color[] = { - { 1.0f, 0.0f, 0.0f, 1.0f }, // red { 0.0f, 1.0f, 0.0f, 1.0f }, // green + { 1.0f, 0.0f, 0.0f, 1.0f }, // red { 0.0f, 0.0f, 1.0f, 1.0f }, // blue { 1.0f, 0.0f, 1.0f, 1.0f }, // purple { 1.0f, 1.0f, 0.0f, 1.0f }, // yellow @@ -814,37 +747,6 @@ Extents Model::getUnscaledMeshExtents() const { return scaledExtents; } -Extents Model::calculateScaledOffsetExtents(const Extents& extents, - glm::vec3 modelPosition, glm::quat modelOrientation) const { - // we need to include any fst scaling, translation, and rotation, which is captured in the offset matrix - glm::vec3 minimum = glm::vec3(getFBXGeometry().offset * glm::vec4(extents.minimum, 1.0f)); - glm::vec3 maximum = glm::vec3(getFBXGeometry().offset * glm::vec4(extents.maximum, 1.0f)); - - Extents scaledOffsetExtents = { ((minimum + _offset) * _scale), - ((maximum + _offset) * _scale) }; - - Extents rotatedExtents = scaledOffsetExtents.getRotated(modelOrientation); - - Extents translatedExtents = { rotatedExtents.minimum + modelPosition, - rotatedExtents.maximum + modelPosition }; - - return translatedExtents; -} - -/// Returns the world space equivalent of some box in model space. -AABox Model::calculateScaledOffsetAABox(const AABox& box, glm::vec3 modelPosition, glm::quat modelOrientation) const { - return AABox(calculateScaledOffsetExtents(Extents(box), modelPosition, modelOrientation)); -} - -glm::vec3 Model::calculateScaledOffsetPoint(const glm::vec3& point) const { - // we need to include any fst scaling, translation, and rotation, which is captured in the offset matrix - glm::vec3 offsetPoint = glm::vec3(getFBXGeometry().offset * glm::vec4(point, 1.0f)); - glm::vec3 scaledPoint = ((offsetPoint + _offset) * _scale); - glm::vec3 rotatedPoint = _rotation * scaledPoint; - glm::vec3 translatedPoint = rotatedPoint + _translation; - return translatedPoint; -} - void Model::clearJointState(int index) { _rig->clearJointState(index); } @@ -892,11 +794,11 @@ void Model::setURL(const QUrl& url) { _url = url; { - render::PendingChanges pendingChanges; + render::Transaction transaction; render::ScenePointer scene = AbstractViewStateInterface::instance()->getMain3DScene(); if (scene) { - removeFromScene(scene, pendingChanges); - scene->enqueuePendingChanges(pendingChanges); + removeFromScene(scene, transaction); + scene->enqueueTransaction(transaction); } else { qCWarning(renderutils) << "Model::setURL(), Unexpected null scene, possibly during application shutdown"; } @@ -1126,12 +1028,6 @@ void Model::simulate(float deltaTime, bool fullUpdate) { || (_snapModelToRegistrationPoint && !_snappedToRegistrationPoint); if (isActive() && fullUpdate) { - // NOTE: This is overly aggressive and we are invalidating the MeshBoxes when in fact they may not be invalid - // they really only become invalid if something about the transform to world space has changed. This is - // not too bad at this point, because it doesn't impact rendering. However it does slow down ray picking - // because ray picking needs valid boxes to work - _calculatedMeshBoxesValid = false; - _calculatedMeshTrianglesValid = false; onInvalidate(); // check for scale to fit @@ -1156,8 +1052,8 @@ void Model::updateRig(float deltaTime, glm::mat4 parentTransform) { } void Model::computeMeshPartLocalBounds() { - for (auto& part : _modelMeshRenderItemsSet) { - assert(part->_meshIndex < _modelMeshRenderItemsSet.size()); + for (auto& part : _modelMeshRenderItems) { + assert(part->_meshIndex < _modelMeshRenderItems.size()); const Model::MeshState& state = _meshStates.at(part->_meshIndex); part->computeAdjustedLocalBound(state.clusterMatrices); } @@ -1178,13 +1074,7 @@ void Model::updateClusterMatrices() { for (int j = 0; j < mesh.clusters.size(); j++) { const FBXCluster& cluster = mesh.clusters.at(j); auto jointMatrix = _rig->getJointTransform(cluster.jointIndex); -#if (GLM_ARCH & GLM_ARCH_SSE2) && !(defined Q_OS_MAC) - glm::mat4 out, inverseBindMatrix = cluster.inverseBindMatrix; - glm_mat4_mul((glm_vec4*)&jointMatrix, (glm_vec4*)&inverseBindMatrix, (glm_vec4*)&out); - state.clusterMatrices[j] = out; -#else - state.clusterMatrices[j] = jointMatrix * cluster.inverseBindMatrix; -#endif + glm_mat4u_mul(jointMatrix, cluster.inverseBindMatrix, state.clusterMatrices[j]); } // Once computed the cluster matrices, update the buffer(s) @@ -1277,7 +1167,7 @@ AABox Model::getRenderableMeshBound() const { } else { // Build a bound using the last known bound from all the renderItems. AABox totalBound; - for (auto& renderItem : _modelMeshRenderItemsSet) { + for (auto& renderItem : _modelMeshRenderItems) { totalBound += renderItem->getBound(); } return totalBound; @@ -1290,11 +1180,11 @@ const render::ItemIDs& Model::fetchRenderItemIDs() const { void Model::createRenderItemSet() { if (_collisionGeometry) { - if (_collisionRenderItemsSet.empty()) { + if (_collisionRenderItems.empty()) { createCollisionRenderItemSet(); } } else { - if (_modelMeshRenderItemsSet.empty()) { + if (_modelMeshRenderItems.empty()) { createVisibleRenderItemSet(); } } @@ -1311,9 +1201,9 @@ void Model::createVisibleRenderItemSet() { } // We should not have any existing renderItems if we enter this section of code - Q_ASSERT(_modelMeshRenderItemsSet.isEmpty()); + Q_ASSERT(_modelMeshRenderItems.isEmpty()); - _modelMeshRenderItemsSet.clear(); + _modelMeshRenderItems.clear(); Transform transform; transform.setTranslation(_translation); @@ -1335,7 +1225,7 @@ void Model::createVisibleRenderItemSet() { // Create the render payloads int numParts = (int)mesh->getNumParts(); for (int partIndex = 0; partIndex < numParts; partIndex++) { - _modelMeshRenderItemsSet << std::make_shared(this, i, partIndex, shapeID, transform, offset); + _modelMeshRenderItems << std::make_shared(this, i, partIndex, shapeID, transform, offset); shapeID++; } } @@ -1351,7 +1241,7 @@ void Model::createCollisionRenderItemSet() { const auto& meshes = _collisionGeometry->getMeshes(); // We should not have any existing renderItems if we enter this section of code - Q_ASSERT(_collisionRenderItemsSet.isEmpty()); + Q_ASSERT(_collisionRenderItems.isEmpty()); Transform identity; identity.setIdentity(); @@ -1372,7 +1262,7 @@ void Model::createCollisionRenderItemSet() { model::MaterialPointer& material = _collisionMaterials[partIndex % NUM_COLLISION_HULL_COLORS]; auto payload = std::make_shared(mesh, partIndex, material); payload->updateTransform(identity, offset); - _collisionRenderItemsSet << payload; + _collisionRenderItems << payload; } } } @@ -1389,38 +1279,38 @@ bool Model::initWhenReady(render::ScenePointer scene) { createRenderItemSet(); - render::PendingChanges pendingChanges; + render::Transaction transaction; - bool addedPendingChanges = false; + bool addedTransaction = false; if (_collisionGeometry) { - foreach (auto renderItem, _collisionRenderItemsSet) { + foreach (auto renderItem, _collisionRenderItems) { auto item = scene->allocateID(); auto renderPayload = std::make_shared(renderItem); - _collisionRenderItems.insert(item, renderPayload); - pendingChanges.resetItem(item, renderPayload); + _collisionRenderItemsMap.insert(item, renderPayload); + transaction.resetItem(item, renderPayload); } - addedPendingChanges = !_collisionRenderItems.empty(); + addedTransaction = !_collisionRenderItems.empty(); } else { bool hasTransparent = false; size_t verticesCount = 0; - foreach (auto renderItem, _modelMeshRenderItemsSet) { + foreach (auto renderItem, _modelMeshRenderItems) { auto item = scene->allocateID(); auto renderPayload = std::make_shared(renderItem); hasTransparent = hasTransparent || renderItem.get()->getShapeKey().isTranslucent(); verticesCount += renderItem.get()->getVerticesCount(); - _modelMeshRenderItems.insert(item, renderPayload); - pendingChanges.resetItem(item, renderPayload); + _modelMeshRenderItemsMap.insert(item, renderPayload); + transaction.resetItem(item, renderPayload); } - addedPendingChanges = !_modelMeshRenderItems.empty(); + addedTransaction = !_modelMeshRenderItemsMap.empty(); _renderInfoVertexCount = verticesCount; - _renderInfoDrawCalls = _modelMeshRenderItems.count(); + _renderInfoDrawCalls = _modelMeshRenderItemsMap.count(); _renderInfoHasTransparent = hasTransparent; } - _addedToScene = addedPendingChanges; - if (addedPendingChanges) { - scene->enqueuePendingChanges(pendingChanges); - // NOTE: updateRender items enqueues identical pendingChanges (using a lambda) + _addedToScene = addedTransaction; + if (addedTransaction) { + scene->enqueueTransaction(transaction); + // NOTE: updateRender items enqueues identical transaction (using a lambda) // so it looks like we're doing double work here, but I don't want to remove the call // for fear there is some side effect we'll miss. -- Andrew 2016.07.21 // TODO: figure out if we really need this call to updateRenderItems() or not. diff --git a/libraries/render-utils/src/Model.h b/libraries/render-utils/src/Model.h index 7c373274e4..e852358bc4 100644 --- a/libraries/render-utils/src/Model.h +++ b/libraries/render-utils/src/Model.h @@ -28,6 +28,7 @@ #include #include #include +#include #include "GeometryCache.h" #include "TextureCache.h" @@ -41,7 +42,7 @@ class ViewFrustum; namespace render { class Scene; - class PendingChanges; + class Transaction; typedef unsigned int ItemID; } class MeshPartPayload; @@ -87,15 +88,14 @@ public: bool needsReload() const { return _needsReload; } bool initWhenReady(render::ScenePointer scene); bool addToScene(std::shared_ptr scene, - render::PendingChanges& pendingChanges) { + render::Transaction& transaction) { auto getters = render::Item::Status::Getters(0); - return addToScene(scene, pendingChanges, getters); + return addToScene(scene, transaction, getters); } bool addToScene(std::shared_ptr scene, - render::PendingChanges& pendingChanges, + render::Transaction& transaction, render::Item::Status::Getters& statusGetters); - void removeFromScene(std::shared_ptr scene, render::PendingChanges& pendingChanges); - void renderSetup(RenderArgs* args); + void removeFromScene(std::shared_ptr scene, render::Transaction& transaction); bool isRenderable() const; bool isVisible() const { return _isVisible; } @@ -248,7 +248,10 @@ public: const MeshState& getMeshState(int index) { return _meshStates.at(index); } uint32_t getGeometryCounter() const { return _deleteGeometryCounter; } - const QMap& getRenderItems() const { return _modelMeshRenderItems; } + const QMap& getRenderItems() const { return _modelMeshRenderItemsMap; } + + void renderDebugMeshBoxes(gpu::Batch& batch); + public slots: void loadURLFinished(bool success); @@ -266,15 +269,6 @@ protected: /// Returns the unscaled extents of the model's mesh Extents getUnscaledMeshExtents() const; - /// Returns the scaled equivalent of some extents in model space. - Extents calculateScaledOffsetExtents(const Extents& extents, glm::vec3 modelPosition, glm::quat modelOrientation) const; - - /// Returns the world space equivalent of some box in model space. - AABox calculateScaledOffsetAABox(const AABox& box, glm::vec3 modelPosition, glm::quat modelOrientation) const; - - /// Returns the scaled equivalent of a point in model space. - glm::vec3 calculateScaledOffsetPoint(const glm::vec3& point) const; - /// Clear the joint states void clearJointState(int index); @@ -293,9 +287,13 @@ protected: SpatiallyNestable* _spatiallyNestableOverride; - glm::vec3 _translation; + glm::vec3 _translation; // this is the translation in world coordinates to the model's registration point glm::quat _rotation; glm::vec3 _scale; + + // For entity models this is the translation for the minimum extent of the model (in original mesh coordinate space) + // to the model's registration point. For avatar models this is the translation from the avatar's hips, as determined + // by the default pose, to the origin. glm::vec3 _offset; static float FAKE_DIMENSION_PLACEHOLDER; @@ -331,14 +329,13 @@ protected: /// Allow sub classes to force invalidating the bboxes void invalidCalculatedMeshBoxes() { - _calculatedMeshBoxesValid = false; - _calculatedMeshPartBoxesValid = false; - _calculatedMeshTrianglesValid = false; + _triangleSetsValid = false; } // hook for derived classes to be notified when setUrl invalidates the current model. virtual void onInvalidate() {}; + protected: virtual void deleteGeometry(); @@ -357,17 +354,12 @@ protected: int _blendNumber; int _appliedBlendNumber; - QHash, AABox> _calculatedMeshPartBoxes; // world coordinate AABoxes for all sub mesh part boxes - - bool _calculatedMeshPartBoxesValid; - QVector _calculatedMeshBoxes; // world coordinate AABoxes for all sub mesh boxes - bool _calculatedMeshBoxesValid; - - QVector< QVector > _calculatedMeshTriangles; // world coordinate triangles for all sub meshes - bool _calculatedMeshTrianglesValid; QMutex _mutex; - void recalculateMeshBoxes(bool pickAgainstTriangles = false); + bool _triangleSetsValid { false }; + void calculateTriangleSets(); + QVector _modelSpaceMeshTriangleSets; // model space triangles for all sub meshes + void createRenderItemSet(); virtual void createVisibleRenderItemSet(); @@ -376,17 +368,16 @@ protected: bool _isWireframe; // debug rendering support - void renderDebugMeshBoxes(gpu::Batch& batch); int _debugMeshBoxesID = GeometryCache::UNKNOWN_ID; static AbstractViewStateInterface* _viewState; - QSet> _collisionRenderItemsSet; - QMap _collisionRenderItems; + QVector> _collisionRenderItems; + QMap _collisionRenderItemsMap; - QSet> _modelMeshRenderItemsSet; - QMap _modelMeshRenderItems; + QVector> _modelMeshRenderItems; + QMap _modelMeshRenderItemsMap; render::ItemIDs _modelMeshRenderItemIDs; diff --git a/libraries/render-utils/src/RenderDeferredTask.cpp b/libraries/render-utils/src/RenderDeferredTask.cpp index 55a9c8b9e4..22aa95090c 100644 --- a/libraries/render-utils/src/RenderDeferredTask.cpp +++ b/libraries/render-utils/src/RenderDeferredTask.cpp @@ -75,7 +75,6 @@ RenderDeferredTask::RenderDeferredTask(RenderFetchCullSortTask::Output items) { // GPU jobs: Start preparing the primary, deferred and lighting buffer const auto primaryFramebuffer = addJob("PreparePrimaryBuffer"); - // const auto fullFrameRangeTimer = addJob("BeginRangeTimer"); const auto opaqueRangeTimer = addJob("BeginOpaqueRangeTimer", "DrawOpaques"); const auto prepareDeferredInputs = PrepareDeferred::Inputs(primaryFramebuffer, lightingModel).hasVarying(); @@ -154,20 +153,25 @@ RenderDeferredTask::RenderDeferredTask(RenderFetchCullSortTask::Output items) { const auto toneMappingInputs = render::Varying(ToneMappingDeferred::Inputs(lightingFramebuffer, primaryFramebuffer)); addJob("ToneMapping", toneMappingInputs); + { // DEbug the bounds of the rendered items, still look at the zbuffer + addJob("DrawMetaBounds", metas); + addJob("DrawOpaqueBounds", opaques); + addJob("DrawTransparentBounds", transparents); + } + // Overlays const auto overlayOpaquesInputs = DrawOverlay3D::Inputs(overlayOpaques, lightingModel).hasVarying(); const auto overlayTransparentsInputs = DrawOverlay3D::Inputs(overlayTransparents, lightingModel).hasVarying(); addJob("DrawOverlay3DOpaque", overlayOpaquesInputs, true); addJob("DrawOverlay3DTransparent", overlayTransparentsInputs, false); + { // DEbug the bounds of the rendered OVERLAY items, still look at the zbuffer + addJob("DrawOverlayOpaqueBounds", overlayOpaques); + addJob("DrawOverlayTransparentBounds", overlayTransparents); + } - // Debugging stages + // Debugging stages { - - - // Bounds do not draw on stencil buffer, so they must come last - addJob("DrawMetaBounds", metas); - // Debugging Deferred buffer job const auto debugFramebuffers = render::Varying(DebugDeferredBuffer::Inputs(deferredFramebuffer, linearDepthTarget, surfaceGeometryFramebuffer, ambientOcclusionFramebuffer)); addJob("DebugDeferredBuffer", debugFramebuffers); @@ -190,7 +194,7 @@ RenderDeferredTask::RenderDeferredTask(RenderFetchCullSortTask::Output items) { { // Grab a texture map representing the different status icons and assign that to the drawStatsuJob auto iconMapPath = PathUtils::resourcesPath() + "icons/statusIconAtlas.svg"; - auto statusIconMap = DependencyManager::get()->getImageTexture(iconMapPath); + auto statusIconMap = DependencyManager::get()->getImageTexture(iconMapPath, NetworkTexture::STRICT_TEXTURE); addJob("DrawStatus", opaques, DrawStatus(statusIconMap)); } } @@ -207,9 +211,6 @@ RenderDeferredTask::RenderDeferredTask(RenderFetchCullSortTask::Output items) { // Blit! addJob("Blit", primaryFramebuffer); - - // addJob("RangeTimer", fullFrameRangeTimer); - } void BeginGPURangeTimer::run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext, gpu::RangeTimerPointer& timer) { @@ -258,8 +259,18 @@ void DrawDeferred::run(const SceneContextPointer& sceneContext, const RenderCont // Setup lighting model for all items; batch.setUniformBuffer(render::ShapePipeline::Slot::LIGHTING_MODEL, lightingModel->getParametersBuffer()); - renderShapes(sceneContext, renderContext, _shapePlumber, inItems, _maxDrawn); + // From the lighting model define a global shapKey ORED with individiual keys + ShapeKey::Builder keyBuilder; + if (lightingModel->isWireframeEnabled()) { + keyBuilder.withWireframe(); + } + ShapeKey globalKey = keyBuilder.build(); + args->_globalShapeKey = globalKey._flags.to_ulong(); + + renderShapes(sceneContext, renderContext, _shapePlumber, inItems, _maxDrawn, globalKey); + args->_batch = nullptr; + args->_globalShapeKey = 0; }); config->setNumDrawn((int)inItems.size()); @@ -294,12 +305,21 @@ void DrawStateSortDeferred::run(const SceneContextPointer& sceneContext, const R // Setup lighting model for all items; batch.setUniformBuffer(render::ShapePipeline::Slot::LIGHTING_MODEL, lightingModel->getParametersBuffer()); + // From the lighting model define a global shapKey ORED with individiual keys + ShapeKey::Builder keyBuilder; + if (lightingModel->isWireframeEnabled()) { + keyBuilder.withWireframe(); + } + ShapeKey globalKey = keyBuilder.build(); + args->_globalShapeKey = globalKey._flags.to_ulong(); + if (_stateSort) { - renderStateSortShapes(sceneContext, renderContext, _shapePlumber, inItems, _maxDrawn); + renderStateSortShapes(sceneContext, renderContext, _shapePlumber, inItems, _maxDrawn, globalKey); } else { - renderShapes(sceneContext, renderContext, _shapePlumber, inItems, _maxDrawn); + renderShapes(sceneContext, renderContext, _shapePlumber, inItems, _maxDrawn, globalKey); } args->_batch = nullptr; + args->_globalShapeKey = 0; }); config->setNumDrawn((int)inItems.size()); diff --git a/libraries/render-utils/src/RenderForwardTask.cpp b/libraries/render-utils/src/RenderForwardTask.cpp index 45a32c1aaf..49090c2f5f 100755 --- a/libraries/render-utils/src/RenderForwardTask.cpp +++ b/libraries/render-utils/src/RenderForwardTask.cpp @@ -73,11 +73,11 @@ void PrepareFramebuffer::run(const SceneContextPointer& sceneContext, const Rend auto colorFormat = gpu::Element::COLOR_SRGBA_32; auto defaultSampler = gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_POINT); - auto colorTexture = gpu::TexturePointer(gpu::Texture::create2D(colorFormat, frameSize.x, frameSize.y, defaultSampler)); + auto colorTexture = gpu::TexturePointer(gpu::Texture::create2D(colorFormat, frameSize.x, frameSize.y, gpu::Texture::SINGLE_MIP, defaultSampler)); _framebuffer->setRenderBuffer(0, colorTexture); auto depthFormat = gpu::Element(gpu::SCALAR, gpu::UINT32, gpu::DEPTH_STENCIL); // Depth24_Stencil8 texel format - auto depthTexture = gpu::TexturePointer(gpu::Texture::create2D(depthFormat, frameSize.x, frameSize.y, defaultSampler)); + auto depthTexture = gpu::TexturePointer(gpu::Texture::create2D(depthFormat, frameSize.x, frameSize.y, gpu::Texture::SINGLE_MIP, defaultSampler)); _framebuffer->setDepthStencilBuffer(depthTexture, depthFormat); } diff --git a/libraries/render-utils/src/RenderPipelines.cpp b/libraries/render-utils/src/RenderPipelines.cpp index c5a6c4b6ca..da264cbf7d 100644 --- a/libraries/render-utils/src/RenderPipelines.cpp +++ b/libraries/render-utils/src/RenderPipelines.cpp @@ -28,6 +28,12 @@ #include "skin_model_shadow_vert.h" #include "skin_model_normal_map_vert.h" +#include "simple_vert.h" +#include "simple_textured_frag.h" +#include "simple_textured_unlit_frag.h" +#include "simple_transparent_textured_frag.h" +#include "simple_transparent_textured_unlit_frag.h" + #include "model_frag.h" #include "model_unlit_frag.h" #include "model_shadow_frag.h" @@ -50,9 +56,13 @@ #include "overlay3D_vert.h" #include "overlay3D_frag.h" +#include "overlay3D_model_frag.h" +#include "overlay3D_model_translucent_frag.h" #include "overlay3D_translucent_frag.h" #include "overlay3D_unlit_frag.h" #include "overlay3D_translucent_unlit_frag.h" +#include "overlay3D_model_unlit_frag.h" +#include "overlay3D_model_translucent_unlit_frag.h" using namespace render; @@ -70,15 +80,24 @@ void lightBatchSetter(const ShapePipeline& pipeline, gpu::Batch& batch); void initOverlay3DPipelines(ShapePlumber& plumber) { auto vertex = gpu::Shader::createVertex(std::string(overlay3D_vert)); + auto vertexModel = gpu::Shader::createVertex(std::string(model_vert)); auto pixel = gpu::Shader::createPixel(std::string(overlay3D_frag)); auto pixelTranslucent = gpu::Shader::createPixel(std::string(overlay3D_translucent_frag)); auto pixelUnlit = gpu::Shader::createPixel(std::string(overlay3D_unlit_frag)); auto pixelTranslucentUnlit = gpu::Shader::createPixel(std::string(overlay3D_translucent_unlit_frag)); + auto pixelModel = gpu::Shader::createPixel(std::string(overlay3D_model_frag)); + auto pixelModelTranslucent = gpu::Shader::createPixel(std::string(overlay3D_model_translucent_frag)); + auto pixelModelUnlit = gpu::Shader::createPixel(std::string(overlay3D_model_unlit_frag)); + auto pixelModelTranslucentUnlit = gpu::Shader::createPixel(std::string(overlay3D_model_translucent_unlit_frag)); auto opaqueProgram = gpu::Shader::createProgram(vertex, pixel); auto translucentProgram = gpu::Shader::createProgram(vertex, pixelTranslucent); auto unlitOpaqueProgram = gpu::Shader::createProgram(vertex, pixelUnlit); auto unlitTranslucentProgram = gpu::Shader::createProgram(vertex, pixelTranslucentUnlit); + auto materialOpaqueProgram = gpu::Shader::createProgram(vertexModel, pixelModel); + auto materialTranslucentProgram = gpu::Shader::createProgram(vertexModel, pixelModelTranslucent); + auto materialUnlitOpaqueProgram = gpu::Shader::createProgram(vertexModel, pixelModel); + auto materialUnlitTranslucentProgram = gpu::Shader::createProgram(vertexModel, pixelModelTranslucent); for (int i = 0; i < 8; i++) { bool isCulled = (i & 1); @@ -103,19 +122,26 @@ void initOverlay3DPipelines(ShapePlumber& plumber) { } ShapeKey::Filter::Builder builder; + isCulled ? builder.withCullFace() : builder.withoutCullFace(); isBiased ? builder.withDepthBias() : builder.withoutDepthBias(); isOpaque ? builder.withOpaque() : builder.withTranslucent(); auto simpleProgram = isOpaque ? opaqueProgram : translucentProgram; auto unlitProgram = isOpaque ? unlitOpaqueProgram : unlitTranslucentProgram; - plumber.addPipeline(builder.withoutUnlit().build(), simpleProgram, state, &lightBatchSetter); - plumber.addPipeline(builder.withUnlit().build(), unlitProgram, state, &batchSetter); + auto materialProgram = isOpaque ? materialOpaqueProgram : materialTranslucentProgram; + auto materialUnlitProgram = isOpaque ? materialUnlitOpaqueProgram : materialUnlitTranslucentProgram; + + plumber.addPipeline(builder.withMaterial().build().key(), materialProgram, state, &lightBatchSetter); + plumber.addPipeline(builder.withMaterial().withUnlit().build().key(), materialUnlitProgram, state, &batchSetter); + plumber.addPipeline(builder.withoutUnlit().withoutMaterial().build().key(), simpleProgram, state, &lightBatchSetter); + plumber.addPipeline(builder.withUnlit().withoutMaterial().build().key(), unlitProgram, state, &batchSetter); } } void initDeferredPipelines(render::ShapePlumber& plumber) { // Vertex shaders + auto simpleVertex = gpu::Shader::createVertex(std::string(simple_vert)); auto modelVertex = gpu::Shader::createVertex(std::string(model_vert)); auto modelNormalMapVertex = gpu::Shader::createVertex(std::string(model_normal_map_vert)); auto modelLightmapVertex = gpu::Shader::createVertex(std::string(model_lightmap_vert)); @@ -126,6 +152,10 @@ void initDeferredPipelines(render::ShapePlumber& plumber) { auto skinModelShadowVertex = gpu::Shader::createVertex(std::string(skin_model_shadow_vert)); // Pixel shaders + auto simplePixel = gpu::Shader::createPixel(std::string(simple_textured_frag)); + auto simpleUnlitPixel = gpu::Shader::createPixel(std::string(simple_textured_unlit_frag)); + auto simpleTranslucentPixel = gpu::Shader::createPixel(std::string(simple_transparent_textured_frag)); + auto simpleTranslucentUnlitPixel = gpu::Shader::createPixel(std::string(simple_transparent_textured_unlit_frag)); auto modelPixel = gpu::Shader::createPixel(std::string(model_frag)); auto modelUnlitPixel = gpu::Shader::createPixel(std::string(model_unlit_frag)); auto modelNormalMapPixel = gpu::Shader::createPixel(std::string(model_normal_map_frag)); @@ -144,78 +174,90 @@ void initDeferredPipelines(render::ShapePlumber& plumber) { // TODO: Refactor this to use a filter // Opaques addPipeline( - Key::Builder(), + Key::Builder().withMaterial(), modelVertex, modelPixel); addPipeline( - Key::Builder().withUnlit(), + Key::Builder(), + simpleVertex, simplePixel); + addPipeline( + Key::Builder().withMaterial().withUnlit(), modelVertex, modelUnlitPixel); addPipeline( - Key::Builder().withTangents(), + Key::Builder().withUnlit(), + simpleVertex, simpleUnlitPixel); + addPipeline( + Key::Builder().withMaterial().withTangents(), modelNormalMapVertex, modelNormalMapPixel); addPipeline( - Key::Builder().withSpecular(), + Key::Builder().withMaterial().withSpecular(), modelVertex, modelSpecularMapPixel); addPipeline( - Key::Builder().withTangents().withSpecular(), + Key::Builder().withMaterial().withTangents().withSpecular(), modelNormalMapVertex, modelNormalSpecularMapPixel); // Translucents addPipeline( - Key::Builder().withTranslucent(), + Key::Builder().withMaterial().withTranslucent(), modelVertex, modelTranslucentPixel); addPipeline( - Key::Builder().withTranslucent().withUnlit(), + Key::Builder().withTranslucent(), + simpleVertex, simpleTranslucentPixel); + addPipeline( + Key::Builder().withMaterial().withTranslucent().withUnlit(), modelVertex, modelTranslucentUnlitPixel); addPipeline( - Key::Builder().withTranslucent().withTangents(), + Key::Builder().withTranslucent().withUnlit(), + simpleVertex, simpleTranslucentUnlitPixel); + addPipeline( + Key::Builder().withMaterial().withTranslucent().withTangents(), modelNormalMapVertex, modelTranslucentPixel); addPipeline( - Key::Builder().withTranslucent().withSpecular(), + Key::Builder().withMaterial().withTranslucent().withSpecular(), modelVertex, modelTranslucentPixel); addPipeline( - Key::Builder().withTranslucent().withTangents().withSpecular(), + Key::Builder().withMaterial().withTranslucent().withTangents().withSpecular(), modelNormalMapVertex, modelTranslucentPixel); addPipeline( // FIXME: Ignore lightmap for translucents meshpart - Key::Builder().withTranslucent().withLightmap(), + Key::Builder().withMaterial().withTranslucent().withLightmap(), modelVertex, modelTranslucentPixel); // Lightmapped addPipeline( - Key::Builder().withLightmap(), + Key::Builder().withMaterial().withLightmap(), modelLightmapVertex, modelLightmapPixel); addPipeline( - Key::Builder().withLightmap().withTangents(), + Key::Builder().withMaterial().withLightmap().withTangents(), modelLightmapNormalMapVertex, modelLightmapNormalMapPixel); addPipeline( - Key::Builder().withLightmap().withSpecular(), + Key::Builder().withMaterial().withLightmap().withSpecular(), modelLightmapVertex, modelLightmapSpecularMapPixel); addPipeline( - Key::Builder().withLightmap().withTangents().withSpecular(), + Key::Builder().withMaterial().withLightmap().withTangents().withSpecular(), modelLightmapNormalMapVertex, modelLightmapNormalSpecularMapPixel); // Skinned addPipeline( - Key::Builder().withSkinned(), + Key::Builder().withMaterial().withSkinned(), skinModelVertex, modelPixel); addPipeline( - Key::Builder().withSkinned().withTangents(), + Key::Builder().withMaterial().withSkinned().withTangents(), skinModelNormalMapVertex, modelNormalMapPixel); addPipeline( - Key::Builder().withSkinned().withSpecular(), + Key::Builder().withMaterial().withSkinned().withSpecular(), skinModelVertex, modelSpecularMapPixel); addPipeline( - Key::Builder().withSkinned().withTangents().withSpecular(), + Key::Builder().withMaterial().withSkinned().withTangents().withSpecular(), skinModelNormalMapVertex, modelNormalSpecularMapPixel); // Skinned and Translucent addPipeline( - Key::Builder().withSkinned().withTranslucent(), + Key::Builder().withMaterial().withSkinned().withTranslucent(), skinModelVertex, modelTranslucentPixel); addPipeline( - Key::Builder().withSkinned().withTranslucent().withTangents(), + Key::Builder().withMaterial().withSkinned().withTranslucent().withTangents(), skinModelNormalMapVertex, modelTranslucentPixel); addPipeline( - Key::Builder().withSkinned().withTranslucent().withSpecular(), + Key::Builder().withMaterial().withSkinned().withTranslucent().withSpecular(), skinModelVertex, modelTranslucentPixel); addPipeline( - Key::Builder().withSkinned().withTranslucent().withTangents().withSpecular(), + Key::Builder().withMaterial().withSkinned().withTranslucent().withTangents().withSpecular(), skinModelNormalMapVertex, modelTranslucentPixel); // Depth-only addPipeline( @@ -244,39 +286,39 @@ void initForwardPipelines(render::ShapePlumber& plumber) { auto addPipeline = std::bind(&addPlumberPipeline, std::ref(plumber), _1, _2, _3); // Opaques addPipeline( - Key::Builder(), + Key::Builder().withMaterial(), modelVertex, modelPixel); addPipeline( - Key::Builder().withUnlit(), + Key::Builder().withMaterial().withUnlit(), modelVertex, modelUnlitPixel); addPipeline( - Key::Builder().withTangents(), + Key::Builder().withMaterial().withTangents(), modelNormalMapVertex, modelNormalMapPixel); addPipeline( - Key::Builder().withSpecular(), + Key::Builder().withMaterial().withSpecular(), modelVertex, modelSpecularMapPixel); addPipeline( - Key::Builder().withTangents().withSpecular(), + Key::Builder().withMaterial().withTangents().withSpecular(), modelNormalMapVertex, modelNormalSpecularMapPixel); // Skinned addPipeline( - Key::Builder().withSkinned(), + Key::Builder().withMaterial().withSkinned(), skinModelVertex, modelPixel); addPipeline( - Key::Builder().withSkinned().withTangents(), + Key::Builder().withMaterial().withSkinned().withTangents(), skinModelNormalMapVertex, modelNormalMapPixel); addPipeline( - Key::Builder().withSkinned().withSpecular(), + Key::Builder().withMaterial().withSkinned().withSpecular(), skinModelVertex, modelSpecularMapPixel); addPipeline( - Key::Builder().withSkinned().withTangents().withSpecular(), + Key::Builder().withMaterial().withSkinned().withTangents().withSpecular(), skinModelNormalMapVertex, modelNormalSpecularMapPixel); } void addPlumberPipeline(ShapePlumber& plumber, const ShapeKey& key, const gpu::ShaderPointer& vertex, const gpu::ShaderPointer& pixel) { // These key-values' pipelines are added by this functor in addition to the key passed - assert(!key.isWireFrame()); + assert(!key.isWireframe()); assert(!key.isDepthBiased()); assert(key.isCullFace()); @@ -319,9 +361,6 @@ void batchSetter(const ShapePipeline& pipeline, gpu::Batch& batch) { // Set a default albedo map batch.setResourceTexture(render::ShapePipeline::Slot::MAP::ALBEDO, DependencyManager::get()->getWhiteTexture()); - // Set a default normal map - batch.setResourceTexture(render::ShapePipeline::Slot::MAP::NORMAL_FITTING, - DependencyManager::get()->getNormalFittingTexture()); // Set a default material if (pipeline.locations->materialBufferUnit >= 0) { diff --git a/libraries/render-utils/src/Skinning.slh b/libraries/render-utils/src/Skinning.slh index 687dab536b..2d1f010029 100644 --- a/libraries/render-utils/src/Skinning.slh +++ b/libraries/render-utils/src/Skinning.slh @@ -18,11 +18,11 @@ layout(std140) uniform skinClusterBuffer { mat4 clusterMatrices[MAX_CLUSTERS]; }; -void skinPosition(vec4 skinClusterIndex, vec4 skinClusterWeight, vec4 inPosition, out vec4 skinnedPosition) { +void skinPosition(ivec4 skinClusterIndex, vec4 skinClusterWeight, vec4 inPosition, out vec4 skinnedPosition) { vec4 newPosition = vec4(0.0, 0.0, 0.0, 0.0); for (int i = 0; i < INDICES_PER_VERTEX; i++) { - mat4 clusterMatrix = clusterMatrices[int(skinClusterIndex[i])]; + mat4 clusterMatrix = clusterMatrices[(skinClusterIndex[i])]; float clusterWeight = skinClusterWeight[i]; newPosition += clusterMatrix * inPosition * clusterWeight; } @@ -30,13 +30,13 @@ void skinPosition(vec4 skinClusterIndex, vec4 skinClusterWeight, vec4 inPosition skinnedPosition = newPosition; } -void skinPositionNormal(vec4 skinClusterIndex, vec4 skinClusterWeight, vec4 inPosition, vec3 inNormal, +void skinPositionNormal(ivec4 skinClusterIndex, vec4 skinClusterWeight, vec4 inPosition, vec3 inNormal, out vec4 skinnedPosition, out vec3 skinnedNormal) { vec4 newPosition = vec4(0.0, 0.0, 0.0, 0.0); vec4 newNormal = vec4(0.0, 0.0, 0.0, 0.0); for (int i = 0; i < INDICES_PER_VERTEX; i++) { - mat4 clusterMatrix = clusterMatrices[int(skinClusterIndex[i])]; + mat4 clusterMatrix = clusterMatrices[(skinClusterIndex[i])]; float clusterWeight = skinClusterWeight[i]; newPosition += clusterMatrix * inPosition * clusterWeight; newNormal += clusterMatrix * vec4(inNormal.xyz, 0.0) * clusterWeight; @@ -46,14 +46,14 @@ void skinPositionNormal(vec4 skinClusterIndex, vec4 skinClusterWeight, vec4 inPo skinnedNormal = newNormal.xyz; } -void skinPositionNormalTangent(vec4 skinClusterIndex, vec4 skinClusterWeight, vec4 inPosition, vec3 inNormal, vec3 inTangent, +void skinPositionNormalTangent(ivec4 skinClusterIndex, vec4 skinClusterWeight, vec4 inPosition, vec3 inNormal, vec3 inTangent, out vec4 skinnedPosition, out vec3 skinnedNormal, out vec3 skinnedTangent) { vec4 newPosition = vec4(0.0, 0.0, 0.0, 0.0); vec4 newNormal = vec4(0.0, 0.0, 0.0, 0.0); vec4 newTangent = vec4(0.0, 0.0, 0.0, 0.0); for (int i = 0; i < INDICES_PER_VERTEX; i++) { - mat4 clusterMatrix = clusterMatrices[int(skinClusterIndex[i])]; + mat4 clusterMatrix = clusterMatrices[(skinClusterIndex[i])]; float clusterWeight = skinClusterWeight[i]; newPosition += clusterMatrix * inPosition * clusterWeight; newNormal += clusterMatrix * vec4(inNormal.xyz, 0.0) * clusterWeight; diff --git a/libraries/render-utils/src/SubsurfaceScattering.cpp b/libraries/render-utils/src/SubsurfaceScattering.cpp index 188381b822..a57657a353 100644 --- a/libraries/render-utils/src/SubsurfaceScattering.cpp +++ b/libraries/render-utils/src/SubsurfaceScattering.cpp @@ -414,7 +414,7 @@ gpu::TexturePointer SubsurfaceScatteringResource::generateScatteringProfile(Rend const int PROFILE_RESOLUTION = 512; // const auto pixelFormat = gpu::Element::COLOR_SRGBA_32; const auto pixelFormat = gpu::Element::COLOR_R11G11B10; - auto profileMap = gpu::TexturePointer(gpu::Texture::create2D(pixelFormat, PROFILE_RESOLUTION, 1, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR, gpu::Sampler::WRAP_CLAMP))); + auto profileMap = gpu::TexturePointer(gpu::Texture::createRenderBuffer(pixelFormat, PROFILE_RESOLUTION, 1, gpu::Texture::SINGLE_MIP, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR, gpu::Sampler::WRAP_CLAMP))); profileMap->setSource("Generated Scattering Profile"); diffuseProfileGPU(profileMap, args); return profileMap; @@ -425,7 +425,7 @@ gpu::TexturePointer SubsurfaceScatteringResource::generatePreIntegratedScatterin const int TABLE_RESOLUTION = 512; // const auto pixelFormat = gpu::Element::COLOR_SRGBA_32; const auto pixelFormat = gpu::Element::COLOR_R11G11B10; - auto scatteringLUT = gpu::TexturePointer(gpu::Texture::create2D(pixelFormat, TABLE_RESOLUTION, TABLE_RESOLUTION, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR, gpu::Sampler::WRAP_CLAMP))); + auto scatteringLUT = gpu::TexturePointer(gpu::Texture::createRenderBuffer(pixelFormat, TABLE_RESOLUTION, TABLE_RESOLUTION, gpu::Texture::SINGLE_MIP, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR, gpu::Sampler::WRAP_CLAMP))); //diffuseScatter(scatteringLUT); scatteringLUT->setSource("Generated pre-integrated scattering"); diffuseScatterGPU(profile, scatteringLUT, args); @@ -434,7 +434,7 @@ gpu::TexturePointer SubsurfaceScatteringResource::generatePreIntegratedScatterin gpu::TexturePointer SubsurfaceScatteringResource::generateScatteringSpecularBeckmann(RenderArgs* args) { const int SPECULAR_RESOLUTION = 256; - auto beckmannMap = gpu::TexturePointer(gpu::Texture::create2D(gpu::Element::COLOR_RGBA_32 /*gpu::Element(gpu::SCALAR, gpu::HALF, gpu::RGB)*/, SPECULAR_RESOLUTION, SPECULAR_RESOLUTION, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR, gpu::Sampler::WRAP_CLAMP))); + auto beckmannMap = gpu::TexturePointer(gpu::Texture::createRenderBuffer(gpu::Element::COLOR_RGBA_32, SPECULAR_RESOLUTION, SPECULAR_RESOLUTION, gpu::Texture::SINGLE_MIP, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR, gpu::Sampler::WRAP_CLAMP))); beckmannMap->setSource("Generated beckmannMap"); computeSpecularBeckmannGPU(beckmannMap, args); return beckmannMap; diff --git a/libraries/render-utils/src/SurfaceGeometryPass.cpp b/libraries/render-utils/src/SurfaceGeometryPass.cpp index f0ac56ac26..a4a83bb6c5 100644 --- a/libraries/render-utils/src/SurfaceGeometryPass.cpp +++ b/libraries/render-utils/src/SurfaceGeometryPass.cpp @@ -72,18 +72,18 @@ void LinearDepthFramebuffer::allocate() { auto height = _frameSize.y; // For Linear Depth: - _linearDepthTexture = gpu::TexturePointer(gpu::Texture::create2D(gpu::Element(gpu::SCALAR, gpu::FLOAT, gpu::RGB), width, height, + _linearDepthTexture = gpu::TexturePointer(gpu::Texture::createRenderBuffer(gpu::Element(gpu::SCALAR, gpu::FLOAT, gpu::RED), width, height, gpu::Texture::SINGLE_MIP, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_LINEAR_MIP_POINT))); _linearDepthFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create("linearDepth")); _linearDepthFramebuffer->setRenderBuffer(0, _linearDepthTexture); _linearDepthFramebuffer->setDepthStencilBuffer(_primaryDepthTexture, _primaryDepthTexture->getTexelFormat()); // For Downsampling: - _halfLinearDepthTexture = gpu::TexturePointer(gpu::Texture::create2D(gpu::Element(gpu::SCALAR, gpu::FLOAT, gpu::RGB), _halfFrameSize.x, _halfFrameSize.y, + const uint16_t HALF_LINEAR_DEPTH_MAX_MIP_LEVEL = 5; + _halfLinearDepthTexture = gpu::TexturePointer(gpu::Texture::createRenderBuffer(gpu::Element(gpu::SCALAR, gpu::FLOAT, gpu::RED), _halfFrameSize.x, _halfFrameSize.y, HALF_LINEAR_DEPTH_MAX_MIP_LEVEL, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_LINEAR_MIP_POINT))); - _halfLinearDepthTexture->autoGenerateMips(5); - _halfNormalTexture = gpu::TexturePointer(gpu::Texture::create2D(gpu::Element(gpu::VEC3, gpu::NUINT8, gpu::RGB), _halfFrameSize.x, _halfFrameSize.y, + _halfNormalTexture = gpu::TexturePointer(gpu::Texture::createRenderBuffer(gpu::Element::COLOR_RGBA_32, _halfFrameSize.x, _halfFrameSize.y, gpu::Texture::SINGLE_MIP, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_LINEAR_MIP_POINT))); _downsampleFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create("halfLinearDepth")); @@ -304,15 +304,15 @@ void SurfaceGeometryFramebuffer::allocate() { auto width = _frameSize.x; auto height = _frameSize.y; - _curvatureTexture = gpu::TexturePointer(gpu::Texture::create2D(gpu::Element::COLOR_RGBA_32, width, height, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_LINEAR_MIP_POINT))); + _curvatureTexture = gpu::TexturePointer(gpu::Texture::createRenderBuffer(gpu::Element::COLOR_RGBA_32, width, height, gpu::Texture::SINGLE_MIP, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_LINEAR_MIP_POINT))); _curvatureFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create("surfaceGeometry::curvature")); _curvatureFramebuffer->setRenderBuffer(0, _curvatureTexture); - _lowCurvatureTexture = gpu::TexturePointer(gpu::Texture::create2D(gpu::Element::COLOR_RGBA_32, width, height, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_LINEAR_MIP_POINT))); + _lowCurvatureTexture = gpu::TexturePointer(gpu::Texture::createRenderBuffer(gpu::Element::COLOR_RGBA_32, width, height, gpu::Texture::SINGLE_MIP, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_LINEAR_MIP_POINT))); _lowCurvatureFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create("surfaceGeometry::lowCurvature")); _lowCurvatureFramebuffer->setRenderBuffer(0, _lowCurvatureTexture); - _blurringTexture = gpu::TexturePointer(gpu::Texture::create2D(gpu::Element::COLOR_RGBA_32, width, height, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_LINEAR_MIP_POINT))); + _blurringTexture = gpu::TexturePointer(gpu::Texture::createRenderBuffer(gpu::Element::COLOR_RGBA_32, width, height, gpu::Texture::SINGLE_MIP, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_LINEAR_MIP_POINT))); _blurringFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create("surfaceGeometry::blurring")); _blurringFramebuffer->setRenderBuffer(0, _blurringTexture); } diff --git a/libraries/render-utils/src/debug_deferred_buffer.slf b/libraries/render-utils/src/debug_deferred_buffer.slf index 079f79c06e..e9750f0054 100644 --- a/libraries/render-utils/src/debug_deferred_buffer.slf +++ b/libraries/render-utils/src/debug_deferred_buffer.slf @@ -13,16 +13,23 @@ // <@include DeferredBufferRead.slh@> +<@include gpu/Color.slh@> +<$declareColorWheel()$> + uniform sampler2D linearDepthMap; uniform sampler2D halfLinearDepthMap; uniform sampler2D halfNormalMap; uniform sampler2D occlusionMap; uniform sampler2D occlusionBlurredMap; -uniform sampler2D curvatureMap; -uniform sampler2D diffusedCurvatureMap; uniform sampler2D scatteringMap; +<$declareDeferredCurvature()$> + +float curvatureAO(float k) { + return 1.0f - (0.0022f * k * k) + (0.0776f * k) + 0.7369f; +} + in vec2 uv; out vec4 outFragColor; diff --git a/libraries/render-utils/src/overlay3D_model.slf b/libraries/render-utils/src/overlay3D_model.slf new file mode 100644 index 0000000000..bb0d84a513 --- /dev/null +++ b/libraries/render-utils/src/overlay3D_model.slf @@ -0,0 +1,88 @@ +<@include gpu/Config.slh@> +<$VERSION_HEADER$> +// Generated on <$_SCRIBE_DATE$> +// overlay3D.slf +// fragment shader +// +// Created by Sam Gateau on 6/16/15. +// Copyright 2015 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +<@include DeferredGlobalLight.slh@> +<$declareEvalSkyboxGlobalColor()$> + +<@include model/Material.slh@> + +<@include gpu/Transform.slh@> +<$declareStandardCameraTransform()$> + +<@include MaterialTextures.slh@> +<$declareMaterialTextures(ALBEDO, ROUGHNESS, _SCRIBE_NULL, _SCRIBE_NULL, EMISSIVE, OCCLUSION)$> + +in vec2 _texCoord0; +in vec2 _texCoord1; +in vec4 _position; +in vec3 _normal; +in vec3 _color; +in float _alpha; + +out vec4 _fragColor; + +void main(void) { + Material mat = getMaterial(); + int matKey = getMaterialKey(mat); + <$fetchMaterialTexturesCoord0(matKey, _texCoord0, albedoTex, roughnessTex, _SCRIBE_NULL, _SCRIBE_NULL, emissiveTex)$> + <$fetchMaterialTexturesCoord1(matKey, _texCoord1, occlusionTex)$> + + float opacity = 1.0; + <$evalMaterialOpacity(albedoTex.a, opacity, matKey, opacity)$>; + <$discardTransparent(opacity)$>; + + vec3 albedo = getMaterialAlbedo(mat); + <$evalMaterialAlbedo(albedoTex, albedo, matKey, albedo)$>; + albedo *= _color; + + float metallic = getMaterialMetallic(mat); + vec3 fresnel = vec3(0.03); // Default Di-electric fresnel value + if (metallic <= 0.5) { + metallic = 0.0; + } else { + fresnel = albedo; + metallic = 1.0; + } + + float roughness = getMaterialRoughness(mat); + <$evalMaterialRoughness(roughnessTex, roughness, matKey, roughness)$>; + + vec3 emissive = getMaterialEmissive(mat); + <$evalMaterialEmissive(emissiveTex, emissive, matKey, emissive)$>; + + + vec3 fragPosition = _position.xyz; + //vec3 fragNormal = normalize(_normal); + + TransformCamera cam = getTransformCamera(); + vec3 fragNormal; + <$transformEyeToWorldDir(cam, _normal, fragNormal)$>; + + vec4 color = vec4(evalSkyboxGlobalColor( + cam._viewInverse, + 1.0, + occlusionTex, + fragPosition, + fragNormal, + albedo, + fresnel, + metallic, + roughness), + opacity); + + // And emissive + color.rgb += emissive * isEmissiveEnabled(); + + // Apply standard tone mapping + _fragColor = vec4(pow(color.xyz, vec3(1.0 / 2.2)), color.w); +} \ No newline at end of file diff --git a/libraries/render-utils/src/overlay3D_model_translucent.slf b/libraries/render-utils/src/overlay3D_model_translucent.slf new file mode 100644 index 0000000000..748eea329c --- /dev/null +++ b/libraries/render-utils/src/overlay3D_model_translucent.slf @@ -0,0 +1,83 @@ +<@include gpu/Config.slh@> +<$VERSION_HEADER$> +// Generated on <$_SCRIBE_DATE$> +// overlay3D_model_transparent.slf +// +// Created by Sam Gateau on 2/27/2017. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +<@include DeferredGlobalLight.slh@> +<$declareEvalGlobalLightingAlphaBlended()$> + +<@include model/Material.slh@> + +<@include gpu/Transform.slh@> +<$declareStandardCameraTransform()$> + +<@include MaterialTextures.slh@> +<$declareMaterialTextures(ALBEDO, ROUGHNESS, _SCRIBE_NULL, _SCRIBE_NULL, EMISSIVE, OCCLUSION)$> + +in vec2 _texCoord0; +in vec2 _texCoord1; +in vec4 _position; +in vec3 _normal; +in vec3 _color; +in float _alpha; + +out vec4 _fragColor; + +void main(void) { + Material mat = getMaterial(); + int matKey = getMaterialKey(mat); + <$fetchMaterialTexturesCoord0(matKey, _texCoord0, albedoTex, roughnessTex, _SCRIBE_NULL, _SCRIBE_NULL, emissiveTex)$> + <$fetchMaterialTexturesCoord1(matKey, _texCoord1, occlusionTex)$> + + float opacity = 1.0; + <$evalMaterialOpacity(albedoTex.a, opacity, matKey, opacity)$>; + + vec3 albedo = getMaterialAlbedo(mat); + <$evalMaterialAlbedo(albedoTex, albedo, matKey, albedo)$>; + albedo *= _color; + + float metallic = getMaterialMetallic(mat); + vec3 fresnel = vec3(0.03); // Default Di-electric fresnel value + if (metallic <= 0.5) { + metallic = 0.0; + } else { + fresnel = albedo; + metallic = 1.0; + } + + float roughness = getMaterialRoughness(mat); + <$evalMaterialRoughness(roughnessTex, roughness, matKey, roughness)$>; + + vec3 emissive = getMaterialEmissive(mat); + <$evalMaterialEmissive(emissiveTex, emissive, matKey, emissive)$>; + + + vec3 fragPosition = _position.xyz; + + TransformCamera cam = getTransformCamera(); + vec3 fragNormal; + <$transformEyeToWorldDir(cam, _normal, fragNormal)$> + + vec4 color = vec4(evalGlobalLightingAlphaBlended( + cam._viewInverse, + 1.0, + occlusionTex, + fragPosition, + fragNormal, + albedo, + fresnel, + metallic, + emissive, + roughness, opacity), + opacity); + + // Apply standard tone mapping + _fragColor = vec4(pow(color.xyz, vec3(1.0 / 2.2)), color.w); +} \ No newline at end of file diff --git a/libraries/render-utils/src/overlay3D_model_translucent_unlit.slf b/libraries/render-utils/src/overlay3D_model_translucent_unlit.slf new file mode 100644 index 0000000000..3dd8138272 --- /dev/null +++ b/libraries/render-utils/src/overlay3D_model_translucent_unlit.slf @@ -0,0 +1,43 @@ +<@include gpu/Config.slh@> +<$VERSION_HEADER$> +// Generated on <$_SCRIBE_DATE$> +// overlay3D-model_transparent_unlit.slf +// fragment shader +// +// Created by Sam Gateau on 2/28/2017. +// Copyright 2015 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +<@include LightingModel.slh@> +<@include model/Material.slh@> + +<@include MaterialTextures.slh@> +<$declareMaterialTextures(ALBEDO)$> + +in vec2 _texCoord0; +in vec3 _normal; +in vec3 _color; +in float _alpha; + +out vec4 _fragColor; + +void main(void) { + + Material mat = getMaterial(); + int matKey = getMaterialKey(mat); + <$fetchMaterialTexturesCoord0(matKey, _texCoord0, albedoTex)$> + + float opacity = 1.0; + <$evalMaterialOpacity(albedoTex.a, opacity, matKey, opacity)$>; + + vec3 albedo = getMaterialAlbedo(mat); + <$evalMaterialAlbedo(albedoTex, albedo, matKey, albedo)$>; + albedo *= _color; + + vec4 color = vec4(albedo * isUnlitEnabled(), opacity); + + _fragColor = vec4(pow(color.xyz, vec3(1.0 / 2.2)), color.w); +} \ No newline at end of file diff --git a/libraries/render-utils/src/overlay3D_model_unlit.slf b/libraries/render-utils/src/overlay3D_model_unlit.slf new file mode 100644 index 0000000000..80c2bb971e --- /dev/null +++ b/libraries/render-utils/src/overlay3D_model_unlit.slf @@ -0,0 +1,44 @@ +<@include gpu/Config.slh@> +<$VERSION_HEADER$> +// Generated on <$_SCRIBE_DATE$> +// overlay3D-model_unlit.slf +// fragment shader +// +// Created by Sam Gateau on 2/28/2017. +// Copyright 2015 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +<@include LightingModel.slh@> +<@include model/Material.slh@> + +<@include MaterialTextures.slh@> +<$declareMaterialTextures(ALBEDO)$> + +in vec2 _texCoord0; +in vec3 _normal; +in vec3 _color; +in float _alpha; + +out vec4 _fragColor; + +void main(void) { + + Material mat = getMaterial(); + int matKey = getMaterialKey(mat); + <$fetchMaterialTexturesCoord0(matKey, _texCoord0, albedoTex)$> + + float opacity = 1.0; + <$evalMaterialOpacity(albedoTex.a, opacity, matKey, opacity)$>; + <$discardTransparent(opacity)$>; + + vec3 albedo = getMaterialAlbedo(mat); + <$evalMaterialAlbedo(albedoTex, albedo, matKey, albedo)$>; + albedo *= _color; + + vec4 color = vec4(albedo * isUnlitEnabled(), opacity); + + _fragColor = vec4(pow(color.xyz, vec3(1.0 / 2.2)), color.w); +} diff --git a/libraries/render-utils/src/simple_textured.slf b/libraries/render-utils/src/simple_textured.slf index 6067c81a1b..550f6546fd 100644 --- a/libraries/render-utils/src/simple_textured.slf +++ b/libraries/render-utils/src/simple_textured.slf @@ -26,15 +26,17 @@ in vec2 _texCoord0; void main(void) { vec4 texel = texture(originalTexture, _texCoord0); + float colorAlpha = _color.a; if (_color.a <= 0.0) { texel = colorToLinearRGBA(texel); + colorAlpha = -_color.a; } const float ALPHA_THRESHOLD = 0.999; - if (_color.a * texel.a < ALPHA_THRESHOLD) { + if (colorAlpha * texel.a < ALPHA_THRESHOLD) { packDeferredFragmentTranslucent( normalize(_normal), - _color.a * texel.a, + colorAlpha * texel.a, _color.rgb * texel.rgb, DEFAULT_FRESNEL, DEFAULT_ROUGHNESS); diff --git a/libraries/render-utils/src/simple_textured_unlit.slf b/libraries/render-utils/src/simple_textured_unlit.slf index 4f02140825..d261fb343a 100644 --- a/libraries/render-utils/src/simple_textured_unlit.slf +++ b/libraries/render-utils/src/simple_textured_unlit.slf @@ -2,7 +2,7 @@ <$VERSION_HEADER$> // Generated on <$_SCRIBE_DATE$> // -// simple.frag +// simple_textured_unlit.frag // fragment shader // // Created by Clément Brisset on 5/29/15. @@ -25,15 +25,17 @@ in vec2 _texCoord0; void main(void) { vec4 texel = texture(originalTexture, _texCoord0.st); + float colorAlpha = _color.a; if (_color.a <= 0.0) { texel = colorToLinearRGBA(texel); + colorAlpha = -_color.a; } const float ALPHA_THRESHOLD = 0.999; - if (_color.a * texel.a < ALPHA_THRESHOLD) { - packDeferredFragmentTranslucent( + if (colorAlpha * texel.a < ALPHA_THRESHOLD) { + packDeferredFragmentTranslucent( normalize(_normal), - _color.a * texel.a, + colorAlpha * texel.a, _color.rgb * texel.rgb, DEFAULT_FRESNEL, DEFAULT_ROUGHNESS); diff --git a/libraries/render-utils/src/simple_transparent_textured.slf b/libraries/render-utils/src/simple_transparent_textured.slf new file mode 100644 index 0000000000..b9eb921e9d --- /dev/null +++ b/libraries/render-utils/src/simple_transparent_textured.slf @@ -0,0 +1,62 @@ +<@include gpu/Config.slh@> +<$VERSION_HEADER$> +// Generated on <$_SCRIBE_DATE$> +// +// simple_transparent_textured.slf +// fragment shader +// +// Created by Sam Gateau on 4/3/17. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +<@include gpu/Color.slh@> + +<@include DeferredBufferWrite.slh@> +<@include DeferredGlobalLight.slh@> +<$declareEvalGlobalLightingAlphaBlended()$> + +<@include gpu/Transform.slh@> +<$declareStandardCameraTransform()$> + +// the albedo texture +uniform sampler2D originalTexture; + +// the interpolated normal +in vec4 _position; +in vec3 _normal; +in vec4 _color; +in vec2 _texCoord0; + +void main(void) { + vec4 texel = texture(originalTexture, _texCoord0.st); + float opacity = _color.a; + if (_color.a <= 0.0) { + texel = colorToLinearRGBA(texel); + opacity = -_color.a; + } + opacity *= texel.a; + vec3 albedo = _color.rgb * texel.rgb; + + vec3 fragPosition = _position.xyz; + vec3 fragNormal = normalize(_normal); + + TransformCamera cam = getTransformCamera(); + + _fragColor0 = vec4(evalGlobalLightingAlphaBlended( + cam._viewInverse, + 1.0, + 1.0, + fragPosition, + fragNormal, + albedo, + DEFAULT_FRESNEL, + 0.0, + vec3(0.0f), + DEFAULT_ROUGHNESS, + opacity), + opacity); + +} \ No newline at end of file diff --git a/libraries/render-utils/src/simple_transparent_textured_unlit.slf b/libraries/render-utils/src/simple_transparent_textured_unlit.slf new file mode 100644 index 0000000000..693d7be2db --- /dev/null +++ b/libraries/render-utils/src/simple_transparent_textured_unlit.slf @@ -0,0 +1,36 @@ +<@include gpu/Config.slh@> +<$VERSION_HEADER$> +// Generated on <$_SCRIBE_DATE$> +// +// simple_transparent_textured_unlit.slf +// fragment shader +// +// Created by Sam Gateau on 4/3/17. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +<@include gpu/Color.slh@> + +// the albedo texture +uniform sampler2D originalTexture; + +// the interpolated normal +in vec3 _normal; +in vec4 _color; +in vec2 _texCoord0; + + +layout(location = 0) out vec4 _fragColor0; + +void main(void) { + vec4 texel = texture(originalTexture, _texCoord0.st); + float colorAlpha = _color.a; + if (_color.a <= 0.0) { + texel = colorToLinearRGBA(texel); + colorAlpha = -_color.a; + } + _fragColor0 = vec4(_color.rgb * texel.rgb, colorAlpha * texel.a); +} \ No newline at end of file diff --git a/libraries/render-utils/src/surfaceGeometry_downsampleDepthNormal.slf b/libraries/render-utils/src/surfaceGeometry_downsampleDepthNormal.slf index 533073b224..205dad124e 100644 --- a/libraries/render-utils/src/surfaceGeometry_downsampleDepthNormal.slf +++ b/libraries/render-utils/src/surfaceGeometry_downsampleDepthNormal.slf @@ -21,17 +21,17 @@ out vec4 outLinearDepth; out vec4 outNormal; void main(void) { - // Gather 2 by 2 quads from texture + // Gather 2 by 2 quads from texture and downsample // Try different filters for Z -// vec4 Zeyes = textureGather(linearDepthMap, varTexCoord0, 0); - // float Zeye = min(min(Zeyes.x, Zeyes.y), min(Zeyes.z, Zeyes.w)); - float Zeye = texture(linearDepthMap, varTexCoord0).x; + vec4 Zeyes = textureGather(linearDepthMap, varTexCoord0, 0); + // float Zeye = texture(linearDepthMap, varTexCoord0).x; vec4 rawNormalsX = textureGather(normalMap, varTexCoord0, 0); vec4 rawNormalsY = textureGather(normalMap, varTexCoord0, 1); vec4 rawNormalsZ = textureGather(normalMap, varTexCoord0, 2); + float Zeye = min(min(Zeyes.x, Zeyes.y), min(Zeyes.z, Zeyes.w)); vec3 normal = vec3(0.0); normal += unpackNormal(vec3(rawNormalsX[0], rawNormalsY[0], rawNormalsZ[0])); diff --git a/libraries/render-utils/src/text/Font.cpp b/libraries/render-utils/src/text/Font.cpp index 4f4ee12622..00fcabd7da 100644 --- a/libraries/render-utils/src/text/Font.cpp +++ b/libraries/render-utils/src/text/Font.cpp @@ -207,9 +207,10 @@ void Font::read(QIODevice& in) { formatGPU = gpu::Element(gpu::VEC4, gpu::NUINT8, gpu::RGBA); formatMip = gpu::Element(gpu::VEC4, gpu::NUINT8, gpu::BGRA); } - _texture = gpu::TexturePointer(gpu::Texture::create2D(formatGPU, image.width(), image.height(), + _texture = gpu::TexturePointer(gpu::Texture::create2D(formatGPU, image.width(), image.height(), gpu::Texture::SINGLE_MIP, gpu::Sampler(gpu::Sampler::FILTER_MIN_POINT_MAG_LINEAR))); - _texture->assignStoredMip(0, formatMip, image.byteCount(), image.constBits()); + _texture->setStoredMipFormat(formatMip); + _texture->assignStoredMip(0, image.byteCount(), image.constBits()); } void Font::setupGPU() { diff --git a/libraries/render/CMakeLists.txt b/libraries/render/CMakeLists.txt index 735bb7f086..8fd05bd320 100644 --- a/libraries/render/CMakeLists.txt +++ b/libraries/render/CMakeLists.txt @@ -3,6 +3,6 @@ AUTOSCRIBE_SHADER_LIB(gpu model) setup_hifi_library() # render needs octree only for getAccuracyAngle(float, int) -link_hifi_libraries(shared gpu model octree) +link_hifi_libraries(shared ktx gpu model octree) target_nsight() diff --git a/libraries/render/src/render/BlurTask.cpp b/libraries/render/src/render/BlurTask.cpp index f8b5546b92..b0329b22a5 100644 --- a/libraries/render/src/render/BlurTask.cpp +++ b/libraries/render/src/render/BlurTask.cpp @@ -108,7 +108,7 @@ bool BlurInOutResource::updateResources(const gpu::FramebufferPointer& sourceFra // _blurredFramebuffer->setDepthStencilBuffer(sourceFramebuffer->getDepthStencilBuffer(), sourceFramebuffer->getDepthStencilBufferFormat()); //} auto blurringSampler = gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_LINEAR_MIP_POINT); - auto blurringTarget = gpu::TexturePointer(gpu::Texture::create2D(sourceFramebuffer->getRenderBuffer(0)->getTexelFormat(), sourceFramebuffer->getWidth(), sourceFramebuffer->getHeight(), blurringSampler)); + auto blurringTarget = gpu::TexturePointer(gpu::Texture::create2D(sourceFramebuffer->getRenderBuffer(0)->getTexelFormat(), sourceFramebuffer->getWidth(), sourceFramebuffer->getHeight(), gpu::Texture::SINGLE_MIP, blurringSampler)); _blurredFramebuffer->setRenderBuffer(0, blurringTarget); } @@ -131,7 +131,7 @@ bool BlurInOutResource::updateResources(const gpu::FramebufferPointer& sourceFra _outputFramebuffer->setDepthStencilBuffer(sourceFramebuffer->getDepthStencilBuffer(), sourceFramebuffer->getDepthStencilBufferFormat()); }*/ auto blurringSampler = gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_LINEAR_MIP_POINT); - auto blurringTarget = gpu::TexturePointer(gpu::Texture::create2D(sourceFramebuffer->getRenderBuffer(0)->getTexelFormat(), sourceFramebuffer->getWidth(), sourceFramebuffer->getHeight(), blurringSampler)); + auto blurringTarget = gpu::TexturePointer(gpu::Texture::create2D(sourceFramebuffer->getRenderBuffer(0)->getTexelFormat(), sourceFramebuffer->getWidth(), sourceFramebuffer->getHeight(), gpu::Texture::SINGLE_MIP, blurringSampler)); _outputFramebuffer->setRenderBuffer(0, blurringTarget); } diff --git a/libraries/render/src/render/DrawTask.cpp b/libraries/render/src/render/DrawTask.cpp index 2829c6f8e7..e8537e3452 100755 --- a/libraries/render/src/render/DrawTask.cpp +++ b/libraries/render/src/render/DrawTask.cpp @@ -39,9 +39,9 @@ void render::renderItems(const SceneContextPointer& sceneContext, const RenderCo } } -void renderShape(RenderArgs* args, const ShapePlumberPointer& shapeContext, const Item& item) { +void renderShape(RenderArgs* args, const ShapePlumberPointer& shapeContext, const Item& item, const ShapeKey& globalKey) { assert(item.getKey().isShape()); - const auto& key = item.getShapeKey(); + auto key = item.getShapeKey() | globalKey; if (key.isValid() && !key.hasOwnPipeline()) { args->_pipeline = shapeContext->pickPipeline(args, key); if (args->_pipeline) { @@ -56,7 +56,7 @@ void renderShape(RenderArgs* args, const ShapePlumberPointer& shapeContext, cons } void render::renderShapes(const SceneContextPointer& sceneContext, const RenderContextPointer& renderContext, - const ShapePlumberPointer& shapeContext, const ItemBounds& inItems, int maxDrawnItems) { + const ShapePlumberPointer& shapeContext, const ItemBounds& inItems, int maxDrawnItems, const ShapeKey& globalKey) { auto& scene = sceneContext->_scene; RenderArgs* args = renderContext->args; @@ -66,12 +66,12 @@ void render::renderShapes(const SceneContextPointer& sceneContext, const RenderC } for (auto i = 0; i < numItemsToDraw; ++i) { auto& item = scene->getItem(inItems[i].id); - renderShape(args, shapeContext, item); + renderShape(args, shapeContext, item, globalKey); } } void render::renderStateSortShapes(const SceneContextPointer& sceneContext, const RenderContextPointer& renderContext, - const ShapePlumberPointer& shapeContext, const ItemBounds& inItems, int maxDrawnItems) { + const ShapePlumberPointer& shapeContext, const ItemBounds& inItems, int maxDrawnItems, const ShapeKey& globalKey) { auto& scene = sceneContext->_scene; RenderArgs* args = renderContext->args; @@ -91,7 +91,7 @@ void render::renderStateSortShapes(const SceneContextPointer& sceneContext, cons { assert(item.getKey().isShape()); - const auto key = item.getShapeKey(); + auto key = item.getShapeKey() | globalKey; if (key.isValid() && !key.hasOwnPipeline()) { auto& bucket = sortedShapes[key]; if (bucket.empty()) { diff --git a/libraries/render/src/render/DrawTask.h b/libraries/render/src/render/DrawTask.h index 27f07921c3..a9c5f3a4d8 100755 --- a/libraries/render/src/render/DrawTask.h +++ b/libraries/render/src/render/DrawTask.h @@ -17,10 +17,8 @@ namespace render { void renderItems(const SceneContextPointer& sceneContext, const RenderContextPointer& renderContext, const ItemBounds& inItems, int maxDrawnItems = -1); -void renderShapes(const SceneContextPointer& sceneContext, const RenderContextPointer& renderContext, const ShapePlumberPointer& shapeContext, const ItemBounds& inItems, int maxDrawnItems = -1); -void renderStateSortShapes(const SceneContextPointer& sceneContext, const RenderContextPointer& renderContext, const ShapePlumberPointer& shapeContext, const ItemBounds& inItems, int maxDrawnItems = -1); - - +void renderShapes(const SceneContextPointer& sceneContext, const RenderContextPointer& renderContext, const ShapePlumberPointer& shapeContext, const ItemBounds& inItems, int maxDrawnItems = -1, const ShapeKey& globalKey = ShapeKey()); +void renderStateSortShapes(const SceneContextPointer& sceneContext, const RenderContextPointer& renderContext, const ShapePlumberPointer& shapeContext, const ItemBounds& inItems, int maxDrawnItems = -1, const ShapeKey& globalKey = ShapeKey()); class DrawLightConfig : public Job::Config { Q_OBJECT diff --git a/libraries/render/src/render/Scene.cpp b/libraries/render/src/render/Scene.cpp index 076879ae25..537d8c1337 100644 --- a/libraries/render/src/render/Scene.cpp +++ b/libraries/render/src/render/Scene.cpp @@ -16,31 +16,31 @@ using namespace render; -void PendingChanges::resetItem(ItemID id, const PayloadPointer& payload) { +void Transaction::resetItem(ItemID id, const PayloadPointer& payload) { if (payload) { _resetItems.push_back(id); _resetPayloads.push_back(payload); } else { - qCDebug(renderlogging) << "WARNING: PendingChanges::resetItem with a null payload!"; + qCDebug(renderlogging) << "WARNING: Transaction::resetItem with a null payload!"; removeItem(id); } } -void PendingChanges::removeItem(ItemID id) { +void Transaction::removeItem(ItemID id) { _removedItems.push_back(id); } -void PendingChanges::updateItem(ItemID id, const UpdateFunctorPointer& functor) { +void Transaction::updateItem(ItemID id, const UpdateFunctorPointer& functor) { _updatedItems.push_back(id); _updateFunctors.push_back(functor); } -void PendingChanges::merge(const PendingChanges& changes) { - _resetItems.insert(_resetItems.end(), changes._resetItems.begin(), changes._resetItems.end()); - _resetPayloads.insert(_resetPayloads.end(), changes._resetPayloads.begin(), changes._resetPayloads.end()); - _removedItems.insert(_removedItems.end(), changes._removedItems.begin(), changes._removedItems.end()); - _updatedItems.insert(_updatedItems.end(), changes._updatedItems.begin(), changes._updatedItems.end()); - _updateFunctors.insert(_updateFunctors.end(), changes._updateFunctors.begin(), changes._updateFunctors.end()); +void Transaction::merge(const Transaction& transaction) { + _resetItems.insert(_resetItems.end(), transaction._resetItems.begin(), transaction._resetItems.end()); + _resetPayloads.insert(_resetPayloads.end(), transaction._resetPayloads.begin(), transaction._resetPayloads.end()); + _removedItems.insert(_removedItems.end(), transaction._removedItems.begin(), transaction._removedItems.end()); + _updatedItems.insert(_updatedItems.end(), transaction._updatedItems.begin(), transaction._updatedItems.end()); + _updateFunctors.insert(_updateFunctors.end(), transaction._updateFunctors.begin(), transaction._updateFunctors.end()); } Scene::Scene(glm::vec3 origin, float size) : @@ -63,27 +63,27 @@ bool Scene::isAllocatedID(const ItemID& id) const { } /// Enqueue change batch to the scene -void Scene::enqueuePendingChanges(const PendingChanges& pendingChanges) { - _changeQueueMutex.lock(); - _changeQueue.push(pendingChanges); - _changeQueueMutex.unlock(); +void Scene::enqueueTransaction(const Transaction& transaction) { + _transactionQueueMutex.lock(); + _transactionQueue.push(transaction); + _transactionQueueMutex.unlock(); } -void consolidateChangeQueue(PendingChangesQueue& queue, PendingChanges& singleBatch) { +void consolidateTransaction(TransactionQueue& queue, Transaction& singleBatch) { while (!queue.empty()) { - const auto& pendingChanges = queue.front(); - singleBatch.merge(pendingChanges); + const auto& transaction = queue.front(); + singleBatch.merge(transaction); queue.pop(); }; } -void Scene::processPendingChangesQueue() { +void Scene::processTransactionQueue() { PROFILE_RANGE(render, __FUNCTION__); - PendingChanges consolidatedPendingChanges; + Transaction consolidatedTransaction; { - std::unique_lock lock(_changeQueueMutex); - consolidateChangeQueue(_changeQueue, consolidatedPendingChanges); + std::unique_lock lock(_transactionQueueMutex); + consolidateTransaction(_transactionQueue, consolidatedTransaction); } { @@ -95,19 +95,19 @@ void Scene::processPendingChangesQueue() { _items.resize(maxID + 100); // allocate the maxId and more } // Now we know for sure that we have enough items in the array to - // capture anything coming from the pendingChanges + // capture anything coming from the transaction // resets and potential NEW items - resetItems(consolidatedPendingChanges._resetItems, consolidatedPendingChanges._resetPayloads); + resetItems(consolidatedTransaction._resetItems, consolidatedTransaction._resetPayloads); // Update the numItemsAtomic counter AFTER the reset changes went through _numAllocatedItems.exchange(maxID); // updates - updateItems(consolidatedPendingChanges._updatedItems, consolidatedPendingChanges._updateFunctors); + updateItems(consolidatedTransaction._updatedItems, consolidatedTransaction._updateFunctors); // removes - removeItems(consolidatedPendingChanges._removedItems); + removeItems(consolidatedTransaction._removedItems); // Update the numItemsAtomic counter AFTER the pending changes went through _numAllocatedItems.exchange(maxID); diff --git a/libraries/render/src/render/Scene.h b/libraries/render/src/render/Scene.h index f2f8403dc9..09a725205d 100644 --- a/libraries/render/src/render/Scene.h +++ b/libraries/render/src/render/Scene.h @@ -19,10 +19,19 @@ namespace render { class Engine; -class PendingChanges { +// Transaction is the mechanism to make any change to the scene. +// Whenever a new item need to be reset, +// or when an item changes its position or its size +// or when an item's payload has to be be updated with new states (coming from outside the scene knowledge) +// or when an item is destroyed +// These changes must be expressed through the corresponding command from the Transaction +// THe Transaction is then queued on the Scene so all the pending transactions can be consolidated and processed at the time +// of updating the scene before it s rendered. +// +class Transaction { public: - PendingChanges() {} - ~PendingChanges() {} + Transaction() {} + ~Transaction() {} void resetItem(ItemID id, const PayloadPointer& payload); void removeItem(ItemID id); @@ -34,7 +43,7 @@ public: void updateItem(ItemID id, const UpdateFunctorPointer& functor); void updateItem(ItemID id) { updateItem(id, nullptr); } - void merge(const PendingChanges& changes); + void merge(const Transaction& transaction); ItemIDs _resetItems; Payloads _resetPayloads; @@ -44,12 +53,12 @@ public: protected: }; -typedef std::queue PendingChangesQueue; +typedef std::queue TransactionQueue; // Scene is a container for Items -// Items are introduced, modified or erased in the scene through PendingChanges -// Once per Frame, the PendingChanges are all flushed +// Items are introduced, modified or erased in the scene through Transaction +// Once per Frame, the Transaction are all flushed // During the flush the standard buckets are updated // Items are notified accordingly on any update message happening class Scene { @@ -66,11 +75,11 @@ public: // THis is the total number of allocated items, this a threadsafe call size_t getNumItems() const { return _numAllocatedItems.load(); } - // Enqueue change batch to the scene - void enqueuePendingChanges(const PendingChanges& pendingChanges); + // Enqueue transaction to the scene + void enqueueTransaction(const Transaction& transaction); - // Process the penging changes equeued - void processPendingChangesQueue(); + // Process the pending transactions queued + void processTransactionQueue(); // This next call are NOT threadsafe, you have to call them from the correct thread to avoid any potential issues @@ -91,8 +100,8 @@ protected: // Thread safe elements that can be accessed from anywhere std::atomic _IDAllocator{ 1 }; // first valid itemID will be One std::atomic _numAllocatedItems{ 1 }; // num of allocated items, matching the _items.size() - std::mutex _changeQueueMutex; - PendingChangesQueue _changeQueue; + std::mutex _transactionQueueMutex; + TransactionQueue _transactionQueue; // The actual database // database of items is protected for editing by a mutex diff --git a/libraries/render/src/render/ShapePipeline.cpp b/libraries/render/src/render/ShapePipeline.cpp index 48e8ee43d5..1c8e73f5d7 100644 --- a/libraries/render/src/render/ShapePipeline.cpp +++ b/libraries/render/src/render/ShapePipeline.cpp @@ -39,6 +39,10 @@ void ShapePlumber::addPipelineHelper(const Filter& filter, ShapeKey key, int bit } } else { // Add the brand new pipeline and cache its location in the lib + auto precedent = _pipelineMap.find(key); + if (precedent != _pipelineMap.end()) { + qCDebug(renderlogging) << "Key already assigned: " << key; + } _pipelineMap.insert(PipelineMap::value_type(key, pipeline)); } } @@ -65,16 +69,11 @@ void ShapePlumber::addPipeline(const Filter& filter, const gpu::ShaderPointer& p slotBindings.insert(gpu::Shader::Binding(std::string("lightBuffer"), Slot::BUFFER::LIGHT)); slotBindings.insert(gpu::Shader::Binding(std::string("lightAmbientBuffer"), Slot::BUFFER::LIGHT_AMBIENT_BUFFER)); slotBindings.insert(gpu::Shader::Binding(std::string("skyboxMap"), Slot::MAP::LIGHT_AMBIENT)); - slotBindings.insert(gpu::Shader::Binding(std::string("normalFittingMap"), Slot::NORMAL_FITTING)); gpu::Shader::makeProgram(*program, slotBindings); auto locations = std::make_shared(); - locations->normalFittingMapUnit = program->getTextures().findLocation("normalFittingMap"); - if (program->getTextures().findLocation("normalFittingMap") > -1) { - locations->normalFittingMapUnit = program->getTextures().findLocation("normalFittingMap"); - } locations->albedoTextureUnit = program->getTextures().findLocation("albedoMap"); locations->roughnessTextureUnit = program->getTextures().findLocation("roughnessMap"); locations->normalTextureUnit = program->getTextures().findLocation("normalMap"); diff --git a/libraries/render/src/render/ShapePipeline.h b/libraries/render/src/render/ShapePipeline.h index e7a14d2f2b..73e8f82f24 100644 --- a/libraries/render/src/render/ShapePipeline.h +++ b/libraries/render/src/render/ShapePipeline.h @@ -22,13 +22,13 @@ namespace render { class ShapeKey { public: enum FlagBit { - TRANSLUCENT = 0, + MATERIAL = 0, + TRANSLUCENT, LIGHTMAP, TANGENTS, SPECULAR, UNLIT, SKINNED, - STEREO, DEPTH_ONLY, DEPTH_BIAS, WIREFRAME, @@ -46,6 +46,10 @@ public: ShapeKey() : _flags{ 0 } {} ShapeKey(const Flags& flags) : _flags{flags} {} + friend ShapeKey operator&(const ShapeKey& _Left, const ShapeKey& _Right) { return ShapeKey(_Left._flags & _Right._flags); } + friend ShapeKey operator|(const ShapeKey& _Left, const ShapeKey& _Right) { return ShapeKey(_Left._flags | _Right._flags); } + friend ShapeKey operator^(const ShapeKey& _Left, const ShapeKey& _Right) { return ShapeKey(_Left._flags ^ _Right._flags); } + class Builder { public: Builder() {} @@ -53,13 +57,13 @@ public: ShapeKey build() const { return ShapeKey{_flags}; } + Builder& withMaterial() { _flags.set(MATERIAL); return (*this); } Builder& withTranslucent() { _flags.set(TRANSLUCENT); return (*this); } Builder& withLightmap() { _flags.set(LIGHTMAP); return (*this); } Builder& withTangents() { _flags.set(TANGENTS); return (*this); } Builder& withSpecular() { _flags.set(SPECULAR); return (*this); } Builder& withUnlit() { _flags.set(UNLIT); return (*this); } Builder& withSkinned() { _flags.set(SKINNED); return (*this); } - Builder& withStereo() { _flags.set(STEREO); return (*this); } Builder& withDepthOnly() { _flags.set(DEPTH_ONLY); return (*this); } Builder& withDepthBias() { _flags.set(DEPTH_BIAS); return (*this); } Builder& withWireframe() { _flags.set(WIREFRAME); return (*this); } @@ -89,6 +93,9 @@ public: Filter build() const { return Filter(_flags, _mask); } + Builder& withMaterial() { _flags.set(MATERIAL); _mask.set(MATERIAL); return (*this); } + Builder& withoutMaterial() { _flags.reset(MATERIAL); _mask.set(MATERIAL); return (*this); } + Builder& withTranslucent() { _flags.set(TRANSLUCENT); _mask.set(TRANSLUCENT); return (*this); } Builder& withOpaque() { _flags.reset(TRANSLUCENT); _mask.set(TRANSLUCENT); return (*this); } @@ -107,9 +114,6 @@ public: Builder& withSkinned() { _flags.set(SKINNED); _mask.set(SKINNED); return (*this); } Builder& withoutSkinned() { _flags.reset(SKINNED); _mask.set(SKINNED); return (*this); } - Builder& withStereo() { _flags.set(STEREO); _mask.set(STEREO); return (*this); } - Builder& withoutStereo() { _flags.reset(STEREO); _mask.set(STEREO); return (*this); } - Builder& withDepthOnly() { _flags.set(DEPTH_ONLY); _mask.set(DEPTH_ONLY); return (*this); } Builder& withoutDepthOnly() { _flags.reset(DEPTH_ONLY); _mask.set(DEPTH_ONLY); return (*this); } @@ -128,22 +132,23 @@ public: Flags _mask{0}; }; Filter(const Filter::Builder& builder) : Filter(builder._flags, builder._mask) {} + ShapeKey key() const { return ShapeKey(_flags); } protected: friend class ShapePlumber; Flags _flags{0}; Flags _mask{0}; }; + bool useMaterial() const { return _flags[MATERIAL]; } bool hasLightmap() const { return _flags[LIGHTMAP]; } bool hasTangents() const { return _flags[TANGENTS]; } bool hasSpecular() const { return _flags[SPECULAR]; } bool isUnlit() const { return _flags[UNLIT]; } bool isTranslucent() const { return _flags[TRANSLUCENT]; } bool isSkinned() const { return _flags[SKINNED]; } - bool isStereo() const { return _flags[STEREO]; } bool isDepthOnly() const { return _flags[DEPTH_ONLY]; } bool isDepthBiased() const { return _flags[DEPTH_BIAS]; } - bool isWireFrame() const { return _flags[WIREFRAME]; } + bool isWireframe() const { return _flags[WIREFRAME]; } bool isCullFace() const { return !_flags[NO_CULL_FACE]; } bool hasOwnPipeline() const { return _flags[OWN_PIPELINE]; } @@ -170,16 +175,16 @@ inline QDebug operator<<(QDebug debug, const ShapeKey& key) { debug << "[ShapeKey: OWN_PIPELINE]"; } else { debug << "[ShapeKey:" + << "useMaterial:" << key.useMaterial() << "hasLightmap:" << key.hasLightmap() << "hasTangents:" << key.hasTangents() << "hasSpecular:" << key.hasSpecular() << "isUnlit:" << key.isUnlit() << "isTranslucent:" << key.isTranslucent() << "isSkinned:" << key.isSkinned() - << "isStereo:" << key.isStereo() << "isDepthOnly:" << key.isDepthOnly() << "isDepthBiased:" << key.isDepthBiased() - << "isWireFrame:" << key.isWireFrame() + << "isWireframe:" << key.isWireframe() << "isCullFace:" << key.isCullFace() << "]"; } @@ -213,8 +218,6 @@ public: OCCLUSION, SCATTERING, LIGHT_AMBIENT, - - NORMAL_FITTING = 10, }; }; @@ -226,7 +229,6 @@ public: int metallicTextureUnit; int emissiveTextureUnit; int occlusionTextureUnit; - int normalFittingMapUnit; int lightingModelBufferUnit; int skinClusterBufferUnit; int materialBufferUnit; diff --git a/libraries/render/src/render/drawItemStatus.slv b/libraries/render/src/render/drawItemStatus.slv index cb4ae7ebd2..792f2733c5 100644 --- a/libraries/render/src/render/drawItemStatus.slv +++ b/libraries/render/src/render/drawItemStatus.slv @@ -75,7 +75,7 @@ void main(void) { vec4(1.0, 1.0, 0.0, 1.0) ); - const vec2 ICON_PIXEL_SIZE = vec2(20, 20); + const vec2 ICON_PIXEL_SIZE = vec2(36, 36); const vec2 MARGIN_PIXEL_SIZE = vec2(2, 2); const vec2 ICON_GRID_SLOTS[MAX_NUM_ICONS] = vec2[MAX_NUM_ICONS](vec2(-1.5, 0.5), vec2(-0.5, 0.5), @@ -114,7 +114,7 @@ void main(void) { varColor = vec4(paintRainbow(abs(iconStatus.y)), 1.0); // Pass the texcoord and the z texcoord is representing the texture icon - varTexcoord = vec3((quadPos.xy + 1.0) * 0.5, iconStatus.z); + varTexcoord = vec3( (quadPos.x + 1.0) * 0.5, (quadPos.y + 1.0) * -0.5, iconStatus.z); // Also changes the size of the notification vec2 iconScale = ICON_PIXEL_SIZE; diff --git a/libraries/script-engine/src/AudioScriptingInterface.cpp b/libraries/script-engine/src/AudioScriptingInterface.cpp index fcc1f201f9..8452494d95 100644 --- a/libraries/script-engine/src/AudioScriptingInterface.cpp +++ b/libraries/script-engine/src/AudioScriptingInterface.cpp @@ -19,11 +19,6 @@ void registerAudioMetaTypes(QScriptEngine* engine) { qScriptRegisterMetaType(engine, soundSharedPointerToScriptValue, soundSharedPointerFromScriptValue); } -AudioScriptingInterface& AudioScriptingInterface::getInstance() { - static AudioScriptingInterface staticInstance; - return staticInstance; -} - AudioScriptingInterface::AudioScriptingInterface() : _localAudioInterface(NULL) { diff --git a/libraries/script-engine/src/AudioScriptingInterface.h b/libraries/script-engine/src/AudioScriptingInterface.h index 07a6b171f4..e97bc329c6 100644 --- a/libraries/script-engine/src/AudioScriptingInterface.h +++ b/libraries/script-engine/src/AudioScriptingInterface.h @@ -14,31 +14,37 @@ #include #include +#include #include class ScriptAudioInjector; -class AudioScriptingInterface : public QObject { +class AudioScriptingInterface : public QObject, public Dependency { Q_OBJECT -public: - static AudioScriptingInterface& getInstance(); + SINGLETON_DEPENDENCY +public: void setLocalAudioInterface(AbstractAudioInterface* audioInterface) { _localAudioInterface = audioInterface; } protected: + // this method is protected to stop C++ callers from calling, but invokable from script Q_INVOKABLE ScriptAudioInjector* playSound(SharedSoundPointer sound, const AudioInjectorOptions& injectorOptions = AudioInjectorOptions()); Q_INVOKABLE void setStereoInput(bool stereo); signals: - void mutedByMixer(); - void environmentMuted(); - void receivedFirstPacket(); - void disconnected(); + void mutedByMixer(); /// the client has been muted by the mixer + void environmentMuted(); /// the entire environment has been muted by the mixer + void receivedFirstPacket(); /// the client has received its first packet from the audio mixer + void disconnected(); /// the client has been disconnected from the audio mixer + void noiseGateOpened(); /// the noise gate has opened + void noiseGateClosed(); /// the noise gate has closed + void inputReceived(const QByteArray& inputSamples); /// a frame of mic input audio has been received and processed private: AudioScriptingInterface(); + AbstractAudioInterface* _localAudioInterface; }; diff --git a/libraries/script-engine/src/BatchLoader.cpp b/libraries/script-engine/src/BatchLoader.cpp index eeaffff5cb..0c65d5c6f0 100644 --- a/libraries/script-engine/src/BatchLoader.cpp +++ b/libraries/script-engine/src/BatchLoader.cpp @@ -66,7 +66,7 @@ void BatchLoader::start(int maxRetries) { qCDebug(scriptengine) << "Loaded: " << url; } else { _data.insert(url, QString()); - qCDebug(scriptengine) << "Could not load: " << url; + qCDebug(scriptengine) << "Could not load: " << url << status; } if (!_finished && _urls.size() == _data.size()) { diff --git a/libraries/script-engine/src/Mat4.cpp b/libraries/script-engine/src/Mat4.cpp index 52b9690321..6676d0cde1 100644 --- a/libraries/script-engine/src/Mat4.cpp +++ b/libraries/script-engine/src/Mat4.cpp @@ -54,7 +54,7 @@ glm::mat4 Mat4::inverse(const glm::mat4& m) const { return glm::inverse(m); } -glm::vec3 Mat4::getFront(const glm::mat4& m) const { +glm::vec3 Mat4::getForward(const glm::mat4& m) const { return glm::vec3(-m[0][2], -m[1][2], -m[2][2]); } diff --git a/libraries/script-engine/src/Mat4.h b/libraries/script-engine/src/Mat4.h index 8b2a8aa8c1..19bbbe178a 100644 --- a/libraries/script-engine/src/Mat4.h +++ b/libraries/script-engine/src/Mat4.h @@ -37,7 +37,9 @@ public slots: glm::mat4 inverse(const glm::mat4& m) const; - glm::vec3 getFront(const glm::mat4& m) const; + // redundant, calls getForward which better describes the returned vector as a direction + glm::vec3 getFront(const glm::mat4& m) const { return getForward(m); } + glm::vec3 getForward(const glm::mat4& m) const; glm::vec3 getRight(const glm::mat4& m) const; glm::vec3 getUp(const glm::mat4& m) const; diff --git a/libraries/script-engine/src/ModelScriptingInterface.cpp b/libraries/script-engine/src/ModelScriptingInterface.cpp new file mode 100644 index 0000000000..f56312568e --- /dev/null +++ b/libraries/script-engine/src/ModelScriptingInterface.cpp @@ -0,0 +1,193 @@ +// +// ModelScriptingInterface.cpp +// libraries/script-engine/src +// +// Created by Seth Alves on 2017-1-27. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include +#include +#include +#include +#include "ScriptEngine.h" +#include "ScriptEngineLogging.h" +#include "ModelScriptingInterface.h" +#include "OBJWriter.h" + +ModelScriptingInterface::ModelScriptingInterface(QObject* parent) : QObject(parent) { + _modelScriptEngine = qobject_cast(parent); + + qScriptRegisterSequenceMetaType>(_modelScriptEngine); + qScriptRegisterMetaType(_modelScriptEngine, meshFaceToScriptValue, meshFaceFromScriptValue); + qScriptRegisterMetaType(_modelScriptEngine, qVectorMeshFaceToScriptValue, qVectorMeshFaceFromScriptValue); +} + +QString ModelScriptingInterface::meshToOBJ(MeshProxyList in) { + QList meshes; + foreach (const MeshProxy* meshProxy, in) { + meshes.append(meshProxy->getMeshPointer()); + } + + return writeOBJToString(meshes); +} + +QScriptValue ModelScriptingInterface::appendMeshes(MeshProxyList in) { + // figure out the size of the resulting mesh + size_t totalVertexCount { 0 }; + size_t totalAttributeCount { 0 }; + size_t totalIndexCount { 0 }; + foreach (const MeshProxy* meshProxy, in) { + MeshPointer mesh = meshProxy->getMeshPointer(); + totalVertexCount += mesh->getNumVertices(); + + int attributeTypeNormal = gpu::Stream::InputSlot::NORMAL; // libraries/gpu/src/gpu/Stream.h + const gpu::BufferView& normalsBufferView = mesh->getAttributeBuffer(attributeTypeNormal); + gpu::BufferView::Index numNormals = (gpu::BufferView::Index)normalsBufferView.getNumElements(); + totalAttributeCount += numNormals; + + totalIndexCount += mesh->getNumIndices(); + } + + // alloc the resulting mesh + gpu::Resource::Size combinedVertexSize = totalVertexCount * sizeof(glm::vec3); + unsigned char* combinedVertexData = new unsigned char[combinedVertexSize]; + unsigned char* combinedVertexDataCursor = combinedVertexData; + + gpu::Resource::Size combinedNormalSize = totalAttributeCount * sizeof(glm::vec3); + unsigned char* combinedNormalData = new unsigned char[combinedNormalSize]; + unsigned char* combinedNormalDataCursor = combinedNormalData; + + gpu::Resource::Size combinedIndexSize = totalIndexCount * sizeof(uint32_t); + unsigned char* combinedIndexData = new unsigned char[combinedIndexSize]; + unsigned char* combinedIndexDataCursor = combinedIndexData; + + uint32_t indexStartOffset { 0 }; + + foreach (const MeshProxy* meshProxy, in) { + MeshPointer mesh = meshProxy->getMeshPointer(); + mesh->forEach( + [&](glm::vec3 position){ + memcpy(combinedVertexDataCursor, &position, sizeof(position)); + combinedVertexDataCursor += sizeof(position); + }, + [&](glm::vec3 normal){ + memcpy(combinedNormalDataCursor, &normal, sizeof(normal)); + combinedNormalDataCursor += sizeof(normal); + }, + [&](uint32_t index){ + index += indexStartOffset; + memcpy(combinedIndexDataCursor, &index, sizeof(index)); + combinedIndexDataCursor += sizeof(index); + }); + + gpu::BufferView::Index numVertices = (gpu::BufferView::Index)mesh->getNumVertices(); + indexStartOffset += numVertices; + } + + model::MeshPointer result(new model::Mesh()); + + gpu::Element vertexElement = gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::XYZ); + gpu::Buffer* combinedVertexBuffer = new gpu::Buffer(combinedVertexSize, combinedVertexData); + gpu::BufferPointer combinedVertexBufferPointer(combinedVertexBuffer); + gpu::BufferView combinedVertexBufferView(combinedVertexBufferPointer, vertexElement); + result->setVertexBuffer(combinedVertexBufferView); + + int attributeTypeNormal = gpu::Stream::InputSlot::NORMAL; // libraries/gpu/src/gpu/Stream.h + gpu::Element normalElement = gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::XYZ); + gpu::Buffer* combinedNormalsBuffer = new gpu::Buffer(combinedNormalSize, combinedNormalData); + gpu::BufferPointer combinedNormalsBufferPointer(combinedNormalsBuffer); + gpu::BufferView combinedNormalsBufferView(combinedNormalsBufferPointer, normalElement); + result->addAttribute(attributeTypeNormal, combinedNormalsBufferView); + + gpu::Element indexElement = gpu::Element(gpu::SCALAR, gpu::UINT32, gpu::RAW); + gpu::Buffer* combinedIndexesBuffer = new gpu::Buffer(combinedIndexSize, combinedIndexData); + gpu::BufferPointer combinedIndexesBufferPointer(combinedIndexesBuffer); + gpu::BufferView combinedIndexesBufferView(combinedIndexesBufferPointer, indexElement); + result->setIndexBuffer(combinedIndexesBufferView); + + std::vector parts; + parts.emplace_back(model::Mesh::Part((model::Index)0, // startIndex + (model::Index)result->getNumIndices(), // numIndices + (model::Index)0, // baseVertex + model::Mesh::TRIANGLES)); // topology + result->setPartBuffer(gpu::BufferView(new gpu::Buffer(parts.size() * sizeof(model::Mesh::Part), + (gpu::Byte*) parts.data()), gpu::Element::PART_DRAWCALL)); + + + MeshProxy* resultProxy = new MeshProxy(result); + return meshToScriptValue(_modelScriptEngine, resultProxy); +} + +QScriptValue ModelScriptingInterface::transformMesh(glm::mat4 transform, MeshProxy* meshProxy) { + if (!meshProxy) { + return QScriptValue(false); + } + MeshPointer mesh = meshProxy->getMeshPointer(); + if (!mesh) { + return QScriptValue(false); + } + + model::MeshPointer result = mesh->map([&](glm::vec3 position){ return glm::vec3(transform * glm::vec4(position, 1.0f)); }, + [&](glm::vec3 normal){ return glm::vec3(transform * glm::vec4(normal, 0.0f)); }, + [&](uint32_t index){ return index; }); + MeshProxy* resultProxy = new MeshProxy(result); + return meshToScriptValue(_modelScriptEngine, resultProxy); +} + +QScriptValue ModelScriptingInterface::newMesh(const QVector& vertices, + const QVector& normals, + const QVector& faces) { + model::MeshPointer mesh(new model::Mesh()); + + // vertices + auto vertexBuffer = std::make_shared(vertices.size() * sizeof(glm::vec3), (gpu::Byte*)vertices.data()); + auto vertexBufferPtr = gpu::BufferPointer(vertexBuffer); + gpu::BufferView vertexBufferView(vertexBufferPtr, 0, vertexBufferPtr->getSize(), + sizeof(glm::vec3), gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::XYZ)); + mesh->setVertexBuffer(vertexBufferView); + + if (vertices.size() == normals.size()) { + // normals + auto normalBuffer = std::make_shared(normals.size() * sizeof(glm::vec3), (gpu::Byte*)normals.data()); + auto normalBufferPtr = gpu::BufferPointer(normalBuffer); + gpu::BufferView normalBufferView(normalBufferPtr, 0, normalBufferPtr->getSize(), + sizeof(glm::vec3), gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::XYZ)); + mesh->addAttribute(gpu::Stream::NORMAL, normalBufferView); + } else { + qCDebug(scriptengine) << "ModelScriptingInterface::newMesh normals must be same length as vertices"; + } + + // indices (faces) + int VERTICES_PER_TRIANGLE = 3; + int indexBufferSize = faces.size() * sizeof(uint32_t) * VERTICES_PER_TRIANGLE; + unsigned char* indexData = new unsigned char[indexBufferSize]; + unsigned char* indexDataCursor = indexData; + foreach(const MeshFace& meshFace, faces) { + for (int i = 0; i < VERTICES_PER_TRIANGLE; i++) { + memcpy(indexDataCursor, &meshFace.vertexIndices[i], sizeof(uint32_t)); + indexDataCursor += sizeof(uint32_t); + } + } + auto indexBuffer = std::make_shared(indexBufferSize, (gpu::Byte*)indexData); + auto indexBufferPtr = gpu::BufferPointer(indexBuffer); + gpu::BufferView indexBufferView(indexBufferPtr, gpu::Element(gpu::SCALAR, gpu::UINT32, gpu::RAW)); + mesh->setIndexBuffer(indexBufferView); + + // parts + std::vector parts; + parts.emplace_back(model::Mesh::Part((model::Index)0, // startIndex + (model::Index)faces.size() * 3, // numIndices + (model::Index)0, // baseVertex + model::Mesh::TRIANGLES)); // topology + mesh->setPartBuffer(gpu::BufferView(new gpu::Buffer(parts.size() * sizeof(model::Mesh::Part), + (gpu::Byte*) parts.data()), gpu::Element::PART_DRAWCALL)); + + + + MeshProxy* meshProxy = new MeshProxy(mesh); + return meshToScriptValue(_modelScriptEngine, meshProxy); +} diff --git a/libraries/script-engine/src/ModelScriptingInterface.h b/libraries/script-engine/src/ModelScriptingInterface.h new file mode 100644 index 0000000000..d899f532d8 --- /dev/null +++ b/libraries/script-engine/src/ModelScriptingInterface.h @@ -0,0 +1,43 @@ +// +// ModelScriptingInterface.h +// libraries/script-engine/src +// +// Created by Seth Alves on 2017-1-27. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + + +#ifndef hifi_ModelScriptingInterface_h +#define hifi_ModelScriptingInterface_h + +#include +#include +#include +#include +#include +#include + +using MeshPointer = std::shared_ptr; +class ScriptEngine; + +class ModelScriptingInterface : public QObject { + Q_OBJECT + +public: + ModelScriptingInterface(QObject* parent); + + Q_INVOKABLE QString meshToOBJ(MeshProxyList in); + Q_INVOKABLE QScriptValue appendMeshes(MeshProxyList in); + Q_INVOKABLE QScriptValue transformMesh(glm::mat4 transform, MeshProxy* meshProxy); + Q_INVOKABLE QScriptValue newMesh(const QVector& vertices, + const QVector& normals, + const QVector& faces); + +private: + ScriptEngine* _modelScriptEngine { nullptr }; +}; + +#endif // hifi_ModelScriptingInterface_h diff --git a/libraries/script-engine/src/Quat.cpp b/libraries/script-engine/src/Quat.cpp index 6c2e7a349e..6d49ed27c1 100644 --- a/libraries/script-engine/src/Quat.cpp +++ b/libraries/script-engine/src/Quat.cpp @@ -68,7 +68,7 @@ glm::quat Quat::inverse(const glm::quat& q) { return glm::inverse(q); } -glm::vec3 Quat::getFront(const glm::quat& orientation) { +glm::vec3 Quat::getForward(const glm::quat& orientation) { return orientation * Vectors::FRONT; } diff --git a/libraries/script-engine/src/Quat.h b/libraries/script-engine/src/Quat.h index bb81f24586..8a88767a41 100644 --- a/libraries/script-engine/src/Quat.h +++ b/libraries/script-engine/src/Quat.h @@ -19,6 +19,16 @@ #include #include +/**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 class Quat : public QObject { Q_OBJECT @@ -35,7 +45,9 @@ public slots: glm::quat fromPitchYawRollDegrees(float pitch, float yaw, float roll); // degrees glm::quat fromPitchYawRollRadians(float pitch, float yaw, float roll); // radians glm::quat inverse(const glm::quat& q); - glm::vec3 getFront(const glm::quat& orientation); + // redundant, calls getForward which better describes the returned vector as a direction + glm::vec3 getFront(const glm::quat& orientation) { return getForward(orientation); } + glm::vec3 getForward(const glm::quat& orientation); glm::vec3 getRight(const glm::quat& orientation); glm::vec3 getUp(const glm::quat& orientation); glm::vec3 safeEulerAngles(const glm::quat& orientation); // degrees diff --git a/libraries/script-engine/src/SceneScriptingInterface.h b/libraries/script-engine/src/SceneScriptingInterface.h index e8ea2e0217..7bc22eb3e6 100644 --- a/libraries/script-engine/src/SceneScriptingInterface.h +++ b/libraries/script-engine/src/SceneScriptingInterface.h @@ -133,7 +133,8 @@ namespace SceneScripting { class SceneScriptingInterface : public QObject, public Dependency { Q_OBJECT SINGLETON_DEPENDENCY - + + public: Q_PROPERTY(bool shouldRenderAvatars READ shouldRenderAvatars WRITE setShouldRenderAvatars) Q_PROPERTY(bool shouldRenderEntities READ shouldRenderEntities WRITE setShouldRenderEntities) diff --git a/libraries/script-engine/src/ScriptCache.cpp b/libraries/script-engine/src/ScriptCache.cpp index 3bc780e28d..601ca6bc95 100644 --- a/libraries/script-engine/src/ScriptCache.cpp +++ b/libraries/script-engine/src/ScriptCache.cpp @@ -188,6 +188,8 @@ void ScriptCache::scriptContentAvailable(int maxRetries) { } } + } else { + qCWarning(scriptengine) << "Warning: scriptContentAvailable for inactive url: " << url; } } diff --git a/libraries/script-engine/src/ScriptCache.h b/libraries/script-engine/src/ScriptCache.h index 6cc318cc15..511d392409 100644 --- a/libraries/script-engine/src/ScriptCache.h +++ b/libraries/script-engine/src/ScriptCache.h @@ -43,6 +43,9 @@ class ScriptCache : public QObject, public Dependency { public: static const QString STATUS_INLINE; static const QString STATUS_CACHED; + static bool isSuccessStatus(const QString& status) { + return status == "Success" || status == STATUS_INLINE || status == STATUS_CACHED; + } void clearCache(); Q_INVOKABLE void clearATPScriptsFromCache(); diff --git a/libraries/script-engine/src/ScriptEngine.cpp b/libraries/script-engine/src/ScriptEngine.cpp index 83f2f5ccc0..caa42bea5e 100644 --- a/libraries/script-engine/src/ScriptEngine.cpp +++ b/libraries/script-engine/src/ScriptEngine.cpp @@ -19,6 +19,9 @@ #include #include +#include +#include + #include #include @@ -34,6 +37,7 @@ #include #include #include +#include #include #include #include @@ -49,6 +53,7 @@ #include "ArrayBufferViewClass.h" #include "BatchLoader.h" +#include "BaseScriptEngine.h" #include "DataViewClass.h" #include "EventTypes.h" #include "FileScriptingInterface.h" // unzip project @@ -63,14 +68,24 @@ #include "RecordingScriptingInterface.h" #include "ScriptEngines.h" #include "TabletScriptingInterface.h" +#include "ModelScriptingInterface.h" + #include #include "MIDIEvent.h" -const QString BaseScriptEngine::SCRIPT_EXCEPTION_FORMAT { "[UncaughtException] %1 in %2:%3" }; +const QString ScriptEngine::_SETTINGS_ENABLE_EXTENDED_EXCEPTIONS { + "com.highfidelity.experimental.enableExtendedJSExceptions" +}; + +static const int MAX_MODULE_ID_LENGTH { 4096 }; +static const int MAX_DEBUG_VALUE_LENGTH { 80 }; + static const QScriptEngine::QObjectWrapOptions DEFAULT_QOBJECT_WRAP_OPTIONS = QScriptEngine::ExcludeDeleteLater | QScriptEngine::ExcludeChildObjects; +static const QScriptValue::PropertyFlags READONLY_PROP_FLAGS { QScriptValue::ReadOnly | QScriptValue::Undeletable }; +static const QScriptValue::PropertyFlags READONLY_HIDDEN_PROP_FLAGS { READONLY_PROP_FLAGS | QScriptValue::SkipInEnumeration }; static const bool HIFI_AUTOREFRESH_FILE_SCRIPTS { true }; @@ -79,7 +94,7 @@ int functionSignatureMetaID = qRegisterMetaTypeargumentCount(); i++) { if (i > 0) { @@ -89,13 +104,9 @@ static QScriptValue debugPrint(QScriptContext* context, QScriptEngine* engine){ } qCDebug(scriptengineScript).noquote() << "script:print()<<" << message; // noquote() so that \n is treated as newline - message = message.replace("\\", "\\\\") - .replace("\n", "\\n") - .replace("\r", "\\r") - .replace("'", "\\'"); - - // FIXME - this approach neeeds revisiting. print() comes here, which ends up doing an evaluate? - engine->evaluate("Script.print('" + message + "')"); + // FIXME - this approach neeeds revisiting. print() comes here, which ends up calling Script.print? + engine->globalObject().property("Script").property("print") + .call(engine->nullValue(), QScriptValueList({ message })); return QScriptValue(); } @@ -139,52 +150,15 @@ QString encodeEntityIdIntoEntityUrl(const QString& url, const QString& entityID) return url + " [EntityID:" + entityID + "]"; } -QString BaseScriptEngine::lintScript(const QString& sourceCode, const QString& fileName, const int lineNumber) { - const auto syntaxCheck = checkSyntax(sourceCode); - if (syntaxCheck.state() != syntaxCheck.Valid) { - const auto error = syntaxCheck.errorMessage(); - const auto line = QString::number(syntaxCheck.errorLineNumber()); - const auto column = QString::number(syntaxCheck.errorColumnNumber()); - const auto message = QString("[SyntaxError] %1 in %2:%3(%4)").arg(error, fileName, line, column); - return message; - } - return QString(); -} - -QString BaseScriptEngine::formatUncaughtException(const QString& overrideFileName) { - QString message; - if (hasUncaughtException()) { - const auto error = uncaughtException(); - const auto backtrace = uncaughtExceptionBacktrace(); - const auto exception = error.toString(); - auto filename = overrideFileName; - if (filename.isEmpty()) { - QScriptContextInfo ctx { currentContext() }; - filename = ctx.fileName(); - } - const auto line = QString::number(uncaughtExceptionLineNumber()); - - message = QString(SCRIPT_EXCEPTION_FORMAT).arg(exception, overrideFileName, line); - if (!backtrace.empty()) { - static const auto lineSeparator = "\n "; - message += QString("\n[Backtrace]%1%2").arg(lineSeparator, backtrace.join(lineSeparator)); - } - } - return message; -} - -QString ScriptEngine::reportUncaughtException(const QString& overrideFileName) { - QString message; - if (!hasUncaughtException()) { - return message; - } - message = formatUncaughtException(overrideFileName.isEmpty() ? _fileNameString : overrideFileName); - scriptErrorMessage(qPrintable(message)); +QString ScriptEngine::logException(const QScriptValue& exception) { + auto message = formatException(exception, _enableExtendedJSExceptions.get()); + scriptErrorMessage(message); return message; } int ScriptEngine::processLevelMaxRetries { ScriptRequest::MAX_RETRIES }; ScriptEngine::ScriptEngine(Context context, const QString& scriptContents, const QString& fileNameString) : + BaseScriptEngine(), _context(context), _scriptContents(scriptContents), _timerFunctionMap(), @@ -194,16 +168,30 @@ ScriptEngine::ScriptEngine(Context context, const QString& scriptContents, const DependencyManager::get()->addScriptEngine(this); connect(this, &QScriptEngine::signalHandlerException, this, [this](const QScriptValue& exception) { - reportUncaughtException(); - clearExceptions(); - }); + if (hasUncaughtException()) { + // the engine's uncaughtException() seems to produce much better stack traces here + emit unhandledException(cloneUncaughtException("signalHandlerException")); + clearExceptions(); + } else { + // ... but may not always be available -- so if needed we fallback to the passed exception + emit unhandledException(exception); + } + }, Qt::DirectConnection); setProcessEventsInterval(MSECS_PER_SECOND); if (isEntityServerScript()) { qCDebug(scriptengine) << "isEntityServerScript() -- limiting maxRetries to 1"; processLevelMaxRetries = 1; } - qCDebug(scriptengine) << getContext() << "processLevelMaxRetries =" << processLevelMaxRetries; + + // this is where all unhandled exceptions end up getting logged + connect(this, &BaseScriptEngine::unhandledException, this, [this](const QScriptValue& err) { + auto output = err.engine() == this ? err : makeError(err); + if (!output.property("detail").isValid()) { + output.setProperty("detail", "UnhandledException"); + } + logException(output); + }); } QString ScriptEngine::getContext() const { @@ -223,13 +211,22 @@ QString ScriptEngine::getContext() const { } ScriptEngine::~ScriptEngine() { + // FIXME: are these scriptInfoMessage/scriptWarningMessage segfaulting anybody else at app shutdown? +#if !defined(Q_OS_LINUX) scriptInfoMessage("Script Engine shutting down:" + getFilename()); +#else + qCDebug(scriptengine) << "~ScriptEngine()" << this; +#endif auto scriptEngines = DependencyManager::get(); if (scriptEngines) { scriptEngines->removeScriptEngine(this); } else { +#if !defined(Q_OS_LINUX) scriptWarningMessage("Script destroyed after ScriptEngines!"); +#else + qCWarning(scriptengine) << ("Script destroyed after ScriptEngines!"); +#endif } } @@ -319,9 +316,12 @@ void ScriptEngine::runDebuggable() { } } _lastUpdate = now; - // Debug and clear exceptions - if (hasUncaughtException()) { - reportUncaughtException(); + + // only clear exceptions if we are not in the middle of evaluating + if (!isEvaluating() && hasUncaughtException()) { + qCWarning(scriptengine) << __FUNCTION__ << "---------- UNCAUGHT EXCEPTION --------"; + qCWarning(scriptengine) << "runDebuggable" << uncaughtException().toString(); + logException(__FUNCTION__); clearExceptions(); } }); @@ -343,7 +343,7 @@ void ScriptEngine::runInThread() { // The thread interface cannot live on itself, and we want to move this into the thread, so // the thread cannot have this as a parent. QThread* workerThread = new QThread(); - workerThread->setObjectName(QString("Script Thread:") + getFilename()); + workerThread->setObjectName(QString("js:") + getFilename().replace("about:","")); moveToThread(workerThread); // NOTE: If you connect any essential signals for proper shutdown or cleanup of @@ -356,10 +356,9 @@ void ScriptEngine::runInThread() { workerThread->start(); } -void ScriptEngine::executeOnScriptThread(std::function function, bool blocking ) { +void ScriptEngine::executeOnScriptThread(std::function function, const Qt::ConnectionType& type ) { if (QThread::currentThread() != thread()) { - QMetaObject::invokeMethod(this, "executeOnScriptThread", blocking ? Qt::BlockingQueuedConnection : Qt::QueuedConnection, - Q_ARG(std::function, function)); + QMetaObject::invokeMethod(this, "executeOnScriptThread", type, Q_ARG(std::function, function)); return; } @@ -464,18 +463,18 @@ void ScriptEngine::loadURL(const QUrl& scriptURL, bool reload) { } void ScriptEngine::scriptErrorMessage(const QString& message) { - qCCritical(scriptengine) << message; - emit errorMessage(message); + qCCritical(scriptengine) << qPrintable(message); + emit errorMessage(message, getFilename()); } void ScriptEngine::scriptWarningMessage(const QString& message) { qCWarning(scriptengine) << message; - emit warningMessage(message); + emit warningMessage(message, getFilename()); } void ScriptEngine::scriptInfoMessage(const QString& message) { qCInfo(scriptengine) << message; - emit infoMessage(message); + emit infoMessage(message, getFilename()); } // Even though we never pass AnimVariantMap directly to and from javascript, the queued invokeMethod of @@ -543,6 +542,40 @@ static QScriptValue createScriptableResourcePrototype(QScriptEngine* engine) { return prototype; } +void ScriptEngine::resetModuleCache(bool deleteScriptCache) { + if (QThread::currentThread() != thread()) { + executeOnScriptThread([=]() { resetModuleCache(deleteScriptCache); }); + return; + } + auto jsRequire = globalObject().property("Script").property("require"); + auto cache = jsRequire.property("cache"); + auto cacheMeta = jsRequire.data(); + + if (deleteScriptCache) { + QScriptValueIterator it(cache); + while (it.hasNext()) { + it.next(); + if (it.flags() & QScriptValue::SkipInEnumeration) { + continue; + } + qCDebug(scriptengine) << "resetModuleCache(true) -- staging " << it.name() << " for cache reset at next require"; + cacheMeta.setProperty(it.name(), true); + } + } + cache = newObject(); + if (!cacheMeta.isObject()) { + cacheMeta = newObject(); + cacheMeta.setProperty("id", "Script.require.cacheMeta"); + cacheMeta.setProperty("type", "cacheMeta"); + jsRequire.setData(cacheMeta); + } + cache.setProperty("__created__", (double)QDateTime::currentMSecsSinceEpoch(), QScriptValue::SkipInEnumeration); +#if DEBUG_JS_MODULES + cache.setProperty("__meta__", cacheMeta, READONLY_HIDDEN_PROP_FLAGS); +#endif + jsRequire.setProperty("cache", cache, READONLY_PROP_FLAGS); +} + void ScriptEngine::init() { if (_isInitialized) { return; // only initialize once @@ -552,15 +585,6 @@ void ScriptEngine::init() { auto entityScriptingInterface = DependencyManager::get(); entityScriptingInterface->init(); - connect(entityScriptingInterface.data(), &EntityScriptingInterface::deletingEntity, this, [this](const EntityItemID& entityID) { - if (_entityScripts.contains(entityID)) { - if (isEntityScriptRunning(entityID)) { - qCWarning(scriptengine) << "deletingEntity while entity script is still running!" << entityID; - } - _entityScripts.remove(entityID); - } - }); - // register various meta-types registerMetaTypes(this); @@ -590,8 +614,7 @@ void ScriptEngine::init() { QScriptValue webSocketConstructorValue = newFunction(WebSocketClass::constructor); globalObject().setProperty("WebSocket", webSocketConstructorValue); - QScriptValue printConstructorValue = newFunction(debugPrint); - globalObject().setProperty("print", printConstructorValue); + globalObject().setProperty("print", newFunction(debugPrint)); QScriptValue audioEffectOptionsConstructorValue = newFunction(AudioEffectOptions::constructor); globalObject().setProperty("AudioEffectOptions", audioEffectOptionsConstructorValue); @@ -604,8 +627,22 @@ void ScriptEngine::init() { qScriptRegisterMetaType(this, qWSCloseCodeToScriptValue, qWSCloseCodeFromScriptValue); qScriptRegisterMetaType(this, wscReadyStateToScriptValue, wscReadyStateFromScriptValue); + // NOTE: You do not want to end up creating new instances of singletons here. They will be on the ScriptEngine thread + // and are likely to be unusable if we "reset" the ScriptEngine by creating a new one (on a whole new thread). + registerGlobalObject("Script", this); - registerGlobalObject("Audio", &AudioScriptingInterface::getInstance()); + + { + // set up Script.require.resolve and Script.require.cache + auto Script = globalObject().property("Script"); + auto require = Script.property("require"); + auto resolve = Script.property("_requireResolve"); + require.setProperty("resolve", resolve, READONLY_PROP_FLAGS); + resetModuleCache(); + } + + registerGlobalObject("Audio", DependencyManager::get().data()); + registerGlobalObject("Entities", entityScriptingInterface.data()); registerGlobalObject("Quat", &_quatLibrary); registerGlobalObject("Vec3", &_vec3Library); @@ -614,7 +651,7 @@ void ScriptEngine::init() { registerGlobalObject("Messages", DependencyManager::get().data()); registerGlobalObject("File", new FileScriptingInterface(this)); - + qScriptRegisterMetaType(this, animVarMapToScriptValue, animVarMapFromScriptValue); qScriptRegisterMetaType(this, resultHandlerToScriptValue, resultHandlerFromScriptValue); @@ -630,6 +667,12 @@ void ScriptEngine::init() { registerGlobalObject("Tablet", DependencyManager::get().data()); registerGlobalObject("Assets", &_assetScriptingInterface); registerGlobalObject("Resources", DependencyManager::get().data()); + + registerGlobalObject("DebugDraw", &DebugDraw::getInstance()); + + registerGlobalObject("Model", new ModelScriptingInterface(this)); + qScriptRegisterMetaType(this, meshToScriptValue, meshFromScriptValue); + qScriptRegisterMetaType(this, meshesToScriptValue, meshesFromScriptValue); } void ScriptEngine::registerValue(const QString& valueName, QScriptValue value) { @@ -871,6 +914,10 @@ void ScriptEngine::addEventHandler(const EntityItemID& entityID, const QString& handlersForEvent << handlerData; // Note that the same handler can be added many times. See removeEntityEventHandler(). } +// this is not redundant -- the version in BaseScriptEngine is specifically not Q_INVOKABLE +QScriptValue ScriptEngine::evaluateInClosure(const QScriptValue& closure, const QScriptProgram& program) { + return BaseScriptEngine::evaluateInClosure(closure, program); +} QScriptValue ScriptEngine::evaluate(const QString& sourceCode, const QString& fileName, int lineNumber) { if (DependencyManager::get()->isStopped()) { @@ -893,23 +940,27 @@ QScriptValue ScriptEngine::evaluate(const QString& sourceCode, const QString& fi // Check syntax auto syntaxError = lintScript(sourceCode, fileName); + if (syntaxError.isError()) { + if (!isEvaluating()) { + syntaxError.setProperty("detail", "evaluate"); + } + raiseException(syntaxError); + maybeEmitUncaughtException("lint"); + return syntaxError; + } QScriptProgram program { sourceCode, fileName, lineNumber }; - if (!syntaxError.isEmpty() || program.isNull()) { - scriptErrorMessage(qPrintable(syntaxError)); - return QScriptValue(); + if (program.isNull()) { + // can this happen? + auto err = makeError("could not create QScriptProgram for " + fileName); + raiseException(err); + maybeEmitUncaughtException("compile"); + return err; } - ++_evaluatesPending; - auto result = BaseScriptEngine::evaluate(program); - --_evaluatesPending; - - if (hasUncaughtException()) { - result = uncaughtException(); - reportUncaughtException(program.fileName()); - emit evaluationFinished(result, true); - clearExceptions(); - } else { - emit evaluationFinished(result, false); + QScriptValue result; + { + result = BaseScriptEngine::evaluate(program); + maybeEmitUncaughtException("evaluate"); } return result; } @@ -923,6 +974,8 @@ void ScriptEngine::run() { return; // bail early - avoid setting state in init(), as evaluate() will bail too } + scriptInfoMessage("Script Engine starting:" + getFilename()); + if (!_isInitialized) { init(); } @@ -930,8 +983,10 @@ void ScriptEngine::run() { _isRunning = true; emit runningStateChanged(); - QScriptValue result = evaluate(_scriptContents, _fileNameString); - + { + evaluate(_scriptContents, _fileNameString); + maybeEmitUncaughtException(__FUNCTION__); + } #ifdef _WIN32 // VS13 does not sleep_until unless it uses the system_clock, see: // https://www.reddit.com/r/cpp_questions/comments/3o71ic/sleep_until_not_working_with_a_time_pointsteady/ @@ -1058,13 +1113,14 @@ void ScriptEngine::run() { } _lastUpdate = now; - // Debug and clear exceptions - if (hasUncaughtException()) { - reportUncaughtException(); + // only clear exceptions if we are not in the middle of evaluating + if (!isEvaluating() && hasUncaughtException()) { + qCWarning(scriptengine) << __FUNCTION__ << "---------- UNCAUGHT EXCEPTION --------"; + qCWarning(scriptengine) << "runInThread" << uncaughtException().toString(); + emit unhandledException(cloneUncaughtException(__FUNCTION__)); clearExceptions(); } } - scriptInfoMessage("Script Engine stopping:" + getFilename()); stopAllTimers(); // make sure all our timers are stopped if the script is ending @@ -1097,9 +1153,11 @@ void ScriptEngine::run() { // we want to only call it in our own run "shutdown" processing. void ScriptEngine::stopAllTimers() { QMutableHashIterator i(_timerFunctionMap); + int j {0}; while (i.hasNext()) { i.next(); QTimer* timer = i.key(); + qCDebug(scriptengine) << getFilename() << "stopAllTimers[" << j++ << "]"; stopTimer(timer); } } @@ -1194,11 +1252,11 @@ void ScriptEngine::timerFired() { auto postTimer = p_high_resolution_clock::now(); auto elapsed = (postTimer - preTimer); _totalTimerExecution += std::chrono::duration_cast(elapsed); - + } else { + qCWarning(scriptengine) << "timerFired -- invalid function" << timerData.function.toVariant().toString(); } } - QObject* ScriptEngine::setupTimerWithInterval(const QScriptValue& function, int intervalMS, bool isSingleShot) { // create the timer, add it to the map, and start it QTimer* newTimer = new QTimer(this); @@ -1215,7 +1273,7 @@ QObject* ScriptEngine::setupTimerWithInterval(const QScriptValue& function, int // make sure the timer stops when the script does connect(this, &ScriptEngine::scriptEnding, newTimer, &QTimer::stop); - CallbackData timerData = {function, currentEntityIdentifier, currentSandboxURL }; + CallbackData timerData = { function, currentEntityIdentifier, currentSandboxURL }; _timerFunctionMap.insert(newTimer, timerData); newTimer->start(intervalMS); @@ -1245,33 +1303,44 @@ void ScriptEngine::stopTimer(QTimer *timer) { timer->stop(); _timerFunctionMap.remove(timer); delete timer; + } else { + qCDebug(scriptengine) << "stopTimer -- not in _timerFunctionMap" << timer; } } QUrl ScriptEngine::resolvePath(const QString& include) const { QUrl url(include); - // first lets check to see if it's already a full URL - if (!url.scheme().isEmpty()) { + // first lets check to see if it's already a full URL -- or a Windows path like "c:/" + if (include.startsWith("/") || url.scheme().length() == 1) { + url = QUrl::fromLocalFile(include); + } + if (!url.isRelative()) { return expandScriptUrl(url); } - QScriptContextInfo contextInfo { currentContext()->parentContext() }; - - // we apparently weren't a fully qualified url, so, let's assume we're relative - // to the original URL of our script - QUrl parentURL = contextInfo.fileName(); - if (parentURL.isEmpty()) { - if (_parentURL.isEmpty()) { - parentURL = QUrl(_fileNameString); - } else { - parentURL = QUrl(_parentURL); - } + // to the first absolute URL in the JS scope chain + QUrl parentURL; + auto context = currentContext(); + do { + QScriptContextInfo contextInfo { context }; + parentURL = QUrl(contextInfo.fileName()); + context = context->parentContext(); + } while (parentURL.isRelative() && context); + + if (parentURL.isRelative()) { + // fallback to the "include" parent (if defined, this will already be absolute) + parentURL = QUrl(_parentURL); } - // if the parent URL's scheme is empty, then this is probably a local file... - if (parentURL.scheme().isEmpty()) { - parentURL = QUrl::fromLocalFile(_fileNameString); + if (parentURL.isRelative()) { + // fallback to the original script engine URL + parentURL = QUrl(_fileNameString); + + // if still relative and path-like, then this is probably a local file... + if (parentURL.isRelative() && url.path().contains("/")) { + parentURL = QUrl::fromLocalFile(_fileNameString); + } } // at this point we should have a legitimate fully qualified URL for our parent @@ -1284,7 +1353,354 @@ QUrl ScriptEngine::resourcesPath() const { } void ScriptEngine::print(const QString& message) { - emit printedMessage(message); + emit printedMessage(message, getFilename()); +} + +// Script.require.resolve -- like resolvePath, but performs more validation and throws exceptions on invalid module identifiers (for consistency with Node.js) +QString ScriptEngine::_requireResolve(const QString& moduleId, const QString& relativeTo) { + if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) { + return QString(); + } + QUrl defaultScriptsLoc = defaultScriptsLocation(); + QUrl url(moduleId); + + auto displayId = moduleId; + if (displayId.length() > MAX_DEBUG_VALUE_LENGTH) { + displayId = displayId.mid(0, MAX_DEBUG_VALUE_LENGTH) + "..."; + } + auto message = QString("Cannot find module '%1' (%2)").arg(displayId); + + auto throwResolveError = [&](const QScriptValue& error) -> QString { + raiseException(error); + maybeEmitUncaughtException("require.resolve"); + return QString(); + }; + + // de-fuzz the input a little by restricting to rational sizes + auto idLength = url.toString().length(); + if (idLength < 1 || idLength > MAX_MODULE_ID_LENGTH) { + auto details = QString("rejecting invalid module id size (%1 chars [1,%2])") + .arg(idLength).arg(MAX_MODULE_ID_LENGTH); + return throwResolveError(makeError(message.arg(details), "RangeError")); + } + + // this regex matches: absolute, dotted or path-like URLs + // (ie: the kind of stuff ScriptEngine::resolvePath already handles) + QRegularExpression qualified ("^\\w+:|^/|^[.]{1,2}(/|$)"); + + // this is for module.require (which is a bound version of require that's always relative to the module path) + if (!relativeTo.isEmpty()) { + url = QUrl(relativeTo).resolved(moduleId); + url = resolvePath(url.toString()); + } else if (qualified.match(moduleId).hasMatch()) { + url = resolvePath(moduleId); + } else { + // check if the moduleId refers to a "system" module + QString systemPath = defaultScriptsLoc.path(); + QString systemModulePath = QString("%1/modules/%2.js").arg(systemPath).arg(moduleId); + url = defaultScriptsLoc; + url.setPath(systemModulePath); + if (!QFileInfo(url.toLocalFile()).isFile()) { + if (!moduleId.contains("./")) { + // the user might be trying to refer to a relative file without anchoring it + // let's do them a favor and test for that case -- offering specific advice if detected + auto unanchoredUrl = resolvePath("./" + moduleId); + if (QFileInfo(unanchoredUrl.toLocalFile()).isFile()) { + auto msg = QString("relative module ids must be anchored; use './%1' instead") + .arg(moduleId); + return throwResolveError(makeError(message.arg(msg))); + } + } + return throwResolveError(makeError(message.arg("system module not found"))); + } + } + + if (url.isRelative()) { + return throwResolveError(makeError(message.arg("could not resolve module id"))); + } + + // if it looks like a local file, verify that it's an allowed path and really a file + if (url.isLocalFile()) { + QFileInfo file(url.toLocalFile()); + QUrl canonical = url; + if (file.exists()) { + canonical.setPath(file.canonicalFilePath()); + } + + bool disallowOutsideFiles = !defaultScriptsLocation().isParentOf(canonical) && !currentSandboxURL.isLocalFile(); + if (disallowOutsideFiles && !PathUtils::isDescendantOf(canonical, currentSandboxURL)) { + return throwResolveError(makeError(message.arg( + QString("path '%1' outside of origin script '%2' '%3'") + .arg(PathUtils::stripFilename(url)) + .arg(PathUtils::stripFilename(currentSandboxURL)) + .arg(canonical.toString()) + ))); + } + if (!file.exists()) { + return throwResolveError(makeError(message.arg("path does not exist: " + url.toLocalFile()))); + } + if (!file.isFile()) { + return throwResolveError(makeError(message.arg("path is not a file: " + url.toLocalFile()))); + } + } + + maybeEmitUncaughtException(__FUNCTION__); + return url.toString(); +} + +// retrieves the current parent module from the JS scope chain +QScriptValue ScriptEngine::currentModule() { + if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) { + return unboundNullValue(); + } + auto jsRequire = globalObject().property("Script").property("require"); + auto cache = jsRequire.property("cache"); + auto candidate = QScriptValue(); + for (auto c = currentContext(); c && !candidate.isObject(); c = c->parentContext()) { + QScriptContextInfo contextInfo { c }; + candidate = cache.property(contextInfo.fileName()); + } + if (!candidate.isObject()) { + return QScriptValue(); + } + return candidate; +} + +// replaces or adds "module" to "parent.children[]" array +// (for consistency with Node.js and userscript cache invalidation without "cache busters") +bool ScriptEngine::registerModuleWithParent(const QScriptValue& module, const QScriptValue& parent) { + auto children = parent.property("children"); + if (children.isArray()) { + auto key = module.property("id"); + auto length = children.property("length").toInt32(); + for (int i = 0; i < length; i++) { + if (children.property(i).property("id").strictlyEquals(key)) { + qCDebug(scriptengine_module) << key.toString() << " updating parent.children[" << i << "] = module"; + children.setProperty(i, module); + return true; + } + } + qCDebug(scriptengine_module) << key.toString() << " appending parent.children[" << length << "] = module"; + children.setProperty(length, module); + return true; + } else if (parent.isValid()) { + qCDebug(scriptengine_module) << "registerModuleWithParent -- unrecognized parent" << parent.toVariant().toString(); + } + return false; +} + +// creates a new JS "module" Object with default metadata properties +QScriptValue ScriptEngine::newModule(const QString& modulePath, const QScriptValue& parent) { + auto closure = newObject(); + auto exports = newObject(); + auto module = newObject(); + qCDebug(scriptengine_module) << "newModule" << modulePath << parent.property("filename").toString(); + + closure.setProperty("module", module, READONLY_PROP_FLAGS); + + // note: this becomes the "exports" free variable, so should not be set read only + closure.setProperty("exports", exports); + + // make the closure available to module instantiation + module.setProperty("__closure__", closure, READONLY_HIDDEN_PROP_FLAGS); + + // for consistency with Node.js Module + module.setProperty("id", modulePath, READONLY_PROP_FLAGS); + module.setProperty("filename", modulePath, READONLY_PROP_FLAGS); + module.setProperty("exports", exports); // not readonly + module.setProperty("loaded", false, READONLY_PROP_FLAGS); + module.setProperty("parent", parent, READONLY_PROP_FLAGS); + module.setProperty("children", newArray(), READONLY_PROP_FLAGS); + + // module.require is a bound version of require that always resolves relative to that module's path + auto boundRequire = QScriptEngine::evaluate("(function(id) { return Script.require(Script.require.resolve(id, this.filename)); })", "(boundRequire)"); + module.setProperty("require", boundRequire, READONLY_PROP_FLAGS); + + return module; +} + +// synchronously fetch a module's source code using BatchLoader +QVariantMap ScriptEngine::fetchModuleSource(const QString& modulePath, const bool forceDownload) { + using UrlMap = QMap; + auto scriptCache = DependencyManager::get(); + QVariantMap req; + qCDebug(scriptengine_module) << "require.fetchModuleSource: " << QUrl(modulePath).fileName() << QThread::currentThread(); + + auto onload = [=, &req](const UrlMap& data, const UrlMap& _status) { + auto url = modulePath; + auto status = _status[url]; + auto contents = data[url]; + qCDebug(scriptengine_module) << "require.fetchModuleSource.onload: " << QUrl(url).fileName() << status << QThread::currentThread(); + if (isStopping()) { + req["status"] = "Stopped"; + req["success"] = false; + } else { + req["url"] = url; + req["status"] = status; + req["success"] = ScriptCache::isSuccessStatus(status); + req["contents"] = contents; + } + }; + + if (forceDownload) { + qCDebug(scriptengine_module) << "require.requestScript -- clearing cache for" << modulePath; + scriptCache->deleteScript(modulePath); + } + BatchLoader* loader = new BatchLoader(QList({ modulePath })); + connect(loader, &BatchLoader::finished, this, onload); + connect(this, &QObject::destroyed, loader, &QObject::deleteLater); + // fail faster? (since require() blocks the engine thread while resolving dependencies) + const int MAX_RETRIES = 1; + + loader->start(MAX_RETRIES); + + if (!loader->isFinished()) { + QTimer monitor; + QEventLoop loop; + QObject::connect(loader, &BatchLoader::finished, this, [this, &monitor, &loop]{ + monitor.stop(); + loop.quit(); + }); + + // this helps detect the case where stop() is invoked during the download + // but not seen in time to abort processing in onload()... + connect(&monitor, &QTimer::timeout, this, [this, &loop, &loader]{ + if (isStopping()) { + loop.exit(-1); + } + }); + monitor.start(500); + loop.exec(); + } + loader->deleteLater(); + return req; +} + +// evaluate a pending module object using the fetched source code +QScriptValue ScriptEngine::instantiateModule(const QScriptValue& module, const QString& sourceCode) { + QScriptValue result; + auto modulePath = module.property("filename").toString(); + auto closure = module.property("__closure__"); + + qCDebug(scriptengine_module) << QString("require.instantiateModule: %1 / %2 bytes") + .arg(QUrl(modulePath).fileName()).arg(sourceCode.length()); + + if (module.property("content-type").toString() == "application/json") { + qCDebug(scriptengine_module) << "... parsing as JSON"; + closure.setProperty("__json", sourceCode); + result = evaluateInClosure(closure, { "module.exports = JSON.parse(__json)", modulePath }); + } else { + // scoped vars for consistency with Node.js + closure.setProperty("require", module.property("require")); + closure.setProperty("__filename", modulePath, READONLY_HIDDEN_PROP_FLAGS); + closure.setProperty("__dirname", QString(modulePath).replace(QRegExp("/[^/]*$"), ""), READONLY_HIDDEN_PROP_FLAGS); + result = evaluateInClosure(closure, { sourceCode, modulePath }); + } + maybeEmitUncaughtException(__FUNCTION__); + return result; +} + +// CommonJS/Node.js like require/module support +QScriptValue ScriptEngine::require(const QString& moduleId) { + qCDebug(scriptengine_module) << "ScriptEngine::require(" << moduleId.left(MAX_DEBUG_VALUE_LENGTH) << ")"; + if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) { + return unboundNullValue(); + } + + auto jsRequire = globalObject().property("Script").property("require"); + auto cacheMeta = jsRequire.data(); + auto cache = jsRequire.property("cache"); + auto parent = currentModule(); + + auto throwModuleError = [&](const QString& modulePath, const QScriptValue& error) { + cache.setProperty(modulePath, nullValue()); + if (!error.isNull()) { +#ifdef DEBUG_JS_MODULES + qCWarning(scriptengine_module) << "throwing module error:" << error.toString() << modulePath << error.property("stack").toString(); +#endif + raiseException(error); + } + maybeEmitUncaughtException("module"); + return unboundNullValue(); + }; + + // start by resolving the moduleId into a fully-qualified path/URL + QString modulePath = _requireResolve(moduleId); + if (modulePath.isNull() || hasUncaughtException()) { + // the resolver already threw an exception -- bail early + maybeEmitUncaughtException(__FUNCTION__); + return unboundNullValue(); + } + + // check the resolved path against the cache + auto module = cache.property(modulePath); + + // modules get cached in `Script.require.cache` and (similar to Node.js) users can access it + // to inspect particular entries and invalidate them by deleting the key: + // `delete Script.require.cache[Script.require.resolve(moduleId)];` + + // cacheMeta is just used right now to tell deleted keys apart from undefined ones + bool invalidateCache = module.isUndefined() && cacheMeta.property(moduleId).isValid(); + + // reset the cacheMeta record so invalidation won't apply next time, even if the module fails to load + cacheMeta.setProperty(modulePath, QScriptValue()); + + auto exports = module.property("exports"); + if (!invalidateCache && exports.isObject()) { + // we have found a cached module -- just need to possibly register it with current parent + qCDebug(scriptengine_module) << QString("require - using cached module '%1' for '%2' (loaded: %3)") + .arg(modulePath).arg(moduleId).arg(module.property("loaded").toString()); + registerModuleWithParent(module, parent); + maybeEmitUncaughtException("cached module"); + return exports; + } + + // bootstrap / register new empty module + module = newModule(modulePath, parent); + registerModuleWithParent(module, parent); + + // add it to the cache (this is done early so any cyclic dependencies pick up) + cache.setProperty(modulePath, module); + + // download the module source + auto req = fetchModuleSource(modulePath, invalidateCache); + + if (!req.contains("success") || !req["success"].toBool()) { + auto error = QString("error retrieving script (%1)").arg(req["status"].toString()); + return throwModuleError(modulePath, error); + } + +#if DEBUG_JS_MODULES + qCDebug(scriptengine_module) << "require.loaded: " << + QUrl(req["url"].toString()).fileName() << req["status"].toString(); +#endif + + auto sourceCode = req["contents"].toString(); + + if (QUrl(modulePath).fileName().endsWith(".json", Qt::CaseInsensitive)) { + module.setProperty("content-type", "application/json"); + } else { + module.setProperty("content-type", "application/javascript"); + } + + // evaluate the module + auto result = instantiateModule(module, sourceCode); + + if (result.isError() && !result.strictlyEquals(module.property("exports"))) { + qCWarning(scriptengine_module) << "-- result.isError --" << result.toString(); + return throwModuleError(modulePath, result); + } + + // mark as fully-loaded + module.setProperty("loaded", true, READONLY_PROP_FLAGS); + + // set up a new reference point for detecting cache key deletion + cacheMeta.setProperty(modulePath, module); + + qCDebug(scriptengine_module) << "//ScriptEngine::require(" << moduleId << ")"; + + maybeEmitUncaughtException(__FUNCTION__); + return module.property("exports"); } // If a callback is specified, the included files will be loaded asynchronously and the callback will be called @@ -1292,28 +1708,16 @@ void ScriptEngine::print(const QString& message) { // If no callback is specified, the included files will be loaded synchronously and will block execution until // all of the files have finished loading. void ScriptEngine::include(const QStringList& includeFiles, QScriptValue callback) { + if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) { + return; + } if (DependencyManager::get()->isStopped()) { scriptWarningMessage("Script.include() while shutting down is ignored... includeFiles:" + includeFiles.join(",") + "parent script:" + getFilename()); return; // bail early } QList urls; - bool knowsSensitivity = false; - Qt::CaseSensitivity sensitivity { Qt::CaseSensitive }; - auto getSensitivity = [&]() { - if (!knowsSensitivity) { - QString path = currentSandboxURL.path(); - QFileInfo upperFI(path.toUpper()); - QFileInfo lowerFI(path.toLower()); - sensitivity = (upperFI == lowerFI) ? Qt::CaseInsensitive : Qt::CaseSensitive; - knowsSensitivity = true; - } - return sensitivity; - }; - // Guard against meaningless query and fragment parts. - // Do NOT use PreferLocalFile as its behavior is unpredictable (e.g., on defaultScriptsLocation()) - const auto strippingFlags = QUrl::RemoveFilename | QUrl::RemoveQuery | QUrl::RemoveFragment; for (QString includeFile : includeFiles) { QString file = ResourceManager::normalizeURL(includeFile); QUrl thisURL; @@ -1330,10 +1734,8 @@ void ScriptEngine::include(const QStringList& includeFiles, QScriptValue callbac thisURL = resolvePath(file); } - if (!isStandardLibrary && !currentSandboxURL.isEmpty() && (thisURL.scheme() == "file") && - (currentSandboxURL.scheme() != "file" || - !thisURL.toString(strippingFlags).startsWith(currentSandboxURL.toString(strippingFlags), getSensitivity()))) { - + bool disallowOutsideFiles = thisURL.isLocalFile() && !isStandardLibrary && !currentSandboxURL.isLocalFile(); + if (disallowOutsideFiles && !PathUtils::isDescendantOf(thisURL, currentSandboxURL)) { scriptWarningMessage("Script.include() ignoring file path" + thisURL.toString() + "outside of original entity script" + currentSandboxURL.toString()); } else { @@ -1370,6 +1772,10 @@ void ScriptEngine::include(const QStringList& includeFiles, QScriptValue callbac }; doWithEnvironment(capturedEntityIdentifier, capturedSandboxURL, operation); + if (hasUncaughtException()) { + emit unhandledException(cloneUncaughtException("evaluateInclude")); + clearExceptions(); + } } else { scriptWarningMessage("Script.include() skipping evaluation of previously included url:" + url.toString()); } @@ -1414,6 +1820,9 @@ void ScriptEngine::include(const QString& includeFile, QScriptValue callback) { // as a stand-alone script. To accomplish this, the ScriptEngine class just emits a signal which // the Application or other context will connect to in order to know to actually load the script void ScriptEngine::load(const QString& loadFile) { + if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) { + return; + } if (DependencyManager::get()->isStopped()) { scriptWarningMessage("Script.load() while shutting down is ignored... loadFile:" + loadFile + "parent script:" + getFilename()); @@ -1471,21 +1880,6 @@ int ScriptEngine::getNumRunningEntityScripts() const { return sum; } -QString ScriptEngine::getEntityScriptStatus(const EntityItemID& entityID) { - if (_entityScripts.contains(entityID)) - return EntityScriptStatus_::valueToKey(_entityScripts[entityID].status).toLower(); - return QString(); -} - -bool ScriptEngine::getEntityScriptDetails(const EntityItemID& entityID, EntityScriptDetails &details) const { - auto it = _entityScripts.constFind(entityID); - if (it == _entityScripts.constEnd()) { - return false; - } - details = it.value(); - return true; -} - void ScriptEngine::setEntityScriptDetails(const EntityItemID& entityID, const EntityScriptDetails& details) { _entityScripts[entityID] = details; emit entityScriptDetailsUpdated(); @@ -1498,31 +1892,221 @@ void ScriptEngine::updateEntityScriptStatus(const EntityItemID& entityID, const emit entityScriptDetailsUpdated(); } -// since all of these operations can be asynch we will always do the actual work in the response handler -// for the download -void ScriptEngine::loadEntityScript(QWeakPointer theEngine, const EntityItemID& entityID, const QString& entityScript, bool forceRedownload) { - auto engine = theEngine.data(); - engine->executeOnScriptThread([=]{ - EntityScriptDetails details = engine->_entityScripts[entityID]; - if (details.status == EntityScriptStatus::PENDING || details.status == EntityScriptStatus::UNLOADED) { - engine->updateEntityScriptStatus(entityID, EntityScriptStatus::LOADING, QThread::currentThread()->objectName()); - } - }); - - // NOTE: If the script content is not currently in the cache, the LAMBDA here will be called on the Main Thread - // which means we're guaranteed that it's not the correct thread for the ScriptEngine. This means - // when we get into entityScriptContentAvailable() we will likely invokeMethod() to get it over - // to the "Entities" ScriptEngine thread. - DependencyManager::get()->getScriptContents(entityScript, [theEngine, entityID](const QString& scriptOrURL, const QString& contents, bool isURL, bool success, const QString &status) { - QSharedPointer strongEngine = theEngine.toStrongRef(); - if (strongEngine) { -#ifdef THREAD_DEBUGGING - qCDebug(scriptengine) << "ScriptEngine::entityScriptContentAvailable() IN LAMBDA contentAvailable on thread [" - << QThread::currentThread() << "] expected thread [" << strongEngine->thread() << "]"; +QVariant ScriptEngine::cloneEntityScriptDetails(const EntityItemID& entityID) { + static const QVariant NULL_VARIANT { qVariantFromValue((QObject*)nullptr) }; + QVariantMap map; + if (entityID.isNull()) { + // TODO: find better way to report JS Error across thread/process boundaries + map["isError"] = true; + map["errorInfo"] = "Error: getEntityScriptDetails -- invalid entityID"; + } else { +#ifdef DEBUG_ENTITY_STATES + qDebug() << "cloneEntityScriptDetails" << entityID << QThread::currentThread(); #endif - strongEngine->entityScriptContentAvailable(entityID, scriptOrURL, contents, isURL, success, status); + EntityScriptDetails scriptDetails; + if (getEntityScriptDetails(entityID, scriptDetails)) { +#ifdef DEBUG_ENTITY_STATES + qDebug() << "gotEntityScriptDetails" << scriptDetails.status << QThread::currentThread(); +#endif + map["isRunning"] = isEntityScriptRunning(entityID); + map["status"] = EntityScriptStatus_::valueToKey(scriptDetails.status).toLower(); + map["errorInfo"] = scriptDetails.errorInfo; + map["entityID"] = entityID.toString(); +#ifdef DEBUG_ENTITY_STATES + { + auto debug = QVariantMap(); + debug["script"] = scriptDetails.scriptText; + debug["scriptObject"] = scriptDetails.scriptObject.toVariant(); + debug["lastModified"] = (qlonglong)scriptDetails.lastModified; + debug["sandboxURL"] = scriptDetails.definingSandboxURL; + map["debug"] = debug; + } +#endif + } else { +#ifdef DEBUG_ENTITY_STATES + qDebug() << "!gotEntityScriptDetails" << QThread::currentThread(); +#endif + map["isError"] = true; + map["errorInfo"] = "Entity script details unavailable"; + map["entityID"] = entityID.toString(); } - }, forceRedownload, processLevelMaxRetries); + } + return map; +} + +QFuture ScriptEngine::getLocalEntityScriptDetails(const EntityItemID& entityID) { + return QtConcurrent::run(this, &ScriptEngine::cloneEntityScriptDetails, entityID); +} + +bool ScriptEngine::getEntityScriptDetails(const EntityItemID& entityID, EntityScriptDetails &details) const { + auto it = _entityScripts.constFind(entityID); + if (it == _entityScripts.constEnd()) { + return false; + } + details = it.value(); + return true; +} + +const static EntityItemID BAD_SCRIPT_UUID_PLACEHOLDER { "{20170224-dead-face-0000-cee000021114}" }; + +void ScriptEngine::processDeferredEntityLoads(const QString& entityScript, const EntityItemID& leaderID) { + QList retryLoads; + QMutableListIterator i(_deferredEntityLoads); + while (i.hasNext()) { + auto retry = i.next(); + if (retry.entityScript == entityScript) { + retryLoads << retry; + i.remove(); + } + } + foreach(DeferredLoadEntity retry, retryLoads) { + // check whether entity was since been deleted + if (!_entityScripts.contains(retry.entityID)) { + qCDebug(scriptengine) << "processDeferredEntityLoads -- entity details gone (entity deleted?)" + << retry.entityID; + continue; + } + + // check whether entity has since been unloaded or otherwise errored-out + auto details = _entityScripts[retry.entityID]; + if (details.status != EntityScriptStatus::PENDING) { + qCDebug(scriptengine) << "processDeferredEntityLoads -- entity status no longer PENDING; " + << retry.entityID << details.status; + continue; + } + + // propagate leader's failure reasons to the pending entity + const auto leaderDetails = _entityScripts[leaderID]; + if (leaderDetails.status != EntityScriptStatus::RUNNING) { + qCDebug(scriptengine) << QString("... pending load of %1 cancelled (leader: %2 status: %3)") + .arg(retry.entityID.toString()).arg(leaderID.toString()).arg(leaderDetails.status); + + auto extraDetail = QString("\n(propagated from %1)").arg(leaderID.toString()); + if (leaderDetails.status == EntityScriptStatus::ERROR_LOADING_SCRIPT || + leaderDetails.status == EntityScriptStatus::ERROR_RUNNING_SCRIPT) { + // propagate same error so User doesn't have to hunt down stampede's leader + updateEntityScriptStatus(retry.entityID, leaderDetails.status, leaderDetails.errorInfo + extraDetail); + } else { + // the leader Entity somehow ended up in some other state (rapid-fire delete or unload could cause) + updateEntityScriptStatus(retry.entityID, EntityScriptStatus::ERROR_LOADING_SCRIPT, + "A previous Entity failed to load using this script URL; reload to try again." + extraDetail); + } + continue; + } + + if (_occupiedScriptURLs.contains(retry.entityScript)) { + qCWarning(scriptengine) << "--- SHOULD NOT HAPPEN -- recursive call into processDeferredEntityLoads" << retry.entityScript; + continue; + } + + // if we made it here then the leading entity was successful so proceed with normal load + loadEntityScript(retry.entityID, retry.entityScript, false); + } +} + +void ScriptEngine::loadEntityScript(const EntityItemID& entityID, const QString& entityScript, bool forceRedownload) { + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(this, "loadEntityScript", + Q_ARG(const EntityItemID&, entityID), + Q_ARG(const QString&, entityScript), + Q_ARG(bool, forceRedownload) + ); + return; + } + PROFILE_RANGE(script, __FUNCTION__); + + if (isStopping() || DependencyManager::get()->isStopped()) { + qCDebug(scriptengine) << "loadEntityScript.start " << entityScript << entityID.toString() + << " but isStopping==" << isStopping() + << " || engines->isStopped==" << DependencyManager::get()->isStopped(); + return; + } + + if (!_entityScripts.contains(entityID)) { + // make sure EntityScriptDetails has an entry for this UUID right away + // (which allows bailing from the loading/provisioning process early if the Entity gets deleted mid-flight) + updateEntityScriptStatus(entityID, EntityScriptStatus::PENDING, "...pending..."); + } + + // This "occupied" approach allows multiple Entities to boot from the same script URL while still taking + // full advantage of cacheable require modules. This only affects Entities literally coming in back-to-back + // before the first one has time to finish loading. + if (_occupiedScriptURLs.contains(entityScript)) { + auto currentEntityID = _occupiedScriptURLs[entityScript]; + if (currentEntityID == BAD_SCRIPT_UUID_PLACEHOLDER) { + if (forceRedownload) { + // script was previously marked unusable, but we're reloading so reset it + _occupiedScriptURLs.remove(entityScript); + } else { + // since not reloading, assume that the exact same input would produce the exact same output again + // note: this state gets reset with "reload all scripts," leaving/returning to a Domain, clear cache, etc. +#ifdef DEBUG_ENTITY_STATES + qCDebug(scriptengine) << QString("loadEntityScript.cancelled entity: %1 script: %2 (previous script failure)") + .arg(entityID.toString()).arg(entityScript); +#endif + updateEntityScriptStatus(entityID, EntityScriptStatus::ERROR_LOADING_SCRIPT, + "A previous Entity failed to load using this script URL; reload to try again."); + return; + } + } else { + // another entity is busy loading from this script URL so wait for them to finish +#ifdef DEBUG_ENTITY_STATES + qCDebug(scriptengine) << QString("loadEntityScript.deferring[%0] entity: %1 script: %2 (waiting on %3)") + .arg(_deferredEntityLoads.size()).arg(entityID.toString()).arg(entityScript).arg(currentEntityID.toString()); +#endif + _deferredEntityLoads.push_back({ entityID, entityScript }); + return; + } + } + + // the scriptURL slot is available; flag as in-use + _occupiedScriptURLs[entityScript] = entityID; + +#ifdef DEBUG_ENTITY_STATES + auto previousStatus = _entityScripts.contains(entityID) ? _entityScripts[entityID].status : EntityScriptStatus::PENDING; + qCDebug(scriptengine) << "loadEntityScript.LOADING: " << entityScript << entityID.toString() + << "(previous: " << previousStatus << ")"; +#endif + + EntityScriptDetails newDetails; + newDetails.scriptText = entityScript; + newDetails.status = EntityScriptStatus::LOADING; + newDetails.definingSandboxURL = currentSandboxURL; + setEntityScriptDetails(entityID, newDetails); + + auto scriptCache = DependencyManager::get(); + // note: see EntityTreeRenderer.cpp for shared pointer lifecycle management + QWeakPointer weakRef(sharedFromThis()); + scriptCache->getScriptContents(entityScript, + [this, weakRef, entityScript, entityID](const QString& url, const QString& contents, bool isURL, bool success, const QString& status) { + QSharedPointer strongRef(weakRef); + if (!strongRef) { + qCWarning(scriptengine) << "loadEntityScript.contentAvailable -- ScriptEngine was deleted during getScriptContents!!"; + return; + } + if (isStopping()) { +#ifdef DEBUG_ENTITY_STATES + qCDebug(scriptengine) << "loadEntityScript.contentAvailable -- stopping"; +#endif + return; + } + executeOnScriptThread([=]{ +#ifdef DEBUG_ENTITY_STATES + qCDebug(scriptengine) << "loadEntityScript.contentAvailable" << status << QUrl(url).fileName() << entityID.toString(); +#endif + if (!isStopping() && _entityScripts.contains(entityID)) { + entityScriptContentAvailable(entityID, url, contents, isURL, success, status); + } else { +#ifdef DEBUG_ENTITY_STATES + qCDebug(scriptengine) << "loadEntityScript.contentAvailable -- aborting"; +#endif + } + // recheck whether us since may have been set to BAD_SCRIPT_UUID_PLACEHOLDER in entityScriptContentAvailable + if (_occupiedScriptURLs.contains(entityScript) && _occupiedScriptURLs[entityScript] == entityID) { + _occupiedScriptURLs.remove(entityScript); + } + }); + }, forceRedownload); } // since all of these operations can be asynch we will always do the actual work in the response handler @@ -1552,25 +2136,51 @@ void ScriptEngine::entityScriptContentAvailable(const EntityItemID& entityID, co auto scriptCache = DependencyManager::get(); bool isFileUrl = isURL && scriptOrURL.startsWith("file://"); - auto fileName = isURL ? scriptOrURL : "EmbeddedEntityScript"; + auto fileName = isURL ? scriptOrURL : "about:EmbeddedEntityScript"; + + const EntityScriptDetails &oldDetails = _entityScripts[entityID]; + const QString entityScript = oldDetails.scriptText; EntityScriptDetails newDetails; newDetails.scriptText = scriptOrURL; - if (!success) { - newDetails.status = EntityScriptStatus::ERROR_LOADING_SCRIPT; - newDetails.errorInfo = "Failed to load script (" + status + ")"; + // If an error happens below, we want to update newDetails with the new status info + // and also abort any pending Entity loads that are waiting on the exact same script URL. + auto setError = [&](const QString &errorInfo, const EntityScriptStatus& status) { + newDetails.errorInfo = errorInfo; + newDetails.status = status; setEntityScriptDetails(entityID, newDetails); + +#ifdef DEBUG_ENTITY_STATES + qCDebug(scriptengine) << "entityScriptContentAvailable -- flagging " << entityScript << " as BAD_SCRIPT_UUID_PLACEHOLDER"; +#endif + // flag the original entityScript as unusuable + _occupiedScriptURLs[entityScript] = BAD_SCRIPT_UUID_PLACEHOLDER; + processDeferredEntityLoads(entityScript, entityID); + }; + + // NETWORK / FILESYSTEM ERRORS + if (!success) { + setError("Failed to load script (" + status + ")", EntityScriptStatus::ERROR_LOADING_SCRIPT); return; } + // SYNTAX ERRORS auto syntaxError = lintScript(contents, fileName); + if (syntaxError.isError()) { + auto message = syntaxError.property("formatted").toString(); + if (message.isEmpty()) { + message = syntaxError.toString(); + } + setError(QString("Bad syntax (%1)").arg(message), EntityScriptStatus::ERROR_RUNNING_SCRIPT); + syntaxError.setProperty("detail", entityID.toString()); + emit unhandledException(syntaxError); + return; + } QScriptProgram program { contents, fileName }; - if (!syntaxError.isNull() || program.isNull()) { - newDetails.status = EntityScriptStatus::ERROR_RUNNING_SCRIPT; - newDetails.errorInfo = QString("Bad syntax (%1)").arg(syntaxError); - setEntityScriptDetails(entityID, newDetails); - qCDebug(scriptengine) << newDetails.errorInfo << scriptOrURL; + if (program.isNull()) { + setError("Bad program (isNull)", EntityScriptStatus::ERROR_RUNNING_SCRIPT); + emit unhandledException(makeError("program.isNull")); return; // done processing script } @@ -1578,71 +2188,94 @@ void ScriptEngine::entityScriptContentAvailable(const EntityItemID& entityID, co setParentURL(scriptOrURL); } + // SANITY/PERFORMANCE CHECK USING SANDBOX const int SANDBOX_TIMEOUT = 0.25 * MSECS_PER_SECOND; BaseScriptEngine sandbox; sandbox.setProcessEventsInterval(SANDBOX_TIMEOUT); - QScriptValue testConstructor; + QScriptValue testConstructor, exception; { QTimer timeout; timeout.setSingleShot(true); timeout.start(SANDBOX_TIMEOUT); connect(&timeout, &QTimer::timeout, [&sandbox, SANDBOX_TIMEOUT, scriptOrURL]{ - auto context = sandbox.currentContext(); - if (context) { qCDebug(scriptengine) << "ScriptEngine::entityScriptContentAvailable timeout(" << scriptOrURL << ")"; // Guard against infinite loops and non-performant code - context->throwError(QString("Timed out (entity constructors are limited to %1ms)").arg(SANDBOX_TIMEOUT)); - } + sandbox.raiseException( + sandbox.makeError(QString("Timed out (entity constructors are limited to %1ms)").arg(SANDBOX_TIMEOUT)) + ); }); + testConstructor = sandbox.evaluate(program); + + if (sandbox.hasUncaughtException()) { + exception = sandbox.cloneUncaughtException(QString("(preflight %1)").arg(entityID.toString())); + sandbox.clearExceptions(); + } else if (testConstructor.isError()) { + exception = testConstructor; + } } - QString exceptionMessage = sandbox.formatUncaughtException(program.fileName()); - if (!exceptionMessage.isNull()) { - newDetails.status = EntityScriptStatus::ERROR_RUNNING_SCRIPT; - newDetails.errorInfo = exceptionMessage; - setEntityScriptDetails(entityID, newDetails); - qCDebug(scriptengine) << "----- ScriptEngine::entityScriptContentAvailable -- hadUncaughtExceptions (" << scriptOrURL << ")"; + if (exception.isError()) { + // create a local copy using makeError to decouple from the sandbox engine + exception = makeError(exception); + setError(formatException(exception, _enableExtendedJSExceptions.get()), EntityScriptStatus::ERROR_RUNNING_SCRIPT); + emit unhandledException(exception); return; } + // CONSTRUCTOR VIABILITY if (!testConstructor.isFunction()) { QString testConstructorType = QString(testConstructor.toVariant().typeName()); if (testConstructorType == "") { testConstructorType = "empty"; } QString testConstructorValue = testConstructor.toString(); - const int maxTestConstructorValueSize = 80; - if (testConstructorValue.size() > maxTestConstructorValueSize) { - testConstructorValue = testConstructorValue.mid(0, maxTestConstructorValueSize) + "..."; + if (testConstructorValue.size() > MAX_DEBUG_VALUE_LENGTH) { + testConstructorValue = testConstructorValue.mid(0, MAX_DEBUG_VALUE_LENGTH) + "..."; } - scriptErrorMessage("Error -- ScriptEngine::loadEntityScript() entity:" + entityID.toString() - + "failed to load entity script -- expected a function, got " + testConstructorType - + "," + testConstructorValue - + "," + scriptOrURL); + auto message = QString("failed to load entity script -- expected a function, got %1, %2") + .arg(testConstructorType).arg(testConstructorValue); - newDetails.status = EntityScriptStatus::ERROR_RUNNING_SCRIPT; - newDetails.errorInfo = "Could not find constructor"; - setEntityScriptDetails(entityID, newDetails); + auto err = makeError(message); + err.setProperty("fileName", scriptOrURL); + err.setProperty("detail", "(constructor " + entityID.toString() + ")"); - qCDebug(scriptengine) << "----- ScriptEngine::entityScriptContentAvailable -- failed to run (" << scriptOrURL << ")"; + setError("Could not find constructor (" + testConstructorType + ")", EntityScriptStatus::ERROR_RUNNING_SCRIPT); + emit unhandledException(err); return; // done processing script } + // (this feeds into refreshFileScript) int64_t lastModified = 0; if (isFileUrl) { QString file = QUrl(scriptOrURL).toLocalFile(); lastModified = (quint64)QFileInfo(file).lastModified().toMSecsSinceEpoch(); } + + // THE ACTUAL EVALUATION AND CONSTRUCTION QScriptValue entityScriptConstructor, entityScriptObject; QUrl sandboxURL = currentSandboxURL.isEmpty() ? scriptOrURL : currentSandboxURL; auto initialization = [&]{ entityScriptConstructor = evaluate(contents, fileName); entityScriptObject = entityScriptConstructor.construct(); + + if (hasUncaughtException()) { + entityScriptObject = cloneUncaughtException("(construct " + entityID.toString() + ")"); + clearExceptions(); + } }; + doWithEnvironment(entityID, sandboxURL, initialization); + if (entityScriptObject.isError()) { + auto exception = entityScriptObject; + setError(formatException(exception, _enableExtendedJSExceptions.get()), EntityScriptStatus::ERROR_RUNNING_SCRIPT); + emit unhandledException(exception); + return; + } + + // ... AND WE HAVE LIFTOFF newDetails.status = EntityScriptStatus::RUNNING; newDetails.scriptObject = entityScriptObject; newDetails.lastModified = lastModified; @@ -1655,9 +2288,12 @@ void ScriptEngine::entityScriptContentAvailable(const EntityItemID& entityID, co // if we got this far, then call the preload method callEntityScriptMethod(entityID, "preload"); + + _occupiedScriptURLs.remove(entityScript); + processDeferredEntityLoads(entityScript, entityID); } -void ScriptEngine::unloadEntityScript(const EntityItemID& entityID) { +void ScriptEngine::unloadEntityScript(const EntityItemID& entityID, bool shouldRemoveFromMap) { if (QThread::currentThread() != thread()) { #ifdef THREAD_DEBUGGING qCDebug(scriptengine) << "*** WARNING *** ScriptEngine::unloadEntityScript() called on wrong thread [" << QThread::currentThread() << "], invoking on correct thread [" << thread() << "] " @@ -1665,7 +2301,8 @@ void ScriptEngine::unloadEntityScript(const EntityItemID& entityID) { #endif QMetaObject::invokeMethod(this, "unloadEntityScript", - Q_ARG(const EntityItemID&, entityID)); + Q_ARG(const EntityItemID&, entityID), + Q_ARG(bool, shouldRemoveFromMap)); return; } #ifdef THREAD_DEBUGGING @@ -1674,13 +2311,33 @@ void ScriptEngine::unloadEntityScript(const EntityItemID& entityID) { #endif if (_entityScripts.contains(entityID)) { + const EntityScriptDetails &oldDetails = _entityScripts[entityID]; if (isEntityScriptRunning(entityID)) { callEntityScriptMethod(entityID, "unload"); } - EntityScriptDetails newDetails; - newDetails.status = EntityScriptStatus::UNLOADED; - setEntityScriptDetails(entityID, newDetails); +#ifdef DEBUG_ENTITY_STATES + else { + qCDebug(scriptengine) << "unload called while !running" << entityID << oldDetails.status; + } +#endif + if (shouldRemoveFromMap) { + // this was a deleted entity, we've been asked to remove it from the map + _entityScripts.remove(entityID); + emit entityScriptDetailsUpdated(); + } else if (oldDetails.status != EntityScriptStatus::UNLOADED) { + EntityScriptDetails newDetails; + newDetails.status = EntityScriptStatus::UNLOADED; + newDetails.lastModified = QDateTime::currentMSecsSinceEpoch(); + // keep scriptText populated for the current need to "debouce" duplicate calls to unloadEntityScript + newDetails.scriptText = oldDetails.scriptText; + setEntityScriptDetails(entityID, newDetails); + } + stopAllTimersForEntityScript(entityID); + { + // FIXME: shouldn't have to do this here, but currently something seems to be firing unloads moments after firing initial load requests + processDeferredEntityLoads(oldDetails.scriptText, entityID); + } } } @@ -1701,15 +2358,14 @@ void ScriptEngine::unloadAllEntityScripts() { } _entityScripts.clear(); emit entityScriptDetailsUpdated(); + _occupiedScriptURLs.clear(); #ifdef DEBUG_ENGINE_STATE - qCDebug(scriptengine) << "---- CURRENT STATE OF ENGINE: --------------------------"; - QScriptValueIterator it(globalObject()); - while (it.hasNext()) { - it.next(); - qCDebug(scriptengine) << it.name() << ":" << it.value().toString(); - } - qCDebug(scriptengine) << "--------------------------------------------------------"; + _debugDump( + "---- CURRENT STATE OF ENGINE: --------------------------", + globalObject(), + "--------------------------------------------------------" + ); #endif // DEBUG_ENGINE_STATE } @@ -1731,17 +2387,7 @@ void ScriptEngine::refreshFileScript(const EntityItemID& entityID) { auto lastModified = QFileInfo(filePath).lastModified().toMSecsSinceEpoch(); if (lastModified > details.lastModified) { scriptInfoMessage("Reloading modified script " + details.scriptText); - - QFile file(filePath); - file.open(QIODevice::ReadOnly); - QString scriptContents = QTextStream(&file).readAll(); - this->unloadEntityScript(entityID); - this->entityScriptContentAvailable(entityID, details.scriptText, scriptContents, true, true, "Success"); - if (!isEntityScriptRunning(entityID)) { - scriptWarningMessage("Reload script " + details.scriptText + " failed"); - } else { - details = _entityScripts[entityID]; - } + loadEntityScript(entityID, details.scriptText, true); } } recurseGuard = false; @@ -1765,14 +2411,11 @@ void ScriptEngine::doWithEnvironment(const EntityItemID& entityID, const QUrl& s #else operation(); #endif - if (hasUncaughtException()) { - reportUncaughtException(); - clearExceptions(); - } - + maybeEmitUncaughtException(!entityID.isNull() ? entityID.toString() : __FUNCTION__); currentEntityIdentifier = oldIdentifier; currentSandboxURL = oldSandboxURL; } + void ScriptEngine::callWithEnvironment(const EntityItemID& entityID, const QUrl& sandboxURL, QScriptValue function, QScriptValue thisObject, QScriptValueList args) { auto operation = [&]() { function.call(thisObject, args); @@ -1847,7 +2490,6 @@ void ScriptEngine::callEntityScriptMethod(const EntityItemID& entityID, const QS } } - void ScriptEngine::callEntityScriptMethod(const EntityItemID& entityID, const QString& methodName, const EntityItemID& otherID, const Collision& collision) { if (QThread::currentThread() != thread()) { #ifdef THREAD_DEBUGGING @@ -1882,3 +2524,4 @@ void ScriptEngine::callEntityScriptMethod(const EntityItemID& entityID, const QS } } } + diff --git a/libraries/script-engine/src/ScriptEngine.h b/libraries/script-engine/src/ScriptEngine.h index a382258973..5ea8d052e9 100644 --- a/libraries/script-engine/src/ScriptEngine.h +++ b/libraries/script-engine/src/ScriptEngine.h @@ -35,11 +35,13 @@ #include "ArrayBufferClass.h" #include "AssetScriptingInterface.h" #include "AudioScriptingInterface.h" +#include "BaseScriptEngine.h" #include "Quat.h" #include "Mat4.h" #include "ScriptCache.h" #include "ScriptUUID.h" #include "Vec3.h" +#include "SettingHandle.h" class QScriptEngineDebugger; @@ -54,6 +56,13 @@ public: QUrl definingSandboxURL; }; +class DeferredLoadEntity { +public: + EntityItemID entityID; + QString entityScript; + //bool forceRedownload; +}; + typedef QList CallbackList; typedef QHash RegisteredEventHandlers; @@ -67,15 +76,7 @@ public: QString scriptText { "" }; QScriptValue scriptObject { QScriptValue() }; int64_t lastModified { 0 }; - QUrl definingSandboxURL { QUrl() }; -}; - -// common base class with just QScriptEngine-dependent helper methods -class BaseScriptEngine : public QScriptEngine { -public: - static const QString SCRIPT_EXCEPTION_FORMAT; - QString lintScript(const QString& sourceCode, const QString& fileName, const int lineNumber = 1); - QString formatUncaughtException(const QString& overrideFileName = QString()); + QUrl definingSandboxURL { QUrl("about:EntityScript") }; }; class ScriptEngine : public BaseScriptEngine, public EntitiesScriptEngineProvider { @@ -91,14 +92,13 @@ public: }; static int processLevelMaxRetries; - ScriptEngine(Context context, const QString& scriptContents = NO_SCRIPT, const QString& fileNameString = QString("")); + ScriptEngine(Context context, const QString& scriptContents = NO_SCRIPT, const QString& fileNameString = QString("about:ScriptEngine")); ~ScriptEngine(); /// run the script in a dedicated thread. This will have the side effect of evalulating /// the current script contents and calling run(). Callers will likely want to register the script with external /// services before calling this. void runInThread(); - Q_INVOKABLE void executeOnScriptThread(std::function function, bool blocking = false); void runDebuggable(); @@ -138,6 +138,8 @@ public: /// evaluate some code in the context of the ScriptEngine and return the result Q_INVOKABLE QScriptValue evaluate(const QString& program, const QString& fileName, int lineNumber = 1); // this is also used by the script tool widget + Q_INVOKABLE QScriptValue evaluateInClosure(const QScriptValue& locals, const QScriptProgram& program); + /// if the script engine is not already running, this will download the URL and start the process of seting it up /// to run... NOTE - this is used by Application currently to load the url. We don't really want it to be exposed /// to scripts. we may not need this to be invokable @@ -158,21 +160,33 @@ public: Q_INVOKABLE void include(const QStringList& includeFiles, QScriptValue callback = QScriptValue()); Q_INVOKABLE void include(const QString& includeFile, QScriptValue callback = QScriptValue()); + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // MODULE related methods + Q_INVOKABLE QScriptValue require(const QString& moduleId); + Q_INVOKABLE void resetModuleCache(bool deleteScriptCache = false); + QScriptValue currentModule(); + bool registerModuleWithParent(const QScriptValue& module, const QScriptValue& parent); + QScriptValue newModule(const QString& modulePath, const QScriptValue& parent = QScriptValue()); + QVariantMap fetchModuleSource(const QString& modulePath, const bool forceDownload = false); + QScriptValue instantiateModule(const QScriptValue& module, const QString& sourceCode); + Q_INVOKABLE QObject* setInterval(const QScriptValue& function, int intervalMS); Q_INVOKABLE QObject* setTimeout(const QScriptValue& function, int timeoutMS); Q_INVOKABLE void clearInterval(QObject* timer) { stopTimer(reinterpret_cast(timer)); } Q_INVOKABLE void clearTimeout(QObject* timer) { stopTimer(reinterpret_cast(timer)); } + Q_INVOKABLE void print(const QString& message); Q_INVOKABLE QUrl resolvePath(const QString& path) const; Q_INVOKABLE QUrl resourcesPath() const; // Entity Script Related methods - Q_INVOKABLE QString getEntityScriptStatus(const EntityItemID& entityID); Q_INVOKABLE bool isEntityScriptRunning(const EntityItemID& entityID) { return _entityScripts.contains(entityID) && _entityScripts[entityID].status == EntityScriptStatus::RUNNING; } - static void loadEntityScript(QWeakPointer theEngine, const EntityItemID& entityID, const QString& entityScript, bool forceRedownload); - Q_INVOKABLE void unloadEntityScript(const EntityItemID& entityID); // will call unload method + QVariant cloneEntityScriptDetails(const EntityItemID& entityID); + QFuture getLocalEntityScriptDetails(const EntityItemID& entityID) override; + Q_INVOKABLE void loadEntityScript(const EntityItemID& entityID, const QString& entityScript, bool forceRedownload); + Q_INVOKABLE void unloadEntityScript(const EntityItemID& entityID, bool shouldRemoveFromMap = false); // will call unload method Q_INVOKABLE void unloadAllEntityScripts(); Q_INVOKABLE void callEntityScriptMethod(const EntityItemID& entityID, const QString& methodName, const QStringList& params = QStringList()) override; @@ -212,7 +226,6 @@ public: bool getEntityScriptDetails(const EntityItemID& entityID, EntityScriptDetails &details) const; public slots: - int evaluatePending() const { return _evaluatesPending; } void callAnimationStateHandler(QScriptValue callback, AnimVariantMap parameters, QStringList names, bool useNames, AnimVariantResultHandler resultHandler); void updateMemoryCost(const qint64&); @@ -223,12 +236,11 @@ signals: void scriptEnding(); void finished(const QString& fileNameString, ScriptEngine* engine); void cleanupMenuItem(const QString& menuItemString); - void printedMessage(const QString& message); - void errorMessage(const QString& message); - void warningMessage(const QString& message); - void infoMessage(const QString& message); + void printedMessage(const QString& message, const QString& scriptName); + void errorMessage(const QString& message, const QString& scriptName); + void warningMessage(const QString& message, const QString& scriptName); + void infoMessage(const QString& message, const QString& scriptName); void runningStateChanged(); - void evaluationFinished(QScriptValue result, bool isException); void loadScript(const QString& scriptName, bool isUserLoaded); void reloadScript(const QString& scriptName, bool isUserLoaded); void doneRunning(); @@ -239,8 +251,12 @@ signals: protected: void init(); + Q_INVOKABLE void executeOnScriptThread(std::function function, const Qt::ConnectionType& type = Qt::QueuedConnection ); + // note: this is not meant to be called directly, but just to have QMetaObject take care of wiring it up in general; + // then inside of init() we just have to do "Script.require.resolve = Script._requireResolve;" + Q_INVOKABLE QString _requireResolve(const QString& moduleId, const QString& relativeTo = QString()); - QString reportUncaughtException(const QString& overrideFileName = QString()); + QString logException(const QScriptValue& exception); void timerFired(); void stopAllTimers(); void stopAllTimersForEntityScript(const EntityItemID& entityID); @@ -248,6 +264,7 @@ protected: void updateEntityScriptStatus(const EntityItemID& entityID, const EntityScriptStatus& status, const QString& errorInfo = QString()); void setEntityScriptDetails(const EntityItemID& entityID, const EntityScriptDetails& details); void setParentURL(const QString& parentURL) { _parentURL = parentURL; } + void processDeferredEntityLoads(const QString& entityScript, const EntityItemID& leaderID); QObject* setupTimerWithInterval(const QScriptValue& function, int intervalMS, bool isSingleShot); void stopTimer(QTimer* timer); @@ -262,17 +279,18 @@ protected: void callWithEnvironment(const EntityItemID& entityID, const QUrl& sandboxURL, QScriptValue function, QScriptValue thisObject, QScriptValueList args); Context _context; - QString _scriptContents; QString _parentURL; std::atomic _isFinished { false }; std::atomic _isRunning { false }; std::atomic _isStopping { false }; - int _evaluatesPending { 0 }; bool _isInitialized { false }; QHash _timerFunctionMap; QSet _includedURLs; QHash _entityScripts; + QHash _occupiedScriptURLs; + QList _deferredEntityLoads; + bool _isThreaded { false }; QScriptEngineDebugger* _debugger { nullptr }; bool _debuggable { false }; @@ -290,11 +308,16 @@ protected: AssetScriptingInterface _assetScriptingInterface{ this }; - std::function _emitScriptUpdates{ [](){ return true; } }; + std::function _emitScriptUpdates{ []() { return true; } }; std::recursive_mutex _lock; std::chrono::microseconds _totalTimerExecution { 0 }; + + static const QString _SETTINGS_ENABLE_EXTENDED_MODULE_COMPAT; + static const QString _SETTINGS_ENABLE_EXTENDED_EXCEPTIONS; + + Setting::Handle _enableExtendedJSExceptions { _SETTINGS_ENABLE_EXTENDED_EXCEPTIONS, true }; }; #endif // hifi_ScriptEngine_h diff --git a/libraries/script-engine/src/ScriptEngineLogging.cpp b/libraries/script-engine/src/ScriptEngineLogging.cpp index 2e5d293728..392bc05129 100644 --- a/libraries/script-engine/src/ScriptEngineLogging.cpp +++ b/libraries/script-engine/src/ScriptEngineLogging.cpp @@ -12,3 +12,4 @@ #include "ScriptEngineLogging.h" Q_LOGGING_CATEGORY(scriptengine, "hifi.scriptengine") +Q_LOGGING_CATEGORY(scriptengine_module, "hifi.scriptengine.module") diff --git a/libraries/script-engine/src/ScriptEngineLogging.h b/libraries/script-engine/src/ScriptEngineLogging.h index 0e614dd5bf..62e46632a6 100644 --- a/libraries/script-engine/src/ScriptEngineLogging.h +++ b/libraries/script-engine/src/ScriptEngineLogging.h @@ -15,6 +15,7 @@ #include Q_DECLARE_LOGGING_CATEGORY(scriptengine) +Q_DECLARE_LOGGING_CATEGORY(scriptengine_module) #endif // hifi_ScriptEngineLogging_h diff --git a/libraries/script-engine/src/ScriptEngines.cpp b/libraries/script-engine/src/ScriptEngines.cpp index b2ff337fb9..88b0e0b7b5 100644 --- a/libraries/script-engine/src/ScriptEngines.cpp +++ b/libraries/script-engine/src/ScriptEngines.cpp @@ -34,34 +34,24 @@ ScriptsModel& getScriptsModel() { return scriptsModel; } -void ScriptEngines::onPrintedMessage(const QString& message) { - auto scriptEngine = qobject_cast(sender()); - auto scriptName = scriptEngine ? scriptEngine->getFilename() : ""; +void ScriptEngines::onPrintedMessage(const QString& message, const QString& scriptName) { emit printedMessage(message, scriptName); } -void ScriptEngines::onErrorMessage(const QString& message) { - auto scriptEngine = qobject_cast(sender()); - auto scriptName = scriptEngine ? scriptEngine->getFilename() : ""; +void ScriptEngines::onErrorMessage(const QString& message, const QString& scriptName) { emit errorMessage(message, scriptName); } -void ScriptEngines::onWarningMessage(const QString& message) { - auto scriptEngine = qobject_cast(sender()); - auto scriptName = scriptEngine ? scriptEngine->getFilename() : ""; +void ScriptEngines::onWarningMessage(const QString& message, const QString& scriptName) { emit warningMessage(message, scriptName); } -void ScriptEngines::onInfoMessage(const QString& message) { - auto scriptEngine = qobject_cast(sender()); - auto scriptName = scriptEngine ? scriptEngine->getFilename() : ""; +void ScriptEngines::onInfoMessage(const QString& message, const QString& scriptName) { emit infoMessage(message, scriptName); } void ScriptEngines::onErrorLoadingScript(const QString& url) { - auto scriptEngine = qobject_cast(sender()); - auto scriptName = scriptEngine ? scriptEngine->getFilename() : ""; - emit errorLoadingScript(url, scriptName); + emit errorLoadingScript(url); } ScriptEngines::ScriptEngines(ScriptEngine::Context context) @@ -364,25 +354,43 @@ QStringList ScriptEngines::getRunningScripts() { } void ScriptEngines::stopAllScripts(bool restart) { + QVector toReload; QReadLocker lock(&_scriptEnginesHashLock); for (QHash::const_iterator it = _scriptEnginesHash.constBegin(); it != _scriptEnginesHash.constEnd(); it++) { + ScriptEngine *scriptEngine = it.value(); // skip already stopped scripts - if (it.value()->isFinished() || it.value()->isStopping()) { + if (scriptEngine->isFinished() || scriptEngine->isStopping()) { continue; } // queue user scripts if restarting - if (restart && it.value()->isUserLoaded()) { - connect(it.value(), &ScriptEngine::finished, this, [this](QString scriptName, ScriptEngine* engine) { - reloadScript(scriptName); - }); + if (restart && scriptEngine->isUserLoaded()) { + toReload << it.key().toString(); } // stop all scripts - it.value()->stop(true); qCDebug(scriptengine) << "stopping script..." << it.key(); + scriptEngine->stop(); } + // wait for engines to stop (ie: providing time for .scriptEnding cleanup handlers to run) before + // triggering reload of any Client scripts / Entity scripts + QTimer::singleShot(500, this, [=]() { + for(const auto &scriptName : toReload) { + auto scriptEngine = getScriptEngine(scriptName); + if (scriptEngine && !scriptEngine->isFinished()) { + qCDebug(scriptengine) << "waiting on script:" << scriptName; + scriptEngine->waitTillDoneRunning(); + qCDebug(scriptengine) << "done waiting on script:" << scriptName; + } + qCDebug(scriptengine) << "reloading script..." << scriptName; + reloadScript(scriptName); + } + if (restart) { + qCDebug(scriptengine) << "stopAllScripts -- emitting scriptsReloading"; + emit scriptsReloading(); + } + }); } bool ScriptEngines::stopScript(const QString& rawScriptURL, bool restart) { @@ -421,9 +429,10 @@ void ScriptEngines::setScriptsLocation(const QString& scriptsLocation) { } void ScriptEngines::reloadAllScripts() { + qCDebug(scriptengine) << "reloadAllScripts -- clearing caches"; DependencyManager::get()->clearCache(); DependencyManager::get()->clearCache(); - emit scriptsReloading(); + qCDebug(scriptengine) << "reloadAllScripts -- stopping all scripts"; stopAllScripts(true); } @@ -456,7 +465,7 @@ ScriptEngine* ScriptEngines::loadScript(const QUrl& scriptFilename, bool isUserL return scriptEngine; } - scriptEngine = new ScriptEngine(_context, NO_SCRIPT, ""); + scriptEngine = new ScriptEngine(_context, NO_SCRIPT, "about:" + scriptFilename.fileName()); scriptEngine->setUserLoaded(isUserLoaded); connect(scriptEngine, &ScriptEngine::doneRunning, this, [scriptEngine] { scriptEngine->deleteLater(); diff --git a/libraries/script-engine/src/ScriptEngines.h b/libraries/script-engine/src/ScriptEngines.h index 2fadfc81f8..63b7e8f11c 100644 --- a/libraries/script-engine/src/ScriptEngines.h +++ b/libraries/script-engine/src/ScriptEngines.h @@ -79,13 +79,13 @@ signals: void errorMessage(const QString& message, const QString& engineName); void warningMessage(const QString& message, const QString& engineName); void infoMessage(const QString& message, const QString& engineName); - void errorLoadingScript(const QString& url, const QString& engineName); + void errorLoadingScript(const QString& url); public slots: - void onPrintedMessage(const QString& message); - void onErrorMessage(const QString& message); - void onWarningMessage(const QString& message); - void onInfoMessage(const QString& message); + void onPrintedMessage(const QString& message, const QString& scriptName); + void onErrorMessage(const QString& message, const QString& scriptName); + void onWarningMessage(const QString& message, const QString& scriptName); + void onInfoMessage(const QString& message, const QString& scriptName); void onErrorLoadingScript(const QString& url); protected slots: diff --git a/libraries/script-engine/src/TabletScriptingInterface.cpp b/libraries/script-engine/src/TabletScriptingInterface.cpp index 32bd7f422e..7747e1b6dc 100644 --- a/libraries/script-engine/src/TabletScriptingInterface.cpp +++ b/libraries/script-engine/src/TabletScriptingInterface.cpp @@ -201,7 +201,7 @@ void TabletProxy::setToolbarMode(bool toolbarMode) { QObject::connect(quickItem, SIGNAL(windowClosed()), this, SLOT(desktopWindowClosed())); - QObject::connect(tabletRootWindow, SIGNAL(webEventReceived(QVariant)), this, SIGNAL(webEventReceived(QVariant))); + QObject::connect(tabletRootWindow, SIGNAL(webEventReceived(QVariant)), this, SLOT(emitWebEvent(QVariant)), Qt::DirectConnection); // forward qml surface events to interface js connect(tabletRootWindow, &QmlWindowClass::fromQml, this, &TabletProxy::fromQml); @@ -250,12 +250,41 @@ static QString getUsername() { } } +void TabletProxy::initialScreen(const QVariant& url) { + if (_qmlTabletRoot) { + pushOntoStack(url); + } else { + _initialScreen = true; + _initialPath = url; + } +} + +bool TabletProxy::isMessageDialogOpen() { + if (_qmlTabletRoot) { + QVariant result; + QMetaObject::invokeMethod(_qmlTabletRoot, "isDialogOpen",Qt::DirectConnection, + Q_RETURN_ARG(QVariant, result)); + + return result.toBool(); + } + + return false; +} + +void TabletProxy::emitWebEvent(QVariant msg) { + emit webEventReceived(msg); +} + +bool TabletProxy::isPathLoaded(QVariant path) { + return path.toString() == _currentPathLoaded.toString(); +} void TabletProxy::setQmlTabletRoot(QQuickItem* qmlTabletRoot, QObject* qmlOffscreenSurface) { std::lock_guard guard(_mutex); _qmlOffscreenSurface = qmlOffscreenSurface; _qmlTabletRoot = qmlTabletRoot; if (_qmlTabletRoot && _qmlOffscreenSurface) { - QObject::connect(_qmlOffscreenSurface, SIGNAL(webEventReceived(QVariant)), this, SIGNAL(webEventReceived(QVariant))); + + QObject::connect(_qmlOffscreenSurface, SIGNAL(webEventReceived(QVariant)), this, SLOT(emitWebEvent(QVariant)), Qt::DirectConnection); // forward qml surface events to interface js connect(dynamic_cast(_qmlOffscreenSurface), &OffscreenQmlSurface::fromQml, [this](QVariant message) { @@ -275,7 +304,8 @@ void TabletProxy::setQmlTabletRoot(QQuickItem* qmlTabletRoot, QObject* qmlOffscr QMetaObject::invokeMethod(_qmlTabletRoot, "loadSource", Q_ARG(const QVariant&, QVariant(TABLET_SOURCE_URL))); } - gotoHomeScreen(); + // force to the tablet to go to the homescreen + loadHomeScreen(true); QMetaObject::invokeMethod(_qmlTabletRoot, "setUsername", Q_ARG(const QVariant&, QVariant(getUsername()))); @@ -286,13 +316,22 @@ void TabletProxy::setQmlTabletRoot(QQuickItem* qmlTabletRoot, QObject* qmlOffscr QMetaObject::invokeMethod(_qmlTabletRoot, "setUsername", Q_ARG(const QVariant&, QVariant(getUsername()))); } }); + + if (_initialScreen) { + pushOntoStack(_initialPath); + _initialScreen = false; + } } else { removeButtonsFromHomeScreen(); _state = State::Uninitialized; emit screenChanged(QVariant("Closed"), QVariant("")); + _currentPathLoaded = ""; } } +void TabletProxy::gotoHomeScreen() { + loadHomeScreen(false); +} void TabletProxy::gotoMenuScreen(const QString& submenu) { QObject* root = nullptr; @@ -310,6 +349,7 @@ void TabletProxy::gotoMenuScreen(const QString& submenu) { QMetaObject::invokeMethod(root, "loadSource", Q_ARG(const QVariant&, QVariant(VRMENU_SOURCE_URL))); _state = State::Menu; emit screenChanged(QVariant("Menu"), QVariant(VRMENU_SOURCE_URL)); + _currentPathLoaded = VRMENU_SOURCE_URL; QMetaObject::invokeMethod(root, "setShown", Q_ARG(const QVariant&, QVariant(true))); } } @@ -329,13 +369,56 @@ void TabletProxy::loadQMLSource(const QVariant& path) { QMetaObject::invokeMethod(root, "loadSource", Q_ARG(const QVariant&, path)); _state = State::QML; emit screenChanged(QVariant("QML"), path); + _currentPathLoaded = path; QMetaObject::invokeMethod(root, "setShown", Q_ARG(const QVariant&, QVariant(true))); } + } else { + qCDebug(scriptengine) << "tablet cannot load QML because _qmlTabletRoot is null"; } } -void TabletProxy::gotoHomeScreen() { - if (_state != State::Home) { +void TabletProxy::pushOntoStack(const QVariant& path) { + if (_qmlTabletRoot) { + auto stack = _qmlTabletRoot->findChild("stack"); + if (stack) { + QMetaObject::invokeMethod(stack, "pushSource", Q_ARG(const QVariant&, path)); + } else { + loadQMLSource(path); + } + } else if (_desktopWindow) { + auto stack = _desktopWindow->asQuickItem()->findChild("stack"); + if (stack) { + QMetaObject::invokeMethod(stack, "pushSource", Q_ARG(const QVariant&, path)); + } else { + qCDebug(scriptengine) << "tablet cannot push QML because _desktopWindow doesn't have child stack"; + } + } else { + qCDebug(scriptengine) << "tablet cannot push QML because _qmlTabletRoot or _desktopWindow is null"; + } +} + +void TabletProxy::popFromStack() { + if (_qmlTabletRoot) { + auto stack = _qmlTabletRoot->findChild("stack"); + if (stack) { + QMetaObject::invokeMethod(stack, "popSource"); + } else { + qCDebug(scriptengine) << "tablet cannot push QML because _qmlTabletRoot doesn't have child stack"; + } + } else if (_desktopWindow) { + auto stack = _desktopWindow->asQuickItem()->findChild("stack"); + if (stack) { + QMetaObject::invokeMethod(stack, "popSource"); + } else { + qCDebug(scriptengine) << "tablet cannot pop QML because _desktopWindow doesn't have child stack"; + } + } else { + qCDebug(scriptengine) << "tablet cannot pop QML because _qmlTabletRoot or _desktopWindow is null"; + } +} + +void TabletProxy::loadHomeScreen(bool forceOntoHomeScreen) { + if ((_state != State::Home && _state != State::Uninitialized) || forceOntoHomeScreen) { if (!_toolbarMode && _qmlTabletRoot) { auto loader = _qmlTabletRoot->findChild("loader"); QObject::connect(loader, SIGNAL(loaded()), this, SLOT(addButtonsToHomeScreen()), Qt::DirectConnection); @@ -349,6 +432,7 @@ void TabletProxy::gotoHomeScreen() { } _state = State::Home; emit screenChanged(QVariant("Home"), QVariant(TABLET_SOURCE_URL)); + _currentPathLoaded = TABLET_SOURCE_URL; } } @@ -373,6 +457,7 @@ void TabletProxy::gotoWebScreen(const QString& url, const QString& injectedJavaS } _state = State::Web; emit screenChanged(QVariant("Web"), QVariant(url)); + _currentPathLoaded = QVariant(url); } QObject* TabletProxy::addButton(const QVariant& properties) { @@ -560,7 +645,7 @@ QQuickItem* TabletProxy::getQmlTablet() const { } QQuickItem* TabletProxy::getQmlMenu() const { - if (!_qmlTabletRoot) { + if (!_qmlTabletRoot) { return nullptr; } @@ -590,7 +675,10 @@ const QString OBJECT_NAME_KEY = "objectName"; const QString STABLE_ORDER_KEY = "stableOrder"; static int s_stableOrder = 1; -TabletButtonProxy::TabletButtonProxy(const QVariantMap& properties) : _uuid(QUuid::createUuid()), _stableOrder(++s_stableOrder), _properties(properties) { +TabletButtonProxy::TabletButtonProxy(const QVariantMap& properties) : + _uuid(QUuid::createUuid()), + _stableOrder(++s_stableOrder), + _properties(properties) { // this is used to uniquely identify this button. _properties[UUID_KEY] = _uuid; _properties[OBJECT_NAME_KEY] = _uuid.toString(); @@ -633,4 +721,3 @@ void TabletButtonProxy::editProperties(QVariantMap properties) { } #include "TabletScriptingInterface.moc" - diff --git a/libraries/script-engine/src/TabletScriptingInterface.h b/libraries/script-engine/src/TabletScriptingInterface.h index e450923758..195db02789 100644 --- a/libraries/script-engine/src/TabletScriptingInterface.h +++ b/libraries/script-engine/src/TabletScriptingInterface.h @@ -56,6 +56,13 @@ public: QQuickWindow* getTabletWindow(); QObject* getFlags(); +signals: + /** jsdoc + * Signaled when a tablet message or dialog is created + * @function TabletProxy#tabletNotification + * @returns {Signal} + */ + void tabletNotification(); private: void processMenuEvents(QObject* object, const QKeyEvent* event); @@ -78,6 +85,8 @@ class TabletProxy : public QObject { Q_OBJECT Q_PROPERTY(QString name READ getName) Q_PROPERTY(bool toolbarMode READ getToolbarMode WRITE setToolbarMode) + Q_PROPERTY(bool landscape READ getLandscape WRITE setLandscape) + Q_PROPERTY(bool tabletShown MEMBER _tabletShown NOTIFY tabletShownChanged) public: TabletProxy(QString name); @@ -90,6 +99,8 @@ public: bool getToolbarMode() const { return _toolbarMode; } void setToolbarMode(bool toolbarMode); + void initialScreen(const QVariant& url); + /**jsdoc * transition to the home screen * @function TabletProxy#gotoHomeScreen @@ -106,6 +117,14 @@ public: Q_INVOKABLE void gotoWebScreen(const QString& url, const QString& injectedJavaScriptUrl); Q_INVOKABLE void loadQMLSource(const QVariant& path); + Q_INVOKABLE void pushOntoStack(const QVariant& path); + Q_INVOKABLE void popFromStack(); + + /** jsdoc + * Check if the tablet has a message dialog open + * @function TabletProxy#isMessageDialogOpen + */ + Q_INVOKABLE bool isMessageDialogOpen(); /**jsdoc * Creates a new button, adds it to this and returns it. @@ -150,8 +169,24 @@ public: */ Q_INVOKABLE void sendToQml(QVariant msg); + /**jsdoc + * Check if the tablet is on the homescreen + * @function TabletProxy#onHomeScreen() + */ Q_INVOKABLE bool onHomeScreen(); + /**jsdoc + * set tablet into our out of landscape mode + * @function TabletProxy#setLandscape + * @param landscape {bool} true for landscape, false for portrait + */ + Q_INVOKABLE void setLandscape(bool landscape) { _landscape = landscape; } + Q_INVOKABLE bool getLandscape() { return _landscape; } + + Q_INVOKABLE bool isPathLoaded(QVariant path); + + QQuickItem* getTabletRoot() const { return _qmlTabletRoot; } + QObject* getTabletSurface(); QQuickItem* getQmlTablet() const; @@ -183,14 +218,26 @@ signals: */ void screenChanged(QVariant type, QVariant url); + /** jsdoc + * Signaled when the tablet becomes visible or becomes invisible + * @function TabletProxy#isTabletShownChanged + * @returns {Signal} + */ + void tabletShownChanged(); + protected slots: void addButtonsToHomeScreen(); void desktopWindowClosed(); + void emitWebEvent(QVariant msg); protected: void removeButtonsFromHomeScreen(); + void loadHomeScreen(bool forceOntoHomeScreen); void addButtonsToToolbar(); void removeButtonsFromToolbar(); + bool _initialScreen { false }; + QVariant _initialPath { "" }; + QVariant _currentPathLoaded { "" }; QString _name; std::mutex _mutex; std::vector> _tabletButtonProxies; @@ -198,9 +245,11 @@ protected: QObject* _qmlOffscreenSurface { nullptr }; QmlWindowClass* _desktopWindow { nullptr }; bool _toolbarMode { false }; + bool _tabletShown { false }; enum class State { Uninitialized, Home, Web, Menu, QML }; State _state { State::Uninitialized }; + bool _landscape { false }; }; /**jsdoc diff --git a/libraries/script-engine/src/Vec3.h b/libraries/script-engine/src/Vec3.h index 5f524eaf74..b3a3dc3035 100644 --- a/libraries/script-engine/src/Vec3.h +++ b/libraries/script-engine/src/Vec3.h @@ -37,6 +37,15 @@ * @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 class Vec3 : public QObject { diff --git a/libraries/script-engine/src/XMLHttpRequestClass.cpp b/libraries/script-engine/src/XMLHttpRequestClass.cpp index 4e528ec52c..1d3c8fda32 100644 --- a/libraries/script-engine/src/XMLHttpRequestClass.cpp +++ b/libraries/script-engine/src/XMLHttpRequestClass.cpp @@ -143,7 +143,7 @@ void XMLHttpRequestClass::open(const QString& method, const QString& url, bool a if (url.toLower().left(METAVERSE_API_URL.length()) == METAVERSE_API_URL) { auto accountManager = DependencyManager::get(); - if (_url.scheme() == "https" && accountManager->hasValidAccessToken()) { + if (accountManager->hasValidAccessToken()) { static const QString HTTP_AUTHORIZATION_HEADER = "Authorization"; QString bearerString = "Bearer " + accountManager->getAccountInfo().getAccessToken().token; _request.setRawHeader(HTTP_AUTHORIZATION_HEADER.toLocal8Bit(), bearerString.toLocal8Bit()); diff --git a/libraries/shared/src/AABox.cpp b/libraries/shared/src/AABox.cpp index 89d5ce709d..3f3146cc04 100644 --- a/libraries/shared/src/AABox.cpp +++ b/libraries/shared/src/AABox.cpp @@ -436,6 +436,38 @@ glm::vec3 AABox::getClosestPointOnFace(const glm::vec4& origin, const glm::vec4& return getClosestPointOnFace(glm::vec3(origin), face); } +bool AABox::touchesAAEllipsoid(const glm::vec3& center, const glm::vec3& radials) const { + // handle case where ellipsoid's alix-aligned box doesn't touch this AABox + if (_corner.x - radials.x > center.x || + _corner.y - radials.y > center.y || + _corner.z - radials.z > center.z || + _corner.x + _scale.x + radials.x < center.x || + _corner.y + _scale.y + radials.y < center.y || + _corner.z + _scale.z + radials.z < center.z) { + return false; + } + + // handle case where ellipsoid is entirely inside this AABox + if (contains(center)) { + return true; + } + + for (int i = 0; i < FACE_COUNT; i++) { + glm::vec3 closest = getClosestPointOnFace(center, (BoxFace)i) - center; + float x = closest.x; + float y = closest.y; + float z = closest.z; + float a = radials.x; + float b = radials.y; + float c = radials.z; + if (x*x/(a*a) + y*y/(b*b) + z*z/(c*c) < 1.0f) { + return true; + } + } + return false; +} + + glm::vec4 AABox::getPlane(BoxFace face) const { switch (face) { case MIN_X_FACE: return glm::vec4(-1.0f, 0.0f, 0.0f, _corner.x); diff --git a/libraries/shared/src/AABox.h b/libraries/shared/src/AABox.h index 2f0b09d67a..a53cc26163 100644 --- a/libraries/shared/src/AABox.h +++ b/libraries/shared/src/AABox.h @@ -70,6 +70,7 @@ public: bool findRayIntersection(const glm::vec3& origin, const glm::vec3& direction, float& distance, BoxFace& face, glm::vec3& surfaceNormal) const; bool touchesSphere(const glm::vec3& center, float radius) const; // fast but may generate false positives + bool touchesAAEllipsoid(const glm::vec3& center, const glm::vec3& radials) const; bool findSpherePenetration(const glm::vec3& center, float radius, glm::vec3& penetration) const; bool findCapsulePenetration(const glm::vec3& start, const glm::vec3& end, float radius, glm::vec3& penetration) const; @@ -109,6 +110,8 @@ public: bool isInvalid() const { return _corner == INFINITY_VECTOR; } + void clear() { _corner = INFINITY_VECTOR; _scale = glm::vec3(0.0f); } + private: glm::vec3 getClosestPointOnFace(const glm::vec3& point, BoxFace face) const; glm::vec3 getClosestPointOnFace(const glm::vec4& origin, const glm::vec4& direction, BoxFace face) const; diff --git a/libraries/shared/src/BaseScriptEngine.cpp b/libraries/shared/src/BaseScriptEngine.cpp new file mode 100644 index 0000000000..c92d629b75 --- /dev/null +++ b/libraries/shared/src/BaseScriptEngine.cpp @@ -0,0 +1,361 @@ +// +// BaseScriptEngine.cpp +// libraries/script-engine/src +// +// Created by Timothy Dedischew on 02/01/17. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include "BaseScriptEngine.h" +#include "SharedLogging.h" + +#include +#include +#include +#include +#include +#include + +#include "Profile.h" + +const QString BaseScriptEngine::SCRIPT_EXCEPTION_FORMAT { "[%0] %1 in %2:%3" }; +const QString BaseScriptEngine::SCRIPT_BACKTRACE_SEP { "\n " }; + +bool BaseScriptEngine::IS_THREADSAFE_INVOCATION(const QThread *thread, const QString& method) { + if (QThread::currentThread() == thread) { + return true; + } + qCCritical(shared) << QString("Scripting::%1 @ %2 -- ignoring thread-unsafe call from %3") + .arg(method).arg(thread ? thread->objectName() : "(!thread)").arg(QThread::currentThread()->objectName()); + qCDebug(shared) << "(please resolve on the calling side by using invokeMethod, executeOnScriptThread, etc.)"; + Q_ASSERT(false); + return false; +} + +// engine-aware JS Error copier and factory +QScriptValue BaseScriptEngine::makeError(const QScriptValue& _other, const QString& type) { + if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) { + return unboundNullValue(); + } + auto other = _other; + if (other.isString()) { + other = newObject(); + other.setProperty("message", _other.toString()); + } + auto proto = globalObject().property(type); + if (!proto.isFunction()) { + proto = globalObject().property(other.prototype().property("constructor").property("name").toString()); + } + if (!proto.isFunction()) { +#ifdef DEBUG_JS_EXCEPTIONS + qCDebug(shared) << "BaseScriptEngine::makeError -- couldn't find constructor for" << type << " -- using Error instead"; +#endif + proto = globalObject().property("Error"); + } + if (other.engine() != this) { + // JS Objects are parented to a specific script engine instance + // -- this effectively ~clones it locally by routing through a QVariant and back + other = toScriptValue(other.toVariant()); + } + // ~ var err = new Error(other.message) + auto err = proto.construct(QScriptValueList({other.property("message")})); + + // transfer over any existing properties + QScriptValueIterator it(other); + while (it.hasNext()) { + it.next(); + err.setProperty(it.name(), it.value()); + } + return err; +} + +// check syntax and when there are issues returns an actual "SyntaxError" with the details +QScriptValue BaseScriptEngine::lintScript(const QString& sourceCode, const QString& fileName, const int lineNumber) { + if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) { + return unboundNullValue(); + } + const auto syntaxCheck = checkSyntax(sourceCode); + if (syntaxCheck.state() != QScriptSyntaxCheckResult::Valid) { + auto err = globalObject().property("SyntaxError") + .construct(QScriptValueList({syntaxCheck.errorMessage()})); + err.setProperty("fileName", fileName); + err.setProperty("lineNumber", syntaxCheck.errorLineNumber()); + err.setProperty("expressionBeginOffset", syntaxCheck.errorColumnNumber()); + err.setProperty("stack", currentContext()->backtrace().join(SCRIPT_BACKTRACE_SEP)); + { + const auto error = syntaxCheck.errorMessage(); + const auto line = QString::number(syntaxCheck.errorLineNumber()); + const auto column = QString::number(syntaxCheck.errorColumnNumber()); + // for compatibility with legacy reporting + const auto message = QString("[SyntaxError] %1 in %2:%3(%4)").arg(error, fileName, line, column); + err.setProperty("formatted", message); + } + return err; + } + return QScriptValue(); +} + +// this pulls from the best available information to create a detailed snapshot of the current exception +QScriptValue BaseScriptEngine::cloneUncaughtException(const QString& extraDetail) { + if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) { + return unboundNullValue(); + } + if (!hasUncaughtException()) { + return unboundNullValue(); + } + auto exception = uncaughtException(); + // ensure the error object is engine-local + auto err = makeError(exception); + + // not sure why Qt does't offer uncaughtExceptionFileName -- but the line number + // on its own is often useless/wrong if arbitrarily married to a filename. + // when the error object already has this info, it seems to be the most reliable + auto fileName = exception.property("fileName").toString(); + auto lineNumber = exception.property("lineNumber").toInt32(); + + // the backtrace, on the other hand, seems most reliable taken from uncaughtExceptionBacktrace + auto backtrace = uncaughtExceptionBacktrace(); + if (backtrace.isEmpty()) { + // fallback to the error object + backtrace = exception.property("stack").toString().split(SCRIPT_BACKTRACE_SEP); + } + // the ad hoc "detail" property can be used now to embed additional clues + auto detail = exception.property("detail").toString(); + if (detail.isEmpty()) { + detail = extraDetail; + } else if (!extraDetail.isEmpty()) { + detail += "(" + extraDetail + ")"; + } + if (lineNumber <= 0) { + lineNumber = uncaughtExceptionLineNumber(); + } + if (fileName.isEmpty()) { + // climb the stack frames looking for something useful to display + for (auto c = currentContext(); c && fileName.isEmpty(); c = c->parentContext()) { + QScriptContextInfo info { c }; + if (!info.fileName().isEmpty()) { + // take fileName:lineNumber as a pair + fileName = info.fileName(); + lineNumber = info.lineNumber(); + if (backtrace.isEmpty()) { + backtrace = c->backtrace(); + } + break; + } + } + } + err.setProperty("fileName", fileName); + err.setProperty("lineNumber", lineNumber ); + err.setProperty("detail", detail); + err.setProperty("stack", backtrace.join(SCRIPT_BACKTRACE_SEP)); + +#ifdef DEBUG_JS_EXCEPTIONS + err.setProperty("_fileName", exception.property("fileName").toString()); + err.setProperty("_stack", uncaughtExceptionBacktrace().join(SCRIPT_BACKTRACE_SEP)); + err.setProperty("_lineNumber", uncaughtExceptionLineNumber()); +#endif + return err; +} + +QString BaseScriptEngine::formatException(const QScriptValue& exception, bool includeExtendedDetails) { + if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) { + return QString(); + } + QString note { "UncaughtException" }; + QString result; + + if (!exception.isObject()) { + return result; + } + const auto message = exception.toString(); + const auto fileName = exception.property("fileName").toString(); + const auto lineNumber = exception.property("lineNumber").toString(); + const auto stacktrace = exception.property("stack").toString(); + + if (includeExtendedDetails) { + // Display additional exception / troubleshooting hints that can be added via the custom Error .detail property + // Example difference: + // [UncaughtExceptions] Error: Can't find variable: foobar in atp:/myentity.js\n... + // [UncaughtException (construct {1eb5d3fa-23b1-411c-af83-163af7220e3f})] Error: Can't find variable: foobar in atp:/myentity.js\n... + if (exception.property("detail").isValid()) { + note += " " + exception.property("detail").toString(); + } + } + + result = QString(SCRIPT_EXCEPTION_FORMAT).arg(note, message, fileName, lineNumber); + if (!stacktrace.isEmpty()) { + result += QString("\n[Backtrace]%1%2").arg(SCRIPT_BACKTRACE_SEP).arg(stacktrace); + } + return result; +} + +bool BaseScriptEngine::raiseException(const QScriptValue& exception) { + if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) { + return false; + } + if (currentContext()) { + // we have an active context / JS stack frame so throw the exception per usual + currentContext()->throwValue(makeError(exception)); + return true; + } else { + // we are within a pure C++ stack frame (ie: being called directly by other C++ code) + // in this case no context information is available so just emit the exception for reporting + emit unhandledException(makeError(exception)); + } + return false; +} + +bool BaseScriptEngine::maybeEmitUncaughtException(const QString& debugHint) { + if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) { + return false; + } + if (!isEvaluating() && hasUncaughtException()) { + emit unhandledException(cloneUncaughtException(debugHint)); + clearExceptions(); + return true; + } + return false; +} + +QScriptValue BaseScriptEngine::evaluateInClosure(const QScriptValue& closure, const QScriptProgram& program) { + PROFILE_RANGE(script, "evaluateInClosure"); + if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) { + return unboundNullValue(); + } + const auto fileName = program.fileName(); + const auto shortName = QUrl(fileName).fileName(); + + QScriptValue result; + QScriptValue oldGlobal; + auto global = closure.property("global"); + if (global.isObject()) { +#ifdef DEBUG_JS + qCDebug(shared) << " setting global = closure.global" << shortName; +#endif + oldGlobal = globalObject(); + setGlobalObject(global); + } + + auto context = pushContext(); + + auto thiz = closure.property("this"); + if (thiz.isObject()) { +#ifdef DEBUG_JS + qCDebug(shared) << " setting this = closure.this" << shortName; +#endif + context->setThisObject(thiz); + } + + context->pushScope(closure); +#ifdef DEBUG_JS + qCDebug(shared) << QString("[%1] evaluateInClosure %2").arg(isEvaluating()).arg(shortName); +#endif + { + result = BaseScriptEngine::evaluate(program); + if (hasUncaughtException()) { + auto err = cloneUncaughtException(__FUNCTION__); +#ifdef DEBUG_JS_EXCEPTIONS + qCWarning(shared) << __FUNCTION__ << "---------- hasCaught:" << err.toString() << result.toString(); + err.setProperty("_result", result); +#endif + result = err; + } + } +#ifdef DEBUG_JS + qCDebug(shared) << QString("[%1] //evaluateInClosure %2").arg(isEvaluating()).arg(shortName); +#endif + popContext(); + + if (oldGlobal.isValid()) { +#ifdef DEBUG_JS + qCDebug(shared) << " restoring global" << shortName; +#endif + setGlobalObject(oldGlobal); + } + + return result; +} + +// Lambda +QScriptValue BaseScriptEngine::newLambdaFunction(std::function operation, const QScriptValue& data, const QScriptEngine::ValueOwnership& ownership) { + auto lambda = new Lambda(this, operation, data); + auto object = newQObject(lambda, ownership); + auto call = object.property("call"); + call.setPrototype(object); // context->callee().prototype() === Lambda QObject + call.setData(data); // context->callee().data() will === data param + return call; +} +QString Lambda::toString() const { + return QString("[Lambda%1]").arg(data.isValid() ? " " + data.toString() : data.toString()); +} + +Lambda::~Lambda() { +#ifdef DEBUG_JS_LAMBDA_FUNCS + qDebug() << "~Lambda" << "this" << this; +#endif +} + +Lambda::Lambda(QScriptEngine *engine, std::function operation, QScriptValue data) + : engine(engine), operation(operation), data(data) { +#ifdef DEBUG_JS_LAMBDA_FUNCS + qDebug() << "Lambda" << data.toString(); +#endif +} +QScriptValue Lambda::call() { + if (!BaseScriptEngine::IS_THREADSAFE_INVOCATION(engine->thread(), __FUNCTION__)) { + return BaseScriptEngine::unboundNullValue(); + } + return operation(engine->currentContext(), engine); +} + +QScriptValue makeScopedHandlerObject(QScriptValue scopeOrCallback, QScriptValue methodOrName) { + auto engine = scopeOrCallback.engine(); + if (!engine) { + return scopeOrCallback; + } + auto scope = QScriptValue(); + auto callback = scopeOrCallback; + if (scopeOrCallback.isObject()) { + if (methodOrName.isString()) { + scope = scopeOrCallback; + callback = scope.property(methodOrName.toString()); + } else if (methodOrName.isFunction()) { + scope = scopeOrCallback; + callback = methodOrName; + } + } + auto handler = engine->newObject(); + handler.setProperty("scope", scope); + handler.setProperty("callback", callback); + return handler; +} + +QScriptValue callScopedHandlerObject(QScriptValue handler, QScriptValue err, QScriptValue result) { + return handler.property("callback").call(handler.property("scope"), QScriptValueList({ err, result })); +} + +#ifdef DEBUG_JS +void BaseScriptEngine::_debugDump(const QString& header, const QScriptValue& object, const QString& footer) { + if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) { + return; + } + if (!header.isEmpty()) { + qCDebug(shared) << header; + } + if (!object.isObject()) { + qCDebug(shared) << "(!isObject)" << object.toVariant().toString() << object.toString(); + return; + } + QScriptValueIterator it(object); + while (it.hasNext()) { + it.next(); + qCDebug(shared) << it.name() << ":" << it.value().toString(); + } + if (!footer.isEmpty()) { + qCDebug(shared) << footer; + } +} +#endif diff --git a/libraries/shared/src/BaseScriptEngine.h b/libraries/shared/src/BaseScriptEngine.h new file mode 100644 index 0000000000..138e46fafa --- /dev/null +++ b/libraries/shared/src/BaseScriptEngine.h @@ -0,0 +1,90 @@ +// +// BaseScriptEngine.h +// libraries/script-engine/src +// +// Created by Timothy Dedischew on 02/01/17. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_BaseScriptEngine_h +#define hifi_BaseScriptEngine_h + +#include +#include +#include + +// common base class for extending QScriptEngine itself +class BaseScriptEngine : public QScriptEngine, public QEnableSharedFromThis { + Q_OBJECT +public: + static const QString SCRIPT_EXCEPTION_FORMAT; + static const QString SCRIPT_BACKTRACE_SEP; + + // threadsafe "unbound" version of QScriptEngine::nullValue() + static const QScriptValue unboundNullValue() { return QScriptValue(0, QScriptValue::NullValue); } + + BaseScriptEngine() {} + + Q_INVOKABLE QScriptValue lintScript(const QString& sourceCode, const QString& fileName, const int lineNumber = 1); + Q_INVOKABLE QScriptValue makeError(const QScriptValue& other = QScriptValue(), const QString& type = "Error"); + Q_INVOKABLE QString formatException(const QScriptValue& exception, bool includeExtendedDetails); + + QScriptValue cloneUncaughtException(const QString& detail = QString()); + QScriptValue evaluateInClosure(const QScriptValue& locals, const QScriptProgram& program); + + // if there is a pending exception and we are at the top level (non-recursive) stack frame, this emits and resets it + bool maybeEmitUncaughtException(const QString& debugHint = QString()); + + // if the currentContext() is valid then throw the passed exception; otherwise, immediately emit it. + // note: this is used in cases where C++ code might call into JS API methods directly + bool raiseException(const QScriptValue& exception); + + // helper to detect and log warnings when other code invokes QScriptEngine/BaseScriptEngine in thread-unsafe ways + static bool IS_THREADSAFE_INVOCATION(const QThread *thread, const QString& method); +signals: + void unhandledException(const QScriptValue& exception); + +protected: + // like `newFunction`, but allows mapping inline C++ lambdas with captures as callable QScriptValues + // even though the context/engine parameters are redundant in most cases, the function signature matches `newFunction` + // anyway so that newLambdaFunction can be used to rapidly prototype / test utility APIs and then if becoming + // permanent more easily promoted into regular static newFunction scenarios. + QScriptValue newLambdaFunction(std::function operation, const QScriptValue& data = QScriptValue(), const QScriptEngine::ValueOwnership& ownership = QScriptEngine::AutoOwnership); + +#ifdef DEBUG_JS + static void _debugDump(const QString& header, const QScriptValue& object, const QString& footer = QString()); +#endif +}; + +// Standardized CPS callback helpers (see: http://fredkschott.com/post/2014/03/understanding-error-first-callbacks-in-node-js/) +// These two helpers allow async JS APIs that use a callback parameter to be more friendly to scripters by accepting thisObject +// context and adopting a consistent and intuitable callback signature: +// function callback(err, result) { if (err) { ... } else { /* do stuff with result */ } } +// +// To use, first pass the user-specified callback args in the same order used with optionally-scoped Qt signal connections: +// auto handler = makeScopedHandlerObject(scopeOrCallback, optionalMethodOrName); +// And then invoke the scoped handler later per CPS conventions: +// auto result = callScopedHandlerObject(handler, err, result); +QScriptValue makeScopedHandlerObject(QScriptValue scopeOrCallback, QScriptValue methodOrName); +QScriptValue callScopedHandlerObject(QScriptValue handler, QScriptValue err, QScriptValue result); + +// Lambda helps create callable QScriptValues out of std::functions: +// (just meant for use from within the script engine itself) +class Lambda : public QObject { + Q_OBJECT +public: + Lambda(QScriptEngine *engine, std::function operation, QScriptValue data); + ~Lambda(); + public slots: + QScriptValue call(); + QString toString() const; +private: + QScriptEngine* engine; + std::function operation; + QScriptValue data; +}; + +#endif // hifi_BaseScriptEngine_h diff --git a/libraries/shared/src/DebugDraw.cpp b/libraries/shared/src/DebugDraw.cpp index 04759e6187..f17671da4d 100644 --- a/libraries/shared/src/DebugDraw.cpp +++ b/libraries/shared/src/DebugDraw.cpp @@ -10,6 +10,8 @@ #include "DebugDraw.h" #include "SharedUtil.h" +using Lock = std::unique_lock; + DebugDraw& DebugDraw::getInstance() { static DebugDraw* instance = globalInstance("com.highfidelity.DebugDraw"); return *instance; @@ -25,22 +27,50 @@ DebugDraw::~DebugDraw() { // world space line, drawn only once 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)); } -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); } -void DebugDraw::removeMarker(const std::string& key) { +void DebugDraw::removeMarker(const QString& key) { + Lock lock(_mapMutex); _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); } -void DebugDraw::removeMyAvatarMarker(const std::string& key) { +void DebugDraw::removeMyAvatarMarker(const QString& key) { + Lock lock(_mapMutex); _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(); +} diff --git a/libraries/shared/src/DebugDraw.h b/libraries/shared/src/DebugDraw.h index f77e281e06..64327585fb 100644 --- a/libraries/shared/src/DebugDraw.h +++ b/libraries/shared/src/DebugDraw.h @@ -10,6 +10,7 @@ #ifndef hifi_DebugDraw_h #define hifi_DebugDraw_h +#include #include #include #include @@ -17,26 +18,69 @@ #include #include -class DebugDraw { +#include +#include + +/**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: static DebugDraw& getInstance(); DebugDraw(); ~DebugDraw(); - // world space line, drawn only once - void drawRay(const glm::vec3& start, const glm::vec3& end, const glm::vec4& color); + /**jsdoc + * 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. - void addMarker(const std::string& key, const glm::quat& rotation, const glm::vec3& position, const glm::vec4& color); - void removeMarker(const std::string& key); + /**jsdoc + * Adds a debug marker to the world. This marker will be drawn every frame until it is removed with DebugDraw.removeMarker. + * 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. - void addMyAvatarMarker(const std::string& key, const glm::quat& rotation, const glm::vec3& position, const glm::vec4& color); - void removeMyAvatarMarker(const std::string& key); + /**jsdoc + * Removes debug marker from the world. Once a marker is removed, it will no longer be visible. + * @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; - using MarkerMap = std::unordered_map; + using MarkerMap = std::map; using Ray = std::tuple; using Rays = std::vector; @@ -44,16 +88,17 @@ public: // accessors used by renderer // - const MarkerMap& getMarkerMap() const { return _markers; } - const MarkerMap& getMyAvatarMarkerMap() const { return _myAvatarMarkers; } + MarkerMap getMarkerMap() const; + MarkerMap getMyAvatarMarkerMap() const; void updateMyAvatarPos(const glm::vec3& pos) { _myAvatarPos = pos; } const glm::vec3& getMyAvatarPos() const { return _myAvatarPos; } void updateMyAvatarRot(const glm::quat& rot) { _myAvatarRot = rot; } const glm::quat& getMyAvatarRot() const { return _myAvatarRot; } - const Rays getRays() const { return _rays; } - void clearRays() { _rays.clear(); } + Rays getRays() const; + void clearRays(); protected: + mutable std::mutex _mapMutex; MarkerMap _markers; MarkerMap _myAvatarMarkers; glm::quat _myAvatarRot; diff --git a/libraries/shared/src/GLMHelpers.h b/libraries/shared/src/GLMHelpers.h index 4aac913768..deb87930fc 100644 --- a/libraries/shared/src/GLMHelpers.h +++ b/libraries/shared/src/GLMHelpers.h @@ -50,7 +50,7 @@ using glm::quat; // this is where the coordinate system is represented const glm::vec3 IDENTITY_RIGHT = glm::vec3( 1.0f, 0.0f, 0.0f); const glm::vec3 IDENTITY_UP = glm::vec3( 0.0f, 1.0f, 0.0f); -const glm::vec3 IDENTITY_FRONT = glm::vec3( 0.0f, 0.0f,-1.0f); +const glm::vec3 IDENTITY_FORWARD = glm::vec3( 0.0f, 0.0f,-1.0f); glm::quat safeMix(const glm::quat& q1, const glm::quat& q2, float alpha); @@ -245,4 +245,53 @@ inline bool isNaN(const glm::quat& value) { return isNaN(value.w) || isNaN(value glm::mat4 orthoInverse(const glm::mat4& m); +// +// Safe replacement of glm_mat4_mul() for unaligned arguments instead of __m128 +// +inline void glm_mat4u_mul(const glm::mat4& m1, const glm::mat4& m2, glm::mat4& r) { + +#if GLM_ARCH & GLM_ARCH_SSE2_BIT + __m128 u0 = _mm_loadu_ps((float*)&m1[0][0]); + __m128 u1 = _mm_loadu_ps((float*)&m1[1][0]); + __m128 u2 = _mm_loadu_ps((float*)&m1[2][0]); + __m128 u3 = _mm_loadu_ps((float*)&m1[3][0]); + + __m128 v0 = _mm_loadu_ps((float*)&m2[0][0]); + __m128 v1 = _mm_loadu_ps((float*)&m2[1][0]); + __m128 v2 = _mm_loadu_ps((float*)&m2[2][0]); + __m128 v3 = _mm_loadu_ps((float*)&m2[3][0]); + + __m128 t0 = _mm_mul_ps(_mm_shuffle_ps(v0, v0, _MM_SHUFFLE(0,0,0,0)), u0); + __m128 t1 = _mm_mul_ps(_mm_shuffle_ps(v0, v0, _MM_SHUFFLE(1,1,1,1)), u1); + __m128 t2 = _mm_mul_ps(_mm_shuffle_ps(v0, v0, _MM_SHUFFLE(2,2,2,2)), u2); + __m128 t3 = _mm_mul_ps(_mm_shuffle_ps(v0, v0, _MM_SHUFFLE(3,3,3,3)), u3); + v0 = _mm_add_ps(_mm_add_ps(t0, t1), _mm_add_ps(t2, t3)); + + t0 = _mm_mul_ps(_mm_shuffle_ps(v1, v1, _MM_SHUFFLE(0,0,0,0)), u0); + t1 = _mm_mul_ps(_mm_shuffle_ps(v1, v1, _MM_SHUFFLE(1,1,1,1)), u1); + t2 = _mm_mul_ps(_mm_shuffle_ps(v1, v1, _MM_SHUFFLE(2,2,2,2)), u2); + t3 = _mm_mul_ps(_mm_shuffle_ps(v1, v1, _MM_SHUFFLE(3,3,3,3)), u3); + v1 = _mm_add_ps(_mm_add_ps(t0, t1), _mm_add_ps(t2, t3)); + + t0 = _mm_mul_ps(_mm_shuffle_ps(v2, v2, _MM_SHUFFLE(0,0,0,0)), u0); + t1 = _mm_mul_ps(_mm_shuffle_ps(v2, v2, _MM_SHUFFLE(1,1,1,1)), u1); + t2 = _mm_mul_ps(_mm_shuffle_ps(v2, v2, _MM_SHUFFLE(2,2,2,2)), u2); + t3 = _mm_mul_ps(_mm_shuffle_ps(v2, v2, _MM_SHUFFLE(3,3,3,3)), u3); + v2 = _mm_add_ps(_mm_add_ps(t0, t1), _mm_add_ps(t2, t3)); + + t0 = _mm_mul_ps(_mm_shuffle_ps(v3, v3, _MM_SHUFFLE(0,0,0,0)), u0); + t1 = _mm_mul_ps(_mm_shuffle_ps(v3, v3, _MM_SHUFFLE(1,1,1,1)), u1); + t2 = _mm_mul_ps(_mm_shuffle_ps(v3, v3, _MM_SHUFFLE(2,2,2,2)), u2); + t3 = _mm_mul_ps(_mm_shuffle_ps(v3, v3, _MM_SHUFFLE(3,3,3,3)), u3); + v3 = _mm_add_ps(_mm_add_ps(t0, t1), _mm_add_ps(t2, t3)); + + _mm_storeu_ps((float*)&r[0][0], v0); + _mm_storeu_ps((float*)&r[1][0], v1); + _mm_storeu_ps((float*)&r[2][0], v2); + _mm_storeu_ps((float*)&r[3][0], v3); +#else + r = m1 * m2; +#endif +} + #endif // hifi_GLMHelpers_h diff --git a/libraries/shared/src/HifiConfigVariantMap.cpp b/libraries/shared/src/HifiConfigVariantMap.cpp index 5be6b2cd74..d0fb14e104 100644 --- a/libraries/shared/src/HifiConfigVariantMap.cpp +++ b/libraries/shared/src/HifiConfigVariantMap.cpp @@ -21,7 +21,7 @@ #include #include -#include "ServerPathUtils.h" +#include "PathUtils.h" #include "SharedLogging.h" QVariantMap HifiConfigVariantMap::mergeCLParametersWithJSONConfig(const QStringList& argumentList) { @@ -127,7 +127,7 @@ void HifiConfigVariantMap::loadConfig(const QStringList& argumentList) { _userConfigFilename = argumentList[userConfigIndex + 1]; } else { // we weren't passed a user config path - _userConfigFilename = ServerPathUtils::getDataFilePath(USER_CONFIG_FILE_NAME); + _userConfigFilename = PathUtils::getAppDataFilePath(USER_CONFIG_FILE_NAME); // as of 1/19/2016 this path was moved so we attempt a migration for first run post migration here @@ -153,7 +153,7 @@ void HifiConfigVariantMap::loadConfig(const QStringList& argumentList) { // we have the old file and not the new file - time to copy the file // make the destination directory if it doesn't exist - auto dataDirectory = ServerPathUtils::getDataDirectory(); + auto dataDirectory = PathUtils::getAppDataPath(); if (QDir().mkpath(dataDirectory)) { if (oldConfigFile.copy(_userConfigFilename)) { qCDebug(shared) << "Migrated config file from" << oldConfigFilename << "to" << _userConfigFilename; diff --git a/libraries/shared/src/PathUtils.cpp b/libraries/shared/src/PathUtils.cpp index 016b9ccfd6..6e3acc5e99 100644 --- a/libraries/shared/src/PathUtils.cpp +++ b/libraries/shared/src/PathUtils.cpp @@ -18,7 +18,7 @@ #include #include "PathUtils.h" #include - +#include // std::once const QString& PathUtils::resourcesPath() { #ifdef Q_OS_MAC @@ -30,18 +30,20 @@ const QString& PathUtils::resourcesPath() { return staticResourcePath; } -QString PathUtils::getRootDataDirectory() { - auto dataPath = QStandardPaths::writableLocation(QStandardPaths::HomeLocation); +QString PathUtils::getAppDataPath() { + return QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + "/"; +} -#ifdef Q_OS_WIN - dataPath += "/AppData/Roaming/"; -#elif defined(Q_OS_OSX) - dataPath += "/Library/Application Support/"; -#else - dataPath += "/.local/share/"; -#endif +QString PathUtils::getAppLocalDataPath() { + return QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation) + "/"; +} - return dataPath; +QString PathUtils::getAppDataFilePath(const QString& filename) { + return QDir(getAppDataPath()).absoluteFilePath(filename); +} + +QString PathUtils::getAppLocalDataFilePath(const QString& filename) { + return QDir(getAppLocalDataPath()).absoluteFilePath(filename); } QString fileNameWithoutExtension(const QString& fileName, const QVector possibleExtensions) { @@ -82,3 +84,28 @@ QUrl defaultScriptsLocation() { QFileInfo fileInfo(path); return QUrl::fromLocalFile(fileInfo.canonicalFilePath()); } + + +QString PathUtils::stripFilename(const QUrl& url) { + // Guard against meaningless query and fragment parts. + // Do NOT use PreferLocalFile as its behavior is unpredictable (e.g., on defaultScriptsLocation()) + return url.toString(QUrl::RemoveFilename | QUrl::RemoveQuery | QUrl::RemoveFragment); +} + +Qt::CaseSensitivity PathUtils::getFSCaseSensitivity() { + static Qt::CaseSensitivity sensitivity { Qt::CaseSensitive }; + static std::once_flag once; + std::call_once(once, [&] { + QString path = defaultScriptsLocation().toLocalFile(); + QFileInfo upperFI(path.toUpper()); + QFileInfo lowerFI(path.toLower()); + sensitivity = (upperFI == lowerFI) ? Qt::CaseInsensitive : Qt::CaseSensitive; + }); + return sensitivity; +} + +bool PathUtils::isDescendantOf(const QUrl& childURL, const QUrl& parentURL) { + QString child = stripFilename(childURL); + QString parent = stripFilename(parentURL); + return child.startsWith(parent, PathUtils::getFSCaseSensitivity()); +} diff --git a/libraries/shared/src/PathUtils.h b/libraries/shared/src/PathUtils.h index 546586fb64..a7af44221c 100644 --- a/libraries/shared/src/PathUtils.h +++ b/libraries/shared/src/PathUtils.h @@ -27,7 +27,17 @@ class PathUtils : public QObject, public Dependency { Q_PROPERTY(QString resources READ resourcesPath) public: static const QString& resourcesPath(); - static QString getRootDataDirectory(); + + static QString getAppDataPath(); + static QString getAppLocalDataPath(); + + static QString getAppDataFilePath(const QString& filename); + static QString getAppLocalDataFilePath(const QString& filename); + + static Qt::CaseSensitivity getFSCaseSensitivity(); + static QString stripFilename(const QUrl& url); + // note: this is FS-case-sensitive version of parentURL.isParentOf(childURL) + static bool isDescendantOf(const QUrl& childURL, const QUrl& parentURL); }; QString fileNameWithoutExtension(const QString& fileName, const QVector possibleExtensions); diff --git a/libraries/shared/src/PointerEvent.cpp b/libraries/shared/src/PointerEvent.cpp index ed9acb9ada..7ec5e78b9f 100644 --- a/libraries/shared/src/PointerEvent.cpp +++ b/libraries/shared/src/PointerEvent.cpp @@ -25,9 +25,9 @@ PointerEvent::PointerEvent() { } PointerEvent::PointerEvent(EventType type, uint32_t id, - const glm::vec2& pos2D, const glm::vec3& pos3D, - const glm::vec3& normal, const glm::vec3& direction, - Button button, uint32_t buttons) : + const glm::vec2& pos2D, const glm::vec3& pos3D, + const glm::vec3& normal, const glm::vec3& direction, + Button button, uint32_t buttons, Qt::KeyboardModifiers keyboardModifiers) : _type(type), _id(id), _pos2D(pos2D), @@ -35,7 +35,8 @@ PointerEvent::PointerEvent(EventType type, uint32_t id, _normal(normal), _direction(direction), _button(button), - _buttons(buttons) + _buttons(buttons), + _keyboardModifiers(keyboardModifiers) { ; } @@ -47,6 +48,9 @@ QScriptValue PointerEvent::toScriptValue(QScriptEngine* engine, const PointerEve case Press: obj.setProperty("type", "Press"); break; + case DoublePress: + obj.setProperty("type", "DoublePress"); + break; case Release: obj.setProperty("type", "Release"); break; @@ -119,6 +123,8 @@ QScriptValue PointerEvent::toScriptValue(QScriptEngine* engine, const PointerEve obj.setProperty("isSecondaryHeld", areFlagsSet(event._buttons, SecondaryButton)); obj.setProperty("isTertiaryHeld", areFlagsSet(event._buttons, TertiaryButton)); + obj.setProperty("keyboardModifiers", QScriptValue(event.getKeyboardModifiers())); + return obj; } @@ -128,6 +134,8 @@ void PointerEvent::fromScriptValue(const QScriptValue& object, PointerEvent& eve QString typeStr = type.isString() ? type.toString() : "Move"; if (typeStr == "Press") { event._type = Press; + } else if (typeStr == "DoublePress") { + event._type = DoublePress; } else if (typeStr == "Release") { event._type = Release; } else { @@ -135,7 +143,7 @@ void PointerEvent::fromScriptValue(const QScriptValue& object, PointerEvent& eve } QScriptValue id = object.property("id"); - event._id = type.isNumber() ? (uint32_t)type.toNumber() : 0; + event._id = id.isNumber() ? (uint32_t)id.toNumber() : 0; glm::vec2 pos2D; vec2FromScriptValue(object.property("pos2D"), event._pos2D); @@ -150,7 +158,8 @@ void PointerEvent::fromScriptValue(const QScriptValue& object, PointerEvent& eve vec3FromScriptValue(object.property("direction"), event._direction); QScriptValue button = object.property("button"); - QString buttonStr = type.isString() ? type.toString() : "NoButtons"; + QString buttonStr = type.isString() ? button.toString() : "NoButtons"; + if (buttonStr == "Primary") { event._button = PrimaryButton; } else if (buttonStr == "Secondary") { @@ -174,5 +183,30 @@ void PointerEvent::fromScriptValue(const QScriptValue& object, PointerEvent& eve if (tertiary) { event._buttons |= TertiaryButton; } + + event._keyboardModifiers = (Qt::KeyboardModifiers)(object.property("keyboardModifiers").toUInt32()); } } + +static const char* typeToStringMap[PointerEvent::NumEventTypes] = { "Press", "DoublePress", "Release", "Move" }; +static const char* buttonsToStringMap[8] = { + "NoButtons", + "PrimaryButton", + "SecondaryButton", + "PrimaryButton | SecondaryButton", + "TertiaryButton", + "PrimaryButton | TertiaryButton", + "SecondaryButton | TertiaryButton", + "PrimaryButton | SecondaryButton | TertiaryButton", +}; + +QDebug& operator<<(QDebug& dbg, const PointerEvent& p) { + dbg.nospace() << "PointerEvent, type = " << typeToStringMap[p.getType()] << ", id = " << p.getID(); + dbg.nospace() << ", pos2D = (" << p.getPos2D().x << ", " << p.getPos2D().y; + dbg.nospace() << "), pos3D = (" << p.getPos3D().x << ", " << p.getPos3D().y << ", " << p.getPos3D().z; + dbg.nospace() << "), normal = (" << p.getNormal().x << ", " << p.getNormal().y << ", " << p.getNormal().z; + dbg.nospace() << "), dir = (" << p.getDirection().x << ", " << p.getDirection().y << ", " << p.getDirection().z; + dbg.nospace() << "), button = " << buttonsToStringMap[p.getButton()] << " " << (int)p.getButton(); + dbg.nospace() << ", buttons = " << buttonsToStringMap[p.getButtons()]; + return dbg; +} diff --git a/libraries/shared/src/PointerEvent.h b/libraries/shared/src/PointerEvent.h index 054835c4fc..ab77328fc1 100644 --- a/libraries/shared/src/PointerEvent.h +++ b/libraries/shared/src/PointerEvent.h @@ -12,6 +12,8 @@ #ifndef hifi_PointerEvent_h #define hifi_PointerEvent_h +#include + #include #include #include @@ -26,16 +28,18 @@ public: }; enum EventType { - Press, // A button has just been pressed - Release, // A button has just been released - Move // The pointer has just moved + Press, // A button has just been pressed + DoublePress, // A button has just been double pressed + Release, // A button has just been released + Move, // The pointer has just moved + NumEventTypes }; PointerEvent(); PointerEvent(EventType type, uint32_t id, const glm::vec2& pos2D, const glm::vec3& pos3D, const glm::vec3& normal, const glm::vec3& direction, - Button button, uint32_t buttons); + Button button, uint32_t buttons, Qt::KeyboardModifiers keyboardModifiers); static QScriptValue toScriptValue(QScriptEngine* engine, const PointerEvent& event); static void fromScriptValue(const QScriptValue& object, PointerEvent& event); @@ -50,6 +54,7 @@ public: const glm::vec3& getDirection() const { return _direction; } Button getButton() const { return _button; } uint32_t getButtons() const { return _buttons; } + Qt::KeyboardModifiers getKeyboardModifiers() const { return _keyboardModifiers; } private: EventType _type; @@ -59,10 +64,13 @@ private: glm::vec3 _normal; // surface normal glm::vec3 _direction; // incoming direction of pointer ray. - Button _button { NoButtons }; // button assosiated with this event, (if type is Press, this will be the button that is pressed) + Button _button { NoButtons }; // button associated with this event, (if type is Press, this will be the button that is pressed) uint32_t _buttons { NoButtons }; // the current state of all the buttons. + Qt::KeyboardModifiers _keyboardModifiers; // set of keys held when event was generated }; +QDebug& operator<<(QDebug& dbg, const PointerEvent& p); + Q_DECLARE_METATYPE(PointerEvent) #endif // hifi_PointerEvent_h diff --git a/libraries/shared/src/RegisteredMetaTypes.cpp b/libraries/shared/src/RegisteredMetaTypes.cpp index 7f12d6cc00..70067b93f3 100644 --- a/libraries/shared/src/RegisteredMetaTypes.cpp +++ b/libraries/shared/src/RegisteredMetaTypes.cpp @@ -43,6 +43,7 @@ void registerMetaTypes(QScriptEngine* engine) { qScriptRegisterMetaType(engine, qVectorQuatToScriptValue, qVectorQuatFromScriptValue); qScriptRegisterMetaType(engine, qVectorBoolToScriptValue, qVectorBoolFromScriptValue); qScriptRegisterMetaType(engine, qVectorFloatToScriptValue, qVectorFloatFromScriptValue); + qScriptRegisterMetaType(engine, qVectorIntToScriptValue, qVectorIntFromScriptValue); qScriptRegisterMetaType(engine, vec2toScriptValue, vec2FromScriptValue); qScriptRegisterMetaType(engine, quatToScriptValue, quatFromScriptValue); qScriptRegisterMetaType(engine, qRectToScriptValue, qRectFromScriptValue); @@ -386,6 +387,15 @@ QScriptValue qVectorFloatToScriptValue(QScriptEngine* engine, const QVector& vector) { + QScriptValue array = engine->newArray(); + for (int i = 0; i < vector.size(); i++) { + int num = vector.at(i); + array.setProperty(i, QScriptValue(num)); + } + return array; +} + void qVectorFloatFromScriptValue(const QScriptValue& array, QVector& vector) { int length = array.property("length").toInteger(); @@ -393,6 +403,15 @@ void qVectorFloatFromScriptValue(const QScriptValue& array, QVector& vect vector << array.property(i).toVariant().toFloat(); } } + +void qVectorIntFromScriptValue(const QScriptValue& array, QVector& vector) { + int length = array.property("length").toInteger(); + + for (int i = 0; i < length; i++) { + vector << array.property(i).toVariant().toInt(); + } +} + // QVector qVectorVec3FromScriptValue(const QScriptValue& array){ QVector newVector; diff --git a/libraries/shared/src/RegisteredMetaTypes.h b/libraries/shared/src/RegisteredMetaTypes.h index 498a8b3b3a..8a15f62eed 100644 --- a/libraries/shared/src/RegisteredMetaTypes.h +++ b/libraries/shared/src/RegisteredMetaTypes.h @@ -113,6 +113,10 @@ QScriptValue qVectorFloatToScriptValue(QScriptEngine* engine, const QVector& vector); QVector qVectorFloatFromScriptValue(const QScriptValue& array); +// vector +QScriptValue qVectorIntToScriptValue(QScriptEngine* engine, const QVector& vector); +void qVectorIntFromScriptValue(const QScriptValue& array, QVector& vector); + QVector qVectorQUuidFromScriptValue(const QScriptValue& array); QScriptValue aaCubeToScriptValue(QScriptEngine* engine, const AACube& aaCube); diff --git a/libraries/shared/src/RenderArgs.h b/libraries/shared/src/RenderArgs.h index b2c05b0548..50722c0deb 100644 --- a/libraries/shared/src/RenderArgs.h +++ b/libraries/shared/src/RenderArgs.h @@ -122,6 +122,7 @@ public: gpu::Batch* _batch = nullptr; std::shared_ptr _whiteTexture; + uint32_t _globalShapeKey { 0 }; bool _enableTexturing { true }; RenderDetails _details; diff --git a/libraries/shared/src/ServerPathUtils.cpp b/libraries/shared/src/ServerPathUtils.cpp deleted file mode 100644 index cf52875c5f..0000000000 --- a/libraries/shared/src/ServerPathUtils.cpp +++ /dev/null @@ -1,31 +0,0 @@ -// -// ServerPathUtils.cpp -// libraries/shared/src -// -// Created by Ryan Huffman on 01/12/16. -// Copyright 2016 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// -#include "ServerPathUtils.h" - -#include -#include -#include -#include - -#include "PathUtils.h" - -QString ServerPathUtils::getDataDirectory() { - auto dataPath = PathUtils::getRootDataDirectory(); - - dataPath += qApp->organizationName() + "/" + qApp->applicationName(); - - return QDir::cleanPath(dataPath); -} - -QString ServerPathUtils::getDataFilePath(QString filename) { - return QDir(getDataDirectory()).absoluteFilePath(filename); -} - diff --git a/libraries/shared/src/ServerPathUtils.h b/libraries/shared/src/ServerPathUtils.h deleted file mode 100644 index 28a9a71f0d..0000000000 --- a/libraries/shared/src/ServerPathUtils.h +++ /dev/null @@ -1,22 +0,0 @@ -// -// ServerPathUtils.h -// libraries/shared/src -// -// Created by Ryan Huffman on 01/12/16. -// Copyright 2016 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -#ifndef hifi_ServerPathUtils_h -#define hifi_ServerPathUtils_h - -#include - -namespace ServerPathUtils { - QString getDataDirectory(); - QString getDataFilePath(QString filename); -} - -#endif // hifi_ServerPathUtils_h \ No newline at end of file diff --git a/libraries/shared/src/ShapeInfo.h b/libraries/shared/src/ShapeInfo.h index a6ff8d6d4a..98b397ee16 100644 --- a/libraries/shared/src/ShapeInfo.h +++ b/libraries/shared/src/ShapeInfo.h @@ -45,7 +45,8 @@ enum ShapeType { SHAPE_TYPE_COMPOUND, SHAPE_TYPE_SIMPLE_HULL, SHAPE_TYPE_SIMPLE_COMPOUND, - SHAPE_TYPE_STATIC_MESH + SHAPE_TYPE_STATIC_MESH, + SHAPE_TYPE_ELLIPSOID }; class ShapeInfo { diff --git a/libraries/shared/src/Trace.cpp b/libraries/shared/src/Trace.cpp index 1e3d490a9c..d7feb65ff3 100644 --- a/libraries/shared/src/Trace.cpp +++ b/libraries/shared/src/Trace.cpp @@ -179,7 +179,7 @@ void Tracer::serialize(const QString& originalPath) { { // "traceEvents":[ - // {"args":{"nv_payload":0},"cat":"hifi.render","name":"render::Scene::processPendingChangesQueue","ph":"B","pid":14796,"tid":21636,"ts":68795933487} + // {"args":{"nv_payload":0},"cat":"hifi.render","name":"render::Scene::processTransactionQueue","ph":"B","pid":14796,"tid":21636,"ts":68795933487} QJsonArray traceEvents; diff --git a/libraries/shared/src/TriangleSet.cpp b/libraries/shared/src/TriangleSet.cpp new file mode 100644 index 0000000000..cdb3fd6b2c --- /dev/null +++ b/libraries/shared/src/TriangleSet.cpp @@ -0,0 +1,76 @@ +// +// TriangleSet.cpp +// libraries/entities/src +// +// Created by Brad Hefta-Gaub on 3/2/2017. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include "GLMHelpers.h" +#include "TriangleSet.h" + +void TriangleSet::insert(const Triangle& t) { + _triangles.push_back(t); + + _bounds += t.v0; + _bounds += t.v1; + _bounds += t.v2; +} + +void TriangleSet::clear() { + _triangles.clear(); + _bounds.clear(); +} + +// Determine of the given ray (origin/direction) in model space intersects with any triangles +// in the set. If an intersection occurs, the distance and surface normal will be provided. +bool TriangleSet::findRayIntersection(const glm::vec3& origin, const glm::vec3& direction, + float& distance, BoxFace& face, glm::vec3& surfaceNormal, bool precision) const { + + bool intersectedSomething = false; + float boxDistance = std::numeric_limits::max(); + float bestDistance = std::numeric_limits::max(); + + if (_bounds.findRayIntersection(origin, direction, boxDistance, face, surfaceNormal)) { + if (precision) { + for (const auto& triangle : _triangles) { + float thisTriangleDistance; + if (findRayTriangleIntersection(origin, direction, triangle, thisTriangleDistance)) { + if (thisTriangleDistance < bestDistance) { + bestDistance = thisTriangleDistance; + intersectedSomething = true; + surfaceNormal = triangle.getNormal(); + distance = bestDistance; + } + } + } + } else { + intersectedSomething = true; + distance = boxDistance; + } + } + + return intersectedSomething; +} + + +bool TriangleSet::convexHullContains(const glm::vec3& point) const { + if (!_bounds.contains(point)) { + return false; + } + + bool insideMesh = true; // optimistic + for (const auto& triangle : _triangles) { + if (!isPointBehindTrianglesPlane(point, triangle.v0, triangle.v1, triangle.v2)) { + // it's not behind at least one so we bail + insideMesh = false; + break; + } + + } + return insideMesh; +} + diff --git a/libraries/shared/src/TriangleSet.h b/libraries/shared/src/TriangleSet.h new file mode 100644 index 0000000000..b54f1a642a --- /dev/null +++ b/libraries/shared/src/TriangleSet.h @@ -0,0 +1,41 @@ +// +// TriangleSet.h +// libraries/entities/src +// +// Created by Brad Hefta-Gaub on 3/2/2017. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include + +#include "AABox.h" +#include "GeometryUtil.h" + +class TriangleSet { +public: + void reserve(size_t size) { _triangles.reserve(size); } // reserve space in the datastructure for size number of triangles + size_t size() const { return _triangles.size(); } + + const Triangle& getTriangle(size_t t) const { return _triangles[t]; } + + void insert(const Triangle& t); + void clear(); + + // Determine if the given ray (origin/direction) in model space intersects with any triangles in the set. If an + // intersection occurs, the distance and surface normal will be provided. + bool findRayIntersection(const glm::vec3& origin, const glm::vec3& direction, + float& distance, BoxFace& face, glm::vec3& surfaceNormal, bool precision) const; + + // Determine if a point is "inside" all the triangles of a convex hull. It is the responsibility of the caller to + // determine that the triangle set is indeed a convex hull. If the triangles added to this set are not in fact a + // convex hull, the result of this method is meaningless and undetermined. + bool convexHullContains(const glm::vec3& point) const; + const AABox& getBounds() const { return _bounds; } + +private: + std::vector _triangles; + AABox _bounds; +}; diff --git a/libraries/shared/src/ViewFrustum.cpp b/libraries/shared/src/ViewFrustum.cpp index a0b7d17e46..7e4f64686b 100644 --- a/libraries/shared/src/ViewFrustum.cpp +++ b/libraries/shared/src/ViewFrustum.cpp @@ -31,7 +31,7 @@ void ViewFrustum::setOrientation(const glm::quat& orientationAsQuaternion) { _orientation = orientationAsQuaternion; _right = glm::vec3(orientationAsQuaternion * glm::vec4(IDENTITY_RIGHT, 0.0f)); _up = glm::vec3(orientationAsQuaternion * glm::vec4(IDENTITY_UP, 0.0f)); - _direction = glm::vec3(orientationAsQuaternion * glm::vec4(IDENTITY_FRONT, 0.0f)); + _direction = glm::vec3(orientationAsQuaternion * glm::vec4(IDENTITY_FORWARD, 0.0f)); _view = glm::translate(mat4(), _position) * glm::mat4_cast(_orientation); } diff --git a/libraries/shared/src/ViewFrustum.h b/libraries/shared/src/ViewFrustum.h index 9a6cb9ab68..221b0b5a07 100644 --- a/libraries/shared/src/ViewFrustum.h +++ b/libraries/shared/src/ViewFrustum.h @@ -153,7 +153,7 @@ private: glm::quat _orientation; // orientation in world-frame // calculated from orientation - glm::vec3 _direction = IDENTITY_FRONT; + glm::vec3 _direction = IDENTITY_FORWARD; glm::vec3 _up = IDENTITY_UP; glm::vec3 _right = IDENTITY_RIGHT; diff --git a/libraries/shared/src/shared/GlobalAppProperties.cpp b/libraries/shared/src/shared/GlobalAppProperties.cpp index f2d8990708..b0ba0bf83d 100644 --- a/libraries/shared/src/shared/GlobalAppProperties.cpp +++ b/libraries/shared/src/shared/GlobalAppProperties.cpp @@ -16,6 +16,7 @@ namespace hifi { namespace properties { const char* OCULUS_STORE = "com.highfidelity.oculusStore"; const char* TEST = "com.highfidelity.test"; const char* TRACING = "com.highfidelity.tracing"; + const char* HMD = "com.highfidelity.hmd"; namespace gl { const char* BACKEND = "com.highfidelity.gl.backend"; diff --git a/libraries/shared/src/shared/GlobalAppProperties.h b/libraries/shared/src/shared/GlobalAppProperties.h index 609f2afd94..b1811586ba 100644 --- a/libraries/shared/src/shared/GlobalAppProperties.h +++ b/libraries/shared/src/shared/GlobalAppProperties.h @@ -18,6 +18,7 @@ namespace hifi { namespace properties { extern const char* OCULUS_STORE; extern const char* TEST; extern const char* TRACING; + extern const char* HMD; namespace gl { extern const char* BACKEND; diff --git a/libraries/shared/src/shared/RateCounter.h b/libraries/shared/src/shared/RateCounter.h index d04d87493a..3cf509b6bf 100644 --- a/libraries/shared/src/shared/RateCounter.h +++ b/libraries/shared/src/shared/RateCounter.h @@ -24,29 +24,34 @@ public: RateCounter() { _rate = 0; } // avoid use of std::atomic copy ctor void increment(size_t count = 1) { - auto now = usecTimestampNow(); - float currentIntervalMs = (now - _start) / (float) USECS_PER_MSEC; - if (currentIntervalMs > (float) INTERVAL) { - float currentCount = _count; - float intervalSeconds = currentIntervalMs / (float) MSECS_PER_SECOND; - _rate = roundf(currentCount / intervalSeconds * _scale) / _scale; - _start = now; - _count = 0; - }; + checkRate(); _count += count; } - float rate() const { return _rate; } + float rate() const { checkRate(); return _rate; } uint8_t precision() const { return PRECISION; } uint32_t interval() const { return INTERVAL; } private: - uint64_t _start { usecTimestampNow() }; - size_t _count { 0 }; + mutable uint64_t _start { usecTimestampNow() }; + mutable size_t _count { 0 }; const float _scale { powf(10, PRECISION) }; - std::atomic _rate; + mutable std::atomic _rate; + + void checkRate() const { + auto now = usecTimestampNow(); + float currentIntervalMs = (now - _start) / (float)USECS_PER_MSEC; + if (currentIntervalMs > (float)INTERVAL) { + float currentCount = _count; + float intervalSeconds = currentIntervalMs / (float)MSECS_PER_SECOND; + _rate = roundf(currentCount / intervalSeconds * _scale) / _scale; + _start = now; + _count = 0; + }; + } + }; #endif diff --git a/libraries/shared/src/shared/Storage.cpp b/libraries/shared/src/shared/Storage.cpp new file mode 100644 index 0000000000..3c46347a49 --- /dev/null +++ b/libraries/shared/src/shared/Storage.cpp @@ -0,0 +1,92 @@ +// +// Created by Bradley Austin Davis on 2016/02/17 +// Copyright 2013-2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include "Storage.h" + +#include +#include +#include + +Q_LOGGING_CATEGORY(storagelogging, "hifi.core.storage") + +using namespace storage; + +ViewStorage::ViewStorage(const storage::StoragePointer& owner, size_t size, const uint8_t* data) + : _owner(owner), _size(size), _data(data) {} + +StoragePointer Storage::createView(size_t viewSize, size_t offset) const { + auto selfSize = size(); + if (0 == viewSize) { + viewSize = selfSize; + } + if ((viewSize + offset) > selfSize) { + throw std::runtime_error("Invalid mapping range"); + } + return std::make_shared(shared_from_this(), viewSize, data() + offset); +} + +StoragePointer Storage::toMemoryStorage() const { + return std::make_shared(size(), data()); +} + +StoragePointer Storage::toFileStorage(const QString& filename) const { + return FileStorage::create(filename, size(), data()); +} + +MemoryStorage::MemoryStorage(size_t size, const uint8_t* data) { + _data.resize(size); + if (data) { + memcpy(_data.data(), data, size); + } +} + +StoragePointer FileStorage::create(const QString& filename, size_t size, const uint8_t* data) { + QFile file(filename); + if (!file.open(QFile::ReadWrite | QIODevice::Truncate)) { + throw std::runtime_error("Unable to open file for writing"); + } + if (!file.resize(size)) { + throw std::runtime_error("Unable to resize file"); + } + { + auto mapped = file.map(0, size); + if (!mapped) { + throw std::runtime_error("Unable to map file"); + } + memcpy(mapped, data, size); + if (!file.unmap(mapped)) { + throw std::runtime_error("Unable to unmap file"); + } + } + file.close(); + return std::make_shared(filename); +} + +FileStorage::FileStorage(const QString& filename) : _file(filename) { + if (_file.open(QFile::ReadOnly)) { + _mapped = _file.map(0, _file.size()); + if (_mapped) { + _valid = true; + } else { + qCWarning(storagelogging) << "Failed to map file " << filename; + } + } else { + qCWarning(storagelogging) << "Failed to open file " << filename; + } +} + +FileStorage::~FileStorage() { + if (_mapped) { + if (!_file.unmap(_mapped)) { + throw std::runtime_error("Unable to unmap file"); + } + } + if (_file.isOpen()) { + _file.close(); + } +} diff --git a/libraries/shared/src/shared/Storage.h b/libraries/shared/src/shared/Storage.h new file mode 100644 index 0000000000..306984040f --- /dev/null +++ b/libraries/shared/src/shared/Storage.h @@ -0,0 +1,82 @@ +// +// Created by Bradley Austin Davis on 2016/02/17 +// Copyright 2013-2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#pragma once +#ifndef hifi_Storage_h +#define hifi_Storage_h + +#include +#include +#include +#include +#include + +namespace storage { + class Storage; + using StoragePointer = std::shared_ptr; + + class Storage : public std::enable_shared_from_this { + public: + virtual ~Storage() {} + virtual const uint8_t* data() const = 0; + virtual size_t size() const = 0; + virtual operator bool() const { return true; } + + StoragePointer createView(size_t size = 0, size_t offset = 0) const; + StoragePointer toFileStorage(const QString& filename) const; + StoragePointer toMemoryStorage() const; + + // Aliases to prevent having to re-write a ton of code + inline size_t getSize() const { return size(); } + inline const uint8_t* readData() const { return data(); } + }; + + class MemoryStorage : public Storage { + public: + MemoryStorage(size_t size, const uint8_t* data = nullptr); + const uint8_t* data() const override { return _data.data(); } + uint8_t* data() { return _data.data(); } + size_t size() const override { return _data.size(); } + operator bool() const override { return true; } + private: + std::vector _data; + }; + + class FileStorage : public Storage { + public: + static StoragePointer create(const QString& filename, size_t size, const uint8_t* data); + FileStorage(const QString& filename); + ~FileStorage(); + // Prevent copying + FileStorage(const FileStorage& other) = delete; + FileStorage& operator=(const FileStorage& other) = delete; + + const uint8_t* data() const override { return _mapped; } + size_t size() const override { return _file.size(); } + operator bool() const override { return _valid; } + private: + bool _valid { false }; + QFile _file; + uint8_t* _mapped { nullptr }; + }; + + class ViewStorage : public Storage { + public: + ViewStorage(const storage::StoragePointer& owner, size_t size, const uint8_t* data); + const uint8_t* data() const override { return _data; } + size_t size() const override { return _size; } + operator bool() const override { return *_owner; } + private: + const storage::StoragePointer _owner; + const size_t _size; + const uint8_t* _data; + }; + +} + +#endif // hifi_Storage_h diff --git a/libraries/ui/CMakeLists.txt b/libraries/ui/CMakeLists.txt index cc2382926f..f2b48446fe 100644 --- a/libraries/ui/CMakeLists.txt +++ b/libraries/ui/CMakeLists.txt @@ -1,3 +1,3 @@ set(TARGET_NAME ui) setup_hifi_library(OpenGL Network Qml Quick Script WebChannel WebSockets XmlPatterns) -link_hifi_libraries(shared networking gl) +link_hifi_libraries(shared networking gl script-engine) diff --git a/libraries/ui/src/OffscreenUi.cpp b/libraries/ui/src/OffscreenUi.cpp index 7724a409f0..1cb9045e79 100644 --- a/libraries/ui/src/OffscreenUi.cpp +++ b/libraries/ui/src/OffscreenUi.cpp @@ -19,7 +19,8 @@ #include #include - +#include +#include #include "FileDialogHelper.h" #include "VrMenu.h" @@ -210,9 +211,20 @@ QQuickItem* OffscreenUi::createMessageBox(Icon icon, const QString& title, const map.insert("buttons", buttons.operator int()); map.insert("defaultButton", defaultButton); QVariant result; - bool invokeResult = QMetaObject::invokeMethod(_desktop, "messageBox", - Q_RETURN_ARG(QVariant, result), - Q_ARG(QVariant, QVariant::fromValue(map))); + bool invokeResult; + auto tabletScriptingInterface = DependencyManager::get(); + TabletProxy* tablet = dynamic_cast(tabletScriptingInterface->getTablet("com.highfidelity.interface.tablet.system")); + if (tablet->getToolbarMode()) { + invokeResult = QMetaObject::invokeMethod(_desktop, "messageBox", + Q_RETURN_ARG(QVariant, result), + Q_ARG(QVariant, QVariant::fromValue(map))); + } else { + QQuickItem* tabletRoot = tablet->getTabletRoot(); + invokeResult = QMetaObject::invokeMethod(tabletRoot, "messageBox", + Q_RETURN_ARG(QVariant, result), + Q_ARG(QVariant, QVariant::fromValue(map))); + emit tabletScriptingInterface->tabletNotification(); + } if (!invokeResult) { qWarning() << "Failed to create message box"; @@ -405,10 +417,22 @@ QQuickItem* OffscreenUi::createInputDialog(const Icon icon, const QString& title map.insert("label", label); map.insert("current", current); QVariant result; - bool invokeResult = QMetaObject::invokeMethod(_desktop, "inputDialog", - Q_RETURN_ARG(QVariant, result), - Q_ARG(QVariant, QVariant::fromValue(map))); + auto tabletScriptingInterface = DependencyManager::get(); + TabletProxy* tablet = dynamic_cast(tabletScriptingInterface->getTablet("com.highfidelity.interface.tablet.system")); + + bool invokeResult; + if (tablet->getToolbarMode()) { + invokeResult = QMetaObject::invokeMethod(_desktop, "inputDialog", + Q_RETURN_ARG(QVariant, result), + Q_ARG(QVariant, QVariant::fromValue(map))); + } else { + QQuickItem* tabletRoot = tablet->getTabletRoot(); + invokeResult = QMetaObject::invokeMethod(tabletRoot, "inputDialog", + Q_RETURN_ARG(QVariant, result), + Q_ARG(QVariant, QVariant::fromValue(map))); + emit tabletScriptingInterface->tabletNotification(); + } if (!invokeResult) { qWarning() << "Failed to create message box"; return nullptr; @@ -422,10 +446,22 @@ QQuickItem* OffscreenUi::createCustomInputDialog(const Icon icon, const QString& map.insert("title", title); map.insert("icon", icon); QVariant result; - bool invokeResult = QMetaObject::invokeMethod(_desktop, "customInputDialog", - Q_RETURN_ARG(QVariant, result), - Q_ARG(QVariant, QVariant::fromValue(map))); + auto tabletScriptingInterface = DependencyManager::get(); + TabletProxy* tablet = dynamic_cast(tabletScriptingInterface->getTablet("com.highfidelity.interface.tablet.system")); + bool invokeResult; + if (tablet->getToolbarMode()) { + invokeResult = QMetaObject::invokeMethod(_desktop, "inputDialog", + Q_RETURN_ARG(QVariant, result), + Q_ARG(QVariant, QVariant::fromValue(map))); + } else { + QQuickItem* tabletRoot = tablet->getTabletRoot(); + invokeResult = QMetaObject::invokeMethod(tabletRoot, "inputDialog", + Q_RETURN_ARG(QVariant, result), + Q_ARG(QVariant, QVariant::fromValue(map))); + emit tabletScriptingInterface->tabletNotification(); + } + if (!invokeResult) { qWarning() << "Failed to create custom message box"; return nullptr; @@ -569,9 +605,20 @@ private slots: QString OffscreenUi::fileDialog(const QVariantMap& properties) { QVariant buildDialogResult; - bool invokeResult = QMetaObject::invokeMethod(_desktop, "fileDialog", - Q_RETURN_ARG(QVariant, buildDialogResult), - Q_ARG(QVariant, QVariant::fromValue(properties))); + bool invokeResult; + auto tabletScriptingInterface = DependencyManager::get(); + TabletProxy* tablet = dynamic_cast(tabletScriptingInterface->getTablet("com.highfidelity.interface.tablet.system")); + if (tablet->getToolbarMode()) { + invokeResult = QMetaObject::invokeMethod(_desktop, "fileDialog", + Q_RETURN_ARG(QVariant, buildDialogResult), + Q_ARG(QVariant, QVariant::fromValue(properties))); + } else { + QQuickItem* tabletRoot = tablet->getTabletRoot(); + invokeResult = QMetaObject::invokeMethod(tabletRoot, "fileDialog", + Q_RETURN_ARG(QVariant, buildDialogResult), + Q_ARG(QVariant, QVariant::fromValue(properties))); + emit tabletScriptingInterface->tabletNotification(); + } if (!invokeResult) { qWarning() << "Failed to create file open dialog"; diff --git a/libraries/ui/src/QmlWindowClass.cpp b/libraries/ui/src/QmlWindowClass.cpp index c60e4fa698..c0e94058ae 100644 --- a/libraries/ui/src/QmlWindowClass.cpp +++ b/libraries/ui/src/QmlWindowClass.cpp @@ -122,12 +122,15 @@ void QmlWindowClass::initQml(QVariantMap properties) { object->setProperty(OFFSCREEN_VISIBILITY_PROPERTY, visible); object->setProperty(SOURCE_PROPERTY, _source); + const QMetaObject *metaObject = _qmlWindow->metaObject(); // Forward messages received from QML on to the script connect(_qmlWindow, SIGNAL(sendToScript(QVariant)), this, SLOT(qmlToScript(const QVariant&)), Qt::QueuedConnection); connect(_qmlWindow, SIGNAL(visibleChanged()), this, SIGNAL(visibleChanged()), Qt::QueuedConnection); - connect(_qmlWindow, SIGNAL(resized(QSizeF)), this, SIGNAL(resized(QSizeF)), Qt::QueuedConnection); - connect(_qmlWindow, SIGNAL(moved(QVector2D)), this, SLOT(hasMoved(QVector2D)), Qt::QueuedConnection); + if (metaObject->indexOfSignal("resized") >= 0) + connect(_qmlWindow, SIGNAL(resized(QSizeF)), this, SIGNAL(resized(QSizeF)), Qt::QueuedConnection); + if (metaObject->indexOfSignal("moved") >= 0) + connect(_qmlWindow, SIGNAL(moved(QVector2D)), this, SLOT(hasMoved(QVector2D)), Qt::QueuedConnection); connect(_qmlWindow, SIGNAL(windowClosed()), this, SLOT(hasClosed()), Qt::QueuedConnection); }); } diff --git a/libraries/ui/src/ui/Menu.cpp b/libraries/ui/src/ui/Menu.cpp index f68fff0204..50833e90fc 100644 --- a/libraries/ui/src/ui/Menu.cpp +++ b/libraries/ui/src/ui/Menu.cpp @@ -470,11 +470,11 @@ void Menu::removeSeparator(const QString& menuName, const QString& separatorName if (menu) { int textAt = findPositionOfMenuItem(menu, separatorName); QList menuActions = menu->actions(); - QAction* separatorText = menuActions[textAt]; if (textAt > 0 && textAt < menuActions.size()) { QAction* separatorLine = menuActions[textAt - 1]; if (separatorLine) { if (separatorLine->isSeparator()) { + QAction* separatorText = menuActions[textAt]; menu->removeAction(separatorText); menu->removeAction(separatorLine); separatorRemoved = true; diff --git a/plugins/hifiKinect/src/KinectPlugin.cpp b/plugins/hifiKinect/src/KinectPlugin.cpp index 0bff69ed57..6d29a261dd 100644 --- a/plugins/hifiKinect/src/KinectPlugin.cpp +++ b/plugins/hifiKinect/src/KinectPlugin.cpp @@ -113,16 +113,16 @@ static controller::StandardPoseChannel KinectJointIndexToPoseIndexMap[KinectJoin controller::RIGHT_FORE_ARM, controller::RIGHT_HAND, - controller::RIGHT_UP_LEG, // hip socket - controller::RIGHT_LEG, // knee? - controller::RIGHT_FOOT, // ankle? - UNKNOWN_JOINT, // ???? - controller::LEFT_UP_LEG, // hip socket controller::LEFT_LEG, // knee? controller::LEFT_FOOT, // ankle? UNKNOWN_JOINT, // ???? + controller::RIGHT_UP_LEG, // hip socket + controller::RIGHT_LEG, // knee? + controller::RIGHT_FOOT, // ankle? + UNKNOWN_JOINT, // ???? + UNKNOWN_JOINT, /* SpineShoulder */ controller::LEFT_HAND_INDEX4, @@ -130,7 +130,6 @@ static controller::StandardPoseChannel KinectJointIndexToPoseIndexMap[KinectJoin controller::RIGHT_HAND_INDEX4, controller::RIGHT_HAND_THUMB4, - }; // in rig frame @@ -228,7 +227,8 @@ void KinectPlugin::init() { { auto getter = [this]()->bool { return _enabled; }; auto setter = [this](bool value) { - _enabled = value; saveSettings(); + _enabled = value; + saveSettings(); if (!_enabled) { auto userInputMapper = DependencyManager::get(); userInputMapper->withLock([&, this]() { @@ -239,6 +239,15 @@ void KinectPlugin::init() { auto preference = new CheckPreference(KINECT_PLUGIN, "Enabled", getter, setter); preferences->addPreference(preference); } + { + auto debugGetter = [this]()->bool { return _debug; }; + auto debugSetter = [this](bool value) { + _debug = value; + saveSettings(); + }; + auto preference = new CheckPreference(KINECT_PLUGIN, "Extra Debugging", debugGetter, debugSetter); + preferences->addPreference(preference); + } } bool KinectPlugin::isSupported() const { @@ -389,37 +398,110 @@ void KinectPlugin::ProcessBody(INT64 time, int bodyCount, IBody** bodies) { if (SUCCEEDED(hr)) { auto jointCount = _countof(joints); - //qDebug() << __FUNCTION__ << "nBodyCount:" << nBodyCount << "body:" << i << "jointCount:" << jointCount; + if (_debug) { + qDebug() << __FUNCTION__ << "nBodyCount:" << bodyCount << "body:" << i << "jointCount:" << jointCount; + } + for (int j = 0; j < jointCount; ++j) { - //QString jointName = kinectJointNames[joints[j].JointType]; glm::vec3 jointPosition { joints[j].Position.X, joints[j].Position.Y, joints[j].Position.Z }; - // Kinect Documentation is unclear on what these orientations are, are they absolute? - // or are the relative to the parent bones. It appears as if it has changed between the - // older 1.x SDK and the 2.0 sdk - // - // https://social.msdn.microsoft.com/Forums/en-US/31c9aff6-7dab-433d-9af9-59942dfd3d69/kinect-v20-preview-sdk-jointorientation-vs-boneorientation?forum=kinectv2sdk - // seems to suggest these are absolute... - // "These quaternions are absolute, so you can take a mesh in local space, transform it by the quaternion, - // and it will match the exact orientation of the bone. If you want relative orientation quaternion, you - // can multiply the absolute quaternion by the inverse of the parent joint's quaternion." - // - // - Bone direction(Y green) - always matches the skeleton. - // - Normal(Z blue) - joint roll, perpendicular to the bone - // - Binormal(X orange) - perpendicular to the bone and normal - - glm::quat jointOrientation { jointOrientations[j].Orientation.x, + // This is the rotation in the kinect camera/sensor frame... we adjust that in update... + // NOTE: glm::quat(W!!!, x, y, z)... not (x,y,z,w)!!! + glm::quat jointOrientation { jointOrientations[j].Orientation.w, + jointOrientations[j].Orientation.x, jointOrientations[j].Orientation.y, - jointOrientations[j].Orientation.z, - jointOrientations[j].Orientation.w }; + jointOrientations[j].Orientation.z }; + + if (_debug) { + QString jointName = kinectJointNames[joints[j].JointType]; + qDebug() << __FUNCTION__ << "joint[" << j << "]:" << jointName + << "position:" << jointPosition + << "orientation:" << jointOrientation + << "isTracked:" << (joints[j].TrackingState != TrackingState_NotTracked); + } // filling in the _joints data... if (joints[j].TrackingState != TrackingState_NotTracked) { _joints[j].position = jointPosition; + + // Kinect Documentation... + // + // https://social.msdn.microsoft.com/Forums/en-US/31c9aff6-7dab-433d-9af9-59942dfd3d69/kinect-v20-preview-sdk-jointorientation-vs-boneorientation?forum=kinectv2sdk + // seems to suggest these are absolute... + // "These quaternions are absolute, so you can take a mesh in local space, transform it by the quaternion, + // and it will match the exact orientation of the bone. If you want relative orientation quaternion, you + // can multiply the absolute quaternion by the inverse of the parent joint's quaternion." + // + // This is consistent with our findings, but does not include "enough information" + // - Bone direction(Y green) - always matches the skeleton. + // - Normal(Z blue) - joint roll, perpendicular to the bone + // - Binormal(X orange) - perpendicular to the bone and normal + + // NOTE: Common notation of vectors on paper... + // (+) is the back of the arrow - this vector is pointing into the page + // (o) is the point of the arrow - this vector is pointing out of the page + // + + // From ABOVE the kinect coordinate frame looks like this: + // + // Assuming standing facing the kinect camera + // Right Hand with fingers pointing up (green/y) + // thumb pointing behind body (blue/z) + // palm facing the head (point out back of my hand, red/x) + // + // The identity rotation relative to the cameras frame... (the joint data from SDK) + // + // y | | | | + // | | | | | + // | | | + // z----(o) \ |right| + // x \_ | + // | | + // | | + // + // Expected... identity rotation for left hand..... [to be verified] + // Left Hand with fingers pointing up (green/y) + // thumb pointing forward (blue/z) + // palm facing outward away from head (point out back of my hand, red/x) + // + // Our desired coordinate system... + // "the local coordinate of the palm in our system"... + // + // From ABOVE the hand canonical axes look like this: + // + // + // | | | | y | | | | + // | | | | | | | | | + // | | | | | + // |left | / x----(+) \ |right| + // | _/ z \_ | + // | | | | + // | | | | + // + // Right hand rule... make the hitch hiking sign... + // thumb points in direction of the axis you want to rotate around + // fisted fingers curl in positive rotation direction.... + // + // To transform from Kinect to our RIGHT Hand.... Negative 90 deg around Y + // + // FIXME -- Double check if JointType_HandRight vs JointType_WristRight is actually + // the joint we want to be using!! + // //_joints[j].orientation = jointOrientation; + if (joints[j].JointType == JointType_HandRight) { + static const quat kinectToHandRight = glm::angleAxis(-PI / 2.0f, Vectors::UNIT_Y); + _joints[j].orientation = jointOrientation * kinectToHandRight; + } else if (joints[j].JointType == JointType_HandLeft) { + // To transform from Kinect to our LEFT Hand.... Postive 90 deg around Y + static const quat kinectToHandLeft = glm::angleAxis(PI / 2.0f, Vectors::UNIT_Y); + _joints[j].orientation = jointOrientation * kinectToHandLeft; + } else { + _joints[j].orientation = jointOrientation; + } + } } } @@ -482,6 +564,7 @@ void KinectPlugin::saveSettings() const { settings.beginGroup(idString); { settings.setValue(QString("enabled"), _enabled); + settings.setValue(QString("extraDebug"), _debug); } settings.endGroup(); } @@ -491,8 +574,8 @@ void KinectPlugin::loadSettings() { QString idString = getID(); settings.beginGroup(idString); { - // enabled _enabled = settings.value("enabled", QVariant(DEFAULT_ENABLED)).toBool(); + _debug = settings.value("extraDebug", QVariant(DEFAULT_ENABLED)).toBool(); } settings.endGroup(); } @@ -541,8 +624,8 @@ void KinectPlugin::InputDevice::update(float deltaTime, const controller::InputC continue; } - // FIXME - determine the correct orientation transform - glm::quat rot = joints[i].orientation; + // Note: we want our rotations presenting in the AVATAR frame, so we need to adjust that here. + glm::quat rot = controllerToAvatarRotation * joints[i].orientation; if (i < prevJoints.size()) { linearVel = (pos - (prevJoints[i].position * METERS_PER_CENTIMETER)) / deltaTime; // m/s diff --git a/plugins/hifiKinect/src/KinectPlugin.h b/plugins/hifiKinect/src/KinectPlugin.h index b10698fa31..90794fa6b0 100644 --- a/plugins/hifiKinect/src/KinectPlugin.h +++ b/plugins/hifiKinect/src/KinectPlugin.h @@ -89,6 +89,7 @@ protected: static const char* KINECT_ID_STRING; bool _enabled { false }; + bool _debug { false }; mutable bool _initialized { false }; // copy of data directly from the KinectDataReader SDK diff --git a/plugins/oculus/src/OculusControllerManager.cpp b/plugins/oculus/src/OculusControllerManager.cpp index ce59eafd50..6445c3c891 100644 --- a/plugins/oculus/src/OculusControllerManager.cpp +++ b/plugins/oculus/src/OculusControllerManager.cpp @@ -20,6 +20,8 @@ #include #include +#include + #include "OculusHelpers.h" Q_DECLARE_LOGGING_CATEGORY(oculus) @@ -31,6 +33,8 @@ static const char* MENU_PATH = "Avatar" ">" "Oculus Touch Controllers"; const char* OculusControllerManager::NAME = "Oculus"; +const quint64 LOST_TRACKING_DELAY = 3000000; + bool OculusControllerManager::isSupported() const { return oculusAvailable(); } @@ -42,26 +46,33 @@ bool OculusControllerManager::activate() { } Q_ASSERT(_session); - // register with UserInputMapper - auto userInputMapper = DependencyManager::get(); + checkForConnectedDevices(); + + return true; +} + +void OculusControllerManager::checkForConnectedDevices() { + if (_touch && _remote) { + return; + } unsigned int controllerConnected = ovr_GetConnectedControllerTypes(_session); - if ((controllerConnected & ovrControllerType_Remote) == ovrControllerType_Remote) { + if (!_remote && (controllerConnected & ovrControllerType_Remote) == ovrControllerType_Remote) { if (OVR_SUCCESS(ovr_GetInputState(_session, ovrControllerType_Remote, &_inputState))) { + auto userInputMapper = DependencyManager::get(); _remote = std::make_shared(*this); userInputMapper->registerDevice(_remote); } } - if ((controllerConnected & ovrControllerType_Touch) != 0) { + if (!_touch && (controllerConnected & ovrControllerType_Touch) != 0) { if (OVR_SUCCESS(ovr_GetInputState(_session, ovrControllerType_Touch, &_inputState))) { + auto userInputMapper = DependencyManager::get(); _touch = std::make_shared(*this); userInputMapper->registerDevice(_touch); } } - - return true; } void OculusControllerManager::deactivate() { @@ -85,6 +96,8 @@ void OculusControllerManager::deactivate() { void OculusControllerManager::pluginUpdate(float deltaTime, const controller::InputCalibrationData& inputCalibrationData) { PerformanceTimer perfTimer("OculusControllerManager::TouchDevice::update"); + checkForConnectedDevices(); + if (_touch) { if (OVR_SUCCESS(ovr_GetInputState(_session, ovrControllerType_Touch, &_inputState))) { _touch->update(deltaTime, inputCalibrationData); @@ -196,9 +209,7 @@ void OculusControllerManager::RemoteDevice::focusOutEvent() { } void OculusControllerManager::TouchDevice::update(float deltaTime, const controller::InputCalibrationData& inputCalibrationData) { - _poseStateMap.clear(); _buttonPressedMap.clear(); - ovrSessionStatus status; if (!OVR_SUCCESS(ovr_GetSessionStatus(_parent._session, &status)) || (ovrFalse == status.HmdMounted)) { // if the HMD isn't on someone's head, don't take input from the controllers @@ -206,15 +217,33 @@ void OculusControllerManager::TouchDevice::update(float deltaTime, const control } int numTrackedControllers = 0; + quint64 currentTime = usecTimestampNow(); static const auto REQUIRED_HAND_STATUS = ovrStatus_OrientationTracked | ovrStatus_PositionTracked; auto tracking = ovr_GetTrackingState(_parent._session, 0, false); ovr_for_each_hand([&](ovrHandType hand) { ++numTrackedControllers; + int controller = (hand == ovrHand_Left ? controller::LEFT_HAND : controller::RIGHT_HAND); if (REQUIRED_HAND_STATUS == (tracking.HandStatusFlags[hand] & REQUIRED_HAND_STATUS)) { + _poseStateMap.erase(controller); handlePose(deltaTime, inputCalibrationData, hand, tracking.HandPoses[hand]); - } else { - _poseStateMap[hand == ovrHand_Left ? controller::LEFT_HAND : controller::RIGHT_HAND].valid = false; + _lostTracking[controller] = false; + _lastControllerPose[controller] = tracking.HandPoses[hand]; + return; } + + if (_lostTracking[controller]) { + if (currentTime > _regainTrackingDeadline[controller]) { + _poseStateMap.erase(controller); + _poseStateMap[controller].valid = false; + return; + } + + } else { + quint64 deadlineToRegainTracking = currentTime + LOST_TRACKING_DELAY; + _regainTrackingDeadline[controller] = deadlineToRegainTracking; + _lostTracking[controller] = true; + } + handleRotationForUntrackedHand(inputCalibrationData, hand, tracking.HandPoses[hand]); }); using namespace controller; // Axes @@ -240,7 +269,7 @@ void OculusControllerManager::TouchDevice::update(float deltaTime, const control if (inputState.Touches & pair.first) { _buttonPressedMap.insert(pair.second); } - } + } // Haptics { @@ -275,6 +304,16 @@ void OculusControllerManager::TouchDevice::handlePose(float deltaTime, } +void OculusControllerManager::TouchDevice::handleRotationForUntrackedHand(const controller::InputCalibrationData& inputCalibrationData, + ovrHandType hand, const ovrPoseStatef& handPose) { + auto poseId = (hand == ovrHand_Left ? controller::LEFT_HAND : controller::RIGHT_HAND); + auto& pose = _poseStateMap[poseId]; + auto lastHandPose = _lastControllerPose[poseId]; + pose = ovrControllerRotationToHandRotation(hand, handPose, lastHandPose); + glm::mat4 controllerToAvatar = glm::inverse(inputCalibrationData.avatarMat) * inputCalibrationData.sensorToWorldMat; + pose = pose.transform(controllerToAvatar); +} + bool OculusControllerManager::TouchDevice::triggerHapticPulse(float strength, float duration, controller::Hand hand) { Locker locker(_lock); bool toReturn = true; diff --git a/plugins/oculus/src/OculusControllerManager.h b/plugins/oculus/src/OculusControllerManager.h index 98e0e3d650..11d699ca8e 100644 --- a/plugins/oculus/src/OculusControllerManager.h +++ b/plugins/oculus/src/OculusControllerManager.h @@ -11,6 +11,7 @@ #include #include +#include #include @@ -75,6 +76,7 @@ private: private: void stopHapticPulse(bool leftHand); void handlePose(float deltaTime, const controller::InputCalibrationData& inputCalibrationData, ovrHandType hand, const ovrPoseStatef& handPose); + void handleRotationForUntrackedHand(const controller::InputCalibrationData& inputCalibrationData, ovrHandType hand, const ovrPoseStatef& handPose); int _trackedControllers { 0 }; // perform an action when the TouchDevice mutex is acquired. @@ -87,10 +89,15 @@ private: float _rightHapticDuration { 0.0f }; float _rightHapticStrength { 0.0f }; mutable std::recursive_mutex _lock; + std::map _lostTracking; + std::map _regainTrackingDeadline; + std::map _lastControllerPose; friend class OculusControllerManager; }; + void checkForConnectedDevices(); + ovrSession _session { nullptr }; ovrInputState _inputState {}; RemoteDevice::Pointer _remote; diff --git a/plugins/oculus/src/OculusDisplayPlugin.cpp b/plugins/oculus/src/OculusDisplayPlugin.cpp index b076170ae5..db8c92ac23 100644 --- a/plugins/oculus/src/OculusDisplayPlugin.cpp +++ b/plugins/oculus/src/OculusDisplayPlugin.cpp @@ -28,6 +28,12 @@ OculusDisplayPlugin::OculusDisplayPlugin() { _compositorDroppedFrames.store(0); } +float OculusDisplayPlugin::getTargetFrameRate() const { + if (_aswActive) { + return _hmdDesc.DisplayRefreshRate / 2.0f; + } + return _hmdDesc.DisplayRefreshRate; +} bool OculusDisplayPlugin::internalActivate() { bool result = Parent::internalActivate(); @@ -185,8 +191,6 @@ void OculusDisplayPlugin::hmdPresent() { } } - - if (!OVR_SUCCESS(result)) { logWarning("Failed to present"); } @@ -195,12 +199,20 @@ void OculusDisplayPlugin::hmdPresent() { static int appDroppedFrames = 0; ovrPerfStats perfStats; ovr_GetPerfStats(_session, &perfStats); + bool shouldResetPresentRate = false; for (int i = 0; i < perfStats.FrameStatsCount; ++i) { const auto& frameStats = perfStats.FrameStats[i]; int delta = frameStats.CompositorDroppedFrameCount - compositorDroppedFrames; _stutterRate.increment(delta); compositorDroppedFrames = frameStats.CompositorDroppedFrameCount; appDroppedFrames = frameStats.AppDroppedFrameCount; + bool newAswState = ovrTrue == frameStats.AswIsActive; + if (_aswActive.exchange(newAswState) != newAswState) { + shouldResetPresentRate = true; + } + } + if (shouldResetPresentRate) { + resetPresentRate(); } _appDroppedFrames.store(appDroppedFrames); _compositorDroppedFrames.store(compositorDroppedFrames); @@ -212,6 +224,7 @@ void OculusDisplayPlugin::hmdPresent() { QJsonObject OculusDisplayPlugin::getHardwareStats() const { QJsonObject hardwareStats; + hardwareStats["asw_active"] = _aswActive.load(); hardwareStats["app_dropped_frame_count"] = _appDroppedFrames.load(); hardwareStats["compositor_dropped_frame_count"] = _compositorDroppedFrames.load(); hardwareStats["long_render_count"] = _longRenders.load(); diff --git a/plugins/oculus/src/OculusDisplayPlugin.h b/plugins/oculus/src/OculusDisplayPlugin.h index 6fc50b829f..9209fd373e 100644 --- a/plugins/oculus/src/OculusDisplayPlugin.h +++ b/plugins/oculus/src/OculusDisplayPlugin.h @@ -20,7 +20,8 @@ public: QString getPreferredAudioInDevice() const override; QString getPreferredAudioOutDevice() const override; - + float getTargetFrameRate() const override; + virtual QJsonObject getHardwareStats() const; protected: @@ -39,6 +40,7 @@ private: gpu::FramebufferPointer _outputFramebuffer; bool _customized { false }; + std::atomic_bool _aswActive; std::atomic_int _compositorDroppedFrames; std::atomic_int _appDroppedFrames; std::atomic_int _longSubmits; diff --git a/plugins/oculus/src/OculusHelpers.cpp b/plugins/oculus/src/OculusHelpers.cpp index 340b804404..18844a1995 100644 --- a/plugins/oculus/src/OculusHelpers.cpp +++ b/plugins/oculus/src/OculusHelpers.cpp @@ -88,7 +88,11 @@ ovrSession acquireOculusSession() { } if (!session) { - if (!OVR_SUCCESS(ovr_Initialize(nullptr))) { + ovrInitParams initParams { + ovrInit_RequestVersion | ovrInit_MixedRendering, OVR_MINOR_VERSION, nullptr, 0, 0 + }; + + if (!OVR_SUCCESS(ovr_Initialize(&initParams))) { logWarning("Failed to initialize Oculus SDK"); return session; } @@ -263,3 +267,37 @@ controller::Pose ovrControllerPoseToHandPose( pose.valid = true; return pose; } + +controller::Pose ovrControllerRotationToHandRotation(ovrHandType hand, const ovrPoseStatef& handPose, + const ovrPoseStatef& lastHandPose) { + static const glm::quat yFlip = glm::angleAxis(PI, Vectors::UNIT_Y); + static const glm::quat quarterX = glm::angleAxis(PI_OVER_TWO, Vectors::UNIT_X); + static const glm::quat touchToHand = yFlip * quarterX; + + static const glm::quat leftQuarterZ = glm::angleAxis(-PI_OVER_TWO, Vectors::UNIT_Z); + static const glm::quat rightQuarterZ = glm::angleAxis(PI_OVER_TWO, Vectors::UNIT_Z); + + static const glm::quat leftRotationOffset = glm::inverse(leftQuarterZ) * touchToHand; + static const glm::quat rightRotationOffset = glm::inverse(rightQuarterZ) * touchToHand; + + static const float CONTROLLER_LENGTH_OFFSET = 0.0762f; // three inches + static const glm::vec3 CONTROLLER_OFFSET = glm::vec3(CONTROLLER_LENGTH_OFFSET / 2.0f, + -CONTROLLER_LENGTH_OFFSET / 2.0f, + CONTROLLER_LENGTH_OFFSET * 1.5f); + static const glm::vec3 leftTranslationOffset = glm::vec3(-1.0f, 1.0f, 1.0f) * CONTROLLER_OFFSET; + static const glm::vec3 rightTranslationOffset = CONTROLLER_OFFSET; + + auto translationOffset = (hand == ovrHand_Left ? leftTranslationOffset : rightTranslationOffset); + auto rotationOffset = (hand == ovrHand_Left ? leftRotationOffset : rightRotationOffset); + + glm::quat rotation = toGlm(handPose.ThePose.Orientation); + + controller::Pose pose; + pose.translation = toGlm(lastHandPose.ThePose.Position); + pose.translation += rotation * translationOffset; + pose.rotation = rotation * rotationOffset; + pose.angularVelocity = toGlm(lastHandPose.AngularVelocity); + pose.velocity = toGlm(lastHandPose.LinearVelocity); + pose.valid = true; + return pose; +} diff --git a/plugins/oculus/src/OculusHelpers.h b/plugins/oculus/src/OculusHelpers.h index 50ba355b0c..5743f8576b 100644 --- a/plugins/oculus/src/OculusHelpers.h +++ b/plugins/oculus/src/OculusHelpers.h @@ -118,3 +118,6 @@ inline ovrPosef ovrPoseFromGlm(const glm::mat4 & m) { controller::Pose ovrControllerPoseToHandPose( ovrHandType hand, const ovrPoseStatef& handPose); + +controller::Pose ovrControllerRotationToHandRotation(ovrHandType hand, + const ovrPoseStatef& handPose, const ovrPoseStatef& lastHandPose); diff --git a/plugins/oculusLegacy/src/OculusLegacyDisplayPlugin.cpp b/plugins/oculusLegacy/src/OculusLegacyDisplayPlugin.cpp index 09f3e6dc8c..b759a06aee 100644 --- a/plugins/oculusLegacy/src/OculusLegacyDisplayPlugin.cpp +++ b/plugins/oculusLegacy/src/OculusLegacyDisplayPlugin.cpp @@ -255,7 +255,7 @@ void OculusLegacyDisplayPlugin::hmdPresent() { memset(eyePoses, 0, sizeof(ovrPosef) * 2); eyePoses[0].Orientation = eyePoses[1].Orientation = ovrRotation; - GLint texture = getGLBackend()->getTextureID(_compositeFramebuffer->getRenderBuffer(0), false); + GLint texture = getGLBackend()->getTextureID(_compositeFramebuffer->getRenderBuffer(0)); auto sync = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0); glFlush(); if (_hmdWindow->makeCurrent()) { diff --git a/plugins/openvr/src/OpenVrDisplayPlugin.cpp b/plugins/openvr/src/OpenVrDisplayPlugin.cpp index 6d503a208a..1adfa8d333 100644 --- a/plugins/openvr/src/OpenVrDisplayPlugin.cpp +++ b/plugins/openvr/src/OpenVrDisplayPlugin.cpp @@ -277,8 +277,8 @@ public: glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0); static const vr::VRTextureBounds_t leftBounds{ 0, 0, 0.5f, 1 }; static const vr::VRTextureBounds_t rightBounds{ 0.5f, 0, 1, 1 }; - - vr::Texture_t texture{ (void*)_colors[currentColorBuffer], vr::API_OpenGL, vr::ColorSpace_Auto }; + + vr::Texture_t texture{ (void*)_colors[currentColorBuffer], vr::TextureType_OpenGL, vr::ColorSpace_Auto }; vr::VRCompositor()->Submit(vr::Eye_Left, &texture, &leftBounds); vr::VRCompositor()->Submit(vr::Eye_Right, &texture, &rightBounds); _plugin._presentRate.increment(); @@ -422,7 +422,7 @@ bool OpenVrDisplayPlugin::internalActivate() { withNonPresentThreadLock([&] { openvr_for_each_eye([&](vr::Hmd_Eye eye) { _eyeOffsets[eye] = toGlm(_system->GetEyeToHeadTransform(eye)); - _eyeProjections[eye] = toGlm(_system->GetProjectionMatrix(eye, DEFAULT_NEAR_CLIP, DEFAULT_FAR_CLIP, vr::API_OpenGL)); + _eyeProjections[eye] = toGlm(_system->GetProjectionMatrix(eye, DEFAULT_NEAR_CLIP, DEFAULT_FAR_CLIP)); }); // FIXME Calculate the proper combined projection by using GetProjectionRaw values from both eyes _cullingProjection = _eyeProjections[0]; @@ -494,9 +494,9 @@ void OpenVrDisplayPlugin::customizeContext() { _compositeInfos[0].texture = _compositeFramebuffer->getRenderBuffer(0); for (size_t i = 0; i < COMPOSITING_BUFFER_SIZE; ++i) { if (0 != i) { - _compositeInfos[i].texture = gpu::TexturePointer(gpu::Texture::create2D(gpu::Element::COLOR_RGBA_32, _renderTargetSize.x, _renderTargetSize.y, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_POINT))); + _compositeInfos[i].texture = gpu::TexturePointer(gpu::Texture::createRenderBuffer(gpu::Element::COLOR_RGBA_32, _renderTargetSize.x, _renderTargetSize.y, gpu::Texture::SINGLE_MIP, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_POINT))); } - _compositeInfos[i].textureID = getGLBackend()->getTextureID(_compositeInfos[i].texture, false); + _compositeInfos[i].textureID = getGLBackend()->getTextureID(_compositeInfos[i].texture); } _submitThread->_canvas = _submitCanvas; _submitThread->start(QThread::HighPriority); @@ -624,7 +624,7 @@ void OpenVrDisplayPlugin::compositeLayers() { glFlush(); if (!newComposite.textureID) { - newComposite.textureID = getGLBackend()->getTextureID(newComposite.texture, false); + newComposite.textureID = getGLBackend()->getTextureID(newComposite.texture); } withPresentThreadLock([&] { _submitThread->update(newComposite); @@ -638,8 +638,8 @@ void OpenVrDisplayPlugin::hmdPresent() { if (_threadedSubmit) { _submitThread->waitForPresent(); } else { - GLuint glTexId = getGLBackend()->getTextureID(_compositeFramebuffer->getRenderBuffer(0), false); - vr::Texture_t vrTexture { (void*)glTexId, vr::API_OpenGL, vr::ColorSpace_Auto }; + GLuint glTexId = getGLBackend()->getTextureID(_compositeFramebuffer->getRenderBuffer(0)); + vr::Texture_t vrTexture { (void*)glTexId, vr::TextureType_OpenGL, vr::ColorSpace_Auto }; vr::VRCompositor()->Submit(vr::Eye_Left, &vrTexture, &OPENVR_TEXTURE_BOUNDS_LEFT); vr::VRCompositor()->Submit(vr::Eye_Right, &vrTexture, &OPENVR_TEXTURE_BOUNDS_RIGHT); vr::VRCompositor()->PostPresentHandoff(); diff --git a/plugins/openvr/src/OpenVrHelpers.cpp b/plugins/openvr/src/OpenVrHelpers.cpp index 29ef640bf3..d9db757b2f 100644 --- a/plugins/openvr/src/OpenVrHelpers.cpp +++ b/plugins/openvr/src/OpenVrHelpers.cpp @@ -114,7 +114,7 @@ void releaseOpenVrSystem() { // HACK: workaround openvr crash, call submit with an invalid texture, right before VR_Shutdown. const GLuint INVALID_GL_TEXTURE_HANDLE = -1; - vr::Texture_t vrTexture{ (void*)INVALID_GL_TEXTURE_HANDLE, vr::API_OpenGL, vr::ColorSpace_Auto }; + vr::Texture_t vrTexture{ (void*)INVALID_GL_TEXTURE_HANDLE, vr::TextureType_OpenGL, vr::ColorSpace_Auto }; static vr::VRTextureBounds_t OPENVR_TEXTURE_BOUNDS_LEFT{ 0, 0, 0.5f, 1 }; static vr::VRTextureBounds_t OPENVR_TEXTURE_BOUNDS_RIGHT{ 0.5f, 0, 1, 1 }; diff --git a/plugins/openvr/src/ViveControllerManager.cpp b/plugins/openvr/src/ViveControllerManager.cpp index 2e930c0fdc..86b37135d2 100644 --- a/plugins/openvr/src/ViveControllerManager.cpp +++ b/plugins/openvr/src/ViveControllerManager.cpp @@ -63,59 +63,6 @@ bool ViveControllerManager::activate() { enableOpenVrKeyboard(_container); - // OpenVR provides 3d mesh representations of the controllers - // Disabled controller rendering code - /* - auto renderModels = vr::VRRenderModels(); - - vr::RenderModel_t model; - if (!_system->LoadRenderModel(CONTROLLER_MODEL_STRING, &model)) { - qDebug() << QString("Unable to load render model %1\n").arg(CONTROLLER_MODEL_STRING); - } else { - model::Mesh* mesh = new model::Mesh(); - model::MeshPointer meshPtr(mesh); - _modelGeometry.setMesh(meshPtr); - - auto indexBuffer = new gpu::Buffer(3 * model.unTriangleCount * sizeof(uint16_t), (gpu::Byte*)model.rIndexData); - auto indexBufferPtr = gpu::BufferPointer(indexBuffer); - auto indexBufferView = new gpu::BufferView(indexBufferPtr, gpu::Element(gpu::SCALAR, gpu::UINT16, gpu::RAW)); - mesh->setIndexBuffer(*indexBufferView); - - auto vertexBuffer = new gpu::Buffer(model.unVertexCount * sizeof(vr::RenderModel_Vertex_t), - (gpu::Byte*)model.rVertexData); - auto vertexBufferPtr = gpu::BufferPointer(vertexBuffer); - auto vertexBufferView = new gpu::BufferView(vertexBufferPtr, - 0, - vertexBufferPtr->getSize() - sizeof(float) * 3, - sizeof(vr::RenderModel_Vertex_t), - gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::RAW)); - mesh->setVertexBuffer(*vertexBufferView); - mesh->addAttribute(gpu::Stream::NORMAL, - gpu::BufferView(vertexBufferPtr, - sizeof(float) * 3, - vertexBufferPtr->getSize() - sizeof(float) * 3, - sizeof(vr::RenderModel_Vertex_t), - gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::RAW))); - //mesh->addAttribute(gpu::Stream::TEXCOORD, - // gpu::BufferView(vertexBufferPtr, - // 2 * sizeof(float) * 3, - // vertexBufferPtr->getSize() - sizeof(float) * 2, - // sizeof(vr::RenderModel_Vertex_t), - // gpu::Element(gpu::VEC2, gpu::FLOAT, gpu::RAW))); - - gpu::Element formatGPU = gpu::Element(gpu::VEC4, gpu::NUINT8, gpu::RGBA); - gpu::Element formatMip = gpu::Element(gpu::VEC4, gpu::NUINT8, gpu::RGBA); - _texture = gpu::TexturePointer( - gpu::Texture::create2D(formatGPU, model.diffuseTexture.unWidth, model.diffuseTexture.unHeight, - gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR))); - _texture->assignStoredMip(0, formatMip, model.diffuseTexture.unWidth * model.diffuseTexture.unHeight * 4 * sizeof(uint8_t), model.diffuseTexture.rubTextureMapData); - _texture->autoGenerateMips(-1); - - _modelLoaded = true; - _renderControllers = true; - } - */ - // register with UserInputMapper auto userInputMapper = DependencyManager::get(); userInputMapper->registerDevice(_inputDevice); @@ -145,70 +92,6 @@ void ViveControllerManager::deactivate() { _registeredWithInputMapper = false; } -void ViveControllerManager::updateRendering(RenderArgs* args, render::ScenePointer scene, render::PendingChanges pendingChanges) { - PerformanceTimer perfTimer("ViveControllerManager::updateRendering"); - - /* - if (_modelLoaded) { - //auto controllerPayload = new render::Payload(this); - //auto controllerPayloadPointer = ViveControllerManager::PayloadPointer(controllerPayload); - //if (_leftHandRenderID == 0) { - // _leftHandRenderID = scene->allocateID(); - // pendingChanges.resetItem(_leftHandRenderID, controllerPayloadPointer); - //} - //pendingChanges.updateItem(_leftHandRenderID, ); - - - controller::Pose leftHand = _inputDevice->_poseStateMap[controller::StandardPoseChannel::LEFT_HAND]; - controller::Pose rightHand = _inputDevice->_poseStateMap[controller::StandardPoseChannel::RIGHT_HAND]; - - gpu::doInBatch(args->_context, [=](gpu::Batch& batch) { - auto geometryCache = DependencyManager::get(); - geometryCache->useSimpleDrawPipeline(batch); - DependencyManager::get()->bindSimpleProgram(batch, true); - - auto mesh = _modelGeometry.getMesh(); - batch.setInputFormat(mesh->getVertexFormat()); - //batch._glBindTexture(GL_TEXTURE_2D, _uexture); - - if (leftHand.isValid()) { - renderHand(leftHand, batch, 1); - } - if (rightHand.isValid()) { - renderHand(rightHand, batch, -1); - } - }); - } - */ -} - -void ViveControllerManager::renderHand(const controller::Pose& pose, gpu::Batch& batch, int sign) { - /* - auto userInputMapper = DependencyManager::get(); - Transform transform(userInputMapper->getSensorToWorldMat()); - transform.postTranslate(pose.getTranslation() + pose.getRotation() * glm::vec3(0, 0, CONTROLLER_LENGTH_OFFSET)); - - glm::quat rotation = pose.getRotation() * glm::angleAxis(PI, glm::vec3(1.0f, 0.0f, 0.0f)) * glm::angleAxis(sign * PI_OVER_TWO, glm::vec3(0.0f, 0.0f, 1.0f)); - transform.postRotate(rotation); - - batch.setModelTransform(transform); - - auto mesh = _modelGeometry.getMesh(); - batch.setInputBuffer(gpu::Stream::POSITION, mesh->getVertexBuffer()); - batch.setInputBuffer(gpu::Stream::NORMAL, - mesh->getVertexBuffer()._buffer, - sizeof(float) * 3, - mesh->getVertexBuffer()._stride); - //batch.setInputBuffer(gpu::Stream::TEXCOORD, - // mesh->getVertexBuffer()._buffer, - // 2 * 3 * sizeof(float), - // mesh->getVertexBuffer()._stride); - batch.setIndexBuffer(gpu::UINT16, mesh->getIndexBuffer()._buffer, 0); - batch.drawIndexed(gpu::TRIANGLES, mesh->getNumIndices(), 0); - */ -} - - void ViveControllerManager::pluginUpdate(float deltaTime, const controller::InputCalibrationData& inputCalibrationData) { if (!_system) { @@ -257,6 +140,11 @@ void ViveControllerManager::InputDevice::update(float deltaTime, const controlle handleHandController(deltaTime, leftHandDeviceIndex, inputCalibrationData, true); handleHandController(deltaTime, rightHandDeviceIndex, inputCalibrationData, false); + // collect poses for all generic trackers + for (int i = 0; i < vr::k_unMaxTrackedDeviceCount; i++) { + handleTrackedObject(i, inputCalibrationData); + } + // handle haptics { Locker locker(_lock); @@ -278,6 +166,31 @@ void ViveControllerManager::InputDevice::update(float deltaTime, const controlle _trackedControllers = numTrackedControllers; } +void ViveControllerManager::InputDevice::handleTrackedObject(uint32_t deviceIndex, const controller::InputCalibrationData& inputCalibrationData) { + + uint32_t poseIndex = controller::TRACKED_OBJECT_00 + deviceIndex; + + if (_system->IsTrackedDeviceConnected(deviceIndex) && + _system->GetTrackedDeviceClass(deviceIndex) == vr::TrackedDeviceClass_GenericTracker && + _nextSimPoseData.vrPoses[deviceIndex].bPoseIsValid && + poseIndex <= controller::TRACKED_OBJECT_15) { + + // process pose + const mat4& mat = _nextSimPoseData.poses[deviceIndex]; + const vec3 linearVelocity = _nextSimPoseData.linearVelocities[deviceIndex]; + const vec3 angularVelocity = _nextSimPoseData.angularVelocities[deviceIndex]; + + controller::Pose pose(extractTranslation(mat), glmExtractRotation(mat), linearVelocity, angularVelocity); + + // transform into avatar frame + glm::mat4 controllerToAvatar = glm::inverse(inputCalibrationData.avatarMat) * inputCalibrationData.sensorToWorldMat; + _poseStateMap[poseIndex] = pose.transform(controllerToAvatar); + } else { + controller::Pose invalidPose; + _poseStateMap[poseIndex] = invalidPose; + } +} + void ViveControllerManager::InputDevice::handleHandController(float deltaTime, uint32_t deviceIndex, const controller::InputCalibrationData& inputCalibrationData, bool isLeftHand) { if (_system->IsTrackedDeviceConnected(deviceIndex) && @@ -291,7 +204,7 @@ void ViveControllerManager::InputDevice::handleHandController(float deltaTime, u handlePoseEvent(deltaTime, inputCalibrationData, mat, linearVelocity, angularVelocity, isLeftHand); vr::VRControllerState_t controllerState = vr::VRControllerState_t(); - if (_system->GetControllerState(deviceIndex, &controllerState)) { + if (_system->GetControllerState(deviceIndex, &controllerState, sizeof(vr::VRControllerState_t))) { // process each button for (uint32_t i = 0; i < vr::k_EButton_Max; ++i) { auto mask = vr::ButtonMaskFromId((vr::EVRButtonId)i); @@ -492,6 +405,24 @@ controller::Input::NamedVector ViveControllerManager::InputDevice::getAvailableI makePair(LEFT_HAND, "LeftHand"), makePair(RIGHT_HAND, "RightHand"), + // 16 tracked poses + makePair(TRACKED_OBJECT_00, "TrackedObject00"), + makePair(TRACKED_OBJECT_01, "TrackedObject01"), + makePair(TRACKED_OBJECT_02, "TrackedObject02"), + makePair(TRACKED_OBJECT_03, "TrackedObject03"), + makePair(TRACKED_OBJECT_04, "TrackedObject04"), + makePair(TRACKED_OBJECT_05, "TrackedObject05"), + makePair(TRACKED_OBJECT_06, "TrackedObject06"), + makePair(TRACKED_OBJECT_07, "TrackedObject07"), + makePair(TRACKED_OBJECT_08, "TrackedObject08"), + makePair(TRACKED_OBJECT_09, "TrackedObject09"), + makePair(TRACKED_OBJECT_10, "TrackedObject10"), + makePair(TRACKED_OBJECT_11, "TrackedObject11"), + makePair(TRACKED_OBJECT_12, "TrackedObject12"), + makePair(TRACKED_OBJECT_13, "TrackedObject13"), + makePair(TRACKED_OBJECT_14, "TrackedObject14"), + makePair(TRACKED_OBJECT_15, "TrackedObject15"), + // app button above trackpad. Input::NamedPair(Input(_deviceID, LEFT_APP_MENU, ChannelType::BUTTON), "LeftApplicationMenu"), Input::NamedPair(Input(_deviceID, RIGHT_APP_MENU, ChannelType::BUTTON), "RightApplicationMenu"), diff --git a/plugins/openvr/src/ViveControllerManager.h b/plugins/openvr/src/ViveControllerManager.h index 3fb166c842..dc1883d5e4 100644 --- a/plugins/openvr/src/ViveControllerManager.h +++ b/plugins/openvr/src/ViveControllerManager.h @@ -43,8 +43,6 @@ public: void pluginFocusOutEvent() override { _inputDevice->focusOutEvent(); } void pluginUpdate(float deltaTime, const controller::InputCalibrationData& inputCalibrationData) override; - void updateRendering(RenderArgs* args, render::ScenePointer scene, render::PendingChanges pendingChanges); - void setRenderControllers(bool renderControllers) { _renderControllers = renderControllers; } private: @@ -62,6 +60,7 @@ private: void hapticsHelper(float deltaTime, bool leftHand); void handleHandController(float deltaTime, uint32_t deviceIndex, const controller::InputCalibrationData& inputCalibrationData, bool isLeftHand); + void handleTrackedObject(uint32_t deviceIndex, const controller::InputCalibrationData& inputCalibrationData); void handleButtonEvent(float deltaTime, uint32_t button, bool pressed, bool touched, bool isLeftHand); void handleAxisEvent(float deltaTime, uint32_t axis, float x, float y, bool isLeftHand); void handlePoseEvent(float deltaTime, const controller::InputCalibrationData& inputCalibrationData, const mat4& mat, diff --git a/script-archive/controllers/kinectToAnimation.js b/script-archive/controllers/kinectToAnimation.js new file mode 100644 index 0000000000..3965b4e4bb --- /dev/null +++ b/script-archive/controllers/kinectToAnimation.js @@ -0,0 +1,66 @@ +var handlerId = 0; + +var ikTypes = { + RotationAndPosition: 0, + RotationOnly: 1, + HmdHead: 2, + HipsRelativeRotationAndPosition: 3, + Off: 4 +}; + +var MAPPING_NAME = "com.highfidelity.examples.kinectToAnimation"; +var mapping = Controller.newMapping(MAPPING_NAME); + +var recentLeftHand; +var recentRightHand; +var recentLeftFoot; +var recentRightFoot; + +mapping.from(Controller.Hardware.Kinect.LeftHand).debug(true).to(function(pose) { recentLeftHand = pose; }); +mapping.from(Controller.Hardware.Kinect.RightHand).debug(true).to(function(pose) { recentRightHand = pose; }); +mapping.from(Controller.Hardware.Kinect.LeftFoot).debug(true).to(function(pose) { recentLeftFoot = pose; }); +mapping.from(Controller.Hardware.Kinect.RightFoot).debug(true).to(function(pose) { recentRightFoot = pose; }); + +function init() { + var t = 0; + var propList = [ + "leftHandType", "leftHandPosition", "leftHandRotation", "rightHandType", "rightHandPosition", "rightHandRotation", + "leftFootType", "leftFootPosition", "leftFootRotation", "rightFootType", "rightFootPosition", "rightFootRotation" + ]; + handlerId = MyAvatar.addAnimationStateHandler(function (props) { + + Vec3.print("recentRightHand.translation:", recentRightHand.translation); + Vec3.print("recentLeftHand.translation:", recentLeftHand.translation); + Vec3.print("recentRightFoot.translation:", recentRightFoot.translation); + Vec3.print("recentLeftFoot.translation:", recentLeftFoot.translation); + + return { + + rightHandType: ikTypes["RotationAndPosition"], + rightHandPosition: recentRightHand.translation, + rightHandRotation: recentRightHand.rotation, + leftHandType: ikTypes["RotationAndPosition"], + leftHandPosition: recentLeftHand.translation, + leftHandRotation: recentLeftHand.rotation, + + rightFootType: ikTypes["RotationAndPosition"], + rightFootPosition: recentRightFoot.translation, + rightFootRotation: recentRightFoot.rotation, + leftFootType: ikTypes["RotationAndPosition"], + leftFootPosition: recentLeftFoot.translation, + leftFootRotation: recentLeftFoot.rotation, + + }; + }, propList); + + Controller.enableMapping(MAPPING_NAME); +} + +init(); +Script.scriptEnding.connect(function(){ + MyAvatar.removeAnimationStateHandler(handlerId); + mapping.disable(); +}); + + + diff --git a/script-archive/controllers/proceduralFootPoseExample.js b/script-archive/controllers/proceduralFootPoseExample.js new file mode 100644 index 0000000000..e2504de04d --- /dev/null +++ b/script-archive/controllers/proceduralFootPoseExample.js @@ -0,0 +1,81 @@ +// +// proceduralFootPoseExample.js +// examples/controllers +// +// Created by Brad Hefta-Gaub on 2015/12/15 +// Copyright 2015 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + + +var MAPPING_NAME = "com.highfidelity.examples.proceduralFootPose"; +var mapping = Controller.newMapping(MAPPING_NAME); +var translation = { x: 0, y: 0.1, z: 0 }; +var translationDx = 0.01; +var translationDy = 0.01; +var translationDz = -0.01; +var TRANSLATION_LIMIT = 0.5; + +var pitch = 45; +var yaw = 0; +var roll = 45; +var pitchDelta = 1; +var yawDelta = -1; +var rollDelta = 1; +var ROTATION_MIN = -90; +var ROTATION_MAX = 90; + +mapping.from(function() { + + // adjust the hand translation in a periodic back and forth motion for each of the 3 axes + translation.x = translation.x + translationDx; + translation.y = translation.y + translationDy; + translation.z = translation.z + translationDz; + if ((translation.x > TRANSLATION_LIMIT) || (translation.x < (-1 * TRANSLATION_LIMIT))) { + translationDx = translationDx * -1; + } + if ((translation.y > TRANSLATION_LIMIT) || (translation.y < (-1 * TRANSLATION_LIMIT))) { + translationDy = translationDy * -1; + } + if ((translation.z > TRANSLATION_LIMIT) || (translation.z < (-1 * TRANSLATION_LIMIT))) { + translationDz = translationDz * -1; + } + + // adjust the hand rotation in a periodic back and forth motion for each of pitch/yaw/roll + pitch = pitch + pitchDelta; + yaw = yaw + yawDelta; + roll = roll + rollDelta; + if ((pitch > ROTATION_MAX) || (pitch < ROTATION_MIN)) { + pitchDelta = pitchDelta * -1; + } + if ((yaw > ROTATION_MAX) || (yaw < ROTATION_MIN)) { + yawDelta = yawDelta * -1; + } + if ((roll > ROTATION_MAX) || (roll < ROTATION_MIN)) { + rollDelta = rollDelta * -1; + } + + var rotation = Quat.fromPitchYawRollDegrees(pitch, yaw, roll); + + var pose = { + translation: translation, + rotation: rotation, + velocity: { x: 0, y: 0, z: 0 }, + angularVelocity: { x: 0, y: 0, z: 0 } + }; + + Vec3.print("foot translation:", translation); + return pose; +}).debug(true).to(Controller.Standard.LeftFoot); + +//mapping.from(Controller.Standard.LeftFoot).debug(true).to(Controller.Actions.LeftFoot); + + +Controller.enableMapping(MAPPING_NAME); + + +Script.scriptEnding.connect(function(){ + mapping.disable(); +}); diff --git a/script-archive/controllers/puppetFeet2.js b/script-archive/controllers/puppetFeet2.js new file mode 100644 index 0000000000..754f3c4270 --- /dev/null +++ b/script-archive/controllers/puppetFeet2.js @@ -0,0 +1,35 @@ +// +// proceduralFootPoseExample.js +// examples/controllers +// +// Created by Brad Hefta-Gaub on 2015/12/15 +// Copyright 2015 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + + +var MAPPING_NAME = "com.highfidelity.examples.proceduralFootPose"; +var mapping = Controller.newMapping(MAPPING_NAME); +var puppetOffset = { x: 0, y: -1, z: 0 }; + +mapping.from(function() { + var leftHandPose = Controller.getPoseValue(Controller.Standard.LeftHand); + + var pose = { + translation: Vec3.sum(leftHandPose.translation, puppetOffset), + rotation: { x: 0, y: 0, z: 0, w: 0 }, //leftHandPose.rotation, + velocity: { x: 0, y: 0, z: 0 }, + angularVelocity: { x: 0, y: 0, z: 0 } + }; + return pose; +}).to(Controller.Standard.LeftFoot); + + +Controller.enableMapping(MAPPING_NAME); + + +Script.scriptEnding.connect(function(){ + mapping.disable(); +}); diff --git a/script-archive/entityScripts/doubleClickExample.js b/script-archive/entityScripts/doubleClickExample.js new file mode 100644 index 0000000000..daff2668ed --- /dev/null +++ b/script-archive/entityScripts/doubleClickExample.js @@ -0,0 +1,19 @@ +(function() { + var _this; + function DoubleClickExample() { + _this = this; + return; + } + + DoubleClickExample.prototype = { + clickDownOnEntity: function() { + print("clickDownOnEntity"); + }, + + doubleclickOnEntity: function() { + print("doubleclickOnEntity"); + } + + }; + return new DoubleClickExample(); +}); \ No newline at end of file diff --git a/script-archive/example/games/grabHockey.js b/script-archive/example/games/grabHockey.js index 961a8af6ad..3f2a2fcf91 100644 --- a/script-archive/example/games/grabHockey.js +++ b/script-archive/example/games/grabHockey.js @@ -142,7 +142,8 @@ function mousePressEvent(event) { if (!pickResults.intersects) { return; } - if (pickResults.properties.dynamic) { + var isDynamic = Entites.getEntityProperties(pickResults.entityID, "dynamic").dynamic; + if (isDynamic) { grabbedEntity = pickResults.entityID; var props = Entities.getEntityProperties(grabbedEntity) originalGravity = props.gravity; diff --git a/script-archive/example/games/hydraGrabHockey.js b/script-archive/example/games/hydraGrabHockey.js index e7d6746309..e357735fe5 100644 --- a/script-archive/example/games/hydraGrabHockey.js +++ b/script-archive/example/games/hydraGrabHockey.js @@ -190,7 +190,10 @@ function controller(side) { direction: Vec3.normalize(Vec3.subtract(this.tipPosition, this.palmPosition)) }; var intersection = getRayIntersection(pickRay, true); - if (intersection.intersects && intersection.properties.dynamic) { + + var isDynamic = Entites.getEntityProperties(intersection.entityID, "dynamic").dynamic; + + if (intersection.intersects && isDynamic) { this.laserWasHovered = true; if (this.triggerHeld && !this.grabbing) { this.grab(intersection.entityID); diff --git a/script-archive/example/scripts/rayPickExample.js b/script-archive/example/scripts/rayPickExample.js index d85138211e..3687176af1 100644 --- a/script-archive/example/scripts/rayPickExample.js +++ b/script-archive/example/scripts/rayPickExample.js @@ -38,7 +38,6 @@ function mouseMoveEvent(event) { if (intersection.intersects) { print("intersection entityID=" + intersection.entityID); - print("intersection properties.modelURL=" + intersection.properties.modelURL); print("intersection face=" + intersection.face); print("intersection distance=" + intersection.distance); print("intersection intersection.x/y/z=" + intersection.intersection.x + ", " diff --git a/script-archive/painting/whiteboard/whiteboardEntityScript.js b/script-archive/painting/whiteboard/whiteboardEntityScript.js index 181932c7cb..01497b406d 100644 --- a/script-archive/painting/whiteboard/whiteboardEntityScript.js +++ b/script-archive/painting/whiteboard/whiteboardEntityScript.js @@ -77,6 +77,8 @@ //Comment out above line and uncomment below line to see difference in performance between using a whitelist, and not using one // this.intersection = Entities.findRayIntersection(pickRay, true); + var type = Entites.getEntityProperties(this.intersection.entityID, "type").type; + if (this.intersection.intersects) { var distance = Vec3.distance(handPosition, this.intersection.intersection); if (distance < MAX_DISTANCE) { @@ -98,7 +100,7 @@ this.oldPosition = null; } } - } else if (this.intersection.properties.type !== "Unknown") { + } else if (type !== "Unknown") { //Sometimes ray will pick against an invisible object with type unkown... so if type is unknown, ignore this.stopPainting(); } diff --git a/script-archive/selfieStick.js b/script-archive/selfieStick.js new file mode 100644 index 0000000000..fd3bb82145 --- /dev/null +++ b/script-archive/selfieStick.js @@ -0,0 +1,121 @@ +// selfieStick.js +// +// Created by Faye Li on March 23, 2016 +// +// Usage instruction: Spacebar toggles camera control - WASD first person free movement or no movement but allowing others to grab the selfie stick +// and control your camera. +// For best result, turn off avatar collisions(Developer > Avatar > Uncheck Enable Avatar Collisions) +// + +// selfieStick.js +// +// Created by Faye Li on March 23, 2016 +// +// Usage instruction: Spacebar toggles camera control - WASD first person free movement or no movement but allowing others to grab the selfie stick +// and control your camera. +// + +(function() { // BEGIN LOCAL_SCOPE + var MODEL_URL = "https://hifi-content.s3.amazonaws.com/faye/twitch-stream/selfie_stick.json"; + var AVATAR_URL = "https://hifi-content.s3.amazonaws.com/jimi/avatar/camera/fst/camera.fst"; + var originalAvatar = null; + var importedEntityIDs = []; + var selfieStickEntityID = null; + var lensEntityID = null; + var freeMovementMode = true; + + turnOffAvatarCollisions(); + changeAvatar(); + importModel(); + processImportedEntities(); + setupSpaceBarControl(); + Script.update.connect(update); + + function turnOffAvatarCollisions() { + Menu.setIsOptionChecked("Enable avatar collisions", 0); + } + + function turnOnAvatarCollisions() { + Menu.setIsOptionChecked("Enable avatar collisions", 1); + } + + function changeAvatar() { + originalAvatar = MyAvatar.skeletonModelURL; + MyAvatar.skeletonModelURL = AVATAR_URL; + } + + function importModel() { + var success = Clipboard.importEntities(MODEL_URL); + var spawnLocation = MyAvatar.position; + if (success) { + importedEntityIDs = Clipboard.pasteEntities(spawnLocation); + } + } + + function processImportedEntities() { + importedEntityIDs.forEach(function(id){ + var props = Entities.getEntityProperties(id); + if (props.name === "Selfie Stick") { + selfieStickEntityID = id; + } else if (props.name === "Lens") { + lensEntityID = id; + } + }); + } + + function setupSpaceBarControl() { + var mappingName = "Handheld-Cam-Space-Bar"; + var myMapping = Controller.newMapping(mappingName); + myMapping.from(Controller.Hardware.Keyboard.Space).to(function(value){ + if ( value === 0 ) { + return; + } + if (freeMovementMode) { + freeMovementMode = false; + Camera.mode = "entity"; + Camera.cameraEntity = lensEntityID; + } else { + freeMovementMode = true; + Camera.mode = "first person"; + } + }); + Controller.enableMapping(mappingName); + } + + function update(deltaTime) { + if (freeMovementMode) { + var upFactor = 0.1; + var upUnitVec = Vec3.normalize(Quat.getUp(MyAvatar.orientation)); + var upOffset = Vec3.multiply(upUnitVec, upFactor); + var forwardFactor = -0.1; + var forwardUnitVec = Vec3.normalize(Quat.getFront(MyAvatar.orientation)); + var forwardOffset = Vec3.multiply(forwardUnitVec, forwardFactor); + var newPos = Vec3.sum(Vec3.sum(MyAvatar.position, upOffset), forwardOffset); + var newRot = MyAvatar.orientation; + Entities.editEntity(selfieStickEntityID, {position: newPos, rotation: newRot}); + } else { + var props = Entities.getEntityProperties(selfieStickEntityID); + var upFactor = 0.1; + var upUnitVec = Vec3.normalize(Quat.getUp(props.rotation)); + var upOffset = Vec3.multiply(upUnitVec, -upFactor); + var forwardFactor = -0.1; + var forwardUnitVec = Vec3.normalize(Quat.getFront(props.rotation)); + var forwardOffset = Vec3.multiply(forwardUnitVec, -forwardFactor); + var newPos = Vec3.sum(Vec3.sum(props.position, upOffset), forwardOffset); + MyAvatar.position = newPos; + MyAvatar.orientation = props.rotation; + } + } + + // Removes all entities we imported and reset settings we've changed + function cleanup() { + importedEntityIDs.forEach(function(id) { + Entities.deleteEntity(id); + }); + Camera.mode = "first person"; + Controller.disableMapping("Handheld-Cam-Space-Bar"); + MyAvatar.skeletonModelURL = originalAvatar; + turnOnAvatarCollisions(); + } + Script.scriptEnding.connect(cleanup); +}()); // END LOCAL_SCOPE \ No newline at end of file diff --git a/script-archive/vrShop/item/shopItemGrab.js b/script-archive/vrShop/item/shopItemGrab.js index a7226675eb..2b63ca1b53 100644 --- a/script-archive/vrShop/item/shopItemGrab.js +++ b/script-archive/vrShop/item/shopItemGrab.js @@ -382,7 +382,10 @@ function MyController(hand) { var grabbableData = getEntityCustomData(GRABBABLE_DATA_KEY, intersection.entityID, DEFAULT_GRABBABLE_DATA); - if (intersection.properties.name == "Grab Debug Entity") { + var properties = Entites.getEntityProperties(intersection.entityID, ["locked", "name"]); + + + if (properties.name == "Grab Debug Entity") { continue; } @@ -399,7 +402,7 @@ function MyController(hand) { this.grabbedEntity = intersection.entityID; this.setState(STATE_NEAR_TRIGGER); return; - } else if (!intersection.properties.locked) { + } else if (!properties.locked) { var ownerObj = getEntityCustomData('ownerKey', intersection.entityID, null); if (ownerObj == null || ownerObj.ownerID === MyAvatar.sessionUUID) { //I can only grab new or already mine items diff --git a/scripts/defaultScripts.js b/scripts/defaultScripts.js index 5d8813e988..81ce72d901 100644 --- a/scripts/defaultScripts.js +++ b/scripts/defaultScripts.js @@ -11,7 +11,7 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -var DEFAULT_SCRIPTS = [ +var DEFAULT_SCRIPTS_COMBINED = [ "system/progress.js", "system/away.js", "system/audio.js", @@ -21,23 +21,19 @@ var DEFAULT_SCRIPTS = [ "system/snapshot.js", "system/help.js", "system/pal.js", // "system/mod.js", // older UX, if you prefer - "system/goto.js", + "system/makeUserConnection.js", + "system/tablet-goto.js", "system/marketplaces/marketplaces.js", "system/edit.js", - "system/tablet-users.js", "system/selectAudioDevice.js", "system/notifications.js", - "system/controllers/controllerDisplayManager.js", - "system/controllers/handControllerGrab.js", - "system/controllers/handControllerPointer.js", - "system/controllers/squeezeHands.js", - "system/controllers/grab.js", - "system/controllers/teleport.js", - "system/controllers/toggleAdvancedMovementForHandControllers.js", "system/dialTone.js", "system/firstPersonHMD.js", "system/tablet-ui/tabletUI.js" ]; +var DEFAULT_SCRIPTS_SEPARATE = [ + "system/controllers/controllerScripts.js" +]; // add a menu item for debugging var MENU_CATEGORY = "Developer"; @@ -64,16 +60,24 @@ if (Menu.menuExists(MENU_CATEGORY) && !Menu.menuItemExists(MENU_CATEGORY, MENU_I }); } -function runDefaultsTogether() { - for (var j in DEFAULT_SCRIPTS) { - Script.include(DEFAULT_SCRIPTS[j]); +function loadSeparateDefaults() { + for (var i in DEFAULT_SCRIPTS_SEPARATE) { + Script.load(DEFAULT_SCRIPTS_SEPARATE[i]); } } -function runDefaultsSeparately() { - for (var i in DEFAULT_SCRIPTS) { - Script.load(DEFAULT_SCRIPTS[i]); +function runDefaultsTogether() { + for (var i in DEFAULT_SCRIPTS_COMBINED) { + Script.include(DEFAULT_SCRIPTS_COMBINED[i]); } + loadSeparateDefaults(); +} + +function runDefaultsSeparately() { + for (var i in DEFAULT_SCRIPTS_COMBINED) { + Script.load(DEFAULT_SCRIPTS_COMBINED[i]); + } + loadSeparateDefaults(); } // start all scripts diff --git a/scripts/developer/debugging/debugWindow.qml b/scripts/developer/debugging/debugWindow.qml index f046a949ef..20fa24358d 100644 --- a/scripts/developer/debugging/debugWindow.qml +++ b/scripts/developer/debugging/debugWindow.qml @@ -18,6 +18,9 @@ Rectangle { width: parent ? parent.width : 100 height: parent ? parent.height : 100 + signal moved(vector2d position); + signal resized(size size); + property var channel; TextArea { diff --git a/scripts/developer/libraries/jasmine/hifi-boot.js b/scripts/developer/libraries/jasmine/hifi-boot.js index 49d7fadd29..405b692d66 100644 --- a/scripts/developer/libraries/jasmine/hifi-boot.js +++ b/scripts/developer/libraries/jasmine/hifi-boot.js @@ -63,7 +63,7 @@ clearTimeout = Script.clearTimeout; clearInterval = Script.clearInterval; - var jasmine = jasmineRequire.core(jasmineRequire); + var jasmine = this.jasmine = jasmineRequire.core(jasmineRequire); var env = jasmine.getEnv(); diff --git a/scripts/developer/libraries/utils.js b/scripts/developer/libraries/utils.js index f39f4d7913..0da9703c87 100644 --- a/scripts/developer/libraries/utils.js +++ b/scripts/developer/libraries/utils.js @@ -311,3 +311,6 @@ clamp = function(val, min, max){ return Math.max(min, Math.min(max, val)) } +easeIn = function(t) { + return Math.pow(t / 1, 5); +} diff --git a/scripts/developer/tests/.gitignore b/scripts/developer/tests/.gitignore new file mode 100644 index 0000000000..7cacbf042c --- /dev/null +++ b/scripts/developer/tests/.gitignore @@ -0,0 +1 @@ +cube_texture.ktx \ No newline at end of file diff --git a/scripts/developer/tests/ambientSoundTest.js b/scripts/developer/tests/ambientSoundTest.js index 5b373715c0..d048d5f73d 100644 --- a/scripts/developer/tests/ambientSoundTest.js +++ b/scripts/developer/tests/ambientSoundTest.js @@ -4,7 +4,7 @@ var uuid = Entities.addEntity({ shape: "Icosahedron", dimensions: Vec3.HALF, script: Script.resolvePath('../../tutorials/entity_scripts/ambientSound.js'), - position: Vec3.sum(Vec3.multiply(5, Quat.getFront(MyAvatar.orientation)), MyAvatar.position), + position: Vec3.sum(Vec3.multiply(5, Quat.getForward(MyAvatar.orientation)), MyAvatar.position), userData: JSON.stringify({ soundURL: WAVE, maxVolume: 0.1, diff --git a/scripts/developer/tests/basicEntityTest/entitySpawner.js b/scripts/developer/tests/basicEntityTest/entitySpawner.js index a2f38f59eb..538e9145f5 100644 --- a/scripts/developer/tests/basicEntityTest/entitySpawner.js +++ b/scripts/developer/tests/basicEntityTest/entitySpawner.js @@ -2,7 +2,7 @@ orientation = Quat.safeEulerAngles(orientation); orientation.x = 0; orientation = Quat.fromVec3Degrees(orientation); - var center = Vec3.sum(MyAvatar.position, Vec3.multiply(3, Quat.getFront(orientation))); + var center = Vec3.sum(MyAvatar.position, Vec3.multiply(3, Quat.getForward(orientation))); // Math.random ensures no caching of script var SCRIPT_URL = Script.resolvePath("myEntityScript.js") diff --git a/scripts/developer/tests/batonSoundEntityTest/batonSoundTestEntitySpawner.js b/scripts/developer/tests/batonSoundEntityTest/batonSoundTestEntitySpawner.js index fdcef8d32c..f5fc35a1de 100644 --- a/scripts/developer/tests/batonSoundEntityTest/batonSoundTestEntitySpawner.js +++ b/scripts/developer/tests/batonSoundEntityTest/batonSoundTestEntitySpawner.js @@ -2,7 +2,7 @@ orientation = Quat.safeEulerAngles(orientation); orientation.x = 0; orientation = Quat.fromVec3Degrees(orientation); - var center = Vec3.sum(MyAvatar.position, Vec3.multiply(3, Quat.getFront(orientation))); + var center = Vec3.sum(MyAvatar.position, Vec3.multiply(3, Quat.getForward(orientation))); // Math.random ensures no caching of script var SCRIPT_URL = Script.resolvePath("batonSoundTestEntityScript.js") diff --git a/scripts/developer/tests/entityServerStampedeTest-entity.js b/scripts/developer/tests/entityServerStampedeTest-entity.js new file mode 100644 index 0000000000..781753908c --- /dev/null +++ b/scripts/developer/tests/entityServerStampedeTest-entity.js @@ -0,0 +1,21 @@ +(function() { + return { + preload: function(uuid) { + var props = Entities.getEntityProperties(uuid); + var shape = props.shape === 'Sphere' ? 'Hexagon' : 'Sphere'; + + Entities.editEntity(uuid, { + shape: shape, + color: { red: 0xff, green: 0xff, blue: 0xff }, + }); + this.name = props.name; + print("preload", this.name); + }, + unload: function(uuid) { + print("unload", this.name); + Entities.editEntity(uuid, { + color: { red: 0x0f, green: 0x0f, blue: 0xff }, + }); + }, + }; +}) diff --git a/scripts/developer/tests/entityServerStampedeTest.js b/scripts/developer/tests/entityServerStampedeTest.js new file mode 100644 index 0000000000..33aa53f9b1 --- /dev/null +++ b/scripts/developer/tests/entityServerStampedeTest.js @@ -0,0 +1,33 @@ +var NUM_ENTITIES = 100; +var RADIUS = 2; +var DIV = NUM_ENTITIES / Math.PI / 2; +var PASS_SCRIPT_URL = Script.resolvePath('entityServerStampedeTest-entity.js'); +var FAIL_SCRIPT_URL = Script.resolvePath('entityStampedeTest-entity-fail.js'); + +var origin = Vec3.sum(MyAvatar.position, Vec3.multiply(5, Quat.getForward(MyAvatar.orientation))); +origin.y += HMD.eyeHeight; + +var uuids = []; + +Script.scriptEnding.connect(function() { + uuids.forEach(function(id) { + Entities.deleteEntity(id); + }); +}); + +for (var i=0; i < NUM_ENTITIES; i++) { + var failGroup = i % 2; + uuids.push(Entities.addEntity({ + type: 'Shape', + shape: failGroup ? 'Sphere' : 'Icosahedron', + name: 'SERVER-entityStampedeTest-' + i, + lifetime: 120, + position: Vec3.sum(origin, Vec3.multiplyQbyV( + MyAvatar.orientation, { x: Math.sin(i / DIV) * RADIUS, y: Math.cos(i / DIV) * RADIUS, z: 0 } + )), + serverScripts: (failGroup ? FAIL_SCRIPT_URL : PASS_SCRIPT_URL) + Settings.getValue('cache_buster'), + dimensions: Vec3.HALF, + color: { red: 0, green: 0, blue: 0 }, + }, !Entities.serversExist())); +} + diff --git a/scripts/developer/tests/entityStampedeTest-entity-fail.js b/scripts/developer/tests/entityStampedeTest-entity-fail.js new file mode 100644 index 0000000000..53c0469055 --- /dev/null +++ b/scripts/developer/tests/entityStampedeTest-entity-fail.js @@ -0,0 +1,3 @@ +(function() { + throw new Error(Script.resolvePath('')); +}) diff --git a/scripts/developer/tests/entityStampedeTest-entity.js b/scripts/developer/tests/entityStampedeTest-entity.js new file mode 100644 index 0000000000..bab4efa8eb --- /dev/null +++ b/scripts/developer/tests/entityStampedeTest-entity.js @@ -0,0 +1,21 @@ +(function() { + return { + preload: function(uuid) { + var props = Entities.getEntityProperties(uuid); + var shape = props.shape === 'Sphere' ? 'Cube' : 'Sphere'; + + Entities.editEntity(uuid, { + shape: shape, + color: { red: 0xff, green: 0xff, blue: 0xff }, + }); + this.name = props.name; + print("preload", this.name); + }, + unload: function(uuid) { + print("unload", this.name); + Entities.editEntity(uuid, { + color: { red: 0xff, green: 0x0f, blue: 0x0f }, + }); + }, + }; +}) diff --git a/scripts/developer/tests/entityStampedeTest.js b/scripts/developer/tests/entityStampedeTest.js new file mode 100644 index 0000000000..644bf0a216 --- /dev/null +++ b/scripts/developer/tests/entityStampedeTest.js @@ -0,0 +1,32 @@ +var NUM_ENTITIES = 100; +var RADIUS = 2; +var DIV = NUM_ENTITIES / Math.PI / 2; +var PASS_SCRIPT_URL = Script.resolvePath('').replace('.js', '-entity.js'); +var FAIL_SCRIPT_URL = Script.resolvePath('').replace('.js', '-entity-fail.js'); + +var origin = Vec3.sum(MyAvatar.position, Vec3.multiply(5, Quat.getForward(MyAvatar.orientation))); +origin.y += HMD.eyeHeight; + +var uuids = []; + +Script.scriptEnding.connect(function() { + uuids.forEach(function(id) { + Entities.deleteEntity(id); + }); +}); + +for (var i=0; i < NUM_ENTITIES; i++) { + var failGroup = i % 2; + uuids.push(Entities.addEntity({ + type: 'Shape', + shape: failGroup ? 'Sphere' : 'Icosahedron', + name: 'entityStampedeTest-' + i, + lifetime: 120, + position: Vec3.sum(origin, Vec3.multiplyQbyV( + MyAvatar.orientation, { x: Math.sin(i / DIV) * RADIUS, y: Math.cos(i / DIV) * RADIUS, z: 0 } + )), + script: (failGroup ? FAIL_SCRIPT_URL : PASS_SCRIPT_URL) + Settings.getValue('cache_buster'), + dimensions: Vec3.HALF, + color: { red: 0, green: 0, blue: 0 }, + }, !Entities.serversExist())); +} diff --git a/scripts/developer/tests/lodTest.js b/scripts/developer/tests/lodTest.js index 4b6706cd70..ce91b54d0f 100644 --- a/scripts/developer/tests/lodTest.js +++ b/scripts/developer/tests/lodTest.js @@ -19,7 +19,7 @@ var WIDTH = MAX_DIM * NUM_SPHERES; var entities = []; var right = Quat.getRight(Camera.orientation); // Starting position will be 30 meters in front of the camera -var position = Vec3.sum(Camera.position, Vec3.multiply(30, Quat.getFront(Camera.orientation))); +var position = Vec3.sum(Camera.position, Vec3.multiply(30, Quat.getForward(Camera.orientation))); position = Vec3.sum(position, Vec3.multiply(-WIDTH/2, right)); for (var i = 0; i < NUM_SPHERES; ++i) { diff --git a/scripts/developer/tests/mat4test.js b/scripts/developer/tests/mat4test.js index ebce420dcb..4e835ec82f 100644 --- a/scripts/developer/tests/mat4test.js +++ b/scripts/developer/tests/mat4test.js @@ -141,12 +141,12 @@ function testInverse() { assert(mat4FuzzyEqual(IDENTITY, Mat4.multiply(test2, Mat4.inverse(test2)))); } -function testFront() { +function testForward() { var test0 = IDENTITY; - assert(mat4FuzzyEqual({x: 0, y: 0, z: -1}, Mat4.getFront(test0))); + assert(mat4FuzzyEqual({x: 0, y: 0, z: -1}, Mat4.getForward(test0))); var test1 = Mat4.createFromScaleRotAndTrans(ONE_HALF, ROT_Y_180, ONE_TWO_THREE); - assert(mat4FuzzyEqual({x: 0, y: 0, z: 1}, Mat4.getFront(test1))); + assert(mat4FuzzyEqual({x: 0, y: 0, z: 1}, Mat4.getForward(test1))); } function testMat4() { @@ -157,7 +157,7 @@ function testMat4() { testTransformPoint(); testTransformVector(); testInverse(); - testFront(); + testForward(); print("MAT4 TEST complete! (" + (testCount - failureCount) + "/" + testCount + ") tests passed!"); } diff --git a/scripts/developer/tests/performance/tribbles.js b/scripts/developer/tests/performance/tribbles.js index 4c04f8b5b7..c5735b7359 100644 --- a/scripts/developer/tests/performance/tribbles.js +++ b/scripts/developer/tests/performance/tribbles.js @@ -43,7 +43,7 @@ var HOW_FAR_UP = RANGE / 1.5; // higher (for uneven ground) above range/2 (for var totalCreated = 0; var offset = Vec3.sum(Vec3.multiply(HOW_FAR_UP, Vec3.UNIT_Y), - Vec3.multiply(HOW_FAR_IN_FRONT_OF_ME, Quat.getFront(Camera.orientation))); + Vec3.multiply(HOW_FAR_IN_FRONT_OF_ME, Quat.getForward(Camera.orientation))); var center = Vec3.sum(MyAvatar.position, offset); function randomVector(range) { diff --git a/scripts/developer/tests/rapidProceduralChange/rapidProceduralChangeTest.js b/scripts/developer/tests/rapidProceduralChange/rapidProceduralChangeTest.js index 6897a1b70f..e28a7b01e2 100644 --- a/scripts/developer/tests/rapidProceduralChange/rapidProceduralChangeTest.js +++ b/scripts/developer/tests/rapidProceduralChange/rapidProceduralChangeTest.js @@ -20,9 +20,9 @@ orientation = Quat.safeEulerAngles(orientation); orientation.x = 0; orientation = Quat.fromVec3Degrees(orientation); -var centerUp = Vec3.sum(MyAvatar.position, Vec3.multiply(3, Quat.getFront(orientation))); +var centerUp = Vec3.sum(MyAvatar.position, Vec3.multiply(3, Quat.getForward(orientation))); centerUp.y += 0.5; -var centerDown = Vec3.sum(MyAvatar.position, Vec3.multiply(3, Quat.getFront(orientation))); +var centerDown = Vec3.sum(MyAvatar.position, Vec3.multiply(3, Quat.getForward(orientation))); centerDown.y -= 0.5; var ENTITY_SHADER_URL = "https://s3-us-west-1.amazonaws.com/hifi-content/eric/shaders/uniformTest.fs"; diff --git a/scripts/developer/tests/scaling.png b/scripts/developer/tests/scaling.png new file mode 100644 index 0000000000..1e6a7df45d Binary files /dev/null and b/scripts/developer/tests/scaling.png differ diff --git a/scripts/developer/tests/sliderTest.html b/scripts/developer/tests/sliderTest.html new file mode 100644 index 0000000000..a672cfeaf8 --- /dev/null +++ b/scripts/developer/tests/sliderTest.html @@ -0,0 +1,157 @@ + + + + Slider Test + + + + + + + + + +
+

Slider Test

+
+
+

Native Input Range Slider

+

+ +

+

Bootstrap Slider

+

+ +

+
+ + + + + + + \ No newline at end of file diff --git a/scripts/developer/tests/sliderTestMain.js b/scripts/developer/tests/sliderTestMain.js new file mode 100644 index 0000000000..22bf4fa911 --- /dev/null +++ b/scripts/developer/tests/sliderTestMain.js @@ -0,0 +1,35 @@ +(function () { + var HTML_URL = Script.resolvePath("sliderTest.html"); + var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); + var button = tablet.addButton({ + text: "SLIDER" + }); + + function onClicked() { + tablet.gotoWebScreen(HTML_URL); + } + + button.clicked.connect(onClicked); + + var onSliderTestScreen = false; + function onScreenChanged(type, url) { + if (type === "Web" && url === HTML_URL) { + // when switching to the slider page, change inputMode to "Mouse", this should make the sliders work. + onSliderTestScreen = true; + Overlays.editOverlay(HMD.tabletScreenID, { inputMode: "Mouse" }); + } else if (onSliderTestScreen) { + // when switching off of the slider page, change inputMode to back to "Touch". + onSliderTestScreen = false; + Overlays.editOverlay(HMD.tabletScreenID, { inputMode: "Touch" }); + } + } + + tablet.screenChanged.connect(onScreenChanged); + + function cleanup() { + tablet.removeButton(button); + tablet.screenChanged.disconnect(onScreenChanged); + } + Script.scriptEnding.connect(cleanup); + +}()); diff --git a/scripts/developer/tests/sphereLODTest.js b/scripts/developer/tests/sphereLODTest.js index dc19094664..d0cb35eaa1 100644 --- a/scripts/developer/tests/sphereLODTest.js +++ b/scripts/developer/tests/sphereLODTest.js @@ -15,7 +15,7 @@ MyAvatar.orientation = Quat.fromPitchYawRollDegrees(0, 0, 0); orientation = Quat.safeEulerAngles(MyAvatar.orientation); orientation.x = 0; orientation = Quat.fromVec3Degrees(orientation); -var tablePosition = Vec3.sum(MyAvatar.position, Quat.getFront(orientation)); +var tablePosition = Vec3.sum(MyAvatar.position, Quat.getForward(orientation)); tablePosition.y += 0.5; diff --git a/scripts/developer/tests/tabletEventBridgeTest.js b/scripts/developer/tests/tabletEventBridgeTest.js index 1fa935bef2..83031741fc 100644 --- a/scripts/developer/tests/tabletEventBridgeTest.js +++ b/scripts/developer/tests/tabletEventBridgeTest.js @@ -16,66 +16,92 @@ var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); var tabletButton = tablet.addButton({ - text: "SOUNDS" + text: "SOUNDS", + icon: "http://s3.amazonaws.com/hifi-public/tony/icons/trombone-i.png", + activeIcon: "http://s3.amazonaws.com/hifi-public/tony/icons/trombone-a.png" }); var WEB_BRIDGE_TEST_HTML = "https://s3.amazonaws.com/hifi-public/tony/webBridgeTest.html?2"; - var TROMBONE_URL = "https://s3.amazonaws.com/hifi-public/tony/audio/sad-trombone.wav"; -var tromboneSound = SoundCache.getSound(TROMBONE_URL); -var tromboneInjector; - var SCREAM_URL = "https://s3.amazonaws.com/hifi-public/tony/audio/wilhelm-scream.wav"; -var screamSound = SoundCache.getSound(SCREAM_URL); -var screamInjector; tabletButton.clicked.connect(function () { - tablet.gotoWebScreen(WEB_BRIDGE_TEST_HTML); + if (shown) { + tablet.gotoHomeScreen(); + } else { + tablet.gotoWebScreen(WEB_BRIDGE_TEST_HTML); + } }); -// hook up to the event bridge -tablet.webEventReceived.connect(function (msg) { +var shown = false; + +function onScreenChanged(type, url) { + if (type === "Web" && url === WEB_BRIDGE_TEST_HTML) { + tabletButton.editProperties({isActive: true}); + + if (!shown) { + // hook up to the event bridge + tablet.webEventReceived.connect(onWebEventReceived); + } + shown = true; + } else { + tabletButton.editProperties({isActive: false}); + + if (shown) { + // disconnect from the event bridge + tablet.webEventReceived.disconnect(onWebEventReceived); + } + shown = false; + } +} + +tablet.screenChanged.connect(onScreenChanged); + +// ctor +function SoundBuddy(url) { + this.sound = SoundCache.getSound(url); + this.injector = null; +} + +SoundBuddy.prototype.play = function (options, doneCallback) { + if (this.sound.downloaded) { + if (this.injector) { + this.injector.setOptions(options); + this.injector.restart(); + } else { + this.injector = Audio.playSound(this.sound, options); + this.injector.finished.connect(function () { + if (doneCallback) { + doneCallback(); + } + }); + } + } +}; + +var tromboneSound = new SoundBuddy(TROMBONE_URL); +var screamSound = new SoundBuddy(SCREAM_URL); +var soundOptions = { position: MyAvatar.position, volume: 1.0, loop: false, localOnly: true }; + +function onWebEventReceived(msg) { Script.print("HIFI: recv web event = " + JSON.stringify(msg)); if (msg === "button-1-play") { - - // play sad trombone - if (tromboneSound.downloaded) { - if (tromboneInjector) { - tromboneInjector.restart(); - } else { - tromboneInjector = Audio.playSound(tromboneSound, { position: MyAvatar.position, - volume: 1.0, - loop: false }); - } - } - - // wait until sound is finished then send a done event - Script.setTimeout(function () { + soundOptions.position = MyAvatar.position; + tromboneSound.play(soundOptions, function () { tablet.emitScriptEvent("button-1-done"); - }, 3500); - } - - if (msg === "button-2-play") { - - // play scream - if (screamSound.downloaded) { - if (screamInjector) { - screamInjector.restart(); - } else { - screamInjector = Audio.playSound(screamSound, { position: MyAvatar.position, - volume: 1.0, - loop: false }); - } - } - - // wait until sound is finished then send a done event - Script.setTimeout(function () { + }); + } else if (msg === "button-2-play") { + soundOptions.position = MyAvatar.position; + screamSound.play(soundOptions, function () { tablet.emitScriptEvent("button-2-done"); - }, 1000); + }); } -}); +} Script.scriptEnding.connect(function () { tablet.removeButton(tabletButton); + if (shown) { + tablet.webEventReceived.disconnect(onWebEventReceived); + } }); diff --git a/scripts/developer/tests/testInterval.js b/scripts/developer/tests/testInterval.js index 94a5fe1fa5..7898610c6d 100644 --- a/scripts/developer/tests/testInterval.js +++ b/scripts/developer/tests/testInterval.js @@ -12,7 +12,7 @@ var UPDATE_HZ = 60; // standard script update rate var UPDATE_INTERVAL = 1000/UPDATE_HZ; // standard script update interval var UPDATE_WORK_EFFORT = 0; // 1000 is light work, 1000000 ~= 30ms -var basePosition = Vec3.sum(Camera.getPosition(), Quat.getFront(Camera.getOrientation())); +var basePosition = Vec3.sum(Camera.getPosition(), Quat.getForward(Camera.getOrientation())); var timerBox = Entities.addEntity( { type: "Box", diff --git a/scripts/developer/tests/unit_tests/entityUnitTests.js b/scripts/developer/tests/unit_tests/entityUnitTests.js index 730fceb144..f2c4b4871f 100644 --- a/scripts/developer/tests/unit_tests/entityUnitTests.js +++ b/scripts/developer/tests/unit_tests/entityUnitTests.js @@ -3,7 +3,7 @@ describe('Entity', function() { var center = Vec3.sum( MyAvatar.position, - Vec3.multiply(3, Quat.getFront(Camera.getOrientation())) + Vec3.multiply(3, Quat.getForward(Camera.getOrientation())) ); var boxEntity; var boxProps = { diff --git a/scripts/developer/tests/unit_tests/moduleTests/cycles/a.js b/scripts/developer/tests/unit_tests/moduleTests/cycles/a.js new file mode 100644 index 0000000000..265cfaa2df --- /dev/null +++ b/scripts/developer/tests/unit_tests/moduleTests/cycles/a.js @@ -0,0 +1,10 @@ +/* eslint-env node */ +var a = exports; +a.done = false; +var b = require('./b.js'); +a.done = true; +a.name = 'a'; +a['a.done?'] = a.done; +a['b.done?'] = b.done; + +print('from a.js a.done =', a.done, '/ b.done =', b.done); diff --git a/scripts/developer/tests/unit_tests/moduleTests/cycles/b.js b/scripts/developer/tests/unit_tests/moduleTests/cycles/b.js new file mode 100644 index 0000000000..c46c872828 --- /dev/null +++ b/scripts/developer/tests/unit_tests/moduleTests/cycles/b.js @@ -0,0 +1,10 @@ +/* eslint-env node */ +var b = exports; +b.done = false; +var a = require('./a.js'); +b.done = true; +b.name = 'b'; +b['a.done?'] = a.done; +b['b.done?'] = b.done; + +print('from b.js a.done =', a.done, '/ b.done =', b.done); diff --git a/scripts/developer/tests/unit_tests/moduleTests/cycles/main.js b/scripts/developer/tests/unit_tests/moduleTests/cycles/main.js new file mode 100644 index 0000000000..0ec39cd656 --- /dev/null +++ b/scripts/developer/tests/unit_tests/moduleTests/cycles/main.js @@ -0,0 +1,17 @@ +/* eslint-env node */ +/* global print */ +/* eslint-disable comma-dangle */ + +print('main.js'); +var a = require('./a.js'), + b = require('./b.js'); + +print('from main.js a.done =', a.done, 'and b.done =', b.done); + +module.exports = { + name: 'main', + a: a, + b: b, + 'a.done?': a.done, + 'b.done?': b.done, +}; diff --git a/scripts/developer/tests/unit_tests/moduleTests/entity/entityConstructorAPIException.js b/scripts/developer/tests/unit_tests/moduleTests/entity/entityConstructorAPIException.js new file mode 100644 index 0000000000..bbe694b578 --- /dev/null +++ b/scripts/developer/tests/unit_tests/moduleTests/entity/entityConstructorAPIException.js @@ -0,0 +1,13 @@ +/* eslint-disable comma-dangle */ +// test module method exception being thrown within main constructor +(function() { + var apiMethod = Script.require('../exceptions/exceptionInFunction.js'); + print(Script.resolvePath(''), "apiMethod", apiMethod); + // this next line throws from within apiMethod + print(apiMethod()); + return { + preload: function(uuid) { + print("entityConstructorAPIException::preload -- never seen --", uuid, Script.resolvePath('')); + }, + }; +}); diff --git a/scripts/developer/tests/unit_tests/moduleTests/entity/entityConstructorModule.js b/scripts/developer/tests/unit_tests/moduleTests/entity/entityConstructorModule.js new file mode 100644 index 0000000000..a4e8c17ab6 --- /dev/null +++ b/scripts/developer/tests/unit_tests/moduleTests/entity/entityConstructorModule.js @@ -0,0 +1,23 @@ +/* global module */ +/* eslint-disable comma-dangle */ +// test dual-purpose module and standalone Entity script +function MyEntity(filename) { + return { + preload: function(uuid) { + print("entityConstructorModule.js::preload"); + if (typeof module === 'object') { + print("module.filename", module.filename); + print("module.parent.filename", module.parent && module.parent.filename); + } + }, + clickDownOnEntity: function(uuid, evt) { + print("entityConstructorModule.js::clickDownOnEntity"); + }, + }; +} + +try { + module.exports = MyEntity; +} catch (e) {} // eslint-disable-line no-empty +print('entityConstructorModule::MyEntity', typeof MyEntity); +(MyEntity); diff --git a/scripts/developer/tests/unit_tests/moduleTests/entity/entityConstructorNested.js b/scripts/developer/tests/unit_tests/moduleTests/entity/entityConstructorNested.js new file mode 100644 index 0000000000..a90d979877 --- /dev/null +++ b/scripts/developer/tests/unit_tests/moduleTests/entity/entityConstructorNested.js @@ -0,0 +1,14 @@ +/* global module */ +// test Entity constructor based on inherited constructor from a module +function constructor() { + print("entityConstructorNested::constructor"); + var MyEntity = Script.require('./entityConstructorModule.js'); + return new MyEntity("-- created from entityConstructorNested --"); +} + +try { + module.exports = constructor; +} catch (e) { + constructor; +} + diff --git a/scripts/developer/tests/unit_tests/moduleTests/entity/entityConstructorNested2.js b/scripts/developer/tests/unit_tests/moduleTests/entity/entityConstructorNested2.js new file mode 100644 index 0000000000..29e0ed65b1 --- /dev/null +++ b/scripts/developer/tests/unit_tests/moduleTests/entity/entityConstructorNested2.js @@ -0,0 +1,25 @@ +/* global module */ +// test Entity constructor based on nested, inherited module constructors +function constructor() { + print("entityConstructorNested2::constructor"); + + // inherit from entityConstructorNested + var MyEntity = Script.require('./entityConstructorNested.js'); + function SubEntity() {} + SubEntity.prototype = new MyEntity('-- created from entityConstructorNested2 --'); + + // create new instance + var entity = new SubEntity(); + // "override" clickDownOnEntity for just this new instance + entity.clickDownOnEntity = function(uuid, evt) { + print("entityConstructorNested2::clickDownOnEntity"); + SubEntity.prototype.clickDownOnEntity.apply(this, arguments); + }; + return entity; +} + +try { + module.exports = constructor; +} catch (e) { + constructor; +} diff --git a/scripts/developer/tests/unit_tests/moduleTests/entity/entityConstructorRequireException.js b/scripts/developer/tests/unit_tests/moduleTests/entity/entityConstructorRequireException.js new file mode 100644 index 0000000000..5872bce529 --- /dev/null +++ b/scripts/developer/tests/unit_tests/moduleTests/entity/entityConstructorRequireException.js @@ -0,0 +1,10 @@ +/* eslint-disable comma-dangle */ +// test module-related exception from within "require" evaluation itself +(function() { + var mod = Script.require('../exceptions/exception.js'); + return { + preload: function(uuid) { + print("entityConstructorRequireException::preload (never happens)", uuid, Script.resolvePath(''), mod); + }, + }; +}); diff --git a/scripts/developer/tests/unit_tests/moduleTests/entity/entityPreloadAPIError.js b/scripts/developer/tests/unit_tests/moduleTests/entity/entityPreloadAPIError.js new file mode 100644 index 0000000000..eaee178b0a --- /dev/null +++ b/scripts/developer/tests/unit_tests/moduleTests/entity/entityPreloadAPIError.js @@ -0,0 +1,13 @@ +/* eslint-disable comma-dangle */ +// test module method exception being thrown within preload +(function() { + var apiMethod = Script.require('../exceptions/exceptionInFunction.js'); + print(Script.resolvePath(''), "apiMethod", apiMethod); + return { + preload: function(uuid) { + // this next line throws from within apiMethod + print(apiMethod()); + print("entityPreloadAPIException::preload -- never seen --", uuid, Script.resolvePath('')); + }, + }; +}); diff --git a/scripts/developer/tests/unit_tests/moduleTests/entity/entityPreloadRequire.js b/scripts/developer/tests/unit_tests/moduleTests/entity/entityPreloadRequire.js new file mode 100644 index 0000000000..50dab9fa7c --- /dev/null +++ b/scripts/developer/tests/unit_tests/moduleTests/entity/entityPreloadRequire.js @@ -0,0 +1,11 @@ +/* eslint-disable comma-dangle */ +// test requiring a module from within preload +(function constructor() { + return { + preload: function(uuid) { + print("entityPreloadRequire::preload"); + var example = Script.require('../example.json'); + print("entityPreloadRequire::example::name", example.name); + }, + }; +}); diff --git a/scripts/developer/tests/unit_tests/moduleTests/example.json b/scripts/developer/tests/unit_tests/moduleTests/example.json new file mode 100644 index 0000000000..42d7fe07da --- /dev/null +++ b/scripts/developer/tests/unit_tests/moduleTests/example.json @@ -0,0 +1,9 @@ +{ + "name": "Example JSON Module", + "last-modified": 1485789862, + "config": { + "title": "My Title", + "width": 800, + "height": 600 + } +} diff --git a/scripts/developer/tests/unit_tests/moduleTests/exceptions/exception.js b/scripts/developer/tests/unit_tests/moduleTests/exceptions/exception.js new file mode 100644 index 0000000000..8d25d6b7a4 --- /dev/null +++ b/scripts/developer/tests/unit_tests/moduleTests/exceptions/exception.js @@ -0,0 +1,4 @@ +/* eslint-env node */ +module.exports = "n/a"; +throw new Error('exception on line 2'); + diff --git a/scripts/developer/tests/unit_tests/moduleTests/exceptions/exceptionInFunction.js b/scripts/developer/tests/unit_tests/moduleTests/exceptions/exceptionInFunction.js new file mode 100644 index 0000000000..69415a0741 --- /dev/null +++ b/scripts/developer/tests/unit_tests/moduleTests/exceptions/exceptionInFunction.js @@ -0,0 +1,38 @@ +/* eslint-env node */ +// dummy lines to make sure exception line number is well below parent test script +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// + + +function myfunc() { + throw new Error('exception on line 32 in myfunc'); +} +module.exports = myfunc; +if (Script[module.filename] === 'throw') { + myfunc(); +} diff --git a/scripts/developer/tests/unit_tests/moduleUnitTests.js b/scripts/developer/tests/unit_tests/moduleUnitTests.js new file mode 100644 index 0000000000..6810dd8b6d --- /dev/null +++ b/scripts/developer/tests/unit_tests/moduleUnitTests.js @@ -0,0 +1,378 @@ +/* eslint-env jasmine, node */ +/* global print:true, Script:true, global:true, require:true */ +/* eslint-disable comma-dangle */ +var isNode = instrumentTestrunner(), + runInterfaceTests = !isNode, + runNetworkTests = true; + +// describe wrappers (note: `xdescribe` indicates a disabled or "pending" jasmine test) +var INTERFACE = { describe: runInterfaceTests ? describe : xdescribe }, + NETWORK = { describe: runNetworkTests ? describe : xdescribe }; + +describe('require', function() { + describe('resolve', function() { + it('should resolve relative filenames', function() { + var expected = Script.resolvePath('./moduleTests/example.json'); + expect(require.resolve('./moduleTests/example.json')).toEqual(expected); + }); + describe('exceptions', function() { + it('should reject blank "" module identifiers', function() { + expect(function() { + require.resolve(''); + }).toThrowError(/Cannot find/); + }); + it('should reject excessive identifier sizes', function() { + expect(function() { + require.resolve(new Array(8193).toString()); + }).toThrowError(/Cannot find/); + }); + it('should reject implicitly-relative filenames', function() { + expect(function() { + var mod = require.resolve('example.js'); + mod.exists; + }).toThrowError(/Cannot find/); + }); + it('should reject unanchored, existing filenames with advice', function() { + expect(function() { + var mod = require.resolve('moduleTests/example.json'); + mod.exists; + }).toThrowError(/use '.\/moduleTests\/example\.json'/); + }); + it('should reject unanchored, non-existing filenames', function() { + expect(function() { + var mod = require.resolve('asdfssdf/example.json'); + mod.exists; + }).toThrowError(/Cannot find.*system module not found/); + }); + it('should reject non-existent filenames', function() { + expect(function() { + require.resolve('./404error.js'); + }).toThrowError(/Cannot find/); + }); + it('should reject identifiers resolving to a directory', function() { + expect(function() { + var mod = require.resolve('.'); + mod.exists; + // console.warn('resolved(.)', mod); + }).toThrowError(/Cannot find/); + expect(function() { + var mod = require.resolve('..'); + mod.exists; + // console.warn('resolved(..)', mod); + }).toThrowError(/Cannot find/); + expect(function() { + var mod = require.resolve('../'); + mod.exists; + // console.warn('resolved(../)', mod); + }).toThrowError(/Cannot find/); + }); + (isNode ? xit : it)('should reject non-system, extensionless identifiers', function() { + expect(function() { + require.resolve('./example'); + }).toThrowError(/Cannot find/); + }); + }); + }); + + describe('JSON', function() { + it('should import .json modules', function() { + var example = require('./moduleTests/example.json'); + expect(example.name).toEqual('Example JSON Module'); + }); + // noet: support for loading JSON via content type workarounds reverted + // (leaving these tests intact in case ever revisited later) + // INTERFACE.describe('interface', function() { + // NETWORK.describe('network', function() { + // xit('should import #content-type=application/json modules', function() { + // var results = require('https://jsonip.com#content-type=application/json'); + // expect(results.ip).toMatch(/^[.0-9]+$/); + // }); + // xit('should import content-type: application/json modules', function() { + // var scope = { 'content-type': 'application/json' }; + // var results = require.call(scope, 'https://jsonip.com'); + // expect(results.ip).toMatch(/^[.0-9]+$/); + // }); + // }); + // }); + + }); + + INTERFACE.describe('system', function() { + it('require("vec3")', function() { + expect(require('vec3')).toEqual(jasmine.any(Function)); + }); + it('require("vec3").method', function() { + expect(require('vec3')().isValid).toEqual(jasmine.any(Function)); + }); + it('require("vec3") as constructor', function() { + var vec3 = require('vec3'); + var v = vec3(1.1, 2.2, 3.3); + expect(v).toEqual(jasmine.any(Object)); + expect(v.isValid).toEqual(jasmine.any(Function)); + expect(v.isValid()).toBe(true); + expect(v.toString()).toEqual('[Vec3 (1.100,2.200,3.300)]'); + }); + }); + + describe('cache', function() { + it('should cache modules by resolved module id', function() { + var value = new Date; + var example = require('./moduleTests/example.json'); + // earmark the module object with a unique value + example['.test'] = value; + var example2 = require('../../tests/unit_tests/moduleTests/example.json'); + expect(example2).toBe(example); + // verify earmark is still the same after a second require() + expect(example2['.test']).toBe(example['.test']); + }); + it('should reload cached modules set to null', function() { + var value = new Date; + var example = require('./moduleTests/example.json'); + example['.test'] = value; + require.cache[require.resolve('../../tests/unit_tests/moduleTests/example.json')] = null; + var example2 = require('../../tests/unit_tests/moduleTests/example.json'); + // verify the earmark is *not* the same as before + expect(example2).not.toBe(example); + expect(example2['.test']).not.toBe(example['.test']); + }); + it('should reload when module property is deleted', function() { + var value = new Date; + var example = require('./moduleTests/example.json'); + example['.test'] = value; + delete require.cache[require.resolve('../../tests/unit_tests/moduleTests/example.json')]; + var example2 = require('../../tests/unit_tests/moduleTests/example.json'); + // verify the earmark is *not* the same as before + expect(example2).not.toBe(example); + expect(example2['.test']).not.toBe(example['.test']); + }); + }); + + describe('cyclic dependencies', function() { + describe('should allow lazy-ref cyclic module resolution', function() { + var main; + beforeEach(function() { + // eslint-disable-next-line + try { this._print = print; } catch (e) {} + // during these tests print() is no-op'd so that it doesn't disrupt the reporter output + print = function() {}; + Script.resetModuleCache(); + }); + afterEach(function() { + print = this._print; + }); + it('main is requirable', function() { + main = require('./moduleTests/cycles/main.js'); + expect(main).toEqual(jasmine.any(Object)); + }); + it('transient a and b done values', function() { + expect(main.a['b.done?']).toBe(true); + expect(main.b['a.done?']).toBe(false); + }); + it('ultimate a.done?', function() { + expect(main['a.done?']).toBe(true); + }); + it('ultimate b.done?', function() { + expect(main['b.done?']).toBe(true); + }); + }); + }); + + describe('JS', function() { + it('should throw catchable local file errors', function() { + expect(function() { + require('file:///dev/null/non-existent-file.js'); + }).toThrowError(/path not found|Cannot find.*non-existent-file/); + }); + it('should throw catchable invalid id errors', function() { + expect(function() { + require(new Array(4096 * 2).toString()); + }).toThrowError(/invalid.*size|Cannot find.*,{30}/); + }); + it('should throw catchable unresolved id errors', function() { + expect(function() { + require('foobar:/baz.js'); + }).toThrowError(/could not resolve|Cannot find.*foobar:/); + }); + + NETWORK.describe('network', function() { + // note: depending on retries these tests can take up to 60 seconds each to timeout + var timeout = 75 * 1000; + it('should throw catchable host errors', function() { + expect(function() { + var mod = require('http://non.existent.highfidelity.io/moduleUnitTest.js'); + print("mod", Object.keys(mod)); + }).toThrowError(/error retrieving script .ServerUnavailable.|Cannot find.*non.existent/); + }, timeout); + it('should throw catchable network timeouts', function() { + expect(function() { + require('http://ping.highfidelity.io:1024'); + }).toThrowError(/error retrieving script .Timeout.|Cannot find.*ping.highfidelity/); + }, timeout); + }); + }); + + INTERFACE.describe('entity', function() { + var sampleScripts = [ + 'entityConstructorAPIException.js', + 'entityConstructorModule.js', + 'entityConstructorNested2.js', + 'entityConstructorNested.js', + 'entityConstructorRequireException.js', + 'entityPreloadAPIError.js', + 'entityPreloadRequire.js', + ].filter(Boolean).map(function(id) { + return Script.require.resolve('./moduleTests/entity/'+id); + }); + + var uuids = []; + function cleanup() { + uuids.splice(0,uuids.length).forEach(function(uuid) { + Entities.deleteEntity(uuid); + }); + } + afterAll(cleanup); + // extra sanity check to avoid lingering entities + Script.scriptEnding.connect(cleanup); + + for (var i=0; i < sampleScripts.length; i++) { + maketest(i); + } + + function maketest(i) { + var script = sampleScripts[ i % sampleScripts.length ]; + var shortname = '['+i+'] ' + script.split('/').pop(); + var position = MyAvatar.position; + position.y -= i/2; + // define a unique jasmine test for the current entity script + it(shortname, function(done) { + var uuid = Entities.addEntity({ + text: shortname, + description: Script.resolvePath('').split('/').pop(), + type: 'Text', + position: position, + rotation: MyAvatar.orientation, + script: script, + scriptTimestamp: +new Date, + lifetime: 20, + lineHeight: 1/8, + dimensions: { x: 2, y: 0.5, z: 0.01 }, + backgroundColor: { red: 0, green: 0, blue: 0 }, + color: { red: 0xff, green: 0xff, blue: 0xff }, + }, !Entities.serversExist() || !Entities.canRezTmp()); + uuids.push(uuid); + function stopChecking() { + if (ii) { + Script.clearInterval(ii); + ii = 0; + } + } + var ii = Script.setInterval(function() { + Entities.queryPropertyMetadata(uuid, "script", function(err, result) { + if (err) { + stopChecking(); + throw new Error(err); + } + if (result.success) { + stopChecking(); + if (/Exception/.test(script)) { + expect(result.status).toMatch(/^error_(loading|running)_script$/); + } else { + expect(result.status).toEqual("running"); + } + Entities.deleteEntity(uuid); + done(); + } else { + print('!result.success', JSON.stringify(result)); + } + }); + }, 100); + Script.setTimeout(stopChecking, 4900); + }, 5000 /* jasmine async timeout */); + } + }); +}); + +// support for isomorphic Node.js / Interface unit testing +// note: run `npm install` from unit_tests/ and then `node moduleUnitTests.js` +function run() {} +function instrumentTestrunner() { + var isNode = typeof process === 'object' && process.title === 'node'; + if (typeof describe === 'function') { + // already running within a test runner; assume jasmine is ready-to-go + return isNode; + } + if (isNode) { + /* eslint-disable no-console */ + // Node.js test mode + // to keep things consistent Node.js uses the local jasmine.js library (instead of an npm version) + var jasmineRequire = require('../../libraries/jasmine/jasmine.js'); + var jasmine = jasmineRequire.core(jasmineRequire); + var env = jasmine.getEnv(); + var jasmineInterface = jasmineRequire.interface(jasmine, env); + for (var p in jasmineInterface) { + global[p] = jasmineInterface[p]; + } + env.addReporter(new (require('jasmine-console-reporter'))); + // testing mocks + Script = { + resetModuleCache: function() { + module.require.cache = {}; + }, + setTimeout: setTimeout, + clearTimeout: clearTimeout, + resolvePath: function(id) { + // this attempts to accurately emulate how Script.resolvePath works + var trace = {}; Error.captureStackTrace(trace); + var base = trace.stack.split('\n')[2].replace(/^.*[(]|[)].*$/g,'').replace(/:[0-9]+:[0-9]+.*$/,''); + if (!id) { + return base; + } + var rel = base.replace(/[^\/]+$/, id); + console.info('rel', rel); + return require.resolve(rel); + }, + require: function(mod) { + return require(Script.require.resolve(mod)); + }, + }; + Script.require.cache = require.cache; + Script.require.resolve = function(mod) { + if (mod === '.' || /^\.\.($|\/)/.test(mod)) { + throw new Error("Cannot find module '"+mod+"' (is dir)"); + } + var path = require.resolve(mod); + // console.info('node-require-reoslved', mod, path); + try { + if (require('fs').lstatSync(path).isDirectory()) { + throw new Error("Cannot find module '"+path+"' (is directory)"); + } + // console.info('!path', path); + } catch (e) { + console.error(e); + } + return path; + }; + print = console.info.bind(console, '[print]'); + /* eslint-enable no-console */ + } else { + // Interface test mode + global = this; + Script.require('../../../system/libraries/utils.js'); + this.jasmineRequire = Script.require('../../libraries/jasmine/jasmine.js'); + Script.require('../../libraries/jasmine/hifi-boot.js'); + require = Script.require; + // polyfill console + /* global console:true */ + console = { + log: print, + info: print.bind(this, '[info]'), + warn: print.bind(this, '[warn]'), + error: print.bind(this, '[error]'), + debug: print.bind(this, '[debug]'), + }; + } + // eslint-disable-next-line + run = function() { global.jasmine.getEnv().execute(); }; + return isNode; +} +run(); diff --git a/scripts/developer/tests/unit_tests/package.json b/scripts/developer/tests/unit_tests/package.json new file mode 100644 index 0000000000..91d719b687 --- /dev/null +++ b/scripts/developer/tests/unit_tests/package.json @@ -0,0 +1,6 @@ +{ + "name": "unit_tests", + "devDependencies": { + "jasmine-console-reporter": "^1.2.7" + } +} diff --git a/scripts/developer/tests/unit_tests/scriptTests/error.js b/scripts/developer/tests/unit_tests/scriptTests/error.js new file mode 100644 index 0000000000..6b9a7c7445 --- /dev/null +++ b/scripts/developer/tests/unit_tests/scriptTests/error.js @@ -0,0 +1,6 @@ +afterError = false; +throw new Error('error.js'); +afterError = true; + +(1,eval)('this').$finishes.push(Script.resolvePath('')); + diff --git a/scripts/developer/tests/unit_tests/scriptTests/nested-error.js b/scripts/developer/tests/unit_tests/scriptTests/nested-error.js new file mode 100644 index 0000000000..3a190545ef --- /dev/null +++ b/scripts/developer/tests/unit_tests/scriptTests/nested-error.js @@ -0,0 +1,10 @@ +afterError = false; +outer = null; +Script.include('./nested/error.js?' + Settings.getValue('cache_buster')); +outer = { + inner: inner.lib, + sibling: sibling.lib, +}; +afterError = true; + +(1,eval)("this").$finishes.push(Script.resolvePath('')); diff --git a/scripts/developer/tests/unit_tests/scriptTests/nested-syntax-error.js b/scripts/developer/tests/unit_tests/scriptTests/nested-syntax-error.js new file mode 100644 index 0000000000..fb0e3679ff --- /dev/null +++ b/scripts/developer/tests/unit_tests/scriptTests/nested-syntax-error.js @@ -0,0 +1,10 @@ +afterError = false; +outer = null; +Script.include('./nested/syntax-error.js?' + Settings.getValue('cache_buster')); +outer = { + inner: inner.lib, + sibling: sibling.lib, +}; +afterError = true; + +(1,eval)("this").$finishes.push(Script.resolvePath('')); diff --git a/scripts/developer/tests/unit_tests/scriptTests/nested/error.js b/scripts/developer/tests/unit_tests/scriptTests/nested/error.js new file mode 100644 index 0000000000..aeb76eec01 --- /dev/null +++ b/scripts/developer/tests/unit_tests/scriptTests/nested/error.js @@ -0,0 +1,5 @@ +afterError = false; +throw new Error('nested/error.js'); +afterError = true; + +(1,eval)("this").$finishes.push(Script.resolvePath('')); diff --git a/scripts/developer/tests/unit_tests/scriptTests/nested/lib.js b/scripts/developer/tests/unit_tests/scriptTests/nested/lib.js new file mode 100644 index 0000000000..1c2cf3b885 --- /dev/null +++ b/scripts/developer/tests/unit_tests/scriptTests/nested/lib.js @@ -0,0 +1,5 @@ +Script.include('sibling.js'); +inner = { + lib: "nested/lib.js", +}; + diff --git a/scripts/developer/tests/unit_tests/scriptTests/nested/sibling.js b/scripts/developer/tests/unit_tests/scriptTests/nested/sibling.js new file mode 100644 index 0000000000..33fa068079 --- /dev/null +++ b/scripts/developer/tests/unit_tests/scriptTests/nested/sibling.js @@ -0,0 +1,3 @@ +sibling = { + lib: "nested/sibling", +}; diff --git a/scripts/developer/tests/unit_tests/scriptTests/nested/syntax-error.js b/scripts/developer/tests/unit_tests/scriptTests/nested/syntax-error.js new file mode 100644 index 0000000000..3b578c2674 --- /dev/null +++ b/scripts/developer/tests/unit_tests/scriptTests/nested/syntax-error.js @@ -0,0 +1,3 @@ +function() { + // intentional SyntaxError... + diff --git a/scripts/developer/tests/unit_tests/scriptTests/top-level-error.js b/scripts/developer/tests/unit_tests/scriptTests/top-level-error.js new file mode 100644 index 0000000000..4ef90ec238 --- /dev/null +++ b/scripts/developer/tests/unit_tests/scriptTests/top-level-error.js @@ -0,0 +1,11 @@ +afterError = false; +outer = null; +Script.include('./nested/lib.js'); +Undefined_symbol; +outer = { + inner: inner.lib, + sibling: sibling.lib, +}; +afterError = true; + +(1,eval)("this").$finishes.push(Script.resolvePath('')); diff --git a/scripts/developer/tests/unit_tests/scriptTests/top-level.js b/scripts/developer/tests/unit_tests/scriptTests/top-level.js new file mode 100644 index 0000000000..ab55007fe9 --- /dev/null +++ b/scripts/developer/tests/unit_tests/scriptTests/top-level.js @@ -0,0 +1,5 @@ +Script.include('./nested/lib.js'); +outer = { + inner: inner.lib, + sibling: sibling.lib, +}; diff --git a/scripts/developer/tests/unit_tests/scriptUnitTests.js b/scripts/developer/tests/unit_tests/scriptUnitTests.js new file mode 100644 index 0000000000..fa8cb44608 --- /dev/null +++ b/scripts/developer/tests/unit_tests/scriptUnitTests.js @@ -0,0 +1,141 @@ +/* eslint-env jasmine */ + +instrument_testrunner(); + +describe('Script', function () { + // get the current filename without calling Script.resolvePath('') + try { + throw new Error('stack'); + } catch(e) { + var filename = e.fileName; + var dirname = filename.split('/').slice(0, -1).join('/') + '/'; + var parentdir = dirname.split('/').slice(0, -2).join('/') + '/'; + } + + // characterization tests + // initially these are just to capture how the app works currently + var testCases = { + // special relative resolves + '': filename, + '.': dirname, + '..': parentdir, + + // local file "magic" tilde path expansion + '/~/defaultScripts.js': ScriptDiscoveryService.defaultScriptsPath + '/defaultScripts.js', + + // these schemes appear to always get resolved to empty URLs + 'qrc://test': '', + 'about:Entities 1': '', + 'ftp://host:port/path': '', + 'data:text/html;text,foo': '', + + 'Entities 1': dirname + 'Entities 1', + './file.js': dirname + 'file.js', + 'c:/temp/': 'file:///c:/temp/', + 'c:/temp': 'file:///c:/temp', + '/temp/': 'file:///temp/', + 'c:/': 'file:///c:/', + 'c:': 'file:///c:', + 'file:///~/libraries/a.js': 'file:///~/libraries/a.js', + '/temp/tested/../file.js': 'file:///temp/tested/../file.js', + '/~/libraries/utils.js': 'file:///~/libraries/utils.js', + '/temp/file.js': 'file:///temp/file.js', + '/~/': 'file:///~/', + + // these schemes appear to always get resolved to the same URL again + 'http://highfidelity.com': 'http://highfidelity.com', + 'atp:/highfidelity': 'atp:/highfidelity', + 'atp:c2d7e3a48cadf9ba75e4f8d9f4d80e75276774880405a093fdee36543aa04f': + 'atp:c2d7e3a48cadf9ba75e4f8d9f4d80e75276774880405a093fdee36543aa04f', + }; + describe('resolvePath', function () { + Object.keys(testCases).forEach(function(input) { + it('(' + JSON.stringify(input) + ')', function () { + expect(Script.resolvePath(input)).toEqual(testCases[input]); + }); + }); + }); + + describe('include', function () { + var old_cache_buster; + var cache_buster = '#' + new Date().getTime().toString(36); + beforeAll(function() { + old_cache_buster = Settings.getValue('cache_buster'); + Settings.setValue('cache_buster', cache_buster); + }); + afterAll(function() { + Settings.setValue('cache_buster', old_cache_buster); + }); + beforeEach(function() { + vec3toStr = undefined; + }); + it('file:///~/system/libraries/utils.js' + cache_buster, function() { + Script.include('file:///~/system/libraries/utils.js' + cache_buster); + expect(vec3toStr).toEqual(jasmine.any(Function)); + }); + it('nested' + cache_buster, function() { + Script.include('./scriptTests/top-level.js' + cache_buster); + expect(outer).toEqual(jasmine.any(Object)); + expect(inner).toEqual(jasmine.any(Object)); + expect(sibling).toEqual(jasmine.any(Object)); + expect(outer.inner).toEqual(inner.lib); + expect(outer.sibling).toEqual(sibling.lib); + }); + describe('errors' + cache_buster, function() { + var finishes, oldFinishes; + beforeAll(function() { + oldFinishes = (1,eval)('this').$finishes; + }); + afterAll(function() { + (1,eval)('this').$finishes = oldFinishes; + }); + beforeEach(function() { + finishes = (1,eval)('this').$finishes = []; + }); + it('error', function() { + // a thrown Error in top-level include aborts that include, but does not throw the error back to JS + expect(function() { + Script.include('./scriptTests/error.js' + cache_buster); + }).not.toThrowError("error.js"); + expect(finishes.length).toBe(0); + }); + it('top-level-error', function() { + // an organice Error in a top-level include aborts that include, but does not throw the error + expect(function() { + Script.include('./scriptTests/top-level-error.js' + cache_buster); + }).not.toThrowError(/Undefined_symbol/); + expect(finishes.length).toBe(0); + }); + it('nested', function() { + // a thrown Error in a nested include aborts the nested include, but does not abort the top-level script + expect(function() { + Script.include('./scriptTests/nested-error.js' + cache_buster); + }).not.toThrowError("nested/error.js"); + expect(finishes.length).toBe(1); + }); + it('nested-syntax-error', function() { + // a SyntaxError in a nested include breaks only that include (the main script should finish unimpeded) + expect(function() { + Script.include('./scriptTests/nested-syntax-error.js' + cache_buster); + }).not.toThrowError(/SyntaxEror/); + expect(finishes.length).toBe(1); + }); + }); + }); +}); + +// enable scriptUnitTests to be loaded directly +function run() {} +function instrument_testrunner() { + if (typeof describe === 'undefined') { + print('instrumenting jasmine', Script.resolvePath('')); + Script.include('../../libraries/jasmine/jasmine.js'); + Script.include('../../libraries/jasmine/hifi-boot.js'); + jasmine.getEnv().addReporter({ jasmineDone: Script.stop }); + run = function() { + print('executing jasmine', Script.resolvePath('')); + jasmine.getEnv().execute(); + }; + } +} +run(); diff --git a/scripts/developer/tests/viveTouchpadTest.js b/scripts/developer/tests/viveTouchpadTest.js index 913da5888d..b5d9575adf 100644 --- a/scripts/developer/tests/viveTouchpadTest.js +++ b/scripts/developer/tests/viveTouchpadTest.js @@ -24,10 +24,10 @@ var boxZAxis, boxYAxis; var prevThumbDown = false; function init() { - boxPosition = Vec3.sum(MyAvatar.position, Vec3.multiply(3, Quat.getFront(Camera.getOrientation()))); - var front = Quat.getFront(Camera.getOrientation()); - boxZAxis = Vec3.normalize(Vec3.cross(front, Y_AXIS)); - boxYAxis = Vec3.normalize(Vec3.cross(boxZAxis, front)); + boxPosition = Vec3.sum(MyAvatar.position, Vec3.multiply(3, Quat.getForward(Camera.getOrientation()))); + var forward = Quat.getForward(Camera.getOrientation()); + boxZAxis = Vec3.normalize(Vec3.cross(forward, Y_AXIS)); + boxYAxis = Vec3.normalize(Vec3.cross(boxZAxis, forward)); boxEntity = Entities.addEntity({ type: "Box", diff --git a/scripts/developer/tests/viveTrackedObjects.js b/scripts/developer/tests/viveTrackedObjects.js new file mode 100644 index 0000000000..78911538e4 --- /dev/null +++ b/scripts/developer/tests/viveTrackedObjects.js @@ -0,0 +1,36 @@ + +var TRACKED_OBJECT_POSES = [ + "TrackedObject00", "TrackedObject01", "TrackedObject02", "TrackedObject03", + "TrackedObject04", "TrackedObject05", "TrackedObject06", "TrackedObject07", + "TrackedObject08", "TrackedObject09", "TrackedObject10", "TrackedObject11", + "TrackedObject12", "TrackedObject13", "TrackedObject14", "TrackedObject15" +]; + +function init() { + Script.update.connect(update); +} + +function shutdown() { + Script.update.disconnect(update); + + TRACKED_OBJECT_POSES.forEach(function (key) { + DebugDraw.removeMyAvatarMarker(key); + }); +} + +var WHITE = {x: 1, y: 1, z: 1, w: 1}; + +function update(dt) { + if (Controller.Hardware.Vive) { + TRACKED_OBJECT_POSES.forEach(function (key) { + var pose = Controller.getPoseValue(Controller.Hardware.Vive[key]); + if (pose.valid) { + DebugDraw.addMyAvatarMarker(key, pose.rotation, pose.translation, WHITE); + } else { + DebugDraw.removeMyAvatarMarker(key); + } + }); + } +} + +init(); diff --git a/scripts/developer/utilities/audio/stats.qml b/scripts/developer/utilities/audio/Stats.qml similarity index 93% rename from scripts/developer/utilities/audio/stats.qml rename to scripts/developer/utilities/audio/Stats.qml index 346e5e3544..7f559ea664 100644 --- a/scripts/developer/utilities/audio/stats.qml +++ b/scripts/developer/utilities/audio/Stats.qml @@ -1,5 +1,5 @@ // -// stats.qml +// Stats.qml // scripts/developer/utilities/audio // // Created by Zach Pomerantz on 9/22/2016 @@ -12,22 +12,21 @@ import QtQuick 2.5 import QtQuick.Controls 1.4 import QtQuick.Layouts 1.3 +import "../../../../resources/qml/controls-uit" as HifiControls + Column { id: stats width: parent.width - height: parent.height property bool showGraphs: toggleGraphs.checked - RowLayout { + Item { width: parent.width height: 30 - Button { + HifiControls.Button { id: toggleGraphs property bool checked: false - - Layout.alignment: Qt.AlignCenter - + anchors.horizontalCenter: parent.horizontalCenter text: checked ? "Hide graphs" : "Show graphs" onClicked: function() { checked = !checked; } } @@ -35,11 +34,9 @@ Column { Grid { width: parent.width - height: parent.height - 30 Column { width: parent.width / 2 - height: parent.height Section { label: "Latency" @@ -76,7 +73,6 @@ Column { Column { width: parent.width / 2 - height: parent.height Section { label: "Mixer (upstream)" @@ -92,4 +88,3 @@ Column { } } } - diff --git a/scripts/developer/utilities/audio/TabletStats.qml b/scripts/developer/utilities/audio/TabletStats.qml new file mode 100644 index 0000000000..130b90f032 --- /dev/null +++ b/scripts/developer/utilities/audio/TabletStats.qml @@ -0,0 +1,89 @@ +// +// TabletStats.qml +// scripts/developer/utilities/audio +// +// Created by David Rowe on 3 Mar 2017. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or https://www.apache.org/licenses/LICENSE-2.0.html +// +import QtQuick 2.5 +import QtQuick.Controls 1.4 +import QtQuick.Layouts 1.3 + +import "../../../../resources/qml/styles-uit" + +Item { + id: dialog + width: 480 + height: 720 + + HifiConstants { id: hifi } + + Rectangle { + id: header + height: 90 + anchors { + top: parent.top + left: parent.left + right: parent.right + } + z: 100 + + gradient: Gradient { + GradientStop { + position: 0 + color: "#2b2b2b" + } + + GradientStop { + position: 1 + color: "#1e1e1e" + } + } + + RalewayBold { + text: "Audio Interface Statistics" + size: 26 + color: "#34a2c7" + anchors.verticalCenter: parent.verticalCenter + anchors.left: parent.left + anchors.leftMargin: hifi.dimensions.contentMargin.x // ####### hifi is not defined + } + } + + Rectangle { + id: main + anchors { + top: header.bottom + bottom: parent.bottom + left: parent.left + right: parent.right + } + + gradient: Gradient { + GradientStop { + position: 0 + color: "#2b2b2b" + } + + GradientStop { + position: 1 + color: "#0f212e" + } + } + + Flickable { + id: scrollView + width: parent.width + height: parent.height + contentWidth: parent.width + contentHeight: stats.height + + Stats { + id: stats + } + } + } +} diff --git a/scripts/developer/utilities/audio/stats.js b/scripts/developer/utilities/audio/stats.js index 493271ac99..382e14df5f 100644 --- a/scripts/developer/utilities/audio/stats.js +++ b/scripts/developer/utilities/audio/stats.js @@ -9,17 +9,23 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -var INITIAL_WIDTH = 400; -var INITIAL_OFFSET = 50; +if (HMD.active && !Settings.getValue("HUDUIEnabled")) { + var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); + var qml = Script.resolvePath("TabletStats.qml"); + tablet.loadQMLSource(qml); + Script.stop(); -// Set up the qml ui -var qml = Script.resolvePath('stats.qml'); -var window = new OverlayWindow({ - title: 'Audio Interface Statistics', - source: qml, - width: 500, height: 520 // stats.qml may be too large for some screens -}); -window.setPosition(INITIAL_OFFSET, INITIAL_OFFSET); +} else { + var INITIAL_WIDTH = 400; + var INITIAL_OFFSET = 50; -window.closed.connect(function() { Script.stop(); }); + var qml = Script.resolvePath("Stats.qml"); + var window = new OverlayWindow({ + title: "Audio Interface Statistics", + source: qml, + width: 500, height: 520 // stats.qml may be too large for some screens + }); + window.setPosition(INITIAL_OFFSET, INITIAL_OFFSET); + window.closed.connect(function () { Script.stop(); }); +} diff --git a/scripts/developer/utilities/record/recorder.js b/scripts/developer/utilities/record/recorder.js index 0e335116d5..ba1c8b0393 100644 --- a/scripts/developer/utilities/record/recorder.js +++ b/scripts/developer/utilities/record/recorder.js @@ -9,12 +9,14 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // +/* globals HIFI_PUBLIC_BUCKET:true, Tool, ToolBar */ + HIFI_PUBLIC_BUCKET = "http://s3.amazonaws.com/hifi-public/"; Script.include("/~/system/libraries/toolBars.js"); var recordingFile = "recording.hfr"; -function setPlayerOptions() { +function setDefaultPlayerOptions() { Recording.setPlayFromCurrentLocation(true); Recording.setPlayerUseDisplayName(false); Recording.setPlayerUseAttachments(false); @@ -38,16 +40,16 @@ var saveIcon; var loadIcon; var spacing; var timerOffset; -setupToolBar(); - var timer = null; var slider = null; + +setupToolBar(); setupTimer(); var watchStop = false; function setupToolBar() { - if (toolBar != null) { + if (toolBar !== null) { print("Multiple calls to Recorder.js:setupToolBar()"); return; } @@ -56,6 +58,8 @@ function setupToolBar() { toolBar = new ToolBar(0, 0, ToolBar.HORIZONTAL); + toolBar.onMove = onToolbarMove; + toolBar.setBack(COLOR_TOOL_BAR, ALPHA_OFF); recordIcon = toolBar.addTool({ @@ -86,7 +90,7 @@ function setupToolBar() { visible: true }, false); - timerOffset = toolBar.width; + timerOffset = toolBar.width + ToolBar.SPACING; spacing = toolBar.addSpacing(0); saveIcon = toolBar.addTool({ @@ -112,15 +116,15 @@ function setupTimer() { text: (0.00).toFixed(3), backgroundColor: COLOR_OFF, x: 0, y: 0, - width: 0, height: 0, - leftMargin: 10, topMargin: 10, + width: 200, height: 25, + leftMargin: 5, topMargin: 3, alpha: 1.0, backgroundAlpha: 1.0, visible: true }); slider = { x: 0, y: 0, w: 200, h: 20, - pos: 0.0, // 0.0 <= pos <= 1.0 + pos: 0.0 // 0.0 <= pos <= 1.0 }; slider.background = Overlays.addOverlay("text", { text: "", @@ -144,20 +148,40 @@ function setupTimer() { }); } +function onToolbarMove(newX, newY, deltaX, deltaY) { + Overlays.editOverlay(timer, { + x: newX + timerOffset - ToolBar.SPACING, + y: newY + }); + + slider.x = newX - ToolBar.SPACING; + slider.y = newY - slider.h - ToolBar.SPACING; + + Overlays.editOverlay(slider.background, { + x: slider.x, + y: slider.y + }); + Overlays.editOverlay(slider.foreground, { + x: slider.x, + y: slider.y + }); +} + function updateTimer() { var text = ""; if (Recording.isRecording()) { text = formatTime(Recording.recorderElapsed()); - } else { - text = formatTime(Recording.playerElapsed()) + " / " + - formatTime(Recording.playerLength()); + text = formatTime(Recording.playerElapsed()) + " / " + formatTime(Recording.playerLength()); } + var timerWidth = text.length * 8 + ((Recording.isRecording()) ? 15 : 0); + Overlays.editOverlay(timer, { - text: text - }) - toolBar.changeSpacing(text.length * 8 + ((Recording.isRecording()) ? 15 : 0), spacing); + text: text, + width: timerWidth + }); + toolBar.changeSpacing(timerWidth + ToolBar.SPACING, spacing); if (Recording.isRecording()) { slider.pos = 1.0; @@ -173,7 +197,7 @@ function updateTimer() { function formatTime(time) { var MIN_PER_HOUR = 60; var SEC_PER_MIN = 60; - var MSEC_PER_SEC = 1000; + var MSEC_DIGITS = 3; var hours = Math.floor(time / (SEC_PER_MIN * MIN_PER_HOUR)); time -= hours * (SEC_PER_MIN * MIN_PER_HOUR); @@ -184,37 +208,19 @@ function formatTime(time) { var seconds = time; var text = ""; - text += (hours > 0) ? hours + ":" : - ""; - text += (minutes > 0) ? ((minutes < 10 && text != "") ? "0" : "") + minutes + ":" : - ""; - text += ((seconds < 10 && text != "") ? "0" : "") + seconds.toFixed(3); + text += (hours > 0) ? hours + ":" : ""; + text += (minutes > 0) ? ((minutes < 10 && text !== "") ? "0" : "") + minutes + ":" : ""; + text += ((seconds < 10 && text !== "") ? "0" : "") + seconds.toFixed(MSEC_DIGITS); return text; } function moveUI() { var relative = { x: 70, y: 40 }; toolBar.move(relative.x, windowDimensions.y - relative.y); - Overlays.editOverlay(timer, { - x: relative.x + timerOffset - ToolBar.SPACING, - y: windowDimensions.y - relative.y - ToolBar.SPACING - }); - - slider.x = relative.x - ToolBar.SPACING; - slider.y = windowDimensions.y - relative.y - slider.h - ToolBar.SPACING; - - Overlays.editOverlay(slider.background, { - x: slider.x, - y: slider.y, - }); - Overlays.editOverlay(slider.foreground, { - x: slider.x, - y: slider.y, - }); } function mousePressEvent(event) { - clickedOverlay = Overlays.getOverlayAtPoint({ x: event.x, y: event.y }); + var clickedOverlay = Overlays.getOverlayAtPoint({ x: event.x, y: event.y }); if (recordIcon === toolBar.clicked(clickedOverlay, false) && !Recording.isPlaying()) { if (!Recording.isRecording()) { @@ -226,7 +232,11 @@ function mousePressEvent(event) { toolBar.setAlpha(ALPHA_OFF, loadIcon); } else { Recording.stopRecording(); - toolBar.selectTool(recordIcon, true ); + toolBar.selectTool(recordIcon, true); + setDefaultPlayerOptions(); + // Plays the recording at the same spot as you recorded it + Recording.setPlayFromCurrentLocation(false); + Recording.setPlayerTime(0); Recording.loadLastRecording(); toolBar.setAlpha(ALPHA_ON, playIcon); toolBar.setAlpha(ALPHA_ON, playLoopIcon); @@ -240,7 +250,6 @@ function mousePressEvent(event) { toolBar.setAlpha(ALPHA_ON, saveIcon); toolBar.setAlpha(ALPHA_ON, loadIcon); } else if (Recording.playerLength() > 0) { - setPlayerOptions(); Recording.setPlayerLoop(false); Recording.startPlaying(); toolBar.setAlpha(ALPHA_OFF, recordIcon); @@ -255,7 +264,6 @@ function mousePressEvent(event) { toolBar.setAlpha(ALPHA_ON, saveIcon); toolBar.setAlpha(ALPHA_ON, loadIcon); } else if (Recording.playerLength() > 0) { - setPlayerOptions(); Recording.setPlayerLoop(true); Recording.startPlaying(); toolBar.setAlpha(ALPHA_OFF, recordIcon); @@ -263,7 +271,7 @@ function mousePressEvent(event) { toolBar.setAlpha(ALPHA_OFF, loadIcon); } } else if (saveIcon === toolBar.clicked(clickedOverlay)) { - if (!Recording.isRecording() && !Recording.isPlaying() && Recording.playerLength() != 0) { + if (!Recording.isRecording() && !Recording.isPlaying() && Recording.playerLength() !== 0) { recordingFile = Window.save("Save recording to file", ".", "Recordings (*.hfr)"); if (!(recordingFile === "null" || recordingFile === null || recordingFile === "")) { Recording.saveRecording(recordingFile); @@ -274,6 +282,7 @@ function mousePressEvent(event) { recordingFile = Window.browse("Load recording from file", ".", "Recordings (*.hfr *.rec *.HFR *.REC)"); if (!(recordingFile === "null" || recordingFile === null || recordingFile === "")) { Recording.loadRecording(recordingFile); + setDefaultPlayerOptions(); } if (Recording.playerLength() > 0) { toolBar.setAlpha(ALPHA_ON, playIcon); @@ -282,8 +291,8 @@ function mousePressEvent(event) { } } } else if (Recording.playerLength() > 0 && - slider.x < event.x && event.x < slider.x + slider.w && - slider.y < event.y && event.y < slider.y + slider.h) { + slider.x < event.x && event.x < slider.x + slider.w && + slider.y < event.y && event.y < slider.y + slider.h) { isSliding = true; slider.pos = (event.x - slider.x) / slider.w; Recording.setPlayerTime(slider.pos * Recording.playerLength()); @@ -308,7 +317,7 @@ function mouseReleaseEvent(event) { function update() { var newDimensions = Controller.getViewportDimensions(); - if (windowDimensions.x != newDimensions.x || windowDimensions.y != newDimensions.y) { + if (windowDimensions.x !== newDimensions.x || windowDimensions.y !== newDimensions.y) { windowDimensions = newDimensions; moveUI(); } diff --git a/scripts/developer/utilities/render/BG.qml b/scripts/developer/utilities/render/BG.qml deleted file mode 100644 index 40ce5a89fe..0000000000 --- a/scripts/developer/utilities/render/BG.qml +++ /dev/null @@ -1,22 +0,0 @@ -// -// BG.qml -// examples/utilities/tools/render -// -// Created by Zach Pomerantz on 2/8/2016 -// Copyright 2016 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or https://www.apache.org/licenses/LICENSE-2.0.html -// -import QtQuick 2.5 -import QtQuick.Controls 1.4 - -Item { - Timer { - running: true; repeat: true - onTriggered: time.text = Render.getConfig("DrawBackgroundDeferred").gpuTime - } - - Text { id: time; font.pointSize: 20 } -} - diff --git a/scripts/developer/utilities/render/debug.js b/scripts/developer/utilities/render/debug.js deleted file mode 100644 index eba967491b..0000000000 --- a/scripts/developer/utilities/render/debug.js +++ /dev/null @@ -1,39 +0,0 @@ -// -// debug.js -// examples/utilities/tools/render -// -// Zach Pomerantz, created on 1/27/2016. -// Copyright 2016 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -oldConfig = Render.toJSON(); -Render.RenderShadowTask.enabled = true; -var RDT = Render.RenderDeferredTask; -RDT.AmbientOcclusion.enabled = true; -RDT.DebugDeferredBuffer.enabled = false; - -// Set up the qml ui -var qml = Script.resolvePath('main.qml'); -var window = new OverlayWindow({ - title: 'Render Engine Configuration', - source: qml, - width: 400, height: 900, -}); -window.setPosition(25, 50); -window.closed.connect(function() { Script.stop(); }); - -// Debug buffer sizing -var resizing = false; -Controller.mousePressEvent.connect(function() { resizing = true; }); -Controller.mouseReleaseEvent.connect(function() { resizing = false; }); -Controller.mouseMoveEvent.connect(function(e) { resizing && setDebugBufferSize(e.x); }); -function setDebugBufferSize(x) { - x = (2.0 * (x / Window.innerWidth) - 1.0); // scale - x = Math.min(Math.max(-1, x), 1); // clamp - Render.RenderDeferredTask.DebugDeferredBuffer.size = {x: x, y: -1, z: 1, w: 1}; -} - -Script.scriptEnding.connect(function() { Render.load(oldConfig); } ); diff --git a/scripts/developer/utilities/render/debugBG.js b/scripts/developer/utilities/render/debugBG.js deleted file mode 100644 index 21fe024c32..0000000000 --- a/scripts/developer/utilities/render/debugBG.js +++ /dev/null @@ -1,21 +0,0 @@ -// -// debugBG.js -// examples/utilities/tools/render -// -// Zach Pomerantz, created on 1/27/2016. -// Copyright 2016 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -// Set up the qml ui -var qml = Script.resolvePath('BG.qml'); -var window = new OverlayWindow({ - title: 'Background Timer', - source: qml, - width: 300 -}); -window.setPosition(25, 50); -window.closed.connect(function() { Script.stop(); }); - diff --git a/scripts/developer/utilities/render/debugRender.js b/scripts/developer/utilities/render/debugCulling.js similarity index 100% rename from scripts/developer/utilities/render/debugRender.js rename to scripts/developer/utilities/render/debugCulling.js diff --git a/scripts/developer/utilities/render/debugFramebuffer.js b/scripts/developer/utilities/render/debugFramebuffer.js deleted file mode 100644 index 12a19085c8..0000000000 --- a/scripts/developer/utilities/render/debugFramebuffer.js +++ /dev/null @@ -1,51 +0,0 @@ -// -// ddebugFramBuffer.js -// examples/utilities/tools/render -// -// Sam Gateau created on 2/18/2016. -// Copyright 2016 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -var DDB = Render.RenderDeferredTask.DebugDeferredBuffer; -oldConfig = DDB.toJSON(); -DDB.enabled = true; - - -// Set up the qml ui -var qml = Script.resolvePath('framebuffer.qml'); -var window = new OverlayWindow({ - title: 'Framebuffer Debug', - source: qml, - width: 400, height: 50, -}); -window.setPosition(25, 50); -window.closed.connect(function() { Script.stop(); }); - -// Debug buffer sizing -var resizing = false; - -Controller.mousePressEvent.connect(function (e) { - if (shouldStartResizing(e.x)) { - resizing = true; - } -}); -Controller.mouseReleaseEvent.connect(function() { resizing = false; }); -Controller.mouseMoveEvent.connect(function (e) { resizing && setDebugBufferSize(e.x); }); - - -function shouldStartResizing(eventX) { - var x = Math.abs(eventX - Window.innerWidth * (1.0 + DDB.size.x) / 2.0); - var mode = DDB.mode; - return mode !== -1 && x < 20; -} - -function setDebugBufferSize(x) { - x = (2.0 * (x / Window.innerWidth) - 1.0); // scale - x = Math.min(Math.max(-1, x), 1); // clamp - DDB.size = { x: x, y: -1, z: 1, w: 1 }; -} - -Script.scriptEnding.connect(function () { DDB.fromJSON(oldConfig); }); diff --git a/scripts/developer/utilities/render/debugToneMapping.js b/scripts/developer/utilities/render/debugToneMapping.js deleted file mode 100644 index ef14c24fb7..0000000000 --- a/scripts/developer/utilities/render/debugToneMapping.js +++ /dev/null @@ -1,20 +0,0 @@ -// -// debugToneMapping.js -// -// Created by Sam Gateau on 6/30/2016 -// Copyright 2016 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or https://www.apache.org/licenses/LICENSE-2.0.html -// - -// Set up the qml ui -var qml = Script.resolvePath('toneMapping.qml'); -var window = new OverlayWindow({ - title: 'Tone Mapping', - source: qml, - width: 400, height: 200, -}); -window.setPosition(250, 1000); -window.closed.connect(function() { Script.stop(); }); - diff --git a/scripts/developer/utilities/render/deferredLighting.qml b/scripts/developer/utilities/render/deferredLighting.qml index 0ac4cbc5b5..778e0e1905 100644 --- a/scripts/developer/utilities/render/deferredLighting.qml +++ b/scripts/developer/utilities/render/deferredLighting.qml @@ -25,7 +25,7 @@ Column { "Lightmap:LightingModel:enableLightmap", "Background:LightingModel:enableBackground", "ssao:AmbientOcclusion:enabled", - "Textures:LightingModel:enableMaterialTexturing", + "Textures:LightingModel:enableMaterialTexturing" ] CheckBox { text: modelData.split(":")[0] @@ -45,6 +45,7 @@ Column { "Diffuse:LightingModel:enableDiffuse", "Specular:LightingModel:enableSpecular", "Albedo:LightingModel:enableAlbedo", + "Wireframe:LightingModel:enableWireframe" ] CheckBox { text: modelData.split(":")[0] @@ -62,7 +63,8 @@ Column { "Directional:LightingModel:enableDirectionalLight", "Point:LightingModel:enablePointLight", "Spot:LightingModel:enableSpotLight", - "Light Contour:LightingModel:showLightContour" + "Light Contour:LightingModel:showLightContour", + "Shadow:RenderShadowTask:enabled" ] CheckBox { text: modelData.split(":")[0] @@ -149,6 +151,7 @@ Column { ListElement { text: "Mid Normal"; color: "White" } ListElement { text: "Low Curvature"; color: "White" } ListElement { text: "Low Normal"; color: "White" } + ListElement { text: "Curvature Occlusion"; color: "White" } ListElement { text: "Debug Scattering"; color: "White" } ListElement { text: "Ambient Occlusion"; color: "White" } ListElement { text: "Ambient Occlusion Blurred"; color: "White" } @@ -159,13 +162,33 @@ Column { } } - Row { + Column { id: metas CheckBox { - text: "Draw Meta Bounds" + text: "Metas" checked: Render.getConfig("DrawMetaBounds")["enabled"] onCheckedChanged: { Render.getConfig("DrawMetaBounds")["enabled"] = checked } } + CheckBox { + text: "Opaques" + checked: Render.getConfig("DrawOpaqueBounds")["enabled"] + onCheckedChanged: { Render.getConfig("DrawOpaqueBounds")["enabled"] = checked } + } + CheckBox { + text: "Transparents" + checked: Render.getConfig("DrawTransparentBounds")["enabled"] + onCheckedChanged: { Render.getConfig("DrawTransparentBounds")["enabled"] = checked } + } + CheckBox { + text: "Overlay Opaques" + checked: Render.getConfig("DrawOverlayOpaqueBounds")["enabled"] + onCheckedChanged: { Render.getConfig("DrawOverlayOpaqueBounds")["enabled"] = checked } + } + CheckBox { + text: "Overlay Transparents" + checked: Render.getConfig("DrawOverlayTransparentBounds")["enabled"] + onCheckedChanged: { Render.getConfig("DrawOverlayTransparentBounds")["enabled"] = checked } + } } } diff --git a/scripts/developer/utilities/render/framebuffer.qml b/scripts/developer/utilities/render/framebuffer.qml deleted file mode 100644 index 65046106dc..0000000000 --- a/scripts/developer/utilities/render/framebuffer.qml +++ /dev/null @@ -1,57 +0,0 @@ -// -// main.qml -// examples/utilities/tools/render -// -// Created by Zach Pomerantz on 2/8/2016 -// Copyright 2016 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or https://www.apache.org/licenses/LICENSE-2.0.html -// -import QtQuick 2.5 -import QtQuick.Controls 1.4 -import "configSlider" - -Column { - spacing: 8 - Column { - id: debug - property var config: Render.getConfig("DebugDeferredBuffer") - - function setDebugMode(mode) { - debug.config.enabled = (mode != -1); - debug.config.mode = mode; - } - - ComboBox { - currentIndex: 0 - model: ListModel { - id: cbItems - ListElement { text: "Off"; color: "Yellow" } - ListElement { text: "Depth"; color: "Green" } - ListElement { text: "Albedo"; color: "Yellow" } - ListElement { text: "Normal"; color: "White" } - ListElement { text: "Roughness"; color: "White" } - ListElement { text: "Metallic"; color: "White" } - ListElement { text: "Emissive"; color: "White" } - ListElement { text: "Unlit"; color: "White" } - ListElement { text: "Occlusion"; color: "White" } - ListElement { text: "Lightmap"; color: "White" } - ListElement { text: "Scattering"; color: "White" } - ListElement { text: "Lighting"; color: "White" } - ListElement { text: "Shadow"; color: "White" } - ListElement { text: "Linear Depth"; color: "White" } - ListElement { text: "Mid Curvature"; color: "White" } - ListElement { text: "Mid Normal"; color: "White" } - ListElement { text: "Low Curvature"; color: "White" } - ListElement { text: "Low Normal"; color: "White" } - ListElement { text: "Debug Scattering"; color: "White" } - ListElement { text: "Ambient Occlusion"; color: "White" } - ListElement { text: "Ambient Occlusion Blurred"; color: "White" } - ListElement { text: "Custom"; color: "White" } - } - width: 200 - onCurrentIndexChanged: { debug.setDebugMode(currentIndex - 1) } - } - } -} diff --git a/scripts/developer/utilities/render/globalLight.qml b/scripts/developer/utilities/render/globalLight.qml deleted file mode 100644 index ac0d7ebcd5..0000000000 --- a/scripts/developer/utilities/render/globalLight.qml +++ /dev/null @@ -1,36 +0,0 @@ -// -// globalLight.qml -// examples/utilities/render -// -// Copyright 2016 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or https://www.apache.org/licenses/LICENSE-2.0.html -// -import QtQuick 2.5 -import QtQuick.Controls 1.4 -import "configSlider" - -Column { - id: root - spacing: 8 - property var currentZoneID - property var zoneProperties - - Component.onCompleted: { - Entities.getProperties - sceneOctree.enabled = true; - itemSelection.enabled = true; - sceneOctree.showVisibleCells = false; - sceneOctree.showEmptyCells = false; - itemSelection.showInsideItems = false; - itemSelection.showInsideSubcellItems = false; - itemSelection.showPartialItems = false; - itemSelection.showPartialSubcellItems = false; - } - Component.onDestruction: { - sceneOctree.enabled = false; - itemSelection.enabled = false; - Render.getConfig("FetchSceneSelection").freezeFrustum = false; - Render.getConfig("CullSceneSelection").freezeFrustum = false; - } diff --git a/scripts/developer/utilities/render/main.qml b/scripts/developer/utilities/render/main.qml deleted file mode 100644 index aecd566207..0000000000 --- a/scripts/developer/utilities/render/main.qml +++ /dev/null @@ -1,122 +0,0 @@ -// -// main.qml -// examples/utilities/tools/render -// -// Created by Zach Pomerantz on 2/8/2016 -// Copyright 2016 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or https://www.apache.org/licenses/LICENSE-2.0.html -// -import QtQuick 2.5 -import QtQuick.Controls 1.4 -import "configSlider" - -Column { - id: root - spacing: 16 - Switch { - checked: true - onClicked: ui.visible = checked - } - - Column { - id: ui - spacing: 8 - - Repeater { - model: [ "Opaque:DrawOpaqueDeferred", "Transparent:DrawTransparentDeferred", - "Opaque Overlays:DrawOverlay3DOpaque", "Transparent Overlays:DrawOverlay3DTransparent" ] - ConfigSlider { - label: qsTr(modelData.split(":")[0]) - integral: true - config: Render.getConfig(modelData.split(":")[1]) - property: "maxDrawn" - max: config.numDrawn - min: -1 - } - } - - Row { - CheckBox { - text: qsTr("Display Status") - onCheckedChanged: { Render.getConfig("DrawStatus").showDisplay = checked } - } - CheckBox { - text: qsTr("Network/Physics Status") - onCheckedChanged: { Render.getConfig("DrawStatus").showNetwork = checked } - } - } - - ConfigSlider { - label: qsTr("Tone Mapping Exposure") - config: Render.getConfig("ToneMapping") - property: "exposure" - min: -10; max: 10 - } - - Column { - id: ambientOcclusion - property var config: Render.getConfig("AmbientOcclusion") - - Label { text: qsTr("Ambient Occlusion") } - // TODO: Add gpuTimer - CheckBox { text: qsTr("Dithering"); checked: ambientOcclusion.config.ditheringEnabled } - Repeater { - model: [ - "Resolution Level:resolutionLevel:4", - "Obscurance Level:obscuranceLevel:1", - "Radius:radius:2", - "Falloff Bias:falloffBias:0.2", - "Edge Sharpness:edgeSharpness:1", - "Blur Radius:blurRadius:6", - "Blur Deviation:blurDeviation:3" - ] - ConfigSlider { - label: qsTr(modelData.split(":")[0]) - config: ambientOcclusion.config - property: modelData.split(":")[1] - max: modelData.split(":")[2] - } - } - Repeater { - model: [ - "Samples:numSamples:32", - "Spiral Turns:numSpiralTurns:30:" - ] - ConfigSlider { - label: qsTr(modelData.split(":")[0]) - integral: true - config: ambientOcclusion.config - property: modelData.split(":")[1] - max: modelData.split(":")[2] - } - } - } - - Column { - id: debug - property var config: Render.getConfig("DebugDeferredBuffer") - - function setDebugMode(mode) { - debug.config.enabled = (mode != 0); - debug.config.mode = mode; - } - - Label { text: qsTr("Debug Buffer") } - ExclusiveGroup { id: bufferGroup } - Repeater { - model: [ - "Off", "Diffuse", "Metallic", "Roughness", "Normal", "Depth", - "Lighting", "Shadow", "Pyramid Depth", "Ambient Occlusion", "Custom Shader" - ] - RadioButton { - text: qsTr(modelData) - exclusiveGroup: bufferGroup - checked: index == 0 - onCheckedChanged: if (checked && index > 0) debug.setDebugMode(index - 1); - } - } - } - } -} diff --git a/scripts/developer/utilities/render/photobooth/html/photobooth.html b/scripts/developer/utilities/render/photobooth/html/photobooth.html index 8964a51f05..f9c79174f3 100644 --- a/scripts/developer/utilities/render/photobooth/html/photobooth.html +++ b/scripts/developer/utilities/render/photobooth/html/photobooth.html @@ -2,166 +2,151 @@ Photo Booth + + - - - - + + body { + margin: 0; + padding: 0; + width: 100%; + color: white; + } + + .top-bar { + height: 90px; + background: linear-gradient(#2b2b2b, #1e1e1e); + font-family: Raleway-Bold; + padding-left: 30px; + padding-right: 30px; + display: flex; + align-items: center; + position: fixed; + width: 480px; + top: 0; + z-index: 1; + font-size: 16px; + } + + .content { + margin-top: 90px; + padding: 30px; + } + + .slider { + margin-left: 70px; + } + + #camera-toggle { + font-family: Raleway-Bold; + font-size: 13px; + text-transform: uppercase; + vertical-align: top; + height: 28px; + min-width: 120px; + padding: 0px 18px; + margin-right: 0px; + border-radius: 5px; + border: none; + color: #121212; + background-color: #afafaf; + background: linear-gradient(#fff 20%, #afafaf 100%); + cursor: pointer; + } + + .dropdown li { + background-color: #ffffff; + } + + + + - -
-
- - - -
- - -
- + +
+
Photobooth
+
+
+
+
+ + + +
+
+ + +
+
+ + +
+
+ +
+ + + + diff --git a/scripts/developer/utilities/render/photobooth/photobooth.js b/scripts/developer/utilities/render/photobooth/photobooth.js deleted file mode 100644 index 3e86d83a98..0000000000 --- a/scripts/developer/utilities/render/photobooth/photobooth.js +++ /dev/null @@ -1,177 +0,0 @@ -(function () { - var SNAPSHOT_DELAY = 500; // 500ms - var PHOTOBOOTH_WINDOW_HTML_URL = Script.resolvePath("./html/photobooth.html"); - var PHOTOBOOTH_SETUP_JSON_URL = Script.resolvePath("./photoboothSetup.json"); - var toolbar = Toolbars.getToolbar("com.highfidelity.interface.toolbar.system"); - var MODEL_BOUNDING_BOX_DIMENSIONS = {x: 1.0174,y: 1.1925,z: 1.0165}; - - var PhotoBooth = {}; - PhotoBooth.init = function () { - var success = Clipboard.importEntities(PHOTOBOOTH_SETUP_JSON_URL); - var frontFactor = 10; - var frontUnitVec = Vec3.normalize(Quat.getFront(MyAvatar.orientation)); - var frontOffset = Vec3.multiply(frontUnitVec,frontFactor); - var rightFactor = 3; - var rightUnitVec = Vec3.normalize(Quat.getRight(MyAvatar.orientation)); - var spawnLocation = Vec3.sum(Vec3.sum(MyAvatar.position,frontOffset),rightFactor); - if (success) { - this.pastedEntityIDs = Clipboard.pasteEntities(spawnLocation); - this.processPastedEntities(); - - } - }; - - PhotoBooth.processPastedEntities = function () { - var cameraResults = {}; - var modelResult; - var modelPos; - this.pastedEntityIDs.forEach(function(id) { - var props = Entities.getEntityProperties(id); - var parts = props["name"].split(":"); - if (parts[0] === "Photo Booth Camera") { - cameraResults[parts[1]] = id; - } - if (parts[0] === "Photo Booth Model") { - modelResult = id; - modelPos = props.position; - } - }); - print(JSON.stringify(cameraResults)); - print(JSON.stringify(modelResult)); - this.cameraEntities = cameraResults; - this.modelEntityID = modelResult; - this.centrePos = modelPos; - }; - - // replace the model in scene with new model - PhotoBooth.changeModel = function (newModelURL) { - // deletes old model - Entities.deleteEntity(this.modelEntityID); - // create new model at centre of the photobooth - var newProps = { - name: "Photo Booth Model", - type: "Model", - modelURL: newModelURL, - position: this.centrePos - }; - var newModelEntityID = Entities.addEntity(newProps); - - // scale model dimensions to fit in bounding box - var scaleModel = function () { - newProps = Entities.getEntityProperties(newModelEntityID); - var myDimensions = newProps.dimensions; - print("myDimensions: " + JSON.stringify(myDimensions)); - var k; - if (myDimensions.x > MODEL_BOUNDING_BOX_DIMENSIONS.x) { - k = MODEL_BOUNDING_BOX_DIMENSIONS.x / myDimensions.x; - myDimensions = Vec3.multiply(k, myDimensions); - } - if (myDimensions.y > MODEL_BOUNDING_BOX_DIMENSIONS.y) { - k = MODEL_BOUNDING_BOX_DIMENSIONS.y / myDimensions.y; - myDimensions = Vec3.multiply(k, myDimensions); - } - if (myDimensions.z > MODEL_BOUNDING_BOX_DIMENSIONS.z) { - k = MODEL_BOUNDING_BOX_DIMENSIONS.z / myDimensions.z; - myDimensions = Vec3.multiply(k, myDimensions); - } - // position the new model on the table - var y_offset = (MODEL_BOUNDING_BOX_DIMENSIONS.y - myDimensions.y) / 2; - var myPosition = Vec3.sum(newProps.position, {x:0, y:-y_offset, z:0}); - Entities.editEntity(newModelEntityID,{position: myPosition, dimensions: myDimensions}); - }; - - // add a delay before scaling to make sure the entity server have gotten the right model dimensions - Script.setTimeout(function () { - scaleModel(); - }, 400); - - this.modelEntityID = newModelEntityID; - }; - - PhotoBooth.destroy = function () { - this.pastedEntityIDs.forEach(function(id) { - Entities.deleteEntity(id); - }); - Entities.deleteEntity(this.modelEntityID); - }; - - var main = function () { - PhotoBooth.init(); - - var photoboothWindowListener = {}; - photoboothWindowListener.onLoad = function (event) { - print("loaded" + event.value); - if (!event.hasOwnProperty("value")){ - return; - } - }; - - photoboothWindowListener.onSelectCamera = function (event) { - print("selected camera " + event.value); - if (!event.hasOwnProperty("value")){ - return; - } - if (event.value === "First Person Camera") { - Camera.mode = "first person"; - } else { - Camera.mode = "entity"; - var cameraID = PhotoBooth.cameraEntities[event.value]; - Camera.setCameraEntity(cameraID); - } - }; - - photoboothWindowListener.onSelectLightingPreset = function (event) { - print("selected lighting preset" + event.value); - if (!event.hasOwnProperty("value")){ - return; - } - }; - - photoboothWindowListener.onClickPictureButton = function (event) { - print("clicked picture button"); - // hide HUD tool bar - toolbar.writeProperty("visible", false); - // hide Overlays (such as Running Scripts or other Dialog UI) - Menu.setIsOptionChecked("Overlays", false); - // hide mouse cursor - Reticle.visible = false; - // giving a delay here before snapshotting so that there is time to hide toolbar and other UIs - // void WindowScriptingInterface::takeSnapshot(bool notify, bool includeAnimated, float aspectRatio) - Script.setTimeout(function () { - Window.takeSnapshot(false, false, 1.91); - // show hidden items after snapshot is taken - toolbar.writeProperty("visible", true); - Menu.setIsOptionChecked("Overlays", true); - // unknown issue: somehow we don't need to reset cursor to visible in script and the mouse still returns after snapshot - // Reticle.visible = true; - }, SNAPSHOT_DELAY); - }; - - photoboothWindowListener.onClickReloadModelButton = function (event) { - print("clicked reload model button " + event.value); - PhotoBooth.changeModel(event.value); - }; - - var photoboothWindow = new OverlayWebWindow({ - title: 'Photo Booth', - source: PHOTOBOOTH_WINDOW_HTML_URL, - width: 450, - height: 450, - visible: true - }); - - photoboothWindow.webEventReceived.connect(function (data) { - var event = JSON.parse(data); - if (photoboothWindowListener[event.type]) { - photoboothWindowListener[event.type](event); - } - }); - }; - main(); - - function cleanup() { - Camera.mode = "first person"; - PhotoBooth.destroy(); - } - Script.scriptEnding.connect(cleanup); -}()); \ No newline at end of file diff --git a/scripts/developer/utilities/render/photobooth/photoboothApp.js b/scripts/developer/utilities/render/photobooth/photoboothApp.js new file mode 100644 index 0000000000..154028f091 --- /dev/null +++ b/scripts/developer/utilities/render/photobooth/photoboothApp.js @@ -0,0 +1,199 @@ +// +// photobooth.js +// scripts/developer/utilities/render/photobooth +// +// Created by Faye Li on 2 Nov 2016 +// Copyright 2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// +/* globals Tablet, Toolbars, Script, HMD, Controller, Menu */ +(function () { + var SNAPSHOT_DELAY = 500; // 500ms + var PHOTOBOOTH_WINDOW_HTML_URL = Script.resolvePath("./html/photobooth.html"); + var PHOTOBOOTH_SETUP_JSON_URL = Script.resolvePath("./photoboothSetup.json"); + var MODEL_BOUNDING_BOX_DIMENSIONS = {x: 1.0174,y: 1.1925,z: 1.0165}; + var PhotoBooth = {}; + var photoboothCreated = false; + var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); + var button = tablet.addButton({ + icon: "icons/tablet-icons/snap-i.svg", + text: "PHOTOBOOTH" + }); + + function onClicked() { + if (photoboothCreated) { + tablet.gotoHomeScreen(); + PhotoBooth.destroy(); + } else { + tablet.gotoWebScreen(PHOTOBOOTH_WINDOW_HTML_URL); + PhotoBooth.init(); + } + } + + function onScreenChanged() { + if (photoboothCreated) { + tablet.gotoHomeScreen(); + PhotoBooth.destroy(); + button.editProperties({isActive: false}); + } else { + button.editProperties({isActive: true}); + } + } + tablet.screenChanged.connect(onScreenChanged); + button.clicked.connect(onClicked); + tablet.webEventReceived.connect(onWebEventReceived); + + + function onWebEventReceived(event) { + print("photobooth.js received a web event:" + event); + // Converts the event to a JavasScript Object + if (typeof event === "string") { + event = JSON.parse(event); + } + if (event.app === "photobooth") { + if (event.type === "onClickPictureButton") { + print("clicked picture button"); + // // hide HUD tool bar + // toolbar.writeProperty("visible", false); + // hide Overlays (such as Running Scripts or other Dialog UI) + Menu.setIsOptionChecked("Overlays", false); + // hide mouse cursor + Reticle.visible = false; + // hide tablet + HMD.closeTablet(); + // // giving a delay here before snapshotting so that there is time to hide other UIs + // void WindowScriptingInterface::takeSnapshot(bool notify, bool includeAnimated, float aspectRatio) + Script.setTimeout(function () { + Window.takeSnapshot(false, false, 1.91); + // show hidden items after snapshot is taken + // issue: currently there's no way to show tablet via a script command. user will have to manually open tablet again + // issue: somehow we don't need to reset cursor to visible in script and the mouse still returns after snapshot + // Reticle.visible = true; + // toolbar.writeProperty("visible", true); + Menu.setIsOptionChecked("Overlays", true); + }, SNAPSHOT_DELAY); + } else if (event.type === "onClickReloadModelButton") { + print("clicked reload model button " + event.data.value); + PhotoBooth.changeModel(event.data.value); + } else if (event.type === "onSelectCamera") { + print("selected camera " + event.data.value); + if (!event.data.hasOwnProperty("value")){ + return; + } + if (event.data.value === "First Person Camera") { + Camera.mode = "first person"; + } else { + Camera.mode = "entity"; + var cameraID = PhotoBooth.cameraEntities[event.data.value]; + Camera.setCameraEntity(cameraID); + } + } else if (event.type === "onRotateSlider") { + var props = {}; + props.rotation = Quat.fromPitchYawRollDegrees(0, event.data.value, 0); + Entities.editEntity(PhotoBooth.modelEntityID, props); + } + } + } + + PhotoBooth.init = function () { + photoboothCreated = true; + var success = Clipboard.importEntities(PHOTOBOOTH_SETUP_JSON_URL); + var frontFactor = 10; + // getForward is preffered as getFront function is deprecated + var frontUnitVec = Vec3.normalize(Quat.getFront(MyAvatar.orientation)); + var frontOffset = Vec3.multiply(frontUnitVec,frontFactor); + var upFactor = 3; + var upUnitVec = Vec3.normalize(Quat.getUp(MyAvatar.orientation)); + var upOffset = Vec3.multiply(upUnitVec, upFactor); + var spawnLocation = Vec3.sum(MyAvatar.position,frontOffset); + spawnLocation = Vec3.sum(spawnLocation, upOffset); + if (success) { + this.pastedEntityIDs = Clipboard.pasteEntities(spawnLocation); + this.processPastedEntities(); + } + }; + + PhotoBooth.processPastedEntities = function () { + var cameraResults = {}; + var modelResult; + var modelPos; + this.pastedEntityIDs.forEach(function(id) { + var props = Entities.getEntityProperties(id); + var parts = props["name"].split(":"); + if (parts[0] === "Photo Booth Camera") { + cameraResults[parts[1]] = id; + } + if (parts[0] === "Photo Booth Model") { + modelResult = id; + modelPos = props.position; + } + }); + print(JSON.stringify(cameraResults)); + print(JSON.stringify(modelResult)); + this.cameraEntities = cameraResults; + this.modelEntityID = modelResult; + this.centrePos = modelPos; + }; + + // replace the model in scene with new model + PhotoBooth.changeModel = function (newModelURL) { + // deletes old model + Entities.deleteEntity(this.modelEntityID); + // create new model at centre of the photobooth + var newProps = { + name: "Photo Booth Model", + type: "Model", + modelURL: newModelURL, + position: this.centrePos + }; + var newModelEntityID = Entities.addEntity(newProps); + + // scale model dimensions to fit in bounding box + var scaleModel = function () { + newProps = Entities.getEntityProperties(newModelEntityID); + var myDimensions = newProps.dimensions; + print("myDimensions: " + JSON.stringify(myDimensions)); + var k; + if (myDimensions.x > MODEL_BOUNDING_BOX_DIMENSIONS.x) { + k = MODEL_BOUNDING_BOX_DIMENSIONS.x / myDimensions.x; + myDimensions = Vec3.multiply(k, myDimensions); + } + if (myDimensions.y > MODEL_BOUNDING_BOX_DIMENSIONS.y) { + k = MODEL_BOUNDING_BOX_DIMENSIONS.y / myDimensions.y; + myDimensions = Vec3.multiply(k, myDimensions); + } + if (myDimensions.z > MODEL_BOUNDING_BOX_DIMENSIONS.z) { + k = MODEL_BOUNDING_BOX_DIMENSIONS.z / myDimensions.z; + myDimensions = Vec3.multiply(k, myDimensions); + } + // position the new model on the table + var y_offset = (MODEL_BOUNDING_BOX_DIMENSIONS.y - myDimensions.y) / 2; + var myPosition = Vec3.sum(newProps.position, {x:0, y:-y_offset, z:0}); + Entities.editEntity(newModelEntityID,{position: myPosition, dimensions: myDimensions}); + }; + + // add a delay before scaling to make sure the entity server have gotten the right model dimensions + Script.setTimeout(function () { + scaleModel(); + }, 400); + + this.modelEntityID = newModelEntityID; + }; + + PhotoBooth.destroy = function () { + this.pastedEntityIDs.forEach(function(id) { + Entities.deleteEntity(id); + }); + Entities.deleteEntity(this.modelEntityID); + photoboothCreated = false; + Camera.mode = "first person"; + }; + + function cleanup() { + tablet.removeButton(button); + PhotoBooth.destroy(); + } + Script.scriptEnding.connect(cleanup); +}()); \ No newline at end of file diff --git a/scripts/modules/vec3.js b/scripts/modules/vec3.js new file mode 100644 index 0000000000..f164f01374 --- /dev/null +++ b/scripts/modules/vec3.js @@ -0,0 +1,69 @@ +// Example of using a "system module" to decouple Vec3's implementation details. +// +// Users would bring Vec3 support in as a module: +// var vec3 = Script.require('vec3'); +// + +// (this example is compatible with using as a Script.include and as a Script.require module) +try { + // Script.require + module.exports = vec3; +} catch(e) { + // Script.include + Script.registerValue("vec3", vec3); +} + +vec3.fromObject = function(v) { + //return new vec3(v.x, v.y, v.z); + //... this is even faster and achieves the same effect + v.__proto__ = vec3.prototype; + return v; +}; + +vec3.prototype = { + multiply: function(v2) { + // later on could support overrides like so: + // if (v2 instanceof quat) { [...] } + // which of the below is faster (C++ or JS)? + // (dunno -- but could systematically find out and go with that version) + + // pure JS option + // return new vec3(this.x * v2.x, this.y * v2.y, this.z * v2.z); + + // hybrid C++ option + return vec3.fromObject(Vec3.multiply(this, v2)); + }, + // detects any NaN and Infinity values + isValid: function() { + return isFinite(this.x) && isFinite(this.y) && isFinite(this.z); + }, + // format Vec3's, eg: + // var v = vec3(); + // print(v); // outputs [Vec3 (0.000, 0.000, 0.000)] + toString: function() { + if (this === vec3.prototype) { + return "{Vec3 prototype}"; + } + function fixed(n) { return n.toFixed(3); } + return "[Vec3 (" + [this.x, this.y, this.z].map(fixed) + ")]"; + }, +}; + +vec3.DEBUG = true; + +function vec3(x, y, z) { + if (!(this instanceof vec3)) { + // if vec3 is called as a function then re-invoke as a constructor + // (so that `value instanceof vec3` holds true for created values) + return new vec3(x, y, z); + } + + // unfold default arguments (vec3(), vec3(.5), vec3(0,1), etc.) + this.x = x !== undefined ? x : 0; + this.y = y !== undefined ? y : this.x; + this.z = z !== undefined ? z : this.y; + + if (vec3.DEBUG && !this.isValid()) + throw new Error('vec3() -- invalid initial values ['+[].slice.call(arguments)+']'); +}; + diff --git a/scripts/system/assets/images/icon-particles.svg b/scripts/system/assets/images/icon-particles.svg new file mode 100644 index 0000000000..5e0105d7cd --- /dev/null +++ b/scripts/system/assets/images/icon-particles.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + diff --git a/scripts/system/assets/images/icon-point-light.svg b/scripts/system/assets/images/icon-point-light.svg new file mode 100644 index 0000000000..896c35b63b --- /dev/null +++ b/scripts/system/assets/images/icon-point-light.svg @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/scripts/system/assets/images/icon-spot-light.svg b/scripts/system/assets/images/icon-spot-light.svg new file mode 100644 index 0000000000..ac2f87bb27 --- /dev/null +++ b/scripts/system/assets/images/icon-spot-light.svg @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/scripts/system/assets/sounds/entitySnap.wav b/scripts/system/assets/sounds/entitySnap.wav new file mode 100644 index 0000000000..4584f3dcaa Binary files /dev/null and b/scripts/system/assets/sounds/entitySnap.wav differ diff --git a/scripts/system/audio.js b/scripts/system/audio.js index 6e7e95d659..7bc8676a2e 100644 --- a/scripts/system/audio.js +++ b/scripts/system/audio.js @@ -45,7 +45,8 @@ function onClicked() { var entity = HMD.tabletID; Entities.editEntity(entity, { textures: JSON.stringify({ "tex.close": HOME_BUTTON_TEXTURE }) }); shouldActivateButton = true; - tablet.gotoMenuScreen("Audio"); + shouldActivateButton = true; + tablet.loadQMLSource("../Audio.qml"); onAudioScreen = true; } } @@ -72,6 +73,9 @@ tablet.screenChanged.connect(onScreenChanged); AudioDevice.muteToggled.connect(onMuteToggled); Script.scriptEnding.connect(function () { + if (onAudioScreen) { + tablet.gotoHomeScreen(); + } button.clicked.disconnect(onClicked); tablet.screenChanged.disconnect(onScreenChanged); AudioDevice.muteToggled.disconnect(onMuteToggled); diff --git a/scripts/system/audioMuteOverlay.js b/scripts/system/audioMuteOverlay.js new file mode 100644 index 0000000000..cf07402d64 --- /dev/null +++ b/scripts/system/audioMuteOverlay.js @@ -0,0 +1,104 @@ +"use strict"; +/* jslint vars: true, plusplus: true, forin: true*/ +/* globals Tablet, Script, AvatarList, Users, Entities, MyAvatar, Camera, Overlays, Vec3, Quat, Controller, print, getControllerWorldLocation */ +/* eslint indent: ["error", 4, { "outerIIFEBody": 0 }] */ +// +// audioMuteOverlay.js +// +// client script that creates an overlay to provide mute feedback +// +// Created by Triplelexx on 17/03/09 +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +(function() { // BEGIN LOCAL_SCOPE + var utilsPath = Script.resolvePath('../developer/libraries/utils.js'); + Script.include(utilsPath); + + var TWEEN_SPEED = 0.025; + var MIX_AMOUNT = 0.25; + + var overlayPosition = Vec3.ZERO; + var tweenPosition = 0; + var startColor = { + red: 170, + green: 170, + blue: 170 + }; + var endColor = { + red: 255, + green: 0, + blue: 0 + }; + var overlayID; + + Script.update.connect(update); + Script.scriptEnding.connect(cleanup); + + function update(dt) { + if (!AudioDevice.getMuted()) { + if (hasOverlay()) { + deleteOverlay(); + } + } else if (!hasOverlay()) { + createOverlay(); + } else { + updateOverlay(); + } + } + + function getOffsetPosition() { + return Vec3.sum(Camera.position, Quat.getFront(Camera.orientation)); + } + + function createOverlay() { + overlayPosition = getOffsetPosition(); + overlayID = Overlays.addOverlay("sphere", { + position: overlayPosition, + rotation: Camera.orientation, + alpha: 0.9, + dimensions: 0.1, + solid: true, + ignoreRayIntersection: true + }); + } + + function hasOverlay() { + return Overlays.getProperty(overlayID, "position") !== undefined; + } + + function updateOverlay() { + // increase by TWEEN_SPEED until completion + if (tweenPosition < 1) { + tweenPosition += TWEEN_SPEED; + } else { + // after tween completion reset to zero and flip values to ping pong + tweenPosition = 0; + for (var component in startColor) { + var storedColor = startColor[component]; + startColor[component] = endColor[component]; + endColor[component] = storedColor; + } + } + // mix previous position with new and mix colors + overlayPosition = Vec3.mix(overlayPosition, getOffsetPosition(), MIX_AMOUNT); + Overlays.editOverlay(overlayID, { + color: colorMix(startColor, endColor, easeIn(tweenPosition)), + position: overlayPosition, + rotation: Camera.orientation + }); + } + + function deleteOverlay() { + Overlays.deleteOverlay(overlayID); + } + + function cleanup() { + deleteOverlay(); + AudioDevice.muteToggled.disconnect(onMuteToggled); + Script.update.disconnect(update); + } +}()); // END LOCAL_SCOPE diff --git a/scripts/system/audioScope.js b/scripts/system/audioScope.js new file mode 100644 index 0000000000..81d8e8fbd4 --- /dev/null +++ b/scripts/system/audioScope.js @@ -0,0 +1,95 @@ +"use strict"; +// +// audioScope.js +// scripts/system/ +// +// Created by Brad Hefta-Gaub on 3/10/2016 +// Copyright 2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// +/* global Script, Tablet, AudioScope, Audio */ + +(function () { // BEGIN LOCAL_SCOPE + + var scopeVisibile = AudioScope.getVisible(); + var scopePaused = AudioScope.getPause(); + var autoPause = false; + + var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); + var showScopeButton = tablet.addButton({ + icon: "icons/tablet-icons/scope.svg", + text: "Audio Scope", + isActive: scopeVisibile + }); + + var scopePauseImage = "icons/tablet-icons/scope-pause.svg"; + var scopePlayImage = "icons/tablet-icons/scope-play.svg"; + + var pauseScopeButton = tablet.addButton({ + icon: scopePaused ? scopePlayImage : scopePauseImage, + text: scopePaused ? "Unpause" : "Pause", + isActive: scopePaused + }); + + var autoPauseScopeButton = tablet.addButton({ + icon: "icons/tablet-icons/scope-auto.svg", + text: "Auto Pause", + isActive: autoPause + }); + + function setScopePause(paused) { + scopePaused = paused; + pauseScopeButton.editProperties({ + isActive: scopePaused, + icon: scopePaused ? scopePlayImage : scopePauseImage, + text: scopePaused ? "Unpause" : "Pause" + }); + AudioScope.setPause(scopePaused); + } + + showScopeButton.clicked.connect(function () { + // toggle button active state + scopeVisibile = !scopeVisibile; + showScopeButton.editProperties({ + isActive: scopeVisibile + }); + + AudioScope.setVisible(scopeVisibile); + }); + + pauseScopeButton.clicked.connect(function () { + // toggle button active state + setScopePause(!scopePaused); + }); + + autoPauseScopeButton.clicked.connect(function () { + // toggle button active state + autoPause = !autoPause; + autoPauseScopeButton.editProperties({ + isActive: autoPause, + text: autoPause ? "Auto Pause" : "Manual" + }); + }); + + Script.scriptEnding.connect(function () { + tablet.removeButton(showScopeButton); + tablet.removeButton(pauseScopeButton); + tablet.removeButton(autoPauseScopeButton); + }); + + Audio.noiseGateOpened.connect(function(){ + if (autoPause) { + setScopePause(false); + } + }); + + Audio.noiseGateClosed.connect(function(){ + // noise gate closed + if (autoPause) { + setScopePause(true); + } + }); + +}()); // END LOCAL_SCOPE \ No newline at end of file diff --git a/scripts/system/away.js b/scripts/system/away.js index 541fe6f679..4ca938d492 100644 --- a/scripts/system/away.js +++ b/scripts/system/away.js @@ -87,8 +87,8 @@ function moveCloserToCamera(positionAtHUD) { // we don't actually want to render at the slerped look at... instead, we want to render // slightly closer to the camera than that. var MOVE_CLOSER_TO_CAMERA_BY = -0.25; - var cameraFront = Quat.getFront(Camera.orientation); - var closerToCamera = Vec3.multiply(cameraFront, MOVE_CLOSER_TO_CAMERA_BY); // slightly closer to camera + var cameraForward = Quat.getForward(Camera.orientation); + var closerToCamera = Vec3.multiply(cameraForward, MOVE_CLOSER_TO_CAMERA_BY); // slightly closer to camera var slightlyCloserPosition = Vec3.sum(positionAtHUD, closerToCamera); return slightlyCloserPosition; diff --git a/scripts/system/controllers/controllerScripts.js b/scripts/system/controllers/controllerScripts.js new file mode 100644 index 0000000000..df11a1e5be --- /dev/null +++ b/scripts/system/controllers/controllerScripts.js @@ -0,0 +1,41 @@ +"use strict"; + +// controllerScripts.js +// +// Created by David Rowe on 15 Mar 2017. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +var CONTOLLER_SCRIPTS = [ + "squeezeHands.js", + "controllerDisplayManager.js", + "handControllerGrab.js", + "handControllerPointer.js", + "grab.js", + "teleport.js", + "toggleAdvancedMovementForHandControllers.js", +]; + +var DEBUG_MENU_ITEM = "Debug defaultScripts.js"; + + +function runDefaultsTogether() { + for (var j in CONTOLLER_SCRIPTS) { + Script.include(CONTOLLER_SCRIPTS[j]); + } +} + +function runDefaultsSeparately() { + for (var i in CONTOLLER_SCRIPTS) { + Script.load(CONTOLLER_SCRIPTS[i]); + } +} + +if (Menu.isOptionChecked(DEBUG_MENU_ITEM)) { + runDefaultsSeparately(); +} else { + runDefaultsTogether(); +} diff --git a/scripts/system/controllers/grab.js b/scripts/system/controllers/grab.js index f0b6663bec..10f477b3af 100644 --- a/scripts/system/controllers/grab.js +++ b/scripts/system/controllers/grab.js @@ -343,7 +343,8 @@ Grabber.prototype.pressEvent = function(event) { return; } - if (!pickResults.properties.dynamic) { + var isDynamic = Entities.getEntityProperties(pickResults.entityID, "dynamic").dynamic; + if (!isDynamic) { // only grab dynamic objects return; } @@ -463,7 +464,7 @@ Grabber.prototype.moveEvent = function(event) { var orientation = Camera.getOrientation(); var dragOffset = Vec3.multiply(drag.x, Quat.getRight(orientation)); dragOffset = Vec3.sum(dragOffset, Vec3.multiply(-drag.y, Quat.getUp(orientation))); - var axis = Vec3.cross(dragOffset, Quat.getFront(orientation)); + var axis = Vec3.cross(dragOffset, Quat.getForward(orientation)); axis = Vec3.normalize(axis); var ROTATE_STRENGTH = 0.4; // magic number tuned by hand var angle = ROTATE_STRENGTH * Math.sqrt((drag.x * drag.x) + (drag.y * drag.y)); @@ -487,7 +488,7 @@ Grabber.prototype.moveEvent = function(event) { if (this.mode === "verticalCylinder") { // for this mode we recompute the plane based on current Camera - var planeNormal = Quat.getFront(Camera.getOrientation()); + var planeNormal = Quat.getForward(Camera.getOrientation()); planeNormal.y = 0; planeNormal = Vec3.normalize(planeNormal); var pointOnCylinder = Vec3.multiply(planeNormal, this.xzDistanceToGrab); diff --git a/scripts/system/controllers/handControllerGrab.js b/scripts/system/controllers/handControllerGrab.js index d982a032cc..d1c00a9d81 100644 --- a/scripts/system/controllers/handControllerGrab.js +++ b/scripts/system/controllers/handControllerGrab.js @@ -13,8 +13,8 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html /* global getEntityCustomData, flatten, Xform, Script, Quat, Vec3, MyAvatar, Entities, Overlays, Settings, - Reticle, Controller, Camera, Messages, Mat4, getControllerWorldLocation, getGrabPointSphereOffset, setGrabCommunications, - Menu, HMD, isInEditMode */ + Reticle, Controller, Camera, Messages, Mat4, getControllerWorldLocation, getGrabPointSphereOffset, + setGrabCommunications, Menu, HMD, isInEditMode */ /* eslint indent: ["error", 4, { "outerIIFEBody": 0 }] */ (function() { // BEGIN LOCAL_SCOPE @@ -71,8 +71,6 @@ var EQUIP_SPHERE_SCALE_FACTOR = 0.65; var WEB_DISPLAY_STYLUS_DISTANCE = 0.5; var WEB_STYLUS_LENGTH = 0.2; var WEB_TOUCH_Y_OFFSET = 0.05; // how far forward (or back with a negative number) to slide stylus in hand -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; // // distant manipulation @@ -133,7 +131,6 @@ var GRAB_POINT_SPHERE_ALPHA = 0.85; // // other constants // - var RIGHT_HAND = 1; var LEFT_HAND = 0; @@ -205,14 +202,14 @@ var HARDWARE_MOUSE_ID = 0; // Value reserved for hardware mouse. var STATE_OFF = 0; var STATE_SEARCHING = 1; var STATE_DISTANCE_HOLDING = 2; -var STATE_NEAR_GRABBING = 3; -var STATE_NEAR_TRIGGER = 4; -var STATE_FAR_TRIGGER = 5; -var STATE_HOLD = 6; -var STATE_ENTITY_STYLUS_TOUCHING = 7; +var STATE_DISTANCE_ROTATING = 3; +var STATE_NEAR_GRABBING = 4; +var STATE_NEAR_TRIGGER = 5; +var STATE_FAR_TRIGGER = 6; +var STATE_HOLD = 7; var STATE_ENTITY_LASER_TOUCHING = 8; -var STATE_OVERLAY_STYLUS_TOUCHING = 9; -var STATE_OVERLAY_LASER_TOUCHING = 10; +var STATE_OVERLAY_LASER_TOUCHING = 9; +var STATE_STYLUS_TOUCHING = 10; var CONTROLLER_STATE_MACHINE = {}; @@ -231,6 +228,11 @@ CONTROLLER_STATE_MACHINE[STATE_DISTANCE_HOLDING] = { enterMethod: "distanceHoldingEnter", updateMethod: "distanceHolding" }; +CONTROLLER_STATE_MACHINE[STATE_DISTANCE_ROTATING] = { + name: "distance_rotating", + enterMethod: "distanceRotatingEnter", + updateMethod: "distanceRotating" +}; CONTROLLER_STATE_MACHINE[STATE_NEAR_GRABBING] = { name: "near_grabbing", enterMethod: "nearGrabbingEnter", @@ -251,21 +253,69 @@ CONTROLLER_STATE_MACHINE[STATE_FAR_TRIGGER] = { enterMethod: "farTriggerEnter", updateMethod: "farTrigger" }; -CONTROLLER_STATE_MACHINE[STATE_ENTITY_STYLUS_TOUCHING] = { - name: "entityTouching", - enterMethod: "entityTouchingEnter", - exitMethod: "entityTouchingExit", - updateMethod: "entityTouching" +CONTROLLER_STATE_MACHINE[STATE_ENTITY_LASER_TOUCHING] = { + name: "entityLaserTouching", + enterMethod: "entityLaserTouchingEnter", + exitMethod: "entityLaserTouchingExit", + updateMethod: "entityLaserTouching" }; -CONTROLLER_STATE_MACHINE[STATE_ENTITY_LASER_TOUCHING] = CONTROLLER_STATE_MACHINE[STATE_ENTITY_STYLUS_TOUCHING]; -CONTROLLER_STATE_MACHINE[STATE_OVERLAY_STYLUS_TOUCHING] = { - name: "overlayTouching", - enterMethod: "overlayTouchingEnter", - exitMethod: "overlayTouchingExit", - updateMethod: "overlayTouching" +CONTROLLER_STATE_MACHINE[STATE_OVERLAY_LASER_TOUCHING] = { + name: "overlayLaserTouching", + enterMethod: "overlayLaserTouchingEnter", + exitMethod: "overlayLaserTouchingExit", + updateMethod: "overlayLaserTouching" +}; +CONTROLLER_STATE_MACHINE[STATE_STYLUS_TOUCHING] = { + name: "stylusTouching", + enterMethod: "stylusTouchingEnter", + exitMethod: "stylusTouchingExit", + updateMethod: "stylusTouching" }; -CONTROLLER_STATE_MACHINE[STATE_OVERLAY_LASER_TOUCHING] = CONTROLLER_STATE_MACHINE[STATE_OVERLAY_STYLUS_TOUCHING]; +function distance2D(a, b) { + var dx = (a.x - b.x); + var dy = (a.y - b.y); + return Math.sqrt(dx * dx + dy * dy); +} + +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)); + + return { + position: worldFingerPosition, + 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) { var entityXform = new Xform(entityProps.rotation, entityProps.position); @@ -347,6 +397,7 @@ function handLaserIntersectItem(position, rotation, start) { direction: rayDirection, length: PICK_MAX_DISTANCE }; + return intersectionInfo; } else { // entity has been destroyed? or is no longer in cache @@ -413,16 +464,18 @@ function entityIsGrabbedByOther(entityID) { var actionID = actionIDs[actionIndex]; var actionArguments = Entities.getActionArguments(entityID, actionID); 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. 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. - return true; + return tag.slice(GRAB_PREFIX_LENGTH, GRAB_PREFIX_LENGTH + UUID_LENGTH - 1); } } - return false; + return null; } function propsArePhysical(props) { @@ -501,6 +554,253 @@ function restore2DMode() { } } +function stylusTargetHasKeyboardFocus(stylusTarget) { + if (stylusTarget.entityID && stylusTarget.entityID !== NULL_UUID) { + return Entities.keyboardFocusEntity === stylusTarget.entityID; + } else if (stylusTarget.overlayID && stylusTarget.overlayID !== NULL_UUID) { + return Overlays.keyboardFocusOverlay === stylusTarget.overlayID; + } +} + +function setKeyboardFocusOnStylusTarget(stylusTarget) { + if (stylusTarget.entityID && stylusTarget.entityID !== NULL_UUID && Entities.wantsHandControllerPointerEvents(stylusTarget.entityID)) { + Overlays.keyboardFocusOverlay = NULL_UUID; + Entities.keyboardFocusEntity = stylusTarget.entityID; + } else if (stylusTarget.overlayID && stylusTarget.overlayID !== NULL_UUID) { + Overlays.keyboardFocusOverlay = stylusTarget.overlayID; + Entities.keyboardFocusEntity = NULL_UUID; + } +} + +function sendHoverEnterEventToStylusTarget(hand, stylusTarget) { + var pointerEvent = { + type: "Move", + id: hand + 1, // 0 is reserved for hardware mouse + pos2D: stylusTarget.position2D, + pos3D: stylusTarget.position, + normal: stylusTarget.normal, + direction: Vec3.subtract(ZERO_VEC, stylusTarget.normal), + button: "None" + }; + + if (stylusTarget.entityID && stylusTarget.entityID !== NULL_UUID) { + Entities.sendHoverEnterEntity(stylusTarget.entityID, pointerEvent); + } else if (stylusTarget.overlayID && stylusTarget.overlayID !== NULL_UUID) { + Overlays.sendHoverEnterOverlay(stylusTarget.overlayID, pointerEvent); + } +} + +function sendHoverOverEventToStylusTarget(hand, stylusTarget) { + var pointerEvent = { + type: "Move", + id: hand + 1, // 0 is reserved for hardware mouse + pos2D: stylusTarget.position2D, + pos3D: stylusTarget.position, + normal: stylusTarget.normal, + direction: Vec3.subtract(ZERO_VEC, stylusTarget.normal), + button: "None" + }; + + if (stylusTarget.entityID && stylusTarget.entityID !== NULL_UUID) { + Entities.sendMouseMoveOnEntity(stylusTarget.entityID, pointerEvent); + Entities.sendHoverOverEntity(stylusTarget.entityID, pointerEvent); + } else if (stylusTarget.overlayID && stylusTarget.overlayID !== NULL_UUID) { + Overlays.sendMouseMoveOnOverlay(stylusTarget.overlayID, pointerEvent); + Overlays.sendHoverOverOverlay(stylusTarget.overlayID, pointerEvent); + } +} + +function sendTouchStartEventToStylusTarget(hand, stylusTarget) { + var pointerEvent = { + type: "Press", + id: hand + 1, // 0 is reserved for hardware mouse + pos2D: stylusTarget.position2D, + pos3D: stylusTarget.position, + normal: stylusTarget.normal, + direction: Vec3.subtract(ZERO_VEC, stylusTarget.normal), + button: "Primary", + isPrimaryHeld: true + }; + + if (stylusTarget.entityID && stylusTarget.entityID !== NULL_UUID) { + Entities.sendMousePressOnEntity(stylusTarget.entityID, pointerEvent); + Entities.sendClickDownOnEntity(stylusTarget.entityID, pointerEvent); + } else if (stylusTarget.overlayID && stylusTarget.overlayID !== NULL_UUID) { + Overlays.sendMousePressOnOverlay(stylusTarget.overlayID, pointerEvent); + } +} + +function sendTouchEndEventToStylusTarget(hand, stylusTarget) { + var pointerEvent = { + type: "Release", + id: hand + 1, // 0 is reserved for hardware mouse + pos2D: stylusTarget.position2D, + pos3D: stylusTarget.position, + normal: stylusTarget.normal, + direction: Vec3.subtract(ZERO_VEC, stylusTarget.normal), + button: "Primary" + }; + + if (stylusTarget.entityID && stylusTarget.entityID !== NULL_UUID) { + Entities.sendMouseReleaseOnEntity(stylusTarget.entityID, pointerEvent); + Entities.sendClickReleaseOnEntity(stylusTarget.entityID, pointerEvent); + Entities.sendHoverLeaveEntity(stylusTarget.entityID, pointerEvent); + } else if (stylusTarget.overlayID && stylusTarget.overlayID !== NULL_UUID) { + Overlays.sendMouseReleaseOnOverlay(stylusTarget.overlayID, pointerEvent); + } +} + +function sendTouchMoveEventToStylusTarget(hand, stylusTarget) { + var pointerEvent = { + type: "Move", + id: hand + 1, // 0 is reserved for hardware mouse + pos2D: stylusTarget.position2D, + pos3D: stylusTarget.position, + normal: stylusTarget.normal, + direction: Vec3.subtract(ZERO_VEC, stylusTarget.normal), + button: "Primary", + isPrimaryHeld: true + }; + + if (stylusTarget.entityID && stylusTarget.entityID !== NULL_UUID) { + Entities.sendMouseMoveOnEntity(stylusTarget.entityID, pointerEvent); + Entities.sendHoldingClickOnEntity(stylusTarget.entityID, pointerEvent); + } else if (stylusTarget.overlayID && stylusTarget.overlayID !== NULL_UUID) { + Overlays.sendMouseMoveOnOverlay(stylusTarget.overlayID, pointerEvent); + } +} + +// will return undefined if entity does not exist. +function calculateStylusTargetFromEntity(stylusTip, entityID) { + var props = entityPropertiesCache.getProps(entityID); + if (props.rotation === undefined) { + // if rotation is missing from props object, then this entity has probably been deleted. + return; + } + + // project stylus tip onto entity plane. + var normal = Vec3.multiplyQbyV(props.rotation, {x: 0, y: 0, z: 1}); + Vec3.multiplyQbyV(props.rotation, {x: 0, y: 1, z: 0}); + var distance = Vec3.dot(Vec3.subtract(stylusTip.position, props.position), normal); + var position = Vec3.subtract(stylusTip.position, Vec3.multiply(normal, distance)); + + // generate normalized coordinates + var invRot = Quat.inverse(props.rotation); + var localPos = Vec3.multiplyQbyV(invRot, Vec3.subtract(position, props.position)); + var invDimensions = { x: 1 / props.dimensions.x, y: 1 / props.dimensions.y, z: 1 / props.dimensions.z }; + var normalizedPosition = Vec3.sum(Vec3.multiplyVbyV(localPos, invDimensions), props.registrationPoint); + + // 2D position on entity plane in meters, relative to the bounding box upper-left hand corner. + var position2D = { x: normalizedPosition.x * props.dimensions.x, y: (1 - normalizedPosition.y) * props.dimensions.y }; // flip y-axis + + return { + entityID: entityID, + overlayID: null, + distance: distance, + position: position, + position2D: position2D, + normal: normal, + normalizedPosition: normalizedPosition, + dimensions: props.dimensions, + valid: true + }; +} + +// will return undefined if overlayID does not exist. +function calculateStylusTargetFromOverlay(stylusTip, overlayID) { + var overlayPosition = Overlays.getProperty(overlayID, "position"); + if (overlayPosition === undefined) { + return; + } + + // project stylusTip onto overlay plane. + var overlayRotation = Overlays.getProperty(overlayID, "rotation"); + if (overlayRotation === undefined) { + return; + } + var normal = Vec3.multiplyQbyV(overlayRotation, {x: 0, y: 0, z: 1}); + var distance = Vec3.dot(Vec3.subtract(stylusTip.position, overlayPosition), normal); + var position = Vec3.subtract(stylusTip.position, Vec3.multiply(normal, distance)); + + // calclulate normalized position + var invRot = Quat.inverse(overlayRotation); + var localPos = Vec3.multiplyQbyV(invRot, Vec3.subtract(position, overlayPosition)); + var dpi = Overlays.getProperty(overlayID, "dpi"); + + var dimensions; + if (dpi) { + // Calculate physical dimensions for web3d overlay from resolution and dpi; "dimensions" property is used as a scale. + var resolution = Overlays.getProperty(overlayID, "resolution"); + if (resolution === undefined) { + return; + } + resolution.z = 1; // Circumvent divide-by-zero. + var scale = Overlays.getProperty(overlayID, "dimensions"); + if (scale === undefined) { + return; + } + scale.z = 0.01; // overlay dimensions are 2D, not 3D. + dimensions = Vec3.multiplyVbyV(Vec3.multiply(resolution, INCHES_TO_METERS / dpi), scale); + } else { + dimensions = Overlays.getProperty(overlayID, "dimensions"); + if (dimensions === undefined) { + return; + } + if (!dimensions.z) { + dimensions.z = 0.01; // sometimes overlay dimensions are 2D, not 3D. + } + } + var invDimensions = { x: 1 / dimensions.x, y: 1 / dimensions.y, z: 1 / dimensions.z }; + var normalizedPosition = Vec3.sum(Vec3.multiplyVbyV(localPos, invDimensions), DEFAULT_REGISTRATION_POINT); + + // 2D position on overlay plane in meters, relative to the bounding box upper-left hand corner. + var position2D = { x: normalizedPosition.x * dimensions.x, y: (1 - normalizedPosition.y) * dimensions.y }; // flip y-axis + + return { + entityID: null, + overlayID: overlayID, + distance: distance, + position: position, + position2D: position2D, + normal: normal, + normalizedPosition: normalizedPosition, + dimensions: dimensions, + valid: true + }; +} + +function isNearStylusTarget(stylusTargets, edgeBorder, minNormalDistance, maxNormalDistance) { + for (var i = 0; i < stylusTargets.length; i++) { + var stylusTarget = stylusTargets[i]; + + // check to see if the projected stylusTip is within within the 2d border + var borderMin = {x: -edgeBorder, y: -edgeBorder}; + var borderMax = {x: stylusTarget.dimensions.x + edgeBorder, y: stylusTarget.dimensions.y + edgeBorder}; + if (stylusTarget.distance >= minNormalDistance && stylusTarget.distance <= maxNormalDistance && + stylusTarget.position2D.x >= borderMin.x && stylusTarget.position2D.y >= borderMin.y && + stylusTarget.position2D.x <= borderMax.x && stylusTarget.position2D.y <= borderMax.y) { + return true; + } + } + return false; +} + +function calculateNearestStylusTarget(stylusTargets) { + var nearestStylusTarget; + + for (var i = 0; i < stylusTargets.length; i++) { + var stylusTarget = stylusTargets[i]; + + if ((!nearestStylusTarget || stylusTarget.distance < nearestStylusTarget.distance) && + stylusTarget.normalizedPosition.x >= 0 && stylusTarget.normalizedPosition.y >= 0 && + stylusTarget.normalizedPosition.x <= 1 && stylusTarget.normalizedPosition.y <= 1) { + nearestStylusTarget = stylusTarget; + } + } + + return nearestStylusTarget; +}; + // EntityPropertiesCache is a helper class that contains a cache of entity properties. // the hope is to prevent excess calls to Entity.getEntityProperties() // @@ -730,6 +1030,16 @@ EquipHotspotBuddy.prototype.update = function(deltaTime, timestamp) { } }; +function getControllerJointIndex(hand) { + if (HMD.isHandControllerAvailable()) { + return MyAvatar.getJointIndex(hand === RIGHT_HAND ? + "_CONTROLLER_RIGHTHAND" : + "_CONTROLLER_LEFTHAND"); + } + + return MyAvatar.getJointIndex("Head"); +} + // global EquipHotspotBuddy instance var equipHotspotBuddy = new EquipHotspotBuddy(); @@ -739,6 +1049,9 @@ function MyController(hand) { this.grabPointIntersectsEntity = false; this.stylus = null; this.homeButtonTouched = false; + this.editTriggered = false; + + this.controllerJointIndex = getControllerJointIndex(this.hand); // 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 @@ -781,7 +1094,6 @@ function MyController(hand) { this.grabbedOverlay = null; this.state = STATE_OFF; this.pointer = null; // entity-id of line object - this.entityActivated = false; this.triggerValue = 0; // rolling average of trigger value this.triggerClicked = false; @@ -791,10 +1103,10 @@ function MyController(hand) { // for visualizations this.overlayLine = null; - - // for lights - this.overlayLine = null; this.searchSphere = null; + this.otherGrabbingLine = null; + + this.otherGrabbingUUID = null; this.waitForTriggerRelease = false; @@ -816,6 +1128,19 @@ function MyController(hand) { this.tabletStabbedPos2D = null; this.tabletStabbedPos3D = null; + this.useFingerInsteadOfStylus = false; + this.fingerPointing = false; + + // initialize stylus tip + var DEFAULT_STYLUS_TIP = { + position: {x: 0, y: 0, z: 0}, + orientation: {x: 0, y: 0, z: 0, w: 0}, + rotation: {x: 0, y: 0, z: 0, w: 0}, + velocity: {x: 0, y: 0, z: 0}, + valid: false + }; + this.stylusTip = DEFAULT_STYLUS_TIP; + var _this = this; var suppressedIn2D = [STATE_OFF, STATE_SEARCHING]; @@ -825,17 +1150,52 @@ function MyController(hand) { return (-1 !== suppressedIn2D.indexOf(this.state)) && isIn2DMode(); }; + this.updateStylusTip = function() { + if (this.useFingerInsteadOfStylus) { + this.stylusTip = getFingerWorldLocation(this.hand); + } else { + this.stylusTip = getControllerWorldLocation(this.handToController(), true); + + // translate tip forward according to constant. + var TIP_OFFSET = {x: 0, y: WEB_STYLUS_LENGTH - WEB_TOUCH_Y_OFFSET, z: 0}; + this.stylusTip.position = Vec3.sum(this.stylusTip.position, Vec3.multiplyQbyV(this.stylusTip.orientation, TIP_OFFSET)); + } + + // compute tip velocity from hand controller motion, it is more accurate then computing it from previous positions. + var pose = Controller.getPoseValue(this.handToController()); + if (pose.valid) { + var worldControllerPos = Vec3.sum(MyAvatar.position, Vec3.multiplyQbyV(MyAvatar.orientation, pose.translation)); + var worldControllerLinearVel = Vec3.multiplyQbyV(MyAvatar.orientation, pose.velocity); + var worldControllerAngularVel = Vec3.multiplyQbyV(MyAvatar.orientation, pose.angularVelocity); + var tipVelocity = Vec3.sum(worldControllerLinearVel, Vec3.cross(worldControllerAngularVel, Vec3.subtract(this.stylusTip.position, worldControllerPos))); + this.stylusTip.velocity = tipVelocity; + } else { + this.stylusTip.velocity = {x: 0, y: 0, z: 0}; + } + }; + this.update = function(deltaTime, timestamp) { this.updateSmoothedTrigger(); this.maybeScaleMyAvatar(); + this.updateStylusTip(); + + 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()) { // 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. - var controllerLocation = getControllerWorldLocation(this.handToController(), true); - this.processStylus(controllerLocation.position); - + this.processStylus(); this.turnOffVisualizations(); return; } @@ -865,11 +1225,12 @@ function MyController(hand) { if ((isInEditMode() && this.grabbedThingID !== HMD.tabletID) && (newState !== STATE_OFF && newState !== STATE_SEARCHING && - newState !== STATE_OVERLAY_STYLUS_TOUCHING && + newState !== STATE_STYLUS_TOUCHING && newState !== STATE_OVERLAY_LASER_TOUCHING)) { return; } - setGrabCommunications((newState === STATE_DISTANCE_HOLDING) || (newState === STATE_NEAR_GRABBING)); + setGrabCommunications((newState === STATE_DISTANCE_HOLDING) || (newState === STATE_DISTANCE_ROTATING) + || (newState === STATE_NEAR_GRABBING)); if (WANT_DEBUG || WANT_DEBUG_STATE) { var oldStateName = stateToName(this.state); var newStateName = stateToName(newState); @@ -920,9 +1281,7 @@ function MyController(hand) { ignoreRayIntersection: true, drawInFront: false, parentID: AVATAR_SELF_ID, - parentJointIndex: MyAvatar.getJointIndex(this.hand === RIGHT_HAND ? - "_CONTROLLER_RIGHTHAND" : - "_CONTROLLER_LEFTHAND") + parentJointIndex: this.controllerJointIndex }); } }; @@ -1001,38 +1360,40 @@ function MyController(hand) { } Overlays.deleteOverlay(this.stylus); this.stylus = null; - if (this.stylusTip) { - Overlays.deleteOverlay(this.stylusTip); - this.stylusTip = null; - } }; - this.overlayLineOn = function(closePoint, farPoint, color) { + this.overlayLineOn = function(closePoint, farPoint, color, farParentID) { if (this.overlayLine === null) { var lineProperties = { name: "line", glow: 1.0, - start: closePoint, - end: farPoint, - color: color, - ignoreRayIntersection: true, // always ignore this - drawInFront: true, // Even when burried inside of something, show it. - visible: true, - alpha: 1 - }; - this.overlayLine = Overlays.addOverlay("line3d", lineProperties); - - } else { - Overlays.editOverlay(this.overlayLine, { lineWidth: 5, start: closePoint, end: farPoint, color: color, - visible: true, ignoreRayIntersection: true, // always ignore this drawInFront: true, // Even when burried inside of something, show it. - alpha: 1 - }); + visible: true, + alpha: 1, + parentID: AVATAR_SELF_ID, + parentJointIndex: this.controllerJointIndex, + endParentID: farParentID + }; + this.overlayLine = Overlays.addOverlay("line3d", lineProperties); + + } else { + if (farParentID && farParentID != NULL_UUID) { + Overlays.editOverlay(this.overlayLine, { + color: color, + endParentID: farParentID + }); + } else { + Overlays.editOverlay(this.overlayLine, { + length: Vec3.distance(farPoint, closePoint), + color: color, + endParentID: farParentID + }); + } } }; @@ -1048,7 +1409,7 @@ function MyController(hand) { } var searchSphereLocation = Vec3.sum(distantPickRay.origin, - Vec3.multiply(distantPickRay.direction, this.searchSphereDistance)); + Vec3.multiply(distantPickRay.direction, this.searchSphereDistance)); this.searchSphereOn(searchSphereLocation, SEARCH_SPHERE_SIZE * this.searchSphereDistance, (this.triggerSmoothedGrab() || this.secondarySqueezed()) ? COLORS_GRAB_SEARCHING_FULL_SQUEEZE : @@ -1061,6 +1422,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) { var MODEL_LIGHT_POSITION = { @@ -1104,14 +1488,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.grabPointSphereOff(); this.lineOff(); this.searchSphereOff(); + this.otherGrabbingLineOff(); restore2DMode(); - }; this.triggerPress = function(value) { @@ -1173,50 +1563,185 @@ function MyController(hand) { return _this.rawThumbValue < THUMB_ON_VALUE; }; - this.processStylus = function(worldHandPosition) { - // see if the hand is near a tablet or web-entity - var candidateEntities = Entities.findEntities(worldHandPosition, WEB_DISPLAY_STYLUS_DISTANCE); - entityPropertiesCache.addEntities(candidateEntities); - var nearWeb = false; - for (var i = 0; i < candidateEntities.length; i++) { - var props = entityPropertiesCache.getProps(candidateEntities[i]); - if (props && (props.type == "Web" || this.isTablet(candidateEntities[i]))) { - nearWeb = true; - break; - } + this.stealTouchFocus = function(stylusTarget) { + // send hover events to target + // record the entity or overlay we are hovering over. + if ((stylusTarget.entityID === this.getOtherHandController().hoverEntity) || + (stylusTarget.overlayID === this.getOtherHandController().hoverOverlay)) { + this.getOtherHandController().relinquishTouchFocus(); } + this.requestTouchFocus(stylusTarget); + }; - var candidateOverlays = Overlays.findOverlays(worldHandPosition, WEB_DISPLAY_STYLUS_DISTANCE); - for (var j = 0; j < candidateOverlays.length; j++) { - if (this.isTablet(candidateOverlays[j])) { - nearWeb = true; - } + this.requestTouchFocus = function(stylusTarget) { + + // send hover events to target if we can. + // record the entity or overlay we are hovering over. + if (stylusTarget.entityID && stylusTarget.entityID !== this.hoverEntity && stylusTarget.entityID !== this.getOtherHandController().hoverEntity) { + this.hoverEntity = stylusTarget.entityID; + sendHoverEnterEventToStylusTarget(this.hand, stylusTarget); + } else if (stylusTarget.overlayID && stylusTarget.overlayID !== this.hoverOverlay && stylusTarget.overlayID !== this.getOtherHandController().hoverOverlay) { + this.hoverOverlay = stylusTarget.overlayID; + sendHoverEnterEventToStylusTarget(this.hand, stylusTarget); } + }; - if (nearWeb) { - 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); - if (this.handleStylusOnWebEntity(rayPickInfo)) { - return; - } - if (this.handleStylusOnWebOverlay(rayPickInfo)) { - return; - } + this.hasTouchFocus = function(stylusTarget) { + return ((stylusTarget.entityID && stylusTarget.entityID === this.hoverEntity) || + (stylusTarget.overlayID && stylusTarget.overlayID === this.hoverOverlay)); + }; + + this.relinquishTouchFocus = function() { + + // send hover leave event. + var pointerEvent = { type: "Move", id: this.hand + 1 }; + if (this.hoverEntity) { + Entities.sendHoverLeaveEntity(this.hoverEntity, pointerEvent); + this.hoverEntity = null; + } else if (this.hoverOverlay) { + Overlays.sendMouseMoveOnOverlay(this.hoverOverlay, pointerEvent); + Overlays.sendHoverOverOverlay(this.hoverOverlay, pointerEvent); + Overlays.sendHoverLeaveOverlay(this.hoverOverlay, pointerEvent); + this.hoverOverlay = null; + } + }; + + this.pointFinger = function(value) { + var HIFI_POINT_INDEX_MESSAGE_CHANNEL = "Hifi-Point-Index"; + if (this.fingerPointing !== value) { + var message; + if (this.hand === RIGHT_HAND) { + message = { pointRightIndex: value }; } else { - this.homeButtonTouched = false; + message = { pointLeftIndex: value }; + } + Messages.sendMessage(HIFI_POINT_INDEX_MESSAGE_CHANNEL, JSON.stringify(message), true); + this.fingerPointing = value; } - } else { + }; + + this.processStylus = function() { + if (!this.stylusTip.valid) { + this.pointFinger(false); + this.hideStylus(); + return; + } + + if (this.useFingerInsteadOfStylus) { this.hideStylus(); } + + var tipPosition = this.stylusTip.position; + + var candidates = { + entities: [], + overlays: [] + }; + + // build list of stylus targets, near the stylusTip + var stylusTargets = []; + var candidateEntities = Entities.findEntities(tipPosition, WEB_DISPLAY_STYLUS_DISTANCE); + entityPropertiesCache.addEntities(candidateEntities); + var i, props, stylusTarget; + for (i = 0; i < candidateEntities.length; i++) { + props = entityPropertiesCache.getProps(candidateEntities[i]); + if (props && (props.type === "Web" || this.isTablet(candidateEntities[i]))) { + stylusTarget = calculateStylusTargetFromEntity(this.stylusTip, candidateEntities[i]); + if (stylusTarget) { + stylusTargets.push(stylusTarget); + } + } + } + + // add the tabletScreen, if it is valid + if (HMD.tabletScreenID && HMD.tabletScreenID !== NULL_UUID && Overlays.getProperty(HMD.tabletScreenID, "visible")) { + stylusTarget = calculateStylusTargetFromOverlay(this.stylusTip, HMD.tabletScreenID); + if (stylusTarget) { + stylusTargets.push(stylusTarget); + } + } + + // add the tablet home button. + if (HMD.homeButtonID && HMD.homeButtonID !== NULL_UUID && Overlays.getProperty(HMD.homeButtonID, "visible")) { + stylusTarget = calculateStylusTargetFromOverlay(this.stylusTip, HMD.homeButtonID); + if (stylusTarget) { + stylusTargets.push(stylusTarget); + } + } + + var TABLET_MIN_HOVER_DISTANCE = 0.01; + var TABLET_MAX_HOVER_DISTANCE = 0.1; + var TABLET_MIN_TOUCH_DISTANCE = -0.05; + var TABLET_MAX_TOUCH_DISTANCE = TABLET_MIN_HOVER_DISTANCE; + var EDGE_BORDER = 0.075; + + var hysteresisOffset = 0.0; + if (this.isNearStylusTarget) { + hysteresisOffset = 0.05; + } + + this.isNearStylusTarget = isNearStylusTarget(stylusTargets, EDGE_BORDER + hysteresisOffset, + TABLET_MIN_TOUCH_DISTANCE - hysteresisOffset, WEB_DISPLAY_STYLUS_DISTANCE + hysteresisOffset); + + if (this.isNearStylusTarget) { + if (!this.useFingerInsteadOfStylus) { + this.showStylus(); + } else { + this.pointFinger(true); + } + } else { + this.hideStylus(); + this.pointFinger(false); + } + + var nearestStylusTarget = calculateNearestStylusTarget(stylusTargets); + + if (nearestStylusTarget && nearestStylusTarget.distance > TABLET_MIN_TOUCH_DISTANCE && + nearestStylusTarget.distance < TABLET_MAX_HOVER_DISTANCE) { + + this.requestTouchFocus(nearestStylusTarget); + + if (!stylusTargetHasKeyboardFocus(nearestStylusTarget)) { + setKeyboardFocusOnStylusTarget(nearestStylusTarget); + } + + if (this.hasTouchFocus(nearestStylusTarget)) { + sendHoverOverEventToStylusTarget(this.hand, nearestStylusTarget); + } + + // filter out presses when tip is moving away from tablet. + // ensure that stylus is within bounding box by checking normalizedPosition + if (nearestStylusTarget.valid && nearestStylusTarget.distance > TABLET_MIN_TOUCH_DISTANCE && + nearestStylusTarget.distance < TABLET_MAX_TOUCH_DISTANCE && Vec3.dot(this.stylusTip.velocity, nearestStylusTarget.normal) < 0 && + nearestStylusTarget.normalizedPosition.x >= 0 && nearestStylusTarget.normalizedPosition.x <= 1 && + nearestStylusTarget.normalizedPosition.y >= 0 && nearestStylusTarget.normalizedPosition.y <= 1) { + + var name; + if (nearestStylusTarget.entityID) { + name = entityPropertiesCache.getProps(nearestStylusTarget.entityID).name; + this.stylusTarget = nearestStylusTarget; + this.setState(STATE_STYLUS_TOUCHING, "begin touching entity '" + name + "'"); + } else if (nearestStylusTarget.overlayID) { + name = Overlays.getProperty(nearestStylusTarget.overlayID, "name"); + this.stylusTarget = nearestStylusTarget; + this.setState(STATE_STYLUS_TOUCHING, "begin touching overlay '" + name + "'"); + } + } + } else { + this.relinquishTouchFocus(); + } + + this.homeButtonTouched = false; }; this.off = function(deltaTime, timestamp) { this.checkForUnexpectedChildren(); + if (this.editTriggered) { + this.editTriggered = false; + } + if (this.triggerSmoothedReleased() && this.secondaryReleased()) { this.waitForTriggerRelease = false; } @@ -1266,25 +1791,7 @@ function MyController(hand) { this.grabPointSphereOff(); } - this.processStylus(worldHandPosition); - }; - - this.handleStylusOnHomeButton = function(rayPickInfo) { - if (rayPickInfo.overlayID) { - var homeButton = rayPickInfo.overlayID; - var hmdHomeButton = HMD.homeButtonID; - if (homeButton === hmdHomeButton) { - if (this.homeButtonTouched === false) { - this.homeButtonTouched = true; - Controller.triggerHapticPulse(HAPTIC_STYLUS_STRENGTH, HAPTIC_STYLUS_DURATION, this.hand); - Messages.sendLocalMessage("home", homeButton); - } - } else { - this.homeButtonTouched = false; - } - } else { - this.homeButtonTouched = false; - } + this.processStylus(); }; this.handleLaserOnHomeButton = function(rayPickInfo) { @@ -1324,20 +1831,27 @@ function MyController(hand) { // Performs ray pick test from the hand controller into the world // @param {number} which hand to use, RIGHT_HAND or LEFT_HAND + // @param {object} if set, use this as as the pick ray, expects origin, direction, and length fields. // @returns {object} returns object with two keys entityID and distance // - this.calcRayPickInfo = function(hand) { - var controllerLocation = getControllerWorldLocation(this.handToController(), true); - var worldHandPosition = controllerLocation.position; - var worldHandRotation = controllerLocation.orientation; + this.calcRayPickInfo = function(hand, pickRayOverride) { - var pickRay = { - origin: PICK_WITH_HAND_RAY ? worldHandPosition : Camera.position, - direction: PICK_WITH_HAND_RAY ? Quat.getUp(worldHandRotation) : Vec3.mix(Quat.getUp(worldHandRotation), - Quat.getFront(Camera.orientation), - HAND_HEAD_MIX_RATIO), - length: PICK_MAX_DISTANCE - }; + var pickRay; + if (pickRayOverride) { + pickRay = pickRayOverride; + } else { + var controllerLocation = getControllerWorldLocation(this.handToController(), true); + var worldHandPosition = controllerLocation.position; + var worldHandRotation = controllerLocation.orientation; + + pickRay = { + origin: PICK_WITH_HAND_RAY ? worldHandPosition : Camera.position, + direction: PICK_WITH_HAND_RAY ? Quat.getUp(worldHandRotation) : Vec3.mix(Quat.getUp(worldHandRotation), + Quat.getFront(Camera.orientation), + HAND_HEAD_MIX_RATIO), + length: PICK_MAX_DISTANCE + }; + } var result = { entityID: null, @@ -1439,9 +1953,10 @@ function MyController(hand) { var props = entityPropertiesCache.getProps(hotspot.entityID); var debug = (WANT_DEBUG_SEARCH_NAME && props.name === WANT_DEBUG_SEARCH_NAME); - var okToEquipFromOtherHand = ((this.getOtherHandController().state == STATE_NEAR_GRABBING || - this.getOtherHandController().state == STATE_DISTANCE_HOLDING) && - this.getOtherHandController().grabbedThingID == hotspot.entityID); + var otherHandControllerState = this.getOtherHandController().state; + var okToEquipFromOtherHand = ((otherHandControllerState === STATE_NEAR_GRABBING + || otherHandControllerState === STATE_DISTANCE_HOLDING || otherHandControllerState === STATE_DISTANCE_ROTATING) + && this.getOtherHandController().grabbedThingID === hotspot.entityID); var hasParent = true; if (props.parentID === NULL_UUID) { hasParent = false; @@ -1455,7 +1970,18 @@ function MyController(hand) { return true; }; + this.entityIsCloneable = function(entityID) { + var entityProps = entityPropertiesCache.getGrabbableProps(entityID); + var props = entityPropertiesCache.getProps(entityID); + if (!props) { + return false; + } + if (entityProps.hasOwnProperty("cloneable")) { + return entityProps.cloneable; + } + return false; + } this.entityIsGrabbable = function(entityID) { var grabbableProps = entityPropertiesCache.getGrabbableProps(entityID); var props = entityPropertiesCache.getProps(entityID); @@ -1522,7 +2048,8 @@ function MyController(hand) { return false; } - if (entityIsGrabbedByOther(entityID)) { + this.otherGrabbingUUID = entityIsGrabbedByOther(entityID); + if (this.otherGrabbingUUID !== null) { // don't distance grab something that is already grabbed. if (debug) { print("distance grab is skipping '" + props.name + "': already grabbed by another."); @@ -1535,7 +2062,7 @@ function MyController(hand) { this.entityIsNearGrabbable = function(entityID, handPosition, maxDistance) { - if (!this.entityIsGrabbable(entityID)) { + if (!this.entityIsCloneable(entityID) && !this.entityIsGrabbable(entityID)) { return false; } @@ -1683,23 +2210,21 @@ function MyController(hand) { return aDistance - bDistance; }); entity = grabbableEntities[0]; - name = entityPropertiesCache.getProps(entity).name; - this.grabbedThingID = entity; - this.grabbedIsOverlay = false; - if (this.entityWantsTrigger(entity)) { - if (this.triggerSmoothedGrab()) { - this.setState(STATE_NEAR_TRIGGER, "near trigger '" + name + "'"); - return; + if (!isInEditMode() || entity == HMD.tabletID) { // tablet is grabbable, even when editing + name = entityPropertiesCache.getProps(entity).name; + this.grabbedThingID = entity; + this.grabbedIsOverlay = false; + if (this.entityWantsTrigger(entity)) { + if (this.triggerSmoothedGrab()) { + this.setState(STATE_NEAR_TRIGGER, "near trigger '" + name + "'"); + return; + } } else { - // potentialNearTriggerEntity = entity; - } - } else { - // If near something grabbable, grab it! - if ((this.triggerSmoothedGrab() || this.secondarySqueezed()) && nearGrabEnabled) { - this.setState(STATE_NEAR_GRABBING, "near grab entity '" + name + "'"); - return; - } else { - // potentialNearGrabEntity = entity; + // If near something grabbable, grab it! + if ((this.triggerSmoothedGrab() || this.secondarySqueezed()) && nearGrabEnabled) { + this.setState(STATE_NEAR_GRABBING, "near grab entity '" + name + "'"); + return; + } } } } @@ -1714,6 +2239,21 @@ function MyController(hand) { } } + if (isInEditMode()) { + this.searchIndicatorOn(rayPickInfo.searchRay); + if (this.triggerSmoothedGrab()) { + if (!this.editTriggered && rayPickInfo.entityID) { + Messages.sendLocalMessage("entityToolUpdates", JSON.stringify({ + method: "selectEntity", + entityID: rayPickInfo.entityID + })); + } + this.editTriggered = true; + } + Reticle.setVisible(false); + return; + } + if (rayPickInfo.entityID) { entity = rayPickInfo.entityID; name = entityPropertiesCache.getProps(entity).name; @@ -1726,17 +2266,40 @@ function MyController(hand) { } else { // potentialFarTriggerEntity = entity; } + this.otherGrabbingLineOff(); } else if (this.entityIsDistanceGrabbable(rayPickInfo.entityID, handPosition)) { if (this.triggerSmoothedGrab() && !isEditing() && farGrabEnabled && farSearching) { this.grabbedThingID = entity; this.grabbedIsOverlay = false; this.grabbedDistance = rayPickInfo.distance; + if (this.getOtherHandController().state === STATE_DISTANCE_HOLDING) { + this.setState(STATE_DISTANCE_ROTATING, "distance rotate '" + name + "'"); + } else { this.setState(STATE_DISTANCE_HOLDING, "distance hold '" + name + "'"); + } return; } else { // potentialFarGrabEntity = entity; } + 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); @@ -1760,7 +2323,7 @@ function MyController(hand) { return false; }; - this.handleStylusOnWebEntity = function (rayPickInfo) { + this.handleLaserOnWebEntity = function (rayPickInfo) { var pointerEvent; if (rayPickInfo.entityID && Entities.wantsHandControllerPointerEvents(rayPickInfo.entityID)) { @@ -1789,9 +2352,8 @@ function MyController(hand) { if (this.hand == mostRecentSearchingHand || (this.hand !== mostRecentSearchingHand && this.getOtherHandController().state !== STATE_SEARCHING && - this.getOtherHandController().state !== STATE_ENTITY_STYLUS_TOUCHING && + this.getOtherHandController().state !== STATE_STYLUS_TOUCHING && this.getOtherHandController().state !== STATE_ENTITY_LASER_TOUCHING && - this.getOtherHandController().state !== STATE_OVERLAY_STYLUS_TOUCHING && this.getOtherHandController().state !== STATE_OVERLAY_LASER_TOUCHING)) { // most recently searching hand has priority over other hand, for the purposes of button highlighting. @@ -1809,140 +2371,13 @@ function MyController(hand) { Entities.sendHoverOverEntity(entity, pointerEvent); } - this.grabbedThingID = entity; - this.grabbedIsOverlay = false; - this.setState(STATE_ENTITY_STYLUS_TOUCHING, "begin touching entity '" + name + "'"); - return true; - - } else if (this.hoverEntity) { - pointerEvent = { - type: "Move", - id: this.hand + 1 - }; - Entities.sendHoverLeaveEntity(this.hoverEntity, pointerEvent); - this.hoverEntity = null; - } - - return false; - }; - - this.handleStylusOnWebOverlay = function (rayPickInfo) { - var pointerEvent; - if (rayPickInfo.overlayID) { - var overlay = rayPickInfo.overlayID; - if (Overlays.keyboardFocusOverlay != overlay) { - Entities.keyboardFocusEntity = null; - Overlays.keyboardFocusOverlay = overlay; - - pointerEvent = { - type: "Move", - id: HARDWARE_MOUSE_ID, - pos2D: projectOntoOverlayXYPlane(overlay, rayPickInfo.intersection), - pos3D: rayPickInfo.intersection, - normal: rayPickInfo.normal, - direction: rayPickInfo.searchRay.direction, - button: "None" - }; - - this.hoverOverlay = overlay; - Overlays.sendHoverEnterOverlay(overlay, pointerEvent); - } - - // Send mouse events for button highlights and tooltips. - if (this.hand == mostRecentSearchingHand || - (this.hand !== mostRecentSearchingHand && - this.getOtherHandController().state !== STATE_SEARCHING && - this.getOtherHandController().state !== STATE_ENTITY_STYLUS_TOUCHING && - this.getOtherHandController().state !== STATE_ENTITY_LASER_TOUCHING && - this.getOtherHandController().state !== STATE_OVERLAY_STYLUS_TOUCHING && - this.getOtherHandController().state !== STATE_OVERLAY_LASER_TOUCHING)) { - - // most recently searching hand has priority over other hand, for the purposes of button highlighting. - pointerEvent = { - type: "Move", - id: HARDWARE_MOUSE_ID, - pos2D: projectOntoOverlayXYPlane(overlay, rayPickInfo.intersection), - pos3D: rayPickInfo.intersection, - normal: rayPickInfo.normal, - direction: rayPickInfo.searchRay.direction, - button: "None" - }; - - Overlays.sendMouseMoveOnOverlay(overlay, pointerEvent); - Overlays.sendHoverOverOverlay(overlay, pointerEvent); - } - - this.grabbedOverlay = overlay; - this.setState(STATE_OVERLAY_STYLUS_TOUCHING, "begin touching overlay '" + overlay + "'"); - return true; - - } else if (this.hoverOverlay) { - pointerEvent = { - type: "Move", - id: HARDWARE_MOUSE_ID - }; - Overlays.sendHoverLeaveOverlay(this.hoverOverlay, pointerEvent); - this.hoverOverlay = null; - } - - return false; - }; - - this.handleLaserOnWebEntity = function(rayPickInfo) { - var pointerEvent; - if (rayPickInfo.entityID && Entities.wantsHandControllerPointerEvents(rayPickInfo.entityID)) { - var entity = rayPickInfo.entityID; - var props = entityPropertiesCache.getProps(entity); - var name = props.name; - - if (Entities.keyboardFocusEntity != entity) { - Overlays.keyboardFocusOverlay = 0; - Entities.keyboardFocusEntity = entity; - - pointerEvent = { - type: "Move", - id: this.hand + 1, // 0 is reserved for hardware mouse - pos2D: projectOntoEntityXYPlane(entity, rayPickInfo.intersection), - pos3D: rayPickInfo.intersection, - normal: rayPickInfo.normal, - direction: rayPickInfo.searchRay.direction, - button: "None" - }; - - this.hoverEntity = entity; - Entities.sendHoverEnterEntity(entity, pointerEvent); - } - - // send mouse events for button highlights and tooltips. - if (this.hand == mostRecentSearchingHand || - (this.hand !== mostRecentSearchingHand && - this.getOtherHandController().state !== STATE_SEARCHING && - this.getOtherHandController().state !== STATE_ENTITY_STYLUS_TOUCHING && - this.getOtherHandController().state !== STATE_ENTITY_LASER_TOUCHING && - this.getOtherHandController().state !== STATE_OVERLAY_STYLUS_TOUCHING && - this.getOtherHandController().state !== STATE_OVERLAY_LASER_TOUCHING)) { - - // most recently searching hand has priority over other hand, for the purposes of button highlighting. - pointerEvent = { - type: "Move", - id: this.hand + 1, // 0 is reserved for hardware mouse - pos2D: projectOntoEntityXYPlane(entity, rayPickInfo.intersection), - pos3D: rayPickInfo.intersection, - normal: rayPickInfo.normal, - direction: rayPickInfo.searchRay.direction, - button: "None" - }; - - Entities.sendMouseMoveOnEntity(entity, pointerEvent); - Entities.sendHoverOverEntity(entity, pointerEvent); - } - - if (this.triggerSmoothedGrab() && (!isEditing() || this.isTablet(entity))) { + if (this.triggerSmoothedGrab()) { this.grabbedThingID = entity; this.grabbedIsOverlay = false; this.setState(STATE_ENTITY_LASER_TOUCHING, "begin touching entity '" + name + "'"); return true; } + } else if (this.hoverEntity) { pointerEvent = { type: "Move", @@ -1955,13 +2390,13 @@ function MyController(hand) { return false; }; - this.handleLaserOnWebOverlay = function(rayPickInfo) { + this.handleLaserOnWebOverlay = function (rayPickInfo) { var pointerEvent; - var overlay; - if (rayPickInfo.overlayID) { - overlay = rayPickInfo.overlayID; - + var overlay = rayPickInfo.overlayID; + if (Overlays.getProperty(overlay, "type") != "web3d") { + return false; + } if (Overlays.keyboardFocusOverlay != overlay) { Entities.keyboardFocusEntity = null; Overlays.keyboardFocusOverlay = overlay; @@ -1984,9 +2419,8 @@ function MyController(hand) { if (this.hand == mostRecentSearchingHand || (this.hand !== mostRecentSearchingHand && this.getOtherHandController().state !== STATE_SEARCHING && - this.getOtherHandController().state !== STATE_ENTITY_STYLUS_TOUCHING && + this.getOtherHandController().state !== STATE_STYLUS_TOUCHING && this.getOtherHandController().state !== STATE_ENTITY_LASER_TOUCHING && - this.getOtherHandController().state !== STATE_OVERLAY_STYLUS_TOUCHING && this.getOtherHandController().state !== STATE_OVERLAY_LASER_TOUCHING)) { // most recently searching hand has priority over other hand, for the purposes of button highlighting. @@ -2036,6 +2470,19 @@ function MyController(hand) { return (dimensions.x * dimensions.y * dimensions.z) * density; }; + this.ensureDynamic = function () { + // if we distance hold something and keep it very still before releasing it, it ends up + // non-dynamic in bullet. If it's too still, give it a little bounce so it will fall. + var props = Entities.getEntityProperties(this.grabbedThingID, ["velocity", "dynamic", "parentID"]); + if (props.dynamic && props.parentID == NULL_UUID) { + var velocity = props.velocity; + if (Vec3.length(velocity) < 0.05) { // see EntityMotionState.cpp DYNAMIC_LINEAR_VELOCITY_THRESHOLD + velocity = { x: 0.0, y: 0.2, z: 0.0 }; + Entities.editEntity(this.grabbedThingID, { velocity: velocity }); + } + } + }; + this.distanceHoldingEnter = function() { this.clearEquipHaptics(); this.grabPointSphereOff(); @@ -2102,25 +2549,20 @@ function MyController(hand) { this.previousRoomControllerPosition = roomControllerPosition; }; - this.ensureDynamic = function() { - // if we distance hold something and keep it very still before releasing it, it ends up - // non-dynamic in bullet. If it's too still, give it a little bounce so it will fall. - var props = Entities.getEntityProperties(this.grabbedThingID, ["velocity", "dynamic", "parentID"]); - if (props.dynamic && props.parentID == NULL_UUID) { - var velocity = props.velocity; - if (Vec3.length(velocity) < 0.05) { // see EntityMotionState.cpp DYNAMIC_LINEAR_VELOCITY_THRESHOLD - velocity = { x: 0.0, y: 0.2, z:0.0 }; - Entities.editEntity(this.grabbedThingID, { velocity: velocity }); - } - } - }; - this.distanceHolding = function(deltaTime, timestamp) { if (!this.triggerClicked) { this.callEntityMethodOnGrabbed("releaseGrab"); this.ensureDynamic(); this.setState(STATE_OFF, "trigger released"); + if (this.getOtherHandController().state === STATE_DISTANCE_ROTATING) { + this.getOtherHandController().setState(STATE_SEARCHING, "trigger released on holding controller"); + // Can't set state of other controller to STATE_DISTANCE_HOLDING because then either: + // (a) The entity would jump to line up with the formerly rotating controller's orientation, or + // (b) The grab beam would need an orientation offset to the controller's true orientation. + // Neither of these options is good, so instead set STATE_SEARCHING and subsequently let the formerly distance + // rotating controller start distance holding the entity if it happens to be pointing at the entity. + } return; } @@ -2209,11 +2651,13 @@ function MyController(hand) { } this.maybeScale(grabbedProperties); + // visualizations - var rayPickInfo = this.calcRayPickInfo(this.hand); - - this.overlayLineOn(rayPickInfo.searchRay.origin, Vec3.subtract(grabbedProperties.position, this.offsetPosition), COLORS_GRAB_DISTANCE_HOLD); + this.overlayLineOn(rayPickInfo.searchRay.origin, + Vec3.subtract(grabbedProperties.position, this.offsetPosition), + COLORS_GRAB_DISTANCE_HOLD, + this.grabbedThingID); var distanceToObject = Vec3.length(Vec3.subtract(MyAvatar.position, this.currentObjectPosition)); var success = Entities.updateAction(this.grabbedThingID, this.actionID, { @@ -2232,6 +2676,64 @@ function MyController(hand) { this.previousRoomControllerPosition = roomControllerPosition; }; + this.distanceRotatingEnter = function() { + this.clearEquipHaptics(); + this.grabPointSphereOff(); + + var controllerLocation = getControllerWorldLocation(this.handToController(), true); + var worldControllerPosition = controllerLocation.position; + var worldControllerRotation = controllerLocation.orientation; + + var grabbedProperties = Entities.getEntityProperties(this.grabbedThingID, GRABBABLE_PROPERTIES); + this.currentObjectPosition = grabbedProperties.position; + this.grabRadius = this.grabbedDistance; + + // Offset between controller vector at the grab radius and the entity position. + var targetPosition = Vec3.multiply(this.grabRadius, Quat.getUp(worldControllerRotation)); + targetPosition = Vec3.sum(targetPosition, worldControllerPosition); + this.offsetPosition = Vec3.subtract(this.currentObjectPosition, targetPosition); + + // Initial controller rotation. + this.previousWorldControllerRotation = worldControllerRotation; + + Controller.triggerHapticPulse(HAPTIC_PULSE_STRENGTH, HAPTIC_PULSE_DURATION, this.hand); + this.turnOffVisualizations(); + }; + + this.distanceRotating = function(deltaTime, timestamp) { + + if (!this.triggerClicked) { + this.callEntityMethodOnGrabbed("releaseGrab"); + this.ensureDynamic(); + this.setState(STATE_OFF, "trigger released"); + return; + } + + var grabbedProperties = Entities.getEntityProperties(this.grabbedThingID, GRABBABLE_PROPERTIES); + + // Delta rotation of grabbing controller since last update. + var worldControllerRotation = getControllerWorldLocation(this.handToController(), true).orientation; + var controllerRotationDelta = Quat.multiply(worldControllerRotation, Quat.inverse(this.previousWorldControllerRotation)); + + // Rotate entity by twice the delta rotation. + controllerRotationDelta = Quat.multiply(controllerRotationDelta, controllerRotationDelta); + + // Perform the rotation in the translation controller's action update. + this.getOtherHandController().currentObjectRotation = Quat.multiply(controllerRotationDelta, + this.getOtherHandController().currentObjectRotation); + + // Rotate about the translation controller's target position. + this.offsetPosition = Vec3.multiplyQbyV(controllerRotationDelta, this.offsetPosition); + this.getOtherHandController().offsetPosition = Vec3.multiplyQbyV(controllerRotationDelta, + this.getOtherHandController().offsetPosition); + + var rayPickInfo = this.calcRayPickInfo(this.hand); + this.overlayLineOn(rayPickInfo.searchRay.origin, Vec3.subtract(grabbedProperties.position, this.offsetPosition), + COLORS_GRAB_DISTANCE_HOLD, this.grabbedThingID); + + this.previousWorldControllerRotation = worldControllerRotation; + } + this.setupHoldAction = function() { this.actionID = Entities.addAction("hold", this.grabbedThingID, { hand: this.hand === RIGHT_HAND ? "right" : "left", @@ -2314,6 +2816,7 @@ function MyController(hand) { this.lineOff(); this.overlayLineOff(); this.searchSphereOff(); + this.otherGrabbingLineOff(); this.dropGestureReset(); this.clearEquipHaptics(); @@ -2322,12 +2825,6 @@ function MyController(hand) { Controller.triggerHapticPulse(HAPTIC_PULSE_STRENGTH, HAPTIC_PULSE_DURATION, this.hand); - if (this.entityActivated) { - var saveGrabbedID = this.grabbedThingID; - this.release(); - this.grabbedThingID = saveGrabbedID; - } - var grabbedProperties; if (this.grabbedIsOverlay) { grabbedProperties = { @@ -2385,6 +2882,9 @@ function MyController(hand) { this.offsetPosition = Vec3.multiplyQbyV(Quat.inverse(Quat.multiply(handRotation, this.offsetRotation)), offset); } + // This boolean is used to check if the object that is grabbed has just been cloned + // It is only set true, if the object that is grabbed creates a new clone. + var isClone = false; var isPhysical = propsArePhysical(grabbedProperties) || (!this.grabbedIsOverlay && entityHasActions(this.grabbedThingID)); if (isPhysical && this.state == STATE_NEAR_GRABBING && grabbedProperties.parentID === NULL_UUID) { @@ -2402,9 +2902,7 @@ function MyController(hand) { this.actionID = null; var handJointIndex; if (this.ignoreIK) { - handJointIndex = MyAvatar.getJointIndex(this.hand === RIGHT_HAND ? - "_CONTROLLER_RIGHTHAND" : - "_CONTROLLER_LEFTHAND"); + handJointIndex = this.controllerJointIndex; } else { handJointIndex = MyAvatar.getJointIndex(this.hand === RIGHT_HAND ? "RightHand" : "LeftHand"); } @@ -2423,6 +2921,52 @@ function MyController(hand) { if (this.grabbedIsOverlay) { Overlays.editOverlay(this.grabbedThingID, reparentProps); } else { + if (grabbedProperties.userData.length > 0) { + try{ + var userData = JSON.parse(grabbedProperties.userData); + var grabInfo = userData.grabbableKey; + if (grabInfo && grabInfo.cloneable) { + var worldEntities = Entities.findEntities(MyAvatar.position, 50); + var count = 0; + worldEntities.forEach(function(item) { + var item = Entities.getEntityProperties(item, ["name"]); + if (item.name.indexOf('-clone-' + grabbedProperties.id) !== -1) { + count++; + } + }) + + var limit = grabInfo.cloneLimit ? grabInfo.cloneLimit : 0; + if (count >= limit && limit !== 0) { + delete limit; + return; + } + + var cloneableProps = Entities.getEntityProperties(grabbedProperties.id); + cloneableProps.name = cloneableProps.name + '-clone-' + grabbedProperties.id; + var lifetime = grabInfo.cloneLifetime ? grabInfo.cloneLifetime : 300; + var dynamic = grabInfo.cloneDynamic ? grabInfo.cloneDynamic : false; + var cUserData = Object.assign({}, userData); + var cProperties = Object.assign({}, cloneableProps); + isClone = true; + + delete cUserData.grabbableKey.cloneLifetime; + delete cUserData.grabbableKey.cloneable; + delete cUserData.grabbableKey.cloneDynamic; + delete cUserData.grabbableKey.cloneLimit; + delete cProperties.id + + cProperties.dynamic = dynamic; + cProperties.locked = false; + cUserData.grabbableKey.triggerable = true; + cUserData.grabbableKey.grabbable = true; + cProperties.lifetime = lifetime; + cProperties.userData = JSON.stringify(cUserData); + var cloneID = Entities.addEntity(cProperties); + this.grabbedThingID = cloneID; + grabbedProperties = Entities.getEntityProperties(cloneID); + } + }catch(e) {} + } Entities.editEntity(this.grabbedThingID, reparentProps); } @@ -2434,7 +2978,6 @@ function MyController(hand) { this.previousParentID[this.grabbedThingID] = grabbedProperties.parentID; this.previousParentJointIndex[this.grabbedThingID] = grabbedProperties.parentJointIndex; } - Messages.sendMessage('Hifi-Object-Manipulation', JSON.stringify({ action: 'equip', grabbedEntity: this.grabbedThingID, @@ -2450,22 +2993,40 @@ function MyController(hand) { }); } - if (this.state == STATE_NEAR_GRABBING) { - this.callEntityMethodOnGrabbed("startNearGrab"); - } else { // this.state == STATE_HOLD - this.callEntityMethodOnGrabbed("startEquip"); + var _this = this; + /* + * Setting context for function that is either called via timer or directly, depending if + * if the object in question is a clone. If it is a clone, we need to make sure that the intial equipment event + * is called correctly, as these just freshly created entity may not have completely initialized. + */ + var grabEquipCheck = function () { + if (_this.state == STATE_NEAR_GRABBING) { + _this.callEntityMethodOnGrabbed("startNearGrab"); + } else { // this.state == STATE_HOLD + _this.callEntityMethodOnGrabbed("startEquip"); + } + + // don't block teleport raypick with equipped entity + Messages.sendMessage('Hifi-Teleport-Ignore-Add', _this.grabbedThingID); + + _this.currentHandControllerTipPosition = + (_this.hand === RIGHT_HAND) ? MyAvatar.rightHandTipPosition : MyAvatar.leftHandTipPosition; + _this.currentObjectTime = Date.now(); + + _this.currentObjectPosition = grabbedProperties.position; + _this.currentObjectRotation = grabbedProperties.rotation; + _this.currentVelocity = ZERO_VEC; + _this.currentAngularVelocity = ZERO_VEC; + + _this.prevDropDetected = false; } - this.currentHandControllerTipPosition = - (this.hand === RIGHT_HAND) ? MyAvatar.rightHandTipPosition : MyAvatar.leftHandTipPosition; - this.currentObjectTime = Date.now(); - - this.currentObjectPosition = grabbedProperties.position; - this.currentObjectRotation = grabbedProperties.rotation; - this.currentVelocity = ZERO_VEC; - this.currentAngularVelocity = ZERO_VEC; - - this.prevDropDetected = false; + if (isClone) { + // 100 ms seems to be sufficient time to force the check even occur after the object has been initialized. + Script.setTimeout(grabEquipCheck, 100); + } else { + grabEquipCheck(); + } }; this.nearGrabbing = function(deltaTime, timestamp) { @@ -2781,10 +3342,10 @@ function MyController(hand) { this.release(); }; - this.entityTouchingEnter = function() { + this.entityLaserTouchingEnter = function() { // test for intersection between controller laser and web entity plane. - var intersectInfo = handLaserIntersectEntity(this.grabbedThingID, - getControllerWorldLocation(this.handToController(), true)); + var controllerLocation = getControllerWorldLocation(this.handToController(), true); + var intersectInfo = handLaserIntersectEntity(this.grabbedThingID, controllerLocation); if (intersectInfo) { var pointerEvent = { type: "Press", @@ -2806,22 +3367,16 @@ function MyController(hand) { this.deadspotExpired = false; var LASER_PRESS_TO_MOVE_DEADSPOT_ANGLE = 0.026; // radians ~ 1.2 degrees - var STYLUS_PRESS_TO_MOVE_DEADSPOT_ANGLE = 0.314; // radians ~ 18 degrees - var theta = this.state === STATE_ENTITY_STYLUS_TOUCHING ? STYLUS_PRESS_TO_MOVE_DEADSPOT_ANGLE : LASER_PRESS_TO_MOVE_DEADSPOT_ANGLE; - this.deadspotRadius = Math.tan(theta) * intersectInfo.distance; // dead spot radius in meters + this.deadspotRadius = Math.tan(LASER_PRESS_TO_MOVE_DEADSPOT_ANGLE) * intersectInfo.distance; // dead spot radius in meters } - if (this.state == STATE_ENTITY_STYLUS_TOUCHING) { - Controller.triggerHapticPulse(HAPTIC_STYLUS_STRENGTH, HAPTIC_STYLUS_DURATION, this.hand); - } else if (this.state == STATE_ENTITY_LASER_TOUCHING) { - Controller.triggerHapticPulse(HAPTIC_LASER_UI_STRENGTH, HAPTIC_LASER_UI_DURATION, this.hand); - } + Controller.triggerHapticPulse(HAPTIC_LASER_UI_STRENGTH, HAPTIC_LASER_UI_DURATION, this.hand); }; - this.entityTouchingExit = function() { + this.entityLaserTouchingExit = function() { // test for intersection between controller laser and web entity plane. - var intersectInfo = handLaserIntersectEntity(this.grabbedThingID, - getControllerWorldLocation(this.handToController(), true)); + var controllerLocation = getControllerWorldLocation(this.handToController(), true); + var intersectInfo = handLaserIntersectEntity(this.grabbedThingID, controllerLocation); if (intersectInfo) { var pointerEvent; if (this.deadspotExpired) { @@ -2849,28 +3404,22 @@ function MyController(hand) { this.grabbedOverlay = null; }; - this.entityTouching = function(dt) { + this.entityLaserTouching = function(dt) { this.touchingEnterTimer += dt; entityPropertiesCache.addEntity(this.grabbedThingID); - if (this.state == STATE_ENTITY_LASER_TOUCHING && !this.triggerSmoothedGrab()) { + if (this.state == STATE_ENTITY_LASER_TOUCHING && !this.triggerSmoothedGrab()) { // AJT: this.setState(STATE_OFF, "released trigger"); return; } // test for intersection between controller laser and web entity plane. - var intersectInfo = handLaserIntersectEntity(this.grabbedThingID, - getControllerWorldLocation(this.handToController(), true)); + var controllerLocation = getControllerWorldLocation(this.handToController(), true); + var intersectInfo = handLaserIntersectEntity(this.grabbedThingID, controllerLocation); if (intersectInfo) { - if (this.state == STATE_ENTITY_STYLUS_TOUCHING && - intersectInfo.distance > WEB_STYLUS_LENGTH / 2.0 + WEB_TOUCH_Y_OFFSET) { - this.setState(STATE_OFF, "pulled away from web entity"); - return; - } - if (Entities.keyboardFocusEntity != this.grabbedThingID) { Overlays.keyboardFocusOverlay = 0; Entities.keyboardFocusEntity = this.grabbedThingID; @@ -2907,14 +3456,14 @@ function MyController(hand) { } }; - this.overlayTouchingEnter = function () { + this.overlayLaserTouchingEnter = function () { // Test for intersection between controller laser and Web overlay plane. - var intersectInfo = - handLaserIntersectOverlay(this.grabbedOverlay, getControllerWorldLocation(this.handToController(), true)); + var controllerLocation = getControllerWorldLocation(this.handToController(), true); + var intersectInfo = handLaserIntersectOverlay(this.grabbedOverlay, controllerLocation); if (intersectInfo) { var pointerEvent = { type: "Press", - id: HARDWARE_MOUSE_ID, + id: this.hand + 1, pos2D: projectOntoOverlayXYPlane(this.grabbedOverlay, intersectInfo.point), pos3D: intersectInfo.point, normal: intersectInfo.normal, @@ -2931,22 +3480,16 @@ function MyController(hand) { this.deadspotExpired = false; var LASER_PRESS_TO_MOVE_DEADSPOT_ANGLE = 0.026; // radians ~ 1.2 degrees - var STYLUS_PRESS_TO_MOVE_DEADSPOT_ANGLE = 0.314; // radians ~ 18 degrees - var theta = this.state === STATE_OVERLAY_STYLUS_TOUCHING ? STYLUS_PRESS_TO_MOVE_DEADSPOT_ANGLE : LASER_PRESS_TO_MOVE_DEADSPOT_ANGLE; - this.deadspotRadius = Math.tan(theta) * intersectInfo.distance; // dead spot radius in meters + this.deadspotRadius = Math.tan(LASER_PRESS_TO_MOVE_DEADSPOT_ANGLE) * intersectInfo.distance; // dead spot radius in meters } - if (this.state == STATE_OVERLAY_STYLUS_TOUCHING) { - Controller.triggerHapticPulse(HAPTIC_STYLUS_STRENGTH, HAPTIC_STYLUS_DURATION, this.hand); - } else if (this.state == STATE_OVERLAY_LASER_TOUCHING) { - Controller.triggerHapticPulse(HAPTIC_LASER_UI_STRENGTH, HAPTIC_LASER_UI_DURATION, this.hand); - } + Controller.triggerHapticPulse(HAPTIC_LASER_UI_STRENGTH, HAPTIC_LASER_UI_DURATION, this.hand); }; - this.overlayTouchingExit = function () { + this.overlayLaserTouchingExit = function () { // Test for intersection between controller laser and Web overlay plane. - var intersectInfo = - handLaserIntersectOverlay(this.grabbedOverlay, getControllerWorldLocation(this.handToController(), true)); + var controllerLocation = getControllerWorldLocation(this.handToController(), true); + var intersectInfo = handLaserIntersectOverlay(this.grabbedOverlay, controllerLocation); if (intersectInfo) { var pointerEvent; @@ -2969,7 +3512,7 @@ function MyController(hand) { if (this.deadspotExpired) { pointerEvent = { type: "Release", - id: HARDWARE_MOUSE_ID, + id: this.hand + 1, pos2D: pos2D, pos3D: pos3D, normal: intersectInfo.normal, @@ -2990,53 +3533,22 @@ function MyController(hand) { this.grabbedOverlay = null; }; - this.overlayTouching = function (dt) { + this.overlayLaserTouching = function (dt) { this.touchingEnterTimer += dt; - if (this.state == STATE_OVERLAY_STYLUS_TOUCHING && this.triggerSmoothedSqueezed()) { - return; - } - if (this.state == STATE_OVERLAY_LASER_TOUCHING && !this.triggerSmoothedGrab()) { this.setState(STATE_OFF, "released trigger"); return; } // Test for intersection between controller laser and Web overlay plane. - var intersectInfo = - handLaserIntersectOverlay(this.grabbedOverlay, getControllerWorldLocation(this.handToController(), true)); + var controllerLocation = getControllerWorldLocation(this.handToController(), true); + var intersectInfo = handLaserIntersectOverlay(this.grabbedOverlay, controllerLocation); if (intersectInfo) { - if (this.state == STATE_OVERLAY_STYLUS_TOUCHING && - intersectInfo.distance > WEB_STYLUS_LENGTH / 2.0 + WEB_TOUCH_Y_OFFSET + WEB_TOUCH_Y_TOUCH_DEADZONE_SIZE) { - this.grabbedThingID = null; - this.setState(STATE_OFF, "pulled away from overlay"); - return; - } - var pos2D = projectOntoOverlayXYPlane(this.grabbedOverlay, intersectInfo.point); var pos3D = intersectInfo.point; - if (this.state == STATE_OVERLAY_STYLUS_TOUCHING && - !this.tabletStabbed && - intersectInfo.distance < WEB_STYLUS_LENGTH / 2.0 + WEB_TOUCH_TOO_CLOSE) { - // they've stabbed the tablet, don't send events until they pull back - this.tabletStabbed = true; - this.tabletStabbedPos2D = pos2D; - this.tabletStabbedPos3D = pos3D; - return; - } - - if (this.tabletStabbed) { - var origin = {x: this.tabletStabbedPos2D.x, y: this.tabletStabbedPos2D.y, z: 0}; - var point = {x: pos2D.x, y: pos2D.y, z: 0}; - var offset = Vec3.distance(origin, point); - var radius = 0.05; - if (offset < radius) { - return; - } - } - if (Overlays.keyboardFocusOverlay != this.grabbedOverlay) { Entities.keyboardFocusEntity = null; Overlays.keyboardFocusOverlay = this.grabbedOverlay; @@ -3044,7 +3556,7 @@ function MyController(hand) { var pointerEvent = { type: "Move", - id: HARDWARE_MOUSE_ID, + id: this.hand + 1, pos2D: pos2D, pos3D: pos3D, normal: intersectInfo.normal, @@ -3072,10 +3584,75 @@ function MyController(hand) { } }; + this.stylusTouchingEnter = function () { + this.stealTouchFocus(this.stylusTarget); + sendTouchStartEventToStylusTarget(this.hand, this.stylusTarget); + Controller.triggerHapticPulse(HAPTIC_STYLUS_STRENGTH, HAPTIC_STYLUS_DURATION, this.hand); + + this.touchingEnterTimer = 0; + this.touchingEnterStylusTarget = this.stylusTarget; + this.deadspotExpired = false; + + var TOUCH_PRESS_TO_MOVE_DEADSPOT = 0.0381; + this.deadspotRadius = TOUCH_PRESS_TO_MOVE_DEADSPOT; + }; + + this.stylusTouchingExit = function () { + + if (this.stylusTarget === undefined) { + return; + } + + // special case to handle home button. + if (this.stylusTarget.overlayID === HMD.homeButtonID) { + Messages.sendLocalMessage("home", this.stylusTarget.overlayID); + } + + // send press event + if (this.deadspotExpired) { + sendTouchEndEventToStylusTarget(this.hand, this.stylusTarget); + } else { + sendTouchEndEventToStylusTarget(this.hand, this.touchingEnterStylusTarget); + } + }; + + this.stylusTouching = function (dt) { + + this.touchingEnterTimer += dt; + + if (this.stylusTarget.entityID) { + entityPropertiesCache.addEntity(this.stylusTarget.entityID); + this.stylusTarget = calculateStylusTargetFromEntity(this.stylusTip, this.stylusTarget.entityID); + } else if (this.stylusTarget.overlayID) { + this.stylusTarget = calculateStylusTargetFromOverlay(this.stylusTip, this.stylusTarget.overlayID); + } + + var TABLET_MIN_TOUCH_DISTANCE = -0.1; + var TABLET_MAX_TOUCH_DISTANCE = 0.01; + + if (this.stylusTarget) { + if (this.stylusTarget.distance > TABLET_MIN_TOUCH_DISTANCE && this.stylusTarget.distance < TABLET_MAX_TOUCH_DISTANCE) { + var POINTER_PRESS_TO_MOVE_DELAY = 0.33; // seconds + if (this.deadspotExpired || this.touchingEnterTimer > POINTER_PRESS_TO_MOVE_DELAY || + distance2D(this.stylusTarget.position2D, this.touchingEnterStylusTarget.position2D) > this.deadspotRadius) { + sendTouchMoveEventToStylusTarget(this.hand, this.stylusTarget); + this.deadspotExpired = true; + } + } else { + this.setState(STATE_OFF, "hand moved away from touch surface"); + } + } else { + this.setState(STATE_OFF, "touch surface was destroyed"); + } + }; + this.release = function() { this.turnOffVisualizations(); if (this.grabbedThingID !== null) { + + Messages.sendMessage('Hifi-Teleport-Ignore-Remove', this.grabbedThingID); + if (this.state === STATE_HOLD) { this.callEntityMethodOnGrabbed("releaseEquip"); } @@ -3137,6 +3714,8 @@ function MyController(hand) { this.cleanup = function() { this.release(); this.grabPointSphereOff(); + this.hideStylus(); + this.overlayLineOff(); }; this.thisHandIsParent = function(props) { @@ -3149,9 +3728,7 @@ function MyController(hand) { return true; } - var controllerJointIndex = MyAvatar.getJointIndex(this.hand === RIGHT_HAND ? - "_CONTROLLER_RIGHTHAND" : - "_CONTROLLER_LEFTHAND"); + var controllerJointIndex = this.controllerJointIndex; if (props.parentJointIndex == controllerJointIndex) { return true; } @@ -3177,9 +3754,7 @@ function MyController(hand) { children = children.concat(Entities.getChildrenIDsOfJoint(AVATAR_SELF_ID, handJointIndex)); // find children of faux controller joint - var controllerJointIndex = MyAvatar.getJointIndex(this.hand === RIGHT_HAND ? - "_CONTROLLER_RIGHTHAND" : - "_CONTROLLER_LEFTHAND"); + var controllerJointIndex = this.controllerJointIndex; children = children.concat(Entities.getChildrenIDsOfJoint(MyAvatar.sessionUUID, controllerJointIndex)); children = children.concat(Entities.getChildrenIDsOfJoint(AVATAR_SELF_ID, controllerJointIndex)); @@ -3191,11 +3766,11 @@ function MyController(hand) { children = children.concat(Entities.getChildrenIDsOfJoint(AVATAR_SELF_ID, controllerCRJointIndex)); children.forEach(function(childID) { - if (childID !== _this.stylus) { + if (childID !== _this.stylus && + childID !== _this.overlayLine) { // we appear to be holding something and this script isn't in a state that would be holding something. // unhook it. if we previously took note of this entity's parent, put it back where it was. This // works around some problems that happen when more than one hand or avatar is passing something around. - print("disconnecting stray child of hand: (" + _this.hand + ") " + childID); if (_this.previousParentID[childID]) { var previousParentID = _this.previousParentID[childID]; var previousParentJointIndex = _this.previousParentJointIndex[childID]; @@ -3213,13 +3788,21 @@ function MyController(hand) { } _this.previouslyUnhooked[childID] = now; - // we don't know if it's an entity or an overlay + if (Overlays.getProperty(childID, "grabbable")) { + // only auto-unhook overlays that were flagged as grabbable. this avoids unhooking overlays + // used in tutorial. + Overlays.editOverlay(childID, { + parentID: previousParentID, + parentJointIndex: previousParentJointIndex + }); + } Entities.editEntity(childID, { parentID: previousParentID, parentJointIndex: previousParentJointIndex }); - Overlays.editOverlay(childID, { parentID: previousParentID, parentJointIndex: previousParentJointIndex }); } else { Entities.editEntity(childID, { parentID: NULL_UUID }); - Overlays.editOverlay(childID, { parentID: NULL_UUID }); + if (Overlays.getProperty(childID, "grabbable")) { + Overlays.editOverlay(childID, { parentID: NULL_UUID }); + } } } }); diff --git a/scripts/system/controllers/handControllerPointer.js b/scripts/system/controllers/handControllerPointer.js index f8a336a017..eb94428100 100644 --- a/scripts/system/controllers/handControllerPointer.js +++ b/scripts/system/controllers/handControllerPointer.js @@ -174,7 +174,7 @@ function calculateRayUICollisionPoint(position, direction) { // interect HUD plane, 1m in front of camera, using formula: // scale = hudNormal dot (hudPoint - position) / hudNormal dot direction // intersection = postion + scale*direction - var hudNormal = Quat.getFront(Camera.getOrientation()); + var hudNormal = Quat.getForward(Camera.getOrientation()); var hudPoint = Vec3.sum(Camera.getPosition(), hudNormal); // must also scale if PLANAR_PERPENDICULAR_HUD_DISTANCE!=1 var denominator = Vec3.dot(hudNormal, direction); if (denominator === 0) { diff --git a/scripts/system/controllers/squeezeHands.js b/scripts/system/controllers/squeezeHands.js index 75e6249dd6..c9de473e28 100644 --- a/scripts/system/controllers/squeezeHands.js +++ b/scripts/system/controllers/squeezeHands.js @@ -11,6 +11,7 @@ // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // +/* eslint indent: ["error", 4, { "outerIIFEBody": 0 }] */ (function() { // BEGIN LOCAL_SCOPE @@ -25,7 +26,11 @@ var OVERLAY_RAMP_RATE = 8.0; var animStateHandlerID; -var isBothIndexesPointing = false; +var leftIndexPointingOverride = 0; +var rightIndexPointingOverride = 0; +var leftThumbRaisedOverride = 0; +var rightThumbRaisedOverride = 0; + var HIFI_POINT_INDEX_MESSAGE_CHANNEL = "Hifi-Point-Index"; var isLeftIndexPointing = false; @@ -53,7 +58,7 @@ function init() { "leftHandOverlayAlpha", "leftHandGraspAlpha", "rightHandOverlayAlpha", "rightHandGraspAlpha", "isLeftHandGrasp", "isLeftIndexPoint", "isLeftThumbRaise", "isLeftIndexPointAndThumbRaise", - "isRightHandGrasp", "isRightIndexPoint", "isRightThumbRaise", "isRightIndexPointAndThumbRaise", + "isRightHandGrasp", "isRightIndexPoint", "isRightThumbRaise", "isRightIndexPointAndThumbRaise" ] ); Messages.subscribe(HIFI_POINT_INDEX_MESSAGE_CHANNEL); @@ -66,21 +71,23 @@ function animStateHandler(props) { leftHandGraspAlpha: lastLeftTrigger, rightHandOverlayAlpha: rightHandOverlayAlpha, rightHandGraspAlpha: lastRightTrigger, - isLeftHandGrasp: !isBothIndexesPointing && !isLeftIndexPointing && !isLeftThumbRaised, - isLeftIndexPoint: (isBothIndexesPointing || isLeftIndexPointing) && !isLeftThumbRaised, - isLeftThumbRaise: !isBothIndexesPointing && !isLeftIndexPointing && isLeftThumbRaised, - isLeftIndexPointAndThumbRaise: (isBothIndexesPointing || isLeftIndexPointing) && isLeftThumbRaised, - isRightHandGrasp: !isBothIndexesPointing && !isRightIndexPointing && !isRightThumbRaised, - isRightIndexPoint: (isBothIndexesPointing || isRightIndexPointing) && !isRightThumbRaised, - isRightThumbRaise: !isBothIndexesPointing && !isRightIndexPointing && isRightThumbRaised, - isRightIndexPointAndThumbRaise: (isBothIndexesPointing || isRightIndexPointing) && isRightThumbRaised + + isLeftHandGrasp: !isLeftIndexPointing && !isLeftThumbRaised, + isLeftIndexPoint: isLeftIndexPointing && !isLeftThumbRaised, + isLeftThumbRaise: !isLeftIndexPointing && isLeftThumbRaised, + isLeftIndexPointAndThumbRaise: isLeftIndexPointing && isLeftThumbRaised, + + isRightHandGrasp: !isRightIndexPointing && !isRightThumbRaised, + isRightIndexPoint: isRightIndexPointing && !isRightThumbRaised, + isRightThumbRaise: !isRightIndexPointing && isRightThumbRaised, + isRightIndexPointAndThumbRaise: isRightIndexPointing && isRightThumbRaised }; } function update(dt) { var leftTrigger = clamp(Controller.getValue(Controller.Standard.LT) + Controller.getValue(Controller.Standard.LeftGrip), 0, 1); var rightTrigger = clamp(Controller.getValue(Controller.Standard.RT) + Controller.getValue(Controller.Standard.RightGrip), 0, 1); - + // Average last few trigger values together for a bit of smoothing var tau = clamp(dt / TRIGGER_SMOOTH_TIMESCALE, 0, 1); lastLeftTrigger = lerp(leftTrigger, lastLeftTrigger, tau); @@ -103,18 +110,61 @@ function update(dt) { } // Pointing index fingers and raising thumbs - isLeftIndexPointing = leftHandPose.valid && Controller.getValue(Controller.Standard.LeftIndexPoint) === 1; - isRightIndexPointing = rightHandPose.valid && Controller.getValue(Controller.Standard.RightIndexPoint) === 1; - isLeftThumbRaised = leftHandPose.valid && Controller.getValue(Controller.Standard.LeftThumbUp) === 1; - isRightThumbRaised = rightHandPose.valid && Controller.getValue(Controller.Standard.RightThumbUp) === 1; + isLeftIndexPointing = (leftIndexPointingOverride > 0) || (leftHandPose.valid && Controller.getValue(Controller.Standard.LeftIndexPoint) === 1); + isRightIndexPointing = (rightIndexPointingOverride > 0) || (rightHandPose.valid && Controller.getValue(Controller.Standard.RightIndexPoint) === 1); + isLeftThumbRaised = (leftThumbRaisedOverride > 0) || (leftHandPose.valid && Controller.getValue(Controller.Standard.LeftThumbUp) === 1); + isRightThumbRaised = (rightThumbRaisedOverride > 0) || (rightHandPose.valid && Controller.getValue(Controller.Standard.RightThumbUp) === 1); } function handleMessages(channel, message, sender) { if (sender === MyAvatar.sessionUUID && channel === HIFI_POINT_INDEX_MESSAGE_CHANNEL) { var data = JSON.parse(message); + if (data.pointIndex !== undefined) { - print("pointIndex: " + data.pointIndex); - isBothIndexesPointing = data.pointIndex; + if (data.pointIndex) { + leftIndexPointingOverride++; + rightIndexPointingOverride++; + } else { + leftIndexPointingOverride--; + rightIndexPointingOverride--; + } + } + if (data.pointLeftIndex !== undefined) { + if (data.pointLeftIndex) { + leftIndexPointingOverride++; + } else { + leftIndexPointingOverride--; + } + } + if (data.pointRightIndex !== undefined) { + if (data.pointRightIndex) { + rightIndexPointingOverride++; + } else { + rightIndexPointingOverride--; + } + } + if (data.raiseThumbs !== undefined) { + if (data.raiseThumbs) { + leftThumbRaisedOverride++; + rightThumbRaisedOverride++; + } else { + leftThumbRaisedOverride--; + rightThumbRaisedOverride--; + } + } + if (data.raiseLeftThumb !== undefined) { + if (data.raiseLeftThumb) { + leftThumbRaisedOverride++; + } else { + leftThumbRaisedOverride--; + } + } + if (data.raiseRightThumb !== undefined) { + if (data.raiseRightThumb) { + rightThumbRaisedOverride++; + } else { + rightThumbRaisedOverride--; + } } } } diff --git a/scripts/system/controllers/teleport.js b/scripts/system/controllers/teleport.js index c058f046db..33c0b3116e 100644 --- a/scripts/system/controllers/teleport.js +++ b/scripts/system/controllers/teleport.js @@ -85,6 +85,7 @@ function Trigger(hand) { } var coolInTimeout = null; +var ignoredEntities = []; var TELEPORTER_STATES = { IDLE: 'idle', @@ -239,11 +240,11 @@ function Teleporter() { // We might hit an invisible entity that is not a seat, so we need to do a second pass. // * In the second pass we pick against visible entities only. // - var intersection = Entities.findRayIntersection(pickRay, true, [], [this.targetEntity], false, true); + var intersection = Entities.findRayIntersection(pickRay, true, [], [this.targetEntity].concat(ignoredEntities), false, true); var teleportLocationType = getTeleportTargetType(intersection); if (teleportLocationType === TARGET.INVISIBLE) { - intersection = Entities.findRayIntersection(pickRay, true, [], [this.targetEntity], true, true); + intersection = Entities.findRayIntersection(pickRay, true, [], [this.targetEntity].concat(ignoredEntities), true, true); teleportLocationType = getTeleportTargetType(intersection); } @@ -296,8 +297,9 @@ function Teleporter() { } else if (teleportLocationType === TARGET.SURFACE) { var offset = getAvatarFootOffset(); intersection.intersection.y += offset; - MyAvatar.position = intersection.intersection; + MyAvatar.goToLocation(intersection.intersection, false, {x: 0, y: 0, z: 0, w: 1}, false); HMD.centerUI(); + MyAvatar.centerBody(); } } }; @@ -440,7 +442,12 @@ function getTeleportTargetType(intersection) { var props = Entities.getEntityProperties(intersection.entityID, ['userData', 'visible']); var data = parseJSON(props.userData); if (data !== undefined && data.seat !== undefined) { - return TARGET.SEAT; + var avatarUuid = Uuid.fromString(data.seat.user); + if (Uuid.isNull(avatarUuid) || !AvatarList.getAvatar(avatarUuid)) { + return TARGET.SEAT; + } else { + return TARGET.INVALID; + } } if (!props.visible) { @@ -513,7 +520,7 @@ function cleanup() { Script.scriptEnding.connect(cleanup); var isDisabled = false; -var handleHandMessages = function(channel, message, sender) { +var handleTeleportMessages = function(channel, message, sender) { var data; if (sender === MyAvatar.sessionUUID) { if (channel === 'Hifi-Teleport-Disabler') { @@ -529,12 +536,20 @@ var handleHandMessages = function(channel, message, sender) { if (message === 'none') { isDisabled = false; } - + } else if (channel === 'Hifi-Teleport-Ignore-Add' && !Uuid.isNull(message) && ignoredEntities.indexOf(message) === -1) { + ignoredEntities.push(message); + } else if (channel === 'Hifi-Teleport-Ignore-Remove' && !Uuid.isNull(message)) { + var removeIndex = ignoredEntities.indexOf(message); + if (removeIndex > -1) { + ignoredEntities.splice(removeIndex, 1); + } } } } Messages.subscribe('Hifi-Teleport-Disabler'); -Messages.messageReceived.connect(handleHandMessages); +Messages.subscribe('Hifi-Teleport-Ignore-Add'); +Messages.subscribe('Hifi-Teleport-Ignore-Remove'); +Messages.messageReceived.connect(handleTeleportMessages); }()); // END LOCAL_SCOPE diff --git a/scripts/system/controllers/toggleAdvancedMovementForHandControllers.js b/scripts/system/controllers/toggleAdvancedMovementForHandControllers.js index 46464dc2e1..e6c9b0aee0 100644 --- a/scripts/system/controllers/toggleAdvancedMovementForHandControllers.js +++ b/scripts/system/controllers/toggleAdvancedMovementForHandControllers.js @@ -17,15 +17,14 @@ var mappingName, basicMapping, isChecked; var TURN_RATE = 1000; var MENU_ITEM_NAME = "Advanced Movement For Hand Controllers"; -var SETTINGS_KEY = 'advancedMovementForHandControllersIsChecked'; var isDisabled = false; -var previousSetting = Settings.getValue(SETTINGS_KEY); -if (previousSetting === '' || previousSetting === false || previousSetting === 'false') { +var previousSetting = MyAvatar.useAdvancedMovementControls; +if (previousSetting === false) { previousSetting = false; isChecked = false; } -if (previousSetting === true || previousSetting === 'true') { +if (previousSetting === true) { previousSetting = true; isChecked = true; } @@ -37,7 +36,6 @@ function addAdvancedMovementItemToSettingsMenu() { isCheckable: true, isChecked: previousSetting }); - } function rotate180() { @@ -72,7 +70,6 @@ function registerBasicMapping() { } return; }); - basicMapping.from(Controller.Standard.LX).to(Controller.Standard.RX); basicMapping.from(Controller.Standard.RY).to(function(value) { if (isDisabled) { return; @@ -112,10 +109,10 @@ function menuItemEvent(menuItem) { if (menuItem == MENU_ITEM_NAME) { isChecked = Menu.isOptionChecked(MENU_ITEM_NAME); if (isChecked === true) { - Settings.setValue(SETTINGS_KEY, true); + MyAvatar.useAdvancedMovementControls = true; disableMappings(); } else if (isChecked === false) { - Settings.setValue(SETTINGS_KEY, false); + MyAvatar.useAdvancedMovementControls = false; enableMappings(); } } diff --git a/scripts/system/edit.js b/scripts/system/edit.js index ad3af3a659..9988df425d 100644 --- a/scripts/system/edit.js +++ b/scripts/system/edit.js @@ -1,7 +1,6 @@ "use strict"; -// newEditEntities.js -// examples +// edit.js // // Created by Brad Hefta-Gaub on 10/2/14. // Persist toolbar by HRS 6/11/15. @@ -13,6 +12,8 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // +/* global Script, SelectionDisplay, LightOverlayManager, CameraManager, Grid, GridTool, EntityListTool, Vec3, SelectionManager, Overlays, OverlayWebWindow, UserActivityLogger, Settings, Entities, Tablet, Toolbars, Messages, Menu, Camera, progressDialog, tooltip, MyAvatar, Quat, Controller, Clipboard, HMD, UndoStack, ParticleExplorerTool */ + (function() { // BEGIN LOCAL_SCOPE var HIFI_PUBLIC_BUCKET = "http://s3.amazonaws.com/hifi-public/"; @@ -33,13 +34,27 @@ Script.include([ "libraries/gridTool.js", "libraries/entityList.js", "particle_explorer/particleExplorerTool.js", - "libraries/lightOverlayManager.js" + "libraries/entityIconOverlayManager.js" ]); var selectionDisplay = SelectionDisplay; var selectionManager = SelectionManager; -var lightOverlayManager = new LightOverlayManager(); +const PARTICLE_SYSTEM_URL = Script.resolvePath("assets/images/icon-particles.svg"); +const POINT_LIGHT_URL = Script.resolvePath("assets/images/icon-point-light.svg"); +const SPOT_LIGHT_URL = Script.resolvePath("assets/images/icon-spot-light.svg"); +entityIconOverlayManager = new EntityIconOverlayManager(['Light', 'ParticleEffect'], function(entityID) { + var properties = Entities.getEntityProperties(entityID, ['type', 'isSpotlight']); + if (properties.type === 'Light') { + return { + url: properties.isSpotlight ? SPOT_LIGHT_URL : POINT_LIGHT_URL, + } + } else { + return { + url: PARTICLE_SYSTEM_URL, + } + } +}); var cameraManager = new CameraManager(); @@ -53,23 +68,57 @@ var entityListTool = new EntityListTool(); selectionManager.addEventListener(function () { selectionDisplay.updateHandles(); - lightOverlayManager.updatePositions(); + entityIconOverlayManager.updatePositions(); + + // Update particle explorer + var needToDestroyParticleExplorer = false; + if (selectionManager.selections.length === 1) { + var selectedEntityID = selectionManager.selections[0]; + if (selectedEntityID === selectedParticleEntityID) { + return; + } + var type = Entities.getEntityProperties(selectedEntityID, "type").type; + if (type === "ParticleEffect") { + // Destroy the old particles web view first + particleExplorerTool.destroyWebView(); + particleExplorerTool.createWebView(); + var properties = Entities.getEntityProperties(selectedEntityID); + var particleData = { + messageType: "particle_settings", + currentProperties: properties + }; + selectedParticleEntityID = selectedEntityID; + particleExplorerTool.setActiveParticleEntity(selectedParticleEntityID); + + particleExplorerTool.webView.webEventReceived.connect(function (data) { + data = JSON.parse(data); + if (data.messageType === "page_loaded") { + particleExplorerTool.webView.emitScriptEvent(JSON.stringify(particleData)); + } + }); + } else { + needToDestroyParticleExplorer = true; + } + } else { + needToDestroyParticleExplorer = true; + } + + if (needToDestroyParticleExplorer && selectedParticleEntityID !== null) { + selectedParticleEntityID = null; + particleExplorerTool.destroyWebView(); + } }); +const KEY_P = 80; //Key code for letter p used for Parenting hotkey. var DEGREES_TO_RADIANS = Math.PI / 180.0; var RADIANS_TO_DEGREES = 180.0 / Math.PI; -var epsilon = 0.001; var MIN_ANGULAR_SIZE = 2; var MAX_ANGULAR_SIZE = 45; var allowLargeModels = true; var allowSmallModels = true; -var SPAWN_DISTANCE = 1; var DEFAULT_DIMENSION = 0.20; -var DEFAULT_TEXT_DIMENSION_X = 1.0; -var DEFAULT_TEXT_DIMENSION_Y = 1.0; -var DEFAULT_TEXT_DIMENSION_Z = 0.01; var DEFAULT_DIMENSIONS = { x: DEFAULT_DIMENSION, @@ -81,13 +130,12 @@ var DEFAULT_LIGHT_DIMENSIONS = Vec3.multiply(20, DEFAULT_DIMENSIONS); var MENU_AUTO_FOCUS_ON_SELECT = "Auto Focus on Select"; var MENU_EASE_ON_FOCUS = "Ease Orientation on Focus"; -var MENU_SHOW_LIGHTS_IN_EDIT_MODE = "Show Lights in Edit Mode"; +var MENU_SHOW_LIGHTS_AND_PARTICLES_IN_EDIT_MODE = "Show Lights and Particle Systems in Edit Mode"; var MENU_SHOW_ZONES_IN_EDIT_MODE = "Show Zones in Edit Mode"; -var SETTING_INSPECT_TOOL_ENABLED = "inspectToolEnabled"; var SETTING_AUTO_FOCUS_ON_SELECT = "autoFocusOnSelect"; var SETTING_EASE_ON_FOCUS = "cameraEaseOnFocus"; -var SETTING_SHOW_LIGHTS_IN_EDIT_MODE = "showLightsInEditMode"; +var SETTING_SHOW_LIGHTS_AND_PARTICLES_IN_EDIT_MODE = "showLightsAndParticlesInEditMode"; var SETTING_SHOW_ZONES_IN_EDIT_MODE = "showZonesInEditMode"; @@ -156,13 +204,13 @@ function hideMarketplace() { marketplaceWindow.setURL("about:blank"); } -function toggleMarketplace() { - if (marketplaceWindow.visible) { - hideMarketplace(); - } else { - showMarketplace(); - } -} +// function toggleMarketplace() { +// if (marketplaceWindow.visible) { +// hideMarketplace(); +// } else { +// showMarketplace(); +// } +// } var TOOLS_PATH = Script.resolvePath("assets/images/tools/"); @@ -175,8 +223,6 @@ var toolBar = (function () { tablet = null; function createNewEntity(properties) { - Settings.setValue(EDIT_SETTING, false); - var dimensions = properties.dimensions ? properties.dimensions : DEFAULT_DIMENSIONS; var position = getPositionToCreateEntity(); var entityID = null; @@ -184,8 +230,12 @@ var toolBar = (function () { position = grid.snapToSurface(grid.snapToGrid(position, false, dimensions), dimensions), properties.position = position; entityID = Entities.addEntity(properties); + if (properties.type == "ParticleEffect") { + selectParticleEntity(entityID); + } } else { - Window.notifyEditError("Can't create " + properties.type + ": " + properties.type + " would be out of bounds."); + Window.notifyEditError("Can't create " + properties.type + ": " + + properties.type + " would be out of bounds."); } selectionManager.clearSelections(); @@ -205,24 +255,63 @@ var toolBar = (function () { } } + var buttonHandlers = {}; // only used to tablet mode + function addButton(name, image, handler) { - var imageUrl = TOOLS_PATH + image; - var button = toolBar.addButton({ - objectName: name, - imageURL: imageUrl, - imageOffOut: 1, - imageOffIn: 2, - imageOnOut: 0, - imageOnIn: 2, - alpha: 0.9, - visible: true - }); - if (handler) { - button.clicked.connect(function () { - Script.setTimeout(handler, 100); - }); + buttonHandlers[name] = handler; + } + + var SHAPE_TYPE_NONE = 0; + var SHAPE_TYPE_SIMPLE_HULL = 1; + var SHAPE_TYPE_SIMPLE_COMPOUND = 2; + var SHAPE_TYPE_STATIC_MESH = 3; + var DYNAMIC_DEFAULT = false; + + function handleNewModelDialogResult(result) { + if (result) { + var url = result.textInput; + var shapeType; + switch (result.comboBox) { + case SHAPE_TYPE_SIMPLE_HULL: + shapeType = "simple-hull"; + break; + case SHAPE_TYPE_SIMPLE_COMPOUND: + shapeType = "simple-compound"; + break; + case SHAPE_TYPE_STATIC_MESH: + shapeType = "static-mesh"; + break; + default: + shapeType = "none"; + } + + var dynamic = result.checkBox !== null ? result.checkBox : DYNAMIC_DEFAULT; + if (shapeType === "static-mesh" && dynamic) { + // The prompt should prevent this case + print("Error: model cannot be both static mesh and dynamic. This should never happen."); + } else if (url) { + createNewEntity({ + type: "Model", + modelURL: url, + shapeType: shapeType, + dynamic: dynamic, + gravity: dynamic ? { x: 0, y: -10, z: 0 } : { x: 0, y: 0, z: 0 } + }); + } + } + } + + function fromQml(message) { // messages are {method, params}, like json-rpc. See also sendToQml. + var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); + tablet.popFromStack(); + switch (message.method) { + case "newModelDialogAdd": + handleNewModelDialogResult(message.params); + break; + case "newEntityButtonClicked": + buttonHandlers[message.params.buttonName](); + break; } - return button; } function initialize() { @@ -239,101 +328,54 @@ var toolBar = (function () { } }); - - if (Settings.getValue("HUDUIEnabled")) { - systemToolbar = Toolbars.getToolbar(SYSTEM_TOOLBAR); - activeButton = systemToolbar.addButton({ - objectName: EDIT_TOGGLE_BUTTON, - imageURL: TOOLS_PATH + "edit.svg", - visible: true, - alpha: 0.9, - defaultState: 1 - }); - } else { - tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); - activeButton = tablet.addButton({ - icon: "icons/tablet-icons/edit-i.svg", - activeIcon: "icons/tablet-icons/edit-a.svg", - text: "EDIT", - sortOrder: 10 - }); - } + tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); + activeButton = tablet.addButton({ + icon: "icons/tablet-icons/edit-i.svg", + activeIcon: "icons/tablet-icons/edit-a.svg", + text: "EDIT", + sortOrder: 10 + }); + tablet.screenChanged.connect(function (type, url) { + if (isActive && (type !== "QML" || url !== "Edit.qml")) { + that.toggle(); + } + }); + tablet.fromQml.connect(fromQml); activeButton.clicked.connect(function() { that.toggle(); }); - toolBar = Toolbars.getToolbar(EDIT_TOOLBAR); - toolBar.writeProperty("shown", false); - addButton("openAssetBrowserButton","assets-01.svg",function(){ + addButton("importEntitiesButton", "assets-01.svg", function() { + var importURL = null; + var fullPath = Window.browse("Select Model to Import", "", "*.json"); + if (fullPath) { + importURL = "file:///" + fullPath; + } + if (importURL) { + if (!isActive && (Entities.canRez() && Entities.canRezTmp())) { + toolBar.toggle(); + } + importSVO(importURL); + } + }); + + addButton("openAssetBrowserButton", "assets-01.svg", function() { Window.showAssetServer(); - }) + }); addButton("newModelButton", "model-01.svg", function () { - var SHAPE_TYPE_NONE = 0; - var SHAPE_TYPE_SIMPLE_HULL = 1; - var SHAPE_TYPE_SIMPLE_COMPOUND = 2; - var SHAPE_TYPE_STATIC_MESH = 3; var SHAPE_TYPES = []; SHAPE_TYPES[SHAPE_TYPE_NONE] = "No Collision"; SHAPE_TYPES[SHAPE_TYPE_SIMPLE_HULL] = "Basic - Whole model"; SHAPE_TYPES[SHAPE_TYPE_SIMPLE_COMPOUND] = "Good - Sub-meshes"; SHAPE_TYPES[SHAPE_TYPE_STATIC_MESH] = "Exact - All polygons"; - var SHAPE_TYPE_DEFAULT = SHAPE_TYPE_STATIC_MESH; - var DYNAMIC_DEFAULT = false; - var result = Window.customPrompt({ - textInput: { - label: "Model URL" - }, - comboBox: { - label: "Automatic Collisions", - index: SHAPE_TYPE_DEFAULT, - items: SHAPE_TYPES - }, - checkBox: { - label: "Dynamic", - checked: DYNAMIC_DEFAULT, - disableForItems: [ - SHAPE_TYPE_STATIC_MESH - ], - checkStateOnDisable: false, - warningOnDisable: "Models with automatic collisions set to 'Exact' cannot be dynamic" - } - }); - if (result) { - var url = result.textInput; - var shapeType; - switch (result.comboBox) { - case SHAPE_TYPE_SIMPLE_HULL: - shapeType = "simple-hull"; - break; - case SHAPE_TYPE_SIMPLE_COMPOUND: - shapeType = "simple-compound"; - break; - case SHAPE_TYPE_STATIC_MESH: - shapeType = "static-mesh"; - break; - default: - shapeType = "none"; - } - - var dynamic = result.checkBox !== null ? result.checkBox : DYNAMIC_DEFAULT; - if (shapeType === "static-mesh" && dynamic) { - // The prompt should prevent this case - print("Error: model cannot be both static mesh and dynamic. This should never happen."); - } else if (url) { - createNewEntity({ - type: "Model", - modelURL: url, - shapeType: shapeType, - dynamic: dynamic, - gravity: dynamic ? { x: 0, y: -10, z: 0 } : { x: 0, y: 0, z: 0 } - }); - } - } + // tablet version of new-model dialog + var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); + tablet.pushOntoStack("NewModelDialog.qml"); }); addButton("newCubeButton", "cube-01.svg", function () { @@ -455,10 +497,12 @@ var toolBar = (function () { entityListTool.clearEntityList(); }; - that.toggle = function () { that.setActive(!isActive); activeButton.editProperties({isActive: isActive}); + if (!isActive) { + tablet.gotoHomeScreen(); + } }; that.setActive = function (active) { @@ -488,6 +532,8 @@ var toolBar = (function () { cameraManager.disable(); selectionDisplay.triggerMapping.disable(); } else { + var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); + tablet.loadQMLSource("Edit.qml"); UserActivityLogger.enabledEdit(); entityListTool.setVisible(true); gridTool.setVisible(true); @@ -498,14 +544,7 @@ var toolBar = (function () { // everybody else to think that Interface has lost focus overall. fogbugzid:558 // Window.setFocus(); } - // Sets visibility of tool buttons, excluding the power button - toolBar.writeProperty("shown", active); - var visible = toolBar.readProperty("visible"); - if (active && !visible) { - toolBar.writeProperty("shown", false); - toolBar.writeProperty("shown", true); - } - lightOverlayManager.setVisible(isActive && Menu.isOptionChecked(MENU_SHOW_LIGHTS_IN_EDIT_MODE)); + entityIconOverlayManager.setVisible(isActive && Menu.isOptionChecked(MENU_SHOW_LIGHTS_AND_PARTICLES_IN_EDIT_MODE)); Entities.setDrawZoneBoundaries(isActive && Menu.isOptionChecked(MENU_SHOW_ZONES_IN_EDIT_MODE)); }; @@ -570,8 +609,8 @@ function findClickedEntity(event) { } var entityResult = Entities.findRayIntersection(pickRay, true); // want precision picking - var lightResult = lightOverlayManager.findRayIntersection(pickRay); - lightResult.accurate = true; + var iconResult = entityIconOverlayManager.findRayIntersection(pickRay); + iconResult.accurate = true; if (pickZones) { Entities.setZonesArePickable(false); @@ -579,18 +618,12 @@ function findClickedEntity(event) { var result; - if (!entityResult.intersects && !lightResult.intersects) { - return null; - } else if (entityResult.intersects && !lightResult.intersects) { + if (iconResult.intersects) { + result = iconResult; + } else if (entityResult.intersects) { result = entityResult; - } else if (!entityResult.intersects && lightResult.intersects) { - result = lightResult; } else { - if (entityResult.distance < lightResult.distance) { - result = entityResult; - } else { - result = lightResult; - } + return null; } if (!result.accurate) { @@ -637,11 +670,9 @@ function mousePressEvent(event) { var mouseCapturedByTool = false; var lastMousePosition = null; -var idleMouseTimerId = null; var CLICK_TIME_THRESHOLD = 500 * 1000; // 500 ms var CLICK_MOVE_DISTANCE_THRESHOLD = 20; var IDLE_MOUSE_TIMEOUT = 200; -var DEFAULT_ENTITY_DRAG_DROP_DISTANCE = 2.0; var lastMouseMoveEvent = null; @@ -669,9 +700,6 @@ function mouseMove(event) { if (!isActive) { return; } - if (idleMouseTimerId) { - Script.clearTimeout(idleMouseTimerId); - } // allow the selectionDisplay and cameraManager to handle the event first, if it doesn't handle it, then do our own thing if (selectionDisplay.mouseMoveEvent(event) || propertyMenu.mouseMoveEvent(event) || cameraManager.mouseMoveEvent(event)) { @@ -682,12 +710,6 @@ function mouseMove(event) { x: event.x, y: event.y }; - - idleMouseTimerId = Script.setTimeout(handleIdleMouse, IDLE_MOUSE_TIMEOUT); -} - -function handleIdleMouse() { - idleMouseTimerId = null; } function mouseReleaseEvent(event) { @@ -715,11 +737,22 @@ function mouseReleaseEvent(event) { } } +function wasTabletClicked(event) { + var rayPick = Camera.computePickRay(event.x, event.y); + var result = Overlays.findRayIntersection(rayPick, true, [HMD.tabletID, HMD.tabletScreenID, HMD.homeButtonID]); + return result.intersects; +} + function mouseClickEvent(event) { var wantDebug = false; - var result, properties; + var result, properties, tabletClicked; if (isActive && event.isLeftButton) { result = findClickedEntity(event); + tabletClicked = wasTabletClicked(event); + if (tabletClicked) { + return; + } + if (result === null || result === undefined) { if (!event.isShifted) { selectionManager.clearSelections(); @@ -769,8 +802,14 @@ function mouseClickEvent(event) { if (0 < x && sizeOK) { selectedEntityID = foundEntity; orientation = MyAvatar.orientation; - intersection = rayPlaneIntersection(pickRay, P, Quat.getFront(orientation)); + intersection = rayPlaneIntersection(pickRay, P, Quat.getForward(orientation)); + if (event.isShifted) { + particleExplorerTool.destroyWebView(); + } + if (properties.type !== "ParticleEffect") { + particleExplorerTool.destroyWebView(); + } if (!event.isShifted) { selectionManager.setSelections([foundEntity]); @@ -843,7 +882,6 @@ function setupModelMenus() { }); modelMenuAddedDelete = true; } - Menu.addMenuItem({ menuName: "Edit", menuItemName: "Entity List...", @@ -851,11 +889,25 @@ function setupModelMenus() { afterItem: "Entities", grouping: "Advanced" }); + + Menu.addMenuItem({ + menuName: "Edit", + menuItemName: "Parent Entity to Last", + afterItem: "Entity List...", + grouping: "Advanced" + }); + + Menu.addMenuItem({ + menuName: "Edit", + menuItemName: "Unparent Entity", + afterItem: "Parent Entity to Last", + grouping: "Advanced" + }); Menu.addMenuItem({ menuName: "Edit", menuItemName: "Allow Selecting of Large Models", shortcutKey: "CTRL+META+L", - afterItem: "Entity List...", + afterItem: "Unparent Entity", isCheckable: true, isChecked: true, grouping: "Advanced" @@ -931,18 +983,18 @@ function setupModelMenus() { }); Menu.addMenuItem({ menuName: "Edit", - menuItemName: MENU_SHOW_LIGHTS_IN_EDIT_MODE, + menuItemName: MENU_SHOW_LIGHTS_AND_PARTICLES_IN_EDIT_MODE, afterItem: MENU_EASE_ON_FOCUS, isCheckable: true, - isChecked: Settings.getValue(SETTING_SHOW_LIGHTS_IN_EDIT_MODE) === "true", + isChecked: Settings.getValue(SETTING_SHOW_LIGHTS_AND_PARTICLES_IN_EDIT_MODE) !== "false", grouping: "Advanced" }); Menu.addMenuItem({ menuName: "Edit", menuItemName: MENU_SHOW_ZONES_IN_EDIT_MODE, - afterItem: MENU_SHOW_LIGHTS_IN_EDIT_MODE, + afterItem: MENU_SHOW_LIGHTS_AND_PARTICLES_IN_EDIT_MODE, isCheckable: true, - isChecked: Settings.getValue(SETTING_SHOW_ZONES_IN_EDIT_MODE) === "true", + isChecked: Settings.getValue(SETTING_SHOW_ZONES_IN_EDIT_MODE) !== "false", grouping: "Advanced" }); @@ -958,6 +1010,8 @@ function cleanupModelMenus() { Menu.removeMenuItem("Edit", "Delete"); } + Menu.removeMenuItem("Edit", "Parent Entity to Last"); + Menu.removeMenuItem("Edit", "Unparent Entity"); Menu.removeMenuItem("Edit", "Entity List..."); Menu.removeMenuItem("Edit", "Allow Selecting of Large Models"); Menu.removeMenuItem("Edit", "Allow Selecting of Small Models"); @@ -971,7 +1025,7 @@ function cleanupModelMenus() { Menu.removeMenuItem("Edit", MENU_AUTO_FOCUS_ON_SELECT); Menu.removeMenuItem("Edit", MENU_EASE_ON_FOCUS); - Menu.removeMenuItem("Edit", MENU_SHOW_LIGHTS_IN_EDIT_MODE); + Menu.removeMenuItem("Edit", MENU_SHOW_LIGHTS_AND_PARTICLES_IN_EDIT_MODE); Menu.removeMenuItem("Edit", MENU_SHOW_ZONES_IN_EDIT_MODE); } @@ -979,7 +1033,7 @@ Script.scriptEnding.connect(function () { toolBar.setActive(false); Settings.setValue(SETTING_AUTO_FOCUS_ON_SELECT, Menu.isOptionChecked(MENU_AUTO_FOCUS_ON_SELECT)); Settings.setValue(SETTING_EASE_ON_FOCUS, Menu.isOptionChecked(MENU_EASE_ON_FOCUS)); - Settings.setValue(SETTING_SHOW_LIGHTS_IN_EDIT_MODE, Menu.isOptionChecked(MENU_SHOW_LIGHTS_IN_EDIT_MODE)); + Settings.setValue(SETTING_SHOW_LIGHTS_AND_PARTICLES_IN_EDIT_MODE, Menu.isOptionChecked(MENU_SHOW_LIGHTS_AND_PARTICLES_IN_EDIT_MODE)); Settings.setValue(SETTING_SHOW_ZONES_IN_EDIT_MODE, Menu.isOptionChecked(MENU_SHOW_ZONES_IN_EDIT_MODE)); progressDialog.cleanup(); @@ -990,6 +1044,9 @@ Script.scriptEnding.connect(function () { Overlays.deleteOverlay(importingSVOImageOverlay); Overlays.deleteOverlay(importingSVOTextOverlay); + + Controller.keyReleaseEvent.disconnect(keyReleaseEvent); + Controller.keyPressEvent.disconnect(keyPressEvent); }); var lastOrientation = null; @@ -1101,10 +1158,71 @@ function recursiveDelete(entities, childrenList) { Entities.deleteEntity(entityID); } } +function unparentSelectedEntities() { + if (SelectionManager.hasSelection()) { + var selectedEntities = selectionManager.selections; + var parentCheck = false; + if (selectedEntities.length < 1) { + Window.notifyEditError("You must have an entity selected inorder to unparent it."); + return; + } + selectedEntities.forEach(function (id, index) { + var parentId = Entities.getEntityProperties(id, ["parentID"]).parentID; + if (parentId !== null && parentId.length > 0 && parentId !== "{00000000-0000-0000-0000-000000000000}") { + parentCheck = true; + } + Entities.editEntity(id, {parentID: null}) + return true; + }); + if (parentCheck) { + if (selectedEntities.length > 1) { + Window.notify("Entities unparented"); + } else { + Window.notify("Entity unparented"); + } + } else { + if (selectedEntities.length > 1) { + Window.notify("Selected Entities have no parents"); + } else { + Window.notify("Selected Entity does not have a parent"); + } + } + } else { + Window.notifyEditError("You have nothing selected to unparent"); + } +} +function parentSelectedEntities() { + if (SelectionManager.hasSelection()) { + var selectedEntities = selectionManager.selections; + if (selectedEntities.length <= 1) { + Window.notifyEditError("You must have multiple entities selected in order to parent them"); + return; + } + var parentCheck = false; + var lastEntityId = selectedEntities[selectedEntities.length-1]; + selectedEntities.forEach(function (id, index) { + if (lastEntityId !== id) { + var parentId = Entities.getEntityProperties(id, ["parentID"]).parentID; + if (parentId !== lastEntityId) { + parentCheck = true; + } + Entities.editEntity(id, {parentID: lastEntityId}) + } + }); + + if(parentCheck) { + Window.notify("Entities parented"); + }else { + Window.notify("Entities are already parented to last"); + } + } else { + Window.notifyEditError("You have nothing selected to parent"); + } +} function deleteSelectedEntities() { if (SelectionManager.hasSelection()) { - selectedParticleEntity = 0; + selectedParticleEntityID = null; particleExplorerTool.destroyWebView(); SelectionManager.saveProperties(); var savedProperties = []; @@ -1164,6 +1282,10 @@ function handeMenuEvent(menuItem) { Entities.setLightsArePickable(Menu.isOptionChecked("Allow Selecting of Lights")); } else if (menuItem === "Delete") { deleteSelectedEntities(); + } else if (menuItem === "Parent Entity to Last") { + parentSelectedEntities(); + } else if (menuItem === "Unparent Entity") { + unparentSelectedEntities(); } else if (menuItem === "Export Entities") { if (!selectionManager.hasSelection()) { Window.notifyEditError("No entities have been selected."); @@ -1199,8 +1321,8 @@ function handeMenuEvent(menuItem) { selectAllEtitiesInCurrentSelectionBox(false); } else if (menuItem === "Select All Entities Touching Box") { selectAllEtitiesInCurrentSelectionBox(true); - } else if (menuItem === MENU_SHOW_LIGHTS_IN_EDIT_MODE) { - lightOverlayManager.setVisible(isActive && Menu.isOptionChecked(MENU_SHOW_LIGHTS_IN_EDIT_MODE)); + } else if (menuItem === MENU_SHOW_LIGHTS_AND_PARTICLES_IN_EDIT_MODE) { + entityIconOverlayManager.setVisible(isActive && Menu.isOptionChecked(MENU_SHOW_LIGHTS_AND_PARTICLES_IN_EDIT_MODE)); } else if (menuItem === MENU_SHOW_ZONES_IN_EDIT_MODE) { Entities.setDrawZoneBoundaries(isActive && Menu.isOptionChecked(MENU_SHOW_ZONES_IN_EDIT_MODE)); } @@ -1208,16 +1330,16 @@ function handeMenuEvent(menuItem) { } function getPositionToCreateEntity() { var HALF_TREE_SCALE = 16384; - var direction = Quat.getFront(MyAvatar.orientation); + var direction = Quat.getForward(MyAvatar.orientation); var distance = 1; var position = Vec3.sum(MyAvatar.position, Vec3.multiply(direction, distance)); if (Camera.mode === "entity" || Camera.mode === "independent") { - position = Vec3.sum(Camera.position, Vec3.multiply(Quat.getFront(Camera.orientation), distance)) + position = Vec3.sum(Camera.position, Vec3.multiply(Quat.getForward(Camera.orientation), distance)); } position.y += 0.5; if (position.x > HALF_TREE_SCALE || position.y > HALF_TREE_SCALE || position.z > HALF_TREE_SCALE) { - return null + return null; } return position; } @@ -1225,17 +1347,17 @@ function getPositionToCreateEntity() { function getPositionToImportEntity() { var dimensions = Clipboard.getContentsDimensions(); var HALF_TREE_SCALE = 16384; - var direction = Quat.getFront(MyAvatar.orientation); + var direction = Quat.getForward(MyAvatar.orientation); var longest = 1; longest = Math.sqrt(Math.pow(dimensions.x, 2) + Math.pow(dimensions.z, 2)); var position = Vec3.sum(MyAvatar.position, Vec3.multiply(direction, longest)); if (Camera.mode === "entity" || Camera.mode === "independent") { - position = Vec3.sum(Camera.position, Vec3.multiply(Quat.getFront(Camera.orientation), longest)) + position = Vec3.sum(Camera.position, Vec3.multiply(Quat.getForward(Camera.orientation), longest)); } if (position.x > HALF_TREE_SCALE || position.y > HALF_TREE_SCALE || position.z > HALF_TREE_SCALE) { - return null + return null; } return position; @@ -1289,13 +1411,12 @@ Window.svoImportRequested.connect(importSVO); Menu.menuItemEvent.connect(handeMenuEvent); -Controller.keyPressEvent.connect(function (event) { +var keyPressEvent = function (event) { if (isActive) { cameraManager.keyPressEvent(event); } -}); - -Controller.keyReleaseEvent.connect(function (event) { +}; +var keyReleaseEvent = function (event) { if (isActive) { cameraManager.keyReleaseEvent(event); } @@ -1329,8 +1450,16 @@ Controller.keyReleaseEvent.connect(function (event) { }); grid.setPosition(newPosition); } + } else if (event.key === KEY_P && event.isControl && !event.isAutoRepeat ) { + if (event.isShifted) { + unparentSelectedEntities(); + } else { + parentSelectedEntities(); + } } -}); +}; +Controller.keyReleaseEvent.connect(keyReleaseEvent); +Controller.keyPressEvent.connect(keyPressEvent); function recursiveAdd(newParentID, parentData) { var children = parentData.children; @@ -1436,11 +1565,11 @@ var ServerScriptStatusMonitor = function(entityID, statusCallback) { Entities.getServerScriptStatus(entityID, onStatusReceived); } }, 1000); - }; + } }; self.stop = function() { self.active = false; - } + }; Entities.getServerScriptStatus(entityID, onStatusReceived); }; @@ -1448,11 +1577,9 @@ var ServerScriptStatusMonitor = function(entityID, statusCallback) { var PropertiesTool = function (opts) { var that = {}; - var webView = new OverlayWebWindow({ - title: 'Entity Properties', - source: ENTITY_PROPERTIES_URL, - toolWindow: true - }); + var webView = null; + webView = Tablet.getTablet("com.highfidelity.interface.tablet.system"); + webView.setVisible = function(value) {}; var visible = false; @@ -1471,7 +1598,7 @@ var PropertiesTool = function (opts) { function updateScriptStatus(info) { info.type = "server_script_status"; webView.emitScriptEvent(JSON.stringify(info)); - }; + } function resetScriptStatus() { updateScriptStatus({ @@ -1482,7 +1609,7 @@ var PropertiesTool = function (opts) { }); } - selectionManager.addEventListener(function (selectionUpdated) { + function updateSelections(selectionUpdated) { var data = { type: 'update' }; @@ -1523,14 +1650,15 @@ var PropertiesTool = function (opts) { } data.selections = selections; webView.emitScriptEvent(JSON.stringify(data)); - }); + } + selectionManager.addEventListener(updateSelections); webView.webEventReceived.connect(function (data) { try { data = JSON.parse(data); } catch(e) { - print('Edit.js received web event that was not valid json.') + print('Edit.js received web event that was not valid json.'); return; } var i, properties, dY, diff, newPosition; @@ -1548,15 +1676,15 @@ var PropertiesTool = function (opts) { for (i = 0; i < selectionManager.selections.length; i++) { Entities.editEntity(selectionManager.selections[i], properties); } - } else { + } else if (data.properties) { if (data.properties.dynamic === false) { // this object is leaving dynamic, so we zero its velocities - data.properties["velocity"] = { + data.properties.velocity = { x: 0, y: 0, z: 0 }; - data.properties["angularVelocity"] = { + data.properties.angularVelocity = { x: 0, y: 0, z: 0 @@ -1580,6 +1708,10 @@ var PropertiesTool = function (opts) { } pushCommandForSelections(); selectionManager._update(); + } else if(data.type === 'parent') { + parentSelectedEntities(); + } else if(data.type === 'unparent') { + unparentSelectedEntities(); } else if(data.type === 'saveUserData'){ //the event bridge and json parsing handle our avatar id string differently. var actualID = data.id.split('"')[1]; @@ -1681,6 +1813,8 @@ var PropertiesTool = function (opts) { } } } + } else if (data.type === "propertiesPageReady") { + updateSelections(true); } }); @@ -1837,6 +1971,9 @@ var PopupMenu = function () { for (var i = 0; i < overlays.length; i++) { Overlays.deleteOverlay(overlays[i]); } + Controller.mousePressEvent.disconnect(self.mousePressEvent); + Controller.mouseMoveEvent.disconnect(self.mouseMoveEvent); + Controller.mouseReleaseEvent.disconnect(self.mouseReleaseEvent); } Controller.mousePressEvent.connect(self.mousePressEvent); @@ -1862,9 +1999,30 @@ var showMenuItem = propertyMenu.addMenuItem("Show in Marketplace"); var propertiesTool = new PropertiesTool(); var particleExplorerTool = new ParticleExplorerTool(); var selectedParticleEntity = 0; +var selectedParticleEntityID = null; + + +function selectParticleEntity(entityID) { + var properties = Entities.getEntityProperties(entityID); + var particleData = { + messageType: "particle_settings", + currentProperties: properties + }; + particleExplorerTool.destroyWebView(); + particleExplorerTool.createWebView(); + + selectedParticleEntity = entityID; + particleExplorerTool.setActiveParticleEntity(entityID); + particleExplorerTool.webView.emitScriptEvent(JSON.stringify(particleData)); +} + entityListTool.webView.webEventReceived.connect(function (data) { data = JSON.parse(data); - if (data.type === "selectionUpdate") { + if (data.type === 'parent') { + parentSelectedEntities(); + } else if(data.type === 'unparent') { + unparentSelectedEntities(); + } else if (data.type === "selectionUpdate") { var ids = data.entityIds; if (ids.length === 1) { if (Entities.getEntityProperties(ids[0], "type").type === "ParticleEffect") { @@ -1873,22 +2031,7 @@ entityListTool.webView.webEventReceived.connect(function (data) { return; } // Destroy the old particles web view first - particleExplorerTool.destroyWebView(); - particleExplorerTool.createWebView(); - var properties = Entities.getEntityProperties(ids[0]); - var particleData = { - messageType: "particle_settings", - currentProperties: properties - }; - selectedParticleEntity = ids[0]; - particleExplorerTool.setActiveParticleEntity(ids[0]); - - particleExplorerTool.webView.webEventReceived.connect(function (data) { - data = JSON.parse(data); - if (data.messageType === "page_loaded") { - particleExplorerTool.webView.emitScriptEvent(JSON.stringify(particleData)); - } - }); + selectParticleEntity(ids[0]); } else { selectedParticleEntity = 0; particleExplorerTool.destroyWebView(); diff --git a/scripts/system/fingerPaint.js b/scripts/system/fingerPaint.js index 959f594212..27206ef9fa 100644 --- a/scripts/system/fingerPaint.js +++ b/scripts/system/fingerPaint.js @@ -13,6 +13,7 @@ button, BUTTON_NAME = "PAINT", isFingerPainting = false, + shouldPointFingers = false, leftHand = null, rightHand = null, leftBrush = null, @@ -308,9 +309,14 @@ Messages.sendMessage(HIFI_POINTER_DISABLE_MESSAGE_CHANNEL, JSON.stringify({ pointerEnabled: enabled }), true); - Messages.sendMessage(HIFI_POINT_INDEX_MESSAGE_CHANNEL, JSON.stringify({ - pointIndex: !enabled - }), true); + + var newShouldPointFingers = !enabled; + if (newShouldPointFingers !== shouldPointFingers) { + Messages.sendMessage(HIFI_POINT_INDEX_MESSAGE_CHANNEL, JSON.stringify({ + pointIndex: newShouldPointFingers + }), true); + shouldPointFingers = newShouldPointFingers; + } } function enableProcessing() { @@ -430,4 +436,4 @@ setUp(); Script.scriptEnding.connect(tearDown); -}()); \ No newline at end of file +}()); diff --git a/scripts/system/generalSettings.js b/scripts/system/generalSettings.js index 0a9fc823ae..7d97f13757 100644 --- a/scripts/system/generalSettings.js +++ b/scripts/system/generalSettings.js @@ -18,7 +18,7 @@ var buttonName = "Settings"; var toolBar = null; var tablet = null; - var settings = "TabletGeneralSettings.qml" + var settings = "TabletGeneralPreferences.qml" function onClicked(){ if (tablet) { tablet.loadQMLSource(settings); diff --git a/scripts/system/goto.js b/scripts/system/goto.js index 0e09ea3d79..d364bf579e 100644 --- a/scripts/system/goto.js +++ b/scripts/system/goto.js @@ -18,13 +18,14 @@ var button; var buttonName = "GOTO"; var toolBar = null; var tablet = null; - +var onGotoScreen = false; function onAddressBarShown(visible) { button.editProperties({isActive: visible}); } function onClicked(){ DialogsManager.toggleAddressBar(); + onGotoScreen = !onGotoScreen; } if (Settings.getValue("HUDUIEnabled")) { @@ -49,6 +50,9 @@ button.clicked.connect(onClicked); DialogsManager.addressBarShown.connect(onAddressBarShown); Script.scriptEnding.connect(function () { + if (onGotoScreen) { + DialogsManager.toggleAddressBar(); + } button.clicked.disconnect(onClicked); if (tablet) { tablet.removeButton(button); diff --git a/scripts/system/help.js b/scripts/system/help.js index 5a1b712fb5..a335b2ef9c 100644 --- a/scripts/system/help.js +++ b/scripts/system/help.js @@ -13,8 +13,10 @@ /* globals Tablet, Script, HMD, Controller, Menu */ (function() { // BEGIN LOCAL_SCOPE - + + var HOME_BUTTON_TEXTURE = Script.resourcesPath() + "meshes/tablet-with-home-button.fbx/tablet-with-home-button.fbm/button-root.png"; var buttonName = "HELP"; + var onHelpScreen = false; var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); var button = tablet.addButton({ icon: "icons/tablet-icons/help-i.svg", @@ -25,18 +27,24 @@ var enabled = false; function onClicked() { - if (enabled) { - Menu.closeInfoView('InfoView_html/help.html'); - enabled = !enabled; - button.editProperties({isActive: enabled}); + if (onHelpScreen) { + tablet.gotoHomeScreen(); } else { + var tabletEntity = HMD.tabletID; + if (tabletEntity) { + Entities.editEntity(tabletEntity, {textures: JSON.stringify({"tex.close" : HOME_BUTTON_TEXTURE})}); + } Menu.triggerOption('Help...'); - enabled = !enabled; - button.editProperties({isActive: enabled}); + onHelpScreen = true; } } + function onScreenChanged(type, url) { + onHelpScreen = false; + } + button.clicked.connect(onClicked); + tablet.screenChanged.connect(onScreenChanged); var POLL_RATE = 500; var interval = Script.setInterval(function () { @@ -48,6 +56,9 @@ }, POLL_RATE); Script.scriptEnding.connect(function () { + if (onHelpScreen) { + tablet.gotoHomeScreen(); + } button.clicked.disconnect(onClicked); Script.clearInterval(interval); if (tablet) { diff --git a/scripts/system/hmd.js b/scripts/system/hmd.js index c206a76e3f..c545e6bcee 100644 --- a/scripts/system/hmd.js +++ b/scripts/system/hmd.js @@ -45,6 +45,7 @@ var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); var desktopOnlyViews = ['Mirror', 'Independent Mode', 'Entity Mode']; function onHmdChanged(isHmd) { + HMD.closeTablet(); if (isHmd) { button.editProperties({ icon: "icons/tablet-icons/switch-desk-i.svg", diff --git a/scripts/system/html/SnapshotReview.html b/scripts/system/html/SnapshotReview.html index d37afb180c..145cfb16a9 100644 --- a/scripts/system/html/SnapshotReview.html +++ b/scripts/system/html/SnapshotReview.html @@ -3,45 +3,43 @@ Share - -
-
-
- -
-
-
-
-
Would you like to share your pics in the Snapshots feed?
-
- - - - -
-
+
+ +
+
+
+
+
+
+
+
+
- + + + +
-
-
- - - - - - - - +
+
-
+
+
+ + + + + + + +
diff --git a/scripts/system/html/css/SnapshotReview.css b/scripts/system/html/css/SnapshotReview.css index c2965f92e1..34b690a021 100644 --- a/scripts/system/html/css/SnapshotReview.css +++ b/scripts/system/html/css/SnapshotReview.css @@ -8,63 +8,81 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html */ - -.snapshot-container { - width: 100%; - padding-top: 3px; -} - -.snapshot-column-left { - width: 320px; - position: absolute; - padding-top: 8px; -} - -.snapshot-column-right { - margin-left: 342px; -} - -.snapshot-column-right > div > img { - width: 100%; -} - -@media (max-width: 768px) { - .snapshot-column-left { - position: initial; - width: 100%; - } - .snapshot-column-right { - margin-left: 0; - width: 100%; - } - .snapshot-column-right > div > img { - margin-top: 18px !important; - } -} - -.snapshot-column-right > div { - position: relative; - padding: 2px; -} - -.snapshot-column-right > div > img { - border: 2px solid #575757; - margin: -2px; -} - -hr { - padding-left: 0; - padding-right: 0; - margin: 21px 0; +body { + padding-top: 0; + padding-bottom: 14px; } .snapsection { + padding-top: 14px; text-align: center; } -.title { - text-transform: uppercase; - font-size: 12px; +.snapsection.title { + padding-top: 0; + text-align: left; +} + +.title label { + font-size: 18px; + position: relative; + top: 12px; +} + +#snapshot-pane { + width: 100%; + height: 100%; + position: absolute; + top: 0; + left: 0; + box-sizing: border-box; + padding-top: 56px; + padding-bottom: 175px; +} + +#snapshot-images { + height: 100%; + width: 100%; + position: relative; +} + +#snapshot-images > div { + position: relative; + text-align: center; +} + +#snapshot-images img { + max-width: 100%; + max-height: 100%; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + vertical-align: middle; +} + +#snapshot-images div.property { + margin-top: 0; + position: absolute; + top: 50%; + left: 7px; + transform: translate(0%, -50%); +} + +#snapshot-images img { + box-sizing: border-box; + padding: 0 7px 0 7px; +} + +#snapshot-images img.multiple { + padding-left: 28px; +} + +#snapshot-controls { + width: 100%; + position: absolute; + left: 0; + bottom: 14px; } .prompt { diff --git a/scripts/system/html/css/edit-style.css b/scripts/system/html/css/edit-style.css index 251d0a2d75..06a60b5405 100644 --- a/scripts/system/html/css/edit-style.css +++ b/scripts/system/html/css/edit-style.css @@ -871,6 +871,7 @@ textarea:enabled[scrolling="true"]::-webkit-resizer { float: right; margin-right: 0; background-color: #ff0000; + min-width: 90px; } #entity-list { diff --git a/scripts/system/html/css/marketplaces.css b/scripts/system/html/css/marketplaces.css index bb57bea3bc..04c132eab1 100644 --- a/scripts/system/html/css/marketplaces.css +++ b/scripts/system/html/css/marketplaces.css @@ -5,6 +5,93 @@ // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html */ + +/* + CSS rules copied from edit-style.css. + Edit-style.css is not used in its entirety because don't want custom scrollbars; default scrollbar styling is used in order + to match other marketplace pages. +*/ + +@font-face { + font-family: Raleway-Regular; + src: url(../../../../resources/fonts/Raleway-Regular.ttf), /* Windows production */ + url(../../../../fonts/Raleway-Regular.ttf), /* OSX production */ + url(../../../../interface/resources/fonts/Raleway-Regular.ttf); /* Development, running script in /HiFi/examples */ +} + +@font-face { + font-family: Raleway-Bold; + src: url(../../../../resources/fonts/Raleway-Bold.ttf), + url(../../../../fonts/Raleway-Bold.ttf), + url(../../../../interface/resources/fonts/Raleway-Bold.ttf); +} + +@font-face { + font-family: Raleway-SemiBold; + src: url(../../../../resources/fonts/Raleway-SemiBold.ttf), + url(../../../../fonts/Raleway-SemiBold.ttf), + url(../../../../interface/resources/fonts/Raleway-SemiBold.ttf); +} + +@font-face { + font-family: FiraSans-SemiBold; + src: url(../../../../resources/fonts/FiraSans-SemiBold.ttf), + url(../../../../fonts/FiraSans-SemiBold.ttf), + url(../../../../interface/resources/fonts/FiraSans-SemiBold.ttf); +} + +* { + margin: 0; + padding: 0; +} + +body { + padding: 21px 21px 21px 21px; + + color: #afafaf; + background-color: #404040; + font-family: Raleway-Regular; + font-size: 15px; + + -webkit-touch-callout: none; + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + + overflow-x: hidden; + overflow-y: auto; +} + +input[type=button] { + font-family: Raleway-Bold; + font-size: 13px; + text-transform: uppercase; + vertical-align: top; + height: 28px; + min-width: 120px; + padding: 0px 18px; + margin-right: 6px; + border-radius: 5px; + border: none; + color: #fff; + background-color: #000; + background: linear-gradient(#343434 20%, #000 100%); + cursor: pointer; +} + +input[type=button].blue { + color: #fff; + background-color: #1080b8; + background: linear-gradient(#00b4ef 20%, #1080b8 100%); +} + + +/* + Marketplaces-specific CSS. +*/ + body { background: white; padding: 0 0 0 0; diff --git a/scripts/system/html/entityList.html b/scripts/system/html/entityList.html index 197d8f550a..9d774f1861 100644 --- a/scripts/system/html/entityList.html +++ b/scripts/system/html/entityList.html @@ -24,7 +24,7 @@
- +
@@ -89,6 +89,7 @@ +
No entities found in view within a 100 meter radius. Try moving to a different location and refreshing.
diff --git a/scripts/system/html/entityProperties.html b/scripts/system/html/entityProperties.html index b11127b26c..35accdd0df 100644 --- a/scripts/system/html/entityProperties.html +++ b/scripts/system/html/entityProperties.html @@ -61,7 +61,7 @@
- +

@@ -77,7 +77,7 @@
- +
+
+ + +
+

diff --git a/scripts/system/html/js/SnapshotReview.js b/scripts/system/html/js/SnapshotReview.js index a1bb350789..d97207384a 100644 --- a/scripts/system/html/js/SnapshotReview.js +++ b/scripts/system/html/js/SnapshotReview.js @@ -10,7 +10,7 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -var paths = [], idCounter = 0, useCheckboxes; +var paths = [], idCounter = 0, imageCount; function addImage(data) { if (!data.localPath) { return; @@ -19,11 +19,16 @@ function addImage(data) { input = document.createElement("INPUT"), label = document.createElement("LABEL"), img = document.createElement("IMG"), + div2 = document.createElement("DIV"), id = "p" + idCounter++; function toggle() { data.share = input.checked; } + div.style.height = "" + Math.floor(100 / imageCount) + "%"; + if (imageCount > 1) { + img.setAttribute("class", "multiple"); + } img.src = data.localPath; div.appendChild(img); - if (useCheckboxes) { // I'd rather use css, but the included stylesheet is quite particular. + if (imageCount > 1) { // I'd rather use css, but the included stylesheet is quite particular. // Our stylesheet(?) requires input.id to match label.for. Otherwise input doesn't display the check state. label.setAttribute('for', id); // cannot do label.for = input.id = id; @@ -31,9 +36,10 @@ function addImage(data) { input.checked = (id === "p0"); data.share = input.checked; input.addEventListener('change', toggle); - div.class = "property checkbox"; - div.appendChild(input); - div.appendChild(label); + div2.setAttribute("class", "property checkbox"); + div2.appendChild(input); + div2.appendChild(label); + div.appendChild(div2); } else { data.share = true; } @@ -43,7 +49,13 @@ function addImage(data) { function handleShareButtons(shareMsg) { var openFeed = document.getElementById('openFeed'); openFeed.checked = shareMsg.openFeedAfterShare; - openFeed.onchange = function () { EventBridge.emitWebEvent(openFeed.checked ? 'setOpenFeedTrue' : 'setOpenFeedFalse'); }; + openFeed.onchange = function () { + EventBridge.emitWebEvent(JSON.stringify({ + type: "snapshot", + action: (openFeed.checked ? "setOpenFeedTrue" : "setOpenFeedFalse") + })); + }; + if (!shareMsg.canShare) { // this means you may or may not be logged in, but can't share // because you are not in a public place. @@ -57,25 +69,42 @@ window.onload = function () { openEventBridge(function () { // Set up a handler for receiving the data, and tell the .js we are ready to receive it. EventBridge.scriptEventReceived.connect(function (message) { + message = JSON.parse(message); + if (message.type !== "snapshot") { + return; + } + // last element of list contains a bool for whether or not we can share stuff - var shareMsg = message.pop(); + var shareMsg = message.action.pop(); handleShareButtons(shareMsg); // rest are image paths which we add - useCheckboxes = message.length > 1; - message.forEach(addImage); + imageCount = message.action.length; + message.action.forEach(addImage); }); - EventBridge.emitWebEvent('ready'); + EventBridge.emitWebEvent(JSON.stringify({ + type: "snapshot", + action: "ready" + })); }); }; // beware of bug: Cannot send objects at top level. (Nested in arrays is fine.) function shareSelected() { - EventBridge.emitWebEvent(paths); -}; + EventBridge.emitWebEvent(JSON.stringify({ + type: "snapshot", + action: paths + })); +} function doNotShare() { - EventBridge.emitWebEvent([]); -}; + EventBridge.emitWebEvent(JSON.stringify({ + type: "snapshot", + action: [] + })); +} function snapshotSettings() { - EventBridge.emitWebEvent("openSettings"); -}; + EventBridge.emitWebEvent(JSON.stringify({ + type: "snapshot", + action: "openSettings" + })); +} diff --git a/scripts/system/html/js/entityList.js b/scripts/system/html/js/entityList.js index 1af9c1e1d6..c6692fc26e 100644 --- a/scripts/system/html/js/entityList.js +++ b/scripts/system/html/js/entityList.js @@ -19,6 +19,7 @@ const VISIBLE_GLYPH = ""; const TRANSPARENCY_GLYPH = ""; const SCRIPT_GLYPH = "k"; const DELETE = 46; // Key code for the delete key. +const KEY_P = 80; // Key code for letter p used for Parenting hotkey. const MAX_ITEMS = Number.MAX_VALUE; // Used to set the max length of the list of discovered entities. debugPrint = function (message) { @@ -26,7 +27,7 @@ debugPrint = function (message) { }; function loaded() { - openEventBridge(function() { + openEventBridge(function() { entityList = new List('entity-list', { valueNames: ['name', 'type', 'url', 'locked', 'visible'], page: MAX_ITEMS}); entityList.clear(); elEntityTable = document.getElementById("entity-table"); @@ -38,7 +39,7 @@ function loaded() { elFilter = document.getElementById("filter"); elInView = document.getElementById("in-view") elRadius = document.getElementById("radius"); - elTeleport = document.getElementById("teleport"); + elExport = document.getElementById("export"); elPal = document.getElementById("pal"); elEntityTable = document.getElementById("entity-table"); elInfoToggle = document.getElementById("info-toggle"); @@ -48,7 +49,7 @@ function loaded() { elNoEntitiesInView = document.getElementById("no-entities-in-view"); elNoEntitiesRadius = document.getElementById("no-entities-radius"); elEntityTableScroll = document.getElementById("entity-table-scroll"); - + document.getElementById("entity-name").onclick = function() { setSortColumn('name'); }; @@ -90,7 +91,7 @@ function loaded() { selection = selection.concat(selectedEntities); } else if (clickEvent.shiftKey && selectedEntities.length > 0) { var previousItemFound = -1; - var clickedItemFound = -1; + var clickedItemFound = -1; for (var entity in entityList.visibleItems) { if (clickedItemFound === -1 && this.dataset.entityId == entityList.visibleItems[entity].values().id) { clickedItemFound = entity; @@ -113,11 +114,11 @@ function loaded() { selection = selection.concat(betweenItems, selectedEntities); } } - + selectedEntities = selection; - + this.className = 'selected'; - + EventBridge.emitWebEvent(JSON.stringify({ type: "selectionUpdate", focus: false, @@ -126,7 +127,7 @@ function loaded() { refreshFooter(); } - + function onRowDoubleClicked() { EventBridge.emitWebEvent(JSON.stringify({ type: "selectionUpdate", @@ -134,7 +135,7 @@ function loaded() { entityIds: [this.dataset.entityId], })); } - + const BYTES_PER_MEGABYTE = 1024 * 1024; function decimalMegabytes(number) { @@ -173,7 +174,7 @@ function loaded() { currentElement.onclick = onRowClicked; currentElement.ondblclick = onRowDoubleClicked; }); - + if (refreshEntityListTimer) { clearTimeout(refreshEntityListTimer); } @@ -183,13 +184,13 @@ function loaded() { item.values({ name: name, url: filename, locked: locked, visible: visible }); } } - + function clearEntities() { entities = {}; entityList.clear(); refreshFooter(); } - + var elSortOrder = { name: document.querySelector('#entity-name .sort-order'), type: document.querySelector('#entity-type .sort-order'), @@ -215,12 +216,12 @@ function loaded() { entityList.sort(currentSortColumn, { order: currentSortOrder }); } setSortColumn('type'); - + function refreshEntities() { clearEntities(); EventBridge.emitWebEvent(JSON.stringify({ type: 'refresh' })); } - + function refreshFooter() { if (selectedEntities.length > 1) { elFooter.firstChild.nodeValue = selectedEntities.length + " entities selected"; @@ -239,7 +240,7 @@ function loaded() { entityList.search(elFilter.value); refreshFooter(); } - + function updateSelectedEntities(selectedIDs) { var notFound = false; for (var id in entities) { @@ -262,7 +263,7 @@ function loaded() { return notFound; } - + elRefresh.onclick = function() { refreshEntities(); } @@ -272,8 +273,8 @@ function loaded() { elToggleVisible.onclick = function () { EventBridge.emitWebEvent(JSON.stringify({ type: 'toggleVisible' })); } - elTeleport.onclick = function () { - EventBridge.emitWebEvent(JSON.stringify({ type: 'teleport' })); + elExport.onclick = function() { + EventBridge.emitWebEvent(JSON.stringify({ type: 'export'})); } elPal.onclick = function () { EventBridge.emitWebEvent(JSON.stringify({ type: 'pal' })); @@ -282,7 +283,7 @@ function loaded() { EventBridge.emitWebEvent(JSON.stringify({ type: 'delete' })); refreshEntities(); } - + document.addEventListener("keydown", function (keyDownEvent) { if (keyDownEvent.target.nodeName === "INPUT") { return; @@ -292,8 +293,15 @@ function loaded() { EventBridge.emitWebEvent(JSON.stringify({ type: 'delete' })); refreshEntities(); } + if (keyDownEvent.keyCode === KEY_P && keyDownEvent.ctrlKey) { + if (keyDownEvent.shiftKey) { + EventBridge.emitWebEvent(JSON.stringify({ type: 'unparent' })); + } else { + EventBridge.emitWebEvent(JSON.stringify({ type: 'parent' })); + } + } }, false); - + var isFilterInView = false; var FILTER_IN_VIEW_ATTRIBUTE = "pressed"; elNoEntitiesInView.style.display = "none"; @@ -320,7 +328,7 @@ function loaded() { if (window.EventBridge !== undefined) { EventBridge.scriptEventReceived.connect(function(data) { data = JSON.parse(data); - + if (data.type === "clearEntityList") { clearEntities(); } else if (data.type == "selectionUpdate") { @@ -330,10 +338,10 @@ function loaded() { } } else if (data.type == "update") { var newEntities = data.entities; - if (newEntities.length == 0) { + if (newEntities && newEntities.length == 0) { elNoEntitiesMessage.style.display = "block"; elFooter.firstChild.nodeValue = "0 entities found"; - } else { + } else if (newEntities) { elNoEntitiesMessage.style.display = "none"; for (var i = 0; i < newEntities.length; i++) { var id = newEntities[i].id; @@ -426,4 +434,3 @@ function loaded() { event.preventDefault(); }, false); } - diff --git a/scripts/system/html/js/entityProperties.js b/scripts/system/html/js/entityProperties.js index 8879c0f34e..2f109597d7 100644 --- a/scripts/system/html/js/entityProperties.js +++ b/scripts/system/html/js/entityProperties.js @@ -24,9 +24,10 @@ var ICON_FOR_TYPE = { } var EDITOR_TIMEOUT_DURATION = 1500; - +const KEY_P = 80; //Key code for letter p used for Parenting hotkey. var colorPickers = []; var lastEntityID = null; + debugPrint = function(message) { EventBridge.emitWebEvent( JSON.stringify({ @@ -273,7 +274,7 @@ function updateCheckedSubProperty(propertyName, propertyValue, subPropertyElemen propertyValue += subPropertyString + ','; } } else { - // We've unchecked, so remove + // We've unchecked, so remove propertyValue = propertyValue.replace(subPropertyString + ",", ""); } @@ -323,13 +324,9 @@ function setUserDataFromEditor(noUpdate) { }) ); } - } - - } - -function userDataChanger(groupName, keyName, checkBoxElement, userDataElement, defaultValue) { +function multiDataUpdater(groupName, updateKeyPair, userDataElement, defaults) { var properties = {}; var parsedData = {}; try { @@ -339,17 +336,31 @@ function userDataChanger(groupName, keyName, checkBoxElement, userDataElement, d } else { parsedData = JSON.parse(userDataElement.value); } - } catch (e) {} if (!(groupName in parsedData)) { parsedData[groupName] = {} } - delete parsedData[groupName][keyName]; - if (checkBoxElement.checked !== defaultValue) { - parsedData[groupName][keyName] = checkBoxElement.checked; - } - + var keys = Object.keys(updateKeyPair); + keys.forEach(function (key) { + delete parsedData[groupName][key]; + if (updateKeyPair[key] !== null && updateKeyPair[key] !== "null") { + if (updateKeyPair[key] instanceof Element) { + if(updateKeyPair[key].type === "checkbox") { + if (updateKeyPair[key].checked !== defaults[key]) { + parsedData[groupName][key] = updateKeyPair[key].checked; + } + } else { + var val = isNaN(updateKeyPair[key].value) ? updateKeyPair[key].value : parseInt(updateKeyPair[key].value); + if (val !== defaults[key]) { + parsedData[groupName][key] = val; + } + } + } else { + parsedData[groupName][key] = updateKeyPair[key]; + } + } + }); if (Object.keys(parsedData[groupName]).length == 0) { delete parsedData[groupName]; } @@ -368,6 +379,12 @@ function userDataChanger(groupName, keyName, checkBoxElement, userDataElement, d properties: properties, }) ); +} +function userDataChanger(groupName, keyName, values, userDataElement, defaultValue) { + var val = {}, def = {}; + val[keyName] = values; + def[keyName] = defaultValue; + multiDataUpdater(groupName, val, userDataElement, def); }; function setTextareaScrolling(element) { @@ -521,6 +538,7 @@ function unbindAllInputs() { function loaded() { openEventBridge(function() { + var allSections = []; var elID = document.getElementById("property-id"); var elType = document.getElementById("property-type"); @@ -584,6 +602,13 @@ function loaded() { var elCollisionSoundURL = document.getElementById("property-collision-sound-url"); var elGrabbable = document.getElementById("property-grabbable"); + + var elCloneable = document.getElementById("property-cloneable"); + var elCloneableDynamic = document.getElementById("property-cloneable-dynamic"); + var elCloneableGroup = document.getElementById("group-cloneable-group"); + var elCloneableLifetime = document.getElementById("property-cloneable-lifetime"); + var elCloneableLimit = document.getElementById("property-cloneable-limit"); + var elWantsTrigger = document.getElementById("property-wants-trigger"); var elIgnoreIK = document.getElementById("property-ignore-ik"); @@ -733,16 +758,16 @@ function loaded() { } } else if (data.type == "update") { - if (data.selections.length == 0) { + if (!data.selections || data.selections.length == 0) { if (editor !== null && lastEntityID !== null) { saveJSONUserData(true); deleteJSONEditor(); } elTypeIcon.style.display = "none"; elType.innerHTML = "No selection"; - elID.innerHTML = ""; + elID.value = ""; disableProperties(); - } else if (data.selections.length > 1) { + } else if (data.selections && data.selections.length > 1) { deleteJSONEditor(); var selections = data.selections; @@ -770,7 +795,7 @@ function loaded() { elTypeIcon.innerHTML = ICON_FOR_TYPE[type]; elTypeIcon.style.display = "inline-block"; - elID.innerHTML = ids.join("
"); + elID.value = ""; disableProperties(); } else { @@ -780,10 +805,10 @@ function loaded() { if (lastEntityID !== '"' + properties.id + '"' && lastEntityID !== null && editor !== null) { saveJSONUserData(true); } - //the event bridge and json parsing handle our avatar id string differently. + //the event bridge and json parsing handle our avatar id string differently. lastEntityID = '"' + properties.id + '"'; - elID.innerHTML = properties.id; + elID.value = properties.id; elType.innerHTML = properties.type; elTypeIcon.innerHTML = ICON_FOR_TYPE[properties.type]; @@ -847,8 +872,16 @@ function loaded() { elCollideOtherAvatar.checked = properties.collidesWith.indexOf("otherAvatar") > -1; elGrabbable.checked = properties.dynamic; + elWantsTrigger.checked = false; elIgnoreIK.checked = true; + + elCloneable.checked = false; + elCloneableDynamic.checked = false; + elCloneableGroup.style.display = elCloneable.checked ? "block": "none"; + elCloneableLimit.value = 0; + elCloneableLifetime.value = 300; + var parsedUserData = {} try { parsedUserData = JSON.parse(properties.userData); @@ -863,8 +896,23 @@ function loaded() { if ("ignoreIK" in parsedUserData["grabbableKey"]) { elIgnoreIK.checked = parsedUserData["grabbableKey"].ignoreIK; } + if ("cloneable" in parsedUserData["grabbableKey"]) { + elCloneable.checked = parsedUserData["grabbableKey"].cloneable; + elCloneableGroup.style.display = elCloneable.checked ? "block": "none"; + 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 : 0; + } + } + } } - } catch (e) {} + } catch (e) { + } elCollisionSoundURL.value = properties.collisionSoundURL; elLifetime.value = properties.lifetime; @@ -1154,8 +1202,38 @@ function loaded() { }); elGrabbable.addEventListener('change', function() { + if(elCloneable.checked) { + elGrabbable.checked = false; + } userDataChanger("grabbableKey", "grabbable", elGrabbable, elUserData, properties.dynamic); }); + elCloneableDynamic.addEventListener('change', function (event){ + userDataChanger("grabbableKey", "cloneDynamic", event.target, elUserData, -1); + }); + elCloneable.addEventListener('change', function (event) { + var checked = event.target.checked; + if (checked) { + multiDataUpdater("grabbableKey", + {cloneLifetime: elCloneableLifetime, cloneLimit: elCloneableLimit, cloneDynamic: elCloneableDynamic, cloneable: event.target}, + elUserData, {}); + elCloneableGroup.style.display = "block"; + EventBridge.emitWebEvent( + '{"id":' + lastEntityID + ', "type":"update", "properties":{"dynamic":false, "grabbable": false}}' + ); + } else { + multiDataUpdater("grabbableKey", + {cloneLifetime: null, cloneLimit: null, cloneDynamic: null, cloneable: false}, + elUserData, {}); + elCloneableGroup.style.display = "none"; + } + }); + + var numberListener = function (event) { + userDataChanger("grabbableKey", event.target.getAttribute("data-user-data-type"), parseInt(event.target.value), elUserData, false); + }; + elCloneableLifetime.addEventListener('change', numberListener); + elCloneableLimit.addEventListener('change', numberListener); + elWantsTrigger.addEventListener('change', function() { userDataChanger("grabbableKey", "wantsTrigger", elWantsTrigger, elUserData, false); }); @@ -1390,7 +1468,7 @@ function loaded() { elZoneFlyingAllowed.addEventListener('change', createEmitCheckedPropertyUpdateFunction('flyingAllowed')); elZoneGhostingAllowed.addEventListener('change', createEmitCheckedPropertyUpdateFunction('ghostingAllowed')); elZoneFilterURL.addEventListener('change', createEmitTextPropertyUpdateFunction('filterURL')); - + var voxelVolumeSizeChangeFunction = createEmitVec3PropertyUpdateFunction( 'voxelVolumeSize', elVoxelVolumeSizeX, elVoxelVolumeSizeY, elVoxelVolumeSizeZ); elVoxelVolumeSizeX.addEventListener('change', voxelVolumeSizeChangeFunction); @@ -1441,7 +1519,15 @@ function loaded() { })); }); - + document.addEventListener("keydown", function (keyDown) { + if (keyDown.keyCode === KEY_P && keyDown.ctrlKey) { + if (keyDown.shiftKey) { + EventBridge.emitWebEvent(JSON.stringify({ type: 'unparent' })); + } else { + EventBridge.emitWebEvent(JSON.stringify({ type: 'parent' })); + } + } + }); window.onblur = function() { // Fake a change event var ev = document.createEvent("HTMLEvents"); @@ -1618,4 +1704,8 @@ function loaded() { document.addEventListener("contextmenu", function(event) { event.preventDefault(); }, false); + + setTimeout(function() { + EventBridge.emitWebEvent(JSON.stringify({ type: 'propertiesPageReady' })); + }, 1000); } diff --git a/scripts/system/html/js/gridControls.js b/scripts/system/html/js/gridControls.js index a245ed4cda..be4271788e 100644 --- a/scripts/system/html/js/gridControls.js +++ b/scripts/system/html/js/gridControls.js @@ -6,6 +6,8 @@ // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +const KEY_P = 80; //Key code for letter p used for Parenting hotkey. + function loaded() { openEventBridge(function() { elPosY = document.getElementById("horiz-y"); @@ -131,10 +133,17 @@ function loaded() { EventBridge.emitWebEvent(JSON.stringify({ type: 'init' })); }); - + document.addEventListener("keydown", function (keyDown) { + if (keyDown.keyCode === KEY_P && keyDown.ctrlKey) { + if (keyDown.shiftKey) { + EventBridge.emitWebEvent(JSON.stringify({ type: 'unparent' })); + } else { + EventBridge.emitWebEvent(JSON.stringify({ type: 'parent' })); + } + } + }) // Disable right-click context menu which is not visible in the HMD and makes it seem like the app has locked document.addEventListener("contextmenu", function (event) { event.preventDefault(); }, false); } - diff --git a/scripts/system/html/js/marketplacesInject.js b/scripts/system/html/js/marketplacesInject.js index 10970a7749..d4937ac9db 100644 --- a/scripts/system/html/js/marketplacesInject.js +++ b/scripts/system/html/js/marketplacesInject.js @@ -26,6 +26,8 @@ var xmlHttpRequest = null; var isPreparing = false; // Explicitly track download request status. + var lastPage = "https://metaverse.highfidelity.com/marketplace?"; + function injectCommonCode(isDirectoryPage) { // Supporting styles from marketplaces.css. @@ -65,7 +67,7 @@ // Footer actions. $("#back-button").on("click", function () { - window.history.back(); + window.location = lastPage; }); $("#all-markets").on("click", function () { EventBridge.emitWebEvent(GOTO_DIRECTORY); @@ -342,7 +344,12 @@ } } + function locationChanged() { + lastPage = location.href; + } + // Load / unload. window.addEventListener("load", onLoad); // More robust to Web site issues than using $(document).ready(). + window.addEventListener("hashchange", locationChanged); }()); diff --git a/scripts/system/html/marketplaces.html b/scripts/system/html/marketplaces.html index 976c0f294f..6051a9df96 100644 --- a/scripts/system/html/marketplaces.html +++ b/scripts/system/html/marketplaces.html @@ -10,7 +10,6 @@ Marketplaces - diff --git a/scripts/system/libraries/WebTablet.js b/scripts/system/libraries/WebTablet.js index dd2aaf346b..f9d37a0086 100644 --- a/scripts/system/libraries/WebTablet.js +++ b/scripts/system/libraries/WebTablet.js @@ -14,6 +14,7 @@ Script.include(Script.resolvePath("../libraries/utils.js")); Script.include(Script.resolvePath("../libraries/controllers.js")); Script.include(Script.resolvePath("../libraries/Xform.js")); +var VEC3_ZERO = {x: 0, y: 0, z: 0}; var X_AXIS = {x: 1, y: 0, z: 0}; var Y_AXIS = {x: 0, y: 1, z: 0}; var DEFAULT_DPI = 34; @@ -22,6 +23,7 @@ var DEFAULT_VERTICAL_FIELD_OF_VIEW = 45; // degrees var SENSOR_TO_ROOM_MATRIX = -2; var CAMERA_MATRIX = -7; var ROT_Y_180 = {x: 0, y: 1, z: 0, w: 0}; +var ROT_IDENT = {x: 0, y: 0, z: 0, w: 1}; var TABLET_TEXTURE_RESOLUTION = { x: 480, y: 706 }; var INCHES_TO_METERS = 1 / 39.3701; var AVATAR_SELF_ID = "{00000000-0000-0000-0000-000000000001}"; @@ -33,7 +35,7 @@ var TABLET_NATURAL_DIMENSIONS = {x: 33.797, y: 50.129, z: 2.269}; var HOME_BUTTON_TEXTURE = "http://hifi-content.s3.amazonaws.com/alan/dev/tablet-with-home-button.fbx/tablet-with-home-button.fbm/button-close.png"; // var HOME_BUTTON_TEXTURE = Script.resourcesPath() + "meshes/tablet-with-home-button.fbx/tablet-with-home-button.fbm/button-close.png"; -var TABLET_MODEL_PATH = "http://hifi-content.s3.amazonaws.com/alan/dev/tablet-with-home-button.fbx"; +// var TABLET_MODEL_PATH = "http://hifi-content.s3.amazonaws.com/alan/dev/tablet-with-home-button.fbx"; var LOCAL_TABLET_MODEL_PATH = Script.resourcesPath() + "meshes/tablet-with-home-button.fbx"; // returns object with two fields: @@ -45,42 +47,40 @@ function calcSpawnInfo(hand, height) { var headPos = (HMD.active && Camera.mode === "first person") ? HMD.position : Camera.position; var headRot = (HMD.active && Camera.mode === "first person") ? HMD.orientation : Camera.orientation; + if (!hand) { + hand = NO_HANDS; + } + if (HMD.active && hand !== NO_HANDS) { var handController = getControllerWorldLocation(hand, true); - var controllerPosition = handController.position; - // base of the tablet is slightly above controller position - var TABLET_BASE_DISPLACEMENT = {x: 0, y: 0.1, z: 0}; - var tabletBase = Vec3.sum(controllerPosition, TABLET_BASE_DISPLACEMENT); - - var d = Vec3.subtract(headPos, tabletBase); - var theta = Math.acos(d.y / Vec3.length(d)); - d.y = 0; - if (Vec3.length(d) < 0.0001) { - d = {x: 1, y: 0, z: 0}; - } else { - d = Vec3.normalize(d); + var TABLET_UP_OFFSET = 0.1; + var TABLET_FORWARD_OFFSET = 0.1; + var normal = Vec3.multiplyQbyV(handController.rotation, {x: 0, y: -1, z: 0}); + var pitch = Math.asin(normal.y); + var MAX_PITCH = Math.PI / 4; + if (pitch < -MAX_PITCH) { + pitch = -MAX_PITCH; + } else if (pitch > MAX_PITCH) { + pitch = MAX_PITCH; } - var w = Vec3.normalize(Vec3.cross(Y_AXIS, d)); - var ANGLE_OFFSET = 25; - var q = Quat.angleAxis(theta * (180 / Math.PI) - (90 - ANGLE_OFFSET), w); - var u = Vec3.multiplyQbyV(q, d); - // use u to compute a full lookAt quaternion. - var lookAtRot = Quat.lookAt(tabletBase, Vec3.sum(tabletBase, u), Y_AXIS); - var yDisplacement = (height / 2); - var zDisplacement = 0.05; - var tabletOffset = Vec3.multiplyQbyV(lookAtRot, {x: 0, y: yDisplacement, z: zDisplacement}); - finalPosition = Vec3.sum(tabletBase, tabletOffset); + // rebuild normal from pitch and heading. + var heading = Math.atan2(normal.z, normal.x); + normal = {x: Math.cos(heading), y: Math.sin(pitch), z: Math.sin(heading)}; + + var position = Vec3.sum(handController.position, {x: 0, y: TABLET_UP_OFFSET, z: 0}); + var rotation = Quat.lookAt({x: 0, y: 0, z: 0}, normal, Y_AXIS); + var offset = Vec3.multiplyQbyV(rotation, {x: 0, y: height / 2, z: TABLET_FORWARD_OFFSET}); return { - position: finalPosition, - rotation: lookAtRot + position: Vec3.sum(offset, position), + rotation: rotation }; } else { - var front = Quat.getFront(headRot); - finalPosition = Vec3.sum(headPos, Vec3.multiply(0.6, front)); - var orientation = Quat.lookAt({x: 0, y: 0, z: 0}, front, {x: 0, y: 1, z: 0}); + var forward = Quat.getForward(headRot); + finalPosition = Vec3.sum(headPos, Vec3.multiply(0.6, forward)); + var orientation = Quat.lookAt({x: 0, y: 0, z: 0}, forward, {x: 0, y: 1, z: 0}); return { position: finalPosition, rotation: Quat.multiply(orientation, {x: 0, y: 1, z: 0, w: 0}) @@ -96,7 +96,7 @@ function calcSpawnInfo(hand, height) { * @param hand [number] -1 indicates no hand, Controller.Standard.RightHand or Controller.Standard.LeftHand * @param clientOnly [bool] true indicates tablet model is only visible to client. */ -WebTablet = function (url, width, dpi, hand, clientOnly) { +WebTablet = function (url, width, dpi, hand, clientOnly, location) { var _this = this; @@ -105,6 +105,7 @@ WebTablet = function (url, width, dpi, hand, clientOnly) { var tabletScaleFactor = this.width / TABLET_NATURAL_DIMENSIONS.x; this.height = TABLET_NATURAL_DIMENSIONS.y * tabletScaleFactor; this.depth = TABLET_NATURAL_DIMENSIONS.z * tabletScaleFactor; + this.landscape = false; if (dpi) { this.dpi = dpi; @@ -112,13 +113,7 @@ WebTablet = function (url, width, dpi, hand, clientOnly) { this.dpi = DEFAULT_DPI * (DEFAULT_WIDTH / this.width); } - var modelURL; - if (Settings.getValue("tabletVisibleToOthers")) { - modelURL = TABLET_MODEL_PATH; - } else { - modelURL = LOCAL_TABLET_MODEL_PATH; - } - + var modelURL = LOCAL_TABLET_MODEL_PATH; var tabletProperties = { name: "WebTablet Tablet", type: "Model", @@ -128,22 +123,20 @@ WebTablet = function (url, width, dpi, hand, clientOnly) { userData: JSON.stringify({ "grabbableKey": {"grabbable": true} }), - dimensions: {x: this.width, y: this.height, z: this.depth}, + dimensions: this.getDimensions(), parentID: AVATAR_SELF_ID }; // compute position, rotation & parentJointIndex of the tablet this.calculateTabletAttachmentProperties(hand, true, tabletProperties); + if (location) { + tabletProperties.localPosition = location.localPosition; + tabletProperties.localRotation = location.localRotation; + } this.cleanUpOldTablets(); - if (Settings.getValue("tabletVisibleToOthers")) { - this.tabletEntityID = Entities.addEntity(tabletProperties, clientOnly); - this.tabletIsOverlay = false; - } else { - this.tabletEntityID = Overlays.addOverlay("model", tabletProperties); - this.tabletIsOverlay = true; - } + this.tabletEntityID = Overlays.addOverlay("model", tabletProperties); if (this.webOverlayID) { Overlays.deleteOverlay(this.webOverlayID); @@ -157,7 +150,7 @@ WebTablet = function (url, width, dpi, hand, clientOnly) { url: url, localPosition: { x: 0, y: WEB_ENTITY_Y_OFFSET, z: -WEB_ENTITY_Z_OFFSET }, localRotation: Quat.angleAxis(180, Y_AXIS), - resolution: TABLET_TEXTURE_RESOLUTION, + resolution: this.getTabletTextureResolution(), dpi: this.dpi, color: { red: 255, green: 255, blue: 255 }, alpha: 1.0, @@ -167,11 +160,11 @@ WebTablet = function (url, width, dpi, hand, clientOnly) { isAA: HMD.active }); - var HOME_BUTTON_Y_OFFSET = (this.height / 2) - 0.009; + var HOME_BUTTON_Y_OFFSET = (this.height / 2) - (this.height / 20); this.homeButtonID = Overlays.addOverlay("sphere", { name: "homeButton", localPosition: {x: -0.001, y: -HOME_BUTTON_Y_OFFSET, z: 0.0}, - localRotation: Quat.angleAxis(0, Y_AXIS), + localRotation: {x: 0, y: 1, z: 0, w: 0}, dimensions: { x: 4 * tabletScaleFactor, y: 4 * tabletScaleFactor, z: 4 * tabletScaleFactor}, alpha: 0.0, visible: true, @@ -184,11 +177,18 @@ WebTablet = function (url, width, dpi, hand, clientOnly) { if (_this.homeButtonID == senderID) { var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); var onHomeScreen = tablet.onHomeScreen(); + var isMessageOpen; if (onHomeScreen) { - HMD.closeTablet(); + isMessageOpen = tablet.isMessageDialogOpen(); + if (isMessageOpen === false) { + HMD.closeTablet(); + } } else { - tablet.gotoHomeScreen(); - _this.setHomeButtonTexture(); + isMessageOpen = tablet.isMessageDialogOpen(); + if (isMessageOpen === false) { + tablet.gotoHomeScreen(); + _this.setHomeButtonTexture(); + } } } }; @@ -199,18 +199,6 @@ WebTablet = function (url, width, dpi, hand, clientOnly) { return Entities.getWebViewRoot(_this.tabletEntityID); }; - this.getLocation = function() { - if (this.tabletIsOverlay) { - var location = Overlays.getProperty(this.tabletEntityID, "localPosition"); - var orientation = Overlays.getProperty(this.tabletEntityID, "localOrientation"); - return { - localPosition: location, - localRotation: orientation - }; - } else { - return Entities.getEntityProperties(_this.tabletEntityID, ["localPosition", "localRotation"]); - } - }; this.clicked = false; this.myOnHmdChanged = function () { @@ -249,6 +237,42 @@ WebTablet = function (url, width, dpi, hand, clientOnly) { Camera.modeUpdated.connect(this.myCameraModeChanged); }; +WebTablet.prototype.getDimensions = function() { + if (this.landscape) { + return { x: this.width * 2, y: this.height, z: this.depth }; + } else { + return { x: this.width, y: this.height, z: this.depth }; + } +}; + +WebTablet.prototype.getTabletTextureResolution = function() { + if (this.landscape) { + return { x: TABLET_TEXTURE_RESOLUTION.x * 2, y: TABLET_TEXTURE_RESOLUTION.y }; + } else { + return TABLET_TEXTURE_RESOLUTION; + } +}; + +WebTablet.prototype.setLandscape = function(newLandscapeValue) { + if (this.landscape == newLandscapeValue) { + return; + } + this.landscape = newLandscapeValue; + Overlays.editOverlay(this.tabletEntityID, { dimensions: this.getDimensions() }); + Overlays.editOverlay(this.webOverlayID, { + resolution: this.getTabletTextureResolution() + }); +}; + +WebTablet.prototype.getLocation = function() { + var location = Overlays.getProperty(this.tabletEntityID, "localPosition"); + var orientation = Overlays.getProperty(this.tabletEntityID, "localOrientation"); + return { + localPosition: location, + localRotation: orientation + }; +}; + WebTablet.prototype.setHomeButtonTexture = function() { Entities.editEntity(this.tabletEntityID, {textures: JSON.stringify({"tex.close": HOME_BUTTON_TEXTURE})}); }; @@ -265,13 +289,37 @@ WebTablet.prototype.getOverlayObject = function () { return Overlays.getOverlayObject(this.webOverlayID); }; +WebTablet.prototype.setWidth = function (width) { + + // scale factor of natural tablet dimensions. + this.width = width || DEFAULT_WIDTH; + var tabletScaleFactor = this.width / TABLET_NATURAL_DIMENSIONS.x; + this.height = TABLET_NATURAL_DIMENSIONS.y * tabletScaleFactor; + this.depth = TABLET_NATURAL_DIMENSIONS.z * tabletScaleFactor; + this.dpi = DEFAULT_DPI * (DEFAULT_WIDTH / this.width); + + // update tablet model dimensions + Overlays.editOverlay(this.tabletEntityID, { dimensions: this.getDimensions() }); + + // update webOverlay + var WEB_ENTITY_Z_OFFSET = (this.depth / 2); + var WEB_ENTITY_Y_OFFSET = 0.004; + Overlays.editOverlay(this.webOverlayID, { + localPosition: { x: 0, y: WEB_ENTITY_Y_OFFSET, z: -WEB_ENTITY_Z_OFFSET }, + dpi: this.dpi + }); + + // update homeButton + var HOME_BUTTON_Y_OFFSET = (this.height / 2) - (this.height / 20); + Overlays.editOverlay(this.homeButtonID, { + localPosition: {x: -0.001, y: -HOME_BUTTON_Y_OFFSET, z: 0.0}, + dimensions: { x: 4 * tabletScaleFactor, y: 4 * tabletScaleFactor, z: 4 * tabletScaleFactor} + }); +}; + WebTablet.prototype.destroy = function () { Overlays.deleteOverlay(this.webOverlayID); - if (this.tabletIsOverlay) { - Overlays.deleteOverlay(this.tabletEntityID); - } else { - Entities.deleteEntity(this.tabletEntityID); - } + Overlays.deleteOverlay(this.tabletEntityID); Overlays.deleteOverlay(this.homeButtonID); HMD.displayModeChanged.disconnect(this.myOnHmdChanged); @@ -314,15 +362,15 @@ WebTablet.prototype.calculateWorldAttitudeRelativeToCamera = function (windowPos // clamp window pos so 2d tablet is not off-screen. var TABLET_TEXEL_PADDING = {x: 60, y: 90}; - var X_CLAMP = (DESKTOP_TABLET_SCALE / 100) * ((TABLET_TEXTURE_RESOLUTION.x / 2) + TABLET_TEXEL_PADDING.x); - var Y_CLAMP = (DESKTOP_TABLET_SCALE / 100) * ((TABLET_TEXTURE_RESOLUTION.y / 2) + TABLET_TEXEL_PADDING.y); + var X_CLAMP = (DESKTOP_TABLET_SCALE / 100) * ((this.getTabletTextureResolution().x / 2) + TABLET_TEXEL_PADDING.x); + var Y_CLAMP = (DESKTOP_TABLET_SCALE / 100) * ((this.getTabletTextureResolution().y / 2) + TABLET_TEXEL_PADDING.y); windowPos.x = clamp(windowPos.x, X_CLAMP, Window.innerWidth - X_CLAMP); windowPos.y = clamp(windowPos.y, Y_CLAMP, Window.innerHeight - Y_CLAMP); var fov = (Settings.getValue('fieldOfView') || DEFAULT_VERTICAL_FIELD_OF_VIEW) * (Math.PI / 180); var MAX_PADDING_FACTOR = 2.2; - var PADDING_FACTOR = Math.min(Window.innerHeight / TABLET_TEXTURE_RESOLUTION.y, MAX_PADDING_FACTOR); - var TABLET_HEIGHT = (TABLET_TEXTURE_RESOLUTION.y / this.dpi) * INCHES_TO_METERS; + var PADDING_FACTOR = Math.min(Window.innerHeight / this.getTabletTextureResolution().y, MAX_PADDING_FACTOR); + var TABLET_HEIGHT = (this.getTabletTextureResolution().y / this.dpi) * INCHES_TO_METERS; var WEB_ENTITY_Z_OFFSET = (this.depth / 2); // calcualte distance from camera @@ -387,16 +435,6 @@ WebTablet.prototype.calculateTabletAttachmentProperties = function (hand, useMou WebTablet.prototype.onHmdChanged = function () { - if (HMD.active) { - Controller.mousePressEvent.disconnect(this.myMousePressEvent); - Controller.mouseMoveEvent.disconnect(this.myMouseMoveEvent); - Controller.mouseReleaseEvent.disconnect(this.myMouseReleaseEvent); - } else { - Controller.mousePressEvent.connect(this.myMousePressEvent); - Controller.mouseMoveEvent.connect(this.myMouseMoveEvent); - Controller.mouseReleaseEvent.connect(this.myMouseReleaseEvent); - } - var tabletProperties = {}; // compute position, rotation & parentJointIndex of the tablet this.calculateTabletAttachmentProperties(NO_HANDS, false, tabletProperties); @@ -456,32 +494,30 @@ WebTablet.prototype.getPosition = function () { WebTablet.prototype.mousePressEvent = function (event) { var pickRay = Camera.computePickRay(event.x, event.y); var entityPickResults; - if (this.tabletIsOverlay) { - entityPickResults = Overlays.findRayIntersection(pickRay, true, [this.tabletEntityID]); - } else { - entityPickResults = Entities.findRayIntersection(pickRay, true, [this.tabletEntityID]); - } + entityPickResults = Overlays.findRayIntersection(pickRay, true, [this.tabletEntityID]); if (entityPickResults.intersects && (entityPickResults.entityID === this.tabletEntityID || entityPickResults.overlayID === this.tabletEntityID)) { var overlayPickResults = Overlays.findRayIntersection(pickRay, true, [this.webOverlayID, this.homeButtonID], []); if (overlayPickResults.intersects && overlayPickResults.overlayID === this.homeButtonID) { var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); var onHomeScreen = tablet.onHomeScreen(); + var isMessageOpen = tablet.isMessageDialogOpen(); if (onHomeScreen) { - HMD.closeTablet(); + if (isMessageOpen === false) { + HMD.closeTablet(); + } } else { - tablet.gotoHomeScreen(); - this.setHomeButtonTexture(); + if (isMessageOpen === false) { + tablet.gotoHomeScreen(); + this.setHomeButtonTexture(); + } + Messages.sendLocalMessage("home", this.homeButtonID); } } else if (!HMD.active && (!overlayPickResults.intersects || overlayPickResults.overlayID !== this.webOverlayID)) { this.dragging = true; var invCameraXform = new Xform(Camera.orientation, Camera.position).inv(); this.initialLocalIntersectionPoint = invCameraXform.xformPoint(entityPickResults.intersection); - if (this.tabletIsOverlay) { - this.initialLocalPosition = Overlays.getProperty(this.tabletEntityID, "localPosition"); - } else { - this.initialLocalPosition = Entities.getEntityProperties(this.tabletEntityID, ["localPosition"]).localPosition; - } + this.initialLocalPosition = Overlays.getProperty(this.tabletEntityID, "localPosition"); } } }; @@ -527,15 +563,9 @@ WebTablet.prototype.mouseMoveEvent = function (event) { var localIntersectionPoint = Vec3.sum(localPickRay.origin, Vec3.multiply(localPickRay.direction, result.distance)); var localOffset = Vec3.subtract(localIntersectionPoint, this.initialLocalIntersectionPoint); var localPosition = Vec3.sum(this.initialLocalPosition, localOffset); - if (this.tabletIsOverlay) { - Overlays.editOverlay(this.tabletEntityID, { - localPosition: localPosition - }); - } else { - Entities.editEntity(this.tabletEntityID, { - localPosition: localPosition - }); - } + Overlays.editOverlay(this.tabletEntityID, { + localPosition: localPosition + }); } } }; diff --git a/scripts/system/libraries/entityCameraTool.js b/scripts/system/libraries/entityCameraTool.js index 301b60f550..6becc81d9b 100644 --- a/scripts/system/libraries/entityCameraTool.js +++ b/scripts/system/libraries/entityCameraTool.js @@ -158,7 +158,7 @@ CameraManager = function() { that.zoomDistance = INITIAL_ZOOM_DISTANCE; that.targetZoomDistance = that.zoomDistance + 3.0; var focalPoint = Vec3.sum(Camera.getPosition(), - Vec3.multiply(that.zoomDistance, Quat.getFront(Camera.getOrientation()))); + Vec3.multiply(that.zoomDistance, Quat.getForward(Camera.getOrientation()))); // Determine the correct yaw and pitch to keep the camera in the same location var dPos = Vec3.subtract(focalPoint, Camera.getPosition()); @@ -435,7 +435,7 @@ CameraManager = function() { }); var q = Quat.multiply(yRot, xRot); - var pos = Vec3.multiply(Quat.getFront(q), that.zoomDistance); + var pos = Vec3.multiply(Quat.getForward(q), that.zoomDistance); Camera.setPosition(Vec3.sum(that.focalPoint, pos)); yRot = Quat.angleAxis(that.yaw - 180, { diff --git a/scripts/system/libraries/lightOverlayManager.js b/scripts/system/libraries/entityIconOverlayManager.js similarity index 67% rename from scripts/system/libraries/lightOverlayManager.js rename to scripts/system/libraries/entityIconOverlayManager.js index 2d3618096b..7f7a293bc3 100644 --- a/scripts/system/libraries/lightOverlayManager.js +++ b/scripts/system/libraries/entityIconOverlayManager.js @@ -1,9 +1,6 @@ -var POINT_LIGHT_URL = "http://s3.amazonaws.com/hifi-public/images/tools/point-light.svg"; -var SPOT_LIGHT_URL = "http://s3.amazonaws.com/hifi-public/images/tools/spot-light.svg"; - -LightOverlayManager = function() { - var self = this; +/* globals EntityIconOverlayManager:true */ +EntityIconOverlayManager = function(entityTypes, getOverlayPropertiesFunc) { var visible = false; // List of all created overlays @@ -22,9 +19,16 @@ LightOverlayManager = function() { for (var id in entityIDs) { var entityID = entityIDs[id]; var properties = Entities.getEntityProperties(entityID); - Overlays.editOverlay(entityOverlays[entityID], { + var overlayProperties = { position: properties.position - }); + }; + if (getOverlayPropertiesFunc) { + var customProperties = getOverlayPropertiesFunc(entityID, properties); + for (var key in customProperties) { + overlayProperties[key] = customProperties[key]; + } + } + Overlays.editOverlay(entityOverlays[entityID], overlayProperties); } }; @@ -34,7 +38,7 @@ LightOverlayManager = function() { if (result.intersects) { for (var id in entityOverlays) { - if (result.overlayID == entityOverlays[id]) { + if (result.overlayID === entityOverlays[id]) { result.entityID = entityIDs[id]; found = true; break; @@ -50,7 +54,7 @@ LightOverlayManager = function() { }; this.setVisible = function(isVisible) { - if (visible != isVisible) { + if (visible !== isVisible) { visible = isVisible; for (var id in entityOverlays) { Overlays.editOverlay(entityOverlays[id], { @@ -62,12 +66,13 @@ LightOverlayManager = function() { // Allocate or get an unused overlay function getOverlay() { - if (unusedOverlays.length == 0) { - var overlay = Overlays.addOverlay("image3d", {}); + var overlay; + if (unusedOverlays.length === 0) { + overlay = Overlays.addOverlay("image3d", {}); allOverlays.push(overlay); } else { - var overlay = unusedOverlays.pop(); - }; + overlay = unusedOverlays.pop(); + } return overlay; } @@ -79,24 +84,32 @@ LightOverlayManager = function() { } function addEntity(entityID) { - var properties = Entities.getEntityProperties(entityID); - if (properties.type == "Light" && !(entityID in entityOverlays)) { + var properties = Entities.getEntityProperties(entityID, ['position', 'type']); + if (entityTypes.indexOf(properties.type) > -1 && !(entityID in entityOverlays)) { var overlay = getOverlay(); entityOverlays[entityID] = overlay; entityIDs[entityID] = entityID; - Overlays.editOverlay(overlay, { + var overlayProperties = { position: properties.position, - url: properties.isSpotlight ? SPOT_LIGHT_URL : POINT_LIGHT_URL, rotation: Quat.fromPitchYawRollDegrees(0, 0, 270), visible: visible, alpha: 0.9, scale: 0.5, + drawInFront: true, + isFacingAvatar: true, color: { red: 255, green: 255, blue: 255 } - }); + }; + if (getOverlayPropertiesFunc) { + var customProperties = getOverlayPropertiesFunc(entityID, properties); + for (var key in customProperties) { + overlayProperties[key] = customProperties[key]; + } + } + Overlays.editOverlay(overlay, overlayProperties); } } @@ -130,4 +143,4 @@ LightOverlayManager = function() { Overlays.deleteOverlay(allOverlays[i]); } }); -}; \ No newline at end of file +}; diff --git a/scripts/system/libraries/entityList.js b/scripts/system/libraries/entityList.js index 6dc2486ffb..3488733289 100644 --- a/scripts/system/libraries/entityList.js +++ b/scripts/system/libraries/entityList.js @@ -1,13 +1,22 @@ -var ENTITY_LIST_HTML_URL = Script.resolvePath('../html/entityList.html'); +"use strict"; + +// entityList.js +// +// Copyright 2014 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +/* global EntityListTool, Tablet, selectionManager, Entities, Camera, MyAvatar, Vec3, Menu, Messages, + cameraManager, MENU_EASE_ON_FOCUS, deleteSelectedEntities, toggleSelectedEntitiesLocked, toggleSelectedEntitiesVisible */ EntityListTool = function(opts) { var that = {}; - var url = ENTITY_LIST_HTML_URL; - var webView = new OverlayWebWindow({ - title: 'Entity List', source: url, toolWindow: true - }); - + var webView = null; + webView = Tablet.getTablet("com.highfidelity.interface.tablet.system"); + webView.setVisible = function(value) {}; var filterInView = false; var searchRadius = 100; @@ -25,7 +34,7 @@ EntityListTool = function(opts) { that.toggleVisible = function() { that.setVisible(!visible); - } + }; selectionManager.addEventListener(function() { var selectedIDs = []; @@ -44,7 +53,7 @@ EntityListTool = function(opts) { that.clearEntityList = function () { var data = { type: 'clearEntityList' - } + }; webView.emitScriptEvent(JSON.stringify(data)); }; @@ -86,8 +95,8 @@ EntityListTool = function(opts) { } var selectedIDs = []; - for (var i = 0; i < selectionManager.selections.length; i++) { - selectedIDs.push(selectionManager.selections[i].id); + for (var j = 0; j < selectionManager.selections.length; j++) { + selectedIDs.push(selectionManager.selections[j].id); } var data = { @@ -96,7 +105,7 @@ EntityListTool = function(opts) { selectedIDs: selectedIDs, }; webView.emitScriptEvent(JSON.stringify(data)); - } + }; webView.webEventReceived.connect(function(data) { data = JSON.parse(data); @@ -119,6 +128,18 @@ EntityListTool = function(opts) { if (selectionManager.hasSelection()) { MyAvatar.position = selectionManager.worldPosition; } + } else if (data.type == "export") { + if (!selectionManager.hasSelection()) { + Window.notifyEditError("No entities have been selected."); + } else { + var filename = Window.save("Select Where to Save", "", "*.json"); + if (filename) { + var success = Clipboard.exportEntities(filename, selectionManager.selections); + if (!success) { + Window.notifyEditError("Export failed."); + } + } + } } else if (data.type == "pal") { var sessionIds = {}; // Collect the sessionsIds of all selected entitities, w/o duplicates. selectionManager.selections.forEach(function (id) { @@ -149,11 +170,11 @@ EntityListTool = function(opts) { } }); - webView.visibleChanged.connect(function () { - if (webView.visible) { - that.sendUpdate(); - } - }); + // webView.visibleChanged.connect(function () { + // if (webView.visible) { + // that.sendUpdate(); + // } + // }); return that; }; diff --git a/scripts/system/libraries/entitySelectionTool.js b/scripts/system/libraries/entitySelectionTool.js index 9c1626caf4..79d45d5cd2 100644 --- a/scripts/system/libraries/entitySelectionTool.js +++ b/scripts/system/libraries/entitySelectionTool.js @@ -11,6 +11,8 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // +/* global HIFI_PUBLIC_BUCKET, SPACE_LOCAL, Script, SelectionManager */ + HIFI_PUBLIC_BUCKET = "http://s3.amazonaws.com/hifi-public/"; SPACE_LOCAL = "local"; @@ -28,7 +30,7 @@ SelectionManager = (function() { var that = {}; function subscribeToUpdateMessages() { - Messages.subscribe('entityToolUpdates'); + Messages.subscribe("entityToolUpdates"); Messages.messageReceived.connect(handleEntitySelectionToolUpdates); } @@ -40,8 +42,20 @@ SelectionManager = (function() { return; } - if (message === 'callUpdate') { - that._update(); + var messageParsed; + try { + messageParsed = JSON.parse(message); + } catch (err) { + print("error -- entitySelectionTool got malformed message: " + message); + } + + // if (message === 'callUpdate') { + // that._update(); + // } + + if (messageParsed.method === "selectEntity") { + print("setting selection to " + messageParsed.entityID); + that.setSelections([messageParsed.entityID]); } } @@ -149,13 +163,14 @@ SelectionManager = (function() { }; that._update = function(selectionUpdated) { - if (that.selections.length == 0) { + var properties = null; + if (that.selections.length === 0) { that.localDimensions = null; that.localPosition = null; that.worldDimensions = null; that.worldPosition = null; } else if (that.selections.length == 1) { - var properties = Entities.getEntityProperties(that.selections[0]); + properties = Entities.getEntityProperties(that.selections[0]); that.localDimensions = properties.dimensions; that.localPosition = properties.position; that.localRotation = properties.rotation; @@ -170,7 +185,7 @@ SelectionManager = (function() { that.localDimensions = null; that.localPosition = null; - var properties = Entities.getEntityProperties(that.selections[0]); + properties = Entities.getEntityProperties(that.selections[0]); var brn = properties.boundingBox.brn; var tfl = properties.boundingBox.tfl; @@ -203,9 +218,9 @@ SelectionManager = (function() { SelectionDisplay.setSpaceMode(SPACE_WORLD); } - for (var i = 0; i < listeners.length; i++) { + for (var j = 0; j < listeners.length; j++) { try { - listeners[i](selectionUpdated === true); + listeners[j](selectionUpdated === true); } catch (e) { print("EntitySelectionTool got exception: " + JSON.stringify(e)); } @@ -229,8 +244,8 @@ function getRelativeCenterPosition(dimensions, registrationPoint) { return { x: -dimensions.x * (registrationPoint.x - 0.5), y: -dimensions.y * (registrationPoint.y - 0.5), - z: -dimensions.z * (registrationPoint.z - 0.5), - } + z: -dimensions.z * (registrationPoint.z - 0.5) + }; } SelectionDisplay = (function() { @@ -253,7 +268,7 @@ SelectionDisplay = (function() { var spaceMode = SPACE_LOCAL; var mode = "UNKNOWN"; - var overlayNames = new Array(); + var overlayNames = []; var lastCameraPosition = Camera.getPosition(); var lastCameraOrientation = Camera.getOrientation(); @@ -679,8 +694,7 @@ SelectionDisplay = (function() { green: 0, blue: 0 }, - ignoreRayIntersection: true, // always ignore this - visible: false, + ignoreRayIntersection: true // always ignore this }); var yRailOverlay = Overlays.addOverlay("line3d", { visible: false, @@ -700,8 +714,7 @@ SelectionDisplay = (function() { green: 255, blue: 0 }, - ignoreRayIntersection: true, // always ignore this - visible: false, + ignoreRayIntersection: true // always ignore this }); var zRailOverlay = Overlays.addOverlay("line3d", { visible: false, @@ -721,8 +734,7 @@ SelectionDisplay = (function() { green: 0, blue: 255 }, - ignoreRayIntersection: true, // always ignore this - visible: false, + ignoreRayIntersection: true // always ignore this }); var rotateZeroOverlay = Overlays.addOverlay("line3d", { @@ -1022,30 +1034,13 @@ SelectionDisplay = (function() { that.triggered = true; if (activeHand !== hand) { // No switching while the other is already triggered, so no need to release. - activeHand = (activeHand === Controller.Standard.RightHand) ? Controller.Standard.LeftHand : Controller.Standard.RightHand; + activeHand = (activeHand === Controller.Standard.RightHand) ? + Controller.Standard.LeftHand : Controller.Standard.RightHand; } if (Reticle.pointingAtSystemOverlay || Overlays.getOverlayAtPoint(Reticle.position)) { return; } - var eventResult = that.mousePressEvent({}); - if (!eventResult || (eventResult === 'selectionBox')) { - var pickRay = controllerComputePickRay(); - if (pickRay) { - var entityIntersection = Entities.findRayIntersection(pickRay, true); - - - var overlayIntersection = Overlays.findRayIntersection(pickRay); - if (entityIntersection.intersects && - (!overlayIntersection.intersects || (entityIntersection.distance < overlayIntersection.distance))) { - - if (HMD.tabletID === entityIntersection.entityID) { - return; - } - - selectionManager.setSelections([entityIntersection.entityID]); - } - } - } + that.mousePressEvent({}); } else if (that.triggered && (value < that.TRIGGER_OFF_VALUE)) { that.triggered = false; that.mouseReleaseEvent({}); @@ -1054,6 +1049,8 @@ SelectionDisplay = (function() { } that.triggerMapping.from(Controller.Standard.RT).peek().to(makeTriggerHandler(Controller.Standard.RightHand)); that.triggerMapping.from(Controller.Standard.LT).peek().to(makeTriggerHandler(Controller.Standard.LeftHand)); + + function controllerComputePickRay() { var controllerPose = getControllerWorldLocation(activeHand, true); if (controllerPose.valid && that.triggered) { @@ -1072,7 +1069,7 @@ SelectionDisplay = (function() { onBegin: tool.onBegin, onMove: tool.onMove, onEnd: tool.onEnd, - } + }; } @@ -1080,8 +1077,8 @@ SelectionDisplay = (function() { for (var i = 0; i < allOverlays.length; i++) { Overlays.deleteOverlay(allOverlays[i]); } - for (var i = 0; i < selectionBoxes.length; i++) { - Overlays.deleteOverlay(selectionBoxes[i]); + for (var j = 0; j < selectionBoxes.length; j++) { + Overlays.deleteOverlay(selectionBoxes[j]); } }; @@ -1125,7 +1122,7 @@ SelectionDisplay = (function() { }); that.updateHandles(); - } + }; that.updateRotationHandles = function() { var diagonal = (Vec3.length(selectionManager.worldDimensions) / 2) * 1.1; @@ -1170,14 +1167,14 @@ SelectionDisplay = (function() { // determine which bottom corner we are closest to /*------------------------------ example: - + BRF +--------+ BLF | | | | BRN +--------+ BLN - + * - + ------------------------------*/ var cameraPosition = Camera.getPosition(); @@ -1572,7 +1569,7 @@ SelectionDisplay = (function() { that.unselectAll = function() {}; that.updateHandles = function() { - if (SelectionManager.selections.length == 0) { + if (SelectionManager.selections.length === 0) { that.setOverlaysVisible(false); return; } @@ -1608,7 +1605,8 @@ SelectionDisplay = (function() { var bottom = -registrationPointDimensions.y; var top = dimensions.y - registrationPointDimensions.y; var near = -registrationPointDimensions.z; - var front = far = dimensions.z - registrationPointDimensions.z; + var far = dimensions.z - registrationPointDimensions.z; + var front = far; var worldTop = SelectionManager.worldDimensions.y / 2; @@ -1808,9 +1806,9 @@ SelectionDisplay = (function() { if (selectionManager.selections.length == 1) { var properties = Entities.getEntityProperties(selectionManager.selections[0]); - if (properties.type == "Light" && properties.isSpotlight == true) { - var stretchHandlesVisible = false; - var extendedStretchHandlesVisible = false; + if (properties.type == "Light" && properties.isSpotlight) { + stretchHandlesVisible = false; + extendedStretchHandlesVisible = false; Overlays.editOverlay(grabberSpotLightCenter, { position: position, @@ -1903,9 +1901,9 @@ SelectionDisplay = (function() { Overlays.editOverlay(grabberPointLightN, { visible: false }); - } else if (properties.type == "Light" && properties.isSpotlight == false) { - var stretchHandlesVisible = false; - var extendedStretchHandlesVisible = false; + } else if (properties.type == "Light" && !properties.isSpotlight) { + stretchHandlesVisible = false; + extendedStretchHandlesVisible = false; Overlays.editOverlay(grabberPointLightT, { position: TOP, rotation: rotation, @@ -2171,28 +2169,32 @@ SelectionDisplay = (function() { })); } - var i = 0; + i = 0; // Only show individual selections boxes if there is more than 1 selection if (selectionManager.selections.length > 1) { for (; i < selectionManager.selections.length; i++) { - var properties = Entities.getEntityProperties(selectionManager.selections[i]); + var props = Entities.getEntityProperties(selectionManager.selections[i]); // Adjust overlay position to take registrationPoint into account // centeredRP = registrationPoint with range [-0.5, 0.5] - var centeredRP = Vec3.subtract(properties.registrationPoint, { + var centeredRP = Vec3.subtract(props.registrationPoint, { x: 0.5, y: 0.5, z: 0.5 }); - var offset = vec3Mult(properties.dimensions, centeredRP); + var offset = vec3Mult(props.dimensions, centeredRP); offset = Vec3.multiply(-1, offset); - offset = Vec3.multiplyQbyV(properties.rotation, offset); - var boxPosition = Vec3.sum(properties.position, offset); + offset = Vec3.multiplyQbyV(props.rotation, offset); + var boxPosition = Vec3.sum(props.position, offset); + + var color = {red: 255, green: 128, blue: 0}; + if (i >= selectionManager.selections.length - 1) color = {red: 255, green: 255, blue: 64}; Overlays.editOverlay(selectionBoxes[i], { position: boxPosition, - rotation: properties.rotation, - dimensions: properties.dimensions, + color: color, + rotation: props.rotation, + dimensions: props.dimensions, visible: true, }); } @@ -2395,7 +2397,7 @@ SelectionDisplay = (function() { if (wantDebug) { print("Start Elevation: " + translateXZTool.startingElevation + ", elevation: " + elevation); } - if ((translateXZTool.startingElevation > 0.0 && elevation < MIN_ELEVATION) || + if ((translateXZTool.startingElevation > 0.0 && elevation < MIN_ELEVATION) || (translateXZTool.startingElevation < 0.0 && elevation > -MIN_ELEVATION)) { if (wantDebug) { print("too close to horizon!"); @@ -2511,7 +2513,7 @@ SelectionDisplay = (function() { onBegin: function(event) { pickRay = generalComputePickRay(event.x, event.y); - upDownPickNormal = Quat.getFront(lastCameraOrientation); + upDownPickNormal = Quat.getForward(lastCameraOrientation); // Remove y component so the y-axis lies along the plane we picking on - this will // give movements that follow the mouse. upDownPickNormal.y = 0; @@ -2584,11 +2586,11 @@ SelectionDisplay = (function() { y: v1.y * v2.y, z: v1.z * v2.z }; - } - // stretchMode - name of mode - // direction - direction to stretch in - // pivot - point to use as a pivot - // offset - the position of the overlay tool relative to the selections center position + }; + // stretchMode - name of mode + // direction - direction to stretch in + // pivot - point to use as a pivot + // offset - the position of the overlay tool relative to the selections center position var makeStretchTool = function(stretchMode, direction, pivot, offset, customOnMove) { var signs = { x: direction.x < 0 ? -1 : (direction.x > 0 ? 1 : 0), @@ -2640,7 +2642,7 @@ SelectionDisplay = (function() { }); // Scale pivot to be in the same range as registrationPoint - var scaledPivot = Vec3.multiply(0.5, pivot) + var scaledPivot = Vec3.multiply(0.5, pivot); deltaPivot = Vec3.subtract(centeredRP, scaledPivot); var scaledOffset = Vec3.multiply(0.5, offset); @@ -2652,14 +2654,16 @@ SelectionDisplay = (function() { var scaledOffsetWorld = vec3Mult(initialDimensions, offsetRP); pickRayPosition = Vec3.sum(initialPosition, Vec3.multiplyQbyV(rotation, scaledOffsetWorld)); + var start = null; + var end = null; if (numDimensions == 1 && mask.x) { - var start = Vec3.multiplyQbyV(rotation, { + start = Vec3.multiplyQbyV(rotation, { x: -10000, y: 0, z: 0 }); start = Vec3.sum(start, properties.position); - var end = Vec3.multiplyQbyV(rotation, { + end = Vec3.multiplyQbyV(rotation, { x: 10000, y: 0, z: 0 @@ -2672,13 +2676,13 @@ SelectionDisplay = (function() { }); } if (numDimensions == 1 && mask.y) { - var start = Vec3.multiplyQbyV(rotation, { + start = Vec3.multiplyQbyV(rotation, { x: 0, y: -10000, z: 0 }); start = Vec3.sum(start, properties.position); - var end = Vec3.multiplyQbyV(rotation, { + end = Vec3.multiplyQbyV(rotation, { x: 0, y: 10000, z: 0 @@ -2691,13 +2695,13 @@ SelectionDisplay = (function() { }); } if (numDimensions == 1 && mask.z) { - var start = Vec3.multiplyQbyV(rotation, { + start = Vec3.multiplyQbyV(rotation, { x: 0, y: 0, z: -10000 }); start = Vec3.sum(start, properties.position); - var end = Vec3.multiplyQbyV(rotation, { + end = Vec3.multiplyQbyV(rotation, { x: 0, y: 0, z: 10000 @@ -2730,13 +2734,13 @@ SelectionDisplay = (function() { }; } } else if (numDimensions == 2) { - if (mask.x == 0) { + if (mask.x === 0) { planeNormal = { x: 1, y: 0, z: 0 }; - } else if (mask.y == 0) { + } else if (mask.y === 0) { planeNormal = { x: 0, y: 1, @@ -2894,7 +2898,7 @@ SelectionDisplay = (function() { }); SelectionManager._update(); - }; + } function radiusStretchFunc(vector, change) { var props = selectionManager.savedProperties[selectionManager.selections[0]]; @@ -3857,7 +3861,7 @@ SelectionDisplay = (function() { }; that.mousePressEvent = function(event) { - var wantDebug = false; + var wantDebug = false; if (!event.isLeftButton && !that.triggered) { // if another mouse button than left is pressed ignore it return false; @@ -3885,11 +3889,9 @@ SelectionDisplay = (function() { Overlays.editOverlay(rollHandle, { ignoreRayIntersection: true }); - var result = Overlays.findRayIntersection(pickRay); + result = Overlays.findRayIntersection(pickRay); if (result.intersects) { - - if (wantDebug) { print("something intersects... "); print(" result.overlayID:" + result.overlayID + "[" + overlayNames[result.overlayID] + "]"); @@ -3989,7 +3991,7 @@ SelectionDisplay = (function() { if (wantDebug) { print("rotate handle case..."); } - + // After testing our stretch handles, then check out rotate handles Overlays.editOverlay(yawHandle, { @@ -4211,7 +4213,7 @@ SelectionDisplay = (function() { case selectionBox: activeTool = translateXZTool; translateXZTool.pickPlanePosition = result.intersection; - translateXZTool.greatestDimension = Math.max(Math.max(SelectionManager.worldDimensions.x, SelectionManager.worldDimensions.y), + translateXZTool.greatestDimension = Math.max(Math.max(SelectionManager.worldDimensions.x, SelectionManager.worldDimensions.y), SelectionManager.worldDimensions.z); if (wantDebug) { print("longest dimension: " + translateXZTool.greatestDimension); @@ -4220,7 +4222,7 @@ SelectionDisplay = (function() { translateXZTool.startingElevation = translateXZTool.elevation(pickRay.origin, translateXZTool.pickPlanePosition); print(" starting elevation: " + translateXZTool.startingElevation); } - + mode = translateXZTool.mode; activeTool.onBegin(event); somethingClicked = 'selectionBox'; @@ -4419,7 +4421,7 @@ SelectionDisplay = (function() { scale: handleSize / 1.25, }); } - } + }; Script.update.connect(that.updateHandleSizes); that.mouseReleaseEvent = function(event) { diff --git a/scripts/system/libraries/gridTool.js b/scripts/system/libraries/gridTool.js index c002aec3b1..0290674a0f 100644 --- a/scripts/system/libraries/gridTool.js +++ b/scripts/system/libraries/gridTool.js @@ -228,10 +228,9 @@ GridTool = function(opts) { var verticalGrid = opts.verticalGrid; var listeners = []; - var url = GRID_CONTROLS_HTML_URL; - var webView = new OverlayWebWindow({ - title: 'Grid', source: url, toolWindow: true - }); + var webView = null; + webView = Tablet.getTablet("com.highfidelity.interface.tablet.system"); + webView.setVisible = function(value) {}; horizontalGrid.addListener(function(data) { webView.emitScriptEvent(JSON.stringify(data)); diff --git a/scripts/system/libraries/soundArray.js b/scripts/system/libraries/soundArray.js index f59c88a723..7e5da11948 100644 --- a/scripts/system/libraries/soundArray.js +++ b/scripts/system/libraries/soundArray.js @@ -36,7 +36,7 @@ SoundArray = function(audioOptions, autoUpdateAudioPosition) { }; this.updateAudioPosition = function() { var position = MyAvatar.position; - var forwardVector = Quat.getFront(MyAvatar.orientation); + var forwardVector = Quat.getForward(MyAvatar.orientation); this.audioOptions.position = Vec3.sum(position, forwardVector); }; }; diff --git a/scripts/system/libraries/toolBars.js b/scripts/system/libraries/toolBars.js index e49f8c4004..351f10e7bd 100644 --- a/scripts/system/libraries/toolBars.js +++ b/scripts/system/libraries/toolBars.js @@ -160,6 +160,7 @@ ToolBar = function(x, y, direction, optionalPersistenceKey, optionalInitialPosit visible: false }); this.spacing = []; + this.onMove = null; this.addTool = function(properties, selectable, selected) { if (direction == ToolBar.HORIZONTAL) { @@ -254,6 +255,9 @@ ToolBar = function(x, y, direction, optionalPersistenceKey, optionalInitialPosit y: y - ToolBar.SPACING }); } + if (this.onMove !== null) { + this.onMove(x, y, dx, dy); + }; } this.setAlpha = function(alpha, tool) { diff --git a/scripts/system/makeUserConnection.js b/scripts/system/makeUserConnection.js new file mode 100644 index 0000000000..674da2d677 --- /dev/null +++ b/scripts/system/makeUserConnection.js @@ -0,0 +1,864 @@ +"use strict"; +// +// makeUserConnetion.js +// scripts/system +// +// Created by David Kelly on 3/7/2017. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// +(function() { // BEGIN LOCAL_SCOPE + +const label = "makeUserConnection"; +const MAX_AVATAR_DISTANCE = 0.2; // m +const GRIP_MIN = 0.05; // goes from 0-1, so 5% pressed is pressed +const MESSAGE_CHANNEL = "io.highfidelity.makeUserConnection"; +const STATES = { + inactive : 0, + waiting: 1, + connecting: 2, + makingConnection: 3 +}; +const STATE_STRINGS = ["inactive", "waiting", "connecting", "makingConnection"]; +const WAITING_INTERVAL = 100; // ms +const CONNECTING_INTERVAL = 100; // ms +const MAKING_CONNECTION_TIMEOUT = 800; // ms +const CONNECTING_TIME = 1600; // ms +const PARTICLE_RADIUS = 0.15; // m +const PARTICLE_ANGLE_INCREMENT = 360/45; // 1hz +const HANDSHAKE_SOUND_URL = "https://s3-us-west-1.amazonaws.com/hifi-content/davidkelly/production/audio/4beat_sweep.wav"; +const SUCCESSFUL_HANDSHAKE_SOUND_URL = "https://s3-us-west-1.amazonaws.com/hifi-content/davidkelly/production/audio/3rdbeat_success_bell.wav"; +const HAPTIC_DATA = { + initial: { duration: 20, strength: 0.6}, // duration is in ms + background: { duration: 100, strength: 0.3 }, // duration is in ms + success: { duration: 60, strength: 1.0} // duration is in ms +}; +const PARTICLE_EFFECT_PROPS = { + "alpha": 0.8, + "azimuthFinish": Math.PI, + "azimuthStart": -1*Math.PI, + "emitRate": 500, + "emitSpeed": 0.0, + "emitterShouldTrail": 1, + "isEmitting": 1, + "lifespan": 3, + "maxParticles": 1000, + "particleRadius": 0.003, + "polarStart": 1, + "polarFinish": 1, + "radiusFinish": 0.008, + "radiusStart": 0.0025, + "speedSpread": 0.025, + "textures": "http://hifi-content.s3.amazonaws.com/alan/dev/Particles/Bokeh-Particle.png", + "color": {"red": 255, "green": 255, "blue": 255}, + "colorFinish": {"red": 0, "green": 164, "blue": 255}, + "colorStart": {"red": 255, "green": 255, "blue": 255}, + "emitOrientation": {"w": -0.71, "x":0.0, "y":0.0, "z": 0.71}, + "emitAcceleration": {"x": 0.0, "y": 0.0, "z": 0.0}, + "accelerationSpread": {"x": 0.0, "y": 0.0, "z": 0.0}, + "dimensions": {"x":0.05, "y": 0.05, "z": 0.05}, + "type": "ParticleEffect" +}; +const MAKING_CONNECTION_PARTICLE_PROPS = { + "alpha": 0.07, + "alphaStart":0.011, + "alphaSpread": 0, + "alphaFinish": 0, + "azimuthFinish": Math.PI, + "azimuthStart": -1*Math.PI, + "emitRate": 2000, + "emitSpeed": 0.0, + "emitterShouldTrail": 1, + "isEmitting": 1, + "lifespan": 3.6, + "maxParticles": 4000, + "particleRadius": 0.048, + "polarStart": 0, + "polarFinish": 1, + "radiusFinish": 0.3, + "radiusStart": 0.04, + "speedSpread": 0.01, + "radiusSpread": 0.9, + "textures": "http://hifi-content.s3.amazonaws.com/alan/dev/Particles/Bokeh-Particle.png", + "color": {"red": 200, "green": 170, "blue": 255}, + "colorFinish": {"red": 0, "green": 134, "blue": 255}, + "colorStart": {"red": 185, "green": 222, "blue": 255}, + "emitOrientation": {"w": -0.71, "x":0.0, "y":0.0, "z": 0.71}, + "emitAcceleration": {"x": 0.0, "y": 0.0, "z": 0.0}, + "accelerationSpread": {"x": 0.0, "y": 0.0, "z": 0.0}, + "dimensions": {"x":0.05, "y": 0.05, "z": 0.05}, + "type": "ParticleEffect" +}; + +var currentHand; +var state = STATES.inactive; +var connectingInterval; +var waitingInterval; +var makingConnectionTimeout; +var animHandlerId; +var connectingId; +var connectingHand; +var waitingList = {}; +var particleEffect; +var waitingBallScale; +var particleRotationAngle = 0.0; +var makingConnectionParticleEffect; +var makingConnectionEmitRate = 2000; +var particleEmitRate = 500; +var handshakeInjector; +var successfulHandshakeInjector; +var handshakeSound; +var successfulHandshakeSound; + +function debug() { + var stateString = "<" + STATE_STRINGS[state] + ">"; + var connecting = "[" + connectingId + "/" + connectingHand + "]"; + print.apply(null, [].concat.apply([label, stateString, JSON.stringify(waitingList), connecting], [].map.call(arguments, JSON.stringify))); +} + +function cleanId(guidWithCurlyBraces) { + return guidWithCurlyBraces.slice(1, -1); +} +function request(options, callback) { // cb(error, responseOfCorrectContentType) of url. A subset of npm request. + var httpRequest = new XMLHttpRequest(), key; + // QT bug: apparently doesn't handle onload. Workaround using readyState. + httpRequest.onreadystatechange = function () { + var READY_STATE_DONE = 4; + var HTTP_OK = 200; + if (httpRequest.readyState >= READY_STATE_DONE) { + var error = (httpRequest.status !== HTTP_OK) && httpRequest.status.toString() + ':' + httpRequest.statusText, + response = !error && httpRequest.responseText, + contentType = !error && httpRequest.getResponseHeader('content-type'); + if (!error && contentType.indexOf('application/json') === 0) { // ignoring charset, etc. + try { + response = JSON.parse(response); + } catch (e) { + error = e; + } + } + if (error) { + response = {statusCode: httpRequest.status}; + } + callback(error, response); + } + }; + if (typeof options === 'string') { + options = {uri: options}; + } + if (options.url) { + options.uri = options.url; + } + if (!options.method) { + options.method = 'GET'; + } + if (options.body && (options.method === 'GET')) { // add query parameters + var params = [], appender = (-1 === options.uri.search('?')) ? '?' : '&'; + for (key in options.body) { + params.push(key + '=' + options.body[key]); + } + options.uri += appender + params.join('&'); + delete options.body; + } + if (options.json) { + options.headers = options.headers || {}; + options.headers["Content-type"] = "application/json"; + options.body = JSON.stringify(options.body); + } + for (key in options.headers || {}) { + httpRequest.setRequestHeader(key, options.headers[key]); + } + httpRequest.open(options.method, options.uri, true); + httpRequest.send(options.body); +} + +function handToString(hand) { + if (hand === Controller.Standard.RightHand) { + return "RightHand"; + } else if (hand === Controller.Standard.LeftHand) { + return "LeftHand"; + } + debug("handToString called without valid hand!"); + return ""; +} + +function stringToHand(hand) { + if (hand == "RightHand") { + return Controller.Standard.RightHand; + } else if (hand == "LeftHand") { + return Controller.Standard.LeftHand; + } + debug("stringToHand called with bad hand string:", hand); + return 0; +} + +function handToHaptic(hand) { + if (hand === Controller.Standard.RightHand) { + return 1; + } else if (hand === Controller.Standard.LeftHand) { + return 0; + } + debug("handToHaptic called without a valid hand!"); + return -1; +} + +function stopWaiting() { + if (waitingInterval) { + waitingInterval = Script.clearInterval(waitingInterval); + } +} + +function stopConnecting() { + if (connectingInterval) { + connectingInterval = Script.clearInterval(connectingInterval); + } +} + +function stopMakingConnection() { + if (makingConnectionTimeout) { + makingConnectionTimeout = Script.clearTimeout(makingConnectionTimeout); + } +} + +// This returns the position of the palm, really. Which relies on the avatar +// having the expected middle1 joint. TODO: fallback for when this isn't part +// of the avatar? +function getHandPosition(avatar, hand) { + if (!hand) { + debug("calling getHandPosition with no hand! (returning avatar position but this is a BUG)"); + debug(new Error().stack); + return avatar.position; + } + var jointName = handToString(hand) + "Middle1"; + return avatar.getJointPosition(avatar.getJointIndex(jointName)); +} + +function shakeHandsAnimation(animationProperties) { + // all we are doing here is moving the right hand to a spot + // that is in front of and a bit above the hips. Basing how + // far in front as scaling with the avatar's height (say hips + // to head distance) + var headIndex = MyAvatar.getJointIndex("Head"); + var offset = 0.5; // default distance of hand in front of you + var result = {}; + if (headIndex) { + offset = 0.8 * MyAvatar.getAbsoluteJointTranslationInObjectFrame(headIndex).y; + } + var handPos = Vec3.multiply(offset, {x: -0.25, y: 0.8, z: 1.3}); + result.rightHandPosition = handPos; + result.rightHandRotation = Quat.fromPitchYawRollDegrees(90, 0, 90); + return result; +} + +function positionFractionallyTowards(posA, posB, frac) { + return Vec3.sum(posA, Vec3.multiply(frac, Vec3.subtract(posB, posA))); +} + +function deleteParticleEffect() { + if (particleEffect) { + particleEffect = Entities.deleteEntity(particleEffect); + } +} + +function deleteMakeConnectionParticleEffect() { + if (makingConnectionParticleEffect) { + makingConnectionParticleEffect = Entities.deleteEntity(makingConnectionParticleEffect); + } +} + +function stopHandshakeSound() { + if (handshakeInjector) { + handshakeInjector.stop(); + handshakeInjector = null; + } +} + +function calcParticlePos(myHand, otherHand, otherOrientation, reset) { + if (reset) { + particleRotationAngle = 0.0; + } + var position = positionFractionallyTowards(myHand, otherHand, 0.5); + particleRotationAngle += PARTICLE_ANGLE_INCREMENT; // about 0.5 hz + var radius = Math.min(PARTICLE_RADIUS, PARTICLE_RADIUS * particleRotationAngle / 360); + var axis = Vec3.mix(Quat.getFront(MyAvatar.orientation), Quat.inverse(Quat.getFront(otherOrientation)), 0.5); + return Vec3.sum(position, Vec3.multiplyQbyV(Quat.angleAxis(particleRotationAngle, axis), {x: 0, y: radius, z: 0})); +} + +// this is called frequently, but usually does nothing +function updateVisualization() { + if (state == STATES.inactive) { + deleteParticleEffect(); + deleteMakeConnectionParticleEffect(); + // this should always be true if inactive, but just in case: + currentHand = undefined; + return; + } + + var myHandPosition = getHandPosition(MyAvatar, currentHand); + var otherHand; + var otherOrientation; + if (connectingId) { + var other = AvatarList.getAvatar(connectingId); + if (other) { + otherOrientation = other.orientation; + otherHand = getHandPosition(other, stringToHand(connectingHand)); + } + } + + var wrist = MyAvatar.getJointPosition(MyAvatar.getJointIndex(handToString(currentHand))); + var d = Math.min(MAX_AVATAR_DISTANCE, Vec3.distance(wrist, myHandPosition)); + switch (state) { + case STATES.waiting: + // no visualization while waiting + deleteParticleEffect(); + deleteMakeConnectionParticleEffect(); + stopHandshakeSound(); + break; + case STATES.connecting: + var particleProps = {}; + // put the position between the 2 hands, if we have a connectingId. This + // helps define the plane in which the particles move. + positionFractionallyTowards(myHandPosition, otherHand, 0.5); + // now manage the rest of the entity + if (!particleEffect) { + particleRotationAngle = 0.0; + particleEmitRate = 500; + particleProps = PARTICLE_EFFECT_PROPS; + particleProps.isEmitting = 0; + particleProps.position = calcParticlePos(myHandPosition, otherHand, otherOrientation); + particleProps.parentID = MyAvatar.sessionUUID; + particleEffect = Entities.addEntity(particleProps, true); + } else { + particleProps.position = calcParticlePos(myHandPosition, otherHand, otherOrientation); + particleProps.isEmitting = 1; + Entities.editEntity(particleEffect, particleProps); + } + if (!makingConnectionParticleEffect) { + var props = MAKING_CONNECTION_PARTICLE_PROPS; + props.parentID = MyAvatar.sessionUUID; + makingConnectionEmitRate = 2000; + props.emitRate = makingConnectionEmitRate; + props.position = myHandPosition; + makingConnectionParticleEffect = Entities.addEntity(props, true); + } else { + makingConnectionEmitRate *= 0.5; + Entities.editEntity(makingConnectionParticleEffect, {emitRate: makingConnectionEmitRate, position: myHandPosition, isEmitting: 1}); + } + break; + case STATES.makingConnection: + particleEmitRate = Math.max(50, particleEmitRate * 0.5); + Entities.editEntity(makingConnectionParticleEffect, {emitRate: 0, isEmitting: 0, position: myHandPosition}); + Entities.editEntity(particleEffect, {position: calcParticlePos(myHandPosition, otherHand, otherOrientation), emitRate: particleEmitRate}); + break; + default: + debug("unexpected state", state); + break; + } +} + +function isNearby(id, hand) { + if (currentHand) { + var handPos = getHandPosition(MyAvatar, currentHand); + var avatar = AvatarList.getAvatar(id); + if (avatar) { + var otherHand = stringToHand(hand); + var distance = Vec3.distance(getHandPosition(avatar, otherHand), handPos); + return (distance < MAX_AVATAR_DISTANCE); + } + } + return false; +} + +function findNearestWaitingAvatar() { + var handPos = getHandPosition(MyAvatar, currentHand); + var minDistance = MAX_AVATAR_DISTANCE; + var nearestAvatar = {}; + Object.keys(waitingList).forEach(function (identifier) { + var avatar = AvatarList.getAvatar(identifier); + if (avatar) { + var hand = stringToHand(waitingList[identifier]); + var distance = Vec3.distance(getHandPosition(avatar, hand), handPos); + if (distance < minDistance) { + minDistance = distance; + nearestAvatar = {avatar: identifier, hand: hand}; + } + } + }); + return nearestAvatar; +} + + +// As currently implemented, we select the closest waiting avatar (if close enough) and send +// them a connectionRequest. If nobody is close enough we send a waiting message, and wait for a +// connectionRequest. If the 2 people who want to connect are both somewhat out of range when they +// initiate the shake, they will race to see who sends the connectionRequest after noticing the +// waiting message. Either way, they will start connecting eachother at that point. +function startHandshake(fromKeyboard) { + if (fromKeyboard) { + debug("adding animation"); + // just in case order of press/unpress is broken + if (animHandlerId) { + animHandlerId = MyAvatar.removeAnimationStateHandler(animHandlerId); + } + animHandlerId = MyAvatar.addAnimationStateHandler(shakeHandsAnimation, []); + } + debug("starting handshake for", currentHand); + pollCount = 0; + state = STATES.waiting; + connectingId = undefined; + connectingHand = undefined; + // just in case + stopWaiting(); + stopConnecting(); + stopMakingConnection(); + + var nearestAvatar = findNearestWaitingAvatar(); + if (nearestAvatar.avatar) { + connectingId = nearestAvatar.avatar; + connectingHand = handToString(nearestAvatar.hand); + debug("sending connectionRequest to", connectingId); + messageSend({ + key: "connectionRequest", + id: connectingId, + hand: handToString(currentHand) + }); + } else { + // send waiting message + debug("sending waiting message"); + messageSend({ + key: "waiting", + hand: handToString(currentHand) + }); + lookForWaitingAvatar(); + } +} + +function endHandshake() { + debug("ending handshake for", currentHand); + + deleteParticleEffect(); + deleteMakeConnectionParticleEffect(); + currentHand = undefined; + // note that setting the state to inactive should really + // only be done here, unless we change how the triggering works, + // as we ignore the key release event when inactive. See updateTriggers + // below. + state = STATES.inactive; + connectingId = undefined; + connectingHand = undefined; + stopWaiting(); + stopConnecting(); + stopMakingConnection(); + stopHandshakeSound(); + // send done to let connection know you are not making connections now + messageSend({ + key: "done" + }); + + if (animHandlerId) { + debug("removing animation"); + MyAvatar.removeAnimationStateHandler(animHandlerId); + } + // No-op if we were successful, but this way we ensure that failures and abandoned handshakes don't leave us in a weird state. + request({uri: requestUrl, method: 'DELETE'}, debug); +} + +function updateTriggers(value, fromKeyboard, hand) { + if (currentHand && hand !== currentHand) { + debug("currentHand", currentHand, "ignoring messages from", hand); + return; + } + if (!currentHand) { + currentHand = hand; + } + // ok now, we are either initiating or quitting... + var isGripping = value > GRIP_MIN; + if (isGripping) { + debug("updateTriggers called - gripping", handToString(hand)); + if (state != STATES.inactive) { + return; + } else { + startHandshake(fromKeyboard); + } + } else { + // TODO: should we end handshake even when inactive? Ponder + debug("updateTriggers called -- no longer gripping", handToString(hand)); + if (state != STATES.inactive) { + endHandshake(); + } else { + return; + } + } +} + +function messageSend(message) { + Messages.sendMessage(MESSAGE_CHANNEL, JSON.stringify(message)); +} + +function lookForWaitingAvatar() { + // we started with nobody close enough, but maybe I've moved + // or they did. Note that 2 people doing this race, so stop + // as soon as you have a connectingId (which means you got their + // message before noticing they were in range in this loop) + + // just in case we reenter before stopping + stopWaiting(); + debug("started looking for waiting avatars"); + waitingInterval = Script.setInterval(function () { + if (state == STATES.waiting && !connectingId) { + // find the closest in-range avatar, and send connection request + var nearestAvatar = findNearestWaitingAvatar(); + if (nearestAvatar.avatar) { + connectingId = nearestAvatar.avatar; + connectingHand = handToString(nearestAvatar.hand); + debug("sending connectionRequest to", connectingId); + messageSend({ + key: "connectionRequest", + id: connectingId, + hand: handToString(currentHand) + }); + } + } else { + // something happened, stop looking for avatars to connect + stopWaiting(); + debug("stopped looking for waiting avatars"); + } + }, WAITING_INTERVAL); +} + +/* There is a mini-state machine after entering STATES.makingConnection. + We make a request (which might immediately succeed, fail, or neither. + If we immediately fail, we tell the user. + Otherwise, we wait MAKING_CONNECTION_TIMEOUT. At that time, we poll until success or fail. + */ +var result, requestBody, pollCount = 0, requestUrl = location.metaverseServerUrl + '/api/v1/user/connection_request'; +function connectionRequestCompleted() { // Final result is in. Do effects. + if (result.status === 'success') { // set earlier + if (!successfulHandshakeInjector) { + successfulHandshakeInjector = Audio.playSound(successfulHandshakeSound, {position: getHandPosition(MyAvatar, currentHand), volume: 0.5, localOnly: true}); + } else { + successfulHandshakeInjector.restart(); + } + Controller.triggerHapticPulse(HAPTIC_DATA.success.strength, HAPTIC_DATA.success.duration, handToHaptic(currentHand)); + // don't change state (so animation continues while gripped) + // but do send a notification, by calling the slot that emits the signal for it + Window.makeConnection(true, result.connection.new_connection ? "You and " + result.connection.username + " are now connected!" : result.connection.username); + UserActivityLogger.makeUserConnection(connectingId, true, result.connection.new_connection ? "new connection" : "already connected"); + return; + } // failed + endHandshake(); + debug("failing with result data", result); + // IWBNI we also did some fail sound/visual effect. + Window.makeConnection(false, result.connection); + UserActivityLogger.makeUserConnection(connectingId, false, result.connection); +} +var POLL_INTERVAL_MS = 200, POLL_LIMIT = 5; +function handleConnectionResponseAndMaybeRepeat(error, response) { + // If response is 'pending', set a short timeout to try again. + // If we fail other than pending, set result and immediately call connectionRequestCompleted. + // If we succceed, set result and call connectionRequestCompleted immediately (if we've been polling), and otherwise on a timeout. + if (response && (response.connection === 'pending')) { + debug(response, 'pollCount', pollCount); + if (pollCount++ >= POLL_LIMIT) { // server will expire, but let's not wait that long. + debug('POLL LIMIT REACHED; TIMEOUT: expired message generated by CLIENT'); + result = {status: 'error', connection: 'expired'}; + connectionRequestCompleted(); + } else { // poll + Script.setTimeout(function () { + request({ + uri: requestUrl, + // N.B.: server gives bad request if we specify json content type, so don't do that. + body: requestBody + }, handleConnectionResponseAndMaybeRepeat); + }, POLL_INTERVAL_MS); + } + } else if (error || (response.status !== 'success')) { + debug('server fail', error, response.status); + if (response && (response.statusCode === 401)) { + error = "All participants must be logged in to connect."; + } + result = error ? {status: 'error', connection: error} : response; + UserActivityLogger.makeUserConnection(connectingId, false, error || response); + connectionRequestCompleted(); + } else { + debug('server success', result); + result = response; + if (pollCount++) { + connectionRequestCompleted(); + } else { // Wait for other guy, so that final succcess is at roughly the same time. + Script.setTimeout(connectionRequestCompleted, MAKING_CONNECTION_TIMEOUT); + } + } +} + +// this should be where we make the appropriate connection call. For now just make the +// visualization change. +function makeConnection(id) { + // send done to let the connection know you have made connection. + messageSend({ + key: "done", + connectionId: id + }); + + state = STATES.makingConnection; + + // continue the haptic background until the timeout fires. When we make calls, we will have an interval + // probably, in which we do this. + Controller.triggerHapticPulse(HAPTIC_DATA.background.strength, MAKING_CONNECTION_TIMEOUT, handToHaptic(currentHand)); + requestBody = {node_id: cleanId(MyAvatar.sessionUUID), proposed_node_id: cleanId(id)}; // for use when repeating + + // It would be "simpler" to skip this and just look at the response, but: + // 1. We don't want to bother the metaverse with request that we know will fail. + // 2. We don't want our code here to be dependent on precisely how the metaverse responds (400, 401, etc.) + if (!Account.isLoggedIn()) { + handleConnectionResponseAndMaybeRepeat("401:Unauthorized", {statusCode: 401}); + return; + } + + // This will immediately set response if successfull (e.g., the other guy got his request in first), or immediate failure, + // and will otherwise poll (using the requestBody we just set). + request({ // + uri: requestUrl, + method: 'POST', + json: true, + body: {user_connection_request: requestBody} + }, handleConnectionResponseAndMaybeRepeat); +} + +// we change states, start the connectionInterval where we check +// to be sure the hand is still close enough. If not, we terminate +// the interval, go back to the waiting state. If we make it +// the entire CONNECTING_TIME, we make the connection. +function startConnecting(id, hand) { + var count = 0; + debug("connecting", id, "hand", hand); + // do we need to do this? + connectingId = id; + connectingHand = hand; + state = STATES.connecting; + + // play sound + if (!handshakeInjector) { + handshakeInjector = Audio.playSound(handshakeSound, {position: getHandPosition(MyAvatar, currentHand), volume: 0.5, localOnly: true}); + } else { + handshakeInjector.restart(); + } + + // send message that we are connecting with them + messageSend({ + key: "connecting", + id: id, + hand: handToString(currentHand) + }); + Controller.triggerHapticPulse(HAPTIC_DATA.initial.strength, HAPTIC_DATA.initial.duration, handToHaptic(currentHand)); + + connectingInterval = Script.setInterval(function () { + count += 1; + Controller.triggerHapticPulse(HAPTIC_DATA.background.strength, HAPTIC_DATA.background.duration, handToHaptic(currentHand)); + if (state != STATES.connecting) { + debug("stopping connecting interval, state changed"); + stopConnecting(); + } else if (!isNearby(id, hand)) { + // gotta go back to waiting + debug(id, "moved, back to waiting"); + stopConnecting(); + messageSend({ + key: "done" + }); + startHandshake(); + } else if (count > CONNECTING_TIME/CONNECTING_INTERVAL) { + debug("made connection with " + id); + makeConnection(id); + stopConnecting(); + } + }, CONNECTING_INTERVAL); +} +/* +A simple sequence diagram: NOTE that the ConnectionAck is somewhat +vestigial, and probably should be removed shortly. + + Avatar A Avatar B + | | + | <-----(waiting) ----- startHandshake +startHandshake - (connectionRequest) -> | + | | + | <----(connectionAck) -------- | + | <-----(connecting) -- startConnecting + startConnecting ---(connecting) ----> | + | | + | connected + connected | + | <--------- (done) ---------- | + | ---------- (done) ---------> | +*/ +function messageHandler(channel, messageString, senderID) { + if (channel !== MESSAGE_CHANNEL) { + return; + } + if (MyAvatar.sessionUUID === senderID) { // ignore my own + return; + } + var message = {}; + try { + message = JSON.parse(messageString); + } catch (e) { + debug(e); + } + switch (message.key) { + case "waiting": + // add this guy to waiting object. Any other message from this person will + // remove it from the list + waitingList[senderID] = message.hand; + break; + case "connectionRequest": + delete waitingList[senderID]; + if (state == STATES.waiting && message.id == MyAvatar.sessionUUID && (!connectingId || connectingId == senderID)) { + // you were waiting for a connection request, so send the ack. Or, you and the other + // guy raced and both send connectionRequests. Handle that too + connectingId = senderID; + connectingHand = message.hand; + messageSend({ + key: "connectionAck", + id: senderID, + hand: handToString(currentHand) + }); + } else { + if (state == STATES.waiting && connectingId == senderID) { + // the person you are trying to connect sent a request to someone else. See the + // if statement above. So, don't cry, just start the handshake over again + startHandshake(); + } + } + break; + case "connectionAck": + delete waitingList[senderID]; + if (state == STATES.waiting && (!connectingId || connectingId == senderID)) { + if (message.id == MyAvatar.sessionUUID) { + // start connecting... + connectingId = senderID; + connectingHand = message.hand; + stopWaiting(); + startConnecting(senderID, message.hand); + } else { + if (connectingId) { + // this is for someone else (we lost race in connectionRequest), + // so lets start over + startHandshake(); + } + } + } + // TODO: check to see if we are waiting for this but the person we are connecting sent it to + // someone else, and try again + break; + case "connecting": + delete waitingList[senderID]; + if (state == STATES.waiting && senderID == connectingId) { + // temporary logging + if (connectingHand != message.hand) { + debug("connecting hand", connectingHand, "not same as connecting hand in message", message.hand); + } + connectingHand = message.hand; + if (message.id != MyAvatar.sessionUUID) { + // the person we were trying to connect is connecting to someone else + // so try again + startHandshake(); + break; + } + startConnecting(senderID, message.hand); + } + break; + case "done": + delete waitingList[senderID]; + if (state == STATES.connecting && connectingId == senderID) { + // if they are done, and didn't connect us, terminate our + // connecting + if (message.connectionId !== MyAvatar.sessionUUID) { + stopConnecting(); + // now just call startHandshake. Should be ok to do so without a + // value for isKeyboard, as we should not change the animation + // state anyways (if any) + startHandshake(); + } + } else { + // if waiting or inactive, lets clear the connecting id. If in makingConnection, + // do nothing + if (state != STATES.makingConnection && connectingId == senderID) { + connectingId = undefined; + connectingHand = undefined; + if (state != STATES.inactive) { + startHandshake(); + } + } + } + break; + default: + debug("unknown message", message); + break; + } +} + +Messages.subscribe(MESSAGE_CHANNEL); +Messages.messageReceived.connect(messageHandler); + + +function makeGripHandler(hand, animate) { + // determine if we are gripping or un-gripping + if (animate) { + return function(value) { + updateTriggers(value, true, hand); + }; + + } else { + return function (value) { + updateTriggers(value, false, hand); + }; + } +} + +function keyPressEvent(event) { + if ((event.text === "x") && !event.isAutoRepeat && !event.isShifted && !event.isMeta && !event.isControl && !event.isAlt) { + updateTriggers(1.0, true, Controller.Standard.RightHand); + } +} +function keyReleaseEvent(event) { + if ((event.text === "x") && !event.isAutoRepeat && !event.isShifted && !event.isMeta && !event.isControl && !event.isAlt) { + updateTriggers(0.0, true, Controller.Standard.RightHand); + } +} +// map controller actions +var connectionMapping = Controller.newMapping(Script.resolvePath('') + '-grip'); +connectionMapping.from(Controller.Standard.LeftGrip).peek().to(makeGripHandler(Controller.Standard.LeftHand)); +connectionMapping.from(Controller.Standard.RightGrip).peek().to(makeGripHandler(Controller.Standard.RightHand)); + +// setup keyboard initiation +Controller.keyPressEvent.connect(keyPressEvent); +Controller.keyReleaseEvent.connect(keyReleaseEvent); + +// xbox controller cuz that's important +connectionMapping.from(Controller.Standard.RB).peek().to(makeGripHandler(Controller.Standard.RightHand, true)); + +// it is easy to forget this and waste a lot of time for nothing +connectionMapping.enable(); + +// connect updateVisualization to update frequently +Script.update.connect(updateVisualization); + +// load the sounds when the script loads +handshakeSound = SoundCache.getSound(HANDSHAKE_SOUND_URL); +successfulHandshakeSound = SoundCache.getSound(SUCCESSFUL_HANDSHAKE_SOUND_URL); + +Script.scriptEnding.connect(function () { + debug("removing controller mappings"); + connectionMapping.disable(); + debug("removing key mappings"); + Controller.keyPressEvent.disconnect(keyPressEvent); + Controller.keyReleaseEvent.disconnect(keyReleaseEvent); + debug("disconnecting updateVisualization"); + Script.update.disconnect(updateVisualization); + deleteParticleEffect(); + deleteMakeConnectionParticleEffect(); +}); + +}()); // END LOCAL_SCOPE + diff --git a/scripts/system/marketplaces/marketplaces.js b/scripts/system/marketplaces/marketplaces.js index 68da7696be..4d26bcadb6 100644 --- a/scripts/system/marketplaces/marketplaces.js +++ b/scripts/system/marketplaces/marketplaces.js @@ -59,6 +59,7 @@ function showMarketplace() { UserActivityLogger.openedMarketplace(); shouldActivateButton = true; + tablet.gotoWebScreen(MARKETPLACE_URL_INITIAL, MARKETPLACES_INJECT_SCRIPT_URL); onMarketplaceScreen = true; @@ -121,6 +122,7 @@ function onClick() { if (onMarketplaceScreen) { // for toolbar-mode: go back to home screen, this will close the window. tablet.gotoHomeScreen(); + onMarketplaceScreen = false; } else { var entity = HMD.tabletID; Entities.editEntity(entity, {textures: JSON.stringify({"tex.close": HOME_BUTTON_TEXTURE})}); @@ -140,6 +142,9 @@ tablet.screenChanged.connect(onScreenChanged); Entities.canWriteAssetsChanged.connect(onCanWriteAssetsChanged); Script.scriptEnding.connect(function () { + if (onMarketplaceScreen) { + tablet.gotoHomeScreen(); + } tablet.removeButton(marketplaceButton); tablet.screenChanged.disconnect(onScreenChanged); Entities.canWriteAssetsChanged.disconnect(onCanWriteAssetsChanged); diff --git a/scripts/system/menu.js b/scripts/system/menu.js index 1d5f8bccd6..4ad5958144 100644 --- a/scripts/system/menu.js +++ b/scripts/system/menu.js @@ -48,6 +48,9 @@ var HOME_BUTTON_TEXTURE = "http://hifi-content.s3.amazonaws.com/alan/dev/tablet- tablet.screenChanged.connect(onScreenChanged); Script.scriptEnding.connect(function () { + if (onMenuScreen) { + tablet.gotoHomeScreen(); + } button.clicked.disconnect(onClicked); tablet.removeButton(button); tablet.screenChanged.disconnect(onScreenChanged); diff --git a/scripts/system/nameTag.js b/scripts/system/nameTag.js index e25db69064..17944bcf85 100644 --- a/scripts/system/nameTag.js +++ b/scripts/system/nameTag.js @@ -33,7 +33,7 @@ Script.setTimeout(function() { }, STARTUP_DELAY); function addNameTag() { - var nameTagPosition = Vec3.sum(MyAvatar.getHeadPosition(), Vec3.multiply(HEAD_OFFSET, Quat.getFront(MyAvatar.orientation))); + var nameTagPosition = Vec3.sum(MyAvatar.getHeadPosition(), Vec3.multiply(HEAD_OFFSET, Quat.getForward(MyAvatar.orientation))); nameTagPosition.y += HEIGHT_ABOVE_HEAD; var nameTagProperties = { name: MyAvatar.displayName + ' Name Tag', @@ -49,7 +49,7 @@ function addNameTag() { function updateNameTag() { var nameTagProps = Entities.getEntityProperties(nameTagEntityID); - var nameTagPosition = Vec3.sum(MyAvatar.getHeadPosition(), Vec3.multiply(HEAD_OFFSET, Quat.getFront(MyAvatar.orientation))); + var nameTagPosition = Vec3.sum(MyAvatar.getHeadPosition(), Vec3.multiply(HEAD_OFFSET, Quat.getForward(MyAvatar.orientation))); nameTagPosition.y += HEIGHT_ABOVE_HEAD; Entities.editEntity(nameTagEntityID, { diff --git a/scripts/system/notifications.js b/scripts/system/notifications.js index 3ae071c7e3..25a5edf3a3 100644 --- a/scripts/system/notifications.js +++ b/scripts/system/notifications.js @@ -94,11 +94,15 @@ var NotificationType = { LOD_WARNING: 2, CONNECTION_REFUSED: 3, EDIT_ERROR: 4, + TABLET: 5, + CONNECTION: 6, properties: [ { text: "Snapshot" }, { text: "Level of Detail" }, { text: "Connection Refused" }, - { text: "Edit error" } + { text: "Edit error" }, + { text: "Tablet" }, + { text: "Connection" } ], getTypeFromMenuItem: function(menuItemName) { if (menuItemName.substr(menuItemName.length - NOTIFICATION_MENU_ITEM_POST.length) !== NOTIFICATION_MENU_ITEM_POST) { @@ -423,21 +427,24 @@ function deleteNotification(index) { arrays.splice(index, 1); } -// wraps whole word to newline -function stringDivider(str, slotWidth, spaceReplacer) { - var left, right; - if (str.length > slotWidth && slotWidth > 0) { - left = str.substring(0, slotWidth); - right = str.substring(slotWidth); - return left + spaceReplacer + stringDivider(right, slotWidth, spaceReplacer); +// Trims extra whitespace and breaks into lines of length no more than MAX_LENGTH, breaking at spaces. Trims extra whitespace. +var MAX_LENGTH = 42; +function wordWrap(string) { + var finishedLines = [], currentLine = ''; + string.split(/\s/).forEach(function (word) { + var tail = currentLine ? ' ' + word : word; + if ((currentLine.length + tail.length) <= MAX_LENGTH) { + currentLine += tail; + } else { + finishedLines.push(currentLine); + currentLine = word; + } + }); + if (currentLine) { + finishedLines.push(currentLine); } - return str; -} - -// formats string to add newline every 43 chars -function wordWrap(str) { - return stringDivider(str, 43.0, "\n"); + return finishedLines.join('\n'); } function update() { @@ -521,6 +528,9 @@ function onEditError(msg) { createNotification(wordWrap(msg), NotificationType.EDIT_ERROR); } +function onNotify(msg) { + createNotification(wordWrap(msg), NotificationType.UNKNOWN); // Needs a generic notification system for user feedback, thus using this +} function onSnapshotTaken(pathStillSnapshot, pathAnimatedSnapshot, notify) { if (notify) { @@ -532,10 +542,22 @@ function onSnapshotTaken(pathStillSnapshot, pathAnimatedSnapshot, notify) { } } +function tabletNotification() { + createNotification("Tablet needs your attention", NotificationType.TABLET); +} + function processingGif() { createNotification("Processing GIF snapshot...", NotificationType.SNAPSHOT); } +function connectionAdded(connectionName) { + createNotification(connectionName, NotificationType.CONNECTION); +} + +function connectionError(error) { + createNotification(wordWrap("Error trying to make connection: " + error), NotificationType.CONNECTION); +} + // handles mouse clicks on buttons function mousePressEvent(event) { var pickRay, @@ -636,8 +658,11 @@ Menu.menuItemEvent.connect(menuItemEvent); Window.domainConnectionRefused.connect(onDomainConnectionRefused); Window.snapshotTaken.connect(onSnapshotTaken); Window.processingGif.connect(processingGif); +Window.connectionAdded.connect(connectionAdded); +Window.connectionError.connect(connectionError); Window.notifyEditError = onEditError; - +Window.notify = onNotify; +Tablet.tabletNotification.connect(tabletNotification); setup(); }()); // END LOCAL_SCOPE diff --git a/scripts/system/pal.js b/scripts/system/pal.js index 70b2739c96..5fbea90025 100644 --- a/scripts/system/pal.js +++ b/scripts/system/pal.js @@ -1,6 +1,6 @@ "use strict"; -/* jslint vars: true, plusplus: true, forin: true*/ -/* globals Tablet, Script, AvatarList, Users, Entities, MyAvatar, Camera, Overlays, Vec3, Quat, Controller, print, getControllerWorldLocation */ +/*jslint vars:true, plusplus:true, forin:true*/ +/*global Tablet, Settings, Script, AvatarList, Users, Entities, MyAvatar, Camera, Overlays, Vec3, Quat, HMD, Controller, Account, UserActivityLogger, Messages, Window, XMLHttpRequest, print, location, getControllerWorldLocation*/ /* eslint indent: ["error", 4, { "outerIIFEBody": 0 }] */ // // pal.js @@ -14,6 +14,12 @@ (function() { // BEGIN LOCAL_SCOPE +var populateNearbyUserList, color, textures, removeOverlays, + controllerComputePickRay, onTabletButtonClicked, onTabletScreenChanged, + receiveMessage, avatarDisconnected, clearLocalQMLDataAndClosePAL, + createAudioInterval, tablet, CHANNEL, getConnectionData, findableByChanged, + avatarAdded, avatarRemoved, avatarSessionChanged; // forward references; + // hardcoding these as it appears we cannot traverse the originalTextures in overlays??? Maybe I've missed // something, will revisit as this is sorta horrible. var UNSELECTED_TEXTURES = { @@ -97,9 +103,8 @@ ExtendedOverlay.prototype.hover = function (hovering) { if (this.key === lastHoveringId) { if (hovering) { return; - } else { - lastHoveringId = 0; } + lastHoveringId = 0; } this.editOverlay({color: color(this.selected, hovering, this.audioLevel)}); if (this.model) { @@ -214,9 +219,8 @@ function convertDbToLinear(decibels) { // 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); + return Math.pow(2, decibels / 10.0); } - function fromQml(message) { // messages are {method, params}, like json-rpc. See also sendToQml. var data; switch (message.method) { @@ -247,20 +251,68 @@ function fromQml(message) { // messages are {method, params}, like json-rpc. See }); } break; - case 'refresh': + case 'refreshNearby': + data = {}; + ExtendedOverlay.some(function (overlay) { // capture the audio data + data[overlay.key] = overlay; + }); removeOverlays(); // If filter is specified from .qml instead of through settings, update the settings. if (message.params.filter !== undefined) { Settings.setValue('pal/filtered', !!message.params.filter); } - populateUserList(message.params.selected); - UserActivityLogger.palAction("refresh", ""); + populateNearbyUserList(message.params.selected, data); + UserActivityLogger.palAction("refresh_nearby", ""); break; - case 'displayNameUpdate': - if (MyAvatar.displayName !== message.params) { - MyAvatar.displayName = message.params; - UserActivityLogger.palAction("display_name_change", ""); - } + case 'refreshConnections': + print('Refreshing Connections...'); + getConnectionData(); + UserActivityLogger.palAction("refresh_connections", ""); + break; + case 'removeConnection': + connectionUserName = message.params; + request({ + uri: METAVERSE_BASE + '/api/v1/user/connections/' + connectionUserName, + method: 'DELETE' + }, function (error, response) { + if (error || (response.status !== 'success')) { + print("Error: unable to remove connection", connectionUserName, error || response.status); + return; + } + getConnectionData(); + }); + break + + case 'removeFriend': + friendUserName = message.params; + request({ + uri: METAVERSE_BASE + '/api/v1/user/friends/' + friendUserName, + method: 'DELETE' + }, function (error, response) { + if (error || (response.status !== 'success')) { + print("Error: unable to unfriend", friendUserName, error || response.status); + return; + } + getConnectionData(); + }); + break + case 'addFriend': + friendUserName = message.params; + request({ + uri: METAVERSE_BASE + '/api/v1/user/friends', + method: 'POST', + json: true, + body: { + username: friendUserName, + } + }, function (error, response) { + if (error || (response.status !== 'success')) { + print("Error: unable to friend " + friendUserName, error || response.status); + return; + } + getConnectionData(); // For now, just refresh all connection data. Later, just refresh the one friended row. + } + ); break; default: print('Unrecognized message from Pal.qml:', JSON.stringify(message)); @@ -270,6 +322,141 @@ function fromQml(message) { // messages are {method, params}, like json-rpc. See function sendToQml(message) { tablet.sendToQml(message); } +function updateUser(data) { + print('PAL update:', JSON.stringify(data)); + sendToQml({ method: 'updateUsername', params: data }); +} +// +// User management services +// +// These are prototype versions that will be changed when the back end changes. +var METAVERSE_BASE = location.metaverseServerUrl; +function request(options, callback) { // cb(error, responseOfCorrectContentType) of url. A subset of npm request. + var httpRequest = new XMLHttpRequest(), key; + // QT bug: apparently doesn't handle onload. Workaround using readyState. + httpRequest.onreadystatechange = function () { + var READY_STATE_DONE = 4; + var HTTP_OK = 200; + if (httpRequest.readyState >= READY_STATE_DONE) { + var error = (httpRequest.status !== HTTP_OK) && httpRequest.status.toString() + ':' + httpRequest.statusText, + response = !error && httpRequest.responseText, + contentType = !error && httpRequest.getResponseHeader('content-type'); + if (!error && contentType.indexOf('application/json') === 0) { // ignoring charset, etc. + try { + response = JSON.parse(response); + } catch (e) { + error = e; + } + } + callback(error, response); + } + }; + if (typeof options === 'string') { + options = {uri: options}; + } + if (options.url) { + options.uri = options.url; + } + if (!options.method) { + options.method = 'GET'; + } + if (options.body && (options.method === 'GET')) { // add query parameters + var params = [], appender = (-1 === options.uri.search('?')) ? '?' : '&'; + for (key in options.body) { + params.push(key + '=' + options.body[key]); + } + options.uri += appender + params.join('&'); + delete options.body; + } + if (options.json) { + options.headers = options.headers || {}; + options.headers["Content-type"] = "application/json"; + options.body = JSON.stringify(options.body); + } + for (key in options.headers || {}) { + httpRequest.setRequestHeader(key, options.headers[key]); + } + httpRequest.open(options.method, options.uri, true); + httpRequest.send(options.body); +} + + +function requestJSON(url, callback) { // callback(data) if successfull. Logs otherwise. + request({ + uri: url + }, function (error, response) { + if (error || (response.status !== 'success')) { + print("Error: unable to get", url, error || response.status); + return; + } + callback(response.data); + }); +} +function getProfilePicture(username, callback) { // callback(url) if successfull. (Logs otherwise) + // FIXME Prototype scrapes profile picture. We should include in user status, and also make available somewhere for myself + request({ + uri: METAVERSE_BASE + '/users/' + username + }, function (error, html) { + var matched = !error && html.match(/img class="users-img" src="([^"]*)"/); + if (!matched) { + print('Error: Unable to get profile picture for', username, error); + return; + } + callback(matched[1]); + }); +} +function getAvailableConnections(domain, callback) { // callback([{usename, location}...]) if successfull. (Logs otherwise) + // The back end doesn't do user connections yet. Fake it by getting all users that have made themselves accessible to us, + // and pretending that they are all connections. + url = METAVERSE_BASE + '/api/v1/users?' + if (domain) { + url += 'status=' + domain.slice(1, -1); // without curly braces + } else { + url += 'filter=connections'; // regardless of whether online + } + requestJSON(url, function (connectionsData) { + // The back end doesn't include the profile picture data, but we can add that here. + // For our current purposes, there's no need to be fancy and try to reduce latency by doing some number of requests in parallel, + // so these requests are all sequential. + var users = connectionsData.users; + function addPicture(index) { + if (index >= users.length) { + return callback(users); + } + var user = users[index]; + getProfilePicture(user.username, function (url) { + user.profileUrl = url; + addPicture(index + 1); + }); + } + addPicture(0); + }); +} + +function getConnectionData(domain) { // Update all the usernames that I am entitled to see, using my login but not dependent on canKick. + function frob(user) { // get into the right format + var formattedSessionId = user.location.node_id || ''; + if (formattedSessionId !== '' && formattedSessionId.indexOf("{") != 0) { + formattedSessionId = "{" + formattedSessionId + "}"; + } + return { + sessionId: formattedSessionId, + userName: user.username, + connection: user.connection, + profileUrl: user.profileUrl, + placeName: (user.location.root || user.location.domain || {}).name || '' + }; + } + getAvailableConnections(domain, function (users) { + if (domain) { + users.forEach(function (user) { + updateUser(frob(user)); + }); + } else { + sendToQml({ method: 'connections', params: users.map(frob) }); + } + }); +} // // Main operations. @@ -281,23 +468,25 @@ function addAvatarNode(id) { solid: true, alpha: 0.8, color: color(selected, false, 0.0), - ignoreRayIntersection: false}, selected, !conserveResources); + ignoreRayIntersection: false + }, selected, !conserveResources); } // Each open/refresh will capture a stable set of avatarsOfInterest, within the specified filter. var avatarsOfInterest = {}; -function populateUserList(selectData) { - var filter = Settings.getValue('pal/filtered') && {distance: Settings.getValue('pal/nearDistance')}; - var data = [], avatars = AvatarList.getAvatarIdentifiers(); - avatarsOfInterest = {}; - var myPosition = filter && Camera.position, +function populateNearbyUserList(selectData, oldAudioData) { + var filter = Settings.getValue('pal/filtered') && {distance: Settings.getValue('pal/nearDistance')}, + data = [], + avatars = AvatarList.getAvatarIdentifiers(), + myPosition = filter && Camera.position, frustum = filter && Camera.frustum, verticalHalfAngle = filter && (frustum.fieldOfView / 2), horizontalHalfAngle = filter && (verticalHalfAngle * frustum.aspectRatio), orientation = filter && Camera.orientation, - front = filter && Quat.getFront(orientation), + forward = filter && Quat.getForward(orientation), verticalAngleNormal = filter && Quat.getRight(orientation), horizontalAngleNormal = filter && Quat.getUp(orientation); - avatars.forEach(function (id) { // sorting the identifiers is just an aid for debugging + avatarsOfInterest = {}; + avatars.forEach(function (id) { var avatar = AvatarList.getAvatar(id); var name = avatar.sessionDisplayName; if (!name) { @@ -312,32 +501,40 @@ function populateUserList(selectData) { return; } var normal = id && filter && Vec3.normalize(Vec3.subtract(avatar.position, myPosition)); - var horizontal = normal && angleBetweenVectorsInPlane(normal, front, horizontalAngleNormal); - var vertical = normal && angleBetweenVectorsInPlane(normal, front, verticalAngleNormal); + var horizontal = normal && angleBetweenVectorsInPlane(normal, forward, horizontalAngleNormal); + var vertical = normal && angleBetweenVectorsInPlane(normal, forward, verticalAngleNormal); if (id && filter && ((Math.abs(horizontal) > horizontalHalfAngle) || (Math.abs(vertical) > verticalHalfAngle))) { return; } + var oldAudio = oldAudioData && oldAudioData[id]; var avatarPalDatum = { + profileUrl: '', displayName: name, userName: '', + connection: '', sessionId: id || '', - audioLevel: 0.0, - avgAudioLevel: 0.0, + audioLevel: (oldAudio && oldAudio.audioLevel) || 0.0, + avgAudioLevel: (oldAudio && oldAudio.avgAudioLevel) || 0.0, admin: false, personalMute: !!id && Users.getPersonalMuteStatus(id), // expects proper boolean, not null - ignore: !!id && Users.getIgnoreStatus(id) // ditto + ignore: !!id && Users.getIgnoreStatus(id), // ditto + isPresent: true }; if (id) { addAvatarNode(id); // No overlay for ourselves // Everyone needs to see admin status. Username and fingerprint returns default constructor output if the requesting user isn't an admin. Users.requestUsernameFromID(id); avatarsOfInterest[id] = true; + } else { + // Return our username from the Account API + avatarPalDatum.userName = Account.username; } data.push(avatarPalDatum); print('PAL data:', JSON.stringify(avatarPalDatum)); }); + getConnectionData(location.domainId); // Even admins don't get relationship data in requestUsernameFromID (which is still needed for admin status, which comes from domain). conserveResources = Object.keys(avatarsOfInterest).length > 20; - sendToQml({ method: 'users', params: data }); + sendToQml({ method: 'nearbyUsers', params: data }); if (selectData) { selectData[2] = true; sendToQml({ method: 'select', params: selectData }); @@ -346,15 +543,15 @@ function populateUserList(selectData) { // The function that handles the reply from the server function usernameFromIDReply(id, username, machineFingerprint, isAdmin) { - var data = [ - (MyAvatar.sessionUUID === id) ? '' : id, // Pal.qml recognizes empty id specially. + var data = { + sessionId: (MyAvatar.sessionUUID === id) ? '' : id, // Pal.qml recognizes empty id specially. // If we get username (e.g., if in future we receive it when we're friends), use it. // Otherwise, use valid machineFingerprint (which is not valid when not an admin). - username || (Users.canKick && machineFingerprint) || '', - isAdmin - ]; + userName: username || (Users.canKick && machineFingerprint) || '', + admin: isAdmin + }; // Ship the data off to QML - sendToQml({ method: 'updateUsername', params: data }); + updateUser(data); } var pingPong = true; @@ -376,16 +573,12 @@ function updateOverlays() { var target = avatar.position; var distance = Vec3.distance(target, eye); var offset = 0.2; - - // base offset on 1/2 distance from hips to head if we can - var headIndex = avatar.getJointIndex("Head"); + var diff = Vec3.subtract(target, eye); // get diff between target and eye (a vector pointing to the eye from avatar position) + var headIndex = avatar.getJointIndex("Head"); // base offset on 1/2 distance from hips to head if we can if (headIndex > 0) { offset = avatar.getAbsoluteJointTranslationInObjectFrame(headIndex).y / 2; } - // get diff between target and eye (a vector pointing to the eye from avatar position) - var diff = Vec3.subtract(target, eye); - // move a bit in front, towards the camera target = Vec3.subtract(target, Vec3.multiply(Vec3.normalize(diff), offset)); @@ -413,7 +606,7 @@ function updateOverlays() { overlay.deleteOverlay(); } }); - // We could re-populateUserList if anything added or removed, but not for now. + // We could re-populateNearbyUserList if anything added or removed, but not for now. HighlightedEntity.updateOverlays(); } function removeOverlays() { @@ -538,6 +731,9 @@ function startup() { Messages.subscribe(CHANNEL); Messages.messageReceived.connect(receiveMessage); Users.avatarDisconnected.connect(avatarDisconnected); + AvatarList.avatarAddedEvent.connect(avatarAdded); + AvatarList.avatarRemovedEvent.connect(avatarRemoved); + AvatarList.avatarSessionChangedEvent.connect(avatarSessionChanged); } startup(); @@ -551,6 +747,7 @@ function off() { Script.update.disconnect(updateOverlays); Controller.mousePressEvent.disconnect(handleMouseEvent); Controller.mouseMoveEvent.disconnect(handleMouseMoveEvent); + tablet.tabletShownChanged.disconnect(tabletVisibilityChanged); isWired = false; } if (audioTimer) { @@ -562,6 +759,12 @@ function off() { Users.requestsDomainListData = false; } +function tabletVisibilityChanged() { + if (!tablet.tabletShown) { + tablet.gotoHomeScreen(); + } +} + var onPalScreen = false; var shouldActivateButton = false; @@ -572,9 +775,10 @@ function onTabletButtonClicked() { } else { shouldActivateButton = true; tablet.loadQMLSource("../Pal.qml"); + tablet.tabletShownChanged.connect(tabletVisibilityChanged); onPalScreen = true; Users.requestsDomainListData = true; - populateUserList(); + populateNearbyUserList(); isWired = true; Script.update.connect(updateOverlays); Controller.mousePressEvent.connect(handleMouseEvent); @@ -602,8 +806,7 @@ function onTabletScreenChanged(type, url) { // var CHANNEL = 'com.highfidelity.pal'; function receiveMessage(channel, messageString, senderID) { - if ((channel !== CHANNEL) || - (senderID !== MyAvatar.sessionUUID)) { + if ((channel !== CHANNEL) || (senderID !== MyAvatar.sessionUUID)) { return; } var message = JSON.parse(messageString); @@ -628,7 +831,7 @@ function scaleAudio(val) { if (val <= LOUDNESS_FLOOR) { audioLevel = val / LOUDNESS_FLOOR * LOUDNESS_SCALE; } else { - audioLevel = (val -(LOUDNESS_FLOOR -1 )) * LOUDNESS_SCALE; + audioLevel = (val - (LOUDNESS_FLOOR - 1)) * LOUDNESS_SCALE; } if (audioLevel > 1.0) { audioLevel = 1; @@ -654,14 +857,14 @@ function getAudioLevel(id) { audioLevel = scaleAudio(Math.log(data.accumulatedLevel + 1) / LOG2); // decay avgAudioLevel - avgAudioLevel = Math.max((1-AUDIO_PEAK_DECAY) * (data.avgAudioLevel || 0), audioLevel); + avgAudioLevel = Math.max((1 - AUDIO_PEAK_DECAY) * (data.avgAudioLevel || 0), audioLevel); data.avgAudioLevel = avgAudioLevel; data.audioLevel = audioLevel; // now scale for the gain. Also, asked to boost the low end, so one simple way is // to take sqrt of the value. Lets try that, see how it feels. - avgAudioLevel = Math.min(1.0, Math.sqrt(avgAudioLevel *(sessionGains[id] || 0.75))); + avgAudioLevel = Math.min(1.0, Math.sqrt(avgAudioLevel * (sessionGains[id] || 0.75))); } return [audioLevel, avgAudioLevel]; } @@ -672,9 +875,8 @@ function createAudioInterval(interval) { return Script.setInterval(function () { var param = {}; AvatarList.getAvatarIdentifiers().forEach(function (id) { - var level = getAudioLevel(id); - // qml didn't like an object with null/empty string for a key, so... - var userId = id || 0; + var level = getAudioLevel(id), + userId = id || 0; // qml didn't like an object with null/empty string for a key, so... param[userId] = level; }); sendToQml({method: 'updateAudioLevel', params: param}); @@ -690,7 +892,22 @@ function clearLocalQMLDataAndClosePAL() { sendToQml({ method: 'clearLocalQMLData' }); } +function avatarAdded(avatarID) { + sendToQml({ method: 'palIsStale', params: [avatarID, 'avatarAdded'] }); +} + +function avatarRemoved(avatarID) { + sendToQml({ method: 'palIsStale', params: [avatarID, 'avatarRemoved'] }); +} + +function avatarSessionChanged(avatarID) { + sendToQml({ method: 'palIsStale', params: [avatarID, 'avatarSessionChanged'] }); +} + function shutdown() { + if (onPalScreen) { + tablet.gotoHomeScreen(); + } button.clicked.disconnect(onTabletButtonClicked); tablet.removeButton(button); tablet.screenChanged.disconnect(onTabletScreenChanged); @@ -700,6 +917,9 @@ function shutdown() { Messages.subscribe(CHANNEL); Messages.messageReceived.disconnect(receiveMessage); Users.avatarDisconnected.disconnect(avatarDisconnected); + AvatarList.avatarAddedEvent.disconnect(avatarAdded); + AvatarList.avatarRemovedEvent.disconnect(avatarRemoved); + AvatarList.avatarSessionChangedEvent.disconnect(avatarSessionChanged); off(); } diff --git a/scripts/system/particle_explorer/particleExplorer.html b/scripts/system/particle_explorer/particleExplorer.html index 260d8a7813..d12ceac14b 100644 --- a/scripts/system/particle_explorer/particleExplorer.html +++ b/scripts/system/particle_explorer/particleExplorer.html @@ -76,4 +76,4 @@ body{
- \ No newline at end of file + diff --git a/scripts/system/particle_explorer/particleExplorer.js b/scripts/system/particle_explorer/particleExplorer.js index 4fd0978a84..e0987ecd09 100644 --- a/scripts/system/particle_explorer/particleExplorer.js +++ b/scripts/system/particle_explorer/particleExplorer.js @@ -32,6 +32,8 @@ var gui = null; var settings = new Settings(); var updateInterval; +var active = false; + var currentInputField; var storedController; //CHANGE TO WHITELIST @@ -358,9 +360,25 @@ function listenForSettingsUpdates() { settings[key] = value; }); - loadGUI(); - } + if (gui) { + manuallyUpdateDisplay(); + } else { + loadGUI(); + } + if (!active) { + // gui.toggleHide(); + gui.closed = false; + } + active = true; + } else if (data.messageType === "particle_close") { + // none of this seems to work. + // if (active) { + // gui.toggleHide(); + // } + active = false; + gui.closed = true; + } }); } @@ -505,4 +523,4 @@ function registerDOMElementsForListenerBlocking() { }); }); }); -} \ No newline at end of file +} diff --git a/scripts/system/particle_explorer/particleExplorerTool.js b/scripts/system/particle_explorer/particleExplorerTool.js index 8a28445c33..b3db475ab0 100644 --- a/scripts/system/particle_explorer/particleExplorerTool.js +++ b/scripts/system/particle_explorer/particleExplorerTool.js @@ -18,26 +18,21 @@ ParticleExplorerTool = function() { var that = {}; that.createWebView = function() { - var url = PARTICLE_EXPLORER_HTML_URL; - that.webView = new OverlayWebWindow({ - title: 'Particle Explorer', - source: url, - toolWindow: true - }); - - that.webView.setVisible(true); + that.webView = Tablet.getTablet("com.highfidelity.interface.tablet.system"); + that.webView.setVisible = function(value) {}; that.webView.webEventReceived.connect(that.webEventReceived); } - that.destroyWebView = function() { if (!that.webView) { return; } - - that.webView.close(); - that.webView = null; that.activeParticleEntity = 0; + + var messageData = { + messageType: "particle_close" + }; + that.webView.emitScriptEvent(JSON.stringify(messageData)); } that.webEventReceived = function(data) { @@ -51,8 +46,5 @@ ParticleExplorerTool = function() { that.activeParticleEntity = id; } - return that; - - -}; \ No newline at end of file +}; diff --git a/scripts/system/selectAudioDevice.js b/scripts/system/selectAudioDevice.js index 9b97b24455..2dd426932f 100644 --- a/scripts/system/selectAudioDevice.js +++ b/scripts/system/selectAudioDevice.js @@ -15,211 +15,266 @@ (function() { // BEGIN LOCAL_SCOPE -if (typeof String.prototype.startsWith != 'function') { - String.prototype.startsWith = function (str){ - return this.slice(0, str.length) == str; - }; -} - -if (typeof String.prototype.endsWith != 'function') { - String.prototype.endsWith = function (str){ - return this.slice(-str.length) == str; - }; -} - -if (typeof String.prototype.trimStartsWith != 'function') { - String.prototype.trimStartsWith = function (str){ - if (this.startsWith(str)) { - return this.substr(str.length); - } - return this; - }; -} - -if (typeof String.prototype.trimEndsWith != 'function') { - String.prototype.trimEndsWith = function (str){ - if (this.endsWith(str)) { - return this.substr(0,this.length - str.length); - } - return this; - }; +const INPUT = "Input"; +const OUTPUT = "Output"; +function parseMenuItem(item) { + const USE = "Use "; + const FOR_INPUT = " for " + INPUT; + const FOR_OUTPUT = " for " + OUTPUT; + if (item.slice(0, USE.length) == USE) { + if (item.slice(-FOR_INPUT.length) == FOR_INPUT) { + return { device: item.slice(USE.length, -FOR_INPUT.length), mode: INPUT }; + } else if (item.slice(-FOR_OUTPUT.length) == FOR_OUTPUT) { + return { device: item.slice(USE.length, -FOR_OUTPUT.length), mode: OUTPUT }; + } + } } +// +// VAR DEFINITIONS +// +var debugPrintStatements = true; const INPUT_DEVICE_SETTING = "audio_input_device"; const OUTPUT_DEVICE_SETTING = "audio_output_device"; - -var selectedInputMenu = ""; -var selectedOutputMenu = ""; - -function setupAudioMenus() { - Menu.addSeparator("Audio", "Input Audio Device"); - - var inputDeviceSetting = Settings.getValue(INPUT_DEVICE_SETTING); - var inputDevices = AudioDevice.getInputDevices(); - var selectedInputDevice = AudioDevice.getInputDevice(); - if (inputDevices.indexOf(inputDeviceSetting) != -1 && selectedInputDevice != inputDeviceSetting) { - if (AudioDevice.setInputDevice(inputDeviceSetting)) { - selectedInputDevice = inputDeviceSetting; - } - } - print("audio input devices: " + inputDevices); - for(var i = 0; i < inputDevices.length; i++) { - var thisDeviceSelected = (inputDevices[i] == selectedInputDevice); - var menuItem = "Use " + inputDevices[i] + " for Input"; - Menu.addMenuItem({ - menuName: "Audio", - menuItemName: menuItem, - isCheckable: true, - isChecked: thisDeviceSelected - }); - if (thisDeviceSelected) { - selectedInputMenu = menuItem; - } - } - - Menu.addSeparator("Audio", "Output Audio Device"); - - var outputDeviceSetting = Settings.getValue(OUTPUT_DEVICE_SETTING); - var outputDevices = AudioDevice.getOutputDevices(); - var selectedOutputDevice = AudioDevice.getOutputDevice(); - if (outputDevices.indexOf(outputDeviceSetting) != -1 && selectedOutputDevice != outputDeviceSetting) { - if (AudioDevice.setOutputDevice(outputDeviceSetting)) { - selectedOutputDevice = outputDeviceSetting; - } - } - print("audio output devices: " + outputDevices); - for (var i = 0; i < outputDevices.length; i++) { - var thisDeviceSelected = (outputDevices[i] == selectedOutputDevice); - var menuItem = "Use " + outputDevices[i] + " for Output"; - Menu.addMenuItem({ - menuName: "Audio", - menuItemName: menuItem, - isCheckable: true, - isChecked: thisDeviceSelected - }); - if (thisDeviceSelected) { - selectedOutputMenu = menuItem; - } - } -} - -function onDevicechanged() { - print("audio devices changed, removing Audio > Devices menu..."); - Menu.removeMenu("Audio > Devices"); - print("now setting up Audio > Devices menu"); - setupAudioMenus(); -} - -// Have a small delay before the menu's get setup and the audio devices can switch to the last selected ones -Script.setTimeout(function () { - print("connecting deviceChanged"); - AudioDevice.deviceChanged.connect(onDevicechanged); - print("setting up Audio > Devices menu for first time"); - setupAudioMenus(); -}, 5000); - -function scriptEnding() { - Menu.removeMenu("Audio > Devices"); -} -Script.scriptEnding.connect(scriptEnding); - - -function menuItemEvent(menuItem) { - if (menuItem.startsWith("Use ")) { - if (menuItem.endsWith(" for Output")) { - var selectedDevice = menuItem.trimStartsWith("Use ").trimEndsWith(" for Output"); - print("output audio selection..." + selectedDevice); - Menu.menuItemEvent.disconnect(menuItemEvent); - Menu.setIsOptionChecked(selectedOutputMenu, false); - selectedOutputMenu = menuItem; - Menu.setIsOptionChecked(selectedOutputMenu, true); - if (AudioDevice.setOutputDevice(selectedDevice)) { - Settings.setValue(OUTPUT_DEVICE_SETTING, selectedDevice); - } - Menu.menuItemEvent.connect(menuItemEvent); - } else if (menuItem.endsWith(" for Input")) { - var selectedDevice = menuItem.trimStartsWith("Use ").trimEndsWith(" for Input"); - print("input audio selection..." + selectedDevice); - Menu.menuItemEvent.disconnect(menuItemEvent); - Menu.setIsOptionChecked(selectedInputMenu, false); - selectedInputMenu = menuItem; - Menu.setIsOptionChecked(selectedInputMenu, true); - if (AudioDevice.setInputDevice(selectedDevice)) { - Settings.setValue(INPUT_DEVICE_SETTING, selectedDevice); - } - Menu.menuItemEvent.connect(menuItemEvent); - } - } -} - -Menu.menuItemEvent.connect(menuItemEvent); - -// Some HMDs (like Oculus CV1) have a built in audio device. If they -// do, then this function will handle switching to that device automatically -// when you goActive with the HMD active. -var wasHmdMounted = false; // assume it's un-mounted to start +var audioDevicesList = []; +var wasHmdActive = false; // assume it's not active to start var switchedAudioInputToHMD = false; var switchedAudioOutputToHMD = false; var previousSelectedInputAudioDevice = ""; var previousSelectedOutputAudioDevice = ""; +var skipMenuEvents = true; + +// +// BEGIN FUNCTION DEFINITIONS +// +function debug() { + if (debugPrintStatements) { + print.apply(null, [].concat.apply(["selectAudioDevice.js:"], [].map.call(arguments, JSON.stringify))); + } +} + + +function setupAudioMenus() { + // menu events can be triggered asynchronously; skip them for 200ms to avoid recursion and false switches + skipMenuEvents = true; + Script.setTimeout(function() { skipMenuEvents = false; }, 200); + + removeAudioMenus(); + + // Setup audio input devices + Menu.addSeparator("Audio", "Input Audio Device"); + var inputDevices = AudioDevice.getInputDevices(); + for (var i = 0; i < inputDevices.length; i++) { + var audioDeviceMenuString = "Use " + inputDevices[i] + " for Input"; + Menu.addMenuItem({ + menuName: "Audio", + menuItemName: audioDeviceMenuString, + isCheckable: true, + isChecked: inputDevices[i] == AudioDevice.getInputDevice() + }); + audioDevicesList.push(audioDeviceMenuString); + } + + // Setup audio output devices + Menu.addSeparator("Audio", "Output Audio Device"); + var outputDevices = AudioDevice.getOutputDevices(); + for (var i = 0; i < outputDevices.length; i++) { + var audioDeviceMenuString = "Use " + outputDevices[i] + " for Output"; + Menu.addMenuItem({ + menuName: "Audio", + menuItemName: audioDeviceMenuString, + isCheckable: true, + isChecked: outputDevices[i] == AudioDevice.getOutputDevice() + }); + audioDevicesList.push(audioDeviceMenuString); + } +} + +function checkDeviceMismatch() { + var inputDeviceSetting = Settings.getValue(INPUT_DEVICE_SETTING); + var interfaceInputDevice = AudioDevice.getInputDevice(); + if (interfaceInputDevice != inputDeviceSetting) { + debug("Input Setting & Device mismatch! Input SETTING: " + inputDeviceSetting + "Input DEVICE IN USE: " + interfaceInputDevice); + switchAudioDevice("Use " + inputDeviceSetting + " for Input"); + } + + var outputDeviceSetting = Settings.getValue(OUTPUT_DEVICE_SETTING); + var interfaceOutputDevice = AudioDevice.getOutputDevice(); + if (interfaceOutputDevice != outputDeviceSetting) { + debug("Output Setting & Device mismatch! Output SETTING: " + outputDeviceSetting + "Output DEVICE IN USE: " + interfaceOutputDevice); + switchAudioDevice("Use " + outputDeviceSetting + " for Output"); + } +} + +function removeAudioMenus() { + Menu.removeSeparator("Audio", "Input Audio Device"); + Menu.removeSeparator("Audio", "Output Audio Device"); + + for (var index = 0; index < audioDevicesList.length; index++) { + if (Menu.menuItemExists("Audio", audioDevicesList[index])) { + Menu.removeMenuItem("Audio", audioDevicesList[index]); + } + } + + Menu.removeMenu("Audio > Devices"); + + audioDevicesList = []; +} + +function onDevicechanged() { + debug("System audio devices changed. Removing and replacing Audio Menus..."); + setupAudioMenus(); + checkDeviceMismatch(); +} + +function onMenuEvent(audioDeviceMenuString) { + if (!skipMenuEvents) { + switchAudioDevice(audioDeviceMenuString); + } +} + +function switchAudioDevice(audioDeviceMenuString) { + // if the device is not plugged in, short-circuit + if (!~audioDevicesList.indexOf(audioDeviceMenuString)) { + return; + } + + var selection = parseMenuItem(audioDeviceMenuString); + if (!selection) { + debug("Invalid Audio audioDeviceMenuString! Doesn't end with 'for Input' or 'for Output'"); + return; + } + + // menu events can be triggered asynchronously; skip them for 200ms to avoid recursion and false switches + skipMenuEvents = true; + Script.setTimeout(function() { skipMenuEvents = false; }, 200); + + var selectedDevice = selection.device; + if (selection.mode == INPUT) { + var currentInputDevice = AudioDevice.getInputDevice(); + if (selectedDevice != currentInputDevice) { + debug("Switching audio INPUT device from " + currentInputDevice + " to " + selectedDevice); + Menu.setIsOptionChecked("Use " + currentInputDevice + " for Input", false); + if (AudioDevice.setInputDevice(selectedDevice)) { + Settings.setValue(INPUT_DEVICE_SETTING, selectedDevice); + Menu.setIsOptionChecked(audioDeviceMenuString, true); + } else { + debug("Error setting audio input device!") + Menu.setIsOptionChecked(audioDeviceMenuString, false); + } + } else { + debug("Selected input device is the same as the current input device!") + Settings.setValue(INPUT_DEVICE_SETTING, selectedDevice); + Menu.setIsOptionChecked(audioDeviceMenuString, true); + AudioDevice.setInputDevice(selectedDevice); // Still try to force-set the device (in case the user's trying to forcefully debug an issue) + } + } else if (selection.mode == OUTPUT) { + var currentOutputDevice = AudioDevice.getOutputDevice(); + if (selectedDevice != currentOutputDevice) { + debug("Switching audio OUTPUT device from " + currentOutputDevice + " to " + selectedDevice); + Menu.setIsOptionChecked("Use " + currentOutputDevice + " for Output", false); + if (AudioDevice.setOutputDevice(selectedDevice)) { + Settings.setValue(OUTPUT_DEVICE_SETTING, selectedDevice); + Menu.setIsOptionChecked(audioDeviceMenuString, true); + } else { + debug("Error setting audio output device!") + Menu.setIsOptionChecked(audioDeviceMenuString, false); + } + } else { + debug("Selected output device is the same as the current output device!") + Settings.setValue(OUTPUT_DEVICE_SETTING, selectedDevice); + Menu.setIsOptionChecked(audioDeviceMenuString, true); + AudioDevice.setOutputDevice(selectedDevice); // Still try to force-set the device (in case the user's trying to forcefully debug an issue) + } + } +} function restoreAudio() { if (switchedAudioInputToHMD) { - print("switching back from HMD preferred audio input to:" + previousSelectedInputAudioDevice); - menuItemEvent("Use " + previousSelectedInputAudioDevice + " for Input"); + debug("Switching back from HMD preferred audio input to: " + previousSelectedInputAudioDevice); + switchAudioDevice("Use " + previousSelectedInputAudioDevice + " for Input"); + switchedAudioInputToHMD = false; } if (switchedAudioOutputToHMD) { - print("switching back from HMD preferred audio output to:" + previousSelectedOutputAudioDevice); - menuItemEvent("Use " + previousSelectedOutputAudioDevice + " for Output"); + debug("Switching back from HMD preferred audio output to: " + previousSelectedOutputAudioDevice); + switchAudioDevice("Use " + previousSelectedOutputAudioDevice + " for Output"); + switchedAudioOutputToHMD = false; } } +// Some HMDs (like Oculus CV1) have a built in audio device. If they +// do, then this function will handle switching to that device automatically +// when you goActive with the HMD active. function checkHMDAudio() { - // Mounted state is changing... handle switching - if (HMD.mounted != wasHmdMounted) { - print("HMD mounted changed..."); + // HMD Active state is changing; handle switching + if (HMD.active != wasHmdActive) { + debug("HMD Active state changed!"); - // We're putting the HMD on... switch to those devices - if (HMD.mounted) { - print("NOW mounted..."); + // We're putting the HMD on; switch to those devices + if (HMD.active) { + debug("HMD is now Active."); var hmdPreferredAudioInput = HMD.preferredAudioInput(); var hmdPreferredAudioOutput = HMD.preferredAudioOutput(); - print("hmdPreferredAudioInput:" + hmdPreferredAudioInput); - print("hmdPreferredAudioOutput:" + hmdPreferredAudioOutput); + debug("hmdPreferredAudioInput: " + hmdPreferredAudioInput); + debug("hmdPreferredAudioOutput: " + hmdPreferredAudioOutput); - - var hmdHasPreferredAudio = (hmdPreferredAudioInput !== "") || (hmdPreferredAudioOutput !== ""); - if (hmdHasPreferredAudio) { - print("HMD has preferred audio!"); + if (hmdPreferredAudioInput !== "") { + debug("HMD has preferred audio input device."); previousSelectedInputAudioDevice = Settings.getValue(INPUT_DEVICE_SETTING); - previousSelectedOutputAudioDevice = Settings.getValue(OUTPUT_DEVICE_SETTING); - print("previousSelectedInputAudioDevice:" + previousSelectedInputAudioDevice); - print("previousSelectedOutputAudioDevice:" + previousSelectedOutputAudioDevice); - if (hmdPreferredAudioInput != previousSelectedInputAudioDevice && hmdPreferredAudioInput !== "") { - print("switching to HMD preferred audio input to:" + hmdPreferredAudioInput); + debug("previousSelectedInputAudioDevice: " + previousSelectedInputAudioDevice); + if (hmdPreferredAudioInput != previousSelectedInputAudioDevice) { switchedAudioInputToHMD = true; - menuItemEvent("Use " + hmdPreferredAudioInput + " for Input"); + switchAudioDevice("Use " + hmdPreferredAudioInput + " for Input"); } - if (hmdPreferredAudioOutput != previousSelectedOutputAudioDevice && hmdPreferredAudioOutput !== "") { - print("switching to HMD preferred audio output to:" + hmdPreferredAudioOutput); + } + if (hmdPreferredAudioOutput !== "") { + debug("HMD has preferred audio output device."); + previousSelectedOutputAudioDevice = Settings.getValue(OUTPUT_DEVICE_SETTING); + debug("previousSelectedOutputAudioDevice: " + previousSelectedOutputAudioDevice); + if (hmdPreferredAudioOutput != previousSelectedOutputAudioDevice) { switchedAudioOutputToHMD = true; - menuItemEvent("Use " + hmdPreferredAudioOutput + " for Output"); + switchAudioDevice("Use " + hmdPreferredAudioOutput + " for Output"); } } } else { - print("HMD NOW un-mounted..."); + debug("HMD no longer active. Restoring audio I/O devices..."); restoreAudio(); } } - wasHmdMounted = HMD.mounted; + wasHmdActive = HMD.active; } -Script.update.connect(checkHMDAudio); +// +// END FUNCTION DEFINITIONS +// +// +// BEGIN SCRIPT BODY +// +// Wait for the C++ systems to fire up before trying to do anything with audio devices +Script.setTimeout(function () { + debug("Connecting deviceChanged(), displayModeChanged(), and switchAudioDevice()..."); + AudioDevice.deviceChanged.connect(onDevicechanged); + HMD.displayModeChanged.connect(checkHMDAudio); + Menu.menuItemEvent.connect(onMenuEvent); + debug("Setting up Audio I/O menu for the first time..."); + setupAudioMenus(); + checkDeviceMismatch(); + debug("Checking HMD audio status...") + checkHMDAudio(); +}, 3000); + +debug("Connecting scriptEnding()"); Script.scriptEnding.connect(function () { restoreAudio(); - Menu.menuItemEvent.disconnect(menuItemEvent); - Script.update.disconnect(checkHMDAudio); + removeAudioMenus(); + Menu.menuItemEvent.disconnect(onMenuEvent); + HMD.displayModeChanged.disconnect(checkHMDAudio); + AudioDevice.deviceChanged.disconnect(onDevicechanged); }); +// +// END SCRIPT BODY +// + }()); // END LOCAL_SCOPE diff --git a/scripts/system/snapshot.js b/scripts/system/snapshot.js index 8f918c9cb2..8f393846c0 100644 --- a/scripts/system/snapshot.js +++ b/scripts/system/snapshot.js @@ -33,29 +33,45 @@ function shouldOpenFeedAfterShare() { return persisted && (persisted !== 'false'); } function showFeedWindow() { - DialogsManager.showFeed(); + if ((HMD.active && Settings.getValue("hmdTabletBecomesToolbar")) + || (!HMD.active && Settings.getValue("desktopTabletBecomesToolbar"))) { + DialogsManager.showFeed(); + } else { + tablet.loadQMLSource("TabletAddressDialog.qml"); + HMD.openTablet(); + } } -var SNAPSHOT_REVIEW_URL = Script.resolvePath("html/SnapshotReview.html"); - var outstanding; -function confirmShare(data) { - var dialog = new OverlayWebWindow('Snapshot Review', SNAPSHOT_REVIEW_URL, 800, 520); - function onMessage(message) { - // Receives message from the html dialog via the qwebchannel EventBridge. This is complicated by the following: - // 1. Although we can send POJOs, we cannot receive a toplevel object. (Arrays of POJOs are fine, though.) - // 2. Although we currently use a single image, we would like to take snapshot, a selfie, a 360 etc. all at the - // same time, show the user all of them, and have the user deselect any that they do not want to share. - // So we'll ultimately be receiving a set of objects, perhaps with different post processing for each. - var isLoggedIn; - var needsLogin = false; - switch (message) { - case 'ready': - dialog.emitScriptEvent(data); // Send it. +var readyData; +function onMessage(message) { + // Receives message from the html dialog via the qwebchannel EventBridge. This is complicated by the following: + // 1. Although we can send POJOs, we cannot receive a toplevel object. (Arrays of POJOs are fine, though.) + // 2. Although we currently use a single image, we would like to take snapshot, a selfie, a 360 etc. all at the + // same time, show the user all of them, and have the user deselect any that they do not want to share. + // So we'll ultimately be receiving a set of objects, perhaps with different post processing for each. + message = JSON.parse(message); + if (message.type !== "snapshot") { + return; + } + + var isLoggedIn; + var needsLogin = false; + switch (message.action) { + case 'ready': // Send it. + tablet.emitScriptEvent(JSON.stringify({ + type: "snapshot", + action: readyData + })); outstanding = 0; break; case 'openSettings': - Desktop.show("hifi/dialogs/GeneralPreferencesDialog.qml", "GeneralPreferencesDialog"); + if ((HMD.active && Settings.getValue("hmdTabletBecomesToolbar")) + || (!HMD.active && Settings.getValue("desktopTabletBecomesToolbar"))) { + Desktop.show("hifi/dialogs/GeneralPreferencesDialog.qml", "General Preferences"); + } else { + tablet.loadQMLSource("TabletGeneralPreferences.qml"); + } break; case 'setOpenFeedFalse': Settings.setValue('openFeedAfterShare', false); @@ -64,10 +80,11 @@ function confirmShare(data) { Settings.setValue('openFeedAfterShare', true); break; default: - dialog.webEventReceived.disconnect(onMessage); - dialog.close(); + //tablet.webEventReceived.disconnect(onMessage); // <<< It's probably this that's missing?! + HMD.closeTablet(); + tablet.gotoHomeScreen(); isLoggedIn = Account.isLoggedIn(); - message.forEach(function (submessage) { + message.action.forEach(function (submessage) { if (submessage.share && !isLoggedIn) { needsLogin = true; submessage.share = false; @@ -81,15 +98,22 @@ function confirmShare(data) { } }); if (!outstanding && shouldOpenFeedAfterShare()) { - showFeedWindow(); + //showFeedWindow(); } if (needsLogin) { // after the possible feed, so that the login is on top Account.checkAndSignalForAccessToken(); } - } } - dialog.webEventReceived.connect(onMessage); - dialog.raise(); +} + +var SNAPSHOT_REVIEW_URL = Script.resolvePath("html/SnapshotReview.html"); +var isInSnapshotReview = false; +function confirmShare(data) { + tablet.gotoWebScreen(SNAPSHOT_REVIEW_URL); + readyData = data; + tablet.webEventReceived.connect(onMessage); + HMD.openTablet(); + isInSnapshotReview = true; } function snapshotShared(errorMessage) { @@ -191,12 +215,12 @@ function resetButtons(pathStillSnapshot, pathAnimatedSnapshot, notify) { if (clearOverlayWhenMoving) { MyAvatar.setClearOverlayWhenMoving(true); // not until after the share dialog } + HMD.openTablet(); } function processingGif() { // show hud Reticle.visible = reticleVisible; - button.clicked.disconnect(onClicked); buttonConnected = false; // show overlays if they were on @@ -205,19 +229,30 @@ function processingGif() { } } +function onTabletScreenChanged(type, url) { + if (isInSnapshotReview) { + tablet.webEventReceived.disconnect(onMessage); + isInSnapshotReview = false; + } +} + button.clicked.connect(onClicked); buttonConnected = true; Window.snapshotShared.connect(snapshotShared); Window.processingGif.connect(processingGif); +tablet.screenChanged.connect(onTabletScreenChanged); Script.scriptEnding.connect(function () { - button.clicked.disconnect(onClicked); - buttonConnected = false; + if (buttonConnected) { + button.clicked.disconnect(onClicked); + buttonConnected = false; + } if (tablet) { tablet.removeButton(button); } Window.snapshotShared.disconnect(snapshotShared); Window.processingGif.disconnect(processingGif); + tablet.screenChanged.disconnect(onTabletScreenChanged); }); }()); // END LOCAL_SCOPE diff --git a/scripts/system/tablet-goto.js b/scripts/system/tablet-goto.js index eb95d9d8a3..84f7357b1a 100644 --- a/scripts/system/tablet-goto.js +++ b/scripts/system/tablet-goto.js @@ -29,10 +29,15 @@ } function onScreenChanged(type, url) { - // for toolbar mode: change button to active when window is first openend, false otherwise. - button.editProperties({isActive: shouldActivateButton}); - shouldActivateButton = false; - onGotoScreen = false; + if (url === gotoQmlSource) { + onGotoScreen = true; + shouldActivateButton = true; + button.editProperties({isActive: shouldActivateButton}); + } else { + shouldActivateButton = false; + onGotoScreen = false; + button.editProperties({isActive: shouldActivateButton}); + } } var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); diff --git a/scripts/system/tablet-ui/tabletUI.js b/scripts/system/tablet-ui/tabletUI.js index d5065cb826..dbfd3e632e 100644 --- a/scripts/system/tablet-ui/tabletUI.js +++ b/scripts/system/tablet-ui/tabletUI.js @@ -12,59 +12,157 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -/* global Script, HMD, WebTablet, UIWebTablet, UserActivityLogger, Settings, Entities, Messages, Tablet, Overlays, MyAvatar */ +/* global Script, HMD, WebTablet, UIWebTablet, UserActivityLogger, Settings, Entities, Messages, Tablet, Overlays, + MyAvatar, Menu */ (function() { // BEGIN LOCAL_SCOPE - var tabletShown = false; - var tabletLocation = null; + var tabletRezzed = false; var activeHand = null; + var DEFAULT_WIDTH = 0.4375; + var DEFAULT_TABLET_SCALE = 100; + var preMakeTime = Date.now(); + var validCheckTime = Date.now(); + var debugTablet = false; + var tabletScalePercentage = 100.0; + UIWebTablet = null; Script.include("../libraries/WebTablet.js"); - function showTabletUI() { - tabletShown = true; - print("show tablet-ui"); - - var DEFAULT_WIDTH = 0.4375; - var DEFAULT_TABLET_SCALE = 100; - var toolbarMode = Tablet.getTablet("com.highfidelity.interface.tablet.system").toolbarMode; - var TABLET_SCALE = DEFAULT_TABLET_SCALE; - if (toolbarMode) { - TABLET_SCALE = Settings.getValue("desktopTabletScale") || DEFAULT_TABLET_SCALE; - } else { - TABLET_SCALE = Settings.getValue("hmdTabletScale") || DEFAULT_TABLET_SCALE; + function tabletIsValid() { + if (!UIWebTablet) { + return false; } - UIWebTablet = new WebTablet("qml/hifi/tablet/TabletRoot.qml", DEFAULT_WIDTH * (TABLET_SCALE / 100), null, activeHand, true); + if (Overlays.getProperty(HMD.tabletID, "type") != "model") { + if (debugTablet) { + print("TABLET is invalid due to frame: " + JSON.stringify(Overlays.getProperty(HMD.tabletID, "type"))); + } + return false; + } + if (Overlays.getProperty(HMD.homeButtonID, "type") != "sphere" || + Overlays.getProperty(HMD.tabletScreenID, "type") != "web3d") { + if (debugTablet) { + print("TABLET is invalid due to other"); + } + return false; + } + return true; + } + + function getTabletScalePercentageFromSettings() { + var toolbarMode = Tablet.getTablet("com.highfidelity.interface.tablet.system").toolbarMode; + var tabletScalePercentage = DEFAULT_TABLET_SCALE; + if (!toolbarMode) { + if (HMD.active) { + tabletScalePercentage = Settings.getValue("hmdTabletScale") || DEFAULT_TABLET_SCALE; + } else { + tabletScalePercentage = Settings.getValue("desktopTabletScale") || DEFAULT_TABLET_SCALE; + } + } + return tabletScalePercentage; + } + + function updateTabletWidthFromSettings() { + var newTabletScalePercentage = getTabletScalePercentageFromSettings(); + if (newTabletScalePercentage !== tabletScalePercentage && UIWebTablet) { + tabletScalePercentage = newTabletScalePercentage; + UIWebTablet.setWidth(DEFAULT_WIDTH * (tabletScalePercentage / 100)); + } + } + + function onHmdChanged() { + updateTabletWidthFromSettings(); + } + + function rezTablet() { + if (debugTablet) { + print("TABLET rezzing"); + } + + tabletScalePercentage = getTabletScalePercentageFromSettings(); + UIWebTablet = new WebTablet("qml/hifi/tablet/TabletRoot.qml", + DEFAULT_WIDTH * (tabletScalePercentage / 100), + null, activeHand, true); UIWebTablet.register(); HMD.tabletID = UIWebTablet.tabletEntityID; HMD.homeButtonID = UIWebTablet.homeButtonID; HMD.tabletScreenID = UIWebTablet.webOverlayID; + HMD.displayModeChanged.connect(onHmdChanged); + + tabletRezzed = true; + } + + function showTabletUI() { + Tablet.getTablet("com.highfidelity.interface.tablet.system").tabletShown = true; + + if (!tabletRezzed || !tabletIsValid()) { + closeTabletUI(); + rezTablet(); + } + + if (UIWebTablet && tabletRezzed) { + if (debugTablet) { + print("TABLET in showTabletUI, already rezzed"); + } + var tabletProperties = {}; + UIWebTablet.calculateTabletAttachmentProperties(activeHand, true, tabletProperties); + tabletProperties.visible = true; + Overlays.editOverlay(HMD.tabletID, tabletProperties); + Overlays.editOverlay(HMD.homeButtonID, { visible: true }); + Overlays.editOverlay(HMD.tabletScreenID, { visible: true }); + Overlays.editOverlay(HMD.tabletScreenID, { maxFPS: 90 }); + } } function hideTabletUI() { - tabletShown = false; - print("hide tablet-ui"); + Tablet.getTablet("com.highfidelity.interface.tablet.system").tabletShown = false; + if (!UIWebTablet) { + return; + } + + if (debugTablet) { + print("TABLET hide"); + } + + Overlays.editOverlay(HMD.tabletID, { visible: false }); + Overlays.editOverlay(HMD.homeButtonID, { visible: false }); + Overlays.editOverlay(HMD.tabletScreenID, { visible: false }); + Overlays.editOverlay(HMD.tabletScreenID, { maxFPS: 1 }); + } + + function closeTabletUI() { + Tablet.getTablet("com.highfidelity.interface.tablet.system").tabletShown = false; if (UIWebTablet) { if (UIWebTablet.onClose) { UIWebTablet.onClose(); } - tabletLocation = UIWebTablet.getLocation(); + if (debugTablet) { + print("TABLET close"); + } UIWebTablet.unregister(); UIWebTablet.destroy(); UIWebTablet = null; HMD.tabletID = null; HMD.homeButtonID = null; HMD.tabletScreenID = null; + } else if (debugTablet) { + print("TABLET closeTabletUI, UIWebTablet is null"); } + tabletRezzed = false; } + function updateShowTablet() { + var MSECS_PER_SEC = 1000.0; + var now = Date.now(); // close the WebTablet if it we go into toolbar mode. + var tabletShown = Tablet.getTablet("com.highfidelity.interface.tablet.system").tabletShown; var toolbarMode = Tablet.getTablet("com.highfidelity.interface.tablet.system").toolbarMode; + var landscape = Tablet.getTablet("com.highfidelity.interface.tablet.system").landscape; + if (tabletShown && toolbarMode) { - hideTabletUI(); + closeTabletUI(); HMD.closeTablet(); return; } @@ -78,29 +176,66 @@ tablet.updateAudioBar(currentMicLevel); } - if (tabletShown && UIWebTablet && Overlays.getOverlayType(UIWebTablet.webOverlayID) != "web3d") { - // when we switch domains, the tablet entity gets destroyed and recreated. this causes - // the overlay to be deleted, but not recreated. If the overlay is deleted for this or any - // other reason, close the tablet. - hideTabletUI(); - HMD.closeTablet(); - } else if (HMD.showTablet && !tabletShown && !toolbarMode) { - UserActivityLogger.openedTablet(Settings.getValue("tabletVisibleToOthers")); + updateTabletWidthFromSettings(); + if (UIWebTablet) { + UIWebTablet.setLandscape(landscape); + } + + if (validCheckTime - now > MSECS_PER_SEC) { + validCheckTime = now; + if (tabletRezzed && UIWebTablet && !tabletIsValid()) { + // when we switch domains, the tablet entity gets destroyed and recreated. this causes + // the overlay to be deleted, but not recreated. If the overlay is deleted for this or any + // other reason, close the tablet. + closeTabletUI(); + HMD.closeTablet(); + if (debugTablet) { + print("TABLET autodestroying"); + } + } + } + + // check for change in tablet scale. + + if (HMD.showTablet && !tabletShown && !toolbarMode) { + UserActivityLogger.openedTablet(false); showTabletUI(); } else if (!HMD.showTablet && tabletShown) { UserActivityLogger.closedTablet(); hideTabletUI(); } + + // if the tablet is an overlay, attempt to pre-create it and then hide it so that when it's + // summoned, it will appear quickly. + if (!toolbarMode) { + if (now - preMakeTime > MSECS_PER_SEC) { + preMakeTime = now; + if (!tabletIsValid()) { + closeTabletUI(); + rezTablet(); + tabletShown = false; + } else if (!tabletShown) { + hideTabletUI(); + } + } + } + } - function toggleHand(channel, hand, senderUUID, localOnly) { + function handleMessage(channel, hand, senderUUID, localOnly) { if (channel === "toggleHand") { activeHand = JSON.parse(hand); } + if (channel === "home") { + if (UIWebTablet) { + Tablet.getTablet("com.highfidelity.interface.tablet.system").landscape = false; + } + } } Messages.subscribe("toggleHand"); - Messages.messageReceived.connect(toggleHand); + Messages.subscribe("home"); + Messages.messageReceived.connect(handleMessage); Script.setInterval(updateShowTablet, 100); @@ -131,7 +266,16 @@ } Script.scriptEnding.connect(function () { - Entities.deleteEntity(HMD.tabletID); + + // if we reload scripts in tablet mode make sure we close the currently open window, by calling gotoHomeScreen + var tabletProxy = Tablet.getTablet("com.highfidelity.interface.tablet.system"); + if (tabletProxy && tabletProxy.toolbarMode) { + tabletProxy.gotoHomeScreen(); + } + + var tabletID = HMD.tabletID; + Entities.deleteEntity(tabletID); + Overlays.deleteOverlay(tabletID); HMD.tabletID = null; HMD.homeButtonID = null; HMD.tabletScreenID = null; diff --git a/scripts/system/tablet-users.js b/scripts/system/tablet-users.js index 8e89ac74b7..591537f9bc 100644 --- a/scripts/system/tablet-users.js +++ b/scripts/system/tablet-users.js @@ -30,7 +30,7 @@ } else { // default to friends if it can't be determined myVisibility = "friends"; - GlobalServices.findableBy = myVisibilty; + GlobalServices.findableBy = myVisibility; } var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); @@ -67,9 +67,12 @@ } function onWebEventReceived(event) { - print("Script received a web event, its type is " + typeof event); if (typeof event === "string") { - event = JSON.parse(event); + try { + event = JSON.parse(event); + } catch(e) { + return; + } } if (event.type === "ready") { // send username to html @@ -115,6 +118,9 @@ tablet.screenChanged.connect(onScreenChanged); function cleanup() { + if (onUsersScreen) { + tablet.gotoHomeScreen(); + } button.clicked.disconnect(onClicked); tablet.removeButton(button); } diff --git a/scripts/system/users.js b/scripts/system/users.js deleted file mode 100644 index 480b9f07a2..0000000000 --- a/scripts/system/users.js +++ /dev/null @@ -1,1281 +0,0 @@ -"use strict"; - -// -// users.js -// examples -// -// Created by David Rowe on 9 Mar 2015. -// Copyright 2015 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// -/*globals HMD, Toolbars, Script, Menu, Overlays, Tablet, Controller, Settings, OverlayWebWindow, Account, GlobalServices */ - -(function() { // BEGIN LOCAL_SCOPE -var button; -var buttonName = "USERS"; -var toolBar = null; -var tablet = null; - -var MENU_ITEM = "Users Online"; - -if (Settings.getValue("HUDUIEnabled")) { - toolBar = Toolbars.getToolbar("com.highfidelity.interface.toolbar.system"); - button = toolBar.addButton({ - objectName: buttonName, - imageURL: Script.resolvePath("assets/images/tools/people.svg"), - visible: true, - alpha: 0.9 - }); -} else { - tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); - button = tablet.addButton({ - icon: "icons/tablet-icons/users-i.svg", - text: "USERS", - isActive: Menu.isOptionChecked(MENU_ITEM), - sortOrder: 11 - }); -} - - -function onClicked() { - Menu.setIsOptionChecked(MENU_ITEM, !Menu.isOptionChecked(MENU_ITEM)); - button.editProperties({isActive: Menu.isOptionChecked(MENU_ITEM)}); -} -button.clicked.connect(onClicked); - -// resolve these paths immediately -var MIN_MAX_BUTTON_SVG = Script.resolvePath("assets/images/tools/min-max-toggle.svg"); -var BASE_URL = Script.resolvePath("assets/images/tools/"); - -var PopUpMenu = function (properties) { - var value = properties.value, - promptOverlay, - valueOverlay, - buttonOverlay, - optionOverlays = [], - isDisplayingOptions = false, - OPTION_MARGIN = 4, - - MIN_MAX_BUTTON_SVG_WIDTH = 17.1, - MIN_MAX_BUTTON_SVG_HEIGHT = 32.5, - MIN_MAX_BUTTON_WIDTH = 14, - MIN_MAX_BUTTON_HEIGHT = MIN_MAX_BUTTON_WIDTH; - - function positionDisplayOptions() { - var y, - i; - - y = properties.y - (properties.values.length - 1) * properties.lineHeight - OPTION_MARGIN; - - for (i = 0; i < properties.values.length; i += 1) { - Overlays.editOverlay(optionOverlays[i], { - y: y - }); - y += properties.lineHeight; - } - } - - function showDisplayOptions() { - var i, - yOffScreen = Controller.getViewportDimensions().y; - - for (i = 0; i < properties.values.length; i += 1) { - optionOverlays[i] = Overlays.addOverlay("text", { - x: properties.x + properties.promptWidth, - y: yOffScreen, - width: properties.width - properties.promptWidth, - height: properties.textHeight + OPTION_MARGIN, // Only need to add margin at top to balance descenders - topMargin: OPTION_MARGIN, - leftMargin: OPTION_MARGIN, - color: properties.optionColor, - alpha: properties.optionAlpha, - backgroundColor: properties.popupBackgroundColor, - backgroundAlpha: properties.popupBackgroundAlpha, - text: properties.displayValues[i], - font: properties.font, - visible: true - }); - } - - positionDisplayOptions(); - - isDisplayingOptions = true; - } - - function deleteDisplayOptions() { - var i; - - for (i = 0; i < optionOverlays.length; i += 1) { - Overlays.deleteOverlay(optionOverlays[i]); - } - - isDisplayingOptions = false; - } - - function handleClick(overlay) { - var clicked = false, - i; - - if (overlay === valueOverlay || overlay === buttonOverlay) { - showDisplayOptions(); - return true; - } - - if (isDisplayingOptions) { - for (i = 0; i < optionOverlays.length; i += 1) { - if (overlay === optionOverlays[i]) { - value = properties.values[i]; - Overlays.editOverlay(valueOverlay, { - text: properties.displayValues[i] - }); - clicked = true; - } - } - - deleteDisplayOptions(); - } - - return clicked; - } - - function updatePosition(x, y) { - properties.x = x; - properties.y = y; - Overlays.editOverlay(promptOverlay, { - x: x, - y: y - }); - Overlays.editOverlay(valueOverlay, { - x: x + properties.promptWidth, - y: y - OPTION_MARGIN - }); - Overlays.editOverlay(buttonOverlay, { - x: x + properties.width - MIN_MAX_BUTTON_WIDTH - 1, - y: y - OPTION_MARGIN + 1 - }); - if (isDisplayingOptions) { - positionDisplayOptions(); - } - } - - function setVisible(visible) { - Overlays.editOverlay(promptOverlay, { - visible: visible - }); - Overlays.editOverlay(valueOverlay, { - visible: visible - }); - Overlays.editOverlay(buttonOverlay, { - visible: visible - }); - } - - function tearDown() { - Overlays.deleteOverlay(promptOverlay); - Overlays.deleteOverlay(valueOverlay); - Overlays.deleteOverlay(buttonOverlay); - if (isDisplayingOptions) { - deleteDisplayOptions(); - } - } - - function getValue() { - return value; - } - - function setValue(newValue) { - var index; - - index = properties.values.indexOf(newValue); - if (index !== -1) { - value = newValue; - Overlays.editOverlay(valueOverlay, { - text: properties.displayValues[index] - }); - } - } - - promptOverlay = Overlays.addOverlay("text", { - x: properties.x, - y: properties.y, - width: properties.promptWidth, - height: properties.textHeight, - topMargin: 0, - leftMargin: 0, - color: properties.promptColor, - alpha: properties.promptAlpha, - backgroundColor: properties.promptBackgroundColor, - backgroundAlpha: properties.promptBackgroundAlpha, - text: properties.prompt, - font: properties.font, - visible: properties.visible - }); - - valueOverlay = Overlays.addOverlay("text", { - x: properties.x + properties.promptWidth, - y: properties.y, - width: properties.width - properties.promptWidth, - height: properties.textHeight + OPTION_MARGIN, // Only need to add margin at top to balance descenders - topMargin: OPTION_MARGIN, - leftMargin: OPTION_MARGIN, - color: properties.optionColor, - alpha: properties.optionAlpha, - backgroundColor: properties.optionBackgroundColor, - backgroundAlpha: properties.optionBackgroundAlpha, - text: properties.displayValues[properties.values.indexOf(value)], - font: properties.font, - visible: properties.visible - }); - - buttonOverlay = Overlays.addOverlay("image", { - x: properties.x + properties.width - MIN_MAX_BUTTON_WIDTH - 1, - y: properties.y, - width: MIN_MAX_BUTTON_WIDTH, - height: MIN_MAX_BUTTON_HEIGHT, - imageURL: MIN_MAX_BUTTON_SVG, - subImage: { - x: 0, - y: 0, - width: MIN_MAX_BUTTON_SVG_WIDTH, - height: MIN_MAX_BUTTON_SVG_HEIGHT / 2 - }, - //color: properties.buttonColor, - alpha: properties.buttonAlpha, - visible: properties.visible - }); - - return { - updatePosition: updatePosition, - setVisible: setVisible, - handleClick: handleClick, - tearDown: tearDown, - getValue: getValue, - setValue: setValue - }; -}; - -var usersWindow = (function () { - - var WINDOW_WIDTH = 260, - WINDOW_MARGIN = 12, - WINDOW_BASE_MARGIN = 24, // A little less is needed in order look correct - WINDOW_FONT = { - size: 12 - }, - WINDOW_FOREGROUND_COLOR = { - red: 240, - green: 240, - blue: 240 - }, - WINDOW_FOREGROUND_ALPHA = 0.95, - WINDOW_HEADING_COLOR = { - red: 180, - green: 180, - blue: 180 - }, - WINDOW_HEADING_ALPHA = 0.95, - WINDOW_BACKGROUND_COLOR = { - red: 80, - green: 80, - blue: 80 - }, - WINDOW_BACKGROUND_ALPHA = 0.8, - windowPane, - windowHeading, - - // Margin on the left and right side of the window to keep - // it from getting too close to the edge of the screen which - // is unclickable. - WINDOW_MARGIN_X = 20, - - // Window border is similar to that of edit.js. - WINDOW_MARGIN_HALF = WINDOW_MARGIN / 2, - WINDOW_BORDER_WIDTH = WINDOW_WIDTH + 2 * WINDOW_MARGIN_HALF, - WINDOW_BORDER_TOP_MARGIN = 2 * WINDOW_MARGIN_HALF, - WINDOW_BORDER_BOTTOM_MARGIN = WINDOW_MARGIN_HALF, - WINDOW_BORDER_LEFT_MARGIN = WINDOW_MARGIN_HALF, - WINDOW_BORDER_RADIUS = 4, - WINDOW_BORDER_COLOR = { red: 255, green: 255, blue: 255 }, - WINDOW_BORDER_ALPHA = 0.5, - windowBorder, - - MIN_MAX_BUTTON_SVG = BASE_URL + "min-max-toggle.svg", - MIN_MAX_BUTTON_SVG_WIDTH = 17.1, - MIN_MAX_BUTTON_SVG_HEIGHT = 32.5, - MIN_MAX_BUTTON_WIDTH = 14, - MIN_MAX_BUTTON_HEIGHT = MIN_MAX_BUTTON_WIDTH, - MIN_MAX_BUTTON_COLOR = { - red: 255, - green: 255, - blue: 255 - }, - MIN_MAX_BUTTON_ALPHA = 0.9, - minimizeButton, - SCROLLBAR_BACKGROUND_WIDTH = 12, - SCROLLBAR_BACKGROUND_COLOR = { - red: 70, - green: 70, - blue: 70 - }, - SCROLLBAR_BACKGROUND_ALPHA = 0.8, - scrollbarBackground, - SCROLLBAR_BAR_MIN_HEIGHT = 5, - SCROLLBAR_BAR_COLOR = { - red: 170, - green: 170, - blue: 170 - }, - SCROLLBAR_BAR_ALPHA = 0.8, - SCROLLBAR_BAR_SELECTED_ALPHA = 0.95, - scrollbarBar, - scrollbarBackgroundHeight, - scrollbarBarHeight, - FRIENDS_BUTTON_SPACER = 6, // Space before add/remove friends button - FRIENDS_BUTTON_SVG = BASE_URL + "add-remove-friends.svg", - FRIENDS_BUTTON_SVG_WIDTH = 107, - FRIENDS_BUTTON_SVG_HEIGHT = 27, - FRIENDS_BUTTON_WIDTH = FRIENDS_BUTTON_SVG_WIDTH, - FRIENDS_BUTTON_HEIGHT = FRIENDS_BUTTON_SVG_HEIGHT, - FRIENDS_BUTTON_COLOR = { - red: 225, - green: 225, - blue: 225 - }, - FRIENDS_BUTTON_ALPHA = 0.95, - FRIENDS_WINDOW_URL = "https://metaverse.highfidelity.com/user/friends", - FRIENDS_WINDOW_WIDTH = 290, - FRIENDS_WINDOW_HEIGHT = 500, - FRIENDS_WINDOW_TITLE = "Add/Remove Friends", - friendsButton, - friendsWindow, - - OPTION_BACKGROUND_COLOR = { - red: 60, - green: 60, - blue: 60 - }, - OPTION_BACKGROUND_ALPHA = 0.1, - - DISPLAY_SPACER = 12, // Space before display control - DISPLAY_PROMPT = "Show me:", - DISPLAY_PROMPT_WIDTH = 60, - DISPLAY_EVERYONE = "everyone", - DISPLAY_FRIENDS = "friends", - DISPLAY_VALUES = [DISPLAY_EVERYONE, DISPLAY_FRIENDS], - DISPLAY_DISPLAY_VALUES = DISPLAY_VALUES, - DISPLAY_OPTIONS_BACKGROUND_COLOR = { - red: 120, - green: 120, - blue: 120 - }, - DISPLAY_OPTIONS_BACKGROUND_ALPHA = 0.9, - displayControl, - - VISIBILITY_SPACER = 6, // Space before visibility control - VISIBILITY_PROMPT = "Visible to:", - VISIBILITY_PROMPT_WIDTH = 60, - VISIBILITY_ALL = "all", - VISIBILITY_FRIENDS = "friends", - VISIBILITY_NONE = "none", - VISIBILITY_VALUES = [VISIBILITY_ALL, VISIBILITY_FRIENDS, VISIBILITY_NONE], - VISIBILITY_DISPLAY_VALUES = ["everyone", "friends", "no one"], - visibilityControl, - - windowHeight, - windowBorderHeight, - windowTextHeight, - windowLineSpacing, - windowLineHeight, // = windowTextHeight + windowLineSpacing - windowMinimumHeight, - - usersOnline, // Raw users data - linesOfUsers = [], // Array of indexes pointing into usersOnline - numUsersToDisplay = 0, - firstUserToDisplay = 0, - - API_URL = "https://metaverse.highfidelity.com/api/v1/users?status=online", - API_FRIENDS_FILTER = "&filter=friends", - HTTP_GET_TIMEOUT = 60000, // ms = 1 minute - usersRequest, - processUsers, - pollUsersTimedOut, - usersTimer = null, - USERS_UPDATE_TIMEOUT = 5000, // ms = 5s - - showMe, - myVisibility, - - MENU_NAME = "View", - MENU_ITEM = "Users Online", - MENU_ITEM_OVERLAYS = "Overlays", - MENU_ITEM_AFTER = MENU_ITEM_OVERLAYS, - - SETTING_USERS_SHOW_ME = "UsersWindow.ShowMe", - SETTING_USERS_VISIBLE_TO = "UsersWindow.VisibleTo", - SETTING_USERS_WINDOW_MINIMIZED = "UsersWindow.Minimized", - SETTING_USERS_WINDOW_OFFSET = "UsersWindow.Offset", - // +ve x, y values are offset from left, top of screen; -ve from right, bottom. - - isLoggedIn = false, - isVisible = true, - isMinimized = false, - isBorderVisible = false, - - viewport, - isMirrorDisplay = false, - isFullscreenMirror = false, - - windowPosition = {}, // Bottom left corner of window pane. - isMovingWindow = false, - movingClickOffset = { x: 0, y: 0 }, - - isUsingScrollbars = false, - isMovingScrollbar = false, - scrollbarBackgroundPosition = {}, - scrollbarBarPosition = {}, - scrollbarBarClickedAt, // 0.0 .. 1.0 - scrollbarValue = 0.0; // 0.0 .. 1.0 - - function isWindowDisabled() { - return !Menu.isOptionChecked(MENU_ITEM) || !Menu.isOptionChecked(MENU_ITEM_OVERLAYS); - } - - function isValueTrue(value) { - // Work around Boolean Settings values being read as string when Interface starts up but as Booleans when re-read after - // Being written if refresh script. - return value === true || value === "true"; - } - - function calculateWindowHeight() { - var AUDIO_METER_HEIGHT = 52, - MIRROR_HEIGHT = 220, - nonUsersHeight, - maxWindowHeight; - - if (isMinimized) { - windowHeight = windowTextHeight + WINDOW_MARGIN + WINDOW_BASE_MARGIN; - windowBorderHeight = windowHeight + WINDOW_BORDER_TOP_MARGIN + WINDOW_BORDER_BOTTOM_MARGIN; - return; - } - - // Reserve space for title, friends button, and option controls - nonUsersHeight = WINDOW_MARGIN + windowLineHeight + - (shouldShowFriendsButton() ? FRIENDS_BUTTON_SPACER + FRIENDS_BUTTON_HEIGHT : 0) + - DISPLAY_SPACER + - windowLineHeight + VISIBILITY_SPACER + - windowLineHeight + WINDOW_BASE_MARGIN; - - // Limit window to height of viewport above window position minus VU meter and mirror if displayed - windowHeight = linesOfUsers.length * windowLineHeight - windowLineSpacing + nonUsersHeight; - maxWindowHeight = windowPosition.y - AUDIO_METER_HEIGHT; - if (isMirrorDisplay && !isFullscreenMirror) { - maxWindowHeight -= MIRROR_HEIGHT; - } - windowHeight = Math.max(Math.min(windowHeight, maxWindowHeight), nonUsersHeight); - windowBorderHeight = windowHeight + WINDOW_BORDER_TOP_MARGIN + WINDOW_BORDER_BOTTOM_MARGIN; - - // Corresponding number of users to actually display - numUsersToDisplay = Math.max(Math.round((windowHeight - nonUsersHeight) / windowLineHeight), 0); - isUsingScrollbars = 0 < numUsersToDisplay && numUsersToDisplay < linesOfUsers.length; - if (isUsingScrollbars) { - firstUserToDisplay = Math.floor(scrollbarValue * (linesOfUsers.length - numUsersToDisplay)); - } else { - firstUserToDisplay = 0; - scrollbarValue = 0.0; - } - } - - function saturateWindowPosition() { - windowPosition.x = Math.max(WINDOW_MARGIN_X, Math.min(viewport.x - WINDOW_WIDTH - WINDOW_MARGIN_X, windowPosition.x)); - windowPosition.y = Math.max(windowMinimumHeight, Math.min(viewport.y, windowPosition.y)); - } - - function updateOverlayPositions() { - // Overlay positions are all relative to windowPosition; windowPosition is the position of the windowPane overlay. - var windowLeft = windowPosition.x, - windowTop = windowPosition.y - windowHeight, - x, - y; - - Overlays.editOverlay(windowBorder, { - x: windowPosition.x - WINDOW_BORDER_LEFT_MARGIN, - y: windowTop - WINDOW_BORDER_TOP_MARGIN - }); - Overlays.editOverlay(windowPane, { - x: windowLeft, - y: windowTop - }); - Overlays.editOverlay(windowHeading, { - x: windowLeft + WINDOW_MARGIN, - y: windowTop + WINDOW_MARGIN - }); - - Overlays.editOverlay(minimizeButton, { - x: windowLeft + WINDOW_WIDTH - WINDOW_MARGIN / 2 - MIN_MAX_BUTTON_WIDTH, - y: windowTop + WINDOW_MARGIN - }); - - scrollbarBackgroundPosition.x = windowLeft + WINDOW_WIDTH - 0.5 * WINDOW_MARGIN - SCROLLBAR_BACKGROUND_WIDTH; - scrollbarBackgroundPosition.y = windowTop + WINDOW_MARGIN + windowTextHeight; - Overlays.editOverlay(scrollbarBackground, { - x: scrollbarBackgroundPosition.x, - y: scrollbarBackgroundPosition.y - }); - scrollbarBarPosition.y = scrollbarBackgroundPosition.y + 1 + - scrollbarValue * (scrollbarBackgroundHeight - scrollbarBarHeight - 2); - Overlays.editOverlay(scrollbarBar, { - x: scrollbarBackgroundPosition.x + 1, - y: scrollbarBarPosition.y - }); - - - x = windowLeft + WINDOW_MARGIN; - y = windowPosition.y - - DISPLAY_SPACER - - windowLineHeight - VISIBILITY_SPACER - - windowLineHeight - WINDOW_BASE_MARGIN; - if (shouldShowFriendsButton()) { - y -= FRIENDS_BUTTON_HEIGHT; - Overlays.editOverlay(friendsButton, { - x: x, - y: y - }); - y += FRIENDS_BUTTON_HEIGHT; - } - - y += DISPLAY_SPACER; - displayControl.updatePosition(x, y); - - y += windowLineHeight + VISIBILITY_SPACER; - visibilityControl.updatePosition(x, y); - } - - function updateUsersDisplay() { - var displayText = "", - user, - userText, - textWidth, - maxTextWidth, - ellipsisWidth, - reducedTextWidth, - i; - - if (!isMinimized) { - maxTextWidth = WINDOW_WIDTH - (isUsingScrollbars ? SCROLLBAR_BACKGROUND_WIDTH : 0) - 2 * WINDOW_MARGIN; - ellipsisWidth = Overlays.textSize(windowPane, "...").width; - reducedTextWidth = maxTextWidth - ellipsisWidth; - - for (i = 0; i < numUsersToDisplay; i += 1) { - user = usersOnline[linesOfUsers[firstUserToDisplay + i]]; - userText = user.text; - textWidth = user.textWidth; - - if (textWidth > maxTextWidth) { - // Trim and append "..." to fit window width - maxTextWidth = maxTextWidth - Overlays.textSize(windowPane, "...").width; - while (textWidth > reducedTextWidth) { - userText = userText.slice(0, -1); - textWidth = Overlays.textSize(windowPane, userText).width; - } - userText += "..."; - } - - displayText += "\n" + userText; - } - - displayText = displayText.slice(1); // Remove leading "\n". - - scrollbarBackgroundHeight = numUsersToDisplay * windowLineHeight - windowLineSpacing / 2; - Overlays.editOverlay(scrollbarBackground, { - height: scrollbarBackgroundHeight, - visible: isLoggedIn && isUsingScrollbars - }); - scrollbarBarHeight = Math.max(numUsersToDisplay / linesOfUsers.length * scrollbarBackgroundHeight, - SCROLLBAR_BAR_MIN_HEIGHT); - Overlays.editOverlay(scrollbarBar, { - height: scrollbarBarHeight, - visible: isLoggedIn && isUsingScrollbars - }); - } - - Overlays.editOverlay(windowBorder, { - height: windowBorderHeight - }); - - Overlays.editOverlay(windowPane, { - height: windowHeight, - text: displayText - }); - - Overlays.editOverlay(windowHeading, { - text: isLoggedIn ? (linesOfUsers.length > 0 ? "Users online" : "No users online") : "Users online - log in to view" - }); - } - - function shouldShowFriendsButton() { - return isVisible && isLoggedIn && !isMinimized; - } - - function updateOverlayVisibility() { - Overlays.editOverlay(windowBorder, { - visible: isVisible && isBorderVisible - }); - Overlays.editOverlay(windowPane, { - visible: isVisible - }); - Overlays.editOverlay(windowHeading, { - visible: isVisible - }); - Overlays.editOverlay(minimizeButton, { - visible: isVisible - }); - Overlays.editOverlay(scrollbarBackground, { - visible: isVisible && isUsingScrollbars && !isMinimized - }); - Overlays.editOverlay(scrollbarBar, { - visible: isVisible && isUsingScrollbars && !isMinimized - }); - Overlays.editOverlay(friendsButton, { - visible: shouldShowFriendsButton() - }); - displayControl.setVisible(isVisible && !isMinimized); - visibilityControl.setVisible(isVisible && !isMinimized); - } - - function checkLoggedIn() { - var wasLoggedIn = isLoggedIn; - - isLoggedIn = Account.isLoggedIn(); - if (isLoggedIn !== wasLoggedIn) { - if (wasLoggedIn) { - setMinimized(true); - calculateWindowHeight(); - updateOverlayPositions(); - updateUsersDisplay(); - } - - updateOverlayVisibility(); - } - } - - function pollUsers() { - var url = API_URL; - - if (showMe === DISPLAY_FRIENDS) { - url += API_FRIENDS_FILTER; - } - - usersRequest = new XMLHttpRequest(); - usersRequest.open("GET", url, true); - usersRequest.timeout = HTTP_GET_TIMEOUT; - usersRequest.ontimeout = pollUsersTimedOut; - usersRequest.onreadystatechange = processUsers; - usersRequest.send(); - } - - processUsers = function () { - var response, - myUsername, - user, - userText, - i; - - if (usersRequest.readyState === usersRequest.DONE) { - if (usersRequest.status === 200) { - response = JSON.parse(usersRequest.responseText); - if (response.status !== "success") { - print("Error: Request for users status returned status = " + response.status); - usersTimer = Script.setTimeout(pollUsers, HTTP_GET_TIMEOUT); // Try again after a longer delay. - return; - } - if (!response.hasOwnProperty("data") || !response.data.hasOwnProperty("users")) { - print("Error: Request for users status returned invalid data"); - usersTimer = Script.setTimeout(pollUsers, HTTP_GET_TIMEOUT); // Try again after a longer delay. - return; - } - - usersOnline = response.data.users; - myUsername = GlobalServices.username; - linesOfUsers = []; - for (i = 0; i < usersOnline.length; i += 1) { - user = usersOnline[i]; - if (user.username !== myUsername && user.online) { - userText = user.username; - if (user.location.root) { - userText += " @ " + user.location.root.name; - } - - usersOnline[i].text = userText; - usersOnline[i].textWidth = Overlays.textSize(windowPane, userText).width; - - linesOfUsers.push(i); - } - } - - checkLoggedIn(); - calculateWindowHeight(); - updateUsersDisplay(); - updateOverlayPositions(); - - } else { - print("Error: Request for users status returned " + usersRequest.status + " " + usersRequest.statusText); - usersTimer = Script.setTimeout(pollUsers, HTTP_GET_TIMEOUT); // Try again after a longer delay. - return; - } - - usersTimer = Script.setTimeout(pollUsers, USERS_UPDATE_TIMEOUT); // Update after finished processing. - } - }; - - pollUsersTimedOut = function () { - print("Error: Request for users status timed out"); - usersTimer = Script.setTimeout(pollUsers, HTTP_GET_TIMEOUT); // Try again after a longer delay. - }; - - function setVisible(visible) { - isVisible = visible; - - if (isVisible) { - if (usersTimer === null) { - pollUsers(); - } - } else { - Script.clearTimeout(usersTimer); - usersTimer = null; - } - - updateOverlayVisibility(); - } - - function setMinimized(minimized) { - isMinimized = minimized; - Overlays.editOverlay(minimizeButton, { - subImage: { - y: isMinimized ? MIN_MAX_BUTTON_SVG_HEIGHT / 2 : 0 - } - }); - updateOverlayVisibility(); - Settings.setValue(SETTING_USERS_WINDOW_MINIMIZED, isMinimized); - } - - function onMenuItemEvent(event) { - if (event === MENU_ITEM) { - setVisible(Menu.isOptionChecked(MENU_ITEM)); - } - } - - function onFindableByChanged(event) { - if (VISIBILITY_VALUES.indexOf(event) !== -1) { - myVisibility = event; - visibilityControl.setValue(event); - Settings.setValue(SETTING_USERS_VISIBLE_TO, myVisibility); - } else { - print("Error: Unrecognized onFindableByChanged value: " + event); - } - } - - function onMousePressEvent(event) { - var clickedOverlay, - numLinesBefore, - overlayX, - overlayY, - minY, - maxY, - lineClicked, - userClicked, - delta; - - if (!isVisible || isWindowDisabled()) { - return; - } - - clickedOverlay = Overlays.getOverlayAtPoint({ - x: event.x, - y: event.y - }); - - if (displayControl.handleClick(clickedOverlay)) { - if (usersTimer !== null) { - Script.clearTimeout(usersTimer); - usersTimer = null; - } - pollUsers(); - showMe = displayControl.getValue(); - Settings.setValue(SETTING_USERS_SHOW_ME, showMe); - return; - } - - if (visibilityControl.handleClick(clickedOverlay)) { - myVisibility = visibilityControl.getValue(); - GlobalServices.findableBy = myVisibility; - Settings.setValue(SETTING_USERS_VISIBLE_TO, myVisibility); - return; - } - - if (clickedOverlay === windowPane) { - - overlayX = event.x - windowPosition.x - WINDOW_MARGIN; - overlayY = event.y - windowPosition.y + windowHeight - WINDOW_MARGIN - windowLineHeight; - - numLinesBefore = Math.round(overlayY / windowLineHeight); - minY = numLinesBefore * windowLineHeight; - maxY = minY + windowTextHeight; - - lineClicked = -1; - if (minY <= overlayY && overlayY <= maxY) { - lineClicked = numLinesBefore; - } - - userClicked = firstUserToDisplay + lineClicked; - - if (0 <= userClicked && userClicked < linesOfUsers.length && 0 <= overlayX && - overlayX <= usersOnline[linesOfUsers[userClicked]].textWidth) { - //print("Go to " + usersOnline[linesOfUsers[userClicked]].username); - location.goToUser(usersOnline[linesOfUsers[userClicked]].username); - } - - return; - } - - if (clickedOverlay === minimizeButton) { - setMinimized(!isMinimized); - calculateWindowHeight(); - updateOverlayPositions(); - updateUsersDisplay(); - return; - } - - if (clickedOverlay === scrollbarBar) { - scrollbarBarClickedAt = (event.y - scrollbarBarPosition.y) / scrollbarBarHeight; - Overlays.editOverlay(scrollbarBar, { - backgroundAlpha: SCROLLBAR_BAR_SELECTED_ALPHA - }); - isMovingScrollbar = true; - return; - } - - if (clickedOverlay === scrollbarBackground) { - delta = scrollbarBarHeight / (scrollbarBackgroundHeight - scrollbarBarHeight); - - if (event.y < scrollbarBarPosition.y) { - scrollbarValue = Math.max(scrollbarValue - delta, 0.0); - } else { - scrollbarValue = Math.min(scrollbarValue + delta, 1.0); - } - - firstUserToDisplay = Math.floor(scrollbarValue * (linesOfUsers.length - numUsersToDisplay)); - updateOverlayPositions(); - updateUsersDisplay(); - return; - } - - if (clickedOverlay === friendsButton) { - if (!friendsWindow) { - friendsWindow = new OverlayWebWindow({ - title: FRIENDS_WINDOW_TITLE, - width: FRIENDS_WINDOW_WIDTH, - height: FRIENDS_WINDOW_HEIGHT, - visible: false - }); - } - friendsWindow.setURL(FRIENDS_WINDOW_URL); - friendsWindow.setVisible(true); - friendsWindow.raise(); - return; - } - - if (clickedOverlay === windowBorder) { - movingClickOffset = { - x: event.x - windowPosition.x, - y: event.y - windowPosition.y - }; - - isMovingWindow = true; - } - } - - function onMouseMoveEvent(event) { - var isVisible; - - if (!isLoggedIn || isWindowDisabled()) { - return; - } - - if (isMovingScrollbar) { - if (scrollbarBackgroundPosition.x - WINDOW_MARGIN <= event.x && - event.x <= scrollbarBackgroundPosition.x + SCROLLBAR_BACKGROUND_WIDTH + WINDOW_MARGIN && - scrollbarBackgroundPosition.y - WINDOW_MARGIN <= event.y && - event.y <= scrollbarBackgroundPosition.y + scrollbarBackgroundHeight + WINDOW_MARGIN) { - scrollbarValue = (event.y - scrollbarBarClickedAt * scrollbarBarHeight - scrollbarBackgroundPosition.y) / - (scrollbarBackgroundHeight - scrollbarBarHeight - 2); - scrollbarValue = Math.min(Math.max(scrollbarValue, 0.0), 1.0); - firstUserToDisplay = Math.floor(scrollbarValue * (linesOfUsers.length - numUsersToDisplay)); - updateOverlayPositions(); - updateUsersDisplay(); - } else { - Overlays.editOverlay(scrollbarBar, { - backgroundAlpha: SCROLLBAR_BAR_ALPHA - }); - isMovingScrollbar = false; - } - } - - if (isMovingWindow) { - windowPosition = { - x: event.x - movingClickOffset.x, - y: event.y - movingClickOffset.y - }; - - saturateWindowPosition(); - calculateWindowHeight(); - updateOverlayPositions(); - updateUsersDisplay(); - - } else { - - isVisible = isBorderVisible; - if (isVisible) { - isVisible = windowPosition.x - WINDOW_BORDER_LEFT_MARGIN <= event.x && - event.x <= windowPosition.x - WINDOW_BORDER_LEFT_MARGIN + WINDOW_BORDER_WIDTH && - windowPosition.y - windowHeight - WINDOW_BORDER_TOP_MARGIN <= event.y && - event.y <= windowPosition.y + WINDOW_BORDER_BOTTOM_MARGIN; - } else { - isVisible = windowPosition.x <= event.x && event.x <= windowPosition.x + WINDOW_WIDTH && - windowPosition.y - windowHeight <= event.y && event.y <= windowPosition.y; - } - if (isVisible !== isBorderVisible) { - isBorderVisible = isVisible; - Overlays.editOverlay(windowBorder, { - visible: isBorderVisible - }); - } - } - } - - function onMouseReleaseEvent() { - var offset = {}; - - if (isWindowDisabled()) { - return; - } - - if (isMovingScrollbar) { - Overlays.editOverlay(scrollbarBar, { - backgroundAlpha: SCROLLBAR_BAR_ALPHA - }); - isMovingScrollbar = false; - } - - if (isMovingWindow) { - // Save offset of bottom of window to nearest edge of the window. - offset.x = (windowPosition.x + WINDOW_WIDTH / 2 < viewport.x / 2) ? - windowPosition.x : windowPosition.x - viewport.x; - offset.y = (windowPosition.y < viewport.y / 2) ? - windowPosition.y : windowPosition.y - viewport.y; - Settings.setValue(SETTING_USERS_WINDOW_OFFSET, JSON.stringify(offset)); - isMovingWindow = false; - } - } - - function onScriptUpdate() { - var oldViewport = viewport, - oldIsMirrorDisplay = isMirrorDisplay, - oldIsFullscreenMirror = isFullscreenMirror, - MIRROR_MENU_ITEM = "Mirror", - FULLSCREEN_MIRROR_MENU_ITEM = "Fullscreen Mirror"; - - if (isWindowDisabled()) { - return; - } - - viewport = Controller.getViewportDimensions(); - isMirrorDisplay = Menu.isOptionChecked(MIRROR_MENU_ITEM); - isFullscreenMirror = Menu.isOptionChecked(FULLSCREEN_MIRROR_MENU_ITEM); - - if (viewport.y !== oldViewport.y || isMirrorDisplay !== oldIsMirrorDisplay || - isFullscreenMirror !== oldIsFullscreenMirror) { - calculateWindowHeight(); - updateUsersDisplay(); - } - - if (viewport.y !== oldViewport.y) { - if (windowPosition.y > oldViewport.y / 2) { - // Maintain position w.r.t. bottom of window. - windowPosition.y = viewport.y - (oldViewport.y - windowPosition.y); - } - } - - if (viewport.x !== oldViewport.x) { - if (windowPosition.x + (WINDOW_WIDTH / 2) > oldViewport.x / 2) { - // Maintain position w.r.t. right of window. - windowPosition.x = viewport.x - (oldViewport.x - windowPosition.x); - } - } - - updateOverlayPositions(); - } - - function setUp() { - var textSizeOverlay, - offsetSetting, - offset = {}, - hmdViewport; - - textSizeOverlay = Overlays.addOverlay("text", { - font: WINDOW_FONT, - visible: false - }); - windowTextHeight = Math.floor(Overlays.textSize(textSizeOverlay, "1").height); - windowLineSpacing = Math.floor(Overlays.textSize(textSizeOverlay, "1\n2").height - 2 * windowTextHeight); - windowLineHeight = windowTextHeight + windowLineSpacing; - windowMinimumHeight = windowTextHeight + WINDOW_MARGIN + WINDOW_BASE_MARGIN; - Overlays.deleteOverlay(textSizeOverlay); - - viewport = Controller.getViewportDimensions(); - - offsetSetting = Settings.getValue(SETTING_USERS_WINDOW_OFFSET); - if (offsetSetting !== "") { - offset = JSON.parse(Settings.getValue(SETTING_USERS_WINDOW_OFFSET)); - } - if (offset.hasOwnProperty("x") && offset.hasOwnProperty("y")) { - windowPosition.x = offset.x < 0 ? viewport.x + offset.x : offset.x; - windowPosition.y = offset.y <= 0 ? viewport.y + offset.y : offset.y; - } else { - hmdViewport = Controller.getRecommendedOverlayRect(); - windowPosition = { - x: (viewport.x - hmdViewport.width) / 2, // HMD viewport is narrower than screen. - y: hmdViewport.height // HMD viewport starts at top of screen but only extends down so far. - }; - } - - saturateWindowPosition(); - calculateWindowHeight(); - - windowBorder = Overlays.addOverlay("rectangle", { - x: 0, - y: viewport.y, // Start up off-screen - width: WINDOW_BORDER_WIDTH, - height: windowBorderHeight, - radius: WINDOW_BORDER_RADIUS, - color: WINDOW_BORDER_COLOR, - alpha: WINDOW_BORDER_ALPHA, - visible: false - }); - - windowPane = Overlays.addOverlay("text", { - x: 0, - y: viewport.y, - width: WINDOW_WIDTH, - height: windowHeight, - topMargin: WINDOW_MARGIN + windowLineHeight, - leftMargin: WINDOW_MARGIN, - color: WINDOW_FOREGROUND_COLOR, - alpha: WINDOW_FOREGROUND_ALPHA, - backgroundColor: WINDOW_BACKGROUND_COLOR, - backgroundAlpha: WINDOW_BACKGROUND_ALPHA, - text: "", - font: WINDOW_FONT, - visible: false - }); - - windowHeading = Overlays.addOverlay("text", { - x: 0, - y: viewport.y, - width: WINDOW_WIDTH - 2 * WINDOW_MARGIN, - height: windowTextHeight, - topMargin: 0, - leftMargin: 0, - color: WINDOW_HEADING_COLOR, - alpha: WINDOW_HEADING_ALPHA, - backgroundAlpha: 0.0, - text: "Users online", - font: WINDOW_FONT, - visible: false - }); - - minimizeButton = Overlays.addOverlay("image", { - x: 0, - y: viewport.y, - width: MIN_MAX_BUTTON_WIDTH, - height: MIN_MAX_BUTTON_HEIGHT, - imageURL: MIN_MAX_BUTTON_SVG, - subImage: { - x: 0, - y: 0, - width: MIN_MAX_BUTTON_SVG_WIDTH, - height: MIN_MAX_BUTTON_SVG_HEIGHT / 2 - }, - color: MIN_MAX_BUTTON_COLOR, - alpha: MIN_MAX_BUTTON_ALPHA, - visible: false - }); - - scrollbarBackgroundPosition = { - x: 0, - y: viewport.y - }; - scrollbarBackground = Overlays.addOverlay("text", { - x: 0, - y: scrollbarBackgroundPosition.y, - width: SCROLLBAR_BACKGROUND_WIDTH, - height: windowTextHeight, - backgroundColor: SCROLLBAR_BACKGROUND_COLOR, - backgroundAlpha: SCROLLBAR_BACKGROUND_ALPHA, - text: "", - visible: false - }); - - scrollbarBarPosition = { - x: 0, - y: viewport.y - }; - scrollbarBar = Overlays.addOverlay("text", { - x: 0, - y: scrollbarBarPosition.y, - width: SCROLLBAR_BACKGROUND_WIDTH - 2, - height: windowTextHeight, - backgroundColor: SCROLLBAR_BAR_COLOR, - backgroundAlpha: SCROLLBAR_BAR_ALPHA, - text: "", - visible: false - }); - - friendsButton = Overlays.addOverlay("image", { - x: 0, - y: viewport.y, - width: FRIENDS_BUTTON_WIDTH, - height: FRIENDS_BUTTON_HEIGHT, - imageURL: FRIENDS_BUTTON_SVG, - subImage: { - x: 0, - y: 0, - width: FRIENDS_BUTTON_SVG_WIDTH, - height: FRIENDS_BUTTON_SVG_HEIGHT - }, - color: FRIENDS_BUTTON_COLOR, - alpha: FRIENDS_BUTTON_ALPHA, - visible: false - }); - - showMe = Settings.getValue(SETTING_USERS_SHOW_ME, ""); - if (DISPLAY_VALUES.indexOf(showMe) === -1) { - showMe = DISPLAY_EVERYONE; - } - - displayControl = new PopUpMenu({ - prompt: DISPLAY_PROMPT, - value: showMe, - values: DISPLAY_VALUES, - displayValues: DISPLAY_DISPLAY_VALUES, - x: 0, - y: viewport.y, - width: WINDOW_WIDTH - 1.5 * WINDOW_MARGIN, - promptWidth: DISPLAY_PROMPT_WIDTH, - lineHeight: windowLineHeight, - textHeight: windowTextHeight, - font: WINDOW_FONT, - promptColor: WINDOW_HEADING_COLOR, - promptAlpha: WINDOW_HEADING_ALPHA, - promptBackgroundColor: WINDOW_BACKGROUND_COLOR, - promptBackgroundAlpha: 0.0, - optionColor: WINDOW_FOREGROUND_COLOR, - optionAlpha: WINDOW_FOREGROUND_ALPHA, - optionBackgroundColor: OPTION_BACKGROUND_COLOR, - optionBackgroundAlpha: OPTION_BACKGROUND_ALPHA, - popupBackgroundColor: DISPLAY_OPTIONS_BACKGROUND_COLOR, - popupBackgroundAlpha: DISPLAY_OPTIONS_BACKGROUND_ALPHA, - buttonColor: MIN_MAX_BUTTON_COLOR, - buttonAlpha: MIN_MAX_BUTTON_ALPHA, - visible: false - }); - - myVisibility = Settings.getValue(SETTING_USERS_VISIBLE_TO, ""); - if (VISIBILITY_VALUES.indexOf(myVisibility) === -1) { - myVisibility = VISIBILITY_FRIENDS; - } - GlobalServices.findableBy = myVisibility; - - visibilityControl = new PopUpMenu({ - prompt: VISIBILITY_PROMPT, - value: myVisibility, - values: VISIBILITY_VALUES, - displayValues: VISIBILITY_DISPLAY_VALUES, - x: 0, - y: viewport.y, - width: WINDOW_WIDTH - 1.5 * WINDOW_MARGIN, - promptWidth: VISIBILITY_PROMPT_WIDTH, - lineHeight: windowLineHeight, - textHeight: windowTextHeight, - font: WINDOW_FONT, - promptColor: WINDOW_HEADING_COLOR, - promptAlpha: WINDOW_HEADING_ALPHA, - promptBackgroundColor: WINDOW_BACKGROUND_COLOR, - promptBackgroundAlpha: 0.0, - optionColor: WINDOW_FOREGROUND_COLOR, - optionAlpha: WINDOW_FOREGROUND_ALPHA, - optionBackgroundColor: OPTION_BACKGROUND_COLOR, - optionBackgroundAlpha: OPTION_BACKGROUND_ALPHA, - popupBackgroundColor: DISPLAY_OPTIONS_BACKGROUND_COLOR, - popupBackgroundAlpha: DISPLAY_OPTIONS_BACKGROUND_ALPHA, - buttonColor: MIN_MAX_BUTTON_COLOR, - buttonAlpha: MIN_MAX_BUTTON_ALPHA, - visible: false - }); - - Controller.mousePressEvent.connect(onMousePressEvent); - Controller.mouseMoveEvent.connect(onMouseMoveEvent); - Controller.mouseReleaseEvent.connect(onMouseReleaseEvent); - - Menu.addMenuItem({ - menuName: MENU_NAME, - menuItemName: MENU_ITEM, - afterItem: MENU_ITEM_AFTER, - isCheckable: true, - isChecked: isVisible - }); - Menu.menuItemEvent.connect(onMenuItemEvent); - - GlobalServices.findableByChanged.connect(onFindableByChanged); - - Script.update.connect(onScriptUpdate); - - pollUsers(); - - // Set minimized at end - setup code does not handle `minimized == false` correctly - setMinimized(isValueTrue(Settings.getValue(SETTING_USERS_WINDOW_MINIMIZED, true))); - } - - function tearDown() { - Menu.removeMenuItem(MENU_NAME, MENU_ITEM); - - Script.clearTimeout(usersTimer); - Overlays.deleteOverlay(windowBorder); - Overlays.deleteOverlay(windowPane); - Overlays.deleteOverlay(windowHeading); - Overlays.deleteOverlay(minimizeButton); - Overlays.deleteOverlay(scrollbarBackground); - Overlays.deleteOverlay(scrollbarBar); - Overlays.deleteOverlay(friendsButton); - displayControl.tearDown(); - visibilityControl.tearDown(); - } - - setUp(); - Script.scriptEnding.connect(tearDown); -}()); - -function cleanup () { - //remove tablet button - button.clicked.disconnect(onClicked); - if (tablet) { - tablet.removeButton(button); - } - if (toolBar) { - toolBar.removeButton(buttonName); - } -} -Script.scriptEnding.connect(cleanup); - -}()); // END LOCAL_SCOPE diff --git a/scripts/system/voxels.js b/scripts/system/voxels.js index 3c219ebc7a..2f1d0eced9 100644 --- a/scripts/system/voxels.js +++ b/scripts/system/voxels.js @@ -253,7 +253,7 @@ function addTerrainBlock() { if (alreadyThere) { // there is already a terrain block under MyAvatar. // try in front of the avatar. - facingPosition = Vec3.sum(MyAvatar.position, Vec3.multiply(8.0, Quat.getFront(Camera.getOrientation()))); + facingPosition = Vec3.sum(MyAvatar.position, Vec3.multiply(8.0, Quat.getForward(Camera.getOrientation()))); facingPosition = Vec3.sum(facingPosition, { x: 8, y: 8, diff --git a/scripts/tutorials/NBody/makePlanets.js b/scripts/tutorials/NBody/makePlanets.js index 58a3c7cc2d..21415ccdc2 100644 --- a/scripts/tutorials/NBody/makePlanets.js +++ b/scripts/tutorials/NBody/makePlanets.js @@ -53,7 +53,7 @@ var deleteButton = toolBar.addOverlay("image", { }); function inFrontOfMe(distance) { - return Vec3.sum(Camera.getPosition(), Vec3.multiply(distance, Quat.getFront(Camera.getOrientation()))); + return Vec3.sum(Camera.getPosition(), Vec3.multiply(distance, Quat.getForward(Camera.getOrientation()))); } function onButtonClick() { diff --git a/scripts/tutorials/butterflies.js b/scripts/tutorials/butterflies.js index 55bafc0a27..9d8d1de52c 100644 --- a/scripts/tutorials/butterflies.js +++ b/scripts/tutorials/butterflies.js @@ -44,8 +44,8 @@ var FIXED_LOCATION = false; if (!FIXED_LOCATION) { var flockPosition = Vec3.sum(MyAvatar.position,Vec3.sum( - Vec3.multiply(Quat.getFront(MyAvatar.orientation), DISTANCE_ABOVE_ME), - Vec3.multiply(Quat.getFront(MyAvatar.orientation), DISTANCE_IN_FRONT_OF_ME))); + Vec3.multiply(Quat.getForward(MyAvatar.orientation), DISTANCE_ABOVE_ME), + Vec3.multiply(Quat.getForward(MyAvatar.orientation), DISTANCE_IN_FRONT_OF_ME))); } else { var flockPosition = { x: 4999.6, y: 4986.5, z: 5003.5 }; } @@ -119,7 +119,7 @@ function updateButterflies(deltaTime) { var HORIZ_SCALE = 0.50; var VERT_SCALE = 0.50; var newHeading = Math.random() * 360.0; - var newVelocity = Vec3.multiply(HORIZ_SCALE, Quat.getFront(Quat.fromPitchYawRollDegrees(0.0, newHeading, 0.0))); + var newVelocity = Vec3.multiply(HORIZ_SCALE, Quat.getForward(Quat.fromPitchYawRollDegrees(0.0, newHeading, 0.0))); newVelocity.y = (Math.random() + 0.5) * VERT_SCALE; Entities.editEntity(butterflies[i], { rotation: Quat.fromPitchYawRollDegrees(-80 + Math.random() * 20, newHeading, (Math.random() - 0.5) * 10), velocity: newVelocity } ); diff --git a/scripts/tutorials/createCow.js b/scripts/tutorials/createCow.js index 7446aa0fd0..16498e0e8c 100644 --- a/scripts/tutorials/createCow.js +++ b/scripts/tutorials/createCow.js @@ -18,7 +18,7 @@ var orientation = MyAvatar.orientation; orientation = Quat.safeEulerAngles(orientation); orientation.x = 0; orientation = Quat.fromVec3Degrees(orientation); -var center = Vec3.sum(MyAvatar.getHeadPosition(), Vec3.multiply(2, Quat.getFront(orientation))); +var center = Vec3.sum(MyAvatar.getHeadPosition(), Vec3.multiply(2, Quat.getForward(orientation))); // An entity is described and created by specifying a map of properties var cow = Entities.addEntity({ diff --git a/scripts/tutorials/createDice.js b/scripts/tutorials/createDice.js index 0d39d11d48..46ad0172aa 100644 --- a/scripts/tutorials/createDice.js +++ b/scripts/tutorials/createDice.js @@ -127,8 +127,8 @@ function mousePressEvent(event) { deleteDice(); } else if (clickedOverlay == diceButton) { var HOW_HARD = 2.0; - var position = Vec3.sum(Camera.getPosition(), Quat.getFront(Camera.getOrientation())); - var velocity = Vec3.multiply(HOW_HARD, Quat.getFront(Camera.getOrientation())); + var position = Vec3.sum(Camera.getPosition(), Quat.getForward(Camera.getOrientation())); + var velocity = Vec3.multiply(HOW_HARD, Quat.getForward(Camera.getOrientation())); shootDice(position, velocity); madeSound = false; } diff --git a/scripts/tutorials/createFlashlight.js b/scripts/tutorials/createFlashlight.js index 0e3581a435..f3e1e72182 100644 --- a/scripts/tutorials/createFlashlight.js +++ b/scripts/tutorials/createFlashlight.js @@ -16,7 +16,7 @@ var center = Vec3.sum(Vec3.sum(MyAvatar.position, { x: 0, y: 0.5, z: 0 -}), Vec3.multiply(0.5, Quat.getFront(Camera.getOrientation()))); +}), Vec3.multiply(0.5, Quat.getForward(Camera.getOrientation()))); var flashlight = Entities.addEntity({ type: "Model", diff --git a/scripts/tutorials/createGolfClub.js b/scripts/tutorials/createGolfClub.js index aa9834276a..21e60f26ef 100644 --- a/scripts/tutorials/createGolfClub.js +++ b/scripts/tutorials/createGolfClub.js @@ -15,7 +15,7 @@ var orientation = MyAvatar.orientation; orientation = Quat.safeEulerAngles(orientation); orientation.x = 0; orientation = Quat.fromVec3Degrees(orientation); -var center = Vec3.sum(MyAvatar.getHeadPosition(), Vec3.multiply(2, Quat.getFront(orientation))); +var center = Vec3.sum(MyAvatar.getHeadPosition(), Vec3.multiply(2, Quat.getForward(orientation))); var CLUB_MODEL = "http://hifi-production.s3.amazonaws.com/tutorials/golfClub/putter_VR.fbx"; var CLUB_COLLISION_HULL = "http://hifi-production.s3.amazonaws.com/tutorials/golfClub/club_collision_hull.obj"; diff --git a/scripts/tutorials/createPictureFrame.js b/scripts/tutorials/createPictureFrame.js index 4a1e5b16a7..873b604bfa 100644 --- a/scripts/tutorials/createPictureFrame.js +++ b/scripts/tutorials/createPictureFrame.js @@ -14,7 +14,7 @@ var center = Vec3.sum(Vec3.sum(MyAvatar.position, { x: 0, y: 0.5, z: 0 -}), Vec3.multiply(1, Quat.getFront(Camera.getOrientation()))); +}), Vec3.multiply(1, Quat.getForward(Camera.getOrientation()))); // this is just a model exported from blender with a texture named 'Picture' on one face. also made it emissive so it doesn't require lighting. var MODEL_URL = "http://hifi-production.s3.amazonaws.com/tutorials/pictureFrame/finalFrame.fbx"; diff --git a/scripts/tutorials/createPingPongGun.js b/scripts/tutorials/createPingPongGun.js index a077e5308d..c86a78e96d 100644 --- a/scripts/tutorials/createPingPongGun.js +++ b/scripts/tutorials/createPingPongGun.js @@ -14,7 +14,7 @@ var center = Vec3.sum(Vec3.sum(MyAvatar.position, { x: 0, y: 0.5, z: 0 -}), Vec3.multiply(0.5, Quat.getFront(Camera.getOrientation()))); +}), Vec3.multiply(0.5, Quat.getForward(Camera.getOrientation()))); var pingPongGunProperties = { diff --git a/scripts/tutorials/createPistol.js b/scripts/tutorials/createPistol.js index ae2f398840..8851f53d09 100644 --- a/scripts/tutorials/createPistol.js +++ b/scripts/tutorials/createPistol.js @@ -6,7 +6,7 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -var center = Vec3.sum(MyAvatar.position, Vec3.multiply(1.5, Quat.getFront(Camera.getOrientation()))); +var center = Vec3.sum(MyAvatar.position, Vec3.multiply(1.5, Quat.getForward(Camera.getOrientation()))); var SCRIPT_URL = "http://hifi-production.s3.amazonaws.com/tutorials/entity_scripts/pistol.js"; var MODEL_URL = "http://hifi-production.s3.amazonaws.com/tutorials/pistol/gun.fbx"; var COLLISION_SOUND_URL = 'http://hifi-production.s3.amazonaws.com/tutorials/pistol/drop.wav' diff --git a/scripts/tutorials/createSoundMaker.js b/scripts/tutorials/createSoundMaker.js index b79c650e27..2d86864982 100644 --- a/scripts/tutorials/createSoundMaker.js +++ b/scripts/tutorials/createSoundMaker.js @@ -13,7 +13,7 @@ var center = Vec3.sum(Vec3.sum(MyAvatar.position, { x: 0, y: 0.5, z: 0 -}), Vec3.multiply(1, Quat.getFront(Camera.getOrientation()))); +}), Vec3.multiply(1, Quat.getForward(Camera.getOrientation()))); function makeBell() { var soundMakerProperties = { diff --git a/scripts/tutorials/entity_scripts/golfClub.js b/scripts/tutorials/entity_scripts/golfClub.js index 2df3be8b60..6342838aa4 100644 --- a/scripts/tutorials/entity_scripts/golfClub.js +++ b/scripts/tutorials/entity_scripts/golfClub.js @@ -57,7 +57,7 @@ // Position yourself facing in the direction you were originally facing, but with a // point on the ground *away* meters from *position* and in front of you. - var offset = Quat.getFront(MyAvatar.orientation); + var offset = Quat.getForward(MyAvatar.orientation); offset.y = 0.0; offset = Vec3.multiply(-away, Vec3.normalize(offset)); var newAvatarPosition = Vec3.sum(position, offset); @@ -72,7 +72,7 @@ } function inFrontOfMe() { - return Vec3.sum(MyAvatar.position, Vec3.multiply(BALL_DROP_DISTANCE, Quat.getFront(MyAvatar.orientation))); + return Vec3.sum(MyAvatar.position, Vec3.multiply(BALL_DROP_DISTANCE, Quat.getForward(MyAvatar.orientation))); } function avatarHalfHeight() { diff --git a/scripts/tutorials/entity_scripts/magneticBlock.js b/scripts/tutorials/entity_scripts/magneticBlock.js new file mode 100644 index 0000000000..1ec5f2a6c6 --- /dev/null +++ b/scripts/tutorials/entity_scripts/magneticBlock.js @@ -0,0 +1,151 @@ +// +// magneticBlock.js +// +// Created by Matti Lahtinen 4/3/2017 +// Copyright 2017 High Fidelity, Inc. +// +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// +// Makes the entity the script is bound to connect to nearby, similarly sized entities, like a magnet. + +(function() { + var SNAPSOUND_SOURCE = SoundCache.getSound(Script.resolvePath("../../system/assets/sounds/entitySnap.wav?xrs")); + var RANGE_MULTIPLER = 1.5; + var MAX_SCALE = 2; + var MIN_SCALE = 0.5; + + // Helper for detecting nearby objects near entityProperties, with the scale calculated by the dimensions of the object. + function findEntitiesInRange(entityProperties) { + var dimensions = entityProperties.dimensions; + // Average of the dimensions instead of full value. + return Entities.findEntities(entityProperties.position, + ((dimensions.x + dimensions.y + dimensions.z) / 3) * RANGE_MULTIPLER); + } + + function getNearestValidEntityProperties(releasedProperties) { + var entities = findEntitiesInRange(releasedProperties); + var nearestEntity = null; + var nearest = Number.MAX_VALUE - 1; + var releaseSize = Vec3.length(releasedProperties.dimensions); + entities.forEach(function(entityId) { + if (entityId !== releasedProperties.id) { + var entity = Entities.getEntityProperties(entityId, ['position', 'rotation', 'dimensions']); + var distance = Vec3.distance(releasedProperties.position, entity.position); + var scale = releaseSize / Vec3.length(entity.dimensions); + + if (distance < nearest && (scale >= MIN_SCALE && scale <= MAX_SCALE)) { + nearestEntity = entity; + nearest = distance; + } + } + }); + return nearestEntity; + } + // Create the 'class' + function MagneticBlock() {} + // Bind pre-emptive events + MagneticBlock.prototype = { + /* + When script is bound to an entity, preload is the first callback called with the entityID. + It will behave as the constructor + */ + preload: function(id) { + /* + We will now override any existing userdata with the grabbable property. + Only retrieving userData + */ + var entityProperties = Entities.getEntityProperties(id, ['userData']); + var userData = { + grabbableKey: {} + }; + // Check if existing userData field exists. + if (entityProperties.userData && entityProperties.userData.length > 0) { + try { + userData = JSON.parse(entityProperties.userData); + if (!userData.grabbableKey) { + userData.grabbableKey = {}; // If by random change there is no grabbableKey in the userData. + } + } catch (e) { + // if user data is not valid json, we will simply overwrite it. + } + } + // Object must be triggerable inorder to bind releaseGrabEvent + userData.grabbableKey.grabbable = true; + + // Apply the new properties to entity of id + Entities.editEntity(id, { + userData: JSON.stringify(userData) + }); + Script.scriptEnding.connect(function() { + Script.removeEventHandler(id, "releaseGrab", this.releaseGrab); + }); + }, + releaseGrab: function(entityId) { + // Release grab is called with entityId, + var released = Entities.getEntityProperties(entityId, ["position", "rotation", "dimensions"]); + var target = getNearestValidEntityProperties(released); + if (target !== null) { + // We found nearest, now lets do the snap calculations + // Plays the snap sound between the two objects. + Audio.playSound(SNAPSOUND_SOURCE, { + volume: 1, + position: Vec3.mix(target.position, released.position, 0.5) + }); + // Check Nearest Axis + var difference = Vec3.subtract(released.position, target.position); + var relativeDifference = Vec3.multiplyQbyV(Quat.inverse(target.rotation), difference); + + var abs = { + x: Math.abs(relativeDifference.x), + y: Math.abs(relativeDifference.y), + z: Math.abs(relativeDifference.z) + }; + // Check what value is greater. and lock down to that axis. + var newRelative = { + x: 0, + y: 0, + z: 0 + }; + if (abs.x >= abs.y && abs.x >= abs.z) { + newRelative.x = target.dimensions.x / 2 + released.dimensions.x / 2; + if (relativeDifference.x < 0) { + newRelative.x = -newRelative.x; + } + } else if (abs.y >= abs.x && abs.y >= abs.z) { + newRelative.y = target.dimensions.y / 2 + released.dimensions.y / 2; + if (relativeDifference.y < 0) { + newRelative.y = -newRelative.y; + } + } else if (abs.z >= abs.x && abs.z >= abs.y) { + newRelative.z = target.dimensions.z / 2 + released.dimensions.z / 2; + if (relativeDifference.z < 0) { + newRelative.z = -newRelative.z; + } + } + // Can be expanded upon to work in nearest 90 degree rotation as well, but was not in spec. + var newPosition = Vec3.multiplyQbyV(target.rotation, newRelative); + Entities.editEntity(entityId, { + // Script relies on the registrationPoint being at the very center of the object. Thus override. + registrationPoint: { + x: 0.5, + y: 0.5, + z: 0.5 + }, + rotation: target.rotation, + position: Vec3.sum(target.position, newPosition) + }); + // Script relies on the registrationPoint being at the very center of the object. Thus override. + Entities.editEntity(target.id, { + registrationPoint: { + x: 0.5, + y: 0.5, + z: 0.5 + } + }); + } + } + }; + return new MagneticBlock(); +}); diff --git a/scripts/tutorials/entity_scripts/pingPongGun.js b/scripts/tutorials/entity_scripts/pingPongGun.js index 4ec0254747..5ba4b15ea7 100644 --- a/scripts/tutorials/entity_scripts/pingPongGun.js +++ b/scripts/tutorials/entity_scripts/pingPongGun.js @@ -94,9 +94,9 @@ }, shootBall: function(gunProperties) { - var forwardVec = Quat.getFront(Quat.multiply(gunProperties.rotation, Quat.fromPitchYawRollDegrees(0, 180, 0))); - forwardVec = Vec3.normalize(forwardVec); - forwardVec = Vec3.multiply(forwardVec, GUN_FORCE); + var forwardVector = Quat.getForward(Quat.multiply(gunProperties.rotation, Quat.fromPitchYawRollDegrees(0, 180, 0))); + forwardVector = Vec3.normalize(forwardVector); + forwardVector = Vec3.multiply(forwardVector, GUN_FORCE); var properties = { name: 'Tutorial Ping Pong Ball', @@ -111,7 +111,7 @@ rotation: gunProperties.rotation, position: this.getGunTipPosition(gunProperties), gravity: PING_PONG_GUN_GRAVITY, - velocity: forwardVec, + velocity: forwardVector, lifetime: 10 }; @@ -131,12 +131,12 @@ getGunTipPosition: function(properties) { //the tip of the gun is going to be in a different place than the center, so we move in space relative to the model to find that position - var frontVector = Quat.getFront(properties.rotation); - var frontOffset = Vec3.multiply(frontVector, GUN_TIP_FWD_OFFSET); + var forwardVector = Quat.getForward(properties.rotation); + var forwardOffset = Vec3.multiply(forwardVector, GUN_TIP_FWD_OFFSET); var upVector = Quat.getUp(properties.rotation); var upOffset = Vec3.multiply(upVector, GUN_TIP_UP_OFFSET); - var gunTipPosition = Vec3.sum(properties.position, frontOffset); + var gunTipPosition = Vec3.sum(properties.position, forwardOffset); gunTipPosition = Vec3.sum(gunTipPosition, upOffset); return gunTipPosition; diff --git a/scripts/tutorials/entity_scripts/pistol.js b/scripts/tutorials/entity_scripts/pistol.js index 73a6daab93..1a570cc80f 100644 --- a/scripts/tutorials/entity_scripts/pistol.js +++ b/scripts/tutorials/entity_scripts/pistol.js @@ -69,7 +69,7 @@ var gunProps = Entities.getEntityProperties(this.entityID, ['position', 'rotation']); this.position = gunProps.position; this.rotation = gunProps.rotation; - this.firingDirection = Quat.getFront(this.rotation); + this.firingDirection = Quat.getForward(this.rotation); var upVec = Quat.getUp(this.rotation); this.barrelPoint = Vec3.sum(this.position, Vec3.multiply(upVec, this.laserOffsets.y)); this.laserTip = Vec3.sum(this.barrelPoint, Vec3.multiply(this.firingDirection, this.laserLength)); @@ -151,8 +151,9 @@ }); }, randFloat(10, 200)); } - if (intersection.properties.dynamic === 1) { - // Any dynaic entity can be shot + var isDynamic = Entities.getEntityProperties(intersection.entityID, "dynamic").dynamic; + if (isDynamic === 1) { + // Any dynamic entity can be shot Entities.editEntity(intersection.entityID, { velocity: Vec3.multiply(this.firingDirection, this.bulletForce) }); @@ -347,7 +348,7 @@ this.laser = Overlays.addOverlay("line3d", { start: ZERO_VECTOR, end: ZERO_VECTOR, - color: COLORS.RED, + color: { red: 255, green: 0, blue: 0}, alpha: 1, visible: true, lineWidth: 2 diff --git a/scripts/tutorials/entity_scripts/sit.js b/scripts/tutorials/entity_scripts/sit.js index 2ba19231e0..3d3bc10fb1 100644 --- a/scripts/tutorials/entity_scripts/sit.js +++ b/scripts/tutorials/entity_scripts/sit.js @@ -1,32 +1,62 @@ +// +// sit.js +// +// Created by Clement Brisset on 3/3/17 +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + (function() { Script.include("/~/system/libraries/utils.js"); + if (!String.prototype.startsWith) { + String.prototype.startsWith = function(searchString, position){ + position = position || 0; + return this.substr(position, searchString.length) === searchString; + }; + } var SETTING_KEY = "com.highfidelity.avatar.isSitting"; - var ROLE = "fly"; var ANIMATION_URL = "https://s3-us-west-1.amazonaws.com/hifi-content/clement/production/animations/sitting_idle.fbx"; var ANIMATION_FPS = 30; var ANIMATION_FIRST_FRAME = 1; var ANIMATION_LAST_FRAME = 10; - var RELEASE_KEYS = ['w', 'a', 's', 'd', 'UP', 'LEFT', 'DOWN', 'RIGHT']; var RELEASE_TIME = 500; // ms var RELEASE_DISTANCE = 0.2; // meters - var MAX_IK_ERROR = 20; - var DESKTOP_UI_CHECK_INTERVAL = 250; + var MAX_IK_ERROR = 30; + var IK_SETTLE_TIME = 250; // ms + var DESKTOP_UI_CHECK_INTERVAL = 100; var DESKTOP_MAX_DISTANCE = 5; - var SIT_DELAY = 25 + var SIT_DELAY = 25; + var MAX_RESET_DISTANCE = 0.5; // meters + var OVERRIDEN_DRIVE_KEYS = [ + DriveKeys.TRANSLATE_X, + DriveKeys.TRANSLATE_Y, + DriveKeys.TRANSLATE_Z, + DriveKeys.STEP_TRANSLATE_X, + DriveKeys.STEP_TRANSLATE_Y, + DriveKeys.STEP_TRANSLATE_Z, + ]; this.entityID = null; - this.timers = {}; this.animStateHandlerID = null; + this.interval = null; + this.sitDownSettlePeriod = null; + this.lastTimeNoDriveKeys = null; + this.sittingDown = false; + + // Preload the animation file + this.animation = AnimationCache.prefetch(ANIMATION_URL); this.preload = function(entityID) { this.entityID = entityID; } this.unload = function() { - if (MyAvatar.sessionUUID === this.getSeatUser()) { - this.sitUp(this.entityID); + if (Settings.getValue(SETTING_KEY) === this.entityID) { + this.standUp(); } - if (this.interval) { + if (this.interval !== null) { Script.clearInterval(this.interval); this.interval = null; } @@ -34,42 +64,66 @@ } this.setSeatUser = function(user) { - var userData = Entities.getEntityProperties(this.entityID, ["userData"]).userData; - userData = JSON.parse(userData); + try { + var userData = Entities.getEntityProperties(this.entityID, ["userData"]).userData; + userData = JSON.parse(userData); - if (user) { - userData.seat.user = user; - } else { - delete userData.seat.user; + if (user !== null) { + userData.seat.user = user; + } else { + delete userData.seat.user; + } + + Entities.editEntity(this.entityID, { + userData: JSON.stringify(userData) + }); + } catch (e) { + // Do Nothing } - - Entities.editEntity(this.entityID, { - userData: JSON.stringify(userData) - }); } this.getSeatUser = function() { - var properties = Entities.getEntityProperties(this.entityID, ["userData", "position"]); - var userData = JSON.parse(properties.userData); + try { + var properties = Entities.getEntityProperties(this.entityID, ["userData", "position"]); + var userData = JSON.parse(properties.userData); - if (userData.seat.user && userData.seat.user !== MyAvatar.sessionUUID) { - var avatar = AvatarList.getAvatar(userData.seat.user); - if (avatar && Vec3.distance(avatar.position, properties.position) > RELEASE_DISTANCE) { - return null; + // If MyAvatar return my uuid + if (userData.seat.user === MyAvatar.sessionUUID) { + return userData.seat.user; } + + + // If Avatar appears to be sitting + if (userData.seat.user) { + var avatar = AvatarList.getAvatar(userData.seat.user); + if (avatar && (Vec3.distance(avatar.position, properties.position) < RELEASE_DISTANCE)) { + return userData.seat.user; + } + } + } catch (e) { + // Do nothing } - return userData.seat.user; + + // Nobody on the seat + return null; } + // Is the seat used this.checkSeatForAvatar = function() { var seatUser = this.getSeatUser(); - var avatarIdentifiers = AvatarList.getAvatarIdentifiers(); - for (var i in avatarIdentifiers) { - var avatar = AvatarList.getAvatar(avatarIdentifiers[i]); - if (avatar && avatar.sessionUUID === seatUser) { - return true; - } + + // If MyAvatar appears to be sitting + if (seatUser === MyAvatar.sessionUUID) { + var properties = Entities.getEntityProperties(this.entityID, ["position"]); + return Vec3.distance(MyAvatar.position, properties.position) < RELEASE_DISTANCE; } - return false; + + return seatUser !== null; + } + + this.rolesToOverride = function() { + return MyAvatar.getAnimationRoles().filter(function(role) { + return role === "fly" || role.startsWith("inAir"); + }); } this.sitDown = function() { @@ -77,41 +131,61 @@ print("Someone is already sitting in that chair."); return; } + print("Sitting down (" + this.entityID + ")"); + this.sittingDown = true; - this.setSeatUser(MyAvatar.sessionUUID); + var now = Date.now(); + this.sitDownSettlePeriod = now + IK_SETTLE_TIME; + this.lastTimeNoDriveKeys = now; var previousValue = Settings.getValue(SETTING_KEY); Settings.setValue(SETTING_KEY, this.entityID); + this.setSeatUser(MyAvatar.sessionUUID); if (previousValue === "") { MyAvatar.characterControllerEnabled = false; MyAvatar.hmdLeanRecenterEnabled = false; - MyAvatar.overrideRoleAnimation(ROLE, ANIMATION_URL, ANIMATION_FPS, true, ANIMATION_FIRST_FRAME, ANIMATION_LAST_FRAME); + var roles = this.rolesToOverride(); + for (i in roles) { + MyAvatar.overrideRoleAnimation(roles[i], ANIMATION_URL, ANIMATION_FPS, true, ANIMATION_FIRST_FRAME, ANIMATION_LAST_FRAME); + } + + for (var i in OVERRIDEN_DRIVE_KEYS) { + MyAvatar.disableDriveKey(OVERRIDEN_DRIVE_KEYS[i]); + } + MyAvatar.resetSensorsAndBody(); } - var that = this; - Script.setTimeout(function() { - var properties = Entities.getEntityProperties(that.entityID, ["position", "rotation"]); - var index = MyAvatar.getJointIndex("Hips"); - MyAvatar.pinJoint(index, properties.position, properties.rotation); + var properties = Entities.getEntityProperties(this.entityID, ["position", "rotation"]); + var index = MyAvatar.getJointIndex("Hips"); + MyAvatar.pinJoint(index, properties.position, properties.rotation); - that.animStateHandlerID = MyAvatar.addAnimationStateHandler(function(properties) { - return { headType: 0 }; - }, ["headType"]); - Script.update.connect(that, that.update); - Controller.keyPressEvent.connect(that, that.keyPressed); - Controller.keyReleaseEvent.connect(that, that.keyReleased); - for (var i in RELEASE_KEYS) { - Controller.captureKeyEvents({ text: RELEASE_KEYS[i] }); - } - }, SIT_DELAY); + this.animStateHandlerID = MyAvatar.addAnimationStateHandler(function(properties) { + return { headType: 0 }; + }, ["headType"]); + Script.update.connect(this, this.update); } - this.sitUp = function() { - this.setSeatUser(null); + this.standUp = function() { + print("Standing up (" + this.entityID + ")"); + MyAvatar.removeAnimationStateHandler(this.animStateHandlerID); + Script.update.disconnect(this, this.update); + + if (MyAvatar.sessionUUID === this.getSeatUser()) { + this.setSeatUser(null); + } if (Settings.getValue(SETTING_KEY) === this.entityID) { - MyAvatar.restoreRoleAnimation(ROLE); + Settings.setValue(SETTING_KEY, ""); + + for (var i in OVERRIDEN_DRIVE_KEYS) { + MyAvatar.enableDriveKey(OVERRIDEN_DRIVE_KEYS[i]); + } + + var roles = this.rolesToOverride(); + for (i in roles) { + MyAvatar.restoreRoleAnimation(roles[i]); + } MyAvatar.characterControllerEnabled = true; MyAvatar.hmdLeanRecenterEnabled = true; @@ -124,19 +198,11 @@ MyAvatar.bodyPitch = 0.0; MyAvatar.bodyRoll = 0.0; }, SIT_DELAY); - - Settings.setValue(SETTING_KEY, ""); - } - - MyAvatar.removeAnimationStateHandler(this.animStateHandlerID); - Script.update.disconnect(this, this.update); - Controller.keyPressEvent.disconnect(this, this.keyPressed); - Controller.keyReleaseEvent.disconnect(this, this.keyReleased); - for (var i in RELEASE_KEYS) { - Controller.releaseKeyEvents({ text: RELEASE_KEYS[i] }); } + this.sittingDown = false; } + // function called by teleport.js if it detects the appropriate userData this.sit = function () { this.sitDown(); } @@ -183,39 +249,59 @@ } } - this.update = function(dt) { - if (MyAvatar.sessionUUID === this.getSeatUser()) { - var properties = Entities.getEntityProperties(this.entityID, ["position"]); + if (this.sittingDown === true) { + var properties = Entities.getEntityProperties(this.entityID); var avatarDistance = Vec3.distance(MyAvatar.position, properties.position); var ikError = MyAvatar.getIKErrorOnLastSolve(); - if (avatarDistance > RELEASE_DISTANCE || ikError > MAX_IK_ERROR) { + var now = Date.now(); + var shouldStandUp = false; + + // Check if a drive key is pressed + var hasActiveDriveKey = false; + for (var i in OVERRIDEN_DRIVE_KEYS) { + if (MyAvatar.getRawDriveKey(OVERRIDEN_DRIVE_KEYS[i]) != 0.0) { + hasActiveDriveKey = true; + break; + } + } + + // Only standup if user has been pushing a drive key for RELEASE_TIME + if (hasActiveDriveKey) { + var elapsed = now - this.lastTimeNoDriveKeys; + shouldStandUp = elapsed > RELEASE_TIME; + } else { + this.lastTimeNoDriveKeys = Date.now(); + } + + // Allow some time for the IK to settle + if (ikError > MAX_IK_ERROR && now > this.sitDownSettlePeriod) { + shouldStandUp = true; + } + + if (MyAvatar.sessionUUID !== this.getSeatUser()) { + shouldStandUp = true; + } + + if (shouldStandUp || avatarDistance > RELEASE_DISTANCE) { print("IK error: " + ikError + ", distance from chair: " + avatarDistance); - this.sitUp(this.entityID); + + // Move avatar in front of the chair to avoid getting stuck in collision hulls + if (avatarDistance < MAX_RESET_DISTANCE) { + var offset = { x: 0, y: 1.0, z: -0.5 - properties.dimensions.z * properties.registrationPoint.z }; + var position = Vec3.sum(properties.position, Vec3.multiplyQbyV(properties.rotation, offset)); + MyAvatar.position = position; + print("Moving Avatar in front of the chair."); + // Delay standing up by 1 cycle. + // This leaves times for the avatar to actually move since a lot + // of the stand up operations are threaded + return; + } + + this.standUp(); } } } - this.keyPressed = function(event) { - if (isInEditMode()) { - return; - } - - if (RELEASE_KEYS.indexOf(event.text) !== -1) { - var that = this; - this.timers[event.text] = Script.setTimeout(function() { - that.sitUp(); - }, RELEASE_TIME); - } - } - this.keyReleased = function(event) { - if (RELEASE_KEYS.indexOf(event.text) !== -1) { - if (this.timers[event.text]) { - Script.clearTimeout(this.timers[event.text]); - delete this.timers[event.text]; - } - } - } - this.canSitDesktop = function() { var properties = Entities.getEntityProperties(this.entityID, ["position"]); var distanceFromSeat = Vec3.distance(MyAvatar.position, properties.position); @@ -223,7 +309,7 @@ } this.hoverEnterEntity = function(event) { - if (isInEditMode() || (MyAvatar.sessionUUID === this.getSeatUser())) { + if (isInEditMode() || this.interval !== null) { return; } @@ -239,18 +325,18 @@ }, DESKTOP_UI_CHECK_INTERVAL); } this.hoverLeaveEntity = function(event) { - if (this.interval) { + if (this.interval !== null) { Script.clearInterval(this.interval); this.interval = null; } this.cleanupOverlay(); } - this.clickDownOnEntity = function () { - if (isInEditMode() || (MyAvatar.sessionUUID === this.getSeatUser())) { + this.clickDownOnEntity = function (id, event) { + if (isInEditMode()) { return; } - if (this.canSitDesktop()) { + if (event.isPrimaryButton && this.canSitDesktop()) { this.sitDown(); } } diff --git a/scripts/tutorials/makeBlocks.js b/scripts/tutorials/makeBlocks.js new file mode 100644 index 0000000000..432f7444c4 --- /dev/null +++ b/scripts/tutorials/makeBlocks.js @@ -0,0 +1,72 @@ +// +// makeBlocks.js +// +// Created by Matti Lahtinen 4/3/2017 +// Copyright 2017 High Fidelity, Inc. +// +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// +// Creates multiple "magnetic" blocks with random colors that users clones of and snap together. + + +(function() { + var MAX_RGB_COMPONENT_VALUE = 256 / 2; // Limit the values to half the maximum. + var MIN_COLOR_VALUE = 127; + var SIZE = 0.3; + var LIFETIME = 600; + var VERTICAL_OFFSET = -0.25; + var ROWS = 3; + var COLUMNS = 3; + // Random Pastel Generator based on Piper's script + function newColor() { + return { + red: randomPastelRGBComponent(), + green: randomPastelRGBComponent(), + blue: randomPastelRGBComponent() + }; + } + // Helper functions. + function randomPastelRGBComponent() { + return Math.floor(Math.random() * MAX_RGB_COMPONENT_VALUE) + MIN_COLOR_VALUE; + } + + var SCRIPT_URL = Script.resolvePath("./entity_scripts/magneticBlock.js"); + + var forwardVector = Quat.getForward(MyAvatar.orientation); + forwardVector.y += VERTICAL_OFFSET; + for (var x = 0; x < COLUMNS; x++) { + for (var y = 0; y < ROWS; y++) { + + var forwardOffset = { + x: 0, + y: SIZE * y + SIZE, + z: SIZE * x + SIZE + }; + + Entities.addEntity({ + type: "Box", + name: "MagneticBlock-" + y + '-' + x, + dimensions: { + x: SIZE, + y: SIZE, + z: SIZE + }, + userData: JSON.stringify({ + grabbableKey: { + cloneable: true, + grabbable: true, + cloneLifetime: LIFETIME, + cloneLimit: 9999 + } + }), + position: Vec3.sum(MyAvatar.position, Vec3.sum(forwardOffset, forwardVector)), + color: newColor(), + script: SCRIPT_URL + }); + } + } + + Script.stop(); +})(); diff --git a/tests/ktx/CMakeLists.txt b/tests/ktx/CMakeLists.txt new file mode 100644 index 0000000000..d72379efd6 --- /dev/null +++ b/tests/ktx/CMakeLists.txt @@ -0,0 +1,15 @@ + +set(TARGET_NAME ktx-test) + +if (WIN32) + SET(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} /ignore:4049 /ignore:4217") +endif() + +# This is not a testcase -- just set it up as a regular hifi project +setup_hifi_project(Quick Gui OpenGL) +set_target_properties(${TARGET_NAME} PROPERTIES FOLDER "Tests/manual-tests/") + +# link in the shared libraries +link_hifi_libraries(shared octree ktx gl gpu gpu-gl render model model-networking networking render-utils fbx entities entities-renderer animation audio avatars script-engine physics) + +package_libraries_for_deployment() diff --git a/tests/ktx/src/main.cpp b/tests/ktx/src/main.cpp new file mode 100644 index 0000000000..3a6fcabf43 --- /dev/null +++ b/tests/ktx/src/main.cpp @@ -0,0 +1,152 @@ +// +// Created by Bradley Austin Davis on 2016/07/01 +// Copyright 2014 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include +#include +#include +#include + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + + +#include +#include +#include +#include + + +QSharedPointer logger; + +gpu::Texture* cacheTexture(const std::string& name, gpu::Texture* srcTexture, bool write = true, bool read = true); + + +void messageHandler(QtMsgType type, const QMessageLogContext& context, const QString& message) { + QString logMessage = LogHandler::getInstance().printMessage((LogMsgType)type, context, message); + + if (!logMessage.isEmpty()) { +#ifdef Q_OS_WIN + OutputDebugStringA(logMessage.toLocal8Bit().constData()); + OutputDebugStringA("\n"); +#endif + logger->addMessage(qPrintable(logMessage + "\n")); + } +} + +const char * LOG_FILTER_RULES = R"V0G0N( +hifi.gpu=true +)V0G0N"; + +QString getRootPath() { + static std::once_flag once; + static QString result; + std::call_once(once, [&] { + QFileInfo file(__FILE__); + QDir parent = file.absolutePath(); + result = QDir::cleanPath(parent.currentPath() + "/../../.."); + }); + return result; +} + +const QString TEST_IMAGE = getRootPath() + "/scripts/developer/tests/cube_texture.png"; +const QString TEST_IMAGE_KTX = getRootPath() + "/scripts/developer/tests/cube_texture.ktx"; + +int main(int argc, char** argv) { + QApplication app(argc, argv); + QCoreApplication::setApplicationName("KTX"); + QCoreApplication::setOrganizationName("High Fidelity"); + QCoreApplication::setOrganizationDomain("highfidelity.com"); + logger.reset(new FileLogger()); + + Q_ASSERT(sizeof(ktx::Header) == 12 + (sizeof(uint32_t) * 13)); + + DependencyManager::set(); + qInstallMessageHandler(messageHandler); + QLoggingCategory::setFilterRules(LOG_FILTER_RULES); + + QImage image(TEST_IMAGE); + gpu::Texture* testTexture = model::TextureUsage::process2DTextureColorFromImage(image, TEST_IMAGE.toStdString(), true, false, true); + + auto ktxMemory = gpu::Texture::serialize(*testTexture); + { + const auto& ktxStorage = ktxMemory->getStorage(); + QFile outFile(TEST_IMAGE_KTX); + if (!outFile.open(QFile::Truncate | QFile::ReadWrite)) { + throw std::runtime_error("Unable to open file"); + } + auto ktxSize = ktxStorage->size(); + outFile.resize(ktxSize); + auto dest = outFile.map(0, ktxSize); + memcpy(dest, ktxStorage->data(), ktxSize); + outFile.unmap(dest); + outFile.close(); + } + + { + auto ktxFile = ktx::KTX::create(std::shared_ptr(new storage::FileStorage(TEST_IMAGE_KTX))); + { + const auto& memStorage = ktxMemory->getStorage(); + const auto& fileStorage = ktxFile->getStorage(); + Q_ASSERT(memStorage->size() == fileStorage->size()); + Q_ASSERT(memStorage->data() != fileStorage->data()); + Q_ASSERT(0 == memcmp(memStorage->data(), fileStorage->data(), memStorage->size())); + Q_ASSERT(ktxFile->_images.size() == ktxMemory->_images.size()); + auto imageCount = ktxFile->_images.size(); + auto startMemory = ktxMemory->_storage->data(); + auto startFile = ktxFile->_storage->data(); + for (size_t i = 0; i < imageCount; ++i) { + auto memImages = ktxMemory->_images[i]; + auto fileImages = ktxFile->_images[i]; + Q_ASSERT(memImages._padding == fileImages._padding); + Q_ASSERT(memImages._numFaces == fileImages._numFaces); + Q_ASSERT(memImages._imageSize == fileImages._imageSize); + Q_ASSERT(memImages._faceSize == fileImages._faceSize); + Q_ASSERT(memImages._faceBytes.size() == memImages._numFaces); + Q_ASSERT(fileImages._faceBytes.size() == fileImages._numFaces); + auto faceCount = fileImages._numFaces; + for (uint32_t face = 0; face < faceCount; ++face) { + auto memFace = memImages._faceBytes[face]; + auto memOffset = memFace - startMemory; + auto fileFace = fileImages._faceBytes[face]; + auto fileOffset = fileFace - startFile; + Q_ASSERT(memOffset % 4 == 0); + Q_ASSERT(memOffset == fileOffset); + } + } + } + } + testTexture->setKtxBacking(TEST_IMAGE_KTX.toStdString()); + return 0; +} + +#include "main.moc" + diff --git a/tests/render-perf/CMakeLists.txt b/tests/render-perf/CMakeLists.txt index d4f90fdace..96cede9c43 100644 --- a/tests/render-perf/CMakeLists.txt +++ b/tests/render-perf/CMakeLists.txt @@ -10,7 +10,7 @@ setup_hifi_project(Quick Gui OpenGL) set_target_properties(${TARGET_NAME} PROPERTIES FOLDER "Tests/manual-tests/") # link in the shared libraries -link_hifi_libraries(shared octree gl gpu gpu-gl render model model-networking networking render-utils fbx entities entities-renderer animation audio avatars script-engine physics) +link_hifi_libraries(shared octree ktx gl gpu gpu-gl render model model-networking networking render-utils fbx entities entities-renderer animation audio avatars script-engine physics) package_libraries_for_deployment() diff --git a/tests/render-perf/src/Camera.hpp b/tests/render-perf/src/Camera.hpp index a3b33ceb14..ada1277c47 100644 --- a/tests/render-perf/src/Camera.hpp +++ b/tests/render-perf/src/Camera.hpp @@ -123,16 +123,16 @@ public: void update(float deltaTime) { if (moving()) { - glm::vec3 camFront = getOrientation() * Vectors::FRONT; + glm::vec3 camForward = getOrientation() * Vectors::FRONT; glm::vec3 camRight = getOrientation() * Vectors::RIGHT; glm::vec3 camUp = getOrientation() * Vectors::UP; float moveSpeed = deltaTime * movementSpeed; if (keys[FORWARD]) { - position += camFront * moveSpeed; + position += camForward * moveSpeed; } if (keys[BACK]) { - position -= camFront * moveSpeed; + position -= camForward * moveSpeed; } if (keys[LEFT]) { position -= camRight * moveSpeed; diff --git a/tests/render-perf/src/main.cpp b/tests/render-perf/src/main.cpp index 7e9d2c426f..b76776b1ab 100644 --- a/tests/render-perf/src/main.cpp +++ b/tests/render-perf/src/main.cpp @@ -642,7 +642,6 @@ protected: gpu::Texture::setAllowedGPUMemoryUsage(MB_TO_BYTES(64)); return; - default: break; } @@ -881,7 +880,7 @@ private: getEntities()->update(); // The pending changes collecting the changes here - render::PendingChanges pendingChanges; + render::Transaction transaction; // FIXME: Move this out of here!, Background / skybox should be driven by the enityt content just like the other entities // Background rendering decision @@ -889,7 +888,7 @@ private: auto backgroundRenderData = std::make_shared(); auto backgroundRenderPayload = std::make_shared(backgroundRenderData); BackgroundRenderData::_item = _main3DScene->allocateID(); - pendingChanges.resetItem(BackgroundRenderData::_item, backgroundRenderPayload); + transaction.resetItem(BackgroundRenderData::_item, backgroundRenderPayload); } // Setup the current Zone Entity lighting { @@ -898,10 +897,10 @@ private: } { - PerformanceTimer perfTimer("SceneProcessPendingChanges"); - _main3DScene->enqueuePendingChanges(pendingChanges); + PerformanceTimer perfTimer("SceneProcessTransaction"); + _main3DScene->enqueueTransaction(transaction); - _main3DScene->processPendingChangesQueue(); + _main3DScene->processTransactionQueue(); } } @@ -915,13 +914,13 @@ private: PROFILE_RANGE(render, __FUNCTION__); PerformanceTimer perfTimer("draw"); // The pending changes collecting the changes here - render::PendingChanges pendingChanges; + render::Transaction transaction; // Setup the current Zone Entity lighting DependencyManager::get()->setGlobalLight(_sunSkyStage.getSunLight()); { - PerformanceTimer perfTimer("SceneProcessPendingChanges"); - _main3DScene->enqueuePendingChanges(pendingChanges); - _main3DScene->processPendingChangesQueue(); + PerformanceTimer perfTimer("SceneProcessTransaction"); + _main3DScene->enqueueTransaction(transaction); + _main3DScene->processTransactionQueue(); } // For now every frame pass the renderContext diff --git a/tests/render-texture-load/src/main.cpp b/tests/render-texture-load/src/main.cpp index 09a420f018..f426cd8024 100644 --- a/tests/render-texture-load/src/main.cpp +++ b/tests/render-texture-load/src/main.cpp @@ -48,6 +48,7 @@ #include #include +#include #include #include #include @@ -455,7 +456,7 @@ protected: return; } auto texture = _textures[_currentTextureIndex]; - texture->setMinMip(texture->minMip() + 1); + texture->setMinMip(texture->getMinMip() + 1); } void loadTexture() { diff --git a/tests/render-utils/CMakeLists.txt b/tests/render-utils/CMakeLists.txt index e7e80f7726..5ec6a28b5c 100644 --- a/tests/render-utils/CMakeLists.txt +++ b/tests/render-utils/CMakeLists.txt @@ -7,5 +7,6 @@ set_target_properties(${TARGET_NAME} PROPERTIES FOLDER "Tests/manual-tests/") # link in the shared libraries link_hifi_libraries(render-utils gl gpu gpu-gl shared) +target_link_libraries(${TARGET_NAME} ${CMAKE_THREAD_LIBS_INIT}) package_libraries_for_deployment() diff --git a/tests/shared/src/GLMHelpersTests.cpp b/tests/shared/src/GLMHelpersTests.cpp index 8d26d35c69..b4af4729a3 100644 --- a/tests/shared/src/GLMHelpersTests.cpp +++ b/tests/shared/src/GLMHelpersTests.cpp @@ -115,8 +115,8 @@ void GLMHelpersTests::testSimd() { a1 = a * b; b1 = b * a; - glm_mat4_mul((glm_vec4*)&a, (glm_vec4*)&b, (glm_vec4*)&a2); - glm_mat4_mul((glm_vec4*)&b, (glm_vec4*)&a, (glm_vec4*)&b2); + glm_mat4u_mul(a, b, a2); + glm_mat4u_mul(b, a, b2); { @@ -133,8 +133,8 @@ void GLMHelpersTests::testSimd() { QElapsedTimer timer; timer.start(); for (size_t i = 0; i < LOOPS; ++i) { - glm_mat4_mul((glm_vec4*)&a, (glm_vec4*)&b, (glm_vec4*)&a2); - glm_mat4_mul((glm_vec4*)&b, (glm_vec4*)&a, (glm_vec4*)&b2); + glm_mat4u_mul(a, b, a2); + glm_mat4u_mul(b, a, b2); } qDebug() << "SIMD " << timer.elapsed(); } diff --git a/tests/shared/src/StorageTests.cpp b/tests/shared/src/StorageTests.cpp new file mode 100644 index 0000000000..fa538f6911 --- /dev/null +++ b/tests/shared/src/StorageTests.cpp @@ -0,0 +1,75 @@ +// +// Created by Bradley Austin Davis on 2016/02/17 +// Copyright 2013-2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include "StorageTests.h" + +QTEST_MAIN(StorageTests) + +using namespace storage; + +StorageTests::StorageTests() { + for (size_t i = 0; i < _testData.size(); ++i) { + _testData[i] = (uint8_t)rand(); + } + _testFile = QDir::tempPath() + "/" + QUuid::createUuid().toString(); +} + +StorageTests::~StorageTests() { + QFileInfo fileInfo(_testFile); + if (fileInfo.exists()) { + QFile(_testFile).remove(); + } +} + + +void StorageTests::testConversion() { + { + QFileInfo fileInfo(_testFile); + QCOMPARE(fileInfo.exists(), false); + } + StoragePointer storagePointer = std::make_unique(_testData.size(), _testData.data()); + QCOMPARE(storagePointer->size(), (quint64)_testData.size()); + QCOMPARE(memcmp(_testData.data(), storagePointer->data(), _testData.size()), 0); + // Convert to a file + storagePointer = storagePointer->toFileStorage(_testFile); + { + QFileInfo fileInfo(_testFile); + QCOMPARE(fileInfo.exists(), true); + QCOMPARE(fileInfo.size(), (qint64)_testData.size()); + } + QCOMPARE(storagePointer->size(), (quint64)_testData.size()); + QCOMPARE(memcmp(_testData.data(), storagePointer->data(), _testData.size()), 0); + + // Convert to memory + storagePointer = storagePointer->toMemoryStorage(); + QCOMPARE(storagePointer->size(), (quint64)_testData.size()); + QCOMPARE(memcmp(_testData.data(), storagePointer->data(), _testData.size()), 0); + { + // ensure the file is unaffected + QFileInfo fileInfo(_testFile); + QCOMPARE(fileInfo.exists(), true); + QCOMPARE(fileInfo.size(), (qint64)_testData.size()); + } + + // truncate the data as a new memory object + auto newSize = _testData.size() / 2; + storagePointer = std::make_unique(newSize, storagePointer->data()); + QCOMPARE(storagePointer->size(), (quint64)newSize); + QCOMPARE(memcmp(_testData.data(), storagePointer->data(), newSize), 0); + + // Convert back to file + storagePointer = storagePointer->toFileStorage(_testFile); + QCOMPARE(storagePointer->size(), (quint64)newSize); + QCOMPARE(memcmp(_testData.data(), storagePointer->data(), newSize), 0); + { + // ensure the file is truncated + QFileInfo fileInfo(_testFile); + QCOMPARE(fileInfo.exists(), true); + QCOMPARE(fileInfo.size(), (qint64)newSize); + } +} diff --git a/tests/shared/src/StorageTests.h b/tests/shared/src/StorageTests.h new file mode 100644 index 0000000000..6a2c153223 --- /dev/null +++ b/tests/shared/src/StorageTests.h @@ -0,0 +1,32 @@ +// +// Created by Bradley Austin Davis on 2016/02/17 +// Copyright 2013-2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_StorageTests_h +#define hifi_StorageTests_h + +#include + +#include +#include + +class StorageTests : public QObject { + Q_OBJECT + +public: + StorageTests(); + ~StorageTests(); + +private slots: + void testConversion(); + +private: + std::array _testData; + QString _testFile; +}; + +#endif // hifi_StorageTests_h diff --git a/tools/CMakeLists.txt b/tools/CMakeLists.txt index a85a112bf5..8dc993e6fe 100644 --- a/tools/CMakeLists.txt +++ b/tools/CMakeLists.txt @@ -17,3 +17,5 @@ set_target_properties(ac-client PROPERTIES FOLDER "Tools") add_subdirectory(skeleton-dump) set_target_properties(skeleton-dump PROPERTIES FOLDER "Tools") +add_subdirectory(atp-get) +set_target_properties(atp-get PROPERTIES FOLDER "Tools") diff --git a/tools/atp-get/CMakeLists.txt b/tools/atp-get/CMakeLists.txt new file mode 100644 index 0000000000..b1646dc023 --- /dev/null +++ b/tools/atp-get/CMakeLists.txt @@ -0,0 +1,3 @@ +set(TARGET_NAME atp-get) +setup_hifi_project(Core Widgets) +link_hifi_libraries(shared networking) diff --git a/tools/atp-get/src/ATPGetApp.cpp b/tools/atp-get/src/ATPGetApp.cpp new file mode 100644 index 0000000000..30054fffea --- /dev/null +++ b/tools/atp-get/src/ATPGetApp.cpp @@ -0,0 +1,269 @@ +// +// ATPGetApp.cpp +// tools/atp-get/src +// +// Created by Seth Alves on 2017-3-15 +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "ATPGetApp.h" + +ATPGetApp::ATPGetApp(int argc, char* argv[]) : + QCoreApplication(argc, argv) +{ + // parse command-line + QCommandLineParser parser; + parser.setApplicationDescription("High Fidelity ATP-Get"); + + const QCommandLineOption helpOption = parser.addHelpOption(); + + const QCommandLineOption verboseOutput("v", "verbose output"); + parser.addOption(verboseOutput); + + const QCommandLineOption domainAddressOption("d", "domain-server address", "127.0.0.1"); + parser.addOption(domainAddressOption); + + const QCommandLineOption cacheSTUNOption("s", "cache stun-server response"); + parser.addOption(cacheSTUNOption); + + const QCommandLineOption listenPortOption("listenPort", "listen port", QString::number(INVALID_PORT)); + parser.addOption(listenPortOption); + + + if (!parser.parse(QCoreApplication::arguments())) { + qCritical() << parser.errorText() << endl; + parser.showHelp(); + Q_UNREACHABLE(); + } + + if (parser.isSet(helpOption)) { + parser.showHelp(); + Q_UNREACHABLE(); + } + + _verbose = parser.isSet(verboseOutput); + if (!_verbose) { + QLoggingCategory::setFilterRules("qt.network.ssl.warning=false"); + + const_cast(&networking())->setEnabled(QtDebugMsg, false); + const_cast(&networking())->setEnabled(QtInfoMsg, false); + const_cast(&networking())->setEnabled(QtWarningMsg, false); + + const_cast(&shared())->setEnabled(QtDebugMsg, false); + const_cast(&shared())->setEnabled(QtInfoMsg, false); + const_cast(&shared())->setEnabled(QtWarningMsg, false); + } + + + QStringList filenames = parser.positionalArguments(); + if (filenames.empty() || filenames.size() > 2) { + qDebug() << "give remote url and optional local filename as arguments"; + parser.showHelp(); + Q_UNREACHABLE(); + } + + _url = QUrl(filenames[0]); + if (_url.scheme() != "atp") { + qDebug() << "url should start with atp:"; + parser.showHelp(); + Q_UNREACHABLE(); + } + + if (filenames.size() == 2) { + _localOutputFile = filenames[1]; + } + + QString domainServerAddress = "127.0.0.1:40103"; + if (parser.isSet(domainAddressOption)) { + domainServerAddress = parser.value(domainAddressOption); + } + + if (_verbose) { + qDebug() << "domain-server address is" << domainServerAddress; + } + + int listenPort = INVALID_PORT; + if (parser.isSet(listenPortOption)) { + listenPort = parser.value(listenPortOption).toInt(); + } + + Setting::init(); + DependencyManager::registerInheritance(); + + DependencyManager::set([&]{ return QString("Mozilla/5.0 (HighFidelityATPGet)"); }); + DependencyManager::set(); + DependencyManager::set(NodeType::Agent, listenPort); + + + auto nodeList = DependencyManager::get(); + + // start the nodeThread so its event loop is running + QThread* nodeThread = new QThread(this); + nodeThread->setObjectName("NodeList Thread"); + nodeThread->start(); + + // make sure the node thread is given highest priority + nodeThread->setPriority(QThread::TimeCriticalPriority); + + // setup a timer for domain-server check ins + QTimer* domainCheckInTimer = new QTimer(nodeList.data()); + connect(domainCheckInTimer, &QTimer::timeout, nodeList.data(), &NodeList::sendDomainServerCheckIn); + domainCheckInTimer->start(DOMAIN_SERVER_CHECK_IN_MSECS); + + // put the NodeList and datagram processing on the node thread + nodeList->moveToThread(nodeThread); + + const DomainHandler& domainHandler = nodeList->getDomainHandler(); + + connect(&domainHandler, SIGNAL(hostnameChanged(const QString&)), SLOT(domainChanged(const QString&))); + // connect(&domainHandler, SIGNAL(resetting()), SLOT(resettingDomain())); + // connect(&domainHandler, SIGNAL(disconnectedFromDomain()), SLOT(clearDomainOctreeDetails())); + connect(&domainHandler, &DomainHandler::domainConnectionRefused, this, &ATPGetApp::domainConnectionRefused); + + connect(nodeList.data(), &NodeList::nodeAdded, this, &ATPGetApp::nodeAdded); + connect(nodeList.data(), &NodeList::nodeKilled, this, &ATPGetApp::nodeKilled); + connect(nodeList.data(), &NodeList::nodeActivated, this, &ATPGetApp::nodeActivated); + // connect(nodeList.data(), &NodeList::uuidChanged, getMyAvatar(), &MyAvatar::setSessionUUID); + // connect(nodeList.data(), &NodeList::uuidChanged, this, &ATPGetApp::setSessionUUID); + connect(nodeList.data(), &NodeList::packetVersionMismatch, this, &ATPGetApp::notifyPacketVersionMismatch); + + nodeList->addSetOfNodeTypesToNodeInterestSet(NodeSet() << NodeType::AudioMixer << NodeType::AvatarMixer + << NodeType::EntityServer << NodeType::AssetServer << NodeType::MessagesMixer); + + DependencyManager::get()->handleLookupString(domainServerAddress, false); + + auto assetClient = DependencyManager::set(); + assetClient->init(); + + QTimer* doTimer = new QTimer(this); + doTimer->setSingleShot(true); + connect(doTimer, &QTimer::timeout, this, &ATPGetApp::timedOut); + doTimer->start(4000); +} + +ATPGetApp::~ATPGetApp() { +} + + +void ATPGetApp::domainConnectionRefused(const QString& reasonMessage, int reasonCodeInt, const QString& extraInfo) { + qDebug() << "domainConnectionRefused"; +} + +void ATPGetApp::domainChanged(const QString& domainHostname) { + if (_verbose) { + qDebug() << "domainChanged"; + } +} + +void ATPGetApp::nodeAdded(SharedNodePointer node) { + if (_verbose) { + qDebug() << "node added: " << node->getType(); + } +} + +void ATPGetApp::nodeActivated(SharedNodePointer node) { + if (node->getType() == NodeType::AssetServer) { + lookup(); + } +} + +void ATPGetApp::nodeKilled(SharedNodePointer node) { + qDebug() << "nodeKilled"; +} + +void ATPGetApp::timedOut() { + finish(1); +} + +void ATPGetApp::notifyPacketVersionMismatch() { + if (_verbose) { + qDebug() << "packet version mismatch"; + } + finish(1); +} + +void ATPGetApp::lookup() { + + auto path = _url.path(); + qDebug() << "path is " << path; + + auto request = DependencyManager::get()->createGetMappingRequest(path); + QObject::connect(request, &GetMappingRequest::finished, this, [=](GetMappingRequest* request) mutable { + auto result = request->getError(); + if (result == GetMappingRequest::NotFound) { + qDebug() << "not found"; + } else if (result == GetMappingRequest::NoError) { + qDebug() << "found, hash is " << request->getHash(); + download(request->getHash()); + } else { + qDebug() << "error -- " << request->getError() << " -- " << request->getErrorString(); + } + request->deleteLater(); + }); + request->start(); +} + +void ATPGetApp::download(AssetHash hash) { + auto assetClient = DependencyManager::get(); + auto assetRequest = new AssetRequest(hash); + + connect(assetRequest, &AssetRequest::finished, this, [this](AssetRequest* request) mutable { + Q_ASSERT(request->getState() == AssetRequest::Finished); + + if (request->getError() == AssetRequest::Error::NoError) { + QString data = QString::fromUtf8(request->getData()); + if (_localOutputFile == "") { + QTextStream cout(stdout); + cout << data; + } else { + QFile outputHandle(_localOutputFile); + if (outputHandle.open(QIODevice::ReadWrite)) { + QTextStream stream( &outputHandle ); + stream << data; + } else { + qDebug() << "couldn't open output file:" << _localOutputFile; + } + } + } + + request->deleteLater(); + finish(0); + }); + + assetRequest->start(); +} + +void ATPGetApp::finish(int exitCode) { + auto nodeList = DependencyManager::get(); + + // send the domain a disconnect packet, force stoppage of domain-server check-ins + nodeList->getDomainHandler().disconnect(); + nodeList->setIsShuttingDown(true); + + // tell the packet receiver we're shutting down, so it can drop packets + nodeList->getPacketReceiver().setShouldDropPackets(true); + + QThread* nodeThread = DependencyManager::get()->thread(); + // remove the NodeList from the DependencyManager + DependencyManager::destroy(); + // ask the node thread to quit and wait until it is done + nodeThread->quit(); + nodeThread->wait(); + + QCoreApplication::exit(exitCode); +} diff --git a/tools/atp-get/src/ATPGetApp.h b/tools/atp-get/src/ATPGetApp.h new file mode 100644 index 0000000000..5507d2aa62 --- /dev/null +++ b/tools/atp-get/src/ATPGetApp.h @@ -0,0 +1,52 @@ +// +// ATPGetApp.h +// tools/atp-get/src +// +// Created by Seth Alves on 2017-3-15 +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + + +#ifndef hifi_ATPGetApp_h +#define hifi_ATPGetApp_h + +#include +#include +#include +#include +#include +#include +#include +#include + + +class ATPGetApp : public QCoreApplication { + Q_OBJECT +public: + ATPGetApp(int argc, char* argv[]); + ~ATPGetApp(); + +private slots: + void domainConnectionRefused(const QString& reasonMessage, int reasonCodeInt, const QString& extraInfo); + void domainChanged(const QString& domainHostname); + void nodeAdded(SharedNodePointer node); + void nodeActivated(SharedNodePointer node); + void nodeKilled(SharedNodePointer node); + void notifyPacketVersionMismatch(); + +private: + NodeList* _nodeList; + void timedOut(); + void lookup(); + void download(AssetHash hash); + void finish(int exitCode); + bool _verbose; + + QUrl _url; + QString _localOutputFile; +}; + +#endif // hifi_ATPGetApp_h diff --git a/tools/atp-get/src/main.cpp b/tools/atp-get/src/main.cpp new file mode 100644 index 0000000000..bddf30c666 --- /dev/null +++ b/tools/atp-get/src/main.cpp @@ -0,0 +1,31 @@ +// +// main.cpp +// tools/atp-get/src +// +// Created by Seth Alves on 2017-3-15 +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html + +#include +#include +#include +#include + +#include + +#include "ATPGetApp.h" + +using namespace std; + +int main(int argc, char * argv[]) { + QCoreApplication::setApplicationName(BuildInfo::AC_CLIENT_SERVER_NAME); + QCoreApplication::setOrganizationName(BuildInfo::MODIFIED_ORGANIZATION); + QCoreApplication::setOrganizationDomain(BuildInfo::ORGANIZATION_DOMAIN); + QCoreApplication::setApplicationVersion(BuildInfo::VERSION); + + ATPGetApp app(argc, argv); + + return app.exec(); +} diff --git a/tools/jsdoc/plugins/hifi.js b/tools/jsdoc/plugins/hifi.js index 8a6d2bf0f2..c15f01efe9 100644 --- a/tools/jsdoc/plugins/hifi.js +++ b/tools/jsdoc/plugins/hifi.js @@ -15,12 +15,16 @@ exports.handlers = { // directories to scan for jsdoc comments var dirList = [ '../../interface/src', + '../../interface/src/avatar', '../../interface/src/scripting', '../../interface/src/ui/overlays', - '../../libraries/script-engine/src', - '../../libraries/networking/src', '../../libraries/animation/src', + '../../libraries/avatars/src', + '../../libraries/controllers/src/controllers/', '../../libraries/entities/src', + '../../libraries/networking/src', + '../../libraries/shared/src', + '../../libraries/script-engine/src', ]; var exts = ['.h', '.cpp']; diff --git a/tutorial/lighter/butaneLighter.js b/tutorial/lighter/butaneLighter.js index 574c2be6c6..f37769f867 100644 --- a/tutorial/lighter/butaneLighter.js +++ b/tutorial/lighter/butaneLighter.js @@ -154,9 +154,12 @@ direction: Quat.inverse(Quat.getFront(flameProperties.rotation)) } var intersection = Entities.findRayIntersection(pickRay, true, [], [_this.entityID, _this.lighterParticleEntity]); - if (intersection.intersects && intersection.distance <= FLAME_LENGTH && intersection.properties.script !== '') { - Entities.callEntityMethod(intersection.properties.id, 'onLit', [_this.triggerValue]); - debugPrint('Light it up! found: ' + intersection.properties.id); + if (intersection.intersects && intersection.distance <= FLAME_LENGTH) { + var properties = Entities.getEntityProperties(intersection.entityID, 'script'); + if (properties.script !== '') { + Entities.callEntityMethod(intersection.entityID, 'onLit', [_this.triggerValue]); + debugPrint('Light it up! found: ' + intersection.entityID); + } } }, releaseEquip: function(entityID, args) { diff --git a/unpublishedScripts/marketplace/boppo/boppoClownEntity.js b/unpublishedScripts/marketplace/boppo/boppoClownEntity.js new file mode 100644 index 0000000000..36f2bf5ab0 --- /dev/null +++ b/unpublishedScripts/marketplace/boppo/boppoClownEntity.js @@ -0,0 +1,80 @@ +// +// boppoClownEntity.js +// +// Created by Thijs Wenker on 3/15/17. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +/* globals LookAtTarget */ + +(function() { + var SFX_PREFIX = 'https://hifi-content.s3-us-west-1.amazonaws.com/caitlyn/production/elBoppo/sfx/'; + var CHANNEL_PREFIX = 'io.highfidelity.boppo_server_'; + var PUNCH_SOUNDS = [ + 'punch_1.wav', + 'punch_2.wav' + ]; + var PUNCH_COOLDOWN = 300; + + Script.include('lookAtEntity.js'); + + var createBoppoClownEntity = function() { + var _this, + _entityID, + _boppoUserData, + _lookAtTarget, + _punchSounds = [], + _lastPlayedPunch = {}; + + var getOwnBoppoUserData = function() { + try { + return JSON.parse(Entities.getEntityProperties(_entityID, ['userData']).userData).Boppo; + } catch (e) { + // e + } + return {}; + }; + + var BoppoClownEntity = function () { + _this = this; + PUNCH_SOUNDS.forEach(function(punch) { + _punchSounds.push(SoundCache.getSound(SFX_PREFIX + punch)); + }); + }; + + BoppoClownEntity.prototype = { + preload: function(entityID) { + _entityID = entityID; + _boppoUserData = getOwnBoppoUserData(); + _lookAtTarget = new LookAtTarget(_entityID); + }, + collisionWithEntity: function(boppoEntity, collidingEntity, collisionInfo) { + if (collisionInfo.type === 0 && + Entities.getEntityProperties(collidingEntity, ['name']).name.indexOf('Boxing Glove ') === 0) { + + if (_lastPlayedPunch[collidingEntity] === undefined || + Date.now() - _lastPlayedPunch[collidingEntity] > PUNCH_COOLDOWN) { + + // If boxing glove detected here: + Messages.sendMessage(CHANNEL_PREFIX + _boppoUserData.gameParentID, 'hit'); + + _lookAtTarget.lookAtByAction(); + var randomPunchIndex = Math.floor(Math.random() * _punchSounds.length); + Audio.playSound(_punchSounds[randomPunchIndex], { + position: collisionInfo.contactPoint + }); + _lastPlayedPunch[collidingEntity] = Date.now(); + } + } + } + + }; + + return new BoppoClownEntity(); + }; + + return createBoppoClownEntity(); +}); diff --git a/unpublishedScripts/marketplace/boppo/boppoServer.js b/unpublishedScripts/marketplace/boppo/boppoServer.js new file mode 100644 index 0000000000..f03154573c --- /dev/null +++ b/unpublishedScripts/marketplace/boppo/boppoServer.js @@ -0,0 +1,303 @@ +// +// boppoServer.js +// +// Created by Thijs Wenker on 3/15/17. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +(function() { + var SFX_PREFIX = 'https://hifi-content.s3-us-west-1.amazonaws.com/caitlyn/production/elBoppo/sfx/'; + var CLOWN_LAUGHS = [ + 'clown_laugh_1.wav', + 'clown_laugh_2.wav', + 'clown_laugh_3.wav', + 'clown_laugh_4.wav' + ]; + var TICK_TOCK_SOUND = 'ticktock%20-%20tock.wav'; + var BOXING_RING_BELL_START = 'boxingRingBell.wav'; + var BOXING_RING_BELL_END = 'boxingRingBell-end.wav'; + var BOPPO_MUSIC = 'boppoMusic.wav'; + var CHANNEL_PREFIX = 'io.highfidelity.boppo_server_'; + var MESSAGE_HIT = 'hit'; + var MESSAGE_ENTER_ZONE = 'enter-zone'; + var MESSAGE_UNLOAD_FIX = 'unload-fix'; + + var DEFAULT_SOUND_VOLUME = 0.6; + + // don't set the search radius too high, it might remove boppo's from other nearby instances + var BOPPO_SEARCH_RADIUS = 4.0; + + var MILLISECONDS_PER_SECOND = 1000; + // Make sure the entities are loaded at startup (TODO: more solid fix) + var LOAD_TIMEOUT = 5000; + var SECONDS_PER_MINUTE = 60; + var DEFAULT_PLAYTIME = 30; // seconds + var BASE_TEN = 10; + var TICK_TOCK_FROM = 3; // seconds + var COOLDOWN_TIME_MS = MILLISECONDS_PER_SECOND * 3; + + var createBoppoServer = function() { + var _this, + _isInitialized = false, + _clownLaughs = [], + _musicInjector, + _music, + _laughingInjector, + _tickTockSound, + _boxingBellRingStart, + _boxingBellRingEnd, + _entityID, + _boppoClownID, + _channel, + _boppoEntities, + _isGameRunning, + _updateInterval, + _timeLeft, + _hits, + _coolDown; + + var getOwnBoppoUserData = function() { + try { + return JSON.parse(Entities.getEntityProperties(_entityID, ['userData']).userData).Boppo; + } catch (e) { + // e + } + return {}; + }; + + var updateBoppoEntities = function() { + Entities.getChildrenIDs(_entityID).forEach(function(entityID) { + try { + var userData = JSON.parse(Entities.getEntityProperties(entityID, ['userData']).userData); + if (userData.Boppo.type !== undefined) { + _boppoEntities[userData.Boppo.type] = entityID; + } + } catch (e) { + // e + } + }); + }; + + var clearUntrackedBoppos = function() { + var position = Entities.getEntityProperties(_entityID, ['position']).position; + Entities.findEntities(position, BOPPO_SEARCH_RADIUS).forEach(function(entityID) { + try { + if (JSON.parse(Entities.getEntityProperties(entityID, ['userData']).userData).Boppo.type === 'boppo') { + Entities.deleteEntity(entityID); + } + } catch (e) { + // e + } + }); + }; + + var updateTimerDisplay = function() { + if (_boppoEntities['timer']) { + var secondsString = _timeLeft % SECONDS_PER_MINUTE; + if (secondsString < BASE_TEN) { + secondsString = '0' + secondsString; + } + var minutesString = Math.floor(_timeLeft / SECONDS_PER_MINUTE); + Entities.editEntity(_boppoEntities['timer'], { + text: minutesString + ':' + secondsString + }); + } + }; + + var updateScoreDisplay = function() { + if (_boppoEntities['score']) { + Entities.editEntity(_boppoEntities['score'], { + text: 'SCORE: ' + _hits + }); + } + }; + + var playSoundAtBoxingRing = function(sound, properties) { + var _properties = properties ? properties : {}; + if (_properties['volume'] === undefined) { + _properties['volume'] = DEFAULT_SOUND_VOLUME; + } + _properties['position'] = Entities.getEntityProperties(_entityID, ['position']).position; + // play beep + return Audio.playSound(sound, _properties); + }; + + var onUpdate = function() { + _timeLeft--; + + if (_timeLeft > 0 && _timeLeft <= TICK_TOCK_FROM) { + // play beep + playSoundAtBoxingRing(_tickTockSound); + } + if (_timeLeft === 0) { + if (_musicInjector !== undefined && _musicInjector.isPlaying()) { + _musicInjector.stop(); + _musicInjector = undefined; + } + playSoundAtBoxingRing(_boxingBellRingEnd); + _isGameRunning = false; + Script.clearInterval(_updateInterval); + _updateInterval = null; + _coolDown = true; + Script.setTimeout(function() { + _coolDown = false; + _this.resetBoppo(); + }, COOLDOWN_TIME_MS); + } + updateTimerDisplay(); + }; + + var onMessage = function(channel, message, sender) { + if (channel === _channel) { + if (message === MESSAGE_HIT) { + _this.hit(); + } else if (message === MESSAGE_ENTER_ZONE && !_isGameRunning) { + _this.resetBoppo(); + } else if (message === MESSAGE_UNLOAD_FIX && _isInitialized) { + _this.unload(); + } + } + }; + + var BoppoServer = function () { + _this = this; + _hits = 0; + _boppoClownID = null; + _coolDown = false; + CLOWN_LAUGHS.forEach(function(clownLaugh) { + _clownLaughs.push(SoundCache.getSound(SFX_PREFIX + clownLaugh)); + }); + _tickTockSound = SoundCache.getSound(SFX_PREFIX + TICK_TOCK_SOUND); + _boxingBellRingStart = SoundCache.getSound(SFX_PREFIX + BOXING_RING_BELL_START); + _boxingBellRingEnd = SoundCache.getSound(SFX_PREFIX + BOXING_RING_BELL_END); + _music = SoundCache.getSound(SFX_PREFIX + BOPPO_MUSIC); + _boppoEntities = {}; + }; + + BoppoServer.prototype = { + preload: function(entityID) { + _entityID = entityID; + _channel = CHANNEL_PREFIX + entityID; + + Messages.sendLocalMessage(_channel, MESSAGE_UNLOAD_FIX); + Script.setTimeout(function() { + clearUntrackedBoppos(); + updateBoppoEntities(); + Messages.subscribe(_channel); + Messages.messageReceived.connect(onMessage); + _this.resetBoppo(); + _isInitialized = true; + }, LOAD_TIMEOUT); + }, + resetBoppo: function() { + if (_boppoClownID !== null) { + print('deleting boppo: ' + _boppoClownID); + Entities.deleteEntity(_boppoClownID); + } + var boppoBaseProperties = Entities.getEntityProperties(_entityID, ['position', 'rotation']); + _boppoClownID = Entities.addEntity({ + angularDamping: 0.0, + collisionSoundURL: 'https://hifi-content.s3.amazonaws.com/caitlyn/production/elBoppo/51460__andre-rocha-nascimento__basket-ball-01-bounce.wav', + collisionsWillMove: true, + compoundShapeURL: 'https://hifi-content.s3.amazonaws.com/caitlyn/production/elBoppo/bopo_phys.obj', + damping: 1.0, + density: 10000, + dimensions: { + x: 1.2668079137802124, + y: 2.0568051338195801, + z: 0.88563752174377441 + }, + dynamic: 1.0, + friction: 1.0, + gravity: { + x: 0, + y: -25, + z: 0 + }, + modelURL: 'https://hifi-content.s3.amazonaws.com/caitlyn/production/elBoppo/elBoppo3_VR.fbx', + name: 'El Boppo the Punching Bag Clown', + registrationPoint: { + x: 0.5, + y: 0, + z: 0.3 + }, + restitution: 0.99, + rotation: boppoBaseProperties.rotation, + position: Vec3.sum(boppoBaseProperties.position, + Vec3.multiplyQbyV(boppoBaseProperties.rotation, { + x: 0.08666179329156876, + y: -1.5698202848434448, + z: 0.1847127377986908 + })), + script: Script.resolvePath('boppoClownEntity.js'), + shapeType: 'compound', + type: 'Model', + userData: JSON.stringify({ + lookAt: { + targetID: _boppoEntities['lookAtThis'], + disablePitch: true, + disableYaw: false, + disableRoll: true, + clearDisabledAxis: true, + rotationOffset: { x: 0.0, y: 180.0, z: 0.0} + }, + Boppo: { + type: 'boppo', + gameParentID: _entityID + }, + grabbableKey: { + grabbable: false + } + }) + }); + updateBoppoEntities(); + _boppoEntities['boppo'] = _boppoClownID; + }, + laugh: function() { + if (_laughingInjector !== undefined && _laughingInjector.isPlaying()) { + return; + } + var randomLaughIndex = Math.floor(Math.random() * _clownLaughs.length); + _laughingInjector = Audio.playSound(_clownLaughs[randomLaughIndex], { + position: Entities.getEntityProperties(_boppoClownID, ['position']).position + }); + }, + hit: function() { + if (_coolDown) { + return; + } + if (!_isGameRunning) { + var boxingRingBoppoData = getOwnBoppoUserData(); + _updateInterval = Script.setInterval(onUpdate, MILLISECONDS_PER_SECOND); + _timeLeft = boxingRingBoppoData.playTimeSeconds ? parseInt(boxingRingBoppoData.playTimeSeconds) : + DEFAULT_PLAYTIME; + _isGameRunning = true; + _hits = 0; + playSoundAtBoxingRing(_boxingBellRingStart); + _musicInjector = playSoundAtBoxingRing(_music, {loop: true, volume: 0.6}); + } + _hits++; + updateTimerDisplay(); + updateScoreDisplay(); + _this.laugh(); + }, + unload: function() { + print('unload called'); + if (_updateInterval) { + Script.clearInterval(_updateInterval); + } + Messages.messageReceived.disconnect(onMessage); + Messages.unsubscribe(_channel); + Entities.deleteEntity(_boppoClownID); + print('endOfUnload'); + } + }; + + return new BoppoServer(); + }; + + return createBoppoServer(); +}); diff --git a/unpublishedScripts/marketplace/boppo/clownGloveDispenser.js b/unpublishedScripts/marketplace/boppo/clownGloveDispenser.js new file mode 100644 index 0000000000..cd0a0c0614 --- /dev/null +++ b/unpublishedScripts/marketplace/boppo/clownGloveDispenser.js @@ -0,0 +1,154 @@ +// +// clownGloveDispenser.js +// +// Created by Thijs Wenker on 8/2/16. +// Copyright 2016 High Fidelity, Inc. +// +// Based on examples/winterSmashUp/targetPractice/shooterPlatform.js +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +(function() { + var _this = this; + + var CHANNEL_PREFIX = 'io.highfidelity.boppo_server_'; + + var leftBoxingGlove = undefined; + var rightBoxingGlove = undefined; + + var inZone = false; + + var wearGloves = function() { + leftBoxingGlove = Entities.addEntity({ + position: MyAvatar.position, + collisionsWillMove: true, + dimensions: { + x: 0.24890634417533875, + y: 0.28214839100837708, + z: 0.21127720177173615 + }, + dynamic: true, + gravity: { + x: 0, + y: -9.8, + z: 0 + }, + modelURL: "https://hifi-content.s3.amazonaws.com/caitlyn/production/elBoppo/LFT_glove_VR3.fbx", + name: "Boxing Glove - Left", + registrationPoint: { + x: 0.5, + y: 0, + z: 0.5 + }, + shapeType: "simple-hull", + type: "Model", + userData: JSON.stringify({ + grabbableKey: { + invertSolidWhileHeld: true + }, + wearable: { + joints: { + LeftHand: [ + {x: 0, y: 0.0, z: 0.02 }, + Quat.fromVec3Degrees({x: 0, y: 0, z: 0}) + ] + } + } + }) + }); + Messages.sendLocalMessage('Hifi-Hand-Grab', JSON.stringify({hand: 'left', entityID: leftBoxingGlove})); + // Allows teleporting while glove is wielded + Messages.sendLocalMessage('Hifi-Teleport-Ignore-Add', leftBoxingGlove); + + rightBoxingGlove = Entities.addEntity({ + position: MyAvatar.position, + collisionsWillMove: true, + dimensions: { + x: 0.24890634417533875, + y: 0.28214839100837708, + z: 0.21127720177173615 + }, + dynamic: true, + gravity: { + x: 0, + y: -9.8, + z: 0 + }, + modelURL: "https://hifi-content.s3.amazonaws.com/caitlyn/production/elBoppo/RT_glove_VR2.fbx", + name: "Boxing Glove - Right", + registrationPoint: { + x: 0.5, + y: 0, + z: 0.5 + }, + shapeType: "simple-hull", + type: "Model", + userData: JSON.stringify({ + grabbableKey: { + invertSolidWhileHeld: true + }, + wearable: { + joints: { + RightHand: [ + {x: 0, y: 0.0, z: 0.02 }, + Quat.fromVec3Degrees({x: 0, y: 0, z: 0}) + ] + } + } + }) + }); + Messages.sendLocalMessage('Hifi-Hand-Grab', JSON.stringify({hand: 'right', entityID: rightBoxingGlove})); + // Allows teleporting while glove is wielded + Messages.sendLocalMessage('Hifi-Teleport-Ignore-Add', rightBoxingGlove); + }; + + var cleanUpGloves = function() { + if (leftBoxingGlove !== undefined) { + Entities.deleteEntity(leftBoxingGlove); + leftBoxingGlove = undefined; + } + if (rightBoxingGlove !== undefined) { + Entities.deleteEntity(rightBoxingGlove); + rightBoxingGlove = undefined; + } + }; + + var wearGlovesIfHMD = function() { + // cleanup your old gloves if they're still there (unlikely) + cleanUpGloves(); + if (HMD.active) { + wearGloves(); + } + }; + + _this.preload = function(entityID) { + HMD.displayModeChanged.connect(function() { + if (inZone) { + wearGlovesIfHMD(); + } + }); + }; + + _this.unload = function() { + cleanUpGloves(); + }; + + _this.enterEntity = function(entityID) { + inZone = true; + print('entered boxing glove dispenser entity'); + wearGlovesIfHMD(); + + // Reset boppo if game is not running: + var parentID = Entities.getEntityProperties(entityID, ['parentID']).parentID; + Messages.sendMessage(CHANNEL_PREFIX + parentID, 'enter-zone'); + }; + + _this.leaveEntity = function(entityID) { + inZone = false; + cleanUpGloves(); + }; + + _this.unload = _this.leaveEntity; +}); diff --git a/unpublishedScripts/marketplace/boppo/createElBoppo.js b/unpublishedScripts/marketplace/boppo/createElBoppo.js new file mode 100644 index 0000000000..4df6a2acda --- /dev/null +++ b/unpublishedScripts/marketplace/boppo/createElBoppo.js @@ -0,0 +1,430 @@ +// +// createElBoppo.js +// +// Created by Thijs Wenker on 3/17/17. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +/* globals SCRIPT_IMPORT_PROPERTIES */ + +var MODELS_PATH = 'https://hifi-content.s3.amazonaws.com/DomainContent/Welcome%20Area/production/models/boxingRing/'; +var WANT_CLEANUP_ON_SCRIPT_ENDING = false; + +var getScriptPath = function(localPath) { + if (this.isCleanupAndSpawnScript) { + return 'https://hifi-content.s3.amazonaws.com/DomainContent/Welcome%20Area/Scripts/boppo/' + localPath; + } + return Script.resolvePath(localPath); +}; + +var getCreatePosition = function() { + // can either return position defined by resetScript or avatar position + if (this.isCleanupAndSpawnScript) { + return SCRIPT_IMPORT_PROPERTIES.rootPosition; + } + return Vec3.sum(MyAvatar.position, {x: 1, z: -2}); +}; + +var boxingRing = Entities.addEntity({ + dimensions: { + x: 4.0584001541137695, + y: 4.0418000221252441, + z: 3.0490000247955322 + }, + modelURL: MODELS_PATH + 'assembled/boppoBoxingRingAssembly.fbx', + name: 'Boxing Ring Assembly', + rotation: { + w: 0.9996337890625, + x: -1.52587890625e-05, + y: -0.026230275630950928, + z: -4.57763671875e-05 + }, + position: getCreatePosition(), + scriptTimestamp: 1489612158459, + serverScripts: getScriptPath('boppoServer.js'), + shapeType: 'static-mesh', + type: 'Model', + userData: JSON.stringify({ + Boppo: { + type: 'boxingring', + playTimeSeconds: 15 + } + }) +}); + +var boppoEntities = [ + { + dimensions: { + x: 0.36947935819625854, + y: 0.25536194443702698, + z: 0.059455446898937225 + }, + modelURL: MODELS_PATH + 'boxingGameSign/boppoSignFrame.fbx', + parentID: boxingRing, + localPosition: { + x: -1.0251024961471558, + y: 0.51661628484725952, + z: -1.1176263093948364 + }, + rotation: { + w: 0.996856689453125, + x: 0.013321161270141602, + y: 0.0024566650390625, + z: 0.078049898147583008 + }, + shapeType: 'box', + type: 'Model' + }, + { + dimensions: { + x: 0.33255371451377869, + y: 0.1812121719121933, + z: 0.0099999997764825821 + }, + lineHeight: 0.125, + name: 'Boxing Ring - High Score Board', + parentID: boxingRing, + localPosition: { + x: -1.0239436626434326, + y: 0.52212876081466675, + z: -1.0971509218215942 + }, + rotation: { + w: 0.9876401424407959, + x: 0.013046503067016602, + y: 0.0012359619140625, + z: 0.15605401992797852 + }, + text: '0:00', + textColor: { + blue: 0, + green: 0, + red: 255 + }, + type: 'Text', + userData: JSON.stringify({ + Boppo: { + type: 'timer' + } + }) + }, + { + dimensions: { + x: 0.50491130352020264, + y: 0.13274604082107544, + z: 0.0099999997764825821 + }, + lineHeight: 0.090000003576278687, + name: 'Boxing Ring - Score Board', + parentID: boxingRing, + localPosition: { + x: -0.77596306800842285, + y: 0.37797555327415466, + z: -1.0910623073577881 + }, + rotation: { + w: 0.9518122673034668, + x: 0.004237703513354063, + y: -0.0010041374480351806, + z: 0.30455198884010315 + }, + text: 'SCORE: 0', + textColor: { + blue: 0, + green: 0, + red: 255 + }, + type: 'Text', + userData: JSON.stringify({ + Boppo: { + type: 'score' + } + }) + }, + { + dimensions: { + x: 0.58153259754180908, + y: 0.1884911060333252, + z: 0.059455446898937225 + }, + modelURL: MODELS_PATH + 'boxingGameSign/boppoSignFrame.fbx', + parentID: boxingRing, + localPosition: { + x: -0.78200173377990723, + y: 0.35684797167778015, + z: -1.108180046081543 + }, + rotation: { + w: 0.97814905643463135, + x: 0.0040436983108520508, + y: -0.0005645751953125, + z: 0.20778214931488037 + }, + shapeType: 'box', + type: 'Model' + }, + { + dimensions: { + x: 4.1867804527282715, + y: 3.5065803527832031, + z: 5.6845207214355469 + }, + name: 'El Boppo the Clown boxing area & glove maker', + parentID: boxingRing, + localPosition: { + x: -0.012308252975344658, + y: 0.054641719907522202, + z: 0.98782551288604736 + }, + rotation: { + w: 1, + x: -1.52587890625e-05, + y: -1.52587890625e-05, + z: -1.52587890625e-05 + }, + script: getScriptPath('clownGloveDispenser.js'), + shapeType: 'box', + type: 'Zone', + visible: false + }, + { + color: { + blue: 255, + green: 5, + red: 255 + }, + dimensions: { + x: 0.20000000298023224, + y: 0.20000000298023224, + z: 0.20000000298023224 + }, + name: 'LookAtBox', + parentID: boxingRing, + localPosition: { + x: -0.1772226095199585, + y: -1.7072629928588867, + z: 1.3122396469116211 + }, + rotation: { + w: 0.999969482421875, + x: 1.52587890625e-05, + y: 0.0043793916702270508, + z: 1.52587890625e-05 + }, + shape: 'Cube', + type: 'Box', + userData: JSON.stringify({ + Boppo: { + type: 'lookAtThis' + } + }) + }, + { + color: { + blue: 209, + green: 157, + red: 209 + }, + dimensions: { + x: 1.6913000345230103, + y: 1.2124500274658203, + z: 0.2572999894618988 + }, + name: 'boppoBackBoard', + parentID: boxingRing, + localPosition: { + x: -0.19500596821308136, + y: -1.1044719219207764, + z: -0.55993378162384033 + }, + rotation: { + w: 0.9807126522064209, + x: -0.19511711597442627, + y: 0.0085297822952270508, + z: 0.0016937255859375 + }, + shape: 'Cube', + type: 'Box', + visible: false + }, + { + color: { + blue: 0, + green: 0, + red: 255 + }, + dimensions: { + x: 1.8155574798583984, + y: 0.92306196689605713, + z: 0.51203572750091553 + }, + name: 'boppoBackBoard', + parentID: boxingRing, + localPosition: { + x: -0.11036647111177444, + y: -0.051978692412376404, + z: -0.79054081439971924 + }, + rotation: { + w: 0.9807431697845459, + x: 0.19505608081817627, + y: 0.0085602998733520508, + z: -0.0017547607421875 + }, + shape: 'Cube', + type: 'Box', + visible: false + }, + { + color: { + blue: 209, + green: 157, + red: 209 + }, + dimensions: { + x: 1.9941408634185791, + y: 1.2124500274658203, + z: 0.2572999894618988 + }, + name: 'boppoBackBoard', + localPosition: { + x: 0.69560068845748901, + y: -1.3840068578720093, + z: 0.059689953923225403 + }, + rotation: { + w: 0.73458456993103027, + x: -0.24113833904266357, + y: -0.56545358896255493, + z: -0.28734266757965088 + }, + shape: 'Cube', + type: 'Box', + visible: false + }, + { + color: { + blue: 82, + green: 82, + red: 82 + }, + dimensions: { + x: 8.3777303695678711, + y: 0.87573593854904175, + z: 7.9759469032287598 + }, + parentID: boxingRing, + localPosition: { + x: -0.38302639126777649, + y: -2.121284008026123, + z: 0.3699878454208374 + }, + rotation: { + w: 0.70711839199066162, + x: -7.62939453125e-05, + y: 0.70705735683441162, + z: -1.52587890625e-05 + }, + shape: 'Triangle', + type: 'Shape' + }, + { + color: { + blue: 209, + green: 157, + red: 209 + }, + dimensions: { + x: 1.889795184135437, + y: 0.86068248748779297, + z: 0.2572999894618988 + }, + name: 'boppoBackBoard', + parentID: boxingRing, + localPosition: { + x: -0.95167744159698486, + y: -1.4756947755813599, + z: -0.042313352227210999 + }, + rotation: { + w: 0.74004733562469482, + x: -0.24461740255355835, + y: 0.56044864654541016, + z: 0.27998781204223633 + }, + shape: 'Cube', + type: 'Box', + visible: false + }, + { + color: { + blue: 0, + green: 0, + red: 255 + }, + dimensions: { + x: 4.0720257759094238, + y: 0.50657749176025391, + z: 1.4769613742828369 + }, + name: 'boppo-stepsRamp', + parentID: boxingRing, + localPosition: { + x: -0.002939039608463645, + y: -1.9770187139511108, + z: 2.2165381908416748 + }, + rotation: { + w: 0.99252307415008545, + x: 0.12184333801269531, + y: -1.52587890625e-05, + z: -1.52587890625e-05 + }, + shape: 'Cube', + type: 'Box', + visible: false + }, + { + color: { + blue: 150, + green: 150, + red: 150 + }, + cutoff: 90, + dimensions: { + x: 5.2220535278320312, + y: 5.2220535278320312, + z: 5.2220535278320312 + }, + falloffRadius: 2, + intensity: 15, + name: 'boxing ring light', + parentID: boxingRing, + localPosition: { + x: -1.4094564914703369, + y: -0.36021926999092102, + z: 0.81797939538955688 + }, + rotation: { + w: 0.9807431697845459, + x: 1.52587890625e-05, + y: -0.19520866870880127, + z: -1.52587890625e-05 + }, + type: 'Light' + } +]; + +boppoEntities.forEach(function(entityProperties) { + entityProperties['parentID'] = boxingRing; + Entities.addEntity(entityProperties); +}); + +if (WANT_CLEANUP_ON_SCRIPT_ENDING) { + Script.scriptEnding.connect(function() { + Entities.deleteEntity(boxingRing); + }); +} diff --git a/unpublishedScripts/marketplace/boppo/lookAtEntity.js b/unpublishedScripts/marketplace/boppo/lookAtEntity.js new file mode 100644 index 0000000000..ba072814f2 --- /dev/null +++ b/unpublishedScripts/marketplace/boppo/lookAtEntity.js @@ -0,0 +1,98 @@ +// +// lookAtTarget.js +// +// Created by Thijs Wenker on 3/15/17. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +/* globals LookAtTarget:true */ + +LookAtTarget = function(sourceEntityID) { + /* private variables */ + var _this, + _options, + _sourceEntityID, + _sourceEntityProperties, + REQUIRED_PROPERTIES = ['position', 'rotation', 'userData'], + LOOK_AT_TAG = 'lookAtTarget'; + + LookAtTarget = function(sourceEntityID) { + _this = this; + _sourceEntityID = sourceEntityID; + _this.updateOptions(); + }; + + /* private functions */ + var updateEntitySourceProperties = function() { + _sourceEntityProperties = Entities.getEntityProperties(_sourceEntityID, REQUIRED_PROPERTIES); + }; + + var getUpdatedActionProperties = function() { + return { + targetRotation: _this.getLookAtRotation(), + angularTimeScale: 0.1, + ttl: 10 + }; + }; + + var getNewActionProperties = function() { + var newActionProperties = getUpdatedActionProperties(); + newActionProperties.tag = LOOK_AT_TAG; + return newActionProperties; + }; + + LookAtTarget.prototype = { + /* public functions */ + updateOptions: function() { + updateEntitySourceProperties(); + _options = JSON.parse(_sourceEntityProperties.userData).lookAt; + }, + getTargetPosition: function() { + return Entities.getEntityProperties(_options.targetID).position; + }, + getLookAtRotation: function() { + _this.updateOptions(); + + var newRotation = Quat.lookAt(_sourceEntityProperties.position, _this.getTargetPosition(), Vec3.UP); + if (_options.rotationOffset !== undefined) { + newRotation = Quat.multiply(newRotation, Quat.fromVec3Degrees(_options.rotationOffset)); + } + if (_options.disablePitch || _options.disableYaw || _options.disablePitch) { + var disabledAxis = _options.clearDisabledAxis ? Vec3.ZERO : + Quat.safeEulerAngles(_sourceEntityProperties.rotation); + var newEulers = Quat.safeEulerAngles(newRotation); + newRotation = Quat.fromVec3Degrees({ + x: _options.disablePitch ? disabledAxis.x : newEulers.x, + y: _options.disableYaw ? disabledAxis.y : newEulers.y, + z: _options.disableRoll ? disabledAxis.z : newEulers.z + }); + } + return newRotation; + }, + lookAtDirectly: function() { + Entities.editEntity(_sourceEntityID, {rotation: _this.getLookAtRotation()}); + }, + lookAtByAction: function() { + var actionIDs = Entities.getActionIDs(_sourceEntityID); + var actionFound = false; + actionIDs.forEach(function(actionID) { + if (actionFound) { + return; + } + var actionArguments = Entities.getActionArguments(_sourceEntityID, actionID); + if (actionArguments.tag === LOOK_AT_TAG) { + actionFound = true; + Entities.updateAction(_sourceEntityID, actionID, getUpdatedActionProperties()); + } + }); + if (!actionFound) { + Entities.addAction('spring', _sourceEntityID, getNewActionProperties()); + } + } + }; + + return new LookAtTarget(sourceEntityID); +};