diff --git a/BUILD.md b/BUILD.md index 25bbc89951..bd264a74ad 100644 --- a/BUILD.md +++ b/BUILD.md @@ -6,26 +6,25 @@ * [BUILD_ANDROID.md](BUILD_ANDROID.md) - additional instructions for Android ### Dependencies - +- [git](https://git-scm.com/downloads): >= 1.6 - [cmake](https://cmake.org/download/): 3.9 -- [Qt](https://www.qt.io/download-open-source): 5.10.1 - [Python](https://www.python.org/downloads/): 3.6 or higher ### CMake External Project Dependencies 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 -- [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 -- [vcpkg](https://github.com/highfidelity/vcpkg): -- [VHACD](https://github.com/virneo/v-hacd) -- [zlib](http://www.zlib.net/): 1.28 (Win32 only) -- [nvtt](https://github.com/highfidelity/nvidia-texture-tools): 2.1.1 (customized) +- [Bullet Physics Engine](https://github.com/bulletphysics/bullet3/releases): 2.83 +- [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 +- [vcpkg](https://github.com/highfidelity/vcpkg): +- [VHACD](https://github.com/virneo/v-hacd) +- [zlib](http://www.zlib.net/): 1.28 (Win32 only) +- [nvtt](https://github.com/highfidelity/nvidia-texture-tools): 2.1.1 (customized) 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. @@ -36,16 +35,14 @@ These are not placed in your normal build tree when doing an out of source build Hifi uses CMake to generate build files and project files for your platform. #### Qt +CMake will download Qt 5.12.3 using vcpkg. -In order for CMake to find the Qt5 find modules, you will need to set a QT_CMAKE_PREFIX_PATH environment variable pointing to your Qt installation. +To override this (i.e. use an installed Qt configuration - you will need to set a QT_CMAKE_PREFIX_PATH environment variable pointing to your Qt **lib/cmake** folder. +This can either be entered directly into your shell session before you build or in your shell profile (e.g.: ~/.bash_profile, ~/.bashrc, ~/.zshrc - this depends on your shell and environment). The path it needs to be set to will depend on where and how Qt5 was installed. e.g. -This can either be entered directly into your shell session before you build or in your shell profile (e.g.: ~/.bash_profile, ~/.bashrc, ~/.zshrc - this depends on your shell and environment). - -The path it needs to be set to will depend on where and how Qt5 was installed. e.g. - - export QT_CMAKE_PREFIX_PATH=/usr/local/Qt5.10.1/5.10.1/gcc_64/lib/cmake - export QT_CMAKE_PREFIX_PATH=/usr/local/qt/5.10.1/clang_64/lib/cmake/ - export QT_CMAKE_PREFIX_PATH=/usr/local/Cellar/qt5/5.10.1/lib/cmake + export QT_CMAKE_PREFIX_PATH=/usr/local/Qt5.12.3/gcc_64/lib/cmake + export QT_CMAKE_PREFIX_PATH=/usr/local/qt/5.12.3/clang_64/lib/cmake/ + export QT_CMAKE_PREFIX_PATH=/usr/local/Cellar/qt5/5.12.3/lib/cmake export QT_CMAKE_PREFIX_PATH=/usr/local/opt/qt5/lib/cmake #### Vcpkg @@ -68,15 +65,15 @@ Create a build directory in the root of your checkout and then run the CMake bui cd build cmake .. -If cmake gives you the same error message repeatedly after the build fails (e.g. you had a typo in the QT_CMAKE_PREFIX_PATH that you fixed but the `.cmake` files still cannot be found), try removing `CMakeCache.txt`. +If cmake gives you the same error message repeatedly after the build fails, try removing `CMakeCache.txt`. #### Variables Any variables that need to be set for CMake to find dependencies can be set as ENV variables in your shell profile, or passed directly to CMake with a `-D` flag appended to the `cmake ..` command. -For example, to pass the QT_CMAKE_PREFIX_PATH variable during build file generation: +For example, to pass the QT_CMAKE_PREFIX_PATH variable (if not using the vcpkg'ed version) during build file generation: - cmake .. -DQT_CMAKE_PREFIX_PATH=/usr/local/qt/5.10.1/lib/cmake + cmake .. -DQT_CMAKE_PREFIX_PATH=/usr/local/qt/5.12.3/lib/cmake #### Finding Dependencies diff --git a/BUILD_ANDROID.md b/BUILD_ANDROID.md index c111e758b5..3047cb827d 100644 --- a/BUILD_ANDROID.md +++ b/BUILD_ANDROID.md @@ -1,64 +1,118 @@ -Please read the [general build guide](BUILD.md) for information on building other platform. Only Android specific instructions are found in this file. +Please read the [general build guide](BUILD.md) for information on building other platforms. Only Android specific instructions are found in this file. **Note that these instructions apply to building for Oculus Quest.** # Dependencies -Building is currently supported on OSX, Windows and Linux platforms, but developers intending to do work on the library dependencies are strongly urged to use 64 bit Linux as a build platform +Building is currently supported on Windows, OSX and Linux, but developers intending to do work on the library dependencies are strongly urged to use 64 bit Linux as a build platform. -You will need the following tools to build Android targets. +### OS specific dependencies -* [Android Studio](https://developer.android.com/studio/index.html) +Please install the dependencies for your OS using the [Windows](BUILD_WIN.md), [OSX](BUILD_OSX.md) or [Linux](BUILD_LINUX.md) build instructions before attempting to build for Android. ### Android Studio -Download the Android Studio installer and run it. Once installed, at the welcome screen, click configure in the lower right corner and select SDK manager +Download the [Android Studio](https://developer.android.com/studio/index.html) installer and run it. Once installed, at the welcome screen, click _Configure_ in the lower right corner and select _SDK Manager_. -From the SDK Platforms tab, select API levels 24 and 26. +From the _SDK Platforms_ tab, select API levels 26 and 28. -From the SDK Tools tab select the following +From the _SDK Tools_ tab, select the following * Android SDK Build-Tools * GPU Debugging Tools -* CMake (even if you have a separate CMake installation) * LLDB * Android SDK Platform-Tools * Android SDK Tools * NDK (even if you have the NDK installed separately) -Make sure the NDK installed version is 18 (or higher) +Still in the _SDK Tools_ tab, click _Show Package Details_. Select CMake 3.6.4. Do this even if you have a separate CMake installation. -# Environment +Also, make sure the NDK installed version is 18 (or higher). -Setting up the environment for android builds requires some additional steps +# Environment -#### Set up machine specific Gradle properties +### Create a keystore in Android Studio +Follow the directions [here](https://developer.android.com/studio/publish/app-signing#generate-key) to create a keystore file. You can save it anywhere (preferably not in the `hifi` folder). -Create a `gradle.properties` file in $HOME/.gradle. Edit the file to contain the following +### Set up machine specific Gradle properties + +Create a `gradle.properties` file in the `.gradle` folder (`$HOME/.gradle` on Unix, `Users//.gradle` on Windows). Edit the file to contain the following HIFI_ANDROID_PRECOMPILED=/Android/hifi_externals + HIFI_ANDROID_KEYSTORE=/.jks + HIFI_ANDROID_KEYSTORE_PASSWORD= + HIFI_ANDROID_KEY_ALIAS= + HIFI_ANDROID_KEY_PASSWORD= -Note, do not use `$HOME` for the path. It must be a fully qualified path name. +Note, do not use $HOME for the path. It must be a fully qualified path name. Also, be sure to use forward slashes in your path. -### Setup the repository +#### If you are building for an Android phone -Clone the repository +Add these lines to `gradle.properties` -`git clone https://github.com/highfidelity/hifi.git` + SUPPRESS_QUEST_INTERFACE + SUPPRESS_QUEST_FRAME_PLAYER -Enter the repository `android` directory +#### If you are building for an Oculus Quest -`cd hifi/android` +Add these lines to `gradle.properties` -Execute two gradle pre-build steps. This steps should only need to be done once, unless you're working on the Android dependencies + SUPPRESS_INTERFACE + SUPPRESS_FRAME_PLAYER -`./gradlew extractDependencies` +The above code to suppress modules is not necessary, but will speed up the build process. -`./gradlew setupDependencies` +### Clone the repository + +`git clone https://github.com/highfidelity/hifi.git ` # Building & Running -* Open Android Studio -* Choose _Open Existing Android Studio Project_ -* Navigate to the `hifi` repository and choose the `android` folder and select _OK_ -* From the _Build_ menu select _Make Project_ -* Once the build completes, from the _Run_ menu select _Run App_ +### Building Modules +* Open Android Studio +* Choose _Open an existing Android Studio project_ +* Navigate to the `hifi` repository and choose the `android` folder and select _OK_ +* Wait for Gradle to sync (this should take around 20 minutes the first time) +* From the _Build_ menu select _Make Project_ + +### Running a Module + +* In the toolbar at the top of Android Studio, next to the green hammer icon, you should see a dropdown menu. +* You may already see a configuration for the module you are trying to build. If so, select it. +* Otherwise, select _Edit Configurations_. + +Your configuration should be as follows + +* Type: Android App +* Module: (you probably want `interface` or `questInterface`) + +For the interface modules, you also need to select the activity to launch. + +#### For the Android phone interface + +* From the _Launch_ drop down menu, select _Specified Activity_ +* In the _Activity_ field directly below, put `io.highfidelity.hifiinterface.PermissionChecker` + +#### For the Oculus Quest interface + +* From the _Launch_ drop down menu, select _Specified Activity_ +* In the _Activity_ field directly below, put `io.highfidelity.questInterface.PermissionsChecker` + +Note the 's' in Permission**s**Checker for the Quest. + +Now you are able to run your module! Click the green play button in the top toolbar of Android Studio +r +# Troubleshooting + +To view a more complete debug log, + +* Click the icon with the two overlapping squares in the upper left corner of the tab where the sync is running (hover text says _Toggle view_) +* To change verbosity, click _File > Settings_. Under _Build, Execution, Deployment > Compiler_ you can add command-line flags, as per Gradle documentation + +Some things you can try if you want to do a clean build + +* Delete the `build` and `.externalNativeBuild` folders from the folder for each module you're building (for example, `hifi/android/apps/interface`) +* If you have set your `HIFI_VCPKG_ROOT` environment variable, delete the contents of that directory; otherwise, delete `AppData/Local/Temp/hifi` +* In Android Studio, click _File > Invalidate Caches / Restart_ and select _Invalidate and Restart_ + +If you see lots of "couldn't acquire lock" errors, +* Open Task Manager and close any running Clang / Gradle processes \ No newline at end of file diff --git a/BUILD_LINUX.md b/BUILD_LINUX.md index 4cfe2f59a0..c0cef86ba4 100644 --- a/BUILD_LINUX.md +++ b/BUILD_LINUX.md @@ -2,56 +2,72 @@ 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 - -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 zlib1g-dev - ## Ubuntu 16.04/18.04 specific build guide - -### Ubuntu 18.04 only +### Ubuntu 16.04 only +Add the following line to *.bash_profile* +`export QT_QPA_FONTDIR=/usr/share/fonts/truetype/dejavu/` +### Ubuntu 18.04 server only Add the universe repository: _(This is not enabled by default on the server edition)_ ```bash sudo add-apt-repository universe sudo apt-get update ``` - -### Prepare environment -Install Qt 5.10.1: +#### Install build tools: +1. First update the repositiories: ```bash -wget http://debian.highfidelity.com/pool/h/hi/hifiqt5.10.1_5.10.1_amd64.deb -sudo dpkg -i hifiqt5.10.1_5.10.1_amd64.deb +sudo apt-get update -y +sudo apt-get upgrade -y ``` - -Install build dependencies: +1. git ```bash -sudo apt-get install libasound2 libxmu-dev libxi-dev freeglut3-dev libasound2-dev libjack0 libjack-dev libxrandr-dev libudev-dev libssl-dev zlib1g-dev +sudo apt-get install git -y ``` - -To compile interface in a server you must install: +Verify by git --version +1. g++ +```bash +sudo apt-get install g++ -y +``` +Verify by g++ --version +1. *Ubuntu 18.04* cmake +```bash +sudo apt-get install cmake -y +``` +Verify by git --version +1. *Ubuntu 16.04* cmake +```bash +wget https://cmake.org/files/v3.14/cmake-3.14.2-Linux-x86_64.sh +sudo sh cmake-3.14.2-Linux-x86_64.sh --prefix=/usr/local --exclude-subdir +``` +#### Install build dependencies: +1. OpenSSL +```bash +sudo apt-get install libssl-dev +``` +Verify with `openssl version` +1. OpenGL +Verify (first install mesa-utils - `sudo apt install mesa-utils -y`) by `glxinfo | grep "OpenGL version"` +```bash +sudo apt-get install libgl1-mesa-dev -y +sudo ln -s /usr/lib/x86_64-linux-gnu/libGL.so.346.35 /usr/lib/x86_64-linux-gnu/libGL.so.1.2.0 +``` +#### To compile interface in a server you must install: ```bash sudo apt-get -y install libpulse0 libnss3 libnspr4 libfontconfig1 libxcursor1 libxcomposite1 libxtst6 libxslt1.1 ``` - -Install build tools: +1. Misc dependencies ```bash -# For Ubuntu 18.04 -sudo apt-get install cmake +sudo apt-get install libasound2 libxmu-dev libxi-dev freeglut3-dev libasound2-dev libjack0 libjack-dev libxrandr-dev libudev-dev libssl-dev zlib1g-dev ``` +1. To compile interface in a server you must install: ```bash -# For Ubuntu 16.04 -wget https://cmake.org/files/v3.9/cmake-3.9.5-Linux-x86_64.sh -sudo sh cmake-3.9.5-Linux-x86_64.sh --prefix=/usr/local --exclude-subdir +sudo apt-get -y install libpulse0 libnss3 libnspr4 libfontconfig1 libxcursor1 libxcomposite1 libxtst6 libxslt1.1 ``` - -Install Python 3: +1. Install Python 3: ```bash sudo apt-get install python3.6 ``` - -Install node, required to build the jsdoc documentation +1. Install node, required to build the jsdoc documentation ```bash sudo apt-get install nodejs ``` @@ -84,9 +100,11 @@ cd hifi/build Prepare makefiles: ```bash -cmake -DQT_CMAKE_PREFIX_PATH=/usr/local/Qt5.10.1/5.10.1/gcc_64/lib/cmake .. +cmake .. ``` +* If cmake fails with a vcpkg error - delete /tmp/hifi/vcpkg. + Start compilation of the server and get a cup of coffee: ```bash make domain-server assignment-client @@ -128,46 +146,34 @@ Go to localhost in the running interface. In Ubuntu 18.04 there is a problem related with NVidia driver library version. -It can be workarounded following these steps: +It can be worked around following these steps: -Uninstall incompatible nvtt libraries: -```bash -sudo apt-get remove libnvtt2 libnvtt-dev -``` +1. Uninstall incompatible nvtt libraries: +`sudo apt-get remove libnvtt2 libnvtt-dev` -Install libssl1.0-dev: -```bash -sudo apt-get -y install libssl1.0-dev -``` +1. Install libssl1.0-dev: +`sudo apt-get -y install libssl1.0-dev` -Clone castano nvidia-texture-tools: -``` -git clone https://github.com/castano/nvidia-texture-tools -cd nvidia-texture-tools/ -``` +1. Clone castano nvidia-texture-tools: +`git clone https://github.com/castano/nvidia-texture-tools` +`cd nvidia-texture-tools/` -Make these changes in repo: -* In file **VERSION** set `2.2.1` -* In file **configure**: - * set `build="release"` - * set `-DNVTT_SHARED=1` +1. Make these changes in repo: +* In file **VERSION** set `2.2.1` +* In file **configure**: + * set `build="release"` + * set `-DNVTT_SHARED=1` -Configure, build and install: -``` -./configure -make -sudo make install -``` +1. Configure, build and install: +`./configure` +`make` +`sudo make install` -Link compiled files: -``` -sudo ln -s /usr/local/lib/libnvcore.so /usr/lib/libnvcore.so -sudo ln -s /usr/local/lib/libnvimage.so /usr/lib/libnvimage.so -sudo ln -s /usr/local/lib/libnvmath.so /usr/lib/libnvmath.so -sudo ln -s /usr/local/lib/libnvtt.so /usr/lib/libnvtt.so -``` +1.. Link compiled files: +`sudo ln -s /usr/local/lib/libnvcore.so /usr/lib/libnvcore.so` +`sudo ln -s /usr/local/lib/libnvimage.so /usr/lib/libnvimage.so` +`sudo ln -s /usr/local/lib/libnvmath.so /usr/lib/libnvmath.so` +`sudo ln -s /usr/local/lib/libnvtt.so /usr/lib/libnvtt.so` -After running this steps you can run interface: -``` -interface/interface -``` +1. After running this steps you can run interface: +`interface/interface` diff --git a/BUILD_OSX.md b/BUILD_OSX.md index 488c38e909..875d49bcb8 100644 --- a/BUILD_OSX.md +++ b/BUILD_OSX.md @@ -4,30 +4,21 @@ Please read the [general build guide](BUILD.md) for information on dependencies [Homebrew](https://brew.sh/) is an excellent package manager for macOS. It makes install of some High Fidelity dependencies very simple. - brew install cmake openssl qt + brew install cmake openssl ### Python 3 -Download an install Python 3.6.6 or higher from [here](https://www.python.org/downloads/). Execute the `Update Shell Profile.command` script that is provided with the installer. +Download an install Python 3.6.6 or higher from [here](https://www.python.org/downloads/). +Execute the `Update Shell Profile.command` script that is provided with the installer. ### OpenSSL -Assuming you've installed OpenSSL using the homebrew instructions above, you'll need to set OPENSSL_ROOT_DIR so CMake can find your installations. +Assuming you've installed OpenSSL using the homebrew instructions above, you'll need to set OPENSSL_ROOT_DIR so CMake can find your installations. For OpenSSL installed via homebrew, set OPENSSL_ROOT_DIR: export OPENSSL_ROOT_DIR=/usr/local/Cellar/openssl/1.0.2l Note that this uses the version from the homebrew formula at the time of this writing, and the version in the path will likely change. - -### Qt - -Assuming you've installed Qt using the homebrew instructions above, you'll need to set QT_CMAKE_PREFIX_PATH so CMake can find your installations. -For Qt installed via homebrew, set QT_CMAKE_PREFIX_PATH: - - export QT_CMAKE_PREFIX_PATH=/usr/local/Cellar/qt/5.10.1/lib/cmake - -Note that this uses the version from the homebrew formula at the time of this writing, and the version in the path will likely change. - ### Xcode If Xcode is your editor of choice, you can ask CMake to generate Xcode project files instead of Unix Makefiles. diff --git a/BUILD_QUEST.md b/BUILD_QUEST.md deleted file mode 100644 index e093969f83..0000000000 --- a/BUILD_QUEST.md +++ /dev/null @@ -1,65 +0,0 @@ -Please read the [general build guide](BUILD.md) for information on building other platform. Only Quest specific instructions are found in this file. - -# Dependencies - -Building is currently supported on OSX, Windows and Linux platforms, but developers intending to do work on the library dependencies are strongly urged to use 64 bit Linux as a build platform - -You will need the following tools to build Android targets. - -* [Android Studio](https://developer.android.com/studio/index.html) - -### Android Studio - -Download the Android Studio installer and run it. Once installed, at the welcome screen, click configure in the lower right corner and select SDK manager - -From the SDK Platforms tab, select API levels 24 and 26. - -From the SDK Tools tab select the following - -* Android SDK Build-Tools -* GPU Debugging Tools -* CMake (even if you have a separate CMake installation) -* LLDB -* Android SDK Platform-Tools -* Android SDK Tools -* NDK (even if you have the NDK installed separately) - -Make sure the NDK installed version is 18 (or higher) - -# Environment - -Setting up the environment for android builds requires some additional steps - -#### Set up machine specific Gradle properties - -Create a `gradle.properties` file in $HOME/.gradle. Edit the file to contain the following - - HIFI_ANDROID_PRECOMPILED=/Android/hifi_externals - HIFI_ANDROID_KEYSTORE=/.jks - HIFI_ANDROID_KEYSTORE_PASSWORD= - HIFI_ANDROID_KEY_ALIAS= - HIFI_ANDROID_KEY_PASSWORD= - -Note, do not use `$HOME` for the path. It must be a fully qualified path name. - -### Setup the repository - -Clone the repository - -`git clone https://github.com/highfidelity/hifi.git` - -Enter the repository `android` directory - -`cd hifi/android` - -# Building & Running - -* Open Android Studio -* Choose _Open Existing Android Studio Project_ -* Navigate to the `hifi` repository and choose the `android` folder and select _OK_ -* Open Gradle.settings and comment out any projects not necessary -* From _File_ menu select _Sync with File System_ to resync Gradle settings -* From the _Build_ menu select _Make Project_ -* From -* Once the build completes, from the _Run_ menu select _Run App_ - diff --git a/BUILD_WIN.md b/BUILD_WIN.md index 01ba2887a3..8f8a64a428 100644 --- a/BUILD_WIN.md +++ b/BUILD_WIN.md @@ -1,17 +1,29 @@ -This is a stand-alone guide for creating your first High Fidelity build for Windows 64-bit. - +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.10.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. +Note: We are now using Visual Studio 2017 or 2019 and Qt 5.12.3. +If you are upgrading from previous versions, do a clean uninstall of those versions before going through this guide. Note: The prerequisites will require about 10 GB of space on your drive. You will also need a system with at least 8GB of main memory. -### Step 1. Visual Studio 2017 & Python +### Step 1. Visual Studio & Python -If you don’t have Community or Professional edition of Visual Studio 2017, download [Visual Studio Community 2017](https://www.visualstudio.com/downloads/). +If you don’t have Community or Professional edition of Visual Studio, download [Visual Studio Community 2019](https://visualstudio.microsoft.com/vs/). If you have Visual Studio 2017, you are not required to download Visual Studio 2019. -When selecting components, check "Desktop development with C++". Also on the right on the Summary toolbar, check "Windows 8.1 SDK and UCRT SDK" and "VC++ 2015.3 v140 toolset (x86,x64)". If you do not already have a python development environment installed, also check "Python Development" in this screen. +When selecting components, check "Desktop development with C++". On the right on the Summary toolbar, select the following components. -If you already have Visual Studio installed and need to add python, open the "Add or remove programs" control panel and find the "Microsoft Visual Studio Installer". Select it and click "Modify". In the installer, select "Modify" again, then check "Python Development" and allow the installer to apply the changes. +#### If you're installing Visual Studio 2017, + +* Windows 8.1 SDK and UCRT SDK +* VC++ 2015.3 v14.00 (v140) toolset for desktop + +#### If you're installing Visual Studio 2019, + +* MSVC v141 - VS 2017 C++ x64/x86 build tools +* MSVC v140 - VS 2015 C++ build tools (v14.00) + +If you do not already have a Python development environment installed, also check "Python Development" in this screen. + +If you already have Visual Studio installed and need to add Python, open the "Add or remove programs" control panel and find the "Microsoft Visual Studio Installer". Select it and click "Modify". In the installer, select "Modify" again, then check "Python Development" and allow the installer to apply the changes. ### Step 1a. Alternate Python @@ -22,28 +34,18 @@ If you do not wish to use the Python installation bundled with Visual Studio, yo 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/). You can access the installer on this [3.9 Version page](https://cmake.org/files/v3.9/). During installation, make sure to check "Add CMake to system PATH for all users" when prompted. - -### Step 3. Installing Qt - -Download and install the [Qt Open Source 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.10.1: "msvc2017 64-bit", "Qt WebEngine", and "Qt Script (Deprecated)". - -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": `C:\Qt\5.10.1\msvc2017_64\lib\cmake` - ### Step 5. 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 15 Win64" -``` +Run Command Prompt from Start and run the following commands: +`cd "%HIFI_DIR%"` +`mkdir build` +`cd build` + +#### If you're using Visual Studio 2017, +Run `cmake .. -G "Visual Studio 15 Win64"`. + +#### If you're using Visual Studio 2019, +Run `cmake .. -G "Visual Studio 16 2019" -A x64`. Where `%HIFI_DIR%` is the directory for the highfidelity repository. @@ -73,11 +75,11 @@ Note: You can also run Interface by launching it from command line or File Explo ## Troubleshooting -For any problems after Step #7, first try this: -* Delete your locally cloned copy of the highfidelity repository -* Restart your computer -* Redownload the [repository](https://github.com/highfidelity/hifi) -* Restart directions from Step #7 +For any problems after Step #7, first try this: +* Delete your locally cloned copy of the highfidelity repository +* Restart your computer +* Redownload the [repository](https://github.com/highfidelity/hifi) +* Restart directions from Step #7 #### CMake gives you the same error message repeatedly after the build fails @@ -85,8 +87,4 @@ Remove `CMakeCache.txt` found in the `%HIFI_DIR%\build` directory. #### CMake can't find OpenSSL -Remove `CMakeCache.txt` found in the `%HIFI_DIR%\build` directory. Verify that your VCPKG_ROOT environment variable is set and pointing to the correct location. Verify that the file `${VCPKG_ROOT}/installed/x64-windows/include/openssl/ssl.h` exists. - -#### Qt is throwing an error - -Make sure you have the correct version (5.10.1) installed and `QT_CMAKE_PREFIX_PATH` environment variable is set correctly. +Remove `CMakeCache.txt` found in the `%HIFI_DIR%\build` directory. Verify that your HIFI_VCPKG_BASE environment variable is set and pointing to the correct location. Verify that the file `${HIFI_VCPKG_BASE}/installed/x64-windows/include/openssl/ssl.h` exists. diff --git a/CMakeLists.txt b/CMakeLists.txt index 49d16ffa4e..1f6cffb7c1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -17,14 +17,19 @@ if (APPLE) set(ENV{MACOSX_DEPLOYMENT_TARGET} 10.9) endif() +set(RELEASE_TYPE "$ENV{RELEASE_TYPE}") +if ((NOT "${RELEASE_TYPE}" STREQUAL "PRODUCTION") AND (NOT "${RELEASE_TYPE}" STREQUAL "PR")) + set(RELEASE_TYPE "DEV") +endif() + if (HIFI_ANDROID) execute_process( - COMMAND ${HIFI_PYTHON_EXEC} ${CMAKE_CURRENT_SOURCE_DIR}/prebuild.py --android ${HIFI_ANDROID_APP} --build-root ${CMAKE_BINARY_DIR} + COMMAND ${HIFI_PYTHON_EXEC} ${CMAKE_CURRENT_SOURCE_DIR}/prebuild.py --release-type ${RELEASE_TYPE} --android ${HIFI_ANDROID_APP} --build-root ${CMAKE_BINARY_DIR} WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} ) else() execute_process( - COMMAND ${HIFI_PYTHON_EXEC} ${CMAKE_CURRENT_SOURCE_DIR}/prebuild.py --build-root ${CMAKE_BINARY_DIR} + COMMAND ${HIFI_PYTHON_EXEC} ${CMAKE_CURRENT_SOURCE_DIR}/prebuild.py --release-type ${RELEASE_TYPE} --build-root ${CMAKE_BINARY_DIR} WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} ) # squelch the Policy CMP0074 warning without requiring an update to cmake 3.12. @@ -36,8 +41,8 @@ endif() if(NOT EXISTS "${CMAKE_BINARY_DIR}/vcpkg.cmake") message(FATAL_ERROR "vcpkg configuration missing.") endif() - include("${CMAKE_BINARY_DIR}/vcpkg.cmake") + project(hifi) include("cmake/init.cmake") include("cmake/compiler.cmake") @@ -93,9 +98,9 @@ if (ANDROID) set(PLATFORM_QT_COMPONENTS AndroidExtras WebView) add_definitions(-DHIFI_ANDROID_APP=\"${HIFI_ANDROID_APP}\") if ( - (${HIFI_ANDROID_APP} STREQUAL "questInterface") OR + (${HIFI_ANDROID_APP} STREQUAL "questInterface") OR (${HIFI_ANDROID_APP} STREQUAL "questFramePlayer") OR - (${HIFI_ANDROID_APP} STREQUAL "framePlayer") + (${HIFI_ANDROID_APP} STREQUAL "framePlayer") ) # We know the quest hardware has this extension, so we can force the use of instanced stereo add_definitions(-DHAVE_EXT_clip_cull_distance) @@ -105,18 +110,18 @@ if (ANDROID) # We can also use our own foveated textures add_definitions(-DHAVE_QCOM_texture_foveated) - # if set, the application itself or some library it depends on MUST implement + # if set, the application itself or some library it depends on MUST implement # `DisplayPluginList getDisplayPlugins()` and `InputPluginList getInputPlugins()` - add_definitions(-DCUSTOM_INPUT_PLUGINS) - add_definitions(-DCUSTOM_DISPLAY_PLUGINS) + add_definitions(-DCUSTOM_INPUT_PLUGINS) + add_definitions(-DCUSTOM_DISPLAY_PLUGINS) set(PLATFORM_PLUGIN_LIBRARIES oculusMobile oculusMobilePlugin) endif() # Allow client code to use preprocessor macros to distinguish between quest and non-quest builds if (${HIFI_ANDROID_APP} STREQUAL "questInterface") - add_definitions(-DANDROID_APP_QUEST_INTERFACE) + add_definitions(-DANDROID_APP_QUEST_INTERFACE) elseif(${HIFI_ANDROID_APP} STREQUAL "interface") - add_definitions(-DANDROID_APP_INTERFACE) + add_definitions(-DANDROID_APP_INTERFACE) endif() else () set(PLATFORM_QT_COMPONENTS WebEngine Xml) @@ -194,6 +199,8 @@ GroupSources("scripts") GroupSources("unpublishedScripts") unset(JS_SRC) +set_packaging_parameters() + # Locate the required Qt build on the filesystem setup_qt() list(APPEND CMAKE_PREFIX_PATH "${QT_CMAKE_PREFIX_PATH}") @@ -203,6 +210,12 @@ find_package( Threads ) add_definitions(-DGLM_FORCE_RADIANS) add_definitions(-DGLM_ENABLE_EXPERIMENTAL) add_definitions(-DGLM_FORCE_CTOR_INIT) + +if (WIN32) + # Deal with fakakta Visual Studo 2017 bug + add_definitions(-DQT_NO_FLOAT16_OPERATORS) +endif() + if (HIFI_USE_OPTIMIZED_IK) MESSAGE(STATUS "SET THE USE IK DEFINITION ") add_definitions(-DHIFI_USE_OPTIMIZED_IK) @@ -215,8 +228,6 @@ setup_externals_binary_dir() option(USE_NSIGHT "Attempt to find the nSight libraries" 1) -set_packaging_parameters() - # FIXME hack to work on the proper Android toolchain if (ANDROID) add_subdirectory(android/apps/${HIFI_ANDROID_APP}) @@ -250,11 +261,11 @@ add_subdirectory(tools) if (BUILD_TESTS) # Turn on testing so that add_test works - # MUST be in the root cmake file for ctest to work + # MUST be in the root cmake file for ctest to work include(CTest) enable_testing() add_subdirectory(tests) - if (BUILD_MANUAL_TESTS) + if (BUILD_MANUAL_TESTS) add_subdirectory(tests-manual) endif() endif() diff --git a/assignment-client/src/avatars/AvatarMixerClientData.cpp b/assignment-client/src/avatars/AvatarMixerClientData.cpp index cdd639ed80..361f87a635 100644 --- a/assignment-client/src/avatars/AvatarMixerClientData.cpp +++ b/assignment-client/src/avatars/AvatarMixerClientData.cpp @@ -204,7 +204,10 @@ void AvatarMixerClientData::processSetTraitsMessage(ReceivedMessage& message, if (traitType == AvatarTraits::SkeletonModelURL) { // special handling for skeleton model URL, since we need to make sure it is in the whitelist checkSkeletonURLAgainstWhitelist(slaveSharedData, sendingNode, packetTraitVersion); +#ifdef AVATAR_POP_CHECK + // Deferred for UX work. With no PoP check, no need to get the .fst. _avatar->fetchAvatarFST(); +#endif } anyTraitsChanged = true; diff --git a/assignment-client/src/avatars/ScriptableAvatar.h b/assignment-client/src/avatars/ScriptableAvatar.h index 54e9fa2fba..34dc25914f 100644 --- a/assignment-client/src/avatars/ScriptableAvatar.h +++ b/assignment-client/src/avatars/ScriptableAvatar.h @@ -138,7 +138,7 @@ public: /// Returns the index of the joint with the specified name, or -1 if not found/unknown. Q_INVOKABLE virtual int getJointIndex(const QString& name) const override; - virtual void setSkeletonModelURL(const QUrl& skeletonModelURL) override; + Q_INVOKABLE virtual void setSkeletonModelURL(const QUrl& skeletonModelURL) override; /**jsdoc * @comment Uses the base class's JSDoc. diff --git a/cmake/macros/FixupInterface.cmake b/cmake/macros/FixupInterface.cmake index 93b5cc25fb..0bfc1cb39c 100644 --- a/cmake/macros/FixupInterface.cmake +++ b/cmake/macros/FixupInterface.cmake @@ -10,28 +10,32 @@ # macro(fixup_interface) - if (APPLE) + if (APPLE) + string(REPLACE " " "\\ " ESCAPED_BUNDLE_NAME ${INTERFACE_BUNDLE_NAME}) + string(REPLACE " " "\\ " ESCAPED_INSTALL_PATH ${INTERFACE_INSTALL_DIR}) + set(_INTERFACE_INSTALL_PATH "${ESCAPED_INSTALL_PATH}/${ESCAPED_BUNDLE_NAME}.app") - string(REPLACE " " "\\ " ESCAPED_BUNDLE_NAME ${INTERFACE_BUNDLE_NAME}) - string(REPLACE " " "\\ " ESCAPED_INSTALL_PATH ${INTERFACE_INSTALL_DIR}) - set(_INTERFACE_INSTALL_PATH "${ESCAPED_INSTALL_PATH}/${ESCAPED_BUNDLE_NAME}.app") + find_program(MACDEPLOYQT_COMMAND macdeployqt PATHS "${QT_DIR}/bin" NO_DEFAULT_PATH) - find_program(MACDEPLOYQT_COMMAND macdeployqt PATHS "${QT_DIR}/bin" NO_DEFAULT_PATH) + if (NOT MACDEPLOYQT_COMMAND AND (PRODUCTION_BUILD OR PR_BUILD)) + message(FATAL_ERROR "Could not find macdeployqt at ${QT_DIR}/bin.\ + It is required to produce an relocatable interface application.\ + Check that the variable QT_DIR points to your Qt installation.\ + ") + endif () - if (NOT MACDEPLOYQT_COMMAND AND (PRODUCTION_BUILD OR PR_BUILD)) - message(FATAL_ERROR "Could not find macdeployqt at ${QT_DIR}/bin.\ - It is required to produce an relocatable interface application.\ - Check that the environment variable QT_DIR points to your Qt installation.\ - ") + if (RELEASE_TYPE STREQUAL "DEV") + install(CODE " + execute_process(COMMAND ${MACDEPLOYQT_COMMAND}\ + \${CMAKE_INSTALL_PREFIX}/${_INTERFACE_INSTALL_PATH}/\ + -verbose=2 -qmldir=${CMAKE_SOURCE_DIR}/interface/resources/qml/\ + )" + COMPONENT ${CLIENT_COMPONENT} + ) + else () + add_custom_command(TARGET ${TARGET_NAME} POST_BUILD + COMMAND ${MACDEPLOYQT_COMMAND} "$/../.." -verbose=2 -qmldir=${CMAKE_SOURCE_DIR}/interface/resources/qml/ + ) + endif() endif () - - install(CODE " - execute_process(COMMAND ${MACDEPLOYQT_COMMAND}\ - \${CMAKE_INSTALL_PREFIX}/${_INTERFACE_INSTALL_PATH}/\ - -verbose=2 -qmldir=${CMAKE_SOURCE_DIR}/interface/resources/qml/\ - )" - COMPONENT ${CLIENT_COMPONENT} - ) - - endif () endmacro() diff --git a/cmake/macros/FixupNitpick.cmake b/cmake/macros/FixupNitpick.cmake index 8477b17823..db96fec724 100644 --- a/cmake/macros/FixupNitpick.cmake +++ b/cmake/macros/FixupNitpick.cmake @@ -10,27 +10,32 @@ # macro(fixup_nitpick) - if (APPLE) - string(REPLACE " " "\\ " ESCAPED_BUNDLE_NAME ${NITPICK_BUNDLE_NAME}) - string(REPLACE " " "\\ " ESCAPED_INSTALL_PATH ${NITPICK_INSTALL_DIR}) - set(_NITPICK_INSTALL_PATH "${ESCAPED_INSTALL_PATH}/${ESCAPED_BUNDLE_NAME}.app") + if (APPLE) + string(REPLACE " " "\\ " ESCAPED_BUNDLE_NAME ${NITPICK_BUNDLE_NAME}) + string(REPLACE " " "\\ " ESCAPED_INSTALL_PATH ${NITPICK_INSTALL_DIR}) + set(_NITPICK_INSTALL_PATH "${ESCAPED_INSTALL_PATH}/${ESCAPED_BUNDLE_NAME}.app") - find_program(MACDEPLOYQT_COMMAND macdeployqt PATHS "${QT_DIR}/bin" NO_DEFAULT_PATH) + find_program(MACDEPLOYQT_COMMAND macdeployqt PATHS "${QT_DIR}/bin" NO_DEFAULT_PATH) - if (NOT MACDEPLOYQT_COMMAND AND (PRODUCTION_BUILD OR PR_BUILD)) - message(FATAL_ERROR "Could not find macdeployqt at ${QT_DIR}/bin.\ - It is required to produce a relocatable nitpick application.\ - Check that the environment variable QT_DIR points to your Qt installation.\ - ") + if (NOT MACDEPLOYQT_COMMAND AND (PRODUCTION_BUILD OR PR_BUILD)) + message(FATAL_ERROR "Could not find macdeployqt at ${QT_DIR}/bin.\ + It is required to produce a relocatable nitpick application.\ + Check that the variable QT_DIR points to your Qt installation.\ + ") + endif () + + if (RELEASE_TYPE STREQUAL "DEV") + install(CODE " + execute_process(COMMAND ${MACDEPLOYQT_COMMAND}\ + \${CMAKE_INSTALL_PREFIX}/${_NITPICK_INSTALL_PATH}/\ + -verbose=2 -qmldir=${CMAKE_SOURCE_DIR}/interface/resources/qml/\ + )" + COMPONENT ${CLIENT_COMPONENT} + ) + else () + add_custom_command(TARGET ${TARGET_NAME} POST_BUILD + COMMAND ${MACDEPLOYQT_COMMAND} "$/../.." -verbose=2 -qmldir=${CMAKE_SOURCE_DIR}/interface/resources/qml/ + ) + endif() endif () - - install(CODE " - execute_process(COMMAND ${MACDEPLOYQT_COMMAND}\ - \${CMAKE_INSTALL_PREFIX}/${_NITPICK_INSTALL_PATH}/\ - -verbose=2 -qmldir=${CMAKE_SOURCE_DIR}/interface/resources/qml/\ - )" - COMPONENT ${CLIENT_COMPONENT} - ) - - endif () endmacro() diff --git a/cmake/macros/PackageLibrariesForDeployment.cmake b/cmake/macros/PackageLibrariesForDeployment.cmake index 29f4617a6f..b3f725b2b0 100644 --- a/cmake/macros/PackageLibrariesForDeployment.cmake +++ b/cmake/macros/PackageLibrariesForDeployment.cmake @@ -39,7 +39,7 @@ macro(PACKAGE_LIBRARIES_FOR_DEPLOYMENT) add_custom_command( TARGET ${TARGET_NAME} POST_BUILD - COMMAND CMD /C "SET PATH=%PATH%;${QT_DIR}/bin && ${WINDEPLOYQT_COMMAND}\ + COMMAND CMD /C "SET PATH=${QT_DIR}/bin;%PATH% && ${WINDEPLOYQT_COMMAND}\ ${EXTRA_DEPLOY_OPTIONS} $<$,$,$>:--release>\ --no-compiler-runtime --no-opengl-sw --no-angle -no-system-d3d-compiler \"$\"" ) diff --git a/cmake/macros/SetupQt.cmake b/cmake/macros/SetupQt.cmake index a1ea9b68bc..b9ee425169 100644 --- a/cmake/macros/SetupQt.cmake +++ b/cmake/macros/SetupQt.cmake @@ -1,4 +1,3 @@ -# # Created by Bradley Austin Davis on 2017/09/02 # Copyright 2013-2017 High Fidelity, Inc. # @@ -6,67 +5,91 @@ # See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html # -# Construct a default QT location from a root path, a version and an architecture - -function(calculate_default_qt_dir _QT_VERSION _RESULT_NAME) - if (ANDROID) - set(QT_DEFAULT_ARCH "android_armv7") - elseif(UWP) - set(QT_DEFAULT_ARCH "winrt_x64_msvc2017") - elseif(APPLE) - set(QT_DEFAULT_ARCH "clang_64") - elseif(WIN32) - set(QT_DEFAULT_ARCH "msvc2017_64") - else() - set(QT_DEFAULT_ARCH "gcc_64") +function(get_sub_directories result curdir) + file(GLOB children RELATIVE ${curdir} ${curdir}/*) + set(dirlist "") + foreach(child ${children}) + if(IS_DIRECTORY ${curdir}/${child}) + LIST(APPEND dirlist ${child}) endif() + endforeach() + set(${result} ${dirlist} PARENT_SCOPE) +endfunction() - if (WIN32 OR (ANDROID AND ("${CMAKE_HOST_SYSTEM_NAME}" STREQUAL "Windows"))) - set(QT_DEFAULT_ROOT "c:/Qt") - else() - set(QT_DEFAULT_ROOT "$ENV{HOME}/Qt") +function(calculate_qt5_version result _QT_DIR) + # All Qt5 packages have little "private" include directories named with the actual Qt version such as: + # .../include/QtCore/5.12.3/QtCore/private + # Sometimes we need to include these private headers for debug hackery. + # Hence we find one of these directories and pick apart its path to determine the actual QT_VERSION. + if (APPLE) + set(_QT_CORE_DIR "${_QT_DIR}/lib/QtCore.framework/Versions/5/Headers") + else() + set(_QT_CORE_DIR "${_QT_DIR}/include/QtCore") + endif() + if(NOT EXISTS "${_QT_CORE_DIR}") + message(FATAL_ERROR "Could not find 'include/QtCore' in '${_QT_DIR}'") + endif() + set(subdirs "") + get_sub_directories(subdirs ${_QT_CORE_DIR}) + + foreach(subdir ${subdirs}) + string(REGEX MATCH "5.[0-9]+.[0-9]+$" _QT_VERSION ${subdir}) + if (NOT "${_QT_VERSION}" STREQUAL "") + # found it! + set(${result} "${_QT_VERSION}" PARENT_SCOPE) + break() endif() - - set_from_env(QT_ROOT QT_ROOT ${QT_DEFAULT_ROOT}) - set_from_env(QT_ARCH QT_ARCH ${QT_DEFAULT_ARCH}) - - set(${_RESULT_NAME} "${QT_ROOT}/${_QT_VERSION}/${QT_ARCH}" PARENT_SCOPE) + endforeach() endfunction() # Sets the QT_CMAKE_PREFIX_PATH and QT_DIR variables # Also enables CMAKE_AUTOMOC and CMAKE_AUTORCC macro(setup_qt) - set_from_env(QT_VERSION QT_VERSION "5.10.1") - # if QT_CMAKE_PREFIX_PATH was not specified before hand, - # try to use the environment variable - if (NOT QT_CMAKE_PREFIX_PATH) - set(QT_CMAKE_PREFIX_PATH "$ENV{QT_CMAKE_PREFIX_PATH}") + # if we are in a development build and QT_CMAKE_PREFIX_PATH is specified + # then use it, + # otherwise, use the vcpkg'ed version + if(NOT DEFINED VCPKG_QT_CMAKE_PREFIX_PATH) + message(FATAL_ERROR "VCPKG_QT_CMAKE_PREFIX_PATH should have been set by hifi_vcpkg.py") endif() - if (("QT_CMAKE_PREFIX_PATH" STREQUAL "") OR (NOT EXISTS "${QT_CMAKE_PREFIX_PATH}")) - calculate_default_qt_dir(${QT_VERSION} QT_DIR) - set(QT_CMAKE_PREFIX_PATH "${QT_DIR}/lib/cmake") + if (NOT DEV_BUILD) + if (APPLE) + # HACK: manually set the QT_CMAKE_PREFIX_PATH so that hard-coded paths find new QT libs where we'll put them + set(QT_CMAKE_PREFIX_PATH "/var/tmp/qt5-install/lib/cmake") + elseif (UNIX AND DEFINED ENV{QT_CMAKE_PREFIX_PATH}) + # HACK: obey QT_CMAKE_PREFIX_PATH to allow UNIX to use older QT libs + set(QT_CMAKE_PREFIX_PATH $ENV{QT_CMAKE_PREFIX_PATH}) + else() + set(QT_CMAKE_PREFIX_PATH ${VCPKG_QT_CMAKE_PREFIX_PATH}) + endif() else() - # figure out where the qt dir is - get_filename_component(QT_DIR "${QT_CMAKE_PREFIX_PATH}/../../" ABSOLUTE) + # DEV_BUILD + if (DEFINED ENV{QT_CMAKE_PREFIX_PATH}) + set(QT_CMAKE_PREFIX_PATH $ENV{QT_CMAKE_PREFIX_PATH}) + else() + set(QT_CMAKE_PREFIX_PATH ${VCPKG_QT_CMAKE_PREFIX_PATH}) + endif() endif() - if (WIN32) + # figure out where the qt dir is + get_filename_component(QT_DIR "${QT_CMAKE_PREFIX_PATH}/../../" ABSOLUTE) + set(QT_VERSION "unknown") + calculate_qt5_version(QT_VERSION "${QT_DIR}") + if (QT_VERSION STREQUAL "unknown") + message(FATAL_ERROR "Could not determine QT_VERSION") + endif() + + if(WIN32) # windows shell does not like backslashes expanded on the command line, # so convert all backslashes in the QT path to forward slashes string(REPLACE \\ / QT_CMAKE_PREFIX_PATH ${QT_CMAKE_PREFIX_PATH}) string(REPLACE \\ / QT_DIR ${QT_DIR}) endif() - # This check doesn't work on Mac - #if (NOT EXISTS "${QT_DIR}/include/QtCore/QtGlobal") - # message(FATAL_ERROR "Unable to locate Qt includes in ${QT_DIR}") - #endif() - - if (NOT EXISTS "${QT_CMAKE_PREFIX_PATH}/Qt5Core/Qt5CoreConfig.cmake") - message(FATAL_ERROR "Unable to locate Qt cmake config in ${QT_CMAKE_PREFIX_PATH}") + if(NOT EXISTS "${QT_CMAKE_PREFIX_PATH}/Qt5Core/Qt5CoreConfig.cmake") + message(FATAL_ERROR "Unable to locate Qt5CoreConfig.cmake in '${QT_CMAKE_PREFIX_PATH}'") endif() - message(STATUS "The Qt build in use is: \"${QT_DIR}\"") + message(STATUS "Using Qt build in : '${QT_DIR}' with version ${QT_VERSION}") # Instruct CMake to run moc automatically when needed. set(CMAKE_AUTOMOC ON) diff --git a/cmake/ports/bullet3/portfile.cmake b/cmake/ports/bullet3/portfile.cmake index d4e7aaf787..9318385de0 100644 --- a/cmake/ports/bullet3/portfile.cmake +++ b/cmake/ports/bullet3/portfile.cmake @@ -25,14 +25,13 @@ vcpkg_from_github( HEAD_REF master ) - vcpkg_configure_cmake( SOURCE_PATH ${SOURCE_PATH} OPTIONS -DCMAKE_WINDOWS_EXPORT_ALL_SYMBOLS=ON -DUSE_MSVC_RUNTIME_LIBRARY_DLL=ON - -DUSE_GLUT=0 - -DUSE_DX11=0 + -DUSE_GLUT=0 + -DUSE_DX11=0 -DBUILD_DEMOS=OFF -DBUILD_OPENGL3_DEMOS=OFF -DBUILD_BULLET3=OFF diff --git a/debian/changelog b/debian/changelog new file mode 100644 index 0000000000..9e55d78ff0 --- /dev/null +++ b/debian/changelog @@ -0,0 +1,5 @@ +hifi-interface (84-1) unstable; urgency=medium + + * Initial release + + -- Seth Alves Tue, 14 May 2019 19:45:31 -0700 diff --git a/debian/compat b/debian/compat new file mode 100644 index 0000000000..b4de394767 --- /dev/null +++ b/debian/compat @@ -0,0 +1 @@ +11 diff --git a/debian/control b/debian/control new file mode 100644 index 0000000000..15d354d0b3 --- /dev/null +++ b/debian/control @@ -0,0 +1,15 @@ +Source: hifi-interface +Section: comm +Priority: optional +Maintainer: Seth Alves +Build-Depends: debhelper (>= 11), cmake +Standards-Version: 4.1.3 +Homepage: https://www.highfidelity.com/ +Vcs-Browser: https://github.com/highfidelity/hifi +Vcs-Git: https://github.com/highfidelity/hifi.git + +Package: hifi-interface +Architecture: any +Depends: ${shlibs:Depends}, ${misc:Depends}, libqt5quick5, libqt5quickcontrols2-5, libqt5quickwidgets5, qml-module-qtquick-controls, qml-module-qtquick-controls2, qml-module-qtquick-dialogs, libqt5webchannel5, qml-module-qtwebchannel, qml-module-qtwebengine, qml-module-qt-labs-folderlistmodel, qml-module-qt-labs-settings +Description: High Fidelity allows creation and sharing of VR experiences. + The High Fidelity metaverse provides built-in social features, including avatar interactions, spatialized audio and interactive physics. Additionally, you have the ability to import any 3D object into your virtual environment. diff --git a/debian/copyright b/debian/copyright new file mode 100644 index 0000000000..4ead9ced4e --- /dev/null +++ b/debian/copyright @@ -0,0 +1,23 @@ +Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ +Upstream-Name: hifi-interface +Source: https://github.com/highfidelity/hifi + +Files: * +Copyright: 2013-2019, High Fidelity, Inc. +License: Apache-2.0 + +License: Apache-2.0 + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + . + https://www.apache.org/licenses/LICENSE-2.0 + . + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + . + On Debian systems, the complete text of the Apache version 2.0 license + can be found in "/usr/share/common-licenses/Apache-2.0". diff --git a/debian/hifi-interface-docs.docs b/debian/hifi-interface-docs.docs new file mode 100644 index 0000000000..efea0a6a24 --- /dev/null +++ b/debian/hifi-interface-docs.docs @@ -0,0 +1,2 @@ +README.Debian +README.source diff --git a/debian/hifi-interface.dirs b/debian/hifi-interface.dirs new file mode 100644 index 0000000000..ad46df23bf --- /dev/null +++ b/debian/hifi-interface.dirs @@ -0,0 +1 @@ +opt/hifi/interface/ diff --git a/debian/patches/hifi_use_system_qt.diff b/debian/patches/hifi_use_system_qt.diff new file mode 100644 index 0000000000..2bd7009928 --- /dev/null +++ b/debian/patches/hifi_use_system_qt.diff @@ -0,0 +1,76 @@ +Index: hifi-interface-84/cmake/macros/SetupQt.cmake +=================================================================== +--- hifi-interface-84.orig/cmake/macros/SetupQt.cmake ++++ hifi-interface-84/cmake/macros/SetupQt.cmake +@@ -18,19 +18,19 @@ function(calculate_default_qt_dir _QT_VE + elseif(WIN32) + set(QT_DEFAULT_ARCH "msvc2017_64") + else() +- set(QT_DEFAULT_ARCH "gcc_64") ++ set(QT_DEFAULT_ARCH "x86_64-linux-gnu") + endif() + + if (WIN32 OR (ANDROID AND ("${CMAKE_HOST_SYSTEM_NAME}" STREQUAL "Windows"))) + set(QT_DEFAULT_ROOT "c:/Qt") + else() +- set(QT_DEFAULT_ROOT "$ENV{HOME}/Qt") ++ set(QT_DEFAULT_ROOT "/usr/lib") + endif() + + set_from_env(QT_ROOT QT_ROOT ${QT_DEFAULT_ROOT}) + set_from_env(QT_ARCH QT_ARCH ${QT_DEFAULT_ARCH}) + +- set(${_RESULT_NAME} "${QT_ROOT}/${_QT_VERSION}/${QT_ARCH}" PARENT_SCOPE) ++ set(${_RESULT_NAME} "${QT_ROOT}/${QT_ARCH}" PARENT_SCOPE) + endfunction() + + # Sets the QT_CMAKE_PREFIX_PATH and QT_DIR variables +@@ -44,7 +44,7 @@ macro(setup_qt) + endif() + if (("QT_CMAKE_PREFIX_PATH" STREQUAL "") OR (NOT EXISTS "${QT_CMAKE_PREFIX_PATH}")) + calculate_default_qt_dir(${QT_VERSION} QT_DIR) +- set(QT_CMAKE_PREFIX_PATH "${QT_DIR}/lib/cmake") ++ set(QT_CMAKE_PREFIX_PATH "${QT_DIR}/cmake") + else() + # figure out where the qt dir is + get_filename_component(QT_DIR "${QT_CMAKE_PREFIX_PATH}/../../" ABSOLUTE) +Index: hifi-interface-84/interface/CMakeLists.txt +=================================================================== +--- hifi-interface-84.orig/interface/CMakeLists.txt ++++ hifi-interface-84/interface/CMakeLists.txt +@@ -35,7 +35,7 @@ else () + add_custom_command( + OUTPUT ${RESOURCES_RCC} + DEPENDS ${RESOURCES_QRC} ${GENERATE_QRC_DEPENDS} +- COMMAND "${QT_DIR}/bin/rcc" ++ COMMAND "rcc" + ARGS ${RESOURCES_QRC} -binary -o ${RESOURCES_RCC} + ) + endif() +@@ -399,6 +399,13 @@ else() + + optional_win_executable_signing() + endif() ++ ++ install( ++ DIRECTORY "$/" ++ DESTINATION ${INTERFACE_INSTALL_DIR} ++ COMPONENT ${CLIENT_COMPONENT} ++ ) ++ + endif() + + if (SCRIPTS_INSTALL_DIR) +Index: hifi-interface-84/tools/nitpick/CMakeLists.txt +=================================================================== +--- hifi-interface-84.orig/tools/nitpick/CMakeLists.txt ++++ hifi-interface-84/tools/nitpick/CMakeLists.txt +@@ -12,7 +12,7 @@ generate_qrc(OUTPUT ${RESOURCES_QRC} PAT + add_custom_command( + OUTPUT ${RESOURCES_RCC} + DEPENDS ${RESOURCES_QRC} ${GENERATE_QRC_DEPENDS} +- COMMAND "${QT_DIR}/bin/rcc" ++ COMMAND "rcc" + ARGS ${RESOURCES_QRC} -binary -o ${RESOURCES_RCC} + ) + diff --git a/debian/patches/series b/debian/patches/series new file mode 100644 index 0000000000..65ac370483 --- /dev/null +++ b/debian/patches/series @@ -0,0 +1 @@ +hifi_use_system_qt.diff diff --git a/debian/rules b/debian/rules new file mode 100755 index 0000000000..8a56eecdfb --- /dev/null +++ b/debian/rules @@ -0,0 +1,24 @@ +#!/usr/bin/make -f + +%: + dh $@ + + +override_dh_auto_configure: + mkdir obj-$(DEB_TARGET_MULTIARCH) + (cd obj-$(DEB_TARGET_MULTIARCH) && cmake .. -DCMAKE_INSTALL_PREFIX=/opt/hifi -DCMAKE_VERBOSE_MAKEFILE=ON -DCMAKE_EXPORT_NO_PACKAGE_REGISTRY=ON "-GUnix Makefiles" -DCMAKE_BUILD_TYPE=RelWithDebInfo -DCLIENT_ONLY=ON -DDOWNLOAD_SERVERLESS_CONTENT=ON -DCMAKE_CXX_COMPILER=/usr/lib/llvm-7/bin/clang\+\+ -DOpenGL_GL_PREFERENCE=GLVND) + +override_dh_auto_build: + (cd obj-$(DEB_TARGET_MULTIARCH) && make -j4) + +S=obj-$(DEB_TARGET_MULTIARCH) +I=debian/hifi-interface/opt/hifi/interface + +override_dh_auto_install: + cp $(S)/interface/interface $(I) + cp $(S)/ext/makefiles/steamworks/project/src/steamworks/redistributable_bin/linux64/libsteam_api.so $(I) + cp $(S)/ext/makefiles/quazip/project/build/libquazip5.so.1 $(I) + cp $(S)/ext/makefiles/polyvox/project/build/library/PolyVoxCore/libPolyVoxCore.so.0 $(I) + cp $(S)/interface/resources.rcc $(I) + cp -r $(S)/interface/scripts $(I)/scripts + cp -r $(S)/interface/plugins $(I)/plugins diff --git a/debian/source/format b/debian/source/format new file mode 100644 index 0000000000..163aaf8d82 --- /dev/null +++ b/debian/source/format @@ -0,0 +1 @@ +3.0 (quilt) diff --git a/debian/source/include-binaries b/debian/source/include-binaries new file mode 100644 index 0000000000..0d2847f83c --- /dev/null +++ b/debian/source/include-binaries @@ -0,0 +1,6 @@ +interface/compiledResources/resources.rcc +tools/nitpick/compiledResources/resources.rcc +__pycache__/hifi_android.cpython-37.pyc +__pycache__/hifi_singleton.cpython-37.pyc +__pycache__/hifi_utils.cpython-37.pyc +__pycache__/hifi_vcpkg.cpython-37.pyc diff --git a/domain-server/src/DomainGatekeeper.cpp b/domain-server/src/DomainGatekeeper.cpp index 29656f4465..f5705a570b 100644 --- a/domain-server/src/DomainGatekeeper.cpp +++ b/domain-server/src/DomainGatekeeper.cpp @@ -128,7 +128,7 @@ void DomainGatekeeper::processConnectRequestPacket(QSharedPointergetFirstPacketReceiveTime()); } else { qDebug() << "Refusing connection from node at" << message->getSenderSockAddr() << "with hardware address" << nodeConnection.hardwareAddress @@ -358,7 +358,8 @@ SharedNodePointer DomainGatekeeper::processAssignmentConnectRequest(const NodeCo nodeData->setNodeVersion(it->second.getNodeVersion()); nodeData->setHardwareAddress(nodeConnection.hardwareAddress); nodeData->setMachineFingerprint(nodeConnection.machineFingerprint); - + // client-side send time of last connect/domain list request + nodeData->setLastDomainCheckinTimestamp(nodeConnection.lastPingTimestamp); nodeData->setWasAssigned(true); // cleanup the PendingAssignedNodeData for this assignment now that it's connecting @@ -499,6 +500,9 @@ SharedNodePointer DomainGatekeeper::processAgentConnectRequest(const NodeConnect // set the machine fingerprint passed in the connect request nodeData->setMachineFingerprint(nodeConnection.machineFingerprint); + // set client-side send time of last connect/domain list request + nodeData->setLastDomainCheckinTimestamp(nodeConnection.lastPingTimestamp); + // also add an interpolation to DomainServerNodeData so that servers can get username in stats nodeData->addOverrideForKey(USERNAME_UUID_REPLACEMENT_STATS_KEY, uuidStringWithoutCurlyBraces(newNode->getUUID()), username); diff --git a/domain-server/src/DomainGatekeeper.h b/domain-server/src/DomainGatekeeper.h index f8d79179d6..92b400882e 100644 --- a/domain-server/src/DomainGatekeeper.h +++ b/domain-server/src/DomainGatekeeper.h @@ -64,7 +64,7 @@ public slots: signals: void killNode(SharedNodePointer node); - void connectedNode(SharedNodePointer node); + void connectedNode(SharedNodePointer node, quint64 requestReceiveTime); public slots: void updateNodePermissions(); diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index 051dd989f5..9a2aaca18b 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -14,6 +14,7 @@ #include #include #include +#include #include #include @@ -57,6 +58,8 @@ #include +using namespace std::chrono; + Q_LOGGING_CATEGORY(domain_server, "hifi.domain_server") Q_LOGGING_CATEGORY(domain_server_ice, "hifi.domain_server.ice") @@ -1068,7 +1071,10 @@ void DomainServer::processListRequestPacket(QSharedPointer mess // update the connecting hostname in case it has changed nodeData->setPlaceName(nodeRequestData.placeName); - sendDomainListToNode(sendingNode, message->getSenderSockAddr()); + // client-side send time of last connect/domain list request + nodeData->setLastDomainCheckinTimestamp(nodeRequestData.lastPingTimestamp); + + sendDomainListToNode(sendingNode, message->getFirstPacketReceiveTime(), message->getSenderSockAddr()); } bool DomainServer::isInInterestSet(const SharedNodePointer& nodeA, const SharedNodePointer& nodeB) { @@ -1130,11 +1136,11 @@ QUrl DomainServer::oauthAuthorizationURL(const QUuid& stateUUID) { return authorizationURL; } -void DomainServer::handleConnectedNode(SharedNodePointer newNode) { +void DomainServer::handleConnectedNode(SharedNodePointer newNode, quint64 requestReceiveTime) { DomainServerNodeData* nodeData = static_cast(newNode->getLinkedData()); // reply back to the user with a PacketType::DomainList - sendDomainListToNode(newNode, nodeData->getSendingSockAddr()); + sendDomainListToNode(newNode, requestReceiveTime, nodeData->getSendingSockAddr()); // if this node is a user (unassigned Agent), signal if (newNode->getType() == NodeType::Agent && !nodeData->wasAssigned()) { @@ -1150,7 +1156,7 @@ void DomainServer::handleConnectedNode(SharedNodePointer newNode) { broadcastNewNode(newNode); } -void DomainServer::sendDomainListToNode(const SharedNodePointer& node, const HifiSockAddr &senderSockAddr) { +void DomainServer::sendDomainListToNode(const SharedNodePointer& node, quint64 requestPacketReceiveTime, const HifiSockAddr &senderSockAddr) { const int NUM_DOMAIN_LIST_EXTENDED_HEADER_BYTES = NUM_BYTES_RFC4122_UUID + NLPacket::NUM_BYTES_LOCALID + NUM_BYTES_RFC4122_UUID + NLPacket::NUM_BYTES_LOCALID + 4; @@ -1158,7 +1164,7 @@ void DomainServer::sendDomainListToNode(const SharedNodePointer& node, const Hif // this data is at the beginning of each of the domain list packets QByteArray extendedHeader(NUM_DOMAIN_LIST_EXTENDED_HEADER_BYTES, 0); QDataStream extendedHeaderStream(&extendedHeader, QIODevice::WriteOnly); - + DomainServerNodeData* nodeData = static_cast(node->getLinkedData()); auto limitedNodeList = DependencyManager::get(); extendedHeaderStream << limitedNodeList->getSessionUUID(); @@ -1167,13 +1173,14 @@ void DomainServer::sendDomainListToNode(const SharedNodePointer& node, const Hif extendedHeaderStream << node->getLocalID(); extendedHeaderStream << node->getPermissions(); extendedHeaderStream << limitedNodeList->getAuthenticatePackets(); + extendedHeaderStream << nodeData->getLastDomainCheckinTimestamp(); + extendedHeaderStream << requestPacketReceiveTime; + extendedHeaderStream << quint64(duration_cast(p_high_resolution_clock::now().time_since_epoch()).count()); auto domainListPackets = NLPacketList::create(PacketType::DomainList, extendedHeader); // always send the node their own UUID back QDataStream domainListStream(domainListPackets.get()); - DomainServerNodeData* nodeData = static_cast(node->getLinkedData()); - // store the nodeInterestSet on this DomainServerNodeData, in case it has changed auto& nodeInterestSet = nodeData->getNodeInterestSet(); diff --git a/domain-server/src/DomainServer.h b/domain-server/src/DomainServer.h index 8276566233..704650e594 100644 --- a/domain-server/src/DomainServer.h +++ b/domain-server/src/DomainServer.h @@ -111,7 +111,7 @@ private slots: void sendHeartbeatToMetaverse() { sendHeartbeatToMetaverse(QString()); } void sendHeartbeatToIceServer(); - void handleConnectedNode(SharedNodePointer newNode); + void handleConnectedNode(SharedNodePointer newNode, quint64 requestReceiveTime); void handleTempDomainSuccess(QNetworkReply* requestReply); void handleTempDomainError(QNetworkReply* requestReply); @@ -172,7 +172,7 @@ private: void handleKillNode(SharedNodePointer nodeToKill); void broadcastNodeDisconnect(const SharedNodePointer& disconnnectedNode); - void sendDomainListToNode(const SharedNodePointer& node, const HifiSockAddr& senderSockAddr); + void sendDomainListToNode(const SharedNodePointer& node, quint64 requestPacketReceiveTime, const HifiSockAddr& senderSockAddr); bool isInInterestSet(const SharedNodePointer& nodeA, const SharedNodePointer& nodeB); diff --git a/domain-server/src/DomainServerNodeData.h b/domain-server/src/DomainServerNodeData.h index f465cceb96..370886cbce 100644 --- a/domain-server/src/DomainServerNodeData.h +++ b/domain-server/src/DomainServerNodeData.h @@ -61,6 +61,9 @@ public: void setMachineFingerprint(const QUuid& machineFingerprint) { _machineFingerprint = machineFingerprint; } const QUuid& getMachineFingerprint() { return _machineFingerprint; } + void setLastDomainCheckinTimestamp(quint64 lastDomainCheckinTimestamp) { _lastDomainCheckinTimestamp = lastDomainCheckinTimestamp; } + quint64 getLastDomainCheckinTimestamp() { return _lastDomainCheckinTimestamp; } + void addOverrideForKey(const QString& key, const QString& value, const QString& overrideValue); void removeOverrideForKey(const QString& key, const QString& value); @@ -93,7 +96,7 @@ private: QString _nodeVersion; QString _hardwareAddress; QUuid _machineFingerprint; - + quint64 _lastDomainCheckinTimestamp; QString _placeName; bool _wasAssigned { false }; diff --git a/domain-server/src/NodeConnectionData.cpp b/domain-server/src/NodeConnectionData.cpp index 0a3782d79b..b3ea005bd1 100644 --- a/domain-server/src/NodeConnectionData.cpp +++ b/domain-server/src/NodeConnectionData.cpp @@ -36,6 +36,8 @@ NodeConnectionData NodeConnectionData::fromDataStream(QDataStream& dataStream, c // now the machine fingerprint dataStream >> newHeader.machineFingerprint; } + + dataStream >> newHeader.lastPingTimestamp; dataStream >> newHeader.nodeType >> newHeader.publicSockAddr >> newHeader.localSockAddr diff --git a/domain-server/src/NodeConnectionData.h b/domain-server/src/NodeConnectionData.h index dd9ca6b650..43661f9caf 100644 --- a/domain-server/src/NodeConnectionData.h +++ b/domain-server/src/NodeConnectionData.h @@ -22,6 +22,7 @@ public: bool isConnectRequest = true); QUuid connectUUID; + quint64 lastPingTimestamp{ 0 }; // client-side send time of last connect/domain list request NodeType_t nodeType; HifiSockAddr publicSockAddr; HifiSockAddr localSockAddr; diff --git a/hifi_vcpkg.py b/hifi_vcpkg.py index 4a8004f3a3..cdd44d304e 100644 --- a/hifi_vcpkg.py +++ b/hifi_vcpkg.py @@ -15,10 +15,12 @@ print = functools.partial(print, flush=True) # Encapsulates the vcpkg system class VcpkgRepo: CMAKE_TEMPLATE = """ +# this file auto-generated by hifi_vcpkg.py get_filename_component(CMAKE_TOOLCHAIN_FILE "{}" ABSOLUTE CACHE) get_filename_component(CMAKE_TOOLCHAIN_FILE_UNCACHED "{}" ABSOLUTE) set(VCPKG_INSTALL_ROOT "{}") set(VCPKG_TOOLS_DIR "{}") +set(VCPKG_QT_CMAKE_PREFIX_PATH "{}") """ CMAKE_TEMPLATE_NON_ANDROID = """ @@ -171,6 +173,11 @@ endif() if not self.args.android: print("Installing build dependencies") self.run(['install', '--triplet', self.triplet, 'hifi-client-deps']) + + # If not android, install our Qt build + if not self.args.android: + print("Installing Qt") + self.installQt() def cleanBuilds(self): # Remove temporary build artifacts @@ -206,22 +213,33 @@ endif() with open(self.tagFile, 'w') as f: f.write(self.tagContents) + def getQt5InstallPath(self): + qt5InstallPath = os.path.join(self.path, 'installed', 'qt5-install') + if platform.system() == "Darwin" and self.args.release_type != "DEV": + # HACK for MacOS Jenkins PRODUCTION and PR builds during Qt-5.12.3 transition + # we always supply /var/tmp/qt5-install for QT_CMAKE_PREFIX_PATH + qt5InstallPath = "/var/tmp/qt5-install" + elif self.args.android: + precompiled = os.path.realpath(self.androidPackagePath) + qt5InstallPath = os.path.realpath(os.path.join(precompiled, 'qt')) + return qt5InstallPath + def writeConfig(self): print("Writing cmake config to {}".format(self.configFilePath)) # Write out the configuration for use by CMake cmakeScript = os.path.join(self.path, 'scripts/buildsystems/vcpkg.cmake') installPath = os.path.join(self.path, 'installed', self.triplet) toolsPath = os.path.join(self.path, 'installed', self.hostTriplet, 'tools') - cmakeTemplate = VcpkgRepo.CMAKE_TEMPLATE - if not self.args.android: - cmakeTemplate += VcpkgRepo.CMAKE_TEMPLATE_NON_ANDROID - else: - precompiled = os.path.realpath(self.androidPackagePath) - qtCmakePrefix = os.path.realpath(os.path.join(precompiled, 'qt/lib/cmake')) - cmakeTemplate += 'set(HIFI_ANDROID_PRECOMPILED "{}")\n'.format(precompiled) - cmakeTemplate += 'set(QT_CMAKE_PREFIX_PATH "{}")\n'.format(qtCmakePrefix) - cmakeConfig = cmakeTemplate.format(cmakeScript, cmakeScript, installPath, toolsPath).replace('\\', '/') + cmakeTemplate = VcpkgRepo.CMAKE_TEMPLATE + if self.args.android: + precompiled = os.path.realpath(self.androidPackagePath) + cmakeTemplate += 'set(HIFI_ANDROID_PRECOMPILED "{}")\n'.format(precompiled) + else: + cmakeTemplate += VcpkgRepo.CMAKE_TEMPLATE_NON_ANDROID + + qtCmakePrefixPath = os.path.join(self.getQt5InstallPath(), "lib/cmake") + cmakeConfig = cmakeTemplate.format(cmakeScript, cmakeScript, installPath, toolsPath, qtCmakePrefixPath).replace('\\', '/') with open(self.configFilePath, 'w') as f: f.write(cmakeConfig) @@ -232,3 +250,28 @@ endif() print("Not implemented") + def installQt(self): + qt5InstallPath = self.getQt5InstallPath() + if not os.path.isdir(qt5InstallPath): + print ('Downloading Qt from AWS') + dest, tail = os.path.split(qt5InstallPath) + + url = 'NOT DEFINED' + if platform.system() == 'Windows': + url = 'https://hifi-public.s3.amazonaws.com/dependencies/vcpkg/qt5-install-5.12.3-windows2.tar.gz' + elif platform.system() == 'Darwin': + url = 'https://hifi-public.s3.amazonaws.com/dependencies/vcpkg/qt5-install-5.12.3-macos.tar.gz' + elif platform.system() == 'Linux': + if platform.linux_distribution()[1][:3] == '16.': + url = 'https://hifi-public.s3.amazonaws.com/dependencies/vcpkg/qt5-install-5.12.3-ubuntu-16.04.tar.gz' + elif platform.linux_distribution()[1][:3] == '18.': + url = 'https://hifi-public.s3.amazonaws.com/dependencies/vcpkg/qt5-install-5.12.3-ubuntu-18.04.tar.gz' + else: + print('UNKNOWN LINUX VERSION!!!') + else: + print('UNKNOWN OPERATING SYSTEM!!!') + + print('Extracting ' + url + ' to ' + dest) + hifi_utils.downloadAndExtract(url, dest) + else: + print ('Qt has already been downloaded') diff --git a/interface/CMakeLists.txt b/interface/CMakeLists.txt index b022984bb7..9553b571c5 100644 --- a/interface/CMakeLists.txt +++ b/interface/CMakeLists.txt @@ -334,11 +334,11 @@ if (APPLE) COMMAND "${CMAKE_COMMAND}" -E copy_directory "${PROJECT_SOURCE_DIR}/resources/fonts" "${RESOURCES_DEV_DIR}/fonts" - #copy serverless for android - COMMAND "${CMAKE_COMMAND}" -E copy_directory - "${PROJECT_SOURCE_DIR}/resources/serverless" - "${RESOURCES_DEV_DIR}/serverless" - # add redirect json to macOS builds. + #copy serverless for android + COMMAND "${CMAKE_COMMAND}" -E copy_directory + "${PROJECT_SOURCE_DIR}/resources/serverless" + "${RESOURCES_DEV_DIR}/serverless" + # add redirect json to macOS builds. COMMAND "${CMAKE_COMMAND}" -E copy_if_different "${PROJECT_SOURCE_DIR}/resources/serverless/redirect.json" "${RESOURCES_DEV_DIR}/serverless/redirect.json" @@ -401,6 +401,20 @@ else() endif() endif() +if (DEV_BUILD AND (APPLE OR UNIX)) + # create a qt.conf file to override hard-coded search paths in Qt libs + set(QT_LIB_PATH "${QT_CMAKE_PREFIX_PATH}/../..") + if (APPLE) + set(QT_CONF_FILE "${RESOURCES_DEV_DIR}/../Resources/qt.conf") + else () + set(QT_CONF_FILE "${INTERFACE_EXEC_DIR}/qt.conf") + endif () + file(GENERATE + OUTPUT "${QT_CONF_FILE}" + CONTENT "[Paths]\nPrefix=${QT_LIB_PATH}\n" + ) +endif() + if (SCRIPTS_INSTALL_DIR) # setup install of scripts beside interface executable install( diff --git a/interface/resources/images/Default-Sky-9-cubemap/Default-Sky-9-cubemap-ambient.jpg b/interface/resources/images/Default-Sky-9-cubemap/Default-Sky-9-cubemap-ambient.jpg new file mode 100644 index 0000000000..7977396159 Binary files /dev/null and b/interface/resources/images/Default-Sky-9-cubemap/Default-Sky-9-cubemap-ambient.jpg differ diff --git a/interface/resources/images/Default-Sky-9-cubemap/Default-Sky-9-cubemap-ambient.ktx b/interface/resources/images/Default-Sky-9-cubemap/Default-Sky-9-cubemap-ambient.ktx new file mode 100644 index 0000000000..a5f4fae4eb Binary files /dev/null and b/interface/resources/images/Default-Sky-9-cubemap/Default-Sky-9-cubemap-ambient.ktx differ diff --git a/interface/resources/images/Default-Sky-9-cubemap/Default-Sky-9-cubemap-ambient.texmeta.json b/interface/resources/images/Default-Sky-9-cubemap/Default-Sky-9-cubemap-ambient.texmeta.json new file mode 100644 index 0000000000..96edd3abf0 --- /dev/null +++ b/interface/resources/images/Default-Sky-9-cubemap/Default-Sky-9-cubemap-ambient.texmeta.json @@ -0,0 +1,9 @@ +{ + "compressed": { + "COMPRESSED_RGB_BPTC_UNSIGNED_FLOAT": "Default-Sky-9-cubemap-ambient_COMPRESSED_RGB_BPTC_UNSIGNED_FLOAT.ktx", + "COMPRESSED_SRGB8_ETC2": "Default-Sky-9-cubemap-ambient_COMPRESSED_SRGB8_ETC2.ktx" + }, + "original": "Default-Sky-9-cubemap-ambient.jpg", + "uncompressed": "Default-Sky-9-cubemap-ambient.ktx", + "version": 1 +} diff --git a/interface/resources/images/Default-Sky-9-cubemap/Default-Sky-9-cubemap-ambient_COMPRESSED_RGB_BPTC_UNSIGNED_FLOAT.ktx b/interface/resources/images/Default-Sky-9-cubemap/Default-Sky-9-cubemap-ambient_COMPRESSED_RGB_BPTC_UNSIGNED_FLOAT.ktx new file mode 100644 index 0000000000..279042e8dd Binary files /dev/null and b/interface/resources/images/Default-Sky-9-cubemap/Default-Sky-9-cubemap-ambient_COMPRESSED_RGB_BPTC_UNSIGNED_FLOAT.ktx differ diff --git a/interface/resources/images/Default-Sky-9-cubemap/Default-Sky-9-cubemap-ambient_COMPRESSED_SRGB8_ETC2.ktx b/interface/resources/images/Default-Sky-9-cubemap/Default-Sky-9-cubemap-ambient_COMPRESSED_SRGB8_ETC2.ktx new file mode 100644 index 0000000000..0a36017dbe Binary files /dev/null and b/interface/resources/images/Default-Sky-9-cubemap/Default-Sky-9-cubemap-ambient_COMPRESSED_SRGB8_ETC2.ktx differ diff --git a/interface/resources/images/Default-Sky-9-cubemap/Default-Sky-9-cubemap.ktx b/interface/resources/images/Default-Sky-9-cubemap/Default-Sky-9-cubemap.ktx index 4231bf7650..7d5e18f3c9 100644 Binary files a/interface/resources/images/Default-Sky-9-cubemap/Default-Sky-9-cubemap.ktx and b/interface/resources/images/Default-Sky-9-cubemap/Default-Sky-9-cubemap.ktx differ diff --git a/interface/resources/images/Default-Sky-9-cubemap/Default-Sky-9-cubemap.texmeta.json b/interface/resources/images/Default-Sky-9-cubemap/Default-Sky-9-cubemap.texmeta.json index 28512662d9..729bb2d70f 100644 --- a/interface/resources/images/Default-Sky-9-cubemap/Default-Sky-9-cubemap.texmeta.json +++ b/interface/resources/images/Default-Sky-9-cubemap/Default-Sky-9-cubemap.texmeta.json @@ -4,5 +4,6 @@ "COMPRESSED_SRGB8_ETC2": "Default-Sky-9-cubemap_COMPRESSED_SRGB8_ETC2.ktx" }, "original": "Default-Sky-9-cubemap.jpg", - "uncompressed": "Default-Sky-9-cubemap.ktx" + "uncompressed": "Default-Sky-9-cubemap.ktx", + "version": 1 } diff --git a/interface/resources/images/Default-Sky-9-cubemap/Default-Sky-9-cubemap_COMPRESSED_RGB_BPTC_UNSIGNED_FLOAT.ktx b/interface/resources/images/Default-Sky-9-cubemap/Default-Sky-9-cubemap_COMPRESSED_RGB_BPTC_UNSIGNED_FLOAT.ktx index c789fa4ac5..9e821605b4 100644 Binary files a/interface/resources/images/Default-Sky-9-cubemap/Default-Sky-9-cubemap_COMPRESSED_RGB_BPTC_UNSIGNED_FLOAT.ktx and b/interface/resources/images/Default-Sky-9-cubemap/Default-Sky-9-cubemap_COMPRESSED_RGB_BPTC_UNSIGNED_FLOAT.ktx differ diff --git a/interface/resources/images/Default-Sky-9-cubemap/Default-Sky-9-cubemap_COMPRESSED_SRGB8_ETC2.ktx b/interface/resources/images/Default-Sky-9-cubemap/Default-Sky-9-cubemap_COMPRESSED_SRGB8_ETC2.ktx index deede32614..405cd9f09b 100644 Binary files a/interface/resources/images/Default-Sky-9-cubemap/Default-Sky-9-cubemap_COMPRESSED_SRGB8_ETC2.ktx and b/interface/resources/images/Default-Sky-9-cubemap/Default-Sky-9-cubemap_COMPRESSED_SRGB8_ETC2.ktx differ diff --git a/interface/resources/qml/hifi/simplifiedUI/avatarApp/AvatarApp.qml b/interface/resources/qml/hifi/simplifiedUI/avatarApp/AvatarApp.qml index 57bec2250f..d6ee593edd 100644 --- a/interface/resources/qml/hifi/simplifiedUI/avatarApp/AvatarApp.qml +++ b/interface/resources/qml/hifi/simplifiedUI/avatarApp/AvatarApp.qml @@ -58,7 +58,8 @@ Rectangle { if (isLoggedIn) { Commerce.getWalletStatus(); } else { - // Show some error to the user + errorText.text = "There was a problem while retrieving your inventory. " + + "Please try closing and re-opening the Avatar app.\n\nLogin status result: " + isLoggedIn; } } @@ -66,11 +67,19 @@ Rectangle { if (walletStatus === 5) { getInventory(); } else { - // Show some error to the user + errorText.text = "There was a problem while retrieving your inventory. " + + "Please try closing and re-opening the Avatar app.\n\nWallet status result: " + walletStatus; } } onInventoryResult: { + if (result.status !== "success") { + errorText.text = "There was a problem while retrieving your inventory. " + + "Please try closing and re-opening the Avatar app.\n\nInventory status: " + result.status + "\nMessage: " + result.message; + } else if (result.data && result.data.assets && result.data.assets.length === 0 && avatarAppInventoryModel.count === 0) { + errorText.text = "You have not created any avatars yet! Create an avatar with the Avatar Creator, then close and re-open the Avatar App." + } + avatarAppInventoryModel.handlePage(result.status !== "success" && result.message, result); root.updatePreviewUrl(); } @@ -172,7 +181,7 @@ Rectangle { anchors.bottom: parent.bottom AnimatedImage { - visible: !inventoryContentsList.visible + visible: !inventoryContentsList.visible && !errorText.visible anchors.centerIn: parent width: 72 height: width @@ -181,7 +190,7 @@ Rectangle { ListView { id: inventoryContentsList - visible: avatarAppInventoryModel.count !== 0 + visible: avatarAppInventoryModel.count !== 0 && !errorText.visible interactive: contentItem.height > height clip: true model: avatarAppInventoryModel @@ -196,6 +205,18 @@ Rectangle { standaloneIncompatible: model.standalone_incompatible } } + + HifiStylesUit.GraphikRegular { + id: errorText + text: "" + visible: text !== "" + anchors.fill: parent + size: 22 + color: simplifiedUI.colors.text.white + wrapMode: Text.Wrap + verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignHCenter + } } diff --git a/interface/resources/qml/hifi/simplifiedUI/settingsApp/audio/Audio.qml b/interface/resources/qml/hifi/simplifiedUI/settingsApp/audio/Audio.qml index 840a2bb69a..1f9aa5bcbc 100644 --- a/interface/resources/qml/hifi/simplifiedUI/settingsApp/audio/Audio.qml +++ b/interface/resources/qml/hifi/simplifiedUI/settingsApp/audio/Audio.qml @@ -19,8 +19,8 @@ Flickable { id: root contentWidth: parent.width contentHeight: audioColumnLayout.height - topMargin: 16 - bottomMargin: 16 + topMargin: 24 + bottomMargin: 24 clip: true function changePeakValuesEnabled(enabled) { @@ -60,7 +60,7 @@ Flickable { HifiStylesUit.GraphikRegular { id: volumeControlsTitle text: "Volume Controls" - Layout.maximumWidth: parent.width + Layout.preferredWidth: parent.width height: paintedHeight size: 22 color: simplifiedUI.colors.text.white @@ -68,57 +68,82 @@ Flickable { SimplifiedControls.Slider { id: peopleVolume - anchors.left: parent.left - anchors.right: parent.right + Layout.preferredWidth: parent.width + Layout.preferredHeight: 30 Layout.topMargin: simplifiedUI.margins.settings.settingsGroupTopMargin - height: 30 labelText: "People Volume" - from: -60 - to: 10 + from: simplifiedUI.numericConstants.mutedValue + to: 20.0 defaultValue: 0.0 - value: AudioScriptingInterface.getAvatarGain() + stepSize: 5.0 + value: AudioScriptingInterface.avatarGain live: true + function updatePeopleGain(sliderValue) { + if (AudioScriptingInterface.avatarGain !== sliderValue) { + AudioScriptingInterface.avatarGain = sliderValue; + } + } onValueChanged: { - if (AudioScriptingInterface.getAvatarGain() != peopleVolume.value) { - AudioScriptingInterface.setAvatarGain(peopleVolume.value); + updatePeopleGain(value); + } + onPressedChanged: { + if (!pressed) { + updatePeopleGain(value); } } } SimplifiedControls.Slider { id: environmentVolume - anchors.left: parent.left - anchors.right: parent.right + Layout.preferredWidth: parent.width + Layout.preferredHeight: 30 Layout.topMargin: 2 - height: 30 labelText: "Environment Volume" - from: -60 - to: 10 + from: simplifiedUI.numericConstants.mutedValue + to: 20.0 defaultValue: 0.0 - value: AudioScriptingInterface.getInjectorGain() + stepSize: 5.0 + value: AudioScriptingInterface.serverInjectorGain live: true + function updateEnvironmentGain(sliderValue) { + if (AudioScriptingInterface.serverInjectorGain !== sliderValue) { + AudioScriptingInterface.serverInjectorGain = sliderValue; + AudioScriptingInterface.localInjectorGain = sliderValue; + } + } onValueChanged: { - if (AudioScriptingInterface.getInjectorGain() != environmentVolume.value) { - AudioScriptingInterface.setInjectorGain(environmentVolume.value); + updateEnvironmentGain(value); + } + onPressedChanged: { + if (!pressed) { + updateEnvironmentGain(value); } } } SimplifiedControls.Slider { id: systemSoundVolume - anchors.left: parent.left - anchors.right: parent.right + Layout.preferredWidth: parent.width + Layout.preferredHeight: 30 Layout.topMargin: 2 - height: 30 labelText: "System Sound Volume" - from: -60 - to: 10 + from: simplifiedUI.numericConstants.mutedValue + to: 20.0 defaultValue: 0.0 - value: AudioScriptingInterface.getSystemInjectorGain() + stepSize: 5.0 + value: AudioScriptingInterface.systemInjectorGain live: true + function updateSystemGain(sliderValue) { + if (AudioScriptingInterface.systemInjectorGain !== sliderValue) { + AudioScriptingInterface.systemInjectorGain = sliderValue; + } + } onValueChanged: { - if (AudioScriptingInterface.getSystemInjectorGain() != systemSoundVolume.value) { - AudioScriptingInterface.setSystemInjectorGain(systemSoundVolume.value); + updateSystemGain(value); + } + onPressedChanged: { + if (!pressed) { + updateSystemGain(value); } } } @@ -144,8 +169,8 @@ Flickable { SimplifiedControls.Switch { id: muteMicrophoneSwitch - width: parent.width - height: 18 + Layout.preferredHeight: 18 + Layout.preferredWidth: parent.width labelTextOn: "Mute Microphone" checked: AudioScriptingInterface.mutedDesktop onClicked: { @@ -155,8 +180,8 @@ Flickable { SimplifiedControls.Switch { id: pushToTalkSwitch - width: parent.width - height: 18 + Layout.preferredHeight: 18 + Layout.preferredWidth: parent.width labelTextOn: "Push to Talk - Press and Hold \"T\" to Talk" checked: AudioScriptingInterface.pushToTalkDesktop onClicked: { @@ -184,12 +209,11 @@ Flickable { ListView { id: inputDeviceListView - anchors.left: parent.left - anchors.right: parent.right + Layout.preferredWidth: parent.width + Layout.preferredHeight: contentItem.height Layout.topMargin: simplifiedUI.margins.settings.settingsGroupTopMargin interactive: false - height: contentItem.height - spacing: 4 + spacing: simplifiedUI.margins.settings.spacingBetweenRadiobuttons clip: true model: AudioScriptingInterface.devices.input delegate: Item { @@ -200,6 +224,8 @@ Flickable { id: inputDeviceCheckbox anchors.left: parent.left width: parent.width - inputLevel.width + height: 16 + wrapLabel: false checked: selectedDesktop text: model.devicename ButtonGroup.group: inputDeviceButtonGroup @@ -221,6 +247,7 @@ Flickable { } SimplifiedControls.Button { + id: audioLoopbackButton property bool audioLoopedBack: AudioScriptingInterface.getLocalEcho() function startAudioLoopback() { @@ -236,29 +263,23 @@ Flickable { } } - Timer { - id: loopbackTimer - interval: 8000 - running: false - repeat: false - onTriggered: { + Component.onDestruction: stopAudioLoopback(); + + onVisibleChanged: { + if (!visible) { stopAudioLoopback(); } } - id: testYourMicButton enabled: !HMD.active - anchors.left: parent.left Layout.topMargin: simplifiedUI.margins.settings.settingsGroupTopMargin width: 160 height: 32 text: audioLoopedBack ? "STOP TESTING" : "TEST YOUR MIC" onClicked: { if (audioLoopedBack) { - loopbackTimer.stop(); stopAudioLoopback(); } else { - loopbackTimer.restart(); startAudioLoopback(); } } @@ -283,12 +304,11 @@ Flickable { ListView { id: outputDeviceListView - anchors.left: parent.left - anchors.right: parent.right + Layout.preferredWidth: parent.width + Layout.preferredHeight: contentItem.height Layout.topMargin: simplifiedUI.margins.settings.settingsGroupTopMargin interactive: false - height: contentItem.height - spacing: 4 + spacing: simplifiedUI.margins.settings.spacingBetweenRadiobuttons clip: true model: AudioScriptingInterface.devices.output delegate: Item { @@ -299,8 +319,10 @@ Flickable { id: outputDeviceCheckbox anchors.left: parent.left width: parent.width + height: 16 checked: selectedDesktop text: model.devicename + wrapLabel: false ButtonGroup.group: outputDeviceButtonGroup onClicked: { AudioScriptingInterface.setOutputDevice(model.info, false); // `false` argument for Desktop mode setting @@ -349,7 +371,6 @@ Flickable { id: testYourSoundButton enabled: !HMD.active - anchors.left: parent.left Layout.topMargin: simplifiedUI.margins.settings.settingsGroupTopMargin width: 160 height: 32 diff --git a/interface/resources/qml/hifi/simplifiedUI/settingsApp/general/General.qml b/interface/resources/qml/hifi/simplifiedUI/settingsApp/general/General.qml index de5e75b7e5..e32890a2dd 100644 --- a/interface/resources/qml/hifi/simplifiedUI/settingsApp/general/General.qml +++ b/interface/resources/qml/hifi/simplifiedUI/settingsApp/general/General.qml @@ -20,8 +20,8 @@ Flickable { id: root contentWidth: parent.width contentHeight: generalColumnLayout.height - topMargin: 16 - bottomMargin: 16 + topMargin: 24 + bottomMargin: 24 clip: true onAvatarNametagModeChanged: { @@ -63,6 +63,7 @@ Flickable { ColumnLayout { id: avatarNameTagsRadioButtonGroup Layout.topMargin: simplifiedUI.margins.settings.settingsGroupTopMargin + spacing: simplifiedUI.margins.settings.spacingBetweenRadiobuttons SimplifiedControls.RadioButton { id: avatarNameTagsOff @@ -110,6 +111,7 @@ Flickable { ColumnLayout { id: performanceRadioButtonGroup Layout.topMargin: simplifiedUI.margins.settings.settingsGroupTopMargin + spacing: simplifiedUI.margins.settings.spacingBetweenRadiobuttons SimplifiedControls.RadioButton { id: performanceLow @@ -157,6 +159,7 @@ Flickable { ColumnLayout { id: cameraRadioButtonGroup Layout.topMargin: simplifiedUI.margins.settings.settingsGroupTopMargin + spacing: simplifiedUI.margins.settings.spacingBetweenRadiobuttons SimplifiedControls.RadioButton { id: firstPerson diff --git a/interface/resources/qml/hifi/simplifiedUI/settingsApp/vr/VR.qml b/interface/resources/qml/hifi/simplifiedUI/settingsApp/vr/VR.qml index a462af0722..c7e3cc9fc2 100644 --- a/interface/resources/qml/hifi/simplifiedUI/settingsApp/vr/VR.qml +++ b/interface/resources/qml/hifi/simplifiedUI/settingsApp/vr/VR.qml @@ -19,8 +19,8 @@ Flickable { id: root contentWidth: parent.width contentHeight: vrColumnLayout.height - topMargin: 16 - bottomMargin: 16 + topMargin: 24 + bottomMargin: 24 clip: true function changePeakValuesEnabled(enabled) { @@ -70,6 +70,7 @@ Flickable { id: controlsRadioButtonGroup width: parent.width Layout.topMargin: simplifiedUI.margins.settings.settingsGroupTopMargin + spacing: simplifiedUI.margins.settings.spacingBetweenRadiobuttons ButtonGroup { id: controlsButtonGroup } @@ -197,12 +198,11 @@ Flickable { ListView { id: inputDeviceListView - anchors.left: parent.left - anchors.right: parent.right + Layout.preferredWidth: parent.width + Layout.preferredHeight: contentItem.height Layout.topMargin: simplifiedUI.margins.settings.settingsGroupTopMargin interactive: false - height: contentItem.height - spacing: 4 + spacing: simplifiedUI.margins.settings.spacingBetweenRadiobuttons clip: true model: AudioScriptingInterface.devices.input delegate: Item { @@ -213,8 +213,10 @@ Flickable { id: inputDeviceCheckbox anchors.left: parent.left width: parent.width - inputLevel.width + height: 16 checked: selectedHMD text: model.devicename + wrapLabel: false ButtonGroup.group: inputDeviceButtonGroup onClicked: { AudioScriptingInterface.setStereoInput(false); // the next selected audio device might not support stereo @@ -234,6 +236,7 @@ Flickable { } SimplifiedControls.Button { + id: audioLoopbackButton property bool audioLoopedBack: AudioScriptingInterface.getLocalEcho() function startAudioLoopback() { @@ -249,29 +252,23 @@ Flickable { } } - Timer { - id: loopbackTimer - interval: 8000 - running: false - repeat: false - onTriggered: { + Component.onDestruction: stopAudioLoopback(); + + onVisibleChanged: { + if (!visible) { stopAudioLoopback(); } } - id: testYourMicButton enabled: HMD.active - anchors.left: parent.left Layout.topMargin: simplifiedUI.margins.settings.settingsGroupTopMargin width: 160 height: 32 text: audioLoopedBack ? "STOP TESTING" : "TEST YOUR MIC" onClicked: { if (audioLoopedBack) { - loopbackTimer.stop(); stopAudioLoopback(); } else { - loopbackTimer.restart(); startAudioLoopback(); } } @@ -296,12 +293,11 @@ Flickable { ListView { id: outputDeviceListView - anchors.left: parent.left - anchors.right: parent.right + Layout.preferredWidth: parent.width + Layout.preferredHeight: contentItem.height Layout.topMargin: simplifiedUI.margins.settings.settingsGroupTopMargin interactive: false - height: contentItem.height - spacing: 4 + spacing: simplifiedUI.margins.settings.spacingBetweenRadiobuttons clip: true model: AudioScriptingInterface.devices.output delegate: Item { @@ -312,11 +308,13 @@ Flickable { id: outputDeviceCheckbox anchors.left: parent.left width: parent.width - checked: selectedDesktop + height: 16 + checked: selectedHMD text: model.devicename + wrapLabel: false ButtonGroup.group: outputDeviceButtonGroup onClicked: { - AudioScriptingInterface.setOutputDevice(model.info, true); // `false` argument for Desktop mode setting + AudioScriptingInterface.setOutputDevice(model.info, true); // `true` argument for VR mode setting } } } @@ -362,7 +360,6 @@ Flickable { id: testYourSoundButton enabled: HMD.active - anchors.left: parent.left Layout.topMargin: simplifiedUI.margins.settings.settingsGroupTopMargin width: 160 height: 32 diff --git a/interface/resources/qml/hifi/simplifiedUI/simplifiedConstants/SimplifiedConstants.qml b/interface/resources/qml/hifi/simplifiedUI/simplifiedConstants/SimplifiedConstants.qml index 5ccc1a7e5c..1f628b041d 100644 --- a/interface/resources/qml/hifi/simplifiedUI/simplifiedConstants/SimplifiedConstants.qml +++ b/interface/resources/qml/hifi/simplifiedUI/simplifiedConstants/SimplifiedConstants.qml @@ -147,7 +147,7 @@ QtObject { } readonly property color darkSeparator: "#595959" - readonly property color darkBackground: "#1A1A1A" + readonly property color darkBackground: "#000000" readonly property color darkBackgroundHighlight: "#575757" readonly property color highlightOnDark: Qt.rgba(1, 1, 1, 0.2) readonly property color white: "#FFFFFF" @@ -182,9 +182,10 @@ QtObject { } readonly property QtObject settings: QtObject { - property real subtitleTopMargin: 2 - property real settingsGroupTopMargin: 10 - property real spacingBetweenSettings: 48 + property int subtitleTopMargin: 2 + property int settingsGroupTopMargin: 24 + property int spacingBetweenSettings: 48 + property int spacingBetweenRadiobuttons: 14 } } @@ -220,4 +221,8 @@ QtObject { } } } + + readonly property QtObject numericConstants: QtObject { + readonly property real mutedValue: -60.0 + } } diff --git a/interface/resources/qml/hifi/simplifiedUI/simplifiedControls/RadioButton.qml b/interface/resources/qml/hifi/simplifiedUI/simplifiedControls/RadioButton.qml index 59c4fa26e4..43d4aaee33 100644 --- a/interface/resources/qml/hifi/simplifiedUI/simplifiedControls/RadioButton.qml +++ b/interface/resources/qml/hifi/simplifiedUI/simplifiedControls/RadioButton.qml @@ -87,7 +87,6 @@ RadioButton { contentItem: Text { id: radioButtonLabel - height: root.radioButtonRadius font.pixelSize: 14 font.family: "Graphik" font.weight: Font.Normal @@ -99,5 +98,6 @@ RadioButton { enabled: root.enabled verticalAlignment: Text.AlignVCenter leftPadding: radioButtonIndicator.width + root.labelLeftMargin + topPadding: -3 // For perfect alignment when using Graphik } } diff --git a/interface/resources/qml/hifi/simplifiedUI/simplifiedControls/Slider.qml b/interface/resources/qml/hifi/simplifiedUI/simplifiedControls/Slider.qml index 2b1dc68261..38b114e9d2 100644 --- a/interface/resources/qml/hifi/simplifiedUI/simplifiedControls/Slider.qml +++ b/interface/resources/qml/hifi/simplifiedUI/simplifiedControls/Slider.qml @@ -30,6 +30,7 @@ Item { property alias live: sliderControl.live property alias stepSize: sliderControl.stepSize property alias snapMode: sliderControl.snapMode + property alias pressed: sliderControl.pressed property real defaultValue: 0.0 HifiStylesUit.GraphikRegular { diff --git a/interface/resources/qml/hifi/simplifiedUI/simplifiedControls/Switch.qml b/interface/resources/qml/hifi/simplifiedUI/simplifiedControls/Switch.qml index e734cd65fe..9377dba9e1 100644 --- a/interface/resources/qml/hifi/simplifiedUI/simplifiedControls/Switch.qml +++ b/interface/resources/qml/hifi/simplifiedUI/simplifiedControls/Switch.qml @@ -70,8 +70,6 @@ Item { } onCheckedChanged: { - root.checkedChanged(); - Tablet.playSound(TabletEnums.ButtonClick); originalSwitch.changeColor(); } diff --git a/interface/resources/qml/hifi/simplifiedUI/topBar/SimplifiedTopBar.qml b/interface/resources/qml/hifi/simplifiedUI/topBar/SimplifiedTopBar.qml index a5a079b4dc..27a786ece2 100644 --- a/interface/resources/qml/hifi/simplifiedUI/topBar/SimplifiedTopBar.qml +++ b/interface/resources/qml/hifi/simplifiedUI/topBar/SimplifiedTopBar.qml @@ -203,7 +203,10 @@ Rectangle { Image { id: outputDeviceButton - property bool outputMuted: false + property bool outputMuted: AudioScriptingInterface.avatarGain === simplifiedUI.numericConstants.mutedValue && + AudioScriptingInterface.serverInjectorGain === simplifiedUI.numericConstants.mutedValue && + AudioScriptingInterface.localInjectorGain === simplifiedUI.numericConstants.mutedValue && + AudioScriptingInterface.systemInjectorGain === simplifiedUI.numericConstants.mutedValue source: outputDeviceButton.outputMuted ? "./images/outputDeviceMuted.svg" : "./images/outputDeviceLoud.svg" anchors.centerIn: parent width: 20 @@ -228,13 +231,16 @@ Rectangle { } onClicked: { Tablet.playSound(TabletEnums.ButtonClick); - outputDeviceButton.outputMuted = !outputDeviceButton.outputMuted; + + if (!outputDeviceButton.outputMuted && !AudioScriptingInterface.muted) { + AudioScriptingInterface.muted = true; + } sendToScript({ "source": "SimplifiedTopBar.qml", "method": "setOutputMuted", "data": { - "outputMuted": outputDeviceButton.outputMuted + "outputMuted": !outputDeviceButton.outputMuted } }); } @@ -242,6 +248,69 @@ Rectangle { } + Item { + id: statusButtonContainer + anchors.verticalCenter: parent.verticalCenter + anchors.left: outputDeviceButtonContainer.right + anchors.leftMargin: 8 + width: 36 + height: width + + Rectangle { + id: statusButton + property string currentStatus + anchors.centerIn: parent + anchors.horizontalCenterOffset: 1 + anchors.verticalCenterOffset: 2 + width: 13 + height: width + radius: width/2 + visible: false + } + + ColorOverlay { + anchors.fill: statusButton + opacity: statusButton.currentStatus ? 1 : 0 + source: statusButton + color: if (statusButton.currentStatus === "busy") { + "#ff001a" + } else if (statusButton.currentStatus === "available") { + "#009036" + } else if (statusButton.currentStatus) { + "#ffed00" + } + } + + Image { + id: focusIcon + source: "./images/focus.svg" + opacity: statusButtonMouseArea.containsMouse ? 1.0 : (statusButton.currentStatus === "busy" ? 0.7 : 0.3) + anchors.centerIn: parent + width: 36 + height: 20 + fillMode: Image.PreserveAspectFit + } + + MouseArea { + id: statusButtonMouseArea + anchors.fill: parent + enabled: statusButton.currentStatus + hoverEnabled: true + onEntered: { + Tablet.playSound(TabletEnums.ButtonHover); + } + onClicked: { + Tablet.playSound(TabletEnums.ButtonClick); + + sendToScript({ + "source": "SimplifiedTopBar.qml", + "method": "toggleStatus" + }); + } + } + } + + Item { id: hmdButtonContainer @@ -250,6 +319,7 @@ Rectangle { anchors.rightMargin: 14 width: 32 height: width + visible: false Image { id: displayModeImage @@ -279,11 +349,6 @@ Rectangle { Tablet.playSound(TabletEnums.ButtonClick); var displayPluginCount = Window.getDisplayPluginCount(); if (HMD.active) { - // This next line seems backwards and shouldn't be necessary - the NOTIFY handler should - // result in `displayModeImage.source` changing automatically - but that's not working. - // This is working. So, I'm keeping it. - displayModeImage.source = "./images/vrMode.svg"; - // Switch to desktop mode - selects first VR display plugin for (var i = 0; i < displayPluginCount; i++) { if (!Window.isDisplayPluginHmd(i)) { @@ -292,11 +357,6 @@ Rectangle { } } } else { - // This next line seems backwards and shouldn't be necessary - the NOTIFY handler should - // result in `displayModeImage.source` changing automatically - but that's not working. - // This is working. So, I'm keeping it. - displayModeImage.source = "./images/desktopMode.svg"; - // Switch to VR mode - selects first HMD display plugin for (var i = 0; i < displayPluginCount; i++) { if (Window.isDisplayPluginHmd(i)) { @@ -306,6 +366,17 @@ Rectangle { } } } + + Component.onCompleted: { + // Don't show VR button unless they have a VR headset. + var displayPluginCount = Window.getDisplayPluginCount(); + for (var i = 0; i < displayPluginCount; i++) { + if (Window.isDisplayPluginHmd(i)) { + hmdButtonContainer.visible = true; + return; + } + } + } } } @@ -385,8 +456,8 @@ Rectangle { } break; - case "updateOutputMuted": - outputDeviceButton.outputMuted = message.data.outputMuted; + case "updateStatusButton": + statusButton.currentStatus = message.data.currentStatus; break; default: diff --git a/interface/resources/qml/hifi/simplifiedUI/topBar/images/focus.svg b/interface/resources/qml/hifi/simplifiedUI/topBar/images/focus.svg new file mode 100644 index 0000000000..f7950650c6 --- /dev/null +++ b/interface/resources/qml/hifi/simplifiedUI/topBar/images/focus.svg @@ -0,0 +1,13 @@ + + + + + + image/svg+xml + + + + + + + diff --git a/interface/resources/qml/hifi/simplifiedUI/topBar/images/status.svg b/interface/resources/qml/hifi/simplifiedUI/topBar/images/status.svg new file mode 100644 index 0000000000..ebd844c471 --- /dev/null +++ b/interface/resources/qml/hifi/simplifiedUI/topBar/images/status.svg @@ -0,0 +1,3 @@ + + + diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 7727708c0f..9064c7676b 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -1190,13 +1190,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo // setup a timer for domain-server check ins QTimer* domainCheckInTimer = new QTimer(this); - QWeakPointer nodeListWeak = nodeList; - connect(domainCheckInTimer, &QTimer::timeout, [this, nodeListWeak] { - auto nodeList = nodeListWeak.lock(); - if (!isServerlessMode() && nodeList) { - nodeList->sendDomainServerCheckIn(); - } - }); + connect(domainCheckInTimer, &QTimer::timeout, nodeList.data(), &NodeList::sendDomainServerCheckIn); domainCheckInTimer->start(DOMAIN_SERVER_CHECK_IN_MSECS); connect(this, &QCoreApplication::aboutToQuit, [domainCheckInTimer] { domainCheckInTimer->stop(); @@ -3328,6 +3322,7 @@ void Application::onDesktopRootContextCreated(QQmlContext* surfaceContext) { surfaceContext->setContextProperty("HMD", DependencyManager::get().data()); surfaceContext->setContextProperty("Scene", DependencyManager::get().data()); surfaceContext->setContextProperty("Render", RenderScriptingInterface::getInstance()); + surfaceContext->setContextProperty("PlatformInfo", PlatformInfoScriptingInterface::getInstance()); surfaceContext->setContextProperty("Workload", _gameWorkload._engine->getConfiguration().get()); surfaceContext->setContextProperty("Reticle", getApplicationCompositor().getReticleInterface()); surfaceContext->setContextProperty("Snapshot", DependencyManager::get().data()); @@ -3451,6 +3446,7 @@ void Application::setupQmlSurface(QQmlContext* surfaceContext, bool setAdditiona surfaceContext->setContextProperty("HiFiAbout", AboutUtil::getInstance()); surfaceContext->setContextProperty("WalletScriptingInterface", DependencyManager::get().data()); surfaceContext->setContextProperty("ResourceRequestObserver", DependencyManager::get().data()); + surfaceContext->setContextProperty("PlatformInfo", PlatformInfoScriptingInterface::getInstance()); } } @@ -3806,10 +3802,14 @@ void Application::handleSandboxStatus(QNetworkReply* reply) { // If this is a first run we short-circuit the address passed in if (_firstRun.get()) { - DependencyManager::get()->goToEntry(); - sentTo = SENT_TO_ENTRY; - _firstRun.set(false); - + if (!_overrideEntry) { + DependencyManager::get()->goToEntry(); + sentTo = SENT_TO_ENTRY; + } else { + DependencyManager::get()->loadSettings(addressLookupString); + sentTo = SENT_TO_PREVIOUS_LOCATION; + } + _firstRun.set(false); } else { QString goingTo = ""; if (addressLookupString.isEmpty()) { @@ -3825,7 +3825,7 @@ void Application::handleSandboxStatus(QNetworkReply* reply) { DependencyManager::get()->loadSettings(addressLookupString); sentTo = SENT_TO_PREVIOUS_LOCATION; } - + UserActivityLogger::getInstance().logAction("startup_sent_to", { { "sent_to", sentTo }, { "sandbox_is_running", sandboxIsRunning }, @@ -3886,6 +3886,7 @@ void Application::setIsInterstitialMode(bool interstitialMode) { } void Application::setIsServerlessMode(bool serverlessDomain) { + DependencyManager::get()->setSendDomainServerCheckInEnabled(!serverlessDomain); auto tree = getEntities()->getTree(); if (tree) { tree->setIsServerlessMode(serverlessDomain); @@ -5439,9 +5440,7 @@ void Application::init() { qCDebug(interfaceapp) << "Loaded settings"; // fire off an immediate domain-server check in now that settings are loaded - if (!isServerlessMode()) { - DependencyManager::get()->sendDomainServerCheckIn(); - } + QMetaObject::invokeMethod(DependencyManager::get().data(), "sendDomainServerCheckIn"); // This allows collision to be set up properly for shape entities supported by GeometryCache. // This is before entity setup to ensure that it's ready for whenever instance collision is initialized. @@ -6404,6 +6403,7 @@ void Application::update(float deltaTime) { PerformanceTimer perfTimer("simulation"); getEntities()->preUpdate(); + _entitySimulation->removeDeadEntities(); auto t0 = std::chrono::high_resolution_clock::now(); auto t1 = t0; @@ -9361,6 +9361,19 @@ void Application::showUrlHandler(const QUrl& url) { } }); } +void Application::overrideEntry(){ + _overrideEntry = true; +} +void Application::forceDisplayName(const QString& displayName) { + getMyAvatar()->setDisplayName(displayName); +} +void Application::forceLoginWithTokens(const QString& tokens) { + DependencyManager::get()->setAccessTokens(tokens); + Setting::Handle(KEEP_ME_LOGGED_IN_SETTING_NAME, true).set(true); +} +void Application::setConfigFileURL(const QString& fileUrl) { + DependencyManager::get()->setConfigFileURL(fileUrl); +} #if defined(Q_OS_ANDROID) void Application::beforeEnterBackground() { diff --git a/interface/src/Application.h b/interface/src/Application.h index 210039beba..837fb8eae6 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -356,6 +356,11 @@ public: void openDirectory(const QString& path); + void overrideEntry(); + void forceDisplayName(const QString& displayName); + void forceLoginWithTokens(const QString& tokens); + void setConfigFileURL(const QString& fileUrl); + signals: void svoImportRequested(const QString& url); @@ -828,5 +833,6 @@ private: bool _resumeAfterLoginDialogActionTaken_WasPostponed { false }; bool _resumeAfterLoginDialogActionTaken_SafeToRun { false }; bool _startUpFinished { false }; + bool _overrideEntry { false }; }; #endif // hifi_Application_h diff --git a/interface/src/Menu.cpp b/interface/src/Menu.cpp index 9341b2316c..bb087c96d5 100644 --- a/interface/src/Menu.cpp +++ b/interface/src/Menu.cpp @@ -49,16 +49,13 @@ #include "DeferredLightingEffect.h" #include "PickManager.h" -#include "LightingModel.h" -#include "AmbientOcclusionEffect.h" -#include "RenderShadowTask.h" -#include "AntialiasingEffect.h" - #include "scripting/SettingsScriptingInterface.h" #if defined(Q_OS_MAC) || defined(Q_OS_WIN) #include "SpeechRecognizer.h" #endif +#include "scripting/RenderScriptingInterface.h" + extern bool DEV_DECIMATE_TEXTURES; Menu* Menu::getInstance() { @@ -367,45 +364,14 @@ Menu::Menu() { // Developer > Render >>> MenuWrapper* renderOptionsMenu = developerMenu->addMenu("Render"); - action = addCheckableActionToQMenuAndActionHash(renderOptionsMenu, MenuOption::AntiAliasing, 0, true); - connect(action, &QAction::triggered, [action] { - auto renderConfig = qApp->getRenderEngine()->getConfiguration(); - if (renderConfig) { - auto mainViewJitterCamConfig = renderConfig->getConfig("RenderMainView.JitterCam"); - auto mainViewAntialiasingConfig = renderConfig->getConfig("RenderMainView.Antialiasing"); - if (mainViewJitterCamConfig && mainViewAntialiasingConfig) { - if (action->isChecked()) { - mainViewJitterCamConfig->play(); - mainViewAntialiasingConfig->setDebugFXAA(false); - } else { - mainViewJitterCamConfig->none(); - mainViewAntialiasingConfig->setDebugFXAA(true); - } - } - } - }); + addCheckableActionToQMenuAndActionHash(renderOptionsMenu, MenuOption::AntiAliasing, 0, RenderScriptingInterface::getInstance()->getAntialiasingEnabled(), + RenderScriptingInterface::getInstance(), SLOT(setAntialiasingEnabled(bool))); - action = addCheckableActionToQMenuAndActionHash(renderOptionsMenu, MenuOption::Shadows, 0, true); - connect(action, &QAction::triggered, [action] { - auto renderConfig = qApp->getRenderEngine()->getConfiguration(); - if (renderConfig) { - auto lightingModelConfig = renderConfig->getConfig("RenderMainView.LightingModel"); - if (lightingModelConfig) { - lightingModelConfig->setShadow(action->isChecked()); - } - } - }); + addCheckableActionToQMenuAndActionHash(renderOptionsMenu, MenuOption::Shadows, 0, RenderScriptingInterface::getInstance()->getShadowsEnabled(), + RenderScriptingInterface::getInstance(), SLOT(setShadowsEnabled(bool))); - action = addCheckableActionToQMenuAndActionHash(renderOptionsMenu, MenuOption::AmbientOcclusion, 0, false); - connect(action, &QAction::triggered, [action] { - auto renderConfig = qApp->getRenderEngine()->getConfiguration(); - if (renderConfig) { - auto lightingModelConfig = renderConfig->getConfig("RenderMainView.LightingModel"); - if (lightingModelConfig) { - lightingModelConfig->setAmbientOcclusion(action->isChecked()); - } - } - }); + addCheckableActionToQMenuAndActionHash(renderOptionsMenu, MenuOption::AmbientOcclusion, 0, RenderScriptingInterface::getInstance()->getAmbientOcclusionEnabled(), + RenderScriptingInterface::getInstance(), SLOT(setAmbientOcclusionEnabled(bool))); addCheckableActionToQMenuAndActionHash(renderOptionsMenu, MenuOption::WorldAxes); addCheckableActionToQMenuAndActionHash(renderOptionsMenu, MenuOption::DefaultSkybox, 0, true); @@ -522,6 +488,12 @@ Menu::Menu() { addCheckableActionToQMenuAndActionHash(renderOptionsMenu, MenuOption::ComputeBlendshapes, 0, true, DependencyManager::get().data(), SLOT(setComputeBlendshapes(bool))); + { + auto drawStatusConfig = qApp->getRenderEngine()->getConfiguration()->getConfig("RenderMainView.DrawStatus"); + addCheckableActionToQMenuAndActionHash(renderOptionsMenu, MenuOption::HighlightTransitions, 0, false, + drawStatusConfig, SLOT(setShowFade(bool))); + } + // Developer > Assets >>> // Menu item is not currently needed but code should be kept in case it proves useful again at some stage. //#define WANT_ASSET_MIGRATION diff --git a/interface/src/Menu.h b/interface/src/Menu.h index eeede178c7..7c462e4e74 100644 --- a/interface/src/Menu.h +++ b/interface/src/Menu.h @@ -228,6 +228,7 @@ namespace MenuOption { const QString NotificationSoundsTablet = "play_notification_sounds_tablet"; const QString ForceCoarsePicking = "Force Coarse Picking"; const QString ComputeBlendshapes = "Compute Blendshapes"; + const QString HighlightTransitions = "Highlight Transitions"; } #endif // hifi_Menu_h diff --git a/interface/src/avatar/AvatarManager.cpp b/interface/src/avatar/AvatarManager.cpp index df620b9a08..f153f66799 100755 --- a/interface/src/avatar/AvatarManager.cpp +++ b/interface/src/avatar/AvatarManager.cpp @@ -412,7 +412,7 @@ AvatarSharedPointer AvatarManager::newSharedAvatar(const QUuid& sessionUUID) { auto otherAvatar = new OtherAvatar(qApp->thread()); otherAvatar->setSessionUUID(sessionUUID); auto nodeList = DependencyManager::get(); - if (!nodeList || !nodeList->isIgnoringNode(sessionUUID)) { + if (nodeList && !nodeList->isIgnoringNode(sessionUUID)) { otherAvatar->createOrb(); } return AvatarSharedPointer(otherAvatar, [](OtherAvatar* ptr) { ptr->deleteLater(); }); @@ -521,6 +521,7 @@ void AvatarManager::buildPhysicsTransaction(PhysicsEngine::Transaction& transact } } } + _otherAvatarsToChangeInPhysics.clear(); } void AvatarManager::handleProcessedPhysicsTransaction(PhysicsEngine::Transaction& transaction) { @@ -645,7 +646,7 @@ void AvatarManager::clearOtherAvatars() { } void AvatarManager::deleteAllAvatars() { - assert(_otherAvatarsToChangeInPhysics.empty()); + _otherAvatarsToChangeInPhysics.clear(); QReadLocker locker(&_hashLock); AvatarHash::iterator avatarIterator = _avatarHash.begin(); while (avatarIterator != _avatarHash.end()) { diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 39f2d9f332..7ac2103543 100644 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -2367,6 +2367,11 @@ void MyAvatar::clearJointsData() { } void MyAvatar::setSkeletonModelURL(const QUrl& skeletonModelURL) { + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(this, "setSkeletonModelURL", Q_ARG(const QUrl&, skeletonModelURL)); + return; + } + _skeletonModelChangeCount++; int skeletonModelChangeCount = _skeletonModelChangeCount; diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index bb5e4c17df..5b12885d1f 100755 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -2409,7 +2409,7 @@ private: void updateEyeContactTarget(float deltaTime); // These are made private for MyAvatar so that you will use the "use" methods instead - virtual void setSkeletonModelURL(const QUrl& skeletonModelURL) override; + Q_INVOKABLE virtual void setSkeletonModelURL(const QUrl& skeletonModelURL) override; virtual void updatePalms() override {} void lateUpdatePalms(); diff --git a/interface/src/avatar/OtherAvatar.cpp b/interface/src/avatar/OtherAvatar.cpp index 107932d5ec..06dcd9767d 100755 --- a/interface/src/avatar/OtherAvatar.cpp +++ b/interface/src/avatar/OtherAvatar.cpp @@ -62,7 +62,7 @@ void OtherAvatar::removeOrb() { } void OtherAvatar::updateOrbPosition() { - if (_otherAvatarOrbMeshPlaceholderID.isNull()) { + if (!_otherAvatarOrbMeshPlaceholderID.isNull()) { EntityItemProperties properties; properties.setPosition(getHead()->getPosition()); DependencyManager::get()->editEntity(_otherAvatarOrbMeshPlaceholderID, properties); diff --git a/interface/src/commerce/Wallet.cpp b/interface/src/commerce/Wallet.cpp index aa50b42075..5af7a357b0 100644 --- a/interface/src/commerce/Wallet.cpp +++ b/interface/src/commerce/Wallet.cpp @@ -654,7 +654,7 @@ QString Wallet::signWithKey(const QByteArray& text, const QString& key) { EC_KEY* ecPrivateKey = NULL; if ((ecPrivateKey = readPrivateKey(keyFilePath()))) { - unsigned char* sig = new unsigned char[ECDSA_size(ecPrivateKey)]; + const auto sig = std::make_unique(ECDSA_size(ecPrivateKey)); unsigned int signatureBytes = 0; @@ -663,10 +663,10 @@ QString Wallet::signWithKey(const QByteArray& text, const QString& key) { QByteArray hashedPlaintext = QCryptographicHash::hash(text, QCryptographicHash::Sha256); int retrn = ECDSA_sign(0, reinterpret_cast(hashedPlaintext.constData()), hashedPlaintext.size(), - sig, &signatureBytes, ecPrivateKey); + sig.get(), &signatureBytes, ecPrivateKey); EC_KEY_free(ecPrivateKey); - QByteArray signature(reinterpret_cast(sig), signatureBytes); + QByteArray signature(reinterpret_cast(sig.get()), signatureBytes); if (retrn != -1) { return signature.toBase64(); } diff --git a/interface/src/main.cpp b/interface/src/main.cpp index 11054d25d0..7fc4a5b651 100644 --- a/interface/src/main.cpp +++ b/interface/src/main.cpp @@ -75,6 +75,7 @@ int main(int argc, const char* argv[]) { QCommandLineOption helpOption = parser.addHelpOption(); QCommandLineOption urlOption("url", "", "value"); + QCommandLineOption noLauncherOption("no-launcher", "Do not execute the launcher"); QCommandLineOption noUpdaterOption("no-updater", "Do not show auto-updater"); QCommandLineOption checkMinSpecOption("checkMinSpec", "Check if machine meets minimum specifications"); QCommandLineOption runServerOption("runServer", "Whether to run the server"); @@ -82,8 +83,11 @@ int main(int argc, const char* argv[]) { QCommandLineOption allowMultipleInstancesOption("allowMultipleInstances", "Allow multiple instances to run"); QCommandLineOption overrideAppLocalDataPathOption("cache", "set test cache ", "dir"); QCommandLineOption overrideScriptsPathOption(SCRIPTS_SWITCH, "set scripts ", "path"); + QCommandLineOption responseTokensOption("tokens", "set response tokens ", "json"); + QCommandLineOption displayNameOption("displayName", "set user display name ", "string"); parser.addOption(urlOption); + parser.addOption(noLauncherOption); parser.addOption(noUpdaterOption); parser.addOption(checkMinSpecOption); parser.addOption(runServerOption); @@ -91,6 +95,8 @@ int main(int argc, const char* argv[]) { parser.addOption(overrideAppLocalDataPathOption); parser.addOption(overrideScriptsPathOption); parser.addOption(allowMultipleInstancesOption); + parser.addOption(responseTokensOption); + parser.addOption(displayNameOption); if (!parser.parse(arguments)) { std::cout << parser.errorText().toStdString() << std::endl; // Avoid Qt log spam @@ -106,6 +112,52 @@ int main(int argc, const char* argv[]) { Q_UNREACHABLE(); } + QString applicationPath; + { + // A temporary application instance is needed to get the location of the running executable + // Tests using high_resolution_clock show that this takes about 30-50 microseconds (on my machine, YMMV) + // If we wanted to avoid the QCoreApplication, we would need to write our own + // cross-platform implementation. + QCoreApplication tempApp(argc, const_cast(argv)); + applicationPath = QCoreApplication::applicationDirPath(); + } + + static const QString APPLICATION_CONFIG_FILENAME = "config.json"; + QDir applicationDir(applicationPath); + QString configFileName = applicationDir.filePath(APPLICATION_CONFIG_FILENAME); + QFile configFile(configFileName); + QString launcherPath; + + if (configFile.exists()) { + if (!configFile.open(QIODevice::ReadOnly)) { + qWarning() << "Found application config, but could not open it"; + } else { + auto contents = configFile.readAll(); + QJsonParseError error; + + auto doc = QJsonDocument::fromJson(contents, &error); + if (error.error) { + qWarning() << "Found application config, but could not parse it: " << error.errorString(); + } else { + static const QString LAUNCHER_PATH_KEY = "launcherPath"; + launcherPath = doc.object()[LAUNCHER_PATH_KEY].toString(); + if (!launcherPath.isEmpty()) { + if (!parser.isSet(noLauncherOption)) { + qDebug() << "Found a launcherPath in application config. Starting launcher."; + QProcess launcher; + launcher.setProgram(launcherPath); + launcher.startDetached(); + return 0; + } else { + qDebug() << "Found a launcherPath in application config, but the launcher" + " has been suppressed. Continuing normal execution."; + } + configFile.close(); + } + } + } + } + // Early check for --traceFile argument auto tracer = DependencyManager::set(); const char * traceFile = nullptr; @@ -353,6 +405,24 @@ int main(int argc, const char* argv[]) { printSystemInformation(); + auto appPointer = dynamic_cast(&app); + if (appPointer) { + if (parser.isSet(urlOption)) { + appPointer->overrideEntry(); + } + if (parser.isSet(displayNameOption)) { + QString displayName = QString(parser.value(displayNameOption)); + appPointer->forceDisplayName(displayName); + } + if (!launcherPath.isEmpty()) { + appPointer->setConfigFileURL(configFileName); + } + if (parser.isSet(responseTokensOption)) { + QString tokens = QString(parser.value(responseTokensOption)); + appPointer->forceLoginWithTokens(tokens); + } + } + QTranslator translator; translator.load("i18n/interface_en"); app.installTranslator(&translator); diff --git a/interface/src/octree/SafeLanding.cpp b/interface/src/octree/SafeLanding.cpp index 479c2a5860..2e11de508b 100644 --- a/interface/src/octree/SafeLanding.cpp +++ b/interface/src/octree/SafeLanding.cpp @@ -10,7 +10,6 @@ // #include "SafeLanding.h" - #include #include "EntityTreeRenderer.h" @@ -35,24 +34,26 @@ bool SafeLanding::SequenceLessThan::operator()(const int& a, const int& b) const } void SafeLanding::startEntitySequence(QSharedPointer entityTreeRenderer) { - auto entityTree = entityTreeRenderer->getTree(); - if (entityTree) { - Locker lock(_lock); - _entityTree = entityTree; - _trackedEntities.clear(); - _trackingEntities = true; - _maxTrackedEntityCount = 0; - connect(std::const_pointer_cast(_entityTree).get(), - &EntityTree::addingEntity, this, &SafeLanding::addTrackedEntity); - connect(std::const_pointer_cast(_entityTree).get(), - &EntityTree::deletingEntity, this, &SafeLanding::deleteTrackedEntity); + if (!entityTreeRenderer.isNull()) { + auto entityTree = entityTreeRenderer->getTree(); + if (entityTree) { + Locker lock(_lock); + _entityTreeRenderer = entityTreeRenderer; + _trackedEntities.clear(); + _trackingEntities = true; + _maxTrackedEntityCount = 0; + connect(std::const_pointer_cast(entityTree).get(), + &EntityTree::addingEntity, this, &SafeLanding::addTrackedEntity, Qt::DirectConnection); + connect(std::const_pointer_cast(entityTree).get(), + &EntityTree::deletingEntity, this, &SafeLanding::deleteTrackedEntity); - _sequenceNumbers.clear(); - _initialStart = INVALID_SEQUENCE; - _initialEnd = INVALID_SEQUENCE; - _startTime = usecTimestampNow(); - EntityTreeRenderer::setEntityLoadingPriorityFunction(&ElevatedPriority); + _sequenceNumbers.clear(); + _initialStart = INVALID_SEQUENCE; + _initialEnd = INVALID_SEQUENCE; + _startTime = usecTimestampNow(); + EntityTreeRenderer::setEntityLoadingPriorityFunction(&ElevatedPriority); + } } } @@ -70,7 +71,12 @@ void SafeLanding::stopEntitySequence() { void SafeLanding::addTrackedEntity(const EntityItemID& entityID) { if (_trackingEntities) { Locker lock(_lock); - EntityItemPointer entity = _entityTree->findEntityByID(entityID); + + if (_entityTreeRenderer.isNull() || _entityTreeRenderer->getTree() == nullptr) { + return; + } + + EntityItemPointer entity = _entityTreeRenderer->getTree()->findEntityByID(entityID); if (entity && !entity->isLocalEntity() && entity->getCreated() < _startTime) { @@ -111,7 +117,7 @@ bool SafeLanding::isLoadSequenceComplete() { Locker lock(_lock); _initialStart = INVALID_SEQUENCE; _initialEnd = INVALID_SEQUENCE; - _entityTree = nullptr; + _entityTreeRenderer.clear(); _trackingEntities = false; // Don't track anything else that comes in. EntityTreeRenderer::setEntityLoadingPriorityFunction(StandardPriority); } @@ -158,7 +164,7 @@ bool SafeLanding::isSequenceNumbersComplete() { return false; } -bool isEntityPhysicsReady(const EntityItemPointer& entity) { +bool SafeLanding::isEntityPhysicsReady(const EntityItemPointer& entity) { if (entity && !entity->getCollisionless()) { const auto& entityType = entity->getType(); if (entityType == EntityTypes::Model) { @@ -168,7 +174,10 @@ bool isEntityPhysicsReady(const EntityItemPointer& entity) { bool hasAABox; entity->getAABox(hasAABox); if (hasAABox && downloadedCollisionTypes.count(modelEntity->getShapeType()) != 0) { - return (!entity->shouldBePhysical() || entity->isInPhysicsSimulation() || modelEntity->computeShapeFailedToLoad()); + auto space = _entityTreeRenderer->getWorkloadSpace(); + uint8_t region = space ? space->getRegion(entity->getSpaceIndex()) : (uint8_t)workload::Region::INVALID; + bool shouldBePhysical = region < workload::Region::R3 && entity->shouldBePhysical(); + return (!shouldBePhysical || entity->isInPhysicsSimulation() || modelEntity->computeShapeFailedToLoad()); } } } diff --git a/interface/src/octree/SafeLanding.h b/interface/src/octree/SafeLanding.h index 51357b60ff..428ca15bdc 100644 --- a/interface/src/octree/SafeLanding.h +++ b/interface/src/octree/SafeLanding.h @@ -38,13 +38,14 @@ private slots: private: bool isSequenceNumbersComplete(); + bool isEntityPhysicsReady(const EntityItemPointer& entity); void debugDumpSequenceIDs() const; bool isEntityLoadingComplete(); std::mutex _lock; using Locker = std::lock_guard; bool _trackingEntities { false }; - EntityTreePointer _entityTree; + QSharedPointer _entityTreeRenderer; using EntityMap = std::map; EntityMap _trackedEntities; diff --git a/interface/src/scripting/Audio.cpp b/interface/src/scripting/Audio.cpp index cb8b211352..6df4729ee0 100644 --- a/interface/src/scripting/Audio.cpp +++ b/interface/src/scripting/Audio.cpp @@ -400,10 +400,19 @@ void Audio::setReverbOptions(const AudioEffectOptions* options) { } void Audio::setAvatarGain(float gain) { + bool changed = false; + if (getAvatarGain() != gain) { + changed = true; + } + withWriteLock([&] { // ask the NodeList to set the master avatar gain DependencyManager::get()->setAvatarGain(QUuid(), gain); }); + + if (changed) { + emit avatarGainChanged(gain); + } } float Audio::getAvatarGain() { @@ -413,10 +422,19 @@ float Audio::getAvatarGain() { } void Audio::setInjectorGain(float gain) { + bool changed = false; + if (getInjectorGain() != gain) { + changed = true; + } + withWriteLock([&] { // ask the NodeList to set the audio injector gain DependencyManager::get()->setInjectorGain(gain); }); + + if (changed) { + emit serverInjectorGainChanged(gain); + } } float Audio::getInjectorGain() { @@ -426,6 +444,11 @@ float Audio::getInjectorGain() { } void Audio::setLocalInjectorGain(float gain) { + bool changed = false; + if (getLocalInjectorGain() != gain) { + changed = true; + } + withWriteLock([&] { if (_localInjectorGain != gain) { _localInjectorGain = gain; @@ -436,6 +459,11 @@ void Audio::setLocalInjectorGain(float gain) { DependencyManager::get()->setLocalInjectorGain(gain); } }); + + + if (changed) { + emit localInjectorGainChanged(gain); + } } float Audio::getLocalInjectorGain() { @@ -445,6 +473,11 @@ float Audio::getLocalInjectorGain() { } void Audio::setSystemInjectorGain(float gain) { + bool changed = false; + if (getSystemInjectorGain() != gain) { + changed = true; + } + withWriteLock([&] { if (_systemInjectorGain != gain) { _systemInjectorGain = gain; @@ -455,6 +488,10 @@ void Audio::setSystemInjectorGain(float gain) { DependencyManager::get()->setSystemInjectorGain(gain); } }); + + if (changed) { + emit systemInjectorGainChanged(gain); + } } float Audio::getSystemInjectorGain() { diff --git a/interface/src/scripting/Audio.h b/interface/src/scripting/Audio.h index ed54dca5c6..c7ac98402c 100644 --- a/interface/src/scripting/Audio.h +++ b/interface/src/scripting/Audio.h @@ -66,6 +66,10 @@ class Audio : public AudioScriptingInterface, protected ReadWriteLockable { * @property {boolean} pushToTalkHMD - true if HMD push-to-talk is enabled, otherwise false. * @property {boolean} pushingToTalk - true if the user is currently pushing-to-talk, otherwise * false. + * @property {float} avatarGain - The gain (relative volume) that avatars' voices are played at. This gain is used at the server. + * @property {float} localInjectorGain - The gain (relative volume) that local injectors (local environment sounds) are played at. + * @property {float} serverInjectorGain - The gain (relative volume) that server injectors (server environment sounds) are played at. This gain is used at the server. + * @property {float} systemInjectorGain - The gain (relative volume) that system sounds are played at. * * @comment The following properties are from AudioScriptingInterface.h. * @property {boolean} isStereoInput - true if the input audio is being used in stereo, otherwise @@ -90,6 +94,10 @@ class Audio : public AudioScriptingInterface, protected ReadWriteLockable { Q_PROPERTY(bool pushToTalkDesktop READ getPTTDesktop WRITE setPTTDesktop NOTIFY pushToTalkDesktopChanged) Q_PROPERTY(bool pushToTalkHMD READ getPTTHMD WRITE setPTTHMD NOTIFY pushToTalkHMDChanged) Q_PROPERTY(bool pushingToTalk READ getPushingToTalk WRITE setPushingToTalk NOTIFY pushingToTalkChanged) + Q_PROPERTY(float avatarGain READ getAvatarGain WRITE setAvatarGain NOTIFY avatarGainChanged) + Q_PROPERTY(float localInjectorGain READ getLocalInjectorGain WRITE setLocalInjectorGain NOTIFY localInjectorGainChanged) + Q_PROPERTY(float serverInjectorGain READ getInjectorGain WRITE setInjectorGain NOTIFY serverInjectorGainChanged) + Q_PROPERTY(float systemInjectorGain READ getSystemInjectorGain WRITE setSystemInjectorGain NOTIFY systemInjectorGainChanged) public: static QString AUDIO; @@ -412,6 +420,38 @@ signals: */ void pushingToTalkChanged(bool talking); + /**jsdoc + * Triggered when the avatar gain changes. + * @function Audio.avatarGainChanged + * @param {float} gain - The new avatar gain value. + * @returns {Signal} + */ + void avatarGainChanged(float gain); + + /**jsdoc + * Triggered when the local injector gain changes. + * @function Audio.localInjectorGainChanged + * @param {float} gain - The new local injector gain value. + * @returns {Signal} + */ + void localInjectorGainChanged(float gain); + + /**jsdoc + * Triggered when the server injector gain changes. + * @function Audio.serverInjectorGainChanged + * @param {float} gain - The new server injector gain value. + * @returns {Signal} + */ + void serverInjectorGainChanged(float gain); + + /**jsdoc + * Triggered when the system injector gain changes. + * @function Audio.systemInjectorGainChanged + * @param {float} gain - The new system injector gain value. + * @returns {Signal} + */ + void systemInjectorGainChanged(float gain); + public slots: /**jsdoc diff --git a/interface/src/scripting/PlatformInfoScriptingInterface.cpp b/interface/src/scripting/PlatformInfoScriptingInterface.cpp index 89d609810c..9a5a08503d 100644 --- a/interface/src/scripting/PlatformInfoScriptingInterface.cpp +++ b/interface/src/scripting/PlatformInfoScriptingInterface.cpp @@ -10,6 +10,9 @@ #include #include +#include +#include + #ifdef Q_OS_WIN #include #elif defined Q_OS_MAC @@ -21,6 +24,17 @@ PlatformInfoScriptingInterface* PlatformInfoScriptingInterface::getInstance() { return &sharedInstance; } + +PlatformInfoScriptingInterface::PlatformInfoScriptingInterface() { + platform::create(); + if (!platform::enumeratePlatform()) { + } +} + +PlatformInfoScriptingInterface::~PlatformInfoScriptingInterface() { + platform::destroy(); +} + QString PlatformInfoScriptingInterface::getOperatingSystemType() { #ifdef Q_OS_WIN return "WINDOWS"; @@ -149,3 +163,52 @@ bool PlatformInfoScriptingInterface::isStandalone() { return qApp->property(hifi::properties::STANDALONE).toBool(); #endif } + +int PlatformInfoScriptingInterface::getNumCPUs() { + return platform::getNumCPUs(); +} + +QString PlatformInfoScriptingInterface::getCPU(int index) { + auto desc = platform::getCPU(index); + return QString(desc.dump().c_str()); +} + +int PlatformInfoScriptingInterface::getNumGPUs() { + return platform::getNumGPUs(); +} + +QString PlatformInfoScriptingInterface::getGPU(int index) { + auto desc = platform::getGPU(index); + return QString(desc.dump().c_str()); +} + +int PlatformInfoScriptingInterface::getNumDisplays() { + return platform::getNumDisplays(); +} + +QString PlatformInfoScriptingInterface::getDisplay(int index) { + auto desc = platform::getDisplay(index); + return QString(desc.dump().c_str()); +} + +QString PlatformInfoScriptingInterface::getMemory() { + auto desc = platform::getMemory(0); + return QString(desc.dump().c_str()); +} + +QString PlatformInfoScriptingInterface::getComputer() { + auto desc = platform::getComputer(); + return QString(desc.dump().c_str()); +} + + +PlatformInfoScriptingInterface::PlatformTier PlatformInfoScriptingInterface::getTierProfiled() { + return (PlatformInfoScriptingInterface::PlatformTier) platform::Profiler::profilePlatform(); +} + +QStringList PlatformInfoScriptingInterface::getPlatformTierNames() { + static const QStringList platformTierNames = { "UNKNWON", "LOW", "MID", "HIGH" }; + return platformTierNames; +} + + diff --git a/interface/src/scripting/PlatformInfoScriptingInterface.h b/interface/src/scripting/PlatformInfoScriptingInterface.h index c065f849f2..0ca9dbff1a 100644 --- a/interface/src/scripting/PlatformInfoScriptingInterface.h +++ b/interface/src/scripting/PlatformInfoScriptingInterface.h @@ -9,6 +9,7 @@ #ifndef hifi_PlatformInfoScriptingInterface_h #define hifi_PlatformInfoScriptingInterface_h +#include #include class QScriptValue; @@ -25,6 +26,20 @@ class QScriptValue; class PlatformInfoScriptingInterface : public QObject { Q_OBJECT + +public: + PlatformInfoScriptingInterface(); + virtual ~PlatformInfoScriptingInterface(); + + // Platform tier enum type + enum PlatformTier { + UNKNOWN = platform::Profiler::Tier::UNKNOWN, + LOW = platform::Profiler::Tier::LOW, + MID = platform::Profiler::Tier::MID, + HIGH = platform::Profiler::Tier::HIGH, + }; + Q_ENUM(PlatformTier); + public slots: /**jsdoc * @function PlatformInfo.getInstance @@ -98,6 +113,96 @@ public slots: * @returns {boolean} true if Interface is running on a stand-alone device, false if it isn't. */ bool isStandalone(); + + /**jsdoc + * Get the number of CPUs. + * @function PlatformInfo.getNumCPUs + * @returns {number} The number of CPUs detected on the hardware platform. + */ + int getNumCPUs(); + + /**jsdoc + * Get the description of the CPU at the index parameter + * expected fields are: + * - cpuVendor... + * @param index The index of the CPU of the platform + * @function PlatformInfo.getCPU + * @returns {string} The CPU description json field + */ + QString getCPU(int index); + + /**jsdoc + * Get the number of GPUs. + * @function PlatformInfo.getNumGPUs + * @returns {number} The number of GPUs detected on the hardware platform. + */ + int getNumGPUs(); + + /**jsdoc + * Get the description of the GPU at the index parameter + * expected fields are: + * - gpuVendor... + * @param index The index of the GPU of the platform + * @function PlatformInfo.getGPU + * @returns {string} The GPU description json field + */ + QString getGPU(int index); + + /**jsdoc + * Get the number of Displays. + * @function PlatformInfo.getNumDisplays + * @returns {number} The number of Displays detected on the hardware platform. + */ + int getNumDisplays(); + + /**jsdoc + * Get the description of the Display at the index parameter + * expected fields are: + * - DisplayVendor... + * @param index The index of the Display of the platform + * @function PlatformInfo.getDisplay + * @returns {string} The Display description json field + */ + QString getDisplay(int index); + + /**jsdoc + * Get the description of the Memory + * expected fields are: + * - MemoryVendor... + * @function PlatformInfo.getMemory + * @returns {string} The Memory description json field + */ + QString getMemory(); + + /**jsdoc + * Get the description of the Computer + * expected fields are: + * - ComputerVendor... + * @function PlatformInfo.getComputer + * @returns {string} The Computer description json field + */ + QString getComputer(); + + + /**jsdoc + * Get the Platform TIer profiled on startup of the Computer + * Platform Tier is an ineger/enum value: + * LOW = 0, MID = 1, HIGH = 2 + * @function PlatformInfo.getTierProfiled + * @returns {number} The Platform Tier profiled on startup. + */ + PlatformTier getTierProfiled(); + + /**jsdoc + * Get the Platform Tier possible Names as an array of strings + * Platform Tier is an ineger/enum value: + * LOW = 0, MID = 1, HIGH = 2 + * @function PlatformInfo.getPlatformTierNames + * @returns {string} The array of names matching the number returned from PlatformInfo.getTierProfiled + */ + QStringList getPlatformTierNames(); + + }; #endif // hifi_PlatformInfoScriptingInterface_h diff --git a/interface/src/scripting/RenderScriptingInterface.cpp b/interface/src/scripting/RenderScriptingInterface.cpp index 8581c7527d..360a75b557 100644 --- a/interface/src/scripting/RenderScriptingInterface.cpp +++ b/interface/src/scripting/RenderScriptingInterface.cpp @@ -7,6 +7,9 @@ // #include "RenderScriptingInterface.h" +#include "LightingModel.h" +#include "AntialiasingEffect.h" + const QString DEFERRED = "deferred"; const QString FORWARD = "forward"; @@ -17,6 +20,9 @@ RenderScriptingInterface* RenderScriptingInterface::getInstance() { RenderScriptingInterface::RenderScriptingInterface() { setRenderMethod((render::Args::RenderMethod)_renderMethodSetting.get() == render::Args::RenderMethod::DEFERRED ? DEFERRED : FORWARD); + setShadowsEnabled(_shadowsEnabledSetting.get()); + setAmbientOcclusionEnabled(_ambientOcclusionEnabledSetting.get()); + setAntialiasingEnabled(_antialiasingEnabledSetting.get()); } QString RenderScriptingInterface::getRenderMethod() { @@ -24,6 +30,11 @@ QString RenderScriptingInterface::getRenderMethod() { } void RenderScriptingInterface::setRenderMethod(const QString& renderMethod) { + render::Args::RenderMethod newMethod = renderMethod == FORWARD ? render::Args::RenderMethod::FORWARD : render::Args::RenderMethod::DEFERRED; + if (_renderMethodSetting.get() == newMethod) { + return; + } + if (QThread::currentThread() != thread()) { QMetaObject::invokeMethod(this, "setRenderMethod", Q_ARG(const QString&, renderMethod)); return; @@ -31,14 +42,81 @@ void RenderScriptingInterface::setRenderMethod(const QString& renderMethod) { auto config = dynamic_cast(qApp->getRenderEngine()->getConfiguration()->getConfig("RenderMainView.DeferredForwardSwitch")); if (config) { - if (renderMethod == DEFERRED) { - _renderMethodSetting.set(render::Args::RenderMethod::DEFERRED); - config->setBranch(render::Args::RenderMethod::DEFERRED); - emit config->dirtyEnabled(); - } else if (renderMethod == FORWARD) { - _renderMethodSetting.set(render::Args::RenderMethod::FORWARD); - config->setBranch(render::Args::RenderMethod::FORWARD); - emit config->dirtyEnabled(); + _renderMethodSetting.set(newMethod); + config->setBranch(newMethod); + emit config->dirtyEnabled(); + } +} + +bool RenderScriptingInterface::getShadowsEnabled() { + return _shadowsEnabledSetting.get(); +} + +void RenderScriptingInterface::setShadowsEnabled(bool enabled) { + if (_shadowsEnabledSetting.get() == enabled) { + return; + } + + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(this, "setShadowsEnabled", Q_ARG(bool, enabled)); + return; + } + + auto lightingModelConfig = qApp->getRenderEngine()->getConfiguration()->getConfig("RenderMainView.LightingModel"); + if (lightingModelConfig) { + Menu::getInstance()->setIsOptionChecked(MenuOption::Shadows, enabled); + _shadowsEnabledSetting.set(enabled); + lightingModelConfig->setShadow(enabled); + } +} + +bool RenderScriptingInterface::getAmbientOcclusionEnabled() { + return _ambientOcclusionEnabledSetting.get(); +} + +void RenderScriptingInterface::setAmbientOcclusionEnabled(bool enabled) { + if (_ambientOcclusionEnabledSetting.get() == enabled) { + return; + } + + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(this, "setAmbientOcclusionEnabled", Q_ARG(bool, enabled)); + return; + } + + auto lightingModelConfig = qApp->getRenderEngine()->getConfiguration()->getConfig("RenderMainView.LightingModel"); + if (lightingModelConfig) { + Menu::getInstance()->setIsOptionChecked(MenuOption::AmbientOcclusion, enabled); + _ambientOcclusionEnabledSetting.set(enabled); + lightingModelConfig->setAmbientOcclusion(enabled); + } +} + +bool RenderScriptingInterface::getAntialiasingEnabled() { + return _antialiasingEnabledSetting.get(); +} + +void RenderScriptingInterface::setAntialiasingEnabled(bool enabled) { + if (_antialiasingEnabledSetting.get() == enabled) { + return; + } + + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(this, "setAntialiasingEnabled", Q_ARG(bool, enabled)); + return; + } + + auto mainViewJitterCamConfig = qApp->getRenderEngine()->getConfiguration()->getConfig("RenderMainView.JitterCam"); + auto mainViewAntialiasingConfig = qApp->getRenderEngine()->getConfiguration()->getConfig("RenderMainView.Antialiasing"); + if (mainViewJitterCamConfig && mainViewAntialiasingConfig) { + Menu::getInstance()->setIsOptionChecked(MenuOption::AntiAliasing, enabled); + _antialiasingEnabledSetting.set(enabled); + if (enabled) { + mainViewJitterCamConfig->play(); + mainViewAntialiasingConfig->setDebugFXAA(false); + } else { + mainViewJitterCamConfig->none(); + mainViewAntialiasingConfig->setDebugFXAA(true); } } } \ No newline at end of file diff --git a/interface/src/scripting/RenderScriptingInterface.h b/interface/src/scripting/RenderScriptingInterface.h index 2aff3a08b6..329433fb60 100644 --- a/interface/src/scripting/RenderScriptingInterface.h +++ b/interface/src/scripting/RenderScriptingInterface.h @@ -26,6 +26,9 @@ class RenderScriptingInterface : public QObject { Q_OBJECT Q_PROPERTY(QString renderMethod READ getRenderMethod WRITE setRenderMethod) + Q_PROPERTY(bool shadowsEnabled READ getShadowsEnabled WRITE setShadowsEnabled) + Q_PROPERTY(bool ambientOcclusionEnabled READ getAmbientOcclusionEnabled WRITE setAmbientOcclusionEnabled) + Q_PROPERTY(bool antialiasingEnabled READ getAntialiasingEnabled WRITE setAntialiasingEnabled) public: RenderScriptingInterface(); @@ -37,8 +40,8 @@ public slots: * Get a config for a job by name * @function Render.getConfig * @param {string} name - Can be: - * - . Search for the first job named job_name traversing the the sub graph of task and jobs (from this task as root) - * - .[.]. Allows you to first look for the parent_name job (from this task as root) and then search from there for the + * - : Search for the first job named job_name traversing the the sub graph of task and jobs (from this task as root) + * - .[.]: Allows you to first look for the parent_name job (from this task as root) and then search from there for the * optional sub_parent_names and finally from there looking for the job_name (assuming every job in the path is found) * @returns {object} The sub job config. */ @@ -58,8 +61,53 @@ public slots: */ void setRenderMethod(const QString& renderMethod); + /**jsdoc + * Whether or not shadows are enabled + * @function Render.getShadowsEnabled + * @returns {bool} true if shadows are enabled, otherwise false + */ + bool getShadowsEnabled(); + + /**jsdoc + * Enables or disables shadows + * @function Render.setShadowsEnabled + * @param {bool} enabled - true to enable shadows, false to disable them + */ + void setShadowsEnabled(bool enabled); + + /**jsdoc + * Whether or not ambient occlusion is enabled + * @function Render.getAmbientOcclusionEnabled + * @returns {bool} true if ambient occlusion is enabled, otherwise false + */ + bool getAmbientOcclusionEnabled(); + + /**jsdoc + * Enables or disables ambient occlusion + * @function Render.setAmbientOcclusionEnabled + * @param {bool} enabled - true to enable ambient occlusion, false to disable it + */ + void setAmbientOcclusionEnabled(bool enabled); + + /**jsdoc + * Whether or not anti-aliasing is enabled + * @function Render.getAntialiasingEnabled + * @returns {bool} true if anti-aliasing is enabled, otherwise false + */ + bool getAntialiasingEnabled(); + + /**jsdoc + * Enables or disables anti-aliasing + * @function Render.setAntialiasingEnabled + * @param {bool} enabled - true to enable anti-aliasing, false to disable it + */ + void setAntialiasingEnabled(bool enabled); + private: Setting::Handle _renderMethodSetting { "renderMethod", RENDER_FORWARD ? render::Args::RenderMethod::FORWARD : render::Args::RenderMethod::DEFERRED }; + Setting::Handle _shadowsEnabledSetting { "shadowsEnabled", true }; + Setting::Handle _ambientOcclusionEnabledSetting { "ambientOcclusionEnabled", false }; + Setting::Handle _antialiasingEnabledSetting { "antialiasingEnabled", true }; }; #endif // hifi_RenderScriptingInterface_h diff --git a/interface/src/scripting/SelectionScriptingInterface.cpp b/interface/src/scripting/SelectionScriptingInterface.cpp index c15b5cde11..5856188868 100644 --- a/interface/src/scripting/SelectionScriptingInterface.cpp +++ b/interface/src/scripting/SelectionScriptingInterface.cpp @@ -44,13 +44,14 @@ SelectionScriptingInterface::SelectionScriptingInterface() { } /**jsdoc + * The type of a specific item in a selection list. * * * * * - * - * + * + * * *
ValueDescription
"avatar"
"entity"
"avatar"The item is an avatar.
"entity"The item is an entity.
* @typedef {string} Selection.ItemType @@ -245,9 +246,10 @@ void SelectionScriptingInterface::printList(const QString& listName) { } /**jsdoc + * A selection list. * @typedef {object} Selection.SelectedItemsList - * @property {Uuid[]} avatars - The IDs of the avatars in the selection. - * @property {Uuid[]} entities - The IDs of the entities in the selection. + * @property {Uuid[]} avatars - The IDs of the avatars in the selection list. + * @property {Uuid[]} entities - The IDs of the entities in the selection list. */ QVariantMap SelectionScriptingInterface::getSelectedItemsList(const QString& listName) const { QReadLocker lock(&_selectionListsLock); @@ -438,18 +440,19 @@ bool SelectionHighlightStyle::fromVariantMap(const QVariantMap& properties) { } /**jsdoc + * The highlighting style of a selection list. * @typedef {object} Selection.HighlightStyle - * @property {Color} outlineUnoccludedColor - Color of the specified highlight region. - * @property {Color} outlineOccludedColor - "" - * @property {Color} fillUnoccludedColor- "" - * @property {Color} fillOccludedColor- "" - * @property {number} outlineUnoccludedAlpha - Alpha value ranging from 0.0 (not visible) to 1.0 - * (fully opaque) for the specified highlight region. - * @property {number} outlineOccludedAlpha - "" - * @property {number} fillUnoccludedAlpha - "" - * @property {number} fillOccludedAlpha - "" - * @property {number} outlineWidth - Width of the outline, in pixels. - * @property {boolean} isOutlineSmooth - true to enable outline smooth fall-off. + * @property {Color} outlineUnoccludedColor=255,178,51 - Unoccluded outline color. + * @property {Color} outlineOccludedColor=255,178,51 - Occluded outline color. + * @property {Color} fillUnoccludedColor=51,178,255 - Unoccluded fill color. + * @property {Color} fillOccludedColor=51,178,255 - Occluded fill color. + * @property {number} outlineUnoccludedAlpha=0.9 - Unoccluded outline alpha, range 0.01.0. + * @property {number} outlineOccludedAlpha=0.9 - Occluded outline alpha, range 0.01.0. + * @property {number} fillUnoccludedAlpha=0.0 - Unoccluded fill alpha, range 0.01.0. + * @property {number} fillOccludedAlpha=0.0 - Occluded fill alpha, range 0.01.0. + * @property {number} outlineWidth=2 - Width of the outline, in pixels. + * @property {boolean} isOutlineSmooth=false - true to fade the outside edge of the outline, false + * to have a sharp edge. */ QVariantMap SelectionHighlightStyle::toVariantMap() const { QVariantMap properties; diff --git a/interface/src/scripting/SelectionScriptingInterface.h b/interface/src/scripting/SelectionScriptingInterface.h index fcb4090184..4386ee5ee6 100644 --- a/interface/src/scripting/SelectionScriptingInterface.h +++ b/interface/src/scripting/SelectionScriptingInterface.h @@ -77,47 +77,45 @@ protected: }; /**jsdoc - * The Selection API provides a means of grouping together avatars and entities in named lists. + * The Selection API provides a means of grouping together and highlighting avatars and entities in named lists. + * * @namespace Selection * * @hifi-interface * @hifi-client-entity * @hifi-avatar * - * @example Outline an entity when it is grabbed by a controller. - * // Create a box and copy the following text into the entity's "Script URL" field. + * @example Outline an entity when it is grabbed by the mouse or a controller. + * // Create an entity and copy the following script into the entity's "Script URL" field. + * // Move the entity behind another entity to see the occluded outline. * (function () { - * print("Starting highlight script..............."); - * var _this = this; - * var prevID = 0; - * var listName = "contextOverlayHighlightList"; - * var listType = "entity"; - * - * _this.startNearGrab = function(entityID){ - * if (prevID !== entityID) { - * Selection.addToSelectedItemsList(listName, listType, entityID); - * prevID = entityID; - * } + * var LIST_NAME = "SelectionExample", + * ITEM_TYPE = "entity", + * HIGHLIGHT_STYLE = { + * outlineUnoccludedColor: { red: 0, green: 180, blue: 239 }, + * outlineUnoccludedAlpha: 0.5, + * outlineOccludedColor: { red: 239, green: 180, blue: 0 }, + * outlineOccludedAlpha: 0.5, + * outlineWidth: 4 + * }; + * + * Selection.enableListHighlight(LIST_NAME, HIGHLIGHT_STYLE); + * + * this.startNearGrab = function (entityID) { + * Selection.addToSelectedItemsList(LIST_NAME, ITEM_TYPE, entityID); * }; - * - * _this.releaseGrab = function(entityID){ - * if (prevID !== 0) { - * Selection.removeFromSelectedItemsList("contextOverlayHighlightList", listType, prevID); - * prevID = 0; - * } + * + * this.startDistanceGrab = function (entityID) { + * Selection.addToSelectedItemsList(LIST_NAME, ITEM_TYPE, entityID); * }; - * - * var cleanup = function(){ - * Entities.findEntities(MyAvatar.position, 1000).forEach(function(entity) { - * try { - * Selection.removeListFromMap(listName); - * } catch (e) { - * print("Error cleaning up."); - * } - * }); + * + * this.releaseGrab = function (entityID) { + * Selection.removeFromSelectedItemsList(LIST_NAME, ITEM_TYPE, entityID); * }; - * - * Script.scriptEnding.connect(cleanup); + * + * Script.scriptEnding.connect(function () { + * Selection.removeListFromMap(LIST_NAME); + * }); * }); */ class SelectionScriptingInterface : public QObject, public Dependency { @@ -127,121 +125,119 @@ public: SelectionScriptingInterface(); /**jsdoc - * Get the names of all the selection lists. - * @function Selection.getListNames - * @returns {list[]} An array of names of all the selection lists. - */ + * Gets the names of all current selection lists. + * @function Selection.getListNames + * @returns {string[]} The names of all current selection lists. + * @example List all the current selection lists. + * print("Selection lists: " + Selection.getListNames()); + */ Q_INVOKABLE QStringList getListNames() const; /**jsdoc - * Delete a named selection list. - * @function Selection.removeListFromMap - * @param {string} listName - The name of the selection list. - * @returns {boolean} true if the selection existed and was successfully removed, otherwise false. - */ + * Deletes a selection list. + * @function Selection.removeListFromMap + * @param {string} listName - The name of the selection list to delete. + * @returns {boolean} true if the selection existed and was successfully removed, otherwise false. + */ Q_INVOKABLE bool removeListFromMap(const QString& listName); /**jsdoc - * Add an item to a selection list. - * @function Selection.addToSelectedItemsList - * @param {string} listName - The name of the selection list to add the item to. - * @param {Selection.ItemType} itemType - The type of the item being added. - * @param {Uuid} id - The ID of the item to add to the selection. - * @returns {boolean} true if the item was successfully added, otherwise false. - */ + * Adds an item to a selection list. The list is created if it doesn't exist. + * @function Selection.addToSelectedItemsList + * @param {string} listName - The name of the selection list to add the item to. + * @param {Selection.ItemType} itemType - The type of item being added. + * @param {Uuid} itemID - The ID of the item to add. + * @returns {boolean} true if the item was successfully added or already existed in the list, otherwise + * false. + */ Q_INVOKABLE bool addToSelectedItemsList(const QString& listName, const QString& itemType, const QUuid& id); + /**jsdoc - * Remove an item from a selection list. - * @function Selection.removeFromSelectedItemsList - * @param {string} listName - The name of the selection list to remove the item from. - * @param {Selection.ItemType} itemType - The type of the item being removed. - * @param {Uuid} id - The ID of the item to remove. - * @returns {boolean} true if the item was successfully removed, otherwise false. - * is returned if the list doesn't contain any data. - */ + * Removes an item from a selection list. + * @function Selection.removeFromSelectedItemsList + * @param {string} listName - The name of the selection list to remove the item from. + * @param {Selection.ItemType} itemType - The type of item being removed. + * @param {Uuid} itemID - The ID of the item to remove. + * @returns {boolean} true if the item was successfully removed or was not in the list, otherwise + * false. + */ Q_INVOKABLE bool removeFromSelectedItemsList(const QString& listName, const QString& itemType, const QUuid& id); + /**jsdoc - * Remove all items from a selection. - * @function Selection.clearSelectedItemsList - * @param {string} listName - The name of the selection list. - * @returns {boolean} true if the item was successfully cleared, otherwise false. - */ + * Removes all items from a selection list. + * @function Selection.clearSelectedItemsList + * @param {string} listName - The name of the selection list. + * @returns {boolean} true always. + */ Q_INVOKABLE bool clearSelectedItemsList(const QString& listName); /**jsdoc - * Print out the list of avatars and entities in a selection to the debug log (not the script log). - * @function Selection.printList - * @param {string} listName - The name of the selection list. - */ + * Prints the list of avatars and entities in a selection to the program log (but not the Script Log window). + * @function Selection.printList + * @param {string} listName - The name of the selection list. + */ Q_INVOKABLE void printList(const QString& listName); /**jsdoc - * Get the list of avatars and entities stored in a selection list. - * @function Selection.getSelectedItemsList - * @param {string} listName - The name of the selection list. - * @returns {Selection.SelectedItemsList} The content of a selection list. If the list name doesn't exist, the function - * returns an empty object with no properties. - */ + * Gets the list of avatars and entities in a selection list. + * @function Selection.getSelectedItemsList + * @param {string} listName - The name of the selection list. + * @returns {Selection.SelectedItemsList} The content of the selection list if the list exists, otherwise an empty object. + */ Q_INVOKABLE QVariantMap getSelectedItemsList(const QString& listName) const; /**jsdoc - * Get the names of the highlighted selection lists. - * @function Selection.getHighlightedListNames - * @returns {string[]} An array of names of the selection list currently highlight enabled. - */ + * Gets the names of all current selection lists that have highlighting enabled. + * @function Selection.getHighlightedListNames + * @returns {string[]} The names of the selection lists that currently have highlighting enabled. + */ Q_INVOKABLE QStringList getHighlightedListNames() const; /**jsdoc - * Enable highlighting for a selection list. - * If the selection list doesn't exist, it will be created. - * All objects in the list will be displayed with the highlight effect specified. - * The function can be called several times with different values in the style to modify it.
- * Note: This function implicitly calls {@link Selection.enableListToScene}. - * @function Selection.enableListHighlight - * @param {string} listName - The name of the selection list. - * @param {Selection.HighlightStyle} highlightStyle - The highlight style. - * @returns {boolean} true if the selection was successfully enabled for highlight. - */ + * Enables highlighting for a selection list. All items in or subsequently added to the list are displayed with the + * highlight effect specified. The method can be called multiple times with different values in the style to modify the + * highlighting. + *

Note: This function implicitly calls {@link Selection.enableListToScene|enableListToScene}.

+ * @function Selection.enableListHighlight + * @param {string} listName - The name of the selection list. + * @param {Selection.HighlightStyle} highlightStyle - The highlight style. + * @returns {boolean} true always. + */ Q_INVOKABLE bool enableListHighlight(const QString& listName, const QVariantMap& highlightStyle); /**jsdoc - * Disable highlighting for the selection list. - * If the selection list doesn't exist or wasn't enabled for highlighting then nothing happens and false is - * returned.
- * Note: This function implicitly calls {@link Selection.disableListToScene}. - * @function Selection.disableListHighlight - * @param {string} listName - The name of the selection list. - * @returns {boolean} true if the selection was successfully disabled for highlight, otherwise - * false. - */ + * Disables highlighting for a selection list. + *

Note: This function implicitly calls {@link Selection.disableListToScene|disableListToScene}.

+ * @function Selection.disableListHighlight + * @param {string} listName - The name of the selection list. + * @returns {boolean} true always. + */ Q_INVOKABLE bool disableListHighlight(const QString& listName); + /**jsdoc - * Enable scene selection for the selection list. - * If the Selection doesn't exist, it will be created. - * All objects in the list will be sent to a scene selection. - * @function Selection.enableListToScene - * @param {string} listName - The name of the selection list. - * @returns {boolean} true if the selection was successfully enabled on the scene, otherwise false. - */ + * Enables scene selection for a selection list. All items in or subsequently added to the list are sent to a scene + * selection in the rendering engine for debugging purposes. + * @function Selection.enableListToScene + * @param {string} listName - The name of the selection list. + * @returns {boolean} true always. + */ Q_INVOKABLE bool enableListToScene(const QString& listName); /**jsdoc - * Disable scene selection for the named selection. - * If the selection list doesn't exist or wasn't enabled on the scene then nothing happens and false is - * returned. - * @function Selection.disableListToScene - * @param {string} listName - The name of the selection list. - * @returns {boolean} true if the selection was successfully disabled on the scene, false otherwise. - */ + * Disables scene selection for a selection list. + * @function Selection.disableListToScene + * @param {string} listName - The name of the selection list. + * @returns {boolean} true always. + */ Q_INVOKABLE bool disableListToScene(const QString& listName); /**jsdoc - * Get the highlight style values for the a selection list. - * If the selection doesn't exist or hasn't been highlight enabled yet, an empty object is returned. - * @function Selection.getListHighlightStyle - * @param {string} listName - The name of the selection list. - * @returns {Selection.HighlightStyle} highlight style - */ + * Gets the current highlighting style for a selection list. + * @function Selection.getListHighlightStyle + * @param {string} listName - The name of the selection list. + * @returns {Selection.HighlightStyle} The highlight style of the selection list if the list exists and highlighting is + * enabled, otherwise an empty object. + */ Q_INVOKABLE QVariantMap getListHighlightStyle(const QString& listName) const; @@ -253,7 +249,7 @@ public: signals: /**jsdoc - * Triggered when a list's content changes. + * Triggered when a selection list's content changes or the list is deleted. * @function Selection.selectedItemsListChanged * @param {string} listName - The name of the selection list that changed. * @returns {Signal} @@ -276,7 +272,6 @@ private: void setupHandler(const QString& selectionName); void removeHandler(const QString& selectionName); - }; #endif // hifi_SelectionScriptingInterface_h diff --git a/interface/src/scripting/SettingsScriptingInterface.h b/interface/src/scripting/SettingsScriptingInterface.h index e907e550f3..25a8b627cb 100644 --- a/interface/src/scripting/SettingsScriptingInterface.h +++ b/interface/src/scripting/SettingsScriptingInterface.h @@ -16,7 +16,8 @@ #include /**jsdoc - * The Settings API provides a facility to store and retrieve values that persist between Interface runs. + * The Settings API provides a facility to store and retrieve values that persist between Interface runs. + * * @namespace Settings * * @hifi-interface @@ -33,7 +34,7 @@ public: public slots: /**jsdoc - * Retrieve the value from a named setting. + * Retrieves the value from a named setting. * @function Settings.getValue * @param {string} key - The name of the setting. * @param {string|number|boolean|object} [defaultValue=""] - The value to return if the setting doesn't exist. @@ -50,8 +51,8 @@ public slots: QVariant getValue(const QString& setting, const QVariant& defaultValue); /**jsdoc - * Store a value in a named setting. If the setting already exists its value is overwritten, otherwise a new setting is - * created. If the value is set to null or undefined, the setting is deleted. + * Stores a value in a named setting. If the setting already exists, its value is overwritten. If the value is + * null or undefined, the setting is deleted. * @function Settings.setValue * @param {string} key - The name of the setting. Be sure to use a unique name if creating a new setting. * @param {string|number|boolean|object|undefined} value - The value to store in the setting. If null or diff --git a/interface/src/scripting/WindowScriptingInterface.h b/interface/src/scripting/WindowScriptingInterface.h index de2441ae62..0c2b494b0b 100644 --- a/interface/src/scripting/WindowScriptingInterface.h +++ b/interface/src/scripting/WindowScriptingInterface.h @@ -313,8 +313,9 @@ public slots: * Takes a snapshot of the current Interface view from the primary camera. When a still image only is captured, * {@link Window.stillSnapshotTaken|stillSnapshotTaken} is emitted; when a still image plus moving images are captured, * {@link Window.processingGifStarted|processingGifStarted} and {@link Window.processingGifCompleted|processingGifCompleted} - * are emitted. The path to store the snapshots and the length of the animated GIF to capture are specified in Settings > - * General > Snapshots. + * are emitted. + *

Snapshots are saved to the path specified in Settings > General > Snapshots, which can be accessed via the + * {@link Snapshot} API.

* * @function Window.takeSnapshot * @param {boolean} [notify=true] - This value is passed on through the {@link Window.stillSnapshotTaken|stillSnapshotTaken} @@ -351,13 +352,15 @@ public slots: * var notify = true; * var animated = true; * var aspect = 1920 / 1080; - * var filename = ""; + * var filename = "example-snapshot"; * Window.takeSnapshot(notify, animated, aspect, filename); */ void takeSnapshot(bool notify = true, bool includeAnimated = false, float aspectRatio = 0.0f, const QString& filename = QString()); /**jsdoc * Takes a still snapshot of the current view from the secondary camera that can be set up through the {@link Render} API. + *

Snapshots are saved to the path specified in Settings > General > Snapshots, which can be accessed via the + * {@link Snapshot} API.

* @function Window.takeSecondaryCameraSnapshot * @param {boolean} [notify=true] - This value is passed on through the {@link Window.stillSnapshotTaken|stillSnapshotTaken} * signal. @@ -372,6 +375,8 @@ public slots: /**jsdoc * Takes a 360° snapshot at a given position for the secondary camera. The secondary camera does not need to have been * set up. + *

Snapshots are saved to the path specified in Settings > General > Snapshots, which can be accessed via the + * {@link Snapshot} API.

* @function Window.takeSecondaryCamera360Snapshot * @param {Vec3} cameraPosition - The position of the camera for the snapshot. * @param {boolean} [cubemapOutputFormat=false] - If true then the snapshot is saved as a cube map image, diff --git a/interface/src/ui/InteractiveWindow.cpp b/interface/src/ui/InteractiveWindow.cpp index 657b6b3ac5..7e8c176424 100644 --- a/interface/src/ui/InteractiveWindow.cpp +++ b/interface/src/ui/InteractiveWindow.cpp @@ -109,7 +109,11 @@ InteractiveWindow::InteractiveWindow(const QString& sourceUrl, const QVariantMap auto mainWindow = qApp->getWindow(); _dockWidget = std::shared_ptr(new DockWidget(title, mainWindow), dockWidgetDeleter); auto quickView = _dockWidget->getQuickView(); - Application::setupQmlSurface(quickView->rootContext(), true); + + Application::setupQmlSurface(quickView->rootContext() , true); + + //add any whitelisted callbacks + OffscreenUi::applyWhiteList(sourceUrl, quickView->rootContext()); /**jsdoc * Configures how a NATIVE window is displayed. @@ -150,6 +154,8 @@ InteractiveWindow::InteractiveWindow(const QString& sourceUrl, const QVariantMap } }); _dockWidget->setSource(QUrl(sourceUrl)); + + mainWindow->addDockWidget(dockArea, _dockWidget.get()); } else { auto offscreenUi = DependencyManager::get(); diff --git a/interface/src/ui/Keyboard.h b/interface/src/ui/Keyboard.h index 2b6829bf2b..7f7944e646 100644 --- a/interface/src/ui/Keyboard.h +++ b/interface/src/ui/Keyboard.h @@ -180,11 +180,7 @@ private: mutable ReadWriteLockable _preferMalletsOverLasersSettingLock; mutable ReadWriteLockable _ignoreItemsLock; -#ifdef Q_OS_ANDROID - Setting::Handle _use3DKeyboard { "use3DKeyboard", false }; -#else Setting::Handle _use3DKeyboard { "use3DKeyboard", true }; -#endif QString _typedCharacters; TextDisplay _textDisplay; diff --git a/interface/src/ui/Snapshot.h b/interface/src/ui/Snapshot.h index f13f4cd587..73c5a8e3c5 100644 --- a/interface/src/ui/Snapshot.h +++ b/interface/src/ui/Snapshot.h @@ -38,6 +38,10 @@ private: /**jsdoc + * The Snapshot API provides access to the path that snapshots are saved to. This path is that provided in + * Settings > General > Snapshots. Snapshots may be taken using Window API functions such as + * {@link Window.takeSnapshot}. + * * @namespace Snapshot * * @hifi-interface @@ -64,23 +68,31 @@ public: signals: /**jsdoc + * Triggered when the path that snapshots are saved to is changed. * @function Snapshot.snapshotLocationSet - * @param {string} location + * @param {string} location - The new snapshots location. * @returns {Signal} + * @example Report when the snapshots location is changed. + * // Run this script then change the snapshots location in Settings > General > Snapshots. + * Snapshot.snapshotLocationSet.connect(function (path) { + * print("New snapshot location: " + path); + * }); */ void snapshotLocationSet(const QString& value); public slots: /**jsdoc + * Gets the path that snapshots are saved to. * @function Snapshot.getSnapshotsLocation - * @returns {string} + * @returns {string} The path to save snapshots to. */ Q_INVOKABLE QString getSnapshotsLocation(); /**jsdoc + * Sets the path that snapshots are saved to. * @function Snapshot.setSnapshotsLocation - * @param {String} location + * @param {String} location - The path to save snapshots to. */ Q_INVOKABLE void setSnapshotsLocation(const QString& location); diff --git a/interface/src/ui/Stats.h b/interface/src/ui/Stats.h index 1939e7fa0e..cb13945320 100644 --- a/interface/src/ui/Stats.h +++ b/interface/src/ui/Stats.h @@ -314,10 +314,11 @@ class Stats : public QQuickItem { STATS_PROPERTY(QVector3D, parabolaPicksUpdated, QVector3D(0, 0, 0)) STATS_PROPERTY(QVector3D, collisionPicksUpdated, QVector3D(0, 0, 0)) -#ifdef DEBUG_EVENT_QUEUE - STATS_PROPERTY(bool, eventQueueDebuggingOn, true) STATS_PROPERTY(int, mainThreadQueueDepth, -1); STATS_PROPERTY(int, nodeListThreadQueueDepth, -1); + +#ifdef DEBUG_EVENT_QUEUE + STATS_PROPERTY(bool, eventQueueDebuggingOn, true) #else STATS_PROPERTY(bool, eventQueueDebuggingOn, false) #endif // DEBUG_EVENT_QUEUE diff --git a/libraries/audio-client/src/AudioClient.cpp b/libraries/audio-client/src/AudioClient.cpp index cef6adcab0..ef0e70a31d 100644 --- a/libraries/audio-client/src/AudioClient.cpp +++ b/libraries/audio-client/src/AudioClient.cpp @@ -1869,6 +1869,7 @@ bool AudioClient::switchOutputToAudioDevice(const QAudioDeviceInfo outputDeviceI if (_audioOutput) { _audioOutputIODevice.close(); _audioOutput->stop(); + _audioOutputInitialized = false; //must be deleted in next eventloop cycle when its called from notify() _audioOutput->deleteLater(); @@ -1939,52 +1940,50 @@ bool AudioClient::switchOutputToAudioDevice(const QAudioDeviceInfo outputDeviceI int requestedSize = _sessionOutputBufferSizeFrames * frameSize * AudioConstants::SAMPLE_SIZE; _audioOutput->setBufferSize(requestedSize); - // initialize mix buffers on the _audioOutput thread to avoid races - connect(_audioOutput, &QAudioOutput::stateChanged, [&, frameSize, requestedSize](QAudio::State state) { - if (state == QAudio::ActiveState) { - // restrict device callback to _outputPeriod samples - _outputPeriod = _audioOutput->periodSize() / AudioConstants::SAMPLE_SIZE; - // device callback may exceed reported period, so double it to avoid stutter - _outputPeriod *= 2; - - _outputMixBuffer = new float[_outputPeriod]; - _outputScratchBuffer = new int16_t[_outputPeriod]; - - // size local output mix buffer based on resampled network frame size - int networkPeriod = _localToOutputResampler ? _localToOutputResampler->getMaxOutput(AudioConstants::NETWORK_FRAME_SAMPLES_STEREO) : AudioConstants::NETWORK_FRAME_SAMPLES_STEREO; - _localOutputMixBuffer = new float[networkPeriod]; - - // local period should be at least twice the output period, - // in case two device reads happen before more data can be read (worst case) - int localPeriod = _outputPeriod * 2; - // round up to an exact multiple of networkPeriod - localPeriod = ((localPeriod + networkPeriod - 1) / networkPeriod) * networkPeriod; - // this ensures lowest latency without stutter from underrun - _localInjectorsStream.resizeForFrameSize(localPeriod); - - int bufferSize = _audioOutput->bufferSize(); - int bufferSamples = bufferSize / AudioConstants::SAMPLE_SIZE; - int bufferFrames = bufferSamples / (float)frameSize; - qCDebug(audioclient) << "frame (samples):" << frameSize; - qCDebug(audioclient) << "buffer (frames):" << bufferFrames; - qCDebug(audioclient) << "buffer (samples):" << bufferSamples; - qCDebug(audioclient) << "buffer (bytes):" << bufferSize; - qCDebug(audioclient) << "requested (bytes):" << requestedSize; - qCDebug(audioclient) << "period (samples):" << _outputPeriod; - qCDebug(audioclient) << "local buffer (samples):" << localPeriod; - - disconnect(_audioOutput, &QAudioOutput::stateChanged, 0, 0); - - // unlock to avoid a deadlock with the device callback (which always succeeds this initialization) - localAudioLock.unlock(); - } - }); connect(_audioOutput, &QAudioOutput::notify, this, &AudioClient::outputNotify); + // start the output device _audioOutputIODevice.start(); - _audioOutput->start(&_audioOutputIODevice); + // initialize mix buffers + + // restrict device callback to _outputPeriod samples + _outputPeriod = _audioOutput->periodSize() / AudioConstants::SAMPLE_SIZE; + // device callback may exceed reported period, so double it to avoid stutter + _outputPeriod *= 2; + + _outputMixBuffer = new float[_outputPeriod]; + _outputScratchBuffer = new int16_t[_outputPeriod]; + + // size local output mix buffer based on resampled network frame size + int networkPeriod = _localToOutputResampler ? _localToOutputResampler->getMaxOutput(AudioConstants::NETWORK_FRAME_SAMPLES_STEREO) : AudioConstants::NETWORK_FRAME_SAMPLES_STEREO; + _localOutputMixBuffer = new float[networkPeriod]; + + // local period should be at least twice the output period, + // in case two device reads happen before more data can be read (worst case) + int localPeriod = _outputPeriod * 2; + // round up to an exact multiple of networkPeriod + localPeriod = ((localPeriod + networkPeriod - 1) / networkPeriod) * networkPeriod; + // this ensures lowest latency without stutter from underrun + _localInjectorsStream.resizeForFrameSize(localPeriod); + + _audioOutputInitialized = true; + + int bufferSize = _audioOutput->bufferSize(); + int bufferSamples = bufferSize / AudioConstants::SAMPLE_SIZE; + int bufferFrames = bufferSamples / (float)frameSize; + qCDebug(audioclient) << "frame (samples):" << frameSize; + qCDebug(audioclient) << "buffer (frames):" << bufferFrames; + qCDebug(audioclient) << "buffer (samples):" << bufferSamples; + qCDebug(audioclient) << "buffer (bytes):" << bufferSize; + qCDebug(audioclient) << "requested (bytes):" << requestedSize; + qCDebug(audioclient) << "period (samples):" << _outputPeriod; + qCDebug(audioclient) << "local buffer (samples):" << localPeriod; + + // unlock to avoid a deadlock with the device callback (which always succeeds this initialization) + localAudioLock.unlock(); + // setup a loopback audio output device _loopbackAudioOutput = new QAudioOutput(outputDeviceInfo, _outputFormat, this); @@ -2082,6 +2081,12 @@ float AudioClient::gainForSource(float distance, float volume) { qint64 AudioClient::AudioOutputIODevice::readData(char * data, qint64 maxSize) { + // lock-free wait for initialization to avoid races + if (!_audio->_audioOutputInitialized.load(std::memory_order_acquire)) { + memset(data, 0, maxSize); + return maxSize; + } + // samples requested from OUTPUT_CHANNEL_COUNT int deviceChannelCount = _audio->_outputFormat.channelCount(); int samplesRequested = (int)(maxSize / AudioConstants::SAMPLE_SIZE) * OUTPUT_CHANNEL_COUNT / deviceChannelCount; @@ -2162,6 +2167,8 @@ qint64 AudioClient::AudioOutputIODevice::readData(char * data, qint64 maxSize) { } bytesWritten = framesPopped * AudioConstants::SAMPLE_SIZE * deviceChannelCount; + assert(bytesWritten <= maxSize); + } else { // nothing on network, don't grab anything from injectors, and just return 0s memset(data, 0, maxSize); @@ -2174,7 +2181,6 @@ qint64 AudioClient::AudioOutputIODevice::readData(char * data, qint64 maxSize) { _audio->_audioFileWav.addRawAudioChunk(reinterpret_cast(scratchBuffer), bytesWritten); } - int bytesAudioOutputUnplayed = _audio->_audioOutput->bufferSize() - _audio->_audioOutput->bytesFree(); float msecsAudioOutputUnplayed = bytesAudioOutputUnplayed / (float)_audio->_outputFormat.bytesForDuration(USECS_PER_MSEC); _audio->_stats.updateOutputMsUnplayed(msecsAudioOutputUnplayed); diff --git a/libraries/audio-client/src/AudioClient.h b/libraries/audio-client/src/AudioClient.h index db70e2f7b3..e209628689 100644 --- a/libraries/audio-client/src/AudioClient.h +++ b/libraries/audio-client/src/AudioClient.h @@ -340,6 +340,7 @@ private: QIODevice* _inputDevice; int _numInputCallbackBytes; QAudioOutput* _audioOutput; + std::atomic _audioOutputInitialized { false }; QAudioFormat _desiredOutputFormat; QAudioFormat _outputFormat; int _outputFrameSize; diff --git a/libraries/audio/src/InboundAudioStream.cpp b/libraries/audio/src/InboundAudioStream.cpp index 5ac3996029..7a81b8a67a 100644 --- a/libraries/audio/src/InboundAudioStream.cpp +++ b/libraries/audio/src/InboundAudioStream.cpp @@ -364,10 +364,19 @@ int InboundAudioStream::popSamples(int maxSamples, bool allOrNothing) { // buffer calculations. setToStarved(); _consecutiveNotMixedCount++; - //Kick PLC to generate a filler frame, reducing 'click' - lostAudioData(allOrNothing ? (maxSamples - samplesAvailable) / _ringBuffer.getNumFrameSamples() : 1); - samplesPopped = _ringBuffer.samplesAvailable(); - if (samplesPopped) { + + // use PLC to generate extrapolated audio data, to reduce clicking + if (allOrNothing) { + int samplesNeeded = maxSamples - samplesAvailable; + int packetsNeeded = (samplesNeeded + _ringBuffer.getNumFrameSamples() - 1) / _ringBuffer.getNumFrameSamples(); + lostAudioData(packetsNeeded); + } else { + lostAudioData(1); + } + samplesAvailable = _ringBuffer.samplesAvailable(); + + if (samplesAvailable > 0) { + samplesPopped = std::min(samplesAvailable, maxSamples); popSamplesNoCheck(samplesPopped); } else { // No samples available means a packet is currently being diff --git a/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp b/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp index e836ecf7eb..78481fdc2e 100644 --- a/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp +++ b/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp @@ -1519,18 +1519,19 @@ void Avatar::scaleVectorRelativeToPosition(glm::vec3 &positionToScale) const { } void Avatar::setSkeletonModelURL(const QUrl& skeletonModelURL) { - AvatarData::setSkeletonModelURL(skeletonModelURL); - if (QThread::currentThread() == thread()) { - - if (!isMyAvatar() && !DependencyManager::get()->isIgnoringNode(getSessionUUID())) { - createOrb(); - } - - _skeletonModel->setURL(_skeletonModelURL); - indicateLoadingStatus(LoadingStatus::LoadModel); - } else { - QMetaObject::invokeMethod(_skeletonModel.get(), "setURL", Qt::QueuedConnection, Q_ARG(QUrl, _skeletonModelURL)); + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(this, "setSkeletonModelURL", Q_ARG(const QUrl&, skeletonModelURL)); + return; } + + AvatarData::setSkeletonModelURL(skeletonModelURL); + + if (!isMyAvatar() && !DependencyManager::get()->isIgnoringNode(getSessionUUID())) { + createOrb(); + } + indicateLoadingStatus(LoadingStatus::LoadModel); + + _skeletonModel->setURL(_skeletonModelURL); } void Avatar::setModelURLFinished(bool success) { diff --git a/libraries/avatars-renderer/src/avatars-renderer/Avatar.h b/libraries/avatars-renderer/src/avatars-renderer/Avatar.h index 61901d662a..adafca1d1f 100644 --- a/libraries/avatars-renderer/src/avatars-renderer/Avatar.h +++ b/libraries/avatars-renderer/src/avatars-renderer/Avatar.h @@ -339,7 +339,7 @@ public: */ Q_INVOKABLE glm::quat jointToWorldRotation(const glm::quat& rotation, const int jointIndex = -1) const; - virtual void setSkeletonModelURL(const QUrl& skeletonModelURL) override; + Q_INVOKABLE virtual void setSkeletonModelURL(const QUrl& skeletonModelURL) override; virtual void setAttachmentData(const QVector& attachmentData) override; void updateDisplayNameAlpha(bool showDisplayName); diff --git a/libraries/avatars/src/AvatarData.h b/libraries/avatars/src/AvatarData.h index 9219c2c03f..e5131ff94b 100755 --- a/libraries/avatars/src/AvatarData.h +++ b/libraries/avatars/src/AvatarData.h @@ -1211,7 +1211,7 @@ public: const QString& getDisplayName() const { return _displayName; } const QString& getSessionDisplayName() const { return _sessionDisplayName; } bool getLookAtSnappingEnabled() const { return _lookAtSnappingEnabled; } - virtual void setSkeletonModelURL(const QUrl& skeletonModelURL); + Q_INVOKABLE virtual void setSkeletonModelURL(const QUrl& skeletonModelURL); virtual void setDisplayName(const QString& displayName); virtual void setSessionDisplayName(const QString& sessionDisplayName) { diff --git a/libraries/display-plugins/src/display-plugins/AbstractHMDScriptingInterface.h b/libraries/display-plugins/src/display-plugins/AbstractHMDScriptingInterface.h index c1253f825f..de6141c9d8 100644 --- a/libraries/display-plugins/src/display-plugins/AbstractHMDScriptingInterface.h +++ b/libraries/display-plugins/src/display-plugins/AbstractHMDScriptingInterface.h @@ -15,7 +15,7 @@ // These properties have JSDoc documentation in HMDScriptingInterface.h. class AbstractHMDScriptingInterface : public QObject { Q_OBJECT - Q_PROPERTY(bool active READ isHMDMode NOTIFY mountedChanged) + Q_PROPERTY(bool active READ isHMDMode NOTIFY displayModeChanged) Q_PROPERTY(float ipd READ getIPD) Q_PROPERTY(float eyeHeight READ getEyeHeight) Q_PROPERTY(float playerHeight READ getPlayerHeight) diff --git a/libraries/entities-renderer/src/RenderableEntityItem.cpp b/libraries/entities-renderer/src/RenderableEntityItem.cpp index fb6fbad2ac..01d31856e0 100644 --- a/libraries/entities-renderer/src/RenderableEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableEntityItem.cpp @@ -32,19 +32,6 @@ using namespace render; using namespace render::entities; -// These or the icon "name" used by the render item status value, they correspond to the atlas texture used by the DrawItemStatus -// job in the current rendering pipeline defined as of now (11/2015) in render-utils/RenderDeferredTask.cpp. -enum class RenderItemStatusIcon { - ACTIVE_IN_BULLET = 0, - PACKET_SENT = 1, - PACKET_RECEIVED = 2, - SIMULATION_OWNER = 3, - HAS_ACTIONS = 4, - OTHER_SIMULATION_OWNER = 5, - ENTITY_HOST_TYPE = 6, - NONE = 255 -}; - void EntityRenderer::initEntityRenderers() { REGISTER_ENTITY_TYPE_WITH_FACTORY(Model, RenderableModelEntityItem::factory) REGISTER_ENTITY_TYPE_WITH_FACTORY(PolyVox, RenderablePolyVoxEntityItem::factory) @@ -67,7 +54,7 @@ void EntityRenderer::makeStatusGetters(const EntityItemPointer& entity, Item::St return render::Item::Status::Value(1.0f - normalizedDelta, (normalizedDelta > 1.0f ? render::Item::Status::Value::GREEN : render::Item::Status::Value::RED), - (unsigned char)RenderItemStatusIcon::PACKET_RECEIVED); + (unsigned char)render::Item::Status::Icon::PACKET_RECEIVED); }); statusGetters.push_back([entity] () -> render::Item::Status::Value { @@ -79,17 +66,17 @@ void EntityRenderer::makeStatusGetters(const EntityItemPointer& entity, Item::St return render::Item::Status::Value(1.0f - normalizedDelta, (normalizedDelta > 1.0f ? render::Item::Status::Value::MAGENTA : render::Item::Status::Value::CYAN), - (unsigned char)RenderItemStatusIcon::PACKET_SENT); + (unsigned char)render::Item::Status::Icon::PACKET_SENT); }); statusGetters.push_back([entity] () -> render::Item::Status::Value { ObjectMotionState* motionState = static_cast(entity->getPhysicsInfo()); if (motionState && motionState->isActive()) { return render::Item::Status::Value(1.0f, render::Item::Status::Value::BLUE, - (unsigned char)RenderItemStatusIcon::ACTIVE_IN_BULLET); + (unsigned char)render::Item::Status::Icon::ACTIVE_IN_BULLET); } return render::Item::Status::Value(0.0f, render::Item::Status::Value::BLUE, - (unsigned char)RenderItemStatusIcon::ACTIVE_IN_BULLET); + (unsigned char)render::Item::Status::Icon::ACTIVE_IN_BULLET); }); statusGetters.push_back([entity, myNodeID] () -> render::Item::Status::Value { @@ -98,39 +85,39 @@ void EntityRenderer::makeStatusGetters(const EntityItemPointer& entity, Item::St if (weOwnSimulation) { return render::Item::Status::Value(1.0f, render::Item::Status::Value::BLUE, - (unsigned char)RenderItemStatusIcon::SIMULATION_OWNER); + (unsigned char)render::Item::Status::Icon::SIMULATION_OWNER); } else if (otherOwnSimulation) { return render::Item::Status::Value(1.0f, render::Item::Status::Value::RED, - (unsigned char)RenderItemStatusIcon::OTHER_SIMULATION_OWNER); + (unsigned char)render::Item::Status::Icon::OTHER_SIMULATION_OWNER); } return render::Item::Status::Value(0.0f, render::Item::Status::Value::BLUE, - (unsigned char)RenderItemStatusIcon::SIMULATION_OWNER); + (unsigned char)render::Item::Status::Icon::SIMULATION_OWNER); }); statusGetters.push_back([entity] () -> render::Item::Status::Value { if (entity->hasActions()) { return render::Item::Status::Value(1.0f, render::Item::Status::Value::GREEN, - (unsigned char)RenderItemStatusIcon::HAS_ACTIONS); + (unsigned char)render::Item::Status::Icon::HAS_ACTIONS); } return render::Item::Status::Value(0.0f, render::Item::Status::Value::GREEN, - (unsigned char)RenderItemStatusIcon::HAS_ACTIONS); + (unsigned char)render::Item::Status::Icon::HAS_ACTIONS); }); statusGetters.push_back([entity, myNodeID] () -> render::Item::Status::Value { if (entity->isAvatarEntity()) { if (entity->getOwningAvatarID() == myNodeID) { return render::Item::Status::Value(1.0f, render::Item::Status::Value::GREEN, - (unsigned char)RenderItemStatusIcon::ENTITY_HOST_TYPE); + (unsigned char)render::Item::Status::Icon::ENTITY_HOST_TYPE); } else { return render::Item::Status::Value(1.0f, render::Item::Status::Value::RED, - (unsigned char)RenderItemStatusIcon::ENTITY_HOST_TYPE); + (unsigned char)render::Item::Status::Icon::ENTITY_HOST_TYPE); } } else if (entity->isLocalEntity()) { return render::Item::Status::Value(1.0f, render::Item::Status::Value::BLUE, - (unsigned char)RenderItemStatusIcon::ENTITY_HOST_TYPE); + (unsigned char)render::Item::Status::Icon::ENTITY_HOST_TYPE); } return render::Item::Status::Value(0.0f, render::Item::Status::Value::GREEN, - (unsigned char)RenderItemStatusIcon::ENTITY_HOST_TYPE); + (unsigned char)render::Item::Status::Icon::ENTITY_HOST_TYPE); }); } diff --git a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp index bf0abe92dd..cc6ba0176b 100644 --- a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp @@ -363,7 +363,7 @@ void RenderableModelEntityItem::computeShapeInfo(ShapeInfo& shapeInfo) { ShapeType type = getShapeType(); auto model = getModel(); - if (!model) { + if (!model || !model->isLoaded()) { type = SHAPE_TYPE_NONE; } diff --git a/libraries/entities/src/EntityScriptingInterface.h b/libraries/entities/src/EntityScriptingInterface.h index c072dedaf9..39700bfc31 100644 --- a/libraries/entities/src/EntityScriptingInterface.h +++ b/libraries/entities/src/EntityScriptingInterface.h @@ -1770,6 +1770,7 @@ signals: /**jsdoc * Triggered on the client that is the physics simulation owner during the collision of two entities. Note: Isn't triggered * for a collision with an avatar. + *

See also, {@link Script.addEventHandler}.

* @function Entities.collisionWithEntity * @param {Uuid} idA - The ID of one entity in the collision. For an entity script, this is the ID of the entity containing * the script. @@ -1882,6 +1883,7 @@ signals: /**jsdoc * Triggered when a mouse button is clicked while the mouse cursor is on an entity, or a controller trigger is fully * pressed while its laser is on an entity. + *

See also, {@link Script.addEventHandler}.

* @function Entities.mousePressOnEntity * @param {Uuid} entityID - The ID of the entity that was pressed. * @param {PointerEvent} event - Details of the event. @@ -1906,6 +1908,7 @@ signals: /**jsdoc * Repeatedly triggered while the mouse cursor or controller laser moves on an entity. + *

See also, {@link Script.addEventHandler}.

* @function Entities.mouseMoveOnEntity * @param {Uuid} entityID - The ID of the entity that was moved on. * @param {PointerEvent} event - Details of the event. @@ -1916,6 +1919,7 @@ signals: /**jsdoc * Triggered when a mouse button is released after clicking on an entity or the controller trigger is partly or fully * released after pressing on an entity, even if the mouse pointer or controller laser has moved off the entity. + *

See also, {@link Script.addEventHandler}.

* @function Entities.mouseReleaseOnEntity * @param {Uuid} entityID - The ID of the entity that was originally pressed. * @param {PointerEvent} event - Details of the event. @@ -1942,6 +1946,7 @@ signals: /**jsdoc * Triggered when a mouse button is clicked while the mouse cursor is on an entity. Note: Not triggered by controller. + *

See also, {@link Script.addEventHandler}.

* @function Entities.clickDownOnEntity * @param {Uuid} entityID - The ID of the entity that was clicked. * @param {PointerEvent} event - Details of the event. @@ -1952,6 +1957,7 @@ signals: /**jsdoc * Repeatedly triggered while a mouse button continues to be held after clicking an entity, even if the mouse cursor has * moved off the entity. Note: Not triggered by controller. + *

See also, {@link Script.addEventHandler}.

* @function Entities.holdingClickOnEntity * @param {Uuid} entityID - The ID of the entity that was originally clicked. * @param {PointerEvent} event - Details of the event. @@ -1962,6 +1968,7 @@ signals: /**jsdoc * Triggered when a mouse button is released after clicking on an entity, even if the mouse cursor has moved off the * entity. Note: Not triggered by controller. + *

See also, {@link Script.addEventHandler}.

* @function Entities.clickReleaseOnEntity * @param {Uuid} entityID - The ID of the entity that was originally clicked. * @param {PointerEvent} event - Details of the event. @@ -1971,6 +1978,7 @@ signals: /**jsdoc * Triggered when the mouse cursor or controller laser starts hovering on an entity. + *

See also, {@link Script.addEventHandler}.

* @function Entities.hoverEnterEntity * @param {Uuid} entityID - The ID of the entity that is being hovered. * @param {PointerEvent} event - Details of the event. @@ -1980,6 +1988,7 @@ signals: /**jsdoc * Repeatedly triggered when the mouse cursor or controller laser moves while hovering over an entity. + *

See also, {@link Script.addEventHandler}.

* @function Entities.hoverOverEntity * @param {Uuid} entityID - The ID of the entity that is being hovered. * @param {PointerEvent} event - Details of the event. @@ -1989,6 +1998,7 @@ signals: /**jsdoc * Triggered when the mouse cursor or controller laser stops hovering over an entity. + *

See also, {@link Script.addEventHandler}.

* @function Entities.hoverLeaveEntity * @param {Uuid} entityID - The ID of the entity that was being hovered. * @param {PointerEvent} event - Details of the event. @@ -1999,6 +2009,7 @@ signals: /**jsdoc * Triggered when an avatar enters an entity. + *

See also, {@link Script.addEventHandler}.

* @function Entities.enterEntity * @param {Uuid} entityID - The ID of the entity that the avatar entered. * @returns {Signal} @@ -2032,6 +2043,7 @@ signals: /**jsdoc * Triggered when an avatar leaves an entity. + *

See also, {@link Script.addEventHandler}.

* @function Entities.leaveEntity * @param {Uuid} entityID - The ID of the entity that the avatar left. * @returns {Signal} diff --git a/libraries/gpu-gl/src/gpu/gl45/GL45BackendTexture.cpp b/libraries/gpu-gl/src/gpu/gl45/GL45BackendTexture.cpp index 4aff76df21..81a6b100d0 100644 --- a/libraries/gpu-gl/src/gpu/gl45/GL45BackendTexture.cpp +++ b/libraries/gpu-gl/src/gpu/gl45/GL45BackendTexture.cpp @@ -220,6 +220,10 @@ void GL45Texture::generateMips() const { (void)CHECK_GL_ERROR(); } +// (NOTE: it seems to work now, but for posterity:) DSA ARB does not work on AMD, so use EXT +// unless EXT is not available on the driver +#define AMD_CUBE_MAP_EXT_WORKAROUND 0 + Size GL45Texture::copyMipFaceLinesFromTexture(uint16_t mip, uint8_t face, const uvec3& size, uint32_t yOffset, GLenum internalFormat, GLenum format, GLenum type, Size sourceSize, const void* sourcePointer) const { Size amountCopied = sourceSize; if (GL_TEXTURE_2D == _target) { @@ -267,22 +271,26 @@ Size GL45Texture::copyMipFaceLinesFromTexture(uint16_t mip, uint8_t face, const case GL_COMPRESSED_SIGNED_R11_EAC: case GL_COMPRESSED_RG11_EAC: case GL_COMPRESSED_SIGNED_RG11_EAC: +#if AMD_CUBE_MAP_EXT_WORKAROUND if (glCompressedTextureSubImage2DEXT) { auto target = GLTexture::CUBE_FACE_LAYOUT[face]; glCompressedTextureSubImage2DEXT(_id, target, mip, 0, yOffset, size.x, size.y, internalFormat, static_cast(sourceSize), sourcePointer); - } else { + } else +#endif + { glCompressedTextureSubImage3D(_id, mip, 0, yOffset, face, size.x, size.y, 1, internalFormat, static_cast(sourceSize), sourcePointer); } break; default: - // DSA ARB does not work on AMD, so use EXT - // unless EXT is not available on the driver +#if AMD_CUBE_MAP_EXT_WORKAROUND if (glTextureSubImage2DEXT) { auto target = GLTexture::CUBE_FACE_LAYOUT[face]; glTextureSubImage2DEXT(_id, target, mip, 0, yOffset, size.x, size.y, format, type, sourcePointer); - } else { + } else +#endif + { glTextureSubImage3D(_id, mip, 0, yOffset, face, size.x, size.y, 1, format, type, sourcePointer); } break; diff --git a/libraries/gpu/src/gpu/Format.cpp b/libraries/gpu/src/gpu/Format.cpp index 7e277ae488..013a5ac4a6 100644 --- a/libraries/gpu/src/gpu/Format.cpp +++ b/libraries/gpu/src/gpu/Format.cpp @@ -26,8 +26,8 @@ const Element Element::COLOR_COMPRESSED_BCX_SRGB { TILE4x4, COMPRESSED, COMPRESS const Element Element::COLOR_COMPRESSED_BCX_SRGBA_MASK { TILE4x4, COMPRESSED, COMPRESSED_BC1_SRGBA }; const Element Element::COLOR_COMPRESSED_BCX_SRGBA { TILE4x4, COMPRESSED, COMPRESSED_BC3_SRGBA }; const Element Element::COLOR_COMPRESSED_BCX_XY { TILE4x4, COMPRESSED, COMPRESSED_BC5_XY }; -const Element Element::COLOR_COMPRESSED_BCX_SRGBA_HIGH { TILE4x4, COMPRESSED, COMPRESSED_BC7_SRGBA }; const Element Element::COLOR_COMPRESSED_BCX_HDR_RGB { TILE4x4, COMPRESSED, COMPRESSED_BC6_RGB }; +const Element Element::COLOR_COMPRESSED_BCX_SRGBA_HIGH { TILE4x4, COMPRESSED, COMPRESSED_BC7_SRGBA }; const Element Element::COLOR_COMPRESSED_ETC2_RGB { TILE4x4, COMPRESSED, COMPRESSED_ETC2_RGB }; const Element Element::COLOR_COMPRESSED_ETC2_SRGB { TILE4x4, COMPRESSED, COMPRESSED_ETC2_SRGB }; diff --git a/libraries/gpu/src/gpu/FrameReader.cpp b/libraries/gpu/src/gpu/FrameReader.cpp index 812f4db7cc..740ec2b26f 100644 --- a/libraries/gpu/src/gpu/FrameReader.cpp +++ b/libraries/gpu/src/gpu/FrameReader.cpp @@ -18,7 +18,6 @@ #include "Batch.h" #include "TextureTable.h" - #include "FrameIOKeys.h" namespace gpu { @@ -324,6 +323,13 @@ TexturePointer Deserializer::readTexture(const json& node, uint32_t external) { readOptional(ktxFile, node, keys::ktxFile); Element ktxTexelFormat, ktxMipFormat; if (!ktxFile.empty()) { + // If we get a texture that starts with ":" we need to re-route it to the resources directory + if (ktxFile.at(0) == ':') { + QString frameReaderPath = __FILE__; + frameReaderPath.replace("\\", "/"); + frameReaderPath.replace("libraries/gpu/src/gpu/framereader.cpp", "interface/resources", Qt::CaseInsensitive); + ktxFile.replace(0, 1, frameReaderPath.toStdString()); + } if (QFileInfo(ktxFile.c_str()).isRelative()) { ktxFile = basedir + ktxFile; } diff --git a/libraries/graphics/src/graphics/skybox.slf b/libraries/graphics/src/graphics/skybox.slf index c20dd94bf4..801fc33c28 100755 --- a/libraries/graphics/src/graphics/skybox.slf +++ b/libraries/graphics/src/graphics/skybox.slf @@ -26,13 +26,14 @@ layout(location=0) in vec3 _normal; layout(location=0) out vec4 _fragColor; void main(void) { - vec3 coord = normalize(_normal); - vec3 color = skybox.color.rgb; + // FIXME: For legacy reasons, when skybox.color.a is 0.5, this is equivalent to: + // skyboxColor * skyboxTexel + // It should actually be: + // mix(skyboxColor, skyboxTexel, skybox.color.a) + // and the blend factor should be user controlled - // blend is only set if there is a cubemap - float check = float(skybox.color.a > 0.0); - color = mix(color, texture(cubeMap, coord).rgb, check); - color *= mix(vec3(1.0), skybox.color.rgb, check * float(skybox.color.a < 1.0)); - - _fragColor = vec4(color, 0.0); + vec3 skyboxTexel = texture(cubeMap, normalize(_normal)).rgb; + vec3 skyboxColor = skybox.color.rgb; + _fragColor = vec4(mix(vec3(1.0), skyboxTexel, float(skybox.color.a > 0.0)) * + mix(vec3(1.0), skyboxColor, float(skybox.color.a < 1.0)), 1.0); } diff --git a/libraries/image/src/image/TextureProcessing.cpp b/libraries/image/src/image/TextureProcessing.cpp index 429859d109..c144ed530a 100644 --- a/libraries/image/src/image/TextureProcessing.cpp +++ b/libraries/image/src/image/TextureProcessing.cpp @@ -690,6 +690,8 @@ void convertImageToLDRTexture(gpu::Texture* texture, Image&& image, BackendTarge compressionOptions.setFormat(nvtt::Format_BC4); } else if (mipFormat == gpu::Element::COLOR_COMPRESSED_BCX_XY) { compressionOptions.setFormat(nvtt::Format_BC5); + } else if (mipFormat == gpu::Element::COLOR_COMPRESSED_BCX_HDR_RGB) { + compressionOptions.setFormat(nvtt::Format_BC6); } else if (mipFormat == gpu::Element::COLOR_COMPRESSED_BCX_SRGBA_HIGH) { alphaMode = nvtt::AlphaMode_Transparency; compressionOptions.setFormat(nvtt::Format_BC7); diff --git a/libraries/image/src/image/TextureProcessing.h b/libraries/image/src/image/TextureProcessing.h index b4036ddd9f..c1817a08a6 100644 --- a/libraries/image/src/image/TextureProcessing.h +++ b/libraries/image/src/image/TextureProcessing.h @@ -57,6 +57,7 @@ namespace TextureUsage { * @typedef {number} TextureCache.TextureType */ enum Type { + // NOTE: add new texture types at the bottom here DEFAULT_TEXTURE, STRICT_TEXTURE, ALBEDO_TEXTURE, diff --git a/libraries/ktx/src/TextureMeta.cpp b/libraries/ktx/src/TextureMeta.cpp index c8427c1f60..f97b5404e1 100644 --- a/libraries/ktx/src/TextureMeta.cpp +++ b/libraries/ktx/src/TextureMeta.cpp @@ -16,6 +16,7 @@ #include const QString TEXTURE_META_EXTENSION = ".texmeta.json"; +const uint16_t KTX_VERSION = 1; bool TextureMeta::deserialize(const QByteArray& data, TextureMeta* meta) { QJsonParseError error; @@ -46,6 +47,9 @@ bool TextureMeta::deserialize(const QByteArray& data, TextureMeta* meta) { } } } + if (root.contains("version")) { + meta->version = root["version"].toInt(); + } return true; } @@ -62,6 +66,7 @@ QByteArray TextureMeta::serialize() { root["original"] = original.toString(); root["uncompressed"] = uncompressed.toString(); root["compressed"] = compressed; + root["version"] = KTX_VERSION; doc.setObject(root); return doc.toJson(); diff --git a/libraries/ktx/src/TextureMeta.h b/libraries/ktx/src/TextureMeta.h index 5450fee110..fc39db6ef1 100644 --- a/libraries/ktx/src/TextureMeta.h +++ b/libraries/ktx/src/TextureMeta.h @@ -19,6 +19,7 @@ #include "khronos/KHR.h" extern const QString TEXTURE_META_EXTENSION; +extern const uint16_t KTX_VERSION; namespace std { template<> struct hash { @@ -37,6 +38,7 @@ struct TextureMeta { QUrl original; QUrl uncompressed; std::unordered_map availableTextureTypes; + uint16_t version { 0 }; }; diff --git a/libraries/model-baker/src/model-baker/Baker.cpp b/libraries/model-baker/src/model-baker/Baker.cpp index 70269c6401..c896613df5 100644 --- a/libraries/model-baker/src/model-baker/Baker.cpp +++ b/libraries/model-baker/src/model-baker/Baker.cpp @@ -27,7 +27,7 @@ namespace baker { class GetModelPartsTask { public: using Input = hfm::Model::Pointer; - using Output = VaryingSet6, hifi::URL, baker::MeshIndicesToModelNames, baker::BlendshapesPerMesh, QHash, std::vector>; + using Output = VaryingSet5, hifi::URL, baker::MeshIndicesToModelNames, baker::BlendshapesPerMesh, std::vector>; using JobModel = Job::ModelIO; void run(const BakeContextPointer& context, const Input& input, Output& output) { @@ -40,8 +40,7 @@ namespace baker { for (int i = 0; i < hfmModelIn->meshes.size(); i++) { blendshapesPerMesh.push_back(hfmModelIn->meshes[i].blendshapes.toStdVector()); } - output.edit4() = hfmModelIn->materials; - output.edit5() = hfmModelIn->joints.toStdVector(); + output.edit4() = hfmModelIn->joints.toStdVector(); } }; @@ -134,17 +133,16 @@ namespace baker { const auto url = modelPartsIn.getN(1); const auto meshIndicesToModelNames = modelPartsIn.getN(2); const auto blendshapesPerMeshIn = modelPartsIn.getN(3); - const auto materials = modelPartsIn.getN(4); - const auto jointsIn = modelPartsIn.getN(5); + const auto jointsIn = modelPartsIn.getN(4); // Calculate normals and tangents for meshes and blendshapes if they do not exist // Note: Normals are never calculated here for OBJ models. OBJ files optionally define normals on a per-face basis, so for consistency normals are calculated beforehand in OBJSerializer. const auto normalsPerMesh = model.addJob("CalculateMeshNormals", meshesIn); - const auto calculateMeshTangentsInputs = CalculateMeshTangentsTask::Input(normalsPerMesh, meshesIn, materials).asVarying(); + const auto calculateMeshTangentsInputs = CalculateMeshTangentsTask::Input(normalsPerMesh, meshesIn).asVarying(); const auto tangentsPerMesh = model.addJob("CalculateMeshTangents", calculateMeshTangentsInputs); const auto calculateBlendshapeNormalsInputs = CalculateBlendshapeNormalsTask::Input(blendshapesPerMeshIn, meshesIn).asVarying(); const auto normalsPerBlendshapePerMesh = model.addJob("CalculateBlendshapeNormals", calculateBlendshapeNormalsInputs); - const auto calculateBlendshapeTangentsInputs = CalculateBlendshapeTangentsTask::Input(normalsPerBlendshapePerMesh, blendshapesPerMeshIn, meshesIn, materials).asVarying(); + const auto calculateBlendshapeTangentsInputs = CalculateBlendshapeTangentsTask::Input(normalsPerBlendshapePerMesh, blendshapesPerMeshIn, meshesIn).asVarying(); const auto tangentsPerBlendshapePerMesh = model.addJob("CalculateBlendshapeTangents", calculateBlendshapeTangentsInputs); // Build the graphics::MeshPointer for each hfm::Mesh diff --git a/libraries/model-baker/src/model-baker/CalculateBlendshapeTangentsTask.cpp b/libraries/model-baker/src/model-baker/CalculateBlendshapeTangentsTask.cpp index ba8fd94f09..905de9b5a7 100644 --- a/libraries/model-baker/src/model-baker/CalculateBlendshapeTangentsTask.cpp +++ b/libraries/model-baker/src/model-baker/CalculateBlendshapeTangentsTask.cpp @@ -19,7 +19,6 @@ void CalculateBlendshapeTangentsTask::run(const baker::BakeContextPointer& conte const auto& normalsPerBlendshapePerMesh = input.get0(); const auto& blendshapesPerMesh = input.get1(); const auto& meshes = input.get2(); - const auto& materials = input.get3(); auto& tangentsPerBlendshapePerMeshOut = output; tangentsPerBlendshapePerMeshOut.reserve(normalsPerBlendshapePerMesh.size()); @@ -30,16 +29,6 @@ void CalculateBlendshapeTangentsTask::run(const baker::BakeContextPointer& conte tangentsPerBlendshapePerMeshOut.emplace_back(); auto& tangentsPerBlendshapeOut = tangentsPerBlendshapePerMeshOut[tangentsPerBlendshapePerMeshOut.size()-1]; - // Check if we actually need to calculate the tangents, or just append empty arrays - bool needTangents = false; - for (const auto& meshPart : mesh.parts) { - auto materialIt = materials.find(meshPart.materialID); - if (materialIt != materials.end() && (*materialIt).needTangentSpace()) { - needTangents = true; - break; - } - } - for (size_t j = 0; j < blendshapes.size(); j++) { const auto& blendshape = blendshapes[j]; const auto& tangentsIn = blendshape.tangents; @@ -53,8 +42,8 @@ void CalculateBlendshapeTangentsTask::run(const baker::BakeContextPointer& conte continue; } - // Check if we can and should calculate tangents (we need normals to calculate the tangents) - if (normals.empty() || !needTangents) { + // Check if we can calculate tangents (we need normals and texcoords to calculate the tangents) + if (normals.empty() || normals.size() != (size_t)mesh.texCoords.size()) { continue; } tangentsOut.resize(normals.size()); diff --git a/libraries/model-baker/src/model-baker/CalculateBlendshapeTangentsTask.h b/libraries/model-baker/src/model-baker/CalculateBlendshapeTangentsTask.h index c24b41d2d9..4ad8fee036 100644 --- a/libraries/model-baker/src/model-baker/CalculateBlendshapeTangentsTask.h +++ b/libraries/model-baker/src/model-baker/CalculateBlendshapeTangentsTask.h @@ -18,7 +18,7 @@ // Calculate blendshape tangents if not already present in the blendshape class CalculateBlendshapeTangentsTask { public: - using Input = baker::VaryingSet4, baker::BlendshapesPerMesh, std::vector, QHash>; + using Input = baker::VaryingSet3, baker::BlendshapesPerMesh, std::vector>; using Output = std::vector; using JobModel = baker::Job::ModelIO; diff --git a/libraries/model-baker/src/model-baker/CalculateMeshTangentsTask.cpp b/libraries/model-baker/src/model-baker/CalculateMeshTangentsTask.cpp index d2144a0e30..297d8cbde7 100644 --- a/libraries/model-baker/src/model-baker/CalculateMeshTangentsTask.cpp +++ b/libraries/model-baker/src/model-baker/CalculateMeshTangentsTask.cpp @@ -13,21 +13,9 @@ #include "ModelMath.h" -bool needTangents(const hfm::Mesh& mesh, const QHash& materials) { - // Check if we actually need to calculate the tangents - for (const auto& meshPart : mesh.parts) { - auto materialIt = materials.find(meshPart.materialID); - if (materialIt != materials.end() && (*materialIt).needTangentSpace()) { - return true; - } - } - return false; -} - void CalculateMeshTangentsTask::run(const baker::BakeContextPointer& context, const Input& input, Output& output) { const auto& normalsPerMesh = input.get0(); const std::vector& meshes = input.get1(); - const auto& materials = input.get2(); auto& tangentsPerMeshOut = output; tangentsPerMeshOut.reserve(meshes.size()); @@ -39,10 +27,10 @@ void CalculateMeshTangentsTask::run(const baker::BakeContextPointer& context, co auto& tangentsOut = tangentsPerMeshOut[tangentsPerMeshOut.size()-1]; // Check if we already have tangents and therefore do not need to do any calculation - // Otherwise confirm if we have the normals needed, and need to calculate the tangents + // Otherwise confirm if we have the normals and texcoords needed if (!tangentsIn.empty()) { tangentsOut = tangentsIn.toStdVector(); - } else if (!normals.empty() && needTangents(mesh, materials)) { + } else if (!normals.empty() && mesh.vertices.size() == mesh.texCoords.size()) { tangentsOut.resize(normals.size()); baker::calculateTangents(mesh, [&mesh, &normals, &tangentsOut](int firstIndex, int secondIndex, glm::vec3* outVertices, glm::vec2* outTexCoords, glm::vec3& outNormal) { diff --git a/libraries/model-baker/src/model-baker/CalculateMeshTangentsTask.h b/libraries/model-baker/src/model-baker/CalculateMeshTangentsTask.h index b8fdb7d5f4..2ad5759476 100644 --- a/libraries/model-baker/src/model-baker/CalculateMeshTangentsTask.h +++ b/libraries/model-baker/src/model-baker/CalculateMeshTangentsTask.h @@ -22,7 +22,7 @@ class CalculateMeshTangentsTask { public: using NormalsPerMesh = std::vector>; - using Input = baker::VaryingSet3, QHash>; + using Input = baker::VaryingSet2>; using Output = baker::TangentsPerMesh; using JobModel = baker::Job::ModelIO; diff --git a/libraries/networking/src/AccountManager.cpp b/libraries/networking/src/AccountManager.cpp index 226433e388..3a7d3e0a67 100644 --- a/libraries/networking/src/AccountManager.cpp +++ b/libraries/networking/src/AccountManager.cpp @@ -97,6 +97,7 @@ void AccountManager::logout() { // remove this account from the account settings file removeAccountFromFile(); + saveLoginStatus(false); emit logoutComplete(); // the username has changed to blank @@ -650,6 +651,39 @@ void AccountManager::refreshAccessToken() { } } +void AccountManager::setAccessTokens(const QString& response) { + QJsonDocument jsonResponse = QJsonDocument::fromJson(response.toUtf8()); + const QJsonObject& rootObject = jsonResponse.object(); + + if (!rootObject.contains("error")) { + // construct an OAuthAccessToken from the json object + + if (!rootObject.contains("access_token") || !rootObject.contains("expires_in") + || !rootObject.contains("token_type")) { + // TODO: error handling - malformed token response + qCDebug(networking) << "Received a response for password grant that is missing one or more expected values."; + } else { + // clear the path from the response URL so we have the right root URL for this access token + QUrl rootURL = rootObject.contains("url") ? rootObject["url"].toString() : _authURL; + rootURL.setPath(""); + + qCDebug(networking) << "Storing an account with access-token for" << qPrintable(rootURL.toString()); + + _accountInfo = DataServerAccountInfo(); + _accountInfo.setAccessTokenFromJSON(rootObject); + emit loginComplete(rootURL); + + persistAccountToFile(); + saveLoginStatus(true); + requestProfile(); + } + } else { + // TODO: error handling + qCDebug(networking) << "Error in response for password grant -" << rootObject["error_description"].toString(); + emit loginFailed(); + } +} + void AccountManager::requestAccessTokenFinished() { QNetworkReply* requestReply = reinterpret_cast(sender()); @@ -895,3 +929,34 @@ void AccountManager::handleKeypairGenerationError() { void AccountManager::setLimitedCommerce(bool isLimited) { _limitedCommerce = isLimited; } + +void AccountManager::saveLoginStatus(bool isLoggedIn) { + if (!_configFileURL.isEmpty()) { + QFile configFile(_configFileURL); + configFile.open(QIODevice::ReadOnly | QIODevice::Text); + QJsonParseError error; + QJsonDocument jsonDocument = QJsonDocument::fromJson(configFile.readAll(), &error); + configFile.close(); + QString launcherPath; + if (error.error == QJsonParseError::NoError) { + QJsonObject rootObject = jsonDocument.object(); + if (rootObject.contains("launcherPath")) { + launcherPath = rootObject["launcherPath"].toString(); + } + if (rootObject.contains("loggedIn")) { + rootObject["loggedIn"] = isLoggedIn; + } + jsonDocument = QJsonDocument(rootObject); + + } + configFile.open(QFile::WriteOnly | QFile::Text | QFile::Truncate); + configFile.write(jsonDocument.toJson()); + configFile.close(); + if (!isLoggedIn && !launcherPath.isEmpty()) { + QProcess launcher; + launcher.setProgram(launcherPath); + launcher.startDetached(); + qApp->quit(); + } + } +} \ No newline at end of file diff --git a/libraries/networking/src/AccountManager.h b/libraries/networking/src/AccountManager.h index 8732042e93..c2187f79cb 100644 --- a/libraries/networking/src/AccountManager.h +++ b/libraries/networking/src/AccountManager.h @@ -102,6 +102,10 @@ public: bool getLimitedCommerce() { return _limitedCommerce; } void setLimitedCommerce(bool isLimited); + void setAccessTokens(const QString& response); + void setConfigFileURL(const QString& fileURL) { _configFileURL = fileURL; } + void saveLoginStatus(bool isLoggedIn); + public slots: void requestAccessToken(const QString& login, const QString& password); void requestAccessTokenWithSteam(QByteArray authSessionTicket); @@ -162,6 +166,7 @@ private: QUuid _sessionID { QUuid::createUuid() }; bool _limitedCommerce { false }; + QString _configFileURL; }; #endif // hifi_AccountManager_h diff --git a/libraries/networking/src/LimitedNodeList.h b/libraries/networking/src/LimitedNodeList.h index 60c91c8ff6..9f4a0177f5 100644 --- a/libraries/networking/src/LimitedNodeList.h +++ b/libraries/networking/src/LimitedNodeList.h @@ -49,7 +49,7 @@ const int INVALID_PORT = -1; -const quint64 NODE_SILENCE_THRESHOLD_MSECS = 20 * 1000; +const quint64 NODE_SILENCE_THRESHOLD_MSECS = 10 * 1000; static const size_t DEFAULT_MAX_CONNECTION_RATE { std::numeric_limits::max() }; diff --git a/libraries/networking/src/NodeList.cpp b/libraries/networking/src/NodeList.cpp index 7d5a60c52b..d6ccd30746 100644 --- a/libraries/networking/src/NodeList.cpp +++ b/libraries/networking/src/NodeList.cpp @@ -11,6 +11,8 @@ #include "NodeList.h" +#include + #include #include #include @@ -37,6 +39,8 @@ #include "SharedUtil.h" #include +using namespace std::chrono; + const int KEEPALIVE_PING_INTERVAL_MS = 1000; NodeList::NodeList(char newOwnerType, int socketListenPort, int dtlsListenPort) : @@ -91,10 +95,10 @@ NodeList::NodeList(char newOwnerType, int socketListenPort, int dtlsListenPort) connect(accountManager.data(), &AccountManager::newKeypair, this, &NodeList::sendDomainServerCheckIn); // clear out NodeList when login is finished and we know our new username - connect(accountManager.data(), SIGNAL(usernameChanged(QString)) , this, SLOT(reset())); + connect(accountManager.data(), &AccountManager::usernameChanged , this, [this]{ reset("Username changed"); }); // clear our NodeList when logout is requested - connect(accountManager.data(), SIGNAL(logoutComplete()) , this, SLOT(reset())); + connect(accountManager.data(), &AccountManager::logoutComplete , this, [this]{ reset("Logged out"); }); // anytime we get a new node we will want to attempt to punch to it connect(this, &LimitedNodeList::nodeAdded, this, &NodeList::startNodeHolePunch); @@ -292,7 +296,8 @@ void NodeList::addSetOfNodeTypesToNodeInterestSet(const NodeSet& setOfNodeTypes) void NodeList::sendDomainServerCheckIn() { - // This function is called by the server check-in timer thread + // On ThreadedAssignments (assignment clients), this function + // is called by the server check-in timer thread // not the NodeList thread. Calling it on the NodeList thread // resulted in starvation of the server check-in function. // be VERY CAREFUL modifying this code as members of NodeList @@ -411,6 +416,8 @@ void NodeList::sendDomainServerCheckIn() { packetStream << FingerprintUtils::getMachineFingerprint(); } + packetStream << quint64(duration_cast(p_high_resolution_clock::now().time_since_epoch()).count()); + // pack our data to send to the domain-server including // the hostname information (so the domain-server can see which place name we came in on) packetStream << _ownerType.load() << publicSockAddr << localSockAddr << _nodeTypesOfInterest.toList(); @@ -617,8 +624,53 @@ void NodeList::processDomainServerConnectionTokenPacket(QSharedPointer message) { + + // parse header information + QDataStream packetStream(message->getMessage()); + + // grab the domain's ID from the beginning of the packet + QUuid domainUUID; + packetStream >> domainUUID; + + Node::LocalID domainLocalID; + packetStream >> domainLocalID; + + // pull our owner (ie. session) UUID from the packet, it's always the first thing + // The short (16 bit) ID comes next. + QUuid newUUID; + Node::LocalID newLocalID; + packetStream >> newUUID; + packetStream >> newLocalID; + + // pull the permissions/right/privileges for this node out of the stream + NodePermissions newPermissions; + packetStream >> newPermissions; + // Is packet authentication enabled? + bool isAuthenticated; + packetStream >> isAuthenticated; + + qint64 now = qint64(duration_cast(p_high_resolution_clock::now().time_since_epoch()).count()); + + quint64 connectRequestTimestamp; + packetStream >> connectRequestTimestamp; + + quint64 domainServerRequestReceiveTime; + packetStream >> domainServerRequestReceiveTime; + + quint64 domainServerPingSendTime; + packetStream >> domainServerPingSendTime; + + qint64 pingLagTime = (now - qint64(connectRequestTimestamp)) / qint64(USECS_PER_MSEC); + + qint64 domainServerRequestLag = (qint64(domainServerRequestReceiveTime) - qint64(connectRequestTimestamp)) / qint64(USECS_PER_MSEC); + quint64 domainServerCheckinProcessingTime = domainServerPingSendTime - domainServerRequestReceiveTime; + qint64 domainServerResponseLag = (now - qint64(domainServerPingSendTime)) / qint64(USECS_PER_MSEC); + if (_domainHandler.getSockAddr().isNull()) { - qWarning() << "IGNORING DomainList packet while not connected to a Domain Server"; + qWarning(networking) << "IGNORING DomainList packet while not connected to a Domain Server: sent " << pingLagTime << " msec ago."; + qWarning(networking) << "DomainList request lag (interface->ds): " << domainServerRequestLag << "msec"; + qWarning(networking) << "DomainList server processing time: " << domainServerCheckinProcessingTime << "usec"; + qWarning(networking) << "DomainList response lag (ds->interface): " << domainServerResponseLag << "msec"; // refuse to process this packet if we aren't currently connected to the DS return; } @@ -630,6 +682,14 @@ void NodeList::processDomainServerList(QSharedPointer message) } #endif + // warn if ping lag is getting long + if (pingLagTime > qint64(MSECS_PER_SECOND)) { + qCDebug(networking) << "DomainList ping is lagging: " << pingLagTime << "msec"; + qCDebug(networking) << "DomainList request lag (interface->ds): " << domainServerRequestLag << "msec"; + qCDebug(networking) << "DomainList server processing time: " << domainServerCheckinProcessingTime << "usec"; + qCDebug(networking) << "DomainList response lag (ds->interface): " << domainServerResponseLag << "msec"; + } + // this is a packet from the domain server, reset the count of un-replied check-ins _domainHandler.clearPendingCheckins(); @@ -638,28 +698,16 @@ void NodeList::processDomainServerList(QSharedPointer message) DependencyManager::get()->flagTimeForConnectionStep(LimitedNodeList::ConnectionStep::ReceiveDSList); - QDataStream packetStream(message->getMessage()); - - // grab the domain's ID from the beginning of the packet - QUuid domainUUID; - packetStream >> domainUUID; - if (_domainHandler.isConnected() && _domainHandler.getUUID() != domainUUID) { // Recieved packet from different domain. - qWarning() << "IGNORING DomainList packet from" << domainUUID << "while connected to" << _domainHandler.getUUID(); + qWarning() << "IGNORING DomainList packet from" << domainUUID << "while connected to" + << _domainHandler.getUUID() << ": sent " << pingLagTime << " msec ago."; + qWarning(networking) << "DomainList request lag (interface->ds): " << domainServerRequestLag << "msec"; + qWarning(networking) << "DomainList server processing time: " << domainServerCheckinProcessingTime << "usec"; + qWarning(networking) << "DomainList response lag (ds->interface): " << domainServerResponseLag << "msec"; return; } - Node::LocalID domainLocalID; - packetStream >> domainLocalID; - - // pull our owner (ie. session) UUID from the packet, it's always the first thing - // The short (16 bit) ID comes next. - QUuid newUUID; - Node::LocalID newLocalID; - packetStream >> newUUID; - packetStream >> newLocalID; - // when connected, if the session ID or local ID were not null and changed, we should reset auto currentLocalID = getSessionLocalID(); auto currentSessionID = getSessionUUID(); @@ -690,13 +738,7 @@ void NodeList::processDomainServerList(QSharedPointer message) DependencyManager::get()->lookupShareableNameForDomainID(domainUUID); } - // pull the permissions/right/privileges for this node out of the stream - NodePermissions newPermissions; - packetStream >> newPermissions; setPermissions(newPermissions); - // Is packet authentication enabled? - bool isAuthenticated; - packetStream >> isAuthenticated; setAuthenticatePackets(isAuthenticated); // pull each node in the packet diff --git a/libraries/networking/src/ReceivedMessage.cpp b/libraries/networking/src/ReceivedMessage.cpp index e70301dab1..7ae408ef7a 100644 --- a/libraries/networking/src/ReceivedMessage.cpp +++ b/libraries/networking/src/ReceivedMessage.cpp @@ -12,6 +12,7 @@ #include "ReceivedMessage.h" #include +#include #include "QSharedPointer" @@ -20,6 +21,8 @@ int sharedPtrReceivedMessageMetaTypeId = qRegisterMetaType(packetList.getFirstPacketReceiveTime().time_since_epoch()).count(); } ReceivedMessage::ReceivedMessage(NLPacket& packet) @@ -41,6 +45,7 @@ ReceivedMessage::ReceivedMessage(NLPacket& packet) _senderSockAddr(packet.getSenderSockAddr()), _isComplete(packet.getPacketPosition() == NLPacket::ONLY) { + _firstPacketReceiveTime = duration_cast(packet.getReceiveTime().time_since_epoch()).count(); } ReceivedMessage::ReceivedMessage(QByteArray byteArray, PacketType packetType, PacketVersion packetVersion, @@ -48,6 +53,7 @@ ReceivedMessage::ReceivedMessage(QByteArray byteArray, PacketType packetType, Pa _data(byteArray), _headData(_data.mid(0, HEAD_DATA_SIZE)), _numPackets(1), + _firstPacketReceiveTime(0), _sourceID(sourceID), _packetType(packetType), _packetVersion(packetVersion), @@ -77,7 +83,13 @@ void ReceivedMessage::appendPacket(NLPacket& packet) { emit progress(getSize()); } - if (packet.getPacketPosition() == NLPacket::PacketPosition::LAST) { + auto packetPosition = packet.getPacketPosition(); + if ((packetPosition == NLPacket::PacketPosition::FIRST) || + (packetPosition == NLPacket::PacketPosition::ONLY)) { + _firstPacketReceiveTime = duration_cast(packet.getReceiveTime().time_since_epoch()).count(); + } + + if (packetPosition == NLPacket::PacketPosition::LAST) { _isComplete = true; emit completed(); } diff --git a/libraries/networking/src/ReceivedMessage.h b/libraries/networking/src/ReceivedMessage.h index af87ef75af..c864616635 100644 --- a/libraries/networking/src/ReceivedMessage.h +++ b/libraries/networking/src/ReceivedMessage.h @@ -48,6 +48,8 @@ public: // Get the number of packets that were used to send this message qint64 getNumPackets() const { return _numPackets; } + qint64 getFirstPacketReceiveTime() const { return _firstPacketReceiveTime; } + qint64 getSize() const { return _data.size(); } qint64 getBytesLeftToRead() const { return _data.size() - _position; } @@ -92,6 +94,7 @@ private: std::atomic _position { 0 }; std::atomic _numPackets { 0 }; + std::atomic _firstPacketReceiveTime { 0 }; NLPacket::LocalID _sourceID { NLPacket::NULL_LOCAL_ID }; PacketType _packetType; diff --git a/libraries/networking/src/udt/Packet.cpp b/libraries/networking/src/udt/Packet.cpp index 0456fa1223..f8b74cea37 100644 --- a/libraries/networking/src/udt/Packet.cpp +++ b/libraries/networking/src/udt/Packet.cpp @@ -171,6 +171,7 @@ void Packet::copyMembers(const Packet& other) { _packetPosition = other._packetPosition; _messageNumber = other._messageNumber; _messagePartNumber = other._messagePartNumber; + _receiveTime = other._receiveTime; } void Packet::readHeader() const { diff --git a/libraries/networking/src/udt/PacketHeaders.cpp b/libraries/networking/src/udt/PacketHeaders.cpp index f8574b3b94..566e1e4946 100644 --- a/libraries/networking/src/udt/PacketHeaders.cpp +++ b/libraries/networking/src/udt/PacketHeaders.cpp @@ -27,7 +27,7 @@ PacketVersion versionForPacketType(PacketType packetType) { case PacketType::StunResponse: return 17; case PacketType::DomainList: - return static_cast(DomainListVersion::AuthenticationOptional); + return static_cast(DomainListVersion::HasTimestamp); case PacketType::EntityAdd: case PacketType::EntityClone: case PacketType::EntityEdit: @@ -72,7 +72,7 @@ PacketVersion versionForPacketType(PacketType packetType) { return static_cast(DomainConnectionDeniedVersion::IncludesExtraInfo); case PacketType::DomainConnectRequest: - return static_cast(DomainConnectRequestVersion::AlwaysHasMachineFingerprint); + return static_cast(DomainConnectRequestVersion::HasTimestamp); case PacketType::DomainServerAddedNode: return static_cast(DomainServerAddedNodeVersion::PermissionsGrid); diff --git a/libraries/networking/src/udt/PacketHeaders.h b/libraries/networking/src/udt/PacketHeaders.h index 5deadd8c43..903c1f4c93 100644 --- a/libraries/networking/src/udt/PacketHeaders.h +++ b/libraries/networking/src/udt/PacketHeaders.h @@ -344,7 +344,8 @@ enum class DomainConnectRequestVersion : PacketVersion { HasProtocolVersions, HasMACAddress, HasMachineFingerprint, - AlwaysHasMachineFingerprint + AlwaysHasMachineFingerprint, + HasTimestamp }; enum class DomainConnectionDeniedVersion : PacketVersion { @@ -363,7 +364,8 @@ enum class DomainListVersion : PacketVersion { PermissionsGrid, GetUsernameFromUUIDSupport, GetMachineFingerprintFromUUIDSupport, - AuthenticationOptional + AuthenticationOptional, + HasTimestamp }; enum class AudioVersion : PacketVersion { diff --git a/libraries/networking/src/udt/PacketList.cpp b/libraries/networking/src/udt/PacketList.cpp index d69ff39197..f6ea12242e 100644 --- a/libraries/networking/src/udt/PacketList.cpp +++ b/libraries/networking/src/udt/PacketList.cpp @@ -13,6 +13,7 @@ #include "../NetworkLogging.h" +#include #include using namespace udt; @@ -261,3 +262,11 @@ qint64 PacketList::writeData(const char* data, qint64 maxSize) { return maxSize; } + +p_high_resolution_clock::time_point PacketList::getFirstPacketReceiveTime() const { + using namespace std::chrono;; + if (!_packets.empty()) { + return _packets.front()->getReceiveTime(); + } + return p_high_resolution_clock::time_point(); +} \ No newline at end of file diff --git a/libraries/networking/src/udt/PacketList.h b/libraries/networking/src/udt/PacketList.h index b9bd6a8c15..8d3ffb2783 100644 --- a/libraries/networking/src/udt/PacketList.h +++ b/libraries/networking/src/udt/PacketList.h @@ -59,6 +59,9 @@ public: virtual qint64 size() const override { return getDataSize(); } qint64 writeString(const QString& string); + + p_high_resolution_clock::time_point getFirstPacketReceiveTime() const; + protected: PacketList(PacketType packetType, QByteArray extendedHeader = QByteArray(), bool isReliable = false, bool isOrdered = false); diff --git a/libraries/physics/src/PhysicalEntitySimulation.cpp b/libraries/physics/src/PhysicalEntitySimulation.cpp index 3c730fc6cf..7bbf1854fa 100644 --- a/libraries/physics/src/PhysicalEntitySimulation.cpp +++ b/libraries/physics/src/PhysicalEntitySimulation.cpp @@ -214,15 +214,26 @@ void PhysicalEntitySimulation::clearEntitiesInternal() { _entitiesToRemoveFromPhysics.clear(); _entitiesToAddToPhysics.clear(); _incomingChanges.clear(); + _entitiesToDeleteLater.clear(); } // virtual void PhysicalEntitySimulation::prepareEntityForDelete(EntityItemPointer entity) { + // this can be called on any thread assert(entity); assert(entity->isDead()); QMutexLocker lock(&_mutex); - entity->clearActions(getThisPointer()); - removeEntityInternal(entity); + _entitiesToDeleteLater.push_back(entity); +} + +void PhysicalEntitySimulation::removeDeadEntities() { + // only ever call this on the main thread + QMutexLocker lock(&_mutex); + for (auto& entity : _entitiesToDeleteLater) { + entity->clearActions(getThisPointer()); + removeEntityInternal(entity); + } + _entitiesToDeleteLater.clear(); } // end EntitySimulation overrides diff --git a/libraries/physics/src/PhysicalEntitySimulation.h b/libraries/physics/src/PhysicalEntitySimulation.h index 817f92cb3c..f5213f7fef 100644 --- a/libraries/physics/src/PhysicalEntitySimulation.h +++ b/libraries/physics/src/PhysicalEntitySimulation.h @@ -80,6 +80,7 @@ protected: // only called by EntitySimulation public: virtual void prepareEntityForDelete(EntityItemPointer entity) override; + void removeDeadEntities(); void buildPhysicsTransaction(PhysicsEngine::Transaction& transaction); void handleProcessedPhysicsTransaction(PhysicsEngine::Transaction& transaction); @@ -121,6 +122,7 @@ private: VectorOfEntityMotionStates _owned; VectorOfEntityMotionStates _bids; SetOfEntities _deadAvatarEntities; + std::vector _entitiesToDeleteLater; workload::SpacePointer _space; uint64_t _nextBidExpiry; uint32_t _lastStepSendPackets { 0 }; diff --git a/libraries/platform/CMakeLists.txt b/libraries/platform/CMakeLists.txt index 2d71babe6f..70f3157e1e 100644 --- a/libraries/platform/CMakeLists.txt +++ b/libraries/platform/CMakeLists.txt @@ -1,5 +1,7 @@ set(TARGET_NAME platform) -setup_hifi_library() +setup_hifi_library() link_hifi_libraries(shared) + +GroupSources("src") target_json() diff --git a/libraries/platform/src/MACOSPlatform.cpp b/libraries/platform/src/MACOSPlatform.cpp deleted file mode 100644 index 7081044879..0000000000 --- a/libraries/platform/src/MACOSPlatform.cpp +++ /dev/null @@ -1,107 +0,0 @@ -// -// Created by Amer Cerkic 05/02/2019 -// Copyright 2019 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 "MACOSPlatform.h" -#include "platformJsonKeys.h" - -#include -#include -#include - -#ifdef Q_OS_MAC -#include -#include -#include -#endif - -using namespace platform; - -static void getCpuId( uint32_t* p, uint32_t ax ) -{ -#ifdef Q_OS_MAC - __asm __volatile - ( "movl %%ebx, %%esi\n\t" - "cpuid\n\t" - "xchgl %%ebx, %%esi" - : "=a" (p[0]), "=S" (p[1]), - "=c" (p[2]), "=d" (p[3]) - : "0" (ax) - ); -#endif -} - - -void MACOSInstance::enumerateCpu() { - json cpu = {}; - uint32_t cpuInfo[4]={0,0,0,0}; - char CPUBrandString[16]; - char CPUModelString[16]; - char CPUClockString[16]; - uint32_t nExIds; - getCpuId(cpuInfo, 0x80000000); - nExIds = cpuInfo[0]; - - for (uint32_t i = 0x80000000; i <= nExIds; ++i) { - getCpuId(cpuInfo, i); - // Interpret CPU brand string - if (i == 0x80000002) { - memcpy(CPUBrandString, cpuInfo, sizeof(cpuInfo)); - } else if (i == 0x80000003) { - memcpy(CPUModelString, cpuInfo, sizeof(cpuInfo)); - } else if (i == 0x80000004) { - memcpy(CPUClockString, cpuInfo, sizeof(cpuInfo)); - } - } - - cpu["cpuBrand"] = CPUBrandString; - cpu["cpuModel"] = CPUModelString; - cpu["cpuClockSpeed"] = CPUClockString; - cpu["cpuNumCores"] = std::thread::hardware_concurrency(); - - _cpu.push_back(cpu); -} - -void MACOSInstance::enumerateGpu() { - GPUIdent* ident = GPUIdent::getInstance(); - json gpu = {}; - gpu["gpuName"] = ident->getName().toUtf8().constData(); - gpu["gpuMemory"] = ident->getMemory(); - gpu["gpuDriver"] = ident->getDriver().toUtf8().constData(); - - _gpu.push_back(gpu); - _display = ident->getOutput(); - -} - -void MACOSInstance::enumerateMemory() { - json ram = {}; - -#ifdef Q_OS_MAC - long pages = sysconf(_SC_PHYS_PAGES); - long page_size = sysconf(_SC_PAGE_SIZE); - ram["totalMemory"] = pages * page_size;; -#endif - _memory.push_back(ram); -} - -void MACOSInstance::enumerateComputer(){ -#ifdef Q_OS_MAC - - //get system name - size_t len=0; - sysctlbyname("hw.model",NULL, &len, NULL, 0); - char* model = (char *) malloc(sizeof(char)*len+1); - sysctlbyname("hw.model", model, &len, NULL,0); - - _computer["computerModel"]=std::string(model); - - free(model); - -#endif -} - diff --git a/libraries/platform/src/WINPlatform.cpp b/libraries/platform/src/WINPlatform.cpp deleted file mode 100644 index 8b37aa4e77..0000000000 --- a/libraries/platform/src/WINPlatform.cpp +++ /dev/null @@ -1,88 +0,0 @@ -// -// Created by Amer Cerkic 05/02/2019 -// Copyright 2019 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 "WINPlatform.h" -#include "platformJsonKeys.h" - -#ifdef Q_OS_WINDOWS -#include -#include -#endif - -#include -#include -#include - - -using namespace platform; - -void WINInstance::enumerateCpu() { - json cpu = {}; - -#ifdef Q_OS_WINDOWS - int CPUInfo[4] = { -1 }; - unsigned nExIds; - unsigned int i = 0; - char CPUBrandString[16]; - char CPUModelString[16]; - char CPUClockString[16]; - // Get the information associated with each extended ID. - __cpuid(CPUInfo, 0x80000000); - nExIds = CPUInfo[0]; - - for (i = 0x80000000; i <= nExIds; ++i) { - __cpuid(CPUInfo, i); - // Interpret CPU brand string - if (i == 0x80000002) { - memcpy(CPUBrandString, CPUInfo, sizeof(CPUInfo)); - } else if (i == 0x80000003) { - memcpy(CPUModelString, CPUInfo, sizeof(CPUInfo)); - } else if (i == 0x80000004) { - memcpy(CPUClockString, CPUInfo, sizeof(CPUInfo)); - } - } - - cpu["cpuBrand"] = CPUBrandString; - cpu["cpuModel"] = CPUModelString; - cpu["cpuClockSpeed"] = CPUClockString; - cpu["cpuNumCores"] = std::thread::hardware_concurrency(); -#endif - - _cpu.push_back(cpu); -} - -void WINInstance::enumerateGpu() { - - GPUIdent* ident = GPUIdent::getInstance(); - - json gpu = {}; - gpu["gpuName"] = ident->getName().toUtf8().constData(); - gpu["gpuMemory"] = ident->getMemory(); - gpu["gpuDriver"] = ident->getDriver().toUtf8().constData(); - - _gpu.push_back(gpu); - _display = ident->getOutput(); -} - -void WINInstance::enumerateMemory() { - json ram = {}; - -#ifdef Q_OS_WINDOWS - MEMORYSTATUSEX statex; - statex.dwLength = sizeof(statex); - GlobalMemoryStatusEx(&statex); - int totalRam = statex.ullTotalPhys / 1024 / 1024; - ram["totalMemory"] = totalRam; -#endif - _memory.push_back(ram); -} - -void WINInstance::enumerateComputer(){ - //no implememntation at this time -} - diff --git a/libraries/platform/src/platform.cpp b/libraries/platform/src/platform.cpp deleted file mode 100644 index 64bd536eee..0000000000 --- a/libraries/platform/src/platform.cpp +++ /dev/null @@ -1,83 +0,0 @@ -// -// Created by Amer Cerkic 05/02/2019 -// Copyright 2019 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 "platform.h" - -#include - -#if defined(Q_OS_WIN) -#include "WINPlatform.h" -#elif defined(Q_OS_MAC) -#include "MACOSPlatform.h" -#elif defined(Q_OS_ANDROID) -#include "AndroidPlatform.h" -#elif defined(Q_OS_LINUX) -#include "LinuxPlatform.h" -#endif - -using namespace platform; - -Instance *_instance; - -void platform::create() { -#if defined(Q_OS_WIN) - _instance =new WINInstance(); -#elif defined(Q_OS_MAC) - _instance = new MACOSInstance(); -#elif defined(Q_OS_ANDROID) - _instance= new AndroidInstance(); -#elif defined(Q_OS_LINUX) - _instance= new LinuxInstance(); -#endif -} - -void platform::destroy() { - if(_instance) - delete _instance; -} - -bool platform::enumeratePlatform() { - return _instance->enumeratePlatform(); -} - -int platform::getNumCPU() { - return _instance->getNumCPU(); -} - -json platform::getCPU(int index) { - return _instance->getCPU(index); -} - -int platform::getNumGPU() { - return _instance->getNumGPU(); -} - -json platform::getGPU(int index) { - return _instance->getGPU(index); -} - -int platform::getNumDisplay() { - return _instance->getNumDisplay(); -} - -json platform::getDisplay(int index) { - return _instance->getDisplay(index); -} - -int platform::getNumMemory() { - return _instance->getNumMemory(); -} - -json platform::getMemory(int index) { - return _instance->getMemory(index); -} - -json platform::getComputer(){ - return _instance->getComputer(); -} diff --git a/libraries/platform/src/platform.h b/libraries/platform/src/platform/Platform.h similarity index 88% rename from libraries/platform/src/platform.h rename to libraries/platform/src/platform/Platform.h index 14ec5fa8b2..7f73ff4ff4 100644 --- a/libraries/platform/src/platform.h +++ b/libraries/platform/src/platform/Platform.h @@ -19,20 +19,20 @@ void create(); void destroy(); bool enumeratePlatform(); -int getNumCPU(); +int getNumCPUs(); json getCPU(int index); -int getNumGPU(); +int getNumGPUs(); json getGPU(int index); -int getNumDisplay(); +int getNumDisplays(); json getDisplay(int index); -int getNumMemory(); +int getNumMemories(); json getMemory(int index); json getComputer(); - + } // namespace platform #endif // hifi_platform_h diff --git a/libraries/platform/src/platform/PlatformKeys.h b/libraries/platform/src/platform/PlatformKeys.h new file mode 100644 index 0000000000..fd29b2ff7f --- /dev/null +++ b/libraries/platform/src/platform/PlatformKeys.h @@ -0,0 +1,57 @@ +// +// Created by Amer Cerkic 05/02/2019 +// Copyright 2019 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_platform_PlatformKeys_h +#define hifi_platform_PlatformKeys_h + +namespace platform { namespace keys{ + namespace cpu { + extern const char* vendor; + extern const char* vendor_Intel; + extern const char* vendor_AMD; + + extern const char* model; + extern const char* clockSpeed; + extern const char* numCores; + } + namespace gpu { + extern const char* vendor; + extern const char* vendor_NVIDIA; + extern const char* vendor_AMD; + extern const char* vendor_Intel; + + extern const char* model; + extern const char* videoMemory; + extern const char* driver; + } + namespace display { + extern const char* description; + extern const char* name; + extern const char* coordsLeft; + extern const char* coordsRight; + extern const char* coordsTop; + extern const char* coordsBottom; + } + extern const char* memTotal; + + namespace computer { + extern const char* OS; + extern const char* OS_WINDOWS; + extern const char* OS_MACOS; + extern const char* OS_LINUX; + extern const char* OS_ANDROID; + + extern const char* vendor; + extern const char* vendor_Apple; + + extern const char* model; + + extern const char* profileTier; + } + } } // namespace plaform::keys + +#endif diff --git a/libraries/platform/src/platform/Profiler.cpp b/libraries/platform/src/platform/Profiler.cpp new file mode 100644 index 0000000000..f77bbec46b --- /dev/null +++ b/libraries/platform/src/platform/Profiler.cpp @@ -0,0 +1,127 @@ +// +// Profiler.cpp +// libraries/platform/src/platform +// +// Created by Sam Gateau on 5/22/2019. +// Copyright 2019 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 "Profiler.h" + +#include "Platform.h" +#include "PlatformKeys.h" + +using namespace platform; + +const std::array Profiler::TierNames = {{ "UNKNOWN", "LOW", "MID", "HIGH" }}; + + +bool filterOnComputer(const platform::json& computer, Profiler::Tier& tier); +bool filterOnComputerMACOS(const platform::json& computer, Profiler::Tier& tier); +bool filterOnProcessors(const platform::json& computer, const platform::json& cpu, const platform::json& gpu, Profiler::Tier& tier); + +Profiler::Tier Profiler::profilePlatform() { + auto platformTier = Profiler::Tier::UNKNOWN; // unknown tier yet + + // first filter on the computer info and catch known models + auto computerInfo = platform::getComputer(); + if (filterOnComputer(computerInfo, platformTier)) { + return platformTier; + } + + // Not filtered yet, let s try to make sense of the cpu and gpu info + auto cpuInfo = platform::getCPU(0); + auto gpuInfo = platform::getGPU(0); + if (filterOnProcessors(computerInfo, cpuInfo, gpuInfo, platformTier)) { + return platformTier; + } + + // Default answer is + return Profiler::Tier::LOW; +} + +// tier filter on computer info +bool filterOnComputer(const platform::json& computer, Profiler::Tier& tier) { + if (computer.count(keys::computer::OS)) { + const auto os = computer[keys::computer::OS].get(); + if (os.compare(keys::computer::OS_MACOS) == 0) { + return filterOnComputerMACOS(computer, tier); + } + } + return false; +} + + +// tier filter on computer MACOS +bool filterOnComputerMACOS(const platform::json& computer, Profiler::Tier& tier) { + + // it s a macos computer, probably can tell from the model name: + if (computer.count(keys::computer::model)) { + const auto model = computer[keys::computer::model].get(); + if (model.find("MacBookAir") != std::string::npos) { + tier = Profiler::Tier::LOW; + return true; + } else if (model.find("MacBookPro") != std::string::npos) { + tier = Profiler::Tier::MID; + return true; + } else if (model.find("MacBook") != std::string::npos) { + tier = Profiler::Tier::LOW; + return true; + } + } + return false; +} + +bool filterOnProcessors(const platform::json& computer, const platform::json& cpu, const platform::json& gpu, Profiler::Tier& tier) { + // first filter on the number of cores, <= 4 means LOW + int numCores = 0; + std::string cpuVendor; + if (cpu.count(keys::cpu::numCores)) { + numCores = cpu[keys::cpu::numCores].get(); + if (numCores <= 4) { + tier = Profiler::Tier::LOW; + return true; + } + + cpuVendor = cpu[keys::cpu::vendor].get(); + } + + // enough cores to be mid or high + // let s look at the gpu + if (gpu.count(keys::gpu::vendor)) { + std::string gpuVendor = gpu[keys::gpu::vendor].get(); + std::string gpuModel = gpu[keys::gpu::model].get(); + + // intel integrated graphics + if (gpuVendor.find(keys::gpu::vendor_Intel) != std::string::npos) { + // go mid because GPU + tier = Profiler::Tier::MID; + return true; + } + // AMD gpu + else if (gpuVendor.find(keys::gpu::vendor_AMD) != std::string::npos) { + // TODO: Filter base on the model of AMD + // that is their integrated graphics, go low! + if (gpuModel.find("Vega 3") != std::string::npos) { + tier = Profiler::Tier::LOW; + return true; + } + + // go high because GPU + tier = Profiler::Tier::HIGH; + return true; + } + // NVIDIA gpu + else if (gpuVendor.find(keys::gpu::vendor_NVIDIA) != std::string::npos) { + // TODO: Filter base on the model of NV + // go high because GPU + tier = Profiler::Tier::HIGH; + return true; + } + } + + // Not able to profile + return false; +} \ No newline at end of file diff --git a/libraries/platform/src/platform/Profiler.h b/libraries/platform/src/platform/Profiler.h new file mode 100644 index 0000000000..fea0622c89 --- /dev/null +++ b/libraries/platform/src/platform/Profiler.h @@ -0,0 +1,34 @@ +// +// Profiler.h +// libraries/platform/src/platform +// +// Created by Sam Gateau on 5/22/2019. +// Copyright 2019 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_platform_Profiler_h +#define hifi_platform_Profiler_h + +#include + +namespace platform { + +class Profiler { +public: + enum Tier { + UNKNOWN = 0, + LOW, + MID, + HIGH, + + NumTiers, // not a valid Tier + }; + static const std::array TierNames; + + static Tier profilePlatform(); +}; + +} +#endif diff --git a/libraries/platform/src/AndroidPlatform.cpp b/libraries/platform/src/platform/backend/AndroidPlatform.cpp similarity index 53% rename from libraries/platform/src/AndroidPlatform.cpp rename to libraries/platform/src/platform/backend/AndroidPlatform.cpp index f35674a984..ee5a7e39b9 100644 --- a/libraries/platform/src/AndroidPlatform.cpp +++ b/libraries/platform/src/platform/backend/AndroidPlatform.cpp @@ -7,38 +7,41 @@ // #include "AndroidPlatform.h" -#include "platformJsonKeys.h" +#include "../PlatformKeys.h" #include using namespace platform; void AndroidInstance::enumerateCpu() { json cpu; - cpu["cpuBrand"] = ""; - cpu["cpuModel"] = ""; - cpu["cpuClockSpeed"] = ""; - cpu["cpuNumCores"] = ""; + cpu[keys::cpu::vendor] = ""; + cpu[keys::cpu::model] = ""; + cpu[keys::cpu::clockSpeed] = ""; + cpu[keys::cpu::numCores] = 0; _cpu.push_back(cpu); } void AndroidInstance::enumerateGpu() { GPUIdent* ident = GPUIdent::getInstance(); json gpu = {}; - gpu["gpuName"] = ident->getName().toUtf8().constData(); - gpu["gpuMemory"] = ident->getMemory(); - gpu["gpuDriver"] = ident->getDriver().toUtf8().constData(); - + gpu[keys::gpu::vendor] = ident->getName().toUtf8().constData(); + gpu[keys::gpu::model] = ident->getName().toUtf8().constData(); + gpu[keys::gpu::videoMemory] = ident->getMemory(); + gpu[keys::gpu::driver] = ident->getDriver().toUtf8().constData(); + _gpu.push_back(gpu); _display = ident->getOutput(); } void AndroidInstance::enumerateMemory() { json ram = {}; - ram["totalMemory"]=""; + ram[keys::memTotal]=0; _memory.push_back(ram); } void AndroidInstance::enumerateComputer(){ - //no implememntation at this time + _computer[keys::computer::OS] = keys::computer::OS_ANDROID; + _computer[keys::computer::vendor] = ""; + _computer[keys::computer::model] = ""; } diff --git a/libraries/platform/src/AndroidPlatform.h b/libraries/platform/src/platform/backend/AndroidPlatform.h similarity index 95% rename from libraries/platform/src/AndroidPlatform.h rename to libraries/platform/src/platform/backend/AndroidPlatform.h index 66d80ca1ef..d1496383c0 100644 --- a/libraries/platform/src/AndroidPlatform.h +++ b/libraries/platform/src/platform/backend/AndroidPlatform.h @@ -9,7 +9,7 @@ #ifndef hifi_AndroidPlatform_h #define hifi_AndroidPlatform_h -#include "platformInstance.h" +#include "PlatformInstance.h" namespace platform { class AndroidInstance : public Instance { diff --git a/libraries/platform/src/LinuxPlatform.cpp b/libraries/platform/src/platform/backend/LinuxPlatform.cpp similarity index 50% rename from libraries/platform/src/LinuxPlatform.cpp rename to libraries/platform/src/platform/backend/LinuxPlatform.cpp index aa63eb1e08..356df27e0a 100644 --- a/libraries/platform/src/LinuxPlatform.cpp +++ b/libraries/platform/src/platform/backend/LinuxPlatform.cpp @@ -7,17 +7,21 @@ // #include "LinuxPlatform.h" -#include "platformJsonKeys.h" +#include "../PlatformKeys.h" + +#include +#include +#include #include using namespace platform; + void LinuxInstance::enumerateCpu() { json cpu = {}; - - cpu["cpuBrand"] = ""; - cpu["cpuModel"] = ""; - cpu["cpuClockSpeed"] = ""; - cpu["cpuNumCores"] = ""; + + cpu[keys::cpu::vendor] = CPUIdent::Vendor(); + cpu[keys::cpu::model] = CPUIdent::Brand(); + cpu[keys::cpu::numCores] = std::thread::hardware_concurrency(); _cpu.push_back(cpu); } @@ -25,9 +29,10 @@ void LinuxInstance::enumerateCpu() { void LinuxInstance::enumerateGpu() { GPUIdent* ident = GPUIdent::getInstance(); json gpu = {}; - gpu["gpuName"] = ident->getName().toUtf8().constData(); - gpu["gpuMemory"] = ident->getMemory(); - gpu["gpuDriver"] = ident->getDriver().toUtf8().constData(); + gpu[keys::gpu::vendor] = ident->getName().toUtf8().constData(); + gpu[keys::gpu::model] = ident->getName().toUtf8().constData(); + gpu[keys::gpu::videoMemory] = ident->getMemory(); + gpu[keys::gpu::driver] = ident->getDriver().toUtf8().constData(); _gpu.push_back(gpu); _display = ident->getOutput(); @@ -35,12 +40,15 @@ void LinuxInstance::enumerateGpu() { void LinuxInstance::enumerateMemory() { json ram = {}; - ram["totalMemory"]=""; + ram[keys::memTotal]=0; _memory.push_back(ram); } void LinuxInstance::enumerateComputer(){ - //no implememntation at this time + + _computer[keys::computer::OS] = keys::computer::OS_LINUX; + _computer[keys::computer::vendor] = ""; + _computer[keys::computer::model] = ""; } diff --git a/libraries/platform/src/LinuxPlatform.h b/libraries/platform/src/platform/backend/LinuxPlatform.h similarity index 95% rename from libraries/platform/src/LinuxPlatform.h rename to libraries/platform/src/platform/backend/LinuxPlatform.h index 63db8557f0..1629101f41 100644 --- a/libraries/platform/src/LinuxPlatform.h +++ b/libraries/platform/src/platform/backend/LinuxPlatform.h @@ -9,7 +9,7 @@ #ifndef hifi_LinuxPlatform_h #define hifi_LinuxPlatform_h -#include "platformInstance.h" +#include "PlatformInstance.h" namespace platform { class LinuxInstance : public Instance { diff --git a/libraries/platform/src/platform/backend/MACOSPlatform.cpp b/libraries/platform/src/platform/backend/MACOSPlatform.cpp new file mode 100644 index 0000000000..2607c47d5b --- /dev/null +++ b/libraries/platform/src/platform/backend/MACOSPlatform.cpp @@ -0,0 +1,77 @@ +// +// Created by Amer Cerkic 05/02/2019 +// Copyright 2019 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 "MACOSPlatform.h" +#include "../PlatformKeys.h" + +#include +#include +#include +#include + +#ifdef Q_OS_MAC +#include +#include +#include +#endif + +using namespace platform; + +void MACOSInstance::enumerateCpu() { + json cpu = {}; + + cpu[keys::cpu::vendor] = CPUIdent::Vendor(); + cpu[keys::cpu::model] = CPUIdent::Brand(); + cpu[keys::cpu::numCores] = std::thread::hardware_concurrency(); + + _cpu.push_back(cpu); +} + +void MACOSInstance::enumerateGpu() { + GPUIdent* ident = GPUIdent::getInstance(); + json gpu = {}; + gpu[keys::gpu::vendor] = ident->getName().toUtf8().constData(); + gpu[keys::gpu::model] = ident->getName().toUtf8().constData(); + gpu[keys::gpu::videoMemory] = ident->getMemory(); + gpu[keys::gpu::driver] = ident->getDriver().toUtf8().constData(); + + _gpu.push_back(gpu); + _display = ident->getOutput(); + +} + +void MACOSInstance::enumerateMemory() { + json ram = {}; + +#ifdef Q_OS_MAC + long pages = sysconf(_SC_PHYS_PAGES); + long page_size = sysconf(_SC_PAGE_SIZE); + ram[keys::memTotal] = pages * page_size; +#endif + _memory.push_back(ram); +} + +void MACOSInstance::enumerateComputer(){ + _computer[keys::computer::OS] = keys::computer::OS_MACOS; + _computer[keys::computer::vendor] = keys::computer::vendor_Apple; + +#ifdef Q_OS_MAC + + //get system name + size_t len=0; + sysctlbyname("hw.model",NULL, &len, NULL, 0); + char* model = (char *) malloc(sizeof(char)*len+1); + sysctlbyname("hw.model", model, &len, NULL,0); + + _computer[keys::computer::model]=std::string(model); + + free(model); + +#endif +} + diff --git a/libraries/platform/src/MACOSPlatform.h b/libraries/platform/src/platform/backend/MACOSPlatform.h similarity index 95% rename from libraries/platform/src/MACOSPlatform.h rename to libraries/platform/src/platform/backend/MACOSPlatform.h index 04b8621698..1c66f5d742 100644 --- a/libraries/platform/src/MACOSPlatform.h +++ b/libraries/platform/src/platform/backend/MACOSPlatform.h @@ -9,7 +9,7 @@ #ifndef hifi_MACOSPlatform_h #define hifi_MACOSPlatform_h -#include "platformInstance.h" +#include "PlatformInstance.h" namespace platform { class MACOSInstance : public Instance { diff --git a/libraries/platform/src/platform/backend/Platform.cpp b/libraries/platform/src/platform/backend/Platform.cpp new file mode 100644 index 0000000000..8e9fda30ed --- /dev/null +++ b/libraries/platform/src/platform/backend/Platform.cpp @@ -0,0 +1,130 @@ +// +// Created by Amer Cerkic 05/02/2019 +// Copyright 2019 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 "../Platform.h" +#include "../PlatformKeys.h" + +namespace platform { namespace keys { + namespace cpu { + const char* vendor = "vendor"; + const char* vendor_Intel = "Intel"; + const char* vendor_AMD = "AMD"; + + const char* model = "model"; + const char* clockSpeed = "clockSpeed"; + const char* numCores = "numCores"; + } + namespace gpu { + const char* vendor = "vendor"; + const char* vendor_NVIDIA = "NVIDIA"; + const char* vendor_AMD = "AMD"; + const char* vendor_Intel = "Intel"; + + const char* model = "model"; + const char* videoMemory = "videoMemory"; + const char* driver = "driver"; + } + namespace display { + const char* description = "description"; + const char* name = "deviceName"; + const char* coordsLeft = "coordinatesleft"; + const char* coordsRight = "coordinatesright"; + const char* coordsTop = "coordinatestop"; + const char* coordsBottom = "coordinatesbottom"; + } + const char* memTotal = "memTotal"; + + namespace computer { + const char* OS = "OS"; + const char* OS_WINDOWS = "WINDOWS"; + const char* OS_MACOS = "MACOS"; + const char* OS_LINUX = "LINUX"; + const char* OS_ANDROID = "ANDROID"; + + const char* vendor = "vendor"; + const char* vendor_Apple = "Apple"; + + const char* model = "model"; + + const char* profileTier = "profileTier"; + } +}} + +#include + +#if defined(Q_OS_WIN) +#include "WINPlatform.h" +#elif defined(Q_OS_MAC) +#include "MACOSPlatform.h" +#elif defined(Q_OS_ANDROID) +#include "AndroidPlatform.h" +#elif defined(Q_OS_LINUX) +#include "LinuxPlatform.h" +#endif + +using namespace platform; + +Instance *_instance; + +void platform::create() { +#if defined(Q_OS_WIN) + _instance =new WINInstance(); +#elif defined(Q_OS_MAC) + _instance = new MACOSInstance(); +#elif defined(Q_OS_ANDROID) + _instance= new AndroidInstance(); +#elif defined(Q_OS_LINUX) + _instance= new LinuxInstance(); +#endif +} + +void platform::destroy() { + if(_instance) + delete _instance; +} + +bool platform::enumeratePlatform() { + return _instance->enumeratePlatform(); +} + +int platform::getNumCPUs() { + return _instance->getNumCPUs(); +} + +json platform::getCPU(int index) { + return _instance->getCPU(index); +} + +int platform::getNumGPUs() { + return _instance->getNumGPUs(); +} + +json platform::getGPU(int index) { + return _instance->getGPU(index); +} + +int platform::getNumDisplays() { + return _instance->getNumDisplays(); +} + +json platform::getDisplay(int index) { + return _instance->getDisplay(index); +} + +int platform::getNumMemories() { + return _instance->getNumMemories(); +} + +json platform::getMemory(int index) { + return _instance->getMemory(index); +} + +json platform::getComputer(){ + return _instance->getComputer(); +} diff --git a/libraries/platform/src/platformInstance.cpp b/libraries/platform/src/platform/backend/PlatformInstance.cpp similarity index 52% rename from libraries/platform/src/platformInstance.cpp rename to libraries/platform/src/platform/backend/PlatformInstance.cpp index 5859577748..164fdb924f 100644 --- a/libraries/platform/src/platformInstance.cpp +++ b/libraries/platform/src/platform/backend/PlatformInstance.cpp @@ -7,8 +7,10 @@ // -#include "platformInstance.h" -#include +#include "PlatformInstance.h" + +#include "../PlatformKeys.h" +#include "../Profiler.h" using namespace platform; @@ -17,6 +19,10 @@ bool Instance::enumeratePlatform() { enumerateCpu(); enumerateGpu(); enumerateMemory(); + + // And profile the platform and put the tier in "computer" + _computer[keys::computer::profileTier] = Profiler::TierNames[Profiler::profilePlatform()]; + return true; } @@ -72,3 +78,44 @@ Instance::~Instance() { _display.clear(); } } + + +json Instance::listAllKeys() { + json allKeys; + allKeys.array({{ + keys::cpu::vendor, + keys::cpu::vendor_Intel, + keys::cpu::vendor_AMD, + keys::cpu::model, + keys::cpu::clockSpeed, + keys::cpu::numCores, + + keys::gpu::vendor, + keys::gpu::vendor_NVIDIA, + keys::gpu::vendor_AMD, + keys::gpu::vendor_Intel, + keys::gpu::model, + keys::gpu::videoMemory, + keys::gpu::driver, + + keys::display::description, + keys::display::name, + keys::display::coordsLeft, + keys::display::coordsRight, + keys::display::coordsTop, + keys::display::coordsBottom, + + keys::memTotal, + + keys::computer::OS, + keys::computer::OS_WINDOWS, + keys::computer::OS_MACOS, + keys::computer::OS_LINUX, + keys::computer::OS_ANDROID, + keys::computer::vendor, + keys::computer::vendor_Apple, + keys::computer::model, + keys::computer::profileTier + }}); + return allKeys; +} diff --git a/libraries/platform/src/platformInstance.h b/libraries/platform/src/platform/backend/PlatformInstance.h similarity index 80% rename from libraries/platform/src/platformInstance.h rename to libraries/platform/src/platform/backend/PlatformInstance.h index 8d0a181e3d..52fa9ec3f2 100644 --- a/libraries/platform/src/platformInstance.h +++ b/libraries/platform/src/platform/backend/PlatformInstance.h @@ -19,16 +19,16 @@ class Instance { public: bool virtual enumeratePlatform(); - int getNumCPU() { return (int)_cpu.size(); } + int getNumCPUs() { return (int)_cpu.size(); } json getCPU(int index); - int getNumGPU() { return (int)_gpu.size(); } + int getNumGPUs() { return (int)_gpu.size(); } json getGPU(int index); - int getNumMemory() { return (int)_memory.size(); } + int getNumMemories() { return (int)_memory.size(); } json getMemory(int index); - int getNumDisplay() { return (int)_display.size(); } + int getNumDisplays() { return (int)_display.size(); } json getDisplay(int index); @@ -41,6 +41,8 @@ public: virtual ~Instance(); + static json listAllKeys(); + protected: std::vector _cpu; std::vector _memory; diff --git a/libraries/platform/src/platform/backend/WINPlatform.cpp b/libraries/platform/src/platform/backend/WINPlatform.cpp new file mode 100644 index 0000000000..e34d87d853 --- /dev/null +++ b/libraries/platform/src/platform/backend/WINPlatform.cpp @@ -0,0 +1,66 @@ +// +// Created by Amer Cerkic 05/02/2019 +// Copyright 2019 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 "WINPlatform.h" +#include "../PlatformKeys.h" + +#include +#include +#include +#include + +#ifdef Q_OS_WIN +#include +#endif + +using namespace platform; + +void WINInstance::enumerateCpu() { + json cpu = {}; + + cpu[keys::cpu::vendor] = CPUIdent::Vendor(); + cpu[keys::cpu::model] = CPUIdent::Brand(); + cpu[keys::cpu::numCores] = std::thread::hardware_concurrency(); + + _cpu.push_back(cpu); +} + +void WINInstance::enumerateGpu() { + + GPUIdent* ident = GPUIdent::getInstance(); + + json gpu = {}; + gpu[keys::gpu::vendor] = ident->getName().toUtf8().constData(); + gpu[keys::gpu::model] = ident->getName().toUtf8().constData(); + gpu[keys::gpu::videoMemory] = ident->getMemory(); + gpu[keys::gpu::driver] = ident->getDriver().toUtf8().constData(); + + _gpu.push_back(gpu); + _display = ident->getOutput(); +} + +void WINInstance::enumerateMemory() { + json ram = {}; + +#ifdef Q_OS_WIN + MEMORYSTATUSEX statex; + statex.dwLength = sizeof(statex); + GlobalMemoryStatusEx(&statex); + int totalRam = statex.ullTotalPhys / 1024 / 1024; + ram[platform::keys::memTotal] = totalRam; +#endif + _memory.push_back(ram); +} + +void WINInstance::enumerateComputer(){ + _computer[keys::computer::OS] = keys::computer::OS_WINDOWS; + _computer[keys::computer::vendor] = ""; + _computer[keys::computer::model] = ""; + +} + diff --git a/libraries/platform/src/WINPlatform.h b/libraries/platform/src/platform/backend/WINPlatform.h similarity index 95% rename from libraries/platform/src/WINPlatform.h rename to libraries/platform/src/platform/backend/WINPlatform.h index 828d27ffc3..e540335d94 100644 --- a/libraries/platform/src/WINPlatform.h +++ b/libraries/platform/src/platform/backend/WINPlatform.h @@ -9,7 +9,7 @@ #ifndef hifi_WinPlatform_h #define hifi_WinPlatform_h -#include "platformInstance.h" +#include "PlatformInstance.h" namespace platform { class WINInstance : public Instance { diff --git a/libraries/platform/src/platformJsonKeys.h b/libraries/platform/src/platformJsonKeys.h deleted file mode 100644 index 31d81a829c..0000000000 --- a/libraries/platform/src/platformJsonKeys.h +++ /dev/null @@ -1,37 +0,0 @@ -// -// Created by Amer Cerkic 05/02/2019 -// Copyright 2019 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_PlatformJsonKeys_h -#define hifi_PlatformJsonKeys_h - -namespace platform { - namespace jsonKeys{ -#if 0 - static const char* cpuBrand { "cpuBrand"}; - static const char* cpuModel {"cpuModel"}; - static const char* cpuClockSpeed {"clockSpeed"}; - static const char* cpuNumCores { "numCores"}; - static const char* gpuName {"GpuName"}; - static const char* gpuMemory {"gpuMemory"}; - static const char* gpuDriver {"gpuDriver"}; - static const char* totalMemory {"totalMem"}; - static const char* displayDescription { "description"}; - static const char* displayName {"deviceName"}; - static const char* displayCoordsLeft {"coordinatesleft"}; - static const char* displayCoordsRight { "coordinatesright"}; - static const char* displayCoordsTop { "coordinatestop"}; - static const char* displayCoordsBottom { "coordinatesbottom"}; - static const char* computerModel { "computerModel"}; - -#endif - - } - -} // namespace plaform::jsonKeys - -#endif diff --git a/libraries/procedural/src/procedural/proceduralSkybox.slf b/libraries/procedural/src/procedural/proceduralSkybox.slf index 12e8de9dc3..f938e0b9a2 100644 --- a/libraries/procedural/src/procedural/proceduralSkybox.slf +++ b/libraries/procedural/src/procedural/proceduralSkybox.slf @@ -42,5 +42,5 @@ void main(void) { color = max(color, vec3(0)); // Procedural Shaders are expected to be Gamma corrected so let's bring back the RGB in linear space for the rest of the pipeline color = pow(color, vec3(2.2)); - _fragColor = vec4(color, 0.0); + _fragColor = vec4(color, 1.0); } diff --git a/libraries/render-utils/src/AntialiasingEffect.cpp b/libraries/render-utils/src/AntialiasingEffect.cpp index a445ea2343..180e914d60 100644 --- a/libraries/render-utils/src/AntialiasingEffect.cpp +++ b/libraries/render-utils/src/AntialiasingEffect.cpp @@ -378,6 +378,8 @@ void JitterSample::configure(const Config& config) { } } else if (config.stop) { _sampleSequence.currentIndex = -1; + } else { + _sampleSequence.currentIndex = config.getIndex(); } _scale = config.scale; } @@ -392,10 +394,10 @@ void JitterSample::run(const render::RenderContextPointer& renderContext, Output } } - jitter.x = 0.0f; - jitter.y = 0.0f; if (current >= 0) { jitter = _sampleSequence.offsets[current]; + } else { + jitter = glm::vec2(0.0f); } } diff --git a/libraries/render-utils/src/AntialiasingEffect.h b/libraries/render-utils/src/AntialiasingEffect.h index 936ade043d..7d8bbb44d9 100644 --- a/libraries/render-utils/src/AntialiasingEffect.h +++ b/libraries/render-utils/src/AntialiasingEffect.h @@ -109,7 +109,6 @@ public: void setDebugFXAA(bool debug) { debugFXAAX = (debug ? 0.0f : 1.0f); emit dirty();} bool debugFXAA() const { return (debugFXAAX == 0.0f ? true : false); } - float blend{ 0.25f }; float sharpen{ 0.05f }; diff --git a/libraries/render-utils/src/DeferredLightingEffect.cpp b/libraries/render-utils/src/DeferredLightingEffect.cpp index b8c720e9ca..82b7f3102a 100644 --- a/libraries/render-utils/src/DeferredLightingEffect.cpp +++ b/libraries/render-utils/src/DeferredLightingEffect.cpp @@ -648,6 +648,7 @@ void DefaultLightingSetup::run(const RenderContextPointer& renderContext) { if (!_defaultLight || !_defaultBackground) { auto defaultSkyboxURL = PathUtils::resourcesUrl() + "images/Default-Sky-9-cubemap/Default-Sky-9-cubemap.texmeta.json"; + auto defaultAmbientURL = PathUtils::resourcesUrl() + "images/Default-Sky-9-cubemap/Default-Sky-9-cubemap-ambient.texmeta.json"; if (!_defaultSkyboxNetworkTexture) { PROFILE_RANGE(render, "Process Default Skybox"); @@ -658,7 +659,7 @@ void DefaultLightingSetup::run(const RenderContextPointer& renderContext) { if (!_defaultAmbientNetworkTexture) { PROFILE_RANGE(render, "Process Default Ambient map"); _defaultAmbientNetworkTexture = DependencyManager::get()->getTexture( - defaultSkyboxURL, image::TextureUsage::AMBIENT_TEXTURE); + defaultAmbientURL, image::TextureUsage::AMBIENT_TEXTURE); } if (_defaultSkyboxNetworkTexture && _defaultSkyboxNetworkTexture->isLoaded() && _defaultSkyboxNetworkTexture->getGPUTexture()) { diff --git a/libraries/render-utils/src/Model.cpp b/libraries/render-utils/src/Model.cpp index 938cc4a485..64a46f3c1e 100644 --- a/libraries/render-utils/src/Model.cpp +++ b/libraries/render-utils/src/Model.cpp @@ -1577,9 +1577,11 @@ void Model::applyMaterialMapping() { priorityMapPerResource[shapeID] = ++_priorityMap[shapeID]; } - auto materialLoaded = [this, networkMaterialResource, shapeIDs, priorityMapPerResource, renderItemsKey, primitiveMode, useDualQuaternionSkinning, - modelMeshRenderItemIDs, modelMeshRenderItemShapes, shouldInvalidatePayloadShapeKeyMap]() { - if (networkMaterialResource->isFailed() || networkMaterialResource->parsedMaterials.names.size() == 0) { + std::weak_ptr weakSelf = shared_from_this(); + auto materialLoaded = [networkMaterialResource, shapeIDs, priorityMapPerResource, renderItemsKey, primitiveMode, useDualQuaternionSkinning, + modelMeshRenderItemIDs, modelMeshRenderItemShapes, shouldInvalidatePayloadShapeKeyMap, weakSelf]() { + std::shared_ptr self = weakSelf.lock(); + if (!self || networkMaterialResource->isFailed() || networkMaterialResource->parsedMaterials.names.size() == 0) { return; } render::Transaction transaction; @@ -1607,8 +1609,8 @@ void Model::applyMaterialMapping() { bool invalidatePayloadShapeKey = shouldInvalidatePayloadShapeKeyMap.at(meshIndex); graphics::MaterialLayer material = graphics::MaterialLayer(networkMaterial, priorityMapPerResource.at(shapeID)); { - std::unique_lock lock(_materialMappingMutex); - _materialMapping[shapeID].push_back(material); + std::unique_lock lock(self->_materialMappingMutex); + self->_materialMapping[shapeID].push_back(material); } transaction.updateItem(itemID, [material, renderItemsKey, invalidatePayloadShapeKey, primitiveMode, useDualQuaternionSkinning](ModelMeshPartPayload& data) { diff --git a/libraries/render-utils/src/RenderDeferredTask.cpp b/libraries/render-utils/src/RenderDeferredTask.cpp index d808823d0c..624869bbf5 100644 --- a/libraries/render-utils/src/RenderDeferredTask.cpp +++ b/libraries/render-utils/src/RenderDeferredTask.cpp @@ -248,7 +248,7 @@ void RenderDeferredTask::build(JobModel& task, const render::Varying& input, ren // Debugging task is happening in the "over" layer after tone mapping and just before HUD { // Debug the bounds of the rendered items, still look at the zbuffer - const auto extraDebugBuffers = RenderDeferredTaskDebug::ExtraBuffers(linearDepthTarget, surfaceGeometryFramebuffer, ambientOcclusionFramebuffer, ambientOcclusionFramebuffer, scatteringResource, velocityBuffer); + const auto extraDebugBuffers = RenderDeferredTaskDebug::ExtraBuffers(linearDepthTarget, surfaceGeometryFramebuffer, ambientOcclusionFramebuffer, ambientOcclusionUniforms, scatteringResource, velocityBuffer); const auto debugInputs = RenderDeferredTaskDebug::Input(fetchedItems, shadowTaskOutputs, lightingStageInputs, lightClusters, prepareDeferredOutputs, extraDebugBuffers, deferredFrameTransform, jitter, lightingModel).asVarying(); task.addJob("DebugRenderDeferredTask", debugInputs); diff --git a/libraries/render/src/render/DrawStatus.cpp b/libraries/render/src/render/DrawStatus.cpp index 76ed5aa663..0484c0c125 100644 --- a/libraries/render/src/render/DrawStatus.cpp +++ b/libraries/render/src/render/DrawStatus.cpp @@ -26,7 +26,7 @@ using namespace render; void DrawStatusConfig::dirtyHelper() { - _isEnabled = showNetwork || showDisplay; + _isEnabled = showNetwork || showDisplay || showFade; emit dirty(); } @@ -79,6 +79,7 @@ const gpu::TexturePointer DrawStatus::getStatusIconMap() const { void DrawStatus::configure(const Config& config) { _showDisplay = config.showDisplay; _showNetwork = config.showNetwork; + _showFade = config.showFade; } void DrawStatus::run(const RenderContextPointer& renderContext, const Input& input) { @@ -96,40 +97,70 @@ void DrawStatus::run(const RenderContextPointer& renderContext, const Input& inp int nbItems = 0; render::ItemBounds itemBounds; std::vector> itemStatus; - { - for (size_t i = 0; i < inItems.size(); ++i) { - const auto& item = inItems[i]; - if (!item.bound.isInvalid()) { - if (!item.bound.isNull()) { - itemBounds.emplace_back(render::ItemBound(item.id, item.bound)); - } else { - itemBounds.emplace_back(item.id, AABox(item.bound.getCorner(), 0.1f)); - } - auto& itemScene = scene->getItem(item.id); + for (size_t i = 0; i < inItems.size(); ++i) { + const auto& item = inItems[i]; + if (!item.bound.isInvalid()) { + if (!item.bound.isNull()) { + itemBounds.emplace_back(render::ItemBound(item.id, item.bound)); + } else { + itemBounds.emplace_back(item.id, AABox(item.bound.getCorner(), 0.1f)); + } - auto itemStatusPointer = itemScene.getStatus(); - if (itemStatusPointer) { - itemStatus.push_back(std::pair()); - // Query the current status values, this is where the statusGetter lambda get called - auto&& currentStatusValues = itemStatusPointer->getCurrentValues(); - int valueNum = 0; - for (int vec4Num = 0; vec4Num < NUM_STATUS_VEC4_PER_ITEM; vec4Num++) { - auto& value = (vec4Num ? itemStatus[nbItems].first : itemStatus[nbItems].second); - value = glm::ivec4(Item::Status::Value::INVALID.getPackedData()); - for (int component = 0; component < VEC4_LENGTH; component++) { - valueNum = vec4Num * VEC4_LENGTH + component; - if (valueNum < (int)currentStatusValues.size()) { - value[component] = currentStatusValues[valueNum].getPackedData(); + auto& itemScene = scene->getItem(item.id); + + if (_showNetwork || _showFade) { + const static auto invalid = glm::ivec4(Item::Status::Value::INVALID.getPackedData()); + itemStatus.emplace_back(invalid, invalid); + int vec4Num = 0; + int vec4Component = 0; + + if (_showNetwork) { + auto itemStatusPointer = itemScene.getStatus(); + if (itemStatusPointer) { + // Query the current status values, this is where the statusGetter lambda get called + auto&& currentStatusValues = itemStatusPointer->getCurrentValues(); + for (const auto& statusValue : currentStatusValues) { + if (vec4Num == NUM_STATUS_VEC4_PER_ITEM) { + // Ran out of space + break; + } + + auto& value = (vec4Num == 0 ? itemStatus[nbItems].first : itemStatus[nbItems].second); + value[vec4Component] = statusValue.getPackedData(); + + ++vec4Component; + if (vec4Component == VEC4_LENGTH) { + vec4Component = 0; + ++vec4Num; } } } - } else { - auto invalid = glm::ivec4(Item::Status::Value::INVALID.getPackedData()); - itemStatus.emplace_back(invalid, invalid); } - nbItems++; + + if (_showFade && vec4Num != NUM_STATUS_VEC4_PER_ITEM) { + auto& value = (vec4Num == 0 ? itemStatus[nbItems].first : itemStatus[nbItems].second); + + Item::Status::Value status; + status.setColor(Item::Status::Value::CYAN); + status.setIcon((unsigned char)Item::Status::Icon::SIMULATION_OWNER); + if (itemScene.getTransitionId() != INVALID_INDEX) { + // We have a transition. Show this icon. + status.setScale(1.0f); + } else { + status.setScale(0.0f); + } + value[vec4Component] = status.getPackedData(); + + ++vec4Component; + if (vec4Component == VEC4_LENGTH) { + vec4Component = 0; + ++vec4Num; + } + } } + + nbItems++; } } @@ -169,7 +200,7 @@ void DrawStatus::run(const RenderContextPointer& renderContext, const Input& inp batch.setPipeline(getDrawItemStatusPipeline()); - if (_showNetwork) { + if (_showNetwork || _showFade) { if (!_instanceBuffer) { _instanceBuffer = std::make_shared(); } diff --git a/libraries/render/src/render/DrawStatus.h b/libraries/render/src/render/DrawStatus.h index 2959ca59c5..6e0783f000 100644 --- a/libraries/render/src/render/DrawStatus.h +++ b/libraries/render/src/render/DrawStatus.h @@ -27,10 +27,12 @@ namespace render { bool showDisplay{ false }; bool showNetwork{ false }; + bool showFade{ false }; public slots: void setShowDisplay(bool enabled) { showDisplay = enabled; dirtyHelper(); } void setShowNetwork(bool enabled) { showNetwork = enabled; dirtyHelper(); } + void setShowFade(bool enabled) { showFade = enabled; dirtyHelper(); } signals: void dirty(); @@ -57,6 +59,7 @@ namespace render { protected: bool _showDisplay; // initialized by Config bool _showNetwork; // initialized by Config + bool _showFade; // initialized by Config gpu::Stream::FormatPointer _drawItemFormat; gpu::PipelinePointer _drawItemBoundsPipeline; diff --git a/libraries/render/src/render/Item.h b/libraries/render/src/render/Item.h index 79557e3e44..d5d3e6942a 100644 --- a/libraries/render/src/render/Item.h +++ b/libraries/render/src/render/Item.h @@ -354,6 +354,17 @@ public: // This is Used for monitoring and dynamically adjust the quality class Status { public: + + enum class Icon { + ACTIVE_IN_BULLET = 0, + PACKET_SENT = 1, + PACKET_RECEIVED = 2, + SIMULATION_OWNER = 3, + HAS_ACTIONS = 4, + OTHER_SIMULATION_OWNER = 5, + ENTITY_HOST_TYPE = 6, + NONE = 255 + }; // Status::Value class is the data used to represent the transient information of a status as a square icon // The "icon" is a square displayed in the 3D scene over the render::Item AABB center. diff --git a/libraries/script-engine/src/ScriptEngine.cpp b/libraries/script-engine/src/ScriptEngine.cpp index 7abb63ca1c..309161206c 100644 --- a/libraries/script-engine/src/ScriptEngine.cpp +++ b/libraries/script-engine/src/ScriptEngine.cpp @@ -987,6 +987,31 @@ void ScriptEngine::addEventHandler(const EntityItemID& entityID, const QString& }; }; + /**jsdoc + * The name of an entity event. When the entity event occurs, any function that has been registered for that event via + * {@link Script.addEventHandler} is called with parameters per the entity event. + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
Event NameEntity Event
"enterEntity"{@link Entities.enterEntity}
"leaveEntity"{@link Entities.leaveEntity}
"mousePressOnEntity"{@link Entities.mousePressOnEntity}
"mouseMoveOnEntity"{@link Entities.mouseMoveOnEntity}
"mouseReleaseOnEntity"{@link Entities.mouseReleaseOnEntity}
"clickDownOnEntity"{@link Entities.clickDownOnEntity}
"holdingClickOnEntity"{@link Entities.holdingClickOnEntity}
"clickReleaseOnEntity"{@link Entities.clickReleaseOnEntity}
"hoverEnterEntity"{@link Entities.hoverEnterEntity}
"hoverOverEntity"{@link Entities.hoverOverEntity}
"hoverLeaveEntity"{@link Entities.hoverLeaveEntity}
"collisionWithEntity"{@link Entities.collisionWithEntity}
+ * + * @typedef {string} Script.EntityEvent + */ connect(entities.data(), &EntityScriptingInterface::enterEntity, this, makeSingleEntityHandler("enterEntity")); connect(entities.data(), &EntityScriptingInterface::leaveEntity, this, makeSingleEntityHandler("leaveEntity")); @@ -2147,7 +2172,7 @@ void ScriptEngine::loadEntityScript(const EntityItemID& entityID, const QString& } /**jsdoc - * Triggered when the script starts for a user. + * Triggered when the script starts for a user. See also, {@link Script.entityScriptPreloadFinished}. *

Note: Can only be connected to via this.preload = function (...) { ... } in the entity script.

*
Available in:Client Entity ScriptsServer Entity Scripts
* @function Entities.preload @@ -2161,7 +2186,7 @@ void ScriptEngine::loadEntityScript(const EntityItemID& entityID, const QString& * this.entityID = entityID; * print("Entity ID: " + this.entityID); * }; - * ); + * }); * * var entityID = Entities.addEntity({ * type: "Box", diff --git a/libraries/script-engine/src/ScriptEngine.h b/libraries/script-engine/src/ScriptEngine.h index 3549578ed5..e58609f01d 100644 --- a/libraries/script-engine/src/ScriptEngine.h +++ b/libraries/script-engine/src/ScriptEngine.h @@ -100,6 +100,8 @@ public: }; /**jsdoc + * The Script API provides facilities for working with scripts. + * * @namespace Script * * @hifi-interface @@ -108,7 +110,14 @@ public: * @hifi-server-entity * @hifi-assignment-client * - * @property {string} context + * @property {string} context - The context that the script is running in: + *
    + *
  • "client": An Interface or avatar script.
  • + *
  • "entity_client": A client entity script.
  • + *
  • "entity_server": A server entity script.
  • + *
  • "agent": An assignment client script.
  • + *
+ * Read-only. */ class ScriptEngine : public BaseScriptEngine, public EntitiesScriptEngineProvider { Q_OBJECT @@ -150,9 +159,19 @@ public: QList getListOfEntityScriptIDs(); /**jsdoc - * Stop the current script. + * Stops and unloads the current script. + *

Warning: If an assignment client script, the script gets restarted after stopping.

* @function Script.stop - * @param {boolean} [marshal=false] + * @param {boolean} [marshal=false] - Marshal. + *

Deprecated: This parameter is deprecated and will be removed.

+ * @example Stop a script after 5s. + * Script.setInterval(function () { + * print("Hello"); + * }, 1000); + * + * Script.setTimeout(function () { + * Script.stop(true); + * }, 5000); */ //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // NOTE - this is intended to be a public interface for Agent scripts, and local scripts, but not for EntityScripts @@ -167,18 +186,20 @@ public: /**jsdoc * @function Script.registerGlobalObject - * @param {string} name - * @param {object} object + * @param {string} name - Name. + * @param {object} object - Object. + * @deprecated This function is deprecated and will be removed. */ /// registers a global object by name Q_INVOKABLE void registerGlobalObject(const QString& name, QObject* object); /**jsdoc * @function Script.registerGetterSetter - * @param {string} name - * @param {object} getter - * @param {object} setter - * @param {string} [parent=""] + * @param {string} name - Name. + * @param {function} getter - Getter. + * @param {function} setter - Setter. + * @param {string} [parent=""] - Parent. + * @deprecated This function is deprecated and will be removed. */ /// registers a global getter/setter Q_INVOKABLE void registerGetterSetter(const QString& name, QScriptEngine::FunctionSignature getter, @@ -186,19 +207,21 @@ public: /**jsdoc * @function Script.registerFunction - * @param {string} name - * @param {object} function - * @param {number} [numArguments=-1] + * @param {string} name - Name. + * @param {function} function - Function. + * @param {number} [numArguments=-1] - Number of arguments. + * @deprecated This function is deprecated and will be removed. */ /// register a global function Q_INVOKABLE void registerFunction(const QString& name, QScriptEngine::FunctionSignature fun, int numArguments = -1); /**jsdoc * @function Script.registerFunction - * @param {string} parent - * @param {string} name - * @param {object} function - * @param {number} [numArguments=-1] + * @param {string} parent - Parent. + * @param {string} name - Name. + * @param {function} function - Function. + * @param {number} [numArguments=-1] - Number of arguments. + * @deprecated This function is deprecated and will be removed. */ /// register a function as a method on a previously registered global object Q_INVOKABLE void registerFunction(const QString& parent, const QString& name, QScriptEngine::FunctionSignature fun, @@ -206,27 +229,30 @@ public: /**jsdoc * @function Script.registerValue - * @param {string} name - * @param {object} value + * @param {string} name - Name. + * @param {object} value - Value. + * @deprecated This function is deprecated and will be removed. */ /// registers a global object by name Q_INVOKABLE void registerValue(const QString& valueName, QScriptValue value); /**jsdoc * @function Script.evaluate - * @param {string} program - * @param {string} filename - * @param {number} [lineNumber=-1] - * @returns {object} + * @param {string} program - Program. + * @param {string} filename - File name. + * @param {number} [lineNumber=-1] - Line number. + * @returns {object} Object. + * @deprecated This function is deprecated and will be removed. */ /// evaluate some code in the context of the ScriptEngine and return the result Q_INVOKABLE QScriptValue evaluate(const QString& program, const QString& fileName, int lineNumber = 1); // this is also used by the script tool widget /**jsdoc * @function Script.evaluateInClosure - * @param {object} locals - * @param {object} program - * @returns {object} + * @param {object} locals - Locals. + * @param {object} program - Program. + * @returns {object} Object. + * @deprecated This function is deprecated and will be removed. */ Q_INVOKABLE QScriptValue evaluateInClosure(const QScriptValue& locals, const QScriptProgram& program); @@ -237,38 +263,53 @@ public: bool hasValidScriptSuffix(const QString& scriptFileName); /**jsdoc + * Gets the context that the script is running in: Interface/avatar, client entity, server entity, or assignment client. * @function Script.getContext - * @returns {string} + * @returns {string} The context that the script is running in: + *
    + *
  • "client": An Interface or avatar script.
  • + *
  • "entity_client": A client entity script.
  • + *
  • "entity_server": A server entity script.
  • + *
  • "agent": An assignment client script.
  • + *
*/ Q_INVOKABLE QString getContext() const; /**jsdoc + * Checks whether the script is running as an Interface or avatar script. * @function Script.isClientScript - * @returns {boolean} + * @returns {boolean} true if the script is running as an Interface or avatar script, false if it + * isn't. */ Q_INVOKABLE bool isClientScript() const { return _context == CLIENT_SCRIPT; } /**jsdoc + * Checks whether the application was compiled as a debug build. * @function Script.isDebugMode - * @returns {boolean} + * @returns {boolean} true if the application was compiled as a debug build, false if it was + * compiled as a release build. */ Q_INVOKABLE bool isDebugMode() const; /**jsdoc + * Checks whether the script is running as a client entity script. * @function Script.isEntityClientScript - * @returns {boolean} + * @returns {boolean} true if the script is running as a client entity script, false if it isn't. */ Q_INVOKABLE bool isEntityClientScript() const { return _context == ENTITY_CLIENT_SCRIPT; } /**jsdoc + * Checks whether the script is running as a server entity script. * @function Script.isEntityServerScript - * @returns {boolean} + * @returns {boolean} true if the script is running as a server entity script, false if it isn't. */ Q_INVOKABLE bool isEntityServerScript() const { return _context == ENTITY_SERVER_SCRIPT; } /**jsdoc + * Checks whether the script is running as an assignment client script. * @function Script.isAgentScript - * @returns {boolean} + * @returns {boolean} true if the script is running as an assignment client script, false if it + * isn't. */ Q_INVOKABLE bool isAgentScript() const { return _context == AGENT_SCRIPT; } @@ -276,25 +317,42 @@ public: // NOTE - these are intended to be public interfaces available to scripts /**jsdoc + * Adds a function to the list of functions called when an entity event occurs on a particular entity. * @function Script.addEventHandler - * @param {Uuid} entityID - * @param {string} eventName - * @param {function} handler + * @param {Uuid} entityID - The ID of the entity. + * @param {Script.EntityEvent} eventName - The name of the entity event. + * @param {function} handler - The function to call when the entity event occurs on the entity. It can be either the name + * of a function or an in-line definition. + * @example Report when a mouse press occurs on a particular entity. + * var entityID = Entities.addEntity({ + * type: "Box", + * position: Vec3.sum(MyAvatar.position, Vec3.multiplyQbyV(MyAvatar.orientation, { x: 0, y: 0, z: -5 })), + * dimensions: { x: 0.5, y: 0.5, z: 0.5 }, + * lifetime: 300 // Delete after 5 minutes. + * }); + * + * function reportMousePress(entityID, event) { + * print("Mouse pressed on entity: " + JSON.stringify(event)); + * } + * + * Script.addEventHandler(entityID, "mousePressOnEntity", reportMousePress); */ Q_INVOKABLE void addEventHandler(const EntityItemID& entityID, const QString& eventName, QScriptValue handler); /**jsdoc + * Removes a function from the list of functions called when an entity event occurs on a particular entity. * @function Script.removeEventHandler - * @param {Uuid} entityID - * @param {string} eventName - * @param {function} handler + * @param {Uuid} entityID - The ID of the entity. + * @param {Script.EntityEvent} eventName - The name of the entity event. + * @param {function} handler - The name of the function to no longer call when the entity event occurs on the entity. */ Q_INVOKABLE void removeEventHandler(const EntityItemID& entityID, const QString& eventName, QScriptValue handler); /**jsdoc - * Start a new Interface or entity script. + * Starts running another script in Interface. + *
Available in:Interface ScriptsAvatar Scripts
* @function Script.load - * @param {string} filename - The URL of the script to load. Can be relative to the current script. + * @param {string} filename - The URL of the script to load. This can be relative to the current script's URL. * @example Load a script from another script. * // First file: scriptA.js * print("This is script A"); @@ -303,7 +361,7 @@ public: * print("This is script B"); * Script.load("scriptA.js"); * - * // If you run scriptB.js you should see both scripts in the running scripts list. + * // If you run scriptB.js you should see both scripts in the Running Scripts dialog. * // And you should see the following output: * // This is script B * // This is script A @@ -311,22 +369,24 @@ public: Q_INVOKABLE void load(const QString& loadfile); /**jsdoc - * Include JavaScript from other files in the current script. If a callback is specified the files are loaded and included - * asynchronously, otherwise they are included synchronously (i.e., script execution blocks while the files are included). + * Includes JavaScript from other files in the current script. If a callback is specified, the files are loaded and + * included asynchronously, otherwise they are included synchronously (i.e., script execution blocks while the files are + * included). * @function Script.include + * @variation 0 * @param {string[]} filenames - The URLs of the scripts to include. Each can be relative to the current script. - * @param {function} [callback=null] - The function to call back when the scripts have been included. Can be an in-line - * function or the name of a function. + * @param {function} [callback=null] - The function to call back when the scripts have been included. It can be either the + * name of a function or an in-line definition. */ Q_INVOKABLE void include(const QStringList& includeFiles, QScriptValue callback = QScriptValue()); /**jsdoc - * Include JavaScript from another file in the current script. If a callback is specified the file is loaded and included + * Includes JavaScript from another file in the current script. If a callback is specified, the file is loaded and included * asynchronously, otherwise it is included synchronously (i.e., script execution blocks while the file is included). * @function Script.include - * @param {string} filename - The URL of the script to include. Can be relative to the current script. - * @param {function} [callback=null] - The function to call back when the script has been included. Can be an in-line - * function or the name of a function. + * @param {string} filename - The URL of the script to include. It can be relative to the current script. + * @param {function} [callback=null] - The function to call back when the script has been included. It can be either the + * name of a function or an in-line definition. * @example Include a script file asynchronously. * // First file: scriptA.js * print("This is script A"); @@ -349,16 +409,21 @@ public: // MODULE related methods /**jsdoc + * Provides access to methods or objects provided in an external JavaScript or JSON file. + * See {@link https://docs.highfidelity.com/script/js-tips.html} for further details. * @function Script.require - * @param {string} module + * @param {string} module - The module to use. May be a JavaScript file or the name of a system module such as + * "sppUi". */ Q_INVOKABLE QScriptValue require(const QString& moduleId); /**jsdoc * @function Script.resetModuleCache - * @param {boolean} [deleteScriptCache=false] + * @param {boolean} [deleteScriptCache=false] - Delete script cache. + * @deprecated This function is deprecated and will be removed. */ Q_INVOKABLE void resetModuleCache(bool deleteScriptCache = false); + QScriptValue currentModule(); bool registerModuleWithParent(const QScriptValue& module, const QScriptValue& parent); QScriptValue newModule(const QString& modulePath, const QScriptValue& parent = QScriptValue()); @@ -366,53 +431,53 @@ public: QScriptValue instantiateModule(const QScriptValue& module, const QString& sourceCode); /**jsdoc - * Call a function at a set interval. + * Calls a function repeatedly, at a set interval. * @function Script.setInterval - * @param {function} function - The function to call. Can be an in-line function or the name of a function. + * @param {function} function - The function to call. This can be either the name of a function or an in-line definition. * @param {number} interval - The interval at which to call the function, in ms. - * @returns {object} A handle to the interval timer. Can be used by {@link Script.clearInterval}. + * @returns {object} A handle to the interval timer. This can be used in {@link Script.clearInterval}. * @example Print a message every second. * Script.setInterval(function () { - * print("Timer fired"); + * print("Interval timer fired"); * }, 1000); */ Q_INVOKABLE QObject* setInterval(const QScriptValue& function, int intervalMS); /**jsdoc - * Call a function after a delay. + * Calls a function once, after a delay. * @function Script.setTimeout - * @param {function} function - The function to call. Can be an in-line function or the name of a function. + * @param {function} function - The function to call. This can be either the name of a function or an in-line definition. * @param {number} timeout - The delay after which to call the function, in ms. - * @returns {object} A handle to the timeout timer. Can be used by {@link Script.clearTimeout}. - * @example Print a message after a second. + * @returns {object} A handle to the timeout timer. This can be used in {@link Script.clearTimeout}. + * @example Print a message once, after a second. * Script.setTimeout(function () { - * print("Timer fired"); + * print("Timeout timer fired"); * }, 1000); */ Q_INVOKABLE QObject* setTimeout(const QScriptValue& function, int timeoutMS); /**jsdoc - * Stop an interval timer set by {@link Script.setInterval|setInterval}. + * Stops an interval timer set by {@link Script.setInterval|setInterval}. * @function Script.clearInterval - * @param {object} timer - The interval timer to clear. + * @param {object} timer - The interval timer to stop. * @example Stop an interval timer. * // Print a message every second. * var timer = Script.setInterval(function () { - * print("Timer fired"); + * print("Interval timer fired"); * }, 1000); * * // Stop the timer after 10 seconds. * Script.setTimeout(function () { - * print("Stop timer"); + * print("Stop interval timer"); * Script.clearInterval(timer); * }, 10000); */ Q_INVOKABLE void clearInterval(QObject* timer) { stopTimer(reinterpret_cast(timer)); } /**jsdoc - * Clear a timeout timer set by {@link Script.setTimeout|setTimeout}. + * Stops a timeout timer set by {@link Script.setTimeout|setTimeout}. * @function Script.clearTimeout - * @param {object} timer - The timeout timer to clear. + * @param {object} timer - The timeout timer to stop. * @example Stop a timeout timer. * // Print a message after two seconds. * var timer = Script.setTimeout(function () { @@ -425,34 +490,53 @@ public: Q_INVOKABLE void clearTimeout(QObject* timer) { stopTimer(reinterpret_cast(timer)); } /**jsdoc + * Prints a message to the program log. + *

Alternatively, you can use {@link print}, {@link console.log}, or one of the other {@link console} methods.

* @function Script.print - * @param {string} message + * @param {string} message - The message to print. + */ + /**jsdoc + * Prints a message to the program log. + *

This is an alias of {@link Script.print}.

+ * @function print + * @param {string} message - The message to print. */ Q_INVOKABLE void print(const QString& message); /**jsdoc - * Resolve a relative path to an absolute path. + * Resolves a relative path to an absolute path. The relative path is relative to the script's location. * @function Script.resolvePath * @param {string} path - The relative path to resolve. * @returns {string} The absolute path. + * @example Report the directory and filename of the running script. + * print(Script.resolvePath("")); + * @example Report the directory of the running script. + * print(Script.resolvePath(".")); + * @example Report the path to a file located relative to the running script. + * print(Script.resolvePath("../assets/sounds/hello.wav")); */ Q_INVOKABLE QUrl resolvePath(const QString& path) const; /**jsdoc + * Gets the path to the resources directory for QML files. * @function Script.resourcesPath - * @returns {string} + * @returns {string} The path to the resources directory for QML files. */ Q_INVOKABLE QUrl resourcesPath() const; /**jsdoc + * Starts timing a section of code in order to send usage data about it to High Fidelity. Shouldn't be used outside of the + * standard scripts. * @function Script.beginProfileRange - * @param {string} label + * @param {string} label - A name that identifies the section of code. */ Q_INVOKABLE void beginProfileRange(const QString& label) const; /**jsdoc + * Finishes timing a section of code in order to send usage data about it to High Fidelity. Shouldn't be used outside of + * the standard scripts. * @function Script.endProfileRange - * @param {string} label + * @param {string} label - A name that identifies the section of code. */ Q_INVOKABLE void endProfileRange(const QString& label) const; @@ -460,9 +544,10 @@ public: // Entity Script Related methods /**jsdoc + * Checks whether an entity has an entity script running. * @function Script.isEntityScriptRunning - * @param {Uuid} entityID - * @returns {boolean} + * @param {Uuid} entityID - The ID of the entity. + * @returns {boolean} true if the entity has an entity script running, false if it doesn't. */ Q_INVOKABLE bool isEntityScriptRunning(const EntityItemID& entityID) { QReadLocker locker { &_entityScriptsLock }; @@ -474,60 +559,71 @@ public: /**jsdoc * @function Script.loadEntityScript - * @param {Uuid} entityID - * @param {string} script - * @param {boolean} forceRedownload + * @param {Uuid} entityID - Entity ID. + * @param {string} script - Script. + * @param {boolean} forceRedownload - Force re-download. + * @deprecated This function is deprecated and will be removed. */ Q_INVOKABLE void loadEntityScript(const EntityItemID& entityID, const QString& entityScript, bool forceRedownload); /**jsdoc * @function Script.unloadEntityScript - * @param {Uuid} entityID - * @param {boolean} [shouldRemoveFromMap=false] + * @param {Uuid} entityID - Entity ID. + * @param {boolean} [shouldRemoveFromMap=false] - Should remove from map. + * @deprecated This function is deprecated and will be removed. */ Q_INVOKABLE void unloadEntityScript(const EntityItemID& entityID, bool shouldRemoveFromMap = false); // will call unload method /**jsdoc * @function Script.unloadAllEntityScripts + * @deprecated This function is deprecated and will be removed. */ Q_INVOKABLE void unloadAllEntityScripts(); /**jsdoc + * Calls a method in an entity script. * @function Script.callEntityScriptMethod - * @param {Uuid} entityID - * @param {string} methodName - * @param {string[]} parameters - * @param {Uuid} [remoteCallerID=Uuid.NULL] + * @param {Uuid} entityID - The ID of the entity running the entity script. + * @param {string} methodName - The name of the method to call. + * @param {string[]} [parameters=[]] - The parameters to call the specified method with. + * @param {Uuid} [remoteCallerID=Uuid.NULL] - An ID that identifies the caller. */ Q_INVOKABLE void callEntityScriptMethod(const EntityItemID& entityID, const QString& methodName, const QStringList& params = QStringList(), const QUuid& remoteCallerID = QUuid()) override; /**jsdoc + * Calls a method in an entity script. * @function Script.callEntityScriptMethod - * @param {Uuid} entityID - * @param {string} methodName - * @param {PointerEvent} event + * @param {Uuid} entityID - Entity ID. + * @param {string} methodName - Method name. + * @param {PointerEvent} event - Pointer event. + * @deprecated This function is deprecated and will be removed. */ Q_INVOKABLE void callEntityScriptMethod(const EntityItemID& entityID, const QString& methodName, const PointerEvent& event); /**jsdoc + * Calls a method in an entity script. * @function Script.callEntityScriptMethod - * @param {Uuid} entityID - * @param {string} methodName - * @param {Uuid} otherID - * @param {Collision} collision + * @param {Uuid} entityID - Entity ID. + * @param {string} methodName - Method name. + * @param {Uuid} otherID - Other entity ID. + * @param {Collision} collision - Collision. + * @deprecated This function is deprecated and will be removed. */ Q_INVOKABLE void callEntityScriptMethod(const EntityItemID& entityID, const QString& methodName, const EntityItemID& otherID, const Collision& collision); /**jsdoc + * Manually runs the JavaScript garbage collector which reclaims memory by disposing of objects that are no longer + * reachable. * @function Script.requestGarbageCollection */ Q_INVOKABLE void requestGarbageCollection() { collectGarbage(); } /**jsdoc * @function Script.generateUUID - * @returns {Uuid} + * @returns {Uuid} A new UUID. + * @deprecated This function is deprecated and will be removed. Use {@link Uuid.generate} instead. */ Q_INVOKABLE QUuid generateUUID() { return QUuid::createUuid(); } @@ -573,7 +669,7 @@ public slots: /**jsdoc * @function Script.callAnimationStateHandler - * @param {function} callback - Callback. + * @param {function} callback - Callback function. * @param {object} parameters - Parameters. * @param {string[]} names - Names. * @param {boolean} useNames - Use names. @@ -584,7 +680,8 @@ public slots: /**jsdoc * @function Script.updateMemoryCost - * @param {number} deltaSize + * @param {number} deltaSize - Delta size. + * @deprecated This function is deprecated and will be removed. */ void updateMemoryCost(const qint64&); @@ -592,31 +689,37 @@ signals: /**jsdoc * @function Script.scriptLoaded - * @param {string} filename + * @param {string} filename - File name. * @returns {Signal} + * @deprecated This signal is deprecated and will be removed. */ void scriptLoaded(const QString& scriptFilename); /**jsdoc * @function Script.errorLoadingScript - * @param {string} filename + * @param {string} filename - File name. * @returns {Signal} + * @deprecated This signal is deprecated and will be removed. */ void errorLoadingScript(const QString& scriptFilename); /**jsdoc - * Triggered regularly at a system-determined frequency. + * Triggered frequently at a system-determined interval. * @function Script.update * @param {number} deltaTime - The time since the last update, in s. * @returns {Signal} + * @example Report script update intervals. + * Script.update.connect(function (deltaTime) { + * print("Update: " + deltaTime); + * }); */ void update(float deltaTime); /**jsdoc - * Triggered when the script is ending. + * Triggered when the script is stopping. * @function Script.scriptEnding * @returns {Signal} - * @example Connect to the scriptEnding signal. + * @example Report when a script is stopping. * print("Script started"); * * Script.scriptEnding.connect(function () { @@ -632,52 +735,60 @@ signals: /**jsdoc * @function Script.finished - * @param {string} filename - * @param {object} engine + * @param {string} filename - File name. + * @param {object} engine - Engine. * @returns {Signal} + * @deprecated This signal is deprecated and will be removed. */ void finished(const QString& fileNameString, ScriptEnginePointer); /**jsdoc * @function Script.cleanupMenuItem - * @param {string} menuItem + * @param {string} menuItem - Menu item. * @returns {Signal} + * @deprecated This signal is deprecated and will be removed. */ void cleanupMenuItem(const QString& menuItemString); /**jsdoc + * Triggered when a script prints a message to the program log via {@link Script.print}, {@link print}, + * {@link console.log}, {@link console.info}, {@link console.warn}, {@link console.error}, or {@link console.debug}. * @function Script.printedMessage - * @param {string} message - * @param {string} scriptName + * @param {string} message - The message. + * @param {string} scriptName - The name of the script that generated the message. * @returns {Signal} */ void printedMessage(const QString& message, const QString& scriptName); /**jsdoc + * Triggered when a script generates an error or {@link console.error} is called. * @function Script.errorMessage - * @param {string} message - * @param {string} scriptName + * @param {string} message - The error message. + * @param {string} scriptName - The name of the script that generated the error message. * @returns {Signal} */ void errorMessage(const QString& message, const QString& scriptName); /**jsdoc + * Triggered when a script generates a warning or {@link console.warn} is called. * @function Script.warningMessage - * @param {string} message - * @param {string} scriptName + * @param {string} message - The warning message. + * @param {string} scriptName - The name of the script that generated the warning message. * @returns {Signal} */ void warningMessage(const QString& message, const QString& scriptName); /**jsdoc + * Triggered when a script generates an information message or {@link console.info} is called. * @function Script.infoMessage - * @param {string} message - * @param {string} scriptName + * @param {string} message - The information message. + * @param {string} scriptName - The name of the script that generated the information message. * @returns {Signal} */ void infoMessage(const QString& message, const QString& scriptName); /**jsdoc + * Triggered when the running state of the script changes, e.g., from running to stopping. * @function Script.runningStateChanged * @returns {Signal} */ @@ -686,26 +797,30 @@ signals: /**jsdoc * @function Script.clearDebugWindow * @returns {Signal} + * @deprecated This signal is deprecated and will be removed. */ void clearDebugWindow(); /**jsdoc * @function Script.loadScript - * @param {string} scriptName - * @param {boolean} isUserLoaded + * @param {string} scriptName - Script name. + * @param {boolean} isUserLoaded - Is user loaded. * @returns {Signal} + * @deprecated This signal is deprecated and will be removed. */ void loadScript(const QString& scriptName, bool isUserLoaded); /**jsdoc * @function Script.reloadScript - * @param {string} scriptName - * @param {boolean} isUserLoaded + * @param {string} scriptName - Script name. + * @param {boolean} isUserLoaded - Is user loaded. * @returns {Signal} + * @deprecated This signal is deprecated and will be removed. */ void reloadScript(const QString& scriptName, bool isUserLoaded); /**jsdoc + * Triggered when the script has stopped. * @function Script.doneRunning * @returns {Signal} */ @@ -714,14 +829,35 @@ signals: /**jsdoc * @function Script.entityScriptDetailsUpdated * @returns {Signal} + * @deprecated This signal is deprecated and will be removed. */ // Emitted when an entity script is added or removed, or when the status of an entity // script is updated (goes from RUNNING to ERROR_RUNNING_SCRIPT, for example) void entityScriptDetailsUpdated(); /**jsdoc + * Triggered when the script starts for the user. See also, {@link Entities.preload}. + *
Available in:Client Entity ScriptsServer Entity Scripts
* @function Script.entityScriptPreloadFinished + * @param {Uuid} entityID - The ID of the entity that the script is running in. * @returns {Signal} + * @example Get the ID of the entity that a client entity script is running in. + * var entityScript = (function () { + * this.entityID = Uuid.NULL; + * + * Script.entityScriptPreloadFinished.connect(function (entityID) { + * this.entityID = entityID; + * print("Entity ID: " + this.entityID); + * }); + * + * var entityID = Entities.addEntity({ + * type: "Box", + * position: Vec3.sum(MyAvatar.position, Vec3.multiplyQbyV(MyAvatar.orientation, { x: 0, y: 0, z: -5 })), + * dimensions: { x: 0.5, y: 0.5, z: 0.5 }, + * color: { red: 255, green: 0, blue: 0 }, + * script: "(" + entityScript + ")", // Could host the script on a Web server instead. + * lifetime: 300 // Delete after 5 minutes. + * }); */ // Emitted when an entity script has finished running preload void entityScriptPreloadFinished(const EntityItemID& entityID); @@ -731,16 +867,18 @@ protected: /**jsdoc * @function Script.executeOnScriptThread - * @param {object} function - * @param {ConnectionType} [type=2] + * @param {function} function - Function. + * @param {ConnectionType} [type=2] - Connection type. + * @deprecated This function is deprecated and will be removed. */ Q_INVOKABLE void executeOnScriptThread(std::function function, const Qt::ConnectionType& type = Qt::QueuedConnection ); /**jsdoc * @function Script._requireResolve - * @param {string} module - * @param {string} [relativeTo=""] - * @returns {string} + * @param {string} module - Module. + * @param {string} [relativeTo=""] - Relative to. + * @returns {string} Result. + * @deprecated This function is deprecated and will be removed. */ // note: this is not meant to be called directly, but just to have QMetaObject take care of wiring it up in general; // then inside of init() we just have to do "Script.require.resolve = Script._requireResolve;" @@ -763,12 +901,13 @@ protected: /**jsdoc * @function Script.entityScriptContentAvailable - * @param {Uuid} entityID - * @param {string} scriptOrURL - * @param {string} contents - * @param {boolean} isURL - * @param {boolean} success - * @param {string} status + * @param {Uuid} entityID - Entity ID. + * @param {string} scriptOrURL - Path. + * @param {string} contents - Contents. + * @param {boolean} isURL - Is a URL. + * @param {boolean} success - Success. + * @param {string} status - Status. + * @deprecated This function is deprecated and will be removed. */ Q_INVOKABLE void entityScriptContentAvailable(const EntityItemID& entityID, const QString& scriptOrURL, const QString& contents, bool isURL, bool success, const QString& status); diff --git a/libraries/shared/src/BaseScriptEngine.h b/libraries/shared/src/BaseScriptEngine.h index 8820a386bf..443c7b0500 100644 --- a/libraries/shared/src/BaseScriptEngine.h +++ b/libraries/shared/src/BaseScriptEngine.h @@ -31,8 +31,32 @@ public: BaseScriptEngine() {} + /**jsdoc + * @function Script.lintScript + * @param {string} sourceCode - Source code. + * @param {string} fileName - File name. + * @param {number} [lineNumber=1] - Line number. + * @returns {object} Object. + * @deprecated This function is deprecated and will be removed. + */ Q_INVOKABLE QScriptValue lintScript(const QString& sourceCode, const QString& fileName, const int lineNumber = 1); + + /**jsdoc + * @function Script.makeError + * @param {object} [other] - Other. + * @param {string} [type="Error"] - Error. + * @returns {object} Object. + * @deprecated This function is deprecated and will be removed. + */ Q_INVOKABLE QScriptValue makeError(const QScriptValue& other = QScriptValue(), const QString& type = "Error"); + + /**jsdoc + * @function Script.formatExecption + * @param {object} exception - Exception. + * @param {boolean} inludeExtendeDetails - Include extended details. + * @returns {string} String. + * @deprecated This function is deprecated and will be removed. + */ Q_INVOKABLE QString formatException(const QScriptValue& exception, bool includeExtendedDetails); QScriptValue cloneUncaughtException(const QString& detail = QString()); @@ -48,6 +72,25 @@ public: // helper to detect and log warnings when other code invokes QScriptEngine/BaseScriptEngine in thread-unsafe ways static bool IS_THREADSAFE_INVOCATION(const QThread *thread, const QString& method); signals: + /**jsdoc + * @function Script.signalHandlerException + * @param {object} exception - Exception. + * @returns {Signal} + * @deprecated This signal is deprecated and will be removed. + */ + // Script.signalHandlerException is exposed by QScriptEngine. + + /**jsdoc + * Triggered when a script generates an unhandled exception. + * @function Script.unhandledException + * @param {object} exception - The details of the exception. + * @returns {Signal} + * @example Report the details of an unhandled exception. + * Script.unhandledException.connect(function (exception) { + * print("Unhandled exception: " + JSON.stringify(exception)); + * }); + * var properties = JSON.parse("{ x: 1"); // Invalid JSON string. + */ void unhandledException(const QScriptValue& exception); protected: diff --git a/libraries/shared/src/CPUIdent.cpp b/libraries/shared/src/CPUIdent.cpp index 718b01b196..b11ba8950d 100644 --- a/libraries/shared/src/CPUIdent.cpp +++ b/libraries/shared/src/CPUIdent.cpp @@ -10,7 +10,16 @@ #include "CPUIdent.h" -#ifdef Q_OS_WIN +#include +#include + +#include "CPUDetect.h" +void getCPUID(uint32_t* p, uint32_t eax) { + cpuidex((int*) p, (int) eax, 0); +} +void getCPUIDEX(uint32_t* p, uint32_t eax, uint32_t ecx) { + cpuidex((int*) p, (int) eax, (int) ecx); +} const CPUIdent::CPUIdent_Internal CPUIdent::CPU_Rep; @@ -72,4 +81,86 @@ std::vector CPUIdent::getAllFeatures() { return features; }; -#endif + +CPUIdent::CPUIdent_Internal::CPUIdent_Internal() + : nIds_{ 0 }, + nExIds_{ 0 }, + isIntel_{ false }, + isAMD_{ false }, + f_1_ECX_{ 0 }, + f_1_EDX_{ 0 }, + f_7_EBX_{ 0 }, + f_7_ECX_{ 0 }, + f_81_ECX_{ 0 }, + f_81_EDX_{ 0 }, + data_{}, + extdata_{} + { + //int cpuInfo[4] = {-1}; + std::array cpui; + + // Calling __cpuid with 0x0 as the function_id argument + // gets the number of the highest valid function ID. + getCPUID(cpui.data(), 0); + nIds_ = cpui[0]; + + for (uint32_t i = 0; i <= nIds_; ++i) { + getCPUIDEX(cpui.data(), i, 0); + data_.push_back(cpui); + } + + // Capture vendor string + char vendor[0x20]; + memset(vendor, 0, sizeof(vendor)); + *reinterpret_cast(vendor) = data_[0][1]; + *reinterpret_cast(vendor + 4) = data_[0][3]; + *reinterpret_cast(vendor + 8) = data_[0][2]; + vendor_ = vendor; + if (vendor_ == "GenuineIntel") { + isIntel_ = true; + } + else if (vendor_ == "AuthenticAMD") { + isAMD_ = true; + } + + // load bitset with flags for function 0x00000001 + if (nIds_ >= 1) { + f_1_ECX_ = data_[1][2]; + f_1_EDX_ = data_[1][3]; + } + + // load bitset with flags for function 0x00000007 + if (nIds_ >= 7) { + f_7_EBX_ = data_[7][1]; + f_7_ECX_ = data_[7][2]; + } + + // Calling __cpuid with 0x80000000 as the function_id argument + // gets the number of the highest valid extended ID. + getCPUID(cpui.data(), 0x80000000); + nExIds_ = cpui[0]; + + char brand[0x40]; + memset(brand, 0, sizeof(brand)); + + for (uint32_t i = 0x80000000; i <= nExIds_; ++i) { + getCPUIDEX(cpui.data(), i, 0); + extdata_.push_back(cpui); + } + + // load bitset with flags for function 0x80000001 + if (nExIds_ >= 0x80000001) { + f_81_ECX_ = extdata_[1][2]; + f_81_EDX_ = extdata_[1][3]; + } + + // Interpret CPU brand string if reported + if (nExIds_ >= 0x80000004) { + memcpy(brand, extdata_[2].data(), sizeof(cpui)); + memcpy(brand + 16, extdata_[3].data(), sizeof(cpui)); + memcpy(brand + 32, extdata_[4].data(), sizeof(cpui)); + brand_ = brand; + } +} + + diff --git a/libraries/shared/src/CPUIdent.h b/libraries/shared/src/CPUIdent.h index 32668149f4..dbf0c3ea91 100644 --- a/libraries/shared/src/CPUIdent.h +++ b/libraries/shared/src/CPUIdent.h @@ -18,17 +18,11 @@ #ifndef hifi_CPUIdent_h #define hifi_CPUIdent_h -#include - #include #include #include #include -#ifdef Q_OS_WIN - -#include - class CPUIdent { // forward declarations @@ -109,88 +103,10 @@ private: class CPUIdent_Internal { public: - CPUIdent_Internal() - : nIds_ { 0 }, - nExIds_ { 0 }, - isIntel_ { false }, - isAMD_ { false }, - f_1_ECX_ { 0 }, - f_1_EDX_ { 0 }, - f_7_EBX_ { 0 }, - f_7_ECX_ { 0 }, - f_81_ECX_ { 0 }, - f_81_EDX_ { 0 }, - data_ {}, - extdata_ {} - { - //int cpuInfo[4] = {-1}; - std::array cpui; + CPUIdent_Internal(); - // Calling __cpuid with 0x0 as the function_id argument - // gets the number of the highest valid function ID. - __cpuid(cpui.data(), 0); - nIds_ = cpui[0]; - - for (int i = 0; i <= nIds_; ++i) { - __cpuidex(cpui.data(), i, 0); - data_.push_back(cpui); - } - - // Capture vendor string - char vendor[0x20]; - memset(vendor, 0, sizeof(vendor)); - *reinterpret_cast(vendor) = data_[0][1]; - *reinterpret_cast(vendor + 4) = data_[0][3]; - *reinterpret_cast(vendor + 8) = data_[0][2]; - vendor_ = vendor; - if (vendor_ == "GenuineIntel") { - isIntel_ = true; - } else if (vendor_ == "AuthenticAMD") { - isAMD_ = true; - } - - // load bitset with flags for function 0x00000001 - if (nIds_ >= 1) { - f_1_ECX_ = data_[1][2]; - f_1_EDX_ = data_[1][3]; - } - - // load bitset with flags for function 0x00000007 - if (nIds_ >= 7) { - f_7_EBX_ = data_[7][1]; - f_7_ECX_ = data_[7][2]; - } - - // Calling __cpuid with 0x80000000 as the function_id argument - // gets the number of the highest valid extended ID. - __cpuid(cpui.data(), 0x80000000); - nExIds_ = cpui[0]; - - char brand[0x40]; - memset(brand, 0, sizeof(brand)); - - for (int i = 0x80000000; i <= nExIds_; ++i) { - __cpuidex(cpui.data(), i, 0); - extdata_.push_back(cpui); - } - - // load bitset with flags for function 0x80000001 - if (nExIds_ >= 0x80000001) { - f_81_ECX_ = extdata_[1][2]; - f_81_EDX_ = extdata_[1][3]; - } - - // Interpret CPU brand string if reported - if (nExIds_ >= 0x80000004) { - memcpy(brand, extdata_[2].data(), sizeof(cpui)); - memcpy(brand + 16, extdata_[3].data(), sizeof(cpui)); - memcpy(brand + 32, extdata_[4].data(), sizeof(cpui)); - brand_ = brand; - } - }; - - int nIds_; - int nExIds_; + uint32_t nIds_; + uint32_t nExIds_; std::string vendor_; std::string brand_; bool isIntel_; @@ -201,12 +117,9 @@ private: std::bitset<32> f_7_ECX_; std::bitset<32> f_81_ECX_; std::bitset<32> f_81_EDX_; - std::vector> data_; - std::vector> extdata_; + std::vector> data_; + std::vector> extdata_; }; - }; -#endif - #endif // hifi_CPUIdent_h diff --git a/libraries/shared/src/GPUIdent.cpp b/libraries/shared/src/GPUIdent.cpp index 773e40aaee..d5c2f3ec6c 100644 --- a/libraries/shared/src/GPUIdent.cpp +++ b/libraries/shared/src/GPUIdent.cpp @@ -28,8 +28,8 @@ #endif -#include +#include #include "SharedLogging.h" GPUIdent GPUIdent::_instance {}; diff --git a/libraries/ui-plugins/src/ui-plugins/PluginContainer.cpp b/libraries/ui-plugins/src/ui-plugins/PluginContainer.cpp index 58dc971cb9..0c71d4fa69 100644 --- a/libraries/ui-plugins/src/ui-plugins/PluginContainer.cpp +++ b/libraries/ui-plugins/src/ui-plugins/PluginContainer.cpp @@ -72,6 +72,11 @@ struct MenuCache { } flushCache(menu); MenuWrapper* parentItem = menu->getMenu(path); + if (!parentItem) { + qWarning() << "Attempted to add item to non-existent path " << path; + return; + } + QAction* action = menu->addActionToQMenuAndActionHash(parentItem, name); if (!groupName.isEmpty()) { QActionGroup* group{ nullptr }; diff --git a/libraries/ui/src/ui/OffscreenQmlSurface.cpp b/libraries/ui/src/ui/OffscreenQmlSurface.cpp index ec0a011bd0..ec0fad5ff0 100644 --- a/libraries/ui/src/ui/OffscreenQmlSurface.cpp +++ b/libraries/ui/src/ui/OffscreenQmlSurface.cpp @@ -307,6 +307,13 @@ void OffscreenQmlSurface::onRootContextCreated(QQmlContext* qmlContext) { #endif } +void OffscreenQmlSurface::applyWhiteList(const QUrl& url, QQmlContext* context) { + QList callbacks = getQmlWhitelist()->getCallbacksForUrl(url); + for(const auto& callback : callbacks){ + callback(context); + } +} + QQmlContext* OffscreenQmlSurface::contextForUrl(const QUrl& qmlSource, QQuickItem* parent, bool forceNewContext) { // Get any whitelist functionality QList callbacks = getQmlWhitelist()->getCallbacksForUrl(qmlSource); diff --git a/libraries/ui/src/ui/OffscreenQmlSurface.h b/libraries/ui/src/ui/OffscreenQmlSurface.h index b8c6808afa..8bb22a8c79 100644 --- a/libraries/ui/src/ui/OffscreenQmlSurface.h +++ b/libraries/ui/src/ui/OffscreenQmlSurface.h @@ -26,7 +26,8 @@ public: static void addWhitelistContextHandler(const std::initializer_list& urls, const QmlContextCallback& callback); static void addWhitelistContextHandler(const QUrl& url, const QmlContextCallback& callback) { addWhitelistContextHandler({ { url } }, callback); }; - + static void applyWhiteList(const QUrl& url,QQmlContext* context); + bool isFocusText() const { return _focusText; } bool getCleaned() { return _isCleaned; } @@ -36,7 +37,8 @@ public: Q_INVOKABLE void lowerKeyboard(); PointerEvent::EventType choosePointerEventType(QEvent::Type type); Q_INVOKABLE unsigned int deviceIdByTouchPoint(qreal x, qreal y); - + + signals: void focusObjectChanged(QObject* newFocus); void focusTextChanged(bool focusText); diff --git a/prebuild.py b/prebuild.py index b401c94e7f..1fb96290b3 100644 --- a/prebuild.py +++ b/prebuild.py @@ -91,6 +91,7 @@ def parse_args(): parser.add_argument('--debug', action='store_true') parser.add_argument('--force-bootstrap', action='store_true') parser.add_argument('--force-build', action='store_true') + parser.add_argument('--release-type', type=str, default="DEV", help="DEV, PR, or PRODUCTION") parser.add_argument('--vcpkg-root', type=str, help='The location of the vcpkg distribution') parser.add_argument('--build-root', required=True, type=str, help='The location of the cmake build') parser.add_argument('--ports-path', type=str, default=defaultPortsPath) diff --git a/scripts/developer/utilities/lib/prop/PropGroup.qml b/scripts/developer/utilities/lib/prop/PropGroup.qml index 384b50ecad..22facb71c6 100644 --- a/scripts/developer/utilities/lib/prop/PropGroup.qml +++ b/scripts/developer/utilities/lib/prop/PropGroup.qml @@ -38,6 +38,15 @@ PropFolderPanel { proItem['type'] = typeof(proItem.object[proItem.property]) } switch(proItem.type) { + case 'string': + case 'PropString': { + var component = Qt.createComponent("PropString.qml"); + component.createObject(propItemsContainer, { + "label": proItem.property, + "object": proItem.object, + "property": proItem.property + }) + } break; case 'boolean': case 'PropBool': { var component = Qt.createComponent("PropBool.qml"); @@ -57,6 +66,7 @@ PropFolderPanel { "min": (proItem["min"] !== undefined ? proItem.min : 0.0), "max": (proItem["max"] !== undefined ? proItem.max : 1.0), "integer": (proItem["integral"] !== undefined ? proItem.integral : false), + "readOnly": (proItem["readOnly"] !== undefined ? proItem["readOnly"] : false), }) } break; case 'PropEnum': { @@ -97,6 +107,22 @@ PropFolderPanel { } } } + + function populateFromObjectProps(object) { + var propsModel = [] + var props = Object.keys(object); + + for (var p in props) { + var o = {}; + o["object"] = object + o["property"] = props[p]; + // o["readOnly"] = true; + o["type"] = "string"; + propsModel.push(o) + } + root.updatePropItems(root.propItemsPanel, propsModel); + } + Component.onCompleted: { } } diff --git a/scripts/developer/utilities/lib/prop/PropItem.qml b/scripts/developer/utilities/lib/prop/PropItem.qml index 339ff10422..03eabae39b 100644 --- a/scripts/developer/utilities/lib/prop/PropItem.qml +++ b/scripts/developer/utilities/lib/prop/PropItem.qml @@ -18,12 +18,16 @@ Item { // Prop item is designed to author an object[property]: property var object: {} property string property: "" + property bool readOnly: false // value is accessed through the "valueVarSetter" and "valueVarGetter" // By default, these just go get or set the value from the object[property] // function defaultGet() { return root.object[root.property]; } function defaultSet(value) { root.object[root.property] = value; } + // function defaultSetReadOnly(value) { log ( "read only " + property + ", NOT setting to " + value); } + // function defaultSetReadOnly(value) {} + // property var valueVarSetter: (root.readOnly ? defaultSetReadOnly : defaultSet) property var valueVarSetter: defaultSet property var valueVarGetter: defaultGet diff --git a/scripts/developer/utilities/lib/prop/PropScalar.qml b/scripts/developer/utilities/lib/prop/PropScalar.qml index 684dd4fed4..4c569eb57e 100644 --- a/scripts/developer/utilities/lib/prop/PropScalar.qml +++ b/scripts/developer/utilities/lib/prop/PropScalar.qml @@ -20,16 +20,12 @@ PropItem { property bool integral: false property var numDigits: 2 - property alias valueVar : sliderControl.value property alias min: sliderControl.minimumValue property alias max: sliderControl.maximumValue - - property bool showValue: true - - + signal valueChanged(real value) Component.onCompleted: { @@ -42,11 +38,11 @@ PropItem { anchors.left: root.splitter.right anchors.verticalCenter: root.verticalCenter - width: root.width * global.valueAreaWidthScale + width: root.width * (root.readOnly ? 1.0 : global.valueAreaWidthScale) horizontalAlignment: global.valueTextAlign height: global.slimHeight - text: sliderControl.value.toFixed(root.integral ? 0 : root.numDigits) + text: root.valueVarGetter().toFixed(root.integral ? 0 : root.numDigits) background: Rectangle { color: global.color @@ -58,12 +54,13 @@ PropItem { HifiControls.Slider { id: sliderControl + visible: !root.readOnly stepSize: root.integral ? 1.0 : 0.0 anchors.left: valueLabel.right anchors.right: root.right anchors.verticalCenter: root.verticalCenter - onValueChanged: { root.valueVarSetter(value) } + onValueChanged: { if (!root.readOnly) { root.valueVarSetter(value)} } } diff --git a/scripts/developer/utilities/lib/prop/PropString.qml b/scripts/developer/utilities/lib/prop/PropString.qml new file mode 100644 index 0000000000..7b7cbc4c91 --- /dev/null +++ b/scripts/developer/utilities/lib/prop/PropString.qml @@ -0,0 +1,41 @@ +// +// PropItem.qml +// +// Created by Sam Gateau on 3/2/2019 +// Copyright 2019 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or https://www.apache.org/licenses/LICENSE-2.0.html +// + +import QtQuick 2.7 + +import controlsUit 1.0 as HifiControls + +PropItem { + Global { id: global } + id: root + + // Scalar Prop + property bool integral: false + property var numDigits: 2 + + PropLabel { + id: valueLabel + + anchors.left: root.splitter.right + anchors.right: root.right + anchors.verticalCenter: root.verticalCenter + horizontalAlignment: global.valueTextAlign + height: global.slimHeight + + text: root.valueVarGetter(); + + background: Rectangle { + color: global.color + border.color: global.colorBorderLight + border.width: global.valueBorderWidth + radius: global.valueBorderRadius + } + } +} diff --git a/scripts/developer/utilities/lib/prop/qmldir b/scripts/developer/utilities/lib/prop/qmldir index c67ab6a41a..e09785846d 100644 --- a/scripts/developer/utilities/lib/prop/qmldir +++ b/scripts/developer/utilities/lib/prop/qmldir @@ -10,5 +10,6 @@ PropFolderPanel 1.0 style/PiFolderPanel.qml PropItem 1.0 PropItem.qml PropScalar 1.0 PropScalar.qml +PropString 1.0 PropString.qml PropEnum 1.0 PropEnum.qml PropColor 1.0 PropColor.qml diff --git a/scripts/developer/utilities/lib/prop/style/Global.qml b/scripts/developer/utilities/lib/prop/style/Global.qml index 4cdee70244..bbc8acfa0b 100644 --- a/scripts/developer/utilities/lib/prop/style/Global.qml +++ b/scripts/developer/utilities/lib/prop/style/Global.qml @@ -24,6 +24,7 @@ Item { readonly property real horizontalMargin: 4 readonly property color color: hifi.colors.baseGray + readonly property color colorBack: hifi.colors.baseGray readonly property color colorBackShadow: hifi.colors.baseGrayShadow readonly property color colorBackHighlight: hifi.colors.baseGrayHighlight readonly property color colorBorderLight: hifi.colors.lightGray diff --git a/scripts/developer/utilities/render/luci.qml b/scripts/developer/utilities/render/luci.qml index a904ec52fc..2dc8fda081 100644 --- a/scripts/developer/utilities/render/luci.qml +++ b/scripts/developer/utilities/render/luci.qml @@ -31,7 +31,7 @@ Rectangle { clip: true Column { - width: render.width + width: parent.width Prop.PropFolderPanel { label: "Shading Model" panelFrameData: Component { @@ -87,14 +87,14 @@ Rectangle { } } } - Jet.TaskPropView { + /* Jet.TaskPropView { id: "le" jobPath: "" label: "Le Render Engine" // anchors.left: parent.left // anchors.right: parent.right - } + }*/ } } } \ No newline at end of file diff --git a/scripts/developer/utilities/render/platform.js b/scripts/developer/utilities/render/platform.js new file mode 100644 index 0000000000..9678bf3ff1 --- /dev/null +++ b/scripts/developer/utilities/render/platform.js @@ -0,0 +1,18 @@ +// Test key commands +PlatformInfo.getComputer() +// {"OS":"WINDOWS","keys":null,"model":"","profileTier":"HIGH","vendor":""} +PlatformInfo.getNumCPUs() +// 1 +PlatformInfo.getCPU(0) +//{"clockSpeed":" 4.00GHz","model":") i7-6700K CPU @ 4.00GHz","numCores":8,"vendor":"Intel(R) Core(TM) i7-6700K CPU @ 4.00GHz"} +PlatformInfo.getNumGPUs() +// 1 +PlatformInfo.getGPU(0) +// {"driver":"25.21.14.1967","model":"NVIDIA GeForce GTX 1080","vendor":"NVIDIA GeForce GTX 1080","videoMemory":8079} + +var window = Desktop.createWindow(Script.resolvePath('./platform.qml'), { + title: "Platform", + presentationMode: Desktop.PresentationMode.NATIVE, + size: {x: 350, y: 700} +}); + diff --git a/scripts/developer/utilities/render/platform.qml b/scripts/developer/utilities/render/platform.qml new file mode 100644 index 0000000000..775ad8e106 --- /dev/null +++ b/scripts/developer/utilities/render/platform.qml @@ -0,0 +1,75 @@ +// +// platform.qml +// +// Created by Sam Gateau on 5/25/2019 +// Copyright 2019 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or https://www.apache.org/licenses/LICENSE-2.0.html +// +import QtQuick 2.7 +import QtQuick.Controls 2.2 +import QtQuick.Layouts 1.3 + +import controlsUit 1.0 as HifiControls + +import "../lib/prop" as Prop + +Rectangle { + anchors.fill: parent + id: platform; + + Prop.Global { id: global;} + color: global.colorBack + + Column { + width: parent.width + + Prop.PropGroup { + id: computer + label: "Computer" + isUnfold: true + + Component.onCompleted: { + computer.populateFromObjectProps(JSON.parse(PlatformInfo.getComputer())) + } + } + Prop.PropGroup { + id: cpu + label: "CPU" + isUnfold: true + + Component.onCompleted: { + cpu.populateFromObjectProps(JSON.parse(PlatformInfo.getCPU(0))) + } + } + Prop.PropGroup { + id: memory + label: "Memory" + isUnfold: true + + Component.onCompleted: { + memory.populateFromObjectProps(JSON.parse(PlatformInfo.getMemory())) + } + } + Prop.PropGroup { + id: gpu + label: "GPU" + isUnfold: true + + Component.onCompleted: { + gpu.populateFromObjectProps(JSON.parse(PlatformInfo.getGPU(0))) + } + } + Prop.PropGroup { + id: display + label: "Display" + isUnfold: true + + Component.onCompleted: { + display.populateFromObjectProps(JSON.parse(PlatformInfo.getDisplay(0))) + } + } + } +} + diff --git a/scripts/simplifiedUI/defaultScripts.js b/scripts/simplifiedUI/defaultScripts.js new file mode 100644 index 0000000000..3213303f2b --- /dev/null +++ b/scripts/simplifiedUI/defaultScripts.js @@ -0,0 +1,41 @@ +"use strict"; +/* jslint vars: true, plusplus: true */ + +// +// defaultScripts.js +// +// Authors: Zach Fox +// Created: 2019-05-23 +// Copyright 2019 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 DEFAULT_SCRIPTS_PATH_PREFIX = ScriptDiscoveryService.defaultScriptsPath + "/"; + + +var DEFAULT_SCRIPTS_SEPARATE = [ + DEFAULT_SCRIPTS_PATH_PREFIX + "system/controllers/controllerScripts.js", + DEFAULT_SCRIPTS_PATH_PREFIX + "ui/simplifiedUI.js" +]; +function loadSeparateDefaults() { + for (var i in DEFAULT_SCRIPTS_SEPARATE) { + Script.load(DEFAULT_SCRIPTS_SEPARATE[i]); + } +} + + +var DEFAULT_SCRIPTS_COMBINED = [ + DEFAULT_SCRIPTS_PATH_PREFIX + "system/request-service.js", + DEFAULT_SCRIPTS_PATH_PREFIX + "system/progress.js", + DEFAULT_SCRIPTS_PATH_PREFIX + "system/away.js" +]; +function runDefaultsTogether() { + for (var i in DEFAULT_SCRIPTS_COMBINED) { + Script.include(DEFAULT_SCRIPTS_COMBINED[i]); + } + loadSeparateDefaults(); +} + + +runDefaultsTogether(); diff --git a/scripts/simplifiedUI/modules/appUi.js b/scripts/simplifiedUI/modules/appUi.js new file mode 100644 index 0000000000..9771348377 --- /dev/null +++ b/scripts/simplifiedUI/modules/appUi.js @@ -0,0 +1,387 @@ +"use strict"; +/* global Tablet, Script */ +// +// libraries/appUi.js +// +// Created by Howard Stearns on 3/20/18. +// Copyright 2018 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 AppUi(properties) { + var request = Script.require('request').request; + /* Example development order: + 1. var AppUi = Script.require('appUi'); + 2. Put appname-i.svg, appname-a.svg in graphicsDirectory (where non-default graphicsDirectory can be added in #3). + 3. ui = new AppUi({buttonName: "APPNAME", home: "qml-or-html-path"}); + (And if converting an existing app, + define var tablet = ui.tablet, button = ui.button; as needed. + remove button.clicked.[dis]connect and tablet.remove(button).) + 4. Define onOpened and onClosed behavior in #3, if any. + (And if converting an existing app, remove screenChanged.[dis]connect.) + 5. Define onMessage and sendMessage in #3, if any. onMessage is wired/unwired on open/close. If you + want a handler to be "always on", connect it yourself at script startup. + (And if converting an existing app, remove code that [un]wires that message handling such as + fromQml/sendToQml or webEventReceived/emitScriptEvent.) + 6. (If converting an existing app, cleanup stuff that is no longer necessary, like references to button, tablet, + and use isOpen, open(), and close() as needed.) + 7. lint! + */ + var that = this; + function defaultButton(name, suffix) { + var base = that[name] || (that.buttonPrefix + suffix); + that[name] = (base.indexOf('/') >= 0) ? base : (that.graphicsDirectory + base); // poor man's merge + } + + // Defaults: + that.tabletName = "com.highfidelity.interface.tablet.system"; + that.inject = ""; + that.graphicsDirectory = "icons/tablet-icons/"; // Where to look for button svgs. See below. + that.additionalAppScreens = []; + that.checkIsOpen = function checkIsOpen(type, tabletUrl) { // Are we active? Value used to set isOpen. + // Actual url may have prefix or suffix. + return that.currentVisibleUrl && + ((that.home.indexOf(that.currentVisibleUrl) > -1) || + (that.additionalAppScreens.indexOf(that.currentVisibleUrl) > -1)); + }; + that.setCurrentVisibleScreenMetadata = function setCurrentVisibleScreenMetadata(type, url) { + that.currentVisibleScreenType = type; + that.currentVisibleUrl = url; + }; + that.open = function open(optionalUrl, optionalInject) { // How to open the app. + var url = optionalUrl || that.home; + var inject = optionalInject || that.inject; + + if (that.isQMLUrl(url)) { + that.tablet.loadQMLSource(url); + } else { + that.tablet.gotoWebScreen(url, inject); + } + }; + // Opens some app on top of the current app (on desktop, opens new window) + that.openNewAppOnTop = function openNewAppOnTop(url, optionalInject) { + var inject = optionalInject || ""; + if (that.isQMLUrl(url)) { + that.tablet.loadQMLOnTop(url); + } else { + that.tablet.loadWebScreenOnTop(url, inject); + } + }; + that.close = function close() { // How to close the app. + that.currentVisibleUrl = ""; + // for toolbar-mode: go back to home screen, this will close the window. + that.tablet.gotoHomeScreen(); + }; + that.buttonActive = function buttonActive(isActive) { // How to make the button active (white). + that.button.editProperties({isActive: isActive}); + }; + that.isQMLUrl = function isQMLUrl(url) { + var type = /.qml$/.test(url) ? 'QML' : 'Web'; + return type === 'QML'; + }; + that.isCurrentlyOnQMLScreen = function isCurrentlyOnQMLScreen() { + return that.currentVisibleScreenType === 'QML'; + }; + + // + // START Notification Handling Defaults + // + that.messagesWaiting = function messagesWaiting(isWaiting) { // How to indicate a message light on button. + // Note that waitingButton doesn't have to exist unless someone explicitly calls this with isWaiting true. + that.button.editProperties({ + icon: isWaiting ? that.normalMessagesButton : that.normalButton, + activeIcon: isWaiting ? that.activeMessagesButton : that.activeButton + }); + }; + that.notificationPollTimeout = [false]; + that.notificationPollTimeoutMs = [60000]; + that.notificationPollEndpoint = [false]; + that.notificationPollStopPaginatingConditionMet = [false]; + that.notificationDataProcessPage = function (data) { + return data; + }; + that.notificationPollCallback = [that.ignore]; + that.notificationPollCaresAboutSince = [false]; + that.notificationInitialCallbackMade = [false]; + that.notificationDisplayBanner = function (message) { + if (!that.isOpen) { + Window.displayAnnouncement(message); + } + }; + // + // END Notification Handling Defaults + // + + // Handlers + that.onScreenChanged = function onScreenChanged(type, url) { + // Set isOpen, wireEventBridge, set buttonActive as appropriate, + // and finally call onOpened() or onClosed() IFF defined. + that.setCurrentVisibleScreenMetadata(type, url); + + if (that.checkIsOpen(type, url)) { + that.wireEventBridge(true); + if (!that.isOpen) { + that.buttonActive(true); + if (that.onOpened) { + that.onOpened(); + } + that.isOpen = true; + } + } else { + // A different screen is now visible, or the tablet has been closed. + // Tablet visibility is controlled separately by `tabletShownChanged()` + that.wireEventBridge(false); + if (that.isOpen) { + that.buttonActive(false); + if (that.onClosed) { + that.onClosed(); + } + that.isOpen = false; + } + } + }; + + // Overwrite with the given properties: + Object.keys(properties).forEach(function (key) { + that[key] = properties[key]; + }); + + // + // START Notification Handling + // + + var currentDataPageToRetrieve = []; + var concatenatedServerResponse = []; + for (var i = 0; i < that.notificationPollEndpoint.length; i++) { + currentDataPageToRetrieve[i] = 1; + concatenatedServerResponse[i] = new Array(); + } + + var MAX_LOG_LENGTH_CHARACTERS = 300; + function requestCallback(error, response, optionalParams) { + var indexOfRequest = optionalParams.indexOfRequest; + var urlOfRequest = optionalParams.urlOfRequest; + + if (error || (response.status !== 'success')) { + print("Error: unable to complete request from URL. Error:", error || response.status); + startNotificationTimer(indexOfRequest); + return; + } + + if (!that.notificationPollStopPaginatingConditionMet[indexOfRequest] || + that.notificationPollStopPaginatingConditionMet[indexOfRequest](response)) { + startNotificationTimer(indexOfRequest); + + var notificationData; + if (concatenatedServerResponse[indexOfRequest].length) { + notificationData = concatenatedServerResponse[indexOfRequest]; + } else { + notificationData = that.notificationDataProcessPage[indexOfRequest](response); + } + console.debug(that.buttonName, + 'truncated notification data for processing:', + JSON.stringify(notificationData).substring(0, MAX_LOG_LENGTH_CHARACTERS)); + that.notificationPollCallback[indexOfRequest](notificationData); + that.notificationInitialCallbackMade[indexOfRequest] = true; + currentDataPageToRetrieve[indexOfRequest] = 1; + concatenatedServerResponse[indexOfRequest] = new Array(); + } else { + concatenatedServerResponse[indexOfRequest] = + concatenatedServerResponse[indexOfRequest].concat(that.notificationDataProcessPage[indexOfRequest](response)); + currentDataPageToRetrieve[indexOfRequest]++; + request({ + json: true, + uri: (urlOfRequest + "&page=" + currentDataPageToRetrieve[indexOfRequest]) + }, requestCallback, optionalParams); + } + } + + + var METAVERSE_BASE = Account.metaverseServerURL; + var MS_IN_SEC = 1000; + that.notificationPoll = function (i) { + if (!that.notificationPollEndpoint[i]) { + return; + } + + // User is "appearing offline" or is not logged in + if (GlobalServices.findableBy === "none" || Account.username === "Unknown user") { + // The notification polling will restart when the user changes their availability + // or when they log in, so it's not necessary to restart a timer here. + console.debug(that.buttonName + " Notifications: User is appearing offline or not logged in. " + + that.buttonName + " will poll for notifications when user logs in and has their availability " + + "set to not appear offline."); + return; + } + + var url = METAVERSE_BASE + that.notificationPollEndpoint[i]; + + var settingsKey = "notifications/" + that.notificationPollEndpoint[i] + "/lastPoll"; + var currentTimestamp = new Date().getTime(); + var lastPollTimestamp = Settings.getValue(settingsKey, currentTimestamp); + if (that.notificationPollCaresAboutSince[i]) { + url = url + "&since=" + lastPollTimestamp / MS_IN_SEC; + } + Settings.setValue(settingsKey, currentTimestamp); + + request({ + json: true, + uri: url + }, + requestCallback, + { + indexOfRequest: i, + urlOfRequest: url + }); + }; + + // This won't do anything if there isn't a notification endpoint set + for (i = 0; i < that.notificationPollEndpoint.length; i++) { + that.notificationPoll(i); + } + + function startNotificationTimer(indexOfRequest) { + that.notificationPollTimeout[indexOfRequest] = Script.setTimeout(function () { + that.notificationPoll(indexOfRequest); + }, that.notificationPollTimeoutMs[indexOfRequest]); + } + + function restartNotificationPoll() { + for (var j = 0; j < that.notificationPollEndpoint.length; j++) { + that.notificationInitialCallbackMade[j] = false; + if (that.notificationPollTimeout[j]) { + Script.clearTimeout(that.notificationPollTimeout[j]); + that.notificationPollTimeout[j] = false; + } + that.notificationPoll(j); + } + } + // + // END Notification Handling + // + + // Properties: + that.tablet = Tablet.getTablet(that.tabletName); + // Must be after we gather properties. + that.buttonPrefix = that.buttonPrefix || that.buttonName.toLowerCase() + "-"; + defaultButton('normalButton', 'i.svg'); + defaultButton('activeButton', 'a.svg'); + defaultButton('normalMessagesButton', 'i-msg.svg'); + defaultButton('activeMessagesButton', 'a-msg.svg'); + var buttonOptions = { + icon: that.normalButton, + activeIcon: that.activeButton, + text: that.buttonName + }; + // `TabletScriptingInterface` looks for the presence of a `sortOrder` key. + // What it SHOULD do is look to see if the value inside that key is defined. + // To get around the current code, we do this instead. + if (that.sortOrder) { + buttonOptions.sortOrder = that.sortOrder; + } + that.button = that.tablet.addButton(buttonOptions); + that.ignore = function ignore() { }; + that.hasOutboundEventBridge = false; + that.hasInboundQmlEventBridge = false; + that.hasInboundHtmlEventBridge = false; + // HTML event bridge uses strings, not objects. Here we abstract over that. + // (Although injected javascript still has to use JSON.stringify/JSON.parse.) + that.sendToHtml = function (messageObject) { + that.tablet.emitScriptEvent(JSON.stringify(messageObject)); + }; + that.fromHtml = function (messageString) { + var parsedMessage = JSON.parse(messageString); + parsedMessage.messageSrc = "HTML"; + that.onMessage(parsedMessage); + }; + that.sendMessage = that.ignore; + that.wireEventBridge = function wireEventBridge(on) { + // Uniquivocally sets that.sendMessage(messageObject) to do the right thing. + // Sets has*EventBridge and wires onMessage to the proper event bridge as appropriate, IFF onMessage defined. + var isCurrentlyOnQMLScreen = that.isCurrentlyOnQMLScreen(); + // Outbound (always, regardless of whether there is an inbound handler). + if (on) { + that.sendMessage = isCurrentlyOnQMLScreen ? that.tablet.sendToQml : that.sendToHtml; + that.hasOutboundEventBridge = true; + } else { + that.sendMessage = that.ignore; + that.hasOutboundEventBridge = false; + } + + if (!that.onMessage) { + return; + } + + // Inbound + if (on) { + if (isCurrentlyOnQMLScreen && !that.hasInboundQmlEventBridge) { + console.debug(that.buttonName, 'connecting', that.tablet.fromQml); + that.tablet.fromQml.connect(that.onMessage); + that.hasInboundQmlEventBridge = true; + } else if (!isCurrentlyOnQMLScreen && !that.hasInboundHtmlEventBridge) { + console.debug(that.buttonName, 'connecting', that.tablet.webEventReceived); + that.tablet.webEventReceived.connect(that.fromHtml); + that.hasInboundHtmlEventBridge = true; + } + } else { + if (that.hasInboundQmlEventBridge) { + console.debug(that.buttonName, 'disconnecting', that.tablet.fromQml); + that.tablet.fromQml.disconnect(that.onMessage); + that.hasInboundQmlEventBridge = false; + } + if (that.hasInboundHtmlEventBridge) { + console.debug(that.buttonName, 'disconnecting', that.tablet.webEventReceived); + that.tablet.webEventReceived.disconnect(that.fromHtml); + that.hasInboundHtmlEventBridge = false; + } + } + }; + that.isOpen = false; + // To facilitate incremental development, only wire onClicked to do something when "home" is defined in properties. + that.onClicked = that.home + ? function onClicked() { + // Call open() or close(), and reset type based on current home property. + if (that.isOpen) { + that.close(); + } else { + that.open(); + } + } : that.ignore; + that.onScriptEnding = function onScriptEnding() { + // Close if necessary, clean up any remaining handlers, and remove the button. + GlobalServices.myUsernameChanged.disconnect(restartNotificationPoll); + GlobalServices.findableByChanged.disconnect(restartNotificationPoll); + that.tablet.screenChanged.disconnect(that.onScreenChanged); + if (that.isOpen) { + that.close(); + that.onScreenChanged("", ""); + } + if (that.button) { + if (that.onClicked) { + that.button.clicked.disconnect(that.onClicked); + } + that.tablet.removeButton(that.button); + } + for (var i = 0; i < that.notificationPollTimeout.length; i++) { + if (that.notificationPollTimeout[i]) { + Script.clearInterval(that.notificationPollTimeout[i]); + that.notificationPollTimeout[i] = false; + } + } + }; + // Set up the handlers. + that.tablet.screenChanged.connect(that.onScreenChanged); + that.button.clicked.connect(that.onClicked); + Script.scriptEnding.connect(that.onScriptEnding); + GlobalServices.findableByChanged.connect(restartNotificationPoll); + GlobalServices.myUsernameChanged.connect(restartNotificationPoll); + if (that.buttonName === Settings.getValue("startUpApp")) { + Settings.setValue("startUpApp", ""); + Script.setTimeout(function () { + that.open(); + }, 1000); + } +} +module.exports = AppUi; diff --git a/scripts/simplifiedUI/modules/request.js b/scripts/simplifiedUI/modules/request.js new file mode 100644 index 0000000000..37f3ac0d7b --- /dev/null +++ b/scripts/simplifiedUI/modules/request.js @@ -0,0 +1,83 @@ +"use strict"; + +// request.js +// +// Created by Cisco Fresquet on 04/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 +// + +/* global module */ +// @module request +// +// This module contains the `request` module implementation + +// =========================================================================================== +module.exports = { + + // ------------------------------------------------------------------ + // cb(error, responseOfCorrectContentType, optionalCallbackParameter) of url. A subset of npm request. + request: function (options, callback, optionalCallbackParameter) { + var httpRequest = new XMLHttpRequest(), key; + // QT bug: apparently doesn't handle onload. Workaround using readyState. + httpRequest.onreadystatechange = function () { + var READY_STATE_DONE = 4; + var HTTP_OK = 200; + if (httpRequest.readyState >= READY_STATE_DONE) { + var error = (httpRequest.status !== HTTP_OK) && httpRequest.status.toString() + ':' + httpRequest.statusText, + response = !error && httpRequest.responseText, + contentType = !error && httpRequest.getResponseHeader('content-type'); + if (!error && contentType.indexOf('application/json') === 0) { // ignoring charset, etc. + try { + response = JSON.parse(response); + } catch (e) { + error = e; + } + } + if (error) { + response = { statusCode: httpRequest.status }; + } + callback(error, response, optionalCallbackParameter); + } + }; + if (typeof options === 'string') { + options = { uri: options }; + } + if (options.url) { + options.uri = options.url; + } + if (!options.method) { + options.method = 'GET'; + } + if (options.body && (options.method === 'GET')) { // add query parameters + var params = [], appender = (-1 === options.uri.search('?')) ? '?' : '&'; + for (key in options.body) { + if (options.body.hasOwnProperty(key)) { + params.push(key + '=' + options.body[key]); + } + } + options.uri += appender + params.join('&'); + delete options.body; + } + if (options.json) { + options.headers = options.headers || {}; + options.headers["Content-type"] = "application/json"; + options.body = JSON.stringify(options.body); + } + for (key in options.headers || {}) { + if (options.headers.hasOwnProperty(key)) { + httpRequest.setRequestHeader(key, options.headers[key]); + } + } + httpRequest.open(options.method, options.uri, true); + httpRequest.send(options.body || null); + } +}; + +// =========================================================================================== +// @function - debug logging +function debug() { + print('RequestModule | ' + [].slice.call(arguments).join(' ')); +} diff --git a/scripts/simplifiedUI/modules/vec3.js b/scripts/simplifiedUI/modules/vec3.js new file mode 100644 index 0000000000..f164f01374 --- /dev/null +++ b/scripts/simplifiedUI/modules/vec3.js @@ -0,0 +1,69 @@ +// Example of using a "system module" to decouple Vec3's implementation details. +// +// Users would bring Vec3 support in as a module: +// var vec3 = Script.require('vec3'); +// + +// (this example is compatible with using as a Script.include and as a Script.require module) +try { + // Script.require + module.exports = vec3; +} catch(e) { + // Script.include + Script.registerValue("vec3", vec3); +} + +vec3.fromObject = function(v) { + //return new vec3(v.x, v.y, v.z); + //... this is even faster and achieves the same effect + v.__proto__ = vec3.prototype; + return v; +}; + +vec3.prototype = { + multiply: function(v2) { + // later on could support overrides like so: + // if (v2 instanceof quat) { [...] } + // which of the below is faster (C++ or JS)? + // (dunno -- but could systematically find out and go with that version) + + // pure JS option + // return new vec3(this.x * v2.x, this.y * v2.y, this.z * v2.z); + + // hybrid C++ option + return vec3.fromObject(Vec3.multiply(this, v2)); + }, + // detects any NaN and Infinity values + isValid: function() { + return isFinite(this.x) && isFinite(this.y) && isFinite(this.z); + }, + // format Vec3's, eg: + // var v = vec3(); + // print(v); // outputs [Vec3 (0.000, 0.000, 0.000)] + toString: function() { + if (this === vec3.prototype) { + return "{Vec3 prototype}"; + } + function fixed(n) { return n.toFixed(3); } + return "[Vec3 (" + [this.x, this.y, this.z].map(fixed) + ")]"; + }, +}; + +vec3.DEBUG = true; + +function vec3(x, y, z) { + if (!(this instanceof vec3)) { + // if vec3 is called as a function then re-invoke as a constructor + // (so that `value instanceof vec3` holds true for created values) + return new vec3(x, y, z); + } + + // unfold default arguments (vec3(), vec3(.5), vec3(0,1), etc.) + this.x = x !== undefined ? x : 0; + this.y = y !== undefined ? y : this.x; + this.z = z !== undefined ? z : this.y; + + if (vec3.DEBUG && !this.isValid()) + throw new Error('vec3() -- invalid initial values ['+[].slice.call(arguments)+']'); +}; + diff --git a/scripts/simplifiedUI/system/away.js b/scripts/simplifiedUI/system/away.js new file mode 100644 index 0000000000..6293c0c452 --- /dev/null +++ b/scripts/simplifiedUI/system/away.js @@ -0,0 +1,387 @@ +"use strict"; + +// +// away.js +// +// examples +// +// Created by Howard Stearns 11/3/15 +// Copyright 2015 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// +// Goes into "paused" when the '.' key (and automatically when started in HMD), and normal when pressing any key. +// See MAIN CONTROL, below, for what "paused" actually does. + +/* eslint indent: ["error", 4, { "outerIIFEBody": 0 }] */ + +(function() { // BEGIN LOCAL_SCOPE + +var BASIC_TIMER_INTERVAL = 50; // 50ms = 20hz +var OVERLAY_WIDTH = 1920; +var OVERLAY_HEIGHT = 1080; +var OVERLAY_DATA = { + width: OVERLAY_WIDTH, + height: OVERLAY_HEIGHT, + imageURL: Script.resolvePath("assets/images/Overlay-Viz-blank.png"), + emissive: true, + drawInFront: true, + alpha: 1 +}; +var AVATAR_MOVE_FOR_ACTIVE_DISTANCE = 0.8; // meters -- no longer away if avatar moves this far while away + +var CAMERA_MATRIX = -7; + +var OVERLAY_DATA_HMD = { + localPosition: {x: 0, y: 0, z: -1 * MyAvatar.sensorToWorldScale}, + localRotation: {x: 0, y: 0, z: 0, w: 1}, + width: OVERLAY_WIDTH, + height: OVERLAY_HEIGHT, + url: Script.resolvePath("assets/images/Overlay-Viz-blank.png"), + color: {red: 255, green: 255, blue: 255}, + alpha: 1, + scale: 2 * MyAvatar.sensorToWorldScale, + emissive: true, + drawInFront: true, + parentID: MyAvatar.SELF_ID, + parentJointIndex: CAMERA_MATRIX, + ignorePickIntersection: true +}; + +var AWAY_INTRO = { + url: "http://hifi-content.s3.amazonaws.com/ozan/dev/anim/standard_anims_160127/kneel.fbx", + playbackRate: 30.0, + loopFlag: false, + startFrame: 0.0, + endFrame: 83.0 +}; + +// MAIN CONTROL +var isEnabled = true; +var wasMuted; // unknonwn? +var isAway = false; // we start in the un-away state +var eventMappingName = "io.highfidelity.away"; // goActive on hand controller button events, too. +var eventMapping = Controller.newMapping(eventMappingName); +var avatarPosition = MyAvatar.position; +var wasHmdMounted = HMD.mounted; +var previousBubbleState = Users.getIgnoreRadiusEnabled(); + +var enterAwayStateWhenFocusLostInVR = HMD.enterAwayStateWhenFocusLostInVR; + +// some intervals we may create/delete +var avatarMovedInterval; + + +// prefetch the kneel animation and hold a ref so it's always resident in memory when we need it. +var _animation = AnimationCache.prefetch(AWAY_INTRO.url); + +function playAwayAnimation() { + MyAvatar.overrideAnimation(AWAY_INTRO.url, + AWAY_INTRO.playbackRate, + AWAY_INTRO.loopFlag, + AWAY_INTRO.startFrame, + AWAY_INTRO.endFrame); +} + +function stopAwayAnimation() { + MyAvatar.restoreAnimation(); +} + +// OVERLAY +var overlay = Overlays.addOverlay("image", OVERLAY_DATA); +var overlayHMD = Overlays.addOverlay("image3d", OVERLAY_DATA_HMD); + +function showOverlay() { + if (HMD.active) { + // make sure desktop version is hidden + Overlays.editOverlay(overlay, { visible: false }); + Overlays.editOverlay(overlayHMD, { visible: true }); + } else { + // make sure HMD is hidden + Overlays.editOverlay(overlayHMD, { visible: false }); + + // Update for current screen size, keeping overlay proportions constant. + var screen = Controller.getViewportDimensions(); + + // keep the overlay it's natural size and always center it... + Overlays.editOverlay(overlay, { + visible: true, + x: ((screen.x - OVERLAY_WIDTH) / 2), + y: ((screen.y - OVERLAY_HEIGHT) / 2) + }); + } +} + +function hideOverlay() { + Overlays.editOverlay(overlay, {visible: false}); + Overlays.editOverlay(overlayHMD, {visible: false}); +} + +hideOverlay(); + +function maybeMoveOverlay() { + if (isAway) { + // if we switched from HMD to Desktop, make sure to hide our HUD overlay and show the + // desktop overlay + if (!HMD.active) { + showOverlay(); // this will also recenter appropriately + } + + if (HMD.active) { + + var sensorScaleFactor = MyAvatar.sensorToWorldScale; + var localPosition = {x: 0, y: 0, z: -1 * sensorScaleFactor}; + Overlays.editOverlay(overlayHMD, { visible: true, localPosition: localPosition, scale: 2 * sensorScaleFactor }); + + // make sure desktop version is hidden + Overlays.editOverlay(overlay, { visible: false }); + + // also remember avatar position + avatarPosition = MyAvatar.position; + + } + } +} + +function ifAvatarMovedGoActive() { + var newAvatarPosition = MyAvatar.position; + if (Vec3.distance(newAvatarPosition, avatarPosition) > AVATAR_MOVE_FOR_ACTIVE_DISTANCE) { + goActive(); + } + avatarPosition = newAvatarPosition; +} + +function goAway(fromStartup) { + if (!isEnabled || isAway) { + return; + } + + // If we're entering away mode from some other state than startup, then we create our move timer immediately. + // However if we're just stating up, we need to delay this process so that we don't think the initial teleport + // is actually a move. + if (fromStartup === undefined || fromStartup === false) { + avatarMovedInterval = Script.setInterval(ifAvatarMovedGoActive, BASIC_TIMER_INTERVAL); + } else { + var WAIT_FOR_MOVE_ON_STARTUP = 3000; // 3 seconds + Script.setTimeout(function() { + avatarMovedInterval = Script.setInterval(ifAvatarMovedGoActive, BASIC_TIMER_INTERVAL); + }, WAIT_FOR_MOVE_ON_STARTUP); + } + + previousBubbleState = Users.getIgnoreRadiusEnabled(); + if (!previousBubbleState) { + Users.toggleIgnoreRadius(); + } + UserActivityLogger.privacyShieldToggled(Users.getIgnoreRadiusEnabled()); + UserActivityLogger.toggledAway(true); + MyAvatar.isAway = true; +} + +function goActive() { + if (!isAway) { + return; + } + + UserActivityLogger.toggledAway(false); + MyAvatar.isAway = false; + + if (Users.getIgnoreRadiusEnabled() !== previousBubbleState) { + Users.toggleIgnoreRadius(); + UserActivityLogger.privacyShieldToggled(Users.getIgnoreRadiusEnabled()); + } + + if (!Window.hasFocus()) { + Window.setFocus(); + } +} + +MyAvatar.wentAway.connect(setAwayProperties); +MyAvatar.wentActive.connect(setActiveProperties); + +function setAwayProperties() { + isAway = true; + wasMuted = Audio.muted; + if (!wasMuted) { + Audio.muted = !Audio.muted; + } + MyAvatar.setEnableMeshVisible(false); // just for our own display, without changing point of view + playAwayAnimation(); // animation is still seen by others + showOverlay(); + + HMD.requestShowHandControllers(); + + // tell the Reticle, we want to stop capturing the mouse until we come back + Reticle.allowMouseCapture = false; + // Allow users to find their way to other applications, our menus, etc. + // For desktop, that means we want the reticle visible. + // For HMD, the hmd preview will show the system mouse because of allowMouseCapture, + // but we want to turn off our Reticle so that we don't get two in preview and a stuck one in headset. + Reticle.visible = !HMD.active; + wasHmdMounted = HMD.mounted; // always remember the correct state + + avatarPosition = MyAvatar.position; +} + +function setActiveProperties() { + isAway = false; + if (Audio.muted && !wasMuted) { + Audio.muted = false; + } + MyAvatar.setEnableMeshVisible(true); // IWBNI we respected Developer->Avatar->Draw Mesh setting. + stopAwayAnimation(); + + HMD.requestHideHandControllers(); + + // update the UI sphere to be centered about the current HMD orientation. + HMD.centerUI(); + + // forget about any IK joint limits + MyAvatar.clearIKJointLimitHistory(); + + // update the avatar hips to point in the same direction as the HMD orientation. + MyAvatar.centerBody(); + + hideOverlay(); + + // tell the Reticle, we are ready to capture the mouse again and it should be visible + Reticle.allowMouseCapture = true; + Reticle.visible = true; + if (HMD.active) { + Reticle.position = HMD.getHUDLookAtPosition2D(); + } + wasHmdMounted = HMD.mounted; // always remember the correct state + + Script.clearInterval(avatarMovedInterval); +} + +function maybeGoActive(event) { + if (event.isAutoRepeat) { // isAutoRepeat is true when held down (or when Windows feels like it) + return; + } + if (!isAway && (event.text === 'ESC')) { + goAway(); + } else { + goActive(); + } +} + +var wasHmdActive = HMD.active; +var wasMouseCaptured = Reticle.mouseCaptured; + +function maybeGoAway() { + // If our active state change (went to or from HMD mode), and we are now in the HMD, go into away + if (HMD.active !== wasHmdActive) { + wasHmdActive = !wasHmdActive; + if (wasHmdActive) { + goAway(); + return; + } + } + + // If the mouse has gone from captured, to non-captured state, then it likely means the person is still in the HMD, + // but tabbed away from the application (meaning they don't have mouse control) and they likely want to go into + // an away state + if (Reticle.mouseCaptured !== wasMouseCaptured) { + wasMouseCaptured = !wasMouseCaptured; + if (!wasMouseCaptured) { + if (enterAwayStateWhenFocusLostInVR) { + goAway(); + return; + } + } + } + + // If you've removed your HMD from your head, and we can detect it, we will also go away... + if (HMD.mounted !== wasHmdMounted) { + wasHmdMounted = HMD.mounted; + print("HMD mounted changed..."); + + // We're putting the HMD on... switch to those devices + if (HMD.mounted) { + print("NOW mounted..."); + } else { + print("HMD NOW un-mounted..."); + + if (HMD.active) { + goAway(); + return; + } + } + } +} + +function setEnabled(value) { + if (!value) { + goActive(); + } + isEnabled = value; +} + +function checkAudioToggled() { + if (isAway && !Audio.muted) { + goActive(); + } +} + + +var CHANNEL_AWAY_ENABLE = "Hifi-Away-Enable"; +var handleMessage = function(channel, message, sender) { + if (channel === CHANNEL_AWAY_ENABLE && sender === MyAvatar.sessionUUID) { + print("away.js | Got message on Hifi-Away-Enable: ", message); + setEnabled(message === 'enable'); + } +}; +Messages.subscribe(CHANNEL_AWAY_ENABLE); +Messages.messageReceived.connect(handleMessage); + +var maybeIntervalTimer = Script.setInterval(function() { + maybeMoveOverlay(); + maybeGoAway(); + checkAudioToggled(); +}, BASIC_TIMER_INTERVAL); + + +Controller.mousePressEvent.connect(goActive); +Controller.keyPressEvent.connect(maybeGoActive); +// Note peek() so as to not interfere with other mappings. +eventMapping.from(Controller.Standard.LeftPrimaryThumb).peek().to(goActive); +eventMapping.from(Controller.Standard.RightPrimaryThumb).peek().to(goActive); +eventMapping.from(Controller.Standard.LeftSecondaryThumb).peek().to(goActive); +eventMapping.from(Controller.Standard.RightSecondaryThumb).peek().to(goActive); +eventMapping.from(Controller.Standard.LT).peek().to(goActive); +eventMapping.from(Controller.Standard.LB).peek().to(goActive); +eventMapping.from(Controller.Standard.LS).peek().to(goActive); +eventMapping.from(Controller.Standard.LeftGrip).peek().to(goActive); +eventMapping.from(Controller.Standard.RT).peek().to(goActive); +eventMapping.from(Controller.Standard.RB).peek().to(goActive); +eventMapping.from(Controller.Standard.RS).peek().to(goActive); +eventMapping.from(Controller.Standard.RightGrip).peek().to(goActive); +eventMapping.from(Controller.Standard.Back).peek().to(goActive); +eventMapping.from(Controller.Standard.Start).peek().to(goActive); +Controller.enableMapping(eventMappingName); + +function awayStateWhenFocusLostInVRChanged(enabled) { + enterAwayStateWhenFocusLostInVR = enabled; +} + +Script.scriptEnding.connect(function () { + Script.clearInterval(maybeIntervalTimer); + goActive(); + HMD.awayStateWhenFocusLostInVRChanged.disconnect(awayStateWhenFocusLostInVRChanged); + Controller.disableMapping(eventMappingName); + Controller.mousePressEvent.disconnect(goActive); + Controller.keyPressEvent.disconnect(maybeGoActive); + Messages.messageReceived.disconnect(handleMessage); + Messages.unsubscribe(CHANNEL_AWAY_ENABLE); +}); + +HMD.awayStateWhenFocusLostInVRChanged.connect(awayStateWhenFocusLostInVRChanged); + +if (HMD.active && !HMD.mounted) { + print("Starting script, while HMD is active and not mounted..."); + goAway(true); +} + + +}()); // END LOCAL_SCOPE diff --git a/scripts/simplifiedUI/system/controllers/+android_questInterface/controllerScripts.js b/scripts/simplifiedUI/system/controllers/+android_questInterface/controllerScripts.js new file mode 100644 index 0000000000..d313efaca1 --- /dev/null +++ b/scripts/simplifiedUI/system/controllers/+android_questInterface/controllerScripts.js @@ -0,0 +1,58 @@ +"use strict"; + +// controllerScripts.js +// +// Created by David Rowe on 15 Mar 2017. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +/* global Script, Menu */ + +var CONTOLLER_SCRIPTS = [ + "squeezeHands.js", + "controllerDisplayManager.js", + "toggleAdvancedMovementForHandControllers.js", + "controllerDispatcher.js", + "controllerModules/nearParentGrabOverlay.js", + "controllerModules/stylusInput.js", + "controllerModules/equipEntity.js", + "controllerModules/nearTrigger.js", + "controllerModules/webSurfaceLaserInput.js", + "controllerModules/inVREditMode.js", + "controllerModules/disableOtherModule.js", + "controllerModules/farTrigger.js", + "controllerModules/teleport.js", + "controllerModules/hudOverlayPointer.js", + "controllerModules/scaleEntity.js", + "controllerModules/nearGrabHyperLinkEntity.js", + "controllerModules/nearTabletHighlight.js", + "controllerModules/nearGrabEntity.js", + "controllerModules/farGrabEntity.js" +]; + +var DEBUG_MENU_ITEM = "Debug defaultScripts.js"; + +function runDefaultsTogether() { + for (var j in CONTOLLER_SCRIPTS) { + if (CONTOLLER_SCRIPTS.hasOwnProperty(j)) { + Script.include(CONTOLLER_SCRIPTS[j]); + } + } +} + +function runDefaultsSeparately() { + for (var i in CONTOLLER_SCRIPTS) { + if (CONTOLLER_SCRIPTS.hasOwnProperty(i)) { + Script.load(CONTOLLER_SCRIPTS[i]); + } + } +} + +if (Menu.isOptionChecked(DEBUG_MENU_ITEM)) { + runDefaultsSeparately(); +} else { + runDefaultsTogether(); +} diff --git a/scripts/simplifiedUI/system/controllers/controllerDispatcher.js b/scripts/simplifiedUI/system/controllers/controllerDispatcher.js new file mode 100644 index 0000000000..0a9fa4dce1 --- /dev/null +++ b/scripts/simplifiedUI/system/controllers/controllerDispatcher.js @@ -0,0 +1,615 @@ +"use strict"; + +// controllerDispatcher.js +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html + +/* jslint bitwise: true */ + +/* global Script, Entities, Overlays, Controller, Vec3, Quat, getControllerWorldLocation, + controllerDispatcherPlugins:true, controllerDispatcherPluginsNeedSort:true, + LEFT_HAND, RIGHT_HAND, NEAR_GRAB_PICK_RADIUS, DEFAULT_SEARCH_SPHERE_DISTANCE, DISPATCHER_PROPERTIES, + getGrabPointSphereOffset, HMD, MyAvatar, Messages, findHandChildEntities, Picks, PickType, Pointers, + PointerManager, getGrabPointSphereOffset, HMD, MyAvatar, Messages, findHandChildEntities, Picks, PickType, Pointers, + PointerManager, print, Keyboard +*/ + +controllerDispatcherPlugins = {}; +controllerDispatcherPluginsNeedSort = false; + +Script.include("/~/system/libraries/utils.js"); +Script.include("/~/system/libraries/controllers.js"); +Script.include("/~/system/libraries/controllerDispatcherUtils.js"); + +(function() { + Script.include("/~/system/libraries/pointersUtils.js"); + + var NEAR_MAX_RADIUS = 0.1; + var NEAR_TABLET_MAX_RADIUS = 0.05; + + var TARGET_UPDATE_HZ = 60; // 50hz good enough, but we're using update + var BASIC_TIMER_INTERVAL_MS = 1000 / TARGET_UPDATE_HZ; + + var PROFILE = false; + var DEBUG = false; + var SHOW_GRAB_SPHERE = false; + + + if (typeof Test !== "undefined") { + PROFILE = true; + } + + function ControllerDispatcher() { + var _this = this; + this.lastInterval = Date.now(); + this.intervalCount = 0; + this.totalDelta = 0; + this.totalVariance = 0; + this.highVarianceCount = 0; + this.veryhighVarianceCount = 0; + this.orderedPluginNames = []; + this.tabletID = null; + this.blacklist = []; + this.pointerManager = new PointerManager(); + this.grabSphereOverlays = [null, null]; + this.targetIDs = {}; + + // a module can occupy one or more "activity" slots while it's running. If all the required slots for a module are + // not set to false (not in use), a module cannot start. When a module is using a slot, that module's name + // is stored as the value, rather than false. + this.activitySlots = { + head: false, + leftHand: false, + rightHand: false, + rightHandTrigger: false, + leftHandTrigger: false, + rightHandEquip: false, + leftHandEquip: false, + mouse: false + }; + + this.laserVisibleStatus = [false, false, false, false]; + this.laserLockStatus = [false, false, false, false]; + + this.slotsAreAvailableForPlugin = function (plugin) { + for (var i = 0; i < plugin.parameters.activitySlots.length; i++) { + if (_this.activitySlots[plugin.parameters.activitySlots[i]]) { + return false; // something is already using a slot which _this plugin requires + } + } + return true; + }; + + this.markSlots = function (plugin, pluginName) { + for (var i = 0; i < plugin.parameters.activitySlots.length; i++) { + _this.activitySlots[plugin.parameters.activitySlots[i]] = pluginName; + } + }; + + this.unmarkSlotsForPluginName = function (runningPluginName) { + // this is used to free activity-slots when a plugin is deactivated while it's running. + for (var activitySlot in _this.activitySlots) { + if (activitySlot.hasOwnProperty(activitySlot) && _this.activitySlots[activitySlot] === runningPluginName) { + _this.activitySlots[activitySlot] = false; + } + } + }; + + this.runningPluginNames = {}; + this.leftTriggerValue = 0; + this.leftTriggerClicked = 0; + this.rightTriggerValue = 0; + this.rightTriggerClicked = 0; + this.leftSecondaryValue = 0; + this.rightSecondaryValue = 0; + + this.leftTriggerPress = function (value) { + _this.leftTriggerValue = value; + }; + this.leftTriggerClick = function (value) { + _this.leftTriggerClicked = value; + }; + this.rightTriggerPress = function (value) { + _this.rightTriggerValue = value; + }; + this.rightTriggerClick = function (value) { + _this.rightTriggerClicked = value; + }; + this.leftSecondaryPress = function (value) { + _this.leftSecondaryValue = value; + }; + this.rightSecondaryPress = function (value) { + _this.rightSecondaryValue = value; + }; + + this.dataGatherers = {}; + this.dataGatherers.leftControllerLocation = function () { + return getControllerWorldLocation(Controller.Standard.LeftHand, true); + }; + this.dataGatherers.rightControllerLocation = function () { + return getControllerWorldLocation(Controller.Standard.RightHand, true); + }; + + this.updateTimings = function () { + _this.intervalCount++; + var thisInterval = Date.now(); + var deltaTimeMsec = thisInterval - _this.lastInterval; + var deltaTime = deltaTimeMsec / 1000; + _this.lastInterval = thisInterval; + + _this.totalDelta += deltaTimeMsec; + + var variance = Math.abs(deltaTimeMsec - BASIC_TIMER_INTERVAL_MS); + _this.totalVariance += variance; + + if (variance > 1) { + _this.highVarianceCount++; + } + + if (variance > 5) { + _this.veryhighVarianceCount++; + } + + return deltaTime; + }; + + this.setIgnorePointerItems = function() { + if (HMD.tabletID && HMD.tabletID !== this.tabletID) { + this.tabletID = HMD.tabletID; + Pointers.setIgnoreItems(_this.leftPointer, _this.blacklist); + Pointers.setIgnoreItems(_this.rightPointer, _this.blacklist); + } + }; + + this.update = function () { + try { + _this.updateInternal(); + } catch (e) { + print(e); + } + Script.setTimeout(_this.update, BASIC_TIMER_INTERVAL_MS); + }; + + this.updateInternal = function () { + if (PROFILE) { + Script.beginProfileRange("dispatch.pre"); + } + var sensorScaleFactor = MyAvatar.sensorToWorldScale; + var deltaTime = _this.updateTimings(); + _this.setIgnorePointerItems(); + + if (controllerDispatcherPluginsNeedSort) { + _this.orderedPluginNames = []; + for (var pluginName in controllerDispatcherPlugins) { + if (controllerDispatcherPlugins.hasOwnProperty(pluginName)) { + _this.orderedPluginNames.push(pluginName); + } + } + _this.orderedPluginNames.sort(function (a, b) { + return controllerDispatcherPlugins[a].parameters.priority - + controllerDispatcherPlugins[b].parameters.priority; + }); + + controllerDispatcherPluginsNeedSort = false; + } + + if (PROFILE) { + Script.endProfileRange("dispatch.pre"); + } + + if (PROFILE) { + Script.beginProfileRange("dispatch.gather"); + } + + var controllerLocations = [ + _this.dataGatherers.leftControllerLocation(), + _this.dataGatherers.rightControllerLocation() + ]; + + // find 3d overlays near each hand + var nearbyOverlayIDs = []; + var h; + for (h = LEFT_HAND; h <= RIGHT_HAND; h++) { + if (controllerLocations[h].valid) { + var nearbyOverlays = + Overlays.findOverlays(controllerLocations[h].position, NEAR_MAX_RADIUS * sensorScaleFactor); + + // Tablet and mini-tablet must be within NEAR_TABLET_MAX_RADIUS in order to be grabbed. + // Mini tablet can only be grabbed the hand it's displayed on. + var tabletIndex = nearbyOverlays.indexOf(HMD.tabletID); + var miniTabletIndex = nearbyOverlays.indexOf(HMD.miniTabletID); + if (tabletIndex !== -1 || miniTabletIndex !== -1) { + var closebyOverlays = + Overlays.findOverlays(controllerLocations[h].position, NEAR_TABLET_MAX_RADIUS * sensorScaleFactor); + // Assumes that the tablet and mini-tablet are not displayed at the same time. + if (tabletIndex !== -1 && closebyOverlays.indexOf(HMD.tabletID) === -1) { + nearbyOverlays.splice(tabletIndex, 1); + } + if (miniTabletIndex !== -1 && + ((closebyOverlays.indexOf(HMD.miniTabletID) === -1) || h !== HMD.miniTabletHand)) { + nearbyOverlays.splice(miniTabletIndex, 1); + } + } + + nearbyOverlays.sort(function (a, b) { + var aPosition = Overlays.getProperty(a, "position"); + var aDistance = Vec3.distance(aPosition, controllerLocations[h].position); + var bPosition = Overlays.getProperty(b, "position"); + var bDistance = Vec3.distance(bPosition, controllerLocations[h].position); + return aDistance - bDistance; + }); + + nearbyOverlayIDs.push(nearbyOverlays); + } else { + nearbyOverlayIDs.push([]); + } + } + + // find entities near each hand + var nearbyEntityProperties = [[], []]; + var nearbyEntityPropertiesByID = {}; + for (h = LEFT_HAND; h <= RIGHT_HAND; h++) { + if (controllerLocations[h].valid) { + var controllerPosition = controllerLocations[h].position; + var findRadius = NEAR_MAX_RADIUS * sensorScaleFactor; + + if (SHOW_GRAB_SPHERE) { + if (this.grabSphereOverlays[h]) { + Overlays.editOverlay(this.grabSphereOverlays[h], { position: controllerLocations[h].position }); + } else { + var grabSphereSize = findRadius * 2; + this.grabSphereOverlays[h] = Overlays.addOverlay("sphere", { + position: controllerLocations[h].position, + dimensions: { x: grabSphereSize, y: grabSphereSize, z: grabSphereSize }, + color: { red: 30, green: 30, blue: 255 }, + alpha: 0.3, + solid: true, + visible: true, + // lineWidth: 2.0, + drawInFront: false, + grabbable: false + }); + } + } + + var nearbyEntityIDs = Entities.findEntities(controllerPosition, findRadius); + for (var j = 0; j < nearbyEntityIDs.length; j++) { + var entityID = nearbyEntityIDs[j]; + var props = Entities.getEntityProperties(entityID, DISPATCHER_PROPERTIES); + props.id = entityID; + props.distance = Vec3.distance(props.position, controllerLocations[h].position); + nearbyEntityPropertiesByID[entityID] = props; + nearbyEntityProperties[h].push(props); + } + } + } + + // raypick for each controller + var rayPicks = [ + Pointers.getPrevPickResult(_this.leftPointer), + Pointers.getPrevPickResult(_this.rightPointer) + ]; + var hudRayPicks = [ + Pointers.getPrevPickResult(_this.leftHudPointer), + Pointers.getPrevPickResult(_this.rightHudPointer) + ]; + var mouseRayPick = Pointers.getPrevPickResult(_this.mouseRayPick); + // if the pickray hit something very nearby, put it into the nearby entities list + for (h = LEFT_HAND; h <= RIGHT_HAND; h++) { + + // XXX find a way to extract searchRay from samuel's stuff + rayPicks[h].searchRay = { + origin: controllerLocations[h].position, + direction: Quat.getUp(controllerLocations[h].orientation), + length: 1000 + }; + + if (rayPicks[h].type === Picks.INTERSECTED_ENTITY) { + // XXX check to make sure this one isn't already in nearbyEntityProperties? + if (rayPicks[h].distance < NEAR_GRAB_PICK_RADIUS * sensorScaleFactor) { + var nearEntityID = rayPicks[h].objectID; + var nearbyProps = Entities.getEntityProperties(nearEntityID, DISPATCHER_PROPERTIES); + nearbyProps.id = nearEntityID; + nearbyProps.distance = rayPicks[h].distance; + nearbyEntityPropertiesByID[nearEntityID] = nearbyProps; + nearbyEntityProperties[h].push(nearbyProps); + } + } + + // sort by distance from each hand + nearbyEntityProperties[h].sort(function (a, b) { + return a.distance - b.distance; + }); + } + + // sometimes, during a HMD snap-turn, an equipped or held item wont be near + // the hand when the findEntities is done. Gather up any hand-children here. + for (h = LEFT_HAND; h <= RIGHT_HAND; h++) { + var handChildrenIDs = findHandChildEntities(h); + handChildrenIDs.forEach(function (handChildID) { + if (handChildID in nearbyEntityPropertiesByID) { + return; + } + var props = Entities.getEntityProperties(handChildID, DISPATCHER_PROPERTIES); + props.id = handChildID; + nearbyEntityPropertiesByID[handChildID] = props; + }); + } + + // also make sure we have the properties from the current module's target + for (var tIDRunningPluginName in _this.runningPluginNames) { + if (_this.runningPluginNames.hasOwnProperty(tIDRunningPluginName)) { + var targetIDs = _this.targetIDs[tIDRunningPluginName]; + if (targetIDs) { + for (var k = 0; k < targetIDs.length; k++) { + var targetID = targetIDs[k]; + if (!nearbyEntityPropertiesByID[targetID]) { + var targetProps = Entities.getEntityProperties(targetID, DISPATCHER_PROPERTIES); + targetProps.id = targetID; + nearbyEntityPropertiesByID[targetID] = targetProps; + } + } + } + } + } + + // bundle up all the data about the current situation + var controllerData = { + triggerValues: [_this.leftTriggerValue, _this.rightTriggerValue], + triggerClicks: [_this.leftTriggerClicked, _this.rightTriggerClicked], + secondaryValues: [_this.leftSecondaryValue, _this.rightSecondaryValue], + controllerLocations: controllerLocations, + nearbyEntityProperties: nearbyEntityProperties, + nearbyEntityPropertiesByID: nearbyEntityPropertiesByID, + nearbyOverlayIDs: nearbyOverlayIDs, + rayPicks: rayPicks, + hudRayPicks: hudRayPicks, + mouseRayPick: mouseRayPick + }; + if (PROFILE) { + Script.endProfileRange("dispatch.gather"); + } + + if (PROFILE) { + Script.beginProfileRange("dispatch.isReady"); + } + // check for plugins that would like to start. ask in order of increasing priority value + for (var pluginIndex = 0; pluginIndex < _this.orderedPluginNames.length; pluginIndex++) { + var orderedPluginName = _this.orderedPluginNames[pluginIndex]; + var candidatePlugin = controllerDispatcherPlugins[orderedPluginName]; + + if (_this.slotsAreAvailableForPlugin(candidatePlugin)) { + if (PROFILE) { + Script.beginProfileRange("dispatch.isReady." + orderedPluginName); + } + var readiness = candidatePlugin.isReady(controllerData, deltaTime); + if (readiness.active) { + // this plugin will start. add it to the list of running plugins and mark the + // activity-slots which this plugin consumes as "in use" + _this.runningPluginNames[orderedPluginName] = true; + _this.markSlots(candidatePlugin, orderedPluginName); + _this.pointerManager.makePointerVisible(candidatePlugin.parameters.handLaser); + if (DEBUG) { + print("controllerDispatcher running " + orderedPluginName); + } + } + if (PROFILE) { + Script.endProfileRange("dispatch.isReady." + orderedPluginName); + } + } + } + if (PROFILE) { + Script.endProfileRange("dispatch.isReady"); + } + + if (PROFILE) { + Script.beginProfileRange("dispatch.run"); + } + // give time to running plugins + for (var runningPluginName in _this.runningPluginNames) { + if (_this.runningPluginNames.hasOwnProperty(runningPluginName)) { + var plugin = controllerDispatcherPlugins[runningPluginName]; + if (!plugin) { + // plugin was deactivated while running. find the activity-slots it was using and make + // them available. + delete _this.runningPluginNames[runningPluginName]; + _this.unmarkSlotsForPluginName(runningPluginName); + } else { + if (PROFILE) { + Script.beginProfileRange("dispatch.run." + runningPluginName); + } + var runningness = plugin.run(controllerData, deltaTime); + + if (DEBUG) { + if (JSON.stringify(_this.targetIDs[runningPluginName]) != JSON.stringify(runningness.targets)) { + print("controllerDispatcher targetIDs[" + runningPluginName + "] = " + + JSON.stringify(runningness.targets)); + } + } + + _this.targetIDs[runningPluginName] = runningness.targets; + if (!runningness.active) { + // plugin is finished running, for now. remove it from the list + // of running plugins and mark its activity-slots as "not in use" + delete _this.runningPluginNames[runningPluginName]; + delete _this.targetIDs[runningPluginName]; + if (DEBUG) { + print("controllerDispatcher deleted targetIDs[" + runningPluginName + "]"); + } + _this.markSlots(plugin, false); + _this.pointerManager.makePointerInvisible(plugin.parameters.handLaser); + if (DEBUG) { + print("controllerDispatcher stopping " + runningPluginName); + } + } + _this.pointerManager.lockPointerEnd(plugin.parameters.handLaser, runningness.laserLockInfo); + if (PROFILE) { + Script.endProfileRange("dispatch.run." + runningPluginName); + } + } + } + } + _this.pointerManager.updatePointersRenderState(controllerData.triggerClicks, controllerData.triggerValues); + if (PROFILE) { + Script.endProfileRange("dispatch.run"); + } + }; + + this.leftBlacklistTabletIDs = []; + this.rightBlacklistTabletIDs = []; + + this.setLeftBlacklist = function () { + Pointers.setIgnoreItems(_this.leftPointer, _this.blacklist.concat(_this.leftBlacklistTabletIDs)); + }; + this.setRightBlacklist = function () { + Pointers.setIgnoreItems(_this.rightPointer, _this.blacklist.concat(_this.rightBlacklistTabletIDs)); + }; + + this.setBlacklist = function() { + _this.setLeftBlacklist(); + _this.setRightBlacklist(); + }; + + var MAPPING_NAME = "com.highfidelity.controllerDispatcher"; + var mapping = Controller.newMapping(MAPPING_NAME); + mapping.from([Controller.Standard.RT]).peek().to(_this.rightTriggerPress); + mapping.from([Controller.Standard.RTClick]).peek().to(_this.rightTriggerClick); + mapping.from([Controller.Standard.LT]).peek().to(_this.leftTriggerPress); + mapping.from([Controller.Standard.LTClick]).peek().to(_this.leftTriggerClick); + + mapping.from([Controller.Standard.RB]).peek().to(_this.rightSecondaryPress); + mapping.from([Controller.Standard.LB]).peek().to(_this.leftSecondaryPress); + mapping.from([Controller.Standard.LeftGrip]).peek().to(_this.leftSecondaryPress); + mapping.from([Controller.Standard.RightGrip]).peek().to(_this.rightSecondaryPress); + + Controller.enableMapping(MAPPING_NAME); + + this.leftPointer = this.pointerManager.createPointer(false, PickType.Ray, { + joint: "_CAMERA_RELATIVE_CONTROLLER_LEFTHAND", + filter: Picks.PICK_OVERLAYS | Picks.PICK_ENTITIES | Picks.PICK_INCLUDE_NONCOLLIDABLE, + triggers: [{action: Controller.Standard.LTClick, button: "Focus"}, {action: Controller.Standard.LTClick, button: "Primary"}], + posOffset: getGrabPointSphereOffset(Controller.Standard.LeftHand, true), + hover: true, + scaleWithParent: true, + distanceScaleEnd: true, + hand: LEFT_HAND + }); + Keyboard.setLeftHandLaser(this.leftPointer); + this.rightPointer = this.pointerManager.createPointer(false, PickType.Ray, { + joint: "_CAMERA_RELATIVE_CONTROLLER_RIGHTHAND", + filter: Picks.PICK_OVERLAYS | Picks.PICK_ENTITIES | Picks.PICK_INCLUDE_NONCOLLIDABLE, + triggers: [{action: Controller.Standard.RTClick, button: "Focus"}, {action: Controller.Standard.RTClick, button: "Primary"}], + posOffset: getGrabPointSphereOffset(Controller.Standard.RightHand, true), + hover: true, + scaleWithParent: true, + distanceScaleEnd: true, + hand: RIGHT_HAND + }); + Keyboard.setRightHandLaser(this.rightPointer); + this.leftHudPointer = this.pointerManager.createPointer(true, PickType.Ray, { + joint: "_CAMERA_RELATIVE_CONTROLLER_LEFTHAND", + filter: Picks.PICK_HUD, + maxDistance: DEFAULT_SEARCH_SPHERE_DISTANCE, + posOffset: getGrabPointSphereOffset(Controller.Standard.LeftHand, true), + triggers: [{action: Controller.Standard.LTClick, button: "Focus"}, {action: Controller.Standard.LTClick, button: "Primary"}], + hover: true, + scaleWithParent: true, + distanceScaleEnd: true, + hand: LEFT_HAND + }); + this.rightHudPointer = this.pointerManager.createPointer(true, PickType.Ray, { + joint: "_CAMERA_RELATIVE_CONTROLLER_RIGHTHAND", + filter: Picks.PICK_HUD, + maxDistance: DEFAULT_SEARCH_SPHERE_DISTANCE, + posOffset: getGrabPointSphereOffset(Controller.Standard.RightHand, true), + triggers: [{action: Controller.Standard.RTClick, button: "Focus"}, {action: Controller.Standard.RTClick, button: "Primary"}], + hover: true, + scaleWithParent: true, + distanceScaleEnd: true, + hand: RIGHT_HAND + }); + + this.mouseRayPick = Pointers.createPointer(PickType.Ray, { + joint: "Mouse", + filter: Picks.PICK_OVERLAYS | Picks.PICK_ENTITIES | Picks.PICK_INCLUDE_NONCOLLIDABLE, + enabled: true + }); + this.handleMessage = function (channel, data, sender) { + var message; + if (sender === MyAvatar.sessionUUID) { + try { + if (channel === 'Hifi-Hand-RayPick-Blacklist') { + message = JSON.parse(data); + var action = message.action; + var id = message.id; + var index = _this.blacklist.indexOf(id); + + if (action === 'add' && index === -1) { + _this.blacklist.push(id); + _this.setBlacklist(); + } + + if (action === 'remove') { + if (index > -1) { + _this.blacklist.splice(index, 1); + _this.setBlacklist(); + } + } + + if (action === "tablet") { + var tabletIDs = message.blacklist ? + [HMD.tabletID, HMD.tabletScreenID, HMD.homeButtonID, HMD.homeButtonHighlightID] : + []; + if (message.hand === LEFT_HAND) { + _this.leftBlacklistTabletIDs = tabletIDs; + _this.setLeftBlacklist(); + } else { + _this.rightBlacklistTabletIDs = tabletIDs; + _this.setRightBlacklist(); + } + } + } + } catch (e) { + print("WARNING: handControllerGrab.js -- error parsing message: " + data); + } + } + }; + + this.cleanup = function () { + Controller.disableMapping(MAPPING_NAME); + _this.pointerManager.removePointers(); + Pointers.removePointer(this.mouseRayPick); + }; + } + + function mouseReleaseOnOverlay(overlayID, event) { + if (HMD.homeButtonID && overlayID === HMD.homeButtonID && event.button === "Primary") { + Messages.sendLocalMessage("home", overlayID); + } + } + + var HAPTIC_STYLUS_STRENGTH = 1.0; + var HAPTIC_STYLUS_DURATION = 20.0; + function mousePress(id, event) { + if (HMD.active) { + var runningPlugins = controllerDispatcher.runningPluginNames; + if (event.id === controllerDispatcher.leftPointer && event.button === "Primary" && runningPlugins.LeftWebSurfaceLaserInput) { + Controller.triggerHapticPulse(HAPTIC_STYLUS_STRENGTH, HAPTIC_STYLUS_DURATION, LEFT_HAND); + } else if (event.id === controllerDispatcher.rightPointer && event.button === "Primary" && runningPlugins.RightWebSurfaceLaserInput) { + Controller.triggerHapticPulse(HAPTIC_STYLUS_STRENGTH, HAPTIC_STYLUS_DURATION, RIGHT_HAND); + } + } + } + + Overlays.mouseReleaseOnOverlay.connect(mouseReleaseOnOverlay); + Overlays.mousePressOnOverlay.connect(mousePress); + Entities.mousePressOnEntity.connect(mousePress); + + var controllerDispatcher = new ControllerDispatcher(); + Messages.subscribe('Hifi-Hand-RayPick-Blacklist'); + Messages.messageReceived.connect(controllerDispatcher.handleMessage); + + Script.scriptEnding.connect(controllerDispatcher.cleanup); + Script.setTimeout(controllerDispatcher.update, BASIC_TIMER_INTERVAL_MS); +}()); diff --git a/scripts/simplifiedUI/system/controllers/controllerDisplay.js b/scripts/simplifiedUI/system/controllers/controllerDisplay.js new file mode 100644 index 0000000000..e40b761307 --- /dev/null +++ b/scripts/simplifiedUI/system/controllers/controllerDisplay.js @@ -0,0 +1,292 @@ +// +// controllerDisplay.js +// +// Created by Anthony J. Thibault on 10/20/16 +// Originally created by Ryan Huffman on 9/21/2016 +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html + +/* globals createControllerDisplay:true, deleteControllerDisplay:true, Controller, Overlays, Vec3, MyAvatar, Quat */ + +function clamp(value, min, max) { + if (value < min) { + return min; + } else if (value > max) { + return max; + } + return value; +} + +function resolveHardware(path) { + if (typeof path === 'string') { + var parts = path.split("."); + function resolveInner(base, path, i) { + if (i >= path.length) { + return base; + } + return resolveInner(base[path[i]], path, ++i); + } + return resolveInner(Controller.Hardware, parts, 0); + } + return path; +} + +var DEBUG = true; +function debug() { + if (DEBUG) { + var args = Array.prototype.slice.call(arguments); + args.unshift("controllerDisplay.js | "); + print.apply(this, args); + } +} + +createControllerDisplay = function(config) { + var controllerDisplay = { + overlays: [], + partOverlays: {}, + parts: {}, + mappingName: "mapping-display-" + Math.random(), + partValues: {}, + + setVisible: function(visible) { + for (var i = 0; i < this.overlays.length; ++i) { + Overlays.editOverlay(this.overlays[i], { + visible: visible + }); + } + }, + + setPartVisible: function(partName, visible) { + // Disabled + /* + if (partName in this.partOverlays) { + for (var i = 0; i < this.partOverlays[partName].length; ++i) { + Overlays.editOverlay(this.partOverlays[partName][i], { + //visible: visible + }); + } + } + */ + }, + + setLayerForPart: function(partName, layerName) { + if (partName in this.parts) { + var part = this.parts[partName]; + if (part.textureLayers && layerName in part.textureLayers) { + var layer = part.textureLayers[layerName]; + var textures = {}; + if (layer.defaultTextureURL) { + textures[part.textureName] = layer.defaultTextureURL; + } + for (var i = 0; i < this.partOverlays[partName].length; ++i) { + Overlays.editOverlay(this.partOverlays[partName][i], { + textures: textures + }); + } + } + } + }, + + resize: function(sensorScaleFactor) { + if (this.overlays.length >= 0) { + var controller = config.controllers[0]; + var position = controller.position; + + // first overlay is main body. + var overlayID = this.overlays[0]; + var localPosition = Vec3.multiply(sensorScaleFactor, Vec3.sum(Vec3.multiplyQbyV(controller.rotation, controller.naturalPosition), position)); + var dimensions = Vec3.multiply(sensorScaleFactor, controller.dimensions); + + Overlays.editOverlay(overlayID, { + dimensions: dimensions, + localPosition: localPosition + }); + + if (controller.parts) { + var i = 1; + for (var partName in controller.parts) { + overlayID = this.overlays[i++]; + var part = controller.parts[partName]; + localPosition = Vec3.subtract(part.naturalPosition, controller.naturalPosition); + var localRotation; + var value = this.partValues[partName]; + var offset, rotation; + if (value !== undefined) { + if (part.type === "linear") { + offset = Vec3.multiply(part.maxTranslation * value, part.axis); + localPosition = Vec3.sum(localPosition, offset); + localRotation = undefined; + } else if (part.type === "joystick") { + rotation = Quat.fromPitchYawRollDegrees(value.y * part.xHalfAngle, 0, value.x * part.yHalfAngle); + if (part.originOffset) { + offset = Vec3.multiplyQbyV(rotation, part.originOffset); + offset = Vec3.subtract(part.originOffset, offset); + } else { + offset = { x: 0, y: 0, z: 0 }; + } + localPosition = Vec3.sum(offset, localPosition); + localRotation = rotation; + } else if (part.type === "rotational") { + value = clamp(value, part.minValue, part.maxValue); + var pct = (value - part.minValue) / part.maxValue; + var angle = pct * part.maxAngle; + rotation = Quat.angleAxis(angle, part.axis); + if (part.origin) { + offset = Vec3.multiplyQbyV(rotation, part.origin); + offset = Vec3.subtract(offset, part.origin); + } else { + offset = { x: 0, y: 0, z: 0 }; + } + localPosition = Vec3.sum(offset, localPosition); + localRotation = rotation; + } + } + if (localRotation !== undefined) { + Overlays.editOverlay(overlayID, { + dimensions: Vec3.multiply(sensorScaleFactor, part.naturalDimensions), + localPosition: Vec3.multiply(sensorScaleFactor, localPosition), + localRotation: localRotation + }); + } else { + Overlays.editOverlay(overlayID, { + dimensions: Vec3.multiply(sensorScaleFactor, part.naturalDimensions), + localPosition: Vec3.multiply(sensorScaleFactor, localPosition) + }); + } + } + } + } + } + }; + + var mapping = Controller.newMapping(controllerDisplay.mappingName); + for (var i = 0; i < config.controllers.length; ++i) { + var controller = config.controllers[i]; + var position = controller.position; + var sensorScaleFactor = MyAvatar.sensorToWorldScale; + + if (controller.naturalPosition) { + position = Vec3.sum(Vec3.multiplyQbyV(controller.rotation, controller.naturalPosition), position); + } else { + controller.naturalPosition = { x: 0, y: 0, z: 0 }; + } + + var baseOverlayID = Overlays.addOverlay("model", { + url: controller.modelURL, + dimensions: Vec3.multiply(sensorScaleFactor, controller.dimensions), + localRotation: controller.rotation, + localPosition: Vec3.multiply(sensorScaleFactor, position), + parentID: MyAvatar.SELF_ID, + parentJointIndex: controller.jointIndex, + ignoreRayIntersection: true + }); + + controllerDisplay.overlays.push(baseOverlayID); + + if (controller.parts) { + for (var partName in controller.parts) { + var part = controller.parts[partName]; + var localPosition = Vec3.subtract(part.naturalPosition, controller.naturalPosition); + var localRotation = { x: 0, y: 0, z: 0, w: 1 }; + + controllerDisplay.parts[partName] = controller.parts[partName]; + + var properties = { + url: part.modelURL, + localPosition: localPosition, + localRotation: localRotation, + parentID: baseOverlayID, + ignoreRayIntersection: true + }; + + if (part.defaultTextureLayer) { + var textures = {}; + textures[part.textureName] = part.textureLayers[part.defaultTextureLayer].defaultTextureURL; + properties.textures = textures; + } + + var overlayID = Overlays.addOverlay("model", properties); + + if (part.type === "rotational") { + var input = resolveHardware(part.input); + mapping.from([input]).peek().to(function(partName) { + return function(value) { + // insert the most recent controller value into controllerDisplay.partValues. + controllerDisplay.partValues[partName] = value; + controllerDisplay.resize(MyAvatar.sensorToWorldScale); + }; + }(partName)); + } else if (part.type === "touchpad") { + var visibleInput = resolveHardware(part.visibleInput); + var xInput = resolveHardware(part.xInput); + var yInput = resolveHardware(part.yInput); + + // TODO: Touchpad inputs are currently only working for half + // of the touchpad. When that is fixed, it would be useful + // to update these to display the current finger position. + mapping.from([visibleInput]).peek().to(function(value) { + }); + mapping.from([xInput]).peek().to(function(value) { + }); + mapping.from([yInput]).peek().invert().to(function(value) { + }); + } else if (part.type === "joystick") { + (function(part, partName) { + var xInput = resolveHardware(part.xInput); + var yInput = resolveHardware(part.yInput); + mapping.from([xInput]).peek().to(function(value) { + // insert the most recent controller value into controllerDisplay.partValues. + if (controllerDisplay.partValues[partName]) { + controllerDisplay.partValues[partName].x = value; + } else { + controllerDisplay.partValues[partName] = {x: value, y: 0}; + } + controllerDisplay.resize(MyAvatar.sensorToWorldScale); + }); + mapping.from([yInput]).peek().to(function(value) { + // insert the most recent controller value into controllerDisplay.partValues. + if (controllerDisplay.partValues[partName]) { + controllerDisplay.partValues[partName].y = value; + } else { + controllerDisplay.partValues[partName] = {x: 0, y: value}; + } + controllerDisplay.resize(MyAvatar.sensorToWorldScale); + }); + })(part, partName); + + } else if (part.type === "linear") { + (function(part, partName) { + var input = resolveHardware(part.input); + mapping.from([input]).peek().to(function(value) { + // insert the most recent controller value into controllerDisplay.partValues. + controllerDisplay.partValues[partName] = value; + controllerDisplay.resize(MyAvatar.sensorToWorldScale); + }); + })(part, partName); + + } else if (part.type === "static") { + // do nothing + } else { + debug("TYPE NOT SUPPORTED: ", part.type); + } + + controllerDisplay.overlays.push(overlayID); + if (!(partName in controllerDisplay.partOverlays)) { + controllerDisplay.partOverlays[partName] = []; + } + controllerDisplay.partOverlays[partName].push(overlayID); + } + } + } + Controller.enableMapping(controllerDisplay.mappingName); + controllerDisplay.resize(MyAvatar.sensorToWorldScale); + return controllerDisplay; +}; + +deleteControllerDisplay = function(controllerDisplay) { + for (var i = 0; i < controllerDisplay.overlays.length; ++i) { + Overlays.deleteOverlay(controllerDisplay.overlays[i]); + } + Controller.disableMapping(controllerDisplay.mappingName); +}; diff --git a/scripts/simplifiedUI/system/controllers/controllerDisplayManager.js b/scripts/simplifiedUI/system/controllers/controllerDisplayManager.js new file mode 100644 index 0000000000..f93f8b1624 --- /dev/null +++ b/scripts/simplifiedUI/system/controllers/controllerDisplayManager.js @@ -0,0 +1,195 @@ +// +// controllerDisplayManager.js +// +// Created by Anthony J. Thibault on 10/20/16 +// Originally created by Ryan Huffman on 9/21/2016 +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html + +/* globals ControllerDisplayManager:true, createControllerDisplay, deleteControllerDisplay, + VIVE_CONTROLLER_CONFIGURATION_LEFT, VIVE_CONTROLLER_CONFIGURATION_RIGHT, Script, HMD, Controller, + MyAvatar, Overlays, TOUCH_CONTROLLER_CONFIGURATION_LEFT, TOUCH_CONTROLLER_CONFIGURATION_RIGHT, Messages */ +/* eslint indent: ["error", 4, { "outerIIFEBody": 0 }] */ + +(function () { + +Script.include("controllerDisplay.js"); +Script.include("viveControllerConfiguration.js"); +Script.include("touchControllerConfiguration.js"); + +var HIDE_CONTROLLERS_ON_EQUIP = false; + +// +// Management of controller display +// +ControllerDisplayManager = function() { + var self = this; + var controllerLeft = null; + var controllerRight = null; + var controllerCheckerIntervalID = null; + + this.setLeftVisible = function(visible) { + if (controllerLeft) { + controllerLeft.setVisible(visible); + } + }; + + this.setRightVisible = function(visible) { + if (controllerRight) { + controllerRight.setVisible(visible); + } + }; + + function updateControllers() { + if (HMD.active && HMD.shouldShowHandControllers()) { + var leftConfig = null; + var rightConfig = null; + + if ("Vive" in Controller.Hardware) { + leftConfig = VIVE_CONTROLLER_CONFIGURATION_LEFT; + rightConfig = VIVE_CONTROLLER_CONFIGURATION_RIGHT; + } + + if ("OculusTouch" in Controller.Hardware) { + leftConfig = TOUCH_CONTROLLER_CONFIGURATION_LEFT; + rightConfig = TOUCH_CONTROLLER_CONFIGURATION_RIGHT; + } + + if (leftConfig !== null && rightConfig !== null) { + if (controllerLeft === null) { + controllerLeft = createControllerDisplay(leftConfig); + controllerLeft.setVisible(true); + } + if (controllerRight === null) { + controllerRight = createControllerDisplay(rightConfig); + controllerRight.setVisible(true); + } + // We've found the controllers, we no longer need to look for active controllers + if (controllerCheckerIntervalID) { + Script.clearInterval(controllerCheckerIntervalID); + controllerCheckerIntervalID = null; + } + + } else { + self.deleteControllerDisplays(); + if (!controllerCheckerIntervalID) { + controllerCheckerIntervalID = Script.setInterval(updateControllers, 1000); + } + } + } else { + // We aren't in HMD mode, we no longer need to look for active controllers + if (controllerCheckerIntervalID) { + Script.clearInterval(controllerCheckerIntervalID); + controllerCheckerIntervalID = null; + } + self.deleteControllerDisplays(); + } + } + + function resizeControllers(sensorScaleFactor) { + if (controllerLeft) { + controllerLeft.resize(sensorScaleFactor); + } + if (controllerRight) { + controllerRight.resize(sensorScaleFactor); + } + } + + var handleMessages = function(channel, message, sender) { + var i, data, name, visible; + if (!controllerLeft && !controllerRight) { + return; + } + + if (sender === MyAvatar.sessionUUID) { + if (channel === 'Controller-Display') { + data = JSON.parse(message); + name = data.name; + visible = data.visible; + if (controllerLeft) { + if (name in controllerLeft.annotations) { + for (i = 0; i < controllerLeft.annotations[name].length; ++i) { + Overlays.editOverlay(controllerLeft.annotations[name][i], { visible: visible }); + } + } + } + if (controllerRight) { + if (name in controllerRight.annotations) { + for (i = 0; i < controllerRight.annotations[name].length; ++i) { + Overlays.editOverlay(controllerRight.annotations[name][i], { visible: visible }); + } + } + } + } else if (channel === 'Controller-Display-Parts') { + data = JSON.parse(message); + for (name in data) { + visible = data[name]; + if (controllerLeft) { + controllerLeft.setPartVisible(name, visible); + } + if (controllerRight) { + controllerRight.setPartVisible(name, visible); + } + } + } else if (channel === 'Controller-Set-Part-Layer') { + data = JSON.parse(message); + for (name in data) { + var layer = data[name]; + if (controllerLeft) { + controllerLeft.setLayerForPart(name, layer); + } + if (controllerRight) { + controllerRight.setLayerForPart(name, layer); + } + } + } else if (channel === 'Hifi-Object-Manipulation') { + if (HIDE_CONTROLLERS_ON_EQUIP) { + data = JSON.parse(message); + visible = data.action !== 'equip'; + if (data.joint === "LeftHand") { + self.setLeftVisible(visible); + } else if (data.joint === "RightHand") { + self.setRightVisible(visible); + } + } + } + } + }; + + Messages.messageReceived.connect(handleMessages); + + this.deleteControllerDisplays = function() { + if (controllerLeft) { + deleteControllerDisplay(controllerLeft); + controllerLeft = null; + } + if (controllerRight) { + deleteControllerDisplay(controllerRight); + controllerRight = null; + } + }; + + this.destroy = function() { + Messages.messageReceived.disconnect(handleMessages); + + HMD.displayModeChanged.disconnect(updateControllers); + HMD.shouldShowHandControllersChanged.disconnect(updateControllers); + + self.deleteControllerDisplays(); + }; + + HMD.displayModeChanged.connect(updateControllers); + HMD.shouldShowHandControllersChanged.connect(updateControllers); + MyAvatar.sensorToWorldScaleChanged.connect(resizeControllers); + + updateControllers(); +}; + +var controllerDisplayManager = new ControllerDisplayManager(); + +Script.scriptEnding.connect(function () { + controllerDisplayManager.destroy(); +}); + +}()); diff --git a/scripts/simplifiedUI/system/controllers/controllerModules/disableOtherModule.js b/scripts/simplifiedUI/system/controllers/controllerModules/disableOtherModule.js new file mode 100644 index 0000000000..7636c56f65 --- /dev/null +++ b/scripts/simplifiedUI/system/controllers/controllerModules/disableOtherModule.js @@ -0,0 +1,83 @@ +"use strict"; + +// disableOtherModule.js +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html + + +/* global Script, MyAvatar, RIGHT_HAND, LEFT_HAND, enableDispatcherModule, disableDispatcherModule, + makeDispatcherModuleParameters, makeRunningValues, getEnabledModuleByName, Messages +*/ + +Script.include("/~/system/libraries/controllerDispatcherUtils.js"); + +(function() { + function DisableModules(hand) { + this.hand = hand; + this.disableModules = false; + this.parameters = makeDispatcherModuleParameters( + 90, + this.hand === RIGHT_HAND ? + ["rightHand", "rightHandEquip", "rightHandTrigger"] : + ["leftHand", "leftHandEquip", "leftHandTrigger"], + [], + 100); + + this.isReady = function(controllerData) { + if (this.disableModules) { + return makeRunningValues(true, [], []); + } + return false; + }; + + this.run = function(controllerData) { + var teleportModuleName = this.hand === RIGHT_HAND ? "RightTeleporter" : "LeftTeleporter"; + var teleportModule = getEnabledModuleByName(teleportModuleName); + + if (teleportModule) { + var ready = teleportModule.isReady(controllerData); + if (ready.active) { + return makeRunningValues(false, [], []); + } + } + if (!this.disableModules) { + return makeRunningValues(false, [], []); + } + return makeRunningValues(true, [], []); + }; + } + + var leftDisableModules = new DisableModules(LEFT_HAND); + var rightDisableModules = new DisableModules(RIGHT_HAND); + + enableDispatcherModule("LeftDisableModules", leftDisableModules); + enableDispatcherModule("RightDisableModules", rightDisableModules); + function handleMessage(channel, message, sender) { + if (sender === MyAvatar.sessionUUID) { + if (channel === 'Hifi-Hand-Disabler') { + if (message === 'left') { + leftDisableModules.disableModules = true; + } else if (message === 'right') { + rightDisableModules.disableModules = true; + } else if (message === 'both') { + leftDisableModules.disableModules = true; + rightDisableModules.disableModules = true; + } else if (message === 'none') { + leftDisableModules.disableModules = false; + rightDisableModules.disableModules = false; + } else { + print("disableOtherModule -- unknown command: " + message); + } + } + } + } + + Messages.subscribe('Hifi-Hand-Disabler'); + function cleanup() { + disableDispatcherModule("LeftDisableModules"); + disableDispatcherModule("RightDisableModules"); + } + Messages.messageReceived.connect(handleMessage); + Script.scriptEnding.connect(cleanup); +}()); diff --git a/scripts/simplifiedUI/system/controllers/controllerModules/equipEntity.js b/scripts/simplifiedUI/system/controllers/controllerModules/equipEntity.js new file mode 100644 index 0000000000..54b56ff271 --- /dev/null +++ b/scripts/simplifiedUI/system/controllers/controllerModules/equipEntity.js @@ -0,0 +1,867 @@ +"use strict"; + +// equipEntity.js +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html + + +/* global Script, Entities, MyAvatar, Controller, RIGHT_HAND, LEFT_HAND, Camera, print, getControllerJointIndex, + enableDispatcherModule, disableDispatcherModule, Messages, makeDispatcherModuleParameters, + makeRunningValues, Settings, entityHasActions, Vec3, Overlays, flatten, Xform, getControllerWorldLocation, ensureDynamic, + entityIsCloneable, cloneEntity, DISPATCHER_PROPERTIES, Uuid, isInEditMode, getGrabbableData, + entityIsEquippable, HMD +*/ + +Script.include("/~/system/libraries/Xform.js"); +Script.include("/~/system/libraries/controllerDispatcherUtils.js"); +Script.include("/~/system/libraries/controllers.js"); +Script.include("/~/system/libraries/cloneEntityUtils.js"); + + +var DEFAULT_SPHERE_MODEL_URL = "http://hifi-content.s3.amazonaws.com/alan/dev/equip-Fresnel-3.fbx"; +var EQUIP_SPHERE_SCALE_FACTOR = 0.65; + + +// Each overlayInfoSet describes a single equip hotspot. +// It is an object with the following keys: +// timestamp - last time this object was updated, used to delete stale hotspot overlays. +// entityID - entity assosicated with this hotspot +// localPosition - position relative to the entity +// hotspot - hotspot object +// overlays - array of overlay objects created by Overlay.addOverlay() +// currentSize - current animated scale value +// targetSize - the target of our scale animations +// type - "sphere" or "model". +function EquipHotspotBuddy() { + // holds map from {string} hotspot.key to {object} overlayInfoSet. + this.map = {}; + + // array of all hotspots that are highlighed. + this.highlightedHotspots = []; +} +EquipHotspotBuddy.prototype.clear = function() { + var keys = Object.keys(this.map); + for (var i = 0; i < keys.length; i++) { + var overlayInfoSet = this.map[keys[i]]; + this.deleteOverlayInfoSet(overlayInfoSet); + } + this.map = {}; + this.highlightedHotspots = []; +}; +EquipHotspotBuddy.prototype.highlightHotspot = function(hotspot) { + this.highlightedHotspots.push(hotspot.key); +}; +EquipHotspotBuddy.prototype.updateHotspot = function(hotspot, timestamp) { + var overlayInfoSet = this.map[hotspot.key]; + if (!overlayInfoSet) { + // create a new overlayInfoSet + overlayInfoSet = { + timestamp: timestamp, + entityID: hotspot.entityID, + localPosition: hotspot.localPosition, + hotspot: hotspot, + currentSize: 0, + targetSize: 1, + overlays: [] + }; + + var dimensions = hotspot.radius * 2 * EQUIP_SPHERE_SCALE_FACTOR; + + if (hotspot.indicatorURL) { + dimensions = hotspot.indicatorScale; + } + + // override default sphere with a user specified model, if it exists. + overlayInfoSet.overlays.push(Overlays.addOverlay("model", { + name: "hotspot overlay", + url: hotspot.indicatorURL ? hotspot.indicatorURL : DEFAULT_SPHERE_MODEL_URL, + position: hotspot.worldPosition, + rotation: { + x: 0, + y: 0, + z: 0, + w: 1 + }, + dimensions: dimensions, + ignoreRayIntersection: true + })); + overlayInfoSet.type = "model"; + this.map[hotspot.key] = overlayInfoSet; + } else { + overlayInfoSet.timestamp = timestamp; + } +}; +EquipHotspotBuddy.prototype.updateHotspots = function(hotspots, timestamp) { + var _this = this; + hotspots.forEach(function(hotspot) { + _this.updateHotspot(hotspot, timestamp); + }); + this.highlightedHotspots = []; +}; +EquipHotspotBuddy.prototype.update = function(deltaTime, timestamp, controllerData) { + + var HIGHLIGHT_SIZE = 1.1; + var NORMAL_SIZE = 1.0; + + var keys = Object.keys(this.map); + for (var i = 0; i < keys.length; i++) { + var overlayInfoSet = this.map[keys[i]]; + + // this overlayInfo is highlighted. + if (this.highlightedHotspots.indexOf(keys[i]) !== -1) { + overlayInfoSet.targetSize = HIGHLIGHT_SIZE; + } else { + overlayInfoSet.targetSize = NORMAL_SIZE; + } + + // start to fade out this hotspot. + if (overlayInfoSet.timestamp !== timestamp) { + overlayInfoSet.targetSize = 0; + } + + // animate the size. + var SIZE_TIMESCALE = 0.1; + var tau = deltaTime / SIZE_TIMESCALE; + if (tau > 1.0) { + tau = 1.0; + } + overlayInfoSet.currentSize += (overlayInfoSet.targetSize - overlayInfoSet.currentSize) * tau; + + if (overlayInfoSet.timestamp !== timestamp && overlayInfoSet.currentSize <= 0.05) { + // this is an old overlay, that has finished fading out, delete it! + overlayInfoSet.overlays.forEach(Overlays.deleteOverlay); + delete this.map[keys[i]]; + } else { + // update overlay position, rotation to follow the object it's attached to. + var props = controllerData.nearbyEntityPropertiesByID[overlayInfoSet.entityID]; + if (props) { + var entityXform = new Xform(props.rotation, props.position); + var position = entityXform.xformPoint(overlayInfoSet.localPosition); + + var dimensions; + if (overlayInfoSet.hotspot.indicatorURL) { + var ratio = overlayInfoSet.currentSize / overlayInfoSet.targetSize; + dimensions = { + x: overlayInfoSet.hotspot.dimensions.x * ratio, + y: overlayInfoSet.hotspot.dimensions.y * ratio, + z: overlayInfoSet.hotspot.dimensions.z * ratio + }; + } else { + dimensions = (overlayInfoSet.hotspot.radius / 2) * overlayInfoSet.currentSize; + } + + overlayInfoSet.overlays.forEach(function(overlay) { + Overlays.editOverlay(overlay, { + position: position, + rotation: props.rotation, + dimensions: dimensions + }); + }); + } else { + overlayInfoSet.overlays.forEach(Overlays.deleteOverlay); + delete this.map[keys[i]]; + } + } + } +}; + + +(function() { + + var ATTACH_POINT_SETTINGS = "io.highfidelity.attachPoints"; + + var HAPTIC_PULSE_STRENGTH = 1.0; + var HAPTIC_PULSE_DURATION = 13.0; + var HAPTIC_TEXTURE_STRENGTH = 0.1; + var HAPTIC_TEXTURE_DURATION = 3.0; + var HAPTIC_TEXTURE_DISTANCE = 0.002; + var HAPTIC_DEQUIP_STRENGTH = 0.75; + var HAPTIC_DEQUIP_DURATION = 50.0; + + var TRIGGER_SMOOTH_RATIO = 0.1; // Time averaging of trigger - 0.0 disables smoothing + var TRIGGER_OFF_VALUE = 0.1; + var TRIGGER_ON_VALUE = TRIGGER_OFF_VALUE + 0.05; // Squeezed just enough to activate search or near grab + var BUMPER_ON_VALUE = 0.5; + var ATTACHPOINT_MAX_DISTANCE = 3.0; + + // var EMPTY_PARENT_ID = "{00000000-0000-0000-0000-000000000000}"; + + var UNEQUIP_KEY = "u"; + + function getWearableData(props) { + if (props.grab.equippable) { + return { + joints: { + LeftHand: [ props.grab.equippableLeftPosition, props.grab.equippableLeftRotation ], + RightHand: [ props.grab.equippableRightPosition, props.grab.equippableRightRotation ] + }, + indicatorURL: props.grab.equippableIndicatorURL, + indicatorScale: props.grab.equippableIndicatorScale, + indicatorOffset: props.grab.equippableIndicatorOffset + }; + } else { + return null; + } + } + + function getAttachPointSettings() { + try { + var str = Settings.getValue(ATTACH_POINT_SETTINGS); + if (str === "false" || str === "") { + return {}; + } else { + return JSON.parse(str); + } + } catch (err) { + print("Error parsing attachPointSettings: " + err); + return {}; + } + } + + function setAttachPointSettings(attachPointSettings) { + var str = JSON.stringify(attachPointSettings); + Settings.setValue(ATTACH_POINT_SETTINGS, str); + } + + function getAttachPointForHotspotFromSettings(hotspot, hand) { + var skeletonModelURL = MyAvatar.skeletonModelURL; + var attachPointSettings = getAttachPointSettings(); + var avatarSettingsData = attachPointSettings[skeletonModelURL]; + if (avatarSettingsData) { + var jointName = (hand === RIGHT_HAND) ? "RightHand" : "LeftHand"; + var joints = avatarSettingsData[hotspot.key]; + if (joints) { + // make sure they are reasonable + if (joints[jointName] && joints[jointName][0] && + Vec3.length(joints[jointName][0]) > ATTACHPOINT_MAX_DISTANCE) { + print("equipEntity -- Warning: rejecting settings attachPoint " + Vec3.length(joints[jointName][0])); + return undefined; + } + return joints[jointName]; + } + } + return undefined; + } + + function storeAttachPointForHotspotInSettings(hotspot, hand, offsetPosition, offsetRotation) { + var attachPointSettings = getAttachPointSettings(); + var skeletonModelURL = MyAvatar.skeletonModelURL; + var avatarSettingsData = attachPointSettings[skeletonModelURL]; + if (!avatarSettingsData) { + avatarSettingsData = {}; + attachPointSettings[skeletonModelURL] = avatarSettingsData; + } + var jointName = (hand === RIGHT_HAND) ? "RightHand" : "LeftHand"; + var joints = avatarSettingsData[hotspot.key]; + if (!joints) { + joints = {}; + avatarSettingsData[hotspot.key] = joints; + } + joints[jointName] = [offsetPosition, offsetRotation]; + setAttachPointSettings(attachPointSettings); + } + + function clearAttachPoints() { + setAttachPointSettings({}); + } + + function EquipEntity(hand) { + this.hand = hand; + this.targetEntityID = null; + this.prevHandIsUpsideDown = false; + this.triggerValue = 0; + this.messageGrabEntity = false; + this.grabEntityProps = null; + this.shouldSendStart = false; + this.equipedWithSecondary = false; + this.handHasBeenRightsideUp = false; + + this.parameters = makeDispatcherModuleParameters( + 115, + this.hand === RIGHT_HAND ? ["rightHand", "rightHandEquip"] : ["leftHand", "leftHandEquip"], + [], + 100); + + var equipHotspotBuddy = new EquipHotspotBuddy(); + + this.setMessageGrabData = function(entityProperties) { + if (entityProperties) { + this.messageGrabEntity = true; + this.grabEntityProps = entityProperties; + } + }; + + // returns a list of all equip-hotspots assosiated with this entity. + // @param {UUID} entityID + // @returns {Object[]} array of objects with the following fields. + // * key {string} a string that can be used to uniquely identify this hotspot + // * entityID {UUID} + // * localPosition {Vec3} position of the hotspot in object space. + // * worldPosition {vec3} position of the hotspot in world space. + // * radius {number} radius of equip hotspot + // * joints {Object} keys are joint names values are arrays of two elements: + // offset position {Vec3} and offset rotation {Quat}, both are in the coordinate system of the joint. + // * indicatorURL {string} url for model to use instead of default sphere. + // * indicatorScale {Vec3} scale factor for model + this.collectEquipHotspots = function(props) { + var result = []; + var entityID = props.id; + var entityXform = new Xform(props.rotation, props.position); + + var wearableProps = getWearableData(props); + var sensorToScaleFactor = MyAvatar.sensorToWorldScale; + if (wearableProps && wearableProps.joints) { + result.push({ + key: entityID.toString() + "0", + entityID: entityID, + localPosition: wearableProps.indicatorOffset, + worldPosition: entityXform.pos, + radius: ((wearableProps.indicatorScale.x + + wearableProps.indicatorScale.y + + wearableProps.indicatorScale.z) / 3) * sensorToScaleFactor, + dimensions: wearableProps.indicatorScale, + joints: wearableProps.joints, + indicatorURL: wearableProps.indicatorURL, + indicatorScale: wearableProps.indicatorScale, + }); + } + return result; + }; + + this.hotspotIsEquippable = function(hotspot, controllerData) { + var props = controllerData.nearbyEntityPropertiesByID[hotspot.entityID]; + + var hasParent = true; + if (props.parentID === Uuid.NULL) { + hasParent = false; + } + + if (hasParent || entityHasActions(hotspot.entityID)) { + return false; + } + + return true; + }; + + this.handToController = function() { + return (this.hand === RIGHT_HAND) ? Controller.Standard.RightHand : Controller.Standard.LeftHand; + }; + + this.updateSmoothedTrigger = function(controllerData) { + var triggerValue = controllerData.triggerValues[this.hand]; + // smooth out trigger value + this.triggerValue = (this.triggerValue * TRIGGER_SMOOTH_RATIO) + + (triggerValue * (1.0 - TRIGGER_SMOOTH_RATIO)); + }; + + this.triggerSmoothedGrab = function() { + return this.triggerClicked; + }; + + this.triggerSmoothedSqueezed = function() { + return this.triggerValue > TRIGGER_ON_VALUE; + }; + + this.triggerSmoothedReleased = function() { + return this.triggerValue < TRIGGER_OFF_VALUE; + }; + + this.secondaryReleased = function() { + return this.rawSecondaryValue < BUMPER_ON_VALUE; + }; + + this.secondarySmoothedSqueezed = function() { + return this.rawSecondaryValue > BUMPER_ON_VALUE; + }; + + this.chooseNearEquipHotspots = function(candidateEntityProps, controllerData) { + var _this = this; + var collectedHotspots = flatten(candidateEntityProps.map(function(props) { + return _this.collectEquipHotspots(props); + })); + var controllerLocation = controllerData.controllerLocations[_this.hand]; + var worldControllerPosition = controllerLocation.position; + var equippableHotspots = collectedHotspots.filter(function(hotspot) { + var hotspotDistance = Vec3.distance(hotspot.worldPosition, worldControllerPosition); + return _this.hotspotIsEquippable(hotspot, controllerData) && + hotspotDistance < hotspot.radius; + }); + return equippableHotspots; + }; + + this.cloneHotspot = function(props, controllerData) { + if (entityIsCloneable(props)) { + var cloneID = cloneEntity(props); + return cloneID; + } + + return null; + }; + + this.chooseBestEquipHotspot = function(candidateEntityProps, controllerData) { + var equippableHotspots = this.chooseNearEquipHotspots(candidateEntityProps, controllerData); + if (equippableHotspots.length > 0) { + // sort by distance; + var controllerLocation = controllerData.controllerLocations[this.hand]; + var worldControllerPosition = controllerLocation.position; + equippableHotspots.sort(function(a, b) { + var aDistance = Vec3.distance(a.worldPosition, worldControllerPosition); + var bDistance = Vec3.distance(b.worldPosition, worldControllerPosition); + return aDistance - bDistance; + }); + return equippableHotspots[0]; + } else { + return null; + } + }; + + this.dropGestureReset = function() { + this.prevHandIsUpsideDown = false; + }; + + this.dropGestureProcess = function (deltaTime) { + var worldHandRotation = getControllerWorldLocation(this.handToController(), true).orientation; + var localHandUpAxis = this.hand === RIGHT_HAND ? { x: 1, y: 0, z: 0 } : { x: -1, y: 0, z: 0 }; + var worldHandUpAxis = Vec3.multiplyQbyV(worldHandRotation, localHandUpAxis); + var DOWN = { x: 0, y: -1, z: 0 }; + + var DROP_ANGLE = Math.PI / 3; + var HYSTERESIS_FACTOR = 1.1; + var ROTATION_ENTER_THRESHOLD = Math.cos(DROP_ANGLE); + var ROTATION_EXIT_THRESHOLD = Math.cos(DROP_ANGLE * HYSTERESIS_FACTOR); + var rotationThreshold = this.prevHandIsUpsideDown ? ROTATION_EXIT_THRESHOLD : ROTATION_ENTER_THRESHOLD; + + var handIsUpsideDown = false; + if (Vec3.dot(worldHandUpAxis, DOWN) > rotationThreshold) { + handIsUpsideDown = true; + } + + if (handIsUpsideDown !== this.prevHandIsUpsideDown) { + this.prevHandIsUpsideDown = handIsUpsideDown; + Controller.triggerHapticPulse(HAPTIC_DEQUIP_STRENGTH, HAPTIC_DEQUIP_DURATION, this.hand); + } + + return handIsUpsideDown; + }; + + this.clearEquipHaptics = function() { + this.prevPotentialEquipHotspot = null; + }; + + this.updateEquipHaptics = function(potentialEquipHotspot, currentLocation) { + if (potentialEquipHotspot && !this.prevPotentialEquipHotspot || + !potentialEquipHotspot && this.prevPotentialEquipHotspot) { + Controller.triggerHapticPulse(HAPTIC_TEXTURE_STRENGTH, HAPTIC_TEXTURE_DURATION, this.hand); + this.lastHapticPulseLocation = currentLocation; + } else if (potentialEquipHotspot && + Vec3.distance(this.lastHapticPulseLocation, currentLocation) > HAPTIC_TEXTURE_DISTANCE) { + Controller.triggerHapticPulse(HAPTIC_TEXTURE_STRENGTH, HAPTIC_TEXTURE_DURATION, this.hand); + this.lastHapticPulseLocation = currentLocation; + } + this.prevPotentialEquipHotspot = potentialEquipHotspot; + }; + + this.startEquipEntity = function (controllerData) { + var _this = this; + + this.dropGestureReset(); + this.clearEquipHaptics(); + Controller.triggerHapticPulse(HAPTIC_PULSE_STRENGTH, HAPTIC_PULSE_DURATION, this.hand); + + var grabbedProperties = Entities.getEntityProperties(this.targetEntityID, DISPATCHER_PROPERTIES); + var grabData = getGrabbableData(grabbedProperties); + + // if an object is "equipped" and has a predefined offset, use it. + if (this.grabbedHotspot) { + var offsets = getAttachPointForHotspotFromSettings(this.grabbedHotspot, this.hand); + if (offsets) { + this.offsetPosition = offsets[0]; + this.offsetRotation = offsets[1]; + } else { + var handJointName = this.hand === RIGHT_HAND ? "RightHand" : "LeftHand"; + if (this.grabbedHotspot.joints[handJointName]) { + this.offsetPosition = this.grabbedHotspot.joints[handJointName][0]; + this.offsetRotation = this.grabbedHotspot.joints[handJointName][1]; + } + } + } + + var handJointIndex; + if (HMD.mounted && HMD.isHandControllerAvailable() && grabData.grabFollowsController) { + handJointIndex = this.controllerJointIndex; + } else { + handJointIndex = MyAvatar.getJointIndex(this.hand === RIGHT_HAND ? "RightHand" : "LeftHand"); + } + + var reparentProps = { + parentID: MyAvatar.SELF_ID, + parentJointIndex: handJointIndex, + localVelocity: {x: 0, y: 0, z: 0}, + localAngularVelocity: {x: 0, y: 0, z: 0}, + localPosition: this.offsetPosition, + localRotation: this.offsetRotation + }; + + var isClone = false; + if (entityIsCloneable(grabbedProperties)) { + var cloneID = this.cloneHotspot(grabbedProperties, controllerData); + this.targetEntityID = cloneID; + controllerData.nearbyEntityPropertiesByID[this.targetEntityID] = grabbedProperties; + isClone = true; + } else if (grabbedProperties.locked) { + this.grabbedHotspot = null; + this.targetEntityID = null; + return; + } + + + // HACK -- when + // https://highfidelity.fogbugz.com/f/cases/21767/entity-edits-shortly-after-an-add-often-fail + // is resolved, this can just be an editEntity rather than a setTimeout. + this.editDelayTimeout = Script.setTimeout(function () { + _this.editDelayTimeout = null; + Entities.editEntity(_this.targetEntityID, reparentProps); + }, 100); + + // we don't want to send startEquip message until the trigger is released. otherwise, + // guns etc will fire right as they are equipped. + this.shouldSendStart = true; + + Messages.sendMessage('Hifi-Object-Manipulation', JSON.stringify({ + action: 'equip', + grabbedEntity: this.targetEntityID, + joint: this.hand === RIGHT_HAND ? "RightHand" : "LeftHand" + })); + + var grabEquipCheck = function() { + var args = [_this.hand === RIGHT_HAND ? "right" : "left", MyAvatar.sessionUUID]; + Entities.callEntityMethod(_this.targetEntityID, "startEquip", args); + }; + + if (isClone) { + // 100 ms seems to be sufficient time to force the check even occur after the object has been initialized. + Script.setTimeout(grabEquipCheck, 100); + } + }; + + this.endEquipEntity = function () { + + if (this.editDelayTimeout) { + Script.clearTimeout(this.editDelayTimeout); + this.editDelayTimeout = null; + } + + this.storeAttachPointInSettings(); + Entities.editEntity(this.targetEntityID, { + parentID: Uuid.NULL, + parentJointIndex: -1 + }); + + var args = [this.hand === RIGHT_HAND ? "right" : "left", MyAvatar.sessionUUID]; + Entities.callEntityMethod(this.targetEntityID, "releaseEquip", args); + + Messages.sendMessage('Hifi-Object-Manipulation', JSON.stringify({ + action: 'release', + grabbedEntity: this.targetEntityID, + joint: this.hand === RIGHT_HAND ? "RightHand" : "LeftHand" + })); + + ensureDynamic(this.targetEntityID); + this.targetEntityID = null; + this.messageGrabEntity = false; + this.grabEntityProps = null; + }; + + this.updateInputs = function (controllerData) { + this.rawTriggerValue = controllerData.triggerValues[this.hand]; + this.triggerClicked = controllerData.triggerClicks[this.hand]; + this.rawSecondaryValue = controllerData.secondaryValues[this.hand]; + this.updateSmoothedTrigger(controllerData); + }; + + this.checkNearbyHotspots = function (controllerData, deltaTime, timestamp) { + this.controllerJointIndex = getControllerJointIndex(this.hand); + + if (this.triggerSmoothedReleased() && this.secondaryReleased()) { + this.waitForTriggerRelease = false; + } + + var controllerLocation = getControllerWorldLocation(this.handToController(), true); + var worldHandPosition = controllerLocation.position; + var candidateEntityProps = controllerData.nearbyEntityProperties[this.hand]; + + + var potentialEquipHotspot = null; + if (this.messageGrabEntity) { + var hotspots = this.collectEquipHotspots(this.grabEntityProps); + if (hotspots.length > -1) { + potentialEquipHotspot = hotspots[0]; + } + } else { + potentialEquipHotspot = this.chooseBestEquipHotspot(candidateEntityProps, controllerData); + } + + if (!this.waitForTriggerRelease) { + this.updateEquipHaptics(potentialEquipHotspot, worldHandPosition); + } + + var nearEquipHotspots = this.chooseNearEquipHotspots(candidateEntityProps, controllerData); + equipHotspotBuddy.updateHotspots(nearEquipHotspots, timestamp); + if (potentialEquipHotspot) { + equipHotspotBuddy.highlightHotspot(potentialEquipHotspot); + } + + equipHotspotBuddy.update(deltaTime, timestamp, controllerData); + + // if the potentialHotspot is cloneable, clone it and return it + // if the potentialHotspot is not cloneable and locked return null + if (potentialEquipHotspot && + (((this.triggerSmoothedSqueezed() || this.secondarySmoothedSqueezed()) && !this.waitForTriggerRelease) || + this.messageGrabEntity)) { + this.grabbedHotspot = potentialEquipHotspot; + this.targetEntityID = this.grabbedHotspot.entityID; + this.startEquipEntity(controllerData); + this.equipedWithSecondary = this.secondarySmoothedSqueezed(); + return makeRunningValues(true, [this.targetEntityID], []); + } else { + return makeRunningValues(false, [], []); + } + }; + + this.isTargetIDValid = function(controllerData) { + var entityProperties = controllerData.nearbyEntityPropertiesByID[this.targetEntityID]; + return entityProperties && "type" in entityProperties; + }; + + this.isReady = function (controllerData, deltaTime) { + var timestamp = Date.now(); + this.updateInputs(controllerData); + this.handHasBeenRightsideUp = false; + return this.checkNearbyHotspots(controllerData, deltaTime, timestamp); + }; + + this.run = function (controllerData, deltaTime) { + var timestamp = Date.now(); + this.updateInputs(controllerData); + + if (!this.messageGrabEntity && !this.isTargetIDValid(controllerData)) { + this.endEquipEntity(); + return makeRunningValues(false, [], []); + } + + if (!this.targetEntityID) { + return this.checkNearbyHotspots(controllerData, deltaTime, timestamp); + } + + if (controllerData.secondaryValues[this.hand] && !this.equipedWithSecondary) { + // this.secondaryReleased() will always be true when not depressed + // so we cannot simply rely on that for release - ensure that the + // trigger was first "prepared" by being pushed in before the release + this.preparingHoldRelease = true; + } + + if (this.preparingHoldRelease && !controllerData.secondaryValues[this.hand]) { + // we have an equipped object and the secondary trigger was released + // short-circuit the other checks and release it + this.preparingHoldRelease = false; + this.endEquipEntity(); + return makeRunningValues(false, [], []); + } + + var handIsUpsideDown = this.dropGestureProcess(deltaTime); + var dropDetected = false; + if (this.handHasBeenRightsideUp) { + dropDetected = handIsUpsideDown; + } + if (!handIsUpsideDown) { + this.handHasBeenRightsideUp = true; + } + + if (this.triggerSmoothedReleased() || this.secondaryReleased()) { + if (this.shouldSendStart) { + // we don't want to send startEquip message until the trigger is released. otherwise, + // guns etc will fire right as they are equipped. + var startArgs = [this.hand === RIGHT_HAND ? "right" : "left", MyAvatar.sessionUUID]; + Entities.callEntityMethod(this.targetEntityID, "startEquip", startArgs); + this.shouldSendStart = false; + } + this.waitForTriggerRelease = false; + if (this.secondaryReleased() && this.equipedWithSecondary) { + this.equipedWithSecondary = false; + } + } + + if (dropDetected && this.prevDropDetected !== dropDetected) { + this.waitForTriggerRelease = true; + } + + // highlight the grabbed hotspot when the dropGesture is detected. + if (dropDetected && this.grabbedHotspot) { + equipHotspotBuddy.updateHotspot(this.grabbedHotspot, timestamp); + equipHotspotBuddy.highlightHotspot(this.grabbedHotspot); + } + + if (dropDetected && !this.waitForTriggerRelease && this.triggerSmoothedGrab()) { + this.waitForTriggerRelease = true; + // store the offset attach points into preferences. + this.endEquipEntity(); + return makeRunningValues(false, [], []); + } + this.prevDropDetected = dropDetected; + + equipHotspotBuddy.update(deltaTime, timestamp, controllerData); + + if (!this.shouldSendStart) { + var args = [this.hand === RIGHT_HAND ? "right" : "left", MyAvatar.sessionUUID]; + Entities.callEntityMethod(this.targetEntityID, "continueEquip", args); + } + + return makeRunningValues(true, [this.targetEntityID], []); + }; + + this.storeAttachPointInSettings = function() { + if (this.grabbedHotspot && this.targetEntityID) { + var prefProps = Entities.getEntityProperties(this.targetEntityID, ["localPosition", "localRotation"]); + if (prefProps && prefProps.localPosition && prefProps.localRotation) { + storeAttachPointForHotspotInSettings(this.grabbedHotspot, this.hand, + prefProps.localPosition, prefProps.localRotation); + } + } + }; + + this.cleanup = function () { + if (this.targetEntityID) { + this.endEquipEntity(); + } + }; + } + + var handleMessage = function(channel, message, sender) { + var data; + if (sender === MyAvatar.sessionUUID) { + if (channel === 'Hifi-Hand-Grab') { + try { + data = JSON.parse(message); + var equipModule = (data.hand === "left") ? leftEquipEntity : rightEquipEntity; + var entityProperties = Entities.getEntityProperties(data.entityID, DISPATCHER_PROPERTIES); + entityProperties.id = data.entityID; + equipModule.setMessageGrabData(entityProperties); + } catch (e) { + print("WARNING: equipEntity.js -- error parsing Hifi-Hand-Grab message: " + message); + } + } else if (channel === 'Hifi-Hand-Drop') { + if (message === "left") { + leftEquipEntity.endEquipEntity(); + } else if (message === "right") { + rightEquipEntity.endEquipEntity(); + } else if (message === "both") { + leftEquipEntity.endEquipEntity(); + rightEquipEntity.endEquipEntity(); + } + } + } + }; + + var clearGrabActions = function(entityID) { + var actionIDs = Entities.getActionIDs(entityID); + var myGrabTag = "grab-" + MyAvatar.sessionUUID; + for (var actionIndex = 0; actionIndex < actionIDs.length; actionIndex++) { + var actionID = actionIDs[actionIndex]; + var actionArguments = Entities.getActionArguments(entityID, actionID); + var tag = actionArguments.tag; + if (tag === myGrabTag) { + Entities.deleteAction(entityID, actionID); + } + } + }; + + var onMousePress = function(event) { + if (isInEditMode() || !event.isLeftButton) { // don't consider any left clicks on the entity while in edit + return; + } + var pickRay = Camera.computePickRay(event.x, event.y); + var intersection = Entities.findRayIntersection(pickRay, true); + if (intersection.intersects) { + var entityID = intersection.entityID; + var entityProperties = Entities.getEntityProperties(entityID, DISPATCHER_PROPERTIES); + entityProperties.id = entityID; + var hasEquipData = getWearableData(entityProperties); + if (hasEquipData && entityIsEquippable(entityProperties)) { + entityProperties.id = entityID; + var rightHandPosition = MyAvatar.getJointPosition("RightHand"); + var leftHandPosition = MyAvatar.getJointPosition("LeftHand"); + var distanceToRightHand = Vec3.distance(entityProperties.position, rightHandPosition); + var distanceToLeftHand = Vec3.distance(entityProperties.position, leftHandPosition); + var leftHandAvailable = leftEquipEntity.targetEntityID === null; + var rightHandAvailable = rightEquipEntity.targetEntityID === null; + if (rightHandAvailable && (distanceToRightHand < distanceToLeftHand || !leftHandAvailable)) { + // clear any existing grab actions on the entity now (their later removal could affect bootstrapping flags) + clearGrabActions(entityID); + rightEquipEntity.setMessageGrabData(entityProperties); + } else if (leftHandAvailable && (distanceToLeftHand < distanceToRightHand || !rightHandAvailable)) { + // clear any existing grab actions on the entity now (their later removal could affect bootstrapping flags) + clearGrabActions(entityID); + leftEquipEntity.setMessageGrabData(entityProperties); + } + } + } + }; + + var onKeyPress = function(event) { + if (event.text.toLowerCase() === UNEQUIP_KEY) { + if (rightEquipEntity.targetEntityID) { + rightEquipEntity.endEquipEntity(); + } + if (leftEquipEntity.targetEntityID) { + leftEquipEntity.endEquipEntity(); + } + } + }; + + var deleteEntity = function(entityID) { + if (rightEquipEntity.targetEntityID === entityID) { + rightEquipEntity.endEquipEntity(); + } + if (leftEquipEntity.targetEntityID === entityID) { + leftEquipEntity.endEquipEntity(); + } + }; + + var clearEntities = function() { + if (rightEquipEntity.targetEntityID) { + rightEquipEntity.endEquipEntity(); + } + if (leftEquipEntity.targetEntityID) { + leftEquipEntity.endEquipEntity(); + } + }; + + Messages.subscribe('Hifi-Hand-Grab'); + Messages.subscribe('Hifi-Hand-Drop'); + Messages.messageReceived.connect(handleMessage); + Controller.mousePressEvent.connect(onMousePress); + Controller.keyPressEvent.connect(onKeyPress); + Entities.deletingEntity.connect(deleteEntity); + Entities.clearingEntities.connect(clearEntities); + + var leftEquipEntity = new EquipEntity(LEFT_HAND); + var rightEquipEntity = new EquipEntity(RIGHT_HAND); + + enableDispatcherModule("LeftEquipEntity", leftEquipEntity); + enableDispatcherModule("RightEquipEntity", rightEquipEntity); + + function cleanup() { + leftEquipEntity.cleanup(); + rightEquipEntity.cleanup(); + disableDispatcherModule("LeftEquipEntity"); + disableDispatcherModule("RightEquipEntity"); + clearAttachPoints(); + Messages.messageReceived.disconnect(handleMessage); + Controller.mousePressEvent.disconnect(onMousePress); + Controller.keyPressEvent.disconnect(onKeyPress); + Entities.deletingEntity.disconnect(deleteEntity); + Entities.clearingEntities.disconnect(clearEntities); + } + Script.scriptEnding.connect(cleanup); +}()); diff --git a/scripts/simplifiedUI/system/controllers/controllerModules/farActionGrabEntity.js b/scripts/simplifiedUI/system/controllers/controllerModules/farActionGrabEntity.js new file mode 100644 index 0000000000..1eaed44ce2 --- /dev/null +++ b/scripts/simplifiedUI/system/controllers/controllerModules/farActionGrabEntity.js @@ -0,0 +1,591 @@ +"use strict"; + +// farActionGrabEntity.js +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html + +/* jslint bitwise: true */ + +/* global Script, Controller, RIGHT_HAND, LEFT_HAND, Mat4, MyAvatar, Vec3, Camera, Quat, + getEnabledModuleByName, makeRunningValues, Entities, + enableDispatcherModule, disableDispatcherModule, entityIsDistanceGrabbable, entityIsGrabbable, + makeDispatcherModuleParameters, MSECS_PER_SEC, HAPTIC_PULSE_STRENGTH, HAPTIC_PULSE_DURATION, + TRIGGER_OFF_VALUE, TRIGGER_ON_VALUE, ZERO_VEC, ensureDynamic, + getControllerWorldLocation, projectOntoEntityXYPlane, ContextOverlay, HMD, + Picks, makeLaserLockInfo, makeLaserParams, AddressManager, getEntityParents, Selection, DISPATCHER_HOVERING_LIST, + worldPositionToRegistrationFrameMatrix, DISPATCHER_PROPERTIES, Uuid, Picks +*/ + +Script.include("/~/system/libraries/controllerDispatcherUtils.js"); +Script.include("/~/system/libraries/controllers.js"); + +(function() { + + var MARGIN = 25; + + function TargetObject(entityID, entityProps) { + this.entityID = entityID; + this.entityProps = entityProps; + this.targetEntityID = null; + this.targetEntityProps = null; + this.previousCollisionStatus = null; + this.madeDynamic = null; + + this.makeDynamic = function() { + if (this.targetEntityID) { + var newProps = { + dynamic: true, + collisionless: true + }; + this.previousCollisionStatus = this.targetEntityProps.collisionless; + Entities.editEntity(this.targetEntityID, newProps); + this.madeDynamic = true; + } + }; + + this.restoreTargetEntityOriginalProps = function() { + if (this.madeDynamic) { + var props = {}; + props.dynamic = false; + props.collisionless = this.previousCollisionStatus; + var zeroVector = {x: 0, y: 0, z:0}; + props.localVelocity = zeroVector; + props.localRotation = zeroVector; + Entities.editEntity(this.targetEntityID, props); + } + }; + + this.getTargetEntity = function() { + var parentPropsLength = this.parentProps.length; + if (parentPropsLength !== 0) { + var targetEntity = { + id: this.parentProps[parentPropsLength - 1].id, + props: this.parentProps[parentPropsLength - 1]}; + this.targetEntityID = targetEntity.id; + this.targetEntityProps = targetEntity.props; + return targetEntity; + } + this.targetEntityID = this.entityID; + this.targetEntityProps = this.entityProps; + return { + id: this.entityID, + props: this.entityProps}; + }; + } + + function FarActionGrabEntity(hand) { + this.hand = hand; + this.grabbedThingID = null; + this.targetObject = null; + this.actionID = null; // action this script created... + this.entityToLockOnto = null; + this.potentialEntityWithContextOverlay = false; + this.entityWithContextOverlay = false; + this.contextOverlayTimer = false; + this.locked = false; + this.reticleMinX = MARGIN; + this.reticleMaxX = null; + this.reticleMinY = MARGIN; + this.reticleMaxY = null; + + this.ignoredEntities = []; + + var ACTION_TTL = 15; // seconds + + var DISTANCE_HOLDING_RADIUS_FACTOR = 3.5; // multiplied by distance between hand and object + var DISTANCE_HOLDING_ACTION_TIMEFRAME = 0.1; // how quickly objects move to their new position + var DISTANCE_HOLDING_UNITY_MASS = 1200; // The mass at which the distance holding action timeframe is unmodified + var DISTANCE_HOLDING_UNITY_DISTANCE = 6; // The distance at which the distance holding action timeframe is unmodified + + this.parameters = makeDispatcherModuleParameters( + 550, + this.hand === RIGHT_HAND ? ["rightHand"] : ["leftHand"], + [], + 100, + makeLaserParams(this.hand, false)); + + + this.handToController = function() { + return (this.hand === RIGHT_HAND) ? Controller.Standard.RightHand : Controller.Standard.LeftHand; + }; + + this.distanceGrabTimescale = function(mass, distance) { + var timeScale = DISTANCE_HOLDING_ACTION_TIMEFRAME * mass / + DISTANCE_HOLDING_UNITY_MASS * distance / + DISTANCE_HOLDING_UNITY_DISTANCE; + if (timeScale < DISTANCE_HOLDING_ACTION_TIMEFRAME) { + timeScale = DISTANCE_HOLDING_ACTION_TIMEFRAME; + } + return timeScale; + }; + + this.getMass = function(dimensions, density) { + return (dimensions.x * dimensions.y * dimensions.z) * density; + }; + + this.startFarGrabAction = function (controllerData, grabbedProperties) { + var controllerLocation = controllerData.controllerLocations[this.hand]; + var worldControllerPosition = controllerLocation.position; + var worldControllerRotation = controllerLocation.orientation; + + // transform the position into room space + var worldToSensorMat = Mat4.inverse(MyAvatar.getSensorToWorldMatrix()); + var roomControllerPosition = Mat4.transformPoint(worldToSensorMat, worldControllerPosition); + + var now = Date.now(); + + // add the action and initialize some variables + this.currentObjectPosition = grabbedProperties.position; + this.currentObjectRotation = grabbedProperties.rotation; + this.currentObjectTime = now; + this.currentCameraOrientation = Camera.orientation; + + this.grabRadius = this.grabbedDistance; + this.grabRadialVelocity = 0.0; + + // offset between controller vector at the grab radius and the entity position + var targetPosition = Vec3.multiply(this.grabRadius, Quat.getUp(worldControllerRotation)); + targetPosition = Vec3.sum(targetPosition, worldControllerPosition); + this.offsetPosition = Vec3.subtract(this.currentObjectPosition, targetPosition); + + // compute a constant based on the initial conditions which we use below to exaggerate hand motion + // onto the held object + this.radiusScalar = Math.log(this.grabRadius + 1.0); + if (this.radiusScalar < 1.0) { + this.radiusScalar = 1.0; + } + + // compute the mass for the purpose of energy and how quickly to move object + this.mass = this.getMass(grabbedProperties.dimensions, grabbedProperties.density); + var distanceToObject = Vec3.length(Vec3.subtract(MyAvatar.position, grabbedProperties.position)); + var timeScale = this.distanceGrabTimescale(this.mass, distanceToObject); + this.linearTimeScale = timeScale; + this.actionID = Entities.addAction("far-grab", this.grabbedThingID, { + targetPosition: this.currentObjectPosition, + linearTimeScale: timeScale, + targetRotation: this.currentObjectRotation, + angularTimeScale: timeScale, + tag: "far-grab-" + MyAvatar.sessionUUID, + ttl: ACTION_TTL + }); + if (this.actionID === Uuid.NULL) { + this.actionID = null; + } + + if (this.actionID !== null) { + var args = [this.hand === RIGHT_HAND ? "right" : "left", MyAvatar.sessionUUID]; + Entities.callEntityMethod(this.grabbedThingID, "startDistanceGrab", args); + } + + Controller.triggerHapticPulse(HAPTIC_PULSE_STRENGTH, HAPTIC_PULSE_DURATION, this.hand); + this.previousRoomControllerPosition = roomControllerPosition; + }; + + this.continueDistanceHolding = function(controllerData) { + var controllerLocation = controllerData.controllerLocations[this.hand]; + var worldControllerPosition = controllerLocation.position; + var worldControllerRotation = controllerLocation.orientation; + + // also transform the position into room space + var worldToSensorMat = Mat4.inverse(MyAvatar.getSensorToWorldMatrix()); + var roomControllerPosition = Mat4.transformPoint(worldToSensorMat, worldControllerPosition); + + var grabbedProperties = Entities.getEntityProperties(this.grabbedThingID, DISPATCHER_PROPERTIES); + var now = Date.now(); + var deltaObjectTime = (now - this.currentObjectTime) / MSECS_PER_SEC; // convert to seconds + this.currentObjectTime = now; + + // the action was set up when this.distanceHolding was called. update the targets. + var radius = Vec3.distance(this.currentObjectPosition, worldControllerPosition) * + this.radiusScalar * DISTANCE_HOLDING_RADIUS_FACTOR; + if (radius < 1.0) { + radius = 1.0; + } + + var roomHandDelta = Vec3.subtract(roomControllerPosition, this.previousRoomControllerPosition); + var worldHandDelta = Mat4.transformVector(MyAvatar.getSensorToWorldMatrix(), roomHandDelta); + var handMoved = Vec3.multiply(worldHandDelta, radius); + this.currentObjectPosition = Vec3.sum(this.currentObjectPosition, handMoved); + + var args = [this.hand === RIGHT_HAND ? "right" : "left", MyAvatar.sessionUUID]; + Entities.callEntityMethod(this.grabbedThingID, "continueDistanceGrab", args); + + // Update radialVelocity + var lastVelocity = Vec3.multiply(worldHandDelta, 1.0 / deltaObjectTime); + var delta = Vec3.normalize(Vec3.subtract(grabbedProperties.position, worldControllerPosition)); + var newRadialVelocity = Vec3.dot(lastVelocity, delta); + + var VELOCITY_AVERAGING_TIME = 0.016; + var blendFactor = deltaObjectTime / VELOCITY_AVERAGING_TIME; + if (blendFactor < 0.0) { + blendFactor = 0.0; + } else if (blendFactor > 1.0) { + blendFactor = 1.0; + } + this.grabRadialVelocity = blendFactor * newRadialVelocity + (1.0 - blendFactor) * this.grabRadialVelocity; + + var RADIAL_GRAB_AMPLIFIER = 10.0; + if (Math.abs(this.grabRadialVelocity) > 0.0) { + this.grabRadius = this.grabRadius + (this.grabRadialVelocity * deltaObjectTime * + this.grabRadius * RADIAL_GRAB_AMPLIFIER); + } + + // don't let grabRadius go all the way to zero, because it can't come back from that + var MINIMUM_GRAB_RADIUS = 0.1; + if (this.grabRadius < MINIMUM_GRAB_RADIUS) { + this.grabRadius = MINIMUM_GRAB_RADIUS; + } + var newTargetPosition = Vec3.multiply(this.grabRadius, Quat.getUp(worldControllerRotation)); + newTargetPosition = Vec3.sum(newTargetPosition, worldControllerPosition); + newTargetPosition = Vec3.sum(newTargetPosition, this.offsetPosition); + + // XXX + // this.maybeScale(grabbedProperties); + + var distanceToObject = Vec3.length(Vec3.subtract(MyAvatar.position, this.currentObjectPosition)); + + this.linearTimeScale = (this.linearTimeScale / 2); + if (this.linearTimeScale <= DISTANCE_HOLDING_ACTION_TIMEFRAME) { + this.linearTimeScale = DISTANCE_HOLDING_ACTION_TIMEFRAME; + } + var success = Entities.updateAction(this.grabbedThingID, this.actionID, { + targetPosition: newTargetPosition, + linearTimeScale: this.linearTimeScale, + targetRotation: this.currentObjectRotation, + angularTimeScale: this.distanceGrabTimescale(this.mass, distanceToObject), + ttl: ACTION_TTL + }); + if (!success) { + print("continueDistanceHolding -- updateAction failed: " + this.actionID); + this.actionID = null; + } + + this.previousRoomControllerPosition = roomControllerPosition; + }; + + this.endFarGrabAction = function () { + ensureDynamic(this.grabbedThingID); + this.distanceHolding = false; + this.distanceRotating = false; + Entities.deleteAction(this.grabbedThingID, this.actionID); + + var args = [this.hand === RIGHT_HAND ? "right" : "left", MyAvatar.sessionUUID]; + Entities.callEntityMethod(this.grabbedThingID, "releaseGrab", args); + if (this.targetObject) { + this.targetObject.restoreTargetEntityOriginalProps(); + } + this.actionID = null; + this.grabbedThingID = null; + this.targetObject = null; + this.potentialEntityWithContextOverlay = false; + }; + + this.updateRecommendedArea = function() { + var dims = Controller.getViewportDimensions(); + this.reticleMaxX = dims.x - MARGIN; + this.reticleMaxY = dims.y - MARGIN; + }; + + this.calculateNewReticlePosition = function(intersection) { + this.updateRecommendedArea(); + var point2d = HMD.overlayFromWorldPoint(intersection); + point2d.x = Math.max(this.reticleMinX, Math.min(point2d.x, this.reticleMaxX)); + point2d.y = Math.max(this.reticleMinY, Math.min(point2d.y, this.reticleMaxY)); + return point2d; + }; + + this.restoreIgnoredEntities = function() { + for (var i = 0; i < this.ignoredEntities.length; i++) { + var data = { + action: 'remove', + id: this.ignoredEntities[i] + }; + Messages.sendMessage('Hifi-Hand-RayPick-Blacklist', JSON.stringify(data)); + } + this.ignoredEntities = []; + }; + + this.notPointingAtEntity = function(controllerData) { + var intersection = controllerData.rayPicks[this.hand]; + var entityProperty = Entities.getEntityProperties(intersection.objectID, DISPATCHER_PROPERTIES); + var entityType = entityProperty.type; + var hudRayPick = controllerData.hudRayPicks[this.hand]; + var point2d = this.calculateNewReticlePosition(hudRayPick.intersection); + if ((intersection.type === Picks.INTERSECTED_ENTITY && entityType === "Web") || + intersection.type === Picks.INTERSECTED_OVERLAY || Window.isPointOnDesktopWindow(point2d)) { + return true; + } + return false; + }; + + this.distanceRotate = function(otherFarGrabModule) { + this.distanceRotating = true; + this.distanceHolding = false; + + var worldControllerRotation = getControllerWorldLocation(this.handToController(), true).orientation; + var controllerRotationDelta = + Quat.multiply(worldControllerRotation, Quat.inverse(this.previousWorldControllerRotation)); + // Rotate entity by twice the delta rotation. + controllerRotationDelta = Quat.multiply(controllerRotationDelta, controllerRotationDelta); + + // Perform the rotation in the translation controller's action update. + otherFarGrabModule.currentObjectRotation = Quat.multiply(controllerRotationDelta, + otherFarGrabModule.currentObjectRotation); + + this.previousWorldControllerRotation = worldControllerRotation; + }; + + this.prepareDistanceRotatingData = function(controllerData) { + var intersection = controllerData.rayPicks[this.hand]; + + var controllerLocation = getControllerWorldLocation(this.handToController(), true); + var worldControllerPosition = controllerLocation.position; + var worldControllerRotation = controllerLocation.orientation; + + var grabbedProperties = Entities.getEntityProperties(intersection.objectID, DISPATCHER_PROPERTIES); + this.currentObjectPosition = grabbedProperties.position; + this.grabRadius = intersection.distance; + + // Offset between controller vector at the grab radius and the entity position. + var targetPosition = Vec3.multiply(this.grabRadius, Quat.getUp(worldControllerRotation)); + targetPosition = Vec3.sum(targetPosition, worldControllerPosition); + this.offsetPosition = Vec3.subtract(this.currentObjectPosition, targetPosition); + + // Initial controller rotation. + this.previousWorldControllerRotation = worldControllerRotation; + }; + + this.destroyContextOverlay = function(controllerData) { + if (this.entityWithContextOverlay) { + ContextOverlay.destroyContextOverlay(this.entityWithContextOverlay); + this.entityWithContextOverlay = false; + this.potentialEntityWithContextOverlay = false; + } + }; + + this.targetIsNull = function() { + var properties = Entities.getEntityProperties(this.grabbedThingID, DISPATCHER_PROPERTIES); + if (Object.keys(properties).length === 0 && this.distanceHolding) { + return true; + } + return false; + }; + + this.isReady = function (controllerData) { + if (HMD.active) { + if (this.notPointingAtEntity(controllerData)) { + return makeRunningValues(false, [], []); + } + + this.distanceHolding = false; + this.distanceRotating = false; + + if (controllerData.triggerValues[this.hand] > TRIGGER_ON_VALUE) { + this.prepareDistanceRotatingData(controllerData); + return makeRunningValues(true, [], []); + } else { + this.destroyContextOverlay(); + return makeRunningValues(false, [], []); + } + } + return makeRunningValues(false, [], []); + }; + + this.run = function (controllerData) { + + var intersection = controllerData.rayPicks[this.hand]; + if (intersection.type === Picks.INTERSECTED_ENTITY && !Window.isPhysicsEnabled()) { + // add to ignored items. + if (this.ignoredEntities.indexOf(intersection.objectID) === -1) { + var data = { + action: 'add', + id: intersection.objectID + }; + Messages.sendMessage('Hifi-Hand-RayPick-Blacklist', JSON.stringify(data)); + this.ignoredEntities.push(intersection.objectID); + } + } + if (controllerData.triggerValues[this.hand] < TRIGGER_OFF_VALUE || + (this.notPointingAtEntity(controllerData) && Window.isPhysicsEnabled()) || this.targetIsNull()) { + this.endFarGrabAction(); + this.restoreIgnoredEntities(); + return makeRunningValues(false, [], []); + } + this.intersectionDistance = controllerData.rayPicks[this.hand].distance; + + var otherModuleName =this.hand === RIGHT_HAND ? "LeftFarActionGrabEntity" : "RightFarActionGrabEntity"; + var otherFarGrabModule = getEnabledModuleByName(otherModuleName); + + // gather up the readiness of the near-grab modules + var nearGrabNames = [ + this.hand === RIGHT_HAND ? "RightScaleAvatar" : "LeftScaleAvatar", + this.hand === RIGHT_HAND ? "RightFarTriggerEntity" : "LeftFarTriggerEntity", + this.hand === RIGHT_HAND ? "RightNearActionGrabEntity" : "LeftNearActionGrabEntity", + this.hand === RIGHT_HAND ? "RightNearParentingGrabEntity" : "LeftNearParentingGrabEntity", + this.hand === RIGHT_HAND ? "RightNearParentingGrabOverlay" : "LeftNearParentingGrabOverlay", + this.hand === RIGHT_HAND ? "RightNearTabletHighlight" : "LeftNearTabletHighlight" + ]; + + var nearGrabReadiness = []; + for (var i = 0; i < nearGrabNames.length; i++) { + var nearGrabModule = getEnabledModuleByName(nearGrabNames[i]); + var ready = nearGrabModule ? nearGrabModule.isReady(controllerData) : makeRunningValues(false, [], []); + nearGrabReadiness.push(ready); + } + + if (this.actionID) { + // if we are doing a distance grab and the object or tablet gets close enough to the controller, + // stop the far-grab so the near-grab or equip can take over. + for (var k = 0; k < nearGrabReadiness.length; k++) { + if (nearGrabReadiness[k].active && (nearGrabReadiness[k].targets[0] === this.grabbedThingID || + HMD.tabletID && nearGrabReadiness[k].targets[0] === HMD.tabletID)) { + this.endFarGrabAction(); + this.restoreIgnoredEntities(); + return makeRunningValues(false, [], []); + } + } + + this.continueDistanceHolding(controllerData); + } else { + // if we are doing a distance search and this controller moves into a position + // where it could near-grab something, stop searching. + for (var j = 0; j < nearGrabReadiness.length; j++) { + if (nearGrabReadiness[j].active) { + this.endFarGrabAction(); + this.restoreIgnoredEntities(); + return makeRunningValues(false, [], []); + } + } + + var rayPickInfo = controllerData.rayPicks[this.hand]; + if (rayPickInfo.type === Picks.INTERSECTED_ENTITY) { + if (controllerData.triggerClicks[this.hand]) { + var entityID = rayPickInfo.objectID; + var targetProps = Entities.getEntityProperties(entityID, DISPATCHER_PROPERTIES); + if (targetProps.href !== "") { + AddressManager.handleLookupString(targetProps.href); + this.restoreIgnoredEntities(); + return makeRunningValues(false, [], []); + } + + this.targetObject = new TargetObject(entityID, targetProps); + this.targetObject.parentProps = getEntityParents(targetProps); + + if (this.contextOverlayTimer) { + Script.clearTimeout(this.contextOverlayTimer); + } + this.contextOverlayTimer = false; + if (entityID === this.entityWithContextOverlay) { + this.destroyContextOverlay(); + } else { + Selection.removeFromSelectedItemsList("contextOverlayHighlightList", "entity", entityID); + } + + var targetEntity = this.targetObject.getTargetEntity(); + entityID = targetEntity.id; + targetProps = targetEntity.props; + + if (entityIsGrabbable(targetProps) || entityIsGrabbable(this.targetObject.entityProps)) { + if (!entityIsDistanceGrabbable(targetProps)) { + this.targetObject.makeDynamic(); + } + + if (!this.distanceRotating) { + this.grabbedThingID = entityID; + this.grabbedDistance = rayPickInfo.distance; + } + + if (otherFarGrabModule.grabbedThingID === this.grabbedThingID && + otherFarGrabModule.distanceHolding) { + this.prepareDistanceRotatingData(controllerData); + this.distanceRotate(otherFarGrabModule); + } else { + this.distanceHolding = true; + this.distanceRotating = false; + this.startFarGrabAction(controllerData, targetProps); + } + } + } else if (!this.entityWithContextOverlay) { + var _this = this; + + if (_this.potentialEntityWithContextOverlay !== rayPickInfo.objectID) { + if (_this.contextOverlayTimer) { + Script.clearTimeout(_this.contextOverlayTimer); + } + _this.contextOverlayTimer = false; + _this.potentialEntityWithContextOverlay = rayPickInfo.objectID; + } + + if (!_this.contextOverlayTimer) { + _this.contextOverlayTimer = Script.setTimeout(function () { + if (!_this.entityWithContextOverlay && + _this.contextOverlayTimer && + _this.potentialEntityWithContextOverlay === rayPickInfo.objectID) { + var props = Entities.getEntityProperties(rayPickInfo.objectID, DISPATCHER_PROPERTIES); + var pointerEvent = { + type: "Move", + id: _this.hand + 1, // 0 is reserved for hardware mouse + pos2D: projectOntoEntityXYPlane(rayPickInfo.objectID, + rayPickInfo.intersection, props), + pos3D: rayPickInfo.intersection, + normal: rayPickInfo.surfaceNormal, + direction: Vec3.subtract(ZERO_VEC, rayPickInfo.surfaceNormal), + button: "Secondary" + }; + if (ContextOverlay.createOrDestroyContextOverlay(rayPickInfo.objectID, pointerEvent)) { + _this.entityWithContextOverlay = rayPickInfo.objectID; + } + } + _this.contextOverlayTimer = false; + }, 500); + } + } + } else if (this.distanceRotating) { + this.distanceRotate(otherFarGrabModule); + } + } + return this.exitIfDisabled(controllerData); + }; + + this.exitIfDisabled = function(controllerData) { + var moduleName = this.hand === RIGHT_HAND ? "RightDisableModules" : "LeftDisableModules"; + var disableModule = getEnabledModuleByName(moduleName); + if (disableModule) { + if (disableModule.disableModules) { + this.endFarGrabAction(); + Selection.removeFromSelectedItemsList(DISPATCHER_HOVERING_LIST, "entity", + this.highlightedEntity); + this.highlightedEntity = null; + this.restoreIgnoredEntities(); + return makeRunningValues(false, [], []); + } + } + var grabbedThing = (this.distanceHolding || this.distanceRotating) ? this.targetObject.entityID : null; + var offset = this.calculateOffset(controllerData); + var laserLockInfo = makeLaserLockInfo(grabbedThing, false, this.hand, offset); + return makeRunningValues(true, [], [], laserLockInfo); + }; + + this.calculateOffset = function(controllerData) { + if (this.distanceHolding || this.distanceRotating) { + var targetProps = Entities.getEntityProperties(this.targetObject.entityID, + [ "position", "rotation", "registrationPoint", "dimensions" ]); + return worldPositionToRegistrationFrameMatrix(targetProps, controllerData.rayPicks[this.hand].intersection); + } + return undefined; + }; + } + + var leftFarActionGrabEntity = new FarActionGrabEntity(LEFT_HAND); + var rightFarActionGrabEntity = new FarActionGrabEntity(RIGHT_HAND); + + enableDispatcherModule("LeftFarActionGrabEntity", leftFarActionGrabEntity); + enableDispatcherModule("RightFarActionGrabEntity", rightFarActionGrabEntity); + + function cleanup() { + disableDispatcherModule("LeftFarActionGrabEntity"); + disableDispatcherModule("RightFarActionGrabEntity"); + } + Script.scriptEnding.connect(cleanup); +}()); diff --git a/scripts/simplifiedUI/system/controllers/controllerModules/farGrabEntity.js b/scripts/simplifiedUI/system/controllers/controllerModules/farGrabEntity.js new file mode 100644 index 0000000000..ecafa3cb26 --- /dev/null +++ b/scripts/simplifiedUI/system/controllers/controllerModules/farGrabEntity.js @@ -0,0 +1,585 @@ +"use strict"; + +// farGrabEntity.js +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html + +/* jslint bitwise: true */ + +/* global Script, Controller, RIGHT_HAND, LEFT_HAND, Mat4, MyAvatar, Vec3, Quat, getEnabledModuleByName, makeRunningValues, + Entities, enableDispatcherModule, disableDispatcherModule, entityIsGrabbable, makeDispatcherModuleParameters, MSECS_PER_SEC, + HAPTIC_PULSE_STRENGTH, HAPTIC_PULSE_DURATION, TRIGGER_OFF_VALUE, TRIGGER_ON_VALUE, ZERO_VEC, + projectOntoEntityXYPlane, ContextOverlay, HMD, Picks, makeLaserLockInfo, makeLaserParams, AddressManager, + getEntityParents, Selection, DISPATCHER_HOVERING_LIST, unhighlightTargetEntity, Messages, findGrabbableGroupParent, + worldPositionToRegistrationFrameMatrix, DISPATCHER_PROPERTIES +*/ + +Script.include("/~/system/libraries/controllerDispatcherUtils.js"); +Script.include("/~/system/libraries/controllers.js"); + +(function () { + var MARGIN = 25; + + function TargetObject(entityID, entityProps) { + this.entityID = entityID; + this.entityProps = entityProps; + this.targetEntityID = null; + this.targetEntityProps = null; + + this.getTargetEntity = function () { + var parentPropsLength = this.parentProps.length; + if (parentPropsLength !== 0) { + var targetEntity = { + id: this.parentProps[parentPropsLength - 1].id, + props: this.parentProps[parentPropsLength - 1] + }; + this.targetEntityID = targetEntity.id; + this.targetEntityProps = targetEntity.props; + return targetEntity; + } + this.targetEntityID = this.entityID; + this.targetEntityProps = this.entityProps; + return { + id: this.entityID, + props: this.entityProps + }; + }; + } + + function FarGrabEntity(hand) { + this.hand = hand; + this.grabbing = false; + this.targetEntityID = null; + this.targetObject = null; + this.previouslyUnhooked = {}; + this.potentialEntityWithContextOverlay = false; + this.entityWithContextOverlay = false; + this.contextOverlayTimer = false; + this.reticleMinX = MARGIN; + this.reticleMaxX = 0; + this.reticleMinY = MARGIN; + this.reticleMaxY = 0; + this.endedGrab = 0; + this.MIN_HAPTIC_PULSE_INTERVAL = 500; // ms + this.disabled = false; + var _this = this; + this.initialControllerRotation = Quat.IDENTITY; + this.currentControllerRotation = Quat.IDENTITY; + this.manipulating = false; + this.wasManipulating = false; + + var FAR_GRAB_JOINTS = [65527, 65528]; // FARGRAB_LEFTHAND_INDEX, FARGRAB_RIGHTHAND_INDEX + + var DISTANCE_HOLDING_RADIUS_FACTOR = 3.5; // multiplied by distance between hand and object + var DISTANCE_HOLDING_ACTION_TIMEFRAME = 0.1; // how quickly objects move to their new position + var DISTANCE_HOLDING_UNITY_MASS = 1200; // The mass at which the distance holding action timeframe is unmodified + var DISTANCE_HOLDING_UNITY_DISTANCE = 6; // The distance at which the distance holding action timeframe is unmodified + + this.parameters = makeDispatcherModuleParameters( + 540, + this.hand === RIGHT_HAND ? ["rightHand"] : ["leftHand"], + [], + 100, + makeLaserParams(this.hand, false)); + + this.getOtherModule = function () { + return getEnabledModuleByName(this.hand === RIGHT_HAND ? ("LeftFarGrabEntity") : ("RightFarGrabEntity")); + }; + + // Get the rotation of the fargrabbed entity. + this.getTargetRotation = function () { + if (this.targetIsNull()) { + return null; + } else { + var props = Entities.getEntityProperties(this.targetEntityID, ["rotation"]); + return props.rotation; + } + }; + + this.getOffhand = function () { + return (this.hand === RIGHT_HAND ? LEFT_HAND : RIGHT_HAND); + } + + // Activation criteria for rotating a fargrabbed entity. If we're changing the mapping, this is where to do it. + this.shouldManipulateTarget = function (controllerData) { + return (controllerData.triggerValues[this.getOffhand()] > TRIGGER_ON_VALUE || controllerData.secondaryValues[this.getOffhand()] > TRIGGER_ON_VALUE) ? true : false; + }; + + // Get the delta between the current rotation and where the controller was when manipulation started. + this.calculateEntityRotationManipulation = function (controllerRotation) { + return Quat.multiply(controllerRotation, Quat.inverse(this.initialControllerRotation)); + }; + + this.setJointTranslation = function (newTargetPosLocal) { + MyAvatar.setJointTranslation(FAR_GRAB_JOINTS[this.hand], newTargetPosLocal); + }; + + this.setJointRotation = function (newTargetRotLocal) { + MyAvatar.setJointRotation(FAR_GRAB_JOINTS[this.hand], newTargetRotLocal); + }; + + this.setJointRotation = function (newTargetRotLocal) { + MyAvatar.setJointRotation(FAR_GRAB_JOINTS[this.hand], newTargetRotLocal); + }; + + this.handToController = function () { + return (this.hand === RIGHT_HAND) ? Controller.Standard.RightHand : Controller.Standard.LeftHand; + }; + + this.distanceGrabTimescale = function (mass, distance) { + var timeScale = DISTANCE_HOLDING_ACTION_TIMEFRAME * mass / + DISTANCE_HOLDING_UNITY_MASS * distance / + DISTANCE_HOLDING_UNITY_DISTANCE; + if (timeScale < DISTANCE_HOLDING_ACTION_TIMEFRAME) { + timeScale = DISTANCE_HOLDING_ACTION_TIMEFRAME; + } + return timeScale; + }; + + this.getMass = function (dimensions, density) { + return (dimensions.x * dimensions.y * dimensions.z) * density; + }; + + this.startFarGrabEntity = function (controllerData, targetProps) { + var controllerLocation = controllerData.controllerLocations[this.hand]; + var worldControllerPosition = controllerLocation.position; + var worldControllerRotation = controllerLocation.orientation; + // transform the position into room space + var worldToSensorMat = Mat4.inverse(MyAvatar.getSensorToWorldMatrix()); + var roomControllerPosition = Mat4.transformPoint(worldToSensorMat, worldControllerPosition); + + var now = Date.now(); + + // add the action and initialize some variables + this.currentObjectPosition = targetProps.position; + this.currentObjectRotation = targetProps.rotation; + this.currentObjectTime = now; + + this.grabRadius = this.grabbedDistance; + this.grabRadialVelocity = 0.0; + + // offset between controller vector at the grab radius and the entity position + var targetPosition = Vec3.multiply(this.grabRadius, Quat.getUp(worldControllerRotation)); + targetPosition = Vec3.sum(targetPosition, worldControllerPosition); + this.offsetPosition = Vec3.subtract(this.currentObjectPosition, targetPosition); + + // compute a constant based on the initial conditions which we use below to exaggerate hand motion + // onto the held object + this.radiusScalar = Math.log(this.grabRadius + 1.0); + if (this.radiusScalar < 1.0) { + this.radiusScalar = 1.0; + } + + // compute the mass for the purpose of energy and how quickly to move object + this.mass = this.getMass(targetProps.dimensions, targetProps.density); + + // Debounce haptic pules. Can occur as near grab controller module vacillates between being ready or not due to + // changing positions and floating point rounding. + if (Date.now() - this.endedGrab > this.MIN_HAPTIC_PULSE_INTERVAL) { + Controller.triggerHapticPulse(HAPTIC_PULSE_STRENGTH, HAPTIC_PULSE_DURATION, this.hand); + } + + unhighlightTargetEntity(this.targetEntityID); + var message = { + hand: this.hand, + entityID: this.targetEntityID + }; + + Messages.sendLocalMessage('Hifi-unhighlight-entity', JSON.stringify(message)); + + var newTargetPosLocal = MyAvatar.worldToJointPoint(targetProps.position); + var newTargetRotLocal = targetProps.rotation; + this.setJointTranslation(newTargetPosLocal); + this.setJointRotation(newTargetRotLocal); + + var args = [this.hand === RIGHT_HAND ? "right" : "left", MyAvatar.sessionUUID]; + Entities.callEntityMethod(targetProps.id, "startDistanceGrab", args); + + this.targetEntityID = targetProps.id; + + + if (this.grabID) { + MyAvatar.releaseGrab(this.grabID); + } + var farJointIndex = FAR_GRAB_JOINTS[this.hand]; + this.grabID = MyAvatar.grab(targetProps.id, farJointIndex, + Entities.worldToLocalPosition(targetProps.position, MyAvatar.SELF_ID, farJointIndex), + Entities.worldToLocalRotation(targetProps.rotation, MyAvatar.SELF_ID, farJointIndex)); + + Messages.sendMessage('Hifi-Object-Manipulation', JSON.stringify({ + action: 'grab', + grabbedEntity: targetProps.id, + joint: this.hand === RIGHT_HAND ? "RightHand" : "LeftHand" + })); + this.grabbing = true; + + this.previousRoomControllerPosition = roomControllerPosition; + }; + + this.continueDistanceHolding = function (controllerData) { + var controllerLocation = controllerData.controllerLocations[this.hand]; + var worldControllerPosition = controllerLocation.position; + var worldControllerRotation = controllerLocation.orientation; + + // also transform the position into room space + var worldToSensorMat = Mat4.inverse(MyAvatar.getSensorToWorldMatrix()); + var roomControllerPosition = Mat4.transformPoint(worldToSensorMat, worldControllerPosition); + + var targetProps = Entities.getEntityProperties(this.targetEntityID, DISPATCHER_PROPERTIES); + var now = Date.now(); + var deltaObjectTime = (now - this.currentObjectTime) / MSECS_PER_SEC; // convert to seconds + this.currentObjectTime = now; + + // the action was set up when this.distanceHolding was called. update the targets. + var radius = Vec3.distance(this.currentObjectPosition, worldControllerPosition) * + this.radiusScalar * DISTANCE_HOLDING_RADIUS_FACTOR; + if (radius < 1.0) { + radius = 1.0; + } + + var roomHandDelta = Vec3.subtract(roomControllerPosition, this.previousRoomControllerPosition); + var worldHandDelta = Mat4.transformVector(MyAvatar.getSensorToWorldMatrix(), roomHandDelta); + var handMoved = Vec3.multiply(worldHandDelta, radius); + this.currentObjectPosition = Vec3.sum(this.currentObjectPosition, handMoved); + + var args = [this.hand === RIGHT_HAND ? "right" : "left", MyAvatar.sessionUUID]; + Entities.callEntityMethod(this.targetEntityID, "continueDistanceGrab", args); + + // Update radialVelocity + var lastVelocity = Vec3.multiply(worldHandDelta, 1.0 / deltaObjectTime); + var delta = Vec3.normalize(Vec3.subtract(targetProps.position, worldControllerPosition)); + var newRadialVelocity = Vec3.dot(lastVelocity, delta); + + var VELOCITY_AVERAGING_TIME = 0.016; + var blendFactor = deltaObjectTime / VELOCITY_AVERAGING_TIME; + if (blendFactor < 0.0) { + blendFactor = 0.0; + } else if (blendFactor > 1.0) { + blendFactor = 1.0; + } + this.grabRadialVelocity = blendFactor * newRadialVelocity + (1.0 - blendFactor) * this.grabRadialVelocity; + + var RADIAL_GRAB_AMPLIFIER = 10.0; + if (Math.abs(this.grabRadialVelocity) > 0.0) { + this.grabRadius = this.grabRadius + (this.grabRadialVelocity * deltaObjectTime * + this.grabRadius * RADIAL_GRAB_AMPLIFIER); + } + + // don't let grabRadius go all the way to zero, because it can't come back from that + var MINIMUM_GRAB_RADIUS = 0.1; + if (this.grabRadius < MINIMUM_GRAB_RADIUS) { + this.grabRadius = MINIMUM_GRAB_RADIUS; + } + var newTargetPosition = Vec3.multiply(this.grabRadius, Quat.getUp(worldControllerRotation)); + newTargetPosition = Vec3.sum(newTargetPosition, worldControllerPosition); + newTargetPosition = Vec3.sum(newTargetPosition, this.offsetPosition); + + var newTargetPosLocal = MyAvatar.worldToJointPoint(newTargetPosition); + + // This block handles the user's ability to rotate the object they're FarGrabbing + if (this.shouldManipulateTarget(controllerData)) { + // Get the pose of the controller that is not grabbing. + var pose = Controller.getPoseValue((this.getOffhand() ? Controller.Standard.RightHand : Controller.Standard.LeftHand)); + if (pose.valid) { + // If we weren't manipulating the object yet, initialize the entity's original position. + if (!this.manipulating) { + // This will only be triggered if we've let go of the off-hand trigger and pulled it again without ending a grab. + // Need to poll the entity's rotation again here. + if (!this.wasManipulating) { + this.initialEntityRotation = this.getTargetRotation(); + } + // Save the original controller orientation, we only care about the delta between this rotation and wherever + // the controller rotates, so that we can apply it to the entity's rotation. + this.initialControllerRotation = Quat.multiply(pose.rotation, MyAvatar.orientation); + this.manipulating = true; + } + } + + var rot = Quat.multiply(pose.rotation, MyAvatar.orientation); + var rotBetween = this.calculateEntityRotationManipulation(rot); + var doubleRot = Quat.multiply(rotBetween, rotBetween); + this.lastJointRotation = Quat.multiply(doubleRot, this.initialEntityRotation); + this.setJointRotation(this.lastJointRotation); + } else { + // If we were manipulating but the user isn't currently expressing this intent, we want to know so we preserve the rotation + // between manipulations without ending the fargrab. + if (this.manipulating) { + this.initialEntityRotation = this.lastJointRotation; + this.wasManipulating = true; + } + this.manipulating = false; + // Reset the inital controller position. + this.initialControllerRotation = Quat.IDENTITY; + } + this.setJointTranslation(newTargetPosLocal); + + this.previousRoomControllerPosition = roomControllerPosition; + }; + + this.endFarGrabEntity = function (controllerData) { + if (this.grabID) { + MyAvatar.releaseGrab(this.grabID); + this.grabID = null; + } + + this.endedGrab = Date.now(); + + var args = [this.hand === RIGHT_HAND ? "right" : "left", MyAvatar.sessionUUID]; + Entities.callEntityMethod(this.targetEntityID, "releaseGrab", args); + Messages.sendMessage('Hifi-Object-Manipulation', JSON.stringify({ + action: 'release', + grabbedEntity: this.targetEntityID, + joint: this.hand === RIGHT_HAND ? "RightHand" : "LeftHand" + })); + unhighlightTargetEntity(this.targetEntityID); + this.grabbing = false; + this.potentialEntityWithContextOverlay = false; + MyAvatar.clearJointData(FAR_GRAB_JOINTS[this.hand]); + this.initialEntityRotation = Quat.IDENTITY; + this.initialControllerRotation = Quat.IDENTITY; + this.targetEntityID = null; + this.manipulating = false; + this.wasManipulating = false; + var otherModule = this.getOtherModule(); + otherModule.disabled = false; + }; + + this.updateRecommendedArea = function () { + var dims = Controller.getViewportDimensions(); + this.reticleMaxX = dims.x - MARGIN; + this.reticleMaxY = dims.y - MARGIN; + }; + + this.calculateNewReticlePosition = function (intersection) { + this.updateRecommendedArea(); + var point2d = HMD.overlayFromWorldPoint(intersection); + point2d.x = Math.max(this.reticleMinX, Math.min(point2d.x, this.reticleMaxX)); + point2d.y = Math.max(this.reticleMinY, Math.min(point2d.y, this.reticleMaxY)); + return point2d; + }; + + this.notPointingAtEntity = function (controllerData) { + var intersection = controllerData.rayPicks[this.hand]; + var entityProperty = Entities.getEntityProperties(intersection.objectID, DISPATCHER_PROPERTIES); + var entityType = entityProperty.type; + var hudRayPick = controllerData.hudRayPicks[this.hand]; + var point2d = this.calculateNewReticlePosition(hudRayPick.intersection); + if ((intersection.type === Picks.INTERSECTED_ENTITY && entityType === "Web") || + intersection.type === Picks.INTERSECTED_OVERLAY || Window.isPointOnDesktopWindow(point2d)) { + return true; + } + return false; + }; + + this.destroyContextOverlay = function (controllerData) { + if (this.entityWithContextOverlay) { + ContextOverlay.destroyContextOverlay(this.entityWithContextOverlay); + this.entityWithContextOverlay = false; + this.potentialEntityWithContextOverlay = false; + } + }; + + this.targetIsNull = function () { + var properties = Entities.getEntityProperties(this.targetEntityID, DISPATCHER_PROPERTIES); + if (Object.keys(properties).length === 0 && this.distanceHolding) { + return true; + } + return false; + }; + + this.getTargetProps = function (controllerData) { + var targetEntity = controllerData.rayPicks[this.hand].objectID; + if (targetEntity) { + var gtProps = Entities.getEntityProperties(targetEntity, DISPATCHER_PROPERTIES); + if (entityIsGrabbable(gtProps)) { + // if we've attempted to grab a child, roll up to the root of the tree + var groupRootProps = findGrabbableGroupParent(controllerData, gtProps); + if (entityIsGrabbable(groupRootProps)) { + return groupRootProps; + } + return gtProps; + } + } + return null; + }; + + this.isReady = function (controllerData) { + if (HMD.active) { + if (this.notPointingAtEntity(controllerData)) { + return makeRunningValues(false, [], []); + } + + this.distanceHolding = false; + + if (controllerData.triggerValues[this.hand] > TRIGGER_ON_VALUE && !this.disabled) { + var otherModule = this.getOtherModule(); + otherModule.disabled = true; + return makeRunningValues(true, [], []); + } else { + this.destroyContextOverlay(); + } + } + return makeRunningValues(false, [], []); + }; + + this.run = function (controllerData) { + if (controllerData.triggerValues[this.hand] < TRIGGER_OFF_VALUE || this.targetIsNull()) { + this.endFarGrabEntity(controllerData); + return makeRunningValues(false, [], []); + } + this.intersectionDistance = controllerData.rayPicks[this.hand].distance; + + // gather up the readiness of the near-grab modules + var nearGrabNames = [ + this.hand === RIGHT_HAND ? "RightScaleAvatar" : "LeftScaleAvatar", + this.hand === RIGHT_HAND ? "RightFarTriggerEntity" : "LeftFarTriggerEntity", + this.hand === RIGHT_HAND ? "RightNearGrabEntity" : "LeftNearGrabEntity" + ]; + if (!this.grabbing) { + nearGrabNames.push(this.hand === RIGHT_HAND ? "RightNearParentingGrabOverlay" : "LeftNearParentingGrabOverlay"); + nearGrabNames.push(this.hand === RIGHT_HAND ? "RightNearTabletHighlight" : "LeftNearTabletHighlight"); + } + + var nearGrabReadiness = []; + for (var i = 0; i < nearGrabNames.length; i++) { + var nearGrabModule = getEnabledModuleByName(nearGrabNames[i]); + var ready = nearGrabModule ? nearGrabModule.isReady(controllerData) : makeRunningValues(false, [], []); + nearGrabReadiness.push(ready); + } + + if (this.targetEntityID) { + // if we are doing a distance grab and the object gets close enough to the controller, + // stop the far-grab so the near-grab or equip can take over. + for (var k = 0; k < nearGrabReadiness.length; k++) { + if (nearGrabReadiness[k].active && (nearGrabReadiness[k].targets[0] === this.targetEntityID)) { + this.endFarGrabEntity(controllerData); + return makeRunningValues(false, [], []); + } + } + + this.continueDistanceHolding(controllerData); + } else { + // if we are doing a distance search and this controller moves into a position + // where it could near-grab something, stop searching. + for (var j = 0; j < nearGrabReadiness.length; j++) { + if (nearGrabReadiness[j].active) { + this.endFarGrabEntity(controllerData); + return makeRunningValues(false, [], []); + } + } + + var rayPickInfo = controllerData.rayPicks[this.hand]; + if (rayPickInfo.type === Picks.INTERSECTED_ENTITY) { + if (controllerData.triggerClicks[this.hand]) { + var entityID = rayPickInfo.objectID; + var targetProps = Entities.getEntityProperties(entityID, DISPATCHER_PROPERTIES); + if (targetProps.href !== "") { + AddressManager.handleLookupString(targetProps.href); + return makeRunningValues(false, [], []); + } + + this.targetObject = new TargetObject(entityID, targetProps); + this.targetObject.parentProps = getEntityParents(targetProps); + + if (this.contextOverlayTimer) { + Script.clearTimeout(this.contextOverlayTimer); + } + this.contextOverlayTimer = false; + if (entityID === this.entityWithContextOverlay) { + this.destroyContextOverlay(); + } else { + Selection.removeFromSelectedItemsList("contextOverlayHighlightList", "entity", entityID); + } + + var targetEntity = this.targetObject.getTargetEntity(); + entityID = targetEntity.id; + targetProps = targetEntity.props; + + if (entityIsGrabbable(targetProps) || entityIsGrabbable(this.targetObject.entityProps)) { + + this.targetEntityID = entityID; + this.grabbedDistance = rayPickInfo.distance; + this.distanceHolding = true; + this.startFarGrabEntity(controllerData, targetProps); + } + } else if (!this.entityWithContextOverlay) { + var _this = this; + + if (_this.potentialEntityWithContextOverlay !== rayPickInfo.objectID) { + if (_this.contextOverlayTimer) { + Script.clearTimeout(_this.contextOverlayTimer); + } + _this.contextOverlayTimer = false; + _this.potentialEntityWithContextOverlay = rayPickInfo.objectID; + } + + if (!_this.contextOverlayTimer) { + _this.contextOverlayTimer = Script.setTimeout(function () { + if (!_this.entityWithContextOverlay && + _this.contextOverlayTimer && + _this.potentialEntityWithContextOverlay === rayPickInfo.objectID) { + var cotProps = Entities.getEntityProperties(rayPickInfo.objectID, + DISPATCHER_PROPERTIES); + var pointerEvent = { + type: "Move", + id: _this.hand + 1, // 0 is reserved for hardware mouse + pos2D: projectOntoEntityXYPlane(rayPickInfo.objectID, + rayPickInfo.intersection, cotProps), + pos3D: rayPickInfo.intersection, + normal: rayPickInfo.surfaceNormal, + direction: Vec3.subtract(ZERO_VEC, rayPickInfo.surfaceNormal), + button: "Secondary" + }; + if (ContextOverlay.createOrDestroyContextOverlay(rayPickInfo.objectID, pointerEvent)) { + _this.entityWithContextOverlay = rayPickInfo.objectID; + } + } + _this.contextOverlayTimer = false; + }, 500); + } + } + } + } + return this.exitIfDisabled(controllerData); + }; + + this.exitIfDisabled = function (controllerData) { + var moduleName = this.hand === RIGHT_HAND ? "RightDisableModules" : "LeftDisableModules"; + var disableModule = getEnabledModuleByName(moduleName); + if (disableModule) { + if (disableModule.disableModules) { + this.endFarGrabEntity(controllerData); + Selection.removeFromSelectedItemsList(DISPATCHER_HOVERING_LIST, "entity", this.highlightedEntity); + this.highlightedEntity = null; + return makeRunningValues(false, [], []); + } + } + var grabbedThing = this.distanceHolding ? this.targetObject.entityID : null; + var offset = this.calculateOffset(controllerData); + var laserLockInfo = makeLaserLockInfo(grabbedThing, false, this.hand, offset); + return makeRunningValues(true, [], [], laserLockInfo); + }; + + this.calculateOffset = function (controllerData) { + if (this.distanceHolding) { + var targetProps = Entities.getEntityProperties(this.targetObject.entityID, + ["position", "rotation", "registrationPoint", "dimensions"]); + return worldPositionToRegistrationFrameMatrix(targetProps, controllerData.rayPicks[this.hand].intersection); + } + return undefined; + }; + } + + var leftFarGrabEntity = new FarGrabEntity(LEFT_HAND); + var rightFarGrabEntity = new FarGrabEntity(RIGHT_HAND); + + enableDispatcherModule("LeftFarGrabEntity", leftFarGrabEntity); + enableDispatcherModule("RightFarGrabEntity", rightFarGrabEntity); + + function cleanup() { + disableDispatcherModule("LeftFarGrabEntity"); + disableDispatcherModule("RightFarGrabEntity"); + } + Script.scriptEnding.connect(cleanup); +}()); diff --git a/scripts/simplifiedUI/system/controllers/controllerModules/farTrigger.js b/scripts/simplifiedUI/system/controllers/controllerModules/farTrigger.js new file mode 100644 index 0000000000..c9c9d3deee --- /dev/null +++ b/scripts/simplifiedUI/system/controllers/controllerModules/farTrigger.js @@ -0,0 +1,102 @@ +"use strict"; + +// farTrigger.js +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html + + +/* global Script, RIGHT_HAND, LEFT_HAND, MyAvatar, + makeRunningValues, Entities, enableDispatcherModule, disableDispatcherModule, makeDispatcherModuleParameters, + getGrabbableData, makeLaserParams, DISPATCHER_PROPERTIES +*/ + +Script.include("/~/system/libraries/controllerDispatcherUtils.js"); +Script.include("/~/system/libraries/controllers.js"); + +(function() { + function entityWantsFarTrigger(props) { + var grabbableData = getGrabbableData(props); + return grabbableData.triggerable; + } + + function FarTriggerEntity(hand) { + this.hand = hand; + this.targetEntityID = null; + this.grabbing = false; + this.previousParentID = {}; + this.previousParentJointIndex = {}; + this.previouslyUnhooked = {}; + + this.parameters = makeDispatcherModuleParameters( + 520, + this.hand === RIGHT_HAND ? ["rightHand"] : ["leftHand"], + [], + 100, + makeLaserParams(this.hand, false)); + + this.getTargetProps = function (controllerData) { + var targetEntity = controllerData.rayPicks[this.hand].objectID; + if (targetEntity && controllerData.rayPicks[this.hand].type === RayPick.INTERSECTED_ENTITY) { + var targetProperties = Entities.getEntityProperties(targetEntity, DISPATCHER_PROPERTIES); + if (entityWantsFarTrigger(targetProperties)) { + return targetProperties; + } + } + return null; + }; + + this.startFarTrigger = function (controllerData) { + var args = [this.hand === RIGHT_HAND ? "right" : "left", MyAvatar.sessionUUID]; + Entities.callEntityMethod(this.targetEntityID, "startFarTrigger", args); + }; + + this.continueFarTrigger = function (controllerData) { + var args = [this.hand === RIGHT_HAND ? "right" : "left", MyAvatar.sessionUUID]; + Entities.callEntityMethod(this.targetEntityID, "continueFarTrigger", args); + }; + + this.endFarTrigger = function (controllerData) { + var args = [this.hand === RIGHT_HAND ? "right" : "left", MyAvatar.sessionUUID]; + Entities.callEntityMethod(this.targetEntityID, "stopFarTrigger", args); + }; + + this.isReady = function (controllerData) { + this.targetEntityID = null; + if (controllerData.triggerClicks[this.hand] === 0) { + return makeRunningValues(false, [], []); + } + + var targetProps = this.getTargetProps(controllerData); + if (targetProps) { + this.targetEntityID = targetProps.id; + this.startFarTrigger(controllerData); + return makeRunningValues(true, [this.targetEntityID], []); + } else { + return makeRunningValues(false, [], []); + } + }; + + this.run = function (controllerData) { + var targetEntity = controllerData.rayPicks[this.hand].objectID; + if (controllerData.triggerClicks[this.hand] === 0 || this.targetEntityID !== targetEntity) { + this.endFarTrigger(controllerData); + return makeRunningValues(false, [], []); + } + this.continueFarTrigger(controllerData); + return makeRunningValues(true, [this.targetEntityID], []); + }; + } + + var leftFarTriggerEntity = new FarTriggerEntity(LEFT_HAND); + var rightFarTriggerEntity = new FarTriggerEntity(RIGHT_HAND); + + enableDispatcherModule("LeftFarTriggerEntity", leftFarTriggerEntity); + enableDispatcherModule("RightFarTriggerEntity", rightFarTriggerEntity); + + function cleanup() { + disableDispatcherModule("LeftFarTriggerEntity"); + disableDispatcherModule("RightFarTriggerEntity"); + } + Script.scriptEnding.connect(cleanup); +}()); diff --git a/scripts/simplifiedUI/system/controllers/controllerModules/hudOverlayPointer.js b/scripts/simplifiedUI/system/controllers/controllerModules/hudOverlayPointer.js new file mode 100644 index 0000000000..f7d5b5a2dd --- /dev/null +++ b/scripts/simplifiedUI/system/controllers/controllerModules/hudOverlayPointer.js @@ -0,0 +1,126 @@ +// +// hudOverlayPointer.js +// +// scripts/system/controllers/controllerModules/ +// +// Created by Dante Ruiz 2017-9-21 +// 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 +// + +/* global Script, Controller, RIGHT_HAND, LEFT_HAND, HMD, makeLaserParams */ +(function() { + Script.include("/~/system/libraries/controllers.js"); + var ControllerDispatcherUtils = Script.require("/~/system/libraries/controllerDispatcherUtils.js"); + var MARGIN = 25; + var HUD_LASER_OFFSET = 2; + function HudOverlayPointer(hand) { + this.hand = hand; + this.running = false; + this.reticleMinX = MARGIN; + this.reticleMaxX; + this.reticleMinY = MARGIN; + this.reticleMaxY; + this.parameters = ControllerDispatcherUtils.makeDispatcherModuleParameters( + 160, // Same as webSurfaceLaserInput. + this.hand === RIGHT_HAND ? ["rightHand"] : ["leftHand"], + [], + 100, + makeLaserParams((this.hand + HUD_LASER_OFFSET), false)); + + this.getFarGrab = function () { + return getEnabledModuleByName(this.hand === RIGHT_HAND ? ("RightFarGrabEntity") : ("LeftFarGrabEntity")); + } + + this.farGrabActive = function () { + var farGrab = this.getFarGrab(); + // farGrab will be null if module isn't loaded. + if (farGrab) { + return farGrab.targetIsNull(); + } else { + return false; + } + }; + + this.getOtherHandController = function() { + return (this.hand === RIGHT_HAND) ? Controller.Standard.LeftHand : Controller.Standard.RightHand; + }; + + this.handToController = function() { + return (this.hand === RIGHT_HAND) ? Controller.Standard.RightHand : Controller.Standard.LeftHand; + }; + + this.updateRecommendedArea = function() { + var dims = Controller.getViewportDimensions(); + this.reticleMaxX = dims.x - MARGIN; + this.reticleMaxY = dims.y - MARGIN; + }; + + this.calculateNewReticlePosition = function(intersection) { + this.updateRecommendedArea(); + var point2d = HMD.overlayFromWorldPoint(intersection); + point2d.x = Math.max(this.reticleMinX, Math.min(point2d.x, this.reticleMaxX)); + point2d.y = Math.max(this.reticleMinY, Math.min(point2d.y, this.reticleMaxY)); + return point2d; + }; + + this.pointingAtTablet = function(controllerData) { + var rayPick = controllerData.rayPicks[this.hand]; + return (HMD.tabletScreenID && HMD.homeButtonID && (rayPick.objectID === HMD.tabletScreenID || rayPick.objectID === HMD.homeButtonID)); + }; + + this.getOtherModule = function() { + return this.hand === RIGHT_HAND ? leftHudOverlayPointer : rightHudOverlayPointer; + }; + + this.processLaser = function(controllerData) { + var controllerLocation = controllerData.controllerLocations[this.hand]; + if ((controllerData.triggerValues[this.hand] < ControllerDispatcherUtils.TRIGGER_ON_VALUE || !controllerLocation.valid) || + this.pointingAtTablet(controllerData)) { + return false; + } + var hudRayPick = controllerData.hudRayPicks[this.hand]; + var point2d = this.calculateNewReticlePosition(hudRayPick.intersection); + if (!Window.isPointOnDesktopWindow(point2d) && !this.triggerClicked) { + return false; + } + + this.triggerClicked = controllerData.triggerClicks[this.hand]; + return true; + }; + + this.isReady = function (controllerData) { + var otherModuleRunning = this.getOtherModule().running; + if (!otherModuleRunning && HMD.active && !this.farGrabActive()) { + if (this.processLaser(controllerData)) { + this.running = true; + return ControllerDispatcherUtils.makeRunningValues(true, [], []); + } else { + this.running = false; + return ControllerDispatcherUtils.makeRunningValues(false, [], []); + } + } + return ControllerDispatcherUtils.makeRunningValues(false, [], []); + }; + + this.run = function (controllerData, deltaTime) { + return this.isReady(controllerData); + }; + } + + + var leftHudOverlayPointer = new HudOverlayPointer(LEFT_HAND); + var rightHudOverlayPointer = new HudOverlayPointer(RIGHT_HAND); + + ControllerDispatcherUtils.enableDispatcherModule("LeftHudOverlayPointer", leftHudOverlayPointer); + ControllerDispatcherUtils.enableDispatcherModule("RightHudOverlayPointer", rightHudOverlayPointer); + + function cleanup() { + ControllerDispatcherUtils.disableDispatcherModule("LeftHudOverlayPointer"); + ControllerDispatcherUtils.disableDispatcherModule("RightHudOverlayPointer"); + } + Script.scriptEnding.connect(cleanup); + +})(); diff --git a/scripts/simplifiedUI/system/controllers/controllerModules/inEditMode.js b/scripts/simplifiedUI/system/controllers/controllerModules/inEditMode.js new file mode 100644 index 0000000000..5709b19efe --- /dev/null +++ b/scripts/simplifiedUI/system/controllers/controllerModules/inEditMode.js @@ -0,0 +1,253 @@ +"use strict"; + +// inEditMode.js +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html + +/* jslint bitwise: true */ + +/* global Script, Controller, RIGHT_HAND, LEFT_HAND, enableDispatcherModule, disableDispatcherModule, makeRunningValues, + Messages, makeDispatcherModuleParameters, HMD, getEnabledModuleByName, TRIGGER_ON_VALUE, isInEditMode, Picks, + makeLaserParams +*/ + +Script.include("/~/system/libraries/controllerDispatcherUtils.js"); +Script.include("/~/system/libraries/controllers.js"); +Script.include("/~/system/libraries/utils.js"); + +(function () { + var MARGIN = 25; + function InEditMode(hand) { + this.hand = hand; + this.isEditing = false; + this.triggerClicked = false; + this.selectedTarget = null; + this.reticleMinX = MARGIN; + this.reticleMaxX = null; + this.reticleMinY = MARGIN; + this.reticleMaxY = null; + + this.parameters = makeDispatcherModuleParameters( + 165, // Lower priority than webSurfaceLaserInput and hudOverlayPointer. + this.hand === RIGHT_HAND ? ["rightHand", "rightHandEquip", "rightHandTrigger"] : ["leftHand", "leftHandEquip", "leftHandTrigger"], + [], + 100, + makeLaserParams(this.hand, false)); + + this.nearTablet = function(overlays) { + for (var i = 0; i < overlays.length; i++) { + if (HMD.tabletID && overlays[i] === HMD.tabletID) { + return true; + } + } + return false; + }; + + this.handToController = function() { + return (this.hand === RIGHT_HAND) ? Controller.Standard.RightHand : Controller.Standard.LeftHand; + }; + + this.pointingAtTablet = function(objectID) { + return (HMD.tabletScreenID && objectID === HMD.tabletScreenID) || + (HMD.homeButtonID && objectID === HMD.homeButtonID); + }; + + this.calculateNewReticlePosition = function(intersection) { + var dims = Controller.getViewportDimensions(); + this.reticleMaxX = dims.x - MARGIN; + this.reticleMaxY = dims.y - MARGIN; + var point2d = HMD.overlayFromWorldPoint(intersection); + point2d.x = Math.max(this.reticleMinX, Math.min(point2d.x, this.reticleMaxX)); + point2d.y = Math.max(this.reticleMinY, Math.min(point2d.y, this.reticleMaxY)); + return point2d; + }; + + this.ENTITY_TOOL_UPDATES_CHANNEL = "entityToolUpdates"; + + this.sendPickData = function(controllerData) { + if (controllerData.triggerClicks[this.hand]) { + var hand = this.hand === RIGHT_HAND ? Controller.Standard.RightHand : Controller.Standard.LeftHand; + if (!this.triggerClicked) { + this.selectedTarget = controllerData.rayPicks[this.hand]; + if (!this.selectedTarget.intersects) { + Messages.sendLocalMessage(this.ENTITY_TOOL_UPDATES_CHANNEL, JSON.stringify({ + method: "clearSelection", + hand: hand + })); + } else { + if (this.selectedTarget.type === Picks.INTERSECTED_ENTITY) { + Messages.sendLocalMessage(this.ENTITY_TOOL_UPDATES_CHANNEL, JSON.stringify({ + method: "selectEntity", + entityID: this.selectedTarget.objectID, + hand: hand + })); + } else if (this.selectedTarget.type === Picks.INTERSECTED_OVERLAY) { + Messages.sendLocalMessage(this.ENTITY_TOOL_UPDATES_CHANNEL, JSON.stringify({ + method: "selectOverlay", + overlayID: this.selectedTarget.objectID, + hand: hand + })); + } + } + } + + this.triggerClicked = true; + } + + this.sendPointingAtData(controllerData); + }; + + this.sendPointingAtData = function(controllerData) { + var rayPick = controllerData.rayPicks[this.hand]; + var hudRayPick = controllerData.hudRayPicks[this.hand]; + var point2d = this.calculateNewReticlePosition(hudRayPick.intersection); + var desktopWindow = Window.isPointOnDesktopWindow(point2d); + var tablet = this.pointingAtTablet(rayPick.objectID); + var rightHand = this.hand === RIGHT_HAND; + Messages.sendLocalMessage(this.ENTITY_TOOL_UPDATES_CHANNEL, JSON.stringify({ + method: "pointingAt", + desktopWindow: desktopWindow, + tablet: tablet, + rightHand: rightHand + })); + }; + + this.runModule = function() { + return makeRunningValues(true, [], []); + }; + + this.exitModule = function() { + return makeRunningValues(false, [], []); + }; + + this.isReady = function(controllerData) { + if (isInEditMode()) { + if (controllerData.triggerValues[this.hand] < TRIGGER_ON_VALUE) { + this.triggerClicked = false; + } + Messages.sendLocalMessage('Hifi-unhighlight-all', ''); + return this.runModule(); + } + this.triggerClicked = false; + return this.exitModule(); + }; + + this.run = function(controllerData) { + + // Tablet stylus. + var tabletStylusInput = getEnabledModuleByName(this.hand === RIGHT_HAND + ? "RightTabletStylusInput" : "LeftTabletStylusInput"); + if (tabletStylusInput) { + var tabletReady = tabletStylusInput.isReady(controllerData); + if (tabletReady.active) { + return this.exitModule(); + } + } + + // Tablet surface. + var webLaser = getEnabledModuleByName(this.hand === RIGHT_HAND + ? "RightWebSurfaceLaserInput" : "LeftWebSurfaceLaserInput"); + if (webLaser) { + var webLaserReady = webLaser.isReady(controllerData); + var target = controllerData.rayPicks[this.hand].objectID; + this.sendPointingAtData(controllerData); + if (webLaserReady.active && this.pointingAtTablet(target)) { + return this.exitModule(); + } + } + + // HUD overlay. + if (!controllerData.triggerClicks[this.hand]) { // Don't grab if trigger pressed when laser starts intersecting. + var hudLaser = getEnabledModuleByName(this.hand === RIGHT_HAND + ? "RightHudOverlayPointer" : "LeftHudOverlayPointer"); + if (hudLaser) { + var hudLaserReady = hudLaser.isReady(controllerData); + if (hudLaserReady.active) { + return this.exitModule(); + } + } + } + + // Tablet highlight and grabbing. + var tabletHighlight = getEnabledModuleByName(this.hand === RIGHT_HAND + ? "RightNearTabletHighlight" : "LeftNearTabletHighlight"); + if (tabletHighlight) { + var tabletHighlightReady = tabletHighlight.isReady(controllerData); + if (tabletHighlightReady.active) { + return this.exitModule(); + } + } + + // Teleport. + var teleport = getEnabledModuleByName(this.hand === RIGHT_HAND ? "RightTeleporter" : "LeftTeleporter"); + if (teleport) { + var teleportReady = teleport.isReady(controllerData); + if (teleportReady.active) { + return this.exitModule(); + } + } + + if ((controllerData.triggerClicks[this.hand] === 0 && controllerData.secondaryValues[this.hand] === 0)) { + var stopRunning = false; + controllerData.nearbyOverlayIDs[this.hand].forEach(function(overlayID) { + var overlayName = Overlays.getProperty(overlayID, "name"); + if (overlayName === "KeyboardAnchor") { + stopRunning = true; + } + }); + + if (stopRunning) { + return this.exitModule(); + } + } + + this.sendPickData(controllerData); + return this.isReady(controllerData); + }; + } + + var leftHandInEditMode = new InEditMode(LEFT_HAND); + var rightHandInEditMode = new InEditMode(RIGHT_HAND); + + enableDispatcherModule("LeftHandInEditMode", leftHandInEditMode); + enableDispatcherModule("RightHandInEditMode", rightHandInEditMode); + + var INEDIT_STATUS_CHANNEL = "Hifi-InEdit-Status"; + var HAND_RAYPICK_BLACKLIST_CHANNEL = "Hifi-Hand-RayPick-Blacklist"; + this.handleMessage = function (channel, data, sender) { + if (channel === INEDIT_STATUS_CHANNEL && sender === MyAvatar.sessionUUID) { + var message; + + try { + message = JSON.parse(data); + } catch (e) { + return; + } + + switch (message.method) { + case "editing": + if (message.hand === LEFT_HAND) { + leftHandInEditMode.isEditing = message.editing; + } else { + rightHandInEditMode.isEditing = message.editing; + } + Messages.sendLocalMessage(HAND_RAYPICK_BLACKLIST_CHANNEL, JSON.stringify({ + action: "tablet", + hand: message.hand, + blacklist: message.editing + })); + break; + } + } + }; + Messages.subscribe(INEDIT_STATUS_CHANNEL); + Messages.messageReceived.connect(this.handleMessage); + + function cleanup() { + disableDispatcherModule("LeftHandInEditMode"); + disableDispatcherModule("RightHandInEditMode"); + } + + Script.scriptEnding.connect(cleanup); +}()); diff --git a/scripts/simplifiedUI/system/controllers/controllerModules/inVREditMode.js b/scripts/simplifiedUI/system/controllers/controllerModules/inVREditMode.js new file mode 100644 index 0000000000..104e37d76c --- /dev/null +++ b/scripts/simplifiedUI/system/controllers/controllerModules/inVREditMode.js @@ -0,0 +1,185 @@ +"use strict"; + +// inVREditMode.js +// +// Created by David Rowe on 16 Sep 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 + +/* global Script, HMD, Messages, MyAvatar, RIGHT_HAND, LEFT_HAND, enableDispatcherModule, disableDispatcherModule, + makeDispatcherModuleParameters, makeRunningValues, getEnabledModuleByName, makeLaserParams +*/ + +Script.include("/~/system/libraries/controllerDispatcherUtils.js"); + +(function () { + + function InVREditMode(hand) { + this.hand = hand; + this.isAppActive = false; + this.isEditing = false; + this.running = false; + var NO_HAND_LASER = -1; // Invalid hand parameter so that standard laser is not displayed. + this.parameters = makeDispatcherModuleParameters( + 166, // Slightly lower priority than inEditMode. + this.hand === RIGHT_HAND + ? ["rightHand", "rightHandEquip", "rightHandTrigger"] + : ["leftHand", "leftHandEquip", "leftHandTrigger"], + [], + 100, + makeLaserParams(NO_HAND_LASER, false) + ); + + this.pointingAtTablet = function (objectID) { + return (HMD.tabletScreenID && objectID === HMD.tabletScreenID) || + (HMD.homeButtonID && objectID === HMD.homeButtonID); + }; + + // The Shapes app has a non-standard laser: in particular, the laser end dot displays on its own when the laser is + // pointing at the Shapes UI. The laser on/off is controlled by this module but the laser is implemented in the Shapes + // app. + // If, in the future, the Shapes app laser interaction is adopted as a standard UI style then the laser could be + // implemented in the controller modules along side the other laser styles. + var INVREDIT_MODULE_RUNNING = "Hifi-InVREdit-Module-Running"; + + this.runModule = function () { + if (!this.running) { + Messages.sendLocalMessage(INVREDIT_MODULE_RUNNING, JSON.stringify({ + hand: this.hand, + running: true + })); + this.running = true; + } + return makeRunningValues(true, [], []); + }; + + this.exitModule = function () { + if (this.running) { + Messages.sendLocalMessage(INVREDIT_MODULE_RUNNING, JSON.stringify({ + hand: this.hand, + running: false + })); + this.running = false; + } + return makeRunningValues(false, [], []); + }; + + this.isReady = function (controllerData) { + if (this.isAppActive) { + return makeRunningValues(true, [], []); + } + return makeRunningValues(false, [], []); + }; + + this.run = function (controllerData) { + // Default behavior if disabling is not enabled. + if (!this.isAppActive) { + return this.exitModule(); + } + + // Tablet stylus. + var tabletStylusInput = getEnabledModuleByName(this.hand === RIGHT_HAND + ? "RightTabletStylusInput" : "LeftTabletStylusInput"); + if (tabletStylusInput) { + var tabletReady = tabletStylusInput.isReady(controllerData); + if (tabletReady.active) { + return this.exitModule(); + } + } + + // Tablet surface. + var overlayLaser = getEnabledModuleByName(this.hand === RIGHT_HAND + ? "RightWebSurfaceLaserInput" : "LeftWebSurfaceLaserInput"); + if (overlayLaser) { + var overlayLaserReady = overlayLaser.isReady(controllerData); + var target = controllerData.rayPicks[this.hand].objectID; + if (overlayLaserReady.active && this.pointingAtTablet(target)) { + return this.exitModule(); + } + } + + // Tablet highlight and grabbing. + var tabletHighlight = getEnabledModuleByName(this.hand === RIGHT_HAND + ? "RightNearTabletHighlight" : "LeftNearTabletHighlight"); + if (tabletHighlight) { + var tabletHighlightReady = tabletHighlight.isReady(controllerData); + if (tabletHighlightReady.active) { + return this.exitModule(); + } + } + + // HUD overlay. + if (!controllerData.triggerClicks[this.hand]) { + var hudLaser = getEnabledModuleByName(this.hand === RIGHT_HAND + ? "RightHudOverlayPointer" : "LeftHudOverlayPointer"); + if (hudLaser) { + var hudLaserReady = hudLaser.isReady(controllerData); + if (hudLaserReady.active) { + return this.exitModule(); + } + } + } + + // Teleport. + var teleporter = getEnabledModuleByName(this.hand === RIGHT_HAND + ? "RightTeleporter" : "LeftTeleporter"); + if (teleporter) { + var teleporterReady = teleporter.isReady(controllerData); + if (teleporterReady.active) { + return this.exitModule(); + } + } + + // Other behaviors are disabled. + return this.runModule(); + }; + } + + var leftHandInVREditMode = new InVREditMode(LEFT_HAND); + var rightHandInVREditMode = new InVREditMode(RIGHT_HAND); + enableDispatcherModule("LeftHandInVREditMode", leftHandInVREditMode); + enableDispatcherModule("RightHandInVREditMode", rightHandInVREditMode); + + var INVREDIT_STATUS_CHANNEL = "Hifi-InVREdit-Status"; + var HAND_RAYPICK_BLACKLIST_CHANNEL = "Hifi-Hand-RayPick-Blacklist"; + this.handleMessage = function (channel, data, sender) { + if (channel === INVREDIT_STATUS_CHANNEL && sender === MyAvatar.sessionUUID) { + var message; + + try { + message = JSON.parse(data); + } catch (e) { + return; + } + + switch (message.method) { + case "active": + leftHandInVREditMode.isAppActive = message.active; + rightHandInVREditMode.isAppActive = message.active; + break; + case "editing": + if (message.hand === LEFT_HAND) { + leftHandInVREditMode.isEditing = message.editing; + } else { + rightHandInVREditMode.isEditing = message.editing; + } + Messages.sendLocalMessage(HAND_RAYPICK_BLACKLIST_CHANNEL, JSON.stringify({ + action: "tablet", + hand: message.hand, + blacklist: message.editing + })); + break; + } + } + }; + Messages.subscribe(INVREDIT_STATUS_CHANNEL); + Messages.messageReceived.connect(this.handleMessage); + + this.cleanup = function () { + disableDispatcherModule("LeftHandInVREditMode"); + disableDispatcherModule("RightHandInVREditMode"); + }; + Script.scriptEnding.connect(this.cleanup); +}()); diff --git a/scripts/simplifiedUI/system/controllers/controllerModules/mouseHMD.js b/scripts/simplifiedUI/system/controllers/controllerModules/mouseHMD.js new file mode 100644 index 0000000000..172923a8e2 --- /dev/null +++ b/scripts/simplifiedUI/system/controllers/controllerModules/mouseHMD.js @@ -0,0 +1,151 @@ +// +// mouseHMD.js +// +// scripts/system/controllers/controllerModules/ +// +// Created by Dante Ruiz 2017-9-22 +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +/* global Script, HMD, Reticle, Vec3, Controller */ + +(function() { + var ControllerDispatcherUtils = Script.require("/~/system/libraries/controllerDispatcherUtils.js"); + + function TimeLock(experation) { + this.experation = experation; + this.last = 0; + this.update = function(time) { + this.last = time || Date.now(); + }; + + this.expired = function(time) { + return ((time || Date.now()) - this.last) > this.experation; + }; + } + + function MouseHMD() { + var _this = this; + this.hmdWasActive = HMD.active; + this.mouseMoved = false; + this.mouseActivity = new TimeLock(5000); + this.handControllerActivity = new TimeLock(4000); + this.parameters = ControllerDispatcherUtils.makeDispatcherModuleParameters( + 10, + ["mouse"], + [], + 100); + + this.onMouseMove = function() { + _this.updateMouseActivity(); + }; + + this.onMouseClick = function() { + _this.updateMouseActivity(); + }; + + this.updateMouseActivity = function(isClick) { + if (_this.ignoreMouseActivity()) { + return; + } + + if (HMD.active) { + var now = Date.now(); + _this.mouseActivity.update(now); + } + }; + + this.adjustReticleDepth = function(controllerData) { + if (Reticle.isPointingAtSystemOverlay(Reticle.position)) { + var reticlePositionOnHUD = HMD.worldPointFromOverlay(Reticle.position); + Reticle.depth = Vec3.distance(reticlePositionOnHUD, HMD.position); + } else { + var APPARENT_MAXIMUM_DEPTH = 100.0; + var result = controllerData.mouseRayPick; + Reticle.depth = result.intersects ? result.distance : APPARENT_MAXIMUM_DEPTH; + } + }; + + this.ignoreMouseActivity = function() { + if (!Reticle.allowMouseCapture) { + return true; + } + + var pos = Reticle.position; + if (!pos || (pos.x === -1 && pos.y === -1)) { + return true; + } + + if (!_this.handControllerActivity.expired()) { + return true; + } + + return false; + }; + + this.triggersPressed = function(controllerData, now) { + var onValue = ControllerDispatcherUtils.TRIGGER_ON_VALUE; + var rightHand = ControllerDispatcherUtils.RIGHT_HAND; + var leftHand = ControllerDispatcherUtils.LEFT_HAND; + var leftTriggerValue = controllerData.triggerValues[leftHand]; + var rightTriggerValue = controllerData.triggerValues[rightHand]; + + if (leftTriggerValue > onValue || rightTriggerValue > onValue) { + this.handControllerActivity.update(now); + return true; + } + + return false; + }; + + this.isReady = function(controllerData, deltaTime) { + var now = Date.now(); + var hmdChanged = this.hmdWasActive !== HMD.active; + this.hmdWasActive = HMD.active; + this.triggersPressed(controllerData, now); + if (HMD.active) { + if (!this.mouseActivity.expired(now) && _this.handControllerActivity.expired()) { + Reticle.visible = true; + return ControllerDispatcherUtils.makeRunningValues(true, [], []); + } else { + Reticle.visible = false; + } + } else if (hmdChanged && !Reticle.visible) { + Reticle.visible = true; + } + + return ControllerDispatcherUtils.makeRunningValues(false, [], []); + }; + + this.run = function(controllerData, deltaTime) { + var now = Date.now(); + var hmdActive = HMD.active; + if (this.mouseActivity.expired(now) || this.triggersPressed(controllerData, now) || !hmdActive) { + if (!hmdActive) { + Reticle.visible = true; + } else { + Reticle.visible = false; + } + + return ControllerDispatcherUtils.makeRunningValues(false, [], []); + } + this.adjustReticleDepth(controllerData); + return ControllerDispatcherUtils.makeRunningValues(true, [], []); + }; + } + + var mouseHMD = new MouseHMD(); + ControllerDispatcherUtils.enableDispatcherModule("MouseHMD", mouseHMD); + + Controller.mouseMoveEvent.connect(mouseHMD.onMouseMove); + Controller.mousePressEvent.connect(mouseHMD.onMouseClick); + + function cleanup() { + ControllerDispatcherUtils.disableDispatcherModule("MouseHMD"); + } + + Script.scriptEnding.connect(cleanup); +})(); diff --git a/scripts/simplifiedUI/system/controllers/controllerModules/nearGrabEntity.js b/scripts/simplifiedUI/system/controllers/controllerModules/nearGrabEntity.js new file mode 100644 index 0000000000..763c1a1ce0 --- /dev/null +++ b/scripts/simplifiedUI/system/controllers/controllerModules/nearGrabEntity.js @@ -0,0 +1,226 @@ +"use strict"; + +// nearGrabEntity.js +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html + + +/* global Script, Entities, MyAvatar, Controller, RIGHT_HAND, LEFT_HAND, getControllerJointIndex, enableDispatcherModule, + disableDispatcherModule, Messages, HAPTIC_PULSE_STRENGTH, HAPTIC_PULSE_DURATION, TRIGGER_OFF_VALUE, + makeDispatcherModuleParameters, entityIsGrabbable, makeRunningValues, NEAR_GRAB_RADIUS, findGrabbableGroupParent, Vec3, + cloneEntity, entityIsCloneable, HAPTIC_PULSE_STRENGTH, HAPTIC_PULSE_DURATION, BUMPER_ON_VALUE, + distanceBetweenPointAndEntityBoundingBox, getGrabbableData, getEnabledModuleByName, DISPATCHER_PROPERTIES, HMD, + NEAR_GRAB_DISTANCE +*/ + +Script.include("/~/system/libraries/controllerDispatcherUtils.js"); +Script.include("/~/system/libraries/cloneEntityUtils.js"); +Script.include("/~/system/libraries/controllers.js"); + +(function() { + + function NearGrabEntity(hand) { + this.hand = hand; + this.targetEntityID = null; + this.grabbing = false; + this.cloneAllowed = true; + this.grabID = null; + + this.parameters = makeDispatcherModuleParameters( + 500, + this.hand === RIGHT_HAND ? ["rightHand"] : ["leftHand"], + [], + 100); + + this.startGrab = function (targetProps) { + if (this.grabID) { + MyAvatar.releaseGrab(this.grabID); + } + + var grabData = getGrabbableData(targetProps); + + var handJointIndex; + if (HMD.mounted && HMD.isHandControllerAvailable() && grabData.grabFollowsController) { + handJointIndex = getControllerJointIndex(this.hand); + } else { + handJointIndex = MyAvatar.getJointIndex(this.hand === RIGHT_HAND ? "RightHand" : "LeftHand"); + } + + this.targetEntityID = targetProps.id; + + var relativePosition = Entities.worldToLocalPosition(targetProps.position, MyAvatar.SELF_ID, handJointIndex); + var relativeRotation = Entities.worldToLocalRotation(targetProps.rotation, MyAvatar.SELF_ID, handJointIndex); + this.grabID = MyAvatar.grab(targetProps.id, handJointIndex, relativePosition, relativeRotation); + }; + + this.startNearGrabEntity = function (targetProps) { + Controller.triggerHapticPulse(HAPTIC_PULSE_STRENGTH, HAPTIC_PULSE_DURATION, this.hand); + + this.startGrab(targetProps); + + var args = [this.hand === RIGHT_HAND ? "right" : "left", MyAvatar.sessionUUID]; + Entities.callEntityMethod(targetProps.id, "startNearGrab", args); + + Messages.sendMessage('Hifi-Object-Manipulation', JSON.stringify({ + action: 'grab', + grabbedEntity: targetProps.id, + joint: this.hand === RIGHT_HAND ? "RightHand" : "LeftHand" + })); + + this.grabbing = true; + }; + + this.endGrab = function () { + if (this.grabID) { + MyAvatar.releaseGrab(this.grabID); + this.grabID = null; + } + }; + + this.endNearGrabEntity = function () { + this.endGrab(); + + var args = [this.hand === RIGHT_HAND ? "right" : "left", MyAvatar.sessionUUID]; + Entities.callEntityMethod(this.targetEntityID, "releaseGrab", args); + Messages.sendMessage('Hifi-Object-Manipulation', JSON.stringify({ + action: 'release', + grabbedEntity: this.targetEntityID, + joint: this.hand === RIGHT_HAND ? "RightHand" : "LeftHand" + })); + + this.grabbing = false; + this.targetEntityID = null; + }; + + this.getTargetProps = function (controllerData) { + // nearbyEntityProperties is already sorted by length from controller + var nearbyEntityProperties = controllerData.nearbyEntityProperties[this.hand]; + var sensorScaleFactor = MyAvatar.sensorToWorldScale; + var nearGrabDistance = NEAR_GRAB_DISTANCE * sensorScaleFactor; + var nearGrabRadius = NEAR_GRAB_RADIUS * sensorScaleFactor; + for (var i = 0; i < nearbyEntityProperties.length; i++) { + var props = nearbyEntityProperties[i]; + var grabPosition = controllerData.controllerLocations[this.hand].position; // Is offset from hand position. + var dist = distanceBetweenPointAndEntityBoundingBox(grabPosition, props); + var distance = Vec3.distance(grabPosition, props.position); + if ((dist > nearGrabDistance) || + (distance > nearGrabRadius)) { // Only smallish entities can be near grabbed. + continue; + } + if (entityIsGrabbable(props) || entityIsCloneable(props)) { + if (!entityIsCloneable(props)) { + // if we've attempted to grab a non-cloneable child, roll up to the root of the tree + var groupRootProps = findGrabbableGroupParent(controllerData, props); + if (entityIsGrabbable(groupRootProps)) { + return groupRootProps; + } + } + return props; + } + } + return null; + }; + + this.isReady = function (controllerData, deltaTime) { + this.targetEntityID = null; + this.grabbing = false; + + if (controllerData.triggerValues[this.hand] < TRIGGER_OFF_VALUE && + controllerData.secondaryValues[this.hand] < TRIGGER_OFF_VALUE) { + this.cloneAllowed = true; + return makeRunningValues(false, [], []); + } + + var scaleModuleName = this.hand === RIGHT_HAND ? "RightScaleEntity" : "LeftScaleEntity"; + var scaleModule = getEnabledModuleByName(scaleModuleName); + if (scaleModule && (scaleModule.grabbedThingID || scaleModule.isReady(controllerData).active)) { + // we're rescaling -- don't start a grab. + return makeRunningValues(false, [], []); + } + + var targetProps = this.getTargetProps(controllerData); + if (targetProps) { + this.targetEntityID = targetProps.id; + return makeRunningValues(true, [this.targetEntityID], []); + } else { + return makeRunningValues(false, [], []); + } + }; + + this.run = function (controllerData, deltaTime) { + + if (this.grabbing) { + if (controllerData.triggerClicks[this.hand] < TRIGGER_OFF_VALUE && + controllerData.secondaryValues[this.hand] < TRIGGER_OFF_VALUE) { + this.endNearGrabEntity(); + return makeRunningValues(false, [], []); + } + + var props = controllerData.nearbyEntityPropertiesByID[this.targetEntityID]; + if (!props) { + props = Entities.getEntityProperties(this.targetEntityID, DISPATCHER_PROPERTIES); + if (!props) { + // entity was deleted + this.grabbing = false; + this.targetEntityID = null; + return makeRunningValues(false, [], []); + } + } + + var args = [this.hand === RIGHT_HAND ? "right" : "left", MyAvatar.sessionUUID]; + Entities.callEntityMethod(this.targetEntityID, "continueNearGrab", args); + } else { + // still searching + var readiness = this.isReady(controllerData); + if (!readiness.active) { + return readiness; + } + if (controllerData.triggerClicks[this.hand] || controllerData.secondaryValues[this.hand] > BUMPER_ON_VALUE) { + // switch to grab + var targetProps = this.getTargetProps(controllerData); + var targetCloneable = entityIsCloneable(targetProps); + + if (targetCloneable) { + if (this.cloneAllowed) { + var cloneID = cloneEntity(targetProps); + if (cloneID !== null) { + var cloneProps = Entities.getEntityProperties(cloneID, DISPATCHER_PROPERTIES); + cloneProps.id = cloneID; + this.grabbing = true; + this.targetEntityID = cloneID; + this.startNearGrabEntity(cloneProps); + this.cloneAllowed = false; // prevent another clone call until inputs released + } + } + } else if (targetProps) { + this.grabbing = true; + this.startNearGrabEntity(targetProps); + } + } + } + + return makeRunningValues(true, [this.targetEntityID], []); + }; + + this.cleanup = function () { + if (this.targetEntityID) { + this.endNearGrabEntity(); + } + }; + } + + var leftNearGrabEntity = new NearGrabEntity(LEFT_HAND); + var rightNearGrabEntity = new NearGrabEntity(RIGHT_HAND); + + enableDispatcherModule("LeftNearGrabEntity", leftNearGrabEntity); + enableDispatcherModule("RightNearGrabEntity", rightNearGrabEntity); + + function cleanup() { + leftNearGrabEntity.cleanup(); + rightNearGrabEntity.cleanup(); + disableDispatcherModule("LeftNearGrabEntity"); + disableDispatcherModule("RightNearGrabEntity"); + } + Script.scriptEnding.connect(cleanup); +}()); diff --git a/scripts/simplifiedUI/system/controllers/controllerModules/nearGrabHyperLinkEntity.js b/scripts/simplifiedUI/system/controllers/controllerModules/nearGrabHyperLinkEntity.js new file mode 100644 index 0000000000..962ae89bb9 --- /dev/null +++ b/scripts/simplifiedUI/system/controllers/controllerModules/nearGrabHyperLinkEntity.js @@ -0,0 +1,91 @@ +"use strict"; + +// nearGrabHyperLinkEntity.js +// +// Created by Dante Ruiz on 03/02/2018 +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html + +/* global Script, MyAvatar, RIGHT_HAND, LEFT_HAND, enableDispatcherModule, disableDispatcherModule, + makeDispatcherModuleParameters, makeRunningValues, TRIGGER_OFF_VALUE, NEAR_GRAB_RADIUS, BUMPER_ON_VALUE, AddressManager +*/ + +(function() { + Script.include("/~/system/libraries/controllerDispatcherUtils.js"); + Script.include("/~/system/libraries/controllers.js"); + + function NearGrabHyperLinkEntity(hand) { + this.hand = hand; + this.targetEntityID = null; + this.hyperlink = ""; + + this.parameters = makeDispatcherModuleParameters( + 485, + this.hand === RIGHT_HAND ? ["rightHand"] : ["leftHand"], + [], + 100); + + + this.getTargetProps = function(controllerData) { + var nearbyEntitiesProperties = controllerData.nearbyEntityProperties[this.hand]; + var sensorScaleFactor = MyAvatar.sensorToWorldScale; + for (var i = 0; i < nearbyEntitiesProperties.length; i++) { + var props = nearbyEntitiesProperties[i]; + if (props.distance > NEAR_GRAB_RADIUS * sensorScaleFactor) { + continue; + } + if (props.href !== "" && props.href !== undefined) { + return props; + } + } + return null; + }; + + this.isReady = function(controllerData) { + this.targetEntityID = null; + if (controllerData.triggerValues[this.hand] < TRIGGER_OFF_VALUE && + controllerData.secondaryValues[this.hand] < TRIGGER_OFF_VALUE) { + return makeRunningValues(false, [], []); + } + + var targetProps = this.getTargetProps(controllerData); + if (targetProps) { + this.hyperlink = targetProps.href; + this.targetEntityID = targetProps.id; + return makeRunningValues(true, [], []); + } + + return makeRunningValues(false, [], []); + }; + + this.run = function(controllerData) { + if ((controllerData.triggerClicks[this.hand] < TRIGGER_OFF_VALUE && + controllerData.secondaryValues[this.hand] < TRIGGER_OFF_VALUE) || this.hyperlink === "") { + return makeRunningValues(false, [], []); + } + + if (controllerData.triggerClicks[this.hand] || + controllerData.secondaryValues[this.hand] > BUMPER_ON_VALUE) { + AddressManager.handleLookupString(this.hyperlink); + return makeRunningValues(false, [], []); + } + + return makeRunningValues(true, [], []); + }; + } + + var leftNearGrabHyperLinkEntity = new NearGrabHyperLinkEntity(LEFT_HAND); + var rightNearGrabHyperLinkEntity = new NearGrabHyperLinkEntity(RIGHT_HAND); + + enableDispatcherModule("LeftNearGrabHyperLink", leftNearGrabHyperLinkEntity); + enableDispatcherModule("RightNearGrabHyperLink", rightNearGrabHyperLinkEntity); + + function cleanup() { + disableDispatcherModule("LeftNearGrabHyperLink"); + disableDispatcherModule("RightNearGrabHyperLink"); + + } + + Script.scriptEnding.connect(cleanup); +}()); diff --git a/scripts/simplifiedUI/system/controllers/controllerModules/nearParentGrabOverlay.js b/scripts/simplifiedUI/system/controllers/controllerModules/nearParentGrabOverlay.js new file mode 100644 index 0000000000..5dcfee23cb --- /dev/null +++ b/scripts/simplifiedUI/system/controllers/controllerModules/nearParentGrabOverlay.js @@ -0,0 +1,255 @@ +"use strict"; + +// nearParentGrabOverlay.js +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html + + +/* global Script, MyAvatar, Controller, RIGHT_HAND, LEFT_HAND, getControllerJointIndex, + enableDispatcherModule, disableDispatcherModule, Messages, HAPTIC_PULSE_STRENGTH, HAPTIC_PULSE_DURATION, + makeDispatcherModuleParameters, Overlays, makeRunningValues, Vec3, resizeTablet, getTabletWidthFromSettings, + NEAR_GRAB_RADIUS, HMD, Uuid, getEnabledModuleByName +*/ + +Script.include("/~/system/libraries/controllerDispatcherUtils.js"); +Script.include("/~/system/libraries/utils.js"); + +(function() { + + // XXX this.ignoreIK = (grabbableData.ignoreIK !== undefined) ? grabbableData.ignoreIK : true; + // XXX this.kinematicGrab = (grabbableData.kinematic !== undefined) ? grabbableData.kinematic : NEAR_GRABBING_KINEMATIC; + + function NearParentingGrabOverlay(hand) { + this.hand = hand; + this.grabbedThingID = null; + this.previousParentID = {}; + this.previousParentJointIndex = {}; + this.previouslyUnhooked = {}; + this.robbed = false; + + this.parameters = makeDispatcherModuleParameters( + 90, + this.hand === RIGHT_HAND ? ["rightHand"] : ["leftHand"], + [], + 100); + + + // XXX does handJointIndex change if the avatar changes? + this.handJointIndex = MyAvatar.getJointIndex(this.hand === RIGHT_HAND ? "RightHand" : "LeftHand"); + + this.getOtherModule = function() { + return (this.hand === RIGHT_HAND) ? leftNearParentingGrabOverlay : rightNearParentingGrabOverlay; + }; + + this.otherHandIsParent = function(props) { + return this.getOtherModule().thisHandIsParent(props); + }; + + this.isGrabbedThingVisible = function() { + return Overlays.getProperty(this.grabbedThingID, "visible"); + }; + + this.thisHandIsParent = function(props) { + if (props.parentID !== MyAvatar.sessionUUID && props.parentID !== MyAvatar.SELF_ID) { + return false; + } + + var handJointIndex = MyAvatar.getJointIndex(this.hand === RIGHT_HAND ? "RightHand" : "LeftHand"); + if (props.parentJointIndex === handJointIndex) { + return true; + } + + var controllerJointIndex = this.controllerJointIndex; + if (props.parentJointIndex === controllerJointIndex) { + return true; + } + + var controllerCRJointIndex = MyAvatar.getJointIndex(this.hand === RIGHT_HAND ? + "_CAMERA_RELATIVE_CONTROLLER_RIGHTHAND" : + "_CAMERA_RELATIVE_CONTROLLER_LEFTHAND"); + + if (props.parentJointIndex === controllerCRJointIndex) { + return true; + } + + return false; + }; + + this.getGrabbedProperties = function() { + return { + position: Overlays.getProperty(this.grabbedThingID, "position"), + rotation: Overlays.getProperty(this.grabbedThingID, "rotation"), + parentID: Overlays.getProperty(this.grabbedThingID, "parentID"), + parentJointIndex: Overlays.getProperty(this.grabbedThingID, "parentJointIndex"), + dynamic: false, + shapeType: "none" + }; + }; + + + this.startNearParentingGrabOverlay = function (controllerData) { + Controller.triggerHapticPulse(HAPTIC_PULSE_STRENGTH, HAPTIC_PULSE_DURATION, this.hand); + + this.controllerJointIndex = getControllerJointIndex(this.hand); + var handJointIndex = this.controllerJointIndex; + + var grabbedProperties = this.getGrabbedProperties(); + + var reparentProps = { + parentID: MyAvatar.SELF_ID, + parentJointIndex: handJointIndex, + velocity: {x: 0, y: 0, z: 0}, + angularVelocity: {x: 0, y: 0, z: 0} + }; + + if (this.thisHandIsParent(grabbedProperties)) { + // this should never happen, but if it does, don't set previous parent to be this hand. + // this.previousParentID[this.grabbedThingID] = NULL; + // this.previousParentJointIndex[this.grabbedThingID] = -1; + } else if (this.otherHandIsParent(grabbedProperties)) { + // the other hand is parent. Steal the object and information + var otherModule = this.getOtherModule(); + this.previousParentID[this.grabbedThingID] = otherModule.previousParentID[this.grabbedThingID]; + this.previousParentJointIndex[this.grabbedThingID] = otherModule.previousParentJointIndex[this.grabbedThingID]; + otherModule.robbed = true; + } else { + this.previousParentID[this.grabbedThingID] = grabbedProperties.parentID; + this.previousParentJointIndex[this.grabbedThingID] = grabbedProperties.parentJointIndex; + } + + // resizeTablet to counter adjust offsets to account for change of scale from sensorToWorldMatrix + if (HMD.tabletID && this.grabbedThingID === HMD.tabletID) { + reparentAndScaleTablet(getTabletWidthFromSettings(), reparentProps); + } else { + Entities.editEntity(this.grabbedThingID, reparentProps); + } + + Messages.sendMessage('Hifi-Object-Manipulation', JSON.stringify({ + action: 'grab', + grabbedEntity: this.grabbedThingID, + joint: this.hand === RIGHT_HAND ? "RightHand" : "LeftHand" + })); + }; + + this.endNearParentingGrabOverlay = function () { + var previousParentID = this.previousParentID[this.grabbedThingID]; + if ((previousParentID === Uuid.NULL || previousParentID === null) && !this.robbed) { + Overlays.editOverlay(this.grabbedThingID, { + parentID: Uuid.NULL, + parentJointIndex: -1 + }); + } else if (!this.robbed){ + // before we grabbed it, overlay was a child of something; put it back. + Entities.editEntity(this.grabbedThingID, { + parentID: this.previousParentID[this.grabbedThingID], + parentJointIndex: this.previousParentJointIndex[this.grabbedThingID] + }); + + // resizeTablet to counter adjust offsets to account for change of scale from sensorToWorldMatrix + if (HMD.tabletID && this.grabbedThingID === HMD.tabletID) { + resizeTablet(getTabletWidthFromSettings(), this.previousParentJointIndex[this.grabbedThingID]); + } + } + + Messages.sendMessage('Hifi-Object-Manipulation', JSON.stringify({ + action: 'release', + grabbedEntity: this.grabbedThingID, + joint: this.hand === RIGHT_HAND ? "RightHand" : "LeftHand" + })); + + this.grabbedThingID = null; + }; + + this.getTargetID = function(overlays, controllerData) { + var sensorScaleFactor = MyAvatar.sensorToWorldScale; + for (var i = 0; i < overlays.length; i++) { + var overlayPosition = Overlays.getProperty(overlays[i], "position"); + var handPosition = controllerData.controllerLocations[this.hand].position; + var distance = Vec3.distance(overlayPosition, handPosition); + if (distance <= NEAR_GRAB_RADIUS * sensorScaleFactor) { + if (overlays[i] !== HMD.miniTabletID || controllerData.secondaryValues[this.hand] === 0) { + // Don't grab mini tablet with grip. + return overlays[i]; + } + } + } + return null; + }; + + this.isEditing = function () { + var inEditModeModule = getEnabledModuleByName(this.hand === RIGHT_HAND + ? "RightHandInEditMode" : "LeftHandInEditMode"); + if (inEditModeModule && inEditModeModule.isEditing) { + return true; + } + var inVREditModeModule = getEnabledModuleByName(this.hand === RIGHT_HAND + ? "RightHandInVREditMode" : "LeftHandInVREditMode"); + if (inVREditModeModule && inVREditModeModule.isEditing) { + return true; + } + return false; + }; + + this.isReady = function (controllerData) { + if ((controllerData.triggerClicks[this.hand] === 0 && controllerData.secondaryValues[this.hand] === 0) + || this.isEditing()) { + this.robbed = false; + return makeRunningValues(false, [], []); + } + + this.grabbedThingID = null; + + var candidateOverlays = controllerData.nearbyOverlayIDs[this.hand]; + var grabbableOverlays = candidateOverlays.filter(function(overlayID) { + return Overlays.getProperty(overlayID, "grabbable"); + }); + + var targetID = this.getTargetID(grabbableOverlays, controllerData); + if (targetID && !this.robbed) { + this.grabbedThingID = targetID; + this.startNearParentingGrabOverlay(controllerData); + return makeRunningValues(true, [this.grabbedThingID], []); + } else { + return makeRunningValues(false, [], []); + } + }; + + this.run = function (controllerData) { + if ((controllerData.triggerClicks[this.hand] === 0 && controllerData.secondaryValues[this.hand] === 0) + || this.isEditing() || !this.isGrabbedThingVisible()) { + this.endNearParentingGrabOverlay(); + this.robbed = false; + return makeRunningValues(false, [], []); + } else { + // check if someone stole the target from us + var grabbedProperties = this.getGrabbedProperties(); + if (!this.thisHandIsParent(grabbedProperties)) { + return makeRunningValues(false, [], []); + } + + return makeRunningValues(true, [this.grabbedThingID], []); + } + }; + + this.cleanup = function () { + if (this.grabbedThingID) { + this.endNearParentingGrabOverlay(); + } + }; + } + + var leftNearParentingGrabOverlay = new NearParentingGrabOverlay(LEFT_HAND); + var rightNearParentingGrabOverlay = new NearParentingGrabOverlay(RIGHT_HAND); + + enableDispatcherModule("LeftNearParentingGrabOverlay", leftNearParentingGrabOverlay); + enableDispatcherModule("RightNearParentingGrabOverlay", rightNearParentingGrabOverlay); + + function cleanup() { + leftNearParentingGrabOverlay.cleanup(); + rightNearParentingGrabOverlay.cleanup(); + disableDispatcherModule("LeftNearParentingGrabOverlay"); + disableDispatcherModule("RightNearParentingGrabOverlay"); + } + Script.scriptEnding.connect(cleanup); +}()); diff --git a/scripts/simplifiedUI/system/controllers/controllerModules/nearTabletHighlight.js b/scripts/simplifiedUI/system/controllers/controllerModules/nearTabletHighlight.js new file mode 100644 index 0000000000..2e046f5dc6 --- /dev/null +++ b/scripts/simplifiedUI/system/controllers/controllerModules/nearTabletHighlight.js @@ -0,0 +1,135 @@ +// +// nearTabletHighlight.js +// +// Highlight the tablet if a hand is near enough to grab it and it isn't grabbed. +// +// Created by David Rowe on 28 Aug 2018. +// Copyright 2018 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +/* global LEFT_HAND, RIGHT_HAND, makeDispatcherModuleParameters, makeRunningValues, enableDispatcherModule, + * disableDispatcherModule, getEnabledModuleByName */ + +Script.include("/~/system/libraries/controllerDispatcherUtils.js"); + +(function () { + + "use strict"; + + var TABLET_GRABBABLE_SELECTION_NAME = "tabletGrabbableSelection"; + var TABLET_GRABBABLE_SELECTION_STYLE = { + outlineUnoccludedColor: { red: 0, green: 180, blue: 239 }, // #00b4ef + outlineUnoccludedAlpha: 1, + outlineOccludedColor: { red: 0, green: 0, blue: 0 }, + outlineOccludedAlpha: 0, + fillUnoccludedColor: { red: 0, green: 0, blue: 0 }, + fillUnoccludedAlpha: 0, + fillOccludedColor: { red: 0, green: 0, blue: 0 }, + fillOccludedAlpha: 0, + outlineWidth: 4, + isOutlineSmooth: false + }; + + var isTabletNearGrabbable = [false, false]; + var isTabletHighlighted = false; + + function setTabletNearGrabbable(hand, enabled) { + if (enabled === isTabletNearGrabbable[hand]) { + return; + } + + isTabletNearGrabbable[hand] = enabled; + + if (isTabletNearGrabbable[LEFT_HAND] || isTabletNearGrabbable[RIGHT_HAND]) { + if (!isTabletHighlighted) { + Selection.addToSelectedItemsList(TABLET_GRABBABLE_SELECTION_NAME, "overlay", HMD.tabletID); + isTabletHighlighted = true; + } + } else { + if (isTabletHighlighted) { + Selection.removeFromSelectedItemsList(TABLET_GRABBABLE_SELECTION_NAME, "overlay", HMD.tabletID); + isTabletHighlighted = false; + } + } + } + + function NearTabletHighlight(hand) { + this.hand = hand; + + this.parameters = makeDispatcherModuleParameters( + 95, + this.hand === RIGHT_HAND ? ["rightHand"] : ["leftHand"], + [], + 100 + ); + + this.isEditing = function () { + var inEditModeModule = getEnabledModuleByName(this.hand === RIGHT_HAND + ? "RightHandInEditMode" : "LeftHandInEditMode"); + if (inEditModeModule && inEditModeModule.isEditing) { + return true; + } + var inVREditModeModule = getEnabledModuleByName(this.hand === RIGHT_HAND + ? "RightHandInVREditMode" : "LeftHandInVREditMode"); + if (inVREditModeModule && inVREditModeModule.isEditing) { + return true; + } + return false; + }; + + this.isNearTablet = function (controllerData) { + return HMD.tabletID && controllerData.nearbyOverlayIDs[this.hand].indexOf(HMD.tabletID) !== -1; + }; + + this.isReady = function (controllerData) { + if (!this.isEditing() && this.isNearTablet(controllerData)) { + return makeRunningValues(true, [], []); + } + setTabletNearGrabbable(this.hand, false); + return makeRunningValues(false, [], []); + }; + + this.run = function (controllerData) { + if (this.isEditing() || !this.isNearTablet(controllerData)) { + setTabletNearGrabbable(this.hand, false); + return makeRunningValues(false, [], []); + } + + if (controllerData.triggerClicks[this.hand] || controllerData.secondaryValues[this.hand]) { + setTabletNearGrabbable(this.hand, false); + return makeRunningValues(false, [], []); + } + + setTabletNearGrabbable(this.hand, true); + return makeRunningValues(true, [], []); + }; + } + + var leftNearTabletHighlight = new NearTabletHighlight(LEFT_HAND); + var rightNearTabletHighlight = new NearTabletHighlight(RIGHT_HAND); + enableDispatcherModule("LeftNearTabletHighlight", leftNearTabletHighlight); + enableDispatcherModule("RightNearTabletHighlight", rightNearTabletHighlight); + + function onDisplayModeChanged() { + if (HMD.active) { + Selection.enableListHighlight(TABLET_GRABBABLE_SELECTION_NAME, TABLET_GRABBABLE_SELECTION_STYLE); + } else { + Selection.disableListHighlight(TABLET_GRABBABLE_SELECTION_NAME); + Selection.clearSelectedItemsList(TABLET_GRABBABLE_SELECTION_NAME); + } + } + HMD.displayModeChanged.connect(onDisplayModeChanged); + HMD.mountedChanged.connect(onDisplayModeChanged); + onDisplayModeChanged(); + + function cleanUp() { + disableDispatcherModule("LeftNearTabletHighlight"); + disableDispatcherModule("RightNearTabletHighlight"); + Selection.disableListHighlight(TABLET_GRABBABLE_SELECTION_NAME); + } + Script.scriptEnding.connect(cleanUp); + +}()); diff --git a/scripts/simplifiedUI/system/controllers/controllerModules/nearTrigger.js b/scripts/simplifiedUI/system/controllers/controllerModules/nearTrigger.js new file mode 100644 index 0000000000..4bff4ea3f0 --- /dev/null +++ b/scripts/simplifiedUI/system/controllers/controllerModules/nearTrigger.js @@ -0,0 +1,120 @@ +"use strict"; + +// nearTrigger.js +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html + + +/* global Script, Entities, MyAvatar, RIGHT_HAND, LEFT_HAND, enableDispatcherModule, disableDispatcherModule, getGrabbableData, + Vec3, TRIGGER_OFF_VALUE, makeDispatcherModuleParameters, makeRunningValues, NEAR_GRAB_RADIUS +*/ + +Script.include("/~/system/libraries/controllerDispatcherUtils.js"); + +(function() { + + function entityWantsNearTrigger(props) { + var grabbableData = getGrabbableData(props); + return grabbableData.triggerable; + } + + function NearTriggerEntity(hand) { + this.hand = hand; + this.targetEntityID = null; + this.grabbing = false; + this.previousParentID = {}; + this.previousParentJointIndex = {}; + this.previouslyUnhooked = {}; + this.startSent = false; + + this.parameters = makeDispatcherModuleParameters( + 480, + this.hand === RIGHT_HAND ? ["rightHandTrigger", "rightHand"] : ["leftHandTrigger", "leftHand"], + [], + 100); + + this.getTargetProps = function (controllerData) { + // nearbyEntityProperties is already sorted by length from controller + var nearbyEntityProperties = controllerData.nearbyEntityProperties[this.hand]; + var sensorScaleFactor = MyAvatar.sensorToWorldScale; + for (var i = 0; i < nearbyEntityProperties.length; i++) { + var props = nearbyEntityProperties[i]; + var handPosition = controllerData.controllerLocations[this.hand].position; + var distance = Vec3.distance(props.position, handPosition); + if (distance > NEAR_GRAB_RADIUS * sensorScaleFactor) { + continue; + } + if (entityWantsNearTrigger(props)) { + return props; + } + } + return null; + }; + + this.startNearTrigger = function (controllerData) { + var args = [this.hand === RIGHT_HAND ? "right" : "left", MyAvatar.sessionUUID]; + Entities.callEntityMethod(this.targetEntityID, "startNearTrigger", args); + }; + + this.continueNearTrigger = function (controllerData) { + var args = [this.hand === RIGHT_HAND ? "right" : "left", MyAvatar.sessionUUID]; + Entities.callEntityMethod(this.targetEntityID, "continueNearTrigger", args); + }; + + this.endNearTrigger = function (controllerData) { + var args = [this.hand === RIGHT_HAND ? "right" : "left", MyAvatar.sessionUUID]; + Entities.callEntityMethod(this.targetEntityID, "stopNearTrigger", args); + }; + + this.isReady = function (controllerData) { + this.targetEntityID = null; + + if (controllerData.triggerValues[this.hand] < TRIGGER_OFF_VALUE) { + return makeRunningValues(false, [], []); + } + + var targetProps = this.getTargetProps(controllerData); + if (targetProps) { + this.targetEntityID = targetProps.id; + return makeRunningValues(true, [this.targetEntityID], []); + } else { + return makeRunningValues(false, [], []); + } + }; + + this.run = function (controllerData) { + if (!this.startSent) { + this.startNearTrigger(controllerData); + this.startSent = true; + } else if (controllerData.triggerValues[this.hand] < TRIGGER_OFF_VALUE) { + this.endNearTrigger(controllerData); + this.startSent = false; + return makeRunningValues(false, [], []); + } else { + this.continueNearTrigger(controllerData); + } + return makeRunningValues(true, [this.targetEntityID], []); + }; + + this.cleanup = function () { + if (this.targetEntityID) { + this.endNearTrigger(); + } + }; + } + + var leftNearTriggerEntity = new NearTriggerEntity(LEFT_HAND); + var rightNearTriggerEntity = new NearTriggerEntity(RIGHT_HAND); + + enableDispatcherModule("LeftNearTriggerEntity", leftNearTriggerEntity); + enableDispatcherModule("RightNearTriggerEntity", rightNearTriggerEntity); + + function cleanup() { + leftNearTriggerEntity.cleanup(); + rightNearTriggerEntity.cleanup(); + disableDispatcherModule("LeftNearTriggerEntity"); + disableDispatcherModule("RightNearTriggerEntity"); + } + Script.scriptEnding.connect(cleanup); +}()); diff --git a/scripts/simplifiedUI/system/controllers/controllerModules/pushToTalk.js b/scripts/simplifiedUI/system/controllers/controllerModules/pushToTalk.js new file mode 100644 index 0000000000..11335ba2f5 --- /dev/null +++ b/scripts/simplifiedUI/system/controllers/controllerModules/pushToTalk.js @@ -0,0 +1,64 @@ +"use strict"; + +// Created by Jason C. Najera on 3/7/2019 +// Copyright 2019 High Fidelity, Inc. +// +// Handles Push-to-Talk functionality for HMD mode. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html + +Script.include("/~/system/libraries/controllerDispatcherUtils.js"); +Script.include("/~/system/libraries/controllers.js"); + +(function() { // BEGIN LOCAL_SCOPE + function PushToTalkHandler() { + var _this = this; + this.active = false; + + this.shouldTalk = function (controllerData) { + // Set up test against controllerData here... + var gripVal = controllerData.secondaryValues[LEFT_HAND] && controllerData.secondaryValues[RIGHT_HAND]; + return (gripVal) ? true : false; + }; + + this.shouldStopTalking = function (controllerData) { + var gripVal = controllerData.secondaryValues[LEFT_HAND] && controllerData.secondaryValues[RIGHT_HAND]; + return (gripVal) ? false : true; + }; + + this.isReady = function (controllerData, deltaTime) { + if (HMD.active && Audio.pushToTalk && this.shouldTalk(controllerData)) { + Audio.pushingToTalk = true; + return makeRunningValues(true, [], []); + } + + return makeRunningValues(false, [], []); + }; + + this.run = function (controllerData, deltaTime) { + if (this.shouldStopTalking(controllerData) || !Audio.pushToTalk) { + Audio.pushingToTalk = false; + print("Stop pushing to talk."); + return makeRunningValues(false, [], []); + } + + return makeRunningValues(true, [], []); + }; + + this.parameters = makeDispatcherModuleParameters( + 950, + ["head"], + [], + 100); + } + + var pushToTalk = new PushToTalkHandler(); + enableDispatcherModule("PushToTalk", pushToTalk); + + function cleanup() { + disableDispatcherModule("PushToTalk"); + }; + + Script.scriptEnding.connect(cleanup); +}()); // END LOCAL_SCOPE diff --git a/scripts/simplifiedUI/system/controllers/controllerModules/scaleAvatar.js b/scripts/simplifiedUI/system/controllers/controllerModules/scaleAvatar.js new file mode 100644 index 0000000000..1868b0228a --- /dev/null +++ b/scripts/simplifiedUI/system/controllers/controllerModules/scaleAvatar.js @@ -0,0 +1,88 @@ +// scaleAvatar.js +// +// Created by Dante Ruiz on 9/11/17 +// +// Grabs physically moveable entities with hydra-like controllers; it works for either near or far objects. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html + +/* global Script, Vec3, MyAvatar, RIGHT_HAND */ + +(function () { + var dispatcherUtils = Script.require("/~/system/libraries/controllerDispatcherUtils.js"); + + function clamp(val, min, max) { + return Math.max(min, Math.min(max, val)); + } + + function ScaleAvatar(hand) { + this.hand = hand; + this.scalingStartAvatarScale = 0; + this.scalingStartDistance = 0; + + this.parameters = dispatcherUtils.makeDispatcherModuleParameters( + 120, + this.hand === RIGHT_HAND ? ["rightHand"] : ["leftHand"], + [], + 100 + ); + + this.otherHand = function() { + return this.hand === dispatcherUtils.RIGHT_HAND ? dispatcherUtils.LEFT_HAND : dispatcherUtils.RIGHT_HAND; + }; + + this.getOtherModule = function() { + var otherModule = this.hand === dispatcherUtils.RIGHT_HAND ? leftScaleAvatar : rightScaleAvatar; + return otherModule; + }; + + this.triggersPressed = function(controllerData) { + if (controllerData.triggerClicks[this.hand] && + controllerData.secondaryValues[this.hand] > dispatcherUtils.BUMPER_ON_VALUE) { + return true; + } + return false; + }; + + this.isReady = function(controllerData) { + var otherModule = this.getOtherModule(); + if (this.triggersPressed(controllerData) && otherModule.triggersPressed(controllerData)) { + this.scalingStartAvatarScale = MyAvatar.scale; + this.scalingStartDistance = Vec3.length(Vec3.subtract(controllerData.controllerLocations[this.hand].position, + controllerData.controllerLocations[this.otherHand()].position)); + return dispatcherUtils.makeRunningValues(true, [], []); + } + return dispatcherUtils.makeRunningValues(false, [], []); + }; + + this.run = function(controllerData) { + var otherModule = this.getOtherModule(); + if (this.triggersPressed(controllerData) && otherModule.triggersPressed(controllerData)) { + if (this.hand === dispatcherUtils.RIGHT_HAND) { + var scalingCurrentDistance = + Vec3.length(Vec3.subtract(controllerData.controllerLocations[this.hand].position, + controllerData.controllerLocations[this.otherHand()].position)); + + var newAvatarScale = (scalingCurrentDistance / this.scalingStartDistance) * this.scalingStartAvatarScale; + MyAvatar.scale = clamp(newAvatarScale, MyAvatar.getDomainMinScale(), MyAvatar.getDomainMaxScale()); + MyAvatar.scaleChanged(); + } + return dispatcherUtils.makeRunningValues(true, [], []); + } + return dispatcherUtils.makeRunningValues(false, [], []); + }; + } + + var leftScaleAvatar = new ScaleAvatar(dispatcherUtils.LEFT_HAND); + var rightScaleAvatar = new ScaleAvatar(dispatcherUtils.RIGHT_HAND); + + dispatcherUtils.enableDispatcherModule("LeftScaleAvatar", leftScaleAvatar); + dispatcherUtils.enableDispatcherModule("RightScaleAvatar", rightScaleAvatar); + + function cleanup() { + dispatcherUtils.disableDispatcherModule("LeftScaleAvatar"); + dispatcherUtils.disableDispatcherModule("RightScaleAvatar"); + } + Script.scriptEnding.connect(cleanup); +})(); diff --git a/scripts/simplifiedUI/system/controllers/controllerModules/scaleEntity.js b/scripts/simplifiedUI/system/controllers/controllerModules/scaleEntity.js new file mode 100644 index 0000000000..50b6c5b853 --- /dev/null +++ b/scripts/simplifiedUI/system/controllers/controllerModules/scaleEntity.js @@ -0,0 +1,110 @@ +// scaleEntity.js +// +// Created by Dante Ruiz on 9/18/17 +// +// Grabs physically moveable entities with hydra-like controllers; it works for either near or far objects. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html + +/* global Script, Vec3, MyAvatar, Entities, RIGHT_HAND, entityIsGrabbable */ + +(function() { + var dispatcherUtils = Script.require("/~/system/libraries/controllerDispatcherUtils.js"); + function ScaleEntity(hand) { + this.hand = hand; + this.grabbedThingID = false; + this.scalingStartDistance = false; + this.scalingStartDimensions = false; + + this.parameters = dispatcherUtils.makeDispatcherModuleParameters( + 120, + this.hand === RIGHT_HAND ? ["rightHandTrigger"] : ["leftHandTrigger"], + [], + 100 + ); + + this.otherHand = function() { + return this.hand === dispatcherUtils.RIGHT_HAND ? dispatcherUtils.LEFT_HAND : dispatcherUtils.RIGHT_HAND; + }; + + this.otherModule = function() { + return this.hand === dispatcherUtils.RIGHT_HAND ? leftScaleEntity : rightScaleEntity; + }; + + this.bumperPressed = function(controllerData) { + return ( controllerData.secondaryValues[this.hand] > dispatcherUtils.BUMPER_ON_VALUE); + }; + + this.getTargetProps = function(controllerData) { + // nearbyEntityProperties is already sorted by length from controller + var nearbyEntityProperties = controllerData.nearbyEntityProperties[this.hand]; + var sensorScaleFactor = MyAvatar.sensorToWorldScale; + for (var i = 0; i < nearbyEntityProperties.length; i++) { + var props = nearbyEntityProperties[i]; + var handPosition = controllerData.controllerLocations[this.hand].position; + var distance = Vec3.distance(props.position, handPosition); + if (distance > dispatcherUtils.NEAR_GRAB_RADIUS * sensorScaleFactor) { + continue; + } + if ((dispatcherUtils.entityIsGrabbable(props) || + dispatcherUtils.propsArePhysical(props)) && !props.locked) { + return props; + } + } + return null; + }; + + this.isReady = function(controllerData) { + var otherModule = this.otherModule(); + if (this.bumperPressed(controllerData) && otherModule.bumperPressed(controllerData)) { + var thisHandTargetProps = this.getTargetProps(controllerData); + var otherHandTargetProps = otherModule.getTargetProps(controllerData); + if (thisHandTargetProps && otherHandTargetProps) { + if (thisHandTargetProps.id === otherHandTargetProps.id) { + if (!entityIsGrabbable(thisHandTargetProps)) { + return dispatcherUtils.makeRunningValues(false, [], []); + } + this.grabbedThingID = thisHandTargetProps.id; + this.scalingStartDistance = + Vec3.length(Vec3.subtract(controllerData.controllerLocations[this.hand].position, + controllerData.controllerLocations[this.otherHand()].position)); + this.scalingStartDimensions = thisHandTargetProps.dimensions; + return dispatcherUtils.makeRunningValues(true, [], []); + } + } + } + this.grabbedThingID = false; + return dispatcherUtils.makeRunningValues(false, [], []); + }; + + this.run = function(controllerData) { + var otherModule = this.otherModule(); + if (this.bumperPressed(controllerData) && otherModule.bumperPressed(controllerData)) { + if (this.hand === dispatcherUtils.RIGHT_HAND) { + var scalingCurrentDistance = + Vec3.length(Vec3.subtract(controllerData.controllerLocations[this.hand].position, + controllerData.controllerLocations[this.otherHand()].position)); + var currentRescale = scalingCurrentDistance / this.scalingStartDistance; + var newDimensions = Vec3.multiply(currentRescale, this.scalingStartDimensions); + Entities.editEntity(this.grabbedThingID, { localDimensions: newDimensions }); + } + return dispatcherUtils.makeRunningValues(true, [], []); + } + this.grabbedThingID = false; + return dispatcherUtils.makeRunningValues(false, [], []); + }; + } + + var leftScaleEntity = new ScaleEntity(dispatcherUtils.LEFT_HAND); + var rightScaleEntity = new ScaleEntity(dispatcherUtils.RIGHT_HAND); + + dispatcherUtils.enableDispatcherModule("LeftScaleEntity", leftScaleEntity); + dispatcherUtils.enableDispatcherModule("RightScaleEntity", rightScaleEntity); + + function cleanup() { + dispatcherUtils.disableDispatcherModule("LeftScaleEntity"); + dispatcherUtils.disableDispatcherModule("RightScaleEntity"); + } + Script.scriptEnding.connect(cleanup); +})(); diff --git a/scripts/simplifiedUI/system/controllers/controllerModules/stylusInput.js b/scripts/simplifiedUI/system/controllers/controllerModules/stylusInput.js new file mode 100644 index 0000000000..c4aa9efd50 --- /dev/null +++ b/scripts/simplifiedUI/system/controllers/controllerModules/stylusInput.js @@ -0,0 +1,220 @@ +"use strict"; + +// stylusInput.js +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html + +/* global Script, MyAvatar, Controller, Uuid, RIGHT_HAND, LEFT_HAND, enableDispatcherModule, disableDispatcherModule, + makeRunningValues, Vec3, makeDispatcherModuleParameters, Overlays, HMD, Settings, getEnabledModuleByName, Pointers, + Picks, PickType +*/ + +Script.include("/~/system/libraries/controllerDispatcherUtils.js"); +Script.include("/~/system/libraries/controllers.js"); + +(function() { + function isNearStylusTarget(stylusTargets, maxNormalDistance) { + var stylusTargetIDs = []; + for (var index = 0; index < stylusTargets.length; index++) { + var stylusTarget = stylusTargets[index]; + if (stylusTarget.distance <= maxNormalDistance && !(HMD.tabletID && stylusTarget.id === HMD.tabletID)) { + stylusTargetIDs.push(stylusTarget.id); + } + } + return stylusTargetIDs; + } + + function getOverlayDistance(controllerPosition, overlayID) { + var position = Overlays.getProperty(overlayID, "position"); + return { + id: overlayID, + distance: Vec3.distance(position, controllerPosition) + }; + } + + function StylusInput(hand) { + this.hand = hand; + + this.parameters = makeDispatcherModuleParameters( + 100, + this.hand === RIGHT_HAND ? ["rightHand"] : ["leftHand"], + [], + 100); + + this.pointer = Pointers.createPointer(PickType.Stylus, { + hand: this.hand, + filter: Picks.PICK_OVERLAYS, + hover: true, + enabled: true + }); + + this.disable = false; + + this.otherModuleNeedsToRun = function(controllerData) { + var grabOverlayModuleName = this.hand === RIGHT_HAND ? "RightNearParentingGrabOverlay" : "LeftNearParentingGrabOverlay"; + var grabOverlayModule = getEnabledModuleByName(grabOverlayModuleName); + var grabEntityModuleName = this.hand === RIGHT_HAND ? "RightNearParentingGrabEntity" : "LeftNearParentingGrabEntity"; + var grabEntityModule = getEnabledModuleByName(grabEntityModuleName); + var grabOverlayModuleReady = grabOverlayModule ? grabOverlayModule.isReady(controllerData) : makeRunningValues(false, [], []); + var grabEntityModuleReady = grabEntityModule ? grabEntityModule.isReady(controllerData) : makeRunningValues(false, [], []); + var farGrabModuleName = this.hand === RIGHT_HAND ? "RightFarActionGrabEntity" : "LeftFarActionGrabEntity"; + var farGrabModule = getEnabledModuleByName(farGrabModuleName); + var farGrabModuleReady = farGrabModule ? farGrabModule.isReady(controllerData) : makeRunningValues(false, [], []); + var nearTabletHighlightModuleName = + this.hand === RIGHT_HAND ? "RightNearTabletHighlight" : "LeftNearTabletHighlight"; + var nearTabletHighlightModule = getEnabledModuleByName(nearTabletHighlightModuleName); + var nearTabletHighlightModuleReady = nearTabletHighlightModule + ? nearTabletHighlightModule.isReady(controllerData) : makeRunningValues(false, [], []); + return grabOverlayModuleReady.active || farGrabModuleReady.active || grabEntityModuleReady.active + || nearTabletHighlightModuleReady.active; + }; + + this.overlayLaserActive = function(controllerData) { + var rightOverlayLaserModule = getEnabledModuleByName("RightWebSurfaceLaserInput"); + var leftOverlayLaserModule = getEnabledModuleByName("LeftWebSurfaceLaserInput"); + var rightModuleRunning = rightOverlayLaserModule ? rightOverlayLaserModule.isReady(controllerData).active : false; + var leftModuleRunning = leftOverlayLaserModule ? leftOverlayLaserModule.isReady(controllerData).active : false; + return leftModuleRunning || rightModuleRunning; + }; + + this.processStylus = function(controllerData) { + if (this.overlayLaserActive(controllerData) || this.otherModuleNeedsToRun(controllerData)) { + Pointers.setRenderState(this.pointer, "disabled"); + return false; + } + + var sensorScaleFactor = MyAvatar.sensorToWorldScale; + + // build list of stylus targets, near the stylusTip + var stylusTargets = []; + var candidateOverlays = controllerData.nearbyOverlayIDs; + var controllerPosition = controllerData.controllerLocations[this.hand].position; + var i, stylusTarget; + + for (i = 0; i < candidateOverlays.length; i++) { + if (!(HMD.tabletID && candidateOverlays[i] === HMD.tabletID) && + Overlays.getProperty(candidateOverlays[i], "visible")) { + stylusTarget = getOverlayDistance(controllerPosition, candidateOverlays[i]); + if (stylusTarget) { + stylusTargets.push(stylusTarget); + } + } + } + + // add the tabletScreen, if it is valid + if (HMD.tabletScreenID && HMD.tabletScreenID !== Uuid.NULL && + Overlays.getProperty(HMD.tabletScreenID, "visible")) { + stylusTarget = getOverlayDistance(controllerPosition, HMD.tabletScreenID); + if (stylusTarget) { + stylusTargets.push(stylusTarget); + } + } + + // add the tablet home button. + if (HMD.homeButtonID && HMD.homeButtonID !== Uuid.NULL && + Overlays.getProperty(HMD.homeButtonID, "visible")) { + stylusTarget = getOverlayDistance(controllerPosition, HMD.homeButtonID); + if (stylusTarget) { + stylusTargets.push(stylusTarget); + } + } + + // Add the mini tablet. + if (HMD.miniTabletScreenID && Overlays.getProperty(HMD.miniTabletScreenID, "visible")) { + stylusTarget = getOverlayDistance(controllerPosition, HMD.miniTabletScreenID); + if (stylusTarget) { + stylusTargets.push(stylusTarget); + } + } + + const WEB_DISPLAY_STYLUS_DISTANCE = (Keyboard.raised && Keyboard.preferMalletsOverLasers) ? 0.2 : 0.5; + var nearStylusTarget = isNearStylusTarget(stylusTargets, WEB_DISPLAY_STYLUS_DISTANCE * sensorScaleFactor); + + if (nearStylusTarget.length !== 0) { + if (!this.disable) { + Pointers.setRenderState(this.pointer,"events on"); + Pointers.setIncludeItems(this.pointer, nearStylusTarget); + } else { + Pointers.setRenderState(this.pointer,"events off"); + } + return true; + } else { + Pointers.setRenderState(this.pointer, "disabled"); + Pointers.setIncludeItems(this.pointer, []); + return false; + } + }; + + this.isReady = function (controllerData) { + var PREFER_STYLUS_OVER_LASER = "preferStylusOverLaser"; + var isUsingStylus = Settings.getValue(PREFER_STYLUS_OVER_LASER, false); + + if (isUsingStylus && this.processStylus(controllerData)) { + Pointers.enablePointer(this.pointer); + this.hand === RIGHT_HAND ? Keyboard.disableRightMallet() : Keyboard.disableLeftMallet(); + return makeRunningValues(true, [], []); + } else { + Pointers.disablePointer(this.pointer); + if (Keyboard.raised && Keyboard.preferMalletsOverLasers) { + this.hand === RIGHT_HAND ? Keyboard.enableRightMallet() : Keyboard.enableLeftMallet(); + } + return makeRunningValues(false, [], []); + } + }; + + this.run = function (controllerData, deltaTime) { + return this.isReady(controllerData); + }; + + this.cleanup = function () { + Pointers.removePointer(this.pointer); + }; + } + + function mouseHoverEnter(overlayID, event) { + if (event.id === leftTabletStylusInput.pointer && !rightTabletStylusInput.disable && !leftTabletStylusInput.disable) { + rightTabletStylusInput.disable = true; + } else if (event.id === rightTabletStylusInput.pointer && !leftTabletStylusInput.disable && !rightTabletStylusInput.disable) { + leftTabletStylusInput.disable = true; + } + } + + function mouseHoverLeave(overlayID, event) { + if (event.id === leftTabletStylusInput.pointer) { + rightTabletStylusInput.disable = false; + } else if (event.id === rightTabletStylusInput.pointer) { + leftTabletStylusInput.disable = false; + } + } + + var HAPTIC_STYLUS_STRENGTH = 1.0; + var HAPTIC_STYLUS_DURATION = 20.0; + function mousePress(overlayID, event) { + if (HMD.active) { + if (event.id === leftTabletStylusInput.pointer && event.button === "Primary") { + Controller.triggerHapticPulse(HAPTIC_STYLUS_STRENGTH, HAPTIC_STYLUS_DURATION, LEFT_HAND); + } else if (event.id === rightTabletStylusInput.pointer && event.button === "Primary") { + Controller.triggerHapticPulse(HAPTIC_STYLUS_STRENGTH, HAPTIC_STYLUS_DURATION, RIGHT_HAND); + } + } + } + + var leftTabletStylusInput = new StylusInput(LEFT_HAND); + var rightTabletStylusInput = new StylusInput(RIGHT_HAND); + + enableDispatcherModule("LeftTabletStylusInput", leftTabletStylusInput); + enableDispatcherModule("RightTabletStylusInput", rightTabletStylusInput); + + Overlays.hoverEnterOverlay.connect(mouseHoverEnter); + Overlays.hoverLeaveOverlay.connect(mouseHoverLeave); + Overlays.mousePressOnOverlay.connect(mousePress); + + this.cleanup = function () { + leftTabletStylusInput.cleanup(); + rightTabletStylusInput.cleanup(); + disableDispatcherModule("LeftTabletStylusInput"); + disableDispatcherModule("RightTabletStylusInput"); + }; + Script.scriptEnding.connect(this.cleanup); +}()); diff --git a/scripts/simplifiedUI/system/controllers/controllerModules/teleport.js b/scripts/simplifiedUI/system/controllers/controllerModules/teleport.js new file mode 100644 index 0000000000..5a51773930 --- /dev/null +++ b/scripts/simplifiedUI/system/controllers/controllerModules/teleport.js @@ -0,0 +1,1108 @@ +"use strict"; + +// Created by james b. pollack @imgntn on 7/2/2016 +// Copyright 2016 High Fidelity, Inc. +// +// Creates a beam and target and then teleports you there. Release when its close to you to cancel. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html + +/* jslint bitwise: true */ + +/* global Script, Entities, MyAvatar, Controller, Quat, RIGHT_HAND, LEFT_HAND, + enableDispatcherModule, disableDispatcherModule, Messages, makeDispatcherModuleParameters, makeRunningValues, Vec3, + HMD, Uuid, AvatarList, Picks, Pointers, PickType +*/ + +Script.include("/~/system/libraries/Xform.js"); +Script.include("/~/system/libraries/controllerDispatcherUtils.js"); +Script.include("/~/system/libraries/controllers.js"); + +(function() { // BEGIN LOCAL_SCOPE + + var TARGET_MODEL_URL = Script.resolvePath("../../assets/models/teleportationSpotBasev8.fbx"); + var SEAT_MODEL_URL = Script.resolvePath("../../assets/models/teleport-seat.fbx"); + + var TARGET_MODEL_DIMENSIONS = { x: 0.6552, y: 0.3063, z: 0.6552 }; + + var COLORS_TELEPORT_SEAT = { + red: 255, + green: 0, + blue: 170 + }; + + var COLORS_TELEPORT_CAN_TELEPORT = { + red: 97, + green: 247, + blue: 255 + }; + + var COLORS_TELEPORT_CANCEL = { + red: 255, + green: 184, + blue: 73 + }; + + var handInfo = { + right: { + controllerInput: Controller.Standard.RightHand + }, + left: { + controllerInput: Controller.Standard.LeftHand + } + }; + + var cancelPath = { + color: COLORS_TELEPORT_CANCEL, + alpha: 0.3, + width: 0.025, + drawInFront: true + }; + + var teleportPath = { + color: COLORS_TELEPORT_CAN_TELEPORT, + alpha: 0.7, + width: 0.025, + drawInFront: true + }; + + var seatPath = { + color: COLORS_TELEPORT_SEAT, + alpha: 0.7, + width: 0.025, + drawInFront: true + }; + + var teleportEnd = { + type: "model", + url: TARGET_MODEL_URL, + dimensions: TARGET_MODEL_DIMENSIONS, + ignorePickIntersection: true + }; + + var seatEnd = { + type: "model", + url: SEAT_MODEL_URL, + dimensions: TARGET_MODEL_DIMENSIONS, + ignorePickIntersection: true + }; + + var collisionEnd = { + type: "shape", + shape: "box", + dimensions: { x: 1.0, y: 0.001, z: 1.0 }, + alpha: 0.0, + ignorePickIntersection: true + }; + + var teleportRenderStates = [{name: "cancel", path: cancelPath}, + {name: "teleport", path: teleportPath, end: teleportEnd}, + {name: "seat", path: seatPath, end: seatEnd}, + {name: "collision", end: collisionEnd}]; + + var DEFAULT_DISTANCE = 8.0; + var teleportDefaultRenderStates = [{name: "cancel", distance: DEFAULT_DISTANCE, path: cancelPath}]; + + var ignoredEntities = []; + + var TELEPORTER_STATES = { + IDLE: 'idle', + TARGETTING: 'targetting', + TARGETTING_INVALID: 'targetting_invalid' + }; + + var TARGET = { + NONE: 'none', // Not currently targetting anything + INVALID: 'invalid', // The current target is invalid (wall, ceiling, etc.) + COLLIDES: 'collides', // Insufficient space to accommodate the avatar capsule + DISCREPANCY: 'discrepancy', // We are not 100% sure the avatar will fit so we trigger safe landing + SURFACE: 'surface', // The current target is a valid surface + SEAT: 'seat' // The current target is a seat + }; + + var speed = 9.3; + var accelerationAxis = {x: 0.0, y: -5.0, z: 0.0}; + + function Teleporter(hand) { + var _this = this; + this.init = false; + this.hand = hand; + this.buttonValue = 0; + this.standardAxisLY = 0.0; + this.standardAxisRY = 0.0; + this.disabled = false; // used by the 'Hifi-Teleport-Disabler' message handler + this.active = false; + this.state = TELEPORTER_STATES.IDLE; + this.currentTarget = TARGET.INVALID; + this.currentResult = null; + this.capsuleThreshold = 0.05; + this.pickHeightOffset = 0.05; + + this.getOtherModule = function() { + var otherModule = this.hand === RIGHT_HAND ? leftTeleporter : rightTeleporter; + return otherModule; + }; + + this.teleportHeadCollisionPick; + this.teleportHandCollisionPick; + this.teleportParabolaHandVisuals; + this.teleportParabolaHandCollisions; + this.teleportParabolaHeadVisuals; + this.teleportParabolaHeadCollisions; + + + this.PLAY_AREA_OVERLAY_MODEL = Script.resolvePath("../../assets/models/trackingSpacev18.fbx"); + this.PLAY_AREA_OVERLAY_MODEL_DIMENSIONS = { x: 1.969, y: 0.001, z: 1.969 }; + this.PLAY_AREA_FLOAT_ABOVE_FLOOR = 0.005; + this.PLAY_AREA_OVERLAY_OFFSET = // Offset from floor. + { x: 0, y: this.PLAY_AREA_OVERLAY_MODEL_DIMENSIONS.y / 2 + this.PLAY_AREA_FLOAT_ABOVE_FLOOR, z: 0 }; + this.PLAY_AREA_SENSOR_OVERLAY_MODEL = Script.resolvePath("../../assets/models/oculusSensorv11.fbx"); + this.PLAY_AREA_SENSOR_OVERLAY_DIMENSIONS = { x: 0.1198, y: 0.2981, z: 0.1198 }; + this.PLAY_AREA_SENSOR_OVERLAY_ROTATION = Quat.fromVec3Degrees({ x: 0, y: -90, z: 0 }); + this.PLAY_AREA_BOX_ALPHA = 1.0; + this.PLAY_AREA_SENSOR_ALPHA = 0.8; + this.playAreaSensorPositions = []; + this.playArea = { x: 0, y: 0 }; + this.playAreaCenterOffset = this.PLAY_AREA_OVERLAY_OFFSET; + this.isPlayAreaVisible = false; + this.wasPlayAreaVisible = false; + this.isPlayAreaAvailable = false; + this.targetOverlayID = null; + this.playAreaOverlay = null; + this.playAreaSensorPositionOverlays = []; + + this.TELEPORT_SCALE_DURATION = 130; + this.TELEPORT_SCALE_TIMEOUT = 25; + this.isTeleportVisible = false; + this.teleportScaleTimer = null; + this.teleportScaleStart = 0; + this.teleportScaleFactor = 0; + this.teleportScaleMode = "head"; + + this.TELEPORTED_FADE_DELAY_DURATION = 900; + this.TELEPORTED_FADE_DURATION = 200; + this.TELEPORTED_FADE_INTERVAL = 25; + this.TELEPORTED_FADE_DELAY_DELTA = this.TELEPORTED_FADE_INTERVAL / this.TELEPORTED_FADE_DELAY_DURATION; + this.TELEPORTED_FADE_DELTA = this.TELEPORTED_FADE_INTERVAL / this.TELEPORTED_FADE_DURATION; + this.teleportedFadeTimer = null; + this.teleportedFadeDelayFactor = 0; + this.teleportedFadeFactor = 0; + this.teleportedPosition = Vec3.ZERO; + this.TELEPORTED_TARGET_ALPHA = 1.0; + this.TELEPORTED_TARGET_ROTATION = Quat.fromVec3Degrees({ x: 0, y: 180, z: 0 }); + this.teleportedTargetOverlay = null; + + this.setPlayAreaDimensions = function () { + var avatarScale = MyAvatar.sensorToWorldScale; + + var playAreaOverlayProperties = { + dimensions: + Vec3.multiply(_this.teleportScaleFactor * avatarScale, { + x: _this.playArea.width, + y: _this.PLAY_AREA_OVERLAY_MODEL_DIMENSIONS.y, + z: _this.playArea.height + }) + }; + + if (_this.teleportScaleFactor < 1) { + // Adjust position of playAreOverlay so that its base is at correct height. + // Always parenting to teleport target is good enough for this. + var sensorToWorldMatrix = MyAvatar.sensorToWorldMatrix; + var sensorToWorldRotation = Mat4.extractRotation(MyAvatar.sensorToWorldMatrix); + var worldToSensorMatrix = Mat4.inverse(sensorToWorldMatrix); + var avatarSensorPosition = Mat4.transformPoint(worldToSensorMatrix, MyAvatar.position); + avatarSensorPosition.y = 0; + + var targetRotation = Overlays.getProperty(_this.targetOverlayID, "rotation"); + var relativePlayAreaCenterOffset = + Vec3.sum(_this.playAreaCenterOffset, { x: 0, y: -TARGET_MODEL_DIMENSIONS.y / 2, z: 0 }); + var localPosition = Vec3.multiplyQbyV(Quat.inverse(targetRotation), + Vec3.multiplyQbyV(sensorToWorldRotation, + Vec3.multiply(avatarScale, Vec3.subtract(relativePlayAreaCenterOffset, avatarSensorPosition)))); + localPosition.y = _this.teleportScaleFactor * localPosition.y; + + playAreaOverlayProperties.parentID = _this.targetOverlayID; + playAreaOverlayProperties.localPosition = localPosition; + } + + Overlays.editOverlay(_this.playAreaOverlay, playAreaOverlayProperties); + + for (var i = 0; i < _this.playAreaSensorPositionOverlays.length; i++) { + localPosition = _this.playAreaSensorPositions[i]; + localPosition = Vec3.multiply(avatarScale, localPosition); + // Position relative to the play area. + localPosition.y = avatarScale * (_this.PLAY_AREA_SENSOR_OVERLAY_DIMENSIONS.y / 2 + - _this.PLAY_AREA_OVERLAY_MODEL_DIMENSIONS.y / 2); + Overlays.editOverlay(_this.playAreaSensorPositionOverlays[i], { + dimensions: Vec3.multiply(_this.teleportScaleFactor * avatarScale, _this.PLAY_AREA_SENSOR_OVERLAY_DIMENSIONS), + parentID: _this.playAreaOverlay, + localPosition: localPosition + }); + } + }; + + this.updatePlayAreaScale = function () { + if (_this.isPlayAreaAvailable) { + _this.setPlayAreaDimensions(); + } + }; + + + this.teleporterSelectionName = "teleporterSelection" + hand.toString(); + this.TELEPORTER_SELECTION_STYLE = { + outlineUnoccludedColor: { red: 0, green: 0, blue: 0 }, + outlineUnoccludedAlpha: 0, + outlineOccludedColor: { red: 0, green: 0, blue: 0 }, + outlineOccludedAlpha: 0, + fillUnoccludedColor: { red: 0, green: 0, blue: 0 }, + fillUnoccludedAlpha: 0, + fillOccludedColor: { red: 0, green: 0, blue: 255 }, + fillOccludedAlpha: 0.84, + outlineWidth: 0, + isOutlineSmooth: false + }; + + this.addToSelectedItemsList = function (properties) { + for (var i = 0, length = teleportRenderStates.length; i < length; i++) { + var state = properties.renderStates[teleportRenderStates[i].name]; + if (state && state.end) { + Selection.addToSelectedItemsList(_this.teleporterSelectionName, "overlay", state.end); + } + } + }; + + + this.cleanup = function() { + Selection.removeListFromMap(_this.teleporterSelectionName); + Pointers.removePointer(_this.teleportParabolaHandVisuals); + Pointers.removePointer(_this.teleportParabolaHandCollisions); + Pointers.removePointer(_this.teleportParabolaHeadVisuals); + Pointers.removePointer(_this.teleportParabolaHeadCollisions); + Picks.removePick(_this.teleportHandCollisionPick); + Picks.removePick(_this.teleportHeadCollisionPick); + Overlays.deleteOverlay(_this.teleportedTargetOverlay); + Overlays.deleteOverlay(_this.playAreaOverlay); + for (var i = 0; i < _this.playAreaSensorPositionOverlays.length; i++) { + Overlays.deleteOverlay(_this.playAreaSensorPositionOverlays[i]); + } + _this.playAreaSensorPositionOverlays = []; + }; + + this.initPointers = function() { + if (_this.init) { + _this.cleanup(); + } + + _this.teleportParabolaHandVisuals = Pointers.createPointer(PickType.Parabola, { + joint: (_this.hand === RIGHT_HAND) ? "_CAMERA_RELATIVE_CONTROLLER_RIGHTHAND" : "_CAMERA_RELATIVE_CONTROLLER_LEFTHAND", + dirOffset: { x: 0, y: 1, z: 0.1 }, + posOffset: { x: (_this.hand === RIGHT_HAND) ? 0.03 : -0.03, y: 0.2, z: 0.02 }, + filter: Picks.PICK_ENTITIES | Picks.PICK_INCLUDE_INVISIBLE, + faceAvatar: true, + scaleWithParent: true, + centerEndY: false, + speed: speed, + accelerationAxis: accelerationAxis, + rotateAccelerationWithAvatar: true, + renderStates: teleportRenderStates, + defaultRenderStates: teleportDefaultRenderStates, + maxDistance: 8.0 + }); + + _this.teleportParabolaHandCollisions = Pointers.createPointer(PickType.Parabola, { + joint: (_this.hand === RIGHT_HAND) ? "_CAMERA_RELATIVE_CONTROLLER_RIGHTHAND" : "_CAMERA_RELATIVE_CONTROLLER_LEFTHAND", + dirOffset: { x: 0, y: 1, z: 0.1 }, + posOffset: { x: (_this.hand === RIGHT_HAND) ? 0.03 : -0.03, y: 0.2, z: 0.02 }, + filter: Picks.PICK_ENTITIES | Picks.PICK_INCLUDE_INVISIBLE, + faceAvatar: true, + scaleWithParent: true, + centerEndY: false, + speed: speed, + accelerationAxis: accelerationAxis, + rotateAccelerationWithAvatar: true, + renderStates: teleportRenderStates, + maxDistance: 8.0 + }); + + _this.teleportParabolaHeadVisuals = Pointers.createPointer(PickType.Parabola, { + joint: "Avatar", + filter: Picks.PICK_ENTITIES | Picks.PICK_INCLUDE_INVISIBLE, + faceAvatar: true, + scaleWithParent: true, + centerEndY: false, + speed: speed, + accelerationAxis: accelerationAxis, + rotateAccelerationWithAvatar: true, + renderStates: teleportRenderStates, + defaultRenderStates: teleportDefaultRenderStates, + maxDistance: 8.0 + }); + + _this.teleportParabolaHeadCollisions = Pointers.createPointer(PickType.Parabola, { + joint: "Avatar", + filter: Picks.PICK_ENTITIES | Picks.PICK_INCLUDE_INVISIBLE, + faceAvatar: true, + scaleWithParent: true, + centerEndY: false, + speed: speed, + accelerationAxis: accelerationAxis, + rotateAccelerationWithAvatar: true, + renderStates: teleportRenderStates, + maxDistance: 8.0 + }); + + _this.addToSelectedItemsList(Pointers.getPointerProperties(_this.teleportParabolaHandVisuals)); + _this.addToSelectedItemsList(Pointers.getPointerProperties(_this.teleportParabolaHeadVisuals)); + + + var capsuleData = MyAvatar.getCollisionCapsule(); + + var sensorToWorldScale = MyAvatar.getSensorToWorldScale(); + + var diameter = 2.0 * capsuleData.radius / sensorToWorldScale; + var height = (Vec3.distance(capsuleData.start, capsuleData.end) + diameter) / sensorToWorldScale; + var capsuleRatio = 5.0 * diameter / height; + var offset = _this.pickHeightOffset * capsuleRatio; + + _this.teleportHandCollisionPick = Picks.createPick(PickType.Collision, { + enabled: true, + parentID: Pointers.getPointerProperties(_this.teleportParabolaHandCollisions).renderStates["collision"].end, + filter: Picks.PICK_ENTITIES | Picks.PICK_AVATARS, + shape: { + shapeType: "capsule-y", + dimensions: { + x: diameter, + y: height, + z: diameter + } + }, + position: { x: 0, y: offset + height * 0.5, z: 0 }, + threshold: _this.capsuleThreshold + }); + + _this.teleportHeadCollisionPick = Picks.createPick(PickType.Collision, { + enabled: true, + parentID: Pointers.getPointerProperties(_this.teleportParabolaHeadCollisions).renderStates["collision"].end, + filter: Picks.PICK_ENTITIES | Picks.PICK_AVATARS, + shape: { + shapeType: "capsule-y", + dimensions: { + x: diameter, + y: height, + z: diameter + } + }, + position: { x: 0, y: offset + height * 0.5, z: 0 }, + threshold: _this.capsuleThreshold + }); + + + _this.playAreaOverlay = Overlays.addOverlay("model", { + url: _this.PLAY_AREA_OVERLAY_MODEL, + drawInFront: false, + visible: false + }); + + _this.teleportedTargetOverlay = Overlays.addOverlay("model", { + url: TARGET_MODEL_URL, + alpha: _this.TELEPORTED_TARGET_ALPHA, + visible: false + }); + + Selection.addToSelectedItemsList(_this.teleporterSelectionName, "overlay", _this.playAreaOverlay); + Selection.addToSelectedItemsList(_this.teleporterSelectionName, "overlay", _this.teleportedTargetOverlay); + + + _this.playArea = HMD.playArea; + _this.isPlayAreaAvailable = HMD.active && _this.playArea.width !== 0 && _this.playArea.height !== 0; + if (_this.isPlayAreaAvailable) { + _this.playAreaCenterOffset = Vec3.sum({ x: _this.playArea.x, y: 0, z: _this.playArea.y }, + _this.PLAY_AREA_OVERLAY_OFFSET); + _this.playAreaSensorPositions = HMD.sensorPositions; + + for (var i = 0; i < _this.playAreaSensorPositions.length; i++) { + if (i > _this.playAreaSensorPositionOverlays.length - 1) { + var overlay = Overlays.addOverlay("model", { + url: _this.PLAY_AREA_SENSOR_OVERLAY_MODEL, + dimensions: _this.PLAY_AREA_SENSOR_OVERLAY_DIMENSIONS, + parentID: _this.playAreaOverlay, + localRotation: _this.PLAY_AREA_SENSOR_OVERLAY_ROTATION, + drawInFront: false, + visible: false + }); + _this.playAreaSensorPositionOverlays.push(overlay); + Selection.addToSelectedItemsList(_this.teleporterSelectionName, "overlay", overlay); + } + } + + _this.setPlayAreaDimensions(); + } + + _this.init = true; + }; + + _this.initPointers(); + + + this.translateXAction = Controller.findAction("TranslateX"); + this.translateYAction = Controller.findAction("TranslateY"); + this.translateZAction = Controller.findAction("TranslateZ"); + + this.setPlayAreaVisible = function (visible, targetOverlayID, fade) { + if (!_this.isPlayAreaAvailable || _this.isPlayAreaVisible === visible) { + return; + } + + _this.wasPlayAreaVisible = _this.isPlayAreaVisible; + _this.isPlayAreaVisible = visible; + _this.targetOverlayID = targetOverlayID; + + if (_this.teleportedFadeTimer !== null) { + Script.clearTimeout(_this.teleportedFadeTimer); + _this.teleportedFadeTimer = null; + } + if (visible || !fade) { + // Immediately make visible or invisible. + _this.isPlayAreaVisible = visible; + Overlays.editOverlay(_this.playAreaOverlay, { + dimensions: Vec3.ZERO, + alpha: _this.PLAY_AREA_BOX_ALPHA, + visible: visible + }); + for (var i = 0; i < _this.playAreaSensorPositionOverlays.length; i++) { + Overlays.editOverlay(_this.playAreaSensorPositionOverlays[i], { + dimensions: Vec3.ZERO, + alpha: _this.PLAY_AREA_SENSOR_ALPHA, + visible: visible + }); + } + Overlays.editOverlay(_this.teleportedTargetOverlay, { visible: false }); + } else { + // Fading out of overlays is initiated in setTeleportVisible(). + } + }; + + this.updatePlayArea = function (position) { + var sensorToWorldMatrix = MyAvatar.sensorToWorldMatrix; + var sensorToWorldRotation = Mat4.extractRotation(MyAvatar.sensorToWorldMatrix); + var worldToSensorMatrix = Mat4.inverse(sensorToWorldMatrix); + var avatarSensorPosition = Mat4.transformPoint(worldToSensorMatrix, MyAvatar.position); + avatarSensorPosition.y = 0; + + var targetXZPosition = { x: position.x, y: 0, z: position.z }; + var avatarXZPosition = MyAvatar.position; + avatarXZPosition.y = 0; + var MIN_PARENTING_DISTANCE = 0.2; // Parenting under this distance results in the play area's rotation jittering. + if (Vec3.distance(targetXZPosition, avatarXZPosition) < MIN_PARENTING_DISTANCE) { + // Set play area position and rotation in world coordinates with no parenting. + Overlays.editOverlay(_this.playAreaOverlay, { + parentID: Uuid.NULL, + position: Vec3.sum(position, + Vec3.multiplyQbyV(sensorToWorldRotation, + Vec3.multiply(MyAvatar.sensorToWorldScale, + Vec3.subtract(_this.playAreaCenterOffset, avatarSensorPosition)))), + rotation: sensorToWorldRotation + }); + } else { + // Set play area position and rotation in local coordinates with parenting. + var targetRotation = Overlays.getProperty(_this.targetOverlayID, "rotation"); + var sensorToTargetRotation = Quat.multiply(Quat.inverse(targetRotation), sensorToWorldRotation); + var relativePlayAreaCenterOffset = + Vec3.sum(_this.playAreaCenterOffset, { x: 0, y: -TARGET_MODEL_DIMENSIONS.y / 2, z: 0 }); + Overlays.editOverlay(_this.playAreaOverlay, { + parentID: _this.targetOverlayID, + localPosition: Vec3.multiplyQbyV(Quat.inverse(targetRotation), + Vec3.multiplyQbyV(sensorToWorldRotation, + Vec3.multiply(MyAvatar.sensorToWorldScale, + Vec3.subtract(relativePlayAreaCenterOffset, avatarSensorPosition)))), + localRotation: sensorToTargetRotation + }); + } + }; + + + this.scaleInTeleport = function () { + _this.teleportScaleFactor = Math.min((Date.now() - _this.teleportScaleStart) / _this.TELEPORT_SCALE_DURATION, 1); + Pointers.editRenderState( + _this.teleportScaleMode === "head" ? _this.teleportParabolaHeadVisuals : _this.teleportParabolaHandVisuals, + "teleport", + { + path: teleportPath, // Teleport beam disappears if not included. + end: { dimensions: Vec3.multiply(_this.teleportScaleFactor, TARGET_MODEL_DIMENSIONS) } + } + ); + if (_this.isPlayAreaVisible) { + _this.setPlayAreaDimensions(); + } + if (_this.teleportScaleFactor < 1) { + _this.teleportScaleTimer = Script.setTimeout(_this.scaleInTeleport, _this.TELEPORT_SCALE_TIMEOUT); + } else { + _this.teleportScaleTimer = null; + } + }; + + this.fadeOutTeleport = function () { + var isAvatarMoving, + i, length; + + isAvatarMoving = Controller.getActionValue(_this.translateXAction) !== 0 + || Controller.getActionValue(_this.translateYAction) !== 0 + || Controller.getActionValue(_this.translateZAction) !== 0; + + if (_this.teleportedFadeDelayFactor > 0 && !_this.isTeleportVisible && !isAvatarMoving) { + // Delay fade. + _this.teleportedFadeDelayFactor = _this.teleportedFadeDelayFactor - _this.TELEPORTED_FADE_DELAY_DELTA; + _this.teleportedFadeTimer = Script.setTimeout(_this.fadeOutTeleport, _this.TELEPORTED_FADE_INTERVAL); + } else if (_this.teleportedFadeFactor > 0 && !_this.isTeleportVisible && !isAvatarMoving) { + // Fade. + _this.teleportedFadeFactor = _this.teleportedFadeFactor - _this.TELEPORTED_FADE_DELTA; + Overlays.editOverlay(_this.teleportedTargetOverlay, { + alpha: _this.teleportedFadeFactor * _this.TELEPORTED_TARGET_ALPHA + }); + if (_this.wasPlayAreaVisible) { + Overlays.editOverlay(_this.playAreaOverlay, { + alpha: _this.teleportedFadeFactor * _this.PLAY_AREA_BOX_ALPHA + }); + var sensorAlpha = _this.teleportedFadeFactor * _this.PLAY_AREA_SENSOR_ALPHA; + for (i = 0, length = _this.playAreaSensorPositionOverlays.length; i < length; i++) { + Overlays.editOverlay(_this.playAreaSensorPositionOverlays[i], { alpha: sensorAlpha }); + } + } + _this.teleportedFadeTimer = Script.setTimeout(_this.fadeOutTeleport, _this.TELEPORTED_FADE_INTERVAL); + } else { + // Make invisible. + Overlays.editOverlay(_this.teleportedTargetOverlay, { visible: false }); + if (_this.wasPlayAreaVisible) { + Overlays.editOverlay(_this.playAreaOverlay, { visible: false }); + for (i = 0, length = _this.playAreaSensorPositionOverlays.length; i < length; i++) { + Overlays.editOverlay(_this.playAreaSensorPositionOverlays[i], { visible: false }); + } + } + _this.teleportedFadeTimer = null; + Selection.disableListHighlight(_this.teleporterSelectionName); + } + }; + + this.cancelFade = function () { + // Other hand may call this to immediately hide fading overlays. + var i, length; + if (_this.teleportedFadeTimer) { + Overlays.editOverlay(_this.teleportedTargetOverlay, { visible: false }); + if (_this.wasPlayAreaVisible) { + Overlays.editOverlay(_this.playAreaOverlay, { visible: false }); + for (i = 0, length = _this.playAreaSensorPositionOverlays.length; i < length; i++) { + Overlays.editOverlay(_this.playAreaSensorPositionOverlays[i], { visible: false }); + } + } + _this.teleportedFadeTimer = null; + } + }; + + this.setTeleportVisible = function (visible, mode, fade) { + // Scales in teleport target and play area when start displaying them. + if (visible === _this.isTeleportVisible) { + return; + } + + if (visible) { + _this.teleportScaleMode = mode; + Pointers.editRenderState( + mode === "head" ? _this.teleportParabolaHeadVisuals : _this.teleportParabolaHandVisuals, + "teleport", + { + path: teleportPath, // Teleport beam disappears if not included. + end: { dimensions: Vec3.ZERO } + } + ); + _this.getOtherModule().cancelFade(); + _this.teleportScaleStart = Date.now(); + _this.teleportScaleFactor = 0; + _this.scaleInTeleport(); + Selection.enableListHighlight(_this.teleporterSelectionName, _this.TELEPORTER_SELECTION_STYLE); + } else { + if (_this.teleportScaleTimer !== null) { + Script.clearTimeout(_this.teleportScaleTimer); + _this.teleportScaleTimer = null; + } + + if (fade) { + // Copy of target at teleported position for fading. + var avatarScale = MyAvatar.sensorToWorldScale; + Overlays.editOverlay(_this.teleportedTargetOverlay, { + position: Vec3.sum(_this.teleportedPosition, { + x: 0, + y: -getAvatarFootOffset() + avatarScale * TARGET_MODEL_DIMENSIONS.y / 2, + z: 0 + }), + rotation: Quat.multiply(_this.TELEPORTED_TARGET_ROTATION, MyAvatar.orientation), + dimensions: Vec3.multiply(avatarScale, TARGET_MODEL_DIMENSIONS), + alpha: _this.TELEPORTED_TARGET_ALPHA, + visible: true + }); + + // Fade out over time. + _this.teleportedFadeDelayFactor = 1.0; + _this.teleportedFadeFactor = 1.0; + _this.teleportedFadeTimer = Script.setTimeout(_this.fadeOutTeleport, _this.TELEPORTED_FADE_DELAY); + } else { + Selection.disableListHighlight(_this.teleporterSelectionName); + } + } + + _this.isTeleportVisible = visible; + }; + + + this.axisButtonStateX = 0; // Left/right axis button pressed. + this.axisButtonStateY = 0; // Up/down axis button pressed. + this.BUTTON_TRANSITION_DELAY = 100; // Allow time for transition from direction buttons to touch-pad. + + this.axisButtonChangeX = function (value) { + if (value !== 0) { + _this.axisButtonStateX = value; + } else { + // Delay direction button release until after teleport possibly pressed. + Script.setTimeout(function () { + _this.axisButtonStateX = value; + }, _this.BUTTON_TRANSITION_DELAY); + } + }; + + this.axisButtonChangeY = function (value) { + if (value !== 0) { + _this.axisButtonStateY = value; + } else { + // Delay direction button release until after teleport possibly pressed. + Script.setTimeout(function () { + _this.axisButtonStateY = value; + }, _this.BUTTON_TRANSITION_DELAY); + } + }; + + this.teleportLocked = function () { + // Lock teleport if in advanced movement mode and have just transitioned from pressing a direction button. + return Controller.getValue(Controller.Hardware.Application.AdvancedMovement) && + (_this.axisButtonStateX !== 0 || _this.axisButtonStateY !== 0); + }; + + this.buttonPress = function (value) { + if (value === 0 || !_this.teleportLocked()) { + _this.buttonValue = value; + } + }; + + this.getStandardLY = function (value) { + _this.standardAxisLY = value; + }; + + this.getStandardRY = function (value) { + _this.standardAxisRY = value; + }; + + // Return value for the getDominantY and getOffhandY functions has to be inverted. + this.getDominantY = function () { + return (MyAvatar.getDominantHand() === "left") ? -(_this.standardAxisLY) : -(_this.standardAxisRY); + }; + + this.getOffhandY = function () { + return (MyAvatar.getDominantHand() === "left") ? -(_this.standardAxisRY) : -(_this.standardAxisLY); + }; + + this.getDominantHand = function () { + return (MyAvatar.getDominantHand() === "left") ? LEFT_HAND : RIGHT_HAND; + } + + this.getOffHand = function () { + return (MyAvatar.getDominantHand() === "left") ? RIGHT_HAND : LEFT_HAND; + } + + this.showReticle = function () { + return (_this.getDominantY() > TELEPORT_DEADZONE) ? true : false; + }; + + this.shouldTeleport = function () { + return (_this.getDominantY() > TELEPORT_DEADZONE && _this.getOffhandY() > TELEPORT_DEADZONE) ? true : false; + }; + + this.shouldCancel = function () { + //return (_this.getDominantY() < -TELEPORT_DEADZONE || _this.getOffhandY() < -TELEPORT_DEADZONE) ? true : false; + return (_this.getDominantY() <= TELEPORT_DEADZONE) ? true : false; + }; + + this.parameters = makeDispatcherModuleParameters( + 80, + this.hand === RIGHT_HAND ? ["rightHand"] : ["leftHand"], + [], + 100); + + this.enterTeleport = function() { + _this.state = TELEPORTER_STATES.TARGETTING; + }; + + this.isReady = function(controllerData, deltaTime) { + if ((Window.interstitialModeEnabled && !Window.isPhysicsEnabled()) || !MyAvatar.allowTeleporting) { + return makeRunningValues(false, [], []); + } + + var otherModule = this.getOtherModule(); + if (!this.disabled && this.showReticle() && !otherModule.active && this.hand === this.getDominantHand()) { + this.active = true; + this.enterTeleport(); + return makeRunningValues(true, [], []); + } + return makeRunningValues(false, [], []); + }; + + this.run = function(controllerData, deltaTime) { + // Kill condition: + if (_this.shouldCancel()) { + _this.disableLasers(); + this.active = false; + return makeRunningValues(false, [], []); + } + + // Get current hand pose information to see if the pose is valid + var pose = Controller.getPoseValue(handInfo[(_this.hand === RIGHT_HAND) ? 'right' : 'left'].controllerInput); + var mode = pose.valid ? _this.hand : 'head'; + if (!pose.valid) { + Pointers.disablePointer(_this.teleportParabolaHandVisuals); + Pointers.disablePointer(_this.teleportParabolaHandCollisions); + Picks.disablePick(_this.teleportHandCollisionPick); + Pointers.enablePointer(_this.teleportParabolaHeadVisuals); + Pointers.enablePointer(_this.teleportParabolaHeadCollisions); + Picks.enablePick(_this.teleportHeadCollisionPick); + } else { + Pointers.enablePointer(_this.teleportParabolaHandVisuals); + Pointers.enablePointer(_this.teleportParabolaHandCollisions); + Picks.enablePick(_this.teleportHandCollisionPick); + Pointers.disablePointer(_this.teleportParabolaHeadVisuals); + Pointers.disablePointer(_this.teleportParabolaHeadCollisions); + Picks.disablePick(_this.teleportHeadCollisionPick); + } + + // We do up to 2 picks to find a teleport location. + // There are 2 types of teleport locations we are interested in: + // + // 1. A visible floor. This can be any entity surface that points within some degree of "up" + // and where the avatar capsule can be positioned without colliding + // + // 2. A seat. The seat can be visible or invisible. + // + // The Collision Pick is currently parented to the end overlay on teleportParabolaXXXXCollisions + // + // TODO + // Parent the collision Pick directly to the teleportParabolaXXXXVisuals and get rid of teleportParabolaXXXXCollisions + // + var result, collisionResult; + if (mode === 'head') { + result = Pointers.getPrevPickResult(_this.teleportParabolaHeadCollisions); + collisionResult = Picks.getPrevPickResult(_this.teleportHeadCollisionPick); + } else { + result = Pointers.getPrevPickResult(_this.teleportParabolaHandCollisions); + collisionResult = Picks.getPrevPickResult(_this.teleportHandCollisionPick); + } + + var teleportLocationType = getTeleportTargetType(result, collisionResult); + + if (teleportLocationType === TARGET.NONE) { + // Use the cancel default state + _this.setTeleportState(mode, "cancel", ""); + } else if (teleportLocationType === TARGET.INVALID) { + _this.setTeleportState(mode, "", "cancel"); + } else if (teleportLocationType === TARGET.COLLIDES) { + _this.setTeleportState(mode, "cancel", "collision"); + } else if (teleportLocationType === TARGET.SURFACE || teleportLocationType === TARGET.DISCREPANCY) { + _this.setTeleportState(mode, "teleport", "collision"); + _this.updatePlayArea(result.intersection); + } else if (teleportLocationType === TARGET.SEAT) { + _this.setTeleportState(mode, "collision", "seat"); + } + return _this.teleport(result, teleportLocationType); + }; + + this.teleport = function(newResult, target) { + var result = newResult; + _this.teleportedPosition = newResult.intersection; + if (!_this.shouldTeleport()) { + return makeRunningValues(true, [], []); + } + + if (target === TARGET.NONE || target === TARGET.INVALID) { + // Do nothing + } else if (target === TARGET.SEAT) { + Entities.callEntityMethod(result.objectID, 'sit'); + } else if (target === TARGET.SURFACE || target === TARGET.DISCREPANCY) { + var offset = getAvatarFootOffset(); + result.intersection.y += offset; + var shouldLandSafe = target === TARGET.DISCREPANCY; + MyAvatar.goToLocation(result.intersection, true, HMD.orientation, false, shouldLandSafe); + HMD.centerUI(); + MyAvatar.centerBody(); + } + + _this.disableLasers(); + _this.active = false; + return makeRunningValues(false, [], []); + }; + + this.disableLasers = function() { + _this.setPlayAreaVisible(false, null, false); + _this.setTeleportVisible(false, null, false); + Pointers.disablePointer(_this.teleportParabolaHandVisuals); + Pointers.disablePointer(_this.teleportParabolaHandCollisions); + Pointers.disablePointer(_this.teleportParabolaHeadVisuals); + Pointers.disablePointer(_this.teleportParabolaHeadCollisions); + Picks.disablePick(_this.teleportHeadCollisionPick); + Picks.disablePick(_this.teleportHandCollisionPick); + }; + + this.teleportState = ""; + + this.setTeleportState = function (mode, visibleState, invisibleState) { + var teleportState = mode + visibleState + invisibleState; + if (teleportState === _this.teleportState) { + return; + } + _this.teleportState = teleportState; + + var pointerID; + if (mode === 'head') { + Pointers.setRenderState(_this.teleportParabolaHeadVisuals, visibleState); + Pointers.setRenderState(_this.teleportParabolaHeadCollisions, invisibleState); + pointerID = _this.teleportParabolaHeadVisuals; + } else { + Pointers.setRenderState(_this.teleportParabolaHandVisuals, visibleState); + Pointers.setRenderState(_this.teleportParabolaHandCollisions, invisibleState); + pointerID = _this.teleportParabolaHandVisuals; + } + var visible = visibleState === "teleport"; + _this.setPlayAreaVisible(visible && MyAvatar.showPlayArea, + Pointers.getPointerProperties(pointerID).renderStates.teleport.end, false); + _this.setTeleportVisible(visible, mode, false); + }; + + this.setIgnoreEntities = function(entitiesToIgnore) { + Pointers.setIgnoreItems(_this.teleportParabolaHandVisuals, entitiesToIgnore); + Pointers.setIgnoreItems(_this.teleportParabolaHandCollisions, entitiesToIgnore); + Pointers.setIgnoreItems(_this.teleportParabolaHeadVisuals, entitiesToIgnore); + Pointers.setIgnoreItems(_this.teleportParabolaHeadCollisions, entitiesToIgnore); + Picks.setIgnoreItems(_this.teleportHeadCollisionPick, entitiesToIgnore); + Picks.setIgnoreItems(_this.teleportHandCollisionPick, entitiesToIgnore); + }; + } + + // related to repositioning the avatar after you teleport + var FOOT_JOINT_NAMES = ["RightToe_End", "RightToeBase", "RightFoot"]; + var DEFAULT_ROOT_TO_FOOT_OFFSET = 0.5; + + function getAvatarFootOffset() { + + // find a valid foot jointIndex + var footJointIndex = -1; + var i, l = FOOT_JOINT_NAMES.length; + for (i = 0; i < l; i++) { + footJointIndex = MyAvatar.getJointIndex(FOOT_JOINT_NAMES[i]); + if (footJointIndex !== -1) { + break; + } + } + if (footJointIndex !== -1) { + // default vertical offset from foot to avatar root. + 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; + } + } + + var mappingName, teleportMapping; + var isViveMapped = false; + + function parseJSON(json) { + try { + return JSON.parse(json); + } catch (e) { + return undefined; + } + } + // When determininig whether you can teleport to a location, the normal of the + // point that is being intersected with is looked at. If this normal is more + // than MAX_ANGLE_FROM_UP_TO_TELEPORT degrees from your avatar's up, then + // you can't teleport there. + var MAX_ANGLE_FROM_UP_TO_TELEPORT = 70; + var MAX_DISCREPANCY_DISTANCE = 1.0; + var MAX_DOT_SIGN = -0.6; + + function checkForMeshDiscrepancy(result, collisionResult) { + var intersectingObjects = collisionResult.intersectingObjects; + if (intersectingObjects.length > 0 && intersectingObjects.length < 3) { + for (var j = 0; j < collisionResult.intersectingObjects.length; j++) { + var intersectingObject = collisionResult.intersectingObjects[j]; + for (var i = 0; i < intersectingObject.collisionContacts.length; i++) { + var normal = intersectingObject.collisionContacts[i].normalOnPick; + var distanceToPick = Vec3.distance(intersectingObject.collisionContacts[i].pointOnPick, result.intersection); + var normalSign = Vec3.dot(normal, Quat.getUp(MyAvatar.orientation)); + if ((distanceToPick > MAX_DISCREPANCY_DISTANCE) || (normalSign > MAX_DOT_SIGN)) { + return false; + } + } + } + return true; + } + return false; + } + + function getTeleportTargetType(result, collisionResult) { + if (result.type === Picks.INTERSECTED_NONE) { + return TARGET.NONE; + } + + var props = Entities.getEntityProperties(result.objectID, ['userData', 'visible']); + var data = parseJSON(props.userData); + if (data !== undefined && data.seat !== undefined) { + var avatarUuid = Uuid.fromString(data.seat.user); + if (Uuid.isNull(avatarUuid) || !AvatarList.getAvatar(avatarUuid).sessionUUID) { + return TARGET.SEAT; + } else { + return TARGET.INVALID; + } + } + var isDiscrepancy = false; + if (collisionResult.collisionRegion != undefined) { + if (collisionResult.intersects) { + isDiscrepancy = checkForMeshDiscrepancy(result, collisionResult); + if (!isDiscrepancy) { + return TARGET.COLLIDES; + } + } + } + + var surfaceNormal = result.surfaceNormal; + var angle = Math.acos(Vec3.dot(surfaceNormal, Quat.getUp(MyAvatar.orientation))) * (180.0 / Math.PI); + + if (angle > MAX_ANGLE_FROM_UP_TO_TELEPORT) { + return TARGET.INVALID; + } else if (isDiscrepancy) { + return TARGET.DISCREPANCY; + } else { + return TARGET.SURFACE; + } + } + + function registerViveTeleportMapping() { + // Disable Vive teleport if touch is transitioning across touch-pad after pressing a direction button. + if (Controller.Hardware.Vive) { + var mappingName = 'Hifi-Teleporter-Dev-Vive-' + Math.random(); + var viveTeleportMapping = Controller.newMapping(mappingName); + viveTeleportMapping.from(Controller.Hardware.Vive.LSX).peek().to(leftTeleporter.axisButtonChangeX); + viveTeleportMapping.from(Controller.Hardware.Vive.LSY).peek().to(leftTeleporter.axisButtonChangeY); + viveTeleportMapping.from(Controller.Hardware.Vive.RSX).peek().to(rightTeleporter.axisButtonChangeX); + viveTeleportMapping.from(Controller.Hardware.Vive.RSY).peek().to(rightTeleporter.axisButtonChangeY); + Controller.enableMapping(mappingName); + isViveMapped = true; + } + } + + function onHardwareChanged() { + // Controller.Hardware.Vive is not immediately available at Interface start-up. + if (!isViveMapped && Controller.Hardware.Vive) { + registerViveTeleportMapping(); + } + } + + Controller.hardwareChanged.connect(onHardwareChanged); + + function registerMappings() { + mappingName = 'Hifi-Teleporter-Dev-' + Math.random(); + teleportMapping = Controller.newMapping(mappingName); + + // Vive teleport button lock-out. + registerViveTeleportMapping(); + + // Teleport actions. + teleportMapping.from(Controller.Standard.LeftPrimaryThumb).peek().to(leftTeleporter.buttonPress); + teleportMapping.from(Controller.Standard.RightPrimaryThumb).peek().to(rightTeleporter.buttonPress); + teleportMapping.from(Controller.Standard.LY).peek().to(leftTeleporter.getStandardLY); + teleportMapping.from(Controller.Standard.RY).peek().to(leftTeleporter.getStandardRY); + teleportMapping.from(Controller.Standard.LY).peek().to(rightTeleporter.getStandardLY); + teleportMapping.from(Controller.Standard.RY).peek().to(rightTeleporter.getStandardRY); + } + + var leftTeleporter = new Teleporter(LEFT_HAND); + var rightTeleporter = new Teleporter(RIGHT_HAND); + + enableDispatcherModule("LeftTeleporter", leftTeleporter); + enableDispatcherModule("RightTeleporter", rightTeleporter); + registerMappings(); + Controller.enableMapping(mappingName); + + function cleanup() { + Controller.hardwareChanged.disconnect(onHardwareChanged); + teleportMapping.disable(); + leftTeleporter.cleanup(); + rightTeleporter.cleanup(); + disableDispatcherModule("LeftTeleporter"); + disableDispatcherModule("RightTeleporter"); + } + Script.scriptEnding.connect(cleanup); + + var handleTeleportMessages = function(channel, message, sender) { + if (sender === MyAvatar.sessionUUID) { + if (channel === 'Hifi-Teleport-Disabler') { + if (message === 'both') { + leftTeleporter.disabled = true; + rightTeleporter.disabled = true; + } + if (message === 'left') { + leftTeleporter.disabled = true; + rightTeleporter.disabled = false; + } + if (message === 'right') { + leftTeleporter.disabled = false; + rightTeleporter.disabled = true; + } + if (message === 'none') { + leftTeleporter.disabled = false; + rightTeleporter.disabled = false; + } + } else if (channel === 'Hifi-Teleport-Ignore-Add' && + !Uuid.isNull(message) && + ignoredEntities.indexOf(message) === -1) { + ignoredEntities.push(message); + leftTeleporter.setIgnoreEntities(ignoredEntities); + rightTeleporter.setIgnoreEntities(ignoredEntities); + } else if (channel === 'Hifi-Teleport-Ignore-Remove' && !Uuid.isNull(message)) { + var removeIndex = ignoredEntities.indexOf(message); + if (removeIndex > -1) { + ignoredEntities.splice(removeIndex, 1); + leftTeleporter.setIgnoreEntities(ignoredEntities); + rightTeleporter.setIgnoreEntities(ignoredEntities); + } + } + } + }; + + MyAvatar.onLoadComplete.connect(function () { + Script.setTimeout(function () { + leftTeleporter.initPointers(); + rightTeleporter.initPointers(); + }, 500); + }); + + Messages.subscribe('Hifi-Teleport-Disabler'); + Messages.subscribe('Hifi-Teleport-Ignore-Add'); + Messages.subscribe('Hifi-Teleport-Ignore-Remove'); + Messages.messageReceived.connect(handleTeleportMessages); + + MyAvatar.sensorToWorldScaleChanged.connect(function () { + leftTeleporter.updatePlayAreaScale(); + rightTeleporter.updatePlayAreaScale(); + }); + +}()); // END LOCAL_SCOPE diff --git a/scripts/simplifiedUI/system/controllers/controllerModules/webSurfaceLaserInput.js b/scripts/simplifiedUI/system/controllers/controllerModules/webSurfaceLaserInput.js new file mode 100644 index 0000000000..cf700a8ad9 --- /dev/null +++ b/scripts/simplifiedUI/system/controllers/controllerModules/webSurfaceLaserInput.js @@ -0,0 +1,288 @@ +"use strict"; + +// webSurfaceLaserInput.js +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html + +/* global Script, Entities, enableDispatcherModule, disableDispatcherModule, makeRunningValues, + makeDispatcherModuleParameters, Overlays, HMD, TRIGGER_ON_VALUE, TRIGGER_OFF_VALUE, getEnabledModuleByName, + ContextOverlay, Picks, makeLaserParams, Settings, MyAvatar, RIGHT_HAND, LEFT_HAND, DISPATCHER_PROPERTIES +*/ + +Script.include("/~/system/libraries/controllerDispatcherUtils.js"); +Script.include("/~/system/libraries/controllers.js"); + +(function() { + const intersectionType = { + None: 0, + WebOverlay: 1, + WebEntity: 2, + HifiKeyboard: 3, + Overlay: 4, + HifiTablet: 5, + }; + + function WebSurfaceLaserInput(hand) { + this.hand = hand; + this.otherHand = this.hand === RIGHT_HAND ? LEFT_HAND : RIGHT_HAND; + this.running = false; + this.ignoredObjects = []; + this.intersectedType = intersectionType["None"]; + + this.parameters = makeDispatcherModuleParameters( + 160, + this.hand === RIGHT_HAND ? ["rightHand"] : ["leftHand"], + [], + 100, + makeLaserParams(hand, true)); + + this.getFarGrab = function () { + return getEnabledModuleByName(this.hand === RIGHT_HAND ? ("RightFarGrabEntity") : ("LeftFarGrabEntity")); + }; + + this.farGrabActive = function () { + var farGrab = this.getFarGrab(); + // farGrab will be null if module isn't loaded. + if (farGrab) { + return farGrab.targetIsNull(); + } else { + return false; + } + }; + + this.grabModuleWantsNearbyOverlay = function(controllerData) { + if (controllerData.triggerValues[this.hand] > TRIGGER_ON_VALUE || controllerData.secondaryValues[this.hand] > BUMPER_ON_VALUE) { + var nearGrabName = this.hand === RIGHT_HAND ? "RightNearParentingGrabOverlay" : "LeftNearParentingGrabOverlay"; + var nearGrabModule = getEnabledModuleByName(nearGrabName); + if (nearGrabModule) { + var candidateOverlays = controllerData.nearbyOverlayIDs[this.hand]; + var grabbableOverlays = candidateOverlays.filter(function(overlayID) { + return Overlays.getProperty(overlayID, "grabbable"); + }); + var target = nearGrabModule.getTargetID(grabbableOverlays, controllerData); + if (target) { + return true; + } + } + nearGrabName = this.hand === RIGHT_HAND ? "RightNearParentingGrabEntity" : "LeftNearParentingGrabEntity"; + nearGrabModule = getEnabledModuleByName(nearGrabName); + if (nearGrabModule && nearGrabModule.isReady(controllerData)) { + // check for if near parent module is active. + var isNearGrabModuleActive = nearGrabModule.isReady(controllerData).active; + if (isNearGrabModuleActive) { + // if true, return true. + return isNearGrabModuleActive; + } else { + // check near action grab entity as a second pass. + nearGrabName = this.hand === RIGHT_HAND ? "RightNearActionGrabEntity" : "LeftNearActionGrabEntity"; + nearGrabModule = getEnabledModuleByName(nearGrabName); + if (nearGrabModule && nearGrabModule.isReady(controllerData)) { + return nearGrabModule.isReady(controllerData).active; + } + } + } + } + + var nearTabletHighlightModule = getEnabledModuleByName(this.hand === RIGHT_HAND + ? "RightNearTabletHighlight" : "LeftNearTabletHighlight"); + if (nearTabletHighlightModule) { + return nearTabletHighlightModule.isNearTablet(controllerData); + } + + return false; + }; + + this.getOtherModule = function() { + return this.hand === RIGHT_HAND ? leftOverlayLaserInput : rightOverlayLaserInput; + }; + + this.addObjectToIgnoreList = function(controllerData) { + if (Window.interstitialModeEnabled && !Window.isPhysicsEnabled()) { + var intersection = controllerData.rayPicks[this.hand]; + var objectID = intersection.objectID; + + if (intersection.type === Picks.INTERSECTED_OVERLAY) { + var overlayIndex = this.ignoredObjects.indexOf(objectID); + + var overlayName = Overlays.getProperty(objectID, "name"); + if (overlayName !== "Loading-Destination-Card-Text" && overlayName !== "Loading-Destination-Card-GoTo-Image" && + overlayName !== "Loading-Destination-Card-GoTo-Image-Hover") { + var data = { + action: 'add', + id: objectID + }; + Messages.sendMessage('Hifi-Hand-RayPick-Blacklist', JSON.stringify(data)); + this.ignoredObjects.push(objectID); + } + } else if (intersection.type === Picks.INTERSECTED_ENTITY) { + var entityIndex = this.ignoredObjects.indexOf(objectID); + var data = { + action: 'add', + id: objectID + }; + Messages.sendMessage('Hifi-Hand-RayPick-Blacklist', JSON.stringify(data)); + this.ignoredObjects.push(objectID); + } + } + }; + + this.restoreIgnoredObjects = function() { + for (var index = 0; index < this.ignoredObjects.length; index++) { + var data = { + action: 'remove', + id: this.ignoredObjects[index] + }; + Messages.sendMessage('Hifi-Hand-RayPick-Blacklist', JSON.stringify(data)); + } + + this.ignoredObjects = []; + }; + + this.getInteractableType = function(controllerData, triggerPressed, checkEntitiesOnly) { + // allow pointing at tablet, unlocked web entities, or web overlays automatically without pressing trigger, + // but for pointing at locked web entities or non-web overlays user must be pressing trigger + var intersection = controllerData.rayPicks[this.hand]; + var objectID = intersection.objectID; + if (intersection.type === Picks.INTERSECTED_OVERLAY && !checkEntitiesOnly) { + if ((HMD.tabletID && objectID === HMD.tabletID) || + (HMD.tabletScreenID && objectID === HMD.tabletScreenID) || + (HMD.homeButtonID && objectID === HMD.homeButtonID)) { + return intersectionType["HifiTablet"]; + } else { + var overlayType = Overlays.getOverlayType(objectID); + var type = intersectionType["None"]; + if (Keyboard.containsID(objectID) && !Keyboard.preferMalletsOverLasers) { + type = intersectionType["HifiKeyboard"]; + } else if (overlayType === "web3d") { + type = intersectionType["WebOverlay"]; + } else if (triggerPressed) { + type = intersectionType["Overlay"]; + } + + return type; + } + } else if (intersection.type === Picks.INTERSECTED_ENTITY) { + var entityProperties = Entities.getEntityProperties(objectID, DISPATCHER_PROPERTIES); + var entityType = entityProperties.type; + var isLocked = entityProperties.locked; + if (entityType === "Web" && (!isLocked || triggerPressed)) { + return intersectionType["WebEntity"]; + } + } + return intersectionType["None"]; + }; + + this.deleteContextOverlay = function() { + var farGrabModule = getEnabledModuleByName(this.hand === RIGHT_HAND ? + "RightFarActionGrabEntity" : + "LeftFarActionGrabEntity"); + if (farGrabModule) { + var entityWithContextOverlay = farGrabModule.entityWithContextOverlay; + + if (entityWithContextOverlay) { + ContextOverlay.destroyContextOverlay(entityWithContextOverlay); + farGrabModule.entityWithContextOverlay = false; + } + } + }; + + this.updateAlwaysOn = function(type) { + var PREFER_STYLUS_OVER_LASER = "preferStylusOverLaser"; + this.parameters.handLaser.alwaysOn = (!Settings.getValue(PREFER_STYLUS_OVER_LASER, false) || type === intersectionType["HifiKeyboard"]); + }; + + this.getDominantHand = function() { + return MyAvatar.getDominantHand() === "right" ? 1 : 0; + }; + + this.dominantHandOverride = false; + + this.isReady = function (controllerData) { + // Trivial rejection for when FarGrab is active. + if (this.farGrabActive()) { + return makeRunningValues(false, [], []); + } + + var isTriggerPressed = controllerData.triggerValues[this.hand] > TRIGGER_OFF_VALUE && + controllerData.triggerValues[this.otherHand] <= TRIGGER_OFF_VALUE; + var type = this.getInteractableType(controllerData, isTriggerPressed, false); + + if (type !== intersectionType["None"] && !this.grabModuleWantsNearbyOverlay(controllerData)) { + if (type === intersectionType["WebOverlay"] || type === intersectionType["WebEntity"] || type === intersectionType["HifiTablet"]) { + var otherModuleRunning = this.getOtherModule().running; + otherModuleRunning = otherModuleRunning && this.getDominantHand() !== this.hand; // Auto-swap to dominant hand. + var allowThisModule = !otherModuleRunning || isTriggerPressed; + + if (!allowThisModule) { + return makeRunningValues(true, [], []); + } + + if (isTriggerPressed) { + this.dominantHandOverride = true; // Override dominant hand. + this.getOtherModule().dominantHandOverride = false; + } + } + + this.updateAlwaysOn(type); + if (this.parameters.handLaser.alwaysOn || isTriggerPressed) { + return makeRunningValues(true, [], []); + } + } + + if (Window.interstitialModeEnabled && Window.isPhysicsEnabled()) { + this.restoreIgnoredObjects(); + } + return makeRunningValues(false, [], []); + }; + + this.shouldThisModuleRun = function(controllerData) { + var otherModuleRunning = this.getOtherModule().running; + otherModuleRunning = otherModuleRunning && this.getDominantHand() !== this.hand; // Auto-swap to dominant hand. + otherModuleRunning = otherModuleRunning || this.getOtherModule().dominantHandOverride; // Override dominant hand. + var grabModuleNeedsToRun = this.grabModuleWantsNearbyOverlay(controllerData); + // only allow for non-near grab + return !otherModuleRunning && !grabModuleNeedsToRun; + }; + + this.run = function(controllerData, deltaTime) { + this.addObjectToIgnoreList(controllerData); + var isTriggerPressed = controllerData.triggerValues[this.hand] > TRIGGER_OFF_VALUE; + var type = this.getInteractableType(controllerData, isTriggerPressed, false); + var laserOn = isTriggerPressed || this.parameters.handLaser.alwaysOn; + this.addObjectToIgnoreList(controllerData); + + if (type === intersectionType["HifiTablet"] && laserOn) { + if (this.shouldThisModuleRun(controllerData)) { + this.running = true; + return makeRunningValues(true, [], []); + } + } else if ((type === intersectionType["WebOverlay"] || type === intersectionType["WebEntity"]) && laserOn) { // auto laser on WebEntities andWebOverlays + if (this.shouldThisModuleRun(controllerData)) { + this.running = true; + return makeRunningValues(true, [], []); + } + } else if ((type === intersectionType["HifiKeyboard"] && laserOn) || type === intersectionType["Overlay"]) { + this.running = true; + return makeRunningValues(true, [], []); + } + + this.deleteContextOverlay(); + this.running = false; + this.dominantHandOverride = false; + return makeRunningValues(false, [], []); + }; + } + + var leftOverlayLaserInput = new WebSurfaceLaserInput(LEFT_HAND); + var rightOverlayLaserInput = new WebSurfaceLaserInput(RIGHT_HAND); + + enableDispatcherModule("LeftWebSurfaceLaserInput", leftOverlayLaserInput); + enableDispatcherModule("RightWebSurfaceLaserInput", rightOverlayLaserInput); + + function cleanup() { + disableDispatcherModule("LeftWebSurfaceLaserInput"); + disableDispatcherModule("RightWebSurfaceLaserInput"); + } + Script.scriptEnding.connect(cleanup); +}()); diff --git a/scripts/simplifiedUI/system/controllers/controllerScripts.js b/scripts/simplifiedUI/system/controllers/controllerScripts.js new file mode 100644 index 0000000000..c9cb61b5f5 --- /dev/null +++ b/scripts/simplifiedUI/system/controllers/controllerScripts.js @@ -0,0 +1,62 @@ +"use strict"; + +// controllerScripts.js +// +// Created by David Rowe on 15 Mar 2017. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +/* global Script, Menu */ + +var CONTOLLER_SCRIPTS = [ + "squeezeHands.js", + "controllerDisplayManager.js", + "grab.js", + //"toggleAdvancedMovementForHandControllers.js", + "handTouch.js", + "controllerDispatcher.js", + "controllerModules/nearParentGrabOverlay.js", + "controllerModules/stylusInput.js", + "controllerModules/equipEntity.js", + "controllerModules/nearTrigger.js", + "controllerModules/webSurfaceLaserInput.js", + "controllerModules/inEditMode.js", + "controllerModules/inVREditMode.js", + "controllerModules/disableOtherModule.js", + "controllerModules/farTrigger.js", + "controllerModules/teleport.js", + "controllerModules/hudOverlayPointer.js", + "controllerModules/mouseHMD.js", + "controllerModules/nearGrabHyperLinkEntity.js", + "controllerModules/nearTabletHighlight.js", + "controllerModules/nearGrabEntity.js", + "controllerModules/farGrabEntity.js", + "controllerModules/pushToTalk.js" +]; + +var DEBUG_MENU_ITEM = "Debug defaultScripts.js"; + +function runDefaultsTogether() { + for (var j in CONTOLLER_SCRIPTS) { + if (CONTOLLER_SCRIPTS.hasOwnProperty(j)) { + Script.include(CONTOLLER_SCRIPTS[j]); + } + } +} + +function runDefaultsSeparately() { + for (var i in CONTOLLER_SCRIPTS) { + if (CONTOLLER_SCRIPTS.hasOwnProperty(i)) { + Script.load(CONTOLLER_SCRIPTS[i]); + } + } +} + +if (Menu.isOptionChecked(DEBUG_MENU_ITEM)) { + runDefaultsSeparately(); +} else { + runDefaultsTogether(); +} diff --git a/scripts/simplifiedUI/system/controllers/godView.js b/scripts/simplifiedUI/system/controllers/godView.js new file mode 100644 index 0000000000..4b406399fd --- /dev/null +++ b/scripts/simplifiedUI/system/controllers/godView.js @@ -0,0 +1,116 @@ +"use strict"; +// +// godView.js +// scripts/system/ +// +// Created by Brad Hefta-Gaub on 1 Jun 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 +// +/* globals HMD, Script, Menu, Tablet, Camera */ +/* eslint indent: ["error", 4, { "outerIIFEBody": 0 }] */ + +(function() { // BEGIN LOCAL_SCOPE + +var godView = false; + +var GOD_CAMERA_OFFSET = -1; // 1 meter below the avatar +var GOD_VIEW_HEIGHT = 300; // 300 meter above the ground +var ABOVE_GROUND_DROP = 2; +var MOVE_BY = 1; + +function moveTo(position) { + if (godView) { + MyAvatar.position = position; + Camera.position = Vec3.sum(MyAvatar.position, {x:0, y: GOD_CAMERA_OFFSET, z: 0}); + } else { + MyAvatar.position = position; + } +} + +function keyPressEvent(event) { + if (godView) { + switch(event.text) { + case "UP": + moveTo(Vec3.sum(MyAvatar.position, {x:0.0, y: 0, z: -1 * MOVE_BY})); + break; + case "DOWN": + moveTo(Vec3.sum(MyAvatar.position, {x:0, y: 0, z: MOVE_BY})); + break; + case "LEFT": + moveTo(Vec3.sum(MyAvatar.position, {x:-1 * MOVE_BY, y: 0, z: 0})); + break; + case "RIGHT": + moveTo(Vec3.sum(MyAvatar.position, {x:MOVE_BY, y: 0, z: 0})); + break; + } + } +} + +function mousePress(event) { + if (godView) { + var pickRay = Camera.computePickRay(event.x, event.y); + var pointingAt = Vec3.sum(pickRay.origin, Vec3.multiply(pickRay.direction,300)); + var moveToPosition = { x: pointingAt.x, y: MyAvatar.position.y, z: pointingAt.z }; + moveTo(moveToPosition); + } +} + + +var oldCameraMode = Camera.mode; + +function startGodView() { + if (!godView) { + oldCameraMode = Camera.mode; + MyAvatar.position = Vec3.sum(MyAvatar.position, {x:0, y: GOD_VIEW_HEIGHT, z: 0}); + Camera.mode = "independent"; + Camera.position = Vec3.sum(MyAvatar.position, {x:0, y: GOD_CAMERA_OFFSET, z: 0}); + Camera.orientation = Quat.fromPitchYawRollDegrees(-90,0,0); + godView = true; + } +} + +function endGodView() { + if (godView) { + Camera.mode = oldCameraMode; + MyAvatar.position = Vec3.sum(MyAvatar.position, {x:0, y: (-1 * GOD_VIEW_HEIGHT) + ABOVE_GROUND_DROP, z: 0}); + godView = false; + } +} + +var button; +var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); + +function onClicked() { + if (godView) { + endGodView(); + } else { + startGodView(); + } +} + +button = tablet.addButton({ + icon: "icons/tablet-icons/switch-desk-i.svg", // FIXME - consider a better icon from Alan + text: "God View" +}); + +button.clicked.connect(onClicked); +Controller.keyPressEvent.connect(keyPressEvent); +Controller.mousePressEvent.connect(mousePress); + + +Script.scriptEnding.connect(function () { + if (godView) { + endGodView(); + } + button.clicked.disconnect(onClicked); + if (tablet) { + tablet.removeButton(button); + } + Controller.keyPressEvent.disconnect(keyPressEvent); + Controller.mousePressEvent.disconnect(mousePress); +}); + +}()); // END LOCAL_SCOPE diff --git a/scripts/simplifiedUI/system/controllers/grab.js b/scripts/simplifiedUI/system/controllers/grab.js new file mode 100644 index 0000000000..1fb82d3843 --- /dev/null +++ b/scripts/simplifiedUI/system/controllers/grab.js @@ -0,0 +1,522 @@ +"use strict"; + +// grab.js +// examples +// +// Created by Eric Levin on May 1, 2015 +// Copyright 2015 High Fidelity, Inc. +// +// Grab's physically moveable entities with the mouse, by applying a spring force. +// +// Updated November 22, 2016 by Philip Rosedale: Add distance attenuation of grab effect +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +/* global MyAvatar, Entities, Script, HMD, Camera, Vec3, Reticle, Overlays, Messages, Quat, Controller, + isInEditMode, entityIsGrabbable, Picks, PickType, Pointers, unhighlightTargetEntity, DISPATCHER_PROPERTIES, + entityIsGrabbable, getMainTabletIDs +*/ +/* jslint bitwise: true */ + +(function() { // BEGIN LOCAL_SCOPE + +Script.include("/~/system/libraries/utils.js"); +Script.include("/~/system/libraries/controllerDispatcherUtils.js"); + +var MOUSE_GRAB_JOINT = 65526; // FARGRAB_MOUSE_INDEX + +var MAX_SOLID_ANGLE = 0.01; // objects that appear smaller than this can't be grabbed + +var DELAY_FOR_30HZ = 33; // milliseconds + +var ZERO_VEC3 = { x: 0, y: 0, z: 0 }; +var IDENTITY_QUAT = { x: 0, y: 0, z: 0, w: 1 }; + +// helper function +function mouseIntersectionWithPlane(pointOnPlane, planeNormal, event, maxDistance) { + var cameraPosition = Camera.getPosition(); + var localPointOnPlane = Vec3.subtract(pointOnPlane, cameraPosition); + var distanceFromPlane = Vec3.dot(localPointOnPlane, planeNormal); + var MIN_DISTANCE_FROM_PLANE = 0.001; + if (Math.abs(distanceFromPlane) < MIN_DISTANCE_FROM_PLANE) { + // camera is touching the plane + return pointOnPlane; + } + var pickRay = Camera.computePickRay(event.x, event.y); + var dirDotNorm = Vec3.dot(pickRay.direction, planeNormal); + var MIN_RAY_PLANE_DOT = 0.00001; + + var localIntersection; + var useMaxForwardGrab = false; + if (Math.abs(dirDotNorm) > MIN_RAY_PLANE_DOT) { + var distanceToIntersection = distanceFromPlane / dirDotNorm; + if (distanceToIntersection > 0 && distanceToIntersection < maxDistance) { + // ray points into the plane + localIntersection = Vec3.multiply(pickRay.direction, distanceFromPlane / dirDotNorm); + } else { + // ray intersects BEHIND the camera or else very far away + // so we clamp the grab point to be the maximum forward position + useMaxForwardGrab = true; + } + } else { + // ray points perpendicular to grab plane + // so we map the grab point to the maximum forward position + useMaxForwardGrab = true; + } + if (useMaxForwardGrab) { + // we re-route the intersection to be in front at max distance. + var rayDirection = Vec3.subtract(pickRay.direction, Vec3.multiply(planeNormal, dirDotNorm)); + rayDirection = Vec3.normalize(rayDirection); + localIntersection = Vec3.multiply(rayDirection, maxDistance); + localIntersection = Vec3.sum(localIntersection, Vec3.multiply(planeNormal, distanceFromPlane)); + } + var worldIntersection = Vec3.sum(cameraPosition, localIntersection); + return worldIntersection; +} + +// Mouse class stores mouse click and drag info +function Mouse() { + this.current = { + x: 0, + y: 0 + }; + this.previous = { + x: 0, + y: 0 + }; + this.rotateStart = { + x: 0, + y: 0 + }; + this.cursorRestore = { + x: 0, + y: 0 + }; +} + +Mouse.prototype.startDrag = function(position) { + this.current = { + x: position.x, + y: position.y + }; + this.startRotateDrag(); +}; + +Mouse.prototype.updateDrag = function(position) { + this.current = { + x: position.x, + y: position.y + }; +}; + +Mouse.prototype.startRotateDrag = function() { + this.previous = { + x: this.current.x, + y: this.current.y + }; + this.rotateStart = { + x: this.current.x, + y: this.current.y + }; + this.cursorRestore = Reticle.getPosition(); +}; + +Mouse.prototype.getDrag = function() { + var delta = { + x: this.current.x - this.previous.x, + y: this.current.y - this.previous.y + }; + this.previous = { + x: this.current.x, + y: this.current.y + }; + return delta; +}; + +Mouse.prototype.restoreRotateCursor = function() { + Reticle.setPosition(this.cursorRestore); + this.current = { + x: this.rotateStart.x, + y: this.rotateStart.y + }; +}; + +var mouse = new Mouse(); + +var beacon = { + type: "cube", + dimensions: { + x: 0.01, + y: 0, + z: 0.01 + }, + color: { + red: 200, + green: 200, + blue: 200 + }, + alpha: 1, + solid: true, + ignoreRayIntersection: true, + visible: true +}; + +// TODO: play sounds again when we aren't leaking AudioInjector threads +// var grabSound = SoundCache.getSound("https://hifi-public.s3.amazonaws.com/eric/sounds/CloseClamp.wav"); +// var releaseSound = SoundCache.getSound("https://hifi-public.s3.amazonaws.com/eric/sounds/ReleaseClamp.wav"); +// var VOLUME = 0.0; + + +// Grabber class stores and computes info for grab behavior +function Grabber() { + this.isGrabbing = false; + this.entityID = null; + this.startPosition = ZERO_VEC3; + this.lastRotation = IDENTITY_QUAT; + this.currentPosition = ZERO_VEC3; + this.planeNormal = ZERO_VEC3; + + // maxDistance is a function of the size of the object. + this.maxDistance = 0; + + // mode defines the degrees of freedom of the grab target positions + // relative to startPosition options include: + // xzPlane (default) + // verticalCylinder (SHIFT) + // rotate (CONTROL) + this.mode = "xzplane"; + + // offset allows the user to grab an object off-center. It points from the object's center + // to the point where the ray intersects the grab plane (at the moment the grab is initiated). + // Future target positions of the ray intersection are on the same plane, and the offset is subtracted + // to compute the target position of the object's center. + this.offset = { + x: 0, + y: 0, + z: 0 + }; + + this.liftKey = false; // SHIFT + this.rotateKey = false; // CONTROL + + this.mouseRayOverlays = Picks.createPick(PickType.Ray, { + joint: "Mouse", + filter: Picks.PICK_OVERLAYS | Picks.PICK_INCLUDE_NONCOLLIDABLE, + enabled: true + }); + var tabletItems = getMainTabletIDs(); + if (tabletItems.length > 0) { + Picks.setIncludeItems(this.mouseRayOverlays, tabletItems); + } + var renderStates = [{name: "grabbed", end: beacon}]; + this.mouseRayEntities = Pointers.createPointer(PickType.Ray, { + joint: "Mouse", + filter: Picks.PICK_ENTITIES | Picks.PICK_INCLUDE_NONCOLLIDABLE, + faceAvatar: true, + scaleWithParent: true, + enabled: true, + renderStates: renderStates + }); +} + +Grabber.prototype.computeNewGrabPlane = function() { + if (!this.isGrabbing) { + return; + } + + var modeWasRotate = (this.mode == "rotate"); + this.mode = "xzPlane"; + this.planeNormal = { + x: 0, + y: 1, + z: 0 + }; + if (this.rotateKey) { + this.mode = "rotate"; + mouse.startRotateDrag(); + } else { + if (modeWasRotate) { + // we reset the mouse screen position whenever we stop rotating + mouse.restoreRotateCursor(); + } + if (this.liftKey) { + this.mode = "verticalCylinder"; + // NOTE: during verticalCylinder mode a new planeNormal will be computed each move + } + } + + this.pointOnPlane = Vec3.subtract(this.currentPosition, this.offset); + var xzOffset = Vec3.subtract(this.pointOnPlane, Camera.getPosition()); + xzOffset.y = 0; + this.xzDistanceToGrab = Vec3.length(xzOffset); +}; + +Grabber.prototype.pressEvent = function(event) { + if (isInEditMode() || HMD.active) { + return; + } + if (event.button !== "LEFT") { + return; + } + if (event.isAlt || event.isMeta) { + return; + } + if (Overlays.getOverlayAtPoint(Reticle.position) > 0) { + // the mouse is pointing at an overlay; don't look for entities underneath the overlay. + return; + } + + var overlayResult = Picks.getPrevPickResult(this.mouseRayOverlays); + if (overlayResult.type != Picks.INTERSECTED_NONE) { + return; + } + + var pickResults = Pointers.getPrevPickResult(this.mouseRayEntities); + if (pickResults.type == Picks.INTERSECTED_NONE) { + Pointers.setRenderState(this.mouseRayEntities, ""); + return; + } + + var props = Entities.getEntityProperties(pickResults.objectID, DISPATCHER_PROPERTIES); + if (!entityIsGrabbable(props)) { + // only grab grabbable objects + return; + } + if (props.grab.equippable) { + // don't mouse-grab click-to-equip entities (let equipEntity.js handle these) + return; + } + + Pointers.setRenderState(this.mouseRayEntities, "grabbed"); + Pointers.setLockEndUUID(this.mouseRayEntities, pickResults.objectID, false); + unhighlightTargetEntity(pickResults.objectID); + + mouse.startDrag(event); + + var clickedEntity = pickResults.objectID; + var entityProperties = Entities.getEntityProperties(clickedEntity, DISPATCHER_PROPERTIES); + this.startPosition = entityProperties.position; + this.lastRotation = entityProperties.rotation; + var cameraPosition = Camera.getPosition(); + + var objectBoundingDiameter = Vec3.length(entityProperties.dimensions); + beacon.dimensions.y = objectBoundingDiameter; + Pointers.editRenderState(this.mouseRayEntities, "grabbed", {end: beacon}); + this.maxDistance = objectBoundingDiameter / MAX_SOLID_ANGLE; + if (Vec3.distance(this.startPosition, cameraPosition) > this.maxDistance) { + // don't allow grabs of things far away + return; + } + + this.isGrabbing = true; + + this.entityID = clickedEntity; + this.currentPosition = entityProperties.position; + + // compute the grab point + var pickRay = Camera.computePickRay(event.x, event.y); + var nearestPoint = Vec3.subtract(this.startPosition, cameraPosition); + var distanceToGrab = Vec3.dot(nearestPoint, pickRay.direction); + nearestPoint = Vec3.multiply(distanceToGrab, pickRay.direction); + this.pointOnPlane = Vec3.sum(cameraPosition, nearestPoint); + + // compute the grab offset (points from point of grab to object center) + this.offset = Vec3.subtract(this.startPosition, this.pointOnPlane); // offset in world-space + MyAvatar.setJointTranslation(MOUSE_GRAB_JOINT, MyAvatar.worldToJointPoint(this.startPosition)); + MyAvatar.setJointRotation(MOUSE_GRAB_JOINT, MyAvatar.worldToJointRotation(this.lastRotation)); + + this.computeNewGrabPlane(); + this.moveEvent(event); + + var args = "mouse"; + Entities.callEntityMethod(this.entityID, "startDistanceGrab", args); + + Messages.sendLocalMessage('Hifi-Object-Manipulation', JSON.stringify({ + action: 'grab', + grabbedEntity: this.entityID + })); + + if (this.grabID) { + MyAvatar.releaseGrab(this.grabID); + this.grabID = null; + } + this.grabID = MyAvatar.grab(this.entityID, MOUSE_GRAB_JOINT, ZERO_VEC3, IDENTITY_QUAT); + + // TODO: play sounds again when we aren't leaking AudioInjector threads + //Audio.playSound(grabSound, { position: entityProperties.position, volume: VOLUME }); +}; + +Grabber.prototype.releaseEvent = function(event) { + if (event.button !== "LEFT" && !HMD.active) { + return; + } + + if (this.moveEventTimer) { + Script.clearTimeout(this.moveEventTimer); + this.moveEventTimer = null; + } + + if (this.isGrabbing) { + this.isGrabbing = false; + + Pointers.setRenderState(this.mouseRayEntities, ""); + Pointers.setLockEndUUID(this.mouseRayEntities, null, false); + + var args = "mouse"; + Entities.callEntityMethod(this.entityID, "releaseGrab", args); + + Messages.sendLocalMessage('Hifi-Object-Manipulation', JSON.stringify({ + action: 'release', + grabbedEntity: this.entityID, + joint: "mouse" + })); + + if (this.grabID) { + MyAvatar.releaseGrab(this.grabID); + this.grabID = null; + } + + MyAvatar.clearJointData(MOUSE_GRAB_JOINT); + + // TODO: play sounds again when we aren't leaking AudioInjector threads + //Audio.playSound(releaseSound, { position: entityProperties.position, volume: VOLUME }); + } +}; + +Grabber.prototype.scheduleMouseMoveProcessor = function(event) { + var _this = this; + if (!this.moveEventTimer) { + this.moveEventTimer = Script.setTimeout(function() { + _this.moveEventProcess(); + }, DELAY_FOR_30HZ); + } +}; + +Grabber.prototype.moveEvent = function(event) { + // during the handling of the event, do as little as possible. We save the updated mouse position, + // and start a timer to react to the change. If more changes arrive before the timer fires, only + // the last update will be considered. This is done to avoid backing-up Qt's event queue. + if (!this.isGrabbing || HMD.active) { + return; + } + mouse.updateDrag(event); + this.scheduleMouseMoveProcessor(); +}; + +Grabber.prototype.moveEventProcess = function() { + this.moveEventTimer = null; + var entityProperties = Entities.getEntityProperties(this.entityID, DISPATCHER_PROPERTIES); + if (!entityProperties || HMD.active) { + return; + } + + this.currentPosition = entityProperties.position; + + if (this.mode === "rotate") { + var drag = mouse.getDrag(); + var orientation = Camera.getOrientation(); + var dragOffset = Vec3.multiply(drag.x, Quat.getRight(orientation)); + dragOffset = Vec3.sum(dragOffset, Vec3.multiply(-drag.y, Quat.getUp(orientation))); + var axis = Vec3.cross(dragOffset, Quat.getForward(orientation)); + axis = Vec3.normalize(axis); + var ROTATE_STRENGTH = 0.4; // magic number tuned by hand + var angle = ROTATE_STRENGTH * Math.sqrt((drag.x * drag.x) + (drag.y * drag.y)); + var deltaQ = Quat.angleAxis(angle, axis); + + this.lastRotation = Quat.multiply(deltaQ, this.lastRotation); + MyAvatar.setJointRotation(MOUSE_GRAB_JOINT, MyAvatar.worldToJointRotation(this.lastRotation)); + + } else { + var newPointOnPlane; + + if (this.mode === "verticalCylinder") { + // for this mode we recompute the plane based on current Camera + var planeNormal = Quat.getForward(Camera.getOrientation()); + planeNormal.y = 0; + planeNormal = Vec3.normalize(planeNormal); + var pointOnCylinder = Vec3.multiply(planeNormal, this.xzDistanceToGrab); + pointOnCylinder = Vec3.sum(Camera.getPosition(), pointOnCylinder); + newPointOnPlane = mouseIntersectionWithPlane(pointOnCylinder, planeNormal, mouse.current, this.maxDistance); + } else { + var cameraPosition = Camera.getPosition(); + newPointOnPlane = mouseIntersectionWithPlane(this.pointOnPlane, this.planeNormal, mouse.current, this.maxDistance); + var relativePosition = Vec3.subtract(newPointOnPlane, cameraPosition); + var distance = Vec3.length(relativePosition); + if (distance > this.maxDistance) { + // clamp distance + relativePosition = Vec3.multiply(relativePosition, this.maxDistance / distance); + newPointOnPlane = Vec3.sum(relativePosition, cameraPosition); + } + } + + MyAvatar.setJointTranslation(MOUSE_GRAB_JOINT, MyAvatar.worldToJointPoint(Vec3.sum(newPointOnPlane, this.offset))); + } + + this.scheduleMouseMoveProcessor(); +}; + +Grabber.prototype.keyReleaseEvent = function(event) { + if (event.text === "SHIFT") { + this.liftKey = false; + } + if (event.text === "CONTROL") { + this.rotateKey = false; + } + this.computeNewGrabPlane(); +}; + +Grabber.prototype.keyPressEvent = function(event) { + if (event.text === "SHIFT") { + this.liftKey = true; + } + if (event.text === "CONTROL") { + this.rotateKey = true; + } + this.computeNewGrabPlane(); +}; + +Grabber.prototype.cleanup = function() { + Pointers.removePointer(this.mouseRayEntities); + Picks.removePick(this.mouseRayOverlays); + if (this.grabID) { + MyAvatar.releaseGrab(this.grabID); + this.grabID = null; + } +}; + +var grabber = new Grabber(); + +function pressEvent(event) { + grabber.pressEvent(event); +} + +function moveEvent(event) { + grabber.moveEvent(event); +} + +function releaseEvent(event) { + grabber.releaseEvent(event); +} + +function keyPressEvent(event) { + grabber.keyPressEvent(event); +} + +function keyReleaseEvent(event) { + grabber.keyReleaseEvent(event); +} + +function cleanup() { + grabber.cleanup(); +} + +Controller.mousePressEvent.connect(pressEvent); +Controller.mouseMoveEvent.connect(moveEvent); +Controller.mouseReleaseEvent.connect(releaseEvent); +Controller.keyPressEvent.connect(keyPressEvent); +Controller.keyReleaseEvent.connect(keyReleaseEvent); +Script.scriptEnding.connect(cleanup); + +}()); // END LOCAL_SCOPE diff --git a/scripts/simplifiedUI/system/controllers/handTouch.js b/scripts/simplifiedUI/system/controllers/handTouch.js new file mode 100644 index 0000000000..c706d054c1 --- /dev/null +++ b/scripts/simplifiedUI/system/controllers/handTouch.js @@ -0,0 +1,958 @@ +// +// scripts/system/libraries/handTouch.js +// +// Created by Luis Cuenca on 12/29/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 +// + +/* jslint bitwise: true */ + +/* global Script, Overlays, Controller, Vec3, MyAvatar, Entities, RayPick +*/ + +(function () { + + var LEAP_MOTION_NAME = "LeapMotion"; + var handTouchEnabled = true; + var leapMotionEnabled = Controller.getRunningInputDeviceNames().indexOf(LEAP_MOTION_NAME) >= 0; + var MSECONDS_AFTER_LOAD = 2000; + var updateFingerWithIndex = 0; + var untouchableEntities = []; + + // Keys to access finger data + var fingerKeys = ["pinky", "ring", "middle", "index", "thumb"]; + + // Additionally close the hands to achieve a grabbing effect + var grabPercent = { left: 0, right: 0 }; + + var Palm = function() { + this.position = {x: 0, y: 0, z: 0}; + this.perpendicular = {x: 0, y: 0, z: 0}; + this.distance = 0; + this.fingers = { + pinky: {x: 0, y: 0, z: 0}, + middle: {x: 0, y: 0, z: 0}, + ring: {x: 0, y: 0, z: 0}, + thumb: {x: 0, y: 0, z: 0}, + index: {x: 0, y: 0, z: 0} + }; + this.set = false; + }; + + var palmData = { + left: new Palm(), + right: new Palm() + }; + + var handJointNames = {left: "LeftHand", right: "RightHand"}; + + // Store which fingers are touching - if all false restate the default poses + var isTouching = { + left: { + pinky: false, + middle: false, + ring: false, + thumb: false, + index: false + }, right: { + pinky: false, + middle: false, + ring: false, + thumb: false, + index: false + } + }; + + // frame count for transition to default pose + + var countToDefault = { + left: 0, + right: 0 + }; + + // joint data for open pose + var dataOpen = { + left: { + pinky: [ + {x: -0.0066, y: -0.0224, z: -0.2174, w: 0.9758}, + {x: 0.0112, y: 0.0001, z: 0.0093, w: 0.9999}, + {x: -0.0346, y: 0.0003, z: -0.0073, w: 0.9994} + ], + ring: [ + {x: -0.0029, y: -0.0094, z: -0.1413, w: 0.9899}, + {x: 0.0112, y: 0.0001, z: 0.0059, w: 0.9999}, + {x: -0.0346, y: 0.0002, z: -0.006, w: 0.9994} + ], + middle: [ + {x: -0.0016, y: 0, z: -0.0286, w: 0.9996}, + {x: 0.0112, y: -0.0001, z: -0.0063, w: 0.9999}, + {x: -0.0346, y: -0.0003, z: 0.0073, w: 0.9994} + ], + index: [ + {x: -0.0016, y: 0.0001, z: 0.0199, w: 0.9998}, + {x: 0.0112, y: 0, z: 0.0081, w: 0.9999}, + {x: -0.0346, y: 0.0008, z: -0.023, w: 0.9991} + ], + thumb: [ + {x: 0.0354, y: 0.0363, z: 0.3275, w: 0.9435}, + {x: -0.0945, y: 0.0938, z: 0.0995, w: 0.9861}, + {x: -0.0952, y: 0.0718, z: 0.1382, w: 0.9832} + ] + }, right: { + pinky: [ + {x: -0.0034, y: 0.023, z: 0.1051, w: 0.9942}, + {x: 0.0106, y: -0.0001, z: -0.0091, w: 0.9999}, + {x: -0.0346, y: -0.0003, z: 0.0075, w: 0.9994} + ], + ring: [ + {x: -0.0013, y: 0.0097, z: 0.0311, w: 0.9995}, + {x: 0.0106, y: -0.0001, z: -0.0056, w: 0.9999}, + {x: -0.0346, y: -0.0002, z: 0.0061, w: 0.9994} + ], + middle: [ + {x: -0.001, y: 0, z: 0.0285, w: 0.9996}, + {x: 0.0106, y: 0.0001, z: 0.0062, w: 0.9999}, + {x: -0.0346, y: 0.0003, z: -0.0074, w: 0.9994} + ], + index: [ + {x: -0.001, y: 0, z: -0.0199, w: 0.9998}, + {x: 0.0106, y: -0.0001, z: -0.0079, w: 0.9999}, + {x: -0.0346, y: -0.0008, z: 0.0229, w: 0.9991} + ], + thumb: [ + {x: 0.0355, y: -0.0363, z: -0.3263, w: 0.9439}, + {x: -0.0946, y: -0.0938, z: -0.0996, w: 0.9861}, + {x: -0.0952, y: -0.0719, z: -0.1376, w: 0.9833} + ] + } + }; + + // joint data for close pose + var dataClose = { + left: { + pinky: [ + {x: 0.5878, y: -0.1735, z: -0.1123, w: 0.7821}, + {x: 0.5704, y: 0.0053, z: 0.0076, w: 0.8213}, + {x: 0.6069, y: -0.0044, z: -0.0058, w: 0.7947} + ], + ring: [ + {x: 0.5761, y: -0.0989, z: -0.1025, w: 0.8048}, + {x: 0.5332, y: 0.0032, z: 0.005, w: 0.846}, + {x: 0.5773, y: -0.0035, z: -0.0049, w: 0.8165} + ], + middle: [ + {x: 0.543, y: -0.0469, z: -0.0333, w: 0.8378}, + {x: 0.5419, y: -0.0034, z: -0.0053, w: 0.8404}, + {x: 0.5015, y: 0.0037, z: 0.0063, w: 0.8651} + ], + index: [ + {x: 0.3051, y: -0.0156, z: -0.014, w: 0.9521}, + {x: 0.6414, y: 0.0051, z: 0.0063, w: 0.7671}, + {x: 0.5646, y: -0.013, z: -0.019, w: 0.8251} + ], + thumb: [ + {x: 0.313, y: -0.0348, z: 0.3192, w: 0.8938}, + {x: 0, y: 0, z: -0.37, w: 0.929}, + {x: 0, y: 0, z: -0.2604, w: 0.9655} + ] + }, right: { + pinky: [ + {x: 0.5881, y: 0.1728, z: 0.1114, w: 0.7823}, + {x: 0.5704, y: -0.0052, z: -0.0075, w: 0.8213}, + {x: 0.6069, y: 0.0046, z: 0.006, w: 0.7947} + ], + ring: [ + {x: 0.5729, y: 0.1181, z: 0.0898, w: 0.8061}, + {x: 0.5332, y: -0.003, z: -0.0048, w: 0.846}, + {x: 0.5773, y: 0.0035, z: 0.005, w: 0.8165} + ], + middle: [ + {x: 0.543, y: 0.0468, z: 0.0332, w: 0.8378}, + {x: 0.5419, y: 0.0034, z: 0.0052, w: 0.8404}, + {x: 0.5047, y: -0.0037, z: -0.0064, w: 0.8632} + ], + index: [ + {x: 0.306, y: -0.0076, z: -0.0584, w: 0.9502}, + {x: 0.6409, y: -0.005, z: -0.006, w: 0.7675}, + {x: 0.5646, y: 0.0129, z: 0.0189, w: 0.8251} + ], + thumb: [ + {x: 0.313, y: 0.0352, z: -0.3181, w: 0.8942}, + {x: 0, y: 0, z: 0.3698, w: 0.9291}, + {x: 0, y: 0, z: 0.2609, w: 0.9654} + ] + } + }; + + // snapshot for the default pose + var dataDefault = { + left: { + pinky: [{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0}], + middle: [{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0}], + ring: [{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0}], + thumb: [{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0}], + index: [{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0}], + set: false + }, + right: { + pinky: [{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0}], + middle: [{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0}], + ring: [{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0}], + thumb: [{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0}], + index: [{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0}], + set: false + } + }; + + // joint data for the current frame + var dataCurrent = { + left: { + pinky: [{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0}], + middle: [{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0}], + ring: [{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0}], + thumb: [{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0}], + index: [{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0}] + }, + right: { + pinky: [{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0}], + middle: [{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0}], + ring: [{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0}], + thumb: [{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0}], + index: [{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0}] + } + }; + + // interpolated values on joint data to smooth movement + var dataDelta = { + left: { + pinky: [{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0}], + middle: [{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0}], + ring: [{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0}], + thumb: [{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0}], + index: [{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0}] + }, + right: { + pinky: [{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0}], + middle: [{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0}], + ring: [{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0}], + thumb: [{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0}], + index: [{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0},{x: 0, y: 0, z: 0, w: 0}] + } + }; + + // Acquire an updated value per hand every 5 frames when finger is touching (faster in) + var touchAnimationSteps = 5; + + // Acquire an updated value per hand every 20 frames when finger is returning to default position (slower out) + var defaultAnimationSteps = 10; + + // Debugging info + var showSphere = false; + var showLines = false; + + // This get setup on creation + var linesCreated = false; + var sphereCreated = false; + + // Register object with API Debugger + var varsToDebug = { + scriptLoaded: false, + toggleDebugSphere: function() { + showSphere = !showSphere; + if (showSphere && !sphereCreated) { + createDebugSphere(); + sphereCreated = true; + } + }, + toggleDebugLines: function() { + showLines = !showLines; + if (showLines && !linesCreated) { + createDebugLines(); + linesCreated = true; + } + }, + fingerPercent: { + left: { + pinky: 0.38, + middle: 0.38, + ring: 0.38, + thumb: 0.38, + index: 0.38 + } , + right: { + pinky: 0.38, + middle: 0.38, + ring: 0.38, + thumb: 0.38, + index: 0.38 + } + }, + triggerValues: { + leftTriggerValue: 0, + leftTriggerClicked: 0, + rightTriggerValue: 0, + rightTriggerClicked: 0, + leftSecondaryValue: 0, + rightSecondaryValue: 0 + }, + palmData: { + left: new Palm(), + right: new Palm() + }, + offset: {x: 0, y: 0, z: 0}, + avatarLoaded: false + }; + + // Add/Subtract the joint data - per finger joint + function addVals(val1, val2, sign) { + var val = []; + if (val1.length !== val2.length) { + return; + } + for (var i = 0; i < val1.length; i++) { + val.push({x: 0, y: 0, z: 0, w: 0}); + val[i].x = val1[i].x + sign*val2[i].x; + val[i].y = val1[i].y + sign*val2[i].y; + val[i].z = val1[i].z + sign*val2[i].z; + val[i].w = val1[i].w + sign*val2[i].w; + } + return val; + } + + // Multiply/Divide the joint data - per finger joint + function multiplyValsBy(val1, num) { + var val = []; + for (var i = 0; i < val1.length; i++) { + val.push({x: 0, y: 0, z: 0, w: 0}); + val[i].x = val1[i].x * num; + val[i].y = val1[i].y * num; + val[i].z = val1[i].z * num; + val[i].w = val1[i].w * num; + } + return val; + } + + // Calculate the finger lengths by adding its joint lengths + function getJointDistances(jointNamesArray) { + var result = {distances: [], totalDistance: 0}; + for (var i = 1; i < jointNamesArray.length; i++) { + var index0 = MyAvatar.getJointIndex(jointNamesArray[i-1]); + var index1 = MyAvatar.getJointIndex(jointNamesArray[i]); + var pos0 = MyAvatar.getJointPosition(index0); + var pos1 = MyAvatar.getJointPosition(index1); + var distance = Vec3.distance(pos0, pos1); + result.distances.push(distance); + result.totalDistance += distance; + } + return result; + } + + function dataRelativeToWorld(side, dataIn, dataOut) { + var handJoint = handJointNames[side]; + var jointIndex = MyAvatar.getJointIndex(handJoint); + var worldPosHand = MyAvatar.jointToWorldPoint({x: 0, y: 0, z: 0}, jointIndex); + + dataOut.position = MyAvatar.jointToWorldPoint(dataIn.position, jointIndex); + var localPerpendicular = side === "right" ? {x: 0.2, y: 0, z: 1} : {x: -0.2, y: 0, z: 1}; + dataOut.perpendicular = Vec3.normalize( + Vec3.subtract(MyAvatar.jointToWorldPoint(localPerpendicular, jointIndex), worldPosHand) + ); + dataOut.distance = dataIn.distance; + for (var i = 0; i < fingerKeys.length; i++) { + var finger = fingerKeys[i]; + dataOut.fingers[finger] = MyAvatar.jointToWorldPoint(dataIn.fingers[finger], jointIndex); + } + } + + function dataRelativeToHandJoint(side, dataIn, dataOut) { + var handJoint = handJointNames[side]; + var jointIndex = MyAvatar.getJointIndex(handJoint); + var worldPosHand = MyAvatar.jointToWorldPoint({x: 0, y: 0, z: 0}, jointIndex); + + dataOut.position = MyAvatar.worldToJointPoint(dataIn.position, jointIndex); + dataOut.perpendicular = MyAvatar.worldToJointPoint(Vec3.sum(worldPosHand, dataIn.perpendicular), jointIndex); + dataOut.distance = dataIn.distance; + for (var i = 0; i < fingerKeys.length; i++) { + var finger = fingerKeys[i]; + dataOut.fingers[finger] = MyAvatar.worldToJointPoint(dataIn.fingers[finger], jointIndex); + } + } + + // Calculate touch field; Sphere at the center of the palm, + // perpendicular vector from the palm plane and origin of the the finger rays + function estimatePalmData(side) { + // Return data object + var data = new Palm(); + + var jointOffset = { x: 0, y: 0, z: 0 }; + + var upperSide = side[0].toUpperCase() + side.substring(1); + var jointIndexHand = MyAvatar.getJointIndex(upperSide + "Hand"); + + // Store position of the hand joint + var worldPosHand = MyAvatar.jointToWorldPoint(jointOffset, jointIndexHand); + var minusWorldPosHand = {x: -worldPosHand.x, y: -worldPosHand.y, z: -worldPosHand.z}; + + // Data for finger rays + var directions = {pinky: undefined, middle: undefined, ring: undefined, thumb: undefined, index: undefined}; + var positions = {pinky: undefined, middle: undefined, ring: undefined, thumb: undefined, index: undefined}; + + var thumbLength = 0; + var weightCount = 0; + + // Calculate palm center + var handJointWeight = 1; + var fingerJointWeight = 2; + + var palmCenter = {x: 0, y: 0, z: 0}; + palmCenter = Vec3.sum(worldPosHand, palmCenter); + + weightCount += handJointWeight; + + for (var i = 0; i < fingerKeys.length; i++) { + var finger = fingerKeys[i]; + var jointSuffixes = 4; // Get 4 joint names with suffix numbers (0, 1, 2, 3) + var jointNames = getJointNames(side, finger, jointSuffixes); + var fingerLength = getJointDistances(jointNames).totalDistance; + + var jointIndex = MyAvatar.getJointIndex(jointNames[0]); + positions[finger] = MyAvatar.jointToWorldPoint(jointOffset, jointIndex); + directions[finger] = Vec3.normalize(Vec3.sum(positions[finger], minusWorldPosHand)); + data.fingers[finger] = Vec3.sum(positions[finger], Vec3.multiply(fingerLength, directions[finger])); + if (finger !== "thumb") { + // finger joints have double the weight than the hand joint + // This would better position the palm estimation + + palmCenter = Vec3.sum(Vec3.multiply(fingerJointWeight, positions[finger]), palmCenter); + weightCount += fingerJointWeight; + } else { + thumbLength = fingerLength; + } + } + + // perpendicular change direction depending on the side + data.perpendicular = (side === "right") ? + Vec3.normalize(Vec3.cross(directions.index, directions.pinky)): + Vec3.normalize(Vec3.cross(directions.pinky, directions.index)); + + data.position = Vec3.multiply(1.0/weightCount, palmCenter); + + if (side === "right") { + varsToDebug.offset = MyAvatar.worldToJointPoint(worldPosHand, jointIndexHand); + } + + var palmDistanceMultiplier = 1.55; // 1.55 based on test/error for the sphere radius that best fits the hand + data.distance = palmDistanceMultiplier*Vec3.distance(data.position, positions.index); + + // move back thumb ray origin + var thumbBackMultiplier = 0.2; + data.fingers.thumb = Vec3.sum( + data.fingers.thumb, Vec3.multiply( -thumbBackMultiplier * thumbLength, data.perpendicular)); + + // return getDataRelativeToHandJoint(side, data); + dataRelativeToHandJoint(side, data, palmData[side]); + palmData[side].set = true; + } + + // Register GlobalDebugger for API Debugger + Script.registerValue("GlobalDebugger", varsToDebug); + + // store the rays for the fingers - only for debug purposes + var fingerRays = { + left: { + pinky: undefined, + middle: undefined, + ring: undefined, + thumb: undefined, + index: undefined + }, + right: { + pinky: undefined, + middle: undefined, + ring: undefined, + thumb: undefined, + index: undefined + } + }; + + // Create debug overlays - finger rays + palm rays + spheres + var palmRay, sphereHand; + + function createDebugLines() { + for (var i = 0; i < fingerKeys.length; i++) { + fingerRays.left[fingerKeys[i]] = Overlays.addOverlay("line3d", { + color: { red: 0, green: 0, blue: 255 }, + start: { x: 0, y: 0, z: 0 }, + end: { x: 0, y: 1, z: 0 }, + visible: showLines + }); + fingerRays.right[fingerKeys[i]] = Overlays.addOverlay("line3d", { + color: { red: 0, green: 0, blue: 255 }, + start: { x: 0, y: 0, z: 0 }, + end: { x: 0, y: 1, z: 0 }, + visible: showLines + }); + } + + palmRay = { + left: Overlays.addOverlay("line3d", { + color: { red: 255, green: 0, blue: 0 }, + start: { x: 0, y: 0, z: 0 }, + end: { x: 0, y: 1, z: 0 }, + visible: showLines + }), + right: Overlays.addOverlay("line3d", { + color: { red: 255, green: 0, blue: 0 }, + start: { x: 0, y: 0, z: 0 }, + end: { x: 0, y: 1, z: 0 }, + visible: showLines + }) + }; + linesCreated = true; + } + + function createDebugSphere() { + sphereHand = { + right: Overlays.addOverlay("sphere", { + position: MyAvatar.position, + color: { red: 0, green: 255, blue: 0 }, + scale: { x: 0.01, y: 0.01, z: 0.01 }, + visible: showSphere + }), + left: Overlays.addOverlay("sphere", { + position: MyAvatar.position, + color: { red: 0, green: 255, blue: 0 }, + scale: { x: 0.01, y: 0.01, z: 0.01 }, + visible: showSphere + }) + }; + sphereCreated = true; + } + + function acquireDefaultPose(side) { + for (var i = 0; i < fingerKeys.length; i++) { + var finger = fingerKeys[i]; + var jointSuffixes = 3; // We need rotation of the 0, 1 and 2 joints + var names = getJointNames(side, finger, jointSuffixes); + for (var j = 0; j < names.length; j++) { + var index = MyAvatar.getJointIndex(names[j]); + var rotation = MyAvatar.getJointRotation(index); + dataDefault[side][finger][j] = dataCurrent[side][finger][j] = rotation; + } + } + dataDefault[side].set = true; + } + + var rayPicks = { + left: { + pinky: undefined, + middle: undefined, + ring: undefined, + thumb: undefined, + index: undefined + }, + right: { + pinky: undefined, + middle: undefined, + ring: undefined, + thumb: undefined, + index: undefined + } + }; + + var dataFailed = { + left: { + pinky: 0, + middle: 0, + ring: 0, + thumb: 0, + index: 0 + }, + right: { + pinky: 0, + middle: 0, + ring: 0, + thumb: 0, + index: 0 + } + }; + + function clearRayPicks(side) { + for (var i = 0; i < fingerKeys.length; i++) { + var finger = fingerKeys[i]; + if (rayPicks[side][finger] !== undefined) { + RayPick.removeRayPick(rayPicks[side][finger]); + rayPicks[side][finger] = undefined; + } + } + } + + function createRayPicks(side) { + var data = palmData[side]; + clearRayPicks(side); + for (var i = 0; i < fingerKeys.length; i++) { + var finger = fingerKeys[i]; + var LOOKUP_DISTANCE_MULTIPLIER = 1.5; + var dist = LOOKUP_DISTANCE_MULTIPLIER*data.distance; + var checkOffset = { + x: data.perpendicular.x * dist, + y: data.perpendicular.y * dist, + z: data.perpendicular.z * dist + }; + + var checkPoint = Vec3.sum(data.position, Vec3.multiply(2, checkOffset)); + var sensorToWorldScale = MyAvatar.getSensorToWorldScale(); + + var origin = data.fingers[finger]; + + var direction = Vec3.normalize(Vec3.subtract(checkPoint, origin)); + + origin = Vec3.multiply(1/sensorToWorldScale, origin); + + rayPicks[side][finger] = RayPick.createRayPick( + { + "enabled": false, + "joint": handJointNames[side], + "posOffset": origin, + "dirOffset": direction, + "filter": RayPick.PICK_ENTITIES + } + ); + + RayPick.setPrecisionPicking(rayPicks[side][finger], true); + } + } + + function activateNextRay(side, index) { + var nextIndex = (index < fingerKeys.length-1) ? index + 1 : 0; + for (var i = 0; i < fingerKeys.length; i++) { + var finger = fingerKeys[i]; + if (i === nextIndex) { + RayPick.enableRayPick(rayPicks[side][finger]); + } else { + RayPick.disableRayPick(rayPicks[side][finger]); + } + } + } + + function updateSphereHand(side) { + var data = new Palm(); + dataRelativeToWorld(side, palmData[side], data); + varsToDebug.palmData[side] = palmData[side]; + + var palmPoint = data.position; + var LOOKUP_DISTANCE_MULTIPLIER = 1.5; + var dist = LOOKUP_DISTANCE_MULTIPLIER*data.distance; + + // Situate the debugging overlays + var checkOffset = { + x: data.perpendicular.x * dist, + y: data.perpendicular.y * dist, + z: data.perpendicular.z * dist + }; + + var spherePos = Vec3.sum(palmPoint, checkOffset); + var checkPoint = Vec3.sum(palmPoint, Vec3.multiply(2, checkOffset)); + + if (showLines) { + Overlays.editOverlay(palmRay[side], { + start: palmPoint, + end: checkPoint, + visible: showLines + }); + for (var i = 0; i < fingerKeys.length; i++) { + Overlays.editOverlay(fingerRays[side][fingerKeys[i]], { + start: data.fingers[fingerKeys[i]], + end: checkPoint, + visible: showLines + }); + } + } + + if (showSphere) { + Overlays.editOverlay(sphereHand[side], { + position: spherePos, + scale: { + x: 2*dist, + y: 2*dist, + z: 2*dist + }, + visible: showSphere + }); + } + + // Update the intersection of only one finger at a time + var finger = fingerKeys[updateFingerWithIndex]; + var nearbyEntities = Entities.findEntities(spherePos, dist); + // Filter the entities that are allowed to be touched + var touchableEntities = nearbyEntities.filter(function (id) { + return untouchableEntities.indexOf(id) == -1; + }); + var intersection; + if (rayPicks[side][finger] !== undefined) { + intersection = RayPick.getPrevRayPickResult(rayPicks[side][finger]); + } + + var animationSteps = defaultAnimationSteps; + var newFingerData = dataDefault[side][finger]; + var isAbleToGrab = false; + if (touchableEntities.length > 0) { + RayPick.setIncludeItems(rayPicks[side][finger], touchableEntities); + + if (intersection === undefined) { + return; + } + + var percent = 0; // Initialize + isAbleToGrab = intersection.intersects && intersection.distance < LOOKUP_DISTANCE_MULTIPLIER*dist; + if (isAbleToGrab && !getTouching(side)) { + acquireDefaultPose(side); // take a snapshot of the default pose before touch starts + newFingerData = dataDefault[side][finger]; // assign default pose to finger data + } + // Store if this finger is touching something + isTouching[side][finger] = isAbleToGrab; + if (isAbleToGrab) { + // update the open/close percentage for this finger + var FINGER_REACT_MULTIPLIER = 2.8; + + percent = intersection.distance/(FINGER_REACT_MULTIPLIER*dist); + + var THUMB_FACTOR = 0.2; + var FINGER_FACTOR = 0.05; + + // Amount of grab coefficient added to the fingers - thumb is higher + var grabMultiplier = finger === "thumb" ? THUMB_FACTOR : FINGER_FACTOR; + percent += grabMultiplier * grabPercent[side]; + + // Calculate new interpolation data + var totalDistance = addVals(dataClose[side][finger], dataOpen[side][finger], -1); + // Assign close/open ratio to finger to simulate touch + newFingerData = addVals(dataOpen[side][finger], multiplyValsBy(totalDistance, percent), 1); + animationSteps = touchAnimationSteps; + } + varsToDebug.fingerPercent[side][finger] = percent; + + } + if (!isAbleToGrab) { + dataFailed[side][finger] = dataFailed[side][finger] === 0 ? 1 : 2; + } else { + dataFailed[side][finger] = 0; + } + // If it only fails once it will not update increments + if (dataFailed[side][finger] !== 1) { + // Calculate animation increments + dataDelta[side][finger] = + multiplyValsBy(addVals(newFingerData, dataCurrent[side][finger], -1), 1.0/animationSteps); + } + } + + // Recreate the finger joint names + function getJointNames(side, finger, count) { + var names = []; + for (var i = 1; i < count+1; i++) { + var name = side[0].toUpperCase()+side.substring(1)+"Hand"+finger[0].toUpperCase()+finger.substring(1)+(i); + names.push(name); + } + return names; + } + + // Capture the controller values + var leftTriggerPress = function (value) { + varsToDebug.triggerValues.leftTriggerValue = value; + // the value for the trigger increments the hand-close percentage + grabPercent.left = value; + }; + + var leftTriggerClick = function (value) { + varsToDebug.triggerValues.leftTriggerClicked = value; + }; + + var rightTriggerPress = function (value) { + varsToDebug.triggerValues.rightTriggerValue = value; + // the value for the trigger increments the hand-close percentage + grabPercent.right = value; + }; + + var rightTriggerClick = function (value) { + varsToDebug.triggerValues.rightTriggerClicked = value; + }; + + var leftSecondaryPress = function (value) { + varsToDebug.triggerValues.leftSecondaryValue = value; + }; + + var rightSecondaryPress = function (value) { + varsToDebug.triggerValues.rightSecondaryValue = value; + }; + + var MAPPING_NAME = "com.highfidelity.handTouch"; + var mapping = Controller.newMapping(MAPPING_NAME); + mapping.from([Controller.Standard.RT]).peek().to(rightTriggerPress); + mapping.from([Controller.Standard.RTClick]).peek().to(rightTriggerClick); + mapping.from([Controller.Standard.LT]).peek().to(leftTriggerPress); + mapping.from([Controller.Standard.LTClick]).peek().to(leftTriggerClick); + + mapping.from([Controller.Standard.RB]).peek().to(rightSecondaryPress); + mapping.from([Controller.Standard.LB]).peek().to(leftSecondaryPress); + mapping.from([Controller.Standard.LeftGrip]).peek().to(leftSecondaryPress); + mapping.from([Controller.Standard.RightGrip]).peek().to(rightSecondaryPress); + + Controller.enableMapping(MAPPING_NAME); + + if (showLines && !linesCreated) { + createDebugLines(); + linesCreated = true; + } + + if (showSphere && !sphereCreated) { + createDebugSphere(); + sphereCreated = true; + } + + function getTouching(side) { + var animating = false; + for (var i = 0; i < fingerKeys.length; i++) { + var finger = fingerKeys[i]; + animating = animating || isTouching[side][finger]; + } + return animating; // return false only if none of the fingers are touching + } + + function reEstimatePalmData() { + ["right", "left"].forEach(function(side) { + estimatePalmData(side); + }); + } + + function recreateRayPicks() { + ["right", "left"].forEach(function(side) { + createRayPicks(side); + }); + } + + function cleanUp() { + ["right", "left"].forEach(function (side) { + if (linesCreated) { + Overlays.deleteOverlay(palmRay[side]); + } + if (sphereCreated) { + Overlays.deleteOverlay(sphereHand[side]); + } + clearRayPicks(side); + for (var i = 0; i < fingerKeys.length; i++) { + var finger = fingerKeys[i]; + var jointSuffixes = 3; // We need to clear the joints 0, 1 and 2 joints + var names = getJointNames(side, finger, jointSuffixes); + for (var j = 0; j < names.length; j++) { + var index = MyAvatar.getJointIndex(names[j]); + MyAvatar.clearJointData(index); + } + if (linesCreated) { + Overlays.deleteOverlay(fingerRays[side][finger]); + } + } + }); + } + + MyAvatar.shouldDisableHandTouchChanged.connect(function (shouldDisable) { + if (shouldDisable) { + if (handTouchEnabled) { + cleanUp(); + } + } else { + if (!handTouchEnabled) { + reEstimatePalmData(); + recreateRayPicks(); + } + } + handTouchEnabled = !shouldDisable; + }); + + Controller.inputDeviceRunningChanged.connect(function (deviceName, isEnabled) { + if (deviceName == LEAP_MOTION_NAME) { + leapMotionEnabled = isEnabled; + } + }); + + MyAvatar.disableHandTouchForIDChanged.connect(function (entityID, disable) { + var entityIndex = untouchableEntities.indexOf(entityID); + if (disable) { + if (entityIndex == -1) { + untouchableEntities.push(entityID); + } + } else { + if (entityIndex != -1) { + untouchableEntities.splice(entityIndex, 1); + } + } + }); + + MyAvatar.onLoadComplete.connect(function () { + // Sometimes the rig is not ready when this signal is trigger + console.log("avatar loaded"); + Script.setTimeout(function() { + reEstimatePalmData(); + recreateRayPicks(); + }, MSECONDS_AFTER_LOAD); + }); + + MyAvatar.sensorToWorldScaleChanged.connect(function() { + reEstimatePalmData(); + }); + + Script.scriptEnding.connect(function () { + cleanUp(); + }); + + Script.update.connect(function () { + + if (!handTouchEnabled || leapMotionEnabled) { + return; + } + + // index of the finger that needs to be updated this frame + updateFingerWithIndex = (updateFingerWithIndex < fingerKeys.length-1) ? updateFingerWithIndex + 1 : 0; + + ["right", "left"].forEach(function(side) { + + if (!palmData[side].set) { + reEstimatePalmData(); + recreateRayPicks(); + } + + // recalculate the base data + updateSphereHand(side); + activateNextRay(side, updateFingerWithIndex); + + // this vars manage the transition to default pose + var isHandTouching = getTouching(side); + countToDefault[side] = isHandTouching ? 0 : countToDefault[side] + 1; + + for (var i = 0; i < fingerKeys.length; i++) { + var finger = fingerKeys[i]; + var jointSuffixes = 3; // We need to update rotation of the 0, 1 and 2 joints + var names = getJointNames(side, finger, jointSuffixes); + + // Add the animation increments + dataCurrent[side][finger] = addVals(dataCurrent[side][finger], dataDelta[side][finger], 1); + + // update every finger joint + for (var j = 0; j < names.length; j++) { + var index = MyAvatar.getJointIndex(names[j]); + // if no finger is touching restate the default poses + if (isHandTouching || (dataDefault[side].set && + countToDefault[side] < fingerKeys.length*touchAnimationSteps)) { + var quatRot = dataCurrent[side][finger][j]; + MyAvatar.setJointRotation(index, quatRot); + } else { + MyAvatar.clearJointData(index); + } + } + } + }); + }); +}()); diff --git a/scripts/simplifiedUI/system/controllers/squeezeHands.js b/scripts/simplifiedUI/system/controllers/squeezeHands.js new file mode 100644 index 0000000000..69f44f46a9 --- /dev/null +++ b/scripts/simplifiedUI/system/controllers/squeezeHands.js @@ -0,0 +1,184 @@ +"use strict"; + +// +// controllers/squeezeHands.js +// +// Created by Anthony J. Thibault +// Copyright 2015 High Fidelity, Inc. +// +// Default script to drive the animation of the hands based on hand controllers. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// +/* global Script, MyAvatar, Messages, Controller */ +/* eslint indent: ["error", 4, { "outerIIFEBody": 0 }] */ + +(function() { // BEGIN LOCAL_SCOPE + +var lastLeftTrigger = 0; +var lastRightTrigger = 0; +var leftHandOverlayAlpha = 0; +var rightHandOverlayAlpha = 0; + +// var CONTROLLER_DEAD_SPOT = 0.25; +var TRIGGER_SMOOTH_TIMESCALE = 0.1; +var OVERLAY_RAMP_RATE = 8.0; + +var animStateHandlerID; + +var leftIndexPointingOverride = 0; +var rightIndexPointingOverride = 0; +var leftThumbRaisedOverride = 0; +var rightThumbRaisedOverride = 0; + +var HIFI_POINT_INDEX_MESSAGE_CHANNEL = "Hifi-Point-Index"; + +var isLeftIndexPointing = false; +var isRightIndexPointing = false; +var isLeftThumbRaised = false; +var isRightThumbRaised = false; + +function clamp(val, min, max) { + return Math.min(Math.max(val, min), max); +} + +// function normalizeControllerValue(val) { +// return clamp((val - CONTROLLER_DEAD_SPOT) / (1 - CONTROLLER_DEAD_SPOT), 0, 1); +// } + +function lerp(a, b, alpha) { + return a * (1 - alpha) + b * alpha; +} + +function init() { + Script.update.connect(update); + animStateHandlerID = MyAvatar.addAnimationStateHandler( + animStateHandler, + [ + "leftHandOverlayAlpha", "leftHandGraspAlpha", + "rightHandOverlayAlpha", "rightHandGraspAlpha", + "isLeftHandGrasp", "isLeftIndexPoint", "isLeftThumbRaise", "isLeftIndexPointAndThumbRaise", + "isRightHandGrasp", "isRightIndexPoint", "isRightThumbRaise", "isRightIndexPointAndThumbRaise" + ] + ); + Messages.subscribe(HIFI_POINT_INDEX_MESSAGE_CHANNEL); + Messages.messageReceived.connect(handleMessages); +} + +function animStateHandler(props) { + return { + leftHandOverlayAlpha: leftHandOverlayAlpha, + leftHandGraspAlpha: lastLeftTrigger, + rightHandOverlayAlpha: rightHandOverlayAlpha, + rightHandGraspAlpha: lastRightTrigger, + + isLeftHandGrasp: !isLeftIndexPointing && !isLeftThumbRaised, + isLeftIndexPoint: isLeftIndexPointing && !isLeftThumbRaised, + isLeftThumbRaise: !isLeftIndexPointing && isLeftThumbRaised, + isLeftIndexPointAndThumbRaise: isLeftIndexPointing && isLeftThumbRaised, + + isRightHandGrasp: !isRightIndexPointing && !isRightThumbRaised, + isRightIndexPoint: isRightIndexPointing && !isRightThumbRaised, + isRightThumbRaise: !isRightIndexPointing && isRightThumbRaised, + isRightIndexPointAndThumbRaise: isRightIndexPointing && isRightThumbRaised + }; +} + +function update(dt) { + var leftTrigger = clamp(Controller.getValue(Controller.Standard.LT) + Controller.getValue(Controller.Standard.LeftGrip), 0, 1); + var rightTrigger = clamp(Controller.getValue(Controller.Standard.RT) + Controller.getValue(Controller.Standard.RightGrip), 0, 1); + + // Average last few trigger values together for a bit of smoothing + var tau = clamp(dt / TRIGGER_SMOOTH_TIMESCALE, 0, 1); + lastLeftTrigger = lerp(leftTrigger, lastLeftTrigger, tau); + lastRightTrigger = lerp(rightTrigger, lastRightTrigger, tau); + + // ramp on/off left hand overlay + var leftHandPose = Controller.getPoseValue(Controller.Standard.LeftHand); + if (leftHandPose.valid) { + leftHandOverlayAlpha = clamp(leftHandOverlayAlpha + OVERLAY_RAMP_RATE * dt, 0, 1); + } else { + leftHandOverlayAlpha = clamp(leftHandOverlayAlpha - OVERLAY_RAMP_RATE * dt, 0, 1); + } + + // ramp on/off right hand overlay + var rightHandPose = Controller.getPoseValue(Controller.Standard.RightHand); + if (rightHandPose.valid) { + rightHandOverlayAlpha = clamp(rightHandOverlayAlpha + OVERLAY_RAMP_RATE * dt, 0, 1); + } else { + rightHandOverlayAlpha = clamp(rightHandOverlayAlpha - OVERLAY_RAMP_RATE * dt, 0, 1); + } + + // Pointing index fingers and raising thumbs + isLeftIndexPointing = (leftIndexPointingOverride > 0) || (leftHandPose.valid && Controller.getValue(Controller.Standard.LeftIndexPoint) === 1); + isRightIndexPointing = (rightIndexPointingOverride > 0) || (rightHandPose.valid && Controller.getValue(Controller.Standard.RightIndexPoint) === 1); + isLeftThumbRaised = (leftThumbRaisedOverride > 0) || (leftHandPose.valid && Controller.getValue(Controller.Standard.LeftThumbUp) === 1); + isRightThumbRaised = (rightThumbRaisedOverride > 0) || (rightHandPose.valid && Controller.getValue(Controller.Standard.RightThumbUp) === 1); +} + +function handleMessages(channel, message, sender) { + if (sender === MyAvatar.sessionUUID && channel === HIFI_POINT_INDEX_MESSAGE_CHANNEL) { + var data = JSON.parse(message); + + if (data.pointIndex !== undefined) { + if (data.pointIndex) { + leftIndexPointingOverride++; + rightIndexPointingOverride++; + } else { + leftIndexPointingOverride--; + rightIndexPointingOverride--; + } + } + if (data.pointLeftIndex !== undefined) { + if (data.pointLeftIndex) { + leftIndexPointingOverride++; + } else { + leftIndexPointingOverride--; + } + } + if (data.pointRightIndex !== undefined) { + if (data.pointRightIndex) { + rightIndexPointingOverride++; + } else { + rightIndexPointingOverride--; + } + } + if (data.raiseThumbs !== undefined) { + if (data.raiseThumbs) { + leftThumbRaisedOverride++; + rightThumbRaisedOverride++; + } else { + leftThumbRaisedOverride--; + rightThumbRaisedOverride--; + } + } + if (data.raiseLeftThumb !== undefined) { + if (data.raiseLeftThumb) { + leftThumbRaisedOverride++; + } else { + leftThumbRaisedOverride--; + } + } + if (data.raiseRightThumb !== undefined) { + if (data.raiseRightThumb) { + rightThumbRaisedOverride++; + } else { + rightThumbRaisedOverride--; + } + } + } +} + +function shutdown() { + Script.update.disconnect(update); + MyAvatar.removeAnimationStateHandler(animStateHandlerID); + Messages.unsubscribe(HIFI_POINT_INDEX_MESSAGE_CHANNEL); + Messages.messageReceived.disconnect(handleMessages); +} + +Script.scriptEnding.connect(shutdown); + +init(); + +}()); // END LOCAL_SCOPE diff --git a/scripts/simplifiedUI/system/controllers/toggleAdvancedMovementForHandControllers.js b/scripts/simplifiedUI/system/controllers/toggleAdvancedMovementForHandControllers.js new file mode 100644 index 0000000000..92f72f8724 --- /dev/null +++ b/scripts/simplifiedUI/system/controllers/toggleAdvancedMovementForHandControllers.js @@ -0,0 +1,174 @@ +"use strict"; + +// Created by james b. pollack @imgntn on 8/18/2016 +// Copyright 2016 High Fidelity, Inc. +// +// advanced movements settings are in individual controller json files +// what we do is check the status of the 'advance movement' checkbox when you enter HMD mode +// if 'advanced movement' is checked...we give you the defaults that are in the json. +// if 'advanced movement' is not checked... we override the advanced controls with basic ones. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html + +/* jslint bitwise: true */ + +/* global Script, Quat, MyAvatar, HMD, Controller, Messages*/ + +(function() { // BEGIN LOCAL_SCOPE + + var TWO_SECONDS_INTERVAL = 2000; + var FLYING_MAPPING_NAME = 'Hifi-Flying-Dev-' + Math.random(); + var DRIVING_MAPPING_NAME = 'Hifi-Driving-Dev-' + Math.random(); + + var flyingMapping = null; + var drivingMapping = null; + + var TURN_RATE = 1000; + var isDisabled = false; + + var previousFlyingState = MyAvatar.getFlyingEnabled(); + var previousDrivingState = false; + + function rotate180() { + var newOrientation = Quat.multiply(MyAvatar.orientation, Quat.angleAxis(180, { + x: 0, + y: 1, + z: 0 + })); + MyAvatar.orientation = newOrientation; + } + + var inFlipTurn = false; + + function registerBasicMapping() { + + drivingMapping = Controller.newMapping(DRIVING_MAPPING_NAME); + drivingMapping.from(Controller.Standard.LY).to(function(value) { + if (isDisabled) { + return; + } + + if (value === 1 && Controller.Hardware.OculusTouch !== undefined) { + rotate180(); + } else if (Controller.Hardware.Vive !== undefined) { + if (value > 0.75 && inFlipTurn === false) { + inFlipTurn = true; + rotate180(); + Script.setTimeout(function() { + inFlipTurn = false; + }, TURN_RATE); + } + } + return; + }); + + flyingMapping = Controller.newMapping(FLYING_MAPPING_NAME); + flyingMapping.from(Controller.Standard.RY).to(function(value) { + if (isDisabled) { + return; + } + + if (value === 1 && Controller.Hardware.OculusTouch !== undefined) { + rotate180(); + } else if (Controller.Hardware.Vive !== undefined) { + if (value > 0.75 && inFlipTurn === false) { + inFlipTurn = true; + rotate180(); + Script.setTimeout(function() { + inFlipTurn = false; + }, TURN_RATE); + } + } + return; + }); + } + + function scriptEnding() { + Controller.disableMapping(FLYING_MAPPING_NAME); + Controller.disableMapping(DRIVING_MAPPING_NAME); + } + + Script.scriptEnding.connect(scriptEnding); + + registerBasicMapping(); + + Script.setTimeout(function() { + if (MyAvatar.useAdvanceMovementControls) { + Controller.disableMapping(DRIVING_MAPPING_NAME); + } else { + Controller.enableMapping(DRIVING_MAPPING_NAME); + } + + if (MyAvatar.getFlyingEnabled()) { + Controller.disableMapping(FLYING_MAPPING_NAME); + } else { + Controller.enableMapping(FLYING_MAPPING_NAME); + } + }, 100); + + + HMD.displayModeChanged.connect(function(isHMDMode) { + if (isHMDMode) { + if (Controller.Hardware.Vive !== undefined || Controller.Hardware.OculusTouch !== undefined) { + if (MyAvatar.useAdvancedMovementControls) { + Controller.disableMapping(DRIVING_MAPPING_NAME); + } else { + Controller.enableMapping(DRIVING_MAPPING_NAME); + } + + if (MyAvatar.getFlyingEnabled()) { + Controller.disableMapping(FLYING_MAPPING_NAME); + } else { + Controller.enableMapping(FLYING_MAPPING_NAME); + } + + } + } + }); + + + function update() { + if ((Controller.Hardware.Vive !== undefined || Controller.Hardware.OculusTouch !== undefined) && HMD.active) { + var flying = MyAvatar.getFlyingEnabled(); + var driving = MyAvatar.useAdvancedMovementControls; + + if (flying !== previousFlyingState) { + if (flying) { + Controller.disableMapping(FLYING_MAPPING_NAME); + } else { + Controller.enableMapping(FLYING_MAPPING_NAME); + } + + previousFlyingState = flying; + } + + if (driving !== previousDrivingState) { + if (driving) { + Controller.disableMapping(DRIVING_MAPPING_NAME); + } else { + Controller.enableMapping(DRIVING_MAPPING_NAME); + } + previousDrivingState = driving; + } + } + Script.setTimeout(update, TWO_SECONDS_INTERVAL); + } + + Script.setTimeout(update, TWO_SECONDS_INTERVAL); + + var HIFI_ADVANCED_MOVEMENT_DISABLER_CHANNEL = 'Hifi-Advanced-Movement-Disabler'; + function handleMessage(channel, message, sender) { + if (channel === HIFI_ADVANCED_MOVEMENT_DISABLER_CHANNEL) { + if (message === 'disable') { + isDisabled = true; + } else if (message === 'enable') { + isDisabled = false; + } + } + } + + Messages.subscribe(HIFI_ADVANCED_MOVEMENT_DISABLER_CHANNEL); + Messages.messageReceived.connect(handleMessage); + +}()); // END LOCAL_SCOPE diff --git a/scripts/simplifiedUI/system/controllers/touchControllerConfiguration.js b/scripts/simplifiedUI/system/controllers/touchControllerConfiguration.js new file mode 100644 index 0000000000..991b77b8af --- /dev/null +++ b/scripts/simplifiedUI/system/controllers/touchControllerConfiguration.js @@ -0,0 +1,372 @@ + +// +// touchControllerConfiguration.js +// +// Created by Ryan Huffman on 12/06/16 +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +/* globals TOUCH_CONTROLLER_CONFIGURATION_LEFT:true, TOUCH_CONTROLLER_CONFIGURATION_RIGHT:true, + Quat, Vec3, Script, MyAvatar, Controller */ +/* eslint camelcase: ["error", { "properties": "never" }] */ + +var leftBaseRotation = Quat.multiply( + Quat.fromPitchYawRollDegrees(-90, 0, 0), + Quat.fromPitchYawRollDegrees(0, 0, 90) +); +var rightBaseRotation = Quat.multiply( + Quat.fromPitchYawRollDegrees(-90, 0, 0), + Quat.fromPitchYawRollDegrees(0, 0, -90) +); + +// keep these in sync with the values from OculusHelpers.cpp +var CONTROLLER_LENGTH_OFFSET = 0.0762; +// var CONTROLLER_LATERAL_OFFSET = 0.0381; +// var CONTROLLER_VERTICAL_OFFSET = 0.0381; +// var CONTROLLER_FORWARD_OFFSET = 0.1524; + +var leftBasePosition = Vec3.multiplyQbyV(leftBaseRotation, { + x: -CONTROLLER_LENGTH_OFFSET / 2.0, + y: CONTROLLER_LENGTH_OFFSET / 2.0, + z: CONTROLLER_LENGTH_OFFSET * 1.5 +}); +var rightBasePosition = Vec3.multiplyQbyV(rightBaseRotation, { + x: CONTROLLER_LENGTH_OFFSET / 2.0, + y: CONTROLLER_LENGTH_OFFSET / 2.0, + z: CONTROLLER_LENGTH_OFFSET * 1.5 +}); + +var BASE_URL = Script.resourcesPath() + "meshes/controller/touch/"; + +TOUCH_CONTROLLER_CONFIGURATION_LEFT = { + name: "Touch", + controllers: [ + { + modelURL: BASE_URL + "touch_l_body.fbx", + jointIndex: MyAvatar.getJointIndex("_CAMERA_RELATIVE_CONTROLLER_LEFTHAND"), + naturalPosition: { x: 0.01648625358939171, y: -0.03551870584487915, z: -0.018527675420045853 }, + dimensions: { x: 0.11053799837827682, y: 0.0995776429772377, z: 0.10139888525009155 }, + rotation: leftBaseRotation, + position: leftBasePosition, + + parts: { + tips: { + type: "static", + modelURL: BASE_URL + "Oculus-Labels-L.fbx", + naturalPosition: { x: -0.022335469722747803, y: 0.00022516027092933655, z: 0.020340695977211 }, + naturalDimensions: { x: 0.132063, y: 0.0856, z: 0.130282 }, + + textureName: "blank", + defaultTextureLayer: "blank", + textureLayers: { + blank: { + defaultTextureURL: BASE_URL + "Oculus-Labels-L.fbx/Oculus-Labels-L.fbm/Blank.png" + }, + trigger: { + defaultTextureURL: BASE_URL + "Oculus-Labels-L.fbx/Oculus-Labels-L.fbm/Trigger.png" + }, + arrows: { + defaultTextureURL: BASE_URL + "Oculus-Labels-L.fbx/Oculus-Labels-L.fbm/Rotate.png" + }, + grip: { + defaultTextureURL: BASE_URL + "Oculus-Labels-L.fbx/Oculus-Labels-L.fbm/Grip-oculus.png" + }, + teleport: { + defaultTextureURL: BASE_URL + "Oculus-Labels-L.fbx/Oculus-Labels-L.fbm/Teleport.png" + }, + both_triggers: { + defaultTextureURL: BASE_URL + "Oculus-Labels-L.fbx/Oculus-Labels-L.fbm/Grip-Trigger.png" + }, + } + }, + + trigger: { + type: "rotational", + modelURL: BASE_URL + "touch_l_trigger.fbx", + naturalPosition: { x: 0.0008544912561774254, y: -0.019867943599820137, z: 0.018800459802150726 }, + naturalDimensions: { x: 0.027509, y: 0.025211, z: 0.018443 }, + + // rotational + input: Controller.Standard.LT, + origin: { x: 0, y: -0.015, z: -0.00 }, + minValue: 0.0, + maxValue: 1.0, + axis: { x: 1, y: 0, z: 0 }, + maxAngle: 17, + + textureName: "tex-highlight", + defaultTextureLayer: "normal", + textureLayers: { + normal: { + defaultTextureURL: BASE_URL + "touch_l_trigger.fbx/touch_l_trigger.fbm/L_controller_DIF.jpg", + }, + highlight: { + defaultTextureURL: BASE_URL + "touch_l_trigger.fbx/touch_l_trigger.fbm/L_controller-highlight_DIF.jpg", + } + } + }, + + grip: { + type: "linear", + modelURL: BASE_URL + "touch_l_bumper.fbx", + naturalPosition: { x: 0.00008066371083259583, y: -0.02715788595378399, z: -0.02448512241244316 }, + naturalDimensions: { x: 0.017444, y: 0.020297, z: 0.026003 }, + + // linear properties + // Offset from origin = 0.36470, 0.11048, 0.11066 + input: "OculusTouch.LeftGrip", + axis: { x: 1, y: 0.302933918, z: 0.302933918 }, + maxTranslation: 0.003967, + + textureName: "tex-highlight", + defaultTextureLayer: "normal", + textureLayers: { + normal: { + defaultTextureURL: BASE_URL + "touch_l_bumper.fbx/touch_l_bumper.fbm/L_controller_DIF.jpg", + }, + highlight: { + defaultTextureURL: BASE_URL + "touch_l_bumper.fbx/touch_l_bumper.fbm/L_controller-highlight_DIF.jpg", + } + } + }, + + joystick: { + type: "joystick", + modelURL: BASE_URL + "touch_l_joystick.fbx", + naturalPosition: { x: 0.0075613949447870255, y: -0.008225866593420506, z: 0.004792703315615654 }, + naturalDimensions: { x: 0.027386, y: 0.033254, z: 0.027272 }, + + // joystick + xInput: "OculusTouch.LX", + yInput: "OculusTouch.LY", + originOffset: { x: 0, y: -0.0028564, z: -0.00 }, + xHalfAngle: 20, + yHalfAngle: 20, + + textureName: "tex-highlight", + defaultTextureLayer: "normal", + textureLayers: { + normal: { + defaultTextureURL: BASE_URL + "touch_l_joystick.fbx/touch_l_joystick.fbm/L_controller_DIF.jpg", + }, + highlight: { + defaultTextureURL: BASE_URL + "touch_l_joystick.fbx/touch_l_joystick.fbm/L_controller-highlight_DIF.jpg", + } + } + }, + + button_a: { + type: "linear", + modelURL: BASE_URL + "touch_l_button_x.fbx", + naturalPosition: { x: -0.009307309985160828, y: -0.00005015172064304352, z: -0.012594521045684814 }, + naturalDimensions: { x: 0.009861, y: 0.004345, z: 0.00982 }, + + input: "OculusTouch.X", + axis: { x: 0, y: -1, z: 0 }, + maxTranslation: 0.001, + + textureName: "tex-highlight", + defaultTextureLayer: "normal", + textureLayers: { + normal: { + defaultTextureURL: BASE_URL + "touch_l_button_x.fbx/touch_l_button_x.fbm/L_controller_DIF.jpg", + }, + highlight: { + defaultTextureURL: BASE_URL + "touch_l_button_x.fbx/touch_l_button_x.fbm/L_controller-highlight_DIF.jpg", + } + } + }, + + button_b: { + type: "linear", + modelURL: BASE_URL + "touch_l_button_y.fbx", + naturalPosition: { x: -0.01616849936544895, y: -0.000050364527851343155, z: 0.0017703399062156677 }, + naturalDimensions: { x: 0.010014, y: 0.004412, z: 0.009972 }, + + input: "OculusTouch.Y", + axis: { x: 0, y: -1, z: 0 }, + maxTranslation: 0.001, + + textureName: "tex-highlight", + defaultTextureLayer: "normal", + textureLayers: { + normal: { + defaultTextureURL: BASE_URL + "touch_l_button_y.fbx/touch_l_button_y.fbm/L_controller_DIF.jpg", + }, + highlight: { + defaultTextureURL: BASE_URL + "touch_l_button_y.fbx/touch_l_button_y.fbm/L_controller-highlight_DIF.jpg", + } + } + }, + } + } + ] +}; + +TOUCH_CONTROLLER_CONFIGURATION_RIGHT = { + name: "Touch", + controllers: [ + { + modelURL: BASE_URL + "touch_r_body.fbx", + jointIndex: MyAvatar.getJointIndex("_CAMERA_RELATIVE_CONTROLLER_RIGHTHAND"), + naturalPosition: { x: -0.016486231237649918, y: -0.03551865369081497, z: -0.018527653068304062 }, + dimensions: { x: 0.11053784191608429, y: 0.09957750141620636, z: 0.10139875113964081 }, + rotation: rightBaseRotation, + position: rightBasePosition, + + parts: { + tips: { + type: "static", + modelURL: BASE_URL + "Oculus-Labels-R.fbx", + naturalPosition: { x: 0.009739525616168976, y: -0.0017818436026573181, z: 0.016794726252555847 }, + naturalDimensions: { x: 0.129049, y: 0.078297, z: 0.139492 }, + + textureName: "blank", + defaultTextureLayer: "blank", + textureLayers: { + blank: { + defaultTextureURL: BASE_URL + "Oculus-Labels-R.fbx/Oculus-Labels-R.fbm/Blank.png" + }, + trigger: { + defaultTextureURL: BASE_URL + "Oculus-Labels-R.fbx/Oculus-Labels-R.fbm/Trigger.png" + }, + arrows: { + defaultTextureURL: BASE_URL + "Oculus-Labels-R.fbx/Oculus-Labels-R.fbm/Rotate.png" + }, + grip: { + defaultTextureURL: BASE_URL + "Oculus-Labels-R.fbx/Oculus-Labels-R.fbm/Grip-oculus.png" + }, + teleport: { + defaultTextureURL: BASE_URL + "Oculus-Labels-R.fbx/Oculus-Labels-R.fbm/Teleport.png" + }, + both_triggers: { + defaultTextureURL: BASE_URL + "Oculus-Labels-R.fbx/Oculus-Labels-R.fbm/Grip-Trigger.png" + }, + } + }, + + trigger: { + type: "rotational", + modelURL: BASE_URL + "touch_r_trigger.fbx", + naturalPosition: { x: -0.0008544912561774254, y: -0.019867943599820137, z: 0.018800459802150726 }, + naturalDimensions: { x: 0.027384, y: 0.025201, z: 0.018425 }, + + // rotational + input: "OculusTouch.RT", + origin: { x: 0, y: -0.015, z: 0 }, + minValue: 0.0, + maxValue: 1.0, + axis: { x: 1, y: 0, z: 0 }, + maxAngle: 17, + + textureName: "tex-highlight", + defaultTextureLayer: "normal", + textureLayers: { + normal: { + defaultTextureURL: BASE_URL + "touch_r_trigger.fbx/touch_r_trigger.fbm/R_controller_DIF.jpg", + }, + highlight: { + defaultTextureURL: BASE_URL + "touch_r_trigger.fbx/touch_r_trigger.fbm/R_controller-highlight_DIF.jpg", + } + } + }, + + grip: { + type: "linear", + modelURL: BASE_URL + "touch_r_bumper.fbx", + naturalPosition: { x: -0.0000806618481874466, y: -0.027157839387655258, z: -0.024485092610120773 }, + naturalDimensions: { x: 0.017268, y: 0.020366, z: 0.02599 }, + + // linear properties + // Offset from origin = 0.36470, 0.11048, 0.11066 + input: "OculusTouch.RightGrip", + axis: { x: -1, y: 0.302933918, z: 0.302933918 }, + maxTranslation: 0.003967, + + + textureName: "tex-highlight", + defaultTextureLayer: "normal", + textureLayers: { + normal: { + defaultTextureURL: BASE_URL + "touch_r_bumper.fbx/touch_r_bumper.fbm/R_controller_DIF.jpg", + }, + highlight: { + defaultTextureURL: BASE_URL + "touch_r_bumper.fbx/touch_r_bumper.fbm/R_controller-highlight_DIF.jpg", + } + } + }, + + joystick: { + type: "joystick", + modelURL: BASE_URL + "touch_r_joystick.fbx", + naturalPosition: { x: -0.007561382371932268, y: -0.008225853554904461, z: 0.00479268841445446 }, + naturalDimensions: { x: 0.027272, y: 0.033254, z: 0.027272 }, + + // joystick + xInput: "OculusTouch.RX", + yInput: "OculusTouch.RY", + originOffset: { x: 0, y: -0.0028564, z: 0 }, + xHalfAngle: 20, + yHalfAngle: 20, + + textureName: "tex-highlight", + defaultTextureLayer: "normal", + textureLayers: { + normal: { + defaultTextureURL: BASE_URL + "touch_r_joystick.fbx/touch_r_joystick.fbm/R_controller_DIF.jpg", + }, + highlight: { + defaultTextureURL: BASE_URL + "touch_r_joystick.fbx/touch_r_joystick.fbm/R_controller-highlight_DIF.jpg", + } + } + }, + + button_a: { + type: "linear", + modelURL: BASE_URL + "touch_r_button_a.fbx", + naturalPosition: { x: 0.009307296946644783, y: -0.00005015172064304352, z: -0.012594504281878471 }, + naturalDimensions: { x: 0.00982, y: 0.004345, z: 0.00982 }, + + input: "OculusTouch.A", + axis: { x: 0, y: -1, z: 0 }, + maxTranslation: 0.001, + + textureName: "tex-highlight", + defaultTextureLayer: "normal", + textureLayers: { + normal: { + defaultTextureURL: BASE_URL + "touch_r_button_a.fbx/touch_r_button_a.fbm/R_controller_DIF.jpg", + }, + highlight: { + defaultTextureURL: BASE_URL + "touch_r_button_a.fbx/touch_r_button_a.fbm/R_controller-highlight_DIF.jpg", + } + } + }, + + button_b: { + type: "linear", + modelURL: BASE_URL + "touch_r_button_b.fbx", + naturalPosition: { x: 0.01616847701370716, y: -0.000050364527851343155, z: 0.0017703361809253693 }, + naturalDimensions: { x: 0.009972, y: 0.004412, z: 0.009972 }, + + input: "OculusTouch.B", + axis: { x: 0, y: -1, z: 0 }, + maxTranslation: 0.001, + + textureName: "tex-highlight", + defaultTextureLayer: "normal", + textureLayers: { + normal: { + defaultTextureURL: BASE_URL + "touch_r_button_b.fbx/touch_r_button_b.fbm/R_controller_DIF.jpg", + }, + highlight: { + defaultTextureURL: BASE_URL + "touch_r_button_b.fbx/touch_r_button_b.fbm/R_controller-highlight_DIF.jpg", + } + } + }, + } + } + ] +}; diff --git a/scripts/simplifiedUI/system/controllers/viveControllerConfiguration.js b/scripts/simplifiedUI/system/controllers/viveControllerConfiguration.js new file mode 100644 index 0000000000..09fd8adacc --- /dev/null +++ b/scripts/simplifiedUI/system/controllers/viveControllerConfiguration.js @@ -0,0 +1,343 @@ +// +// viveControllerConfiguration.js +// +// Created by Anthony J. Thibault on 10/20/16 +// Originally created by Ryan Huffman on 9/21/2016 +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +/* globals VIVE_CONTROLLER_CONFIGURATION_LEFT:true, VIVE_CONTROLLER_CONFIGURATION_RIGHT:true, + MyAvatar, Quat, Script, Vec3, Controller */ +/* eslint camelcase: ["error", { "properties": "never" }] */ + +// var LEFT_JOINT_INDEX = MyAvatar.getJointIndex("_CONTROLLER_LEFTHAND"); +// var RIGHT_JOINT_INDEX = MyAvatar.getJointIndex("_CONTROLLER_RIGHTHAND"); + +var leftBaseRotation = Quat.multiply( + Quat.fromPitchYawRollDegrees(0, 0, 45), + Quat.multiply( + Quat.fromPitchYawRollDegrees(90, 0, 0), + Quat.fromPitchYawRollDegrees(0, 0, 90) + ) +); + +var rightBaseRotation = Quat.multiply( + Quat.fromPitchYawRollDegrees(0, 0, -45), + Quat.multiply( + Quat.fromPitchYawRollDegrees(90, 0, 0), + Quat.fromPitchYawRollDegrees(0, 0, -90) + ) +); + +// keep these in sync with the values from plugins/openvr/src/OpenVrHelpers.cpp:303 +var CONTROLLER_LATERAL_OFFSET = 0.0381; +var CONTROLLER_VERTICAL_OFFSET = 0.0495; +var CONTROLLER_FORWARD_OFFSET = 0.1371; +var leftBasePosition = { + x: CONTROLLER_VERTICAL_OFFSET, + y: CONTROLLER_FORWARD_OFFSET, + z: CONTROLLER_LATERAL_OFFSET +}; +var rightBasePosition = { + x: -CONTROLLER_VERTICAL_OFFSET, + y: CONTROLLER_FORWARD_OFFSET, + z: CONTROLLER_LATERAL_OFFSET +}; + +var viveNaturalDimensions = { + x: 0.1174320001155138, + y: 0.08361100335605443, + z: 0.21942697931081057 +}; + +var viveNaturalPosition = { + x: 0, + y: -0.034076502197422087, + z: 0.06380049744620919 +}; + +var BASE_URL = Script.resourcesPath(); +// var TIP_TEXTURE_BASE_URL = BASE_URL + "meshes/controller/vive_tips.fbm/"; + +var viveModelURL = BASE_URL + "meshes/controller/vive_body.fbx"; +// var viveTipsModelURL = BASE_URL + "meshes/controller/vive_tips.fbx"; +var viveTriggerModelURL = "meshes/controller/vive_trigger.fbx"; + +VIVE_CONTROLLER_CONFIGURATION_LEFT = { + name: "Vive", + controllers: [ + { + modelURL: viveModelURL, + jointIndex: MyAvatar.getJointIndex("_CAMERA_RELATIVE_CONTROLLER_LEFTHAND"), + naturalPosition: viveNaturalPosition, + rotation: leftBaseRotation, + position: Vec3.multiplyQbyV(Quat.fromPitchYawRollDegrees(0, 0, 45), leftBasePosition), + + dimensions: viveNaturalDimensions, + + parts: { + // DISABLED FOR NOW + /* + tips: { + type: "static", + modelURL: viveTipsModelURL, + naturalPosition: {"x":-0.004377640783786774,"y":-0.034371938556432724,"z":0.06769277155399323}, + naturalDimensions: {x: 0.191437, y: 0.094095, z: 0.085656}, + + textureName: "Tex.Blank", + defaultTextureLayer: "blank", + textureLayers: { + blank: { + defaultTextureURL: TIP_TEXTURE_BASE_URL + "/Blank.png" + }, + trigger: { + defaultTextureURL: TIP_TEXTURE_BASE_URL + "/Trigger.png" + }, + arrows: { + defaultTextureURL: TIP_TEXTURE_BASE_URL + "/Rotate.png" + }, + grip: { + defaultTextureURL: TIP_TEXTURE_BASE_URL + "/Grip.png" + }, + teleport: { + defaultTextureURL: TIP_TEXTURE_BASE_URL + "/Teleport.png" + } + } + }, + */ + + // The touchpad type draws a dot indicating the current touch/thumb position + // and swaps in textures based on the thumb position. + touchpad: { + type: "touchpad", + modelURL: BASE_URL + "meshes/controller/vive_trackpad.fbx", + visibleInput: "Vive.RSTouch", + xInput: "Vive.LX", + yInput: "Vive.LY", + naturalPosition: {"x":0,"y":0.000979491975158453,"z":0.04872849956154823}, + naturalDimensions: {x: 0.042824, y: 0.012537, z: 0.043115}, + minValue: 0.0, + maxValue: 1.0, + minPosition: { x: -0.035, y: 0.004, z: -0.005 }, + maxPosition: { x: -0.035, y: 0.004, z: -0.005 }, + disable_textureName: "Tex.touchpad-blank", + + disable_defaultTextureLayer: "blank", + disable_textureLayers: { + blank: { + defaultTextureURL: BASE_URL + "meshes/controller/vive_trackpad.fbx/Touchpad.fbm/touchpad-blank.jpg" + }, + teleport: { + defaultTextureURL: BASE_URL + "meshes/controller/vive_trackpad.fbx/Touchpad.fbm/touchpad-teleport-active-LG.jpg" + }, + arrows: { + defaultTextureURL: BASE_URL + "meshes/controller/vive_trackpad.fbx/Touchpad.fbm/touchpad-look-arrows.jpg" + } + } + }, + + trigger: { + type: "rotational", + modelURL: BASE_URL + "meshes/controller/vive_trigger.fbx", + input: Controller.Standard.LT, + naturalPosition: {"x":0.000004500150680541992,"y":-0.027690507471561432,"z":0.04830199480056763}, + naturalDimensions: {x: 0.019105, y: 0.022189, z: 0.01909}, + origin: { x: 0, y: -0.015, z: -0.00 }, + minValue: 0.0, + maxValue: 1.0, + axis: { x: -1, y: 0, z: 0 }, + maxAngle: 25, + + textureName: "Tex.black-trigger", + defaultTextureLayer: "normal", + textureLayers: { + normal: { + defaultTextureURL: BASE_URL + viveTriggerModelURL + "/Trigger.fbm/black.jpg" + }, + highlight: { + defaultTextureURL: BASE_URL + viveTriggerModelURL + "/Trigger.fbm/yellow.jpg" + } + } + }, + + l_grip: { + type: "static", + modelURL: BASE_URL + "meshes/controller/vive_l_grip.fbx", + naturalPosition: {"x":-0.01720449887216091,"y":-0.014324013143777847,"z":0.08714400231838226}, + naturalDimensions: {x: 0.010094, y: 0.015064, z: 0.029552} + }, + + r_grip: { + type: "static", + modelURL: BASE_URL + "meshes/controller/vive_r_grip.fbx", + naturalPosition: {"x":0.01720449887216091,"y":-0.014324013143777847,"z":0.08714400231838226}, + naturalDimensions: {x: 0.010083, y: 0.015064, z: 0.029552} + }, + + sys_button: { + type: "static", + modelURL: BASE_URL + "meshes/controller/vive_sys_button.fbx", + naturalPosition: {"x":0,"y":0.0020399854984134436,"z":0.08825899660587311}, + naturalDimensions: {x: 0.009986, y: 0.004282, z: 0.010264} + }, + + button: { + type: "static", + modelURL: BASE_URL + "meshes/controller/vive_button.fbx", + naturalPosition: {"x":0,"y":0.005480996798723936,"z":0.019918499514460564}, + naturalDimensions: {x: 0.009986, y: 0.004496, z: 0.010121} + }, + button2: { + type: "static", + modelURL: BASE_URL + "meshes/controller/vive_button.fbx", + naturalPosition: {"x":0,"y":0.005480996798723936,"z":0.019918499514460564}, + naturalDimensions: {x: 0.009986, y: 0.004496, z: 0.010121} + } + } + } + ] +}; + + +VIVE_CONTROLLER_CONFIGURATION_RIGHT = { + name: "Vive Right", + controllers: [ + { + modelURL: viveModelURL, + jointIndex: MyAvatar.getJointIndex("_CAMERA_RELATIVE_CONTROLLER_RIGHTHAND"), + rotation: rightBaseRotation, + position: Vec3.multiplyQbyV(Quat.fromPitchYawRollDegrees(0, 0, -45), rightBasePosition), + + dimensions: viveNaturalDimensions, + + naturalPosition: { + x: 0, + y: -0.034076502197422087, + z: 0.06380049744620919 + }, + + parts: { + // DISABLED FOR NOW + /* + tips: { + type: "static", + modelURL: viveTipsModelURL, + naturalPosition: {"x":-0.004377640783786774,"y":-0.034371938556432724,"z":0.06769277155399323}, + naturalDimensions: {x: 0.191437, y: 0.094095, z: 0.085656}, + + textureName: "Tex.Blank", + + defaultTextureLayer: "blank", + textureLayers: { + blank: { + defaultTextureURL: TIP_TEXTURE_BASE_URL + "/Blank.png" + }, + trigger: { + defaultTextureURL: TIP_TEXTURE_BASE_URL + "/Trigger.png" + }, + arrows: { + defaultTextureURL: TIP_TEXTURE_BASE_URL + "/Rotate.png" + }, + grip: { + defaultTextureURL: TIP_TEXTURE_BASE_URL + "/Grip.png" + }, + teleport: { + defaultTextureURL: TIP_TEXTURE_BASE_URL + "/Teleport.png" + } + } + }, + */ + + // The touchpad type draws a dot indicating the current touch/thumb position + // and swaps in textures based on the thumb position. + touchpad: { + type: "touchpad", + modelURL: BASE_URL + "meshes/controller/vive_trackpad.fbx", + visibleInput: "Vive.RSTouch", + xInput: "Vive.RX", + yInput: "Vive.RY", + naturalPosition: { x: 0, y: 0.000979491975158453, z: 0.04872849956154823 }, + naturalDimensions: {x: 0.042824, y: 0.012537, z: 0.043115}, + minValue: 0.0, + maxValue: 1.0, + minPosition: { x: -0.035, y: 0.004, z: -0.005 }, + maxPosition: { x: -0.035, y: 0.004, z: -0.005 }, + disable_textureName: "Tex.touchpad-blank", + + disable_defaultTextureLayer: "blank", + disable_textureLayers: { + blank: { + defaultTextureURL: BASE_URL + "meshes/controller/vive_trackpad.fbx/Touchpad.fbm/touchpad-blank.jpg" + }, + teleport: { + defaultTextureURL: BASE_URL + "meshes/controller/vive_trackpad.fbx/Touchpad.fbm/touchpad-teleport-active-LG.jpg" + }, + arrows: { + defaultTextureURL: BASE_URL + "meshes/controller/vive_trackpad.fbx/Touchpad.fbm/touchpad-look-arrows-active.jpg" + } + } + }, + + trigger: { + type: "rotational", + modelURL: BASE_URL + "meshes/controller/vive_trigger.fbx", + input: Controller.Standard.RT, + naturalPosition: {"x":0.000004500150680541992,"y":-0.027690507471561432,"z":0.04830199480056763}, + naturalDimensions: {x: 0.019105, y: 0.022189, z: 0.01909}, + origin: { x: 0, y: -0.015, z: -0.00 }, + minValue: 0.0, + maxValue: 1.0, + axis: { x: -1, y: 0, z: 0 }, + maxAngle: 25, + + textureName: "Tex.black-trigger", + defaultTextureLayer: "normal", + textureLayers: { + normal: { + defaultTextureURL: BASE_URL + viveTriggerModelURL + "/Trigger.fbm/black.jpg" + }, + highlight: { + defaultTextureURL: BASE_URL + viveTriggerModelURL + "/Trigger.fbm/yellow.jpg" + } + } + }, + + l_grip: { + type: "static", + modelURL: BASE_URL + "meshes/controller/vive_l_grip.fbx", + naturalPosition: {"x":-0.01720449887216091,"y":-0.014324013143777847,"z":0.08714400231838226}, + naturalDimensions: {x: 0.010094, y: 0.015064, z: 0.029552} + }, + + r_grip: { + type: "static", + modelURL: BASE_URL + "meshes/controller/vive_r_grip.fbx", + naturalPosition: {"x":0.01720449887216091,"y":-0.014324013143777847,"z":0.08714400231838226}, + naturalDimensions: {x: 0.010083, y: 0.015064, z: 0.029552} + }, + + sys_button: { + type: "static", + modelURL: BASE_URL + "meshes/controller/vive_sys_button.fbx", + naturalPosition: {"x":0,"y":0.0020399854984134436,"z":0.08825899660587311}, + naturalDimensions: {x: 0.009986, y: 0.004282, z: 0.010264} + }, + + button: { + type: "static", + modelURL: BASE_URL + "meshes/controller/vive_button.fbx", + naturalPosition: {"x":0,"y":0.005480996798723936,"z":0.019918499514460564}, + naturalDimensions: {x: 0.009986, y: 0.004496, z: 0.010121} + }, + button2: { + type: "static", + modelURL: BASE_URL + "meshes/controller/vive_button.fbx", + naturalPosition: {"x":0,"y":0.005480996798723936,"z":0.019918499514460564}, + naturalDimensions: {x: 0.009986, y: 0.004496, z: 0.010121} + } + } + } + ] +}; diff --git a/scripts/simplifiedUI/system/progress.js b/scripts/simplifiedUI/system/progress.js new file mode 100644 index 0000000000..b373612790 --- /dev/null +++ b/scripts/simplifiedUI/system/progress.js @@ -0,0 +1,387 @@ +"use strict"; + +// +// progress.js +// examples +// +// Created by David Rowe on 29 Jan 2015. +// Copyright 2015 High Fidelity, Inc. +// +// This script displays a progress download indicator when downloads are in progress. +// +// 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 debug() { + //print.apply(null, arguments); + } + + Script.include("/~/system/libraries/globals.js"); + var rawProgress = 100, // % raw value. + displayProgress = 100, // % smoothed value to display. + alpha = 0.0, + alphaDelta = 0.0, // > 0 if fading in; < 0 if fading out. + ALPHA_DELTA_IN = 0.15, + ALPHA_DELTA_OUT = -0.02, + fadeTimer = null, + FADE_INTERVAL = 30, // ms between changes in alpha. + fadeWaitTimer = null, + FADE_OUT_WAIT = 1000, // Wait before starting to fade out after progress 100%. + visible = false, + + BAR_DESKTOP_2K_WIDTH = 2240, // Width of SVG image in pixels. Sized for 1920 x 1080 display with 6 visible repeats. + BAR_DESKTOP_2K_REPEAT = 320, // Length of repeat in bar = 2240 / 7. + BAR_DESKTOP_2K_HEIGHT = 3, // Display height of SVG + BAR_DESKTOP_2K_URL = Script.resolvePath("assets/images/progress-bar-2k.svg"), + + BAR_DESKTOP_4K_WIDTH = 4480, // Width of SVG image in pixels. Sized for 4096 x 1920 display with 6 visible repeats. + BAR_DESKTOP_4K_REPEAT = 640, // Length of repeat in bar = 2240 / 7. + BAR_DESKTOP_4K_HEIGHT = 6, // Display height of SVG + BAR_DESKTOP_4K_URL = Script.resolvePath("assets/images/progress-bar-4k.svg"), + + BAR_HMD_WIDTH = 2240, // Desktop image works with HMD well. + BAR_HMD_REPEAT = 320, + BAR_HMD_HEIGHT = 3, + BAR_HMD_URL = Script.resolvePath("assets/images/progress-bar-2k.svg"), + + BAR_Y_OFFSET_DESKTOP = 0, // Offset of progress bar while in desktop mode + BAR_Y_OFFSET_HMD = -100, // Offset of progress bar while in HMD + + ANIMATION_SECONDS_PER_REPEAT = 4, // Speed of bar animation + + TEXT_HEIGHT = 32, + TEXT_WIDTH = 256, + TEXT_URL = Script.resolvePath("assets/images/progress-bar-text.svg"), + windowWidth = 0, + windowHeight = 0, + barDesktop = {}, + barHMD = {}, + textDesktop = {}, // Separate desktop and HMD overlays because can't change text size after overlay created. + textHMD = {}, + SCALE_TEXT_DESKTOP = 0.6, + SCALE_TEXT_HMD = 1.0, + isHMD = false, + + // Max seen since downloads started. This is reset when all downloads have completed. + maxSeen = 0, + + // Progress is defined as: (pending_downloads + active_downloads) / max_seen + // We keep track of both the current progress (rawProgress) and the + // best progress we've seen (bestRawProgress). As you are downloading, you may + // encounter new assets that require downloads, increasing the number of + // pending downloads and thus decreasing your overall progress. + bestRawProgress = 0, + + // True if we have known active downloads + isDownloading = false, + + // Entities are streamed to users, so you don't receive them all at once; instead, you + // receive them over a period of time. In many cases we end up in a situation where + // + // The initial delay cooldown keeps us from tracking progress before the allotted time + // has passed. + INITIAL_DELAY_COOLDOWN_TIME = 1000, + initialDelayCooldown = 0, + + isInInterstitialMode = false; + + function fade() { + + alpha = alpha + alphaDelta; + + if (alpha < 0) { + alpha = 0; + } else if (alpha > 1) { + alpha = 1; + } + + if (alpha === 0 || alpha === 1) { // Finished fading in or out + alphaDelta = 0; + Script.clearInterval(fadeTimer); + } + + if (alpha === 0) { // Finished fading out + visible = false; + } + + Overlays.editOverlay(barDesktop.overlay, { + alpha: alpha, + visible: visible && !isHMD + }); + Overlays.editOverlay(barHMD.overlay, { + alpha: alpha, + visible: visible && isHMD + }); + Overlays.editOverlay(textDesktop.overlay, { + alpha: alpha, + visible: visible && !isHMD + }); + Overlays.editOverlay(textHMD.overlay, { + alpha: alpha, + visible: visible && isHMD + }); + } + + Window.domainChanged.connect(function () { + isDownloading = false; + bestRawProgress = 100; + rawProgress = 100; + displayProgress = 100; + }); + + function onDownloadInfoChanged(info) { + + debug("PROGRESS: Download info changed ", info.downloading.length, info.pending, maxSeen); + + // Update raw progress value + if (info.downloading.length + info.pending === 0) { + isDownloading = false; + rawProgress = 100; + bestRawProgress = 100; + initialDelayCooldown = INITIAL_DELAY_COOLDOWN_TIME; + } else { + var count = info.downloading.length + info.pending; + if (!isDownloading) { + isDownloading = true; + bestRawProgress = 0; + rawProgress = 0; + initialDelayCooldown = INITIAL_DELAY_COOLDOWN_TIME; + displayProgress = 0; + maxSeen = count; + } + if (count > maxSeen) { + maxSeen = count; + } + if (initialDelayCooldown <= 0) { + rawProgress = ((maxSeen - count) / maxSeen) * 100; + + if (rawProgress > bestRawProgress) { + bestRawProgress = rawProgress; + } + } + } + debug("PROGRESS:", rawProgress, bestRawProgress, maxSeen); + } + + function createOverlays() { + barDesktop.overlay = Overlays.addOverlay("image", { + imageURL: barDesktop.url, + subImage: { + x: 0, + y: 0, + width: barDesktop.width - barDesktop.repeat, + height: barDesktop.height + }, + width: barDesktop.width, + height: barDesktop.height, + visible: false, + alpha: 0.0 + }); + barHMD.overlay = Overlays.addOverlay("image", { + imageURL: BAR_HMD_URL, + subImage: { + x: 0, + y: 0, + width: BAR_HMD_WIDTH - BAR_HMD_REPEAT, + height: BAR_HMD_HEIGHT + }, + width: barHMD.width, + height: barHMD.height, + visible: false, + alpha: 0.0 + }); + textDesktop.overlay = Overlays.addOverlay("image", { + imageURL: TEXT_URL, + width: textDesktop.width, + height: textDesktop.height, + visible: false, + alpha: 0.0 + }); + textHMD.overlay = Overlays.addOverlay("image", { + imageURL: TEXT_URL, + width: textHMD.width, + height: textHMD.height, + visible: false, + alpha: 0.0 + }); + } + + function deleteOverlays() { + Overlays.deleteOverlay(barDesktop.overlay); + Overlays.deleteOverlay(barHMD.overlay); + Overlays.deleteOverlay(textDesktop.overlay); + Overlays.deleteOverlay(textHMD.overlay); + } + + function updateProgressBarLocation() { + var viewport = Controller.getViewportDimensions(); + + windowWidth = viewport.x; + windowHeight = viewport.y; + isHMD = HMD.active; + + if (isHMD) { + + Overlays.editOverlay(barHMD.overlay, { + x: windowWidth / 2 - barHMD.width / 2, + y: windowHeight - 2 * barHMD.height + BAR_Y_OFFSET_HMD + }); + + Overlays.editOverlay(textHMD.overlay, { + x: windowWidth / 2 - textHMD.width / 2, + y: windowHeight - 2 * barHMD.height - textHMD.height + BAR_Y_OFFSET_HMD + }); + + } else { + + Overlays.editOverlay(barDesktop.overlay, { + x: windowWidth / 2 - barDesktop.width / 2, + y: windowHeight - 2 * barDesktop.height + BAR_Y_OFFSET_DESKTOP, + width: barDesktop.width + }); + + Overlays.editOverlay(textDesktop.overlay, { + x: windowWidth / 2 - textDesktop.width / 2, + y: windowHeight - 2 * barDesktop.height - textDesktop.height + BAR_Y_OFFSET_DESKTOP + }); + } + } + + function update() { + var viewport, diff, x, gpuTextures; + + initialDelayCooldown -= 30; + + if (displayProgress < rawProgress) { + diff = rawProgress - displayProgress; + if (diff < 0.5) { + displayProgress = rawProgress; + } else { + displayProgress += diff * 0.05; + } + } + + gpuTextures = Render.getConfig("Stats").texturePendingGPUTransferCount; + + // Update state + if (!visible) { // Not visible because no recent downloads + if ((displayProgress < 100 || gpuTextures > 0) && !isInInterstitialMode && !isInterstitialOverlaysVisible) { // Have started downloading so fade in + visible = true; + alphaDelta = ALPHA_DELTA_IN; + fadeTimer = Script.setInterval(fade, FADE_INTERVAL); + } + } else if (alphaDelta !== 0.0) { // Fading in or out + if (alphaDelta > 0) { + if (rawProgress === 100 && gpuTextures === 0) { // Was downloading but now have finished so fade out + alphaDelta = ALPHA_DELTA_OUT; + } + } else { + if (displayProgress < 100 || gpuTextures > 0) { // Was finished downloading but have resumed so fade in + alphaDelta = ALPHA_DELTA_IN; + } + } + } else { // Fully visible because downloading or recently so + if (fadeWaitTimer === null) { + if (rawProgress === 100 && gpuTextures === 0) { // Was downloading but have finished so fade out soon + fadeWaitTimer = Script.setTimeout(function () { + alphaDelta = ALPHA_DELTA_OUT; + fadeTimer = Script.setInterval(fade, FADE_INTERVAL); + fadeWaitTimer = null; + }, FADE_OUT_WAIT); + } + } else { + if (displayProgress < 100 || gpuTextures > 0) { // Was finished and waiting to fade out but have resumed so + // don't fade out + Script.clearInterval(fadeWaitTimer); + fadeWaitTimer = null; + } + } + } + + if (visible) { + x = ((Date.now() / 1000) % ANIMATION_SECONDS_PER_REPEAT) / ANIMATION_SECONDS_PER_REPEAT; + if (!isHMD) { + x = x * barDesktop.repeat; + } else { + x = x * BAR_HMD_REPEAT; + } + if (isInInterstitialMode || isInterstitialOverlaysVisible) { + visible = false; + } + + // Update progress bar + Overlays.editOverlay(barDesktop.overlay, { + visible: !isHMD && visible, + bounds: { + x: barDesktop.repeat - x, + y: windowHeight - barDesktop.height, + width: barDesktop.width - barDesktop.repeat, + height: barDesktop.height + } + }); + + Overlays.editOverlay(barHMD.overlay, { + visible: isHMD && visible, + bounds: { + x: BAR_HMD_REPEAT - x, + y: windowHeight - BAR_HMD_HEIGHT, + width: BAR_HMD_WIDTH - BAR_HMD_REPEAT, + height: BAR_HMD_HEIGHT + } + }); + + Overlays.editOverlay(textDesktop.overlay, { + visible: !isHMD && visible + }); + + Overlays.editOverlay(textHMD.overlay, { + visible: isHMD && visible + }); + + // Update 2D overlays to maintain positions at bottom middle of window + viewport = Controller.getViewportDimensions(); + + if (viewport.x !== windowWidth || viewport.y !== windowHeight || isHMD !== HMD.active) { + updateProgressBarLocation(); + } + } + } + + function interstitialModeChanged(inMode) { + isInInterstitialMode = inMode; + } + + function setUp() { + var is4k = Window.innerWidth > 3000; + + isHMD = HMD.active; + + barDesktop.width = is4k ? BAR_DESKTOP_4K_WIDTH - BAR_DESKTOP_4K_REPEAT : BAR_DESKTOP_2K_WIDTH - BAR_DESKTOP_2K_REPEAT; + barDesktop.height = is4k ? BAR_DESKTOP_4K_HEIGHT : BAR_DESKTOP_2K_HEIGHT; + barDesktop.repeat = is4k ? BAR_DESKTOP_4K_REPEAT : BAR_DESKTOP_2K_REPEAT; + barDesktop.url = is4k ? BAR_DESKTOP_4K_URL : BAR_DESKTOP_2K_URL; + barHMD.width = BAR_HMD_WIDTH - BAR_HMD_REPEAT; + barHMD.height = BAR_HMD_HEIGHT; + + textDesktop.width = SCALE_TEXT_DESKTOP * TEXT_WIDTH; + textDesktop.height = SCALE_TEXT_DESKTOP * TEXT_HEIGHT; + textHMD.width = SCALE_TEXT_HMD * TEXT_WIDTH; + textHMD.height = SCALE_TEXT_HMD * TEXT_HEIGHT; + + createOverlays(); + } + + function tearDown() { + deleteOverlays(); + } + + setUp(); + Window.interstitialModeChanged.connect(interstitialModeChanged); + GlobalServices.downloadInfoChanged.connect(onDownloadInfoChanged); + GlobalServices.updateDownloadInfo(); + Script.setInterval(update, 1000 / 60); + Script.scriptEnding.connect(tearDown); + +}()); // END LOCAL_SCOPE diff --git a/scripts/simplifiedUI/system/request-service.js b/scripts/simplifiedUI/system/request-service.js new file mode 100644 index 0000000000..b57f2d4cd7 --- /dev/null +++ b/scripts/simplifiedUI/system/request-service.js @@ -0,0 +1,48 @@ +"use strict"; +// +// request-service.js +// +// Created by Howard Stearns on May 22, 2018 +// Copyright 2018 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 + + // QML has its own XMLHttpRequest, but: + // - npm request is easier to use. + // - It is not easy to hack QML's XMLHttpRequest to use our MetaverseServer, and to supply the user's auth when contacting it. + // a. Our custom XMLHttpRequestClass object only works with QScriptEngine, not QML's javascript. + // b. We have hacked profiles that intercept requests to our MetavserseServer (providing the correct auth), but those + // only work in QML WebEngineView. Setting up communication between ordinary QML and a hiddent WebEngineView is + // tantamount to the following anyway, and would still have to duplicate the code from request.js. + + // So, this script does two things: + // 1. Allows any root .qml to signal sendToScript({id: aString, method: 'http.request', params: byNameOptions}) + // We will then asynchonously call fromScript({id: theSameString, method: 'http.response', error: errorOrFalsey, response: body}) + // on that root object. + // RootHttpRequest.qml does this. + // 2. If the uri used (computed from byNameOptions, see request.js) is to our metaverse, we will use the appropriate auth. + + var request = Script.require('request').request; + var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); + function fromQml(message) { // messages are {id, method, params}, like json-rpc. See also sendToQml. + switch (message.method) { + case 'http.request': + request(message.params, function (error, response) { + tablet.sendToQml({ + id: message.id, + method: 'http.response', + error: error, // Alas, this isn't always a JSON-RPC conforming error object. + response: response, + jsonrpc: '2.0' + }); + }); + break; + } + } + tablet.fromQml.connect(fromQml); + Script.scriptEnding.connect(function () { tablet.fromQml.disconnect(fromQml); }); +}()); // END LOCAL_SCOPE diff --git a/scripts/system/simplifiedUI/images/inputDeviceMuted.svg b/scripts/simplifiedUI/ui/images/inputDeviceMuted.svg similarity index 100% rename from scripts/system/simplifiedUI/images/inputDeviceMuted.svg rename to scripts/simplifiedUI/ui/images/inputDeviceMuted.svg diff --git a/scripts/system/simplifiedUI/images/outputDeviceMuted.svg b/scripts/simplifiedUI/ui/images/outputDeviceMuted.svg similarity index 100% rename from scripts/system/simplifiedUI/images/outputDeviceMuted.svg rename to scripts/simplifiedUI/ui/images/outputDeviceMuted.svg diff --git a/scripts/system/simplifiedNametag/resources/modules/defaultLocalEntityProps.js b/scripts/simplifiedUI/ui/simplifiedNametag/resources/modules/defaultLocalEntityProps.js similarity index 100% rename from scripts/system/simplifiedNametag/resources/modules/defaultLocalEntityProps.js rename to scripts/simplifiedUI/ui/simplifiedNametag/resources/modules/defaultLocalEntityProps.js diff --git a/scripts/system/simplifiedNametag/resources/modules/entityMaker.js b/scripts/simplifiedUI/ui/simplifiedNametag/resources/modules/entityMaker.js similarity index 100% rename from scripts/system/simplifiedNametag/resources/modules/entityMaker.js rename to scripts/simplifiedUI/ui/simplifiedNametag/resources/modules/entityMaker.js diff --git a/scripts/system/simplifiedNametag/resources/modules/nameTagListManager.js b/scripts/simplifiedUI/ui/simplifiedNametag/resources/modules/nameTagListManager.js similarity index 93% rename from scripts/system/simplifiedNametag/resources/modules/nameTagListManager.js rename to scripts/simplifiedUI/ui/simplifiedNametag/resources/modules/nameTagListManager.js index 98f73456d6..f8b6f2e6cd 100644 --- a/scripts/system/simplifiedNametag/resources/modules/nameTagListManager.js +++ b/scripts/simplifiedUI/ui/simplifiedNametag/resources/modules/nameTagListManager.js @@ -10,16 +10,6 @@ // Helps manage the list of avatars added to the nametag list // - -var ON = 'ON'; -var OFF = 'OFF'; -var DEBUG_ON = true; -var DEBUG_OFF = false; -var log = Script.require( - 'https://hifi-content.s3.amazonaws.com/milad/ROLC/d/ROLC_High-Fidelity/02_Organize/O_Projects/Repos/hifi-content/developerTools/sharedLibraries/easyLog/easyLog.js?' - + Date.now())(DEBUG_OFF, 'nameTagListManager.js'); - - var EntityMaker = Script.require('./entityMaker.js?' + Date.now()); var entityProps = Script.require('./defaultLocalEntityProps.js?' + Date.now()); var textHelper = new (Script.require('./textHelper.js?' + Date.now())); @@ -63,13 +53,12 @@ function maybeClearInterval() { var Z_SIZE = 0.01; var LINE_HEIGHT_SCALER = 0.99; var DISTANCE_SCALER_ON = 0.35; -var DISTANCE_SCALER_ALWAYS_ON = 0.65; +var DISTANCE_SCALER_ALWAYS_ON = 0.45; var distanceScaler = DISTANCE_SCALER_ON; var userScaler = 1.0; var DEFAULT_LINE_HEIGHT = entityProps.lineHeight; function calculateInitialProperties(uuid) { var avatar = _this.avatars[uuid]; - var avatarInfo = avatar.avatarInfo; var adjustedScaler = null; var distance = null; @@ -79,7 +68,7 @@ function calculateInitialProperties(uuid) { var name = null; // Handle if we are asking for the main or sub properties - name = avatarInfo.displayName; + name = getCorrectName(uuid); // Use the text helper to calculate what our dimensions for the text box should be textHelper @@ -244,6 +233,25 @@ function nameTagListManager() { } +// Get the correct display name +function getCorrectName(uuid) { + var avatar = _this.avatars[uuid]; + var avatarInfo = avatar.avatarInfo; + + var displayNameToUse = avatarInfo.sessionDisplayName.trim(); + + if (displayNameToUse === "") { + displayNameToUse = avatarInfo.displayName.trim(); + } + + if (displayNameToUse === "") { + displayNameToUse = "anonymous"; + } + + return displayNameToUse; +} + + // Create or make visible either the sub or the main tag. var REDRAW_TIMEOUT_AMOUNT_MS = 500; var LEFT_MARGIN_SCALER = 0.15; @@ -251,20 +259,19 @@ var RIGHT_MARGIN_SCALER = 0.10; var TOP_MARGIN_SCALER = 0.07; var BOTTOM_MARGIN_SCALER = 0.03; var ABOVE_HEAD_OFFSET = 0.30; -var DISTANCE_SCALER_INTERPOLATION_OFFSET_ALWAYSON = 25; +var DISTANCE_SCALER_INTERPOLATION_OFFSET_ALWAYSON = 15; var DISTANCE_SCALER_INTERPOLATION_OFFSET_ON = 10; var maxDistance = MAX_RADIUS_IGNORE_METERS; -var onModeScalar = 0.60; -var alwaysOnModeScalar = -0.55; +var onModeScalar = 0.55; +var alwaysOnModeScalar = -0.75; function makeNameTag(uuid) { var avatar = _this.avatars[uuid]; - var avatarInfo = avatar.avatarInfo; var nametag = avatar.nametag; - // Make sure an anonymous name is covered before sending to calculate - - avatarInfo.displayName = avatarInfo.displayName === "" ? "anonymous" : avatarInfo.displayName.trim(); - avatar.previousName = avatarInfo.displayName; + // Get the correct name for the nametag + var name = getCorrectName(uuid); + avatar.previousName = name; + nametag.add("text", name); // Returns back the properties we need based on what we are looking for and the distance from the avatar var calculatedProps = calculateInitialProperties(uuid); @@ -272,14 +279,12 @@ function makeNameTag(uuid) { var scaledDimensions = calculatedProps.scaledDimensions; var lineHeight = calculatedProps.lineHeight; - // Capture the inital dimensions, distance, and displayName in case we need to redraw - avatar.previousDisplayName = avatarInfo.displayName; + // Capture the inital dimensions and distance in case we need to redraw avatar.initialDimensions = scaledDimensions; avatar.initialDistance = distance; - var name = avatarInfo.displayName; + var parentID = uuid; - nametag.add("text", name); // Multiply the new dimensions and line height with the user selected scaler scaledDimensions = Vec3.multiply(scaledDimensions, userScaler); @@ -334,12 +339,11 @@ function makeNameTag(uuid) { // Check to see if the display named changed or if the distance is big enough to need a redraw. var MAX_RADIUS_IGNORE_METERS = 22; -var MAX_ON_MODE_DISTANCE = 30; +var MAX_ON_MODE_DISTANCE = 35; var CHECK_AVATAR = true; var MIN_DISTANCE = 0.2; function maybeRedraw(uuid) { var avatar = _this.avatars[uuid]; - var avatarInfo = avatar.avatarInfo; getAvatarData(uuid); getDistance(uuid); @@ -352,10 +356,10 @@ function maybeRedraw(uuid) { showHide(uuid, "show"); } - avatarInfo.displayName = avatarInfo.displayName === "" ? "anonymous" : avatarInfo.displayName.trim(); + var name = getCorrectName(uuid); - if (avatar.previousName !== avatarInfo.displayName) { - updateName(uuid, avatarInfo.displayName); + if (avatar.previousName !== name) { + updateName(uuid, name); } else { redraw(uuid); } diff --git a/scripts/system/simplifiedNametag/resources/modules/objectAssign.js b/scripts/simplifiedUI/ui/simplifiedNametag/resources/modules/objectAssign.js similarity index 100% rename from scripts/system/simplifiedNametag/resources/modules/objectAssign.js rename to scripts/simplifiedUI/ui/simplifiedNametag/resources/modules/objectAssign.js diff --git a/scripts/system/simplifiedNametag/resources/modules/pickRayController.js b/scripts/simplifiedUI/ui/simplifiedNametag/resources/modules/pickRayController.js similarity index 100% rename from scripts/system/simplifiedNametag/resources/modules/pickRayController.js rename to scripts/simplifiedUI/ui/simplifiedNametag/resources/modules/pickRayController.js diff --git a/scripts/system/simplifiedNametag/resources/modules/textHelper.js b/scripts/simplifiedUI/ui/simplifiedNametag/resources/modules/textHelper.js similarity index 100% rename from scripts/system/simplifiedNametag/resources/modules/textHelper.js rename to scripts/simplifiedUI/ui/simplifiedNametag/resources/modules/textHelper.js diff --git a/scripts/system/simplifiedNametag/simplifiedNametag.js b/scripts/simplifiedUI/ui/simplifiedNametag/simplifiedNametag.js similarity index 99% rename from scripts/system/simplifiedNametag/simplifiedNametag.js rename to scripts/simplifiedUI/ui/simplifiedNametag/simplifiedNametag.js index 75379f4e02..9b4d9cfad3 100644 --- a/scripts/system/simplifiedNametag/simplifiedNametag.js +++ b/scripts/simplifiedUI/ui/simplifiedNametag/simplifiedNametag.js @@ -8,7 +8,6 @@ // // Click on someone to get a nametag for them // - var PickRayController = Script.require('./resources/modules/pickRayController.js?' + Date.now()); var NameTagListManager = Script.require('./resources/modules/nameTagListManager.js?' + Date.now()); var pickRayController = new PickRayController(); diff --git a/scripts/simplifiedUI/ui/simplifiedStatusIndicator/simplifiedStatusIndicator.js b/scripts/simplifiedUI/ui/simplifiedStatusIndicator/simplifiedStatusIndicator.js new file mode 100644 index 0000000000..9968260034 --- /dev/null +++ b/scripts/simplifiedUI/ui/simplifiedStatusIndicator/simplifiedStatusIndicator.js @@ -0,0 +1,243 @@ +// +// simplifiedStatusIndicator.js +// +// Created by Robin Wilson on 2019-05-17 +// Copyright 2019 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 simplifiedStatusIndicator(properties) { + var that = this; + var DEBUG = false; + + // #region HEARTBEAT + + // Send heartbeat with updates to database + // When this stops, the database will set status to offline + var HEARTBEAT_TIMEOUT_MS = 5000, + heartbeat; + function startHeartbeatTimer() { + if (heartbeat) { + Script.clearTimeout(heartbeat); + heartbeat = false; + } + + heartbeat = Script.setTimeout(function() { + heartbeat = false; + getStatus(setStatus); + }, HEARTBEAT_TIMEOUT_MS); + } + + // #endregion HEARTBEAT + + + // #region SEND/GET STATUS REQUEST + + function setStatusExternally(newStatus) { + if (!newStatus) { + return; + } + + setStatus(newStatus); + } + + + var request = Script.require('request').request, + REQUEST_URL = "https://highfidelity.co/api/statusIndicator/"; + function setStatus(newStatus) { + if (heartbeat) { + Script.clearTimeout(heartbeat); + heartbeat = false; + } + + if (newStatus && currentStatus !== newStatus) { + currentStatus = newStatus; + that.statusChanged(); + } + + var queryParamString = "type=heartbeat"; + queryParamString += "&username=" + AccountServices.username; + + var displayNameToSend = MyAvatar.sessionDisplayName; + if (displayNameToSend === "") { + displayNameToSend = MyAvatar.displayName; + } + queryParamString += "&displayName=" + displayNameToSend; + queryParamString += "&status=" + currentStatus; + queryParamString += "&organization=" + location.hostname; + + var uri = REQUEST_URL + "?" + queryParamString; + + if (DEBUG) { + console.log("simplifiedStatusIndicator: setStatus: " + uri); + } + + request({ + uri: uri + }, function (error, response) { + startHeartbeatTimer(); + + if (error || !response || response.status !== "success") { + console.error("Error with setStatus: " + JSON.stringify(response)); + return; + } + }); + } + + // Get status from database + function getStatus(callback) { + var queryParamString = "type=getStatus"; + queryParamString += "&username=" + AccountServices.username; + + var uri = REQUEST_URL + "?" + queryParamString; + + if (DEBUG) { + console.log("simplifiedStatusIndicator: getStatus: " + uri); + } + + request({ + uri: uri + }, function (error, response) { + if (error || !response || response.status !== "success") { + console.error("Error with getStatus: " + JSON.stringify(response)); + } else if (response.data.userStatus.toLowerCase() !== "offline") { + if (response.data.userStatus !== currentStatus) { + currentStatus = response.data.userStatus; + that.statusChanged(); + } + } + + if (callback) { + callback(); + } + }); + } + + + function getLocalStatus() { + return currentStatus; + } + + // #endregion SEND/GET STATUS REQUEST + + + // #region SIGNALS + + var currentStatus = "available"; // Default is available + function toggleStatus() { + if (currentStatus === "busy") { + currentStatus = "available"; + // Else if current status is "available" OR anything else (custom) + } else { + currentStatus = "busy"; + } + + that.statusChanged(); + + setStatus(); + } + + + // When avatar becomes active from being away + // Set status back to previousStatus + function onWentActive() { + if (currentStatus !== previousStatus) { + currentStatus = previousStatus; + that.statusChanged(); + } + setStatus(); + } + + + // When avatar goes away, set status to busy + var previousStatus; + function onWentAway() { + previousStatus = currentStatus; + if (currentStatus !== "busy") { + currentStatus = "busy"; + that.statusChanged(); + } + setStatus(); + } + + + // Domain changed update avatar location + function onDomainChanged() { + var queryParamString = "type=updateEmployee"; + queryParamString += "&username=" + AccountServices.username; + queryParamString += "&location=unknown"; + + var uri = REQUEST_URL + "?" + queryParamString; + + if (DEBUG) { + console.log("simplifiedStatusIndicator: onDomainChanged: " + uri); + } + + request({ + uri: uri + }, function (error, response) { + if (error || !response || response.status !== "success") { + console.error("Error with onDomainChanged: " + JSON.stringify(response)); + } else { + // successfully sent updateLocation + if (DEBUG) { + console.log("simplifiedStatusIndicator: Successfully updated location after domain change."); + } + } + }); + } + + + function statusChanged() { + + } + + // #endregion SIGNALS + + + // #region APP LIFETIME + + // Creates the app button and sets up signals and hearbeat + function startup() { + MyAvatar.wentAway.connect(onWentAway); + MyAvatar.wentActive.connect(onWentActive); + MyAvatar.displayNameChanged.connect(setStatus); + Window.domainChanged.connect(onDomainChanged); + + getStatus(setStatus); + } + + + // Cleans up timeouts, signals, and overlays + function unload() { + MyAvatar.wentAway.disconnect(onWentAway); + MyAvatar.wentActive.disconnect(onWentActive); + MyAvatar.displayNameChanged.disconnect(setStatus); + Window.domainChanged.disconnect(onDomainChanged); + if (heartbeat) { + Script.clearTimeout(heartbeat); + heartbeat = false; + } + } + + // #endregion APP LIFETIME + + that.startup = startup; + that.unload = unload; + that.toggleStatus = toggleStatus; + that.setStatus = setStatus; + that.getLocalStatus = getLocalStatus; + that.statusChanged = statusChanged; + + // Overwrite with the given properties + var overwriteableKeys = ["statusChanged"]; + Object.keys(properties).forEach(function (key) { + if (overwriteableKeys.indexOf(key) > -1) { + that[key] = properties[key]; + } + }); +} + +module.exports = simplifiedStatusIndicator; \ No newline at end of file diff --git a/scripts/system/simplifiedUI/simplifiedUI.js b/scripts/simplifiedUI/ui/simplifiedUI.js similarity index 77% rename from scripts/system/simplifiedUI/simplifiedUI.js rename to scripts/simplifiedUI/ui/simplifiedUI.js index c6181d2ad9..a6183c5ab9 100644 --- a/scripts/system/simplifiedUI/simplifiedUI.js +++ b/scripts/simplifiedUI/ui/simplifiedUI.js @@ -5,7 +5,7 @@ // simplifiedUI.js // // Authors: Wayne Chen & Zach Fox -// Created on: 5/1/2019 +// Created: 2019-05-01 // Copyright 2019 High Fidelity, Inc. // // Distributed under the Apache License, Version 2.0. @@ -45,7 +45,8 @@ function runNewDefaultsTogether() { // Uncomment this out once the work is actually complete. // Until then, users are required to access some functionality from the top menu bar. //var MENU_NAMES = ["File", "Edit", "Display", "View", "Navigate", "Settings", "Developer", "Help"]; -var MENU_NAMES = ["File", "Edit", "View", "Navigate", "Help"]; +var MENU_NAMES = ["File", "Edit", "Display", "View", "Navigate", "Help", + "Settings > General...", "Settings > Controls...", "Settings > Audio...", "Settings > Graphics...", "Settings > Security..."]; var keepMenusSetting = Settings.getValue("simplifiedUI/keepMenus", false); function maybeRemoveDesktopMenu() { if (!keepMenusSetting) { @@ -102,6 +103,8 @@ var AVATAR_APP_PRESENTATION_MODE = Desktop.PresentationMode.NATIVE; var AVATAR_APP_WIDTH_PX = 480; var AVATAR_APP_HEIGHT_PX = 615; var avatarAppWindow = false; +var POPOUT_SAFE_MARGIN_X = 30; +var POPOUT_SAFE_MARGIN_Y = 30; function toggleAvatarApp() { if (avatarAppWindow) { avatarAppWindow.close(); @@ -118,6 +121,10 @@ function toggleAvatarApp() { size: { x: AVATAR_APP_WIDTH_PX, y: AVATAR_APP_HEIGHT_PX + }, + position: { + x: Math.max(Window.x + POPOUT_SAFE_MARGIN_X, Window.x + Window.innerWidth / 2 - AVATAR_APP_WIDTH_PX / 2), + y: Math.max(Window.y + POPOUT_SAFE_MARGIN_Y, Window.y + Window.innerHeight / 2 - AVATAR_APP_HEIGHT_PX / 2) } }); @@ -180,6 +187,10 @@ function toggleSettingsApp() { size: { x: SETTINGS_APP_WIDTH_PX, y: SETTINGS_APP_HEIGHT_PX + }, + position: { + x: Math.max(Window.x + POPOUT_SAFE_MARGIN_X, Window.x + Window.innerWidth / 2 - SETTINGS_APP_WIDTH_PX / 2), + y: Math.max(Window.y + POPOUT_SAFE_MARGIN_Y, Window.y + Window.innerHeight / 2 - SETTINGS_APP_HEIGHT_PX / 2) } }); @@ -237,40 +248,57 @@ function updateOutputDeviceMutedOverlay(isMuted) { } -var savedAvatarGain = Audio.getAvatarGain(); -var savedInjectorGain = Audio.getInjectorGain(); -var savedLocalInjectorGain = Audio.getLocalInjectorGain(); -var savedSystemInjectorGain = Audio.getSystemInjectorGain(); +var savedAvatarGain = Audio.avatarGain; +var savedServerInjectorGain = Audio.serverInjectorGain; +var savedLocalInjectorGain = Audio.localInjectorGain; +var savedSystemInjectorGain = Audio.systemInjectorGain; +var MUTED_VALUE_DB = -60; // This should always match `SimplifiedConstants.qml` -> numericConstants -> mutedValue! function setOutputMuted(outputMuted) { - updateOutputDeviceMutedOverlay(outputMuted); - if (outputMuted) { - savedAvatarGain = Audio.getAvatarGain(); - savedInjectorGain = Audio.getInjectorGain(); - savedLocalInjectorGain = Audio.getLocalInjectorGain(); - savedSystemInjectorGain = Audio.getSystemInjectorGain(); + savedAvatarGain = Audio.avatarGain; + savedServerInjectorGain = Audio.serverInjectorGain; + savedLocalInjectorGain = Audio.localInjectorGain; + savedSystemInjectorGain = Audio.systemInjectorGain; - Audio.setAvatarGain(-60); - Audio.setInjectorGain(-60); - Audio.setLocalInjectorGain(-60); - Audio.setSystemInjectorGain(-60); + Audio.avatarGain = MUTED_VALUE_DB; + Audio.serverInjectorGain = MUTED_VALUE_DB; + Audio.localInjectorGain = MUTED_VALUE_DB; + Audio.systemInjectorGain = MUTED_VALUE_DB; } else { - if (savedAvatarGain === -60) { + if (savedAvatarGain === MUTED_VALUE_DB) { savedAvatarGain = 0; } - Audio.setAvatarGain(savedAvatarGain); - if (savedInjectorGain === -60) { - savedInjectorGain = 0; + Audio.avatarGain = savedAvatarGain; + if (savedServerInjectorGain === MUTED_VALUE_DB) { + savedServerInjectorGain = 0; } - Audio.setInjectorGain(savedInjectorGain); - if (savedLocalInjectorGain === -60) { + Audio.serverInjectorGain = savedServerInjectorGain; + if (savedLocalInjectorGain === MUTED_VALUE_DB) { savedLocalInjectorGain = 0; } - Audio.setLocalInjectorGain(savedLocalInjectorGain); - if (savedSystemInjectorGain === -60) { + Audio.localInjectorGain = savedLocalInjectorGain; + if (savedSystemInjectorGain === MUTED_VALUE_DB) { savedSystemInjectorGain = 0; } - Audio.setSystemInjectorGain(savedSystemInjectorGain); + Audio.systemInjectorGain = savedSystemInjectorGain; + } +} + + +var WAIT_FOR_TOP_BAR_MS = 1000; +function sendLocalStatusToQml() { + var currentStatus = si.getLocalStatus(); + + if (topBarWindow && currentStatus) { + topBarWindow.sendToQml({ + "source": "simplifiedUI.js", + "method": "updateStatusButton", + "data": { + "currentStatus": currentStatus + } + }); + } else { + Script.setTimeout(sendLocalStatusToQml, WAIT_FOR_TOP_BAR_MS); } } @@ -294,6 +322,10 @@ function onMessageFromTopBar(message) { setOutputMuted(message.data.outputMuted); break; + case "toggleStatus": + si.toggleStatus(); + break; + default: console.log("Unrecognized message from " + TOP_BAR_MESSAGE_SOURCE + ": " + JSON.stringify(message)); break; @@ -311,7 +343,10 @@ function onTopBarClosed() { function isOutputMuted() { - return Audio.getAvatarGain() === -60 && Audio.getInjectorGain() === -60 && Audio.getLocalInjectorGain() === -60 && Audio.getSystemInjectorGain() === -60; + return Audio.avatarGain === MUTED_VALUE_DB && + Audio.serverInjectorGain === MUTED_VALUE_DB && + Audio.localInjectorGain === MUTED_VALUE_DB && + Audio.systemInjectorGain === MUTED_VALUE_DB; } @@ -345,13 +380,11 @@ function loadSimplifiedTopBar() { topBarWindow.fromQml.connect(onMessageFromTopBar); topBarWindow.closed.connect(onTopBarClosed); - topBarWindow.sendToQml({ - "source": "simplifiedUI.js", - "method": "updateOutputMuted", - "data": { - "outputMuted": isOutputMuted() - } - }) + // The eventbridge takes a nonzero time to initialize, so we have to wait a bit + // for the QML to load and for that to happen before updating the UI. + Script.setTimeout(function() { + sendLocalStatusToQml(); + }, WAIT_FOR_TOP_BAR_MS); } @@ -435,7 +468,19 @@ function ensureFirstPersonCameraInHMD(isHMDMode) { } -var simplifiedNametag = Script.require("../simplifiedNametag/simplifiedNametag.js"); +function onStatusChanged() { + sendLocalStatusToQml(); +} + + +function maybeUpdateOutputDeviceMutedOverlay() { + updateOutputDeviceMutedOverlay(isOutputMuted()); +} + + +var simplifiedNametag = Script.require("./simplifiedNametag/simplifiedNametag.js?" + Date.now()); +var SimplifiedStatusIndicator = Script.require("./simplifiedStatusIndicator/simplifiedStatusIndicator.js?" + Date.now()); +var si; var oldShowAudioTools; var oldShowBubbleTools; var keepExistingUIAndScriptsSetting = Settings.getValue("simplifiedUI/keepExistingUIAndScripts", false); @@ -456,11 +501,19 @@ function startup() { loadSimplifiedTopBar(); simplifiedNametag.create(); + si = new SimplifiedStatusIndicator({ + statusChanged: onStatusChanged + }); + si.startup(); updateInputDeviceMutedOverlay(Audio.muted); updateOutputDeviceMutedOverlay(isOutputMuted()); Audio.mutedDesktopChanged.connect(onDesktopInputDeviceMutedChanged); Window.geometryChanged.connect(onGeometryChanged); HMD.displayModeChanged.connect(ensureFirstPersonCameraInHMD); + Audio.avatarGainChanged.connect(maybeUpdateOutputDeviceMutedOverlay); + Audio.localInjectorGainChanged.connect(maybeUpdateOutputDeviceMutedOverlay); + Audio.serverInjectorGainChanged.connect(maybeUpdateOutputDeviceMutedOverlay); + Audio.systemInjectorGainChanged.connect(maybeUpdateOutputDeviceMutedOverlay); oldShowAudioTools = AvatarInputs.showAudioTools; AvatarInputs.showAudioTools = false; @@ -506,10 +559,15 @@ function shutdown() { maybeDeleteOutputDeviceMutedOverlay(); simplifiedNametag.destroy(); + si.unload(); Audio.mutedDesktopChanged.disconnect(onDesktopInputDeviceMutedChanged); Window.geometryChanged.disconnect(onGeometryChanged); HMD.displayModeChanged.disconnect(ensureFirstPersonCameraInHMD); + Audio.avatarGainChanged.disconnect(maybeUpdateOutputDeviceMutedOverlay); + Audio.localInjectorGainChanged.disconnect(maybeUpdateOutputDeviceMutedOverlay); + Audio.serverInjectorGainChanged.disconnect(maybeUpdateOutputDeviceMutedOverlay); + Audio.systemInjectorGainChanged.disconnect(maybeUpdateOutputDeviceMutedOverlay); AvatarInputs.showAudioTools = oldShowAudioTools; AvatarInputs.showBubbleTools = oldShowBubbleTools; diff --git a/scripts/system/assets/data/createAppTooltips.json b/scripts/system/assets/data/createAppTooltips.json index cc622c2184..b59797fca7 100644 --- a/scripts/system/assets/data/createAppTooltips.json +++ b/scripts/system/assets/data/createAppTooltips.json @@ -492,6 +492,9 @@ "canCastShadow": { "tooltip": "If enabled, this geometry of this entity casts shadows when a shadow-casting light source shines on it." }, + "ignorePickIntersection": { + "tooltip": "If enabled, this entity will not be considered for ray picks, and will also not occlude other entities when picking." + }, "parentID": { "tooltip": "The ID of the entity or avatar that this entity is parented to." }, diff --git a/scripts/system/html/js/entityProperties.js b/scripts/system/html/js/entityProperties.js index d7800ada5d..e64543d41f 100644 --- a/scripts/system/html/js/entityProperties.js +++ b/scripts/system/html/js/entityProperties.js @@ -1329,7 +1329,7 @@ const GROUPS = [ propertyID: "grab.grabFollowsController", }, { - label: "Cast shadows", + label: "Cast Shadows", type: "bool", propertyID: "canCastShadow", }, @@ -1339,6 +1339,11 @@ const GROUPS = [ propertyID: "href", placeholder: "URL", }, + { + label: "Ignore Pick Intersection", + type: "bool", + propertyID: "ignorePickIntersection", + }, { label: "Script", type: "string", diff --git a/tools/ci-scripts/postbuild.py b/tools/ci-scripts/postbuild.py new file mode 100644 index 0000000000..ab01e7c795 --- /dev/null +++ b/tools/ci-scripts/postbuild.py @@ -0,0 +1,134 @@ +# Post build script +import os +import sys +import shutil +import zipfile + +SOURCE_PATH = os.path.abspath(os.path.join(os.path.dirname(sys.argv[0]), '..', '..')) +# FIXME move the helper python modules somewher other than the root of the repo +sys.path.append(SOURCE_PATH) + +import hifi_utils + +BUILD_PATH = os.path.join(SOURCE_PATH, 'build') +INTERFACE_BUILD_PATH = os.path.join(BUILD_PATH, 'interface', 'Release') +WIPE_PATHS = [] + +if sys.platform == "win32": + WIPE_PATHS = [ + 'jsdoc', + 'resources/serverless' + ] +elif sys.platform == "darwin": + INTERFACE_BUILD_PATH = os.path.join(INTERFACE_BUILD_PATH, "Interface.app", "Contents", "Resources") + WIPE_PATHS = [ + 'jsdoc', + 'serverless' + ] + + + +# Customize the output filename +def computeArchiveName(): + RELEASE_TYPE = os.getenv("RELEASE_TYPE", "DEV") + RELEASE_NUMBER = os.getenv("RELEASE_NUMBER", "") + GIT_PR_COMMIT_SHORT = os.getenv("SHA7", "") + + if RELEASE_TYPE == "PRODUCTION": + BUILD_VERSION = "{}-{}".format(RELEASE_NUMBER, GIT_PR_COMMIT_SHORT) + elif RELEASE_TYPE == "PR": + BUILD_VERSION = "PR{}-{}".format(RELEASE_NUMBER, GIT_PR_COMMIT_SHORT) + else: + BUILD_VERSION = "dev" + + if sys.platform == "win32": + PLATFORM = "windows" + elif sys.platform == "darwin": + PLATFORM = "mac" + else: + PLATFORM = "other" + + ARCHIVE_NAME = "HighFidelity-Beta-Interface-{}-{}".format(BUILD_VERSION, PLATFORM) + return ARCHIVE_NAME + +def wipeClientBuildPath(relativePath): + targetPath = os.path.join(INTERFACE_BUILD_PATH, relativePath) + print("Checking path {}".format(targetPath)) + if os.path.exists(targetPath): + print("Removing path {}".format(targetPath)) + shutil.rmtree(targetPath) + +def fixupMacZip(filename): + fullPath = os.path.join(BUILD_PATH, "{}.zip".format(filename)) + outFullPath = "{}.zip".format(fullPath) + print("Fixup mac ZIP file {}".format(fullPath)) + with zipfile.ZipFile(fullPath) as inzip: + with zipfile.ZipFile(outFullPath, 'w') as outzip: + rootPath = inzip.infolist()[0].filename + for entry in inzip.infolist(): + if entry.filename == rootPath: + continue + newFilename = entry.filename[len(rootPath):] + # ignore the icon + if newFilename.startswith('Icon'): + continue + # ignore the server console + if newFilename.startswith('Console.app'): + continue + # ignore the nitpick app + if newFilename.startswith('nitpick.app'): + continue + # ignore the serverless content + if newFilename.startswith('interface.app/Contents/Resources/serverless'): + continue + # if we made it here, include the file in the output + buffer = inzip.read(entry.filename) + entry.filename = newFilename + outzip.writestr(entry, buffer) + outzip.close() + print("Replacing {} with fixed {}".format(fullPath, outFullPath)) + shutil.move(outFullPath, fullPath) + +def fixupWinZip(filename): + fullPath = os.path.join(BUILD_PATH, "{}.zip".format(filename)) + outFullPath = "{}.zip".format(fullPath) + print("Fixup windows ZIP file {}".format(fullPath)) + with zipfile.ZipFile(fullPath) as inzip: + with zipfile.ZipFile(outFullPath, 'w') as outzip: + for entry in inzip.infolist(): + # ignore the server console + if entry.filename.startswith('server-console/'): + continue + # ignore the nitpick app + if entry.filename.startswith('nitpick/'): + continue + # if we made it here, include the file in the output + buffer = inzip.read(entry.filename) + outzip.writestr(entry, buffer) + outzip.close() + print("Replacing {} with fixed {}".format(fullPath, outFullPath)) + shutil.move(outFullPath, fullPath) + + +for wipePath in WIPE_PATHS: + wipeClientBuildPath(wipePath) + +# Need the archive name for ad-hoc zip file manipulation +archiveName = computeArchiveName() + +cpackCommand = [ + 'cpack', + '-G', 'ZIP', + '-D', "CPACK_PACKAGE_FILE_NAME={}".format(archiveName), + '-D', "CPACK_INCLUDE_TOPLEVEL_DIRECTORY=OFF" + ] + +print("Create ZIP version of installer archive") +print(cpackCommand) +hifi_utils.executeSubprocess(cpackCommand, folder=BUILD_PATH) + +if sys.platform == "win32": + fixupWinZip(archiveName) +elif sys.platform == "darwin": + fixupMacZip(archiveName) + diff --git a/tools/nitpick/CMakeLists.txt b/tools/nitpick/CMakeLists.txt index 44eace5e70..6076f80c16 100644 --- a/tools/nitpick/CMakeLists.txt +++ b/tools/nitpick/CMakeLists.txt @@ -195,5 +195,7 @@ if (WIN32) set(TARGET_INSTALL_DIR ${NITPICK_INSTALL_DIR}) set(TARGET_INSTALL_COMPONENT ${CLIENT_COMPONENT}) + package_libraries_for_deployment() +elseif (APPLE) package_libraries_for_deployment() endif() diff --git a/tools/nitpick/src/PythonInterface.cpp b/tools/nitpick/src/PythonInterface.cpp index dcf4ecc682..48ca3b30e0 100644 --- a/tools/nitpick/src/PythonInterface.cpp +++ b/tools/nitpick/src/PythonInterface.cpp @@ -16,12 +16,16 @@ QString PythonInterface::getPythonCommand() { #ifdef Q_OS_WIN if (_pythonCommand.isNull()) { - QString pythonPath = PathUtils::getPathToExecutable("python.exe"); + // Use the python launcher as we need python 3, and python 2 may be installed + // See https://www.python.org/dev/peps/pep-0397/ + const QString pythonLauncherExecutable{ "py.exe" }; + QString pythonPath = PathUtils::getPathToExecutable(pythonLauncherExecutable); if (!pythonPath.isNull()) { _pythonCommand = pythonPath + _pythonExe; - } else { - QMessageBox::critical(0, "python.exe not found", - "Please verify that pyton.exe is in the PATH"); + } + else { + QMessageBox::critical(0, pythonLauncherExecutable + " not found", + "Please verify that py.exe is in the PATH"); exit(-1); } } @@ -35,4 +39,4 @@ QString PythonInterface::getPythonCommand() { #endif return _pythonCommand; -} +} \ No newline at end of file diff --git a/tools/nitpick/src/PythonInterface.h b/tools/nitpick/src/PythonInterface.h index 7972d55cce..d51b51d007 100644 --- a/tools/nitpick/src/PythonInterface.h +++ b/tools/nitpick/src/PythonInterface.h @@ -18,7 +18,7 @@ public: private: #ifdef Q_OS_WIN - const QString _pythonExe{ "python.exe" }; + const QString _pythonExe{ "py.exe" }; #else // Both Mac and Linux use "python" const QString _pythonExe{ "python" }; @@ -27,4 +27,4 @@ private: QString _pythonCommand; }; -#endif // hifi_PythonInterface_h +#endif // hifi_PythonInterface_h \ No newline at end of file diff --git a/tools/qt-builder/README.md b/tools/qt-builder/README.md new file mode 100644 index 0000000000..b7861a5e53 --- /dev/null +++ b/tools/qt-builder/README.md @@ -0,0 +1,235 @@ +# General +This document describes the process to build Qt 5.12.3. +Note that there are two patches. The first (to qfloat16.h) is needed to compile QT 5.12.3 on Visual Studio 2017 due to a bug in Visual Studio (*bitset* will not compile. Note that there is a change in CMakeLists.txt to support this. +The second patch is to OpenSL ES audio. +## Requirements +### Windows +1. Visual Studio 2017 + If you don’t have Community or Professional edition of Visual Studio 2017, download [Visual Studio Community 2017](https://www.visualstudio.com/downloads/). +Install with defaults + +1. python 2.7.16 +Check if needed running `python --version` - should return python 2.7.x +Install from https://www.python.org/ftp/python/2.7.16/python-2.7.16.amd64.msi +Add path to python executable to PATH. + +NOTE: REMOVE python 3 from PATH. Our regular build uses python 3. This will still work, because HIFI_PYTHON_EXEC points to the python 3 executable. + +Verify that python runs python 2.7 (run “python --version”) +1. git >= 1.6 +Check if needed `git --version` +Download from https://git-scm.com/download/win +Verify by entering `git --version` +1. perl >= 5.14 +Install from Strawberry Perl - http://strawberryperl.com/ - 5.28.1.1 64 bit to C:\Strawberry\ +Verify by running `perl --version` +1. flex and bison +Download from https://sourceforge.net/projects/winflexbison/files/latest/download +Uncompress in C:\flex_bison +Rename win-bison.exe to bison.exe and win-flex.exe to flex.exe +Add C:\flex_bison to PATH +Verify by running `flex --version` +Verify by running `bison --version` +1. gperf +Install from http://gnuwin32.sourceforge.net/downlinks/gperf.php +Add C:\Program Files (x86)\GnuWin32\bin to PATH +Verify by running `gperf --version` +1. 7-zip +Download from https://www.7-zip.org/download.html +1. Bash shell +From *Settings* select *Update & Security* +Select *For Developers* +Enable *Developer mode* +Restart PC +Open Control Panel and select *Programs and Features* +Select *Turn Windows features on or off* +Check *Windows Subsystem for Linux* +Click *Restart now* +Download from the Microsoft Store - Search for *bash* and choose the latest Ubuntu version +[First run will take a few minutes] +Enter a user name - all small letters (this is used for *sudo* commands) +Choose a password +### Linux +Tested on Ubuntu 16.04 and 18.04. +**16.04 NEEDED FOR JENKINS~~ ** +1. qt5 requirements +edit /etc/apt/sources.list (edit as root) +replace all *# deb-src* with *deb-src* (in vi `1,$s/# deb-src/deb-src/`) +`sudo apt-get update -y` +`sudo apt-get upgrade -y` +`sudo apt-get build-dep qt5-default -y` +1. git >= 1.6 +Check if needed `git --version` +`sudo apt-get install git -y` +Verify again +1. python +Check if needed `python --version` - should return python 2.7.x +`sudo apt-get install python -y` (not python 3!) +Verify again +1. gperf +Check if needed `gperf --version` +`sudo apt-get install gperf -y` +Verify again +1. bison and flex +Check if needed `flex --version` and `bison --version` +`sudo apt-get install flex bison -y` +Verify again +1. pkg-config (needed for qtwebengine) +Check if needed `pkg-config --version` +`sudo apt-get install pkg-config -y` +Verify again +1. OpenGL +Verify (first install mesa-utils - `sudo apt install mesa-utils -y`) by `glxinfo | grep "OpenGL version"` +`sudo apt-get install libgl1-mesa-dev -y` +`sudo ln -s /usr/lib/x86_64-linux-gnu/libGL.so.346.35 /usr/lib/x86_64-linux-gnu/libGL.so.1.2.0` +Verify again +1. make +Check if needed `make --version` +`sudo apt-get install make -y` +Verify again +1. g++ +Check if needed + `g++ --version` +`sudo apt-get install g++ -y` +Verify again +1. dbus-1 (needed for qtwebengine) +`sudo apt-get install libdbus-glib-1-dev -y` +1. nss (needed for qtwebengine) +`sudo apt-get install libnss3-dev -y` +### Mac +1. git >= 1.6 +Check if needed `git --version` +Install from https://git-scm.com/download/mac +Verify again +1. pkg-config +brew fontconfig dbus-glib stall pkg-config +1. dbus-1 +brew install dbus-glib +## Build Process +### General +qt is cloned to the qt5 folder. +The build is performed in the qt5-build folder. +Build products are installed to the qt5-install folder. +Before running configure, make sure that the qt5-build folder is empty. + +**Only run the git patches once!!!** +### Windows +Before building, verify that **HIFI_VCPKG_BASE_VERSION** points to a *vcpkg* folder containing *packages\openssl-windows_x64-windows*. +If not, follow https://github.com/highfidelity/vcpkg to install *vcpkg* and then *openssl*. +#### Preparing source files +`git clone --recursive https://code.qt.io/qt/qt5.git -b 5.12.3 --single-branch` + +* Copy the **patches** folder to qt5 +* Copy the **qt5vars.bat** file to qt5 +* Apply the two patches to Qt + +`cd qt5` +`git apply --ignore-space-change --ignore-whitespace patches/qfloat16.patch` +`git apply --ignore-space-change --ignore-whitespace patches/aec.patch` +`cd ..` +#### Configuring +`mkdir qt5-install` +`mkdir qt5-build` +`cd qt5-build` + +run `..\qt5\qt5vars.bat` +`cd ..\..\qt5-build` + +`..\qt5\configure -force-debug-info -opensource -confirm-license -opengl desktop -platform win32-msvc -openssl-linked OPENSSL_LIBS="-lssleay32 -llibeay32" -I %HIFI_VCPKG_BASE_VERSION%\packages\openssl-windows_x64-windows\include -L %HIFI_VCPKG_BASE_VERSION%\packages\openssl-windows_x64-windows\lib -nomake examples -nomake tests -skip qttranslations -skip qtserialport -skip qt3d -skip qtlocation -skip qtwayland -skip qtsensors -skip qtgamepad -skip qtspeech -skip qtcharts -skip qtx11extras -skip qtmacextras -skip qtvirtualkeyboard -skip qtpurchasing -skip qtdatavis3d -no-warnings-are-errors -no-pch -prefix ..\qt5-install` +#### Make +`nmake` +`nmake install` +#### Fixing +The *.prl* files have an absolute path that needs to be removed (see http://www.linuxfromscratch.org/blfs/view/stable-systemd/x/qtwebengine.html) +1. Open a bash terminal +1. `cd` to the *qt5-install* folder (e.g. `cd /mnt/d/qt5-install/`) +1. Run the following command +`find . -name \*.prl -exec sed -i -e '/^QMAKE_PRL_BUILD_DIR/d' {} \;` +1. Copy *qt.conf* to *qt5-install\bin* +#### Uploading +Create a tar file called qt5-install.tar from the qt5-install folder (e.g. using 7-zip) +Create a gzip file called qt5-install.tar.gz from the qt5-install.tar file just created (e.g. using 7-zip) +Upload qt5-install.tar.gz to https://hifi-qa.s3.amazonaws.com/qt5/Windows/ +### Linux +#### Preparing source files +`git clone --recursive git://code.qt.io/qt/qt5.git -b 5.12.3 --single-branch` + +* Copy the **patches** folder to qt5 +* Apply one patch to Qt +`cd qt5` +`git apply --ignore-space-change --ignore-whitespace patches/aec.patch` +`cd ..` +#### Configuring +`mkdir qt5-install` +`mkdir qt5-build` +`cd qt5-build` + +*Ubuntu 16.04* +`../qt5/configure -opensource -confirm-license -platform linux-g++-64 -qt-zlib -qt-libjpeg -qt-libpng -qt-xcb -qt-freetype -qt-pcre -qt-harfbuzz -nomake examples -nomake tests -skip qttranslations -skip qtserialport -skip qt3d -skip qtlocation -skip qtwayland -skip qtsensors -skip qtgamepad -skip qtspeech -skip qtcharts -skip qtmacextras -skip qtvirtualkeyboard -skip qtpurchasing -skip qtdatavis3d -no-warnings-are-errors -no-pch -no-egl -no-icu -prefix ../qt5-install` + +*Ubuntu 18.04* +`../qt5/configure -force-debug-info -release -opensource -confirm-license -gdb-index -recheck-all -nomake tests -nomake examples -skip qttranslations -skip qtserialport -skip qt3d -skip qtlocation -skip qtwayland -skip qtsensors -skip qtgamepad -skip qtspeech -skip qtcharts -skip qtx11extras -skip qtmacextras -skip qtvirtualkeyboard -skip qtpurchasing -skip qtdatavis3d -no-warnings-are-errors -no-pch -c++std c++14 -prefix ../qt5-install` + + +???`../qt5/configure -opensource -confirm-license -gdb-index -nomake examples -nomake tests -skip qttranslations -skip qtserialport -skip qt3d -skip qtlocation -skip qtwayland -skip qtsensors -skip qtgamepad -skip qtspeech -skip qtcharts -skip qtmacextras -skip qtvirtualkeyboard -skip qtpurchasing -skip qtdatavis3d -no-warnings-are-errors -no-pch -prefix ../qt5-install` +#### Make +`make` + +????*Ubuntu 18.04 only* +????`make module-qtwebengine` +????`make module-qtscript` + +*Both* +`make install` +#### Fixing +1. The *.prl* files have an absolute path that needs to be removed (see http://www.linuxfromscratch.org/blfs/view/stable-systemd/x/qtwebengine.html) +`cd ../qt5-install` +`find . -name \*.prl -exec sed -i -e '/^QMAKE_PRL_BUILD_DIR/d' {} \;` +1. Copy *qt.conf* to *qt5-install\bin* +#### Uploading +*Ubuntu 16.04* +1. Return to the home folder +`cd ..` +1. Open a python 3 shell +`python3` +1. Run the following snippet: +`import os` +`import tarfile` +`filename=tarfile.open("qt5-install.tar.gz", "w:gz")` +`filename.add("qt5-install", os.path.basename("qt5-install"))` +`exit()` +1. Upload qt5-install.tar.gz to https://hifi-qa.s3.amazonaws.com/qt5/Ubuntu/16.04 + +*Ubuntu 18.04* +``tar -zcvf qt5-install.tar.gz qt5-install` +1. Upload qt5-install.tar.gz to https://hifi-qa.s3.amazonaws.com/qt5/Ubuntu/18.04 + +1. ### Mac +#### Preparing source files +git clone --recursive git://code.qt.io/qt/qt5.git -b 5.12.3 --single-branch + +* Copy the **patches** folder to qt5 +* Apply one patch to Qt +`cd qt5` +`git apply --ignore-space-change --ignore-whitespace patches/aec.patch` +`cd ..` +#### Configuring +`mkdir qt5-install` +`mkdir qt5-build` +`cd ../qt5-build` + +`../qt5/configure -force-debug-info -opensource -confirm-license -qt-zlib -qt-libjpeg -qt-libpng -qt-freetype -qt-pcre -qt-harfbuzz -nomake examples -nomake tests -skip qttranslations -skip qtserialport -skip qt3d -skip qtlocation -skip qtwayland -skip qtsensors -skip qtgamepad -skip qtspeech -skip qtcharts -skip qtx11extras -skip qtmacextras -skip qtvirtualkeyboard -skip qtpurchasing -skip qtdatavis3d -no-warnings-are-errors -no-pch -prefix ../qt5-install` +#### Make +`make` +`make install` +#### Fixing +1. The *.prl* files have an absolute path that needs to be removed (see http://www.linuxfromscratch.org/blfs/view/stable-systemd/x/qtwebengine.html) +`cd ../qt5-install` +`find . -name \*.prl -exec sed -i -e '/^QMAKE_PRL_BUILD_DIR/d' {} \;` +`cd ..` +1. Copy *qt.conf* to *qt5-install\bin* +#### Uploading +`tar -zcvf qt5-install.tar.gz qt5-install` +Upload qt5-install.tar.gz to https://hifi-qa.s3.amazonaws.com/qt5/Mac/ +## Problems +*configure* errors, if any, may be viewed in **config.log** and **config.summary** diff --git a/tools/qt-builder/patches/aec.patch b/tools/qt-builder/patches/aec.patch new file mode 100644 index 0000000000..be159d857a --- /dev/null +++ b/tools/qt-builder/patches/aec.patch @@ -0,0 +1,40 @@ +diff --git a/qtmultimedia/src/plugins/opensles/qopenslesaudioinput.cpp b/qtmultimedia/src/plugins/opensles/qopenslesaudioinput.cpp +index ad87cb0..54ed18a 100644 +--- a/qtmultimedia/src/plugins/opensles/qopenslesaudioinput.cpp ++++ b/qtmultimedia/src/plugins/opensles/qopenslesaudioinput.cpp +@@ -117,6 +117,8 @@ QOpenSLESAudioInput::QOpenSLESAudioInput(const QByteArray &device) + m_recorderPreset = SL_ANDROID_RECORDING_PRESET_CAMCORDER; + else if (qstrcmp(device, QT_ANDROID_PRESET_VOICE_RECOGNITION) == 0) + m_recorderPreset = SL_ANDROID_RECORDING_PRESET_VOICE_RECOGNITION; ++ else if (qstrcmp(device, QT_ANDROID_PRESET_VOICE_COMMUNICATION) == 0) ++ m_recorderPreset = SL_ANDROID_RECORDING_PRESET_VOICE_COMMUNICATION; + else + m_recorderPreset = SL_ANDROID_RECORDING_PRESET_GENERIC; + #endif +diff --git a/qtmultimedia/src/plugins/opensles/qopenslesaudioinput.h b/qtmultimedia/src/plugins/opensles/qopenslesaudioinput.h +index ad84db0..35cc379 100644 +--- a/qtmultimedia/src/plugins/opensles/qopenslesaudioinput.h ++++ b/qtmultimedia/src/plugins/opensles/qopenslesaudioinput.h +@@ -50,6 +50,7 @@ + #define QT_ANDROID_PRESET_MIC "mic" + #define QT_ANDROID_PRESET_CAMCORDER "camcorder" + #define QT_ANDROID_PRESET_VOICE_RECOGNITION "voicerecognition" ++#define QT_ANDROID_PRESET_VOICE_COMMUNICATION "voicecommunication" + + #endif + +diff --git a/qtmultimedia/src/plugins/opensles/qopenslesengine.cpp b/qtmultimedia/src/plugins/opensles/qopenslesengine.cpp +index 1a16cc2..2577fb3 100644 +--- a/qtmultimedia/src/plugins/opensles/qopenslesengine.cpp ++++ b/qtmultimedia/src/plugins/opensles/qopenslesengine.cpp +@@ -114,7 +114,8 @@ QList QOpenSLESEngine::availableDevices(QAudio::Mode mode) const + #ifdef ANDROID + devices << QT_ANDROID_PRESET_MIC + << QT_ANDROID_PRESET_CAMCORDER +- << QT_ANDROID_PRESET_VOICE_RECOGNITION; ++ << QT_ANDROID_PRESET_VOICE_RECOGNITION ++ << QT_ANDROID_PRESET_VOICE_COMMUNICATION; + #else + devices << "default"; + #endif + \ No newline at end of file diff --git a/tools/qt-builder/patches/qfloat16.patch b/tools/qt-builder/patches/qfloat16.patch new file mode 100644 index 0000000000..2773573264 --- /dev/null +++ b/tools/qt-builder/patches/qfloat16.patch @@ -0,0 +1,44 @@ +diff --git a/qtbase/src/corelib/global/qfloat16.h b/qtbase/src/corelib/global/qfloat16.h +index 3e50ad8467..2453ff8847 100644 +--- a/qtbase/src/corelib/global/qfloat16.h ++++ b/qtbase/src/corelib/global/qfloat16.h +@@ -83,7 +83,9 @@ private: + Q_CORE_EXPORT static const quint32 shifttable[]; + + friend bool qIsNull(qfloat16 f) Q_DECL_NOTHROW; ++#if ! defined(QT_NO_FLOAT16_OPERATORS) + friend qfloat16 operator-(qfloat16 a) Q_DECL_NOTHROW; ++#endif + }; + + Q_DECLARE_TYPEINFO(qfloat16, Q_PRIMITIVE_TYPE); +@@ -165,6 +167,7 @@ inline qfloat16::operator float() const Q_DECL_NOTHROW + } + #endif + ++#if ! defined(QT_NO_FLOAT16_OPERATORS) + inline qfloat16 operator-(qfloat16 a) Q_DECL_NOTHROW + { + qfloat16 f; +@@ -206,11 +209,12 @@ QF16_MAKE_ARITH_OP_INT(-) + QF16_MAKE_ARITH_OP_INT(*) + QF16_MAKE_ARITH_OP_INT(/) + #undef QF16_MAKE_ARITH_OP_INT +- ++#endif + QT_WARNING_PUSH + QT_WARNING_DISABLE_CLANG("-Wfloat-equal") + QT_WARNING_DISABLE_GCC("-Wfloat-equal") + ++#if ! defined(QT_NO_FLOAT16_OPERATORS) + inline bool operator>(qfloat16 a, qfloat16 b) Q_DECL_NOTHROW { return static_cast(a) > static_cast(b); } + inline bool operator<(qfloat16 a, qfloat16 b) Q_DECL_NOTHROW { return static_cast(a) < static_cast(b); } + inline bool operator>=(qfloat16 a, qfloat16 b) Q_DECL_NOTHROW { return static_cast(a) >= static_cast(b); } +@@ -244,6 +248,7 @@ QF16_MAKE_BOOL_OP_INT(<=) + QF16_MAKE_BOOL_OP_INT(==) + QF16_MAKE_BOOL_OP_INT(!=) + #undef QF16_MAKE_BOOL_OP_INT ++#endif + + QT_WARNING_POP + diff --git a/tools/qt-builder/qt.conf b/tools/qt-builder/qt.conf new file mode 100644 index 0000000000..01f4e8c707 --- /dev/null +++ b/tools/qt-builder/qt.conf @@ -0,0 +1,2 @@ +[Paths] +Prefix=.. diff --git a/tools/qt-builder/qt5vars.bat b/tools/qt-builder/qt5vars.bat new file mode 100644 index 0000000000..22a976827b --- /dev/null +++ b/tools/qt-builder/qt5vars.bat @@ -0,0 +1,17 @@ +@echo off + +REM Set up \Microsoft Visual Studio 2017, where is \c amd64, \c x86, etc. +CALL "C:\Program Files (x86)\Microsoft Visual Studio\2017\Community\VC\Auxiliary\Build\vcvarsall.bat" x64 + +REM Edit this location to point to the source code of Qt +SET _ROOT=..\qt5 + +SET PATH=%_ROOT%\qtbase\bin;%_ROOT%\gnuwin32\bin;%PATH% + +REM Uncomment the below line when using a git checkout of the source repository +SET PATH=%_ROOT%\qtrepotools\bin;%PATH% + +SET _ROOT= + +REM Keeps the command line open when this script is run. +cmd /k \ No newline at end of file