diff --git a/.github/workflows/master_build.yml b/.github/workflows/master_build.yml index 2caddef182..ee6ef88d0e 100644 --- a/.github/workflows/master_build.yml +++ b/.github/workflows/master_build.yml @@ -83,15 +83,15 @@ jobs: shell: bash run: | echo "${{ steps.buildenv1.outputs.symbols_archive }}" - echo "ARTIFACT_PATTERN=Vircadia-Alpha-*.$INSTALLER_EXT" >> $GITHUB_ENV + echo "ARTIFACT_PATTERN=Vircadia-*.$INSTALLER_EXT" >> $GITHUB_ENV # Build type variables echo "GIT_COMMIT_SHORT=${{ steps.buildenv1.outputs.github_sha_short }}" >> $GITHUB_ENV if [ "${{ matrix.build_type }}" = "full" ]; then echo "CLIENT_ONLY=FALSE" >> $GITHUB_ENV - echo "INSTALLER=Vircadia-Alpha-$BUILD_NUMBER-$GIT_COMMIT_SHORT.$INSTALLER_EXT" >> $GITHUB_ENV + echo "INSTALLER=Vircadia-$BUILD_NUMBER-$GIT_COMMIT_SHORT.$INSTALLER_EXT" >> $GITHUB_ENV else echo "CLIENT_ONLY=TRUE" >> $GITHUB_ENV - echo "INSTALLER=Vircadia-Alpha-Interface-$BUILD_NUMBER-$GIT_COMMIT_SHORT.$INSTALLER_EXT" >> $GITHUB_ENV + echo "INSTALLER=Vircadia-Interface-$BUILD_NUMBER-$GIT_COMMIT_SHORT.$INSTALLER_EXT" >> $GITHUB_ENV fi - name: Clear working directory if: startsWith(matrix.os, 'windows') diff --git a/.github/workflows/pr_build.yml b/.github/workflows/pr_build.yml index 8a3dce98ec..9d6984b5b2 100644 --- a/.github/workflows/pr_build.yml +++ b/.github/workflows/pr_build.yml @@ -86,9 +86,9 @@ jobs: echo "${{ steps.buildenv1.outputs.symbols_archive }}" echo "GIT_COMMIT_SHORT=${{ steps.buildenv1.outputs.github_sha_short }}" >> $GITHUB_ENV if [[ "${{ matrix.build_type }}" != "android" ]]; then - echo "ARTIFACT_PATTERN=Vircadia-Alpha-PR${{ github.event.number }}-*.$INSTALLER_EXT" >> $GITHUB_ENV + echo "ARTIFACT_PATTERN=Vircadia-PR${{ github.event.number }}-*.$INSTALLER_EXT" >> $GITHUB_ENV # Build type variables - echo "INSTALLER=Vircadia-Alpha-$RELEASE_NUMBER-$GIT_COMMIT_SHORT.$INSTALLER_EXT" >> $GITHUB_ENV + echo "INSTALLER=Vircadia-$RELEASE_NUMBER-$GIT_COMMIT_SHORT.$INSTALLER_EXT" >> $GITHUB_ENV else echo "ARTIFACT_PATTERN=*.$INSTALLER_EXT" >> $GITHUB_ENV fi diff --git a/BUILD.md b/BUILD.md index 2d94d1b5b1..b09700c2e7 100644 --- a/BUILD.md +++ b/BUILD.md @@ -59,9 +59,9 @@ You do not need to install vcpkg. Building the dependencies can be lengthy and the resulting files will be stored in your OS temp directory. However, those files can potentially get cleaned up by the OS, so in order to avoid this and having to redo the lengthy build step, you can set the following environment variable: -export HIFI_VCPKG_BASE=/path/to/directory + export HIFI_VCPKG_BASE=/path/to/directory -Where /path/to/directory is the path to a directory where you wish the build files to get stored. +Where `/path/to/directory` is the path to a directory where you wish the build files to get stored. #### Generating Build Files diff --git a/BUILD_WIN.md b/BUILD_WIN.md index 1035b1c366..96f570981a 100644 --- a/BUILD_WIN.md +++ b/BUILD_WIN.md @@ -7,38 +7,41 @@ This is a stand-alone guide for creating your first Vircadia build for Windows 6 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. +**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 & Python 3.x 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++". On the right on the Summary toolbar, select the following components. +When selecting components, check "Desktop development with C++". -#### If you're installing Visual Studio 2017, +If you do not already have a Python 3.x development environment installed and want to install it with Visual Studio, check "Python Development". 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. + +On the right on the Summary toolbar, select the following components based on your Visual Studio version. + +#### 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, +#### If you're installing Visual Studio 2019 +* MSVC v142 - VS 2019 C++ X64/x86 build tools * 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 3.x development environment installed, also check "Python Development" in this screen. +### Step 1a. Alternate Python -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 - -If you do not wish to use the Python installation bundled with Visual Studio, you can download the installer from [here](https://www.python.org/downloads/). Ensure you get version 3.6.6 or higher. +If you do not wish to use the Python installation bundled with Visual Studio, you can download the installer from [here](https://www.python.org/downloads/). Ensure that you get version 3.6.6 or higher. ### Step 2. Python Dependencies -In a command-line that can access Python's pip you will need to run the following command: +In an administrator command-line that can access Python's pip you will need to run the following command: `pip install distro` +If you do not use an administrator command-line, you will get errors. + ### Step 3. Installing CMake Download and install the latest version of CMake 3.15. @@ -46,7 +49,11 @@ Download and install the latest version of CMake 3.15. Download the file named win64-x64 Installer from the [CMake Website](https://cmake.org/download/). You can access the installer on this [3.15 Version page](https://cmake.org/files/v3.15/). During installation, make sure to check "Add CMake to system PATH for all users" when prompted. -### Step 4. Create VCPKG environment variable +### Step 4. Node.JS and NPM + +Install version 10.15.0 LTS (or greater) of [Node.JS and NPM](). + +### Step 5. Create VCPKG environment variable In the next step, you will use CMake to build Vircadia. By default, the CMake process builds dependency files in Windows' `%TEMP%` directory, which is periodically cleared by the operating system. To prevent you from having to re-build the dependencies in the event that Windows clears that directory, we recommend that you create a `HIFI_VCPKG_BASE` environment variable linked to a directory somewhere on your machine. That directory will contain all dependency files until you manually remove them. To create this variable: @@ -65,7 +72,7 @@ To create this variable: * Set "Variable name" to `HIFI_VCPKG_BOOTSTRAP` * Set "Variable value" to `1` -### Step 5. Running CMake to Generate Build Files +### Step 6. Running CMake to Generate Build Files Run Command Prompt from Start and run the following commands: `cd "%VIRCADIA_DIR%"` @@ -80,7 +87,7 @@ Run `cmake .. -G "Visual Studio 16 2019" -A x64`. Where `%VIRCADIA_DIR%` is the directory for the Vircadia repository. -### Step 6. Making a Build +### Step 7. Making a Build Open `%VIRCADIA_DIR%\build\vircadia.sln` using Visual Studio. @@ -88,7 +95,7 @@ Change the Solution Configuration (menu ribbon under the menu bar, next to the g Run from the menu bar `Build > Build Solution`. -### Step 7. Testing Interface +### Step 8. Testing Interface Create another environment variable (see Step #3) * Set "Variable name": `_NO_DEBUG_HEAP` @@ -104,11 +111,11 @@ Note: You can also run Interface by launching it from command line or File Explo ## Troubleshooting -For any problems after Step #6, first try this: +For any problems after Step #7, first try this: * Delete your locally cloned copy of the Vircadia repository * Restart your computer * Redownload the [repository](https://github.com/vircadia/vircadia) -* Restart directions from Step #6 +* Restart directions from Step #7 #### CMake gives you the same error message repeatedly after the build fails diff --git a/INSTALLER.md b/INSTALLER.md index 994725ac28..84ee14eaaa 100644 --- a/INSTALLER.md +++ b/INSTALLER.md @@ -60,8 +60,8 @@ To produce an executable installer on Windows, the following are required: 1. Copy `Release\ApplicationID.dll` to `C:\Program Files (x86)\NSIS\Plugins\x86-ansi\` 1. Copy `ReleaseUnicode\ApplicationID.dll` to `C:\Program Files (x86)\NSIS\Plugins\x86-unicode\` -1. [Node.JS and NPM]() - 1. Install version 10.15.0 LTS +1. [Node.JS and NPM]() + 1. Install version 10.15.0 LTS (or greater) ##### Code Signing (optional) diff --git a/LICENSE b/LICENSE index 8dfe384174..d5ca6ae075 100644 --- a/LICENSE +++ b/LICENSE @@ -1,5 +1,5 @@ Copyright (c) 2013-2019, High Fidelity, Inc. -Copyright (c) 2019-2020, Vircadia contributors. +Copyright (c) 2019-2021, Vircadia contributors. All rights reserved. https://vircadia.com diff --git a/README.md b/README.md index c7a9ee8d52..d260c76d0d 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@ -# Vircadia +# Vircadia (Codename Athena) ### What is this? -Vircadia is a 3D social software project seeking to incrementally bring about a truly free and open metaverse, in desktop and XR. +Vircadia™ is a 3D social software project seeking to incrementally bring about a truly free and open metaverse, in desktop and XR. -### [Download](https://vircadia.com/download-vircadia/) +### [Website](https://vircadia.com/) | [Discord](https://discordapp.com/invite/Pvx2vke) | [Download](https://vircadia.com/download-vircadia/) ### Releases @@ -12,50 +12,46 @@ Vircadia is a 3D social software project seeking to incrementally bring about a ### How to build the Interface -[For Windows](https://github.com/vircadia/vircadia/blob/master/BUILD_WIN.md) - -[For Mac](https://github.com/vircadia/vircadia/blob/master/BUILD_OSX.md) - -[For Linux](https://github.com/vircadia/vircadia/blob/master/BUILD_LINUX.md) - -[For Linux - Vircadia Builder](https://github.com/vircadia/vircadia-builder) +- [For Windows](https://github.com/vircadia/vircadia/blob/master/BUILD_WIN.md) +- [For Mac](https://github.com/vircadia/vircadia/blob/master/BUILD_OSX.md) +- [For Linux](https://github.com/vircadia/vircadia/blob/master/BUILD_LINUX.md) +- [For Linux - Vircadia Builder](https://github.com/vircadia/vircadia-builder) ### How to deploy a Server -[For Windows and Linux](https://vircadia.com/deploy-a-server/) +- [For Windows and Linux](https://vircadia.com/deploy-a-server/) ### How to build a Server -[For Linux - Vircadia Builder](https://github.com/vircadia/vircadia-builder) +- [For Windows](https://github.com/vircadia/vircadia/blob/master/BUILD_WIN.md) +- [For Linux](https://github.com/vircadia/vircadia/blob/master/BUILD_LINUX.md) +- [For Linux - Vircadia Builder](https://github.com/vircadia/vircadia-builder) ### How to generate an Installer -[For Windows](https://github.com/vircadia/vircadia/blob/master/INSTALLER.md) +- [For Windows](https://github.com/vircadia/vircadia/blob/master/INSTALLER.md) +- [For Mac](https://github.com/vircadia/vircadia/blob/master/INSTALLER.md#os-x) +- [For Linux - AppImage - Vircadia Builder](https://github.com/vircadia/vircadia-builder/blob/master/README.md#building-appimages) -[For Linux - AppImage - Vircadia Builder](https://github.com/vircadia/vircadia-builder/blob/master/README.md#building-appimages) - -### Boot to Metaverse: The Goal +### Boot to Metaverse: [The Goal](https://vircadia.com/vision/) Having a place to experience adventure, a place to relax with calm breath, that's a world to live in. An engine to support infinite combinations and possibilities of worlds without censorship and interruption, that's a metaverse. Finding a way to make infinite realities our reality is the dream. ### Boot to Metaverse: The Technicals -Many developers have had personal combinations of High Fidelity from C++ modifications to different default scripts, all of which are lost to time as their fullest potential is never truly shared and propagated through the system. +Vircadia consists of many projects and codebases with its unifying structure's goal being a decentralized metaverse. -The goal of this project is to achieve the metaverse dream through shared contribution and building. Setting goals that are achievable yet meaningful is key to making proper forward progress on the technical front whilst maintaining morale. +- The Interface (Codename Athena) - You are here! +- The Server (Codename Athena) - You are also here! +- The UI Framework (Codename Nyx) - Codebase coming soon. +- [The Metaverse (Codename Iamus)](https://github.com/vircadia/Iamus/) +- [The Metaverse Dashboard (Codename Iamus)](https://github.com/vircadia/project-iamus-dashboard/) +- [The Launcher (Codename Pantheon)](https://github.com/vircadia/pantheon-launcher/) -### Why High Fidelity's Virtual Reality Platform? - -Because of all the options, it is the only starting point that is open-source, cross-platform, fully VR integrated + fully desktop integrated with an aim for quality visuals and performance. It also provides a foundation to build from including components like entity management, full body IK, etc. - -WebXR offers the open-source and decentralized aspect but does not have any of the full featured starting points such as avatars, IK, etc. which means that a lot of ground work will have to be laid to make something functional. Far more work will need to be done to create a truly seamless and extensive experience as well. - -Platforms like NeosVR or VRChat are not viable from go due to their fundamental closed-source and centralized nature. A metaverse to live in cannot have the keys handed over to any singular entity, if any at all. - -We need to do the best we can with what we've got and our best bet as open source developers is to not redesign the wheel if we can help it! +#### Child Projects +- [Vircadia Builder for Linux](https://github.com/vircadia/vircadia-builder/) +- [General Documentation](https://github.com/vircadia/vircadia-docs-sphinx/) ### Contribution -A special thanks to the contributors of Vircadia. - -[Contribution](CONTRIBUTING.md) +There are many contributors to Vircadia. Code writers, reviewers, testers, documentation writers, modelers, and general supporters of the project are all integral to its development and success towards its goals. Find out how you can [contribute](CONTRIBUTING.md)! diff --git a/android/build_android.sh b/android/build_android.sh index 9cf1b9e2ab..d3c79afdbe 100755 --- a/android/build_android.sh +++ b/android/build_android.sh @@ -17,7 +17,7 @@ fi ANDROID_APP=interface ANDROID_OUTPUT_DIR=./apps/${ANDROID_APP}/build/outputs/apk/${ANDROID_BUILD_TYPE} ANDROID_OUTPUT_FILE=${ANDROID_APP}-${ANDROID_BUILD_TYPE}.apk -ANDROID_APK_NAME=Vircadia-Alpha-${ANDROID_APK_SUFFIX} +ANDROID_APK_NAME=Vircadia-${ANDROID_APK_SUFFIX} ./gradlew -PHIFI_ANDROID_PRECOMPILED=${HIFI_ANDROID_PRECOMPILED} -PVERSION_CODE=${VERSION_CODE} -PRELEASE_NUMBER=${RELEASE_NUMBER} -PRELEASE_TYPE=${RELEASE_TYPE} ${ANDROID_APP}:${ANDROID_BUILD_TARGET} cp ${ANDROID_OUTPUT_DIR}/${ANDROID_OUTPUT_FILE} ./${ANDROID_APK_NAME} diff --git a/cmake/installer/installer-header.bmp b/cmake/installer/installer-header.bmp index de8448ed44..99862ffdb4 100644 Binary files a/cmake/installer/installer-header.bmp and b/cmake/installer/installer-header.bmp differ diff --git a/cmake/installer/uninstaller-header.bmp b/cmake/installer/uninstaller-header.bmp index de8448ed44..99862ffdb4 100644 Binary files a/cmake/installer/uninstaller-header.bmp and b/cmake/installer/uninstaller-header.bmp differ diff --git a/cmake/macros/GenerateInstallers.cmake b/cmake/macros/GenerateInstallers.cmake index 0442df55cf..640cc1720f 100644 --- a/cmake/macros/GenerateInstallers.cmake +++ b/cmake/macros/GenerateInstallers.cmake @@ -31,7 +31,7 @@ macro(GENERATE_INSTALLERS) set(CPACK_PACKAGE_NAME ${_DISPLAY_NAME}) set(CPACK_PACKAGE_VENDOR "Vircadia") set(CPACK_PACKAGE_VERSION ${BUILD_VERSION}) - set(CPACK_PACKAGE_FILE_NAME "Vircadia-Alpha${_PACKAGE_NAME_EXTRA}-${BUILD_VERSION}") + set(CPACK_PACKAGE_FILE_NAME "Vircadia${_PACKAGE_NAME_EXTRA}-${BUILD_VERSION}") set(CPACK_NSIS_DISPLAY_NAME ${_DISPLAY_NAME}) set(CPACK_NSIS_PACKAGE_NAME ${_DISPLAY_NAME}) if (PR_BUILD) diff --git a/cmake/macros/SetPackagingParameters.cmake b/cmake/macros/SetPackagingParameters.cmake index c9cf91bd3d..9311594938 100644 --- a/cmake/macros/SetPackagingParameters.cmake +++ b/cmake/macros/SetPackagingParameters.cmake @@ -23,6 +23,7 @@ macro(SET_PACKAGING_PARAMETERS) set_from_env(RELEASE_TYPE RELEASE_TYPE "DEV") set_from_env(RELEASE_NUMBER RELEASE_NUMBER "") + set_from_env(RELEASE_NAME RELEASE_NAME "") set_from_env(STABLE_BUILD STABLE_BUILD 0) set_from_env(INITIAL_STARTUP_LOCATION INITIAL_STARTUP_LOCATION "") set_from_env(BYPASS_SIGNING BYPASS_SIGNING 0) diff --git a/cmake/templates/BuildInfo.h.in b/cmake/templates/BuildInfo.h.in index 02f6a50919..7f3a63d4b4 100644 --- a/cmake/templates/BuildInfo.h.in +++ b/cmake/templates/BuildInfo.h.in @@ -4,6 +4,7 @@ // // Created by Stephen Birarda on 1/14/16. // Copyright 2015 High Fidelity, Inc. +// Copyright 2021 Vircadia contributors. // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html @@ -24,6 +25,7 @@ namespace BuildInfo { const QString MODIFIED_ORGANIZATION = "@BUILD_ORGANIZATION@"; const QString ORGANIZATION_DOMAIN = "vircadia.com"; const QString VERSION = "@BUILD_VERSION@"; + const QString RELEASE_NAME = "@RELEASE_NAME@"; const QString BUILD_NUMBER = "@BUILD_NUMBER@"; const QString BUILD_GLOBAL_SERVICES = "@BUILD_GLOBAL_SERVICES@"; const QString BUILD_TIME = "@BUILD_TIME@"; diff --git a/domain-server/resources/web/js/shared.js b/domain-server/resources/web/js/shared.js index 542771c60e..bec8d19119 100644 --- a/domain-server/resources/web/js/shared.js +++ b/domain-server/resources/web/js/shared.js @@ -505,7 +505,7 @@ function createDomainIDPrompt(callback) { swal({ title: 'Finish Registering Domain', type: 'input', - text: 'Enter a label for this machine.

This will help you identify which domain ID belongs to which machine.

This is a required step for registration.

', + text: 'Enter a label for this Domain Server.

This will help you identify which domain ID belongs to which server.

This is a required step for registration.

Acceptable characters are [A-Z][a-z0-9]+-_.Vircadia Github." + text: "Website" size: 20 onLinkActivated: { - About.openUrl("https:/github.com/vircadia/vircadia"); + About.openUrl("https://vircadia.com"); } } - Item { height: 40; width: 1 } + RalewayRegular { + textFormat: Text.StyledText + linkColor: "#00B4EF" + color: "white" + text: "Source" + size: 20 + onLinkActivated: { + About.openUrl("https://github.com/vircadia/vircadia"); + } + + } + Item { height: 25; width: 1 } Row { spacing: 5 Image { @@ -117,7 +133,7 @@ Rectangle { Item { height: 20; width: 1 } RalewayRegular { color: "white" - text: "© 2019-2020 Vircadia contributors." + text: "© 2019 - 2021 Vircadia contributors." size: 14 } RalewayRegular { @@ -135,5 +151,23 @@ Rectangle { About.openUrl("http://www.apache.org/licenses/LICENSE-2.0.html"); } } + Item { height: 35; width: 1 } + RalewayRegular { + color: "white" + text: "In memoriam," + size: 14 + } + RalewayRegular { + color: "white" + text: "2012 - 2019 the High Fidelity virtual reality project." + size: 14 + } + Item { height: 5; width: 1 } + Image { + id: hifiLogo + width: 200; height: 50 + fillMode: Image.PreserveAspectFit + source: "../../../images/about-highfidelity.png" + } } } diff --git a/interface/src/AboutUtil.cpp b/interface/src/AboutUtil.cpp index b9bea2d85c..d2a00854b5 100644 --- a/interface/src/AboutUtil.cpp +++ b/interface/src/AboutUtil.cpp @@ -4,6 +4,7 @@ // // Created by Vlad Stelmahovsky on 15/5/2018. // Copyright 2018 High Fidelity, Inc. +// Copyright 2021 Vircadia contributors. // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html @@ -40,6 +41,10 @@ QString AboutUtil::getBuildVersion() const { return BuildInfo::VERSION; } +QString AboutUtil::getReleaseName() const { + return BuildInfo::RELEASE_NAME; +} + QString AboutUtil::getQtVersion() const { return qVersion(); } @@ -57,15 +62,15 @@ void AboutUtil::openUrl(const QString& url) const { auto tablet = DependencyManager::get()->getTablet("com.highfidelity.interface.tablet.system"); auto hmd = DependencyManager::get(); - auto offscreenUi = DependencyManager::get(); + auto offscreenUI = DependencyManager::get(); - if (tablet->getToolbarMode()) { - offscreenUi->load("Browser.qml", [=](QQmlContext* context, QObject* newObject) { + if (tablet->getToolbarMode() && offscreenUI) { + offscreenUI->load("Browser.qml", [=](QQmlContext* context, QObject* newObject) { newObject->setProperty("url", url); }); } else { - if (!hmd->getShouldShowTablet() && !qApp->isHMDMode()) { - offscreenUi->load("Browser.qml", [=](QQmlContext* context, QObject* newObject) { + if (!hmd->getShouldShowTablet() && !qApp->isHMDMode() && offscreenUI) { + offscreenUI->load("Browser.qml", [=](QQmlContext* context, QObject* newObject) { newObject->setProperty("url", url); }); } else { diff --git a/interface/src/AboutUtil.h b/interface/src/AboutUtil.h index 8cc76dad1e..f072ae8b4a 100644 --- a/interface/src/AboutUtil.h +++ b/interface/src/AboutUtil.h @@ -30,12 +30,14 @@ * Read-only. * @property {string} buildDate - The build date of Interface that is currently running. Read-only. * @property {string} buildVersion - The build version of Interface that is currently running. Read-only. + * @property {string} releaseName - The release codename of the version that Interface is currently running. Read-only. * @property {string} qtVersion - The Qt version used in Interface that is currently running. Read-only. * * @example Report information on the version of Interface currently running. * print("Interface platform: " + About.platform); * print("Interface build date: " + About.buildDate); * print("Interface version: " + About.buildVersion); + * print("Interface release name: " + About.releaseName); * print("Qt version: " + About.qtVersion); */ @@ -66,6 +68,7 @@ class AboutUtil : public QObject { Q_PROPERTY(QString platform READ getPlatformName CONSTANT) Q_PROPERTY(QString buildDate READ getBuildDate CONSTANT) Q_PROPERTY(QString buildVersion READ getBuildVersion CONSTANT) + Q_PROPERTY(QString releaseName READ getReleaseName CONSTANT) Q_PROPERTY(QString qtVersion READ getQtVersion CONSTANT) public: static AboutUtil* getInstance(); @@ -74,6 +77,7 @@ public: QString getPlatformName() const { return "Vircadia"; } QString getBuildDate() const; QString getBuildVersion() const; + QString getReleaseName() const; QString getQtVersion() const; public slots: diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index cc46d8591b..331e6226ab 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -7180,6 +7180,10 @@ void Application::updateWindowTitle() const { QString buildVersion = " - Vircadia - " + (BuildInfo::BUILD_TYPE == BuildInfo::BuildType::Stable ? QString("Version") : QString("Build")) + " " + applicationVersion(); + + if (BuildInfo::RELEASE_NAME != "") { + buildVersion += " - " + BuildInfo::RELEASE_NAME; + } QString connectionStatus = isInErrorState ? " (ERROR CONNECTING)" : nodeList->getDomainHandler().isConnected() ? "" : " (NOT CONNECTED)"; diff --git a/interface/src/Bookmarks.cpp b/interface/src/Bookmarks.cpp index 9a8d8eb279..263723ebe0 100644 --- a/interface/src/Bookmarks.cpp +++ b/interface/src/Bookmarks.cpp @@ -61,7 +61,6 @@ void Bookmarks::deleteBookmark(const QString& bookmarkName) { void Bookmarks::addBookmarkToFile(const QString& bookmarkName, const QVariant& bookmark) { Menu* menubar = Menu::getInstance(); if (contains(bookmarkName)) { - auto offscreenUi = DependencyManager::get(); ModalDialogListener* dlg = OffscreenUi::asyncWarning("Duplicate Bookmark", "The bookmark name you entered already exists in your list.", QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes); diff --git a/interface/src/avatar/AvatarPackager.cpp b/interface/src/avatar/AvatarPackager.cpp index 90def7ad43..d43b7d9575 100644 --- a/interface/src/avatar/AvatarPackager.cpp +++ b/interface/src/avatar/AvatarPackager.cpp @@ -58,7 +58,9 @@ bool AvatarPackager::open() { if (tablet->getToolbarMode()) { static const QUrl url{ "hifi/AvatarPackagerWindow.qml" }; - DependencyManager::get()->show(url, "AvatarPackager", packageModelDialogCreated); + if (auto offscreenUI = DependencyManager::get()) { + offscreenUI->show(url, "AvatarPackager", packageModelDialogCreated); + } return true; } diff --git a/interface/src/raypick/ParabolaPointer.cpp b/interface/src/raypick/ParabolaPointer.cpp index 216248f8b5..2915793c40 100644 --- a/interface/src/raypick/ParabolaPointer.cpp +++ b/interface/src/raypick/ParabolaPointer.cpp @@ -415,7 +415,7 @@ gpu::PipelinePointer ParabolaPointer::RenderState::ParabolaRenderItem::getParabo for (auto& key : keys) { gpu::StatePointer state = gpu::StatePointer(new gpu::State()); - state->setDepthTest(true, true, gpu::LESS_EQUAL); + state->setDepthTest(true, !std::get<0>(key), gpu::LESS_EQUAL); if (std::get<0>(key)) { PrepareStencil::testMask(*state); } else { diff --git a/interface/src/scripting/AssetMappingsScriptingInterface.cpp b/interface/src/scripting/AssetMappingsScriptingInterface.cpp index c5769ef4bb..5b90474d23 100644 --- a/interface/src/scripting/AssetMappingsScriptingInterface.cpp +++ b/interface/src/scripting/AssetMappingsScriptingInterface.cpp @@ -76,8 +76,13 @@ void AssetMappingsScriptingInterface::uploadFile(QString path, QString mapping, "Use the field below to place your file in a specific folder or to rename it. " "Specifying a new folder name will automatically create that folder for you."; - auto offscreenUi = DependencyManager::get(); - auto result = offscreenUi->inputDialog(OffscreenUi::ICON_INFORMATION, "Specify Asset Path", + auto offscreenUI = DependencyManager::get(); + if (!offscreenUI) { + completedCallback.call({ -1 }); + return; + } + + auto result = offscreenUI->inputDialog(OffscreenUi::ICON_INFORMATION, "Specify Asset Path", dropEvent ? dropHelpText : helpText, mapping); if (!result.isValid() || result.toString() == "") { @@ -94,7 +99,7 @@ void AssetMappingsScriptingInterface::uploadFile(QString path, QString mapping, // Check for override if (isKnownMapping(mapping)) { auto message = mapping + "\n" + "This file already exists. Do you want to overwrite it?"; - auto button = offscreenUi->messageBox(OffscreenUi::ICON_QUESTION, "Overwrite File", message, + auto button = offscreenUI->messageBox(OffscreenUi::ICON_QUESTION, "Overwrite File", message, QMessageBox::Yes | QMessageBox::No, QMessageBox::No); if (button == QMessageBox::No) { completedCallback.call({ -1 }); diff --git a/interface/src/scripting/DesktopScriptingInterface.cpp b/interface/src/scripting/DesktopScriptingInterface.cpp index f78f7853ca..e527561b05 100644 --- a/interface/src/scripting/DesktopScriptingInterface.cpp +++ b/interface/src/scripting/DesktopScriptingInterface.cpp @@ -99,11 +99,16 @@ void DesktopScriptingInterface::setHUDAlpha(float alpha) { } void DesktopScriptingInterface::show(const QString& path, const QString& title) { + auto offscreenUI = DependencyManager::get(); + if (!offscreenUI) { + return; + } + if (QThread::currentThread() != thread()) { QMetaObject::invokeMethod(this, "show", Qt::QueuedConnection, Q_ARG(QString, path), Q_ARG(QString, title)); return; } - DependencyManager::get()->show(path, title); + offscreenUI->show(path, title); } InteractiveWindowPointer DesktopScriptingInterface::createWindow(const QString& sourceUrl, const QVariantMap& properties) { diff --git a/interface/src/scripting/HMDScriptingInterface.cpp b/interface/src/scripting/HMDScriptingInterface.cpp index 8f7ae7c4dc..79c0452a45 100644 --- a/interface/src/scripting/HMDScriptingInterface.cpp +++ b/interface/src/scripting/HMDScriptingInterface.cpp @@ -96,8 +96,9 @@ bool HMDScriptingInterface::shouldShowHandControllers() const { void HMDScriptingInterface::activateHMDHandMouse() { QWriteLocker lock(&_hmdHandMouseLock); - auto offscreenUi = DependencyManager::get(); - offscreenUi->getDesktop()->setProperty("hmdHandMouseActive", true); + if (auto offscreenUI = DependencyManager::get()) { + offscreenUI->getDesktop()->setProperty("hmdHandMouseActive", true); + } _hmdHandMouseCount++; } @@ -105,8 +106,9 @@ void HMDScriptingInterface::deactivateHMDHandMouse() { QWriteLocker lock(&_hmdHandMouseLock); _hmdHandMouseCount = std::max(_hmdHandMouseCount - 1, 0); if (_hmdHandMouseCount == 0) { - auto offscreenUi = DependencyManager::get(); - offscreenUi->getDesktop()->setProperty("hmdHandMouseActive", false); + if (auto offscreenUI = DependencyManager::get()) { + offscreenUI->getDesktop()->setProperty("hmdHandMouseActive", false); + } } } diff --git a/interface/src/scripting/PlatformInfoScriptingInterface.cpp b/interface/src/scripting/PlatformInfoScriptingInterface.cpp index 9adf514718..c1f325237e 100644 --- a/interface/src/scripting/PlatformInfoScriptingInterface.cpp +++ b/interface/src/scripting/PlatformInfoScriptingInterface.cpp @@ -217,7 +217,7 @@ PlatformInfoScriptingInterface::PlatformTier PlatformInfoScriptingInterface::get } QStringList PlatformInfoScriptingInterface::getPlatformTierNames() { - static const QStringList platformTierNames = { "UNKNWON", "LOW", "MID", "HIGH" }; + static const QStringList platformTierNames = { "UNKNOWN", "LOW", "MID", "HIGH" }; return platformTierNames; } diff --git a/interface/src/scripting/WindowScriptingInterface.cpp b/interface/src/scripting/WindowScriptingInterface.cpp index 14a0d04023..98335e5d3a 100644 --- a/interface/src/scripting/WindowScriptingInterface.cpp +++ b/interface/src/scripting/WindowScriptingInterface.cpp @@ -199,9 +199,9 @@ void WindowScriptingInterface::setInterstitialModeEnabled(bool enableInterstitia DependencyManager::get()->getDomainHandler().setInterstitialModeEnabled(enableInterstitialMode); } -bool WindowScriptingInterface::isPointOnDesktopWindow(QVariant point) { - auto offscreenUi = DependencyManager::get(); - return offscreenUi->isPointOnDesktopWindow(point); +bool WindowScriptingInterface::isPointOnDesktopWindow(QVariant point) { + auto offscreenUI = DependencyManager::get(); + return offscreenUI ? offscreenUI->isPointOnDesktopWindow(point) : false; } /// Makes sure that the reticle is visible, use this in blocking forms that require a reticle and @@ -553,12 +553,14 @@ int WindowScriptingInterface::openMessageBox(QString title, QString text, int bu * @typedef {number} Window.MessageBoxButton */ int WindowScriptingInterface::createMessageBox(QString title, QString text, int buttons, int defaultButton) { - auto messageBox = DependencyManager::get()->createMessageBox(OffscreenUi::ICON_INFORMATION, title, text, - static_cast>(buttons), static_cast(defaultButton)); - connect(messageBox, SIGNAL(selected(int)), this, SLOT(onMessageBoxSelected(int))); + if (auto offscreenUI = DependencyManager::get()) { + auto messageBox = offscreenUI->createMessageBox(OffscreenUi::ICON_INFORMATION, title, text, + static_cast>(buttons), static_cast(defaultButton)); + connect(messageBox, SIGNAL(selected(int)), this, SLOT(onMessageBoxSelected(int))); - _lastMessageBoxID += 1; - _messageBoxes.insert(_lastMessageBoxID, messageBox); + _lastMessageBoxID += 1; + _messageBoxes.insert(_lastMessageBoxID, messageBox); + } return _lastMessageBoxID; } @@ -646,13 +648,17 @@ void WindowScriptingInterface::setActiveDisplayPlugin(int index) { } void WindowScriptingInterface::openWebBrowser(const QString& url) { + auto offscreenUI = DependencyManager::get(); + if (!offscreenUI) { + return; + } + if (QThread::currentThread() != thread()) { QMetaObject::invokeMethod(this, "openWebBrowser", Q_ARG(const QString&, url)); return; } - auto offscreenUi = DependencyManager::get(); - offscreenUi->load("Browser.qml", [=](QQmlContext* context, QObject* newObject) { + offscreenUI->load("Browser.qml", [=](QQmlContext* context, QObject* newObject) { if (!url.isEmpty()) { newObject->setProperty("url", url); } diff --git a/interface/src/ui/ApplicationOverlay.cpp b/interface/src/ui/ApplicationOverlay.cpp index e91b1d725c..144f64c385 100644 --- a/interface/src/ui/ApplicationOverlay.cpp +++ b/interface/src/ui/ApplicationOverlay.cpp @@ -100,10 +100,10 @@ void ApplicationOverlay::renderQmlUi(RenderArgs* renderArgs) { // threads, we need to use a sync object to deteremine when // the current UI texture is no longer being read from, and only // then release it back to the UI for re-use - auto offscreenUi = DependencyManager::get(); + auto offscreenUI = DependencyManager::get(); OffscreenQmlSurface::TextureAndFence newTextureAndFence; - bool newTextureAvailable = offscreenUi->fetchTexture(newTextureAndFence); + bool newTextureAvailable = offscreenUI ? offscreenUI->fetchTexture(newTextureAndFence) : false; if (newTextureAvailable) { _uiTexture->setExternalTexture(newTextureAndFence.first, newTextureAndFence.second); } diff --git a/interface/src/ui/InteractiveWindow.cpp b/interface/src/ui/InteractiveWindow.cpp index 0ac1f05737..daf80acf00 100644 --- a/interface/src/ui/InteractiveWindow.cpp +++ b/interface/src/ui/InteractiveWindow.cpp @@ -362,10 +362,11 @@ InteractiveWindow::InteractiveWindow(const QString& sourceUrl, const QVariantMap object->setObjectName("InteractiveWindow"); object->setProperty(SOURCE_PROPERTY, sourceURL); }; - auto offscreenUi = DependencyManager::get(); - // Build the event bridge and wrapper on the main thread - offscreenUi->loadInNewContext(CONTENT_WINDOW_QML, objectInitLambda, contextInitLambda); + if (auto offscreenUI = DependencyManager::get()) { + // Build the event bridge and wrapper on the main thread + offscreenUI->loadInNewContext(CONTENT_WINDOW_QML, objectInitLambda, contextInitLambda); + } } } diff --git a/interface/src/ui/Stats.h b/interface/src/ui/Stats.h index 27ab375a42..ebd65de612 100644 --- a/interface/src/ui/Stats.h +++ b/interface/src/ui/Stats.h @@ -248,9 +248,9 @@ private: \ * Read-only. * @property {string} lodStatus - Description of the current LOD. * Read-only. - * @property {string} numEntityUpdates - The number of entity updates that happened last frame. + * @property {number} numEntityUpdates - The number of entity updates that happened last frame. * Read-only. - * @property {string} numNeededEntityUpdates - The total number of entity updates scheduled for last frame. + * @property {number} numNeededEntityUpdates - The total number of entity updates scheduled for last frame. * Read-only. * @property {string} timingStats - Details of the average time (ms) spent in and number of calls made to different parts of * the code. Provided only if timingExpanded is true. Only the top 10 items are provided if @@ -547,8 +547,8 @@ class Stats : public QQuickItem { STATS_PROPERTY(int, lodAngle, 0) STATS_PROPERTY(int, lodTargetFramerate, 0) STATS_PROPERTY(QString, lodStatus, QString()) - STATS_PROPERTY(int, numEntityUpdates, 0) - STATS_PROPERTY(int, numNeededEntityUpdates, 0) + STATS_PROPERTY(quint64, numEntityUpdates, 0) + STATS_PROPERTY(quint64, numNeededEntityUpdates, 0) STATS_PROPERTY(QString, timingStats, QString()) STATS_PROPERTY(QString, gameUpdateStats, QString()) STATS_PROPERTY(int, serverElements, 0) diff --git a/interface/src/ui/overlays/Overlays.cpp b/interface/src/ui/overlays/Overlays.cpp index 5e43c5df8d..e9e310e68b 100644 --- a/interface/src/ui/overlays/Overlays.cpp +++ b/interface/src/ui/overlays/Overlays.cpp @@ -1212,8 +1212,8 @@ float Overlays::width() { return result; } - auto offscreenUi = DependencyManager::get(); - return offscreenUi->getWindow()->size().width(); + auto offscreenUI = DependencyManager::get(); + return offscreenUI ? offscreenUI->getWindow()->size().width() : -1.0f; } float Overlays::height() { @@ -1224,8 +1224,8 @@ float Overlays::height() { return result; } - auto offscreenUi = DependencyManager::get(); - return offscreenUi->getWindow()->size().height(); + auto offscreenUI = DependencyManager::get(); + return offscreenUI ? offscreenUI->getWindow()->size().height() : -1.0f; } void Overlays::mousePressOnPointerEvent(const QUuid& id, const PointerEvent& event) { diff --git a/interface/src/ui/overlays/QmlOverlay.cpp b/interface/src/ui/overlays/QmlOverlay.cpp index 2afb29bb91..c097e7dd97 100644 --- a/interface/src/ui/overlays/QmlOverlay.cpp +++ b/interface/src/ui/overlays/QmlOverlay.cpp @@ -24,13 +24,17 @@ QmlOverlay::QmlOverlay(const QUrl& url, const QmlOverlay* overlay) } void QmlOverlay::buildQmlElement(const QUrl& url) { + auto offscreenUI = DependencyManager::get(); + if (!offscreenUI) { + return; + } + if (QThread::currentThread() != thread()) { QMetaObject::invokeMethod(this, "buildQmlElement", Q_ARG(QUrl, url)); return; } - auto offscreenUi = DependencyManager::get(); - offscreenUi->load(url, [=](QQmlContext* context, QObject* object) { + offscreenUI->load(url, [=](QQmlContext* context, QObject* object) { _qmlElement = dynamic_cast(object); connect(_qmlElement, &QObject::destroyed, this, &QmlOverlay::qmlElementDestroyed); }); diff --git a/libraries/avatars-renderer/src/avatars-renderer/SkeletonModel.cpp b/libraries/avatars-renderer/src/avatars-renderer/SkeletonModel.cpp index 4950b86f75..15ff09d13d 100644 --- a/libraries/avatars-renderer/src/avatars-renderer/SkeletonModel.cpp +++ b/libraries/avatars-renderer/src/avatars-renderer/SkeletonModel.cpp @@ -33,6 +33,7 @@ SkeletonModel::SkeletonModel(Avatar* owningAvatar, QObject* parent) : { // SkeletonModels, and by extention Avatars, use Dual Quaternion skinning. _useDualQuaternionSkinning = true; + _forceOffset = true; // Avatars all cast shadow setCanCastShadow(true); @@ -156,17 +157,13 @@ void SkeletonModel::simulate(float deltaTime, bool fullUpdate) { updateAttitude(_owningAvatar->getWorldOrientation()); setBlendshapeCoefficients(_owningAvatar->getHead()->getSummedBlendshapeCoefficients()); + Parent::simulate(deltaTime, fullUpdate); if (fullUpdate) { - - Parent::simulate(deltaTime, fullUpdate); - // let rig compute the model offset glm::vec3 registrationPoint; if (_rig.getModelRegistrationPoint(registrationPoint)) { setOffset(registrationPoint); } - } else { - Parent::simulate(deltaTime, fullUpdate); } // FIXME: This texture loading logic should probably live in Avatar, to mirror RenderableModelEntityItem, diff --git a/libraries/entities-renderer/src/EntityTreeRenderer.h b/libraries/entities-renderer/src/EntityTreeRenderer.h index f7623aad10..009e5f6c4f 100644 --- a/libraries/entities-renderer/src/EntityTreeRenderer.h +++ b/libraries/entities-renderer/src/EntityTreeRenderer.h @@ -136,8 +136,8 @@ public: static bool addMaterialToAvatar(const QUuid& avatarID, graphics::MaterialLayer material, const std::string& parentMaterialName); static bool removeMaterialFromAvatar(const QUuid& avatarID, graphics::MaterialPointer material, const std::string& parentMaterialName); - int getPrevNumEntityUpdates() const { return _prevNumEntityUpdates; } - int getPrevTotalNeededEntityUpdates() const { return _prevTotalNeededEntityUpdates; } + size_t getPrevNumEntityUpdates() const { return _prevNumEntityUpdates; } + size_t getPrevTotalNeededEntityUpdates() const { return _prevTotalNeededEntityUpdates; } signals: void enterEntity(const EntityItemID& entityItemID); @@ -253,8 +253,8 @@ private: ReadWriteLockable _changedEntitiesGuard; std::unordered_set _changedEntities; - int _prevNumEntityUpdates { 0 }; - int _prevTotalNeededEntityUpdates { 0 }; + size_t _prevNumEntityUpdates { 0 }; + size_t _prevTotalNeededEntityUpdates { 0 }; std::unordered_set _renderablesToUpdate; std::unordered_map _entitiesInScene; diff --git a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp index c694f496d6..6213853de7 100644 --- a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp @@ -149,10 +149,12 @@ void RenderableModelEntityItem::updateModelBounds() { bool overridingModelTransform = model->isOverridingModelTransformAndOffset(); glm::vec3 scaledDimensions = getScaledDimensions(); glm::vec3 registrationPoint = getRegistrationPoint(); + bool needsSimulate = false; if (!overridingModelTransform && (model->getScaleToFitDimensions() != scaledDimensions || - model->getRegistrationPoint() != registrationPoint || - !model->getIsScaledToFit() || _needsToRescaleModel)) { + model->getRegistrationPoint() != registrationPoint || + !model->getIsScaledToFit() || _needsToRescaleModel || + _useOriginalPivot == model->getSnapModelToRegistrationPoint())) { // The machinery for updateModelBounds will give existing models the opportunity to fix their // translation/rotation/scale/registration. The first two are straightforward, but the latter two // have guards to make sure they don't happen after they've already been set. Here we reset those guards. @@ -162,9 +164,9 @@ void RenderableModelEntityItem::updateModelBounds() { // now recalculate the bounds and registration model->setScaleToFit(true, scaledDimensions); - model->setSnapModelToRegistrationPoint(true, registrationPoint); + model->setSnapModelToRegistrationPoint(!_useOriginalPivot, registrationPoint); updateRenderItems = true; - model->scaleToFit(); + needsSimulate = true; _needsToRescaleModel = false; } @@ -176,10 +178,11 @@ void RenderableModelEntityItem::updateModelBounds() { updateRenderItems = true; } - if (_needsInitialSimulation || _needsJointSimulation || isAnimatingSomething()) { + if (_needsInitialSimulation || _needsJointSimulation || needsSimulate || isAnimatingSomething()) { // NOTE: on isAnimatingSomething() we need to call Model::simulate() which calls Rig::updateRig() // TODO: there is opportunity to further optimize the isAnimatingSomething() case. model->simulate(0.0f); + locationChanged(); _needsInitialSimulation = false; _needsJointSimulation = false; updateRenderItems = true; @@ -219,6 +222,16 @@ EntityItemProperties RenderableModelEntityItem::getProperties(const EntityProper return properties; } +glm::vec3 RenderableModelEntityItem::getPivot() const { + auto model = getModel(); + auto pivot = EntityItem::getPivot(); + if (!model || !model->isLoaded() || !_useOriginalPivot) { + return pivot; + } + + return pivot + model->getOriginalOffset(); +} + bool RenderableModelEntityItem::supportsDetailedIntersection() const { return true; } @@ -443,14 +456,15 @@ void RenderableModelEntityItem::computeShapeInfo(ShapeInfo& shapeInfo) { // multiply each point by scale before handing the point-set off to the physics engine. // also determine the extents of the collision model. glm::vec3 registrationOffset = dimensions * (ENTITY_ITEM_DEFAULT_REGISTRATION_POINT - getRegistrationPoint()); + glm::vec3 offset = model->getSnapModelToRegistrationPoint() ? model->getOffset() : glm::vec3(0.0f); for (int32_t i = 0; i < pointCollection.size(); i++) { for (int32_t j = 0; j < pointCollection[i].size(); j++) { // back compensate for registration so we can apply that offset to the shapeInfo later - pointCollection[i][j] = scaleToFit * (pointCollection[i][j] + model->getOffset()) - registrationOffset; + pointCollection[i][j] = scaleToFit * (pointCollection[i][j] + offset) - registrationOffset; } } - shapeInfo.setParams(type, 0.5f * extents, getCompoundShapeURL()); - adjustShapeInfoByRegistration(shapeInfo); + shapeInfo.setParams(type, 0.5f * extents, getCompoundShapeURL() + model->getSnapModelToRegistrationPoint()); + adjustShapeInfoByRegistration(shapeInfo, model->getSnapModelToRegistrationPoint()); } else if (type >= SHAPE_TYPE_SIMPLE_HULL && type <= SHAPE_TYPE_STATIC_MESH) { updateModelBounds(); model->updateGeometry(); @@ -682,8 +696,8 @@ void RenderableModelEntityItem::computeShapeInfo(ShapeInfo& shapeInfo) { } } - shapeInfo.setParams(type, 0.5f * extents.size(), getModelURL()); - adjustShapeInfoByRegistration(shapeInfo); + shapeInfo.setParams(type, 0.5f * extents.size(), getModelURL() + model->getSnapModelToRegistrationPoint()); + adjustShapeInfoByRegistration(shapeInfo, model->getSnapModelToRegistrationPoint()); } else { EntityItem::computeShapeInfo(shapeInfo); } diff --git a/libraries/entities-renderer/src/RenderableModelEntityItem.h b/libraries/entities-renderer/src/RenderableModelEntityItem.h index 4501f6d88c..1ef11b6906 100644 --- a/libraries/entities-renderer/src/RenderableModelEntityItem.h +++ b/libraries/entities-renderer/src/RenderableModelEntityItem.h @@ -42,7 +42,7 @@ protected: void setModel(const ModelPointer& model); ModelPointer getModel() const; - bool _needsInitialSimulation{ true }; + bool _needsInitialSimulation { true }; private: ModelPointer _model; }; @@ -63,6 +63,7 @@ public: virtual EntityItemProperties getProperties(const EntityPropertyFlags& desiredProperties, bool allowEmptyDesiredProperties) const override; void updateModelBounds(); + glm::vec3 getPivot() const override; virtual bool supportsDetailedIntersection() const override; virtual bool findDetailedRayIntersection(const glm::vec3& origin, const glm::vec3& direction, OctreeElementPointer& element, float& distance, diff --git a/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.cpp b/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.cpp index f0a6684654..d779409e9c 100644 --- a/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.cpp @@ -723,8 +723,10 @@ ShapeType RenderablePolyVoxEntityItem::getShapeType() const { } void RenderablePolyVoxEntityItem::setRegistrationPoint(const glm::vec3& value) { - if (value != _registrationPoint) { - _shapeReady = false; + if (value != getRegistrationPoint()) { + withWriteLock([&] { + _shapeReady = false; + }); EntityItem::setRegistrationPoint(value); startUpdates(); } diff --git a/libraries/entities/src/EntityItem.cpp b/libraries/entities/src/EntityItem.cpp index d7a5e992e1..4f80b8b5d1 100644 --- a/libraries/entities/src/EntityItem.cpp +++ b/libraries/entities/src/EntityItem.cpp @@ -1607,8 +1607,13 @@ void EntityItem::recordCreationTime() { const Transform EntityItem::getTransformToCenter(bool& success) const { Transform result = getTransform(success); - if (getRegistrationPoint() != ENTITY_ITEM_HALF_VEC3) { // If it is not already centered, translate to center - result.postTranslate((ENTITY_ITEM_HALF_VEC3 - getRegistrationPoint()) * getScaledDimensions()); // Position to center + glm::vec3 pivot = getPivot(); + if (pivot != ENTITY_ITEM_ZERO_VEC3) { + result.postTranslate(pivot); + } + glm::vec3 registrationPoint = getRegistrationPoint(); + if (registrationPoint != ENTITY_ITEM_HALF_VEC3) { // If it is not already centered, translate to center + result.postTranslate((ENTITY_ITEM_HALF_VEC3 - registrationPoint) * getScaledDimensions()); // Position to center } return result; } @@ -1623,15 +1628,16 @@ AACube EntityItem::getMaximumAACube(bool& success) const { _recalcMaxAACube = false; // we want to compute the furthestExtent that an entity can extend out from its "position" // to do this we compute the max of these two vec3s: registration and 1-registration - // and then scale by dimensions - glm::vec3 maxExtents = getScaledDimensions() * glm::max(_registrationPoint, glm::vec3(1.0f) - _registrationPoint); + // and then scale by dimensions and add the absolute value of the pivot + glm::vec3 registrationPoint = getRegistrationPoint(); + glm::vec3 maxExtents = getScaledDimensions() * glm::max(registrationPoint, glm::vec3(1.0f) - registrationPoint); // there exists a sphere that contains maxExtents for all rotations float radius = glm::length(maxExtents); // put a cube around the sphere // TODO? replace _maxAACube with _boundingSphereRadius - glm::vec3 minimumCorner = centerOfRotation - glm::vec3(radius, radius, radius); + glm::vec3 minimumCorner = (centerOfRotation + getWorldOrientation() * getPivot()) - glm::vec3(radius, radius, radius); _maxAACube = AACube(minimumCorner, radius * 2.0f); } } else { @@ -1650,9 +1656,12 @@ AACube EntityItem::getMinimumAACube(bool& success) const { if (success) { _recalcMinAACube = false; glm::vec3 dimensions = getScaledDimensions(); - glm::vec3 unrotatedMinRelativeToEntity = - (dimensions * _registrationPoint); - glm::vec3 unrotatedMaxRelativeToEntity = dimensions * (glm::vec3(1.0f, 1.0f, 1.0f) - _registrationPoint); + glm::vec3 registrationPoint = getRegistrationPoint(); + glm::vec3 pivot = getPivot(); + glm::vec3 unrotatedMinRelativeToEntity = -(dimensions * registrationPoint); + glm::vec3 unrotatedMaxRelativeToEntity = dimensions * (ENTITY_ITEM_ONE_VEC3 - registrationPoint); Extents extents = { unrotatedMinRelativeToEntity, unrotatedMaxRelativeToEntity }; + extents.shiftBy(pivot); extents.rotate(getWorldOrientation()); // shift the extents to be relative to the position/registration point @@ -1680,9 +1689,12 @@ AABox EntityItem::getAABox(bool& success) const { if (success) { _recalcAABox = false; glm::vec3 dimensions = getScaledDimensions(); - glm::vec3 unrotatedMinRelativeToEntity = - (dimensions * _registrationPoint); - glm::vec3 unrotatedMaxRelativeToEntity = dimensions * (glm::vec3(1.0f, 1.0f, 1.0f) - _registrationPoint); + glm::vec3 registrationPoint = getRegistrationPoint(); + glm::vec3 pivot = getPivot(); + glm::vec3 unrotatedMinRelativeToEntity = -(dimensions * registrationPoint); + glm::vec3 unrotatedMaxRelativeToEntity = dimensions * (ENTITY_ITEM_ONE_VEC3 - registrationPoint); Extents extents = { unrotatedMinRelativeToEntity, unrotatedMaxRelativeToEntity }; + extents.shiftBy(pivot); extents.rotate(getWorldOrientation()); // shift the extents to be relative to the position/registration point @@ -1722,12 +1734,22 @@ float EntityItem::getRadius() const { return 0.5f * glm::length(getScaledDimensions()); } -void EntityItem::adjustShapeInfoByRegistration(ShapeInfo& info) const { - if (_registrationPoint != ENTITY_ITEM_DEFAULT_REGISTRATION_POINT) { - glm::mat4 scale = glm::scale(getScaledDimensions()); - glm::mat4 registration = scale * glm::translate(ENTITY_ITEM_DEFAULT_REGISTRATION_POINT - getRegistrationPoint()); - glm::vec3 regTransVec = glm::vec3(registration[3]); // extract position component from matrix - info.setOffset(regTransVec); +void EntityItem::adjustShapeInfoByRegistration(ShapeInfo& info, bool includePivot) const { + glm::vec3 offset; + glm::vec3 registrationPoint = getRegistrationPoint(); + if (registrationPoint != ENTITY_ITEM_DEFAULT_REGISTRATION_POINT) { + offset += (ENTITY_ITEM_DEFAULT_REGISTRATION_POINT - registrationPoint) * getScaledDimensions(); + } + + if (includePivot) { + glm::vec3 pivot = getPivot(); + if (pivot != ENTITY_ITEM_ZERO_VEC3) { + offset += pivot; + } + } + + if (offset != ENTITY_ITEM_ZERO_VEC3) { + info.setOffset(offset); } } @@ -1739,7 +1761,7 @@ bool EntityItem::contains(const glm::vec3& point) const { // anything with shapeType == SPHERE must collide as a bounding sphere in the world-frame regardless of dimensions // therefore we must do math using an unscaled localPoint relative to sphere center glm::vec3 dimensions = getScaledDimensions(); - glm::vec3 localPoint = point - (getWorldPosition() + getWorldOrientation() * (dimensions * (ENTITY_ITEM_DEFAULT_REGISTRATION_POINT - getRegistrationPoint()))); + glm::vec3 localPoint = point - (getWorldPosition() + getWorldOrientation() * (dimensions * (ENTITY_ITEM_DEFAULT_REGISTRATION_POINT - getRegistrationPoint()) + getPivot())); const float HALF_SQUARED = 0.25f; return glm::length2(localPoint) < HALF_SQUARED * glm::length2(dimensions); } @@ -1797,11 +1819,16 @@ float EntityItem::getVolumeEstimate() const { } void EntityItem::setRegistrationPoint(const glm::vec3& value) { - if (value != _registrationPoint) { - withWriteLock([&] { + bool changed = false; + withWriteLock([&] { + if (value != _registrationPoint) { _registrationPoint = glm::clamp(value, glm::vec3(ENTITY_ITEM_MIN_REGISTRATION_POINT), glm::vec3(ENTITY_ITEM_MAX_REGISTRATION_POINT)); - }); + changed = true; + } + }); + + if (changed) { dimensionsChanged(); // Registration Point affects the bounding box markDirtyFlags(Simulation::DIRTY_SHAPE); } @@ -2892,11 +2919,9 @@ QString EntityItem::getCollisionSoundURL() const { } glm::vec3 EntityItem::getRegistrationPoint() const { - glm::vec3 result; - withReadLock([&] { - result = _registrationPoint; + return resultWithReadLock([&] { + return _registrationPoint; }); - return result; } float EntityItem::getAngularDamping() const { diff --git a/libraries/entities/src/EntityItem.h b/libraries/entities/src/EntityItem.h index 2a6952fc0d..bc3fd8b61e 100644 --- a/libraries/entities/src/EntityItem.h +++ b/libraries/entities/src/EntityItem.h @@ -204,6 +204,7 @@ public: virtual glm::vec3 getScaledDimensions() const; virtual void setScaledDimensions(const glm::vec3& value); virtual glm::vec3 getRaycastDimensions() const { return getScaledDimensions(); } + virtual glm::vec3 getPivot() const { return glm::vec3(0.0f); } // pivot offset for positioning, mainly for model entities glm::vec3 getUnscaledDimensions() const; virtual void setUnscaledDimensions(const glm::vec3& value); @@ -403,7 +404,7 @@ public: // TODO: get rid of users of getRadius()... float getRadius() const; - virtual void adjustShapeInfoByRegistration(ShapeInfo& info) const; + virtual void adjustShapeInfoByRegistration(ShapeInfo& info, bool includePivot = true) const; virtual bool contains(const glm::vec3& point) const; virtual bool isReadyToComputeShape() const { return !isDead(); } diff --git a/libraries/entities/src/EntityItemProperties.cpp b/libraries/entities/src/EntityItemProperties.cpp index b7fa84e623..47871736de 100644 --- a/libraries/entities/src/EntityItemProperties.cpp +++ b/libraries/entities/src/EntityItemProperties.cpp @@ -538,6 +538,7 @@ EntityPropertyFlags EntityItemProperties::getChangedProperties() const { CHECK_PROPERTY_CHANGE(PROP_RELAY_PARENT_JOINTS, relayParentJoints); CHECK_PROPERTY_CHANGE(PROP_GROUP_CULLED, groupCulled); CHECK_PROPERTY_CHANGE(PROP_BLENDSHAPE_COEFFICIENTS, blendshapeCoefficients); + CHECK_PROPERTY_CHANGE(PROP_USE_ORIGINAL_PIVOT, useOriginalPivot); changedProperties += _animation.getChangedProperties(); // Light @@ -1003,6 +1004,9 @@ EntityPropertyFlags EntityItemProperties::getChangedProperties() const { * @property {string} blendshapeCoefficients - A JSON string of a map of blendshape names to values. Only stores set values. * When editing this property, only coefficients that you are editing will change; it will not explicitly reset other * coefficients. + * @property {boolean} useOriginalPivot=false - If false, the model will be centered based on its content, + * ignoring any offset in the model itself. If true, the model will respect its original offset. Currently, + * only pivots relative to {x: 0, y: 0, z: 0} are supported. * @property {string} textures="" - A JSON string of texture name, URL pairs used when rendering the model in place of the * model's original textures. Use a texture name from the originalTextures property to override that texture. * Only the texture names and URLs to be overridden need be specified; original textures are used where there are no @@ -1737,6 +1741,7 @@ QScriptValue EntityItemProperties::copyToScriptValue(QScriptEngine* engine, bool COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_RELAY_PARENT_JOINTS, relayParentJoints); COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_GROUP_CULLED, groupCulled); COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_BLENDSHAPE_COEFFICIENTS, blendshapeCoefficients); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_USE_ORIGINAL_PIVOT, useOriginalPivot); if (!psuedoPropertyFlagsButDesiredEmpty) { _animation.copyToScriptValue(_desiredProperties, properties, engine, skipDefaults, defaultEntityProperties); } @@ -2147,6 +2152,7 @@ void EntityItemProperties::copyFromScriptValue(const QScriptValue& object, bool COPY_PROPERTY_FROM_QSCRIPTVALUE(relayParentJoints, bool, setRelayParentJoints); COPY_PROPERTY_FROM_QSCRIPTVALUE(groupCulled, bool, setGroupCulled); COPY_PROPERTY_FROM_QSCRIPTVALUE(blendshapeCoefficients, QString, setBlendshapeCoefficients); + COPY_PROPERTY_FROM_QSCRIPTVALUE(useOriginalPivot, bool, setUseOriginalPivot); _animation.copyFromScriptValue(object, _defaultSettings); // Light @@ -2440,6 +2446,7 @@ void EntityItemProperties::merge(const EntityItemProperties& other) { COPY_PROPERTY_IF_CHANGED(relayParentJoints); COPY_PROPERTY_IF_CHANGED(groupCulled); COPY_PROPERTY_IF_CHANGED(blendshapeCoefficients); + COPY_PROPERTY_IF_CHANGED(useOriginalPivot); _animation.merge(other._animation); // Light @@ -2797,6 +2804,7 @@ bool EntityItemProperties::getPropertyInfo(const QString& propertyName, EntityPr ADD_PROPERTY_TO_MAP(PROP_RELAY_PARENT_JOINTS, RelayParentJoints, relayParentJoints, bool); ADD_PROPERTY_TO_MAP(PROP_GROUP_CULLED, GroupCulled, groupCulled, bool); ADD_PROPERTY_TO_MAP(PROP_BLENDSHAPE_COEFFICIENTS, BlendshapeCoefficients, blendshapeCoefficients, QString); + ADD_PROPERTY_TO_MAP(PROP_USE_ORIGINAL_PIVOT, UseOriginalPivot, useOriginalPivot, bool); { // Animation ADD_GROUP_PROPERTY_TO_MAP(PROP_ANIMATION_URL, Animation, animation, URL, url); ADD_GROUP_PROPERTY_TO_MAP(PROP_ANIMATION_ALLOW_TRANSLATION, Animation, animation, AllowTranslation, allowTranslation); @@ -3239,6 +3247,7 @@ OctreeElement::AppendState EntityItemProperties::encodeEntityEditPacket(PacketTy APPEND_ENTITY_PROPERTY(PROP_RELAY_PARENT_JOINTS, properties.getRelayParentJoints()); APPEND_ENTITY_PROPERTY(PROP_GROUP_CULLED, properties.getGroupCulled()); APPEND_ENTITY_PROPERTY(PROP_BLENDSHAPE_COEFFICIENTS, properties.getBlendshapeCoefficients()); + APPEND_ENTITY_PROPERTY(PROP_USE_ORIGINAL_PIVOT, properties.getUseOriginalPivot()); _staticAnimation.setProperties(properties); _staticAnimation.appendToEditPacket(packetData, requestedProperties, propertyFlags, propertiesDidntFit, propertyCount, appendState); @@ -3729,6 +3738,7 @@ bool EntityItemProperties::decodeEntityEditPacket(const unsigned char* data, int READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_RELAY_PARENT_JOINTS, bool, setRelayParentJoints); READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_GROUP_CULLED, bool, setGroupCulled); READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_BLENDSHAPE_COEFFICIENTS, QString, setBlendshapeCoefficients); + READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_USE_ORIGINAL_PIVOT, bool, setUseOriginalPivot); properties.getAnimation().decodeFromEditPacket(propertyFlags, dataAt, processedBytes); } @@ -4139,6 +4149,7 @@ void EntityItemProperties::markAllChanged() { _relayParentJointsChanged = true; _groupCulledChanged = true; _blendshapeCoefficientsChanged = true; + _useOriginalPivotChanged = true; _animation.markAllChanged(); // Light @@ -4712,6 +4723,9 @@ QList EntityItemProperties::listChangedProperties() { if (blendshapeCoefficientsChanged()) { out += "blendshapeCoefficients"; } + if (useOriginalPivotChanged()) { + out += "useOriginalPivot"; + } getAnimation().listChangedProperties(out); // Light diff --git a/libraries/entities/src/EntityItemProperties.h b/libraries/entities/src/EntityItemProperties.h index df163e2cd6..40c376e0e3 100644 --- a/libraries/entities/src/EntityItemProperties.h +++ b/libraries/entities/src/EntityItemProperties.h @@ -302,6 +302,7 @@ public: DEFINE_PROPERTY(PROP_RELAY_PARENT_JOINTS, RelayParentJoints, relayParentJoints, bool, ENTITY_ITEM_DEFAULT_RELAY_PARENT_JOINTS); DEFINE_PROPERTY_REF(PROP_GROUP_CULLED, GroupCulled, groupCulled, bool, false); DEFINE_PROPERTY_REF(PROP_BLENDSHAPE_COEFFICIENTS, BlendshapeCoefficients, blendshapeCoefficients, QString, ""); + DEFINE_PROPERTY_REF(PROP_USE_ORIGINAL_PIVOT, UseOriginalPivot, useOriginalPivot, bool, false); DEFINE_PROPERTY_GROUP(Animation, animation, AnimationPropertyGroup); // Light diff --git a/libraries/entities/src/EntityPropertyFlags.h b/libraries/entities/src/EntityPropertyFlags.h index 76585450b8..aed0d6c458 100644 --- a/libraries/entities/src/EntityPropertyFlags.h +++ b/libraries/entities/src/EntityPropertyFlags.h @@ -218,16 +218,17 @@ enum EntityPropertyList { PROP_RELAY_PARENT_JOINTS = PROP_DERIVED_6, PROP_GROUP_CULLED = PROP_DERIVED_7, PROP_BLENDSHAPE_COEFFICIENTS = PROP_DERIVED_8, + PROP_USE_ORIGINAL_PIVOT = PROP_DERIVED_9, // Animation - PROP_ANIMATION_URL = PROP_DERIVED_9, - PROP_ANIMATION_ALLOW_TRANSLATION = PROP_DERIVED_10, - PROP_ANIMATION_FPS = PROP_DERIVED_11, - PROP_ANIMATION_FRAME_INDEX = PROP_DERIVED_12, - PROP_ANIMATION_PLAYING = PROP_DERIVED_13, - PROP_ANIMATION_LOOP = PROP_DERIVED_14, - PROP_ANIMATION_FIRST_FRAME = PROP_DERIVED_15, - PROP_ANIMATION_LAST_FRAME = PROP_DERIVED_16, - PROP_ANIMATION_HOLD = PROP_DERIVED_17, + PROP_ANIMATION_URL = PROP_DERIVED_10, + PROP_ANIMATION_ALLOW_TRANSLATION = PROP_DERIVED_11, + PROP_ANIMATION_FPS = PROP_DERIVED_12, + PROP_ANIMATION_FRAME_INDEX = PROP_DERIVED_13, + PROP_ANIMATION_PLAYING = PROP_DERIVED_14, + PROP_ANIMATION_LOOP = PROP_DERIVED_15, + PROP_ANIMATION_FIRST_FRAME = PROP_DERIVED_16, + PROP_ANIMATION_LAST_FRAME = PROP_DERIVED_17, + PROP_ANIMATION_HOLD = PROP_DERIVED_18, // Light PROP_IS_SPOTLIGHT = PROP_DERIVED_0, diff --git a/libraries/entities/src/EntityTreeElement.cpp b/libraries/entities/src/EntityTreeElement.cpp index 9af0bbfdb6..66d44d89de 100644 --- a/libraries/entities/src/EntityTreeElement.cpp +++ b/libraries/entities/src/EntityTreeElement.cpp @@ -205,10 +205,7 @@ EntityItemID EntityTreeElement::evalDetailedRayIntersection(const glm::vec3& ori // (this is faster and more likely to cull results than the filter check below so we do it first) bool success; AABox entityBox = entity->getAABox(success); - if (!success) { - return; - } - if (!entityBox.rayHitsBoundingSphere(origin, direction)) { + if (!success || !entityBox.rayHitsBoundingSphere(origin, direction)) { return; } @@ -226,7 +223,7 @@ EntityItemID EntityTreeElement::evalDetailedRayIntersection(const glm::vec3& ori glm::vec3 dimensions = entity->getRaycastDimensions(); glm::vec3 registrationPoint = entity->getRegistrationPoint(); - glm::vec3 corner = -(dimensions * registrationPoint); + glm::vec3 corner = -(dimensions * registrationPoint) + entity->getPivot(); AABox entityFrameBox(corner, dimensions); @@ -277,11 +274,12 @@ bool EntityTreeElement::findSpherePenetration(const glm::vec3& center, float rad bool result = false; withReadLock([&] { foreach(EntityItemPointer entity, _entityItems) { - glm::vec3 entityCenter = entity->getWorldPosition(); + bool success; + glm::vec3 entityCenter = entity->getCenterPosition(success); float entityRadius = entity->getRadius(); // don't penetrate yourself - if (entityCenter == center && entityRadius == radius) { + if (!success || (entityCenter == center && entityRadius == radius)) { return; } @@ -349,15 +347,12 @@ EntityItemID EntityTreeElement::evalDetailedParabolaIntersection(const glm::vec3 // (this is faster and more likely to cull results than the filter check below so we do it first) bool success; AABox entityBox = entity->getAABox(success); - if (!success) { - return; - } // Instead of checking parabolaInstersectsBoundingSphere here, we are just going to check if the plane // defined by the parabola slices the sphere. The solution to parabolaIntersectsBoundingSphere is cubic, // the solution to which is more computationally expensive than the quadratic AABox::findParabolaIntersection // below - if (!entityBox.parabolaPlaneIntersectsBoundingSphere(origin, velocity, acceleration, normal)) { + if (!success || !entityBox.parabolaPlaneIntersectsBoundingSphere(origin, velocity, acceleration, normal)) { return; } @@ -375,7 +370,7 @@ EntityItemID EntityTreeElement::evalDetailedParabolaIntersection(const glm::vec3 glm::vec3 dimensions = entity->getRaycastDimensions(); glm::vec3 registrationPoint = entity->getRegistrationPoint(); - glm::vec3 corner = -(dimensions * registrationPoint); + glm::vec3 corner = -(dimensions * registrationPoint) + entity->getPivot(); AABox entityFrameBox(corner, dimensions); @@ -445,7 +440,6 @@ void EntityTreeElement::evalEntitiesInSphere(const glm::vec3& position, float ra bool success; AABox entityBox = entity->getAABox(success); - // if the sphere doesn't intersect with our world frame AABox, we don't need to consider the more complex case glm::vec3 penetration; if (success && entityBox.findSpherePenetration(position, radius, penetration)) { @@ -464,10 +458,9 @@ void EntityTreeElement::evalEntitiesInSphere(const glm::vec3& position, float ra float entityTrueRadius = dimensions.x / 2.0f; bool success; - if (findSphereSpherePenetration(position, radius, entity->getCenterPosition(success), entityTrueRadius, penetration)) { - if (success) { - foundEntities.push_back(entity->getID()); - } + glm::vec3 center = entity->getCenterPosition(success); + if (success && findSphereSpherePenetration(position, radius, center, entityTrueRadius, penetration)) { + foundEntities.push_back(entity->getID()); } } else { // determine the worldToEntityMatrix that doesn't include scale because @@ -478,7 +471,7 @@ void EntityTreeElement::evalEntitiesInSphere(const glm::vec3& position, float ra glm::mat4 worldToEntityMatrix = glm::inverse(entityToWorldMatrix); glm::vec3 registrationPoint = entity->getRegistrationPoint(); - glm::vec3 corner = -(dimensions * registrationPoint); + glm::vec3 corner = -(dimensions * registrationPoint) + entity->getPivot(); AABox entityFrameBox(corner, dimensions); @@ -499,7 +492,6 @@ void EntityTreeElement::evalEntitiesInSphereWithType(const glm::vec3& position, bool success; AABox entityBox = entity->getAABox(success); - // if the sphere doesn't intersect with our world frame AABox, we don't need to consider the more complex case glm::vec3 penetration; if (success && entityBox.findSpherePenetration(position, radius, penetration)) { @@ -518,10 +510,9 @@ void EntityTreeElement::evalEntitiesInSphereWithType(const glm::vec3& position, float entityTrueRadius = dimensions.x / 2.0f; bool success; - if (findSphereSpherePenetration(position, radius, entity->getCenterPosition(success), entityTrueRadius, penetration)) { - if (success) { - foundEntities.push_back(entity->getID()); - } + glm::vec3 center = entity->getCenterPosition(success); + if (success && findSphereSpherePenetration(position, radius, center, entityTrueRadius, penetration)) { + foundEntities.push_back(entity->getID()); } } else { // determine the worldToEntityMatrix that doesn't include scale because @@ -532,7 +523,7 @@ void EntityTreeElement::evalEntitiesInSphereWithType(const glm::vec3& position, glm::mat4 worldToEntityMatrix = glm::inverse(entityToWorldMatrix); glm::vec3 registrationPoint = entity->getRegistrationPoint(); - glm::vec3 corner = -(dimensions * registrationPoint); + glm::vec3 corner = -(dimensions * registrationPoint) + entity->getPivot(); AABox entityFrameBox(corner, dimensions); @@ -575,12 +566,11 @@ void EntityTreeElement::evalEntitiesInSphereWithName(const glm::vec3& position, // NOTE: entity->getRadius() doesn't return the true radius, it returns the radius of the // maximum bounding sphere, which is actually larger than our actual radius float entityTrueRadius = dimensions.x / 2.0f; - bool success; - if (findSphereSpherePenetration(position, radius, entity->getCenterPosition(success), entityTrueRadius, penetration)) { - if (success) { - foundEntities.push_back(entity->getID()); - } + glm::vec3 center = entity->getCenterPosition(success); + + if (success && findSphereSpherePenetration(position, radius, center, entityTrueRadius, penetration)) { + foundEntities.push_back(entity->getID()); } } else { // determine the worldToEntityMatrix that doesn't include scale because @@ -591,7 +581,7 @@ void EntityTreeElement::evalEntitiesInSphereWithName(const glm::vec3& position, glm::mat4 worldToEntityMatrix = glm::inverse(entityToWorldMatrix); glm::vec3 registrationPoint = entity->getRegistrationPoint(); - glm::vec3 corner = -(dimensions * registrationPoint); + glm::vec3 corner = -(dimensions * registrationPoint) + entity->getPivot(); AABox entityFrameBox(corner, dimensions); @@ -612,6 +602,7 @@ void EntityTreeElement::evalEntitiesInCube(const AACube& cube, PickFilter search bool success; AABox entityBox = entity->getAABox(success); + // FIXME - handle entity->getShapeType() == SHAPE_TYPE_SPHERE case better // FIXME - consider allowing the entity to determine penetration so that // entities could presumably dull actuall hull testing if they wanted to @@ -642,6 +633,7 @@ void EntityTreeElement::evalEntitiesInBox(const AABox& box, PickFilter searchFil bool success; AABox entityBox = entity->getAABox(success); + // FIXME - handle entity->getShapeType() == SHAPE_TYPE_SPHERE case better // FIXME - consider allowing the entity to determine penetration so that // entities could presumably dull actuall hull testing if they wanted to @@ -680,7 +672,7 @@ void EntityTreeElement::evalEntitiesInFrustum(const ViewFrustum& frustum, PickFi }); } -void EntityTreeElement::getEntities(EntityItemFilter& filter, QVector& foundEntities) { +void EntityTreeElement::getEntities(EntityItemFilter& filter, QVector& foundEntities) { forEachEntity([&](EntityItemPointer entity) { if (filter(entity)) { foundEntities.push_back(entity); diff --git a/libraries/entities/src/ModelEntityItem.cpp b/libraries/entities/src/ModelEntityItem.cpp index 4716db6a41..61362896ee 100644 --- a/libraries/entities/src/ModelEntityItem.cpp +++ b/libraries/entities/src/ModelEntityItem.cpp @@ -73,6 +73,7 @@ EntityItemProperties ModelEntityItem::getProperties(const EntityPropertyFlags& d COPY_ENTITY_PROPERTY_TO_PROPERTIES(relayParentJoints, getRelayParentJoints); COPY_ENTITY_PROPERTY_TO_PROPERTIES(groupCulled, getGroupCulled); COPY_ENTITY_PROPERTY_TO_PROPERTIES(blendshapeCoefficients, getBlendshapeCoefficients); + COPY_ENTITY_PROPERTY_TO_PROPERTIES(useOriginalPivot, getUseOriginalPivot); withReadLock([&] { _animationProperties.getProperties(properties); }); @@ -96,6 +97,7 @@ bool ModelEntityItem::setSubClassProperties(const EntityItemProperties& properti SET_ENTITY_PROPERTY_FROM_PROPERTIES(relayParentJoints, setRelayParentJoints); SET_ENTITY_PROPERTY_FROM_PROPERTIES(groupCulled, setGroupCulled); SET_ENTITY_PROPERTY_FROM_PROPERTIES(blendshapeCoefficients, setBlendshapeCoefficients); + SET_ENTITY_PROPERTY_FROM_PROPERTIES(useOriginalPivot, setUseOriginalPivot); withWriteLock([&] { AnimationPropertyGroup animationProperties = _animationProperties; @@ -130,6 +132,7 @@ int ModelEntityItem::readEntitySubclassDataFromBuffer(const unsigned char* data, READ_ENTITY_PROPERTY(PROP_RELAY_PARENT_JOINTS, bool, setRelayParentJoints); READ_ENTITY_PROPERTY(PROP_GROUP_CULLED, bool, setGroupCulled); READ_ENTITY_PROPERTY(PROP_BLENDSHAPE_COEFFICIENTS, QString, setBlendshapeCoefficients); + READ_ENTITY_PROPERTY(PROP_USE_ORIGINAL_PIVOT, bool, setUseOriginalPivot); // grab a local copy of _animationProperties to avoid multiple locks int bytesFromAnimation; @@ -169,6 +172,7 @@ EntityPropertyFlags ModelEntityItem::getEntityProperties(EncodeBitstreamParams& requestedProperties += PROP_RELAY_PARENT_JOINTS; requestedProperties += PROP_GROUP_CULLED; requestedProperties += PROP_BLENDSHAPE_COEFFICIENTS; + requestedProperties += PROP_USE_ORIGINAL_PIVOT; requestedProperties += _animationProperties.getEntityProperties(params); return requestedProperties; @@ -198,6 +202,7 @@ void ModelEntityItem::appendSubclassData(OctreePacketData* packetData, EncodeBit APPEND_ENTITY_PROPERTY(PROP_RELAY_PARENT_JOINTS, getRelayParentJoints()); APPEND_ENTITY_PROPERTY(PROP_GROUP_CULLED, getGroupCulled()); APPEND_ENTITY_PROPERTY(PROP_BLENDSHAPE_COEFFICIENTS, getBlendshapeCoefficients()); + APPEND_ENTITY_PROPERTY(PROP_USE_ORIGINAL_PIVOT, getUseOriginalPivot()); withReadLock([&] { _animationProperties.appendSubclassData(packetData, params, entityTreeElementExtraEncodeData, requestedProperties, @@ -251,6 +256,7 @@ void ModelEntityItem::debugDump() const { qCDebug(entities) << " model URL:" << getModelURL(); qCDebug(entities) << " compound shape URL:" << getCompoundShapeURL(); qCDebug(entities) << " blendshapeCoefficients:" << getBlendshapeCoefficients(); + qCDebug(entities) << " useOrigialPivot:" << getUseOriginalPivot(); } void ModelEntityItem::setShapeType(ShapeType type) { @@ -713,3 +719,25 @@ QVector ModelEntityItem::getBlendshapeCoefficientVector() { return _blendshapeCoefficientsVector; }); } + +void ModelEntityItem::setUseOriginalPivot(bool value) { + bool changed = false; + withWriteLock([&] { + if (_useOriginalPivot != value) { + _needsRenderUpdate = true; + _useOriginalPivot = value; + changed = true; + } + }); + + if (changed) { + markDirtyFlags(Simulation::DIRTY_SHAPE | Simulation::DIRTY_MASS); + locationChanged(); + } +} + +bool ModelEntityItem::getUseOriginalPivot() const { + return resultWithReadLock([&] { + return _useOriginalPivot; + }); +} diff --git a/libraries/entities/src/ModelEntityItem.h b/libraries/entities/src/ModelEntityItem.h index b835b48d13..6e92b225a1 100644 --- a/libraries/entities/src/ModelEntityItem.h +++ b/libraries/entities/src/ModelEntityItem.h @@ -119,6 +119,9 @@ public: bool blendshapesChanged() const { return _blendshapesChanged; } QVector getBlendshapeCoefficientVector(); + bool getUseOriginalPivot() const; + void setUseOriginalPivot(bool useOriginalPivot); + private: void setAnimationSettings(const QString& value); // only called for old bitstream format bool applyNewAnimationProperties(AnimationPropertyGroup newProperties); @@ -152,6 +155,7 @@ protected: bool _relayParentJoints; bool _groupCulled { false }; QVariantMap _blendshapeCoefficientsMap; + bool _useOriginalPivot { false }; ThreadSafeValueCache _compoundShapeURL; diff --git a/libraries/hfm/src/hfm/HFM.cpp b/libraries/hfm/src/hfm/HFM.cpp index 8fb0720c0d..dd13d6d4f3 100644 --- a/libraries/hfm/src/hfm/HFM.cpp +++ b/libraries/hfm/src/hfm/HFM.cpp @@ -104,7 +104,7 @@ bool HFMModel::convexHullContains(const glm::vec3& point) const { auto checkEachPrimitive = [=](HFMMesh& mesh, QVector indices, int primitiveSize) -> bool { // Check whether the point is "behind" all the primitives. // But first must transform from model-frame into mesh-frame - glm::vec3 transformedPoint = glm::vec3(glm::inverse(mesh.modelTransform) * glm::vec4(point, 1.0f)); + glm::vec3 transformedPoint = glm::vec3(glm::inverse(offset * mesh.modelTransform) * glm::vec4(point, 1.0f)); int verticesSize = mesh.vertices.size(); for (int j = 0; j < indices.size() - 2; // -2 in case the vertices aren't the right size -- we access j + 2 below diff --git a/libraries/networking/src/udt/PacketHeaders.h b/libraries/networking/src/udt/PacketHeaders.h index ced3d8756b..5d105dc38f 100644 --- a/libraries/networking/src/udt/PacketHeaders.h +++ b/libraries/networking/src/udt/PacketHeaders.h @@ -283,6 +283,7 @@ enum class EntityVersion : PacketVersion { ZoneOcclusion, ModelBlendshapes, TransparentWeb, + UseOriginalPivot, UserAgent, // Add new versions above here diff --git a/libraries/procedural/src/procedural/Procedural.cpp b/libraries/procedural/src/procedural/Procedural.cpp index 88c111f8fd..66dde1ca56 100644 --- a/libraries/procedural/src/procedural/Procedural.cpp +++ b/libraries/procedural/src/procedural/Procedural.cpp @@ -125,7 +125,7 @@ Procedural::Procedural() { opaqueStencil(_opaqueState); _transparentState->setCullMode(gpu::State::CULL_NONE); - _transparentState->setDepthTest(true, true, gpu::LESS_EQUAL); + _transparentState->setDepthTest(true, false, gpu::LESS_EQUAL); _transparentState->setBlendFunction(true, gpu::State::SRC_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::INV_SRC_ALPHA, gpu::State::FACTOR_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::ONE); diff --git a/libraries/render-utils/src/GeometryCache.cpp b/libraries/render-utils/src/GeometryCache.cpp index ea66ac19ec..325e228120 100644 --- a/libraries/render-utils/src/GeometryCache.cpp +++ b/libraries/render-utils/src/GeometryCache.cpp @@ -2027,7 +2027,7 @@ void GeometryCache::useGridPipeline(gpu::Batch& batch, GridBuffer gridBuffer, bo for (auto& key : keys) { gpu::StatePointer state = gpu::StatePointer(new gpu::State()); - state->setDepthTest(true, true, gpu::LESS_EQUAL); + state->setDepthTest(true, !std::get<0>(key), gpu::LESS_EQUAL); if (std::get<0>(key)) { PrepareStencil::testMask(*state); } else { @@ -2135,7 +2135,7 @@ gpu::PipelinePointer GeometryCache::getWebBrowserProgram(bool transparent, bool auto pipeline = (transparent || forward) ? web_browser_forward : web_browser; gpu::StatePointer state = gpu::StatePointer(new gpu::State()); - state->setDepthTest(true, true, gpu::LESS_EQUAL); + state->setDepthTest(true, !transparent, gpu::LESS_EQUAL); // FIXME: do we need a testMaskDrawNoAA? PrepareStencil::testMaskDrawShapeNoAA(*state); state->setBlendFunction(transparent, @@ -2207,7 +2207,7 @@ gpu::PipelinePointer GeometryCache::getSimplePipeline(bool textured, bool transp } else { state->setCullMode(gpu::State::CULL_BACK); } - state->setDepthTest(true, true, gpu::LESS_EQUAL); + state->setDepthTest(true, !config.isTransparent(), gpu::LESS_EQUAL); if (config.hasDepthBias()) { state->setDepthBias(1.0f); state->setDepthBiasSlopeScale(1.0f); diff --git a/libraries/render-utils/src/Model.cpp b/libraries/render-utils/src/Model.cpp index 9ad343639d..521214354e 100644 --- a/libraries/render-utils/src/Model.cpp +++ b/libraries/render-utils/src/Model.cpp @@ -343,7 +343,11 @@ bool Model::findRayIntersectionAgainstSubMeshes(const glm::vec3& origin, const g } // extents is the entity relative, scaled, centered extents of the entity - glm::mat4 modelToWorldMatrix = createMatFromQuatAndPos(_rotation, _translation); + glm::mat4 transRot = createMatFromQuatAndPos(_rotation, _translation); + glm::mat4 modelToWorldMatrix = transRot; + if (!_snapModelToRegistrationPoint) { + modelToWorldMatrix = modelToWorldMatrix * glm::translate(getOriginalOffset()); + } glm::mat4 worldToModelMatrix = glm::inverse(modelToWorldMatrix); Extents modelExtents = getMeshExtents(); // NOTE: unrotated @@ -375,8 +379,12 @@ bool Model::findRayIntersectionAgainstSubMeshes(const glm::vec3& origin, const g calculateTriangleSets(hfmModel); } - glm::mat4 meshToModelMatrix = glm::scale(_scale) * glm::translate(_offset); - glm::mat4 meshToWorldMatrix = modelToWorldMatrix * meshToModelMatrix; + glm::mat4 meshToWorldMatrix = transRot; + if (_snapModelToRegistrationPoint || _forceOffset) { + meshToWorldMatrix = meshToWorldMatrix * (glm::scale(_scale) * glm::translate(_offset)); + } else { + meshToWorldMatrix = meshToWorldMatrix * (glm::scale(_scale) * glm::translate(getNaturalDimensions() * (0.5f - _registrationPoint))); + } glm::mat4 worldToMeshMatrix = glm::inverse(meshToWorldMatrix); glm::vec3 meshFrameOrigin = glm::vec3(worldToMeshMatrix * glm::vec4(origin, 1.0f)); @@ -498,7 +506,11 @@ bool Model::findParabolaIntersectionAgainstSubMeshes(const glm::vec3& origin, co } // extents is the entity relative, scaled, centered extents of the entity - glm::mat4 modelToWorldMatrix = createMatFromQuatAndPos(_rotation, _translation); + glm::mat4 transRot = createMatFromQuatAndPos(_rotation, _translation); + glm::mat4 modelToWorldMatrix = transRot; + if (!_snapModelToRegistrationPoint) { + modelToWorldMatrix = modelToWorldMatrix * glm::translate(getOriginalOffset()); + } glm::mat4 worldToModelMatrix = glm::inverse(modelToWorldMatrix); Extents modelExtents = getMeshExtents(); // NOTE: unrotated @@ -531,8 +543,12 @@ bool Model::findParabolaIntersectionAgainstSubMeshes(const glm::vec3& origin, co calculateTriangleSets(hfmModel); } - glm::mat4 meshToModelMatrix = glm::scale(_scale) * glm::translate(_offset); - glm::mat4 meshToWorldMatrix = modelToWorldMatrix * meshToModelMatrix; + glm::mat4 meshToWorldMatrix = transRot; + if (_snapModelToRegistrationPoint || _forceOffset) { + meshToWorldMatrix = meshToWorldMatrix * (glm::scale(_scale) * glm::translate(_offset)); + } else { + meshToWorldMatrix = meshToWorldMatrix * (glm::scale(_scale) * glm::translate(getNaturalDimensions() * (0.5f - _registrationPoint))); + } glm::mat4 worldToMeshMatrix = glm::inverse(meshToWorldMatrix); glm::vec3 meshFrameOrigin = glm::vec3(worldToMeshMatrix * glm::vec4(origin, 1.0f)); @@ -1182,17 +1198,8 @@ glm::vec3 Model::getNaturalDimensions() const { } Extents Model::getMeshExtents() const { - if (!isLoaded()) { - return Extents(); - } - const Extents& extents = getHFMModel().meshExtents; - - // even though our caller asked for "unscaled" we need to include any fst scaling, translation, and rotation, which - // is captured in the offset matrix - glm::vec3 minimum = glm::vec3(getHFMModel().offset * glm::vec4(extents.minimum, 1.0f)); - glm::vec3 maximum = glm::vec3(getHFMModel().offset * glm::vec4(extents.maximum, 1.0f)); - Extents scaledExtents = { minimum * _scale, maximum * _scale }; - return scaledExtents; + Extents extents = getUnscaledMeshExtents(); + return { extents.minimum * _scale, extents.maximum * _scale }; } Extents Model::getUnscaledMeshExtents() const { @@ -1417,6 +1424,15 @@ void Model::snapToRegistrationPoint() { _snappedToRegistrationPoint = true; } +glm::vec3 Model::getOriginalOffset() const { + Extents modelMeshExtents = getUnscaledMeshExtents(); + glm::vec3 dimensions = (modelMeshExtents.maximum - modelMeshExtents.minimum); + glm::vec3 offset = modelMeshExtents.minimum + (0.5f * dimensions); + glm::mat4 transform = glm::scale(_scale) * glm::translate(offset); + return transform[3]; +} + + void Model::setUseDualQuaternionSkinning(bool value) { _useDualQuaternionSkinning = value; } @@ -1437,7 +1453,8 @@ void Model::simulate(float deltaTime, bool fullUpdate) { snapToRegistrationPoint(); } // update the world space transforms for all joints - glm::mat4 parentTransform = glm::scale(_scale) * glm::translate(_offset); + glm::mat4 parentTransform = glm::scale(_scale) * ((_snapModelToRegistrationPoint || _forceOffset) ? + glm::translate(_offset) : glm::translate(getNaturalDimensions() * (0.5f - _registrationPoint))); updateRig(deltaTime, parentTransform); } } diff --git a/libraries/render-utils/src/Model.h b/libraries/render-utils/src/Model.h index 1e7ab55d5a..9bf828f9fb 100644 --- a/libraries/render-utils/src/Model.h +++ b/libraries/render-utils/src/Model.h @@ -167,6 +167,7 @@ public: void setSnapModelToRegistrationPoint(bool snapModelToRegistrationPoint, const glm::vec3& registrationPoint); bool getSnapModelToRegistrationPoint() { return _snapModelToRegistrationPoint; } + bool getSnappedToRegistrationPoint() { return _snappedToRegistrationPoint; } virtual void simulate(float deltaTime, bool fullUpdate = true); virtual void updateClusterMatrices(); @@ -203,6 +204,7 @@ public: void setOffset(const glm::vec3& offset); const glm::vec3& getOffset() const { return _offset; } + glm::vec3 getOriginalOffset() const; void setScaleToFit(bool scaleToFit, float largestDimension = 0.0f, bool forceRescale = false); void setScaleToFit(bool scaleToFit, const glm::vec3& dimensions, bool forceRescale = false); @@ -348,6 +350,7 @@ public: virtual bool replaceScriptableModelMeshPart(scriptable::ScriptableModelBasePointer model, int meshIndex, int partIndex) override; void scaleToFit(); + void snapToRegistrationPoint(); bool getUseDualQuaternionSkinning() const { return _useDualQuaternionSkinning; } void setUseDualQuaternionSkinning(bool value); @@ -409,14 +412,14 @@ protected: bool _snapModelToRegistrationPoint; /// is the model's offset automatically adjusted to a registration point in model space bool _snappedToRegistrationPoint; /// are we currently snapped to a registration point - glm::vec3 _registrationPoint = glm::vec3(0.5f); /// the point in model space our center is snapped to + glm::vec3 _registrationPoint { glm::vec3(0.5f) }; /// the point in model space our center is snapped to + bool _forceOffset { false }; std::vector _meshStates; virtual void initJointStates(); void setScaleInternal(const glm::vec3& scale); - void snapToRegistrationPoint(); virtual void updateRig(float deltaTime, glm::mat4 parentTransform); diff --git a/libraries/render-utils/src/text/Font.cpp b/libraries/render-utils/src/text/Font.cpp index a30bbad0e5..425e4f2da5 100644 --- a/libraries/render-utils/src/text/Font.cpp +++ b/libraries/render-utils/src/text/Font.cpp @@ -276,7 +276,7 @@ void Font::setupGPU() { for (auto& key : keys) { auto state = std::make_shared(); state->setCullMode(gpu::State::CULL_BACK); - state->setDepthTest(true, true, gpu::LESS_EQUAL); + state->setDepthTest(true, !std::get<0>(key), gpu::LESS_EQUAL); state->setBlendFunction(std::get<0>(key), gpu::State::SRC_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::INV_SRC_ALPHA, gpu::State::FACTOR_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::ONE); diff --git a/libraries/shared/src/LogHandler.cpp b/libraries/shared/src/LogHandler.cpp index 72d53f0bcb..098e5e9a80 100644 --- a/libraries/shared/src/LogHandler.cpp +++ b/libraries/shared/src/LogHandler.cpp @@ -64,6 +64,8 @@ LogHandler::LogHandler() { _shouldOutputThreadID = true; } else if (option == "milliseconds") { _shouldDisplayMilliseconds = true; + } else if (option == "keep_repeats") { + _keepRepeats = true; } else if (option != "") { fprintf(stdout, "Unrecognized option in VIRCADIA_LOG_OPTIONS: '%s'\n", option.toUtf8().constData()); } @@ -202,7 +204,18 @@ QString LogHandler::printMessage(LogMsgType type, const QMessageLogContext& cont resetColor = colorReset(); } - fprintf(stdout, "%s%s%s", color, qPrintable(logMessage), resetColor); + if (_keepRepeats || _previousMessage != message) { + if (_repeatCount > 0) { + fprintf(stdout, "[Previous message was repeated %i times]\n", _repeatCount); + } + + fprintf(stdout, "%s%s%s", color, qPrintable(logMessage), resetColor); + _repeatCount = 0; + } else { + _repeatCount++; + } + + _previousMessage = message; #ifdef Q_OS_WIN // On windows, this will output log lines into the Visual Studio "output" tab OutputDebugStringA(qPrintable(logMessage)); diff --git a/libraries/shared/src/LogHandler.h b/libraries/shared/src/LogHandler.h index 7bd5c69c63..71df2a4189 100644 --- a/libraries/shared/src/LogHandler.h +++ b/libraries/shared/src/LogHandler.h @@ -67,6 +67,11 @@ private: bool _shouldOutputThreadID { false }; bool _shouldDisplayMilliseconds { false }; bool _useColor { false }; + bool _keepRepeats { false }; + + QString _previousMessage; + int _repeatCount { 0 }; + int _currentMessageID { 0 }; struct RepeatedMessageRecord { diff --git a/libraries/ui/src/DockWidget.cpp b/libraries/ui/src/DockWidget.cpp index 13c4d9a548..f899def2c6 100644 --- a/libraries/ui/src/DockWidget.cpp +++ b/libraries/ui/src/DockWidget.cpp @@ -26,14 +26,15 @@ static void quickViewDeleter(QQuickView* quickView) { } DockWidget::DockWidget(const QString& title, QWidget* parent) : QDockWidget(title, parent) { - auto offscreenUi = DependencyManager::get(); - auto qmlEngine = offscreenUi->getSurfaceContext()->engine(); - _quickView = std::shared_ptr(new QQuickView(qmlEngine, nullptr), quickViewDeleter); - _quickView->setFormat(getDefaultOpenGLSurfaceFormat()); - QWidget* widget = QWidget::createWindowContainer(_quickView.get()); - setWidget(widget); - QWidget* headerWidget = new QWidget(); - setTitleBarWidget(headerWidget); + if (auto offscreenUI = DependencyManager::get()) { + auto qmlEngine = offscreenUI->getSurfaceContext()->engine(); + _quickView = std::shared_ptr(new QQuickView(qmlEngine, nullptr), quickViewDeleter); + _quickView->setFormat(getDefaultOpenGLSurfaceFormat()); + QWidget* widget = QWidget::createWindowContainer(_quickView.get()); + setWidget(widget); + QWidget* headerWidget = new QWidget(); + setTitleBarWidget(headerWidget); + } } void DockWidget::setSource(const QUrl& url) { diff --git a/libraries/ui/src/InfoView.cpp b/libraries/ui/src/InfoView.cpp index 478401c6f8..c14ff6bf64 100644 --- a/libraries/ui/src/InfoView.cpp +++ b/libraries/ui/src/InfoView.cpp @@ -65,17 +65,18 @@ void InfoView::show(const QString& path, bool firstOrChangedOnly, QString urlQue } infoVersion.set(version); } - auto offscreenUi = DependencyManager::get(); - QString infoViewName(NAME + "_" + path); - offscreenUi->show(QML, NAME + "_" + path, [=](QQmlContext* context, QObject* newObject){ - QQuickItem* item = dynamic_cast(newObject); - item->setWidth(1024); - item->setHeight(720); - InfoView* newInfoView = newObject->findChild(); - Q_ASSERT(newInfoView); - newInfoView->parent()->setObjectName(infoViewName); - newInfoView->setUrl(url); - }); + if (auto offscreenUI = DependencyManager::get()) { + QString infoViewName(NAME + "_" + path); + offscreenUI->show(QML, NAME + "_" + path, [=] (QQmlContext* context, QObject* newObject) { + QQuickItem* item = dynamic_cast(newObject); + item->setWidth(1024); + item->setHeight(720); + InfoView* newInfoView = newObject->findChild(); + Q_ASSERT(newInfoView); + newInfoView->parent()->setObjectName(infoViewName); + newInfoView->setUrl(url); + }); + } } QUrl InfoView::url() { diff --git a/libraries/ui/src/OffscreenQmlElement.h b/libraries/ui/src/OffscreenQmlElement.h index 69009533c6..ac1bcb0866 100644 --- a/libraries/ui/src/OffscreenQmlElement.h +++ b/libraries/ui/src/OffscreenQmlElement.h @@ -53,25 +53,30 @@ private: } \ \ void x::show(std::function f) { \ - auto offscreenUi = DependencyManager::get(); \ + auto offscreenUI = DependencyManager::get(); \ if (!registered) { \ x::registerType(); \ } \ - offscreenUi->show(QML, NAME, f); \ + if (offscreenUI) { \ + offscreenUI->show(QML, NAME, f); \ + } \ } \ \ void x::hide() { \ - auto offscreenUi = DependencyManager::get(); \ - offscreenUi->hide(NAME); \ + if (auto offscreenUI = DependencyManager::get()) { \ + offscreenUI->hide(NAME); \ + } \ } \ \ void x::toggle(std::function f) { \ - auto offscreenUi = DependencyManager::get(); \ - offscreenUi->toggle(QML, NAME, f); \ + if (auto offscreenUI = DependencyManager::get()) { \ + offscreenUI->toggle(QML, NAME, f); \ + } \ } \ void x::load(std::function f) { \ - auto offscreenUi = DependencyManager::get(); \ - offscreenUi->load(QML, f); \ + if (auto offscreenUI = DependencyManager::get()) { \ + offscreenUI->load(QML, f); \ + } \ } #define HIFI_QML_DEF_LAMBDA(x, f) \ @@ -82,21 +87,25 @@ private: qmlRegisterType("Hifi", 1, 0, NAME.toLocal8Bit().constData()); \ } \ void x::show() { \ - auto offscreenUi = DependencyManager::get(); \ - offscreenUi->show(QML, NAME, f); \ + if (auto offscreenUI = DependencyManager::get()) { \ + offscreenUI->show(QML, NAME, f); \ + } \ } \ void x::hide() { \ - auto offscreenUi = DependencyManager::get(); \ - offscreenUi->hide(NAME); \ + if (auto offscreenUI = DependencyManager::get()) { \ + offscreenUI->hide(NAME); \ + } \ } \ \ void x::toggle() { \ - auto offscreenUi = DependencyManager::get(); \ - offscreenUi->toggle(QML, NAME, f); \ + if (auto offscreenUI = DependencyManager::get()) { \ + offscreenUI->toggle(QML, NAME, f); \ + } \ } \ void x::load() { \ - auto offscreenUi = DependencyManager::get(); \ - offscreenUi->load(QML, f); \ + if (auto offscreenUI = DependencyManager::get()) { \ + offscreenUI->load(QML, f); \ + } \ } #endif diff --git a/libraries/ui/src/OffscreenUi.cpp b/libraries/ui/src/OffscreenUi.cpp index 34184057e0..26e77dcb5f 100644 --- a/libraries/ui/src/OffscreenUi.cpp +++ b/libraries/ui/src/OffscreenUi.cpp @@ -189,10 +189,12 @@ void OffscreenUi::show(const QUrl& url, const QString& name, std::function f) { @@ -208,11 +210,14 @@ void OffscreenUi::toggle(const QUrl& url, const QString& name, std::functionfindChild(name); - if (item) { - return QQmlProperty(item, OFFSCREEN_VISIBILITY_PROPERTY).read().toBool(); - } else { - return false; + auto rootItem = getRootItem(); + if (rootItem) { + QQuickItem* item = rootItem->findChild(name); + if (item) { + return QQmlProperty(item, OFFSCREEN_VISIBILITY_PROPERTY).read().toBool(); + } } + return false; } class MessageBoxListener : public ModalDialogListener { @@ -280,12 +287,11 @@ QQuickItem* OffscreenUi::createMessageBox(Icon icon, const QString& title, const bool invokeResult; auto tabletScriptingInterface = DependencyManager::get(); TabletProxy* tablet = dynamic_cast(tabletScriptingInterface->getTablet("com.highfidelity.interface.tablet.system")); - if (tablet->getToolbarMode()) { + if (tablet->getToolbarMode() && _desktop) { invokeResult = QMetaObject::invokeMethod(_desktop, "messageBox", Q_RETURN_ARG(QVariant, result), Q_ARG(QVariant, QVariant::fromValue(map))); - } else { - QQuickItem* tabletRoot = tablet->getTabletRoot(); + } else if (QQuickItem* tabletRoot = tablet->getTabletRoot()) { invokeResult = QMetaObject::invokeMethod(tabletRoot, "messageBox", Q_RETURN_ARG(QVariant, result), Q_ARG(QVariant, QVariant::fromValue(map))); @@ -533,21 +539,21 @@ ModalDialogListener* OffscreenUi::customInputDialogAsync(const Icon icon, const } void OffscreenUi::togglePinned() { - bool invokeResult = QMetaObject::invokeMethod(_desktop, "togglePinned"); + bool invokeResult = _desktop && QMetaObject::invokeMethod(_desktop, "togglePinned"); if (!invokeResult) { qWarning() << "Failed to toggle window visibility"; } } void OffscreenUi::setPinned(bool pinned) { - bool invokeResult = QMetaObject::invokeMethod(_desktop, "setPinned", Q_ARG(QVariant, pinned)); + bool invokeResult = _desktop && QMetaObject::invokeMethod(_desktop, "setPinned", Q_ARG(QVariant, pinned)); if (!invokeResult) { qWarning() << "Failed to set window visibility"; } } void OffscreenUi::setConstrainToolbarToCenterX(bool constrained) { - bool invokeResult = QMetaObject::invokeMethod(_desktop, "setConstrainToolbarToCenterX", Q_ARG(QVariant, constrained)); + bool invokeResult = _desktop && QMetaObject::invokeMethod(_desktop, "setConstrainToolbarToCenterX", Q_ARG(QVariant, constrained)); if (!invokeResult) { qWarning() << "Failed to set toolbar constraint"; } @@ -575,17 +581,17 @@ QQuickItem* OffscreenUi::createInputDialog(const Icon icon, const QString& title TabletProxy* tablet = dynamic_cast(tabletScriptingInterface->getTablet("com.highfidelity.interface.tablet.system")); bool invokeResult; - if (tablet->getToolbarMode()) { + if (tablet->getToolbarMode() && _desktop) { invokeResult = QMetaObject::invokeMethod(_desktop, "inputDialog", Q_RETURN_ARG(QVariant, result), Q_ARG(QVariant, QVariant::fromValue(map))); - } else { - QQuickItem* tabletRoot = tablet->getTabletRoot(); + } else if (QQuickItem* tabletRoot = tablet->getTabletRoot()) { invokeResult = QMetaObject::invokeMethod(tabletRoot, "inputDialog", Q_RETURN_ARG(QVariant, result), Q_ARG(QVariant, QVariant::fromValue(map))); emit tabletScriptingInterface->tabletNotification(); } + if (!invokeResult) { qWarning() << "Failed to create message box"; return nullptr; @@ -603,12 +609,11 @@ QQuickItem* OffscreenUi::createCustomInputDialog(const Icon icon, const QString& TabletProxy* tablet = dynamic_cast(tabletScriptingInterface->getTablet("com.highfidelity.interface.tablet.system")); bool invokeResult; - if (tablet->getToolbarMode()) { + if (tablet->getToolbarMode() && _desktop) { invokeResult = QMetaObject::invokeMethod(_desktop, "inputDialog", Q_RETURN_ARG(QVariant, result), Q_ARG(QVariant, QVariant::fromValue(map))); - } else { - QQuickItem* tabletRoot = tablet->getTabletRoot(); + } else if (QQuickItem* tabletRoot = tablet->getTabletRoot()) { invokeResult = QMetaObject::invokeMethod(tabletRoot, "inputDialog", Q_RETURN_ARG(QVariant, result), Q_ARG(QVariant, QVariant::fromValue(map))); @@ -718,7 +723,7 @@ QObject* OffscreenUi::getRootMenu() { } void OffscreenUi::unfocusWindows() { - bool invokeResult = QMetaObject::invokeMethod(_desktop, "unfocusWindows"); + bool invokeResult = _desktop && QMetaObject::invokeMethod(_desktop, "unfocusWindows"); Q_ASSERT(invokeResult); } @@ -752,12 +757,11 @@ QString OffscreenUi::fileDialog(const QVariantMap& properties) { bool invokeResult; auto tabletScriptingInterface = DependencyManager::get(); TabletProxy* tablet = dynamic_cast(tabletScriptingInterface->getTablet("com.highfidelity.interface.tablet.system")); - if (tablet->getToolbarMode()) { + if (tablet->getToolbarMode() && _desktop) { invokeResult = QMetaObject::invokeMethod(_desktop, "fileDialog", Q_RETURN_ARG(QVariant, buildDialogResult), Q_ARG(QVariant, QVariant::fromValue(properties))); - } else { - QQuickItem* tabletRoot = tablet->getTabletRoot(); + } else if (QQuickItem* tabletRoot = tablet->getTabletRoot()) { invokeResult = QMetaObject::invokeMethod(tabletRoot, "fileDialog", Q_RETURN_ARG(QVariant, buildDialogResult), Q_ARG(QVariant, QVariant::fromValue(properties))); @@ -782,12 +786,11 @@ ModalDialogListener* OffscreenUi::fileDialogAsync(const QVariantMap& properties) bool invokeResult; auto tabletScriptingInterface = DependencyManager::get(); TabletProxy* tablet = dynamic_cast(tabletScriptingInterface->getTablet("com.highfidelity.interface.tablet.system")); - if (tablet->getToolbarMode()) { + if (tablet->getToolbarMode() && _desktop) { invokeResult = QMetaObject::invokeMethod(_desktop, "fileDialog", Q_RETURN_ARG(QVariant, buildDialogResult), Q_ARG(QVariant, QVariant::fromValue(properties))); - } else { - QQuickItem* tabletRoot = tablet->getTabletRoot(); + } else if (QQuickItem* tabletRoot = tablet->getTabletRoot()) { invokeResult = QMetaObject::invokeMethod(tabletRoot, "fileDialog", Q_RETURN_ARG(QVariant, buildDialogResult), Q_ARG(QVariant, QVariant::fromValue(properties))); @@ -1003,12 +1006,11 @@ QString OffscreenUi::assetDialog(const QVariantMap& properties) { bool invokeResult; auto tabletScriptingInterface = DependencyManager::get(); TabletProxy* tablet = dynamic_cast(tabletScriptingInterface->getTablet("com.highfidelity.interface.tablet.system")); - if (tablet->getToolbarMode()) { + if (tablet->getToolbarMode() && _desktop) { invokeResult = QMetaObject::invokeMethod(_desktop, "assetDialog", Q_RETURN_ARG(QVariant, buildDialogResult), Q_ARG(QVariant, QVariant::fromValue(properties))); - } else { - QQuickItem* tabletRoot = tablet->getTabletRoot(); + } else if (QQuickItem* tabletRoot = tablet->getTabletRoot()) { invokeResult = QMetaObject::invokeMethod(tabletRoot, "assetDialog", Q_RETURN_ARG(QVariant, buildDialogResult), Q_ARG(QVariant, QVariant::fromValue(properties))); @@ -1034,12 +1036,11 @@ ModalDialogListener *OffscreenUi::assetDialogAsync(const QVariantMap& properties bool invokeResult; auto tabletScriptingInterface = DependencyManager::get(); TabletProxy* tablet = dynamic_cast(tabletScriptingInterface->getTablet("com.highfidelity.interface.tablet.system")); - if (tablet->getToolbarMode()) { + if (tablet->getToolbarMode() && _desktop) { invokeResult = QMetaObject::invokeMethod(_desktop, "assetDialog", Q_RETURN_ARG(QVariant, buildDialogResult), Q_ARG(QVariant, QVariant::fromValue(properties))); - } else { - QQuickItem* tabletRoot = tablet->getTabletRoot(); + } else if (QQuickItem* tabletRoot = tablet->getTabletRoot()) { invokeResult = QMetaObject::invokeMethod(tabletRoot, "assetDialog", Q_RETURN_ARG(QVariant, buildDialogResult), Q_ARG(QVariant, QVariant::fromValue(properties))); diff --git a/libraries/ui/src/QmlFragmentClass.cpp b/libraries/ui/src/QmlFragmentClass.cpp index fbd045fdb1..1219094afc 100644 --- a/libraries/ui/src/QmlFragmentClass.cpp +++ b/libraries/ui/src/QmlFragmentClass.cpp @@ -14,9 +14,6 @@ #include -#include "OffscreenUi.h" - - std::mutex QmlFragmentClass::_mutex; std::map QmlFragmentClass::_fragments; @@ -40,7 +37,6 @@ QScriptValue QmlFragmentClass::internal_constructor(QScriptContext* context, QSc } auto properties = parseArguments(context); - auto offscreenUi = DependencyManager::get(); QmlFragmentClass* retVal = new QmlFragmentClass(restricted, qml.toString()); Q_ASSERT(retVal); if (QThread::currentThread() != qApp->thread()) { diff --git a/libraries/ui/src/QmlWebWindowClass.cpp b/libraries/ui/src/QmlWebWindowClass.cpp index 282161497a..c7851d416f 100644 --- a/libraries/ui/src/QmlWebWindowClass.cpp +++ b/libraries/ui/src/QmlWebWindowClass.cpp @@ -14,7 +14,6 @@ #include #include -#include "OffscreenUi.h" static const char* const URL_PROPERTY = "source"; static const char* const SCRIPT_PROPERTY = "scriptUrl"; @@ -22,7 +21,6 @@ static const char* const SCRIPT_PROPERTY = "scriptUrl"; // Method called by Qt scripts to create a new web window in the overlay QScriptValue QmlWebWindowClass::internal_constructor(QScriptContext* context, QScriptEngine* engine, bool restricted) { auto properties = parseArguments(context); - auto offscreenUi = DependencyManager::get(); QmlWebWindowClass* retVal = new QmlWebWindowClass(restricted); Q_ASSERT(retVal); if (QThread::currentThread() != qApp->thread()) { diff --git a/libraries/ui/src/QmlWindowClass.cpp b/libraries/ui/src/QmlWindowClass.cpp index 13a289a5fd..ae2292dc09 100644 --- a/libraries/ui/src/QmlWindowClass.cpp +++ b/libraries/ui/src/QmlWindowClass.cpp @@ -72,7 +72,6 @@ QVariantMap QmlWindowClass::parseArguments(QScriptContext* context) { // Method called by Qt scripts to create a new web window in the overlay QScriptValue QmlWindowClass::internal_constructor(QScriptContext* context, QScriptEngine* engine, bool restricted) { auto properties = parseArguments(context); - auto offscreenUi = DependencyManager::get(); QmlWindowClass* retVal = new QmlWindowClass(restricted); Q_ASSERT(retVal); if (QThread::currentThread() != qApp->thread()) { @@ -349,7 +348,6 @@ void QmlWindowClass::raise() { return; } - auto offscreenUi = DependencyManager::get(); if (_qmlWindow) { QMetaObject::invokeMethod(asQuickItem(), "raise", Qt::DirectConnection); } diff --git a/libraries/ui/src/ui/TabletScriptingInterface.cpp b/libraries/ui/src/ui/TabletScriptingInterface.cpp index 040bb750d0..61c74dc17c 100644 --- a/libraries/ui/src/ui/TabletScriptingInterface.cpp +++ b/libraries/ui/src/ui/TabletScriptingInterface.cpp @@ -321,8 +321,8 @@ void TabletScriptingInterface::processEvent(const QKeyEvent* event) { QObject* TabletScriptingInterface::getFlags() { Q_ASSERT(QThread::currentThread() == qApp->thread()); - auto offscreenUi = DependencyManager::get(); - return offscreenUi->getFlags(); + auto offscreenUI = DependencyManager::get(); + return offscreenUI ? offscreenUI->getFlags() : nullptr; } // @@ -364,8 +364,6 @@ void TabletProxy::setToolbarMode(bool toolbarMode) { _toolbarMode = toolbarMode; - auto offscreenUi = DependencyManager::get(); - if (toolbarMode) { #if !defined(DISABLE_QML) closeDialog(); @@ -388,13 +386,18 @@ void TabletProxy::setToolbarMode(bool toolbarMode) { if (_currentPathLoaded != TABLET_HOME_SOURCE_URL) { loadHomeScreen(true); } - //check if running scripts window opened and save it for reopen in Tablet - if (offscreenUi->isVisible("RunningScripts")) { - offscreenUi->hide("RunningScripts"); - _showRunningScripts = true; + + auto offscreenUI = DependencyManager::get(); + if (offscreenUI) { + //check if running scripts window opened and save it for reopen in Tablet + if (offscreenUI->isVisible("RunningScripts")) { + offscreenUI->hide("RunningScripts"); + _showRunningScripts = true; + } + + offscreenUI->hideDesktopWindows(); } - offscreenUi->hideDesktopWindows(); // destroy desktop window if (_desktopWindow) { _desktopWindow->deleteLater(); @@ -577,9 +580,9 @@ void TabletProxy::gotoMenuScreen(const QString& submenu) { root = _desktopWindow->asQuickItem(); } - if (root) { - auto offscreenUi = DependencyManager::get(); - QObject* menu = offscreenUi->getRootMenu(); + auto offscreenUI = DependencyManager::get(); + if (root && offscreenUI) { + QObject* menu = offscreenUI->getRootMenu(); QMetaObject::invokeMethod(root, "setMenuProperties", Q_ARG(QVariant, QVariant::fromValue(menu)), Q_ARG(const QVariant&, QVariant(submenu))); QMetaObject::invokeMethod(root, "loadSource", Q_ARG(const QVariant&, QVariant(VRMENU_SOURCE_URL))); _state = State::Menu; diff --git a/plugins/openvr/src/OpenVrHelpers.cpp b/plugins/openvr/src/OpenVrHelpers.cpp index ce7625eedb..eed2242602 100644 --- a/plugins/openvr/src/OpenVrHelpers.cpp +++ b/plugins/openvr/src/OpenVrHelpers.cpp @@ -203,14 +203,14 @@ void updateFromOpenVrKeyboardInput() { } void finishOpenVrKeyboardInput() { - auto offscreenUi = DependencyManager::get(); + auto offscreenUI = DependencyManager::get(); updateFromOpenVrKeyboardInput(); // Simulate an enter press on the top level window to trigger the action - if (0 == (_currentHints & Qt::ImhMultiLine)) { + if (0 == (_currentHints & Qt::ImhMultiLine) && offscreenUI) { auto keyPress = QKeyEvent(QEvent::KeyPress, Qt::Key_Return, Qt::KeyboardModifiers(), QString("\n")); auto keyRelease = QKeyEvent(QEvent::KeyRelease, Qt::Key_Return, Qt::KeyboardModifiers()); - qApp->sendEvent(offscreenUi->getWindow(), &keyPress); - qApp->sendEvent(offscreenUi->getWindow(), &keyRelease); + qApp->sendEvent(offscreenUI->getWindow(), &keyPress); + qApp->sendEvent(offscreenUI->getWindow(), &keyRelease); } } @@ -221,10 +221,8 @@ void enableOpenVrKeyboard(PluginContainer* container) { if (disableSteamVrKeyboard) { return; } - auto offscreenUi = DependencyManager::get(); _overlay = vr::VROverlay(); - auto menu = container->getPrimaryMenu(); auto action = menu->getActionForOption(MenuOption::Overlays); @@ -282,7 +280,9 @@ void handleOpenVrEvents() { case vr::VREvent_KeyboardClosed: _keyboardFocusObject = nullptr; _keyboardShown = false; - DependencyManager::get()->unfocusWindows(); + if (auto offscreenUI = DependencyManager::get()) { + offscreenUI->unfocusWindows(); + } break; default: diff --git a/scripts/defaultScripts.js b/scripts/defaultScripts.js index 71fb644528..b27008e255 100644 --- a/scripts/defaultScripts.js +++ b/scripts/defaultScripts.js @@ -36,7 +36,8 @@ var DEFAULT_SCRIPTS_COMBINED = [ "system/inspect.js", "system/keyboardShortcuts/keyboardShortcuts.js", "system/checkForUpdates.js", - "system/onFirstRun.js" + "system/onFirstRun.js", + "system/appreciate/appreciate_app.js" ]; var DEFAULT_SCRIPTS_SEPARATE = [ "system/controllers/controllerScripts.js", diff --git a/scripts/system/appreciate/README.md b/scripts/system/appreciate/README.md new file mode 100644 index 0000000000..b3f9a3649b --- /dev/null +++ b/scripts/system/appreciate/README.md @@ -0,0 +1,40 @@ +# Appreciate + +## Description + +Show someone else that you like what they're doing. Open the app to see usage instructions and some options! + +## Releases + +### v1.5 | [48d8247](https://github.com/highfidelity/hifi-content/commit/48d8247) + +- Fixed an issue where Appreciate app users wearing avatars without a specific joint wouldn't hear the Appreciate sound or see the Appreciation Dodecahedron + +### 2019-03-08_11-37-00 | Marketplace v1.4 | [93bf464](https://github.com/highfidelity/hifi-content/commit/93bf464) + +- Fixed an issue where a user could press the "Z" key to appreciate while the Appreciate UI was focused even if the Appreciate switch was turned off +- Fixed an issue where Appreciation Intensity decayed too quickly after switching from HMD mode to Desktop mode + +### 2019-02-22_10-49-00 | Marketplace v1.3 | [51704b5](https://github.com/highfidelity/hifi-content/commit/51704b5) + +- Optimize app +- Add option to not show Appreciation Dodecahedron while Appreciating +- Forward Z keypresses to the Appreciate script that the user makes when the App's HTML UI is focused + +### 2019-02-19_13-09-00 | Marketplace v1.2 | [0e2fa82](https://github.com/highfidelity/hifi-content/commit/0e2fa82) + +- Introduced functionality to stop running versions of Appreciate when those versions are baked into the client installation AND other versions of Appreciate are running + +### 2019-02-15_17-03-00 | Marketplace v1.1 | [83f8927](https://github.com/highfidelity/hifi-content/commit/83f8927) + +- Ensure that old Appreciation Dodecahedrons will be deleted in the event of a client crashing while Appreciating + +### 2019-02-14_10-00-00 | Marketplace v1.0 | [658ed4e](https://github.com/highfidelity/hifi-content/commit/658ed4e) + +- Initial Release + +## Project Links +[Trello Card](https://trello.com/c/2iMbEgdw/36-appreciation-app) + +## Known issues +- N/A diff --git a/scripts/system/appreciate/appreciate.jpg b/scripts/system/appreciate/appreciate.jpg new file mode 100644 index 0000000000..615991743b Binary files /dev/null and b/scripts/system/appreciate/appreciate.jpg differ diff --git a/scripts/system/appreciate/appreciate_app.js b/scripts/system/appreciate/appreciate_app.js new file mode 100644 index 0000000000..dce2a93502 --- /dev/null +++ b/scripts/system/appreciate/appreciate_app.js @@ -0,0 +1,1138 @@ +/* + Appreciate + Created by Zach Fox on 2019-01-30 + 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 () { + // ************************************* + // START UTILITY FUNCTIONS + // ************************************* + // #region Utilities + var MS_PER_S = 1000; + var CM_PER_M = 100; + var HALF = 0.5; + + + // Returns the first valid joint position from the list of supplied test joint positions. + // If none are valid, returns MyAvatar.position. + function getValidJointPosition(jointsToTest) { + var currentJointIndex; + + for (var i = 0; i < jointsToTest.length; i++) { + currentJointIndex = MyAvatar.getJointIndex(jointsToTest[i]); + + if (currentJointIndex > -1) { + return MyAvatar.getJointPosition(jointsToTest[i]); + } + } + + return MyAvatar.position; + } + + + // Returns the world position halfway between the user's hands + function getAppreciationPosition() { + var validLeftJoints = ["LeftHandMiddle2", "LeftHand", "LeftArm"]; + var leftPosition = getValidJointPosition(validLeftJoints); + + var validRightJoints = ["RightHandMiddle2", "RightHand", "RightArm"];; + var rightPosition = getValidJointPosition(validRightJoints); + + var centerPosition = Vec3.sum(leftPosition, rightPosition); + centerPosition = Vec3.multiply(centerPosition, HALF); + + return centerPosition; + } + + + // Returns a linearly scaled value based on `factor` and the other inputs + function linearScale(factor, minInput, maxInput, minOutput, maxOutput) { + return minOutput + (maxOutput - minOutput) * + (factor - minInput) / (maxInput - minInput); + } + + + // Linearly scales an RGB color between 0 and 1 based on RGB color values + // between 0 and 255. + function linearScaleColor(intensity, min, max) { + var output = { + "red": 0, + "green": 0, + "blue": 0 + }; + + output.red = linearScale(intensity, 0, 1, min.red, max.red); + output.green = linearScale(intensity, 0, 1, min.green, max.green); + output.blue = linearScale(intensity, 0, 1, min.blue, max.blue); + + return output; + } + + + function randomFloat(min, max) { + return Math.random() * (max - min) + min; + } + + + // Updates the Current Intensity Meter UI element. Called when intensity changes. + function updateCurrentIntensityUI() { + ui.sendMessage({method: "updateCurrentIntensityUI", currentIntensity: currentIntensity}); + } + // #endregion + // ************************************* + // END UTILITY FUNCTIONS + // ************************************* + + // If the interval that updates the intensity interval exists, + // clear it. + var updateIntensityEntityInterval = false; + var UPDATE_INTENSITY_ENTITY_INTERVAL_MS = 75; + function maybeClearUpdateIntensityEntityInterval() { + if (updateIntensityEntityInterval) { + Script.clearInterval(updateIntensityEntityInterval); + updateIntensityEntityInterval = false; + } + + if (intensityEntity) { + Entities.deleteEntity(intensityEntity); + intensityEntity = false; + } + } + + + // Determines if any XYZ JSON object has changed "enough" based on + // last xyz values and current xyz values. + // Used for determining if angular velocity and dimensions have changed enough. + var lastAngularVelocity = { + "x": 0, + "y": 0, + "z": 0 + }; + var ANGVEL_DISTANCE_THRESHOLD_PERCENT_CHANGE = 0.35; + var lastDimensions= { + "x": 0, + "y": 0, + "z": 0 + }; + var DIMENSIONS_DISTANCE_THRESHOLD_PERCENT_CHANGE = 0.2; + function xyzVecChangedEnough(current, last, thresh) { + var currentLength = Math.sqrt( + Math.pow(current.x, TWO) + Math.pow(current.y, TWO) + Math.pow(current.z, TWO)); + var lastLength = Math.sqrt( + Math.pow(last.x, TWO) + Math.pow(last.y, TWO) + Math.pow(last.z, TWO)); + + var change = Math.abs(currentLength - lastLength); + if (change/lastLength > thresh) { + return true; + } + + return false; + } + + + // Determines if color values have changed "enough" based on + // last color and current color + var lastColor = { + "red": 0, + "blue": 0, + "green": 0 + }; + var COLOR_DISTANCE_THRESHOLD_PERCENT_CHANGE = 0.35; + var TWO = 2; + function colorChangedEnough(current, last, thresh) { + var currentLength = Math.sqrt( + Math.pow(current.red, TWO) + Math.pow(current.green, TWO) + Math.pow(current.blue, TWO)); + var lastLength = Math.sqrt( + Math.pow(last.red, TWO) + Math.pow(last.green, TWO) + Math.pow(last.blue, TWO)); + + var change = Math.abs(currentLength - lastLength); + if (change/lastLength > thresh) { + return true; + } + + return false; + } + + + // Updates the intensity entity based on the user's avatar's hand position and the + // current intensity of their appreciation. + // Many of these property values are empirically determined. + var intensityEntity = false; + var INTENSITY_ENTITY_MAX_DIMENSIONS = { + "x": 0.24, + "y": 0.24, + "z": 0.24 + }; + var INTENSITY_ENTITY_MIN_ANGULAR_VELOCITY = { + "x": -0.21, + "y": -0.21, + "z": -0.21 + }; + var INTENSITY_ENTITY_MAX_ANGULAR_VELOCITY = { + "x": 0.21, + "y": 0.21, + "z": 0.21 + }; + var intensityEntityColorMin = { + "red": 82, + "green": 196, + "blue": 145 + }; + var INTENSITY_ENTITY_COLOR_MAX_DEFAULT = { + "red": 5, + "green": 255, + "blue": 5 + }; + var MIN_COLOR_MULTIPLIER = 0.4; + var intensityEntityColorMax = JSON.parse(Settings.getValue("appreciate/entityColor", + JSON.stringify(INTENSITY_ENTITY_COLOR_MAX_DEFAULT))); + var ANGVEL_ENTITY_MULTIPLY_FACTOR = 62; + var INTENSITY_ENTITY_NAME = "Appreciation Dodecahedron"; + var INTENSITY_ENTITY_PROPERTIES = { + "name": INTENSITY_ENTITY_NAME, + "type": "Shape", + "shape": "Dodecahedron", + "dimensions": { + "x": 0, + "y": 0, + "z": 0 + }, + "angularVelocity": { + "x": 0, + "y": 0, + "z": 0 + }, + "angularDamping": 0, + "grab": { + "grabbable": false, + "equippableLeftRotation": { + "x": -0.0000152587890625, + "y": -0.0000152587890625, + "z": -0.0000152587890625, + "w": 1 + }, + "equippableRightRotation": { + "x": -0.0000152587890625, + "y": -0.0000152587890625, + "z": -0.0000152587890625, + "w": 1 + } + }, + "collisionless": true, + "ignoreForCollisions": true, + "queryAACube": { + "x": -0.17320507764816284, + "y": -0.17320507764816284, + "z": -0.17320507764816284, + "scale": 0.3464101552963257 + }, + "damping": 0, + "color": intensityEntityColorMin, + "clientOnly": false, + "avatarEntity": true, + "localEntity": false, + "faceCamera": false, + "isFacingAvatar": false + }; + var currentInitialAngularVelocity = { + "x": 0, + "y": 0, + "z": 0 + }; + function updateIntensityEntity() { + if (!showAppreciationEntity) { + return; + } + + if (currentIntensity > 0) { + if (intensityEntity) { + intensityEntityColorMin.red = intensityEntityColorMax.red * MIN_COLOR_MULTIPLIER; + intensityEntityColorMin.green = intensityEntityColorMax.green * MIN_COLOR_MULTIPLIER; + intensityEntityColorMin.blue = intensityEntityColorMax.blue * MIN_COLOR_MULTIPLIER; + + var color = linearScaleColor(currentIntensity, intensityEntityColorMin, intensityEntityColorMax); + + var propsToUpdate = { + position: getAppreciationPosition() + }; + + var currentDimensions = Vec3.multiply(INTENSITY_ENTITY_MAX_DIMENSIONS, currentIntensity); + if (xyzVecChangedEnough(currentDimensions, lastDimensions, DIMENSIONS_DISTANCE_THRESHOLD_PERCENT_CHANGE)) { + propsToUpdate.dimensions = currentDimensions; + lastDimensions = currentDimensions; + } + + var currentAngularVelocity = Vec3.multiply(currentInitialAngularVelocity, + currentIntensity * ANGVEL_ENTITY_MULTIPLY_FACTOR); + if (xyzVecChangedEnough(currentAngularVelocity, lastAngularVelocity, ANGVEL_DISTANCE_THRESHOLD_PERCENT_CHANGE)) { + propsToUpdate.angularVelocity = currentAngularVelocity; + lastAngularVelocity = currentAngularVelocity; + } + + var currentColor = color; + if (colorChangedEnough(currentColor, lastColor, COLOR_DISTANCE_THRESHOLD_PERCENT_CHANGE)) { + propsToUpdate.color = currentColor; + lastColor = currentColor; + } + + Entities.editEntity(intensityEntity, propsToUpdate); + } else { + var props = INTENSITY_ENTITY_PROPERTIES; + props.position = getAppreciationPosition(); + + currentInitialAngularVelocity.x = + randomFloat(INTENSITY_ENTITY_MIN_ANGULAR_VELOCITY.x, INTENSITY_ENTITY_MAX_ANGULAR_VELOCITY.x); + currentInitialAngularVelocity.y = + randomFloat(INTENSITY_ENTITY_MIN_ANGULAR_VELOCITY.y, INTENSITY_ENTITY_MAX_ANGULAR_VELOCITY.y); + currentInitialAngularVelocity.z = + randomFloat(INTENSITY_ENTITY_MIN_ANGULAR_VELOCITY.z, INTENSITY_ENTITY_MAX_ANGULAR_VELOCITY.z); + props.angularVelocity = currentInitialAngularVelocity; + + intensityEntity = Entities.addEntity(props, "avatar"); + } + } else { + if (intensityEntity) { + Entities.deleteEntity(intensityEntity); + intensityEntity = false; + } + + maybeClearUpdateIntensityEntityInterval(); + } + } + + + // Function that AppUI calls when the App's UI opens + function onOpened() { + updateCurrentIntensityUI(); + } + + + // Locally pre-caches all of the sounds in the sounds/claps and sounds/whistles + // directories. + var NUM_CLAP_SOUNDS = 16; + var NUM_WHISTLE_SOUNDS = 17; + var clapSounds = []; + var whistleSounds = []; + function getSounds() { + for (var i = 1; i < NUM_CLAP_SOUNDS + 1; i++) { + clapSounds.push(SoundCache.getSound(Script.resolvePath( + "resources/sounds/claps/" + ("0" + i).slice(-2) + ".wav"))); + } + for (i = 1; i < NUM_WHISTLE_SOUNDS + 1; i++) { + whistleSounds.push(SoundCache.getSound(Script.resolvePath( + "resources/sounds/whistles/" + ("0" + i).slice(-2) + ".wav"))); + } + } + + + // Locally pre-caches the Cheering and Clapping animations + var whistlingAnimation = false; + var clappingAnimation = false; + function getAnimations() { + var animationURL = Script.resolvePath("resources/animations/Cheering.fbx"); + var resource = AnimationCache.prefetch(animationURL); + var animation = AnimationCache.getAnimation(animationURL); + whistlingAnimation = {url: animationURL, animation: animation, resource: resource}; + + animationURL = Script.resolvePath("resources/animations/Clapping.fbx"); + resource = AnimationCache.prefetch(animationURL); + animation = AnimationCache.getAnimation(animationURL); + clappingAnimation = {url: animationURL, animation: animation, resource: resource}; + } + + + // If we're currently fading out the appreciation sounds on an interval, + // clear that interval. + function maybeClearSoundFadeInterval() { + if (soundFadeInterval) { + Script.clearInterval(soundFadeInterval); + soundFadeInterval = false; + } + } + + + // Fade out the appreciation sounds by quickly + // lowering the global current intensity. + var soundFadeInterval = false; + var FADE_INTERVAL_MS = 20; + var FADE_OUT_STEP_SIZE = 0.05; // Unitless + function fadeOutAndStopSound() { + maybeClearSoundFadeInterval(); + + soundFadeInterval = Script.setInterval(function() { + currentIntensity -= FADE_OUT_STEP_SIZE; + + if (currentIntensity <= 0) { + if (soundInjector) { + soundInjector.stop(); + soundInjector = false; + } + + updateCurrentIntensityUI(); + + maybeClearSoundFadeInterval(); + } + + fadeIntensity(currentIntensity, INTENSITY_MAX_STEP_SIZE_DESKTOP); + }, FADE_INTERVAL_MS); + } + + + // Calculates the audio injector volume based on + // the current global appreciation intensity and some min/max values. + var MIN_VOLUME_CLAP = 0.05; + var MAX_VOLUME_CLAP = 1.0; + var MIN_VOLUME_WHISTLE = 0.07; + var MAX_VOLUME_WHISTLE = 0.16; + function calculateInjectorVolume() { + var minInputVolume = 0; + var maxInputVolume = MAX_CLAP_INTENSITY; + var minOutputVolume = MIN_VOLUME_CLAP; + var maxOutputVolume = MAX_VOLUME_CLAP; + + if (currentSound === "whistle") { + minInputVolume = MAX_CLAP_INTENSITY; + maxInputVolume = MAX_WHISTLE_INTENSITY; + minOutputVolume = MIN_VOLUME_WHISTLE; + maxOutputVolume = MAX_VOLUME_WHISTLE; + } + + var vol = linearScale(currentIntensity, minInputVolume, + maxInputVolume, minOutputVolume, maxOutputVolume); + return vol; + } + + + // Modifies the global currentIntensity. Moves towards the targetIntensity, + // but never moves faster than a given max step size per function call. + // Also clamps the intensity to a min of 0 and a max of 1.0. + var currentIntensity = 0; + var INTENSITY_MAX_STEP_SIZE = 0.003; // Unitless, determined empirically + var INTENSITY_MAX_STEP_SIZE_DESKTOP = 1; // Unitless, determined empirically + var MAX_CLAP_INTENSITY = 0.55; // Unitless, determined empirically + var MAX_WHISTLE_INTENSITY = 1.0; // Unitless, determined empirically + function fadeIntensity(targetIntensity, maxStepSize) { + if (!maxStepSize) { + maxStepSize = INTENSITY_MAX_STEP_SIZE; + } + + var volumeDelta = targetIntensity - currentIntensity; + volumeDelta = Math.min(Math.abs(volumeDelta), maxStepSize); + + if (targetIntensity < currentIntensity) { + volumeDelta *= -1; + } + + currentIntensity += volumeDelta; + + currentIntensity = Math.max(0.0, Math.min( + neverWhistleEnabled ? MAX_CLAP_INTENSITY : MAX_WHISTLE_INTENSITY, currentIntensity)); + + updateCurrentIntensityUI(); + + // Don't adjust volume or position while a sound is playing. + if (!soundInjector || soundInjector.isPlaying()) { + return; + } + + var injectorOptions = { + position: getAppreciationPosition(), + volume: calculateInjectorVolume() + }; + + soundInjector.setOptions(injectorOptions); + } + + + // Call this function to actually play a sound. + // Doesn't play a new sound if a sound is playing AND (you're whistling OR you're in HMD) + // Injectors are placed between the user's hands (at the same location as the apprecation + // entity) and are randomly pitched between a MIN and MAX value. + // Only uses one injector, ever. + var soundInjector = false; + var MINIMUM_PITCH = 0.85; + var MAXIMUM_PITCH = 1.15; + function playSound(sound) { + if (soundInjector && soundInjector.isPlaying() && (currentSound === "whistle" || HMD.active)) { + return; + } + + if (soundInjector) { + soundInjector.stop(); + soundInjector = false; + } + + soundInjector = Audio.playSound(sound, { + position: getAppreciationPosition(), + volume: calculateInjectorVolume(), + pitch: randomFloat(MINIMUM_PITCH, MAXIMUM_PITCH) + }); + } + + + // Returns true if the global intensity and user settings dictate that clapping is the + // right thing to do. + function shouldClap() { + return (currentIntensity > 0.0 && neverWhistleEnabled) || + (currentIntensity > 0.0 && currentIntensity <= MAX_CLAP_INTENSITY); + } + + + // Returns true if the global intensity and user settings dictate that whistling is the + // right thing to do. + function shouldWhistle() { + return currentIntensity > MAX_CLAP_INTENSITY && + currentIntensity <= MAX_WHISTLE_INTENSITY; + } + + + // Selects the correct sound, then plays it. + var currentSound; + function selectAndPlaySound() { + if (shouldClap()) { + currentSound = "clap"; + playSound(clapSounds[Math.floor(Math.random() * clapSounds.length)]); + } else if (shouldWhistle()) { + currentSound = "whistle"; + playSound(whistleSounds[Math.floor(Math.random() * whistleSounds.length)]); + } + } + + + // If there exists a VR debounce timer (used for not playing sounds too often), + // clear it. + function maybeClearVRDebounceTimer() { + if (vrDebounceTimer) { + Script.clearTimeout(vrDebounceTimer); + vrDebounceTimer = false; + } + } + + + // Calculates the current intensity of appreciation based on the user's + // hand velocity (rotational and linear). + // Each type of velocity is weighted differently when determining the final intensity. + // The VR debounce timer length changes based on current intensity. This forces + // sounds to play further apart when the user isn't appreciating hard. + var MAX_VELOCITY_CM_PER_SEC = 110; // determined empirically + var MAX_ANGULAR_VELOCITY_LENGTH = 1.5; // determined empirically + var LINEAR_VELOCITY_WEIGHT = 0.7; // This and the line below must add up to 1.0 + var ANGULAR_VELOCITY_LENGTH_WEIGHT = 0.3; // This and the line below must add up to 1.0 + var vrDebounceTimer = false; + var VR_DEBOUNCE_TIMER_TIMEOUT_MIN_MS = 20; // determined empirically + var VR_DEBOUNCE_TIMER_TIMEOUT_MAX_MS = 200; // determined empirically + function calculateHandEffect(linearVelocity, angularVelocity){ + var leftHandLinearVelocityCMPerSec = linearVelocity.left; + var rightHandLinearVelocityCMPerSec = linearVelocity.right; + var averageLinearVelocity = (leftHandLinearVelocityCMPerSec + rightHandLinearVelocityCMPerSec) / 2; + averageLinearVelocity = Math.min(averageLinearVelocity, MAX_VELOCITY_CM_PER_SEC); + + var leftHandAngularVelocityLength = Vec3.length(angularVelocity.left); + var rightHandAngularVelocityLength = Vec3.length(angularVelocity.right); + var averageAngularVelocityIntensity = (leftHandAngularVelocityLength + rightHandAngularVelocityLength) / 2; + averageAngularVelocityIntensity = Math.min(averageAngularVelocityIntensity, MAX_ANGULAR_VELOCITY_LENGTH); + + var appreciationIntensity = + averageLinearVelocity / MAX_VELOCITY_CM_PER_SEC * LINEAR_VELOCITY_WEIGHT + + averageAngularVelocityIntensity / MAX_ANGULAR_VELOCITY_LENGTH * ANGULAR_VELOCITY_LENGTH_WEIGHT; + + fadeIntensity(appreciationIntensity); + + var vrDebounceTimeout = VR_DEBOUNCE_TIMER_TIMEOUT_MIN_MS + + (VR_DEBOUNCE_TIMER_TIMEOUT_MAX_MS - VR_DEBOUNCE_TIMER_TIMEOUT_MIN_MS) * (1.0 - appreciationIntensity); + // This timer forces a minimum tail duration for all sound clips + if (!vrDebounceTimer) { + selectAndPlaySound(); + vrDebounceTimer = Script.setTimeout(function() { + vrDebounceTimer = false; + }, vrDebounceTimeout); + } + } + + + // Gets both hands' linear velocity. + var lastLeftHandPosition = false; + var lastRightHandPosition = false; + function getHandsLinearVelocity() { + var linearVelocity = { + left: 0, + right: 0 + }; + + var leftHandPosition = MyAvatar.getJointPosition("LeftHand"); + var rightHandPosition = MyAvatar.getJointPosition("RightHand"); + + if (!lastLeftHandPosition || !lastRightHandPosition) { + lastLeftHandPosition = leftHandPosition; + lastRightHandPosition = rightHandPosition; + return linearVelocity; + } + + var leftHandDistanceCM = Vec3.distance(leftHandPosition, lastLeftHandPosition) * CM_PER_M; + var rightHandDistanceCM = Vec3.distance(rightHandPosition, lastRightHandPosition) * CM_PER_M; + + linearVelocity.left = leftHandDistanceCM / HAND_VELOCITY_CHECK_INTERVAL_MS * MS_PER_S; + linearVelocity.right = rightHandDistanceCM / HAND_VELOCITY_CHECK_INTERVAL_MS * MS_PER_S; + + lastLeftHandPosition = leftHandPosition; + lastRightHandPosition = rightHandPosition; + + return linearVelocity; + } + + + // Gets both hands' angular velocity. + var lastLeftHandRotation = false; + var lastRightHandRotation = false; + function getHandsAngularVelocity() { + var angularVelocity = { + left: {x: 0, y: 0, z: 0}, + right: {x: 0, y: 0, z: 0} + }; + + var leftHandRotation = MyAvatar.getJointRotation(MyAvatar.getJointIndex("LeftHand")); + var rightHandRotation = MyAvatar.getJointRotation(MyAvatar.getJointIndex("RightHand")); + + if (!lastLeftHandRotation || !lastRightHandRotation) { + lastLeftHandRotation = leftHandRotation; + lastRightHandRotation = rightHandRotation; + return angularVelocity; + } + + var leftHandAngleDelta = Quat.multiply(leftHandRotation, Quat.inverse(lastLeftHandRotation)); + var rightHandAngleDelta = Quat.multiply(rightHandRotation, Quat.inverse(lastRightHandRotation)); + + leftHandAngleDelta = Quat.safeEulerAngles(leftHandAngleDelta); + rightHandAngleDelta = Quat.safeEulerAngles(rightHandAngleDelta); + + angularVelocity.left = Vec3.multiply(leftHandAngleDelta, 1 / HAND_VELOCITY_CHECK_INTERVAL_MS); + angularVelocity.right = Vec3.multiply(rightHandAngleDelta, 1 / HAND_VELOCITY_CHECK_INTERVAL_MS); + + lastLeftHandRotation = leftHandRotation; + lastRightHandRotation = rightHandRotation; + + return angularVelocity; + } + + + // Calculates the hand effect (see above). Gets called on an interval, + // but only if the user's hands are above their head. This saves processing power. + // Also sets up the `updateIntensityEntity` interval. + function handVelocityCheck() { + if (!handsAreAboveHead) { + return; + } + + var handsLinearVelocity = getHandsLinearVelocity(); + var handsAngularVelocity = getHandsAngularVelocity(); + + calculateHandEffect(handsLinearVelocity, handsAngularVelocity); + + if (!updateIntensityEntityInterval && showAppreciationEntity) { + updateIntensityEntityInterval = Script.setInterval(updateIntensityEntity, UPDATE_INTENSITY_ENTITY_INTERVAL_MS); + } + } + + + // If handVelocityCheckInterval is set up, clear it. + function maybeClearHandVelocityCheck() { + if (handVelocityCheckInterval) { + Script.clearInterval(handVelocityCheckInterval); + handVelocityCheckInterval = false; + } + } + + + // If handVelocityCheckInterval is set up, clear it. + // Also stop the sound injector and set currentIntensity to 0. + function maybeClearHandVelocityCheckIntervalAndStopSound() { + maybeClearHandVelocityCheck(); + + if (soundInjector) { + soundInjector.stop(); + soundInjector = false; + } + + currentIntensity = 0.0; + } + + + // Sets up an interval that'll check the avatar's hand's velocities. + // This is used for calculating the effect. + // If the user isn't in HMD, we'll never set up this interval. + var handVelocityCheckInterval = false; + var HAND_VELOCITY_CHECK_INTERVAL_MS = 10; + function maybeSetupHandVelocityCheckInterval() { + // `!HMD.active` clause isn't really necessary, just extra protection + if (handVelocityCheckInterval || !HMD.active) { + return; + } + + handVelocityCheckInterval = Script.setInterval(handVelocityCheck, HAND_VELOCITY_CHECK_INTERVAL_MS); + } + + + // Checks the position of the user's hands to determine if they're above their head. + // If they are, sets up the hand velocity check interval (see above). + // If they aren't, clears that interval and stops the apprecation sound. + var handsAreAboveHead = false; + function handPositionCheck() { + var leftHandPosition = MyAvatar.getJointPosition("LeftHand"); + var rightHandPosition = MyAvatar.getJointPosition("RightHand"); + var headJointPosition = MyAvatar.getJointPosition("Head"); + + var headY = headJointPosition.y; + + handsAreAboveHead = (rightHandPosition.y > headY && leftHandPosition.y > headY); + + if (handsAreAboveHead) { + maybeSetupHandVelocityCheckInterval(); + } else { + maybeClearHandVelocityCheck(); + fadeOutAndStopSound(); + } + } + + + // If handPositionCheckInterval is set up, clear it. + function maybeClearHandPositionCheckInterval() { + if (handPositionCheckInterval) { + Script.clearInterval(handPositionCheckInterval); + handPositionCheckInterval = false; + } + } + + + // If the app is enabled, sets up an interval that'll check if the avatar's hands are above their head. + var handPositionCheckInterval = false; + var HAND_POSITION_CHECK_INTERVAL_MS = 200; + function maybeSetupHandPositionCheckInterval() { + if (!appreciateEnabled || !HMD.active) { + return; + } + + maybeClearHandPositionCheckInterval(); + + handPositionCheckInterval = Script.setInterval(handPositionCheck, HAND_POSITION_CHECK_INTERVAL_MS); + } + + + // If the interval that periodically lowers the apprecation volume is set up, clear it. + function maybeClearSlowAppreciationInterval() { + if (slowAppreciationInterval) { + Script.clearInterval(slowAppreciationInterval); + slowAppreciationInterval = false; + } + } + + + // Stop appreciating. Called when Appreciating from Desktop mode. + function stopAppreciating() { + maybeClearStopAppreciatingTimeout(); + maybeClearSlowAppreciationInterval(); + maybeClearUpdateIntensityEntityInterval(); + MyAvatar.restoreAnimation(); + currentAnimationFPS = INITIAL_ANIMATION_FPS; + currentlyPlayingFrame = 0; + currentAnimationTimestamp = 0; + } + + + // If the timeout that stops the user's apprecation is set up, clear it. + function maybeClearStopAppreciatingTimeout() { + if (stopAppreciatingTimeout) { + Script.clearTimeout(stopAppreciatingTimeout); + stopAppreciatingTimeout = false; + } + } + + + function calculateCurrentAnimationFPS(frameCount) { + var animationTimestampDeltaMS = Date.now() - currentAnimationTimestamp; + var frameDelta = animationTimestampDeltaMS / MS_PER_S * currentAnimationFPS; + + currentlyPlayingFrame = (currentlyPlayingFrame + frameDelta) % frameCount; + + currentAnimationFPS = currentIntensity * CHEERING_FPS_MAX + INITIAL_ANIMATION_FPS; + + currentAnimationFPS = Math.min(currentAnimationFPS, CHEERING_FPS_MAX); + + if (currentAnimation === clappingAnimation) { + currentAnimationFPS += CLAP_ANIMATION_FPS_BOOST; + } + } + + + // Called on an interval. Slows down the user's appreciation! + var VOLUME_STEP_DOWN_DESKTOP = 0.01; // Unitless, determined empirically + function slowAppreciation() { + currentIntensity -= VOLUME_STEP_DOWN_DESKTOP; + fadeIntensity(currentIntensity, INTENSITY_MAX_STEP_SIZE_DESKTOP); + + currentAnimation = selectAnimation(); + + if (!currentAnimation) { + stopAppreciating(); + return; + } + + var frameCount = currentAnimation.animation.frames.length; + + calculateCurrentAnimationFPS(frameCount); + + MyAvatar.overrideAnimation(currentAnimation.url, currentAnimationFPS, true, currentlyPlayingFrame, frameCount); + + currentAnimationTimestamp = Date.now(); + } + + + // Selects the proper animation to use when Appreciating in Desktop mode. + function selectAnimation() { + if (shouldClap()) { + if (currentAnimation === whistlingAnimation) { + currentAnimationTimestamp = 0; + } + return clappingAnimation; + } else if (shouldWhistle()) { + if (currentAnimation === clappingAnimation) { + currentAnimationTimestamp = 0; + } + return whistlingAnimation; + } else { + return false; + } + } + + + // Called when the Z key is pressed (and some other conditions are met). + // 1. (Maybe) clears old intervals + // 2. Steps up the global currentIntensity, then forces the effect/sound to fade/play immediately + // 3. Selects an animation to play based on various factors, then plays it + // - Stops appreciating if the selected animation is falsey + // 4. Sets up the "Slow Appreciation" interval which slows appreciation over time + // 5. Modifies the avatar's animation based on the current appreciation intensity + // - Since there's no way to modify animation FPS on-the-fly, we have to calculate + // where the animation should start based on where it was before changing FPS + // 6. Sets up the `updateIntensityEntity` interval if one isn't already setup + var INITIAL_ANIMATION_FPS = 7; + var SLOW_APPRECIATION_INTERVAL_MS = 100; + var CHEERING_FPS_MAX = 80; + var VOLUME_STEP_UP_DESKTOP = 0.035; // Unitless, determined empirically + var CLAP_ANIMATION_FPS_BOOST = 15; + var currentAnimation = false; + var currentAnimationFPS = INITIAL_ANIMATION_FPS; + var slowAppreciationInterval = false; + var currentlyPlayingFrame = 0; + var currentAnimationTimestamp; + function keyPressed() { + // Don't do anything if the animations aren't cached. + if (!whistlingAnimation || !clappingAnimation) { + return; + } + + maybeClearSoundFadeInterval(); + maybeClearStopAppreciatingTimeout(); + + currentIntensity += VOLUME_STEP_UP_DESKTOP; + fadeIntensity(currentIntensity, INTENSITY_MAX_STEP_SIZE_DESKTOP); + selectAndPlaySound(); + + currentAnimation = selectAnimation(); + + if (!currentAnimation) { + stopAppreciating(); + return; + } + + if (!slowAppreciationInterval) { + slowAppreciationInterval = Script.setInterval(slowAppreciation, SLOW_APPRECIATION_INTERVAL_MS); + } + + var frameCount = currentAnimation.animation.frames.length; + + if (currentAnimationTimestamp > 0) { + calculateCurrentAnimationFPS(frameCount); + } else { + currentlyPlayingFrame = 0; + } + + MyAvatar.overrideAnimation(currentAnimation.url, currentAnimationFPS, true, currentlyPlayingFrame, frameCount); + currentAnimationTimestamp = Date.now(); + + if (!updateIntensityEntityInterval && showAppreciationEntity) { + updateIntensityEntityInterval = Script.setInterval(updateIntensityEntity, UPDATE_INTENSITY_ENTITY_INTERVAL_MS); + } + } + + + // The listener for all in-app keypresses. Listens for an unshifted, un-alted, un-ctrl'd + // "Z" keypress. Only listens when in Desktop mode. If the user is holding the key down, + // we make sure not to call the `keyPressed()` handler too often using the `desktopDebounceTimer`. + var desktopDebounceTimer = false; + var DESKTOP_DEBOUNCE_TIMEOUT_MS = 160; + function keyPressEvent(event) { + if (!appreciateEnabled) { + return; + } + + if ((event.text.toUpperCase() === "Z") && + !event.isShifted && + !event.isMeta && + !event.isControl && + !event.isAlt && + !HMD.active) { + + if (event.isAutoRepeat) { + if (!desktopDebounceTimer) { + keyPressed(); + + desktopDebounceTimer = Script.setTimeout(function() { + desktopDebounceTimer = false; + }, DESKTOP_DEBOUNCE_TIMEOUT_MS); + } + } else { + keyPressed(); + } + } + } + + + // Sets up a timeout that will fade out the appreciation sound, then stop it. + var stopAppreciatingTimeout = false; + var STOP_APPRECIATING_TIMEOUT_MS = 1000; + function stopAppreciatingSoon() { + maybeClearStopAppreciatingTimeout(); + + if (currentIntensity > 0) { + stopAppreciatingTimeout = Script.setTimeout(fadeOutAndStopSound, STOP_APPRECIATING_TIMEOUT_MS); + } + } + + + // When the "Z" key is released, we want to stop appreciating a short time later. + function keyReleaseEvent(event) { + if (!appreciateEnabled) { + return; + } + + if ((event.text.toUpperCase() === "Z") && + !event.isAutoRepeat) { + stopAppreciatingSoon(); + } + } + + + // Enables or disables the app's main functionality + var appreciateEnabled = Settings.getValue("appreciate/enabled", false); + var neverWhistleEnabled = Settings.getValue("appreciate/neverWhistle", false); + var showAppreciationEntity = Settings.getValue("appreciate/showAppreciationEntity", true); + var keyEventsWired = false; + function enableOrDisableAppreciate() { + maybeClearHandPositionCheckInterval(); + maybeClearHandVelocityCheckIntervalAndStopSound(); + + if (appreciateEnabled) { + maybeSetupHandPositionCheckInterval(); + + if (!keyEventsWired && !HMD.active) { + Controller.keyPressEvent.connect(keyPressEvent); + Controller.keyReleaseEvent.connect(keyReleaseEvent); + keyEventsWired = true; + } + } else { + stopAppreciating(); + + if (keyEventsWired) { + Controller.keyPressEvent.disconnect(keyPressEvent); + Controller.keyReleaseEvent.disconnect(keyReleaseEvent); + keyEventsWired = false; + } + } + } + + + // Handles incoming messages from the UI + // - "eventBridgeReady" - The App's UI will send this when it's ready to + // receive events over the Event Bridge + // - "appreciateSwitchClicked" - The App's UI will send this when the user + // clicks the main toggle switch in the top right of the app + // - "neverWhistleCheckboxClicked" - Sent when the user clicks the + // "Never Whistle" checkbox + // - "setEntityColor" - Sent when the user chooses a new Entity Color. + function onMessage(message) { + if (message.app !== "appreciate") { + return; + } + + switch (message.method) { + case "eventBridgeReady": + ui.sendMessage({ + method: "updateUI", + appreciateEnabled: appreciateEnabled, + neverWhistleEnabled: neverWhistleEnabled, + showAppreciationEntity: showAppreciationEntity, + isFirstRun: Settings.getValue("appreciate/firstRun", true), + entityColor: intensityEntityColorMax + }); + break; + + case "appreciateSwitchClicked": + Settings.setValue("appreciate/firstRun", false); + appreciateEnabled = message.appreciateEnabled; + Settings.setValue("appreciate/enabled", appreciateEnabled); + enableOrDisableAppreciate(); + break; + + case "neverWhistleCheckboxClicked": + neverWhistleEnabled = message.neverWhistle; + Settings.setValue("appreciate/neverWhistle", neverWhistleEnabled); + break; + + case "showAppreciationEntityCheckboxClicked": + showAppreciationEntity = message.showAppreciationEntity; + Settings.setValue("appreciate/showAppreciationEntity", showAppreciationEntity); + break; + + case "setEntityColor": + intensityEntityColorMax = message.entityColor; + Settings.setValue("appreciate/entityColor", JSON.stringify(intensityEntityColorMax)); + break; + + case "zKeyDown": + var pressEvent = { + "text": "Z", + "isShifted": false, + "isMeta": false, + "isControl": false, + "isAlt": false, + "isAutoRepeat": message.repeat + }; + keyPressEvent(pressEvent); + break; + + case "zKeyUp": + var releaseEvent = { + "text": "Z", + "isShifted": false, + "isMeta": false, + "isControl": false, + "isAlt": false, + "isAutoRepeat": false + }; + keyReleaseEvent(releaseEvent); + break; + + default: + console.log("Unhandled message from appreciate_ui.js: " + JSON.stringify(message)); + break; + } + } + + + // Searches through all of your avatar entities and deletes any with the name + // that equals the one set when rezzing the Intensity Entity + function cleanupOldIntensityEntities() { + MyAvatar.getAvatarEntitiesVariant().forEach(function(avatarEntity) { + var name = Entities.getEntityProperties(avatarEntity.id, 'name').name; + if (name === INTENSITY_ENTITY_NAME && avatarEntity.id !== intensityEntity) { + Entities.deleteEntity(avatarEntity.id); + } + }); + } + + + // Called when the script is stopped. STOP ALL THE THINGS! + function onScriptEnding() { + maybeClearHandPositionCheckInterval(); + maybeClearHandVelocityCheckIntervalAndStopSound(); + maybeClearSoundFadeInterval(); + maybeClearVRDebounceTimer(); + maybeClearUpdateIntensityEntityInterval(); + cleanupOldIntensityEntities(); + + maybeClearStopAppreciatingTimeout(); + stopAppreciating(); + + if (desktopDebounceTimer) { + Script.clearTimeout(desktopDebounceTimer); + desktopDebounceTimer = false; + } + + if (keyEventsWired) { + Controller.keyPressEvent.disconnect(keyPressEvent); + Controller.keyReleaseEvent.disconnect(keyReleaseEvent); + keyEventsWired = false; + } + + if (intensityEntity) { + Entities.deleteEntity(intensityEntity); + intensityEntity = false; + } + + HMD.displayModeChanged.disconnect(enableOrDisableAppreciate); + } + + + // When called, this function will stop the versions of this script that are + // baked into the client installation IF there's another version of the script + // running that ISN'T the baked version. + function maybeStopBakedScriptVersions() { + var THIS_SCRIPT_FILENAME = "appreciate_app.js"; + var RELATIVE_PATH_TO_BAKED_SCRIPT = "system/experiences/appreciate/appResources/appData/" + THIS_SCRIPT_FILENAME; + var bakedLocalScriptPaths = []; + var alsoRunningNonBakedVersion = false; + + var runningScripts = ScriptDiscoveryService.getRunning(); + runningScripts.forEach(function(scriptObject) { + if (scriptObject.local && scriptObject.url.indexOf(RELATIVE_PATH_TO_BAKED_SCRIPT) > -1) { + bakedLocalScriptPaths.push(scriptObject.path); + } + + if (scriptObject.name === THIS_SCRIPT_FILENAME && scriptObject.url.indexOf(RELATIVE_PATH_TO_BAKED_SCRIPT) === -1) { + alsoRunningNonBakedVersion = true; + } + }); + + if (alsoRunningNonBakedVersion && bakedLocalScriptPaths.length > 0) { + for (var i = 0; i < bakedLocalScriptPaths.length; i++) { + ScriptDiscoveryService.stopScript(bakedLocalScriptPaths[i]); + } + } + } + + + // Called when the script starts up + var BUTTON_NAME = "APPRECIATE"; + var APP_UI_URL = Script.resolvePath('resources/appreciate_ui.html'); + var CLEANUP_INTENSITY_ENTITIES_STARTUP_DELAY_MS = 5000; + var AppUI = Script.require('appUi'); + var ui; + function startup() { + ui = new AppUI({ + buttonName: BUTTON_NAME, + home: APP_UI_URL, + // clap by Rena from the Noun Project + graphicsDirectory: Script.resolvePath("./resources/images/icons/"), + onOpened: onOpened, + onMessage: onMessage + }); + + cleanupOldIntensityEntities(); + // We need this because sometimes avatar entities load after this script does. + Script.setTimeout(cleanupOldIntensityEntities, CLEANUP_INTENSITY_ENTITIES_STARTUP_DELAY_MS); + enableOrDisableAppreciate(); + getSounds(); + getAnimations(); + HMD.displayModeChanged.connect(enableOrDisableAppreciate); + maybeStopBakedScriptVersions(); + } + + + Script.scriptEnding.connect(onScriptEnding); + startup(); +})(); + diff --git a/scripts/system/appreciate/resource.json b/scripts/system/appreciate/resource.json new file mode 100644 index 0000000000..a83500fe12 --- /dev/null +++ b/scripts/system/appreciate/resource.json @@ -0,0 +1,41 @@ +{ + "name": "Appreciate App", + "version": "1.5.0", + "description": "Show someone else that you like what they're doing. Open the app to see usage instructions and some options!", + "homepage": "http://www.vircadia.com", + "bugs": "", + "keywords": [ + "Clapping", + "Clap", + "Applause" + ], + "icon": "/appreciate.jpg", + "images": [ + "/appreciate.jpg" + ], + "author": { + "name": "High Fidelity", + "email": "", + "url": "", + "license": "Apache 2.0" + }, + "sublicense": [ + ], + "contributors": [ + ], + "repository": { + "type": "git", + "url": "https://github.com/vircadia/vircadia-content.git" + }, + "main": "/appreciate_app.js", + "type": "app", + "meta": { + }, + "dependencies": { + }, + "engines": { + }, + "resource": { + "version": 1.0.0 + } +} \ No newline at end of file diff --git a/scripts/system/appreciate/resources/animations/Cheering.fbx b/scripts/system/appreciate/resources/animations/Cheering.fbx new file mode 100644 index 0000000000..8787bf4bd8 Binary files /dev/null and b/scripts/system/appreciate/resources/animations/Cheering.fbx differ diff --git a/scripts/system/appreciate/resources/animations/Clapping.fbx b/scripts/system/appreciate/resources/animations/Clapping.fbx new file mode 100644 index 0000000000..d05b41866d Binary files /dev/null and b/scripts/system/appreciate/resources/animations/Clapping.fbx differ diff --git a/scripts/system/appreciate/resources/appreciate_ui.html b/scripts/system/appreciate/resources/appreciate_ui.html new file mode 100644 index 0000000000..24ffb6fb64 --- /dev/null +++ b/scripts/system/appreciate/resources/appreciate_ui.html @@ -0,0 +1,84 @@ + + + + + + Appreciate + + + + + + + + +
+
+ +
+
+
+ Appreciate v1.5 +
+ +
+ + + +
+ Intensity Meter +
+ +
+
+
+ +
+ Options + + + + + +
+ + +
+
+ +
+
+ Desktop Mode:
Tap or hold the Z key on your keyboard! +
+
+ VR Mode:
Raise your hands above your head and shake them! +
+
+
+ + + + + diff --git a/scripts/system/appreciate/resources/css/style.css b/scripts/system/appreciate/resources/css/style.css new file mode 100644 index 0000000000..abd58b1b22 --- /dev/null +++ b/scripts/system/appreciate/resources/css/style.css @@ -0,0 +1,284 @@ +*, *:before, *:after { + -webkit-box-sizing: inherit; + -moz-box-sizing: inherit; + box-sizing: inherit; +} + +html { + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; +} + +body { + font-family: 'Raleway', sans-serif; + background-color: #393939; + color: #afafaf; + overflow: hidden; + margin: 0; + padding: 0; +} + +#mainContainer { + width: 100vw; + height: 100vh; +} + +#loadingContainer { + background-color: rgba(0, 0, 0, 0.8); + background-image: url('../images/loadingSpinner.svg'); + background-repeat: no-repeat; + background-position: center center; + width: 100vw; + height: 100vh; + position: fixed; + z-index: 999; +} + +#firstRun { + background-color: rgba(0, 0, 0, 0.9); + width: 100vw; + height: calc(100vh - 60px); + position: fixed; + z-index: 998; + padding: 8px 12px 0 50%; + font-size: 24px; + text-align: right; +} + +#tutorialArrow { + border: solid #00b4ef; + border-width: 0 5px 5px 0; + margin-right: 28px; + display: inline-block; + padding: 5px; + transform: rotate(-135deg); + -webkit-transform: rotate(-135deg); +} + +/* START SWITCH CSS +Mostly from: https://www.w3schools.com/howto/howto_css_switch.asp +*/ +#titleBarContainer { + display: flex; + align-items: center; + height: 60px; + padding: 0 16px; + font-size: 24px; + background-color: #121212; + color: #ffffff; + justify-content: space-between; +} + +/* The switch - the box around the slider */ +.switch { + position: relative; + display: block; + width: 70px; + height: 34px; +} + +/* Hide default HTML checkbox */ +.switch input { + opacity: 0; + width: 0; + height: 0; +} + +/* The slider */ +.slider { + position: absolute; + cursor: pointer; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: #ccc; + -webkit-transition: .4s; + transition: .4s; +} + +.slider:before { + position: absolute; + content: ""; + height: 26px; + width: 26px; + left: 4px; + bottom: 4px; + background-color: white; + -webkit-transition: .4s; + transition: .4s; +} + +input:checked + .slider { + background-color: #00b4ef; +} + +input:focus + .slider { + box-shadow: 0 0 1px #00b4ef; +} + +input:checked + .slider:before { + -webkit-transform: translateX(35px); + -ms-transform: translateX(35px); + transform: translateX(35px); +} + +/* Rounded sliders */ +.slider.round { + border-radius: 34px; +} + +.slider.round:before { + border-radius: 50%; +} +/* END SWITCH CSS */ + +/* START PROGRESS BAR CSS */ +#progressBarContainer { + width: calc(100vw - 24px); + margin: 24px auto 0 auto; +} + +#progressBarContainer > span { + font-size: 18px; +} + +#currentIntensityDisplay { + width: 100%; + height: 175px; + margin-top: 8px; + background: #FFFFFF; + background-image: linear-gradient(to right, #EEE 0, #EEE 55%, #FFF 55%, #FFF 100%); +} + +#crosshatch { + display: none; + float: right; + position: relative; + top: -175px; + height: 175px; + width: 45%; + background: repeating-linear-gradient(45deg, transparent 0px, transparent 4px, rgba(0, 0, 0, 0.1) 4px, rgba(0, 0, 0, 0.1) 8px); +} + +#currentIntensity { + display: block; + height: 100%; + background-color: #1ac567; + background-image: linear-gradient(to right,#1ac567 0, #C62147 100%); + position: relative; + overflow: hidden; +} +/* END PROGRESS BAR CSS */ + +#optionsContainer { + display: flex; + flex-direction: column; + height: 150px; + width: calc(100vw - 24px); + margin: 12px 12px 0 12px; + position: absolute; +} + +#colorPickerContainer { + margin: 8px 0 0 0; + visibility: hidden; +} + +#colorPickerContainer > input { + font-family: 'Raleway', sans-serif; + height: 34px; + font-size: 18px; + min-width: 185px; +} + +.checkmark { + position: absolute; + top: 0; + left: 0; + height: 25px; + width: 25px; + background-color: #eee; +} + +/* Create the checkmark/indicator (hidden when not checked) */ +.checkmark:after { + content: ""; + position: absolute; + display: none; +} + +#neverWhistleContainer, +#showAppreciationEntityContainer { + display: block; + margin: 8px 0 0 0; + height: 25px; + position: relative; + padding-left: 35px; + cursor: pointer; + font-size: 18px; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} + +#showAppreciationEntityContainer { + margin-top: 16px; +} + +/* Hide the browser's default checkbox */ +#neverWhistleContainer input, +#showAppreciationEntityContainer input { + position: absolute; + opacity: 0; + cursor: pointer; + height: 0; + width: 0; +} + +/* On mouse-over, add a grey background color */ +#neverWhistleContainer:hover input ~ .checkmark, +#showAppreciationEntityContainer:hover input ~ .checkmark { + background-color: #ccc; +} + +/* When the checkbox is checked, add a blue background */ +#neverWhistleContainer input:checked ~ .checkmark, +#showAppreciationEntityContainer input:checked ~ .checkmark { + background-color: #0093C5; +} + +/* Show the checkmark when checked */ +#neverWhistleContainer input:checked ~ .checkmark:after, +#showAppreciationEntityContainer input:checked ~ .checkmark:after { + display: block; +} + +/* Style the checkmark/indicator */ +#neverWhistleContainer .checkmark:after, +#showAppreciationEntityContainer .checkmark:after { + left: 9px; + top: 3px; + width: 8px; + height: 15px; + border: solid white; + border-width: 0 3px 3px 0; + transform: rotate(45deg); + -webkit-transform: rotate(45deg); +} + +#instructions { + position: fixed; + height: 150px; + bottom: 0; + left: 0; + right: 0; + margin: 0 12px; + font-size: 18px; +} + +#instructions > div { + margin-top: 16px; +} \ No newline at end of file diff --git a/scripts/system/appreciate/resources/images/icons/appreciate-a.svg b/scripts/system/appreciate/resources/images/icons/appreciate-a.svg new file mode 100644 index 0000000000..44cd326ea1 --- /dev/null +++ b/scripts/system/appreciate/resources/images/icons/appreciate-a.svg @@ -0,0 +1,89 @@ + + + + + + image/svg+xml + + SCHOOL_ICONS_100 + + + + + + SCHOOL_ICONS_100 + + + + + Created by Rena + from the Noun Project + diff --git a/scripts/system/appreciate/resources/images/icons/appreciate-i.svg b/scripts/system/appreciate/resources/images/icons/appreciate-i.svg new file mode 100644 index 0000000000..74ae65b19f --- /dev/null +++ b/scripts/system/appreciate/resources/images/icons/appreciate-i.svg @@ -0,0 +1,89 @@ + + + + + + image/svg+xml + + SCHOOL_ICONS_100 + + + + + + SCHOOL_ICONS_100 + + + + + Created by Rena + from the Noun Project + diff --git a/scripts/system/appreciate/resources/images/loadingSpinner.svg b/scripts/system/appreciate/resources/images/loadingSpinner.svg new file mode 100644 index 0000000000..a290bb8c60 --- /dev/null +++ b/scripts/system/appreciate/resources/images/loadingSpinner.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/scripts/system/appreciate/resources/js/appreciate_ui.js b/scripts/system/appreciate/resources/js/appreciate_ui.js new file mode 100644 index 0000000000..b955193b45 --- /dev/null +++ b/scripts/system/appreciate/resources/js/appreciate_ui.js @@ -0,0 +1,188 @@ +/* + Appreciate + Created by Zach Fox on 2019-01-30 + 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 +*/ + +/* globals document EventBridge setTimeout */ + +// Called when the user clicks the switch in the top right of the app. +// Sends an event to the App JS and clears the `firstRun` `div`. +function appreciateSwitchClicked(checkbox) { + EventBridge.emitWebEvent(JSON.stringify({ + app: "appreciate", + method: "appreciateSwitchClicked", + appreciateEnabled: checkbox.checked + })); + document.getElementById("firstRun").style.display = "none"; +} + +// Called when the user checks/unchecks the Never Whistle checkbox. +// Adds the crosshatch div to the UI and sends an event to the App JS. +function neverWhistleCheckboxClicked(checkbox) { + var crosshatch = document.getElementById("crosshatch"); + if (checkbox.checked) { + crosshatch.style.display = "inline-block"; + } else { + crosshatch.style.display = "none"; + } + + EventBridge.emitWebEvent(JSON.stringify({ + app: "appreciate", + method: "neverWhistleCheckboxClicked", + neverWhistle: checkbox.checked + })); +} + +// Called when the user checks/unchecks the Show Appreciation Entity checkbox. +// Sends an event to the App JS. +function showAppreciationEntityCheckboxClicked(checkbox) { + EventBridge.emitWebEvent(JSON.stringify({ + app: "appreciate", + method: "showAppreciationEntityCheckboxClicked", + showAppreciationEntity: checkbox.checked + })); + + if (checkbox.checked) { + document.getElementById("colorPickerContainer").style.visibility = "visible"; + } else { + document.getElementById("colorPickerContainer").style.visibility = "hidden"; + } +} + +// Called when the user changes the entity's color using the jscolor picker. +// Modifies the color of the Intensity Meter gradient and sends a message to the App JS. +var START_COLOR_MULTIPLIER = 0.2; +function setEntityColor(jscolor) { + var newEntityColor = { + "red": jscolor.rgb[0], + "green": jscolor.rgb[1], + "blue": jscolor.rgb[2] + }; + + var startColor = { + "red": Math.floor(newEntityColor.red * START_COLOR_MULTIPLIER), + "green": Math.floor(newEntityColor.green * START_COLOR_MULTIPLIER), + "blue": Math.floor(newEntityColor.blue * START_COLOR_MULTIPLIER) + }; + + var currentIntensityDisplayWidth = document.getElementById("currentIntensityDisplay").offsetWidth; + var bgString = "linear-gradient(to right, rgb(" + startColor.red + ", " + + startColor.green + ", " + startColor.blue + ") 0, " + + jscolor.toHEXString() + " " + currentIntensityDisplayWidth + "px)"; + document.getElementById("currentIntensity").style.backgroundImage = bgString; + + EventBridge.emitWebEvent(JSON.stringify({ + app: "appreciate", + method: "setEntityColor", + entityColor: newEntityColor + })); +} + +// Handle EventBridge messages from *_app.js. +function onScriptEventReceived(message) { + try { + message = JSON.parse(message); + } catch (error) { + console.log("Couldn't parse script event message: " + error); + return; + } + + // This message gets sent by `entityList.js` when it shouldn't! + if (message.type === "removeEntities") { + return; + } + + switch (message.method) { + case "updateUI": + if (message.isFirstRun) { + document.getElementById("firstRun").style.display = "block"; + } + document.getElementById("appreciateSwitch").checked = message.appreciateEnabled; + document.getElementById("neverWhistleCheckbox").checked = message.neverWhistleEnabled; + + var showAppreciationEntityCheckbox = document.getElementById("showAppreciationEntityCheckbox"); + showAppreciationEntityCheckbox.checked = message.showAppreciationEntity; + if (showAppreciationEntityCheckbox.checked) { + document.getElementById("colorPickerContainer").style.visibility = "visible"; + } else { + document.getElementById("colorPickerContainer").style.visibility = "hidden"; + } + + if (message.neverWhistleEnabled) { + var crosshatch = document.getElementById("crosshatch"); + crosshatch.style.display = "inline-block"; + } + + document.getElementById("loadingContainer").style.display = "none"; + + var color = document.getElementById("colorPicker").jscolor; + color.fromRGB(message.entityColor.red, message.entityColor.green, message.entityColor.blue); + + var startColor = { + "red": Math.floor(color.rgb[0] * START_COLOR_MULTIPLIER), + "green": Math.floor(color.rgb[1] * START_COLOR_MULTIPLIER), + "blue": Math.floor(color.rgb[2] * START_COLOR_MULTIPLIER) + }; + var currentIntensityDisplayWidth = document.getElementById("currentIntensityDisplay").offsetWidth; + document.getElementById("currentIntensity").style.backgroundImage = + "linear-gradient(to right, rgb(" + startColor.red + ", " + + startColor.green + ", " + startColor.blue + ") 0, " + + color.toHEXString() + " " + currentIntensityDisplayWidth + "px)"; + break; + + case "updateCurrentIntensityUI": + document.getElementById("currentIntensity").style.width = message.currentIntensity * 100 + "%"; + break; + + default: + console.log("Unknown message received from appreciate_app.js! " + JSON.stringify(message)); + break; + } +} + +// This function detects a keydown on the document, which enables the app +// to forward these keypress events to the app JS. +function onKeyDown(e) { + var key = e.key.toUpperCase(); + if (key === "Z") { + EventBridge.emitWebEvent(JSON.stringify({ + app: "appreciate", + method: "zKeyDown", + repeat: e.repeat + })); + } +} + +// This function detects a keyup on the document, which enables the app +// to forward these keypress events to the app JS. +function onKeyUp(e) { + var key = e.key.toUpperCase(); + if (key === "Z") { + EventBridge.emitWebEvent(JSON.stringify({ + app: "appreciate", + method: "zKeyUp" + })); + } +} + +// This delay is necessary to allow for the JS EventBridge to become active. +// The delay is still necessary for HTML apps in RC78+. +var EVENTBRIDGE_SETUP_DELAY = 500; +function onLoad() { + setTimeout(function() { + EventBridge.scriptEventReceived.connect(onScriptEventReceived); + EventBridge.emitWebEvent(JSON.stringify({ + app: "appreciate", + method: "eventBridgeReady" + })); + }, EVENTBRIDGE_SETUP_DELAY); + + document.addEventListener("keydown", onKeyDown); + document.addEventListener("keyup", onKeyUp); +} + +onLoad(); \ No newline at end of file diff --git a/scripts/system/appreciate/resources/js/jscolor.js b/scripts/system/appreciate/resources/js/jscolor.js new file mode 100644 index 0000000000..9b5e8e6cbc --- /dev/null +++ b/scripts/system/appreciate/resources/js/jscolor.js @@ -0,0 +1,1855 @@ +/** + * jscolor - JavaScript Color Picker + * + * @link http://jscolor.com + * @license For open source use: GPLv3 + * For commercial use: JSColor Commercial License + * @author Jan Odvarko + * @version 2.0.5 + * + * See usage examples at http://jscolor.com/examples/ + */ + + +"use strict"; + + +if (!window.jscolor) { window.jscolor = (function () { + + +var jsc = { + + + register : function () { + jsc.attachDOMReadyEvent(jsc.init); + jsc.attachEvent(document, 'mousedown', jsc.onDocumentMouseDown); + jsc.attachEvent(document, 'touchstart', jsc.onDocumentTouchStart); + jsc.attachEvent(window, 'resize', jsc.onWindowResize); + }, + + + init : function () { + if (jsc.jscolor.lookupClass) { + jsc.jscolor.installByClassName(jsc.jscolor.lookupClass); + } + }, + + + tryInstallOnElements : function (elms, className) { + var matchClass = new RegExp('(^|\\s)(' + className + ')(\\s*(\\{[^}]*\\})|\\s|$)', 'i'); + + for (var i = 0; i < elms.length; i += 1) { + if (elms[i].type !== undefined && elms[i].type.toLowerCase() == 'color') { + if (jsc.isColorAttrSupported) { + // skip inputs of type 'color' if supported by the browser + continue; + } + } + var m; + if (!elms[i].jscolor && elms[i].className && (m = elms[i].className.match(matchClass))) { + var targetElm = elms[i]; + var optsStr = null; + + var dataOptions = jsc.getDataAttr(targetElm, 'jscolor'); + if (dataOptions !== null) { + optsStr = dataOptions; + } else if (m[4]) { + optsStr = m[4]; + } + + var opts = {}; + if (optsStr) { + try { + opts = (new Function ('return (' + optsStr + ')'))(); + } catch(eParseError) { + jsc.warn('Error parsing jscolor options: ' + eParseError + ':\n' + optsStr); + } + } + targetElm.jscolor = new jsc.jscolor(targetElm, opts); + } + } + }, + + + isColorAttrSupported : (function () { + var elm = document.createElement('input'); + if (elm.setAttribute) { + elm.setAttribute('type', 'color'); + if (elm.type.toLowerCase() == 'color') { + return true; + } + } + return false; + })(), + + + isCanvasSupported : (function () { + var elm = document.createElement('canvas'); + return !!(elm.getContext && elm.getContext('2d')); + })(), + + + fetchElement : function (mixed) { + return typeof mixed === 'string' ? document.getElementById(mixed) : mixed; + }, + + + isElementType : function (elm, type) { + return elm.nodeName.toLowerCase() === type.toLowerCase(); + }, + + + getDataAttr : function (el, name) { + var attrName = 'data-' + name; + var attrValue = el.getAttribute(attrName); + if (attrValue !== null) { + return attrValue; + } + return null; + }, + + + attachEvent : function (el, evnt, func) { + if (el.addEventListener) { + el.addEventListener(evnt, func, false); + } else if (el.attachEvent) { + el.attachEvent('on' + evnt, func); + } + }, + + + detachEvent : function (el, evnt, func) { + if (el.removeEventListener) { + el.removeEventListener(evnt, func, false); + } else if (el.detachEvent) { + el.detachEvent('on' + evnt, func); + } + }, + + + _attachedGroupEvents : {}, + + + attachGroupEvent : function (groupName, el, evnt, func) { + if (!jsc._attachedGroupEvents.hasOwnProperty(groupName)) { + jsc._attachedGroupEvents[groupName] = []; + } + jsc._attachedGroupEvents[groupName].push([el, evnt, func]); + jsc.attachEvent(el, evnt, func); + }, + + + detachGroupEvents : function (groupName) { + if (jsc._attachedGroupEvents.hasOwnProperty(groupName)) { + for (var i = 0; i < jsc._attachedGroupEvents[groupName].length; i += 1) { + var evt = jsc._attachedGroupEvents[groupName][i]; + jsc.detachEvent(evt[0], evt[1], evt[2]); + } + delete jsc._attachedGroupEvents[groupName]; + } + }, + + + attachDOMReadyEvent : function (func) { + var fired = false; + var fireOnce = function () { + if (!fired) { + fired = true; + func(); + } + }; + + if (document.readyState === 'complete') { + setTimeout(fireOnce, 1); // async + return; + } + + if (document.addEventListener) { + document.addEventListener('DOMContentLoaded', fireOnce, false); + + // Fallback + window.addEventListener('load', fireOnce, false); + + } else if (document.attachEvent) { + // IE + document.attachEvent('onreadystatechange', function () { + if (document.readyState === 'complete') { + document.detachEvent('onreadystatechange', arguments.callee); + fireOnce(); + } + }) + + // Fallback + window.attachEvent('onload', fireOnce); + + // IE7/8 + if (document.documentElement.doScroll && window == window.top) { + var tryScroll = function () { + if (!document.body) { return; } + try { + document.documentElement.doScroll('left'); + fireOnce(); + } catch (e) { + setTimeout(tryScroll, 1); + } + }; + tryScroll(); + } + } + }, + + + warn : function (msg) { + if (window.console && window.console.warn) { + window.console.warn(msg); + } + }, + + + preventDefault : function (e) { + if (e.preventDefault) { e.preventDefault(); } + e.returnValue = false; + }, + + + captureTarget : function (target) { + // IE + if (target.setCapture) { + jsc._capturedTarget = target; + jsc._capturedTarget.setCapture(); + } + }, + + + releaseTarget : function () { + // IE + if (jsc._capturedTarget) { + jsc._capturedTarget.releaseCapture(); + jsc._capturedTarget = null; + } + }, + + + fireEvent : function (el, evnt) { + if (!el) { + return; + } + if (document.createEvent) { + var ev = document.createEvent('HTMLEvents'); + ev.initEvent(evnt, true, true); + el.dispatchEvent(ev); + } else if (document.createEventObject) { + var ev = document.createEventObject(); + el.fireEvent('on' + evnt, ev); + } else if (el['on' + evnt]) { // alternatively use the traditional event model + el['on' + evnt](); + } + }, + + + classNameToList : function (className) { + return className.replace(/^\s+|\s+$/g, '').split(/\s+/); + }, + + + // The className parameter (str) can only contain a single class name + hasClass : function (elm, className) { + if (!className) { + return false; + } + return -1 != (' ' + elm.className.replace(/\s+/g, ' ') + ' ').indexOf(' ' + className + ' '); + }, + + + // The className parameter (str) can contain multiple class names separated by whitespace + setClass : function (elm, className) { + var classList = jsc.classNameToList(className); + for (var i = 0; i < classList.length; i += 1) { + if (!jsc.hasClass(elm, classList[i])) { + elm.className += (elm.className ? ' ' : '') + classList[i]; + } + } + }, + + + // The className parameter (str) can contain multiple class names separated by whitespace + unsetClass : function (elm, className) { + var classList = jsc.classNameToList(className); + for (var i = 0; i < classList.length; i += 1) { + var repl = new RegExp( + '^\\s*' + classList[i] + '\\s*|' + + '\\s*' + classList[i] + '\\s*$|' + + '\\s+' + classList[i] + '(\\s+)', + 'g' + ); + elm.className = elm.className.replace(repl, '$1'); + } + }, + + + getStyle : function (elm) { + return window.getComputedStyle ? window.getComputedStyle(elm) : elm.currentStyle; + }, + + + setStyle : (function () { + var helper = document.createElement('div'); + var getSupportedProp = function (names) { + for (var i = 0; i < names.length; i += 1) { + if (names[i] in helper.style) { + return names[i]; + } + } + }; + var props = { + borderRadius: getSupportedProp(['borderRadius', 'MozBorderRadius', 'webkitBorderRadius']), + boxShadow: getSupportedProp(['boxShadow', 'MozBoxShadow', 'webkitBoxShadow']) + }; + return function (elm, prop, value) { + switch (prop.toLowerCase()) { + case 'opacity': + var alphaOpacity = Math.round(parseFloat(value) * 100); + elm.style.opacity = value; + elm.style.filter = 'alpha(opacity=' + alphaOpacity + ')'; + break; + default: + elm.style[props[prop]] = value; + break; + } + }; + })(), + + + setBorderRadius : function (elm, value) { + jsc.setStyle(elm, 'borderRadius', value || '0'); + }, + + + setBoxShadow : function (elm, value) { + jsc.setStyle(elm, 'boxShadow', value || 'none'); + }, + + + getElementPos : function (e, relativeToViewport) { + var x=0, y=0; + var rect = e.getBoundingClientRect(); + x = rect.left; + y = rect.top; + if (!relativeToViewport) { + var viewPos = jsc.getViewPos(); + x += viewPos[0]; + y += viewPos[1]; + } + return [x, y]; + }, + + + getElementSize : function (e) { + return [e.offsetWidth, e.offsetHeight]; + }, + + + // get pointer's X/Y coordinates relative to viewport + getAbsPointerPos : function (e) { + if (!e) { e = window.event; } + var x = 0, y = 0; + if (typeof e.changedTouches !== 'undefined' && e.changedTouches.length) { + // touch devices + x = e.changedTouches[0].clientX; + y = e.changedTouches[0].clientY; + } else if (typeof e.clientX === 'number') { + x = e.clientX; + y = e.clientY; + } + return { x: x, y: y }; + }, + + + // get pointer's X/Y coordinates relative to target element + getRelPointerPos : function (e) { + if (!e) { e = window.event; } + var target = e.target || e.srcElement; + var targetRect = target.getBoundingClientRect(); + + var x = 0, y = 0; + + var clientX = 0, clientY = 0; + if (typeof e.changedTouches !== 'undefined' && e.changedTouches.length) { + // touch devices + clientX = e.changedTouches[0].clientX; + clientY = e.changedTouches[0].clientY; + } else if (typeof e.clientX === 'number') { + clientX = e.clientX; + clientY = e.clientY; + } + + x = clientX - targetRect.left; + y = clientY - targetRect.top; + return { x: x, y: y }; + }, + + + getViewPos : function () { + var doc = document.documentElement; + return [ + (window.pageXOffset || doc.scrollLeft) - (doc.clientLeft || 0), + (window.pageYOffset || doc.scrollTop) - (doc.clientTop || 0) + ]; + }, + + + getViewSize : function () { + var doc = document.documentElement; + return [ + (window.innerWidth || doc.clientWidth), + (window.innerHeight || doc.clientHeight), + ]; + }, + + + redrawPosition : function () { + + if (jsc.picker && jsc.picker.owner) { + var thisObj = jsc.picker.owner; + + var tp, vp; + + if (thisObj.fixed) { + // Fixed elements are positioned relative to viewport, + // therefore we can ignore the scroll offset + tp = jsc.getElementPos(thisObj.targetElement, true); // target pos + vp = [0, 0]; // view pos + } else { + tp = jsc.getElementPos(thisObj.targetElement); // target pos + vp = jsc.getViewPos(); // view pos + } + + var ts = jsc.getElementSize(thisObj.targetElement); // target size + var vs = jsc.getViewSize(); // view size + var ps = jsc.getPickerOuterDims(thisObj); // picker size + var a, b, c; + switch (thisObj.position.toLowerCase()) { + case 'left': a=1; b=0; c=-1; break; + case 'right':a=1; b=0; c=1; break; + case 'top': a=0; b=1; c=-1; break; + default: a=0; b=1; c=1; break; + } + var l = (ts[b]+ps[b])/2; + + // compute picker position + if (!thisObj.smartPosition) { + var pp = [ + tp[a], + tp[b]+ts[b]-l+l*c + ]; + } else { + var pp = [ + -vp[a]+tp[a]+ps[a] > vs[a] ? + (-vp[a]+tp[a]+ts[a]/2 > vs[a]/2 && tp[a]+ts[a]-ps[a] >= 0 ? tp[a]+ts[a]-ps[a] : tp[a]) : + tp[a], + -vp[b]+tp[b]+ts[b]+ps[b]-l+l*c > vs[b] ? + (-vp[b]+tp[b]+ts[b]/2 > vs[b]/2 && tp[b]+ts[b]-l-l*c >= 0 ? tp[b]+ts[b]-l-l*c : tp[b]+ts[b]-l+l*c) : + (tp[b]+ts[b]-l+l*c >= 0 ? tp[b]+ts[b]-l+l*c : tp[b]+ts[b]-l-l*c) + ]; + } + + var x = pp[a]; + var y = pp[b]; + var positionValue = thisObj.fixed ? 'fixed' : 'absolute'; + var contractShadow = + (pp[0] + ps[0] > tp[0] || pp[0] < tp[0] + ts[0]) && + (pp[1] + ps[1] < tp[1] + ts[1]); + + jsc._drawPosition(thisObj, x, y, positionValue, contractShadow); + } + }, + + + _drawPosition : function (thisObj, x, y, positionValue, contractShadow) { + var vShadow = contractShadow ? 0 : thisObj.shadowBlur; // px + + jsc.picker.wrap.style.position = positionValue; + jsc.picker.wrap.style.left = x + 'px'; + jsc.picker.wrap.style.top = y + 'px'; + + jsc.setBoxShadow( + jsc.picker.boxS, + thisObj.shadow ? + new jsc.BoxShadow(0, vShadow, thisObj.shadowBlur, 0, thisObj.shadowColor) : + null); + }, + + + getPickerDims : function (thisObj) { + var displaySlider = !!jsc.getSliderComponent(thisObj); + var dims = [ + 2 * thisObj.insetWidth + 2 * thisObj.padding + thisObj.width + + (displaySlider ? 2 * thisObj.insetWidth + jsc.getPadToSliderPadding(thisObj) + thisObj.sliderSize : 0), + 2 * thisObj.insetWidth + 2 * thisObj.padding + thisObj.height + + (thisObj.closable ? 2 * thisObj.insetWidth + thisObj.padding + thisObj.buttonHeight : 0) + ]; + return dims; + }, + + + getPickerOuterDims : function (thisObj) { + var dims = jsc.getPickerDims(thisObj); + return [ + dims[0] + 2 * thisObj.borderWidth, + dims[1] + 2 * thisObj.borderWidth + ]; + }, + + + getPadToSliderPadding : function (thisObj) { + return Math.max(thisObj.padding, 1.5 * (2 * thisObj.pointerBorderWidth + thisObj.pointerThickness)); + }, + + + getPadYComponent : function (thisObj) { + switch (thisObj.mode.charAt(1).toLowerCase()) { + case 'v': return 'v'; break; + } + return 's'; + }, + + + getSliderComponent : function (thisObj) { + if (thisObj.mode.length > 2) { + switch (thisObj.mode.charAt(2).toLowerCase()) { + case 's': return 's'; break; + case 'v': return 'v'; break; + } + } + return null; + }, + + + onDocumentMouseDown : function (e) { + if (!e) { e = window.event; } + var target = e.target || e.srcElement; + + if (target._jscLinkedInstance) { + if (target._jscLinkedInstance.showOnClick) { + target._jscLinkedInstance.show(); + } + } else if (target._jscControlName) { + jsc.onControlPointerStart(e, target, target._jscControlName, 'mouse'); + } else { + // Mouse is outside the picker controls -> hide the color picker! + if (jsc.picker && jsc.picker.owner) { + jsc.picker.owner.hide(); + } + } + }, + + + onDocumentTouchStart : function (e) { + if (!e) { e = window.event; } + var target = e.target || e.srcElement; + + if (target._jscLinkedInstance) { + if (target._jscLinkedInstance.showOnClick) { + target._jscLinkedInstance.show(); + } + } else if (target._jscControlName) { + jsc.onControlPointerStart(e, target, target._jscControlName, 'touch'); + } else { + if (jsc.picker && jsc.picker.owner) { + jsc.picker.owner.hide(); + } + } + }, + + + onWindowResize : function (e) { + jsc.redrawPosition(); + }, + + + onParentScroll : function (e) { + // hide the picker when one of the parent elements is scrolled + if (jsc.picker && jsc.picker.owner) { + jsc.picker.owner.hide(); + } + }, + + + _pointerMoveEvent : { + mouse: 'mousemove', + touch: 'touchmove' + }, + _pointerEndEvent : { + mouse: 'mouseup', + touch: 'touchend' + }, + + + _pointerOrigin : null, + _capturedTarget : null, + + + onControlPointerStart : function (e, target, controlName, pointerType) { + var thisObj = target._jscInstance; + + jsc.preventDefault(e); + jsc.captureTarget(target); + + var registerDragEvents = function (doc, offset) { + jsc.attachGroupEvent('drag', doc, jsc._pointerMoveEvent[pointerType], + jsc.onDocumentPointerMove(e, target, controlName, pointerType, offset)); + jsc.attachGroupEvent('drag', doc, jsc._pointerEndEvent[pointerType], + jsc.onDocumentPointerEnd(e, target, controlName, pointerType)); + }; + + registerDragEvents(document, [0, 0]); + + if (window.parent && window.frameElement) { + var rect = window.frameElement.getBoundingClientRect(); + var ofs = [-rect.left, -rect.top]; + registerDragEvents(window.parent.window.document, ofs); + } + + var abs = jsc.getAbsPointerPos(e); + var rel = jsc.getRelPointerPos(e); + jsc._pointerOrigin = { + x: abs.x - rel.x, + y: abs.y - rel.y + }; + + switch (controlName) { + case 'pad': + // if the slider is at the bottom, move it up + switch (jsc.getSliderComponent(thisObj)) { + case 's': if (thisObj.hsv[1] === 0) { thisObj.fromHSV(null, 100, null); }; break; + case 'v': if (thisObj.hsv[2] === 0) { thisObj.fromHSV(null, null, 100); }; break; + } + jsc.setPad(thisObj, e, 0, 0); + break; + + case 'sld': + jsc.setSld(thisObj, e, 0); + break; + } + + jsc.dispatchFineChange(thisObj); + }, + + + onDocumentPointerMove : function (e, target, controlName, pointerType, offset) { + return function (e) { + var thisObj = target._jscInstance; + switch (controlName) { + case 'pad': + if (!e) { e = window.event; } + jsc.setPad(thisObj, e, offset[0], offset[1]); + jsc.dispatchFineChange(thisObj); + break; + + case 'sld': + if (!e) { e = window.event; } + jsc.setSld(thisObj, e, offset[1]); + jsc.dispatchFineChange(thisObj); + break; + } + } + }, + + + onDocumentPointerEnd : function (e, target, controlName, pointerType) { + return function (e) { + var thisObj = target._jscInstance; + jsc.detachGroupEvents('drag'); + jsc.releaseTarget(); + // Always dispatch changes after detaching outstanding mouse handlers, + // in case some user interaction will occur in user's onchange callback + // that would intrude with current mouse events + jsc.dispatchChange(thisObj); + }; + }, + + + dispatchChange : function (thisObj) { + if (thisObj.valueElement) { + if (jsc.isElementType(thisObj.valueElement, 'input')) { + jsc.fireEvent(thisObj.valueElement, 'change'); + } + } + }, + + + dispatchFineChange : function (thisObj) { + if (thisObj.onFineChange) { + var callback; + if (typeof thisObj.onFineChange === 'string') { + callback = new Function (thisObj.onFineChange); + } else { + callback = thisObj.onFineChange; + } + callback.call(thisObj); + } + }, + + + setPad : function (thisObj, e, ofsX, ofsY) { + var pointerAbs = jsc.getAbsPointerPos(e); + var x = ofsX + pointerAbs.x - jsc._pointerOrigin.x - thisObj.padding - thisObj.insetWidth; + var y = ofsY + pointerAbs.y - jsc._pointerOrigin.y - thisObj.padding - thisObj.insetWidth; + + var xVal = x * (360 / (thisObj.width - 1)); + var yVal = 100 - (y * (100 / (thisObj.height - 1))); + + switch (jsc.getPadYComponent(thisObj)) { + case 's': thisObj.fromHSV(xVal, yVal, null, jsc.leaveSld); break; + case 'v': thisObj.fromHSV(xVal, null, yVal, jsc.leaveSld); break; + } + }, + + + setSld : function (thisObj, e, ofsY) { + var pointerAbs = jsc.getAbsPointerPos(e); + var y = ofsY + pointerAbs.y - jsc._pointerOrigin.y - thisObj.padding - thisObj.insetWidth; + + var yVal = 100 - (y * (100 / (thisObj.height - 1))); + + switch (jsc.getSliderComponent(thisObj)) { + case 's': thisObj.fromHSV(null, yVal, null, jsc.leavePad); break; + case 'v': thisObj.fromHSV(null, null, yVal, jsc.leavePad); break; + } + }, + + + _vmlNS : 'jsc_vml_', + _vmlCSS : 'jsc_vml_css_', + _vmlReady : false, + + + initVML : function () { + if (!jsc._vmlReady) { + // init VML namespace + var doc = document; + if (!doc.namespaces[jsc._vmlNS]) { + doc.namespaces.add(jsc._vmlNS, 'urn:schemas-microsoft-com:vml'); + } + if (!doc.styleSheets[jsc._vmlCSS]) { + var tags = ['shape', 'shapetype', 'group', 'background', 'path', 'formulas', 'handles', 'fill', 'stroke', 'shadow', 'textbox', 'textpath', 'imagedata', 'line', 'polyline', 'curve', 'rect', 'roundrect', 'oval', 'arc', 'image']; + var ss = doc.createStyleSheet(); + ss.owningElement.id = jsc._vmlCSS; + for (var i = 0; i < tags.length; i += 1) { + ss.addRule(jsc._vmlNS + '\\:' + tags[i], 'behavior:url(#default#VML);'); + } + } + jsc._vmlReady = true; + } + }, + + + createPalette : function () { + + var paletteObj = { + elm: null, + draw: null + }; + + if (jsc.isCanvasSupported) { + // Canvas implementation for modern browsers + + var canvas = document.createElement('canvas'); + var ctx = canvas.getContext('2d'); + + var drawFunc = function (width, height, type) { + canvas.width = width; + canvas.height = height; + + ctx.clearRect(0, 0, canvas.width, canvas.height); + + var hGrad = ctx.createLinearGradient(0, 0, canvas.width, 0); + hGrad.addColorStop(0 / 6, '#F00'); + hGrad.addColorStop(1 / 6, '#FF0'); + hGrad.addColorStop(2 / 6, '#0F0'); + hGrad.addColorStop(3 / 6, '#0FF'); + hGrad.addColorStop(4 / 6, '#00F'); + hGrad.addColorStop(5 / 6, '#F0F'); + hGrad.addColorStop(6 / 6, '#F00'); + + ctx.fillStyle = hGrad; + ctx.fillRect(0, 0, canvas.width, canvas.height); + + var vGrad = ctx.createLinearGradient(0, 0, 0, canvas.height); + switch (type.toLowerCase()) { + case 's': + vGrad.addColorStop(0, 'rgba(255,255,255,0)'); + vGrad.addColorStop(1, 'rgba(255,255,255,1)'); + break; + case 'v': + vGrad.addColorStop(0, 'rgba(0,0,0,0)'); + vGrad.addColorStop(1, 'rgba(0,0,0,1)'); + break; + } + ctx.fillStyle = vGrad; + ctx.fillRect(0, 0, canvas.width, canvas.height); + }; + + paletteObj.elm = canvas; + paletteObj.draw = drawFunc; + + } else { + // VML fallback for IE 7 and 8 + + jsc.initVML(); + + var vmlContainer = document.createElement('div'); + vmlContainer.style.position = 'relative'; + vmlContainer.style.overflow = 'hidden'; + + var hGrad = document.createElement(jsc._vmlNS + ':fill'); + hGrad.type = 'gradient'; + hGrad.method = 'linear'; + hGrad.angle = '90'; + hGrad.colors = '16.67% #F0F, 33.33% #00F, 50% #0FF, 66.67% #0F0, 83.33% #FF0' + + var hRect = document.createElement(jsc._vmlNS + ':rect'); + hRect.style.position = 'absolute'; + hRect.style.left = -1 + 'px'; + hRect.style.top = -1 + 'px'; + hRect.stroked = false; + hRect.appendChild(hGrad); + vmlContainer.appendChild(hRect); + + var vGrad = document.createElement(jsc._vmlNS + ':fill'); + vGrad.type = 'gradient'; + vGrad.method = 'linear'; + vGrad.angle = '180'; + vGrad.opacity = '0'; + + var vRect = document.createElement(jsc._vmlNS + ':rect'); + vRect.style.position = 'absolute'; + vRect.style.left = -1 + 'px'; + vRect.style.top = -1 + 'px'; + vRect.stroked = false; + vRect.appendChild(vGrad); + vmlContainer.appendChild(vRect); + + var drawFunc = function (width, height, type) { + vmlContainer.style.width = width + 'px'; + vmlContainer.style.height = height + 'px'; + + hRect.style.width = + vRect.style.width = + (width + 1) + 'px'; + hRect.style.height = + vRect.style.height = + (height + 1) + 'px'; + + // Colors must be specified during every redraw, otherwise IE won't display + // a full gradient during a subsequential redraw + hGrad.color = '#F00'; + hGrad.color2 = '#F00'; + + switch (type.toLowerCase()) { + case 's': + vGrad.color = vGrad.color2 = '#FFF'; + break; + case 'v': + vGrad.color = vGrad.color2 = '#000'; + break; + } + }; + + paletteObj.elm = vmlContainer; + paletteObj.draw = drawFunc; + } + + return paletteObj; + }, + + + createSliderGradient : function () { + + var sliderObj = { + elm: null, + draw: null + }; + + if (jsc.isCanvasSupported) { + // Canvas implementation for modern browsers + + var canvas = document.createElement('canvas'); + var ctx = canvas.getContext('2d'); + + var drawFunc = function (width, height, color1, color2) { + canvas.width = width; + canvas.height = height; + + ctx.clearRect(0, 0, canvas.width, canvas.height); + + var grad = ctx.createLinearGradient(0, 0, 0, canvas.height); + grad.addColorStop(0, color1); + grad.addColorStop(1, color2); + + ctx.fillStyle = grad; + ctx.fillRect(0, 0, canvas.width, canvas.height); + }; + + sliderObj.elm = canvas; + sliderObj.draw = drawFunc; + + } else { + // VML fallback for IE 7 and 8 + + jsc.initVML(); + + var vmlContainer = document.createElement('div'); + vmlContainer.style.position = 'relative'; + vmlContainer.style.overflow = 'hidden'; + + var grad = document.createElement(jsc._vmlNS + ':fill'); + grad.type = 'gradient'; + grad.method = 'linear'; + grad.angle = '180'; + + var rect = document.createElement(jsc._vmlNS + ':rect'); + rect.style.position = 'absolute'; + rect.style.left = -1 + 'px'; + rect.style.top = -1 + 'px'; + rect.stroked = false; + rect.appendChild(grad); + vmlContainer.appendChild(rect); + + var drawFunc = function (width, height, color1, color2) { + vmlContainer.style.width = width + 'px'; + vmlContainer.style.height = height + 'px'; + + rect.style.width = (width + 1) + 'px'; + rect.style.height = (height + 1) + 'px'; + + grad.color = color1; + grad.color2 = color2; + }; + + sliderObj.elm = vmlContainer; + sliderObj.draw = drawFunc; + } + + return sliderObj; + }, + + + leaveValue : 1<<0, + leaveStyle : 1<<1, + leavePad : 1<<2, + leaveSld : 1<<3, + + + BoxShadow : (function () { + var BoxShadow = function (hShadow, vShadow, blur, spread, color, inset) { + this.hShadow = hShadow; + this.vShadow = vShadow; + this.blur = blur; + this.spread = spread; + this.color = color; + this.inset = !!inset; + }; + + BoxShadow.prototype.toString = function () { + var vals = [ + Math.round(this.hShadow) + 'px', + Math.round(this.vShadow) + 'px', + Math.round(this.blur) + 'px', + Math.round(this.spread) + 'px', + this.color + ]; + if (this.inset) { + vals.push('inset'); + } + return vals.join(' '); + }; + + return BoxShadow; + })(), + + + // + // Usage: + // var myColor = new jscolor( [, ]) + // + + jscolor : function (targetElement, options) { + + // General options + // + this.value = null; // initial HEX color. To change it later, use methods fromString(), fromHSV() and fromRGB() + this.valueElement = targetElement; // element that will be used to display and input the color code + this.styleElement = targetElement; // element that will preview the picked color using CSS backgroundColor + this.required = true; // whether the associated text can be left empty + this.refine = true; // whether to refine the entered color code (e.g. uppercase it and remove whitespace) + this.hash = false; // whether to prefix the HEX color code with # symbol + this.uppercase = true; // whether to show the color code in upper case + this.onFineChange = null; // called instantly every time the color changes (value can be either a function or a string with javascript code) + this.activeClass = 'jscolor-active'; // class to be set to the target element when a picker window is open on it + this.overwriteImportant = false; // whether to overwrite colors of styleElement using !important + this.minS = 0; // min allowed saturation (0 - 100) + this.maxS = 100; // max allowed saturation (0 - 100) + this.minV = 0; // min allowed value (brightness) (0 - 100) + this.maxV = 100; // max allowed value (brightness) (0 - 100) + + // Accessing the picked color + // + this.hsv = [0, 0, 100]; // read-only [0-360, 0-100, 0-100] + this.rgb = [255, 255, 255]; // read-only [0-255, 0-255, 0-255] + + // Color Picker options + // + this.width = 181; // width of color palette (in px) + this.height = 101; // height of color palette (in px) + this.showOnClick = true; // whether to display the color picker when user clicks on its target element + this.mode = 'HSV'; // HSV | HVS | HS | HV - layout of the color picker controls + this.position = 'bottom'; // left | right | top | bottom - position relative to the target element + this.smartPosition = true; // automatically change picker position when there is not enough space for it + this.sliderSize = 16; // px + this.crossSize = 8; // px + this.closable = false; // whether to display the Close button + this.closeText = 'Close'; + this.buttonColor = '#000000'; // CSS color + this.buttonHeight = 18; // px + this.padding = 12; // px + this.backgroundColor = '#FFFFFF'; // CSS color + this.borderWidth = 1; // px + this.borderColor = '#BBBBBB'; // CSS color + this.borderRadius = 8; // px + this.insetWidth = 1; // px + this.insetColor = '#BBBBBB'; // CSS color + this.shadow = true; // whether to display shadow + this.shadowBlur = 15; // px + this.shadowColor = 'rgba(0,0,0,0.2)'; // CSS color + this.pointerColor = '#4C4C4C'; // px + this.pointerBorderColor = '#FFFFFF'; // px + this.pointerBorderWidth = 1; // px + this.pointerThickness = 2; // px + this.zIndex = 1000; + this.container = null; // where to append the color picker (BODY element by default) + + + for (var opt in options) { + if (options.hasOwnProperty(opt)) { + this[opt] = options[opt]; + } + } + + + this.hide = function () { + if (isPickerOwner()) { + detachPicker(); + } + }; + + + this.show = function () { + drawPicker(); + }; + + + this.redraw = function () { + if (isPickerOwner()) { + drawPicker(); + } + }; + + + this.importColor = function () { + if (!this.valueElement) { + this.exportColor(); + } else { + if (jsc.isElementType(this.valueElement, 'input')) { + if (!this.refine) { + if (!this.fromString(this.valueElement.value, jsc.leaveValue)) { + if (this.styleElement) { + this.styleElement.style.backgroundImage = this.styleElement._jscOrigStyle.backgroundImage; + this.styleElement.style.backgroundColor = this.styleElement._jscOrigStyle.backgroundColor; + this.styleElement.style.color = this.styleElement._jscOrigStyle.color; + } + this.exportColor(jsc.leaveValue | jsc.leaveStyle); + } + } else if (!this.required && /^\s*$/.test(this.valueElement.value)) { + this.valueElement.value = ''; + if (this.styleElement) { + this.styleElement.style.backgroundImage = this.styleElement._jscOrigStyle.backgroundImage; + this.styleElement.style.backgroundColor = this.styleElement._jscOrigStyle.backgroundColor; + this.styleElement.style.color = this.styleElement._jscOrigStyle.color; + } + this.exportColor(jsc.leaveValue | jsc.leaveStyle); + + } else if (this.fromString(this.valueElement.value)) { + // managed to import color successfully from the value -> OK, don't do anything + } else { + this.exportColor(); + } + } else { + // not an input element -> doesn't have any value + this.exportColor(); + } + } + }; + + + this.exportColor = function (flags) { + if (!(flags & jsc.leaveValue) && this.valueElement) { + var value = this.toString(); + if (this.uppercase) { value = value.toUpperCase(); } + if (this.hash) { value = '#' + value; } + + // if (jsc.isElementType(this.valueElement, 'input')) { + // this.valueElement.value = value; + // } else { + // this.valueElement.innerHTML = value; + // } + } + if (!(flags & jsc.leaveStyle)) { + if (this.styleElement) { + var bgColor = '#' + this.toString(); + var fgColor = this.isLight() ? '#000' : '#FFF'; + + this.styleElement.style.backgroundImage = 'none'; + this.styleElement.style.backgroundColor = bgColor; + this.styleElement.style.color = fgColor; + + if (this.overwriteImportant) { + this.styleElement.setAttribute('style', + 'background: ' + bgColor + ' !important; ' + + 'color: ' + fgColor + ' !important;' + ); + } + } + } + if (!(flags & jsc.leavePad) && isPickerOwner()) { + redrawPad(); + } + if (!(flags & jsc.leaveSld) && isPickerOwner()) { + redrawSld(); + } + }; + + + // h: 0-360 + // s: 0-100 + // v: 0-100 + // + this.fromHSV = function (h, s, v, flags) { // null = don't change + if (h !== null) { + if (isNaN(h)) { return false; } + h = Math.max(0, Math.min(360, h)); + } + if (s !== null) { + if (isNaN(s)) { return false; } + s = Math.max(0, Math.min(100, this.maxS, s), this.minS); + } + if (v !== null) { + if (isNaN(v)) { return false; } + v = Math.max(0, Math.min(100, this.maxV, v), this.minV); + } + + this.rgb = HSV_RGB( + h===null ? this.hsv[0] : (this.hsv[0]=h), + s===null ? this.hsv[1] : (this.hsv[1]=s), + v===null ? this.hsv[2] : (this.hsv[2]=v) + ); + + this.exportColor(flags); + }; + + + // r: 0-255 + // g: 0-255 + // b: 0-255 + // + this.fromRGB = function (r, g, b, flags) { // null = don't change + if (r !== null) { + if (isNaN(r)) { return false; } + r = Math.max(0, Math.min(255, r)); + } + if (g !== null) { + if (isNaN(g)) { return false; } + g = Math.max(0, Math.min(255, g)); + } + if (b !== null) { + if (isNaN(b)) { return false; } + b = Math.max(0, Math.min(255, b)); + } + + var hsv = RGB_HSV( + r===null ? this.rgb[0] : r, + g===null ? this.rgb[1] : g, + b===null ? this.rgb[2] : b + ); + if (hsv[0] !== null) { + this.hsv[0] = Math.max(0, Math.min(360, hsv[0])); + } + if (hsv[2] !== 0) { + this.hsv[1] = hsv[1]===null ? null : Math.max(0, this.minS, Math.min(100, this.maxS, hsv[1])); + } + this.hsv[2] = hsv[2]===null ? null : Math.max(0, this.minV, Math.min(100, this.maxV, hsv[2])); + + // update RGB according to final HSV, as some values might be trimmed + var rgb = HSV_RGB(this.hsv[0], this.hsv[1], this.hsv[2]); + this.rgb[0] = rgb[0]; + this.rgb[1] = rgb[1]; + this.rgb[2] = rgb[2]; + + this.exportColor(flags); + }; + + + this.fromString = function (str, flags) { + var m; + if (m = str.match(/^\W*([0-9A-F]{3}([0-9A-F]{3})?)\W*$/i)) { + // HEX notation + // + + if (m[1].length === 6) { + // 6-char notation + this.fromRGB( + parseInt(m[1].substr(0,2),16), + parseInt(m[1].substr(2,2),16), + parseInt(m[1].substr(4,2),16), + flags + ); + } else { + // 3-char notation + this.fromRGB( + parseInt(m[1].charAt(0) + m[1].charAt(0),16), + parseInt(m[1].charAt(1) + m[1].charAt(1),16), + parseInt(m[1].charAt(2) + m[1].charAt(2),16), + flags + ); + } + return true; + + } else if (m = str.match(/^\W*rgba?\(([^)]*)\)\W*$/i)) { + var params = m[1].split(','); + var re = /^\s*(\d*)(\.\d+)?\s*$/; + var mR, mG, mB; + if ( + params.length >= 3 && + (mR = params[0].match(re)) && + (mG = params[1].match(re)) && + (mB = params[2].match(re)) + ) { + var r = parseFloat((mR[1] || '0') + (mR[2] || '')); + var g = parseFloat((mG[1] || '0') + (mG[2] || '')); + var b = parseFloat((mB[1] || '0') + (mB[2] || '')); + this.fromRGB(r, g, b, flags); + return true; + } + } + return false; + }; + + + this.toString = function () { + return ( + (0x100 | Math.round(this.rgb[0])).toString(16).substr(1) + + (0x100 | Math.round(this.rgb[1])).toString(16).substr(1) + + (0x100 | Math.round(this.rgb[2])).toString(16).substr(1) + ); + }; + + + this.toHEXString = function () { + return '#' + this.toString().toUpperCase(); + }; + + + this.toRGBString = function () { + return ('rgb(' + + Math.round(this.rgb[0]) + ',' + + Math.round(this.rgb[1]) + ',' + + Math.round(this.rgb[2]) + ')' + ); + }; + + + this.isLight = function () { + return ( + 0.213 * this.rgb[0] + + 0.715 * this.rgb[1] + + 0.072 * this.rgb[2] > + 255 / 2 + ); + }; + + + this._processParentElementsInDOM = function () { + if (this._linkedElementsProcessed) { return; } + this._linkedElementsProcessed = true; + + var elm = this.targetElement; + do { + // If the target element or one of its parent nodes has fixed position, + // then use fixed positioning instead + // + // Note: In Firefox, getComputedStyle returns null in a hidden iframe, + // that's why we need to check if the returned style object is non-empty + var currStyle = jsc.getStyle(elm); + if (currStyle && currStyle.position.toLowerCase() === 'fixed') { + this.fixed = true; + } + + if (elm !== this.targetElement) { + // Ensure to attach onParentScroll only once to each parent element + // (multiple targetElements can share the same parent nodes) + // + // Note: It's not just offsetParents that can be scrollable, + // that's why we loop through all parent nodes + if (!elm._jscEventsAttached) { + jsc.attachEvent(elm, 'scroll', jsc.onParentScroll); + elm._jscEventsAttached = true; + } + } + } while ((elm = elm.parentNode) && !jsc.isElementType(elm, 'body')); + }; + + + // r: 0-255 + // g: 0-255 + // b: 0-255 + // + // returns: [ 0-360, 0-100, 0-100 ] + // + function RGB_HSV (r, g, b) { + r /= 255; + g /= 255; + b /= 255; + var n = Math.min(Math.min(r,g),b); + var v = Math.max(Math.max(r,g),b); + var m = v - n; + if (m === 0) { return [ null, 0, 100 * v ]; } + var h = r===n ? 3+(b-g)/m : (g===n ? 5+(r-b)/m : 1+(g-r)/m); + return [ + 60 * (h===6?0:h), + 100 * (m/v), + 100 * v + ]; + } + + + // h: 0-360 + // s: 0-100 + // v: 0-100 + // + // returns: [ 0-255, 0-255, 0-255 ] + // + function HSV_RGB (h, s, v) { + var u = 255 * (v / 100); + + if (h === null) { + return [ u, u, u ]; + } + + h /= 60; + s /= 100; + + var i = Math.floor(h); + var f = i%2 ? h-i : 1-(h-i); + var m = u * (1 - s); + var n = u * (1 - s * f); + switch (i) { + case 6: + case 0: return [u,n,m]; + case 1: return [n,u,m]; + case 2: return [m,u,n]; + case 3: return [m,n,u]; + case 4: return [n,m,u]; + case 5: return [u,m,n]; + } + } + + + function detachPicker () { + jsc.unsetClass(THIS.targetElement, THIS.activeClass); + jsc.picker.wrap.parentNode.removeChild(jsc.picker.wrap); + delete jsc.picker.owner; + } + + + function drawPicker () { + + // At this point, when drawing the picker, we know what the parent elements are + // and we can do all related DOM operations, such as registering events on them + // or checking their positioning + THIS._processParentElementsInDOM(); + + if (!jsc.picker) { + jsc.picker = { + owner: null, + wrap : document.createElement('div'), + box : document.createElement('div'), + boxS : document.createElement('div'), // shadow area + boxB : document.createElement('div'), // border + pad : document.createElement('div'), + padB : document.createElement('div'), // border + padM : document.createElement('div'), // mouse/touch area + padPal : jsc.createPalette(), + cross : document.createElement('div'), + crossBY : document.createElement('div'), // border Y + crossBX : document.createElement('div'), // border X + crossLY : document.createElement('div'), // line Y + crossLX : document.createElement('div'), // line X + sld : document.createElement('div'), + sldB : document.createElement('div'), // border + sldM : document.createElement('div'), // mouse/touch area + sldGrad : jsc.createSliderGradient(), + sldPtrS : document.createElement('div'), // slider pointer spacer + sldPtrIB : document.createElement('div'), // slider pointer inner border + sldPtrMB : document.createElement('div'), // slider pointer middle border + sldPtrOB : document.createElement('div'), // slider pointer outer border + btn : document.createElement('div'), + btnT : document.createElement('span') // text + }; + + jsc.picker.pad.appendChild(jsc.picker.padPal.elm); + jsc.picker.padB.appendChild(jsc.picker.pad); + jsc.picker.cross.appendChild(jsc.picker.crossBY); + jsc.picker.cross.appendChild(jsc.picker.crossBX); + jsc.picker.cross.appendChild(jsc.picker.crossLY); + jsc.picker.cross.appendChild(jsc.picker.crossLX); + jsc.picker.padB.appendChild(jsc.picker.cross); + jsc.picker.box.appendChild(jsc.picker.padB); + jsc.picker.box.appendChild(jsc.picker.padM); + + jsc.picker.sld.appendChild(jsc.picker.sldGrad.elm); + jsc.picker.sldB.appendChild(jsc.picker.sld); + jsc.picker.sldB.appendChild(jsc.picker.sldPtrOB); + jsc.picker.sldPtrOB.appendChild(jsc.picker.sldPtrMB); + jsc.picker.sldPtrMB.appendChild(jsc.picker.sldPtrIB); + jsc.picker.sldPtrIB.appendChild(jsc.picker.sldPtrS); + jsc.picker.box.appendChild(jsc.picker.sldB); + jsc.picker.box.appendChild(jsc.picker.sldM); + + jsc.picker.btn.appendChild(jsc.picker.btnT); + jsc.picker.box.appendChild(jsc.picker.btn); + + jsc.picker.boxB.appendChild(jsc.picker.box); + jsc.picker.wrap.appendChild(jsc.picker.boxS); + jsc.picker.wrap.appendChild(jsc.picker.boxB); + } + + var p = jsc.picker; + + var displaySlider = !!jsc.getSliderComponent(THIS); + var dims = jsc.getPickerDims(THIS); + var crossOuterSize = (2 * THIS.pointerBorderWidth + THIS.pointerThickness + 2 * THIS.crossSize); + var padToSliderPadding = jsc.getPadToSliderPadding(THIS); + var borderRadius = Math.min( + THIS.borderRadius, + Math.round(THIS.padding * Math.PI)); // px + var padCursor = 'crosshair'; + + // wrap + p.wrap.style.clear = 'both'; + p.wrap.style.width = (dims[0] + 2 * THIS.borderWidth) + 'px'; + p.wrap.style.height = (dims[1] + 2 * THIS.borderWidth) + 'px'; + p.wrap.style.zIndex = THIS.zIndex; + + // picker + p.box.style.width = dims[0] + 'px'; + p.box.style.height = dims[1] + 'px'; + + p.boxS.style.position = 'absolute'; + p.boxS.style.left = '0'; + p.boxS.style.top = '0'; + p.boxS.style.width = '100%'; + p.boxS.style.height = '100%'; + jsc.setBorderRadius(p.boxS, borderRadius + 'px'); + + // picker border + p.boxB.style.position = 'relative'; + p.boxB.style.border = THIS.borderWidth + 'px solid'; + p.boxB.style.borderColor = THIS.borderColor; + p.boxB.style.background = THIS.backgroundColor; + jsc.setBorderRadius(p.boxB, borderRadius + 'px'); + + // IE hack: + // If the element is transparent, IE will trigger the event on the elements under it, + // e.g. on Canvas or on elements with border + p.padM.style.background = + p.sldM.style.background = + '#FFF'; + jsc.setStyle(p.padM, 'opacity', '0'); + jsc.setStyle(p.sldM, 'opacity', '0'); + + // pad + p.pad.style.position = 'relative'; + p.pad.style.width = THIS.width + 'px'; + p.pad.style.height = THIS.height + 'px'; + + // pad palettes (HSV and HVS) + p.padPal.draw(THIS.width, THIS.height, jsc.getPadYComponent(THIS)); + + // pad border + p.padB.style.position = 'absolute'; + p.padB.style.left = THIS.padding + 'px'; + p.padB.style.top = THIS.padding + 'px'; + p.padB.style.border = THIS.insetWidth + 'px solid'; + p.padB.style.borderColor = THIS.insetColor; + + // pad mouse area + p.padM._jscInstance = THIS; + p.padM._jscControlName = 'pad'; + p.padM.style.position = 'absolute'; + p.padM.style.left = '0'; + p.padM.style.top = '0'; + p.padM.style.width = (THIS.padding + 2 * THIS.insetWidth + THIS.width + padToSliderPadding / 2) + 'px'; + p.padM.style.height = dims[1] + 'px'; + p.padM.style.cursor = padCursor; + + // pad cross + p.cross.style.position = 'absolute'; + p.cross.style.left = + p.cross.style.top = + '0'; + p.cross.style.width = + p.cross.style.height = + crossOuterSize + 'px'; + + // pad cross border Y and X + p.crossBY.style.position = + p.crossBX.style.position = + 'absolute'; + p.crossBY.style.background = + p.crossBX.style.background = + THIS.pointerBorderColor; + p.crossBY.style.width = + p.crossBX.style.height = + (2 * THIS.pointerBorderWidth + THIS.pointerThickness) + 'px'; + p.crossBY.style.height = + p.crossBX.style.width = + crossOuterSize + 'px'; + p.crossBY.style.left = + p.crossBX.style.top = + (Math.floor(crossOuterSize / 2) - Math.floor(THIS.pointerThickness / 2) - THIS.pointerBorderWidth) + 'px'; + p.crossBY.style.top = + p.crossBX.style.left = + '0'; + + // pad cross line Y and X + p.crossLY.style.position = + p.crossLX.style.position = + 'absolute'; + p.crossLY.style.background = + p.crossLX.style.background = + THIS.pointerColor; + p.crossLY.style.height = + p.crossLX.style.width = + (crossOuterSize - 2 * THIS.pointerBorderWidth) + 'px'; + p.crossLY.style.width = + p.crossLX.style.height = + THIS.pointerThickness + 'px'; + p.crossLY.style.left = + p.crossLX.style.top = + (Math.floor(crossOuterSize / 2) - Math.floor(THIS.pointerThickness / 2)) + 'px'; + p.crossLY.style.top = + p.crossLX.style.left = + THIS.pointerBorderWidth + 'px'; + + // slider + p.sld.style.overflow = 'hidden'; + p.sld.style.width = THIS.sliderSize + 'px'; + p.sld.style.height = THIS.height + 'px'; + + // slider gradient + p.sldGrad.draw(THIS.sliderSize, THIS.height, '#000', '#000'); + + // slider border + p.sldB.style.display = displaySlider ? 'block' : 'none'; + p.sldB.style.position = 'absolute'; + p.sldB.style.right = THIS.padding + 'px'; + p.sldB.style.top = THIS.padding + 'px'; + p.sldB.style.border = THIS.insetWidth + 'px solid'; + p.sldB.style.borderColor = THIS.insetColor; + + // slider mouse area + p.sldM._jscInstance = THIS; + p.sldM._jscControlName = 'sld'; + p.sldM.style.display = displaySlider ? 'block' : 'none'; + p.sldM.style.position = 'absolute'; + p.sldM.style.right = '0'; + p.sldM.style.top = '0'; + p.sldM.style.width = (THIS.sliderSize + padToSliderPadding / 2 + THIS.padding + 2 * THIS.insetWidth) + 'px'; + p.sldM.style.height = dims[1] + 'px'; + p.sldM.style.cursor = 'default'; + + // slider pointer inner and outer border + p.sldPtrIB.style.border = + p.sldPtrOB.style.border = + THIS.pointerBorderWidth + 'px solid ' + THIS.pointerBorderColor; + + // slider pointer outer border + p.sldPtrOB.style.position = 'absolute'; + p.sldPtrOB.style.left = -(2 * THIS.pointerBorderWidth + THIS.pointerThickness) + 'px'; + p.sldPtrOB.style.top = '0'; + + // slider pointer middle border + p.sldPtrMB.style.border = THIS.pointerThickness + 'px solid ' + THIS.pointerColor; + + // slider pointer spacer + p.sldPtrS.style.width = THIS.sliderSize + 'px'; + p.sldPtrS.style.height = sliderPtrSpace + 'px'; + + // the Close button + function setBtnBorder () { + var insetColors = THIS.insetColor.split(/\s+/); + var outsetColor = insetColors.length < 2 ? insetColors[0] : insetColors[1] + ' ' + insetColors[0] + ' ' + insetColors[0] + ' ' + insetColors[1]; + p.btn.style.borderColor = outsetColor; + } + p.btn.style.display = THIS.closable ? 'block' : 'none'; + p.btn.style.position = 'absolute'; + p.btn.style.left = THIS.padding + 'px'; + p.btn.style.bottom = THIS.padding + 'px'; + p.btn.style.padding = '0 15px'; + p.btn.style.height = THIS.buttonHeight + 'px'; + p.btn.style.border = THIS.insetWidth + 'px solid'; + setBtnBorder(); + p.btn.style.color = THIS.buttonColor; + p.btn.style.font = '12px sans-serif'; + p.btn.style.textAlign = 'center'; + try { + p.btn.style.cursor = 'pointer'; + } catch(eOldIE) { + p.btn.style.cursor = 'hand'; + } + p.btn.onmousedown = function () { + THIS.hide(); + }; + p.btnT.style.lineHeight = THIS.buttonHeight + 'px'; + p.btnT.innerHTML = ''; + p.btnT.appendChild(document.createTextNode(THIS.closeText)); + + // place pointers + redrawPad(); + redrawSld(); + + // If we are changing the owner without first closing the picker, + // make sure to first deal with the old owner + if (jsc.picker.owner && jsc.picker.owner !== THIS) { + jsc.unsetClass(jsc.picker.owner.targetElement, THIS.activeClass); + } + + // Set the new picker owner + jsc.picker.owner = THIS; + + // The redrawPosition() method needs picker.owner to be set, that's why we call it here, + // after setting the owner + if (jsc.isElementType(container, 'body')) { + jsc.redrawPosition(); + } else { + jsc._drawPosition(THIS, 0, 0, 'relative', false); + } + + if (p.wrap.parentNode != container) { + container.appendChild(p.wrap); + } + + jsc.setClass(THIS.targetElement, THIS.activeClass); + } + + + function redrawPad () { + // redraw the pad pointer + switch (jsc.getPadYComponent(THIS)) { + case 's': var yComponent = 1; break; + case 'v': var yComponent = 2; break; + } + var x = Math.round((THIS.hsv[0] / 360) * (THIS.width - 1)); + var y = Math.round((1 - THIS.hsv[yComponent] / 100) * (THIS.height - 1)); + var crossOuterSize = (2 * THIS.pointerBorderWidth + THIS.pointerThickness + 2 * THIS.crossSize); + var ofs = -Math.floor(crossOuterSize / 2); + jsc.picker.cross.style.left = (x + ofs) + 'px'; + jsc.picker.cross.style.top = (y + ofs) + 'px'; + + // redraw the slider + switch (jsc.getSliderComponent(THIS)) { + case 's': + var rgb1 = HSV_RGB(THIS.hsv[0], 100, THIS.hsv[2]); + var rgb2 = HSV_RGB(THIS.hsv[0], 0, THIS.hsv[2]); + var color1 = 'rgb(' + + Math.round(rgb1[0]) + ',' + + Math.round(rgb1[1]) + ',' + + Math.round(rgb1[2]) + ')'; + var color2 = 'rgb(' + + Math.round(rgb2[0]) + ',' + + Math.round(rgb2[1]) + ',' + + Math.round(rgb2[2]) + ')'; + jsc.picker.sldGrad.draw(THIS.sliderSize, THIS.height, color1, color2); + break; + case 'v': + var rgb = HSV_RGB(THIS.hsv[0], THIS.hsv[1], 100); + var color1 = 'rgb(' + + Math.round(rgb[0]) + ',' + + Math.round(rgb[1]) + ',' + + Math.round(rgb[2]) + ')'; + var color2 = '#000'; + jsc.picker.sldGrad.draw(THIS.sliderSize, THIS.height, color1, color2); + break; + } + } + + + function redrawSld () { + var sldComponent = jsc.getSliderComponent(THIS); + if (sldComponent) { + // redraw the slider pointer + switch (sldComponent) { + case 's': var yComponent = 1; break; + case 'v': var yComponent = 2; break; + } + var y = Math.round((1 - THIS.hsv[yComponent] / 100) * (THIS.height - 1)); + jsc.picker.sldPtrOB.style.top = (y - (2 * THIS.pointerBorderWidth + THIS.pointerThickness) - Math.floor(sliderPtrSpace / 2)) + 'px'; + } + } + + + function isPickerOwner () { + return jsc.picker && jsc.picker.owner === THIS; + } + + + function blurValue () { + THIS.importColor(); + } + + + // Find the target element + if (typeof targetElement === 'string') { + var id = targetElement; + var elm = document.getElementById(id); + if (elm) { + this.targetElement = elm; + } else { + jsc.warn('Could not find target element with ID \'' + id + '\''); + } + } else if (targetElement) { + this.targetElement = targetElement; + } else { + jsc.warn('Invalid target element: \'' + targetElement + '\''); + } + + if (this.targetElement._jscLinkedInstance) { + jsc.warn('Cannot link jscolor twice to the same element. Skipping.'); + return; + } + this.targetElement._jscLinkedInstance = this; + + // Find the value element + this.valueElement = jsc.fetchElement(this.valueElement); + // Find the style element + this.styleElement = jsc.fetchElement(this.styleElement); + + var THIS = this; + var container = + this.container ? + jsc.fetchElement(this.container) : + document.getElementsByTagName('body')[0]; + var sliderPtrSpace = 3; // px + + // For BUTTON elements it's important to stop them from sending the form when clicked + // (e.g. in Safari) + if (jsc.isElementType(this.targetElement, 'button')) { + if (this.targetElement.onclick) { + var origCallback = this.targetElement.onclick; + this.targetElement.onclick = function (evt) { + origCallback.call(this, evt); + return false; + }; + } else { + this.targetElement.onclick = function () { return false; }; + } + } + + /* + var elm = this.targetElement; + do { + // If the target element or one of its offsetParents has fixed position, + // then use fixed positioning instead + // + // Note: In Firefox, getComputedStyle returns null in a hidden iframe, + // that's why we need to check if the returned style object is non-empty + var currStyle = jsc.getStyle(elm); + if (currStyle && currStyle.position.toLowerCase() === 'fixed') { + this.fixed = true; + } + + if (elm !== this.targetElement) { + // attach onParentScroll so that we can recompute the picker position + // when one of the offsetParents is scrolled + if (!elm._jscEventsAttached) { + jsc.attachEvent(elm, 'scroll', jsc.onParentScroll); + elm._jscEventsAttached = true; + } + } + } while ((elm = elm.offsetParent) && !jsc.isElementType(elm, 'body')); + */ + + // valueElement + if (this.valueElement) { + if (jsc.isElementType(this.valueElement, 'input')) { + var updateField = function () { + THIS.fromString(THIS.valueElement.value, jsc.leaveValue); + jsc.dispatchFineChange(THIS); + }; + jsc.attachEvent(this.valueElement, 'keyup', updateField); + jsc.attachEvent(this.valueElement, 'input', updateField); + jsc.attachEvent(this.valueElement, 'blur', blurValue); + this.valueElement.setAttribute('autocomplete', 'off'); + } + } + + // styleElement + if (this.styleElement) { + this.styleElement._jscOrigStyle = { + backgroundImage : this.styleElement.style.backgroundImage, + backgroundColor : this.styleElement.style.backgroundColor, + color : this.styleElement.style.color + }; + } + + if (this.value) { + // Try to set the color from the .value option and if unsuccessful, + // export the current color + this.fromString(this.value) || this.exportColor(); + } else { + this.importColor(); + } + } + +}; + + +//================================ +// Public properties and methods +//================================ + + +// By default, search for all elements with class="jscolor" and install a color picker on them. +// +// You can change what class name will be looked for by setting the property jscolor.lookupClass +// anywhere in your HTML document. To completely disable the automatic lookup, set it to null. +// +jsc.jscolor.lookupClass = 'jscolor'; + + +jsc.jscolor.installByClassName = function (className) { + var inputElms = document.getElementsByTagName('input'); + var buttonElms = document.getElementsByTagName('button'); + + jsc.tryInstallOnElements(inputElms, className); + jsc.tryInstallOnElements(buttonElms, className); +}; + + +jsc.register(); + + +return jsc.jscolor; + + +})(); } diff --git a/scripts/system/appreciate/resources/sounds/claps/01.wav b/scripts/system/appreciate/resources/sounds/claps/01.wav new file mode 100644 index 0000000000..b4baa2888a Binary files /dev/null and b/scripts/system/appreciate/resources/sounds/claps/01.wav differ diff --git a/scripts/system/appreciate/resources/sounds/claps/02.wav b/scripts/system/appreciate/resources/sounds/claps/02.wav new file mode 100644 index 0000000000..21cbf3fa4a Binary files /dev/null and b/scripts/system/appreciate/resources/sounds/claps/02.wav differ diff --git a/scripts/system/appreciate/resources/sounds/claps/03.wav b/scripts/system/appreciate/resources/sounds/claps/03.wav new file mode 100644 index 0000000000..bc5d35760d Binary files /dev/null and b/scripts/system/appreciate/resources/sounds/claps/03.wav differ diff --git a/scripts/system/appreciate/resources/sounds/claps/04.wav b/scripts/system/appreciate/resources/sounds/claps/04.wav new file mode 100644 index 0000000000..cacaf44b6e Binary files /dev/null and b/scripts/system/appreciate/resources/sounds/claps/04.wav differ diff --git a/scripts/system/appreciate/resources/sounds/claps/05.wav b/scripts/system/appreciate/resources/sounds/claps/05.wav new file mode 100644 index 0000000000..ffb688b5b2 Binary files /dev/null and b/scripts/system/appreciate/resources/sounds/claps/05.wav differ diff --git a/scripts/system/appreciate/resources/sounds/claps/06.wav b/scripts/system/appreciate/resources/sounds/claps/06.wav new file mode 100644 index 0000000000..81716f26be Binary files /dev/null and b/scripts/system/appreciate/resources/sounds/claps/06.wav differ diff --git a/scripts/system/appreciate/resources/sounds/claps/07.wav b/scripts/system/appreciate/resources/sounds/claps/07.wav new file mode 100644 index 0000000000..4c20ceba2c Binary files /dev/null and b/scripts/system/appreciate/resources/sounds/claps/07.wav differ diff --git a/scripts/system/appreciate/resources/sounds/claps/08.wav b/scripts/system/appreciate/resources/sounds/claps/08.wav new file mode 100644 index 0000000000..c39e4b1dfc Binary files /dev/null and b/scripts/system/appreciate/resources/sounds/claps/08.wav differ diff --git a/scripts/system/appreciate/resources/sounds/claps/09.wav b/scripts/system/appreciate/resources/sounds/claps/09.wav new file mode 100644 index 0000000000..791433c024 Binary files /dev/null and b/scripts/system/appreciate/resources/sounds/claps/09.wav differ diff --git a/scripts/system/appreciate/resources/sounds/claps/10.wav b/scripts/system/appreciate/resources/sounds/claps/10.wav new file mode 100644 index 0000000000..b359475b51 Binary files /dev/null and b/scripts/system/appreciate/resources/sounds/claps/10.wav differ diff --git a/scripts/system/appreciate/resources/sounds/claps/11.wav b/scripts/system/appreciate/resources/sounds/claps/11.wav new file mode 100644 index 0000000000..40b8415725 Binary files /dev/null and b/scripts/system/appreciate/resources/sounds/claps/11.wav differ diff --git a/scripts/system/appreciate/resources/sounds/claps/12.wav b/scripts/system/appreciate/resources/sounds/claps/12.wav new file mode 100644 index 0000000000..68656f7c21 Binary files /dev/null and b/scripts/system/appreciate/resources/sounds/claps/12.wav differ diff --git a/scripts/system/appreciate/resources/sounds/claps/13.wav b/scripts/system/appreciate/resources/sounds/claps/13.wav new file mode 100644 index 0000000000..e6a716ccdd Binary files /dev/null and b/scripts/system/appreciate/resources/sounds/claps/13.wav differ diff --git a/scripts/system/appreciate/resources/sounds/claps/14.wav b/scripts/system/appreciate/resources/sounds/claps/14.wav new file mode 100644 index 0000000000..a5e8b5ad49 Binary files /dev/null and b/scripts/system/appreciate/resources/sounds/claps/14.wav differ diff --git a/scripts/system/appreciate/resources/sounds/claps/15.wav b/scripts/system/appreciate/resources/sounds/claps/15.wav new file mode 100644 index 0000000000..7f3072e3e0 Binary files /dev/null and b/scripts/system/appreciate/resources/sounds/claps/15.wav differ diff --git a/scripts/system/appreciate/resources/sounds/claps/16.wav b/scripts/system/appreciate/resources/sounds/claps/16.wav new file mode 100644 index 0000000000..f76bee0429 Binary files /dev/null and b/scripts/system/appreciate/resources/sounds/claps/16.wav differ diff --git a/scripts/system/appreciate/resources/sounds/whistles/01.wav b/scripts/system/appreciate/resources/sounds/whistles/01.wav new file mode 100644 index 0000000000..d8b27da990 Binary files /dev/null and b/scripts/system/appreciate/resources/sounds/whistles/01.wav differ diff --git a/scripts/system/appreciate/resources/sounds/whistles/02.wav b/scripts/system/appreciate/resources/sounds/whistles/02.wav new file mode 100644 index 0000000000..0c0874087e Binary files /dev/null and b/scripts/system/appreciate/resources/sounds/whistles/02.wav differ diff --git a/scripts/system/appreciate/resources/sounds/whistles/03.wav b/scripts/system/appreciate/resources/sounds/whistles/03.wav new file mode 100644 index 0000000000..5a78406601 Binary files /dev/null and b/scripts/system/appreciate/resources/sounds/whistles/03.wav differ diff --git a/scripts/system/appreciate/resources/sounds/whistles/04.wav b/scripts/system/appreciate/resources/sounds/whistles/04.wav new file mode 100644 index 0000000000..eb4cb40675 Binary files /dev/null and b/scripts/system/appreciate/resources/sounds/whistles/04.wav differ diff --git a/scripts/system/appreciate/resources/sounds/whistles/05.wav b/scripts/system/appreciate/resources/sounds/whistles/05.wav new file mode 100644 index 0000000000..f261e29aac Binary files /dev/null and b/scripts/system/appreciate/resources/sounds/whistles/05.wav differ diff --git a/scripts/system/appreciate/resources/sounds/whistles/06.wav b/scripts/system/appreciate/resources/sounds/whistles/06.wav new file mode 100644 index 0000000000..7194c02d9b Binary files /dev/null and b/scripts/system/appreciate/resources/sounds/whistles/06.wav differ diff --git a/scripts/system/appreciate/resources/sounds/whistles/07.wav b/scripts/system/appreciate/resources/sounds/whistles/07.wav new file mode 100644 index 0000000000..43d65f3af7 Binary files /dev/null and b/scripts/system/appreciate/resources/sounds/whistles/07.wav differ diff --git a/scripts/system/appreciate/resources/sounds/whistles/08.wav b/scripts/system/appreciate/resources/sounds/whistles/08.wav new file mode 100644 index 0000000000..612bc5b5fa Binary files /dev/null and b/scripts/system/appreciate/resources/sounds/whistles/08.wav differ diff --git a/scripts/system/appreciate/resources/sounds/whistles/09.wav b/scripts/system/appreciate/resources/sounds/whistles/09.wav new file mode 100644 index 0000000000..d900d59f03 Binary files /dev/null and b/scripts/system/appreciate/resources/sounds/whistles/09.wav differ diff --git a/scripts/system/appreciate/resources/sounds/whistles/10.wav b/scripts/system/appreciate/resources/sounds/whistles/10.wav new file mode 100644 index 0000000000..3c9b7fa9dc Binary files /dev/null and b/scripts/system/appreciate/resources/sounds/whistles/10.wav differ diff --git a/scripts/system/appreciate/resources/sounds/whistles/11.wav b/scripts/system/appreciate/resources/sounds/whistles/11.wav new file mode 100644 index 0000000000..8315b21cca Binary files /dev/null and b/scripts/system/appreciate/resources/sounds/whistles/11.wav differ diff --git a/scripts/system/appreciate/resources/sounds/whistles/12.wav b/scripts/system/appreciate/resources/sounds/whistles/12.wav new file mode 100644 index 0000000000..c787336068 Binary files /dev/null and b/scripts/system/appreciate/resources/sounds/whistles/12.wav differ diff --git a/scripts/system/appreciate/resources/sounds/whistles/13.wav b/scripts/system/appreciate/resources/sounds/whistles/13.wav new file mode 100644 index 0000000000..fd303d27fc Binary files /dev/null and b/scripts/system/appreciate/resources/sounds/whistles/13.wav differ diff --git a/scripts/system/appreciate/resources/sounds/whistles/14.wav b/scripts/system/appreciate/resources/sounds/whistles/14.wav new file mode 100644 index 0000000000..eda14dc291 Binary files /dev/null and b/scripts/system/appreciate/resources/sounds/whistles/14.wav differ diff --git a/scripts/system/appreciate/resources/sounds/whistles/15.wav b/scripts/system/appreciate/resources/sounds/whistles/15.wav new file mode 100644 index 0000000000..f86e9e2cb2 Binary files /dev/null and b/scripts/system/appreciate/resources/sounds/whistles/15.wav differ diff --git a/scripts/system/appreciate/resources/sounds/whistles/16.wav b/scripts/system/appreciate/resources/sounds/whistles/16.wav new file mode 100644 index 0000000000..22ff07a650 Binary files /dev/null and b/scripts/system/appreciate/resources/sounds/whistles/16.wav differ diff --git a/scripts/system/appreciate/resources/sounds/whistles/17.wav b/scripts/system/appreciate/resources/sounds/whistles/17.wav new file mode 100644 index 0000000000..498d6df56a Binary files /dev/null and b/scripts/system/appreciate/resources/sounds/whistles/17.wav differ diff --git a/scripts/system/controllers/controllerModules/inEditMode.js b/scripts/system/controllers/controllerModules/inEditMode.js index 5709b19efe..8453a7d8d3 100644 --- a/scripts/system/controllers/controllerModules/inEditMode.js +++ b/scripts/system/controllers/controllerModules/inEditMode.js @@ -2,6 +2,9 @@ // inEditMode.js // +// Copyright 2014 High Fidelity, Inc. +// Copyright 2020 Vircadia contributors. +// // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html @@ -80,7 +83,9 @@ Script.include("/~/system/libraries/utils.js"); Messages.sendLocalMessage(this.ENTITY_TOOL_UPDATES_CHANNEL, JSON.stringify({ method: "selectEntity", entityID: this.selectedTarget.objectID, - hand: hand + hand: hand, + surfaceNormal: this.selectedTarget.surfaceNormal, + intersection: this.selectedTarget.intersection })); } else if (this.selectedTarget.type === Picks.INTERSECTED_OVERLAY) { Messages.sendLocalMessage(this.ENTITY_TOOL_UPDATES_CHANNEL, JSON.stringify({ diff --git a/scripts/system/create/assets/data/createAppTooltips.json b/scripts/system/create/assets/data/createAppTooltips.json index f898d594c6..a3feabe965 100644 --- a/scripts/system/create/assets/data/createAppTooltips.json +++ b/scripts/system/create/assets/data/createAppTooltips.json @@ -601,6 +601,9 @@ "groupCulled": { "tooltip": "If false, individual pieces of the entity may be culled by the render engine. If true, either the entire entity will be culled, or it won't at all." }, + "useOriginalPivot": { + "tooltip": "If false, the model will be centered based on its content, ignoring any offset in the model itself. If true, the model will respect its original offset." + }, "webColor": { "tooltip": "The tint of the web entity." }, diff --git a/scripts/system/create/audioFeedback/audioFeedback.js b/scripts/system/create/audioFeedback/audioFeedback.js index f1900d5716..881afddfaf 100644 --- a/scripts/system/create/audioFeedback/audioFeedback.js +++ b/scripts/system/create/audioFeedback/audioFeedback.js @@ -12,9 +12,10 @@ audioFeedback = (function() { var that = {}; - + var confirmationSound = SoundCache.getSound(Script.resolvePath("./sounds/confirmation.mp3")); var rejectionSound = SoundCache.getSound(Script.resolvePath("./sounds/rejection.mp3")); + var actionSound = SoundCache.getSound(Script.resolvePath("./sounds/action.mp3")); that.confirmation = function() { //Play a confirmation sound var injector = Audio.playSound(confirmationSound, { @@ -25,8 +26,15 @@ audioFeedback = (function() { that.rejection = function() { //Play a rejection sound var injector = Audio.playSound(rejectionSound, { - "volume": 0.3, - "localOnly": true + "volume": 0.3, + "localOnly": true + }); + } + + that.action = function() { //Play an action sound + var injector = Audio.playSound(actionSound, { + "volume": 0.3, + "localOnly": true }); } diff --git a/scripts/system/create/audioFeedback/sounds/action.mp3 b/scripts/system/create/audioFeedback/sounds/action.mp3 new file mode 100644 index 0000000000..fd004847b0 Binary files /dev/null and b/scripts/system/create/audioFeedback/sounds/action.mp3 differ diff --git a/scripts/system/create/edit.js b/scripts/system/create/edit.js index 9e94b68ba1..9bd0147002 100644 --- a/scripts/system/create/edit.js +++ b/scripts/system/create/edit.js @@ -109,6 +109,8 @@ var entityIconOverlayManager = new EntityIconOverlayManager(["Light", "ParticleE }); var hmdMultiSelectMode = false; +var expectingRotateAsClickedSurface = false; +var keepSelectedOnNextClick = false; var cameraManager = new CameraManager(); @@ -739,6 +741,7 @@ var toolBar = (function () { grabbable: result.grabbable }, dynamic: dynamic, + useOriginalPivot: result.useOriginalPivot }); } } @@ -1106,25 +1109,50 @@ function findClickedEntity(event) { } var result; - - if (iconResult.intersects) { - result = iconResult; - } else if (entityResult.intersects) { - result = entityResult; + if (expectingRotateAsClickedSurface) { + if (!SelectionManager.hasSelection() || !SelectionManager.hasUnlockedSelection()) { + audioFeedback.rejection(); + Window.notifyEditError("You have nothing selected, or the selection is locked."); + expectingRotateAsClickedSurface = false; + } else { + //Rotate Selection according the Surface Normal + var normalRotation = Quat.lookAtSimple(Vec3.ZERO, Vec3.multiply(entityResult.surfaceNormal, -1)); + selectionDisplay.rotateSelection(normalRotation); + //Translate Selection according the clicked Surface + var distanceFromSurface; + if (selectionDisplay.getSpaceMode() === "world"){ + distanceFromSurface = SelectionManager.worldDimensions.z / 2; + } else { + distanceFromSurface = SelectionManager.localDimensions.z / 2; + } + selectionDisplay.moveSelection(Vec3.sum(entityResult.intersection, Vec3.multiplyQbyV( normalRotation, {"x": 0.0, "y":0.0, "z": distanceFromSurface}))); + selectionManager._update(false, this); + pushCommandForSelections(); + expectingRotateAsClickedSurface = false; + audioFeedback.action(); + } + keepSelectedOnNextClick = true; + return null; } else { - return null; - } + if (iconResult.intersects) { + result = iconResult; + } else if (entityResult.intersects) { + result = entityResult; + } else { + return null; + } - if (!result.accurate) { - return null; - } + if (!result.accurate) { + return null; + } - var foundEntity = result.entityID; - return { - pickRay: pickRay, - entityID: foundEntity, - intersection: result.intersection - }; + var foundEntity = result.entityID; + return { + pickRay: pickRay, + entityID: foundEntity, + intersection: result.intersection + }; + } } // Handles selections on overlays while in edit mode by querying entities from @@ -1295,7 +1323,10 @@ function mouseClickEvent(event) { if (result === null || result === undefined) { if (!event.isShifted) { - selectionManager.clearSelections(this); + if (!keepSelectedOnNextClick) { + selectionManager.clearSelections(this); + } + keepSelectedOnNextClick = false; } return; } @@ -2052,6 +2083,26 @@ function gridToAvatarKey(value) { alignGridToAvatar(); } } +function rotateAsNextClickedSurfaceKey(value) { + if (value === 0) { // on release + rotateAsNextClickedSurface(); + } +} +function quickRotate90xKey(value) { + if (value === 0) { // on release + selectionDisplay.rotate90degreeSelection("X"); + } +} +function quickRotate90yKey(value) { + if (value === 0) { // on release + selectionDisplay.rotate90degreeSelection("Y"); + } +} +function quickRotate90zKey(value) { + if (value === 0) { // on release + selectionDisplay.rotate90degreeSelection("Z"); + } +} function recursiveAdd(newParentID, parentData) { if (parentData.children !== undefined) { var children = parentData.children; @@ -2819,6 +2870,10 @@ mapping.from([Controller.Hardware.Keyboard.J]).to(gridKey); mapping.from([Controller.Hardware.Keyboard.G]).to(viewGridKey); mapping.from([Controller.Hardware.Keyboard.H]).to(snapKey); mapping.from([Controller.Hardware.Keyboard.K]).to(gridToAvatarKey); +mapping.from([Controller.Hardware.Keyboard["0"]]).to(rotateAsNextClickedSurfaceKey); +mapping.from([Controller.Hardware.Keyboard["7"]]).to(quickRotate90xKey); +mapping.from([Controller.Hardware.Keyboard["8"]]).to(quickRotate90yKey); +mapping.from([Controller.Hardware.Keyboard["9"]]).to(quickRotate90zKey); mapping.from([Controller.Hardware.Keyboard.X]) .when([Controller.Hardware.Keyboard.Control]) .to(whenReleased(function() { selectionManager.cutSelectedEntities() })); @@ -2867,6 +2922,14 @@ keyUpEventFromUIWindow = function(keyUpEvent) { snapKey(pressedValue); } else if (keyUpEvent.keyCodeString === "K") { gridToAvatarKey(pressedValue); + } else if (keyUpEvent.keyCodeString === "0") { + rotateAsNextClickedSurfaceKey(pressedValue); + } else if (keyUpEvent.keyCodeString === "7") { + quickRotate90xKey(pressedValue); + } else if (keyUpEvent.keyCodeString === "8") { + quickRotate90yKey(pressedValue); + } else if (keyUpEvent.keyCodeString === "9") { + quickRotate90zKey(pressedValue); } else if (keyUpEvent.controlKey && keyUpEvent.keyCodeString === "X") { selectionManager.cutSelectedEntities(); } else if (keyUpEvent.controlKey && keyUpEvent.keyCodeString === "C") { @@ -3015,4 +3078,14 @@ function toggleGridVisibility() { } } +function rotateAsNextClickedSurface() { + if (!SelectionManager.hasSelection() || !SelectionManager.hasUnlockedSelection()) { + audioFeedback.rejection(); + Window.notifyEditError("You have nothing selected, or the selection is locked."); + expectingRotateAsClickedSurface = false; + } else { + expectingRotateAsClickedSurface = true; + } +} + }()); // END LOCAL_SCOPE diff --git a/scripts/system/create/entityList/entityList.js b/scripts/system/create/entityList/entityList.js index a4d4decedb..5119d7d3da 100644 --- a/scripts/system/create/entityList/entityList.js +++ b/scripts/system/create/entityList/entityList.js @@ -383,6 +383,14 @@ EntityListTool = function(shouldUseEditTabletApp) { SelectionManager.selectTopFamily(); } else if (data.type === 'teleportToEntity') { SelectionManager.teleportToEntity(); + } else if (data.type === 'rotateAsTheNextClickedSurface') { + rotateAsNextClickedSurface(); + } else if (data.type === 'quickRotate90x') { + selectionDisplay.rotate90degreeSelection("X"); + } else if (data.type === 'quickRotate90y') { + selectionDisplay.rotate90degreeSelection("Y"); + } else if (data.type === 'quickRotate90z') { + selectionDisplay.rotate90degreeSelection("Z"); } else if (data.type === 'moveEntitySelectionToAvatar') { SelectionManager.moveEntitiesSelectionToAvatar(); } else if (data.type === 'loadConfigSetting') { diff --git a/scripts/system/create/entityList/html/entityList.html b/scripts/system/create/entityList/html/entityList.html index 9817f9ddf9..93585c7338 100644 --- a/scripts/system/create/entityList/html/entityList.html +++ b/scripts/system/create/entityList/html/entityList.html @@ -24,6 +24,11 @@ +
+ + + +
@@ -32,9 +37,7 @@
- - - +
@@ -58,8 +61,7 @@
- - + D
@@ -130,12 +132,6 @@
- + + + + +