diff --git a/BUILD.md b/BUILD.md index c45b7cb636..30302d611b 100644 --- a/BUILD.md +++ b/BUILD.md @@ -1,28 +1,25 @@ ### Dependencies -* [cmake](https://cmake.org/download/) ~> 3.3.2 -* [Qt](https://www.qt.io/download-open-source) ~> 5.6.2 -* [OpenSSL](https://www.openssl.org/community/binaries.html) - * IMPORTANT: Use the latest available version of OpenSSL to avoid security vulnerabilities. -* [VHACD](https://github.com/virneo/v-hacd)(clone this repository)(Optional) +- [cmake](https://cmake.org/download/): 3.9 +- [Qt](https://www.qt.io/download-open-source): 5.9.1 +- [OpenSSL](https://www.openssl.org/): Use the latest available version of OpenSSL to avoid security vulnerabilities. +- [VHACD](https://github.com/virneo/v-hacd)(clone this repository)(Optional) -#### CMake External Project Dependencies +### CMake External Project Dependencies -* [boostconfig](https://github.com/boostorg/config) ~> 1.58 -* [Bullet Physics Engine](https://github.com/bulletphysics/bullet3/releases) ~> 2.83 -* [GLEW](http://glew.sourceforge.net/) -* [glm](https://glm.g-truc.net/0.9.5/index.html) ~> 0.9.5.4 -* [gverb](https://github.com/highfidelity/gverb) -* [Oculus SDK](https://developer.oculus.com/downloads/) ~> 0.6 (Win32) / 0.5 (Mac / Linux) -* [oglplus](http://oglplus.org/) ~> 0.63 -* [OpenVR](https://github.com/ValveSoftware/openvr) ~> 0.91 (Win32 only) -* [Polyvox](http://www.volumesoffun.com/) ~> 0.2.1 -* [QuaZip](https://sourceforge.net/projects/quazip/files/quazip/) ~> 0.7.1 -* [SDL2](https://www.libsdl.org/download-2.0.php) ~> 2.0.3 -* [soxr](https://sourceforge.net/p/soxr/wiki/Home/) ~> 0.1.1 -* [Intel Threading Building Blocks](https://www.threadingbuildingblocks.org/) ~> 4.3 -* [Sixense](http://sixense.com/) ~> 071615 -* [zlib](http://www.zlib.net/) ~> 1.28 (Win32 only) +These dependencies need not be installed manually. They are automatically downloaded on the platforms where they are required. +- [Bullet Physics Engine](https://github.com/bulletphysics/bullet3/releases): 2.83 +- [GLEW](http://glew.sourceforge.net/): 1.13 +- [glm](https://glm.g-truc.net/0.9.8/index.html): 0.9.8 +- [Oculus SDK](https://developer.oculus.com/downloads/): 1.11 (Win32) / 0.5 (Mac) +- [OpenVR](https://github.com/ValveSoftware/openvr): 1.0.6 (Win32 only) +- [Polyvox](http://www.volumesoffun.com/): 0.2.1 +- [QuaZip](https://sourceforge.net/projects/quazip/files/quazip/): 0.7.3 +- [SDL2](https://www.libsdl.org/download-2.0.php): 2.0.3 +- [Intel Threading Building Blocks](https://www.threadingbuildingblocks.org/): 4.3 +- [Sixense](http://sixense.com/): 071615 +- [zlib](http://www.zlib.net/): 1.28 (Win32 only) +- nVidia Texture Tools: 2.1 The above dependencies will be downloaded, built, linked and included automatically by CMake where we require them. The CMakeLists files that handle grabbing each of the following external dependencies can be found in the [cmake/externals folder](cmake/externals). The resulting downloads, source files and binaries will be placed in the `build/ext` folder in each of the subfolders for each external project. diff --git a/BUILD_LINUX.md b/BUILD_LINUX.md index d40576a75d..038f53154c 100644 --- a/BUILD_LINUX.md +++ b/BUILD_LINUX.md @@ -1,7 +1,96 @@ +# Linux build guide + Please read the [general build guide](BUILD.md) for information on dependencies required for all platforms. Only Linux specific instructions are found in this file. -### Qt5 Dependencies +## Qt5 Dependencies Should you choose not to install Qt5 via a package manager that handles dependencies for you, you may be missing some Qt5 dependencies. On Ubuntu, for example, the following additional packages are required: libasound2 libxmu-dev libxi-dev freeglut3-dev libasound2-dev libjack0 libjack-dev libxrandr-dev libudev-dev libssl-dev + +## Ubuntu 16.04 specific build guide + +### Prepare environment + +Install qt: +```bash +wget http://debian.highfidelity.com/pool/h/hi/hifi-qt5.6.1_5.6.1_amd64.deb +sudo dpkg -i hifi-qt5.6.1_5.6.1_amd64.deb +``` + +Install build dependencies: +```bash +sudo apt-get install libasound2 libxmu-dev libxi-dev freeglut3-dev libasound2-dev libjack0 libjack-dev libxrandr-dev libudev-dev libssl-dev +``` + +To compile interface in a server you must install: +```bash +sudo apt -y install libpulse0 libnss3 libnspr4 libfontconfig1 libxcursor1 libxcomposite1 libxtst6 libxslt1.1 +``` + +Install build tools: +```bash +sudo apt install cmake +``` + +### Get code and checkout the tag you need + +Clone this repository: +```bash +git clone https://github.com/highfidelity/hifi.git +``` + +To compile a RELEASE version checkout the tag you need getting a list of all tags: +```bash +git fetch -a +git tags +``` + +Then checkout last tag with: +```bash +git checkout tags/RELEASE-6819 +``` + +Or go to the highfidelity download page (https://highfidelity.com/download) to get the release version. For example, if there is a BETA 6731 type: +```bash +git checkout tags/RELEASE-6731 +``` + +### Compiling + +Create the build directory: +```bash +mkdir -p hifi/build +cd hifi/build +``` + +Prepare makefiles: +```bash +cmake -DQT_CMAKE_PREFIX_PATH=/usr/local/Qt5.6.1/5.6/gcc_64/lib/cmake .. +``` + +Start compilation and get a cup of coffee: +```bash +make domain-server assignment-client interface +``` + +In a server does not make sense to compile interface + +### Running the software + +Running domain server: +```bash +./domain-server/domain-server +``` + +Running assignment client: +```bash +./assignment-client/assignment-client -n 6 +``` + +Running interface: +```bash +./interface/interface +``` + +Go to localhost in running interface. diff --git a/BUILD_WIN.md b/BUILD_WIN.md index 818a176f75..3e93656d45 100644 --- a/BUILD_WIN.md +++ b/BUILD_WIN.md @@ -1,42 +1,45 @@ This is a stand-alone guide for creating your first High Fidelity build for Windows 64-bit. ## Building High Fidelity +Note: We are now using Visual Studio 2017 and Qt 5.9.1. If you are upgrading from Visual Studio 2013 and Qt 5.6.2, do a clean uninstall of those versions before going through this guide. -### Step 1. Installing Visual Studio 2013 +Note: The prerequisites will require about 10 GB of space on your drive. -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. +### Step 1. Visual Studio 2017 -Note: Newer versions of Visual Studio are not yet compatible. +If you don’t have Community or Professional edition of Visual Studio 2017, download [Visual Studio Community 2017](https://www.visualstudio.com/downloads/). + +When selecting components, check "Desktop development with C++." Also check "Windows 8.1 SDK and UCRT SDK" and "VC++ 2015.3 v140 toolset (x86,x64)" on the Summary toolbar on the right. ### Step 2. Installing CMake -Download and install the [CMake 3.8.0 win64-x64 Installer](https://cmake.org/files/v3.8/cmake-3.8.0-win64-x64.msi). Make sure "Add CMake to system PATH for all users" is checked when going through the installer. +Download and install the latest version of CMake 3.9. Download the file named win64-x64 Installer from the [CMake Website](https://cmake.org/download/). Make sure to check "Add CMake to system PATH for all users" when prompted during installation. ### Step 3. Installing Qt -Download and install the [Qt 5.6.2 for Windows 64-bit (VS 2013)](http://download.qt.io/official_releases/qt/5.6/5.6.2/qt-opensource-windows-x86-msvc2013_64-5.6.2.exe). +Download and install the [Qt Online Installer](https://www.qt.io/download-open-source/?hsCtaTracking=f977210e-de67-475f-a32b-65cec207fd03%7Cd62710cd-e1db-46aa-8d4d-2f1c1ffdacea). While installing, you only need to have the following components checked under Qt 5.9.1: "msvc2017 64-bit", "Qt WebEngine", and "Qt Script (Deprecated)". -Keep the default components checked when going through the installer. +Note: Installing the Sources is optional but recommended if you have room for them (~2GB). ### Step 4. Setting Qt Environment Variable 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": `%QT_DIR%\5.6\msvc2013_64\lib\cmake` +* Set "Variable value": `C:\Qt\5.9.1\msvc2017_64\lib\cmake` ### Step 5. Installing OpenSSL -Download and install the [Win64 OpenSSL v1.0.2L Installer](https://slproweb.com/download/Win64OpenSSL-1_0_2L.exe). +Download and install the Win64 OpenSSL v1.0.2 Installer[https://slproweb.com/products/Win32OpenSSL.html]. ### 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" -```` +cmake .. -G "Visual Studio 15 Win64" +``` Where `%HIFI_DIR%` is the directory for the highfidelity repository. @@ -72,14 +75,6 @@ For any problems after Step #6, first try this: 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.2) installed and `QT_CMAKE_PREFIX_PATH` environment variable is set correctly. +Make sure you have the correct version (5.9.1) installed and `QT_CMAKE_PREFIX_PATH` environment variable is set correctly. diff --git a/CMakeLists.txt b/CMakeLists.txt index 3c90256134..9712b2d32e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,4 +1,8 @@ -cmake_minimum_required(VERSION 3.2) +if (WIN32) + cmake_minimum_required(VERSION 3.7) +else() + cmake_minimum_required(VERSION 3.2) +endif() if (USE_ANDROID_TOOLCHAIN) set(CMAKE_TOOLCHAIN_FILE "${CMAKE_CURRENT_SOURCE_DIR}/cmake/android/android.toolchain.cmake") @@ -33,6 +37,10 @@ set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -DDEBUG") find_package( Threads ) if (WIN32) + if (NOT "${CMAKE_SIZEOF_VOID_P}" EQUAL "8") + message( FATAL_ERROR "Only 64 bit builds supported." ) + endif() + add_definitions(-DNOMINMAX -D_CRT_SECURE_NO_WARNINGS) if (NOT WINDOW_SDK_PATH) @@ -41,16 +49,13 @@ if (WIN32) # sets path for Microsoft SDKs # if you get build error about missing 'glu32' this path is likely wrong - if (MSVC10) - set(WINDOW_SDK_PATH "C:\\Program Files\\Microsoft SDKs\\Windows\\v7.1 " CACHE PATH "Windows SDK PATH") - elseif (MSVC12) - if ("${CMAKE_SIZEOF_VOID_P}" EQUAL "8") - set(WINDOW_SDK_FOLDER "x64") - else() - set(WINDOW_SDK_FOLDER "x86") - endif() + if (MSVC_VERSION GREATER_EQUAL 1910) # VS 2017 + set(WINDOW_SDK_PATH "C:/Program Files (x86)/Windows Kits/10/Lib/${CMAKE_VS_WINDOWS_TARGET_PLATFORM_VERSION}/x64" CACHE PATH "Windows SDK PATH") + elseif (MSVC_VERSION GREATER_EQUAL 1800) # VS 2013 set(WINDOW_SDK_PATH "C:\\Program Files (x86)\\Windows Kits\\8.1\\Lib\\winv6.3\\um\\${WINDOW_SDK_FOLDER}" CACHE PATH "Windows SDK PATH") - endif () + else() + message( FATAL_ERROR "Visual Studio 2013 or higher required." ) + endif() if (DEBUG_DISCOVERED_SDK_PATH) message(STATUS "The discovered Windows SDK path is ${WINDOW_SDK_PATH}") @@ -103,7 +108,7 @@ else () endif () if (APPLE) - set(CMAKE_XCODE_ATTRIBUTE_CLANG_CXX_LANGUAGE_STANDARD "c++0x") + set(CMAKE_XCODE_ATTRIBUTE_CLANG_CXX_LANGUAGE_STANDARD "c++11") set(CMAKE_XCODE_ATTRIBUTE_CLANG_CXX_LIBRARY "libc++") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} --stdlib=libc++") endif () diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a0d867ade9..4654c311cc 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://wiki.highfidelity.com/wiki/Coding_Standards) + * Follow the [coding standard](https://docs.highfidelity.com/build-guide/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/Test Plan 2.docx b/Test Plan 2.docx new file mode 100644 index 0000000000..da60821b53 Binary files /dev/null and b/Test Plan 2.docx differ diff --git a/assignment-client/CMakeLists.txt b/assignment-client/CMakeLists.txt index 0ef2db3b9d..1a27ddd479 100644 --- a/assignment-client/CMakeLists.txt +++ b/assignment-client/CMakeLists.txt @@ -13,7 +13,7 @@ setup_memory_debugger() link_hifi_libraries( audio avatars octree gpu model fbx entities networking animation recording shared script-engine embedded-webserver - physics plugins + controllers physics plugins midi ) if (WIN32) diff --git a/assignment-client/src/Agent.cpp b/assignment-client/src/Agent.cpp index 8aec5adb1f..c44fdf74ff 100644 --- a/assignment-client/src/Agent.cpp +++ b/assignment-client/src/Agent.cpp @@ -23,6 +23,7 @@ #include #include #include +#include #include #include #include @@ -50,14 +51,14 @@ #include "RecordingScriptingInterface.h" #include "AbstractAudioInterface.h" -#include "AvatarAudioTimer.h" static const int RECEIVED_AUDIO_STREAM_CAPACITY_FRAMES = 10; Agent::Agent(ReceivedMessage& message) : ThreadedAssignment(message), _receivedAudioStream(RECEIVED_AUDIO_STREAM_CAPACITY_FRAMES, RECEIVED_AUDIO_STREAM_CAPACITY_FRAMES), - _audioGate(AudioConstants::SAMPLE_RATE, AudioConstants::MONO) + _audioGate(AudioConstants::SAMPLE_RATE, AudioConstants::MONO), + _avatarAudioTimer(this) { _entityEditSender.setPacketsPerSecond(DEFAULT_ENTITY_PPS_PER_SCRIPT); DependencyManager::get()->setPacketSender(&_entityEditSender); @@ -81,6 +82,9 @@ Agent::Agent(ReceivedMessage& message) : DependencyManager::set(); DependencyManager::set(); + // Needed to ensure the creation of the DebugDraw instance on the main thread + DebugDraw::getInstance(); + auto& packetReceiver = DependencyManager::get()->getPacketReceiver(); @@ -92,6 +96,14 @@ Agent::Agent(ReceivedMessage& message) : this, "handleOctreePacket"); packetReceiver.registerListener(PacketType::Jurisdiction, this, "handleJurisdictionPacket"); packetReceiver.registerListener(PacketType::SelectedAudioFormat, this, "handleSelectedAudioFormat"); + + + // 100Hz timer for audio + const int TARGET_INTERVAL_MSEC = 10; // 10ms + connect(&_avatarAudioTimer, &QTimer::timeout, this, &Agent::processAgentAvatarAudio); + _avatarAudioTimer.setSingleShot(false); + _avatarAudioTimer.setInterval(TARGET_INTERVAL_MSEC); + _avatarAudioTimer.setTimerType(Qt::PreciseTimer); } void Agent::playAvatarSound(SharedSoundPointer sound) { @@ -471,14 +483,7 @@ void Agent::executeScript() { 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); - _avatarAudioTimerThread.start(); + QMetaObject::invokeMethod(&_avatarAudioTimer, "start"); // Agents should run at 45hz static const int AVATAR_DATA_HZ = 45; @@ -557,7 +562,7 @@ void Agent::setIsAvatar(bool isAvatar) { _avatarIdentityTimer->start(AVATAR_IDENTITY_PACKET_SEND_INTERVAL_MSECS); // FIXME - we shouldn't really need to constantly send identity packets // tell the avatarAudioTimer to start ticking - emit startAvatarAudioTimer(); + QMetaObject::invokeMethod(&_avatarAudioTimer, "start"); } @@ -586,7 +591,7 @@ void Agent::setIsAvatar(bool isAvatar) { nodeList->sendPacket(std::move(packet), *node); }); } - emit stopAvatarAudioTimer(); + QMetaObject::invokeMethod(&_avatarAudioTimer, "stop"); } } @@ -604,6 +609,24 @@ void Agent::processAgentAvatar() { AvatarData::AvatarDataDetail dataDetail = (randFloat() < AVATAR_SEND_FULL_UPDATE_RATIO) ? AvatarData::SendAllData : AvatarData::CullSmallData; QByteArray avatarByteArray = scriptedAvatar->toByteArrayStateful(dataDetail); + + int maximumByteArraySize = NLPacket::maxPayloadSize(PacketType::AvatarData) - sizeof(AvatarDataSequenceNumber); + + if (avatarByteArray.size() > maximumByteArraySize) { + qWarning() << " scriptedAvatar->toByteArrayStateful() resulted in very large buffer:" << avatarByteArray.size() << "... attempt to drop facial data"; + avatarByteArray = scriptedAvatar->toByteArrayStateful(dataDetail, true); + + if (avatarByteArray.size() > maximumByteArraySize) { + qWarning() << " scriptedAvatar->toByteArrayStateful() without facial data resulted in very large buffer:" << avatarByteArray.size() << "... reduce to MinimumData"; + avatarByteArray = scriptedAvatar->toByteArrayStateful(AvatarData::MinimumData, true); + + if (avatarByteArray.size() > maximumByteArraySize) { + qWarning() << " scriptedAvatar->toByteArrayStateful() MinimumData resulted in very large buffer:" << avatarByteArray.size() << "... FAIL!!"; + return; + } + } + } + scriptedAvatar->doneEncoding(true); static AvatarDataSequenceNumber sequenceNumber = 0; @@ -796,8 +819,7 @@ void Agent::aboutToFinish() { DependencyManager::destroy(); DependencyManager::destroy(); - emit stopAvatarAudioTimer(); - _avatarAudioTimerThread.quit(); + QMetaObject::invokeMethod(&_avatarAudioTimer, "stop"); // cleanup codec & encoder if (_codec && _encoder) { diff --git a/assignment-client/src/Agent.h b/assignment-client/src/Agent.h index 549a0858b7..da60a07367 100644 --- a/assignment-client/src/Agent.h +++ b/assignment-client/src/Agent.h @@ -81,9 +81,6 @@ private slots: void processAgentAvatar(); void processAgentAvatarAudio(); -signals: - void startAvatarAudioTimer(); - void stopAvatarAudioTimer(); private: void negotiateAudioFormat(); void selectAudioFormat(const QString& selectedCodecName); @@ -112,13 +109,13 @@ private: QHash _outgoingScriptAudioSequenceNumbers; AudioGate _audioGate; - bool _audioGateOpen { false }; + bool _audioGateOpen { true }; bool _isNoiseGateEnabled { false }; CodecPluginPointer _codec; QString _selectedCodecName; Encoder* _encoder { nullptr }; - QThread _avatarAudioTimerThread; + QTimer _avatarAudioTimer; bool _flushEncoder { false }; }; diff --git a/assignment-client/src/AssignmentClientMonitor.cpp b/assignment-client/src/AssignmentClientMonitor.cpp index 070034d54b..5539d6a0bb 100644 --- a/assignment-client/src/AssignmentClientMonitor.cpp +++ b/assignment-client/src/AssignmentClientMonitor.cpp @@ -91,9 +91,22 @@ void AssignmentClientMonitor::simultaneousWaitOnChildren(int waitMsecs) { } } -void AssignmentClientMonitor::childProcessFinished(qint64 pid) { +void AssignmentClientMonitor::childProcessFinished(qint64 pid, int exitCode, QProcess::ExitStatus exitStatus) { + auto message = "Child process " + QString::number(pid) + " has %1 with exit code " + QString::number(exitCode) + "."; + if (_childProcesses.remove(pid)) { - qDebug() << "Child process" << pid << "has finished. Removed from internal map."; + message.append(" Removed from internal map."); + } else { + message.append(" Could not find process in internal map."); + } + + switch (exitStatus) { + case QProcess::NormalExit: + qDebug() << qPrintable(message.arg("returned")); + break; + case QProcess::CrashExit: + qCritical() << qPrintable(message.arg("crashed")); + break; } } @@ -221,7 +234,9 @@ void AssignmentClientMonitor::spawnChildClient() { auto pid = assignmentClient->processId(); // make sure we hear that this process has finished when it does connect(assignmentClient, static_cast(&QProcess::finished), - this, [this, pid]() { childProcessFinished(pid); }); + this, [this, pid](int exitCode, QProcess::ExitStatus exitStatus) { + childProcessFinished(pid, exitCode, exitStatus); + }); qDebug() << "Spawned a child client with PID" << assignmentClient->processId(); diff --git a/assignment-client/src/AssignmentClientMonitor.h b/assignment-client/src/AssignmentClientMonitor.h index a7f69a559b..8848d503ae 100644 --- a/assignment-client/src/AssignmentClientMonitor.h +++ b/assignment-client/src/AssignmentClientMonitor.h @@ -44,7 +44,7 @@ public: void stopChildProcesses(); private slots: void checkSpares(); - void childProcessFinished(qint64 pid); + void childProcessFinished(qint64 pid, int exitCode, QProcess::ExitStatus exitStatus); void handleChildStatusPacket(QSharedPointer message); bool handleHTTPRequest(HTTPConnection* connection, const QUrl& url, bool skipSubHandler = false) override; diff --git a/assignment-client/src/AvatarAudioTimer.cpp b/assignment-client/src/AvatarAudioTimer.cpp deleted file mode 100644 index d031b9d9f6..0000000000 --- a/assignment-client/src/AvatarAudioTimer.cpp +++ /dev/null @@ -1,36 +0,0 @@ -// -// AvatarAudioTimer.cpp -// assignment-client/src -// -// Created by David Kelly on 10/12/13. -// 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 -#include -#include "AvatarAudioTimer.h" - -// this should send a signal every 10ms, with pretty good precision. Hardcoding -// to 10ms since that's what you'd want for audio. -void AvatarAudioTimer::start() { - auto startTime = usecTimestampNow(); - quint64 frameCounter = 0; - const int TARGET_INTERVAL_USEC = 10000; // 10ms - while (!_quit) { - ++frameCounter; - - // tick every 10ms from startTime - quint64 targetTime = startTime + frameCounter * TARGET_INTERVAL_USEC; - quint64 now = usecTimestampNow(); - - // avoid quint64 underflow - if (now < targetTime) { - usleep(targetTime - now); - } - - emit avatarTick(); - } - qDebug() << "AvatarAudioTimer is finished"; -} diff --git a/assignment-client/src/AvatarAudioTimer.h b/assignment-client/src/AvatarAudioTimer.h deleted file mode 100644 index 1f6381b030..0000000000 --- a/assignment-client/src/AvatarAudioTimer.h +++ /dev/null @@ -1,31 +0,0 @@ -// -// AvatarAudioTimer.h -// assignment-client/src -// -// Created by David Kelly on 10/12/13. -// 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_AvatarAudioTimer_h -#define hifi_AvatarAudioTimer_h - -#include - -class AvatarAudioTimer : public QObject { - Q_OBJECT - -signals: - void avatarTick(); - -public slots: - void start(); - void stop() { _quit = true; } - -private: - bool _quit { false }; -}; - -#endif //hifi_AvatarAudioTimer_h diff --git a/assignment-client/src/audio/AudioMixerSlavePool.cpp b/assignment-client/src/audio/AudioMixerSlavePool.cpp index 643361ac5d..d2c19d97ba 100644 --- a/assignment-client/src/audio/AudioMixerSlavePool.cpp +++ b/assignment-client/src/audio/AudioMixerSlavePool.cpp @@ -76,7 +76,7 @@ void AudioMixerSlavePool::processPackets(ConstIter begin, ConstIter end) { void AudioMixerSlavePool::mix(ConstIter begin, ConstIter end, unsigned int frame, float throttlingRatio) { _function = &AudioMixerSlave::mix; - _configure = [&](AudioMixerSlave& slave) { + _configure = [=](AudioMixerSlave& slave) { slave.configureMix(_begin, _end, _frame, _throttlingRatio); }; _frame = frame; diff --git a/assignment-client/src/avatars/AvatarMixerClientData.cpp b/assignment-client/src/avatars/AvatarMixerClientData.cpp index 4d80bc7d17..a4bf8fa253 100644 --- a/assignment-client/src/avatars/AvatarMixerClientData.cpp +++ b/assignment-client/src/avatars/AvatarMixerClientData.cpp @@ -108,9 +108,6 @@ void AvatarMixerClientData::ignoreOther(SharedNodePointer self, SharedNodePointe void AvatarMixerClientData::removeFromRadiusIgnoringSet(SharedNodePointer self, const QUuid& other) { if (isRadiusIgnoring(other)) { _radiusIgnoredOthers.erase(other); - auto exitingSpaceBubblePacket = NLPacket::create(PacketType::ExitingSpaceBubble, NUM_BYTES_RFC4122_UUID); - exitingSpaceBubblePacket->write(other.toRfc4122()); - DependencyManager::get()->sendUnreliablePacket(*exitingSpaceBubblePacket, *self); } } diff --git a/assignment-client/src/avatars/AvatarMixerSlave.cpp b/assignment-client/src/avatars/AvatarMixerSlave.cpp index 7e37f583ff..34feafbd4d 100644 --- a/assignment-client/src/avatars/AvatarMixerSlave.cpp +++ b/assignment-client/src/avatars/AvatarMixerSlave.cpp @@ -383,11 +383,11 @@ void AvatarMixerSlave::broadcastAvatarDataToAgent(const SharedNodePointer& node) qCWarning(avatars) << "otherAvatar.toByteArray() without facial data resulted in very large buffer:" << bytes.size() << "... reduce to MinimumData"; bytes = otherAvatar->toByteArray(AvatarData::MinimumData, lastEncodeForOther, lastSentJointsForOther, hasFlagsOut, dropFaceTracking, distanceAdjust, viewerPosition, &lastSentJointsForOther); - } - if (bytes.size() > MAX_ALLOWED_AVATAR_DATA) { - qCWarning(avatars) << "otherAvatar.toByteArray() MinimumData resulted in very large buffer:" << bytes.size() << "... FAIL!!"; - includeThisAvatar = false; + if (bytes.size() > MAX_ALLOWED_AVATAR_DATA) { + qCWarning(avatars) << "otherAvatar.toByteArray() MinimumData resulted in very large buffer:" << bytes.size() << "... FAIL!!"; + includeThisAvatar = false; + } } } diff --git a/assignment-client/src/avatars/AvatarMixerSlavePool.cpp b/assignment-client/src/avatars/AvatarMixerSlavePool.cpp index cb5ae7735a..8afbc1cfe4 100644 --- a/assignment-client/src/avatars/AvatarMixerSlavePool.cpp +++ b/assignment-client/src/avatars/AvatarMixerSlavePool.cpp @@ -69,7 +69,7 @@ static AvatarMixerSlave slave; void AvatarMixerSlavePool::processIncomingPackets(ConstIter begin, ConstIter end) { _function = &AvatarMixerSlave::processIncomingPackets; - _configure = [&](AvatarMixerSlave& slave) { + _configure = [=](AvatarMixerSlave& slave) { slave.configure(begin, end); }; run(begin, end); @@ -79,7 +79,7 @@ void AvatarMixerSlavePool::broadcastAvatarData(ConstIter begin, ConstIter end, p_high_resolution_clock::time_point lastFrameTimestamp, float maxKbpsPerNode, float throttlingRatio) { _function = &AvatarMixerSlave::broadcastAvatarData; - _configure = [&](AvatarMixerSlave& slave) { + _configure = [=](AvatarMixerSlave& slave) { slave.configureBroadcast(begin, end, lastFrameTimestamp, maxKbpsPerNode, throttlingRatio); }; run(begin, end); diff --git a/assignment-client/src/avatars/ScriptableAvatar.cpp b/assignment-client/src/avatars/ScriptableAvatar.cpp index 6443e7bf4b..c7715d4014 100644 --- a/assignment-client/src/avatars/ScriptableAvatar.cpp +++ b/assignment-client/src/avatars/ScriptableAvatar.cpp @@ -19,7 +19,7 @@ #include "ScriptableAvatar.h" -QByteArray ScriptableAvatar::toByteArrayStateful(AvatarDataDetail dataDetail) { +QByteArray ScriptableAvatar::toByteArrayStateful(AvatarDataDetail dataDetail, bool dropFaceTracking) { _globalPosition = getPosition(); return AvatarData::toByteArrayStateful(dataDetail); } diff --git a/assignment-client/src/avatars/ScriptableAvatar.h b/assignment-client/src/avatars/ScriptableAvatar.h index 1028912e55..b1039b5ac0 100644 --- a/assignment-client/src/avatars/ScriptableAvatar.h +++ b/assignment-client/src/avatars/ScriptableAvatar.h @@ -28,7 +28,7 @@ public: Q_INVOKABLE AnimationDetails getAnimationDetails(); virtual void setSkeletonModelURL(const QUrl& skeletonModelURL) override; - virtual QByteArray toByteArrayStateful(AvatarDataDetail dataDetail) override; + virtual QByteArray toByteArrayStateful(AvatarDataDetail dataDetail, bool dropFaceTracking = false) override; private slots: diff --git a/assignment-client/src/entities/EntityServer.cpp b/assignment-client/src/entities/EntityServer.cpp index ac686e2e0a..2c8f8a9e37 100644 --- a/assignment-client/src/entities/EntityServer.cpp +++ b/assignment-client/src/entities/EntityServer.cpp @@ -50,6 +50,12 @@ EntityServer::~EntityServer() { tree->removeNewlyCreatedHook(this); } +void EntityServer::aboutToFinish() { + DependencyManager::get()->cleanup(); + + OctreeServer::aboutToFinish(); +} + void EntityServer::handleEntityPacket(QSharedPointer message, SharedNodePointer senderNode) { if (_octreeInboundPacketProcessor) { _octreeInboundPacketProcessor->queueReceivedPacket(message, senderNode); diff --git a/assignment-client/src/entities/EntityServer.h b/assignment-client/src/entities/EntityServer.h index 40676e79bd..26c2f149aa 100644 --- a/assignment-client/src/entities/EntityServer.h +++ b/assignment-client/src/entities/EntityServer.h @@ -59,6 +59,8 @@ public: virtual void trackSend(const QUuid& dataID, quint64 dataLastEdited, const QUuid& sessionID) override; virtual void trackViewerGone(const QUuid& sessionID) override; + virtual void aboutToFinish() override; + public slots: virtual void nodeAdded(SharedNodePointer node) override; virtual void nodeKilled(SharedNodePointer node) override; diff --git a/assignment-client/src/octree/OctreeSendThread.cpp b/assignment-client/src/octree/OctreeSendThread.cpp index 7db06f12c0..868b377ced 100644 --- a/assignment-client/src/octree/OctreeSendThread.cpp +++ b/assignment-client/src/octree/OctreeSendThread.cpp @@ -81,7 +81,6 @@ bool OctreeSendThread::process() { // don't do any send processing until the initial load of the octree is complete... if (_myServer->isInitialLoadComplete()) { if (auto node = _node.lock()) { - _nodeMissingCount = 0; OctreeQueryNode* nodeData = static_cast(node->getLinkedData()); // Sometimes the node data has not yet been linked, in which case we can't really do anything @@ -129,8 +128,7 @@ AtomicUIntStat OctreeSendThread::_totalSpecialBytes { 0 }; AtomicUIntStat OctreeSendThread::_totalSpecialPackets { 0 }; -int OctreeSendThread::handlePacketSend(SharedNodePointer node, OctreeQueryNode* nodeData, int& trueBytesSent, - int& truePacketsSent, bool dontSuppressDuplicate) { +int OctreeSendThread::handlePacketSend(SharedNodePointer node, OctreeQueryNode* nodeData, bool dontSuppressDuplicate) { OctreeServer::didHandlePacketSend(this); // if we're shutting down, then exit early @@ -141,15 +139,14 @@ int OctreeSendThread::handlePacketSend(SharedNodePointer node, OctreeQueryNode* bool debug = _myServer->wantsDebugSending(); quint64 now = usecTimestampNow(); - bool packetSent = false; // did we send a packet? - int packetsSent = 0; + int numPackets = 0; // Here's where we check to see if this packet is a duplicate of the last packet. If it is, we will silently // obscure the packet and not send it. This allows the callers and upper level logic to not need to know about // this rate control savings. if (!dontSuppressDuplicate && nodeData->shouldSuppressDuplicatePacket()) { nodeData->resetOctreePacket(); // we still need to reset it though! - return packetsSent; // without sending... + return numPackets; // without sending... } // If we've got a stats message ready to send, then see if we can piggyback them together @@ -163,12 +160,15 @@ int OctreeSendThread::handlePacketSend(SharedNodePointer node, OctreeQueryNode* // copy octree message to back of stats message statsPacket.write(nodeData->getPacket().getData(), nodeData->getPacket().getDataSize()); - // since a stats message is only included on end of scene, don't consider any of these bytes "wasted", since + int numBytes = statsPacket.getDataSize(); + _totalBytes += numBytes; + _totalPackets++; + // since a stats message is only included on end of scene, don't consider any of these bytes "wasted" // there was nothing else to send. int thisWastedBytes = 0; - _totalWastedBytes += thisWastedBytes; - _totalBytes += statsPacket.getDataSize(); - _totalPackets++; + //_totalWastedBytes += 0; + _trueBytesSent += numBytes; + numPackets++; if (debug) { NLPacket& sentPacket = nodeData->getPacket(); @@ -191,18 +191,22 @@ int OctreeSendThread::handlePacketSend(SharedNodePointer node, OctreeQueryNode* // actually send it OctreeServer::didCallWriteDatagram(this); DependencyManager::get()->sendUnreliablePacket(statsPacket, *node); - packetSent = true; } else { // not enough room in the packet, send two packets + + // first packet OctreeServer::didCallWriteDatagram(this); DependencyManager::get()->sendUnreliablePacket(statsPacket, *node); - // since a stats message is only included on end of scene, don't consider any of these bytes "wasted", since + int numBytes = statsPacket.getDataSize(); + _totalBytes += numBytes; + _totalPackets++; + // since a stats message is only included on end of scene, don't consider any of these bytes "wasted" // there was nothing else to send. int thisWastedBytes = 0; - _totalWastedBytes += thisWastedBytes; - _totalBytes += statsPacket.getDataSize(); - _totalPackets++; + //_totalWastedBytes += 0; + _trueBytesSent += numBytes; + numPackets++; if (debug) { NLPacket& sentPacket = nodeData->getPacket(); @@ -221,19 +225,18 @@ int OctreeSendThread::handlePacketSend(SharedNodePointer node, OctreeQueryNode* "] wasted bytes:" << thisWastedBytes << " [" << _totalWastedBytes << "]"; } - trueBytesSent += statsPacket.getDataSize(); - truePacketsSent++; - packetsSent++; - + // second packet OctreeServer::didCallWriteDatagram(this); DependencyManager::get()->sendUnreliablePacket(nodeData->getPacket(), *node); - packetSent = true; - int packetSizeWithHeader = nodeData->getPacket().getDataSize(); - thisWastedBytes = udt::MAX_PACKET_SIZE - packetSizeWithHeader; - _totalWastedBytes += thisWastedBytes; - _totalBytes += nodeData->getPacket().getDataSize(); + numBytes = nodeData->getPacket().getDataSize(); + _totalBytes += numBytes; _totalPackets++; + // we count wasted bytes here because we were unable to fit the stats packet + thisWastedBytes = udt::MAX_PACKET_SIZE - numBytes; + _totalWastedBytes += thisWastedBytes; + _trueBytesSent += numBytes; + numPackets++; if (debug) { NLPacket& sentPacket = nodeData->getPacket(); @@ -259,13 +262,14 @@ int OctreeSendThread::handlePacketSend(SharedNodePointer node, OctreeQueryNode* // just send the octree packet OctreeServer::didCallWriteDatagram(this); DependencyManager::get()->sendUnreliablePacket(nodeData->getPacket(), *node); - packetSent = true; - int packetSizeWithHeader = nodeData->getPacket().getDataSize(); - int thisWastedBytes = udt::MAX_PACKET_SIZE - packetSizeWithHeader; - _totalWastedBytes += thisWastedBytes; - _totalBytes += packetSizeWithHeader; + int numBytes = nodeData->getPacket().getDataSize(); + _totalBytes += numBytes; _totalPackets++; + int thisWastedBytes = udt::MAX_PACKET_SIZE - numBytes; + _totalWastedBytes += thisWastedBytes; + numPackets++; + _trueBytesSent += numBytes; if (debug) { NLPacket& sentPacket = nodeData->getPacket(); @@ -280,23 +284,21 @@ int OctreeSendThread::handlePacketSend(SharedNodePointer node, OctreeQueryNode* qDebug() << "Sending packet at " << now << " [" << _totalPackets <<"]: sequence: " << sequence << " timestamp: " << timestamp << - " size: " << packetSizeWithHeader << " [" << _totalBytes << + " size: " << numBytes << " [" << _totalBytes << "] wasted bytes:" << thisWastedBytes << " [" << _totalWastedBytes << "]"; } } } // remember to track our stats - if (packetSent) { + if (numPackets > 0) { nodeData->stats.packetSent(nodeData->getPacket().getPayloadSize()); - trueBytesSent += nodeData->getPacket().getPayloadSize(); - truePacketsSent++; - packetsSent++; nodeData->octreePacketSent(); nodeData->resetOctreePacket(); } - return packetsSent; + _truePacketsSent += numPackets; + return numPackets; } /// Version of octree element distributor that sends the deepest LOD level at once @@ -315,13 +317,9 @@ int OctreeSendThread::packetDistributor(SharedNodePointer node, OctreeQueryNode* preDistributionProcessing(); } - // calculate max number of packets that can be sent during this interval - int clientMaxPacketsPerInterval = std::max(1, (nodeData->getMaxQueryPacketsPerSecond() / INTERVALS_PER_SECOND)); - int maxPacketsPerInterval = std::min(clientMaxPacketsPerInterval, _myServer->getPacketsPerClientPerInterval()); - - int truePacketsSent = 0; - int trueBytesSent = 0; - int packetsSentThisInterval = 0; + _truePacketsSent = 0; + _trueBytesSent = 0; + _packetsSentThisInterval = 0; bool isFullScene = nodeData->shouldForceFullScene(); if (isFullScene) { @@ -334,17 +332,9 @@ int OctreeSendThread::packetDistributor(SharedNodePointer node, OctreeQueryNode* && ((!viewFrustumChanged && nodeData->getViewFrustumJustStoppedChanging()) || nodeData->hasLodChanged())); } - bool somethingToSend = true; // assume we have something - - // If our packet already has content in it, then we must use the color choice of the waiting packet. - // If we're starting a fresh packet, then... - // If we're moving, and the client asked for low res, then we force monochrome, otherwise, use - // the clients requested color state. - - // If we have a packet waiting, and our desired want color, doesn't match the current waiting packets color - // then let's just send that waiting packet. if (nodeData->isPacketWaiting()) { - packetsSentThisInterval += handlePacketSend(node, nodeData, trueBytesSent, truePacketsSent); + // send the waiting packet + _packetsSentThisInterval += handlePacketSend(node, nodeData); } else { nodeData->resetOctreePacket(); } @@ -375,8 +365,7 @@ int OctreeSendThread::packetDistributor(SharedNodePointer node, OctreeQueryNode* //unsigned long encodeTime = nodeData->stats.getTotalEncodeTime(); //unsigned long elapsedTime = nodeData->stats.getElapsedTime(); - int packetsJustSent = handlePacketSend(node, nodeData, trueBytesSent, truePacketsSent, isFullScene); - packetsSentThisInterval += packetsJustSent; + _packetsSentThisInterval += handlePacketSend(node, nodeData, isFullScene); // If we're starting a full scene, then definitely we want to empty the elementBag if (isFullScene) { @@ -404,185 +393,44 @@ int OctreeSendThread::packetDistributor(SharedNodePointer node, OctreeQueryNode* // If we have something in our elementBag, then turn them into packets and send them out... if (!nodeData->elementBag.isEmpty()) { - int bytesWritten = 0; quint64 start = usecTimestampNow(); - // TODO: add these to stats page - //quint64 startCompressTimeMsecs = OctreePacketData::getCompressContentTime() / 1000; - //quint64 startCompressCalls = OctreePacketData::getCompressContentCalls(); - - int extraPackingAttempts = 0; - bool completedScene = false; - - while (somethingToSend && packetsSentThisInterval < maxPacketsPerInterval && !nodeData->isShuttingDown()) { - float lockWaitElapsedUsec = OctreeServer::SKIP_TIME; - float encodeElapsedUsec = OctreeServer::SKIP_TIME; - float compressAndWriteElapsedUsec = OctreeServer::SKIP_TIME; - float packetSendingElapsedUsec = OctreeServer::SKIP_TIME; - - quint64 startInside = usecTimestampNow(); - - bool lastNodeDidntFit = false; // assume each node fits - if (!nodeData->elementBag.isEmpty()) { - - quint64 lockWaitStart = usecTimestampNow(); - _myServer->getOctree()->withReadLock([&]{ - quint64 lockWaitEnd = usecTimestampNow(); - lockWaitElapsedUsec = (float)(lockWaitEnd - lockWaitStart); - quint64 encodeStart = usecTimestampNow(); - - OctreeElementPointer subTree = nodeData->elementBag.extract(); - if (!subTree) { - return; - } - - float octreeSizeScale = nodeData->getOctreeSizeScale(); - int boundaryLevelAdjustClient = nodeData->getBoundaryLevelAdjust(); - - int boundaryLevelAdjust = boundaryLevelAdjustClient + - (viewFrustumChanged ? LOW_RES_MOVING_ADJUST : NO_BOUNDARY_ADJUST); - - EncodeBitstreamParams params(INT_MAX, WANT_EXISTS_BITS, DONT_CHOP, - viewFrustumChanged, boundaryLevelAdjust, octreeSizeScale, - isFullScene, _myServer->getJurisdiction(), nodeData); - nodeData->copyCurrentViewFrustum(params.viewFrustum); - if (viewFrustumChanged) { - nodeData->copyLastKnownViewFrustum(params.lastViewFrustum); - } - - // Our trackSend() function is implemented by the server subclass, and will be called back - // during the encodeTreeBitstream() as new entities/data elements are sent - params.trackSend = [this, node](const QUuid& dataID, quint64 dataEdited) { - _myServer->trackSend(dataID, dataEdited, node->getUUID()); - }; - - // TODO: should this include the lock time or not? This stat is sent down to the client, - // it seems like it may be a good idea to include the lock time as part of the encode time - // are reported to client. Since you can encode without the lock - nodeData->stats.encodeStarted(); - - bytesWritten = _myServer->getOctree()->encodeTreeBitstream(subTree, &_packetData, nodeData->elementBag, params); - - quint64 encodeEnd = usecTimestampNow(); - encodeElapsedUsec = (float)(encodeEnd - encodeStart); - - // If after calling encodeTreeBitstream() there are no nodes left to send, then we know we've - // sent the entire scene. We want to know this below so we'll actually write this content into - // the packet and send it - completedScene = nodeData->elementBag.isEmpty(); - - if (params.stopReason == EncodeBitstreamParams::DIDNT_FIT) { - lastNodeDidntFit = true; - extraPackingAttempts++; - } - - nodeData->stats.encodeStopped(); - }); - } else { - // If the bag was empty then we didn't even attempt to encode, and so we know the bytesWritten were 0 - bytesWritten = 0; - somethingToSend = false; // this will cause us to drop out of the loop... - } - - // If the last node didn't fit, but we're in compressed mode, then we actually want to see if we can fit a - // little bit more in this packet. To do this we write into the packet, but don't send it yet, we'll - // keep attempting to write in compressed mode to add more compressed segments - - // We only consider sending anything if there is something in the _packetData to send... But - // if bytesWritten == 0 it means either the subTree couldn't fit or we had an empty bag... Both cases - // mean we should send the previous packet contents and reset it. - if (completedScene || lastNodeDidntFit) { - - if (_packetData.hasContent()) { - quint64 compressAndWriteStart = usecTimestampNow(); - - // if for some reason the finalized size is greater than our available size, then probably the "compressed" - // form actually inflated beyond our padding, and in this case we will send the current packet, then - // write to out new packet... - unsigned int writtenSize = _packetData.getFinalizedSize() + sizeof(OCTREE_PACKET_INTERNAL_SECTION_SIZE); - - if (writtenSize > nodeData->getAvailable()) { - packetsSentThisInterval += handlePacketSend(node, nodeData, trueBytesSent, truePacketsSent); - } - - nodeData->writeToPacket(_packetData.getFinalizedData(), _packetData.getFinalizedSize()); - quint64 compressAndWriteEnd = usecTimestampNow(); - compressAndWriteElapsedUsec = (float)(compressAndWriteEnd - compressAndWriteStart); - } - - // If we're not running compressed, then we know we can just send now. Or if we're running compressed, but - // the packet doesn't have enough space to bother attempting to pack more... - bool sendNow = true; - - if (!completedScene && (nodeData->getAvailable() >= MINIMUM_ATTEMPT_MORE_PACKING && - extraPackingAttempts <= REASONABLE_NUMBER_OF_PACKING_ATTEMPTS)) { - sendNow = false; // try to pack more - } - - int targetSize = MAX_OCTREE_PACKET_DATA_SIZE; - if (sendNow) { - quint64 packetSendingStart = usecTimestampNow(); - packetsSentThisInterval += handlePacketSend(node, nodeData, trueBytesSent, truePacketsSent); - quint64 packetSendingEnd = usecTimestampNow(); - packetSendingElapsedUsec = (float)(packetSendingEnd - packetSendingStart); - - targetSize = nodeData->getAvailable() - sizeof(OCTREE_PACKET_INTERNAL_SECTION_SIZE); - extraPackingAttempts = 0; - } else { - // If we're in compressed mode, then we want to see if we have room for more in this wire packet. - // but we've finalized the _packetData, so we want to start a new section, we will do that by - // resetting the packet settings with the max uncompressed size of our current available space - // in the wire packet. We also include room for our section header, and a little bit of padding - // to account for the fact that whenc compressing small amounts of data, we sometimes end up with - // a larger compressed size then uncompressed size - targetSize = nodeData->getAvailable() - sizeof(OCTREE_PACKET_INTERNAL_SECTION_SIZE) - COMPRESS_PADDING; - } - _packetData.changeSettings(true, targetSize); // will do reset - NOTE: Always compressed - - } - OctreeServer::trackTreeWaitTime(lockWaitElapsedUsec); - OctreeServer::trackEncodeTime(encodeElapsedUsec); - OctreeServer::trackCompressAndWriteTime(compressAndWriteElapsedUsec); - OctreeServer::trackPacketSendingTime(packetSendingElapsedUsec); - - quint64 endInside = usecTimestampNow(); - quint64 elapsedInsideUsecs = endInside - startInside; - OctreeServer::trackInsideTime((float)elapsedInsideUsecs); - } - - if (somethingToSend && _myServer->wantsVerboseDebug()) { - qCDebug(octree) << "Hit PPS Limit, packetsSentThisInterval =" << packetsSentThisInterval - << " maxPacketsPerInterval = " << maxPacketsPerInterval - << " clientMaxPacketsPerInterval = " << clientMaxPacketsPerInterval; - } + traverseTreeAndSendContents(node, nodeData, viewFrustumChanged, isFullScene); // Here's where we can/should allow the server to send other data... // send the environment packet // TODO: should we turn this into a while loop to better handle sending multiple special packets if (_myServer->hasSpecialPacketsToSend(node) && !nodeData->isShuttingDown()) { int specialPacketsSent = 0; - trueBytesSent += _myServer->sendSpecialPackets(node, nodeData, specialPacketsSent); + int specialBytesSent = _myServer->sendSpecialPackets(node, nodeData, specialPacketsSent); nodeData->resetOctreePacket(); // because nodeData's _sequenceNumber has changed - truePacketsSent += specialPacketsSent; - packetsSentThisInterval += specialPacketsSent; + _truePacketsSent += specialPacketsSent; + _trueBytesSent += specialBytesSent; + _packetsSentThisInterval += specialPacketsSent; _totalPackets += specialPacketsSent; - _totalBytes += trueBytesSent; + _totalBytes += specialBytesSent; _totalSpecialPackets += specialPacketsSent; - _totalSpecialBytes += trueBytesSent; + _totalSpecialBytes += specialBytesSent; } + // calculate max number of packets that can be sent during this interval + int clientMaxPacketsPerInterval = std::max(1, (nodeData->getMaxQueryPacketsPerSecond() / INTERVALS_PER_SECOND)); + int maxPacketsPerInterval = std::min(clientMaxPacketsPerInterval, _myServer->getPacketsPerClientPerInterval()); + // Re-send packets that were nacked by the client - while (nodeData->hasNextNackedPacket() && packetsSentThisInterval < maxPacketsPerInterval) { + while (nodeData->hasNextNackedPacket() && _packetsSentThisInterval < maxPacketsPerInterval) { const NLPacket* packet = nodeData->getNextNackedPacket(); if (packet) { DependencyManager::get()->sendUnreliablePacket(*packet, *node); - truePacketsSent++; - packetsSentThisInterval++; + int numBytes = packet->getDataSize(); + _truePacketsSent++; + _trueBytesSent += numBytes; + _packetsSentThisInterval++; - _totalBytes += packet->getDataSize(); _totalPackets++; + _totalBytes += numBytes; _totalWastedBytes += udt::MAX_PACKET_SIZE - packet->getDataSize(); } } @@ -591,12 +439,6 @@ int OctreeSendThread::packetDistributor(SharedNodePointer node, OctreeQueryNode* int elapsedmsec = (end - start) / USECS_PER_MSEC; OctreeServer::trackLoopTime(elapsedmsec); - // TODO: add these to stats page - //quint64 endCompressCalls = OctreePacketData::getCompressContentCalls(); - //int elapsedCompressCalls = endCompressCalls - startCompressCalls; - //quint64 endCompressTimeMsecs = OctreePacketData::getCompressContentTime() / 1000; - //int elapsedCompressTimeMsecs = endCompressTimeMsecs - startCompressTimeMsecs; - // if after sending packets we've emptied our bag, then we want to remember that we've sent all // the octree elements from the current view frustum if (nodeData->elementBag.isEmpty()) { @@ -606,17 +448,147 @@ int OctreeSendThread::packetDistributor(SharedNodePointer node, OctreeQueryNode* // If this was a full scene then make sure we really send out a stats packet at this point so that // the clients will know the scene is stable if (isFullScene) { - int thisTrueBytesSent = 0; - int thisTruePacketsSent = 0; nodeData->stats.sceneCompleted(); - int packetsJustSent = handlePacketSend(node, nodeData, thisTrueBytesSent, thisTruePacketsSent, true); - _totalBytes += thisTrueBytesSent; - _totalPackets += thisTruePacketsSent; - truePacketsSent += packetsJustSent; + handlePacketSend(node, nodeData, true); } } } // end if bag wasn't empty, and so we sent stuff... - return truePacketsSent; + return _truePacketsSent; +} + +void OctreeSendThread::traverseTreeAndSendContents(SharedNodePointer node, OctreeQueryNode* nodeData, bool viewFrustumChanged, bool isFullScene) { + // calculate max number of packets that can be sent during this interval + int clientMaxPacketsPerInterval = std::max(1, (nodeData->getMaxQueryPacketsPerSecond() / INTERVALS_PER_SECOND)); + int maxPacketsPerInterval = std::min(clientMaxPacketsPerInterval, _myServer->getPacketsPerClientPerInterval()); + + int extraPackingAttempts = 0; + bool completedScene = false; + + bool somethingToSend = true; // assume we have something + while (somethingToSend && _packetsSentThisInterval < maxPacketsPerInterval && !nodeData->isShuttingDown()) { + float lockWaitElapsedUsec = OctreeServer::SKIP_TIME; + float encodeElapsedUsec = OctreeServer::SKIP_TIME; + float compressAndWriteElapsedUsec = OctreeServer::SKIP_TIME; + float packetSendingElapsedUsec = OctreeServer::SKIP_TIME; + + quint64 startInside = usecTimestampNow(); + + bool lastNodeDidntFit = false; // assume each node fits + if (!nodeData->elementBag.isEmpty()) { + + quint64 lockWaitStart = usecTimestampNow(); + _myServer->getOctree()->withReadLock([&]{ + quint64 lockWaitEnd = usecTimestampNow(); + lockWaitElapsedUsec = (float)(lockWaitEnd - lockWaitStart); + quint64 encodeStart = usecTimestampNow(); + + OctreeElementPointer subTree = nodeData->elementBag.extract(); + if (!subTree) { + return; + } + + float octreeSizeScale = nodeData->getOctreeSizeScale(); + int boundaryLevelAdjustClient = nodeData->getBoundaryLevelAdjust(); + + int boundaryLevelAdjust = boundaryLevelAdjustClient + + (viewFrustumChanged ? LOW_RES_MOVING_ADJUST : NO_BOUNDARY_ADJUST); + + EncodeBitstreamParams params(INT_MAX, WANT_EXISTS_BITS, DONT_CHOP, + viewFrustumChanged, boundaryLevelAdjust, octreeSizeScale, + isFullScene, _myServer->getJurisdiction(), nodeData); + nodeData->copyCurrentViewFrustum(params.viewFrustum); + if (viewFrustumChanged) { + nodeData->copyLastKnownViewFrustum(params.lastViewFrustum); + } + + // Our trackSend() function is implemented by the server subclass, and will be called back + // during the encodeTreeBitstream() as new entities/data elements are sent + params.trackSend = [this](const QUuid& dataID, quint64 dataEdited) { + _myServer->trackSend(dataID, dataEdited, _nodeUuid); + }; + + // TODO: should this include the lock time or not? This stat is sent down to the client, + // it seems like it may be a good idea to include the lock time as part of the encode time + // are reported to client. Since you can encode without the lock + nodeData->stats.encodeStarted(); + + // NOTE: this is where the tree "contents" are actaully packed + _myServer->getOctree()->encodeTreeBitstream(subTree, &_packetData, nodeData->elementBag, params); + + quint64 encodeEnd = usecTimestampNow(); + encodeElapsedUsec = (float)(encodeEnd - encodeStart); + + // If after calling encodeTreeBitstream() there are no nodes left to send, then we know we've + // sent the entire scene. We want to know this below so we'll actually write this content into + // the packet and send it + completedScene = nodeData->elementBag.isEmpty(); + + if (params.stopReason == EncodeBitstreamParams::DIDNT_FIT) { + lastNodeDidntFit = true; + extraPackingAttempts++; + } + + nodeData->stats.encodeStopped(); + }); + } else { + somethingToSend = false; // this will cause us to drop out of the loop... + } + + if (completedScene || lastNodeDidntFit) { + // we probably want to flush what has accumulated in nodeData but: + // do we have more data to send? and is there room? + if (_packetData.hasContent()) { + // yes, more data to send + quint64 compressAndWriteStart = usecTimestampNow(); + unsigned int additionalSize = _packetData.getFinalizedSize() + sizeof(OCTREE_PACKET_INTERNAL_SECTION_SIZE); + if (additionalSize > nodeData->getAvailable()) { + // no room --> flush what we've got + _packetsSentThisInterval += handlePacketSend(node, nodeData); + } + + // either there is room, or we've flushed and reset nodeData's data buffer + // so we can transfer whatever is in _packetData to nodeData + nodeData->writeToPacket(_packetData.getFinalizedData(), _packetData.getFinalizedSize()); + compressAndWriteElapsedUsec = (float)(usecTimestampNow()- compressAndWriteStart); + } + + bool sendNow = completedScene || + nodeData->getAvailable() < MINIMUM_ATTEMPT_MORE_PACKING || + extraPackingAttempts > REASONABLE_NUMBER_OF_PACKING_ATTEMPTS; + + int targetSize = MAX_OCTREE_PACKET_DATA_SIZE; + if (sendNow) { + quint64 packetSendingStart = usecTimestampNow(); + _packetsSentThisInterval += handlePacketSend(node, nodeData); + quint64 packetSendingEnd = usecTimestampNow(); + packetSendingElapsedUsec = (float)(packetSendingEnd - packetSendingStart); + + targetSize = nodeData->getAvailable() - sizeof(OCTREE_PACKET_INTERNAL_SECTION_SIZE); + extraPackingAttempts = 0; + } else { + // We want to see if we have room for more in this wire packet but we've copied the _packetData, + // so we want to start a new section. We will do that by resetting the packet settings with the max + // size of our current available space in the wire packet plus room for our section header and a + // little bit of padding. + targetSize = nodeData->getAvailable() - sizeof(OCTREE_PACKET_INTERNAL_SECTION_SIZE) - COMPRESS_PADDING; + } + _packetData.changeSettings(true, targetSize); // will do reset - NOTE: Always compressed + } + OctreeServer::trackTreeWaitTime(lockWaitElapsedUsec); + OctreeServer::trackEncodeTime(encodeElapsedUsec); + OctreeServer::trackCompressAndWriteTime(compressAndWriteElapsedUsec); + OctreeServer::trackPacketSendingTime(packetSendingElapsedUsec); + + quint64 endInside = usecTimestampNow(); + quint64 elapsedInsideUsecs = endInside - startInside; + OctreeServer::trackInsideTime((float)elapsedInsideUsecs); + } + + if (somethingToSend && _myServer->wantsVerboseDebug()) { + qCDebug(octree) << "Hit PPS Limit, packetsSentThisInterval =" << _packetsSentThisInterval + << " maxPacketsPerInterval = " << maxPacketsPerInterval + << " clientMaxPacketsPerInterval = " << clientMaxPacketsPerInterval; + } } diff --git a/assignment-client/src/octree/OctreeSendThread.h b/assignment-client/src/octree/OctreeSendThread.h index 06c9b5f1d6..d158539f57 100644 --- a/assignment-client/src/octree/OctreeSendThread.h +++ b/assignment-client/src/octree/OctreeSendThread.h @@ -34,7 +34,7 @@ public: void setIsShuttingDown(); bool isShuttingDown() { return _isShuttingDown; } - + QUuid getNodeUuid() const { return _nodeUuid; } static AtomicUIntStat _totalBytes; @@ -53,20 +53,23 @@ protected: /// Called before a packetDistributor pass to allow for pre-distribution processing virtual void preDistributionProcessing() {}; + virtual void traverseTreeAndSendContents(SharedNodePointer node, OctreeQueryNode* nodeData, bool viewFrustumChanged, bool isFullScene); OctreeServer* _myServer { nullptr }; QWeakPointer _node; private: - int handlePacketSend(SharedNodePointer node, OctreeQueryNode* nodeData, int& trueBytesSent, int& truePacketsSent, bool dontSuppressDuplicate = false); + int handlePacketSend(SharedNodePointer node, OctreeQueryNode* nodeData, bool dontSuppressDuplicate = false); int packetDistributor(SharedNodePointer node, OctreeQueryNode* nodeData, bool viewFrustumChanged); - + QUuid _nodeUuid; OctreePacketData _packetData; - int _nodeMissingCount { 0 }; + int _truePacketsSent { 0 }; // available for debug stats + int _trueBytesSent { 0 }; // available for debug stats + int _packetsSentThisInterval { 0 }; // used for bandwidth throttle condition bool _isShuttingDown { false }; }; diff --git a/assignment-client/src/scripts/EntityScriptServer.cpp b/assignment-client/src/scripts/EntityScriptServer.cpp index 489478ff9a..e7433e7c05 100644 --- a/assignment-client/src/scripts/EntityScriptServer.cpp +++ b/assignment-client/src/scripts/EntityScriptServer.cpp @@ -16,6 +16,7 @@ #include #include #include +#include #include #include #include @@ -67,6 +68,9 @@ EntityScriptServer::EntityScriptServer(ReceivedMessage& message) : ThreadedAssig DependencyManager::set(); DependencyManager::set(ScriptEngine::ENTITY_SERVER_SCRIPT); + // Needed to ensure the creation of the DebugDraw instance on the main thread + DebugDraw::getInstance(); + auto& packetReceiver = DependencyManager::get()->getPacketReceiver(); packetReceiver.registerListenerForTypes({ PacketType::OctreeStats, PacketType::EntityData, PacketType::EntityErase }, this, "handleOctreePacket"); diff --git a/cmake/externals/quazip/CMakeLists.txt b/cmake/externals/quazip/CMakeLists.txt index 7af13dafa7..01650a432d 100644 --- a/cmake/externals/quazip/CMakeLists.txt +++ b/cmake/externals/quazip/CMakeLists.txt @@ -21,8 +21,8 @@ endif () ExternalProject_Add( ${EXTERNAL_NAME} - URL https://s3-us-west-1.amazonaws.com/hifi-production/dependencies/quazip-0.7.2.zip - URL_MD5 2955176048a31262c09259ca8d309d19 + URL https://hifi-public.s3.amazonaws.com/dependencies/quazip-0.7.3.zip + URL_MD5 ed03754d39b9da1775771819b8001d45 BINARY_DIR ${EXTERNAL_PROJECT_PREFIX}/build CMAKE_ARGS ${QUAZIP_CMAKE_ARGS} LOG_DOWNLOAD 1 diff --git a/cmake/externals/wasapi/CMakeLists.txt b/cmake/externals/wasapi/CMakeLists.txt index 1bf195fc84..4437024962 100644 --- a/cmake/externals/wasapi/CMakeLists.txt +++ b/cmake/externals/wasapi/CMakeLists.txt @@ -6,8 +6,8 @@ if (WIN32) include(ExternalProject) ExternalProject_Add( ${EXTERNAL_NAME} - URL http://hifi-public.s3.amazonaws.com/dependencies/qtaudio_wasapi8.zip - URL_MD5 b01510437ea15527156bc25cdf733bd9 + URL http://hifi-public.s3.amazonaws.com/dependencies/qtaudio_wasapi9.zip + URL_MD5 94f4765bdbcd53cd099f349ae031e769 CONFIGURE_COMMAND "" BUILD_COMMAND "" INSTALL_COMMAND "" diff --git a/cmake/macros/GenerateInstallers.cmake b/cmake/macros/GenerateInstallers.cmake index 0def701739..6c131168d5 100644 --- a/cmake/macros/GenerateInstallers.cmake +++ b/cmake/macros/GenerateInstallers.cmake @@ -22,23 +22,17 @@ macro(GENERATE_INSTALLERS) set(CPACK_PACKAGE_FILE_NAME "HighFidelity-Beta-${BUILD_VERSION}") set(CPACK_NSIS_DISPLAY_NAME ${_DISPLAY_NAME}) set(CPACK_NSIS_PACKAGE_NAME ${_DISPLAY_NAME}) + if (PR_BUILD) + set(CPACK_NSIS_COMPRESSOR "/SOLID bzip2") + endif () set(CPACK_PACKAGE_INSTALL_DIRECTORY ${_DISPLAY_NAME}) + if (WIN32) - # include CMake module that will install compiler system libraries - # so that we have msvcr120 and msvcp120 installed with targets - set(CMAKE_INSTALL_SYSTEM_RUNTIME_DESTINATION ${INTERFACE_INSTALL_DIR}) - - # as long as we're including sixense plugin with installer - # we need re-distributables for VS 2011 as well - # this should be removed if/when sixense support is pulled - set(CMAKE_INSTALL_SYSTEM_RUNTIME_LIBS - "${EXTERNALS_BINARY_DIR}/sixense/project/src/sixense/samples/win64/msvcr100.dll" - "${EXTERNALS_BINARY_DIR}/sixense/project/src/sixense/samples/win64/msvcp100.dll" - ) - + # Do not install the Visual Studio C runtime libraries. The installer will do this automatically + set(CMAKE_INSTALL_SYSTEM_RUNTIME_LIBS_SKIP TRUE) + include(InstallRequiredSystemLibraries) - set(CPACK_NSIS_MUI_ICON "${HF_CMAKE_DIR}/installer/installer.ico") # install and reference the Add/Remove icon @@ -90,3 +84,4 @@ macro(GENERATE_INSTALLERS) include(CPack) endmacro() + diff --git a/cmake/macros/InstallBesideConsole.cmake b/cmake/macros/InstallBesideConsole.cmake index d5777fff12..3c991acf86 100644 --- a/cmake/macros/InstallBesideConsole.cmake +++ b/cmake/macros/InstallBesideConsole.cmake @@ -22,9 +22,12 @@ macro(install_beside_console) else () # setup install of executable and things copied by fixup/windeployqt install( - FILES "$/" + DIRECTORY "$/" DESTINATION ${COMPONENT_INSTALL_DIR} COMPONENT ${SERVER_COMPONENT} + PATTERN "*.pdb" EXCLUDE + PATTERN "*.lib" EXCLUDE + PATTERN "*.exp" EXCLUDE ) # on windows for PR and production builds, sign the executable diff --git a/cmake/macros/SetPackagingParameters.cmake b/cmake/macros/SetPackagingParameters.cmake index 82a4a7d080..6f35b76f1d 100644 --- a/cmake/macros/SetPackagingParameters.cmake +++ b/cmake/macros/SetPackagingParameters.cmake @@ -126,8 +126,14 @@ macro(SET_PACKAGING_PARAMETERS) # check if we need to find signtool if (PRODUCTION_BUILD OR PR_BUILD) - find_program(SIGNTOOL_EXECUTABLE signtool PATHS "C:/Program Files (x86)/Windows Kits/8.1" PATH_SUFFIXES "bin/x64") - + if (MSVC_VERSION GREATER_EQUAL 1910) # VS 2017 + find_program(SIGNTOOL_EXECUTABLE signtool PATHS "C:/Program Files (x86)/Windows Kits/10" PATH_SUFFIXES "bin/${CMAKE_VS_WINDOWS_TARGET_PLATFORM_VERSION}/x64") + elseif (MSVC_VERSION GREATER_EQUAL 1800) # VS 2013 + find_program(SIGNTOOL_EXECUTABLE signtool PATHS "C:/Program Files (x86)/Windows Kits/8.1" PATH_SUFFIXES "bin/x64") + else() + message( FATAL_ERROR "Visual Studio 2013 or higher required." ) + endif() + if (NOT SIGNTOOL_EXECUTABLE) message(FATAL_ERROR "Code signing of executables was requested but signtool.exe could not be found.") endif () diff --git a/cmake/templates/FixupBundlePostBuild.cmake.in b/cmake/templates/FixupBundlePostBuild.cmake.in index 57d1fd787f..d4726884c2 100644 --- a/cmake/templates/FixupBundlePostBuild.cmake.in +++ b/cmake/templates/FixupBundlePostBuild.cmake.in @@ -11,34 +11,28 @@ include(BundleUtilities) -# replace copy_resolved_item_into_bundle -# -# The official version of copy_resolved_item_into_bundle will print out a "warning:" when -# the resolved item matches the resolved embedded item. This not not really an issue that -# should rise to the level of a "warning" so we replace this message with a "status:" -# -function(copy_resolved_item_into_bundle resolved_item resolved_embedded_item) - if (WIN32) - # ignore case on Windows - string(TOLOWER "${resolved_item}" resolved_item_compare) - string(TOLOWER "${resolved_embedded_item}" resolved_embedded_item_compare) - else() - set(resolved_item_compare "${resolved_item}") - set(resolved_embedded_item_compare "${resolved_embedded_item}") +function(gp_resolved_file_type_override resolved_file type_var) + if( file MATCHES ".*VCRUNTIME140.*" ) + set(type "system" PARENT_SCOPE) endif() - - if ("${resolved_item_compare}" STREQUAL "${resolved_embedded_item_compare}") - # this is our only change from the original version - message(STATUS "status: resolved_item == resolved_embedded_item - not copying...") - else() - #message(STATUS "copying COMMAND ${CMAKE_COMMAND} -E copy ${resolved_item} ${resolved_embedded_item}") - execute_process(COMMAND ${CMAKE_COMMAND} -E copy "${resolved_item}" "${resolved_embedded_item}") - if(UNIX AND NOT APPLE) - file(RPATH_REMOVE FILE "${resolved_embedded_item}") - endif() + if( file MATCHES ".*concrt140.*" ) + set(type "system" PARENT_SCOPE) + endif() + if( file MATCHES ".*msvcp140.*" ) + set(type "system" PARENT_SCOPE) + endif() + if( file MATCHES ".*vcruntime140.*" ) + set(type "system" PARENT_SCOPE) + endif() + if( file MATCHES ".*api-ms-win-crt-conio.*" ) + set(type "system" PARENT_SCOPE) + endif() + if( file MATCHES ".*api-ms-win-core-winrt.*" ) + set(type "system" PARENT_SCOPE) endif() endfunction() + message(STATUS "FIXUP_LIBS for fixup_bundle called for bundle ${BUNDLE_EXECUTABLE} are @FIXUP_LIBS@") message(STATUS "Scanning for plugins from ${BUNDLE_PLUGIN_DIR}") @@ -52,3 +46,4 @@ endif() file(GLOB EXTRA_PLUGINS "${BUNDLE_PLUGIN_DIR}/*.${PLUGIN_EXTENSION}") fixup_bundle("${BUNDLE_EXECUTABLE}" "${EXTRA_PLUGINS}" "@FIXUP_LIBS@") + diff --git a/cmake/templates/NSIS.template.in b/cmake/templates/NSIS.template.in index 710fd81316..5af51ff8c9 100644 --- a/cmake/templates/NSIS.template.in +++ b/cmake/templates/NSIS.template.in @@ -49,7 +49,7 @@ Var STR_CONTAINS_VAR_3 Var STR_CONTAINS_VAR_4 Var STR_RETURN_VAR - + Function StrContains Exch $STR_NEEDLE Exch 1 @@ -343,22 +343,29 @@ SectionEnd ;-------------------------------- ;Pages !insertmacro MUI_PAGE_WELCOME - + !insertmacro MUI_PAGE_LICENSE "@CPACK_RESOURCE_FILE_LICENSE@" + + Page custom InstallTypesPage ReadInstallTypes + + !define MUI_PAGE_CUSTOMFUNCTION_PRE AbortFunction !insertmacro MUI_PAGE_DIRECTORY - + ;Start Menu Folder Page Configuration !define MUI_STARTMENUPAGE_REGISTRY_ROOT "HKLM" !define MUI_STARTMENUPAGE_REGISTRY_KEY "Software\@CPACK_PACKAGE_VENDOR@\@CPACK_PACKAGE_INSTALL_REGISTRY_KEY@" !define MUI_STARTMENUPAGE_REGISTRY_VALUENAME "Start Menu Folder" + + !define MUI_PAGE_CUSTOMFUNCTION_PRE AbortFunction !insertmacro MUI_PAGE_STARTMENU Application $STARTMENU_FOLDER - + + !define MUI_PAGE_CUSTOMFUNCTION_PRE AbortFunction @CPACK_NSIS_PAGE_COMPONENTS@ - + Page custom PostInstallOptionsPage ReadPostInstallOptions !insertmacro MUI_PAGE_INSTFILES - + !insertmacro MUI_UNPAGE_CONFIRM !insertmacro MUI_UNPAGE_INSTFILES @@ -438,9 +445,14 @@ Var DesktopServerCheckbox Var ServerStartupCheckbox Var LaunchServerNowCheckbox Var LaunchClientNowCheckbox +Var CleanInstallCheckbox Var CurrentOffset Var OffsetUnits Var CopyFromProductionCheckbox +Var ExpressInstallRadioButton +Var CustomInstallRadioButton +Var InstallTypeDialog +Var Express !macro SetPostInstallOption Checkbox OptionName Default ; reads the value for the given post install option to the registry @@ -458,6 +470,60 @@ Var CopyFromProductionCheckbox ${EndIf} !macroend +Function InstallTypesPage + !insertmacro MUI_HEADER_TEXT "Choose Installation Type" "Express or Custom Install" + + nsDialogs::Create 1018 + Pop $InstallTypeDialog + + ${If} $InstallTypeDialog == error + Abort + ${EndIf} + + StrCpy $CurrentOffset 0 + StrCpy $OffsetUnits u + StrCpy $Express "0" + + ${If} ${SectionIsSelected} ${@CLIENT_COMPONENT_NAME@} + ${NSD_CreateRadioButton} 30% $CurrentOffset$OffsetUnits 100% 10u "Express Install (Recommended)"; $\nInstalls High Fidelity Interface and High Fidelity Sandbox" + pop $ExpressInstallRadioButton + ${NSD_OnClick} $ExpressInstallRadioButton ChangeExpressLabel + IntOp $CurrentOffset $CurrentOffset + 15 + + ${NSD_CreateRadiobutton} 30% $CurrentOffset$OffsetUnits 100% 10u "Custom Install (Advanced)" + pop $CustomInstallRadioButton + ${NSD_OnClick} $CustomInstallRadioButton ChangeCustomLabel + ${EndIf} + + ; Express Install selected by default + ${NSD_Check} $ExpressInstallRadioButton + Call ChangeExpressLabel + + nsDialogs::Show +FunctionEnd + +Function ChangeExpressLabel + Push $R1 + GetDlgItem $R1 $HWNDPARENT 1 + SendMessage $R1 ${WM_SETTEXT} 0 "STR:Install" + Pop $R1 +FunctionEnd + +Function ChangeCustomLabel + Push $R1 + GetDlgItem $R1 $HWNDPARENT 1 + SendMessage $R1 ${WM_SETTEXT} 0 "STR:Next >" + Pop $R1 +FunctionEnd + +Function AbortFunction + ; Check if Express is set, if so, abort the post install options page + Call HandleInstallTypes ; Sets Express if ExpressInstallRadioButton is checked and installs with defaults + StrCmp $Express "1" 0 end + Abort + end: +FunctionEnd + Function PostInstallOptionsPage !insertmacro MUI_HEADER_TEXT "Setup Options" "" @@ -475,27 +541,18 @@ Function PostInstallOptionsPage ${NSD_CreateCheckbox} 0 $CurrentOffset$OffsetUnits 100% 10u "&Create a desktop shortcut for @INTERFACE_HF_SHORTCUT_NAME@" Pop $DesktopClientCheckbox IntOp $CurrentOffset $CurrentOffset + 15 - + ; set the checkbox state depending on what is present in the registry !insertmacro SetPostInstallOption $DesktopClientCheckbox @CLIENT_DESKTOP_SHORTCUT_REG_KEY@ ${BST_CHECKED} ${EndIf} - + ${If} ${SectionIsSelected} ${@SERVER_COMPONENT_NAME@} ${NSD_CreateCheckbox} 0 $CurrentOffset$OffsetUnits 100% 10u "&Create a desktop shortcut for @CONSOLE_HF_SHORTCUT_NAME@" Pop $DesktopServerCheckbox - + IntOp $CurrentOffset $CurrentOffset + 15 + ; set the checkbox state depending on what is present in the registry !insertmacro SetPostInstallOption $DesktopServerCheckbox @CONSOLE_DESKTOP_SHORTCUT_REG_KEY@ ${BST_UNCHECKED} - - IntOp $CurrentOffset $CurrentOffset + 15 - - ${NSD_CreateCheckbox} 0 $CurrentOffset$OffsetUnits 100% 10u "&Launch @CONSOLE_HF_SHORTCUT_NAME@ on startup" - Pop $ServerStartupCheckbox - - ; set the checkbox state depending on what is present in the registry - !insertmacro SetPostInstallOption $ServerStartupCheckbox @CONSOLE_STARTUP_REG_KEY@ ${BST_CHECKED} - - IntOp $CurrentOffset $CurrentOffset + 15 ${EndIf} ${If} ${SectionIsSelected} ${@SERVER_COMPONENT_NAME@} @@ -511,17 +568,33 @@ Function PostInstallOptionsPage IntOp $CurrentOffset $CurrentOffset + 15 ${EndIf} - + ${If} ${SectionIsSelected} ${@CLIENT_COMPONENT_NAME@} - ${NSD_CreateCheckbox} 0 $CurrentOffset$OffsetUnits 100% 10u "&Launch @INTERFACE_HF_SHORTCUT_NAME@ after install" - Pop $LaunchClientNowCheckbox - - ; set the checkbox state depending on what is present in the registry - !insertmacro SetPostInstallOption $LaunchClientNowCheckbox @CLIENT_LAUNCH_NOW_REG_KEY@ ${BST_CHECKED} - ${StrContains} $substringResult "/forceNoLaunchClient" $CMDLINE - ${IfNot} $substringResult == "" - ${NSD_SetState} $LaunchClientNowCheckbox ${BST_UNCHECKED} - ${EndIf} + ${NSD_CreateCheckbox} 0 $CurrentOffset$OffsetUnits 100% 10u "&Launch @INTERFACE_HF_SHORTCUT_NAME@ after install" + Pop $LaunchClientNowCheckbox + IntOp $CurrentOffset $CurrentOffset + 30 + + ; set the checkbox state depending on what is present in the registry + !insertmacro SetPostInstallOption $LaunchClientNowCheckbox @CLIENT_LAUNCH_NOW_REG_KEY@ ${BST_CHECKED} + ${StrContains} $substringResult "/forceNoLaunchClient" $CMDLINE + ${IfNot} $substringResult == "" + ${NSD_SetState} $LaunchClientNowCheckbox ${BST_UNCHECKED} + ${EndIf} + ${EndIf} + + ${If} ${SectionIsSelected} ${@SERVER_COMPONENT_NAME@} + ${NSD_CreateCheckbox} 0 $CurrentOffset$OffsetUnits 100% 10u "&Launch @CONSOLE_HF_SHORTCUT_NAME@ on startup" + Pop $ServerStartupCheckbox + IntOp $CurrentOffset $CurrentOffset + 15 + + ; set the checkbox state depending on what is present in the registry + !insertmacro SetPostInstallOption $ServerStartupCheckbox @CONSOLE_STARTUP_REG_KEY@ ${BST_CHECKED} + ${EndIf} + + ${If} ${SectionIsSelected} ${@CLIENT_COMPONENT_NAME@} + ${NSD_CreateCheckbox} 0 $CurrentOffset$OffsetUnits 100% 10u "&Perform a clean install (Delete older settings and content)" + Pop $CleanInstallCheckbox + IntOp $CurrentOffset $CurrentOffset + 15 ${EndIf} ${If} @PR_BUILD@ == 1 @@ -541,9 +614,15 @@ Function PostInstallOptionsPage ${NSD_CreateCheckbox} 0 $CurrentOffset$OffsetUnits 100% 10u "&Copy settings and content from production install" Pop $CopyFromProductionCheckbox - ${NSD_SetState} $CopyFromProductionCheckbox ${BST_CHECKED} + ${NSD_SetState} $CopyFromProductionCheckbox ${BST_UNCHECKED} ${EndIf} - + + ; Check if Express is set, if so, abort the post install options page + Call HandleInstallTypes ; Sets Express if ExpressInstallRadioButton is checked and installs with defaults + StrCmp $Express "1" 0 end + Abort + end: + nsDialogs::Show FunctionEnd @@ -558,6 +637,17 @@ Var ServerStartupState Var LaunchServerNowState Var LaunchClientNowState Var CopyFromProductionState +Var CleanInstallState +Var ExpressInstallState +Var CustomInstallState + +Function ReadInstallTypes + ${If} ${SectionIsSelected} ${@CLIENT_COMPONENT_NAME@} + ; check if the user asked for express/custom install + ${NSD_GetState} $ExpressInstallRadioButton $ExpressInstallState + ${NSD_GetState} $CustomInstallRadioButton $CustomInstallState + ${EndIf} +FunctionEnd Function ReadPostInstallOptions ${If} ${SectionIsSelected} ${@CLIENT_COMPONENT_NAME@} @@ -579,13 +669,40 @@ Function ReadPostInstallOptions ${EndIf} ${If} ${SectionIsSelected} ${@SERVER_COMPONENT_NAME@} - ; check if we need to launch the server post-install - ${NSD_GetState} $LaunchServerNowCheckbox $LaunchServerNowState + ; check if we need to launch the server post-install + ${NSD_GetState} $LaunchServerNowCheckbox $LaunchServerNowState ${EndIf} - + ${If} ${SectionIsSelected} ${@CLIENT_COMPONENT_NAME@} - ; check if we need to launch the client post-install - ${NSD_GetState} $LaunchClientNowCheckbox $LaunchClientNowState + ; check if we need to launch the client post-install + ${NSD_GetState} $LaunchClientNowCheckbox $LaunchClientNowState + ${EndIf} + + ${If} ${SectionIsSelected} ${@CLIENT_COMPONENT_NAME@} + ; check if the user asked for a clean install + ${NSD_GetState} $CleanInstallCheckbox $CleanInstallState + ${EndIf} +FunctionEnd + +Function HandleInstallTypes + ${If} $ExpressInstallState == ${BST_CHECKED} + + StrCpy $Express "1" + + ; over ride custom checkboxes and select defaults + ${NSD_SetState} $DesktopClientCheckbox ${BST_CHECKED} + ${NSD_SetState} $ServerStartupCheckbox ${BST_CHECKED} + ${NSD_SetState} $LaunchServerNowCheckbox ${BST_CHECKED} + ${NSD_SetState} $LaunchClientNowCheckbox ${BST_CHECKED} + + ${If} @PR_BUILD@ == 1 + ${NSD_SetState} $CopyFromProductionCheckbox ${BST_UNCHECKED} + ${EndIf} + + ; call ReadPostInstallOptions and HandlePostInstallOptions with defaults selected + Call ReadPostInstallOptions + Call HandlePostInstallOptions + ${EndIf} FunctionEnd @@ -610,6 +727,7 @@ Function HandlePostInstallOptions !insertmacro WritePostInstallOption @CONSOLE_DESKTOP_SHORTCUT_REG_KEY@ NO ${EndIf} + ; check if the user asked to have Sandbox launched every startup ${If} $ServerStartupState == ${BST_CHECKED} ; in case we added a shortcut in the global context, pull that now @@ -628,6 +746,15 @@ Function HandlePostInstallOptions !insertmacro WritePostInstallOption @CONSOLE_STARTUP_REG_KEY@ NO ${EndIf} ${EndIf} + + ${If} ${SectionIsSelected} ${@CLIENT_COMPONENT_NAME@} + ; check if the user asked for a clean install + ${If} $CleanInstallState == ${BST_CHECKED} + SetShellVarContext current + RMDir /r "$APPDATA\@BUILD_ORGANIZATION@" + RMDir /r "$LOCALAPPDATA\@BUILD_ORGANIZATION@" + ${EndIf} + ${EndIf} ${If} @PR_BUILD@ == 1 @@ -726,6 +853,8 @@ Section "-Core installation" ; Rename the incorrectly cased Raleway font Rename "$INSTDIR\resources\qml\styles-uit\RalewaySemibold.qml" "$INSTDIR\resources\qml\styles-uit\RalewaySemiBold.qml" + ExecWait "$INSTDIR\vcredist_x64.exe /install /q /norestart" + ; Remove the Old Interface directory and vcredist_x64.exe (from installs prior to Server Console) RMDir /r "$INSTDIR\Interface" Delete "$INSTDIR\vcredist_x64.exe" diff --git a/domain-server/src/DomainGatekeeper.cpp b/domain-server/src/DomainGatekeeper.cpp index 62f56184f4..fc595be67e 100644 --- a/domain-server/src/DomainGatekeeper.cpp +++ b/domain-server/src/DomainGatekeeper.cpp @@ -385,7 +385,7 @@ SharedNodePointer DomainGatekeeper::processAgentConnectRequest(const NodeConnect // user is attempting to prove their identity to us, but we don't have enough information sendConnectionTokenPacket(username, nodeConnection.senderSockAddr); // ask for their public key right now to make sure we have it - requestUserPublicKey(username); + requestUserPublicKey(username, true); getGroupMemberships(username); // optimistically get started on group memberships #ifdef WANT_DEBUG qDebug() << "stalling login because we have no username-signature:" << username; @@ -521,7 +521,10 @@ bool DomainGatekeeper::verifyUserSignature(const QString& username, const HifiSockAddr& senderSockAddr) { // it's possible this user can be allowed to connect, but we need to check their username signature auto lowerUsername = username.toLower(); - QByteArray publicKeyArray = _userPublicKeys.value(lowerUsername); + KeyFlagPair publicKeyPair = _userPublicKeys.value(lowerUsername); + + QByteArray publicKeyArray = publicKeyPair.first; + bool isOptimisticKey = publicKeyPair.second; const QUuid& connectionToken = _connectionTokenHash.value(lowerUsername); @@ -555,10 +558,16 @@ bool DomainGatekeeper::verifyUserSignature(const QString& username, return true; } else { - if (!senderSockAddr.isNull()) { - qDebug() << "Error decrypting username signature for " << username << "- denying connection."; + // we only send back a LoginError if this wasn't an "optimistic" key + // (a key that we hoped would work but is probably stale) + + if (!senderSockAddr.isNull() && !isOptimisticKey) { + qDebug() << "Error decrypting username signature for" << username << "- denying connection."; sendConnectionDeniedPacket("Error decrypting username signature.", senderSockAddr, DomainHandler::ConnectionRefusedReason::LoginError); + } else if (!senderSockAddr.isNull()) { + qDebug() << "Error decrypting username signature for" << username << "with optimisitic key -" + << "re-requesting public key and delaying connection"; } // free up the public key, we don't need it anymore @@ -604,20 +613,7 @@ bool DomainGatekeeper::isWithinMaxCapacity() { return true; } - -void DomainGatekeeper::preloadAllowedUserPublicKeys() { - QStringList allowedUsers = _server->_settingsManager.getAllNames(); - - if (allowedUsers.size() > 0) { - // in the future we may need to limit how many requests here - for now assume that lists of allowed users are not - // going to create > 100 requests - foreach(const QString& username, allowedUsers) { - requestUserPublicKey(username); - } - } -} - -void DomainGatekeeper::requestUserPublicKey(const QString& username) { +void DomainGatekeeper::requestUserPublicKey(const QString& username, bool isOptimistic) { // don't request public keys for the standard psuedo-account-names if (NodePermissions::standardNames.contains(username, Qt::CaseInsensitive)) { return; @@ -628,7 +624,7 @@ void DomainGatekeeper::requestUserPublicKey(const QString& username) { // public-key request for this username is already flight, not rerequesting return; } - _inFlightPublicKeyRequests += lowerUsername; + _inFlightPublicKeyRequests.insert(lowerUsername, isOptimistic); // even if we have a public key for them right now, request a new one in case it has just changed JSONCallbackParameters callbackParams; @@ -640,7 +636,7 @@ void DomainGatekeeper::requestUserPublicKey(const QString& username) { const QString USER_PUBLIC_KEY_PATH = "api/v1/users/%1/public_key"; - qDebug() << "Requesting public key for user" << username; + qDebug().nospace() << "Requesting " << (isOptimistic ? "optimistic " : " ") << "public key for user " << username; DependencyManager::get()->sendRequest(USER_PUBLIC_KEY_PATH.arg(username), AccountManagerAuth::None, @@ -662,16 +658,21 @@ void DomainGatekeeper::publicKeyJSONCallback(QNetworkReply& requestReply) { QJsonObject jsonObject = QJsonDocument::fromJson(requestReply.readAll()).object(); QString username = extractUsernameFromPublicKeyRequest(requestReply); + bool isOptimisticKey = _inFlightPublicKeyRequests.take(username); + if (jsonObject["status"].toString() == "success" && !username.isEmpty()) { // pull the public key as a QByteArray from this response const QString JSON_DATA_KEY = "data"; const QString JSON_PUBLIC_KEY_KEY = "public_key"; - _userPublicKeys[username.toLower()] = - QByteArray::fromBase64(jsonObject[JSON_DATA_KEY].toObject()[JSON_PUBLIC_KEY_KEY].toString().toUtf8()); - } + qDebug().nospace() << "Extracted " << (isOptimisticKey ? "optimistic " : " ") << "public key for " << username.toLower(); - _inFlightPublicKeyRequests.remove(username); + _userPublicKeys[username.toLower()] = + { + QByteArray::fromBase64(jsonObject[JSON_DATA_KEY].toObject()[JSON_PUBLIC_KEY_KEY].toString().toUtf8()), + isOptimisticKey + }; + } } void DomainGatekeeper::publicKeyJSONErrorCallback(QNetworkReply& requestReply) { @@ -927,9 +928,12 @@ void DomainGatekeeper::getDomainOwnerFriendsList() { callbackParams.errorCallbackMethod = "getDomainOwnerFriendsListErrorCallback"; const QString GET_FRIENDS_LIST_PATH = "api/v1/user/friends"; - DependencyManager::get()->sendRequest(GET_FRIENDS_LIST_PATH, AccountManagerAuth::Required, - QNetworkAccessManager::GetOperation, callbackParams, QByteArray(), - NULL, QVariantMap()); + if (DependencyManager::get()->hasValidAccessToken()) { + DependencyManager::get()->sendRequest(GET_FRIENDS_LIST_PATH, AccountManagerAuth::Required, + QNetworkAccessManager::GetOperation, callbackParams, QByteArray(), + NULL, QVariantMap()); + } + } void DomainGatekeeper::getDomainOwnerFriendsListJSONCallback(QNetworkReply& requestReply) { diff --git a/domain-server/src/DomainGatekeeper.h b/domain-server/src/DomainGatekeeper.h index 163f255411..09db075e07 100644 --- a/domain-server/src/DomainGatekeeper.h +++ b/domain-server/src/DomainGatekeeper.h @@ -39,8 +39,6 @@ public: const QUuid& walletUUID, const QString& nodeVersion); QUuid assignmentUUIDForPendingAssignment(const QUuid& tempUUID); - void preloadAllowedUserPublicKeys(); - void removeICEPeer(const QUuid& peerUUID) { _icePeers.remove(peerUUID); } static void sendProtocolMismatchConnectionDenial(const HifiSockAddr& senderSockAddr); @@ -93,7 +91,7 @@ private: void pingPunchForConnectingPeer(const SharedNetworkPeer& peer); - void requestUserPublicKey(const QString& username); + void requestUserPublicKey(const QString& username, bool isOptimistic = false); DomainServer* _server; @@ -102,8 +100,17 @@ private: QHash _icePeers; QHash _connectionTokenHash; - QHash _userPublicKeys; - QSet _inFlightPublicKeyRequests; // keep track of which we've already asked for + + // the word "optimistic" below is used for keys that we request during user connection before the user has + // had a chance to upload a new public key + + // we don't send back user signature decryption errors for those keys so that there isn't a thrasing of key re-generation + // and connection refusal + + using KeyFlagPair = QPair; + + QHash _userPublicKeys; // keep track of keys and flag them as optimistic or not + QHash _inFlightPublicKeyRequests; // keep track of keys we've asked for (and if it was optimistic) QSet _domainOwnerFriends; // keep track of friends of the domain owner QSet _inFlightGroupMembershipsRequests; // keep track of which we've already asked for diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index c5171620de..163bd48f1b 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -160,9 +160,7 @@ DomainServer::DomainServer(int argc, char* argv[]) : getTemporaryName(); } - _gatekeeper.preloadAllowedUserPublicKeys(); // so they can connect on first request - - //send signal to DomainMetadata when descriptors changed + // send signal to DomainMetadata when descriptors changed _metadata = new DomainMetadata(this); connect(&_settingsManager, &DomainServerSettingsManager::settingsUpdated, _metadata, &DomainMetadata::descriptorsChanged); diff --git a/interface/CMakeLists.txt b/interface/CMakeLists.txt index dcb1cacef9..81c8a44baf 100644 --- a/interface/CMakeLists.txt +++ b/interface/CMakeLists.txt @@ -195,7 +195,7 @@ link_hifi_libraries( shared octree ktx gpu gl gpu-gl procedural model render recording fbx networking model-networking entities avatars trackers audio audio-client animation script-engine physics - render-utils entities-renderer avatars-renderer ui auto-updater + render-utils entities-renderer avatars-renderer ui auto-updater midi controllers plugins image trackers ui-plugins display-plugins input-plugins ${NON_ANDROID_LIBRARIES} @@ -309,9 +309,12 @@ else (APPLE) # setup install of executable and things copied by fixup/windeployqt install( - FILES "$/" + DIRECTORY "$/" DESTINATION ${INTERFACE_INSTALL_DIR} COMPONENT ${CLIENT_COMPONENT} + PATTERN "*.pdb" EXCLUDE + PATTERN "*.lib" EXCLUDE + PATTERN "*.exp" EXCLUDE ) set(SCRIPTS_INSTALL_DIR "${INTERFACE_INSTALL_DIR}") diff --git a/interface/resources/controllers/keyboardMouse.json b/interface/resources/controllers/keyboardMouse.json index 8baf56684a..f377e02e5f 100644 --- a/interface/resources/controllers/keyboardMouse.json +++ b/interface/resources/controllers/keyboardMouse.json @@ -15,7 +15,7 @@ { "comment" : "Mouse turn need to be small continuous increments", "from": { "makeAxis" : [ [ "Keyboard.MouseMoveLeft" ], - [ "Keyboard.MouseMoveRight" ] + [ "Keyboard.MouseMoveRight" ] ] }, "when": [ "Application.InHMD", "Application.SnapTurn", "Keyboard.RightMouseButton" ], @@ -31,8 +31,8 @@ { "comment" : "Touchpad turn need to be small continuous increments, but without the RMB constraint", "from": { "makeAxis" : [ [ "Keyboard.TouchpadLeft" ], - [ "Keyboard.TouchpadRight" ] - ] + [ "Keyboard.TouchpadRight" ] + ] }, "when": [ "Application.InHMD", "Application.SnapTurn" ], "to": "Actions.StepYaw", @@ -81,7 +81,11 @@ { "from": { "makeAxis" : ["Keyboard.MouseMoveLeft", "Keyboard.MouseMoveRight"] }, "when": "Keyboard.RightMouseButton", - "to": "Actions.Yaw" + "to": "Actions.Yaw", + "filters": + [ + { "type": "scale", "scale": 0.6 } + ] }, { "from": "Keyboard.W", "to": "Actions.LONGITUDINAL_FORWARD" }, @@ -102,8 +106,19 @@ { "from": "Keyboard.PgDown", "to": "Actions.VERTICAL_DOWN" }, { "from": "Keyboard.PgUp", "to": "Actions.VERTICAL_UP" }, - { "from": "Keyboard.MouseMoveUp", "when": "Keyboard.RightMouseButton", "to": "Actions.PITCH_UP" }, - { "from": "Keyboard.MouseMoveDown", "when": "Keyboard.RightMouseButton", "to": "Actions.PITCH_DOWN" }, + { "from": "Keyboard.MouseMoveUp", "when": "Keyboard.RightMouseButton", "to": "Actions.PITCH_UP", + "filters": + [ + { "type": "scale", "scale": 0.6 } + ] + + }, + { "from": "Keyboard.MouseMoveDown", "when": "Keyboard.RightMouseButton", "to": "Actions.PITCH_DOWN", + "filters": + [ + { "type": "scale", "scale": 0.6 } + ] + }, { "from": "Keyboard.TouchpadDown", "to": "Actions.PITCH_DOWN" }, { "from": "Keyboard.TouchpadUp", "to": "Actions.PITCH_UP" }, diff --git a/interface/resources/controllers/standard.json b/interface/resources/controllers/standard.json index 166f1a6869..0d5c095585 100644 --- a/interface/resources/controllers/standard.json +++ b/interface/resources/controllers/standard.json @@ -109,6 +109,23 @@ { "from": "Standard.Head", "to": "Actions.Head" }, { "from": "Standard.LeftArm", "to": "Actions.LeftArm" }, - { "from": "Standard.RightArm", "to": "Actions.RightArm" } + { "from": "Standard.RightArm", "to": "Actions.RightArm" }, + + { "from": "Standard.TrackedObject00", "to" : "Actions.TrackedObject00" }, + { "from": "Standard.TrackedObject01", "to" : "Actions.TrackedObject01" }, + { "from": "Standard.TrackedObject02", "to" : "Actions.TrackedObject02" }, + { "from": "Standard.TrackedObject03", "to" : "Actions.TrackedObject03" }, + { "from": "Standard.TrackedObject04", "to" : "Actions.TrackedObject04" }, + { "from": "Standard.TrackedObject05", "to" : "Actions.TrackedObject05" }, + { "from": "Standard.TrackedObject06", "to" : "Actions.TrackedObject06" }, + { "from": "Standard.TrackedObject07", "to" : "Actions.TrackedObject07" }, + { "from": "Standard.TrackedObject08", "to" : "Actions.TrackedObject08" }, + { "from": "Standard.TrackedObject09", "to" : "Actions.TrackedObject09" }, + { "from": "Standard.TrackedObject10", "to" : "Actions.TrackedObject10" }, + { "from": "Standard.TrackedObject11", "to" : "Actions.TrackedObject11" }, + { "from": "Standard.TrackedObject12", "to" : "Actions.TrackedObject12" }, + { "from": "Standard.TrackedObject13", "to" : "Actions.TrackedObject13" }, + { "from": "Standard.TrackedObject14", "to" : "Actions.TrackedObject14" }, + { "from": "Standard.TrackedObject15", "to" : "Actions.TrackedObject15" } ] } diff --git a/interface/resources/controllers/vive.json b/interface/resources/controllers/vive.json index 8f58ef3c98..02fc09c815 100644 --- a/interface/resources/controllers/vive.json +++ b/interface/resources/controllers/vive.json @@ -1,6 +1,15 @@ { "name": "Vive to Standard", "channels": [ + { "from": "Vive.LY", "to": "Standard.LeftIndexPoint", + "peek": true, + "filters": [ { "type": "hysteresis", "min": 0.7, "max": 0.75 } ] + }, + { "from": "Vive.RY", "to": "Standard.RightIndexPoint", + "peek": true, + "filters": [ { "type": "hysteresis", "min": 0.7, "max": 0.75 } ] + }, + { "from": "Vive.LY", "when": "Vive.LSY", "filters": ["invert"], "to": "Standard.LY" }, { "from": "Vive.LX", "when": "Vive.LSX", "to": "Standard.LX" }, { @@ -13,6 +22,10 @@ { "from": "Vive.LeftGrip", "to": "Standard.LeftGrip" }, { "from": "Vive.LS", "to": "Standard.LS" }, + { "from": "Vive.LSTouch", "to": "Standard.LeftThumbUp", + "peek": true, + "filters": [ { "type": "logicalNot" } ] + }, { "from": "Vive.LSTouch", "to": "Standard.LSTouch" }, { "from": "Vive.RY", "when": "Vive.RSY", "filters": ["invert"], "to": "Standard.RY" }, @@ -27,6 +40,10 @@ { "from": "Vive.RightGrip", "to": "Standard.RightGrip" }, { "from": "Vive.RS", "to": "Standard.RS" }, + { "from": "Vive.RSTouch", "to": "Standard.RightThumbUp", + "peek": true, + "filters": [ { "type": "logicalNot" } ] + }, { "from": "Vive.RSTouch", "to": "Standard.RSTouch" }, { "from": "Vive.LSCenter", "to": "Standard.LeftPrimaryThumb" }, @@ -59,7 +76,24 @@ { "from": "Vive.Head", "to" : "Standard.Head"}, - { "from": "Vive.RightArm", "to" : "Standard.RightArm"}, - { "from": "Vive.LeftArm", "to" : "Standard.LeftArm"} + { "from": "Vive.RightArm", "to" : "Standard.RightArm" }, + { "from": "Vive.LeftArm", "to" : "Standard.LeftArm" }, + + { "from": "Vive.TrackedObject00", "to" : "Standard.TrackedObject00" }, + { "from": "Vive.TrackedObject01", "to" : "Standard.TrackedObject01" }, + { "from": "Vive.TrackedObject02", "to" : "Standard.TrackedObject02" }, + { "from": "Vive.TrackedObject03", "to" : "Standard.TrackedObject03" }, + { "from": "Vive.TrackedObject04", "to" : "Standard.TrackedObject04" }, + { "from": "Vive.TrackedObject05", "to" : "Standard.TrackedObject05" }, + { "from": "Vive.TrackedObject06", "to" : "Standard.TrackedObject06" }, + { "from": "Vive.TrackedObject07", "to" : "Standard.TrackedObject07" }, + { "from": "Vive.TrackedObject08", "to" : "Standard.TrackedObject08" }, + { "from": "Vive.TrackedObject09", "to" : "Standard.TrackedObject09" }, + { "from": "Vive.TrackedObject10", "to" : "Standard.TrackedObject10" }, + { "from": "Vive.TrackedObject11", "to" : "Standard.TrackedObject11" }, + { "from": "Vive.TrackedObject12", "to" : "Standard.TrackedObject12" }, + { "from": "Vive.TrackedObject13", "to" : "Standard.TrackedObject13" }, + { "from": "Vive.TrackedObject14", "to" : "Standard.TrackedObject14" }, + { "from": "Vive.TrackedObject15", "to" : "Standard.TrackedObject15" } ] } diff --git a/interface/resources/icons/tablet-icons/edit-disabled.svg b/interface/resources/icons/tablet-icons/edit-disabled.svg new file mode 100644 index 0000000000..4869b30dd9 --- /dev/null +++ b/interface/resources/icons/tablet-icons/edit-disabled.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + diff --git a/interface/resources/icons/tablet-icons/spectator-a.svg b/interface/resources/icons/tablet-icons/spectator-a.svg new file mode 100644 index 0000000000..22ebde999b --- /dev/null +++ b/interface/resources/icons/tablet-icons/spectator-a.svg @@ -0,0 +1,19 @@ + + + + + + + + diff --git a/interface/resources/icons/tablet-icons/spectator-i.svg b/interface/resources/icons/tablet-icons/spectator-i.svg new file mode 100644 index 0000000000..3e6c1a7dd9 --- /dev/null +++ b/interface/resources/icons/tablet-icons/spectator-i.svg @@ -0,0 +1,22 @@ + + + + + + + + + diff --git a/interface/resources/images/cursor-arrow.png b/interface/resources/images/cursor-arrow.png new file mode 100644 index 0000000000..408881b585 Binary files /dev/null and b/interface/resources/images/cursor-arrow.png differ diff --git a/interface/resources/images/cursor-link.png b/interface/resources/images/cursor-link.png new file mode 100644 index 0000000000..67ff4c7b67 Binary files /dev/null and b/interface/resources/images/cursor-link.png differ diff --git a/interface/resources/images/cursor-none.png b/interface/resources/images/cursor-none.png new file mode 100644 index 0000000000..0b3f499242 Binary files /dev/null and b/interface/resources/images/cursor-none.png differ diff --git a/interface/resources/images/cursor-reticle.png b/interface/resources/images/cursor-reticle.png new file mode 100644 index 0000000000..2d9ba8fd65 Binary files /dev/null and b/interface/resources/images/cursor-reticle.png differ diff --git a/interface/resources/images/inspect-icon.png b/interface/resources/images/inspect-icon.png new file mode 100644 index 0000000000..9259de23e9 Binary files /dev/null and b/interface/resources/images/inspect-icon.png differ diff --git a/interface/resources/images/static.gif b/interface/resources/images/static.gif new file mode 100644 index 0000000000..fbe46f48e6 Binary files /dev/null and b/interface/resources/images/static.gif differ diff --git a/interface/resources/qml/AssetServer.qml b/interface/resources/qml/AssetServer.qml index eb47afc951..ee37dbd8db 100644 --- a/interface/resources/qml/AssetServer.qml +++ b/interface/resources/qml/AssetServer.qml @@ -28,6 +28,7 @@ ScrollingWindow { minSize: Qt.vector2d(200, 300) property int colorScheme: hifi.colorSchemes.dark + property int selectionMode: SelectionMode.ExtendedSelection HifiConstants { id: hifi } @@ -35,7 +36,8 @@ ScrollingWindow { property var assetProxyModel: Assets.proxyModel; property var assetMappingsModel: Assets.mappingModel; property var currentDirectory; - + property var selectedItems: treeView.selection.selectedIndexes.length; + Settings { category: "Overlay.AssetServer" property alias x: root.x @@ -48,7 +50,7 @@ ScrollingWindow { assetMappingsModel.errorGettingMappings.connect(handleGetMappingsError); reload(); } - + function doDeleteFile(path) { console.log("Deleting " + path); @@ -118,11 +120,23 @@ ScrollingWindow { function canAddToWorld(path) { var supportedExtensions = [/\.fbx\b/i, /\.obj\b/i]; + + if (selectedItems > 1) { + return false; + } return supportedExtensions.reduce(function(total, current) { return total | new RegExp(current).test(path); }, false); } + + function canRename() { + if (treeView.selection.hasSelection && selectedItems == 1) { + return true; + } else { + return false; + } + } function clear() { Assets.mappingModel.clear(); @@ -151,13 +165,17 @@ ScrollingWindow { var SHAPE_TYPE_SIMPLE_HULL = 1; var SHAPE_TYPE_SIMPLE_COMPOUND = 2; var SHAPE_TYPE_STATIC_MESH = 3; - + var SHAPE_TYPE_BOX = 4; + var SHAPE_TYPE_SPHERE = 5; + 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"; - + SHAPE_TYPES[SHAPE_TYPE_BOX] = "Box"; + SHAPE_TYPES[SHAPE_TYPE_SPHERE] = "Sphere"; + var SHAPE_TYPE_DEFAULT = SHAPE_TYPE_STATIC_MESH; var DYNAMIC_DEFAULT = false; var prompt = desktop.customInputDialog({ @@ -196,6 +214,12 @@ ScrollingWindow { case SHAPE_TYPE_STATIC_MESH: shapeType = "static-mesh"; break; + case SHAPE_TYPE_BOX: + shapeType = "box"; + break; + case SHAPE_TYPE_SPHERE: + shapeType = "sphere"; + break; default: shapeType = "none"; } @@ -289,23 +313,37 @@ ScrollingWindow { }); } function deleteFile(index) { + var path = []; + if (!index) { - index = treeView.selection.currentIndex; + for (var i = 0; i < selectedItems; i++) { + treeView.selection.setCurrentIndex(treeView.selection.selectedIndexes[i], 0x100); + index = treeView.selection.currentIndex; + path[i] = assetProxyModel.data(index, 0x100); + } } - var path = assetProxyModel.data(index, 0x100); + if (!path) { return; } - + + var modalMessage = ""; + var items = selectedItems.toString(); var isFolder = assetProxyModel.data(treeView.selection.currentIndex, 0x101); var typeString = isFolder ? 'folder' : 'file'; - + + if (selectedItems > 1) { + modalMessage = "You are about to delete " + items + " items \nDo you want to continue?"; + } else { + modalMessage = "You are about to delete the following " + typeString + ":\n" + path + "\nDo you want to continue?"; + } + var object = desktop.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?" + text: modalMessage }); object.selected.connect(function(button) { if (button === OriginalDialogs.StandardButton.Yes) { @@ -445,20 +483,20 @@ ScrollingWindow { 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 + enabled: canRename() } HifiControls.Button { @@ -514,6 +552,7 @@ ScrollingWindow { treeModel: assetProxyModel canEdit: true colorScheme: root.colorScheme + selectionMode: SelectionMode.ExtendedSelection modifyEl: renameEl diff --git a/interface/resources/qml/controls-uit/CheckBox.qml b/interface/resources/qml/controls-uit/CheckBox.qml index 36161e3c3e..b279b7ca8d 100644 --- a/interface/resources/qml/controls-uit/CheckBox.qml +++ b/interface/resources/qml/controls-uit/CheckBox.qml @@ -18,7 +18,7 @@ Original.CheckBox { id: checkBox property int colorScheme: hifi.colorSchemes.light - property string color: hifi.colors.lightGray + property string color: hifi.colors.lightGrayText readonly property bool isLightColorScheme: colorScheme == hifi.colorSchemes.light property bool isRedCheck: false property int boxSize: 14 diff --git a/interface/resources/qml/controls-uit/RadioButton.qml b/interface/resources/qml/controls-uit/RadioButton.qml new file mode 100644 index 0000000000..ab11ec68b1 --- /dev/null +++ b/interface/resources/qml/controls-uit/RadioButton.qml @@ -0,0 +1,71 @@ +// +// RadioButton.qml +// +// Created by Cain Kilgore on 20th July 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 as Original +import QtQuick.Controls.Styles 1.4 + +import "../styles-uit" +import "../controls-uit" as HifiControls + +Original.RadioButton { + id: radioButton + HifiConstants { id: hifi } + + property int colorScheme: hifi.colorSchemes.light + readonly property bool isLightColorScheme: colorScheme == hifi.colorSchemes.light + + readonly property int boxSize: 14 + readonly property int boxRadius: 3 + readonly property int checkSize: 10 + readonly property int checkRadius: 2 + + style: RadioButtonStyle { + indicator: Rectangle { + id: box + width: boxSize + height: boxSize + radius: 7 + gradient: Gradient { + GradientStop { + position: 0.2 + color: pressed || hovered + ? (radioButton.isLightColorScheme ? hifi.colors.checkboxDarkStart : hifi.colors.checkboxLightStart) + : (radioButton.isLightColorScheme ? hifi.colors.checkboxLightStart : hifi.colors.checkboxDarkStart) + } + GradientStop { + position: 1.0 + color: pressed || hovered + ? (radioButton.isLightColorScheme ? hifi.colors.checkboxDarkFinish : hifi.colors.checkboxLightFinish) + : (radioButton.isLightColorScheme ? hifi.colors.checkboxLightFinish : hifi.colors.checkboxDarkFinish) + } + } + + Rectangle { + id: check + width: checkSize + height: checkSize + radius: 7 + anchors.centerIn: parent + color: "#00B4EF" + border.width: 1 + border.color: "#36CDFF" + visible: checked && !pressed || !checked && pressed + } + } + + label: RalewaySemiBold { + text: control.text + size: hifi.fontSizes.inputLabel + color: isLightColorScheme ? hifi.colors.lightGray : hifi.colors.lightGrayText + x: radioButton.boxSize / 2 + } + } +} diff --git a/interface/resources/qml/controls-uit/Separator.qml b/interface/resources/qml/controls-uit/Separator.qml new file mode 100644 index 0000000000..5a775221f6 --- /dev/null +++ b/interface/resources/qml/controls-uit/Separator.qml @@ -0,0 +1,38 @@ +// +// Separator.qml +// +// Created by Zach Fox on 2017-06-06 +// 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 "../styles-uit" + +Item { + // Size + height: 2; + Rectangle { + // Size + width: parent.width; + height: 1; + // Anchors + anchors.left: parent.left; + anchors.bottom: parent.bottom; + anchors.bottomMargin: height; + // Style + color: hifi.colors.baseGrayShadow; + } + Rectangle { + // Size + width: parent.width; + height: 1; + // Anchors + anchors.left: parent.left; + anchors.bottom: parent.bottom; + // Style + color: hifi.colors.baseGrayHighlight; + } +} diff --git a/interface/resources/qml/controls-uit/Slider.qml b/interface/resources/qml/controls-uit/Slider.qml index 39831546e1..89bae9bcde 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/(slider.maximumValue - slider.minimumValue)) - 1 + height: slider.width * (slider.value - slider.minimumValue) / (slider.maximumValue - slider.minimumValue) - 1 radius: height / 2 anchors { top: parent.top diff --git a/interface/resources/qml/controls-uit/Switch.qml b/interface/resources/qml/controls-uit/Switch.qml new file mode 100644 index 0000000000..d54f986717 --- /dev/null +++ b/interface/resources/qml/controls-uit/Switch.qml @@ -0,0 +1,156 @@ +// +// Switch.qml +// +// Created by Zach Fox on 2017-06-06 +// 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 as Original +import QtQuick.Controls.Styles 1.4 + +import "../styles-uit" + +Item { + id: rootSwitch; + + property int colorScheme: hifi.colorSchemes.light; + readonly property bool isLightColorScheme: colorScheme == hifi.colorSchemes.light; + property int switchWidth: 70; + readonly property int switchRadius: height/2; + property string labelTextOff: ""; + property string labelGlyphOffText: ""; + property int labelGlyphOffSize: 32; + property string labelTextOn: ""; + property string labelGlyphOnText: ""; + property int labelGlyphOnSize: 32; + property alias checked: originalSwitch.checked; + signal onCheckedChanged; + signal clicked; + + Original.Switch { + id: originalSwitch; + activeFocusOnPress: true; + anchors.top: rootSwitch.top; + anchors.left: rootSwitch.left; + anchors.leftMargin: rootSwitch.width/2 - rootSwitch.switchWidth/2; + onCheckedChanged: rootSwitch.onCheckedChanged(); + onClicked: rootSwitch.clicked(); + + style: SwitchStyle { + + padding { + top: 3; + left: 3; + right: 3; + bottom: 3; + } + + groove: Rectangle { + color: "#252525"; + implicitWidth: rootSwitch.switchWidth; + implicitHeight: rootSwitch.height; + radius: rootSwitch.switchRadius; + } + + handle: Rectangle { + id: switchHandle; + implicitWidth: rootSwitch.height - padding.top - padding.bottom; + implicitHeight: implicitWidth; + radius: implicitWidth/2; + border.color: hifi.colors.lightGrayText; + color: hifi.colors.lightGray; + + MouseArea { + anchors.fill: parent; + hoverEnabled: true; + onEntered: parent.color = hifi.colors.blueHighlight; + onExited: parent.color = hifi.colors.lightGray; + } + } + } + } + + // OFF Label + Item { + anchors.right: originalSwitch.left; + anchors.rightMargin: 10; + anchors.top: rootSwitch.top; + height: rootSwitch.height; + + RalewaySemiBold { + id: labelOff; + text: labelTextOff; + size: hifi.fontSizes.inputLabel; + color: originalSwitch.checked ? hifi.colors.lightGrayText : "#FFFFFF"; + anchors.top: parent.top; + anchors.right: parent.right; + width: paintedWidth; + height: parent.height; + verticalAlignment: Text.AlignVCenter; + } + + HiFiGlyphs { + id: labelGlyphOff; + text: labelGlyphOffText; + size: labelGlyphOffSize; + color: labelOff.color; + anchors.top: parent.top; + anchors.topMargin: 2; + anchors.right: labelOff.left; + anchors.rightMargin: 4; + } + + MouseArea { + anchors.top: parent.top; + anchors.bottom: parent.bottom; + anchors.left: labelGlyphOff.left; + anchors.right: labelOff.right; + onClicked: { + originalSwitch.checked = false; + } + } + } + + // ON Label + Item { + anchors.left: originalSwitch.right; + anchors.leftMargin: 10; + anchors.top: rootSwitch.top; + height: rootSwitch.height; + + RalewaySemiBold { + id: labelOn; + text: labelTextOn; + size: hifi.fontSizes.inputLabel; + color: originalSwitch.checked ? "#FFFFFF" : hifi.colors.lightGrayText; + anchors.top: parent.top; + anchors.left: parent.left; + width: paintedWidth; + height: parent.height; + verticalAlignment: Text.AlignVCenter; + } + + HiFiGlyphs { + id: labelGlyphOn; + text: labelGlyphOnText; + size: labelGlyphOnSize; + color: labelOn.color; + anchors.top: parent.top; + anchors.left: labelOn.right; + } + + MouseArea { + anchors.top: parent.top; + anchors.bottom: parent.bottom; + anchors.left: labelOn.left; + anchors.right: labelGlyphOn.right; + onClicked: { + originalSwitch.checked = true; + } + } + } +} diff --git a/interface/resources/qml/controls/RadioButton.qml b/interface/resources/qml/controls/RadioButton.qml new file mode 100644 index 0000000000..6bd9c860d2 --- /dev/null +++ b/interface/resources/qml/controls/RadioButton.qml @@ -0,0 +1,17 @@ +import QtQuick.Controls 1.3 as Original +import QtQuick.Controls.Styles 1.3 +import "../styles" +import "." +Original.RadioButton { + text: "Radio Button" + style: RadioButtonStyle { + indicator: FontAwesome { + text: control.checked ? "\uf046" : "\uf096" + } + label: Text { + text: control.text + renderType: Text.QtRendering + } + } + +} diff --git a/interface/resources/qml/dialogs/preferences/PrimaryHandPreference.qml b/interface/resources/qml/dialogs/preferences/PrimaryHandPreference.qml new file mode 100644 index 0000000000..cfc2e94ed9 --- /dev/null +++ b/interface/resources/qml/dialogs/preferences/PrimaryHandPreference.qml @@ -0,0 +1,101 @@ +// +// PrimaryHandPreference.qml +// +// Created by Cain Kilgore on 20th July 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 "../../controls-uit" + +Preference { + id: root + property alias box1: box1 + property alias box2: box2 + + height: control.height + hifi.dimensions.controlInterlineHeight + + Component.onCompleted: { + if (preference.value == "left") { + box1.checked = true; + } else { + box2.checked = true; + } + } + + function save() { + if (box1.checked && !box2.checked) { + preference.value = "left"; + } + if (!box1.checked && box2.checked) { + preference.value = "right"; + } + preference.save(); + } + + Item { + id: control + anchors { + left: parent.left + right: parent.right + bottom: parent.bottom + } + height: Math.max(labelName.height, box1.height, box2.height) + + Label { + id: labelName + text: root.label + ":" + colorScheme: hifi.colorSchemes.dark + anchors { + left: parent.left + right: box1.left + rightMargin: hifi.dimensions.labelPadding + verticalCenter: parent.verticalCenter + } + horizontalAlignment: Text.AlignRight + wrapMode: Text.Wrap + } + + RadioButton { + id: box1 + text: "Left" + width: 60 + anchors { + right: box2.left + verticalCenter: parent.verticalCenter + } + onClicked: { + if (box2.checked) { + box2.checked = false; + } + if (!box1.checked && !box2.checked) { + box1.checked = true; + } + } + colorScheme: hifi.colorSchemes.dark + } + + RadioButton { + id: box2 + text: "Right" + width: 60 + anchors { + right: parent.right + verticalCenter: parent.verticalCenter + } + onClicked: { + if (box1.checked) { + box1.checked = false; + } + if (!box1.checked && !box2.checked) { + box2.checked = true; + } + } + colorScheme: hifi.colorSchemes.dark + } + } +} diff --git a/interface/resources/qml/dialogs/preferences/Section.qml b/interface/resources/qml/dialogs/preferences/Section.qml index a813dc6b5f..4a16036a69 100644 --- a/interface/resources/qml/dialogs/preferences/Section.qml +++ b/interface/resources/qml/dialogs/preferences/Section.qml @@ -72,6 +72,8 @@ Preference { property var avatarBuilder: Component { AvatarPreference { } } property var buttonBuilder: Component { ButtonPreference { } } property var comboBoxBuilder: Component { ComboBoxPreference { } } + property var spinnerSliderBuilder: Component { SpinnerSliderPreference { } } + property var primaryHandBuilder: Component { PrimaryHandPreference { } } property var preferences: [] property int checkBoxCount: 0 @@ -86,7 +88,7 @@ Preference { } function buildPreference(preference) { - console.log("\tPreference type " + preference.type + " name " + preference.name) + console.log("\tPreference type " + preference.type + " name " + preference.name); var builder; switch (preference.type) { case Preference.Editable: @@ -128,6 +130,16 @@ Preference { checkBoxCount = 0; builder = comboBoxBuilder; break; + + case Preference.SpinnerSlider: + checkBoxCount = 0; + builder = spinnerSliderBuilder; + break; + + case Preference.PrimaryHand: + checkBoxCount++; + builder = primaryHandBuilder; + break; }; if (builder) { diff --git a/interface/resources/qml/dialogs/preferences/SpinnerSliderPreference.qml b/interface/resources/qml/dialogs/preferences/SpinnerSliderPreference.qml new file mode 100644 index 0000000000..3cba67bc82 --- /dev/null +++ b/interface/resources/qml/dialogs/preferences/SpinnerSliderPreference.qml @@ -0,0 +1,111 @@ +// +// SpinnerSliderPreference.qml +// +// Created by Cain Kilgore on 11th July 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 "../../dialogs" +import "../../controls-uit" + +Preference { + id: root + property alias slider: slider + property alias spinner: spinner + height: control.height + hifi.dimensions.controlInterlineHeight + + Component.onCompleted: { + slider.value = preference.value; + spinner.value = preference.value; + } + + function save() { + preference.value = slider.value; + preference.save(); + } + + Item { + id: control + anchors { + left: parent.left + right: parent.right + bottom: parent.bottom + } + height: Math.max(labelText.height, slider.height, spinner.height, button.height) + + Label { + id: labelText + text: root.label + ":" + colorScheme: hifi.colorSchemes.dark + anchors { + left: parent.left + right: slider.left + rightMargin: hifi.dimensions.labelPadding + verticalCenter: parent.verticalCenter + } + horizontalAlignment: Text.AlignRight + wrapMode: Text.Wrap + } + + Slider { + id: slider + value: preference.value + width: 100 + minimumValue: MyAvatar.getDomainMinScale() + maximumValue: MyAvatar.getDomainMaxScale() + stepSize: preference.step + onValueChanged: { + spinner.value = value + } + anchors { + right: spinner.left + rightMargin: 10 + verticalCenter: parent.verticalCenter + } + colorScheme: hifi.colorSchemes.dark + } + + SpinBox { + id: spinner + decimals: preference.decimals + value: preference.value + minimumValue: MyAvatar.getDomainMinScale() + maximumValue: MyAvatar.getDomainMaxScale() + width: 100 + onValueChanged: { + slider.value = value; + } + anchors { + right: button.left + rightMargin: 10 + verticalCenter: parent.verticalCenter + } + colorScheme: hifi.colorSchemes.dark + } + + GlyphButton { + id: button + onClicked: { + if (spinner.maximumValue >= 1) { + spinner.value = 1 + slider.value = 1 + } else { + spinner.value = spinner.maximumValue + slider.value = spinner.maximumValue + } + } + width: 30 + glyph: hifi.glyphs.reload + anchors { + right: parent.right + verticalCenter: parent.verticalCenter + } + colorScheme: hifi.colorSchemes.dark + } + } +} \ No newline at end of file diff --git a/interface/resources/qml/hifi/LetterboxMessage.qml b/interface/resources/qml/hifi/LetterboxMessage.qml index 754876b2c1..fa9d7aa6f0 100644 --- a/interface/resources/qml/hifi/LetterboxMessage.qml +++ b/interface/resources/qml/hifi/LetterboxMessage.qml @@ -32,14 +32,15 @@ Item { radius: popupRadius } Rectangle { - width: Math.max(parent.width * 0.75, 400) + id: textContainer; + width: Math.max(parent.width * 0.8, 400) height: contentContainer.height + 50 anchors.centerIn: parent radius: popupRadius color: "white" Item { id: contentContainer - width: parent.width - 60 + width: parent.width - 50 height: childrenRect.height anchors.centerIn: parent Item { @@ -92,7 +93,7 @@ Item { anchors.top: parent.top anchors.topMargin: -20 anchors.right: parent.right - anchors.rightMargin: -25 + anchors.rightMargin: -20 MouseArea { anchors.fill: closeGlyphButton hoverEnabled: true @@ -127,11 +128,51 @@ Item { color: hifi.colors.darkGray wrapMode: Text.WordWrap textFormat: Text.StyledText + onLinkActivated: { + Qt.openUrlExternally(link) + } } } } + // Left gray MouseArea MouseArea { - anchors.fill: parent + anchors.left: parent.left; + anchors.right: textContainer.left; + anchors.top: textContainer.top; + anchors.bottom: textContainer.bottom; + acceptedButtons: Qt.LeftButton + onClicked: { + letterbox.visible = false + } + } + // Right gray MouseArea + MouseArea { + anchors.left: textContainer.left; + anchors.right: parent.left; + anchors.top: textContainer.top; + anchors.bottom: textContainer.bottom; + acceptedButtons: Qt.LeftButton + onClicked: { + letterbox.visible = false + } + } + // Top gray MouseArea + MouseArea { + anchors.left: parent.left; + anchors.right: parent.right; + anchors.top: parent.top; + anchors.bottom: textContainer.top; + acceptedButtons: Qt.LeftButton + onClicked: { + letterbox.visible = false + } + } + // Bottom gray MouseArea + MouseArea { + anchors.left: parent.left; + anchors.right: parent.right; + anchors.top: textContainer.bottom; + anchors.bottom: parent.bottom; acceptedButtons: Qt.LeftButton onClicked: { letterbox.visible = false diff --git a/interface/resources/qml/hifi/SpectatorCamera.qml b/interface/resources/qml/hifi/SpectatorCamera.qml new file mode 100644 index 0000000000..3a8559ab1e --- /dev/null +++ b/interface/resources/qml/hifi/SpectatorCamera.qml @@ -0,0 +1,374 @@ +// +// SpectatorCamera.qml +// qml/hifi +// +// Spectator Camera +// +// Created by Zach Fox on 2017-06-05 +// 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 as Hifi +import QtQuick 2.5 +import QtQuick.Controls 1.4 +import "../styles-uit" +import "../controls-uit" as HifiControlsUit +import "../controls" as HifiControls + +// references HMD, XXX from root context + +Rectangle { + HifiConstants { id: hifi; } + + id: spectatorCamera; + // Style + color: hifi.colors.baseGray; + + // The letterbox used for popup messages + LetterboxMessage { + id: letterboxMessage; + z: 999; // Force the popup on top of everything else + } + function letterbox(headerGlyph, headerText, message) { + letterboxMessage.headerGlyph = headerGlyph; + letterboxMessage.headerText = headerText; + letterboxMessage.text = message; + letterboxMessage.visible = true; + letterboxMessage.popupRadius = 0; + } + + // + // TITLE BAR START + // + Item { + id: titleBarContainer; + // Size + width: spectatorCamera.width; + height: 50; + // Anchors + anchors.left: parent.left; + anchors.top: parent.top; + + // "Spectator" text + RalewaySemiBold { + id: titleBarText; + text: "Spectator"; + // Text size + size: hifi.fontSizes.overlayTitle; + // Anchors + anchors.fill: parent; + anchors.leftMargin: 16; + // Style + color: hifi.colors.lightGrayText; + // Alignment + horizontalAlignment: Text.AlignHLeft; + verticalAlignment: Text.AlignVCenter; + } + + // Separator + HifiControlsUit.Separator { + anchors.left: parent.left; + anchors.right: parent.right; + anchors.bottom: parent.bottom; + } + } + // + // TITLE BAR END + // + + // + // SPECTATOR APP DESCRIPTION START + // + Item { + id: spectatorDescriptionContainer; + // Size + width: spectatorCamera.width; + height: childrenRect.height; + // Anchors + anchors.left: parent.left; + anchors.top: titleBarContainer.bottom; + + // (i) Glyph + HiFiGlyphs { + id: spectatorDescriptionGlyph; + text: hifi.glyphs.info; + // Size + width: 20; + height: parent.height; + size: 60; + // Anchors + anchors.left: parent.left; + anchors.leftMargin: 20; + anchors.top: parent.top; + anchors.topMargin: 0; + // Style + color: hifi.colors.lightGrayText; + horizontalAlignment: Text.AlignHLeft; + verticalAlignment: Text.AlignTop; + } + + // "Spectator" app description text + RalewayLight { + id: spectatorDescriptionText; + text: "Spectator lets you change what your monitor displays while you're using a VR headset. Use Spectator when streaming and recording video."; + // Text size + size: 14; + // Size + width: 350; + height: paintedHeight; + // Anchors + anchors.top: parent.top; + anchors.topMargin: 15; + anchors.left: spectatorDescriptionGlyph.right; + anchors.leftMargin: 40; + // Style + color: hifi.colors.lightGrayText; + wrapMode: Text.WordWrap; + // Alignment + horizontalAlignment: Text.AlignHLeft; + verticalAlignment: Text.AlignVCenter; + } + + // "Learn More" text + RalewayRegular { + id: spectatorLearnMoreText; + text: "Learn More About Spectator"; + // Text size + size: 14; + // Size + width: paintedWidth; + height: paintedHeight; + // Anchors + anchors.top: spectatorDescriptionText.bottom; + anchors.topMargin: 10; + anchors.left: spectatorDescriptionText.anchors.left; + anchors.leftMargin: spectatorDescriptionText.anchors.leftMargin; + // Style + color: hifi.colors.blueAccent; + wrapMode: Text.WordWrap; + font.underline: true; + // Alignment + horizontalAlignment: Text.AlignHLeft; + verticalAlignment: Text.AlignVCenter; + + MouseArea { + anchors.fill: parent; + hoverEnabled: enabled; + onClicked: { + letterbox(hifi.glyphs.question, + "Spectator Camera", + "By default, your monitor shows a preview of what you're seeing in VR. " + + "Using the Spectator Camera app, your monitor can display the view " + + "from a virtual hand-held camera - perfect for taking selfies or filming " + + "your friends!
" + + "

Streaming and Recording

" + + "We recommend OBS for streaming and recording the contents of your monitor to services like " + + "Twitch, YouTube Live, and Facebook Live.

" + + "To get started using OBS, click this link now. The page will open in an external browser:
" + + 'OBS Official Overview Guide'); + } + onEntered: parent.color = hifi.colors.blueHighlight; + onExited: parent.color = hifi.colors.blueAccent; + } + } + + // Separator + HifiControlsUit.Separator { + anchors.left: parent.left; + anchors.right: parent.right; + anchors.top: spectatorLearnMoreText.bottom; + anchors.topMargin: spectatorDescriptionText.anchors.topMargin; + } + } + // + // SPECTATOR APP DESCRIPTION END + // + + + // + // SPECTATOR CONTROLS START + // + Item { + id: spectatorControlsContainer; + // Size + height: spectatorCamera.height - spectatorDescriptionContainer.height - titleBarContainer.height; + // Anchors + anchors.top: spectatorDescriptionContainer.bottom; + anchors.topMargin: 20; + anchors.left: parent.left; + anchors.leftMargin: 25; + anchors.right: parent.right; + anchors.rightMargin: anchors.leftMargin; + + // "Camera On" Checkbox + HifiControlsUit.CheckBox { + id: cameraToggleCheckBox; + colorScheme: hifi.colorSchemes.dark; + anchors.left: parent.left; + anchors.top: parent.top; + text: "Spectator Camera On"; + boxSize: 24; + onClicked: { + sendToScript({method: (checked ? 'spectatorCameraOn' : 'spectatorCameraOff')}); + spectatorCameraPreview.ready = checked; + } + } + + // Instructions or Preview + Rectangle { + id: spectatorCameraImageContainer; + anchors.left: parent.left; + anchors.top: cameraToggleCheckBox.bottom; + anchors.topMargin: 20; + anchors.right: parent.right; + height: 250; + color: cameraToggleCheckBox.checked ? "transparent" : "black"; + + AnimatedImage { + source: "../../images/static.gif" + visible: !cameraToggleCheckBox.checked; + anchors.fill: parent; + opacity: 0.15; + } + + // Instructions (visible when display texture isn't set) + FiraSansRegular { + id: spectatorCameraInstructions; + text: "Turn on Spectator Camera for a preview\nof what your monitor shows."; + size: 16; + color: hifi.colors.lightGrayText; + visible: !cameraToggleCheckBox.checked; + anchors.fill: parent; + horizontalAlignment: Text.AlignHCenter; + verticalAlignment: Text.AlignVCenter; + } + + // Spectator Camera Preview + Hifi.ResourceImageItem { + id: spectatorCameraPreview; + visible: cameraToggleCheckbox.checked; + url: monitorShowsSwitch.checked ? "resource://spectatorCameraFrame" : "resource://hmdPreviewFrame"; + ready: cameraToggleCheckBox.checked; + mirrorVertically: true; + anchors.fill: parent; + onVisibleChanged: { + ready = cameraToggleCheckBox.checked; + update(); + } + } + } + + + // "Monitor Shows" Switch Label Glyph + HiFiGlyphs { + id: monitorShowsSwitchLabelGlyph; + text: hifi.glyphs.screen; + size: 32; + color: hifi.colors.blueHighlight; + anchors.top: spectatorCameraImageContainer.bottom; + anchors.topMargin: 13; + anchors.left: parent.left; + } + // "Monitor Shows" Switch Label + RalewayLight { + id: monitorShowsSwitchLabel; + text: "MONITOR SHOWS:"; + anchors.top: spectatorCameraImageContainer.bottom; + anchors.topMargin: 20; + anchors.left: monitorShowsSwitchLabelGlyph.right; + anchors.leftMargin: 6; + size: 16; + width: paintedWidth; + height: paintedHeight; + color: hifi.colors.lightGrayText; + verticalAlignment: Text.AlignVCenter; + } + // "Monitor Shows" Switch + HifiControlsUit.Switch { + id: monitorShowsSwitch; + height: 30; + anchors.left: parent.left; + anchors.right: parent.right; + anchors.top: monitorShowsSwitchLabel.bottom; + anchors.topMargin: 10; + labelTextOff: "HMD Preview"; + labelTextOn: "Camera View"; + labelGlyphOnText: hifi.glyphs.alert; + onCheckedChanged: { + sendToScript({method: 'setMonitorShowsCameraView', params: checked}); + } + } + + // "Switch View From Controller" Checkbox + HifiControlsUit.CheckBox { + id: switchViewFromControllerCheckBox; + colorScheme: hifi.colorSchemes.dark; + anchors.left: parent.left; + anchors.top: monitorShowsSwitch.bottom; + anchors.topMargin: 25; + text: ""; + boxSize: 24; + onClicked: { + sendToScript({method: 'changeSwitchViewFromControllerPreference', params: checked}); + } + } + } + // + // SPECTATOR CONTROLS END + // + + // + // FUNCTION DEFINITIONS START + // + // + // Function Name: fromScript() + // + // Relevant Variables: + // None + // + // Arguments: + // message: The message sent from the SpectatorCamera JavaScript. + // Messages are in format "{method, params}", like json-rpc. + // + // Description: + // Called when a message is received from spectatorCamera.js. + // + function fromScript(message) { + switch (message.method) { + case 'updateSpectatorCameraCheckbox': + cameraToggleCheckBox.checked = message.params; + break; + case 'updateMonitorShowsSwitch': + monitorShowsSwitch.checked = message.params; + break; + case 'updateControllerMappingCheckbox': + switchViewFromControllerCheckBox.checked = message.setting; + switchViewFromControllerCheckBox.enabled = true; + if (message.controller === "OculusTouch") { + switchViewFromControllerCheckBox.text = "Clicking Touch's Left Thumbstick Switches Monitor View"; + } else if (message.controller === "Vive") { + switchViewFromControllerCheckBox.text = "Clicking Left Thumb Pad Switches Monitor View"; + } else { + switchViewFromControllerCheckBox.text = "Pressing Ctrl+0 Switches Monitor View"; + switchViewFromControllerCheckBox.checked = true; + switchViewFromControllerCheckBox.enabled = false; + } + break; + case 'showPreviewTextureNotInstructions': + console.log('showPreviewTextureNotInstructions recvd', JSON.stringify(message)); + spectatorCameraPreview.url = message.url; + spectatorCameraPreview.visible = message.setting; + break; + default: + console.log('Unrecognized message from spectatorCamera.js:', JSON.stringify(message)); + } + } + signal sendToScript(var message); + + // + // FUNCTION DEFINITIONS END + // +} diff --git a/interface/resources/qml/hifi/audio/Audio.qml b/interface/resources/qml/hifi/audio/Audio.qml index 4e0b4cebe2..a819e032eb 100644 --- a/interface/resources/qml/hifi/audio/Audio.qml +++ b/interface/resources/qml/hifi/audio/Audio.qml @@ -33,7 +33,7 @@ Rectangle { // only show the title if loaded through a "loader" function showTitle() { - return root.parent.objectName == "loader"; + return (root.parent !== null) && root.parent.objectName == "loader"; } property bool showPeaks: true; @@ -135,27 +135,29 @@ Rectangle { delegate: Item { width: parent.width; height: 36; + + AudioControls.CheckBox { + id: checkbox + anchors.verticalCenter: parent.verticalCenter + anchors.left: parent.left + text: display; + wrap: false; + checked: selected; + enabled: false; + } - RowLayout { - width: parent.width; + MouseArea { + anchors.fill: checkbox + onClicked: Audio.setInputDevice(info); + } - AudioControls.CheckBox { - Layout.maximumWidth: parent.width - inputPeak.width - 40; - text: display; - wrap: false; - checked: selected; - onClicked: { - selected = checked; - checked = Qt.binding(function() { return selected; }); // restore binding - } - } - InputPeak { - id: inputPeak; - visible: Audio.devices.input.peakValuesAvailable; - peak: model.peak; - Layout.alignment: Qt.AlignRight; - Layout.rightMargin: 30; - } + InputPeak { + id: inputPeak; + visible: Audio.devices.input.peakValuesAvailable; + peak: model.peak; + anchors.verticalCenter: parent.verticalCenter + anchors.right: parent.right + anchors.rightMargin: 30 } } } @@ -193,13 +195,19 @@ Rectangle { delegate: Item { width: parent.width; height: 36; + AudioControls.CheckBox { + id: checkbox + anchors.verticalCenter: parent.verticalCenter + anchors.left: parent.left text: display; checked: selected; - onClicked: { - selected = checked; - checked = Qt.binding(function() { return selected; }); // restore binding - } + enabled: false; + } + + MouseArea { + anchors.fill: checkbox + onClicked: Audio.setOutputDevice(info); } } } diff --git a/interface/resources/qml/hifi/audio/MicBar.qml b/interface/resources/qml/hifi/audio/MicBar.qml index 10e12551b7..e757d7cadf 100644 --- a/interface/resources/qml/hifi/audio/MicBar.qml +++ b/interface/resources/qml/hifi/audio/MicBar.qml @@ -27,7 +27,7 @@ Rectangle { color: "#00000000"; border { - width: (standalone || Audio.muted || mouseArea.containsMouse) ? 2 : 0; + width: mouseArea.containsMouse || mouseArea.containsPress ? 2 : 0; color: colors.border; } @@ -61,7 +61,7 @@ Rectangle { drag.target: dragTarget; } - Item { + QtObject { id: colors; readonly property string unmuted: "#FFF"; @@ -72,7 +72,7 @@ Rectangle { readonly property string red: colors.muted; readonly property string fill: "#55000000"; readonly property string border: standalone ? "#80FFFFFF" : "#55FFFFFF"; - readonly property string icon: (Audio.muted && !mouseArea.containsMouse) ? muted : unmuted; + readonly property string icon: Audio.muted ? muted : unmuted; } Item { @@ -92,10 +92,8 @@ Rectangle { readonly property string unmutedIcon: "../../../icons/tablet-icons/mic-unmute-i.svg"; readonly property string mutedIcon: "../../../icons/tablet-icons/mic-mute-i.svg"; - function exclusiveOr(a, b) { return (a || b) && !(a && b); } - id: image; - source: exclusiveOr(Audio.muted, mouseArea.containsMouse) ? mutedIcon : unmutedIcon; + source: Audio.muted ? mutedIcon : unmutedIcon; width: 30; height: 30; @@ -118,9 +116,9 @@ Rectangle { Item { id: status; - readonly property string color: (Audio.muted && !mouseArea.containsMouse) ? colors.muted : colors.unmuted; + readonly property string color: Audio.muted ? colors.muted : colors.unmuted; - visible: Audio.muted || mouseArea.containsMouse; + visible: Audio.muted; anchors { left: parent.left; @@ -133,14 +131,14 @@ Rectangle { Text { anchors { - horizontalCenter: parent.horizontalCenter; + horizontalCenter: parent.horizontalCenter; verticalCenter: parent.verticalCenter; } color: parent.color; - text: Audio.muted ? (mouseArea.containsMouse ? "UNMUTE" : "MUTED") : "MUTE"; - font.pointSize: 12; + text: Audio.muted ? "MUTED" : "MUTE"; + font.pointSize: 12; } Rectangle { @@ -150,7 +148,7 @@ Rectangle { } width: 50; - height: 4; + height: 4; color: parent.color; } @@ -161,7 +159,7 @@ Rectangle { } width: 50; - height: 4; + height: 4; color: parent.color; } } diff --git a/interface/resources/qml/hifi/dialogs/GeneralPreferencesDialog.qml b/interface/resources/qml/hifi/dialogs/GeneralPreferencesDialog.qml index cabc09e49b..94665794d6 100644 --- a/interface/resources/qml/hifi/dialogs/GeneralPreferencesDialog.qml +++ b/interface/resources/qml/hifi/dialogs/GeneralPreferencesDialog.qml @@ -17,7 +17,7 @@ PreferencesDialog { id: root objectName: "GeneralPreferencesDialog" title: "General Settings" - showCategories: ["UI", "Snapshots", "Scripts", "Privacy", "Octree", "HMD", "Sixense Controllers", "Perception Neuron", "Kinect", "Leap Motion"] + showCategories: ["UI", "Snapshots", "Scripts", "Privacy", "Octree", "HMD", "Game Controller", "Sixense Controllers", "Perception Neuron", "Kinect", "Leap Motion"] property var settings: Settings { category: root.objectName property alias x: root.x diff --git a/interface/resources/qml/hifi/dialogs/TabletAssetServer.qml b/interface/resources/qml/hifi/dialogs/TabletAssetServer.qml index 47c9af1f57..a7b4ad7a53 100644 --- a/interface/resources/qml/hifi/dialogs/TabletAssetServer.qml +++ b/interface/resources/qml/hifi/dialogs/TabletAssetServer.qml @@ -37,6 +37,7 @@ Rectangle { property var assetProxyModel: Assets.proxyModel; property var assetMappingsModel: Assets.mappingModel; property var currentDirectory; + property var selectedItems: treeView.selection.selectedIndexes.length; Settings { category: "Overlay.AssetServer" @@ -119,11 +120,23 @@ Rectangle { function canAddToWorld(path) { var supportedExtensions = [/\.fbx\b/i, /\.obj\b/i]; + + if (selectedItems > 1) { + return false; + } return supportedExtensions.reduce(function(total, current) { return total | new RegExp(current).test(path); }, false); } + + function canRename() { + if (treeView.selection.hasSelection && selectedItems == 1) { + return true; + } else { + return false; + } + } function clear() { Assets.mappingModel.clear(); @@ -152,13 +165,17 @@ Rectangle { var SHAPE_TYPE_SIMPLE_HULL = 1; var SHAPE_TYPE_SIMPLE_COMPOUND = 2; var SHAPE_TYPE_STATIC_MESH = 3; - + var SHAPE_TYPE_BOX = 4; + var SHAPE_TYPE_SPHERE = 5; + 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"; - + SHAPE_TYPES[SHAPE_TYPE_BOX] = "Box"; + SHAPE_TYPES[SHAPE_TYPE_SPHERE] = "Sphere"; + var SHAPE_TYPE_DEFAULT = SHAPE_TYPE_STATIC_MESH; var DYNAMIC_DEFAULT = false; var prompt = tabletRoot.customInputDialog({ @@ -197,6 +214,12 @@ Rectangle { case SHAPE_TYPE_STATIC_MESH: shapeType = "static-mesh"; break; + case SHAPE_TYPE_BOX: + shapeType = "box"; + break; + case SHAPE_TYPE_SPHERE: + shapeType = "sphere"; + break; default: shapeType = "none"; } @@ -290,23 +313,37 @@ Rectangle { }); } function deleteFile(index) { + var path = []; + if (!index) { - index = treeView.selection.currentIndex; + for (var i = 0; i < selectedItems; i++) { + treeView.selection.setCurrentIndex(treeView.selection.selectedIndexes[i], 0x100); + index = treeView.selection.currentIndex; + path[i] = assetProxyModel.data(index, 0x100); + } } - var path = assetProxyModel.data(index, 0x100); + if (!path) { return; } + var modalMessage = ""; + var items = selectedItems.toString(); var isFolder = assetProxyModel.data(treeView.selection.currentIndex, 0x101); var typeString = isFolder ? 'folder' : 'file'; + + if (selectedItems > 1) { + modalMessage = "You are about to delete " + items + " items \nDo you want to continue?"; + } else { + modalMessage = "You are about to delete the following " + typeString + ":\n" + path + "\nDo you want to continue?"; + } 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?" + text: modalMessage }); object.selected.connect(function(button) { if (button === OriginalDialogs.StandardButton.Yes) { @@ -459,7 +496,7 @@ Rectangle { width: 80 onClicked: root.renameFile() - enabled: treeView.selection.hasSelection + enabled: canRename() } HifiControls.Button { @@ -506,7 +543,7 @@ Rectangle { } HifiControls.Tree { id: treeView - height: 430 + height: 290 anchors.leftMargin: hifi.dimensions.contentMargin.x + 2 // Extra for border anchors.rightMargin: hifi.dimensions.contentMargin.x + 2 // Extra for border anchors.left: parent.left @@ -515,6 +552,7 @@ Rectangle { treeModel: assetProxyModel canEdit: true colorScheme: root.colorScheme + selectionMode: SelectionMode.ExtendedSelection modifyEl: renameEl diff --git a/interface/resources/qml/hifi/tablet/EditTabView.qml b/interface/resources/qml/hifi/tablet/EditTabView.qml index e4a20a0316..e94325f399 100644 --- a/interface/resources/qml/hifi/tablet/EditTabView.qml +++ b/interface/resources/qml/hifi/tablet/EditTabView.qml @@ -21,7 +21,7 @@ TabView { enabled: true property string originalUrl: "" - Rectangle { + Rectangle { color: "#404040" Text { @@ -180,7 +180,7 @@ TabView { WebView { id: entityListToolWebView - url: "../../../../../scripts/system/html/entityList.html" + url: Paths.defaultScripts + "/system/html/entityList.html" anchors.fill: parent enabled: true } @@ -194,7 +194,7 @@ TabView { WebView { id: entityPropertiesWebView - url: "../../../../../scripts/system/html/entityProperties.html" + url: Paths.defaultScripts + "/system/html/entityProperties.html" anchors.fill: parent enabled: true } @@ -208,7 +208,7 @@ TabView { WebView { id: gridControlsWebView - url: "../../../../../scripts/system/html/gridControls.html" + url: Paths.defaultScripts + "/system/html/gridControls.html" anchors.fill: parent enabled: true } @@ -222,7 +222,7 @@ TabView { WebView { id: particleExplorerWebView - url: "../../../../../scripts/system/particle_explorer/particleExplorer.html" + url: Paths.defaultScripts + "/system/particle_explorer/particleExplorer.html" anchors.fill: parent enabled: true } @@ -293,16 +293,16 @@ TabView { break; case 'list': editTabView.currentIndex = 1; - break; + break; case 'properties': editTabView.currentIndex = 2; - break; + break; case 'grid': editTabView.currentIndex = 3; - break; + break; case 'particle': editTabView.currentIndex = 4; - break; + break; default: console.warn('Attempt to switch to invalid tab:', id); } @@ -310,4 +310,4 @@ TabView { console.warn('Attempt to switch tabs with invalid input:', JSON.stringify(id)); } } -} \ No newline at end of file +} diff --git a/interface/resources/qml/hifi/tablet/NewModelDialog.qml b/interface/resources/qml/hifi/tablet/NewModelDialog.qml index 5040c043f4..47d28486a9 100644 --- a/interface/resources/qml/hifi/tablet/NewModelDialog.qml +++ b/interface/resources/qml/hifi/tablet/NewModelDialog.qml @@ -65,7 +65,8 @@ Rectangle { onClicked: { newModelDialog.keyboardEnabled = HMD.active parent.focus = true; - parent.forceActiveFocus() + parent.forceActiveFocus(); + modelURL.cursorPosition = modelURL.positionAt(mouseX, mouseY, TextInput.CursorBetweenCharaters); } } } @@ -144,7 +145,9 @@ Rectangle { model: ["No Collision", "Basic - Whole model", "Good - Sub-meshes", - "Exact - All polygons"] + "Exact - All polygons", + "Box", + "Sphere"] } Row { diff --git a/interface/resources/qml/hifi/tablet/TabletGeneralPreferences.qml b/interface/resources/qml/hifi/tablet/TabletGeneralPreferences.qml index 18e7898dd0..dee2eed9c3 100644 --- a/interface/resources/qml/hifi/tablet/TabletGeneralPreferences.qml +++ b/interface/resources/qml/hifi/tablet/TabletGeneralPreferences.qml @@ -32,6 +32,6 @@ StackView { TabletPreferencesDialog { id: root objectName: "TabletGeneralPreferences" - showCategories: ["UI", "Snapshots", "Scripts", "Privacy", "Octree", "HMD", "Sixense Controllers", "Perception Neuron", "Kinect", "Vive Pucks Configuration", "Leap Motion"] + showCategories: ["UI", "Snapshots", "Scripts", "Privacy", "Octree", "HMD", "Game Controller", "Sixense Controllers", "Perception Neuron", "Kinect", "Leap Motion"] } } diff --git a/interface/resources/qml/hifi/tablet/TabletMenuStack.qml b/interface/resources/qml/hifi/tablet/TabletMenuStack.qml index 2fd33e9cbc..9076cd6c48 100644 --- a/interface/resources/qml/hifi/tablet/TabletMenuStack.qml +++ b/interface/resources/qml/hifi/tablet/TabletMenuStack.qml @@ -114,6 +114,7 @@ Item { } function clearMenus() { + topMenu = null d.clear() } diff --git a/interface/resources/qml/hifi/tablet/TabletRoot.qml b/interface/resources/qml/hifi/tablet/TabletRoot.qml index faa4013bce..c7df6ac64f 100644 --- a/interface/resources/qml/hifi/tablet/TabletRoot.qml +++ b/interface/resources/qml/hifi/tablet/TabletRoot.qml @@ -202,5 +202,11 @@ Item { width: 480 height: 706 - function setShown(value) {} + function setShown(value) { + if (value === true) { + HMD.openTablet() + } else { + HMD.closeTablet() + } + } } diff --git a/interface/resources/qml/hifi/tablet/tabletWindows/preferences/Section.qml b/interface/resources/qml/hifi/tablet/tabletWindows/preferences/Section.qml index 9dd0956000..8cf254809d 100644 --- a/interface/resources/qml/hifi/tablet/tabletWindows/preferences/Section.qml +++ b/interface/resources/qml/hifi/tablet/tabletWindows/preferences/Section.qml @@ -81,6 +81,8 @@ Preference { property var avatarBuilder: Component { AvatarPreference { } } property var buttonBuilder: Component { ButtonPreference { } } property var comboBoxBuilder: Component { ComboBoxPreference { } } + property var spinnerSliderBuilder: Component { SpinnerSliderPreference { } } + property var primaryHandBuilder: Component { PrimaryHandPreference { } } property var preferences: [] property int checkBoxCount: 0 @@ -143,6 +145,16 @@ Preference { //to be not overlapped when drop down is active zpos = root.z + 1000 - itemNum break; + + case Preference.SpinnerSlider: + checkBoxCount = 0; + builder = spinnerSliderBuilder; + break; + + case Preference.PrimaryHand: + checkBoxCount++; + builder = primaryHandBuilder; + break; }; if (builder) { diff --git a/interface/resources/qml/styles-uit/HifiConstants.qml b/interface/resources/qml/styles-uit/HifiConstants.qml index aa968c85ef..1556a9c0c0 100644 --- a/interface/resources/qml/styles-uit/HifiConstants.qml +++ b/interface/resources/qml/styles-uit/HifiConstants.qml @@ -50,7 +50,7 @@ Item { id: colors // Base colors - readonly property color baseGray: "#404040" + readonly property color baseGray: "#393939" readonly property color darkGray: "#121212" readonly property color baseGrayShadow: "#252525" readonly property color baseGrayHighlight: "#575757" diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index ddd1870723..549e5338a0 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -61,6 +61,7 @@ #include #include #include +#include #include #include #include @@ -69,6 +70,7 @@ #include #include #include +#include "ui/overlays/ContextOverlayInterface.h" #include #include #include @@ -167,6 +169,7 @@ #if defined(Q_OS_MAC) || defined(Q_OS_WIN) #include "SpeechRecognizer.h" #endif +#include "ui/ResourceImageItem.h" #include "ui/AddressBarDialog.h" #include "ui/AvatarInputs.h" #include "ui/DialogsManager.h" @@ -396,6 +399,10 @@ public: return true; } } + + if (message->message == WM_DEVICECHANGE) { + Midi::USBchanged(); // re-scan the MIDI bus + } } return false; } @@ -565,6 +572,7 @@ bool setupEssentials(int& argc, char** argv, bool runningMarkerExisted) { DependencyManager::set(); DependencyManager::set(); DependencyManager::set(); + DependencyManager::set(); DependencyManager::set(); DependencyManager::set(); DependencyManager::set(); @@ -587,6 +595,7 @@ bool setupEssentials(int& argc, char** argv, bool runningMarkerExisted) { DependencyManager::set(); DependencyManager::set(); DependencyManager::set(); + DependencyManager::set(); return previousSessionCrashed; } @@ -615,6 +624,7 @@ 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_PREFER_AVATAR_FINGER_OVER_STYLUS = false; +const QString DEFAULT_CURSOR_NAME = "DEFAULT"; Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bool runningMarkerExisted) : QApplication(argc, argv), @@ -634,6 +644,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo _hmdTabletBecomesToolbarSetting("hmdTabletBecomesToolbar", DEFAULT_HMD_TABLET_BECOMES_TOOLBAR), _preferAvatarFingerOverStylusSetting("preferAvatarFingerOverStylus", DEFAULT_PREFER_AVATAR_FINGER_OVER_STYLUS), _constrainToolbarPosition("toolbar/constrainToolbarToCenterX", true), + _preferredCursor("preferredCursor", DEFAULT_CURSOR_NAME), _scaleMirror(1.0f), _rotateMirror(0.0f), _raiseMirror(0.0f), @@ -929,14 +940,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo _glWidget->setFocusPolicy(Qt::StrongFocus); _glWidget->setFocus(); -#ifdef Q_OS_MAC - auto cursorTarget = _window; // OSX doesn't seem to provide for hiding the cursor only on the GL widget -#else - // On windows and linux, hiding the top level cursor also means it's invisible when hovering over the - // window menu, which is a pain, so only hide it for the GL surface - auto cursorTarget = _glWidget; -#endif - cursorTarget->setCursor(Qt::BlankCursor); + showCursor(Cursor::Manager::lookupIcon(_preferredCursor.get())); // enable mouse tracking; otherwise, we only get drag events _glWidget->setMouseTracking(true); @@ -1003,7 +1007,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo properties["processor_l2_cache_count"] = procInfo.numProcessorCachesL2; properties["processor_l3_cache_count"] = procInfo.numProcessorCachesL3; } - + properties["first_run"] = firstRun.get(); // add the user's machine ID to the launch event @@ -1320,12 +1324,16 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo // Keyboard focus handling for Web overlays. auto overlays = &(qApp->getOverlays()); - connect(overlays, &Overlays::mousePressOnOverlay, [=](OverlayID overlayID, const PointerEvent& event) { - setKeyboardFocusEntity(UNKNOWN_ENTITY_ID); - setKeyboardFocusOverlay(overlayID); + connect(overlays, &Overlays::mousePressOnOverlay, [=](const OverlayID& overlayID, const PointerEvent& event) { + auto thisOverlay = std::dynamic_pointer_cast(overlays->getOverlay(overlayID)); + // Only Web overlays can have keyboard focus. + if (thisOverlay) { + setKeyboardFocusEntity(UNKNOWN_ENTITY_ID); + setKeyboardFocusOverlay(overlayID); + } }); - connect(overlays, &Overlays::overlayDeleted, [=](OverlayID overlayID) { + connect(overlays, &Overlays::overlayDeleted, [=](const OverlayID& overlayID) { if (overlayID == _keyboardFocusedOverlay.get()) { setKeyboardFocusOverlay(UNKNOWN_OVERLAY_ID); } @@ -1340,6 +1348,21 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo setKeyboardFocusEntity(UNKNOWN_ENTITY_ID); }); + connect(overlays, + SIGNAL(mousePressOnOverlay(const OverlayID&, const PointerEvent&)), + DependencyManager::get().data(), + SLOT(contextOverlays_mousePressOnOverlay(const OverlayID&, const PointerEvent&))); + + connect(overlays, + SIGNAL(hoverEnterOverlay(const OverlayID&, const PointerEvent&)), + DependencyManager::get().data(), + SLOT(contextOverlays_hoverEnterOverlay(const OverlayID&, const PointerEvent&))); + + connect(overlays, + SIGNAL(hoverLeaveOverlay(const OverlayID&, const PointerEvent&)), + DependencyManager::get().data(), + SLOT(contextOverlays_hoverLeaveOverlay(const OverlayID&, const PointerEvent&))); + // Add periodic checks to send user activity data static int CHECK_NEARBY_AVATARS_INTERVAL_MS = 10000; static int NEARBY_AVATAR_RADIUS_METERS = 10; @@ -1467,6 +1490,14 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo properties["throttled"] = _displayPlugin ? _displayPlugin->isThrottled() : false; + QJsonObject bytesDownloaded; + bytesDownloaded["atp"] = statTracker->getStat(STAT_ATP_RESOURCE_TOTAL_BYTES).toInt(); + bytesDownloaded["http"] = statTracker->getStat(STAT_HTTP_RESOURCE_TOTAL_BYTES).toInt(); + bytesDownloaded["file"] = statTracker->getStat(STAT_FILE_RESOURCE_TOTAL_BYTES).toInt(); + bytesDownloaded["total"] = bytesDownloaded["atp"].toInt() + bytesDownloaded["http"].toInt() + + bytesDownloaded["file"].toInt(); + properties["bytesDownloaded"] = bytesDownloaded; + auto myAvatar = getMyAvatar(); glm::vec3 avatarPosition = myAvatar->getPosition(); properties["avatar_has_moved"] = lastAvatarPosition != avatarPosition; @@ -1704,9 +1735,7 @@ QString Application::getUserAgent() { void Application::toggleTabletUI(bool shouldOpen) const { auto tabletScriptingInterface = DependencyManager::get(); auto hmd = DependencyManager::get(); - TabletProxy* tablet = dynamic_cast(tabletScriptingInterface->getTablet(SYSTEM_TABLET)); - bool messageOpen = tablet->isMessageDialogOpen(); - if ((!messageOpen || (messageOpen && !hmd->getShouldShowTablet())) && !(shouldOpen && hmd->getShouldShowTablet())) { + if (!(shouldOpen && hmd->getShouldShowTablet())) { auto HMD = DependencyManager::get(); HMD->toggleShouldShowTablet(); } @@ -1728,9 +1757,16 @@ void Application::checkChangeCursor() { } } -void Application::showCursor(const QCursor& cursor) { +void Application::showCursor(const Cursor::Icon& cursor) { QMutexLocker locker(&_changeCursorLock); - _desiredCursor = cursor; + + auto managedCursor = Cursor::Manager::instance().getCursor(); + auto curIcon = managedCursor->getIcon(); + if (curIcon != cursor) { + managedCursor->setIcon(cursor); + curIcon = cursor; + } + _desiredCursor = cursor == Cursor::Icon::SYSTEM ? Qt::ArrowCursor : Qt::BlankCursor; _cursorNeedsChanging = true; } @@ -1971,7 +2007,7 @@ void Application::initializeGL() { static const QString RENDER_FORWARD = "HIFI_RENDER_FORWARD"; bool isDeferred = !QProcessEnvironment::systemEnvironment().contains(RENDER_FORWARD); _renderEngine->addJob("UpdateScene"); - _renderEngine->addJob("SecondaryCameraFrame", cullFunctor); + _renderEngine->addJob("SecondaryCameraJob", cullFunctor); _renderEngine->addJob("RenderMainView", cullFunctor, isDeferred); _renderEngine->load(); _renderEngine->registerScene(_main3DScene); @@ -2019,6 +2055,7 @@ void Application::initializeUi() { LoginDialog::registerType(); Tooltip::registerType(); UpdateDialog::registerType(); + qmlRegisterType("Hifi", 1, 0, "ResourceImageItem"); qmlRegisterType("Hifi", 1, 0, "Preference"); auto offscreenUi = DependencyManager::get(); @@ -2104,7 +2141,6 @@ void Application::initializeUi() { surfaceContext->setContextProperty("AvatarManager", DependencyManager::get().data()); surfaceContext->setContextProperty("UndoStack", &_undoStackScriptingInterface); surfaceContext->setContextProperty("LODManager", DependencyManager::get().data()); - surfaceContext->setContextProperty("Paths", DependencyManager::get().data()); surfaceContext->setContextProperty("HMD", DependencyManager::get().data()); surfaceContext->setContextProperty("Scene", DependencyManager::get().data()); surfaceContext->setContextProperty("Render", _renderEngine->getConfiguration().get()); @@ -2114,6 +2150,7 @@ void Application::initializeUi() { surfaceContext->setContextProperty("ApplicationCompositor", &getApplicationCompositor()); surfaceContext->setContextProperty("AvatarInputs", AvatarInputs::getInstance()); + surfaceContext->setContextProperty("ContextOverlay", DependencyManager::get().data()); if (auto steamClient = PluginManager::getInstance()->getSteamClientPlugin()) { surfaceContext->setContextProperty("Steam", new SteamScriptingInterface(engine, steamClient.get())); @@ -2149,9 +2186,11 @@ void Application::initializeUi() { _window->setMenuBar(new Menu()); auto compositorHelper = DependencyManager::get(); - connect(compositorHelper.data(), &CompositorHelper::allowMouseCaptureChanged, [=] { + connect(compositorHelper.data(), &CompositorHelper::allowMouseCaptureChanged, this, [=] { if (isHMDMode()) { - showCursor(compositorHelper->getAllowMouseCapture() ? Qt::BlankCursor : Qt::ArrowCursor); + showCursor(compositorHelper->getAllowMouseCapture() ? + Cursor::Manager::lookupIcon(_preferredCursor.get()) : + Cursor::Icon::SYSTEM); } }); @@ -2217,7 +2256,7 @@ void Application::paintGL() { QMutexLocker viewLocker(&_viewMutex); _viewFrustum.calculate(); } - renderArgs = RenderArgs(_gpuContext, getEntities(), lodManager->getOctreeSizeScale(), + renderArgs = RenderArgs(_gpuContext, lodManager->getOctreeSizeScale(), lodManager->getBoundaryLevelAdjust(), RenderArgs::DEFAULT_RENDER_MODE, RenderArgs::MONO, RenderArgs::RENDER_DEBUG_NONE); { @@ -2303,7 +2342,7 @@ void Application::paintGL() { } } else if (_myCamera.getMode() == CAMERA_MODE_MIRROR) { if (isHMDMode()) { - auto mirrorBodyOrientation = myAvatar->getWorldAlignedOrientation() * glm::quat(glm::vec3(0.0f, PI + _rotateMirror, 0.0f)); + auto mirrorBodyOrientation = myAvatar->getOrientation() * glm::quat(glm::vec3(0.0f, PI + _rotateMirror, 0.0f)); glm::quat hmdRotation = extractRotation(myAvatar->getHMDSensorMatrix()); // Mirror HMD yaw and roll @@ -2325,7 +2364,7 @@ void Application::paintGL() { + mirrorBodyOrientation * glm::vec3(0.0f, 0.0f, 1.0f) * MIRROR_FULLSCREEN_DISTANCE * _scaleMirror + mirrorBodyOrientation * hmdOffset); } else { - _myCamera.setOrientation(myAvatar->getWorldAlignedOrientation() + _myCamera.setOrientation(myAvatar->getOrientation() * glm::quat(glm::vec3(0.0f, PI + _rotateMirror, 0.0f))); _myCamera.setPosition(myAvatar->getDefaultEyePosition() + glm::vec3(0, _raiseMirror * myAvatar->getUniformScale(), 0) @@ -2493,6 +2532,12 @@ void Application::setPreferAvatarFingerOverStylus(bool value) { _preferAvatarFingerOverStylusSetting.set(value); } +void Application::setPreferredCursor(const QString& cursorName) { + qCDebug(interfaceapp) << "setPreferredCursor" << cursorName; + _preferredCursor.set(cursorName.isEmpty() ? DEFAULT_CURSOR_NAME : cursorName); + showCursor(Cursor::Manager::lookupIcon(_preferredCursor.get())); +} + void Application::setSettingConstrainToolbarPosition(bool setting) { _constrainToolbarPosition.set(setting); DependencyManager::get()->setConstrainToolbarToCenterX(setting); @@ -2727,7 +2772,12 @@ bool Application::importSVOFromURL(const QString& urlString) { return true; } -bool _renderRequested { false }; +void Application::onPresent(quint32 frameCount) { + if (shouldPaint()) { + postEvent(this, new QEvent(static_cast(Idle)), Qt::HighEventPriority); + postEvent(this, new QEvent(static_cast(Paint)), Qt::HighEventPriority); + } +} bool Application::event(QEvent* event) { if (!Menu::getInstance()) { @@ -2740,16 +2790,12 @@ bool Application::event(QEvent* event) { static_cast(event)->call(); return true; - case Event::Present: - if (!_renderRequested) { - float nsecsElapsed = (float)_lastTimeUpdated.nsecsElapsed(); - if (shouldPaint(nsecsElapsed)) { - _renderRequested = true; - _lastTimeUpdated.start(); - idle(nsecsElapsed); - postEvent(this, new QEvent(static_cast(Paint)), Qt::HighEventPriority); - } - } + // Explicit idle keeps the idle running at a lower interval, but without any rendering + // see (windowMinimizedChanged) + case Event::Idle: + idle(); + // Clear the event queue of pending idle calls + removePostedEvents(this, Idle); return true; case Event::Paint: @@ -2757,9 +2803,8 @@ bool Application::event(QEvent* event) { // or AvatarInputs will mysteriously move to the bottom-right AvatarInputs::getInstance()->update(); paintGL(); - // wait for the next present event before starting idle / paint again - removePostedEvents(this, Present); - _renderRequested = false; + // Clear the event queue of pending paint calls + removePostedEvents(this, Paint); return true; default: @@ -2999,7 +3044,9 @@ void Application::keyPressEvent(QKeyEvent* event) { break; case Qt::Key_F: { - _physicsEngine->dumpNextStats(); + if (isOption) { + _physicsEngine->dumpNextStats(); + } break; } @@ -3043,9 +3090,13 @@ void Application::keyPressEvent(QKeyEvent* event) { auto cursor = Cursor::Manager::instance().getCursor(); auto curIcon = cursor->getIcon(); if (curIcon == Cursor::Icon::DEFAULT) { - cursor->setIcon(Cursor::Icon::LINK); + showCursor(Cursor::Icon::RETICLE); + } else if (curIcon == Cursor::Icon::RETICLE) { + showCursor(Cursor::Icon::SYSTEM); + } else if (curIcon == Cursor::Icon::SYSTEM) { + showCursor(Cursor::Icon::LINK); } else { - cursor->setIcon(Cursor::Icon::DEFAULT); + showCursor(Cursor::Icon::DEFAULT); } } else { resetSensors(true); @@ -3572,7 +3623,7 @@ bool Application::acceptSnapshot(const QString& urlString) { static uint32_t _renderedFrameIndex { INVALID_FRAME }; -bool Application::shouldPaint(float nsecsElapsed) { +bool Application::shouldPaint() { if (_aboutToQuit) { return false; } @@ -3592,11 +3643,9 @@ bool Application::shouldPaint(float nsecsElapsed) { (float)paintDelaySamples / paintDelayUsecs << "us"; } #endif - - float msecondsSinceLastUpdate = nsecsElapsed / NSECS_PER_USEC / USECS_PER_MSEC; - + // Throttle if requested - if (displayPlugin->isThrottled() && (msecondsSinceLastUpdate < THROTTLED_SIM_FRAME_PERIOD_MS)) { + if (displayPlugin->isThrottled() && (_lastTimeUpdated.elapsed() < THROTTLED_SIM_FRAME_PERIOD_MS)) { return false; } @@ -3725,8 +3774,8 @@ void updateCpuInformation() { // Update friendly structure auto& myCpuInfo = myCpuInfos[i]; myCpuInfo.update(cpuInfo); - PROFILE_COUNTER(app, myCpuInfo.name.c_str(), { - { "kernel", myCpuInfo.kernelUsage }, + PROFILE_COUNTER(app, myCpuInfo.name.c_str(), { + { "kernel", myCpuInfo.kernelUsage }, { "user", myCpuInfo.userUsage } }); } @@ -3793,7 +3842,7 @@ void getCpuUsage(vec3& systemAndUser) { void setupCpuMonitorThread() { initCpuUsage(); auto cpuMonitorThread = QThread::currentThread(); - + QTimer* timer = new QTimer(); timer->setInterval(50); QObject::connect(timer, &QTimer::timeout, [] { @@ -3813,7 +3862,7 @@ void setupCpuMonitorThread() { #endif -void Application::idle(float nsecsElapsed) { +void Application::idle() { PerformanceTimer perfTimer("idle"); // Update the deadlock watchdog @@ -3870,7 +3919,8 @@ void Application::idle(float nsecsElapsed) { steamClient->runCallbacks(); } - float secondsSinceLastUpdate = nsecsElapsed / NSECS_PER_MSEC / MSECS_PER_SECOND; + float secondsSinceLastUpdate = (float)_lastTimeUpdated.nsecsElapsed() / NSECS_PER_MSEC / MSECS_PER_SECOND; + _lastTimeUpdated.start(); // If the offscreen Ui has something active that is NOT the root, then assume it has keyboard focus. if (_keyboardDeviceHasFocus && offscreenUi && offscreenUi->getWindow()->activeFocusItem() != offscreenUi->getRootItem()) { @@ -4104,6 +4154,7 @@ void Application::loadSettings() { //DependencyManager::get()->setAutomaticLODAdjust(false); Menu::getInstance()->loadSettings(); + // If there is a preferred plugin, we probably messed it up with the menu settings, so fix it. auto pluginManager = PluginManager::getInstance(); auto plugins = pluginManager->getPreferredDisplayPlugins(); @@ -4117,24 +4168,44 @@ void Application::loadSettings() { break; } } - } else { + } + + Setting::Handle firstRun { Settings::firstRun, true }; + bool isFirstPerson = false; + if (firstRun.get()) { // If this is our first run, and no preferred devices were set, default to // an HMD device if available. - Setting::Handle firstRun { Settings::firstRun, true }; - if (firstRun.get()) { - auto displayPlugins = pluginManager->getDisplayPlugins(); - for (auto& plugin : displayPlugins) { - if (plugin->isHmd()) { - if (auto action = menu->getActionForOption(plugin->getName())) { - action->setChecked(true); - action->trigger(); - break; - } + auto displayPlugins = pluginManager->getDisplayPlugins(); + for (auto& plugin : displayPlugins) { + if (plugin->isHmd()) { + if (auto action = menu->getActionForOption(plugin->getName())) { + action->setChecked(true); + action->trigger(); + break; } } } + + isFirstPerson = (qApp->isHMDMode()); + } else { + // if this is not the first run, the camera will be initialized differently depending on user settings + + if (qApp->isHMDMode()) { + // if the HMD is active, use first-person camera, unless the appropriate setting is checked + isFirstPerson = menu->isOptionChecked(MenuOption::FirstPersonHMD); + } else { + // if HMD is not active, only use first person if the menu option is checked + isFirstPerson = menu->isOptionChecked(MenuOption::FirstPerson); + } } + // finish initializing the camera, based on everything we checked above. Third person camera will be used if no settings + // dictated that we should be in first person + Menu::getInstance()->setIsOptionChecked(MenuOption::FirstPerson, isFirstPerson); + Menu::getInstance()->setIsOptionChecked(MenuOption::ThirdPerson, !isFirstPerson); + _myCamera.setMode((isFirstPerson) ? CAMERA_MODE_FIRST_PERSON : CAMERA_MODE_THIRD_PERSON); + cameraMenuChanged(); + auto inputs = pluginManager->getInputPlugins(); for (auto plugin : inputs) { if (!plugin->isActive()) { @@ -4183,7 +4254,6 @@ void Application::init() { DependencyManager::get()->init(); DependencyManager::get()->init(); - _myCamera.setMode(CAMERA_MODE_FIRST_PERSON); _timerStart.start(); _lastTimeUpdated.start(); @@ -4439,27 +4509,29 @@ void Application::cameraModeChanged() { void Application::cameraMenuChanged() { - if (Menu::getInstance()->isOptionChecked(MenuOption::FullscreenMirror)) { + auto menu = Menu::getInstance(); + if (menu->isOptionChecked(MenuOption::FullscreenMirror)) { if (_myCamera.getMode() != CAMERA_MODE_MIRROR) { _myCamera.setMode(CAMERA_MODE_MIRROR); + getMyAvatar()->reset(false, false, false); // to reset any active MyAvatar::FollowHelpers } - } else if (Menu::getInstance()->isOptionChecked(MenuOption::FirstPerson)) { + } else if (menu->isOptionChecked(MenuOption::FirstPerson)) { if (_myCamera.getMode() != CAMERA_MODE_FIRST_PERSON) { _myCamera.setMode(CAMERA_MODE_FIRST_PERSON); getMyAvatar()->setBoomLength(MyAvatar::ZOOM_MIN); } - } else if (Menu::getInstance()->isOptionChecked(MenuOption::ThirdPerson)) { + } else if (menu->isOptionChecked(MenuOption::ThirdPerson)) { if (_myCamera.getMode() != CAMERA_MODE_THIRD_PERSON) { _myCamera.setMode(CAMERA_MODE_THIRD_PERSON); if (getMyAvatar()->getBoomLength() == MyAvatar::ZOOM_MIN) { getMyAvatar()->setBoomLength(MyAvatar::ZOOM_DEFAULT); } } - } else if (Menu::getInstance()->isOptionChecked(MenuOption::IndependentMode)) { + } else if (menu->isOptionChecked(MenuOption::IndependentMode)) { if (_myCamera.getMode() != CAMERA_MODE_INDEPENDENT) { _myCamera.setMode(CAMERA_MODE_INDEPENDENT); } - } else if (Menu::getInstance()->isOptionChecked(MenuOption::CameraEntityMode)) { + } else if (menu->isOptionChecked(MenuOption::CameraEntityMode)) { if (_myCamera.getMode() != CAMERA_MODE_ENTITY) { _myCamera.setMode(CAMERA_MODE_ENTITY); } @@ -5353,6 +5425,17 @@ void Application::displaySide(RenderArgs* renderArgs, Camera& theCamera, bool se } renderArgs->_debugFlags = renderDebugFlags; //ViveControllerManager::getInstance().updateRendering(renderArgs, _main3DScene, transaction); + + RenderArgs::OutlineFlags renderOutlineFlags = RenderArgs::RENDER_OUTLINE_NONE; + auto contextOverlayInterface = DependencyManager::get(); + if (contextOverlayInterface->getEnabled()) { + if (DependencyManager::get()->getIsInMarketplaceInspectionMode()) { + renderOutlineFlags = RenderArgs::RENDER_OUTLINE_MARKETPLACE_MODE; + } else { + renderOutlineFlags = RenderArgs::RENDER_OUTLINE_WIREFRAMES; + } + } + renderArgs->_outlineFlags = renderOutlineFlags; } } @@ -5427,6 +5510,10 @@ void Application::updateWindowTitle() const { qCDebug(interfaceapp, "Application title set to: %s", title.toStdString().c_str()); #endif _window->setWindowTitle(title); + + // updateTitleWindow gets called whenever there's a change regarding the domain, so rather + // than placing this within domainChanged, it's placed here to cover the other potential cases. + DependencyManager::get< MessagesClient >()->sendLocalMessage("Toolbar-DomainChanged", ""); } void Application::clearDomainOctreeDetails() { @@ -5812,6 +5899,7 @@ void Application::registerScriptEngineWithApplicationServices(ScriptEngine* scri auto entityScriptServerLog = DependencyManager::get(); scriptEngine->registerGlobalObject("EntityScriptServerLog", entityScriptServerLog.data()); scriptEngine->registerGlobalObject("AvatarInputs", AvatarInputs::getInstance()); + scriptEngine->registerGlobalObject("ContextOverlay", DependencyManager::get().data()); qScriptRegisterMetaType(scriptEngine, OverlayIDtoScriptValue, OverlayIDfromScriptValue); @@ -6583,11 +6671,11 @@ void Application::setPreviousScriptLocation(const QString& location) { } void Application::loadScriptURLDialog() const { - auto newScript = OffscreenUi::getText(OffscreenUi::ICON_NONE, "Open and Run Script", "Script URL"); + QString newScript = OffscreenUi::getText(OffscreenUi::ICON_NONE, "Open and Run Script", "Script URL"); if (QUrl(newScript).scheme() == "atp") { OffscreenUi::warning("Error Loading Script", "Cannot load client script over ATP"); } else if (!newScript.isEmpty()) { - DependencyManager::get()->loadScript(newScript); + DependencyManager::get()->loadScript(newScript.trimmed()); } } @@ -7007,6 +7095,7 @@ void Application::updateDisplayMode() { auto oldDisplayPlugin = _displayPlugin; if (_displayPlugin) { + disconnect(_displayPluginPresentConnection); _displayPlugin->deactivate(); } @@ -7047,6 +7136,7 @@ void Application::updateDisplayMode() { _offscreenContext->makeCurrent(); getApplicationCompositor().setDisplayPlugin(newDisplayPlugin); _displayPlugin = newDisplayPlugin; + _displayPluginPresentConnection = connect(_displayPlugin.get(), &DisplayPlugin::presented, this, &Application::onPresent); offscreenUi->getDesktop()->setProperty("repositionLocked", wasRepositionLocked); } diff --git a/interface/src/Application.h b/interface/src/Application.h index cf0ae91a0f..300bd4ac02 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -55,6 +55,7 @@ #include "BandwidthRecorder.h" #include "FancyCamera.h" #include "ConnectionMonitor.h" +#include "CursorManager.h" #include "gpu/Context.h" #include "Menu.h" #include "octree/OctreePacketProcessor.h" @@ -128,8 +129,7 @@ public: virtual DisplayPluginPointer getActiveDisplayPlugin() const override; enum Event { - Present = DisplayPlugin::Present, - Paint, + Paint = QEvent::User + 1, Idle, Lambda }; @@ -165,7 +165,7 @@ public: QSize getDeviceSize() const; bool hasFocus() const; - void showCursor(const QCursor& cursor); + void showCursor(const Cursor::Icon& cursor); bool isThrottleRendering() const; @@ -400,11 +400,15 @@ public slots: void loadDomainConnectionDialog(); void showScriptLogs(); + const QString getPreferredCursor() const { return _preferredCursor.get(); } + void setPreferredCursor(const QString& cursor); + private slots: void showDesktop(); void clearDomainOctreeDetails(); void clearDomainAvatars(); void onAboutToQuit(); + void onPresent(quint32 frameCount); void resettingDomain(); @@ -451,8 +455,8 @@ private: void cleanupBeforeQuit(); - bool shouldPaint(float nsecsElapsed); - void idle(float nsecsElapsed); + bool shouldPaint(); + void idle(); void update(float deltaTime); // Various helper functions called during update() @@ -514,6 +518,7 @@ private: OffscreenGLCanvas* _offscreenContext { nullptr }; DisplayPluginPointer _displayPlugin; + QMetaObject::Connection _displayPluginPresentConnection; mutable std::mutex _displayPluginLock; InputPluginList _activeInputPlugins; @@ -564,6 +569,7 @@ private: Setting::Handle _hmdTabletBecomesToolbarSetting; Setting::Handle _preferAvatarFingerOverStylusSetting; Setting::Handle _constrainToolbarPosition; + Setting::Handle _preferredCursor; float _scaleMirror; float _rotateMirror; @@ -637,7 +643,7 @@ private: void checkChangeCursor(); mutable QMutex _changeCursorLock { QMutex::Recursive }; - QCursor _desiredCursor{ Qt::BlankCursor }; + Qt::CursorShape _desiredCursor{ Qt::BlankCursor }; bool _cursorNeedsChanging { false }; QThread* _deadlockWatchdogThread; @@ -678,7 +684,7 @@ private: QTimer _addAssetToWorldErrorTimer; FileScriptingInterface* _fileDownload; - AudioInjector* _snapshotSoundInjector { nullptr }; + AudioInjectorPointer _snapshotSoundInjector; SharedSoundPointer _snapshotSound; DisplayPluginPointer _autoSwitchDisplayModeSupportedHMDPlugin; diff --git a/interface/src/Menu.cpp b/interface/src/Menu.cpp index 8b616c3029..56bd3eb749 100644 --- a/interface/src/Menu.cpp +++ b/interface/src/Menu.cpp @@ -222,19 +222,19 @@ Menu::Menu() { cameraModeGroup->setExclusive(true); // View > First Person - cameraModeGroup->addAction(addCheckableActionToQMenuAndActionHash(viewMenu, - MenuOption::FirstPerson, 0, - true, qApp, SLOT(cameraMenuChanged()))); + cameraModeGroup->addAction(addCheckableActionToQMenuAndActionHash( + viewMenu, MenuOption::FirstPerson, Qt::CTRL | Qt::Key_F, + true, qApp, SLOT(cameraMenuChanged()))); // View > Third Person - cameraModeGroup->addAction(addCheckableActionToQMenuAndActionHash(viewMenu, - MenuOption::ThirdPerson, 0, - false, qApp, SLOT(cameraMenuChanged()))); + cameraModeGroup->addAction(addCheckableActionToQMenuAndActionHash( + viewMenu, MenuOption::ThirdPerson, Qt::CTRL | Qt::Key_G, + false, qApp, SLOT(cameraMenuChanged()))); // View > Mirror - cameraModeGroup->addAction(addCheckableActionToQMenuAndActionHash(viewMenu, - MenuOption::FullscreenMirror, 0, - false, qApp, SLOT(cameraMenuChanged()))); + cameraModeGroup->addAction(addCheckableActionToQMenuAndActionHash( + viewMenu, MenuOption::FullscreenMirror, Qt::CTRL | Qt::Key_H, + false, qApp, SLOT(cameraMenuChanged()))); // View > Independent [advanced] cameraModeGroup->addAction(addCheckableActionToQMenuAndActionHash(viewMenu, @@ -680,7 +680,7 @@ Menu::Menu() { // Developer > Physics >>> MenuWrapper* physicsOptionsMenu = developerMenu->addMenu("Physics"); { - auto drawStatusConfig = qApp->getRenderEngine()->getConfiguration()->getConfig(); + auto drawStatusConfig = qApp->getRenderEngine()->getConfiguration()->getConfig("RenderMainView.DrawStatus"); addCheckableActionToQMenuAndActionHash(physicsOptionsMenu, MenuOption::PhysicsShowOwned, 0, false, drawStatusConfig, SLOT(setShowNetwork(bool))); } diff --git a/interface/src/SecondaryCamera.cpp b/interface/src/SecondaryCamera.cpp index f59d2fcc7a..56b8b3ef85 100644 --- a/interface/src/SecondaryCamera.cpp +++ b/interface/src/SecondaryCamera.cpp @@ -9,9 +9,11 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // +#include "Application.h" #include "SecondaryCamera.h" #include #include +#include using RenderArgsPointer = std::shared_ptr; @@ -27,39 +29,32 @@ void MainRenderTask::build(JobModel& task, const render::Varying& inputs, render } } -void SecondaryCameraRenderTaskConfig::resetSize(int width, int height) { // FIXME: Add an arg here for "destinationFramebuffer" - bool wasEnabled = isEnabled(); - setEnabled(false); - auto textureCache = DependencyManager::get(); - textureCache->resetSpectatorCameraFramebuffer(width, height); // FIXME: Call the correct reset function based on the "destinationFramebuffer" arg - setEnabled(wasEnabled); -} - -void SecondaryCameraRenderTaskConfig::resetSizeSpectatorCamera(int width, int height) { // Carefully adjust the framebuffer / texture. - resetSize(width, height); -} - -class BeginSecondaryCameraFrame { // Changes renderContext for our framebuffer and and view. +class SecondaryCameraJob { // Changes renderContext for our framebuffer and view. + QUuid _attachedEntityId{}; glm::vec3 _position{}; glm::quat _orientation{}; float _vFoV{}; float _nearClipPlaneDistance{}; float _farClipPlaneDistance{}; + EntityPropertyFlags _attachedEntityPropertyFlags; + QSharedPointer _entityScriptingInterface; public: - using Config = BeginSecondaryCameraFrameConfig; - using JobModel = render::Job::ModelO; - BeginSecondaryCameraFrame() { + using Config = SecondaryCameraJobConfig; + using JobModel = render::Job::ModelO; + SecondaryCameraJob() { _cachedArgsPointer = std::make_shared(_cachedArgs); + _entityScriptingInterface = DependencyManager::get(); + _attachedEntityPropertyFlags += PROP_POSITION; + _attachedEntityPropertyFlags += PROP_ROTATION; } void configure(const Config& config) { - if (config.enabled || config.alwaysEnabled) { - _position = config.position; - _orientation = config.orientation; - _vFoV = config.vFoV; - _nearClipPlaneDistance = config.nearClipPlaneDistance; - _farClipPlaneDistance = config.farClipPlaneDistance; - } + _attachedEntityId = config.attachedEntityId; + _position = config.position; + _orientation = config.orientation; + _vFoV = config.vFoV; + _nearClipPlaneDistance = config.nearClipPlaneDistance; + _farClipPlaneDistance = config.farClipPlaneDistance; } void run(const render::RenderContextPointer& renderContext, RenderArgsPointer& cachedArgs) { @@ -83,8 +78,14 @@ public: }); auto srcViewFrustum = args->getViewFrustum(); - srcViewFrustum.setPosition(_position); - srcViewFrustum.setOrientation(_orientation); + if (!_attachedEntityId.isNull()) { + EntityItemProperties entityProperties = _entityScriptingInterface->getEntityProperties(_attachedEntityId, _attachedEntityPropertyFlags); + srcViewFrustum.setPosition(entityProperties.getPosition()); + srcViewFrustum.setOrientation(entityProperties.getRotation()); + } else { + srcViewFrustum.setPosition(_position); + srcViewFrustum.setOrientation(_orientation); + } srcViewFrustum.setProjection(glm::perspective(glm::radians(_vFoV), ((float)args->_viewport.z / (float)args->_viewport.w), _nearClipPlaneDistance, _farClipPlaneDistance)); // Without calculating the bound planes, the secondary camera will use the same culling frustum as the main camera, // which is not what we want here. @@ -99,6 +100,41 @@ protected: RenderArgsPointer _cachedArgsPointer; }; +void SecondaryCameraJobConfig::setPosition(glm::vec3 pos) { + if (attachedEntityId.isNull()) { + position = pos; + emit dirty(); + } else { + qDebug() << "ERROR: Cannot set position of SecondaryCamera while attachedEntityId is set."; + } +} + +void SecondaryCameraJobConfig::setOrientation(glm::quat orient) { + if (attachedEntityId.isNull()) { + orientation = orient; + emit dirty(); + } else { + qDebug() << "ERROR: Cannot set orientation of SecondaryCamera while attachedEntityId is set."; + } +} + +void SecondaryCameraJobConfig::enableSecondaryCameraRenderConfigs(bool enabled) { + qApp->getRenderEngine()->getConfiguration()->getConfig()->setEnabled(enabled); + setEnabled(enabled); +} + +void SecondaryCameraJobConfig::resetSizeSpectatorCamera(int width, int height) { // Carefully adjust the framebuffer / texture. + qApp->getRenderEngine()->getConfiguration()->getConfig()->resetSize(width, height); +} + +void SecondaryCameraRenderTaskConfig::resetSize(int width, int height) { // FIXME: Add an arg here for "destinationFramebuffer" + bool wasEnabled = isEnabled(); + setEnabled(false); + auto textureCache = DependencyManager::get(); + textureCache->resetSpectatorCameraFramebuffer(width, height); // FIXME: Call the correct reset function based on the "destinationFramebuffer" arg + setEnabled(wasEnabled); +} + class EndSecondaryCameraFrame { // Restores renderContext. public: using JobModel = render::Job::ModelI; @@ -119,7 +155,7 @@ public: }; void SecondaryCameraRenderTask::build(JobModel& task, const render::Varying& inputs, render::Varying& outputs, render::CullFunctor cullFunctor) { - const auto cachedArg = task.addJob("BeginSecondaryCamera"); + const auto cachedArg = task.addJob("SecondaryCamera"); const auto items = task.addJob("FetchCullSort", cullFunctor); assert(items.canCast()); task.addJob("RenderDeferredTask", items); diff --git a/interface/src/SecondaryCamera.h b/interface/src/SecondaryCamera.h index 5ad19c9614..0941959c0a 100644 --- a/interface/src/SecondaryCamera.h +++ b/interface/src/SecondaryCamera.h @@ -28,34 +28,40 @@ public: void build(JobModel& task, const render::Varying& inputs, render::Varying& outputs, render::CullFunctor cullFunctor, bool isDeferred = true); }; -class BeginSecondaryCameraFrameConfig : public render::Task::Config { // Exposes secondary camera parameters to JavaScript. +class SecondaryCameraJobConfig : public render::Task::Config { // Exposes secondary camera parameters to JavaScript. Q_OBJECT - Q_PROPERTY(glm::vec3 position MEMBER position NOTIFY dirty) // of viewpoint to render from - Q_PROPERTY(glm::quat orientation MEMBER orientation NOTIFY dirty) // of viewpoint to render from + Q_PROPERTY(QUuid attachedEntityId MEMBER attachedEntityId NOTIFY dirty) // entity whose properties define camera position and orientation + Q_PROPERTY(glm::vec3 position READ getPosition WRITE setPosition) // of viewpoint to render from + Q_PROPERTY(glm::quat orientation READ getOrientation WRITE setOrientation) // of viewpoint to render from Q_PROPERTY(float vFoV MEMBER vFoV NOTIFY dirty) // Secondary camera's vertical field of view. In degrees. Q_PROPERTY(float nearClipPlaneDistance MEMBER nearClipPlaneDistance NOTIFY dirty) // Secondary camera's near clip plane distance. In meters. Q_PROPERTY(float farClipPlaneDistance MEMBER farClipPlaneDistance NOTIFY dirty) // Secondary camera's far clip plane distance. In meters. public: + QUuid attachedEntityId{}; glm::vec3 position{}; glm::quat orientation{}; - float vFoV{ 45.0f }; - float nearClipPlaneDistance{ 0.1f }; - float farClipPlaneDistance{ 100.0f }; - BeginSecondaryCameraFrameConfig() : render::Task::Config(false) {} + float vFoV{ DEFAULT_FIELD_OF_VIEW_DEGREES }; + float nearClipPlaneDistance{ DEFAULT_NEAR_CLIP }; + float farClipPlaneDistance{ DEFAULT_FAR_CLIP }; + SecondaryCameraJobConfig() : render::Task::Config(false) {} signals: void dirty(); +public slots: + glm::vec3 getPosition() { return position; } + void setPosition(glm::vec3 pos); + glm::quat getOrientation() { return orientation; } + void setOrientation(glm::quat orient); + void enableSecondaryCameraRenderConfigs(bool enabled); + void resetSizeSpectatorCamera(int width, int height); }; class SecondaryCameraRenderTaskConfig : public render::Task::Config { Q_OBJECT public: SecondaryCameraRenderTaskConfig() : render::Task::Config(false) {} -private: void resetSize(int width, int height); signals: void dirty(); -public slots: - void resetSizeSpectatorCamera(int width, int height); }; class SecondaryCameraRenderTask { diff --git a/interface/src/avatar/AvatarManager.cpp b/interface/src/avatar/AvatarManager.cpp index c46d61cf68..bd545c64e0 100644 --- a/interface/src/avatar/AvatarManager.cpp +++ b/interface/src/avatar/AvatarManager.cpp @@ -63,7 +63,6 @@ AvatarManager::AvatarManager(QObject* parent) : packetReceiver.registerListener(PacketType::BulkAvatarData, this, "processAvatarDataPacket"); packetReceiver.registerListener(PacketType::KillAvatar, this, "processKillAvatar"); packetReceiver.registerListener(PacketType::AvatarIdentity, this, "processAvatarIdentityPacket"); - packetReceiver.registerListener(PacketType::ExitingSpaceBubble, this, "processExitingSpaceBubble"); // when we hear that the user has ignored an avatar by session UUID // immediately remove that avatar instead of waiting for the absence of packets from avatar mixer @@ -320,9 +319,6 @@ void AvatarManager::handleRemovedAvatar(const AvatarSharedPointer& removedAvatar if (removalReason == KillAvatarReason::TheirAvatarEnteredYourBubble) { emit DependencyManager::get()->enteredIgnoreRadius(); - } - if (removalReason == KillAvatarReason::TheirAvatarEnteredYourBubble || removalReason == YourAvatarEnteredTheirBubble) { - DependencyManager::get()->radiusIgnoreNodeBySessionID(avatar->getSessionUUID(), true); } else if (removalReason == KillAvatarReason::AvatarDisconnected) { // remove from node sets, if present DependencyManager::get()->removeFromIgnoreMuteSets(avatar->getSessionUUID()); @@ -434,8 +430,7 @@ void AvatarManager::handleCollisionEvents(const CollisionEvents& collisionEvents // but most avatars are roughly the same size, so let's not be so fancy yet. const float AVATAR_STRETCH_FACTOR = 1.0f; - - _collisionInjectors.remove_if([](QPointer& injector) { + _collisionInjectors.remove_if([](const AudioInjectorPointer& injector) { return !injector || injector->isFinished(); }); diff --git a/interface/src/avatar/AvatarManager.h b/interface/src/avatar/AvatarManager.h index f1e71f7367..30801807d6 100644 --- a/interface/src/avatar/AvatarManager.h +++ b/interface/src/avatar/AvatarManager.h @@ -22,11 +22,11 @@ #include #include #include +#include #include "AvatarMotionState.h" #include "MyAvatar.h" -class AudioInjector; class AvatarManager : public AvatarHashMap { Q_OBJECT @@ -104,7 +104,7 @@ private: std::shared_ptr _myAvatar; quint64 _lastSendAvatarDataTime = 0; // Controls MyAvatar send data rate. - std::list> _collisionInjectors; + std::list _collisionInjectors; RateCounter<> _myAvatarSendRate; int _numAvatarsUpdated { 0 }; diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index f6fb87dad9..474aa48e63 100755 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -69,8 +69,8 @@ const float MAX_BOOST_SPEED = 0.5f * MAX_WALKING_SPEED; // action motor gets add const float MIN_AVATAR_SPEED = 0.05f; const float MIN_AVATAR_SPEED_SQUARED = MIN_AVATAR_SPEED * MIN_AVATAR_SPEED; // speed is set to zero below this -const float YAW_SPEED_DEFAULT = 120.0f; // degrees/sec -const float PITCH_SPEED_DEFAULT = 90.0f; // degrees/sec +const float YAW_SPEED_DEFAULT = 100.0f; // degrees/sec +const float PITCH_SPEED_DEFAULT = 75.0f; // degrees/sec // TODO: normalize avatar speed for standard avatar size, then scale all motion logic // to properly follow avatar size. @@ -255,6 +255,13 @@ MyAvatar::~MyAvatar() { _lookAtTargetAvatar.reset(); } +void MyAvatar::setDominantHand(const QString& hand) { + if (hand == DOMINANT_LEFT_HAND || hand == DOMINANT_RIGHT_HAND) { + _dominantHand = hand; + emit dominantHandChanged(_dominantHand); + } +} + void MyAvatar::registerMetaTypes(QScriptEngine* engine) { QScriptValue value = engine->newQObject(this, QScriptEngine::QtOwnership, QScriptEngine::ExcludeDeleteLater | QScriptEngine::ExcludeChildObjects); engine->globalObject().setProperty("MyAvatar", value); @@ -295,7 +302,7 @@ void MyAvatar::simulateAttachments(float deltaTime) { // don't update attachments here, do it in harvestResultsFromPhysicsSimulation() } -QByteArray MyAvatar::toByteArrayStateful(AvatarDataDetail dataDetail) { +QByteArray MyAvatar::toByteArrayStateful(AvatarDataDetail dataDetail, bool dropFaceTracking) { CameraMode mode = qApp->getCamera().getMode(); _globalPosition = getPosition(); // This might not be right! Isn't the capsule local offset in avatar space, and don't we need to add the radius to the y as well? -HRS 5/26/17 @@ -607,7 +614,7 @@ void MyAvatar::simulate(float deltaTime) { MovingEntitiesOperator moveOperator(entityTree); forEachDescendant([&](SpatiallyNestablePointer object) { // if the queryBox has changed, tell the entity-server - if (object->computePuffedQueryAACube() && object->getNestableType() == NestableType::Entity) { + if (object->getNestableType() == NestableType::Entity && object->checkAndMaybeUpdateQueryAACube()) { EntityItemPointer entity = std::static_pointer_cast(object); bool success; AACube newCube = entity->getQueryAACube(success); @@ -926,6 +933,7 @@ void MyAvatar::saveData() { Settings settings; settings.beginGroup("Avatar"); + settings.setValue("dominantHand", _dominantHand); settings.setValue("headPitch", getHead()->getBasePitch()); settings.setValue("scale", _targetScale); @@ -1075,9 +1083,6 @@ void MyAvatar::loadData() { getHead()->setBasePitch(loadSetting(settings, "headPitch", 0.0f)); - _targetScale = loadSetting(settings, "scale", 1.0f); - setScale(glm::vec3(_targetScale)); - _prefOverrideAnimGraphUrl.set(QUrl(settings.value("animGraphURL", "").toString())); _fullAvatarURLFromPreferences = settings.value("fullAvatarURL", AvatarData::defaultFullAvatarModelUrl()).toUrl(); _fullAvatarModelName = settings.value("fullAvatarModelName", DEFAULT_FULL_AVATAR_MODEL_NAME).toString(); @@ -1125,7 +1130,7 @@ void MyAvatar::loadData() { setCollisionSoundURL(settings.value("collisionSoundURL", DEFAULT_AVATAR_COLLISION_SOUND_URL).toString()); setSnapTurn(settings.value("useSnapTurn", _useSnapTurn).toBool()); setClearOverlayWhenMoving(settings.value("clearOverlayWhenMoving", _clearOverlayWhenMoving).toBool()); - + setDominantHand(settings.value("dominantHand", _dominantHand).toString().toLower()); settings.endGroup(); setEnableMeshVisible(Menu::getInstance()->isOptionChecked(MenuOption::MeshVisible)); @@ -1294,7 +1299,7 @@ eyeContactTarget MyAvatar::getEyeContactTarget() { } glm::vec3 MyAvatar::getDefaultEyePosition() const { - return getPosition() + getWorldAlignedOrientation() * Quaternions::Y_180 * _skeletonModel->getDefaultEyeModelPosition(); + return getPosition() + getOrientation() * Quaternions::Y_180 * _skeletonModel->getDefaultEyeModelPosition(); } const float SCRIPT_PRIORITY = 1.0f + 1.0f; @@ -1589,9 +1594,14 @@ void MyAvatar::updateMotors() { motorRotation = getMyHead()->getHeadOrientation(); } else { // non-hovering = walking: follow camera twist about vertical but not lift - // so we decompose camera's rotation and store the twist part in motorRotation + // we decompose camera's rotation and store the twist part in motorRotation + // however, we need to perform the decomposition in the avatar-frame + // using the local UP axis and then transform back into world-frame + glm::quat orientation = getOrientation(); + glm::quat headOrientation = glm::inverse(orientation) * getMyHead()->getHeadOrientation(); // avatar-frame glm::quat liftRotation; - swingTwistDecomposition(getMyHead()->getHeadOrientation(), _worldUpDirection, liftRotation, motorRotation); + swingTwistDecomposition(headOrientation, Vectors::UNIT_Y, liftRotation, motorRotation); + motorRotation = orientation * motorRotation; } const float DEFAULT_MOTOR_TIMESCALE = 0.2f; const float INVALID_MOTOR_TIMESCALE = 1.0e6f; @@ -1645,11 +1655,31 @@ void MyAvatar::prepareForPhysicsSimulation() { _prePhysicsRoomPose = AnimPose(_sensorToWorldMatrix); } +// There are a number of possible strategies for this set of tools through endRender, below. +void MyAvatar::nextAttitude(glm::vec3 position, glm::quat orientation) { + bool success; + Transform trans = getTransform(success); + if (!success) { + qCWarning(interfaceapp) << "Warning -- MyAvatar::nextAttitude failed"; + return; + } + trans.setTranslation(position); + trans.setRotation(orientation); + SpatiallyNestable::setTransform(trans, success); + if (!success) { + qCWarning(interfaceapp) << "Warning -- MyAvatar::nextAttitude failed"; + } + updateAttitude(orientation); +} + void MyAvatar::harvestResultsFromPhysicsSimulation(float deltaTime) { - glm::vec3 position = getPosition(); - glm::quat orientation = getOrientation(); + glm::vec3 position; + glm::quat orientation; if (_characterController.isEnabledAndReady()) { _characterController.getPositionAndOrientation(position, orientation); + } else { + position = getPosition(); + orientation = getOrientation(); } nextAttitude(position, orientation); _bodySensorMatrix = _follow.postPhysicsUpdate(*this, _bodySensorMatrix); @@ -2227,6 +2257,14 @@ void MyAvatar::clampScaleChangeToDomainLimits(float desiredScale) { qCDebug(interfaceapp, "Changed scale to %f", (double)_targetScale); } +float MyAvatar::getDomainMinScale() { + return _domainMinimumScale; +} + +float MyAvatar::getDomainMaxScale() { + return _domainMaximumScale; +} + void MyAvatar::increaseSize() { // make sure we're starting from an allowable scale clampTargetScaleToDomainLimits(); @@ -2274,17 +2312,27 @@ void MyAvatar::restrictScaleFromDomainSettings(const QJsonObject& domainSettings if (_domainMinimumScale > _domainMaximumScale) { std::swap(_domainMinimumScale, _domainMaximumScale); } + // Set avatar current scale + Settings settings; + settings.beginGroup("Avatar"); + _targetScale = loadSetting(settings, "scale", 1.0f); - qCDebug(interfaceapp, "This domain requires a minimum avatar scale of %f and a maximum avatar scale of %f", - (double)_domainMinimumScale, (double)_domainMaximumScale); + qCDebug(interfaceapp) << "This domain requires a minimum avatar scale of " << _domainMinimumScale + << " and a maximum avatar scale of " << _domainMaximumScale + << ". Current avatar scale is " << _targetScale; // debug to log if this avatar's scale in this domain will be clamped - auto clampedScale = glm::clamp(_targetScale, _domainMinimumScale, _domainMaximumScale); + float clampedScale = glm::clamp(_targetScale, _domainMinimumScale, _domainMaximumScale); if (_targetScale != clampedScale) { - qCDebug(interfaceapp, "Avatar scale will be clamped to %f because %f is not allowed by current domain", - (double)clampedScale, (double)_targetScale); + qCDebug(interfaceapp) << "Current avatar scale is clamped to " << clampedScale + << " because " << _targetScale << " is not allowed by current domain"; + // The current scale of avatar should not be more than domain's max_avatar_scale and not less than domain's min_avatar_scale . + _targetScale = clampedScale; } + + setScale(glm::vec3(_targetScale)); + settings.endGroup(); } void MyAvatar::clearScaleRestriction() { @@ -2826,7 +2874,8 @@ bool MyAvatar::FollowHelper::shouldActivateVertical(const MyAvatar& myAvatar, co void MyAvatar::FollowHelper::prePhysicsUpdate(MyAvatar& myAvatar, const glm::mat4& desiredBodyMatrix, const glm::mat4& currentBodyMatrix, bool hasDriveInput) { - if (myAvatar.getHMDLeanRecenterEnabled()) { + if (myAvatar.getHMDLeanRecenterEnabled() && + qApp->getCamera().getMode() != CAMERA_MODE_MIRROR) { if (!isActive(Rotation) && (shouldActivateRotation(myAvatar, desiredBodyMatrix, currentBodyMatrix) || hasDriveInput)) { activate(Rotation); } diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index 4d599230fb..fa5ffc4b93 100644 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -43,6 +43,7 @@ enum AudioListenerMode { FROM_CAMERA, CUSTOM }; + Q_DECLARE_METATYPE(AudioListenerMode); class MyAvatar : public Avatar { @@ -141,6 +142,9 @@ class MyAvatar : public Avatar { Q_PROPERTY(float hmdRollControlDeadZone READ getHMDRollControlDeadZone WRITE setHMDRollControlDeadZone) Q_PROPERTY(float hmdRollControlRate READ getHMDRollControlRate WRITE setHMDRollControlRate) + const QString DOMINANT_LEFT_HAND = "left"; + const QString DOMINANT_RIGHT_HAND = "right"; + public: enum DriveKeys { TRANSLATE_X = 0, @@ -339,6 +343,9 @@ public: Q_INVOKABLE bool getClearOverlayWhenMoving() const { return _clearOverlayWhenMoving; } Q_INVOKABLE void setClearOverlayWhenMoving(bool on) { _clearOverlayWhenMoving = on; } + Q_INVOKABLE void setDominantHand(const QString& hand); + Q_INVOKABLE QString getDominantHand() const { return _dominantHand; } + Q_INVOKABLE void setHMDLeanRecenterEnabled(bool value) { _hmdLeanRecenterEnabled = value; } Q_INVOKABLE bool getHMDLeanRecenterEnabled() const { return _hmdLeanRecenterEnabled; } @@ -424,7 +431,6 @@ public: Q_INVOKABLE QString getFullAvatarModelName() const { return _fullAvatarModelName; } void resetFullAvatarURL(); - virtual void setAttachmentData(const QVector& attachmentData) override; MyCharacterController* getCharacterController() { return &_characterController; } @@ -432,6 +438,7 @@ public: void updateMotors(); void prepareForPhysicsSimulation(); + void nextAttitude(glm::vec3 position, glm::quat orientation); // Can be safely called at any time. void harvestResultsFromPhysicsSimulation(float deltaTime); const QString& getCollisionSoundURL() { return _collisionSoundURL; } @@ -551,11 +558,12 @@ public: Q_INVOKABLE bool isUp(const glm::vec3& direction) { return glm::dot(direction, _worldUpDirection) > 0.0f; }; // true iff direction points up wrt avatar's definition of up. Q_INVOKABLE bool isDown(const glm::vec3& direction) { return glm::dot(direction, _worldUpDirection) < 0.0f; }; - public slots: void increaseSize(); void decreaseSize(); void resetSize(); + float getDomainMinScale(); + float getDomainMaxScale(); void goToLocation(const glm::vec3& newPosition, bool hasOrientation = false, const glm::quat& newOrientation = glm::quat(), @@ -607,12 +615,13 @@ signals: void wentAway(); void wentActive(); void skeletonChanged(); + void dominantHandChanged(const QString& hand); private: bool requiresSafeLanding(const glm::vec3& positionIn, glm::vec3& positionOut); - virtual QByteArray toByteArrayStateful(AvatarDataDetail dataDetail) override; + virtual QByteArray toByteArrayStateful(AvatarDataDetail dataDetail, bool dropFaceTracking) override; void simulate(float deltaTime); void updateFromTrackers(float deltaTime); @@ -718,6 +727,7 @@ private: QUrl _fstAnimGraphOverrideUrl; bool _useSnapTurn { true }; bool _clearOverlayWhenMoving { true }; + QString _dominantHand { DOMINANT_RIGHT_HAND }; const float ROLL_CONTROL_DEAD_ZONE_DEFAULT = 8.0f; // deg const float ROLL_CONTROL_RATE_DEFAULT = 2.5f; // deg/sec/deg diff --git a/interface/src/avatar/MyHead.cpp b/interface/src/avatar/MyHead.cpp index 9f2d080cd6..7fc6b9fa26 100644 --- a/interface/src/avatar/MyHead.cpp +++ b/interface/src/avatar/MyHead.cpp @@ -29,7 +29,7 @@ MyHead::MyHead(MyAvatar* owningAvatar) : Head(owningAvatar) { glm::quat MyHead::getHeadOrientation() const { // NOTE: Head::getHeadOrientation() is not used for orienting the camera "view" while in Oculus mode, so // you may wonder why this code is here. This method will be called while in Oculus mode to determine how - // to change the driving direction while in Oculus mode. It is used to support driving toward where you're + // to change the driving direction while in Oculus mode. It is used to support driving toward where your // head is looking. Note that in oculus mode, your actual camera view and where your head is looking is not // always the same. @@ -39,7 +39,7 @@ glm::quat MyHead::getHeadOrientation() const { return headPose.rotation * Quaternions::Y_180; } - return myAvatar->getWorldAlignedOrientation() * glm::quat(glm::radians(glm::vec3(_basePitch, 0.0f, 0.0f))); + return myAvatar->getOrientation() * glm::quat(glm::radians(glm::vec3(_basePitch, 0.0f, 0.0f))); } void MyHead::simulate(float deltaTime) { diff --git a/interface/src/scripting/Audio.cpp b/interface/src/scripting/Audio.cpp index 8125f9a9f0..9719c23885 100644 --- a/interface/src/scripting/Audio.cpp +++ b/interface/src/scripting/Audio.cpp @@ -133,4 +133,12 @@ void Audio::setReverb(bool enable) { void Audio::setReverbOptions(const AudioEffectOptions* options) { DependencyManager::get()->setReverbOptions(options); -} \ No newline at end of file +} + +void Audio::setInputDevice(const QAudioDeviceInfo& device) { + _devices.chooseInputDevice(device); +} + +void Audio::setOutputDevice(const QAudioDeviceInfo& device) { + _devices.chooseOutputDevice(device); +} diff --git a/interface/src/scripting/Audio.h b/interface/src/scripting/Audio.h index ca89521489..bd40de4303 100644 --- a/interface/src/scripting/Audio.h +++ b/interface/src/scripting/Audio.h @@ -50,6 +50,8 @@ public: void showMicMeter(bool show); void setInputVolume(float volume); + Q_INVOKABLE void setInputDevice(const QAudioDeviceInfo& device); + Q_INVOKABLE void setOutputDevice(const QAudioDeviceInfo& device); Q_INVOKABLE void setReverb(bool enable); Q_INVOKABLE void setReverbOptions(const AudioEffectOptions* options); @@ -79,7 +81,7 @@ private: float _inputVolume { 1.0f }; float _inputLevel { 0.0f }; bool _isMuted { false }; - bool _enableNoiseReduction; + bool _enableNoiseReduction { true }; // Match default value of AudioClient::_isNoiseGateEnabled. bool _contextIsHMD { false }; AudioDevices* getDevices() { return &_devices; } diff --git a/interface/src/scripting/AudioDevices.cpp b/interface/src/scripting/AudioDevices.cpp index c3958f2ad9..c06f921a7f 100644 --- a/interface/src/scripting/AudioDevices.cpp +++ b/interface/src/scripting/AudioDevices.cpp @@ -12,6 +12,7 @@ #include #include +#include #include "AudioDevices.h" @@ -39,15 +40,32 @@ Setting::Handle& getSetting(bool contextIsHMD, QAudio::Mode mode) { enum AudioDeviceRole { DisplayRole = Qt::DisplayRole, CheckStateRole = Qt::CheckStateRole, - PeakRole = Qt::UserRole + PeakRole = Qt::UserRole, + InfoRole = Qt::UserRole + 1 }; QHash AudioDeviceList::_roles { { DisplayRole, "display" }, { CheckStateRole, "selected" }, - { PeakRole, "peak" } + { PeakRole, "peak" }, + { InfoRole, "info" } }; +static QString getTargetDevice(bool hmd, QAudio::Mode mode) { + QString deviceName; + auto& setting = getSetting(hmd, mode); + if (setting.isSet()) { + deviceName = setting.get(); + } else if (hmd) { + if (mode == QAudio::AudioInput) { + deviceName = qApp->getActiveDisplayPlugin()->getPreferredAudioInDevice(); + } else { // if (_mode == QAudio::AudioOutput) + deviceName = qApp->getActiveDisplayPlugin()->getPreferredAudioOutDevice(); + } + } + return deviceName; +} + Qt::ItemFlags AudioDeviceList::_flags { Qt::ItemIsSelectable | Qt::ItemIsEnabled }; QVariant AudioDeviceList::data(const QModelIndex& index, int role) const { @@ -56,85 +74,44 @@ QVariant AudioDeviceList::data(const QModelIndex& index, int role) const { } if (role == DisplayRole) { - return _devices.at(index.row())->display; + return _devices.at(index.row()).display; } else if (role == CheckStateRole) { - return _devices.at(index.row())->selected; + return _devices.at(index.row()).selected; + } else if (role == InfoRole) { + return QVariant::fromValue(_devices.at(index.row()).info); } else { return QVariant(); } } QVariant AudioInputDeviceList::data(const QModelIndex& index, int role) const { - if (index.isValid() && index.row() < rowCount() && role == PeakRole) { - return std::static_pointer_cast(_devices.at(index.row()))->peak; + if (!index.isValid() || index.row() >= rowCount()) { + return QVariant(); + } + + if (role == PeakRole) { + return std::static_pointer_cast(_devices.at(index.row())).peak; } else { return AudioDeviceList::data(index, role); } } -bool AudioDeviceList::setData(const QModelIndex& index, const QVariant& value, int role) { - if (!index.isValid() || index.row() >= rowCount() || role != CheckStateRole) { - return false; - } +void AudioDeviceList::resetDevice(bool contextIsHMD) { + auto client = DependencyManager::get().data(); + QString deviceName = getTargetDevice(contextIsHMD, _mode); + // FIXME can't use blocking connections here, so we can't determine whether the switch succeeded or not + // We need to have the AudioClient emit signals on switch success / failure + QMetaObject::invokeMethod(client, "switchAudioDevice", + Q_ARG(QAudio::Mode, _mode), Q_ARG(QString, deviceName)); - // only allow switching to a new device, not deactivating an in-use device - auto selected = value.toBool(); - if (!selected) { - return false; - } - - return setDevice(index.row(), true); -} - -bool AudioDeviceList::setDevice(int row, bool fromUser) { - bool success = false; - auto& device = _devices[row]; - _userSelection = fromUser; - - // skip if already selected - if (!device->selected) { - auto client = DependencyManager::get(); - QMetaObject::invokeMethod(client.data(), "switchAudioDevice", - Q_ARG(QAudio::Mode, _mode), - Q_ARG(const QAudioDeviceInfo&, device->info)); - - if (success) { - device->selected = true; - if (fromUser) { - emit deviceSelected(device->info, _selectedDevice); - } - emit deviceChanged(device->info); - } - } - - emit dataChanged(createIndex(0, 0), createIndex(rowCount() - 1, 0)); - return success; -} - -void AudioDeviceList::resetDevice(bool contextIsHMD, const QString& device) { - bool success { false }; - - // try to set the last selected device - if (!device.isNull()) { - auto i = 0; - for (; i < rowCount(); ++i) { - if (device == _devices[i]->info.deviceName()) { - break; - } - } - if (i < rowCount()) { - success = setDevice(i, false); - } - - // the selection failed - reset it - if (!success) { - emit deviceSelected(); - } - } +#if 0 + bool switchResult = false; + QMetaObject::invokeMethod(client, "switchAudioDevice", Qt::BlockingQueuedConnection, + Q_RETURN_ARG(bool, switchResult), + Q_ARG(QAudio::Mode, _mode), Q_ARG(QString, deviceName)); // try to set to the default device for this mode - if (!success) { - auto client = DependencyManager::get().data(); + if (!switchResult) { if (contextIsHMD) { QString deviceName; if (_mode == QAudio::AudioInput) { @@ -150,12 +127,12 @@ void AudioDeviceList::resetDevice(bool contextIsHMD, const QString& device) { QMetaObject::invokeMethod(client, "switchAudioDevice", Q_ARG(QAudio::Mode, _mode)); } } +#endif } void AudioDeviceList::onDeviceChanged(const QAudioDeviceInfo& device) { auto oldDevice = _selectedDevice; _selectedDevice = device; - QModelIndex index; for (auto i = 0; i < rowCount(); ++i) { AudioDevice& device = *_devices[i]; @@ -164,15 +141,9 @@ void AudioDeviceList::onDeviceChanged(const QAudioDeviceInfo& device) { device.selected = false; } else if (device.info == _selectedDevice) { device.selected = true; - index = createIndex(i, 0); } } - if (_userSelection) { - _userSelection = false; - emit deviceSelected(_selectedDevice, oldDevice); - } - emit deviceChanged(_selectedDevice); emit dataChanged(createIndex(0, 0), createIndex(rowCount() - 1, 0)); } @@ -237,21 +208,11 @@ AudioDevices::AudioDevices(bool& contextIsHMD) : _contextIsHMD(contextIsHMD) { _outputs.onDeviceChanged(client->getActiveAudioDevice(QAudio::AudioOutput)); _inputs.onDevicesChanged(client->getAudioDevices(QAudio::AudioInput)); _outputs.onDevicesChanged(client->getAudioDevices(QAudio::AudioOutput)); - - connect(&_inputs, &AudioDeviceList::deviceSelected, [&](const QAudioDeviceInfo& device, const QAudioDeviceInfo& previousDevice) { - onDeviceSelected(QAudio::AudioInput, device, previousDevice); - }); - connect(&_outputs, &AudioDeviceList::deviceSelected, [&](const QAudioDeviceInfo& device, const QAudioDeviceInfo& previousDevice) { - onDeviceSelected(QAudio::AudioOutput, device, previousDevice); - }); } void AudioDevices::onContextChanged(const QString& context) { - auto input = getSetting(_contextIsHMD, QAudio::AudioInput).get(); - auto output = getSetting(_contextIsHMD, QAudio::AudioOutput).get(); - - _inputs.resetDevice(_contextIsHMD, input); - _outputs.resetDevice(_contextIsHMD, output); + _inputs.resetDevice(_contextIsHMD); + _outputs.resetDevice(_contextIsHMD); } void AudioDevices::onDeviceSelected(QAudio::Mode mode, const QAudioDeviceInfo& device, const QAudioDeviceInfo& previousDevice) { @@ -292,29 +253,43 @@ void AudioDevices::onDeviceSelected(QAudio::Mode mode, const QAudioDeviceInfo& d void AudioDevices::onDeviceChanged(QAudio::Mode mode, const QAudioDeviceInfo& device) { if (mode == QAudio::AudioInput) { + if (_requestedInputDevice == device) { + onDeviceSelected(QAudio::AudioInput, device, _inputs._selectedDevice); + _requestedInputDevice = QAudioDeviceInfo(); + } _inputs.onDeviceChanged(device); } else { // if (mode == QAudio::AudioOutput) + if (_requestedOutputDevice == device) { + onDeviceSelected(QAudio::AudioOutput, device, _outputs._selectedDevice); + _requestedOutputDevice = QAudioDeviceInfo(); + } _outputs.onDeviceChanged(device); } } void AudioDevices::onDevicesChanged(QAudio::Mode mode, const QList& devices) { - static bool initialized { false }; - auto initialize = [&]{ - if (initialized) { - onContextChanged(QString()); - } else { - initialized = true; - } - }; - + static std::once_flag once; if (mode == QAudio::AudioInput) { _inputs.onDevicesChanged(devices); - static std::once_flag inputFlag; - std::call_once(inputFlag, initialize); } else { // if (mode == QAudio::AudioOutput) _outputs.onDevicesChanged(devices); - static std::once_flag outputFlag; - std::call_once(outputFlag, initialize); } + std::call_once(once, [&] { onContextChanged(QString()); }); +} + + +void AudioDevices::chooseInputDevice(const QAudioDeviceInfo& device) { + auto client = DependencyManager::get(); + _requestedInputDevice = device; + QMetaObject::invokeMethod(client.data(), "switchAudioDevice", + Q_ARG(QAudio::Mode, QAudio::AudioInput), + Q_ARG(const QAudioDeviceInfo&, device)); +} + +void AudioDevices::chooseOutputDevice(const QAudioDeviceInfo& device) { + auto client = DependencyManager::get(); + _requestedOutputDevice = device; + QMetaObject::invokeMethod(client.data(), "switchAudioDevice", + Q_ARG(QAudio::Mode, QAudio::AudioOutput), + Q_ARG(const QAudioDeviceInfo&, device)); } diff --git a/interface/src/scripting/AudioDevices.h b/interface/src/scripting/AudioDevices.h index 897feb63e3..4c1820e9c8 100644 --- a/interface/src/scripting/AudioDevices.h +++ b/interface/src/scripting/AudioDevices.h @@ -44,14 +44,11 @@ public: // get/set devices through a QML ListView QVariant data(const QModelIndex& index, int role) const override; - bool setData(const QModelIndex& index, const QVariant &value, int role) override; // reset device to the last selected device in this context, or the default - void resetDevice(bool contextIsHMD, const QString& device); + void resetDevice(bool contextIsHMD); signals: - void deviceSelected(const QAudioDeviceInfo& device = QAudioDeviceInfo(), - const QAudioDeviceInfo& previousDevice = QAudioDeviceInfo()); void deviceChanged(const QAudioDeviceInfo& device); protected slots: @@ -61,12 +58,9 @@ protected slots: protected: friend class AudioDevices; - bool setDevice(int index, bool fromUser); - static QHash _roles; static Qt::ItemFlags _flags; - QAudio::Mode _mode { QAudio::AudioOutput }; - bool _userSelection { false }; + const QAudio::Mode _mode; QAudioDeviceInfo _selectedDevice; QList> _devices; }; @@ -118,6 +112,8 @@ class AudioDevices : public QObject { public: AudioDevices(bool& contextIsHMD); + void chooseInputDevice(const QAudioDeviceInfo& device); + void chooseOutputDevice(const QAudioDeviceInfo& device); signals: void nop(); @@ -135,9 +131,11 @@ private: AudioDeviceList* getOutputList() { return &_outputs; } AudioInputDeviceList _inputs; - AudioDeviceList _outputs; + AudioDeviceList _outputs { QAudio::AudioOutput }; + QAudioDeviceInfo _requestedOutputDevice; + QAudioDeviceInfo _requestedInputDevice; - bool& _contextIsHMD; + const bool& _contextIsHMD; }; }; diff --git a/interface/src/ui/JSConsole.cpp b/interface/src/ui/JSConsole.cpp index 443f11fb23..8c1ff87d13 100644 --- a/interface/src/ui/JSConsole.cpp +++ b/interface/src/ui/JSConsole.cpp @@ -9,6 +9,8 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // +#include "JSConsole.h" + #include #include #include @@ -20,17 +22,17 @@ #include #include "Application.h" -#include "JSConsole.h" #include "ScriptHighlighting.h" const int NO_CURRENT_HISTORY_COMMAND = -1; -const int MAX_HISTORY_SIZE = 64; +const int MAX_HISTORY_SIZE = 256; +const QString HISTORY_FILENAME = "JSConsole.history.json"; const QString COMMAND_STYLE = "color: #266a9b;"; const QString RESULT_SUCCESS_STYLE = "color: #677373;"; const QString RESULT_INFO_STYLE = "color: #223bd1;"; -const QString RESULT_WARNING_STYLE = "color: #d13b22;"; +const QString RESULT_WARNING_STYLE = "color: #999922;"; const QString RESULT_ERROR_STYLE = "color: #d13b22;"; const QString GUTTER_PREVIOUS_COMMAND = "<"; @@ -38,14 +40,33 @@ const QString GUTTER_ERROR = "X"; const QString JSConsole::_consoleFileName { "about:console" }; -JSConsole::JSConsole(QWidget* parent, ScriptEngine* scriptEngine) : +const QString JSON_KEY = "entries"; +QList _readLines(const QString& filename) { + QFile file(filename); + file.open(QFile::ReadOnly); + auto json = QTextStream(&file).readAll().toUtf8(); + auto root = QJsonDocument::fromJson(json).object(); + // TODO: check root["version"] + return root[JSON_KEY].toVariant().toStringList(); +} + +void _writeLines(const QString& filename, const QList& lines) { + QFile file(filename); + file.open(QFile::WriteOnly); + auto root = QJsonObject(); + root["version"] = 1.0; + root["last-modified"] = QDateTime::currentDateTime().toTimeSpec(Qt::OffsetFromUTC).toString(Qt::ISODate); + root[JSON_KEY] = QJsonArray::fromStringList(lines); + auto json = QJsonDocument(root).toJson(); + QTextStream(&file) << json; +} + +JSConsole::JSConsole(QWidget* parent, const QSharedPointer& scriptEngine) : QWidget(parent), _ui(new Ui::Console), _currentCommandInHistory(NO_CURRENT_HISTORY_COMMAND), - _commandHistory(), - _ownScriptEngine(scriptEngine == NULL), - _scriptEngine(NULL) { - + _savedHistoryFilename(QStandardPaths::writableLocation(QStandardPaths::DataLocation) + "/" + HISTORY_FILENAME), + _commandHistory(_readLines(_savedHistoryFilename)) { _ui->setupUi(this); _ui->promptTextEdit->setLineWrapMode(QTextEdit::NoWrap); _ui->promptTextEdit->setWordWrapMode(QTextOption::NoWrap); @@ -68,61 +89,68 @@ JSConsole::JSConsole(QWidget* parent, ScriptEngine* scriptEngine) : } JSConsole::~JSConsole() { - disconnect(_scriptEngine, SIGNAL(printedMessage(const QString&)), this, SLOT(handlePrint(const QString&))); - disconnect(_scriptEngine, SIGNAL(errorMessage(const QString&)), this, SLOT(handleError(const QString&))); - if (_ownScriptEngine) { - _scriptEngine->deleteLater(); + if (_scriptEngine) { + disconnect(_scriptEngine.data(), SIGNAL(printedMessage(const QString&)), this, SLOT(handlePrint(const QString&))); + disconnect(_scriptEngine.data(), SIGNAL(errorMessage(const QString&)), this, SLOT(handleError(const QString&))); + _scriptEngine.reset(); } delete _ui; } -void JSConsole::setScriptEngine(ScriptEngine* scriptEngine) { +void JSConsole::setScriptEngine(const QSharedPointer& scriptEngine) { if (_scriptEngine == scriptEngine && scriptEngine != NULL) { return; } if (_scriptEngine != NULL) { - disconnect(_scriptEngine, &ScriptEngine::printedMessage, this, &JSConsole::handlePrint); - disconnect(_scriptEngine, &ScriptEngine::infoMessage, this, &JSConsole::handleInfo); - disconnect(_scriptEngine, &ScriptEngine::warningMessage, this, &JSConsole::handleWarning); - disconnect(_scriptEngine, &ScriptEngine::errorMessage, this, &JSConsole::handleError); - if (_ownScriptEngine) { - _scriptEngine->deleteLater(); - } + disconnect(_scriptEngine.data(), &ScriptEngine::printedMessage, this, &JSConsole::handlePrint); + disconnect(_scriptEngine.data(), &ScriptEngine::infoMessage, this, &JSConsole::handleInfo); + disconnect(_scriptEngine.data(), &ScriptEngine::warningMessage, this, &JSConsole::handleWarning); + disconnect(_scriptEngine.data(), &ScriptEngine::errorMessage, this, &JSConsole::handleError); + _scriptEngine.reset(); } // if scriptEngine is NULL then create one and keep track of it using _ownScriptEngine - _ownScriptEngine = (scriptEngine == NULL); - _scriptEngine = _ownScriptEngine ? DependencyManager::get()->loadScript(_consoleFileName, false) : scriptEngine; + if (scriptEngine.isNull()) { + _scriptEngine = QSharedPointer(DependencyManager::get()->loadScript(_consoleFileName, false), &QObject::deleteLater); + } else { + _scriptEngine = scriptEngine; + } - connect(_scriptEngine, &ScriptEngine::printedMessage, this, &JSConsole::handlePrint); - connect(_scriptEngine, &ScriptEngine::infoMessage, this, &JSConsole::handleInfo); - connect(_scriptEngine, &ScriptEngine::warningMessage, this, &JSConsole::handleWarning); - connect(_scriptEngine, &ScriptEngine::errorMessage, this, &JSConsole::handleError); + connect(_scriptEngine.data(), &ScriptEngine::printedMessage, this, &JSConsole::handlePrint); + connect(_scriptEngine.data(), &ScriptEngine::infoMessage, this, &JSConsole::handleInfo); + connect(_scriptEngine.data(), &ScriptEngine::warningMessage, this, &JSConsole::handleWarning); + connect(_scriptEngine.data(), &ScriptEngine::errorMessage, this, &JSConsole::handleError); } void JSConsole::executeCommand(const QString& command) { - _commandHistory.prepend(command); - if (_commandHistory.length() > MAX_HISTORY_SIZE) { - _commandHistory.removeLast(); + if (_commandHistory.isEmpty() || _commandHistory.constFirst() != command) { + _commandHistory.prepend(command); + if (_commandHistory.length() > MAX_HISTORY_SIZE) { + _commandHistory.removeLast(); + } + _writeLines(_savedHistoryFilename, _commandHistory); } _ui->promptTextEdit->setDisabled(true); appendMessage(">", "" + command.toHtmlEscaped() + ""); - QFuture future = QtConcurrent::run(this, &JSConsole::executeCommandInWatcher, command); + QWeakPointer weakScriptEngine = _scriptEngine; + auto consoleFileName = _consoleFileName; + QFuture future = QtConcurrent::run([weakScriptEngine, consoleFileName, command]()->QScriptValue{ + QScriptValue result; + auto scriptEngine = weakScriptEngine.lock(); + if (scriptEngine) { + BLOCKING_INVOKE_METHOD(scriptEngine.data(), "evaluate", + Q_RETURN_ARG(QScriptValue, result), + Q_ARG(const QString&, command), + Q_ARG(const QString&, consoleFileName)); + } + return result; + }); _executeWatcher.setFuture(future); } -QScriptValue JSConsole::executeCommandInWatcher(const QString& command) { - QScriptValue result; - BLOCKING_INVOKE_METHOD(_scriptEngine, "evaluate", - Q_RETURN_ARG(QScriptValue, result), - Q_ARG(const QString&, command), - Q_ARG(const QString&, _consoleFileName)); - return result; -} - void JSConsole::commandFinished() { QScriptValue result = _executeWatcher.result(); @@ -182,7 +210,7 @@ bool JSConsole::eventFilter(QObject* sender, QEvent* event) { // a new QTextBlock isn't created. keyEvent->setModifiers(keyEvent->modifiers() & ~Qt::ShiftModifier); } else { - QString command = _ui->promptTextEdit->toPlainText().trimmed(); + QString command = _ui->promptTextEdit->toPlainText().replace("\r\n","\n").trimmed(); if (!command.isEmpty()) { QTextCursor cursor = _ui->promptTextEdit->textCursor(); diff --git a/interface/src/ui/JSConsole.h b/interface/src/ui/JSConsole.h index 864f847071..5010b5b9ca 100644 --- a/interface/src/ui/JSConsole.h +++ b/interface/src/ui/JSConsole.h @@ -17,6 +17,7 @@ #include #include #include +#include #include "ui_console.h" #include "ScriptEngine.h" @@ -29,10 +30,10 @@ const int CONSOLE_HEIGHT = 200; class JSConsole : public QWidget { Q_OBJECT public: - JSConsole(QWidget* parent, ScriptEngine* scriptEngine = NULL); + JSConsole(QWidget* parent, const QSharedPointer& scriptEngine = QSharedPointer()); ~JSConsole(); - void setScriptEngine(ScriptEngine* scriptEngine = NULL); + void setScriptEngine(const QSharedPointer& scriptEngine = QSharedPointer()); void clear(); public slots: @@ -58,16 +59,14 @@ private: void setToNextCommandInHistory(); void setToPreviousCommandInHistory(); void resetCurrentCommandHistory(); - QScriptValue executeCommandInWatcher(const QString& command); QFutureWatcher _executeWatcher; Ui::Console* _ui; int _currentCommandInHistory; + QString _savedHistoryFilename; QList _commandHistory; - // Keeps track if the script engine is created inside the JSConsole - bool _ownScriptEngine; QString _rootCommand; - ScriptEngine* _scriptEngine; + QSharedPointer _scriptEngine; static const QString _consoleFileName; }; diff --git a/interface/src/ui/PreferencesDialog.cpp b/interface/src/ui/PreferencesDialog.cpp index 41a4ebdf68..c9e59ada23 100644 --- a/interface/src/ui/PreferencesDialog.cpp +++ b/interface/src/ui/PreferencesDialog.cpp @@ -106,6 +106,12 @@ void setupPreferences() { auto setter = [](bool value) { qApp->setPreferAvatarFingerOverStylus(value); }; preferences->addPreference(new CheckPreference(UI_CATEGORY, "Prefer Avatar Finger Over Stylus", getter, setter)); } + { + static const QString RETICLE_ICON_NAME = { Cursor::Manager::getIconName(Cursor::Icon::RETICLE) }; + auto getter = []()->bool { return qApp->getPreferredCursor() == RETICLE_ICON_NAME; }; + auto setter = [](bool value) { qApp->setPreferredCursor(value ? RETICLE_ICON_NAME : QString()); }; + preferences->addPreference(new CheckPreference(UI_CATEGORY, "Use reticle cursor instead of arrow", getter, setter)); + } // Snapshots static const QString SNAPSHOTS { "Snapshots" }; @@ -181,15 +187,23 @@ void setupPreferences() { preference->setStep(1); preferences->addPreference(preference); } + { + auto getter = [=]()->QString { return myAvatar->getDominantHand(); }; + auto setter = [=](const QString& value) { myAvatar->setDominantHand(value); }; + preferences->addPreference(new PrimaryHandPreference(AVATAR_TUNING, "Dominant Hand", getter, setter)); + } { auto getter = [=]()->float { return myAvatar->getUniformScale(); }; auto setter = [=](float value) { myAvatar->setTargetScale(value); }; - auto preference = new SpinnerPreference(AVATAR_TUNING, "Avatar scale (default is 1.0)", getter, setter); - preference->setMin(0.01f); - preference->setMax(99.9f); + auto preference = new SpinnerSliderPreference(AVATAR_TUNING, "Avatar Scale", getter, setter); + preference->setStep(0.05f); preference->setDecimals(2); - preference->setStep(1); preferences->addPreference(preference); + + // When the Interface is first loaded, this section setupPreferences(); is loaded - + // causing the myAvatar->getDomainMinScale() and myAvatar->getDomainMaxScale() to get set to incorrect values + // which can't be changed across domain switches. Having these values loaded up when you load the Dialog each time + // is a way around this, therefore they're not specified here but in the QML. } { auto getter = []()->float { return DependencyManager::get()->getEyeClosingThreshold(); }; @@ -297,17 +311,6 @@ void setupPreferences() { preferences->addPreference(preference); } - - { - auto getter = []()->float { return controller::InputDevice::getReticleMoveSpeed(); }; - auto setter = [](float value) { controller::InputDevice::setReticleMoveSpeed(value); }; - auto preference = new SpinnerPreference("Sixense Controllers", "Reticle movement speed", getter, setter); - preference->setMin(0); - preference->setMax(100); - preference->setStep(1); - preferences->addPreference(preference); - } - { static const QString RENDER("Graphics"); auto renderConfig = qApp->getRenderEngine()->getConfiguration(); diff --git a/interface/src/ui/ResourceImageItem.cpp b/interface/src/ui/ResourceImageItem.cpp new file mode 100644 index 0000000000..5b7c1896fe --- /dev/null +++ b/interface/src/ui/ResourceImageItem.cpp @@ -0,0 +1,115 @@ +// +// ResourceImageItem.cpp +// +// Created by David Kelly and Howard Stearns on 2017/06/08 +// 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 "ResourceImageItem.h" + +#include +#include +#include +#include + +#include + +ResourceImageItem::ResourceImageItem() : QQuickFramebufferObject() { + auto textureCache = DependencyManager::get(); + connect(textureCache.data(), SIGNAL(spectatorCameraFramebufferReset()), this, SLOT(update())); +} + +void ResourceImageItem::setUrl(const QString& url) { + if (url != m_url) { + m_url = url; + update(); + } +} + +void ResourceImageItem::setReady(bool ready) { + if (ready != m_ready) { + m_ready = ready; + update(); + } +} + +void ResourceImageItemRenderer::onUpdateTimer() { + if (_ready) { + if (_networkTexture && _networkTexture->isLoaded()) { + if(_fboMutex.tryLock()) { + invalidateFramebufferObject(); + qApp->getActiveDisplayPlugin()->copyTextureToQuickFramebuffer(_networkTexture, _copyFbo, &_fenceSync); + _fboMutex.unlock(); + } else { + qDebug() << "couldn't get a lock, using last frame"; + } + } else { + _networkTexture = DependencyManager::get()->getTexture(_url); + } + } + update(); +} + +ResourceImageItemRenderer::ResourceImageItemRenderer() : QQuickFramebufferObject::Renderer() { + connect(&_updateTimer, SIGNAL(timeout()), this, SLOT(onUpdateTimer())); + auto textureCache = DependencyManager::get(); +} + +void ResourceImageItemRenderer::synchronize(QQuickFramebufferObject* item) { + ResourceImageItem* resourceImageItem = static_cast(item); + + resourceImageItem->setFlag(QQuickItem::ItemHasContents); + + _url = resourceImageItem->getUrl(); + _ready = resourceImageItem->getReady(); + _visible = resourceImageItem->isVisible(); + _window = resourceImageItem->window(); + + _networkTexture = DependencyManager::get()->getTexture(_url); + static const int UPDATE_TIMER_DELAY_IN_MS = 100; // 100 ms = 10 hz for now + if (_ready && _visible && !_updateTimer.isActive()) { + _updateTimer.start(UPDATE_TIMER_DELAY_IN_MS); + } else if (!(_ready && _visible) && _updateTimer.isActive()) { + _updateTimer.stop(); + } +} + +QOpenGLFramebufferObject* ResourceImageItemRenderer::createFramebufferObject(const QSize& size) { + if (_copyFbo) { + delete _copyFbo; + } + QOpenGLFramebufferObjectFormat format; + format.setAttachment(QOpenGLFramebufferObject::CombinedDepthStencil); + _copyFbo = new QOpenGLFramebufferObject(size, format); + _copyFbo->bind(); + return new QOpenGLFramebufferObject(size, format); +} + +void ResourceImageItemRenderer::render() { + auto f = QOpenGLContext::currentContext()->extraFunctions(); + + if (_fenceSync) { + f->glWaitSync(_fenceSync, 0, GL_TIMEOUT_IGNORED); + f->glDeleteSync(_fenceSync); + _fenceSync = 0; + } + if (_ready) { + _fboMutex.lock(); + _copyFbo->bind(); + QOpenGLFramebufferObject::blitFramebuffer(framebufferObject(), _copyFbo, GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT, GL_NEAREST); + + // this clears the copyFbo texture + // so next frame starts fresh - helps + // when aspect ratio changes + _copyFbo->takeTexture(); + _copyFbo->bind(); + _copyFbo->release(); + + _fboMutex.unlock(); + } + glFlush(); + _window->resetOpenGLState(); +} diff --git a/interface/src/ui/ResourceImageItem.h b/interface/src/ui/ResourceImageItem.h new file mode 100644 index 0000000000..985ab5a66e --- /dev/null +++ b/interface/src/ui/ResourceImageItem.h @@ -0,0 +1,63 @@ +// +// ResourceImageItem.h +// +// Created by David Kelly and Howard Stearns on 2017/06/08 +// 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_ResourceImageItem_h +#define hifi_ResourceImageItem_h + +#include "Application.h" + +#include +#include +#include + +#include + +class ResourceImageItemRenderer : public QObject, public QQuickFramebufferObject::Renderer { + Q_OBJECT +public: + ResourceImageItemRenderer(); + QOpenGLFramebufferObject* createFramebufferObject(const QSize& size) override; + void synchronize(QQuickFramebufferObject* item) override; + void render() override; +private: + bool _ready; + QString _url; + bool _visible; + + NetworkTexturePointer _networkTexture; + QQuickWindow* _window; + QMutex _fboMutex; + QOpenGLFramebufferObject* _copyFbo { nullptr }; + GLsync _fenceSync { 0 }; + QTimer _updateTimer; +public slots: + void onUpdateTimer(); +}; + +class ResourceImageItem : public QQuickFramebufferObject { + Q_OBJECT + Q_PROPERTY(QString url READ getUrl WRITE setUrl) + Q_PROPERTY(bool ready READ getReady WRITE setReady) +public: + ResourceImageItem(); + QString getUrl() const { return m_url; } + void setUrl(const QString& url); + bool getReady() const { return m_ready; } + void setReady(bool ready); + QQuickFramebufferObject::Renderer* createRenderer() const override { return new ResourceImageItemRenderer; } + +private: + QString m_url; + bool m_ready { false }; + +}; + +#endif // hifi_ResourceImageItem_h diff --git a/interface/src/ui/SnapshotAnimated.cpp b/interface/src/ui/SnapshotAnimated.cpp index 70767b007d..3c00be8358 100644 --- a/interface/src/ui/SnapshotAnimated.cpp +++ b/interface/src/ui/SnapshotAnimated.cpp @@ -13,8 +13,9 @@ #include #include #include +#include -#include +#include #include "SnapshotAnimated.h" QTimer* SnapshotAnimated::snapshotAnimatedTimer = NULL; diff --git a/interface/src/ui/TestingDialog.cpp b/interface/src/ui/TestingDialog.cpp index f55eb63a5b..bba14cd5d7 100644 --- a/interface/src/ui/TestingDialog.cpp +++ b/interface/src/ui/TestingDialog.cpp @@ -24,12 +24,12 @@ TestingDialog::TestingDialog(QWidget* parent) : _console->setFixedHeight(TESTING_CONSOLE_HEIGHT); auto _engines = DependencyManager::get(); - _engine = _engines->loadScript(qApp->applicationDirPath() + testRunnerRelativePath); + _engine.reset(_engines->loadScript(qApp->applicationDirPath() + testRunnerRelativePath)); _console->setScriptEngine(_engine); - connect(_engine, &ScriptEngine::finished, this, &TestingDialog::onTestingFinished); + connect(_engine.data(), &ScriptEngine::finished, this, &TestingDialog::onTestingFinished); } void TestingDialog::onTestingFinished(const QString& scriptPath) { - _engine = nullptr; - _console->setScriptEngine(nullptr); + _engine.reset(); + _console->setScriptEngine(); } diff --git a/interface/src/ui/TestingDialog.h b/interface/src/ui/TestingDialog.h index b90b8f2e99..055e43eaf7 100644 --- a/interface/src/ui/TestingDialog.h +++ b/interface/src/ui/TestingDialog.h @@ -29,7 +29,7 @@ public: private: std::unique_ptr _console; - ScriptEngine* _engine; + QSharedPointer _engine; }; #endif diff --git a/interface/src/ui/overlays/Billboard3DOverlay.cpp b/interface/src/ui/overlays/Billboard3DOverlay.cpp index 182e7da978..f5668caa71 100644 --- a/interface/src/ui/overlays/Billboard3DOverlay.cpp +++ b/interface/src/ui/overlays/Billboard3DOverlay.cpp @@ -37,9 +37,11 @@ QVariant Billboard3DOverlay::getProperty(const QString &property) { return Planar3DOverlay::getProperty(property); } -void Billboard3DOverlay::applyTransformTo(Transform& transform, bool force) { +bool Billboard3DOverlay::applyTransformTo(Transform& transform, bool force) { + bool transformChanged = false; if (force || usecTimestampNow() > _transformExpiry) { - PanelAttachable::applyTransformTo(transform, true); - pointTransformAtCamera(transform, getOffsetRotation()); + transformChanged = PanelAttachable::applyTransformTo(transform, true); + transformChanged |= pointTransformAtCamera(transform, getOffsetRotation()); } + return transformChanged; } diff --git a/interface/src/ui/overlays/Billboard3DOverlay.h b/interface/src/ui/overlays/Billboard3DOverlay.h index d256a92afe..d429537b5b 100644 --- a/interface/src/ui/overlays/Billboard3DOverlay.h +++ b/interface/src/ui/overlays/Billboard3DOverlay.h @@ -27,7 +27,7 @@ public: QVariant getProperty(const QString& property) override; protected: - virtual void applyTransformTo(Transform& transform, bool force = false) override; + virtual bool applyTransformTo(Transform& transform, bool force = false) override; }; #endif // hifi_Billboard3DOverlay_h diff --git a/interface/src/ui/overlays/Billboardable.cpp b/interface/src/ui/overlays/Billboardable.cpp index a01d62bfd1..34a4ef6df5 100644 --- a/interface/src/ui/overlays/Billboardable.cpp +++ b/interface/src/ui/overlays/Billboardable.cpp @@ -28,7 +28,7 @@ QVariant Billboardable::getProperty(const QString &property) { return QVariant(); } -void Billboardable::pointTransformAtCamera(Transform& transform, glm::quat offsetRotation) { +bool Billboardable::pointTransformAtCamera(Transform& transform, glm::quat offsetRotation) { if (isFacingAvatar()) { glm::vec3 billboardPos = transform.getTranslation(); glm::vec3 cameraPos = qApp->getCamera().getPosition(); @@ -38,5 +38,7 @@ void Billboardable::pointTransformAtCamera(Transform& transform, glm::quat offse glm::quat rotation(glm::vec3(elevation, azimuth, 0)); transform.setRotation(rotation); transform.postRotate(offsetRotation); + return true; } + return false; } diff --git a/interface/src/ui/overlays/Billboardable.h b/interface/src/ui/overlays/Billboardable.h index e2d29a2769..46d9ac6479 100644 --- a/interface/src/ui/overlays/Billboardable.h +++ b/interface/src/ui/overlays/Billboardable.h @@ -27,7 +27,7 @@ protected: void setProperties(const QVariantMap& properties); QVariant getProperty(const QString& property); - void pointTransformAtCamera(Transform& transform, glm::quat offsetRotation = {1, 0, 0, 0}); + bool pointTransformAtCamera(Transform& transform, glm::quat offsetRotation = {1, 0, 0, 0}); private: bool _isFacingAvatar = false; diff --git a/interface/src/ui/overlays/ContextOverlayInterface.cpp b/interface/src/ui/overlays/ContextOverlayInterface.cpp new file mode 100644 index 0000000000..e406d139d0 --- /dev/null +++ b/interface/src/ui/overlays/ContextOverlayInterface.cpp @@ -0,0 +1,271 @@ +// +// ContextOverlayInterface.cpp +// interface/src/ui/overlays +// +// Created by Zach Fox on 2017-07-14. +// 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 "ContextOverlayInterface.h" +#include "Application.h" + +#include + +static const float CONTEXT_OVERLAY_TABLET_OFFSET = 30.0f; // Degrees +static const float CONTEXT_OVERLAY_TABLET_ORIENTATION = 210.0f; // Degrees +static const float CONTEXT_OVERLAY_TABLET_DISTANCE = 0.65F; // Meters +ContextOverlayInterface::ContextOverlayInterface() { + // "context_overlay" debug log category disabled by default. + // Create your own "qtlogging.ini" file and set your "QT_LOGGING_CONF" environment variable + // if you'd like to enable/disable certain categories. + // More details: http://doc.qt.io/qt-5/qloggingcategory.html#configuring-categories + QLoggingCategory::setFilterRules(QStringLiteral("hifi.context_overlay.debug=false")); + + _entityScriptingInterface = DependencyManager::get(); + _hmdScriptingInterface = DependencyManager::get(); + _tabletScriptingInterface = DependencyManager::get(); + + _entityPropertyFlags += PROP_POSITION; + _entityPropertyFlags += PROP_ROTATION; + _entityPropertyFlags += PROP_MARKETPLACE_ID; + _entityPropertyFlags += PROP_DIMENSIONS; + _entityPropertyFlags += PROP_REGISTRATION_POINT; + + // initially, set _enabled to match the switch. Later we enable/disable via the getter/setters + // if we are in edit or pal (for instance). Note this is temporary, as we expect to enable this all + // the time after getting edge highlighting, etc... + _enabled = _settingSwitch.get(); + + auto entityTreeRenderer = DependencyManager::get().data(); + connect(entityTreeRenderer, SIGNAL(mousePressOnEntity(const EntityItemID&, const PointerEvent&)), this, SLOT(createOrDestroyContextOverlay(const EntityItemID&, const PointerEvent&))); + connect(entityTreeRenderer, SIGNAL(hoverEnterEntity(const EntityItemID&, const PointerEvent&)), this, SLOT(contextOverlays_hoverEnterEntity(const EntityItemID&, const PointerEvent&))); + connect(entityTreeRenderer, SIGNAL(hoverLeaveEntity(const EntityItemID&, const PointerEvent&)), this, SLOT(contextOverlays_hoverLeaveEntity(const EntityItemID&, const PointerEvent&))); + connect(_tabletScriptingInterface->getTablet("com.highfidelity.interface.tablet.system"), &TabletProxy::tabletShownChanged, this, [&]() { + if (_contextOverlayJustClicked && _hmdScriptingInterface->isMounted()) { + QUuid tabletFrameID = _hmdScriptingInterface->getCurrentTabletFrameID(); + auto myAvatar = DependencyManager::get()->getMyAvatar(); + glm::quat cameraOrientation = qApp->getCamera().getOrientation(); + QVariantMap props; + props.insert("position", vec3toVariant(myAvatar->getEyePosition() + glm::quat(glm::radians(glm::vec3(0.0f, CONTEXT_OVERLAY_TABLET_OFFSET, 0.0f))) * (CONTEXT_OVERLAY_TABLET_DISTANCE * (cameraOrientation * Vectors::FRONT)))); + props.insert("orientation", quatToVariant(cameraOrientation * glm::quat(glm::radians(glm::vec3(0.0f, CONTEXT_OVERLAY_TABLET_ORIENTATION, 0.0f))))); + qApp->getOverlays().editOverlay(tabletFrameID, props); + _contextOverlayJustClicked = false; + } + }); +} + +static const uint32_t LEFT_HAND_HW_ID = 1; +static const xColor CONTEXT_OVERLAY_COLOR = { 255, 255, 255 }; +static const float CONTEXT_OVERLAY_INSIDE_DISTANCE = 1.0f; // in meters +static const float CONTEXT_OVERLAY_CLOSE_DISTANCE = 1.5f; // in meters +static const float CONTEXT_OVERLAY_CLOSE_SIZE = 0.12f; // in meters, same x and y dims +static const float CONTEXT_OVERLAY_FAR_SIZE = 0.08f; // in meters, same x and y dims +static const float CONTEXT_OVERLAY_CLOSE_OFFSET_ANGLE = 20.0f; +static const float CONTEXT_OVERLAY_UNHOVERED_ALPHA = 0.85f; +static const float CONTEXT_OVERLAY_HOVERED_ALPHA = 1.0f; +static const float CONTEXT_OVERLAY_UNHOVERED_PULSEMIN = 0.6f; +static const float CONTEXT_OVERLAY_UNHOVERED_PULSEMAX = 1.0f; +static const float CONTEXT_OVERLAY_UNHOVERED_PULSEPERIOD = 1.0f; +static const float CONTEXT_OVERLAY_UNHOVERED_COLORPULSE = 1.0f; +static const float CONTEXT_OVERLAY_FAR_OFFSET = 0.1f; + +void ContextOverlayInterface::setEnabled(bool enabled) { + // only enable/disable if the setting in 'on'. If it is 'off', + // make sure _enabled is always false. + if (_settingSwitch.get()) { + _enabled = enabled; + } else { + _enabled = false; + } +} + +bool ContextOverlayInterface::createOrDestroyContextOverlay(const EntityItemID& entityItemID, const PointerEvent& event) { + if (_enabled && event.getButton() == PointerEvent::SecondaryButton) { + if (contextOverlayFilterPassed(entityItemID)) { + qCDebug(context_overlay) << "Creating Context Overlay on top of entity with ID: " << entityItemID; + + // Add all necessary variables to the stack + EntityItemProperties entityProperties = _entityScriptingInterface->getEntityProperties(entityItemID, _entityPropertyFlags); + glm::vec3 cameraPosition = qApp->getCamera().getPosition(); + float distanceFromCameraToEntity = glm::distance(entityProperties.getPosition(), cameraPosition); + glm::vec3 entityDimensions = entityProperties.getDimensions(); + glm::vec3 entityPosition = entityProperties.getPosition(); + glm::vec3 contextOverlayPosition = entityProperties.getPosition(); + glm::vec2 contextOverlayDimensions; + + // Update the position of the overlay if the registration point of the entity + // isn't default + if (entityProperties.getRegistrationPoint() != glm::vec3(0.5f)) { + glm::vec3 adjustPos = entityProperties.getRegistrationPoint() - glm::vec3(0.5f); + entityPosition = entityPosition - (entityProperties.getRotation() * (adjustPos * entityProperties.getDimensions())); + } + + enableEntityHighlight(entityItemID); + + AABox boundingBox = AABox(entityPosition - (entityDimensions / 2.0f), entityDimensions * 2.0f); + + // Update the cached Entity Marketplace ID + _entityMarketplaceID = entityProperties.getMarketplaceID(); + + + if (!_currentEntityWithContextOverlay.isNull() && _currentEntityWithContextOverlay != entityItemID) { + disableEntityHighlight(_currentEntityWithContextOverlay); + } + + // Update the cached "Current Entity with Context Overlay" variable + setCurrentEntityWithContextOverlay(entityItemID); + + // Here, we determine the position and dimensions of the Context Overlay. + if (boundingBox.contains(cameraPosition)) { + // If the camera is inside the box... + // ...position the Context Overlay 1 meter in front of the camera. + contextOverlayPosition = cameraPosition + CONTEXT_OVERLAY_INSIDE_DISTANCE * (qApp->getCamera().getOrientation() * Vectors::FRONT); + contextOverlayDimensions = glm::vec2(CONTEXT_OVERLAY_CLOSE_SIZE, CONTEXT_OVERLAY_CLOSE_SIZE) * glm::distance(contextOverlayPosition, cameraPosition); + } else if (distanceFromCameraToEntity < CONTEXT_OVERLAY_CLOSE_DISTANCE) { + // Else if the entity is too close to the camera... + // ...rotate the Context Overlay to the right of the entity. + // This makes it easy to inspect things you're holding. + float offsetAngle = -CONTEXT_OVERLAY_CLOSE_OFFSET_ANGLE; + if (event.getID() == LEFT_HAND_HW_ID) { + offsetAngle *= -1; + } + contextOverlayPosition = (glm::quat(glm::radians(glm::vec3(0.0f, offsetAngle, 0.0f))) * (entityPosition - cameraPosition)) + cameraPosition; + contextOverlayDimensions = glm::vec2(CONTEXT_OVERLAY_CLOSE_SIZE, CONTEXT_OVERLAY_CLOSE_SIZE) * glm::distance(contextOverlayPosition, cameraPosition); + } else { + // Else, place the Context Overlay some offset away from the entity's bounding + // box in the direction of the camera. + glm::vec3 direction = glm::normalize(entityPosition - cameraPosition); + float distance; + BoxFace face; + glm::vec3 normal; + boundingBox.findRayIntersection(cameraPosition, direction, distance, face, normal); + contextOverlayPosition = (cameraPosition + direction * distance) - direction * CONTEXT_OVERLAY_FAR_OFFSET; + contextOverlayDimensions = glm::vec2(CONTEXT_OVERLAY_FAR_SIZE, CONTEXT_OVERLAY_FAR_SIZE) * glm::distance(contextOverlayPosition, cameraPosition); + } + + // Finally, setup and draw the Context Overlay + if (_contextOverlayID == UNKNOWN_OVERLAY_ID || !qApp->getOverlays().isAddedOverlay(_contextOverlayID)) { + _contextOverlay = std::make_shared(); + _contextOverlay->setAlpha(CONTEXT_OVERLAY_UNHOVERED_ALPHA); + _contextOverlay->setPulseMin(CONTEXT_OVERLAY_UNHOVERED_PULSEMIN); + _contextOverlay->setPulseMax(CONTEXT_OVERLAY_UNHOVERED_PULSEMAX); + _contextOverlay->setColorPulse(CONTEXT_OVERLAY_UNHOVERED_COLORPULSE); + _contextOverlay->setIgnoreRayIntersection(false); + _contextOverlay->setDrawInFront(true); + _contextOverlay->setURL(PathUtils::resourcesPath() + "images/inspect-icon.png"); + _contextOverlay->setIsFacingAvatar(true); + _contextOverlayID = qApp->getOverlays().addOverlay(_contextOverlay); + } + _contextOverlay->setPosition(contextOverlayPosition); + _contextOverlay->setDimensions(contextOverlayDimensions); + _contextOverlay->setRotation(entityProperties.getRotation()); + _contextOverlay->setVisible(true); + + return true; + } + } else { + if (!_currentEntityWithContextOverlay.isNull()) { + return destroyContextOverlay(_currentEntityWithContextOverlay, event); + } + return false; + } + return false; +} + +bool ContextOverlayInterface::contextOverlayFilterPassed(const EntityItemID& entityItemID) { + EntityItemProperties entityProperties = _entityScriptingInterface->getEntityProperties(entityItemID, _entityPropertyFlags); + return (entityProperties.getMarketplaceID().length() != 0); +} + +bool ContextOverlayInterface::destroyContextOverlay(const EntityItemID& entityItemID, const PointerEvent& event) { + if (_contextOverlayID != UNKNOWN_OVERLAY_ID) { + qCDebug(context_overlay) << "Destroying Context Overlay on top of entity with ID: " << entityItemID; + disableEntityHighlight(entityItemID); + setCurrentEntityWithContextOverlay(QUuid()); + _entityMarketplaceID.clear(); + // Destroy the Context Overlay + qApp->getOverlays().deleteOverlay(_contextOverlayID); + _contextOverlay = NULL; + _contextOverlayID = UNKNOWN_OVERLAY_ID; + return true; + } + return false; +} + +bool ContextOverlayInterface::destroyContextOverlay(const EntityItemID& entityItemID) { + return ContextOverlayInterface::destroyContextOverlay(entityItemID, PointerEvent()); +} + +void ContextOverlayInterface::contextOverlays_mousePressOnOverlay(const OverlayID& overlayID, const PointerEvent& event) { + if (overlayID == _contextOverlayID && event.getButton() == PointerEvent::PrimaryButton) { + qCDebug(context_overlay) << "Clicked Context Overlay. Entity ID:" << _currentEntityWithContextOverlay << "Overlay ID:" << overlayID; + openMarketplace(); + destroyContextOverlay(_currentEntityWithContextOverlay, PointerEvent()); + _contextOverlayJustClicked = true; + } +} + +void ContextOverlayInterface::contextOverlays_hoverEnterOverlay(const OverlayID& overlayID, const PointerEvent& event) { + if (_contextOverlayID != UNKNOWN_OVERLAY_ID && _contextOverlay) { + qCDebug(context_overlay) << "Started hovering over Context Overlay. Overlay ID:" << overlayID; + _contextOverlay->setColor(CONTEXT_OVERLAY_COLOR); + _contextOverlay->setColorPulse(0.0f); // pulse off + _contextOverlay->setPulsePeriod(0.0f); // pulse off + _contextOverlay->setAlpha(CONTEXT_OVERLAY_HOVERED_ALPHA); + } +} + +void ContextOverlayInterface::contextOverlays_hoverLeaveOverlay(const OverlayID& overlayID, const PointerEvent& event) { + if (_contextOverlayID != UNKNOWN_OVERLAY_ID && _contextOverlay) { + qCDebug(context_overlay) << "Stopped hovering over Context Overlay. Overlay ID:" << overlayID; + _contextOverlay->setColor(CONTEXT_OVERLAY_COLOR); + _contextOverlay->setColorPulse(CONTEXT_OVERLAY_UNHOVERED_COLORPULSE); + _contextOverlay->setPulsePeriod(CONTEXT_OVERLAY_UNHOVERED_PULSEPERIOD); + _contextOverlay->setAlpha(CONTEXT_OVERLAY_UNHOVERED_ALPHA); + } +} + +void ContextOverlayInterface::contextOverlays_hoverEnterEntity(const EntityItemID& entityID, const PointerEvent& event) { + if (contextOverlayFilterPassed(entityID)) { + enableEntityHighlight(entityID); + } +} + +void ContextOverlayInterface::contextOverlays_hoverLeaveEntity(const EntityItemID& entityID, const PointerEvent& event) { + if (_currentEntityWithContextOverlay != entityID) { + disableEntityHighlight(entityID); + } +} + +static const QString MARKETPLACE_BASE_URL = "https://metaverse.highfidelity.com/marketplace/items/"; + +void ContextOverlayInterface::openMarketplace() { + // lets open the tablet and go to the current item in + // the marketplace (if the current entity has a + // marketplaceID) + if (!_currentEntityWithContextOverlay.isNull() && _entityMarketplaceID.length() > 0) { + auto tablet = dynamic_cast(_tabletScriptingInterface->getTablet("com.highfidelity.interface.tablet.system")); + // construct the url to the marketplace item + QString url = MARKETPLACE_BASE_URL + _entityMarketplaceID; + tablet->gotoWebScreen(url); + _hmdScriptingInterface->openTablet(); + _isInMarketplaceInspectionMode = true; + } +} + +void ContextOverlayInterface::enableEntityHighlight(const EntityItemID& entityItemID) { + if (!qApp->getEntities()->getTree()->findEntityByEntityItemID(entityItemID)->getShouldHighlight()) { + qCDebug(context_overlay) << "Setting 'shouldHighlight' to 'true' for Entity ID:" << entityItemID; + qApp->getEntities()->getTree()->findEntityByEntityItemID(entityItemID)->setShouldHighlight(true); + } +} + +void ContextOverlayInterface::disableEntityHighlight(const EntityItemID& entityItemID) { + if (qApp->getEntities()->getTree()->findEntityByEntityItemID(entityItemID)->getShouldHighlight()) { + qCDebug(context_overlay) << "Setting 'shouldHighlight' to 'false' for Entity ID:" << entityItemID; + qApp->getEntities()->getTree()->findEntityByEntityItemID(entityItemID)->setShouldHighlight(false); + } +} diff --git a/interface/src/ui/overlays/ContextOverlayInterface.h b/interface/src/ui/overlays/ContextOverlayInterface.h new file mode 100644 index 0000000000..ba9cb68575 --- /dev/null +++ b/interface/src/ui/overlays/ContextOverlayInterface.h @@ -0,0 +1,85 @@ +// +// ContextOverlayInterface.h +// interface/src/ui/overlays +// +// Created by Zach Fox on 2017-07-14. +// 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_ContextOverlayInterface_h +#define hifi_ContextOverlayInterface_h + +#include +#include + +#include +#include +#include +#include "avatar/AvatarManager.h" + +#include "EntityScriptingInterface.h" +#include "ui/overlays/Image3DOverlay.h" +#include "ui/overlays/Overlays.h" +#include "scripting/HMDScriptingInterface.h" + +#include "EntityTree.h" +#include "ContextOverlayLogging.h" + +/**jsdoc +* @namespace ContextOverlay +*/ +class ContextOverlayInterface : public QObject, public Dependency { + Q_OBJECT + + Q_PROPERTY(QUuid entityWithContextOverlay READ getCurrentEntityWithContextOverlay WRITE setCurrentEntityWithContextOverlay) + Q_PROPERTY(bool enabled READ getEnabled WRITE setEnabled) + Q_PROPERTY(bool isInMarketplaceInspectionMode READ getIsInMarketplaceInspectionMode WRITE setIsInMarketplaceInspectionMode) + QSharedPointer _entityScriptingInterface; + EntityPropertyFlags _entityPropertyFlags; + QSharedPointer _hmdScriptingInterface; + QSharedPointer _tabletScriptingInterface; + OverlayID _contextOverlayID { UNKNOWN_OVERLAY_ID }; + std::shared_ptr _contextOverlay { nullptr }; +public: + ContextOverlayInterface(); + + Q_INVOKABLE QUuid getCurrentEntityWithContextOverlay() { return _currentEntityWithContextOverlay; } + void setCurrentEntityWithContextOverlay(const QUuid& entityID) { _currentEntityWithContextOverlay = entityID; } + void setEnabled(bool enabled); + bool getEnabled() { return _enabled; } + bool getIsInMarketplaceInspectionMode() { return _isInMarketplaceInspectionMode; } + void setIsInMarketplaceInspectionMode(bool mode) { _isInMarketplaceInspectionMode = mode; } + +public slots: + bool createOrDestroyContextOverlay(const EntityItemID& entityItemID, const PointerEvent& event); + bool destroyContextOverlay(const EntityItemID& entityItemID, const PointerEvent& event); + bool destroyContextOverlay(const EntityItemID& entityItemID); + void contextOverlays_mousePressOnOverlay(const OverlayID& overlayID, const PointerEvent& event); + void contextOverlays_hoverEnterOverlay(const OverlayID& overlayID, const PointerEvent& event); + void contextOverlays_hoverLeaveOverlay(const OverlayID& overlayID, const PointerEvent& event); + void contextOverlays_hoverEnterEntity(const EntityItemID& entityID, const PointerEvent& event); + void contextOverlays_hoverLeaveEntity(const EntityItemID& entityID, const PointerEvent& event); + bool contextOverlayFilterPassed(const EntityItemID& entityItemID); + +private: + bool _verboseLogging { true }; + bool _enabled { true }; + QUuid _currentEntityWithContextOverlay{}; + QString _entityMarketplaceID; + bool _contextOverlayJustClicked { false }; + + bool _isInMarketplaceInspectionMode { false }; + + Setting::Handle _settingSwitch { "inspectionMode", false }; + + void openMarketplace(); + void enableEntityHighlight(const EntityItemID& entityItemID); + void disableEntityHighlight(const EntityItemID& entityItemID); + +}; + +#endif // hifi_ContextOverlayInterface_h diff --git a/interface/src/ui/overlays/ContextOverlayLogging.cpp b/interface/src/ui/overlays/ContextOverlayLogging.cpp new file mode 100644 index 0000000000..c2c3fb7734 --- /dev/null +++ b/interface/src/ui/overlays/ContextOverlayLogging.cpp @@ -0,0 +1,14 @@ +// +// ContextOverlayLogging.cpp +// interface/src/ui/overlays +// +// Created by Zach Fox on 2017-07-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 "ContextOverlayLogging.h" + +Q_LOGGING_CATEGORY(context_overlay, "hifi.context_overlay") diff --git a/interface/src/ui/overlays/ContextOverlayLogging.h b/interface/src/ui/overlays/ContextOverlayLogging.h new file mode 100644 index 0000000000..182ebc1425 --- /dev/null +++ b/interface/src/ui/overlays/ContextOverlayLogging.h @@ -0,0 +1,20 @@ +// +// ContextOverlayLogging.h +// interface/src/ui/overlays +// +// Created by Zach Fox on 2017-07-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 +// + +#pragma once +#ifndef hifi_ContextOverlayLogging_h +#define hifi_ContextOverlayLogging_h + +#include + +Q_DECLARE_LOGGING_CATEGORY(context_overlay) + +#endif // hifi_ContextOverlayLogging_h diff --git a/interface/src/ui/overlays/Image3DOverlay.cpp b/interface/src/ui/overlays/Image3DOverlay.cpp index c8c9c36a1d..7dfee2c491 100644 --- a/interface/src/ui/overlays/Image3DOverlay.cpp +++ b/interface/src/ui/overlays/Image3DOverlay.cpp @@ -99,10 +99,14 @@ void Image3DOverlay::render(RenderArgs* args) { const float MAX_COLOR = 255.0f; xColor color = getColor(); float alpha = getAlpha(); - + Transform transform = getTransform(); - applyTransformTo(transform, true); - setTransform(transform); + bool transformChanged = applyTransformTo(transform, true); + // If the transform is not modified, setting the transform to + // itself will cause drift over time due to floating point errors. + if (transformChanged) { + setTransform(transform); + } transform.postScale(glm::vec3(getDimensions(), 1.0f)); batch->setModelTransform(transform); diff --git a/interface/src/ui/overlays/Overlay.cpp b/interface/src/ui/overlays/Overlay.cpp index b650da3522..675dff7e93 100644 --- a/interface/src/ui/overlays/Overlay.cpp +++ b/interface/src/ui/overlays/Overlay.cpp @@ -20,7 +20,7 @@ Overlay::Overlay() : _renderItemID(render::Item::INVALID_ITEM_ID), _isLoaded(true), _alpha(DEFAULT_ALPHA), - _pulse(0.0f), + _pulse(1.0f), _pulseMax(0.0f), _pulseMin(0.0f), _pulsePeriod(1.0f), diff --git a/interface/src/ui/overlays/Overlays.cpp b/interface/src/ui/overlays/Overlays.cpp index 72682fcb8c..b6c7dcbd46 100644 --- a/interface/src/ui/overlays/Overlays.cpp +++ b/interface/src/ui/overlays/Overlays.cpp @@ -116,7 +116,7 @@ void Overlays::renderHUD(RenderArgs* renderArgs) { QMutexLocker locker(&_mutex); foreach(Overlay::Pointer thisOverlay, _overlaysHUD) { - + // Reset all batch pipeline settings between overlay geometryCache->useSimpleDrawPipeline(batch); batch.setResourceTexture(0, textureCache->getWhiteTexture()); // FIXME - do we really need to do this?? @@ -136,7 +136,7 @@ void Overlays::enable() { _enabled = true; } -// Note, can't be invoked by scripts, but can be called by the InterfaceParentFinder +// Note, can't be invoked by scripts, but can be called by the InterfaceParentFinder // class on packet processing threads Overlay::Pointer Overlays::getOverlay(OverlayID id) const { QMutexLocker locker(&_mutex); @@ -244,8 +244,8 @@ OverlayID Overlays::cloneOverlay(OverlayID id) { bool Overlays::editOverlay(OverlayID id, const QVariant& properties) { if (QThread::currentThread() != thread()) { - // NOTE editOverlay can be called very frequently in scripts and can't afford to - // block waiting on the main thread. Additionally, no script actually + // NOTE editOverlay can be called very frequently in scripts and can't afford to + // block waiting on the main thread. Additionally, no script actually // examines the return value and does something useful with it, so use a non-blocking // invoke and just always return true QMetaObject::invokeMethod(this, "editOverlay", Q_ARG(OverlayID, id), Q_ARG(QVariant, properties)); @@ -705,28 +705,28 @@ bool Overlays::isAddedOverlay(OverlayID id) { return _overlaysHUD.contains(id) || _overlaysWorld.contains(id); } -void Overlays::sendMousePressOnOverlay(OverlayID overlayID, const PointerEvent& event) { - emit mousePressOnOverlay(overlayID, event); +void Overlays::sendMousePressOnOverlay(const OverlayID& overlayID, const PointerEvent& event) { + QMetaObject::invokeMethod(this, "mousePressOnOverlay", Q_ARG(OverlayID, overlayID), Q_ARG(PointerEvent, event)); } -void Overlays::sendMouseReleaseOnOverlay(OverlayID overlayID, const PointerEvent& event) { - emit mouseReleaseOnOverlay(overlayID, event); +void Overlays::sendMouseReleaseOnOverlay(const OverlayID& overlayID, const PointerEvent& event) { + QMetaObject::invokeMethod(this, "mouseReleaseOnOverlay", Q_ARG(OverlayID, overlayID), Q_ARG(PointerEvent, event)); } -void Overlays::sendMouseMoveOnOverlay(OverlayID overlayID, const PointerEvent& event) { - emit mouseMoveOnOverlay(overlayID, event); +void Overlays::sendMouseMoveOnOverlay(const OverlayID& overlayID, const PointerEvent& event) { + QMetaObject::invokeMethod(this, "mouseMoveOnOverlay", Q_ARG(OverlayID, overlayID), Q_ARG(PointerEvent, event)); } -void Overlays::sendHoverEnterOverlay(OverlayID id, PointerEvent event) { - emit hoverEnterOverlay(id, event); +void Overlays::sendHoverEnterOverlay(const OverlayID& id, const PointerEvent& event) { + QMetaObject::invokeMethod(this, "hoverEnterOverlay", Q_ARG(OverlayID, id), Q_ARG(PointerEvent, event)); } -void Overlays::sendHoverOverOverlay(OverlayID id, PointerEvent event) { - emit hoverOverOverlay(id, event); +void Overlays::sendHoverOverOverlay(const OverlayID& id, const PointerEvent& event) { + QMetaObject::invokeMethod(this, "hoverOverOverlay", Q_ARG(OverlayID, id), Q_ARG(PointerEvent, event)); } -void Overlays::sendHoverLeaveOverlay(OverlayID id, PointerEvent event) { - emit hoverLeaveOverlay(id, event); +void Overlays::sendHoverLeaveOverlay(const OverlayID& id, const PointerEvent& event) { + QMetaObject::invokeMethod(this, "hoverLeaveOverlay", Q_ARG(OverlayID, id), Q_ARG(PointerEvent, event)); } OverlayID Overlays::getKeyboardFocusOverlay() { @@ -775,7 +775,7 @@ float Overlays::height() { static const uint32_t MOUSE_POINTER_ID = 0; -static glm::vec2 projectOntoOverlayXYPlane(glm::vec3 position, glm::quat rotation, glm::vec2 dimensions, const PickRay& pickRay, +static glm::vec2 projectOntoOverlayXYPlane(glm::vec3 position, glm::quat rotation, glm::vec2 dimensions, const PickRay& pickRay, const RayToOverlayIntersectionResult& rayPickResult) { // Project the intersection point onto the local xy plane of the overlay. @@ -818,15 +818,20 @@ static PointerEvent::Button toPointerButton(const QMouseEvent& event) { } } -PointerEvent Overlays::calculatePointerEvent(Overlay::Pointer overlay, PickRay ray, +PointerEvent Overlays::calculateOverlayPointerEvent(OverlayID overlayID, PickRay ray, RayToOverlayIntersectionResult rayPickResult, QMouseEvent* event, PointerEvent::EventType eventType) { + auto overlay = std::dynamic_pointer_cast(getOverlay(overlayID)); + if (getOverlayType(overlayID) == "web3d") { + overlay = std::dynamic_pointer_cast(getOverlay(overlayID)); + } + if (!overlay) { + return PointerEvent(); + } + glm::vec3 position = overlay->getPosition(); + glm::quat rotation = overlay->getRotation(); + glm::vec2 dimensions = overlay->getSize(); - auto thisOverlay = std::dynamic_pointer_cast(overlay); - - auto position = thisOverlay->getPosition(); - auto rotation = thisOverlay->getRotation(); - auto dimensions = thisOverlay->getSize(); glm::vec2 pos2D = projectOntoOverlayXYPlane(position, rotation, dimensions, ray, rayPickResult); @@ -874,13 +879,9 @@ bool Overlays::mousePressEvent(QMouseEvent* event) { 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 mousePressOnOverlay(_currentClickingOnOverlayID, pointerEvent); - return true; - } + PointerEvent pointerEvent = calculateOverlayPointerEvent(_currentClickingOnOverlayID, ray, rayPickResult, event, PointerEvent::Press); + emit mousePressOnOverlay(_currentClickingOnOverlayID, pointerEvent); + return true; } emit mousePressOffOverlay(); return false; @@ -894,13 +895,9 @@ bool Overlays::mouseDoublePressEvent(QMouseEvent* event) { 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; - } + auto pointerEvent = calculateOverlayPointerEvent(_currentClickingOnOverlayID, ray, rayPickResult, event, PointerEvent::Press); + emit mouseDoublePressOnOverlay(_currentClickingOnOverlayID, pointerEvent); + return true; } emit mouseDoublePressOffOverlay(); return false; @@ -912,13 +909,8 @@ bool Overlays::mouseReleaseEvent(QMouseEvent* event) { PickRay ray = qApp->computePickRay(event->x(), event->y()); RayToOverlayIntersectionResult rayPickResult = findRayIntersectionForMouseEvent(ray); if (rayPickResult.intersects) { - - // Only Web overlays can have focus. - auto thisOverlay = std::dynamic_pointer_cast(getOverlay(rayPickResult.overlayID)); - if (thisOverlay) { - auto pointerEvent = calculatePointerEvent(thisOverlay, ray, rayPickResult, event, PointerEvent::Release); - emit mouseReleaseOnOverlay(rayPickResult.overlayID, pointerEvent); - } + auto pointerEvent = calculateOverlayPointerEvent(rayPickResult.overlayID, ray, rayPickResult, event, PointerEvent::Release); + emit mouseReleaseOnOverlay(rayPickResult.overlayID, pointerEvent); } _currentClickingOnOverlayID = UNKNOWN_OVERLAY_ID; @@ -931,40 +923,29 @@ bool Overlays::mouseMoveEvent(QMouseEvent* event) { PickRay ray = qApp->computePickRay(event->x(), event->y()); RayToOverlayIntersectionResult rayPickResult = findRayIntersectionForMouseEvent(ray); if (rayPickResult.intersects) { + auto pointerEvent = calculateOverlayPointerEvent(rayPickResult.overlayID, ray, rayPickResult, event, PointerEvent::Move); + emit mouseMoveOnOverlay(rayPickResult.overlayID, pointerEvent); - // Only Web overlays can have focus. - auto thisOverlay = std::dynamic_pointer_cast(getOverlay(rayPickResult.overlayID)); - if (thisOverlay) { - auto pointerEvent = calculatePointerEvent(thisOverlay, ray, rayPickResult, event, PointerEvent::Move); - emit mouseMoveOnOverlay(rayPickResult.overlayID, pointerEvent); - - // If previously hovering over a different overlay then leave hover on that overlay. - if (_currentHoverOverOverlayID != UNKNOWN_OVERLAY_ID && rayPickResult.overlayID != _currentHoverOverOverlayID) { - auto thisOverlay = std::dynamic_pointer_cast(getOverlay(_currentHoverOverOverlayID)); - if (thisOverlay) { - auto pointerEvent = calculatePointerEvent(thisOverlay, ray, rayPickResult, event, PointerEvent::Move); - emit hoverLeaveOverlay(_currentHoverOverOverlayID, pointerEvent); - } - } - - // If hovering over a new overlay then enter hover on that overlay. - if (rayPickResult.overlayID != _currentHoverOverOverlayID) { - emit hoverEnterOverlay(rayPickResult.overlayID, pointerEvent); - } - - // Hover over current overlay. - emit hoverOverOverlay(rayPickResult.overlayID, pointerEvent); - - _currentHoverOverOverlayID = rayPickResult.overlayID; + // If previously hovering over a different overlay then leave hover on that overlay. + if (_currentHoverOverOverlayID != UNKNOWN_OVERLAY_ID && rayPickResult.overlayID != _currentHoverOverOverlayID) { + auto pointerEvent = calculateOverlayPointerEvent(_currentHoverOverOverlayID, ray, rayPickResult, event, PointerEvent::Move); + emit hoverLeaveOverlay(_currentHoverOverOverlayID, pointerEvent); } + + // If hovering over a new overlay then enter hover on that overlay. + if (rayPickResult.overlayID != _currentHoverOverOverlayID) { + emit hoverEnterOverlay(rayPickResult.overlayID, pointerEvent); + } + + // Hover over current overlay. + emit hoverOverOverlay(rayPickResult.overlayID, pointerEvent); + + _currentHoverOverOverlayID = rayPickResult.overlayID; } else { // If previously hovering an overlay then leave hover. if (_currentHoverOverOverlayID != UNKNOWN_OVERLAY_ID) { - auto thisOverlay = std::dynamic_pointer_cast(getOverlay(_currentHoverOverOverlayID)); - if (thisOverlay) { - auto pointerEvent = calculatePointerEvent(thisOverlay, ray, rayPickResult, event, PointerEvent::Move); - emit hoverLeaveOverlay(_currentHoverOverOverlayID, pointerEvent); - } + auto pointerEvent = calculateOverlayPointerEvent(_currentHoverOverOverlayID, ray, rayPickResult, event, PointerEvent::Move); + emit hoverLeaveOverlay(_currentHoverOverOverlayID, pointerEvent); _currentHoverOverOverlayID = UNKNOWN_OVERLAY_ID; } diff --git a/interface/src/ui/overlays/Overlays.h b/interface/src/ui/overlays/Overlays.h index 100f853a96..a915acb06a 100644 --- a/interface/src/ui/overlays/Overlays.h +++ b/interface/src/ui/overlays/Overlays.h @@ -131,7 +131,7 @@ public slots: OverlayID cloneOverlay(OverlayID id); /**jsdoc - * Edit an overlay's properties. + * Edit an overlay's properties. * * @function Overlays.editOverlay * @param {Overlays.OverlayID} overlayID The ID of the overlay to edit. @@ -288,13 +288,13 @@ public slots: #endif - void sendMousePressOnOverlay(OverlayID overlayID, const PointerEvent& event); - void sendMouseReleaseOnOverlay(OverlayID overlayID, const PointerEvent& event); - void sendMouseMoveOnOverlay(OverlayID overlayID, const PointerEvent& event); + void sendMousePressOnOverlay(const OverlayID& overlayID, const PointerEvent& event); + void sendMouseReleaseOnOverlay(const OverlayID& overlayID, const PointerEvent& event); + void sendMouseMoveOnOverlay(const OverlayID& overlayID, const PointerEvent& event); - void sendHoverEnterOverlay(OverlayID id, PointerEvent event); - void sendHoverOverOverlay(OverlayID id, PointerEvent event); - void sendHoverLeaveOverlay(OverlayID id, PointerEvent event); + void sendHoverEnterOverlay(const OverlayID& id, const PointerEvent& event); + void sendHoverOverOverlay(const OverlayID& id, const PointerEvent& event); + void sendHoverLeaveOverlay(const OverlayID& id, const PointerEvent& event); OverlayID getKeyboardFocusOverlay(); void setKeyboardFocusOverlay(OverlayID id); @@ -323,7 +323,7 @@ signals: private: void cleanupOverlaysToDelete(); - mutable QMutex _mutex; + mutable QMutex _mutex { QMutex::Recursive }; QMap _overlaysHUD; QMap _overlaysWorld; #if OVERLAY_PANELS @@ -337,7 +337,7 @@ private: #endif bool _enabled = true; - PointerEvent calculatePointerEvent(Overlay::Pointer overlay, PickRay ray, RayToOverlayIntersectionResult rayPickResult, + PointerEvent calculateOverlayPointerEvent(OverlayID overlayID, PickRay ray, RayToOverlayIntersectionResult rayPickResult, QMouseEvent* event, PointerEvent::EventType eventType); OverlayID _currentClickingOnOverlayID { UNKNOWN_OVERLAY_ID }; diff --git a/interface/src/ui/overlays/PanelAttachable.cpp b/interface/src/ui/overlays/PanelAttachable.cpp index 421155083c..aae8893667 100644 --- a/interface/src/ui/overlays/PanelAttachable.cpp +++ b/interface/src/ui/overlays/PanelAttachable.cpp @@ -61,7 +61,7 @@ void PanelAttachable::setProperties(const QVariantMap& properties) { } } -void PanelAttachable::applyTransformTo(Transform& transform, bool force) { +bool PanelAttachable::applyTransformTo(Transform& transform, bool force) { if (force || usecTimestampNow() > _transformExpiry) { const quint64 TRANSFORM_UPDATE_PERIOD = 100000; // frequency is 10 Hz _transformExpiry = usecTimestampNow() + TRANSFORM_UPDATE_PERIOD; @@ -71,7 +71,9 @@ void PanelAttachable::applyTransformTo(Transform& transform, bool force) { transform.postTranslate(getOffsetPosition()); transform.postRotate(getOffsetRotation()); transform.postScale(getOffsetScale()); + return true; } #endif } + return false; } diff --git a/interface/src/ui/overlays/PanelAttachable.h b/interface/src/ui/overlays/PanelAttachable.h index 4f37cd2258..1598aa4700 100644 --- a/interface/src/ui/overlays/PanelAttachable.h +++ b/interface/src/ui/overlays/PanelAttachable.h @@ -67,7 +67,7 @@ protected: /// set position, rotation and scale on transform based on offsets, and parent panel offsets /// if force is false, only apply transform if it hasn't been applied in the last .1 seconds - virtual void applyTransformTo(Transform& transform, bool force = false); + virtual bool applyTransformTo(Transform& transform, bool force = false); quint64 _transformExpiry = 0; private: diff --git a/interface/src/ui/overlays/Planar3DOverlay.h b/interface/src/ui/overlays/Planar3DOverlay.h index 9c502ab75e..8127d4ebb9 100644 --- a/interface/src/ui/overlays/Planar3DOverlay.h +++ b/interface/src/ui/overlays/Planar3DOverlay.h @@ -21,6 +21,7 @@ public: Planar3DOverlay(const Planar3DOverlay* planar3DOverlay); virtual AABox getBounds() const override; + virtual glm::vec2 getSize() const { return _dimensions; }; glm::vec2 getDimensions() const { return _dimensions; } void setDimensions(float value) { _dimensions = glm::vec2(value); } diff --git a/interface/src/ui/overlays/Web3DOverlay.cpp b/interface/src/ui/overlays/Web3DOverlay.cpp index 80f11fa552..8dc5ae2ca2 100644 --- a/interface/src/ui/overlays/Web3DOverlay.cpp +++ b/interface/src/ui/overlays/Web3DOverlay.cpp @@ -100,30 +100,14 @@ Web3DOverlay::~Web3DOverlay() { } _webSurface->pause(); - _webSurface->disconnect(_connection); - - QObject::disconnect(_mousePressConnection); - _mousePressConnection = QMetaObject::Connection(); - QObject::disconnect(_mouseReleaseConnection); - _mouseReleaseConnection = QMetaObject::Connection(); - QObject::disconnect(_mouseMoveConnection); - _mouseMoveConnection = QMetaObject::Connection(); - QObject::disconnect(_hoverLeaveConnection); - _hoverLeaveConnection = QMetaObject::Connection(); - - QObject::disconnect(_emitScriptEventConnection); - _emitScriptEventConnection = QMetaObject::Connection(); - QObject::disconnect(_webEventReceivedConnection); - _webEventReceivedConnection = QMetaObject::Connection(); - - // The lifetime of the QML surface MUST be managed by the main thread - // Additionally, we MUST use local variables copied by value, rather than - // member variables, since they would implicitly refer to a this that - // is no longer valid - auto webSurface = _webSurface; - AbstractViewStateInterface::instance()->postLambdaEvent([webSurface] { - DependencyManager::get()->release(QML, webSurface); - }); + auto overlays = &(qApp->getOverlays()); + QObject::disconnect(overlays, &Overlays::mousePressOnOverlay, this, nullptr); + QObject::disconnect(overlays, &Overlays::mouseReleaseOnOverlay, this, nullptr); + QObject::disconnect(overlays, &Overlays::mouseMoveOnOverlay, this, nullptr); + QObject::disconnect(overlays, &Overlays::hoverLeaveOverlay, this, nullptr); + QObject::disconnect(this, &Web3DOverlay::scriptEventReceived, _webSurface.data(), &OffscreenQmlSurface::emitScriptEvent); + QObject::disconnect(_webSurface.data(), &OffscreenQmlSurface::webEventReceived, this, &Web3DOverlay::webEventReceived); + DependencyManager::get()->release(QML, _webSurface); _webSurface.reset(); } auto geometryCache = DependencyManager::get(); @@ -153,6 +137,9 @@ QString Web3DOverlay::pickURL() { void Web3DOverlay::loadSourceURL() { + if (!_webSurface) { + return; + } QUrl sourceUrl(_url); if (sourceUrl.scheme() == "http" || sourceUrl.scheme() == "https" || @@ -205,6 +192,7 @@ void Web3DOverlay::loadSourceURL() { _webSurface->getSurfaceContext()->setContextProperty("SoundCache", DependencyManager::get().data()); _webSurface->getSurfaceContext()->setContextProperty("pathToFonts", "../../"); + tabletScriptingInterface->setQmlTabletRoot("com.highfidelity.interface.tablet.system", _webSurface.data()); // mark the TabletProxy object as cpp ownership. @@ -252,23 +240,24 @@ void Web3DOverlay::render(RenderArgs* args) { } }; - _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 overlays = &(qApp->getOverlays()); + QObject::connect(overlays, &Overlays::mousePressOnOverlay, this, forwardPointerEvent); + QObject::connect(overlays, &Overlays::mouseReleaseOnOverlay, this, forwardPointerEvent); + QObject::connect(overlays, &Overlays::mouseMoveOnOverlay, this, forwardPointerEvent); + QObject::connect(overlays, &Overlays::hoverLeaveOverlay, this, [=](OverlayID overlayID, const PointerEvent& event) { auto self = weakSelf.lock(); if (!self) { return; } - if (self->_pressed && overlayID == selfOverlayID) { + if (overlayID == selfOverlayID && (self->_pressed || (!self->_activeTouchPoints.empty() && self->_touchBeginAccepted))) { PointerEvent endEvent(PointerEvent::Release, event.getID(), event.getPos2D(), event.getPos3D(), event.getNormal(), event.getDirection(), event.getButton(), event.getButtons(), event.getKeyboardModifiers()); - forwardPointerEvent(overlayID, event); + forwardPointerEvent(overlayID, endEvent); } - }, Qt::DirectConnection); + }); - _emitScriptEventConnection = connect(this, &Web3DOverlay::scriptEventReceived, _webSurface.data(), &OffscreenQmlSurface::emitScriptEvent); - _webEventReceivedConnection = connect(_webSurface.data(), &OffscreenQmlSurface::webEventReceived, this, &Web3DOverlay::webEventReceived); + QObject::connect(this, &Web3DOverlay::scriptEventReceived, _webSurface.data(), &OffscreenQmlSurface::emitScriptEvent); + QObject::connect(_webSurface.data(), &OffscreenQmlSurface::webEventReceived, this, &Web3DOverlay::webEventReceived); } else { if (_currentMaxFPS != _desiredMaxFPS) { setMaxFPS(_desiredMaxFPS); @@ -361,19 +350,68 @@ void Web3DOverlay::handlePointerEventAsTouch(const PointerEvent& event) { return; } - glm::vec2 windowPos = event.getPos2D() * (METERS_TO_INCHES * _dpi); - QPointF windowPoint(windowPos.x, windowPos.y); - - if (event.getType() == PointerEvent::Press && event.getButton() == PointerEvent::PrimaryButton) { - this->_pressed = true; - } else if (event.getType() == PointerEvent::Release && event.getButton() == PointerEvent::PrimaryButton) { - this->_pressed = false; + //do not send secondary button events to tablet + if (event.getButton() == PointerEvent::SecondaryButton || + //do not block composed events + event.getButtons() == PointerEvent::SecondaryButton) { + return; } - QEvent::Type touchType; - Qt::TouchPointState touchPointState; - QEvent::Type mouseType; + QPointF windowPoint; + { + glm::vec2 windowPos = event.getPos2D() * (METERS_TO_INCHES * _dpi); + windowPoint = QPointF(windowPos.x, windowPos.y); + } + + Qt::TouchPointState state = Qt::TouchPointStationary; + if (event.getType() == PointerEvent::Press && event.getButton() == PointerEvent::PrimaryButton) { + state = Qt::TouchPointPressed; + } else if (event.getType() == PointerEvent::Release) { + state = Qt::TouchPointReleased; + } else if (_activeTouchPoints.count(event.getID()) && windowPoint != _activeTouchPoints[event.getID()].pos()) { + state = Qt::TouchPointMoved; + } + + QEvent::Type touchType = QEvent::TouchUpdate; + if (_activeTouchPoints.empty()) { + // If the first active touch point is being created, send a begin + touchType = QEvent::TouchBegin; + } if (state == Qt::TouchPointReleased && _activeTouchPoints.size() == 1 && _activeTouchPoints.count(event.getID())) { + // If the last active touch point is being released, send an end + touchType = QEvent::TouchEnd; + } + + { + QTouchEvent::TouchPoint point; + point.setId(event.getID()); + point.setState(state); + point.setPos(windowPoint); + point.setScreenPos(windowPoint); + _activeTouchPoints[event.getID()] = point; + } + + QTouchEvent touchEvent(touchType, &_touchDevice, event.getKeyboardModifiers()); + { + QList touchPoints; + Qt::TouchPointStates touchPointStates; + for (const auto& entry : _activeTouchPoints) { + touchPointStates |= entry.second.state(); + touchPoints.push_back(entry.second); + } + + touchEvent.setWindow(_webSurface->getWindow()); + touchEvent.setTarget(_webSurface->getRootItem()); + touchEvent.setTouchPoints(touchPoints); + touchEvent.setTouchPointStates(touchPointStates); + } + + // 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". + // + // In Qt 5.9 mouse events must be sent before touch events to make sure some QtQuick components will + // receive mouse events Qt::MouseButton button = Qt::NoButton; Qt::MouseButtons buttons = Qt::NoButton; if (event.getButton() == PointerEvent::PrimaryButton) { @@ -383,87 +421,29 @@ void Web3DOverlay::handlePointerEventAsTouch(const PointerEvent& event) { buttons |= Qt::LeftButton; } - switch (event.getType()) { - case PointerEvent::Press: - touchType = QEvent::TouchBegin; - touchPointState = Qt::TouchPointPressed; - mouseType = QEvent::MouseButtonPress; - break; - case PointerEvent::Release: - touchType = QEvent::TouchEnd; - touchPointState = Qt::TouchPointReleased; - mouseType = QEvent::MouseButtonRelease; - break; - case PointerEvent::Move: - 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; - } - - //do not send secondary button events to tablet - if (event.getButton() == PointerEvent::SecondaryButton || - //do not block composed events - event.getButtons() == PointerEvent::SecondaryButton) { - return; - } - - QTouchEvent::TouchPoint point; - point.setId(event.getID()); - point.setState(touchPointState); - point.setPos(windowPoint); - point.setScreenPos(windowPoint); - QList touchPoints; - touchPoints.push_back(point); - - QTouchEvent* touchEvent = new QTouchEvent(touchType, &_touchDevice, event.getKeyboardModifiers()); - touchEvent->setWindow(_webSurface->getWindow()); - touchEvent->setTarget(_webSurface->getRootItem()); - touchEvent->setTouchPoints(touchPoints); - touchEvent->setTouchPointStates(touchPointState); - - // 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". - // - // In Qt 5.9 mouse events must be sent before touch events to make sure some QtQuick components will - // receive mouse events #if QT_VERSION >= QT_VERSION_CHECK(5, 9, 0) - if (!(this->_pressed && event.getType() == PointerEvent::Move)) { - QMouseEvent* mouseEvent = new QMouseEvent(mouseType, windowPoint, windowPoint, windowPoint, button, buttons, Qt::NoModifier); - QCoreApplication::postEvent(_webSurface->getWindow(), mouseEvent); + if (event.getType() == PointerEvent::Move) { + QMouseEvent mouseEvent(QEvent::MouseMove, windowPoint, windowPoint, windowPoint, button, buttons, Qt::NoModifier); + QCoreApplication::sendEvent(_webSurface->getWindow(), &mouseEvent); } #endif - QCoreApplication::postEvent(_webSurface->getWindow(), touchEvent); + + if (touchType == QEvent::TouchBegin) { + _touchBeginAccepted = QCoreApplication::sendEvent(_webSurface->getWindow(), &touchEvent); + } else if (_touchBeginAccepted) { + QCoreApplication::sendEvent(_webSurface->getWindow(), &touchEvent); + } + + // If this was a release event, remove the point from the active touch points + if (state == Qt::TouchPointReleased) { + _activeTouchPoints.erase(event.getID()); + } #if QT_VERSION < QT_VERSION_CHECK(5, 9, 0) - if (this->_pressed && event.getType() == PointerEvent::Move) { - return; + if (event.getType() == PointerEvent::Move) { + QMouseEvent mouseEvent(QEvent::MouseMove, windowPoint, windowPoint, windowPoint, button, buttons, Qt::NoModifier); + QCoreApplication::sendEvent(_webSurface->getWindow(), &mouseEvent); } - QMouseEvent* mouseEvent = new QMouseEvent(mouseType, windowPoint, windowPoint, windowPoint, button, buttons, Qt::NoModifier); - QCoreApplication::postEvent(_webSurface->getWindow(), mouseEvent); #endif } @@ -506,8 +486,8 @@ void Web3DOverlay::handlePointerEventAsMouse(const PointerEvent& event) { return; } - QMouseEvent* mouseEvent = new QMouseEvent(type, windowPoint, windowPoint, windowPoint, button, buttons, Qt::NoModifier); - QCoreApplication::postEvent(_webSurface->getWindow(), mouseEvent); + QMouseEvent mouseEvent(type, windowPoint, windowPoint, windowPoint, button, buttons, Qt::NoModifier); + QCoreApplication::sendEvent(_webSurface->getWindow(), &mouseEvent); } void Web3DOverlay::setProperties(const QVariantMap& properties) { @@ -609,12 +589,15 @@ void Web3DOverlay::setScriptURL(const QString& scriptURL) { _scriptURL = scriptURL; if (_webSurface) { AbstractViewStateInterface::instance()->postLambdaEvent([this, scriptURL] { + if (!_webSurface) { + return; + } _webSurface->getRootItem()->setProperty("scriptURL", scriptURL); }); } } -glm::vec2 Web3DOverlay::getSize() { +glm::vec2 Web3DOverlay::getSize() const { return _resolution / _dpi * INCHES_TO_METERS * getDimensions(); }; @@ -632,9 +615,5 @@ Web3DOverlay* Web3DOverlay::createClone() const { } void Web3DOverlay::emitScriptEvent(const QVariant& message) { - if (QThread::currentThread() != thread()) { - QMetaObject::invokeMethod(this, "emitScriptEvent", Qt::QueuedConnection, Q_ARG(QVariant, message)); - } else { - emit scriptEventReceived(message); - } + QMetaObject::invokeMethod(this, "scriptEventReceived", Q_ARG(QVariant, message)); } diff --git a/interface/src/ui/overlays/Web3DOverlay.h b/interface/src/ui/overlays/Web3DOverlay.h index 1e3706ed25..c08499cdf4 100644 --- a/interface/src/ui/overlays/Web3DOverlay.h +++ b/interface/src/ui/overlays/Web3DOverlay.h @@ -50,7 +50,7 @@ public: void setProperties(const QVariantMap& properties) override; QVariant getProperty(const QString& property) override; - glm::vec2 getSize(); + glm::vec2 getSize() const override; virtual bool findRayIntersection(const glm::vec3& origin, const glm::vec3& direction, float& distance, BoxFace& face, glm::vec3& surfaceNormal) override; @@ -72,7 +72,6 @@ signals: private: InputMode _inputMode { Touch }; QSharedPointer _webSurface; - QMetaObject::Connection _connection; gpu::TexturePointer _texture; QString _url; QString _scriptURL; @@ -82,20 +81,14 @@ private: bool _showKeyboardFocusHighlight{ true }; bool _pressed{ false }; + bool _touchBeginAccepted { false }; + std::map _activeTouchPoints; QTouchDevice _touchDevice; uint8_t _desiredMaxFPS { 10 }; uint8_t _currentMaxFPS { 0 }; bool _mayNeedResize { false }; - - QMetaObject::Connection _mousePressConnection; - QMetaObject::Connection _mouseReleaseConnection; - QMetaObject::Connection _mouseMoveConnection; - QMetaObject::Connection _hoverLeaveConnection; - - QMetaObject::Connection _emitScriptEventConnection; - QMetaObject::Connection _webEventReceivedConnection; }; #endif // hifi_Web3DOverlay_h diff --git a/libraries/animation/src/AnimInverseKinematics.cpp b/libraries/animation/src/AnimInverseKinematics.cpp index d7076a443e..57c00e7183 100644 --- a/libraries/animation/src/AnimInverseKinematics.cpp +++ b/libraries/animation/src/AnimInverseKinematics.cpp @@ -23,20 +23,23 @@ #include "CubicHermiteSpline.h" #include "AnimUtil.h" -static void lookupJointChainInfo(AnimInverseKinematics::JointChainInfo* jointChainInfos, size_t numJointChainInfos, +static const float JOINT_CHAIN_INTERP_TIME = 0.25f; + +static void lookupJointInfo(const AnimInverseKinematics::JointChainInfo& jointChainInfo, int indexA, int indexB, - AnimInverseKinematics::JointChainInfo** jointChainInfoA, - AnimInverseKinematics::JointChainInfo** jointChainInfoB) { - *jointChainInfoA = nullptr; - *jointChainInfoB = nullptr; - for (size_t i = 0; i < numJointChainInfos; i++) { - if (jointChainInfos[i].jointIndex == indexA) { - *jointChainInfoA = jointChainInfos + i; + const AnimInverseKinematics::JointInfo** jointInfoA, + const AnimInverseKinematics::JointInfo** jointInfoB) { + *jointInfoA = nullptr; + *jointInfoB = nullptr; + for (size_t i = 0; i < jointChainInfo.jointInfoVec.size(); i++) { + const AnimInverseKinematics::JointInfo* jointInfo = &jointChainInfo.jointInfoVec[i]; + if (jointInfo->jointIndex == indexA) { + *jointInfoA = jointInfo; } - if (jointChainInfos[i].jointIndex == indexB) { - *jointChainInfoB = jointChainInfos + i; + if (jointInfo->jointIndex == indexB) { + *jointInfoB = jointInfo; } - if (*jointChainInfoA && *jointChainInfoB) { + if (*jointInfoA && *jointInfoB) { break; } } @@ -149,25 +152,28 @@ void AnimInverseKinematics::setTargetVars(const QString& jointName, const QStrin } void AnimInverseKinematics::computeTargets(const AnimVariantMap& animVars, std::vector& targets, const AnimPoseVec& underPoses) { - // build a list of valid targets from _targetVarVec and animVars - _maxTargetIndex = -1; + _hipsTargetIndex = -1; - bool removeUnfoundJoints = false; + + targets.reserve(_targetVarVec.size()); for (auto& targetVar : _targetVarVec) { + + // update targetVar jointIndex cache if (targetVar.jointIndex == -1) { - // this targetVar hasn't been validated yet... int jointIndex = _skeleton->nameToJointIndex(targetVar.jointName); if (jointIndex >= 0) { // this targetVar has a valid joint --> cache the indices targetVar.jointIndex = jointIndex; } else { qCWarning(animation) << "AnimInverseKinematics could not find jointName" << targetVar.jointName << "in skeleton"; - removeUnfoundJoints = true; } - } else { - IKTarget target; + } + + IKTarget target; + if (targetVar.jointIndex != -1) { target.setType(animVars.lookup(targetVar.typeVar, (int)IKTarget::Type::RotationAndPosition)); + target.setIndex(targetVar.jointIndex); if (target.getType() != IKTarget::Type::Unknown) { AnimPose absPose = _skeleton->getAbsolutePose(targetVar.jointIndex, underPoses); glm::quat rotation = animVars.lookupRigToGeometry(targetVar.rotationVar, absPose.rot()); @@ -175,7 +181,6 @@ void AnimInverseKinematics::computeTargets(const AnimVariantMap& animVars, std:: float weight = animVars.lookup(targetVar.weightVar, targetVar.weight); target.setPose(rotation, translation); - target.setIndex(targetVar.jointIndex); target.setWeight(weight); target.setFlexCoefficients(targetVar.numFlexCoefficients, targetVar.flexCoefficients); @@ -188,39 +193,20 @@ void AnimInverseKinematics::computeTargets(const AnimVariantMap& animVars, std:: glm::vec3 poleReferenceVector = animVars.lookupRigToGeometryVector(targetVar.poleReferenceVectorVar, Vectors::UNIT_Z); target.setPoleReferenceVector(glm::normalize(poleReferenceVector)); - targets.push_back(target); - - if (targetVar.jointIndex > _maxTargetIndex) { - _maxTargetIndex = targetVar.jointIndex; - } - // record the index of the hips ik target. if (target.getIndex() == _hipsIndex) { - _hipsTargetIndex = (int)targets.size() - 1; + _hipsTargetIndex = (int)targets.size(); } } + } else { + target.setType((int)IKTarget::Type::Unknown); } - } - if (removeUnfoundJoints) { - int numVars = (int)_targetVarVec.size(); - int i = 0; - while (i < numVars) { - if (_targetVarVec[i].jointIndex == -1) { - if (numVars > 1) { - // swap i for last element - _targetVarVec[i] = _targetVarVec[numVars - 1]; - } - _targetVarVec.pop_back(); - --numVars; - } else { - ++i; - } - } + targets.push_back(target); } } -void AnimInverseKinematics::solve(const AnimContext& context, const std::vector& targets) { +void AnimInverseKinematics::solve(const AnimContext& context, const std::vector& targets, float dt, JointChainInfoVec& jointChainInfoVec) { // compute absolute poses that correspond to relative target poses AnimPoseVec absolutePoses; absolutePoses.resize(_relativePoses.size()); @@ -234,26 +220,75 @@ void AnimInverseKinematics::solve(const AnimContext& context, const std::vector< accumulator.clearAndClean(); } - float maxError = FLT_MAX; + float maxError = 0.0f; int numLoops = 0; const int MAX_IK_LOOPS = 16; - const float MAX_ERROR_TOLERANCE = 0.1f; // cm - while (maxError > MAX_ERROR_TOLERANCE && numLoops < MAX_IK_LOOPS) { + while (numLoops < MAX_IK_LOOPS) { ++numLoops; bool debug = context.getEnableDebugDrawIKChains() && numLoops == MAX_IK_LOOPS; // solve all targets - for (auto& target: targets) { - if (target.getType() == IKTarget::Type::Spline) { - solveTargetWithSpline(context, target, absolutePoses, debug); - } else { - solveTargetWithCCD(context, target, absolutePoses, debug); + for (size_t i = 0; i < targets.size(); i++) { + switch (targets[i].getType()) { + case IKTarget::Type::Unknown: + break; + case IKTarget::Type::Spline: + solveTargetWithSpline(context, targets[i], absolutePoses, debug, jointChainInfoVec[i]); + break; + default: + solveTargetWithCCD(context, targets[i], absolutePoses, debug, jointChainInfoVec[i]); + break; + } + } + + // on last iteration, interpolate jointChains, if necessary + if (numLoops == MAX_IK_LOOPS) { + for (size_t i = 0; i < _prevJointChainInfoVec.size(); i++) { + if (_prevJointChainInfoVec[i].timer > 0.0f) { + float alpha = (JOINT_CHAIN_INTERP_TIME - _prevJointChainInfoVec[i].timer) / JOINT_CHAIN_INTERP_TIME; + size_t chainSize = std::min(_prevJointChainInfoVec[i].jointInfoVec.size(), jointChainInfoVec[i].jointInfoVec.size()); + for (size_t j = 0; j < chainSize; j++) { + jointChainInfoVec[i].jointInfoVec[j].rot = safeMix(_prevJointChainInfoVec[i].jointInfoVec[j].rot, jointChainInfoVec[i].jointInfoVec[j].rot, alpha); + jointChainInfoVec[i].jointInfoVec[j].trans = lerp(_prevJointChainInfoVec[i].jointInfoVec[j].trans, jointChainInfoVec[i].jointInfoVec[j].trans, alpha); + } + + // if joint chain was just disabled, ramp the weight toward zero. + if (_prevJointChainInfoVec[i].target.getType() != IKTarget::Type::Unknown && + jointChainInfoVec[i].target.getType() == IKTarget::Type::Unknown) { + IKTarget newTarget = _prevJointChainInfoVec[i].target; + newTarget.setWeight((1.0f - alpha) * _prevJointChainInfoVec[i].target.getWeight()); + jointChainInfoVec[i].target = newTarget; + } + } + } + } + + // copy jointChainInfoVecs into accumulators + for (size_t i = 0; i < targets.size(); i++) { + const std::vector& jointInfoVec = jointChainInfoVec[i].jointInfoVec; + + // don't accumulate disabled or rotation only ik targets. + IKTarget::Type type = jointChainInfoVec[i].target.getType(); + if (type != IKTarget::Type::Unknown && type != IKTarget::Type::RotationOnly) { + float weight = jointChainInfoVec[i].target.getWeight(); + if (weight > 0.0f) { + for (size_t j = 0; j < jointInfoVec.size(); j++) { + const JointInfo& info = jointInfoVec[j]; + if (info.jointIndex >= 0) { + _rotationAccumulators[info.jointIndex].add(info.rot, weight); + _translationAccumulators[info.jointIndex].add(info.trans, weight); + } + } + } } } // harvest accumulated rotations and apply the average for (int i = 0; i < (int)_relativePoses.size(); ++i) { + if (i == _hipsIndex) { + continue; // don't apply accumulators to hips + } if (_rotationAccumulators[i].size() > 0) { _relativePoses[i].rot() = _rotationAccumulators[i].getAverage(); _rotationAccumulators[i].clear(); @@ -289,7 +324,7 @@ void AnimInverseKinematics::solve(const AnimContext& context, const std::vector< // finally set the relative rotation of each tip to agree with absolute target rotation for (auto& target: targets) { int tipIndex = target.getIndex(); - int parentIndex = _skeleton->getParentIndex(tipIndex); + int parentIndex = (tipIndex >= 0) ? _skeleton->getParentIndex(tipIndex) : -1; // update rotationOnly targets that don't lie on the ik chain of other ik targets. if (parentIndex != -1 && !_rotationAccumulators[tipIndex].isDirty() && target.getType() == IKTarget::Type::RotationOnly) { @@ -308,9 +343,34 @@ void AnimInverseKinematics::solve(const AnimContext& context, const std::vector< absolutePoses[tipIndex].rot() = targetRotation; } } + + // copy jointChainInfoVec into _prevJointChainInfoVec, and update timers + for (size_t i = 0; i < jointChainInfoVec.size(); i++) { + _prevJointChainInfoVec[i].timer = _prevJointChainInfoVec[i].timer - dt; + if (_prevJointChainInfoVec[i].timer <= 0.0f) { + _prevJointChainInfoVec[i] = jointChainInfoVec[i]; + _prevJointChainInfoVec[i].target = targets[i]; + // store relative poses into unknown/rotation only joint chains. + // so we have something to interpolate from if this chain is activated. + IKTarget::Type type = _prevJointChainInfoVec[i].target.getType(); + if (type == IKTarget::Type::Unknown || type == IKTarget::Type::RotationOnly) { + for (size_t j = 0; j < _prevJointChainInfoVec[i].jointInfoVec.size(); j++) { + auto& info = _prevJointChainInfoVec[i].jointInfoVec[j]; + if (info.jointIndex >= 0) { + info.rot = _relativePoses[info.jointIndex].rot(); + info.trans = _relativePoses[info.jointIndex].trans(); + } else { + info.rot = Quaternions::IDENTITY; + info.trans = glm::vec3(0.0f); + } + } + } + } + } } -void AnimInverseKinematics::solveTargetWithCCD(const AnimContext& context, const IKTarget& target, const AnimPoseVec& absolutePoses, bool debug) { +void AnimInverseKinematics::solveTargetWithCCD(const AnimContext& context, const IKTarget& target, const AnimPoseVec& absolutePoses, + bool debug, JointChainInfo& jointChainInfoOut) const { size_t chainDepth = 0; IKTarget::Type targetType = target.getType(); @@ -338,9 +398,6 @@ void AnimInverseKinematics::solveTargetWithCCD(const AnimContext& context, const // the tip's parent-relative as we proceed up the chain glm::quat tipParentOrientation = absolutePoses[pivotIndex].rot(); - const size_t MAX_CHAIN_DEPTH = 30; - JointChainInfo jointChainInfos[MAX_CHAIN_DEPTH]; - // NOTE: if this code is removed, the head will remain rigid, causing the spine/hips to thrust forward backward // as the head is nodded. if (targetType == IKTarget::Type::HmdHead || @@ -368,7 +425,7 @@ void AnimInverseKinematics::solveTargetWithCCD(const AnimContext& context, const } glm::vec3 tipRelativeTranslation = _relativePoses[target.getIndex()].trans(); - jointChainInfos[chainDepth] = { tipRelativeRotation, tipRelativeTranslation, target.getWeight(), tipIndex, constrained }; + jointChainInfoOut.jointInfoVec[chainDepth] = { tipRelativeRotation, tipRelativeTranslation, tipIndex, constrained }; } // cache tip absolute position @@ -379,7 +436,7 @@ void AnimInverseKinematics::solveTargetWithCCD(const AnimContext& context, const // descend toward root, pivoting each joint to get tip closer to target position while (pivotIndex != _hipsIndex && pivotsParentIndex != -1) { - assert(chainDepth < MAX_CHAIN_DEPTH); + assert(chainDepth < jointChainInfoOut.jointInfoVec.size()); // compute the two lines that should be aligned glm::vec3 jointPosition = absolutePoses[pivotIndex].trans(); @@ -444,9 +501,8 @@ void AnimInverseKinematics::solveTargetWithCCD(const AnimContext& context, const glm::quat twistPart; glm::vec3 axis = glm::normalize(deltaRotation * leverArm); swingTwistDecomposition(missingRotation, axis, swingPart, twistPart); - float dotSign = copysignf(1.0f, twistPart.w); const float LIMIT_LEAK_FRACTION = 0.1f; - deltaRotation = glm::normalize(glm::lerp(glm::quat(), dotSign * twistPart, LIMIT_LEAK_FRACTION)) * deltaRotation; + deltaRotation = safeLerp(glm::quat(), twistPart, LIMIT_LEAK_FRACTION); } } } @@ -455,9 +511,8 @@ void AnimInverseKinematics::solveTargetWithCCD(const AnimContext& context, const // An HmdHead target slaves the orientation of the end-effector by distributing rotation // deltas up the hierarchy. Its target position is enforced later (by shifting the hips). deltaRotation = target.getRotation() * glm::inverse(tipOrientation); - float dotSign = copysignf(1.0f, deltaRotation.w); const float ANGLE_DISTRIBUTION_FACTOR = 0.45f; - deltaRotation = glm::normalize(glm::lerp(glm::quat(), dotSign * deltaRotation, ANGLE_DISTRIBUTION_FACTOR)); + deltaRotation = safeLerp(glm::quat(), deltaRotation, ANGLE_DISTRIBUTION_FACTOR); } // compute joint's new parent-relative rotation after swing @@ -480,7 +535,7 @@ void AnimInverseKinematics::solveTargetWithCCD(const AnimContext& context, const } glm::vec3 newTrans = _relativePoses[pivotIndex].trans(); - jointChainInfos[chainDepth] = { newRot, newTrans, target.getWeight(), pivotIndex, constrained }; + jointChainInfoOut.jointInfoVec[chainDepth] = { newRot, newTrans, pivotIndex, constrained }; // keep track of tip's new transform as we descend towards root tipPosition = jointPosition + deltaRotation * (tipPosition - jointPosition); @@ -502,24 +557,25 @@ void AnimInverseKinematics::solveTargetWithCCD(const AnimContext& context, const int baseParentJointIndex = _skeleton->getParentIndex(baseJointIndex); AnimPose topPose, midPose, basePose; int topChainIndex = -1, baseChainIndex = -1; + const size_t MAX_CHAIN_DEPTH = 30; AnimPose postAbsPoses[MAX_CHAIN_DEPTH]; AnimPose accum = absolutePoses[_hipsIndex]; AnimPose baseParentPose = absolutePoses[_hipsIndex]; for (int i = (int)chainDepth - 1; i >= 0; i--) { - accum = accum * AnimPose(glm::vec3(1.0f), jointChainInfos[i].relRot, jointChainInfos[i].relTrans); + accum = accum * AnimPose(glm::vec3(1.0f), jointChainInfoOut.jointInfoVec[i].rot, jointChainInfoOut.jointInfoVec[i].trans); postAbsPoses[i] = accum; - if (jointChainInfos[i].jointIndex == topJointIndex) { + if (jointChainInfoOut.jointInfoVec[i].jointIndex == topJointIndex) { topChainIndex = i; topPose = accum; } - if (jointChainInfos[i].jointIndex == midJointIndex) { + if (jointChainInfoOut.jointInfoVec[i].jointIndex == midJointIndex) { midPose = accum; } - if (jointChainInfos[i].jointIndex == baseJointIndex) { + if (jointChainInfoOut.jointInfoVec[i].jointIndex == baseJointIndex) { baseChainIndex = i; basePose = accum; } - if (jointChainInfos[i].jointIndex == baseParentJointIndex) { + if (jointChainInfoOut.jointInfoVec[i].jointIndex == baseParentJointIndex) { baseParentPose = accum; } } @@ -599,21 +655,16 @@ void AnimInverseKinematics::solveTargetWithCCD(const AnimContext& context, const } glm::quat newBaseRelRot = glm::inverse(baseParentPose.rot()) * poleRot * basePose.rot(); - jointChainInfos[baseChainIndex].relRot = newBaseRelRot; + jointChainInfoOut.jointInfoVec[baseChainIndex].rot = newBaseRelRot; glm::quat newTopRelRot = glm::inverse(midPose.rot()) * glm::inverse(poleRot) * topPose.rot(); - jointChainInfos[topChainIndex].relRot = newTopRelRot; + jointChainInfoOut.jointInfoVec[topChainIndex].rot = newTopRelRot; } } } - for (size_t i = 0; i < chainDepth; i++) { - _rotationAccumulators[jointChainInfos[i].jointIndex].add(jointChainInfos[i].relRot, jointChainInfos[i].weight); - _translationAccumulators[jointChainInfos[i].jointIndex].add(jointChainInfos[i].relTrans, jointChainInfos[i].weight); - } - if (debug) { - debugDrawIKChain(jointChainInfos, chainDepth, context); + debugDrawIKChain(jointChainInfoOut, context); } } @@ -628,7 +679,7 @@ static CubicHermiteSplineFunctorWithArcLength computeSplineFromTipAndBase(const } // pre-compute information about each joint influeced by this spline IK target. -void AnimInverseKinematics::computeSplineJointInfosForIKTarget(const AnimContext& context, const IKTarget& target) { +void AnimInverseKinematics::computeAndCacheSplineJointInfosForIKTarget(const AnimContext& context, const IKTarget& target) const { std::vector splineJointInfoVec; // build spline between the default poses. @@ -681,13 +732,13 @@ void AnimInverseKinematics::computeSplineJointInfosForIKTarget(const AnimContext _splineJointInfoMap[target.getIndex()] = splineJointInfoVec; } -const std::vector* AnimInverseKinematics::findOrCreateSplineJointInfo(const AnimContext& context, const IKTarget& target) { +const std::vector* AnimInverseKinematics::findOrCreateSplineJointInfo(const AnimContext& context, const IKTarget& target) const { // find or create splineJointInfo for this target auto iter = _splineJointInfoMap.find(target.getIndex()); if (iter != _splineJointInfoMap.end()) { return &(iter->second); } else { - computeSplineJointInfosForIKTarget(context, target); + computeAndCacheSplineJointInfosForIKTarget(context, target); auto iter = _splineJointInfoMap.find(target.getIndex()); if (iter != _splineJointInfoMap.end()) { return &(iter->second); @@ -697,10 +748,8 @@ const std::vector* AnimInverseKinematics return nullptr; } -void AnimInverseKinematics::solveTargetWithSpline(const AnimContext& context, const IKTarget& target, const AnimPoseVec& absolutePoses, bool debug) { - - const size_t MAX_CHAIN_DEPTH = 30; - JointChainInfo jointChainInfos[MAX_CHAIN_DEPTH]; +void AnimInverseKinematics::solveTargetWithSpline(const AnimContext& context, const IKTarget& target, const AnimPoseVec& absolutePoses, + bool debug, JointChainInfo& jointChainInfoOut) const { const int baseIndex = _hipsIndex; @@ -720,7 +769,7 @@ void AnimInverseKinematics::solveTargetWithSpline(const AnimContext& context, co // This prevents the rotation interpolation from rotating the wrong physical way (but correct mathematical way) // when the head is arched backwards very far. - glm::quat halfRot = glm::normalize(glm::lerp(basePose.rot(), tipPose.rot(), 0.5f)); + glm::quat halfRot = safeLerp(basePose.rot(), tipPose.rot(), 0.5f); if (glm::dot(halfRot * Vectors::UNIT_Z, basePose.rot() * Vectors::UNIT_Z) < 0.0f) { tipPose.rot() = -tipPose.rot(); } @@ -743,7 +792,7 @@ void AnimInverseKinematics::solveTargetWithSpline(const AnimContext& context, co if (target.getIndex() == _headIndex) { rotT = t * t; } - glm::quat twistRot = glm::normalize(glm::lerp(basePose.rot(), tipPose.rot(), rotT)); + glm::quat twistRot = safeLerp(basePose.rot(), tipPose.rot(), rotT); // compute the rotation by using the derivative of the spline as the y-axis, and the twistRot x-axis glm::vec3 y = glm::normalize(spline.d(t)); @@ -783,19 +832,14 @@ void AnimInverseKinematics::solveTargetWithSpline(const AnimContext& context, co } } - jointChainInfos[i] = { relPose.rot(), relPose.trans(), target.getWeight(), splineJointInfo.jointIndex, constrained }; + jointChainInfoOut.jointInfoVec[i] = { relPose.rot(), relPose.trans(), splineJointInfo.jointIndex, constrained }; parentAbsPose = flexedAbsPose; } } - for (size_t i = 0; i < splineJointInfoVec->size(); i++) { - _rotationAccumulators[jointChainInfos[i].jointIndex].add(jointChainInfos[i].relRot, jointChainInfos[i].weight); - _translationAccumulators[jointChainInfos[i].jointIndex].add(jointChainInfos[i].relTrans, jointChainInfos[i].weight); - } - if (debug) { - debugDrawIKChain(jointChainInfos, splineJointInfoVec->size(), context); + debugDrawIKChain(jointChainInfoOut, context); } } @@ -806,6 +850,24 @@ const AnimPoseVec& AnimInverseKinematics::evaluate(const AnimVariantMap& animVar return _relativePoses; } +AnimPose AnimInverseKinematics::applyHipsOffset() const { + glm::vec3 hipsOffset = _hipsOffset; + AnimPose relHipsPose = _relativePoses[_hipsIndex]; + float offsetLength = glm::length(hipsOffset); + const float MIN_HIPS_OFFSET_LENGTH = 0.03f; + if (offsetLength > MIN_HIPS_OFFSET_LENGTH) { + float scaleFactor = ((offsetLength - MIN_HIPS_OFFSET_LENGTH) / offsetLength); + glm::vec3 scaledHipsOffset = scaleFactor * hipsOffset; + if (_hipsParentIndex == -1) { + relHipsPose.trans() = _relativePoses[_hipsIndex].trans() + scaledHipsOffset; + } else { + AnimPose absHipsPose = _skeleton->getAbsolutePose(_hipsIndex, _relativePoses); + absHipsPose.trans() += scaledHipsOffset; + relHipsPose = _skeleton->getAbsolutePose(_hipsParentIndex, _relativePoses).inverse() * absHipsPose; + } + } + return relHipsPose; +} //virtual const AnimPoseVec& AnimInverseKinematics::overlay(const AnimVariantMap& animVars, const AnimContext& context, float dt, Triggers& triggersOut, const AnimPoseVec& underPoses) { @@ -850,33 +912,88 @@ const AnimPoseVec& AnimInverseKinematics::overlay(const AnimVariantMap& animVars _relativePoses = underPoses; } else { + JointChainInfoVec jointChainInfoVec(targets.size()); + { + PROFILE_RANGE_EX(simulation_animation, "ik/jointChainInfo", 0xffff00ff, 0); + + // initialize a new jointChainInfoVec, this will hold the results for solving each ik chain. + JointInfo defaultJointInfo = { glm::quat(), glm::vec3(), -1, false }; + for (size_t i = 0; i < targets.size(); i++) { + size_t chainDepth = (size_t)_skeleton->getChainDepth(targets[i].getIndex()); + jointChainInfoVec[i].jointInfoVec.reserve(chainDepth); + jointChainInfoVec[i].target = targets[i]; + int index = targets[i].getIndex(); + for (size_t j = 0; j < chainDepth; j++) { + jointChainInfoVec[i].jointInfoVec.push_back(defaultJointInfo); + jointChainInfoVec[i].jointInfoVec[j].jointIndex = index; + index = _skeleton->getParentIndex(index); + } + } + + // identify joint chains that have changed types this frame. + _prevJointChainInfoVec.resize(jointChainInfoVec.size()); + for (size_t i = 0; i < _prevJointChainInfoVec.size(); i++) { + if (_prevJointChainInfoVec[i].timer <= 0.0f && + (jointChainInfoVec[i].target.getType() != _prevJointChainInfoVec[i].target.getType() || + jointChainInfoVec[i].target.getPoleVectorEnabled() != _prevJointChainInfoVec[i].target.getPoleVectorEnabled())) { + _prevJointChainInfoVec[i].timer = JOINT_CHAIN_INTERP_TIME; + } + } + } + { PROFILE_RANGE_EX(simulation_animation, "ik/shiftHips", 0xffff00ff, 0); - if (_hipsTargetIndex >= 0 && _hipsTargetIndex < (int)targets.size()) { + if (_hipsTargetIndex >= 0) { + assert(_hipsTargetIndex < (int)targets.size()); + // slam the hips to match the _hipsTarget + AnimPose absPose = targets[_hipsTargetIndex].getPose(); + int parentIndex = _skeleton->getParentIndex(targets[_hipsTargetIndex].getIndex()); - if (parentIndex != -1) { - _relativePoses[_hipsIndex] = _skeleton->getAbsolutePose(parentIndex, _relativePoses).inverse() * absPose; - } else { - _relativePoses[_hipsIndex] = absPose; + AnimPose parentAbsPose = _skeleton->getAbsolutePose(parentIndex, _relativePoses); + + // do smooth interpolation of hips, if necessary. + if (_prevJointChainInfoVec[_hipsTargetIndex].timer > 0.0f && _prevJointChainInfoVec[_hipsTargetIndex].jointInfoVec.size() > 0) { + float alpha = (JOINT_CHAIN_INTERP_TIME - _prevJointChainInfoVec[_hipsTargetIndex].timer) / JOINT_CHAIN_INTERP_TIME; + + auto& info = _prevJointChainInfoVec[_hipsTargetIndex].jointInfoVec[0]; + AnimPose prevHipsRelPose(info.rot, info.trans); + AnimPose prevHipsAbsPose = parentAbsPose * prevHipsRelPose; + ::blend(1, &prevHipsAbsPose, &absPose, alpha, &absPose); } - } else { + + _relativePoses[_hipsIndex] = parentAbsPose.inverse() * absPose; + _relativePoses[_hipsIndex].scale() = glm::vec3(1.0f); + _hipsOffset = Vectors::ZERO; + + } else if (_hipsIndex >= 0) { + // if there is no hips target, shift hips according to the _hipsOffset from the previous frame - float offsetLength = glm::length(_hipsOffset); - const float MIN_HIPS_OFFSET_LENGTH = 0.03f; - if (offsetLength > MIN_HIPS_OFFSET_LENGTH && _hipsIndex >= 0) { - float scaleFactor = ((offsetLength - MIN_HIPS_OFFSET_LENGTH) / offsetLength); - glm::vec3 hipsOffset = scaleFactor * _hipsOffset; - if (_hipsParentIndex == -1) { - _relativePoses[_hipsIndex].trans() = _relativePoses[_hipsIndex].trans() + hipsOffset; - } else { - auto absHipsPose = _skeleton->getAbsolutePose(_hipsIndex, _relativePoses); - absHipsPose.trans() += hipsOffset; - _relativePoses[_hipsIndex] = _skeleton->getAbsolutePose(_hipsParentIndex, _relativePoses).inverse() * absHipsPose; + AnimPose relHipsPose = applyHipsOffset(); + + // determine if we should begin interpolating the hips. + for (size_t i = 0; i < targets.size(); i++) { + if (_prevJointChainInfoVec[i].target.getIndex() == _hipsIndex) { + if (_prevJointChainInfoVec[i].timer > 0.0f) { + // smoothly lerp in hipsOffset + float alpha = (JOINT_CHAIN_INTERP_TIME - _prevJointChainInfoVec[i].timer) / JOINT_CHAIN_INTERP_TIME; + AnimPose prevRelHipsPose(_prevJointChainInfoVec[i].jointInfoVec[0].rot, _prevJointChainInfoVec[i].jointInfoVec[0].trans); + ::blend(1, &prevRelHipsPose, &relHipsPose, alpha, &relHipsPose); + } + break; } } + + _relativePoses[_hipsIndex] = relHipsPose; + } + + // if there is an active jointChainInfo for the hips store the post shifted hips into it. + // This is so we have a valid pose to interplate from when the hips target is disabled. + if (_hipsTargetIndex >= 0) { + jointChainInfoVec[_hipsTargetIndex].jointInfoVec[0].rot = _relativePoses[_hipsIndex].rot(); + jointChainInfoVec[_hipsTargetIndex].jointInfoVec[0].trans = _relativePoses[_hipsIndex].trans(); } // update all HipsRelative targets to account for the hips shift/ik target. @@ -920,15 +1037,14 @@ const AnimPoseVec& AnimInverseKinematics::overlay(const AnimVariantMap& animVars { PROFILE_RANGE_EX(simulation_animation, "ik/ccd", 0xffff00ff, 0); + preconditionRelativePosesToAvoidLimbLock(context, targets); - solve(context, targets); + solve(context, targets, dt, jointChainInfoVec); } if (_hipsTargetIndex < 0) { PROFILE_RANGE_EX(simulation_animation, "ik/measureHipsOffset", 0xffff00ff, 0); - computeHipsOffset(targets, underPoses, dt); - } else { - _hipsOffset = Vectors::ZERO; + _hipsOffset = computeHipsOffset(targets, underPoses, dt, _hipsOffset); } } @@ -937,23 +1053,15 @@ const AnimPoseVec& AnimInverseKinematics::overlay(const AnimVariantMap& animVars } } - if (_leftHandIndex > -1) { - _uncontrolledLeftHandPose = _skeleton->getAbsolutePose(_leftHandIndex, underPoses); - } - if (_rightHandIndex > -1) { - _uncontrolledRightHandPose = _skeleton->getAbsolutePose(_rightHandIndex, underPoses); - } - if (_hipsIndex > -1) { - _uncontrolledHipsPose = _skeleton->getAbsolutePose(_hipsIndex, underPoses); - } - return _relativePoses; } -void AnimInverseKinematics::computeHipsOffset(const std::vector& targets, const AnimPoseVec& underPoses, float dt) { +glm::vec3 AnimInverseKinematics::computeHipsOffset(const std::vector& targets, const AnimPoseVec& underPoses, float dt, glm::vec3 prevHipsOffset) const { + // 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) + glm::vec3 hipsOffset = prevHipsOffset; glm::vec3 newHipsOffset = Vectors::ZERO; for (auto& target: targets) { int targetIndex = target.getIndex(); @@ -969,9 +1077,9 @@ void AnimInverseKinematics::computeHipsOffset(const std::vector& targe } 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(); - _hipsOffset += target.getTranslation() - actual; + hipsOffset += target.getTranslation() - actual; // and ignore all other targets - newHipsOffset = _hipsOffset; + newHipsOffset = hipsOffset; break; } else if (target.getType() == IKTarget::Type::RotationAndPosition) { glm::vec3 actualPosition = _skeleton->getAbsolutePose(targetIndex, _relativePoses).trans(); @@ -991,16 +1099,18 @@ void AnimInverseKinematics::computeHipsOffset(const std::vector& targe } } - // smooth transitions by relaxing _hipsOffset toward the new value + // 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 += (newHipsOffset - _hipsOffset) * tau; + hipsOffset += (newHipsOffset - hipsOffset) * tau; // clamp the hips offset - float hipsOffsetLength = glm::length(_hipsOffset); + float hipsOffsetLength = glm::length(hipsOffset); if (hipsOffsetLength > _maxHipsOffsetLength) { - _hipsOffset *= _maxHipsOffsetLength / hipsOffsetLength; + hipsOffset *= _maxHipsOffsetLength / hipsOffsetLength; } + + return hipsOffset; } void AnimInverseKinematics::setMaxHipsOffsetLength(float maxLength) { @@ -1414,8 +1524,6 @@ void AnimInverseKinematics::setSkeletonInternal(AnimSkeleton::ConstPointer skele targetVar.jointIndex = -1; } - _maxTargetIndex = -1; - for (auto& accumulator: _rotationAccumulators) { accumulator.clearAndClean(); } @@ -1446,10 +1554,6 @@ void AnimInverseKinematics::setSkeletonInternal(AnimSkeleton::ConstPointer skele _leftHandIndex = -1; _rightHandIndex = -1; } - - _uncontrolledLeftHandPose = AnimPose(); - _uncontrolledRightHandPose = AnimPose(); - _uncontrolledHipsPose = AnimPose(); } static glm::vec3 sphericalToCartesian(float phi, float theta) { @@ -1495,14 +1599,14 @@ void AnimInverseKinematics::debugDrawRelativePoses(const AnimContext& context) c } } -void AnimInverseKinematics::debugDrawIKChain(JointChainInfo* jointChainInfos, size_t numJointChainInfos, const AnimContext& context) const { +void AnimInverseKinematics::debugDrawIKChain(const JointChainInfo& jointChainInfo, const AnimContext& context) const { AnimPoseVec poses = _relativePoses; // copy debug joint rotations into the relative poses - for (size_t i = 0; i < numJointChainInfos; i++) { - const JointChainInfo& info = jointChainInfos[i]; - poses[info.jointIndex].rot() = info.relRot; - poses[info.jointIndex].trans() = info.relTrans; + for (size_t i = 0; i < jointChainInfo.jointInfoVec.size(); i++) { + const JointInfo& info = jointChainInfo.jointInfoVec[i]; + poses[info.jointIndex].rot() = info.rot; + poses[info.jointIndex].trans() = info.trans; } // convert relative poses to absolute @@ -1519,9 +1623,9 @@ void AnimInverseKinematics::debugDrawIKChain(JointChainInfo* jointChainInfos, si // draw each pose for (int i = 0; i < (int)poses.size(); i++) { int parentIndex = _skeleton->getParentIndex(i); - JointChainInfo* jointInfo = nullptr; - JointChainInfo* parentJointInfo = nullptr; - lookupJointChainInfo(jointChainInfos, numJointChainInfos, i, parentIndex, &jointInfo, &parentJointInfo); + const JointInfo* jointInfo = nullptr; + const JointInfo* parentJointInfo = nullptr; + lookupJointInfo(jointChainInfo, i, parentIndex, &jointInfo, &parentJointInfo); if (jointInfo && parentJointInfo) { // transform local axes into world space. @@ -1608,7 +1712,7 @@ void AnimInverseKinematics::debugDrawConstraints(const AnimContext& context) con const int NUM_SWING_STEPS = 10; for (int i = 0; i < NUM_SWING_STEPS + 1; i++) { - glm::quat rot = glm::normalize(glm::lerp(minRot, maxRot, i * (1.0f / NUM_SWING_STEPS))); + glm::quat rot = safeLerp(minRot, maxRot, i * (1.0f / NUM_SWING_STEPS)); glm::vec3 axis = transformVectorFast(geomToWorldMatrix, parentAbsRot * rot * refRot * Vectors::UNIT_Y); DebugDraw::getInstance().drawRay(pos, pos + TWIST_LENGTH * axis, CYAN); } @@ -1626,7 +1730,7 @@ void AnimInverseKinematics::debugDrawConstraints(const AnimContext& context) con const int NUM_SWING_STEPS = 10; for (int i = 0; i < NUM_SWING_STEPS + 1; i++) { - glm::quat rot = glm::normalize(glm::lerp(minRot, maxRot, i * (1.0f / NUM_SWING_STEPS))); + glm::quat rot = safeLerp(minRot, maxRot, i * (1.0f / NUM_SWING_STEPS)); glm::vec3 axis = transformVectorFast(geomToWorldMatrix, parentAbsRot * rot * refRot * Vectors::UNIT_X); DebugDraw::getInstance().drawRay(pos, pos + TWIST_LENGTH * axis, CYAN); } @@ -1666,10 +1770,9 @@ void AnimInverseKinematics::blendToPoses(const AnimPoseVec& targetPoses, const A // relax toward poses int numJoints = (int)_relativePoses.size(); for (int i = 0; i < numJoints; ++i) { - float dotSign = copysignf(1.0f, glm::dot(_relativePoses[i].rot(), targetPoses[i].rot())); if (_rotationAccumulators[i].isDirty()) { // this joint is affected by IK --> blend toward the targetPoses rotation - _relativePoses[i].rot() = glm::normalize(glm::lerp(_relativePoses[i].rot(), dotSign * targetPoses[i].rot(), blendFactor)); + _relativePoses[i].rot() = safeLerp(_relativePoses[i].rot(), targetPoses[i].rot(), blendFactor); } else { // this joint is NOT affected by IK --> slam to underPoses rotation _relativePoses[i].rot() = underPoses[i].rot(); diff --git a/libraries/animation/src/AnimInverseKinematics.h b/libraries/animation/src/AnimInverseKinematics.h index d473ae3698..7f7640aa24 100644 --- a/libraries/animation/src/AnimInverseKinematics.h +++ b/libraries/animation/src/AnimInverseKinematics.h @@ -26,14 +26,21 @@ class RotationConstraint; class AnimInverseKinematics : public AnimNode { public: - struct JointChainInfo { - glm::quat relRot; - glm::vec3 relTrans; - float weight; + struct JointInfo { + glm::quat rot; + glm::vec3 trans; int jointIndex; bool constrained; }; + struct JointChainInfo { + std::vector jointInfoVec; + IKTarget target; + float timer { 0.0f }; + }; + + using JointChainInfoVec = std::vector; + explicit AnimInverseKinematics(const QString& id); virtual ~AnimInverseKinematics() override; @@ -66,23 +73,22 @@ public: void setSolutionSource(SolutionSource solutionSource) { _solutionSource = solutionSource; } void setSolutionSourceVar(const QString& solutionSourceVar) { _solutionSourceVar = solutionSourceVar; } - const AnimPose& getUncontrolledLeftHandPose() { return _uncontrolledLeftHandPose; } - const AnimPose& getUncontrolledRightHandPose() { return _uncontrolledRightHandPose; } - const AnimPose& getUncontrolledHipPose() { return _uncontrolledHipsPose; } - protected: void computeTargets(const AnimVariantMap& animVars, std::vector& targets, const AnimPoseVec& underPoses); - void solve(const AnimContext& context, const std::vector& targets); - void solveTargetWithCCD(const AnimContext& context, const IKTarget& target, const AnimPoseVec& absolutePoses, bool debug); - void solveTargetWithSpline(const AnimContext& context, const IKTarget& target, const AnimPoseVec& absolutePoses, bool debug); + void solve(const AnimContext& context, const std::vector& targets, float dt, JointChainInfoVec& jointChainInfoVec); + void solveTargetWithCCD(const AnimContext& context, const IKTarget& target, const AnimPoseVec& absolutePoses, + bool debug, JointChainInfo& jointChainInfoOut) const; + void solveTargetWithSpline(const AnimContext& context, const IKTarget& target, const AnimPoseVec& absolutePoses, + bool debug, JointChainInfo& jointChainInfoOut) const; virtual void setSkeletonInternal(AnimSkeleton::ConstPointer skeleton) override; - void debugDrawIKChain(JointChainInfo* jointChainInfos, size_t numJointChainInfos, const AnimContext& context) const; + void debugDrawIKChain(const JointChainInfo& jointChainInfo, const AnimContext& context) const; void debugDrawRelativePoses(const AnimContext& context) const; void debugDrawConstraints(const AnimContext& context) const; void debugDrawSpineSplines(const AnimContext& context, const std::vector& targets) const; void initRelativePosesFromSolutionSource(SolutionSource solutionSource, const AnimPoseVec& underPose); void blendToPoses(const AnimPoseVec& targetPoses, const AnimPoseVec& underPose, float blendFactor); void preconditionRelativePosesToAvoidLimbLock(const AnimContext& context, const std::vector& targets); + AnimPose applyHipsOffset() const; // used to pre-compute information about each joint influeced by a spline IK target. struct SplineJointInfo { @@ -91,8 +97,8 @@ protected: AnimPose offsetPose; // local offset from the spline to the joint. }; - void computeSplineJointInfosForIKTarget(const AnimContext& context, const IKTarget& target); - const std::vector* findOrCreateSplineJointInfo(const AnimContext& context, const IKTarget& target); + void computeAndCacheSplineJointInfosForIKTarget(const AnimContext& context, const IKTarget& target) const; + const std::vector* findOrCreateSplineJointInfo(const AnimContext& context, const IKTarget& target) const; // for AnimDebugDraw rendering virtual const AnimPoseVec& getPosesInternal() const override { return _relativePoses; } @@ -101,7 +107,7 @@ protected: void clearConstraints(); void initConstraints(); void initLimitCenterPoses(); - void computeHipsOffset(const std::vector& targets, const AnimPoseVec& underPoses, float dt); + glm::vec3 computeHipsOffset(const std::vector& targets, const AnimPoseVec& underPoses, float dt, glm::vec3 prevHipsOffset) const; // no copies AnimInverseKinematics(const AnimInverseKinematics&) = delete; @@ -136,7 +142,7 @@ protected: AnimPoseVec _relativePoses; // current relative poses AnimPoseVec _limitCenterPoses; // relative - std::map> _splineJointInfoMap; + mutable std::map> _splineJointInfoMap; // experimental data for moving hips during IK glm::vec3 _hipsOffset { Vectors::ZERO }; @@ -148,18 +154,12 @@ protected: int _leftHandIndex { -1 }; int _rightHandIndex { -1 }; - // _maxTargetIndex is tracked to help optimize the recalculation of absolute poses - // during the the cyclic coordinate descent algorithm - int _maxTargetIndex { 0 }; - float _maxErrorOnLastSolve { FLT_MAX }; bool _previousEnableDebugIKTargets { false }; SolutionSource _solutionSource { SolutionSource::RelaxToUnderPoses }; QString _solutionSourceVar; - AnimPose _uncontrolledLeftHandPose { AnimPose() }; - AnimPose _uncontrolledRightHandPose { AnimPose() }; - AnimPose _uncontrolledHipsPose { AnimPose() }; + JointChainInfoVec _prevJointChainInfoVec; }; #endif // hifi_AnimInverseKinematics_h diff --git a/libraries/animation/src/AnimSkeleton.cpp b/libraries/animation/src/AnimSkeleton.cpp index 062e016660..804ffb0583 100644 --- a/libraries/animation/src/AnimSkeleton.cpp +++ b/libraries/animation/src/AnimSkeleton.cpp @@ -42,6 +42,20 @@ int AnimSkeleton::getNumJoints() const { return _jointsSize; } +int AnimSkeleton::getChainDepth(int jointIndex) const { + if (jointIndex >= 0) { + int chainDepth = 0; + int index = jointIndex; + do { + chainDepth++; + index = _joints[index].parentIndex; + } while (index != -1); + return chainDepth; + } else { + return 0; + } +} + const AnimPose& AnimSkeleton::getAbsoluteBindPose(int jointIndex) const { return _absoluteBindPoses[jointIndex]; } diff --git a/libraries/animation/src/AnimSkeleton.h b/libraries/animation/src/AnimSkeleton.h index 6315f2d62b..99c9a148f7 100644 --- a/libraries/animation/src/AnimSkeleton.h +++ b/libraries/animation/src/AnimSkeleton.h @@ -28,6 +28,7 @@ public: int nameToJointIndex(const QString& jointName) const; const QString& getJointName(int jointIndex) const; int getNumJoints() const; + int getChainDepth(int jointIndex) const; // absolute pose, not relative to parent const AnimPose& getAbsoluteBindPose(int jointIndex) const; diff --git a/libraries/animation/src/AnimUtil.cpp b/libraries/animation/src/AnimUtil.cpp index a4659f1e76..bcf30642e8 100644 --- a/libraries/animation/src/AnimUtil.cpp +++ b/libraries/animation/src/AnimUtil.cpp @@ -28,7 +28,7 @@ void blend(size_t numPoses, const AnimPose* a, const AnimPose* b, float alpha, A } result[i].scale() = lerp(aPose.scale(), bPose.scale(), alpha); - result[i].rot() = glm::normalize(glm::lerp(aPose.rot(), q2, alpha)); + result[i].rot() = safeLerp(aPose.rot(), bPose.rot(), alpha); result[i].trans() = lerp(aPose.trans(), bPose.trans(), alpha); } } diff --git a/libraries/animation/src/AnimUtil.h b/libraries/animation/src/AnimUtil.h index 055fd630eb..d215fdc654 100644 --- a/libraries/animation/src/AnimUtil.h +++ b/libraries/animation/src/AnimUtil.h @@ -21,4 +21,14 @@ glm::quat averageQuats(size_t numQuats, const glm::quat* quats); float accumulateTime(float startFrame, float endFrame, float timeScale, float currentFrame, float dt, bool loopFlag, const QString& id, AnimNode::Triggers& triggersOut); +inline glm::quat safeLerp(const glm::quat& a, const glm::quat& b, float alpha) { + // adjust signs if necessary + glm::quat bTemp = b; + float dot = glm::dot(a, bTemp); + if (dot < 0.0f) { + bTemp = -bTemp; + } + return glm::normalize(glm::lerp(a, bTemp, alpha)); +} + #endif diff --git a/libraries/animation/src/IKTarget.h b/libraries/animation/src/IKTarget.h index 5567539659..325a1b40b6 100644 --- a/libraries/animation/src/IKTarget.h +++ b/libraries/animation/src/IKTarget.h @@ -56,8 +56,8 @@ private: glm::vec3 _poleReferenceVector; bool _poleVectorEnabled { false }; int _index { -1 }; - Type _type { Type::RotationAndPosition }; - float _weight; + Type _type { Type::Unknown }; + float _weight { 0.0f }; float _flexCoefficients[MAX_FLEX_COEFFICIENTS]; size_t _numFlexCoefficients; }; diff --git a/libraries/animation/src/Rig.cpp b/libraries/animation/src/Rig.cpp index 6ebb68773f..fc0ca73c96 100644 --- a/libraries/animation/src/Rig.cpp +++ b/libraries/animation/src/Rig.cpp @@ -1115,36 +1115,13 @@ void Rig::updateHands(bool leftHandEnabled, bool rightHandEnabled, bool hipsEnab const glm::vec3 bodyCapsuleEnd = bodyCapsuleCenter + glm::vec3(0, bodyCapsuleHalfHeight, 0); const float HAND_RADIUS = 0.05f; - - const float RELAX_DURATION = 0.6f; - const float CONTROL_DURATION = 0.4f; - const bool TO_CONTROLLED = true; - const bool FROM_CONTROLLED = false; - const bool LEFT_HAND = true; - const bool RIGHT_HAND = false; - const float ELBOW_POLE_VECTOR_BLEND_FACTOR = 0.95f; if (leftHandEnabled) { - if (!_isLeftHandControlled) { - _leftHandControlTimeRemaining = CONTROL_DURATION; - _isLeftHandControlled = true; - } glm::vec3 handPosition = leftHandPose.trans(); glm::quat handRotation = leftHandPose.rot(); - if (_leftHandControlTimeRemaining > 0.0f) { - // Move hand from non-controlled position to controlled position. - _leftHandControlTimeRemaining = std::max(_leftHandControlTimeRemaining - dt, 0.0f); - AnimPose handPose(Vectors::ONE, handRotation, handPosition); - if (transitionHandPose(_leftHandControlTimeRemaining, CONTROL_DURATION, handPose, - LEFT_HAND, TO_CONTROLLED, handPose)) { - handPosition = handPose.trans(); - handRotation = handPose.rot(); - } - } - if (!hipsEnabled) { // prevent the hand IK targets from intersecting the body capsule glm::vec3 displacement; @@ -1157,9 +1134,6 @@ void Rig::updateHands(bool leftHandEnabled, bool rightHandEnabled, bool hipsEnab _animVars.set("leftHandRotation", handRotation); _animVars.set("leftHandType", (int)IKTarget::Type::RotationAndPosition); - _lastLeftHandControlledPose = AnimPose(Vectors::ONE, handRotation, handPosition); - _isLeftHandControlled = true; - // compute pole vector int handJointIndex = _animSkeleton->nameToJointIndex("LeftHand"); int armJointIndex = _animSkeleton->nameToJointIndex("LeftArm"); @@ -1187,47 +1161,17 @@ void Rig::updateHands(bool leftHandEnabled, bool rightHandEnabled, bool hipsEnab _prevLeftHandPoleVectorValid = false; _animVars.set("leftHandPoleVectorEnabled", false); - if (_isLeftHandControlled) { - _leftHandRelaxTimeRemaining = RELAX_DURATION; - _isLeftHandControlled = false; - } + _animVars.unset("leftHandPosition"); + _animVars.unset("leftHandRotation"); + _animVars.set("leftHandType", (int)IKTarget::Type::HipsRelativeRotationAndPosition); - if (_leftHandRelaxTimeRemaining > 0.0f) { - // Move hand from controlled position to non-controlled position. - _leftHandRelaxTimeRemaining = std::max(_leftHandRelaxTimeRemaining - dt, 0.0f); - AnimPose handPose; - if (transitionHandPose(_leftHandRelaxTimeRemaining, RELAX_DURATION, _lastLeftHandControlledPose, - LEFT_HAND, FROM_CONTROLLED, handPose)) { - _animVars.set("leftHandPosition", handPose.trans()); - _animVars.set("leftHandRotation", handPose.rot()); - _animVars.set("leftHandType", (int)IKTarget::Type::RotationAndPosition); - } - } else { - _animVars.unset("leftHandPosition"); - _animVars.unset("leftHandRotation"); - _animVars.set("leftHandType", (int)IKTarget::Type::HipsRelativeRotationAndPosition); - } } if (rightHandEnabled) { - if (!_isRightHandControlled) { - _rightHandControlTimeRemaining = CONTROL_DURATION; - _isRightHandControlled = true; - } glm::vec3 handPosition = rightHandPose.trans(); glm::quat handRotation = rightHandPose.rot(); - if (_rightHandControlTimeRemaining > 0.0f) { - // Move hand from non-controlled position to controlled position. - _rightHandControlTimeRemaining = std::max(_rightHandControlTimeRemaining - dt, 0.0f); - AnimPose handPose(Vectors::ONE, handRotation, handPosition); - if (transitionHandPose(_rightHandControlTimeRemaining, CONTROL_DURATION, handPose, RIGHT_HAND, TO_CONTROLLED, handPose)) { - handPosition = handPose.trans(); - handRotation = handPose.rot(); - } - } - if (!hipsEnabled) { // prevent the hand IK targets from intersecting the body capsule glm::vec3 displacement; @@ -1240,9 +1184,6 @@ void Rig::updateHands(bool leftHandEnabled, bool rightHandEnabled, bool hipsEnab _animVars.set("rightHandRotation", handRotation); _animVars.set("rightHandType", (int)IKTarget::Type::RotationAndPosition); - _lastRightHandControlledPose = AnimPose(Vectors::ONE, handRotation, handPosition); - _isRightHandControlled = true; - // compute pole vector int handJointIndex = _animSkeleton->nameToJointIndex("RightHand"); int armJointIndex = _animSkeleton->nameToJointIndex("RightArm"); @@ -1270,25 +1211,9 @@ void Rig::updateHands(bool leftHandEnabled, bool rightHandEnabled, bool hipsEnab _prevRightHandPoleVectorValid = false; _animVars.set("rightHandPoleVectorEnabled", false); - if (_isRightHandControlled) { - _rightHandRelaxTimeRemaining = RELAX_DURATION; - _isRightHandControlled = false; - } - - if (_rightHandRelaxTimeRemaining > 0.0f) { - // Move hand from controlled position to non-controlled position. - _rightHandRelaxTimeRemaining = std::max(_rightHandRelaxTimeRemaining - dt, 0.0f); - AnimPose handPose; - if (transitionHandPose(_rightHandRelaxTimeRemaining, RELAX_DURATION, _lastRightHandControlledPose, RIGHT_HAND, FROM_CONTROLLED, handPose)) { - _animVars.set("rightHandPosition", handPose.trans()); - _animVars.set("rightHandRotation", handPose.rot()); - _animVars.set("rightHandType", (int)IKTarget::Type::RotationAndPosition); - } - } else { - _animVars.unset("rightHandPosition"); - _animVars.unset("rightHandRotation"); - _animVars.set("rightHandType", (int)IKTarget::Type::HipsRelativeRotationAndPosition); - } + _animVars.unset("rightHandPosition"); + _animVars.unset("rightHandRotation"); + _animVars.set("rightHandType", (int)IKTarget::Type::HipsRelativeRotationAndPosition); } } @@ -1737,39 +1662,38 @@ void Rig::computeAvatarBoundingCapsule( ikNode.setTargetVars("RightFoot", "rightFootPosition", "rightFootRotation", "rightFootType", "rightFootWeight", 1.0f, {}, QString(), QString(), QString()); - - AnimPose geometryToRig = _modelOffset * _geometryOffset; - - AnimPose hips(glm::vec3(1), glm::quat(), glm::vec3()); + glm::vec3 hipsPosition(0.0f); int hipsIndex = indexOfJoint("Hips"); if (hipsIndex >= 0) { - hips = geometryToRig * _animSkeleton->getAbsoluteBindPose(hipsIndex); + hipsPosition = transformPoint(_geometryToRigTransform, _animSkeleton->getAbsoluteDefaultPose(hipsIndex).trans()); } AnimVariantMap animVars; + animVars.setRigToGeometryTransform(_rigToGeometryTransform); glm::quat handRotation = glm::angleAxis(PI, Vectors::UNIT_X); - animVars.set("leftHandPosition", hips.trans()); + animVars.set("leftHandPosition", hipsPosition); animVars.set("leftHandRotation", handRotation); animVars.set("leftHandType", (int)IKTarget::Type::RotationAndPosition); - animVars.set("rightHandPosition", hips.trans()); + animVars.set("rightHandPosition", hipsPosition); animVars.set("rightHandRotation", handRotation); animVars.set("rightHandType", (int)IKTarget::Type::RotationAndPosition); int rightFootIndex = indexOfJoint("RightFoot"); int leftFootIndex = indexOfJoint("LeftFoot"); if (rightFootIndex != -1 && leftFootIndex != -1) { - glm::vec3 foot = Vectors::ZERO; + glm::vec3 geomFootPosition = glm::vec3(0.0f, _animSkeleton->getAbsoluteDefaultPose(rightFootIndex).trans().y, 0.0f); + glm::vec3 footPosition = transformPoint(_geometryToRigTransform, geomFootPosition); glm::quat footRotation = glm::angleAxis(0.5f * PI, Vectors::UNIT_X); - animVars.set("leftFootPosition", foot); + animVars.set("leftFootPosition", footPosition); animVars.set("leftFootRotation", footRotation); animVars.set("leftFootType", (int)IKTarget::Type::RotationAndPosition); - animVars.set("rightFootPosition", foot); + animVars.set("rightFootPosition", footPosition); animVars.set("rightFootRotation", footRotation); animVars.set("rightFootType", (int)IKTarget::Type::RotationAndPosition); } // call overlay twice: once to verify AnimPoseVec joints and again to do the IK AnimNode::Triggers triggersOut; - AnimContext context(false, false, false, glm::mat4(), glm::mat4()); + AnimContext context(false, false, false, _geometryToRigTransform, _rigToGeometryTransform); float dt = 1.0f; // the value of this does not matter ikNode.overlay(animVars, context, dt, triggersOut, _animSkeleton->getRelativeBindPoses()); AnimPoseVec finalPoses = ikNode.overlay(animVars, context, dt, triggersOut, _animSkeleton->getRelativeBindPoses()); @@ -1802,34 +1726,13 @@ void Rig::computeAvatarBoundingCapsule( // compute bounding shape parameters // NOTE: we assume that the longest side of totalExtents is the yAxis... - glm::vec3 diagonal = (geometryToRig * totalExtents.maximum) - (geometryToRig * totalExtents.minimum); + glm::vec3 diagonal = (transformPoint(_geometryToRigTransform, totalExtents.maximum) - + transformPoint(_geometryToRigTransform, totalExtents.minimum)); // ... and assume the radiusOut is half the RMS of the X and Z sides: radiusOut = 0.5f * sqrtf(0.5f * (diagonal.x * diagonal.x + diagonal.z * diagonal.z)); heightOut = diagonal.y - 2.0f * radiusOut; glm::vec3 rootPosition = finalPoses[geometry.rootJointIndex].trans(); - glm::vec3 rigCenter = (geometryToRig * (0.5f * (totalExtents.maximum + totalExtents.minimum))); - localOffsetOut = rigCenter - (geometryToRig * rootPosition); -} - -bool Rig::transitionHandPose(float deltaTime, float totalDuration, AnimPose& controlledHandPose, bool isLeftHand, - bool isToControlled, AnimPose& returnHandPose) { - auto ikNode = getAnimInverseKinematicsNode(); - if (ikNode) { - float alpha = 1.0f - deltaTime / totalDuration; - const AnimPose geometryToRigTransform(_geometryToRigTransform); - AnimPose uncontrolledHandPose; - if (isLeftHand) { - uncontrolledHandPose = geometryToRigTransform * ikNode->getUncontrolledLeftHandPose(); - } else { - uncontrolledHandPose = geometryToRigTransform * ikNode->getUncontrolledRightHandPose(); - } - if (isToControlled) { - ::blend(1, &uncontrolledHandPose, &controlledHandPose, alpha, &returnHandPose); - } else { - ::blend(1, &controlledHandPose, &uncontrolledHandPose, alpha, &returnHandPose); - } - return true; - } - return false; + glm::vec3 rigCenter = transformPoint(_geometryToRigTransform, (0.5f * (totalExtents.maximum + totalExtents.minimum))); + localOffsetOut = rigCenter - transformPoint(_geometryToRigTransform, rootPosition); } diff --git a/libraries/animation/src/Rig.h b/libraries/animation/src/Rig.h index c17a7b9c8f..5293fa1fe7 100644 --- a/libraries/animation/src/Rig.h +++ b/libraries/animation/src/Rig.h @@ -340,18 +340,6 @@ protected: int _nextStateHandlerId { 0 }; QMutex _stateMutex; - bool transitionHandPose(float deltaTime, float totalDuration, AnimPose& controlledHandPose, bool isLeftHand, - bool isToControlled, AnimPose& returnHandPose); - - bool _isLeftHandControlled { false }; - bool _isRightHandControlled { false }; - float _leftHandControlTimeRemaining { 0.0f }; - float _rightHandControlTimeRemaining { 0.0f }; - float _leftHandRelaxTimeRemaining { 0.0f }; - float _rightHandRelaxTimeRemaining { 0.0f }; - AnimPose _lastLeftHandControlledPose; - AnimPose _lastRightHandControlledPose; - glm::vec3 _prevRightFootPoleVector { Vectors::UNIT_Z }; bool _prevRightFootPoleVectorValid { false }; diff --git a/libraries/audio-client/src/AudioClient.cpp b/libraries/audio-client/src/AudioClient.cpp index 16cecf6366..19b2c07d50 100644 --- a/libraries/audio-client/src/AudioClient.cpp +++ b/libraries/audio-client/src/AudioClient.cpp @@ -92,6 +92,7 @@ void AudioClient::checkDevices() { auto inputDevices = getAvailableDevices(QAudio::AudioInput); auto outputDevices = getAvailableDevices(QAudio::AudioOutput); + Lock lock(_deviceMutex); if (inputDevices != _inputDevices) { _inputDevices.swap(inputDevices); emit devicesChanged(QAudio::AudioInput, _inputDevices); @@ -210,9 +211,9 @@ AudioClient::AudioClient() : connect(&_receivedAudioStream, &MixedProcessedAudioStream::processSamples, this, &AudioClient::processReceivedSamples, Qt::DirectConnection); - connect(this, &AudioClient::changeDevice, this, [=](const QAudioDeviceInfo& outputDeviceInfo) { + connect(this, &AudioClient::changeDevice, this, [=](const QAudioDeviceInfo& outputDeviceInfo) { qCDebug(audioclient) << "got AudioClient::changeDevice signal, about to call switchOutputToAudioDevice() outputDeviceInfo: [" << outputDeviceInfo.deviceName() << "]"; - switchOutputToAudioDevice(outputDeviceInfo); + switchOutputToAudioDevice(outputDeviceInfo); }); connect(&_receivedAudioStream, &InboundAudioStream::mismatchedAudioCodec, this, &AudioClient::handleMismatchAudioFormat); @@ -266,10 +267,10 @@ void AudioClient::cleanupBeforeQuit() { // so this must be explicitly, synchronously stopped static ConditionalGuard guard; if (QThread::currentThread() != thread()) { - // This will likely be called from the main thread, but we don't want to do blocking queued calls - // from the main thread, so we use a normal auto-connection invoke, and then use a conditional to wait + // This will likely be called from the main thread, but we don't want to do blocking queued calls + // from the main thread, so we use a normal auto-connection invoke, and then use a conditional to wait // for completion - // The effect is the same, yes, but we actually want to avoid the use of Qt::BlockingQueuedConnection + // The effect is the same, yes, but we actually want to avoid the use of Qt::BlockingQueuedConnection // in the code QMetaObject::invokeMethod(this, "cleanupBeforeQuit"); guard.wait(); @@ -638,7 +639,7 @@ void AudioClient::handleAudioEnvironmentDataPacket(QSharedPointerreadPrimitive(&bitset); bool hasReverb = oneAtBit(bitset, HAS_REVERB_BIT); - + if (hasReverb) { float reverbTime, wetLevel; message->readPrimitive(&reverbTime); @@ -736,7 +737,7 @@ void AudioClient::Gate::flush() { void AudioClient::handleNoisyMutePacket(QSharedPointer message) { if (!_muted) { toggleMute(); - + // have the audio scripting interface emit a signal to say we were muted by the mixer emit mutedByMixer(); } @@ -745,7 +746,7 @@ void AudioClient::handleNoisyMutePacket(QSharedPointer message) void AudioClient::handleMuteEnvironmentPacket(QSharedPointer message) { glm::vec3 position; float radius; - + message->readPrimitive(&position); message->readPrimitive(&radius); @@ -778,7 +779,7 @@ void AudioClient::handleSelectedAudioFormat(QSharedPointer mess } void AudioClient::selectAudioFormat(const QString& selectedCodecName) { - + _selectedCodecName = selectedCodecName; qCDebug(audioclient) << "Selected Codec:" << _selectedCodecName; @@ -795,7 +796,7 @@ void AudioClient::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); qCDebug(audioclient) << "Selected Codec Plugin:" << _codec.get(); break; @@ -803,7 +804,7 @@ void AudioClient::selectAudioFormat(const QString& selectedCodecName) { } } - + bool AudioClient::switchAudioDevice(QAudio::Mode mode, const QAudioDeviceInfo& deviceInfo) { auto device = deviceInfo; @@ -1211,11 +1212,11 @@ bool AudioClient::mixLocalAudioInjectors(float* mixBuffer) { // lock the injectors Lock lock(_injectorsMutex); - QVector injectorsToRemove; + QVector injectorsToRemove; memset(mixBuffer, 0, AudioConstants::NETWORK_FRAME_SAMPLES_STEREO * sizeof(float)); - for (AudioInjector* injector : _activeLocalAudioInjectors) { + for (const AudioInjectorPointer& injector : _activeLocalAudioInjectors) { // the lock guarantees that injectorBuffer, if found, is invariant AudioInjectorLocalBuffer* injectorBuffer = injector->getLocalBuffer(); if (injectorBuffer) { @@ -1228,7 +1229,7 @@ bool AudioClient::mixLocalAudioInjectors(float* mixBuffer) { // get one frame from the injector memset(_localScratchBuffer, 0, bytesToRead); if (0 < injectorBuffer->readData((char*)_localScratchBuffer, bytesToRead)) { - + if (injector->isAmbisonic()) { // no distance attenuation @@ -1257,36 +1258,36 @@ bool AudioClient::mixLocalAudioInjectors(float* mixBuffer) { for (int i = 0; i < AudioConstants::NETWORK_FRAME_SAMPLES_STEREO; i++) { mixBuffer[i] += convertToFloat(_localScratchBuffer[i]) * gain; } - + } else { // calculate distance, gain and azimuth for hrtf glm::vec3 relativePosition = injector->getPosition() - _positionGetter(); float distance = glm::max(glm::length(relativePosition), EPSILON); - float gain = gainForSource(distance, injector->getVolume()); + float gain = gainForSource(distance, injector->getVolume()); float azimuth = azimuthForSource(relativePosition); - + // mono gets spatialized into mixBuffer - injector->getLocalHRTF().render(_localScratchBuffer, mixBuffer, HRTF_DATASET_INDEX, + injector->getLocalHRTF().render(_localScratchBuffer, mixBuffer, HRTF_DATASET_INDEX, azimuth, distance, gain, AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL); } - + } else { - + qCDebug(audioclient) << "injector has no more data, marking finished for removal"; injector->finishLocalInjection(); injectorsToRemove.append(injector); } } else { - + qCDebug(audioclient) << "injector has no local buffer, marking as finished for removal"; injector->finishLocalInjection(); injectorsToRemove.append(injector); } } - - for (AudioInjector* injector : injectorsToRemove) { + + for (const AudioInjectorPointer& injector : injectorsToRemove) { qCDebug(audioclient) << "removing injector"; _activeLocalAudioInjectors.removeOne(injector); } @@ -1377,7 +1378,7 @@ void AudioClient::setIsStereoInput(bool isStereoInput) { } } -bool AudioClient::outputLocalInjector(AudioInjector* injector) { +bool AudioClient::outputLocalInjector(const AudioInjectorPointer& injector) { AudioInjectorLocalBuffer* injectorBuffer = injector->getLocalBuffer(); if (injectorBuffer) { // local injectors are on the AudioInjectorsThread, so we must guard access @@ -1719,9 +1720,9 @@ int AudioClient::calculateNumberOfFrameSamples(int numBytes) const { float AudioClient::azimuthForSource(const glm::vec3& relativePosition) { glm::quat inverseOrientation = glm::inverse(_orientationGetter()); - + glm::vec3 rotatedSourcePosition = inverseOrientation * relativePosition; - + // project the rotated source position vector onto the XZ plane rotatedSourcePosition.y = 0.0f; @@ -1729,15 +1730,15 @@ float AudioClient::azimuthForSource(const glm::vec3& relativePosition) { float rotatedSourcePositionLength2 = glm::length2(rotatedSourcePosition); if (rotatedSourcePositionLength2 > SOURCE_DISTANCE_THRESHOLD) { - + // produce an oriented angle about the y-axis glm::vec3 direction = rotatedSourcePosition * (1.0f / fastSqrtf(rotatedSourcePositionLength2)); float angle = fastAcosf(glm::clamp(-direction.z, -1.0f, 1.0f)); // UNIT_NEG_Z is "forward" return (direction.x < 0.0f) ? -angle : angle; - } else { + } else { // no azimuth if they are in same spot - return 0.0f; + return 0.0f; } } @@ -1877,9 +1878,9 @@ void AudioClient::startThread() { moveToNewNamedThread(this, "Audio Thread", [this] { start(); }); } -void AudioClient::setInputVolume(float volume) { - if (_audioInput && volume != (float)_audioInput->volume()) { - _audioInput->setVolume(volume); +void AudioClient::setInputVolume(float volume) { + if (_audioInput && volume != (float)_audioInput->volume()) { + _audioInput->setVolume(volume); emit inputVolumeChanged(_audioInput->volume()); } } diff --git a/libraries/audio-client/src/AudioClient.h b/libraries/audio-client/src/AudioClient.h index 882e93beee..82643649a4 100644 --- a/libraries/audio-client/src/AudioClient.h +++ b/libraries/audio-client/src/AudioClient.h @@ -143,7 +143,7 @@ public: Q_INVOKABLE void setAvatarBoundingBoxParameters(glm::vec3 corner, glm::vec3 scale); - bool outputLocalInjector(AudioInjector* injector) override; + bool outputLocalInjector(const AudioInjectorPointer& injector) override; QAudioDeviceInfo getActiveAudioDevice(QAudio::Mode mode) const; QList getAudioDevices(QAudio::Mode mode) const; @@ -372,7 +372,7 @@ private: AudioIOStats _stats; AudioGate* _audioGate { nullptr }; - bool _audioGateOpen { false }; + bool _audioGateOpen { true }; AudioPositionGetter _positionGetter; AudioOrientationGetter _orientationGetter; @@ -388,7 +388,7 @@ private: bool _hasReceivedFirstPacket { false }; - QVector _activeLocalAudioInjectors; + QVector _activeLocalAudioInjectors; bool _isPlayingBackRecording { false }; diff --git a/libraries/audio/src/AbstractAudioInterface.h b/libraries/audio/src/AbstractAudioInterface.h index 2e14b9956b..8b48b55206 100644 --- a/libraries/audio/src/AbstractAudioInterface.h +++ b/libraries/audio/src/AbstractAudioInterface.h @@ -18,6 +18,7 @@ #include #include "AudioInjectorOptions.h" +#include "AudioInjector.h" class AudioInjector; class AudioInjectorLocalBuffer; @@ -35,7 +36,7 @@ public: // threadsafe // moves injector->getLocalBuffer() to another thread (so removes its parent) // take care to delete it when ~AudioInjector, as parenting Qt semantics will not work - virtual bool outputLocalInjector(AudioInjector* injector) = 0; + virtual bool outputLocalInjector(const AudioInjectorPointer& injector) = 0; public slots: virtual bool shouldLoopbackInjectors() { return false; } diff --git a/libraries/audio/src/AudioInjector.cpp b/libraries/audio/src/AudioInjector.cpp index 47e6c98144..9f32372a8e 100644 --- a/libraries/audio/src/AudioInjector.cpp +++ b/libraries/audio/src/AudioInjector.cpp @@ -27,8 +27,6 @@ #include "AudioSRC.h" #include "AudioHelpers.h" -int audioInjectorPtrMetaTypeId = qRegisterMetaType(); - AbstractAudioInterface* AudioInjector::_localAudioInterface{ nullptr }; AudioInjectorState operator& (AudioInjectorState lhs, AudioInjectorState rhs) { @@ -92,11 +90,6 @@ void AudioInjector::finish() { emit finished(); deleteLocalBuffer(); - - if (stateHas(AudioInjectorState::PendingDelete)) { - // we've been asked to delete after finishing, trigger a deleteLater here - deleteLater(); - } } void AudioInjector::restart() { @@ -132,7 +125,7 @@ void AudioInjector::restart() { } } -bool AudioInjector::inject(bool(AudioInjectorManager::*injection)(AudioInjector*)) { +bool AudioInjector::inject(bool(AudioInjectorManager::*injection)(const AudioInjectorPointer&)) { _state = AudioInjectorState::NotFinished; int byteOffset = 0; @@ -150,7 +143,7 @@ bool AudioInjector::inject(bool(AudioInjectorManager::*injection)(AudioInjector* bool success = true; if (!_options.localOnly) { auto injectorManager = DependencyManager::get(); - if (!(*injectorManager.*injection)(this)) { + if (!(*injectorManager.*injection)(sharedFromThis())) { success = false; finishNetworkInjection(); } @@ -173,7 +166,7 @@ bool AudioInjector::injectLocally() { // call this function on the AudioClient's thread // this will move the local buffer's thread to the LocalInjectorThread - success = _localAudioInterface->outputLocalInjector(this); + success = _localAudioInterface->outputLocalInjector(sharedFromThis()); if (!success) { qCDebug(audio) << "AudioInjector::injectLocally could not output locally via _localAudioInterface"; @@ -418,20 +411,16 @@ void AudioInjector::triggerDeleteAfterFinish() { } if (stateHas(AudioInjectorState::Finished)) { - stopAndDeleteLater(); + stop(); } else { _state |= AudioInjectorState::PendingDelete; } } -void AudioInjector::stopAndDeleteLater() { - stop(); - QMetaObject::invokeMethod(this, "deleteLater", Qt::QueuedConnection); -} - -AudioInjector* AudioInjector::playSound(SharedSoundPointer sound, const float volume, const float stretchFactor, const glm::vec3 position) { +AudioInjectorPointer AudioInjector::playSound(SharedSoundPointer sound, const float volume, + const float stretchFactor, const glm::vec3 position) { if (!sound || !sound->isReady()) { - return nullptr; + return AudioInjectorPointer(); } AudioInjectorOptions options; @@ -462,8 +451,8 @@ AudioInjector* AudioInjector::playSound(SharedSoundPointer sound, const float vo return playSoundAndDelete(resampled, options); } -AudioInjector* AudioInjector::playSoundAndDelete(const QByteArray& buffer, const AudioInjectorOptions options) { - AudioInjector* sound = playSound(buffer, options); +AudioInjectorPointer AudioInjector::playSoundAndDelete(const QByteArray& buffer, const AudioInjectorOptions options) { + AudioInjectorPointer sound = playSound(buffer, options); if (sound) { sound->_state |= AudioInjectorState::PendingDelete; @@ -473,8 +462,9 @@ AudioInjector* AudioInjector::playSoundAndDelete(const QByteArray& buffer, const } -AudioInjector* AudioInjector::playSound(const QByteArray& buffer, const AudioInjectorOptions options) { - AudioInjector* injector = new AudioInjector(buffer, options); +AudioInjectorPointer AudioInjector::playSound(const QByteArray& buffer, const AudioInjectorOptions options) { + AudioInjectorPointer injector = AudioInjectorPointer::create(buffer, options); + if (!injector->inject(&AudioInjectorManager::threadInjector)) { qWarning() << "AudioInjector::playSound failed to thread injector"; } diff --git a/libraries/audio/src/AudioInjector.h b/libraries/audio/src/AudioInjector.h index a901c2520f..fc197f7ba0 100644 --- a/libraries/audio/src/AudioInjector.h +++ b/libraries/audio/src/AudioInjector.h @@ -32,6 +32,8 @@ class AbstractAudioInterface; class AudioInjectorManager; +class AudioInjector; +using AudioInjectorPointer = QSharedPointer; enum class AudioInjectorState : uint8_t { @@ -46,19 +48,19 @@ AudioInjectorState operator& (AudioInjectorState lhs, AudioInjectorState rhs); AudioInjectorState& operator|= (AudioInjectorState& lhs, AudioInjectorState rhs); // In order to make scripting cleaner for the AudioInjector, the script now holds on to the AudioInjector object -// until it dies. -class AudioInjector : public QObject { +// until it dies. +class AudioInjector : public QObject, public QEnableSharedFromThis { Q_OBJECT public: AudioInjector(const Sound& sound, const AudioInjectorOptions& injectorOptions); AudioInjector(const QByteArray& audioData, const AudioInjectorOptions& injectorOptions); ~AudioInjector(); - + bool isFinished() const { return (stateHas(AudioInjectorState::Finished)); } - + int getCurrentSendOffset() const { return _currentSendOffset; } void setCurrentSendOffset(int currentSendOffset) { _currentSendOffset = currentSendOffset; } - + AudioInjectorLocalBuffer* getLocalBuffer() const { return _localBuffer; } AudioHRTF& getLocalHRTF() { return _localHRTF; } AudioFOA& getLocalFOA() { return _localFOA; } @@ -72,36 +74,36 @@ public: bool stateHas(AudioInjectorState state) const ; static void setLocalAudioInterface(AbstractAudioInterface* audioInterface) { _localAudioInterface = audioInterface; } - static AudioInjector* playSoundAndDelete(const QByteArray& buffer, const AudioInjectorOptions options); - static AudioInjector* playSound(const QByteArray& buffer, const AudioInjectorOptions options); - static AudioInjector* playSound(SharedSoundPointer sound, const float volume, const float stretchFactor, const glm::vec3 position); + static AudioInjectorPointer playSoundAndDelete(const QByteArray& buffer, const AudioInjectorOptions options); + static AudioInjectorPointer playSound(const QByteArray& buffer, const AudioInjectorOptions options); + static AudioInjectorPointer playSound(SharedSoundPointer sound, const float volume, + const float stretchFactor, const glm::vec3 position); public slots: void restart(); - + void stop(); void triggerDeleteAfterFinish(); - void stopAndDeleteLater(); - + const AudioInjectorOptions& getOptions() const { return _options; } void setOptions(const AudioInjectorOptions& options); - + float getLoudness() const { return _loudness; } bool isPlaying() const { return !stateHas(AudioInjectorState::Finished); } void finish(); void finishLocalInjection(); void finishNetworkInjection(); - + signals: void finished(); void restarting(); - + private: int64_t injectNextFrame(); - bool inject(bool(AudioInjectorManager::*injection)(AudioInjector*)); + bool inject(bool(AudioInjectorManager::*injection)(const AudioInjectorPointer&)); bool injectLocally(); void deleteLocalBuffer(); - + static AbstractAudioInterface* _localAudioInterface; QByteArray _audioData; @@ -112,17 +114,15 @@ private: int _currentSendOffset { 0 }; std::unique_ptr _currentPacket { nullptr }; AudioInjectorLocalBuffer* _localBuffer { nullptr }; - + int64_t _nextFrame { 0 }; std::unique_ptr _frameTimer { nullptr }; quint16 _outgoingSequenceNumber { 0 }; - + // when the injector is local, we need this AudioHRTF _localHRTF; AudioFOA _localFOA; friend class AudioInjectorManager; }; -Q_DECLARE_METATYPE(AudioInjector*) - #endif // hifi_AudioInjector_h diff --git a/libraries/audio/src/AudioInjectorManager.cpp b/libraries/audio/src/AudioInjectorManager.cpp index c66e209ea9..f30d3093ec 100644 --- a/libraries/audio/src/AudioInjectorManager.cpp +++ b/libraries/audio/src/AudioInjectorManager.cpp @@ -21,26 +21,26 @@ AudioInjectorManager::~AudioInjectorManager() { _shouldStop = true; - + Lock lock(_injectorsMutex); - + // make sure any still living injectors are stopped and deleted while (!_injectors.empty()) { // grab the injector at the front auto& timePointerPair = _injectors.top(); - + // ask it to stop and be deleted - timePointerPair.second->stopAndDeleteLater(); - + timePointerPair.second->stop(); + _injectors.pop(); } - + // get rid of the lock now that we've stopped all living injectors lock.unlock(); - + // in case the thread is waiting for injectors wake it up now _injectorReady.notify_one(); - + // quit and wait on the manager thread, if we ever created it if (_thread) { _thread->quit(); @@ -51,10 +51,10 @@ AudioInjectorManager::~AudioInjectorManager() { void AudioInjectorManager::createThread() { _thread = new QThread; _thread->setObjectName("Audio Injector Thread"); - + // when the thread is started, have it call our run to handle injection of audio connect(_thread, &QThread::started, this, &AudioInjectorManager::run, Qt::DirectConnection); - + // start the thread _thread->start(); } @@ -63,20 +63,20 @@ void AudioInjectorManager::run() { while (!_shouldStop) { // wait until the next injector is ready, or until we get a new injector given to us Lock lock(_injectorsMutex); - + if (_injectors.size() > 0) { // when does the next injector need to send a frame? // do we get to wait or should we just go for it now? - + auto timeInjectorPair = _injectors.top(); - + auto nextTimestamp = timeInjectorPair.first; int64_t difference = int64_t(nextTimestamp - usecTimestampNow()); - + if (difference > 0) { _injectorReady.wait_for(lock, std::chrono::microseconds(difference)); } - + if (_injectors.size() > 0) { // loop through the injectors in the map and send whatever frames need to go out auto front = _injectors.top(); @@ -90,7 +90,7 @@ void AudioInjectorManager::run() { // either way we're popping this injector off - get a copy first auto injector = front.second; _injectors.pop(); - + if (!injector.isNull()) { // this is an injector that's ready to go, have it send a frame now auto nextCallDelta = injector->injectNextFrame(); @@ -100,7 +100,7 @@ void AudioInjectorManager::run() { heldInjectors.emplace(heldInjectors.end(), usecTimestampNow() + nextCallDelta, injector); } } - + if (_injectors.size() > 0) { front = _injectors.top(); } else { @@ -120,10 +120,10 @@ void AudioInjectorManager::run() { // we have no current injectors, wait until we get at least one before we do anything _injectorReady.wait(lock); } - + // unlock the lock in case something in process events needs to modify the queue lock.unlock(); - + QCoreApplication::processEvents(); } } @@ -139,36 +139,36 @@ bool AudioInjectorManager::wouldExceedLimits() { // Should be called inside of a return false; } -bool AudioInjectorManager::threadInjector(AudioInjector* injector) { +bool AudioInjectorManager::threadInjector(const AudioInjectorPointer& injector) { if (_shouldStop) { qCDebug(audio) << "AudioInjectorManager::threadInjector asked to thread injector but is shutting down."; return false; } - + // guard the injectors vector with a mutex Lock lock(_injectorsMutex); - + if (wouldExceedLimits()) { return false; } else { if (!_thread) { createThread(); } - + // move the injector to the QThread injector->moveToThread(_thread); - + // add the injector to the queue with a send timestamp of now - _injectors.emplace(usecTimestampNow(), InjectorQPointer { injector }); - + _injectors.emplace(usecTimestampNow(), injector); + // notify our wait condition so we can inject two frames for this injector immediately _injectorReady.notify_one(); - + return true; } } -bool AudioInjectorManager::restartFinishedInjector(AudioInjector* injector) { +bool AudioInjectorManager::restartFinishedInjector(const AudioInjectorPointer& injector) { if (_shouldStop) { qCDebug(audio) << "AudioInjectorManager::threadInjector asked to thread injector but is shutting down."; return false; @@ -181,8 +181,8 @@ bool AudioInjectorManager::restartFinishedInjector(AudioInjector* injector) { return false; } else { // add the injector to the queue with a send timestamp of now - _injectors.emplace(usecTimestampNow(), InjectorQPointer { injector }); - + _injectors.emplace(usecTimestampNow(), injector); + // notify our wait condition so we can inject two frames for this injector immediately _injectorReady.notify_one(); } diff --git a/libraries/audio/src/AudioInjectorManager.h b/libraries/audio/src/AudioInjectorManager.h index de5537856e..9aca3014e3 100644 --- a/libraries/audio/src/AudioInjectorManager.h +++ b/libraries/audio/src/AudioInjectorManager.h @@ -23,7 +23,7 @@ #include -class AudioInjector; +#include "AudioInjector.h" class AudioInjectorManager : public QObject, public Dependency { Q_OBJECT @@ -33,39 +33,38 @@ public: private slots: void run(); private: - - using InjectorQPointer = QPointer; - using TimeInjectorPointerPair = std::pair; - + + using TimeInjectorPointerPair = std::pair; + struct greaterTime { bool operator() (const TimeInjectorPointerPair& x, const TimeInjectorPointerPair& y) const { return x.first > y.first; } }; - + using InjectorQueue = std::priority_queue, greaterTime>; using Mutex = std::mutex; using Lock = std::unique_lock; - - bool threadInjector(AudioInjector* injector); - bool restartFinishedInjector(AudioInjector* injector); + + bool threadInjector(const AudioInjectorPointer& injector); + bool restartFinishedInjector(const AudioInjectorPointer& injector); void notifyInjectorReadyCondition() { _injectorReady.notify_one(); } bool wouldExceedLimits(); - + AudioInjectorManager() {}; AudioInjectorManager(const AudioInjectorManager&) = delete; AudioInjectorManager& operator=(const AudioInjectorManager&) = delete; - + void createThread(); - + QThread* _thread { nullptr }; bool _shouldStop { false }; InjectorQueue _injectors; Mutex _injectorsMutex; std::condition_variable _injectorReady; - + friend class AudioInjector; }; diff --git a/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp b/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp index 2a223a1eaf..d775fe05f0 100644 --- a/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp +++ b/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp @@ -151,11 +151,6 @@ glm::vec3 Avatar::getNeckPosition() const { return _skeletonModel->getNeckPosition(neckPosition) ? neckPosition : getPosition(); } - -glm::quat Avatar::getWorldAlignedOrientation () const { - return computeRotationFromBodyToWorldUp() * getOrientation(); -} - AABox Avatar::getBounds() const { if (!_skeletonModel->isRenderable() || _skeletonModel->needsFixupInScene()) { // approximately 2m tall, scaled to user request. @@ -436,6 +431,11 @@ void Avatar::slamPosition(const glm::vec3& newPosition) { _lastVelocity = glm::vec3(0.0f); } +void Avatar::updateAttitude(const glm::quat& orientation) { + _skeletonModel->updateAttitude(orientation); + _worldUpDirection = orientation * Vectors::UNIT_Y; +} + void Avatar::applyPositionDelta(const glm::vec3& delta) { setPosition(getPosition() + delta); _positionDeltaAccumulator += delta; @@ -628,22 +628,6 @@ void Avatar::render(RenderArgs* renderArgs) { } } -glm::quat Avatar::computeRotationFromBodyToWorldUp(float proportion) const { - glm::quat orientation = getOrientation(); - glm::vec3 currentUp = orientation * IDENTITY_UP; - float angle = acosf(glm::clamp(glm::dot(currentUp, _worldUpDirection), -1.0f, 1.0f)); - if (angle < EPSILON) { - return glm::quat(); - } - glm::vec3 axis; - if (angle > 179.99f * RADIANS_PER_DEGREE) { // 180 degree rotation; must use another axis - axis = orientation * IDENTITY_RIGHT; - } else { - axis = glm::normalize(glm::cross(currentUp, _worldUpDirection)); - } - return glm::angleAxis(angle * proportion, axis); -} - void Avatar::fixupModelsInScene(const render::ScenePointer& scene) { _attachmentsToDelete.clear(); @@ -915,17 +899,34 @@ glm::vec3 Avatar::getDefaultJointTranslation(int index) const { } glm::quat Avatar::getAbsoluteDefaultJointRotationInObjectFrame(int index) const { - glm::quat rotation; - glm::quat rot = _skeletonModel->getRig().getAnimSkeleton()->getAbsoluteDefaultPose(index).rot(); - return Quaternions::Y_180 * rot; + // To make this thread safe, we hold onto the model by smart ptr, which prevents it from being deleted while we are accessing it. + auto model = getSkeletonModel(); + if (model) { + auto skeleton = model->getRig().getAnimSkeleton(); + if (skeleton && index >= 0 && index < skeleton->getNumJoints()) { + // The rotation part of the geometry-to-rig transform is always identity so we can skip it. + // Y_180 is to convert from rig-frame into avatar-frame + return Quaternions::Y_180 * skeleton->getAbsoluteDefaultPose(index).rot(); + } + } + return Quaternions::Y_180; } glm::vec3 Avatar::getAbsoluteDefaultJointTranslationInObjectFrame(int index) const { - glm::vec3 translation; - const Rig& rig = _skeletonModel->getRig(); - glm::vec3 trans = rig.getAnimSkeleton()->getAbsoluteDefaultPose(index).trans(); - glm::mat4 y180Mat = createMatFromQuatAndPos(Quaternions::Y_180, glm::vec3()); - return transformPoint(y180Mat * rig.getGeometryToRigTransform(), trans); + // To make this thread safe, we hold onto the model by smart ptr, which prevents it from being deleted while we are accessing it. + auto model = getSkeletonModel(); + if (model) { + const Rig& rig = model->getRig(); + auto skeleton = rig.getAnimSkeleton(); + if (skeleton && index >= 0 && index < skeleton->getNumJoints()) { + // trans is in geometry frame. + glm::vec3 trans = skeleton->getAbsoluteDefaultPose(index).trans(); + // Y_180 is to convert from rig-frame into avatar-frame + glm::mat4 geomToAvatarMat = Matrices::Y_180 * rig.getGeometryToRigTransform(); + return transformPoint(geomToAvatarMat, trans); + } + } + return Vectors::ZERO; } glm::quat Avatar::getAbsoluteJointRotationInObjectFrame(int index) const { @@ -1050,7 +1051,34 @@ int Avatar::getJointIndex(const QString& name) const { QStringList Avatar::getJointNames() const { QStringList result; withValidJointIndicesCache([&]() { - result = _modelJointIndicesCache.keys(); + // find out how large the vector needs to be + int maxJointIndex = -1; + QHashIterator k(_modelJointIndicesCache); + while (k.hasNext()) { + k.next(); + int index = k.value(); + if (index > maxJointIndex) { + maxJointIndex = index; + } + } + // iterate through the hash and put joint names + // into the vector at their indices + QVector resultVector(maxJointIndex+1); + QHashIterator i(_modelJointIndicesCache); + while (i.hasNext()) { + i.next(); + int index = i.value(); + resultVector[index] = i.key(); + } + // convert to QList and drop out blanks + result = resultVector.toList(); + QMutableListIterator j(result); + while (j.hasNext()) { + QString jointName = j.next(); + if (jointName.isEmpty()) { + j.remove(); + } + } }); return result; } @@ -1374,14 +1402,14 @@ glm::quat Avatar::getUncachedRightPalmRotation() const { return rightPalmRotation; } -void Avatar::setPosition(const glm::vec3& position) { - AvatarData::setPosition(position); - updateAttitude(); +void Avatar::setPositionViaScript(const glm::vec3& position) { + setPosition(position); + updateAttitude(getOrientation()); } -void Avatar::setOrientation(const glm::quat& orientation) { - AvatarData::setOrientation(orientation); - updateAttitude(); +void Avatar::setOrientationViaScript(const glm::quat& orientation) { + setOrientation(orientation); + updateAttitude(orientation); } void Avatar::updatePalms() { @@ -1453,8 +1481,7 @@ void Avatar::addToScene(AvatarSharedPointer myHandle, const render::ScenePointer if (scene) { auto nodelist = DependencyManager::get(); if (showAvatars - && !nodelist->isIgnoringNode(getSessionUUID()) - && !nodelist->isRadiusIgnoringNode(getSessionUUID())) { + && !nodelist->isIgnoringNode(getSessionUUID())) { render::Transaction transaction; addToScene(myHandle, scene, transaction); scene->enqueueTransaction(transaction); diff --git a/libraries/avatars-renderer/src/avatars-renderer/Avatar.h b/libraries/avatars-renderer/src/avatars-renderer/Avatar.h index 89db519abc..2c75012209 100644 --- a/libraries/avatars-renderer/src/avatars-renderer/Avatar.h +++ b/libraries/avatars-renderer/src/avatars-renderer/Avatar.h @@ -112,8 +112,6 @@ public: const Head* getHead() const { return static_cast(_headData); } Head* getHead() { return static_cast(_headData); } - glm::quat getWorldAlignedOrientation() const; - AABox getBounds() const; /// Returns the distance to use as a LOD parameter. @@ -184,7 +182,7 @@ public: void scaleVectorRelativeToPosition(glm::vec3 &positionToScale) const; void slamPosition(const glm::vec3& position); - virtual void updateAttitude() override { _skeletonModel->updateAttitude(); } + virtual void updateAttitude(const glm::quat& orientation) override; // Call this when updating Avatar position with a delta. This will allow us to // _accurately_ measure position changes and compute the resulting velocity @@ -197,10 +195,8 @@ public: void getCapsule(glm::vec3& start, glm::vec3& end, float& radius); float computeMass(); - using SpatiallyNestable::setPosition; - virtual void setPosition(const glm::vec3& position) override; - using SpatiallyNestable::setOrientation; - virtual void setOrientation(const glm::quat& orientation) override; + void setPositionViaScript(const glm::vec3& position) override; + void setOrientationViaScript(const glm::quat& orientation) override; // these call through to the SpatiallyNestable versions, but they are here to expose these to javascript. Q_INVOKABLE virtual const QUuid getParentID() const override { return SpatiallyNestable::getParentID(); } @@ -240,7 +236,7 @@ public: bool hasNewJointData() const { return _hasNewJointData; } float getBoundingRadius() const; - + void addToScene(AvatarSharedPointer self, const render::ScenePointer& scene); void ensureInScene(AvatarSharedPointer self, const render::ScenePointer& scene); bool isInScene() const { return render::Item::isValidID(_renderItemID); } @@ -303,7 +299,6 @@ protected: glm::vec3 getBodyRightDirection() const { return getOrientation() * IDENTITY_RIGHT; } glm::vec3 getBodyUpDirection() const { return getOrientation() * IDENTITY_UP; } - glm::quat computeRotationFromBodyToWorldUp(float proportion = 1.0f) const; void measureMotionDerivatives(float deltaTime); float getSkeletonHeight() const; diff --git a/libraries/avatars-renderer/src/avatars-renderer/SkeletonModel.cpp b/libraries/avatars-renderer/src/avatars-renderer/SkeletonModel.cpp index 9651951b46..c0d5fc07d7 100644 --- a/libraries/avatars-renderer/src/avatars-renderer/SkeletonModel.cpp +++ b/libraries/avatars-renderer/src/avatars-renderer/SkeletonModel.cpp @@ -118,16 +118,16 @@ void SkeletonModel::updateRig(float deltaTime, glm::mat4 parentTransform) { _rig.updateFromEyeParameters(eyeParams); } -void SkeletonModel::updateAttitude() { +void SkeletonModel::updateAttitude(const glm::quat& orientation) { setTranslation(_owningAvatar->getSkeletonPosition()); - setRotation(_owningAvatar->getOrientation() * Quaternions::Y_180); + setRotation(orientation * Quaternions::Y_180); setScale(glm::vec3(1.0f, 1.0f, 1.0f) * _owningAvatar->getScale()); } // Called by Avatar::simulate after it has set the joint states (fullUpdate true if changed), // but just before head has been simulated. void SkeletonModel::simulate(float deltaTime, bool fullUpdate) { - updateAttitude(); + updateAttitude(_owningAvatar->getOrientation()); if (fullUpdate) { setBlendshapeCoefficients(_owningAvatar->getHead()->getSummedBlendshapeCoefficients()); diff --git a/libraries/avatars-renderer/src/avatars-renderer/SkeletonModel.h b/libraries/avatars-renderer/src/avatars-renderer/SkeletonModel.h index e48884c581..919e82825c 100644 --- a/libraries/avatars-renderer/src/avatars-renderer/SkeletonModel.h +++ b/libraries/avatars-renderer/src/avatars-renderer/SkeletonModel.h @@ -35,7 +35,7 @@ public: void simulate(float deltaTime, bool fullUpdate = true) override; void updateRig(float deltaTime, glm::mat4 parentTransform) override; - void updateAttitude(); + void updateAttitude(const glm::quat& orientation); /// Returns the index of the left hand joint, or -1 if not found. int getLeftHandJointIndex() const { return isActive() ? getFBXGeometry().leftHandJointIndex : -1; } diff --git a/libraries/avatars/src/AvatarData.cpp b/libraries/avatars/src/AvatarData.cpp index cc64d21f0f..9570056353 100644 --- a/libraries/avatars/src/AvatarData.cpp +++ b/libraries/avatars/src/AvatarData.cpp @@ -57,6 +57,27 @@ static const float DEFAULT_AVATAR_DENSITY = 1000.0f; // density of water #define ASSERT(COND) do { if (!(COND)) { abort(); } } while(0) +size_t AvatarDataPacket::maxFaceTrackerInfoSize(size_t numBlendshapeCoefficients) { + return FACE_TRACKER_INFO_SIZE + numBlendshapeCoefficients * sizeof(float); +} + +size_t AvatarDataPacket::maxJointDataSize(size_t numJoints) { + const size_t validityBitsSize = (size_t)std::ceil(numJoints / (float)BITS_IN_BYTE); + + size_t totalSize = sizeof(uint8_t); // numJoints + + totalSize += validityBitsSize; // Orientations mask + totalSize += numJoints * sizeof(SixByteQuat); // Orientations + totalSize += validityBitsSize; // Translations mask + totalSize += numJoints * sizeof(SixByteTrans); // Translations + + size_t NUM_FAUX_JOINT = 2; + totalSize += NUM_FAUX_JOINT * (sizeof(SixByteQuat) + sizeof(SixByteTrans)); // faux joints + + return totalSize; +} + + AvatarData::AvatarData() : SpatiallyNestable(NestableType::Avatar, QUuid()), _handPosition(0.0f), @@ -70,22 +91,6 @@ AvatarData::AvatarData() : _targetVelocity(0.0f), _density(DEFAULT_AVATAR_DENSITY) { - setBodyPitch(0.0f); - setBodyYaw(-90.0f); - setBodyRoll(0.0f); - - ASSERT(sizeof(AvatarDataPacket::Header) == AvatarDataPacket::HEADER_SIZE); - ASSERT(sizeof(AvatarDataPacket::AvatarGlobalPosition) == AvatarDataPacket::AVATAR_GLOBAL_POSITION_SIZE); - ASSERT(sizeof(AvatarDataPacket::AvatarLocalPosition) == AvatarDataPacket::AVATAR_LOCAL_POSITION_SIZE); - ASSERT(sizeof(AvatarDataPacket::AvatarBoundingBox) == AvatarDataPacket::AVATAR_BOUNDING_BOX_SIZE); - ASSERT(sizeof(AvatarDataPacket::AvatarOrientation) == AvatarDataPacket::AVATAR_ORIENTATION_SIZE); - ASSERT(sizeof(AvatarDataPacket::AvatarScale) == AvatarDataPacket::AVATAR_SCALE_SIZE); - ASSERT(sizeof(AvatarDataPacket::LookAtPosition) == AvatarDataPacket::LOOK_AT_POSITION_SIZE); - ASSERT(sizeof(AvatarDataPacket::AudioLoudness) == AvatarDataPacket::AUDIO_LOUDNESS_SIZE); - ASSERT(sizeof(AvatarDataPacket::SensorToWorldMatrix) == AvatarDataPacket::SENSOR_TO_WORLD_SIZE); - ASSERT(sizeof(AvatarDataPacket::AdditionalFlags) == AvatarDataPacket::ADDITIONAL_FLAGS_SIZE); - ASSERT(sizeof(AvatarDataPacket::ParentInfo) == AvatarDataPacket::PARENT_INFO_SIZE); - ASSERT(sizeof(AvatarDataPacket::FaceTrackerInfo) == AvatarDataPacket::FACE_TRACKER_INFO_SIZE); } AvatarData::~AvatarData() { @@ -102,23 +107,6 @@ const QUrl& AvatarData::defaultFullAvatarModelUrl() { return _defaultFullAvatarModelUrl; } -// There are a number of possible strategies for this set of tools through endRender, below. -void AvatarData::nextAttitude(glm::vec3 position, glm::quat orientation) { - bool success; - Transform trans = getTransform(success); - if (!success) { - qCWarning(avatars) << "Warning -- AvatarData::nextAttitude failed"; - return; - } - trans.setTranslation(position); - trans.setRotation(orientation); - SpatiallyNestable::setTransform(trans, success); - if (!success) { - qCWarning(avatars) << "Warning -- AvatarData::nextAttitude failed"; - } - updateAttitude(); -} - void AvatarData::setTargetScale(float targetScale) { auto newValue = glm::clamp(targetScale, MIN_AVATAR_SCALE, MAX_AVATAR_SCALE); if (_targetScale != newValue) { @@ -169,12 +157,12 @@ float AvatarData::getDistanceBasedMinTranslationDistance(glm::vec3 viewerPositio // we want to track outbound data in this case... -QByteArray AvatarData::toByteArrayStateful(AvatarDataDetail dataDetail) { +QByteArray AvatarData::toByteArrayStateful(AvatarDataDetail dataDetail, bool dropFaceTracking) { AvatarDataPacket::HasFlags hasFlagsOut; auto lastSentTime = _lastToByteArray; _lastToByteArray = usecTimestampNow(); return AvatarData::toByteArray(dataDetail, lastSentTime, getLastSentJointData(), - hasFlagsOut, false, false, glm::vec3(0), nullptr, + hasFlagsOut, dropFaceTracking, false, glm::vec3(0), nullptr, &_outboundDataRate); } @@ -189,15 +177,11 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent lazyInitHeadData(); - QByteArray avatarDataByteArray(udt::MAX_PACKET_SIZE, 0); - unsigned char* destinationBuffer = reinterpret_cast(avatarDataByteArray.data()); - unsigned char* startPosition = destinationBuffer; - // special case, if we were asked for no data, then just include the flags all set to nothing if (dataDetail == NoData) { AvatarDataPacket::HasFlags packetStateFlags = 0; - memcpy(destinationBuffer, &packetStateFlags, sizeof(packetStateFlags)); - return avatarDataByteArray.left(sizeof(packetStateFlags)); + QByteArray avatarDataByteArray(reinterpret_cast(&packetStateFlags), sizeof(packetStateFlags)); + return avatarDataByteArray; } // FIXME - @@ -258,6 +242,15 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent hasJointData = sendAll || !sendMinimum; } + + const size_t byteArraySize = AvatarDataPacket::MAX_CONSTANT_HEADER_SIZE + + (hasFaceTrackerInfo ? AvatarDataPacket::maxFaceTrackerInfoSize(_headData->getNumSummedBlendshapeCoefficients()) : 0) + + (hasJointData ? AvatarDataPacket::maxJointDataSize(_jointData.size()) : 0); + + QByteArray avatarDataByteArray((int)byteArraySize, 0); + unsigned char* destinationBuffer = reinterpret_cast(avatarDataByteArray.data()); + unsigned char* startPosition = destinationBuffer; + // Leading flags, to indicate how much data is actually included in the packet... AvatarDataPacket::HasFlags packetStateFlags = (hasAvatarGlobalPosition ? AvatarDataPacket::PACKET_HAS_AVATAR_GLOBAL_POSITION : 0) @@ -478,12 +471,15 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent unsigned char* validityPosition = destinationBuffer; unsigned char validity = 0; int validityBit = 0; + int numValidityBytes = (int)std::ceil(numJoints / (float)BITS_IN_BYTE); #ifdef WANT_DEBUG int rotationSentCount = 0; unsigned char* beforeRotations = destinationBuffer; #endif + destinationBuffer += numValidityBytes; // Move pointer past the validity bytes + if (sentJointDataOut) { sentJointDataOut->resize(_jointData.size()); // Make sure the destination is resized before using it } @@ -503,6 +499,8 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent #ifdef WANT_DEBUG rotationSentCount++; #endif + destinationBuffer += packOrientationQuatToSixBytes(destinationBuffer, data.rotation); + if (sentJointDataOut) { auto jointDataOut = *sentJointDataOut; jointDataOut[i].rotation = data.rotation; @@ -512,28 +510,14 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent } } if (++validityBit == BITS_IN_BYTE) { - *destinationBuffer++ = validity; + *validityPosition++ = validity; validityBit = validity = 0; } } if (validityBit != 0) { - *destinationBuffer++ = validity; + *validityPosition++ = validity; } - validityBit = 0; - validity = *validityPosition++; - for (int i = 0; i < _jointData.size(); i++) { - const JointData& data = _jointData[i]; - if (validity & (1 << validityBit)) { - destinationBuffer += packOrientationQuatToSixBytes(destinationBuffer, data.rotation); - } - if (++validityBit == BITS_IN_BYTE) { - validityBit = 0; - validity = *validityPosition++; - } - } - - // joint translation data validityPosition = destinationBuffer; validity = 0; @@ -544,6 +528,8 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent unsigned char* beforeTranslations = destinationBuffer; #endif + destinationBuffer += numValidityBytes; // Move pointer past the validity bytes + float minTranslation = !distanceAdjust ? AVATAR_MIN_TRANSLATION : getDistanceBasedMinTranslationDistance(viewerPosition); float maxTranslationDimension = 0.0; @@ -562,6 +548,9 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent maxTranslationDimension = glm::max(fabsf(data.translation.y), maxTranslationDimension); maxTranslationDimension = glm::max(fabsf(data.translation.z), maxTranslationDimension); + destinationBuffer += + packFloatVec3ToSignedTwoByteFixed(destinationBuffer, data.translation, TRANSLATION_COMPRESSION_RADIX); + if (sentJointDataOut) { auto jointDataOut = *sentJointDataOut; jointDataOut[i].translation = data.translation; @@ -571,27 +560,13 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent } } if (++validityBit == BITS_IN_BYTE) { - *destinationBuffer++ = validity; + *validityPosition++ = validity; validityBit = validity = 0; } } if (validityBit != 0) { - *destinationBuffer++ = validity; - } - - validityBit = 0; - validity = *validityPosition++; - for (int i = 0; i < _jointData.size(); i++) { - const JointData& data = _jointData[i]; - if (validity & (1 << validityBit)) { - destinationBuffer += - packFloatVec3ToSignedTwoByteFixed(destinationBuffer, data.translation, TRANSLATION_COMPRESSION_RADIX); - } - if (++validityBit == BITS_IN_BYTE) { - validityBit = 0; - validity = *validityPosition++; - } + *validityPosition++ = validity; } // faux joints @@ -624,6 +599,12 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent } int avatarDataSize = destinationBuffer - startPosition; + + if (avatarDataSize > (int)byteArraySize) { + qCCritical(avatars) << "AvatarData::toByteArray buffer overflow"; // We've overflown into the heap + ASSERT(false); + } + return avatarDataByteArray.left(avatarDataSize); } // NOTE: This is never used in a "distanceAdjust" mode, so it's ok that it doesn't use a variable minimum rotation/translation @@ -1743,6 +1724,24 @@ void AvatarData::sendAvatarDataPacket() { bool cullSmallData = (randFloat() < AVATAR_SEND_FULL_UPDATE_RATIO); auto dataDetail = cullSmallData ? SendAllData : CullSmallData; QByteArray avatarByteArray = toByteArrayStateful(dataDetail); + + int maximumByteArraySize = NLPacket::maxPayloadSize(PacketType::AvatarData) - sizeof(AvatarDataSequenceNumber); + + if (avatarByteArray.size() > maximumByteArraySize) { + qCWarning(avatars) << "toByteArrayStateful() resulted in very large buffer:" << avatarByteArray.size() << "... attempt to drop facial data"; + avatarByteArray = toByteArrayStateful(dataDetail, true); + + if (avatarByteArray.size() > maximumByteArraySize) { + qCWarning(avatars) << "toByteArrayStateful() without facial data resulted in very large buffer:" << avatarByteArray.size() << "... reduce to MinimumData"; + avatarByteArray = toByteArrayStateful(MinimumData, true); + + if (avatarByteArray.size() > maximumByteArraySize) { + qCWarning(avatars) << "toByteArrayStateful() MinimumData resulted in very large buffer:" << avatarByteArray.size() << "... FAIL!!"; + return; + } + } + } + doneEncoding(cullSmallData); static AvatarDataSequenceNumber sequenceNumber = 0; @@ -2081,6 +2080,7 @@ void AvatarData::fromJson(const QJsonObject& json, bool useFrameSkeleton) { currentBasis = std::make_shared(Transform::fromJson(json[JSON_AVATAR_BASIS])); } + glm::quat orientation; 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 @@ -2092,12 +2092,14 @@ void AvatarData::fromJson(const QJsonObject& json, bool useFrameSkeleton) { auto relativeTransform = Transform::fromJson(json[JSON_AVATAR_RELATIVE]); auto worldTransform = currentBasis->worldTransform(relativeTransform); setPosition(worldTransform.getTranslation()); - setOrientation(worldTransform.getRotation()); + orientation = worldTransform.getRotation(); } else { // We still set the position in the case that there is no movement. setPosition(currentBasis->getTranslation()); - setOrientation(currentBasis->getRotation()); + orientation = currentBasis->getRotation(); } + setOrientation(orientation); + updateAttitude(orientation); // Do after avatar orientation because head look-at needs avatar orientation. if (json.contains(JSON_AVATAR_HEAD)) { @@ -2215,11 +2217,11 @@ void AvatarData::setBodyRoll(float bodyRoll) { setOrientation(glm::quat(glm::radians(eulerAngles))); } -void AvatarData::setPosition(const glm::vec3& position) { +void AvatarData::setPositionViaScript(const glm::vec3& position) { SpatiallyNestable::setPosition(position); } -void AvatarData::setOrientation(const glm::quat& orientation) { +void AvatarData::setOrientationViaScript(const glm::quat& orientation) { SpatiallyNestable::setOrientation(orientation); } @@ -2347,6 +2349,12 @@ AvatarEntityMap AvatarData::getAvatarEntityData() const { return result; } +void AvatarData::insertDetachedEntityID(const QUuid entityID) { + _avatarEntitiesLock.withWriteLock([&] { + _avatarEntityDetached.insert(entityID); + }); +} + void AvatarData::setAvatarEntityData(const AvatarEntityMap& avatarEntityData) { if (avatarEntityData.size() > MAX_NUM_AVATAR_ENTITIES) { // the data is suspect diff --git a/libraries/avatars/src/AvatarData.h b/libraries/avatars/src/AvatarData.h index 1f672328ba..0e936c49e0 100644 --- a/libraries/avatars/src/AvatarData.h +++ b/libraries/avatars/src/AvatarData.h @@ -140,35 +140,41 @@ namespace AvatarDataPacket { const HasFlags PACKET_HAS_JOINT_DATA = 1U << 11; const size_t AVATAR_HAS_FLAGS_SIZE = 2; + using SixByteQuat = uint8_t[6]; + using SixByteTrans = uint8_t[6]; + // NOTE: AvatarDataPackets start with a uint16_t sequence number that is not reflected in the Header structure. PACKED_BEGIN struct Header { HasFlags packetHasFlags; // state flags, indicated which additional records are included in the packet } PACKED_END; const size_t HEADER_SIZE = 2; + static_assert(sizeof(Header) == HEADER_SIZE, "AvatarDataPacket::Header size doesn't match."); PACKED_BEGIN struct AvatarGlobalPosition { float globalPosition[3]; // avatar's position } PACKED_END; const size_t AVATAR_GLOBAL_POSITION_SIZE = 12; + static_assert(sizeof(AvatarGlobalPosition) == AVATAR_GLOBAL_POSITION_SIZE, "AvatarDataPacket::AvatarGlobalPosition size doesn't match."); PACKED_BEGIN struct AvatarBoundingBox { float avatarDimensions[3]; // avatar's bounding box in world space units, but relative to the position. float boundOriginOffset[3]; // offset from the position of the avatar to the origin of the bounding box } PACKED_END; const size_t AVATAR_BOUNDING_BOX_SIZE = 24; + static_assert(sizeof(AvatarBoundingBox) == AVATAR_BOUNDING_BOX_SIZE, "AvatarDataPacket::AvatarBoundingBox size doesn't match."); - - using SixByteQuat = uint8_t[6]; PACKED_BEGIN struct AvatarOrientation { SixByteQuat avatarOrientation; // encodeded and compressed by packOrientationQuatToSixBytes() } PACKED_END; const size_t AVATAR_ORIENTATION_SIZE = 6; + static_assert(sizeof(AvatarOrientation) == AVATAR_ORIENTATION_SIZE, "AvatarDataPacket::AvatarOrientation size doesn't match."); PACKED_BEGIN struct AvatarScale { SmallFloat scale; // avatar's scale, compressed by packFloatRatioToTwoByte() } PACKED_END; const size_t AVATAR_SCALE_SIZE = 2; + static_assert(sizeof(AvatarScale) == AVATAR_SCALE_SIZE, "AvatarDataPacket::AvatarScale size doesn't match."); PACKED_BEGIN struct LookAtPosition { float lookAtPosition[3]; // world space position that eyes are focusing on. @@ -180,11 +186,13 @@ namespace AvatarDataPacket { // POTENTIAL SAVINGS - 12 bytes } PACKED_END; const size_t LOOK_AT_POSITION_SIZE = 12; + static_assert(sizeof(LookAtPosition) == LOOK_AT_POSITION_SIZE, "AvatarDataPacket::LookAtPosition size doesn't match."); PACKED_BEGIN struct AudioLoudness { uint8_t audioLoudness; // current loudness of microphone compressed with packFloatGainToByte() } PACKED_END; const size_t AUDIO_LOUDNESS_SIZE = 1; + static_assert(sizeof(AudioLoudness) == AUDIO_LOUDNESS_SIZE, "AvatarDataPacket::AudioLoudness size doesn't match."); PACKED_BEGIN struct SensorToWorldMatrix { // FIXME - these 20 bytes are only used by viewers if my avatar has "attachments" @@ -199,11 +207,13 @@ namespace AvatarDataPacket { // relative to the avatar position. } PACKED_END; const size_t SENSOR_TO_WORLD_SIZE = 20; + static_assert(sizeof(SensorToWorldMatrix) == SENSOR_TO_WORLD_SIZE, "AvatarDataPacket::SensorToWorldMatrix size doesn't match."); PACKED_BEGIN struct AdditionalFlags { uint8_t flags; // additional flags: hand state, key state, eye tracking } PACKED_END; const size_t ADDITIONAL_FLAGS_SIZE = 1; + static_assert(sizeof(AdditionalFlags) == ADDITIONAL_FLAGS_SIZE, "AvatarDataPacket::AdditionalFlags size doesn't match."); // only present if HAS_REFERENTIAL flag is set in AvatarInfo.flags PACKED_BEGIN struct ParentInfo { @@ -211,6 +221,7 @@ namespace AvatarDataPacket { uint16_t parentJointIndex; } PACKED_END; const size_t PARENT_INFO_SIZE = 18; + static_assert(sizeof(ParentInfo) == PARENT_INFO_SIZE, "AvatarDataPacket::ParentInfo size doesn't match."); // will only ever be included if the avatar has a parent but can change independent of changes to parent info // and so we keep it a separate record @@ -218,6 +229,22 @@ namespace AvatarDataPacket { float localPosition[3]; // parent frame translation of the avatar } PACKED_END; const size_t AVATAR_LOCAL_POSITION_SIZE = 12; + static_assert(sizeof(AvatarLocalPosition) == AVATAR_LOCAL_POSITION_SIZE, "AvatarDataPacket::AvatarLocalPosition size doesn't match."); + + const size_t MAX_CONSTANT_HEADER_SIZE = HEADER_SIZE + + AVATAR_GLOBAL_POSITION_SIZE + + AVATAR_BOUNDING_BOX_SIZE + + AVATAR_ORIENTATION_SIZE + + AVATAR_SCALE_SIZE + + LOOK_AT_POSITION_SIZE + + AUDIO_LOUDNESS_SIZE + + SENSOR_TO_WORLD_SIZE + + ADDITIONAL_FLAGS_SIZE + + PARENT_INFO_SIZE + + AVATAR_LOCAL_POSITION_SIZE; + + + // variable length structure follows // only present if IS_FACE_TRACKER_CONNECTED flag is set in AvatarInfo.flags PACKED_BEGIN struct FaceTrackerInfo { @@ -229,8 +256,9 @@ namespace AvatarDataPacket { // float blendshapeCoefficients[numBlendshapeCoefficients]; } PACKED_END; const size_t FACE_TRACKER_INFO_SIZE = 17; + static_assert(sizeof(FaceTrackerInfo) == FACE_TRACKER_INFO_SIZE, "AvatarDataPacket::FaceTrackerInfo size doesn't match."); + size_t maxFaceTrackerInfoSize(size_t numBlendshapeCoefficients); - // variable length structure follows /* struct JointData { uint8_t numJoints; @@ -240,6 +268,7 @@ namespace AvatarDataPacket { SixByteTrans translation[numValidTranslations]; // encodeded and compressed by packFloatVec3ToSignedTwoByteFixed() }; */ + size_t maxJointDataSize(size_t numJoints); } static const float MAX_AVATAR_SCALE = 1000.0f; @@ -322,14 +351,14 @@ public: class AvatarData : public QObject, public SpatiallyNestable { Q_OBJECT - Q_PROPERTY(glm::vec3 position READ getPosition WRITE setPosition) + Q_PROPERTY(glm::vec3 position READ getPosition WRITE setPositionViaScript) Q_PROPERTY(float scale READ getTargetScale WRITE setTargetScale) Q_PROPERTY(glm::vec3 handPosition READ getHandPosition WRITE setHandPosition) Q_PROPERTY(float bodyYaw READ getBodyYaw WRITE setBodyYaw) Q_PROPERTY(float bodyPitch READ getBodyPitch WRITE setBodyPitch) Q_PROPERTY(float bodyRoll READ getBodyRoll WRITE setBodyRoll) - Q_PROPERTY(glm::quat orientation READ getOrientation WRITE setOrientation) + Q_PROPERTY(glm::quat orientation READ getOrientation WRITE setOrientationViaScript) Q_PROPERTY(glm::quat headOrientation READ getHeadOrientation WRITE setHeadOrientation) Q_PROPERTY(float headPitch READ getHeadPitch WRITE setHeadPitch) Q_PROPERTY(float headYaw READ getHeadYaw WRITE setHeadYaw) @@ -387,7 +416,7 @@ public: SendAllData } AvatarDataDetail; - virtual QByteArray toByteArrayStateful(AvatarDataDetail dataDetail); + virtual QByteArray toByteArrayStateful(AvatarDataDetail dataDetail, bool dropFaceTracking = false); virtual QByteArray toByteArray(AvatarDataDetail dataDetail, quint64 lastSentTime, const QVector& lastSentJointData, AvatarDataPacket::HasFlags& hasFlagsOut, bool dropFaceTracking, bool distanceAdjust, glm::vec3 viewerPosition, @@ -411,13 +440,10 @@ public: float getBodyRoll() const; void setBodyRoll(float bodyRoll); - using SpatiallyNestable::setPosition; - virtual void setPosition(const glm::vec3& position) override; - using SpatiallyNestable::setOrientation; - virtual void setOrientation(const glm::quat& orientation) override; + virtual void setPositionViaScript(const glm::vec3& position); + virtual void setOrientationViaScript(const glm::quat& orientation); - void nextAttitude(glm::vec3 position, glm::quat orientation); // Can be safely called at any time. - virtual void updateAttitude() {} // Tell skeleton mesh about changes + virtual void updateAttitude(const glm::quat& orientation) {} glm::quat getHeadOrientation() const { lazyInitHeadData(); @@ -586,6 +612,7 @@ public: Q_INVOKABLE AvatarEntityMap getAvatarEntityData() const; Q_INVOKABLE void setAvatarEntityData(const AvatarEntityMap& avatarEntityData); void setAvatarEntityDataChanged(bool value) { _avatarEntityDataChanged = value; } + void insertDetachedEntityID(const QUuid entityID); AvatarEntityIDs getAndClearRecentlyDetachedIDs(); // thread safe diff --git a/libraries/avatars/src/AvatarHashMap.cpp b/libraries/avatars/src/AvatarHashMap.cpp index e8c37bdaa8..3712080cdb 100644 --- a/libraries/avatars/src/AvatarHashMap.cpp +++ b/libraries/avatars/src/AvatarHashMap.cpp @@ -170,13 +170,6 @@ void AvatarHashMap::processKillAvatar(QSharedPointer message, S removeAvatar(sessionUUID, reason); } -void AvatarHashMap::processExitingSpaceBubble(QSharedPointer message, SharedNodePointer sendingNode) { - // read the node id - QUuid sessionUUID = QUuid::fromRfc4122(message->readWithoutCopy(NUM_BYTES_RFC4122_UUID)); - auto nodeList = DependencyManager::get(); - nodeList->radiusIgnoreNodeBySessionID(sessionUUID, false); -} - void AvatarHashMap::removeAvatar(const QUuid& sessionUUID, KillAvatarReason removalReason) { QWriteLocker locker(&_hashLock); diff --git a/libraries/avatars/src/AvatarHashMap.h b/libraries/avatars/src/AvatarHashMap.h index 21ea8081c7..68fc232685 100644 --- a/libraries/avatars/src/AvatarHashMap.h +++ b/libraries/avatars/src/AvatarHashMap.h @@ -60,7 +60,6 @@ protected slots: void processAvatarDataPacket(QSharedPointer message, SharedNodePointer sendingNode); void processAvatarIdentityPacket(QSharedPointer message, SharedNodePointer sendingNode); void processKillAvatar(QSharedPointer message, SharedNodePointer sendingNode); - void processExitingSpaceBubble(QSharedPointer message, SharedNodePointer sendingNode); protected: AvatarHashMap(); diff --git a/libraries/avatars/src/HeadData.cpp b/libraries/avatars/src/HeadData.cpp index 8ae33a1b4f..1fda984e78 100644 --- a/libraries/avatars/src/HeadData.cpp +++ b/libraries/avatars/src/HeadData.cpp @@ -83,6 +83,11 @@ static const QMap& getBlendshapesLookupMap() { return blendshapeLookupMap; } +int HeadData::getNumSummedBlendshapeCoefficients() const { + int maxSize = std::max(_blendshapeCoefficients.size(), _transientBlendshapeCoefficients.size()); + return maxSize; +} + const QVector& HeadData::getSummedBlendshapeCoefficients() { int maxSize = std::max(_blendshapeCoefficients.size(), _transientBlendshapeCoefficients.size()); if (_summedBlendshapeCoefficients.size() != maxSize) { diff --git a/libraries/avatars/src/HeadData.h b/libraries/avatars/src/HeadData.h index c15714bd73..bcc2cacde5 100644 --- a/libraries/avatars/src/HeadData.h +++ b/libraries/avatars/src/HeadData.h @@ -57,6 +57,7 @@ public: void setBlendshape(QString name, float val); const QVector& getBlendshapeCoefficients() const { return _blendshapeCoefficients; } const QVector& getSummedBlendshapeCoefficients(); + int getNumSummedBlendshapeCoefficients() const; void setBlendshapeCoefficients(const QVector& blendshapeCoefficients) { _blendshapeCoefficients = blendshapeCoefficients; } const glm::vec3& getLookAtPosition() const { return _lookAtPosition; } diff --git a/libraries/controllers/src/controllers/Actions.cpp b/libraries/controllers/src/controllers/Actions.cpp index b3c0ed3f05..d8dd7f5e35 100644 --- a/libraries/controllers/src/controllers/Actions.cpp +++ b/libraries/controllers/src/controllers/Actions.cpp @@ -101,6 +101,23 @@ namespace controller { makePosePair(Action::RIGHT_HAND_PINKY3, "RightHandPinky3"), makePosePair(Action::RIGHT_HAND_PINKY4, "RightHandPinky4"), + makePosePair(Action::TRACKED_OBJECT_00, "TrackedObject00"), + makePosePair(Action::TRACKED_OBJECT_01, "TrackedObject01"), + makePosePair(Action::TRACKED_OBJECT_02, "TrackedObject02"), + makePosePair(Action::TRACKED_OBJECT_03, "TrackedObject03"), + makePosePair(Action::TRACKED_OBJECT_04, "TrackedObject04"), + makePosePair(Action::TRACKED_OBJECT_05, "TrackedObject05"), + makePosePair(Action::TRACKED_OBJECT_06, "TrackedObject06"), + makePosePair(Action::TRACKED_OBJECT_07, "TrackedObject07"), + makePosePair(Action::TRACKED_OBJECT_08, "TrackedObject08"), + makePosePair(Action::TRACKED_OBJECT_09, "TrackedObject09"), + makePosePair(Action::TRACKED_OBJECT_10, "TrackedObject10"), + makePosePair(Action::TRACKED_OBJECT_11, "TrackedObject11"), + makePosePair(Action::TRACKED_OBJECT_12, "TrackedObject12"), + makePosePair(Action::TRACKED_OBJECT_13, "TrackedObject13"), + makePosePair(Action::TRACKED_OBJECT_14, "TrackedObject14"), + makePosePair(Action::TRACKED_OBJECT_15, "TrackedObject15"), + 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 ec4800c9aa..6319b5746e 100644 --- a/libraries/controllers/src/controllers/Actions.h +++ b/libraries/controllers/src/controllers/Actions.h @@ -146,6 +146,23 @@ enum class Action { RIGHT_HAND_PINKY3, RIGHT_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_ACTIONS, }; diff --git a/libraries/controllers/src/controllers/InputDevice.cpp b/libraries/controllers/src/controllers/InputDevice.cpp index dd27726a55..a907842a17 100644 --- a/libraries/controllers/src/controllers/InputDevice.cpp +++ b/libraries/controllers/src/controllers/InputDevice.cpp @@ -15,20 +15,6 @@ namespace controller { - const float DEFAULT_HAND_RETICLE_MOVE_SPEED = 37.5f; - float InputDevice::_reticleMoveSpeed = DEFAULT_HAND_RETICLE_MOVE_SPEED; - - //Constants for getCursorPixelRangeMultiplier() - const float MIN_PIXEL_RANGE_MULT = 0.4f; - const float MAX_PIXEL_RANGE_MULT = 2.0f; - const float RANGE_MULT = (MAX_PIXEL_RANGE_MULT - MIN_PIXEL_RANGE_MULT) * 0.01f; - - //Returns a multiplier to be applied to the cursor range for the controllers - float InputDevice::getCursorPixelRangeMult() { - //scales (0,100) to (MINIMUM_PIXEL_RANGE_MULT, MAXIMUM_PIXEL_RANGE_MULT) - return InputDevice::_reticleMoveSpeed * RANGE_MULT + MIN_PIXEL_RANGE_MULT; - } - float InputDevice::getButton(int channel) const { if (!_buttonPressedMap.empty()) { if (_buttonPressedMap.find(channel) != _buttonPressedMap.end()) { diff --git a/libraries/controllers/src/controllers/InputDevice.h b/libraries/controllers/src/controllers/InputDevice.h index 9d46930104..ff665d912d 100644 --- a/libraries/controllers/src/controllers/InputDevice.h +++ b/libraries/controllers/src/controllers/InputDevice.h @@ -73,10 +73,6 @@ public: int getDeviceID() { return _deviceID; } void setDeviceID(int deviceID) { _deviceID = deviceID; } - static float getCursorPixelRangeMult(); - static float getReticleMoveSpeed() { return _reticleMoveSpeed; } - static void setReticleMoveSpeed(float reticleMoveSpeed) { _reticleMoveSpeed = reticleMoveSpeed; } - Input makeInput(StandardButtonChannel button) const; Input makeInput(StandardAxisChannel axis) const; Input makeInput(StandardPoseChannel pose) const; @@ -99,9 +95,6 @@ protected: ButtonPressedMap _buttonPressedMap; AxisStateMap _axisStateMap; PoseStateMap _poseStateMap; - -private: - static float _reticleMoveSpeed; }; } diff --git a/libraries/controllers/src/controllers/StandardController.cpp b/libraries/controllers/src/controllers/StandardController.cpp index 40b87bc6b2..ed729867df 100644 --- a/libraries/controllers/src/controllers/StandardController.cpp +++ b/libraries/controllers/src/controllers/StandardController.cpp @@ -166,6 +166,23 @@ Input::NamedVector StandardController::getAvailableInputs() const { makePair(DD, "Down"), makePair(DL, "Left"), makePair(DR, "Right"), + + 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") }; return availableInputs; } diff --git a/libraries/controllers/src/controllers/impl/Filter.cpp b/libraries/controllers/src/controllers/impl/Filter.cpp index e75465f007..b6c3ed4d27 100644 --- a/libraries/controllers/src/controllers/impl/Filter.cpp +++ b/libraries/controllers/src/controllers/impl/Filter.cpp @@ -22,6 +22,7 @@ #include "filters/DeadZoneFilter.h" #include "filters/HysteresisFilter.h" #include "filters/InvertFilter.h" +#include "filters/NotFilter.h" #include "filters/PulseFilter.h" #include "filters/ScaleFilter.h" #include "filters/TranslateFilter.h" @@ -40,6 +41,7 @@ REGISTER_FILTER_CLASS_INSTANCE(ConstrainToPositiveIntegerFilter, "constrainToPos REGISTER_FILTER_CLASS_INSTANCE(DeadZoneFilter, "deadZone") REGISTER_FILTER_CLASS_INSTANCE(HysteresisFilter, "hysteresis") REGISTER_FILTER_CLASS_INSTANCE(InvertFilter, "invert") +REGISTER_FILTER_CLASS_INSTANCE(NotFilter, "logicalNot") REGISTER_FILTER_CLASS_INSTANCE(ScaleFilter, "scale") REGISTER_FILTER_CLASS_INSTANCE(PulseFilter, "pulse") REGISTER_FILTER_CLASS_INSTANCE(TranslateFilter, "translate") diff --git a/libraries/controllers/src/controllers/impl/RouteBuilderProxy.cpp b/libraries/controllers/src/controllers/impl/RouteBuilderProxy.cpp index 581d51ea61..bc1ef55725 100644 --- a/libraries/controllers/src/controllers/impl/RouteBuilderProxy.cpp +++ b/libraries/controllers/src/controllers/impl/RouteBuilderProxy.cpp @@ -24,6 +24,7 @@ #include "filters/DeadZoneFilter.h" #include "filters/HysteresisFilter.h" #include "filters/InvertFilter.h" +#include "filters/NotFilter.h" #include "filters/PulseFilter.h" #include "filters/ScaleFilter.h" #include "filters/TranslateFilter.h" @@ -148,6 +149,11 @@ QObject* RouteBuilderProxy::pulse(float interval) { return this; } +QObject* RouteBuilderProxy::logicalNot() { + addFilter(std::make_shared()); + return this; +} + void RouteBuilderProxy::addFilter(Filter::Pointer filter) { _route->filters.push_back(filter); } diff --git a/libraries/controllers/src/controllers/impl/RouteBuilderProxy.h b/libraries/controllers/src/controllers/impl/RouteBuilderProxy.h index 102e72b3ad..75f3747566 100644 --- a/libraries/controllers/src/controllers/impl/RouteBuilderProxy.h +++ b/libraries/controllers/src/controllers/impl/RouteBuilderProxy.h @@ -53,6 +53,7 @@ class RouteBuilderProxy : public QObject { Q_INVOKABLE QObject* postTransform(glm::mat4 transform); Q_INVOKABLE QObject* rotate(glm::quat rotation); Q_INVOKABLE QObject* lowVelocity(float rotationConstant, float translationConstant); + Q_INVOKABLE QObject* logicalNot(); private: void to(const Endpoint::Pointer& destination); diff --git a/libraries/controllers/src/controllers/impl/filters/NotFilter.cpp b/libraries/controllers/src/controllers/impl/filters/NotFilter.cpp new file mode 100644 index 0000000000..73460aed91 --- /dev/null +++ b/libraries/controllers/src/controllers/impl/filters/NotFilter.cpp @@ -0,0 +1,10 @@ +#include "NotFilter.h" + +using namespace controller; + +NotFilter::NotFilter() { +} + +float NotFilter::apply(float value) const { + return (value == 0.0f) ? 1.0f : 0.0f; +} diff --git a/libraries/controllers/src/controllers/impl/filters/NotFilter.h b/libraries/controllers/src/controllers/impl/filters/NotFilter.h new file mode 100644 index 0000000000..ceb7d29de3 --- /dev/null +++ b/libraries/controllers/src/controllers/impl/filters/NotFilter.h @@ -0,0 +1,21 @@ +#pragma once +#ifndef hifi_Controllers_Filters_Not_h +#define hifi_Controllers_Filters_Not_h + +#include "../Filter.h" + +namespace controller { + +class NotFilter : public Filter { + REGISTER_FILTER_CLASS(NotFilter); +public: + NotFilter(); + + virtual float apply(float value) const override; + virtual Pose apply(Pose value) const override { return value; } +}; + +} + +#endif + diff --git a/libraries/display-plugins/src/display-plugins/NullDisplayPlugin.h b/libraries/display-plugins/src/display-plugins/NullDisplayPlugin.h index 1da0441d8f..8d01539e8a 100644 --- a/libraries/display-plugins/src/display-plugins/NullDisplayPlugin.h +++ b/libraries/display-plugins/src/display-plugins/NullDisplayPlugin.h @@ -19,6 +19,7 @@ public: bool hasFocus() const override; void submitFrame(const gpu::FramePointer& newFrame) override; QImage getScreenshot(float aspectRatio = 0.0f) const override; + void copyTextureToQuickFramebuffer(NetworkTexturePointer source, QOpenGLFramebufferObject* target, GLsync* fenceSync) override {}; private: static const QString NAME; }; diff --git a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp index 67bbb452ca..e1259fc5fc 100644 --- a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp @@ -16,6 +16,7 @@ #include #include +#include #if defined(Q_OS_MAC) #include #endif @@ -41,7 +42,7 @@ #include #include #include - +#include #include "CompositorHelper.h" #include "Logging.h" @@ -55,7 +56,7 @@ out vec4 outFragColor; float sRGBFloatToLinear(float value) { const float SRGB_ELBOW = 0.04045; - + return (value <= SRGB_ELBOW) ? value / 12.92 : pow((value + 0.055) / 1.055, 2.4); } @@ -121,10 +122,10 @@ public: PROFILE_SET_THREAD_NAME("Present Thread"); // FIXME determine the best priority balance between this and the main thread... - // It may be dependent on the display plugin being used, since VR plugins should + // It may be dependent on the display plugin being used, since VR plugins should // have higher priority on rendering (although we could say that the Oculus plugin // doesn't need that since it has async timewarp). - // A higher priority here + // A higher priority here setPriority(QThread::HighPriority); OpenGLDisplayPlugin* currentPlugin{ nullptr }; Q_ASSERT(_context); @@ -233,7 +234,7 @@ public: // Move the context back to the presentation thread _context->moveToThread(this); - // restore control of the context to the presentation thread and signal + // restore control of the context to the presentation thread and signal // the end of the operation _finishedMainThreadOperation = true; lock.unlock(); @@ -291,7 +292,7 @@ bool OpenGLDisplayPlugin::activate() { if (!RENDER_THREAD) { RENDER_THREAD = _presentThread; } - + // Child classes may override this in order to do things like initialize // libraries, etc if (!internalActivate()) { @@ -411,7 +412,7 @@ void OpenGLDisplayPlugin::customizeContext() { gpu::Shader::makeProgram(*program); gpu::StatePointer state = gpu::StatePointer(new gpu::State()); state->setDepthTest(gpu::State::DepthTest(false)); - state->setBlendFunction(true, + state->setBlendFunction(true, gpu::State::SRC_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::INV_SRC_ALPHA, gpu::State::FACTOR_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::ONE); _overlayPipeline = gpu::Pipeline::create(program, state); @@ -496,16 +497,48 @@ void OpenGLDisplayPlugin::submitFrame(const gpu::FramePointer& newFrame) { _newFrameQueue.push(newFrame); }); } + void OpenGLDisplayPlugin::renderFromTexture(gpu::Batch& batch, const gpu::TexturePointer texture, glm::ivec4 viewport, const glm::ivec4 scissor) { + renderFromTexture(batch, texture, viewport, scissor, gpu::FramebufferPointer()); +} + +void OpenGLDisplayPlugin::renderFromTexture(gpu::Batch& batch, const gpu::TexturePointer texture, glm::ivec4 viewport, const glm::ivec4 scissor, gpu::FramebufferPointer copyFbo /*=gpu::FramebufferPointer()*/) { + auto fbo = gpu::FramebufferPointer(); batch.enableStereo(false); batch.resetViewTransform(); - batch.setFramebuffer(gpu::FramebufferPointer()); + batch.setFramebuffer(fbo); batch.clearColorFramebuffer(gpu::Framebuffer::BUFFER_COLOR0, vec4(0)); batch.setStateScissorRect(scissor); batch.setViewportTransform(viewport); batch.setResourceTexture(0, texture); batch.setPipeline(_presentPipeline); batch.draw(gpu::TRIANGLE_STRIP, 4); + if (copyFbo) { + gpu::Vec4i copyFboRect(0, 0, copyFbo->getWidth(), copyFbo->getHeight()); + gpu::Vec4i sourceRect(scissor.x, scissor.y, scissor.x + scissor.z, scissor.y + scissor.w); + float aspectRatio = (float)scissor.w / (float) scissor.z; // height/width + // scale width first + int xOffset = 0; + int yOffset = 0; + int newWidth = copyFbo->getWidth(); + int newHeight = std::round(aspectRatio * (float) copyFbo->getWidth()); + if (newHeight > copyFbo->getHeight()) { + // ok, so now fill height instead + newHeight = copyFbo->getHeight(); + newWidth = std::round((float)copyFbo->getHeight() / aspectRatio); + xOffset = (copyFbo->getWidth() - newWidth) / 2; + } else { + yOffset = (copyFbo->getHeight() - newHeight) / 2; + } + gpu::Vec4i copyRect(xOffset, yOffset, xOffset + newWidth, yOffset + newHeight); + batch.setFramebuffer(copyFbo); + + batch.resetViewTransform(); + batch.setViewportTransform(copyFboRect); + batch.setStateScissorRect(copyFboRect); + batch.clearColorFramebuffer(gpu::Framebuffer::BUFFER_COLOR0, {0.0f, 0.0f, 0.0f, 1.0f}); + batch.blit(fbo, sourceRect, copyFbo, copyRect); + } } void OpenGLDisplayPlugin::updateFrameData() { @@ -686,7 +719,7 @@ void OpenGLDisplayPlugin::resetPresentRate() { // _presentRate = RateCounter<100>(); } -float OpenGLDisplayPlugin::renderRate() const { +float OpenGLDisplayPlugin::renderRate() const { return _renderRate.rate(); } @@ -821,3 +854,53 @@ void OpenGLDisplayPlugin::updateCompositeFramebuffer() { _compositeFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create("OpenGLDisplayPlugin::composite", gpu::Element::COLOR_RGBA_32, renderSize.x, renderSize.y)); } } + +void OpenGLDisplayPlugin::copyTextureToQuickFramebuffer(NetworkTexturePointer networkTexture, QOpenGLFramebufferObject* target, GLsync* fenceSync) { + auto glBackend = const_cast(*this).getGLBackend(); + withMainThreadContext([&] { + GLuint sourceTexture = glBackend->getTextureID(networkTexture->getGPUTexture()); + GLuint targetTexture = target->texture(); + GLuint fbo[2] {0, 0}; + + // need mipmaps for blitting texture + glGenerateTextureMipmap(sourceTexture); + + // create 2 fbos (one for initial texture, second for scaled one) + glCreateFramebuffers(2, fbo); + + // setup source fbo + glBindFramebuffer(GL_FRAMEBUFFER, fbo[0]); + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, sourceTexture, 0); + + GLint texWidth = networkTexture->getWidth(); + GLint texHeight = networkTexture->getHeight(); + + // setup destination fbo + glBindFramebuffer(GL_FRAMEBUFFER, fbo[1]); + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, targetTexture, 0); + glClearColor(0.0f, 0.0f, 0.0f, 0.0f); + glClear(GL_COLOR_BUFFER_BIT); + + + // maintain aspect ratio, filling the width first if possible. If that makes the height too + // much, fill height instead. TODO: only do this when texture changes + GLint newX = 0; + GLint newY = 0; + float aspectRatio = (float)texHeight / (float)texWidth; + GLint newWidth = target->width(); + GLint newHeight = std::round(aspectRatio * (float) target->width()); + if (newHeight > target->height()) { + newHeight = target->height(); + newWidth = std::round((float)target->height() / aspectRatio); + newX = (target->width() - newWidth) / 2; + } else { + newY = (target->height() - newHeight) / 2; + } + glBlitNamedFramebuffer(fbo[0], fbo[1], 0, 0, texWidth, texHeight, newX, newY, newX + newWidth, newY + newHeight, GL_DEPTH_BUFFER_BIT|GL_COLOR_BUFFER_BIT, GL_NEAREST); + + // don't delete the textures! + glDeleteFramebuffers(2, fbo); + *fenceSync = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0); + }); +} + diff --git a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.h b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.h index 7e7889ff47..2f93fa630d 100644 --- a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.h +++ b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.h @@ -38,7 +38,7 @@ protected: using Condition = std::condition_variable; public: ~OpenGLDisplayPlugin(); - // These must be final to ensure proper ordering of operations + // These must be final to ensure proper ordering of operations // between the main thread and the presentation thread bool activate() override final; void deactivate() override final; @@ -79,6 +79,8 @@ public: // Three threads, one for rendering, one for texture transfers, one reserved for the GL driver int getRequiredThreadCount() const override { return 3; } + void copyTextureToQuickFramebuffer(NetworkTexturePointer source, QOpenGLFramebufferObject* target, GLsync* fenceSync) override; + protected: friend class PresentThread; @@ -103,7 +105,7 @@ protected: // Returns true on successful activation virtual bool internalActivate() { return true; } virtual void internalDeactivate() {} - + // Returns true on successful activation of standby session virtual bool activateStandBySession() { return true; } virtual void deactivateSession() {} @@ -111,6 +113,7 @@ protected: // Plugin specific functionality to send the composed scene to the output window or device virtual void internalPresent(); + void renderFromTexture(gpu::Batch& batch, const gpu::TexturePointer texture, glm::ivec4 viewport, const glm::ivec4 scissor, gpu::FramebufferPointer fbo); void renderFromTexture(gpu::Batch& batch, const gpu::TexturePointer texture, glm::ivec4 viewport, const glm::ivec4 scissor); virtual void updateFrameData(); diff --git a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp index ea91890f33..b183850e7f 100644 --- a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp @@ -134,7 +134,7 @@ void HmdDisplayPlugin::customizeContext() { state->setBlendFunction(true, gpu::State::SRC_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::INV_SRC_ALPHA, gpu::State::FACTOR_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::ONE); - + gpu::Shader::BindingSet bindings; bindings.insert({ "lineData", LINE_DATA_SLOT });; gpu::Shader::makeProgram(*program, bindings); @@ -243,6 +243,8 @@ void HmdDisplayPlugin::internalPresent() { glm::ivec4 viewport = getViewportForSourceSize(sourceSize); glm::ivec4 scissor = viewport; + auto fbo = gpu::FramebufferPointer(); + render([&](gpu::Batch& batch) { if (_monoPreview) { @@ -285,11 +287,15 @@ void HmdDisplayPlugin::internalPresent() { viewport = ivec4(scissorOffset - scaledShiftLeftBy, viewportOffset, viewportSizeX, viewportSizeY); } + // TODO: only bother getting and passing in the hmdPreviewFramebuffer if the camera is on + fbo = DependencyManager::get()->getHmdPreviewFramebuffer(windowSize.x, windowSize.y); + viewport.z *= 2; } - renderFromTexture(batch, _compositeFramebuffer->getRenderBuffer(0), viewport, scissor); + renderFromTexture(batch, _compositeFramebuffer->getRenderBuffer(0), viewport, scissor, fbo); }); swapBuffers(); + } else if (_clearPreviewFlag) { QImage image; if (_vsyncEnabled) { @@ -312,7 +318,7 @@ void HmdDisplayPlugin::internalPresent() { _previewTexture->assignStoredMip(0, image.byteCount(), image.constBits()); _previewTexture->setAutoGenerateMips(true); } - + auto viewport = getViewportForSourceSize(uvec2(_previewTexture->getDimensions())); render([&](gpu::Batch& batch) { @@ -323,7 +329,7 @@ void HmdDisplayPlugin::internalPresent() { } postPreview(); - // If preview is disabled, we need to check to see if the window size has changed + // If preview is disabled, we need to check to see if the window size has changed // and re-render the no-preview message if (_disablePreview) { auto window = _container->getPrimaryWidget(); @@ -510,7 +516,7 @@ void HmdDisplayPlugin::OverlayRenderer::build() { indices = std::make_shared(); //UV mapping source: http://www.mvps.org/directx/articles/spheremap.htm - + static const float fov = CompositorHelper::VIRTUAL_UI_TARGET_FOV.y; static const float aspectRatio = CompositorHelper::VIRTUAL_UI_ASPECT_RATIO; static const uint16_t stacks = 128; @@ -672,7 +678,7 @@ bool HmdDisplayPlugin::setHandLaser(uint32_t hands, HandLaserMode mode, const ve _handLasers[1] = info; } }); - // FIXME defer to a child class plugin to determine if hand lasers are actually + // FIXME defer to a child class plugin to determine if hand lasers are actually // available based on the presence or absence of hand controllers return true; } @@ -687,7 +693,7 @@ bool HmdDisplayPlugin::setExtraLaser(HandLaserMode mode, const vec4& color, cons _extraLaserStart = sensorSpaceStart; }); - // FIXME defer to a child class plugin to determine if hand lasers are actually + // FIXME defer to a child class plugin to determine if hand lasers are actually // available based on the presence or absence of hand controllers return true; } @@ -702,7 +708,7 @@ void HmdDisplayPlugin::compositeExtra() { if (_presentHandPoses[0] == IDENTITY_MATRIX && _presentHandPoses[1] == IDENTITY_MATRIX && !_presentExtraLaser.valid()) { return; } - + render([&](gpu::Batch& batch) { batch.setFramebuffer(_compositeFramebuffer); batch.setModelTransform(Transform()); diff --git a/libraries/entities-renderer/src/EntityTreeRenderer.cpp b/libraries/entities-renderer/src/EntityTreeRenderer.cpp index a8eca41077..d55352d31e 100644 --- a/libraries/entities-renderer/src/EntityTreeRenderer.cpp +++ b/libraries/entities-renderer/src/EntityTreeRenderer.cpp @@ -675,7 +675,7 @@ void EntityTreeRenderer::mouseMoveEvent(QMouseEvent* event) { PickRay ray = _viewState->computePickRay(event->x(), event->y()); - bool precisionPicking = false; // for mouse moves we do not do precision picking + bool precisionPicking = true; // for mouse moves we do precision picking RayToEntityIntersectionResult rayPickResult = findRayIntersectionWorker(ray, Octree::TryLock, precisionPicking); if (rayPickResult.intersects) { diff --git a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp index 0b6271a6b1..911fdf4184 100644 --- a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp @@ -23,6 +23,7 @@ #include #include #include +#include #include "EntityTreeRenderer.h" #include "EntitiesRendererLogging.h" @@ -371,6 +372,21 @@ void RenderableModelEntityItem::render(RenderArgs* args) { _model->updateRenderItems(); } + // this simple logic should say we set showingEntityHighlight to true whenever we are in marketplace mode and we have a marketplace id, or + // whenever we are not set to none and shouldHighlight is true. + bool showingEntityHighlight = ((bool)(args->_outlineFlags & (int)RenderArgs::RENDER_OUTLINE_MARKETPLACE_MODE) && getMarketplaceID().length() != 0) || + (args->_outlineFlags != RenderArgs::RENDER_OUTLINE_NONE && getShouldHighlight()); + if (showingEntityHighlight) { + static glm::vec4 yellowColor(1.0f, 1.0f, 0.0f, 1.0f); + gpu::Batch& batch = *args->_batch; + bool success; + auto shapeTransform = getTransformToCenter(success); + if (success) { + batch.setModelTransform(shapeTransform); // we want to include the scale as well + DependencyManager::get()->renderWireCubeInstance(args, batch, yellowColor); + } + } + if (!hasModel() || (_model && _model->didVisualGeometryRequestFail())) { static glm::vec4 greenColor(0.0f, 1.0f, 0.0f, 1.0f); gpu::Batch& batch = *args->_batch; @@ -1282,3 +1298,11 @@ void RenderableModelEntityItem::mapJoints(const QStringList& modelJointNames) { } } } + +bool RenderableModelEntityItem::getMeshes(MeshProxyList& result) { + if (!_model || !_model->isLoaded()) { + return false; + } + BLOCKING_INVOKE_METHOD(_model.get(), "getMeshes", Q_RETURN_ARG(MeshProxyList, result)); + return !result.isEmpty(); +} diff --git a/libraries/entities-renderer/src/RenderableModelEntityItem.h b/libraries/entities-renderer/src/RenderableModelEntityItem.h index 2d240c01a6..a5ba7bedda 100644 --- a/libraries/entities-renderer/src/RenderableModelEntityItem.h +++ b/libraries/entities-renderer/src/RenderableModelEntityItem.h @@ -116,6 +116,8 @@ public: return _animation; } + bool getMeshes(MeshProxyList& result) override; + private: QVariantMap parseTexturesToMap(QString textures); void remapTextures(); diff --git a/libraries/entities-renderer/src/RenderableParticleEffectEntityItem.cpp b/libraries/entities-renderer/src/RenderableParticleEffectEntityItem.cpp index 10bd70be13..a3c0d9877e 100644 --- a/libraries/entities-renderer/src/RenderableParticleEffectEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableParticleEffectEntityItem.cpp @@ -13,14 +13,13 @@ #include #include #include +#include #include #include #include "EntitiesRendererLogging.h" #include "RenderableParticleEffectEntityItem.h" -#include "untextured_particle_vert.h" -#include "untextured_particle_frag.h" #include "textured_particle_vert.h" #include "textured_particle_frag.h" @@ -29,6 +28,16 @@ class ParticlePayloadData { public: static const size_t VERTEX_PER_PARTICLE = 4; + static uint8_t CUSTOM_PIPELINE_NUMBER; + static render::ShapePipelinePointer shapePipelineFactory(const render::ShapePlumber& plumber, const render::ShapeKey& key); + static void registerShapePipeline() { + if (!CUSTOM_PIPELINE_NUMBER) { + CUSTOM_PIPELINE_NUMBER = render::ShapePipeline::registerCustomShapePipelineFactory(shapePipelineFactory); + } + } + + static std::weak_ptr _texturedPipeline; + template struct InterpolationData { T start; @@ -70,9 +79,6 @@ public: offsetof(ParticlePrimitive, uv), gpu::Stream::PER_INSTANCE); } - void setPipeline(PipelinePointer pipeline) { _pipeline = pipeline; } - const PipelinePointer& getPipeline() const { return _pipeline; } - const Transform& getModelTransform() const { return _modelTransform; } void setModelTransform(const Transform& modelTransform) { _modelTransform = modelTransform; } @@ -90,15 +96,15 @@ public: bool getVisibleFlag() const { return _visibleFlag; } void setVisibleFlag(bool visibleFlag) { _visibleFlag = visibleFlag; } - + void render(RenderArgs* args) const { - assert(_pipeline); gpu::Batch& batch = *args->_batch; - batch.setPipeline(_pipeline); if (_texture) { batch.setResourceTexture(0, _texture); + } else { + batch.setResourceTexture(0, DependencyManager::get()->getWhiteTexture()); } batch.setModelTransform(_modelTransform); @@ -113,7 +119,6 @@ public: protected: Transform _modelTransform; AABox _bound; - PipelinePointer _pipeline; FormatPointer _vertexFormat { std::make_shared() }; BufferPointer _particleBuffer { std::make_shared() }; BufferView _uniformBuffer; @@ -142,23 +147,49 @@ namespace render { payload->render(args); } } + template <> + const ShapeKey shapeGetShapeKey(const ParticlePayloadData::Pointer& payload) { + return render::ShapeKey::Builder().withCustom(ParticlePayloadData::CUSTOM_PIPELINE_NUMBER).withTranslucent().build(); + } } +uint8_t ParticlePayloadData::CUSTOM_PIPELINE_NUMBER = 0; +std::weak_ptr ParticlePayloadData::_texturedPipeline; + +render::ShapePipelinePointer ParticlePayloadData::shapePipelineFactory(const render::ShapePlumber& plumber, const render::ShapeKey& key) { + auto texturedPipeline = _texturedPipeline.lock(); + if (!texturedPipeline) { + auto state = std::make_shared(); + state->setCullMode(gpu::State::CULL_BACK); + state->setDepthTest(true, false, gpu::LESS_EQUAL); + state->setBlendFunction(true, gpu::State::SRC_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::ONE, + gpu::State::FACTOR_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::ONE); + PrepareStencil::testMask(*state); + + auto vertShader = gpu::Shader::createVertex(std::string(textured_particle_vert)); + auto fragShader = gpu::Shader::createPixel(std::string(textured_particle_frag)); + + auto program = gpu::Shader::createProgram(vertShader, fragShader); + _texturedPipeline = texturedPipeline = gpu::Pipeline::create(program, state); + } + + return std::make_shared(texturedPipeline, nullptr, nullptr, nullptr); +} EntityItemPointer RenderableParticleEffectEntityItem::factory(const EntityItemID& entityID, const EntityItemProperties& properties) { auto entity = std::make_shared(entityID); entity->setProperties(properties); + + // As we create the first ParticuleSystem entity, let s register its special shapePIpeline factory: + ParticlePayloadData::registerShapePipeline(); + return entity; } RenderableParticleEffectEntityItem::RenderableParticleEffectEntityItem(const EntityItemID& entityItemID) : ParticleEffectEntityItem(entityItemID) { - // lazy creation of particle system pipeline - if (!_untexturedPipeline || !_texturedPipeline) { - createPipelines(); - } } bool RenderableParticleEffectEntityItem::addToScene(const EntityItemPointer& self, @@ -167,7 +198,6 @@ bool RenderableParticleEffectEntityItem::addToScene(const EntityItemPointer& sel _scene = scene; _renderItemId = _scene->allocateID(); auto particlePayloadData = std::make_shared(); - particlePayloadData->setPipeline(_untexturedPipeline); auto renderPayload = std::make_shared(particlePayloadData); render::Item::Status::Getters statusGetters; makeEntityItemStatusGetters(getThisPointer(), statusGetters); @@ -276,47 +306,14 @@ void RenderableParticleEffectEntityItem::updateRenderItem() { if (_texture && _texture->isLoaded()) { payload.setTexture(_texture->getGPUTexture()); - payload.setPipeline(_texturedPipeline); } else { payload.setTexture(nullptr); - payload.setPipeline(_untexturedPipeline); } }); _scene->enqueueTransaction(transaction); } -void RenderableParticleEffectEntityItem::createPipelines() { - if (!_untexturedPipeline) { - auto state = std::make_shared(); - state->setCullMode(gpu::State::CULL_BACK); - state->setDepthTest(true, false, gpu::LESS_EQUAL); - state->setBlendFunction(true, gpu::State::SRC_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::ONE, - gpu::State::FACTOR_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::ONE); - PrepareStencil::testMask(*state); - - auto vertShader = gpu::Shader::createVertex(std::string(untextured_particle_vert)); - auto fragShader = gpu::Shader::createPixel(std::string(untextured_particle_frag)); - - auto program = gpu::Shader::createProgram(vertShader, fragShader); - _untexturedPipeline = gpu::Pipeline::create(program, state); - } - if (!_texturedPipeline) { - auto state = std::make_shared(); - state->setCullMode(gpu::State::CULL_BACK); - state->setDepthTest(true, false, gpu::LESS_EQUAL); - state->setBlendFunction(true, gpu::State::SRC_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::ONE, - gpu::State::FACTOR_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::ONE); - PrepareStencil::testMask(*state); - - auto vertShader = gpu::Shader::createVertex(std::string(textured_particle_vert)); - auto fragShader = gpu::Shader::createPixel(std::string(textured_particle_frag)); - - auto program = gpu::Shader::createProgram(vertShader, fragShader); - _texturedPipeline = gpu::Pipeline::create(program, state); - } -} - void RenderableParticleEffectEntityItem::notifyBoundChanged() { if (!render::Item::isValidID(_renderItemId)) { return; diff --git a/libraries/entities-renderer/src/RenderableParticleEffectEntityItem.h b/libraries/entities-renderer/src/RenderableParticleEffectEntityItem.h index 678b582b41..b0d7e1c920 100644 --- a/libraries/entities-renderer/src/RenderableParticleEffectEntityItem.h +++ b/libraries/entities-renderer/src/RenderableParticleEffectEntityItem.h @@ -34,16 +34,13 @@ protected: virtual void locationChanged(bool tellPhysics = true) override { EntityItem::locationChanged(tellPhysics); notifyBoundChanged(); } virtual void dimensionsChanged() override { EntityItem::dimensionsChanged(); notifyBoundChanged(); } - void notifyBoundChanged(); + void notifyBoundChanged(); - void createPipelines(); - render::ScenePointer _scene; render::ItemID _renderItemId{ render::Item::INVALID_ITEM_ID }; NetworkTexturePointer _texture; - gpu::PipelinePointer _untexturedPipeline; - gpu::PipelinePointer _texturedPipeline; + }; diff --git a/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.cpp b/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.cpp index 88a5d2b873..6687d4e721 100644 --- a/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.cpp @@ -59,12 +59,8 @@ #include "EntityEditPacketSender.h" #include "PhysicalEntitySimulation.h" -gpu::PipelinePointer RenderablePolyVoxEntityItem::_pipeline = nullptr; -gpu::PipelinePointer RenderablePolyVoxEntityItem::_wireframePipeline = nullptr; - const float MARCHING_CUBE_COLLISION_HULL_OFFSET = 0.5; - /* A PolyVoxEntity has several interdependent parts: @@ -116,6 +112,10 @@ EntityItemPointer RenderablePolyVoxEntityItem::factory(const EntityItemID& entit EntityItemPointer entity{ new RenderablePolyVoxEntityItem(entityID) }; entity->setProperties(properties); std::static_pointer_cast(entity)->initializePolyVox(); + + // As we create the first Polyvox entity, let's register its special shapePipeline factory: + PolyVoxPayload::registerShapePipeline(); + return entity; } @@ -732,35 +732,6 @@ 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)); - - gpu::Shader::BindingSet slotBindings; - slotBindings.insert(gpu::Shader::Binding(std::string("materialBuffer"), MATERIAL_GPU_SLOT)); - slotBindings.insert(gpu::Shader::Binding(std::string("xMap"), 0)); - slotBindings.insert(gpu::Shader::Binding(std::string("yMap"), 1)); - slotBindings.insert(gpu::Shader::Binding(std::string("zMap"), 2)); - - gpu::ShaderPointer program = gpu::Shader::createProgram(vertexShader, pixelShader); - gpu::Shader::makeProgram(*program, slotBindings); - - auto state = std::make_shared(); - state->setCullMode(gpu::State::CULL_BACK); - state->setDepthTest(true, true, gpu::LESS_EQUAL); - PrepareStencil::testMaskDrawShape(*state); - - _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); - PrepareStencil::testMaskDrawShape(*wireframeState); - - _wireframePipeline = gpu::Pipeline::create(program, wireframeState); - } if (!_vertexFormat) { auto vf = std::make_shared(); @@ -771,11 +742,6 @@ void RenderablePolyVoxEntityItem::render(RenderArgs* args) { gpu::Batch& batch = *args->_batch; - // Pick correct Pipeline - bool wireframe = (render::ShapeKey(args->_globalShapeKey).isWireframe()); - auto pipeline = (wireframe ? _wireframePipeline : _pipeline); - batch.setPipeline(pipeline); - Transform transform(voxelToWorldMatrix()); batch.setModelTransform(transform); batch.setInputFormat(_vertexFormat); @@ -817,7 +783,7 @@ void RenderablePolyVoxEntityItem::render(RenderArgs* args) { batch.setResourceTexture(2, DependencyManager::get()->getWhiteTexture()); } - int voxelVolumeSizeLocation = pipeline->getProgram()->getUniforms().findLocation("voxelVolumeSize"); + int voxelVolumeSizeLocation = args->_shapePipeline->pipeline->getProgram()->getUniforms().findLocation("voxelVolumeSize"); batch._glUniform3f(voxelVolumeSizeLocation, voxelVolumeSize.x, voxelVolumeSize.y, voxelVolumeSize.z); batch.drawIndexed(gpu::TRIANGLES, (gpu::uint32)mesh->getNumIndices(), 0); @@ -848,6 +814,48 @@ void RenderablePolyVoxEntityItem::removeFromScene(const EntityItemPointer& self, render::Item::clearID(_myItem); } +uint8_t PolyVoxPayload::CUSTOM_PIPELINE_NUMBER = 0; + +std::shared_ptr PolyVoxPayload::_pipeline; +std::shared_ptr PolyVoxPayload::_wireframePipeline; + +render::ShapePipelinePointer PolyVoxPayload::shapePipelineFactory(const render::ShapePlumber& plumber, const render::ShapeKey& key) { + if (!_pipeline) { + gpu::ShaderPointer vertexShader = gpu::Shader::createVertex(std::string(polyvox_vert)); + gpu::ShaderPointer pixelShader = gpu::Shader::createPixel(std::string(polyvox_frag)); + + gpu::Shader::BindingSet slotBindings; + slotBindings.insert(gpu::Shader::Binding(std::string("materialBuffer"), PolyVoxPayload::MATERIAL_GPU_SLOT)); + slotBindings.insert(gpu::Shader::Binding(std::string("xMap"), 0)); + slotBindings.insert(gpu::Shader::Binding(std::string("yMap"), 1)); + slotBindings.insert(gpu::Shader::Binding(std::string("zMap"), 2)); + + gpu::ShaderPointer program = gpu::Shader::createProgram(vertexShader, pixelShader); + gpu::Shader::makeProgram(*program, slotBindings); + + auto state = std::make_shared(); + state->setCullMode(gpu::State::CULL_BACK); + state->setDepthTest(true, true, gpu::LESS_EQUAL); + PrepareStencil::testMaskDrawShape(*state); + + _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); + PrepareStencil::testMaskDrawShape(*wireframeState); + + _wireframePipeline = gpu::Pipeline::create(program, wireframeState); + } + + if (key.isWireframe()) { + return std::make_shared(_wireframePipeline, nullptr, nullptr, nullptr); + } else { + return std::make_shared(_pipeline, nullptr, nullptr, nullptr); + } +} + namespace render { template <> const ItemKey payloadGetKey(const PolyVoxPayload::Pointer& payload) { return ItemKey::Builder::opaqueShape(); @@ -871,6 +879,10 @@ namespace render { payload->_owner->getRenderableInterface()->render(args); } } + + template <> const ShapeKey shapeGetShapeKey(const PolyVoxPayload::Pointer& payload) { + return ShapeKey::Builder().withCustom(PolyVoxPayload::CUSTOM_PIPELINE_NUMBER).build(); + } } @@ -1619,7 +1631,7 @@ void RenderablePolyVoxEntityItem::bonkNeighbors() { void RenderablePolyVoxEntityItem::locationChanged(bool tellPhysics) { EntityItem::locationChanged(tellPhysics); - if (!_pipeline || !render::Item::isValidID(_myItem)) { + if (!render::Item::isValidID(_myItem)) { return; } render::ScenePointer scene = AbstractViewStateInterface::instance()->getMain3DScene(); @@ -1651,6 +1663,7 @@ bool RenderablePolyVoxEntityItem::getMeshes(MeshProxyList& result) { // the mesh will be in voxel-space. transform it into object-space meshProxy = new SimpleMeshProxy( _mesh->map([=](glm::vec3 position){ return glm::vec3(transform * glm::vec4(position, 1.0f)); }, + [=](glm::vec3 color){ return color; }, [=](glm::vec3 normal){ return glm::normalize(glm::vec3(transform * glm::vec4(normal, 0.0f))); }, [&](uint32_t index){ return index; })); result << meshProxy; diff --git a/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.h b/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.h index 45625ada6d..8f20a7a298 100644 --- a/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.h +++ b/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.h @@ -28,6 +28,19 @@ class PolyVoxPayload { public: + + static uint8_t CUSTOM_PIPELINE_NUMBER; + static render::ShapePipelinePointer shapePipelineFactory(const render::ShapePlumber& plumber, const render::ShapeKey& key); + static void registerShapePipeline() { + if (!CUSTOM_PIPELINE_NUMBER) { + CUSTOM_PIPELINE_NUMBER = render::ShapePipeline::registerCustomShapePipelineFactory(shapePipelineFactory); + } + } + + static const int MATERIAL_GPU_SLOT = 3; + static std::shared_ptr _pipeline; + static std::shared_ptr _wireframePipeline; + PolyVoxPayload(EntityItemPointer owner) : _owner(owner), _bounds(AABox()) { } typedef render::Payload Payload; typedef Payload::DataPointer Pointer; @@ -40,6 +53,7 @@ namespace render { template <> const ItemKey payloadGetKey(const PolyVoxPayload::Pointer& payload); template <> const Item::Bound payloadGetBound(const PolyVoxPayload::Pointer& payload); template <> void payloadRender(const PolyVoxPayload::Pointer& payload, RenderArgs* args); + template <> const ShapeKey shapeGetShapeKey(const PolyVoxPayload::Pointer& payload); } @@ -168,10 +182,7 @@ private: NetworkTexturePointer _yTexture; NetworkTexturePointer _zTexture; - const int MATERIAL_GPU_SLOT = 3; render::ItemID _myItem{ render::Item::INVALID_ITEM_ID }; - static gpu::PipelinePointer _pipeline; - static gpu::PipelinePointer _wireframePipeline; ShapeInfo _shapeInfo; diff --git a/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp b/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp index 62ab3377a8..3f9497741f 100644 --- a/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp @@ -82,6 +82,80 @@ bool RenderableShapeEntityItem::isTransparent() { } } +void RenderableShapeEntityItem::computeShapeInfo(ShapeInfo& info) { + + // This will be called whenever DIRTY_SHAPE flag (set by dimension change, etc) + // is set. + + const glm::vec3 entityDimensions = getDimensions(); + + switch (_shape){ + case entity::Shape::Quad: + case entity::Shape::Cube: { + _collisionShapeType = SHAPE_TYPE_BOX; + } + break; + case entity::Shape::Sphere: { + + float diameter = entityDimensions.x; + const float MIN_DIAMETER = 0.001f; + const float MIN_RELATIVE_SPHERICAL_ERROR = 0.001f; + if (diameter > MIN_DIAMETER + && fabsf(diameter - entityDimensions.y) / diameter < MIN_RELATIVE_SPHERICAL_ERROR + && fabsf(diameter - entityDimensions.z) / diameter < MIN_RELATIVE_SPHERICAL_ERROR) { + + _collisionShapeType = SHAPE_TYPE_SPHERE; + } + else { + _collisionShapeType = SHAPE_TYPE_ELLIPSOID; + } + } + break; + case entity::Shape::Cylinder: { + _collisionShapeType = SHAPE_TYPE_CYLINDER_Y; + // TODO WL21389: determine if rotation is axis-aligned + //const Transform::Quat & rot = _transform.getRotation(); + + // TODO WL21389: some way to tell apart SHAPE_TYPE_CYLINDER_Y, _X, _Z based on rotation and + // hull ( or dimensions, need circular cross section) + // Should allow for minor variance along axes? + + } + break; + case entity::Shape::Triangle: + case entity::Shape::Hexagon: + case entity::Shape::Octagon: + case entity::Shape::Circle: + case entity::Shape::Tetrahedron: + case entity::Shape::Octahedron: + case entity::Shape::Dodecahedron: + case entity::Shape::Icosahedron: + case entity::Shape::Cone: { + //TODO WL21389: SHAPE_TYPE_SIMPLE_HULL and pointCollection (later) + _collisionShapeType = SHAPE_TYPE_ELLIPSOID; + } + break; + case entity::Shape::Torus: + { + // Not in GeometryCache::buildShapes, unsupported. + _collisionShapeType = SHAPE_TYPE_ELLIPSOID; + //TODO WL21389: SHAPE_TYPE_SIMPLE_HULL and pointCollection (later if desired support) + } + break; + default:{ + _collisionShapeType = SHAPE_TYPE_ELLIPSOID; + } + break; + } + + EntityItem::computeShapeInfo(info); +} + +// This value specifes how the shape should be treated by physics calculations. +ShapeType RenderableShapeEntityItem::getShapeType() const { + return _collisionShapeType; +} + void RenderableShapeEntityItem::render(RenderArgs* args) { PerformanceTimer perfTimer("RenderableShapeEntityItem::render"); //Q_ASSERT(getType() == EntityTypes::Shape); diff --git a/libraries/entities-renderer/src/RenderableShapeEntityItem.h b/libraries/entities-renderer/src/RenderableShapeEntityItem.h index 0cc6a54f81..d603cdedef 100644 --- a/libraries/entities-renderer/src/RenderableShapeEntityItem.h +++ b/libraries/entities-renderer/src/RenderableShapeEntityItem.h @@ -28,9 +28,18 @@ public: bool isTransparent() override; + virtual void computeShapeInfo(ShapeInfo& info) override; + ShapeType getShapeType() const override; + + private: std::unique_ptr _procedural { nullptr }; + //! This is SHAPE_TYPE_ELLIPSOID rather than SHAPE_TYPE_NONE to maintain + //! prior functionality where new or unsupported shapes are treated as + //! ellipsoids. + ShapeType _collisionShapeType{ ShapeType::SHAPE_TYPE_ELLIPSOID }; + SIMPLE_RENDERABLE(); }; diff --git a/libraries/entities-renderer/src/RenderableWebEntityItem.cpp b/libraries/entities-renderer/src/RenderableWebEntityItem.cpp index 9a898b4071..ba8e0c18e7 100644 --- a/libraries/entities-renderer/src/RenderableWebEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableWebEntityItem.cpp @@ -69,7 +69,7 @@ RenderableWebEntityItem::~RenderableWebEntityItem() { } } -bool RenderableWebEntityItem::buildWebSurface(QSharedPointer renderer) { +bool RenderableWebEntityItem::buildWebSurface() { if (_currentWebCount >= MAX_CONCURRENT_WEB_VIEWS) { qWarning() << "Too many concurrent web views to create new view"; return false; @@ -132,6 +132,8 @@ bool RenderableWebEntityItem::buildWebSurface(QSharedPointer handlePointerEvent(event); } }; + + auto renderer = DependencyManager::get(); _mousePressConnection = QObject::connect(renderer.data(), &EntityTreeRenderer::mousePressOnEntity, forwardPointerEvent); _mouseReleaseConnection = QObject::connect(renderer.data(), &EntityTreeRenderer::mouseReleaseOnEntity, forwardPointerEvent); _mouseMoveConnection = QObject::connect(renderer.data(), &EntityTreeRenderer::mouseMoveOnEntity, forwardPointerEvent); @@ -185,8 +187,7 @@ void RenderableWebEntityItem::render(RenderArgs* args) { #endif if (!_webSurface) { - auto renderer = qSharedPointerCast(args->_renderData); - if (!buildWebSurface(renderer)) { + if (!buildWebSurface()) { return; } _fadeStartTime = usecTimestampNow(); diff --git a/libraries/entities-renderer/src/RenderableWebEntityItem.h b/libraries/entities-renderer/src/RenderableWebEntityItem.h index 0f5d307766..a2318081b6 100644 --- a/libraries/entities-renderer/src/RenderableWebEntityItem.h +++ b/libraries/entities-renderer/src/RenderableWebEntityItem.h @@ -57,7 +57,7 @@ public: virtual QObject* getRootItem() override; private: - bool buildWebSurface(QSharedPointer renderer); + bool buildWebSurface(); void destroyWebSurface(); glm::vec2 getWindowSize() const; diff --git a/libraries/entities-renderer/src/untextured_particle.slf b/libraries/entities-renderer/src/untextured_particle.slf deleted file mode 100644 index 11f25bb693..0000000000 --- a/libraries/entities-renderer/src/untextured_particle.slf +++ /dev/null @@ -1,18 +0,0 @@ -<@include gpu/Config.slh@> -<$VERSION_HEADER$> -// Generated on <$_SCRIBE_DATE$> -// fragment shader -// -// 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 -// - -in vec4 _color; - -out vec4 outFragColor; - -void main(void) { - outFragColor = _color; -} diff --git a/libraries/entities-renderer/src/untextured_particle.slv b/libraries/entities-renderer/src/untextured_particle.slv deleted file mode 100644 index 85f9d438bf..0000000000 --- a/libraries/entities-renderer/src/untextured_particle.slv +++ /dev/null @@ -1,25 +0,0 @@ -<@include gpu/Config.slh@> -<$VERSION_HEADER$> -// Generated on <$_SCRIBE_DATE$> -// -// particle vertex shader -// -// 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/Inputs.slh@> -<@include gpu/Color.slh@> -<@include gpu/Transform.slh@> -<$declareStandardTransform()$> - -out vec4 _color; - -void main(void) { - // pass along the color - _color = colorToLinearRGBA(inColor); - - TransformCamera cam = getTransformCamera(); - TransformObject obj = getTransformObject(); - <$transformModelToClipPos(cam, obj, inPosition, gl_Position)$> -} \ No newline at end of file diff --git a/libraries/entities/src/AddEntityOperator.cpp b/libraries/entities/src/AddEntityOperator.cpp index e86e70dd80..78d986f538 100644 --- a/libraries/entities/src/AddEntityOperator.cpp +++ b/libraries/entities/src/AddEntityOperator.cpp @@ -46,10 +46,8 @@ bool AddEntityOperator::preRecursion(const OctreeElementPointer& element) { // If this element is the best fit for the new entity properties, then add/or update it if (entityTreeElement->bestFitBounds(_newEntityBox)) { - + _tree->addEntityMapEntry(_newEntity); entityTreeElement->addEntityItem(_newEntity); - _tree->setContainingElement(_newEntity->getEntityItemID(), entityTreeElement); - _foundNew = true; keepSearching = false; } else { diff --git a/libraries/entities/src/DeleteEntityOperator.cpp b/libraries/entities/src/DeleteEntityOperator.cpp index 709c281341..cbd0ad7839 100644 --- a/libraries/entities/src/DeleteEntityOperator.cpp +++ b/libraries/entities/src/DeleteEntityOperator.cpp @@ -96,7 +96,7 @@ bool DeleteEntityOperator::preRecursion(const OctreeElementPointer& element) { bool entityDeleted = entityTreeElement->removeEntityItem(theEntity); // remove it from the element assert(entityDeleted); (void)entityDeleted; // quite warning - _tree->setContainingElement(details.entity->getEntityItemID(), NULL); // update or id to element lookup + _tree->clearEntityMapEntry(details.entity->getEntityItemID()); _foundCount++; } } diff --git a/libraries/entities/src/EntityEditPacketSender.cpp b/libraries/entities/src/EntityEditPacketSender.cpp index 7845b0d5e3..ee0fcf8218 100644 --- a/libraries/entities/src/EntityEditPacketSender.cpp +++ b/libraries/entities/src/EntityEditPacketSender.cpp @@ -54,6 +54,7 @@ void EntityEditPacketSender::queueEditAvatarEntityMessage(PacketType type, EntityItemProperties entityProperties = entity->getProperties(); entityProperties.merge(properties); + std::lock_guard lock(_mutex); QScriptValue scriptProperties = EntityItemNonDefaultPropertiesToScriptValue(&_scriptEngine, entityProperties); QVariant variantProperties = scriptProperties.toVariant(); QJsonDocument jsonProperties = QJsonDocument::fromVariant(variantProperties); diff --git a/libraries/entities/src/EntityEditPacketSender.h b/libraries/entities/src/EntityEditPacketSender.h index 9190a8296a..81efaa865c 100644 --- a/libraries/entities/src/EntityEditPacketSender.h +++ b/libraries/entities/src/EntityEditPacketSender.h @@ -14,6 +14,8 @@ #include +#include + #include "EntityItem.h" #include "AvatarData.h" @@ -49,6 +51,7 @@ private: EntityItemID entityItemID, const EntityItemProperties& properties); private: + std::mutex _mutex; AvatarData* _myAvatar { nullptr }; QScriptEngine _scriptEngine; }; diff --git a/libraries/entities/src/EntityItem.cpp b/libraries/entities/src/EntityItem.cpp index 23ce097cc2..c4136a0430 100644 --- a/libraries/entities/src/EntityItem.cpp +++ b/libraries/entities/src/EntityItem.cpp @@ -89,7 +89,8 @@ EntityItem::EntityItem(const EntityItemID& entityItemID) : EntityItem::~EntityItem() { // clear out any left-over actions - EntityTreePointer entityTree = _element ? _element->getTree() : nullptr; + EntityTreeElementPointer element = _element; // use local copy of _element for logic below + EntityTreePointer entityTree = element ? element->getTree() : nullptr; EntitySimulationPointer simulation = entityTree ? entityTree->getSimulation() : nullptr; if (simulation) { clearActions(simulation); @@ -132,6 +133,7 @@ EntityPropertyFlags EntityItem::getEntityProperties(EncodeBitstreamParams& param requestedProperties += PROP_LOCKED; requestedProperties += PROP_USER_DATA; requestedProperties += PROP_MARKETPLACE_ID; + requestedProperties += PROP_SHOULD_HIGHLIGHT; requestedProperties += PROP_NAME; requestedProperties += PROP_HREF; requestedProperties += PROP_DESCRIPTION; @@ -181,7 +183,6 @@ OctreeElement::AppendState EntityItem::appendEntityData(OctreePacketData* packet EntityPropertyFlags propertyFlags(PROP_LAST_ITEM); EntityPropertyFlags requestedProperties = getEntityProperties(params); - EntityPropertyFlags propertiesDidntFit = requestedProperties; // If we are being called for a subsequent pass at appendEntityData() that failed to completely encode this item, // then our entityTreeElementExtraEncodeData should include data about which properties we need to append. @@ -189,6 +190,8 @@ OctreeElement::AppendState EntityItem::appendEntityData(OctreePacketData* packet requestedProperties = entityTreeElementExtraEncodeData->entities.value(getEntityItemID()); } + EntityPropertyFlags propertiesDidntFit = requestedProperties; + LevelDetails entityLevel = packetData->startLevel(); quint64 lastEdited = getLastEdited(); @@ -276,6 +279,7 @@ OctreeElement::AppendState EntityItem::appendEntityData(OctreePacketData* packet APPEND_ENTITY_PROPERTY(PROP_LOCKED, getLocked()); APPEND_ENTITY_PROPERTY(PROP_USER_DATA, getUserData()); APPEND_ENTITY_PROPERTY(PROP_MARKETPLACE_ID, getMarketplaceID()); + APPEND_ENTITY_PROPERTY(PROP_SHOULD_HIGHLIGHT, getShouldHighlight()); APPEND_ENTITY_PROPERTY(PROP_NAME, getName()); APPEND_ENTITY_PROPERTY(PROP_COLLISION_SOUND_URL, getCollisionSoundURL()); APPEND_ENTITY_PROPERTY(PROP_HREF, getHref()); @@ -827,6 +831,10 @@ int EntityItem::readEntityDataFromBuffer(const unsigned char* data, int bytesLef READ_ENTITY_PROPERTY(PROP_MARKETPLACE_ID, QString, setMarketplaceID); } + if (args.bitstreamVersion >= VERSION_ENTITIES_HAS_SHOULD_HIGHLIGHT) { + READ_ENTITY_PROPERTY(PROP_SHOULD_HIGHLIGHT, bool, setShouldHighlight); + } + READ_ENTITY_PROPERTY(PROP_NAME, QString, setName); READ_ENTITY_PROPERTY(PROP_COLLISION_SOUND_URL, QString, setCollisionSoundURL); READ_ENTITY_PROPERTY(PROP_HREF, QString, setHref); @@ -880,8 +888,9 @@ int EntityItem::readEntityDataFromBuffer(const unsigned char* data, int bytesLef // Tracking for editing roundtrips here. We will tell our EntityTree that we just got incoming data about // and entity that was edited at some time in the past. The tree will determine how it wants to track this // information. - if (_element && _element->getTree()) { - _element->getTree()->trackIncomingEntityLastEdited(lastEditedFromBufferAdjusted, bytesRead); + EntityTreeElementPointer element = _element; // use local copy of _element for logic below + if (element && element->getTree()) { + element->getTree()->trackIncomingEntityLastEdited(lastEditedFromBufferAdjusted, bytesRead); } @@ -1352,8 +1361,7 @@ bool EntityItem::setProperties(const EntityItemProperties& properties) { SET_ENTITY_PROPERTY_FROM_PROPERTIES(lastEditedBy, setLastEditedBy); AACube saveQueryAACube = _queryAACube; - checkAndAdjustQueryAACube(); - if (saveQueryAACube != _queryAACube) { + if (checkAndMaybeUpdateQueryAACube() && saveQueryAACube != _queryAACube) { somethingChanged = true; } @@ -1421,26 +1429,20 @@ void EntityItem::setDimensions(const glm::vec3& value) { /// AACube EntityItem::getMaximumAACube(bool& success) const { if (_recalcMaxAACube) { - // * we know that the position is the center of rotation glm::vec3 centerOfRotation = getPosition(success); // also where _registration point is if (success) { _recalcMaxAACube = false; - // * we know that the registration point is the center of rotation - // * we can calculate the length of the furthest extent from the registration point - // as the dimensions * max (registrationPoint, (1.0,1.0,1.0) - registrationPoint) - glm::vec3 dimensions = getDimensions(); - glm::vec3 registrationPoint = (dimensions * _registrationPoint); - glm::vec3 registrationRemainder = (dimensions * (glm::vec3(1.0f, 1.0f, 1.0f) - _registrationPoint)); - glm::vec3 furthestExtentFromRegistration = glm::max(registrationPoint, registrationRemainder); + // we want to compute the furthestExtent that an entity can extend out from its "position" + // to do this we compute the max of these two vec3s: registration and 1-registration + // and then scale by dimensions + glm::vec3 maxExtents = getDimensions() * glm::max(_registrationPoint, glm::vec3(1.0f) - _registrationPoint); - // * we know that if you rotate in any direction you would create a sphere - // that has a radius of the length of furthest extent from registration point - float radius = glm::length(furthestExtentFromRegistration); + // there exists a sphere that contains maxExtents for all rotations + float radius = glm::length(maxExtents); - // * we know that the minimum bounding cube of this maximum possible sphere is - // (center - radius) to (center + radius) + // put a cube around the sphere + // TODO? replace _maxAACube with _boundingSphereRadius glm::vec3 minimumCorner = centerOfRotation - glm::vec3(radius, radius, radius); - _maxAACube = AACube(minimumCorner, radius * 2.0f); } } else { @@ -1632,6 +1634,8 @@ void EntityItem::updateDimensions(const glm::vec3& value) { if (getDimensions() != value) { setDimensions(value); markDirtyFlags(Simulation::DIRTY_SHAPE | Simulation::DIRTY_MASS); + _queryAACubeSet = false; + dimensionsChanged(); } } @@ -2056,7 +2060,8 @@ bool EntityItem::removeActionInternal(const QUuid& actionID, EntitySimulationPoi _previouslyDeletedActions.insert(actionID, usecTimestampNow()); if (_objectActions.contains(actionID)) { if (!simulation) { - EntityTreePointer entityTree = _element ? _element->getTree() : nullptr; + EntityTreeElementPointer element = _element; // use local copy of _element for logic below + EntityTreePointer entityTree = element ? element->getTree() : nullptr; simulation = entityTree ? entityTree->getSimulation() : nullptr; } @@ -2804,6 +2809,20 @@ void EntityItem::setMarketplaceID(const QString& value) { }); } +bool EntityItem::getShouldHighlight() const { + bool result; + withReadLock([&] { + result = _shouldHighlight; + }); + return result; +} + +void EntityItem::setShouldHighlight(const bool value) { + withWriteLock([&] { + _shouldHighlight = value; + }); +} + uint32_t EntityItem::getDirtyFlags() const { uint32_t result; withReadLock([&] { diff --git a/libraries/entities/src/EntityItem.h b/libraries/entities/src/EntityItem.h index 92c83651aa..36ac6ba1cc 100644 --- a/libraries/entities/src/EntityItem.h +++ b/libraries/entities/src/EntityItem.h @@ -316,6 +316,9 @@ public: QString getMarketplaceID() const; void setMarketplaceID(const QString& value); + bool getShouldHighlight() const; + void setShouldHighlight(const bool value); + // TODO: get rid of users of getRadius()... float getRadius() const; @@ -532,6 +535,7 @@ protected: QString _userData; SimulationOwner _simulationOwner; QString _marketplaceID; + bool _shouldHighlight { false }; QString _name; QString _href; //Hyperlink href QString _description; //Hyperlink description diff --git a/libraries/entities/src/EntityItemProperties.cpp b/libraries/entities/src/EntityItemProperties.cpp index a207902789..1bd75f78d4 100644 --- a/libraries/entities/src/EntityItemProperties.cpp +++ b/libraries/entities/src/EntityItemProperties.cpp @@ -289,6 +289,7 @@ EntityPropertyFlags EntityItemProperties::getChangedProperties() const { CHECK_PROPERTY_CHANGE(PROP_RADIUS_START, radiusStart); CHECK_PROPERTY_CHANGE(PROP_RADIUS_FINISH, radiusFinish); CHECK_PROPERTY_CHANGE(PROP_MARKETPLACE_ID, marketplaceID); + CHECK_PROPERTY_CHANGE(PROP_SHOULD_HIGHLIGHT, shouldHighlight); CHECK_PROPERTY_CHANGE(PROP_NAME, name); CHECK_PROPERTY_CHANGE(PROP_BACKGROUND_MODE, backgroundMode); CHECK_PROPERTY_CHANGE(PROP_SOURCE_URL, sourceUrl); @@ -406,6 +407,7 @@ QScriptValue EntityItemProperties::copyToScriptValue(QScriptEngine* engine, bool COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_LOCKED, locked); COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_USER_DATA, userData); COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_MARKETPLACE_ID, marketplaceID); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_SHOULD_HIGHLIGHT, shouldHighlight); COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_NAME, name); COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_COLLISION_SOUND_URL, collisionSoundURL); @@ -982,6 +984,7 @@ void EntityItemProperties::entityPropertyFlagsFromScriptValue(const QScriptValue ADD_PROPERTY_TO_MAP(PROP_RADIUS_START, RadiusStart, radiusStart, float); ADD_PROPERTY_TO_MAP(PROP_RADIUS_FINISH, RadiusFinish, radiusFinish, float); ADD_PROPERTY_TO_MAP(PROP_MARKETPLACE_ID, MarketplaceID, marketplaceID, QString); + ADD_PROPERTY_TO_MAP(PROP_SHOULD_HIGHLIGHT, ShouldHighlight, shouldHighlight, bool); ADD_PROPERTY_TO_MAP(PROP_KEYLIGHT_COLOR, KeyLightColor, keyLightColor, xColor); ADD_PROPERTY_TO_MAP(PROP_KEYLIGHT_INTENSITY, KeyLightIntensity, keyLightIntensity, float); ADD_PROPERTY_TO_MAP(PROP_KEYLIGHT_AMBIENT_INTENSITY, KeyLightAmbientIntensity, keyLightAmbientIntensity, float); @@ -1334,6 +1337,7 @@ bool EntityItemProperties::encodeEntityEditPacket(PacketType command, EntityItem APPEND_ENTITY_PROPERTY(PROP_SHAPE, properties.getShape()); } APPEND_ENTITY_PROPERTY(PROP_MARKETPLACE_ID, properties.getMarketplaceID()); + APPEND_ENTITY_PROPERTY(PROP_SHOULD_HIGHLIGHT, properties.getShouldHighlight()); APPEND_ENTITY_PROPERTY(PROP_NAME, properties.getName()); APPEND_ENTITY_PROPERTY(PROP_COLLISION_SOUND_URL, properties.getCollisionSoundURL()); APPEND_ENTITY_PROPERTY(PROP_ACTION_DATA, properties.getActionData()); @@ -1632,6 +1636,7 @@ bool EntityItemProperties::decodeEntityEditPacket(const unsigned char* data, int } READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_MARKETPLACE_ID, QString, setMarketplaceID); + READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_SHOULD_HIGHLIGHT, bool, setShouldHighlight); READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_NAME, QString, setName); READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_COLLISION_SOUND_URL, QString, setCollisionSoundURL); READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_ACTION_DATA, QByteArray, setActionData); @@ -1746,6 +1751,7 @@ void EntityItemProperties::markAllChanged() { //_alphaFinishChanged = true; _marketplaceIDChanged = true; + _shouldHighlightChanged = true; _keyLight.markAllChanged(); @@ -2157,12 +2163,17 @@ QList EntityItemProperties::listChangedProperties() { return out; } -bool EntityItemProperties::parentDependentPropertyChanged() const { - return localPositionChanged() || positionChanged() || - localRotationChanged() || rotationChanged() || - localVelocityChanged() || localAngularVelocityChanged(); +bool EntityItemProperties::transformChanged() const { + return positionChanged() || rotationChanged() || + localPositionChanged() || localRotationChanged(); } bool EntityItemProperties::parentRelatedPropertyChanged() const { - return parentDependentPropertyChanged() || parentIDChanged() || parentJointIndexChanged(); + return positionChanged() || rotationChanged() || + localPositionChanged() || localRotationChanged() || + parentIDChanged() || parentJointIndexChanged(); +} + +bool EntityItemProperties::queryAACubeRelatedPropertyChanged() const { + return parentRelatedPropertyChanged() || dimensionsChanged(); } diff --git a/libraries/entities/src/EntityItemProperties.h b/libraries/entities/src/EntityItemProperties.h index b526ac663c..8c636addb5 100644 --- a/libraries/entities/src/EntityItemProperties.h +++ b/libraries/entities/src/EntityItemProperties.h @@ -86,8 +86,9 @@ public: EntityPropertyFlags getChangedProperties() const; - bool parentDependentPropertyChanged() const; // was there a changed in a property that requires parent info to interpret? - bool parentRelatedPropertyChanged() const; // parentDependentPropertyChanged or parentID or parentJointIndex + bool transformChanged() const; + bool parentRelatedPropertyChanged() const; + bool queryAACubeRelatedPropertyChanged() const; AABox getAABox() const; @@ -170,6 +171,7 @@ public: DEFINE_PROPERTY(PROP_RADIUS_FINISH, RadiusFinish, radiusFinish, float, ParticleEffectEntityItem::DEFAULT_RADIUS_FINISH); DEFINE_PROPERTY(PROP_EMITTER_SHOULD_TRAIL, EmitterShouldTrail, emitterShouldTrail, bool, ParticleEffectEntityItem::DEFAULT_EMITTER_SHOULD_TRAIL); DEFINE_PROPERTY_REF(PROP_MARKETPLACE_ID, MarketplaceID, marketplaceID, QString, ENTITY_ITEM_DEFAULT_MARKETPLACE_ID); + DEFINE_PROPERTY_REF(PROP_SHOULD_HIGHLIGHT, ShouldHighlight, shouldHighlight, bool, ENTITY_ITEM_DEFAULT_SHOULD_HIGHLIGHT); DEFINE_PROPERTY_GROUP(KeyLight, keyLight, KeyLightPropertyGroup); DEFINE_PROPERTY_REF(PROP_VOXEL_VOLUME_SIZE, VoxelVolumeSize, voxelVolumeSize, glm::vec3, PolyVoxEntityItem::DEFAULT_VOXEL_VOLUME_SIZE); DEFINE_PROPERTY_REF(PROP_VOXEL_DATA, VoxelData, voxelData, QByteArray, PolyVoxEntityItem::DEFAULT_VOXEL_DATA); diff --git a/libraries/entities/src/EntityItemPropertiesDefaults.h b/libraries/entities/src/EntityItemPropertiesDefaults.h index d52c5d9aab..43d0e33ba6 100644 --- a/libraries/entities/src/EntityItemPropertiesDefaults.h +++ b/libraries/entities/src/EntityItemPropertiesDefaults.h @@ -27,6 +27,7 @@ const glm::vec3 ENTITY_ITEM_HALF_VEC3 = glm::vec3(0.5f); const bool ENTITY_ITEM_DEFAULT_LOCKED = false; const QString ENTITY_ITEM_DEFAULT_USER_DATA = QString(""); const QString ENTITY_ITEM_DEFAULT_MARKETPLACE_ID = QString(""); +const bool ENTITY_ITEM_DEFAULT_SHOULD_HIGHLIGHT = false; const QUuid ENTITY_ITEM_DEFAULT_SIMULATOR_ID = QUuid(); const float ENTITY_ITEM_DEFAULT_ALPHA = 1.0f; diff --git a/libraries/entities/src/EntityPropertyFlags.h b/libraries/entities/src/EntityPropertyFlags.h index b3cfc143c2..9600d0d4fe 100644 --- a/libraries/entities/src/EntityPropertyFlags.h +++ b/libraries/entities/src/EntityPropertyFlags.h @@ -78,6 +78,7 @@ enum EntityPropertyList { PROP_COMPOUND_SHAPE_URL, // used by Model + zones entities PROP_MARKETPLACE_ID, // all entities + PROP_SHOULD_HIGHLIGHT, // all entities PROP_ACCELERATION, // all entities PROP_SIMULATION_OWNER, // formerly known as PROP_SIMULATOR_ID PROP_NAME, // all entities diff --git a/libraries/entities/src/EntityScriptingInterface.cpp b/libraries/entities/src/EntityScriptingInterface.cpp index f22631d363..e21c9581e1 100644 --- a/libraries/entities/src/EntityScriptingInterface.cpp +++ b/libraries/entities/src/EntityScriptingInterface.cpp @@ -21,6 +21,7 @@ #include #include #include +#include #include "EntityItemID.h" #include "EntitiesLogging.h" @@ -220,7 +221,7 @@ QUuid EntityScriptingInterface::addEntity(const EntityItemProperties& properties _entityTree->withWriteLock([&] { EntityItemPointer entity = _entityTree->addEntity(id, propertiesWithSimID); if (entity) { - if (propertiesWithSimID.parentRelatedPropertyChanged()) { + if (propertiesWithSimID.queryAACubeRelatedPropertyChanged()) { // due to parenting, the server may not know where something is in world-space, so include the bounding cube. bool success; AACube queryAACube = entity->getQueryAACube(success); @@ -434,7 +435,7 @@ QUuid EntityScriptingInterface::editEntity(QUuid id, const EntityItemProperties& entity->rememberHasSimulationOwnershipBid(); } } - if (properties.parentRelatedPropertyChanged() && entity->computePuffedQueryAACube()) { + if (properties.queryAACubeRelatedPropertyChanged()) { properties.setQueryAACube(entity->getQueryAACube()); } entity->setLastBroadcast(usecTimestampNow()); @@ -444,7 +445,7 @@ QUuid EntityScriptingInterface::editEntity(QUuid id, const EntityItemProperties& // if they've changed. entity->forEachDescendant([&](SpatiallyNestablePointer descendant) { if (descendant->getNestableType() == NestableType::Entity) { - if (descendant->computePuffedQueryAACube()) { + if (descendant->checkAndMaybeUpdateQueryAACube()) { EntityItemPointer entityDescendant = std::static_pointer_cast(descendant); EntityItemProperties newQueryCubeProperties; newQueryCubeProperties.setQueryAACube(descendant->getQueryAACube()); @@ -499,6 +500,11 @@ void EntityScriptingInterface::deleteEntity(QUuid id) { const QUuid myNodeID = nodeList->getSessionUUID(); if (entity->getClientOnly() && entity->getOwningAvatarID() != myNodeID) { // don't delete other avatar's avatarEntities + // If you actually own the entity but the onwership property is not set because of a domain switch + // The lines below makes sure the entity is deleted once its properties are set. + auto avatarHashMap = DependencyManager::get(); + AvatarSharedPointer myAvatar = avatarHashMap->getAvatarBySessionID(myNodeID); + myAvatar->insertDetachedEntityID(id); shouldDelete = false; return; } @@ -1730,9 +1736,7 @@ glm::mat4 EntityScriptingInterface::getEntityTransform(const QUuid& 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; + result = translation * rotation; } }); } @@ -1747,9 +1751,7 @@ glm::mat4 EntityScriptingInterface::getEntityLocalTransform(const QUuid& entityI 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; + result = translation * rotation; } }); } diff --git a/libraries/entities/src/EntityTree.cpp b/libraries/entities/src/EntityTree.cpp index 4773f45af7..f17b991b69 100644 --- a/libraries/entities/src/EntityTree.cpp +++ b/libraries/entities/src/EntityTree.cpp @@ -90,13 +90,17 @@ void EntityTree::eraseAllOctreeElements(bool createNewRoot) { if (_simulation) { _simulation->clearEntities(); } - { - QWriteLocker locker(&_entityToElementLock); - foreach(EntityTreeElementPointer element, _entityToElementMap) { - element->cleanupEntities(); + QHash localMap; + localMap.swap(_entityMap); + this->withWriteLock([&] { + foreach(EntityItemPointer entity, localMap) { + EntityTreeElementPointer element = entity->getElement(); + if (element) { + element->cleanupEntities(); + } } - _entityToElementMap.clear(); - } + }); + localMap.clear(); Octree::eraseAllOctreeElements(createNewRoot); resetClientEditStats(); @@ -136,29 +140,24 @@ void EntityTree::postAddEntity(EntityItemPointer entity) { } bool EntityTree::updateEntity(const EntityItemID& entityID, const EntityItemProperties& properties, const SharedNodePointer& senderNode) { - EntityTreeElementPointer containingElement = getContainingElement(entityID); + EntityItemPointer entity; + { + QReadLocker locker(&_entityMapLock); + entity = _entityMap.value(entityID); + } + if (!entity) { + return false; + } + return updateEntity(entity, properties, senderNode); +} + +bool EntityTree::updateEntity(EntityItemPointer entity, const EntityItemProperties& origProperties, + const SharedNodePointer& senderNode) { + EntityTreeElementPointer containingElement = entity->getElement(); if (!containingElement) { return false; } - EntityItemPointer existingEntity = containingElement->getEntityWithEntityItemID(entityID); - if (!existingEntity) { - return false; - } - - return updateEntityWithElement(existingEntity, properties, containingElement, senderNode); -} - -bool EntityTree::updateEntity(EntityItemPointer entity, const EntityItemProperties& properties, const SharedNodePointer& senderNode) { - EntityTreeElementPointer containingElement = getContainingElement(entity->getEntityItemID()); - if (!containingElement) { - return false; - } - return updateEntityWithElement(entity, properties, containingElement, senderNode); -} - -bool EntityTree::updateEntityWithElement(EntityItemPointer entity, const EntityItemProperties& origProperties, - EntityTreeElementPointer containingElement, const SharedNodePointer& senderNode) { EntityItemProperties properties = origProperties; bool allowLockChange; @@ -190,7 +189,7 @@ bool EntityTree::updateEntityWithElement(EntityItemPointer entity, const EntityI bool success; AACube queryCube = entity->getQueryAACube(success); if (!success) { - qCDebug(entities) << "Warning -- failed to get query-cube for" << entity->getID(); + qCWarning(entities) << "failed to get query-cube for" << entity->getID(); } UpdateEntityOperator theOperator(getThisPointer(), containingElement, entity, queryCube); recurseTreeWithOperator(&theOperator); @@ -331,9 +330,8 @@ bool EntityTree::updateEntityWithElement(EntityItemPointer entity, const EntityI } // TODO: this final containingElement check should eventually be removed (or wrapped in an #ifdef DEBUG). - containingElement = getContainingElement(entity->getEntityItemID()); - if (!containingElement) { - qCDebug(entities) << "UNEXPECTED!!!! after updateEntity() we no longer have a containing element??? entityID=" + if (!entity->getElement()) { + qCWarning(entities) << "EntityTree::updateEntity() we no longer have a containing element for entityID=" << entity->getEntityItemID(); return false; } @@ -366,7 +364,7 @@ EntityItemPointer EntityTree::addEntity(const EntityItemID& entityID, const Enti // You should not call this on existing entities that are already part of the tree! Call updateEntity() EntityTreeElementPointer containingElement = getContainingElement(entityID); if (containingElement) { - qCDebug(entities) << "UNEXPECTED!!! ----- don't call addEntity() on existing entity items. entityID=" << entityID + qCWarning(entities) << "EntityTree::addEntity() on existing entity item with entityID=" << entityID << "containingElement=" << containingElement.get(); return result; } @@ -422,7 +420,7 @@ void EntityTree::deleteEntity(const EntityItemID& entityID, bool force, bool ign EntityTreeElementPointer containingElement = getContainingElement(entityID); if (!containingElement) { if (!ignoreWarnings) { - qCDebug(entities) << "UNEXPECTED!!!! EntityTree::deleteEntity() entityID doesn't exist!!! entityID=" << entityID; + qCWarning(entities) << "EntityTree::deleteEntity() on non-existent entityID=" << entityID; } return; } @@ -430,8 +428,7 @@ void EntityTree::deleteEntity(const EntityItemID& entityID, bool force, bool ign EntityItemPointer existingEntity = containingElement->getEntityWithEntityItemID(entityID); if (!existingEntity) { if (!ignoreWarnings) { - qCDebug(entities) << "UNEXPECTED!!!! don't call EntityTree::deleteEntity() on entity items that don't exist. " - "entityID=" << entityID; + qCWarning(entities) << "EntityTree::deleteEntity() on non-existant entity item with entityID=" << entityID; } return; } @@ -478,7 +475,7 @@ void EntityTree::deleteEntities(QSet entityIDs, bool force, bool i EntityTreeElementPointer containingElement = getContainingElement(entityID); if (!containingElement) { if (!ignoreWarnings) { - qCDebug(entities) << "UNEXPECTED!!!! EntityTree::deleteEntities() entityID doesn't exist!!! entityID=" << entityID; + qCWarning(entities) << "EntityTree::deleteEntities() on non-existent entityID=" << entityID; } continue; } @@ -486,8 +483,7 @@ void EntityTree::deleteEntities(QSet entityIDs, bool force, bool i EntityItemPointer existingEntity = containingElement->getEntityWithEntityItemID(entityID); if (!existingEntity) { if (!ignoreWarnings) { - qCDebug(entities) << "UNEXPECTED!!!! don't call EntityTree::deleteEntities() on entity items that don't exist. " - "entityID=" << entityID; + qCWarning(entities) << "EntityTree::deleteEntities() on non-existent entity item with entityID=" << entityID; } continue; } @@ -975,7 +971,7 @@ int EntityTree::processEditPacketData(ReceivedMessage& message, const unsigned c const SharedNodePointer& senderNode) { if (!getIsServer()) { - qCDebug(entities) << "UNEXPECTED!!! processEditPacketData() should only be called on a server tree."; + qCWarning(entities) << "EntityTree::processEditPacketData() should only be called on a server tree."; return 0; } @@ -1502,27 +1498,43 @@ int EntityTree::processEraseMessageDetails(const QByteArray& dataByteArray, cons } EntityTreeElementPointer EntityTree::getContainingElement(const EntityItemID& entityItemID) /*const*/ { - QReadLocker locker(&_entityToElementLock); - EntityTreeElementPointer element = _entityToElementMap.value(entityItemID); - return element; + EntityItemPointer entity; + { + QReadLocker locker(&_entityMapLock); + entity = _entityMap.value(entityItemID); + } + if (entity) { + return entity->getElement(); + } + return EntityTreeElementPointer(nullptr); } -void EntityTree::setContainingElement(const EntityItemID& entityItemID, EntityTreeElementPointer element) { - QWriteLocker locker(&_entityToElementLock); - if (element) { - _entityToElementMap[entityItemID] = element; - } else { - _entityToElementMap.remove(entityItemID); +void EntityTree::addEntityMapEntry(EntityItemPointer entity) { + EntityItemID id = entity->getEntityItemID(); + QWriteLocker locker(&_entityMapLock); + EntityItemPointer otherEntity = _entityMap.value(id); + if (otherEntity) { + qCWarning(entities) << "EntityTree::addEntityMapEntry() found pre-existing id " << id; + assert(false); + return; } + _entityMap.insert(id, entity); +} + +void EntityTree::clearEntityMapEntry(const EntityItemID& id) { + QWriteLocker locker(&_entityMapLock); + _entityMap.remove(id); } void EntityTree::debugDumpMap() { + // QHash's are implicitly shared, so we make a shared copy and use that instead. + // This way we might be able to avoid both a lock and a true copy. + QHash localMap(_entityMap); qCDebug(entities) << "EntityTree::debugDumpMap() --------------------------"; - QReadLocker locker(&_entityToElementLock); - QHashIterator i(_entityToElementMap); + QHashIterator i(localMap); while (i.hasNext()) { i.next(); - qCDebug(entities) << i.key() << ": " << i.value().get(); + qCDebug(entities) << i.key() << ": " << i.value()->getElement().get(); } qCDebug(entities) << "-----------------------------------------------------"; } @@ -1663,6 +1675,7 @@ QVector EntityTree::sendEntities(EntityEditPacketSender* packetSen addToNeedsParentFixupList(entity); } entity->forceQueryAACubeUpdate(); + entity->checkAndMaybeUpdateQueryAACube(); moveOperator.addEntityToMoveList(entity, entity->getQueryAACube()); i++; } else { @@ -1681,7 +1694,7 @@ QVector EntityTree::sendEntities(EntityEditPacketSender* packetSen EntityItemPointer entity = localTree->findEntityByEntityItemID(newID); if (entity) { // queue the packet to send to the server - entity->computePuffedQueryAACube(); + entity->updateQueryAACube(); EntityItemProperties properties = entity->getProperties(); properties.markAllChanged(); // so the entire property set is considered new, since we're making a new entity packetSender->queueEditEntityMessage(PacketType::EntityAdd, localTree, newID, properties); diff --git a/libraries/entities/src/EntityTree.h b/libraries/entities/src/EntityTree.h index 24e6c364b1..efb8cf81ba 100644 --- a/libraries/entities/src/EntityTree.h +++ b/libraries/entities/src/EntityTree.h @@ -119,9 +119,6 @@ public: // use this method if you only know the entityID bool updateEntity(const EntityItemID& entityID, const EntityItemProperties& properties, const SharedNodePointer& senderNode = SharedNodePointer(nullptr)); - // use this method if you have a pointer to the entity (avoid an extra entity lookup) - bool updateEntity(EntityItemPointer entity, const EntityItemProperties& properties, const SharedNodePointer& senderNode = SharedNodePointer(nullptr)); - // check if the avatar is a child of this entity, If so set the avatar parentID to null void unhookChildAvatar(const EntityItemID entityID); void deleteEntity(const EntityItemID& entityID, bool force = false, bool ignoreWarnings = true); @@ -183,7 +180,8 @@ public: int processEraseMessageDetails(const QByteArray& buffer, const SharedNodePointer& sourceNode); EntityTreeElementPointer getContainingElement(const EntityItemID& entityItemID) /*const*/; - void setContainingElement(const EntityItemID& entityItemID, EntityTreeElementPointer element); + void addEntityMapEntry(EntityItemPointer entity); + void clearEntityMapEntry(const EntityItemID& id); void debugDumpMap(); virtual void dumpTree() override; virtual void pruneTree() override; @@ -275,9 +273,8 @@ signals: protected: void processRemovedEntities(const DeleteEntityOperator& theOperator); - bool updateEntityWithElement(EntityItemPointer entity, const EntityItemProperties& properties, - EntityTreeElementPointer containingElement, - const SharedNodePointer& senderNode = SharedNodePointer(nullptr)); + bool updateEntity(EntityItemPointer entity, const EntityItemProperties& properties, + const SharedNodePointer& senderNode = SharedNodePointer(nullptr)); static bool findNearPointOperation(const OctreeElementPointer& element, void* extraData); static bool findInSphereOperation(const OctreeElementPointer& element, void* extraData); static bool findInCubeOperation(const OctreeElementPointer& element, void* extraData); @@ -309,8 +306,8 @@ protected: _deletedEntityItemIDs << id; } - mutable QReadWriteLock _entityToElementLock; - QHash _entityToElementMap; + mutable QReadWriteLock _entityMapLock; + QHash _entityMap; EntitySimulationPointer _simulation; diff --git a/libraries/entities/src/EntityTreeElement.cpp b/libraries/entities/src/EntityTreeElement.cpp index cce7ee006f..487bf60f61 100644 --- a/libraries/entities/src/EntityTreeElement.cpp +++ b/libraries/entities/src/EntityTreeElement.cpp @@ -885,10 +885,10 @@ EntityItemPointer EntityTreeElement::getEntityWithEntityItemID(const EntityItemI void EntityTreeElement::cleanupEntities() { withWriteLock([&] { foreach(EntityItemPointer entity, _entityItems) { + // NOTE: only EntityTreeElement should ever be changing the value of entity->_element // NOTE: We explicitly don't delete the EntityItem here because since we only // access it by smart pointers, when we remove it from the _entityItems // we know that it will be deleted. - //delete entity; entity->_element = NULL; } _entityItems.clear(); @@ -903,6 +903,7 @@ bool EntityTreeElement::removeEntityWithEntityItemID(const EntityItemID& id) { EntityItemPointer& entity = _entityItems[i]; if (entity->getEntityItemID() == id) { foundEntity = true; + // NOTE: only EntityTreeElement should ever be changing the value of entity->_element entity->_element = NULL; _entityItems.removeAt(i); break; @@ -918,6 +919,7 @@ bool EntityTreeElement::removeEntityItem(EntityItemPointer entity) { numEntries = _entityItems.removeAll(entity); }); if (numEntries > 0) { + // NOTE: only EntityTreeElement should ever be changing the value of entity->_element assert(entity->_element.get() == this); entity->_element = NULL; return true; @@ -999,9 +1001,11 @@ int EntityTreeElement::readElementDataFromBuffer(const unsigned char* data, int if (!bestFitBefore && bestFitAfter) { // This is the case where the entity existed, and is in some element in our tree... if (currentContainingElement.get() != this) { - currentContainingElement->removeEntityItem(entityItem); + // if the currentContainingElement is non-null, remove the entity from it + if (currentContainingElement) { + currentContainingElement->removeEntityItem(entityItem); + } addEntityItem(entityItem); - _myTree->setContainingElement(entityItemID, getThisPointer()); } } } @@ -1032,9 +1036,9 @@ int EntityTreeElement::readElementDataFromBuffer(const unsigned char* data, int // don't add if we've recently deleted.... if (!_myTree->isDeletedEntity(entityItem->getID())) { + _myTree->addEntityMapEntry(entityItem); addEntityItem(entityItem); // add this new entity to this elements entities entityItemID = entityItem->getEntityItemID(); - _myTree->setContainingElement(entityItemID, getThisPointer()); _myTree->postAddEntity(entityItem); if (entityItem->getCreated() == UNKNOWN_CREATED_TIME) { entityItem->recordCreationTime(); diff --git a/libraries/entities/src/MovingEntitiesOperator.cpp b/libraries/entities/src/MovingEntitiesOperator.cpp index ab97c67aa2..42e5a2ece5 100644 --- a/libraries/entities/src/MovingEntitiesOperator.cpp +++ b/libraries/entities/src/MovingEntitiesOperator.cpp @@ -51,7 +51,7 @@ MovingEntitiesOperator::~MovingEntitiesOperator() { void MovingEntitiesOperator::addEntityToMoveList(EntityItemPointer entity, const AACube& newCube) { - EntityTreeElementPointer oldContainingElement = _tree->getContainingElement(entity->getEntityItemID()); + EntityTreeElementPointer oldContainingElement = entity->getElement(); AABox newCubeClamped = newCube.clamp((float)-HALF_TREE_SCALE, (float)HALF_TREE_SCALE); if (_wantDebug) { @@ -193,7 +193,6 @@ bool MovingEntitiesOperator::preRecursion(const OctreeElementPointer& element) { // If this element is the best fit for the new bounds of this entity then add the entity to the element if (!details.newFound && entityTreeElement->bestFitBounds(details.newCube)) { - EntityItemID entityItemID = details.entity->getEntityItemID(); // remove from the old before adding EntityTreeElementPointer oldElement = details.entity->getElement(); if (oldElement != entityTreeElement) { @@ -201,7 +200,6 @@ bool MovingEntitiesOperator::preRecursion(const OctreeElementPointer& element) { oldElement->removeEntityItem(details.entity); } entityTreeElement->addEntityItem(details.entity); - _tree->setContainingElement(entityItemID, entityTreeElement); } _foundNewCount++; //details.newFound = true; // TODO: would be nice to add this optimization diff --git a/libraries/entities/src/ShapeEntityItem.cpp b/libraries/entities/src/ShapeEntityItem.cpp index 018d8c568a..eb1bed503c 100644 --- a/libraries/entities/src/ShapeEntityItem.cpp +++ b/libraries/entities/src/ShapeEntityItem.cpp @@ -160,12 +160,6 @@ void ShapeEntityItem::appendSubclassData(OctreePacketData* packetData, EncodeBit APPEND_ENTITY_PROPERTY(PROP_ALPHA, getAlpha()); } -// 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_ELLIPSOID; -} - void ShapeEntityItem::setColor(const rgbColor& value) { memcpy(_color, value, sizeof(rgbColor)); } @@ -223,10 +217,12 @@ bool ShapeEntityItem::findDetailedRayIntersection(const glm::vec3& origin, const void ShapeEntityItem::debugDump() const { quint64 now = usecTimestampNow(); qCDebug(entities) << "SHAPE EntityItem id:" << getEntityItemID() << "---------------------------------------------"; - qCDebug(entities) << " shape:" << stringFromShape(_shape); + qCDebug(entities) << " name:" << _name; + qCDebug(entities) << " shape:" << stringFromShape(_shape) << " (EnumId: " << _shape << " )"; qCDebug(entities) << " color:" << _color[0] << "," << _color[1] << "," << _color[2]; qCDebug(entities) << " position:" << debugTreeVector(getPosition()); qCDebug(entities) << " dimensions:" << debugTreeVector(getDimensions()); qCDebug(entities) << " getLastEdited:" << debugTime(getLastEdited(), now); + qCDebug(entities) << "SHAPE EntityItem Ptr:" << this; } diff --git a/libraries/entities/src/ShapeEntityItem.h b/libraries/entities/src/ShapeEntityItem.h index 96f69deb0c..60fcfd628a 100644 --- a/libraries/entities/src/ShapeEntityItem.h +++ b/libraries/entities/src/ShapeEntityItem.h @@ -84,7 +84,6 @@ public: QColor getQColor() const; void setColor(const QColor& value); - ShapeType getShapeType() const override; bool shouldBePhysical() const override { return !isDead(); } bool supportsDetailedRayIntersection() const override; diff --git a/libraries/entities/src/SimpleEntitySimulation.cpp b/libraries/entities/src/SimpleEntitySimulation.cpp index e406926141..d41b22137e 100644 --- a/libraries/entities/src/SimpleEntitySimulation.cpp +++ b/libraries/entities/src/SimpleEntitySimulation.cpp @@ -136,7 +136,7 @@ void SimpleEntitySimulation::sortEntitiesThatMoved() { SetOfEntities::iterator itemItr = _entitiesToSort.begin(); while (itemItr != _entitiesToSort.end()) { EntityItemPointer entity = *itemItr; - entity->computePuffedQueryAACube(); + entity->checkAndMaybeUpdateQueryAACube(); ++itemItr; } EntitySimulation::sortEntitiesThatMoved(); diff --git a/libraries/entities/src/UpdateEntityOperator.cpp b/libraries/entities/src/UpdateEntityOperator.cpp index ec6051af04..7a5c87187a 100644 --- a/libraries/entities/src/UpdateEntityOperator.cpp +++ b/libraries/entities/src/UpdateEntityOperator.cpp @@ -138,8 +138,8 @@ bool UpdateEntityOperator::preRecursion(const OctreeElementPointer& element) { qCDebug(entities) << " _foundNew=" << _foundNew; } - // If we haven't yet found the old entity, and this subTreeContains our old - // entity, then we need to keep searching. + // If we haven't yet found the old element, and this subTreeContains our old element, + // then we need to keep searching. if (!_foundOld && subtreeContainsOld) { if (_wantDebug) { @@ -169,7 +169,6 @@ bool UpdateEntityOperator::preRecursion(const OctreeElementPointer& element) { // NOTE: we know we haven't yet added it to its new element because _removeOld is true EntityTreeElementPointer oldElement = _existingEntity->getElement(); oldElement->removeEntityItem(_existingEntity); - _tree->setContainingElement(_entityItemID, NULL); if (oldElement != _containingElement) { qCDebug(entities) << "WARNING entity moved during UpdateEntityOperator recursion"; @@ -187,8 +186,8 @@ bool UpdateEntityOperator::preRecursion(const OctreeElementPointer& element) { } } - // If we haven't yet found the new entity, and this subTreeContains our new - // entity, then we need to keep searching. + // If we haven't yet found the new element, and this subTreeContains our new element, + // then we need to keep searching. if (!_foundNew && subtreeContainsNew) { if (_wantDebug) { @@ -221,7 +220,6 @@ bool UpdateEntityOperator::preRecursion(const OctreeElementPointer& element) { } } entityTreeElement->addEntityItem(_existingEntity); - _tree->setContainingElement(_entityItemID, entityTreeElement); } _foundNew = true; // we found the new element _removeOld = false; // and it has already been removed from the old diff --git a/libraries/fbx/src/OBJReader.cpp b/libraries/fbx/src/OBJReader.cpp index 417901b9ab..3099782588 100644 --- a/libraries/fbx/src/OBJReader.cpp +++ b/libraries/fbx/src/OBJReader.cpp @@ -118,11 +118,39 @@ glm::vec3 OBJTokenizer::getVec3() { auto z = getFloat(); auto v = glm::vec3(x, y, z); while (isNextTokenFloat()) { - // the spec(s) get(s) vague here. might be w, might be a color... chop it off. + // ignore any following floats nextToken(); } return v; } +bool OBJTokenizer::getVertex(glm::vec3& vertex, glm::vec3& vertexColor) { + // Used for vertices which may also have a vertex color (RGB [0,1]) to follow. + // NOTE: Returns true if there is a vertex color. + auto x = getFloat(); // N.B.: getFloat() has side-effect + auto y = getFloat(); // And order of arguments is different on Windows/Linux. + auto z = getFloat(); + vertex = glm::vec3(x, y, z); + + auto r = 1.0f, g = 1.0f, b = 1.0f; + bool hasVertexColor = false; + if (isNextTokenFloat()) { + // If there's another float it's one of two things: a W component or an R component. The standard OBJ spec + // doesn't output a W component, so we're making the assumption that if a float follows (unless it's + // only a single value) that it's a vertex color. + r = getFloat(); + if (isNextTokenFloat()) { + // Safe to assume the following values are the green/blue components. + g = getFloat(); + b = getFloat(); + + hasVertexColor = true; + } + + vertexColor = glm::vec3(r, g, b); + } + + return hasVertexColor; +} glm::vec2 OBJTokenizer::getVec2() { float uCoord = getFloat(); float vCoord = 1.0f - getFloat(); @@ -140,7 +168,9 @@ void setMeshPartDefaults(FBXMeshPart& meshPart, QString materialID) { } // OBJFace -bool OBJFace::add(const QByteArray& vertexIndex, const QByteArray& textureIndex, const QByteArray& normalIndex, const QVector& vertices) { +// NOTE (trent, 7/20/17): The vertexColors vector being passed-in isn't necessary here, but I'm just +// pairing it with the vertices vector for consistency. +bool OBJFace::add(const QByteArray& vertexIndex, const QByteArray& textureIndex, const QByteArray& normalIndex, const QVector& vertices, const QVector& vertexColors) { bool ok; int index = vertexIndex.toInt(&ok); if (!ok) { @@ -321,6 +351,8 @@ bool OBJReader::parseOBJGroup(OBJTokenizer& tokenizer, const QVariantHash& mappi bool result = true; int originalFaceCountForDebugging = 0; QString currentGroup; + bool anyVertexColor { false }; + int vertexCount { 0 }; setMeshPartDefaults(meshPart, QString("dontknow") + QString::number(mesh.parts.count())); @@ -382,7 +414,25 @@ bool OBJReader::parseOBJGroup(OBJTokenizer& tokenizer, const QVariantHash& mappi #endif } } else if (token == "v") { - vertices.append(tokenizer.getVec3()); + glm::vec3 vertex; + glm::vec3 vertexColor { glm::vec3(1.0f) }; + + bool hasVertexColor = tokenizer.getVertex(vertex, vertexColor); + vertices.append(vertex); + + // if any vertex has color, they all need to. + if (hasVertexColor && !anyVertexColor) { + // we've had a gap of zero or more vertices without color, followed + // by one that has color. catch up: + for (int i = 0; i < vertexCount; i++) { + vertexColors.append(glm::vec3(1.0f)); + } + anyVertexColor = true; + } + if (anyVertexColor) { + vertexColors.append(vertexColor); + } + vertexCount++; } else if (token == "vn") { normals.append(tokenizer.getVec3()); } else if (token == "vt") { @@ -410,7 +460,8 @@ bool OBJReader::parseOBJGroup(OBJTokenizer& tokenizer, const QVariantHash& mappi assert(parts.count() >= 1); assert(parts.count() <= 3); const QByteArray noData {}; - face.add(parts[0], (parts.count() > 1) ? parts[1] : noData, (parts.count() > 2) ? parts[2] : noData, vertices); + face.add(parts[0], (parts.count() > 1) ? parts[1] : noData, (parts.count() > 2) ? parts[2] : noData, + vertices, vertexColors); face.groupName = currentGroup; face.materialName = currentMaterialName; } @@ -540,6 +591,15 @@ FBXGeometry* OBJReader::readOBJ(QByteArray& model, const QVariantHash& mapping, glm::vec3 v1 = checked_at(vertices, face.vertexIndices[1]); glm::vec3 v2 = checked_at(vertices, face.vertexIndices[2]); + glm::vec3 vc0, vc1, vc2; + bool hasVertexColors = (vertexColors.size() > 0); + if (hasVertexColors) { + // If there are any vertex colors, it's safe to assume all meshes had them exported. + vc0 = checked_at(vertexColors, face.vertexIndices[0]); + vc1 = checked_at(vertexColors, face.vertexIndices[1]); + vc2 = checked_at(vertexColors, face.vertexIndices[2]); + } + // Scale the vertices if the OBJ file scale is specified as non-one. if (scaleGuess != 1.0f) { v0 *= scaleGuess; @@ -555,6 +615,13 @@ FBXGeometry* OBJReader::readOBJ(QByteArray& model, const QVariantHash& mapping, meshPart.triangleIndices.append(mesh.vertices.count()); mesh.vertices << v2; + if (hasVertexColors) { + // Add vertex colors. + mesh.colors << vc0; + mesh.colors << vc1; + mesh.colors << vc2; + } + glm::vec3 n0, n1, n2; if (face.normalIndices.count()) { n0 = checked_at(normals, face.normalIndices[0]); @@ -690,6 +757,7 @@ void fbxDebugDump(const FBXGeometry& fbxgeo) { qCDebug(modelformat) << " meshes.count() =" << fbxgeo.meshes.count(); foreach (FBXMesh mesh, fbxgeo.meshes) { qCDebug(modelformat) << " vertices.count() =" << mesh.vertices.count(); + qCDebug(modelformat) << " colors.count() =" << mesh.colors.count(); qCDebug(modelformat) << " normals.count() =" << mesh.normals.count(); /*if (mesh.normals.count() == mesh.vertices.count()) { for (int i = 0; i < mesh.normals.count(); i++) { diff --git a/libraries/fbx/src/OBJReader.h b/libraries/fbx/src/OBJReader.h index 18a4b89f1e..9a32871590 100644 --- a/libraries/fbx/src/OBJReader.h +++ b/libraries/fbx/src/OBJReader.h @@ -20,6 +20,7 @@ public: void ungetChar(char ch) { _device->ungetChar(ch); } const QString getComment() const { return _comment; } glm::vec3 getVec3(); + bool getVertex(glm::vec3& vertex, glm::vec3& vertexColor); glm::vec2 getVec2(); float getFloat() { return std::stof((nextToken() != OBJTokenizer::DATUM_TOKEN) ? nullptr : getDatum().data()); } @@ -38,7 +39,8 @@ public: QString groupName; // We don't make use of hierarchical structure, but it can be preserved for debugging and future use. QString materialName; // Add one more set of vertex data. Answers true if successful - bool add(const QByteArray& vertexIndex, const QByteArray& textureIndex, const QByteArray& normalIndex, const QVector& vertices); + bool add(const QByteArray& vertexIndex, const QByteArray& textureIndex, const QByteArray& normalIndex, + const QVector& vertices, const QVector& vertexColors); // Return a set of one or more OBJFaces from this one, in which each is just a triangle. // Even though FBXMeshPart can handle quads, it would be messy to try to keep track of mixed-size faces, so we treat everything as triangles. QVector triangulate(); @@ -65,7 +67,8 @@ class OBJReader: public QObject { // QObject so we can make network requests. Q_OBJECT public: typedef QVector FaceGroup; - QVector vertices; // all that we ever encounter while reading + QVector vertices; + QVector vertexColors; QVector textureUVs; QVector normals; QVector faceGroups; diff --git a/libraries/fbx/src/OBJWriter.cpp b/libraries/fbx/src/OBJWriter.cpp index 034263eb53..e0d3d36b06 100644 --- a/libraries/fbx/src/OBJWriter.cpp +++ b/libraries/fbx/src/OBJWriter.cpp @@ -40,8 +40,6 @@ static QString formatFloat(double n) { } 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; @@ -49,10 +47,15 @@ bool writeOBJToTextStream(QTextStream& out, QList meshes) { int currentVertexStartOffset = 0; int currentNormalStartOffset = 0; - // write out vertices + // write out vertices (and maybe colors) foreach (const MeshPointer& mesh, meshes) { meshVertexStartOffset.append(currentVertexStartOffset); const gpu::BufferView& vertexBuffer = mesh->getVertexBuffer(); + + const gpu::BufferView& colorsBufferView = mesh->getAttributeBuffer(gpu::Stream::COLOR); + gpu::BufferView::Index numColors = (gpu::BufferView::Index)colorsBufferView.getNumElements(); + gpu::BufferView::Index colorIndex = 0; + int vertexCount = 0; gpu::BufferView::Iterator vertexItr = vertexBuffer.cbegin(); while (vertexItr != vertexBuffer.cend()) { @@ -60,7 +63,15 @@ bool writeOBJToTextStream(QTextStream& out, QList meshes) { out << "v "; out << formatFloat(v[0]) << " "; out << formatFloat(v[1]) << " "; - out << formatFloat(v[2]) << "\n"; + out << formatFloat(v[2]); + if (colorIndex < numColors) { + glm::vec3 color = colorsBufferView.get(colorIndex); + out << " " << formatFloat(color[0]); + out << " " << formatFloat(color[1]); + out << " " << formatFloat(color[2]); + colorIndex++; + } + out << "\n"; vertexItr++; vertexCount++; } @@ -72,7 +83,7 @@ bool writeOBJToTextStream(QTextStream& out, QList meshes) { bool haveNormals = true; foreach (const MeshPointer& mesh, meshes) { meshNormalStartOffset.append(currentNormalStartOffset); - const gpu::BufferView& normalsBufferView = mesh->getAttributeBuffer(attributeTypeNormal); + const gpu::BufferView& normalsBufferView = mesh->getAttributeBuffer(gpu::Stream::InputSlot::NORMAL); gpu::BufferView::Index numNormals = (gpu::BufferView::Index)normalsBufferView.getNumElements(); for (gpu::BufferView::Index i = 0; i < numNormals; i++) { glm::vec3 normal = normalsBufferView.get(i); diff --git a/libraries/gl/src/gl/GLShaders.cpp b/libraries/gl/src/gl/GLShaders.cpp index fd0c6788cb..8ef0198676 100644 --- a/libraries/gl/src/gl/GLShaders.cpp +++ b/libraries/gl/src/gl/GLShaders.cpp @@ -182,7 +182,6 @@ GLuint compileProgram(const std::vector& glshaders, std::string& error) filestream.close(); } */ - delete[] temp; glDeleteProgram(glprogram); return 0; diff --git a/libraries/gpu-gl/src/gpu/gl41/GL41BackendInput.cpp b/libraries/gpu-gl/src/gpu/gl41/GL41BackendInput.cpp index 43b48f721f..e8ebcbe05c 100644 --- a/libraries/gpu-gl/src/gpu/gl41/GL41BackendInput.cpp +++ b/libraries/gpu-gl/src/gpu/gl41/GL41BackendInput.cpp @@ -95,7 +95,7 @@ void GL41Backend::updateInput() { // GLenum perLocationStride = strides[bufferNum]; GLenum perLocationStride = attrib._element.getLocationSize(); GLuint stride = (GLuint)strides[bufferNum]; - GLuint pointer = (GLuint)(attrib._offset + offsets[bufferNum]); + uintptr_t pointer = (uintptr_t)(attrib._offset + offsets[bufferNum]); GLboolean isNormalized = attrib._element.isNormalized(); for (size_t locNum = 0; locNum < locationCount; ++locNum) { diff --git a/libraries/gpu/src/gpu/Buffer.h b/libraries/gpu/src/gpu/Buffer.h index 2eb2267f0d..c5df94235c 100644 --- a/libraries/gpu/src/gpu/Buffer.h +++ b/libraries/gpu/src/gpu/Buffer.h @@ -192,7 +192,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 - _offset) / _stride; } + Size getNumElements() const { return _size / _stride; } //Template iterator with random access on the buffer sysmem template diff --git a/libraries/input-plugins/src/input-plugins/KeyboardMouseDevice.cpp b/libraries/input-plugins/src/input-plugins/KeyboardMouseDevice.cpp index b5a2fc6b3c..e9ec6d8910 100755 --- a/libraries/input-plugins/src/input-plugins/KeyboardMouseDevice.cpp +++ b/libraries/input-plugins/src/input-plugins/KeyboardMouseDevice.cpp @@ -236,6 +236,7 @@ controller::Input::NamedVector KeyboardMouseDevice::InputDevice::getAvailableInp availableInputs.append(Input::NamedPair(makeInput(Qt::Key_Shift), "Shift")); availableInputs.append(Input::NamedPair(makeInput(Qt::Key_PageUp), QKeySequence(Qt::Key_PageUp).toString())); availableInputs.append(Input::NamedPair(makeInput(Qt::Key_PageDown), QKeySequence(Qt::Key_PageDown).toString())); + availableInputs.append(Input::NamedPair(makeInput(Qt::Key_Tab), QKeySequence(Qt::Key_Tab).toString())); availableInputs.append(Input::NamedPair(makeInput(Qt::LeftButton), "LeftMouseButton")); availableInputs.append(Input::NamedPair(makeInput(Qt::MiddleButton), "MiddleMouseButton")); diff --git a/libraries/midi/CMakeLists.txt b/libraries/midi/CMakeLists.txt new file mode 100644 index 0000000000..dc54819c2b --- /dev/null +++ b/libraries/midi/CMakeLists.txt @@ -0,0 +1,3 @@ +set(TARGET_NAME midi) +setup_hifi_library(Network) +link_hifi_libraries(shared networking) diff --git a/libraries/midi/src/Midi.cpp b/libraries/midi/src/Midi.cpp new file mode 100644 index 0000000000..ad650cf067 --- /dev/null +++ b/libraries/midi/src/Midi.cpp @@ -0,0 +1,275 @@ +// +// Midi.cpp +// libraries/midi/src +// +// Created by Burt Sloane +// 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 "Midi.h" + + +#include + + +#if defined Q_OS_WIN32 +#include "Windows.h" +#endif + + +#if defined Q_OS_WIN32 +const int MIDI_BYTE_MASK = 0x0FF; +const int MIDI_SHIFT_NOTE = 8; +const int MIDI_SHIFT_VELOCITY = 16; +#endif +const int MIDI_STATUS_MASK = 0x0F0; +const int MIDI_NOTE_OFF = 0x080; +const int MIDI_NOTE_ON = 0x090; +const int MIDI_CONTROL_CHANGE = 0x0b0; +const int MIDI_CHANNEL_MODE_ALL_NOTES_OFF = 0x07b; + + +static Midi* instance = NULL; // communicate this to non-class callbacks +static bool thruModeEnabled = false; + +std::vector Midi::midiinexclude; +std::vector Midi::midioutexclude; + + +#if defined Q_OS_WIN32 + +#pragma comment(lib, "Winmm.lib") + +// +std::vector midihin; +std::vector midihout; + + +void CALLBACK MidiInProc(HMIDIIN hMidiIn, UINT wMsg, DWORD_PTR dwInstance, DWORD_PTR dwParam1, DWORD_PTR dwParam2) { + switch (wMsg) { + case MIM_OPEN: + // message not used + break; + case MIM_CLOSE: + for (int i = 0; i < midihin.size(); i++) { + if (midihin[i] == hMidiIn) { + midihin[i] = NULL; + instance->allNotesOff(); + } + } + break; + case MIM_DATA: { + int status = MIDI_BYTE_MASK & dwParam1; + int note = MIDI_BYTE_MASK & (dwParam1 >> MIDI_SHIFT_NOTE); + int vel = MIDI_BYTE_MASK & (dwParam1 >> MIDI_SHIFT_VELOCITY); + if (thruModeEnabled) { + instance->sendNote(status, note, vel); // relay the note on to all other midi devices + } + instance->noteReceived(status, note, vel); // notify the javascript + break; + } + } +} + + +void CALLBACK MidiOutProc(HMIDIOUT hmo, UINT wMsg, DWORD_PTR dwInstance, DWORD_PTR dwParam1, DWORD_PTR dwParam2) { + switch (wMsg) { + case MOM_OPEN: + // message not used + break; + case MOM_CLOSE: + for (int i = 0; i < midihout.size(); i++) { + if (midihout[i] == hmo) { + midihout[i] = NULL; + instance->allNotesOff(); + } + } + break; + } +} + + +void Midi::sendNote(int status, int note, int vel) { + for (int i = 0; i < midihout.size(); i++) { + if (midihout[i] != NULL) { + midiOutShortMsg(midihout[i], status + (note << MIDI_SHIFT_NOTE) + (vel << MIDI_SHIFT_VELOCITY)); + } + } +} + + +void Midi::MidiSetup() { + midihin.clear(); + midihout.clear(); + + MIDIINCAPS incaps; + for (unsigned int i = 0; i < midiInGetNumDevs(); i++) { + midiInGetDevCaps(i, &incaps, sizeof(MIDIINCAPS)); + + bool found = false; + for (int j = 0; j < midiinexclude.size(); j++) { + if (midiinexclude[j].toStdString().compare(incaps.szPname) == 0) { + found = true; + break; + } + } + if (!found) { // EXCLUDE AN INPUT BY NAME + HMIDIIN tmphin; + midiInOpen(&tmphin, i, (DWORD_PTR)MidiInProc, NULL, CALLBACK_FUNCTION); + midiInStart(tmphin); + midihin.push_back(tmphin); + } + + } + + MIDIOUTCAPS outcaps; + for (unsigned int i = 0; i < midiOutGetNumDevs(); i++) { + midiOutGetDevCaps(i, &outcaps, sizeof(MIDIINCAPS)); + + bool found = false; + for (int j = 0; j < midioutexclude.size(); j++) { + if (midioutexclude[j].toStdString().compare(outcaps.szPname) == 0) { + found = true; + break; + } + } + if (!found) { // EXCLUDE AN OUTPUT BY NAME + HMIDIOUT tmphout; + midiOutOpen(&tmphout, i, (DWORD_PTR)MidiOutProc, NULL, CALLBACK_FUNCTION); + midihout.push_back(tmphout); + } + } + + allNotesOff(); +} + +void Midi::MidiCleanup() { + allNotesOff(); + + for (int i = 0; i < midihin.size(); i++) { + if (midihin[i] != NULL) { + midiInStop(midihin[i]); + midiInClose(midihin[i]); + } + } + for (int i = 0; i < midihout.size(); i++) { + if (midihout[i] != NULL) { + midiOutClose(midihout[i]); + } + } + midihin.clear(); + midihout.clear(); +} +#else +void Midi::sendNote(int status, int note, int vel) { +} + +void Midi::MidiSetup() { + allNotesOff(); +} + +void Midi::MidiCleanup() { + allNotesOff(); +} +#endif + +void Midi::noteReceived(int status, int note, int velocity) { + if (((status & MIDI_STATUS_MASK) != MIDI_NOTE_OFF) && + ((status & MIDI_STATUS_MASK) != MIDI_NOTE_ON)) { + return; // NOTE: only sending note-on and note-off to Javascript + } + + QVariantMap eventData; + eventData["status"] = status; + eventData["note"] = note; + eventData["velocity"] = velocity; + emit midiNote(eventData); +} + +// + +Midi::Midi() { + instance = this; +#if defined Q_OS_WIN32 + midioutexclude.push_back("Microsoft GS Wavetable Synth"); // we don't want to hear this thing +#endif + MidiSetup(); +} + +Midi::~Midi() { +} + +void Midi::playMidiNote(int status, int note, int velocity) { + sendNote(status, note, velocity); +} + +void Midi::allNotesOff() { + sendNote(MIDI_CONTROL_CHANGE, MIDI_CHANNEL_MODE_ALL_NOTES_OFF, 0); // all notes off +} + +void Midi::resetDevices() { + MidiCleanup(); + MidiSetup(); +} + +void Midi::USBchanged() { + instance->MidiCleanup(); + instance->MidiSetup(); +} + +// + +QStringList Midi::listMidiDevices(bool output) { + QStringList rv; +#if defined Q_OS_WIN32 + if (output) { + MIDIOUTCAPS outcaps; + for (unsigned int i = 0; i < midiOutGetNumDevs(); i++) { + midiOutGetDevCaps(i, &outcaps, sizeof(MIDIINCAPS)); + rv.append(outcaps.szPname); + } + } else { + MIDIINCAPS incaps; + for (unsigned int i = 0; i < midiInGetNumDevs(); i++) { + midiInGetDevCaps(i, &incaps, sizeof(MIDIINCAPS)); + rv.append(incaps.szPname); + } + } +#endif + return rv; +} + +void Midi::unblockMidiDevice(QString name, bool output) { + if (output) { + for (unsigned long i = 0; i < midioutexclude.size(); i++) { + if (midioutexclude[i].toStdString().compare(name.toStdString()) == 0) { + midioutexclude.erase(midioutexclude.begin() + i); + break; + } + } + } else { + for (unsigned long i = 0; i < midiinexclude.size(); i++) { + if (midiinexclude[i].toStdString().compare(name.toStdString()) == 0) { + midiinexclude.erase(midiinexclude.begin() + i); + break; + } + } + } +} + +void Midi::blockMidiDevice(QString name, bool output) { + unblockMidiDevice(name, output); // make sure it's only in there once + if (output) { + midioutexclude.push_back(name); + } else { + midiinexclude.push_back(name); + } +} + +void Midi::thruModeEnable(bool enable) { + thruModeEnabled = enable; +} + diff --git a/libraries/midi/src/Midi.h b/libraries/midi/src/Midi.h new file mode 100644 index 0000000000..0ffa27986d --- /dev/null +++ b/libraries/midi/src/Midi.h @@ -0,0 +1,72 @@ +// +// Midi.h +// libraries/midi/src +// +// Created by Burt Sloane +// 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_Midi_h +#define hifi_Midi_h + +#include +#include +#include + +#include +#include + +class Midi : public QObject, public Dependency { + Q_OBJECT + SINGLETON_DEPENDENCY + +public: + void noteReceived(int status, int note, int velocity); // relay a note to Javascript + void sendNote(int status, int note, int vel); // relay a note to MIDI outputs + static void USBchanged(); + +private: + static std::vector midiinexclude; + static std::vector midioutexclude; + +private: + void MidiSetup(); + void MidiCleanup(); + +signals: + void midiNote(QVariantMap eventData); + +public slots: +/// play a note on all connected devices +/// @param {int} status: 0x80 is noteoff, 0x90 is noteon (if velocity=0, noteoff), etc +/// @param {int} note: midi note number +/// @param {int} velocity: note velocity (0 means noteoff) +Q_INVOKABLE void playMidiNote(int status, int note, int velocity); + +/// turn off all notes on all connected devices +Q_INVOKABLE void allNotesOff(); + +/// clean up and re-discover attached devices +Q_INVOKABLE void resetDevices(); + +/// ask for a list of inputs/outputs +Q_INVOKABLE QStringList listMidiDevices(bool output); + +/// block an input/output by name +Q_INVOKABLE void blockMidiDevice(QString name, bool output); + +/// unblock an input/output by name +Q_INVOKABLE void unblockMidiDevice(QString name, bool output); + +/// repeat all incoming notes to all outputs (default disabled) +Q_INVOKABLE void thruModeEnable(bool enable); + +public: + Midi(); + virtual ~Midi(); +}; + +#endif // hifi_Midi_h diff --git a/libraries/model-networking/src/model-networking/TextureCache.cpp b/libraries/model-networking/src/model-networking/TextureCache.cpp index 20749cd567..85bde4c2f1 100644 --- a/libraries/model-networking/src/model-networking/TextureCache.cpp +++ b/libraries/model-networking/src/model-networking/TextureCache.cpp @@ -54,6 +54,7 @@ const std::string TextureCache::KTX_EXT { "ktx" }; static const QString RESOURCE_SCHEME = "resource"; static const QUrl SPECTATOR_CAMERA_FRAME_URL("resource://spectatorCameraFrame"); +static const QUrl HMD_PREVIEW_FRAME_URL("resource://hmdPreviewFrame"); static const float SKYBOX_LOAD_PRIORITY { 10.0f }; // Make sure skybox loads first static const float HIGH_MIPS_LOAD_PRIORITY { 9.0f }; // Make sure high mips loads after skybox but before models @@ -219,7 +220,7 @@ gpu::TexturePointer TextureCache::cacheTextureByHash(const std::string& hash, co gpu::TexturePointer getFallbackTextureForType(image::TextureUsage::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 + // 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; @@ -369,7 +370,7 @@ void NetworkTexture::makeRequest() { if (!_sourceIsKTX) { Resource::makeRequest(); return; - } + } // We special-handle ktx requests to run 2 concurrent requests right off the bat PROFILE_ASYNC_BEGIN(resource, "Resource:" + getType(), QString::number(_requestID), { { "url", _url.toString() }, { "activeURL", _activeUrl.toString() } }); @@ -908,7 +909,7 @@ void ImageReader::read() { } } - // If we found the texture either because it's in use or via KTX deserialization, + // If we found the texture either because it's in use or via KTX deserialization, // set the image and return immediately. if (texture) { QMetaObject::invokeMethod(resource.data(), "setImage", @@ -957,7 +958,7 @@ void ImageReader::read() { qCWarning(modelnetworking) << "Unable to serialize texture to KTX " << _url; } - // We replace the texture with the one stored in the cache. This deals with the possible race condition of two different + // 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 texture = textureCache->cacheTextureByHash(hash, texture); @@ -969,23 +970,44 @@ void ImageReader::read() { Q_ARG(int, texture->getHeight())); } - NetworkTexturePointer TextureCache::getResourceTexture(QUrl resourceTextureUrl) { gpu::TexturePointer texture; if (resourceTextureUrl == SPECTATOR_CAMERA_FRAME_URL) { if (!_spectatorCameraNetworkTexture) { _spectatorCameraNetworkTexture.reset(new NetworkTexture(resourceTextureUrl)); } - texture = _spectatorCameraFramebuffer->getRenderBuffer(0); - if (texture) { - _spectatorCameraNetworkTexture->setImage(texture, texture->getWidth(), texture->getHeight()); - return _spectatorCameraNetworkTexture; + if (_spectatorCameraFramebuffer) { + texture = _spectatorCameraFramebuffer->getRenderBuffer(0); + if (texture) { + _spectatorCameraNetworkTexture->setImage(texture, texture->getWidth(), texture->getHeight()); + return _spectatorCameraNetworkTexture; + } + } + } + // FIXME: Generalize this, DRY up this code + if (resourceTextureUrl == HMD_PREVIEW_FRAME_URL) { + if (!_hmdPreviewNetworkTexture) { + _hmdPreviewNetworkTexture.reset(new NetworkTexture(resourceTextureUrl)); + } + if (_hmdPreviewFramebuffer) { + texture = _hmdPreviewFramebuffer->getRenderBuffer(0); + if (texture) { + _hmdPreviewNetworkTexture->setImage(texture, texture->getWidth(), texture->getHeight()); + return _hmdPreviewNetworkTexture; + } } } return NetworkTexturePointer(); } +const gpu::FramebufferPointer& TextureCache::getHmdPreviewFramebuffer(int width, int height) { + if (!_hmdPreviewFramebuffer || _hmdPreviewFramebuffer->getWidth() != width || _hmdPreviewFramebuffer->getHeight() != height) { + _hmdPreviewFramebuffer.reset(gpu::Framebuffer::create("hmdPreview",gpu::Element::COLOR_SRGBA_32, width, height)); + } + return _hmdPreviewFramebuffer; +} + const gpu::FramebufferPointer& TextureCache::getSpectatorCameraFramebuffer() { if (!_spectatorCameraFramebuffer) { resetSpectatorCameraFramebuffer(2048, 1024); @@ -996,4 +1018,5 @@ const gpu::FramebufferPointer& TextureCache::getSpectatorCameraFramebuffer() { void TextureCache::resetSpectatorCameraFramebuffer(int width, int height) { _spectatorCameraFramebuffer.reset(gpu::Framebuffer::create("spectatorCamera", gpu::Element::COLOR_SRGBA_32, width, height)); _spectatorCameraNetworkTexture.reset(); + emit spectatorCameraFramebufferReset(); } diff --git a/libraries/model-networking/src/model-networking/TextureCache.h b/libraries/model-networking/src/model-networking/TextureCache.h index 43edc3593d..f5a0ec5215 100644 --- a/libraries/model-networking/src/model-networking/TextureCache.h +++ b/libraries/model-networking/src/model-networking/TextureCache.h @@ -74,7 +74,7 @@ protected: virtual bool isCacheable() const override { return _loaded; } virtual void downloadFinished(const QByteArray& data) override; - + Q_INVOKABLE void loadContent(const QByteArray& content); Q_INVOKABLE void setImage(gpu::TexturePointer texture, int originalWidth, int originalHeight); @@ -170,6 +170,10 @@ public: NetworkTexturePointer getResourceTexture(QUrl resourceTextureUrl); const gpu::FramebufferPointer& getSpectatorCameraFramebuffer(); void resetSpectatorCameraFramebuffer(int width, int height); + const gpu::FramebufferPointer& getHmdPreviewFramebuffer(int width, int height); + +signals: + void spectatorCameraFramebufferReset(); protected: // Overload ResourceCache::prefetch to allow specifying texture type for loads @@ -202,6 +206,9 @@ private: NetworkTexturePointer _spectatorCameraNetworkTexture; gpu::FramebufferPointer _spectatorCameraFramebuffer; + + NetworkTexturePointer _hmdPreviewNetworkTexture; + gpu::FramebufferPointer _hmdPreviewFramebuffer; }; #endif // hifi_TextureCache_h diff --git a/libraries/model/src/model/Geometry.cpp b/libraries/model/src/model/Geometry.cpp index f88c8233ea..5627746c43 100755 --- a/libraries/model/src/model/Geometry.cpp +++ b/libraries/model/src/model/Geometry.cpp @@ -11,8 +11,6 @@ #include "Geometry.h" -#include - using namespace model; Mesh::Mesh() : @@ -136,11 +134,13 @@ Box Mesh::evalPartsBound(int partStart, int partEnd) const { model::MeshPointer Mesh::map(std::function vertexFunc, + std::function colorFunc, std::function normalFunc, - std::function indexFunc) { + std::function indexFunc) const { // 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; @@ -151,6 +151,21 @@ model::MeshPointer Mesh::map(std::function vertexFunc, vertexDataCursor += sizeof(pos); } + // color data + int attributeTypeColor = gpu::Stream::COLOR; + const gpu::BufferView& colorsBufferView = getAttributeBuffer(attributeTypeColor); + gpu::BufferView::Index numColors = (gpu::BufferView::Index)colorsBufferView.getNumElements(); + + gpu::Resource::Size colorSize = numColors * sizeof(glm::vec3); + unsigned char* resultColorData = new unsigned char[colorSize]; + unsigned char* colorDataCursor = resultColorData; + + for (gpu::BufferView::Index i = 0; i < numColors; i++) { + glm::vec3 color = colorFunc(colorsBufferView.get(i)); + memcpy(colorDataCursor, &color, sizeof(color)); + colorDataCursor += sizeof(color); + } + // normal data int attributeTypeNormal = gpu::Stream::InputSlot::NORMAL; // libraries/gpu/src/gpu/Stream.h const gpu::BufferView& normalsBufferView = getAttributeBuffer(attributeTypeNormal); @@ -187,6 +202,12 @@ model::MeshPointer Mesh::map(std::function vertexFunc, gpu::BufferView resultVertexBufferView(resultVertexBufferPointer, vertexElement); result->setVertexBuffer(resultVertexBufferView); + gpu::Element colorElement = gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::XYZ); + gpu::Buffer* resultColorsBuffer = new gpu::Buffer(colorSize, resultColorData); + gpu::BufferPointer resultColorsBufferPointer(resultColorsBuffer); + gpu::BufferView resultColorsBufferView(resultColorsBufferPointer, colorElement); + result->addAttribute(attributeTypeColor, resultColorsBufferView); + gpu::Element normalElement = gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::XYZ); gpu::Buffer* resultNormalsBuffer = new gpu::Buffer(normalSize, resultNormalData); gpu::BufferPointer resultNormalsBufferPointer(resultNormalsBuffer); @@ -215,6 +236,7 @@ model::MeshPointer Mesh::map(std::function vertexFunc, void Mesh::forEach(std::function vertexFunc, + std::function colorFunc, std::function normalFunc, std::function indexFunc) { // vertex data @@ -224,6 +246,14 @@ void Mesh::forEach(std::function vertexFunc, vertexFunc(vertexBufferView.get(i)); } + // color data + int attributeTypeColor = gpu::Stream::InputSlot::COLOR; // libraries/gpu/src/gpu/Stream.h + const gpu::BufferView& colorsBufferView = getAttributeBuffer(attributeTypeColor); + gpu::BufferView::Index numColors = (gpu::BufferView::Index)colorsBufferView.getNumElements(); + for (gpu::BufferView::Index i = 0; i < numColors; i++) { + colorFunc(colorsBufferView.get(i)); + } + // normal data int attributeTypeNormal = gpu::Stream::InputSlot::NORMAL; // libraries/gpu/src/gpu/Stream.h const gpu::BufferView& normalsBufferView = getAttributeBuffer(attributeTypeNormal); diff --git a/libraries/model/src/model/Geometry.h b/libraries/model/src/model/Geometry.h index a3198eed26..260de313ab 100755 --- a/libraries/model/src/model/Geometry.h +++ b/libraries/model/src/model/Geometry.h @@ -123,10 +123,12 @@ public: // create a copy of this mesh after passing its vertices, normals, and indexes though the provided functions MeshPointer map(std::function vertexFunc, + std::function colorFunc, std::function normalFunc, - std::function indexFunc); + std::function indexFunc) const; void forEach(std::function vertexFunc, + std::function colorFunc, std::function normalFunc, std::function indexFunc); diff --git a/libraries/networking/src/AssetResourceRequest.cpp b/libraries/networking/src/AssetResourceRequest.cpp index f4a3b20fd5..a41283cc0d 100644 --- a/libraries/networking/src/AssetResourceRequest.cpp +++ b/libraries/networking/src/AssetResourceRequest.cpp @@ -187,6 +187,9 @@ void AssetResourceRequest::onDownloadProgress(qint64 bytesReceived, qint64 bytes emit progress(bytesReceived, bytesTotal); auto now = p_high_resolution_clock::now(); + + // Recording ATP bytes downloaded in stats + DependencyManager::get()->updateStat(STAT_ATP_RESOURCE_TOTAL_BYTES, bytesReceived); // if we haven't received the full asset check if it is time to output progress to log // we do so every X seconds to assist with ATP download tracking @@ -201,6 +204,5 @@ void AssetResourceRequest::onDownloadProgress(qint64 bytesReceived, qint64 bytes _lastProgressDebug = now; } - } diff --git a/libraries/networking/src/FileResourceRequest.cpp b/libraries/networking/src/FileResourceRequest.cpp index d0e2721679..26857716e1 100644 --- a/libraries/networking/src/FileResourceRequest.cpp +++ b/libraries/networking/src/FileResourceRequest.cpp @@ -20,7 +20,7 @@ void FileResourceRequest::doSend() { auto statTracker = DependencyManager::get(); statTracker->incrementStat(STAT_FILE_REQUEST_STARTED); - + int fileSize = 0; QString filename = _url.toLocalFile(); // sometimes on windows, we see the toLocalFile() return null, @@ -53,6 +53,7 @@ void FileResourceRequest::doSend() { } _result = ResourceRequest::Success; + fileSize = file.size(); } } else { @@ -68,6 +69,8 @@ void FileResourceRequest::doSend() { if (_result == ResourceRequest::Success) { statTracker->incrementStat(STAT_FILE_REQUEST_SUCCESS); + // Recording FILE bytes downloaded in stats + statTracker->updateStat(STAT_FILE_RESOURCE_TOTAL_BYTES,fileSize); } else { statTracker->incrementStat(STAT_FILE_REQUEST_FAILED); } diff --git a/libraries/networking/src/HTTPResourceRequest.cpp b/libraries/networking/src/HTTPResourceRequest.cpp index 266ea429a0..c6d0370a70 100644 --- a/libraries/networking/src/HTTPResourceRequest.cpp +++ b/libraries/networking/src/HTTPResourceRequest.cpp @@ -201,6 +201,11 @@ void HTTPResourceRequest::onDownloadProgress(qint64 bytesReceived, qint64 bytesT _sendTimer->start(); emit progress(bytesReceived, bytesTotal); + + // Recording HTTP bytes downloaded in stats + DependencyManager::get()->updateStat(STAT_HTTP_RESOURCE_TOTAL_BYTES, bytesReceived); + + } void HTTPResourceRequest::onTimeout() { diff --git a/libraries/networking/src/NodeList.cpp b/libraries/networking/src/NodeList.cpp index 262f0318b6..48e5c8a62c 100644 --- a/libraries/networking/src/NodeList.cpp +++ b/libraries/networking/src/NodeList.cpp @@ -241,10 +241,6 @@ void NodeList::reset() { _numNoReplyDomainCheckIns = 0; - // lock and clear our set of radius ignored IDs - _radiusIgnoredSetLock.lockForWrite(); - _radiusIgnoredNodeIDs.clear(); - _radiusIgnoredSetLock.unlock(); // lock and clear our set of ignored IDs _ignoredSetLock.lockForWrite(); _ignoredNodeIDs.clear(); @@ -331,7 +327,7 @@ void NodeList::sendDomainServerCheckIn() { << "but no keypair is present. Waiting for keypair generation to complete."; accountManager->generateNewUserKeypair(); - // don't send the check in packet - wait for the keypair first + // don't send the check in packet - wait for the new public key to be available to the domain-server first return; } @@ -810,22 +806,6 @@ void NodeList::sendIgnoreRadiusStateToNode(const SharedNodePointer& destinationN sendPacket(std::move(ignorePacket), *destinationNode); } -void NodeList::radiusIgnoreNodeBySessionID(const QUuid& nodeID, bool radiusIgnoreEnabled) { - if (radiusIgnoreEnabled) { - QReadLocker radiusIgnoredSetLocker{ &_radiusIgnoredSetLock }; // read lock for insert - // add this nodeID to our set of ignored IDs - _radiusIgnoredNodeIDs.insert(nodeID); - } else { - QWriteLocker radiusIgnoredSetLocker{ &_radiusIgnoredSetLock }; // write lock for unsafe_erase - _radiusIgnoredNodeIDs.unsafe_erase(nodeID); - } -} - -bool NodeList::isRadiusIgnoringNode(const QUuid& nodeID) const { - QReadLocker radiusIgnoredSetLocker{ &_radiusIgnoredSetLock }; // read lock for reading - return _radiusIgnoredNodeIDs.find(nodeID) != _radiusIgnoredNodeIDs.cend(); -} - void NodeList::ignoreNodeBySessionID(const QUuid& nodeID, bool ignoreEnabled) { // enumerate the nodes to send a reliable ignore packet to each that can leverage it if (!nodeID.isNull() && _sessionUUID != nodeID) { @@ -950,7 +930,7 @@ void NodeList::maybeSendIgnoreSetToNode(SharedNodePointer newNode) { if (_personalMutedNodeIDs.size() > 0) { // setup a packet list so we can send the stream of ignore IDs - auto personalMutePacketList = NLPacketList::create(PacketType::NodeIgnoreRequest, QByteArray(), true); + auto personalMutePacketList = NLPacketList::create(PacketType::NodeIgnoreRequest, QByteArray(), true, true); // Force the "enabled" flag in this packet to true personalMutePacketList->writePrimitive(true); @@ -977,7 +957,7 @@ void NodeList::maybeSendIgnoreSetToNode(SharedNodePointer newNode) { if (_ignoredNodeIDs.size() > 0) { // setup a packet list so we can send the stream of ignore IDs - auto ignorePacketList = NLPacketList::create(PacketType::NodeIgnoreRequest, QByteArray(), true); + auto ignorePacketList = NLPacketList::create(PacketType::NodeIgnoreRequest, QByteArray(), true, true); // Force the "enabled" flag in this packet to true ignorePacketList->writePrimitive(true); diff --git a/libraries/networking/src/NodeList.h b/libraries/networking/src/NodeList.h index 6db760b3ca..b3a12153e5 100644 --- a/libraries/networking/src/NodeList.h +++ b/libraries/networking/src/NodeList.h @@ -77,8 +77,6 @@ public: void toggleIgnoreRadius() { ignoreNodesInRadius(!getIgnoreRadiusEnabled()); } void enableIgnoreRadius() { ignoreNodesInRadius(true); } void disableIgnoreRadius() { ignoreNodesInRadius(false); } - void radiusIgnoreNodeBySessionID(const QUuid& nodeID, bool radiusIgnoreEnabled); - bool isRadiusIgnoringNode(const QUuid& other) const; void ignoreNodeBySessionID(const QUuid& nodeID, bool ignoreEnabled); bool isIgnoringNode(const QUuid& nodeID) const; void personalMuteNodeBySessionID(const QUuid& nodeID, bool muteEnabled); @@ -166,8 +164,6 @@ private: QTimer _keepAlivePingTimer; bool _requestsDomainListData; - mutable QReadWriteLock _radiusIgnoredSetLock; - tbb::concurrent_unordered_set _radiusIgnoredNodeIDs; mutable QReadWriteLock _ignoredSetLock; tbb::concurrent_unordered_set _ignoredNodeIDs; mutable QReadWriteLock _personalMutedSetLock; diff --git a/libraries/networking/src/ResourceCache.cpp b/libraries/networking/src/ResourceCache.cpp index 43f0e9335d..fbdfa4b87a 100644 --- a/libraries/networking/src/ResourceCache.cpp +++ b/libraries/networking/src/ResourceCache.cpp @@ -102,6 +102,8 @@ QSharedPointer ResourceCacheSharedItems::getHighestPendingRequest() { QSharedPointer highestResource; Lock lock(_mutex); + bool currentHighestIsFile = false; + for (int i = 0; i < _pendingRequests.size();) { // Clear any freed resources auto resource = _pendingRequests.at(i).lock(); @@ -112,10 +114,12 @@ QSharedPointer ResourceCacheSharedItems::getHighestPendingRequest() { // Check load priority float priority = resource->getLoadPriority(); - if (priority >= highestPriority) { + bool isFile = resource->getURL().scheme() == URL_SCHEME_FILE; + if (priority >= highestPriority && (isFile || !currentHighestIsFile)) { highestPriority = priority; highestIndex = i; highestResource = resource; + currentHighestIsFile = isFile; } i++; } diff --git a/libraries/networking/src/ResourceRequest.h b/libraries/networking/src/ResourceRequest.h index 39bcb3fe93..3ee86025a2 100644 --- a/libraries/networking/src/ResourceRequest.h +++ b/libraries/networking/src/ResourceRequest.h @@ -33,6 +33,9 @@ const QString STAT_HTTP_REQUEST_CACHE = "CacheHTTPRequest"; const QString STAT_ATP_MAPPING_REQUEST_STARTED = "StartedATPMappingRequest"; const QString STAT_ATP_MAPPING_REQUEST_FAILED = "FailedATPMappingRequest"; const QString STAT_ATP_MAPPING_REQUEST_SUCCESS = "SuccessfulATPMappingRequest"; +const QString STAT_HTTP_RESOURCE_TOTAL_BYTES = "HTTPBytesDownloaded"; +const QString STAT_ATP_RESOURCE_TOTAL_BYTES = "ATPBytesDownloaded"; +const QString STAT_FILE_RESOURCE_TOTAL_BYTES = "FILEBytesDownloaded"; class ResourceRequest : public QObject { Q_OBJECT diff --git a/libraries/networking/src/udt/PacketHeaders.cpp b/libraries/networking/src/udt/PacketHeaders.cpp index 240697d890..d2500196d9 100644 --- a/libraries/networking/src/udt/PacketHeaders.cpp +++ b/libraries/networking/src/udt/PacketHeaders.cpp @@ -62,7 +62,7 @@ PacketVersion versionForPacketType(PacketType packetType) { case PacketType::EntityEdit: case PacketType::EntityData: case PacketType::EntityPhysics: - return VERSION_ENTITIES_BULLET_DYNAMICS; + return VERSION_ENTITIES_HAS_SHOULD_HIGHLIGHT; case PacketType::EntityQuery: return static_cast(EntityQueryPacketVersion::JSONFilterWithFamilyTree); case PacketType::AvatarIdentity: diff --git a/libraries/networking/src/udt/PacketHeaders.h b/libraries/networking/src/udt/PacketHeaders.h index 6c42193e11..cb3db791b4 100644 --- a/libraries/networking/src/udt/PacketHeaders.h +++ b/libraries/networking/src/udt/PacketHeaders.h @@ -105,7 +105,6 @@ public: UsernameFromIDReply, ViewFrustum, RequestsDomainListData, - ExitingSpaceBubble, PerAvatarGainSet, EntityScriptGetStatus, EntityScriptGetStatusReply, @@ -219,6 +218,7 @@ const PacketVersion VERSION_ENTITIES_PHYSICS_PACKET = 67; const PacketVersion VERSION_ENTITIES_ZONE_FILTERS = 68; const PacketVersion VERSION_ENTITIES_HINGE_CONSTRAINT = 69; const PacketVersion VERSION_ENTITIES_BULLET_DYNAMICS = 70; +const PacketVersion VERSION_ENTITIES_HAS_SHOULD_HIGHLIGHT = 71; enum class EntityQueryPacketVersion: PacketVersion { JSONFilter = 18, diff --git a/libraries/octree/src/Octree.cpp b/libraries/octree/src/Octree.cpp index 2e93f3515f..cceecaeed9 100644 --- a/libraries/octree/src/Octree.cpp +++ b/libraries/octree/src/Octree.cpp @@ -29,8 +29,12 @@ #include #include #include +#include +#include #include #include +#include +#include #include #include @@ -1667,7 +1671,28 @@ bool Octree::readJSONFromGzippedFile(QString qFileName) { return readJSONFromStream(-1, jsonStream); } +// hack to get the marketplace id into the entities. We will create a way to get this from a hash of +// the entity later, but this helps us move things along for now +QString getMarketplaceID(const QString& urlString) { + // the url should be http://mpassets.highfidelity.com/-v1/.extension + // a regex for the this is a PITA as there are several valid versions of uuids, and so + // lets strip out the uuid (if any) and try to create a UUID from the string, relying on + // QT to parse it + static const QRegularExpression re("^http:\\/\\/mpassets.highfidelity.com\\/([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})-v[\\d]+\\/.*"); + QRegularExpressionMatch match = re.match(urlString); + if (match.hasMatch()) { + QString matched = match.captured(1); + if (QUuid(matched).isNull()) { + qDebug() << "invalid uuid for marketplaceID"; + } else { + return matched; + } + } + return QString(); +} + bool Octree::readFromURL(const QString& urlString) { + QString marketplaceID = getMarketplaceID(urlString); auto request = std::unique_ptr(DependencyManager::get()->createResourceRequest(this, urlString)); @@ -1686,11 +1711,11 @@ bool Octree::readFromURL(const QString& urlString) { auto data = request->getData(); QDataStream inputStream(data); - return readFromStream(data.size(), inputStream); + return readFromStream(data.size(), inputStream, marketplaceID); } -bool Octree::readFromStream(unsigned long streamLength, QDataStream& inputStream) { +bool Octree::readFromStream(unsigned long streamLength, QDataStream& inputStream, const QString& marketplaceID) { // decide if this is binary SVO or JSON-formatted SVO QIODevice *device = inputStream.device(); char firstChar; @@ -1702,7 +1727,7 @@ bool Octree::readFromStream(unsigned long streamLength, QDataStream& inputStream return readSVOFromStream(streamLength, inputStream); } else { qCDebug(octree) << "Reading from JSON SVO Stream length:" << streamLength; - return readJSONFromStream(streamLength, inputStream); + return readJSONFromStream(streamLength, inputStream, marketplaceID); } } @@ -1838,9 +1863,31 @@ bool Octree::readSVOFromStream(unsigned long streamLength, QDataStream& inputStr return fileOk; } +// hack to get the marketplace id into the entities. We will create a way to get this from a hash of +// the entity later, but this helps us move things along for now +QJsonDocument addMarketplaceIDToDocumentEntities(QJsonDocument& doc, const QString& marketplaceID) { + if (!marketplaceID.isEmpty()) { + QJsonDocument newDoc; + QJsonObject rootObj = doc.object(); + QJsonArray newEntitiesArray; + + // build a new entities array + auto entitiesArray = rootObj["Entities"].toArray(); + for(auto it = entitiesArray.begin(); it != entitiesArray.end(); it++) { + auto entity = (*it).toObject(); + entity["marketplaceID"] = marketplaceID; + newEntitiesArray.append(entity); + } + rootObj["Entities"] = newEntitiesArray; + newDoc.setObject(rootObj); + return newDoc; + } + return doc; +} + const int READ_JSON_BUFFER_SIZE = 2048; -bool Octree::readJSONFromStream(unsigned long streamLength, QDataStream& inputStream) { +bool Octree::readJSONFromStream(unsigned long streamLength, QDataStream& inputStream, const QString& marketplaceID /*=""*/) { // if the data is gzipped we may not have a useful bytesAvailable() result, so just keep reading until // we get an eof. Leave streamLength parameter for consistency. @@ -1860,6 +1907,9 @@ bool Octree::readJSONFromStream(unsigned long streamLength, QDataStream& inputSt } QJsonDocument asDocument = QJsonDocument::fromJson(jsonBuffer); + if (!marketplaceID.isEmpty()) { + asDocument = addMarketplaceIDToDocumentEntities(asDocument, marketplaceID); + } QVariant asVariant = asDocument.toVariant(); QVariantMap asMap = asVariant.toMap(); bool success = readFromMap(asMap); diff --git a/libraries/octree/src/Octree.h b/libraries/octree/src/Octree.h index 512a0ab64e..38454d20b5 100644 --- a/libraries/octree/src/Octree.h +++ b/libraries/octree/src/Octree.h @@ -212,14 +212,14 @@ public: virtual bool handlesEditPacketType(PacketType packetType) const { return false; } virtual int processEditPacketData(ReceivedMessage& message, const unsigned char* editData, int maxLength, const SharedNodePointer& sourceNode) { return 0; } - + virtual bool recurseChildrenWithData() const { return true; } virtual bool rootElementHasData() const { return false; } virtual int minimumRequiredRootDataBytes() const { return 0; } virtual bool suppressEmptySubtrees() const { return true; } virtual void releaseSceneEncodeData(OctreeElementExtraEncodeData* extraEncodeData) const { } virtual bool mustIncludeAllChildData() const { return true; } - + /// some versions of the SVO file will include breaks with buffer lengths between each buffer chunk in the SVO /// file. If the Octree subclass expects this for this particular version of the file, it should override this /// method and return true. @@ -236,15 +236,15 @@ public: void reaverageOctreeElements(OctreeElementPointer startElement = OctreeElementPointer()); void deleteOctreeElementAt(float x, float y, float z, float s); - + /// Find the voxel at position x,y,z,s /// \return pointer to the OctreeElement or NULL if none at x,y,z,s. OctreeElementPointer getOctreeElementAt(float x, float y, float z, float s) const; - + /// Find the voxel at position x,y,z,s /// \return pointer to the OctreeElement or to the smallest enclosing parent if none at x,y,z,s. OctreeElementPointer getOctreeEnclosingElementAt(float x, float y, float z, float s) const; - + OctreeElementPointer getOrCreateChildElementAt(float x, float y, float z, float s); OctreeElementPointer getOrCreateChildElementContaining(const AACube& box); @@ -261,7 +261,7 @@ public: int encodeTreeBitstream(const OctreeElementPointer& element, OctreePacketData* packetData, OctreeElementBag& bag, EncodeBitstreamParams& params) ; - + bool isDirty() const { return _isDirty; } void clearDirtyBit() { _isDirty = false; } void setDirtyBit() { _isDirty = true; } @@ -301,9 +301,9 @@ public: // Octree importers bool readFromFile(const char* filename); bool readFromURL(const QString& url); // will support file urls as well... - bool readFromStream(unsigned long streamLength, QDataStream& inputStream); + bool readFromStream(unsigned long streamLength, QDataStream& inputStream, const QString& marketplaceID=""); bool readSVOFromStream(unsigned long streamLength, QDataStream& inputStream); - bool readJSONFromStream(unsigned long streamLength, QDataStream& inputStream); + bool readJSONFromStream(unsigned long streamLength, QDataStream& inputStream, const QString& marketplaceID=""); bool readJSONFromGzippedFile(QString qFileName); virtual bool readFromMap(QVariantMap& entityDescription) = 0; diff --git a/libraries/physics/src/CharacterController.cpp b/libraries/physics/src/CharacterController.cpp index bd4d1201c7..9a7abc4e98 100755 --- a/libraries/physics/src/CharacterController.cpp +++ b/libraries/physics/src/CharacterController.cpp @@ -445,7 +445,7 @@ void CharacterController::handleChangedCollisionGroup() { void CharacterController::updateUpAxis(const glm::quat& rotation) { _currentUp = quatRotate(glmToBullet(rotation), LOCAL_UP_AXIS); - if (_state != State::Hover && _rigidBody) { + if (_rigidBody) { _rigidBody->setGravity(_gravity * _currentUp); } } diff --git a/libraries/physics/src/EntityMotionState.cpp b/libraries/physics/src/EntityMotionState.cpp index ecfb2b55e4..f02dcee8f6 100644 --- a/libraries/physics/src/EntityMotionState.cpp +++ b/libraries/physics/src/EntityMotionState.cpp @@ -484,11 +484,7 @@ bool EntityMotionState::shouldSendUpdate(uint32_t simulationStep) { return false; } - if (_entity->dynamicDataNeedsTransmit()) { - return true; - } - - if (_entity->queryAABoxNeedsUpdate()) { + if (_entity->dynamicDataNeedsTransmit() || _entity->queryAACubeNeedsUpdate()) { return true; } @@ -577,9 +573,11 @@ void EntityMotionState::sendUpdate(OctreeEditPacketSender* packetSender, uint32_ properties.setActionData(_serverActionData); } - if (properties.parentRelatedPropertyChanged() && _entity->computePuffedQueryAACube()) { - // due to parenting, the server may not know where something is in world-space, so include the bounding cube. - properties.setQueryAACube(_entity->getQueryAACube()); + if (properties.transformChanged()) { + if (_entity->checkAndMaybeUpdateQueryAACube()) { + // due to parenting, the server may not know where something is in world-space, so include the bounding cube. + properties.setQueryAACube(_entity->getQueryAACube()); + } } // set the LastEdited of the properties but NOT the entity itself @@ -643,7 +641,7 @@ void EntityMotionState::sendUpdate(OctreeEditPacketSender* packetSender, uint32_ _entity->forEachDescendant([&](SpatiallyNestablePointer descendant) { if (descendant->getNestableType() == NestableType::Entity) { EntityItemPointer entityDescendant = std::static_pointer_cast(descendant); - if (descendant->computePuffedQueryAACube()) { + if (descendant->checkAndMaybeUpdateQueryAACube()) { EntityItemProperties newQueryCubeProperties; newQueryCubeProperties.setQueryAACube(descendant->getQueryAACube()); newQueryCubeProperties.setLastEdited(properties.getLastEdited()); diff --git a/libraries/physics/src/ShapeFactory.cpp b/libraries/physics/src/ShapeFactory.cpp index d209667966..97174b2216 100644 --- a/libraries/physics/src/ShapeFactory.cpp +++ b/libraries/physics/src/ShapeFactory.cpp @@ -17,7 +17,7 @@ #include "BulletUtil.h" // These are the same normalized directions used by the btShapeHull class. -// 12 points for the face centers of a duodecohedron plus another 30 points +// 12 points for the face centers of a dodecahedron plus another 30 points // for the midpoints the edges, for a total of 42. const uint32_t NUM_UNIT_SPHERE_DIRECTIONS = 42; static const btVector3 _unitSphereDirections[NUM_UNIT_SPHERE_DIRECTIONS] = { @@ -288,6 +288,38 @@ const btCollisionShape* ShapeFactory::createShapeFromInfo(const ShapeInfo& info) shape = new btCapsuleShape(radius, height); } break; + case SHAPE_TYPE_CAPSULE_X: { + glm::vec3 halfExtents = info.getHalfExtents(); + float radius = halfExtents.y; + float height = 2.0f * halfExtents.x; + shape = new btCapsuleShapeX(radius, height); + } + break; + case SHAPE_TYPE_CAPSULE_Z: { + glm::vec3 halfExtents = info.getHalfExtents(); + float radius = halfExtents.x; + float height = 2.0f * halfExtents.z; + shape = new btCapsuleShapeZ(radius, height); + } + break; + case SHAPE_TYPE_CYLINDER_X: { + const glm::vec3 halfExtents = info.getHalfExtents(); + const btVector3 btHalfExtents(halfExtents.x, halfExtents.y, halfExtents.z); + shape = new btCylinderShapeX(btHalfExtents); + } + break; + case SHAPE_TYPE_CYLINDER_Z: { + const glm::vec3 halfExtents = info.getHalfExtents(); + const btVector3 btHalfExtents(halfExtents.x, halfExtents.y, halfExtents.z); + shape = new btCylinderShapeZ(btHalfExtents); + } + break; + case SHAPE_TYPE_CYLINDER_Y: { + const glm::vec3 halfExtents = info.getHalfExtents(); + const btVector3 btHalfExtents(halfExtents.x, halfExtents.y, halfExtents.z); + shape = new btCylinderShape(btHalfExtents); + } + break; case SHAPE_TYPE_COMPOUND: case SHAPE_TYPE_SIMPLE_HULL: { const ShapeInfo::PointCollection& pointCollection = info.getPointCollection(); diff --git a/libraries/plugins/src/plugins/DisplayPlugin.cpp b/libraries/plugins/src/plugins/DisplayPlugin.cpp index 747c72c08e..20c72159c4 100644 --- a/libraries/plugins/src/plugins/DisplayPlugin.cpp +++ b/libraries/plugins/src/plugins/DisplayPlugin.cpp @@ -18,6 +18,19 @@ void DisplayPlugin::incrementPresentCount() { ++_presentedFrameIndex; - // Alert the app that it needs to paint a new presentation frame - qApp->postEvent(qApp, new QEvent(static_cast(Present)), Qt::HighEventPriority); + { + QMutexLocker locker(&_presentMutex); + _presentCondition.wakeAll(); + } + + emit presented(_presentedFrameIndex); } + +void DisplayPlugin::waitForPresent() { + QMutexLocker locker(&_presentMutex); + while (isActive()) { + if (_presentCondition.wait(&_presentMutex, MSECS_PER_SECOND)) { + break; + } + } +} \ No newline at end of file diff --git a/libraries/plugins/src/plugins/DisplayPlugin.h b/libraries/plugins/src/plugins/DisplayPlugin.h index 7bfdbddbc5..9e18ee534d 100644 --- a/libraries/plugins/src/plugins/DisplayPlugin.h +++ b/libraries/plugins/src/plugins/DisplayPlugin.h @@ -17,14 +17,17 @@ #include #include #include +#include +#include #include #include #include #include - #include "Plugin.h" +class QOpenGLFramebufferObject; + class QImage; enum Eye { @@ -60,8 +63,12 @@ namespace gpu { using TexturePointer = std::shared_ptr; } +class NetworkTexture; +using NetworkTexturePointer = QSharedPointer; +typedef struct __GLsync *GLsync; + // Stereo display functionality -// TODO move out of this file don't derive DisplayPlugin from this. Instead use dynamic casting when +// TODO move out of this file don't derive DisplayPlugin from this. Instead use dynamic casting when // displayPlugin->isStereo returns true class StereoDisplay { public: @@ -78,7 +85,7 @@ public: }; // HMD display functionality -// TODO move out of this file don't derive DisplayPlugin from this. Instead use dynamic casting when +// TODO move out of this file don't derive DisplayPlugin from this. Instead use dynamic casting when // displayPlugin->isHmd returns true class HmdDisplay : public StereoDisplay { public: @@ -129,10 +136,6 @@ class DisplayPlugin : public Plugin, public HmdDisplay { Q_OBJECT using Parent = Plugin; public: - enum Event { - Present = QEvent::User + 1 - }; - virtual int getRequiredThreadCount() const { return 0; } virtual bool isHmd() const { return false; } virtual int getHmdScreen() const { return -1; } @@ -142,7 +145,7 @@ public: virtual float getTargetFrameRate() const { return 1.0f; } virtual bool hasAsyncReprojection() const { return false; } - /// Returns a boolean value indicating whether the display is currently visible + /// Returns a boolean value indicating whether the display is currently visible /// to the user. For monitor displays, false might indicate that a screensaver, /// or power-save mode is active. For HMDs it may reflect a sensor indicating /// whether the HMD is being worn @@ -204,22 +207,27 @@ public: // Rate at which rendered frames are being skipped virtual float droppedFrameRate() const { return -1.0f; } virtual bool getSupportsAutoSwitch() { return false; } - + // Hardware specific stats virtual QJsonObject getHardwareStats() const { return QJsonObject(); } + virtual void copyTextureToQuickFramebuffer(NetworkTexturePointer source, QOpenGLFramebufferObject* target, GLsync* fenceSync) = 0; + uint32_t presentCount() const { return _presentedFrameIndex; } // Time since last call to incrementPresentCount (only valid if DEBUG_PAINT_DELAY is defined) int64_t getPaintDelayUsecs() const; virtual void cycleDebugOutput() {} + void waitForPresent(); + static const QString& MENU_PATH(); signals: void recommendedFramebufferSizeChanged(const QSize& size); void resetSensorsRequested(); + void presented(quint32 frame); protected: void incrementPresentCount(); @@ -227,6 +235,8 @@ protected: gpu::ContextPointer _gpuContext; private: + QMutex _presentMutex; + QWaitCondition _presentCondition; std::atomic _presentedFrameIndex; mutable std::mutex _paintDelayMutex; QElapsedTimer _paintDelayTimer; diff --git a/libraries/render-utils/src/GeometryCache.cpp b/libraries/render-utils/src/GeometryCache.cpp index 49b844efb0..5f2acff16f 100644 --- a/libraries/render-utils/src/GeometryCache.cpp +++ b/libraries/render-utils/src/GeometryCache.cpp @@ -29,6 +29,7 @@ #include "gpu/StandardShaderLib.h" #include "model/TextureMap.h" +#include "render/Args.h" #include "standardTransformPNTC_vert.h" #include "standardDrawTexture_frag.h" @@ -459,7 +460,8 @@ _nextID(0) { // Set the defaults needed for a simple program batch.setResourceTexture(render::ShapePipeline::Slot::MAP::ALBEDO, DependencyManager::get()->getWhiteTexture()); - } + }, + nullptr ); GeometryCache::_simpleTransparentPipeline = std::make_shared(getSimplePipeline(false, true, true, false), nullptr, @@ -467,11 +469,12 @@ _nextID(0) { // Set the defaults needed for a simple program batch.setResourceTexture(render::ShapePipeline::Slot::MAP::ALBEDO, DependencyManager::get()->getWhiteTexture()); - } + }, + nullptr ); GeometryCache::_simpleWirePipeline = std::make_shared(getSimplePipeline(false, false, true, true), nullptr, - [](const render::ShapePipeline&, gpu::Batch& batch, RenderArgs* args) {}); + [](const render::ShapePipeline&, gpu::Batch& batch, RenderArgs* args) {}, nullptr); } GeometryCache::~GeometryCache() { diff --git a/libraries/render-utils/src/Model.cpp b/libraries/render-utils/src/Model.cpp index 45be09b701..42bb91ce94 100644 --- a/libraries/render-utils/src/Model.cpp +++ b/libraries/render-utils/src/Model.cpp @@ -24,6 +24,7 @@ #include #include #include +#include #include "AbstractViewStateInterface.h" #include "MeshPartPayload.h" @@ -462,6 +463,41 @@ bool Model::convexHullContains(glm::vec3 point) { return false; } +MeshProxyList Model::getMeshes() const { + MeshProxyList result; + const Geometry::Pointer& renderGeometry = getGeometry(); + const Geometry::GeometryMeshes& meshes = renderGeometry->getMeshes(); + + if (!isLoaded()) { + return result; + } + + Transform offset; + offset.setScale(_scale); + offset.postTranslate(_offset); + glm::mat4 offsetMat = offset.getMatrix(); + + for (std::shared_ptr mesh : meshes) { + if (!mesh) { + continue; + } + + MeshProxy* meshProxy = new SimpleMeshProxy( + mesh->map( + [=](glm::vec3 position) { + return glm::vec3(offsetMat * glm::vec4(position, 1.0f)); + }, + [=](glm::vec3 color) { return color; }, + [=](glm::vec3 normal) { + return glm::normalize(glm::vec3(offsetMat * glm::vec4(normal, 0.0f))); + }, + [&](uint32_t index) { return index; })); + result << meshProxy; + } + + return result; +} + void Model::calculateTriangleSets() { PROFILE_RANGE(render, __FUNCTION__); diff --git a/libraries/render-utils/src/Model.h b/libraries/render-utils/src/Model.h index 3eb796b763..497346c138 100644 --- a/libraries/render-utils/src/Model.h +++ b/libraries/render-utils/src/Model.h @@ -257,6 +257,8 @@ public: int getResourceDownloadAttempts() { return _renderWatcher.getResourceDownloadAttempts(); } int getResourceDownloadAttemptsRemaining() { return _renderWatcher.getResourceDownloadAttemptsRemaining(); } + Q_INVOKABLE MeshProxyList getMeshes() const; + public slots: void loadURLFinished(bool success); diff --git a/libraries/render-utils/src/SoftAttachmentModel.cpp b/libraries/render-utils/src/SoftAttachmentModel.cpp index f15b696006..63b18d49b8 100644 --- a/libraries/render-utils/src/SoftAttachmentModel.cpp +++ b/libraries/render-utils/src/SoftAttachmentModel.cpp @@ -35,6 +35,10 @@ void SoftAttachmentModel::updateClusterMatrices() { if (!_needsUpdateClusterMatrices) { return; } + if (!isLoaded()) { + return; + } + _needsUpdateClusterMatrices = false; const FBXGeometry& geometry = getFBXGeometry(); diff --git a/libraries/render/src/render/Args.h b/libraries/render/src/render/Args.h index 449a3ac22b..6a91081c95 100644 --- a/libraries/render/src/render/Args.h +++ b/libraries/render/src/render/Args.h @@ -63,6 +63,12 @@ namespace render { public: enum RenderMode { DEFAULT_RENDER_MODE, SHADOW_RENDER_MODE, DIFFUSE_RENDER_MODE, NORMAL_RENDER_MODE, MIRROR_RENDER_MODE, SECONDARY_CAMERA_RENDER_MODE }; enum DisplayMode { MONO, STEREO_MONITOR, STEREO_HMD }; + enum OutlineFlags { + RENDER_OUTLINE_NONE = 0, + RENDER_OUTLINE_WIREFRAMES = 1, + RENDER_OUTLINE_MARKETPLACE_MODE = 2, + RENDER_OUTLINE_SHADER = 4 + }; enum DebugFlags { RENDER_DEBUG_NONE = 0, RENDER_DEBUG_HULLS = 1 @@ -71,7 +77,6 @@ namespace render { Args() {} Args(const gpu::ContextPointer& context, - QSharedPointer renderData = QSharedPointer(nullptr), float sizeScale = 1.0f, int boundaryLevelAdjust = 0, RenderMode renderMode = DEFAULT_RENDER_MODE, @@ -79,7 +84,6 @@ namespace render { DebugFlags debugFlags = RENDER_DEBUG_NONE, gpu::Batch* batch = nullptr) : _context(context), - _renderData(renderData), _sizeScale(sizeScale), _boundaryLevelAdjust(boundaryLevelAdjust), _renderMode(renderMode), @@ -104,7 +108,6 @@ namespace render { std::shared_ptr _context; std::shared_ptr _blitFramebuffer; std::shared_ptr _shapePipeline; - QSharedPointer _renderData; std::stack _viewFrustums; glm::ivec4 _viewport { 0.0f, 0.0f, 1.0f, 1.0f }; glm::vec3 _boomOffset { 0.0f, 0.0f, 1.0f }; @@ -112,6 +115,7 @@ namespace render { int _boundaryLevelAdjust { 0 }; RenderMode _renderMode { DEFAULT_RENDER_MODE }; DisplayMode _displayMode { MONO }; + OutlineFlags _outlineFlags{ RENDER_OUTLINE_NONE }; DebugFlags _debugFlags { RENDER_DEBUG_NONE }; gpu::Batch* _batch = nullptr; diff --git a/libraries/render/src/render/DrawTask.cpp b/libraries/render/src/render/DrawTask.cpp index 1153a737a1..a3175ffdec 100755 --- a/libraries/render/src/render/DrawTask.cpp +++ b/libraries/render/src/render/DrawTask.cpp @@ -45,8 +45,9 @@ void renderShape(RenderArgs* args, const ShapePlumberPointer& shapeContext, cons if (key.isValid() && !key.hasOwnPipeline()) { args->_shapePipeline = shapeContext->pickPipeline(args, key); if (args->_shapePipeline) { + args->_shapePipeline->prepareShapeItem(args, key, item); item.render(args); - } + } args->_shapePipeline = nullptr; } else if (key.hasOwnPipeline()) { item.render(args); @@ -110,10 +111,11 @@ void render::renderStateSortShapes(const RenderContextPointer& renderContext, for (auto& pipelineKey : sortedPipelines) { auto& bucket = sortedShapes[pipelineKey]; args->_shapePipeline = shapeContext->pickPipeline(args, pipelineKey); - if (!args->_shapePipeline) { + if (!args->_shapePipeline) { continue; } for (auto& item : bucket) { + args->_shapePipeline->prepareShapeItem(args, pipelineKey, item); item.render(args); } } diff --git a/libraries/render/src/render/ShapePipeline.cpp b/libraries/render/src/render/ShapePipeline.cpp index 79ac5c0990..c83c0b44fc 100644 --- a/libraries/render/src/render/ShapePipeline.cpp +++ b/libraries/render/src/render/ShapePipeline.cpp @@ -17,18 +17,34 @@ using namespace render; +ShapePipeline::CustomFactoryMap ShapePipeline::_globalCustomFactoryMap; + +ShapePipeline::CustomKey ShapePipeline::registerCustomShapePipelineFactory(CustomFactory factory) { + ShapePipeline::CustomKey custom = (ShapePipeline::CustomKey) _globalCustomFactoryMap.size() + 1; + _globalCustomFactoryMap[custom] = factory; + return custom; +} + + void ShapePipeline::prepare(gpu::Batch& batch, RenderArgs* args) { - if (batchSetter) { - batchSetter(*this, batch, args); + if (_batchSetter) { + _batchSetter(*this, batch, args); } } +void ShapePipeline::prepareShapeItem(RenderArgs* args, const ShapeKey& key, const Item& shape) { + if (_itemSetter) { + _itemSetter(*this, args, shape); + } +} + + ShapeKey::Filter::Builder::Builder() { _mask.set(OWN_PIPELINE); _mask.set(INVALID); } -void ShapePlumber::addPipelineHelper(const Filter& filter, ShapeKey key, int bit, const PipelinePointer& pipeline) { +void ShapePlumber::addPipelineHelper(const Filter& filter, ShapeKey key, int bit, const PipelinePointer& pipeline) const { // Iterate over all keys if (bit < (int)ShapeKey::FlagBit::NUM_FLAGS) { addPipelineHelper(filter, key, bit + 1, pipeline); @@ -48,12 +64,12 @@ void ShapePlumber::addPipelineHelper(const Filter& filter, ShapeKey key, int bit } void ShapePlumber::addPipeline(const Key& key, const gpu::ShaderPointer& program, const gpu::StatePointer& state, - BatchSetter batchSetter) { - addPipeline(Filter{key}, program, state, batchSetter); + BatchSetter batchSetter, ItemSetter itemSetter) { + addPipeline(Filter{key}, program, state, batchSetter, itemSetter); } void ShapePlumber::addPipeline(const Filter& filter, const gpu::ShaderPointer& program, const gpu::StatePointer& state, - BatchSetter batchSetter) { + BatchSetter batchSetter, ItemSetter itemSetter) { gpu::Shader::BindingSet slotBindings; slotBindings.insert(gpu::Shader::Binding(std::string("lightingModelBuffer"), Slot::BUFFER::LIGHTING_MODEL)); slotBindings.insert(gpu::Shader::Binding(std::string("skinClusterBuffer"), Slot::BUFFER::SKINNING)); @@ -90,7 +106,7 @@ void ShapePlumber::addPipeline(const Filter& filter, const gpu::ShaderPointer& p ShapeKey key{filter._flags}; auto gpuPipeline = gpu::Pipeline::create(program, state); - auto shapePipeline = std::make_shared(gpuPipeline, locations, batchSetter); + auto shapePipeline = std::make_shared(gpuPipeline, locations, batchSetter, itemSetter); addPipelineHelper(filter, key, 0, shapePipeline); } @@ -103,23 +119,34 @@ const ShapePipelinePointer ShapePlumber::pickPipeline(RenderArgs* args, const Ke const auto& pipelineIterator = _pipelineMap.find(key); if (pipelineIterator == _pipelineMap.end()) { - // The first time we can't find a pipeline, we should log it + // The first time we can't find a pipeline, we should try things to solve that if (_missingKeys.find(key) == _missingKeys.end()) { - _missingKeys.insert(key); - qCDebug(renderlogging) << "Couldn't find a pipeline for" << key; + if (key.isCustom()) { + auto factoryIt = ShapePipeline::_globalCustomFactoryMap.find(key.getCustom()); + if ((factoryIt != ShapePipeline::_globalCustomFactoryMap.end()) && (factoryIt)->second) { + // found a factory for the custom key, can now generate a shape pipeline for this case: + addPipelineHelper(Filter(key), key, 0, (factoryIt)->second(*this, key)); + + return pickPipeline(args, key); + } else { + qCDebug(renderlogging) << "ShapePlumber::Couldn't find a custom pipeline factory for " << key.getCustom() << " key is: " << key; + } + } + + _missingKeys.insert(key); + qCDebug(renderlogging) << "ShapePlumber::Couldn't find a pipeline for" << key; } return PipelinePointer(nullptr); } PipelinePointer shapePipeline(pipelineIterator->second); - auto& batch = args->_batch; // Setup the one pipeline (to rule them all) - batch->setPipeline(shapePipeline->pipeline); + args->_batch->setPipeline(shapePipeline->pipeline); // Run the pipeline's BatchSetter on the passed in batch - if (shapePipeline->batchSetter) { - shapePipeline->batchSetter(*shapePipeline, *batch, args); + if (shapePipeline->_batchSetter) { + shapePipeline->_batchSetter(*shapePipeline, *(args->_batch), args); } return shapePipeline; diff --git a/libraries/render/src/render/ShapePipeline.h b/libraries/render/src/render/ShapePipeline.h index 1b97baae7c..13daac2db7 100644 --- a/libraries/render/src/render/ShapePipeline.h +++ b/libraries/render/src/render/ShapePipeline.h @@ -19,6 +19,8 @@ #include "Args.h" namespace render { +class Item; +class ShapePlumber; class ShapeKey { public: @@ -38,7 +40,19 @@ public: OWN_PIPELINE, INVALID, + CUSTOM_0, + CUSTOM_1, + CUSTOM_2, + CUSTOM_3, + CUSTOM_4, + CUSTOM_5, + CUSTOM_6, + CUSTOM_7, + NUM_FLAGS, // Not a valid flag + + CUSTOM_MASK = (0xFF << CUSTOM_0), + }; using Flags = std::bitset; @@ -73,6 +87,8 @@ public: Builder& withOwnPipeline() { _flags.set(OWN_PIPELINE); return (*this); } Builder& invalidate() { _flags.set(INVALID); return (*this); } + Builder& withCustom(uint8_t custom) { _flags &= (~CUSTOM_MASK); _flags |= (custom << CUSTOM_0); return (*this); } + static const ShapeKey ownPipeline() { return Builder().withOwnPipeline(); } static const ShapeKey invalid() { return Builder().invalidate(); } @@ -127,6 +143,9 @@ public: Builder& withCullFace() { _flags.reset(NO_CULL_FACE); _mask.set(NO_CULL_FACE); return (*this); } Builder& withoutCullFace() { _flags.set(NO_CULL_FACE); _mask.set(NO_CULL_FACE); return (*this); } + Builder& withCustom(uint8_t custom) { _flags &= (~CUSTOM_MASK); _flags |= (custom << CUSTOM_0); _mask |= (CUSTOM_MASK); return (*this); } + Builder& withoutCustom() { _flags &= (~CUSTOM_MASK); _mask |= (CUSTOM_MASK); return (*this); } + protected: friend class Filter; Flags _flags{0}; @@ -155,6 +174,9 @@ public: bool hasOwnPipeline() const { return _flags[OWN_PIPELINE]; } bool isValid() const { return !_flags[INVALID]; } + uint8_t getCustom() const { return (_flags.to_ulong() & CUSTOM_MASK) >> CUSTOM_0; } + bool isCustom() const { return (_flags.to_ulong() & CUSTOM_MASK); } + // Comparator for use in stl containers class Hash { public: @@ -240,22 +262,39 @@ public: }; using LocationsPointer = std::shared_ptr; - using BatchSetter = std::function; + using BatchSetter = std::function; - ShapePipeline(gpu::PipelinePointer pipeline, LocationsPointer locations, BatchSetter batchSetter) : - pipeline(pipeline), locations(locations), batchSetter(batchSetter) {} + using ItemSetter = std::function; - // Normally, a pipeline is accessed thorugh pickPipeline. If it needs to be set manually, + ShapePipeline(gpu::PipelinePointer pipeline, LocationsPointer locations, BatchSetter batchSetter, ItemSetter itemSetter) : + pipeline(pipeline), + locations(locations), + _batchSetter(batchSetter), + _itemSetter(itemSetter) {} + + // Normally, a pipeline is accessed through pickPipeline. If it needs to be set manually, // after calling setPipeline this method should be called to prepare the pipeline with default buffers. - void prepare(gpu::Batch& batch, RenderArgs* args); + void prepare(gpu::Batch& batch, Args* args); gpu::PipelinePointer pipeline; std::shared_ptr locations; + void prepareShapeItem(Args* args, const ShapeKey& key, const Item& shape); + protected: friend class ShapePlumber; - BatchSetter batchSetter; + BatchSetter _batchSetter; + ItemSetter _itemSetter; +public: + using CustomKey = uint8_t; + using CustomFactory = std::function (const ShapePlumber& plumber, const ShapeKey& key)>; + using CustomFactoryMap = std::map; + + static CustomFactoryMap _globalCustomFactoryMap; + + static CustomKey registerCustomShapePipelineFactory(CustomFactory factory); + }; using ShapePipelinePointer = std::shared_ptr; @@ -270,22 +309,24 @@ public: using Locations = Pipeline::Locations; using LocationsPointer = Pipeline::LocationsPointer; using BatchSetter = Pipeline::BatchSetter; + using ItemSetter = Pipeline::ItemSetter; void addPipeline(const Key& key, const gpu::ShaderPointer& program, const gpu::StatePointer& state, - BatchSetter batchSetter = nullptr); + BatchSetter batchSetter = nullptr, ItemSetter itemSetter = nullptr); void addPipeline(const Filter& filter, const gpu::ShaderPointer& program, const gpu::StatePointer& state, - BatchSetter batchSetter = nullptr); + BatchSetter batchSetter = nullptr, ItemSetter itemSetter = nullptr); const PipelinePointer pickPipeline(RenderArgs* args, const Key& key) const; protected: - void addPipelineHelper(const Filter& filter, Key key, int bit, const PipelinePointer& pipeline); - PipelineMap _pipelineMap; + void addPipelineHelper(const Filter& filter, Key key, int bit, const PipelinePointer& pipeline) const; + mutable PipelineMap _pipelineMap; private: mutable std::unordered_set _missingKeys; }; + using ShapePlumberPointer = std::shared_ptr; } diff --git a/libraries/script-engine/CMakeLists.txt b/libraries/script-engine/CMakeLists.txt index 0c0004d132..87296b187f 100644 --- a/libraries/script-engine/CMakeLists.txt +++ b/libraries/script-engine/CMakeLists.txt @@ -16,6 +16,6 @@ if (NOT ANDROID) endif () -link_hifi_libraries(shared networking octree gpu procedural model model-networking ktx recording avatars fbx entities controllers animation audio physics image) +link_hifi_libraries(shared networking octree gpu procedural model model-networking ktx recording avatars fbx entities controllers animation audio physics image midi) # ui includes gl, but link_hifi_libraries does not use transitive includes, so gl must be explicit include_hifi_library_headers(gl) diff --git a/libraries/script-engine/src/ModelScriptingInterface.cpp b/libraries/script-engine/src/ModelScriptingInterface.cpp index 762d9ffb29..3234a079ec 100644 --- a/libraries/script-engine/src/ModelScriptingInterface.cpp +++ b/libraries/script-engine/src/ModelScriptingInterface.cpp @@ -38,16 +38,22 @@ QString ModelScriptingInterface::meshToOBJ(MeshProxyList in) { QScriptValue ModelScriptingInterface::appendMeshes(MeshProxyList in) { // figure out the size of the resulting mesh size_t totalVertexCount { 0 }; - size_t totalAttributeCount { 0 }; + size_t totalColorCount { 0 }; + size_t totalNormalCount { 0 }; size_t totalIndexCount { 0 }; foreach (const MeshProxy* meshProxy, in) { MeshPointer mesh = meshProxy->getMeshPointer(); totalVertexCount += mesh->getNumVertices(); + int attributeTypeColor = gpu::Stream::InputSlot::COLOR; // libraries/gpu/src/gpu/Stream.h + const gpu::BufferView& colorsBufferView = mesh->getAttributeBuffer(attributeTypeColor); + gpu::BufferView::Index numColors = (gpu::BufferView::Index)colorsBufferView.getNumElements(); + totalColorCount += numColors; + 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; + totalNormalCount += numNormals; totalIndexCount += mesh->getNumIndices(); } @@ -57,7 +63,11 @@ QScriptValue ModelScriptingInterface::appendMeshes(MeshProxyList in) { unsigned char* combinedVertexData = new unsigned char[combinedVertexSize]; unsigned char* combinedVertexDataCursor = combinedVertexData; - gpu::Resource::Size combinedNormalSize = totalAttributeCount * sizeof(glm::vec3); + gpu::Resource::Size combinedColorSize = totalColorCount * sizeof(glm::vec3); + unsigned char* combinedColorData = new unsigned char[combinedColorSize]; + unsigned char* combinedColorDataCursor = combinedColorData; + + gpu::Resource::Size combinedNormalSize = totalNormalCount * sizeof(glm::vec3); unsigned char* combinedNormalData = new unsigned char[combinedNormalSize]; unsigned char* combinedNormalDataCursor = combinedNormalData; @@ -74,6 +84,10 @@ QScriptValue ModelScriptingInterface::appendMeshes(MeshProxyList in) { memcpy(combinedVertexDataCursor, &position, sizeof(position)); combinedVertexDataCursor += sizeof(position); }, + [&](glm::vec3 color){ + memcpy(combinedColorDataCursor, &color, sizeof(color)); + combinedColorDataCursor += sizeof(color); + }, [&](glm::vec3 normal){ memcpy(combinedNormalDataCursor, &normal, sizeof(normal)); combinedNormalDataCursor += sizeof(normal); @@ -96,6 +110,13 @@ QScriptValue ModelScriptingInterface::appendMeshes(MeshProxyList in) { gpu::BufferView combinedVertexBufferView(combinedVertexBufferPointer, vertexElement); result->setVertexBuffer(combinedVertexBufferView); + int attributeTypeColor = gpu::Stream::InputSlot::COLOR; // libraries/gpu/src/gpu/Stream.h + gpu::Element colorElement = gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::XYZ); + gpu::Buffer* combinedColorsBuffer = new gpu::Buffer(combinedColorSize, combinedColorData); + gpu::BufferPointer combinedColorsBufferPointer(combinedColorsBuffer); + gpu::BufferView combinedColorsBufferView(combinedColorsBufferPointer, colorElement); + result->addAttribute(attributeTypeColor, combinedColorsBufferView); + 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); @@ -132,12 +153,48 @@ QScriptValue ModelScriptingInterface::transformMesh(glm::mat4 transform, MeshPro } model::MeshPointer result = mesh->map([&](glm::vec3 position){ return glm::vec3(transform * glm::vec4(position, 1.0f)); }, + [&](glm::vec3 color){ return color; }, [&](glm::vec3 normal){ return glm::vec3(transform * glm::vec4(normal, 0.0f)); }, [&](uint32_t index){ return index; }); MeshProxy* resultProxy = new SimpleMeshProxy(result); return meshToScriptValue(_modelScriptEngine, resultProxy); } +QScriptValue ModelScriptingInterface::getVertexCount(MeshProxy* meshProxy) { + if (!meshProxy) { + return QScriptValue(false); + } + MeshPointer mesh = meshProxy->getMeshPointer(); + if (!mesh) { + return QScriptValue(false); + } + + gpu::BufferView::Index numVertices = (gpu::BufferView::Index)mesh->getNumVertices(); + + return numVertices; +} + +QScriptValue ModelScriptingInterface::getVertex(MeshProxy* meshProxy, int vertexIndex) { + if (!meshProxy) { + return QScriptValue(false); + } + MeshPointer mesh = meshProxy->getMeshPointer(); + if (!mesh) { + return QScriptValue(false); + } + + const gpu::BufferView& vertexBufferView = mesh->getVertexBuffer(); + gpu::BufferView::Index numVertices = (gpu::BufferView::Index)mesh->getNumVertices(); + + if (vertexIndex < 0 || vertexIndex >= numVertices) { + return QScriptValue(false); + } + + glm::vec3 pos = vertexBufferView.get(vertexIndex); + return vec3toScriptValue(_modelScriptEngine, pos); +} + + QScriptValue ModelScriptingInterface::newMesh(const QVector& vertices, const QVector& normals, const QVector& faces) { diff --git a/libraries/script-engine/src/ModelScriptingInterface.h b/libraries/script-engine/src/ModelScriptingInterface.h index ba23623acf..3c239f006f 100644 --- a/libraries/script-engine/src/ModelScriptingInterface.h +++ b/libraries/script-engine/src/ModelScriptingInterface.h @@ -29,6 +29,8 @@ public: Q_INVOKABLE QScriptValue newMesh(const QVector& vertices, const QVector& normals, const QVector& faces); + Q_INVOKABLE QScriptValue getVertexCount(MeshProxy* meshProxy); + Q_INVOKABLE QScriptValue getVertex(MeshProxy* meshProxy, int vertexIndex); private: QScriptEngine* _modelScriptEngine { nullptr }; diff --git a/libraries/script-engine/src/ScriptAudioInjector.cpp b/libraries/script-engine/src/ScriptAudioInjector.cpp index c0ad2debd9..516f62401f 100644 --- a/libraries/script-engine/src/ScriptAudioInjector.cpp +++ b/libraries/script-engine/src/ScriptAudioInjector.cpp @@ -21,7 +21,7 @@ QScriptValue injectorToScriptValue(QScriptEngine* engine, ScriptAudioInjector* c // when the script goes down we want to cleanup the injector QObject::connect(engine, &QScriptEngine::destroyed, in, &ScriptAudioInjector::stopInjectorImmediately, Qt::DirectConnection); - + return engine->newQObject(in, QScriptEngine::ScriptOwnership); } @@ -29,10 +29,10 @@ void injectorFromScriptValue(const QScriptValue& object, ScriptAudioInjector*& o out = qobject_cast(object.toQObject()); } -ScriptAudioInjector::ScriptAudioInjector(AudioInjector* injector) : +ScriptAudioInjector::ScriptAudioInjector(const AudioInjectorPointer& injector) : _injector(injector) { - QObject::connect(injector, &AudioInjector::finished, this, &ScriptAudioInjector::finished); + QObject::connect(injector.data(), &AudioInjector::finished, this, &ScriptAudioInjector::finished); } ScriptAudioInjector::~ScriptAudioInjector() { @@ -44,5 +44,5 @@ ScriptAudioInjector::~ScriptAudioInjector() { void ScriptAudioInjector::stopInjectorImmediately() { qCDebug(scriptengine) << "ScriptAudioInjector::stopInjectorImmediately called to stop audio injector immediately."; - _injector->stopAndDeleteLater(); + _injector->stop(); } diff --git a/libraries/script-engine/src/ScriptAudioInjector.h b/libraries/script-engine/src/ScriptAudioInjector.h index 4de12af62c..4c2871dd34 100644 --- a/libraries/script-engine/src/ScriptAudioInjector.h +++ b/libraries/script-engine/src/ScriptAudioInjector.h @@ -18,31 +18,31 @@ class ScriptAudioInjector : public QObject { Q_OBJECT - + Q_PROPERTY(bool playing READ isPlaying) Q_PROPERTY(float loudness READ getLoudness) Q_PROPERTY(AudioInjectorOptions options WRITE setOptions READ getOptions) public: - ScriptAudioInjector(AudioInjector* injector); + ScriptAudioInjector(const AudioInjectorPointer& injector); ~ScriptAudioInjector(); public slots: void restart() { _injector->restart(); } void stop() { _injector->stop(); } - + const AudioInjectorOptions& getOptions() const { return _injector->getOptions(); } void setOptions(const AudioInjectorOptions& options) { _injector->setOptions(options); } - + float getLoudness() const { return _injector->getLoudness(); } bool isPlaying() const { return _injector->isPlaying(); } - + signals: void finished(); - + protected slots: void stopInjectorImmediately(); private: - QPointer _injector; - + AudioInjectorPointer _injector; + friend QScriptValue injectorToScriptValue(QScriptEngine* engine, ScriptAudioInjector* const& in); }; diff --git a/libraries/script-engine/src/ScriptEngine.cpp b/libraries/script-engine/src/ScriptEngine.cpp index 3a876a0e0a..8b8eed02fd 100644 --- a/libraries/script-engine/src/ScriptEngine.cpp +++ b/libraries/script-engine/src/ScriptEngine.cpp @@ -77,6 +77,7 @@ #include +#include "../../midi/src/Midi.h" // FIXME why won't a simpler include work? #include "MIDIEvent.h" const QString ScriptEngine::_SETTINGS_ENABLE_EXTENDED_EXCEPTIONS { @@ -667,6 +668,8 @@ void ScriptEngine::init() { registerGlobalObject("Audio", DependencyManager::get().data()); + registerGlobalObject("Midi", DependencyManager::get().data()); + registerGlobalObject("Entities", entityScriptingInterface.data()); registerGlobalObject("Quat", &_quatLibrary); registerGlobalObject("Vec3", &_vec3Library); diff --git a/libraries/shared/src/PathUtils.h b/libraries/shared/src/PathUtils.h index 14eb81dd9a..3cb3cd3b63 100644 --- a/libraries/shared/src/PathUtils.h +++ b/libraries/shared/src/PathUtils.h @@ -13,6 +13,8 @@ #define hifi_PathUtils_h #include +#include + #include "DependencyManager.h" /**jsdoc @@ -23,7 +25,8 @@ class PathUtils : public QObject, public Dependency { Q_OBJECT SINGLETON_DEPENDENCY - Q_PROPERTY(QString resources READ resourcesPath) + Q_PROPERTY(QString resources READ resourcesPath CONSTANT) + Q_PROPERTY(QUrl defaultScripts READ defaultScriptsLocation CONSTANT) public: static const QString& resourcesPath(); diff --git a/libraries/shared/src/Preferences.h b/libraries/shared/src/Preferences.h index f1915a9d6a..6fa2cb9b1f 100644 --- a/libraries/shared/src/Preferences.h +++ b/libraries/shared/src/Preferences.h @@ -52,9 +52,11 @@ public: Browsable, Slider, Spinner, + SpinnerSlider, Checkbox, Button, ComboBox, + PrimaryHand, // Special casing for an unusual preference Avatar }; @@ -254,6 +256,15 @@ public: Type getType() override { return Spinner; } }; +class SpinnerSliderPreference : public FloatPreference { + Q_OBJECT +public: + SpinnerSliderPreference(const QString& category, const QString& name, Getter getter, Setter setter) + : FloatPreference(category, name, getter, setter) { } + + Type getType() override { return SpinnerSlider; } +}; + class IntSpinnerPreference : public IntPreference { Q_OBJECT public: @@ -329,6 +340,14 @@ public: Type getType() override { return Checkbox; } }; +class PrimaryHandPreference : public StringPreference { + Q_OBJECT +public: + PrimaryHandPreference(const QString& category, const QString& name, Getter getter, Setter setter) + : StringPreference(category, name, getter, setter) { } + Type getType() override { return PrimaryHand; } +}; + #endif diff --git a/libraries/shared/src/ShapeInfo.cpp b/libraries/shared/src/ShapeInfo.cpp index 496e94f8bd..a556548b25 100644 --- a/libraries/shared/src/ShapeInfo.cpp +++ b/libraries/shared/src/ShapeInfo.cpp @@ -29,6 +29,7 @@ void ShapeInfo::clear() { } void ShapeInfo::setParams(ShapeType type, const glm::vec3& halfExtents, QString url) { + //TODO WL21389: Does this need additional cases and handling added? _url = ""; _type = type; setHalfExtents(halfExtents); @@ -55,6 +56,9 @@ void ShapeInfo::setParams(ShapeType type, const glm::vec3& halfExtents, QString } void ShapeInfo::setBox(const glm::vec3& halfExtents) { + //TODO WL21389: Should this pointlist clearance added in case + // this is a re-purposed instance? + // See https://github.com/highfidelity/hifi/pull/11024#discussion_r128885491 _url = ""; _type = SHAPE_TYPE_BOX; setHalfExtents(halfExtents); @@ -62,6 +66,7 @@ void ShapeInfo::setBox(const glm::vec3& halfExtents) { } void ShapeInfo::setSphere(float radius) { + //TODO WL21389: See comment in setBox regarding clearance _url = ""; _type = SHAPE_TYPE_SPHERE; radius = glm::max(radius, MIN_HALF_EXTENT); @@ -70,12 +75,14 @@ void ShapeInfo::setSphere(float radius) { } void ShapeInfo::setPointCollection(const ShapeInfo::PointCollection& pointCollection) { + //TODO WL21389: May need to skip resetting type here. _pointCollection = pointCollection; _type = (_pointCollection.size() > 0) ? SHAPE_TYPE_COMPOUND : SHAPE_TYPE_NONE; _doubleHashKey.clear(); } void ShapeInfo::setCapsuleY(float radius, float halfHeight) { + //TODO WL21389: See comment in setBox regarding clearance _url = ""; _type = SHAPE_TYPE_CAPSULE_Y; radius = glm::max(radius, MIN_HALF_EXTENT); @@ -117,6 +124,7 @@ int ShapeInfo::getLargestSubshapePointCount() const { } float ShapeInfo::computeVolume() const { + //TODO WL21389: Add support for other ShapeTypes( CYLINDER_X, CYLINDER_Y, etc). const float DEFAULT_VOLUME = 1.0f; float volume = DEFAULT_VOLUME; switch(_type) { @@ -136,7 +144,10 @@ float ShapeInfo::computeVolume() const { } case SHAPE_TYPE_CAPSULE_Y: { float radius = _halfExtents.x; - volume = PI * radius * radius * (2.0f * (_halfExtents.y - _halfExtents.x) + 4.0f * radius / 3.0f); + // Need to offset halfExtents.y by x to account for the system treating + // the y extent of the capsule as the cylindrical height + spherical radius. + float cylinderHeight = 2.0f * (_halfExtents.y - _halfExtents.x); + volume = PI * radius * radius * (cylinderHeight + 4.0f * radius / 3.0f); break; } default: @@ -147,6 +158,7 @@ float ShapeInfo::computeVolume() const { } bool ShapeInfo::contains(const glm::vec3& point) const { + //TODO WL21389: Add support for other ShapeTypes like Ellipsoid/Compound. switch(_type) { case SHAPE_TYPE_SPHERE: return glm::length(point) <= _halfExtents.x; @@ -191,6 +203,7 @@ bool ShapeInfo::contains(const glm::vec3& point) const { } const DoubleHashKey& ShapeInfo::getHash() const { + //TODO WL21389: Need to include the pointlist for SIMPLE_HULL in hash // NOTE: we cache the key so we only ever need to compute it once for any valid ShapeInfo instance. if (_doubleHashKey.isNull() && _type != SHAPE_TYPE_NONE) { bool useOffset = glm::length2(_offset) > MIN_SHAPE_OFFSET * MIN_SHAPE_OFFSET; diff --git a/libraries/shared/src/SharedUtil.cpp b/libraries/shared/src/SharedUtil.cpp index 58b8aead45..99bdfc4d90 100644 --- a/libraries/shared/src/SharedUtil.cpp +++ b/libraries/shared/src/SharedUtil.cpp @@ -47,6 +47,7 @@ extern "C" FILE * __cdecl __iob_func(void) { #include #include #include +#include #include #include #include @@ -1077,14 +1078,20 @@ void setMaxCores(uint8_t maxCores) { #endif } -#ifdef Q_OS_WIN -VOID CALLBACK parentDiedCallback(PVOID lpParameter, BOOLEAN timerOrWaitFired) { - if (!timerOrWaitFired && qApp) { +void quitWithParentProcess() { + if (qApp) { qDebug() << "Parent process died, quitting"; qApp->quit(); } } +#ifdef Q_OS_WIN +VOID CALLBACK parentDiedCallback(PVOID lpParameter, BOOLEAN timerOrWaitFired) { + if (!timerOrWaitFired) { + quitWithParentProcess(); + } +} + void watchParentProcess(int parentPID) { DWORD processID = parentPID; HANDLE procHandle = OpenProcess(PROCESS_ALL_ACCESS, FALSE, processID); @@ -1092,8 +1099,17 @@ void watchParentProcess(int parentPID) { HANDLE newHandle; RegisterWaitForSingleObject(&newHandle, procHandle, parentDiedCallback, NULL, INFINITE, WT_EXECUTEONLYONCE); } -#else +#elif defined(Q_OS_MAC) || defined(Q_OS_LINUX) void watchParentProcess(int parentPID) { - qWarning() << "Parent PID option not implemented on this plateform"; + auto timer = new QTimer(qApp); + timer->setInterval(MSECS_PER_SECOND); + QObject::connect(timer, &QTimer::timeout, qApp, [parentPID]() { + auto ppid = getppid(); + if (parentPID != ppid) { + // If the PPID changed, then that means our parent process died. + quitWithParentProcess(); + } + }); + timer->start(); } -#endif // Q_OS_WIN \ No newline at end of file +#endif diff --git a/libraries/shared/src/SpatiallyNestable.cpp b/libraries/shared/src/SpatiallyNestable.cpp index 4fe9cda825..fc189c5e3e 100644 --- a/libraries/shared/src/SpatiallyNestable.cpp +++ b/libraries/shared/src/SpatiallyNestable.cpp @@ -946,11 +946,35 @@ AACube SpatiallyNestable::getMaximumAACube(bool& success) const { return AACube(getPosition(success) - glm::vec3(defaultAACubeSize / 2.0f), defaultAACubeSize); } -bool SpatiallyNestable::checkAndAdjustQueryAACube() { - bool success; +const float PARENTED_EXPANSION_FACTOR = 3.0f; + +bool SpatiallyNestable::checkAndMaybeUpdateQueryAACube() { + bool success = false; AACube maxAACube = getMaximumAACube(success); - if (success && (!_queryAACubeSet || !_queryAACube.contains(maxAACube))) { - setQueryAACube(maxAACube); + if (success) { + // maybe update _queryAACube + if (!_queryAACubeSet || (_parentID.isNull() && _children.size() == 0) || !_queryAACube.contains(maxAACube)) { + if (_parentJointIndex != INVALID_JOINT_INDEX || _children.size() > 0 ) { + // make an expanded AACube centered on the object + float scale = PARENTED_EXPANSION_FACTOR * maxAACube.getScale(); + _queryAACube = AACube(maxAACube.calcCenter() - glm::vec3(0.5f * scale), scale); + } else { + _queryAACube = maxAACube; + } + + getThisPointer()->forEachDescendant([&](SpatiallyNestablePointer descendant) { + bool childSuccess; + AACube descendantAACube = descendant->getQueryAACube(childSuccess); + if (childSuccess) { + if (_queryAACube.contains(descendantAACube)) { + return; + } + _queryAACube += descendantAACube.getMinimumPoint(); + _queryAACube += descendantAACube.getMaximumPoint(); + } + }); + _queryAACubeSet = true; + } } return success; } @@ -961,46 +985,34 @@ void SpatiallyNestable::setQueryAACube(const AACube& queryAACube) { return; } _queryAACube = queryAACube; - if (queryAACube.getScale() > 0.0f) { - _queryAACubeSet = true; - } + _queryAACubeSet = true; } -bool SpatiallyNestable::queryAABoxNeedsUpdate() const { - bool success; - AACube currentAACube = getMaximumAACube(success); - if (!success) { - qCDebug(shared) << "can't getMaximumAACube for" << getID(); - return false; +bool SpatiallyNestable::queryAACubeNeedsUpdate() const { + if (!_queryAACubeSet) { + return true; } // make sure children are still in their boxes, also. bool childNeedsUpdate = false; getThisPointer()->forEachDescendant([&](SpatiallyNestablePointer descendant) { - if (!childNeedsUpdate && descendant->queryAABoxNeedsUpdate()) { + if (!childNeedsUpdate && descendant->queryAACubeNeedsUpdate()) { childNeedsUpdate = true; } }); - if (childNeedsUpdate) { - return true; - } - - if (_queryAACubeSet && _queryAACube.contains(currentAACube)) { - return false; - } - - return true; + return childNeedsUpdate; } -bool SpatiallyNestable::computePuffedQueryAACube() { - if (!queryAABoxNeedsUpdate()) { - return false; - } +void SpatiallyNestable::updateQueryAACube() { bool success; - AACube currentAACube = getMaximumAACube(success); - // make an AACube with edges thrice as long and centered on the object - _queryAACube = AACube(currentAACube.getCorner() - glm::vec3(currentAACube.getScale()), currentAACube.getScale() * 3.0f); - _queryAACubeSet = true; + AACube maxAACube = getMaximumAACube(success); + if (_parentJointIndex != INVALID_JOINT_INDEX || _children.size() > 0 ) { + // make an expanded AACube centered on the object + float scale = PARENTED_EXPANSION_FACTOR * maxAACube.getScale(); + _queryAACube = AACube(maxAACube.calcCenter() - glm::vec3(0.5f * scale), scale); + } else { + _queryAACube = maxAACube; + } getThisPointer()->forEachDescendant([&](SpatiallyNestablePointer descendant) { bool success; @@ -1013,8 +1025,7 @@ bool SpatiallyNestable::computePuffedQueryAACube() { _queryAACube += descendantAACube.getMaximumPoint(); } }); - - return true; + _queryAACubeSet = true; } AACube SpatiallyNestable::getQueryAACube(bool& success) const { diff --git a/libraries/shared/src/SpatiallyNestable.h b/libraries/shared/src/SpatiallyNestable.h index b98ab4c358..78a014d7c1 100644 --- a/libraries/shared/src/SpatiallyNestable.h +++ b/libraries/shared/src/SpatiallyNestable.h @@ -102,11 +102,11 @@ public: virtual glm::vec3 getParentAngularVelocity(bool& success) const; virtual AACube getMaximumAACube(bool& success) const; - virtual bool checkAndAdjustQueryAACube(); - virtual bool computePuffedQueryAACube(); + bool checkAndMaybeUpdateQueryAACube(); + void updateQueryAACube(); virtual void setQueryAACube(const AACube& queryAACube); - virtual bool queryAABoxNeedsUpdate() const; + virtual bool queryAACubeNeedsUpdate() const; void forceQueryAACubeUpdate() { _queryAACubeSet = false; } virtual AACube getQueryAACube(bool& success) const; virtual AACube getQueryAACube() const; @@ -197,7 +197,7 @@ protected: mutable QHash _children; virtual void locationChanged(bool tellPhysics = true); // called when a this object's location has changed - virtual void dimensionsChanged() { } // called when a this object's dimensions have changed + virtual void dimensionsChanged() { _queryAACubeSet = false; } // called when a this object's dimensions have changed virtual void parentDeleted() { } // called on children of a deleted parent // _queryAACube is used to decide where something lives in the octree diff --git a/libraries/shared/src/ThreadHelpers.cpp b/libraries/shared/src/ThreadHelpers.cpp index 8f3d16a577..654b8e0252 100644 --- a/libraries/shared/src/ThreadHelpers.cpp +++ b/libraries/shared/src/ThreadHelpers.cpp @@ -10,29 +10,71 @@ #include +// Support for viewing the thread name in the debugger. +// Note, Qt actually does this for you but only in debug builds +// Code from https://msdn.microsoft.com/en-us/library/xcb2z8hs.aspx +// and matches logic in `qt_set_thread_name` in qthread_win.cpp +#ifdef Q_OS_WIN +#include +#pragma pack(push,8) +struct THREADNAME_INFO { + DWORD dwType; // Must be 0x1000. + LPCSTR szName; // Pointer to name (in user addr space). + DWORD dwThreadID; // Thread ID (-1=caller thread). + DWORD dwFlags; // Reserved for future use, must be zero. +}; +#pragma pack(pop) +#endif + +void setThreadName(const std::string& name) { +#ifdef Q_OS_WIN + static const DWORD MS_VC_EXCEPTION = 0x406D1388; + THREADNAME_INFO info{ 0x1000, name.c_str(), (DWORD)-1, 0 }; + __try { + RaiseException(MS_VC_EXCEPTION, 0, sizeof(info) / sizeof(ULONG_PTR), (ULONG_PTR*)&info); + } __except (EXCEPTION_EXECUTE_HANDLER) { } +#endif +} + +void moveToNewNamedThread(QObject* object, const QString& name, std::function preStartCallback, std::function startCallback, QThread::Priority priority) { + Q_ASSERT(QThread::currentThread() == object->thread()); + + // Create the target thread + QThread* thread = new QThread(); + thread->setObjectName(name); + + // Execute any additional work to do before the thread starts like moving members to the target thread. + // This is required as QObject::moveToThread isn't virutal, so we can't override it on objects that contain + // an OpenGLContext and ensure that the context moves to the target thread as well. + preStartCallback(thread); + + // Link the in-thread initialization code + QObject::connect(thread, &QThread::started, [name, startCallback] { + if (!name.isEmpty()) { + // Make it easy to spot our thread processes inside the debugger + setThreadName("Hifi_" + name.toStdString()); + } + startCallback(); + }); + + // Make sure the thread will be destroyed and cleaned up. The assumption here is that the incoming object + // will be destroyed and the thread will quit when that occurs. + QObject::connect(object, &QObject::destroyed, thread, &QThread::quit); + // When the thread itself stops running, it should also be deleted. + QObject::connect(thread, &QThread::finished, thread, &QThread::deleteLater); + + // put the object on the thread + object->moveToThread(thread); + thread->start(); + if (priority != QThread::InheritPriority) { + thread->setPriority(priority); + } +} void moveToNewNamedThread(QObject* object, const QString& name, std::function startCallback, QThread::Priority priority) { - Q_ASSERT(QThread::currentThread() == object->thread()); - // setup a thread for the NodeList and its PacketReceiver - QThread* thread = new QThread(); - thread->setObjectName(name); - - QString tempName = name; - QObject::connect(thread, &QThread::started, [startCallback] { - startCallback(); - }); - // Make sure the thread will be destroyed and cleaned up - QObject::connect(object, &QObject::destroyed, thread, &QThread::quit); - QObject::connect(thread, &QThread::finished, thread, &QThread::deleteLater); - - // put the object on the thread - object->moveToThread(thread); - thread->start(); - if (priority != QThread::InheritPriority) { - thread->setPriority(priority); - } + moveToNewNamedThread(object, name, [](QThread*){}, startCallback, priority); } void moveToNewNamedThread(QObject* object, const QString& name, QThread::Priority priority) { - moveToNewNamedThread(object, name, [] {}, priority); + moveToNewNamedThread(object, name, [](QThread*){}, []{}, priority); } diff --git a/libraries/shared/src/ThreadHelpers.h b/libraries/shared/src/ThreadHelpers.h index 6e024f787a..d236344dc5 100644 --- a/libraries/shared/src/ThreadHelpers.h +++ b/libraries/shared/src/ThreadHelpers.h @@ -32,8 +32,17 @@ void withLock(QMutex& lock, F function) { function(); } -void moveToNewNamedThread(QObject* object, const QString& name, std::function startCallback, QThread::Priority priority = QThread::InheritPriority); -void moveToNewNamedThread(QObject* object, const QString& name, QThread::Priority priority = QThread::InheritPriority); +void moveToNewNamedThread(QObject* object, const QString& name, + std::function preStartCallback, + std::function startCallback, + QThread::Priority priority = QThread::InheritPriority); + +void moveToNewNamedThread(QObject* object, const QString& name, + std::function startCallback, + QThread::Priority priority = QThread::InheritPriority); + +void moveToNewNamedThread(QObject* object, const QString& name, + QThread::Priority priority = QThread::InheritPriority); class ConditionalGuard { public: diff --git a/libraries/shared/src/shared/Camera.h b/libraries/shared/src/shared/Camera.h index 5f2162ff6e..c7b943f0dd 100644 --- a/libraries/shared/src/shared/Camera.h +++ b/libraries/shared/src/shared/Camera.h @@ -127,7 +127,7 @@ private: glm::mat4 _projection; // derived - glm::vec3 _position; + glm::vec3 _position { 0.0f, 0.0f, 0.0f }; glm::quat _orientation; bool _isKeepLookingAt{ false }; glm::vec3 _lookingAt; diff --git a/libraries/shared/src/shared/FileCache.cpp b/libraries/shared/src/shared/FileCache.cpp index 695336847e..9957c6bfc6 100644 --- a/libraries/shared/src/shared/FileCache.cpp +++ b/libraries/shared/src/shared/FileCache.cpp @@ -258,7 +258,8 @@ namespace cache { }; } -void FileCache::eject(const FilePointer& file) { +// Take file pointer by value to insure it doesn't get destructed during the "erase()" calls +void FileCache::eject(FilePointer file) { file->_locked = false; const auto& length = file->getLength(); const auto& key = file->getKey(); diff --git a/libraries/shared/src/shared/FileCache.h b/libraries/shared/src/shared/FileCache.h index 1580674ca0..94fa57457a 100644 --- a/libraries/shared/src/shared/FileCache.h +++ b/libraries/shared/src/shared/FileCache.h @@ -119,7 +119,7 @@ private: void clean(); void clear(); // Remove a file from the cache - void eject(const FilePointer& file); + void eject(FilePointer file); size_t getOverbudgetAmount() const; diff --git a/libraries/shared/src/shared/QtHelpers.cpp b/libraries/shared/src/shared/QtHelpers.cpp index 1ce1c3e07c..3e8c6d57ed 100644 --- a/libraries/shared/src/shared/QtHelpers.cpp +++ b/libraries/shared/src/shared/QtHelpers.cpp @@ -11,11 +11,24 @@ #include #include #include +#include +#include "../Profile.h" Q_LOGGING_CATEGORY(thread_safety, "hifi.thread_safety") namespace hifi { namespace qt { +static QHash threadHash; +static QReadWriteLock threadHashLock; + +void addBlockingForbiddenThread(const QString& name, QThread* thread) { + if (!thread) { + thread = QThread::currentThread(); + } + QWriteLocker locker(&threadHashLock); + threadHash[thread] = name; +} + bool blockingInvokeMethod( const char* function, QObject *obj, const char *member, @@ -30,9 +43,23 @@ bool blockingInvokeMethod( QGenericArgument val7, QGenericArgument val8, QGenericArgument val9) { - if (QThread::currentThread() == qApp->thread()) { + auto currentThread = QThread::currentThread(); + if (currentThread == qApp->thread()) { qCWarning(thread_safety) << "BlockingQueuedConnection invoked on main thread from " << function; + return QMetaObject::invokeMethod(obj, member, + Qt::BlockingQueuedConnection, ret, val0, val1, val2, val3, val4, val5, val6, val7, val8, val9); + } + + { + QReadLocker locker(&threadHashLock); + for (const auto& thread : threadHash.keys()) { + if (currentThread == thread) { + qCWarning(thread_safety) << "BlockingQueuedConnection invoked on forbidden thread " << threadHash[thread]; + } + } } + + PROFILE_RANGE(app, function); return QMetaObject::invokeMethod(obj, member, Qt::BlockingQueuedConnection, ret, val0, val1, val2, val3, val4, val5, val6, val7, val8, val9); } diff --git a/libraries/shared/src/shared/QtHelpers.h b/libraries/shared/src/shared/QtHelpers.h index 5da65a378f..2133119324 100644 --- a/libraries/shared/src/shared/QtHelpers.h +++ b/libraries/shared/src/shared/QtHelpers.h @@ -14,6 +14,7 @@ namespace hifi { namespace qt { +void addBlockingForbiddenThread(const QString& name, QThread* thread = nullptr); bool blockingInvokeMethod( const char* function, diff --git a/libraries/shared/src/shared/Storage.cpp b/libraries/shared/src/shared/Storage.cpp index b07f896df0..7e9f86b049 100644 --- a/libraries/shared/src/shared/Storage.cpp +++ b/libraries/shared/src/shared/Storage.cpp @@ -92,9 +92,8 @@ FileStorage::FileStorage(const QString& filename) : _file(filename) { FileStorage::~FileStorage() { if (_mapped) { - if (!_file.unmap(_mapped)) { - throw std::runtime_error("Unable to unmap file"); - } + _file.unmap(_mapped); + _mapped = nullptr; } if (_file.isOpen()) { _file.close(); diff --git a/libraries/ui/src/CursorManager.cpp b/libraries/ui/src/CursorManager.cpp index f768b5f227..106574ff4f 100644 --- a/libraries/ui/src/CursorManager.cpp +++ b/libraries/ui/src/CursorManager.cpp @@ -31,12 +31,34 @@ namespace Cursor { } }; - static QMap ICONS; + QMap Manager::ICON_NAMES { + { Icon::SYSTEM, "SYSTEM", }, + { Icon::DEFAULT, "DEFAULT", }, + { Icon::LINK, "LINK", }, + { Icon::ARROW, "ARROW", }, + { Icon::RETICLE, "RETICLE", }, + }; + QMap Manager::ICONS; static uint16_t _customIconId = Icon::USER_BASE; Manager::Manager() { - ICONS[Icon::DEFAULT] = PathUtils::resourcesPath() + "images/arrow.png"; - ICONS[Icon::LINK] = PathUtils::resourcesPath() + "images/link.png"; + ICONS[Icon::SYSTEM] = PathUtils::resourcesPath() + "images/cursor-none.png"; + ICONS[Icon::DEFAULT] = PathUtils::resourcesPath() + "images/cursor-arrow.png"; + ICONS[Icon::LINK] = PathUtils::resourcesPath() + "images/cursor-link.png"; + ICONS[Icon::ARROW] = PathUtils::resourcesPath() + "images/cursor-arrow.png"; + ICONS[Icon::RETICLE] = PathUtils::resourcesPath() + "images/cursor-reticle.png"; + } + + Icon Manager::lookupIcon(const QString& name) { + for (const auto& kv : ICON_NAMES.toStdMap()) { + if (kv.second == name) { + return static_cast(kv.first); + } + } + return Icon::DEFAULT; + } + const QString& Manager::getIconName(const Icon& icon) { + return ICON_NAMES.count(icon) ? ICON_NAMES[icon] : ICON_NAMES[Icon::DEFAULT]; } Manager& Manager::instance() { diff --git a/libraries/ui/src/CursorManager.h b/libraries/ui/src/CursorManager.h index 99d5ccdc77..2a92cc7f45 100644 --- a/libraries/ui/src/CursorManager.h +++ b/libraries/ui/src/CursorManager.h @@ -18,16 +18,18 @@ namespace Cursor { }; enum Icon { + SYSTEM, DEFAULT, LINK, GRAB, + ARROW, + RETICLE, // Add new system cursors here // User cursors will have ids over this value USER_BASE = 0xFF, }; - class Instance { public: virtual Source getType() const = 0; @@ -49,6 +51,11 @@ namespace Cursor { uint16_t registerIcon(const QString& path); QList registeredIcons() const; const QString& getIconImage(uint16_t icon); + + static QMap ICONS; + static QMap ICON_NAMES; + static Icon lookupIcon(const QString& name); + static const QString& getIconName(const Icon& icon); private: float _scale{ 1.0f }; }; diff --git a/libraries/ui/src/ui/OffscreenQmlSurface.cpp b/libraries/ui/src/ui/OffscreenQmlSurface.cpp index 9df727424f..2012ebbe30 100644 --- a/libraries/ui/src/ui/OffscreenQmlSurface.cpp +++ b/libraries/ui/src/ui/OffscreenQmlSurface.cpp @@ -264,9 +264,6 @@ QNetworkAccessManager* QmlNetworkAccessManagerFactory::create(QObject* parent) { return new QmlNetworkAccessManager(parent); } -static QQmlEngine* globalEngine { nullptr }; -static size_t globalEngineRefCount { 0 }; - QString getEventBridgeJavascript() { // FIXME: Refactor with similar code in RenderableWebEntityItem QString javaScriptToInject; @@ -300,9 +297,45 @@ private: }; +#define SINGLE_QML_ENGINE 0 + +#if SINGLE_QML_ENGINE +static QQmlEngine* globalEngine{ nullptr }; +static size_t globalEngineRefCount{ 0 }; +#endif + +void initializeQmlEngine(QQmlEngine* engine, QQuickWindow* window) { + engine->setNetworkAccessManagerFactory(new QmlNetworkAccessManagerFactory); + auto importList = engine->importPathList(); + importList.insert(importList.begin(), PathUtils::resourcesPath()); + engine->setImportPathList(importList); + for (const auto& path : importList) { + qDebug() << path; + } + + if (!engine->incubationController()) { + engine->setIncubationController(window->incubationController()); + } + auto rootContext = engine->rootContext(); + rootContext->setContextProperty("GL", ::getGLContextData()); + rootContext->setContextProperty("urlHandler", new UrlHandler()); + rootContext->setContextProperty("resourceDirectoryUrl", QUrl::fromLocalFile(PathUtils::resourcesPath())); + rootContext->setContextProperty("pathToFonts", "../../"); + rootContext->setContextProperty("ApplicationInterface", qApp); + auto javaScriptToInject = getEventBridgeJavascript(); + if (!javaScriptToInject.isEmpty()) { + rootContext->setContextProperty("eventBridgeJavaScriptToInject", QVariant(javaScriptToInject)); + } + rootContext->setContextProperty("FileTypeProfile", new FileTypeProfile(rootContext)); + rootContext->setContextProperty("HFWebEngineProfile", new HFWebEngineProfile(rootContext)); + rootContext->setContextProperty("HFTabletWebEngineProfile", new HFTabletWebEngineProfile(rootContext)); + rootContext->setContextProperty("Paths", DependencyManager::get().data()); +} QQmlEngine* acquireEngine(QQuickWindow* window) { Q_ASSERT(QThread::currentThread() == qApp->thread()); + + QQmlEngine* result = nullptr; if (QThread::currentThread() != qApp->thread()) { qCWarning(uiLogging) << "Cannot acquire QML engine on any thread but the main thread"; } @@ -311,47 +344,34 @@ QQmlEngine* acquireEngine(QQuickWindow* window) { qmlRegisterType("Hifi", 1, 0, "SoundEffect"); }); + +#if SINGLE_QML_ENGINE if (!globalEngine) { Q_ASSERT(0 == globalEngineRefCount); globalEngine = new QQmlEngine(); - globalEngine->setNetworkAccessManagerFactory(new QmlNetworkAccessManagerFactory); - - auto importList = globalEngine->importPathList(); - importList.insert(importList.begin(), PathUtils::resourcesPath()); - globalEngine->setImportPathList(importList); - for (const auto& path : importList) { - qDebug() << path; - } - - if (!globalEngine->incubationController()) { - globalEngine->setIncubationController(window->incubationController()); - } - auto rootContext = globalEngine->rootContext(); - rootContext->setContextProperty("GL", ::getGLContextData()); - rootContext->setContextProperty("urlHandler", new UrlHandler()); - rootContext->setContextProperty("resourceDirectoryUrl", QUrl::fromLocalFile(PathUtils::resourcesPath())); - rootContext->setContextProperty("pathToFonts", "../../"); - rootContext->setContextProperty("ApplicationInterface", qApp); - auto javaScriptToInject = getEventBridgeJavascript(); - if (!javaScriptToInject.isEmpty()) { - rootContext->setContextProperty("eventBridgeJavaScriptToInject", QVariant(javaScriptToInject)); - } - rootContext->setContextProperty("FileTypeProfile", new FileTypeProfile(rootContext)); - rootContext->setContextProperty("HFWebEngineProfile", new HFWebEngineProfile(rootContext)); - rootContext->setContextProperty("HFTabletWebEngineProfile", new HFTabletWebEngineProfile(rootContext)); + initializeQmlEngine(result, window); + ++globalEngineRefCount; } + result = globalEngine; +#else + result = new QQmlEngine(); + initializeQmlEngine(result, window); +#endif - ++globalEngineRefCount; - return globalEngine; + return result; } -void releaseEngine() { +void releaseEngine(QQmlEngine* engine) { Q_ASSERT(QThread::currentThread() == qApp->thread()); +#if SINGLE_QML_ENGINE Q_ASSERT(0 != globalEngineRefCount); if (0 == --globalEngineRefCount) { globalEngine->deleteLater(); globalEngine = nullptr; } +#else + engine->deleteLater(); +#endif } void OffscreenQmlSurface::cleanup() { @@ -456,11 +476,11 @@ OffscreenQmlSurface::~OffscreenQmlSurface() { QObject::disconnect(qApp); cleanup(); - + auto engine = _qmlContext->engine(); _canvas->deleteLater(); _rootItem->deleteLater(); _quickWindow->deleteLater(); - releaseEngine(); + releaseEngine(engine); } void OffscreenQmlSurface::onAboutToQuit() { diff --git a/libraries/ui/src/ui/TabletScriptingInterface.cpp b/libraries/ui/src/ui/TabletScriptingInterface.cpp index 857cae15cc..0fd32b42e6 100644 --- a/libraries/ui/src/ui/TabletScriptingInterface.cpp +++ b/libraries/ui/src/ui/TabletScriptingInterface.cpp @@ -187,6 +187,7 @@ TabletProxy::TabletProxy(QObject* parent, const QString& name) : QObject(parent) if (QThread::currentThread() != qApp->thread()) { qCWarning(uiLogging) << "Creating tablet proxy on wrong thread " << _name; } + connect(this, &TabletProxy::tabletShownChanged, this, &TabletProxy::onTabletShown); } TabletProxy::~TabletProxy() { @@ -194,6 +195,7 @@ TabletProxy::~TabletProxy() { if (QThread::currentThread() != thread()) { qCWarning(uiLogging) << "Destroying tablet proxy on wrong thread" << _name; } + disconnect(this, &TabletProxy::tabletShownChanged, this, &TabletProxy::onTabletShown); } void TabletProxy::setToolbarMode(bool toolbarMode) { @@ -208,12 +210,13 @@ void TabletProxy::setToolbarMode(bool toolbarMode) { _toolbarMode = toolbarMode; + auto offscreenUi = DependencyManager::get(); + if (toolbarMode) { removeButtonsFromHomeScreen(); addButtonsToToolbar(); // create new desktop window - auto offscreenUi = DependencyManager::get(); auto tabletRootWindow = new TabletRootWindow(); tabletRootWindow->initQml(QVariantMap()); auto quickItem = tabletRootWindow->asQuickItem(); @@ -227,11 +230,18 @@ void TabletProxy::setToolbarMode(bool toolbarMode) { // forward qml surface events to interface js connect(tabletRootWindow, &QmlWindowClass::fromQml, this, &TabletProxy::fromQml); } else { - _state = State::Home; removeButtonsFromToolbar(); - addButtonsToHomeScreen(); - emit screenChanged(QVariant("Home"), QVariant(TABLET_SOURCE_URL)); + if (_currentPathLoaded == TABLET_SOURCE_URL) { + addButtonsToHomeScreen(); + } else { + loadHomeScreen(true); + } + //check if running scripts window opened and save it for reopen in Tablet + if (offscreenUi->isVisible("RunningScripts")) { + offscreenUi->hide("RunningScripts"); + _showRunningScripts = true; + } // destroy desktop window if (_desktopWindow) { _desktopWindow->deleteLater(); @@ -313,6 +323,13 @@ void TabletProxy::emitWebEvent(const QVariant& msg) { emit webEventReceived(msg); } +void TabletProxy::onTabletShown() { + if (_tabletShown && _showRunningScripts) { + _showRunningScripts = false; + pushOntoStack("../../hifi/dialogs/TabletRunningScripts.qml"); + } +} + bool TabletProxy::isPathLoaded(const QVariant& path) { if (QThread::currentThread() != thread()) { bool result = false; @@ -362,9 +379,16 @@ void TabletProxy::setQmlTabletRoot(OffscreenQmlSurface* qmlOffscreenSurface) { }); if (_initialScreen) { - pushOntoStack(_initialPath); + if (!_showRunningScripts) { + pushOntoStack(_initialPath); + } _initialScreen = false; } + + if (_showRunningScripts) { + //show Tablet. Make sure, setShown implemented in TabletRoot.qml + QMetaObject::invokeMethod(_qmlTabletRoot, "setShown", Q_ARG(const QVariant&, QVariant(true))); + } } else { removeButtonsFromHomeScreen(); _state = State::Uninitialized; @@ -463,14 +487,14 @@ void TabletProxy::loadQMLSource(const QVariant& path) { } if (root) { - if (_state != State::QML) { - removeButtonsFromHomeScreen(); - QMetaObject::invokeMethod(root, "loadSource", Q_ARG(const QVariant&, path)); - _state = State::QML; + removeButtonsFromHomeScreen(); //works only in Tablet + QMetaObject::invokeMethod(root, "loadSource", Q_ARG(const QVariant&, path)); + _state = State::QML; + if (path != _currentPathLoaded) { emit screenChanged(QVariant("QML"), path); - _currentPathLoaded = path; - QMetaObject::invokeMethod(root, "setShown", Q_ARG(const QVariant&, QVariant(true))); } + _currentPathLoaded = path; + QMetaObject::invokeMethod(root, "setShown", Q_ARG(const QVariant&, QVariant(true))); } else { qCDebug(uiLogging) << "tablet cannot load QML because _qmlTabletRoot is null"; } @@ -483,6 +507,11 @@ bool TabletProxy::pushOntoStack(const QVariant& path) { return result; } + //set landscape off when pushing menu items while in Create mode + if (_landscape) { + setLandscape(false); + } + QObject* root = nullptr; if (!_toolbarMode && _qmlTabletRoot) { root = _qmlTabletRoot; @@ -501,7 +530,7 @@ bool TabletProxy::pushOntoStack(const QVariant& path) { qCDebug(uiLogging) << "tablet cannot push QML because _qmlTabletRoot or _desktopWindow is null"; } - return root; + return (root != nullptr); } void TabletProxy::popFromStack() { @@ -825,7 +854,13 @@ TabletButtonProxy::~TabletButtonProxy() { void TabletButtonProxy::setQmlButton(QQuickItem* qmlButton) { Q_ASSERT(QThread::currentThread() == qApp->thread()); + if (_qmlButton) { + QObject::disconnect(_qmlButton, &QQuickItem::destroyed, this, nullptr); + } _qmlButton = qmlButton; + if (_qmlButton) { + QObject::connect(_qmlButton, &QQuickItem::destroyed, this, [this] { _qmlButton = nullptr; }); + } } void TabletButtonProxy::setToolbarButtonProxy(QObject* toolbarButtonProxy) { diff --git a/libraries/ui/src/ui/TabletScriptingInterface.h b/libraries/ui/src/ui/TabletScriptingInterface.h index e61398585e..af38cb9351 100644 --- a/libraries/ui/src/ui/TabletScriptingInterface.h +++ b/libraries/ui/src/ui/TabletScriptingInterface.h @@ -232,6 +232,7 @@ protected slots: void addButtonsToHomeScreen(); void desktopWindowClosed(); void emitWebEvent(const QVariant& msg); + void onTabletShown(); protected: void removeButtonsFromHomeScreen(); void loadHomeScreen(bool forceOntoHomeScreen); @@ -252,6 +253,7 @@ protected: enum class State { Uninitialized, Home, Web, Menu, QML }; State _state { State::Uninitialized }; bool _landscape { false }; + bool _showRunningScripts { false }; }; Q_DECLARE_METATYPE(TabletProxy*); diff --git a/libraries/ui/src/ui/types/SoundEffect.h b/libraries/ui/src/ui/types/SoundEffect.h index 656f98dd8d..a7e29d86f9 100644 --- a/libraries/ui/src/ui/types/SoundEffect.h +++ b/libraries/ui/src/ui/types/SoundEffect.h @@ -15,6 +15,7 @@ #include class AudioInjector; +using AudioInjectorPointer = QSharedPointer; // SoundEffect object, exposed to qml only, not interface JavaScript. // This is used to play spatial sound effects on tablets/web entities from within QML. @@ -38,7 +39,7 @@ protected: QUrl _url; float _volume { 1.0f }; SharedSoundPointer _sound; - AudioInjector* _injector { nullptr }; + AudioInjectorPointer _injector; }; #endif // hifi_SoundEffect_h diff --git a/plugins/hifiNeuron/src/NeuronPlugin.cpp b/plugins/hifiNeuron/src/NeuronPlugin.cpp index 93e7da028f..4e27777628 100644 --- a/plugins/hifiNeuron/src/NeuronPlugin.cpp +++ b/plugins/hifiNeuron/src/NeuronPlugin.cpp @@ -367,6 +367,12 @@ void NeuronPlugin::init() { auto preferences = DependencyManager::get(); static const QString NEURON_PLUGIN { "Perception Neuron" }; + { + auto getter = [this]()->bool { return _enabled; }; + auto setter = [this](bool value) { _enabled = value; saveSettings(); }; + auto preference = new CheckPreference(NEURON_PLUGIN, "Enabled", getter, setter); + preferences->addPreference(preference); + } { auto getter = [this]()->QString { return _serverAddress; }; auto setter = [this](const QString& value) { _serverAddress = value; saveSettings(); }; @@ -387,12 +393,6 @@ void NeuronPlugin::init() { preference->setStep(1); preferences->addPreference(preference); } - { - auto getter = [this]()->bool { return _enabled; }; - auto setter = [this](bool value) { _enabled = value; saveSettings(); }; - auto preference = new CheckPreference(NEURON_PLUGIN, "Enabled", getter, setter); - preferences->addPreference(preference); - } } bool NeuronPlugin::isSupported() const { @@ -455,6 +455,10 @@ void NeuronPlugin::deactivate() { } void NeuronPlugin::pluginUpdate(float deltaTime, const controller::InputCalibrationData& inputCalibrationData) { + if (!_enabled) { + return; + } + std::vector joints; { // lock and copy diff --git a/plugins/hifiSdl2/src/SDL2Manager.cpp b/plugins/hifiSdl2/src/SDL2Manager.cpp index 5ab5758412..021cb4dfec 100644 --- a/plugins/hifiSdl2/src/SDL2Manager.cpp +++ b/plugins/hifiSdl2/src/SDL2Manager.cpp @@ -11,8 +11,10 @@ #include -#include #include +#include +#include +#include #include "SDL2Manager.h" @@ -38,10 +40,13 @@ static_assert( (int)controller::RY == (int)SDL_CONTROLLER_AXIS_RIGHTY && (int)controller::LT == (int)SDL_CONTROLLER_AXIS_TRIGGERLEFT && (int)controller::RT == (int)SDL_CONTROLLER_AXIS_TRIGGERRIGHT, - "SDL2 equvalence: Enums and values from StandardControls.h are assumed to match enums from SDL_gamecontroller.h"); + "SDL2 equivalence: Enums and values from StandardControls.h are assumed to match enums from SDL_gamecontroller.h"); const char* SDL2Manager::NAME = "SDL2"; +const char* SDL2Manager::SDL2_ID_STRING = "SDL2"; + +const bool DEFAULT_ENABLED = false; SDL_JoystickID SDL2Manager::getInstanceId(SDL_GameController* controller) { SDL_Joystick* joystick = SDL_GameControllerGetJoystick(controller); @@ -49,6 +54,20 @@ SDL_JoystickID SDL2Manager::getInstanceId(SDL_GameController* controller) { } void SDL2Manager::init() { + loadSettings(); + + auto preferences = DependencyManager::get(); + static const QString SDL2_PLUGIN { "Game Controller" }; + { + auto getter = [this]()->bool { return _isEnabled; }; + auto setter = [this](bool value) { + _isEnabled = value; + saveSettings(); + }; + auto preference = new CheckPreference(SDL2_PLUGIN, "Enabled", getter, setter); + preferences->addPreference(preference); + } + bool initSuccess = (SDL_Init(SDL_INIT_GAMECONTROLLER | SDL_INIT_HAPTIC) == 0); if (initSuccess) { @@ -110,6 +129,27 @@ void SDL2Manager::deactivate() { InputPlugin::deactivate(); } +const char* SETTINGS_ENABLED_KEY = "enabled"; + +void SDL2Manager::saveSettings() const { + Settings settings; + QString idString = getID(); + settings.beginGroup(idString); + { + settings.setValue(QString(SETTINGS_ENABLED_KEY), _isEnabled); + } + settings.endGroup(); +} + +void SDL2Manager::loadSettings() { + Settings settings; + QString idString = getID(); + settings.beginGroup(idString); + { + _isEnabled = settings.value(SETTINGS_ENABLED_KEY, QVariant(DEFAULT_ENABLED)).toBool(); + } + settings.endGroup(); +} bool SDL2Manager::isSupported() const { return true; @@ -122,6 +162,10 @@ void SDL2Manager::pluginFocusOutEvent() { } void SDL2Manager::pluginUpdate(float deltaTime, const controller::InputCalibrationData& inputCalibrationData) { + if (!_isEnabled) { + return; + } + if (_isInitialized) { auto userInputMapper = DependencyManager::get(); for (auto joystick : _openJoysticks) { diff --git a/plugins/hifiSdl2/src/SDL2Manager.h b/plugins/hifiSdl2/src/SDL2Manager.h index 9cb4d268c0..48e779a204 100644 --- a/plugins/hifiSdl2/src/SDL2Manager.h +++ b/plugins/hifiSdl2/src/SDL2Manager.h @@ -25,6 +25,7 @@ public: // Plugin functions bool isSupported() const override; const QString getName() const override { return NAME; } + const QString getID() const override { return SDL2_ID_STRING; } QStringList getSubdeviceNames() override; @@ -39,6 +40,9 @@ public: void pluginFocusOutEvent() override; void pluginUpdate(float deltaTime, const controller::InputCalibrationData& inputCalibrationData) override; + virtual void saveSettings() const override; + virtual void loadSettings() override; + signals: void joystickAdded(Joystick* joystick); void joystickRemoved(Joystick* joystick); @@ -77,8 +81,10 @@ private: int buttonRelease() const { return SDL_RELEASED; } QMap _openJoysticks; - bool _isInitialized { false } ; + bool _isEnabled { false }; + bool _isInitialized { false }; static const char* NAME; + static const char* SDL2_ID_STRING; QStringList _subdeviceNames; }; diff --git a/plugins/hifiSixense/CMakeLists.txt b/plugins/hifiSixense/CMakeLists.txt index 14676217db..58aeb6c88f 100644 --- a/plugins/hifiSixense/CMakeLists.txt +++ b/plugins/hifiSixense/CMakeLists.txt @@ -6,9 +6,11 @@ # See the accompanying file LICENSE or http:#www.apache.org/licenses/LICENSE-2.0.html # -if (NOT ANDROID) - set(TARGET_NAME hifiSixense) - setup_hifi_plugin(Script Qml Widgets) - link_hifi_libraries(shared controllers ui plugins ui-plugins input-plugins) - target_sixense() -endif () +# FIXME if we want to re-enable this, we need to fix the mechanism for installing the +# dependency dlls `msvcr100` and `msvcp100` WITHOUT using CMAKE_INSTALL_SYSTEM_RUNTIME_LIBS +#if (NOT ANDROID) +# set(TARGET_NAME hifiSixense) +# setup_hifi_plugin(Script Qml Widgets) +# link_hifi_libraries(shared controllers ui plugins ui-plugins input-plugins) +# target_sixense() +#endif () diff --git a/plugins/hifiSixense/src/SixenseManager.cpp b/plugins/hifiSixense/src/SixenseManager.cpp index 7d443bd50d..c2c1cb2ab2 100644 --- a/plugins/hifiSixense/src/SixenseManager.cpp +++ b/plugins/hifiSixense/src/SixenseManager.cpp @@ -29,6 +29,7 @@ #include #include #include +#include #include #include @@ -46,19 +47,26 @@ static const unsigned int BUTTON_TRIGGER = 1U << 8; const glm::vec3 SixenseManager::DEFAULT_AVATAR_POSITION { -0.25f, -0.35f, -0.3f }; // in hydra frame const float SixenseManager::CONTROLLER_THRESHOLD { 0.35f }; + +bool SixenseManager::_isEnabled = false; bool SixenseManager::_sixenseLoaded = false; +#define BAIL_IF_NOT_ENABLED \ + if (!_isEnabled) { \ + return; \ + } + #define BAIL_IF_NOT_LOADED \ if (!_sixenseLoaded) { \ return; \ } - - const char* SixenseManager::NAME { "Sixense" }; -const char* SixenseManager::HYDRA_ID_STRING { "Razer Hydra" }; +const char* SixenseManager::SIXENSE_ID_STRING { "Sixense" }; -const char* MENU_PARENT { "Developer" }; +const bool DEFAULT_ENABLED = false; + +const char* MENU_PARENT{ "Developer" }; const char* MENU_NAME { "Sixense" }; const char* MENU_PATH { "Developer" ">" "Sixense" }; const char* TOGGLE_SMOOTH { "Smooth Sixense Movement" }; @@ -73,6 +81,22 @@ bool SixenseManager::isSupported() const { #endif } +void SixenseManager::init() { + loadSettings(); + + auto preferences = DependencyManager::get(); + static const QString SIXENSE_PLUGIN { "Sixense Controllers" }; + { + auto getter = [this]()->bool { return _isEnabled; }; + auto setter = [this](bool value) { + _isEnabled = value; + saveSettings(); + }; + auto preference = new CheckPreference(SIXENSE_PLUGIN, "Enabled", getter, setter); + preferences->addPreference(preference); + } +} + bool SixenseManager::activate() { InputPlugin::activate(); @@ -133,6 +157,7 @@ void SixenseManager::setSixenseFilter(bool filter) { } void SixenseManager::pluginUpdate(float deltaTime, const controller::InputCalibrationData& inputCalibrationData) { + BAIL_IF_NOT_ENABLED BAIL_IF_NOT_LOADED #ifdef HAVE_SIXENSE @@ -553,14 +578,19 @@ QString SixenseManager::InputDevice::getDefaultMappingConfig() const { return MAPPING_JSON; } +const char* SETTINGS_ENABLED_KEY = "enabled"; +const char* SETTINGS_AVATAR_POSITION_KEY = "avatarPosition"; +const char* SETTINGS_AVATAR_ROTATION_KEY = "avatarPosition"; + // virtual void SixenseManager::saveSettings() const { Settings settings; QString idString = getID(); settings.beginGroup(idString); { - settings.setVec3Value(QString("avatarPosition"), _inputDevice->_avatarPosition); - settings.setQuatValue(QString("avatarRotation"), _inputDevice->_avatarRotation); + settings.setValue(QString(SETTINGS_ENABLED_KEY), _isEnabled); + settings.setVec3Value(QString(SETTINGS_AVATAR_POSITION_KEY), _inputDevice->_avatarPosition); + settings.setQuatValue(QString(SETTINGS_AVATAR_ROTATION_KEY), _inputDevice->_avatarRotation); } settings.endGroup(); } @@ -570,8 +600,9 @@ void SixenseManager::loadSettings() { QString idString = getID(); settings.beginGroup(idString); { - settings.getVec3ValueIfValid(QString("avatarPosition"), _inputDevice->_avatarPosition); - settings.getQuatValueIfValid(QString("avatarRotation"), _inputDevice->_avatarRotation); + _isEnabled = settings.value(SETTINGS_ENABLED_KEY, QVariant(DEFAULT_ENABLED)).toBool(); + settings.getVec3ValueIfValid(QString(SETTINGS_AVATAR_POSITION_KEY), _inputDevice->_avatarPosition); + settings.getQuatValueIfValid(QString(SETTINGS_AVATAR_ROTATION_KEY), _inputDevice->_avatarRotation); } settings.endGroup(); } diff --git a/plugins/hifiSixense/src/SixenseManager.h b/plugins/hifiSixense/src/SixenseManager.h index 5b2c140868..e9d571ad8c 100644 --- a/plugins/hifiSixense/src/SixenseManager.h +++ b/plugins/hifiSixense/src/SixenseManager.h @@ -29,12 +29,14 @@ public: // Plugin functions virtual bool isSupported() const override; virtual const QString getName() const override { return NAME; } - virtual const QString getID() const override { return HYDRA_ID_STRING; } + virtual const QString getID() const override { return SIXENSE_ID_STRING; } // Sixense always seems to initialize even if the hydras are not present. Is there // a way we can properly detect whether the hydras are present? // bool isHandController() const override { return true; } + virtual void init() override; + virtual bool activate() override; virtual void deactivate() override; @@ -93,8 +95,9 @@ private: std::shared_ptr _inputDevice { std::make_shared() }; static const char* NAME; - static const char* HYDRA_ID_STRING; + static const char* SIXENSE_ID_STRING; + static bool _isEnabled; static bool _sixenseLoaded; }; diff --git a/plugins/oculus/src/OculusBaseDisplayPlugin.cpp b/plugins/oculus/src/OculusBaseDisplayPlugin.cpp index 93f4787f0f..0df504dfa2 100644 --- a/plugins/oculus/src/OculusBaseDisplayPlugin.cpp +++ b/plugins/oculus/src/OculusBaseDisplayPlugin.cpp @@ -126,15 +126,17 @@ void OculusBaseDisplayPlugin::internalDeactivate() { } bool OculusBaseDisplayPlugin::activateStandBySession() { - _session = acquireOculusSession(); if (!_session) { - return false; + _session = acquireOculusSession(); } - return true; + return _session; } void OculusBaseDisplayPlugin::deactivateSession() { - releaseOculusSession(); - _session = nullptr; + // FIXME + // Switching to Qt 5.9 exposed a race condition or similar issue that caused a crash when putting on an Rift + // while already in VR mode. Commenting these out is a workaround. + //releaseOculusSession(); + //_session = nullptr; } void OculusBaseDisplayPlugin::updatePresentPose() { //mat4 sensorResetMat; diff --git a/plugins/oculusLegacy/CMakeLists.txt b/plugins/oculusLegacy/CMakeLists.txt index f4e1bb76a2..12d0236cc2 100644 --- a/plugins/oculusLegacy/CMakeLists.txt +++ b/plugins/oculusLegacy/CMakeLists.txt @@ -13,7 +13,7 @@ if (APPLE) set(TARGET_NAME oculusLegacy) setup_hifi_plugin() - link_hifi_libraries(shared gl gpu gpu-gl plugins ui ui-plugins display-plugins input-plugins) + link_hifi_libraries(shared gl gpu gpu-gl plugins ui ui-plugins display-plugins input-plugins midi) include_hifi_library_headers(octree) diff --git a/plugins/openvr/src/OpenVrDisplayPlugin.cpp b/plugins/openvr/src/OpenVrDisplayPlugin.cpp index 7a73c91c7d..b31f55edeb 100644 --- a/plugins/openvr/src/OpenVrDisplayPlugin.cpp +++ b/plugins/openvr/src/OpenVrDisplayPlugin.cpp @@ -282,7 +282,7 @@ public: 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::TextureType_OpenGL, vr::ColorSpace_Auto }; + vr::Texture_t texture{ (void*)(uintptr_t)_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(); @@ -643,7 +643,7 @@ void OpenVrDisplayPlugin::hmdPresent() { _submitThread->waitForPresent(); } else { GLuint glTexId = getGLBackend()->getTextureID(_compositeFramebuffer->getRenderBuffer(0)); - vr::Texture_t vrTexture { (void*)glTexId, vr::TextureType_OpenGL, vr::ColorSpace_Auto }; + vr::Texture_t vrTexture { (void*)(uintptr_t)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 7e287a16c3..c8a0cb5f8b 100644 --- a/plugins/openvr/src/OpenVrHelpers.cpp +++ b/plugins/openvr/src/OpenVrHelpers.cpp @@ -129,7 +129,7 @@ void releaseOpenVrSystem() { #endif // HACK: workaround openvr crash, call submit with an invalid texture, right before VR_Shutdown. - const GLuint INVALID_GL_TEXTURE_HANDLE = -1; + const void* INVALID_GL_TEXTURE_HANDLE = (void*)(uintptr_t)-1; 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/scripts/developer/tests/basicEntityTest/shapeSpawner.js b/scripts/developer/tests/basicEntityTest/shapeSpawner.js new file mode 100644 index 0000000000..9fcaf3a3db --- /dev/null +++ b/scripts/developer/tests/basicEntityTest/shapeSpawner.js @@ -0,0 +1,30 @@ +// compute a position to create the object relative to avatar +var forwardOffset = Vec3.multiply(2.0, Quat.getFront(MyAvatar.orientation)); +var objectPosition = Vec3.sum(MyAvatar.position, forwardOffset); + +var LIFETIME = 1800; //seconds +var DIM_HEIGHT = 1, DIM_WIDTH = 1, DIM_DEPTH = 1; +var COLOR_R = 100, COLOR_G = 10, COLOR_B = 200; + +var properties = { + name: "ShapeSpawnTest", + type: "Shape", + shape: "Cylinder", + dimensions: {x: DIM_WIDTH, y: DIM_HEIGHT, z: DIM_DEPTH}, + color: {red: COLOR_R, green: COLOR_G, blue: COLOR_B}, + position: objectPosition, + lifetime: LIFETIME, +}; + +// create the object +var entityId = Entities.addEntity(properties); + +function cleanup() { + Entities.deleteEntity(entityId); +} + +// delete the object when this script is stopped +Script.scriptEnding.connect(cleanup); + + + diff --git a/scripts/developer/tests/entityLookupCostMeasurement.js b/scripts/developer/tests/entityLookupCostMeasurement.js new file mode 100644 index 0000000000..15697fe13d --- /dev/null +++ b/scripts/developer/tests/entityLookupCostMeasurement.js @@ -0,0 +1,104 @@ +// Creates a large number of entities on the cardinal planes of the octree (all +// objects will live in the root octree element). Measures how long it takes +// to update the properties of the first and last entity. The difference +// between the two measurements shows how the cost of lookup changes as a +// function of the number of entities. For best results run this in an +// otherwise empty domain. + +var firstId; +var lastId; +var NUM_ENTITIES_ON_SIDE = 25; + +// create the objects +createObjects = function () { + var STRIDE = 0.75; + var WIDTH = 0.5; + var DIMENSIONS = { x: WIDTH, y: WIDTH, z: WIDTH }; + var LIFETIME = 20; + + var properties = { + name: "", + type : "Box", + dimensions : DIMENSIONS, + position : { x: 0, y: 0, z: 0}, + lifetime : LIFETIME, + color : { red:255, green: 64, blue: 255 } + }; + + // xy + var planeName = "xy"; + for (var i = 0; i < NUM_ENTITIES_ON_SIDE; ++i) { + for (var j = 0; j < NUM_ENTITIES_ON_SIDE; ++j) { + properties.name = "Box-" + planeName + "-" + i + "." + j; + properties.position = { x: i * STRIDE, y: j * STRIDE, z: 0 }; + var red = i * 255 / NUM_ENTITIES_ON_SIDE; + var green = j * 255 / NUM_ENTITIES_ON_SIDE; + var blue = 0; + properties.color = { red: red, green: green, blue: blue }; + if (i == 0 && j == 0) { + firstId = Entities.addEntity(properties); + } else { + Entities.addEntity(properties); + } + } + } + + // yz + var planeName = "yz"; + for (var i = 0; i < NUM_ENTITIES_ON_SIDE; ++i) { + for (var j = 0; j < NUM_ENTITIES_ON_SIDE; ++j) { + properties.name = "Box-" + planeName + "-" + i + "." + j; + properties.position = { x: 0, y: i * STRIDE, z: j * STRIDE }; + var red = 0; + var green = i * 255 / NUM_ENTITIES_ON_SIDE; + var blue = j * 255 / NUM_ENTITIES_ON_SIDE; + properties.color = { red: red, green: green, blue: blue }; + Entities.addEntity(properties); + } + } + + // zx + var planeName = "zx"; + for (var i = 0; i < NUM_ENTITIES_ON_SIDE; ++i) { + for (var j = 0; j < NUM_ENTITIES_ON_SIDE; ++j) { + properties.name = "Box-" + planeName + "-" + i + "." + j; + properties.position = { x: j * STRIDE, y: 0, z: i * STRIDE }; + var red = j * 255 / NUM_ENTITIES_ON_SIDE; + var green = 0; + var blue = i * 255 / NUM_ENTITIES_ON_SIDE; + properties.color = { red: red, green: green, blue: blue }; + lastId = Entities.addEntity(properties); + } + } +}; + +createObjects(); + +// measure the time it takes to edit the first and last entities many times +// (requires a lookup by entityId each time) +changeProperties = function (id) { + var newProperties = { color : { red: 255, green: 255, blue: 255 } }; + Entities.editEntity(id, newProperties); +} + +// first +var NUM_CHANGES = 10000; +var firstStart = Date.now(); +for (var k = 0; k < NUM_CHANGES; ++k) { + changeProperties(firstId); +} +var firstEnd = Date.now(); +var firstDt = firstEnd - firstStart; + +// last +var lastStart = Date.now(); +for (var k = 0; k < NUM_CHANGES; ++k) { + changeProperties(lastId); +} +var lastEnd = Date.now(); +var lastDt = lastEnd - lastStart; + +// print the results +var numEntities = 3 * NUM_ENTITIES_ON_SIDE * NUM_ENTITIES_ON_SIDE; +print("numEntities = " + numEntities + " numEdits = " + NUM_CHANGES + " firstDt = " + firstDt + " lastDt = " + lastDt); + diff --git a/scripts/developer/tests/puck-attach.js b/scripts/developer/tests/puck-attach.js index 2d0a2e6d02..04d5db5710 100644 --- a/scripts/developer/tests/puck-attach.js +++ b/scripts/developer/tests/puck-attach.js @@ -1,21 +1,13 @@ // // Created by Anthony J. Thibault on 2017/06/20 +// Modified by Robbie Uvanni to support multiple pucks and easier placement of pucks on entities, on 2017/08/01 // 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 // // When this script is running, a new app button, named "PUCKTACH", will be added to the toolbar/tablet. -// Click this app to bring up the puck attachment panel. This panel contains the following fields. -// -// * Tracked Object - A drop down list of all the available pucks found. If no pucks are found this list will only have a single NONE entry. -// Closing and re-opening the app will refresh this list. -// * Model URL - A model url of the model you wish to be attached to the specified puck. -// * Position X, Y, Z - used to apply an offset between the puck and the attached model. -// * Rot X, Y, Z - used to apply euler angle offsets, in degrees, between the puck and the attached model. -// * Create Attachment - when this button is pressed a new Entity will be created at the specified puck's location. -// If a puck atttachment already exists, it will be destroyed before the new entity is created. -// * Destroy Attachmetn - destroies the entity attached to the puck. +// Click this app to bring up the puck attachment panel. // /* eslint indent: ["error", 4, { "outerIIFEBody": 0 }] */ @@ -25,7 +17,7 @@ Script.include("/~/system/libraries/Xform.js"); (function() { // BEGIN LOCAL_SCOPE var TABLET_BUTTON_NAME = "PUCKTACH"; -var HTML_URL = "https://s3.amazonaws.com/hifi-public/tony/html/puck-attach.html"; +var TABLET_APP_URL = "https://hifi-content.s3.amazonaws.com/seefo/production/puck-attach/puck-attach.html"; var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); var tabletButton = tablet.addButton({ @@ -34,32 +26,13 @@ var tabletButton = tablet.addButton({ activeIcon: "https://s3.amazonaws.com/hifi-public/tony/icons/puck-a.svg" }); -tabletButton.clicked.connect(function () { - if (shown) { - tablet.gotoHomeScreen(); - } else { - tablet.gotoWebScreen(HTML_URL); - } -}); - var shown = false; -var attachedEntity; -var attachedObj; - function onScreenChanged(type, url) { - if (type === "Web" && url === HTML_URL) { + if (type === "Web" && url === TABLET_APP_URL) { tabletButton.editProperties({isActive: true}); if (!shown) { // hook up to event bridge tablet.webEventReceived.connect(onWebEventReceived); - - Script.setTimeout(function () { - // send available tracked objects to the html running in the tablet. - var availableTrackedObjects = getAvailableTrackedObjects(); - tablet.emitScriptEvent(JSON.stringify(availableTrackedObjects)); - - // print("PUCK-ATTACH: availableTrackedObjects = " + JSON.stringify(availableTrackedObjects)); - }, 1000); // wait 1 sec before sending.. } shown = true; } else { @@ -71,104 +44,264 @@ function onScreenChanged(type, url) { shown = false; } } - tablet.screenChanged.connect(onScreenChanged); +function pad(num, size) { + var tempString = "000000000" + num; + return tempString.substr(tempString.length - size); +} function indexToTrackedObjectName(index) { return "TrackedObject" + pad(index, 2); } - function getAvailableTrackedObjects() { var available = []; var NUM_TRACKED_OBJECTS = 16; var i; for (i = 0; i < NUM_TRACKED_OBJECTS; i++) { var key = indexToTrackedObjectName(i); - var pose = Controller.getPoseValue(Controller.Hardware.Vive[key]); + var pose = Controller.getPoseValue(Controller.Standard[key]); if (pose && pose.valid) { available.push(i); } } return available; } - -function attach(obj) { - attachedEntity = Entities.addEntity({ - type: "Model", - name: "puck-attach-entity", - modelURL: obj.modelurl - }); - attachedObj = obj; - var localPos = {x: Number(obj.posx), y: Number(obj.posy), z: Number(obj.posz)}; - var localRot = Quat.fromVec3Degrees({x: Number(obj.rotx), y: Number(obj.roty), z: Number(obj.rotz)}); - attachedObj.localXform = new Xform(localRot, localPos); - var key = indexToTrackedObjectName(Number(attachedObj.puckno)); - attachedObj.key = key; - - // print("PUCK-ATTACH: attachedObj = " + JSON.stringify(attachedObj)); - - Script.update.connect(update); - update(); +function sendAvailableTrackedObjects() { + tablet.emitScriptEvent(JSON.stringify({ + pucks: getAvailableTrackedObjects(), + selectedPuck: ((lastPuck === undefined) ? -1 : lastPuck.name) + })); } -function remove() { - if (attachedEntity) { - Script.update.disconnect(update); - Entities.deleteEntity(attachedEntity); - attachedEntity = undefined; +function getRelativePosition(origin, rotation, offset) { + var relativeOffset = Vec3.multiplyQbyV(rotation, offset); + var worldPosition = Vec3.sum(origin, relativeOffset); + return worldPosition; +} +function getPropertyForEntity(entityID, propertyName) { + return Entities.getEntityProperties(entityID, [propertyName])[propertyName]; +} +function entityExists(entityID) { + return Object.keys(Entities.getEntityProperties(entityID)).length > 0; +} + +var VIVE_PUCK_MODEL = "http://content.highfidelity.com/seefo/production/puck-attach/vive_tracker_puck.obj"; +var VIVE_PUCK_DIMENSIONS = { x: 0.0945, y: 0.0921, z: 0.0423 }; // 1/1000th scale of model +var VIVE_PUCK_SEARCH_DISTANCE = 1.5; // metres +var VIVE_PUCK_SPAWN_DISTANCE = 0.5; // metres +var VIVE_PUCK_TRACKED_OBJECT_MAX_DISTANCE = 10.0; // metres +var VIVE_PUCK_NAME = "Tracked Puck"; + +var trackedPucks = { }; +var lastPuck; + +function createPuck(puck) { + // create a puck entity and add it to our list of pucks + var action = indexToTrackedObjectName(puck.puckno); + var pose = Controller.getPoseValue(Controller.Standard[action]); + + if (pose && pose.valid) { + var spawnOffset = Vec3.multiply(Vec3.FRONT, VIVE_PUCK_SPAWN_DISTANCE); + var spawnPosition = getRelativePosition(MyAvatar.position, MyAvatar.orientation, spawnOffset); + + // should be an overlay + var puckEntityProperties = { + name: "Tracked Puck", + type: "Model", + modelURL: VIVE_PUCK_MODEL, + dimensions: VIVE_PUCK_DIMENSIONS, + position: spawnPosition, + userData: '{ "grabbableKey": { "grabbable": true, "kinematic": false } }' + }; + + var puckEntityID = Entities.addEntity(puckEntityProperties); + + // if we've already created this puck, destroy it + if (trackedPucks.hasOwnProperty(puck.puckno)) { + destroyPuck(puck.puckno); + } + // if we had an unfinalized puck, destroy it + if (lastPuck !== undefined) { + destroyPuck(lastPuck.name); + } + // create our new unfinalized puck + trackedPucks[puck.puckno] = { + puckEntityID: puckEntityID, + trackedEntityID: "" + }; + lastPuck = trackedPucks[puck.puckno]; + lastPuck.name = Number(puck.puckno); } - attachedObj = undefined; } +function finalizePuck(puckName) { + // find nearest entity and change its parent to the puck + + if (!trackedPucks.hasOwnProperty(puckName)) { + print('2'); + return; + } + if (lastPuck === undefined) { + print('3'); + return; + } + if (lastPuck.name !== Number(puckName)) { + print('1'); + return; + } + + var puckPosition = getPropertyForEntity(lastPuck.puckEntityID, "position"); + var foundEntities = Entities.findEntities(puckPosition, VIVE_PUCK_SEARCH_DISTANCE); -function pad(num, size) { - var tempString = "000000000" + num; - return tempString.substr(tempString.length - size); -} + var foundEntity; + var leastDistance = Number.MAX_VALUE; -function update() { - if (attachedEntity && attachedObj && Controller.Hardware.Vive) { - var pose = Controller.getPoseValue(Controller.Hardware.Vive[attachedObj.key]); - var avatarXform = new Xform(MyAvatar.orientation, MyAvatar.position); - var puckXform = new Xform(pose.rotation, pose.translation); - var finalXform = Xform.mul(avatarXform, Xform.mul(puckXform, attachedObj.localXform)); - if (pose && pose.valid) { - Entities.editEntity(attachedEntity, { - position: finalXform.pos, - rotation: finalXform.rot - }); - } else { - if (pose) { - print("PUCK-ATTACH: WARNING: invalid pose for " + attachedObj.key); - } else { - print("PUCK-ATTACH: WARNING: could not find key " + attachedObj.key); + for (var i = 0; i < foundEntities.length; i++) { + var entity = foundEntities[i]; + + if (getPropertyForEntity(entity, "name") !== VIVE_PUCK_NAME) { + var entityPosition = getPropertyForEntity(entity, "position"); + var d = Vec3.distance(entityPosition, puckPosition); + + if (d < leastDistance) { + leastDistance = d; + foundEntity = entity; } } } + + if (foundEntity) { + lastPuck.trackedEntityID = foundEntity; + // remember the userdata and collisionless flag for the tracked entity since + // we're about to remove it and make it ungrabbable and collisionless + lastPuck.trackedEntityUserData = getPropertyForEntity(foundEntity, "userData"); + lastPuck.trackedEntityCollisionFlag = getPropertyForEntity(foundEntity, "collisionless"); + // update properties of the tracked entity + Entities.editEntity(lastPuck.trackedEntityID, { + "parentID": lastPuck.puckEntityID, + "userData": '{ "grabbableKey": { "grabbable": false } }', + "collisionless": 1 + }); + // remove reference to puck since it is now calibrated and finalized + lastPuck = undefined; + } +} +function updatePucks() { + // for each puck, update its position and orientation + for (var puckName in trackedPucks) { + if (!trackedPucks.hasOwnProperty(puckName)) { + continue; + } + var action = indexToTrackedObjectName(puckName); + var pose = Controller.getPoseValue(Controller.Standard[action]); + if (pose && pose.valid) { + var puck = trackedPucks[puckName]; + if (puck.trackedEntityID) { + if (entityExists(puck.trackedEntityID)) { + var avatarXform = new Xform(MyAvatar.orientation, MyAvatar.position); + var puckXform = new Xform(pose.rotation, pose.translation); + var finalXform = Xform.mul(avatarXform, puckXform); + + var d = Vec3.distance(MyAvatar.position, finalXform.pos); + if (d > VIVE_PUCK_TRACKED_OBJECT_MAX_DISTANCE) { + print('tried to move tracked object too far away: ' + d); + return; + } + + Entities.editEntity(puck.puckEntityID, { + position: finalXform.pos, + rotation: finalXform.rot + }); + + // in case someone grabbed both entities and destroyed the + // child/parent relationship + Entities.editEntity(puck.trackedEntityID, { + parentID: puck.puckEntityID + }); + } else { + destroyPuck(puckName); + } + } + } + } +} +function destroyPuck(puckName) { + // unparent entity and delete its parent + if (!trackedPucks.hasOwnProperty(puckName)) { + return; + } + + var puck = trackedPucks[puckName]; + var puckEntityID = puck.puckEntityID; + var trackedEntityID = puck.trackedEntityID; + + // remove the puck as a parent entity and restore the tracked entities + // former userdata and collision flag + Entities.editEntity(trackedEntityID, { + "parentID": "{00000000-0000-0000-0000-000000000000}", + "userData": puck.trackedEntityUserData, + "collisionless": puck.trackedEntityCollisionFlag + }); + + delete trackedPucks[puckName]; + + // in some cases, the entity deletion may occur before the parent change + // has been processed, resulting in both the puck and the tracked entity + // to be deleted so we wait 100ms before deleting the puck, assuming + // that the parent change has occured + Script.setTimeout(function() { + // delete the puck + Entities.deleteEntity(puckEntityID); + }, 100); +} +function destroyPucks() { + // remove all pucks and unparent entities + for (var puckName in trackedPucks) { + if (trackedPucks.hasOwnProperty(puckName)) { + destroyPuck(puckName); + } + } } function onWebEventReceived(msg) { var obj = {}; - try { + + try { obj = JSON.parse(msg); - } catch (err) { - return; + } catch (err) { + return; } - if (obj.cmd === "attach") { - remove(); - attach(obj); - } else if (obj.cmd === "detach") { - remove(); + + switch (obj.cmd) { + case "ready": + sendAvailableTrackedObjects(); + break; + case "create": + createPuck(obj); + break; + case "finalize": + finalizePuck(obj.puckno); + break; + case "destroy": + destroyPuck(obj.puckno); + break; } } +Script.update.connect(updatePucks); Script.scriptEnding.connect(function () { - remove(); tablet.removeButton(tabletButton); + destroyPucks(); if (shown) { tablet.webEventReceived.disconnect(onWebEventReceived); tablet.gotoHomeScreen(); } tablet.screenChanged.disconnect(onScreenChanged); }); - -}()); // END LOCAL_SCOPE +tabletButton.clicked.connect(function () { + if (shown) { + tablet.gotoHomeScreen(); + } else { + tablet.gotoWebScreen(TABLET_APP_URL); + } +}); +}()); // END LOCAL_SCOPE \ No newline at end of file diff --git a/scripts/developer/tests/viveTrackedObjects.js b/scripts/developer/tests/viveTrackedObjects.js index 4155afb82b..1d60f658d9 100644 --- a/scripts/developer/tests/viveTrackedObjects.js +++ b/scripts/developer/tests/viveTrackedObjects.js @@ -23,7 +23,7 @@ var BLUE = {x: 0, y: 0, 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]); + var pose = Controller.getPoseValue(Controller.Standard[key]); if (pose.valid) { DebugDraw.addMyAvatarMarker(key, pose.rotation, pose.translation, BLUE); } else { diff --git a/scripts/system/chat.js b/scripts/system/chat.js index d03c6aae98..fa997e20cc 100644 --- a/scripts/system/chat.js +++ b/scripts/system/chat.js @@ -43,6 +43,7 @@ var speechBubbleOffset = {x: 0, y: 0.3, z: 0.0}; // The offset from the joint to whic the speech bubble is attached. var speechBubbleJointName = 'Head'; // The name of the joint to which the speech bubble is attached. var speechBubbleLineHeight = 0.05; // The height of a line of text in the speech bubble. + var SPEECH_BUBBLE_MAX_WIDTH = 1; // meters // Load the persistent variables from the Settings, with defaults. function loadSettings() { @@ -370,14 +371,14 @@ // Change the avatar size to bigger. function biggerSize() { //print("biggerSize"); - logMessage("Increasing avatar size bigger!", null); + logMessage("Increasing avatar size", null); MyAvatar.increaseSize(); } // Change the avatar size to smaller. function smallerSize() { //print("smallerSize"); - logMessage("Decreasing avatar size smaler!", null); + logMessage("Decreasing avatar size", null); MyAvatar.decreaseSize(); } @@ -470,14 +471,13 @@ case '?': case 'help': - logMessage('Type "/?" or "/help" for help, which is this!', null); - logMessage('Type "/name " to set your chat name, or "/name" to use your display name, or a random name if that is not defined.', null); - logMessage('Type "/shutup" to shut up your overhead chat message.', null); - logMessage('Type "/say " to say something.', null); - logMessage('Type "/clear" to clear your cha, nullt log.', null); - logMessage('Type "/who" to ask who is h, nullere to chat.', null); - logMessage('Type "/bigger", "/smaller" or "/normal" to change, null your avatar size.', null); - logMessage('(Sorry, that\'s all there is so far!)', null); + logMessage('Type "/?" or "/help" for help', null); + logMessage('Type "/name " to set your chat name, or "/name" to use your display name. If your display name is not defined, a random name will be used.', null); + logMessage('Type "/close" to close your overhead chat message.', null); + logMessage('Type "/say " to display a new message.', null); + logMessage('Type "/clear" to clear your chat log.', null); + logMessage('Type "/who" to ask who is in the chat session.', null); + logMessage('Type "/bigger", "/smaller" or "/normal" to change your avatar size.', null); break; case 'name': @@ -498,9 +498,9 @@ } break; - case 'shutup': + case 'close': popDownSpeechBubble(); - logMessage('Overhead chat message shut up.', null); + logMessage('Overhead chat message closed.', null); break; case 'say': @@ -646,8 +646,16 @@ //print("updateSpeechBubble:", "speechBubbleMessage", speechBubbleMessage, "textSize", textSize.width, textSize.height); var fudge = 0.02; + var width = textSize.width + fudge; - var height = textSize.height + fudge; + var height = speechBubbleLineHeight + fudge; + + if (textSize.width >= SPEECH_BUBBLE_MAX_WIDTH) { + var numLines = Math.ceil(width); + height = speechBubbleLineHeight * numLines + fudge; + width = SPEECH_BUBBLE_MAX_WIDTH; + } + dimensions = { x: width, y: height, @@ -673,6 +681,7 @@ Vec3.sum( headPosition, rotatedOffset); + position.y += height / 2; // offset based on half of bubble height speechBubbleParams.position = position; if (!speechBubbleTextID) { diff --git a/scripts/system/controllers/handControllerGrab.js b/scripts/system/controllers/handControllerGrab.js index 04921fe14d..bd8ab26506 100644 --- a/scripts/system/controllers/handControllerGrab.js +++ b/scripts/system/controllers/handControllerGrab.js @@ -187,6 +187,11 @@ var DEFAULT_GRABBABLE_DATA = { var USE_BLACKLIST = true; var blacklist = []; +var hoveredEntityID = false; +var contextOverlayTimer = false; +var entityWithContextOverlay = false; +var contextualHand = -1; + var FORBIDDEN_GRAB_NAMES = ["Grab Debug Entity", "grab pointer"]; var FORBIDDEN_GRAB_TYPES = ["Unknown", "Light", "PolyLine", "Zone"]; @@ -222,6 +227,7 @@ CONTROLLER_STATE_MACHINE[STATE_OFF] = { CONTROLLER_STATE_MACHINE[STATE_SEARCHING] = { name: "searching", enterMethod: "searchEnter", + exitMethod: "searchExit", updateMethod: "search" }; CONTROLLER_STATE_MACHINE[STATE_DISTANCE_HOLDING] = { @@ -349,7 +355,9 @@ function projectOntoXYPlane(worldPos, position, rotation, dimensions, registrati function projectOntoEntityXYPlane(entityID, worldPos) { var props = entityPropertiesCache.getProps(entityID); - return projectOntoXYPlane(worldPos, props.position, props.rotation, props.dimensions, props.registrationPoint); + if (props) { + return projectOntoXYPlane(worldPos, props.position, props.rotation, props.dimensions, props.registrationPoint); + } } function projectOntoOverlayXYPlane(overlayID, worldPos) { @@ -367,7 +375,9 @@ function projectOntoOverlayXYPlane(overlayID, worldPos) { dimensions = Vec3.multiplyVbyV(Vec3.multiply(resolution, INCHES_TO_METERS / dpi), scale); } else { dimensions = Overlays.getProperty(overlayID, "dimensions"); - dimensions.z = 0.01; // overlay dimensions are 2D, not 3D. + if (dimensions.z) { + dimensions.z = 0.01; // overlay dimensions are 2D, not 3D. + } } return projectOntoXYPlane(worldPos, position, rotation, dimensions, DEFAULT_REGISTRATION_POINT); @@ -1032,9 +1042,18 @@ EquipHotspotBuddy.prototype.update = function(deltaTime, timestamp) { function getControllerJointIndex(hand) { if (HMD.isHandControllerAvailable()) { - return MyAvatar.getJointIndex(hand === RIGHT_HAND ? - "_CONTROLLER_RIGHTHAND" : - "_CONTROLLER_LEFTHAND"); + var controllerJointIndex = -1; + if (Camera.mode === "first person") { + controllerJointIndex = MyAvatar.getJointIndex(hand === RIGHT_HAND ? + "_CONTROLLER_RIGHTHAND" : + "_CONTROLLER_LEFTHAND"); + } else if (Camera.mode === "third person") { + controllerJointIndex = MyAvatar.getJointIndex(hand === RIGHT_HAND ? + "_CAMERA_RELATIVE_CONTROLLER_RIGHTHAND" : + "_CAMERA_RELATIVE_CONTROLLER_LEFTHAND"); + } + + return controllerJointIndex; } return MyAvatar.getJointIndex("Head"); @@ -1801,9 +1820,14 @@ function MyController(hand) { if (isInEditMode() && !this.isNearStylusTarget && HMD.isHandControllerAvailable()) { // Always showing lasers while in edit mode and hands/stylus is not active. + var rayPickInfo = this.calcRayPickInfo(this.hand); - this.intersectionDistance = (rayPickInfo.entityID || rayPickInfo.overlayID) ? rayPickInfo.distance : 0; - this.searchIndicatorOn(rayPickInfo.searchRay); + if (rayPickInfo.isValid) { + this.intersectionDistance = (rayPickInfo.entityID || rayPickInfo.overlayID) ? rayPickInfo.distance : 0; + this.searchIndicatorOn(rayPickInfo.searchRay); + } else { + this.searchIndicatorOff(); + } } else { this.searchIndicatorOff(); } @@ -1852,12 +1876,14 @@ function MyController(hand) { this.calcRayPickInfo = function(hand, pickRayOverride) { var pickRay; + var valid = true if (pickRayOverride) { pickRay = pickRayOverride; } else { var controllerLocation = getControllerWorldLocation(this.handToController(), true); var worldHandPosition = controllerLocation.position; var worldHandRotation = controllerLocation.orientation; + valid = !(worldHandPosition === undefined); pickRay = { origin: PICK_WITH_HAND_RAY ? worldHandPosition : Camera.position, @@ -1872,7 +1898,8 @@ function MyController(hand) { entityID: null, overlayID: null, searchRay: pickRay, - distance: PICK_MAX_DISTANCE + distance: PICK_MAX_DISTANCE, + isValid: valid }; var now = Date.now(); @@ -2172,6 +2199,14 @@ function MyController(hand) { } }; + this.searchExit = function () { + contextualHand = -1; + if (hoveredEntityID) { + Entities.sendHoverLeaveEntity(hoveredEntityID, pointerEvent); + } + hoveredEntityID = false; + }; + this.search = function(deltaTime, timestamp) { var _this = this; var name; @@ -2201,6 +2236,65 @@ function MyController(hand) { entityPropertiesCache.addEntity(rayPickInfo.entityID); } + pointerEvent = { + type: "Move", + id: this.hand + 1, // 0 is reserved for hardware mouse + pos2D: projectOntoEntityXYPlane(rayPickInfo.entityID, rayPickInfo.intersection), + pos3D: rayPickInfo.intersection, + normal: rayPickInfo.normal, + direction: rayPickInfo.searchRay.direction, + button: "None" + }; + if (rayPickInfo.entityID) { + if (hoveredEntityID !== rayPickInfo.entityID) { + if (contextOverlayTimer) { + Script.clearTimeout(contextOverlayTimer); + contextOverlayTimer = false; + } + if (hoveredEntityID) { + Entities.sendHoverLeaveEntity(hoveredEntityID, pointerEvent); + } + hoveredEntityID = rayPickInfo.entityID; + Entities.sendHoverEnterEntity(hoveredEntityID, pointerEvent); + } + + // If we already have a context overlay, we don't want to move it to + // another entity while we're searching. + if (!entityWithContextOverlay && !contextOverlayTimer) { + contextOverlayTimer = Script.setTimeout(function () { + if (rayPickInfo.entityID === hoveredEntityID && + !entityWithContextOverlay && + contextualHand !== -1 && + contextOverlayTimer) { + var pointerEvent = { + type: "Move", + id: contextualHand + 1, // 0 is reserved for hardware mouse + pos2D: projectOntoEntityXYPlane(rayPickInfo.entityID, rayPickInfo.intersection), + pos3D: rayPickInfo.intersection, + normal: rayPickInfo.normal, + direction: rayPickInfo.searchRay.direction, + button: "Secondary" + }; + if (ContextOverlay.createOrDestroyContextOverlay(rayPickInfo.entityID, pointerEvent)) { + entityWithContextOverlay = rayPickInfo.entityID; + hoveredEntityID = false; + } + } + contextOverlayTimer = false; + }, 500); + contextualHand = this.hand; + } + } else { + if (hoveredEntityID) { + Entities.sendHoverLeaveEntity(hoveredEntityID, pointerEvent); + hoveredEntityID = false; + } + if (contextOverlayTimer) { + Script.clearTimeout(contextOverlayTimer); + contextOverlayTimer = false; + } + } + var candidateHotSpotEntities = Entities.findEntities(handPosition, MAX_EQUIP_HOTSPOT_RADIUS); entityPropertiesCache.addEntities(candidateHotSpotEntities); @@ -2405,8 +2499,11 @@ function MyController(hand) { button: "None" }; - this.hoverEntity = entity; - Entities.sendHoverEnterEntity(entity, pointerEvent); + if (this.hoverEntity !== entity) { + Entities.sendHoverLeaveEntity(this.hoverEntity, pointerEvent); + this.hoverEntity = entity; + Entities.sendHoverEnterEntity(this.hoverEntity, pointerEvent); + } } // send mouse events for button highlights and tooltips. @@ -2455,25 +2552,25 @@ function MyController(hand) { var pointerEvent; if (rayPickInfo.overlayID) { var overlay = rayPickInfo.overlayID; - if (Overlays.getProperty(overlay, "type") != "web3d") { - return false; - } - if (Overlays.keyboardFocusOverlay != overlay) { + if ((Overlays.getProperty(overlay, "type") === "web3d") && 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" - }; + pointerEvent = { + type: "Move", + id: HARDWARE_MOUSE_ID, + pos2D: projectOntoOverlayXYPlane(overlay, rayPickInfo.intersection), + pos3D: rayPickInfo.intersection, + normal: rayPickInfo.normal, + direction: rayPickInfo.searchRay.direction, + button: "None" + }; + if (this.hoverOverlay !== overlay) { + Overlays.sendHoverLeaveOverlay(this.hoverOverlay, pointerEvent); this.hoverOverlay = overlay; - Overlays.sendHoverEnterOverlay(overlay, pointerEvent); + Overlays.sendHoverEnterOverlay(this.hoverOverlay, pointerEvent); } // Send mouse events for button highlights and tooltips. @@ -3449,6 +3546,15 @@ function MyController(hand) { var existingSearchDistance = this.searchSphereDistance; this.release(); + if (hoveredEntityID) { + Entities.sendHoverLeaveEntity(hoveredEntityID, pointerEvent); + hoveredEntityID = false; + } + if (entityWithContextOverlay) { + ContextOverlay.destroyContextOverlay(entityWithContextOverlay); + entityWithContextOverlay = false; + } + if (isInEditMode()) { this.searchSphereDistance = existingSearchDistance; } @@ -3569,7 +3675,7 @@ function MyController(hand) { }; this.overlayLaserTouchingEnter = function () { - // Test for intersection between controller laser and Web overlay plane. + // Test for intersection between controller laser and overlay plane. var controllerLocation = getControllerWorldLocation(this.handToController(), true); var intersectInfo = handLaserIntersectOverlay(this.grabbedOverlay, controllerLocation); if (intersectInfo) { diff --git a/scripts/system/controllers/teleport.js b/scripts/system/controllers/teleport.js index b058ec670f..15ba314a1d 100644 --- a/scripts/system/controllers/teleport.js +++ b/scripts/system/controllers/teleport.js @@ -381,7 +381,13 @@ function getAvatarFootOffset() { } if (footJointIndex != -1) { // default vertical offset from foot to avatar root. - return -MyAvatar.getAbsoluteDefaultJointTranslationInObjectFrame(footJointIndex).y; + var footPos = MyAvatar.getAbsoluteDefaultJointTranslationInObjectFrame(footJointIndex); + if (footPos.x === 0 && footPos.y === 0 && footPos.z === 0.0) { + // if footPos is exactly zero, it's probably wrong because avatar is currently loading, fall back to default. + return DEFAULT_ROOT_TO_FOOT_OFFSET * MyAvatar.scale; + } else { + return -footPos.y; + } } else { return DEFAULT_ROOT_TO_FOOT_OFFSET * MyAvatar.scale; } diff --git a/scripts/system/edit.js b/scripts/system/edit.js index acea42237b..c141c7cd52 100644 --- a/scripts/system/edit.js +++ b/scripts/system/edit.js @@ -1,5 +1,3 @@ -"use strict"; - // edit.js // // Created by Brad Hefta-Gaub on 10/2/14. @@ -17,6 +15,8 @@ (function() { // BEGIN LOCAL_SCOPE +"use strict"; + var HIFI_PUBLIC_BUCKET = "http://s3.amazonaws.com/hifi-public/"; var EDIT_TOGGLE_BUTTON = "com.highfidelity.interface.system.editButton"; var SYSTEM_TOOLBAR = "com.highfidelity.interface.toolbar.system"; @@ -26,11 +26,8 @@ Script.include([ "libraries/stringHelpers.js", "libraries/dataViewHelpers.js", "libraries/progressDialog.js", - "libraries/entitySelectionTool.js", - "libraries/ToolTip.js", - "libraries/entityCameraTool.js", "libraries/gridTool.js", "libraries/entityList.js", @@ -41,19 +38,19 @@ Script.include([ var selectionDisplay = SelectionDisplay; var selectionManager = SelectionManager; -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"); +var PARTICLE_SYSTEM_URL = Script.resolvePath("assets/images/icon-particles.svg"); +var POINT_LIGHT_URL = Script.resolvePath("assets/images/icon-point-light.svg"); +var 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, - } + }; } }); @@ -94,7 +91,7 @@ selectionManager.addEventListener(function () { } }); -const KEY_P = 80; //Key code for letter p used for Parenting hotkey. +var 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; @@ -123,6 +120,8 @@ var SETTING_EASE_ON_FOCUS = "cameraEaseOnFocus"; var SETTING_SHOW_LIGHTS_AND_PARTICLES_IN_EDIT_MODE = "showLightsAndParticlesInEditMode"; var SETTING_SHOW_ZONES_IN_EDIT_MODE = "showZonesInEditMode"; +var CREATE_ENABLED_ICON = "icons/tablet-icons/edit-i.svg"; +var CREATE_DISABLED_ICON = "icons/tablet-icons/edit-disabled.svg"; // marketplace info, etc. not quite ready yet. var SHOULD_SHOW_PROPERTY_MENU = false; @@ -130,6 +129,7 @@ var INSUFFICIENT_PERMISSIONS_ERROR_MSG = "You do not have the necessary permissi var INSUFFICIENT_PERMISSIONS_IMPORT_ERROR_MSG = "You do not have the necessary permissions to place items on this domain."; var isActive = false; +var createButton = null; var IMPORTING_SVO_OVERLAY_WIDTH = 144; var IMPORTING_SVO_OVERLAY_HEIGHT = 30; @@ -272,7 +272,8 @@ var toolBar = (function () { properties.userData = JSON.stringify({ grabbableKey: { grabbable: true } }); } entityID = Entities.addEntity(properties); - if (properties.type == "ParticleEffect") { + + if (properties.type === "ParticleEffect") { selectParticleEntity(entityID); } @@ -334,6 +335,8 @@ var toolBar = (function () { var SHAPE_TYPE_SIMPLE_HULL = 1; var SHAPE_TYPE_SIMPLE_COMPOUND = 2; var SHAPE_TYPE_STATIC_MESH = 3; + var SHAPE_TYPE_BOX = 4; + var SHAPE_TYPE_SPHERE = 5; var DYNAMIC_DEFAULT = false; function handleNewModelDialogResult(result) { @@ -350,6 +353,12 @@ var toolBar = (function () { case SHAPE_TYPE_STATIC_MESH: shapeType = "static-mesh"; break; + case SHAPE_TYPE_BOX: + shapeType = "box"; + break; + case SHAPE_TYPE_SPHERE: + shapeType = "sphere"; + break; default: shapeType = "none"; } @@ -385,7 +394,6 @@ var toolBar = (function () { function initialize() { Script.scriptEnding.connect(cleanup); - Window.domainChanged.connect(function () { that.setActive(false); that.clearEntityList(); @@ -397,21 +405,28 @@ var toolBar = (function () { } }); + var createButtonIconRsrc = ((Entities.canRez() || Entities.canRezTmp()) ? CREATE_ENABLED_ICON : CREATE_DISABLED_ICON); tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); activeButton = tablet.addButton({ - icon: "icons/tablet-icons/edit-i.svg", + icon: createButtonIconRsrc, activeIcon: "icons/tablet-icons/edit-a.svg", text: "CREATE", sortOrder: 10 }); + createButton = activeButton; tablet.screenChanged.connect(function (type, url) { if (isActive && (type !== "QML" || url !== "Edit.qml")) { - that.toggle(); + that.setActive(false) } }); tablet.fromQml.connect(fromQml); - activeButton.clicked.connect(function() { + createButton.clicked.connect(function() { + if ( ! (Entities.canRez() || Entities.canRezTmp()) ) { + Window.notifyEditError(INSUFFICIENT_PERMISSIONS_ERROR_MSG); + return; + } + that.toggle(); }); @@ -440,6 +455,8 @@ var toolBar = (function () { 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"; + SHAPE_TYPES[SHAPE_TYPE_BOX] = "Box"; + SHAPE_TYPES[SHAPE_TYPE_SPHERE] = "Sphere"; var SHAPE_TYPE_DEFAULT = SHAPE_TYPE_STATIC_MESH; // tablet version of new-model dialog @@ -604,6 +621,7 @@ var toolBar = (function () { }; that.setActive = function (active) { + ContextOverlay.enabled = !active; Settings.setValue(EDIT_SETTING, active); if (active) { Controller.captureEntityClickEvents(); @@ -642,8 +660,9 @@ var toolBar = (function () { grid.setEnabled(true); propertiesTool.setVisible(true); selectionDisplay.triggerMapping.enable(); - print("starting tablet in landscape mode") + print("starting tablet in landscape mode"); tablet.landscape = true; + entityIconOverlayManager.setIconsSelectable(null,false); // Not sure what the following was meant to accomplish, but it currently causes // everybody else to think that Interface has lost focus overall. fogbugzid:558 // Window.setFocus(); @@ -760,8 +779,38 @@ function handleOverlaySelectionToolUpdates(channel, message, sender) { } } +// Handles any edit mode updates required when domains have switched +function handleDomainChange() { + if ( (createButton === null) || (createButton === undefined) ){ + //--EARLY EXIT--( nothing to safely update ) + return; + } + + var hasRezPermissions = (Entities.canRez() || Entities.canRezTmp()); + createButton.editProperties({ + icon: (hasRezPermissions ? CREATE_ENABLED_ICON : CREATE_DISABLED_ICON), + }); +} + +function handleMessagesReceived(channel, message, sender) { + switch( channel ){ + case 'entityToolUpdates': { + handleOverlaySelectionToolUpdates( channel, message, sender ); + break; + } + case 'Toolbar-DomainChanged': { + handleDomainChange(); + break; + } + default: { + return; + } + } +} + +Messages.subscribe('Toolbar-DomainChanged'); Messages.subscribe("entityToolUpdates"); -Messages.messageReceived.connect(handleOverlaySelectionToolUpdates); +Messages.messageReceived.connect(handleMessagesReceived); var mouseHasMovedSincePress = false; var mousePressStartTime = 0; @@ -941,6 +990,7 @@ function mouseClickEvent(event) { } else { selectionManager.addEntity(foundEntity, true); } + if (wantDebug) { print("Model selected: " + foundEntity); } @@ -1180,6 +1230,8 @@ Script.scriptEnding.connect(function () { Messages.messageReceived.disconnect(handleOverlaySelectionToolUpdates); Messages.unsubscribe("entityToolUpdates"); + Messages.unsubscribe("Toolbar-DomainChanged"); + createButton = null; }); var lastOrientation = null; @@ -1305,7 +1357,7 @@ function unparentSelectedEntities() { if (parentId !== null && parentId.length > 0 && parentId !== "{00000000-0000-0000-0000-000000000000}") { parentCheck = true; } - Entities.editEntity(id, {parentID: null}) + Entities.editEntity(id, {parentID: null}); return true; }); if (parentCheck) { @@ -1340,7 +1392,7 @@ function parentSelectedEntities() { if (parentId !== lastEntityId) { parentCheck = true; } - Entities.editEntity(id, {parentID: lastEntityId}) + Entities.editEntity(id, {parentID: lastEntityId}); } }); @@ -1513,9 +1565,9 @@ function importSVO(importURL) { var entityPositions = []; var entityParentIDs = []; - var properties = Entities.getEntityProperties(pastedEntityIDs[0], ["type"]); + var propType = Entities.getEntityProperties(pastedEntityIDs[0], ["type"]).type; var NO_ADJUST_ENTITY_TYPES = ["Zone", "Light", "ParticleEffect"]; - if (NO_ADJUST_ENTITY_TYPES.indexOf(properties.type) === -1) { + if (NO_ADJUST_ENTITY_TYPES.indexOf(propType) === -1) { var targetDirection; if (Camera.mode === "entity" || Camera.mode === "independent") { targetDirection = Camera.orientation; @@ -1528,36 +1580,36 @@ function importSVO(importURL) { var deltaParallel = HALF_TREE_SCALE; // Distance to move entities parallel to targetDirection. var deltaPerpendicular = Vec3.ZERO; // Distance to move entities perpendicular to targetDirection. for (var i = 0, length = pastedEntityIDs.length; i < length; i++) { - var properties = Entities.getEntityProperties(pastedEntityIDs[i], ["position", "dimensions", + var curLoopEntityProps = Entities.getEntityProperties(pastedEntityIDs[i], ["position", "dimensions", "registrationPoint", "rotation", "parentID"]); var adjustedPosition = adjustPositionPerBoundingBox(targetPosition, targetDirection, - properties.registrationPoint, properties.dimensions, properties.rotation); - var delta = Vec3.subtract(adjustedPosition, properties.position); + curLoopEntityProps.registrationPoint, curLoopEntityProps.dimensions, curLoopEntityProps.rotation); + var delta = Vec3.subtract(adjustedPosition, curLoopEntityProps.position); var distance = Vec3.dot(delta, targetDirection); deltaParallel = Math.min(distance, deltaParallel); deltaPerpendicular = Vec3.sum(Vec3.subtract(delta, Vec3.multiply(distance, targetDirection)), deltaPerpendicular); - entityPositions[i] = properties.position; - entityParentIDs[i] = properties.parentID; + entityPositions[i] = curLoopEntityProps.position; + entityParentIDs[i] = curLoopEntityProps.parentID; } deltaPerpendicular = Vec3.multiply(1 / pastedEntityIDs.length, deltaPerpendicular); deltaPosition = Vec3.sum(Vec3.multiply(deltaParallel, targetDirection), deltaPerpendicular); } if (grid.getSnapToGrid()) { - var properties = Entities.getEntityProperties(pastedEntityIDs[0], ["position", "dimensions", + var firstEntityProps = Entities.getEntityProperties(pastedEntityIDs[0], ["position", "dimensions", "registrationPoint"]); - var position = Vec3.sum(deltaPosition, properties.position); - position = grid.snapToSurface(grid.snapToGrid(position, false, properties.dimensions, - properties.registrationPoint), properties.dimensions, properties.registrationPoint); - deltaPosition = Vec3.subtract(position, properties.position); + var positionPreSnap = Vec3.sum(deltaPosition, firstEntityProps.position); + position = grid.snapToSurface(grid.snapToGrid(positionPreSnap, false, firstEntityProps.dimensions, + firstEntityProps.registrationPoint), firstEntityProps.dimensions, firstEntityProps.registrationPoint); + deltaPosition = Vec3.subtract(position, firstEntityProps.position); } if (!Vec3.equal(deltaPosition, Vec3.ZERO)) { - for (var i = 0, length = pastedEntityIDs.length; i < length; i++) { - if (Uuid.isNull(entityParentIDs[i])) { - Entities.editEntity(pastedEntityIDs[i], { - position: Vec3.sum(deltaPosition, entityPositions[i]) + for (var editEntityIndex = 0, numEntities = pastedEntityIDs.length; editEntityIndex < numEntities; editEntityIndex++) { + if (Uuid.isNull(entityParentIDs[editEntityIndex])) { + Entities.editEntity(pastedEntityIDs[editEntityIndex], { + position: Vec3.sum(deltaPosition, entityPositions[editEntityIndex]) }); } } @@ -2142,6 +2194,7 @@ var PopupMenu = function () { }; function cleanup() { + ContextOverlay.enabled = true; for (var i = 0; i < overlays.length; i++) { Overlays.deleteOverlay(overlays[i]); } @@ -2175,10 +2228,9 @@ var particleExplorerTool = new ParticleExplorerTool(); var selectedParticleEntity = 0; var selectedParticleEntityID = null; - function selectParticleEntity(entityID) { var properties = Entities.getEntityProperties(entityID); - + selectedParticleEntityID = entityID; if (properties.emitOrientation) { properties.emitOrientation = Quat.safeEulerAngles(properties.emitOrientation); } @@ -2203,7 +2255,7 @@ entityListTool.webView.webEventReceived.connect(function (data) { try { data = JSON.parse(data); } catch(e) { - print("edit.js: Error parsing JSON: " + e.name + " data " + data) + print("edit.js: Error parsing JSON: " + e.name + " data " + data); return; } @@ -2220,7 +2272,6 @@ entityListTool.webView.webEventReceived.connect(function (data) { return; } // Destroy the old particles web view first - selectParticleEntity(ids[0]); } else { selectedParticleEntity = 0; particleExplorerTool.destroyWebView(); diff --git a/scripts/system/hmd.js b/scripts/system/hmd.js index c545e6bcee..c9c3dbe493 100644 --- a/scripts/system/hmd.js +++ b/scripts/system/hmd.js @@ -40,21 +40,23 @@ function updateControllerDisplay() { var button; var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); -// Independent and Entity mode make people sick. Third Person and Mirror have traps that we need to work through. -// Disable them in hmd. -var desktopOnlyViews = ['Mirror', 'Independent Mode', 'Entity Mode']; +// Independent and Entity mode make people sick; disable them in hmd. +var desktopOnlyViews = ['Independent Mode', 'Entity Mode']; + +var switchToVR = "ENTER VR"; +var switchToDesktop = "EXIT VR"; function onHmdChanged(isHmd) { HMD.closeTablet(); if (isHmd) { button.editProperties({ icon: "icons/tablet-icons/switch-desk-i.svg", - text: "DESKTOP" + text: switchToDesktop }); } else { button.editProperties({ icon: "icons/tablet-icons/switch-vr-i.svg", - text: "VR" + text: switchToVR }); } desktopOnlyViews.forEach(function (view) { @@ -71,7 +73,7 @@ function onClicked() { if (headset) { button = tablet.addButton({ icon: HMD.active ? "icons/tablet-icons/switch-desk-i.svg" : "icons/tablet-icons/switch-vr-i.svg", - text: HMD.active ? "DESKTOP" : "VR", + text: HMD.active ? switchToDesktop : switchToVR, sortOrder: 2 }); onHmdChanged(HMD.active); diff --git a/scripts/system/html/js/entityProperties.js b/scripts/system/html/js/entityProperties.js index 777ef54085..ce2f766946 100644 --- a/scripts/system/html/js/entityProperties.js +++ b/scripts/system/html/js/entityProperties.js @@ -475,6 +475,15 @@ function unbindAllInputs() { } } +function clearSelection() { + if(document.selection && document.selection.empty) { + document.selection.empty(); + } else if(window.getSelection) { + var sel = window.getSelection(); + sel.removeAllRanges(); + } +} + function loaded() { openEventBridge(function() { @@ -1051,6 +1060,7 @@ function loaded() { activeElement.select(); } } + clearSelection(); } }); } @@ -1074,7 +1084,7 @@ function loaded() { elDimensionsZ.addEventListener('change', dimensionsChangeFunction); elParentID.addEventListener('change', createEmitTextPropertyUpdateFunction('parentID')); - elParentJointIndex.addEventListener('change', createEmitNumberPropertyUpdateFunction('parentJointIndex')); + elParentJointIndex.addEventListener('change', createEmitNumberPropertyUpdateFunction('parentJointIndex', 0)); var registrationChangeFunction = createEmitVec3PropertyUpdateFunction( 'registrationPoint', elRegistrationX, elRegistrationY, elRegistrationZ); diff --git a/scripts/system/libraries/WebTablet.js b/scripts/system/libraries/WebTablet.js index 142ed6e7b6..53f88ea62d 100644 --- a/scripts/system/libraries/WebTablet.js +++ b/scripts/system/libraries/WebTablet.js @@ -76,7 +76,7 @@ function calcSpawnInfo(hand, tabletHeight) { var TABLET_RAKE_ANGLE = 30; rotation = Quat.multiply(Quat.angleAxis(TABLET_RAKE_ANGLE, Vec3.multiplyQbyV(lookAt, Vec3.UNIT_X)), lookAt); - var RELATIVE_SPAWN_OFFSET = { x: 0, y: 0.4, z: 0.05 }; + var RELATIVE_SPAWN_OFFSET = { x: 0, y: 0.6, z: 0.1 }; position = Vec3.sum(position, Vec3.multiplyQbyV(rotation, Vec3.multiply(tabletHeight, RELATIVE_SPAWN_OFFSET))); return { diff --git a/scripts/system/libraries/entityIconOverlayManager.js b/scripts/system/libraries/entityIconOverlayManager.js index f557a05f60..a374783b1f 100644 --- a/scripts/system/libraries/entityIconOverlayManager.js +++ b/scripts/system/libraries/entityIconOverlayManager.js @@ -1,6 +1,7 @@ /* globals EntityIconOverlayManager:true */ EntityIconOverlayManager = function(entityTypes, getOverlayPropertiesFunc) { + var visible = false; // List of all created overlays @@ -69,6 +70,29 @@ EntityIconOverlayManager = function(entityTypes, getOverlayPropertiesFunc) { } }; + + this.setIconsSelectable = function(arrayOfSelectedEntityIDs, isIconsSelectable) { + if (arrayOfSelectedEntityIDs === null) { + for (var id in entityOverlays) { + Overlays.editOverlay(entityOverlays[id], { + ignoreRayIntersection: isIconsSelectable + }); + } + } else { + for (var id in entityOverlays) { + if (arrayOfSelectedEntityIDs.indexOf(id) !== -1) { // in the entityOverlays array and selectable + Overlays.editOverlay(entityOverlays[id], { + ignoreRayIntersection: isIconsSelectable + }); + } else { + Overlays.editOverlay(entityOverlays[id], { + ignoreRayIntersection: !isIconsSelectable + }); + } + } + } + }; + // Allocate or get an unused overlay function getOverlay() { var overlay; @@ -114,6 +138,9 @@ EntityIconOverlayManager = function(entityTypes, getOverlayPropertiesFunc) { overlayProperties[key] = customProperties[key]; } } + if(properties.type === 'ParticleEffect' || properties.type === 'Light'){ + overlayProperties.ignoreRayIntersection = true; + } Overlays.editOverlay(overlay, overlayProperties); } } diff --git a/scripts/system/libraries/entitySelectionTool.js b/scripts/system/libraries/entitySelectionTool.js index 725803f824..77b62913bf 100644 --- a/scripts/system/libraries/entitySelectionTool.js +++ b/scripts/system/libraries/entitySelectionTool.js @@ -1560,7 +1560,6 @@ SelectionDisplay = (function() { visible: rotationOverlaysVisible }); - // TODO: we have not implemented the rotating handle/controls yet... so for now, these handles are hidden Overlays.editOverlay(yawHandle, { visible: rotateHandlesVisible, position: yawCorner, @@ -3615,24 +3614,21 @@ SelectionDisplay = (function() { onMove: function(event) { var pickRay = generalComputePickRay(event.x, event.y); Overlays.editOverlay(selectionBox, { - ignoreRayIntersection: true, visible: false }); Overlays.editOverlay(baseOfEntityProjectionOverlay, { - ignoreRayIntersection: true, visible: false }); - Overlays.editOverlay(rotateOverlayTarget, { - ignoreRayIntersection: false - }); - var result = Overlays.findRayIntersection(pickRay); + var result = Overlays.findRayIntersection(pickRay, true, [rotateOverlayTarget]); if (result.intersects) { var center = yawCenter; var zero = yawZero; + // TODO: these vectors are backwards to their names, doesn't matter for this use case (inverted vectors still give same angle) var centerToZero = Vec3.subtract(center, zero); var centerToIntersect = Vec3.subtract(center, result.intersection); + // TODO: orientedAngle wants normalized centerToZero and centerToIntersect var angleFromZero = Vec3.orientedAngle(centerToZero, centerToIntersect, rotationNormal); var distanceFromCenter = Vec3.distance(center, result.intersection); var snapToInner = distanceFromCenter < innerRadius; @@ -3785,17 +3781,12 @@ SelectionDisplay = (function() { onMove: function(event) { var pickRay = generalComputePickRay(event.x, event.y); Overlays.editOverlay(selectionBox, { - ignoreRayIntersection: true, visible: false }); Overlays.editOverlay(baseOfEntityProjectionOverlay, { - ignoreRayIntersection: true, visible: false }); - Overlays.editOverlay(rotateOverlayTarget, { - ignoreRayIntersection: false - }); - var result = Overlays.findRayIntersection(pickRay); + var result = Overlays.findRayIntersection(pickRay, true, [rotateOverlayTarget]); if (result.intersects) { var properties = Entities.getEntityProperties(selectionManager.selections[0]); @@ -3947,17 +3938,12 @@ SelectionDisplay = (function() { onMove: function(event) { var pickRay = generalComputePickRay(event.x, event.y); Overlays.editOverlay(selectionBox, { - ignoreRayIntersection: true, visible: false }); Overlays.editOverlay(baseOfEntityProjectionOverlay, { - ignoreRayIntersection: true, visible: false }); - Overlays.editOverlay(rotateOverlayTarget, { - ignoreRayIntersection: false - }); - var result = Overlays.findRayIntersection(pickRay); + var result = Overlays.findRayIntersection(pickRay, true, [rotateOverlayTarget]); if (result.intersects) { var properties = Entities.getEntityProperties(selectionManager.selections[0]); @@ -4074,21 +4060,10 @@ SelectionDisplay = (function() { return false; } - // before we do a ray test for grabbers, disable the ray intersection for our selection box - Overlays.editOverlay(selectionBox, { - ignoreRayIntersection: true - }); - Overlays.editOverlay(yawHandle, { - ignoreRayIntersection: true - }); - Overlays.editOverlay(pitchHandle, { - ignoreRayIntersection: true - }); - Overlays.editOverlay(rollHandle, { - ignoreRayIntersection: true - }); + entityIconOverlayManager.setIconsSelectable(selectionManager.selections,true); - result = Overlays.findRayIntersection(pickRay); + // ignore ray intersection for our selection box and yaw/pitch/roll + result = Overlays.findRayIntersection(pickRay, true, null, [ yawHandle, pitchHandle, rollHandle, selectionBox ] ); if (result.intersects) { if (wantDebug) { print("something intersects... "); @@ -4191,17 +4166,8 @@ SelectionDisplay = (function() { } - // After testing our stretch handles, then check out rotate handles - Overlays.editOverlay(yawHandle, { - ignoreRayIntersection: false - }); - Overlays.editOverlay(pitchHandle, { - ignoreRayIntersection: false - }); - Overlays.editOverlay(rollHandle, { - ignoreRayIntersection: false - }); - var result = Overlays.findRayIntersection(pickRay); + // Only intersect versus yaw/pitch/roll. + var result = Overlays.findRayIntersection(pickRay, true, [ yawHandle, pitchHandle, rollHandle ] ); var overlayOrientation; var overlayCenter; @@ -4306,6 +4272,7 @@ SelectionDisplay = (function() { }); + // TODO: these three duplicate prior three, remove them. Overlays.editOverlay(yawHandle, { visible: false }); @@ -4402,10 +4369,8 @@ SelectionDisplay = (function() { } if (!somethingClicked) { - Overlays.editOverlay(selectionBox, { - ignoreRayIntersection: false - }); - var result = Overlays.findRayIntersection(pickRay); + // Only intersect versus selectionBox. + var result = Overlays.findRayIntersection(pickRay, true, [selectionBox]); if (result.intersects) { switch (result.overlayID) { case selectionBox: diff --git a/scripts/system/marketplaces/marketplaces.js b/scripts/system/marketplaces/marketplaces.js index 3be8143830..7b25589e92 100644 --- a/scripts/system/marketplaces/marketplaces.js +++ b/scripts/system/marketplaces/marketplaces.js @@ -11,135 +11,140 @@ /* global Tablet, Script, HMD, UserActivityLogger, Entities */ /* eslint indent: ["error", 4, { "outerIIFEBody": 0 }] */ -(function() { // BEGIN LOCAL_SCOPE +(function () { // BEGIN LOCAL_SCOPE -Script.include("../libraries/WebTablet.js"); + Script.include("../libraries/WebTablet.js"); -var MARKETPLACE_URL = "https://metaverse.highfidelity.com/marketplace"; -var MARKETPLACE_URL_INITIAL = MARKETPLACE_URL + "?"; // Append "?" to signal injected script that it's the initial page. -var MARKETPLACES_URL = Script.resolvePath("../html/marketplaces.html"); -var MARKETPLACES_INJECT_SCRIPT_URL = Script.resolvePath("../html/js/marketplacesInject.js"); + var MARKETPLACE_URL = "https://metaverse.highfidelity.com/marketplace"; + var MARKETPLACE_URL_INITIAL = MARKETPLACE_URL + "?"; // Append "?" to signal injected script that it's the initial page. + var MARKETPLACES_URL = Script.resolvePath("../html/marketplaces.html"); + var MARKETPLACES_INJECT_SCRIPT_URL = Script.resolvePath("../html/js/marketplacesInject.js"); -var HOME_BUTTON_TEXTURE = "http://hifi-content.s3.amazonaws.com/alan/dev/tablet-with-home-button.fbx/tablet-with-home-button.fbm/button-root.png"; -// var HOME_BUTTON_TEXTURE = Script.resourcesPath() + "meshes/tablet-with-home-button.fbx/tablet-with-home-button.fbm/button-root.png"; + var HOME_BUTTON_TEXTURE = "http://hifi-content.s3.amazonaws.com/alan/dev/tablet-with-home-button.fbx/tablet-with-home-button.fbm/button-root.png"; + // var HOME_BUTTON_TEXTURE = Script.resourcesPath() + "meshes/tablet-with-home-button.fbx/tablet-with-home-button.fbm/button-root.png"; -// Event bridge messages. -var CLARA_IO_DOWNLOAD = "CLARA.IO DOWNLOAD"; -var CLARA_IO_STATUS = "CLARA.IO STATUS"; -var CLARA_IO_CANCEL_DOWNLOAD = "CLARA.IO CANCEL DOWNLOAD"; -var CLARA_IO_CANCELLED_DOWNLOAD = "CLARA.IO CANCELLED DOWNLOAD"; -var GOTO_DIRECTORY = "GOTO_DIRECTORY"; -var QUERY_CAN_WRITE_ASSETS = "QUERY_CAN_WRITE_ASSETS"; -var CAN_WRITE_ASSETS = "CAN_WRITE_ASSETS"; -var WARN_USER_NO_PERMISSIONS = "WARN_USER_NO_PERMISSIONS"; + // Event bridge messages. + var CLARA_IO_DOWNLOAD = "CLARA.IO DOWNLOAD"; + var CLARA_IO_STATUS = "CLARA.IO STATUS"; + var CLARA_IO_CANCEL_DOWNLOAD = "CLARA.IO CANCEL DOWNLOAD"; + var CLARA_IO_CANCELLED_DOWNLOAD = "CLARA.IO CANCELLED DOWNLOAD"; + var GOTO_DIRECTORY = "GOTO_DIRECTORY"; + var QUERY_CAN_WRITE_ASSETS = "QUERY_CAN_WRITE_ASSETS"; + var CAN_WRITE_ASSETS = "CAN_WRITE_ASSETS"; + var WARN_USER_NO_PERMISSIONS = "WARN_USER_NO_PERMISSIONS"; -var CLARA_DOWNLOAD_TITLE = "Preparing Download"; -var messageBox = null; -var isDownloadBeingCancelled = false; + var CLARA_DOWNLOAD_TITLE = "Preparing Download"; + var messageBox = null; + var isDownloadBeingCancelled = false; -var CANCEL_BUTTON = 4194304; // QMessageBox::Cancel -var NO_BUTTON = 0; // QMessageBox::NoButton + var CANCEL_BUTTON = 4194304; // QMessageBox::Cancel + var NO_BUTTON = 0; // QMessageBox::NoButton -var NO_PERMISSIONS_ERROR_MESSAGE = "Cannot download model because you can't write to \nthe domain's Asset Server."; + var NO_PERMISSIONS_ERROR_MESSAGE = "Cannot download model because you can't write to \nthe domain's Asset Server."; -function onMessageBoxClosed(id, button) { - if (id === messageBox && button === CANCEL_BUTTON) { - isDownloadBeingCancelled = true; - messageBox = null; - tablet.emitScriptEvent(CLARA_IO_CANCEL_DOWNLOAD); + function onMessageBoxClosed(id, button) { + if (id === messageBox && button === CANCEL_BUTTON) { + isDownloadBeingCancelled = true; + messageBox = null; + tablet.emitScriptEvent(CLARA_IO_CANCEL_DOWNLOAD); + } } -} -Window.messageBoxClosed.connect(onMessageBoxClosed); + Window.messageBoxClosed.connect(onMessageBoxClosed); -var onMarketplaceScreen = false; + var onMarketplaceScreen = false; -function showMarketplace() { - UserActivityLogger.openedMarketplace(); - tablet.gotoWebScreen(MARKETPLACE_URL_INITIAL, MARKETPLACES_INJECT_SCRIPT_URL); - tablet.webEventReceived.connect(function (message) { + function showMarketplace() { + UserActivityLogger.openedMarketplace(); + tablet.gotoWebScreen(MARKETPLACE_URL_INITIAL, MARKETPLACES_INJECT_SCRIPT_URL); + tablet.webEventReceived.connect(function (message) { - if (message === GOTO_DIRECTORY) { - tablet.gotoWebScreen(MARKETPLACES_URL, MARKETPLACES_INJECT_SCRIPT_URL); - } + if (message === GOTO_DIRECTORY) { + tablet.gotoWebScreen(MARKETPLACES_URL, MARKETPLACES_INJECT_SCRIPT_URL); + } - if (message === QUERY_CAN_WRITE_ASSETS) { - tablet.emitScriptEvent(CAN_WRITE_ASSETS + " " + Entities.canWriteAssets()); - } + if (message === QUERY_CAN_WRITE_ASSETS) { + tablet.emitScriptEvent(CAN_WRITE_ASSETS + " " + Entities.canWriteAssets()); + } - if (message === WARN_USER_NO_PERMISSIONS) { - Window.alert(NO_PERMISSIONS_ERROR_MESSAGE); - } + if (message === WARN_USER_NO_PERMISSIONS) { + Window.alert(NO_PERMISSIONS_ERROR_MESSAGE); + } - if (message.slice(0, CLARA_IO_STATUS.length) === CLARA_IO_STATUS) { - if (isDownloadBeingCancelled) { + if (message.slice(0, CLARA_IO_STATUS.length) === CLARA_IO_STATUS) { + if (isDownloadBeingCancelled) { + return; + } + + var text = message.slice(CLARA_IO_STATUS.length); + if (messageBox === null) { + messageBox = Window.openMessageBox(CLARA_DOWNLOAD_TITLE, text, CANCEL_BUTTON, NO_BUTTON); + } else { + Window.updateMessageBox(messageBox, CLARA_DOWNLOAD_TITLE, text, CANCEL_BUTTON, NO_BUTTON); + } return; } - var text = message.slice(CLARA_IO_STATUS.length); - if (messageBox === null) { - messageBox = Window.openMessageBox(CLARA_DOWNLOAD_TITLE, text, CANCEL_BUTTON, NO_BUTTON); - } else { - Window.updateMessageBox(messageBox, CLARA_DOWNLOAD_TITLE, text, CANCEL_BUTTON, NO_BUTTON); + if (message.slice(0, CLARA_IO_DOWNLOAD.length) === CLARA_IO_DOWNLOAD) { + if (messageBox !== null) { + Window.closeMessageBox(messageBox); + messageBox = null; + } + return; } - return; - } - if (message.slice(0, CLARA_IO_DOWNLOAD.length) === CLARA_IO_DOWNLOAD) { - if (messageBox !== null) { - Window.closeMessageBox(messageBox); - messageBox = null; + if (message === CLARA_IO_CANCELLED_DOWNLOAD) { + isDownloadBeingCancelled = false; } - return; - } + }); + } - if (message === CLARA_IO_CANCELLED_DOWNLOAD) { - isDownloadBeingCancelled = false; - } + var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); + var marketplaceButton = tablet.addButton({ + icon: "icons/tablet-icons/market-i.svg", + activeIcon: "icons/tablet-icons/market-a.svg", + text: "MARKET", + sortOrder: 9 }); -} -var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); -var marketplaceButton = tablet.addButton({ - icon: "icons/tablet-icons/market-i.svg", - activeIcon: "icons/tablet-icons/market-a.svg", - text: "MARKET", - sortOrder: 9 -}); - -function onCanWriteAssetsChanged() { - var message = CAN_WRITE_ASSETS + " " + Entities.canWriteAssets(); - tablet.emitScriptEvent(message); -} - -function onClick() { - if (onMarketplaceScreen) { - // for toolbar-mode: go back to home screen, this will close the window. - tablet.gotoHomeScreen(); - } else { - var entity = HMD.tabletID; - Entities.editEntity(entity, {textures: JSON.stringify({"tex.close": HOME_BUTTON_TEXTURE})}); - showMarketplace(); + function onCanWriteAssetsChanged() { + var message = CAN_WRITE_ASSETS + " " + Entities.canWriteAssets(); + tablet.emitScriptEvent(message); } -} -function onScreenChanged(type, url) { - onMarketplaceScreen = type === "Web" && url === MARKETPLACE_URL_INITIAL - // for toolbar mode: change button to active when window is first openend, false otherwise. - marketplaceButton.editProperties({isActive: onMarketplaceScreen}); -} - -marketplaceButton.clicked.connect(onClick); -tablet.screenChanged.connect(onScreenChanged); -Entities.canWriteAssetsChanged.connect(onCanWriteAssetsChanged); - -Script.scriptEnding.connect(function () { - if (onMarketplaceScreen) { - tablet.gotoHomeScreen(); + function onClick() { + if (onMarketplaceScreen) { + // for toolbar-mode: go back to home screen, this will close the window. + tablet.gotoHomeScreen(); + } else { + var entity = HMD.tabletID; + Entities.editEntity(entity, { textures: JSON.stringify({ "tex.close": HOME_BUTTON_TEXTURE }) }); + showMarketplace(); + } } - tablet.removeButton(marketplaceButton); - tablet.screenChanged.disconnect(onScreenChanged); - Entities.canWriteAssetsChanged.disconnect(onCanWriteAssetsChanged); -}); + + function onScreenChanged(type, url) { + onMarketplaceScreen = type === "Web" && url === MARKETPLACE_URL_INITIAL + // for toolbar mode: change button to active when window is first openend, false otherwise. + marketplaceButton.editProperties({ isActive: onMarketplaceScreen }); + if (type === "Web" && url.indexOf(MARKETPLACE_URL) !== -1) { + ContextOverlay.isInMarketplaceInspectionMode = true; + } else { + ContextOverlay.isInMarketplaceInspectionMode = false; + } + } + + marketplaceButton.clicked.connect(onClick); + 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); + }); }()); // END LOCAL_SCOPE diff --git a/scripts/system/pal.js b/scripts/system/pal.js index 8ea22192fc..2a8a89ae7d 100644 --- a/scripts/system/pal.js +++ b/scripts/system/pal.js @@ -710,6 +710,7 @@ function off() { Controller.mouseMoveEvent.disconnect(handleMouseMoveEvent); tablet.tabletShownChanged.disconnect(tabletVisibilityChanged); isWired = false; + ContextOverlay.enabled = true } if (audioTimer) { Script.clearInterval(audioTimer); @@ -722,6 +723,7 @@ function off() { function tabletVisibilityChanged() { if (!tablet.tabletShown) { + ContextOverlay.enabled = true; tablet.gotoHomeScreen(); } } @@ -732,7 +734,9 @@ function onTabletButtonClicked() { if (onPalScreen) { // for toolbar-mode: go back to home screen, this will close the window. tablet.gotoHomeScreen(); + ContextOverlay.enabled = true; } else { + ContextOverlay.enabled = false; tablet.loadQMLSource(PAL_QML_SOURCE); tablet.tabletShownChanged.connect(tabletVisibilityChanged); Users.requestsDomainListData = true; @@ -863,6 +867,7 @@ function avatarDisconnected(nodeID) { function clearLocalQMLDataAndClosePAL() { sendToQml({ method: 'clearLocalQMLData' }); if (onPalScreen) { + ContextOverlay.enabled = true; tablet.gotoHomeScreen(); } } diff --git a/scripts/system/tablet-ui/tabletUI.js b/scripts/system/tablet-ui/tabletUI.js index 9188f39a2e..257a56bf09 100644 --- a/scripts/system/tablet-ui/tabletUI.js +++ b/scripts/system/tablet-ui/tabletUI.js @@ -104,7 +104,6 @@ function showTabletUI() { checkTablet() - gTablet.tabletShown = true; if (!tabletRezzed || !tabletIsValid()) { closeTabletUI(); @@ -123,6 +122,7 @@ Overlays.editOverlay(HMD.tabletScreenID, { visible: true }); Overlays.editOverlay(HMD.tabletScreenID, { maxFPS: 90 }); } + gTablet.tabletShown = true; } function hideTabletUI() { diff --git a/scripts/tutorials/createMidiSphere.js b/scripts/tutorials/createMidiSphere.js new file mode 100644 index 0000000000..56eae98585 --- /dev/null +++ b/scripts/tutorials/createMidiSphere.js @@ -0,0 +1,50 @@ +// +// Created by James B. Pollack @imgntn on April 18, 2016. +// Adapted by Burt +// 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 SCRIPT_URL = "file:///e:/hifi/scripts/tutorials/entity_scripts/midiSphere.js"; +var SCRIPT_URL = "http://hifi-files.s3-website-us-west-2.amazonaws.com/midiSphere.js"; +var center = Vec3.sum(MyAvatar.position, Vec3.multiply(0.5, Quat.getForward(Camera.getOrientation()))); + +var BALL_GRAVITY = { + x: 0, + y: 0, + z: 0 +}; + +var BALL_DIMENSIONS = { + x: 0.4, + y: 0.4, + z: 0.4 +}; + + +var BALL_COLOR = { + red: 255, + green: 0, + blue: 0 +}; + +var midiSphereProperties = { + name: 'MIDI Sphere', + shapeType: 'sphere', + type: 'Sphere', + script: SCRIPT_URL, + color: BALL_COLOR, + dimensions: BALL_DIMENSIONS, + gravity: BALL_GRAVITY, + dynamic: false, + position: center, + collisionless: false, + ignoreForCollisions: true +}; + +var midiSphere = Entities.addEntity(midiSphereProperties); + +Script.stop(); diff --git a/scripts/tutorials/entity_scripts/midiSphere.js b/scripts/tutorials/entity_scripts/midiSphere.js new file mode 100644 index 0000000000..980f8f834e --- /dev/null +++ b/scripts/tutorials/entity_scripts/midiSphere.js @@ -0,0 +1,52 @@ +// midiSphere.js +// +// Script Type: Entity +// Created by James B. Pollack @imgntn on 9/21/2015 +// Adapted by Burt +// Copyright 2015 High Fidelity, Inc. +// +// This script listens to MIDI and makes the ball change color. +// 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; + + function MidiSphere() { + _this = this; + this.clicked = false; + return; + } + + MidiSphere.prototype = { + preload: function(entityID) { + this.entityID = entityID; + Midi.midiNote.connect(function(eventData) { + print("MidiSphere.noteReceived: "+JSON.stringify(eventData)); + Entities.editEntity(entityID, { color: { red: 2*eventData.note, green: 2*eventData.note, blue: 2*eventData.note} }); + }); + print("MidiSphere.preload"); + }, + unload: function(entityID) { + print("MidiSphere.unload"); + }, + + clickDownOnEntity: function(entityID, mouseEvent) { + print("MidiSphere.clickDownOnEntity"); + if (this.clicked) { + Entities.editEntity(entityID, { color: { red: 0, green: 255, blue: 255} }); + this.clicked = false; + Midi.playMidiNote(144, 64, 0); + } else { + Entities.editEntity(entityID, { color: { red: 255, green: 255, blue: 0} }); + this.clicked = true; + Midi.playMidiNote(144, 64, 100); + } + } + + }; + + // entity scripts should return a newly constructed object of our type + return new MidiSphere(); +}); \ No newline at end of file diff --git a/scripts/tutorials/entity_scripts/sit.js b/scripts/tutorials/entity_scripts/sit.js index 3d3bc10fb1..70456ea493 100644 --- a/scripts/tutorials/entity_scripts/sit.js +++ b/scripts/tutorials/entity_scripts/sit.js @@ -12,9 +12,9 @@ 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; - }; + position = position || 0; + return this.substr(position, searchString.length) === searchString; + }; } var SETTING_KEY = "com.highfidelity.avatar.isSitting"; @@ -122,10 +122,20 @@ this.rolesToOverride = function() { return MyAvatar.getAnimationRoles().filter(function(role) { - return role === "fly" || role.startsWith("inAir"); + return !(role.startsWith("right") || role.startsWith("left")); }); } + // Handler for user changing the avatar model while sitting. There's currently an issue with changing avatar models while override role animations are applied, + // so to avoid that problem, re-apply the role overrides once the model has finished changing. + this.modelURLChangeFinished = function () { + print("Sitter's model has FINISHED changing. Reapply anim role overrides."); + var roles = this.rolesToOverride(); + for (i in roles) { + MyAvatar.overrideRoleAnimation(roles[i], ANIMATION_URL, ANIMATION_FPS, true, ANIMATION_FIRST_FRAME, ANIMATION_LAST_FRAME); + } + } + this.sitDown = function() { if (this.checkSeatForAvatar()) { print("Someone is already sitting in that chair."); @@ -164,12 +174,14 @@ return { headType: 0 }; }, ["headType"]); Script.update.connect(this, this.update); + MyAvatar.onLoadComplete.connect(this, this.modelURLChangeFinished); } this.standUp = function() { print("Standing up (" + this.entityID + ")"); MyAvatar.removeAnimationStateHandler(this.animStateHandlerID); Script.update.disconnect(this, this.update); + MyAvatar.onLoadComplete.disconnect(this, this.modelURLChangeFinished); if (MyAvatar.sessionUUID === this.getSeatUser()) { this.setSeatUser(null); @@ -331,7 +343,7 @@ } this.cleanupOverlay(); } - + this.clickDownOnEntity = function (id, event) { if (isInEditMode()) { return; diff --git a/tests/render-perf/CMakeLists.txt b/tests/render-perf/CMakeLists.txt index b6989a57b7..77c8ab2177 100644 --- a/tests/render-perf/CMakeLists.txt +++ b/tests/render-perf/CMakeLists.txt @@ -12,8 +12,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 networking model fbx ktx image octree gl gpu gpu-gl render model-networking networking render-utils entities entities-renderer animation audio avatars script-engine physics procedural) - +link_hifi_libraries(shared networking model fbx ktx image octree gl gpu gpu-gl render model-networking networking render-utils entities entities-renderer animation audio avatars script-engine physics procedural midi) package_libraries_for_deployment() diff --git a/tests/render-perf/src/main.cpp b/tests/render-perf/src/main.cpp index ce47a896aa..dbb315a9ae 100644 --- a/tests/render-perf/src/main.cpp +++ b/tests/render-perf/src/main.cpp @@ -681,7 +681,7 @@ private: _renderCount = _renderThread._presentCount.load(); update(); - RenderArgs renderArgs(_renderThread._gpuContext, _octree, DEFAULT_OCTREE_SIZE_SCALE, + RenderArgs renderArgs(_renderThread._gpuContext, DEFAULT_OCTREE_SIZE_SCALE, 0, RenderArgs::DEFAULT_RENDER_MODE, RenderArgs::MONO, RenderArgs::RENDER_DEBUG_NONE); diff --git a/tests/shaders/src/main.cpp b/tests/shaders/src/main.cpp index d10ab1ddbe..9847e9f7b9 100644 --- a/tests/shaders/src/main.cpp +++ b/tests/shaders/src/main.cpp @@ -57,8 +57,6 @@ #include #include -#include -#include #include #include @@ -172,7 +170,6 @@ void QTestWindow::draw() { testShaderBuild(skin_model_normal_map_vert, model_translucent_frag); testShaderBuild(model_shadow_vert, model_shadow_frag); - testShaderBuild(untextured_particle_vert, untextured_particle_frag); testShaderBuild(textured_particle_vert, textured_particle_frag); /* FIXME: Bring back the ssao shader tests testShaderBuild(gaussian_blur_vertical_vert, gaussian_blur_frag); diff --git a/tools/atp-client/src/ATPClientApp.cpp b/tools/atp-client/src/ATPClientApp.cpp index 3e2f8ca71d..7d091aec74 100644 --- a/tools/atp-client/src/ATPClientApp.cpp +++ b/tools/atp-client/src/ATPClientApp.cpp @@ -23,6 +23,7 @@ #include #include #include +#include #include "ATPClientApp.h" @@ -137,6 +138,7 @@ ATPClientApp::ATPClientApp(int argc, char* argv[]) : Setting::init(); DependencyManager::registerInheritance(); + DependencyManager::set(); DependencyManager::set([&]{ return QString(HIGH_FIDELITY_ATP_CLIENT_USER_AGENT); }); DependencyManager::set(); DependencyManager::set(NodeType::Agent, _listenPort); diff --git a/unpublishedScripts/DomainContent/Cupcake/eatable.js b/unpublishedScripts/DomainContent/Cupcake/eatable.js new file mode 100644 index 0000000000..8b261b7ea2 --- /dev/null +++ b/unpublishedScripts/DomainContent/Cupcake/eatable.js @@ -0,0 +1,36 @@ +// +// eatable.js +// +// Created by Alan-Michael Moody on 7/24/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 () { + var NOMNOM_SOUND = SoundCache.getSound('http://hifi-content.s3.amazonaws.com/DomainContent/production/audio/vegcrunch.wav'); + + var _entityID; + + this.preload = function (entityID) { + _entityID = entityID; + }; + + this.collisionWithEntity = function(entityUuid, collisionEntityID, collisionInfo) { + var entity = Entities.getEntityProperties(collisionEntityID, ['userData', 'name']); + var isClone = (entity.name.substring(1).split('-')[0] === 'clone'); + var isEatable = (JSON.parse(entity.userData).eatable); + + if (isEatable && isClone) { + Audio.playSound(NOMNOM_SOUND, { + position: Entities.getEntityProperties(_entityID, ['position']).position, + volume: 0.2, + localOnly: false + }); + + Entities.deleteEntity(collisionEntityID); + } + }; +}); \ No newline at end of file diff --git a/unpublishedScripts/gravity.js b/unpublishedScripts/gravity.js new file mode 100644 index 0000000000..0ef7b0caa9 --- /dev/null +++ b/unpublishedScripts/gravity.js @@ -0,0 +1,56 @@ +// +// gravity.js +// +// Created by Alan-Michael Moody on 7/24/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 _entityID; + + this.preload = function (entityID) { + _entityID = entityID; + }; + + function update(deltatime) { + var planet = Entities.getEntityProperties(_entityID); + + //normalization happens in rotationBetween. + var direction = Vec3.subtract(MyAvatar.position, planet.position); + var localUp = Quat.getUp(MyAvatar.orientation); + + MyAvatar.orientation = Quat.normalize(Quat.multiply(Quat.rotationBetween(localUp, direction), MyAvatar.orientation)); + } + + function init() { + Script.update.connect(update); + } + + function clean() { + Script.update.disconnect(update); + MyAvatar.orientation = Quat.fromVec3Degrees({ + x: 0, + y: 0, + z: 0 + }); + } + + var _switch = true; + + this.clickDownOnEntity = function(uuid, mouseEvent) { + + if (_switch) { + init(); + } else { + clean(); + } + _switch = !_switch; + }; + + Script.scriptEnding.connect(clean); + +}); \ No newline at end of file diff --git a/unpublishedScripts/marketplace/camera-move/Eye-Camera.svg b/unpublishedScripts/marketplace/camera-move/Eye-Camera.svg new file mode 100644 index 0000000000..c58700823d --- /dev/null +++ b/unpublishedScripts/marketplace/camera-move/Eye-Camera.svg @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + diff --git a/unpublishedScripts/marketplace/camera-move/_debug.js b/unpublishedScripts/marketplace/camera-move/_debug.js new file mode 100644 index 0000000000..d8c05d646e --- /dev/null +++ b/unpublishedScripts/marketplace/camera-move/_debug.js @@ -0,0 +1,133 @@ +/* eslint-env jquery, browser */ +/* eslint-disable comma-dangle, no-empty */ +/* global EventBridge: true, PARAMS, signal, assert, log, debugPrint, + bridgedSettings, _utils, jquerySettings, */ + +// helper functions for debugging and testing the UI in an external web brower +var _debug = { + handleUncaughtException: function onerror(message, fileName, lineNumber, colNumber, err) { + if (message === onerror.lastMessage) { + return; + } + onerror.lastMessage = message; + var error = (err || Error.lastError); + // var stack = error && error.stack; + var output = _utils.normalizeStackTrace(error || { message: message }); + window.console.error(['window.onerror: ', output, message]); // eslint-disable-line no-console + var errorNode = document.querySelector('#errors'), + textNode = errorNode && errorNode.querySelector('.output'); + if (textNode) { + textNode.innerText = output; + } + if (errorNode) { + errorNode.style.display = 'block'; + } + if (error){ + error.onerrored = true; + } + }, + loadScriptNodes: function loadScriptNodes(selector) { + // scripts are loaded this way to ensure that when the client script refreshes, so are the app's dependencies + [].forEach.call(document.querySelectorAll(selector), function(script) { + script.parentNode.removeChild(script); + if (script.src) { + script.src += location.search; + } + script.type = 'application/javascript'; + document.write(script.outerHTML); + }); + }, + + // TESTING MOCKs + openEventBridgeMock: function openEventBridgeMock(onEventBridgeOpened) { + var updatedValues = openEventBridgeMock.updatedValues = {}; + // emulate EventBridge's API + EventBridge = { + emitWebEvent: signal(function emitWebEvent(message){}), + scriptEventReceived: signal(function scriptEventReceived(message){}), + }; + EventBridge.emitWebEvent.connect(onEmitWebEvent); + onEventBridgeOpened(EventBridge); + assert(!bridgedSettings.onUnhandledMessage); + bridgedSettings.onUnhandledMessage = function(msg) { + log('bridgedSettings.onUnhandledMessage', msg); + return true; + }; + // manually trigger initial bootstrapping responses (that the client script would normally send) + bridgedSettings.handleExtraParams({uuid: PARAMS.uuid, ns: PARAMS.ns, extraParams: { + mock: true, + appVersion: 'browsermock', + toggleKey: { text: 'SPACE', isShifted: true }, + mode: { + toolbar: true, + browser: true, + desktop: true, + tablet: /tablet/.test(location) || /android|ipad|iphone/i.test(navigator.userAgent), + hmd: /hmd/.test(location), + }, + } }); + bridgedSettings.setValue('ui-show-advanced-options', true); + + function log(msg) { + // eslint-disable-next-line no-console + console.log.apply(console, ['[mock] ' + msg].concat([].slice.call(arguments,1))); + } + + // generate mock data in response to outgoing web page events + function onEmitWebEvent(message) { + try { + var obj = JSON.parse(message); + } catch (e) {} + if (!obj) { + // message isn't JSON so just log it and bail early + log('consuming non-callback web event', message); + return; + } + switch (obj.method) { + case 'valueUpdated': { + log('valueUpdated',obj.params); + updatedValues[obj.params[0]] = obj.params[1]; + return; + } + case 'Settings.getValue': { + var key = obj.params[0]; + var node = jquerySettings.findNodeByKey(key, true); + // log('Settings.getValue.findNodeByKey', key, node); + var type = node && (node.dataset.hifiType || node.dataset.type || node.type); + switch (type) { + case 'hifiButton': + case 'hifiCheckbox': { + obj.result = /tooltip|advanced-options/i.test(key) || PARAMS.tooltiptest ? true : Math.random() > 0.5; + } break; + case 'hifiRadioGroup': { + var radios = $(node).find('input[type=radio]').toArray(); + while (Math.random() < 0.9) { + radios.push(radios.shift()); + } + obj.result = radios[0].value; + } break; + case 'hifiSpinner': + case 'hifiSlider': { + var step = node.step || 1, precision = (1/step).toString().length - 1; + var magnitude = node.max || (precision >=1 ? Math.pow(10, precision-1) : 10); + obj.result = parseFloat((Math.random() * magnitude).toFixed(precision||1)); + } break; + default: { + log('unhandled node type for making dummy data: ' + [key, node && node.type, type, node && node.type] + ' @ ' + (node && node.id)); + obj.result = updatedValues[key] || false; + } break; + } + debugPrint('mock getValue data %c%s = %c%s', 'color:blue', + JSON.stringify(key), 'color:green', JSON.stringify(obj.result)); + } break; + default: { + log('ignoring outbound method call', obj); + } break; + } + setTimeout(function() { + EventBridge.scriptEventReceived(JSON.stringify(obj)); + }, 100); + } + }, +}; + diff --git a/unpublishedScripts/marketplace/camera-move/_json-persist.js b/unpublishedScripts/marketplace/camera-move/_json-persist.js new file mode 100644 index 0000000000..5915842d1b --- /dev/null +++ b/unpublishedScripts/marketplace/camera-move/_json-persist.js @@ -0,0 +1,130 @@ +/* eslint-env jquery, browser */ +/* eslint-disable comma-dangle, no-empty */ +/* global _utils, PARAMS, VERSION, signal, assert, log, debugPrint, + bridgedSettings, POPUP */ + +// JSON export / import helpers proto module +var SettingsJSON = (function() { + _utils.exists; + assert.exists; + + return { + setPath: setPath, + rollupPaths: rollupPaths, + encodeNodes: encodeNodes, + exportAll: exportAll, + showSettings: showSettings, + applyJSON: applyJSON, + promptJSON: promptJSON, + popupJSON: popupJSON, + }; + + function encodeNodes(resolver) { + return resolver.getAllNodes().reduce((function(out, input, i) { + //debugPrint('input['+i+']', input.id); + var id = input.id, + key = resolver.getKey(id); + //debugPrint('toJSON', id, key, input.id); + setPath(out, key.split('/'), resolver.getValue(key)); + return out; + }).bind(this), {}); + } + + function setPath(obj, path, value) { + var key = path.pop(); + obj = path.reduce(function(obj, subkey) { + return obj[subkey] = obj[subkey] || {}; + }, obj); + //debugPrint('setPath', key, Object.keys(obj)); + obj[key] = value; + } + + function rollupPaths(obj, output, path) { + path = path || []; + output = output || {}; + // log('rollupPaths', Object.keys(obj||{}), Object.keys(output), path); + for (var p in obj) { + path.push(p); + var value = obj[p]; + if (value && typeof value === 'object') { + rollupPaths(obj[p], output, path); + } else { + output[path.join('/')] = value; + } + path.pop(); + } + return output; + } + + function exportAll(resolver, name) { + var settings = encodeNodes(resolver); + Object.keys(settings).forEach(function(prop) { + if (typeof settings[prop] === 'object') { + _utils.sortedAssign(settings[prop]); + } + }); + return { + version: VERSION, + name: name || undefined, + settings: settings, + _metadata: { timestamp: new Date(), PARAMS: PARAMS, url: location.href, } + }; + } + + function showSettings(resolver, saveName) { + popupJSON(saveName || '(current settings)', Object.assign(exportAll(resolver, saveName), { + extraParams: bridgedSettings.extraParams, + })); + } + + function popupJSON(title, tmp) { + var HTML = document.getElementById('POPUP').innerHTML + .replace(/\bxx-script\b/g, 'script') + .replace('JSON', JSON.stringify(tmp, 0, 2).replace(/\n/g, '
')); + if (/WebWindowEx/.test(navigator.userAgent) ) { + bridgedSettings.sendEvent({ + method: 'overlayWebWindow', + userAgent: navigator.userAgent, + options: { + title: 'app-camera-move-export' + (title ? '::'+title : ''), + content: HTML, + }, + }); + } else { + // append a footer to the data URI so it displays cleaner in the built-in browser window that opens + var footer = '<\!-- #' + HTML.substr(0,256).replace(/./g,' ') + (title || 'Camera Move Settings'); + window.open("data:text/html;escape," + encodeURIComponent(HTML) + footer,"app-camera-move-export"); + } + } + + function applyJSON(resolver, name, tmp) { + assert(tmp && 'version' in tmp && 'settings' in tmp, 'invalid settings record: ' + JSON.stringify(tmp)); + var settings = rollupPaths(tmp.settings); + for (var p in settings) { + if (/^[.]/.test(p)) { + continue; + } + var key = resolver.getId(p, true); + if (!key) { + log('$applySettings -- skipping unregistered Settings key: ', p); + } else { + resolver.setValue(p, settings[p], name+'.settings.'+p); + } + } + } + + function promptJSON() { + var json = window.prompt('(paste JSON here)', ''); + if (!json) { + return; + } + try { + log('parsing json', json); + json = JSON.parse(json); + } catch (e) { + throw new Error('Could not parse pasted JSON: ' + e + '\n\n' + (json||'').replace(/").children(0).text(hash); + + // patch less with absolute line number reporting + options.errorReporting && options.errorReporting.$patch && options.errorReporting.$patch(); + + // to recompile inline styles (in response to onresize or when developing), + // a fresh copy of the source nodes gets swapped-in + var newNodes = lessManager.elements.clone().appendTo(document.body); + // note: refresh(reload, modifyVars, clearFileCache) + less.refresh(false, globalVars).then(function(result) { + debugPrint('less.refresh completed OK', result); + })['catch'](function(err) { + log('less ERROR:', err); + }); + var oldNodes = onViewportUpdated.lastNodes; + oldNodes && oldNodes.remove(); + onViewportUpdated.lastNodes = newNodes; + } + onViewportUpdated.lastHash = hash; + } +} diff --git a/unpublishedScripts/marketplace/camera-move/_tooltips.js b/unpublishedScripts/marketplace/camera-move/_tooltips.js new file mode 100644 index 0000000000..94206b2ef9 --- /dev/null +++ b/unpublishedScripts/marketplace/camera-move/_tooltips.js @@ -0,0 +1,169 @@ +/* eslint-env jquery, browser */ +/* eslint-disable comma-dangle, no-empty */ +/* global PARAMS, signal, assert, log, debugPrint */ + +// ---------------------------------------------------------------------------- +// manage jquery-tooltipster hover tooltips +var TooltipManager = (function(global) { + Object.assign(TooltipManager, { + BASECONFIG: { + theme: ['tooltipster-noir'], + side: ['right','top','bottom', 'left'], + updateAnimation: 'scale', + delay: [750, 1000], + distance: { right: 24, left: 8, top: 8, bottom: 8 }, + contentAsHTML: true, + }, + }); + + function TooltipManager(options) { + assert(options.elements && options.tooltips, 'TooltipManager constructor expects .elements and .tooltips'); + Object.assign(this, { + instances: [], + options: options, + config: Object.assign({}, TooltipManager.BASECONFIG, { + trigger: !options.testmode ? 'hover' : 'click', + interactive: options.testmode, + minWidth: options.viewport && options.viewport.min.width, + maxWidth: options.viewport && options.viewport.max.width, + }), + }); + options.enabled && this.initialize(); + } + + TooltipManager.prototype = { + constructor: TooltipManager, + initialize: function() { + var options = this.options, + _config = this.config, + _self = this, + candidates = $(options.elements); + + candidates.add($('button')).each(function() { + var id = this.id, + input = $(this), + tip = options.tooltips[id] || options.tooltips[input.data('for')]; + + var alreadyTipped = input.is('.tooltipstered') || input.closest('.tooltipstered').get(0); + if (alreadyTipped || !tip) { + return !tip && _debugPrint('!tooltippable -- missing tooltip for ' + (id || input.data('for') || input.text())); + } + var config = Object.assign({ content: tip }, _config); + + function mergeConfig() { + var attr = $(this).attr('data-tooltipster'), + object = $(this).data('tooltipster'); + typeof object === 'object' && Object.assign(config, object); + attr && Object.assign(config, JSON.parse(attr)); + } + try { + input.parents(':data(tooltipster),[data-tooltipster]').each(mergeConfig); + input.each(mergeConfig); // prioritize own settings + } catch(e) { + console.error('error extracting tooltipster data:' + [e, id]); + } + + var target = $(input.closest('.tooltip-target').get(0) || + (input.is('input') && input) || null); + + assert(target && target[0] && tip); + debugPrint('binding tooltip', config, target[0].nodeName, id || target[0]); + var instance = target.tooltipster(config) + .tooltipster('instance'); + + instance.on('close', function(event) { + if (options.keepopen === target) { + debugPrint(event.type, 'canceling close keepopen === target', id); + event.stop(); + options.keepopen = null; + } + }); + instance.on('before', function(event) { + debugPrint(event.type, 'before', event); + !options.testmode && _self.closeAll(); + !options.enabled && event.stop(); + }); + target.find(':focusable, input, [tabindex], button, .control') + .add(target).add(input) + .add(input.closest(':focusable, input, [tabindex]')) + .on({ + click: function(evt) { + if (input.is('button')) { + return setTimeout(instance.close.bind(instance,null),50); + } + options.keepopen = target; + }, + focus: instance.open.bind(instance, null), + blur: function(evt) { + instance.close(); _self.openFocusedTooltip(); + }, + }); + _self.instances.push(instance); + }); + return this.instances; + }, + openFocusedTooltip: function() { + if (!this.options.enabled) { + return; + } + setTimeout(function() { + if (!document.activeElement || document.activeElement === document.body || + !$(document.activeElement).closest('section')) { + return; + } + var tip = $([]) + .add($(document.activeElement)) + .add($(document.activeElement).find('.tooltipstered')) + .add($(document.activeElement).closest('.tooltipstered')) + .filter('.tooltipstered'); + if (tip.is('.tooltipstered')) { + // log('opening focused tooltip', tip.length, tip[0].id); + tip.tooltipster('open'); + } + }, 1); + }, + rapidClose: function(instance, reopen) { + if (!instance.status().open) { + return; + } + instance.elementTooltip() && $(instance.elementTooltip()).hide(); + instance.close(function() { + reopen && instance.open(); + }); + }, + openAll: function() { + $('.tooltipstered').tooltipster('open'); + }, + closeAll: function() { + $.tooltipster.instances().forEach(function(instance) { + this.rapidClose(instance); + }.bind(this)); + }, + updateViewport: function(viewport) { + var options = { + minWidth: viewport.min.width, + maxWidth: viewport.max.width, + }; + Object.assign(this.config, options); + $.tooltipster.setDefaults(options); + debugPrint('updating tooltipster options', JSON.stringify(options)); + $.tooltipster.instances().forEach(function(instance) { + instance.option('minWidth', options.minWidth); + instance.option('maxWidth', options.maxWidth); + this.rapidClose(instance, instance.status().open); + }.bind(this)); + }, + enable: function() { + this.options.enabled = true; + if (this.options.testmode) { + this.openAll(); + } + }, + disable: function() { + this.options.enabled = false; + this.closeAll(); + }, + };// prototype + + return TooltipManager; +})(this); diff --git a/unpublishedScripts/marketplace/camera-move/app-camera-move.js b/unpublishedScripts/marketplace/camera-move/app-camera-move.js new file mode 100644 index 0000000000..f58dd3d3bd --- /dev/null +++ b/unpublishedScripts/marketplace/camera-move/app-camera-move.js @@ -0,0 +1,644 @@ +// app-camera-move.js +// +// Created by Timothy Dedischew on 05/05/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 +// +// This Client script sets up the Camera Control Tablet App, which can be used to configure and +// drive your avatar with easing/smoothing movement constraints for a less jittery filming experience. + +/* eslint-disable comma-dangle, no-empty */ +"use strict"; + +var VERSION = '0.0.1', + NAMESPACE = 'app-camera-move', + APP_HTML_URL = Script.resolvePath('app.html'), + BUTTON_CONFIG = { + text: '\nCam Drive', + icon: Script.resolvePath('Eye-Camera.svg'), + }, + DEFAULT_TOGGLE_KEY = { text: 'SPACE' }; + +var MINIMAL_CURSOR_SCALE = 0.5, + FILENAME = Script.resolvePath(''), + WANT_DEBUG = Settings.getValue(NAMESPACE + '/debug', false) + EPSILON = 1e-6; + +function log() { + print( NAMESPACE + ' | ' + [].slice.call(arguments).join(' ') ); +} + +var require = Script.require, + debugPrint = function(){}, + _debugChannel = NAMESPACE + '.stats', + overlayDebugOutput = function(){}; + +if (WANT_DEBUG) { + log('WANT_DEBUG is true; instrumenting debug support', WANT_DEBUG); + _instrumentDebug(); +} + +var _utils = require('./modules/_utils.js'), + assert = _utils.assert, + CustomSettingsApp = require('./modules/custom-settings-app/CustomSettingsApp.js'), + movementUtils = require('./modules/movement-utils.js?'+ +new Date), + configUtils = require('./modules/config-utils.js'), + AvatarUpdater = require('./avatar-updater.js'); + + +Object.assign = Object.assign || _utils.assign; + +var cameraControls, eventMapper, cameraConfig, applicationConfig; + +var DEFAULTS = { + 'namespace': NAMESPACE, + 'debug': WANT_DEBUG, + 'jitter-test': false, + 'camera-move-enabled': false, + 'thread-update-mode': movementUtils.CameraControls.SCRIPT_UPDATE, + 'fps': 90, + 'drive-mode': movementUtils.DriveModes.MOTOR, + 'use-head': true, + 'stay-grounded': true, + 'prevent-roll': true, + 'constant-delta-time': false, + 'minimal-cursor': false, + 'normalize-inputs': false, + 'enable-mouse-smooth': true, + 'translation-max-velocity': 5.50, + 'translation-ease-in': 1.25, + 'translation-ease-out': 5.50, + 'rotation-max-velocity': 90.00, + 'rotation-ease-in': 1.00, + 'rotation-ease-out': 5.50, + 'rotation-x-speed': 45, + 'rotation-y-speed': 60, + 'rotation-z-speed': 1, + 'mouse-multiplier': 1.0, + 'keyboard-multiplier': 1.0, + + 'ui-enable-tooltips': true, + 'ui-show-advanced-options': false, + + 'Avatar/Draw Mesh': true, + 'Scene/shouldRenderEntities': true, + 'Scene/shouldRenderAvatars': true, + 'Avatar/Show My Eye Vectors': false, + 'Avatar/Show Other Eye Vectors': false, +}; + +// map setting names to/from corresponding Menu and API properties +var APPLICATION_SETTINGS = { + 'Avatar/Enable Avatar Collisions': { + menu: 'Avatar > Enable Avatar Collisions', + object: [ MyAvatar, 'collisionsEnabled' ], + }, + 'Avatar/Draw Mesh': { + menu: 'Developer > Draw Mesh', + object: [ MyAvatar, 'getEnableMeshVisible', 'setEnableMeshVisible' ], + }, + 'Avatar/Show My Eye Vectors': { menu: 'Developer > Show My Eye Vectors' }, + 'Avatar/Show Other Eye Vectors': { menu: 'Developer > Show Other Eye Vectors' }, + 'Avatar/useSnapTurn': { object: [ MyAvatar, 'getSnapTurn', 'setSnapTurn' ] }, + 'Avatar/lookAtSnappingEnabled': 'lookAtSnappingEnabled' in MyAvatar && { + menu: 'Developer > Enable LookAt Snapping', + object: [ MyAvatar, 'lookAtSnappingEnabled' ] + }, + 'Scene/shouldRenderEntities': { object: [ Scene, 'shouldRenderEntities' ] }, + 'Scene/shouldRenderAvatars': { object: [ Scene, 'shouldRenderAvatars' ] }, + 'camera-move-enabled': { + get: function() { + return cameraControls && cameraControls.enabled; + }, + set: function(nv) { + cameraControls.setEnabled(!!nv); + }, + }, +}; + +var DEBUG_INFO = { + // these values are also sent to the tablet app after EventBridge initialization + appVersion: VERSION, + utilsVersion: _utils.version, + movementVersion: movementUtils.version, + configVersion: configUtils.version, + clientScript: Script.resolvePath(''), + MyAvatar: { + supportsPitchSpeed: 'pitchSpeed' in MyAvatar, + supportsYawSpeed: 'yawSpeed' in MyAvatar, + supportsLookAtSnappingEnabled: 'lookAtSnappingEnabled' in MyAvatar, + }, + Reticle: { + supportsScale: 'scale' in Reticle, + }, + protocolVersion: location.protocolVersion, +}; + +var globalState = { + // cached values from the last animation frame + previousValues: { + reset: function() { + this.pitchYawRoll = Vec3.ZERO; + this.thrust = Vec3.ZERO; + }, + }, + + // batch updates to MyAvatar/Camera properties (submitting together seems to help reduce jitter) + pendingChanges: _utils.DeferredUpdater.createGroup({ + Camera: Camera, + MyAvatar: MyAvatar, + }, { dedupe: false }), + + // current input controls' effective velocities + currentVelocities: new movementUtils.VelocityTracker({ + translation: Vec3.ZERO, + rotation: Vec3.ZERO, + zoom: Vec3.ZERO, + }), +}; + +function main() { + log('initializing...', VERSION); + + var tablet = Tablet.getTablet('com.highfidelity.interface.tablet.system'), + button = tablet.addButton(BUTTON_CONFIG); + + Script.scriptEnding.connect(function() { + tablet.removeButton(button); + button = null; + }); + + // track runtime state (applicationConfig) and Settings state (cameraConfig) + applicationConfig = new configUtils.ApplicationConfig({ + namespace: DEFAULTS.namespace, + config: APPLICATION_SETTINGS, + }); + cameraConfig = new configUtils.SettingsConfig({ + namespace: DEFAULTS.namespace, + defaultValues: DEFAULTS, + }); + + var toggleKey = DEFAULT_TOGGLE_KEY; + if (cameraConfig.getValue('toggle-key')) { + try { + toggleKey = JSON.parse(cameraConfig.getValue('toggle-key')); + } catch (e) {} + } + // monitor configuration changes / keep tablet app up-to-date + var MONITOR_INTERVAL_MS = 1000; + _startConfigationMonitor(applicationConfig, cameraConfig, MONITOR_INTERVAL_MS); + + // ---------------------------------------------------------------------------- + // set up the tablet app + log('APP_HTML_URL', APP_HTML_URL); + var settingsApp = new CustomSettingsApp({ + namespace: cameraConfig.namespace, + uuid: cameraConfig.uuid, + settingsAPI: cameraConfig, + url: APP_HTML_URL, + tablet: tablet, + extraParams: Object.assign({ + toggleKey: toggleKey, + }, getSystemMetadata(), DEBUG_INFO), + debug: WANT_DEBUG > 1, + }); + Script.scriptEnding.connect(settingsApp, 'cleanup'); + settingsApp.valueUpdated.connect(function(key, value, oldValue, origin) { + log('settingsApp.valueUpdated: '+ key + ' = ' + JSON.stringify(value) + ' (was: ' + JSON.stringify(oldValue) + ')'); + if (/tablet/i.test(origin)) { + // apply relevant settings immediately if changed from the tablet UI + if (applicationConfig.applyValue(key, value, origin)) { + log('settingsApp applied immediate setting', key, value); + } + } + }); + + // process custom eventbridge messages + settingsApp.onUnhandledMessage = function(msg) { + switch (msg.method) { + case 'window.close': { + this.toggle(false); + } break; + case 'reloadClientScript': { + log('reloadClientScript...'); + _utils.reloadClientScript(FILENAME); + } break; + case 'resetSensors': { + Menu.triggerOption('Reset Sensors'); + Script.setTimeout(function() { + MyAvatar.bodyPitch = 0; + MyAvatar.bodyRoll = 0; + MyAvatar.orientation = Quat.cancelOutRollAndPitch(MyAvatar.orientation); + }, 500); + } break; + case 'reset': { + var resetValues = {}; + // maintain current value of 'show advanced' so user can observe any advanced settings being reset + var showAdvancedKey = cameraConfig.resolve('ui-show-advanced-options'); + resetValues[showAdvancedKey] = cameraConfig.getValue(showAdvancedKey); + Object.keys(DEFAULTS).reduce(function(out, key) { + var resolved = cameraConfig.resolve(key); + out[resolved] = resolved in out ? out[resolved] : DEFAULTS[key]; + return out; + }, resetValues); + Object.keys(applicationConfig.config).reduce(function(out, key) { + var resolved = applicationConfig.resolve(key); + out[resolved] = resolved in out ? out[resolved] : applicationConfig.getValue(key); + return out; + }, resetValues); + log('restting to system defaults:', JSON.stringify(resetValues, 0, 2)); + for (var p in resetValues) { + var value = resetValues[p]; + applicationConfig.applyValue(p, value, 'reset'); + cameraConfig.setValue(p, value); + } + } break; + default: { + log('onUnhandledMessage', JSON.stringify(msg,0,2)); + } break; + } + }; + + // ---------------------------------------------------------------------------- + // set up the keyboard/mouse/controller input state manager + eventMapper = new movementUtils.MovementEventMapper({ + namespace: DEFAULTS.namespace, + mouseSmooth: cameraConfig.getValue('enable-mouse-smooth'), + mouseMultiplier: cameraConfig.getValue('mouse-multiplier'), + keyboardMultiplier: cameraConfig.getValue('keyboard-multiplier'), + eventFilter: function eventFilter(from, event, defaultFilter) { + var result = defaultFilter(from, event), + driveKeyName = event.driveKeyName; + if (!result || !driveKeyName) { + if (from === 'Keyboard.RightMouseButton') { + // let the app know when the user is mouse looking + settingsApp.syncValue('Keyboard.RightMouseButton', event.actionValue, 'eventFilter'); + } + return 0; + } + if (cameraConfig.getValue('normalize-inputs')) { + result = _utils.sign(result); + } + if (from === 'Actions.Pitch') { + result *= cameraConfig.getFloat('rotation-x-speed'); + } else if (from === 'Actions.Yaw') { + result *= cameraConfig.getFloat('rotation-y-speed'); + } + return result; + }, + }); + Script.scriptEnding.connect(eventMapper, 'disable'); + // keep track of these changes live so the controller mapping can be kept in sync + applicationConfig.register({ + 'enable-mouse-smooth': { object: [ eventMapper.options, 'mouseSmooth' ] }, + 'keyboard-multiplier': { object: [ eventMapper.options, 'keyboardMultiplier' ] }, + 'mouse-multiplier': { object: [ eventMapper.options, 'mouseMultiplier' ] }, + }); + + // ---------------------------------------------------------------------------- + // set up the top-level camera controls manager / animator + var avatarUpdater = new AvatarUpdater({ + debugChannel: _debugChannel, + globalState: globalState, + getCameraMovementSettings: getCameraMovementSettings, + getMovementState: _utils.bind(eventMapper, 'getState'), + }); + cameraControls = new movementUtils.CameraControls({ + namespace: DEFAULTS.namespace, + update: avatarUpdater, + threadMode: cameraConfig.getValue('thread-update-mode'), + fps: cameraConfig.getValue('fps'), + getRuntimeSeconds: _utils.getRuntimeSeconds, + }); + Script.scriptEnding.connect(cameraControls, 'disable'); + applicationConfig.register({ + 'thread-update-mode': { object: [ cameraControls, 'threadMode' ] }, + 'fps': { object: [ cameraControls, 'fps' ] }, + }); + + // ---------------------------------------------------------------------------- + // set up SPACEBAR for toggling camera movement mode + var spacebar = new _utils.KeyListener(Object.assign(toggleKey, { + onKeyPressEvent: function(event) { + cameraControls.setEnabled(!cameraControls.enabled); + }, + })); + Script.scriptEnding.connect(spacebar, 'disconnect'); + + // ---------------------------------------------------------------------------- + // set up ESC for resetting all drive key states + Script.scriptEnding.connect(new _utils.KeyListener({ + text: 'ESC', + onKeyPressEvent: function(event) { + if (cameraControls.enabled) { + log('ESC pressed -- resetting drive keys:', JSON.stringify({ + virtualDriveKeys: eventMapper.states, + movementState: eventMapper.getState(), + }, 0, 2)); + eventMapper.states.reset(); + MyAvatar.velocity = Vec3.ZERO; + MyAvatar.angularVelocity = Vec3.ZERO; + } + }, + }), 'disconnect'); + + // set up the tablet button to toggle the UI display + button.clicked.connect(settingsApp, function(enable) { + Object.assign(this.extraParams, getSystemMetadata()); + button.editProperties({ text: '(opening)' + BUTTON_CONFIG.text, isActive: true }); + this.toggle(enable); + }); + + settingsApp.isActiveChanged.connect(function(isActive) { + updateButtonText(); + if (Overlays.getOverlayType(HMD.tabletScreenID)) { + var fromMode = Overlays.getProperty(HMD.tabletScreenID, 'inputMode'), + inputMode = isActive ? "Mouse" : "Touch"; + log('switching HMD.tabletScreenID from inputMode', fromMode, 'to', inputMode); + Overlays.editOverlay(HMD.tabletScreenID, { inputMode: inputMode }); + } + }); + + cameraControls.modeChanged.connect(onCameraModeChanged); + + function updateButtonText() { + var lines = [ + settingsApp.isActive ? '(app open)' : '', + cameraControls.enabled ? (avatarUpdater.update.momentaryFPS||0).toFixed(2) + 'fps' : BUTTON_CONFIG.text.trim() + ]; + button && button.editProperties({ text: lines.join('\n') }); + } + + var fpsTimeout = 0; + cameraControls.enabledChanged.connect(function(enabled) { + log('enabledChanged', enabled); + button && button.editProperties({ isActive: enabled }); + if (enabled) { + onCameraControlsEnabled(); + fpsTimeout = Script.setInterval(updateButtonText, 1000); + } else { + if (fpsTimeout) { + Script.clearInterval(fpsTimeout); + fpsTimeout = 0; + } + eventMapper.disable(); + avatarUpdater._resetMyAvatarMotor({ MyAvatar: MyAvatar }); + updateButtonText(); + if (settingsApp.isActive) { + settingsApp.syncValue('Keyboard.RightMouseButton', false, 'cameraControls.disabled'); + } + } + overlayDebugOutput.overlayID && Overlays.editOverlay(overlayDebugOutput.overlayID, { visible: enabled }); + }); + + // when certain settings change we need to reset the drive systems + var resetIfChanged = [ + 'minimal-cursor', 'drive-mode', 'fps', 'thread-update-mode', + 'mouse-multiplier', 'keyboard-multiplier', + 'enable-mouse-smooth', 'constant-delta-time', + ].filter(Boolean).map(_utils.bind(cameraConfig, 'resolve')); + + cameraConfig.valueUpdated.connect(function(key, value, oldValue, origin) { + var triggerReset = !!~resetIfChanged.indexOf(key); + log('cameraConfig.valueUpdated: ' + key + ' = ' + JSON.stringify(value), '(was:' + JSON.stringify(oldValue) + ')', + 'triggerReset: ' + triggerReset); + + if (/tablet/i.test(origin)) { + if (applicationConfig.applyValue(key, value, origin)) { + log('cameraConfig applied immediate setting', key, value); + } + + } + triggerReset && cameraControls.reset(); + }); + + if (cameraConfig.getValue('camera-move-enabled')) { + cameraControls.enable(); + } + + log('DEFAULTS', JSON.stringify(DEFAULTS, 0, 2)); +} // main() + +function onCameraControlsEnabled() { + log('onCameraControlsEnabled'); + globalState.previousValues.reset(); + globalState.currentVelocities.reset(); + globalState.pendingChanges.reset(); + eventMapper.enable(); + if (cameraConfig.getValue('minimal-cursor')) { + Reticle.scale = MINIMAL_CURSOR_SCALE; + } + log('cameraConfig', JSON.stringify({ + cameraConfig: getCameraMovementSettings(), + })); +} + +// reset orientation-related values when the Camera.mode changes +function onCameraModeChanged(mode, oldMode) { + globalState.pendingChanges.reset(); + globalState.previousValues.reset(); + eventMapper.reset(); + var preventRoll = cameraConfig.getValue('prevent-roll'); + var avatarOrientation = cameraConfig.getValue('use-head') ? MyAvatar.headOrientation : MyAvatar.orientation; + if (preventRoll) { + avatarOrientation = Quat.cancelOutRollAndPitch(avatarOrientation); + } + switch (Camera.mode) { + case 'mirror': + case 'entity': + case 'independent': + globalState.currentVelocities.reset(); + break; + default: + Camera.position = MyAvatar.position; + Camera.orientation = avatarOrientation; + break; + } + MyAvatar.orientation = avatarOrientation; + if (preventRoll) { + MyAvatar.headPitch = MyAvatar.headRoll = 0; + } +} + +// consolidate and normalize cameraConfig settings +function getCameraMovementSettings() { + return { + epsilon: EPSILON, + debug: cameraConfig.getValue('debug'), + jitterTest: cameraConfig.getValue('jitter-test'), + driveMode: cameraConfig.getValue('drive-mode'), + threadMode: cameraConfig.getValue('thread-update-mode'), + fps: cameraConfig.getValue('fps'), + useHead: cameraConfig.getValue('use-head'), + stayGrounded: cameraConfig.getValue('stay-grounded'), + preventRoll: cameraConfig.getValue('prevent-roll'), + useConstantDeltaTime: cameraConfig.getValue('constant-delta-time'), + + collisionsEnabled: applicationConfig.getValue('Avatar/Enable Avatar Collisions'), + mouseSmooth: cameraConfig.getValue('enable-mouse-smooth'), + mouseMultiplier: cameraConfig.getValue('mouse-multiplier'), + keyboardMultiplier: cameraConfig.getValue('keyboard-multiplier'), + + rotation: _getEasingGroup(cameraConfig, 'rotation'), + translation: _getEasingGroup(cameraConfig, 'translation'), + zoom: _getEasingGroup(cameraConfig, 'zoom'), + }; + + // extract a single easing group (translation, rotation, or zoom) from cameraConfig + function _getEasingGroup(cameraConfig, group) { + var multiplier = 1.0; + if (group === 'zoom') { + // BoomIn / TranslateCameraZ support is only partially plumbed -- for now use scaled translation easings + group = 'translation'; + multiplier = 0.001; + } + return { + easeIn: cameraConfig.getFloat(group + '-ease-in'), + easeOut: cameraConfig.getFloat(group + '-ease-out'), + maxVelocity: multiplier * cameraConfig.getFloat(group + '-max-velocity'), + speed: Vec3.multiply(multiplier, { + x: cameraConfig.getFloat(group + '-x-speed'), + y: cameraConfig.getFloat(group + '-y-speed'), + z: cameraConfig.getFloat(group + '-z-speed') + }), + }; + } +} + +// monitor and sync Application state -> Settings values +function _startConfigationMonitor(applicationConfig, cameraConfig, interval) { + return Script.setInterval(function monitor() { + var settingNames = Object.keys(applicationConfig.config); + settingNames.forEach(function(key) { + applicationConfig.resyncValue(key); // align Menus <=> APIs + var value = cameraConfig.getValue(key), + appValue = applicationConfig.getValue(key); + if (appValue !== undefined && String(appValue) !== String(value)) { + log('applicationConfig -> cameraConfig', + key, [typeof appValue, appValue], '(was:'+[typeof value, value]+')'); + cameraConfig.setValue(key, appValue); // align Application <=> Settings + } + }); + }, interval); +} + +// ---------------------------------------------------------------------------- +// DEBUG overlay support (enable by setting app-camera-move/debug = true in settings +// ---------------------------------------------------------------------------- +function _instrumentDebug() { + debugPrint = log; + var cacheBuster = '?' + new Date().getTime().toString(36); + require = Script.require(Script.resolvePath('./modules/_utils.js') + cacheBuster).makeDebugRequire(Script.resolvePath('.')); + APP_HTML_URL += cacheBuster; + overlayDebugOutput = _createOverlayDebugOutput({ + lineHeight: 12, + font: { size: 12 }, + width: 250, height: 800 }); + // auto-disable camera move mode when debugging + Script.scriptEnding.connect(function() { + cameraConfig && cameraConfig.setValue('camera-move-enabled', false); + }); +} + +function _fixedPrecisionStringifiyFilter(key, value, object) { + if (typeof value === 'object' && value && 'w' in value) { + return Quat.safeEulerAngles(value); + } else if (typeof value === 'number') { + return value.toFixed(4)*1; + } + return value; +} + +function _createOverlayDebugOutput(options) { + options = require('./modules/_utils.js').assign({ + x: 0, y: 0, width: 500, height: 800, visible: false + }, options || {}); + options.lineHeight = options.lineHeight || Math.round(options.height / 36); + options.font = options.font || { size: Math.floor(options.height / 36) }; + overlayDebugOutput.overlayID = Overlays.addOverlay('text', options); + + Messages.subscribe(_debugChannel); + Messages.messageReceived.connect(onMessageReceived); + + Script.scriptEnding.connect(function() { + Overlays.deleteOverlay(overlayDebugOutput.overlayID); + Messages.unsubscribe(_debugChannel); + Messages.messageReceived.disconnect(onMessageReceived); + }); + function overlayDebugOutput(output) { + var text = JSON.stringify(output, _fixedPrecisionStringifiyFilter, 2); + if (text !== overlayDebugOutput.lastText) { + overlayDebugOutput.lastText = text; + Overlays.editOverlay(overlayDebugOutput.overlayID, { text: text }); + } + } + function onMessageReceived(channel, message, ssend, local) { + if (local && channel === _debugChannel) { + overlayDebugOutput(JSON.parse(message)); + } + } + return overlayDebugOutput; +} + +// ---------------------------------------------------------------------------- +_patchCameraModeSetting(); +function _patchCameraModeSetting() { + // FIXME: looks like the Camera API suffered a regression where Camera.mode = 'first person' or 'third person' + // no longer works from the API; setting via Menu items still seems to work though. + Camera.$setModeString = Camera.$setModeString || function(mode) { + // 'independent' => "Independent Mode", 'first person' => 'First Person', etc. + var cameraMenuItem = (mode+'') + .replace(/^(independent|entity)$/, '$1 mode') + .replace(/\b[a-z]/g, function(ch) { + return ch.toUpperCase(); + }); + + log('working around Camera.mode bug by enabling the menuItem:', cameraMenuItem); + Menu.setIsOptionChecked(cameraMenuItem, true); + }; +} + +function getSystemMetadata() { + var tablet = Tablet.getTablet('com.highfidelity.interface.tablet.system'); + return { + mode: { + hmd: HMD.active, + desktop: !HMD.active, + toolbar: Uuid.isNull(HMD.tabletID), + tablet: !Uuid.isNull(HMD.tabletID), + }, + tablet: { + toolbarMode: tablet.toolbarMode, + desktopScale: Settings.getValue('desktopTabletScale'), + hmdScale: Settings.getValue('hmdTabletScale'), + }, + window: { + width: Window.innerWidth, + height: Window.innerHeight, + }, + desktop: { + width: Desktop.width, + height: Desktop.height, + }, + }; +} + +// ---------------------------------------------------------------------------- +main(); + +if (typeof module !== 'object') { + // if uncaught exceptions occur, show the first in an alert with option to stop the script + Script.unhandledException.connect(function onUnhandledException(error) { + Script.unhandledException.disconnect(onUnhandledException); + log('UNHANDLED EXCEPTION', error, error && error.stack); + try { + cameraControls.disable(); + } catch (e) {} + // show the error message and first two stack entries + var trace = _utils.normalizeStackTrace(error); + var message = [ error ].concat(trace.split('\n').slice(0,2)).concat('stop script?').join('\n'); + Window.confirm('app-camera-move error: ' + message.substr(0,256)) && Script.stop(); + }); +} diff --git a/unpublishedScripts/marketplace/camera-move/app.html b/unpublishedScripts/marketplace/camera-move/app.html new file mode 100644 index 0000000000..100a8237b5 --- /dev/null +++ b/unpublishedScripts/marketplace/camera-move/app.html @@ -0,0 +1,1340 @@ + + + + Camera Move + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+

Camera Move

+ +
...
+
+
+ +
+
+

Translation

+
+ + + + avatar walking/flying speed limit (ie: the value eased-into) + +
+
+ +
+ +
    +
  • lower values gently ramp-into moving
  • +
  • higher values rapidly accelerate to top speed
  • +
+
+
+ +
+ +
    +
  • lower values bring movement to a rolling stop
  • +
  • higher values stop movements more immediately
  • +
+
+
+ +
+ +
+

Rotation

+
+ + + look up/down (pitch) and turn left/right (yaw) speed limit +
+
+ +
+ +
    +
  • lower values gently start turning or looking up/down
  • +
  • higher values quickly turn and look around
  • +
+
+
+ +
+ +
    +
  • lower values bring turning/looking to a rolling stop
  • +
  • higher values stop turning/looking more immediately
  • +
+
+
+ +
+ +
+

Options

+
+ + + + +
+
+ + + +
+ +
+
+ +
+
+
keybinding:  
+
+
+ +
+
+
+
+ + + + + + + + + + + +
+ + diff --git a/unpublishedScripts/marketplace/camera-move/app.js b/unpublishedScripts/marketplace/camera-move/app.js new file mode 100644 index 0000000000..0546ead02a --- /dev/null +++ b/unpublishedScripts/marketplace/camera-move/app.js @@ -0,0 +1,613 @@ +// app.js -- support functions + +/* eslint-env console, jquery, browser, shared-node-browser */ +/* eslint-disable comma-dangle */ +/* global Mousetrap, TooltipManager, SettingsJSON, PARAMS, signal, assert, log, debugPrint */ +// ---------------------------------------------------------------------------- + +var viewportUpdated, bridgedSettings, jquerySettings, tooltipManager, lessManager; + +function setupUI() { + $('#debug-menu button, footer button') + .hifiButton({ + create: function() { + $(this).addClass('tooltip-target') + .data('tooltipster', { side: ['top','bottom'] }); + } + }); + + var $json = SettingsJSON; + + window.buttonHandlers = { + 'test-event-bridge': function() { + log('bridgedSettings.eventBridge === Window.EventBridge', bridgedSettings.eventBridge === window.EventBridge); + bridgedSettings.sendEvent({ method: 'test-event-bridge' }); + EventBridge.emitWebEvent('EventBridge.emitWebEvent: testing 1..2..3..'); + }, + 'page-reload': function() { + log('triggering location.reload'); + location.reload(); + }, + 'script-reload': function() { + log('triggering script.reload'); + bridgedSettings.sendEvent({ method: 'reloadClientScript' }); + }, + 'reset-sensors': function() { + log('resetting avatar orientation'); + bridgedSettings.sendEvent({ method: 'resetSensors' }); + }, + 'reset-to-defaults': function() { + tooltipManager && tooltipManager.closeAll(); + document.activeElement && document.activeElement.blur(); + document.body.focus(); + setTimeout(function() { + bridgedSettings.sendEvent({ method: 'reset' }); + },1); + }, + 'copy-json': $json.showSettings.bind($json, jquerySettings, null), + 'paste-json': function() { + $json.applyJSON( + jquerySettings, + 'pasted', + $json.promptJSON() + ); + }, + 'toggle-advanced-options': function(evt) { + var checkbox = $(this).hifiButton('instance').checkbox; + var on = checkbox.value(!checkbox.value()); + $('body').toggleClass('ui-show-advanced-options', on); + evt.stopPropagation(); + evt.preventDefault(); + if ($(this).is('.tooltipstered')) { + $(this).tooltipster('instance').content((on ? 'hide' : 'show') + ' advanced options'); + } + if (checkbox.value()) { + $('.scrollable').delay(100).animate({ + scrollTop: innerHeight - $('header').innerHeight() - 24 + }, 1500); + } + }, + 'appVersion': function(evt) { + evt.shiftKey && $json.showSettings(jquerySettings); + }, + 'errors': function(evt) { + $(evt.target).is('button') && $(this).find('.output').text('').end().hide(); + }, + }; + buttonHandlers['button-toggle-advanced-options'] = + buttonHandlers['toggle-advanced-options']; + Object.keys(window.buttonHandlers).forEach(function(p) { + $('#' + p).on('click', window.buttonHandlers[p]); + }); + + // ---------------------------------------------------------------- + // trim whitespace in labels + $('label').contents().filter(function() { + if (this.nodeType !== window.Node.TEXT_NODE) { + return false; + } + this.textContent = this.textContent.trim(); + return !this.textContent.length; + }).remove(); + + var settingsNodes = $('fieldset,button[data-for],input:not([type=radio])'); + settingsNodes.each(function() { + // set up the bidirectional mapping between DOM and Settings + jquerySettings.registerNode(this); + }); + + var spinnerOptions = { + disabled: true, + create: function() { + var input = $(this), + key = assert(jquerySettings.getKey(input.data('for'))); + + var options = input.hifiSpinner('instance').options; + options.min = options.min || 0.0; + + bridgedSettings.getValueAsync(key, function(err, result) { + input.filter(':not([data-autoenable=false])').hifiSpinner('enable'); + jquerySettings.setValue(key, result); + }); + }, + }; + + $( ".rows > label" ).each(function() { + var label = $(this), + input = label.find('input'), + type = input.data('type') || input.attr('type'); + label.wrap('
').parent().addClass(['hifi-'+type, type, 'row'].join(' ')) + .on('focus.row, click.row, hover.row', function() { + $(this).find('.tooltipstered').tooltipster('open'); + }); + }); + + debugPrint('initializing hifiSpinners'); + // numeric settings + $( ".number.row" ) + .find( "input[data-type=number]" ) + .addClass('setting') + .hifiSpinner(spinnerOptions); + + // radio groups settings + $( ".radio.rows" ) + .find('label').addClass('tooltip-target').end() + .addClass('setting') + .hifiRadioGroup({ + direction: 'vertical', + disabled: true, + create: function() { + assert(this !== window); + var group = $(this), id = this.id; + var key = assert(jquerySettings.getKey(group.data('for'))); + + bridgedSettings.getValueAsync(key, function(err, result) { + debugPrint('> GOT RADIO', key, id, result); + group.filter(':not([data-autoenable=false])').hifiRadioGroup('enable'); + jquerySettings.setValue(key, result); + group.change(); + }); + }, + }) + + // checkbox settings + $( "input[type=checkbox]" ) + .addClass('setting') + .hifiCheckbox({ + disabled: true, + create: function() { + var key = assert(jquerySettings.getKey(this.id)), + input = $(this); + input.closest('label').addClass('tooltip-target'); + bridgedSettings.getValueAsync(key, function(err, result) { + input.filter(':not([data-autoenable=false])').hifiCheckbox('enable'); + jquerySettings.setValue(key, result); + }); + }, + }); + + // slider + numeric settings + // use the whole row as a tooltip target + $( ".slider.row" ).addClass('tooltip-target').data('tooltipster', { + distance: -20, + side: ['top', 'bottom'], + }).each(function(ent) { + var element = $(this).find( ".control" ), + input = $(this).find('input'), + id = input.prop('id'), + key = assert(jquerySettings.getKey(id)); + + var commonOptions = { + disabled: true, + min: parseFloat(input.prop('min') || 0), + max: parseFloat(input.prop('max') || 10), + step: parseFloat(input.prop('step') || 0.01), + autoenable: input.data('autoenable') !== 'false', + }; + debugPrint('commonOptions', commonOptions); + + // see: https://api.jqueryui.com/slider/ for more options + var slider = element.hifiSlider(Object.assign({ + orientation: "horizontal", + range: "min", + animate: 'fast', + value: 0.0 + }, commonOptions)).hifiSlider('instance'); + + debugPrint('initializing hifiSlider->hifiSpinner'); + // setup chrome up/down arrow steps and propagate input field -> slider + var spinner = input.on('change', function() { + var value = spinner.value(); + if (isFinite(value) && slider.value() !== value) { + slider.value(value); + } + }).addClass('setting') + .hifiSpinner( + Object.assign({}, commonOptions, { max: 1e4 }) + ).hifiSpinner('instance'); + + bridgedSettings.getValueAsync(key, function(err, result) { + slider.options.autoenable !== false && slider.enable(); + spinner.options.autoenable !== false && spinner.enable(); + spinner.value(result); + }); + }); + + $('#fps').hifiSpinner(spinnerOptions).closest('.row').css('pointer-events', 'all').on('click.subinput', function(evt) { + jquerySettings.setValue('thread-update-mode', 'requestAnimationFrame'); + evt.target.focus(); + }); + // detect invalid numbers entered into spinner fields + $(':ui-hifiSpinner').on('change.validation', function(evt) { + var spinner = $(this).hifiSpinner('instance'); + $(this).closest('.row').toggleClass('invalid', !spinner.isValid()); + }); + + // ---------------------------------------------------------------------------- + // allow tabbing between checkboxes using the container row + $(':ui-hifiCheckbox,:ui-hifiRadioButton').prop('tabindex', -1).closest('.row').prop('tabindex', 0); + + + // select the input field text when first focused + $('input').not('input[type=radio],input[type=checkbox]').on('focus', function () { + var dt = (new Date - this.blurredAt); + if (!(dt < 5)) { // debounce + this.blurredAt = +new Date; + $(this).one('mouseup.selectit', function() { + $(this).select(); + return false; + }).select(); + } + }).on('blur', function(e) { + this.blurredAt = new Date; + }); + + // monitor changes to specific settings that affect the UI + var monitors = { + // advanced options toggle + 'ui-show-advanced-options': function onChange(value) { + function handle(err, result) { + log('** ui-show-advanced-options updated', result+''); + $('body').toggleClass('ui-show-advanced-options', !!result); + jquerySettings.setValue('ui-show-advanced-options', result) + } + if (!onChange.fetched) { + bridgedSettings.getValueAsync('ui-show-advanced-options', handle); + return onChange.fetched = true; + } + handle(null, value); + }, + + // UI tooltips toggle + 'ui-enable-tooltips': function(value) { + if (!tooltipManager) return; + if (value) { + tooltipManager.enable(); + tooltipManager.openFocusedTooltip(); + } else { + tooltipManager.disable(); + } + }, + + // enable/disable fps field (based on whether thread mode is requestAnimationFrame) + 'thread-update-mode': function(value) { + var enabled = (value === 'requestAnimationFrame'), fps = $('#fps'); + fps.hifiSpinner(enabled ? 'enable' : 'disable'); + fps.closest('.row').toggleClass('disabled', !enabled); + }, + + // flag BODY with CSS class to indicate active camera move mode + 'camera-move-enabled': function(value) { + $('body').toggleClass('camera-move-enabled', value); + }, + + // update the "keybinding" and #appVersion extraParams displays + '.extraParams': function extraParams(value, other) { + value = bridgedSettings.extraParams; + if (value.mode) { + for (var p in value.mode) { + // tablet-mode, hmd-mode, etc. + $('body').toggleClass(p + '-mode', value.mode[p]); + } + document.oncontextmenu = value.mode.tablet ? function(evt) { return evt.preventDefault(),false; } : null; + $('[data-type=number]').prop('type', value.mode.tablet ? 'number' : 'text'); + } + var versionDisplay = [ + value.appVersion || '(unknown appVersion)', + PARAMS.debug && '(debug)', + value.mode && value.mode.tablet ? '(tablet)' : '', + ].filter(Boolean).join(' | '); + $('#appVersion').find('.output').text(versionDisplay).end().show(); + + if (value.toggleKey) { + $('#toggleKey').find('.binding').empty() + .append(getKeysHTML(value.toggleKey)).end().show(); + } + + var activateLookAtOption = value.MyAvatar && value.MyAvatar.supportsLookAtSnappingEnabled; + $(jquerySettings.findNodeByKey('Avatar/lookAtSnappingEnabled')) + .hifiCheckbox(activateLookAtOption ? 'enable' : 'disable') + .closest('.row').toggleClass('disabled', !activateLookAtOption) + .css('pointer-events', 'all') // so tooltips display regardless + + var activateCursorOption = value.Reticle && value.Reticle.supportsScale; + $('#minimal-cursor:ui-hifiCheckbox') + .hifiCheckbox(activateCursorOption ? 'enable' : 'disable') + .closest('.row').toggleClass('disabled', !activateCursorOption) + .css('pointer-events', 'all') // so tooltips display regardless + }, + + // gray out / ungray out page content if user is mouse looking around in Interface + // (otherwise the cursor still interacts with web content...) + 'Keyboard.RightMouseButton': function(localValue, key, value) { + debugPrint(localValue, '... Keyboard.RightMouseButton:' + value); + window.active = !value; + }, + }; + monitors['toggle-advanced-options'] = monitors['ui-toggle-advanced-options']; + monitorSettings(monitors); + // disable selection + // $('input').css('-webkit-user-select', 'none'); + + viewportUpdated.connect(lessManager, 'onViewportUpdated'); + + setupTooltips(); + + $(window).trigger('resize'); // populate viewport + + // set up DOM MutationObservers + settingsNodes.each(function tcobo() { + if (this.dataset.hifiType === 'hifiButton') { + return; + } + var id = assert(this.dataset['for'] || this.id, 'could not id for node: ' + this.outerHTML); + assert(!tcobo[id]); // detect dupes + tcobo[id] = true; + debugPrint('OBSERVING NODE', id, this.id || this.getAttribute('for')); + jquerySettings.observeNode(this); + }); + + // set up key bindings + setupMousetrapKeys(); + + function getKeysHTML(binding) { + var text = binding.text || ('(#' + binding.key + ')'); + // translate hifi's proprietary key scheme into human-friendly KBDs + return [ 'Control', 'Meta', 'Alt', 'Super', 'Menu', 'Shifted' ] + .map(function(flag) { + return binding['is' + flag] && flag; + }) + .concat(text) + .filter(Boolean) + .map(function(key) { + return '' + key.replace('Shifted','Shift') + ''; + }) + .join('-'); + } +} // setupUI + +function setupTooltips() { + // extract the tooltip captions + var tooltips = window.tooltips = {}; + var target = '[id], [data-for], [for]'; + $('.tooltip') + .removeClass('tooltip').addClass('x-tooltip') + .each(function() { + var element = $(this), + input = $(element.parent().find('input').get(0) || + element.closest('button').get(0)); + id = element.prop('id') || element.data('for') || + input.prop('id') || input.data('for'); + assert(id); + tooltips[id] = this.outerHTML; + }).hide(); + tooltipManager = new TooltipManager({ + enabled: false, + testmode: PARAMS.tooltiptest, + viewport: PARAMS.viewport, + tooltips: tooltips, + elements: '#reset-to-defaults, button, input', + }); + viewportUpdated.connect(tooltipManager, 'updateViewport'); + // tooltips aren't needed right away, so defer initializing for better page load times + window.setTimeout(tooltipManager.initialize.bind(tooltipManager), 1000); +} + +// helper for instrumenting local jquery onchange handlers +function monitorSettings(options) { + return Object.keys(options).reduce(function(out, id) { + var key = bridgedSettings.resolve(id), + domId = jquerySettings.getId(key, true); + + if (!domId) { + var placeholder = { + id: id, + type: 'placeholder', + toString: function() { + return this.id; + }, + value: undefined, + }; + jquerySettings.registerSetting(placeholder, key); + debugPrint('registered placeholder value for setting', id, key); + assert(jquerySettings.findNodeByKey(key) === placeholder); + } + + // if (domId === 'toggle-advanced-options') alert([key,id,domId, jquerySettings.findNodeByKey(key)]) + assert(function assertion(){ + return typeof key === 'string'; + }, 'monitorSettings -- received invalid key type'); + + var context = { + id: id, + key: key, + domId: domId, + options: options, + lastValue: undefined, + initializer: function(hint) { + var key = this.key, + lastValue = this.lastValue; + if (lastValue !== undefined) { + return log('skipping repeat initializer', key, hint); + } + this.lastValue = lastValue = jquerySettings.getValue(key); + this._onChange.call(jquerySettings, key, lastValue, undefined, hint); + }, + _onChange: function _onChange(key, value) { + var currentValue = this.getValue(context.id), + jsonCurrentValue = JSON.stringify(currentValue); + + if (jsonCurrentValue === context.jsonLastValue) { + if (jsonCurrentValue !== undefined) { + debugPrint([context.key, '_onChange', this, 'not triggering _onChange for duplicated value']); + } + return; + } + context.jsonLastValue = jsonCurrentValue; + var args = [].slice.call(arguments, 0); + debugPrint('monitorSetting._onChange', context.key, value, [].concat(args).pop()); + context.options[context.id].apply(this, [ currentValue ].concat(args)); + }, + + onValueReceived: function(key) { + if (key === this.key) { + this._onChange.apply(bridgedSettings, arguments); + } + }, + onMutationEvent: function(event) { + if (event.key === this.key) { + context._onChange.call(jquerySettings, event.key, event.value, event.oldValue, event.hifiType+':mutation'); + } + }, + onPendingRequestsFinished: function onPendingRequestsFinished() { + bridgedSettings.pendingRequestsFinished.disconnect(this, 'onPendingRequestsFinished'); + this.initializer('pendingRequestsFinished'); + }, + }; + + bridgedSettings.valueReceived.connect(context, 'onValueReceived'); + jquerySettings.mutationEvent.connect(context, 'onMutationEvent'); + + if (bridgedSettings.pendingRequestCount()) { + bridgedSettings.pendingRequestsFinished.connect(context, 'onPendingRequestsFinished'); + } else { + window.setTimeout(context.initializer.bind(context, 'monitorSettings init'), 1); + } + return context; + }, {}); +} + +function initializeDOM() { + + Object.defineProperty(window, 'active', { + get: function() { + return window._active; + }, + set: function(nv) { + nv = !!nv; + window._active = nv; + debugPrint('window.active == ' + nv); + if (!nv) { + document.activeElement && document.activeElement.blur(); + document.body.focus(); + tooltipManager && tooltipManager.disable(); + debugPrint('TOOLTIPS DISABLED'); + } else if (tooltipManager && bridgedSettings) { + if (bridgedSettings.getValue('ui-enable-tooltips')){ + tooltipManager.enable(); + debugPrint('TOOLTIPS RE-ENABLED'); + } + } + $('body').toggleClass('active', window._active); + }, + }); + $('body').toggleClass('active', window._active = true); + + function checkAnim(evt) { + if (!checkAnim.disabled) { + if ($('.scrollable').is(':animated')) { + $('.scrollable').stop(); + log(evt.type, 'stop animation'); + } + } + } + viewportUpdated = signal(function viewportUpdated(viewport) {}); + function triggerViewportUpdate() { + var viewport = { + inner: { width: innerWidth, height: innerHeight }, + client: { + width: document.body.clientWidth || window.innerWidth, + height: document.body.clientHeight || window.innerHeight, + }, + min: { width: window.innerWidth / 3, height: 32 }, + max: { width: window.innerWidth * 7/8, height: window.innerHeight * 7/8 }, + }; + debugPrint('viewportUpdated', viewport); + PARAMS.viewport = Object.assign(PARAMS.viewport||{}, viewport); + viewportUpdated(viewport, triggerViewportUpdate.lastViewport); + triggerViewportUpdate.lastViewport = viewport; + } + $(window).on({ + resize: function resize() { + window.clearTimeout(resize.to); + resize.to = window.setTimeout(triggerViewportUpdate, 100); + }, + mousedown: checkAnim, mouseup: checkAnim, scroll: checkAnim, wheel: checkAnim, + blur: function() { + log('** BLUR ** '); + $('body').addClass('window-blurred'); + document.body.focus(); + document.activeElement && document.activeElement.blur(); + // tooltipManager.closeAll(); + }, + focus: function() { + log('** FOCUS **'); + $('body').removeClass('window-blurred'); + }, + }); +} + +function setupMousetrapKeys() { + if (!window.Mousetrap) { + return log('WARNING: window.Mousetrap not found; not configurating keybindings'); + } + mousetrapMultiBind({ + 'ctrl+a, option+a': function global(evt, combo) { + $(document.activeElement).filter('input').select(); + }, + 'enter': function global(evt, combo) { + var node = document.activeElement; + if ($(node).is('input')) { + log('enter on input element'); + tooltipManager.closeAll(); + node.blur(); + var nexts = $('[tabindex],input,:focusable').not('[tabindex=-1],.ui-slider-handle'); + nexts.add(nexts.find('input')); + nexts = nexts.toArray(); + if (~nexts.indexOf(node)) { + var nextActive = nexts[nexts.indexOf(node)+1]; + log('setting focus to', nextActive); + $(nextActive).focus(); + } else { + log('could not deduce next tabbable element', nexts.length, this); + } + } + return true; + }, + 'ctrl+w': bridgedSettings.sendEvent.bind(bridgedSettings, { method: 'window.close' }), + 'r': location.reload.bind(location), + 'space': function global(evt, combo) { + log('SPACE', evt.target, document.activeElement); + $(document.activeElement).filter('.row').find(':ui-hifiCheckbox,:ui-hifiRadioButton').click(); + if (!$(document.activeElement).is('input,.ui-widget')) { + return false; + } + return true; + }, + }); + // $('input').addClass('mousetrap'); + function mousetrapMultiBind(a, b) { + var obj = typeof a === 'object' ? a : + Object.defineProperty({}, a, {enumerable: true, value: b }); + Object.keys(obj).forEach(function(key) { + var method = obj[key].name === 'global' ? 'bindGlobal' : 'bind'; + key.split(/\s*,\s*/).forEach(function(combo) { + debugPrint('Mousetrap', method, combo, typeof obj[key]); + Mousetrap[method](combo, function(evt, combo) { + debugPrint('Mousetrap', method, combo); + return obj[key].apply(this, arguments); + }); + }); + }); + } +} + +// support the URL having a #node-id (or #debug=1&node-id) hash fragment jumping to that element +function jumpToAnchor(id) { + id = JSON.stringify(id); + $('[id='+id+'],[name='+id+']').first().each(function() { + log('jumpToAnchor', id); + $(this).show(); + this.scrollIntoView({ behavior: 'smooth' }); + }); +}; diff --git a/unpublishedScripts/marketplace/camera-move/avatar-updater.js b/unpublishedScripts/marketplace/camera-move/avatar-updater.js new file mode 100644 index 0000000000..5a91a0e8a8 --- /dev/null +++ b/unpublishedScripts/marketplace/camera-move/avatar-updater.js @@ -0,0 +1,268 @@ +/* eslint-env commonjs */ +// ---------------------------------------------------------------------------- + +// helper module that performs the avatar movement update calculations + +module.exports = AvatarUpdater; + +var _utils = require('./modules/_utils.js'), + assert = _utils.assert; + +var movementUtils = require('./modules/movement-utils.js'); + +function AvatarUpdater(options) { + options = options || {}; + assert(function assertion() { + return typeof options.getCameraMovementSettings === 'function' && + typeof options.getMovementState === 'function' && + options.globalState; + }); + + var DEFAULT_MOTOR_TIMESCALE = 1e6; // a large value that matches Interface's default + var EASED_MOTOR_TIMESCALE = 0.01; // a small value to make Interface quickly apply MyAvatar.motorVelocity + var EASED_MOTOR_THRESHOLD = 0.1; // above this speed (m/s) EASED_MOTOR_TIMESCALE is used + var ACCELERATION_MULTIPLIERS = { translation: 1, rotation: 1, zoom: 1 }; + var STAYGROUNDED_PITCH_THRESHOLD = 45.0; // degrees; ground level is maintained when pitch is within this threshold + var MIN_DELTA_TIME = 0.0001; // to avoid math overflow, never consider dt less than this value + var DEG_TO_RAD = Math.PI / 180.0; + update.frameCount = 0; + update.endTime = update.windowStartTime = _utils.getRuntimeSeconds(); + update.windowFrame = update.windowStartFrame= 0; + + this.update = update; + this.options = options; + this._resetMyAvatarMotor = _resetMyAvatarMotor; + this._applyDirectPitchYaw = _applyDirectPitchYaw; + + var globalState = options.globalState; + var getCameraMovementSettings = options.getCameraMovementSettings; + var getMovementState = options.getMovementState; + var _debugChannel = options.debugChannel; + function update(dt) { + update.frameCount++; + var startTime = _utils.getRuntimeSeconds(); + var settings = getCameraMovementSettings(), + EPSILON = settings.epsilon; + + var independentCamera = Camera.mode === 'independent', + headPitch = MyAvatar.headPitch; + + var actualDeltaTime = startTime - update.endTime, + practicalDeltaTime = Math.max(MIN_DELTA_TIME, actualDeltaTime), + deltaTime; + + if (settings.useConstantDeltaTime) { + deltaTime = settings.threadMode === movementUtils.CameraControls.ANIMATION_FRAME ? + (1 / settings.fps) : (1 / 90); + } else if (settings.threadMode === movementUtils.CameraControls.SCRIPT_UPDATE) { + deltaTime = dt; + } else { + deltaTime = practicalDeltaTime; + } + + var orientationProperty = settings.useHead ? 'headOrientation' : 'orientation', + currentOrientation = independentCamera ? Camera.orientation : MyAvatar[orientationProperty], + currentPosition = MyAvatar.position; + + var previousValues = globalState.previousValues, + pendingChanges = globalState.pendingChanges, + currentVelocities = globalState.currentVelocities; + + var movementState = getMovementState({ update: deltaTime }), + targetState = movementUtils.applyEasing(deltaTime, 'easeIn', settings, movementState, ACCELERATION_MULTIPLIERS), + dragState = movementUtils.applyEasing(deltaTime, 'easeOut', settings, currentVelocities, ACCELERATION_MULTIPLIERS); + + currentVelocities.integrate(targetState, currentVelocities, dragState, settings); + + var currentSpeed = Vec3.length(currentVelocities.translation), + targetSpeed = Vec3.length(movementState.translation), + verticalHold = movementState.isGrounded && settings.stayGrounded && Math.abs(headPitch) < STAYGROUNDED_PITCH_THRESHOLD; + + var deltaOrientation = Quat.fromVec3Degrees(Vec3.multiply(deltaTime, currentVelocities.rotation)), + targetOrientation = Quat.normalize(Quat.multiply(currentOrientation, deltaOrientation)); + + var targetVelocity = Vec3.multiplyQbyV(targetOrientation, currentVelocities.translation); + + if (verticalHold) { + targetVelocity.y = 0; + } + + var deltaPosition = Vec3.multiply(deltaTime, targetVelocity); + + _resetMyAvatarMotor(pendingChanges); + + if (!independentCamera) { + var DriveModes = movementUtils.DriveModes; + switch (settings.driveMode) { + case DriveModes.MOTOR: { + if (currentSpeed > EPSILON || targetSpeed > EPSILON) { + var motorTimescale = (currentSpeed > EASED_MOTOR_THRESHOLD ? EASED_MOTOR_TIMESCALE : DEFAULT_MOTOR_TIMESCALE); + var motorPitch = Quat.fromPitchYawRollDegrees(headPitch, 180, 0), + motorVelocity = Vec3.multiplyQbyV(motorPitch, currentVelocities.translation); + if (verticalHold) { + motorVelocity.y = 0; + } + Object.assign(pendingChanges.MyAvatar, { + motorVelocity: motorVelocity, + motorTimescale: motorTimescale + }); + } + break; + } + case DriveModes.THRUST: { + var thrustVector = currentVelocities.translation, + maxThrust = settings.translation.maxVelocity, + thrust; + if (targetSpeed > EPSILON) { + thrust = movementUtils.calculateThrust(maxThrust * 5, thrustVector, previousValues.thrust); + } else if (currentSpeed > 1 && Vec3.length(previousValues.thrust) > 1) { + thrust = Vec3.multiply(-currentSpeed / 10.0, thrustVector); + } else { + thrust = Vec3.ZERO; + } + if (thrust) { + thrust = Vec3.multiplyQbyV(MyAvatar[orientationProperty], thrust); + if (verticalHold) { + thrust.y = 0; + } + } + previousValues.thrust = pendingChanges.MyAvatar.setThrust = thrust; + break; + } + case DriveModes.POSITION: { + pendingChanges.MyAvatar.position = Vec3.sum(currentPosition, deltaPosition); + break; + } + default: { + throw new Error('unknown driveMode: ' + settings.driveMode); + } + } + } + + var finalOrientation; + switch (Camera.mode) { + case 'mirror': // fall through + case 'independent': + targetOrientation = settings.preventRoll ? Quat.cancelOutRoll(targetOrientation) : targetOrientation; + var boomVector = Vec3.multiply(-currentVelocities.zoom.z, Quat.getFront(targetOrientation)), + deltaCameraPosition = Vec3.sum(boomVector, deltaPosition); + Object.assign(pendingChanges.Camera, { + position: Vec3.sum(Camera.position, deltaCameraPosition), + orientation: targetOrientation + }); + break; + case 'entity': + finalOrientation = targetOrientation; + break; + default: // 'first person', 'third person' + finalOrientation = targetOrientation; + break; + } + + if (settings.jitterTest) { + finalOrientation = Quat.multiply(MyAvatar[orientationProperty], Quat.fromPitchYawRollDegrees(0, 60 * deltaTime, 0)); + // Quat.fromPitchYawRollDegrees(0, _utils.getRuntimeSeconds() * 60, 0) + } + + if (finalOrientation) { + if (settings.preventRoll) { + finalOrientation = Quat.cancelOutRoll(finalOrientation); + } + previousValues.finalOrientation = pendingChanges.MyAvatar[orientationProperty] = Quat.normalize(finalOrientation); + } + + if (!movementState.mouseSmooth && movementState.isRightMouseButton) { + // directly apply mouse pitch and yaw when mouse smoothing is disabled + _applyDirectPitchYaw(deltaTime, movementState, settings); + } + + var endTime = _utils.getRuntimeSeconds(); + var cycleTime = endTime - update.endTime; + update.endTime = endTime; + + pendingChanges.submit(); + + if ((endTime - update.windowStartTime) > 3) { + update.momentaryFPS = (update.frameCount - update.windowStartFrame) / + (endTime - update.windowStartTime); + update.windowStartFrame = update.frameCount; + update.windowStartTime = endTime; + } + + if (_debugChannel && update.windowStartFrame === update.frameCount) { + Messages.sendLocalMessage(_debugChannel, JSON.stringify({ + threadFrames: update.threadFrames, + frame: update.frameCount, + threadMode: settings.threadMode, + driveMode: settings.driveMode, + orientationProperty: orientationProperty, + isGrounded: movementState.isGrounded, + targetAnimationFPS: settings.threadMode === movementUtils.CameraControls.ANIMATION_FRAME ? settings.fps : undefined, + actualFPS: 1 / actualDeltaTime, + effectiveAnimationFPS: 1 / deltaTime, + seconds: { + startTime: startTime, + endTime: endTime + }, + milliseconds: { + actualDeltaTime: actualDeltaTime * 1000, + deltaTime: deltaTime * 1000, + cycleTime: cycleTime * 1000, + calculationTime: (endTime - startTime) * 1000 + }, + finalOrientation: finalOrientation, + thrust: thrust, + maxVelocity: settings.translation, + targetVelocity: targetVelocity, + currentSpeed: currentSpeed, + targetSpeed: targetSpeed + }, 0, 2)); + } + } + + function _resetMyAvatarMotor(targetObject) { + if (MyAvatar.motorTimescale !== DEFAULT_MOTOR_TIMESCALE) { + targetObject.MyAvatar.motorTimescale = DEFAULT_MOTOR_TIMESCALE; + } + if (MyAvatar.motorReferenceFrame !== 'avatar') { + targetObject.MyAvatar.motorReferenceFrame = 'avatar'; + } + if (Vec3.length(MyAvatar.motorVelocity)) { + targetObject.MyAvatar.motorVelocity = Vec3.ZERO; + } + } + + function _applyDirectPitchYaw(deltaTime, movementState, settings) { + var orientationProperty = settings.useHead ? 'headOrientation' : 'orientation', + rotation = movementState.rotation, + speed = Vec3.multiply(-DEG_TO_RAD / 2.0, settings.rotation.speed); + + var previousValues = globalState.previousValues, + pendingChanges = globalState.pendingChanges, + currentVelocities = globalState.currentVelocities; + + var previous = previousValues.pitchYawRoll, + target = Vec3.multiply(deltaTime, Vec3.multiplyVbyV(rotation, speed)), + pitchYawRoll = Vec3.mix(previous, target, 0.5), + orientation = Quat.fromVec3Degrees(pitchYawRoll); + + previousValues.pitchYawRoll = pitchYawRoll; + + if (pendingChanges.MyAvatar.headOrientation || pendingChanges.MyAvatar.orientation) { + var newOrientation = Quat.multiply(MyAvatar[orientationProperty], orientation); + delete pendingChanges.MyAvatar.headOrientation; + delete pendingChanges.MyAvatar.orientation; + if (settings.preventRoll) { + newOrientation = Quat.cancelOutRoll(newOrientation); + } + MyAvatar[orientationProperty] = newOrientation; + } else if (pendingChanges.Camera.orientation) { + var cameraOrientation = Quat.multiply(Camera.orientation, orientation); + if (settings.preventRoll) { + cameraOrientation = Quat.cancelOutRoll(cameraOrientation); + } + Camera.orientation = cameraOrientation; + } + currentVelocities.rotation = Vec3.ZERO; + } +} diff --git a/unpublishedScripts/marketplace/camera-move/hifi-jquery-ui.js b/unpublishedScripts/marketplace/camera-move/hifi-jquery-ui.js new file mode 100644 index 0000000000..31f2186ee1 --- /dev/null +++ b/unpublishedScripts/marketplace/camera-move/hifi-jquery-ui.js @@ -0,0 +1,311 @@ +// extended jQuery UI controls + +/* eslint-env console, jquery, browser */ +/* eslint-disable comma-dangle, no-empty */ +/* global assert, log, debugPrint */ +// ---------------------------------------------------------------------------- + +// WIDGET BASE +Object.assign($.Widget.prototype, { + // common bootstrapping across widget types + initHifiControl: function initHifiControl(hifiType) { + initHifiControl.widgetCount = (initHifiControl.widgetCount || 0) + 1; + hifiType = hifiType || this.widgetName; + + var element = this.element, options = this.options, node = element.get(0), dataset = node.dataset; + assert(!this.element.is('.initialized')); + this.element.addClass('initialized'); + var attributes = [].reduce.call(node.attributes, function(out, attribute) { + out[attribute.name] = attribute.value; + return out; + }, {}); + + var searchOrder = [ options, dataset, attributes, node ]; + function setData(key, fallback) { + var value = searchOrder.map(function(obj) { + return obj[key]; + }).concat(fallback).filter(function(value) { + return value !== undefined; + })[0]; + return value === undefined ? null : (dataset[key] = value); + } + options.hifiWidgetId = hifiType + '-' + initHifiControl.widgetCount; + node.id = node.id || options.hifiWidgetId; + dataset.hifiType = hifiType; + setData('type'); + setData('for', node.id); + setData('checked'); + if (setData('value', null) !== null) { + element.attr('value', dataset.value); + } + + return node.id; + }, + hifiFindWidget: function(hifiType, quiet) { + var selector = ':ui-'+hifiType; + var _for = JSON.stringify(this.element.data('for')||undefined), + element = _for && $('[id='+_for+']').filter(selector); + if (!element.is(selector)) { + element = this.element.closest(selector); + } + var instance = element.filter(selector)[hifiType]('instance'); + + if (!instance && !quiet) { + // eslint-disable-next-line no-console + console.error([ + instance, 'could not find target instance ' + selector + + ' for ' + this.element.data('hifi-type') + + ' #' + this.element.prop('id') + ' for=' + this.element.data('for') + ]); + } + return instance; + }, +}); + +// CHECKBOX +$.widget('ui.hifiCheckbox', $.ui.checkboxradio, { + value: function value(nv) { + if (arguments.length) { + var currentValue = this.element.prop('checked'); + if (nv !== currentValue){ + this.element.prop('checked', nv); + this.element.change(); + } + } + return this.element.prop('checked'); + }, + _create: function() { + var id = this.initHifiControl(); + this.element.attr('value', id); + // add an implicit label if missing + var forId = 'for=' + JSON.stringify(id); + var label = $(this.element.get(0)).closest('label').add($('label[' + forId + ']')); + if (!label.get(0)) { + $('').appendTo(this.element); + } + this._super(); + this.element.on('change._hifiCheckbox, click._hifiCheckbox', function() { + var checked = this.value(), + attr = this.element.attr('checked'); + if (checked && !attr) { + this.element.attr('checked', 'checked'); + } else if (!checked && attr) { + this.element.removeAttr('checked'); + } + this.refresh(); + }.bind(this)); + }, +}); + +// BUTTON +$.widget('ui.hifiButton', $.ui.button, { + value: function(nv) { + var dataset = this.element[0].dataset; + if (arguments.length) { + var checked = (dataset.checked === 'true'); + nv = (nv === 'true' || !!nv); + if (nv !== checked) { + debugPrint('hifibutton checked changed', nv, checked); + dataset.checked = nv; + this.element.change(); + } else { + debugPrint('hifibutton value same', nv, checked); + } + } + return dataset.checked === 'true'; + }, + _create: function() { + this.element.data('type', 'checkbox'); + this.initHifiControl(); + this._super(); + this.element[0].dataset.checked = !!this.element.attr('checked'); + var _for = this.element.data('for') || undefined; + if (_for && _for !== this.element[0].id) { + _for = JSON.stringify(_for); + var checkbox = this.hifiFindWidget('hifiCheckbox', true); + if (!checkbox) { + var input = $('').hide(); + input.appendTo(this.element); + checkbox = input.find('input') + .hifiCheckbox() + .hifiCheckbox('instance'); + } + this.element.find('.tooltip-target').removeClass('tooltip-target'); + this.element.prop('id', 'button-'+this.element.prop('id')); + checkbox.element.on('change._hifiButton', function() { + debugPrint('checkbox -> button'); + this.value(checkbox.value()); + }.bind(this)); + this.element.on('change', function() { + debugPrint('button -> checkbox'); + checkbox.value(this.value()); + }.bind(this)); + this.checkbox = checkbox; + } + }, +}); + +// RADIO BUTTON +$.widget('ui.hifiRadioButton', $.ui.checkboxradio, { + value: function value(nv) { + if (arguments.length) { + this.element.prop('checked', !!nv); + this.element.change(); + } + return this.element.prop('checked'); + }, + _create: function() { + var id = this.initHifiControl(); + this.element.attr('value', this.element.data('value') || id); + // console.log(this.element[0]); + assert(this.element.data('for')); + this._super(); + + this.element.on('change._hifiRadioButton, click._hifiRadioButton', function() { + var group = this.hifiFindWidget('hifiRadioGroup'), + checked = !!this.element.attr('checked'), + dotchecked = this.element.prop('checked'), + value = this.element.attr('value'); + + if (dotchecked !== checked || group.value() !== value) { + if (dotchecked && group.value() !== value) { + log(value, 'UPDATING GRUOP', group.element[0].id); + group.value(value); + } + } + }.bind(this)); + }, +}); + +// RADIO GROUP +$.widget('ui.hifiRadioGroup', $.ui.controlgroup, { + radio: function(selector) { + return this.element.find(':ui-hifiRadioButton' + selector).hifiRadioButton('instance'); + }, + refresh: function() { + var value = this.value(); + this.element.find(':ui-hifiRadioButton').each(function() { + $(this).prop('checked', $(this).attr('value') === value).hifiRadioButton('refresh'); + }); + this._super(); + }, + value: function value(nv) { + if (arguments.length) { + var id = this.element[0].id, + previous = this.value(); + debugPrint('RADIOBUTTON GROUP value', id + ' = ' + nv + '(was: ' + previous + ')'); + this.element.attr('value', nv); + this.refresh(); + } + return this.element.attr('value'); + }, + _create: function(x) { + debugPrint('ui.hifiRadioGroup._create', this.element[0]); + this.initHifiControl(); + this.options.items = { + hifiRadioButton: 'input[type=radio]', + }; + this._super(); + // allow setting correct radio button by assign to .value property (or $.fn.val() etc.) + Object.defineProperty(this.element[0], 'value', { + set: function(nv) { + try { + this.radio('#' + nv).value(true); + } catch (e) {} + return this.value(); + }.bind(this), + get: function() { + return this.element.attr('value'); + }.bind(this), + }); + }, +}); + +// SPINNER (numeric input + up/down buttons) +$.widget('ui.hifiSpinner', $.ui.spinner, { + value: function value(nv) { + if (arguments.length) { + var num = parseFloat(nv); + debugPrint('ui.hifiSpinner.value set', this.element[0].id, num, '(was: ' + this.value() + ')', 'raw:'+nv); + this._value(num); + this.element.change(); + } + return parseFloat(this.element.val()); + }, + _value: function(value, allowAny) { + debugPrint('ui.hifiSpinner._value', value, allowAny); + return this._super(value, allowAny); + }, + _create: function() { + this.initHifiControl(); + var step = this.options.step = this.options.step || 1.0; + // allow step=".01" for precision and data-step=".1" for default increment amount + this.options.prescale = parseFloat(this.element.data('step') || step) / (step); + this._super(); + this.previous = null; + this.element.on('change._hifiSpinner', function() { + var value = this.value(), + invalid = !this.isValid(); + debugPrint('hifiSpinner.changed', value, invalid ? '!!!invalid' : 'valid'); + !invalid && this.element.attr('value', value); + }.bind(this)); + }, + _spin: function( step, event ) { + step = step * this.options.prescale * ( + event.shiftKey ? 0.1 : event.ctrlKey ? 10 : 1 + ); + return this._super( step, event ); + }, + _stop: function( event, ui ) { + try { + return this._super(event, ui); + } finally { + if (/mouse/.test(event && event.type)) { + var value = this.value(); + if ((value || value === 0) && !isNaN(value) && this.previous !== null && this.previous !== value) { + this.value(this.value()); + } + this.previous = value; + } + } + }, + _format: function(n) { + var precision = this._precision(); + return parseFloat(n).toFixed(precision); + }, + _events: { + mousewheel: function(event, delta) { + if (document.activeElement === this.element[0]) { + // fix broken mousewheel on Chrome / embedded webkit + delta = delta === undefined ? -(event.originalEvent.deltaY+event.originalEvent.deltaX) : delta; + $.ui.spinner.prototype._events.mousewheel.call(this, event, delta); + } + } + } +}); + +// SLIDER +$.widget('ui.hifiSlider', $.ui.slider, { + value: function value(nv) { + if (arguments.length) { + var num = this._trimAlignValue(nv); + debugPrint('hifiSlider.value', nv, num); + if (this.options.value !== num) { + this.options.value = num; + this.element.change(); + } + } + return this.options.value; + }, + _create: function() { + this.initHifiControl(); + this._super(); + this.element + .attr('type', this.element.attr('type') || 'slider') + .find('.ui-slider-handle').html('
').end() + .on('change', function() { + this.hifiFindWidget('hifiSpinner').value(this.value()); + this._refresh(); + }.bind(this)); + }, +}); diff --git a/unpublishedScripts/marketplace/camera-move/modules/EnumMeta.js b/unpublishedScripts/marketplace/camera-move/modules/EnumMeta.js new file mode 100644 index 0000000000..e463442c05 --- /dev/null +++ b/unpublishedScripts/marketplace/camera-move/modules/EnumMeta.js @@ -0,0 +1,238 @@ +// EnumMeta.js -- helper module that maps related enum values to printable names and ids + +/* eslint-env commonjs */ +/* global DriveKeys, console */ + +var VERSION = '0.0.1'; +var WANT_DEBUG = false; + +function _debugPrint() { + // eslint-disable-next-line no-console + (typeof Script === 'object' ? print : console.log)('EnumMeta | ' + [].slice.call(arguments).join(' ')); +} + +var debugPrint = WANT_DEBUG ? _debugPrint : function(){}; + +try { + /* global process */ + if (process.title === 'node') { + _defineNodeJSMocks(); + } +} catch (e) { /* eslint-disable-line empty-block */ } + +_debugPrint(VERSION); + +// FIXME: C++ emits this action event, but doesn't expose it yet to scripting +// (ie: as Actions.ZOOM or Actions.TranslateCameraZ) +var ACTION_TRANSLATE_CAMERA_Z = { + actionName: 'TranslateCameraZ', + actionID: 12, + driveKeyName: 'ZOOM' +}; + +module.exports = { + version: VERSION, + DriveKeyNames: invertKeys(DriveKeys), + Controller: { + Hardware: Object.keys(Controller.Hardware).reduce(function(names, prop) { + names[prop+'Names'] = invertKeys(Controller.Hardware[prop]); + return names; + }, {}), + ActionNames: _getActionNames(), + StandardNames: invertKeys(Controller.Standard) + }, + getDriveKeyNameFromActionName: getDriveKeyNameFromActionName, + getActionNameFromDriveKeyName: getActionNameFromDriveKeyName, + eventKeyText2KeyboardName: eventKeyText2KeyboardName, + keyboardName2eventKeyText: keyboardName2eventKeyText, + ACTION_TRANSLATE_CAMERA_Z: ACTION_TRANSLATE_CAMERA_Z, + INVALID_ACTION_ID: Controller.findAction('INVALID_ACTION_ID_FOO') +}; + +_debugPrint('///'+VERSION, Object.keys(module.exports)); + +var actionsMapping = {}, driveKeyMapping = {}; + +initializeMappings(actionsMapping, driveKeyMapping); + +function invertKeys(object) { + if (!object) { + return object; + } + return Object.keys(object).reduce(function(out, key) { + out[object[key]] = key; + return out; + }, {}); +} + +function _getActionNames() { + var ActionNames = invertKeys(Controller.Hardware.Actions); + ActionNames[ACTION_TRANSLATE_CAMERA_Z.actionID] = ACTION_TRANSLATE_CAMERA_Z.actionName; + function mapActionName(actionName) { + var actionKey = Controller.Hardware.Actions[actionName], + actionID = Controller.findAction(actionName), + similarName = eventKeyText2KeyboardName(actionName), + existingName = ActionNames[actionID]; + + var keyName = actionName; + + if (actionID === module.exports.INVALID_ACTION_ID) { + _debugPrint('actionID === INVALID_ACTION_ID', actionName); + } + switch (actionName) { + case 'StepTranslateX': actionName = 'StepTranslate'; break; + case 'StepTranslateY': actionName = 'StepTranslate'; break; + case 'StepTranslateZ': actionName = 'StepTranslate'; break; + case 'ACTION1': actionName = 'PrimaryAction'; break; + case 'ACTION2': actionName = 'SecondaryAction'; break; + } + debugPrint(keyName, actionName, actionKey, actionID); + + similarName = similarName.replace('Lateral','Strafe').replace(/^(?:Longitudinal|Vertical)/, ''); + if (actionID in ActionNames) { + // check if overlap is just BoomIn <=> BOOM_IN + if (similarName !== existingName && actionName !== existingName) { + throw new Error('assumption failed: overlapping actionID:'+JSON.stringify({ + actionID: actionID, + actionKey: actionKey, + actionName: actionName, + similarName: similarName, + keyName: keyName, + existingName: existingName + },0,2)); + } + } else { + ActionNames[actionID] = actionName; + ActionNames[actionKey] = keyName; + } + } + // first map non-legacy (eg: Up and not VERTICAL_UP) actions + Object.keys(Controller.Hardware.Actions).filter(function(name) { + return /[a-z]/.test(name); + }).sort().reverse().forEach(mapActionName); + // now legacy actions + Object.keys(Controller.Hardware.Actions).filter(function(name) { + return !/[a-z]/.test(name); + }).sort().reverse().forEach(mapActionName); + + return ActionNames; +} + +// attempts to brute-force translate an Action name into a DriveKey name +// eg: _translateActionName('TranslateX') === 'TRANSLATE_X' +// eg: _translateActionName('Yaw') === 'YAW' +function _translateActionName(name, _index) { + name = name || ''; + var key = name; + var re = new RegExp('[A-Z][a-z0-9]+', 'g'); + key = key.replace(re, function(Word) { + return Word.toUpperCase()+'_'; + }) + .replace(/_$/, ''); + + if (key in DriveKeys) { + debugPrint('getDriveKeyFromEventName', _index, name, key, DriveKeys[key]); + return key; + } +} + +function getActionNameFromDriveKeyName(driveKeyName) { + return driveKeyMapping[driveKeyName]; +} +// maps an action lookup value to a DriveKey name +// eg: actionName: 'Yaw' === 'YAW' +// actionKey: Controller.Actions.Yaw => 'YAW' +// actionID: Controller.findAction('Yaw') => 'YAW' +function getDriveKeyNameFromActionName(lookupValue) { + if (lookupValue === ACTION_TRANSLATE_CAMERA_Z.actionName || + lookupValue === ACTION_TRANSLATE_CAMERA_Z.actionID) { + return ACTION_TRANSLATE_CAMERA_Z.driveKeyName; + } + if (typeof lookupValue === 'string') { + lookupValue = Controller.findAction(lookupValue); + } + return actionsMapping[lookupValue]; +} + +// maps a Controller.key*Event event.text -> Controller.Hardware.Keyboard[name] +// eg: ('Page Up') === 'PgUp' +// eg: ('LEFT') === 'Left' +function eventKeyText2KeyboardName(text) { + if (eventKeyText2KeyboardName[text]) { + // use memoized value + return eventKeyText2KeyboardName[text]; + } + var keyboardName = (text||'').toUpperCase().split(/[ _]/).map(function(WORD) { + return WORD.replace(/([A-Z])(\w*)/g, function(_, A, b) { + return (A.toUpperCase() + b.toLowerCase()); + }); + }).join('').replace('Page','Pg'); + return eventKeyText2KeyboardName[text] = eventKeyText2KeyboardName[keyboardName] = keyboardName; +} + +// maps a Controller.Hardware.Keyboard[name] -> Controller.key*Event event.text +// eg: ('PgUp') === 'PAGE UP' +// eg: ('Shift') === 'SHIFT' +function keyboardName2eventKeyText(keyName) { + if (keyboardName2eventKeyText[keyName]) { + // use memoized value + return keyboardName2eventKeyText[keyName]; + } + var text = keyName.replace('Pg', 'Page'); + var caseWords = text.match(/[A-Z][a-z0-9]+/g) || [ text ]; + var eventText = caseWords.map(function(str) { + return str.toUpperCase(); + }).join('_'); + return keyboardName2eventKeyText[keyName] = eventText; +} + +function initializeMappings(actionMap, driveKeyMap) { + _debugPrint('creating mapping'); + var ref = ACTION_TRANSLATE_CAMERA_Z; + actionMap[ref.actionName] = actionMap[ref.actionID] = ref.driveKeyName; + actionMap.BoomIn = 'ZOOM'; + actionMap.BoomOut = 'ZOOM'; + + Controller.getActionNames().sort().reduce( + function(out, name, index, arr) { + var actionKey = arr[index]; + var actionID = Controller.findAction(name); + var value = actionID in out ? out[actionID] : _translateActionName(name, index); + if (value !== undefined) { + var prefix = (actionID in out ? '+++' : '---'); + debugPrint(prefix + ' Action2DriveKeyName['+name+'('+actionID+')] = ' + value); + driveKeyMap[value] = driveKeyMap[value] || name; + } + out[name] = out[actionID] = out[actionKey] = value; + return out; + }, actionMap); +} + +// ---------------------------------------------------------------------------- +// mocks for litmus testing using Node.js command line tools +function _defineNodeJSMocks() { + /* eslint-disable no-global-assign */ + DriveKeys = { + TRANSLATE_X: 12345 + }; + Controller = { + getActionNames: function() { + return Object.keys(this.Hardware.Actions); + }, + findAction: function(name) { + return this.Hardware.Actions[name] || 4095; + }, + Hardware: { + Actions: { + TranslateX: 54321 + }, + Application: { + Grounded: 1 + }, + Keyboard: { + A: 65 + } + } + }; + /* eslint-enable no-global-assign */ +} diff --git a/unpublishedScripts/marketplace/camera-move/modules/_utils.js b/unpublishedScripts/marketplace/camera-move/modules/_utils.js new file mode 100644 index 0000000000..bf0f09aac8 --- /dev/null +++ b/unpublishedScripts/marketplace/camera-move/modules/_utils.js @@ -0,0 +1,457 @@ +// _utils.js -- misc. helper classes/functions + +"use strict"; +/* eslint-env commonjs, hifi */ +/* eslint-disable comma-dangle, no-empty */ +/* global HIRES_CLOCK, Desktop, OverlayWebWindow */ +// var HIRES_CLOCK = (typeof Window === 'object' && Window && Window.performance) && Window.performance.now; +var USE_HIRES_CLOCK = typeof HIRES_CLOCK === 'function'; + +var exports = { + version: '0.0.1c' + (USE_HIRES_CLOCK ? '-hires' : ''), + bind: bind, + signal: signal, + assign: assign, + sortedAssign: sortedAssign, + sign: sign, + assert: assert, + makeDebugRequire: makeDebugRequire, + DeferredUpdater: DeferredUpdater, + KeyListener: KeyListener, + getRuntimeSeconds: getRuntimeSeconds, + createAnimationStepper: createAnimationStepper, + reloadClientScript: reloadClientScript, + + normalizeStackTrace: normalizeStackTrace, + BrowserUtils: BrowserUtils, +}; +try { + module.exports = exports; // Interface / Node.js +} catch (e) { + this._utils = assign(this._utils || {}, exports); // browser +} + +// ---------------------------------------------------------------------------- +function makeDebugRequire(relativeTo) { + return function boundDebugRequire(id) { + return debugRequire(id, relativeTo); + }; +} +function debugRequire(id, relativeTo) { + if (typeof Script === 'object') { + relativeTo = (relativeTo||Script.resolvePath('.')).replace(/\/+$/, ''); + // hack-around for use during local development / testing that forces every require to re-fetch the script from the server + var modulePath = Script._requireResolve(id, relativeTo+'/') + '?' + new Date().getTime().toString(36); + print('========== DEBUGREQUIRE:' + modulePath); + Script.require.cache[modulePath] = Script.require.cache[id] = undefined; + Script.require.__qt_data__[modulePath] = Script.require.__qt_data__[id] = true; + return Script.require(modulePath); + } else { + return require(id); + } +} + +// examples: +// assert(function assertion() { return (conditions === true) }, 'assertion failed!') +// var neededValue = assert(idString, 'idString not specified!'); +// assert(false, 'unexpected state'); +function assert(truthy, message) { + message = message || 'Assertion Failed:'; + + if (typeof truthy === 'function' && truthy.name === 'assertion') { + // extract function body to display with the assertion message + var assertion = (truthy+'').replace(/[\r\n]/g, ' ') + .replace(/^[^{]+\{|\}$|^\s*|\s*$/g, '').trim() + .replace(/^return /,'').replace(/\s[\r\n\t\s]+/g, ' '); + message += ' ' + JSON.stringify(assertion); + try { + truthy = truthy(); + } catch (e) { + message += '(exception: ' + e +')'; + } + } + if (!truthy) { + message += ' ('+truthy+')'; + throw new Error(message); + } + return truthy; +} + + +// ---------------------------------------------------------------------------- +function sign(x) { + x = +x; + if (x === 0 || isNaN(x)) { + return Number(x); + } + return x > 0 ? 1 : -1; +} +// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign#Polyfill +/* eslint-disable */ +function assign(target, varArgs) { // .length of function is 2 + 'use strict'; + if (target == null) { // TypeError if undefined or 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) { // Skip over if undefined or null + for (var nextKey in nextSource) { + // Avoid bugs when hasOwnProperty is shadowed + if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) { + to[nextKey] = nextSource[nextKey]; + } + } + } + } + return to; +} +/* eslint-enable */ +// //https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign#Polyfill + +// hack to sort keys in v8 for prettier JSON exports +function sortedAssign(target, sources) { + var allParams = assign.apply(this, [{}].concat([].slice.call(arguments))); + for (var p in target) { + delete target[p]; + } + Object.keys(allParams).sort(function(a,b) { + function swapCase(ch) { + return /[A-Z]/.test(ch) ? ch.toLowerCase() : ch.toUpperCase(); + } + a = a.replace(/^./, swapCase); + b = b.replace(/^./, swapCase); + a = /Version/.test(a) ? 'AAAA'+a : a; + b = /Version/.test(b) ? 'AAAA'+b : b; + return a < b ? -1 : a > b ? 1 : 0; + }).forEach(function(key) { + target[key] = allParams[key]; + }); + return target; +} + +// ---------------------------------------------------------------------------- +// @function - bind a function to a `this` context +// @param {Object} - the `this` context +// @param {Function|String} - function or method name +bind.debug = true; +function bind(thiz, method) { + var methodName = typeof method === 'string' ? method : method.name; + method = thiz[method] || method; + if (bind.debug && methodName) { + methodName = methodName.replace(/[^A-Za-z0-9_$]/g, '_'); + var debug = {}; + debug[methodName] = method; + return eval('1,function bound'+methodName+'() { return debug.'+methodName+'.apply(thiz, arguments); }'); + } + return function() { + return method.apply(thiz, arguments); + }; +} + +// @function - Qt signal polyfill +function signal(template) { + var callbacks = []; + return Object.defineProperties(function() { + var args = [].slice.call(arguments); + callbacks.forEach(function(obj) { + obj.handler.apply(obj.scope, args); + }); + }, { + connect: { value: function(scope, handler) { + callbacks.push({scope: scope, handler: scope[handler] || handler || scope}); + }}, + disconnect: { value: function(scope, handler) { + var match = {scope: scope, handler: scope[handler] || handler || scope}; + callbacks = callbacks.filter(function(obj) { + return !(obj.scope === match.scope && obj.handler === match.handler); + }); + }} + }); +} +// ---------------------------------------------------------------------------- +function normalizeStackTrace(err, options) { + options = options || {}; + // * Chromium: " at (file://.../filename.js:45:65)" + // * Interface: " at filename.js:45" + var output = err.stack ? err.stack.replace( + /((?:https?|file):[/][/].*?):(\d+)(?::\d+)?([)]|\s|$)/g, + function(_, url, lineNumber, suffix) { + var fileref = url.split(/[?#]/)[0].split('/').pop(); + if (Array.isArray(options.wrapFilesWith)) { + fileref = options.wrapFilesWith[0] + fileref + options.wrapFilesWith[1]; + } + if (Array.isArray(options.wrapLineNumbersWith)) { + lineNumber = options.wrapLineNumbersWith[0] + lineNumber + options.wrapLineNumbersWith[1]; + } + return fileref + ':' + lineNumber + suffix; + } + ).replace(/[(]([-\w.%:]+[.](?:html|js))[)]/g, '$1') : err.message; + return ' '+output; +} + +// utilities specific to use from web browsers / embedded Interface web windows +function BrowserUtils(global) { + global = global || (1,eval)('this'); + return { + global: global, + console: global.console, + log: function(msg) { return this.console.log('browserUtils | ' + [].slice.call(arguments).join(' ')); }, + makeConsoleWorkRight: function(console, forcePatching) { + if (console.$patched || !(forcePatching || global.qt)) { + return console; + } + var patched = ['log','debug','info','warn','error'].reduce(function(output, method) { + output[method] = function() { + return console[method]([].slice.call(arguments).join(' ')); + }; + return output; + }, { $patched: console }); + for (var p in console) { + if (typeof console[p] === 'function' && !(p in patched)) { + patched[p] = console[p].bind(console); + } + } + patched.__proto__ = console; // let scope chain find constants and other non-function values + return patched; + }, + parseQueryParams: function(querystring) { + return this.extendWithQueryParams({}, querystring); + }, + extendWithQueryParams: function(obj, querystring) { + querystring = querystring || global.location.href; + querystring.replace(/\b(\w+)=([^&?#]+)/g, function(_, key, value) { + value = unescape(value); + obj[key] = value; + }); + return obj; + }, + // openEventBridge handles the cluster of scenarios Interface has imposed on webviews for making EventBridge connections + openEventBridge: function openEventBridge(callback) { + this.log('openEventBridge |', 'typeof global.EventBridge == ' + [typeof global.EventBridge, global.EventBridge ]); + var error; + try { + global.EventBridge.toString = function() { return '[global.EventBridge at startup]'; }; + global.EventBridge.scriptEventReceived.connect.exists; + // this.log('openEventBridge| EventBridge already exists... -- invoking callback', 'typeof EventBridge == ' + typeof global.EventBridge); + try { + return callback(global.EventBridge); + } catch(e) { + error = e; + } + } catch (e) { + this.log('EventBridge not found in a usable state -- attempting to instrument via qt.webChannelTransport', + Object.keys(global.EventBridge||{})); + var QWebChannel = assert(global.QWebChannel, 'expected global.QWebChannel to exist'), + qt = assert(global.qt, 'expected global.qt to exist'); + assert(qt.webChannelTransport, 'expected global.qt.webChannelTransport to exist'); + new QWebChannel(qt.webChannelTransport, bind(this, function (channel) { + var objects = channel.objects; + if (global.EventBridge) { + log('>>> global.EventBridge was unavailable at page load, but has spontaneously materialized; ' + + [ typeof global.EventBridge, global.EventBridge ]); + } + var eventBridge = objects.eventBridge || (objects.eventBridgeWrapper && objects.eventBridgeWrapper.eventBridge); + eventBridge.toString = function() { return '[window.EventBridge per QWebChannel]'; }; + assert(!global.EventBridge || global.EventBridge === eventBridge, 'global.EventBridge !== QWebChannel eventBridge\n' + + [global.EventBridge, eventBridge]); + global.EventBridge = eventBridge; + global.EventBridge.$WebChannel = channel; + this.log('openEventBridge opened -- invoking callback', 'typeof EventBridge === ' + typeof global.EventBridge); + callback(global.EventBridge); + })); + } + if (error) { + throw error; + } + }, + }; +} +// ---------------------------------------------------------------------------- +// queue property/method updates to target so that they can be applied all-at-once +function DeferredUpdater(target, options) { + options = options || {}; + // define _meta as a non-enumerable (so it doesn't show up in for (var p in ...) loops) + Object.defineProperty(this, '_meta', { enumerable: false, value: { + target: target, + lastValue: {}, + dedupe: options.dedupe, + }}); +} +DeferredUpdater.prototype = { + reset: function() { + var self = this; + Object.keys(this).forEach(function(property) { + delete self[property]; + }); + this._meta.lastValue = {}; + }, + submit: function() { + var meta = this._meta, + target = meta.target, + self = this, + submitted = {}; + self.submit = getRuntimeSeconds(); + Object.keys(self).forEach(function(property) { + var newValue = self[property]; + submitted[property] = newValue; + if (typeof target[property] === 'function') { + target[property](newValue); + } else { + target[property] = newValue; + } + delete self[property]; + }); + return submitted; + } +}; +// create a group of deferred updaters eg: DeferredUpdater.createGroup({ MyAvatar: MyAvatar, Camera: Camera }) +DeferredUpdater.createGroup = function(items, options) { + var result = { + __proto__: { + reset: function() { + Object.keys(this).forEach(bind(this, function(item) { + this[item].reset(); + })); + }, + submit: function() { + var submitted = {}; + Object.keys(this).forEach(bind(this, function(item) { + submitted[item] = this[item].submit(); + })); + return submitted; + } + } + }; + Object.keys(items).forEach(function(item) { + result[item] = new DeferredUpdater(items[item], options); + }); + return result; +}; + +// ---------------------------------------------------------------------------- + +// session runtime in seconds +getRuntimeSeconds.EPOCH = getRuntimeSeconds(0); +function getRuntimeSeconds(since) { + since = since === undefined ? getRuntimeSeconds.EPOCH : since; + var now = USE_HIRES_CLOCK ? new HIRES_CLOCK() : +new Date; + return ((now / 1000.0) - since); +} + +// requestAnimationFrame emulation +function createAnimationStepper(options) { + options = options || {}; + var fps = options.fps || 30, + waitMs = 1000 / fps, + getTime = options.getRuntimeSeconds || getRuntimeSeconds, + lastUpdateTime = -1e-6, + timeout = 0; + + requestAnimationFrame.fps = fps; + requestAnimationFrame.reset = function() { + if (timeout) { + Script.clearTimeout(timeout); + timeout = 0; + } + }; + + function requestAnimationFrame(update) { + requestAnimationFrame.reset(); + timeout = Script.setTimeout(function() { + timeout = 0; + update(getTime(lastUpdateTime)); + lastUpdateTime = getTime(); + }, waitMs ); + } + + return requestAnimationFrame; +} + +// ---------------------------------------------------------------------------- +// KeyListener provides a scoped wrapper where options.onKeyPressEvent gets +// called when a key event matches the specified event.text / key spec +// example: var listener = new KeyListener({ text: 'SPACE', isShifted: false, onKeyPressEvent: function(event) { ... } }); +// Script.scriptEnding.connect(listener, 'disconnect'); +function KeyListener(options) { + assert(typeof options === 'object' && 'text' in options && 'onKeyPressEvent' in options); + + this._options = options; + assign(this, { + modifiers: this._getEventModifiers(options, true) + }, options); + this.log = options.log || function log() { + print('KeyListener | ', [].slice.call(arguments).join(' ')); + }; + this.log('created KeyListener', JSON.stringify(this.text), this.modifiers); + this.connect(); +} +KeyListener.prototype = { + _getEventModifiers: function(event, trueOnly) { + return '(' + [ 'Control', 'Meta', 'Alt', 'Super', 'Menu', 'Shifted' ].map(function(mod) { + var isMod = 'is' + mod, + value = event[isMod], + found = (trueOnly ? value : typeof value === 'boolean'); + return found && isMod + ' == ' + value; + }).filter(Boolean).sort().join(' | ') + ')'; + }, + handleEvent: function(event, target) { + if (event.text === this.text) { + var modifiers = this._getEventModifiers(event, true); + if (modifiers !== this.modifiers) { + return; + } + return this[target](event); + } + }, + connect: function() { + return this.$bindEvents(true); + }, + disconnect: function() { + return this.$bindEvents(false); + }, + $onKeyPressEvent: function(event) { + return this.handleEvent(event, 'onKeyPressEvent'); + }, + $onKeyReleaseEvent: function(event) { + return this.handleEvent(event, 'onKeyReleaseEvent'); + }, + $bindEvents: function(connect) { + if (this.onKeyPressEvent) { + Controller.keyPressEvent[connect ? 'connect' : 'disconnect'](this, '$onKeyPressEvent'); + } + if (this.onKeyReleaseEvent) { + Controller.keyReleaseEvent[connect ? 'connect' : 'disconnect'](this, '$onKeyReleaseEvent'); + } + Controller[(connect ? 'capture' : 'release') + 'KeyEvents'](this._options); + } +}; + +// helper to reload a client script +reloadClientScript._findRunning = function(filename) { + return ScriptDiscoveryService.getRunning().filter(function(script) { + return 0 === script.path.indexOf(filename); + }); +}; +function reloadClientScript(filename) { + function log() { + print('reloadClientScript | ', [].slice.call(arguments).join(' ')); + } + log('attempting to reload using stopScript(..., true):', filename); + var result = ScriptDiscoveryService.stopScript(filename, true); + if (!result) { + var matches = reloadClientScript._findRunning(filename), + path = matches[0] && matches[0].path; + if (path) { + log('attempting to reload using matched getRunning path: ' + path); + result = ScriptDiscoveryService.stopScript(path, true); + } + } + log('///result:' + result); + return result; +} diff --git a/unpublishedScripts/marketplace/camera-move/modules/config-utils.js b/unpublishedScripts/marketplace/camera-move/modules/config-utils.js new file mode 100644 index 0000000000..3b037edbea --- /dev/null +++ b/unpublishedScripts/marketplace/camera-move/modules/config-utils.js @@ -0,0 +1,252 @@ +// config-utils.js -- helpers for coordinating Application runtime vs. Settings configuration +// +// * ApplicationConfig -- Menu items and API values. +// * SettingsConfig -- scoped Settings values. + +"use strict"; +/* eslint-env commonjs */ +/* global log */ +module.exports = { + version: '0.0.1a', + ApplicationConfig: ApplicationConfig, + SettingsConfig: SettingsConfig +}; + +var _utils = require('./_utils.js'), + assert = _utils.assert; +Object.assign = Object.assign || _utils.assign; + +function _debugPrint() { + print('config-utils | ' + [].slice.call(arguments).join(' ')); +} + +var debugPrint = function() {}; + +// ---------------------------------------------------------------------------- +// Application-specific configuration values using runtime state / API props +// +// options.config[] supports the following item formats: +// 'settingsName': { menu: 'Menu > MenuItem'}, // assumes MenuItem is a checkbox / checkable value +// 'settingsName': { object: [ MyAvatar, 'property' ] }, +// 'settingsName': { object: [ MyAvatar, 'getterMethod', 'setterMethod' ] }, +// 'settingsName': { menu: 'Menu > MenuItem', object: [ MyAvatar, 'property' ] }, +// 'settingsName': { get: function getter() { ...}, set: function(nv) { ... } }, + +function ApplicationConfig(options) { + options = options || {}; + assert('namespace' in options && 'config' in options); + if (options.debug) { + debugPrint = _debugPrint; + debugPrint('debugPrinting enabled'); + } + this.namespace = options.namespace; + this.valueUpdated = _utils.signal(function valueUpdated(key, newValue, oldValue, origin){}); + + this.config = {}; + this.register(options.config); +} +ApplicationConfig.prototype = { + resolve: function resolve(key) { + assert(typeof key === 'string', 'ApplicationConfig.resolve error: key is not a string: ' + key); + if (0 !== key.indexOf('.') && !~key.indexOf('/')) { + key = [ this.namespace, key ].join('/'); + } + return (key in this.config) ? key : (debugPrint('ApplicationConfig -- could not resolve key: ' + key),undefined); + }, + registerItem: function(settingName, item) { + item._settingName = settingName; + item.settingName = ~settingName.indexOf('/') ? settingName : [ this.namespace, settingName ].join('/'); + return this.config[item.settingName] = this.config[settingName] = new ApplicationConfigItem(item); + }, + // process items into fully-qualfied ApplicationConfigItem instances + register: function(items) { + for (var p in items) { + var item = items[p]; + item && this.registerItem(p, item); + } + }, + _getItem: function(key) { + return this.config[this.resolve(key)]; + }, + getValue: function(key, defaultValue) { + var item = this._getItem(key); + if (!item) { + return defaultValue; + } + return item.get(); + }, + setValue: function setValue(key, value) { + key = this.resolve(key); + var lastValue = this.getValue(key, value); + var ret = this._getItem(key).set(value); + if (lastValue !== value) { + this.valueUpdated(key, value, lastValue, 'ApplicationConfig.setValue'); + } + return ret; + }, + // sync dual-source (ie: Menu + API) items + resyncValue: function(key) { + var item = this._getItem(key); + return item && item.resync(); + }, + // sync Settings values -> Application state + applyValue: function applyValue(key, value, origin) { + if (this.resolve(key)) { + var appValue = this.getValue(key, value); + debugPrint('applyValue', key, value, origin ? '['+origin+']' : '', appValue); + if (appValue !== value) { + this.setValue(key, value); + debugPrint('applied new setting', key, value, '(was:'+appValue+')'); + return true; + } + } + } +}; + +// ApplicationConfigItem represents a single API/Menu item accessor +function ApplicationConfigItem(item) { + Object.assign(this, item); + Object.assign(this, { + _item: item.get && item, + _object: this._parseObjectConfig(this.object), + _menu: this._parseMenuConfig(this.menu) + }); + this.authority = this._item ? 'item' : this._object ? 'object' : this._menu ? 'menu' : null; + this._authority = this['_'+this.authority]; + debugPrint('_authority', this.authority, this._authority, Object.keys(this._authority)); + assert(this._authority, 'expected item.get, .object or .menu definition; ' + this.settingName); +} +ApplicationConfigItem.prototype = { + resync: function resync() { + var authoritativeValue = this._authority.get(); + if (this._menu && this._menu.get() !== authoritativeValue) { + _debugPrint(this.settingName, this._menu.menuItem, + '... menu value ('+this._menu.get()+') out of sync;', + 'setting to authoritativeValue ('+authoritativeValue+')'); + this._menu.set(authoritativeValue); + } + if (this._object && this._object.get() !== authoritativeValue) { + _debugPrint(this.settingName, this._object.getter || this._object.property, + '... object value ('+this._object.get()+') out of sync;', + 'setting to authoritativeValue ('+authoritativeValue+')'); + this._object.set(authoritativeValue); + } + }, + toString: function() { + return '[ApplicationConfigItem ' + [ + 'setting:' + JSON.stringify(this.settingName), + 'authority:' + JSON.stringify(this.authority), + this._object && 'object:' + JSON.stringify(this._object.property || this._object.getter), + this._menu && 'menu:' + JSON.stringify(this._menu.menu) + ].filter(Boolean).join(' ') + ']'; + }, + get: function get() { + return this._authority.get(); + }, + set: function set(nv) { + this._object && this._object.set(nv); + this._menu && this._menu.set(nv); + return nv; + }, + _raiseError: function(errorMessage) { + if (this.debug) { + throw new Error(errorMessage); + } else { + _debugPrint('ERROR: ' + errorMessage); + } + }, + _parseObjectConfig: function(parts) { + if (!Array.isArray(parts) || parts.length < 2) { + return null; + } + var object = parts[0], getter = parts[1], setter = parts[2]; + if (typeof object[getter] === 'function' && typeof object[setter] === 'function') { + // [ API, 'getter', 'setter' ] + return { + object: object, getter: getter, setter: setter, + get: function getObjectValue() { + return this.object[this.getter](); + }, + set: function setObjectValue(nv) { + return this.object[this.setter](nv), nv; + } + }; + } else if (getter in object) { + // [ API, 'property' ] + return { + object: object, property: getter, + get: function() { + return this.object[this.property]; + }, + set: function(nv) { + return this.object[this.property] = nv; + } + }; + } + this._raiseError('{ object: [ Object, getterOrPropertyName, setterName ] } -- invalid params or does not exist: ' + + [ this.settingName, this.object, getter, setter ].join(' | ')); + }, + _parseMenuConfig: function(menu) { + if (!menu || typeof menu !== 'string') { + return null; + } + var parts = menu.split(/\s*>\s*/), menuItemName = parts.pop(), menuName = parts.join(' > '); + if (menuItemName && Menu.menuItemExists(menuName, menuItemName)) { + return { + menu: menu, menuName: menuName, menuItemName: menuItemName, + get: function() { + return Menu.isOptionChecked(this.menuItemName); + }, + set: function(nv) { + return Menu.setIsOptionChecked(this.menuItemName, nv), nv; + } + }; + } + this._raiseError('{ menu: "Menu > Item" } structure -- invalid params or does not exist: ' + + [ this.settingName, this.menu, menuName, menuItemName ].join(' | ')); + } +}; // ApplicationConfigItem.prototype + +// ---------------------------------------------------------------------------- +// grouped configuration using the Settings.* API +function SettingsConfig(options) { + options = options || {}; + assert('namespace' in options); + this.namespace = options.namespace; + this.defaultValues = {}; + this.valueUpdated = _utils.signal(function valueUpdated(key, newValue, oldValue, origin){}); + if (options.defaultValues) { + Object.keys(options.defaultValues) + .forEach(_utils.bind(this, function(key) { + var fullSettingsKey = this.resolve(key); + this.defaultValues[fullSettingsKey] = options.defaultValues[key]; + })); + } +} +SettingsConfig.prototype = { + resolve: function(key) { + assert(typeof key === 'string', 'SettingsConfig.resolve error: key is not a string: ' + key); + return (0 !== key.indexOf('.') && !~key.indexOf('/')) ? + [ this.namespace, key ].join('/') : key; + }, + getValue: function(key, defaultValue) { + key = this.resolve(key); + defaultValue = defaultValue === undefined ? this.defaultValues[key] : defaultValue; + return Settings.getValue(key, defaultValue); + }, + setValue: function setValue(key, value) { + key = this.resolve(key); + var lastValue = this.getValue(key); + var ret = Settings.setValue(key, value); + if (lastValue !== value) { + this.valueUpdated(key, value, lastValue, 'SettingsConfig.setValue'); + } + return ret; + }, + getFloat: function getFloat(key, defaultValue) { + key = this.resolve(key); + defaultValue = defaultValue === undefined ? this.defaultValues[key] : defaultValue; + var value = parseFloat(this.getValue(key, defaultValue)); + return isFinite(value) ? value : isFinite(defaultValue) ? defaultValue : 0.0; + } +}; diff --git a/unpublishedScripts/marketplace/camera-move/modules/custom-settings-app/CustomSettingsApp.js b/unpublishedScripts/marketplace/camera-move/modules/custom-settings-app/CustomSettingsApp.js new file mode 100644 index 0000000000..c76adea940 --- /dev/null +++ b/unpublishedScripts/marketplace/camera-move/modules/custom-settings-app/CustomSettingsApp.js @@ -0,0 +1,324 @@ +// CustomSettingsApp.js -- manages Settings between a Client script and connected "settings" tablet WebView page +// see browser/BridgedSettings.js for the webView side + +// example: +// var button = tablet.addButton({ text: 'My Settings' }); +// var mySettingsApp = new CustomSettingsApp({ +// namespace: 'mySettingsGroup', +// url: Script.resolvePath('myapp.html'), +// uuid: button.uuid, +// tablet: tablet +// }); +// +// // settings are automatically sync'd from the web page back to Interface; to be notified when that happens, use: +// myAppSettings.valueUpdated.connect(function(name, value, oldValue, origin) { +// print('setting updated from web page', name, value, oldValue, origin); +// }); +// +// // settings are also automatically sync'd from Interface back to the web page; to manually sync a value, use: +// myAppSettings.syncValue(fullSettingsKey, value); + +/* eslint-env commonjs */ +"use strict"; + +CustomSettingsApp.version = '0.0.0'; +module.exports = CustomSettingsApp; + +var _utils = require('../_utils.js'); +Object.assign = Object.assign || _utils.assign; + +function assert(truthy, message) { + return _utils.assert.call(this, truthy, 'CustomSettingsApp | ' + message); +} + +function _debugPrint() { + print('CustomSettingsApp | ' + [].slice.call(arguments).join(' ')); +} +var debugPrint = function() {}; + +function CustomSettingsApp(options) { + assert('url' in options, 'expected options.url'); + if (options.debug) { + debugPrint = _debugPrint; + } + + this.url = options.url; + this.namespace = options.namespace || 'BridgedSettings'; + this.uuid = options.uuid || Uuid.generate(); + this.recheckInterval = options.recheckInterval || 1000; + + this.settingsScreenVisible = false; + this.isActive = false; + + this.extraParams = Object.assign(options.extraParams || {}, { + customSettingsVersion: CustomSettingsApp.version+'', + protocolVersion: location.protocolVersion && location.protocolVersion() + }); + + var params = { + namespace: this.namespace, + uuid: this.uuid, + debug: options.debug || undefined + }; + + // encode PARAMS into '?key=value&...' + var query = Object.keys(params).map(function encodeValue(key) { + var value = encodeURIComponent(params[key] === undefined ? '' : params[key]); + return [ key, value ].join('='); + }).join('&'); + this.url += '?&' + query; + + this.isActiveChanged = _utils.signal(function(isActive) {}); + this.valueUpdated = _utils.signal(function(key, value, oldValue, origin) {}); + + this.settingsAPI = options.settingsAPI || Settings; + + // keep track of accessed settings so they can be kept in sync if changed externally + this._activeSettings = { sent: {}, received: {}, remote: {} }; + + if (options.tablet) { + this._initialize(options.tablet); + } +} + +CustomSettingsApp.prototype = { + tablet: null, + resolve: function(key) { + if (0 === key.indexOf('.') || ~key.indexOf('/')) { + // key is already qualified under a group; return as-is + return key; + } + // nest under the current namespace + return [ this.namespace, key ].join('/'); + }, + sendEvent: function(msg) { + assert(this.tablet, '!this.tablet'); + msg.ns = msg.ns || this.namespace; + msg.uuid = msg.uuid || this.uuid; + this.tablet.emitScriptEvent(JSON.stringify(msg)); + }, + getValue: function(key, defaultValue) { + key = this.resolve(key); + return key in this._activeSettings.remote ? this._activeSettings.remote[key] : defaultValue; + }, + setValue: function(key, value) { + key = this.resolve(key); + var current = this.getValue(key); + if (current !== value) { + return this.syncValue(key, value, 'CustomSettingsApp.setValue'); + } + return false; + }, + syncValue: function(key, value, origin) { + key = this.resolve(key); + var oldValue = this._activeSettings.remote[key]; + assert(value !== null, 'CustomSettingsApp.syncValue value is null'); + this.sendEvent({ id: 'valueUpdated', params: [key, value, oldValue, origin] }); + this._activeSettings.sent[key] = value; + this._activeSettings.remote[key] = value; + this.valueUpdated(key, value, oldValue, (origin ? origin+':' : '') + 'CustomSettingsApp.syncValue'); + }, + onScreenChanged: function onScreenChanged(type, url) { + this.settingsScreenVisible = (url === this.url); + debugPrint('===> onScreenChanged', type, url, 'settingsScreenVisible: ' + this.settingsScreenVisible); + if (this.isActive && !this.settingsScreenVisible) { + this.isActiveChanged(this.isActive = false); + } + }, + + _apiGetValue: function(key, defaultValue) { + // trim rooted keys like "/desktopTabletBecomesToolbar" => "desktopTabletBecomesToolbar" + key = key.replace(/^\//,''); + return this.settingsAPI.getValue(key, defaultValue); + }, + _apiSetValue: function(key, value) { + key = key.replace(/^\//,''); + return this.settingsAPI.setValue(key, value); + }, + _setValue: function(key, value, oldValue, origin) { + var current = this._apiGetValue(key), + lastRemoteValue = this._activeSettings.remote[key]; + debugPrint('.setValue(' + JSON.stringify({key: key, value: value, current: current, lastRemoteValue: lastRemoteValue })+')'); + this._activeSettings.received[key] = value; + this._activeSettings.remote[key] = value; + var result; + if (lastRemoteValue !== value) { + this.valueUpdated(key, value, lastRemoteValue, 'CustomSettingsApp.tablet'); + } + if (current !== value) { + result = this._apiSetValue(key, value); + } + return result; + }, + + _handleValidatedMessage: function(obj, msg) { + var tablet = this.tablet; + if (!tablet) { + throw new Error('_handleValidatedMessage called when not connected to tablet...'); + } + var params = Array.isArray(obj.params) ? obj.params : [obj.params]; + var parts = (obj.method||'').split('.'), api = parts[0], method = parts[1]; + switch(api) { + case 'valueUpdated': obj.result = this._setValue.apply(this, params); break; + case 'Settings': + if (method && params[0]) { + var key = this.resolve(params[0]), value = params[1]; + debugPrint('>>>>', method, key, value); + switch (method) { + case 'getValue': + obj.result = this._apiGetValue(key, value); + this._activeSettings.sent[key] = obj.result; + this._activeSettings.remote[key] = obj.result; + break; + case 'setValue': + obj.result = this._setValue(key, value, params[2], params[3]); + break; + default: + obj.error = 'unmapped Settings method: ' + method; + throw new Error(obj.error); + } + break; + } + default: if (this.onUnhandledMessage) { + this.onUnhandledMessage(obj, msg); + } else { + obj.error = 'unmapped method call: ' + msg; + } + } + if (obj.id) { + // if message has an id, reply with the same message obj which now has a .result or .error field + // note: a small delay is needed because of an apparent race condition between ScriptEngine and Tablet WebViews + Script.setTimeout(_utils.bind(this, function() { + this.sendEvent(obj); + }), 100); + } else if (obj.error) { + throw new Error(obj.error); + } + }, + onWebEventReceived: function onWebEventReceived(msg) { + debugPrint('onWebEventReceived', msg); + var tablet = this.tablet; + if (!tablet) { + throw new Error('onWebEventReceived called when not connected to tablet...'); + } + if (msg === this.url) { + if (this.isActive) { + // user (or page) refreshed the web view; trigger !isActive so client script can perform cleanup + this.isActiveChanged(this.isActive = false); + } + this.isActiveChanged(this.isActive = true); + // reply to initial HTML page ACK with any extraParams that were specified + this.sendEvent({ id: 'extraParams', extraParams: this.extraParams }); + return; + } + try { + var obj = assert(JSON.parse(msg)); + } catch (e) { + return; + } + if (obj.ns === this.namespace && obj.uuid === this.uuid) { + debugPrint('valid onWebEventReceived', msg); + this._handleValidatedMessage(obj, msg); + } + }, + + _initialize: function(tablet) { + if (this.tablet) { + throw new Error('CustomSettingsApp._initialize called but this.tablet already has a value'); + } + this.tablet = tablet; + tablet.webEventReceived.connect(this, 'onWebEventReceived'); + tablet.screenChanged.connect(this, 'onScreenChanged'); + + this.onAPIValueUpdated = function(key, newValue, oldValue, origin) { + if (this._activeSettings.remote[key] !== newValue) { + _debugPrint('onAPIValueUpdated: ' + key + ' = ' + JSON.stringify(newValue), + '(was: ' + JSON.stringify(oldValue) +')'); + this.syncValue(key, newValue, (origin ? origin+':' : '') + 'CustomSettingsApp.onAPIValueUpdated'); + } + }; + this.isActiveChanged.connect(this, function(isActive) { + this._activeSettings.remote = {}; // reset assumptions about remote values + isActive ? this.$startMonitor() : this.$stopMonitor(); + }); + debugPrint('CustomSettingsApp...initialized', this.namespace); + }, + + $syncSettings: function() { + for (var p in this._activeSettings.sent) { + var value = this._apiGetValue(p), + lastValue = this._activeSettings.remote[p]; + if (value !== undefined && value !== null && value !== '' && value !== lastValue) { + _debugPrint('CustomSettingsApp... detected external settings change', p, value); + this.syncValue(p, value, 'Settings'); + this.valueUpdated(p, value, lastValue, 'CustomSettingsApp.$syncSettings'); + } + } + }, + $startMonitor: function() { + if (!(this.recheckInterval > 0)) { + _debugPrint('$startMonitor -- recheckInterval <= 0; not starting settings monitor thread'); + return false; + } + if (this.interval) { + this.$stopMonitor(); + } + if (this.settingsAPI.valueUpdated) { + _debugPrint('settingsAPI supports valueUpdated -- binding to detect settings changes', this.settingsAPI); + this.settingsAPI.valueUpdated.connect(this, 'onAPIValueUpdated'); + } + this.interval = Script.setInterval(_utils.bind(this, '$syncSettings'), this.recheckInterval); + _debugPrint('STARTED MONITORING THREAD'); + }, + $stopMonitor: function() { + if (this.interval) { + Script.clearInterval(this.interval); + this.interval = 0; + if (this.settingsAPI.valueUpdated) { + this.settingsAPI.valueUpdated.disconnect(this, 'onAPIValueUpdated'); + } + _debugPrint('stopped monitoring thread'); + return true; + } + }, + + cleanup: function() { + if (!this.tablet) { + return _debugPrint('CustomSettingsApp...cleanup called when not initialized'); + } + var tablet = this.tablet; + tablet.webEventReceived.disconnect(this, 'onWebEventReceived'); + tablet.screenChanged.disconnect(this, 'onScreenChanged'); + this.$stopMonitor(); + if (this.isActive) { + try { + this.isActiveChanged(this.isActive = false); + } catch (e) { + _debugPrint('ERROR: cleanup error during isActiveChanged(false)', e); + } + } + this.toggle(false); + this.settingsScreenVisible = false; + this.tablet = null; + debugPrint('cleanup completed', this.namespace); + }, + + toggle: function(show) { + if (!this.tablet) { + return _debugPrint('CustomSettingsApp...cleanup called when not initialized'); + } + if (typeof show !== 'boolean') { + show = !this.settingsScreenVisible; + } + + if (this.settingsScreenVisible && !show) { + this.tablet.gotoHomeScreen(); + } else if (!this.settingsScreenVisible && show) { + Script.setTimeout(_utils.bind(this, function() { + // Interface sometimes crashes if not for adding a small timeout here :( + this.tablet.gotoWebScreen(this.url); + }), 1); + } + } +}; + diff --git a/unpublishedScripts/marketplace/camera-move/modules/custom-settings-app/browser/BridgedSettings.js b/unpublishedScripts/marketplace/camera-move/modules/custom-settings-app/browser/BridgedSettings.js new file mode 100644 index 0000000000..9b4735689f --- /dev/null +++ b/unpublishedScripts/marketplace/camera-move/modules/custom-settings-app/browser/BridgedSettings.js @@ -0,0 +1,197 @@ +// BridgedSettings.js -- HTML-side implementation of bridged/async Settings +// see ../CustomSettingsApp.js for the corresponding Interface script + +/* eslint-env commonjs, browser */ +/* eslint-disable comma-dangle, no-empty */ + +(function(global) { + "use strict"; + + BridgedSettings.version = '0.0.2'; + + try { + module.exports = BridgedSettings; + } catch (e) { + global.BridgedSettings = BridgedSettings; + } + + var _utils = global._utils || (typeof require === 'function' && require('../../_utils.js')); + if (!_utils || !_utils.signal) { + throw new Error('html.BridgedSettings.js -- expected _utils to be available on the global object (ie: window._utils)'); + } + var signal = _utils.signal; + + function log() { + console.info('bridgedSettings | ' + [].slice.call(arguments).join(' ')); // eslint-disable-line no-console + } + log('version', BridgedSettings.version); + + var debugPrint = function() {}; // = log + + function BridgedSettings(options) { + options = options || {}; + // Note: Interface changed how window.EventBridge behaves again; it now arbitrarily replaces the global value + // sometime after the initial page load, invaliding any held references to it. + // As a workaround this proxies the local property to the current global value. + var _lastEventBridge = global.EventBridge; + Object.defineProperty(this, 'eventBridge', { enumerable: true, get: function() { + if (_lastEventBridge !== global.EventBridge) { + log('>>> EventBridge changed in-flight', '(was: ' + _lastEventBridge + ' | is: ' + global.EventBridge + ')'); + _lastEventBridge = global.EventBridge; + } + return global.EventBridge; + }}); + Object.assign(this, { + //eventBridge: options.eventBridge || global.EventBridge, + namespace: options.namespace || 'BridgedSettings', + uuid: options.uuid || undefined, + valueReceived: signal(function valueReceived(key, newValue, oldValue, origin){}), + callbackError: signal(function callbackError(error, message){}), + pendingRequestsFinished: signal(function pendingRequestsFinished(){}), + extraParams: options.extraParams || {}, + _hifiValues: {}, + + debug: options.debug, + log: log.bind({}, options.namespace + ' |'), + debugPrint: function() { + return this.debug && this.log.apply(this, arguments); + }, + _boundScriptEventReceived: this.onScriptEventReceived.bind(this), + callbacks: Object.defineProperties(options.callbacks || {}, { + extraParams: { value: this.handleExtraParams }, + valueUpdated: { value: this.handleValueUpdated }, + }) + }); + this.log('connecting to EventBridge.scriptEventReceived'); + this.eventBridge.scriptEventReceived.connect(this._boundScriptEventReceived); + } + + BridgedSettings.prototype = { + _callbackId: 1, + toString: function() { + return '[BridgedSettings namespace='+this.namespace+']'; + }, + resolve: function(key) { + if (0 !== key.indexOf('.') && !~key.indexOf('/')) { + return [ this.namespace, key ].join('/'); + } else { + return key; + } + }, + handleValueUpdated: function(msg) { + // client script notified us that a value was updated on that side + var key = this.resolve(msg.params[0]), + value = msg.params[1], + oldValue = msg.params[2], + origin = msg.params[3]; + log('callbacks.valueUpdated', key, value, oldValue, origin); + this._hifiValues[key] = value; + this.valueReceived(key, value, oldValue, (origin?origin+':':'') + 'callbacks.valueUpdated'); + }, + handleExtraParams: function(msg) { + // client script sent us extraParams + var extraParams = msg.extraParams; + var previousParams = JSON.parse(JSON.stringify(this.extraParams)); + + _utils.sortedAssign(this.extraParams, extraParams); + + this._hifiValues['.extraParams'] = this.extraParams; + this.debugPrint('received .extraParams', JSON.stringify(extraParams,0,2)); + this.valueReceived('.extraParams', this.extraParams, previousParams, 'html.bridgedSettings.handleExtraParams'); + }, + cleanup: function() { + try { + this.eventBridge.scriptEventReceived.disconnect(this._boundScriptEventReceived); + } catch (e) { + this.log('error disconnecting from scriptEventReceived:', e); + } + }, + pendingRequestCount: function() { + return Object.keys(this.callbacks).length; + }, + _handleValidatedMessage: function(obj, msg) { + var callback = this.callbacks[obj.id]; + if (callback) { + try { + return callback.call(this, obj) || true; + } catch (e) { + this.log('CALLBACK ERROR', this.namespace, obj.id, '_onScriptEventReceived', e); + this.callbackError(e, obj); + if (this.debug) { + throw e; + } + } + } else if (this.onUnhandledMessage) { + return this.onUnhandledMessage(obj, msg); + } + }, + onScriptEventReceived: function(msg) { + this.debugPrint(this.namespace, '_onScriptEventReceived......' + msg); + try { + var obj = JSON.parse(msg); + var validSender = obj.ns === this.namespace && obj.uuid === this.uuid; + if (validSender) { + return this._handleValidatedMessage(obj, msg); + } else { + debugPrint('xskipping', JSON.stringify([obj.ns, obj.uuid]), JSON.stringify(this), msg); + } + } catch (e) { + log('rpc error:', e, msg); + return e; + } + }, + sendEvent: function(msg) { + msg.ns = msg.ns || this.namespace; + msg.uuid = msg.uuid || this.uuid; + debugPrint('sendEvent', JSON.stringify(msg)); + this.eventBridge.emitWebEvent(JSON.stringify(msg)); + }, + getValue: function(key, defaultValue) { + key = this.resolve(key); + return key in this._hifiValues ? this._hifiValues[key] : defaultValue; + }, + setValue: function(key, value) { + key = this.resolve(key); + var current = this.getValue(key); + if (current !== value) { + debugPrint('SET VALUE : ' + JSON.stringify({ key: key, current: current, value: value })); + return this.syncValue(key, value, 'setValue'); + } + this._hifiValues[key] = value; + return false; + }, + syncValue: function(key, value, origin) { + return this.sendEvent({ method: 'valueUpdated', params: [key, value, this.getValue(key), origin] }); + }, + getValueAsync: function(key, defaultValue, callback) { + key = this.resolve(key); + if (typeof defaultValue === 'function') { + callback = defaultValue; + defaultValue = undefined; + } + var params = defaultValue !== undefined ? [ key, defaultValue ] : [ key ], + event = { method: 'Settings.getValue', params: params }; + + this.debugPrint('< getValueAsync...', key, params); + if (callback) { + event.id = this._callbackId++; + this.callbacks[event.id] = function(obj) { + try { + callback(obj.error, obj.result); + if (!obj.error) { + this._hifiValues[key] = obj.result; + } + } finally { + delete this.callbacks[event.id]; + } + if (this.pendingRequestCount() === 0) { + setTimeout(function() { + this.pendingRequestsFinished(); + }.bind(this), 1); + } + }; + } + this.sendEvent(event); + }, + }; +})(this); diff --git a/unpublishedScripts/marketplace/camera-move/modules/custom-settings-app/browser/JQuerySettings.js b/unpublishedScripts/marketplace/camera-move/modules/custom-settings-app/browser/JQuerySettings.js new file mode 100644 index 0000000000..66b1aad53a --- /dev/null +++ b/unpublishedScripts/marketplace/camera-move/modules/custom-settings-app/browser/JQuerySettings.js @@ -0,0 +1,195 @@ +// JQuerySettings.js -- HTML-side helper class for managing settings-linked jQuery UI elements + +/* eslint-env jquery, commonjs, browser */ +/* eslint-disable comma-dangle, no-empty */ +/* global assert, log */ +// ---------------------------------------------------------------------------- +(function(global) { + "use strict"; + + JQuerySettings.version = '0.0.0'; + + try { + module.exports = JQuerySettings; + } catch (e) { + global.JQuerySettings= JQuerySettings; + } + + var _utils = global._utils || (typeof require === 'function' && require('../../_utils.js')); + + if (!_utils || !_utils.signal) { + throw new Error('html.JQuerySettings.js -- expected _utils to be available on the global object (ie: window._utils)'+module); + } + var signal = _utils.signal, + assert = _utils.assert; + + function log() { + console.info('jquerySettings | ' + [].slice.call(arguments).join(' ')); // eslint-disable-line no-console + } + log('version', JQuerySettings.version); + + var debugPrint = function() {}; // = log + + function JQuerySettings(options) { + assert('namespace' in options); + + Object.assign(this, { + id2Setting: {}, // DOM id -> qualified Settings key + Setting2id: {}, // qualified Settings key -> DOM id + observers: {}, // DOM MutationObservers + mutationEvent: signal(function mutationEvent(event) {}), + boundOnDOMMutation: this._onDOMMutation.bind(this), + }, options); + } + JQuerySettings.idCounter = 0; + JQuerySettings.prototype = { + toString: function() { + return '[JQuerySettings namespace='+this.namespace+']'; + }, + mutationConfig: { + attributes: true, + attributeOldValue: true, + attributeFilter: [ 'value', 'checked', 'data-checked', 'data-value' ] + }, + _onDOMMutation: function(mutations, observer) { + mutations.forEach(function(mutation, index) { + var target = mutation.target, + targetId = target.dataset['for'] || target.id, + domId = target.id, + attrValue = target.getAttribute(mutation.attributeName), + hifiType = target.dataset.hifiType, + value = hifiType ? $(target)[hifiType]('instance').value() : attrValue, + oldValue = mutation.oldValue; + var event = { + key: this.getKey(targetId, true) || this.getKey(domId), + value: value, + oldValue: oldValue, + hifiType: hifiType, + domId: domId, + domType: target.getAttribute('type') || target.type, + targetId: targetId, + attrValue: attrValue, + domName: target.name, + type: mutation.type, + }; + + switch (typeof value) { + case 'boolean': event.oldValue = !!event.oldValue; break; + case 'number': + var tmp = parseFloat(oldValue); + if (isFinite(tmp)) { + event.oldValue = tmp; + } + break; + } + + return (event.oldValue === event.value) ? + debugPrint('SKIP NON-MUTATION', event.key, event.hifiType) : + this.mutationEvent(event); + }.bind(this)); + }, + observeNode: function(node) { + assert(node.id); + var observer = this.observers[node.id]; + if (!observer) { + observer = new MutationObserver(this.boundOnDOMMutation); + observer.observe(node, this.mutationConfig); + this.observers[node.id] = observer; + } + debugPrint('observeNode', node.id, node.dataset.hifiType, node.name); + return observer; + }, + resolve: function(key) { + assert(typeof key === 'string'); + if (0 !== key.indexOf('.') && !~key.indexOf('/')) { + return [ this.namespace, key ].join('/'); + } else { + return key; + } + }, + registerSetting: function(id, key) { + assert(id, 'registerSetting -- invalid id: ' + id + ' for key:' + key); + this.id2Setting[id] = key; + if (!(key in this.Setting2id)) { + this.Setting2id[key] = id; + } else { + key = null; + } + debugPrint('JQuerySettings.registerSetting -- registered: ' + JSON.stringify({ id: id, key: key })); + }, + registerNode: function(node) { + var element = $(node), + target = element.data('for') || element.attr('for') || element.prop('id'); + assert(target, 'registerNode could determine settings target: ' + node.outerHTML); + if (!node.id) { + node.id = ['id', target.replace(/[^-\w]/g,'-'), JQuerySettings.idCounter++ ].join('-'); + } + var key = node.dataset['key'] = this.resolve(target); + this.registerSetting(node.id, key); + + debugPrint('registerNode', node.id, target, key); + // return this.observeNode(node); + }, + // lookup the DOM id for a given Settings key + getId: function(key, missingOk) { + key = this.resolve(key); + assert(missingOk || function assertion(){ + return typeof key === 'string'; + }); + if (key in this.Setting2id || missingOk) { + return this.Setting2id[key]; + } + log('WARNING: jquerySettings.getId: !Setting2id['+key+'] ' + this.Setting2id[key], key in this.Setting2id); + }, + getAllNodes: function() { + return Object.keys(this.Setting2id) + .map(function(key) { + return this.findNodeByKey(key); + }.bind(this)) + .filter(function(node) { + return node.type !== 'placeholder'; + }).filter(Boolean); + }, + // lookup the Settings key for a given DOM id + getKey: function(id, missingOk) { + if ((id in this.id2Setting) || missingOk) { + return this.id2Setting[id]; + } + log('WARNING: jquerySettings.getKey: !id2Setting['+id+']'); + }, + // lookup the DOM node for a given Settings key + findNodeByKey: function(key, missingOk) { + key = this.resolve(key); + var id = this.getId(key, missingOk); + var node = typeof id === 'object' ? id : document.getElementById(id); + if (node || missingOk) { + return node; + } + log('WARNING: jquerySettings.findNodeByKey -- node not found:', 'key=='+key, 'id=='+id); + }, + getValue: function(key, defaultValue) { + return this.getNodeValue(this.findNodeByKey(key)); + }, + setValue: function(key, value, origin) { + return this.setNodeValue(this.findNodeByKey(key), value, origin || 'setValue'); + }, + getNodeValue: function(node) { + assert(node && typeof node === 'object', 'getNodeValue expects a DOM node'); + node = node.jquery ? node.get(0) : node; + if (node.type === 'placeholder') { + return node.value; + } + assert(node.dataset.hifiType); + return $(node)[node.dataset.hifiType]('instance').value(); + }, + setNodeValue: function(node, newValue) { + assert(node, 'JQuerySettings::setNodeValue -- invalid node:' + node); + node = node.jquery ? node[0] : node; + if (node.type === 'placeholder') { + return node.value = newValue; + } + var hifiType = assert(node.dataset.hifiType); + return $(node)[hifiType]('instance').value(newValue); + }, + }; +})(this); diff --git a/unpublishedScripts/marketplace/camera-move/modules/movement-utils.js b/unpublishedScripts/marketplace/camera-move/modules/movement-utils.js new file mode 100644 index 0000000000..feedc75c83 --- /dev/null +++ b/unpublishedScripts/marketplace/camera-move/modules/movement-utils.js @@ -0,0 +1,773 @@ +// movement-utils.js -- helper classes for managing related Controller.*Event and input API bindings + +/* eslint-disable comma-dangle, no-empty */ +/* global require: true, DriveKeys, console, __filename, __dirname */ +/* eslint-env commonjs */ +"use strict"; + +module.exports = { + version: '0.0.2c', + + CameraControls: CameraControls, + MovementEventMapper: MovementEventMapper, + MovementMapping: MovementMapping, + VelocityTracker: VelocityTracker, + VirtualDriveKeys: VirtualDriveKeys, + + applyEasing: applyEasing, + calculateThrust: calculateThrust, + vec3damp: vec3damp, + vec3eclamp: vec3eclamp, + + DriveModes: { + POSITION: 'position', // ~ MyAvatar.position + MOTOR: 'motor', // ~ MyAvatar.motorVelocity + THRUST: 'thrust', // ~ MyAvatar.setThrust + }, +}; + +var MAPPING_TEMPLATE = require('./movement-utils.mapping.json'); +var WANT_DEBUG = false; + +function log() { + // eslint-disable-next-line no-console + (typeof Script === 'object' ? print : console.log)('movement-utils | ' + [].slice.call(arguments).join(' ')); +} + +var debugPrint = function() {}; + +log(module.exports.version); + +var _utils = require('./_utils.js'), + assert = _utils.assert; + +if (WANT_DEBUG) { + require = _utils.makeDebugRequire(__dirname); + _utils = require('./_utils.js'); // re-require in debug mode + debugPrint = log; +} + +Object.assign = Object.assign || _utils.assign; + +var enumMeta = require('./EnumMeta.js'); +assert(enumMeta.version >= '0.0.1', 'enumMeta >= 0.0.1 expected but got: ' + enumMeta.version); + +Object.assign(MovementEventMapper, { + CAPTURE_DRIVE_KEYS: 'drive-keys', + CAPTURE_ACTION_EVENTS: 'action-events', +}); + +function MovementEventMapper(options) { + assert('namespace' in options, '.namespace expected ' + Object.keys(options) ); + this.namespace = options.namespace; + this.enabled = false; + + this.options = Object.assign({ + namespace: this.namespace, + captureMode: MovementEventMapper.CAPTURE_ACTION_EVENTS, + excludeNames: null, + mouseSmooth: true, + keyboardMultiplier: 1.0, + mouseMultiplier: 1.0, + eventFilter: null, + controllerMapping: MAPPING_TEMPLATE, + }, options); + + this.isShifted = false; + this.isGrounded = false; + this.isRightMouseButton = false; + this.rightMouseButtonReleased = undefined; + + this.inputMapping = new MovementMapping(this.options); + this.inputMapping.virtualActionEvent.connect(this, 'onVirtualActionEvent'); +} +MovementEventMapper.prototype = { + constructor: MovementEventMapper, + defaultEventFilter: function(from, event) { + return event.actionValue; + }, + getState: function(options) { + var state = this.states ? this.states.getDriveKeys(options) : {}; + + state.enabled = this.enabled; + + state.mouseSmooth = this.options.mouseSmooth; + state.captureMode = this.options.captureMode; + state.mouseMultiplier = this.options.mouseMultiplier; + state.keyboardMultiplier = this.options.keyboardMultiplier; + + state.isGrounded = this.isGrounded; + state.isShifted = this.isShifted; + state.isRightMouseButton = this.isRightMouseButton; + state.rightMouseButtonReleased = this.rightMouseButtonReleased; + + return state; + }, + updateOptions: function(options) { + return _updateOptions(this.options, options || {}, this.constructor.name); + }, + applyOptions: function(options, applyNow) { + if (this.updateOptions(options) && applyNow) { + this.reset(); + } + }, + reset: function() { + if (this.enabled) { + this.disable(); + this.enable(); + } + }, + disable: function() { + this.inputMapping.disable(); + this.bindEvents(false); + this.enabled = false; + }, + enable: function() { + if (!this.enabled) { + this.enabled = true; + this.states = new VirtualDriveKeys({ + eventFilter: this.options.eventFilter && _utils.bind(this, this.options.eventFilter) + }); + this.bindEvents(true); + this.inputMapping.updateOptions(this.options); + this.inputMapping.enable(); + } + }, + bindEvents: function bindEvents(capture) { + var captureMode = this.options.captureMode; + assert(function assertion() { + return captureMode === MovementEventMapper.CAPTURE_ACTION_EVENTS || + captureMode === MovementEventMapper.CAPTURE_DRIVE_KEYS; + }); + log('bindEvents....', capture, this.options.captureMode); + var exclude = Array.isArray(this.options.excludeNames) && this.options.excludeNames; + + var tmp; + if (!capture || this.options.captureMode === MovementEventMapper.CAPTURE_ACTION_EVENTS) { + tmp = capture ? 'captureActionEvents' : 'releaseActionEvents'; + log('bindEvents -- ', tmp.toUpperCase()); + Controller[tmp](); + } + if (!capture || this.options.captureMode === MovementEventMapper.CAPTURE_DRIVE_KEYS) { + tmp = capture ? 'disableDriveKey' : 'enableDriveKey'; + log('bindEvents -- ', tmp.toUpperCase()); + for (var p in DriveKeys) { + if (capture && (exclude && ~exclude.indexOf(p))) { + log(tmp.toUpperCase(), 'excluding DriveKey===' + p); + } else { + MyAvatar[tmp](DriveKeys[p]); + } + } + } + try { + Controller.actionEvent[capture ? 'connect' : 'disconnect'](this, 'onActionEvent'); + } catch (e) { } + + if (!capture || !/person/i.test(Camera.mode)) { + Controller[capture ? 'captureWheelEvents' : 'releaseWheelEvents'](); + try { + Controller.wheelEvent[capture ? 'connect' : 'disconnect'](this, 'onWheelEvent'); + } catch (e) { /* eslint-disable-line empty-block */ } + } + }, + onWheelEvent: function onWheelEvent(event) { + var actionID = enumMeta.ACTION_TRANSLATE_CAMERA_Z, + actionValue = -event.delta; + return this.onActionEvent(actionID, actionValue, event); + }, + onActionEvent: function(actionID, actionValue, extra) { + var actionName = enumMeta.Controller.ActionNames[actionID], + driveKeyName = enumMeta.getDriveKeyNameFromActionName(actionName), + prefix = (actionValue > 0 ? '+' : actionValue < 0 ? '-' : ' '); + + var event = { + id: prefix + actionName, + actionName: actionName, + driveKey: DriveKeys[driveKeyName], + driveKeyName: driveKeyName, + actionValue: actionValue, + extra: extra + }; + // debugPrint('onActionEvent', actionID, actionName, driveKeyName); + this.states.handleActionEvent('Actions.' + actionName, event); + }, + onVirtualActionEvent: function(from, event) { + if (from === 'Application.Grounded') { + this.isGrounded = !!event.applicationValue; + } else if (from === 'Keyboard.Shift') { + this.isShifted = !!event.value; + } else if (from === 'Keyboard.RightMouseButton') { + this.isRightMouseButton = !!event.value; + this.rightMouseButtonReleased = !event.value ? new Date : undefined; + } + this.states.handleActionEvent(from, event); + } +}; // MovementEventMapper.prototype + +// ---------------------------------------------------------------------------- +// helper JS class to track drive keys -> translation / rotation influences +function VirtualDriveKeys(options) { + options = options || {}; + Object.defineProperties(this, { + $pendingReset: { value: {} }, + $eventFilter: { value: options.eventFilter }, + $valueUpdated: { value: _utils.signal(function valueUpdated(action, newValue, oldValue){}) } + }); +} +VirtualDriveKeys.prototype = { + constructor: VirtualDriveKeys, + update: function update(dt) { + Object.keys(this.$pendingReset).forEach(_utils.bind(this, function(i) { + var event = this.$pendingReset[i].event; + (event.driveKey in this) && this.setValue(event, 0); + })); + }, + getValue: function(driveKey, defaultValue) { + return driveKey in this ? this[driveKey] : defaultValue; + }, + _defaultFilter: function(from, event) { + return event.actionValue; + }, + handleActionEvent: function(from, event) { + var value = this.$eventFilter ? this.$eventFilter(from, event, this._defaultFilter) : event.actionValue; + return event.driveKeyName && this.setValue(event, value); + }, + setValue: function(event, value) { + var driveKeyName = event.driveKeyName, + driveKey = DriveKeys[driveKeyName], + id = event.id, + previous = this[driveKey], + autoReset = (driveKeyName === 'ZOOM'); + + this[driveKey] = value; + + if (previous !== value) { + this.$valueUpdated(event, value, previous); + } + if (value === 0.0) { + delete this.$pendingReset[id]; + } else if (autoReset) { + this.$pendingReset[id] = { event: event, value: value }; + } + }, + reset: function() { + Object.keys(this).forEach(_utils.bind(this, function(p) { + this[p] = 0.0; + })); + Object.keys(this.$pendingReset).forEach(_utils.bind(this, function(p) { + delete this.$pendingReset[p]; + })); + }, + toJSON: function() { + var obj = {}; + for (var key in this) { + if (enumMeta.DriveKeyNames[key]) { + obj[enumMeta.DriveKeyNames[key]] = this[key]; + } + } + return obj; + }, + getDriveKeys: function(options) { + options = options || {}; + try { + return { + translation: { + x: this.getValue(DriveKeys.TRANSLATE_X) || 0, + y: this.getValue(DriveKeys.TRANSLATE_Y) || 0, + z: this.getValue(DriveKeys.TRANSLATE_Z) || 0 + }, + rotation: { + x: this.getValue(DriveKeys.PITCH) || 0, + y: this.getValue(DriveKeys.YAW) || 0, + z: 'ROLL' in DriveKeys && this.getValue(DriveKeys.ROLL) || 0 + }, + zoom: Vec3.multiply(this.getValue(DriveKeys.ZOOM) || 0, Vec3.ONE) + }; + } finally { + options.update && this.update(options.update); + } + } +}; + +// ---------------------------------------------------------------------------- +// MovementMapping + +function MovementMapping(options) { + options = options || {}; + assert('namespace' in options && 'controllerMapping' in options); + this.namespace = options.namespace; + this.enabled = false; + this.options = { + keyboardMultiplier: 1.0, + mouseMultiplier: 1.0, + mouseSmooth: true, + captureMode: MovementEventMapper.CAPTURE_ACTION_EVENTS, + excludeNames: null, + controllerMapping: MAPPING_TEMPLATE, + }; + this.updateOptions(options); + this.virtualActionEvent = _utils.signal(function virtualActionEvent(from, event) {}); +} +MovementMapping.prototype = { + constructor: MovementMapping, + enable: function() { + this.enabled = true; + if (this.mapping) { + this.mapping.disable(); + } + this.mapping = this._createMapping(); + log('ENABLE CONTROLLER MAPPING', this.mapping.name); + this.mapping.enable(); + }, + disable: function() { + this.enabled = false; + if (this.mapping) { + log('DISABLE CONTROLLER MAPPING', this.mapping.name); + this.mapping.disable(); + } + }, + reset: function() { + var enabled = this.enabled; + enabled && this.disable(); + this.mapping = this._createMapping(); + enabled && this.enable(); + }, + updateOptions: function(options) { + return _updateOptions(this.options, options || {}, this.constructor.name); + }, + applyOptions: function(options, applyNow) { + if (this.updateOptions(options) && applyNow) { + this.reset(); + } + }, + onShiftKey: function onShiftKey(value, key) { + var event = { + type: value ? 'keypress' : 'keyrelease', + keyboardKey: key, + keyboardText: 'SHIFT', + keyboardValue: value, + actionName: 'Shift', + actionValue: !!value, + value: !!value, + at: +new Date + }; + this.virtualActionEvent('Keyboard.Shift', event); + }, + onRightMouseButton: function onRightMouseButton(value, key) { + var event = { + type: value ? 'mousepress' : 'mouserelease', + keyboardKey: key, + keyboardValue: value, + actionName: 'RightMouseButton', + actionValue: !!value, + value: !!value, + at: +new Date + }; + this.virtualActionEvent('Keyboard.RightMouseButton', event); + }, + onApplicationEvent: function _onApplicationEvent(key, name, value) { + var event = { + type: 'application', + actionName: 'Application.' + name, + applicationKey: key, + applicationName: name, + applicationValue: value, + actionValue: !!value, + value: !!value + }; + this.virtualActionEvent('Application.' + name, event); + }, + _createMapping: function() { + this._mapping = this._getTemplate(); + var mappingJSON = JSON.stringify(this._mapping, 0, 2); + var mapping = Controller.parseMapping(mappingJSON); + debugPrint(mappingJSON); + mapping.name = mapping.name || this._mapping.name; + + mapping.from(Controller.Hardware.Keyboard.Shift).peek().to(_utils.bind(this, 'onShiftKey')); + mapping.from(Controller.Hardware.Keyboard.RightMouseButton).peek().to(_utils.bind(this, 'onRightMouseButton')); + + var boundApplicationHandler = _utils.bind(this, 'onApplicationEvent'); + Object.keys(Controller.Hardware.Application).forEach(function(name) { + var key = Controller.Hardware.Application[name]; + debugPrint('observing Controller.Hardware.Application.'+ name, key); + mapping.from(key).to(function(value) { + boundApplicationHandler(key, name, value); + }); + }); + + return mapping; + }, + _getTemplate: function() { + assert(this.options.controllerMapping, 'MovementMapping._getTemplate -- !this.options.controllerMapping'); + var template = JSON.parse(JSON.stringify(this.options.controllerMapping)); // make a local copy + template.name = this.namespace; + template.channels = template.channels.filter(function(item) { + // ignore any "JSON comment" or other bindings without a from spec + return item.from && item.from.makeAxis; + }); + var exclude = Array.isArray(this.options.excludeNames) ? this.options.excludeNames : []; + if (!this.options.mouseSmooth) { + exclude.push('Keyboard.RightMouseButton'); + } + + log('EXCLUSIONS:' + exclude); + + template.channels = template.channels.filter(_utils.bind(this, function(item, i) { + debugPrint('channel['+i+']', item.from && item.from.makeAxis, item.to, JSON.stringify(item.filters) || ''); + // var hasFilters = Array.isArray(item.filters) && !item.filters[1]; + item.filters = Array.isArray(item.filters) ? item.filters : + typeof item.filters === 'string' ? [ { type: item.filters }] : [ item.filters ]; + + if (/Mouse/.test(item.from && item.from.makeAxis)) { + item.filters.push({ type: 'scale', scale: this.options.mouseMultiplier }); + log('applied mouse multiplier:', item.from.makeAxis, item.when, item.to, this.options.mouseMultiplier); + } else if (/Keyboard/.test(item.from && item.from.makeAxis)) { + item.filters.push({ type: 'scale', scale: this.options.keyboardMultiplier }); + log('applied keyboard multiplier:', item.from.makeAxis, item.when, item.to, this.options.keyboardMultiplier); + } + item.filters = item.filters.filter(Boolean); + if (~exclude.indexOf(item.to)) { + log('EXCLUDING item.to === ' + item.to); + return false; + } + var when = Array.isArray(item.when) ? item.when : [item.when]; + for (var j=0; j < when.length; j++) { + if (~exclude.indexOf(when[j])) { + log('EXCLUDING item.when contains ' + when[j]); + return false; + } + } + function shouldInclude(p, i) { + if (~exclude.indexOf(p)) { + log('EXCLUDING from.makeAxis[][' + i + '] === ' + p); + return false; + } + return true; + } + + if (item.from && Array.isArray(item.from.makeAxis)) { + var makeAxis = item.from.makeAxis; + item.from.makeAxis = makeAxis.map(function(axis) { + if (Array.isArray(axis)) { + return axis.filter(shouldInclude); + } else { + return shouldInclude(axis, -1) && axis; + } + }).filter(Boolean); + } + return true; + })); + debugPrint(JSON.stringify(template,0,2)); + return template; + } +}; // MovementMapping.prototype + +// update target properties from source, but iff the property already exists in target +function _updateOptions(target, source, debugName) { + debugName = debugName || '_updateOptions'; + var changed = 0; + if (!source || typeof source !== 'object') { + return changed; + } + for (var p in target) { + if (p in source && target[p] !== source[p]) { + log(debugName, 'updating source.'+p, target[p] + ' -> ' + source[p]); + target[p] = source[p]; + changed++; + } + } + for (p in source) { + (!(p in target)) && log(debugName, 'warning: ignoring unknown option:', p, (source[p] +'').substr(0, 40)+'...'); + } + return changed; +} + +// ---------------------------------------------------------------------------- +function calculateThrust(maxVelocity, targetVelocity, previousThrust) { + var THRUST_FALLOFF = 0.1; // round to ZERO if component is below this threshold + // Note: MyAvatar.setThrust might need an update to account for the recent avatar density changes... + // For now, this discovered scaling factor seems to accomodate a similar easing effect to the other movement models. + var magicScalingFactor = 12.0 * (maxVelocity + 120) / 16 - Math.sqrt( maxVelocity / 8 ); + + var targetThrust = Vec3.multiply(magicScalingFactor, targetVelocity); + targetThrust = vec3eclamp(targetThrust, THRUST_FALLOFF, maxVelocity); + if (Vec3.length(MyAvatar.velocity) > maxVelocity) { + targetThrust = Vec3.multiply(0.5, targetThrust); + } + return targetThrust; +} + +// ---------------------------------------------------------------------------- +// clamp components and magnitude to maxVelocity, rounding to Vec3.ZERO if below epsilon +function vec3eclamp(velocity, epsilon, maxVelocity) { + velocity = { + x: Math.max(-maxVelocity, Math.min(maxVelocity, velocity.x)), + y: Math.max(-maxVelocity, Math.min(maxVelocity, velocity.y)), + z: Math.max(-maxVelocity, Math.min(maxVelocity, velocity.z)) + }; + + if (Math.abs(velocity.x) < epsilon) { + velocity.x = 0; + } + if (Math.abs(velocity.y) < epsilon) { + velocity.y = 0; + } + if (Math.abs(velocity.z) < epsilon) { + velocity.z = 0; + } + + var length = Vec3.length(velocity); + if (length > maxVelocity) { + velocity = Vec3.multiply(maxVelocity, Vec3.normalize(velocity)); + } else if (length < epsilon) { + velocity = Vec3.ZERO; + } + return velocity; +} + +function vec3damp(active, positiveEffect, negativeEffect) { + // If force isn't being applied in a direction, incorporate negative effect (drag); + negativeEffect = { + x: active.x ? 0 : negativeEffect.x, + y: active.y ? 0 : negativeEffect.y, + z: active.z ? 0 : negativeEffect.z, + }; + return Vec3.subtract(Vec3.sum(active, positiveEffect), negativeEffect); +} + +// ---------------------------------------------------------------------------- +function VelocityTracker(defaultValues) { + Object.defineProperty(this, 'defaultValues', { configurable: true, value: defaultValues }); +} +VelocityTracker.prototype = { + constructor: VelocityTracker, + reset: function() { + Object.assign(this, this.defaultValues); + }, + integrate: function(targetState, currentVelocities, drag, settings) { + var args = [].slice.call(arguments); + this._applyIntegration('translation', args); + this._applyIntegration('rotation', args); + this._applyIntegration('zoom', args); + }, + _applyIntegration: function(component, args) { + return this._integrate.apply(this, [component].concat(args)); + }, + _integrate: function(component, targetState, currentVelocities, drag, settings) { + assert(targetState[component], component + ' not found in targetState (which has: ' + Object.keys(targetState) + ')'); + var result = vec3damp( + targetState[component], + currentVelocities[component], + drag[component] + ); + var maxVelocity = settings[component].maxVelocity; + return this[component] = vec3eclamp(result, settings.epsilon, maxVelocity); + }, +}; + +// ---------------------------------------------------------------------------- +// ---------------------------------------------------------------------------- +Object.assign(CameraControls, { + SCRIPT_UPDATE: 'update', + ANIMATION_FRAME: 'requestAnimationFrame', // emulated + NEXT_TICK: 'nextTick', // emulated + SET_IMMEDIATE: 'setImmediate', // emulated + //WORKER_THREAD: 'workerThread', +}); + +function CameraControls(options) { + options = options || {}; + assert('update' in options && 'threadMode' in options); + this.updateObject = typeof options.update === 'function' ? options : options.update; + assert(typeof this.updateObject.update === 'function', + 'construction options expected either { update: function(){}... } object or a function(){}'); + this.update = _utils.bind(this.updateObject, 'update'); + this.threadMode = options.threadMode; + this.fps = options.fps || 60; + this.getRuntimeSeconds = options.getRuntimeSeconds || function() { + return +new Date / 1000.0; + }; + this.backupOptions = _utils.DeferredUpdater.createGroup({ + MyAvatar: MyAvatar, + Camera: Camera, + Reticle: Reticle, + }); + + this.enabled = false; + this.enabledChanged = _utils.signal(function enabledChanged(enabled){}); + this.modeChanged = _utils.signal(function modeChanged(mode, oldMode){}); +} +CameraControls.prototype = { + constructor: CameraControls, + $animate: null, + $start: function() { + if (this.$animate) { + return; + } + + var lastTime; + switch (this.threadMode) { + case CameraControls.SCRIPT_UPDATE: { + this.$animate = this.update; + Script.update.connect(this, '$animate'); + this.$animate.disconnect = _utils.bind(this, function() { + Script.update.disconnect(this, '$animate'); + }); + } break; + + case CameraControls.ANIMATION_FRAME: { + this.requestAnimationFrame = _utils.createAnimationStepper({ + getRuntimeSeconds: this.getRuntimeSeconds, + fps: this.fps + }); + this.$animate = _utils.bind(this, function(dt) { + this.update(dt); + this.requestAnimationFrame(this.$animate); + }); + this.$animate.disconnect = _utils.bind(this.requestAnimationFrame, 'reset'); + this.requestAnimationFrame(this.$animate); + } break; + + case CameraControls.SET_IMMEDIATE: { + // emulate process.setImmediate (attempt to execute at start of next update frame, sans Script.update throttling) + lastTime = this.getRuntimeSeconds(); + this.$animate = Script.setInterval(_utils.bind(this, function() { + this.update(this.getRuntimeSeconds(lastTime)); + lastTime = this.getRuntimeSeconds(); + }), 5); + this.$animate.disconnect = function() { + Script.clearInterval(this); + }; + } break; + + case CameraControls.NEXT_TICK: { + // emulate process.nextTick (attempt to queue at the very next opportunity beyond current scope) + lastTime = this.getRuntimeSeconds(); + this.$animate = _utils.bind(this, function() { + this.$animate.timeout = 0; + if (this.$animate.quit) { + return; + } + this.update(this.getRuntimeSeconds(lastTime)); + lastTime = this.getRuntimeSeconds(); + this.$animate.timeout = Script.setTimeout(this.$animate, 0); + }); + this.$animate.quit = false; + this.$animate.disconnect = function() { + this.timeout && Script.clearTimeout(this.timeout); + this.timeout = 0; + this.quit = true; + }; + this.$animate(); + } break; + + default: throw new Error('unknown threadMode: ' + this.threadMode); + } + log( + '...$started update thread', '(threadMode: ' + this.threadMode + ')', + this.threadMode === CameraControls.ANIMATION_FRAME && this.fps + ); + }, + $stop: function() { + if (!this.$animate) { + return; + } + try { + this.$animate.disconnect(); + } catch (e) { + log('$animate.disconnect error: ' + e, '(threadMode: ' + this.threadMode +')'); + } + this.$animate = null; + log('...$stopped updated thread', '(threadMode: ' + this.threadMode +')'); + }, + onModeUpdated: function onModeUpdated(mode, oldMode) { + oldMode = oldMode || this.previousMode; + this.previousMode = mode; + log('onModeUpdated', oldMode + '->' + mode); + // user changed modes, so leave the current mode intact later when restoring backup values + delete this.backupOptions.Camera.$setModeString; + if (/person/.test(oldMode) && /person/.test(mode)) { + return; // disregard first -> third and third ->first transitions + } + this.modeChanged(mode, oldMode); + }, + + reset: function() { + if (this.enabled) { + this.disable(); + this.enable(); + } + }, + setEnabled: function setEnabled(enabled) { + if (!this.enabled && enabled) { + this.enable(); + } else if (this.enabled && !enabled) { + this.disable(); + } + }, + enable: function enable() { + if (this.enabled) { + throw new Error('CameraControls.enable -- already enabled..'); + } + log('ENABLE enableCameraMove', this.threadMode); + + this._backup(); + + this.previousMode = Camera.mode; + Camera.modeUpdated.connect(this, 'onModeUpdated'); + + this.$start(); + + this.enabledChanged(this.enabled = true); + }, + disable: function disable() { + log("DISABLE CameraControls"); + try { + Camera.modeUpdated.disconnect(this, 'onModeUpdated'); + } catch (e) { + debugPrint(e); + } + this.$stop(); + + this._restore(); + + if (this.enabled !== false) { + this.enabledChanged(this.enabled = false); + } + }, + _restore: function() { + var submitted = this.backupOptions.submit(); + log('restored previous values: ' + JSON.stringify(submitted,0,2)); + return submitted; + }, + _backup: function() { + this.backupOptions.reset(); + Object.assign(this.backupOptions.Reticle, { + scale: Reticle.scale, + }); + Object.assign(this.backupOptions.Camera, { + $setModeString: Camera.mode, + }); + Object.assign(this.backupOptions.MyAvatar, { + motorTimescale: MyAvatar.motorTimescale, + motorReferenceFrame: MyAvatar.motorReferenceFrame, + motorVelocity: Vec3.ZERO, + velocity: Vec3.ZERO, + angularVelocity: Vec3.ZERO, + }); + }, +}; // CameraControls + +// ---------------------------------------------------------------------------- +function applyEasing(deltaTime, direction, settings, state, scaling) { + var obj = {}; + for (var p in scaling) { + var group = settings[p], // translation | rotation | zoom + easeConst = group[direction], // easeIn | easeOut + scale = scaling[p], + stateVector = state[p]; + obj[p] = Vec3.multiply(easeConst * scale * deltaTime, stateVector); + } + return obj; +} diff --git a/unpublishedScripts/marketplace/camera-move/modules/movement-utils.mapping.json b/unpublishedScripts/marketplace/camera-move/modules/movement-utils.mapping.json new file mode 100644 index 0000000000..ec915a9fad --- /dev/null +++ b/unpublishedScripts/marketplace/camera-move/modules/movement-utils.mapping.json @@ -0,0 +1,98 @@ +{ + "name": "app-camera-move", + "channels": [ + + { "comment": "------------------ Actions.TranslateX -------------------" }, + { + "from": {"makeAxis": [["Keyboard.Left"],["Keyboard.Right"]]}, + "when": "Keyboard.Shift", + "to": "Actions.TranslateX" + }, + { + "from": {"makeAxis": [["Keyboard.A","Keyboard.TouchpadLeft"],["Keyboard.D","Keyboard.TouchpadRight"]]}, + "when": "Keyboard.Shift", + "to": "Actions.TranslateX" + }, + + { "comment": "------------------ Actions.TranslateY -------------------" }, + { + "from": {"makeAxis": [["Keyboard.C","Keyboard.PgDown"],["Keyboard.E","Keyboard.PgUp"]]}, + "when": "!Keyboard.Shift", + "to": "Actions.TranslateY" + }, + + { "comment": "------------------ Actions.TranslateZ -------------------" }, + { + "from": {"makeAxis": [["Keyboard.W"],["Keyboard.S"]]}, + "when": "!Keyboard.Shift", + "to": "Actions.TranslateZ" + }, + { + "from": {"makeAxis": [["Keyboard.Up"],["Keyboard.Down"]]}, + "when": "!Keyboard.Shift", + "to": "Actions.TranslateZ" + }, + + { "comment": "------------------ Actions.Yaw -------------------" }, + { + "from": {"makeAxis": [["Keyboard.A","Keyboard.TouchpadLeft"],["Keyboard.D","Keyboard.TouchpadRight"]]}, + "when": "!Keyboard.Shift", + "to": "Actions.Yaw", + "filters": ["invert"] + }, + { + "from": {"makeAxis": [["Keyboard.MouseMoveLeft"],["Keyboard.MouseMoveRight"]]}, + "when": "Keyboard.RightMouseButton", + "to": "Actions.Yaw", + "filters": ["invert"] + }, + { + "from": {"makeAxis": [["Keyboard.Left"],["Keyboard.Right"]]}, + "when": "!Keyboard.Shift", + "to": "Actions.Yaw", + "filters": ["invert"] + }, + { + "from": {"makeAxis": [["Keyboard.Left"],["Keyboard.Right"]]}, + "when": ["Application.SnapTurn", "!Keyboard.Shift"], + "to": "Actions.StepYaw", + "filters": + [ + "invert", + { "type": "pulse", "interval": 0.5, "resetOnZero": true }, + { "type": "scale", "scale": 22.5 } + ] + }, + + { "comment": "------------------ Actions.Pitch -------------------" }, + { + "from": {"makeAxis": [["Keyboard.W"],["Keyboard.S"]]}, + "when": "Keyboard.Shift", + "to": "Actions.Pitch", + "filters": ["invert"] + }, + { + "from": {"makeAxis": [["Keyboard.MouseMoveUp"],["Keyboard.MouseMoveDown"]]}, + "when": "Keyboard.RightMouseButton", + "to": "Actions.Pitch", + "filters": ["invert"] + }, + { + "from": {"makeAxis": [["Keyboard.Up"],["Keyboard.Down"]]}, + "when": "Keyboard.Shift", + "to": "Actions.Pitch", + "filters": ["invert"] + }, + + { "comment": "------------------ Actions.BoomIn -------------------" }, + { + "from": {"makeAxis": [["Keyboard.C","Keyboard.PgDown"],["Keyboard.E","Keyboard.PgUp"]]}, + "when": "Keyboard.Shift", + "to": "Actions.BoomIn", + "filters": [{"type": "scale","scale": 0.005}] + }, + + { "comment": "------------------ end -------------------" } + + ] +} diff --git a/unpublishedScripts/marketplace/dart/createDart.js b/unpublishedScripts/marketplace/dart/createDart.js new file mode 100644 index 0000000000..ff13a695a8 --- /dev/null +++ b/unpublishedScripts/marketplace/dart/createDart.js @@ -0,0 +1,91 @@ +"use strict"; +// +// createDart.js +// +// Created by MrRoboman on 17/05/04 +// Copyright 2017 High Fidelity, Inc. +// +// Creates five throwing darts. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html + +var MODEL_URL = "https://hifi-content.s3.amazonaws.com/wadewatts/dart.fbx"; +var SCRIPT_URL = Script.resolvePath("./dart.js?v=" + Date.now()); +var START_POSITION = Vec3.sum(MyAvatar.position, Vec3.multiply(Quat.getFront(MyAvatar.orientation), 0.75)); +var START_ROTATION = MyAvatar.orientation +var SCALE_FACTOR = 1; + +var dart = { + type: "Model", + shapeType: "box", + name: "Dart", + description: "Throw it at something!", + script: SCRIPT_URL, + modelURL: MODEL_URL, + position: START_POSITION, + rotation: START_ROTATION, + lifetime: 300, + gravity: { + x: 0, + y: -9.8, + z: 0 + }, + dimensions: { + x: 0.1, + y: 0.1, + z: 0.3 + }, + dynamic: true, + owningAvatarID: MyAvatar.sessionUUID, + userData: JSON.stringify({ + grabbableKey: { + grabbable: true, + invertSolidWhileHeld: true, + ignoreIK: false + } + }) +}; + +var avatarUp = Quat.getUp(MyAvatar.orientation); +var platformPosition = Vec3.sum(START_POSITION, Vec3.multiply(avatarUp, -0.05)); +var platform = { + type: "Box", + name: "Dart Platform", + description: "Holds darts", + position: platformPosition, + rotation: START_ROTATION, + lifetime: 60, + dimensions: { + x: 0.15 * 5, + y: 0.01, + z: 0.3 + }, + color: { + red: 129, + green: 92, + blue: 11 + }, + owningAvatarID: MyAvatar.sessionUUID, + userData: JSON.stringify({ + grabbableKey: { + grabbable: true, + invertSolidWhileHeld: true, + ignoreIK: false + } + }) +}; + + +Entities.addEntity(platform); + +var dartCount = 5; +var avatarRight = Quat.getRight(MyAvatar.orientation); +for (var i = 0; i < dartCount; i++) { + var j = i - Math.floor(dartCount / 2); + var position = Vec3.sum(START_POSITION, Vec3.multiply(avatarRight, 0.15 * j)); + dart.position = position; + Entities.addEntity(dart); +} + +Script.stop(); diff --git a/unpublishedScripts/marketplace/dart/dart.js b/unpublishedScripts/marketplace/dart/dart.js new file mode 100644 index 0000000000..9debe910e3 --- /dev/null +++ b/unpublishedScripts/marketplace/dart/dart.js @@ -0,0 +1,81 @@ +"use strict"; +// +// dart.js +// +// Created by MrRoboman on 17/05/13 +// Copyright 2017 High Fidelity, Inc. +// +// Simple throwing dart. Sticks to static objects. +// +// 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 THROW_FACTOR = 3; + var DART_SOUND_URL = Script.resolvePath('https://hifi-content.s3.amazonaws.com/wadewatts/dart.wav?v=' + Date.now()); + + var Dart = function() {}; + + Dart.prototype = { + + preload: function(entityID) { + this.entityID = entityID; + this.actionID = null; + this.soundHasPlayed = true; + this.dartSound = SoundCache.getSound(DART_SOUND_URL); + }, + + playDartSound: function(sound) { + if (this.soundHasPlayed) { + return; + } + this.soundHasPlayed = true; + var position = Entities.getEntityProperties(this.entityID, 'position').position; + var audioProperties = { + volume: 0.15, + position: position + }; + Audio.playSound(this.dartSound, audioProperties); + }, + + releaseGrab: function() { + this.soundHasPlayed = false; + var velocity = Entities.getEntityProperties(this.entityID, 'velocity').velocity; + + var newVelocity = {}; + Object.keys(velocity).forEach(function(key) { + newVelocity[key] = velocity[key] * THROW_FACTOR; + }); + + Entities.editEntity(this.entityID, { + velocity: newVelocity + }); + + if (this.actionID) { + Entities.deleteAction(this.entityID, this.actionID); + } + this.actionID = Entities.addAction("travel-oriented", this.entityID, { + forward: { x: 0, y: 0, z: 1 }, + angularTimeScale: 0.1, + tag: "throwing dart", + ttl: 3600 + }); + }, + + collisionWithEntity: function(myID, otherID, collisionInfo) { + this.playDartSound(); + + Entities.editEntity(myID, { + velocity: {x: 0, y: 0, z: 0}, + angularVelocity: {x: 0, y: 0, z: 0} + }); + + if (this.actionID) { + Entities.deleteAction(myID, this.actionID); + this.actionID = null; + } + } + }; + + return new Dart(); +}); diff --git a/unpublishedScripts/marketplace/doppleganger-attachments/app-doppleganger-attachments.js b/unpublishedScripts/marketplace/doppleganger-attachments/app-doppleganger-attachments.js index 4617cf47b6..c09ad602f8 100644 --- a/unpublishedScripts/marketplace/doppleganger-attachments/app-doppleganger-attachments.js +++ b/unpublishedScripts/marketplace/doppleganger-attachments/app-doppleganger-attachments.js @@ -9,21 +9,44 @@ // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // +/* eslint-env commonjs */ +/* global DriveKeys, require:true, console */ +/* eslint-disable comma-dangle */ -var require = Script.require; +// decomment next line for automatic require cache-busting +// var require = function require(id) { return Script.require(id + '?'+new Date().getTime().toString(36)); }; +if (typeof require !== 'function') { + require = Script.require; +} +var VERSION = '0.0.0'; var WANT_DEBUG = false; +var DEBUG_MOVE_AS_YOU_MOVE = false; +var ROTATE_AS_YOU_MOVE = false; + +log(VERSION); var DopplegangerClass = require('./doppleganger.js'), DopplegangerAttachments = require('./doppleganger-attachments.js'), - modelHelper = require('./model-helper.js').modelHelper; + DebugControls = require('./doppleganger-debug.js'), + modelHelper = require('./model-helper.js').modelHelper, + utils = require('./utils.js'); + +// eslint-disable-next-line camelcase +var isWebpack = typeof __webpack_require__ === 'function'; + +var buttonConfig = utils.assign({ + text: 'MIRROR', +}, !isWebpack ? { + icon: Script.resolvePath('./doppleganger-i.svg'), + activeIcon: Script.resolvePath('./doppleganger-a.svg'), +} : { + icon: require('./doppleganger-i.svg.json'), + activeIcon: require('./doppleganger-a.svg.json'), +}); var tablet = Tablet.getTablet('com.highfidelity.interface.tablet.system'), - button = tablet.addButton({ - icon: Script.resolvePath('./doppleganger-i.svg'), - activeIcon: Script.resolvePath('./doppleganger-a.svg'), - text: 'MIRROR' - }); + button = tablet.addButton(buttonConfig); Script.scriptEnding.connect(function() { tablet.removeButton(button); @@ -65,6 +88,59 @@ var doppleganger = new DopplegangerClass({ } } +// add support for "move as you move" +{ + var movementKeys = 'W,A,S,D,Up,Down,Right,Left'.split(','); + var controllerKeys = 'LX,LY,RY'.split(','); + var translationKeys = Object.keys(DriveKeys).filter(function(p) { + return /translate/i.test(p); + }); + var startingPosition; + + // returns an array of any active driving keys (eg: ['W', 'TRANSLATE_Z']) + function currentDrivers() { + return [].concat( + movementKeys.map(function(key) { + return Controller.getValue(Controller.Hardware.Keyboard[key]) && key; + }) + ).concat( + controllerKeys.map(function(key) { + return Controller.getValue(Controller.Standard[key]) !== 0.0 && key; + }) + ).concat( + translationKeys.map(function(key) { + return MyAvatar.getRawDriveKey(DriveKeys[key]) !== 0.0 && key; + }) + ).filter(Boolean); + } + + doppleganger.jointsUpdated.connect(function(objectID) { + var drivers = currentDrivers(), + isDriving = drivers.length > 0; + if (isDriving) { + if (startingPosition) { + debugPrint('resetting startingPosition since drivers == ', drivers.join('|')); + startingPosition = null; + } + } else if (HMD.active || DEBUG_MOVE_AS_YOU_MOVE) { + startingPosition = startingPosition || MyAvatar.position; + var movement = Vec3.subtract(MyAvatar.position, startingPosition); + startingPosition = MyAvatar.position; + // Vec3.length(movement) > 0.0001 && Vec3.print('+avatarMovement', movement); + + // "mirror" the relative translation vector + movement.x *= -1; + movement.z *= -1; + var props = {}; + props.position = doppleganger.position = Vec3.sum(doppleganger.position, movement); + if (ROTATE_AS_YOU_MOVE) { + props.rotation = doppleganger.orientation = MyAvatar.orientation; + } + modelHelper.editObject(doppleganger.objectID, props); + } + }); +} + // hide the doppleganger if this client script is unloaded Script.scriptEnding.connect(doppleganger, 'stop'); @@ -103,15 +179,21 @@ doppleganger.modelLoaded.connect(function(error, result) { // add debug indicators, but only if the user has configured the settings value if (Settings.getValue('debug.doppleganger', false)) { - WANT_DEBUG = true; - DopplegangerClass.addDebugControls(doppleganger); + WANT_DEBUG = WANT_DEBUG || true; + DopplegangerClass.WANT_DEBUG = WANT_DEBUG; + DopplegangerAttachments.WANT_DEBUG = WANT_DEBUG; + new DebugControls(doppleganger); +} + +function log() { + // eslint-disable-next-line no-console + (typeof console === 'object' ? console.log : print)('app-doppleganger | ' + [].slice.call(arguments).join(' ')); } function debugPrint() { - if (WANT_DEBUG) { - print('app-doppleganger | ' + [].slice.call(arguments).join(' ')); - } + WANT_DEBUG && log.apply(this, arguments); } + // ---------------------------------------------------------------------------- UserActivityLogger.logAction('doppleganger_app_load'); diff --git a/unpublishedScripts/marketplace/doppleganger-attachments/dist/app-doppleganger-marketplace.js b/unpublishedScripts/marketplace/doppleganger-attachments/dist/app-doppleganger-marketplace.js new file mode 100644 index 0000000000..c2cf2a2353 --- /dev/null +++ b/unpublishedScripts/marketplace/doppleganger-attachments/dist/app-doppleganger-marketplace.js @@ -0,0 +1,1500 @@ +/******/ (function(modules) { // webpackBootstrap +/******/ // The module cache +/******/ var installedModules = {}; +/******/ +/******/ // The require function +/******/ function __webpack_require__(moduleId) { +/******/ +/******/ // Check if module is in cache +/******/ if(installedModules[moduleId]) { +/******/ return installedModules[moduleId].exports; +/******/ } +/******/ // Create a new module (and put it into the cache) +/******/ var module = installedModules[moduleId] = { +/******/ i: moduleId, +/******/ l: false, +/******/ exports: {} +/******/ }; +/******/ +/******/ // Execute the module function +/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); +/******/ +/******/ // Flag the module as loaded +/******/ module.l = true; +/******/ +/******/ // Return the exports of the module +/******/ return module.exports; +/******/ } +/******/ +/******/ +/******/ // expose the modules object (__webpack_modules__) +/******/ __webpack_require__.m = modules; +/******/ +/******/ // expose the module cache +/******/ __webpack_require__.c = installedModules; +/******/ +/******/ // define getter function for harmony exports +/******/ __webpack_require__.d = function(exports, name, getter) { +/******/ if(!__webpack_require__.o(exports, name)) { +/******/ Object.defineProperty(exports, name, { +/******/ configurable: false, +/******/ enumerable: true, +/******/ get: getter +/******/ }); +/******/ } +/******/ }; +/******/ +/******/ // getDefaultExport function for compatibility with non-harmony modules +/******/ __webpack_require__.n = function(module) { +/******/ var getter = module && module.__esModule ? +/******/ function getDefault() { return module['default']; } : +/******/ function getModuleExports() { return module; }; +/******/ __webpack_require__.d(getter, 'a', getter); +/******/ return getter; +/******/ }; +/******/ +/******/ // Object.prototype.hasOwnProperty.call +/******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); }; +/******/ +/******/ // __webpack_public_path__ +/******/ __webpack_require__.p = ""; +/******/ +/******/ // Load entry module and return exports +/******/ return __webpack_require__(__webpack_require__.s = 3); +/******/ }) +/************************************************************************/ +/******/ ([ +/* 0 */ +/***/ (function(module, exports) { + +/* eslint-env commonjs */ +/* global console */ + +module.exports = { + version: '0.0.1', + bind: bind, + signal: signal, + assign: assign, + assert: assert +}; + +function log() { + // eslint-disable-next-line no-console + (typeof console === 'object' ? console.log : print)('utils | ' + [].slice.call(arguments).join(' ')); +} +log(module.exports.version); + +// @function - bind a function to a `this` context +// @param {Object} - the `this` context +// @param {Function|String} - function or method name +// @param {value} varargs... - optional curry-right arguments (passed to method after any explicit arguments) +function bind(thiz, method, varargs) { + method = thiz[method] || method; + varargs = [].slice.call(arguments, 2); + return function() { + var args = [].slice.call(arguments).concat(varargs); + return method.apply(thiz, args); + }; +} + +// @function - Qt signal polyfill +function signal(template) { + var callbacks = []; + return Object.defineProperties(function() { + var args = [].slice.call(arguments); + callbacks.forEach(function(obj) { + obj.handler.apply(obj.scope, args); + }); + }, { + connect: { value: function(scope, handler) { + var callback = {scope: scope, handler: scope[handler] || handler || scope}; + if (!callback.handler || !callback.handler.apply) { + throw new Error('invalid arguments to connect:' + [template, scope, handler]); + } + callbacks.push({scope: scope, handler: scope[handler] || handler || scope}); + }}, + disconnect: { value: function(scope, handler) { + var match = {scope: scope, handler: scope[handler] || handler || scope}; + callbacks = callbacks.filter(function(obj) { + return !(obj.scope === match.scope && obj.handler === match.handler); + }); + }} + }); +} + +// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign#Polyfill +/* eslint-disable */ +function assign(target, varArgs) { // .length of function is 2 + 'use strict'; + if (target == null) { // TypeError if undefined or 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) { // Skip over if undefined or null + for (var nextKey in nextSource) { + // Avoid bugs when hasOwnProperty is shadowed + if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) { + to[nextKey] = nextSource[nextKey]; + } + } + } + } + return to; +} +/* eslint-enable */ +// //https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign#Polyfill + +// examples: +// assert(function assertion() { return (conditions === true) }, 'assertion failed!') +// var neededValue = assert(idString, 'idString not specified!'); +// assert(false, 'unexpected state'); +function assert(truthy, message) { + message = message || 'Assertion Failed:'; + + if (typeof truthy === 'function' && truthy.name === 'assertion') { + // extract function body to display with the assertion message + var assertion = (truthy+'').replace(/[\r\n]/g, ' ') + .replace(/^[^{]+\{|\}$|^\s*|\s*$/g, '').trim() + .replace(/^return /,'').replace(/\s[\r\n\t\s]+/g, ' '); + message += ' ' + JSON.stringify(assertion); + try { + truthy = truthy(); + } catch (e) { + message += '(exception: ' + e +')'; + } + } + if (!truthy) { + message += ' ('+truthy+')'; + throw new Error(message); + } + return truthy; +} + + +/***/ }), +/* 1 */ +/***/ (function(module, exports, __webpack_require__) { + +// model-helper.js +// +// Created by Timothy Dedischew on 06/01/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 +// + +/* eslint-env commonjs */ +/* global console */ +// @module model-helper +// +// This module provides ModelReadyWatcher (a helper class for knowing when a model becomes usable inworld) and +// also initial plumbing helpers to eliminate unnecessary API differences when working with Model Overlays and +// Model Entities at a high-level from scripting. + +var utils = __webpack_require__(0), + assert = utils.assert; + +module.exports = { + version: '0.0.1', + ModelReadyWatcher: ModelReadyWatcher +}; + +function log() { + // eslint-disable-next-line no-console + (typeof console === 'object' ? console.log : print)('model-helper | ' + [].slice.call(arguments).join(' ')); +} +log(module.exports.version); + +var _objectDeleted = utils.signal(function objectDeleted(objectID){}); +// proxy for _objectDeleted that only binds deletion tracking if script actually connects to the unified signal +var objectDeleted = utils.assign(function objectDeleted(objectID){}, { + connect: function() { + Overlays.overlayDeleted.connect(_objectDeleted); + // Entities.deletingEntity.connect(objectDeleted); + Script.scriptEnding.connect(function() { + Overlays.overlayDeleted.disconnect(_objectDeleted); + // Entities.deletingEntity.disconnect(objectDeleted); + }); + // hereafter _objectDeleted.connect will be used instead + this.connect = utils.bind(_objectDeleted, 'connect'); + return this.connect.apply(this, arguments); + }, + disconnect: utils.bind(_objectDeleted, 'disconnect') +}); + +var modelHelper = module.exports.modelHelper = { + // Entity <-> Overlay property translations + _entityFromOverlay: { + modelURL: function url() { + return this.url; + }, + dimensions: function dimensions() { + return Vec3.multiply(this.scale, this.naturalDimensions); + } + }, + _overlayFromEntity: { + url: function modelURL() { + return this.modelURL; + }, + scale: function scale() { + return this.dimensions && this.naturalDimensions && { + x: this.dimensions.x / this.naturalDimensions.x, + y: this.dimensions.y / this.naturalDimensions.y, + z: this.dimensions.z / this.naturalDimensions.z + }; + } + }, + objectDeleted: objectDeleted, + type: function(objectID) { + // TODO: support Model Entities (by detecting type from objectID, which is already possible) + return !Uuid.isNull(objectID) ? 'overlay' : null; + }, + addObject: function(properties) { + var type = 'overlay'; // this.resolveType(properties) + switch (type) { + case 'overlay': return Overlays.addOverlay(properties.type, this.toOverlayProps(properties)); + } + return false; + }, + editObject: function(objectID, properties) { + switch (this.type(objectID)) { + case 'overlay': return Overlays.editOverlay(objectID, this.toOverlayProps(properties)); + } + return false; + }, + deleteObject: function(objectID) { + return this.type(objectID) === 'overlay' && Overlays.deleteOverlay(objectID); + }, + getProperty: function(objectID, propertyName) { + return this.type(objectID) === 'overlay' && Overlays.getProperty(objectID, propertyName); + }, + getProperties: function(objectID, filter) { + switch (this.type(objectID)) { + case 'overlay': + filter = Array.isArray(filter) ? filter : [ + 'position', 'rotation', 'localPosition', 'localRotation', 'parentID', + 'parentJointIndex', 'scale', 'name', 'visible', 'type', 'url', + 'dimensions', 'naturalDimensions', 'grabbable' + ]; + var properties = filter.reduce(function(out, propertyName) { + out[propertyName] = Overlays.getProperty(objectID, propertyName); + return out; + }, {}); + return this.toEntityProps(properties); + } + return null; + }, + // adapt Entity conventions to Overlay (eg: { modelURL: ... } -> { url: ... }) + toOverlayProps: function(properties) { + var result = {}; + for (var from in this._overlayFromEntity) { + var adapter = this._overlayFromEntity[from]; + result[from] = adapter.call(properties, from, adapter.name); + } + return utils.assign(result, properties); + }, + // adapt Overlay conventions to Entity (eg: { url: ... } -> { modelURL: ... }) + toEntityProps: function(properties) { + var result = {}; + for (var from in this._entityToOverlay) { + var adapter = this._entityToOverlay[from]; + result[from] = adapter.call(properties, from, adapter.name); + } + return utils.assign(result, properties); + }, + editObjects: function(updatedObjects) { + var objectIDs = Object.keys(updatedObjects), + type = this.type(objectIDs[0]); + switch (type) { + case 'overlay': + var translated = {}; + for (var objectID in updatedObjects) { + translated[objectID] = this.toOverlayProps(updatedObjects[objectID]); + } + return Overlays.editOverlays(translated); + } + return false; + }, + getJointIndex: function(objectID, name) { + switch (this.type(objectID)) { + case 'overlay': return Overlays.getProperty(objectID, 'jointNames').indexOf(name); + } + return -1; + }, + getJointNames: function(objectID) { + switch (this.type(objectID)) { + case 'overlay': return Overlays.getProperty(objectID, 'jointNames'); + } + return []; + }, + // @function - derives mirrored joint names from a list of regular joint names + // @param {Array} - list of joint names to mirror + // @return {Array} - list of mirrored joint names (note: entries for non-mirrored joints will be `undefined`) + deriveMirroredJointNames: function(jointNames) { + return jointNames.map(function(name, i) { + if (/Left/.test(name)) { + return name.replace('Left', 'Right'); + } + if (/Right/.test(name)) { + return name.replace('Right', 'Left'); + } + return undefined; + }); + }, + getJointPosition: function(objectID, index) { + switch (this.type(objectID)) { + case 'overlay': return Overlays.getProperty(objectID, 'jointPositions')[index]; + } + return Vec3.ZERO; + }, + getJointPositions: function(objectID) { + switch (this.type(objectID)) { + case 'overlay': return Overlays.getProperty(objectID, 'jointPositions'); + } + return []; + }, + getJointOrientation: function(objectID, index) { + switch (this.type(objectID)) { + case 'overlay': return Overlays.getProperty(objectID, 'jointOrientations')[index]; + } + return Quat.normalize({}); + }, + getJointOrientations: function(objectID) { + switch (this.type(objectID)) { + case 'overlay': return Overlays.getProperty(objectID, 'jointOrientations'); + } + }, + getJointTranslation: function(objectID, index) { + switch (this.type(objectID)) { + case 'overlay': return Overlays.getProperty(objectID, 'jointTranslations')[index]; + } + return Vec3.ZERO; + }, + getJointTranslations: function(objectID) { + switch (this.type(objectID)) { + case 'overlay': return Overlays.getProperty(objectID, 'jointTranslations'); + } + return []; + }, + getJointRotation: function(objectID, index) { + switch (this.type(objectID)) { + case 'overlay': return Overlays.getProperty(objectID, 'jointRotations')[index]; + } + return Quat.normalize({}); + }, + getJointRotations: function(objectID) { + switch (this.type(objectID)) { + case 'overlay': return Overlays.getProperty(objectID, 'jointRotations'); + } + return []; + } +}; // modelHelper + + +// @property {PreconditionFunction} - indicates when the model's jointNames have become available +ModelReadyWatcher.JOINTS = function(state) { + return Array.isArray(state.jointNames); +}; +// @property {PreconditionFunction} - indicates when a model's naturalDimensions have become available +ModelReadyWatcher.DIMENSIONS = function(state) { + return Vec3.length(state.naturalDimensions) > 0; +}; +// @property {PreconditionFunction} - indicates when both a model's naturalDimensions and jointNames have become available +ModelReadyWatcher.JOINTS_AND_DIMENSIONS = function(state) { + // eslint-disable-next-line new-cap + return ModelReadyWatcher.JOINTS(state) && ModelReadyWatcher.DIMENSIONS(state); +}; +// @property {int} - interval used for continually rechecking model readiness, until ready or a timeout occurs +ModelReadyWatcher.RECHECK_MS = 50; +// @property {int} - default wait time before considering a model unready-able. +ModelReadyWatcher.DEFAULT_TIMEOUT_SECS = 10; + +// @private @class - waits for model to become usable inworld and tracks errors/timeouts +// @param [Object} options -- key/value config options: +// @param {ModelResource} options.resource - a ModelCache prefetched resource to observe for determining load state +// @param {Uuid} options.objectID - an inworld object to observe for determining readiness states +// @param {Function} [options.precondition=ModelReadyWatcher.JOINTS] - the precondition used to determine if the model is usable +// @param {int} [options.maxWaitSeconds=10] - max seconds to wait for the model to become usable, after which a timeout error is emitted +// @return {ModelReadyWatcher} +function ModelReadyWatcher(options) { + options = utils.assign({ + precondition: ModelReadyWatcher.JOINTS, + maxWaitSeconds: ModelReadyWatcher.DEFAULT_TIMEOUT_SECS + }, options); + + assert(!Uuid.isNull(options.objectID), 'Error: invalid options.objectID'); + assert(options.resource && 'state' in options.resource, 'Error: invalid options.resource'); + assert(typeof options.precondition === 'function', 'Error: invalid options.precondition'); + + utils.assign(this, { + resource: options.resource, + objectID: options.objectID, + precondition: options.precondition, + + // @signal - emitted when the model becomes ready, or with the error that prevented it + modelReady: utils.signal(function modelReady(error, result){}), + + // @public + ready: false, // tracks readiness state + jointNames: null, // populated with detected jointNames + naturalDimensions: null, // populated with detected naturalDimensions + + _startTime: new Date, + _watchdogTimer: Script.setTimeout(utils.bind(this, function() { + this._watchdogTimer = null; + }), options.maxWaitSeconds * 1000), + _interval: Script.setInterval(utils.bind(this, '_waitUntilReady'), ModelReadyWatcher.RECHECK_MS) + }); + + this.modelReady.connect(this, function(error, result) { + this.ready = !error && result; + }); +} + +ModelReadyWatcher.prototype = { + contructor: ModelReadyWatcher, + // @public method -- cancels monitoring and (if model was not yet ready) emits an error + cancel: function() { + return this._stop() && !this.ready && this.modelReady('cancelled', null); + }, + // stop pending timers + _stop: function() { + var stopped = 0; + if (this._watchdogTimer) { + Script.clearTimeout(this._watchdogTimer); + this._watchdogTimer = null; + stopped++; + } + if (this._interval) { + Script.clearInterval(this._interval); + this._interval = null; + stopped++; + } + return stopped; + }, + // the monitoring thread func + _waitUntilReady: function() { + var error = null, result = null; + if (!this._watchdogTimer) { + error = this.precondition.name || 'timeout'; + } else if (this.resource.state === Resource.State.FAILED) { + error = 'prefetch_failed'; + } else if (this.resource.state === Resource.State.FINISHED) { + // in theory there will always be at least one "joint name" that represents the main submesh + var names = modelHelper.getJointNames(this.objectID); + if (Array.isArray(names) && names.length) { + this.jointNames = names; + } + var props = modelHelper.getProperties(this.objectID, ['naturalDimensions']); + if (props && props.naturalDimensions && Vec3.length(props.naturalDimensions)) { + this.naturalDimensions = props.naturalDimensions; + } + var state = { + resource: this.resource, + objectID: this.objectID, + waitTime: (new Date - this._startTime) / 1000, + jointNames: this.jointNames, + naturalDimensions: this.naturalDimensions + }; + if (this.precondition(state)) { + result = state; + } + } + if (error || result !== null) { + this._stop(); + this.modelReady(error, result); + } + } +}; // ModelReadyWatcher.prototype + + +/***/ }), +/* 2 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +// doppleganger.js +// +// Created by Timothy Dedischew on 04/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 +// + +/* eslint-env commonjs */ +/* global console */ +// @module doppleganger +// +// This module contains the `Doppleganger` class implementation for creating an inspectable replica of +// an Avatar (as a model directly in front of and facing them). Joint positions and rotations are copied +// over in an update thread, so that the model automatically mirrors the Avatar's joint movements. +// An Avatar can then for example walk around "themselves" and examine from the back, etc. +// +// This should be helpful for inspecting your own look and debugging avatars, etc. +// +// The doppleganger is created as an overlay so that others do not see it -- and this also allows for the +// highest possible update rate when keeping joint data in sync. + +module.exports = Doppleganger; + +Doppleganger.version = '0.0.1a'; +log(Doppleganger.version); + +var _modelHelper = __webpack_require__(1), + modelHelper = _modelHelper.modelHelper, + ModelReadyWatcher = _modelHelper.ModelReadyWatcher, + utils = __webpack_require__(0); + +// @property {bool} - toggle verbose debug logging on/off +Doppleganger.WANT_DEBUG = false; + +// @property {bool} - when set true, Script.update will be used instead of setInterval for syncing joint data +Doppleganger.USE_SCRIPT_UPDATE = false; + +// @property {int} - the frame rate to target when using setInterval for joint updates +Doppleganger.TARGET_FPS = 60; + +// @class Doppleganger - Creates a new instance of a Doppleganger. +// @param {Avatar} [options.avatar=MyAvatar] - Avatar used to retrieve position and joint data. +// @param {bool} [options.mirrored=true] - Apply "symmetric mirroring" of Left/Right joints. +// @param {bool} [options.autoUpdate=true] - Automatically sync joint data. +function Doppleganger(options) { + this.options = options = options || {}; + this.avatar = options.avatar || MyAvatar; + this.mirrored = 'mirrored' in options ? options.mirrored : true; + this.autoUpdate = 'autoUpdate' in options ? options.autoUpdate : true; + + // @public + this.active = false; // whether doppleganger is currently being displayed/updated + this.objectID = null; // current doppleganger's Overlay or Entity id + this.frame = 0; // current joint update frame + + // @signal - emitted when .active state changes + this.activeChanged = utils.signal(function(active, reason) {}); + // @signal - emitted once model is either loaded or errors out + this.modelLoaded = utils.signal(function(error, result){}); + // @signal - emitted each time the model's joint data has been synchronized + this.jointsUpdated = utils.signal(function(objectID){}); +} + +Doppleganger.prototype = { + // @public @method - toggles doppleganger on/off + toggle: function() { + if (this.active) { + debugPrint('toggling off'); + this.stop(); + } else { + debugPrint('toggling on'); + this.start(); + } + return this.active; + }, + + // @public @method - synchronize the joint data between Avatar / doppleganger + update: function() { + this.frame++; + try { + if (!this.objectID) { + throw new Error('!this.objectID'); + } + + if (this.avatar.skeletonModelURL !== this.skeletonModelURL) { + return this.stop('avatar_changed'); + } + + var rotations = this.avatar.getJointRotations(); + var translations = this.avatar.getJointTranslations(); + var size = rotations.length; + + // note: this mismatch can happen when the avatar's model is actively changing + if (size !== translations.length || + (this.jointStateCount && size !== this.jointStateCount)) { + debugPrint('mismatched joint counts (avatar model likely changed)', size, translations.length, this.jointStateCount); + this.stop('avatar_changed_joints'); + return; + } + this.jointStateCount = size; + + if (this.mirrored) { + var mirroredIndexes = this.mirroredIndexes; + var outRotations = new Array(size); + var outTranslations = new Array(size); + for (var i=0; i < size; i++) { + var index = mirroredIndexes[i]; + if (index < 0 || index === false) { + index = i; + } + var rot = rotations[index]; + var trans = translations[index]; + trans.x *= -1; + rot.y *= -1; + rot.z *= -1; + outRotations[i] = rot; + outTranslations[i] = trans; + } + rotations = outRotations; + translations = outTranslations; + } + var jointUpdates = { + jointRotations: rotations, + jointTranslations: translations + }; + modelHelper.editObject(this.objectID, jointUpdates); + this.jointsUpdated(this.objectID, jointUpdates); + } catch (e) { + log('.update error: '+ e, index, e.stack); + this.stop('update_error'); + throw e; + } + }, + + // @public @method - show the doppleganger (and start the update thread, if options.autoUpdate was specified). + // @param {vec3} [options.position=(in front of avatar)] - starting position + // @param {quat} [options.orientation=avatar.orientation] - starting orientation + start: function(options) { + options = utils.assign(this.options, options); + if (this.objectID) { + log('start() called but object model already exists', this.objectID); + return; + } + var avatar = this.avatar; + if (!avatar.jointNames.length) { + return this.stop('joints_unavailable'); + } + + this.frame = 0; + var localPosition = Vec3.multiply(2, Quat.getForward(avatar.orientation)); + this.position = options.position || Vec3.sum(avatar.position, localPosition); + this.orientation = options.orientation || avatar.orientation; + this.skeletonModelURL = avatar.skeletonModelURL; + this.scale = avatar.scale || 1.0; + this.jointStateCount = 0; + this.jointNames = avatar.jointNames; + this.type = options.type || 'overlay'; + this.mirroredNames = modelHelper.deriveMirroredJointNames(this.jointNames); + this.mirroredIndexes = this.mirroredNames.map(function(name) { + return name ? avatar.getJointIndex(name) : false; + }); + + this.objectID = modelHelper.addObject({ + type: this.type === 'overlay' ? 'model' : 'Model', + modelURL: this.skeletonModelURL, + position: this.position, + rotation: this.orientation, + scale: this.scale + }); + Script.scriptEnding.connect(this, function() { + modelHelper.deleteObject(this.objectID); + }); + debugPrint('doppleganger created; objectID =', this.objectID); + + // trigger clean up (and stop updates) if the object gets deleted + this.onObjectDeleted = function(uuid) { + if (uuid === this.objectID) { + log('onObjectDeleted', uuid); + this.stop('object_deleted'); + } + }; + modelHelper.objectDeleted.connect(this, 'onObjectDeleted'); + + if ('onLoadComplete' in avatar) { + // stop the current doppleganger if Avatar loads a different model URL + this.onLoadComplete = function() { + if (avatar.skeletonModelURL !== this.skeletonModelURL) { + this.stop('avatar_changed_load'); + } + }; + avatar.onLoadComplete.connect(this, 'onLoadComplete'); + } + + this.onModelLoaded = function(error, result) { + if (error) { + return this.stop(error); + } + debugPrint('model ('+modelHelper.type(this.objectID)+')' + ' is ready; # joints == ' + result.jointNames.length); + var naturalDimensions = this.naturalDimensions = modelHelper.getProperties(this.objectID, ['naturalDimensions']).naturalDimensions; + debugPrint('naturalDimensions:', JSON.stringify(naturalDimensions)); + var props = { visible: true }; + if (naturalDimensions) { + props.dimensions = this.dimensions = Vec3.multiply(this.scale, naturalDimensions); + } + debugPrint('scaledDimensions:', this.scale, JSON.stringify(props.dimensions)); + modelHelper.editObject(this.objectID, props); + if (!options.position) { + this.syncVerticalPosition(); + } + if (this.autoUpdate) { + this._createUpdateThread(); + } + }; + + this._resource = ModelCache.prefetch(this.skeletonModelURL); + this._modelReadier = new ModelReadyWatcher({ + resource: this._resource, + objectID: this.objectID + }); + this._modelReadier.modelReady.connect(this, 'onModelLoaded'); + this.activeChanged(this.active = true, 'start'); + }, + + // @public @method - hide the doppleganger + // @param {String} [reason=stop] - the reason stop was called + stop: function(reason) { + reason = reason || 'stop'; + if (this.onUpdate) { + Script.update.disconnect(this, 'onUpdate'); + delete this.onUpdate; + } + if (this._interval) { + Script.clearInterval(this._interval); + this._interval = undefined; + } + if (this.onObjectDeleted) { + modelHelper.objectDeleted.disconnect(this, 'onObjectDeleted'); + delete this.onObjectDeleted; + } + if (this.onLoadComplete) { + this.avatar.onLoadComplete.disconnect(this, 'onLoadComplete'); + delete this.onLoadComplete; + } + if (this.onModelLoaded) { + this._modelReadier && this._modelReadier.modelReady.disconnect(this, 'onModelLoaded'); + this._modelReadier = this.onModelLoaded = undefined; + } + if (this.objectID) { + modelHelper.deleteObject(this.objectID); + this.objectID = undefined; + } + if (this.active) { + this.activeChanged(this.active = false, reason); + } else if (reason) { + debugPrint('already stopped so not triggering another activeChanged; latest reason was:', reason); + } + }, + // @public @method - Reposition the doppleganger so it sees "eye to eye" with the Avatar. + // @param {String} [byJointName=Hips] - the reference joint used to align the Doppleganger and Avatar + syncVerticalPosition: function(byJointName) { + byJointName = byJointName || 'Hips'; + var positions = modelHelper.getJointPositions(this.objectID), + properties = modelHelper.getProperties(this.objectID), + dopplePosition = properties.position, + doppleJointIndex = modelHelper.getJointIndex(this.objectID, byJointName),// names.indexOf(byJointName), + doppleJointPosition = positions[doppleJointIndex]; + + debugPrint('........... doppleJointPosition', JSON.stringify({ + byJointName: byJointName, + dopplePosition: dopplePosition, + doppleJointIndex: doppleJointIndex, + doppleJointPosition: doppleJointPosition, + properties: properties.type, + positions: positions[0] + },0,2)); + var avatarPosition = this.avatar.position, + avatarJointIndex = this.avatar.getJointIndex(byJointName), + avatarJointPosition = this.avatar.getJointPosition(avatarJointIndex); + + var offset = (avatarJointPosition.y - doppleJointPosition.y); + debugPrint('adjusting for offset', offset); + if (properties.type === 'model') { + dopplePosition.y = avatarPosition.y + offset; + } else { + dopplePosition.y = avatarPosition.y - offset; + } + + this.position = dopplePosition; + modelHelper.editObject(this.objectID, { position: this.position }); + }, + + // @private @method - creates the update thread to synchronize joint data + _createUpdateThread: function() { + if (Doppleganger.USE_SCRIPT_UPDATE) { + debugPrint('creating Script.update thread'); + this.onUpdate = this.update; + Script.update.connect(this, 'onUpdate'); + } else { + debugPrint('creating Script.setInterval thread @ ~', Doppleganger.TARGET_FPS +'fps'); + var timeout = 1000 / Doppleganger.TARGET_FPS; + this._interval = Script.setInterval(utils.bind(this, 'update'), timeout); + } + } + +}; + +// @function - debug logging +function log() { + // eslint-disable-next-line no-console + (typeof console === 'object' ? console.log : print)('doppleganger | ' + [].slice.call(arguments).join(' ')); +} + +function debugPrint() { + Doppleganger.WANT_DEBUG && log.apply(this, arguments); +} + + +/***/ }), +/* 3 */ +/***/ (function(module, exports, __webpack_require__) { + +// doppleganger-app.js +// +// Created by Timothy Dedischew on 04/21/2017. +// Copyright 2017 High Fidelity, Inc. +// +// This Client script creates an instance of a Doppleganger that can be toggled on/off via tablet button. +// (for more info see doppleganger.js) +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// +/* eslint-env commonjs */ +/* global DriveKeys, require:true, console */ +/* eslint-disable comma-dangle */ + +// decomment next line for automatic require cache-busting +// var require = function require(id) { return Script.require(id + '?'+new Date().getTime().toString(36)); }; +if (false) { + require = Script.require; +} + +var VERSION = '0.0.0'; +var WANT_DEBUG = false; +var DEBUG_MOVE_AS_YOU_MOVE = false; +var ROTATE_AS_YOU_MOVE = false; + +log(VERSION); + +var DopplegangerClass = __webpack_require__(2), + DopplegangerAttachments = __webpack_require__(4), + DebugControls = __webpack_require__(5), + modelHelper = __webpack_require__(1).modelHelper, + utils = __webpack_require__(0); + +// eslint-disable-next-line camelcase +var isWebpack = typeof __webpack_require__ === 'function'; + +var buttonConfig = utils.assign({ + text: 'MIRROR', +}, !isWebpack ? { + icon: Script.resolvePath('./doppleganger-i.svg'), + activeIcon: Script.resolvePath('./doppleganger-a.svg'), +} : { + icon: __webpack_require__(6), + activeIcon: __webpack_require__(7), +}); + +var tablet = Tablet.getTablet('com.highfidelity.interface.tablet.system'), + button = tablet.addButton(buttonConfig); + +Script.scriptEnding.connect(function() { + tablet.removeButton(button); + button = null; +}); + +var doppleganger = new DopplegangerClass({ + avatar: MyAvatar, + mirrored: false, + autoUpdate: true, + type: 'overlay' +}); + +// add support for displaying regular (non-soft) attachments on the doppleganger +{ + var RECHECK_ATTACHMENT_MS = 1000; + var dopplegangerAttachments = new DopplegangerAttachments(doppleganger), + attachmentInterval = null, + lastHash = dopplegangerAttachments.getAttachmentsHash(); + + // monitor for attachment changes, but only when the doppleganger is active + doppleganger.activeChanged.connect(function(active, reason) { + if (attachmentInterval) { + Script.clearInterval(attachmentInterval); + } + if (active) { + attachmentInterval = Script.setInterval(checkAttachmentsHash, RECHECK_ATTACHMENT_MS); + } else { + attachmentInterval = null; + } + }); + function checkAttachmentsHash() { + var currentHash = dopplegangerAttachments.getAttachmentsHash(); + if (currentHash !== lastHash) { + lastHash = currentHash; + debugPrint('app-doppleganger | detect attachment change'); + dopplegangerAttachments.refreshAttachments(); + } + } +} + +// add support for "move as you move" +{ + var movementKeys = 'W,A,S,D,Up,Down,Right,Left'.split(','); + var controllerKeys = 'LX,LY,RY'.split(','); + var translationKeys = Object.keys(DriveKeys).filter(function(p) { + return /translate/i.test(p); + }); + var startingPosition; + + // returns an array of any active driving keys (eg: ['W', 'TRANSLATE_Z']) + function currentDrivers() { + return [].concat( + movementKeys.map(function(key) { + return Controller.getValue(Controller.Hardware.Keyboard[key]) && key; + }) + ).concat( + controllerKeys.map(function(key) { + return Controller.getValue(Controller.Standard[key]) !== 0.0 && key; + }) + ).concat( + translationKeys.map(function(key) { + return MyAvatar.getRawDriveKey(DriveKeys[key]) !== 0.0 && key; + }) + ).filter(Boolean); + } + + doppleganger.jointsUpdated.connect(function(objectID) { + var drivers = currentDrivers(), + isDriving = drivers.length > 0; + if (isDriving) { + if (startingPosition) { + debugPrint('resetting startingPosition since drivers == ', drivers.join('|')); + startingPosition = null; + } + } else if (HMD.active || DEBUG_MOVE_AS_YOU_MOVE) { + startingPosition = startingPosition || MyAvatar.position; + var movement = Vec3.subtract(MyAvatar.position, startingPosition); + startingPosition = MyAvatar.position; + // Vec3.length(movement) > 0.0001 && Vec3.print('+avatarMovement', movement); + + // "mirror" the relative translation vector + movement.x *= -1; + movement.z *= -1; + var props = {}; + props.position = doppleganger.position = Vec3.sum(doppleganger.position, movement); + if (ROTATE_AS_YOU_MOVE) { + props.rotation = doppleganger.orientation = MyAvatar.orientation; + } + modelHelper.editObject(doppleganger.objectID, props); + } + }); +} + +// hide the doppleganger if this client script is unloaded +Script.scriptEnding.connect(doppleganger, 'stop'); + +// hide the doppleganger if the user switches domains (which might place them arbitrarily far away in world space) +function onDomainChanged() { + if (doppleganger.active) { + doppleganger.stop('domain_changed'); + } +} +Window.domainChanged.connect(onDomainChanged); +Window.domainConnectionRefused.connect(onDomainChanged); +Script.scriptEnding.connect(function() { + Window.domainChanged.disconnect(onDomainChanged); + Window.domainConnectionRefused.disconnect(onDomainChanged); +}); + +// toggle on/off via tablet button +button.clicked.connect(doppleganger, 'toggle'); + +// highlight tablet button based on current doppleganger state +doppleganger.activeChanged.connect(function(active, reason) { + if (button) { + button.editProperties({ isActive: active }); + debugPrint('doppleganger.activeChanged', active, reason); + } +}); + +// alert the user if there was an error applying their skeletonModelURL +doppleganger.modelLoaded.connect(function(error, result) { + if (doppleganger.active && error) { + Window.alert('doppleganger | ' + error + '\n' + doppleganger.skeletonModelURL); + } +}); + +// ---------------------------------------------------------------------------- + +// add debug indicators, but only if the user has configured the settings value +if (Settings.getValue('debug.doppleganger', false)) { + WANT_DEBUG = WANT_DEBUG || true; + DopplegangerClass.WANT_DEBUG = WANT_DEBUG; + DopplegangerAttachments.WANT_DEBUG = WANT_DEBUG; + new DebugControls(doppleganger); +} + +function log() { + // eslint-disable-next-line no-console + (typeof console === 'object' ? console.log : print)('app-doppleganger | ' + [].slice.call(arguments).join(' ')); +} + +function debugPrint() { + WANT_DEBUG && log.apply(this, arguments); +} + +// ---------------------------------------------------------------------------- + +UserActivityLogger.logAction('doppleganger_app_load'); +doppleganger.activeChanged.connect(function(active, reason) { + if (active) { + UserActivityLogger.logAction('doppleganger_enable'); + } else { + if (reason === 'stop') { + // user intentionally toggled the doppleganger + UserActivityLogger.logAction('doppleganger_disable'); + } else { + debugPrint('doppleganger stopped:', reason); + UserActivityLogger.logAction('doppleganger_autodisable', { reason: reason }); + } + } +}); +dopplegangerAttachments.attachmentsUpdated.connect(function(attachments) { + UserActivityLogger.logAction('doppleganger_attachments', { count: attachments.length }); +}); + + + +/***/ }), +/* 4 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + +/* eslint-env commonjs */ +/* eslint-disable comma-dangle */ +/* global console */ + +// var require = function(id) { return Script.require(id + '?'+new Date().getTime().toString(36)); } +module.exports = DopplegangerAttachments; + +DopplegangerAttachments.version = '0.0.1b'; +DopplegangerAttachments.WANT_DEBUG = false; + +var _modelHelper = __webpack_require__(1), + modelHelper = _modelHelper.modelHelper, + ModelReadyWatcher = _modelHelper.ModelReadyWatcher, + utils = __webpack_require__(0); + +function log() { + // eslint-disable-next-line no-console + (typeof console === 'object' ? console.log : print)('doppleganger-attachments | ' + [].slice.call(arguments).join(' ')); +} +log(DopplegangerAttachments.version); + +function debugPrint() { + DopplegangerAttachments.WANT_DEBUG && log.apply(this, arguments); +} + +function DopplegangerAttachments(doppleganger, options) { + utils.assign(this, { + _options: options, + doppleganger: doppleganger, + attachments: undefined, + manualJointSync: true, + attachmentsUpdated: utils.signal(function attachmentsUpdated(currentAttachments, previousAttachments){}), + }); + this._initialize(); + debugPrint('DopplegangerAttachments...', JSON.stringify(options)); +} +DopplegangerAttachments.prototype = { + // "hash" the current attachments (so that changes can be detected) + getAttachmentsHash: function() { + return JSON.stringify(this.doppleganger.avatar.getAttachmentsVariant()); + }, + // create a pure Object copy of the current native attachments variant + _cloneAttachmentsVariant: function() { + return JSON.parse(JSON.stringify(this.doppleganger.avatar.getAttachmentsVariant())); + }, + // fetch and resolve attachments to include jointIndex and other relevant $metadata + _getResolvedAttachments: function() { + var attachments = this._cloneAttachmentsVariant(), + objectID = this.doppleganger.objectID; + function toString() { + return '[attachment #' + this.$index + ' ' + this.$path + ' @ ' + this.jointName + '{' + this.$jointIndex + '}]'; + } + return attachments.map(function(attachment, i) { + var jointIndex = modelHelper.getJointIndex(objectID, attachment.jointName), + path = (attachment.modelUrl+'').split(/[?#]/)[0].split('/').slice(-3).join('/'); + return Object.defineProperties(attachment, { + $hash: { value: JSON.stringify(attachment) }, + $index: { value: i }, + $jointIndex: { value: jointIndex }, + $path: { value: path }, + toString: { value: toString }, + }); + }); + }, + // compare before / after attachment sets to determine which ones need to be (re)created + refreshAttachments: function() { + if (!this.doppleganger.objectID) { + return log('refreshAttachments -- canceling; !this.doppleganger.objectID'); + } + var before = this.attachments || [], + beforeIndex = before.reduce(function(out, att, index) { + out[att.$hash] = index; return out; + }, {}); + var after = this._getResolvedAttachments(), + afterIndex = after.reduce(function(out, att, index) { + out[att.$hash] = index; return out; + }, {}); + + Object.keys(beforeIndex).concat(Object.keys(afterIndex)).forEach(function(hash) { + if (hash in beforeIndex && hash in afterIndex) { + // print('reusing previous attachment', hash); + after[afterIndex[hash]] = before[beforeIndex[hash]]; + } else if (!(hash in afterIndex)) { + var attachment = before[beforeIndex[hash]]; + attachment.properties && attachment.properties.objectID && + modelHelper.deleteObject(attachment.properties.objectID); + delete attachment.properties; + } + }); + this.attachments = after; + this._createAttachmentObjects(); + this.attachmentsUpdated(after, before); + }, + _createAttachmentObjects: function() { + try { + var attachments = this.attachments, + parentID = this.doppleganger.objectID, + jointNames = this.doppleganger.jointNames, + properties = modelHelper.getProperties(this.doppleganger.objectID), + modelType = properties && properties.type; + utils.assert(modelType === 'model' || modelType === 'Model', 'unrecognized doppleganger modelType:' + modelType); + debugPrint('DopplegangerAttachments..._createAttachmentObjects', JSON.stringify({ + modelType: modelType, + attachments: attachments.length, + parentID: parentID, + jointNames: jointNames.join(' | '), + },0,2)); + return attachments.map(utils.bind(this, function(attachment, i) { + var objectType = modelHelper.type(attachment.properties && attachment.properties.objectID); + if (objectType === 'overlay') { + debugPrint('skipping already-provisioned attachment object', objectType, attachment.properties && attachment.properties.name); + return attachment; + } + var jointIndex = attachment.$jointIndex, // jointNames.indexOf(attachment.jointName), + scale = this.doppleganger.avatar.scale * (attachment.scale||1.0); + + attachment.properties = utils.assign({ + name: attachment.toString(), + type: modelType, + modelURL: attachment.modelUrl, + scale: scale, + dimensions: modelHelper.type(parentID) === 'entity' ? + Vec3.multiply(attachment.scale||1.0, Vec3.ONE) : undefined, + visible: false, + collisionless: true, + dynamic: false, + shapeType: 'none', + lifetime: 60, + }, !this.manualJointSync && { + parentID: parentID, + parentJointIndex: jointIndex, + localPosition: attachment.translation, + localRotation: Quat.fromVec3Degrees(attachment.rotation), + }); + var objectID = attachment.properties.objectID = modelHelper.addObject(attachment.properties); + utils.assert(!Uuid.isNull(objectID), 'could not create attachment: ' + [objectID, JSON.stringify(attachment.properties,0,2)]); + attachment._resource = ModelCache.prefetch(attachment.properties.modelURL); + attachment._modelReadier = new ModelReadyWatcher({ + resource: attachment._resource, + objectID: objectID, + }); + this.doppleganger.activeChanged.connect(attachment._modelReadier, '_stop'); + + attachment._modelReadier.modelReady.connect(this, function(err, result) { + if (err) { + log('>>>>> modelReady ERROR <<<<<: ' + err, attachment.properties.modelURL); + modelHelper.deleteObject(objectID); + return objectID = null; + } + debugPrint('attachment model ('+modelHelper.type(result.objectID)+') is ready; # joints ==', + result.jointNames && result.jointNames.length, JSON.stringify(result.naturalDimensions), result.objectID); + var properties = modelHelper.getProperties(result.objectID), + naturalDimensions = attachment.properties.naturalDimensions = properties.naturalDimensions || result.naturalDimensions; + modelHelper.editObject(objectID, { + dimensions: naturalDimensions ? Vec3.multiply(attachment.scale, naturalDimensions) : undefined, + localRotation: Quat.normalize({}), + localPosition: Vec3.ZERO, + }); + this.onJointsUpdated(parentID); // trigger once to initialize position/rotation + // give time for model overlay to "settle", then make it visible + Script.setTimeout(utils.bind(this, function() { + modelHelper.editObject(objectID, { + visible: true, + }); + attachment._loaded = true; + }), 100); + }); + return attachment; + })); + } catch (e) { + log('_createAttachmentObjects ERROR:', e.stack || e, e.fileName, e.lineNumber); + } + }, + + _removeAttachmentObjects: function() { + if (this.attachments) { + this.attachments.forEach(function(attachment) { + if (attachment.properties) { + if (attachment.properties.objectID) { + modelHelper.deleteObject(attachment.properties.objectID); + } + delete attachment.properties.objectID; + } + }); + delete this.attachments; + } + }, + + onJointsUpdated: function onJointsUpdated(objectID, jointUpdates) { + var jointOrientations = modelHelper.getJointOrientations(objectID), + jointPositions = modelHelper.getJointPositions(objectID), + parentID = objectID, + avatarScale = this.doppleganger.scale, + manualJointSync = this.manualJointSync; + + if (!this.attachments) { + this.refreshAttachments(); + } + var updatedObjects = this.attachments.reduce(function(updates, attachment, i) { + if (!attachment.properties || !attachment._loaded) { + return updates; + } + var objectID = attachment.properties.objectID, + jointIndex = attachment.$jointIndex, + jointOrientation = jointOrientations[jointIndex], + jointPosition = jointPositions[jointIndex]; + + var translation = Vec3.multiply(avatarScale, attachment.translation), + rotation = Quat.fromVec3Degrees(attachment.rotation); + + var localPosition = Vec3.multiplyQbyV(jointOrientation, translation), + localRotation = rotation; + + updates[objectID] = manualJointSync ? { + visible: true, + position: Vec3.sum(jointPosition, localPosition), + rotation: Quat.multiply(jointOrientation, localRotation), + scale: avatarScale * attachment.scale, + } : { + visible: true, + parentID: parentID, + parentJointIndex: jointIndex, + localRotation: localRotation, + localPosition: localPosition, + scale: attachment.scale, + }; + return updates; + }, {}); + modelHelper.editObjects(updatedObjects); + }, + + _initialize: function() { + var doppleganger = this.doppleganger; + if ('$attachmentControls' in doppleganger) { + throw new Error('only one set of attachment controls can be added per doppleganger'); + } + doppleganger.$attachmentControls = this; + doppleganger.activeChanged.connect(this, function(active) { + if (active) { + doppleganger.jointsUpdated.connect(this, 'onJointsUpdated'); + } else { + doppleganger.jointsUpdated.disconnect(this, 'onJointsUpdated'); + this._removeAttachmentObjects(); + } + }); + + Script.scriptEnding.connect(this, '_removeAttachmentObjects'); + }, +}; + + +/***/ }), +/* 5 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; +// -- ADVANCED DEBUGGING -- +// @function - Add debug joint indicators / extra debugging info. +// @param {Doppleganger} - existing Doppleganger instance to add controls to +// +// @note: +// * rightclick toggles mirror mode on/off +// * shift-rightclick toggles the debug indicators on/off +// * clicking on an indicator displays the joint name and mirrored joint name in the debug log. +// +// Example use: +// var doppleganger = new DopplegangerClass(); +// DopplegangerClass.addDebugControls(doppleganger); + + +/* eslint-env commonjs */ +/* eslint-disable comma-dangle */ +/* global console */ + +var DopplegangerClass = __webpack_require__(2), + modelHelper = __webpack_require__(1).modelHelper, + utils = __webpack_require__(0); + +module.exports = DebugControls; +// mixin addDebugControls to DopplegangerClass for backwards-compatibility +DopplegangerClass.addDebugControls = function(doppleganger) { + new DebugControls(doppleganger); + return doppleganger; +}; + +DebugControls.version = '0.0.0'; +DebugControls.COLOR_DEFAULT = { red: 255, blue: 255, green: 255 }; +DebugControls.COLOR_SELECTED = { red: 0, blue: 255, green: 0 }; + +function log() { + // eslint-disable-next-line no-console + (typeof console === 'object' ? console.log : print)('doppleganger-debug | ' + [].slice.call(arguments).join(' ')); +} + +function DebugControls(doppleganger) { + this.enableIndicators = true; + this.selectedJointName = null; + this.debugOverlayIDs = undefined; + this.jointSelected = utils.signal(function(result) {}); + this.doppleganger = doppleganger; + this._initialize(); +} +DebugControls.prototype = { + start: function() { + if (!this.onMousePressEvent) { + this.onMousePressEvent = this._onMousePressEvent; + Controller.mousePressEvent.connect(this, 'onMousePressEvent'); + this.doppleganger.jointsUpdated.connect(this, 'onJointsUpdated'); + } + }, + + stop: function() { + this.removeIndicators(); + if (this.onMousePressEvent) { + this.doppleganger.jointsUpdated.disconnect(this, 'onJointsUpdated'); + Controller.mousePressEvent.disconnect(this, 'onMousePressEvent'); + delete this.onMousePressEvent; + } + }, + + createIndicators: function(jointNames) { + this.jointNames = jointNames; + return jointNames.map(function(name, i) { + return Overlays.addOverlay('shape', { + shape: 'Icosahedron', + scale: 0.1, + solid: false, + alpha: 0.5 + }); + }); + }, + + removeIndicators: function() { + if (this.debugOverlayIDs) { + this.debugOverlayIDs.forEach(Overlays.deleteOverlay); + this.debugOverlayIDs = undefined; + } + }, + + onJointsUpdated: function(overlayID) { + if (!this.enableIndicators) { + return; + } + var jointNames = Overlays.getProperty(overlayID, 'jointNames'), + jointOrientations = Overlays.getProperty(overlayID, 'jointOrientations'), + jointPositions = Overlays.getProperty(overlayID, 'jointPositions'), + selectedIndex = jointNames.indexOf(this.selectedJointName); + + if (!this.debugOverlayIDs) { + this.debugOverlayIDs = this.createIndicators(jointNames); + } + + // batch all updates into a single call (using the editOverlays({ id: {props...}, ... }) API) + var updatedOverlays = this.debugOverlayIDs.reduce(function(updates, id, i) { + updates[id] = { + position: jointPositions[i], + rotation: jointOrientations[i], + color: i === selectedIndex ? DebugControls.COLOR_SELECTED : DebugControls.COLOR_DEFAULT, + solid: i === selectedIndex + }; + return updates; + }, {}); + Overlays.editOverlays(updatedOverlays); + }, + + _onMousePressEvent: function(evt) { + if (evt.isLeftButton) { + if (!this.enableIndicators || !this.debugOverlayIDs) { + return; + } + var ray = Camera.computePickRay(evt.x, evt.y), + hit = Overlays.findRayIntersection(ray, true, this.debugOverlayIDs); + + hit.jointIndex = this.debugOverlayIDs.indexOf(hit.overlayID); + hit.jointName = this.jointNames[hit.jointIndex]; + this.jointSelected(hit); + } else if (evt.isRightButton) { + if (evt.isShifted) { + this.enableIndicators = !this.enableIndicators; + if (!this.enableIndicators) { + this.removeIndicators(); + } + } else { + this.doppleganger.mirrored = !this.doppleganger.mirrored; + } + } + }, + + _initialize: function() { + if ('$debugControls' in this.doppleganger) { + throw new Error('only one set of debug controls can be added per doppleganger'); + } + this.doppleganger.$debugControls = this; + + this.doppleganger.activeChanged.connect(this, function(active) { + if (active) { + this.start(); + } else { + this.stop(); + } + }); + + this.jointSelected.connect(this, function(hit) { + this.selectedJointName = hit.jointName; + if (hit.jointIndex < 0) { + return; + } + hit.mirroredJointName = modelHelper.deriveMirroredJointNames([hit.jointName])[0]; + log('selected joint:', JSON.stringify(hit, 0, 2)); + }); + + Script.scriptEnding.connect(this, 'removeIndicators'); + }, +}; // DebugControls.prototype + + +/***/ }), +/* 6 */ +/***/ (function(module, exports) { + +module.exports = "data:image/svg+xml;xml,\n\n\nimage/svg+xml\n\t.st0{fill:#FFFFFF;}\n"; + +/***/ }), +/* 7 */ +/***/ (function(module, exports) { + +module.exports = "data:image/svg+xml;xml,\n\n\nimage/svg+xml\n\t.st0{fill:#FFFFFF;}\n"; + +/***/ }) +/******/ ]); \ No newline at end of file diff --git a/unpublishedScripts/marketplace/doppleganger-attachments/doppleganger-attachments.js b/unpublishedScripts/marketplace/doppleganger-attachments/doppleganger-attachments.js index a3b3873c2d..7526f56511 100644 --- a/unpublishedScripts/marketplace/doppleganger-attachments/doppleganger-attachments.js +++ b/unpublishedScripts/marketplace/doppleganger-attachments/doppleganger-attachments.js @@ -1,10 +1,12 @@ "use strict"; /* eslint-env commonjs */ /* eslint-disable comma-dangle */ +/* global console */ +// var require = function(id) { return Script.require(id + '?'+new Date().getTime().toString(36)); } module.exports = DopplegangerAttachments; -DopplegangerAttachments.version = '0.0.0'; +DopplegangerAttachments.version = '0.0.1b'; DopplegangerAttachments.WANT_DEBUG = false; var _modelHelper = require('./model-helper.js'), @@ -13,8 +15,10 @@ var _modelHelper = require('./model-helper.js'), utils = require('./utils.js'); function log() { - print('doppleganger-attachments | ' + [].slice.call(arguments).join(' ')); + // eslint-disable-next-line no-console + (typeof console === 'object' ? console.log : print)('doppleganger-attachments | ' + [].slice.call(arguments).join(' ')); } +log(DopplegangerAttachments.version); function debugPrint() { DopplegangerAttachments.WANT_DEBUG && log.apply(this, arguments); @@ -61,6 +65,9 @@ DopplegangerAttachments.prototype = { }, // compare before / after attachment sets to determine which ones need to be (re)created refreshAttachments: function() { + if (!this.doppleganger.objectID) { + return log('refreshAttachments -- canceling; !this.doppleganger.objectID'); + } var before = this.attachments || [], beforeIndex = before.reduce(function(out, att, index) { out[att.$hash] = index; return out; @@ -90,18 +97,19 @@ DopplegangerAttachments.prototype = { var attachments = this.attachments, parentID = this.doppleganger.objectID, jointNames = this.doppleganger.jointNames, - properties = modelHelper.getProperties(this.doppleganger.objectID); - + properties = modelHelper.getProperties(this.doppleganger.objectID), + modelType = properties && properties.type; + utils.assert(modelType === 'model' || modelType === 'Model', 'unrecognized doppleganger modelType:' + modelType); debugPrint('DopplegangerAttachments..._createAttachmentObjects', JSON.stringify({ - type: properties.type, + modelType: modelType, attachments: attachments.length, parentID: parentID, jointNames: jointNames.join(' | '), },0,2)); return attachments.map(utils.bind(this, function(attachment, i) { - var type = modelHelper.type(attachment.properties && attachment.properties.objectID); - if (type === 'overlay') { - debugPrint('skipping already-provisioned attachment object', type, attachment.properties && attachment.properties.name); + var objectType = modelHelper.type(attachment.properties && attachment.properties.objectID); + if (objectType === 'overlay') { + debugPrint('skipping already-provisioned attachment object', objectType, attachment.properties && attachment.properties.name); return attachment; } var jointIndex = attachment.$jointIndex, // jointNames.indexOf(attachment.jointName), @@ -109,7 +117,7 @@ DopplegangerAttachments.prototype = { attachment.properties = utils.assign({ name: attachment.toString(), - type: properties.type, + type: modelType, modelURL: attachment.modelUrl, scale: scale, dimensions: modelHelper.type(parentID) === 'entity' ? @@ -119,14 +127,16 @@ DopplegangerAttachments.prototype = { dynamic: false, shapeType: 'none', lifetime: 60, - grabbable: true, }, !this.manualJointSync && { parentID: parentID, parentJointIndex: jointIndex, + localPosition: attachment.translation, + localRotation: Quat.fromVec3Degrees(attachment.rotation), }); var objectID = attachment.properties.objectID = modelHelper.addObject(attachment.properties); + utils.assert(!Uuid.isNull(objectID), 'could not create attachment: ' + [objectID, JSON.stringify(attachment.properties,0,2)]); attachment._resource = ModelCache.prefetch(attachment.properties.modelURL); - attachment._modelReadier = new ModelReadyWatcher( { + attachment._modelReadier = new ModelReadyWatcher({ resource: attachment._resource, objectID: objectID, }); @@ -134,21 +144,23 @@ DopplegangerAttachments.prototype = { attachment._modelReadier.modelReady.connect(this, function(err, result) { if (err) { - log('>>>>> modelReady ERROR <<<<<: ' + err, attachment.modelUrl); + log('>>>>> modelReady ERROR <<<<<: ' + err, attachment.properties.modelURL); modelHelper.deleteObject(objectID); return objectID = null; } debugPrint('attachment model ('+modelHelper.type(result.objectID)+') is ready; # joints ==', - result.jointNames && result.jointNames.length, result.naturalDimensions, result.objectID); + result.jointNames && result.jointNames.length, JSON.stringify(result.naturalDimensions), result.objectID); var properties = modelHelper.getProperties(result.objectID), - naturalDimensions = attachment.properties.naturalDimensions = properties.naturalDimensions; - modelHelper.editObject(result.objectID, { + naturalDimensions = attachment.properties.naturalDimensions = properties.naturalDimensions || result.naturalDimensions; + modelHelper.editObject(objectID, { dimensions: naturalDimensions ? Vec3.multiply(attachment.scale, naturalDimensions) : undefined, + localRotation: Quat.normalize({}), + localPosition: Vec3.ZERO, }); this.onJointsUpdated(parentID); // trigger once to initialize position/rotation // give time for model overlay to "settle", then make it visible Script.setTimeout(utils.bind(this, function() { - modelHelper.editObject(result.objectID, { + modelHelper.editObject(objectID, { visible: true, }); attachment._loaded = true; @@ -175,7 +187,7 @@ DopplegangerAttachments.prototype = { } }, - onJointsUpdated: function onJointsUpdated(objectID) { + onJointsUpdated: function onJointsUpdated(objectID, jointUpdates) { var jointOrientations = modelHelper.getJointOrientations(objectID), jointPositions = modelHelper.getJointPositions(objectID), parentID = objectID, @@ -195,8 +207,9 @@ DopplegangerAttachments.prototype = { jointPosition = jointPositions[jointIndex]; var translation = Vec3.multiply(avatarScale, attachment.translation), - rotation = Quat.fromVec3Degrees(attachment.rotation), - localPosition = Vec3.multiplyQbyV(jointOrientation, translation), + rotation = Quat.fromVec3Degrees(attachment.rotation); + + var localPosition = Vec3.multiplyQbyV(jointOrientation, translation), localRotation = rotation; updates[objectID] = manualJointSync ? { @@ -212,7 +225,6 @@ DopplegangerAttachments.prototype = { localPosition: localPosition, scale: attachment.scale, }; - onJointsUpdated[objectID] = updates[objectID]; return updates; }, {}); modelHelper.editObjects(updatedObjects); @@ -221,7 +233,7 @@ DopplegangerAttachments.prototype = { _initialize: function() { var doppleganger = this.doppleganger; if ('$attachmentControls' in doppleganger) { - throw new Error('only one set of debug controls can be added per doppleganger'); + throw new Error('only one set of attachment controls can be added per doppleganger'); } doppleganger.$attachmentControls = this; doppleganger.activeChanged.connect(this, function(active) { diff --git a/unpublishedScripts/marketplace/doppleganger-attachments/doppleganger-debug.js b/unpublishedScripts/marketplace/doppleganger-attachments/doppleganger-debug.js new file mode 100644 index 0000000000..b0a83117fb --- /dev/null +++ b/unpublishedScripts/marketplace/doppleganger-attachments/doppleganger-debug.js @@ -0,0 +1,158 @@ +// -- ADVANCED DEBUGGING -- +// @function - Add debug joint indicators / extra debugging info. +// @param {Doppleganger} - existing Doppleganger instance to add controls to +// +// @note: +// * rightclick toggles mirror mode on/off +// * shift-rightclick toggles the debug indicators on/off +// * clicking on an indicator displays the joint name and mirrored joint name in the debug log. +// +// Example use: +// var doppleganger = new DopplegangerClass(); +// DopplegangerClass.addDebugControls(doppleganger); + +"use strict"; +/* eslint-env commonjs */ +/* eslint-disable comma-dangle */ +/* global console */ + +var DopplegangerClass = require('./doppleganger.js'), + modelHelper = require('./model-helper.js').modelHelper, + utils = require('./utils.js'); + +module.exports = DebugControls; +// mixin addDebugControls to DopplegangerClass for backwards-compatibility +DopplegangerClass.addDebugControls = function(doppleganger) { + new DebugControls(doppleganger); + return doppleganger; +}; + +DebugControls.version = '0.0.0'; +DebugControls.COLOR_DEFAULT = { red: 255, blue: 255, green: 255 }; +DebugControls.COLOR_SELECTED = { red: 0, blue: 255, green: 0 }; + +function log() { + // eslint-disable-next-line no-console + (typeof console === 'object' ? console.log : print)('doppleganger-debug | ' + [].slice.call(arguments).join(' ')); +} + +function DebugControls(doppleganger) { + this.enableIndicators = true; + this.selectedJointName = null; + this.debugOverlayIDs = undefined; + this.jointSelected = utils.signal(function(result) {}); + this.doppleganger = doppleganger; + this._initialize(); +} +DebugControls.prototype = { + start: function() { + if (!this.onMousePressEvent) { + this.onMousePressEvent = this._onMousePressEvent; + Controller.mousePressEvent.connect(this, 'onMousePressEvent'); + this.doppleganger.jointsUpdated.connect(this, 'onJointsUpdated'); + } + }, + + stop: function() { + this.removeIndicators(); + if (this.onMousePressEvent) { + this.doppleganger.jointsUpdated.disconnect(this, 'onJointsUpdated'); + Controller.mousePressEvent.disconnect(this, 'onMousePressEvent'); + delete this.onMousePressEvent; + } + }, + + createIndicators: function(jointNames) { + this.jointNames = jointNames; + return jointNames.map(function(name, i) { + return Overlays.addOverlay('shape', { + shape: 'Icosahedron', + scale: 0.1, + solid: false, + alpha: 0.5 + }); + }); + }, + + removeIndicators: function() { + if (this.debugOverlayIDs) { + this.debugOverlayIDs.forEach(Overlays.deleteOverlay); + this.debugOverlayIDs = undefined; + } + }, + + onJointsUpdated: function(overlayID) { + if (!this.enableIndicators) { + return; + } + var jointNames = Overlays.getProperty(overlayID, 'jointNames'), + jointOrientations = Overlays.getProperty(overlayID, 'jointOrientations'), + jointPositions = Overlays.getProperty(overlayID, 'jointPositions'), + selectedIndex = jointNames.indexOf(this.selectedJointName); + + if (!this.debugOverlayIDs) { + this.debugOverlayIDs = this.createIndicators(jointNames); + } + + // batch all updates into a single call (using the editOverlays({ id: {props...}, ... }) API) + var updatedOverlays = this.debugOverlayIDs.reduce(function(updates, id, i) { + updates[id] = { + position: jointPositions[i], + rotation: jointOrientations[i], + color: i === selectedIndex ? DebugControls.COLOR_SELECTED : DebugControls.COLOR_DEFAULT, + solid: i === selectedIndex + }; + return updates; + }, {}); + Overlays.editOverlays(updatedOverlays); + }, + + _onMousePressEvent: function(evt) { + if (evt.isLeftButton) { + if (!this.enableIndicators || !this.debugOverlayIDs) { + return; + } + var ray = Camera.computePickRay(evt.x, evt.y), + hit = Overlays.findRayIntersection(ray, true, this.debugOverlayIDs); + + hit.jointIndex = this.debugOverlayIDs.indexOf(hit.overlayID); + hit.jointName = this.jointNames[hit.jointIndex]; + this.jointSelected(hit); + } else if (evt.isRightButton) { + if (evt.isShifted) { + this.enableIndicators = !this.enableIndicators; + if (!this.enableIndicators) { + this.removeIndicators(); + } + } else { + this.doppleganger.mirrored = !this.doppleganger.mirrored; + } + } + }, + + _initialize: function() { + if ('$debugControls' in this.doppleganger) { + throw new Error('only one set of debug controls can be added per doppleganger'); + } + this.doppleganger.$debugControls = this; + + this.doppleganger.activeChanged.connect(this, function(active) { + if (active) { + this.start(); + } else { + this.stop(); + } + }); + + this.jointSelected.connect(this, function(hit) { + this.selectedJointName = hit.jointName; + if (hit.jointIndex < 0) { + return; + } + hit.mirroredJointName = modelHelper.deriveMirroredJointNames([hit.jointName])[0]; + log('selected joint:', JSON.stringify(hit, 0, 2)); + }); + + Script.scriptEnding.connect(this, 'removeIndicators'); + }, +}; // DebugControls.prototype diff --git a/unpublishedScripts/marketplace/doppleganger-attachments/doppleganger.js b/unpublishedScripts/marketplace/doppleganger-attachments/doppleganger.js index bebd36df45..190a8aa69e 100644 --- a/unpublishedScripts/marketplace/doppleganger-attachments/doppleganger.js +++ b/unpublishedScripts/marketplace/doppleganger-attachments/doppleganger.js @@ -10,6 +10,7 @@ // /* eslint-env commonjs */ +/* global console */ // @module doppleganger // // This module contains the `Doppleganger` class implementation for creating an inspectable replica of @@ -24,9 +25,13 @@ module.exports = Doppleganger; +Doppleganger.version = '0.0.1a'; +log(Doppleganger.version); + var _modelHelper = require('./model-helper.js'), modelHelper = _modelHelper.modelHelper, - ModelReadyWatcher = _modelHelper.ModelReadyWatcher; + ModelReadyWatcher = _modelHelper.ModelReadyWatcher, + utils = require('./utils.js'); // @property {bool} - toggle verbose debug logging on/off Doppleganger.WANT_DEBUG = false; @@ -48,16 +53,16 @@ function Doppleganger(options) { this.autoUpdate = 'autoUpdate' in options ? options.autoUpdate : true; // @public - this.active = false; // whether doppleganger is currently being displayed/updated + this.active = false; // whether doppleganger is currently being displayed/updated this.objectID = null; // current doppleganger's Overlay or Entity id - this.frame = 0; // current joint update frame + this.frame = 0; // current joint update frame // @signal - emitted when .active state changes - this.activeChanged = signal(function(active, reason) {}); + this.activeChanged = utils.signal(function(active, reason) {}); // @signal - emitted once model is either loaded or errors out - this.modelLoaded = signal(function(error, result){}); + this.modelLoaded = utils.signal(function(error, result){}); // @signal - emitted each time the model's joint data has been synchronized - this.jointsUpdated = signal(function(objectID){}); + this.jointsUpdated = utils.signal(function(objectID){}); } Doppleganger.prototype = { @@ -118,11 +123,12 @@ Doppleganger.prototype = { rotations = outRotations; translations = outTranslations; } - modelHelper.editObject(this.objectID, { + var jointUpdates = { jointRotations: rotations, jointTranslations: translations - }); - this.jointsUpdated(this.objectID); + }; + modelHelper.editObject(this.objectID, jointUpdates); + this.jointsUpdated(this.objectID, jointUpdates); } catch (e) { log('.update error: '+ e, index, e.stack); this.stop('update_error'); @@ -134,7 +140,7 @@ Doppleganger.prototype = { // @param {vec3} [options.position=(in front of avatar)] - starting position // @param {quat} [options.orientation=avatar.orientation] - starting orientation start: function(options) { - options = assign(this.options, options); + options = utils.assign(this.options, options); if (this.objectID) { log('start() called but object model already exists', this.objectID); return; @@ -194,11 +200,11 @@ Doppleganger.prototype = { return this.stop(error); } debugPrint('model ('+modelHelper.type(this.objectID)+')' + ' is ready; # joints == ' + result.jointNames.length); - var naturalDimensions = modelHelper.getProperties(this.objectID, ['naturalDimensions']).naturalDimensions; + var naturalDimensions = this.naturalDimensions = modelHelper.getProperties(this.objectID, ['naturalDimensions']).naturalDimensions; debugPrint('naturalDimensions:', JSON.stringify(naturalDimensions)); var props = { visible: true }; if (naturalDimensions) { - props.dimensions = Vec3.multiply(this.scale, naturalDimensions); + props.dimensions = this.dimensions = Vec3.multiply(this.scale, naturalDimensions); } debugPrint('scaledDimensions:', this.scale, JSON.stringify(props.dimensions)); modelHelper.editObject(this.objectID, props); @@ -296,220 +302,18 @@ Doppleganger.prototype = { } else { debugPrint('creating Script.setInterval thread @ ~', Doppleganger.TARGET_FPS +'fps'); var timeout = 1000 / Doppleganger.TARGET_FPS; - this._interval = Script.setInterval(bind(this, 'update'), timeout); + this._interval = Script.setInterval(utils.bind(this, 'update'), timeout); } } }; -// @function - bind a function to a `this` context -// @param {Object} - the `this` context -// @param {Function|String} - function or method name -function bind(thiz, method) { - method = thiz[method] || method; - return function() { - return method.apply(thiz, arguments); - }; -} - -// @function - Qt signal polyfill -function signal(template) { - var callbacks = []; - return Object.defineProperties(function() { - var args = [].slice.call(arguments); - callbacks.forEach(function(obj) { - obj.handler.apply(obj.scope, args); - }); - }, { - connect: { value: function(scope, handler) { - var callback = {scope: scope, handler: scope[handler] || handler || scope}; - if (!callback.handler || !callback.handler.apply) { - throw new Error('invalid arguments to connect:' + [template, scope, handler]); - } - callbacks.push({scope: scope, handler: scope[handler] || handler || scope}); - }}, - disconnect: { value: function(scope, handler) { - var match = {scope: scope, handler: scope[handler] || handler || scope}; - callbacks = callbacks.filter(function(obj) { - return !(obj.scope === match.scope && obj.handler === match.handler); - }); - }} - }); -} - -// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign#Polyfill -/* eslint-disable */ -function assign(target, varArgs) { // .length of function is 2 - 'use strict'; - if (target == null) { // TypeError if undefined or 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) { // Skip over if undefined or null - for (var nextKey in nextSource) { - // Avoid bugs when hasOwnProperty is shadowed - if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) { - to[nextKey] = nextSource[nextKey]; - } - } - } - } - return to; -} -/* eslint-enable */ -// //https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign#Polyfill - // @function - debug logging function log() { - print('doppleganger | ' + [].slice.call(arguments).join(' ')); + // eslint-disable-next-line no-console + (typeof console === 'object' ? console.log : print)('doppleganger | ' + [].slice.call(arguments).join(' ')); } function debugPrint() { Doppleganger.WANT_DEBUG && log.apply(this, arguments); } - -// -- ADVANCED DEBUGGING -- -// @function - Add debug joint indicators / extra debugging info. -// @param {Doppleganger} - existing Doppleganger instance to add controls to -// -// @note: -// * rightclick toggles mirror mode on/off -// * shift-rightclick toggles the debug indicators on/off -// * clicking on an indicator displays the joint name and mirrored joint name in the debug log. -// -// Example use: -// var doppleganger = new Doppleganger(); -// Doppleganger.addDebugControls(doppleganger); -Doppleganger.addDebugControls = function(doppleganger) { - DebugControls.COLOR_DEFAULT = { red: 255, blue: 255, green: 255 }; - DebugControls.COLOR_SELECTED = { red: 0, blue: 255, green: 0 }; - - function DebugControls() { - this.enableIndicators = true; - this.selectedJointName = null; - this.debugOverlayIDs = undefined; - this.jointSelected = signal(function(result) {}); - } - DebugControls.prototype = { - start: function() { - if (!this.onMousePressEvent) { - this.onMousePressEvent = this._onMousePressEvent; - Controller.mousePressEvent.connect(this, 'onMousePressEvent'); - } - }, - - stop: function() { - this.removeIndicators(); - if (this.onMousePressEvent) { - Controller.mousePressEvent.disconnect(this, 'onMousePressEvent'); - delete this.onMousePressEvent; - } - }, - - createIndicators: function(jointNames) { - this.jointNames = jointNames; - return jointNames.map(function(name, i) { - return Overlays.addOverlay('shape', { - shape: 'Icosahedron', - scale: 0.1, - solid: false, - alpha: 0.5 - }); - }); - }, - - removeIndicators: function() { - if (this.debugOverlayIDs) { - this.debugOverlayIDs.forEach(Overlays.deleteOverlay); - this.debugOverlayIDs = undefined; - } - }, - - onJointsUpdated: function(overlayID) { - if (!this.enableIndicators) { - return; - } - var jointNames = Overlays.getProperty(overlayID, 'jointNames'), - jointOrientations = Overlays.getProperty(overlayID, 'jointOrientations'), - jointPositions = Overlays.getProperty(overlayID, 'jointPositions'), - selectedIndex = jointNames.indexOf(this.selectedJointName); - - if (!this.debugOverlayIDs) { - this.debugOverlayIDs = this.createIndicators(jointNames); - } - - // batch all updates into a single call (using the editOverlays({ id: {props...}, ... }) API) - var updatedOverlays = this.debugOverlayIDs.reduce(function(updates, id, i) { - updates[id] = { - position: jointPositions[i], - rotation: jointOrientations[i], - color: i === selectedIndex ? DebugControls.COLOR_SELECTED : DebugControls.COLOR_DEFAULT, - solid: i === selectedIndex - }; - return updates; - }, {}); - Overlays.editOverlays(updatedOverlays); - }, - - _onMousePressEvent: function(evt) { - if (!evt.isLeftButton || !this.enableIndicators || !this.debugOverlayIDs) { - return; - } - var ray = Camera.computePickRay(evt.x, evt.y), - hit = Overlays.findRayIntersection(ray, true, this.debugOverlayIDs); - - hit.jointIndex = this.debugOverlayIDs.indexOf(hit.overlayID); - hit.jointName = this.jointNames[hit.jointIndex]; - this.jointSelected(hit); - } - }; - - if ('$debugControls' in doppleganger) { - throw new Error('only one set of debug controls can be added per doppleganger'); - } - var debugControls = new DebugControls(); - doppleganger.$debugControls = debugControls; - - function onMousePressEvent(evt) { - if (evt.isRightButton) { - if (evt.isShifted) { - debugControls.enableIndicators = !debugControls.enableIndicators; - if (!debugControls.enableIndicators) { - debugControls.removeIndicators(); - } - } else { - doppleganger.mirrored = !doppleganger.mirrored; - } - } - } - - doppleganger.activeChanged.connect(function(active) { - if (active) { - debugControls.start(); - doppleganger.jointsUpdated.connect(debugControls, 'onJointsUpdated'); - Controller.mousePressEvent.connect(onMousePressEvent); - } else { - Controller.mousePressEvent.disconnect(onMousePressEvent); - doppleganger.jointsUpdated.disconnect(debugControls, 'onJointsUpdated'); - debugControls.stop(); - } - }); - - debugControls.jointSelected.connect(function(hit) { - debugControls.selectedJointName = hit.jointName; - if (hit.jointIndex < 0) { - return; - } - hit.mirroredJointName = modelHelper.deriveMirroredJointNames([hit.jointName])[0]; - log('selected joint:', JSON.stringify(hit, 0, 2)); - }); - - Script.scriptEnding.connect(debugControls, 'removeIndicators'); - - return doppleganger; -}; diff --git a/unpublishedScripts/marketplace/doppleganger-attachments/makefile b/unpublishedScripts/marketplace/doppleganger-attachments/makefile new file mode 100644 index 0000000000..eaf02dbed9 --- /dev/null +++ b/unpublishedScripts/marketplace/doppleganger-attachments/makefile @@ -0,0 +1,11 @@ +all: + @echo "make dist" + +dist: doppleganger-a.svg.json doppleganger-i.svg.json dist/app-doppleganger-marketplace.js + @echo "OK" + +%.svg.json: %.svg + cat $< | jq -sR '"data:image/svg+xml;xml,"+.' > $@ + +dist/app-doppleganger-marketplace.js: *.js + ./node_modules/.bin/webpack --verbose app-doppleganger-attachments.js $@ diff --git a/unpublishedScripts/marketplace/doppleganger-attachments/model-helper.js b/unpublishedScripts/marketplace/doppleganger-attachments/model-helper.js index 2dda2c12ec..8edaf50cb7 100644 --- a/unpublishedScripts/marketplace/doppleganger-attachments/model-helper.js +++ b/unpublishedScripts/marketplace/doppleganger-attachments/model-helper.js @@ -8,6 +8,7 @@ // /* eslint-env commonjs */ +/* global console */ // @module model-helper // // This module provides ModelReadyWatcher (a helper class for knowing when a model becomes usable inworld) and @@ -18,10 +19,16 @@ var utils = require('./utils.js'), assert = utils.assert; module.exports = { - version: '0.0.0', + version: '0.0.1', ModelReadyWatcher: ModelReadyWatcher }; +function log() { + // eslint-disable-next-line no-console + (typeof console === 'object' ? console.log : print)('model-helper | ' + [].slice.call(arguments).join(' ')); +} +log(module.exports.version); + var _objectDeleted = utils.signal(function objectDeleted(objectID){}); // proxy for _objectDeleted that only binds deletion tracking if script actually connects to the unified signal var objectDeleted = utils.assign(function objectDeleted(objectID){}, { diff --git a/unpublishedScripts/marketplace/doppleganger-attachments/package.json b/unpublishedScripts/marketplace/doppleganger-attachments/package.json new file mode 100644 index 0000000000..f18136fc1b --- /dev/null +++ b/unpublishedScripts/marketplace/doppleganger-attachments/package.json @@ -0,0 +1,5 @@ +{ + "devDependencies": { + "webpack": "^3.0.0" + } +} diff --git a/unpublishedScripts/marketplace/doppleganger-attachments/readme.md b/unpublishedScripts/marketplace/doppleganger-attachments/readme.md new file mode 100644 index 0000000000..d0ebf2d6e1 --- /dev/null +++ b/unpublishedScripts/marketplace/doppleganger-attachments/readme.md @@ -0,0 +1,4 @@ +note: to rebuild webpack version: +* install `jq` https://stedolan.github.io/jq (used to encode the icon.svg's as Data URI JSON strings) +* `npm install` +* `make dist` diff --git a/unpublishedScripts/marketplace/doppleganger-attachments/utils.js b/unpublishedScripts/marketplace/doppleganger-attachments/utils.js index 76c6e1ef7f..b34af1e632 100644 --- a/unpublishedScripts/marketplace/doppleganger-attachments/utils.js +++ b/unpublishedScripts/marketplace/doppleganger-attachments/utils.js @@ -1,12 +1,20 @@ /* eslint-env commonjs */ +/* global console */ module.exports = { + version: '0.0.1', bind: bind, signal: signal, assign: assign, assert: assert }; +function log() { + // eslint-disable-next-line no-console + (typeof console === 'object' ? console.log : print)('utils | ' + [].slice.call(arguments).join(' ')); +} +log(module.exports.version); + // @function - bind a function to a `this` context // @param {Object} - the `this` context // @param {Function|String} - function or method name diff --git a/unpublishedScripts/marketplace/spectator-camera/cameraOn.wav b/unpublishedScripts/marketplace/spectator-camera/cameraOn.wav new file mode 100644 index 0000000000..76dbb647b1 Binary files /dev/null and b/unpublishedScripts/marketplace/spectator-camera/cameraOn.wav differ diff --git a/unpublishedScripts/marketplace/spectator-camera/spectator-camera.fbx b/unpublishedScripts/marketplace/spectator-camera/spectator-camera.fbx new file mode 100644 index 0000000000..6584264c0d Binary files /dev/null and b/unpublishedScripts/marketplace/spectator-camera/spectator-camera.fbx differ diff --git a/unpublishedScripts/marketplace/spectator-camera/spectatorCamera.js b/unpublishedScripts/marketplace/spectator-camera/spectatorCamera.js new file mode 100644 index 0000000000..f0b943ad92 --- /dev/null +++ b/unpublishedScripts/marketplace/spectator-camera/spectatorCamera.js @@ -0,0 +1,514 @@ +"use strict"; +/*jslint vars:true, plusplus:true, forin:true*/ +/*global Tablet, Script, */ +/* eslint indent: ["error", 4, { "outerIIFEBody": 0 }] */ +// +// spectatorCamera.js +// +// Created by Zach Fox on 2017-06-05 +// 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 + + // FUNCTION VAR DECLARATIONS + var sendToQml, addOrRemoveButton, onTabletScreenChanged, fromQml, + onTabletButtonClicked, wireEventBridge, startup, shutdown, registerButtonMappings; + + // Function Name: inFrontOf() + // + // Description: + // -Returns the position in front of the given "position" argument, where the forward vector is based off + // the "orientation" argument and the amount in front is based off the "distance" argument. + function inFrontOf(distance, position, orientation) { + return Vec3.sum(position || MyAvatar.position, + Vec3.multiply(distance, Quat.getForward(orientation || MyAvatar.orientation))); + } + + // Function Name: spectatorCameraOn() + // + // Description: + // -Call this function to set up the spectator camera and + // spawn the camera entity. + // + // Relevant Variables: + // -spectatorCameraConfig: The render configuration of the spectator camera + // render job. It controls various attributes of the Secondary Camera, such as: + // -The entity ID to follow + // -Position + // -Orientation + // -Rendered texture size + // -Vertical field of view + // -Near clip plane distance + // -Far clip plane distance + // -viewFinderOverlay: The in-world overlay that displays the spectator camera's view. + // -camera: The in-world entity that corresponds to the spectator camera. + // -cameraIsDynamic: "false" for now - maybe it shouldn't be? False means that the camera won't drift when you let go... + // -cameraRotation: The rotation of the spectator camera. + // -cameraPosition: The position of the spectator camera. + // -glassPaneWidth: The width of the glass pane above the spectator camera that holds the viewFinderOverlay. + // -viewFinderOverlayDim: The x, y, and z dimensions of the viewFinderOverlay. + // -camera: The camera model which is grabbable. + // -viewFinderOverlay: The preview of what the spectator camera is viewing, placed inside the glass pane. + var spectatorCameraConfig = Render.getConfig("SecondaryCamera"); + var viewFinderOverlay = false; + var camera = false; + var cameraIsDynamic = false; + var cameraRotation; + var cameraPosition; + var glassPaneWidth = 0.16; + // The negative y dimension for viewFinderOverlay is necessary for now due to the way Image3DOverlay + // draws textures, but should be looked into at some point. Also the z dimension shouldn't affect + // the overlay since it is an Image3DOverlay so it is set to 0. + var viewFinderOverlayDim = { x: glassPaneWidth, y: -glassPaneWidth, z: 0 }; + function spectatorCameraOn() { + // Sets the special texture size based on the window it is displayed in, which doesn't include the menu bar + spectatorCameraConfig.enableSecondaryCameraRenderConfigs(true); + spectatorCameraConfig.resetSizeSpectatorCamera(Window.innerWidth, Window.innerHeight); + cameraRotation = Quat.multiply(MyAvatar.orientation, Quat.fromPitchYawRollDegrees(15, -155, 0)), cameraPosition = inFrontOf(0.85, Vec3.sum(MyAvatar.position, { x: 0, y: 0.28, z: 0 })); + camera = Entities.addEntity({ + "angularDamping": 1, + "damping": 1, + "collidesWith": "static,dynamic,kinematic,", + "collisionMask": 7, + "dynamic": cameraIsDynamic, + "modelURL": Script.resolvePath("spectator-camera.fbx"), + "registrationPoint": { + "x": 0.56, + "y": 0.545, + "z": 0.23 + }, + "rotation": cameraRotation, + "position": cameraPosition, + "shapeType": "simple-compound", + "type": "Model", + "userData": "{\"grabbableKey\":{\"grabbable\":true}}" + }, true); + spectatorCameraConfig.attachedEntityId = camera; + updateOverlay(); + setDisplay(monitorShowsCameraView); + // Change button to active when window is first openend OR if the camera is on, false otherwise. + if (button) { + button.editProperties({ isActive: onSpectatorCameraScreen || camera }); + } + Audio.playSound(CAMERA_ON_SOUND, { + volume: 0.15, + position: cameraPosition, + localOnly: true + }); + } + + // Function Name: spectatorCameraOff() + // + // Description: + // -Call this function to shut down the spectator camera and + // destroy the camera entity. "isChangingDomains" is true when this function is called + // from the "Window.domainChanged()" signal. + var WAIT_AFTER_DOMAIN_SWITCH_BEFORE_CAMERA_DELETE_MS = 1 * 1000; + function spectatorCameraOff(isChangingDomains) { + function deleteCamera() { + Entities.deleteEntity(camera); + camera = false; + if (button) { + // Change button to active when window is first openend OR if the camera is on, false otherwise. + button.editProperties({ isActive: onSpectatorCameraScreen || camera }); + } + } + + spectatorCameraConfig.attachedEntityId = false; + spectatorCameraConfig.enableSecondaryCameraRenderConfigs(false); + if (camera) { + // Workaround for Avatar Entities not immediately having properties after + // the "Window.domainChanged()" signal is emitted. + // Should be removed after FB6155 is fixed. + if (isChangingDomains) { + Script.setTimeout(function () { + deleteCamera(); + spectatorCameraOn(); + }, WAIT_AFTER_DOMAIN_SWITCH_BEFORE_CAMERA_DELETE_MS); + } else { + deleteCamera(); + } + } + if (viewFinderOverlay) { + Overlays.deleteOverlay(viewFinderOverlay); + } + viewFinderOverlay = false; + setDisplay(monitorShowsCameraView); + } + + // Function Name: addOrRemoveButton() + // + // Description: + // -Used to add or remove the "SPECTATOR" app button from the HUD/tablet. Set the "isShuttingDown" argument + // to true if you're calling this function upon script shutdown. Set the "isHMDmode" to true if the user is + // in HMD; otherwise set to false. + // + // Relevant Variables: + // -button: The tablet button. + // -buttonName: The name of the button. + // -showSpectatorInDesktop: Set to "true" to show the "SPECTATOR" app in desktop mode. + var button = false; + var buttonName = "SPECTATOR"; + var showSpectatorInDesktop = false; + function addOrRemoveButton(isShuttingDown, isHMDMode) { + if (!tablet) { + print("Warning in addOrRemoveButton(): 'tablet' undefined!"); + return; + } + if (!button) { + if ((isHMDMode || showSpectatorInDesktop) && !isShuttingDown) { + button = tablet.addButton({ + text: buttonName, + icon: "icons/tablet-icons/spectator-i.svg", + activeIcon: "icons/tablet-icons/spectator-a.svg" + }); + button.clicked.connect(onTabletButtonClicked); + } + } else if (button) { + if ((!isHMDMode && !showSpectatorInDesktop) || isShuttingDown) { + button.clicked.disconnect(onTabletButtonClicked); + tablet.removeButton(button); + button = false; + } + } else { + print("ERROR adding/removing Spectator button!"); + } + } + + // Function Name: startup() + // + // Description: + // -startup() will be called when the script is loaded. + // + // Relevant Variables: + // -tablet: The tablet instance to be modified. + var tablet = null; + function startup() { + tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); + addOrRemoveButton(false, HMD.active); + tablet.screenChanged.connect(onTabletScreenChanged); + Window.domainChanged.connect(onDomainChanged); + Window.geometryChanged.connect(resizeViewFinderOverlay); + Controller.keyPressEvent.connect(keyPressEvent); + HMD.displayModeChanged.connect(onHMDChanged); + viewFinderOverlay = false; + camera = false; + registerButtonMappings(); + } + + // Function Name: wireEventBridge() + // + // Description: + // -Used to connect/disconnect the script's response to the tablet's "fromQml" signal. Set the "on" argument to enable or + // disable to event bridge. + // + // Relevant Variables: + // -hasEventBridge: true/false depending on whether we've already connected the event bridge. + var hasEventBridge = false; + function wireEventBridge(on) { + if (!tablet) { + print("Warning in wireEventBridge(): 'tablet' undefined!"); + return; + } + if (on) { + if (!hasEventBridge) { + tablet.fromQml.connect(fromQml); + hasEventBridge = true; + } + } else { + if (hasEventBridge) { + tablet.fromQml.disconnect(fromQml); + hasEventBridge = false; + } + } + } + + // Function Name: setDisplay() + // + // Description: + // -There are two bool variables that determine what the "url" argument to "setDisplayTexture(url)" should be: + // Camera on/off switch, and the "Monitor Shows" on/off switch. + // This results in four possible cases for the argument. Those four cases are: + // 1. Camera is off; "Monitor Shows" is "HMD Preview": "url" is "" + // 2. Camera is off; "Monitor Shows" is "Camera View": "url" is "" + // 3. Camera is on; "Monitor Shows" is "HMD Preview": "url" is "" + // 4. Camera is on; "Monitor Shows" is "Camera View": "url" is "resource://spectatorCameraFrame" + function setDisplay(showCameraView) { + + var url = (camera) ? (showCameraView ? "resource://spectatorCameraFrame" : "resource://hmdPreviewFrame") : ""; + sendToQml({ method: 'showPreviewTextureNotInstructions', setting: !!url, url: url}); + + // FIXME: temporary hack to avoid setting the display texture to hmdPreviewFrame + // until it is the correct mono. + if (url === "resource://hmdPreviewFrame") { + Window.setDisplayTexture(""); + } else { + Window.setDisplayTexture(url); + } + } + const MONITOR_SHOWS_CAMERA_VIEW_DEFAULT = false; + var monitorShowsCameraView = !!Settings.getValue('spectatorCamera/monitorShowsCameraView', MONITOR_SHOWS_CAMERA_VIEW_DEFAULT); + function setMonitorShowsCameraView(showCameraView) { + if (showCameraView === monitorShowsCameraView) { + return; + } + monitorShowsCameraView = showCameraView; + setDisplay(showCameraView); + Settings.setValue('spectatorCamera/monitorShowsCameraView', showCameraView); + } + function setMonitorShowsCameraViewAndSendToQml(showCameraView) { + setMonitorShowsCameraView(showCameraView); + sendToQml({ method: 'updateMonitorShowsSwitch', params: showCameraView }); + } + function keyPressEvent(event) { + if ((event.text === "0") && !event.isAutoRepeat && !event.isShifted && !event.isMeta && event.isControl && !event.isAlt) { + setMonitorShowsCameraViewAndSendToQml(!monitorShowsCameraView); + } + } + function updateOverlay() { + // The only way I found to update the viewFinderOverlay without turning the spectator camera on and off is to delete and recreate the + // overlay, which is inefficient but resizing the window shouldn't be performed often + if (viewFinderOverlay) { + Overlays.deleteOverlay(viewFinderOverlay); + } + viewFinderOverlay = Overlays.addOverlay("image3d", { + url: "resource://spectatorCameraFrame", + emissive: true, + parentID: camera, + alpha: 1, + localRotation: { w: 1, x: 0, y: 0, z: 0 }, + localPosition: { x: 0, y: 0.13, z: 0.126 }, + dimensions: viewFinderOverlayDim + }); + } + + // Function Name: resizeViewFinderOverlay() + // + // Description: + // -A function called when the window is moved/resized, which changes the viewFinderOverlay's texture and dimensions to be + // appropriately altered to fit inside the glass pane while not distorting the texture. The "geometryChanged" argument gives information + // on how the window changed, including x, y, width, and height. + // + // Relevant Variables: + // -glassPaneRatio: The aspect ratio of the glass pane, currently set as a 16:9 aspect ratio (change if model changes). + // -verticalScale: The amount the viewFinderOverlay should be scaled if the window size is vertical. + // -squareScale: The amount the viewFinderOverlay should be scaled if the window size is not vertical but is more square than the + // glass pane's aspect ratio. + function resizeViewFinderOverlay(geometryChanged) { + var glassPaneRatio = 16 / 9; + var verticalScale = 1 / glassPaneRatio; + var squareScale = verticalScale * (1 + (1 - (1 / (geometryChanged.width / geometryChanged.height)))); + + if (geometryChanged.height > geometryChanged.width) { //vertical window size + viewFinderOverlayDim = { x: (glassPaneWidth * verticalScale), y: (-glassPaneWidth * verticalScale), z: 0 }; + } else if ((geometryChanged.width / geometryChanged.height) < glassPaneRatio) { //square-ish window size, in-between vertical and horizontal + viewFinderOverlayDim = { x: (glassPaneWidth * squareScale), y: (-glassPaneWidth * squareScale), z: 0 }; + } else { //horizontal window size + viewFinderOverlayDim = { x: glassPaneWidth, y: -glassPaneWidth, z: 0 }; + } + updateOverlay(); + spectatorCameraConfig.resetSizeSpectatorCamera(geometryChanged.width, geometryChanged.height); + setDisplay(monitorShowsCameraView); + } + + const SWITCH_VIEW_FROM_CONTROLLER_DEFAULT = false; + var switchViewFromController = !!Settings.getValue('spectatorCamera/switchViewFromController', SWITCH_VIEW_FROM_CONTROLLER_DEFAULT); + function setControllerMappingStatus(status) { + if (!controllerMapping) { + return; + } + if (status) { + controllerMapping.enable(); + } else { + controllerMapping.disable(); + } + } + function setSwitchViewFromController(setting) { + if (setting === switchViewFromController) { + return; + } + switchViewFromController = setting; + setControllerMappingStatus(switchViewFromController); + Settings.setValue('spectatorCamera/switchViewFromController', setting); + } + + // Function Name: registerButtonMappings() + // + // Description: + // -Updates controller button mappings for Spectator Camera. + // + // Relevant Variables: + // -controllerMappingName: The name of the controller mapping. + // -controllerMapping: The controller mapping itself. + // -controllerType: "OculusTouch", "Vive", "Other". + var controllerMappingName; + var controllerMapping; + var controllerType = "Other"; + function registerButtonMappings() { + var VRDevices = Controller.getDeviceNames().toString(); + if (VRDevices) { + if (VRDevices.indexOf("Vive") !== -1) { + controllerType = "Vive"; + } else if (VRDevices.indexOf("OculusTouch") !== -1) { + controllerType = "OculusTouch"; + } else { + sendToQml({ method: 'updateControllerMappingCheckbox', setting: switchViewFromController, controller: controllerType }); + return; // Neither Vive nor Touch detected + } + } + + controllerMappingName = 'Hifi-SpectatorCamera-Mapping'; + controllerMapping = Controller.newMapping(controllerMappingName); + if (controllerType === "OculusTouch") { + controllerMapping.from(Controller.Standard.LS).to(function (value) { + if (value === 1.0) { + setMonitorShowsCameraViewAndSendToQml(!monitorShowsCameraView); + } + return; + }); + } else if (controllerType === "Vive") { + controllerMapping.from(Controller.Standard.LeftPrimaryThumb).to(function (value) { + if (value === 1.0) { + setMonitorShowsCameraViewAndSendToQml(!monitorShowsCameraView); + } + return; + }); + } + setControllerMappingStatus(switchViewFromController); + sendToQml({ method: 'updateControllerMappingCheckbox', setting: switchViewFromController, controller: controllerType }); + } + + // Function Name: onTabletButtonClicked() + // + // Description: + // -Fired when the Spectator Camera app button is pressed. + // + // Relevant Variables: + // -SPECTATOR_CAMERA_QML_SOURCE: The path to the SpectatorCamera QML + // -onSpectatorCameraScreen: true/false depending on whether we're looking at the spectator camera app. + var SPECTATOR_CAMERA_QML_SOURCE = Script.resourcesPath() + "qml/hifi/SpectatorCamera.qml"; + var onSpectatorCameraScreen = false; + function onTabletButtonClicked() { + if (!tablet) { + print("Warning in onTabletButtonClicked(): 'tablet' undefined!"); + return; + } + if (onSpectatorCameraScreen) { + // for toolbar-mode: go back to home screen, this will close the window. + tablet.gotoHomeScreen(); + } else { + tablet.loadQMLSource(SPECTATOR_CAMERA_QML_SOURCE); + sendToQml({ method: 'updateSpectatorCameraCheckbox', params: !!camera }); + sendToQml({ method: 'updateMonitorShowsSwitch', params: monitorShowsCameraView }); + if (!controllerMapping) { + registerButtonMappings(); + } else { + sendToQml({ method: 'updateControllerMappingCheckbox', setting: switchViewFromController, controller: controllerType }); + } + Menu.setIsOptionChecked("Disable Preview", false); + Menu.setIsOptionChecked("Mono Preview", true); + } + } + + // Function Name: onTabletScreenChanged() + // + // Description: + // -Called when the TabletScriptingInterface::screenChanged() signal is emitted. The "type" argument can be either the string + // value of "Home", "Web", "Menu", "QML", or "Closed". The "url" argument is only valid for Web and QML. + function onTabletScreenChanged(type, url) { + onSpectatorCameraScreen = (type === "QML" && url === SPECTATOR_CAMERA_QML_SOURCE); + wireEventBridge(onSpectatorCameraScreen); + // Change button to active when window is first openend OR if the camera is on, false otherwise. + if (button) { + button.editProperties({ isActive: onSpectatorCameraScreen || camera }); + } + } + + // Function Name: sendToQml() + // + // Description: + // -Use this function to send a message to the QML (i.e. to change appearances). The "message" argument is what is sent to + // SpectatorCamera QML in the format "{method, params}", like json-rpc. See also fromQml(). + function sendToQml(message) { + tablet.sendToQml(message); + } + + // Function Name: fromQml() + // + // Description: + // -Called when a message is received from SpectatorCamera.qml. The "message" argument is what is sent from the SpectatorCamera QML + // in the format "{method, params}", like json-rpc. See also sendToQml(). + function fromQml(message) { + switch (message.method) { + case 'spectatorCameraOn': + spectatorCameraOn(); + break; + case 'spectatorCameraOff': + spectatorCameraOff(); + break; + case 'setMonitorShowsCameraView': + setMonitorShowsCameraView(message.params); + break; + case 'changeSwitchViewFromControllerPreference': + setSwitchViewFromController(message.params); + break; + default: + print('Unrecognized message from SpectatorCamera.qml:', JSON.stringify(message)); + } + } + + // Function Name: onHMDChanged() + // + // Description: + // -Called from C++ when HMD mode is changed. The argument "isHMDMode" is true if HMD is on; false otherwise. + function onHMDChanged(isHMDMode) { + if (!controllerMapping) { + registerButtonMappings(); + } + setDisplay(monitorShowsCameraView); + addOrRemoveButton(false, isHMDMode); + if (!isHMDMode && !showSpectatorInDesktop) { + spectatorCameraOff(); + } + } + + // Function Name: shutdown() + // + // Description: + // -shutdown() will be called when the script ends (i.e. is stopped). + function shutdown() { + spectatorCameraOff(); + Window.domainChanged.disconnect(onDomainChanged); + Window.geometryChanged.disconnect(resizeViewFinderOverlay); + addOrRemoveButton(true, HMD.active); + if (tablet) { + tablet.screenChanged.disconnect(onTabletScreenChanged); + if (onSpectatorCameraScreen) { + tablet.gotoHomeScreen(); + } + } + HMD.displayModeChanged.disconnect(onHMDChanged); + Controller.keyPressEvent.disconnect(keyPressEvent); + if (controllerMapping) { + controllerMapping.disable(); + } + } + + // Function Name: onDomainChanged() + // + // Description: + // -A small utility function used when the Window.domainChanged() signal is fired. + function onDomainChanged() { + spectatorCameraOff(true); + } + + // These functions will be called when the script is loaded. + var CAMERA_ON_SOUND = SoundCache.getSound(Script.resolvePath("cameraOn.wav")); + startup(); + Script.scriptEnding.connect(shutdown); + +}()); // END LOCAL_SCOPE diff --git a/unpublishedScripts/marketplace/xylophone/pUtils.js b/unpublishedScripts/marketplace/xylophone/pUtils.js deleted file mode 100644 index 2cafbc1f50..0000000000 --- a/unpublishedScripts/marketplace/xylophone/pUtils.js +++ /dev/null @@ -1,34 +0,0 @@ -// -// pUtils.js -// -// Created by Patrick Gosch on 03/28/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 -// - -getEntityTextures = function(id) { - var results = null; - var properties = Entities.getEntityProperties(id, "textures"); - if (properties.textures) { - try { - results = JSON.parse(properties.textures); - } catch (err) { - logDebug(err); - logDebug(properties.textures); - } - } - return results ? results : {}; -}; - -setEntityTextures = function(id, textureList) { - var json = JSON.stringify(textureList); - Entities.editEntity(id, {textures: json}); -}; - -editEntityTextures = function(id, textureName, textureURL) { - var textureList = getEntityTextures(id); - textureList[textureName] = textureURL; - setEntityTextures(id, textureList); -}; diff --git a/unpublishedScripts/marketplace/xylophone/xylophoneKey.js b/unpublishedScripts/marketplace/xylophone/xylophoneKey.js index afba7e8075..3d131252c8 100644 --- a/unpublishedScripts/marketplace/xylophone/xylophoneKey.js +++ b/unpublishedScripts/marketplace/xylophone/xylophoneKey.js @@ -9,12 +9,12 @@ // (function() { - Script.include(Script.resolvePath("pUtils.js")); - var TIMEOUT = 150; - var TEXGRAY = Script.resolvePath("xylotex_bar_gray.png"); - var TEXBLACK = Script.resolvePath("xylotex_bar_black.png"); + var TIMEOUT = 50; // at 30 ms, the key's color sometimes fails to switch when hit + var TEXTURE_GRAY = Script.resolvePath("xylotex_bar_gray.png"); + var TEXTURE_BLACK = Script.resolvePath("xylotex_bar_black.png"); + var IS_DEBUG = false; var _this; - + function XylophoneKey() { _this = this; } @@ -22,7 +22,7 @@ XylophoneKey.prototype = { sound: null, isWaiting: false, - homePos: null, + homePosition: null, injector: null, preload: function(entityID) { @@ -34,31 +34,66 @@ collisionWithEntity: function(thisEntity, otherEntity, collision) { if (collision.type === 0) { - _this.hit(); + _this.hit(otherEntity); } }, - clickDownOnEntity: function() { - _this.hit(); + clickDownOnEntity: function(otherEntity) { + _this.hit(otherEntity); }, - hit: function() { + hit: function(otherEntity) { if (!_this.isWaiting) { _this.isWaiting = true; - _this.homePos = Entities.getEntityProperties(_this.entityID, ["position"]).position; - _this.injector = Audio.playSound(_this.sound, {position: _this.homePos, volume: 1}); - editEntityTextures(_this.entityID, "file5", TEXGRAY); + _this.homePosition = Entities.getEntityProperties(_this.entityID, ["position"]).position; + _this.injector = Audio.playSound(_this.sound, {position: _this.homePosition, volume: 1}); + _this.editEntityTextures(_this.entityID, "file5", TEXTURE_GRAY); + + var HAPTIC_STRENGTH = 1; + var HAPTIC_DURATION = 20; + var userData = JSON.parse(Entities.getEntityProperties(otherEntity, 'userData').userData); + if (userData.hasOwnProperty('hand')){ + Controller.triggerHapticPulse(HAPTIC_STRENGTH, HAPTIC_DURATION, userData.hand); + } + _this.timeout(); } }, timeout: function() { Script.setTimeout(function() { - editEntityTextures(_this.entityID, "file5", TEXBLACK); + _this.editEntityTextures(_this.entityID, "file5", TEXTURE_BLACK); _this.isWaiting = false; }, TIMEOUT); + }, + + getEntityTextures: function(id) { + var results = null; + var properties = Entities.getEntityProperties(id, "textures"); + if (properties.textures) { + try { + results = JSON.parse(properties.textures); + } catch (err) { + if (IS_DEBUG) { + print(err); + print(properties.textures); + } + } + } + return results ? results : {}; + }, + + setEntityTextures: function(id, textureList) { + var json = JSON.stringify(textureList); + Entities.editEntity(id, {textures: json}); + }, + + editEntityTextures: function(id, textureName, textureURL) { + var textureList = _this.getEntityTextures(id); + textureList[textureName] = textureURL; + _this.setEntityTextures(id, textureList); + } }; return new XylophoneKey(); - }); diff --git a/unpublishedScripts/marketplace/xylophone/xylophoneMallet.js b/unpublishedScripts/marketplace/xylophone/xylophoneMallet.js new file mode 100644 index 0000000000..061d1303bb --- /dev/null +++ b/unpublishedScripts/marketplace/xylophone/xylophoneMallet.js @@ -0,0 +1,25 @@ +// +// xylophoneMallet.js +// +// Created by Johnathan Franck on 07/30/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() { + function XylophoneMallet() { + } + + XylophoneMallet.prototype = { + startEquip: function(entityID, args) { + var LEFT_HAND = 0; + var RIGHT_HAND = 1; + var userData = JSON.parse(Entities.getEntityProperties(entityID, 'userData').userData); + userData.hand = args[0] === "left" ? LEFT_HAND : RIGHT_HAND; + Entities.editEntity(entityID, {userData: JSON.stringify(userData)}); + } + }; + + return new XylophoneMallet(); +}); diff --git a/unpublishedScripts/marketplace/xylophone/xylophoneRezzer.js b/unpublishedScripts/marketplace/xylophone/xylophoneRezzer.js index 6416f81037..6adf8710a7 100644 --- a/unpublishedScripts/marketplace/xylophone/xylophoneRezzer.js +++ b/unpublishedScripts/marketplace/xylophone/xylophoneRezzer.js @@ -8,65 +8,70 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -var soundFiles = ["C4.wav", "D4.wav", "E4.wav", "F4.wav", "G4.wav", "A4.wav", "B4.wav", "C5.wav"]; -var keyModelURL = Script.resolvePath("xyloKey_2_a_e.fbx"); -var keyScriptURL = Script.resolvePath("xylophoneKey.js"); -var TEXBLACK = Script.resolvePath("xylotex_bar_black.png"); -var malletModelURL = Script.resolvePath("Mallet3-2pc.fbx"); -var malletModelColliderURL = Script.resolvePath("Mallet3-2bpc_phys.obj"); +var SOUND_FILES = ["C4.wav", "D4.wav", "E4.wav", "F4.wav", "G4.wav", "A4.wav", "B4.wav", "C5.wav"]; +var KEY_MODEL_URL = Script.resolvePath("xyloKey_2_a_e.fbx"); +var KEY_SCRIPT_URL = Script.resolvePath("xylophoneKey.js"); +var MALLET_SCRIPT_URL = Script.resolvePath("xylophoneMallet.js"); +var TEXTURE_BLACK = Script.resolvePath("xylotex_bar_black.png"); +var MALLET_MODEL_URL = Script.resolvePath("Mallet3-2pc.fbx"); +var MALLET_MODEL_COLLIDER_URL = Script.resolvePath("Mallet3-2bpc_phys.obj"); +var FORWARD = { x: 0, y: 0, z: -1 }; var center = MyAvatar.position; -var fwd = {x:0, y:0, z:-1}; -var xyloFramePos = Vec3.sum(center, Vec3.multiply(fwd, 0.8)); -var xyloFrameID = Entities.addEntity( { +var XYLOPHONE_FORWARD_OFFSET = 0.8; +var xylophoneFramePosition = Vec3.sum(center, Vec3.multiply(FORWARD, XYLOPHONE_FORWARD_OFFSET)); +var xylophoneFrameID = Entities.addEntity({ name: "Xylophone", type: "Model", modelURL: Script.resolvePath("xylophoneFrameWithWave.fbx"), - position: xyloFramePos, - rotation: Quat.fromVec3Radians({x:0, y:Math.PI, z:0}), + position: xylophoneFramePosition, + rotation: Quat.fromVec3Radians({ x: 0, y: Math.PI, z: 0 }), shapeType: "static-mesh" }); -center.y += (0.45); // key Y offset from frame -var keyPos, keyRot, ud, td, keyID; -for (var i = 1; i <= soundFiles.length; i++) { +var KEY_Y_OFFSET = 0.45; +center.y += KEY_Y_OFFSET; +var keyPosition, keyRotation, userData, textureData, keyID; +var ROTATION_START = 0.9; +var ROTATION_DELTA = 0.2; +for (var i = 1; i <= SOUND_FILES.length; i++) { + + keyRotation = Quat.fromVec3Radians({ x: 0, y: ROTATION_START - (i*ROTATION_DELTA), z: 0 }); + keyPosition = Vec3.sum(center, Vec3.multiplyQbyV(keyRotation, FORWARD)); - keyRot = Quat.fromVec3Radians({x:0, y:(0.9 - (i*0.2)), z:0}); - keyPos = Vec3.sum(center, Vec3.multiplyQbyV(keyRot, fwd)); - - ud = { - soundFile: soundFiles[i-1] + userData = { + soundFile: SOUND_FILES[i-1] }; - td = { + textureData = { "file4": Script.resolvePath("xylotex_bar" + i + ".png"), - "file5": TEXBLACK + "file5": TEXTURE_BLACK }; - keyID = Entities.addEntity( { + keyID = Entities.addEntity({ name: ("XyloKey" + i), type: "Model", - modelURL: keyModelURL, - position: keyPos, - rotation: keyRot, + modelURL: KEY_MODEL_URL, + position: keyPosition, + rotation: keyRotation, shapeType: "static-mesh", - script: keyScriptURL, - textures: JSON.stringify(td), - userData: JSON.stringify(ud), - parentID: xyloFrameID - } ); + script: KEY_SCRIPT_URL, + textures: JSON.stringify(textureData), + userData: JSON.stringify(userData), + parentID: xylophoneFrameID + }); } // if rezzed on/above something, wait until after model has loaded so you can read its dimensions then move object on to that surface. -var pickRay = {origin: center, direction: {x:0, y:-1, z:0}}; +var pickRay = {origin: center, direction: {x: 0, y: -1, z: 0}}; var intersection = Entities.findRayIntersection(pickRay, true); if (intersection.intersects && (intersection.distance < 10)) { var surfaceY = intersection.intersection.y; Script.setTimeout( function() { // should add loop to check for fbx loaded instead of delay - var xyloDimensions = Entities.getEntityProperties(xyloFrameID, ["dimensions"]).dimensions; - xyloFramePos.y = surfaceY + (xyloDimensions.y/2); - Entities.editEntity(xyloFrameID, {position: xyloFramePos}); + var xylophoneDimensions = Entities.getEntityProperties(xylophoneFrameID, ["dimensions"]).dimensions; + xylophoneFramePosition.y = surfaceY + (xylophoneDimensions.y/2); + Entities.editEntity(xylophoneFrameID, {position: xylophoneFramePosition}); rezMallets(); }, 2000); } else { @@ -75,28 +80,50 @@ if (intersection.intersects && (intersection.distance < 10)) { } function rezMallets() { - var malletProps = { + var malletProperties = { name: "Xylophone Mallet", type: "Model", - modelURL: malletModelURL, - compoundShapeURL: malletModelColliderURL, - collidesWith: "static,dynamic,kinematic,", + modelURL: MALLET_MODEL_URL, + compoundShapeURL: MALLET_MODEL_COLLIDER_URL, + collidesWith: "static,dynamic,kinematic", collisionMask: 7, collisionsWillMove: 1, dynamic: 1, damping: 1, angularDamping: 1, shapeType: "compound", - userData: "{\"grabbableKey\":{\"grabbable\":true}}", - dimensions: {"x": 0.057845603674650192, "y": 0.057845607399940491, "z": 0.30429631471633911} // not being set from fbx for some reason. + script: MALLET_SCRIPT_URL, + userData: JSON.stringify({ + grabbableKey: { + invertSolidWhileHeld: true + }, + wearable: { + joints: { + LeftHand: [ + { x: 0, y: 0.2, z: 0.04 }, + Quat.fromVec3Degrees({ x: 0, y: 90, z: 90 }) + ], + RightHand: [ + { x: 0, y: 0.2, z: 0.04 }, + Quat.fromVec3Degrees({ x: 0, y: 90, z: 90 }) + ] + } + } + }), + dimensions: { "x": 0.057845603674650192, "y": 0.057845607399940491, "z": 0.30429631471633911 } // not being set from fbx for some reason. }; - malletProps.position = Vec3.sum(xyloFramePos, {x: 0.1, y: 0.55, z: 0}); - malletProps.rotation = Quat.fromVec3Radians({x:0, y:Math.PI - 0.1, z:0}); - Entities.addEntity(malletProps); + var LEFT_MALLET_POSITION = { x: 0.1, y: 0.55, z: 0 }; + var LEFT_MALLET_ROTATION = { x: 0, y: Math.PI - 0.1, z: 0 }; + var RIGHT_MALLET_POSITION = { x: -0.1, y: 0.55, z: 0 }; + var RIGHT_MALLET_ROTATION = { x: 0, y: Math.PI + 0.1, z: 0 }; - malletProps.position = Vec3.sum(xyloFramePos, {x: -0.1, y: 0.55, z: 0}); - malletProps.rotation = Quat.fromVec3Radians({x:0, y:Math.PI + 0.1, z:0}); - Entities.addEntity(malletProps); + malletProperties.position = Vec3.sum(xylophoneFramePosition, LEFT_MALLET_POSITION); + malletProperties.rotation = Quat.fromVec3Radians(LEFT_MALLET_ROTATION); + Entities.addEntity(malletProperties); + + malletProperties.position = Vec3.sum(xylophoneFramePosition, RIGHT_MALLET_POSITION); + malletProperties.rotation = Quat.fromVec3Radians(RIGHT_MALLET_ROTATION); + Entities.addEntity(malletProperties); Script.stop(); } \ No newline at end of file