diff --git a/.eslintrc.js b/.eslintrc.js index 67921be395..9900825b23 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -36,6 +36,7 @@ module.exports = { "GlobalServices": false, "GooglePoly": false, "Graphics": false, + "HifiAbout": false, "HMD": false, "LaserPointers": false, "location": true, diff --git a/BUILD.md b/BUILD.md index 4a0274cea6..00b17743e9 100644 --- a/BUILD.md +++ b/BUILD.md @@ -9,14 +9,12 @@ - [cmake](https://cmake.org/download/): 3.9 - [Qt](https://www.qt.io/download-open-source): 5.10.1 -- [OpenSSL](https://www.openssl.org/): Use the latest available 1.0 version (**NOT** 1.1) of OpenSSL to avoid security vulnerabilities. -- [VHACD](https://github.com/virneo/v-hacd)(clone this repository)(Optional) +- [Python](https://www.python.org/downloads/): 3.6 or higher ### CMake External Project Dependencies These dependencies need not be installed manually. They are automatically downloaded on the platforms where they are required. - [Bullet Physics Engine](https://github.com/bulletphysics/bullet3/releases): 2.83 -- [GLEW](http://glew.sourceforge.net/): 1.13 - [glm](https://glm.g-truc.net/0.9.8/index.html): 0.9.8 - [Oculus SDK](https://developer.oculus.com/downloads/): 1.11 (Win32) / 0.5 (Mac) - [OpenVR](https://github.com/ValveSoftware/openvr): 1.0.6 (Win32 only) @@ -24,16 +22,15 @@ These dependencies need not be installed manually. They are automatically downlo - [QuaZip](https://sourceforge.net/projects/quazip/files/quazip/): 0.7.3 - [SDL2](https://www.libsdl.org/download-2.0.php): 2.0.3 - [Intel Threading Building Blocks](https://www.threadingbuildingblocks.org/): 4.3 -- [Sixense](http://sixense.com/): 071615 +- [vcpkg](https://github.com/highfidelity/vcpkg): +- [VHACD](https://github.com/virneo/v-hacd) - [zlib](http://www.zlib.net/): 1.28 (Win32 only) -- nVidia Texture Tools: 2.1 +- [nvtt](https://github.com/highfidelity/nvidia-texture-tools): 2.1.1 (customized) The above dependencies will be downloaded, built, linked and included automatically by CMake where we require them. The CMakeLists files that handle grabbing each of the following external dependencies can be found in the [cmake/externals folder](cmake/externals). The resulting downloads, source files and binaries will be placed in the `build/ext` folder in each of the subfolders for each external project. These are not placed in your normal build tree when doing an out of source build so that they do not need to be re-downloaded and re-compiled every time the CMake build folder is cleared. Should you want to force a re-download and re-compile of a specific external, you can simply remove that directory from the appropriate subfolder in `build/ext`. Should you want to force a re-download and re-compile of all externals, just remove the `build/ext` folder. -If you would like to use a specific install of a dependency instead of the version that would be grabbed as a CMake ExternalProject, you can pass -DUSE\_LOCAL\_$NAME=0 (where $NAME is the name of the subfolder in [cmake/externals](cmake/externals)) when you run CMake to tell it not to get that dependency as an external project. - #### CMake Hifi uses CMake to generate build files and project files for your platform. @@ -46,6 +43,7 @@ This can either be entered directly into your shell session before you build or The path it needs to be set to will depend on where and how Qt5 was installed. e.g. + export QT_CMAKE_PREFIX_PATH=/usr/local/Qt5.10.1/5.10.1/gcc_64/lib/cmake export QT_CMAKE_PREFIX_PATH=/usr/local/qt/5.10.1/clang_64/lib/cmake/ export QT_CMAKE_PREFIX_PATH=/usr/local/Cellar/qt5/5.10.1/lib/cmake export QT_CMAKE_PREFIX_PATH=/usr/local/opt/qt5/lib/cmake @@ -80,9 +78,22 @@ In the examples below the variable $NAME would be replaced by the name of the de * $NAME_ROOT_DIR - set this variable in your ENV * HIFI_LIB_DIR - set this variable in your ENV to your High Fidelity lib folder, should contain a folder '$name' - ### Optional Components +#### Build Options + +The following build options can be used when running CMake + +* BUILD_CLIENT +* BUILD_SERVER +* BUILD_TESTS +* BUILD_TOOLS + +#### Developer Build Options + +* USE_GLES +* DISABLE_UI + #### Devices You can support external input/output devices such as Leap Motion, MIDI, and more by adding each individual SDK in the visible building path. Refer to the readme file available in each device folder in [interface/external/](interface/external) for the detailed explanation of the requirements to use the device. diff --git a/BUILD_LINUX.md b/BUILD_LINUX.md index 0daef5ae05..15c5915c51 100644 --- a/BUILD_LINUX.md +++ b/BUILD_LINUX.md @@ -6,13 +6,20 @@ Please read the [general build guide](BUILD.md) for information on dependencies Should you choose not to install Qt5 via a package manager that handles dependencies for you, you may be missing some Qt5 dependencies. On Ubuntu, for example, the following additional packages are required: - libasound2 libxmu-dev libxi-dev freeglut3-dev libasound2-dev libjack0 libjack-dev libxrandr-dev libudev-dev libssl-dev + libasound2 libxmu-dev libxi-dev freeglut3-dev libasound2-dev libjack0 libjack-dev libxrandr-dev libudev-dev libssl-dev zlib1g-dev -## Ubuntu 16.04 specific build guide +## Ubuntu 16.04/18.04 specific build guide + +### Ubuntu 18.04 only +Add the universe repository: +_(This is not enabled by default on the server edition)_ +```bash +sudo add-apt-repository universe +sudo apt-get update +``` ### Prepare environment -hifiqt5.10.1 -Install qt: +Install Qt 5.10.1: ```bash wget http://debian.highfidelity.com/pool/h/hi/hifiqt5.10.1_5.10.1_amd64.deb sudo dpkg -i hifiqt5.10.1_5.10.1_amd64.deb @@ -20,19 +27,25 @@ sudo dpkg -i hifiqt5.10.1_5.10.1_amd64.deb Install build dependencies: ```bash -sudo apt-get install libasound2 libxmu-dev libxi-dev freeglut3-dev libasound2-dev libjack0 libjack-dev libxrandr-dev libudev-dev libssl-dev +sudo apt-get install libasound2 libxmu-dev libxi-dev freeglut3-dev libasound2-dev libjack0 libjack-dev libxrandr-dev libudev-dev libssl-dev zlib1g-dev ``` To compile interface in a server you must install: ```bash -sudo apt -y install libpulse0 libnss3 libnspr4 libfontconfig1 libxcursor1 libxcomposite1 libxtst6 libxslt1.1 +sudo apt-get -y install libpulse0 libnss3 libnspr4 libfontconfig1 libxcursor1 libxcomposite1 libxtst6 libxslt1.1 ``` Install build tools: ```bash -sudo apt install cmake +sudo apt-get install cmake ``` +Install Python 3: +```bash +sudo apt-get install python3.6 +``` + + ### Get code and checkout the tag you need Clone this repository: @@ -48,12 +61,7 @@ git tags Then checkout last tag with: ```bash -git checkout tags/RELEASE-6819 -``` - -Or go to the highfidelity download page (https://highfidelity.com/download) to get the release version. For example, if there is a BETA 6731 type: -```bash -git checkout tags/RELEASE-6731 +git checkout tags/v0.71.0 ``` ### Compiling @@ -66,15 +74,20 @@ cd hifi/build Prepare makefiles: ```bash -cmake -DQT_CMAKE_PREFIX_PATH=/usr/local/Qt5.10.1/5.10/gcc_64/lib/cmake .. +cmake -DQT_CMAKE_PREFIX_PATH=/usr/local/Qt5.10.1/5.10.1/gcc_64/lib/cmake .. ``` -Start compilation and get a cup of coffee: +Start compilation of the server and get a cup of coffee: ```bash -make domain-server assignment-client interface +make domain-server assignment-client ``` -In a server does not make sense to compile interface +To compile interface: +```bash +make interface +``` + +In a server, it does not make sense to compile interface ### Running the software @@ -93,4 +106,4 @@ Running interface: ./interface/interface ``` -Go to localhost in running interface. +Go to localhost in the running interface. diff --git a/BUILD_OSX.md b/BUILD_OSX.md index 62102b3e18..488c38e909 100644 --- a/BUILD_OSX.md +++ b/BUILD_OSX.md @@ -6,6 +6,10 @@ Please read the [general build guide](BUILD.md) for information on dependencies brew install cmake openssl qt +### Python 3 + +Download an install Python 3.6.6 or higher from [here](https://www.python.org/downloads/). Execute the `Update Shell Profile.command` script that is provided with the installer. + ### OpenSSL Assuming you've installed OpenSSL using the homebrew instructions above, you'll need to set OPENSSL_ROOT_DIR so CMake can find your installations. @@ -28,7 +32,9 @@ Note that this uses the version from the homebrew formula at the time of this wr If Xcode is your editor of choice, you can ask CMake to generate Xcode project files instead of Unix Makefiles. - cmake .. -GXcode + cmake .. -G Xcode + +If `cmake` complains about Python 3 being missing, you may need to update your CMake binary with command `brew upgrade cmake`, or by downloading and running the latest CMake installer, depending on how you originally instaled CMake After running cmake, you will have the make files or Xcode project file necessary to build all of the components. Open the hifi.xcodeproj file, choose ALL_BUILD from the Product > Scheme menu (or target drop down), and click Run. diff --git a/BUILD_WIN.md b/BUILD_WIN.md index 90d2995e7d..073b048911 100644 --- a/BUILD_WIN.md +++ b/BUILD_WIN.md @@ -5,11 +5,17 @@ Note: We are now using Visual Studio 2017 and Qt 5.10.1. If you are upgrading fr Note: The prerequisites will require about 10 GB of space on your drive. You will also need a system with at least 8GB of main memory. -### Step 1. Visual Studio 2017 +### Step 1. Visual Studio 2017 & Python If you don’t have Community or Professional edition of Visual Studio 2017, download [Visual Studio Community 2017](https://www.visualstudio.com/downloads/). -When selecting components, check "Desktop development with C++." Also on the right on the Summary toolbar, check "Windows 8.1 SDK and UCRT SDK" and "VC++ 2015.3 v140 toolset (x86,x64)". +When selecting components, check "Desktop development with C++". Also on the right on the Summary toolbar, check "Windows 8.1 SDK and UCRT SDK" and "VC++ 2015.3 v140 toolset (x86,x64)". If you do not already have a python development environment installed, also check "Python Development" in this screen. + +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. ### Step 2. Installing CMake diff --git a/CMakeLists.txt b/CMakeLists.txt index 2a01b18a57..4e5dbe935a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -7,17 +7,36 @@ else() cmake_minimum_required(VERSION 3.2) endif() +include("${CMAKE_CURRENT_SOURCE_DIR}/cmake/macros/TargetPython.cmake") +target_python() + +if (HIFI_ANDROID ) + execute_process( + COMMAND ${HIFI_PYTHON_EXEC} ${CMAKE_CURRENT_SOURCE_DIR}/prebuild.py --android --build-root ${CMAKE_BINARY_DIR} + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} + ) +else() + execute_process( + COMMAND ${HIFI_PYTHON_EXEC} ${CMAKE_CURRENT_SOURCE_DIR}/prebuild.py --build-root ${CMAKE_BINARY_DIR} + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} + ) + # squelch the Policy CMP0074 warning without requiring an update to cmake 3.12. + if (CMAKE_VERSION VERSION_GREATER_EQUAL 3.12) + cmake_policy(SET CMP0074 NEW) + endif() +endif() + +if(NOT EXISTS "${CMAKE_BINARY_DIR}/vcpkg.cmake") + message(FATAL_ERROR "vcpkg configuration missing.") +endif() + +include("${CMAKE_BINARY_DIR}/vcpkg.cmake") project(hifi) - include("cmake/init.cmake") - include("cmake/compiler.cmake") -if (BUILD_SCRIBE_ONLY) - add_subdirectory(tools/scribe) - add_subdirectory(tools/shader_reflect) - return() -endif() +add_paths_to_fixup_libs(${VCPKG_INSTALL_ROOT}/bin) +add_paths_to_fixup_libs(${VCPKG_INSTALL_ROOT}/debug/bin) if (NOT DEFINED CLIENT_ONLY) set(CLIENT_ONLY 0) @@ -35,7 +54,8 @@ endif() set(BUILD_CLIENT_OPTION ON) set(BUILD_SERVER_OPTION ON) -set(BUILD_TESTS_OPTION ON) +set(BUILD_TESTS_OPTION OFF) +set(BUILD_MANUAL_TESTS_OPTION ${BUILD_TESTS_OPTION}) set(BUILD_TOOLS_OPTION ON) set(BUILD_INSTALLER_OPTION ON) set(GLES_OPTION OFF) @@ -61,16 +81,18 @@ if (ANDROID) set(GLES_OPTION ON) set(PLATFORM_QT_COMPONENTS AndroidExtras WebView) else () - set(PLATFORM_QT_COMPONENTS WebEngine WebEngineWidgets) + set(PLATFORM_QT_COMPONENTS WebEngine) endif () if (USE_GLES AND (NOT ANDROID)) set(DISABLE_QML_OPTION ON) endif() + option(BUILD_CLIENT "Build client components" ${BUILD_CLIENT_OPTION}) option(BUILD_SERVER "Build server components" ${BUILD_SERVER_OPTION}) option(BUILD_TESTS "Build tests" ${BUILD_TESTS_OPTION}) +option(BUILD_MANUAL_TESTS "Build manual tests" ${BUILD_MANUAL_TESTS_OPTION}) option(BUILD_TOOLS "Build tools" ${BUILD_TOOLS_OPTION}) option(BUILD_INSTALLER "Build installer" ${BUILD_INSTALLER_OPTION}) option(USE_GLES "Use OpenGL ES" ${GLES_OPTION}) @@ -138,6 +160,8 @@ list(APPEND CMAKE_PREFIX_PATH "${QT_CMAKE_PREFIX_PATH}") find_package( Threads ) add_definitions(-DGLM_FORCE_RADIANS) +add_definitions(-DGLM_ENABLE_EXPERIMENTAL) +add_definitions(-DGLM_FORCE_CTOR_INIT) set(HIFI_LIBRARY_DIR "${CMAKE_CURRENT_SOURCE_DIR}/libraries") set(EXTERNAL_PROJECT_PREFIX "project") @@ -162,7 +186,6 @@ if (BUILD_SERVER) set_target_properties(domain-server PROPERTIES FOLDER "Apps") add_subdirectory(ice-server) set_target_properties(ice-server PROPERTIES FOLDER "Apps") - add_subdirectory(server-console) endif() if (BUILD_CLIENT) @@ -174,6 +197,7 @@ endif() if (BUILD_CLIENT OR BUILD_SERVER) add_subdirectory(plugins) + add_subdirectory(server-console) endif() # BUILD_TOOLS option will be handled inside the tools's CMakeLists.txt because 'scribe' tool is required for build anyway @@ -185,7 +209,9 @@ if (BUILD_TESTS) include(CTest) enable_testing() add_subdirectory(tests) - add_subdirectory(tests-manual) + if (BUILD_MANUAL_TESTS) + add_subdirectory(tests-manual) + endif() endif() if (BUILD_INSTALLER) diff --git a/LICENSE b/LICENSE index 60e86a1cc7..deb80afc19 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2013-2016, High Fidelity, Inc. +Copyright (c) 2013-2018, High Fidelity, Inc. All rights reserved. licensing@highfidelity.io diff --git a/VCPKG.md b/VCPKG.md new file mode 100644 index 0000000000..c426dc618f --- /dev/null +++ b/VCPKG.md @@ -0,0 +1,75 @@ +[VCPKG](https://github.com/Microsoft/vcpkg) is an open source package management system created by Microsoft, intially just for Windows based system, but eventually extended to cover Linux and OSX as well, and in theory extensible enough to cover additional operating systems. + +VCPKG is now our primary mechanism for managing the external libraries and tools on which we rely to build our applications. + +Conventional usage of VCPKG involves cloning the repository, running the bootstrapping script to build the vcpkg binary, and then calling the binary to install a set of libraries. The libraries themselves are specified by a set of port files inside the [repository](https://github.com/Microsoft/vcpkg/tree/master/ports) + +Because the main VCPKG repository does not contain all the ports we want, and because we want to be able to manage the precise versions of our dependencies, rather than allow it to be outside of our control, instead of using the main vcpkg repository, we use a combination of a [fork](https://github.com/highfidelity/vcpkg) of the repository (which allows us to customize the vcpkg binary, currently necessary to deal with some out of date tools on our build hosts) and a set of [custom port files](./cmake/ports) stored in our own repository. + +## Adding new packages to vcpkg + +Note... Android vcpkg usage is still experimental. Contact Austin for more detailed information if you need to add a new package for use by Android. + +### Setup development environment + +In order to add new packages, you will need to set up an environment for testing. This assumes you already have the tools for normal Hifi development (git, cmake, a working C++ compiler, etc) + +* Clone our vcpkg [fork](https://github.com/highfidelity/vcpkg) +* Remove the ports directory from the checkout and symlink to our own [custom port files](./cmake/ports) +* Bootstrap the vcpkg binary with the `bootstrap-vcpkg.sh` or `bootstrap-vcpkg.bat` script + +### Add a new port skeleton + +Your new package will require, at minimum, a `CONTROL` file and a `portfile.cmake` file, located in a subdirectory of the ports folder. Assuming you're creating a new dependency named `foo` it should be located in `ports/foo` under the vcpkg directory. The `CONTROL` file will contain a small number of fields, such as the name, version, description and any other vcpkg ports on which you depend. The `portfile.cmake` is a CMake script that will instruct vcpkg how to build the packages. We'll cover that in more depth in a moment. For now, just create one and leave it blank. + +### Add a reference to your package to one or more of the hifi meta-packages + +We have three meta-packages used to do our building. When you modify one of these packages, make sure to bump the version number in the `CONTROL` file for the package + +#### hifi-deps + +This metapackage contains anything required for building the server or shared components. For instance, the `glm`, `tbb` and `zlib` packages are declared here because they're used everywhere, not just in our client application code. + +#### hifi-client-deps + +This metapackage contains anything required for building the client. For example, `sdl2` is listed here because it's required for our joystick input, but not for the server or shared components. Note that `hifi-client-deps` depends on `hifi-deps`, so you don't have to declare something twice if it's used in both he server and client. Just declare it in `hifi-deps` and it will still be includeded transitively. + +#### hifi-host-tools + +This metapackage contains anything we use to create executables that will then be used in the build process. The `hifi-deps` and `hifi-client-deps` packages are built for the target architecture, which may be different than the host architecture (for instance, when building for Android). The `hifi-host-tools` packages are always build for the host architecture, because they're tools that are intended to be run as part of the build process. Scribe for example is used at build time to generate shaders. Building an arm64 version of Scribe is useless because we need to run it on the host machine. + +Note that packages can appear in both the `hifi-host-tools` and one of the other metapackages, indicating that the package both contains a library which we will use at runtime, and a tool which we will use at build time. The `spirv-tools` package is an example. + +### Implement the portfile.cmake + +How the portfile is written depends on what kind of package you're working with. It's basically still a CMake script, but there are a number of [functions](https://vcpkg.readthedocs.io/en/latest/maintainers/portfile-functions/) available to make fetching and building packages easier. + +Typically there are three areas you need to deal with + +* Getting the source +* Building the source +* Installing the artifacts + +#### Getting sources + +Getting sources from github, gitlab or bitbucket is easy. There are special functions specifcially for those. See the [etc2comp portfile](./cmake/ports/etc2comp/portfile.cmake) for an example of fetching source via github. + +If the project isn't available that way, you can still use the [vcpkg_download_distfile](https://vcpkg.readthedocs.io/en/latest/maintainers/vcpkg_download_distfile/) function to explicitly download an archive and then use [vcpkg_extract_source_archive](https://vcpkg.readthedocs.io/en/latest/maintainers/vcpkg_extract_source_archive/) to unpack it. See the [zlib portfile](./cmake/ports/zlib/portfile.cmake) for an example there. + +#### Building + +If your package uses CMake, you'll be able to use the [vcpkg_configure_cmake](https://vcpkg.readthedocs.io/en/latest/maintainers/vcpkg_configure_cmake/) and [vcpkg_build_cmake](https://vcpkg.readthedocs.io/en/latest/maintainers/vcpkg_build_cmake/) commands to configure and build the package. If you're going to be relying on the CMake installation functionality, you can just call [vcpkg_install_cmake](https://vcpkg.readthedocs.io/en/latest/maintainers/vcpkg_install_cmake/), since it will implicitly run the build before the install. + +If your package is not binary, but doesn't use CMake, you're just going to have to figure it out. + +If your package is binary, then you can just skip this step + +#### Installing + +Once you've built the package, you need to install the artifacts in the target directory. Ideally, your package's CMake INSTALL commands will do the right thing. However, there are usually some things you have to do manually. Since VCPKG will build both the release and debug versions for all packages, you need to make sure if your package installed headers that you remove the _debug_ versions of these headers. This is typically done with the `file(REMOVE_RECURSE ${CURRENT_PACKAGES_DIR}/debug/include)`. Additionally, if your package creates any standalone executables, you need to make sure they're installed in the destination `tools` directory, not the `bin` or `lib` directories, which are specifically for shared library binaries (like .so or .dll files) and link library files (like .a or .lib files) respectively. + +If you're dealing with a binary package, then you'll need to explicitly perform all the required copies from the location where you extracted the archive to the installation directory. An example of this is available in the [openssl-android portfile](./cmake/ports/openssl-android/portfile.cmake) + +### Commit and test + +Once you've tested building your new package locally, you'll need to commit and push the changes and additions to the portfiles you've made and then monitor the build hosts to verify that the new package successfully built on all the target environments. diff --git a/android/app/CMakeLists.txt b/android/app/CMakeLists.txt index d24a84c6a5..19dce330c1 100644 --- a/android/app/CMakeLists.txt +++ b/android/app/CMakeLists.txt @@ -1,12 +1,14 @@ set(TARGET_NAME native-lib) setup_hifi_library() -link_hifi_libraries(shared task networking gl gpu qml image fbx render-utils physics entities octree ${PLATFORM_GL_BACKEND}) +link_hifi_libraries(shared task networking gl gpu qml image fbx hfm render-utils physics entities octree ${PLATFORM_GL_BACKEND}) target_opengl() target_bullet() set(INTERFACE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../../interface") add_subdirectory("${INTERFACE_DIR}" "libraries/interface") include_directories("${INTERFACE_DIR}/src") +set(HIFI_CODEC_PLUGIN_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../../plugins/hifiCodec") +add_subdirectory("${HIFI_CODEC_PLUGIN_DIR}" "libraries/hifiCodecPlugin") target_link_libraries(native-lib android log m interface) diff --git a/android/app/build.gradle b/android/app/build.gradle index d3463411b8..cbc31479ec 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -23,8 +23,6 @@ android { '-DANDROID_TOOLCHAIN=clang', '-DANDROID_STL=c++_shared', '-DQT_CMAKE_PREFIX_PATH=' + HIFI_ANDROID_PRECOMPILED + '/qt/lib/cmake', - '-DNATIVE_SCRIBE=' + HIFI_ANDROID_PRECOMPILED + '/scribe' + EXEC_SUFFIX, - '-DNATIVE_SHREFLECT=' + HIFI_ANDROID_PRECOMPILED + '/shreflect' + EXEC_SUFFIX, '-DHIFI_ANDROID_PRECOMPILED=' + HIFI_ANDROID_PRECOMPILED, '-DRELEASE_NUMBER=' + RELEASE_NUMBER, '-DRELEASE_TYPE=' + RELEASE_TYPE, @@ -53,6 +51,9 @@ android { debug { buildConfigField "String", "BACKTRACE_URL", "\"" + (System.getenv("CMAKE_BACKTRACE_URL") ? System.getenv("CMAKE_BACKTRACE_URL") : '') + "\"" buildConfigField "String", "BACKTRACE_TOKEN", "\"" + (System.getenv("CMAKE_BACKTRACE_TOKEN") ? System.getenv("CMAKE_BACKTRACE_TOKEN") : '') + "\"" + buildConfigField "String", "OAUTH_CLIENT_ID", "\"" + (System.getenv("OAUTH_CLIENT_ID") ? System.getenv("OAUTH_CLIENT_ID") : '') + "\"" + buildConfigField "String", "OAUTH_CLIENT_SECRET", "\"" + (System.getenv("OAUTH_CLIENT_SECRET") ? System.getenv("OAUTH_CLIENT_SECRET") : '') + "\"" + buildConfigField "String", "OAUTH_REDIRECT_URI", "\"" + (System.getenv("OAUTH_REDIRECT_URI") ? System.getenv("OAUTH_REDIRECT_URI") : '') + "\"" } release { minifyEnabled false @@ -63,6 +64,9 @@ android { project.hasProperty("HIFI_ANDROID_KEY_PASSWORD")? signingConfigs.release : null buildConfigField "String", "BACKTRACE_URL", "\"" + (System.getenv("CMAKE_BACKTRACE_URL") ? System.getenv("CMAKE_BACKTRACE_URL") : '') + "\"" buildConfigField "String", "BACKTRACE_TOKEN", "\"" + (System.getenv("CMAKE_BACKTRACE_TOKEN") ? System.getenv("CMAKE_BACKTRACE_TOKEN") : '') + "\"" + buildConfigField "String", "OAUTH_CLIENT_ID", "\"" + (System.getenv("OAUTH_CLIENT_ID") ? System.getenv("OAUTH_CLIENT_ID") : '') + "\"" + buildConfigField "String", "OAUTH_CLIENT_SECRET", "\"" + (System.getenv("OAUTH_CLIENT_SECRET") ? System.getenv("OAUTH_CLIENT_SECRET") : '') + "\"" + buildConfigField "String", "OAUTH_REDIRECT_URI", "\"" + (System.getenv("OAUTH_REDIRECT_URI") ? System.getenv("OAUTH_REDIRECT_URI") : '') + "\"" } } @@ -80,8 +84,10 @@ android { if (Os.isFamily(Os.FAMILY_UNIX)) { def uploadDumpSymsTask = rootProject.getTasksByName("uploadBreakpadDumpSyms${variant.name.capitalize()}", false).first() def runDumpSymsTask = rootProject.getTasksByName("runBreakpadDumpSyms${variant.name.capitalize()}", false).first() + def renameHifiACTask = rootProject.getTasksByName("renameHifiACTask${variant.name.capitalize()}", false).first() runDumpSymsTask.dependsOn(task) variant.assemble.dependsOn(uploadDumpSymsTask) + variant.mergeResources.dependsOn(renameHifiACTask) } } @@ -133,6 +139,10 @@ dependencies { implementation 'com.android.support.constraint:constraint-layout:1.0.2' implementation 'com.android.support:design:26.1.0' + compile 'com.android.support:support-v4:26.1.0' + compile 'com.android.support:appcompat-v7:26.1.0' + compile 'com.android.support:support-vector-drawable:26.1.0' + implementation 'com.android.support:appcompat-v7:26.1.0' compile 'com.android.support:recyclerview-v7:26.1.0' compile 'com.android.support:cardview-v7:26.1.0' diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 7255e1f295..57e708068f 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -9,6 +9,7 @@ + @@ -70,11 +71,24 @@ android:screenOrientation="portrait" android:theme="@style/Theme.AppCompat.Translucent.NoActionBar" /> + + + + + + + + diff --git a/android/app/src/main/cpp/native.cpp b/android/app/src/main/cpp/native.cpp index ce5af01f29..f9c7751a3e 100644 --- a/android/app/src/main/cpp/native.cpp +++ b/android/app/src/main/cpp/native.cpp @@ -24,8 +24,12 @@ #include #include +#define AUTO_LOGOUT_SETTING_NAME "wallet/autoLogout" +#define WALLET_USERNAME_SETTING_NAME "wallet/savedUsername" + QAndroidJniObject __interfaceActivity; QAndroidJniObject __loginCompletedListener; +QAndroidJniObject __signupCompletedListener; QAndroidJniObject __loadCompleteListener; QAndroidJniObject __usernameChangedListener; void tempMessageHandler(QtMsgType type, const QMessageLogContext& context, const QString& message) { @@ -156,7 +160,7 @@ JNIEXPORT void Java_io_highfidelity_hifiinterface_InterfaceActivity_nativeOnCrea JavaVM* jvm; env->GetJavaVM(&jvm); - QObject::connect(&AndroidHelper::instance(), &AndroidHelper::androidActivityRequested, [jvm](const QString& a, const bool backToScene, QList args) { + QObject::connect(&AndroidHelper::instance(), &AndroidHelper::androidActivityRequested, [jvm](const QString& a, const bool backToScene, QMap args) { JNIEnv* myNewEnv; JavaVMAttachArgs jvmArgs; jvmArgs.version = JNI_VERSION_1_6; // choose your JNI version @@ -182,9 +186,11 @@ JNIEXPORT void Java_io_highfidelity_hifiinterface_InterfaceActivity_nativeOnCrea jmethodID mapClassConstructor = myNewEnv->GetMethodID(hashMapClass, "", "()V"); jobject hashmap = myNewEnv->NewObject(hashMapClass, mapClassConstructor); jmethodID mapClassPut = myNewEnv->GetMethodID(hashMapClass, "put", "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;"); - for (const QString& arg: args) { - QAndroidJniObject jArg = QAndroidJniObject::fromString(arg); - myNewEnv->CallObjectMethod(hashmap, mapClassPut, QAndroidJniObject::fromString("url").object(), jArg.object()); + QMap::iterator i; + for (i = args.begin(); i != args.end(); ++i) { + QAndroidJniObject jKey = QAndroidJniObject::fromString(i.key()); + QAndroidJniObject jValue = QAndroidJniObject::fromString(i.value()); + myNewEnv->CallObjectMethod(hashmap, mapClassPut, jKey.object(), jValue.object()); } __interfaceActivity.callMethod("openAndroidActivity", "(Ljava/lang/String;ZLjava/util/HashMap;)V", string.object(), jBackToScene, hashmap); if (attachedHere) { @@ -207,11 +213,13 @@ JNIEXPORT void Java_io_highfidelity_hifiinterface_InterfaceActivity_nativeOnDest JNIEXPORT void Java_io_highfidelity_hifiinterface_InterfaceActivity_nativeGotoUrl(JNIEnv* env, jobject obj, jstring url) { QAndroidJniObject jniUrl("java/lang/String", "(Ljava/lang/String;)V", url); DependencyManager::get()->loadSettings(jniUrl.toString()); + AndroidHelper::instance().muteMic(); } JNIEXPORT void Java_io_highfidelity_hifiinterface_InterfaceActivity_nativeGoToUser(JNIEnv* env, jobject obj, jstring username) { QAndroidJniObject jniUsername("java/lang/String", "(Ljava/lang/String;)V", username); DependencyManager::get()->goToUser(jniUsername.toString(), false); + AndroidHelper::instance().muteMic(); } JNIEXPORT void Java_io_highfidelity_hifiinterface_InterfaceActivity_nativeOnPause(JNIEnv* env, jobject obj) { @@ -256,9 +264,75 @@ JNIEXPORT jstring JNICALL Java_io_highfidelity_hifiinterface_fragment_HomeFragme } JNIEXPORT void JNICALL -Java_io_highfidelity_hifiinterface_fragment_LoginFragment_nativeLogin(JNIEnv *env, jobject instance, +Java_io_highfidelity_hifiinterface_HifiUtils_updateHifiSetting(JNIEnv *env, jobject instance, + jstring group_, jstring key_, + jboolean value_) { + const char *c_group = env->GetStringUTFChars(group_, 0); + const char *c_key = env->GetStringUTFChars(key_, 0); + + const QString group = QString::fromUtf8(c_group); + const QString key = QString::fromUtf8(c_key); + + env->ReleaseStringUTFChars(group_, c_group); + env->ReleaseStringUTFChars(key_, c_key); + + bool value = value_; + + Setting::Handle setting { QStringList() << group << key , !value }; + setting.set(value); +} + +JNIEXPORT jboolean JNICALL +Java_io_highfidelity_hifiinterface_HifiUtils_getHifiSettingBoolean(JNIEnv *env, + jobject instance, + jstring group_, + jstring key_, + jboolean defaultValue) { + const char *c_group = env->GetStringUTFChars(group_, 0); + const char *c_key = env->GetStringUTFChars(key_, 0); + + const QString group = QString::fromUtf8(c_group); + const QString key = QString::fromUtf8(c_key); + + env->ReleaseStringUTFChars(group_, c_group); + env->ReleaseStringUTFChars(key_, c_key); + + Setting::Handle setting { QStringList() << group << key , defaultValue}; + return setting.get(); +} + +JNIEXPORT jboolean JNICALL +Java_io_highfidelity_hifiinterface_HifiUtils_isUserLoggedIn(JNIEnv *env, jobject instance) { + return DependencyManager::get()->isLoggedIn(); +} + +JNIEXPORT jboolean JNICALL +Java_io_highfidelity_hifiinterface_HifiUtils_isKeepingLoggedIn(JNIEnv *env, jobject instance) { + Setting::Handle setting(AUTO_LOGOUT_SETTING_NAME, true); + return !setting.get(); +} + +JNIEXPORT void JNICALL +Java_io_highfidelity_hifiinterface_fragment_LoginFragment_cancelLogin(JNIEnv *env, jobject instance) { + + auto accountManager = DependencyManager::get(); + + QObject::disconnect(accountManager.data(), &AccountManager::loginComplete, nullptr, nullptr); + QObject::disconnect(accountManager.data(), &AccountManager::loginFailed, nullptr, nullptr); + +} + +JNIEXPORT void JNICALL +Java_io_highfidelity_hifiinterface_fragment_SignupFragment_cancelLogin(JNIEnv *env, + jobject instance) { + + Java_io_highfidelity_hifiinterface_fragment_LoginFragment_cancelLogin(env, instance); +} + +JNIEXPORT void JNICALL +Java_io_highfidelity_hifiinterface_fragment_LoginFragment_login(JNIEnv *env, jobject instance, jstring username_, jstring password_, - jobject usernameChangedListener) { + jboolean keepLoggedIn) { const char *c_username = env->GetStringUTFChars(username_, 0); const char *c_password = env->GetStringUTFChars(password_, 0); QString username = QString(c_username); @@ -269,31 +343,131 @@ Java_io_highfidelity_hifiinterface_fragment_LoginFragment_nativeLogin(JNIEnv *en auto accountManager = DependencyManager::get(); __loginCompletedListener = QAndroidJniObject(instance); - __usernameChangedListener = QAndroidJniObject(usernameChangedListener); - QObject::connect(accountManager.data(), &AccountManager::loginComplete, [](const QUrl& authURL) { + QObject::connect(accountManager.data(), &AccountManager::loginComplete, [username, keepLoggedIn](const QUrl& authURL) { jboolean jSuccess = (jboolean) true; - __loginCompletedListener.callMethod("handleLoginCompleted", "(Z)V", jSuccess); + if (__loginCompletedListener.isValid()) { + __loginCompletedListener.callMethod("handleLoginCompleted", "(Z)V", jSuccess); + } + Setting::Handle(AUTO_LOGOUT_SETTING_NAME).set(!keepLoggedIn); + QString usernameToSave = keepLoggedIn ? username : ""; + Setting::Handle(WALLET_USERNAME_SETTING_NAME).set(usernameToSave); }); QObject::connect(accountManager.data(), &AccountManager::loginFailed, []() { jboolean jSuccess = (jboolean) false; - __loginCompletedListener.callMethod("handleLoginCompleted", "(Z)V", jSuccess); - }); - - QObject::connect(accountManager.data(), &AccountManager::usernameChanged, [](const QString& username) { - QAndroidJniObject string = QAndroidJniObject::fromString(username); - __usernameChangedListener.callMethod("handleUsernameChanged", "(Ljava/lang/String;)V", string.object()); + if (__loginCompletedListener.isValid()) { + __loginCompletedListener.callMethod("handleLoginCompleted", "(Z)V", jSuccess); + } }); QMetaObject::invokeMethod(accountManager.data(), "requestAccessToken", Q_ARG(const QString&, username), Q_ARG(const QString&, password)); } -JNIEXPORT jboolean JNICALL -Java_io_highfidelity_hifiinterface_fragment_FriendsFragment_nativeIsLoggedIn(JNIEnv *env, jobject instance) { +JNIEXPORT void JNICALL +Java_io_highfidelity_hifiinterface_fragment_LoginFragment_retrieveAccessToken(JNIEnv *env, + jobject instance, + jstring authCode_, + jstring clientId_, + jstring clientSecret_, + jstring redirectUri_) { + const char *c_authCode = env->GetStringUTFChars(authCode_, 0); + const char *c_clientId = env->GetStringUTFChars(clientId_, 0); + const char *c_clientSecret = env->GetStringUTFChars(clientSecret_, 0); + const char *c_redirectUri = env->GetStringUTFChars(redirectUri_, 0); + + QString authCode = QString(c_authCode); + QString clientId = QString(c_clientId); + QString clientSecret = QString(c_clientSecret); + QString redirectUri = QString(c_redirectUri); + + env->ReleaseStringUTFChars(authCode_, c_authCode); + env->ReleaseStringUTFChars(clientId_, c_clientId); + env->ReleaseStringUTFChars(clientSecret_, c_clientSecret); + env->ReleaseStringUTFChars(redirectUri_, c_redirectUri); + auto accountManager = DependencyManager::get(); - return accountManager->isLoggedIn(); + + __loginCompletedListener = QAndroidJniObject(instance); // TODO: use a different listener? + + QObject::connect(accountManager.data(), &AccountManager::loginComplete, [](const QUrl& authURL) { + jboolean jSuccess = (jboolean) true; + if (__loginCompletedListener.isValid()) { + __loginCompletedListener.callMethod("handleLoginCompleted", "(Z)V", jSuccess); + } + }); + + QObject::connect(accountManager.data(), &AccountManager::loginFailed, []() { + jboolean jSuccess = (jboolean) false; + if (__loginCompletedListener.isValid()) { + __loginCompletedListener.callMethod("handleLoginCompleted", "(Z)V", jSuccess); + } + }); + + QMetaObject::invokeMethod(accountManager.data(), "requestAccessTokenWithAuthCode", + Q_ARG(const QString&, authCode), Q_ARG(const QString&, clientId), + Q_ARG(const QString&, clientSecret), Q_ARG(const QString&, redirectUri)); + +} + +JNIEXPORT void JNICALL +Java_io_highfidelity_hifiinterface_fragment_SignupFragment_login(JNIEnv *env, + jobject instance, + jstring username_, + jstring password_, + jboolean keepLoggedIn) { + Java_io_highfidelity_hifiinterface_fragment_LoginFragment_login(env, instance, username_, password_, keepLoggedIn); +} + +JNIEXPORT void Java_io_highfidelity_hifiinterface_InterfaceActivity_nativeInitAfterAppLoaded(JNIEnv* env, jobject obj) { + AndroidHelper::instance().moveToThread(qApp->thread()); +} + +JNIEXPORT void JNICALL +Java_io_highfidelity_hifiinterface_fragment_SignupFragment_signup(JNIEnv *env, jobject instance, + jstring email_, jstring username_, + jstring password_) { + + const char *c_email = env->GetStringUTFChars(email_, 0); + const char *c_username = env->GetStringUTFChars(username_, 0); + const char *c_password = env->GetStringUTFChars(password_, 0); + QString email = QString(c_email); + QString username = QString(c_username); + QString password = QString(c_password); + env->ReleaseStringUTFChars(email_, c_email); + env->ReleaseStringUTFChars(username_, c_username); + env->ReleaseStringUTFChars(password_, c_password); + + __signupCompletedListener = QAndroidJniObject(instance); + + // disconnect any previous callback + QObject::disconnect(&AndroidHelper::instance(), &AndroidHelper::handleSignupCompleted, nullptr, nullptr); + QObject::disconnect(&AndroidHelper::instance(), &AndroidHelper::handleSignupFailed, nullptr, nullptr); + + QObject::connect(&AndroidHelper::instance(), &AndroidHelper::handleSignupCompleted, []() { + jboolean jSuccess = (jboolean) true; + if (__signupCompletedListener.isValid()) { + __signupCompletedListener.callMethod("handleSignupCompleted", "()V", jSuccess); + } + }); + + QObject::connect(&AndroidHelper::instance(), &AndroidHelper::handleSignupFailed, [](QString errorString) { + if (__signupCompletedListener.isValid()) { + QAndroidJniObject string = QAndroidJniObject::fromString(errorString); + __signupCompletedListener.callMethod("handleSignupFailed", "(Ljava/lang/String;)V", string.object()); + } + }); + + AndroidHelper::instance().signup(email, username, password); +} + +JNIEXPORT void JNICALL +Java_io_highfidelity_hifiinterface_fragment_SignupFragment_cancelSignup(JNIEnv *env, jobject instance) { + QObject::disconnect(&AndroidHelper::instance(), &AndroidHelper::handleSignupCompleted, nullptr, nullptr); + QObject::disconnect(&AndroidHelper::instance(), &AndroidHelper::handleSignupFailed, nullptr, nullptr); + + __signupCompletedListener = nullptr; } JNIEXPORT jstring JNICALL @@ -318,23 +492,40 @@ Java_io_highfidelity_hifiinterface_SplashActivity_registerLoadCompleteListener(J }); } -JNIEXPORT jboolean JNICALL -Java_io_highfidelity_hifiinterface_MainActivity_nativeIsLoggedIn(JNIEnv *env, jobject instance) { - return DependencyManager::get()->isLoggedIn(); -} JNIEXPORT void JNICALL -Java_io_highfidelity_hifiinterface_MainActivity_nativeLogout(JNIEnv *env, jobject instance) { +Java_io_highfidelity_hifiinterface_MainActivity_logout(JNIEnv *env, jobject instance) { DependencyManager::get()->logout(); } JNIEXPORT jstring JNICALL -Java_io_highfidelity_hifiinterface_MainActivity_nativeGetDisplayName(JNIEnv *env, +Java_io_highfidelity_hifiinterface_MainActivity_getUsername(JNIEnv *env, jobject instance) { QString username = DependencyManager::get()->getAccountInfo().getUsername(); return env->NewStringUTF(username.toLatin1().data()); } +JNIEXPORT void JNICALL +Java_io_highfidelity_hifiinterface_MainActivity_setUsernameChangedListener(JNIEnv *env, + jobject instance, + jobject usernameChangedListener) { + __usernameChangedListener = QAndroidJniObject(usernameChangedListener); + + if (!__usernameChangedListener.isValid()) { + return; + } + + auto accountManager = DependencyManager::get(); + + QObject::connect(accountManager.data(), &AccountManager::usernameChanged, [](const QString& username) { + QAndroidJniObject string = QAndroidJniObject::fromString(username); + if (__usernameChangedListener.isValid()) { + __usernameChangedListener.callMethod("handleUsernameChanged", "(Ljava/lang/String;)V", string.object()); + } + }); + +} + JNIEXPORT void JNICALL Java_io_highfidelity_hifiinterface_InterfaceActivity_nativeBeforeEnterBackground(JNIEnv *env, jobject obj) { AndroidHelper::instance().notifyBeforeEnterBackground(); @@ -355,5 +546,11 @@ JNIEXPORT void Java_io_highfidelity_hifiinterface_WebViewActivity_nativeProcessU AndroidHelper::instance().processURL(QString::fromUtf8(nativeString)); } +JNIEXPORT void JNICALL +Java_io_highfidelity_hifiinterface_receiver_HeadsetStateReceiver_notifyHeadsetOn(JNIEnv *env, + jobject instance, + jboolean pluggedIn) { + AndroidHelper::instance().notifyHeadsetOn(pluggedIn); +} } diff --git a/android/app/src/main/java/io/highfidelity/hifiinterface/HifiUtils.java b/android/app/src/main/java/io/highfidelity/hifiinterface/HifiUtils.java index f92cd0a385..a85e18d9a9 100644 --- a/android/app/src/main/java/io/highfidelity/hifiinterface/HifiUtils.java +++ b/android/app/src/main/java/io/highfidelity/hifiinterface/HifiUtils.java @@ -64,4 +64,10 @@ public class HifiUtils { public native String protocolVersionSignature(); + public native boolean isUserLoggedIn(); + + public native void updateHifiSetting(String group, String key, boolean value); + public native boolean getHifiSettingBoolean(String group, String key, boolean defaultValue); + + public native boolean isKeepingLoggedIn(); } diff --git a/android/app/src/main/java/io/highfidelity/hifiinterface/InterfaceActivity.java b/android/app/src/main/java/io/highfidelity/hifiinterface/InterfaceActivity.java index f161783d6a..50aea59663 100644 --- a/android/app/src/main/java/io/highfidelity/hifiinterface/InterfaceActivity.java +++ b/android/app/src/main/java/io/highfidelity/hifiinterface/InterfaceActivity.java @@ -13,6 +13,7 @@ package io.highfidelity.hifiinterface; import android.content.Context; import android.content.Intent; +import android.content.IntentFilter; import android.content.pm.ActivityInfo; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; @@ -38,8 +39,10 @@ import java.lang.reflect.Field; import java.util.ArrayList; import java.util.HashMap; import java.util.List; +import java.util.Map; import io.highfidelity.hifiinterface.fragment.WebViewFragment; +import io.highfidelity.hifiinterface.receiver.HeadsetStateReceiver; /*import com.google.vr.cardboard.DisplaySynchronizer; import com.google.vr.cardboard.DisplayUtils; @@ -55,6 +58,7 @@ public class InterfaceActivity extends QtActivity implements WebViewFragment.OnW private static final int NORMAL_DPI = 160; private Vibrator mVibrator; + private HeadsetStateReceiver headsetStateReceiver; //public static native void handleHifiURL(String hifiURLString); private native long nativeOnCreate(InterfaceActivity instance, AssetManager assetManager); @@ -65,13 +69,14 @@ public class InterfaceActivity extends QtActivity implements WebViewFragment.OnW private native void nativeEnterBackground(); private native void nativeEnterForeground(); private native long nativeOnExitVr(); + private native void nativeInitAfterAppLoaded(); private AssetManager assetManager; private static boolean inVrMode; private boolean nativeEnterBackgroundCallEnqueued = false; - private SlidingDrawer webSlidingDrawer; + private SlidingDrawer mWebSlidingDrawer; // private GvrApi gvrApi; // Opaque native pointer to the Application C++ object. // This object is owned by the InterfaceActivity instance and passed to the native methods. @@ -111,17 +116,6 @@ public class InterfaceActivity extends QtActivity implements WebViewFragment.OnW //nativeGvrApi = nativeOnCreate(this, assetManager /*, gvrApi.getNativeGvrContext()*/); - Point size = new Point(); - getWindowManager().getDefaultDisplay().getRealSize(size); - - try { - PackageInfo pInfo = this.getPackageManager().getPackageInfo(getPackageName(), 0); - String version = pInfo.versionName; -// setAppVersion(version); - } catch (PackageManager.NameNotFoundException e) { - Log.e("GVR", "Error getting application version", e); - } - final View rootView = getWindow().getDecorView().findViewById(android.R.id.content); // This is a workaround to hide the menu bar when the virtual keyboard is shown from Qt @@ -132,25 +126,7 @@ public class InterfaceActivity extends QtActivity implements WebViewFragment.OnW }); startActivity(new Intent(this, SplashActivity.class)); mVibrator = (Vibrator) this.getSystemService(VIBRATOR_SERVICE); - - FrameLayout mainLayout = findViewById(android.R.id.content); - LayoutInflater inflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE); - webSlidingDrawer = (SlidingDrawer) inflater.inflate(R.layout.web_drawer, mainLayout, false); - QtLayout qtLayout = (QtLayout) mainLayout.getChildAt(0); - QtLayout.LayoutParams layoutParams = new QtLayout.LayoutParams(webSlidingDrawer.getLayoutParams()); - webSlidingDrawer.setOnDrawerCloseListener(() -> { - WebViewFragment webViewFragment = (WebViewFragment) getFragmentManager().findFragmentByTag("webViewFragment"); - webViewFragment.close(); - }); - int widthPx = Math.max(size.x, size.y); - int heightPx = Math.min(size.x, size.y); - - layoutParams.x = (int) (widthPx - WEB_DRAWER_RIGHT_MARGIN * getResources().getDisplayMetrics().xdpi / NORMAL_DPI); - layoutParams.y = (int) (heightPx - WEB_DRAWER_BOTTOM_MARGIN * getResources().getDisplayMetrics().ydpi / NORMAL_DPI); - - layoutParams.resolveLayoutDirection(View.LAYOUT_DIRECTION_RTL); - qtLayout.addView(webSlidingDrawer, layoutParams); - webSlidingDrawer.setVisibility(View.GONE); + headsetStateReceiver = new HeadsetStateReceiver(); } @Override @@ -161,6 +137,7 @@ public class InterfaceActivity extends QtActivity implements WebViewFragment.OnW } else { nativeEnterBackground(); } + unregisterReceiver(headsetStateReceiver); //gvrApi.pauseTracking(); } @@ -183,6 +160,7 @@ public class InterfaceActivity extends QtActivity implements WebViewFragment.OnW nativeEnterForeground(); surfacesWorkaround(); keepInterfaceRunning = false; + registerReceiver(headsetStateReceiver, new IntentFilter(Intent.ACTION_HEADSET_PLUG)); //gvrApi.resumeTracking(); } @@ -280,14 +258,47 @@ public class InterfaceActivity extends QtActivity implements WebViewFragment.OnW protected void onNewIntent(Intent intent) { super.onNewIntent(intent); if (intent.hasExtra(DOMAIN_URL)) { - webSlidingDrawer.setVisibility(View.GONE); + hideWebDrawer(); nativeGotoUrl(intent.getStringExtra(DOMAIN_URL)); } else if (intent.hasExtra(EXTRA_GOTO_USERNAME)) { - webSlidingDrawer.setVisibility(View.GONE); + hideWebDrawer(); nativeGoToUser(intent.getStringExtra(EXTRA_GOTO_USERNAME)); } } + private void hideWebDrawer() { + if (mWebSlidingDrawer != null) { + mWebSlidingDrawer.setVisibility(View.GONE); + } + } + + public void showWebDrawer() { + if (mWebSlidingDrawer == null) { + FrameLayout mainLayout = findViewById(android.R.id.content); + LayoutInflater inflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE); + QtLayout qtLayout = (QtLayout) mainLayout.getChildAt(0); + mWebSlidingDrawer = (SlidingDrawer) inflater.inflate(R.layout.web_drawer, mainLayout, false); + + QtLayout.LayoutParams layoutParams = new QtLayout.LayoutParams(mWebSlidingDrawer.getLayoutParams()); + mWebSlidingDrawer.setOnDrawerCloseListener(() -> { + WebViewFragment webViewFragment = (WebViewFragment) getFragmentManager().findFragmentByTag("webViewFragment"); + webViewFragment.close(); + }); + + Point size = new Point(); + getWindowManager().getDefaultDisplay().getRealSize(size); + int widthPx = Math.max(size.x, size.y); + int heightPx = Math.min(size.x, size.y); + + layoutParams.x = (int) (widthPx - WEB_DRAWER_RIGHT_MARGIN * getResources().getDisplayMetrics().xdpi / NORMAL_DPI); + layoutParams.y = (int) (heightPx - WEB_DRAWER_BOTTOM_MARGIN * getResources().getDisplayMetrics().ydpi / NORMAL_DPI); + + layoutParams.resolveLayoutDirection(View.LAYOUT_DIRECTION_RTL); + qtLayout.addView(mWebSlidingDrawer, layoutParams); + } + mWebSlidingDrawer.setVisibility(View.VISIBLE); + } + public void openAndroidActivity(String activityName, boolean backToScene) { openAndroidActivity(activityName, backToScene, null); } @@ -296,29 +307,37 @@ public class InterfaceActivity extends QtActivity implements WebViewFragment.OnW switch (activityName) { case "Home": case "Privacy Policy": - case "Login": { nativeBeforeEnterBackground(); Intent intent = new Intent(this, MainActivity.class); intent.putExtra(MainActivity.EXTRA_FRAGMENT, activityName); intent.putExtra(MainActivity.EXTRA_BACK_TO_SCENE, backToScene); startActivity(intent); break; - } + case "Login": + nativeBeforeEnterBackground(); + Intent loginIntent = new Intent(this, LoginMenuActivity.class); + loginIntent.putExtra(LoginMenuActivity.EXTRA_BACK_TO_SCENE, backToScene); + loginIntent.putExtra(LoginMenuActivity.EXTRA_BACK_ON_SKIP, true); + if (args != null && args.containsKey(DOMAIN_URL)) { + loginIntent.putExtra(LoginMenuActivity.EXTRA_DOMAIN_URL, (String) args.get(DOMAIN_URL)); + } + startActivity(loginIntent); + break; case "WebView": runOnUiThread(() -> { - webSlidingDrawer.setVisibility(View.VISIBLE); - if (!webSlidingDrawer.isOpened()) { - webSlidingDrawer.animateOpen(); + showWebDrawer(); + if (!mWebSlidingDrawer.isOpened()) { + mWebSlidingDrawer.animateOpen(); } if (args != null && args.containsKey(WebViewActivity.WEB_VIEW_ACTIVITY_EXTRA_URL)) { WebViewFragment webViewFragment = (WebViewFragment) getFragmentManager().findFragmentByTag("webViewFragment"); webViewFragment.loadUrl((String) args.get(WebViewActivity.WEB_VIEW_ACTIVITY_EXTRA_URL), true); webViewFragment.setToolbarVisible(true); webViewFragment.setCloseAction(() -> { - if (webSlidingDrawer.isOpened()) { - webSlidingDrawer.animateClose(); + if (mWebSlidingDrawer.isOpened()) { + mWebSlidingDrawer.animateClose(); } - webSlidingDrawer.setVisibility(View.GONE); + hideWebDrawer(); }); } }); @@ -335,6 +354,9 @@ public class InterfaceActivity extends QtActivity implements WebViewFragment.OnW if (nativeEnterBackgroundCallEnqueued) { nativeEnterBackground(); } + runOnUiThread(() -> { + nativeInitAfterAppLoaded(); + }); } public void performHapticFeedback(int duration) { @@ -361,4 +383,7 @@ public class InterfaceActivity extends QtActivity implements WebViewFragment.OnW public void onExpand() { keepInterfaceRunning = true; } + + @Override + public void onOAuthAuthorizeCallback(Uri uri) { } } diff --git a/android/app/src/main/java/io/highfidelity/hifiinterface/LoginMenuActivity.java b/android/app/src/main/java/io/highfidelity/hifiinterface/LoginMenuActivity.java new file mode 100644 index 0000000000..5cb196249d --- /dev/null +++ b/android/app/src/main/java/io/highfidelity/hifiinterface/LoginMenuActivity.java @@ -0,0 +1,210 @@ +package io.highfidelity.hifiinterface; + + +import android.app.Fragment; +import android.app.FragmentManager; +import android.app.FragmentTransaction; +import android.content.Intent; +import android.os.Bundle; +import android.support.v7.app.AppCompatActivity; +import android.view.View; +import io.highfidelity.hifiinterface.fragment.LoginFragment; +import io.highfidelity.hifiinterface.fragment.OnBackPressedListener; +import io.highfidelity.hifiinterface.fragment.SignupFragment; +import io.highfidelity.hifiinterface.fragment.StartMenuFragment; + +public class LoginMenuActivity extends AppCompatActivity + implements StartMenuFragment.StartMenuInteractionListener, + LoginFragment.OnLoginInteractionListener, + SignupFragment.OnSignupInteractionListener { + + /** + * Set EXTRA_FINISH_ON_BACK to finish the app when back button is pressed + */ + public static final String EXTRA_FINISH_ON_BACK = "finishOnBack"; + + /** + * Set EXTRA_BACK_TO_SCENE to back to the scene + */ + public static final String EXTRA_BACK_TO_SCENE = "backToScene"; + + /** + * Set EXTRA_BACK_ON_SKIP to finish this activity when skip button is pressed + */ + public static final String EXTRA_BACK_ON_SKIP = "backOnSkip"; + + public static final String EXTRA_DOMAIN_URL = "url"; + + private boolean finishOnBack; + private boolean backToScene; + private boolean backOnSkip; + private String domainUrlToBack; + + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_encourage_login); + + finishOnBack = getIntent().getBooleanExtra(EXTRA_FINISH_ON_BACK, false); + backToScene = getIntent().getBooleanExtra(EXTRA_BACK_TO_SCENE, false); + domainUrlToBack = getIntent().getStringExtra(EXTRA_DOMAIN_URL); + backOnSkip = getIntent().getBooleanExtra(EXTRA_BACK_ON_SKIP, false); + + if (savedInstanceState != null) { + finishOnBack = savedInstanceState.getBoolean(EXTRA_FINISH_ON_BACK, false); + backToScene = savedInstanceState.getBoolean(EXTRA_BACK_TO_SCENE, false); + backOnSkip = savedInstanceState.getBoolean(EXTRA_BACK_ON_SKIP, false); + domainUrlToBack = savedInstanceState.getString(EXTRA_DOMAIN_URL); + } + + loadMenuFragment(); + } + + @Override + protected void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + outState.putBoolean(EXTRA_FINISH_ON_BACK, finishOnBack); + outState.putBoolean(EXTRA_BACK_TO_SCENE, backToScene); + outState.putString(EXTRA_DOMAIN_URL, domainUrlToBack); + } + + @Override + protected void onRestoreInstanceState(Bundle savedInstanceState) { + super.onRestoreInstanceState(savedInstanceState); + finishOnBack = savedInstanceState.getBoolean(EXTRA_FINISH_ON_BACK, false); + backToScene = savedInstanceState.getBoolean(EXTRA_BACK_TO_SCENE, false); + backOnSkip = savedInstanceState.getBoolean(EXTRA_BACK_ON_SKIP, false); + domainUrlToBack = savedInstanceState.getString(EXTRA_DOMAIN_URL); + } + + private void loadMenuFragment() { + FragmentManager fragmentManager = getFragmentManager(); + FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction(); + Fragment fragment = StartMenuFragment.newInstance(); + fragmentTransaction.replace(R.id.content_frame, fragment); + fragmentTransaction.addToBackStack(fragment.toString()); + fragmentTransaction.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN); + fragmentTransaction.commit(); + hideStatusBar(); + } + + @Override + protected void onResume() { + super.onResume(); + hideStatusBar(); + } + + private void hideStatusBar() { + View decorView = getWindow().getDecorView(); + // Hide the status bar. + int uiOptions = View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN; + decorView.setSystemUiVisibility(uiOptions); + } + + @Override + public void onSignupButtonClicked() { + loadSignupFragment(); + } + + @Override + public void onLoginButtonClicked() { + loadLoginFragment(false); + } + + @Override + public void onSkipLoginClicked() { + if (backOnSkip) { + onBackPressed(); + } else { + loadMainActivity(); + } + } + + @Override + public void onSteamLoginButtonClicked() { + loadLoginFragment(true); + } + + private void loadSignupFragment() { + FragmentManager fragmentManager = getFragmentManager(); + FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction(); + Fragment fragment = SignupFragment.newInstance(); + String tag = getString(R.string.tagFragmentSignup); + fragmentTransaction.replace(R.id.content_frame, fragment, tag); + fragmentTransaction.addToBackStack(tag); + fragmentTransaction.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN); + fragmentTransaction.commit(); + hideStatusBar(); + } + + private void loadLoginFragment(boolean useOauth) { + FragmentManager fragmentManager = getFragmentManager(); + FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction(); + Fragment fragment = LoginFragment.newInstance(useOauth); + String tag = getString(R.string.tagFragmentLogin); + fragmentTransaction.replace(R.id.content_frame, fragment, tag); + fragmentTransaction.addToBackStack(tag); + fragmentTransaction.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN); + fragmentTransaction.commit(); + hideStatusBar(); + } + + @Override + public void onLoginCompleted() { + loadMainActivity(); + } + + @Override + public void onCancelLogin() { + getFragmentManager().popBackStack(); + } + + @Override + public void onCancelSignup() { + getFragmentManager().popBackStack(); + } + + private void loadMainActivity() { + finish(); + if (backToScene) { + backToScene = false; + goToDomain(domainUrlToBack != null? domainUrlToBack : ""); + } else { + startActivity(new Intent(this, MainActivity.class)); + } + } + + private void goToDomain(String domainUrl) { + Intent intent = new Intent(this, InterfaceActivity.class); + intent.putExtra(InterfaceActivity.DOMAIN_URL, domainUrl); + finish(); + intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP); + startActivity(intent); + } + + + @Override + public void onSignupCompleted() { + loadMainActivity(); + } + + @Override + public void onBackPressed() { + FragmentManager fm = getFragmentManager(); + int index = fm.getBackStackEntryCount() - 1; + if (index > 0) { + FragmentManager.BackStackEntry backEntry = fm.getBackStackEntryAt(index); + String tag = backEntry.getName(); + Fragment topFragment = getFragmentManager().findFragmentByTag(tag); + if (!(topFragment instanceof OnBackPressedListener) || + !((OnBackPressedListener) topFragment).doBack()) { + super.onBackPressed(); + } + } else if (finishOnBack){ + finishAffinity(); + } else { + finish(); + } + } +} diff --git a/android/app/src/main/java/io/highfidelity/hifiinterface/MainActivity.java b/android/app/src/main/java/io/highfidelity/hifiinterface/MainActivity.java index db6f0fca24..e17b530f1c 100644 --- a/android/app/src/main/java/io/highfidelity/hifiinterface/MainActivity.java +++ b/android/app/src/main/java/io/highfidelity/hifiinterface/MainActivity.java @@ -1,5 +1,6 @@ package io.highfidelity.hifiinterface; +import android.app.Activity; import android.app.Fragment; import android.app.FragmentManager; import android.app.FragmentTransaction; @@ -31,12 +32,12 @@ import com.squareup.picasso.Picasso; import io.highfidelity.hifiinterface.fragment.FriendsFragment; import io.highfidelity.hifiinterface.fragment.HomeFragment; -import io.highfidelity.hifiinterface.fragment.LoginFragment; import io.highfidelity.hifiinterface.fragment.PolicyFragment; +import io.highfidelity.hifiinterface.fragment.SettingsFragment; +import io.highfidelity.hifiinterface.fragment.SignupFragment; import io.highfidelity.hifiinterface.task.DownloadProfileImageTask; public class MainActivity extends AppCompatActivity implements NavigationView.OnNavigationItemSelectedListener, - LoginFragment.OnLoginInteractionListener, HomeFragment.OnHomeInteractionListener, FriendsFragment.OnHomeInteractionListener { @@ -44,12 +45,13 @@ public class MainActivity extends AppCompatActivity implements NavigationView.On public static final String DEFAULT_FRAGMENT = "Home"; public static final String EXTRA_FRAGMENT = "fragment"; public static final String EXTRA_BACK_TO_SCENE = "backToScene"; + public static final String EXTRA_BACK_TO_URL = "url"; private String TAG = "HighFidelity"; - public native boolean nativeIsLoggedIn(); - public native void nativeLogout(); - public native String nativeGetDisplayName(); + public native void logout(); + public native void setUsernameChangedListener(Activity usernameChangedListener); + public native String getUsername(); private DrawerLayout mDrawerLayout; private NavigationView mNavigationView; @@ -61,6 +63,7 @@ public class MainActivity extends AppCompatActivity implements NavigationView.On private MenuItem mPeopleMenuItem; private boolean backToScene; + private String backToUrl; @Override protected void onCreate(Bundle savedInstanceState) { @@ -80,6 +83,8 @@ public class MainActivity extends AppCompatActivity implements NavigationView.On mPeopleMenuItem = mNavigationView.getMenu().findItem(R.id.action_people); + updateDebugMenu(mNavigationView.getMenu()); + Toolbar toolbar = findViewById(R.id.toolbar); toolbar.setTitleTextAppearance(this, R.style.HomeActionBarTitleStyle); setSupportActionBar(toolbar); @@ -102,17 +107,23 @@ public class MainActivity extends AppCompatActivity implements NavigationView.On loadFragment(DEFAULT_FRAGMENT); } - if (getIntent().hasExtra(EXTRA_BACK_TO_SCENE)) { - backToScene = getIntent().getBooleanExtra(EXTRA_BACK_TO_SCENE, false); + backToScene = getIntent().getBooleanExtra(EXTRA_BACK_TO_SCENE, false); + backToUrl = getIntent().getStringExtra(EXTRA_BACK_TO_URL); + } + } + + private void updateDebugMenu(Menu menu) { + if (BuildConfig.DEBUG) { + for (int i=0; i < menu.size(); i++) { + if (menu.getItem(i).getItemId() == R.id.action_debug_settings) { + menu.getItem(i).setVisible(true); + } } } } private void loadFragment(String fragment) { switch (fragment) { - case "Login": - loadLoginFragment(); - break; case "Home": loadHomeFragment(true); break; @@ -130,28 +141,35 @@ public class MainActivity extends AppCompatActivity implements NavigationView.On private void loadHomeFragment(boolean addToBackStack) { Fragment fragment = HomeFragment.newInstance(); - loadFragment(fragment, getString(R.string.home), getString(R.string.tagFragmentHome), addToBackStack); + loadFragment(fragment, getString(R.string.home), getString(R.string.tagFragmentHome), addToBackStack, true); } - private void loadLoginFragment() { - Fragment fragment = LoginFragment.newInstance(); - - loadFragment(fragment, getString(R.string.login), getString(R.string.tagFragmentLogin), true); + private void startLoginMenuActivity() { + Intent intent = new Intent(this, LoginMenuActivity.class); + intent.putExtra(LoginMenuActivity.EXTRA_BACK_ON_SKIP, true); + startActivity(intent); } private void loadPrivacyPolicyFragment() { Fragment fragment = PolicyFragment.newInstance(); - loadFragment(fragment, getString(R.string.privacyPolicy), getString(R.string.tagFragmentPolicy), true); + loadFragment(fragment, getString(R.string.privacyPolicy), getString(R.string.tagFragmentPolicy), true, true); } private void loadPeopleFragment() { Fragment fragment = FriendsFragment.newInstance(); - loadFragment(fragment, getString(R.string.people), getString(R.string.tagFragmentPeople), true); + loadFragment(fragment, getString(R.string.people), getString(R.string.tagFragmentPeople), true, true); } - private void loadFragment(Fragment fragment, String title, String tag, boolean addToBackStack) { + private void loadSettingsFragment() { + SettingsFragment fragment = SettingsFragment.newInstance(); + + loadFragment(fragment, getString(R.string.settings), getString(R.string.tagSettings), true, true); + } + + + private void loadFragment(Fragment newFragment, String title, String tag, boolean addToBackStack, boolean goBackUntilHome) { FragmentManager fragmentManager = getFragmentManager(); // check if it's the same fragment @@ -163,17 +181,19 @@ public class MainActivity extends AppCompatActivity implements NavigationView.On return; // cancel as we are already in that fragment } - // go back until first transaction - int backStackEntryCount = fragmentManager.getBackStackEntryCount(); - for (int i = 0; i < backStackEntryCount - 1; i++) { - fragmentManager.popBackStackImmediate(); + if (goBackUntilHome) { + // go back until first transaction + int backStackEntryCount = fragmentManager.getBackStackEntryCount(); + for (int i = 0; i < backStackEntryCount - 1; i++) { + fragmentManager.popBackStackImmediate(); + } } // this case is when we wanted to go home.. rollback already did that! // But asking for a new Home fragment makes it easier to have an updated list so we let it to continue FragmentTransaction ft = fragmentManager.beginTransaction(); - ft.replace(R.id.content_frame, fragment, tag); + ft.replace(R.id.content_frame, newFragment, tag); if (addToBackStack) { ft.addToBackStack(title); @@ -185,7 +205,7 @@ public class MainActivity extends AppCompatActivity implements NavigationView.On private void updateLoginMenu() { - if (nativeIsLoggedIn()) { + if (HifiUtils.getInstance().isUserLoggedIn()) { mLoginPanel.setVisibility(View.GONE); mProfilePanel.setVisibility(View.VISIBLE); mLogoutOption.setVisibility(View.VISIBLE); @@ -201,7 +221,7 @@ public class MainActivity extends AppCompatActivity implements NavigationView.On } private void updateProfileHeader() { - updateProfileHeader(nativeGetDisplayName()); + updateProfileHeader(getUsername()); } private void updateProfileHeader(String username) { if (!username.isEmpty()) { @@ -241,6 +261,9 @@ public class MainActivity extends AppCompatActivity implements NavigationView.On case R.id.action_people: loadPeopleFragment(); return true; + case R.id.action_debug_settings: + loadSettingsFragment(); + return true; } return false; } @@ -248,15 +271,22 @@ public class MainActivity extends AppCompatActivity implements NavigationView.On @Override protected void onStart() { super.onStart(); + setUsernameChangedListener(this); updateLoginMenu(); } + @Override + protected void onStop() { + super.onStop(); + setUsernameChangedListener(null); + } + public void onLoginClicked(View view) { - loadLoginFragment(); + startLoginMenuActivity(); } public void onLogoutClicked(View view) { - nativeLogout(); + logout(); updateLoginMenu(); exitLoggedInFragment(); @@ -278,7 +308,7 @@ public class MainActivity extends AppCompatActivity implements NavigationView.On } private void goToLastLocation() { - goToDomain(""); + goToDomain(backToUrl != null? backToUrl : ""); } private void goToDomain(String domainUrl) { @@ -297,16 +327,6 @@ public class MainActivity extends AppCompatActivity implements NavigationView.On startActivity(intent); } - @Override - public void onLoginCompleted() { - loadHomeFragment(false); - updateLoginMenu(); - if (backToScene) { - backToScene = false; - goToLastLocation(); - } - } - public void handleUsernameChanged(String username) { runOnUiThread(() -> updateProfileHeader(username)); } @@ -351,7 +371,6 @@ public class MainActivity extends AppCompatActivity implements NavigationView.On public void onBackPressed() { // if a fragment needs to internally manage back presses.. FragmentManager fm = getFragmentManager(); - Log.d("[BACK]", "getBackStackEntryCount " + fm.getBackStackEntryCount()); Fragment friendsFragment = fm.findFragmentByTag(getString(R.string.tagFragmentPeople)); if (friendsFragment != null && friendsFragment instanceof FriendsFragment) { if (((FriendsFragment) friendsFragment).onBackPressed()) { diff --git a/android/app/src/main/java/io/highfidelity/hifiinterface/SplashActivity.java b/android/app/src/main/java/io/highfidelity/hifiinterface/SplashActivity.java index e0aa967aaa..bb42467ace 100644 --- a/android/app/src/main/java/io/highfidelity/hifiinterface/SplashActivity.java +++ b/android/app/src/main/java/io/highfidelity/hifiinterface/SplashActivity.java @@ -3,7 +3,6 @@ package io.highfidelity.hifiinterface; import android.app.Activity; import android.content.Intent; import android.os.Bundle; -import android.os.Handler; import android.view.View; public class SplashActivity extends Activity { @@ -37,7 +36,13 @@ public class SplashActivity extends Activity { } public void onAppLoadedComplete() { - startActivity(new Intent(this, MainActivity.class)); + if (HifiUtils.getInstance().isUserLoggedIn()) { + startActivity(new Intent(this, MainActivity.class)); + } else { + Intent menuIntent = new Intent(this, LoginMenuActivity.class); + menuIntent.putExtra(LoginMenuActivity.EXTRA_FINISH_ON_BACK, true); + startActivity(menuIntent); + } SplashActivity.this.finish(); } } diff --git a/android/app/src/main/java/io/highfidelity/hifiinterface/WebViewActivity.java b/android/app/src/main/java/io/highfidelity/hifiinterface/WebViewActivity.java index 5d65bcad51..e906c4b734 100644 --- a/android/app/src/main/java/io/highfidelity/hifiinterface/WebViewActivity.java +++ b/android/app/src/main/java/io/highfidelity/hifiinterface/WebViewActivity.java @@ -28,11 +28,18 @@ import java.net.MalformedURLException; import java.net.URL; import io.highfidelity.hifiinterface.fragment.WebViewFragment; +import io.highfidelity.hifiinterface.fragment.WebViewFragment.OnWebViewInteractionListener; -public class WebViewActivity extends Activity implements WebViewFragment.OnWebViewInteractionListener { +public class WebViewActivity extends Activity implements OnWebViewInteractionListener { public static final String WEB_VIEW_ACTIVITY_EXTRA_URL = "url"; + public static final String WEB_VIEW_ACTIVITY_EXTRA_CLEAR_COOKIES = "clear_cookies"; + public static final String RESULT_OAUTH_CODE = "code"; + public static final String RESULT_OAUTH_STATE = "state"; + private static final String FRAGMENT_TAG = "WebViewActivity_WebFragment"; + private static final String OAUTH_CODE = "code"; + private static final String OAUTH_STATE = "state"; private native void nativeProcessURL(String url); @@ -47,14 +54,15 @@ public class WebViewActivity extends Activity implements WebViewFragment.OnWebVi mActionBar = getActionBar(); mActionBar.setDisplayHomeAsUpEnabled(true); - loadWebViewFragment(getIntent().getStringExtra(WEB_VIEW_ACTIVITY_EXTRA_URL)); + loadWebViewFragment(getIntent().getStringExtra(WEB_VIEW_ACTIVITY_EXTRA_URL), getIntent().getBooleanExtra(WEB_VIEW_ACTIVITY_EXTRA_CLEAR_COOKIES, false)); } - private void loadWebViewFragment(String url) { + private void loadWebViewFragment(String url, boolean clearCookies) { WebViewFragment fragment = WebViewFragment.newInstance(); Bundle bundle = new Bundle(); bundle.putString(WebViewFragment.URL, url); bundle.putBoolean(WebViewFragment.TOOLBAR_VISIBLE, false); + bundle.putBoolean(WebViewFragment.CLEAR_COOKIES, clearCookies); fragment.setArguments(bundle); FragmentManager fragmentManager = getFragmentManager(); FragmentTransaction ft = fragmentManager.beginTransaction(); @@ -131,4 +139,13 @@ public class WebViewActivity extends Activity implements WebViewFragment.OnWebVi @Override public void onExpand() { } + @Override + public void onOAuthAuthorizeCallback(Uri uri) { + Intent result = new Intent(); + result.putExtra(RESULT_OAUTH_CODE, uri.getQueryParameter(OAUTH_CODE)); + result.putExtra(RESULT_OAUTH_STATE, uri.getQueryParameter(OAUTH_STATE)); + setResult(Activity.RESULT_OK, result); + finish(); + } + } diff --git a/android/app/src/main/java/io/highfidelity/hifiinterface/fragment/FriendsFragment.java b/android/app/src/main/java/io/highfidelity/hifiinterface/fragment/FriendsFragment.java index 2a008d7950..e19a9c5a7a 100644 --- a/android/app/src/main/java/io/highfidelity/hifiinterface/fragment/FriendsFragment.java +++ b/android/app/src/main/java/io/highfidelity/hifiinterface/fragment/FriendsFragment.java @@ -23,8 +23,6 @@ import io.highfidelity.hifiinterface.view.UserListAdapter; public class FriendsFragment extends Fragment { - public native boolean nativeIsLoggedIn(); - public native String nativeGetAccessToken(); private RecyclerView mUsersView; @@ -98,13 +96,17 @@ public class FriendsFragment extends Fragment { mUsersAdapter.setListener(new UserListAdapter.AdapterListener() { @Override - public void onEmptyAdapter() { - mSwipeRefreshLayout.setRefreshing(false); + public void onEmptyAdapter(boolean shouldStopRefreshing) { + if (shouldStopRefreshing) { + mSwipeRefreshLayout.setRefreshing(false); + } } @Override - public void onNonEmptyAdapter() { - mSwipeRefreshLayout.setRefreshing(false); + public void onNonEmptyAdapter(boolean shouldStopRefreshing) { + if (shouldStopRefreshing) { + mSwipeRefreshLayout.setRefreshing(false); + } } @Override @@ -115,6 +117,8 @@ public class FriendsFragment extends Fragment { mUsersView.setAdapter(mUsersAdapter); + mUsersAdapter.startLoad(); + mSlidingUpPanelLayout.setFadeOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { diff --git a/android/app/src/main/java/io/highfidelity/hifiinterface/fragment/HomeFragment.java b/android/app/src/main/java/io/highfidelity/hifiinterface/fragment/HomeFragment.java index 7bd373cf1d..86b8625cfe 100644 --- a/android/app/src/main/java/io/highfidelity/hifiinterface/fragment/HomeFragment.java +++ b/android/app/src/main/java/io/highfidelity/hifiinterface/fragment/HomeFragment.java @@ -76,18 +76,22 @@ public class HomeFragment extends Fragment { }); mDomainAdapter.setListener(new DomainAdapter.AdapterListener() { @Override - public void onEmptyAdapter() { + public void onEmptyAdapter(boolean shouldStopRefreshing) { searchNoResultsView.setText(R.string.search_no_results); searchNoResultsView.setVisibility(View.VISIBLE); mDomainsView.setVisibility(View.GONE); - mSwipeRefreshLayout.setRefreshing(false); + if (shouldStopRefreshing) { + mSwipeRefreshLayout.setRefreshing(false); + } } @Override - public void onNonEmptyAdapter() { + public void onNonEmptyAdapter(boolean shouldStopRefreshing) { searchNoResultsView.setVisibility(View.GONE); mDomainsView.setVisibility(View.VISIBLE); - mSwipeRefreshLayout.setRefreshing(false); + if (shouldStopRefreshing) { + mSwipeRefreshLayout.setRefreshing(false); + } } @Override @@ -96,11 +100,20 @@ public class HomeFragment extends Fragment { } }); mDomainsView.setAdapter(mDomainAdapter); + mDomainAdapter.startLoad(); mSearchView = rootView.findViewById(R.id.searchView); mSearchIconView = rootView.findViewById(R.id.search_mag_icon); mClearSearch = rootView.findViewById(R.id.search_clear); + getActivity().getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN); + + return rootView; + } + + @Override + public void onStart() { + super.onStart(); mSearchView.addTextChangedListener(new TextWatcher() { @Override public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {} @@ -142,10 +155,6 @@ public class HomeFragment extends Fragment { mDomainAdapter.loadDomains(mSearchView.getText().toString(), true); } }); - - getActivity().getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN); - - return rootView; } @Override diff --git a/android/app/src/main/java/io/highfidelity/hifiinterface/fragment/LoginFragment.java b/android/app/src/main/java/io/highfidelity/hifiinterface/fragment/LoginFragment.java index f29c237ed7..28406d5986 100644 --- a/android/app/src/main/java/io/highfidelity/hifiinterface/fragment/LoginFragment.java +++ b/android/app/src/main/java/io/highfidelity/hifiinterface/fragment/LoginFragment.java @@ -2,36 +2,65 @@ package io.highfidelity.hifiinterface.fragment; import android.app.Activity; import android.app.Fragment; -import android.app.ProgressDialog; import android.content.Context; import android.content.Intent; import android.net.Uri; import android.os.Bundle; -import android.text.Editable; -import android.text.TextWatcher; +import android.support.annotation.Nullable; import android.util.Log; +import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputMethodManager; import android.widget.Button; +import android.widget.CheckBox; import android.widget.EditText; import android.widget.TextView; -import io.highfidelity.hifiinterface.R; +import org.qtproject.qt5.android.QtNative; -public class LoginFragment extends Fragment { +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.util.Random; + +import io.highfidelity.hifiinterface.BuildConfig; +import io.highfidelity.hifiinterface.HifiUtils; +import io.highfidelity.hifiinterface.R; +import io.highfidelity.hifiinterface.WebViewActivity; + +import static org.qtproject.qt5.android.QtActivityDelegate.ApplicationActive; +import static org.qtproject.qt5.android.QtActivityDelegate.ApplicationInactive; + +public class LoginFragment extends Fragment + implements OnBackPressedListener { + + private static final String ARG_USE_OAUTH = "use_oauth"; + private static final String TAG = "Interface"; + + private final String OAUTH_CLIENT_ID = BuildConfig.OAUTH_CLIENT_ID; + private final String OAUTH_REDIRECT_URI = BuildConfig.OAUTH_REDIRECT_URI; + private final String OAUTH_AUTHORIZE_BASE_URL = "https://highfidelity.com/oauth/authorize"; + private static final int OAUTH_AUTHORIZE_REQUEST = 1; private EditText mUsername; private EditText mPassword; private TextView mError; - private TextView mForgotPassword; private Button mLoginButton; + private CheckBox mKeepMeLoggedInCheckbox; + private ViewGroup mLoginForm; + private ViewGroup mLoggingInFrame; + private ViewGroup mLoggedInFrame; + private boolean mLoginInProgress; + private boolean mLoginSuccess; + private boolean mUseOauth; + private String mOauthState; - private ProgressDialog mDialog; + public native void login(String username, String password, boolean keepLoggedIn); + private native void retrieveAccessToken(String authCode, String clientId, String clientSecret, String redirectUri); - public native void nativeLogin(String username, String password, Activity usernameChangedListener); + public native void cancelLogin(); private LoginFragment.OnLoginInteractionListener mListener; @@ -39,11 +68,22 @@ public class LoginFragment extends Fragment { // Required empty public constructor } - public static LoginFragment newInstance() { + public static LoginFragment newInstance(boolean useOauth) { LoginFragment fragment = new LoginFragment(); + Bundle args = new Bundle(); + args.putBoolean(ARG_USE_OAUTH, useOauth); + fragment.setArguments(args); return fragment; } + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + if (getArguments() != null) { + mUseOauth = getArguments().getBoolean(ARG_USE_OAUTH, false); + } + } + @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { @@ -53,58 +93,29 @@ public class LoginFragment extends Fragment { mPassword = rootView.findViewById(R.id.password); mError = rootView.findViewById(R.id.error); mLoginButton = rootView.findViewById(R.id.loginButton); - mForgotPassword = rootView.findViewById(R.id.forgotPassword); + mLoginForm = rootView.findViewById(R.id.loginForm); + mLoggingInFrame = rootView.findViewById(R.id.loggingInFrame); + mLoggedInFrame = rootView.findViewById(R.id.loggedInFrame); + mKeepMeLoggedInCheckbox = rootView.findViewById(R.id.keepMeLoggedIn); - mUsername.addTextChangedListener(new TextWatcher() { - boolean ignoreNextChange = false; - boolean hadBlankSpace = false; - @Override - public void beforeTextChanged(CharSequence charSequence, int start, int count, int after) { - hadBlankSpace = charSequence.length() > 0 && charSequence.charAt(charSequence.length()-1) == ' '; - } + rootView.findViewById(R.id.forgotPassword).setOnClickListener(view -> onForgotPasswordClicked()); - @Override - public void onTextChanged(CharSequence charSequence, int start, int count, int after) { + rootView.findViewById(R.id.cancel).setOnClickListener(view -> onCancelLogin()); - } + rootView.findViewById(R.id.getStarted).setOnClickListener(view -> onGetStartedClicked()); - @Override - public void afterTextChanged(Editable editable) { - if (!ignoreNextChange) { - ignoreNextChange = true; - boolean spaceFound = false; - for (int i = 0; i < editable.length(); i++) { - if (editable.charAt(i) == ' ') { - spaceFound=true; - editable.delete(i, i + 1); - i--; - } - } + mLoginButton.setOnClickListener(view -> onLoginButtonClicked()); - if (hadBlankSpace && !spaceFound && editable.length() > 0) { - editable.delete(editable.length()-1, editable.length()); - } + rootView.findViewById(R.id.takeMeInWorld).setOnClickListener(view -> skipLogin()); + mPassword.setOnEditorActionListener((textView, actionId, keyEvent) -> onPasswordEditorAction(textView, actionId, keyEvent)); - editable.append(' '); - ignoreNextChange = false; - } + mKeepMeLoggedInCheckbox.setChecked(HifiUtils.getInstance().isKeepingLoggedIn()); - } - }); - - - mLoginButton.setOnClickListener(view -> login()); - - mForgotPassword.setOnClickListener(view -> forgotPassword()); - - mPassword.setOnEditorActionListener( - (textView, actionId, keyEvent) -> { - if (actionId == EditorInfo.IME_ACTION_DONE) { - mLoginButton.performClick(); - return true; - } - return false; - }); + if (mUseOauth) { + openWebForAuthorization(); + } else { + showLoginForm(); + } return rootView; } @@ -125,14 +136,67 @@ public class LoginFragment extends Fragment { mListener = null; } + @Override + public void onResume() { + super.onResume(); + // This hack intends to keep Qt threads running even after the app comes from background + QtNative.setApplicationState(ApplicationActive); + } + @Override public void onStop() { super.onStop(); - cancelActivityIndicator(); + // Leave the Qt app paused + QtNative.setApplicationState(ApplicationInactive); hideKeyboard(); } - public void login() { + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + if (requestCode == OAUTH_AUTHORIZE_REQUEST) { + if (resultCode == Activity.RESULT_OK) { + String authCode = data.getStringExtra(WebViewActivity.RESULT_OAUTH_CODE); + String state = data.getStringExtra(WebViewActivity.RESULT_OAUTH_STATE); + if (state != null && state.equals(mOauthState) && mListener != null) { + mOauthState = null; + showActivityIndicator(); + mLoginInProgress = true; + retrieveAccessToken(authCode, BuildConfig.OAUTH_CLIENT_ID, BuildConfig.OAUTH_CLIENT_SECRET, BuildConfig.OAUTH_REDIRECT_URI); + } + } else { + onCancelLogin(); + } + } + + } + + private void onCancelLogin() { + if (mListener != null) { + mListener.onCancelLogin(); + } + } + + private boolean onPasswordEditorAction(TextView textView, int actionId, KeyEvent keyEvent) { + if (actionId == EditorInfo.IME_ACTION_DONE) { + mLoginButton.performClick(); + return true; + } + return false; + } + + private void skipLogin() { + if (mListener != null) { + mListener.onSkipLoginClicked(); + } + } + + private void onGetStartedClicked() { + if (mListener != null) { + mListener.onLoginCompleted(); + } + } + + public void onLoginButtonClicked() { String username = mUsername.getText().toString().trim(); String password = mPassword.getText().toString(); hideKeyboard(); @@ -142,7 +206,10 @@ public class LoginFragment extends Fragment { mLoginButton.setEnabled(false); hideError(); showActivityIndicator(); - nativeLogin(username, password, getActivity()); + mLoginInProgress = true; + mLoginSuccess = false; + boolean keepUserLoggedIn = mKeepMeLoggedInCheckbox.isChecked(); + login(username, password, keepUserLoggedIn); } } @@ -154,25 +221,32 @@ public class LoginFragment extends Fragment { } } - private void forgotPassword() { + private void onForgotPasswordClicked() { Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse("https://highfidelity.com/users/password/new")); startActivity(intent); } private void showActivityIndicator() { - if (mDialog == null) { - mDialog = new ProgressDialog(getContext()); - } - mDialog.setMessage(getString(R.string.logging_in)); - mDialog.setCancelable(false); - mDialog.show(); + mLoginForm.setVisibility(View.GONE); + mLoggedInFrame.setVisibility(View.GONE); + mLoggingInFrame.setVisibility(View.VISIBLE); + mLoggingInFrame.bringToFront(); } - private void cancelActivityIndicator() { - if (mDialog != null) { - mDialog.cancel(); - } + private void showLoginForm() { + mLoggingInFrame.setVisibility(View.GONE); + mLoggedInFrame.setVisibility(View.GONE); + mLoginForm.setVisibility(View.VISIBLE); + mLoginForm.bringToFront(); } + + private void showLoggedInMessage() { + mLoginForm.setVisibility(View.GONE); + mLoggingInFrame.setVisibility(View.GONE); + mLoggedInFrame.setVisibility(View.VISIBLE); + mLoggedInFrame.bringToFront(); + } + private void showError(String error) { mError.setText(error); mError.setVisibility(View.VISIBLE); @@ -184,22 +258,71 @@ public class LoginFragment extends Fragment { } public void handleLoginCompleted(boolean success) { - Log.d("[LOGIN]", "handleLoginCompleted " + success); + mLoginInProgress = false; getActivity().runOnUiThread(() -> { mLoginButton.setEnabled(true); - cancelActivityIndicator(); if (success) { - if (mListener != null) { - mListener.onLoginCompleted(); - } + mLoginSuccess = true; + showLoggedInMessage(); } else { - showError(getString(R.string.login_username_or_password_incorrect)); + if (!mUseOauth) { + showLoginForm(); + showError(getString(R.string.login_username_or_password_incorrect)); + } else { + openWebForAuthorization(); + } } }); } + @Override + public boolean doBack() { + if (mLoginInProgress) { + cancelLogin(); + showLoginForm(); + mLoginInProgress = false; + mLoginButton.setEnabled(true); + return true; + } else if (mLoginSuccess) { + onGetStartedClicked(); + return true; + } else { + return false; + } + } + + private void updateOauthState() { + // as we only use oauth for steam that's ok for now + mOauthState = "steam-" + Long.toString(new Random().nextLong()); + } + + private String buildAuthorizeUrl() { + StringBuilder sb = new StringBuilder(OAUTH_AUTHORIZE_BASE_URL); + sb.append("?client_id=").append(OAUTH_CLIENT_ID); + try { + String redirectUri = URLEncoder.encode(OAUTH_REDIRECT_URI, "utf-8"); + sb.append("&redirect_uri=").append(redirectUri); + } catch (UnsupportedEncodingException e) { + Log.e(TAG, "Cannot build oauth autorization url", e); + } + sb.append("&response_type=code&scope=owner"); + sb.append("&state=").append(mOauthState); + return sb.toString(); + } + + private void openWebForAuthorization() { + Intent openUrlIntent = new Intent(getActivity(), WebViewActivity.class); + updateOauthState(); + openUrlIntent.putExtra(WebViewActivity.WEB_VIEW_ACTIVITY_EXTRA_URL, buildAuthorizeUrl()); + openUrlIntent.putExtra(WebViewActivity.WEB_VIEW_ACTIVITY_EXTRA_CLEAR_COOKIES, true); + startActivityForResult(openUrlIntent, OAUTH_AUTHORIZE_REQUEST); + } + + public interface OnLoginInteractionListener { void onLoginCompleted(); + void onCancelLogin(); + void onSkipLoginClicked(); } } diff --git a/android/app/src/main/java/io/highfidelity/hifiinterface/fragment/OnBackPressedListener.java b/android/app/src/main/java/io/highfidelity/hifiinterface/fragment/OnBackPressedListener.java new file mode 100644 index 0000000000..c160138cea --- /dev/null +++ b/android/app/src/main/java/io/highfidelity/hifiinterface/fragment/OnBackPressedListener.java @@ -0,0 +1,11 @@ +package io.highfidelity.hifiinterface.fragment; + +public interface OnBackPressedListener { + + /** + * Processes the back pressed event and returns true if it was managed by this Fragment + * @return + */ + boolean doBack(); + +} diff --git a/android/app/src/main/java/io/highfidelity/hifiinterface/fragment/SettingsFragment.java b/android/app/src/main/java/io/highfidelity/hifiinterface/fragment/SettingsFragment.java new file mode 100644 index 0000000000..e32dc3b996 --- /dev/null +++ b/android/app/src/main/java/io/highfidelity/hifiinterface/fragment/SettingsFragment.java @@ -0,0 +1,64 @@ +package io.highfidelity.hifiinterface.fragment; + +import android.content.SharedPreferences; +import android.media.audiofx.AcousticEchoCanceler; +import android.os.Bundle; +import android.preference.PreferenceFragment; +import android.preference.PreferenceManager; +import android.support.annotation.Nullable; + +import io.highfidelity.hifiinterface.HifiUtils; +import io.highfidelity.hifiinterface.R; + +public class SettingsFragment extends PreferenceFragment implements SharedPreferences.OnSharedPreferenceChangeListener { + + private final String HIFI_SETTINGS_ANDROID_GROUP = "Android"; + private final String HIFI_SETTINGS_AEC_KEY = "aec"; + private final String PREFERENCE_KEY_AEC = "aec"; + + private final boolean DEFAULT_AEC_ENABLED = true; + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + addPreferencesFromResource(R.xml.settings); + boolean aecAvailable = AcousticEchoCanceler.isAvailable(); + PreferenceManager.setDefaultValues(getContext(), R.xml.settings, false); + + if (!aecAvailable) { + findPreference(PREFERENCE_KEY_AEC).setEnabled(false); + HifiUtils.getInstance().updateHifiSetting(HIFI_SETTINGS_ANDROID_GROUP, HIFI_SETTINGS_AEC_KEY, false); + } + + getPreferenceScreen().getSharedPreferences().edit().putBoolean(PREFERENCE_KEY_AEC, + aecAvailable && HifiUtils.getInstance().getHifiSettingBoolean(HIFI_SETTINGS_ANDROID_GROUP, HIFI_SETTINGS_AEC_KEY, DEFAULT_AEC_ENABLED)).commit(); + } + + public static SettingsFragment newInstance() { + SettingsFragment fragment = new SettingsFragment(); + return fragment; + } + + @Override + public void onResume() { + super.onResume(); + getPreferenceScreen().getSharedPreferences().registerOnSharedPreferenceChangeListener(this); + } + + @Override + public void onPause() { + super.onPause(); + getPreferenceScreen().getSharedPreferences().unregisterOnSharedPreferenceChangeListener(this); + } + + @Override + public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { + switch (key) { + case "aec": + HifiUtils.getInstance().updateHifiSetting(HIFI_SETTINGS_ANDROID_GROUP, HIFI_SETTINGS_AEC_KEY, sharedPreferences.getBoolean(key, false)); + break; + default: + break; + } + } +} diff --git a/android/app/src/main/java/io/highfidelity/hifiinterface/fragment/SignupFragment.java b/android/app/src/main/java/io/highfidelity/hifiinterface/fragment/SignupFragment.java new file mode 100644 index 0000000000..a11ae31fa1 --- /dev/null +++ b/android/app/src/main/java/io/highfidelity/hifiinterface/fragment/SignupFragment.java @@ -0,0 +1,260 @@ +package io.highfidelity.hifiinterface.fragment; + +import android.app.Fragment; +import android.content.Context; +import android.os.Bundle; +import android.view.KeyEvent; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.inputmethod.EditorInfo; +import android.view.inputmethod.InputMethodManager; +import android.widget.Button; +import android.widget.CheckBox; +import android.widget.EditText; +import android.widget.TextView; + +import org.qtproject.qt5.android.QtNative; + +import io.highfidelity.hifiinterface.HifiUtils; +import io.highfidelity.hifiinterface.R; + +import static org.qtproject.qt5.android.QtActivityDelegate.ApplicationActive; +import static org.qtproject.qt5.android.QtActivityDelegate.ApplicationInactive; + +public class SignupFragment extends Fragment + implements OnBackPressedListener { + + private EditText mEmail; + private EditText mUsername; + private EditText mPassword; + private TextView mError; + private TextView mActivityText; + private Button mSignupButton; + private CheckBox mKeepMeLoggedInCheckbox; + + private ViewGroup mSignupForm; + private ViewGroup mLoggingInFrame; + private ViewGroup mLoggedInFrame; + + private boolean mLoginInProgress; + private boolean mSignupInProgress; + private boolean mSignupSuccess; + + public native void signup(String email, String username, String password); // move to SignupFragment + public native void cancelSignup(); + public native void login(String username, String password, boolean keepLoggedIn); + public native void cancelLogin(); + + private SignupFragment.OnSignupInteractionListener mListener; + + public SignupFragment() { + // Required empty public constructor + } + + public static SignupFragment newInstance() { + SignupFragment fragment = new SignupFragment(); + return fragment; + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + View rootView = inflater.inflate(R.layout.fragment_signup, container, false); + + mEmail = rootView.findViewById(R.id.email); + mUsername = rootView.findViewById(R.id.username); + mPassword = rootView.findViewById(R.id.password); + mError = rootView.findViewById(R.id.error); + mSignupButton = rootView.findViewById(R.id.signupButton); + mActivityText = rootView.findViewById(R.id.activityText); + mKeepMeLoggedInCheckbox = rootView.findViewById(R.id.keepMeLoggedIn); + + mSignupForm = rootView.findViewById(R.id.signupForm); + mLoggedInFrame = rootView.findViewById(R.id.loggedInFrame); + mLoggingInFrame = rootView.findViewById(R.id.loggingInFrame); + + rootView.findViewById(R.id.cancel).setOnClickListener(view -> onCancelSignup()); + + mSignupButton.setOnClickListener(view -> signup()); + + rootView.findViewById(R.id.getStarted).setOnClickListener(view -> onGetStartedClicked()); + + mPassword.setOnEditorActionListener((textView, actionId, keyEvent) -> onPasswordEditorAction(textView, actionId, keyEvent)); + + mKeepMeLoggedInCheckbox.setChecked(HifiUtils.getInstance().isKeepingLoggedIn()); + + return rootView; + } + + @Override + public void onAttach(Context context) { + super.onAttach(context); + if (context instanceof OnSignupInteractionListener) { + mListener = (OnSignupInteractionListener) context; + } else { + throw new RuntimeException(context.toString() + + " must implement OnSignupInteractionListener"); + } + } + + @Override + public void onDetach() { + super.onDetach(); + mListener = null; + } + + @Override + public void onResume() { + super.onResume(); + // This hack intends to keep Qt threads running even after the app comes from background + QtNative.setApplicationState(ApplicationActive); + } + + @Override + public void onStop() { + super.onStop(); + // Leave the Qt app paused + QtNative.setApplicationState(ApplicationInactive); + hideKeyboard(); + } + + private boolean onPasswordEditorAction(TextView textView, int actionId, KeyEvent keyEvent) { + if (actionId == EditorInfo.IME_ACTION_DONE) { + mSignupButton.performClick(); + return true; + } + return false; + } + + private void onCancelSignup() { + if (mListener != null) { + mListener.onCancelSignup(); + } + } + + public void signup() { + String email = mEmail.getText().toString().trim(); + String username = mUsername.getText().toString().trim(); + String password = mPassword.getText().toString(); + hideKeyboard(); + if (email.isEmpty() || username.isEmpty() || password.isEmpty()) { + showError(getString(R.string.signup_email_username_or_password_incorrect)); + } else { + mSignupButton.setEnabled(false); + hideError(); + mActivityText.setText(R.string.creating_account); + showActivityIndicator(); + mSignupInProgress = true; + mSignupSuccess = false; + signup(email, username, password); + } + } + + private void hideKeyboard() { + View view = getActivity().getCurrentFocus(); + if (view != null) { + InputMethodManager imm = (InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE); + imm.hideSoftInputFromWindow(view.getWindowToken(), 0); + } + } + + private void showActivityIndicator() { + mSignupForm.setVisibility(View.GONE); + mLoggedInFrame.setVisibility(View.GONE); + mLoggingInFrame.setVisibility(View.VISIBLE); + } + + private void showLoggedInMessage() { + mSignupForm.setVisibility(View.GONE); + mLoggingInFrame.setVisibility(View.GONE); + mLoggedInFrame.setVisibility(View.VISIBLE); + } + + private void showSignupForm() { + mLoggingInFrame.setVisibility(View.GONE); + mLoggedInFrame.setVisibility(View.GONE); + mSignupForm.setVisibility(View.VISIBLE); + } + private void showError(String error) { + mError.setText(error); + mError.setVisibility(View.VISIBLE); + } + + private void hideError() { + mError.setText(""); + mError.setVisibility(View.INVISIBLE); + } + + public interface OnSignupInteractionListener { + void onSignupCompleted(); + void onCancelSignup(); + } + + private void onGetStartedClicked() { + if (mListener != null) { + mListener.onSignupCompleted(); + } + } + + + public void handleSignupCompleted() { + mSignupInProgress = false; + String username = mUsername.getText().toString().trim(); + String password = mPassword.getText().toString(); + getActivity().runOnUiThread(() -> { + mActivityText.setText(R.string.logging_in); + }); + mLoginInProgress = true; + boolean keepUserLoggedIn = mKeepMeLoggedInCheckbox.isChecked(); + login(username, password, keepUserLoggedIn); + } + + public void handleSignupFailed(String error) { + mSignupInProgress = false; + getActivity().runOnUiThread(() -> { + mSignupButton.setEnabled(true); + showSignupForm(); + mError.setText(error); + mError.setVisibility(View.VISIBLE); + }); + } + + public void handleLoginCompleted(boolean success) { + mLoginInProgress = false; + getActivity().runOnUiThread(() -> { + mSignupButton.setEnabled(true); + if (success) { + mSignupSuccess = true; + showLoggedInMessage(); + } else { + // Registration was successful but login failed. + // Let the user to login manually + mListener.onCancelSignup(); + showSignupForm(); + } + }); + } + + @Override + public boolean doBack() { + if (mSignupInProgress) { + cancelSignup(); + } else if (mLoginInProgress) { + cancelLogin(); + } + + if (mSignupInProgress || mLoginInProgress) { + showSignupForm(); + mLoginInProgress = false; + mSignupInProgress = false; + mSignupButton.setEnabled(true); + return true; + } else if (mSignupSuccess) { + onGetStartedClicked(); + return true; + } else { + return false; + } + } +} diff --git a/android/app/src/main/java/io/highfidelity/hifiinterface/fragment/StartMenuFragment.java b/android/app/src/main/java/io/highfidelity/hifiinterface/fragment/StartMenuFragment.java new file mode 100644 index 0000000000..fe77991962 --- /dev/null +++ b/android/app/src/main/java/io/highfidelity/hifiinterface/fragment/StartMenuFragment.java @@ -0,0 +1,93 @@ +package io.highfidelity.hifiinterface.fragment; + +import android.app.Fragment; +import android.content.Context; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import io.highfidelity.hifiinterface.R; + +public class StartMenuFragment extends Fragment { + + private String TAG = "HighFidelity"; + private StartMenuInteractionListener mListener; + + public StartMenuFragment() { + // Required empty public constructor + } + + public static StartMenuFragment newInstance() { + StartMenuFragment fragment = new StartMenuFragment(); + return fragment; + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + // Inflate the layout for this fragment + View rootView = inflater.inflate(R.layout.fragment_login_menu, container, false); + rootView.findViewById(R.id.signupButton).setOnClickListener(view -> { + if (mListener != null) { + mListener.onSignupButtonClicked(); + } + }); + + rootView.findViewById(R.id.loginButton).setOnClickListener(view -> { + if (mListener != null) { + mListener.onLoginButtonClicked(); + } + }); + + rootView.findViewById(R.id.steamLoginButton).setOnClickListener(view -> { + if (mListener != null) { + mListener.onSteamLoginButtonClicked(); + } + }); + + rootView.findViewById(R.id.takeMeInWorld).setOnClickListener(view -> { + if (mListener != null) { + mListener.onSkipLoginClicked(); + } + }); + + + + return rootView; + } + + @Override + public void onAttach(Context context) { + super.onAttach(context); + if (context instanceof StartMenuInteractionListener) { + mListener = (StartMenuInteractionListener) context; + } else { + throw new RuntimeException(context.toString() + + " must implement StartMenuInteractionListener"); + } + } + + @Override + public void onDetach() { + super.onDetach(); + mListener = null; + } + + /** + * This interface must be implemented by activities that contain this + * fragment to allow an interaction in this fragment to be communicated + * to the activity and potentially other fragments contained in that + * activity. + *

+ * See the Android Training lesson Communicating with Other Fragments for more information. + */ + public interface StartMenuInteractionListener { + void onSignupButtonClicked(); + void onLoginButtonClicked(); + void onSkipLoginClicked(); + void onSteamLoginButtonClicked(); + } +} diff --git a/android/app/src/main/java/io/highfidelity/hifiinterface/fragment/WebViewFragment.java b/android/app/src/main/java/io/highfidelity/hifiinterface/fragment/WebViewFragment.java index 2d887d5a19..3614fe47e4 100644 --- a/android/app/src/main/java/io/highfidelity/hifiinterface/fragment/WebViewFragment.java +++ b/android/app/src/main/java/io/highfidelity/hifiinterface/fragment/WebViewFragment.java @@ -4,9 +4,11 @@ import android.app.Fragment; import android.content.Context; import android.content.Intent; import android.graphics.Bitmap; +import android.net.Uri; import android.net.http.SslError; import android.os.Bundle; import android.os.Handler; +import android.text.TextUtils; import android.view.GestureDetector; import android.view.KeyEvent; import android.view.LayoutInflater; @@ -14,6 +16,7 @@ import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import android.view.animation.AlphaAnimation; +import android.webkit.CookieManager; import android.webkit.SslErrorHandler; import android.webkit.WebChromeClient; import android.webkit.WebResourceError; @@ -25,6 +28,7 @@ import android.webkit.WebViewClient; import android.widget.ProgressBar; import android.widget.Toast; +import io.highfidelity.hifiinterface.BuildConfig; import io.highfidelity.hifiinterface.R; import io.highfidelity.hifiinterface.WebViewActivity; @@ -32,6 +36,7 @@ public class WebViewFragment extends Fragment implements GestureDetector.OnGestu public static final String URL = "url"; public static final String TOOLBAR_VISIBLE = "toolbar_visible"; + public static final String CLEAR_COOKIES = "clear_cookies"; private static final long DELAY_HIDE_TOOLBAR_MILLIS = 3000; private static final long FADE_OUT_DURATION = 2000; @@ -41,6 +46,7 @@ public class WebViewFragment extends Fragment implements GestureDetector.OnGestu private ProgressBar mProgressBar; private String mUrl; private boolean mToolbarVisible; + private boolean mClearCookies; private OnWebViewInteractionListener mListener; private Runnable mCloseAction; @@ -170,6 +176,7 @@ public class WebViewFragment extends Fragment implements GestureDetector.OnGestu if (getArguments() != null) { mUrl = getArguments().getString(URL); mToolbarVisible = getArguments().getBoolean(TOOLBAR_VISIBLE); + mClearCookies = getArguments().getBoolean(CLEAR_COOKIES); } } @@ -179,6 +186,10 @@ public class WebViewFragment extends Fragment implements GestureDetector.OnGestu View rootView = inflater.inflate(R.layout.fragment_web_view, container, false); mProgressBar = rootView.findViewById(R.id.toolbarProgressBar); myWebView = rootView.findViewById(R.id.web_view); + if (mClearCookies) { + CookieManager.getInstance().removeAllCookies(null); + } + mHandler = new Handler(); gestureDetector = new GestureDetector(this); gestureDetector.setOnDoubleTapListener(new GestureDetector.OnDoubleTapListener() { @@ -251,6 +262,7 @@ public class WebViewFragment extends Fragment implements GestureDetector.OnGestu void onWebLoaded(String url, SafenessLevel safenessLevel); void onTitleReceived(String title); void onExpand(); + void onOAuthAuthorizeCallback(Uri uri); } @@ -320,6 +332,18 @@ public class WebViewFragment extends Fragment implements GestureDetector.OnGestu super.onLoadResource(view, url); } } + + @Override + public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) { + if (!TextUtils.isEmpty(BuildConfig.OAUTH_REDIRECT_URI) && + request.getUrl().toString().startsWith(BuildConfig.OAUTH_REDIRECT_URI)) { + if (mListener != null) { + mListener.onOAuthAuthorizeCallback(request.getUrl()); + } + return true; + } + return super.shouldOverrideUrlLoading(view, request); + } } class HiFiWebChromeClient extends WebChromeClient { diff --git a/android/app/src/main/java/io/highfidelity/hifiinterface/receiver/HeadsetStateReceiver.java b/android/app/src/main/java/io/highfidelity/hifiinterface/receiver/HeadsetStateReceiver.java new file mode 100644 index 0000000000..5645912d73 --- /dev/null +++ b/android/app/src/main/java/io/highfidelity/hifiinterface/receiver/HeadsetStateReceiver.java @@ -0,0 +1,18 @@ +package io.highfidelity.hifiinterface.receiver; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.media.AudioManager; +import android.util.Log; + +public class HeadsetStateReceiver extends BroadcastReceiver { + + private native void notifyHeadsetOn(boolean pluggedIn); + + @Override + public void onReceive(Context context, Intent intent) { + AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); + notifyHeadsetOn(audioManager.isWiredHeadsetOn()); + } +} diff --git a/android/app/src/main/java/io/highfidelity/hifiinterface/view/DomainAdapter.java b/android/app/src/main/java/io/highfidelity/hifiinterface/view/DomainAdapter.java index 71d634e9ea..78251ac4a4 100644 --- a/android/app/src/main/java/io/highfidelity/hifiinterface/view/DomainAdapter.java +++ b/android/app/src/main/java/io/highfidelity/hifiinterface/view/DomainAdapter.java @@ -12,6 +12,7 @@ import android.widget.TextView; import com.squareup.picasso.Picasso; +import java.util.Arrays; import java.util.List; import io.highfidelity.hifiinterface.R; @@ -36,19 +37,41 @@ public class DomainAdapter extends RecyclerView.Adapter 0) { + mDomains = Arrays.copyOf(DOMAINS_TMP_CACHE, DOMAINS_TMP_CACHE.length); + notifyDataSetChanged(); + if (mAdapterListener != null) { + if (mDomains.length == 0) { + mAdapterListener.onEmptyAdapter(false); + } else { + mAdapterListener.onNonEmptyAdapter(false); + } + } + } + } + } + public void loadDomains(String filterText, boolean forceRefresh) { domainProvider.retrieve(filterText, new DomainProvider.DomainCallback() { @Override @@ -60,13 +83,18 @@ public class DomainAdapter extends RecyclerView.Adapter USERS_TMP_CACHE; + public UserListAdapter(Context c, UsersProvider usersProvider) { mContext = c; mInflater = LayoutInflater.from(mContext); mProvider = usersProvider; - loadUsers(); } public void setListener(AdapterListener adapterListener) { mAdapterListener = adapterListener; } + public void startLoad() { + useTmpCachedUsers(); + loadUsers(); + } + + private void useTmpCachedUsers() { + synchronized (this) { + if (USERS_TMP_CACHE != null && USERS_TMP_CACHE.size() > 0) { + mUsers = new ArrayList<>(USERS_TMP_CACHE.size()); + mUsers.addAll(USERS_TMP_CACHE); + notifyDataSetChanged(); + if (mAdapterListener != null) { + if (mUsers.isEmpty()) { + mAdapterListener.onEmptyAdapter(false); + } else { + mAdapterListener.onNonEmptyAdapter(false); + } + } + } + } + } + public void loadUsers() { mProvider.retrieve(new UsersProvider.UsersCallback() { @Override public void retrieveOk(List users) { mUsers = new ArrayList<>(users); notifyDataSetChanged(); - if (mAdapterListener != null) { - if (mUsers.isEmpty()) { - mAdapterListener.onEmptyAdapter(); - } else { - mAdapterListener.onNonEmptyAdapter(); + + synchronized (this) { + USERS_TMP_CACHE = new ArrayList<>(mUsers.size()); + USERS_TMP_CACHE.addAll(mUsers); + + if (mAdapterListener != null) { + if (mUsers.isEmpty()) { + mAdapterListener.onEmptyAdapter(true); + } else { + mAdapterListener.onNonEmptyAdapter(true); + } } } } @@ -240,8 +269,9 @@ public class UserListAdapter extends RecyclerView.Adapter diff --git a/android/app/src/main/res/drawable/ic_eye_noshow.xml b/android/app/src/main/res/drawable/ic_eye_noshow.xml new file mode 100644 index 0000000000..1d5304afac --- /dev/null +++ b/android/app/src/main/res/drawable/ic_eye_noshow.xml @@ -0,0 +1,27 @@ + + + + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/drawable/ic_eye_show.xml b/android/app/src/main/res/drawable/ic_eye_show.xml new file mode 100644 index 0000000000..273ecc8339 --- /dev/null +++ b/android/app/src/main/res/drawable/ic_eye_show.xml @@ -0,0 +1,15 @@ + + + + + + diff --git a/android/app/src/main/res/drawable/ic_right_arrow.xml b/android/app/src/main/res/drawable/ic_right_arrow.xml new file mode 100644 index 0000000000..e35d1a2733 --- /dev/null +++ b/android/app/src/main/res/drawable/ic_right_arrow.xml @@ -0,0 +1,11 @@ + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/drawable/ic_steam.xml b/android/app/src/main/res/drawable/ic_steam.xml new file mode 100644 index 0000000000..9b739c1f73 --- /dev/null +++ b/android/app/src/main/res/drawable/ic_steam.xml @@ -0,0 +1,11 @@ + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/drawable/rounded_button.xml b/android/app/src/main/res/drawable/rounded_button_color1.xml similarity index 100% rename from android/app/src/main/res/drawable/rounded_button.xml rename to android/app/src/main/res/drawable/rounded_button_color1.xml diff --git a/android/app/src/main/res/drawable/rounded_button_color3.xml b/android/app/src/main/res/drawable/rounded_button_color3.xml new file mode 100644 index 0000000000..6230885b30 --- /dev/null +++ b/android/app/src/main/res/drawable/rounded_button_color3.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/drawable/rounded_button_color4.xml b/android/app/src/main/res/drawable/rounded_button_color4.xml new file mode 100644 index 0000000000..679bf24513 --- /dev/null +++ b/android/app/src/main/res/drawable/rounded_button_color4.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/drawable/rounded_edit.xml b/android/app/src/main/res/drawable/rounded_edit.xml deleted file mode 100644 index 3c1cac4d1d..0000000000 --- a/android/app/src/main/res/drawable/rounded_edit.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/android/app/src/main/res/drawable/rounded_secondary_button.xml b/android/app/src/main/res/drawable/rounded_secondary_button.xml new file mode 100644 index 0000000000..6230885b30 --- /dev/null +++ b/android/app/src/main/res/drawable/rounded_secondary_button.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/drawable/selector_show_password.xml b/android/app/src/main/res/drawable/selector_show_password.xml new file mode 100644 index 0000000000..a44092aceb --- /dev/null +++ b/android/app/src/main/res/drawable/selector_show_password.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/layout/activity_encourage_login.xml b/android/app/src/main/res/layout/activity_encourage_login.xml new file mode 100644 index 0000000000..d7c9ff6b4d --- /dev/null +++ b/android/app/src/main/res/layout/activity_encourage_login.xml @@ -0,0 +1,14 @@ + + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/layout/fragment_login.xml b/android/app/src/main/res/layout/fragment_login.xml index c50e6c1380..d12b84cc8d 100644 --- a/android/app/src/main/res/layout/fragment_login.xml +++ b/android/app/src/main/res/layout/fragment_login.xml @@ -6,6 +6,17 @@ android:layout_height="match_parent" android:background="@color/backgroundLight"> + + + + - - - + app:layout_constraintBottom_toBottomOf="parent" + android:visibility="gone"> + + + - - +

-
- Y - -
+
+
+
+ +
+
+
+ +
+
+
+ Y +
+ +
- +
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + +
TypeDNameFileVertsTextsText MBBakedDrawsk
- +
+
+ +
+
+
+ +
+
- No entities found in view within a 100 meter radius. Try moving to a different location and refreshing. + There are no entities to display. Please check your filters or create an entity to begin. +
+
+
+ diff --git a/scripts/system/html/entityProperties.html b/scripts/system/html/entityProperties.html index 9614f8b8fe..01395368b0 100644 --- a/scripts/system/html/entityProperties.html +++ b/scripts/system/html/entityProperties.html @@ -1,4 +1,4 @@ - - - -
- - TextM - -
- - -
-
- - -
-
- - -
-
-
- -
-
-
-
-
-
-
-
- Background color -
-
-
-
-
-
-
- -
- - ImageM - -
- - -
-
- -
- - WebM - -
- - -
-
- - -
-
- -
- - Voxel volume size m - -
-
-
-
-
- -
- - -
-
- - -
-
- - -
-
- -
- - MaterialM - -
-
- - -
- -
- -

-
- - - - Saved! -
-
-
- -
-
-
-
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- - -
- -
-
-
-
- -
- -
-
-
-
- -
- - -
-
- -
-
- +
+
diff --git a/scripts/system/html/img/expand.svg b/scripts/system/html/img/expand.svg new file mode 100644 index 0000000000..f57e624374 --- /dev/null +++ b/scripts/system/html/img/expand.svg @@ -0,0 +1,85 @@ + + + +image/svg+xml + + + + + + + + + \ No newline at end of file diff --git a/scripts/system/html/js/createAppTooltip.js b/scripts/system/html/js/createAppTooltip.js new file mode 100644 index 0000000000..a5c961a7e2 --- /dev/null +++ b/scripts/system/html/js/createAppTooltip.js @@ -0,0 +1,116 @@ +// createAppTooltip.js +// +// Created by Thijs Wenker on 17 Oct 2018 +// Copyright 2018 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html + +const CREATE_APP_TOOLTIP_OFFSET = 20; +const TOOLTIP_DELAY = 500; // ms +const TOOLTIP_DEBUG = false; + +function CreateAppTooltip() { + this._tooltipData = null; + this._tooltipDiv = null; + this._delayTimeout = null; + this._isEnabled = false; +} + +CreateAppTooltip.prototype = { + _tooltipData: null, + _tooltipDiv: null, + _delayTimeout: null, + _isEnabled: null, + + _removeTooltipIfExists: function() { + if (this._delayTimeout !== null) { + window.clearTimeout(this._delayTimeout); + this._delayTimeout = null; + } + + if (this._tooltipDiv !== null) { + this._tooltipDiv.remove(); + this._tooltipDiv = null; + } + }, + + setIsEnabled: function(isEnabled) { + this._isEnabled = isEnabled; + }, + + setTooltipData: function(tooltipData) { + this._tooltipData = tooltipData; + }, + + registerTooltipElement: function(element, tooltipID) { + element.addEventListener("mouseover", function() { + if (!this._isEnabled) { + return; + } + + this._removeTooltipIfExists(); + + this._delayTimeout = window.setTimeout(function() { + let tooltipData = this._tooltipData[tooltipID]; + + if (!tooltipData || tooltipData.tooltip === "") { + if (!TOOLTIP_DEBUG) { + return; + } + tooltipData = { tooltip: 'PLEASE SET THIS TOOLTIP' }; + } + + let elementRect = element.getBoundingClientRect(); + let elTip = document.createElement("div"); + elTip.className = "create-app-tooltip"; + + let elTipDescription = document.createElement("div"); + elTipDescription.className = "create-app-tooltip-description"; + elTipDescription.innerText = tooltipData.tooltip; + elTip.appendChild(elTipDescription); + + let jsAttribute = tooltipID; + if (tooltipData.jsPropertyName) { + jsAttribute = tooltipData.jsPropertyName; + } + + if (!tooltipData.skipJSProperty) { + let elTipJSAttribute = document.createElement("div"); + elTipJSAttribute.className = "create-app-tooltip-js-attribute"; + elTipJSAttribute.innerText = `JS Attribute: ${jsAttribute}`; + elTip.appendChild(elTipJSAttribute); + } + + document.body.appendChild(elTip); + + let elementTop = window.pageYOffset + elementRect.top; + + let desiredTooltipTop = elementTop + element.clientHeight + CREATE_APP_TOOLTIP_OFFSET; + let desiredTooltipLeft = window.pageXOffset + elementRect.left; + + if ((window.innerHeight + window.pageYOffset) < (desiredTooltipTop + elTip.clientHeight)) { + // show above when otherwise out of bounds + elTip.style.top = elementTop - CREATE_APP_TOOLTIP_OFFSET - elTip.clientHeight; + } else { + // show tooltip below by default + elTip.style.top = desiredTooltipTop; + } + if ((window.innerWidth + window.pageXOffset) < (desiredTooltipLeft + elTip.clientWidth)) { + elTip.style.left = document.body.clientWidth + window.pageXOffset - elTip.offsetWidth; + } else { + elTip.style.left = desiredTooltipLeft; + } + + this._tooltipDiv = elTip; + }.bind(this), TOOLTIP_DELAY); + }.bind(this), false); + element.addEventListener("mouseout", function() { + if (!this._isEnabled) { + return; + } + + this._removeTooltipIfExists(); + }.bind(this), false); + } +}; diff --git a/scripts/system/html/js/draggableNumber.js b/scripts/system/html/js/draggableNumber.js new file mode 100644 index 0000000000..1f4bc21441 --- /dev/null +++ b/scripts/system/html/js/draggableNumber.js @@ -0,0 +1,158 @@ +// draggableNumber.js +// +// Created by David Back on 7 Nov 2018 +// Copyright 2018 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html + +const DELTA_X_FOCUS_THRESHOLD = 1; + +function DraggableNumber(min, max, step, decimals) { + this.min = min; + this.max = max; + this.step = step !== undefined ? step : 1; + this.decimals = decimals; + this.initialMouseEvent = null; + this.lastMouseEvent = null; + this.valueChangeFunction = null; + this.initialize(); +} + +DraggableNumber.prototype = { + mouseDown: function(event) { + if (event.target === this.elText) { + this.initialMouseEvent = event; + this.lastMouseEvent = event; + document.addEventListener("mousemove", this.onDocumentMouseMove); + document.addEventListener("mouseup", this.onDocumentMouseUp); + } + }, + + mouseUp: function(event) { + if (event.target === this.elText && this.initialMouseEvent) { + let dx = event.clientX - this.initialMouseEvent.clientX; + if (dx <= DELTA_X_FOCUS_THRESHOLD) { + this.elInput.style.visibility = "visible"; + this.elText.style.visibility = "hidden"; + } + this.initialMouseEvent = null; + } + }, + + documentMouseMove: function(event) { + if (this.lastMouseEvent) { + let initialValue = this.elInput.value; + let dx = event.clientX - this.lastMouseEvent.clientX; + let changeValue = dx !== 0; + if (changeValue) { + while (dx !== 0) { + if (dx > 0) { + this.stepUp(); + --dx; + } else { + this.stepDown(); + ++dx; + } + } + if (this.valueChangeFunction) { + this.valueChangeFunction(); + } + } + this.lastMouseEvent = event; + } + }, + + documentMouseUp: function(event) { + this.lastMouseEvent = null; + document.removeEventListener("mousemove", this.onDocumentMouseMove); + document.removeEventListener("mouseup", this.onDocumentMouseUp); + }, + + stepUp: function() { + this.elInput.stepUp(); + this.inputChange(); + }, + + stepDown: function() { + this.elInput.stepDown(); + this.inputChange(); + }, + + setValue: function(newValue) { + if (newValue !== "" && this.decimals !== undefined) { + this.elInput.value = parseFloat(newValue).toFixed(this.decimals); + } else { + this.elInput.value = newValue; + } + this.elText.firstChild.data = this.elInput.value; + }, + + setValueChangeFunction: function(valueChangeFunction) { + if (this.valueChangeFunction) { + this.elInput.removeEventListener("change", this.valueChangeFunction); + } + this.valueChangeFunction = valueChangeFunction.bind(this.elInput); + this.elInput.addEventListener("change", this.valueChangeFunction); + }, + + inputChange: function() { + this.setValue(this.elInput.value); + }, + + inputBlur: function() { + this.elInput.style.visibility = "hidden"; + this.elText.style.visibility = "visible"; + }, + + initialize: function() { + this.onMouseDown = this.mouseDown.bind(this); + this.onMouseUp = this.mouseUp.bind(this); + this.onDocumentMouseMove = this.documentMouseMove.bind(this); + this.onDocumentMouseUp = this.documentMouseUp.bind(this); + this.onStepUp = this.stepUp.bind(this); + this.onStepDown = this.stepDown.bind(this); + this.onInputChange = this.inputChange.bind(this); + this.onInputBlur = this.inputBlur.bind(this); + + this.elDiv = document.createElement('div'); + this.elDiv.className = "draggable-number"; + + this.elText = document.createElement('label'); + this.elText.className = "draggable-number text"; + this.elText.innerText = " "; + this.elText.style.visibility = "visible"; + this.elText.addEventListener("mousedown", this.onMouseDown); + this.elText.addEventListener("mouseup", this.onMouseUp); + + this.elLeftArrow = document.createElement('span'); + this.elRightArrow = document.createElement('span'); + this.elLeftArrow.className = 'draggable-number left-arrow'; + this.elLeftArrow.innerHTML = 'D'; + this.elLeftArrow.addEventListener("click", this.onStepDown); + this.elRightArrow.className = 'draggable-number right-arrow'; + this.elRightArrow.innerHTML = 'D'; + this.elRightArrow.addEventListener("click", this.onStepUp); + + this.elInput = document.createElement('input'); + this.elInput.className = "draggable-number input"; + this.elInput.setAttribute("type", "number"); + if (this.min !== undefined) { + this.elInput.setAttribute("min", this.min); + } + if (this.max !== undefined) { + this.elInput.setAttribute("max", this.max); + } + if (this.step !== undefined) { + this.elInput.setAttribute("step", this.step); + } + this.elInput.style.visibility = "hidden"; + this.elInput.addEventListener("change", this.onInputChange); + this.elInput.addEventListener("blur", this.onInputBlur); + + this.elText.appendChild(this.elLeftArrow); + this.elText.appendChild(this.elInput); + this.elText.appendChild(this.elRightArrow); + this.elDiv.appendChild(this.elText); + } +}; diff --git a/scripts/system/html/js/entityList.js b/scripts/system/html/js/entityList.js index 5cd5f6d610..00c50169a6 100644 --- a/scripts/system/html/js/entityList.js +++ b/scripts/system/html/js/entityList.js @@ -6,22 +6,221 @@ // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -var entities = {}; -var selectedEntities = []; -var currentSortColumn = 'type'; -var currentSortOrder = 'des'; -var entityList = null; -var refreshEntityListTimer = null; +const ASCENDING_SORT = 1; +const DESCENDING_SORT = -1; const ASCENDING_STRING = '▴'; const DESCENDING_STRING = '▾'; -const LOCKED_GLYPH = ""; -const VISIBLE_GLYPH = ""; -const TRANSPARENCY_GLYPH = ""; -const BAKED_GLYPH = "" -const SCRIPT_GLYPH = "k"; -const DELETE = 46; // Key code for the delete key. -const KEY_P = 80; // Key code for letter p used for Parenting hotkey. -const MAX_ITEMS = Number.MAX_VALUE; // Used to set the max length of the list of discovered entities. +const BYTES_PER_MEGABYTE = 1024 * 1024; +const IMAGE_MODEL_NAME = 'default-image-model.fbx'; +const COLLAPSE_EXTRA_INFO = "E"; +const EXPAND_EXTRA_INFO = "D"; +const FILTER_IN_VIEW_ATTRIBUTE = "pressed"; +const WINDOW_NONVARIABLE_HEIGHT = 227; +const EMPTY_ENTITY_ID = "0"; +const MAX_LENGTH_RADIUS = 9; +const MINIMUM_COLUMN_WIDTH = 24; +const SCROLLBAR_WIDTH = 20; +const RESIZER_WIDTH = 10; + +const COLUMNS = { + type: { + columnHeader: "Type", + propertyID: "type", + initialWidth: 0.16, + initiallyShown: true, + alwaysShown: true, + }, + name: { + columnHeader: "Name", + propertyID: "name", + initialWidth: 0.34, + initiallyShown: true, + alwaysShown: true, + }, + url: { + columnHeader: "File", + dropdownLabel: "File", + propertyID: "url", + initialWidth: 0.34, + initiallyShown: true, + }, + locked: { + columnHeader: "", + glyph: true, + propertyID: "locked", + initialWidth: 0.08, + initiallyShown: true, + alwaysShown: true, + }, + visible: { + columnHeader: "", + glyph: true, + propertyID: "visible", + initialWidth: 0.08, + initiallyShown: true, + alwaysShown: true, + }, + verticesCount: { + columnHeader: "Verts", + dropdownLabel: "Vertices", + propertyID: "verticesCount", + initialWidth: 0.08, + }, + texturesCount: { + columnHeader: "Texts", + dropdownLabel: "Textures", + propertyID: "texturesCount", + initialWidth: 0.08, + }, + texturesSize: { + columnHeader: "Text MB", + dropdownLabel: "Texture Size", + propertyID: "texturesSize", + initialWidth: 0.10, + }, + hasTransparent: { + columnHeader: "", + glyph: true, + dropdownLabel: "Transparency", + propertyID: "hasTransparent", + initialWidth: 0.04, + }, + isBaked: { + columnHeader: "", + glyph: true, + dropdownLabel: "Baked", + propertyID: "isBaked", + initialWidth: 0.08, + }, + drawCalls: { + columnHeader: "Draws", + dropdownLabel: "Draws", + propertyID: "drawCalls", + initialWidth: 0.08, + }, + hasScript: { + columnHeader: "k", + glyph: true, + dropdownLabel: "Script", + propertyID: "hasScript", + initialWidth: 0.06, + }, +}; + +const COMPARE_ASCENDING = function(a, b) { + let va = a[currentSortColumn]; + let vb = b[currentSortColumn]; + + if (va < vb) { + return -1; + } else if (va > vb) { + return 1; + } else if (a.id < b.id) { + return -1; + } + + return 1; +}; +const COMPARE_DESCENDING = function(a, b) { + return COMPARE_ASCENDING(b, a); +}; + +const FILTER_TYPES = [ + "Shape", + "Model", + "Image", + "Light", + "Zone", + "Web", + "Material", + "ParticleEffect", + "PolyLine", + "PolyVox", + "Text", +]; + +const ICON_FOR_TYPE = { + Shape: "n", + Model: "", + Image: "", + Light: "p", + Zone: "o", + Web: "q", + Material: "", + ParticleEffect: "", + PolyLine: "", + PolyVox: "", + Text: "l", +}; + +// List of all entities +let entities = []; +// List of all entities, indexed by Entity ID +let entitiesByID = {}; +// The filtered and sorted list of entities passed to ListView +let visibleEntities = []; +// List of all entities that are currently selected +let selectedEntities = []; + +let entityList = null; // The ListView + +/** + * @type EntityListContextMenu + */ +let entityListContextMenu = null; + +let currentSortColumn = 'type'; +let currentSortOrder = ASCENDING_SORT; +let elSortOrders = {}; +let typeFilters = []; +let isFilterInView = false; + +let columns = []; +let columnsByID = {}; +let currentResizeEl = null; +let startResizeEvent = null; +let resizeColumnIndex = 0; +let startThClick = null; + +let elEntityTable, + elEntityTableHeader, + elEntityTableBody, + elEntityTableScroll, + elEntityTableHeaderRow, + elRefresh, + elToggleLocked, + elToggleVisible, + elDelete, + elFilterTypeMultiselectBox, + elFilterTypeText, + elFilterTypeOptions, + elFilterSearch, + elFilterInView, + elFilterRadius, + elExport, + elPal, + elSelectedEntitiesCount, + elVisibleEntitiesCount, + elNoEntitiesMessage, + elColumnsMultiselectBox, + elColumnsOptions, + elToggleSpaceMode; + +const ENABLE_PROFILING = false; +let profileIndent = ''; +const PROFILE_NOOP = function(_name, fn, args) { + fn.apply(this, args); +} ; +const PROFILE = !ENABLE_PROFILING ? PROFILE_NOOP : function(name, fn, args) { + console.log("PROFILE-Web " + profileIndent + "(" + name + ") Begin"); + let previousIndent = profileIndent; + profileIndent += ' '; + let before = Date.now(); + fn.apply(this, args); + let delta = Date.now() - before; + profileIndent = previousIndent; + console.log("PROFILE-Web " + profileIndent + "(" + name + ") End " + delta + "ms"); +}; debugPrint = function (message) { console.log(message); @@ -29,125 +228,318 @@ debugPrint = function (message) { function loaded() { openEventBridge(function() { - entityList = new List('entity-list', { valueNames: ['name', 'type', 'url', 'locked', 'visible'], page: MAX_ITEMS}); - entityList.clear(); elEntityTable = document.getElementById("entity-table"); + elEntityTableHeader = document.getElementById("entity-table-header"); elEntityTableBody = document.getElementById("entity-table-body"); + elEntityTableScroll = document.getElementById("entity-table-scroll"); elRefresh = document.getElementById("refresh"); elToggleLocked = document.getElementById("locked"); elToggleVisible = document.getElementById("visible"); elDelete = document.getElementById("delete"); - elFilter = document.getElementById("filter"); - elInView = document.getElementById("in-view") - elRadius = document.getElementById("radius"); + elFilterTypeMultiselectBox = document.getElementById("filter-type-multiselect-box"); + elFilterTypeText = document.getElementById("filter-type-text"); + elFilterTypeOptions = document.getElementById("filter-type-options"); + elFilterSearch = document.getElementById("filter-search"); + elFilterInView = document.getElementById("filter-in-view"); + elFilterRadius = document.getElementById("filter-radius"); elExport = document.getElementById("export"); elPal = document.getElementById("pal"); - elEntityTable = document.getElementById("entity-table"); - elInfoToggle = document.getElementById("info-toggle"); - elInfoToggleGlyph = elInfoToggle.firstChild; - elFooter = document.getElementById("footer-text"); + elSelectedEntitiesCount = document.getElementById("selected-entities-count"); + elVisibleEntitiesCount = document.getElementById("visible-entities-count"); elNoEntitiesMessage = document.getElementById("no-entities"); - elNoEntitiesInView = document.getElementById("no-entities-in-view"); - elNoEntitiesRadius = document.getElementById("no-entities-radius"); - elEntityTableScroll = document.getElementById("entity-table-scroll"); + elColumnsMultiselectBox = document.getElementById("entity-table-columns-multiselect-box"); + elColumnsOptions = document.getElementById("entity-table-columns-options"); + elToggleSpaceMode = document.getElementById('toggle-space-mode'); + + document.body.onclick = onBodyClick; + elToggleLocked.onclick = function() { + EventBridge.emitWebEvent(JSON.stringify({ type: 'toggleLocked' })); + }; + elToggleVisible.onclick = function() { + EventBridge.emitWebEvent(JSON.stringify({ type: 'toggleVisible' })); + }; + elExport.onclick = function() { + EventBridge.emitWebEvent(JSON.stringify({ type: 'export'})); + }; + elPal.onclick = function() { + EventBridge.emitWebEvent(JSON.stringify({ type: 'pal' })); + }; + elDelete.onclick = function() { + EventBridge.emitWebEvent(JSON.stringify({ type: 'delete' })); + }; + elToggleSpaceMode.onclick = function() { + EventBridge.emitWebEvent(JSON.stringify({ type: 'toggleSpaceMode' })); + }; + elRefresh.onclick = refreshEntities; + elFilterTypeMultiselectBox.onclick = onToggleTypeDropdown; + elFilterSearch.onkeyup = refreshEntityList; + elFilterSearch.onsearch = refreshEntityList; + elFilterInView.onclick = onToggleFilterInView; + elFilterRadius.onkeyup = onRadiusChange; + elFilterRadius.onchange = onRadiusChange; + elColumnsMultiselectBox.onclick = onToggleColumnsDropdown; + + // create filter type dropdown checkboxes with label and icon for each type + for (let i = 0; i < FILTER_TYPES.length; ++i) { + let type = FILTER_TYPES[i]; + let typeFilterID = "filter-type-" + type; + + let elDiv = document.createElement('div'); + elDiv.onclick = onToggleTypeFilter; + elFilterTypeOptions.appendChild(elDiv); + + let elInput = document.createElement('input'); + elInput.setAttribute("type", "checkbox"); + elInput.setAttribute("id", typeFilterID); + elInput.setAttribute("filterType", type); + elInput.checked = true; // all types are checked initially + elDiv.appendChild(elInput); + + let elLabel = document.createElement('label'); + elLabel.setAttribute("for", typeFilterID); + elLabel.innerText = type; + elDiv.appendChild(elLabel); + + let elSpan = document.createElement('span'); + elSpan.setAttribute("class", "typeIcon"); + elSpan.innerHTML = ICON_FOR_TYPE[type]; - document.getElementById("entity-name").onclick = function() { - setSortColumn('name'); - }; - document.getElementById("entity-type").onclick = function() { - setSortColumn('type'); - }; - document.getElementById("entity-url").onclick = function() { - setSortColumn('url'); - }; - document.getElementById("entity-locked").onclick = function () { - setSortColumn('locked'); - }; - document.getElementById("entity-visible").onclick = function () { - setSortColumn('visible'); - }; - document.getElementById("entity-verticesCount").onclick = function () { - setSortColumn('verticesCount'); - }; - document.getElementById("entity-texturesCount").onclick = function () { - setSortColumn('texturesCount'); - }; - document.getElementById("entity-texturesSize").onclick = function () { - setSortColumn('texturesSize'); - }; - document.getElementById("entity-hasTransparent").onclick = function () { - setSortColumn('hasTransparent'); - }; - document.getElementById("entity-isBaked").onclick = function () { - setSortColumn('isBaked'); - }; - document.getElementById("entity-drawCalls").onclick = function () { - setSortColumn('drawCalls'); - }; - document.getElementById("entity-hasScript").onclick = function () { - setSortColumn('hasScript'); - }; + elLabel.insertBefore(elSpan, elLabel.childNodes[0]); + + toggleTypeFilter(elInput, false); // add all types to the initial types filter + } + + // create columns + elHeaderTr = document.createElement("tr"); + elEntityTableHeader.appendChild(elHeaderTr); + let columnIndex = 0; + for (let columnID in COLUMNS) { + let columnData = COLUMNS[columnID]; + + let thID = "entity-" + columnID; + let elTh = document.createElement("th"); + elTh.setAttribute("id", thID); + elTh.setAttribute("data-resizable-column-id", thID); + if (columnData.glyph) { + let elGlyph = document.createElement("span"); + elGlyph.className = "glyph"; + elGlyph.innerHTML = columnData.columnHeader; + elTh.appendChild(elGlyph); + } else { + elTh.innerText = columnData.columnHeader; + } + elTh.onmousedown = function() { + startThClick = this; + }; + elTh.onmouseup = function() { + if (startThClick === this) { + setSortColumn(columnID); + } + startThClick = null; + }; + + let elResizer = document.createElement("span"); + elResizer.className = "resizer"; + elResizer.innerHTML = " "; + elResizer.setAttribute("columnIndex", columnIndex); + elResizer.onmousedown = onStartResize; + elTh.appendChild(elResizer); + + let elSortOrder = document.createElement("span"); + elSortOrder.className = "sort-order"; + elTh.appendChild(elSortOrder); + elHeaderTr.appendChild(elTh); + + elSortOrders[columnID] = elSortOrder; + + // add column to columns dropdown if it is not set to be always shown + if (columnData.alwaysShown !== true) { + let columnDropdownID = "entity-table-column-" + columnID; + + let elDiv = document.createElement('div'); + elDiv.onclick = onToggleColumn; + elColumnsOptions.appendChild(elDiv); + + let elInput = document.createElement('input'); + elInput.setAttribute("type", "checkbox"); + elInput.setAttribute("id", columnDropdownID); + elInput.setAttribute("columnID", columnID); + elInput.checked = columnData.initiallyShown === true; + elDiv.appendChild(elInput); + + let elLabel = document.createElement('label'); + elLabel.setAttribute("for", columnDropdownID); + elLabel.innerText = columnData.dropdownLabel; + elDiv.appendChild(elLabel); + } + + let initialWidth = columnData.initiallyShown === true ? columnData.initialWidth : 0; + columns.push({ + columnID: columnID, + elTh: elTh, + elResizer: elResizer, + width: initialWidth, + data: columnData + }); + columnsByID[columnID] = columns[columnIndex]; + + ++columnIndex; + } + + elEntityTableHeaderRow = document.querySelectorAll("#entity-table thead th"); + + entityList = new ListView(elEntityTableBody, elEntityTableScroll, elEntityTableHeaderRow, + createRow, updateRow, clearRow, WINDOW_NONVARIABLE_HEIGHT); + + entityListContextMenu = new EntityListContextMenu(); + + function startRenamingEntity(entityID) { + let entity = entitiesByID[entityID]; + if (!entity || entity.locked || !entity.elRow) { + return; + } + + let elCell = entity.elRow.childNodes[getColumnIndex("name")]; + let elRenameInput = document.createElement("input"); + elRenameInput.setAttribute('class', 'rename-entity'); + elRenameInput.value = entity.name; + let ignoreClicks = function(event) { + event.stopPropagation(); + }; + elRenameInput.onclick = ignoreClicks; + elRenameInput.ondblclick = ignoreClicks; + elRenameInput.onkeyup = function(keyEvent) { + if (keyEvent.key === "Enter") { + elRenameInput.blur(); + } + }; + + elRenameInput.onblur = function(event) { + let value = elRenameInput.value; + EventBridge.emitWebEvent(JSON.stringify({ + type: 'rename', + entityID: entityID, + name: value + })); + entity.name = value; + elCell.innerText = value; + }; + + elCell.innerHTML = ""; + elCell.appendChild(elRenameInput); + + elRenameInput.select(); + } + + entityListContextMenu.setOnSelectedCallback(function(optionName, selectedEntityID) { + switch (optionName) { + case "Cut": + EventBridge.emitWebEvent(JSON.stringify({ type: 'cut' })); + break; + case "Copy": + EventBridge.emitWebEvent(JSON.stringify({ type: 'copy' })); + break; + case "Paste": + EventBridge.emitWebEvent(JSON.stringify({ type: 'paste' })); + break; + case "Rename": + startRenamingEntity(selectedEntityID); + break; + case "Duplicate": + EventBridge.emitWebEvent(JSON.stringify({ type: 'duplicate' })); + break; + case "Delete": + EventBridge.emitWebEvent(JSON.stringify({ type: 'delete' })); + break; + } + }); + + function onRowContextMenu(clickEvent) { + let entityID = this.dataset.entityID; + + if (!selectedEntities.includes(entityID)) { + let selection = [entityID]; + updateSelectedEntities(selection); + + EventBridge.emitWebEvent(JSON.stringify({ + type: "selectionUpdate", + focus: false, + entityIds: selection, + })); + } + + let enabledContextMenuItems = ['Copy', 'Paste', 'Duplicate']; + if (entitiesByID[entityID] && !entitiesByID[entityID].locked) { + enabledContextMenuItems.push('Cut'); + enabledContextMenuItems.push('Rename'); + enabledContextMenuItems.push('Delete'); + } + + entityListContextMenu.open(clickEvent, entityID, enabledContextMenuItems); + } function onRowClicked(clickEvent) { - var id = this.dataset.entityId; - var selection = [this.dataset.entityId]; + let entityID = this.dataset.entityID; + let selection = [entityID]; + if (clickEvent.ctrlKey) { - var selectedIndex = selectedEntities.indexOf(id); + let selectedIndex = selectedEntities.indexOf(entityID); if (selectedIndex >= 0) { - selection = selectedEntities; - selection.splice(selectedIndex, 1) + selection = []; + selection = selection.concat(selectedEntities); + selection.splice(selectedIndex, 1); } else { selection = selection.concat(selectedEntities); } } else if (clickEvent.shiftKey && selectedEntities.length > 0) { - var previousItemFound = -1; - var clickedItemFound = -1; - for (var entity in entityList.visibleItems) { - if (clickedItemFound === -1 && this.dataset.entityId == entityList.visibleItems[entity].values().id) { - clickedItemFound = entity; - } else if(previousItemFound === -1 && selectedEntities[0] == entityList.visibleItems[entity].values().id) { - previousItemFound = entity; + let previousItemFound = -1; + let clickedItemFound = -1; + for (let i = 0, len = visibleEntities.length; i < len; ++i) { + let entity = visibleEntities[i]; + if (clickedItemFound === -1 && entityID === entity.id) { + clickedItemFound = i; + } else if (previousItemFound === -1 && selectedEntities[0] === entity.id) { + previousItemFound = i; } } if (previousItemFound !== -1 && clickedItemFound !== -1) { - var betweenItems = []; - var toItem = Math.max(previousItemFound, clickedItemFound); - // skip first and last item in this loop, we add them to selection after the loop - for (var i = (Math.min(previousItemFound, clickedItemFound) + 1); i < toItem; i++) { - entityList.visibleItems[i].elm.className = 'selected'; - betweenItems.push(entityList.visibleItems[i].values().id); + selection = []; + let toItem = Math.max(previousItemFound, clickedItemFound); + for (let i = Math.min(previousItemFound, clickedItemFound); i <= toItem; i++) { + selection.push(visibleEntities[i].id); } if (previousItemFound > clickedItemFound) { // always make sure that we add the items in the right order - betweenItems.reverse(); + selection.reverse(); } - selection = selection.concat(betweenItems, selectedEntities); + } + } else if (!clickEvent.ctrlKey && !clickEvent.shiftKey && selectedEntities.length === 1) { + // if reselecting the same entity then start renaming it + if (selectedEntities[0] === entityID) { + startRenamingEntity(entityID); } } - - selectedEntities = selection; - - this.className = 'selected'; + + updateSelectedEntities(selection, false); EventBridge.emitWebEvent(JSON.stringify({ type: "selectionUpdate", focus: false, entityIds: selection, })); - - refreshFooter(); } function onRowDoubleClicked() { + let selection = [this.dataset.entityID]; + updateSelectedEntities(selection, false); + EventBridge.emitWebEvent(JSON.stringify({ type: "selectionUpdate", focus: true, - entityIds: [this.dataset.entityId], + entityIds: selection, })); } - - const BYTES_PER_MEGABYTE = 1024 * 1024; - + function decimalMegabytes(number) { return number ? (number / BYTES_PER_MEGABYTE).toFixed(1) : ""; } @@ -156,314 +548,691 @@ function loaded() { return number ? number : ""; } - function addEntity(id, name, type, url, locked, visible, verticesCount, texturesCount, texturesSize, hasTransparent, - isBaked, drawCalls, hasScript) { - - var urlParts = url.split('/'); - var filename = urlParts[urlParts.length - 1]; - - var IMAGE_MODEL_NAME = 'default-image-model.fbx'; - - if (filename === IMAGE_MODEL_NAME) { - type = "Image"; - } - - if (entities[id] === undefined) { - entityList.add([{ - id: id, name: name, type: type, url: filename, locked: locked, visible: visible, - verticesCount: displayIfNonZero(verticesCount), texturesCount: displayIfNonZero(texturesCount), - texturesSize: decimalMegabytes(texturesSize), hasTransparent: hasTransparent, - isBaked: isBaked, drawCalls: displayIfNonZero(drawCalls), hasScript: hasScript - }], - function (items) { - var currentElement = items[0].elm; - var id = items[0]._values.id; - entities[id] = { - id: id, - name: name, - el: currentElement, - item: items[0] + function getFilename(url) { + let urlParts = url.split('/'); + return urlParts[urlParts.length - 1]; + } + + function updateEntityData(entityData) { + entities = []; + entitiesByID = {}; + visibleEntities.length = 0; // maintains itemData reference in ListView + + PROFILE("map-data", function() { + entityData.forEach(function(entity) { + let type = entity.type; + let filename = getFilename(entity.url); + if (filename === IMAGE_MODEL_NAME) { + type = "Image"; + } + + let entityData = { + id: entity.id, + name: entity.name, + type: type, + url: filename, + fullUrl: entity.url, + locked: entity.locked, + visible: entity.visible, + verticesCount: displayIfNonZero(entity.verticesCount), + texturesCount: displayIfNonZero(entity.texturesCount), + texturesSize: decimalMegabytes(entity.texturesSize), + hasTransparent: entity.hasTransparent, + isBaked: entity.isBaked, + drawCalls: displayIfNonZero(entity.drawCalls), + hasScript: entity.hasScript, + elRow: null, // if this entity has a visible row element assigned to it + selected: false // if this entity is selected for edit regardless of having a visible row }; - currentElement.setAttribute('id', 'entity_' + id); - currentElement.setAttribute('title', url); - currentElement.dataset.entityId = id; - currentElement.onclick = onRowClicked; - currentElement.ondblclick = onRowDoubleClicked; + + entities.push(entityData); + entitiesByID[entityData.id] = entityData; }); - } else { - var item = entities[id].item; - item.values({ name: name, url: filename, locked: locked, visible: visible }); - } + }); + + refreshEntityList(); } + + function refreshEntityList() { + PROFILE("refresh-entity-list", function() { + PROFILE("filter", function() { + let searchTerm = elFilterSearch.value.toLowerCase(); + visibleEntities = entities.filter(function(e) { + let type = e.type === "Box" || e.type === "Sphere" ? "Shape" : e.type; + let typeFilter = typeFilters.indexOf(type) > -1; + let searchFilter = searchTerm === '' || (e.name.toLowerCase().indexOf(searchTerm) > -1 || + e.type.toLowerCase().indexOf(searchTerm) > -1 || + e.fullUrl.toLowerCase().indexOf(searchTerm) > -1 || + e.id.toLowerCase().indexOf(searchTerm) > -1); + return typeFilter && searchFilter; + }); + }); + + PROFILE("sort", function() { + let cmp = currentSortOrder === ASCENDING_SORT ? COMPARE_ASCENDING : COMPARE_DESCENDING; + visibleEntities.sort(cmp); + }); + PROFILE("update-dom", function() { + entityList.itemData = visibleEntities; + entityList.refresh(); + updateColumnWidths(); + }); + + refreshFooter(); + refreshNoEntitiesMessage(); + }); + } + function removeEntities(deletedIDs) { - for (i = 0, length = deletedIDs.length; i < length; i++) { - delete entities[deletedIDs[i]]; - entityList.remove("id", deletedIDs[i]); + // Loop from the back so we can pop items off while iterating + + // delete any entities matching deletedIDs list from entities and entitiesByID lists + // if the entity had an associated row element then ensure row is unselected and clear it's entity + for (let j = entities.length - 1; j >= 0; --j) { + let id = entities[j].id; + for (let i = 0, length = deletedIDs.length; i < length; ++i) { + if (id === deletedIDs[i]) { + let elRow = entities[j].elRow; + if (elRow) { + elRow.className = ''; + elRow.dataset.entityID = EMPTY_ENTITY_ID; + } + entities.splice(j, 1); + delete entitiesByID[id]; + break; + } + } } - } - - function scheduleRefreshEntityList() { - var REFRESH_DELAY = 50; - if (refreshEntityListTimer) { - clearTimeout(refreshEntityListTimer); + + // delete any entities matching deletedIDs list from selectedEntities list + for (let j = selectedEntities.length - 1; j >= 0; --j) { + let id = selectedEntities[j].id; + for (let i = 0, length = deletedIDs.length; i < length; ++i) { + if (id === deletedIDs[i]) { + selectedEntities.splice(j, 1); + break; + } + } } - refreshEntityListTimer = setTimeout(refreshEntityListObject, REFRESH_DELAY); - } - function clearEntities() { - entities = {}; - entityList.clear(); + // delete any entities matching deletedIDs list from visibleEntities list + // if this was a row that was above our current row offset (a hidden top row in the top buffer), + // then decrease row offset accordingly + let firstVisibleRow = entityList.getFirstVisibleRowIndex(); + for (let j = visibleEntities.length - 1; j >= 0; --j) { + let id = visibleEntities[j].id; + for (let i = 0, length = deletedIDs.length; i < length; ++i) { + if (id === deletedIDs[i]) { + if (j < firstVisibleRow && entityList.rowOffset > 0) { + entityList.rowOffset--; + } + visibleEntities.splice(j, 1); + break; + } + } + } + + entityList.refresh(); + refreshFooter(); + refreshNoEntitiesMessage(); } - - var elSortOrder = { - name: document.querySelector('#entity-name .sort-order'), - type: document.querySelector('#entity-type .sort-order'), - url: document.querySelector('#entity-url .sort-order'), - locked: document.querySelector('#entity-locked .sort-order'), - visible: document.querySelector('#entity-visible .sort-order'), - verticesCount: document.querySelector('#entity-verticesCount .sort-order'), - texturesCount: document.querySelector('#entity-texturesCount .sort-order'), - texturesSize: document.querySelector('#entity-texturesSize .sort-order'), - hasTransparent: document.querySelector('#entity-hasTransparent .sort-order'), - isBaked: document.querySelector('#entity-isBaked .sort-order'), - drawCalls: document.querySelector('#entity-drawCalls .sort-order'), - hasScript: document.querySelector('#entity-hasScript .sort-order'), - } - function setSortColumn(column) { - if (currentSortColumn == column) { - currentSortOrder = currentSortOrder == "asc" ? "desc" : "asc"; - } else { - elSortOrder[currentSortColumn].innerHTML = ""; - currentSortColumn = column; - currentSortOrder = "asc"; + + function clearEntities() { + // clear the associated entity ID from all visible row elements + let firstVisibleRow = entityList.getFirstVisibleRowIndex(); + let lastVisibleRow = entityList.getLastVisibleRowIndex(); + for (let i = firstVisibleRow; i <= lastVisibleRow && i < visibleEntities.length; i++) { + let entity = visibleEntities[i]; + entity.elRow.dataset.entityID = EMPTY_ENTITY_ID; } - elSortOrder[column].innerHTML = currentSortOrder == "asc" ? ASCENDING_STRING : DESCENDING_STRING; - entityList.sort(currentSortColumn, { order: currentSortOrder }); + + entities = []; + entitiesByID = {}; + visibleEntities.length = 0; // maintains itemData reference in ListView + + entityList.resetToTop(); + entityList.clear(); + + refreshFooter(); + refreshNoEntitiesMessage(); } - setSortColumn('type'); + function setSortColumn(column) { + PROFILE("set-sort-column", function() { + if (currentSortColumn === column) { + currentSortOrder *= -1; + } else { + elSortOrders[currentSortColumn].innerHTML = ""; + currentSortColumn = column; + currentSortOrder = ASCENDING_SORT; + } + refreshSortOrder(); + refreshEntityList(); + }); + } + + function refreshSortOrder() { + elSortOrders[currentSortColumn].innerHTML = currentSortOrder === ASCENDING_SORT ? ASCENDING_STRING : DESCENDING_STRING; + } + function refreshEntities() { - clearEntities(); EventBridge.emitWebEvent(JSON.stringify({ type: 'refresh' })); } - + function refreshFooter() { - if (selectedEntities.length > 1) { - elFooter.firstChild.nodeValue = selectedEntities.length + " entities selected"; - } else if (selectedEntities.length === 1) { - elFooter.firstChild.nodeValue = "1 entity selected"; - } else if (entityList.visibleItems.length === 1) { - elFooter.firstChild.nodeValue = "1 entity found"; + elSelectedEntitiesCount.innerText = selectedEntities.length; + elVisibleEntitiesCount.innerText = visibleEntities.length; + } + + function refreshNoEntitiesMessage() { + if (visibleEntities.length > 0) { + elNoEntitiesMessage.style.display = "none"; } else { - elFooter.firstChild.nodeValue = entityList.visibleItems.length + " entities found"; + elNoEntitiesMessage.style.display = "block"; } } - - function refreshEntityListObject() { - refreshEntityListTimer = null; - entityList.sort(currentSortColumn, { order: currentSortOrder }); - entityList.search(elFilter.value); - refreshFooter(); - } - - function updateSelectedEntities(selectedIDs) { - var notFound = false; - for (var id in entities) { - entities[id].el.className = ''; - } - + + function updateSelectedEntities(selectedIDs, autoScroll) { + let notFound = false; + + // reset all currently selected entities and their rows first + selectedEntities.forEach(function(id) { + let entity = entitiesByID[id]; + if (entity !== undefined) { + entity.selected = false; + if (entity.elRow) { + entity.elRow.className = ''; + } + } + }); + + // then reset selected entities list with newly selected entities and set them selected selectedEntities = []; - for (var i = 0; i < selectedIDs.length; i++) { - var id = selectedIDs[i]; + selectedIDs.forEach(function(id) { selectedEntities.push(id); - if (id in entities) { - var entity = entities[id]; - entity.el.className = 'selected'; + let entity = entitiesByID[id]; + if (entity !== undefined) { + entity.selected = true; + if (entity.elRow) { + entity.elRow.className = 'selected'; + } } else { notFound = true; } + }); + + if (autoScroll && selectedIDs.length > 0) { + let firstItem = Number.MAX_VALUE; + let lastItem = -1; + let itemFound = false; + visibleEntities.forEach(function(entity, index) { + if (selectedIDs.indexOf(entity.id) !== -1) { + if (firstItem > index) { + firstItem = index; + } + if (lastItem < index) { + lastItem = index; + } + itemFound = true; + } + }); + if (itemFound) { + entityList.scrollToRow(firstItem, lastItem); + } } + elToggleSpaceMode.disabled = selectedIDs.length > 1; + refreshFooter(); return notFound; } - - elRefresh.onclick = function() { - refreshEntities(); + + function createRow() { + let elRow = document.createElement("tr"); + columns.forEach(function(column) { + let elRowColumn = document.createElement("td"); + elRowColumn.className = createColumnClassName(column.columnID); + elRow.appendChild(elRowColumn); + }); + elRow.oncontextmenu = onRowContextMenu; + elRow.onclick = onRowClicked; + elRow.ondblclick = onRowDoubleClicked; + return elRow; } - elToggleLocked.onclick = function () { - EventBridge.emitWebEvent(JSON.stringify({ type: 'toggleLocked' })); - } - elToggleVisible.onclick = function () { - EventBridge.emitWebEvent(JSON.stringify({ type: 'toggleVisible' })); - } - elExport.onclick = function() { - EventBridge.emitWebEvent(JSON.stringify({ type: 'export'})); - } - elPal.onclick = function () { - EventBridge.emitWebEvent(JSON.stringify({ type: 'pal' })); - } - elDelete.onclick = function() { - EventBridge.emitWebEvent(JSON.stringify({ type: 'delete' })); - } - - document.addEventListener("keydown", function (keyDownEvent) { - if (keyDownEvent.target.nodeName === "INPUT") { - return; - } - var keyCode = keyDownEvent.keyCode; - if (keyCode === DELETE) { - EventBridge.emitWebEvent(JSON.stringify({ type: 'delete' })); - refreshEntities(); - } - if (keyDownEvent.keyCode === KEY_P && keyDownEvent.ctrlKey) { - if (keyDownEvent.shiftKey) { - EventBridge.emitWebEvent(JSON.stringify({ type: 'unparent' })); + + function updateRow(elRow, itemData) { + // update all column texts and glyphs to this entity's data + for (let i = 0; i < columns.length; ++i) { + let column = columns[i]; + let elCell = elRow.childNodes[i]; + if (column.data.glyph) { + elCell.innerHTML = itemData[column.data.propertyID] ? column.data.columnHeader : null; } else { - EventBridge.emitWebEvent(JSON.stringify({ type: 'parent' })); + elCell.innerText = itemData[column.data.propertyID]; + } + elCell.style = "min-width:" + column.widthPx + "px;" + "max-width:" + column.widthPx + "px;"; + elCell.className = createColumnClassName(column.columnID); + } + + // if this entity was previously selected flag it's row as selected + if (itemData.selected) { + elRow.className = 'selected'; + } else { + elRow.className = ''; + } + + // if this row previously had an associated entity ID that wasn't the new entity ID then clear + // the ID from the row and the row element from the previous entity's data, then set the new + // entity ID to the row and the row element to the new entity's data + let prevEntityID = elRow.dataset.entityID; + let newEntityID = itemData.id; + let validPrevItemID = prevEntityID !== undefined && prevEntityID !== EMPTY_ENTITY_ID; + if (validPrevItemID && prevEntityID !== newEntityID && entitiesByID[prevEntityID].elRow === elRow) { + elRow.dataset.entityID = EMPTY_ENTITY_ID; + entitiesByID[prevEntityID].elRow = null; + } + if (!validPrevItemID || prevEntityID !== newEntityID) { + elRow.dataset.entityID = newEntityID; + entitiesByID[newEntityID].elRow = elRow; + } + } + + function clearRow(elRow) { + // reset all texts and glyphs for each of the row's columns + for (let i = 0; i < columns.length; ++i) { + let cell = elRow.childNodes[i]; + if (columns[i].data.glyph) { + cell.innerHTML = ""; + } else { + cell.innerText = ""; } } - }, false); - var isFilterInView = false; - var FILTER_IN_VIEW_ATTRIBUTE = "pressed"; - elNoEntitiesInView.style.display = "none"; - elInView.onclick = function () { + // clear the row from any associated entity + let entityID = elRow.dataset.entityID; + if (entityID && entitiesByID[entityID]) { + entitiesByID[entityID].elRow = null; + } + + // reset the row to hidden and clear the entity from the row + elRow.className = ''; + elRow.dataset.entityID = EMPTY_ENTITY_ID; + } + + function onToggleFilterInView() { isFilterInView = !isFilterInView; if (isFilterInView) { - elInView.setAttribute(FILTER_IN_VIEW_ATTRIBUTE, FILTER_IN_VIEW_ATTRIBUTE); - elNoEntitiesInView.style.display = "inline"; + elFilterInView.setAttribute(FILTER_IN_VIEW_ATTRIBUTE, FILTER_IN_VIEW_ATTRIBUTE); } else { - elInView.removeAttribute(FILTER_IN_VIEW_ATTRIBUTE); - elNoEntitiesInView.style.display = "none"; + elFilterInView.removeAttribute(FILTER_IN_VIEW_ATTRIBUTE); } EventBridge.emitWebEvent(JSON.stringify({ type: "filterInView", filterInView: isFilterInView })); refreshEntities(); } - - elRadius.onchange = function () { - elRadius.value = Math.max(elRadius.value, 0); - EventBridge.emitWebEvent(JSON.stringify({ type: 'radius', radius: elRadius.value })); + + function onRadiusChange() { + elFilterRadius.value = elFilterRadius.value.replace(/[^0-9]/g, ''); + elFilterRadius.value = Math.max(elFilterRadius.value, 0); + EventBridge.emitWebEvent(JSON.stringify({ type: 'radius', radius: elFilterRadius.value })); refreshEntities(); - elNoEntitiesRadius.firstChild.nodeValue = elRadius.value; + } + + function getColumnIndex(columnID) { + for (let i = 0; i < columns.length; ++i) { + if (columns[i].columnID === columnID) { + return i; + } + } + return -1; + } + + function createColumnClassName(columnID) { + let column = columnsByID[columnID]; + let visible = column.elTh.style.visibility !== "hidden"; + let className = column.data.glyph ? "glyph" : ""; + className += visible ? "" : " hidden"; + return className; + } + + function isColumnsDropdownVisible() { + return elColumnsOptions.style.display === "block"; + } + + function toggleColumnsDropdown() { + elColumnsOptions.style.display = isColumnsDropdownVisible() ? "none" : "block"; + } + + function onToggleColumnsDropdown(event) { + toggleColumnsDropdown(); + if (isTypeDropdownVisible()) { + toggleTypeDropdown(); + } + event.stopPropagation(); + } + + function toggleColumn(elInput, refresh) { + let columnID = elInput.getAttribute("columnID"); + let columnChecked = elInput.checked; + + if (columnChecked) { + let widthNeeded = columnsByID[columnID].data.initialWidth; + + let numberVisibleColumns = 0; + for (let i = 0; i < columns.length; ++i) { + let column = columns[i]; + if (column.columnID === columnID) { + column.width = widthNeeded; + } else if (column.width > 0) { + ++numberVisibleColumns; + } + } + + for (let i = 0; i < columns.length; ++i) { + let column = columns[i]; + if (column.columnID !== columnID && column.width > 0) { + column.width -= column.width * widthNeeded; + } + } + } else { + let widthLoss = 0; + + let numberVisibleColumns = 0; + for (let i = 0; i < columns.length; ++i) { + let column = columns[i]; + if (column.columnID === columnID) { + widthLoss = column.width; + column.width = 0; + } else if (column.width > 0) { + ++numberVisibleColumns; + } + } + + for (let i = 0; i < columns.length; ++i) { + let column = columns[i]; + if (column.columnID !== columnID && column.width > 0) { + let newTotalWidth = (1 - widthLoss); + column.width += (column.width / newTotalWidth) * widthLoss; + } + } + } + + updateColumnWidths(); + } + + function onToggleColumn(event) { + let elTarget = event.target; + if (elTarget instanceof HTMLInputElement) { + toggleColumn(elTarget, true); + } + event.stopPropagation(); + } + + function isTypeDropdownVisible() { + return elFilterTypeOptions.style.display === "block"; + } + + function toggleTypeDropdown() { + elFilterTypeOptions.style.display = isTypeDropdownVisible() ? "none" : "block"; + } + + function onToggleTypeDropdown(event) { + toggleTypeDropdown(); + if (isColumnsDropdownVisible()) { + toggleColumnsDropdown(); + } + event.stopPropagation(); + } + + function toggleTypeFilter(elInput, refresh) { + let type = elInput.getAttribute("filterType"); + let typeChecked = elInput.checked; + + let typeFilterIndex = typeFilters.indexOf(type); + if (!typeChecked && typeFilterIndex > -1) { + typeFilters.splice(typeFilterIndex, 1); + } else if (typeChecked && typeFilterIndex === -1) { + typeFilters.push(type); + } + + if (typeFilters.length === 0) { + elFilterTypeText.innerText = "No Types"; + } else if (typeFilters.length === FILTER_TYPES.length) { + elFilterTypeText.innerText = "All Types"; + } else { + elFilterTypeText.innerText = "Types..."; + } + + if (refresh) { + refreshEntityList(); + } + } + + function onToggleTypeFilter(event) { + let elTarget = event.target; + if (elTarget instanceof HTMLInputElement) { + toggleTypeFilter(elTarget, true); + } + event.stopPropagation(); + } + + function onBodyClick(event) { + // if clicking anywhere outside of the multiselect dropdowns (since click event bubbled up to onBodyClick and + // propagation wasn't stopped in the toggle type/column callbacks) and the dropdown is open then close it + if (isTypeDropdownVisible()) { + toggleTypeDropdown(); + } + if (isColumnsDropdownVisible()) { + toggleColumnsDropdown(); + } + } + + function onStartResize(event) { + startResizeEvent = event; + resizeColumnIndex = parseInt(this.getAttribute("columnIndex")); + event.stopPropagation(); + } + + function updateColumnWidths() { + let fullWidth = elEntityTableBody.offsetWidth; + let remainingWidth = fullWidth; + let scrollbarVisible = elEntityTableScroll.scrollHeight > elEntityTableScroll.clientHeight; + let resizerRight = scrollbarVisible ? SCROLLBAR_WIDTH - RESIZER_WIDTH/2 : -RESIZER_WIDTH/2; + let visibleColumns = 0; + + for (let i = columns.length - 1; i > 0; --i) { + let column = columns[i]; + column.widthPx = Math.ceil(column.width * fullWidth); + column.elTh.style = "min-width:" + column.widthPx + "px;" + "max-width:" + column.widthPx + "px;"; + let columnVisible = column.width > 0; + column.elTh.style.visibility = columnVisible ? "visible" : "hidden"; + if (column.elResizer) { + column.elResizer.style = "right:" + resizerRight + "px;"; + column.elResizer.style.visibility = columnVisible && visibleColumns > 0 ? "visible" : "hidden"; + } + resizerRight += column.widthPx; + remainingWidth -= column.widthPx; + if (columnVisible) { + ++visibleColumns; + } + } + + // assign all remaining space to the first column + let column = columns[0]; + column.widthPx = remainingWidth; + column.width = remainingWidth / fullWidth; + column.elTh.style = "min-width:" + column.widthPx + "px;" + "max-width:" + column.widthPx + "px;"; + let columnVisible = column.width > 0; + column.elTh.style.visibility = columnVisible ? "visible" : "hidden"; + if (column.elResizer) { + column.elResizer.style = "right:" + resizerRight + "px;"; + column.elResizer.style.visibility = columnVisible && visibleColumns > 0 ? "visible" : "hidden"; + } + + entityList.refresh(); + } + + document.onmousemove = function(ev) { + if (startResizeEvent) { + startTh = null; + + let column = columns[resizeColumnIndex]; + + let nextColumnIndex = resizeColumnIndex + 1; + let nextColumn = columns[nextColumnIndex]; + while (nextColumn.width === 0) { + nextColumn = columns[++nextColumnIndex]; + } + + let fullWidth = elEntityTableBody.offsetWidth; + let dx = ev.clientX - startResizeEvent.clientX; + let dPct = dx / fullWidth; + + let newColWidth = column.width + dPct; + let newNextColWidth = nextColumn.width - dPct; + + if (newColWidth * fullWidth >= MINIMUM_COLUMN_WIDTH && newNextColWidth * fullWidth >= MINIMUM_COLUMN_WIDTH) { + column.width += dPct; + nextColumn.width -= dPct; + updateColumnWidths(); + startResizeEvent = ev; + } + } + } + + document.onmouseup = function(ev) { + startResizeEvent = null; + ev.stopPropagation(); } + function setSpaceMode(spaceMode) { + if (spaceMode === "local") { + elToggleSpaceMode.className = "space-mode-local hifi-edit-button"; + elToggleSpaceMode.innerText = "Local"; + } else { + elToggleSpaceMode.className = "space-mode-world hifi-edit-button"; + elToggleSpaceMode.innerText = "World"; + } + } + + const KEY_CODES = { + BACKSPACE: 8, + DELETE: 46 + }; + + document.addEventListener("keyup", function (keyUpEvent) { + if (keyUpEvent.target.nodeName === "INPUT") { + return; + } + + let {code, key, keyCode, altKey, ctrlKey, metaKey, shiftKey} = keyUpEvent; + + let controlKey = window.navigator.platform.startsWith("Mac") ? metaKey : ctrlKey; + + let keyCodeString; + switch (keyCode) { + case KEY_CODES.DELETE: + keyCodeString = "Delete"; + break; + case KEY_CODES.BACKSPACE: + keyCodeString = "Backspace"; + break; + default: + keyCodeString = String.fromCharCode(keyUpEvent.keyCode); + break; + } + + if (controlKey && keyCodeString === "A") { + let visibleEntityIDs = visibleEntities.map(visibleEntity => visibleEntity.id); + let selectionIncludesAllVisibleEntityIDs = visibleEntityIDs.every(visibleEntityID => { + return selectedEntities.includes(visibleEntityID); + }); + + let selection = []; + + if (!selectionIncludesAllVisibleEntityIDs) { + selection = visibleEntityIDs; + } + + updateSelectedEntities(selection); + + EventBridge.emitWebEvent(JSON.stringify({ + type: "selectionUpdate", + focus: false, + entityIds: selection, + })); + + return; + } + + + EventBridge.emitWebEvent(JSON.stringify({ + type: 'keyUpEvent', + keyUpEvent: { + code, + key, + keyCode, + keyCodeString, + altKey, + controlKey, + shiftKey, + } + })); + }, false); + if (window.EventBridge !== undefined) { EventBridge.scriptEventReceived.connect(function(data) { data = JSON.parse(data); - if (data.type === "clearEntityList") { clearEntities(); - } else if (data.type == "selectionUpdate") { - var notFound = updateSelectedEntities(data.selectedIDs); + } else if (data.type === "selectionUpdate") { + let notFound = updateSelectedEntities(data.selectedIDs, true); if (notFound) { refreshEntities(); } } else if (data.type === "update" && data.selectedIDs !== undefined) { - var newEntities = data.entities; - if (newEntities && newEntities.length == 0) { - elNoEntitiesMessage.style.display = "block"; - elFooter.firstChild.nodeValue = "0 entities found"; - } else if (newEntities) { - elNoEntitiesMessage.style.display = "none"; - for (var i = 0; i < newEntities.length; i++) { - var id = newEntities[i].id; - addEntity(id, newEntities[i].name, newEntities[i].type, newEntities[i].url, - newEntities[i].locked ? LOCKED_GLYPH : null, - newEntities[i].visible ? VISIBLE_GLYPH : null, - newEntities[i].verticesCount, newEntities[i].texturesCount, newEntities[i].texturesSize, - newEntities[i].hasTransparent ? TRANSPARENCY_GLYPH : null, - newEntities[i].isBaked ? BAKED_GLYPH : null, - newEntities[i].drawCalls, - newEntities[i].hasScript ? SCRIPT_GLYPH : null); + PROFILE("update", function() { + let newEntities = data.entities; + if (newEntities) { + if (newEntities.length === 0) { + clearEntities(); + } else { + updateEntityData(newEntities); + updateSelectedEntities(data.selectedIDs, true); + } } - updateSelectedEntities(data.selectedIDs); - scheduleRefreshEntityList(); - resize(); - } + setSpaceMode(data.spaceMode); + }); } else if (data.type === "removeEntities" && data.deletedIDs !== undefined && data.selectedIDs !== undefined) { removeEntities(data.deletedIDs); - updateSelectedEntities(data.selectedIDs); - scheduleRefreshEntityList(); + updateSelectedEntities(data.selectedIDs, true); } else if (data.type === "deleted" && data.ids) { removeEntities(data.ids); - refreshFooter(); + } else if (data.type === "setSpaceMode") { + setSpaceMode(data.spaceMode); } }); - setTimeout(refreshEntities, 1000); } - - function resize() { - // Take up available window space - elEntityTableScroll.style.height = window.innerHeight - 207; - - var SCROLLABAR_WIDTH = 21; - var tds = document.querySelectorAll("#entity-table-body tr:first-child td"); - var ths = document.querySelectorAll("#entity-table thead th"); - if (tds.length >= ths.length) { - // Update the widths of the header cells to match the body - for (var i = 0; i < ths.length; i++) { - ths[i].width = tds[i].offsetWidth; - } - } else { - // Reasonable widths if nothing is displayed - var tableWidth = document.getElementById("entity-table").offsetWidth - SCROLLABAR_WIDTH; - if (showExtraInfo) { - ths[0].width = 0.10 * tableWidth; - ths[1].width = 0.20 * tableWidth; - ths[2].width = 0.20 * tableWidth; - ths[3].width = 0.04 * tableWidth; - ths[4].width = 0.04 * tableWidth; - ths[5].width = 0.08 * tableWidth; - ths[6].width = 0.08 * tableWidth; - ths[7].width = 0.10 * tableWidth; - ths[8].width = 0.04 * tableWidth; - ths[9].width = 0.08 * tableWidth; - ths[10].width = 0.04 * tableWidth + SCROLLABAR_WIDTH; - } else { - ths[0].width = 0.16 * tableWidth; - ths[1].width = 0.34 * tableWidth; - ths[2].width = 0.34 * tableWidth; - ths[3].width = 0.08 * tableWidth; - ths[4].width = 0.08 * tableWidth; - } - } - }; - - window.onresize = resize; - elFilter.onchange = resize; - elFilter.onblur = refreshFooter; - - - var showExtraInfo = false; - var COLLAPSE_EXTRA_INFO = "E"; - var EXPAND_EXTRA_INFO = "D"; - - function toggleInfo(event) { - showExtraInfo = !showExtraInfo; - if (showExtraInfo) { - elEntityTable.className = "showExtraInfo"; - elInfoToggleGlyph.innerHTML = COLLAPSE_EXTRA_INFO; - } else { - elEntityTable.className = ""; - elInfoToggleGlyph.innerHTML = EXPAND_EXTRA_INFO; - } - resize(); - event.stopPropagation(); - } - elInfoToggle.addEventListener("click", toggleInfo, true); - - - resize(); + + refreshSortOrder(); + refreshEntities(); + + window.onresize = updateColumnWidths; }); - + augmentSpinButtons(); - // Disable right-click context menu which is not visible in the HMD and makes it seem like the app has locked document.addEventListener("contextmenu", function (event) { + entityListContextMenu.close(); + + // Disable default right-click context menu which is not visible in the HMD and makes it seem like the app has locked event.preventDefault(); }, false); + + // close context menu when switching focus to another window + $(window).blur(function() { + entityListContextMenu.close(); + }); } diff --git a/scripts/system/html/js/entityListContextMenu.js b/scripts/system/html/js/entityListContextMenu.js new file mode 100644 index 0000000000..d71719f252 --- /dev/null +++ b/scripts/system/html/js/entityListContextMenu.js @@ -0,0 +1,163 @@ +// +// entityListContextMenu.js +// +// exampleContextMenus.js was originally created by David Rowe on 22 Aug 2018. +// Modified to entityListContextMenu.js by Thijs Wenker on 10 Oct 2018 +// Copyright 2018 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +/* eslint-env browser */ +const CONTEXT_MENU_CLASS = "context-menu"; + +/** + * ContextMenu class for EntityList + * @constructor + */ +function EntityListContextMenu() { + this._elContextMenu = null; + this._onSelectedCallback = null; + this._listItems = []; + this._initialize(); +} + +EntityListContextMenu.prototype = { + + /** + * @private + */ + _elContextMenu: null, + + /** + * @private + */ + _onSelectedCallback: null, + + /** + * @private + */ + _selectedEntityID: null, + + /** + * @private + */ + _listItems: null, + + /** + * Close the context menu + */ + close: function() { + if (this.isContextMenuOpen()) { + this._elContextMenu.style.display = "none"; + } + }, + + isContextMenuOpen: function() { + return this._elContextMenu.style.display === "block"; + }, + + /** + * Open the context menu + * @param clickEvent + * @param selectedEntityID + * @param enabledOptions + */ + open: function(clickEvent, selectedEntityID, enabledOptions) { + this._selectedEntityID = selectedEntityID; + + this._listItems.forEach(function(listItem) { + let enabled = enabledOptions.includes(listItem.label); + listItem.enabled = enabled; + listItem.element.setAttribute('class', enabled ? '' : 'disabled'); + }); + + this._elContextMenu.style.display = "block"; + this._elContextMenu.style.left + = Math.min(clickEvent.pageX, document.body.offsetWidth - this._elContextMenu.offsetWidth).toString() + "px"; + this._elContextMenu.style.top + = Math.min(clickEvent.pageY, document.body.clientHeight - this._elContextMenu.offsetHeight).toString() + "px"; + clickEvent.stopPropagation(); + }, + + /** + * Set the callback for when a menu item is selected + * @param onSelectedCallback + */ + setOnSelectedCallback: function(onSelectedCallback) { + this._onSelectedCallback = onSelectedCallback; + }, + + /** + * Add a labeled item to the context menu + * @param itemLabel + * @private + */ + _addListItem: function(itemLabel) { + let elListItem = document.createElement("li"); + elListItem.innerText = itemLabel; + + let listItem = { + label: itemLabel, + element: elListItem, + enabled: false + }; + + elListItem.addEventListener("click", function () { + if (listItem.enabled && this._onSelectedCallback) { + this._onSelectedCallback.call(this, itemLabel, this._selectedEntityID); + } + }.bind(this), false); + + elListItem.setAttribute('class', 'disabled'); + + this._listItems.push(listItem); + this._elContextMenu.appendChild(elListItem); + }, + + /** + * Add a separator item to the context menu + * @private + */ + _addListSeparator: function() { + let elListItem = document.createElement("li"); + elListItem.setAttribute('class', 'separator'); + this._elContextMenu.appendChild(elListItem); + }, + + /** + * Initialize the context menu. + * @private + */ + _initialize: function() { + this._elContextMenu = document.createElement("ul"); + this._elContextMenu.setAttribute("class", CONTEXT_MENU_CLASS); + document.body.appendChild(this._elContextMenu); + + this._addListItem("Cut"); + this._addListItem("Copy"); + this._addListItem("Paste"); + this._addListSeparator(); + this._addListItem("Rename"); + this._addListItem("Duplicate"); + this._addListItem("Delete"); + + // Ignore clicks on context menu background or separator. + this._elContextMenu.addEventListener("click", function(event) { + // Sink clicks on context menu background or separator but let context menu item clicks through. + if (event.target.classList.contains(CONTEXT_MENU_CLASS)) { + event.stopPropagation(); + } + }); + + // Provide means to close context menu without clicking menu item. + document.body.addEventListener("click", this.close.bind(this)); + document.body.addEventListener("keydown", function(event) { + // Close context menu with Esc key. + if (this.isContextMenuOpen() && event.key === "Escape") { + this.close(); + } + }.bind(this)); + } +}; diff --git a/scripts/system/html/js/entityProperties.js b/scripts/system/html/js/entityProperties.js index 31984d2d01..c49e0d88f6 100644 --- a/scripts/system/html/js/entityProperties.js +++ b/scripts/system/html/js/entityProperties.js @@ -1,18 +1,16 @@ // entityProperties.js // // Created by Ryan Huffman on 13 Nov 2014 +// Modified by David Back on 19 Oct 2018 // Copyright 2014 High Fidelity, Inc. // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -/* global alert, augmentSpinButtons, clearTimeout, console, document, Element, EventBridge, - HifiEntityUI, JSONEditor, openEventBridge, setTimeout, window, _ $ */ - -var PI = 3.14159265358979; -var DEGREES_TO_RADIANS = PI / 180.0; -var RADIANS_TO_DEGREES = 180.0 / PI; -var ICON_FOR_TYPE = { +/* global alert, augmentSpinButtons, clearTimeout, console, document, Element, + EventBridge, JSONEditor, openEventBridge, setTimeout, window, _ $ */ + +const ICON_FOR_TYPE = { Box: "V", Sphere: "n", Shape: "n", @@ -27,250 +25,1725 @@ var ICON_FOR_TYPE = { Multiple: "", PolyLine: "", Material: "" +}; + +const DEGREES_TO_RADIANS = Math.PI / 180.0; + +const NO_SELECTION = "w"; + +const PROPERTY_SPACE_MODE = { + ALL: 0, + LOCAL: 1, + WORLD: 2 }; -var EDITOR_TIMEOUT_DURATION = 1500; -var KEY_P = 80; // Key code for letter p used for Parenting hotkey. +const GROUPS = [ + { + id: "base", + properties: [ + { + label: NO_SELECTION, + type: "icon", + icons: ICON_FOR_TYPE, + propertyID: "type", + replaceID: "placeholder-property-type", + }, + { + label: "Name", + type: "string", + propertyID: "name", + placeholder: "Name", + replaceID: "placeholder-property-name", + }, + { + label: "ID", + type: "string", + propertyID: "id", + placeholder: "ID", + readOnly: true, + replaceID: "placeholder-property-id", + }, + { + label: "Description", + type: "string", + propertyID: "description", + }, + { + label: "Parent", + type: "string", + propertyID: "parentID", + }, + { + label: "Parent Joint Index", + type: "number", + propertyID: "parentJointIndex", + }, + { + label: "", + glyph: "", + type: "bool", + propertyID: "locked", + replaceID: "placeholder-property-locked", + }, + { + label: "", + glyph: "", + type: "bool", + propertyID: "visible", + replaceID: "placeholder-property-visible", + }, + ] + }, + { + id: "shape", + addToGroup: "base", + properties: [ + { + label: "Shape", + type: "dropdown", + options: { Cube: "Box", Sphere: "Sphere", Tetrahedron: "Tetrahedron", Octahedron: "Octahedron", + Icosahedron: "Icosahedron", Dodecahedron: "Dodecahedron", Hexagon: "Hexagon", + Triangle: "Triangle", Octagon: "Octagon", Cylinder: "Cylinder", Cone: "Cone", + Circle: "Circle", Quad: "Quad" }, + propertyID: "shape", + }, + { + label: "Color", + type: "color", + propertyID: "color", + }, + ] + }, + { + id: "text", + addToGroup: "base", + properties: [ + { + label: "Text", + type: "string", + propertyID: "text", + }, + { + label: "Text Color", + type: "color", + propertyID: "textColor", + }, + { + label: "Background Color", + type: "color", + propertyID: "backgroundColor", + }, + { + label: "Line Height", + type: "number", + min: 0, + step: 0.005, + decimals: 4, + unit: "m", + propertyID: "lineHeight" + }, + { + label: "Face Camera", + type: "bool", + propertyID: "faceCamera" + }, + ] + }, + { + id: "zone", + addToGroup: "base", + properties: [ + { + label: "Flying Allowed", + type: "bool", + propertyID: "flyingAllowed", + }, + { + label: "Ghosting Allowed", + type: "bool", + propertyID: "ghostingAllowed", + }, + { + label: "Filter", + type: "string", + propertyID: "filterURL", + }, + { + label: "Key Light", + type: "dropdown", + options: { inherit: "Inherit", disabled: "Off", enabled: "On" }, + propertyID: "keyLightMode", + + }, + { + label: "Key Light Color", + type: "color", + propertyID: "keyLight.color", + showPropertyRule: { "keyLightMode": "enabled" }, + }, + { + label: "Light Intensity", + type: "number", + min: 0, + max: 10, + step: 0.1, + decimals: 2, + propertyID: "keyLight.intensity", + showPropertyRule: { "keyLightMode": "enabled" }, + }, + { + label: "Light Horizontal Angle", + type: "number", + multiplier: DEGREES_TO_RADIANS, + decimals: 2, + unit: "deg", + propertyID: "keyLight.direction.x", + showPropertyRule: { "keyLightMode": "enabled" }, + }, + { + label: "Light Vertical Angle", + type: "number", + multiplier: DEGREES_TO_RADIANS, + decimals: 2, + unit: "deg", + propertyID: "keyLight.direction.y", + showPropertyRule: { "keyLightMode": "enabled" }, + }, + { + label: "Cast Shadows", + type: "bool", + propertyID: "keyLight.castShadows", + showPropertyRule: { "keyLightMode": "enabled" }, + }, + { + label: "Skybox", + type: "dropdown", + options: { inherit: "Inherit", disabled: "Off", enabled: "On" }, + propertyID: "skyboxMode", + }, + { + label: "Skybox Color", + type: "color", + propertyID: "skybox.color", + showPropertyRule: { "skyboxMode": "enabled" }, + }, + { + label: "Skybox Source", + type: "string", + propertyID: "skybox.url", + showPropertyRule: { "skyboxMode": "enabled" }, + }, + { + label: "Ambient Light", + type: "dropdown", + options: { inherit: "Inherit", disabled: "Off", enabled: "On" }, + propertyID: "ambientLightMode", + }, + { + label: "Ambient Intensity", + type: "number", + min: 0, + max: 10, + step: 0.1, + decimals: 2, + propertyID: "ambientLight.ambientIntensity", + showPropertyRule: { "ambientLightMode": "enabled" }, + }, + { + label: "Ambient Source", + type: "string", + propertyID: "ambientLight.ambientURL", + showPropertyRule: { "ambientLightMode": "enabled" }, + }, + { + type: "buttons", + buttons: [ { id: "copy", label: "Copy from Skybox", + className: "black", onClick: copySkyboxURLToAmbientURL } ], + propertyID: "copyURLToAmbient", + showPropertyRule: { "ambientLightMode": "enabled" }, + }, + { + label: "Haze", + type: "dropdown", + options: { inherit: "Inherit", disabled: "Off", enabled: "On" }, + propertyID: "hazeMode", + }, + { + label: "Range", + type: "number", + min: 5, + max: 10000, + step: 5, + decimals: 0, + unit: "m", + propertyID: "haze.hazeRange", + showPropertyRule: { "hazeMode": "enabled" }, + }, + { + label: "Use Altitude", + type: "bool", + propertyID: "haze.hazeAltitudeEffect", + showPropertyRule: { "hazeMode": "enabled" }, + }, + { + label: "Base", + type: "number", + min: -1000, + max: 1000, + step: 10, + decimals: 0, + unit: "m", + propertyID: "haze.hazeBaseRef", + showPropertyRule: { "hazeMode": "enabled" }, + }, + { + label: "Ceiling", + type: "number", + min: -1000, + max: 5000, + step: 10, + decimals: 0, + unit: "m", + propertyID: "haze.hazeCeiling", + showPropertyRule: { "hazeMode": "enabled" }, + }, + { + label: "Haze Color", + type: "color", + propertyID: "haze.hazeColor", + showPropertyRule: { "hazeMode": "enabled" }, + }, + { + label: "Background Blend", + type: "number", + min: 0, + max: 1, + step: 0.01, + decimals: 2, + propertyID: "haze.hazeBackgroundBlend", + showPropertyRule: { "hazeMode": "enabled" }, + }, + { + label: "Enable Glare", + type: "bool", + propertyID: "haze.hazeEnableGlare", + showPropertyRule: { "hazeMode": "enabled" }, + }, + { + label: "Glare Color", + type: "color", + propertyID: "haze.hazeGlareColor", + showPropertyRule: { "hazeMode": "enabled" }, + }, + { + label: "Glare Angle", + type: "number", + min: 0, + max: 180, + step: 1, + decimals: 0, + propertyID: "haze.hazeGlareAngle", + showPropertyRule: { "hazeMode": "enabled" }, + }, + { + label: "Bloom", + type: "dropdown", + options: { inherit: "Inherit", disabled: "Off", enabled: "On" }, + propertyID: "bloomMode", + }, + { + label: "Bloom Intensity", + type: "number", + min: 0, + max: 1, + step: 0.01, + decimals: 2, + propertyID: "bloom.bloomIntensity", + showPropertyRule: { "bloomMode": "enabled" }, + }, + { + label: "Bloom Threshold", + type: "number", + min: 0, + max: 1, + step: 0.01, + decimals: 2, + propertyID: "bloom.bloomThreshold", + showPropertyRule: { "bloomMode": "enabled" }, + }, + { + label: "Bloom Size", + type: "number", + min: 0, + max: 2, + step: 0.01, + decimals: 2, + propertyID: "bloom.bloomSize", + showPropertyRule: { "bloomMode": "enabled" }, + }, + ] + }, + { + id: "model", + addToGroup: "base", + properties: [ + { + label: "Model", + type: "string", + propertyID: "modelURL", + }, + { + label: "Collision Shape", + type: "dropdown", + options: { "none": "No Collision", "box": "Box", "sphere": "Sphere", "compound": "Compound" , + "simple-hull": "Basic - Whole model", "simple-compound": "Good - Sub-meshes" , + "static-mesh": "Exact - All polygons (non-dynamic only)" }, + propertyID: "shapeType", + }, + { + label: "Compound Shape", + type: "string", + propertyID: "compoundShapeURL", + }, + { + label: "Animation", + type: "string", + propertyID: "animation.url", + }, + { + label: "Play Automatically", + type: "bool", + propertyID: "animation.running", + }, + { + label: "Loop", + type: "bool", + propertyID: "animation.loop", + }, + { + label: "Allow Transition", + type: "bool", + propertyID: "animation.allowTranslation", + }, + { + label: "Hold", + type: "bool", + propertyID: "animation.hold", + }, + { + label: "Animation Frame", + type: "number", + propertyID: "animation.currentFrame", + }, + { + label: "First Frame", + type: "number", + propertyID: "animation.firstFrame", + }, + { + label: "Last Frame", + type: "number", + propertyID: "animation.lastFrame", + }, + { + label: "Animation FPS", + type: "number", + propertyID: "animation.fps", + }, + { + label: "Texture", + type: "textarea", + propertyID: "textures", + }, + { + label: "Original Texture", + type: "textarea", + propertyID: "originalTextures", + readOnly: true, + }, + ] + }, + { + id: "image", + addToGroup: "base", + properties: [ + { + label: "Image", + type: "string", + placeholder: "URL", + propertyID: "image", + }, + ] + }, + { + id: "web", + addToGroup: "base", + properties: [ + { + label: "Source", + type: "string", + propertyID: "sourceUrl", + }, + { + label: "Source Resolution", + type: "number", + propertyID: "dpi", + }, + ] + }, + { + id: "light", + addToGroup: "base", + properties: [ + { + label: "Light Color", + type: "color", + propertyID: "lightColor", + propertyName: "color", // actual entity property name + }, + { + label: "Intensity", + type: "number", + min: 0, + step: 0.1, + decimals: 1, + propertyID: "intensity", + }, + { + label: "Fall-Off Radius", + type: "number", + min: 0, + step: 0.1, + decimals: 1, + unit: "m", + propertyID: "falloffRadius", + }, + { + label: "Spotlight", + type: "bool", + propertyID: "isSpotlight", + }, + { + label: "Spotlight Exponent", + type: "number", + step: 0.01, + decimals: 2, + propertyID: "exponent", + }, + { + label: "Spotlight Cut-Off", + type: "number", + step: 0.01, + decimals: 2, + propertyID: "cutoff", + }, + ] + }, + { + id: "material", + addToGroup: "base", + properties: [ + { + label: "Material URL", + type: "string", + propertyID: "materialURL", + }, + { + label: "Material Data", + type: "textarea", + buttons: [ { id: "clear", label: "Clear Material Data", className: "red", onClick: clearMaterialData }, + { id: "edit", label: "Edit as JSON", className: "blue", onClick: newJSONMaterialEditor }, + { id: "save", label: "Save Material Data", className: "black", onClick: saveMaterialData } ], + propertyID: "materialData", + }, + { + label: "Submesh to Replace", + type: "number", + min: 0, + step: 1, + propertyID: "submeshToReplace", + }, + { + label: "Material Name to Replace", + type: "string", + propertyID: "materialNameToReplace", + }, + { + label: "Select Submesh", + type: "bool", + propertyID: "selectSubmesh", + }, + { + label: "Priority", + type: "number", + min: 0, + propertyID: "priority", + }, + { + label: "Material Position", + type: "vec2", + vec2Type: "xy", + min: 0, + max: 1, + step: 0.1, + decimals: 4, + subLabels: [ "x", "y" ], + propertyID: "materialMappingPos", + }, + { + label: "Material Scale", + type: "vec2", + vec2Type: "wh", + min: 0, + step: 0.1, + decimals: 4, + subLabels: [ "width", "height" ], + propertyID: "materialMappingScale", + }, + { + label: "Material Rotation", + type: "number", + step: 0.1, + decimals: 2, + unit: "deg", + propertyID: "materialMappingRot", + }, + ] + }, + { + id: "particles", + addToGroup: "base", + properties: [ + { + label: "Emit", + type: "bool", + propertyID: "isEmitting", + }, + { + label: "Lifespan", + type: "number", + unit: "s", + min: 0.01, + max: 10, + step: 0.01, + decimals: 2, + propertyID: "lifespan", + }, + { + label: "Max Particles", + type: "number", + min: 1, + max: 10000, + step: 1, + propertyID: "maxParticles", + }, + { + label: "Texture", + type: "texture", + propertyID: "particleTextures", + propertyName: "textures", // actual entity property name + }, + ] + }, + { + id: "particles_emit", + label: "EMIT", + isMinor: true, + properties: [ + { + label: "Emit Rate", + type: "number", + min: 1, + max: 1000, + step: 1, + propertyID: "emitRate", + }, + { + label: "Emit Speed", + type: "number", + min: 0, + max: 5, + step: 0.01, + decimals: 2, + propertyID: "emitSpeed", + }, + { + label: "Speed Spread", + type: "number", + min: 0, + max: 5, + step: 0.01, + decimals: 2, + propertyID: "speedSpread", + }, + { + label: "Emit Dimensions", + type: "vec3", + vec3Type: "xyz", + min: 0, + step: 0.01, + round: 100, + subLabels: [ "x", "y", "z" ], + propertyID: "emitDimensions", + }, + { + label: "Emit Radius Start", + type: "number", + min: 0, + max: 1, + step: 0.01, + decimals: 2, + propertyID: "emitRadiusStart" + }, + { + label: "Emit Orientation", + type: "vec3", + vec3Type: "pyr", + step: 0.01, + round: 100, + subLabels: [ "x", "y", "z" ], + unit: "deg", + propertyID: "emitOrientation", + }, + { + label: "Trails", + type: "bool", + propertyID: "emitterShouldTrail", + }, + ] + }, + { + id: "particles_size", + label: "SIZE", + isMinor: true, + properties: [ + { + type: "triple", + label: "Size", + properties: [ + { + label: "Start", + type: "number", + min: 0, + max: 4, + step: 0.01, + decimals: 2, + propertyID: "radiusStart", + fallbackProperty: "particleRadius", + }, + { + label: "Middle", + type: "number", + min: 0, + max: 4, + step: 0.01, + decimals: 2, + propertyID: "particleRadius", + }, + { + label: "Finish", + type: "number", + min: 0, + max: 4, + step: 0.01, + decimals: 2, + propertyID: "radiusFinish", + fallbackProperty: "particleRadius", + }, + ] + }, + { + label: "Size Spread", + type: "number", + min: 0, + max: 4, + step: 0.01, + decimals: 2, + propertyID: "radiusSpread", + }, + ] + }, + { + id: "particles_color", + label: "COLOR", + isMinor: true, + properties: [ + { + type: "triple", + label: "Color", + properties: [ + { + label: "Start", + type: "color", + propertyID: "colorStart", + fallbackProperty: "color", + }, + { + label: "Middle", + type: "color", + propertyID: "particleColor", + propertyName: "color", // actual entity property name + }, + { + label: "Finish", + type: "color", + propertyID: "colorFinish", + fallbackProperty: "color", + }, + ] + }, + { + label: "Color Spread", + type: "color", + propertyID: "colorSpread", + }, + ] + }, + { + id: "particles_alpha", + label: "ALPHA", + isMinor: true, + properties: [ + { + type: "triple", + label: "Alpha", + properties: [ + { + label: "Start", + type: "number", + min: 0, + max: 1, + step: 0.01, + decimals: 2, + propertyID: "alphaStart", + fallbackProperty: "alpha", + }, + { + label: "Middle", + type: "number", + min: 0, + max: 1, + step: 0.01, + decimals: 2, + propertyID: "alpha", + }, + { + label: "Finish", + type: "number", + min: 0, + max: 1, + step: 0.01, + decimals: 2, + propertyID: "alphaFinish", + fallbackProperty: "alpha", + }, + ] + }, + { + label: "Alpha Spread", + type: "number", + min: 0, + max: 1, + step: 0.01, + decimals: 2, + propertyID: "alphaSpread", + }, + ] + }, + { + id: "particles_acceleration", + label: "ACCELERATION", + isMinor: true, + properties: [ + { + label: "Emit Acceleration", + type: "vec3", + vec3Type: "xyz", + step: 0.01, + round: 100, + subLabels: [ "x", "y", "z" ], + propertyID: "emitAcceleration", + }, + { + label: "Acceleration Spread", + type: "vec3", + vec3Type: "xyz", + step: 0.01, + round: 100, + subLabels: [ "x", "y", "z" ], + propertyID: "accelerationSpread", + }, + ] + }, + { + id: "particles_spin", + label: "SPIN", + isMinor: true, + properties: [ + { + type: "triple", + label: "Alpha", + properties: [ + { + label: "Start", + type: "number", + min: -360, + max: 360, + step: 1, + decimals: 0, + multiplier: DEGREES_TO_RADIANS, + unit: "deg", + propertyID: "spinStart", + fallbackProperty: "particleSpin", + }, + { + label: "Middle", + type: "number", + min: -360, + max: 360, + step: 1, + decimals: 0, + multiplier: DEGREES_TO_RADIANS, + unit: "deg", + propertyID: "particleSpin", + }, + { + label: "Finish", + type: "number", + min: -360, + max: 360, + step: 1, + decimals: 0, + multiplier: DEGREES_TO_RADIANS, + unit: "deg", + propertyID: "spinFinish", + fallbackProperty: "particleSpin", + }, + ] + }, + { + label: "Spin Spread", + type: "number", + min: 0, + max: 360, + step: 1, + decimals: 0, + multiplier: DEGREES_TO_RADIANS, + unit: "deg", + propertyID: "spinSpread", + }, + { + label: "Rotate with Entity", + type: "bool", + propertyID: "rotateWithEntity", + }, + ] + }, + { + id: "particles_constraints", + label: "CONSTRAINTS", + isMinor: true, + properties: [ + { + type: "triple", + label: "Horizontal Angle", + properties: [ + { + label: "Start", + type: "number", + min: -180, + max: 0, + step: 1, + decimals: 0, + multiplier: DEGREES_TO_RADIANS, + unit: "deg", + propertyID: "polarStart", + }, + { + label: "Finish", + type: "number", + min: 0, + max: 180, + step: 1, + decimals: 0, + multiplier: DEGREES_TO_RADIANS, + unit: "deg", + propertyID: "polarFinish", + }, + ], + }, + { + type: "triple", + label: "Vertical Angle", + properties: [ + { + label: "Start", + type: "number", + min: 0, + max: 180, + step: 1, + decimals: 0, + multiplier: DEGREES_TO_RADIANS, + unit: "deg", + propertyID: "azimuthStart", + }, + { + label: "Finish", + type: "number", + min: 0, + max: 180, + step: 1, + decimals: 0, + multiplier: DEGREES_TO_RADIANS, + unit: "deg", + propertyID: "azimuthFinish", + }, + ] + } + ] + }, + { + id: "spatial", + label: "SPATIAL", + properties: [ + { + label: "Position", + type: "vec3", + vec3Type: "xyz", + decimals: 4, + subLabels: [ "x", "y", "z" ], + unit: "m", + propertyID: "position", + spaceMode: PROPERTY_SPACE_MODE.WORLD, + }, + { + label: "Local Position", + type: "vec3", + vec3Type: "xyz", + decimals: 4, + subLabels: [ "x", "y", "z" ], + unit: "m", + propertyID: "localPosition", + spaceMode: PROPERTY_SPACE_MODE.LOCAL, + }, + { + label: "Rotation", + type: "vec3", + vec3Type: "pyr", + step: 0.1, + decimals: 4, + subLabels: [ "x", "y", "z" ], + unit: "deg", + propertyID: "rotation", + spaceMode: PROPERTY_SPACE_MODE.WORLD, + }, + { + label: "Local Rotation", + type: "vec3", + vec3Type: "pyr", + step: 0.1, + decimals: 4, + subLabels: [ "x", "y", "z" ], + unit: "deg", + propertyID: "localRotation", + spaceMode: PROPERTY_SPACE_MODE.LOCAL, + }, + { + label: "Dimensions", + type: "vec3", + vec3Type: "xyz", + min: 0, + step: 0.1, + decimals: 4, + subLabels: [ "x", "y", "z" ], + unit: "m", + propertyID: "dimensions", + spaceMode: PROPERTY_SPACE_MODE.WORLD, + }, + { + label: "Local Dimensions", + type: "vec3", + vec3Type: "xyz", + min: 0, + step: 0.1, + decimals: 4, + subLabels: [ "x", "y", "z" ], + unit: "m", + propertyID: "localDimensions", + spaceMode: PROPERTY_SPACE_MODE.LOCAL, + }, + { + label: "Scale", + type: "number", + defaultValue: 100, + unit: "%", + buttons: [ { id: "rescale", label: "Rescale", className: "blue", onClick: rescaleDimensions }, + { id: "reset", label: "Reset Dimensions", className: "red", onClick: resetToNaturalDimensions } ], + propertyID: "scale", + }, + { + label: "Pivot", + type: "vec3", + vec3Type: "xyz", + step: 0.1, + decimals: 4, + subLabels: [ "x", "y", "z" ], + unit: "(ratio of dimension)", + propertyID: "registrationPoint", + }, + { + label: "Align", + type: "buttons", + buttons: [ { id: "selection", label: "Selection to Grid", className: "black", onClick: moveSelectionToGrid }, + { id: "all", label: "All to Grid", className: "black", onClick: moveAllToGrid } ], + propertyID: "alignToGrid", + }, + ] + }, + { + id: "behavior", + label: "BEHAVIOR", + properties: [ + { + label: "Grabbable", + type: "bool", + propertyID: "grab.grabbable", + }, + { + label: "Cloneable", + type: "bool", + propertyID: "cloneable", + }, + { + label: "Clone Lifetime", + type: "number", + unit: "s", + propertyID: "cloneLifetime", + showPropertyRule: { "cloneable": "true" }, + }, + { + label: "Clone Limit", + type: "number", + propertyID: "cloneLimit", + showPropertyRule: { "cloneable": "true" }, + }, + { + label: "Clone Dynamic", + type: "bool", + propertyID: "cloneDynamic", + showPropertyRule: { "cloneable": "true" }, + }, + { + label: "Clone Avatar Entity", + type: "bool", + propertyID: "cloneAvatarEntity", + showPropertyRule: { "cloneable": "true" }, + }, + { + label: "Triggerable", + type: "bool", + propertyID: "grab.triggerable", + }, + { + label: "Follow Controller", + type: "bool", + propertyID: "grab.grabFollowsController", + }, + { + label: "Cast shadows", + type: "bool", + propertyID: "canCastShadow", + }, + { + label: "Link", + type: "string", + propertyID: "href", + }, + { + label: "Script", + type: "string", + buttons: [ { id: "reload", label: "F", className: "glyph", onClick: reloadScripts } ], + propertyID: "script", + }, + { + label: "Server Script", + type: "string", + buttons: [ { id: "reload", label: "F", className: "glyph", onClick: reloadServerScripts } ], + propertyID: "serverScripts", + }, + { + label: "Server Script Status", + type: "placeholder", + indentedLabel: true, + propertyID: "serverScriptStatus", + }, + { + label: "Lifetime", + type: "number", + unit: "s", + propertyID: "lifetime", + }, + { + label: "User Data", + type: "textarea", + buttons: [ { id: "clear", label: "Clear User Data", className: "red", onClick: clearUserData }, + { id: "edit", label: "Edit as JSON", className: "blue", onClick: newJSONEditor }, + { id: "save", label: "Save User Data", className: "black", onClick: saveUserData } ], + propertyID: "userData", + }, + ] + }, + { + id: "collision", + label: "COLLISION", + properties: [ + { + label: "Collides", + type: "bool", + inverse: true, + propertyID: "collisionless", + }, + { + label: "Static Entities", + type: "bool", + propertyID: "collidesWithStatic", + propertyName: "static", // actual subProperty name + subPropertyOf: "collidesWith", + showPropertyRule: { "collisionless": "false" }, + }, + { + label: "Kinematic Entities", + type: "bool", + propertyID: "collidesWithKinematic", + propertyName: "kinematic", // actual subProperty name + subPropertyOf: "collidesWith", + showPropertyRule: { "collisionless": "false" }, + }, + { + label: "Dynamic Entities", + type: "bool", + propertyID: "collidesWithDynamic", + propertyName: "dynamic", // actual subProperty name + subPropertyOf: "collidesWith", + showPropertyRule: { "collisionless": "false" }, + }, + { + label: "My Avatar", + type: "bool", + propertyID: "collidesWithMyAvatar", + propertyName: "myAvatar", // actual subProperty name + subPropertyOf: "collidesWith", + showPropertyRule: { "collisionless": "false" }, + }, + { + label: "Other Avatars", + type: "bool", + propertyID: "collidesWithOtherAvatar", + propertyName: "otherAvatar", // actual subProperty name + subPropertyOf: "collidesWith", + showPropertyRule: { "collisionless": "false" }, + }, + { + label: "Collision sound URL", + type: "string", + propertyID: "collisionSoundURL", + showPropertyRule: { "collisionless": "false" }, + }, + { + label: "Dynamic", + type: "bool", + propertyID: "dynamic", + }, + ] + }, + { + id: "physics", + label: "PHYSICS", + properties: [ + { + label: "Linear Velocity", + type: "vec3", + vec3Type: "xyz", + step: 0.1, + decimals: 4, + subLabels: [ "x", "y", "z" ], + unit: "m/s", + propertyID: "localVelocity", + }, + { + label: "Linear Damping", + type: "number", + min: 0, + max: 1, + step: 0.01, + decimals: 2, + propertyID: "damping", + }, + { + label: "Angular Velocity", + type: "vec3", + vec3Type: "pyr", + multiplier: DEGREES_TO_RADIANS, + decimals: 4, + subLabels: [ "x", "y", "z" ], + unit: "deg/s", + propertyID: "localAngularVelocity", + }, + { + label: "Angular Damping", + type: "number", + min: 0, + max: 1, + step: 0.01, + decimals: 4, + propertyID: "angularDamping", + }, + { + label: "Bounciness", + type: "number", + min: 0, + max: 1, + step: 0.01, + decimals: 4, + propertyID: "restitution", + }, + { + label: "Friction", + type: "number", + min: 0, + max: 10, + step: 0.1, + decimals: 4, + propertyID: "friction", + }, + { + label: "Density", + type: "number", + min: 100, + max: 10000, + step: 1, + decimals: 4, + propertyID: "density", + }, + { + label: "Gravity", + type: "vec3", + vec3Type: "xyz", + subLabels: [ "x", "y", "z" ], + step: 0.1, + decimals: 4, + unit: "m/s2", + propertyID: "gravity", + }, + { + label: "Acceleration", + type: "vec3", + vec3Type: "xyz", + subLabels: [ "x", "y", "z" ], + decimals: 4, + unit: "m/s2", + propertyID: "acceleration", + }, + ] + }, +]; + +const GROUPS_PER_TYPE = { + None: [ 'base', 'spatial', 'behavior', 'collision', 'physics' ], + Shape: [ 'base', 'shape', 'spatial', 'behavior', 'collision', 'physics' ], + Text: [ 'base', 'text', 'spatial', 'behavior', 'collision', 'physics' ], + Zone: [ 'base', 'zone', 'spatial', 'behavior', 'collision', 'physics' ], + Model: [ 'base', 'model', 'spatial', 'behavior', 'collision', 'physics' ], + Image: [ 'base', 'image', 'spatial', 'behavior', 'collision', 'physics' ], + Web: [ 'base', 'web', 'spatial', 'behavior', 'collision', 'physics' ], + Light: [ 'base', 'light', 'spatial', 'behavior', 'collision', 'physics' ], + Material: [ 'base', 'material', 'spatial', 'behavior' ], + ParticleEffect: [ 'base', 'particles', 'particles_emit', 'particles_size', 'particles_color', 'particles_alpha', + 'particles_acceleration', 'particles_spin', 'particles_constraints', 'spatial', 'behavior', 'physics' ], + PolyLine: [ 'base', 'spatial', 'behavior', 'collision', 'physics' ], + PolyVox: [ 'base', 'spatial', 'behavior', 'collision', 'physics' ], + Multiple: [ 'base', 'spatial', 'behavior', 'collision', 'physics' ], +}; + +const EDITOR_TIMEOUT_DURATION = 1500; +const DEBOUNCE_TIMEOUT = 125; + +const COLOR_MIN = 0; +const COLOR_MAX = 255; +const COLOR_STEP = 1; + +const MATERIAL_PREFIX_STRING = "mat::"; + +const PENDING_SCRIPT_STATUS = "[ Fetching status ]"; +const NOT_RUNNING_SCRIPT_STATUS = "Not running"; +const ENTITY_SCRIPT_STATUS = { + pending: "Pending", + loading: "Loading", + error_loading_script: "Error loading script", // eslint-disable-line camelcase + error_running_script: "Error running script", // eslint-disable-line camelcase + running: "Running", + unloaded: "Unloaded" +}; + +const PROPERTY_NAME_DIVISION = { + GROUP: 0, + PROPERTY: 1, + SUBPROPERTY: 2, +}; + +const VECTOR_ELEMENTS = { + X_NUMBER: 0, + Y_NUMBER: 1, + Z_NUMBER: 2, +}; + +const COLOR_ELEMENTS = { + COLOR_PICKER: 0, + RED_NUMBER: 1, + GREEN_NUMBER: 2, + BLUE_NUMBER: 3, +}; + +const TEXTURE_ELEMENTS = { + IMAGE: 0, + TEXT_INPUT: 1, +}; + +const JSON_EDITOR_ROW_DIV_INDEX = 2; + +var elGroups = {}; +var properties = {}; var colorPickers = {}; +var particlePropertyUpdates = {}; +var selectedEntityProperties; var lastEntityID = null; +var createAppTooltip = new CreateAppTooltip(); +let currentSpaceMode = PROPERTY_SPACE_MODE.LOCAL; -var MATERIAL_PREFIX_STRING = "mat::"; +function createElementFromHTML(htmlString) { + let elTemplate = document.createElement('template'); + elTemplate.innerHTML = htmlString.trim(); + return elTemplate.content.firstChild; +} -var PENDING_SCRIPT_STATUS = "[ Fetching status ]"; +/** + * GENERAL PROPERTY/GROUP FUNCTIONS + */ -function debugPrint(message) { - EventBridge.emitWebEvent( - JSON.stringify({ - type: "print", - message: message - }) - ); +function getPropertyInputElement(propertyID) { + let property = properties[propertyID]; + switch (property.data.type) { + case 'string': + case 'bool': + case 'dropdown': + case 'textarea': + case 'texture': + return property.elInput; + case 'number': + return property.elNumber.elInput; + case 'vec3': + case 'vec2': + return { x: property.elNumberX.elInput, y: property.elNumberY.elInput, z: property.elNumberZ.elInput }; + case 'color': + return { red: property.elNumberR.elInput, green: property.elNumberG.elInput, blue: property.elNumberB.elInput }; + case 'icon': + return property.elLabel; + default: + return undefined; + } } function enableChildren(el, selector) { - var elSelectors = el.querySelectorAll(selector); - for (var selectorIndex = 0; selectorIndex < elSelectors.length; ++selectorIndex) { + let elSelectors = el.querySelectorAll(selector); + for (let selectorIndex = 0; selectorIndex < elSelectors.length; ++selectorIndex) { elSelectors[selectorIndex].removeAttribute('disabled'); } } function disableChildren(el, selector) { - var elSelectors = el.querySelectorAll(selector); - for (var selectorIndex = 0; selectorIndex < elSelectors.length; ++selectorIndex) { + let elSelectors = el.querySelectorAll(selector); + for (let selectorIndex = 0; selectorIndex < elSelectors.length; ++selectorIndex) { elSelectors[selectorIndex].setAttribute('disabled', 'disabled'); } } function enableProperties() { - enableChildren(document.getElementById("properties-list"), "input, textarea, checkbox, .dropdown dl, .color-picker"); + enableChildren(document.getElementById("properties-list"), + "input, textarea, checkbox, .dropdown dl, .color-picker , .draggable-number.text"); enableChildren(document, ".colpick"); - var elLocked = document.getElementById("property-locked"); - + + let elLocked = getPropertyInputElement("locked"); if (elLocked.checked === false) { removeStaticUserData(); removeStaticMaterialData(); } } - function disableProperties() { - disableChildren(document.getElementById("properties-list"), "input, textarea, checkbox, .dropdown dl, .color-picker"); + disableChildren(document.getElementById("properties-list"), + "input, textarea, checkbox, .dropdown dl, .color-picker, .draggable-number.text"); disableChildren(document, ".colpick"); - for (var pickKey in colorPickers) { + for (let pickKey in colorPickers) { colorPickers[pickKey].colpickHide(); } - var elLocked = document.getElementById("property-locked"); - + + let elLocked = getPropertyInputElement("locked"); if (elLocked.checked === true) { - if ($('#userdata-editor').css('display') === "block") { + if ($('#property-userData-editor').css('display') === "block") { showStaticUserData(); } - if ($('#materialdata-editor').css('display') === "block") { + if ($('#property-materialData-editor').css('display') === "block") { showStaticMaterialData(); } } } -function showElements(els, show) { - for (var i = 0; i < els.length; i++) { - els[i].style.display = (show) ? 'table' : 'none'; +function showPropertyElement(propertyID, show) { + let elProperty = properties[propertyID].elContainer; + elProperty.style.display = show ? "flex" : "none"; +} + +function resetProperties() { + for (let propertyID in properties) { + let property = properties[propertyID]; + let propertyData = property.data; + + switch (propertyData.type) { + case 'string': { + property.elInput.value = ""; + break; + } + case 'bool': { + property.elInput.checked = false; + break; + } + case 'number': { + if (propertyData.defaultValue !== undefined) { + property.elNumber.setValue(propertyData.defaultValue); + } else { + property.elNumber.setValue(""); + } + break; + } + case 'vec3': + case 'vec2': { + property.elNumberX.setValue(""); + property.elNumberY.setValue(""); + if (property.elNumberZ !== undefined) { + property.elNumberZ.setValue(""); + } + break; + } + case 'color': { + property.elColorPicker.style.backgroundColor = "rgb(" + 0 + "," + 0 + "," + 0 + ")"; + property.elNumberR.setValue(""); + property.elNumberG.setValue(""); + property.elNumberB.setValue(""); + break; + } + case 'dropdown': { + property.elInput.value = ""; + setDropdownText(property.elInput); + break; + } + case 'textarea': { + property.elInput.value = ""; + setTextareaScrolling(property.elInput); + break; + } + case 'icon': { + property.elSpan.style.display = "none"; + break; + } + case 'texture': { + property.elInput.value = ""; + property.elInput.imageLoad(property.elInput.value); + break; + } + } + + let showPropertyRules = properties[propertyID].showPropertyRules; + if (showPropertyRules !== undefined) { + for (let propertyToHide in showPropertyRules) { + showPropertyElement(propertyToHide, false); + } + } + } + + let elServerScriptError = document.getElementById("property-serverScripts-error"); + let elServerScriptStatus = document.getElementById("property-serverScripts-status"); + elServerScriptError.parentElement.style.display = "none"; + elServerScriptStatus.innerText = NOT_RUNNING_SCRIPT_STATUS; +} + +function showGroupsForType(type) { + if (type === "Box" || type === "Sphere") { + type = "Shape"; + } + + let typeGroups = GROUPS_PER_TYPE[type]; + + for (let groupKey in elGroups) { + let elGroup = elGroups[groupKey]; + if (typeGroups && typeGroups.indexOf(groupKey) > -1) { + elGroup.style.display = "block"; + } else { + elGroup.style.display = "none"; + } } } -function updateProperty(propertyName, propertyValue) { - var properties = {}; - properties[propertyName] = propertyValue; - updateProperties(properties); +function getPropertyValue(originalPropertyName) { + // if this is a compound property name (i.e. animation.running) + // then split it by . up to 3 times to find property value + let propertyValue; + let splitPropertyName = originalPropertyName.split('.'); + if (splitPropertyName.length > 1) { + let propertyGroupName = splitPropertyName[PROPERTY_NAME_DIVISION.GROUP]; + let propertyName = splitPropertyName[PROPERTY_NAME_DIVISION.PROPERTY]; + let groupProperties = selectedEntityProperties[propertyGroupName]; + if (groupProperties === undefined || groupProperties[propertyName] === undefined) { + return undefined; + } + if (splitPropertyName.length === PROPERTY_NAME_DIVISION.SUBPROPERTY + 1) { + let subPropertyName = splitPropertyName[PROPERTY_NAME_DIVISION.SUBPROPERTY]; + propertyValue = groupProperties[propertyName][subPropertyName]; + } else { + propertyValue = groupProperties[propertyName]; + } + } else { + propertyValue = selectedEntityProperties[originalPropertyName]; + } + return propertyValue; } -function updateProperties(properties) { +function updateVisibleSpaceModeProperties() { + for (let propertyID in properties) { + if (properties.hasOwnProperty(propertyID)) { + let property = properties[propertyID]; + let propertySpaceMode = property.spaceMode; + if (propertySpaceMode !== PROPERTY_SPACE_MODE.ALL) { + showPropertyElement(propertyID, propertySpaceMode === currentSpaceMode); + } else { + showPropertyElement(propertyID, true); + } + } + } +} + + +/** + * PROPERTY UPDATE FUNCTIONS + */ + +function updateProperty(originalPropertyName, propertyValue, isParticleProperty) { + let propertyUpdate = {}; + // if this is a compound property name (i.e. animation.running) then split it by . up to 3 times + let splitPropertyName = originalPropertyName.split('.'); + if (splitPropertyName.length > 1) { + let propertyGroupName = splitPropertyName[PROPERTY_NAME_DIVISION.GROUP]; + let propertyName = splitPropertyName[PROPERTY_NAME_DIVISION.PROPERTY]; + propertyUpdate[propertyGroupName] = {}; + if (splitPropertyName.length === PROPERTY_NAME_DIVISION.SUBPROPERTY + 1) { + let subPropertyName = splitPropertyName[PROPERTY_NAME_DIVISION.SUBPROPERTY]; + propertyUpdate[propertyGroupName][propertyName] = {}; + propertyUpdate[propertyGroupName][propertyName][subPropertyName] = propertyValue; + } else { + propertyUpdate[propertyGroupName][propertyName] = propertyValue; + } + } else { + propertyUpdate[originalPropertyName] = propertyValue; + } + // queue up particle property changes with the debounced sync to avoid + // causing particle emitting to reset excessively with each value change + if (isParticleProperty) { + Object.keys(propertyUpdate).forEach(function (propertyUpdateKey) { + particlePropertyUpdates[propertyUpdateKey] = propertyUpdate[propertyUpdateKey]; + }); + particleSyncDebounce(); + } else { + updateProperties(propertyUpdate); + } +} + +var particleSyncDebounce = _.debounce(function () { + updateProperties(particlePropertyUpdates); + particlePropertyUpdates = {}; +}, DEBOUNCE_TIMEOUT); + +function updateProperties(propertiesToUpdate) { EventBridge.emitWebEvent(JSON.stringify({ id: lastEntityID, type: "update", - properties: properties + properties: propertiesToUpdate })); } -function createEmitCheckedPropertyUpdateFunction(propertyName) { +function createEmitTextPropertyUpdateFunction(propertyName, isParticleProperty) { return function() { - updateProperty(propertyName, this.checked); + updateProperty(propertyName, this.value, isParticleProperty); }; } -function createEmitGroupCheckedPropertyUpdateFunction(group, propertyName) { +function createEmitCheckedPropertyUpdateFunction(propertyName, inverse, isParticleProperty) { return function() { - var properties = {}; - properties[group] = {}; - properties[group][propertyName] = this.checked; - updateProperties(properties); + updateProperty(propertyName, inverse ? !this.checked : this.checked, isParticleProperty); }; } -function createEmitNumberPropertyUpdateFunction(propertyName, decimals) { - decimals = ((decimals === undefined) ? 4 : decimals); +function createEmitNumberPropertyUpdateFunction(propertyName, multiplier, isParticleProperty) { return function() { - var value = parseFloat(this.value).toFixed(decimals); - updateProperty(propertyName, value); - }; -} - -function createEmitGroupNumberPropertyUpdateFunction(group, propertyName) { - return function() { - var properties = {}; - properties[group] = {}; - properties[group][propertyName] = this.value; - updateProperties(properties); - }; -} - -function createImageURLUpdateFunction(propertyName) { - return function () { - var newTextures = JSON.stringify({ "tex.picture": this.value }); - updateProperty(propertyName, newTextures); - }; -} - -function createEmitTextPropertyUpdateFunction(propertyName) { - return function() { - updateProperty(propertyName, this.value); - }; -} - -function createZoneComponentModeChangedFunction(zoneComponent, zoneComponentModeInherit, - zoneComponentModeDisabled, zoneComponentModeEnabled) { - - return function() { - var zoneComponentMode; - - if (zoneComponentModeInherit.checked) { - zoneComponentMode = 'inherit'; - } else if (zoneComponentModeDisabled.checked) { - zoneComponentMode = 'disabled'; - } else if (zoneComponentModeEnabled.checked) { - zoneComponentMode = 'enabled'; + if (multiplier === undefined) { + multiplier = 1; } - - updateProperty(zoneComponent, zoneComponentMode); + let value = parseFloat(this.value) * multiplier; + updateProperty(propertyName, value, isParticleProperty); }; } -function createEmitGroupTextPropertyUpdateFunction(group, propertyName) { - return function() { - var properties = {}; - properties[group] = {}; - properties[group][propertyName] = this.value; - updateProperties(properties); - }; -} - -function createEmitVec2PropertyUpdateFunction(property, elX, elY) { +function createEmitVec2PropertyUpdateFunction(propertyName, elX, elY, multiplier, isParticleProperty) { return function () { - var properties = {}; - properties[property] = { - x: elX.value, - y: elY.value + if (multiplier === undefined) { + multiplier = 1; + } + let newValue = { + x: elX.value * multiplier, + y: elY.value * multiplier }; - updateProperties(properties); + updateProperty(propertyName, newValue, isParticleProperty); }; } -function createEmitVec3PropertyUpdateFunction(property, elX, elY, elZ) { +function createEmitVec3PropertyUpdateFunction(propertyName, elX, elY, elZ, multiplier, isParticleProperty) { return function() { - var properties = {}; - properties[property] = { - x: elX.value, - y: elY.value, - z: elZ.value - }; - updateProperties(properties); - }; -} - -function createEmitGroupVec3PropertyUpdateFunction(group, property, elX, elY, elZ) { - return function() { - var properties = {}; - properties[group] = {}; - properties[group][property] = { - x: elX.value, - y: elY.value, - z: elZ ? elZ.value : 0 - }; - updateProperties(properties); - }; -} - -function createEmitVec3PropertyUpdateFunctionWithMultiplier(property, elX, elY, elZ, multiplier) { - return function() { - var properties = {}; - properties[property] = { + if (multiplier === undefined) { + multiplier = 1; + } + let newValue = { x: elX.value * multiplier, y: elY.value * multiplier, z: elZ.value * multiplier }; - updateProperties(properties); + updateProperty(propertyName, newValue, isParticleProperty); }; } -function createEmitColorPropertyUpdateFunction(property, elRed, elGreen, elBlue) { +function createEmitColorPropertyUpdateFunction(propertyName, elRed, elGreen, elBlue, isParticleProperty) { return function() { - emitColorPropertyUpdate(property, elRed.value, elGreen.value, elBlue.value); + emitColorPropertyUpdate(propertyName, elRed.value, elGreen.value, elBlue.value, isParticleProperty); }; } -function emitColorPropertyUpdate(property, red, green, blue, group) { - var properties = {}; - if (group) { - properties[group] = {}; - properties[group][property] = { - red: red, - green: green, - blue: blue - }; - } else { - properties[property] = { - red: red, - green: green, - blue: blue - }; - } - updateProperties(properties); -} - - -function createEmitGroupColorPropertyUpdateFunction(group, property, elRed, elGreen, elBlue) { - return function() { - var properties = {}; - properties[group] = {}; - properties[group][property] = { - red: elRed.value, - green: elGreen.value, - blue: elBlue.value - }; - updateProperties(properties); +function emitColorPropertyUpdate(propertyName, red, green, blue, isParticleProperty) { + let newValue = { + red: red, + green: green, + blue: blue }; + updateProperty(propertyName, newValue, isParticleProperty); } -function updateCheckedSubProperty(propertyName, propertyValue, subPropertyElement, subPropertyString) { +function updateCheckedSubProperty(propertyName, propertyValue, subPropertyElement, subPropertyString, isParticleProperty) { if (subPropertyElement.checked) { if (propertyValue.indexOf(subPropertyString)) { propertyValue += subPropertyString + ','; @@ -279,11 +1752,564 @@ function updateCheckedSubProperty(propertyName, propertyValue, subPropertyElemen // We've unchecked, so remove propertyValue = propertyValue.replace(subPropertyString + ",", ""); } - updateProperty(propertyName, propertyValue); + updateProperty(propertyName, propertyValue, isParticleProperty); +} + +function createImageURLUpdateFunction(propertyName, isParticleProperty) { + return function () { + let newTextures = JSON.stringify({ "tex.picture": this.value }); + updateProperty(propertyName, newTextures, isParticleProperty); + }; +} + + +/** + * PROPERTY ELEMENT CREATION FUNCTIONS + */ + +function createStringProperty(property, elProperty) { + let propertyName = property.name; + let elementID = property.elementID; + let propertyData = property.data; + + elProperty.className = "text"; + + let elInput = createElementFromHTML(` + + `) + + + elInput.addEventListener('change', createEmitTextPropertyUpdateFunction(propertyName, property.isParticleProperty)); + + elProperty.appendChild(elInput); + + if (propertyData.buttons !== undefined) { + addButtons(elProperty, elementID, propertyData.buttons, false); + } + + return elInput; +} + +function createBoolProperty(property, elProperty) { + let propertyName = property.name; + let elementID = property.elementID; + let propertyData = property.data; + + elProperty.className = "checkbox"; + + if (propertyData.glyph !== undefined) { + let elSpan = document.createElement('span'); + elSpan.innerHTML = propertyData.glyph; + elSpan.className = 'icon'; + elProperty.appendChild(elSpan); + } + + let elInput = document.createElement('input'); + elInput.setAttribute("id", elementID); + elInput.setAttribute("type", "checkbox"); + + elProperty.appendChild(elInput); + elProperty.appendChild(createElementFromHTML(``)); + + let subPropertyOf = propertyData.subPropertyOf; + if (subPropertyOf !== undefined) { + elInput.addEventListener('change', function() { + updateCheckedSubProperty(subPropertyOf, selectedEntityProperties[subPropertyOf], + elInput, propertyName, property.isParticleProperty); + }); + } else { + elInput.addEventListener('change', createEmitCheckedPropertyUpdateFunction(propertyName, propertyData.inverse, + property.isParticleProperty)); + } + + return elInput; +} + +function createNumberProperty(property, elProperty) { + let propertyName = property.name; + let elementID = property.elementID; + let propertyData = property.data; + + elProperty.className = "draggable-number"; + + let elDraggableNumber = new DraggableNumber(propertyData.min, propertyData.max, + propertyData.step, propertyData.decimals); + + let defaultValue = propertyData.defaultValue; + if (defaultValue !== undefined) { + elDraggableNumber.elInput.value = defaultValue; + } + + let valueChangeFunction = createEmitNumberPropertyUpdateFunction(propertyName, propertyData.multiplier, + property.isParticleProperty); + elDraggableNumber.setValueChangeFunction(valueChangeFunction); + + elDraggableNumber.elInput.setAttribute("id", elementID); + elProperty.appendChild(elDraggableNumber.elDiv); + + if (propertyData.buttons !== undefined) { + addButtons(elDraggableNumber.elText, elementID, propertyData.buttons, false); + } + + return elDraggableNumber; +} + +function createVec3Property(property, elProperty) { + let propertyName = property.name; + let elementID = property.elementID; + let propertyData = property.data; + + elProperty.className = propertyData.vec3Type + " fstuple"; + + let elNumberX = createTupleNumberInput(elProperty, elementID, propertyData.subLabels[VECTOR_ELEMENTS.X_NUMBER], + propertyData.min, propertyData.max, propertyData.step, propertyData.decimals); + let elNumberY = createTupleNumberInput(elProperty, elementID, propertyData.subLabels[VECTOR_ELEMENTS.Y_NUMBER], + propertyData.min, propertyData.max, propertyData.step, propertyData.decimals); + let elNumberZ = createTupleNumberInput(elProperty, elementID, propertyData.subLabels[VECTOR_ELEMENTS.Z_NUMBER], + propertyData.min, propertyData.max, propertyData.step, propertyData.decimals); + + let valueChangeFunction = createEmitVec3PropertyUpdateFunction(propertyName, elNumberX.elInput, elNumberY.elInput, + elNumberZ.elInput, propertyData.multiplier, + property.isParticleProperty); + elNumberX.setValueChangeFunction(valueChangeFunction); + elNumberY.setValueChangeFunction(valueChangeFunction); + elNumberZ.setValueChangeFunction(valueChangeFunction); + + let elResult = []; + elResult[VECTOR_ELEMENTS.X_NUMBER] = elNumberX; + elResult[VECTOR_ELEMENTS.Y_NUMBER] = elNumberY; + elResult[VECTOR_ELEMENTS.Z_NUMBER] = elNumberZ; + return elResult; +} + +function createVec2Property(property, elProperty) { + let propertyName = property.name; + let elementID = property.elementID; + let propertyData = property.data; + + elProperty.className = propertyData.vec2Type + " fstuple"; + + let elTuple = document.createElement('div'); + elTuple.className = "tuple"; + + elProperty.appendChild(elTuple); + + let elNumberX = createTupleNumberInput(elProperty, elementID, propertyData.subLabels[VECTOR_ELEMENTS.X_NUMBER], + propertyData.min, propertyData.max, propertyData.step, propertyData.decimals); + let elNumberY = createTupleNumberInput(elProperty, elementID, propertyData.subLabels[VECTOR_ELEMENTS.Y_NUMBER], + propertyData.min, propertyData.max, propertyData.step, propertyData.decimals); + + let valueChangeFunction = createEmitVec2PropertyUpdateFunction(propertyName, elNumberX.elInput, elNumberY.elInput, + propertyData.multiplier, property.isParticleProperty); + elNumberX.setValueChangeFunction(valueChangeFunction); + elNumberY.setValueChangeFunction(valueChangeFunction); + + let elResult = []; + elResult[VECTOR_ELEMENTS.X_NUMBER] = elNumberX; + elResult[VECTOR_ELEMENTS.Y_NUMBER] = elNumberY; + return elResult; +} + +function createColorProperty(property, elProperty) { + let propertyName = property.name; + let elementID = property.elementID; + + elProperty.className = "rgb fstuple"; + + let elColorPicker = document.createElement('div'); + elColorPicker.className = "color-picker"; + elColorPicker.setAttribute("id", elementID); + + let elTuple = document.createElement('div'); + elTuple.className = "tuple"; + + elProperty.appendChild(elColorPicker); + elProperty.appendChild(elTuple); + + let elNumberR = createTupleNumberInput(elTuple, elementID, "red", COLOR_MIN, COLOR_MAX, COLOR_STEP); + let elNumberG = createTupleNumberInput(elTuple, elementID, "green", COLOR_MIN, COLOR_MAX, COLOR_STEP); + let elNumberB = createTupleNumberInput(elTuple, elementID, "blue", COLOR_MIN, COLOR_MAX, COLOR_STEP); + + let valueChangeFunction = createEmitColorPropertyUpdateFunction(propertyName, elNumberR.elInput, elNumberG.elInput, + elNumberB.elInput, property.isParticleProperty); + elNumberR.setValueChangeFunction(valueChangeFunction); + elNumberG.setValueChangeFunction(valueChangeFunction); + elNumberB.setValueChangeFunction(valueChangeFunction); + + let colorPickerID = "#" + elementID; + colorPickers[colorPickerID] = $(colorPickerID).colpick({ + colorScheme: 'dark', + layout: 'rgbhex', + color: '000000', + submit: false, // We don't want to have a submission button + onShow: function(colpick) { + $(colorPickerID).attr('active', 'true'); + // The original color preview within the picker needs to be updated on show because + // prior to the picker being shown we don't have access to the selections' starting color. + colorPickers[colorPickerID].colpickSetColor({ + "r": elNumberR.elInput.value, + "g": elNumberG.elInput.value, + "b": elNumberB.elInput.value + }); + }, + onHide: function(colpick) { + $(colorPickerID).attr('active', 'false'); + }, + onChange: function(hsb, hex, rgb, el) { + $(el).css('background-color', '#' + hex); + emitColorPropertyUpdate(propertyName, rgb.r, rgb.g, rgb.b); + } + }); + + let elResult = []; + elResult[COLOR_ELEMENTS.COLOR_PICKER] = elColorPicker; + elResult[COLOR_ELEMENTS.RED_NUMBER] = elNumberR; + elResult[COLOR_ELEMENTS.GREEN_NUMBER] = elNumberG; + elResult[COLOR_ELEMENTS.BLUE_NUMBER] = elNumberB; + return elResult; +} + +function createDropdownProperty(property, propertyID, elProperty) { + let propertyName = property.name; + let elementID = property.elementID; + let propertyData = property.data; + + elProperty.className = "dropdown"; + + let elInput = document.createElement('select'); + elInput.setAttribute("id", elementID); + elInput.setAttribute("propertyID", propertyID); + + for (let optionKey in propertyData.options) { + let option = document.createElement('option'); + option.value = optionKey; + option.text = propertyData.options[optionKey]; + elInput.add(option); + } + + elInput.addEventListener('change', createEmitTextPropertyUpdateFunction(propertyName, property.isParticleProperty)); + + elProperty.appendChild(elInput); + + return elInput; +} + +function createTextareaProperty(property, elProperty) { + let propertyName = property.name; + let elementID = property.elementID; + let propertyData = property.data; + + elProperty.className = "textarea"; + + let elInput = document.createElement('textarea'); + elInput.setAttribute("id", elementID); + if (propertyData.readOnly) { + elInput.readOnly = true; + } + + elInput.addEventListener('change', createEmitTextPropertyUpdateFunction(propertyName, property.isParticleProperty)); + + elProperty.appendChild(elInput); + + if (propertyData.buttons !== undefined) { + addButtons(elProperty, elementID, propertyData.buttons, true); + } + + return elInput; +} + +function createIconProperty(property, elProperty) { + let elementID = property.elementID; + let propertyData = property.data; + + elProperty.className = "value"; + + let elSpan = document.createElement('span'); + elSpan.setAttribute("id", elementID + "-icon"); + elSpan.className = 'icon'; + + elProperty.appendChild(elSpan); + + return elSpan; +} + +function createTextureProperty(property, elProperty) { + let elementID = property.elementID; + + elProperty.className = "texture"; + + let elDiv = document.createElement("div"); + let elImage = document.createElement("img"); + elDiv.className = "texture-image no-texture"; + elDiv.appendChild(elImage); + + let elInput = document.createElement('input'); + elInput.setAttribute("id", elementID); + elInput.setAttribute("type", "text"); + + let imageLoad = _.debounce(function (url) { + if (url.slice(0, 5).toLowerCase() === "atp:/") { + elImage.src = ""; + elImage.style.display = "none"; + elDiv.classList.remove("with-texture"); + elDiv.classList.remove("no-texture"); + elDiv.classList.add("no-preview"); + } else if (url.length > 0) { + elDiv.classList.remove("no-texture"); + elDiv.classList.remove("no-preview"); + elDiv.classList.add("with-texture"); + elImage.src = url; + elImage.style.display = "block"; + } else { + elImage.src = ""; + elImage.style.display = "none"; + elDiv.classList.remove("with-texture"); + elDiv.classList.remove("no-preview"); + elDiv.classList.add("no-texture"); + } + }, DEBOUNCE_TIMEOUT * 2); + elInput.imageLoad = imageLoad; + elInput.oninput = function (event) { + // Add throttle + let url = event.target.value; + imageLoad(url); + updateProperty(property.name, url, property.isParticleProperty) + }; + elInput.onchange = elInput.oninput; + + elProperty.appendChild(elInput); + elProperty.appendChild(elDiv); + + let elResult = []; + elResult[TEXTURE_ELEMENTS.IMAGE] = elImage; + elResult[TEXTURE_ELEMENTS.TEXT_INPUT] = elInput; + return elResult; +} + +function createButtonsProperty(property, elProperty, elLabel) { + let elementID = property.elementID; + let propertyData = property.data; + + elProperty.className = "text"; + + if (propertyData.buttons !== undefined) { + addButtons(elProperty, elementID, propertyData.buttons, false); + } + + return elProperty; +} + +function createTupleNumberInput(elTuple, propertyElementID, subLabel, min, max, step, decimals) { + let elementID = propertyElementID + "-" + subLabel.toLowerCase(); + + let elLabel = document.createElement('label'); + elLabel.className = "sublabel " + subLabel; + elLabel.innerText = subLabel[0].toUpperCase() + subLabel.slice(1); + elLabel.setAttribute("for", elementID); + elLabel.style.visibility = "visible"; + + let elDraggableNumber = new DraggableNumber(min, max, step, decimals); + elDraggableNumber.elInput.setAttribute("id", elementID); + elDraggableNumber.elDiv.className += " fstuple"; + elDraggableNumber.elText.insertBefore(elLabel, elDraggableNumber.elLeftArrow); + elTuple.appendChild(elDraggableNumber.elDiv); + + return elDraggableNumber; +} + +function addButtons(elProperty, propertyID, buttons, newRow) { + let elDiv = document.createElement('div'); + elDiv.className = "row"; + + buttons.forEach(function(button) { + let elButton = document.createElement('input'); + elButton.className = button.className; + elButton.setAttribute("type", "button"); + elButton.setAttribute("id", propertyID + "-button-" + button.id); + elButton.setAttribute("value", button.label); + elButton.addEventListener("click", button.onClick); + if (newRow) { + elDiv.appendChild(elButton); + } else { + elProperty.appendChild(elButton); + } + }); + + if (newRow) { + elProperty.appendChild(document.createElement('br')); + elProperty.appendChild(elDiv); + } +} + +function createProperty(propertyData, propertyElementID, propertyName, propertyID, elProperty) { + let property = { + data: propertyData, + elementID: propertyElementID, + name: propertyName, + elProperty: elProperty, + }; + let propertyType = propertyData.type; + + switch (propertyType) { + case 'string': { + property.elInput = createStringProperty(property, elProperty); + break; + } + case 'bool': { + property.elInput = createBoolProperty(property, elProperty); + break; + } + case 'number': { + property.elNumber = createNumberProperty(property, elProperty); + break; + } + case 'vec3': { + let elVec3 = createVec3Property(property, elProperty); + property.elNumberX = elVec3[VECTOR_ELEMENTS.X_NUMBER]; + property.elNumberY = elVec3[VECTOR_ELEMENTS.Y_NUMBER]; + property.elNumberZ = elVec3[VECTOR_ELEMENTS.Z_NUMBER]; + break; + } + case 'vec2': { + let elVec2 = createVec2Property(property, elProperty); + property.elNumberX = elVec2[VECTOR_ELEMENTS.X_NUMBER]; + property.elNumberY = elVec2[VECTOR_ELEMENTS.Y_NUMBER]; + break; + } + case 'color': { + let elColor = createColorProperty(property, elProperty); + property.elColorPicker = elColor[COLOR_ELEMENTS.COLOR_PICKER]; + property.elNumberR = elColor[COLOR_ELEMENTS.RED_NUMBER]; + property.elNumberG = elColor[COLOR_ELEMENTS.GREEN_NUMBER]; + property.elNumberB = elColor[COLOR_ELEMENTS.BLUE_NUMBER]; + break; + } + case 'dropdown': { + property.elInput = createDropdownProperty(property, propertyID, elProperty); + break; + } + case 'textarea': { + property.elInput = createTextareaProperty(property, elProperty); + break; + } + case 'icon': { + property.elSpan = createIconProperty(property, elProperty); + break; + } + case 'texture': { + let elTexture = createTextureProperty(property, elProperty); + property.elImage = elTexture[TEXTURE_ELEMENTS.IMAGE]; + property.elInput = elTexture[TEXTURE_ELEMENTS.TEXT_INPUT]; + break; + } + case 'buttons': { + property.elProperty = createButtonsProperty(property, elProperty); + break; + } + case 'placeholder': + case 'sub-header': { + break; + } + default: { + console.log("EntityProperties - Unknown property type " + + propertyType + " set to property " + propertyID); + break; + } + } + + return property; +} + + +/** + * BUTTON CALLBACKS + */ + +function rescaleDimensions() { + EventBridge.emitWebEvent(JSON.stringify({ + type: "action", + action: "rescaleDimensions", + percentage: parseFloat(document.getElementById("property-scale").value) + })); +} + +function moveSelectionToGrid() { + EventBridge.emitWebEvent(JSON.stringify({ + type: "action", + action: "moveSelectionToGrid" + })); +} + +function moveAllToGrid() { + EventBridge.emitWebEvent(JSON.stringify({ + type: "action", + action: "moveAllToGrid" + })); +} + +function resetToNaturalDimensions() { + EventBridge.emitWebEvent(JSON.stringify({ + type: "action", + action: "resetToNaturalDimensions" + })); +} + +function reloadScripts() { + EventBridge.emitWebEvent(JSON.stringify({ + type: "action", + action: "reloadClientScripts" + })); +} + +function reloadServerScripts() { + // invalidate the current status (so that same-same updates can still be observed visually) + document.getElementById("property-serverScripts-status").innerText = PENDING_SCRIPT_STATUS; + EventBridge.emitWebEvent(JSON.stringify({ + type: "action", + action: "reloadServerScripts" + })); +} + +function copySkyboxURLToAmbientURL() { + let skyboxURL = getPropertyInputElement("skybox.url").value; + getPropertyInputElement("ambientLight.ambientURL").value = skyboxURL; + updateProperty("ambientLight.ambientURL", skyboxURL, false); +} + + +/** + * USER DATA FUNCTIONS + */ + +function clearUserData() { + let elUserData = getPropertyInputElement("userData"); + deleteJSONEditor(); + elUserData.value = ""; + showUserDataTextArea(); + showNewJSONEditorButton(); + hideSaveUserDataButton(); + updateProperty('userData', elUserData.value, false); +} + +function newJSONEditor() { + deleteJSONEditor(); + createJSONEditor(); + let data = {}; + setEditorJSON(data); + hideUserDataTextArea(); + hideNewJSONEditorButton(); + showSaveUserDataButton(); +} + +function saveUserData() { + saveJSONUserData(true); } function setUserDataFromEditor(noUpdate) { - var json = null; + let json = null; try { json = editor.get(); } catch (e) { @@ -292,7 +2318,7 @@ function setUserDataFromEditor(noUpdate) { if (json === null) { return; } else { - var text = editor.getText(); + let text = editor.getText(); if (noUpdate === true) { EventBridge.emitWebEvent( JSON.stringify({ @@ -305,17 +2331,17 @@ function setUserDataFromEditor(noUpdate) { ); return; } else { - updateProperty('userData', text); + updateProperty('userData', text, false); } } } function multiDataUpdater(groupName, updateKeyPair, userDataElement, defaults, removeKeys) { - var properties = {}; - var parsedData = {}; - var keysToBeRemoved = removeKeys ? removeKeys : []; + let propertyUpdate = {}; + let parsedData = {}; + let keysToBeRemoved = removeKeys ? removeKeys : []; try { - if ($('#userdata-editor').css('height') !== "0px") { + if ($('#property-userData-editor').css('height') !== "0px") { // if there is an expanded, we want to use its json. parsedData = getEditorJSON(); } else { @@ -328,14 +2354,14 @@ function multiDataUpdater(groupName, updateKeyPair, userDataElement, defaults, r if (!(groupName in parsedData)) { parsedData[groupName] = {}; } - var keys = Object.keys(updateKeyPair); + let keys = Object.keys(updateKeyPair); keys.forEach(function (key) { if (updateKeyPair[key] !== null && updateKeyPair[key] !== "null") { if (updateKeyPair[key] instanceof Element) { if (updateKeyPair[key].type === "checkbox") { parsedData[groupName][key] = updateKeyPair[key].checked; } else { - var val = isNaN(updateKeyPair[key].value) ? updateKeyPair[key].value : parseInt(updateKeyPair[key].value); + let val = isNaN(updateKeyPair[key].value) ? updateKeyPair[key].value : parseInt(updateKeyPair[key].value); parsedData[groupName][key] = val; } } else { @@ -355,159 +2381,21 @@ function multiDataUpdater(groupName, updateKeyPair, userDataElement, defaults, r delete parsedData[groupName]; } if (Object.keys(parsedData).length > 0) { - properties.userData = JSON.stringify(parsedData); + propertyUpdate.userData = JSON.stringify(parsedData); } else { - properties.userData = ''; + propertyUpdate.userData = ''; } - userDataElement.value = properties.userData; + userDataElement.value = propertyUpdate.userData; - updateProperties(properties); -} -function userDataChanger(groupName, keyName, values, userDataElement, defaultValue, removeKeys) { - var val = {}, def = {}; - val[keyName] = values; - def[keyName] = defaultValue; - multiDataUpdater(groupName, val, userDataElement, def, removeKeys); -} - -function setMaterialDataFromEditor(noUpdate) { - var json = null; - try { - json = materialEditor.get(); - } catch (e) { - alert('Invalid JSON code - look for red X in your code ', +e); - } - if (json === null) { - return; - } else { - var text = materialEditor.getText(); - if (noUpdate === true) { - EventBridge.emitWebEvent( - JSON.stringify({ - id: lastEntityID, - type: "saveMaterialData", - properties: { - materialData: text - } - }) - ); - return; - } else { - updateProperty('materialData', text); - } - } -} - -function setTextareaScrolling(element) { - var isScrolling = element.scrollHeight > element.offsetHeight; - element.setAttribute("scrolling", isScrolling ? "true" : "false"); -} - - -var materialEditor = null; - -function createJSONMaterialEditor() { - var container = document.getElementById("materialdata-editor"); - var options = { - search: false, - mode: 'tree', - modes: ['code', 'tree'], - name: 'materialData', - onModeChange: function() { - $('.jsoneditor-poweredBy').remove(); - }, - onError: function(e) { - alert('JSON editor:' + e); - }, - onChange: function() { - var currentJSONString = materialEditor.getText(); - - if (currentJSONString === '{"":""}') { - return; - } - $('#materialdata-save').attr('disabled', false); - - - } - }; - materialEditor = new JSONEditor(container, options); -} - -function hideNewJSONMaterialEditorButton() { - $('#materialdata-new-editor').hide(); -} - -function showSaveMaterialDataButton() { - $('#materialdata-save').show(); -} - -function hideSaveMaterialDataButton() { - $('#materialdata-save').hide(); -} - -function showNewJSONMaterialEditorButton() { - $('#materialdata-new-editor').show(); -} - -function showMaterialDataTextArea() { - $('#property-material-data').show(); -} - -function hideMaterialDataTextArea() { - $('#property-material-data').hide(); -} - -function showStaticMaterialData() { - if (materialEditor !== null) { - $('#static-materialdata').show(); - $('#static-materialdata').css('height', $('#materialdata-editor').height()); - $('#static-materialdata').text(materialEditor.getText()); - } -} - -function removeStaticMaterialData() { - $('#static-materialdata').hide(); -} - -function setMaterialEditorJSON(json) { - materialEditor.set(json); - if (materialEditor.hasOwnProperty('expandAll')) { - materialEditor.expandAll(); - } -} - -function getMaterialEditorJSON() { - return materialEditor.get(); -} - -function deleteJSONMaterialEditor() { - if (materialEditor !== null) { - materialEditor.destroy(); - materialEditor = null; - } -} - -var savedMaterialJSONTimer = null; - -function saveJSONMaterialData(noUpdate) { - setMaterialDataFromEditor(noUpdate); - $('#materialdata-saved').show(); - $('#materialdata-save').attr('disabled', true); - if (savedMaterialJSONTimer !== null) { - clearTimeout(savedMaterialJSONTimer); - } - savedMaterialJSONTimer = setTimeout(function() { - $('#materialdata-saved').hide(); - - }, EDITOR_TIMEOUT_DURATION); + updateProperties(propertyUpdate); } var editor = null; function createJSONEditor() { - var container = document.getElementById("userdata-editor"); - var options = { + let container = document.getElementById("property-userData-editor"); + let options = { search: false, mode: 'tree', modes: ['code', 'tree'], @@ -519,12 +2407,12 @@ function createJSONEditor() { alert('JSON editor:' + e); }, onChange: function() { - var currentJSONString = editor.getText(); + let currentJSONString = editor.getText(); if (currentJSONString === '{"":""}') { return; } - $('#userdata-save').attr('disabled', false); + $('#property-userData-button-save').attr('disabled', false); } @@ -532,40 +2420,48 @@ function createJSONEditor() { editor = new JSONEditor(container, options); } -function hideNewJSONEditorButton() { - $('#userdata-new-editor').hide(); -} - function showSaveUserDataButton() { - $('#userdata-save').show(); + $('#property-userData-button-save').show(); } function hideSaveUserDataButton() { - $('#userdata-save').hide(); + $('#property-userData-button-save').hide(); +} + +function disableSaveUserDataButton() { + $('#property-userData-button-save').attr('disabled', true); } function showNewJSONEditorButton() { - $('#userdata-new-editor').show(); + $('#property-userData-button-edit').show(); +} + +function hideNewJSONEditorButton() { + $('#property-userData-button-edit').hide(); } function showUserDataTextArea() { - $('#property-user-data').show(); + $('#property-userData').show(); } function hideUserDataTextArea() { - $('#property-user-data').hide(); + $('#property-userData').hide(); +} + +function hideUserDataSaved() { + $('#property-userData-saved').hide(); } function showStaticUserData() { if (editor !== null) { - $('#static-userdata').show(); - $('#static-userdata').css('height', $('#userdata-editor').height()); - $('#static-userdata').text(editor.getText()); + $('#property-userData-static').show(); + $('#property-userData-static').css('height', $('#property-userData-editor').height()); + $('#property-userData-static').text(editor.getText()); } } function removeStaticUserData() { - $('#static-userdata').hide(); + $('#property-userData-static').hide(); } function setEditorJSON(json) { @@ -590,299 +2486,423 @@ var savedJSONTimer = null; function saveJSONUserData(noUpdate) { setUserDataFromEditor(noUpdate); - $('#userdata-saved').show(); - $('#userdata-save').attr('disabled', true); + $('#property-userData-saved').show(); + $('#property-userData-button-save').attr('disabled', true); if (savedJSONTimer !== null) { clearTimeout(savedJSONTimer); } savedJSONTimer = setTimeout(function() { - $('#userdata-saved').hide(); + hideUserDataSaved(); + }, EDITOR_TIMEOUT_DURATION); +} + +/** + * MATERIAL DATA FUNCTIONS + */ + +function clearMaterialData() { + let elMaterialData = getPropertyInputElement("materialData"); + deleteJSONMaterialEditor(); + elMaterialData.value = ""; + showMaterialDataTextArea(); + showNewJSONMaterialEditorButton(); + hideSaveMaterialDataButton(); + updateProperty('materialData', elMaterialData.value, false); +} + +function newJSONMaterialEditor() { + deleteJSONMaterialEditor(); + createJSONMaterialEditor(); + let data = {}; + setMaterialEditorJSON(data); + hideMaterialDataTextArea(); + hideNewJSONMaterialEditorButton(); + showSaveMaterialDataButton(); +} + +function saveMaterialData() { + saveJSONMaterialData(true); +} + +function setMaterialDataFromEditor(noUpdate) { + let json = null; + try { + json = materialEditor.get(); + } catch (e) { + alert('Invalid JSON code - look for red X in your code ', +e); + } + if (json === null) { + return; + } else { + let text = materialEditor.getText(); + if (noUpdate === true) { + EventBridge.emitWebEvent( + JSON.stringify({ + id: lastEntityID, + type: "saveMaterialData", + properties: { + materialData: text + } + }) + ); + return; + } else { + updateProperty('materialData', text, false); + } + } +} + +var materialEditor = null; + +function createJSONMaterialEditor() { + let container = document.getElementById("property-materialData-editor"); + let options = { + search: false, + mode: 'tree', + modes: ['code', 'tree'], + name: 'materialData', + onModeChange: function() { + $('.jsoneditor-poweredBy').remove(); + }, + onError: function(e) { + alert('JSON editor:' + e); + }, + onChange: function() { + let currentJSONString = materialEditor.getText(); + + if (currentJSONString === '{"":""}') { + return; + } + $('#property-materialData-button-save').attr('disabled', false); + + + } + }; + materialEditor = new JSONEditor(container, options); +} + +function showSaveMaterialDataButton() { + $('#property-materialData-button-save').show(); +} + +function hideSaveMaterialDataButton() { + $('#property-materialData-button-save').hide(); +} + +function disableSaveMaterialDataButton() { + $('#property-materialData-button-save').attr('disabled', true); +} + +function showNewJSONMaterialEditorButton() { + $('#property-materialData-button-edit').show(); +} + +function hideNewJSONMaterialEditorButton() { + $('#property-materialData-button-edit').hide(); +} + +function showMaterialDataTextArea() { + $('#property-materialData').show(); +} + +function hideMaterialDataTextArea() { + $('#property-materialData').hide(); +} + +function hideMaterialDataSaved() { + $('#property-materialData-saved').hide(); +} + +function showStaticMaterialData() { + if (materialEditor !== null) { + $('#property-materialData-static').show(); + $('#property-materialData-static').css('height', $('#property-materialData-editor').height()); + $('#property-materialData-static').text(materialEditor.getText()); + } +} + +function removeStaticMaterialData() { + $('#property-materialData-static').hide(); +} + +function setMaterialEditorJSON(json) { + materialEditor.set(json); + if (materialEditor.hasOwnProperty('expandAll')) { + materialEditor.expandAll(); + } +} + +function getMaterialEditorJSON() { + return materialEditor.get(); +} + +function deleteJSONMaterialEditor() { + if (materialEditor !== null) { + materialEditor.destroy(); + materialEditor = null; + } +} + +var savedMaterialJSONTimer = null; + +function saveJSONMaterialData(noUpdate) { + setMaterialDataFromEditor(noUpdate); + $('#property-materialData-saved').show(); + $('#property-materialData-button-save').attr('disabled', true); + if (savedMaterialJSONTimer !== null) { + clearTimeout(savedMaterialJSONTimer); + } + savedMaterialJSONTimer = setTimeout(function() { + hideMaterialDataSaved(); }, EDITOR_TIMEOUT_DURATION); } function bindAllNonJSONEditorElements() { - var inputs = $('input'); - var i; - for (i = 0; i < inputs.length; i++) { - var input = inputs[i]; - var field = $(input); + let inputs = $('input'); + let i; + for (i = 0; i < inputs.length; ++i) { + let input = inputs[i]; + let field = $(input); // TODO FIXME: (JSHint) Functions declared within loops referencing // an outer scoped variable may lead to confusing semantics. field.on('focus', function(e) { - if (e.target.id === "userdata-new-editor" || e.target.id === "userdata-clear" || e.target.id === "materialdata-new-editor" || e.target.id === "materialdata-clear") { + if (e.target.id === "property-userData-button-edit" || e.target.id === "property-userData-button-clear" || + e.target.id === "property-materialData-button-edit" || e.target.id === "property-materialData-button-clear") { return; } else { - if ($('#userdata-editor').css('height') !== "0px") { - saveJSONUserData(true); + if ($('#property-userData-editor').css('height') !== "0px") { + saveUserData(); } - if ($('#materialdata-editor').css('height') !== "0px") { - saveJSONMaterialData(true); + if ($('#property-materialData-editor').css('height') !== "0px") { + saveMaterialData(); } } }); } } -function unbindAllInputs() { - var inputs = $('input'); - var i; - for (i = 0; i < inputs.length; i++) { - var input = inputs[i]; - var field = $(input); - field.unbind(); + +/** + * DROPDOWN FUNCTIONS + */ + +function setDropdownText(dropdown) { + let lis = dropdown.parentNode.getElementsByTagName("li"); + let text = ""; + for (let i = 0; i < lis.length; ++i) { + if (String(lis[i].getAttribute("value")) === String(dropdown.value)) { + text = lis[i].textContent; + } } + dropdown.firstChild.textContent = text; +} + +function toggleDropdown(event) { + let element = event.target; + if (element.nodeName !== "DT") { + element = element.parentNode; + } + element = element.parentNode; + let isDropped = element.getAttribute("dropped"); + element.setAttribute("dropped", isDropped !== "true" ? "true" : "false"); +} + +function setDropdownValue(event) { + let dt = event.target.parentNode.parentNode.previousSibling; + dt.value = event.target.getAttribute("value"); + dt.firstChild.textContent = event.target.textContent; + + dt.parentNode.setAttribute("dropped", "false"); + + let evt = document.createEvent("HTMLEvents"); + evt.initEvent("change", true, true); + dt.dispatchEvent(evt); +} + + +/** + * TEXTAREA / PARENT MATERIAL NAME FUNCTIONS + */ + +function setTextareaScrolling(element) { + let isScrolling = element.scrollHeight > element.offsetHeight; + element.setAttribute("scrolling", isScrolling ? "true" : "false"); } function showParentMaterialNameBox(number, elNumber, elString) { if (number) { - $('#property-parent-material-id-number-container').show(); - $('#property-parent-material-id-string-container').hide(); + $('#property-submeshToReplace').parent().show(); + $('#property-materialNameToReplace').parent().hide(); elString.value = ""; } else { - $('#property-parent-material-id-string-container').show(); - $('#property-parent-material-id-number-container').hide(); + $('#property-materialNameToReplace').parent().show(); + $('#property-submeshToReplace').parent().hide(); elNumber.value = 0; } } + function loaded() { openEventBridge(function() { + let elPropertiesList = document.getElementById("properties-list"); - var elPropertiesList = document.getElementById("properties-list"); - var elID = document.getElementById("property-id"); - var elType = document.getElementById("property-type"); - var elTypeIcon = document.getElementById("type-icon"); - var elName = document.getElementById("property-name"); - var elLocked = document.getElementById("property-locked"); - var elVisible = document.getElementById("property-visible"); - var elPositionX = document.getElementById("property-pos-x"); - var elPositionY = document.getElementById("property-pos-y"); - var elPositionZ = document.getElementById("property-pos-z"); - var elMoveSelectionToGrid = document.getElementById("move-selection-to-grid"); - var elMoveAllToGrid = document.getElementById("move-all-to-grid"); - - var elDimensionsX = document.getElementById("property-dim-x"); - var elDimensionsY = document.getElementById("property-dim-y"); - var elDimensionsZ = document.getElementById("property-dim-z"); - var elResetToNaturalDimensions = document.getElementById("reset-to-natural-dimensions"); - var elRescaleDimensionsPct = document.getElementById("dimension-rescale-pct"); - var elRescaleDimensionsButton = document.getElementById("dimension-rescale-button"); - - var elParentID = document.getElementById("property-parent-id"); - var elParentJointIndex = document.getElementById("property-parent-joint-index"); - - var elRegistrationX = document.getElementById("property-reg-x"); - var elRegistrationY = document.getElementById("property-reg-y"); - var elRegistrationZ = document.getElementById("property-reg-z"); - - var elRotationX = document.getElementById("property-rot-x"); - var elRotationY = document.getElementById("property-rot-y"); - var elRotationZ = document.getElementById("property-rot-z"); - - var elLinearVelocityX = document.getElementById("property-lvel-x"); - var elLinearVelocityY = document.getElementById("property-lvel-y"); - var elLinearVelocityZ = document.getElementById("property-lvel-z"); - var elLinearDamping = document.getElementById("property-ldamping"); - - var elAngularVelocityX = document.getElementById("property-avel-x"); - var elAngularVelocityY = document.getElementById("property-avel-y"); - var elAngularVelocityZ = document.getElementById("property-avel-z"); - var elAngularDamping = document.getElementById("property-adamping"); - - var elRestitution = document.getElementById("property-restitution"); - var elFriction = document.getElementById("property-friction"); - - var elGravityX = document.getElementById("property-grav-x"); - var elGravityY = document.getElementById("property-grav-y"); - var elGravityZ = document.getElementById("property-grav-z"); - - var elAccelerationX = document.getElementById("property-lacc-x"); - var elAccelerationY = document.getElementById("property-lacc-y"); - var elAccelerationZ = document.getElementById("property-lacc-z"); - - var elDensity = document.getElementById("property-density"); - var elCollisionless = document.getElementById("property-collisionless"); - var elDynamic = document.getElementById("property-dynamic"); - var elCollideStatic = document.getElementById("property-collide-static"); - var elCollideDynamic = document.getElementById("property-collide-dynamic"); - var elCollideKinematic = document.getElementById("property-collide-kinematic"); - var elCollideMyAvatar = document.getElementById("property-collide-myAvatar"); - var elCollideOtherAvatar = document.getElementById("property-collide-otherAvatar"); - var elCollisionSoundURL = document.getElementById("property-collision-sound-url"); - - var elGrabbable = document.getElementById("property-grabbable"); - - var elCloneable = document.getElementById("property-cloneable"); - var elCloneableDynamic = document.getElementById("property-cloneable-dynamic"); - var elCloneableAvatarEntity = document.getElementById("property-cloneable-avatarEntity"); - var elCloneableGroup = document.getElementById("group-cloneable-group"); - var elCloneableLifetime = document.getElementById("property-cloneable-lifetime"); - var elCloneableLimit = document.getElementById("property-cloneable-limit"); - - var elTriggerable = document.getElementById("property-triggerable"); - var elIgnoreIK = document.getElementById("property-ignore-ik"); - - var elLifetime = document.getElementById("property-lifetime"); - var elScriptURL = document.getElementById("property-script-url"); - var elScriptTimestamp = document.getElementById("property-script-timestamp"); - var elReloadScriptsButton = document.getElementById("reload-script-button"); - var elServerScripts = document.getElementById("property-server-scripts"); - var elReloadServerScriptsButton = document.getElementById("reload-server-scripts-button"); - var elServerScriptStatus = document.getElementById("server-script-status"); - var elServerScriptError = document.getElementById("server-script-error"); - var elUserData = document.getElementById("property-user-data"); - var elClearUserData = document.getElementById("userdata-clear"); - var elSaveUserData = document.getElementById("userdata-save"); - var elNewJSONEditor = document.getElementById('userdata-new-editor'); - var elColorControlVariant2 = document.getElementById("property-color-control2"); - var elColorRed = document.getElementById("property-color-red"); - var elColorGreen = document.getElementById("property-color-green"); - var elColorBlue = document.getElementById("property-color-blue"); - - var elShape = document.getElementById("property-shape"); - - var elCanCastShadow = document.getElementById("property-can-cast-shadow"); - - var elLightSpotLight = document.getElementById("property-light-spot-light"); - var elLightColor = document.getElementById("property-light-color"); - var elLightColorRed = document.getElementById("property-light-color-red"); - var elLightColorGreen = document.getElementById("property-light-color-green"); - var elLightColorBlue = document.getElementById("property-light-color-blue"); - - var elLightIntensity = document.getElementById("property-light-intensity"); - var elLightFalloffRadius = document.getElementById("property-light-falloff-radius"); - var elLightExponent = document.getElementById("property-light-exponent"); - var elLightCutoff = document.getElementById("property-light-cutoff"); - - var elModelURL = document.getElementById("property-model-url"); - var elShapeType = document.getElementById("property-shape-type"); - var elCompoundShapeURL = document.getElementById("property-compound-shape-url"); - var elModelAnimationURL = document.getElementById("property-model-animation-url"); - var elModelAnimationPlaying = document.getElementById("property-model-animation-playing"); - var elModelAnimationFPS = document.getElementById("property-model-animation-fps"); - var elModelAnimationFrame = document.getElementById("property-model-animation-frame"); - var elModelAnimationFirstFrame = document.getElementById("property-model-animation-first-frame"); - var elModelAnimationLastFrame = document.getElementById("property-model-animation-last-frame"); - var elModelAnimationLoop = document.getElementById("property-model-animation-loop"); - var elModelAnimationHold = document.getElementById("property-model-animation-hold"); - var elModelAnimationAllowTranslation = document.getElementById("property-model-animation-allow-translation"); - var elModelTextures = document.getElementById("property-model-textures"); - var elModelOriginalTextures = document.getElementById("property-model-original-textures"); - - var elMaterialURL = document.getElementById("property-material-url"); - //var elMaterialMappingMode = document.getElementById("property-material-mapping-mode"); - var elPriority = document.getElementById("property-priority"); - var elParentMaterialNameString = document.getElementById("property-parent-material-id-string"); - var elParentMaterialNameNumber = document.getElementById("property-parent-material-id-number"); - var elParentMaterialNameCheckbox = document.getElementById("property-parent-material-id-checkbox"); - var elMaterialMappingPosX = document.getElementById("property-material-mapping-pos-x"); - var elMaterialMappingPosY = document.getElementById("property-material-mapping-pos-y"); - var elMaterialMappingScaleX = document.getElementById("property-material-mapping-scale-x"); - var elMaterialMappingScaleY = document.getElementById("property-material-mapping-scale-y"); - var elMaterialMappingRot = document.getElementById("property-material-mapping-rot"); - var elMaterialData = document.getElementById("property-material-data"); - var elClearMaterialData = document.getElementById("materialdata-clear"); - var elSaveMaterialData = document.getElementById("materialdata-save"); - var elNewJSONMaterialEditor = document.getElementById('materialdata-new-editor'); - - var elImageURL = document.getElementById("property-image-url"); - - var elWebSourceURL = document.getElementById("property-web-source-url"); - var elWebDPI = document.getElementById("property-web-dpi"); - - var elDescription = document.getElementById("property-description"); - - var elHyperlinkHref = document.getElementById("property-hyperlink-href"); - - var elTextText = document.getElementById("property-text-text"); - var elTextLineHeight = document.getElementById("property-text-line-height"); - var elTextTextColor = document.getElementById("property-text-text-color"); - var elTextFaceCamera = document.getElementById("property-text-face-camera"); - var elTextTextColorRed = document.getElementById("property-text-text-color-red"); - var elTextTextColorGreen = document.getElementById("property-text-text-color-green"); - var elTextTextColorBlue = document.getElementById("property-text-text-color-blue"); - var elTextBackgroundColor = document.getElementById("property-text-background-color"); - var elTextBackgroundColorRed = document.getElementById("property-text-background-color-red"); - var elTextBackgroundColorGreen = document.getElementById("property-text-background-color-green"); - var elTextBackgroundColorBlue = document.getElementById("property-text-background-color-blue"); - - // Key light - var elZoneKeyLightModeInherit = document.getElementById("property-zone-key-light-mode-inherit"); - var elZoneKeyLightModeDisabled = document.getElementById("property-zone-key-light-mode-disabled"); - var elZoneKeyLightModeEnabled = document.getElementById("property-zone-key-light-mode-enabled"); - - var elZoneKeyLightColor = document.getElementById("property-zone-key-light-color"); - var elZoneKeyLightColorRed = document.getElementById("property-zone-key-light-color-red"); - var elZoneKeyLightColorGreen = document.getElementById("property-zone-key-light-color-green"); - var elZoneKeyLightColorBlue = document.getElementById("property-zone-key-light-color-blue"); - var elZoneKeyLightIntensity = document.getElementById("property-zone-key-intensity"); - var elZoneKeyLightDirectionX = document.getElementById("property-zone-key-light-direction-x"); - var elZoneKeyLightDirectionY = document.getElementById("property-zone-key-light-direction-y"); - - var elZoneKeyLightCastShadows = document.getElementById("property-zone-key-light-cast-shadows"); - - // Skybox - var elZoneSkyboxModeInherit = document.getElementById("property-zone-skybox-mode-inherit"); - var elZoneSkyboxModeDisabled = document.getElementById("property-zone-skybox-mode-disabled"); - var elZoneSkyboxModeEnabled = document.getElementById("property-zone-skybox-mode-enabled"); - - // Ambient light - var elCopySkyboxURLToAmbientURL = document.getElementById("copy-skybox-url-to-ambient-url"); - - var elZoneAmbientLightModeInherit = document.getElementById("property-zone-ambient-light-mode-inherit"); - var elZoneAmbientLightModeDisabled = document.getElementById("property-zone-ambient-light-mode-disabled"); - var elZoneAmbientLightModeEnabled = document.getElementById("property-zone-ambient-light-mode-enabled"); - - var elZoneAmbientLightIntensity = document.getElementById("property-zone-key-ambient-intensity"); - var elZoneAmbientLightURL = document.getElementById("property-zone-key-ambient-url"); - - // Haze - var elZoneHazeModeInherit = document.getElementById("property-zone-haze-mode-inherit"); - var elZoneHazeModeDisabled = document.getElementById("property-zone-haze-mode-disabled"); - var elZoneHazeModeEnabled = document.getElementById("property-zone-haze-mode-enabled"); + let templatePropertyRow = document.getElementById('property-row'); - var elZoneHazeRange = document.getElementById("property-zone-haze-range"); - var elZoneHazeColor = document.getElementById("property-zone-haze-color"); - var elZoneHazeColorRed = document.getElementById("property-zone-haze-color-red"); - var elZoneHazeColorGreen = document.getElementById("property-zone-haze-color-green"); - var elZoneHazeColorBlue = document.getElementById("property-zone-haze-color-blue"); - var elZoneHazeGlareColor = document.getElementById("property-zone-haze-glare-color"); - var elZoneHazeGlareColorRed = document.getElementById("property-zone-haze-glare-color-red"); - var elZoneHazeGlareColorGreen = document.getElementById("property-zone-haze-glare-color-green"); - var elZoneHazeGlareColorBlue = document.getElementById("property-zone-haze-glare-color-blue"); - var elZoneHazeEnableGlare = document.getElementById("property-zone-haze-enable-light-blend"); - var elZoneHazeGlareAngle = document.getElementById("property-zone-haze-blend-angle"); + GROUPS.forEach(function(group) { + let elGroup; + if (group.addToGroup !== undefined) { + let fieldset = document.getElementById("properties-" + group.addToGroup); + elGroup = document.createElement('div'); + fieldset.appendChild(elGroup); + } else { + elGroup = document.createElement('div'); + elGroup.className = 'section ' + (group.isMinor ? "minor" : "major"); + elGroup.setAttribute("id", "properties-" + group.id); + elPropertiesList.appendChild(elGroup); + } + + if (group.label !== undefined) { + let elLegend = document.createElement('div'); + elLegend.className = "section-header"; + + elLegend.appendChild(createElementFromHTML(`
${group.label}
`)); + + let elSpan = document.createElement('span'); + elSpan.className = ".collapse-icon"; + elSpan.innerText = "M"; + elLegend.appendChild(elSpan); + elGroup.appendChild(elLegend); + } + + group.properties.forEach(function(propertyData) { + let propertyType = propertyData.type; + let propertyID = propertyData.propertyID; + let propertyName = propertyData.propertyName !== undefined ? propertyData.propertyName : propertyID; + let propertySpaceMode = propertyData.spaceMode !== undefined ? propertyData.spaceMode : PROPERTY_SPACE_MODE.ALL; + let propertyElementID = "property-" + propertyID; + propertyElementID = propertyElementID.replace('.', '-'); + + let elContainer, elLabel; + + if (propertyData.replaceID === undefined) { + // Create subheader, or create new property and append it. + if (propertyType === "sub-header") { + elContainer = createElementFromHTML( + `
${propertyData.label}
`); + } else { + elContainer = document.createElement('div'); + elContainer.setAttribute("id", "div-" + propertyElementID); + elContainer.className = 'property container'; + } + + if (group.twoColumn && propertyData.column !== undefined && propertyData.column !== -1) { + let columnName = group.id + "column" + propertyData.column; + let elColumn = document.getElementById(columnName); + if (!elColumn) { + let columnDivName = group.id + "columnDiv"; + let elColumnDiv = document.getElementById(columnDivName); + if (!elColumnDiv) { + elColumnDiv = document.createElement('div'); + elColumnDiv.className = "two-column"; + elColumnDiv.setAttribute("id", group.id + "columnDiv"); + elGroup.appendChild(elColumnDiv); + } + elColumn = document.createElement('fieldset'); + elColumn.className = "column"; + elColumn.setAttribute("id", columnName); + elColumnDiv.appendChild(elColumn); + } + elColumn.appendChild(elContainer); + } else { + elGroup.appendChild(elContainer); + } + + elLabel = document.createElement('label'); + elLabel.setAttribute("for", propertyElementID); + if (propertyData.indentedLabel || propertyData.showPropertyRule !== undefined) { + let elSpan = document.createElement('span'); + elSpan.className = 'indented'; + elSpan.innerText = propertyData.label !== undefined ? propertyData.label : ""; + elLabel.appendChild(elSpan); + } else { + elLabel.innerText = propertyData.label !== undefined ? propertyData.label : ""; + } + elContainer.appendChild(elLabel); + } else { + elContainer = document.getElementById(propertyData.replaceID); + } + + if (elLabel) { + createAppTooltip.registerTooltipElement(elLabel, propertyID); + } + + let elProperty = createElementFromHTML('
'); + elContainer.appendChild(elProperty); + + if (propertyType === 'triple') { + elProperty.className = 'flex-row'; + for (let i = 0; i < propertyData.properties.length; ++i) { + let innerPropertyData = propertyData.properties[i]; + + let elWrapper = createElementFromHTML('
'); + + let propertyID = innerPropertyData.propertyID; + let propertyName = innerPropertyData.propertyName !== undefined ? innerPropertyData.propertyName : propertyID; + let propertyElementID = "property-" + propertyID; + propertyElementID = propertyElementID.replace('.', '-'); + + elWrapper.appendChild(createElementFromHTML(`
${innerPropertyData.label}
`)); + elProperty.appendChild(elWrapper); + + let property = createProperty(innerPropertyData, propertyElementID, propertyName, propertyID, elWrapper.childNodes[0]); + property.isParticleProperty = group.id.includes("particles"); + property.elContainer = elContainer; + property.spaceMode = propertySpaceMode; + + if (property.type !== 'placeholder') { + properties[propertyID] = property; + } + } + } else { + let property = createProperty(propertyData, propertyElementID, propertyName, propertyID, elProperty); + property.isParticleProperty = group.id.includes("particles"); + property.elContainer = elContainer; + property.spaceMode = propertySpaceMode; + + if (property.type !== 'placeholder') { + properties[propertyID] = property; + } + + let showPropertyRule = propertyData.showPropertyRule; + if (showPropertyRule !== undefined) { + let dependentProperty = Object.keys(showPropertyRule)[0]; + let dependentPropertyValue = showPropertyRule[dependentProperty]; + if (properties[dependentProperty] === undefined) { + properties[dependentProperty] = {}; + } + if (properties[dependentProperty].showPropertyRules === undefined) { + properties[dependentProperty].showPropertyRules = {}; + } + properties[dependentProperty].showPropertyRules[propertyID] = dependentPropertyValue; + } + } + }); + + elGroups[group.id] = elGroup; + }); + + let minorSections = document.querySelectorAll(".section.minor"); + minorSections[minorSections.length - 1].className += " last"; + + updateVisibleSpaceModeProperties(); - var elZoneHazeAltitudeEffect = document.getElementById("property-zone-haze-altitude-effect"); - var elZoneHazeBaseRef = document.getElementById("property-zone-haze-base"); - var elZoneHazeCeiling = document.getElementById("property-zone-haze-ceiling"); - - var elZoneHazeBackgroundBlend = document.getElementById("property-zone-haze-background-blend"); - - var elZoneSkyboxColor = document.getElementById("property-zone-skybox-color"); - var elZoneSkyboxColorRed = document.getElementById("property-zone-skybox-color-red"); - var elZoneSkyboxColorGreen = document.getElementById("property-zone-skybox-color-green"); - var elZoneSkyboxColorBlue = document.getElementById("property-zone-skybox-color-blue"); - var elZoneSkyboxURL = document.getElementById("property-zone-skybox-url"); - - var elZoneFlyingAllowed = document.getElementById("property-zone-flying-allowed"); - var elZoneGhostingAllowed = document.getElementById("property-zone-ghosting-allowed"); - var elZoneFilterURL = document.getElementById("property-zone-filter-url"); - - var elVoxelVolumeSizeX = document.getElementById("property-voxel-volume-size-x"); - var elVoxelVolumeSizeY = document.getElementById("property-voxel-volume-size-y"); - var elVoxelVolumeSizeZ = document.getElementById("property-voxel-volume-size-z"); - var elVoxelSurfaceStyle = document.getElementById("property-voxel-surface-style"); - var elXTextureURL = document.getElementById("property-x-texture-url"); - var elYTextureURL = document.getElementById("property-y-texture-url"); - var elZTextureURL = document.getElementById("property-z-texture-url"); - if (window.EventBridge !== undefined) { - var properties; EventBridge.scriptEventReceived.connect(function(data) { data = JSON.parse(data); if (data.type === "server_script_status") { + let elServerScriptError = document.getElementById("property-serverScripts-error"); + let elServerScriptStatus = document.getElementById("property-serverScripts-status"); elServerScriptError.value = data.errorInfo; // If we just set elServerScriptError's diplay to block or none, we still end up with // it's parent contributing 21px bottom padding even when elServerScriptError is display:none. @@ -891,244 +2911,59 @@ function loaded() { if (data.statusRetrieved === false) { elServerScriptStatus.innerText = "Failed to retrieve status"; } else if (data.isRunning) { - var ENTITY_SCRIPT_STATUS = { - pending: "Pending", - loading: "Loading", - error_loading_script: "Error loading script", // eslint-disable-line camelcase - error_running_script: "Error running script", // eslint-disable-line camelcase - running: "Running", - unloaded: "Unloaded" - }; elServerScriptStatus.innerText = ENTITY_SCRIPT_STATUS[data.status] || data.status; } else { - elServerScriptStatus.innerText = "Not running"; + elServerScriptStatus.innerText = NOT_RUNNING_SCRIPT_STATUS; } } else if (data.type === "update" && data.selections) { - + if (data.spaceMode !== undefined) { + currentSpaceMode = data.spaceMode === "local" ? PROPERTY_SPACE_MODE.LOCAL : PROPERTY_SPACE_MODE.WORLD; + } if (data.selections.length === 0) { if (lastEntityID !== null) { if (editor !== null) { - saveJSONUserData(true); + saveUserData(); deleteJSONEditor(); } if (materialEditor !== null) { - saveJSONMaterialData(true); + saveMaterialData(); deleteJSONMaterialEditor(); } } - elTypeIcon.style.display = "none"; - elType.innerHTML = "No selection"; - elPropertiesList.className = ''; - - elID.value = ""; - elName.value = ""; - elLocked.checked = false; - elVisible.checked = false; - - elParentID.value = ""; - elParentJointIndex.value = ""; - - elColorRed.value = ""; - elColorGreen.value = ""; - elColorBlue.value = ""; - elColorControlVariant2.style.backgroundColor = "rgb(" + 0 + "," + 0 + "," + 0 + ")"; + resetProperties(); + showGroupsForType("None"); - elPositionX.value = ""; - elPositionY.value = ""; - elPositionZ.value = ""; - - elRotationX.value = ""; - elRotationY.value = ""; - elRotationZ.value = ""; - - elDimensionsX.value = ""; - elDimensionsY.value = ""; - elDimensionsZ.value = ""; - - elRegistrationX.value = ""; - elRegistrationY.value = ""; - elRegistrationZ.value = ""; - - elLinearVelocityX.value = ""; - elLinearVelocityY.value = ""; - elLinearVelocityZ.value = ""; - elLinearDamping.value = ""; - - elAngularVelocityX.value = ""; - elAngularVelocityY.value = ""; - elAngularVelocityZ.value = ""; - elAngularDamping.value = ""; - - elGravityX.value = ""; - elGravityY.value = ""; - elGravityZ.value = ""; - - elAccelerationX.value = ""; - elAccelerationY.value = ""; - elAccelerationZ.value = ""; - - elRestitution.value = ""; - elFriction.value = ""; - elDensity.value = ""; - - elCollisionless.checked = false; - elDynamic.checked = false; - - elCollideStatic.checked = false; - elCollideKinematic.checked = false; - elCollideDynamic.checked = false; - elCollideMyAvatar.checked = false; - elCollideOtherAvatar.checked = false; - - elGrabbable.checked = false; - elTriggerable.checked = false; - elIgnoreIK.checked = false; - - elCloneable.checked = false; - elCloneableDynamic.checked = false; - elCloneableAvatarEntity.checked = false; - elCloneableGroup.style.display = "none"; - elCloneableLimit.value = ""; - elCloneableLifetime.value = ""; - - showElements(document.getElementsByClassName('can-cast-shadow-section'), true); - elCanCastShadow.checked = false; - - elCollisionSoundURL.value = ""; - elLifetime.value = ""; - elScriptURL.value = ""; - elServerScripts.value = ""; - elHyperlinkHref.value = ""; - elDescription.value = ""; - + let elIcon = properties.type.elSpan; + elIcon.innerText = NO_SELECTION; + elIcon.style.display = 'inline-block'; + deleteJSONEditor(); - elUserData.value = ""; + getPropertyInputElement("userData").value = ""; showUserDataTextArea(); showSaveUserDataButton(); showNewJSONEditorButton(); - - // Shape Properties - elShape.value = "Cube"; - setDropdownText(elShape); - - // Light Properties - elLightSpotLight.checked = false; - elLightColor.style.backgroundColor = "rgb(" + 0 + "," + 0 + "," + 0 + ")"; - elLightColorRed.value = ""; - elLightColorGreen.value = ""; - elLightColorBlue.value = ""; - elLightIntensity.value = ""; - elLightFalloffRadius.value = ""; - elLightExponent.value = ""; - elLightCutoff.value = ""; - - // Model Properties - elModelURL.value = ""; - elCompoundShapeURL.value = ""; - elShapeType.value = "none"; - setDropdownText(elShapeType); - elModelAnimationURL.value = "" - elModelAnimationPlaying.checked = false; - elModelAnimationFPS.value = ""; - elModelAnimationFrame.value = ""; - elModelAnimationFirstFrame.value = ""; - elModelAnimationLastFrame.value = ""; - elModelAnimationLoop.checked = false; - elModelAnimationHold.checked = false; - elModelAnimationAllowTranslation.checked = false; - elModelTextures.value = ""; - elModelOriginalTextures.value = ""; - - // Zone Properties - elZoneFlyingAllowed.checked = false; - elZoneGhostingAllowed.checked = false; - elZoneFilterURL.value = ""; - elZoneKeyLightColor.style.backgroundColor = "rgb(" + 0 + "," + 0 + "," + 0 + ")"; - elZoneKeyLightColorRed.value = ""; - elZoneKeyLightColorGreen.value = ""; - elZoneKeyLightColorBlue.value = ""; - elZoneKeyLightIntensity.value = ""; - elZoneKeyLightDirectionX.value = ""; - elZoneKeyLightDirectionY.value = ""; - elZoneKeyLightCastShadows.checked = false; - elZoneAmbientLightIntensity.value = ""; - elZoneAmbientLightURL.value = ""; - elZoneHazeRange.value = ""; - elZoneHazeColor.style.backgroundColor = "rgb(" + 0 + "," + 0 + "," + 0 + ")"; - elZoneHazeColorRed.value = ""; - elZoneHazeColorGreen.value = ""; - elZoneHazeColorBlue.value = ""; - elZoneHazeBackgroundBlend.value = 0; - elZoneHazeGlareColor.style.backgroundColor = "rgb(" + 0 + "," + 0 + "," + 0 + ")"; - elZoneHazeGlareColorRed.value = ""; - elZoneHazeGlareColorGreen.value = ""; - elZoneHazeGlareColorBlue.value = ""; - elZoneHazeEnableGlare.checked = false; - elZoneHazeGlareAngle.value = ""; - elZoneHazeAltitudeEffect.checked = false; - elZoneHazeBaseRef.value = ""; - elZoneHazeCeiling.value = ""; - elZoneSkyboxColor.style.backgroundColor = "rgb(" + 0 + "," + 0 + "," + 0 + ")"; - elZoneSkyboxColorRed.value = ""; - elZoneSkyboxColorGreen.value = ""; - elZoneSkyboxColorBlue.value = ""; - elZoneSkyboxURL.value = ""; - showElements(document.getElementsByClassName('keylight-section'), true); - showElements(document.getElementsByClassName('skybox-section'), true); - showElements(document.getElementsByClassName('ambient-section'), true); - showElements(document.getElementsByClassName('haze-section'), true); - - // Text Properties - elTextText.value = ""; - elTextLineHeight.value = ""; - elTextFaceCamera.checked = false; - elTextTextColor.style.backgroundColor = "rgb(" + 0 + "," + 0 + "," + 0 + ")"; - elTextTextColorRed.value = ""; - elTextTextColorGreen.value = ""; - elTextTextColorBlue.value = ""; - elTextBackgroundColor.style.backgroundColor = "rgb(" + 0 + "," + 0 + "," + 0 + ")"; - elTextBackgroundColorRed.value = ""; - elTextBackgroundColorGreen.value = ""; - elTextBackgroundColorBlue.value = ""; - - // Image Properties - elImageURL.value = ""; - - // Web Properties - elWebSourceURL.value = ""; - elWebDPI.value = ""; - - // Material Properties - elMaterialURL.value = ""; - elParentMaterialNameNumber.value = ""; - elParentMaterialNameCheckbox.checked = false; - elPriority.value = ""; - elMaterialMappingPosX.value = ""; - elMaterialMappingPosY.value = ""; - elMaterialMappingScaleX.value = ""; - elMaterialMappingScaleY.value = ""; - elMaterialMappingRot.value = ""; - + deleteJSONMaterialEditor(); - elMaterialData.value = ""; + getPropertyInputElement("materialData").value = ""; showMaterialDataTextArea(); showSaveMaterialDataButton(); showNewJSONMaterialEditorButton(); - + disableProperties(); } else if (data.selections.length > 1) { deleteJSONEditor(); deleteJSONMaterialEditor(); - var selections = data.selections; + + let selections = data.selections; - var ids = []; - var types = {}; - var numTypes = 0; + let ids = []; + let types = {}; + let numTypes = 0; - for (var i = 0; i < selections.length; i++) { + for (let i = 0; i < selections.length; ++i) { ids.push(selections[i].id); - var currentSelectedType = selections[i].properties.type; + let currentSelectedType = selections[i].properties.type; if (types[currentSelectedType] === undefined) { types[currentSelectedType] = 0; numTypes += 1; @@ -1136,1183 +2971,508 @@ function loaded() { types[currentSelectedType]++; } - var type = "Multiple"; + let type = "Multiple"; if (numTypes === 1) { type = selections[0].properties.type; } - - elType.innerHTML = type + " (" + data.selections.length + ")"; - elTypeIcon.innerHTML = ICON_FOR_TYPE[type]; - elTypeIcon.style.display = "inline-block"; - elPropertiesList.className = ''; - - elID.value = ""; - + + resetProperties(); + showGroupsForType(type); + + let typeProperty = properties["type"]; + typeProperty.elSpan.innerHTML = typeProperty.data.icons[type]; + typeProperty.elSpan.style.display = "inline-block"; + disableProperties(); } else { - - properties = data.selections[0].properties; - if (lastEntityID !== '"' + properties.id + '"' && lastEntityID !== null) { + selectedEntityProperties = data.selections[0].properties; + + if (lastEntityID !== '"' + selectedEntityProperties.id + '"' && lastEntityID !== null) { if (editor !== null) { - saveJSONUserData(true); + saveUserData(); } if (materialEditor !== null) { - saveJSONMaterialData(true); + saveMaterialData(); } } - var doSelectElement = lastEntityID === '"' + properties.id + '"'; + let doSelectElement = lastEntityID === '"' + selectedEntityProperties.id + '"'; // the event bridge and json parsing handle our avatar id string differently. - lastEntityID = '"' + properties.id + '"'; - elID.value = properties.id; + lastEntityID = '"' + selectedEntityProperties.id + '"'; // HTML workaround since image is not yet a separate entity type - var IMAGE_MODEL_NAME = 'default-image-model.fbx'; - if (properties.type === "Model") { - var urlParts = properties.modelURL.split('/'); - var propsFilename = urlParts[urlParts.length - 1]; + let IMAGE_MODEL_NAME = 'default-image-model.fbx'; + if (selectedEntityProperties.type === "Model") { + let urlParts = selectedEntityProperties.modelURL.split('/'); + let propsFilename = urlParts[urlParts.length - 1]; if (propsFilename === IMAGE_MODEL_NAME) { - properties.type = "Image"; + selectedEntityProperties.type = "Image"; } } - // Create class name for css ruleset filtering - elPropertiesList.className = properties.type + 'Menu'; - - elType.innerHTML = properties.type; - elTypeIcon.innerHTML = ICON_FOR_TYPE[properties.type]; - elTypeIcon.style.display = "inline-block"; - - elLocked.checked = properties.locked; - - elName.value = properties.name; - - elVisible.checked = properties.visible; - - elPositionX.value = properties.position.x.toFixed(4); - elPositionY.value = properties.position.y.toFixed(4); - elPositionZ.value = properties.position.z.toFixed(4); - - elDimensionsX.value = properties.dimensions.x.toFixed(4); - elDimensionsY.value = properties.dimensions.y.toFixed(4); - elDimensionsZ.value = properties.dimensions.z.toFixed(4); - - elParentID.value = properties.parentID; - elParentJointIndex.value = properties.parentJointIndex; - - elRegistrationX.value = properties.registrationPoint.x.toFixed(4); - elRegistrationY.value = properties.registrationPoint.y.toFixed(4); - elRegistrationZ.value = properties.registrationPoint.z.toFixed(4); - - elRotationX.value = properties.rotation.x.toFixed(4); - elRotationY.value = properties.rotation.y.toFixed(4); - elRotationZ.value = properties.rotation.z.toFixed(4); - - elLinearVelocityX.value = properties.velocity.x.toFixed(4); - elLinearVelocityY.value = properties.velocity.y.toFixed(4); - elLinearVelocityZ.value = properties.velocity.z.toFixed(4); - elLinearDamping.value = properties.damping.toFixed(2); - - elAngularVelocityX.value = (properties.angularVelocity.x * RADIANS_TO_DEGREES).toFixed(4); - elAngularVelocityY.value = (properties.angularVelocity.y * RADIANS_TO_DEGREES).toFixed(4); - elAngularVelocityZ.value = (properties.angularVelocity.z * RADIANS_TO_DEGREES).toFixed(4); - elAngularDamping.value = properties.angularDamping.toFixed(4); - - elRestitution.value = properties.restitution.toFixed(4); - elFriction.value = properties.friction.toFixed(4); - - elGravityX.value = properties.gravity.x.toFixed(4); - elGravityY.value = properties.gravity.y.toFixed(4); - elGravityZ.value = properties.gravity.z.toFixed(4); - - elAccelerationX.value = properties.acceleration.x.toFixed(4); - elAccelerationY.value = properties.acceleration.y.toFixed(4); - elAccelerationZ.value = properties.acceleration.z.toFixed(4); - - elDensity.value = properties.density.toFixed(4); - elCollisionless.checked = properties.collisionless; - elDynamic.checked = properties.dynamic; - - elCollideStatic.checked = properties.collidesWith.indexOf("static") > -1; - elCollideKinematic.checked = properties.collidesWith.indexOf("kinematic") > -1; - elCollideDynamic.checked = properties.collidesWith.indexOf("dynamic") > -1; - elCollideMyAvatar.checked = properties.collidesWith.indexOf("myAvatar") > -1; - elCollideOtherAvatar.checked = properties.collidesWith.indexOf("otherAvatar") > -1; - - elGrabbable.checked = properties.dynamic; - - elTriggerable.checked = false; - elIgnoreIK.checked = true; - - elCloneable.checked = properties.cloneable; - elCloneableDynamic.checked = properties.cloneDynamic; - elCloneableAvatarEntity.checked = properties.cloneAvatarEntity; - elCloneableGroup.style.display = elCloneable.checked ? "block": "none"; - elCloneableLimit.value = properties.cloneLimit; - elCloneableLifetime.value = properties.cloneLifetime; + showGroupsForType(selectedEntityProperties.type); - var grabbablesSet = false; - var parsedUserData = {}; - try { - parsedUserData = JSON.parse(properties.userData); - - if ("grabbableKey" in parsedUserData) { - grabbablesSet = true; - var grabbableData = parsedUserData.grabbableKey; - if ("grabbable" in grabbableData) { - elGrabbable.checked = grabbableData.grabbable; - } else { - elGrabbable.checked = true; + for (let propertyID in properties) { + let property = properties[propertyID]; + let propertyData = property.data; + let propertyName = property.name; + let propertyValue = getPropertyValue(propertyName); + + let isSubProperty = propertyData.subPropertyOf !== undefined; + if (propertyValue === undefined && !isSubProperty) { + continue; + } + + let isPropertyNotNumber = false; + switch (propertyData.type) { + case 'number': + isPropertyNotNumber = isNaN(propertyValue) || propertyValue === null; + break; + case 'vec3': + case 'vec2': + isPropertyNotNumber = isNaN(propertyValue.x) || propertyValue.x === null; + break; + case 'color': + isPropertyNotNumber = isNaN(propertyValue.red) || propertyValue.red === null; + break; + } + if (isPropertyNotNumber && propertyData.fallbackProperty !== undefined) { + propertyValue = getPropertyValue(propertyData.fallbackProperty); + } + + switch (propertyData.type) { + case 'string': { + property.elInput.value = propertyValue; + break; } - if ("triggerable" in grabbableData) { - elTriggerable.checked = grabbableData.triggerable; - } else if ("wantsTrigger" in grabbableData) { - elTriggerable.checked = grabbableData.wantsTrigger; - } else { - elTriggerable.checked = false; + case 'bool': { + let inverse = propertyData.inverse !== undefined ? propertyData.inverse : false; + if (isSubProperty) { + let propertyValue = selectedEntityProperties[propertyData.subPropertyOf]; + let subProperties = propertyValue.split(","); + let subPropertyValue = subProperties.indexOf(propertyName) > -1; + property.elInput.checked = inverse ? !subPropertyValue : subPropertyValue; + } else { + property.elInput.checked = inverse ? !propertyValue : propertyValue; + } + break; } - if ("ignoreIK" in grabbableData) { - elIgnoreIK.checked = grabbableData.ignoreIK; - } else { - elIgnoreIK.checked = true; + case 'number': { + let multiplier = propertyData.multiplier !== undefined ? propertyData.multiplier : 1; + let value = propertyValue / multiplier; + if (propertyData.round !== undefined) { + value = Math.round(value.round) / propertyData.round; + } + property.elNumber.setValue(value); + break; + } + case 'vec3': + case 'vec2': { + let multiplier = propertyData.multiplier !== undefined ? propertyData.multiplier : 1; + let valueX = propertyValue.x / multiplier; + let valueY = propertyValue.y / multiplier; + let valueZ = propertyValue.z / multiplier; + if (propertyData.round !== undefined) { + valueX = Math.round(valueX * propertyData.round) / propertyData.round; + valueY = Math.round(valueY * propertyData.round) / propertyData.round; + valueZ = Math.round(valueZ * propertyData.round) / propertyData.round; + } + if (propertyData.decimals !== undefined) { + property.elNumberX.setValue(valueX.toFixed(propertyData.decimals)); + property.elNumberY.setValue(valueY.toFixed(propertyData.decimals)); + if (property.elNumberZ !== undefined) { + property.elNumberZ.setValue(valueZ.toFixed(propertyData.decimals)); + } + } else { + property.elNumberX.setValue(valueX); + property.elNumberY.setValue(valueY); + if (property.elNumberZ !== undefined) { + property.elNumberZ.setValue(valueZ); + } + } + break; + } + case 'color': { + property.elColorPicker.style.backgroundColor = "rgb(" + propertyValue.red + "," + + propertyValue.green + "," + + propertyValue.blue + ")"; + property.elNumberR.setValue(propertyValue.red); + property.elNumberG.setValue(propertyValue.green); + property.elNumberB.setValue(propertyValue.blue); + break; + } + case 'dropdown': { + property.elInput.value = propertyValue; + setDropdownText(property.elInput); + break; + } + case 'textarea': { + property.elInput.value = propertyValue; + setTextareaScrolling(property.elInput); + break; + } + case 'icon': { + property.elSpan.innerHTML = propertyData.icons[propertyValue]; + property.elSpan.style.display = "inline-block"; + break; + } + case 'texture': { + property.elInput.value = propertyValue; + property.elInput.imageLoad(property.elInput.value); + break; + } + } + + let showPropertyRules = property.showPropertyRules; + if (showPropertyRules !== undefined) { + for (let propertyToShow in showPropertyRules) { + let showIfThisPropertyValue = showPropertyRules[propertyToShow]; + let show = String(propertyValue) === String(showIfThisPropertyValue); + showPropertyElement(propertyToShow, show); } } - } catch (e) { - // TODO: What should go here? - } - if (!grabbablesSet) { - elGrabbable.checked = true; - elTriggerable.checked = false; - elIgnoreIK.checked = true; - elCloneable.checked = false; } - elCollisionSoundURL.value = properties.collisionSoundURL; - elLifetime.value = properties.lifetime; - elScriptURL.value = properties.script; - elScriptTimestamp.value = properties.scriptTimestamp; - elServerScripts.value = properties.serverScripts; + updateVisibleSpaceModeProperties(); - var json = null; + if (selectedEntityProperties.type === "Image") { + let imageLink = JSON.parse(selectedEntityProperties.textures)["tex.picture"]; + getPropertyInputElement("image").value = imageLink; + } else if (selectedEntityProperties.type === "Material") { + let elParentMaterialNameString = getPropertyInputElement("materialNameToReplace"); + let elParentMaterialNameNumber = getPropertyInputElement("submeshToReplace"); + let elParentMaterialNameCheckbox = getPropertyInputElement("selectSubmesh"); + let parentMaterialName = selectedEntityProperties.parentMaterialName; + if (parentMaterialName.startsWith(MATERIAL_PREFIX_STRING)) { + elParentMaterialNameString.value = parentMaterialName.replace(MATERIAL_PREFIX_STRING, ""); + showParentMaterialNameBox(false, elParentMaterialNameNumber, elParentMaterialNameString); + elParentMaterialNameCheckbox.checked = false; + } else { + elParentMaterialNameNumber.value = parseInt(parentMaterialName); + showParentMaterialNameBox(true, elParentMaterialNameNumber, elParentMaterialNameString); + elParentMaterialNameCheckbox.checked = true; + } + } + + let json = null; try { - json = JSON.parse(properties.userData); + json = JSON.parse(selectedEntityProperties.userData); } catch (e) { // normal text deleteJSONEditor(); - elUserData.value = properties.userData; + getPropertyInputElement("userData").value = selectedEntityProperties.userData; showUserDataTextArea(); showNewJSONEditorButton(); hideSaveUserDataButton(); + hideUserDataSaved(); } if (json !== null) { if (editor === null) { createJSONEditor(); } - setEditorJSON(json); showSaveUserDataButton(); hideUserDataTextArea(); hideNewJSONEditorButton(); + hideUserDataSaved(); } - var materialJson = null; + let materialJson = null; try { - materialJson = JSON.parse(properties.materialData); + materialJson = JSON.parse(selectedEntityProperties.materialData); } catch (e) { // normal text deleteJSONMaterialEditor(); - elMaterialData.value = properties.materialData; + getPropertyInputElement("materialData").value = selectedEntityProperties.materialData; showMaterialDataTextArea(); showNewJSONMaterialEditorButton(); hideSaveMaterialDataButton(); + hideMaterialDataSaved(); } if (materialJson !== null) { if (materialEditor === null) { createJSONMaterialEditor(); } - setMaterialEditorJSON(materialJson); showSaveMaterialDataButton(); hideMaterialDataTextArea(); hideNewJSONMaterialEditorButton(); + hideMaterialDataSaved(); } - - elHyperlinkHref.value = properties.href; - elDescription.value = properties.description; - - - if (properties.type === "Shape" || properties.type === "Box" || properties.type === "Sphere") { - elShape.value = properties.shape; - setDropdownText(elShape); - } - - if (properties.type === "Shape" || properties.type === "Box" || - properties.type === "Sphere" || properties.type === "ParticleEffect") { - elColorRed.value = properties.color.red; - elColorGreen.value = properties.color.green; - elColorBlue.value = properties.color.blue; - elColorControlVariant2.style.backgroundColor = "rgb(" + properties.color.red + "," + - properties.color.green + "," + properties.color.blue + ")"; - } - - if (properties.type === "Model" || - properties.type === "Shape" || properties.type === "Box" || properties.type === "Sphere") { - - elCanCastShadow.checked = properties.canCastShadow; - } - - if (properties.type === "Model") { - elModelURL.value = properties.modelURL; - elShapeType.value = properties.shapeType; - setDropdownText(elShapeType); - elCompoundShapeURL.value = properties.compoundShapeURL; - elModelAnimationURL.value = properties.animation.url; - elModelAnimationPlaying.checked = properties.animation.running; - elModelAnimationFPS.value = properties.animation.fps; - elModelAnimationFrame.value = properties.animation.currentFrame; - elModelAnimationFirstFrame.value = properties.animation.firstFrame; - elModelAnimationLastFrame.value = properties.animation.lastFrame; - elModelAnimationLoop.checked = properties.animation.loop; - elModelAnimationHold.checked = properties.animation.hold; - elModelAnimationAllowTranslation.checked = properties.animation.allowTranslation; - elModelTextures.value = properties.textures; - setTextareaScrolling(elModelTextures); - elModelOriginalTextures.value = properties.originalTextures; - setTextareaScrolling(elModelOriginalTextures); - } else if (properties.type === "Web") { - elWebSourceURL.value = properties.sourceUrl; - elWebDPI.value = properties.dpi; - } else if (properties.type === "Image") { - var imageLink = JSON.parse(properties.textures)["tex.picture"]; - elImageURL.value = imageLink; - } else if (properties.type === "Text") { - elTextText.value = properties.text; - elTextLineHeight.value = properties.lineHeight.toFixed(4); - elTextFaceCamera.checked = properties.faceCamera; - elTextTextColor.style.backgroundColor = "rgb(" + properties.textColor.red + "," + - properties.textColor.green + "," + - properties.textColor.blue + ")"; - elTextTextColorRed.value = properties.textColor.red; - elTextTextColorGreen.value = properties.textColor.green; - elTextTextColorBlue.value = properties.textColor.blue; - elTextBackgroundColor.style.backgroundColor = "rgb(" + properties.backgroundColor.red + "," + - properties.backgroundColor.green + "," + - properties.backgroundColor.blue + ")"; - elTextBackgroundColorRed.value = properties.backgroundColor.red; - elTextBackgroundColorGreen.value = properties.backgroundColor.green; - elTextBackgroundColorBlue.value = properties.backgroundColor.blue; - } else if (properties.type === "Light") { - elLightSpotLight.checked = properties.isSpotlight; - - elLightColor.style.backgroundColor = "rgb(" + properties.color.red + "," + - properties.color.green + "," + properties.color.blue + ")"; - elLightColorRed.value = properties.color.red; - elLightColorGreen.value = properties.color.green; - elLightColorBlue.value = properties.color.blue; - - elLightIntensity.value = properties.intensity.toFixed(1); - elLightFalloffRadius.value = properties.falloffRadius.toFixed(1); - elLightExponent.value = properties.exponent.toFixed(2); - elLightCutoff.value = properties.cutoff.toFixed(2); - } else if (properties.type === "Zone") { - // Key light - elZoneKeyLightModeInherit.checked = (properties.keyLightMode === 'inherit'); - elZoneKeyLightModeDisabled.checked = (properties.keyLightMode === 'disabled'); - elZoneKeyLightModeEnabled.checked = (properties.keyLightMode === 'enabled'); - - elZoneKeyLightColor.style.backgroundColor = "rgb(" + properties.keyLight.color.red + "," + - properties.keyLight.color.green + "," + properties.keyLight.color.blue + ")"; - elZoneKeyLightColorRed.value = properties.keyLight.color.red; - elZoneKeyLightColorGreen.value = properties.keyLight.color.green; - elZoneKeyLightColorBlue.value = properties.keyLight.color.blue; - elZoneKeyLightIntensity.value = properties.keyLight.intensity.toFixed(2); - elZoneKeyLightDirectionX.value = properties.keyLight.direction.x.toFixed(2); - elZoneKeyLightDirectionY.value = properties.keyLight.direction.y.toFixed(2); - - elZoneKeyLightCastShadows.checked = properties.keyLight.castShadows; - - // Skybox - elZoneSkyboxModeInherit.checked = (properties.skyboxMode === 'inherit'); - elZoneSkyboxModeDisabled.checked = (properties.skyboxMode === 'disabled'); - elZoneSkyboxModeEnabled.checked = (properties.skyboxMode === 'enabled'); - - // Ambient light - elZoneAmbientLightModeInherit.checked = (properties.ambientLightMode === 'inherit'); - elZoneAmbientLightModeDisabled.checked = (properties.ambientLightMode === 'disabled'); - elZoneAmbientLightModeEnabled.checked = (properties.ambientLightMode === 'enabled'); - - elZoneAmbientLightIntensity.value = properties.ambientLight.ambientIntensity.toFixed(2); - elZoneAmbientLightURL.value = properties.ambientLight.ambientURL; - - // Haze - elZoneHazeModeInherit.checked = (properties.hazeMode === 'inherit'); - elZoneHazeModeDisabled.checked = (properties.hazeMode === 'disabled'); - elZoneHazeModeEnabled.checked = (properties.hazeMode === 'enabled'); - - elZoneHazeRange.value = properties.haze.hazeRange.toFixed(0); - elZoneHazeColor.style.backgroundColor = "rgb(" + - properties.haze.hazeColor.red + "," + - properties.haze.hazeColor.green + "," + - properties.haze.hazeColor.blue + ")"; - - elZoneHazeColorRed.value = properties.haze.hazeColor.red; - elZoneHazeColorGreen.value = properties.haze.hazeColor.green; - elZoneHazeColorBlue.value = properties.haze.hazeColor.blue; - elZoneHazeBackgroundBlend.value = properties.haze.hazeBackgroundBlend.toFixed(2); - - elZoneHazeGlareColor.style.backgroundColor = "rgb(" + - properties.haze.hazeGlareColor.red + "," + - properties.haze.hazeGlareColor.green + "," + - properties.haze.hazeGlareColor.blue + ")"; - - elZoneHazeGlareColorRed.value = properties.haze.hazeGlareColor.red; - elZoneHazeGlareColorGreen.value = properties.haze.hazeGlareColor.green; - elZoneHazeGlareColorBlue.value = properties.haze.hazeGlareColor.blue; - - elZoneHazeEnableGlare.checked = properties.haze.hazeEnableGlare; - elZoneHazeGlareAngle.value = properties.haze.hazeGlareAngle.toFixed(0); - - elZoneHazeAltitudeEffect.checked = properties.haze.hazeAltitudeEffect; - elZoneHazeBaseRef.value = properties.haze.hazeBaseRef.toFixed(0); - elZoneHazeCeiling.value = properties.haze.hazeCeiling.toFixed(0); - - elShapeType.value = properties.shapeType; - elCompoundShapeURL.value = properties.compoundShapeURL; - - elZoneSkyboxColor.style.backgroundColor = "rgb(" + properties.skybox.color.red + "," + - properties.skybox.color.green + "," + properties.skybox.color.blue + ")"; - elZoneSkyboxColorRed.value = properties.skybox.color.red; - elZoneSkyboxColorGreen.value = properties.skybox.color.green; - elZoneSkyboxColorBlue.value = properties.skybox.color.blue; - elZoneSkyboxURL.value = properties.skybox.url; - - elZoneFlyingAllowed.checked = properties.flyingAllowed; - elZoneGhostingAllowed.checked = properties.ghostingAllowed; - elZoneFilterURL.value = properties.filterURL; - - // Show/hide sections as required - showElements(document.getElementsByClassName('skybox-section'), - elZoneSkyboxModeEnabled.checked); - - showElements(document.getElementsByClassName('keylight-section'), - elZoneKeyLightModeEnabled.checked); - - showElements(document.getElementsByClassName('ambient-section'), - elZoneAmbientLightModeEnabled.checked); - - showElements(document.getElementsByClassName('haze-section'), - elZoneHazeModeEnabled.checked); - } else if (properties.type === "PolyVox") { - elVoxelVolumeSizeX.value = properties.voxelVolumeSize.x.toFixed(2); - elVoxelVolumeSizeY.value = properties.voxelVolumeSize.y.toFixed(2); - elVoxelVolumeSizeZ.value = properties.voxelVolumeSize.z.toFixed(2); - elVoxelSurfaceStyle.value = properties.voxelSurfaceStyle; - setDropdownText(elVoxelSurfaceStyle); - elXTextureURL.value = properties.xTextureURL; - elYTextureURL.value = properties.yTextureURL; - elZTextureURL.value = properties.zTextureURL; - } else if (properties.type === "Material") { - elMaterialURL.value = properties.materialURL; - //elMaterialMappingMode.value = properties.materialMappingMode; - //setDropdownText(elMaterialMappingMode); - elPriority.value = properties.priority; - if (properties.parentMaterialName.startsWith(MATERIAL_PREFIX_STRING)) { - elParentMaterialNameString.value = properties.parentMaterialName.replace(MATERIAL_PREFIX_STRING, ""); - showParentMaterialNameBox(false, elParentMaterialNameNumber, elParentMaterialNameString); - elParentMaterialNameCheckbox.checked = false; - } else { - elParentMaterialNameNumber.value = parseInt(properties.parentMaterialName); - showParentMaterialNameBox(true, elParentMaterialNameNumber, elParentMaterialNameString); - elParentMaterialNameCheckbox.checked = true; - } - elMaterialMappingPosX.value = properties.materialMappingPos.x.toFixed(4); - elMaterialMappingPosY.value = properties.materialMappingPos.y.toFixed(4); - elMaterialMappingScaleX.value = properties.materialMappingScale.x.toFixed(4); - elMaterialMappingScaleY.value = properties.materialMappingScale.y.toFixed(4); - elMaterialMappingRot.value = properties.materialMappingRot.toFixed(2); - } - - // Only these types can cast a shadow - if (properties.type === "Model" || - properties.type === "Shape" || properties.type === "Box" || properties.type === "Sphere") { - - showElements(document.getElementsByClassName('can-cast-shadow-section'), true); - } else { - showElements(document.getElementsByClassName('can-cast-shadow-section'), false); - } - - if (properties.locked) { + + if (selectedEntityProperties.locked) { disableProperties(); - elLocked.removeAttribute('disabled'); + getPropertyInputElement("locked").removeAttribute('disabled'); } else { enableProperties(); - elSaveUserData.disabled = true; - elSaveMaterialData.disabled = true; + disableSaveUserDataButton(); + disableSaveMaterialDataButton() } - - var activeElement = document.activeElement; + + let activeElement = document.activeElement; if (doSelectElement && typeof activeElement.select !== "undefined") { activeElement.select(); } } + } else if (data.type === 'tooltipsReply') { + createAppTooltip.setIsEnabled(!data.hmdActive); + createAppTooltip.setTooltipData(data.tooltips); + } else if (data.type === 'hmdActiveChanged') { + createAppTooltip.setIsEnabled(!data.hmdActive); + } else if (data.type === 'setSpaceMode') { + currentSpaceMode = data.spaceMode === "local" ? PROPERTY_SPACE_MODE.LOCAL : PROPERTY_SPACE_MODE.WORLD; + updateVisibleSpaceModeProperties(); } }); + + // Request tooltips as soon as we can process a reply: + EventBridge.emitWebEvent(JSON.stringify({ type: 'tooltipsRequest' })); } - - elLocked.addEventListener('change', createEmitCheckedPropertyUpdateFunction('locked')); - elName.addEventListener('change', createEmitTextPropertyUpdateFunction('name')); - elHyperlinkHref.addEventListener('change', createEmitTextPropertyUpdateFunction('href')); - elDescription.addEventListener('change', createEmitTextPropertyUpdateFunction('description')); - elVisible.addEventListener('change', createEmitCheckedPropertyUpdateFunction('visible')); - - var positionChangeFunction = createEmitVec3PropertyUpdateFunction( - 'position', elPositionX, elPositionY, elPositionZ); - elPositionX.addEventListener('change', positionChangeFunction); - elPositionY.addEventListener('change', positionChangeFunction); - elPositionZ.addEventListener('change', positionChangeFunction); - - var dimensionsChangeFunction = createEmitVec3PropertyUpdateFunction( - 'dimensions', elDimensionsX, elDimensionsY, elDimensionsZ); - elDimensionsX.addEventListener('change', dimensionsChangeFunction); - elDimensionsY.addEventListener('change', dimensionsChangeFunction); - elDimensionsZ.addEventListener('change', dimensionsChangeFunction); - - elParentID.addEventListener('change', createEmitTextPropertyUpdateFunction('parentID')); - elParentJointIndex.addEventListener('change', createEmitNumberPropertyUpdateFunction('parentJointIndex', 0)); - - var registrationChangeFunction = createEmitVec3PropertyUpdateFunction( - 'registrationPoint', elRegistrationX, elRegistrationY, elRegistrationZ); - elRegistrationX.addEventListener('change', registrationChangeFunction); - elRegistrationY.addEventListener('change', registrationChangeFunction); - elRegistrationZ.addEventListener('change', registrationChangeFunction); - - var rotationChangeFunction = createEmitVec3PropertyUpdateFunction( - 'rotation', elRotationX, elRotationY, elRotationZ); - elRotationX.addEventListener('change', rotationChangeFunction); - elRotationY.addEventListener('change', rotationChangeFunction); - elRotationZ.addEventListener('change', rotationChangeFunction); - - var velocityChangeFunction = createEmitVec3PropertyUpdateFunction( - 'velocity', elLinearVelocityX, elLinearVelocityY, elLinearVelocityZ); - elLinearVelocityX.addEventListener('change', velocityChangeFunction); - elLinearVelocityY.addEventListener('change', velocityChangeFunction); - elLinearVelocityZ.addEventListener('change', velocityChangeFunction); - elLinearDamping.addEventListener('change', createEmitNumberPropertyUpdateFunction('damping')); - - var angularVelocityChangeFunction = createEmitVec3PropertyUpdateFunctionWithMultiplier( - 'angularVelocity', elAngularVelocityX, elAngularVelocityY, elAngularVelocityZ, DEGREES_TO_RADIANS); - elAngularVelocityX.addEventListener('change', angularVelocityChangeFunction); - elAngularVelocityY.addEventListener('change', angularVelocityChangeFunction); - elAngularVelocityZ.addEventListener('change', angularVelocityChangeFunction); - elAngularDamping.addEventListener('change', createEmitNumberPropertyUpdateFunction('angularDamping')); - - elRestitution.addEventListener('change', createEmitNumberPropertyUpdateFunction('restitution')); - elFriction.addEventListener('change', createEmitNumberPropertyUpdateFunction('friction')); - - var gravityChangeFunction = createEmitVec3PropertyUpdateFunction( - 'gravity', elGravityX, elGravityY, elGravityZ); - elGravityX.addEventListener('change', gravityChangeFunction); - elGravityY.addEventListener('change', gravityChangeFunction); - elGravityZ.addEventListener('change', gravityChangeFunction); - - var accelerationChangeFunction = createEmitVec3PropertyUpdateFunction( - 'acceleration', elAccelerationX, elAccelerationY, elAccelerationZ); - elAccelerationX.addEventListener('change', accelerationChangeFunction); - elAccelerationY.addEventListener('change', accelerationChangeFunction); - elAccelerationZ.addEventListener('change', accelerationChangeFunction); - - elDensity.addEventListener('change', createEmitNumberPropertyUpdateFunction('density')); - elCollisionless.addEventListener('change', createEmitCheckedPropertyUpdateFunction('collisionless')); - elDynamic.addEventListener('change', createEmitCheckedPropertyUpdateFunction('dynamic')); - - elCollideDynamic.addEventListener('change', function() { - updateCheckedSubProperty("collidesWith", properties.collidesWith, elCollideDynamic, 'dynamic'); - }); - - elCollideKinematic.addEventListener('change', function() { - updateCheckedSubProperty("collidesWith", properties.collidesWith, elCollideKinematic, 'kinematic'); - }); - - elCollideStatic.addEventListener('change', function() { - updateCheckedSubProperty("collidesWith", properties.collidesWith, elCollideStatic, 'static'); - }); - elCollideMyAvatar.addEventListener('change', function() { - updateCheckedSubProperty("collidesWith", properties.collidesWith, elCollideMyAvatar, 'myAvatar'); - }); - elCollideOtherAvatar.addEventListener('change', function() { - updateCheckedSubProperty("collidesWith", properties.collidesWith, elCollideOtherAvatar, 'otherAvatar'); - }); - - elGrabbable.addEventListener('change', function() { - if (elCloneable.checked) { - elGrabbable.checked = false; - } - userDataChanger("grabbableKey", "grabbable", elGrabbable, elUserData, true); - }); - elCloneable.addEventListener('change', createEmitCheckedPropertyUpdateFunction('cloneable')); - elCloneableDynamic.addEventListener('change', createEmitCheckedPropertyUpdateFunction('cloneDynamic')); - elCloneableAvatarEntity.addEventListener('change', createEmitCheckedPropertyUpdateFunction('cloneAvatarEntity')); - elCloneableLifetime.addEventListener('change', createEmitNumberPropertyUpdateFunction('cloneLifetime')); - elCloneableLimit.addEventListener('change', createEmitNumberPropertyUpdateFunction('cloneLimit')); - - elTriggerable.addEventListener('change', function() { - userDataChanger("grabbableKey", "triggerable", elTriggerable, elUserData, false, ['wantsTrigger']); + // Server Script Status + let elServerScriptStatusOuter = document.getElementById('div-property-serverScriptStatus'); + let elServerScriptStatusContainer = document.getElementById('div-property-serverScriptStatus').childNodes[1]; + let serverScriptStatusElementID = 'property-serverScripts-status'; + createAppTooltip.registerTooltipElement(elServerScriptStatusOuter.childNodes[0], "serverScriptsStatus"); + let elServerScriptStatus = document.createElement('div'); + elServerScriptStatus.setAttribute("id", serverScriptStatusElementID); + elServerScriptStatusContainer.appendChild(elServerScriptStatus); + + // Server Script Error + let elServerScripts = getPropertyInputElement("serverScripts"); + elDiv = document.createElement('div'); + elDiv.className = "property"; + let elServerScriptError = document.createElement('textarea'); + let serverScriptErrorElementID = 'property-serverScripts-error'; + elServerScriptError.setAttribute("id", serverScriptErrorElementID); + elDiv.appendChild(elServerScriptError); + elServerScriptStatusContainer.appendChild(elDiv); + + let elScript = getPropertyInputElement("script"); + elScript.parentNode.className = "url refresh"; + elServerScripts.parentNode.className = "url refresh"; + + // User Data + let userDataProperty = properties["userData"]; + let elUserData = userDataProperty.elInput; + let userDataElementID = userDataProperty.elementID; + elDiv = elUserData.parentNode; + let elStaticUserData = document.createElement('div'); + elStaticUserData.setAttribute("id", userDataElementID + "-static"); + let elUserDataEditor = document.createElement('div'); + elUserDataEditor.setAttribute("id", userDataElementID + "-editor"); + let elUserDataSaved = document.createElement('span'); + elUserDataSaved.setAttribute("id", userDataElementID + "-saved"); + elUserDataSaved.innerText = "Saved!"; + elDiv.childNodes[JSON_EDITOR_ROW_DIV_INDEX].appendChild(elUserDataSaved); + elDiv.insertBefore(elStaticUserData, elUserData); + elDiv.insertBefore(elUserDataEditor, elUserData); + + // Material Data + let materialDataProperty = properties["materialData"]; + let elMaterialData = materialDataProperty.elInput; + let materialDataElementID = materialDataProperty.elementID; + elDiv = elMaterialData.parentNode; + let elStaticMaterialData = document.createElement('div'); + elStaticMaterialData.setAttribute("id", materialDataElementID + "-static"); + let elMaterialDataEditor = document.createElement('div'); + elMaterialDataEditor.setAttribute("id", materialDataElementID + "-editor"); + let elMaterialDataSaved = document.createElement('span'); + elMaterialDataSaved.setAttribute("id", materialDataElementID + "-saved"); + elMaterialDataSaved.innerText = "Saved!"; + elDiv.childNodes[JSON_EDITOR_ROW_DIV_INDEX].appendChild(elMaterialDataSaved); + elDiv.insertBefore(elStaticMaterialData, elMaterialData); + elDiv.insertBefore(elMaterialDataEditor, elMaterialData); + + // Special Property Callbacks + let elParentMaterialNameString = getPropertyInputElement("materialNameToReplace"); + let elParentMaterialNameNumber = getPropertyInputElement("submeshToReplace"); + let elParentMaterialNameCheckbox = getPropertyInputElement("selectSubmesh"); + elParentMaterialNameString.addEventListener('change', function () { + updateProperty("parentMaterialName", MATERIAL_PREFIX_STRING + this.value, false); }); - elIgnoreIK.addEventListener('change', function() { - userDataChanger("grabbableKey", "ignoreIK", elIgnoreIK, elUserData, true); + elParentMaterialNameNumber.addEventListener('change', function () { + updateProperty("parentMaterialName", this.value, false); }); - - elCollisionSoundURL.addEventListener('change', createEmitTextPropertyUpdateFunction('collisionSoundURL')); - - elLifetime.addEventListener('change', createEmitNumberPropertyUpdateFunction('lifetime')); - elScriptURL.addEventListener('change', createEmitTextPropertyUpdateFunction('script')); - elScriptTimestamp.addEventListener('change', createEmitNumberPropertyUpdateFunction('scriptTimestamp')); - elServerScripts.addEventListener('change', createEmitTextPropertyUpdateFunction('serverScripts')); - elServerScripts.addEventListener('change', function() { - // invalidate the current status (so that same-same updates can still be observed visually) - elServerScriptStatus.innerText = PENDING_SCRIPT_STATUS; - }); - - elClearUserData.addEventListener("click", function() { - deleteJSONEditor(); - elUserData.value = ""; - showUserDataTextArea(); - showNewJSONEditorButton(); - hideSaveUserDataButton(); - updateProperty('userData', elUserData.value); - }); - - elSaveUserData.addEventListener("click", function() { - saveJSONUserData(true); - }); - - elUserData.addEventListener('change', createEmitTextPropertyUpdateFunction('userData')); - - elNewJSONEditor.addEventListener('click', function() { - deleteJSONEditor(); - createJSONEditor(); - var data = {}; - setEditorJSON(data); - hideUserDataTextArea(); - hideNewJSONEditorButton(); - showSaveUserDataButton(); - }); - - elClearMaterialData.addEventListener("click", function() { - deleteJSONMaterialEditor(); - elMaterialData.value = ""; - showMaterialDataTextArea(); - showNewJSONMaterialEditorButton(); - hideSaveMaterialDataButton(); - updateProperty('materialData', elMaterialData.value); - }); - - elSaveMaterialData.addEventListener("click", function() { - saveJSONMaterialData(true); - }); - - elMaterialData.addEventListener('change', createEmitTextPropertyUpdateFunction('materialData')); - - elNewJSONMaterialEditor.addEventListener('click', function() { - deleteJSONMaterialEditor(); - createJSONMaterialEditor(); - var data = {}; - setMaterialEditorJSON(data); - hideMaterialDataTextArea(); - hideNewJSONMaterialEditorButton(); - showSaveMaterialDataButton(); - }); - - var colorChangeFunction = createEmitColorPropertyUpdateFunction( - 'color', elColorRed, elColorGreen, elColorBlue); - elColorRed.addEventListener('change', colorChangeFunction); - elColorGreen.addEventListener('change', colorChangeFunction); - elColorBlue.addEventListener('change', colorChangeFunction); - colorPickers['#property-color-control2'] = $('#property-color-control2').colpick({ - colorScheme: 'dark', - layout: 'hex', - color: '000000', - submit: false, // We don't want to have a submission button - onShow: function(colpick) { - $('#property-color-control2').attr('active', 'true'); - // The original color preview within the picker needs to be updated on show because - // prior to the picker being shown we don't have access to the selections' starting color. - colorPickers['#property-color-control2'].colpickSetColor({ - "r": elColorRed.value, - "g": elColorGreen.value, - "b": elColorBlue.value}); - }, - onHide: function(colpick) { - $('#property-color-control2').attr('active', 'false'); - }, - onChange: function(hsb, hex, rgb, el) { - $(el).css('background-color', '#' + hex); - emitColorPropertyUpdate('color', rgb.r, rgb.g, rgb.b); - } - }); - - elLightSpotLight.addEventListener('change', createEmitCheckedPropertyUpdateFunction('isSpotlight')); - - var lightColorChangeFunction = createEmitColorPropertyUpdateFunction( - 'color', elLightColorRed, elLightColorGreen, elLightColorBlue); - elLightColorRed.addEventListener('change', lightColorChangeFunction); - elLightColorGreen.addEventListener('change', lightColorChangeFunction); - elLightColorBlue.addEventListener('change', lightColorChangeFunction); - colorPickers['#property-light-color'] = $('#property-light-color').colpick({ - colorScheme: 'dark', - layout: 'hex', - color: '000000', - submit: false, // We don't want to have a submission button - onShow: function(colpick) { - $('#property-light-color').attr('active', 'true'); - // The original color preview within the picker needs to be updated on show because - // prior to the picker being shown we don't have access to the selections' starting color. - colorPickers['#property-light-color'].colpickSetColor({ - "r": elLightColorRed.value, - "g": elLightColorGreen.value, - "b": elLightColorBlue.value - }); - }, - onHide: function(colpick) { - $('#property-light-color').attr('active', 'false'); - }, - onChange: function(hsb, hex, rgb, el) { - $(el).css('background-color', '#' + hex); - emitColorPropertyUpdate('color', rgb.r, rgb.g, rgb.b); - } - }); - - elLightIntensity.addEventListener('change', createEmitNumberPropertyUpdateFunction('intensity', 1)); - elLightFalloffRadius.addEventListener('change', createEmitNumberPropertyUpdateFunction('falloffRadius', 1)); - elLightExponent.addEventListener('change', createEmitNumberPropertyUpdateFunction('exponent', 2)); - elLightCutoff.addEventListener('change', createEmitNumberPropertyUpdateFunction('cutoff', 2)); - - elShape.addEventListener('change', createEmitTextPropertyUpdateFunction('shape')); - - elCanCastShadow.addEventListener('change', createEmitCheckedPropertyUpdateFunction('canCastShadow')); - - elImageURL.addEventListener('change', createImageURLUpdateFunction('textures')); - - elWebSourceURL.addEventListener('change', createEmitTextPropertyUpdateFunction('sourceUrl')); - elWebDPI.addEventListener('change', createEmitNumberPropertyUpdateFunction('dpi', 0)); - - elModelURL.addEventListener('change', createEmitTextPropertyUpdateFunction('modelURL')); - elShapeType.addEventListener('change', createEmitTextPropertyUpdateFunction('shapeType')); - elCompoundShapeURL.addEventListener('change', createEmitTextPropertyUpdateFunction('compoundShapeURL')); - - elModelAnimationURL.addEventListener('change', createEmitGroupTextPropertyUpdateFunction('animation', 'url')); - elModelAnimationPlaying.addEventListener('change',createEmitGroupCheckedPropertyUpdateFunction('animation', 'running')); - elModelAnimationFPS.addEventListener('change', createEmitGroupNumberPropertyUpdateFunction('animation', 'fps')); - elModelAnimationFrame.addEventListener('change', - createEmitGroupNumberPropertyUpdateFunction('animation', 'currentFrame')); - elModelAnimationFirstFrame.addEventListener('change', - createEmitGroupNumberPropertyUpdateFunction('animation', 'firstFrame')); - elModelAnimationLastFrame.addEventListener('change', - createEmitGroupNumberPropertyUpdateFunction('animation', 'lastFrame')); - elModelAnimationLoop.addEventListener('change', createEmitGroupCheckedPropertyUpdateFunction('animation', 'loop')); - elModelAnimationHold.addEventListener('change', createEmitGroupCheckedPropertyUpdateFunction('animation', 'hold')); - elModelAnimationAllowTranslation.addEventListener('change', - createEmitGroupCheckedPropertyUpdateFunction('animation', 'allowTranslation')); - - elModelTextures.addEventListener('change', createEmitTextPropertyUpdateFunction('textures')); - - elMaterialURL.addEventListener('change', createEmitTextPropertyUpdateFunction('materialURL')); - //elMaterialMappingMode.addEventListener('change', createEmitTextPropertyUpdateFunction('materialMappingMode')); - elPriority.addEventListener('change', createEmitNumberPropertyUpdateFunction('priority', 0)); - - elParentMaterialNameString.addEventListener('change', function () { updateProperty("parentMaterialName", MATERIAL_PREFIX_STRING + this.value); }); - elParentMaterialNameNumber.addEventListener('change', function () { updateProperty("parentMaterialName", this.value); }); elParentMaterialNameCheckbox.addEventListener('change', function () { if (this.checked) { - updateProperty("parentMaterialName", elParentMaterialNameNumber.value); + updateProperty("parentMaterialName", elParentMaterialNameNumber.value, false); showParentMaterialNameBox(true, elParentMaterialNameNumber, elParentMaterialNameString); } else { - updateProperty("parentMaterialName", MATERIAL_PREFIX_STRING + elParentMaterialNameString.value); + updateProperty("parentMaterialName", MATERIAL_PREFIX_STRING + elParentMaterialNameString.value, false); showParentMaterialNameBox(false, elParentMaterialNameNumber, elParentMaterialNameString); } }); + + getPropertyInputElement("image").addEventListener('change', createImageURLUpdateFunction('textures', false)); + + // Collapsible sections + let elCollapsible = document.getElementsByClassName("section-header"); - var materialMappingPosChangeFunction = createEmitVec2PropertyUpdateFunction('materialMappingPos', elMaterialMappingPosX, elMaterialMappingPosY); - elMaterialMappingPosX.addEventListener('change', materialMappingPosChangeFunction); - elMaterialMappingPosY.addEventListener('change', materialMappingPosChangeFunction); - var materialMappingScaleChangeFunction = createEmitVec2PropertyUpdateFunction('materialMappingScale', elMaterialMappingScaleX, elMaterialMappingScaleY); - elMaterialMappingScaleX.addEventListener('change', materialMappingScaleChangeFunction); - elMaterialMappingScaleY.addEventListener('change', materialMappingScaleChangeFunction); - elMaterialMappingRot.addEventListener('change', createEmitNumberPropertyUpdateFunction('materialMappingRot', 2)); + let toggleCollapsedEvent = function(event) { + let element = this.parentNode; + let isCollapsed = element.dataset.collapsed !== "true"; + element.dataset.collapsed = isCollapsed ? "true" : false; + element.setAttribute("collapsed", isCollapsed ? "true" : "false"); + element.getElementsByClassName(".collapse-icon")[0].textContent = isCollapsed ? "L" : "M"; + }; - elTextText.addEventListener('change', createEmitTextPropertyUpdateFunction('text')); - elTextFaceCamera.addEventListener('change', createEmitCheckedPropertyUpdateFunction('faceCamera')); - elTextLineHeight.addEventListener('change', createEmitNumberPropertyUpdateFunction('lineHeight')); - var textTextColorChangeFunction = createEmitColorPropertyUpdateFunction( - 'textColor', elTextTextColorRed, elTextTextColorGreen, elTextTextColorBlue); - elTextTextColorRed.addEventListener('change', textTextColorChangeFunction); - elTextTextColorGreen.addEventListener('change', textTextColorChangeFunction); - elTextTextColorBlue.addEventListener('change', textTextColorChangeFunction); - colorPickers['#property-text-text-color'] = $('#property-text-text-color').colpick({ - colorScheme: 'dark', - layout: 'hex', - color: '000000', - submit: false, // We don't want to have a submission button - onShow: function(colpick) { - $('#property-text-text-color').attr('active', 'true'); - // The original color preview within the picker needs to be updated on show because - // prior to the picker being shown we don't have access to the selections' starting color. - colorPickers['#property-text-text-color'].colpickSetColor({ - "r": elTextTextColorRed.value, - "g": elTextTextColorGreen.value, - "b": elTextTextColorBlue.value - }); - }, - onHide: function(colpick) { - $('#property-text-text-color').attr('active', 'false'); - }, - onChange: function(hsb, hex, rgb, el) { - $(el).css('background-color', '#' + hex); - $(el).attr('active', 'false'); - emitColorPropertyUpdate('textColor', rgb.r, rgb.g, rgb.b); - } - }); + for (let collapseIndex = 0, numCollapsibles = elCollapsible.length; collapseIndex < numCollapsibles; ++collapseIndex) { + let curCollapsibleElement = elCollapsible[collapseIndex]; + curCollapsibleElement.addEventListener("click", toggleCollapsedEvent, true); + } + + // Textarea scrollbars + let elTextareas = document.getElementsByTagName("TEXTAREA"); - var textBackgroundColorChangeFunction = createEmitColorPropertyUpdateFunction( - 'backgroundColor', elTextBackgroundColorRed, elTextBackgroundColorGreen, elTextBackgroundColorBlue); + let textareaOnChangeEvent = function(event) { + setTextareaScrolling(event.target); + }; - elTextBackgroundColorRed.addEventListener('change', textBackgroundColorChangeFunction); - elTextBackgroundColorGreen.addEventListener('change', textBackgroundColorChangeFunction); - elTextBackgroundColorBlue.addEventListener('change', textBackgroundColorChangeFunction); - colorPickers['#property-text-background-color'] = $('#property-text-background-color').colpick({ - colorScheme: 'dark', - layout: 'hex', - color: '000000', - submit: false, // We don't want to have a submission button - onShow: function(colpick) { - $('#property-text-background-color').attr('active', 'true'); - // The original color preview within the picker needs to be updated on show because - // prior to the picker being shown we don't have access to the selections' starting color. - colorPickers['#property-text-background-color'].colpickSetColor({ - "r": elTextBackgroundColorRed.value, - "g": elTextBackgroundColorGreen.value, - "b": elTextBackgroundColorBlue.value - }); - }, - onHide: function(colpick) { - $('#property-text-background-color').attr('active', 'false'); - }, - onChange: function(hsb, hex, rgb, el) { - $(el).css('background-color', '#' + hex); - emitColorPropertyUpdate('backgroundColor', rgb.r, rgb.g, rgb.b); - } - }); - - // Key light - var keyLightModeChanged = createZoneComponentModeChangedFunction('keyLightMode', - elZoneKeyLightModeInherit, elZoneKeyLightModeDisabled, elZoneKeyLightModeEnabled); - - elZoneKeyLightModeInherit.addEventListener('change', keyLightModeChanged); - elZoneKeyLightModeDisabled.addEventListener('change', keyLightModeChanged); - elZoneKeyLightModeEnabled.addEventListener('change', keyLightModeChanged); - - colorPickers['#property-zone-key-light-color'] = $('#property-zone-key-light-color').colpick({ - colorScheme: 'dark', - layout: 'hex', - color: '000000', - submit: false, // We don't want to have a submission button - onShow: function(colpick) { - $('#property-zone-key-light-color').attr('active', 'true'); - // The original color preview within the picker needs to be updated on show because - // prior to the picker being shown we don't have access to the selections' starting color. - colorPickers['#property-zone-key-light-color'].colpickSetColor({ - "r": elZoneKeyLightColorRed.value, - "g": elZoneKeyLightColorGreen.value, - "b": elZoneKeyLightColorBlue.value - }); - }, - onHide: function(colpick) { - $('#property-zone-key-light-color').attr('active', 'false'); - }, - onChange: function(hsb, hex, rgb, el) { - $(el).css('background-color', '#' + hex); - emitColorPropertyUpdate('color', rgb.r, rgb.g, rgb.b, 'keyLight'); - } - }); - var zoneKeyLightColorChangeFunction = createEmitGroupColorPropertyUpdateFunction('keyLight', 'color', - elZoneKeyLightColorRed, elZoneKeyLightColorGreen, elZoneKeyLightColorBlue); - - elZoneKeyLightColorRed.addEventListener('change', zoneKeyLightColorChangeFunction); - elZoneKeyLightColorGreen.addEventListener('change', zoneKeyLightColorChangeFunction); - elZoneKeyLightColorBlue.addEventListener('change', zoneKeyLightColorChangeFunction); - elZoneKeyLightIntensity.addEventListener('change', - createEmitGroupNumberPropertyUpdateFunction('keyLight', 'intensity')); - - var zoneKeyLightDirectionChangeFunction = createEmitGroupVec3PropertyUpdateFunction('keyLight', 'direction', - elZoneKeyLightDirectionX, elZoneKeyLightDirectionY); - - elZoneKeyLightDirectionX.addEventListener('change', zoneKeyLightDirectionChangeFunction); - elZoneKeyLightDirectionY.addEventListener('change', zoneKeyLightDirectionChangeFunction); - - elZoneKeyLightCastShadows.addEventListener('change', - createEmitGroupCheckedPropertyUpdateFunction('keyLight', 'castShadows')); - - // Skybox - var skyboxModeChanged = createZoneComponentModeChangedFunction('skyboxMode', - elZoneSkyboxModeInherit, elZoneSkyboxModeDisabled, elZoneSkyboxModeEnabled); - - elZoneSkyboxModeInherit.addEventListener('change', skyboxModeChanged); - elZoneSkyboxModeDisabled.addEventListener('change', skyboxModeChanged); - elZoneSkyboxModeEnabled.addEventListener('change', skyboxModeChanged); - - // Ambient light - elCopySkyboxURLToAmbientURL.addEventListener("click", function () { - document.getElementById("property-zone-key-ambient-url").value = properties.skybox.url; - properties.ambientLight.ambientURL = properties.skybox.url; - updateProperties(properties); - }); - - var ambientLightModeChanged = createZoneComponentModeChangedFunction('ambientLightMode', - elZoneAmbientLightModeInherit, elZoneAmbientLightModeDisabled, elZoneAmbientLightModeEnabled); - - elZoneAmbientLightModeInherit.addEventListener('change', ambientLightModeChanged); - elZoneAmbientLightModeDisabled.addEventListener('change', ambientLightModeChanged); - elZoneAmbientLightModeEnabled.addEventListener('change', ambientLightModeChanged); - - elZoneAmbientLightIntensity.addEventListener('change', - createEmitGroupNumberPropertyUpdateFunction('ambientLight', 'ambientIntensity')); - - elZoneAmbientLightURL.addEventListener('change', - createEmitGroupTextPropertyUpdateFunction('ambientLight', 'ambientURL')); - - // Haze - var hazeModeChanged = createZoneComponentModeChangedFunction('hazeMode', - elZoneHazeModeInherit, elZoneHazeModeDisabled, elZoneHazeModeEnabled); - - elZoneHazeModeInherit.addEventListener('change', hazeModeChanged); - elZoneHazeModeDisabled.addEventListener('change', hazeModeChanged); - elZoneHazeModeEnabled.addEventListener('change', hazeModeChanged); - - elZoneHazeRange.addEventListener('change', createEmitGroupNumberPropertyUpdateFunction('haze', 'hazeRange')); - - colorPickers['#property-zone-haze-color'] = $('#property-zone-haze-color').colpick({ - colorScheme: 'dark', - layout: 'hex', - color: '000000', - submit: false, // We don't want to have a submission button - onShow: function(colpick) { - $('#property-zone-haze-color').attr('active', 'true'); - // The original color preview within the picker needs to be updated on show because - // prior to the picker being shown we don't have access to the selections' starting color. - colorPickers['#property-zone-haze-color'].colpickSetColor({ - "r": elZoneHazeColorRed.value, - "g": elZoneHazeColorGreen.value, - "b": elZoneHazeColorBlue.value - }); - }, - onHide: function(colpick) { - $('#property-zone-haze-color').attr('active', 'false'); - }, - onChange: function(hsb, hex, rgb, el) { - $(el).css('background-color', '#' + hex); - emitColorPropertyUpdate('hazeColor', rgb.r, rgb.g, rgb.b, 'haze'); - } - }); - var zoneHazeColorChangeFunction = createEmitGroupColorPropertyUpdateFunction('haze', 'hazeColor', - elZoneHazeColorRed, - elZoneHazeColorGreen, - elZoneHazeColorBlue); - - elZoneHazeColorRed.addEventListener('change', zoneHazeColorChangeFunction); - elZoneHazeColorGreen.addEventListener('change', zoneHazeColorChangeFunction); - elZoneHazeColorBlue.addEventListener('change', zoneHazeColorChangeFunction); - - colorPickers['#property-zone-haze-glare-color'] = $('#property-zone-haze-glare-color').colpick({ - colorScheme: 'dark', - layout: 'hex', - color: '000000', - submit: false, // We don't want to have a submission button - onShow: function(colpick) { - $('#property-zone-haze-glare-color').attr('active', 'true'); - // The original color preview within the picker needs to be updated on show because - // prior to the picker being shown we don't have access to the selections' starting color. - colorPickers['#property-zone-haze-glare-color'].colpickSetColor({ - "r": elZoneHazeGlareColorRed.value, - "g": elZoneHazeGlareColorGreen.value, - "b": elZoneHazeGlareColorBlue.value - }); - }, - onHide: function(colpick) { - $('#property-zone-haze-glare-color').attr('active', 'false'); - }, - onChange: function(hsb, hex, rgb, el) { - $(el).css('background-color', '#' + hex); - emitColorPropertyUpdate('hazeGlareColor', rgb.r, rgb.g, rgb.b, 'haze'); - } - }); - var zoneHazeGlareColorChangeFunction = createEmitGroupColorPropertyUpdateFunction('haze', 'hazeGlareColor', - elZoneHazeGlareColorRed, - elZoneHazeGlareColorGreen, - elZoneHazeGlareColorBlue); - - elZoneHazeGlareColorRed.addEventListener('change', zoneHazeGlareColorChangeFunction); - elZoneHazeGlareColorGreen.addEventListener('change', zoneHazeGlareColorChangeFunction); - elZoneHazeGlareColorBlue.addEventListener('change', zoneHazeGlareColorChangeFunction); - - elZoneHazeEnableGlare.addEventListener('change', - createEmitGroupCheckedPropertyUpdateFunction('haze', 'hazeEnableGlare')); - elZoneHazeGlareAngle.addEventListener('change', createEmitGroupNumberPropertyUpdateFunction('haze', 'hazeGlareAngle')); - - elZoneHazeAltitudeEffect.addEventListener('change', - createEmitGroupCheckedPropertyUpdateFunction('haze', 'hazeAltitudeEffect')); - elZoneHazeCeiling.addEventListener('change', createEmitGroupNumberPropertyUpdateFunction('haze', 'hazeCeiling')); - elZoneHazeBaseRef.addEventListener('change', createEmitGroupNumberPropertyUpdateFunction('haze', 'hazeBaseRef')); - - elZoneHazeBackgroundBlend.addEventListener('change', - createEmitGroupNumberPropertyUpdateFunction('haze', 'hazeBackgroundBlend')); - - var zoneSkyboxColorChangeFunction = createEmitGroupColorPropertyUpdateFunction('skybox', 'color', - elZoneSkyboxColorRed, elZoneSkyboxColorGreen, elZoneSkyboxColorBlue); - elZoneSkyboxColorRed.addEventListener('change', zoneSkyboxColorChangeFunction); - elZoneSkyboxColorGreen.addEventListener('change', zoneSkyboxColorChangeFunction); - elZoneSkyboxColorBlue.addEventListener('change', zoneSkyboxColorChangeFunction); - colorPickers['#property-zone-skybox-color'] = $('#property-zone-skybox-color').colpick({ - colorScheme: 'dark', - layout: 'hex', - color: '000000', - submit: false, // We don't want to have a submission button - onShow: function(colpick) { - $('#property-zone-skybox-color').attr('active', 'true'); - // The original color preview within the picker needs to be updated on show because - // prior to the picker being shown we don't have access to the selections' starting color. - colorPickers['#property-zone-skybox-color'].colpickSetColor({ - "r": elZoneSkyboxColorRed.value, - "g": elZoneSkyboxColorGreen.value, - "b": elZoneSkyboxColorBlue.value - }); - }, - onHide: function(colpick) { - $('#property-zone-skybox-color').attr('active', 'false'); - }, - onChange: function(hsb, hex, rgb, el) { - $(el).css('background-color', '#' + hex); - emitColorPropertyUpdate('color', rgb.r, rgb.g, rgb.b, 'skybox'); - } - }); - - elZoneSkyboxURL.addEventListener('change', createEmitGroupTextPropertyUpdateFunction('skybox', 'url')); - - elZoneFlyingAllowed.addEventListener('change', createEmitCheckedPropertyUpdateFunction('flyingAllowed')); - elZoneGhostingAllowed.addEventListener('change', createEmitCheckedPropertyUpdateFunction('ghostingAllowed')); - elZoneFilterURL.addEventListener('change', createEmitTextPropertyUpdateFunction('filterURL')); - - var voxelVolumeSizeChangeFunction = createEmitVec3PropertyUpdateFunction( - 'voxelVolumeSize', elVoxelVolumeSizeX, elVoxelVolumeSizeY, elVoxelVolumeSizeZ); - elVoxelVolumeSizeX.addEventListener('change', voxelVolumeSizeChangeFunction); - elVoxelVolumeSizeY.addEventListener('change', voxelVolumeSizeChangeFunction); - elVoxelVolumeSizeZ.addEventListener('change', voxelVolumeSizeChangeFunction); - elVoxelSurfaceStyle.addEventListener('change', createEmitTextPropertyUpdateFunction('voxelSurfaceStyle')); - elXTextureURL.addEventListener('change', createEmitTextPropertyUpdateFunction('xTextureURL')); - elYTextureURL.addEventListener('change', createEmitTextPropertyUpdateFunction('yTextureURL')); - elZTextureURL.addEventListener('change', createEmitTextPropertyUpdateFunction('zTextureURL')); - - elMoveSelectionToGrid.addEventListener("click", function() { - EventBridge.emitWebEvent(JSON.stringify({ - type: "action", - action: "moveSelectionToGrid" - })); - }); - elMoveAllToGrid.addEventListener("click", function() { - EventBridge.emitWebEvent(JSON.stringify({ - type: "action", - action: "moveAllToGrid" - })); - }); - elResetToNaturalDimensions.addEventListener("click", function() { - EventBridge.emitWebEvent(JSON.stringify({ - type: "action", - action: "resetToNaturalDimensions" - })); - }); - elRescaleDimensionsButton.addEventListener("click", function() { - EventBridge.emitWebEvent(JSON.stringify({ - type: "action", - action: "rescaleDimensions", - percentage: parseFloat(elRescaleDimensionsPct.value) - })); - }); - elReloadScriptsButton.addEventListener("click", function() { - EventBridge.emitWebEvent(JSON.stringify({ - type: "action", - action: "reloadClientScripts" - })); - }); - elReloadServerScriptsButton.addEventListener("click", function() { - // invalidate the current status (so that same-same updates can still be observed visually) - elServerScriptStatus.innerText = PENDING_SCRIPT_STATUS; - EventBridge.emitWebEvent(JSON.stringify({ - type: "action", - action: "reloadServerScripts" - })); - }); - - document.addEventListener("keydown", function (keyDown) { - if (keyDown.keyCode === KEY_P && keyDown.ctrlKey) { - if (keyDown.shiftKey) { - EventBridge.emitWebEvent(JSON.stringify({ type: 'unparent' })); - } else { - EventBridge.emitWebEvent(JSON.stringify({ type: 'parent' })); + for (let textAreaIndex = 0, numTextAreas = elTextareas.length; textAreaIndex < numTextAreas; ++textAreaIndex) { + let curTextAreaElement = elTextareas[textAreaIndex]; + setTextareaScrolling(curTextAreaElement); + curTextAreaElement.addEventListener("input", textareaOnChangeEvent, false); + curTextAreaElement.addEventListener("change", textareaOnChangeEvent, false); + /* FIXME: Detect and update textarea scrolling attribute on resize. Unfortunately textarea doesn't have a resize + event; mouseup is a partial stand-in but doesn't handle resizing if mouse moves outside textarea rectangle. */ + curTextAreaElement.addEventListener("mouseup", textareaOnChangeEvent, false); + } + + // Dropdowns + // For each dropdown the following replacement is created in place of the original dropdown... + // Structure created: + //
+ //
display textcarat
+ //
+ //
    + //
  • 0) { + let el = elDropdowns[0]; + el.parentNode.removeChild(el); + elDropdowns = document.getElementsByTagName("select"); + } + + const KEY_CODES = { + BACKSPACE: 8, + DELETE: 46 + }; + + document.addEventListener("keyup", function (keyUpEvent) { + if (keyUpEvent.target.nodeName === "INPUT") { + return; + } + let {code, key, keyCode, altKey, ctrlKey, metaKey, shiftKey} = keyUpEvent; + + let controlKey = window.navigator.platform.startsWith("Mac") ? metaKey : ctrlKey; + + let keyCodeString; + switch (keyCode) { + case KEY_CODES.DELETE: + keyCodeString = "Delete"; + break; + case KEY_CODES.BACKSPACE: + keyCodeString = "Backspace"; + break; + default: + keyCodeString = String.fromCharCode(keyUpEvent.keyCode); + break; + } + + EventBridge.emitWebEvent(JSON.stringify({ + type: 'keyUpEvent', + keyUpEvent: { + code, + key, + keyCode, + keyCodeString, + altKey, + controlKey, + shiftKey, + } + })); + }, false); + window.onblur = function() { // Fake a change event - var ev = document.createEvent("HTMLEvents"); + let ev = document.createEvent("HTMLEvents"); ev.initEvent("change", true, true); document.activeElement.dispatchEvent(ev); }; - + // For input and textarea elements, select all of the text on focus - var els = document.querySelectorAll("input, textarea"); - for (var i = 0; i < els.length; i++) { + let els = document.querySelectorAll("input, textarea"); + for (let i = 0; i < els.length; ++i) { els[i].onfocus = function (e) { e.target.select(); }; } + + bindAllNonJSONEditorElements(); - bindAllNonJSONEditorElements(); + showGroupsForType("None"); + resetProperties(); + disableProperties(); }); - // Collapsible sections - var elCollapsible = document.getElementsByClassName("section-header"); - - var toggleCollapsedEvent = function(event) { - var element = event.target.parentNode.parentNode; - var isCollapsed = element.dataset.collapsed !== "true"; - element.dataset.collapsed = isCollapsed ? "true" : false; - element.setAttribute("collapsed", isCollapsed ? "true" : "false"); - element.getElementsByClassName(".collapse-icon")[0].textContent = isCollapsed ? "L" : "M"; - }; - - for (var collapseIndex = 0, numCollapsibles = elCollapsible.length; collapseIndex < numCollapsibles; ++collapseIndex) { - var curCollapsibleElement = elCollapsible[collapseIndex]; - curCollapsibleElement.getElementsByTagName('span')[0].addEventListener("click", toggleCollapsedEvent, true); - } - - - // Textarea scrollbars - var elTextareas = document.getElementsByTagName("TEXTAREA"); - - var textareaOnChangeEvent = function(event) { - setTextareaScrolling(event.target); - }; - - for (var textAreaIndex = 0, numTextAreas = elTextareas.length; textAreaIndex < numTextAreas; ++textAreaIndex) { - var curTextAreaElement = elTextareas[textAreaIndex]; - setTextareaScrolling(curTextAreaElement); - curTextAreaElement.addEventListener("input", textareaOnChangeEvent, false); - curTextAreaElement.addEventListener("change", textareaOnChangeEvent, false); - /* FIXME: Detect and update textarea scrolling attribute on resize. Unfortunately textarea doesn't have a resize - event; mouseup is a partial stand-in but doesn't handle resizing if mouse moves outside textarea rectangle. */ - curTextAreaElement.addEventListener("mouseup", textareaOnChangeEvent, false); - } - - // Dropdowns - // For each dropdown the following replacement is created in place of the original dropdown... - // Structure created: - //
    - //
    display textcarat
    - //
    - //
      - //
    • 0) { - var el = elDropdowns[0]; - el.parentNode.removeChild(el); - elDropdowns = document.getElementsByTagName("select"); - } - augmentSpinButtons(); // Disable right-click context menu which is not visible in the HMD and makes it seem like the app has locked diff --git a/scripts/system/html/js/gridControls.js b/scripts/system/html/js/gridControls.js index 79a169400a..70e91071fb 100644 --- a/scripts/system/html/js/gridControls.js +++ b/scripts/system/html/js/gridControls.js @@ -6,8 +6,6 @@ // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -const KEY_P = 80; //Key code for letter p used for Parenting hotkey. - function loaded() { openEventBridge(function() { elPosY = document.getElementById("horiz-y"); @@ -105,7 +103,7 @@ function loaded() { elColorBlue.value = blue; gridColor = { red: red, green: green, blue: blue }; emitUpdate(); - } + }; elColorRed.addEventListener('change', colorChangeFunction); elColorGreen.addEventListener('change', colorChangeFunction); @@ -131,15 +129,47 @@ function loaded() { EventBridge.emitWebEvent(JSON.stringify({ type: 'init' })); }); - document.addEventListener("keydown", function (keyDown) { - if (keyDown.keyCode === KEY_P && keyDown.ctrlKey) { - if (keyDown.shiftKey) { - EventBridge.emitWebEvent(JSON.stringify({ type: 'unparent' })); - } else { - EventBridge.emitWebEvent(JSON.stringify({ type: 'parent' })); - } - } - }) + + const KEY_CODES = { + BACKSPACE: 8, + DELETE: 46 + }; + + document.addEventListener("keyup", function (keyUpEvent) { + if (keyUpEvent.target.nodeName === "INPUT") { + return; + } + let {code, key, keyCode, altKey, ctrlKey, metaKey, shiftKey} = keyUpEvent; + + let controlKey = window.navigator.platform.startsWith("Mac") ? metaKey : ctrlKey; + + let keyCodeString; + switch (keyCode) { + case KEY_CODES.DELETE: + keyCodeString = "Delete"; + break; + case KEY_CODES.BACKSPACE: + keyCodeString = "Backspace"; + break; + default: + keyCodeString = String.fromCharCode(keyUpEvent.keyCode); + break; + } + + EventBridge.emitWebEvent(JSON.stringify({ + type: 'keyUpEvent', + keyUpEvent: { + code, + key, + keyCode, + keyCodeString, + altKey, + controlKey, + shiftKey, + } + })); + }, false); + // Disable right-click context menu which is not visible in the HMD and makes it seem like the app has locked document.addEventListener("contextmenu", function (event) { event.preventDefault(); diff --git a/scripts/system/html/js/listView.js b/scripts/system/html/js/listView.js new file mode 100644 index 0000000000..07aba53f1c --- /dev/null +++ b/scripts/system/html/js/listView.js @@ -0,0 +1,316 @@ +// listView.js +// +// Created by David Back on 27 Aug 2018 +// Copyright 2018 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html + +const SCROLL_ROWS = 2; // number of rows used as scrolling buffer, each time we pass this number of rows we scroll +const FIRST_ROW_INDEX = 2; // the first elRow element's index in the child nodes of the table body + +debugPrint = function (message) { + console.log(message); +}; + +function ListView(elTableBody, elTableScroll, elTableHeaderRow, createRowFunction, + updateRowFunction, clearRowFunction, WINDOW_NONVARIABLE_HEIGHT) { + this.elTableBody = elTableBody; + this.elTableScroll = elTableScroll; + this.elTableHeaderRow = elTableHeaderRow; + + this.elTopBuffer = null; + this.elBottomBuffer = null; + + this.createRowFunction = createRowFunction; + this.updateRowFunction = updateRowFunction; + this.clearRowFunction = clearRowFunction; + + // the list of row elements created in the table up to max viewable height plus SCROLL_ROWS rows for scrolling buffer + this.elRows = []; + // the list of all row item data to show in the scrolling table, passed to updateRowFunction to set to each row + this.itemData = []; + // the current index within the itemData list that is set to the top most elRow element + this.rowOffset = 0; + // height of the elRow elements + this.rowHeight = 0; + // the previous elTableScroll.scrollTop value when the elRows were last shifted for scrolling + this.lastRowShiftScrollTop = 0; + + this.initialize(); +} + +ListView.prototype = { + getNumRows: function() { + return this.elRows.length; + }, + + getScrollHeight: function() { + return this.rowHeight * SCROLL_ROWS; + }, + + getFirstVisibleRowIndex: function() { + return this.rowOffset; + }, + + getLastVisibleRowIndex: function() { + return this.getFirstVisibleRowIndex() + entityList.getNumRows() - 1; + }, + + resetToTop: function() { + this.rowOffset = 0; + this.lastRowShiftScrollTop = 0; + this.refreshBuffers(); + this.elTableScroll.scrollTop = 0; + }, + + clear: function() { + for (let i = 0; i < this.getNumRows(); i++) { + let elRow = this.elTableBody.childNodes[i + FIRST_ROW_INDEX]; + this.clearRowFunction(elRow); + elRow.style.display = "none"; // hide cleared rows + } + }, + + onScroll: function() { + var that = this.listView; + that.scroll(); + }, + + scroll: function() { + let scrollTop = this.elTableScroll.scrollTop; + let scrollHeight = this.getScrollHeight(); + let nextRowChangeScrollTop = this.lastRowShiftScrollTop + scrollHeight; + let totalItems = this.itemData.length; + let numRows = this.getNumRows(); + + // if the top of the scroll area has past the amount of scroll row space since the last point of scrolling and there + // are still more rows to scroll to then trigger a scroll down by the min of the scroll row space or number of + // remaining rows below + // if the top of the scroll area has gone back above the last point of scrolling then trigger a scroll up by min of + // the scroll row space or number of rows above + if (scrollTop >= nextRowChangeScrollTop && numRows + this.rowOffset < totalItems) { + let numScrolls = Math.ceil((scrollTop - nextRowChangeScrollTop) / scrollHeight); + let numScrollRows = numScrolls * SCROLL_ROWS; + if (numScrollRows + this.rowOffset + numRows > totalItems) { + numScrollRows = totalItems - this.rowOffset - numRows; + } + this.scrollRows(numScrollRows); + } else if (scrollTop < this.lastRowShiftScrollTop) { + let numScrolls = Math.ceil((this.lastRowShiftScrollTop - scrollTop) / scrollHeight); + let numScrollRows = numScrolls * SCROLL_ROWS; + if (this.rowOffset - numScrollRows < 0) { + numScrollRows = this.rowOffset; + } + this.scrollRows(-numScrollRows); + } + }, + + scrollRows: function(numScrollRows) { + let numScrollRowsAbsolute = Math.abs(numScrollRows); + if (numScrollRowsAbsolute === 0) { + return; + } + + let scrollDown = numScrollRows > 0; + + let prevTopHeight = parseInt(this.elTopBuffer.getAttribute("height")); + let prevBottomHeight = parseInt(this.elBottomBuffer.getAttribute("height")); + + // if the number of rows to scroll at once is greater than the total visible number of row elements, + // then just advance the rowOffset accordingly and allow the refresh below to update all rows + if (numScrollRowsAbsolute > this.getNumRows()) { + this.rowOffset += numScrollRows; + } else { + // for each row to scroll down, move the top row element to the bottom of the + // table before the bottom buffer and reset it's row data to the new item + // for each row to scroll up, move the bottom row element to the top of + // the table before the top row and reset it's row data to the new item + for (let i = 0; i < numScrollRowsAbsolute; i++) { + let topRow = this.elTableBody.childNodes[FIRST_ROW_INDEX]; + let rowToMove = scrollDown ? topRow : this.elTableBody.childNodes[FIRST_ROW_INDEX + this.getNumRows() - 1]; + let rowIndex = scrollDown ? this.getNumRows() + this.rowOffset : this.rowOffset - 1; + let moveRowBefore = scrollDown ? this.elBottomBuffer : topRow; + this.elTableBody.removeChild(rowToMove); + this.elTableBody.insertBefore(rowToMove, moveRowBefore); + this.updateRowFunction(rowToMove, this.itemData[rowIndex]); + this.rowOffset += scrollDown ? 1 : -1; + } + } + + // add/remove the row space that was scrolled away to the top buffer height and last scroll point + // add/remove the row space that was scrolled away to the bottom buffer height + let scrolledSpace = this.rowHeight * numScrollRows; + let newTopHeight = prevTopHeight + scrolledSpace; + let newBottomHeight = prevBottomHeight - scrolledSpace; + this.elTopBuffer.setAttribute("height", newTopHeight); + this.elBottomBuffer.setAttribute("height", newBottomHeight); + this.lastRowShiftScrollTop += scrolledSpace; + + // if scrolling more than the total number of visible rows at once then refresh all row data + if (numScrollRowsAbsolute > this.getNumRows()) { + this.refresh(); + } + }, + + /** + * Scrolls firstRowIndex with least effort, also tries to make the window include the other selections in case lastRowIndex is set. + * In the case that firstRowIndex and lastRowIndex are already within the visible bounds then nothing will happen. + * @param {number} firstRowIndex - The row that will be scrolled to. + * @param {number} lastRowIndex - The last index of the bound. + */ + scrollToRow: function (firstRowIndex, lastRowIndex) { + lastRowIndex = lastRowIndex ? lastRowIndex : firstRowIndex; + let boundingTop = firstRowIndex * this.rowHeight; + let boundingBottom = (lastRowIndex * this.rowHeight) + this.rowHeight; + if ((boundingBottom - boundingTop) > this.elTableScroll.clientHeight) { + boundingBottom = boundingTop + this.elTableScroll.clientHeight; + } + + let currentVisibleAreaTop = this.elTableScroll.scrollTop; + let currentVisibleAreaBottom = currentVisibleAreaTop + this.elTableScroll.clientHeight; + + if (boundingTop < currentVisibleAreaTop) { + this.elTableScroll.scrollTop = boundingTop; + } else if (boundingBottom > currentVisibleAreaBottom) { + this.elTableScroll.scrollTop = boundingBottom - (this.elTableScroll.clientHeight); + } + }, + + refresh: function() { + // block refreshing before rows are initialized + let numRows = this.getNumRows(); + if (numRows === 0) { + return; + } + + let prevScrollTop = this.elTableScroll.scrollTop; + + // start with all row data cleared and initially set to invisible + this.clear(); + + // if we are at the bottom of the list adjust row offset to make sure all rows stay in view + this.refreshRowOffset(); + + // update all row data and set rows visible until max visible items reached + for (let i = 0; i < numRows; i++) { + let rowIndex = i + this.rowOffset; + if (rowIndex >= this.itemData.length) { + break; + } + let rowElementIndex = i + FIRST_ROW_INDEX; + let elRow = this.elTableBody.childNodes[rowElementIndex]; + let itemData = this.itemData[rowIndex]; + this.updateRowFunction(elRow, itemData); + elRow.style.display = ""; // make sure the row is visible + } + + // update the top and bottom buffer heights to adjust for above changes + this.refreshBuffers(); + + // adjust the last row shift scroll point based on how much the current scroll point changed + let scrollTopDifference = this.elTableScroll.scrollTop - prevScrollTop; + if (scrollTopDifference !== 0) { + this.lastRowShiftScrollTop += scrollTopDifference; + if (this.lastRowShiftScrollTop < 0) { + this.lastRowShiftScrollTop = 0; + } + } + }, + + refreshBuffers: function() { + // top buffer height is the number of hidden rows above the top row + let topHiddenRows = this.rowOffset; + let topBufferHeight = this.rowHeight * topHiddenRows; + this.elTopBuffer.setAttribute("height", topBufferHeight); + + // bottom buffer height is the number of hidden rows below the bottom row (last scroll buffer row) + let bottomHiddenRows = this.itemData.length - this.getNumRows() - this.rowOffset; + let bottomBufferHeight = this.rowHeight * bottomHiddenRows; + if (bottomHiddenRows < 0) { + bottomBufferHeight = 0; + } + this.elBottomBuffer.setAttribute("height", bottomBufferHeight); + }, + + refreshRowOffset: function() { + // make sure the row offset isn't causing visible rows to pass the end of the item list and is clamped to 0 + var numRows = this.getNumRows(); + if (this.rowOffset + numRows > this.itemData.length) { + this.rowOffset = this.itemData.length - numRows; + } + if (this.rowOffset < 0) { + this.rowOffset = 0; + } + }, + + onResize: function() { + var that = this.listView; + that.resize(); + }, + + resize: function() { + if (!this.elTableBody || !this.elTableScroll) { + debugPrint("ListView.resize - no valid table body or table scroll element"); + return; + } + + let prevScrollTop = this.elTableScroll.scrollTop; + + // take up available window space + this.elTableScroll.style.height = window.innerHeight - WINDOW_NONVARIABLE_HEIGHT; + let viewableHeight = parseInt(this.elTableScroll.style.height) + 1; + + // remove all existing row elements and clear row list + for (let i = 0; i < this.getNumRows(); i++) { + let elRow = this.elRows[i]; + this.elTableBody.removeChild(elRow); + } + this.elRows = []; + + // create new row elements inserted between the top and bottom buffers up until the max viewable scroll area + let usedHeight = 0; + while (usedHeight < viewableHeight) { + let newRow = this.createRowFunction(); + this.elTableBody.insertBefore(newRow, this.elBottomBuffer); + this.rowHeight = newRow.offsetHeight; + usedHeight += this.rowHeight; + this.elRows.push(newRow); + } + + // add SCROLL_ROWS extras rows for scrolling buffer purposes + for (let i = 0; i < SCROLL_ROWS; i++) { + let scrollRow = this.createRowFunction(); + this.elTableBody.insertBefore(scrollRow, this.elBottomBuffer); + this.elRows.push(scrollRow); + } + + // restore the scroll point to the same scroll point from before above changes + this.elTableScroll.scrollTop = prevScrollTop; + + this.refresh(); + }, + + initialize: function() { + if (!this.elTableBody || !this.elTableScroll) { + debugPrint("ListView.initialize - no valid table body or table scroll element"); + return; + } + + this.elTopBuffer = document.createElement("tr"); + this.elTableBody.appendChild(this.elTopBuffer); + this.elTopBuffer.setAttribute("height", 0); + + this.elBottomBuffer = document.createElement("tr"); + this.elTableBody.appendChild(this.elBottomBuffer); + this.elBottomBuffer.setAttribute("height", 0); + + this.elTableScroll.listView = this; + this.elTableScroll.onscroll = this.onScroll; + window.listView = this; + window.onresize = this.onResize; + + // initialize all row elements + this.resize(); + } +}; diff --git a/scripts/system/html/js/marketplacesInject.js b/scripts/system/html/js/marketplacesInject.js index 9b91d06d41..1bfa9d4b20 100644 --- a/scripts/system/html/js/marketplacesInject.js +++ b/scripts/system/html/js/marketplacesInject.js @@ -1,3 +1,5 @@ +/* global $, window, MutationObserver */ + // // marketplacesInject.js // @@ -11,7 +13,6 @@ // (function () { - // Event bridge messages. var CLARA_IO_DOWNLOAD = "CLARA.IO DOWNLOAD"; var CLARA_IO_STATUS = "CLARA.IO STATUS"; @@ -24,8 +25,9 @@ var canWriteAssets = false; var xmlHttpRequest = null; - var isPreparing = false; // Explicitly track download request status. + var isPreparing = false; // Explicitly track download request status. + var limitedCommerce = false; var commerceMode = false; var userIsLoggedIn = false; var walletNeedsSetup = false; @@ -33,7 +35,6 @@ var messagesWaiting = false; function injectCommonCode(isDirectoryPage) { - // Supporting styles from marketplaces.css. // Glyph font family, size, and spacing adjusted because HiFi-Glyphs cannot be used cross-domain. $("head").append( @@ -59,7 +60,7 @@ ); // Footer. - var isInitialHiFiPage = location.href === marketplaceBaseURL + "/marketplace?"; + var isInitialHiFiPage = location.href === (marketplaceBaseURL + "/marketplace?"); $("body").append( '
      ' + (!isInitialHiFiPage ? '' : '') + @@ -74,7 +75,9 @@ (document.referrer !== "") ? window.history.back() : window.location = (marketplaceBaseURL + "/marketplace?"); }); $("#all-markets").on("click", function () { - EventBridge.emitWebEvent(GOTO_DIRECTORY); + EventBridge.emitWebEvent(JSON.stringify({ + type: GOTO_DIRECTORY + })); }); } @@ -90,15 +93,15 @@ window.location = "https://clara.io/library?gameCheck=true&public=true"; }); $('#exploreHifiMarketplace').on('click', function () { - window.location = marketplaceBaseURL + "/marketplace"; + window.location = marketplaceBaseURL + "/marketplace?"; }); } - emitWalletSetupEvent = function() { + emitWalletSetupEvent = function () { EventBridge.emitWebEvent(JSON.stringify({ type: "WALLET_SETUP" })); - } + }; function maybeAddSetupWalletButton() { if (!$('body').hasClass("walletsetup-injected") && userIsLoggedIn && walletNeedsSetup) { @@ -113,7 +116,7 @@ var span = document.createElement('span'); span.style = "margin:10px 5px;color:#1b6420;font-size:15px;"; - span.innerHTML = "Setup your Wallet to get money and shop in Marketplace."; + span.innerHTML = "Activate your Wallet to get money and shop in Marketplace."; var xButton = document.createElement('a'); xButton.id = "xButton"; @@ -167,7 +170,7 @@ var span = document.createElement('span'); span.style = "margin:10px;color:#1b6420;font-size:15px;"; - span.innerHTML = "to purchase items from the Marketplace."; + span.innerHTML = "to get items from the Marketplace."; var xButton = document.createElement('a'); xButton.id = "xButton"; @@ -193,40 +196,6 @@ } } - function maybeAddPurchasesButton() { - if (userIsLoggedIn) { - // Why isn't this an id?! This really shouldn't be a class on the website, but it is. - var navbarBrandElement = document.getElementsByClassName('navbar-brand')[0]; - var purchasesElement = document.createElement('a'); - var dropDownElement = document.getElementById('user-dropdown'); - - $('#user-dropdown').find('.username')[0].style = "max-width:80px;white-space:nowrap;overflow:hidden;" + - "text-overflow:ellipsis;display:inline-block;position:relative;top:4px;"; - $('#user-dropdown').find('.caret')[0].style = "position:relative;top:-3px;"; - - purchasesElement.id = "purchasesButton"; - purchasesElement.setAttribute('href', "#"); - purchasesElement.innerHTML = ""; - if (messagesWaiting) { - purchasesElement.innerHTML += " "; - } - purchasesElement.innerHTML += "My Purchases"; - // FRONTEND WEBDEV RANT: The username dropdown should REALLY not be programmed to be on the same - // line as the search bar, overlaid on top of the search bar, floated right, and then relatively bumped up using "top:-50px". - $('.navbar-brand').css('margin-right', '10px'); - purchasesElement.style = "height:100%;margin-top:18px;font-weight:bold;float:right;margin-right:" + (dropDownElement.offsetWidth + 30) + - "px;position:relative;z-index:999;"; - navbarBrandElement.parentNode.insertAdjacentElement('beforeend', purchasesElement); - $('#purchasesButton').on('click', function () { - EventBridge.emitWebEvent(JSON.stringify({ - type: "PURCHASES", - referrerURL: window.location.href, - hasUpdates: messagesWaiting - })); - }); - } - } - function changeDropdownMenu() { var logInOrOutButton = document.createElement('a'); logInOrOutButton.id = "logInOrOutButton"; @@ -281,11 +250,12 @@ $(this).attr('href', '#'); } cost = $(this).closest('.col-xs-3').find('.item-cost').text(); + var costInt = parseInt(cost, 10); $(this).closest('.col-xs-3').prev().attr("class", 'col-xs-6'); $(this).closest('.col-xs-3').attr("class", 'col-xs-6'); - var priceElement = $(this).find('.price') + var priceElement = $(this).find('.price'); priceElement.css({ "padding": "3px 5px", "height": "40px", @@ -310,8 +280,12 @@ var getString = "GET"; // Protection against the button getting stuck in the "BUY"/"GET" state. // That happens when the browser gets two MOUSEENTER events before getting a - // MOUSELEAVE event. - if ($this.text() === buyString || $this.text() === getString) { + // MOUSELEAVE event. Also, if not available for sale, just return. + if ($this.text() === buyString || + $this.text() === getString || + $this.text() === 'invalidated' || + $this.text() === 'sold out' || + $this.text() === 'not for sale' ) { return; } $this.data('initialHtml', $this.html()); @@ -319,7 +293,8 @@ var cost = $(this).parent().siblings().text(); if (parseInt(cost) > 0) { $this.text(buyString); - } else { + } + if (parseInt(cost) == 0) { $this.text(getString); } }); @@ -331,6 +306,12 @@ $('.grid-item').find('#price-or-edit').find('a').on('click', function () { + var price = $(this).closest('.grid-item').find('.price').text(); + if (price === 'invalidated' || + price === 'sold out' || + price === 'not for sale') { + return false; + } buyButtonClicked($(this).closest('.grid-item').attr('data-item-id'), $(this).closest('.grid-item').find('.item-title').text(), $(this).closest('.grid-item').find('.creator').find('.value').text(), @@ -355,12 +336,12 @@ function injectAddScrollbarToCategories() { $('#categories-dropdown').on('show.bs.dropdown', function () { $('body > div.container').css('display', 'none') - $('#categories-dropdown > ul.dropdown-menu').css({ 'overflow': 'auto', 'height': 'calc(100vh - 110px)' }) + $('#categories-dropdown > ul.dropdown-menu').css({ 'overflow': 'auto', 'height': 'calc(100vh - 110px)' }); }); $('#categories-dropdown').on('hide.bs.dropdown', function () { - $('body > div.container').css('display', '') - $('#categories-dropdown > ul.dropdown-menu').css({ 'overflow': '', 'height': '' }) + $('body > div.container').css('display', ''); + $('#categories-dropdown > ul.dropdown-menu').css({ 'overflow': '', 'height': '' }); }); } @@ -382,7 +363,6 @@ mutations.forEach(function (mutation) { injectBuyButtonOnMainPage(); }); - //observer.disconnect(); }); var config = { attributes: true, childList: true, characterData: true }; observer.observe(target, config); @@ -390,7 +370,6 @@ // Try this here in case it works (it will if the user just pressed the "back" button, // since that doesn't trigger another AJAX request. injectBuyButtonOnMainPage(); - maybeAddPurchasesButton(); } } @@ -411,7 +390,12 @@ var href = purchaseButton.attr('href'); purchaseButton.attr('href', '#'); + var cost = $('.item-cost').text(); + var costInt = parseInt(cost, 10); var availability = $.trim($('.item-availability').text()); + if (limitedCommerce && (costInt > 0)) { + availability = ''; + } if (availability === 'available') { purchaseButton.css({ "background": "linear-gradient(#00b4ef, #0093C5)", @@ -428,14 +412,13 @@ }); } - var cost = $('.item-cost').text(); var type = $('.item-type').text(); var isUpdating = window.location.href.indexOf('edition=') > -1; var urlParams = new URLSearchParams(window.location.search); if (isUpdating) { purchaseButton.html('UPDATE FOR FREE'); } else if (availability !== 'available') { - purchaseButton.html('UNAVAILABLE (' + availability + ')'); + purchaseButton.html('UNAVAILABLE ' + (availability ? ('(' + availability + ')') : '')); } else if (parseInt(cost) > 0 && $('#side-info').find('#buyItemButton').size() === 0) { purchaseButton.html('PURCHASE ' + cost); @@ -451,9 +434,8 @@ "itemPage", urlParams.get('edition'), type); - } - }); - maybeAddPurchasesButton(); + } + }); } } @@ -503,129 +485,128 @@ $(".top-title .col-sm-4").append(downloadContainer); downloadContainer.append(downloadFBX); } - - // Automatic download to High Fidelity. - function startAutoDownload() { - - // One file request at a time. - if (isPreparing) { - console.log("WARNING: Clara.io FBX: Prepare only one download at a time"); - return; - } - - // User must be able to write to Asset Server. - if (!canWriteAssets) { - console.log("ERROR: Clara.io FBX: File download cancelled because no permissions to write to Asset Server"); - EventBridge.emitWebEvent(WARN_USER_NO_PERMISSIONS); - return; - } - - // User must be logged in. - var loginButton = $("#topnav a[href='/signup']"); - if (loginButton.length > 0) { - loginButton[0].click(); - return; - } - - // Obtain zip file to download for requested asset. - // Reference: https://clara.io/learn/sdk/api/export - - //var XMLHTTPREQUEST_URL = "https://clara.io/api/scenes/{uuid}/export/fbx?zip=true¢erScene=true&alignSceneGround=true&fbxUnit=Meter&fbxVersion=7&fbxEmbedTextures=true&imageFormat=WebGL"; - // 13 Jan 2017: Specify FBX version 5 and remove some options in order to make Clara.io site more likely to - // be successful in generating zip files. - var XMLHTTPREQUEST_URL = "https://clara.io/api/scenes/{uuid}/export/fbx?fbxUnit=Meter&fbxVersion=5&fbxEmbedTextures=true&imageFormat=WebGL"; - - var uuid = location.href.match(/\/view\/([a-z0-9\-]*)/)[1]; - var url = XMLHTTPREQUEST_URL.replace("{uuid}", uuid); - - xmlHttpRequest = new XMLHttpRequest(); - var responseTextIndex = 0; - var zipFileURL = ""; - - xmlHttpRequest.onreadystatechange = function () { - // Messages are appended to responseText; process the new ones. - var message = this.responseText.slice(responseTextIndex); - var statusMessage = ""; - - if (isPreparing) { // Ignore messages in flight after finished/cancelled. - var lines = message.split(/[\n\r]+/); - - for (var i = 0, length = lines.length; i < length; i++) { - if (lines[i].slice(0, 5) === "data:") { - // Parse line. - var data; - try { - data = JSON.parse(lines[i].slice(5)); - } - catch (e) { - data = {}; - } - - // Extract status message. - if (data.hasOwnProperty("message") && data.message !== null) { - statusMessage = data.message; - console.log("Clara.io FBX: " + statusMessage); - } - - // Extract zip file URL. - if (data.hasOwnProperty("files") && data.files.length > 0) { - zipFileURL = data.files[0].url; - if (zipFileURL.slice(-4) !== ".zip") { - console.log(JSON.stringify(data)); // Data for debugging. - } - } - } - } - - if (statusMessage !== "") { - // Update the UI with the most recent status message. - EventBridge.emitWebEvent(CLARA_IO_STATUS + " " + statusMessage); - } - } - - responseTextIndex = this.responseText.length; - }; - - // Note: onprogress doesn't have computable total length so can't use it to determine % complete. - - xmlHttpRequest.onload = function () { - var statusMessage = ""; - - if (!isPreparing) { - return; - } - - isPreparing = false; - - var HTTP_OK = 200; - if (this.status !== HTTP_OK) { - statusMessage = "Zip file request terminated with " + this.status + " " + this.statusText; - console.log("ERROR: Clara.io FBX: " + statusMessage); - EventBridge.emitWebEvent(CLARA_IO_STATUS + " " + statusMessage); - } else if (zipFileURL.slice(-4) !== ".zip") { - statusMessage = "Error creating zip file for download."; - console.log("ERROR: Clara.io FBX: " + statusMessage + ": " + zipFileURL); - EventBridge.emitWebEvent(CLARA_IO_STATUS + " " + statusMessage); - } else { - EventBridge.emitWebEvent(CLARA_IO_DOWNLOAD + " " + zipFileURL); - console.log("Clara.io FBX: File download initiated for " + zipFileURL); - } - - xmlHttpRequest = null; - } - - isPreparing = true; - - console.log("Clara.io FBX: Request zip file for " + uuid); - EventBridge.emitWebEvent(CLARA_IO_STATUS + " Initiating download"); - - xmlHttpRequest.open("POST", url, true); - xmlHttpRequest.setRequestHeader("Accept", "text/event-stream"); - xmlHttpRequest.send(); - } } } + // Automatic download to High Fidelity. + function startAutoDownload() { + // One file request at a time. + if (isPreparing) { + console.log("WARNING: Clara.io FBX: Prepare only one download at a time"); + return; + } + + // User must be able to write to Asset Server. + if (!canWriteAssets) { + console.log("ERROR: Clara.io FBX: File download cancelled because no permissions to write to Asset Server"); + EventBridge.emitWebEvent(JSON.stringify({ + type: WARN_USER_NO_PERMISSIONS + })); + return; + } + + // User must be logged in. + var loginButton = $("#topnav a[href='/signup']"); + if (loginButton.length > 0) { + loginButton[0].click(); + return; + } + + // Obtain zip file to download for requested asset. + // Reference: https://clara.io/learn/sdk/api/export + + //var XMLHTTPREQUEST_URL = "https://clara.io/api/scenes/{uuid}/export/fbx?zip=true¢erScene=true&alignSceneGround=true&fbxUnit=Meter&fbxVersion=7&fbxEmbedTextures=true&imageFormat=WebGL"; + // 13 Jan 2017: Specify FBX version 5 and remove some options in order to make Clara.io site more likely to + // be successful in generating zip files. + var XMLHTTPREQUEST_URL = "https://clara.io/api/scenes/{uuid}/export/fbx?fbxUnit=Meter&fbxVersion=5&fbxEmbedTextures=true&imageFormat=WebGL"; + + var uuid = location.href.match(/\/view\/([a-z0-9\-]*)/)[1]; + var url = XMLHTTPREQUEST_URL.replace("{uuid}", uuid); + + xmlHttpRequest = new XMLHttpRequest(); + var responseTextIndex = 0; + var zipFileURL = ""; + + xmlHttpRequest.onreadystatechange = function () { + // Messages are appended to responseText; process the new ones. + var message = this.responseText.slice(responseTextIndex); + var statusMessage = ""; + + if (isPreparing) { // Ignore messages in flight after finished/cancelled. + var lines = message.split(/[\n\r]+/); + + for (var i = 0, length = lines.length; i < length; i++) { + if (lines[i].slice(0, 5) === "data:") { + // Parse line. + var data; + try { + data = JSON.parse(lines[i].slice(5)); + } + catch (e) { + data = {}; + } + + // Extract zip file URL. + if (data.hasOwnProperty("files") && data.files.length > 0) { + zipFileURL = data.files[0].url; + } + } + } + + if (statusMessage !== "") { + // Update the UI with the most recent status message. + EventBridge.emitWebEvent(JSON.stringify({ + type: CLARA_IO_STATUS, + status: statusMessage + })); + } + } + + responseTextIndex = this.responseText.length; + }; + + // Note: onprogress doesn't have computable total length so can't use it to determine % complete. + + xmlHttpRequest.onload = function () { + var statusMessage = ""; + + if (!isPreparing) { + return; + } + + isPreparing = false; + + var HTTP_OK = 200; + if (this.status !== HTTP_OK) { + EventBridge.emitWebEvent(JSON.stringify({ + type: CLARA_IO_STATUS, + status: statusMessage + })); + } else if (zipFileURL.slice(-4) !== ".zip") { + EventBridge.emitWebEvent(JSON.stringify({ + type: CLARA_IO_STATUS, + status: (statusMessage + ": " + zipFileURL) + })); + } else { + EventBridge.emitWebEvent(JSON.stringify({ + type: CLARA_IO_DOWNLOAD + })); + } + + xmlHttpRequest = null; + } + + isPreparing = true; + EventBridge.emitWebEvent(JSON.stringify({ + type: CLARA_IO_STATUS, + status: "Initiating download" + })); + + xmlHttpRequest.open("POST", url, true); + xmlHttpRequest.setRequestHeader("Accept", "text/event-stream"); + xmlHttpRequest.send(); + } + function injectClaraCode() { // Make space for marketplaces footer in Clara pages. @@ -663,7 +644,9 @@ updateClaraCodeInterval = undefined; }); - EventBridge.emitWebEvent(QUERY_CAN_WRITE_ASSETS); + EventBridge.emitWebEvent(JSON.stringify({ + type: QUERY_CAN_WRITE_ASSETS + })); } function cancelClaraDownload() { @@ -673,7 +656,9 @@ xmlHttpRequest.abort(); xmlHttpRequest = null; console.log("Clara.io FBX: File download cancelled"); - EventBridge.emitWebEvent(CLARA_IO_CANCELLED_DOWNLOAD); + EventBridge.emitWebEvent(JSON.stringify({ + type: CLARA_IO_CANCELLED_DOWNLOAD + })); } } @@ -708,25 +693,23 @@ function onLoad() { EventBridge.scriptEventReceived.connect(function (message) { - if (message.slice(0, CAN_WRITE_ASSETS.length) === CAN_WRITE_ASSETS) { - canWriteAssets = message.slice(-4) === "true"; - } else if (message.slice(0, CLARA_IO_CANCEL_DOWNLOAD.length) === CLARA_IO_CANCEL_DOWNLOAD) { + message = JSON.parse(message); + if (message.type === CAN_WRITE_ASSETS) { + canWriteAssets = message.canWriteAssets; + } else if (message.type === CLARA_IO_CANCEL_DOWNLOAD) { cancelClaraDownload(); - } else { - var parsedJsonMessage = JSON.parse(message); - - if (parsedJsonMessage.type === "marketplaces") { - if (parsedJsonMessage.action === "commerceSetting") { - commerceMode = !!parsedJsonMessage.data.commerceMode; - userIsLoggedIn = !!parsedJsonMessage.data.userIsLoggedIn; - walletNeedsSetup = !!parsedJsonMessage.data.walletNeedsSetup; - marketplaceBaseURL = parsedJsonMessage.data.metaverseServerURL; - if (marketplaceBaseURL.indexOf('metaverse.') !== -1) { - marketplaceBaseURL = marketplaceBaseURL.replace('metaverse.', ''); - } - messagesWaiting = parsedJsonMessage.data.messagesWaiting; - injectCode(); + } else if (message.type === "marketplaces") { + if (message.action === "commerceSetting") { + limitedCommerce = !!message.data.limitedCommerce; + commerceMode = !!message.data.commerceMode; + userIsLoggedIn = !!message.data.userIsLoggedIn; + walletNeedsSetup = !!message.data.walletNeedsSetup; + marketplaceBaseURL = message.data.metaverseServerURL; + if (marketplaceBaseURL.indexOf('metaverse.') !== -1) { + marketplaceBaseURL = marketplaceBaseURL.replace('metaverse.', ''); } + messagesWaiting = message.data.messagesWaiting; + injectCode(); } } }); @@ -739,6 +722,6 @@ } // Load / unload. - window.addEventListener("load", onLoad); // More robust to Web site issues than using $(document).ready(). - window.addEventListener("page:change", onLoad); // Triggered after Marketplace HTML is changed + window.addEventListener("load", onLoad); // More robust to Web site issues than using $(document).ready(). + window.addEventListener("page:change", onLoad); // Triggered after Marketplace HTML is changed }()); diff --git a/scripts/system/html/js/miniTablet.js b/scripts/system/html/js/miniTablet.js new file mode 100644 index 0000000000..c48201cef5 --- /dev/null +++ b/scripts/system/html/js/miniTablet.js @@ -0,0 +1,157 @@ +// +// miniTablet.js +// +// Created by David Rowe on 20 Aug 2018. +// Copyright 2018 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +/* global EventBridge */ +/* eslint-env browser */ + +(function () { + + "use strict"; + + var // EventBridge + READY_MESSAGE = "ready", // Engine <== Dialog + HOVER_MESSAGE = "hover", // Engine <== Dialog + UNHOVER_MESSAGE = "unhover", // Engine <== Dialog + MUTE_MESSAGE = "mute", // Engine <=> Dialog + GOTO_MESSAGE = "goto", // Engine <=> Dialog + EXPAND_MESSAGE = "expand", // Engine <== Dialog + + muteButton, + muteImage, + gotoButton, + gotoImage, + expandButton, + + // Work around buttons staying hovered when mini tablet is replaced by tablet proper then subsequently redisplayed. + isUnhover = true; + + + function setUnhover() { + if (!isUnhover) { + gotoButton.classList.add("unhover"); + expandButton.classList.add("unhover"); + isUnhover = true; + } + } + + function clearUnhover() { + if (isUnhover) { + gotoButton.classList.remove("unhover"); + expandButton.classList.remove("unhover"); + isUnhover = false; + } + } + + + function onScriptEventReceived(data) { + var message; + + try { + message = JSON.parse(data); + } catch (e) { + console.error("EventBridge message error"); + return; + } + + switch (message.type) { + case MUTE_MESSAGE: + muteImage.src = message.icon; + break; + case GOTO_MESSAGE: + gotoImage.src = message.icon; + break; + } + } + + function onBodyHover() { + EventBridge.emitWebEvent(JSON.stringify({ + type: HOVER_MESSAGE, + target: "body" + })); + } + + function onBodyUnhover() { + EventBridge.emitWebEvent(JSON.stringify({ + type: UNHOVER_MESSAGE, + target: "body" + })); + } + + function onButtonHover() { + EventBridge.emitWebEvent(JSON.stringify({ + type: HOVER_MESSAGE, + target: "button" + })); + clearUnhover(); + } + + function onMuteButtonClick() { + EventBridge.emitWebEvent(JSON.stringify({ + type: MUTE_MESSAGE + })); + } + + function onGotoButtonClick() { + setUnhover(); + EventBridge.emitWebEvent(JSON.stringify({ + type: GOTO_MESSAGE + })); + } + + function onExpandButtonClick() { + setUnhover(); + EventBridge.emitWebEvent(JSON.stringify({ + type: EXPAND_MESSAGE + })); + } + + function connectEventBridge() { + EventBridge.scriptEventReceived.connect(onScriptEventReceived); + EventBridge.emitWebEvent(JSON.stringify({ + type: READY_MESSAGE + })); + } + + function disconnectEventBridge() { + EventBridge.scriptEventReceived.disconnect(onScriptEventReceived); + } + + + function onUnload() { + disconnectEventBridge(); + } + + function onLoad() { + muteButton = document.getElementById("mute"); + muteImage = document.getElementById("mute-img"); + gotoButton = document.getElementById("goto"); + gotoImage = document.getElementById("goto-img"); + expandButton = document.getElementById("expand"); + + connectEventBridge(); + + document.body.addEventListener("mouseenter", onBodyHover, false); + document.body.addEventListener("mouseleave", onBodyUnhover, false); + + muteButton.addEventListener("mouseenter", onButtonHover, false); + gotoButton.addEventListener("mouseenter", onButtonHover, false); + expandButton.addEventListener("mouseenter", onButtonHover, false); + muteButton.addEventListener("click", onMuteButtonClick, true); + gotoButton.addEventListener("click", onGotoButtonClick, true); + expandButton.addEventListener("click", onExpandButtonClick, true); + + document.body.onunload = function () { + onUnload(); + }; + } + + onLoad(); + +}()); diff --git a/scripts/system/particle_explorer/underscore-min.js b/scripts/system/html/js/underscore-min.js similarity index 100% rename from scripts/system/particle_explorer/underscore-min.js rename to scripts/system/html/js/underscore-min.js diff --git a/scripts/system/html/miniTablet.html b/scripts/system/html/miniTablet.html new file mode 100644 index 0000000000..69703f6d2e --- /dev/null +++ b/scripts/system/html/miniTablet.html @@ -0,0 +1,32 @@ + + + + + + + + + + +
      +
      + +
      +
      + +
      +
      + +
      +
      + + + diff --git a/scripts/system/interstitialPage.js b/scripts/system/interstitialPage.js new file mode 100644 index 0000000000..b7c5809b3a --- /dev/null +++ b/scripts/system/interstitialPage.js @@ -0,0 +1,645 @@ +// +// interstitialPage.js +// scripts/system +// +// Created by Dante Ruiz on 08/02/2018. +// Copyright 2012 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +/* global Script, Controller, Overlays, Quat, MyAvatar, Entities, print, Vec3, AddressManager, Render, Window, Toolbars, + Camera, HMD, location, Account, Xform*/ + +(function() { + Script.include("/~/system/libraries/Xform.js"); + Script.include("/~/system/libraries/globals.js"); + var DEBUG = false; + var TOTAL_LOADING_PROGRESS = 3.7; + var EPSILON = 0.05; + var TEXTURE_EPSILON = 0.01; + var isVisible = false; + var VOLUME = 0.4; + var tune = SoundCache.getSound(Script.resolvePath("/~/system/assets/sounds/crystals_and_voices.mp3")); + var sample = null; + var MAX_LEFT_MARGIN = 1.9; + var INNER_CIRCLE_WIDTH = 4.7; + var DEFAULT_Z_OFFSET = 5.45; + var LOADING_IMAGE_WIDTH_PIXELS = 1024; + var previousCameraMode = Camera.mode; + + var renderViewTask = Render.getConfig("RenderMainView"); + var toolbar = Toolbars.getToolbar("com.highfidelity.interface.toolbar.system"); + var request = Script.require('request').request; + var BUTTON_PROPERTIES = { + text: "Interstitial" + }; + + var tablet = null; + var button = null; + + var errorConnectingToDomain = false; + + // Tips have a character limit of 69 + var userTips = [ + "Tip: Visit TheSpot to explore featured domains!", + "Tip: Visit our docs online to learn more about scripting!", + "Tip: Don't want others invading your personal space? Turn on the Bubble!", + "Tip: Want to make a friend? Shake hands with them in VR!", + "Tip: Enjoy live music? Visit Rust to dance your heart out!", + "Tip: Have you visited BodyMart to check out the new avatars recently?", + "Tip: Use the Create app to import models and create custom entities.", + "Tip: We're open source! Feel free to contribute to our code on GitHub!", + "Tip: What emotes have you used in the Emote app?", + "Tip: Take and share your snapshots with everyone using the Snap app.", + "Tip: Did you know you can show websites in-world by creating a web entity?", + "Tip: Find out more information about domains by visiting our website!", + "Tip: Did you know you can get cool new apps from the Marketplace?", + "Tip: Print your snapshots from the Snap app to share with others!", + "Tip: Log in to make friends, visit new domains, and save avatars!" + ]; + + var DEFAULT_DIMENSIONS = { x: 24, y: 24, z: 24 }; + + var BLACK_SPHERE = Script.resolvePath("/~/system/assets/models/black-sphere.fbx"); + var BUTTON = Script.resourcesPath() + "images/interstitialPage/button.png"; + var BUTTON_HOVER = Script.resourcesPath() + "images/interstitialPage/button_hover.png"; + var LOADING_BAR_PLACARD = Script.resourcesPath() + "images/loadingBar_placard.png"; + var LOADING_BAR_PROGRESS = Script.resourcesPath() + "images/loadingBar_progress.png"; + + ModelCache.prefetch(BLACK_SPHERE); + TextureCache.prefetch(BUTTON); + TextureCache.prefetch(BUTTON_HOVER); + TextureCache.prefetch(LOADING_BAR_PLACARD); + TextureCache.prefetch(LOADING_BAR_PROGRESS); + + var loadingSphereID = Overlays.addOverlay("model", { + name: "Loading-Sphere", + position: Vec3.sum(Vec3.sum(MyAvatar.position, { x: 0.0, y: -1.0, z: 0.0 }), Vec3.multiplyQbyV(MyAvatar.orientation, { x: 0, y: 0.95, z: 0 })), + orientation: Quat.multiply(Quat.fromVec3Degrees({ x: 0, y: 180, z: 0 }), MyAvatar.orientation), + url: BLACK_SPHERE, + dimensions: DEFAULT_DIMENSIONS, + alpha: 1, + visible: isVisible, + ignoreRayIntersection: true, + drawInFront: true, + grabbable: false, + parentID: MyAvatar.SELF_ID + }); + + var anchorOverlay = Overlays.addOverlay("cube", { + dimensions: { x: 0.2, y: 0.2, z: 0.2 }, + visible: false, + grabbable: false, + ignoreRayIntersection: true, + localPosition: { x: 0.0, y: getAnchorLocalYOffset(), z: DEFAULT_Z_OFFSET }, + orientation: Quat.multiply(Quat.fromVec3Degrees({ x: 0, y: 180, z: 0 }), MyAvatar.orientation), + solid: true, + drawInFront: true, + parentID: loadingSphereID + }); + + + var domainName = ""; + var domainNameTextID = Overlays.addOverlay("text3d", { + name: "Loading-Destination-Card-Text", + localPosition: { x: 0.0, y: 0.8, z: -0.001 }, + text: domainName, + textAlpha: 1, + backgroundAlpha: 1, + lineHeight: 0.42, + visible: isVisible, + ignoreRayIntersection: true, + drawInFront: true, + grabbable: false, + localOrientation: Quat.fromVec3Degrees({ x: 0, y: 180, z: 0 }), + parentID: anchorOverlay + }); + + var domainText = ""; + var domainDescription = Overlays.addOverlay("text3d", { + name: "Loading-Hostname", + localPosition: { x: 0.0, y: 0.32, z: 0.0 }, + text: domainText, + textAlpha: 1, + backgroundAlpha: 1, + lineHeight: 0.13, + visible: isVisible, + ignoreRayIntersection: true, + drawInFront: true, + grabbable: false, + localOrientation: Quat.fromVec3Degrees({ x: 0, y: 180, z: 0 }), + parentID: anchorOverlay + }); + + var toolTip = ""; + + var domainToolTip = Overlays.addOverlay("text3d", { + name: "Loading-Tooltip", + localPosition: { x: 0.0, y: -1.6, z: 0.0 }, + text: toolTip, + textAlpha: 1, + backgroundAlpha: 0.00393, + lineHeight: 0.13, + visible: isVisible, + ignoreRayIntersection: true, + drawInFront: true, + grabbable: false, + localOrientation: Quat.fromVec3Degrees({ x: 0, y: 180, z: 0 }), + parentID: anchorOverlay + }); + + var loadingToTheSpotText = Overlays.addOverlay("text3d", { + name: "Loading-Destination-Card-Text", + localPosition: { x: 0.0, y: -1.687, z: -0.3 }, + text: "Go To TheSpot", + textAlpha: 1, + backgroundAlpha: 0.00393, + lineHeight: 0.10, + visible: isVisible, + ignoreRayIntersection: true, + dimensions: { x: 1, y: 0.17 }, + drawInFront: true, + grabbable: false, + localOrientation: Quat.fromVec3Degrees({ x: 0, y: 180, z: 0 }), + parentID: anchorOverlay + }); + + var loadingToTheSpotID = Overlays.addOverlay("image3d", { + name: "Loading-Destination-Card-GoTo-Image", + localPosition: { x: 0.0, y: -1.75, z: -0.3 }, + url: BUTTON, + alpha: 1, + visible: isVisible, + emissive: true, + ignoreRayIntersection: false, + drawInFront: true, + grabbable: false, + localOrientation: Quat.fromVec3Degrees({ x: 0, y: 180, z: 0 }), + parentID: anchorOverlay + }); + + var loadingToTheSpotHoverID = Overlays.addOverlay("image3d", { + name: "Loading-Destination-Card-GoTo-Image-Hover", + localPosition: { x: 0.0, y: -1.75, z: -0.3 }, + url: BUTTON_HOVER, + alpha: 1, + visible: false, + emissive: true, + ignoreRayIntersection: false, + drawInFront: true, + grabbable: false, + localOrientation: Quat.fromVec3Degrees({ x: 0, y: 180, z: 0 }), + parentID: anchorOverlay + }); + + + var loadingBarProgress = Overlays.addOverlay("image3d", { + name: "Loading-Bar-Progress", + localPosition: { x: 0.0, y: -0.86, z: 0.0 }, + url: LOADING_BAR_PROGRESS, + alpha: 1, + dimensions: { x: TOTAL_LOADING_PROGRESS, y: 0.3}, + visible: isVisible, + emissive: true, + ignoreRayIntersection: false, + drawInFront: true, + grabbable: false, + localOrientation: Quat.fromVec3Degrees({ x: 0.0, y: 180.0, z: 0.0 }), + parentID: anchorOverlay, + keepAspectRatio: false + }); + + var loadingBarPlacard = Overlays.addOverlay("image3d", { + name: "Loading-Bar-Placard", + localPosition: { x: 0.0, y: -0.99, z: 0.4 }, + url: LOADING_BAR_PLACARD, + alpha: 1, + dimensions: { x: 4, y: 2.8 }, + visible: isVisible, + emissive: true, + ignoreRayIntersection: false, + drawInFront: true, + grabbable: false, + localOrientation: Quat.fromVec3Degrees({ x: 0.0, y: 180.0, z: 0.0 }), + parentID: anchorOverlay + }); + + var TARGET_UPDATE_HZ = 30; + var BASIC_TIMER_INTERVAL_MS = 1000 / TARGET_UPDATE_HZ; + var lastInterval = Date.now(); + var currentDomain = "no domain"; + var timer = null; + var target = 0; + var textureMemSizeStabilityCount = 0; + var textureMemSizeAtLastCheck = 0; + + var connectionToDomainFailed = false; + + function getAnchorLocalYOffset() { + var loadingSpherePosition = Overlays.getProperty(loadingSphereID, "position"); + var loadingSphereOrientation = Overlays.getProperty(loadingSphereID, "rotation"); + var overlayXform = new Xform(loadingSphereOrientation, loadingSpherePosition); + var worldToOverlayXform = overlayXform.inv(); + var headPosition = MyAvatar.getHeadPosition(); + var headPositionInOverlaySpace = worldToOverlayXform.xformPoint(headPosition); + return headPositionInOverlaySpace.y; + } + + function getLeftMargin(overlayID, text) { + var textSize = Overlays.textSize(overlayID, text); + var sizeDifference = ((INNER_CIRCLE_WIDTH - textSize.width) / 2); + var leftMargin = -(MAX_LEFT_MARGIN - sizeDifference); + return leftMargin; + } + + function lerp(a, b, t) { + return ((1 - t) * a + t * b); + } + + function resetValues() { + updateProgressBar(0.0); + } + + function startInterstitialPage() { + if (timer === null) { + updateOverlays(false); + startAudio(); + target = 0; + textureMemSizeStabilityCount = 0; + textureMemSizeAtLastCheck = 0; + currentProgress = 0.0; + connectionToDomainFailed = false; + previousCameraMode = Camera.mode; + Camera.mode = "first person"; + updateProgressBar(0.0); + timer = Script.setTimeout(update, 2000); + } + } + + function toggleInterstitialPage(isInErrorState) { + errorConnectingToDomain = isInErrorState; + if (!errorConnectingToDomain) { + domainChanged(location); + } + } + + function restartAudio() { + tune.ready.disconnect(restartAudio); + startAudio(); + } + + function startAudio() { + if (tune.downloaded) { + sample = Audio.playSound(tune, { + localOnly: true, + position: MyAvatar.getHeadPosition(), + volume: VOLUME + }); + } else { + tune.ready.connect(restartAudio); + } + } + + function endAudio() { + sample.stop(); + sample = null; + } + + function domainChanged(domain) { + if (domain !== currentDomain) { + MyAvatar.restoreAnimation(); + resetValues(); + var name = location.placename; + domainName = name.charAt(0).toUpperCase() + name.slice(1); + var doRequest = true; + if (name.length === 0 && location.href === "file:///~/serverless/tutorial.json") { + domainName = "Serverless Domain (Tutorial)"; + doRequest = false; + } + var domainNameLeftMargin = getLeftMargin(domainNameTextID, domainName); + var textProperties = { + text: domainName, + leftMargin: domainNameLeftMargin + }; + + if (doRequest) { + var url = Account.metaverseServerURL + '/api/v1/places/' + domain; + request({ + uri: url + }, function(error, data) { + if (data.status === "success") { + var domainInfo = data.data; + var domainDescriptionText = domainInfo.place.description; + var leftMargin = getLeftMargin(domainDescription, domainDescriptionText); + var domainDescriptionProperties = { + text: domainDescriptionText, + leftMargin: leftMargin + }; + Overlays.editOverlay(domainDescription, domainDescriptionProperties); + } + }); + } else { + var domainDescriptionProperties = { + text: "" + }; + Overlays.editOverlay(domainDescription, domainDescriptionProperties); + } + + var randomIndex = Math.floor(Math.random() * userTips.length); + var tip = userTips[randomIndex]; + var tipLeftMargin = getLeftMargin(domainToolTip, tip); + var toolTipProperties = { + text: tip, + leftMargin: tipLeftMargin + }; + + Overlays.editOverlay(domainNameTextID, textProperties); + Overlays.editOverlay(domainToolTip, toolTipProperties); + + + startInterstitialPage(); + currentDomain = domain; + } + } + + var THE_PLACE = (HifiAbout.buildVersion === "dev") ? "hifi://TheSpot-dev" : "hifi://TheSpot"; + function clickedOnOverlay(overlayID, event) { + if (loadingToTheSpotHoverID === overlayID) { + location.handleLookupString(THE_PLACE); + Overlays.editOverlay(loadingToTheSpotHoverID, { visible: false }); + Overlays.editOverlay(loadingToTheSpotID, { visible: true }); + } + } + + function onEnterOverlay(overlayID, event) { + if (currentDomain === "no domain") { + return; + } + if (overlayID === loadingToTheSpotID) { + Overlays.editOverlay(loadingToTheSpotID, { visible: false }); + Overlays.editOverlay(loadingToTheSpotHoverID, { visible: true }); + } + } + + function onLeaveOverlay(overlayID, event) { + if (currentDomain === "no domain") { + return; + } + if (overlayID === loadingToTheSpotHoverID) { + Overlays.editOverlay(loadingToTheSpotHoverID, { visible: false }); + Overlays.editOverlay(loadingToTheSpotID, { visible: true }); + } + } + + var currentProgress = 0.0; + + function updateOverlays(physicsEnabled) { + + if (isInterstitialOverlaysVisible !== !physicsEnabled && !physicsEnabled === true) { + // visible changed to true. + isInterstitialOverlaysVisible = !physicsEnabled; + } + + var properties = { + visible: !physicsEnabled + }; + + var mainSphereProperties = { + visible: !physicsEnabled + }; + + var domainTextProperties = { + text: domainText, + visible: !physicsEnabled + }; + + var loadingBarProperties = { + visible: !physicsEnabled + }; + + if (!HMD.active) { + MyAvatar.headOrientation = Quat.multiply(Quat.cancelOutRollAndPitch(MyAvatar.headOrientation), Quat.fromPitchYawRollDegrees(-3.0, 0, 0)); + } + + renderViewTask.getConfig("LightingModel")["enableAmbientLight"] = physicsEnabled; + renderViewTask.getConfig("LightingModel")["enableDirectionalLight"] = physicsEnabled; + renderViewTask.getConfig("LightingModel")["enablePointLight"] = physicsEnabled; + Overlays.editOverlay(loadingSphereID, mainSphereProperties); + Overlays.editOverlay(loadingToTheSpotText, properties); + Overlays.editOverlay(loadingToTheSpotID, properties); + Overlays.editOverlay(domainNameTextID, properties); + Overlays.editOverlay(domainDescription, domainTextProperties); + Overlays.editOverlay(domainToolTip, properties); + Overlays.editOverlay(loadingBarPlacard, properties); + Overlays.editOverlay(loadingBarProgress, loadingBarProperties); + + if (!DEBUG) { + Menu.setIsOptionChecked("Show Overlays", physicsEnabled); + if (!HMD.active) { + toolbar.writeProperty("visible", physicsEnabled); + } + } + + resetValues(); + + if (physicsEnabled) { + Camera.mode = previousCameraMode; + } + + if (isInterstitialOverlaysVisible !== !physicsEnabled && !physicsEnabled === false) { + // visible changed to false. + isInterstitialOverlaysVisible = !physicsEnabled; + } + } + + function scaleInterstitialPage(sensorToWorldScale) { + var yOffset = getAnchorLocalYOffset(); + var localPosition = { + x: 0.0, + y: yOffset, + z: 5.45 + }; + + Overlays.editOverlay(anchorOverlay, { localPosition: localPosition }); + } + + function sleep(milliseconds) { + var start = new Date().getTime(); + for (var i = 0; i < 1e7; i++) { + if ((new Date().getTime() - start) > milliseconds) { + break; + } + } + } + + function updateProgressBar(progress) { + var progressPercentage = progress / TOTAL_LOADING_PROGRESS; + var subImageWidth = progressPercentage * LOADING_IMAGE_WIDTH_PIXELS; + + var start = TOTAL_LOADING_PROGRESS / 2; + var end = 0; + var xLocalPosition = (progressPercentage * (end - start)) + start; + var properties = { + localPosition: { x: xLocalPosition, y: -0.93, z: 0.0 }, + dimensions: { + x: progress, + y: 0.3 + }, + localOrientation: Quat.fromVec3Degrees({ x: 0.0, y: 180.0, z: 0.0 }), + subImage: { + x: 0.0, + y: 0.0, + width: subImageWidth, + height: 128 + } + }; + + Overlays.editOverlay(loadingBarProgress, properties); + } + + var MAX_TEXTURE_STABILITY_COUNT = 30; + var INTERVAL_PROGRESS = 0.04; + var INTERVAL_PROGRESS_PHYSICS_ENABLED = 0.09; + function update() { + var renderStats = Render.getConfig("Stats"); + var physicsEnabled = Window.isPhysicsEnabled(); + var thisInterval = Date.now(); + var deltaTime = (thisInterval - lastInterval); + lastInterval = thisInterval; + + var domainLoadingProgressPercentage = Window.domainLoadingProgress(); + var progress = ((TOTAL_LOADING_PROGRESS * 0.4) * domainLoadingProgressPercentage); + if (progress >= target) { + target = progress; + } + + if (currentProgress >= ((TOTAL_LOADING_PROGRESS * 0.4) - TEXTURE_EPSILON)) { + var textureResourceGPUMemSize = renderStats.textureResourceGPUMemSize; + var texturePopulatedGPUMemSize = renderStats.textureResourcePopulatedGPUMemSize; + + if (textureMemSizeAtLastCheck === textureResourceGPUMemSize) { + textureMemSizeStabilityCount++; + } else { + textureMemSizeStabilityCount = 0; + } + + textureMemSizeAtLastCheck = textureResourceGPUMemSize; + + if (textureMemSizeStabilityCount >= MAX_TEXTURE_STABILITY_COUNT) { + + if (textureResourceGPUMemSize > 0) { + var gpuPercantage = (TOTAL_LOADING_PROGRESS * 0.6) * (texturePopulatedGPUMemSize / textureResourceGPUMemSize); + var totalProgress = progress + gpuPercantage; + if (totalProgress >= target) { + target = totalProgress; + } + } + } + } + + if ((physicsEnabled && (currentProgress < TOTAL_LOADING_PROGRESS))) { + target = TOTAL_LOADING_PROGRESS; + } + + currentProgress = lerp(currentProgress, target, (physicsEnabled ? INTERVAL_PROGRESS_PHYSICS_ENABLED : INTERVAL_PROGRESS)); + + updateProgressBar(currentProgress); + + if (errorConnectingToDomain) { + updateOverlays(errorConnectingToDomain); + // setting hover id to invisible + Overlays.editOverlay(loadingToTheSpotHoverID, { visible: false }); + endAudio(); + currentDomain = "no domain"; + timer = null; + // The toolbar doesn't become visible in time to match the speed of + // the signal handling of redirectErrorStateChanged in both this script + // and the redirectOverlays.js script. Use a sleep function to ensure + // the toolbar becomes visible again. + sleep(300); + if (!HMD.active) { + toolbar.writeProperty("visible", true); + } + return; + } else if ((physicsEnabled && (currentProgress >= (TOTAL_LOADING_PROGRESS - EPSILON)))) { + updateOverlays((physicsEnabled || connectionToDomainFailed)); + // setting hover id to invisible + Overlays.editOverlay(loadingToTheSpotHoverID, { visible: false }); + endAudio(); + currentDomain = "no domain"; + timer = null; + return; + } + timer = Script.setTimeout(update, BASIC_TIMER_INTERVAL_MS); + } + var whiteColor = { red: 255, green: 255, blue: 255 }; + var greyColor = { red: 125, green: 125, blue: 125 }; + + Overlays.mouseReleaseOnOverlay.connect(clickedOnOverlay); + Overlays.hoverEnterOverlay.connect(onEnterOverlay); + Overlays.hoverLeaveOverlay.connect(onLeaveOverlay); + location.hostChanged.connect(domainChanged); + Window.redirectErrorStateChanged.connect(toggleInterstitialPage); + + MyAvatar.sensorToWorldScaleChanged.connect(scaleInterstitialPage); + MyAvatar.sessionUUIDChanged.connect(function() { + var avatarSessionUUID = MyAvatar.sessionUUID; + Overlays.editOverlay(loadingSphereID, { + position: Vec3.sum(Vec3.sum(MyAvatar.position, {x: 0.0, y: -1.0, z: 0.0}), Vec3.multiplyQbyV(MyAvatar.orientation, {x: 0, y: 0.95, z: 0})), + orientation: Quat.multiply(Quat.fromVec3Degrees({x: 0, y: 180, z: 0}), MyAvatar.orientation), + parentID: avatarSessionUUID + }); + }); + + var toggle = true; + if (DEBUG) { + tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); + button = tablet.addButton(BUTTON_PROPERTIES); + + button.clicked.connect(function() { + toggle = !toggle; + updateOverlays(toggle); + }); + } + + // set left margin of text. + var loadingTextProperties = { + leftMargin: getLeftMargin(loadingToTheSpotText, "Go To TheSpot") + 0.045 + }; + + Overlays.editOverlay(loadingToTheSpotText, loadingTextProperties); + + function cleanup() { + Overlays.deleteOverlay(loadingSphereID); + Overlays.deleteOverlay(loadingToTheSpotID); + Overlays.deleteOverlay(loadingToTheSpotHoverID); + Overlays.deleteOverlay(domainNameTextID); + Overlays.deleteOverlay(domainDescription); + Overlays.deleteOverlay(domainToolTip); + Overlays.deleteOverlay(loadingBarPlacard); + Overlays.deleteOverlay(loadingBarProgress); + Overlays.deleteOverlay(anchorOverlay); + + if (DEBUG) { + tablet.removeButton(button); + } + + renderViewTask.getConfig("LightingModel")["enableAmbientLight"] = true; + renderViewTask.getConfig("LightingModel")["enableDirectionalLight"] = true; + renderViewTask.getConfig("LightingModel")["enablePointLight"] = true; + Menu.setIsOptionChecked("Show Overlays", true); + if (!HMD.active) { + toolbar.writeProperty("visible", true); + } + } + + // location.hostname may already be set by the time the script is loaded. + // Show the interstitial page if the domain isn't loaded. + if (!location.isConnected) { + domainChanged(location.hostname); + } + + Script.scriptEnding.connect(cleanup); +}()); diff --git a/scripts/system/libraries/EditEntityList.qml b/scripts/system/libraries/EditEntityList.qml new file mode 100644 index 0000000000..4fc5ff19ef --- /dev/null +++ b/scripts/system/libraries/EditEntityList.qml @@ -0,0 +1,12 @@ +import QtQuick 2.7 +import QtQuick.Controls 2.2 +import QtWebChannel 1.0 +import QtGraphicalEffects 1.0 +import "qrc:///qml/controls" as HifiControls + +HifiControls.WebView { + id: entityListToolWebView + url: Qt.resolvedUrl("../html/entityList.html") + enabled: true + blurOnCtrlShift: false +} diff --git a/scripts/system/libraries/WebTablet.js b/scripts/system/libraries/WebTablet.js index c201a251d0..8e5b41deda 100644 --- a/scripts/system/libraries/WebTablet.js +++ b/scripts/system/libraries/WebTablet.js @@ -129,9 +129,7 @@ WebTablet = function (url, width, dpi, hand, clientOnly, location, visible) { url: modelURL, // for overlay grabbable: true, // for overlay loadPriority: 10.0, // for overlay - userData: JSON.stringify({ - "grabbableKey": {"grabbable": true} - }), + grab: { grabbable: true }, dimensions: { x: tabletWidth, y: tabletHeight, z: tabletDepth }, parentID: MyAvatar.SELF_ID, visible: visible, diff --git a/scripts/system/libraries/accountUtils.js b/scripts/system/libraries/accountUtils.js index 9f0d690a5f..42204340c7 100644 --- a/scripts/system/libraries/accountUtils.js +++ b/scripts/system/libraries/accountUtils.js @@ -6,11 +6,5 @@ // openLoginWindow = function openLoginWindow() { - if ((HMD.active && Settings.getValue("hmdTabletBecomesToolbar", false)) - || (!HMD.active && Settings.getValue("desktopTabletBecomesToolbar", true))) { - Menu.triggerOption("Login/Sign Up"); - } else { - tablet.loadQMLOnTop("dialogs/TabletLoginDialog.qml"); - HMD.openTablet(); - } + Menu.triggerOption("Login/Sign Up"); }; diff --git a/scripts/system/libraries/connectionUtils.js b/scripts/system/libraries/connectionUtils.js index 166d379330..7b988d3117 100644 --- a/scripts/system/libraries/connectionUtils.js +++ b/scripts/system/libraries/connectionUtils.js @@ -20,7 +20,7 @@ function requestJSON(url, callback) { // callback(data) if successfull. Logs oth uri: url }, function (error, response) { if (error || (response.status !== 'success')) { - print("Error: unable to get", url, error || response.status); + print("Error: unable to get URL", error || response.status); return; } callback(response.data); diff --git a/scripts/system/libraries/controllerDispatcherUtils.js b/scripts/system/libraries/controllerDispatcherUtils.js index a386dcf5b4..e9d5255d28 100644 --- a/scripts/system/libraries/controllerDispatcherUtils.js +++ b/scripts/system/libraries/controllerDispatcherUtils.js @@ -5,7 +5,7 @@ // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -/* global module, Camera, HMD, MyAvatar, controllerDispatcherPlugins:true, Quat, Vec3, Overlays, Xform, +/* global module, Camera, HMD, MyAvatar, controllerDispatcherPlugins:true, Quat, Vec3, Overlays, Xform, Mat4, Selection, Uuid, MSECS_PER_SEC:true , LEFT_HAND:true, RIGHT_HAND:true, FORBIDDEN_GRAB_TYPES:true, HAPTIC_PULSE_STRENGTH:true, HAPTIC_PULSE_DURATION:true, ZERO_VEC:true, ONE_VEC:true, @@ -48,6 +48,7 @@ BUMPER_ON_VALUE:true, getEntityParents:true, findHandChildEntities:true, + findFarGrabJointChildEntities:true, makeLaserParams:true, TEAR_AWAY_DISTANCE:true, TEAR_AWAY_COUNT:true, @@ -58,7 +59,8 @@ highlightTargetEntity:true, clearHighlightedEntities:true, unhighlightTargetEntity:true, - distanceBetweenEntityLocalPositionAndBoundingBox: true + distanceBetweenEntityLocalPositionAndBoundingBox: true, + worldPositionToRegistrationFrameMatrix: true */ MSECS_PER_SEC = 1000.0; @@ -126,13 +128,25 @@ DISPATCHER_PROPERTIES = [ "parentJointIndex", "density", "dimensions", - "userData", "type", "href", "cloneable", "cloneDynamic", "localPosition", - "localRotation" + "localRotation", + "grab.grabbable", + "grab.grabKinematic", + "grab.grabFollowsController", + "grab.triggerable", + "grab.equippable", + "grab.equippableLeftPosition", + "grab.equippableLeftRotation", + "grab.equippableRightPosition", + "grab.equippableRightRotation", + "grab.equippableIndicatorURL", + "grab.equippableIndicatorScale", + "grab.equippableIndicatorOffset", + "userData" ]; // priority -- a lower priority means the module will be asked sooner than one with a higher priority in a given update step @@ -214,25 +228,56 @@ getGrabbableData = function (ggdProps) { } catch (err) { userDataParsed = {}; } + if (userDataParsed.grabbableKey) { grabbableData = userDataParsed.grabbableKey; + } else { + grabbableData = ggdProps.grab; } + + // extract grab-related properties, provide defaults if any are missing if (!grabbableData.hasOwnProperty("grabbable")) { grabbableData.grabbable = true; } - if (!grabbableData.hasOwnProperty("ignoreIK")) { - grabbableData.ignoreIK = true; + // kinematic has been renamed to grabKinematic + if (!grabbableData.hasOwnProperty("grabKinematic") && + !grabbableData.hasOwnProperty("kinematic")) { + grabbableData.grabKinematic = true; } - if (!grabbableData.hasOwnProperty("kinematic")) { - grabbableData.kinematic = true; + if (!grabbableData.hasOwnProperty("grabKinematic")) { + grabbableData.grabKinematic = grabbableData.kinematic; } - if (!grabbableData.hasOwnProperty("wantsTrigger")) { - grabbableData.wantsTrigger = false; + // ignoreIK has been renamed to grabFollowsController + if (!grabbableData.hasOwnProperty("grabFollowsController") && + !grabbableData.hasOwnProperty("ignoreIK")) { + grabbableData.grabFollowsController = true; } - if (!grabbableData.hasOwnProperty("triggerable")) { + if (!grabbableData.hasOwnProperty("grabFollowsController")) { + grabbableData.grabFollowsController = grabbableData.ignoreIK; + } + // wantsTrigger has been renamed to triggerable + if (!grabbableData.hasOwnProperty("triggerable") && + !grabbableData.hasOwnProperty("wantsTrigger")) { grabbableData.triggerable = false; } - + if (!grabbableData.hasOwnProperty("triggerable")) { + grabbableData.triggerable = grabbableData.wantsTrigger; + } + if (!grabbableData.hasOwnProperty("equippable")) { + grabbableData.equippable = false; + } + if (!grabbableData.hasOwnProperty("equippableLeftPosition")) { + grabbableData.equippableLeftPosition = { x: 0, y: 0, z: 0 }; + } + if (!grabbableData.hasOwnProperty("equippableLeftRotation")) { + grabbableData.equippableLeftPosition = { x: 0, y: 0, z: 0, w: 1 }; + } + if (!grabbableData.hasOwnProperty("equippableRightPosition")) { + grabbableData.equippableRightPosition = { x: 0, y: 0, z: 0 }; + } + if (!grabbableData.hasOwnProperty("equippableRightRotation")) { + grabbableData.equippableRightPosition = { x: 0, y: 0, z: 0, w: 1 }; + } return grabbableData; }; @@ -416,6 +461,18 @@ findHandChildEntities = function(hand) { }); }; +findFarGrabJointChildEntities = function(hand) { + // find children of avatar's far-grab joint + var farGrabJointIndex = MyAvatar.getJointIndex(hand === RIGHT_HAND ? "_FARGRAB_RIGHTHAND" : "_FARGRAB_LEFTHAND"); + var children = Entities.getChildrenIDsOfJoint(MyAvatar.sessionUUID, farGrabJointIndex); + children = children.concat(Entities.getChildrenIDsOfJoint(MyAvatar.SELF_ID, farGrabJointIndex)); + + return children.filter(function (childID) { + var childType = Entities.getNestableType(childID); + return childType == "entity"; + }); +}; + distanceBetweenEntityLocalPositionAndBoundingBox = function(entityProps, jointGrabOffset) { var DEFAULT_REGISTRATION_POINT = { x: 0.5, y: 0.5, z: 0.5 }; var rotInv = Quat.inverse(entityProps.localRotation); @@ -487,6 +544,30 @@ entityIsFarGrabbedByOther = function(entityID) { return false; }; + +worldPositionToRegistrationFrameMatrix = function(wptrProps, pos) { + // get world matrix for intersection point + var intersectionMat = new Xform({ x: 0, y: 0, z:0, w: 1 }, pos); + + // calculate world matrix for registrationPoint addjusted entity + var DEFAULT_REGISTRATION_POINT = { x: 0.5, y: 0.5, z: 0.5 }; + var regRatio = Vec3.subtract(DEFAULT_REGISTRATION_POINT, wptrProps.registrationPoint); + var regOffset = Vec3.multiplyVbyV(regRatio, wptrProps.dimensions); + var regOffsetRot = Vec3.multiplyQbyV(wptrProps.rotation, regOffset); + var modelMat = new Xform(wptrProps.rotation, Vec3.sum(wptrProps.position, regOffsetRot)); + + // get inverse of model matrix + var modelMatInv = modelMat.inv(); + + // transform world intersection point into object's registrationPoint frame + var xformMat = Xform.mul(modelMatInv, intersectionMat); + + // convert to Mat4 + var offsetMat = Mat4.createFromRotAndTrans(xformMat.rot, xformMat.pos); + return offsetMat; +}; + + if (typeof module !== 'undefined') { module.exports = { makeDispatcherModuleParameters: makeDispatcherModuleParameters, @@ -508,6 +589,7 @@ if (typeof module !== 'undefined') { projectOntoEntityXYPlane: projectOntoEntityXYPlane, TRIGGER_OFF_VALUE: TRIGGER_OFF_VALUE, TRIGGER_ON_VALUE: TRIGGER_ON_VALUE, - DISPATCHER_HOVERING_LIST: DISPATCHER_HOVERING_LIST + DISPATCHER_HOVERING_LIST: DISPATCHER_HOVERING_LIST, + worldPositionToRegistrationFrameMatrix: worldPositionToRegistrationFrameMatrix }; } diff --git a/scripts/system/libraries/controllers.js b/scripts/system/libraries/controllers.js index cc20c196aa..be7d22e073 100644 --- a/scripts/system/libraries/controllers.js +++ b/scripts/system/libraries/controllers.js @@ -5,20 +5,25 @@ // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -/* global MyAvatar, Vec3, Controller, Quat */ +/* global MyAvatar, Vec3, HMD, Controller, Camera, Quat, Settings, + getGrabPointSphereOffset:true, + setGrabCommunications:true, + getGrabCommunications:true, + getControllerWorldLocation:true + */ var GRAB_COMMUNICATIONS_SETTING = "io.highfidelity.isFarGrabbing"; setGrabCommunications = function setFarGrabCommunications(on) { Settings.setValue(GRAB_COMMUNICATIONS_SETTING, on ? "on" : ""); -} +}; getGrabCommunications = function getFarGrabCommunications() { return !!Settings.getValue(GRAB_COMMUNICATIONS_SETTING, ""); -} +}; // this offset needs to match the one in libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp:378 -var GRAB_POINT_SPHERE_OFFSET = { x: 0.04, y: 0.13, z: 0.039 }; // x = upward, y = forward, z = lateral getGrabPointSphereOffset = function(handController, ignoreSensorToWorldScale) { + var GRAB_POINT_SPHERE_OFFSET = { x: 0.04, y: 0.13, z: 0.039 }; // x = upward, y = forward, z = lateral var offset = GRAB_POINT_SPHERE_OFFSET; if (handController === Controller.Standard.LeftHand) { offset = { @@ -39,7 +44,7 @@ getControllerWorldLocation = function (handController, doOffset) { var orientation; var position; var valid = false; - + if (handController >= 0) { var pose = Controller.getPoseValue(handController); valid = pose.valid; diff --git a/scripts/system/libraries/entityCameraTool.js b/scripts/system/libraries/entityCameraTool.js index 73e73d67a6..4410f19a5e 100644 --- a/scripts/system/libraries/entityCameraTool.js +++ b/scripts/system/libraries/entityCameraTool.js @@ -98,16 +98,18 @@ CameraManager = function() { } function getActionForKeyEvent(event) { - var action = keyToActionMapping[event.key]; - if (action !== undefined) { - if (event.isShifted) { - if (action === "orbitForward") { - action = "orbitUp"; - } else if (action === "orbitBackward") { - action = "orbitDown"; + if (!event.isControl) { + var action = keyToActionMapping[event.key]; + if (action !== undefined) { + if (event.isShifted) { + if (action === "orbitForward") { + action = "orbitUp"; + } else if (action === "orbitBackward") { + action = "orbitDown"; + } } + return action; } - return action; } return null; } diff --git a/scripts/system/libraries/entityList.js b/scripts/system/libraries/entityList.js index 678b2eeb0b..8942c84f33 100644 --- a/scripts/system/libraries/entityList.js +++ b/scripts/system/libraries/entityList.js @@ -9,7 +9,24 @@ // /* global EntityListTool, Tablet, selectionManager, Entities, Camera, MyAvatar, Vec3, Menu, Messages, - cameraManager, MENU_EASE_ON_FOCUS, deleteSelectedEntities, toggleSelectedEntitiesLocked, toggleSelectedEntitiesVisible */ + cameraManager, MENU_EASE_ON_FOCUS, deleteSelectedEntities, toggleSelectedEntitiesLocked, toggleSelectedEntitiesVisible, + keyUpEventFromUIWindow */ + +var PROFILING_ENABLED = false; +var profileIndent = ''; +const PROFILE_NOOP = function(_name, fn, args) { + fn.apply(this, args); +}; +PROFILE = !PROFILING_ENABLED ? PROFILE_NOOP : function(name, fn, args) { + console.log("PROFILE-Script " + profileIndent + "(" + name + ") Begin"); + var previousIndent = profileIndent; + profileIndent += ' '; + var before = Date.now(); + fn.apply(this, args); + var delta = Date.now() - before; + profileIndent = previousIndent; + console.log("PROFILE-Script " + profileIndent + "(" + name + ") End " + delta + "ms"); +}; EntityListTool = function(shouldUseEditTabletApp) { var that = {}; @@ -20,7 +37,7 @@ EntityListTool = function(shouldUseEditTabletApp) { var ENTITY_LIST_WIDTH = 495; var MAX_DEFAULT_CREATE_TOOLS_HEIGHT = 778; var entityListWindow = new CreateWindow( - Script.resourcesPath() + "qml/hifi/tablet/EditEntityList.qml", + Script.resolvePath("EditEntityList.qml"), 'Entity List', 'com.highfidelity.create.entityListWindow', function () { @@ -66,18 +83,27 @@ EntityListTool = function(shouldUseEditTabletApp) { that.setVisible(false); function emitJSONScriptEvent(data) { - var dataString = JSON.stringify(data); - webView.emitScriptEvent(dataString); - if (entityListWindow.window) { - entityListWindow.window.emitScriptEvent(dataString); - } + var dataString; + PROFILE("Script-JSON.stringify", function() { + dataString = JSON.stringify(data); + }); + PROFILE("Script-emitScriptEvent", function() { + webView.emitScriptEvent(dataString); + if (entityListWindow.window) { + entityListWindow.window.emitScriptEvent(dataString); + } + }); } - + that.toggleVisible = function() { that.setVisible(!visible); }; - selectionManager.addEventListener(function() { + selectionManager.addEventListener(function(isSelectionUpdate, caller) { + if (caller === that) { + // ignore events that we emitted from the entity list itself + return; + } var selectedIDs = []; for (var i = 0; i < selectionManager.selections.length; i++) { @@ -90,6 +116,13 @@ EntityListTool = function(shouldUseEditTabletApp) { }); }); + that.setSpaceMode = function(spaceMode) { + emitJSONScriptEvent({ + type: 'setSpaceMode', + spaceMode: spaceMode + }); + }; + that.clearEntityList = function() { emitJSONScriptEvent({ type: 'clearEntityList' @@ -103,7 +136,7 @@ EntityListTool = function(shouldUseEditTabletApp) { selectedIDs: selectedIDs }); }; - + that.deleteEntities = function (deletedIDs) { emitJSONScriptEvent({ type: "deleted", @@ -116,59 +149,67 @@ EntityListTool = function(shouldUseEditTabletApp) { } that.sendUpdate = function() { - var entities = []; + PROFILE('Script-sendUpdate', function() { + var entities = []; - var ids; - if (filterInView) { - ids = Entities.findEntitiesInFrustum(Camera.frustum); - } else { - ids = Entities.findEntities(MyAvatar.position, searchRadius); - } - - var cameraPosition = Camera.position; - for (var i = 0; i < ids.length; i++) { - var id = ids[i]; - var properties = Entities.getEntityProperties(id); - - if (!filterInView || Vec3.distance(properties.position, cameraPosition) <= searchRadius) { - var url = ""; - if (properties.type === "Model") { - url = properties.modelURL; - } else if (properties.type === "Material") { - url = properties.materialURL; + var ids; + PROFILE("findEntities", function() { + if (filterInView) { + ids = Entities.findEntitiesInFrustum(Camera.frustum); + } else { + ids = Entities.findEntities(MyAvatar.position, searchRadius); } - entities.push({ - id: id, - name: properties.name, - type: properties.type, - url: url, - locked: properties.locked, - visible: properties.visible, - verticesCount: (properties.renderInfo !== undefined ? - valueIfDefined(properties.renderInfo.verticesCount) : ""), - texturesCount: (properties.renderInfo !== undefined ? - valueIfDefined(properties.renderInfo.texturesCount) : ""), - texturesSize: (properties.renderInfo !== undefined ? - valueIfDefined(properties.renderInfo.texturesSize) : ""), - hasTransparent: (properties.renderInfo !== undefined ? - valueIfDefined(properties.renderInfo.hasTransparent) : ""), - isBaked: properties.type === "Model" ? url.toLowerCase().endsWith(".baked.fbx") : false, - drawCalls: (properties.renderInfo !== undefined ? - valueIfDefined(properties.renderInfo.drawCalls) : ""), - hasScript: properties.script !== "" - }); + }); + + var cameraPosition = Camera.position; + PROFILE("getMultipleProperties", function () { + var multipleProperties = Entities.getMultipleEntityProperties(ids, ['name', 'type', 'locked', + 'visible', 'renderInfo', 'modelURL', 'materialURL', 'script']); + for (var i = 0; i < multipleProperties.length; i++) { + var properties = multipleProperties[i]; + + if (!filterInView || Vec3.distance(properties.position, cameraPosition) <= searchRadius) { + var url = ""; + if (properties.type === "Model") { + url = properties.modelURL; + } else if (properties.type === "Material") { + url = properties.materialURL; + } + entities.push({ + id: ids[i], + name: properties.name, + type: properties.type, + url: url, + locked: properties.locked, + visible: properties.visible, + verticesCount: (properties.renderInfo !== undefined ? + valueIfDefined(properties.renderInfo.verticesCount) : ""), + texturesCount: (properties.renderInfo !== undefined ? + valueIfDefined(properties.renderInfo.texturesCount) : ""), + texturesSize: (properties.renderInfo !== undefined ? + valueIfDefined(properties.renderInfo.texturesSize) : ""), + hasTransparent: (properties.renderInfo !== undefined ? + valueIfDefined(properties.renderInfo.hasTransparent) : ""), + isBaked: properties.type === "Model" ? url.toLowerCase().endsWith(".baked.fbx") : false, + drawCalls: (properties.renderInfo !== undefined ? + valueIfDefined(properties.renderInfo.drawCalls) : ""), + hasScript: properties.script !== "" + }); + } + } + }); + + var selectedIDs = []; + for (var j = 0; j < selectionManager.selections.length; j++) { + selectedIDs.push(selectionManager.selections[j]); } - } - var selectedIDs = []; - for (var j = 0; j < selectionManager.selections.length; j++) { - selectedIDs.push(selectionManager.selections[j]); - } - - emitJSONScriptEvent({ - type: "update", - entities: entities, - selectedIDs: selectedIDs, + emitJSONScriptEvent({ + type: "update", + entities: entities, + selectedIDs: selectedIDs, + spaceMode: SelectionDisplay.getSpaceMode(), + }); }); }; @@ -186,7 +227,7 @@ EntityListTool = function(shouldUseEditTabletApp) { try { data = JSON.parse(data); } catch(e) { - print("entityList.js: Error parsing JSON: " + e.name + " data " + data); + print("entityList.js: Error parsing JSON"); return; } @@ -196,7 +237,7 @@ EntityListTool = function(shouldUseEditTabletApp) { for (var i = 0; i < ids.length; i++) { entityIDs.push(ids[i]); } - selectionManager.setSelections(entityIDs); + selectionManager.setSelections(entityIDs, that); if (data.focus) { cameraManager.enable(); cameraManager.focus(selectionManager.worldPosition, @@ -217,7 +258,7 @@ EntityListTool = function(shouldUseEditTabletApp) { Window.saveAsync("Select Where to Save", "", "*.json"); } } else if (data.type === "pal") { - var sessionIds = {}; // Collect the sessionsIds of all selected entitities, w/o duplicates. + var sessionIds = {}; // Collect the sessionsIds of all selected entities, w/o duplicates. selectionManager.selections.forEach(function (id) { var lastEditedBy = Entities.getEntityProperties(id, 'lastEditedBy').lastEditedBy; if (lastEditedBy) { @@ -243,6 +284,23 @@ EntityListTool = function(shouldUseEditTabletApp) { filterInView = data.filterInView === true; } else if (data.type === "radius") { searchRadius = data.radius; + } else if (data.type === "cut") { + SelectionManager.cutSelectedEntities(); + } else if (data.type === "copy") { + SelectionManager.copySelectedEntities(); + } else if (data.type === "paste") { + SelectionManager.pasteEntities(); + } else if (data.type === "duplicate") { + SelectionManager.duplicateSelection(); + that.sendUpdate(); + } else if (data.type === "rename") { + Entities.editEntity(data.entityID, {name: data.name}); + // make sure that the name also gets updated in the properties window + SelectionManager._update(); + } else if (data.type === "toggleSpaceMode") { + SelectionDisplay.toggleSpaceMode(); + } else if (data.type === 'keyUpEvent') { + keyUpEventFromUIWindow(data.keyUpEvent); } }; diff --git a/scripts/system/libraries/entitySelectionTool.js b/scripts/system/libraries/entitySelectionTool.js index 1c7d5244a1..e10678a611 100644 --- a/scripts/system/libraries/entitySelectionTool.js +++ b/scripts/system/libraries/entitySelectionTool.js @@ -14,38 +14,33 @@ // /* global SelectionManager, SelectionDisplay, grid, rayPlaneIntersection, rayPlaneIntersection2, pushCommandForSelections, - getMainTabletIDs, getControllerWorldLocation */ + getMainTabletIDs, getControllerWorldLocation, TRIGGER_ON_VALUE */ -var SPACE_LOCAL = "local"; -var SPACE_WORLD = "world"; -var HIGHLIGHT_LIST_NAME = "editHandleHighlightList"; +const SPACE_LOCAL = "local"; +const SPACE_WORLD = "world"; +const HIGHLIGHT_LIST_NAME = "editHandleHighlightList"; Script.include([ "./controllers.js", + "./controllerDispatcherUtils.js", "./utils.js" ]); + +function deepCopy(v) { + return JSON.parse(JSON.stringify(v)); +} + SelectionManager = (function() { var that = {}; - /** - * @description Removes known to be broken properties from a properties object - * @param properties - * @return properties - */ - var fixRemoveBrokenProperties = function (properties) { - // Reason: Entity property is always set to 0,0,0 which causes it to override angularVelocity (see MS17131) - delete properties.localAngularVelocity; - return properties; - } - // FUNCTION: SUBSCRIBE TO UPDATE MESSAGES function subscribeToUpdateMessages() { Messages.subscribe("entityToolUpdates"); Messages.messageReceived.connect(handleEntitySelectionToolUpdates); } - // FUNCTION: HANDLE ENTITY SELECTION TOOL UDPATES + // FUNCTION: HANDLE ENTITY SELECTION TOOL UPDATES function handleEntitySelectionToolUpdates(channel, message, sender) { if (channel !== 'entityToolUpdates') { return; @@ -59,7 +54,7 @@ SelectionManager = (function() { try { messageParsed = JSON.parse(message); } catch (err) { - print("ERROR: entitySelectionTool.handleEntitySelectionToolUpdates - got malformed message: " + message); + print("ERROR: entitySelectionTool.handleEntitySelectionToolUpdates - got malformed message"); return; } @@ -68,7 +63,7 @@ SelectionManager = (function() { if (wantDebug) { print("setting selection to " + messageParsed.entityID); } - that.setSelections([messageParsed.entityID]); + that.setSelections([messageParsed.entityID], that); } } else if (messageParsed.method === "clearSelection") { if (!SelectionDisplay.triggered() || SelectionDisplay.triggeredHand === messageParsed.hand) { @@ -129,7 +124,7 @@ SelectionManager = (function() { that.savedProperties = {}; for (var i = 0; i < that.selections.length; i++) { var entityID = that.selections[i]; - that.savedProperties[entityID] = fixRemoveBrokenProperties(Entities.getEntityProperties(entityID)); + that.savedProperties[entityID] = Entities.getEntityProperties(entityID); } }; @@ -141,7 +136,7 @@ SelectionManager = (function() { return that.selections.length > 0; }; - that.setSelections = function(entityIDs) { + that.setSelections = function(entityIDs, caller) { that.selections = []; for (var i = 0; i < entityIDs.length; i++) { var entityID = entityIDs[i]; @@ -149,10 +144,10 @@ SelectionManager = (function() { Selection.addToSelectedItemsList(HIGHLIGHT_LIST_NAME, "entity", entityID); } - that._update(true); + that._update(true, caller); }; - that.addEntity = function(entityID, toggleSelection) { + that.addEntity = function(entityID, toggleSelection, caller) { if (entityID) { var idx = -1; for (var i = 0; i < that.selections.length; i++) { @@ -170,7 +165,7 @@ SelectionManager = (function() { } } - that._update(true); + that._update(true, caller); }; function removeEntityByID(entityID) { @@ -181,21 +176,21 @@ SelectionManager = (function() { } } - that.removeEntity = function (entityID) { + that.removeEntity = function (entityID, caller) { removeEntityByID(entityID); - that._update(true); + that._update(true, caller); }; - that.removeEntities = function(entityIDs) { + that.removeEntities = function(entityIDs, caller) { for (var i = 0, length = entityIDs.length; i < length; i++) { removeEntityByID(entityIDs[i]); } - that._update(true); + that._update(true, caller); }; - that.clearSelections = function() { + that.clearSelections = function(caller) { that.selections = []; - that._update(true); + that._update(true, caller); }; that.addChildrenEntities = function(parentEntityID, entityList) { @@ -209,9 +204,11 @@ SelectionManager = (function() { } }; - // Return true if the given entity with `properties` is being grabbed by an avatar. + // Determine if an entity is being grabbed. // This is mostly a heuristic - there is no perfect way to know if an entity is being // grabbed. + // + // @return {boolean} true if the given entity with `properties` is being grabbed by an avatar function nonDynamicEntityIsBeingGrabbedByAvatar(properties) { if (properties.dynamic || Uuid.isNull(properties.parentID)) { return false; @@ -225,7 +222,9 @@ SelectionManager = (function() { var grabJointNames = [ 'RightHand', 'LeftHand', '_CONTROLLER_RIGHTHAND', '_CONTROLLER_LEFTHAND', - '_CAMERA_RELATIVE_CONTROLLER_RIGHTHAND', '_CAMERA_RELATIVE_CONTROLLER_LEFTHAND']; + '_CAMERA_RELATIVE_CONTROLLER_RIGHTHAND', '_CAMERA_RELATIVE_CONTROLLER_LEFTHAND', + '_FARGRAB_RIGHTHAND', '_FARGRAB_LEFTHAND', '_FARGRAB_MOUSE' + ]; for (var i = 0; i < grabJointNames.length; ++i) { if (avatar.getJointIndex(grabJointNames[i]) === properties.parentJointIndex) { @@ -236,6 +235,12 @@ SelectionManager = (function() { return false; } + var entityClipboard = { + entities: {}, // Map of id -> properties for copied entities + position: { x: 0, y: 0, z: 0 }, + dimensions: { x: 0, y: 0, z: 0 }, + }; + that.duplicateSelection = function() { var entitiesToDuplicate = []; var duplicatedEntityIDs = []; @@ -257,7 +262,7 @@ SelectionManager = (function() { var originalEntityID = entitiesToDuplicate[i]; var properties = that.savedProperties[originalEntityID]; if (properties === undefined) { - properties = fixRemoveBrokenProperties(Entities.getEntityProperties(originalEntityID)); + properties = Entities.getEntityProperties(originalEntityID); } if (!properties.locked && (!properties.clientOnly || properties.owningAvatarID === MyAvatar.sessionUUID)) { if (nonDynamicEntityIsBeingGrabbedByAvatar(properties)) { @@ -276,7 +281,7 @@ SelectionManager = (function() { var actionArguments = Entities.getActionArguments(properties.id, actionID); if (actionArguments) { var type = actionArguments.type; - if (type == 'hold' || type == 'far-grab') { + if (type === 'hold' || type === 'far-grab') { continue; } delete actionArguments.ttl; @@ -313,7 +318,166 @@ SelectionManager = (function() { return duplicatedEntityIDs; }; - that._update = function(selectionUpdated) { + // Create the entities in entityProperties, maintaining parent-child relationships. + // @param entityPropertites {array} - Array of entity property objects + that.createEntities = function(entityProperties) { + var entitiesToCreate = []; + var createdEntityIDs = []; + var createdChildrenWithOldParents = []; + var originalEntityToNewEntityID = []; + + that.saveProperties(); + + for (var i = 0; i < entityProperties.length; ++i) { + var properties = entityProperties[i]; + if (properties.parentID in originalEntityToNewEntityID) { + properties.parentID = originalEntityToNewEntityID[properties.parentID]; + } else { + delete properties.parentID; + } + + delete properties.actionData; + var newEntityID = Entities.addEntity(properties); + + if (newEntityID) { + createdEntityIDs.push({ + entityID: newEntityID, + properties: properties + }); + if (properties.parentID !== Uuid.NULL) { + createdChildrenWithOldParents[newEntityID] = properties.parentID; + } + originalEntityToNewEntityID[properties.id] = newEntityID; + properties.id = newEntityID; + } + } + + return createdEntityIDs; + }; + + that.cutSelectedEntities = function() { + that.copySelectedEntities(); + deleteSelectedEntities(); + }; + + that.copySelectedEntities = function() { + var entityProperties = Entities.getMultipleEntityProperties(that.selections); + var entities = {}; + entityProperties.forEach(function(props) { + entities[props.id] = props; + }); + + function appendChildren(entityID, entities) { + var childrenIDs = Entities.getChildrenIDs(entityID); + for (var i = 0; i < childrenIDs.length; ++i) { + var id = childrenIDs[i]; + if (!(id in entities)) { + entities[id] = Entities.getEntityProperties(id); + appendChildren(id, entities); + } + } + } + + var len = entityProperties.length; + for (var i = 0; i < len; ++i) { + appendChildren(entityProperties[i].id, entities); + } + + for (var id in entities) { + var parentID = entities[id].parentID; + entities[id].root = !(parentID in entities); + } + + entityClipboard.entities = []; + + var ids = Object.keys(entities); + while (ids.length > 0) { + // Go through all remaining entities. + // If an entity does not have a parent left, move it into the list + for (var i = 0; i < ids.length; ++i) { + var id = ids[i]; + var parentID = entities[id].parentID; + if (parentID in entities) { + continue; + } + entityClipboard.entities.push(entities[id]); + delete entities[id]; + } + ids = Object.keys(entities); + } + + // Calculate size + if (entityClipboard.entities.length === 0) { + entityClipboard.dimensions = { x: 0, y: 0, z: 0 }; + entityClipboard.position = { x: 0, y: 0, z: 0 }; + } else { + var properties = entityClipboard.entities; + var brn = properties[0].boundingBox.brn; + var tfl = properties[0].boundingBox.tfl; + for (var i = 1; i < properties.length; i++) { + var bb = properties[i].boundingBox; + brn.x = Math.min(bb.brn.x, brn.x); + brn.y = Math.min(bb.brn.y, brn.y); + brn.z = Math.min(bb.brn.z, brn.z); + tfl.x = Math.max(bb.tfl.x, tfl.x); + tfl.y = Math.max(bb.tfl.y, tfl.y); + tfl.z = Math.max(bb.tfl.z, tfl.z); + } + entityClipboard.dimensions = { + x: tfl.x - brn.x, + y: tfl.y - brn.y, + z: tfl.z - brn.z + }; + entityClipboard.position = { + x: brn.x + entityClipboard.dimensions.x / 2, + y: brn.y + entityClipboard.dimensions.y / 2, + z: brn.z + entityClipboard.dimensions.z / 2 + }; + } + }; + + that.pasteEntities = function() { + var dimensions = entityClipboard.dimensions; + var maxDimension = Math.max(dimensions.x, dimensions.y, dimensions.z); + var pastePosition = getPositionToCreateEntity(maxDimension); + var deltaPosition = Vec3.subtract(pastePosition, entityClipboard.position); + + var copiedProperties = []; + var ids = []; + entityClipboard.entities.forEach(function(originalProperties) { + var properties = deepCopy(originalProperties); + if (properties.root) { + properties.position = Vec3.sum(properties.position, deltaPosition); + delete properties.localPosition; + } else { + delete properties.position; + } + copiedProperties.push(properties); + }); + + var currentSelections = deepCopy(SelectionManager.selections); + + function redo(copiedProperties) { + var created = that.createEntities(copiedProperties); + var ids = []; + for (var i = 0; i < created.length; ++i) { + ids.push(created[i].entityID); + } + SelectionManager.setSelections(ids); + } + + function undo(copiedProperties) { + for (var i = 0; i < copiedProperties.length; ++i) { + Entities.deleteEntity(copiedProperties[i].id); + } + SelectionManager.setSelections(currentSelections); + } + + redo(copiedProperties); + undoHistory.pushCommand(undo, copiedProperties, redo, copiedProperties); + }; + + that._update = function(selectionUpdated, caller) { var properties = null; if (that.selections.length === 0) { that.localDimensions = null; @@ -336,7 +500,7 @@ SelectionManager = (function() { that.entityType = properties.type; if (selectionUpdated) { - SelectionDisplay.setSpaceMode(SPACE_LOCAL); + SelectionDisplay.useDesiredSpaceMode(); } } else { properties = Entities.getEntityProperties(that.selections[0], ['type', 'boundingBox']); @@ -373,12 +537,12 @@ SelectionManager = (function() { }; // For 1+ selections we can only modify selections in world space - SelectionDisplay.setSpaceMode(SPACE_WORLD); + SelectionDisplay.setSpaceMode(SPACE_WORLD, false); } for (var j = 0; j < listeners.length; j++) { try { - listeners[j](selectionUpdated === true); + listeners[j](selectionUpdated === true, caller); } catch (e) { print("ERROR: entitySelectionTool.update got exception: " + JSON.stringify(e)); } @@ -403,98 +567,103 @@ function normalizeDegrees(degrees) { SelectionDisplay = (function() { var that = {}; - var NEGATE_VECTOR = -1; + const COLOR_GREEN = { red: 31, green: 198, blue: 166 }; + const COLOR_BLUE = { red: 0, green: 147, blue: 197 }; + const COLOR_RED = { red: 226, green: 51, blue: 77 }; + const COLOR_HOVER = { red: 227, green: 227, blue: 227 }; + const COLOR_ROTATE_CURRENT_RING = { red: 255, green: 99, blue: 9 }; + const COLOR_BOUNDING_EDGE = { red: 87, green: 87, blue: 87 }; + const COLOR_SCALE_CUBE = { red: 106, green: 106, blue: 106 }; + const COLOR_SCALE_CUBE_SELECTED = { red: 18, green: 18, blue: 18 }; + const COLOR_DEBUG_PICK_PLANE = { red: 255, green: 255, blue: 255 }; + const COLOR_DEBUG_PICK_PLANE_HIT = { red: 255, green: 165, blue: 0 }; - var COLOR_GREEN = { red: 31, green: 198, blue: 166 }; - var COLOR_BLUE = { red: 0, green: 147, blue: 197 }; - var COLOR_RED = { red: 226, green: 51, blue: 77 }; - var COLOR_HOVER = { red: 227, green: 227, blue: 227 }; - var COLOR_ROTATE_CURRENT_RING = { red: 255, green: 99, blue: 9 }; - var COLOR_SCALE_EDGE = { red: 87, green: 87, blue: 87 }; - var COLOR_SCALE_CUBE = { red: 106, green: 106, blue: 106 }; - var COLOR_SCALE_CUBE_SELECTED = { red: 18, green: 18, blue: 18 }; + const TRANSLATE_ARROW_CYLINDER_OFFSET = 0.1; + const TRANSLATE_ARROW_CYLINDER_CAMERA_DISTANCE_MULTIPLE = 0.005; + const TRANSLATE_ARROW_CYLINDER_Y_MULTIPLE = 7.5; + const TRANSLATE_ARROW_CONE_CAMERA_DISTANCE_MULTIPLE = 0.025; + const TRANSLATE_ARROW_CONE_OFFSET_CYLINDER_DIMENSION_MULTIPLE = 0.83; - var TRANSLATE_ARROW_CYLINDER_OFFSET = 0.1; - var TRANSLATE_ARROW_CYLINDER_CAMERA_DISTANCE_MULTIPLE = 0.005; - var TRANSLATE_ARROW_CYLINDER_Y_MULTIPLE = 7.5; - var TRANSLATE_ARROW_CONE_CAMERA_DISTANCE_MULTIPLE = 0.025; - var TRANSLATE_ARROW_CONE_OFFSET_CYLINDER_DIMENSION_MULTIPLE = 0.83; - - var ROTATE_RING_CAMERA_DISTANCE_MULTIPLE = 0.15; - var ROTATE_CTRL_SNAP_ANGLE = 22.5; - var ROTATE_DEFAULT_SNAP_ANGLE = 1; - var ROTATE_DEFAULT_TICK_MARKS_ANGLE = 5; - var ROTATE_RING_IDLE_INNER_RADIUS = 0.92; - var ROTATE_RING_SELECTED_INNER_RADIUS = 0.9; + const ROTATE_RING_CAMERA_DISTANCE_MULTIPLE = 0.15; + const ROTATE_CTRL_SNAP_ANGLE = 22.5; + const ROTATE_DEFAULT_SNAP_ANGLE = 1; + const ROTATE_DEFAULT_TICK_MARKS_ANGLE = 5; + const ROTATE_RING_IDLE_INNER_RADIUS = 0.92; + const ROTATE_RING_SELECTED_INNER_RADIUS = 0.9; // These are multipliers for sizing the rotation degrees display while rotating an entity - var ROTATE_DISPLAY_DISTANCE_MULTIPLIER = 2; - var ROTATE_DISPLAY_SIZE_X_MULTIPLIER = 0.2; - var ROTATE_DISPLAY_SIZE_Y_MULTIPLIER = 0.09; - var ROTATE_DISPLAY_LINE_HEIGHT_MULTIPLIER = 0.07; + const ROTATE_DISPLAY_DISTANCE_MULTIPLIER = 2; + const ROTATE_DISPLAY_SIZE_X_MULTIPLIER = 0.2; + const ROTATE_DISPLAY_SIZE_Y_MULTIPLIER = 0.09; + const ROTATE_DISPLAY_LINE_HEIGHT_MULTIPLIER = 0.07; - var STRETCH_SPHERE_OFFSET = 0.06; - var STRETCH_SPHERE_CAMERA_DISTANCE_MULTIPLE = 0.01; - var STRETCH_MINIMUM_DIMENSION = 0.001; - var STRETCH_ALL_MINIMUM_DIMENSION = 0.01; - var STRETCH_DIRECTION_ALL_CAMERA_DISTANCE_MULTIPLE = 6; - var STRETCH_PANEL_WIDTH = 0.01; + const STRETCH_CUBE_OFFSET = 0.06; + const STRETCH_CUBE_CAMERA_DISTANCE_MULTIPLE = 0.02; + const STRETCH_MINIMUM_DIMENSION = 0.001; + const STRETCH_PANEL_WIDTH = 0.01; - var SCALE_CUBE_OFFSET = 0.5; - var SCALE_CUBE_CAMERA_DISTANCE_MULTIPLE = 0.0125; - - var CLONER_OFFSET = { x: 0.9, y: -0.9, z: 0.9 }; + const SCALE_OVERLAY_CAMERA_DISTANCE_MULTIPLE = 0.02; + const SCALE_DIMENSIONS_CAMERA_DISTANCE_MULTIPLE = 0.5; + const SCALE_MINIMUM_DIMENSION = 0.01; - var CTRL_KEY_CODE = 16777249; + const BOUNDING_EDGE_OFFSET = 0.5; - var RAIL_AXIS_LENGTH = 10000; + const DUPLICATOR_OFFSET = { x: 0.9, y: -0.9, z: 0.9 }; + + const CTRL_KEY_CODE = 16777249; - var TRANSLATE_DIRECTION = { + const RAIL_AXIS_LENGTH = 10000; + + const NEGATE_VECTOR = -1; + const NO_HAND = -1; + + const DEBUG_PICK_PLANE_HIT_LIMIT = 200; + const DEBUG_PICK_PLANE_HIT_CAMERA_DISTANCE_MULTIPLE = 0.01; + + const TRANSLATE_DIRECTION = { X: 0, Y: 1, Z: 2 }; - var STRETCH_DIRECTION = { + const STRETCH_DIRECTION = { X: 0, Y: 1, Z: 2, ALL: 3 }; - var SCALE_DIRECTION = { - LBN: 0, - RBN: 1, - LBF: 2, - RBF: 3, - LTN: 4, - RTN: 5, - LTF: 6, - RTF: 7 - }; - - var ROTATE_DIRECTION = { + const ROTATE_DIRECTION = { PITCH: 0, YAW: 1, ROLL: 2 }; - - var NO_TRIGGER_HAND = -1; + /** + * The current space mode, this could have been a forced space mode since we do not support multi selection while in + * local space mode. + * @type {string} - should only be set to SPACE_LOCAL or SPACE_WORLD + */ var spaceMode = SPACE_LOCAL; + + /** + * The desired space mode, this is the user set space mode, which should be respected whenever it is possible. In the case + * of multi entity selection this space mode may differ from the actual spaceMode. + * @type {string} - should only be set to SPACE_LOCAL or SPACE_WORLD + */ + var desiredSpaceMode = SPACE_LOCAL; + var overlayNames = []; var lastControllerPoses = [ getControllerWorldLocation(Controller.Standard.LeftHand, true), getControllerWorldLocation(Controller.Standard.RightHand, true) ]; - var rotationZero; - var rotationNormal; - var rotationDegreesPosition; - var worldRotationX; var worldRotationY; var worldRotationZ; + + var activeStretchCubePanelOffset = null; var previousHandle = null; var previousHandleHelper = null; @@ -589,20 +758,18 @@ SelectionDisplay = (function() { leftMargin: 0 }); - var handlePropertiesStretchSpheres = { - alpha: 1, - shape: "Sphere", + var handlePropertiesStretchCubes = { solid: true, visible: false, ignoreRayIntersection: false, drawInFront: true }; - var handleStretchXSphere = Overlays.addOverlay("shape", handlePropertiesStretchSpheres); - Overlays.editOverlay(handleStretchXSphere, { color: COLOR_RED }); - var handleStretchYSphere = Overlays.addOverlay("shape", handlePropertiesStretchSpheres); - Overlays.editOverlay(handleStretchYSphere, { color: COLOR_GREEN }); - var handleStretchZSphere = Overlays.addOverlay("shape", handlePropertiesStretchSpheres); - Overlays.editOverlay(handleStretchZSphere, { color: COLOR_BLUE }); + var handleStretchXCube = Overlays.addOverlay("cube", handlePropertiesStretchCubes); + Overlays.editOverlay(handleStretchXCube, { color: COLOR_RED }); + var handleStretchYCube = Overlays.addOverlay("cube", handlePropertiesStretchCubes); + Overlays.editOverlay(handleStretchYCube, { color: COLOR_GREEN }); + var handleStretchZCube = Overlays.addOverlay("cube", handlePropertiesStretchCubes); + Overlays.editOverlay(handleStretchZCube, { color: COLOR_BLUE }); var handlePropertiesStretchPanel = { shape: "Quad", @@ -619,8 +786,7 @@ SelectionDisplay = (function() { var handleStretchZPanel = Overlays.addOverlay("shape", handlePropertiesStretchPanel); Overlays.editOverlay(handleStretchZPanel, { color: COLOR_BLUE }); - var handlePropertiesScaleCubes = { - alpha: 1, + var handleScaleCube = Overlays.addOverlay("cube", { size: 0.025, color: COLOR_SCALE_CUBE, solid: true, @@ -628,38 +794,30 @@ SelectionDisplay = (function() { ignoreRayIntersection: false, drawInFront: true, borderSize: 1.4 - }; - var handleScaleLBNCube = Overlays.addOverlay("cube", handlePropertiesScaleCubes); // (-x, -y, -z) - var handleScaleRBNCube = Overlays.addOverlay("cube", handlePropertiesScaleCubes); // ( x, -y, -z) - var handleScaleLBFCube = Overlays.addOverlay("cube", handlePropertiesScaleCubes); // (-x, -y, z) - var handleScaleRBFCube = Overlays.addOverlay("cube", handlePropertiesScaleCubes); // ( x, -y, z) - var handleScaleLTNCube = Overlays.addOverlay("cube", handlePropertiesScaleCubes); // (-x, y, -z) - var handleScaleRTNCube = Overlays.addOverlay("cube", handlePropertiesScaleCubes); // ( x, y, -z) - var handleScaleLTFCube = Overlays.addOverlay("cube", handlePropertiesScaleCubes); // (-x, y, z) - var handleScaleRTFCube = Overlays.addOverlay("cube", handlePropertiesScaleCubes); // ( x, y, z) + }); - var handlePropertiesScaleEdge = { + var handlePropertiesBoundingEdge = { alpha: 1, - color: COLOR_SCALE_EDGE, + color: COLOR_BOUNDING_EDGE, visible: false, ignoreRayIntersection: true, drawInFront: true, lineWidth: 0.2 }; - var handleScaleTREdge = Overlays.addOverlay("line3d", handlePropertiesScaleEdge); - var handleScaleTLEdge = Overlays.addOverlay("line3d", handlePropertiesScaleEdge); - var handleScaleTFEdge = Overlays.addOverlay("line3d", handlePropertiesScaleEdge); - var handleScaleTNEdge = Overlays.addOverlay("line3d", handlePropertiesScaleEdge); - var handleScaleBREdge = Overlays.addOverlay("line3d", handlePropertiesScaleEdge); - var handleScaleBLEdge = Overlays.addOverlay("line3d", handlePropertiesScaleEdge); - var handleScaleBFEdge = Overlays.addOverlay("line3d", handlePropertiesScaleEdge); - var handleScaleBNEdge = Overlays.addOverlay("line3d", handlePropertiesScaleEdge); - var handleScaleNREdge = Overlays.addOverlay("line3d", handlePropertiesScaleEdge); - var handleScaleNLEdge = Overlays.addOverlay("line3d", handlePropertiesScaleEdge); - var handleScaleFREdge = Overlays.addOverlay("line3d", handlePropertiesScaleEdge); - var handleScaleFLEdge = Overlays.addOverlay("line3d", handlePropertiesScaleEdge); + var handleBoundingTREdge = Overlays.addOverlay("line3d", handlePropertiesBoundingEdge); + var handleBoundingTLEdge = Overlays.addOverlay("line3d", handlePropertiesBoundingEdge); + var handleBoundingTFEdge = Overlays.addOverlay("line3d", handlePropertiesBoundingEdge); + var handleBoundingTNEdge = Overlays.addOverlay("line3d", handlePropertiesBoundingEdge); + var handleBoundingBREdge = Overlays.addOverlay("line3d", handlePropertiesBoundingEdge); + var handleBoundingBLEdge = Overlays.addOverlay("line3d", handlePropertiesBoundingEdge); + var handleBoundingBFEdge = Overlays.addOverlay("line3d", handlePropertiesBoundingEdge); + var handleBoundingBNEdge = Overlays.addOverlay("line3d", handlePropertiesBoundingEdge); + var handleBoundingNREdge = Overlays.addOverlay("line3d", handlePropertiesBoundingEdge); + var handleBoundingNLEdge = Overlays.addOverlay("line3d", handlePropertiesBoundingEdge); + var handleBoundingFREdge = Overlays.addOverlay("line3d", handlePropertiesBoundingEdge); + var handleBoundingFLEdge = Overlays.addOverlay("line3d", handlePropertiesBoundingEdge); - var handleCloner = Overlays.addOverlay("cube", { + var handleDuplicator = Overlays.addOverlay("cube", { alpha: 1, size: 0.05, color: COLOR_GREEN, @@ -724,7 +882,7 @@ SelectionDisplay = (function() { blue: 255 }, ignoreRayIntersection: true // always ignore this -}); + }); var allOverlays = [ handleTranslateXCone, @@ -738,33 +896,26 @@ SelectionDisplay = (function() { handleRotateRollRing, handleRotateCurrentRing, rotationDegreesDisplay, - handleStretchXSphere, - handleStretchYSphere, - handleStretchZSphere, + handleStretchXCube, + handleStretchYCube, + handleStretchZCube, handleStretchXPanel, handleStretchYPanel, handleStretchZPanel, - handleScaleLBNCube, - handleScaleRBNCube, - handleScaleLBFCube, - handleScaleRBFCube, - handleScaleLTNCube, - handleScaleRTNCube, - handleScaleLTFCube, - handleScaleRTFCube, - handleScaleTREdge, - handleScaleTLEdge, - handleScaleTFEdge, - handleScaleTNEdge, - handleScaleBREdge, - handleScaleBLEdge, - handleScaleBFEdge, - handleScaleBNEdge, - handleScaleNREdge, - handleScaleNLEdge, - handleScaleFREdge, - handleScaleFLEdge, - handleCloner, + handleScaleCube, + handleBoundingTREdge, + handleBoundingTLEdge, + handleBoundingTFEdge, + handleBoundingTNEdge, + handleBoundingBREdge, + handleBoundingBLEdge, + handleBoundingBFEdge, + handleBoundingBNEdge, + handleBoundingNREdge, + handleBoundingNLEdge, + handleBoundingFREdge, + handleBoundingFLEdge, + handleDuplicator, selectionBox, iconSelectionBox, xRailOverlay, @@ -772,7 +923,7 @@ SelectionDisplay = (function() { zRailOverlay ]; - var maximumHandleInAllOverlays = handleCloner; + var maximumHandleInAllOverlays = handleDuplicator; overlayNames[handleTranslateXCone] = "handleTranslateXCone"; overlayNames[handleTranslateXCylinder] = "handleTranslateXCylinder"; @@ -787,49 +938,64 @@ SelectionDisplay = (function() { overlayNames[handleRotateCurrentRing] = "handleRotateCurrentRing"; overlayNames[rotationDegreesDisplay] = "rotationDegreesDisplay"; - overlayNames[handleStretchXSphere] = "handleStretchXSphere"; - overlayNames[handleStretchYSphere] = "handleStretchYSphere"; - overlayNames[handleStretchZSphere] = "handleStretchZSphere"; + overlayNames[handleStretchXCube] = "handleStretchXCube"; + overlayNames[handleStretchYCube] = "handleStretchYCube"; + overlayNames[handleStretchZCube] = "handleStretchZCube"; overlayNames[handleStretchXPanel] = "handleStretchXPanel"; overlayNames[handleStretchYPanel] = "handleStretchYPanel"; overlayNames[handleStretchZPanel] = "handleStretchZPanel"; - overlayNames[handleScaleLBNCube] = "handleScaleLBNCube"; - overlayNames[handleScaleRBNCube] = "handleScaleRBNCube"; - overlayNames[handleScaleLBFCube] = "handleScaleLBFCube"; - overlayNames[handleScaleRBFCube] = "handleScaleRBFCube"; - overlayNames[handleScaleLTNCube] = "handleScaleLTNCube"; - overlayNames[handleScaleRTNCube] = "handleScaleRTNCube"; - overlayNames[handleScaleLTFCube] = "handleScaleLTFCube"; - overlayNames[handleScaleRTFCube] = "handleScaleRTFCube"; + overlayNames[handleScaleCube] = "handleScaleCube"; - overlayNames[handleScaleTREdge] = "handleScaleTREdge"; - overlayNames[handleScaleTLEdge] = "handleScaleTLEdge"; - overlayNames[handleScaleTFEdge] = "handleScaleTFEdge"; - overlayNames[handleScaleTNEdge] = "handleScaleTNEdge"; - overlayNames[handleScaleBREdge] = "handleScaleBREdge"; - overlayNames[handleScaleBLEdge] = "handleScaleBLEdge"; - overlayNames[handleScaleBFEdge] = "handleScaleBFEdge"; - overlayNames[handleScaleBNEdge] = "handleScaleBNEdge"; - overlayNames[handleScaleNREdge] = "handleScaleNREdge"; - overlayNames[handleScaleNLEdge] = "handleScaleNLEdge"; - overlayNames[handleScaleFREdge] = "handleScaleFREdge"; - overlayNames[handleScaleFLEdge] = "handleScaleFLEdge"; + overlayNames[handleBoundingTREdge] = "handleBoundingTREdge"; + overlayNames[handleBoundingTLEdge] = "handleBoundingTLEdge"; + overlayNames[handleBoundingTFEdge] = "handleBoundingTFEdge"; + overlayNames[handleBoundingTNEdge] = "handleBoundingTNEdge"; + overlayNames[handleBoundingBREdge] = "handleBoundingBREdge"; + overlayNames[handleBoundingBLEdge] = "handleBoundingBLEdge"; + overlayNames[handleBoundingBFEdge] = "handleBoundingBFEdge"; + overlayNames[handleBoundingBNEdge] = "handleBoundingBNEdge"; + overlayNames[handleBoundingNREdge] = "handleBoundingNREdge"; + overlayNames[handleBoundingNLEdge] = "handleBoundingNLEdge"; + overlayNames[handleBoundingFREdge] = "handleBoundingFREdge"; + overlayNames[handleBoundingFLEdge] = "handleBoundingFLEdge"; - overlayNames[handleCloner] = "handleCloner"; + overlayNames[handleDuplicator] = "handleDuplicator"; overlayNames[selectionBox] = "selectionBox"; overlayNames[iconSelectionBox] = "iconSelectionBox"; var activeTool = null; var handleTools = {}; + + var debugPickPlaneEnabled = false; + var debugPickPlane = Overlays.addOverlay("shape", { + shape: "Quad", + alpha: 0.25, + color: COLOR_DEBUG_PICK_PLANE, + solid: true, + visible: false, + ignoreRayIntersection: true, + drawInFront: false + }); + var debugPickPlaneHits = []; // We get mouseMoveEvents from the handControllers, via handControllerPointer. // But we dont' get mousePressEvents. - that.triggerMapping = Controller.newMapping(Script.resolvePath('') + '-click'); - Script.scriptEnding.connect(that.triggerMapping.disable); - that.triggeredHand = NO_TRIGGER_HAND; + that.triggerClickMapping = Controller.newMapping(Script.resolvePath('') + '-click'); + that.triggerPressMapping = Controller.newMapping(Script.resolvePath('') + '-press'); + that.triggeredHand = NO_HAND; + that.pressedHand = NO_HAND; that.triggered = function() { - return that.triggeredHand !== NO_TRIGGER_HAND; + return that.triggeredHand !== NO_HAND; + }; + function pointingAtDesktopWindowOrTablet(hand) { + var pointingAtDesktopWindow = (hand === Controller.Standard.RightHand && + SelectionManager.pointingAtDesktopWindowRight) || + (hand === Controller.Standard.LeftHand && + SelectionManager.pointingAtDesktopWindowLeft); + var pointingAtTablet = (hand === Controller.Standard.RightHand && SelectionManager.pointingAtTabletRight) || + (hand === Controller.Standard.LeftHand && SelectionManager.pointingAtTabletLeft); + return pointingAtDesktopWindow || pointingAtTablet; } function makeClickHandler(hand) { return function (clicked) { @@ -837,26 +1003,39 @@ SelectionDisplay = (function() { if (that.triggered() && hand !== that.triggeredHand) { return; } - if (!that.triggered() && clicked) { - var pointingAtDesktopWindow = (hand === Controller.Standard.RightHand && - SelectionManager.pointingAtDesktopWindowRight) || - (hand === Controller.Standard.LeftHand && - SelectionManager.pointingAtDesktopWindowLeft); - var pointingAtTablet = (hand === Controller.Standard.RightHand && SelectionManager.pointingAtTabletRight) || - (hand === Controller.Standard.LeftHand && SelectionManager.pointingAtTabletLeft); - if (pointingAtDesktopWindow || pointingAtTablet) { - return; - } + if (!that.triggered() && clicked && !pointingAtDesktopWindowOrTablet(hand)) { that.triggeredHand = hand; that.mousePressEvent({}); } else if (that.triggered() && !clicked) { - that.triggeredHand = NO_TRIGGER_HAND; + that.triggeredHand = NO_HAND; that.mouseReleaseEvent({}); } }; } - that.triggerMapping.from(Controller.Standard.RTClick).peek().to(makeClickHandler(Controller.Standard.RightHand)); - that.triggerMapping.from(Controller.Standard.LTClick).peek().to(makeClickHandler(Controller.Standard.LeftHand)); + function makePressHandler(hand) { + return function (value) { + if (value >= TRIGGER_ON_VALUE && !that.triggered() && !pointingAtDesktopWindowOrTablet(hand)) { + that.pressedHand = hand; + that.updateHighlight({}); + } else { + that.pressedHand = NO_HAND; + that.resetPreviousHandleColor(); + } + } + } + that.triggerClickMapping.from(Controller.Standard.RTClick).peek().to(makeClickHandler(Controller.Standard.RightHand)); + that.triggerClickMapping.from(Controller.Standard.LTClick).peek().to(makeClickHandler(Controller.Standard.LeftHand)); + that.triggerPressMapping.from(Controller.Standard.RT).peek().to(makePressHandler(Controller.Standard.RightHand)); + that.triggerPressMapping.from(Controller.Standard.LT).peek().to(makePressHandler(Controller.Standard.LeftHand)); + that.enableTriggerMapping = function() { + that.triggerClickMapping.enable(); + that.triggerPressMapping.enable(); + }; + that.disableTriggerMapping = function() { + that.triggerClickMapping.disable(); + that.triggerPressMapping.disable(); + }; + Script.scriptEnding.connect(that.disableTriggerMapping); // FUNCTION DEF(s): Intersection Check Helpers function testRayIntersect(queryRay, overlayIncludes, overlayExcludes) { @@ -936,6 +1115,7 @@ SelectionDisplay = (function() { var hitTool = handleTools[ hitOverlayID ]; if (hitTool) { activeTool = hitTool; + that.clearDebugPickPlane(); if (activeTool.onBegin) { activeTool.onBegin(event, pickRay, results); } else { @@ -983,35 +1163,10 @@ SelectionDisplay = (function() { } return Uuid.NULL; }; - - // FUNCTION: MOUSE MOVE EVENT - var lastMouseEvent = null; - that.mouseMoveEvent = function(event) { - var wantDebug = false; - if (wantDebug) { - print("=============== eST::MouseMoveEvent BEG ======================="); - } - lastMouseEvent = event; - if (activeTool) { - if (wantDebug) { - print(" Trigger ActiveTool(" + activeTool.mode + ")'s onMove"); - } - activeTool.onMove(event); - - if (wantDebug) { - print(" Trigger SelectionManager::update"); - } - SelectionManager._update(); - - if (wantDebug) { - print("=============== eST::MouseMoveEvent END ======================="); - } - // EARLY EXIT--(Move handled via active tool) - return true; - } - + + that.updateHighlight = function(event) { // if no tool is active, then just look for handles to highlight... - var pickRay = generalComputePickRay(event.x, event.y); + var pickRay = generalComputePickRay(event.x, event.y); var result = Overlays.findRayIntersection(pickRay); var pickedColor; var highlightNeeded = false; @@ -1021,32 +1176,25 @@ SelectionDisplay = (function() { case handleTranslateXCone: case handleTranslateXCylinder: case handleRotatePitchRing: - case handleStretchXSphere: + case handleStretchXCube: pickedColor = COLOR_RED; highlightNeeded = true; break; case handleTranslateYCone: case handleTranslateYCylinder: case handleRotateYawRing: - case handleStretchYSphere: + case handleStretchYCube: pickedColor = COLOR_GREEN; highlightNeeded = true; break; case handleTranslateZCone: case handleTranslateZCylinder: case handleRotateRollRing: - case handleStretchZSphere: + case handleStretchZCube: pickedColor = COLOR_BLUE; highlightNeeded = true; break; - case handleScaleLBNCube: - case handleScaleRBNCube: - case handleScaleLBFCube: - case handleScaleRBFCube: - case handleScaleLTNCube: - case handleScaleRTNCube: - case handleScaleLTFCube: - case handleScaleRTFCube: + case handleScaleCube: pickedColor = COLOR_SCALE_CUBE; highlightNeeded = true; break; @@ -1069,7 +1217,36 @@ SelectionDisplay = (function() { } else { that.resetPreviousHandleColor(); } + }; + // FUNCTION: MOUSE MOVE EVENT + var lastMouseEvent = null; + that.mouseMoveEvent = function(event) { + var wantDebug = false; + if (wantDebug) { + print("=============== eST::MouseMoveEvent BEG ======================="); + } + lastMouseEvent = event; + if (activeTool) { + if (wantDebug) { + print(" Trigger ActiveTool(" + activeTool.mode + ")'s onMove"); + } + activeTool.onMove(event); + + if (wantDebug) { + print(" Trigger SelectionManager::update"); + } + SelectionManager._update(false, that); + + if (wantDebug) { + print("=============== eST::MouseMoveEvent END ======================="); + } + // EARLY EXIT--(Move handled via active tool) + return true; + } + + that.updateHighlight(event); + if (wantDebug) { print("=============== eST::MouseMoveEvent END ======================="); } @@ -1124,7 +1301,7 @@ SelectionDisplay = (function() { lastMouseEvent.isControl = event.isControl; lastMouseEvent.isAlt = event.isAlt; activeTool.onMove(lastMouseEvent); - SelectionManager._update(); + SelectionManager._update(false, this); } }; @@ -1140,7 +1317,7 @@ SelectionDisplay = (function() { lastMouseEvent.isControl = event.isControl; lastMouseEvent.isAlt = event.isAlt; activeTool.onMove(lastMouseEvent); - SelectionManager._update(); + SelectionManager._update(false, this); } }; @@ -1166,8 +1343,9 @@ SelectionDisplay = (function() { }; function controllerComputePickRay() { - var controllerPose = getControllerWorldLocation(that.triggeredHand, true); - if (controllerPose.valid && that.triggered()) { + var hand = that.triggered() ? that.triggeredHand : that.pressedHand; + var controllerPose = getControllerWorldLocation(hand, true); + if (controllerPose.valid) { var controllerPosition = controllerPose.translation; // This gets point direction right, but if you want general quaternion it would be more complicated: var controllerDirection = Quat.getUp(controllerPose.rotation); @@ -1178,6 +1356,12 @@ SelectionDisplay = (function() { function generalComputePickRay(x, y) { return controllerComputePickRay() || Camera.computePickRay(x, y); } + + function getControllerAvatarFramePositionFromPickRay(pickRay) { + var controllerPosition = Vec3.subtract(pickRay.origin, MyAvatar.position); + controllerPosition = Vec3.multiplyQbyV(Quat.inverse(MyAvatar.orientation), controllerPosition); + return controllerPosition; + } function getDistanceToCamera(position) { var cameraPosition = Camera.getPosition(); @@ -1200,6 +1384,7 @@ SelectionDisplay = (function() { for (var i = 0; i < allOverlays.length; i++) { Overlays.deleteOverlay(allOverlays[i]); } + that.clearDebugPickPlane(); }; that.select = function(entityID, event) { @@ -1217,8 +1402,21 @@ SelectionDisplay = (function() { that.updateHandles(); }; + + /** + * This callback is used for spaceMode changes. + * @callback spaceModeChangedCallback + * @param {string} spaceMode + */ + + /** + * set this property with a callback to keep track of spaceMode changes. + * @type {spaceModeChangedCallback} + */ + that.onSpaceModeChange = null; + // FUNCTION: SET SPACE MODE - that.setSpaceMode = function(newSpaceMode) { + that.setSpaceMode = function(newSpaceMode, isDesiredChange) { var wantDebug = false; if (wantDebug) { print("======> SetSpaceMode called. ========"); @@ -1228,7 +1426,15 @@ SelectionDisplay = (function() { if (wantDebug) { print(" Updating SpaceMode From: " + spaceMode + " To: " + newSpaceMode); } + if (isDesiredChange) { + desiredSpaceMode = newSpaceMode; + } spaceMode = newSpaceMode; + + if (that.onSpaceModeChange !== null) { + that.onSpaceModeChange(newSpaceMode); + } + that.updateHandles(); } else if (wantDebug) { print("WARNING: entitySelectionTool.setSpaceMode - Can't update SpaceMode. CurrentMode: " + @@ -1254,14 +1460,36 @@ SelectionDisplay = (function() { if (wantDebug) { print("PreToggle: " + spaceMode); } - spaceMode = (spaceMode === SPACE_LOCAL) ? SPACE_WORLD : SPACE_LOCAL; - that.updateHandles(); + that.setSpaceMode((spaceMode === SPACE_LOCAL) ? SPACE_WORLD : SPACE_LOCAL, true); if (wantDebug) { print("PostToggle: " + spaceMode); print("======== ToggleSpaceMode called. <========="); } }; + /** + * Switches the display mode back to the set desired display mode + */ + that.useDesiredSpaceMode = function() { + var wantDebug = false; + if (wantDebug) { + print("========> UseDesiredSpaceMode called. ========="); + } + that.setSpaceMode(desiredSpaceMode, false); + if (wantDebug) { + print("PostToggle: " + spaceMode); + print("======== UseDesiredSpaceMode called. <========="); + } + }; + + /** + * Get the currently set SpaceMode + * @returns {string} spaceMode + */ + that.getSpaceMode = function() { + return spaceMode; + }; + function addHandleTool(overlay, tool) { handleTools[overlay] = tool; return tool; @@ -1303,6 +1531,7 @@ SelectionDisplay = (function() { if (SelectionManager.selections.length === 0) { that.setOverlaysVisible(false); + that.clearDebugPickPlane(); return; } @@ -1424,127 +1653,56 @@ SelectionDisplay = (function() { dimensions: arrowConeDimensions }); - // UPDATE SCALE CUBES - var scaleCubeOffsetX = SCALE_CUBE_OFFSET * dimensions.x; - var scaleCubeOffsetY = SCALE_CUBE_OFFSET * dimensions.y; - var scaleCubeOffsetZ = SCALE_CUBE_OFFSET * dimensions.z; - var scaleCubeRotation = spaceMode === SPACE_LOCAL ? rotation : Quat.IDENTITY; - var scaleLBNCubePosition = { x: -scaleCubeOffsetX, y: -scaleCubeOffsetY, z: -scaleCubeOffsetZ }; - scaleLBNCubePosition = Vec3.sum(position, Vec3.multiplyQbyV(rotation, scaleLBNCubePosition)); - var scaleLBNCubeToCamera = getDistanceToCamera(scaleLBNCubePosition); - var scaleRBNCubePosition = { x: scaleCubeOffsetX, y: -scaleCubeOffsetY, z: -scaleCubeOffsetZ }; - scaleRBNCubePosition = Vec3.sum(position, Vec3.multiplyQbyV(rotation, scaleRBNCubePosition)); - var scaleRBNCubeToCamera = getDistanceToCamera(scaleRBNCubePosition); - var scaleLBFCubePosition = { x: -scaleCubeOffsetX, y: -scaleCubeOffsetY, z: scaleCubeOffsetZ }; - scaleLBFCubePosition = Vec3.sum(position, Vec3.multiplyQbyV(rotation, scaleLBFCubePosition)); - var scaleLBFCubeToCamera = getDistanceToCamera(scaleLBFCubePosition); - var scaleRBFCubePosition = { x: scaleCubeOffsetX, y: -scaleCubeOffsetY, z: scaleCubeOffsetZ }; - scaleRBFCubePosition = Vec3.sum(position, Vec3.multiplyQbyV(rotation, scaleRBFCubePosition)); - var scaleRBFCubeToCamera = getDistanceToCamera(scaleRBFCubePosition); - var scaleLTNCubePosition = { x: -scaleCubeOffsetX, y: scaleCubeOffsetY, z: -scaleCubeOffsetZ }; - scaleLTNCubePosition = Vec3.sum(position, Vec3.multiplyQbyV(rotation, scaleLTNCubePosition)); - var scaleLTNCubeToCamera = getDistanceToCamera(scaleLTNCubePosition); - var scaleRTNCubePosition = { x: scaleCubeOffsetX, y: scaleCubeOffsetY, z: -scaleCubeOffsetZ }; - scaleRTNCubePosition = Vec3.sum(position, Vec3.multiplyQbyV(rotation, scaleRTNCubePosition)); - var scaleRTNCubeToCamera = getDistanceToCamera(scaleRTNCubePosition); - var scaleLTFCubePosition = { x: -scaleCubeOffsetX, y: scaleCubeOffsetY, z: scaleCubeOffsetZ }; - scaleLTFCubePosition = Vec3.sum(position, Vec3.multiplyQbyV(rotation, scaleLTFCubePosition)); - var scaleLTFCubeToCamera = getDistanceToCamera(scaleLTFCubePosition); - var scaleRTFCubePosition = { x: scaleCubeOffsetX, y: scaleCubeOffsetY, z: scaleCubeOffsetZ }; - scaleRTFCubePosition = Vec3.sum(position, Vec3.multiplyQbyV(rotation, scaleRTFCubePosition)); - var scaleRTFCubeToCamera = getDistanceToCamera(scaleRTFCubePosition); - - var scaleCubeToCamera = Math.min(scaleLBNCubeToCamera, scaleRBNCubeToCamera, scaleLBFCubeToCamera, - scaleRBFCubeToCamera, scaleLTNCubeToCamera, scaleRTNCubeToCamera, - scaleLTFCubeToCamera, scaleRTFCubeToCamera); - var scaleCubeDimension = scaleCubeToCamera * SCALE_CUBE_CAMERA_DISTANCE_MULTIPLE; + // UPDATE SCALE CUBE + var scaleCubeRotation = spaceMode === SPACE_LOCAL ? rotation : Quat.IDENTITY; + var scaleCubeDimension = rotateDimension * SCALE_OVERLAY_CAMERA_DISTANCE_MULTIPLE / + ROTATE_RING_CAMERA_DISTANCE_MULTIPLE; var scaleCubeDimensions = { x: scaleCubeDimension, y: scaleCubeDimension, z: scaleCubeDimension }; - - Overlays.editOverlay(handleScaleLBNCube, { - position: scaleLBNCubePosition, - rotation: scaleCubeRotation, - dimensions: scaleCubeDimensions - }); - Overlays.editOverlay(handleScaleRBNCube, { - position: scaleRBNCubePosition, - rotation: scaleCubeRotation, - dimensions: scaleCubeDimensions - }); - Overlays.editOverlay(handleScaleLBFCube, { - position: scaleLBFCubePosition, - rotation: scaleCubeRotation, - dimensions: scaleCubeDimensions - }); - Overlays.editOverlay(handleScaleRBFCube, { - position: scaleRBFCubePosition, - rotation: scaleCubeRotation, - dimensions: scaleCubeDimensions - }); - Overlays.editOverlay(handleScaleLTNCube, { - position: scaleLTNCubePosition, - rotation: scaleCubeRotation, - dimensions: scaleCubeDimensions - }); - Overlays.editOverlay(handleScaleRTNCube, { - position: scaleRTNCubePosition, - rotation: scaleCubeRotation, - dimensions: scaleCubeDimensions - }); - Overlays.editOverlay(handleScaleLTFCube, { - position: scaleLTFCubePosition, - rotation: scaleCubeRotation, - dimensions: scaleCubeDimensions - }); - Overlays.editOverlay(handleScaleRTFCube, { - position: scaleRTFCubePosition, + Overlays.editOverlay(handleScaleCube, { + position: position, rotation: scaleCubeRotation, dimensions: scaleCubeDimensions }); - // UPDATE SCALE EDGES - Overlays.editOverlay(handleScaleTREdge, { start: scaleRTNCubePosition, end: scaleRTFCubePosition }); - Overlays.editOverlay(handleScaleTLEdge, { start: scaleLTNCubePosition, end: scaleLTFCubePosition }); - Overlays.editOverlay(handleScaleTFEdge, { start: scaleLTFCubePosition, end: scaleRTFCubePosition }); - Overlays.editOverlay(handleScaleTNEdge, { start: scaleLTNCubePosition, end: scaleRTNCubePosition }); - Overlays.editOverlay(handleScaleBREdge, { start: scaleRBNCubePosition, end: scaleRBFCubePosition }); - Overlays.editOverlay(handleScaleBLEdge, { start: scaleLBNCubePosition, end: scaleLBFCubePosition }); - Overlays.editOverlay(handleScaleBFEdge, { start: scaleLBFCubePosition, end: scaleRBFCubePosition }); - Overlays.editOverlay(handleScaleBNEdge, { start: scaleLBNCubePosition, end: scaleRBNCubePosition }); - Overlays.editOverlay(handleScaleNREdge, { start: scaleRTNCubePosition, end: scaleRBNCubePosition }); - Overlays.editOverlay(handleScaleNLEdge, { start: scaleLTNCubePosition, end: scaleLBNCubePosition }); - Overlays.editOverlay(handleScaleFREdge, { start: scaleRTFCubePosition, end: scaleRBFCubePosition }); - Overlays.editOverlay(handleScaleFLEdge, { start: scaleLTFCubePosition, end: scaleLBFCubePosition }); - - // UPDATE STRETCH SPHERES - var stretchSphereDimension = rotateDimension * STRETCH_SPHERE_CAMERA_DISTANCE_MULTIPLE / - ROTATE_RING_CAMERA_DISTANCE_MULTIPLE; - var stretchSphereDimensions = { x: stretchSphereDimension, y: stretchSphereDimension, z: stretchSphereDimension }; - var stretchSphereOffset = rotateDimension * STRETCH_SPHERE_OFFSET / ROTATE_RING_CAMERA_DISTANCE_MULTIPLE; - var stretchXPosition = { x: stretchSphereOffset, y: 0, z: 0 }; - stretchXPosition = Vec3.sum(position, Vec3.multiplyQbyV(rotation, stretchXPosition)); - Overlays.editOverlay(handleStretchXSphere, { - position: stretchXPosition, - dimensions: stretchSphereDimensions - }); - var stretchYPosition = { x: 0, y: stretchSphereOffset, z: 0 }; - stretchYPosition = Vec3.sum(position, Vec3.multiplyQbyV(rotation, stretchYPosition)); - Overlays.editOverlay(handleStretchYSphere, { - position: stretchYPosition, - dimensions: stretchSphereDimensions - }); - var stretchZPosition = { x: 0, y: 0, z: stretchSphereOffset }; - stretchZPosition = Vec3.sum(position, Vec3.multiplyQbyV(rotation, stretchZPosition)); - Overlays.editOverlay(handleStretchZSphere, { - position: stretchZPosition, - dimensions: stretchSphereDimensions - }); - + // UPDATE BOUNDING BOX EDGES + var edgeOffsetX = BOUNDING_EDGE_OFFSET * dimensions.x; + var edgeOffsetY = BOUNDING_EDGE_OFFSET * dimensions.y; + var edgeOffsetZ = BOUNDING_EDGE_OFFSET * dimensions.z; + var LBNPosition = { x: -edgeOffsetX, y: -edgeOffsetY, z: -edgeOffsetZ }; + LBNPosition = Vec3.sum(position, Vec3.multiplyQbyV(rotation, LBNPosition)); + var RBNPosition = { x: edgeOffsetX, y: -edgeOffsetY, z: -edgeOffsetZ }; + RBNPosition = Vec3.sum(position, Vec3.multiplyQbyV(rotation, RBNPosition)); + var LBFPosition = { x: -edgeOffsetX, y: -edgeOffsetY, z: edgeOffsetZ }; + LBFPosition = Vec3.sum(position, Vec3.multiplyQbyV(rotation, LBFPosition)); + var RBFPosition = { x: edgeOffsetX, y: -edgeOffsetY, z: edgeOffsetZ }; + RBFPosition = Vec3.sum(position, Vec3.multiplyQbyV(rotation, RBFPosition)); + var LTNPosition = { x: -edgeOffsetX, y: edgeOffsetY, z: -edgeOffsetZ }; + LTNPosition = Vec3.sum(position, Vec3.multiplyQbyV(rotation, LTNPosition)); + var RTNPosition = { x: edgeOffsetX, y: edgeOffsetY, z: -edgeOffsetZ }; + RTNPosition = Vec3.sum(position, Vec3.multiplyQbyV(rotation, RTNPosition)); + var LTFPosition = { x: -edgeOffsetX, y: edgeOffsetY, z: edgeOffsetZ }; + LTFPosition = Vec3.sum(position, Vec3.multiplyQbyV(rotation, LTFPosition)); + var RTFPosition = { x: edgeOffsetX, y: edgeOffsetY, z: edgeOffsetZ }; + RTFPosition = Vec3.sum(position, Vec3.multiplyQbyV(rotation, RTFPosition)); + Overlays.editOverlay(handleBoundingTREdge, { start: RTNPosition, end: RTFPosition }); + Overlays.editOverlay(handleBoundingTLEdge, { start: LTNPosition, end: LTFPosition }); + Overlays.editOverlay(handleBoundingTFEdge, { start: LTFPosition, end: RTFPosition }); + Overlays.editOverlay(handleBoundingTNEdge, { start: LTNPosition, end: RTNPosition }); + Overlays.editOverlay(handleBoundingBREdge, { start: RBNPosition, end: RBFPosition }); + Overlays.editOverlay(handleBoundingBLEdge, { start: LBNPosition, end: LBFPosition }); + Overlays.editOverlay(handleBoundingBFEdge, { start: LBFPosition, end: RBFPosition }); + Overlays.editOverlay(handleBoundingBNEdge, { start: LBNPosition, end: RBNPosition }); + Overlays.editOverlay(handleBoundingNREdge, { start: RTNPosition, end: RBNPosition }); + Overlays.editOverlay(handleBoundingNLEdge, { start: LTNPosition, end: LBNPosition }); + Overlays.editOverlay(handleBoundingFREdge, { start: RTFPosition, end: RBFPosition }); + Overlays.editOverlay(handleBoundingFLEdge, { start: LTFPosition, end: LBFPosition }); + // UPDATE STRETCH HIGHLIGHT PANELS - var scaleRBFCubePositionRotated = Vec3.multiplyQbyV(rotationInverse, scaleRBFCubePosition); - var scaleRTFCubePositionRotated = Vec3.multiplyQbyV(rotationInverse, scaleRTFCubePosition); - var scaleLTNCubePositionRotated = Vec3.multiplyQbyV(rotationInverse, scaleLTNCubePosition); - var scaleRTNCubePositionRotated = Vec3.multiplyQbyV(rotationInverse, scaleRTNCubePosition); - var stretchPanelXDimensions = Vec3.subtract(scaleRTNCubePositionRotated, scaleRBFCubePositionRotated); + var RBFPositionRotated = Vec3.multiplyQbyV(rotationInverse, RBFPosition); + var RTFPositionRotated = Vec3.multiplyQbyV(rotationInverse, RTFPosition); + var LTNPositionRotated = Vec3.multiplyQbyV(rotationInverse, LTNPosition); + var RTNPositionRotated = Vec3.multiplyQbyV(rotationInverse, RTNPosition); + var stretchPanelXDimensions = Vec3.subtract(RTNPositionRotated, RBFPositionRotated); var tempY = Math.abs(stretchPanelXDimensions.y); stretchPanelXDimensions.x = STRETCH_PANEL_WIDTH; stretchPanelXDimensions.y = Math.abs(stretchPanelXDimensions.z); @@ -1555,7 +1713,7 @@ SelectionDisplay = (function() { rotation: rotationZ, dimensions: stretchPanelXDimensions }); - var stretchPanelYDimensions = Vec3.subtract(scaleLTNCubePositionRotated, scaleRTFCubePositionRotated); + var stretchPanelYDimensions = Vec3.subtract(LTNPositionRotated, RTFPositionRotated); var tempX = Math.abs(stretchPanelYDimensions.x); stretchPanelYDimensions.x = Math.abs(stretchPanelYDimensions.z); stretchPanelYDimensions.y = STRETCH_PANEL_WIDTH; @@ -1566,7 +1724,7 @@ SelectionDisplay = (function() { rotation: rotationY, dimensions: stretchPanelYDimensions }); - var stretchPanelZDimensions = Vec3.subtract(scaleLTNCubePositionRotated, scaleRBFCubePositionRotated); + var stretchPanelZDimensions = Vec3.subtract(LTNPositionRotated, RBFPositionRotated); tempX = Math.abs(stretchPanelZDimensions.x); stretchPanelZDimensions.x = Math.abs(stretchPanelZDimensions.y); stretchPanelZDimensions.y = tempX; @@ -1578,6 +1736,46 @@ SelectionDisplay = (function() { dimensions: stretchPanelZDimensions }); + // UPDATE STRETCH CUBES + var stretchCubeDimension = rotateDimension * STRETCH_CUBE_CAMERA_DISTANCE_MULTIPLE / + ROTATE_RING_CAMERA_DISTANCE_MULTIPLE; + var stretchCubeDimensions = { x: stretchCubeDimension, y: stretchCubeDimension, z: stretchCubeDimension }; + var stretchCubeOffset = rotateDimension * STRETCH_CUBE_OFFSET / ROTATE_RING_CAMERA_DISTANCE_MULTIPLE; + var stretchXPosition, stretchYPosition, stretchZPosition; + if (isActiveTool(handleStretchXCube)) { + stretchXPosition = Vec3.subtract(stretchPanelXPosition, activeStretchCubePanelOffset); + } else { + stretchXPosition = { x: stretchCubeOffset, y: 0, z: 0 }; + stretchXPosition = Vec3.sum(position, Vec3.multiplyQbyV(rotation, stretchXPosition)); + } + if (isActiveTool(handleStretchYCube)) { + stretchYPosition = Vec3.subtract(stretchPanelYPosition, activeStretchCubePanelOffset); + } else { + stretchYPosition = { x: 0, y: stretchCubeOffset, z: 0 }; + stretchYPosition = Vec3.sum(position, Vec3.multiplyQbyV(rotation, stretchYPosition)); + } + if (isActiveTool(handleStretchZCube)) { + stretchZPosition = Vec3.subtract(stretchPanelZPosition, activeStretchCubePanelOffset); + } else { + stretchZPosition = { x: 0, y: 0, z: stretchCubeOffset }; + stretchZPosition = Vec3.sum(position, Vec3.multiplyQbyV(rotation, stretchZPosition)); + } + Overlays.editOverlay(handleStretchXCube, { + position: stretchXPosition, + rotation: rotationX, + dimensions: stretchCubeDimensions + }); + Overlays.editOverlay(handleStretchYCube, { + position: stretchYPosition, + rotation: rotationY, + dimensions: stretchCubeDimensions + }); + Overlays.editOverlay(handleStretchZCube, { + position: stretchZPosition, + rotation: rotationZ, + dimensions: stretchCubeDimensions + }); + // UPDATE SELECTION BOX (CURRENTLY INVISIBLE WITH 0 ALPHA FOR TRANSLATE XZ TOOL) var inModeRotate = isActiveTool(handleRotatePitchRing) || isActiveTool(handleRotateYawRing) || @@ -1597,15 +1795,15 @@ SelectionDisplay = (function() { Overlays.editOverlay(iconSelectionBox, { visible: false }); } - // UPDATE CLONER (CURRENTLY HIDDEN FOR NOW) - var handleClonerOffset = { - x: CLONER_OFFSET.x * dimensions.x, - y: CLONER_OFFSET.y * dimensions.y, - z: CLONER_OFFSET.z * dimensions.z + // UPDATE DUPLICATOR (CURRENTLY HIDDEN FOR NOW) + var handleDuplicatorOffset = { + x: DUPLICATOR_OFFSET.x * dimensions.x, + y: DUPLICATOR_OFFSET.y * dimensions.y, + z: DUPLICATOR_OFFSET.z * dimensions.z }; - var handleClonerPos = Vec3.sum(position, Vec3.multiplyQbyV(rotation, handleClonerOffset)); - Overlays.editOverlay(handleCloner, { - position: handleClonerPos, + var handleDuplicatorPos = Vec3.sum(position, Vec3.multiplyQbyV(rotation, handleDuplicatorOffset)); + Overlays.editOverlay(handleDuplicator, { + position: handleDuplicatorPos, rotation: rotation, dimensions: scaleCubeDimensions }); @@ -1622,26 +1820,21 @@ SelectionDisplay = (function() { that.setHandleRotateRollVisible(!activeTool || isActiveTool(handleRotateRollRing)); var showScaleStretch = !activeTool && SelectionManager.selections.length === 1 && spaceMode === SPACE_LOCAL; - that.setHandleStretchXVisible(showScaleStretch || isActiveTool(handleStretchXSphere)); - that.setHandleStretchYVisible(showScaleStretch || isActiveTool(handleStretchYSphere)); - that.setHandleStretchZVisible(showScaleStretch || isActiveTool(handleStretchZSphere)); - that.setHandleScaleCubeVisible(showScaleStretch || isActiveTool(handleScaleLBNCube) || - isActiveTool(handleScaleRBNCube) || isActiveTool(handleScaleLBFCube) || - isActiveTool(handleScaleRBFCube) || isActiveTool(handleScaleLTNCube) || - isActiveTool(handleScaleRTNCube) || isActiveTool(handleScaleLTFCube) || - isActiveTool(handleScaleRTFCube) || isActiveTool(handleStretchXSphere) || - isActiveTool(handleStretchYSphere) || isActiveTool(handleStretchZSphere)); + that.setHandleStretchXVisible(showScaleStretch || isActiveTool(handleStretchXCube)); + that.setHandleStretchYVisible(showScaleStretch || isActiveTool(handleStretchYCube)); + that.setHandleStretchZVisible(showScaleStretch || isActiveTool(handleStretchZCube)); + that.setHandleScaleVisible(showScaleStretch || isActiveTool(handleScaleCube)); var showOutlineForZone = (SelectionManager.selections.length === 1 && typeof SelectionManager.savedProperties[SelectionManager.selections[0]] !== "undefined" && SelectionManager.savedProperties[SelectionManager.selections[0]].type === "Zone"); - that.setHandleScaleEdgeVisible(showOutlineForZone || (!isActiveTool(handleRotatePitchRing) && + that.setHandleBoundingEdgeVisible(showOutlineForZone || (!isActiveTool(handleRotatePitchRing) && !isActiveTool(handleRotateYawRing) && !isActiveTool(handleRotateRollRing))); - // keep cloner always hidden for now since you can hold Alt to clone while - // translating an entity - we may bring cloner back for HMD only later - // that.setHandleClonerVisible(!activeTool || isActiveTool(handleCloner)); + // keep duplicator always hidden for now since you can hold Alt to duplicate while + // translating an entity - we may bring duplicator back for HMD only later + // that.setHandleDuplicatorVisible(!activeTool || isActiveTool(handleDuplicator)); if (wantDebug) { print("====== Update Handles <======="); @@ -1721,229 +1914,162 @@ SelectionDisplay = (function() { }; that.setHandleStretchXVisible = function(isVisible) { - Overlays.editOverlay(handleStretchXSphere, { visible: isVisible }); + Overlays.editOverlay(handleStretchXCube, { visible: isVisible }); }; that.setHandleStretchYVisible = function(isVisible) { - Overlays.editOverlay(handleStretchYSphere, { visible: isVisible }); + Overlays.editOverlay(handleStretchYCube, { visible: isVisible }); }; that.setHandleStretchZVisible = function(isVisible) { - Overlays.editOverlay(handleStretchZSphere, { visible: isVisible }); + Overlays.editOverlay(handleStretchZCube, { visible: isVisible }); }; // FUNCTION: SET HANDLE SCALE VISIBLE that.setHandleScaleVisible = function(isVisible) { - that.setHandleScaleCubeVisible(isVisible); - that.setHandleScaleEdgeVisible(isVisible); + that.setHandleScaleVisible(isVisible); + that.setHandleBoundingEdgeVisible(isVisible); }; - that.setHandleScaleCubeVisible = function(isVisible) { - Overlays.editOverlay(handleScaleLBNCube, { visible: isVisible }); - Overlays.editOverlay(handleScaleRBNCube, { visible: isVisible }); - Overlays.editOverlay(handleScaleLBFCube, { visible: isVisible }); - Overlays.editOverlay(handleScaleRBFCube, { visible: isVisible }); - Overlays.editOverlay(handleScaleLTNCube, { visible: isVisible }); - Overlays.editOverlay(handleScaleRTNCube, { visible: isVisible }); - Overlays.editOverlay(handleScaleLTFCube, { visible: isVisible }); - Overlays.editOverlay(handleScaleRTFCube, { visible: isVisible }); + that.setHandleScaleVisible = function(isVisible) { + Overlays.editOverlay(handleScaleCube, { visible: isVisible }); }; - that.setHandleScaleEdgeVisible = function(isVisible) { - Overlays.editOverlay(handleScaleTREdge, { visible: isVisible }); - Overlays.editOverlay(handleScaleTLEdge, { visible: isVisible }); - Overlays.editOverlay(handleScaleTFEdge, { visible: isVisible }); - Overlays.editOverlay(handleScaleTNEdge, { visible: isVisible }); - Overlays.editOverlay(handleScaleBREdge, { visible: isVisible }); - Overlays.editOverlay(handleScaleBLEdge, { visible: isVisible }); - Overlays.editOverlay(handleScaleBFEdge, { visible: isVisible }); - Overlays.editOverlay(handleScaleBNEdge, { visible: isVisible }); - Overlays.editOverlay(handleScaleNREdge, { visible: isVisible }); - Overlays.editOverlay(handleScaleNLEdge, { visible: isVisible }); - Overlays.editOverlay(handleScaleFREdge, { visible: isVisible }); - Overlays.editOverlay(handleScaleFLEdge, { visible: isVisible }); + that.setHandleBoundingEdgeVisible = function(isVisible) { + Overlays.editOverlay(handleBoundingTREdge, { visible: isVisible }); + Overlays.editOverlay(handleBoundingTLEdge, { visible: isVisible }); + Overlays.editOverlay(handleBoundingTFEdge, { visible: isVisible }); + Overlays.editOverlay(handleBoundingTNEdge, { visible: isVisible }); + Overlays.editOverlay(handleBoundingBREdge, { visible: isVisible }); + Overlays.editOverlay(handleBoundingBLEdge, { visible: isVisible }); + Overlays.editOverlay(handleBoundingBFEdge, { visible: isVisible }); + Overlays.editOverlay(handleBoundingBNEdge, { visible: isVisible }); + Overlays.editOverlay(handleBoundingNREdge, { visible: isVisible }); + Overlays.editOverlay(handleBoundingNLEdge, { visible: isVisible }); + Overlays.editOverlay(handleBoundingFREdge, { visible: isVisible }); + Overlays.editOverlay(handleBoundingFLEdge, { visible: isVisible }); }; - // FUNCTION: SET HANDLE CLONER VISIBLE - that.setHandleClonerVisible = function(isVisible) { - Overlays.editOverlay(handleCloner, { visible: isVisible }); + // FUNCTION: SET HANDLE DUPLICATOR VISIBLE + that.setHandleDuplicatorVisible = function(isVisible) { + Overlays.editOverlay(handleDuplicator, { visible: isVisible }); }; - // TOOL DEFINITION: TRANSLATE XZ TOOL - var initialXZPick = null; - var isConstrained = false; - var constrainMajorOnly = false; - var startPosition = null; - var duplicatedEntityIDs = null; - var translateXZTool = addHandleTool(selectionBox, { - mode: 'TRANSLATE_XZ', - pickPlanePosition: { x: 0, y: 0, z: 0 }, - greatestDimension: 0.0, - startingDistance: 0.0, - startingElevation: 0.0, - onBegin: function(event, pickRay, pickResult, doClone) { - var wantDebug = false; - if (wantDebug) { - print("================== TRANSLATE_XZ(Beg) -> ======================="); - Vec3.print(" pickRay", pickRay); - Vec3.print(" pickRay.origin", pickRay.origin); - Vec3.print(" pickResult.intersection", pickResult.intersection); - } - - // Duplicate entities if alt is pressed. This will make a - // copy of the selected entities and move the _original_ entities, not - // the new ones. - if (event.isAlt || doClone) { - duplicatedEntityIDs = SelectionManager.duplicateSelection(); - var ids = []; - for (var i = 0; i < duplicatedEntityIDs.length; ++i) { - ids.push(duplicatedEntityIDs[i].entityID); - } - SelectionManager.setSelections(ids); - } else { - duplicatedEntityIDs = null; - } - - SelectionManager.saveProperties(); - that.resetPreviousHandleColor(); - - that.setHandleTranslateVisible(false); - that.setHandleRotateVisible(false); - that.setHandleScaleCubeVisible(false); - that.setHandleStretchVisible(false); - that.setHandleClonerVisible(false); - - startPosition = SelectionManager.worldPosition; - - translateXZTool.pickPlanePosition = pickResult.intersection; - translateXZTool.greatestDimension = Math.max(Math.max(SelectionManager.worldDimensions.x, - SelectionManager.worldDimensions.y), - SelectionManager.worldDimensions.z); - translateXZTool.startingDistance = Vec3.distance(pickRay.origin, SelectionManager.position); - translateXZTool.startingElevation = translateXZTool.elevation(pickRay.origin, translateXZTool.pickPlanePosition); - if (wantDebug) { - print(" longest dimension: " + translateXZTool.greatestDimension); - print(" starting distance: " + translateXZTool.startingDistance); - print(" starting elevation: " + translateXZTool.startingElevation); - } - - initialXZPick = rayPlaneIntersection(pickRay, translateXZTool.pickPlanePosition, { - x: 0, - y: 1, - z: 0 - }); - - isConstrained = false; - if (wantDebug) { - print("================== TRANSLATE_XZ(End) <- ======================="); - } - }, - onEnd: function(event, reason) { - pushCommandForSelections(duplicatedEntityIDs); - if (isConstrained) { - Overlays.editOverlay(xRailOverlay, { - visible: false - }); - Overlays.editOverlay(zRailOverlay, { - visible: false - }); - } - }, - elevation: function(origin, intersection) { - return (origin.y - intersection.y) / Vec3.distance(origin, intersection); - }, - onMove: function(event) { - var wantDebug = false; - var pickRay = generalComputePickRay(event.x, event.y); - - var pick = rayPlaneIntersection2(pickRay, translateXZTool.pickPlanePosition, { - x: 0, - y: 1, - z: 0 - }); - - // If the pick ray doesn't hit the pick plane in this direction, do nothing. - // this will happen when someone drags across the horizon from the side they started on. - if (!pick) { + // FUNCTION: DEBUG PICK PLANE + that.showDebugPickPlane = function(pickPlanePosition, pickPlaneNormal) { + var planePlusNormal = Vec3.sum(pickPlanePosition, pickPlaneNormal); + var rotation = Quat.lookAtSimple(planePlusNormal, pickPlanePosition); + var dimensionXZ = getDistanceToCamera(pickPlanePosition) * 1.25; + var dimensions = { x:dimensionXZ, y:dimensionXZ, z:STRETCH_PANEL_WIDTH }; + Overlays.editOverlay(debugPickPlane, { + position: pickPlanePosition, + rotation: rotation, + dimensions: dimensions, + visible: true + }); + }; + + that.showDebugPickPlaneHit = function(pickHitPosition) { + var dimension = getDistanceToCamera(pickHitPosition) * DEBUG_PICK_PLANE_HIT_CAMERA_DISTANCE_MULTIPLE; + var pickPlaneHit = Overlays.addOverlay("shape", { + alpha: 0.5, + shape: "Sphere", + solid: true, + visible: true, + ignoreRayIntersection: true, + drawInFront: false, + color: COLOR_DEBUG_PICK_PLANE_HIT, + position: pickHitPosition, + dimensions: { x: dimension, y: dimension, z: dimension } + }); + debugPickPlaneHits.push(pickPlaneHit); + if (debugPickPlaneHits.length > DEBUG_PICK_PLANE_HIT_LIMIT) { + var removedPickPlaneHit = debugPickPlaneHits.shift(); + Overlays.deleteOverlay(removedPickPlaneHit); + } + }; + + that.clearDebugPickPlane = function() { + Overlays.editOverlay(debugPickPlane, { visible: false }); + for (var i = 0; i < debugPickPlaneHits.length; i++) { + Overlays.deleteOverlay(debugPickPlaneHits[i]); + } + debugPickPlaneHits = []; + }; + + // TOOL DEFINITION: HANDLE TRANSLATE XZ TOOL + function addHandleTranslateXZTool(overlay, mode, doDuplicate) { + var initialPick = null; + var isConstrained = false; + var constrainMajorOnly = false; + var startPosition = null; + var duplicatedEntityIDs = null; + var pickPlanePosition = null; + var pickPlaneNormal = { x: 0, y: 1, z: 0 }; + var greatestDimension = 0.0; + var startingDistance = 0.0; + var startingElevation = 0.0; + addHandleTool(overlay, { + mode: mode, + onBegin: function(event, pickRay, pickResult) { + var wantDebug = false; if (wantDebug) { - print(" "+ translateXZTool.mode + "Pick ray does not intersect XZ plane."); - } - - // EARLY EXIT--(Invalid ray detected.) - return; - } - - var vector = Vec3.subtract(pick, initialXZPick); - - // If the mouse is too close to the horizon of the pick plane, stop moving - var MIN_ELEVATION = 0.02; // largest dimension of object divided by distance to it - var elevation = translateXZTool.elevation(pickRay.origin, pick); - if (wantDebug) { - print("Start Elevation: " + translateXZTool.startingElevation + ", elevation: " + elevation); - } - if ((translateXZTool.startingElevation > 0.0 && elevation < MIN_ELEVATION) || - (translateXZTool.startingElevation < 0.0 && elevation > -MIN_ELEVATION)) { - if (wantDebug) { - print(" "+ translateXZTool.mode + " - too close to horizon!"); + print("================== TRANSLATE_XZ(Beg) -> ======================="); + Vec3.print(" pickRay", pickRay); + Vec3.print(" pickRay.origin", pickRay.origin); + Vec3.print(" pickResult.intersection", pickResult.intersection); } - // EARLY EXIT--(Don't proceed past the reached limit.) - return; - } - - // If the angular size of the object is too small, stop moving - var MIN_ANGULAR_SIZE = 0.01; // Radians - if (translateXZTool.greatestDimension > 0) { - var angularSize = Math.atan(translateXZTool.greatestDimension / Vec3.distance(pickRay.origin, pick)); - if (wantDebug) { - print("Angular size = " + angularSize); - } - if (angularSize < MIN_ANGULAR_SIZE) { - return; - } - } - - // If shifted, constrain to one axis - if (event.isShifted) { - if (Math.abs(vector.x) > Math.abs(vector.z)) { - vector.z = 0; + // Duplicate entities if alt is pressed. This will make a + // copy of the selected entities and move the _original_ entities, not + // the new ones. + if (event.isAlt || doDuplicate) { + duplicatedEntityIDs = SelectionManager.duplicateSelection(); + var ids = []; + for (var i = 0; i < duplicatedEntityIDs.length; ++i) { + ids.push(duplicatedEntityIDs[i].entityID); + } + SelectionManager.setSelections(ids); } else { - vector.x = 0; + duplicatedEntityIDs = null; } - if (!isConstrained) { - var xStart = Vec3.sum(startPosition, { - x: -RAIL_AXIS_LENGTH, - y: 0, - z: 0 - }); - var xEnd = Vec3.sum(startPosition, { - x: RAIL_AXIS_LENGTH, - y: 0, - z: 0 - }); - var zStart = Vec3.sum(startPosition, { - x: 0, - y: 0, - z: -RAIL_AXIS_LENGTH - }); - var zEnd = Vec3.sum(startPosition, { - x: 0, - y: 0, - z: RAIL_AXIS_LENGTH - }); - Overlays.editOverlay(xRailOverlay, { - start: xStart, - end: xEnd, - visible: true - }); - Overlays.editOverlay(zRailOverlay, { - start: zStart, - end: zEnd, - visible: true - }); - isConstrained = true; + + SelectionManager.saveProperties(); + that.resetPreviousHandleColor(); + + that.setHandleTranslateVisible(false); + that.setHandleRotateVisible(false); + that.setHandleScaleVisible(false); + that.setHandleStretchVisible(false); + that.setHandleDuplicatorVisible(false); + + startPosition = SelectionManager.worldPosition; + pickPlanePosition = pickResult.intersection; + greatestDimension = Math.max(Math.max(SelectionManager.worldDimensions.x, + SelectionManager.worldDimensions.y), + SelectionManager.worldDimensions.z); + startingDistance = Vec3.distance(pickRay.origin, SelectionManager.position); + startingElevation = this.elevation(pickRay.origin, pickPlanePosition); + if (wantDebug) { + print(" longest dimension: " + greatestDimension); + print(" starting distance: " + startingDistance); + print(" starting elevation: " + startingElevation); } - } else { + + initialPick = rayPlaneIntersection(pickRay, pickPlanePosition, pickPlaneNormal); + + if (debugPickPlaneEnabled) { + that.showDebugPickPlane(pickPlanePosition, pickPlaneNormal); + that.showDebugPickPlaneHit(initialPick); + } + + isConstrained = false; + if (wantDebug) { + print("================== TRANSLATE_XZ(End) <- ======================="); + } + }, + onEnd: function(event, reason) { + pushCommandForSelections(duplicatedEntityIDs); if (isConstrained) { Overlays.editOverlay(xRailOverlay, { visible: false @@ -1951,60 +2077,166 @@ SelectionDisplay = (function() { Overlays.editOverlay(zRailOverlay, { visible: false }); - isConstrained = false; } - } + }, + elevation: function(origin, intersection) { + return (origin.y - intersection.y) / Vec3.distance(origin, intersection); + }, + onMove: function(event) { + var wantDebug = false; + var pickRay = generalComputePickRay(event.x, event.y); - constrainMajorOnly = event.isControl; - var negateAndHalve = -0.5; - var cornerPosition = Vec3.sum(startPosition, Vec3.multiply(negateAndHalve, SelectionManager.worldDimensions)); - vector = Vec3.subtract( - grid.snapToGrid(Vec3.sum(cornerPosition, vector), constrainMajorOnly), - cornerPosition); + var newPick = rayPlaneIntersection2(pickRay, pickPlanePosition, pickPlaneNormal); - // editing a parent will cause all the children to automatically follow along, so don't - // edit any entity who has an ancestor in SelectionManager.selections - var toMove = SelectionManager.selections.filter(function (selection) { - if (SelectionManager.selections.indexOf(SelectionManager.savedProperties[selection].parentID) >= 0) { - return false; // a parent is also being moved, so don't issue an edit for this entity - } else { - return true; + // If the pick ray doesn't hit the pick plane in this direction, do nothing. + // this will happen when someone drags across the horizon from the side they started on. + if (!newPick) { + if (wantDebug) { + print(" "+ mode + "Pick ray does not intersect XZ plane."); + } + + // EARLY EXIT--(Invalid ray detected.) + return; } - }); - - for (var i = 0; i < toMove.length; i++) { - var properties = SelectionManager.savedProperties[toMove[i]]; - if (!properties) { - continue; + + if (debugPickPlaneEnabled) { + that.showDebugPickPlaneHit(newPick); } - var newPosition = Vec3.sum(properties.position, { - x: vector.x, - y: 0, - z: vector.z - }); - Entities.editEntity(toMove[i], { - position: newPosition - }); + var vector = Vec3.subtract(newPick, initialPick); + + // If the mouse is too close to the horizon of the pick plane, stop moving + var MIN_ELEVATION = 0.02; // largest dimension of object divided by distance to it + var elevation = this.elevation(pickRay.origin, newPick); if (wantDebug) { - print("translateXZ... "); - Vec3.print(" vector:", vector); - Vec3.print(" newPosition:", properties.position); - Vec3.print(" newPosition:", newPosition); + print("Start Elevation: " + startingElevation + ", elevation: " + elevation); } - } + if ((startingElevation > 0.0 && elevation < MIN_ELEVATION) || + (startingElevation < 0.0 && elevation > -MIN_ELEVATION)) { + if (wantDebug) { + print(" "+ mode + " - too close to horizon!"); + } - SelectionManager._update(); - } - }); + // EARLY EXIT--(Don't proceed past the reached limit.) + return; + } + + // If the angular size of the object is too small, stop moving + var MIN_ANGULAR_SIZE = 0.01; // Radians + if (greatestDimension > 0) { + var angularSize = Math.atan(greatestDimension / Vec3.distance(pickRay.origin, newPick)); + if (wantDebug) { + print("Angular size = " + angularSize); + } + if (angularSize < MIN_ANGULAR_SIZE) { + return; + } + } + + // If shifted, constrain to one axis + if (event.isShifted) { + if (Math.abs(vector.x) > Math.abs(vector.z)) { + vector.z = 0; + } else { + vector.x = 0; + } + if (!isConstrained) { + var xStart = Vec3.sum(startPosition, { + x: -RAIL_AXIS_LENGTH, + y: 0, + z: 0 + }); + var xEnd = Vec3.sum(startPosition, { + x: RAIL_AXIS_LENGTH, + y: 0, + z: 0 + }); + var zStart = Vec3.sum(startPosition, { + x: 0, + y: 0, + z: -RAIL_AXIS_LENGTH + }); + var zEnd = Vec3.sum(startPosition, { + x: 0, + y: 0, + z: RAIL_AXIS_LENGTH + }); + Overlays.editOverlay(xRailOverlay, { + start: xStart, + end: xEnd, + visible: true + }); + Overlays.editOverlay(zRailOverlay, { + start: zStart, + end: zEnd, + visible: true + }); + isConstrained = true; + } + } else { + if (isConstrained) { + Overlays.editOverlay(xRailOverlay, { + visible: false + }); + Overlays.editOverlay(zRailOverlay, { + visible: false + }); + isConstrained = false; + } + } + + constrainMajorOnly = event.isControl; + var negateAndHalve = -0.5; + var cornerPosition = Vec3.sum(startPosition, Vec3.multiply(negateAndHalve, SelectionManager.worldDimensions)); + vector = Vec3.subtract( + grid.snapToGrid(Vec3.sum(cornerPosition, vector), constrainMajorOnly), + cornerPosition); + + // editing a parent will cause all the children to automatically follow along, so don't + // edit any entity who has an ancestor in SelectionManager.selections + var toMove = SelectionManager.selections.filter(function (selection) { + if (SelectionManager.selections.indexOf(SelectionManager.savedProperties[selection].parentID) >= 0) { + return false; // a parent is also being moved, so don't issue an edit for this entity + } else { + return true; + } + }); + + for (var i = 0; i < toMove.length; i++) { + var properties = SelectionManager.savedProperties[toMove[i]]; + if (!properties) { + continue; + } + var newPosition = Vec3.sum(properties.position, { + x: vector.x, + y: 0, + z: vector.z + }); + Entities.editEntity(toMove[i], { + position: newPosition + }); + + if (wantDebug) { + print("translateXZ... "); + Vec3.print(" vector:", vector); + Vec3.print(" newPosition:", properties.position); + Vec3.print(" newPosition:", newPosition); + } + } + + SelectionManager._update(false, this); + } + }); + } // TOOL DEFINITION: HANDLE TRANSLATE TOOL function addHandleTranslateTool(overlay, mode, direction) { - var pickNormal = null; - var lastPick = null; - var initialPosition = null; + var pickPlanePosition = null; + var pickPlaneNormal = null; + var initialPick = null; var projectionVector = null; var previousPickRay = null; + var rotation = null; addHandleTool(overlay, { mode: mode, onBegin: function(event, pickRay, pickResult) { @@ -2030,13 +2262,12 @@ SelectionDisplay = (function() { } else if (direction === TRANSLATE_DIRECTION.Z) { axisVector = { x: 0, y: 0, z: 1 }; } - - var rotation = spaceMode === SPACE_LOCAL ? SelectionManager.localRotation : SelectionManager.worldRotation; + + rotation = spaceMode === SPACE_LOCAL ? SelectionManager.localRotation : SelectionManager.worldRotation; axisVector = Vec3.multiplyQbyV(rotation, axisVector); - pickNormal = Vec3.cross(Vec3.cross(pickRay.direction, axisVector), axisVector); - - lastPick = rayPlaneIntersection(pickRay, SelectionManager.worldPosition, pickNormal); - initialPosition = SelectionManager.worldPosition; + pickPlaneNormal = Vec3.cross(Vec3.cross(pickRay.direction, axisVector), axisVector); + pickPlanePosition = SelectionManager.worldPosition; + initialPick = rayPlaneIntersection(pickRay, pickPlanePosition, pickPlaneNormal); SelectionManager.saveProperties(); that.resetPreviousHandleColor(); @@ -2046,10 +2277,15 @@ SelectionDisplay = (function() { that.setHandleTranslateZVisible(direction === TRANSLATE_DIRECTION.Z); that.setHandleRotateVisible(false); that.setHandleStretchVisible(false); - that.setHandleScaleCubeVisible(false); - that.setHandleClonerVisible(false); + that.setHandleScaleVisible(false); + that.setHandleDuplicatorVisible(false); previousPickRay = pickRay; + + if (debugPickPlaneEnabled) { + that.showDebugPickPlane(pickPlanePosition, pickPlaneNormal); + that.showDebugPickPlaneHit(initialPick); + } }, onEnd: function(event, reason) { pushCommandForSelections(duplicatedEntityIDs); @@ -2058,12 +2294,16 @@ SelectionDisplay = (function() { var pickRay = generalComputePickRay(event.x, event.y); // Use previousPickRay if new pickRay will cause resulting rayPlaneIntersection values to wrap around - if (usePreviousPickRay(pickRay.direction, previousPickRay.direction, pickNormal)) { + if (usePreviousPickRay(pickRay.direction, previousPickRay.direction, pickPlaneNormal)) { pickRay = previousPickRay; } - var newIntersection = rayPlaneIntersection(pickRay, initialPosition, pickNormal); - var vector = Vec3.subtract(newIntersection, lastPick); + var newPick = rayPlaneIntersection(pickRay, pickPlanePosition, pickPlaneNormal); + if (debugPickPlaneEnabled) { + that.showDebugPickPlaneHit(newPick); + } + + var vector = Vec3.subtract(newPick, initialPick); if (direction === TRANSLATE_DIRECTION.X) { projectionVector = { x: 1, y: 0, z: 0 }; @@ -2072,8 +2312,6 @@ SelectionDisplay = (function() { } else if (direction === TRANSLATE_DIRECTION.Z) { projectionVector = { x: 0, y: 0, z: 1 }; } - - var rotation = spaceMode === SPACE_LOCAL ? SelectionManager.localRotation : SelectionManager.worldRotation; projectionVector = Vec3.multiplyQbyV(rotation, projectionVector); var dotVector = Vec3.dot(vector, projectionVector); @@ -2108,374 +2346,307 @@ SelectionDisplay = (function() { previousPickRay = pickRay; - SelectionManager._update(); + SelectionManager._update(false, this); } }); } - // FUNCTION: VEC 3 MULT - var vec3Mult = function(v1, v2) { - return { - x: v1.x * v2.x, - y: v1.y * v2.y, - z: v1.z * v2.z - }; - }; - // TOOL DEFINITION: HANDLE STRETCH TOOL - function makeStretchTool(stretchMode, directionEnum, directionVec, pivot, offset, stretchPanel, scaleHandle) { - var directionFor3DStretch = directionVec; - var distanceFor3DStretch = 0; - var DISTANCE_INFLUENCE_THRESHOLD = 1.2; - - var signs = { - x: directionVec.x < 0 ? -1 : (directionVec.x > 0 ? 1 : 0), - y: directionVec.y < 0 ? -1 : (directionVec.y > 0 ? 1 : 0), - z: directionVec.z < 0 ? -1 : (directionVec.z > 0 ? 1 : 0) - }; - - var mask = { - x: Math.abs(directionVec.x) > 0 ? 1 : 0, - y: Math.abs(directionVec.y) > 0 ? 1 : 0, - z: Math.abs(directionVec.z) > 0 ? 1 : 0 - }; - - var numDimensions = mask.x + mask.y + mask.z; - - var planeNormal = null; - var lastPick = null; - var lastPick3D = null; + function addHandleStretchTool(overlay, mode, directionEnum) { + var initialPick = null; var initialPosition = null; var initialDimensions = null; - var initialProperties = null; - var registrationPoint = null; - var deltaPivot = null; - var deltaPivot3D = null; - var pickRayPosition = null; - var pickRayPosition3D = null; var rotation = null; + var registrationPoint = null; + var pickPlanePosition = null; + var pickPlaneNormal = null; var previousPickRay = null; - - var onBegin = function(event, pickRay, pickResult) { - var properties = Entities.getEntityProperties(SelectionManager.selections[0]); - initialProperties = properties; - rotation = (spaceMode === SPACE_LOCAL) ? properties.rotation : Quat.IDENTITY; - - if (spaceMode === SPACE_LOCAL) { + var directionVector = null; + var axisVector = null; + var signs = null; + var mask = null; + var stretchPanel = null; + var handleStretchCube = null; + var deltaPivot = null; + addHandleTool(overlay, { + mode: mode, + onBegin: function(event, pickRay, pickResult) { + if (directionEnum === STRETCH_DIRECTION.X) { + stretchPanel = handleStretchXPanel; + handleStretchCube = handleStretchXCube; + directionVector = { x: -1, y: 0, z: 0 }; + } else if (directionEnum === STRETCH_DIRECTION.Y) { + stretchPanel = handleStretchYPanel; + handleStretchCube = handleStretchYCube; + directionVector = { x: 0, y: -1, z: 0 }; + } else if (directionEnum === STRETCH_DIRECTION.Z) { + stretchPanel = handleStretchZPanel; + handleStretchCube = handleStretchZCube; + directionVector = { x: 0, y: 0, z: -1 }; + } + rotation = SelectionManager.localRotation; initialPosition = SelectionManager.localPosition; initialDimensions = SelectionManager.localDimensions; registrationPoint = SelectionManager.localRegistrationPoint; - } else { - rotation = SelectionManager.worldRotation; - initialPosition = SelectionManager.worldPosition; - initialDimensions = SelectionManager.worldDimensions; - registrationPoint = SelectionManager.worldRegistrationPoint; - } - - // Modify range of registrationPoint to be [-0.5, 0.5] - var centeredRP = Vec3.subtract(registrationPoint, { - x: 0.5, - y: 0.5, - z: 0.5 - }); - - // Scale pivot to be in the same range as registrationPoint - var scaledPivot = Vec3.multiply(0.5, pivot); - deltaPivot = Vec3.subtract(centeredRP, scaledPivot); - - var scaledOffset = Vec3.multiply(0.5, offset); - - // Offset from the registration point - var offsetRP = Vec3.subtract(scaledOffset, centeredRP); - - // Scaled offset in world coordinates - var scaledOffsetWorld = vec3Mult(initialDimensions, offsetRP); - - pickRayPosition = Vec3.sum(initialPosition, Vec3.multiplyQbyV(rotation, scaledOffsetWorld)); - - if (directionFor3DStretch) { - // pivot, offset and pickPlanePosition for 3D manipulation - var scaledPivot3D = Vec3.multiply(0.5, Vec3.multiply(1.0, directionFor3DStretch)); - deltaPivot3D = Vec3.subtract(centeredRP, scaledPivot3D); - pickRayPosition3D = Vec3.sum(initialPosition, Vec3.multiplyQbyV(rotation, scaledOffsetWorld)); - } - - if (numDimensions === 1) { - if (mask.x === 1) { - planeNormal = { - x: 0, - y: 1, - z: 0 - }; - } else if (mask.y === 1) { - planeNormal = { - x: 1, - y: 0, - z: 0 - }; - } else { - planeNormal = { - x: 0, - y: 1, - z: 0 - }; - } - } else if (numDimensions === 2) { - if (mask.x === 0) { - planeNormal = { - x: 1, - y: 0, - z: 0 - }; - } else if (mask.y === 0) { - planeNormal = { - x: 0, - y: 1, - z: 0 - }; - } else { - planeNormal = { - x: 0, - y: 0, - z: 1 - }; - } - } - - planeNormal = Vec3.multiplyQbyV(rotation, planeNormal); - lastPick = rayPlaneIntersection(pickRay, - pickRayPosition, - planeNormal); - var planeNormal3D = { - x: 0, - y: 0, - z: 0 - }; - if (directionFor3DStretch) { - lastPick3D = rayPlaneIntersection(pickRay, - pickRayPosition3D, - planeNormal3D); - distanceFor3DStretch = Vec3.length(Vec3.subtract(pickRayPosition3D, pickRay.origin)); - } + axisVector = Vec3.multiply(NEGATE_VECTOR, directionVector); + axisVector = Vec3.multiplyQbyV(rotation, axisVector); + + signs = { + x: directionVector.x < 0 ? -1 : (directionVector.x > 0 ? 1 : 0), + y: directionVector.y < 0 ? -1 : (directionVector.y > 0 ? 1 : 0), + z: directionVector.z < 0 ? -1 : (directionVector.z > 0 ? 1 : 0) + }; + mask = { + x: Math.abs(directionVector.x) > 0 ? 1 : 0, + y: Math.abs(directionVector.y) > 0 ? 1 : 0, + z: Math.abs(directionVector.z) > 0 ? 1 : 0 + }; + + var pivot = directionVector; + var offset = Vec3.multiply(directionVector, NEGATE_VECTOR); + + // Modify range of registrationPoint to be [-0.5, 0.5] + var centeredRP = Vec3.subtract(registrationPoint, { + x: 0.5, + y: 0.5, + z: 0.5 + }); - that.setHandleTranslateVisible(false); - that.setHandleRotateVisible(false); - that.setHandleScaleCubeVisible(true); - that.setHandleStretchXVisible(directionEnum === STRETCH_DIRECTION.X); - that.setHandleStretchYVisible(directionEnum === STRETCH_DIRECTION.Y); - that.setHandleStretchZVisible(directionEnum === STRETCH_DIRECTION.Z); - that.setHandleClonerVisible(false); + // Scale pivot to be in the same range as registrationPoint + var scaledPivot = Vec3.multiply(0.5, pivot); + deltaPivot = Vec3.subtract(centeredRP, scaledPivot); + + var scaledOffset = Vec3.multiply(0.5, offset); + + // Offset from the registration point + var offsetRP = Vec3.subtract(scaledOffset, centeredRP); + + // Scaled offset in world coordinates + var scaledOffsetWorld = Vec3.multiplyVbyV(initialDimensions, offsetRP); + + pickPlaneNormal = Vec3.cross(Vec3.cross(pickRay.direction, axisVector), axisVector); + pickPlanePosition = Vec3.sum(initialPosition, Vec3.multiplyQbyV(rotation, scaledOffsetWorld)); + initialPick = rayPlaneIntersection(pickRay, pickPlanePosition, pickPlaneNormal); + + that.setHandleTranslateVisible(false); + that.setHandleRotateVisible(false); + that.setHandleScaleVisible(true); + that.setHandleStretchXVisible(directionEnum === STRETCH_DIRECTION.X); + that.setHandleStretchYVisible(directionEnum === STRETCH_DIRECTION.Y); + that.setHandleStretchZVisible(directionEnum === STRETCH_DIRECTION.Z); + that.setHandleDuplicatorVisible(false); + + SelectionManager.saveProperties(); + that.resetPreviousHandleColor(); + + var collisionToRemove = "myAvatar"; + var properties = Entities.getEntityProperties(SelectionManager.selections[0]); + if (properties.collidesWith.indexOf(collisionToRemove) > -1) { + var newCollidesWith = properties.collidesWith.replace(collisionToRemove, ""); + Entities.editEntity(SelectionManager.selections[0], {collidesWith: newCollidesWith}); + that.replaceCollisionsAfterStretch = true; + } + + if (stretchPanel !== null) { + Overlays.editOverlay(stretchPanel, { visible: true }); + } + var stretchCubePosition = Overlays.getProperty(handleStretchCube, "position"); + var stretchPanelPosition = Overlays.getProperty(stretchPanel, "position"); + activeStretchCubePanelOffset = Vec3.subtract(stretchPanelPosition, stretchCubePosition); + + previousPickRay = pickRay; + + if (debugPickPlaneEnabled) { + that.showDebugPickPlane(pickPlanePosition, pickPlaneNormal); + that.showDebugPickPlaneHit(initialPick); + } + }, + onEnd: function(event, reason) { + if (that.replaceCollisionsAfterStretch) { + var newCollidesWith = SelectionManager.savedProperties[SelectionManager.selections[0]].collidesWith; + Entities.editEntity(SelectionManager.selections[0], {collidesWith: newCollidesWith}); + that.replaceCollisionsAfterStretch = false; + } + + if (stretchPanel !== null) { + Overlays.editOverlay(stretchPanel, { visible: false }); + } + activeStretchCubePanelOffset = null; + + pushCommandForSelections(); + }, + onMove: function(event) { + var pickRay = generalComputePickRay(event.x, event.y); + + // Use previousPickRay if new pickRay will cause resulting rayPlaneIntersection values to wrap around + if (usePreviousPickRay(pickRay.direction, previousPickRay.direction, pickPlaneNormal)) { + pickRay = previousPickRay; + } + + var newPick = rayPlaneIntersection(pickRay, pickPlanePosition, pickPlaneNormal); + if (debugPickPlaneEnabled) { + that.showDebugPickPlaneHit(newPick); + } + + var changeInDimensions = Vec3.subtract(newPick, initialPick); + var dotVector = Vec3.dot(changeInDimensions, axisVector); + changeInDimensions = Vec3.multiply(dotVector, axisVector); + changeInDimensions = Vec3.multiplyQbyV(Quat.inverse(rotation), changeInDimensions); + changeInDimensions = Vec3.multiplyVbyV(mask, changeInDimensions); + changeInDimensions = grid.snapToSpacing(changeInDimensions); + changeInDimensions = Vec3.multiply(NEGATE_VECTOR, Vec3.multiplyVbyV(signs, changeInDimensions)); + + var newDimensions = Vec3.sum(initialDimensions, changeInDimensions); + + var minimumDimension = STRETCH_MINIMUM_DIMENSION; + if (newDimensions.x < minimumDimension) { + newDimensions.x = minimumDimension; + changeInDimensions.x = minimumDimension - initialDimensions.x; + } + if (newDimensions.y < minimumDimension) { + newDimensions.y = minimumDimension; + changeInDimensions.y = minimumDimension - initialDimensions.y; + } + if (newDimensions.z < minimumDimension) { + newDimensions.z = minimumDimension; + changeInDimensions.z = minimumDimension - initialDimensions.z; + } + + var changeInPosition = Vec3.multiplyQbyV(rotation, Vec3.multiplyVbyV(deltaPivot, changeInDimensions)); + var newPosition = Vec3.sum(initialPosition, changeInPosition); - SelectionManager.saveProperties(); - that.resetPreviousHandleColor(); - - if (stretchPanel !== null) { - Overlays.editOverlay(stretchPanel, { visible: true }); - } - if (scaleHandle !== null) { - Overlays.editOverlay(scaleHandle, { color: COLOR_SCALE_CUBE_SELECTED }); - } - - var collisionToRemove = "myAvatar"; - if (properties.collidesWith.indexOf(collisionToRemove) > -1) { - var newCollidesWith = properties.collidesWith.replace(collisionToRemove, ""); - Entities.editEntity(SelectionManager.selections[0], {collidesWith: newCollidesWith}); - that.replaceCollisionsAfterStretch = true; - } - - previousPickRay = pickRay; - }; - - var onEnd = function(event, reason) { - if (stretchPanel !== null) { - Overlays.editOverlay(stretchPanel, { visible: false }); - } - if (scaleHandle !== null) { - Overlays.editOverlay(scaleHandle, { color: COLOR_SCALE_CUBE }); - } - - if (that.replaceCollisionsAfterStretch) { - var newCollidesWith = SelectionManager.savedProperties[SelectionManager.selections[0]].collidesWith; - Entities.editEntity(SelectionManager.selections[0], {collidesWith: newCollidesWith}); - that.replaceCollisionsAfterStretch = false; - } - - pushCommandForSelections(); - }; - - var onMove = function(event) { - var proportional = directionEnum === STRETCH_DIRECTION.ALL; - - var position, rotation; - if (spaceMode === SPACE_LOCAL) { - position = SelectionManager.localPosition; - rotation = SelectionManager.localRotation; - } else { - position = SelectionManager.worldPosition; - rotation = SelectionManager.worldRotation; - } - - var localDeltaPivot = deltaPivot; - var localSigns = signs; - var pickRay = generalComputePickRay(event.x, event.y); - - // Use previousPickRay if new pickRay will cause resulting rayPlaneIntersection values to wrap around - if (usePreviousPickRay(pickRay.direction, previousPickRay.direction, planeNormal)) { - pickRay = previousPickRay; - } - - // Are we using handControllers or Mouse - only relevant for 3D tools - var controllerPose = getControllerWorldLocation(that.triggeredHand, true); - var vector = null; - var newPick = null; - if (HMD.isHMDAvailable() && HMD.isHandControllerAvailable() && - controllerPose.valid && that.triggered() && directionFor3DStretch) { - localDeltaPivot = deltaPivot3D; - newPick = pickRay.origin; - vector = Vec3.subtract(newPick, lastPick3D); - vector = Vec3.multiplyQbyV(Quat.inverse(rotation), vector); - if (distanceFor3DStretch > DISTANCE_INFLUENCE_THRESHOLD) { - // Range of Motion - vector = Vec3.multiply(distanceFor3DStretch , vector); + Entities.editEntity(SelectionManager.selections[0], { + position: newPosition, + dimensions: newDimensions + }); + + var wantDebug = false; + if (wantDebug) { + print(mode); + Vec3.print(" changeInDimensions:", changeInDimensions); + Vec3.print(" newDimensions:", newDimensions); + Vec3.print(" changeInPosition:", changeInPosition); + Vec3.print(" newPosition:", newPosition); } - localSigns = directionFor3DStretch; - } else { - newPick = rayPlaneIntersection(pickRay, pickRayPosition, planeNormal); - vector = Vec3.subtract(newPick, lastPick); - vector = Vec3.multiplyQbyV(Quat.inverse(rotation), vector); - vector = vec3Mult(mask, vector); - } - - vector = grid.snapToSpacing(vector); - - var changeInDimensions = Vec3.multiply(NEGATE_VECTOR, vec3Mult(localSigns, vector)); - if (directionEnum === STRETCH_DIRECTION.ALL) { - var toCameraDistance = getDistanceToCamera(position); - var dimensionsMultiple = toCameraDistance * STRETCH_DIRECTION_ALL_CAMERA_DISTANCE_MULTIPLE; - changeInDimensions = Vec3.multiply(changeInDimensions, dimensionsMultiple); - } - - var newDimensions; - if (proportional) { - var absoluteX = Math.abs(changeInDimensions.x); - var absoluteY = Math.abs(changeInDimensions.y); - var absoluteZ = Math.abs(changeInDimensions.z); - var percentChange = 0; - if (absoluteX > absoluteY && absoluteX > absoluteZ) { - percentChange = changeInDimensions.x / initialProperties.dimensions.x; - percentChange = changeInDimensions.x / initialDimensions.x; - } else if (absoluteY > absoluteZ) { - percentChange = changeInDimensions.y / initialProperties.dimensions.y; - percentChange = changeInDimensions.y / initialDimensions.y; - } else { - percentChange = changeInDimensions.z / initialProperties.dimensions.z; - percentChange = changeInDimensions.z / initialDimensions.z; - } - percentChange += 1.0; - newDimensions = Vec3.multiply(percentChange, initialDimensions); - } else { - newDimensions = Vec3.sum(initialDimensions, changeInDimensions); - } - - var minimumDimension = directionEnum === - STRETCH_DIRECTION.ALL ? STRETCH_ALL_MINIMUM_DIMENSION : STRETCH_MINIMUM_DIMENSION; - if (newDimensions.x < minimumDimension) { - newDimensions.x = minimumDimension; - changeInDimensions.x = minimumDimension - initialDimensions.x; - } - if (newDimensions.y < minimumDimension) { - newDimensions.y = minimumDimension; - changeInDimensions.y = minimumDimension - initialDimensions.y; - } - if (newDimensions.z < minimumDimension) { - newDimensions.z = minimumDimension; - changeInDimensions.z = minimumDimension - initialDimensions.z; - } - - var changeInPosition = Vec3.multiplyQbyV(rotation, vec3Mult(localDeltaPivot, changeInDimensions)); - if (directionEnum === STRETCH_DIRECTION.ALL) { - changeInPosition = { x: 0, y: 0, z: 0 }; - } - var newPosition = Vec3.sum(initialPosition, changeInPosition); - - Entities.editEntity(SelectionManager.selections[0], { - position: newPosition, - dimensions: newDimensions - }); - var wantDebug = false; - if (wantDebug) { - print(stretchMode); - Vec3.print(" vector:", vector); - Vec3.print(" changeInDimensions:", changeInDimensions); - Vec3.print(" newDimensions:", newDimensions); - Vec3.print(" changeInPosition:", changeInPosition); - Vec3.print(" newPosition:", newPosition); + previousPickRay = pickRay; + + SelectionManager._update(false, this); } - - previousPickRay = pickRay; - - SelectionManager._update(); - };// End of onMove def - - return { - mode: stretchMode, - onBegin: onBegin, - onMove: onMove, - onEnd: onEnd - }; - } - - function addHandleStretchTool(overlay, mode, directionEnum) { - var directionVector, offset, stretchPanel; - if (directionEnum === STRETCH_DIRECTION.X) { - stretchPanel = handleStretchXPanel; - directionVector = { x: -1, y: 0, z: 0 }; - } else if (directionEnum === STRETCH_DIRECTION.Y) { - stretchPanel = handleStretchYPanel; - directionVector = { x: 0, y: -1, z: 0 }; - } else if (directionEnum === STRETCH_DIRECTION.Z) { - stretchPanel = handleStretchZPanel; - directionVector = { x: 0, y: 0, z: -1 }; - } - offset = Vec3.multiply(directionVector, NEGATE_VECTOR); - var tool = makeStretchTool(mode, directionEnum, directionVector, directionVector, offset, stretchPanel, null); - return addHandleTool(overlay, tool); + }); } // TOOL DEFINITION: HANDLE SCALE TOOL - function addHandleScaleTool(overlay, mode, directionEnum) { - var directionVector, offset, selectedHandle; - if (directionEnum === SCALE_DIRECTION.LBN) { - directionVector = { x: 1, y: 1, z: 1 }; - selectedHandle = handleScaleLBNCube; - } else if (directionEnum === SCALE_DIRECTION.RBN) { - directionVector = { x: -1, y: 1, z: 1 }; - selectedHandle = handleScaleRBNCube; - } else if (directionEnum === SCALE_DIRECTION.LBF) { - directionVector = { x: 1, y: 1, z: -1 }; - selectedHandle = handleScaleLBFCube; - } else if (directionEnum === SCALE_DIRECTION.RBF) { - directionVector = { x: -1, y: 1, z: -1 }; - selectedHandle = handleScaleRBFCube; - } else if (directionEnum === SCALE_DIRECTION.LTN) { - directionVector = { x: 1, y: -1, z: 1 }; - selectedHandle = handleScaleLTNCube; - } else if (directionEnum === SCALE_DIRECTION.RTN) { - directionVector = { x: -1, y: -1, z: 1 }; - selectedHandle = handleScaleRTNCube; - } else if (directionEnum === SCALE_DIRECTION.LTF) { - directionVector = { x: 1, y: -1, z: -1 }; - selectedHandle = handleScaleLTFCube; - } else if (directionEnum === SCALE_DIRECTION.RTF) { - directionVector = { x: -1, y: -1, z: -1 }; - selectedHandle = handleScaleRTFCube; - } - offset = Vec3.multiply(directionVector, NEGATE_VECTOR); - var tool = makeStretchTool(mode, STRETCH_DIRECTION.ALL, directionVector, directionVector, offset, null, selectedHandle); - return addHandleTool(overlay, tool); + function addHandleScaleTool(overlay, mode) { + var initialPick = null; + var initialPosition = null; + var initialDimensions = null; + var pickPlanePosition = null; + var pickPlaneNormal = null; + var previousPickRay = null; + addHandleTool(overlay, { + mode: mode, + onBegin: function(event, pickRay, pickResult) { + initialPosition = SelectionManager.localPosition; + initialDimensions = SelectionManager.localDimensions; + + pickPlanePosition = initialPosition; + pickPlaneNormal = Vec3.subtract(pickRay.origin, pickPlanePosition); + initialPick = rayPlaneIntersection(pickRay, pickPlanePosition, pickPlaneNormal); + + that.setHandleTranslateVisible(false); + that.setHandleRotateVisible(false); + that.setHandleScaleVisible(true); + that.setHandleStretchVisible(false); + that.setHandleDuplicatorVisible(false); + + SelectionManager.saveProperties(); + that.resetPreviousHandleColor(); + + var collisionToRemove = "myAvatar"; + var properties = Entities.getEntityProperties(SelectionManager.selections[0]); + if (properties.collidesWith.indexOf(collisionToRemove) > -1) { + var newCollidesWith = properties.collidesWith.replace(collisionToRemove, ""); + Entities.editEntity(SelectionManager.selections[0], {collidesWith: newCollidesWith}); + that.replaceCollisionsAfterStretch = true; + } + + previousPickRay = pickRay; + + if (debugPickPlaneEnabled) { + that.showDebugPickPlane(pickPlanePosition, pickPlaneNormal); + that.showDebugPickPlaneHit(initialPick); + } + }, + onEnd: function(event, reason) { + if (that.replaceCollisionsAfterStretch) { + var newCollidesWith = SelectionManager.savedProperties[SelectionManager.selections[0]].collidesWith; + Entities.editEntity(SelectionManager.selections[0], {collidesWith: newCollidesWith}); + that.replaceCollisionsAfterStretch = false; + } + + pushCommandForSelections(); + }, + onMove: function(event) { + var pickRay = generalComputePickRay(event.x, event.y); + + // Use previousPickRay if new pickRay will cause resulting rayPlaneIntersection values to wrap around + if (usePreviousPickRay(pickRay.direction, previousPickRay.direction, pickPlaneNormal)) { + pickRay = previousPickRay; + } + + var newPick = rayPlaneIntersection(pickRay, pickPlanePosition, pickPlaneNormal); + if (debugPickPlaneEnabled) { + that.showDebugPickPlaneHit(newPick); + } + + var toCameraDistance = getDistanceToCamera(initialPosition); + var dimensionsMultiple = toCameraDistance * SCALE_DIMENSIONS_CAMERA_DISTANCE_MULTIPLE; + var changeInDimensions = Vec3.subtract(newPick, initialPick); + changeInDimensions = Vec3.multiplyQbyV(Quat.inverse(Camera.orientation), changeInDimensions); + changeInDimensions = grid.snapToSpacing(changeInDimensions); + changeInDimensions = Vec3.multiply(changeInDimensions, dimensionsMultiple); + + var averageDimensionChange = (changeInDimensions.x + changeInDimensions.y + changeInDimensions.z) / 3; + var averageInitialDimension = (initialDimensions.x + initialDimensions.y + initialDimensions.z) / 3; + percentChange = averageDimensionChange / averageInitialDimension; + percentChange += 1.0; + + var newDimensions = Vec3.multiply(percentChange, initialDimensions); + newDimensions.x = Math.abs(newDimensions.x); + newDimensions.y = Math.abs(newDimensions.y); + newDimensions.z = Math.abs(newDimensions.z); + + var minimumDimension = SCALE_MINIMUM_DIMENSION; + if (newDimensions.x < minimumDimension) { + newDimensions.x = minimumDimension; + changeInDimensions.x = minimumDimension - initialDimensions.x; + } + if (newDimensions.y < minimumDimension) { + newDimensions.y = minimumDimension; + changeInDimensions.y = minimumDimension - initialDimensions.y; + } + if (newDimensions.z < minimumDimension) { + newDimensions.z = minimumDimension; + changeInDimensions.z = minimumDimension - initialDimensions.z; + } + + Entities.editEntity(SelectionManager.selections[0], { dimensions: newDimensions }); + + var wantDebug = false; + if (wantDebug) { + print(mode); + Vec3.print(" changeInDimensions:", changeInDimensions); + Vec3.print(" newDimensions:", newDimensions); + } + + previousPickRay = pickRay; + + SelectionManager._update(false, this); + } + }); } // FUNCTION: UPDATE ROTATION DEGREES OVERLAY @@ -2540,8 +2711,11 @@ SelectionDisplay = (function() { function addHandleRotateTool(overlay, mode, direction) { var selectedHandle = null; var worldRotation = null; - var rotationCenter = null; var initialRotation = null; + var rotationCenter = null; + var rotationNormal = null; + var rotationZero = null; + var rotationDegreesPosition = null; addHandleTool(overlay, { mode: mode, onBegin: function(event, pickRay, pickResult) { @@ -2549,18 +2723,7 @@ SelectionDisplay = (function() { if (wantDebug) { print("================== " + getMode() + "(addHandleRotateTool onBegin) -> ======================="); } - - SelectionManager.saveProperties(); - that.resetPreviousHandleColor(); - - that.setHandleTranslateVisible(false); - that.setHandleRotatePitchVisible(direction === ROTATE_DIRECTION.PITCH); - that.setHandleRotateYawVisible(direction === ROTATE_DIRECTION.YAW); - that.setHandleRotateRollVisible(direction === ROTATE_DIRECTION.ROLL); - that.setHandleStretchVisible(false); - that.setHandleScaleCubeVisible(false); - that.setHandleClonerVisible(false); - + if (direction === ROTATE_DIRECTION.PITCH) { rotationNormal = { x: 1, y: 0, z: 0 }; worldRotation = worldRotationY; @@ -2574,6 +2737,21 @@ SelectionDisplay = (function() { worldRotation = worldRotationX; selectedHandle = handleRotateRollRing; } + + initialRotation = spaceMode === SPACE_LOCAL ? SelectionManager.localRotation : SelectionManager.worldRotation; + rotationNormal = Vec3.multiplyQbyV(initialRotation, rotationNormal); + rotationCenter = SelectionManager.worldPosition; + + SelectionManager.saveProperties(); + that.resetPreviousHandleColor(); + + that.setHandleTranslateVisible(false); + that.setHandleRotatePitchVisible(direction === ROTATE_DIRECTION.PITCH); + that.setHandleRotateYawVisible(direction === ROTATE_DIRECTION.YAW); + that.setHandleRotateRollVisible(direction === ROTATE_DIRECTION.ROLL); + that.setHandleStretchVisible(false); + that.setHandleScaleVisible(false); + that.setHandleDuplicatorVisible(false); Overlays.editOverlay(selectedHandle, { hasTickMarks: true, @@ -2581,11 +2759,6 @@ SelectionDisplay = (function() { innerRadius: ROTATE_RING_SELECTED_INNER_RADIUS }); - initialRotation = spaceMode === SPACE_LOCAL ? SelectionManager.localRotation : SelectionManager.worldRotation; - rotationNormal = Vec3.multiplyQbyV(initialRotation, rotationNormal); - - rotationCenter = SelectionManager.worldPosition; - Overlays.editOverlay(rotationDegreesDisplay, { visible: true }); Overlays.editOverlay(handleRotateCurrentRing, { position: rotationCenter, @@ -2597,16 +2770,21 @@ SelectionDisplay = (function() { // editOverlays may not have committed rotation changes. // Compute zero position based on where the overlay will be eventually. - var result = rayPlaneIntersection(pickRay, rotationCenter, rotationNormal); + var initialPick = rayPlaneIntersection(pickRay, rotationCenter, rotationNormal); // In case of a parallel ray, this will be null, which will cause early-out // in the onMove helper. - rotationZero = result; + rotationZero = initialPick; var rotationCenterToZero = Vec3.subtract(rotationZero, rotationCenter); var rotationCenterToZeroLength = Vec3.length(rotationCenterToZero); rotationDegreesPosition = Vec3.sum(rotationCenter, Vec3.multiply(Vec3.normalize(rotationCenterToZero), rotationCenterToZeroLength * ROTATE_DISPLAY_DISTANCE_MULTIPLIER)); updateRotationDegreesOverlay(0, rotationDegreesPosition); + + if (debugPickPlaneEnabled) { + that.showDebugPickPlane(rotationCenter, rotationNormal); + that.showDebugPickPlaneHit(initialPick); + } if (wantDebug) { print("================== " + getMode() + "(addHandleRotateTool onBegin) <- ======================="); @@ -2668,6 +2846,10 @@ SelectionDisplay = (function() { updateSelectionsRotation(rotationChange, rotationCenter); updateRotationDegreesOverlay(-angleFromZero, rotationDegreesPosition); + if (direction === ROTATE_DIRECTION.YAW) { + angleFromZero *= -1; + } + var startAtCurrent = 0; var endAtCurrent = angleFromZero; var maxDegrees = 360; @@ -2679,17 +2861,9 @@ SelectionDisplay = (function() { startAt: startAtCurrent, endAt: endAtCurrent }); - - // not sure why but this seems to be needed to fix an reverse rotation for yaw ring only - if (direction === ROTATE_DIRECTION.YAW) { - if (spaceMode === SPACE_LOCAL) { - Overlays.editOverlay(handleRotateCurrentRing, { rotation: worldRotationZ }); - } else { - var rotationDegrees = 90; - Overlays.editOverlay(handleRotateCurrentRing, { - rotation: Quat.fromPitchYawRollDegrees(-rotationDegrees, 0, 0) - }); - } + + if (debugPickPlaneEnabled) { + that.showDebugPickPlaneHit(result); } } @@ -2698,43 +2872,11 @@ SelectionDisplay = (function() { } } }); - } + } - // TOOL DEFINITION: HANDLE CLONER - addHandleTool(handleCloner, { - mode: "CLONE", - onBegin: function(event, pickRay, pickResult) { - var doClone = true; - translateXZTool.onBegin(event,pickRay,pickResult,doClone); - }, - elevation: function (event) { - translateXZTool.elevation(event); - }, - - onEnd: function (event) { - translateXZTool.onEnd(event); - }, - - onMove: function (event) { - translateXZTool.onMove(event); - } - }); - - addHandleTool(iconSelectionBox, { - mode: "TRANSLATE_XZ", - onBegin: function (event, pickRay, pickResult) { - translateXZTool.onBegin(event, pickRay, pickResult, false); - }, - elevation: function (event) { - translateXZTool.elevation(event); - }, - onEnd: function (event) { - translateXZTool.onEnd(event); - }, - onMove: function (event) { - translateXZTool.onMove(event); - } - }); + addHandleTranslateXZTool(selectionBox, "TRANSLATE_XZ", false); + addHandleTranslateXZTool(iconSelectionBox, "TRANSLATE_XZ", false); + addHandleTranslateXZTool(handleDuplicator, "DUPLICATE", true); addHandleTranslateTool(handleTranslateXCone, "TRANSLATE_X", TRANSLATE_DIRECTION.X); addHandleTranslateTool(handleTranslateXCylinder, "TRANSLATE_X", TRANSLATE_DIRECTION.X); @@ -2747,18 +2889,11 @@ SelectionDisplay = (function() { addHandleRotateTool(handleRotateYawRing, "ROTATE_YAW", ROTATE_DIRECTION.YAW); addHandleRotateTool(handleRotateRollRing, "ROTATE_ROLL", ROTATE_DIRECTION.ROLL); - addHandleStretchTool(handleStretchXSphere, "STRETCH_X", STRETCH_DIRECTION.X); - addHandleStretchTool(handleStretchYSphere, "STRETCH_Y", STRETCH_DIRECTION.Y); - addHandleStretchTool(handleStretchZSphere, "STRETCH_Z", STRETCH_DIRECTION.Z); - - addHandleScaleTool(handleScaleLBNCube, "SCALE_LBN", SCALE_DIRECTION.LBN); - addHandleScaleTool(handleScaleRBNCube, "SCALE_RBN", SCALE_DIRECTION.RBN); - addHandleScaleTool(handleScaleLBFCube, "SCALE_LBF", SCALE_DIRECTION.LBF); - addHandleScaleTool(handleScaleRBFCube, "SCALE_RBF", SCALE_DIRECTION.RBF); - addHandleScaleTool(handleScaleLTNCube, "SCALE_LTN", SCALE_DIRECTION.LTN); - addHandleScaleTool(handleScaleRTNCube, "SCALE_RTN", SCALE_DIRECTION.RTN); - addHandleScaleTool(handleScaleLTFCube, "SCALE_LTF", SCALE_DIRECTION.LTF); - addHandleScaleTool(handleScaleRTFCube, "SCALE_RTF", SCALE_DIRECTION.RTF); + addHandleStretchTool(handleStretchXCube, "STRETCH_X", STRETCH_DIRECTION.X); + addHandleStretchTool(handleStretchYCube, "STRETCH_Y", STRETCH_DIRECTION.Y); + addHandleStretchTool(handleStretchZCube, "STRETCH_Z", STRETCH_DIRECTION.Z); + addHandleScaleTool(handleScaleCube, "SCALE"); + return that; }()); diff --git a/scripts/system/libraries/globals.js b/scripts/system/libraries/globals.js index 0c382314c9..b51a905e0a 100644 --- a/scripts/system/libraries/globals.js +++ b/scripts/system/libraries/globals.js @@ -9,3 +9,5 @@ // HIFI_PUBLIC_BUCKET = "http://s3.amazonaws.com/hifi-public/"; + +isInterstitialOverlaysVisible = false; diff --git a/scripts/system/libraries/gridTool.js b/scripts/system/libraries/gridTool.js index 3a114f23c7..1fd3cbfbaa 100644 --- a/scripts/system/libraries/gridTool.js +++ b/scripts/system/libraries/gridTool.js @@ -1,3 +1,5 @@ +/* global keyUpEventFromUIWindow */ + var GRID_CONTROLS_HTML_URL = Script.resolvePath('../html/gridControls.html'); Grid = function() { @@ -267,28 +269,29 @@ GridTool = function(opts) { try { data = JSON.parse(data); } catch (e) { - print("gridTool.js: Error parsing JSON: " + e.name + " data " + data); return; } - if (data.type == "init") { + if (data.type === "init") { horizontalGrid.emitUpdate(); - } else if (data.type == "update") { + } else if (data.type === "update") { horizontalGrid.update(data); for (var i = 0; i < listeners.length; i++) { listeners[i] && listeners[i](data); } - } else if (data.type == "action") { + } else if (data.type === "action") { var action = data.action; - if (action == "moveToAvatar") { + if (action === "moveToAvatar") { var position = MyAvatar.getJointPosition("LeftFoot"); - if (position.x == 0 && position.y == 0 && position.z == 0) { + if (position.x === 0 && position.y === 0 && position.z === 0) { position = MyAvatar.position; } horizontalGrid.setPosition(position); - } else if (action == "moveToSelection") { + } else if (action === "moveToSelection") { horizontalGrid.moveToSelection(); } + } else if (data.type === 'keyUpEvent') { + keyUpEventFromUIWindow(data.keyUpEvent); } }; diff --git a/scripts/system/libraries/utils.js b/scripts/system/libraries/utils.js index f7b5f6db8d..c0a52a45c6 100644 --- a/scripts/system/libraries/utils.js +++ b/scripts/system/libraries/utils.js @@ -102,7 +102,6 @@ getEntityUserData = function(id) { results = JSON.parse(properties.userData); } catch(err) { logDebug(err); - logDebug(properties.userData); } } return results ? results : {}; @@ -356,13 +355,14 @@ getTabletWidthFromSettings = function () { var DEFAULT_TABLET_WIDTH = 0.4375; var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); var toolbarMode = tablet.toolbarMode; - var DEFAULT_TABLET_SCALE = 70; - var tabletScalePercentage = DEFAULT_TABLET_SCALE; + var DEFAULT_DESKTOP_TABLET_SCALE = 75; + var DEFAULT_HMD_TABLET_SCALE = 60; + var tabletScalePercentage = DEFAULT_HMD_TABLET_SCALE; if (!toolbarMode) { if (HMD.active) { - tabletScalePercentage = Settings.getValue("hmdTabletScale") || DEFAULT_TABLET_SCALE; + tabletScalePercentage = Settings.getValue("hmdTabletScale") || DEFAULT_HMD_TABLET_SCALE; } else { - tabletScalePercentage = Settings.getValue("desktopTabletScale") || DEFAULT_TABLET_SCALE; + tabletScalePercentage = Settings.getValue("desktopTabletScale") || DEFAULT_DESKTOP_TABLET_SCALE; } } return DEFAULT_TABLET_WIDTH * (tabletScalePercentage / 100); diff --git a/scripts/system/makeUserConnection.js b/scripts/system/makeUserConnection.js index 5dee36d147..d205d368dd 100644 --- a/scripts/system/makeUserConnection.js +++ b/scripts/system/makeUserConnection.js @@ -32,7 +32,7 @@ var WAITING_INTERVAL = 100; // ms var CONNECTING_INTERVAL = 100; // ms var MAKING_CONNECTION_TIMEOUT = 800; // ms - var CONNECTING_TIME = 1600; // ms + var CONNECTING_TIME = 100; // ms One interval. var PARTICLE_RADIUS = 0.15; // m var PARTICLE_ANGLE_INCREMENT = 360 / 45; // 1hz var HANDSHAKE_SOUND_URL = "https://s3-us-west-1.amazonaws.com/hifi-content/davidkelly/production/audio/4beat_sweep.wav"; diff --git a/scripts/system/marketplaces/marketplaces.js b/scripts/system/marketplaces/marketplaces.js index 7b4f05193f..be25d1265f 100644 --- a/scripts/system/marketplaces/marketplaces.js +++ b/scripts/system/marketplaces/marketplaces.js @@ -10,1153 +10,808 @@ /* global Tablet, Script, HMD, UserActivityLogger, Entities, Account, Wallet, ContextOverlay, Settings, Camera, Vec3, Quat, MyAvatar, Clipboard, Menu, Grid, Uuid, GlobalServices, openLoginWindow, getConnectionData, Overlays, SoundCache, - DesktopPreviewProvider */ + DesktopPreviewProvider, ResourceRequestObserver */ /* eslint indent: ["error", 4, { "outerIIFEBody": 0 }] */ var selectionDisplay = null; // for gridTool.js to ignore (function () { // BEGIN LOCAL_SCOPE +var AppUi = Script.require('appUi'); +Script.include("/~/system/libraries/gridTool.js"); +Script.include("/~/system/libraries/connectionUtils.js"); - Script.include("/~/system/libraries/WebTablet.js"); - Script.include("/~/system/libraries/gridTool.js"); - Script.include("/~/system/libraries/connectionUtils.js"); +var MARKETPLACE_CHECKOUT_QML_PATH = "hifi/commerce/checkout/Checkout.qml"; +var MARKETPLACE_INSPECTIONCERTIFICATE_QML_PATH = "hifi/commerce/inspectionCertificate/InspectionCertificate.qml"; +var MARKETPLACE_ITEM_TESTER_QML_PATH = "hifi/commerce/marketplaceItemTester/MarketplaceItemTester.qml"; +var MARKETPLACE_PURCHASES_QML_PATH = "hifi/commerce/wallet/Wallet.qml"; // HRS FIXME "hifi/commerce/purchases/Purchases.qml"; +var MARKETPLACE_WALLET_QML_PATH = "hifi/commerce/wallet/Wallet.qml"; +var MARKETPLACES_INJECT_SCRIPT_URL = Script.resolvePath("../html/js/marketplacesInject.js"); +var MARKETPLACES_URL = Script.resolvePath("../html/marketplaces.html"); +var METAVERSE_SERVER_URL = Account.metaverseServerURL; +var REZZING_SOUND = SoundCache.getSound(Script.resolvePath("../assets/sounds/rezzing.wav")); - var METAVERSE_SERVER_URL = Account.metaverseServerURL; - var MARKETPLACE_URL = METAVERSE_SERVER_URL + "/marketplace"; - var MARKETPLACE_URL_INITIAL = MARKETPLACE_URL + "?"; // Append "?" to signal injected script that it's the initial page. - var MARKETPLACES_URL = Script.resolvePath("../html/marketplaces.html"); - var MARKETPLACES_INJECT_SCRIPT_URL = Script.resolvePath("../html/js/marketplacesInject.js"); - var MARKETPLACE_CHECKOUT_QML_PATH = "hifi/commerce/checkout/Checkout.qml"; - var MARKETPLACE_PURCHASES_QML_PATH = "hifi/commerce/purchases/Purchases.qml"; - var MARKETPLACE_WALLET_QML_PATH = "hifi/commerce/wallet/Wallet.qml"; - var MARKETPLACE_INSPECTIONCERTIFICATE_QML_PATH = "commerce/inspectionCertificate/InspectionCertificate.qml"; - var REZZING_SOUND = SoundCache.getSound(Script.resolvePath("../assets/sounds/rezzing.wav")); +// Event bridge messages. +var CLARA_IO_DOWNLOAD = "CLARA.IO DOWNLOAD"; +var CLARA_IO_STATUS = "CLARA.IO STATUS"; +var CLARA_IO_CANCEL_DOWNLOAD = "CLARA.IO CANCEL DOWNLOAD"; +var CLARA_IO_CANCELLED_DOWNLOAD = "CLARA.IO CANCELLED DOWNLOAD"; +var GOTO_DIRECTORY = "GOTO_DIRECTORY"; +var QUERY_CAN_WRITE_ASSETS = "QUERY_CAN_WRITE_ASSETS"; +var CAN_WRITE_ASSETS = "CAN_WRITE_ASSETS"; +var WARN_USER_NO_PERMISSIONS = "WARN_USER_NO_PERMISSIONS"; - var HOME_BUTTON_TEXTURE = "http://hifi-content.s3.amazonaws.com/alan/dev/tablet-with-home-button.fbx/tablet-with-home-button.fbm/button-root.png"; - // var HOME_BUTTON_TEXTURE = Script.resourcesPath() + "meshes/tablet-with-home-button.fbx/tablet-with-home-button.fbm/button-root.png"; +var CLARA_DOWNLOAD_TITLE = "Preparing Download"; +var messageBox = null; +var isDownloadBeingCancelled = false; - // Event bridge messages. - var CLARA_IO_DOWNLOAD = "CLARA.IO DOWNLOAD"; - var CLARA_IO_STATUS = "CLARA.IO STATUS"; - var CLARA_IO_CANCEL_DOWNLOAD = "CLARA.IO CANCEL DOWNLOAD"; - var CLARA_IO_CANCELLED_DOWNLOAD = "CLARA.IO CANCELLED DOWNLOAD"; - var GOTO_DIRECTORY = "GOTO_DIRECTORY"; - var QUERY_CAN_WRITE_ASSETS = "QUERY_CAN_WRITE_ASSETS"; - var CAN_WRITE_ASSETS = "CAN_WRITE_ASSETS"; - var WARN_USER_NO_PERMISSIONS = "WARN_USER_NO_PERMISSIONS"; +var CANCEL_BUTTON = 4194304; // QMessageBox::Cancel +var NO_BUTTON = 0; // QMessageBox::NoButton - var CLARA_DOWNLOAD_TITLE = "Preparing Download"; - var messageBox = null; - var isDownloadBeingCancelled = false; +var NO_PERMISSIONS_ERROR_MESSAGE = "Cannot download model because you can't write to \nthe domain's Asset Server."; - var CANCEL_BUTTON = 4194304; // QMessageBox::Cancel - var NO_BUTTON = 0; // QMessageBox::NoButton - var NO_PERMISSIONS_ERROR_MESSAGE = "Cannot download model because you can't write to \nthe domain's Asset Server."; +var resourceRequestEvents = []; +function signalResourceRequestEvent(data) { + // Once we can tie resource request events to specific resources, + // we will have to update the "0" in here. + var resourceData = "from: " + data.extra + ": " + data.url.toString().replace("__NONE__,", ""); - function onMessageBoxClosed(id, button) { - if (id === messageBox && button === CANCEL_BUTTON) { - isDownloadBeingCancelled = true; - messageBox = null; - tablet.emitScriptEvent(CLARA_IO_CANCEL_DOWNLOAD); + if (resourceObjectsInTest[0].resourceDataArray.indexOf(resourceData) === -1) { + resourceObjectsInTest[0].resourceDataArray.push(resourceData); + + resourceObjectsInTest[0].resourceAccessEventText += "[" + data.date.toISOString() + "] " + + resourceData + "\n"; + + ui.tablet.sendToQml({ + method: "resourceRequestEvent", + data: data, + resourceAccessEventText: resourceObjectsInTest[0].resourceAccessEventText + }); + } +} + +function onResourceRequestEvent(data) { + // Once we can tie resource request events to specific resources, + // we will have to update the "0" in here. + if (resourceObjectsInTest[0] && resourceObjectsInTest[0].currentlyRecordingResources) { + var resourceRequestEvent = { + "date": new Date(), + "url": data.url, + "callerId": data.callerId, + "extra": data.extra + }; + resourceRequestEvents.push(resourceRequestEvent); + signalResourceRequestEvent(resourceRequestEvent); + } +} + +function onMessageBoxClosed(id, button) { + if (id === messageBox && button === CANCEL_BUTTON) { + isDownloadBeingCancelled = true; + messageBox = null; + ui.sendToHtml({ + type: CLARA_IO_CANCEL_DOWNLOAD + }); + } +} + +function onCanWriteAssetsChanged() { + ui.sendToHtml({ + type: CAN_WRITE_ASSETS, + canWriteAssets: Entities.canWriteAssets() + }); +} + + +var tabletShouldBeVisibleInSecondaryCamera = false; +function setTabletVisibleInSecondaryCamera(visibleInSecondaryCam) { + if (visibleInSecondaryCam) { + // if we're potentially showing the tablet, only do so if it was visible before + if (!tabletShouldBeVisibleInSecondaryCamera) { + return; } + } else { + // if we're hiding the tablet, check to see if it was visible in the first place + tabletShouldBeVisibleInSecondaryCamera = Overlays.getProperty(HMD.tabletID, "isVisibleInSecondaryCamera"); } - var onMarketplaceScreen = false; - var onCommerceScreen = false; + Overlays.editOverlay(HMD.tabletID, { isVisibleInSecondaryCamera: visibleInSecondaryCam }); + Overlays.editOverlay(HMD.homeButtonID, { isVisibleInSecondaryCamera: visibleInSecondaryCam }); + Overlays.editOverlay(HMD.homeButtonHighlightID, { isVisibleInSecondaryCamera: visibleInSecondaryCam }); + Overlays.editOverlay(HMD.tabletScreenID, { isVisibleInSecondaryCamera: visibleInSecondaryCam }); +} - var debugCheckout = false; - var debugError = false; - function showMarketplace() { - if (!debugCheckout) { - UserActivityLogger.openedMarketplace(); - tablet.gotoWebScreen(MARKETPLACE_URL_INITIAL, MARKETPLACES_INJECT_SCRIPT_URL); +function openWallet() { + ui.open(MARKETPLACE_WALLET_QML_PATH); +} +function setupWallet(referrer) { + // Needs to be done within the QML page in order to get access to QmlCommerce + openWallet(); + var ALLOWANCE_FOR_EVENT_BRIDGE_SETUP = 0; + Script.setTimeout(function () { + ui.tablet.sendToQml({ + method: 'updateWalletReferrer', + referrer: referrer + }); + }, ALLOWANCE_FOR_EVENT_BRIDGE_SETUP); +} + +function onMarketplaceOpen(referrer) { + var match; + if (Account.loggedIn && walletNeedsSetup()) { + if (referrer === MARKETPLACE_URL_INITIAL) { + setupWallet('marketplace cta'); } else { - tablet.pushOntoStack(MARKETPLACE_CHECKOUT_QML_PATH); - sendToQml({ - method: 'updateCheckoutQML', params: { - itemId: '424611a2-73d0-4c03-9087-26a6a279257b', - itemName: '2018-02-15 Finnegon', - itemPrice: (debugError ? 10 : 3), - itemHref: 'http://devmpassets.highfidelity.com/424611a2-73d0-4c03-9087-26a6a279257b-v1/finnigon.fst', - categories: ["Miscellaneous"] - } - }); - } - } - - function messagesWaiting(isWaiting) { - if (marketplaceButton) { - marketplaceButton.editProperties({ - icon: (isWaiting ? WAITING_ICON : NORMAL_ICON), - activeIcon: (isWaiting ? WAITING_ACTIVE : NORMAL_ACTIVE) - }); - } - } - - function onCanWriteAssetsChanged() { - var message = CAN_WRITE_ASSETS + " " + Entities.canWriteAssets(); - tablet.emitScriptEvent(message); - } - - - var tabletShouldBeVisibleInSecondaryCamera = false; - function setTabletVisibleInSecondaryCamera(visibleInSecondaryCam) { - if (visibleInSecondaryCam) { - // if we're potentially showing the tablet, only do so if it was visible before - if (!tabletShouldBeVisibleInSecondaryCamera) { - return; - } - } else { - // if we're hiding the tablet, check to see if it was visible in the first place - tabletShouldBeVisibleInSecondaryCamera = Overlays.getProperty(HMD.tabletID, "isVisibleInSecondaryCamera"); - } - - Overlays.editOverlay(HMD.tabletID, { isVisibleInSecondaryCamera : visibleInSecondaryCam }); - Overlays.editOverlay(HMD.homeButtonID, { isVisibleInSecondaryCamera : visibleInSecondaryCam }); - Overlays.editOverlay(HMD.homeButtonHighlightID, { isVisibleInSecondaryCamera : visibleInSecondaryCam }); - Overlays.editOverlay(HMD.tabletScreenID, { isVisibleInSecondaryCamera : visibleInSecondaryCam }); - } - - function openWallet() { - tablet.pushOntoStack(MARKETPLACE_WALLET_QML_PATH); - } - - function setCertificateInfo(currentEntityWithContextOverlay, itemCertificateId) { - wireEventBridge(true); - var certificateId = itemCertificateId || (Entities.getEntityProperties(currentEntityWithContextOverlay, ['certificateID']).certificateID); - sendToQml({ - method: 'inspectionCertificate_setCertificateId', - entityId: currentEntityWithContextOverlay, - certificateId: certificateId - }); - } - - function onUsernameChanged() { - if (onMarketplaceScreen) { - tablet.gotoWebScreen(MARKETPLACE_URL_INITIAL, MARKETPLACES_INJECT_SCRIPT_URL); - } - } - - var userHasUpdates = false; - function sendCommerceSettings() { - tablet.emitScriptEvent(JSON.stringify({ - type: "marketplaces", - action: "commerceSetting", - data: { - commerceMode: Settings.getValue("commerce", true), - userIsLoggedIn: Account.loggedIn, - walletNeedsSetup: Wallet.walletStatus === 1, - metaverseServerURL: Account.metaverseServerURL, - messagesWaiting: userHasUpdates - } - })); - } - - // BEGIN AVATAR SELECTOR LOGIC - var UNSELECTED_COLOR = { red: 0x1F, green: 0xC6, blue: 0xA6 }; - var SELECTED_COLOR = { red: 0xF3, green: 0x91, blue: 0x29 }; - var HOVER_COLOR = { red: 0xD0, green: 0xD0, blue: 0xD0 }; - - var overlays = {}; // Keeps track of all our extended overlay data objects, keyed by target identifier. - - function ExtendedOverlay(key, type, properties) { // A wrapper around overlays to store the key it is associated with. - overlays[key] = this; - this.key = key; - this.selected = false; - this.hovering = false; - this.activeOverlay = Overlays.addOverlay(type, properties); // We could use different overlays for (un)selected... - } - // Instance methods: - ExtendedOverlay.prototype.deleteOverlay = function () { // remove display and data of this overlay - Overlays.deleteOverlay(this.activeOverlay); - delete overlays[this.key]; - }; - - ExtendedOverlay.prototype.editOverlay = function (properties) { // change display of this overlay - Overlays.editOverlay(this.activeOverlay, properties); - }; - - function color(selected, hovering) { - var base = hovering ? HOVER_COLOR : selected ? SELECTED_COLOR : UNSELECTED_COLOR; - function scale(component) { - var delta = 0xFF - component; - return component; - } - return { red: scale(base.red), green: scale(base.green), blue: scale(base.blue) }; - } - // so we don't have to traverse the overlays to get the last one - var lastHoveringId = 0; - ExtendedOverlay.prototype.hover = function (hovering) { - this.hovering = hovering; - if (this.key === lastHoveringId) { - if (hovering) { - return; - } - lastHoveringId = 0; - } - this.editOverlay({ color: color(this.selected, hovering) }); - if (hovering) { - // un-hover the last hovering overlay - if (lastHoveringId && lastHoveringId !== this.key) { - ExtendedOverlay.get(lastHoveringId).hover(false); - } - lastHoveringId = this.key; - } - }; - ExtendedOverlay.prototype.select = function (selected) { - if (this.selected === selected) { - return; - } - - this.editOverlay({ color: color(selected, this.hovering) }); - this.selected = selected; - }; - // Class methods: - var selectedId = false; - ExtendedOverlay.isSelected = function (id) { - return selectedId === id; - }; - ExtendedOverlay.get = function (key) { // answer the extended overlay data object associated with the given avatar identifier - return overlays[key]; - }; - ExtendedOverlay.some = function (iterator) { // Bails early as soon as iterator returns truthy. - var key; - for (key in overlays) { - if (iterator(ExtendedOverlay.get(key))) { - return; - } - } - }; - ExtendedOverlay.unHover = function () { // calls hover(false) on lastHoveringId (if any) - if (lastHoveringId) { - ExtendedOverlay.get(lastHoveringId).hover(false); - } - }; - - // hit(overlay) on the one overlay intersected by pickRay, if any. - // noHit() if no ExtendedOverlay was intersected (helps with hover) - ExtendedOverlay.applyPickRay = function (pickRay, hit, noHit) { - var pickedOverlay = Overlays.findRayIntersection(pickRay); // Depends on nearer coverOverlays to extend closer to us than farther ones. - if (!pickedOverlay.intersects) { - if (noHit) { - return noHit(); - } - return; - } - ExtendedOverlay.some(function (overlay) { // See if pickedOverlay is one of ours. - if ((overlay.activeOverlay) === pickedOverlay.overlayID) { - hit(overlay); - return true; - } - }); - }; - - function addAvatarNode(id) { - return new ExtendedOverlay(id, "sphere", { - drawInFront: true, - solid: true, - alpha: 0.8, - color: color(false, false), - ignoreRayIntersection: false - }); - } - - var pingPong = true; - function updateOverlays() { - var eye = Camera.position; - AvatarList.getAvatarIdentifiers().forEach(function (id) { - if (!id) { - return; // don't update ourself, or avatars we're not interested in - } - var avatar = AvatarList.getAvatar(id); - if (!avatar) { - return; // will be deleted below if there had been an overlay. - } - var overlay = ExtendedOverlay.get(id); - if (!overlay) { // For now, we're treating this as a temporary loss, as from the personal space bubble. Add it back. - overlay = addAvatarNode(id); - } - var target = avatar.position; - var distance = Vec3.distance(target, eye); - var offset = 0.2; - var diff = Vec3.subtract(target, eye); // get diff between target and eye (a vector pointing to the eye from avatar position) - var headIndex = avatar.getJointIndex("Head"); // base offset on 1/2 distance from hips to head if we can - if (headIndex > 0) { - offset = avatar.getAbsoluteJointTranslationInObjectFrame(headIndex).y / 2; - } - - // move a bit in front, towards the camera - target = Vec3.subtract(target, Vec3.multiply(Vec3.normalize(diff), offset)); - - // now bump it up a bit - target.y = target.y + offset; - - overlay.ping = pingPong; - overlay.editOverlay({ - color: color(ExtendedOverlay.isSelected(id), overlay.hovering), - position: target, - dimensions: 0.032 * distance - }); - }); - pingPong = !pingPong; - ExtendedOverlay.some(function (overlay) { // Remove any that weren't updated. (User is gone.) - if (overlay.ping === pingPong) { - overlay.deleteOverlay(); - } - }); - } - function removeOverlays() { - selectedId = false; - lastHoveringId = 0; - ExtendedOverlay.some(function (overlay) { - overlay.deleteOverlay(); - }); - } - - // - // Clicks. - // - function usernameFromIDReply(id, username, machineFingerprint, isAdmin) { - if (selectedId === id) { - var message = { - method: 'updateSelectedRecipientUsername', - userName: username === "" ? "unknown username" : username - }; - sendToQml(message); - } - } - function handleClick(pickRay) { - ExtendedOverlay.applyPickRay(pickRay, function (overlay) { - var nextSelectedStatus = !overlay.selected; - var avatarId = overlay.key; - selectedId = nextSelectedStatus ? avatarId : false; - if (nextSelectedStatus) { - Users.requestUsernameFromID(avatarId); - } - var message = { - method: 'selectRecipient', - id: avatarId, - isSelected: nextSelectedStatus, - displayName: '"' + AvatarList.getAvatar(avatarId).sessionDisplayName + '"', - userName: '' - }; - sendToQml(message); - - ExtendedOverlay.some(function (overlay) { - var id = overlay.key; - var selected = ExtendedOverlay.isSelected(id); - overlay.select(selected); - }); - - return true; - }); - } - function handleMouseEvent(mousePressEvent) { // handleClick if we get one. - if (!mousePressEvent.isLeftButton) { - return; - } - handleClick(Camera.computePickRay(mousePressEvent.x, mousePressEvent.y)); - } - function handleMouseMove(pickRay) { // given the pickRay, just do the hover logic - ExtendedOverlay.applyPickRay(pickRay, function (overlay) { - overlay.hover(true); - }, function () { - ExtendedOverlay.unHover(); - }); - } - - // handy global to keep track of which hand is the mouse (if any) - var currentHandPressed = 0; - var TRIGGER_CLICK_THRESHOLD = 0.85; - var TRIGGER_PRESS_THRESHOLD = 0.05; - - function handleMouseMoveEvent(event) { // find out which overlay (if any) is over the mouse position - var pickRay; - if (HMD.active) { - if (currentHandPressed !== 0) { - pickRay = controllerComputePickRay(currentHandPressed); + match = referrer.match(/\/item\/(\w+)$/); + if (match && match[1]) { + setupWallet(match[1]); + } else if (referrer.indexOf(METAVERSE_SERVER_URL) === -1) { // not a url + setupWallet(referrer); } else { - // nothing should hover, so - ExtendedOverlay.unHover(); - return; + print("WARNING: opening marketplace to", referrer, "without wallet setup."); } - } else { - pickRay = Camera.computePickRay(event.x, event.y); - } - handleMouseMove(pickRay); - } - function handleTriggerPressed(hand, value) { - // The idea is if you press one trigger, it is the one - // we will consider the mouse. Even if the other is pressed, - // we ignore it until this one is no longer pressed. - var isPressed = value > TRIGGER_PRESS_THRESHOLD; - if (currentHandPressed === 0) { - currentHandPressed = isPressed ? hand : 0; - return; - } - if (currentHandPressed === hand) { - currentHandPressed = isPressed ? hand : 0; - return; - } - // otherwise, the other hand is still triggered - // so do nothing. - } - - // We get mouseMoveEvents from the handControllers, via handControllerPointer. - // But we don't get mousePressEvents. - var triggerMapping = Controller.newMapping(Script.resolvePath('') + '-click'); - var triggerPressMapping = Controller.newMapping(Script.resolvePath('') + '-press'); - function controllerComputePickRay(hand) { - var controllerPose = getControllerWorldLocation(hand, true); - if (controllerPose.valid) { - return { origin: controllerPose.position, direction: Quat.getUp(controllerPose.orientation) }; } } - function makeClickHandler(hand) { - return function (clicked) { - if (clicked > TRIGGER_CLICK_THRESHOLD) { - var pickRay = controllerComputePickRay(hand); - handleClick(pickRay); - } - }; - } - function makePressHandler(hand) { - return function (value) { - handleTriggerPressed(hand, value); - }; - } - triggerMapping.from(Controller.Standard.RTClick).peek().to(makeClickHandler(Controller.Standard.RightHand)); - triggerMapping.from(Controller.Standard.LTClick).peek().to(makeClickHandler(Controller.Standard.LeftHand)); - triggerPressMapping.from(Controller.Standard.RT).peek().to(makePressHandler(Controller.Standard.RightHand)); - triggerPressMapping.from(Controller.Standard.LT).peek().to(makePressHandler(Controller.Standard.LeftHand)); - // END AVATAR SELECTOR LOGIC +} - var grid = new Grid(); - function adjustPositionPerBoundingBox(position, direction, registration, dimensions, orientation) { - // Adjust the position such that the bounding box (registration, dimenions, and orientation) lies behind the original - // position in the given direction. - var CORNERS = [ - { x: 0, y: 0, z: 0 }, - { x: 0, y: 0, z: 1 }, - { x: 0, y: 1, z: 0 }, - { x: 0, y: 1, z: 1 }, - { x: 1, y: 0, z: 0 }, - { x: 1, y: 0, z: 1 }, - { x: 1, y: 1, z: 0 }, - { x: 1, y: 1, z: 1 }, - ]; +function openMarketplace(optionalItemOrUrl) { + // This is a bit of a kluge, but so is the whole file. + // If given a whole path, use it with no cta. + // If given an id, build the appropriate url and use the id as the cta. + // Otherwise, use home and 'marketplace cta'. + // AND... if call onMarketplaceOpen to setupWallet if we need to. + var url = optionalItemOrUrl || MARKETPLACE_URL_INITIAL; + // If optionalItemOrUrl contains the metaverse base, then it's a url, not an item id. + if (optionalItemOrUrl && optionalItemOrUrl.indexOf(METAVERSE_SERVER_URL) === -1) { + url = MARKETPLACE_URL + '/items/' + optionalItemOrUrl; + } + ui.open(url, MARKETPLACES_INJECT_SCRIPT_URL); +} - // Go through all corners and find least (most negative) distance in front of position. - var distance = 0; - for (var i = 0, length = CORNERS.length; i < length; i++) { - var cornerVector = - Vec3.multiplyQbyV(orientation, Vec3.multiplyVbyV(Vec3.subtract(CORNERS[i], registration), dimensions)); - var cornerDistance = Vec3.dot(cornerVector, direction); - distance = Math.min(cornerDistance, distance); +// Function Name: wireQmlEventBridge() +// +// Description: +// -Used to connect/disconnect the script's response to the tablet's "fromQml" signal. Set the "on" argument to enable or +// disable to event bridge. +// +// Relevant Variables: +// -hasEventBridge: true/false depending on whether we've already connected the event bridge. +var hasEventBridge = false; +function wireQmlEventBridge(on) { + if (!ui.tablet) { + print("Warning in wireQmlEventBridge(): 'tablet' undefined!"); + return; + } + if (on) { + if (!hasEventBridge) { + ui.tablet.fromQml.connect(onQmlMessageReceived); + hasEventBridge = true; } - position = Vec3.sum(Vec3.multiply(distance, direction), position); - return position; + } else { + if (hasEventBridge) { + ui.tablet.fromQml.disconnect(onQmlMessageReceived); + hasEventBridge = false; + } + } +} + +var contextOverlayEntity = ""; +function openInspectionCertificateQML(currentEntityWithContextOverlay) { + ui.open(MARKETPLACE_INSPECTIONCERTIFICATE_QML_PATH); + contextOverlayEntity = currentEntityWithContextOverlay; +} + +function setCertificateInfo(currentEntityWithContextOverlay, itemCertificateId) { + var certificateId = itemCertificateId || + (Entities.getEntityProperties(currentEntityWithContextOverlay, ['certificateID']).certificateID); + ui.tablet.sendToQml({ + method: 'inspectionCertificate_setCertificateId', + entityId: currentEntityWithContextOverlay, + certificateId: certificateId + }); +} + +function onUsernameChanged() { + if (onMarketplaceScreen) { + openMarketplace(); + } +} + +function walletNeedsSetup() { + return WalletScriptingInterface.walletStatus === 1; +} + +function sendCommerceSettings() { + ui.sendToHtml({ + type: "marketplaces", + action: "commerceSetting", + data: { + commerceMode: Settings.getValue("commerce", true), + userIsLoggedIn: Account.loggedIn, + walletNeedsSetup: walletNeedsSetup(), + metaverseServerURL: Account.metaverseServerURL, + limitedCommerce: WalletScriptingInterface.limitedCommerce + } + }); +} + +var grid = new Grid(); +function adjustPositionPerBoundingBox(position, direction, registration, dimensions, orientation) { + // Adjust the position such that the bounding box (registration, dimenions, and orientation) lies behind the original + // position in the given direction. + var CORNERS = [ + { x: 0, y: 0, z: 0 }, + { x: 0, y: 0, z: 1 }, + { x: 0, y: 1, z: 0 }, + { x: 0, y: 1, z: 1 }, + { x: 1, y: 0, z: 0 }, + { x: 1, y: 0, z: 1 }, + { x: 1, y: 1, z: 0 }, + { x: 1, y: 1, z: 1 } + ]; + + // Go through all corners and find least (most negative) distance in front of position. + var distance = 0; + for (var i = 0, length = CORNERS.length; i < length; i++) { + var cornerVector = + Vec3.multiplyQbyV(orientation, Vec3.multiplyVbyV(Vec3.subtract(CORNERS[i], registration), dimensions)); + var cornerDistance = Vec3.dot(cornerVector, direction); + distance = Math.min(cornerDistance, distance); + } + position = Vec3.sum(Vec3.multiply(distance, direction), position); + return position; +} + +var HALF_TREE_SCALE = 16384; +function getPositionToCreateEntity(extra) { + var CREATE_DISTANCE = 2; + var position; + var delta = extra !== undefined ? extra : 0; + if (Camera.mode === "entity" || Camera.mode === "independent") { + position = Vec3.sum(Camera.position, Vec3.multiply(Quat.getForward(Camera.orientation), CREATE_DISTANCE + delta)); + } else { + position = Vec3.sum(MyAvatar.position, Vec3.multiply(Quat.getForward(MyAvatar.orientation), CREATE_DISTANCE + delta)); + position.y += 0.5; } - var HALF_TREE_SCALE = 16384; - function getPositionToCreateEntity(extra) { - var CREATE_DISTANCE = 2; - var position; - var delta = extra !== undefined ? extra : 0; - if (Camera.mode === "entity" || Camera.mode === "independent") { - position = Vec3.sum(Camera.position, Vec3.multiply(Quat.getForward(Camera.orientation), CREATE_DISTANCE + delta)); - } else { - position = Vec3.sum(MyAvatar.position, Vec3.multiply(Quat.getForward(MyAvatar.orientation), CREATE_DISTANCE + delta)); - position.y += 0.5; - } + if (position.x > HALF_TREE_SCALE || position.y > HALF_TREE_SCALE || position.z > HALF_TREE_SCALE) { + return null; + } + return position; +} - if (position.x > HALF_TREE_SCALE || position.y > HALF_TREE_SCALE || position.z > HALF_TREE_SCALE) { - return null; - } - return position; +function defaultFor(arg, val) { + return typeof arg !== 'undefined' ? arg : val; +} + +var CERT_ID_URLPARAM_LENGTH = 15; // length of "certificate_id=" +function rezEntity(itemHref, itemType, marketplaceItemTesterId) { + var isWearable = itemType === "wearable"; + var success = Clipboard.importEntities(itemHref, true, marketplaceItemTesterId); + var wearableLocalPosition = null; + var wearableLocalRotation = null; + var wearableLocalDimensions = null; + var wearableDimensions = null; + marketplaceItemTesterId = defaultFor(marketplaceItemTesterId, -1); + + if (itemType === "contentSet") { + console.log("Item is a content set; codepath shouldn't go here."); + return; } - function rezEntity(itemHref, itemType) { - var isWearable = itemType === "wearable"; - var success = Clipboard.importEntities(itemHref); - var wearableLocalPosition = null; - var wearableLocalRotation = null; - var wearableLocalDimensions = null; - var wearableDimensions = null; - - if (itemType === "contentSet") { - console.log("Item is a content set; codepath shouldn't go here."); - return; + if (isWearable) { + var wearableTransforms = Settings.getValue("io.highfidelity.avatarStore.checkOut.transforms"); + if (!wearableTransforms) { + // TODO delete this clause + wearableTransforms = Settings.getValue("io.highfidelity.avatarStore.checkOut.tranforms"); } - - if (isWearable) { - var wearableTransforms = Settings.getValue("io.highfidelity.avatarStore.checkOut.transforms"); - if (!wearableTransforms) { - // TODO delete this clause - wearableTransforms = Settings.getValue("io.highfidelity.avatarStore.checkOut.tranforms"); - } - var certPos = itemHref.search("certificate_id="); // TODO how do I parse a URL from here? - if (certPos >= 0) { - certPos += 15; // length of "certificate_id=" - var certURLEncoded = itemHref.substring(certPos); - var certB64Encoded = decodeURIComponent(certURLEncoded); - for (var key in wearableTransforms) { - if (wearableTransforms.hasOwnProperty(key)) { - var certificateTransforms = wearableTransforms[key].certificateTransforms; - if (certificateTransforms) { - for (var certID in certificateTransforms) { - if (certificateTransforms.hasOwnProperty(certID) && - certID == certB64Encoded) { - var certificateTransform = certificateTransforms[certID]; - wearableLocalPosition = certificateTransform.localPosition; - wearableLocalRotation = certificateTransform.localRotation; - wearableLocalDimensions = certificateTransform.localDimensions; - wearableDimensions = certificateTransform.dimensions; - } + var certPos = itemHref.search("certificate_id="); // TODO how do I parse a URL from here? + if (certPos >= 0) { + certPos += CERT_ID_URLPARAM_LENGTH; + var certURLEncoded = itemHref.substring(certPos); + var certB64Encoded = decodeURIComponent(certURLEncoded); + for (var key in wearableTransforms) { + if (wearableTransforms.hasOwnProperty(key)) { + var certificateTransforms = wearableTransforms[key].certificateTransforms; + if (certificateTransforms) { + for (var certID in certificateTransforms) { + if (certificateTransforms.hasOwnProperty(certID) && + certID === certB64Encoded) { + var certificateTransform = certificateTransforms[certID]; + wearableLocalPosition = certificateTransform.localPosition; + wearableLocalRotation = certificateTransform.localRotation; + wearableLocalDimensions = certificateTransform.localDimensions; + wearableDimensions = certificateTransform.dimensions; } } } } } } + } - if (success) { - var VERY_LARGE = 10000; - var isLargeImport = Clipboard.getClipboardContentsLargestDimension() >= VERY_LARGE; - var position = Vec3.ZERO; + if (success) { + var VERY_LARGE = 10000; + var isLargeImport = Clipboard.getClipboardContentsLargestDimension() >= VERY_LARGE; + var position = Vec3.ZERO; + if (!isLargeImport) { + position = getPositionToCreateEntity(Clipboard.getClipboardContentsLargestDimension() / 2); + } + if (position !== null && position !== undefined) { + var pastedEntityIDs = Clipboard.pasteEntities(position); if (!isLargeImport) { - position = getPositionToCreateEntity(Clipboard.getClipboardContentsLargestDimension() / 2); - } - if (position !== null && position !== undefined) { - var pastedEntityIDs = Clipboard.pasteEntities(position); - if (!isLargeImport) { - // The first entity in Clipboard gets the specified position with the rest being relative to it. Therefore, move - // entities after they're imported so that they're all the correct distance in front of and with geometric mean - // centered on the avatar/camera direction. - var deltaPosition = Vec3.ZERO; - var entityPositions = []; - var entityParentIDs = []; + // The first entity in Clipboard gets the specified position with the rest being relative to it. Therefore, move + // entities after they're imported so that they're all the correct distance in front of and with geometric mean + // centered on the avatar/camera direction. + var deltaPosition = Vec3.ZERO; + var entityPositions = []; + var entityParentIDs = []; - var propType = Entities.getEntityProperties(pastedEntityIDs[0], ["type"]).type; - var NO_ADJUST_ENTITY_TYPES = ["Zone", "Light", "ParticleEffect"]; - if (NO_ADJUST_ENTITY_TYPES.indexOf(propType) === -1) { - var targetDirection; - if (Camera.mode === "entity" || Camera.mode === "independent") { - targetDirection = Camera.orientation; - } else { - targetDirection = MyAvatar.orientation; - } - targetDirection = Vec3.multiplyQbyV(targetDirection, Vec3.UNIT_Z); - - var targetPosition = getPositionToCreateEntity(); - var deltaParallel = HALF_TREE_SCALE; // Distance to move entities parallel to targetDirection. - var deltaPerpendicular = Vec3.ZERO; // Distance to move entities perpendicular to targetDirection. - for (var i = 0, length = pastedEntityIDs.length; i < length; i++) { - var curLoopEntityProps = Entities.getEntityProperties(pastedEntityIDs[i], ["position", "dimensions", - "registrationPoint", "rotation", "parentID"]); - var adjustedPosition = adjustPositionPerBoundingBox(targetPosition, targetDirection, - curLoopEntityProps.registrationPoint, curLoopEntityProps.dimensions, curLoopEntityProps.rotation); - var delta = Vec3.subtract(adjustedPosition, curLoopEntityProps.position); - var distance = Vec3.dot(delta, targetDirection); - deltaParallel = Math.min(distance, deltaParallel); - deltaPerpendicular = Vec3.sum(Vec3.subtract(delta, Vec3.multiply(distance, targetDirection)), - deltaPerpendicular); - entityPositions[i] = curLoopEntityProps.position; - entityParentIDs[i] = curLoopEntityProps.parentID; - } - deltaPerpendicular = Vec3.multiply(1 / pastedEntityIDs.length, deltaPerpendicular); - deltaPosition = Vec3.sum(Vec3.multiply(deltaParallel, targetDirection), deltaPerpendicular); + var propType = Entities.getEntityProperties(pastedEntityIDs[0], ["type"]).type; + var NO_ADJUST_ENTITY_TYPES = ["Zone", "Light", "ParticleEffect"]; + if (NO_ADJUST_ENTITY_TYPES.indexOf(propType) === -1) { + var targetDirection; + if (Camera.mode === "entity" || Camera.mode === "independent") { + targetDirection = Camera.orientation; + } else { + targetDirection = MyAvatar.orientation; } + targetDirection = Vec3.multiplyQbyV(targetDirection, Vec3.UNIT_Z); - if (grid.getSnapToGrid()) { - var firstEntityProps = Entities.getEntityProperties(pastedEntityIDs[0], ["position", "dimensions", - "registrationPoint"]); - var positionPreSnap = Vec3.sum(deltaPosition, firstEntityProps.position); - position = grid.snapToSurface(grid.snapToGrid(positionPreSnap, false, firstEntityProps.dimensions, - firstEntityProps.registrationPoint), firstEntityProps.dimensions, firstEntityProps.registrationPoint); - deltaPosition = Vec3.subtract(position, firstEntityProps.position); + var targetPosition = getPositionToCreateEntity(); + // Distance to move entities parallel to targetDirection. + var deltaParallel = HALF_TREE_SCALE; + // Distance to move entities perpendicular to targetDirection. + var deltaPerpendicular = Vec3.ZERO; + for (var i = 0, length = pastedEntityIDs.length; i < length; i++) { + var curLoopEntityProps = Entities.getEntityProperties(pastedEntityIDs[i], ["position", "dimensions", + "registrationPoint", "rotation", "parentID"]); + var adjustedPosition = adjustPositionPerBoundingBox(targetPosition, targetDirection, + curLoopEntityProps.registrationPoint, curLoopEntityProps.dimensions, curLoopEntityProps.rotation); + var delta = Vec3.subtract(adjustedPosition, curLoopEntityProps.position); + var distance = Vec3.dot(delta, targetDirection); + deltaParallel = Math.min(distance, deltaParallel); + deltaPerpendicular = Vec3.sum(Vec3.subtract(delta, Vec3.multiply(distance, targetDirection)), + deltaPerpendicular); + entityPositions[i] = curLoopEntityProps.position; + entityParentIDs[i] = curLoopEntityProps.parentID; } + deltaPerpendicular = Vec3.multiply(1 / pastedEntityIDs.length, deltaPerpendicular); + deltaPosition = Vec3.sum(Vec3.multiply(deltaParallel, targetDirection), deltaPerpendicular); + } - if (!Vec3.equal(deltaPosition, Vec3.ZERO)) { - for (var editEntityIndex = 0, numEntities = pastedEntityIDs.length; editEntityIndex < numEntities; editEntityIndex++) { - if (Uuid.isNull(entityParentIDs[editEntityIndex])) { - Entities.editEntity(pastedEntityIDs[editEntityIndex], { - position: Vec3.sum(deltaPosition, entityPositions[editEntityIndex]) - }); - } + if (grid.getSnapToGrid()) { + var firstEntityProps = Entities.getEntityProperties(pastedEntityIDs[0], ["position", "dimensions", + "registrationPoint"]); + var positionPreSnap = Vec3.sum(deltaPosition, firstEntityProps.position); + position = grid.snapToSurface(grid.snapToGrid(positionPreSnap, false, firstEntityProps.dimensions, + firstEntityProps.registrationPoint), firstEntityProps.dimensions, firstEntityProps.registrationPoint); + deltaPosition = Vec3.subtract(position, firstEntityProps.position); + } + + if (!Vec3.equal(deltaPosition, Vec3.ZERO)) { + for (var editEntityIndex = 0, + numEntities = pastedEntityIDs.length; editEntityIndex < numEntities; editEntityIndex++) { + if (Uuid.isNull(entityParentIDs[editEntityIndex])) { + Entities.editEntity(pastedEntityIDs[editEntityIndex], { + position: Vec3.sum(deltaPosition, entityPositions[editEntityIndex]) + }); } } } + } - if (isWearable) { - // apply the relative offsets saved during checkout - var offsets = {}; - if (wearableLocalPosition) { - offsets.localPosition = wearableLocalPosition; - } - if (wearableLocalRotation) { - offsets.localRotation = wearableLocalRotation; - } - if (wearableLocalDimensions) { - offsets.localDimensions = wearableLocalDimensions; - } else if (wearableDimensions) { - offsets.dimensions = wearableDimensions; - } - // we currently assume a wearable is a single entity - Entities.editEntity(pastedEntityIDs[0], offsets); + if (isWearable) { + // apply the relative offsets saved during checkout + var offsets = {}; + if (wearableLocalPosition) { + offsets.localPosition = wearableLocalPosition; } - - var rezPosition = Entities.getEntityProperties(pastedEntityIDs[0], "position").position; - - Audio.playSound(REZZING_SOUND, { - volume: 1.0, - position: rezPosition, - localOnly: true - }); - - } else { - Window.notifyEditError("Can't import entities: entities would be out of bounds."); - } - } else { - Window.notifyEditError("There was an error importing the entity file."); - } - } - - var referrerURL; // Used for updating Purchases QML - var filterText; // Used for updating Purchases QML - function onMessage(message) { - if (message === GOTO_DIRECTORY) { - tablet.gotoWebScreen(MARKETPLACES_URL, MARKETPLACES_INJECT_SCRIPT_URL); - } else if (message === QUERY_CAN_WRITE_ASSETS) { - tablet.emitScriptEvent(CAN_WRITE_ASSETS + " " + Entities.canWriteAssets()); - } else if (message === WARN_USER_NO_PERMISSIONS) { - Window.alert(NO_PERMISSIONS_ERROR_MESSAGE); - } else if (message.slice(0, CLARA_IO_STATUS.length) === CLARA_IO_STATUS) { - if (isDownloadBeingCancelled) { - return; + if (wearableLocalRotation) { + offsets.localRotation = wearableLocalRotation; + } + if (wearableLocalDimensions) { + offsets.localDimensions = wearableLocalDimensions; + } else if (wearableDimensions) { + offsets.dimensions = wearableDimensions; + } + // we currently assume a wearable is a single entity + Entities.editEntity(pastedEntityIDs[0], offsets); } - var text = message.slice(CLARA_IO_STATUS.length); - if (messageBox === null) { - messageBox = Window.openMessageBox(CLARA_DOWNLOAD_TITLE, text, CANCEL_BUTTON, NO_BUTTON); - } else { - Window.updateMessageBox(messageBox, CLARA_DOWNLOAD_TITLE, text, CANCEL_BUTTON, NO_BUTTON); - } - return; - } else if (message.slice(0, CLARA_IO_DOWNLOAD.length) === CLARA_IO_DOWNLOAD) { - if (messageBox !== null) { - Window.closeMessageBox(messageBox); - messageBox = null; - } - return; - } else if (message === CLARA_IO_CANCELLED_DOWNLOAD) { - isDownloadBeingCancelled = false; - } else { - var parsedJsonMessage = JSON.parse(message); - if (parsedJsonMessage.type === "CHECKOUT") { - wireEventBridge(true); - tablet.pushOntoStack(MARKETPLACE_CHECKOUT_QML_PATH); - sendToQml({ - method: 'updateCheckoutQML', - params: parsedJsonMessage - }); - } else if (parsedJsonMessage.type === "REQUEST_SETTING") { - sendCommerceSettings(); - } else if (parsedJsonMessage.type === "PURCHASES") { - referrerURL = parsedJsonMessage.referrerURL; - filterText = ""; - tablet.pushOntoStack(MARKETPLACE_PURCHASES_QML_PATH); - } else if (parsedJsonMessage.type === "LOGIN") { - openLoginWindow(); - } else if (parsedJsonMessage.type === "WALLET_SETUP") { - wireEventBridge(true); - sendToQml({ - method: 'updateWalletReferrer', - referrer: "marketplace cta" - }); - openWallet(); - } else if (parsedJsonMessage.type === "MY_ITEMS") { - referrerURL = MARKETPLACE_URL_INITIAL; - filterText = ""; - tablet.pushOntoStack(MARKETPLACE_PURCHASES_QML_PATH); - wireEventBridge(true); - sendToQml({ - method: 'purchases_showMyItems' - }); - } - } - } + var rezPosition = Entities.getEntityProperties(pastedEntityIDs[0], "position").position; - function onButtonClicked() { - if (!tablet) { - print("Warning in buttonClicked(): 'tablet' undefined!"); - return; - } - if (onMarketplaceScreen || onCommerceScreen) { - // for toolbar-mode: go back to home screen, this will close the window. - tablet.gotoHomeScreen(); - } else { - if (HMD.tabletID) { - Entities.editEntity(HMD.tabletID, { textures: JSON.stringify({ "tex.close": HOME_BUTTON_TEXTURE }) }); - } - showMarketplace(); - } - } - - // Function Name: sendToQml() - // - // Description: - // -Use this function to send a message to the QML (i.e. to change appearances). The "message" argument is what is sent to - // the QML in the format "{method, params}", like json-rpc. See also fromQml(). - function sendToQml(message) { - tablet.sendToQml(message); - } - - var sendAssetRecipient; - var sendAssetParticleEffectUpdateTimer; - var particleEffectTimestamp; - var sendAssetParticleEffect; - var SEND_ASSET_PARTICLE_TIMER_UPDATE = 250; - var SEND_ASSET_PARTICLE_EMITTING_DURATION = 3000; - var SEND_ASSET_PARTICLE_LIFETIME_SECONDS = 8; - var SEND_ASSET_PARTICLE_PROPERTIES = { - accelerationSpread: { x: 0, y: 0, z: 0 }, - alpha: 1, - alphaFinish: 1, - alphaSpread: 0, - alphaStart: 1, - azimuthFinish: 0, - azimuthStart: -6, - color: { red: 255, green: 222, blue: 255 }, - colorFinish: { red: 255, green: 229, blue: 225 }, - colorSpread: { red: 0, green: 0, blue: 0 }, - colorStart: { red: 243, green: 255, blue: 255 }, - emitAcceleration: { x: 0, y: 0, z: 0 }, // Immediately gets updated to be accurate - emitDimensions: { x: 0, y: 0, z: 0 }, - emitOrientation: { x: 0, y: 0, z: 0 }, - emitRate: 4, - emitSpeed: 2.1, - emitterShouldTrail: true, - isEmitting: 1, - lifespan: SEND_ASSET_PARTICLE_LIFETIME_SECONDS + 1, // Immediately gets updated to be accurate - lifetime: SEND_ASSET_PARTICLE_LIFETIME_SECONDS + 1, - maxParticles: 20, - name: 'asset-particles', - particleRadius: 0.2, - polarFinish: 0, - polarStart: 0, - radiusFinish: 0.05, - radiusSpread: 0, - radiusStart: 0.2, - speedSpread: 0, - textures: "http://hifi-content.s3.amazonaws.com/alan/dev/Particles/Bokeh-Particle-HFC.png", - type: 'ParticleEffect' - }; - - function updateSendAssetParticleEffect() { - var timestampNow = Date.now(); - if ((timestampNow - particleEffectTimestamp) > (SEND_ASSET_PARTICLE_LIFETIME_SECONDS * 1000)) { - deleteSendAssetParticleEffect(); - return; - } else if ((timestampNow - particleEffectTimestamp) > SEND_ASSET_PARTICLE_EMITTING_DURATION) { - Entities.editEntity(sendAssetParticleEffect, { - isEmitting: 0 - }); - } else if (sendAssetParticleEffect) { - var recipientPosition = AvatarList.getAvatar(sendAssetRecipient).position; - var distance = Vec3.distance(recipientPosition, MyAvatar.position); - var accel = Vec3.subtract(recipientPosition, MyAvatar.position); - accel.y -= 3.0; - var life = Math.sqrt(2 * distance / Vec3.length(accel)); - Entities.editEntity(sendAssetParticleEffect, { - emitAcceleration: accel, - lifespan: life + Audio.playSound(REZZING_SOUND, { + volume: 1.0, + position: rezPosition, + localOnly: true }); + + } else { + Window.notifyEditError("Can't import entities: entities would be out of bounds."); } + } else { + Window.notifyEditError("There was an error importing the entity file."); } +} - function deleteSendAssetParticleEffect() { - if (sendAssetParticleEffectUpdateTimer) { - Script.clearInterval(sendAssetParticleEffectUpdateTimer); - sendAssetParticleEffectUpdateTimer = null; - } - if (sendAssetParticleEffect) { - sendAssetParticleEffect = Entities.deleteEntity(sendAssetParticleEffect); - } - sendAssetRecipient = null; - } - - var savedDisablePreviewOptionLocked = false; - var savedDisablePreviewOption = Menu.isOptionChecked("Disable Preview");; - function maybeEnableHMDPreview() { - // Set a small timeout to prevent sensitive data from being shown during - // UI fade - Script.setTimeout(function () { - setTabletVisibleInSecondaryCamera(true); - DesktopPreviewProvider.setPreviewDisabledReason("USER"); - Menu.setIsOptionChecked("Disable Preview", savedDisablePreviewOption); - savedDisablePreviewOptionLocked = false; - }, 150); - } - - // Function Name: fromQml() - // - // Description: - // -Called when a message is received from Checkout.qml. The "message" argument is what is sent from the Checkout QML - // in the format "{method, params}", like json-rpc. - function fromQml(message) { - switch (message.method) { - case 'purchases_openWallet': - case 'checkout_openWallet': - case 'checkout_setUpClicked': - openWallet(); - break; - case 'purchases_walletNotSetUp': - wireEventBridge(true); - sendToQml({ - method: 'updateWalletReferrer', - referrer: "purchases" - }); - openWallet(); - break; - case 'checkout_walletNotSetUp': - wireEventBridge(true); - sendToQml({ - method: 'updateWalletReferrer', - referrer: message.referrer === "itemPage" ? message.itemId : message.referrer - }); - openWallet(); - break; - case 'checkout_cancelClicked': - tablet.gotoWebScreen(MARKETPLACE_URL + '/items/' + message.params, MARKETPLACES_INJECT_SCRIPT_URL); - // TODO: Make Marketplace a QML app that's a WebView wrapper so we can use the app stack. - // I don't think this is trivial to do since we also want to inject some JS into the DOM. - //tablet.popFromStack(); - break; - case 'header_goToPurchases': - case 'checkout_goToPurchases': - referrerURL = MARKETPLACE_URL_INITIAL; - filterText = message.filterText; - tablet.pushOntoStack(MARKETPLACE_PURCHASES_QML_PATH); - break; - case 'checkout_itemLinkClicked': - tablet.gotoWebScreen(MARKETPLACE_URL + '/items/' + message.itemId, MARKETPLACES_INJECT_SCRIPT_URL); - break; - case 'checkout_continueShopping': - tablet.gotoWebScreen(MARKETPLACE_URL_INITIAL, MARKETPLACES_INJECT_SCRIPT_URL); - //tablet.popFromStack(); - break; - case 'purchases_itemInfoClicked': - var itemId = message.itemId; - if (itemId && itemId !== "") { - tablet.gotoWebScreen(MARKETPLACE_URL + '/items/' + itemId, MARKETPLACES_INJECT_SCRIPT_URL); - } - break; - case 'checkout_rezClicked': - case 'purchases_rezClicked': - rezEntity(message.itemHref, message.itemType); - break; - case 'header_marketplaceImageClicked': - case 'purchases_backClicked': - tablet.gotoWebScreen(message.referrerURL, MARKETPLACES_INJECT_SCRIPT_URL); - break; - case 'purchases_goToMarketplaceClicked': - tablet.gotoWebScreen(MARKETPLACE_URL_INITIAL, MARKETPLACES_INJECT_SCRIPT_URL); - break; - case 'updateItemClicked': - tablet.gotoWebScreen(message.upgradeUrl + "?edition=" + message.itemEdition, - MARKETPLACES_INJECT_SCRIPT_URL); - break; - case 'giftAsset': - - break; - case 'passphrasePopup_cancelClicked': - case 'needsLogIn_cancelClicked': - tablet.gotoWebScreen(MARKETPLACE_URL_INITIAL, MARKETPLACES_INJECT_SCRIPT_URL); - break; - case 'needsLogIn_loginClicked': - openLoginWindow(); - break; - case 'disableHmdPreview': - if (!savedDisablePreviewOption) { - savedDisablePreviewOption = Menu.isOptionChecked("Disable Preview"); - savedDisablePreviewOptionLocked = true; - } - - if (!savedDisablePreviewOption) { - DesktopPreviewProvider.setPreviewDisabledReason("SECURE_SCREEN"); - Menu.setIsOptionChecked("Disable Preview", true); - setTabletVisibleInSecondaryCamera(false); - } - break; - case 'maybeEnableHmdPreview': - maybeEnableHMDPreview(); - break; - case 'purchases_openGoTo': - tablet.loadQMLSource("hifi/tablet/TabletAddressDialog.qml"); - break; - case 'purchases_itemCertificateClicked': - setCertificateInfo("", message.itemCertificateId); - break; - case 'inspectionCertificate_closeClicked': - tablet.gotoHomeScreen(); - break; - case 'inspectionCertificate_requestOwnershipVerification': - ContextOverlay.requestOwnershipVerification(message.entity); - break; - case 'inspectionCertificate_showInMarketplaceClicked': - tablet.gotoWebScreen(message.marketplaceUrl, MARKETPLACES_INJECT_SCRIPT_URL); - break; - case 'header_myItemsClicked': - referrerURL = MARKETPLACE_URL_INITIAL; - filterText = ""; - tablet.pushOntoStack(MARKETPLACE_PURCHASES_QML_PATH); - wireEventBridge(true); - sendToQml({ - method: 'purchases_showMyItems' - }); - break; - case 'refreshConnections': - // Guard to prevent this code from being executed while sending money -- - // we only want to execute this while sending non-HFC gifts - if (!onWalletScreen) { - print('Refreshing Connections...'); - getConnectionData(false); - } - break; - case 'enable_ChooseRecipientNearbyMode': - // Guard to prevent this code from being executed while sending money -- - // we only want to execute this while sending non-HFC gifts - if (!onWalletScreen) { - if (!isUpdateOverlaysWired) { - Script.update.connect(updateOverlays); - isUpdateOverlaysWired = true; - } - } - break; - case 'disable_ChooseRecipientNearbyMode': - // Guard to prevent this code from being executed while sending money -- - // we only want to execute this while sending non-HFC gifts - if (!onWalletScreen) { - if (isUpdateOverlaysWired) { - Script.update.disconnect(updateOverlays); - isUpdateOverlaysWired = false; - } - removeOverlays(); - } - break; - case 'wallet_availableUpdatesReceived': - case 'purchases_availableUpdatesReceived': - userHasUpdates = message.numUpdates > 0; - messagesWaiting(userHasUpdates); - break; - case 'purchases_updateWearables': - var currentlyWornWearables = []; - var ATTACHMENT_SEARCH_RADIUS = 100; // meters (just in case) - - var nearbyEntities = Entities.findEntitiesByType('Model', MyAvatar.position, ATTACHMENT_SEARCH_RADIUS); - - for (var i = 0; i < nearbyEntities.length; i++) { - var currentProperties = Entities.getEntityProperties(nearbyEntities[i], ['certificateID', 'editionNumber', 'parentID']); - if (currentProperties.parentID === MyAvatar.sessionUUID) { - currentlyWornWearables.push({ - entityID: nearbyEntities[i], - entityCertID: currentProperties.certificateID, - entityEdition: currentProperties.editionNumber - }); - } - } - - sendToQml({ method: 'updateWearables', wornWearables: currentlyWornWearables }); - break; - case 'sendAsset_sendPublicly': - if (message.assetName !== "") { - deleteSendAssetParticleEffect(); - sendAssetRecipient = message.recipient; - var amount = message.amount; - var props = SEND_ASSET_PARTICLE_PROPERTIES; - props.parentID = MyAvatar.sessionUUID; - props.position = MyAvatar.position; - props.position.y += 0.2; - if (message.effectImage) { - props.textures = message.effectImage; - } - sendAssetParticleEffect = Entities.addEntity(props, true); - particleEffectTimestamp = Date.now(); - updateSendAssetParticleEffect(); - sendAssetParticleEffectUpdateTimer = Script.setInterval(updateSendAssetParticleEffect, SEND_ASSET_PARTICLE_TIMER_UPDATE); - } - break; - case 'http.request': - // Handled elsewhere, don't log. - break; - case 'goToPurchases_fromWalletHome': // HRS FIXME What's this about? - break; - default: - print('Unrecognized message from Checkout.qml or Purchases.qml: ' + JSON.stringify(message)); - } - } - - // Function Name: wireEventBridge() - // - // Description: - // -Used to connect/disconnect the script's response to the tablet's "fromQml" signal. Set the "on" argument to enable or - // disable to event bridge. - // - // Relevant Variables: - // -hasEventBridge: true/false depending on whether we've already connected the event bridge. - var hasEventBridge = false; - function wireEventBridge(on) { - if (!tablet) { - print("Warning in wireEventBridge(): 'tablet' undefined!"); +var referrerURL; // Used for updating Purchases QML +var filterText; // Used for updating Purchases QML +function onWebEventReceived(message) { + message = JSON.parse(message); + if (message.type === GOTO_DIRECTORY) { + // This is the chooser between marketplaces. Only OUR markteplace + // requires/makes-use-of wallet, so doesn't go through openMarketplace bottleneck. + ui.open(MARKETPLACES_URL, MARKETPLACES_INJECT_SCRIPT_URL); + } else if (message.type === QUERY_CAN_WRITE_ASSETS) { + ui.sendToHtml(CAN_WRITE_ASSETS + " " + Entities.canWriteAssets()); + } else if (message.type === WARN_USER_NO_PERMISSIONS) { + Window.alert(NO_PERMISSIONS_ERROR_MESSAGE); + } else if (message.type === CLARA_IO_STATUS) { + if (isDownloadBeingCancelled) { return; } - if (on) { - if (!hasEventBridge) { - tablet.fromQml.connect(fromQml); - hasEventBridge = true; - } + + var text = message.status; + if (messageBox === null) { + messageBox = Window.openMessageBox(CLARA_DOWNLOAD_TITLE, text, CANCEL_BUTTON, NO_BUTTON); } else { - if (hasEventBridge) { - tablet.fromQml.disconnect(fromQml); - hasEventBridge = false; - } + Window.updateMessageBox(messageBox, CLARA_DOWNLOAD_TITLE, text, CANCEL_BUTTON, NO_BUTTON); } - } - - // Function Name: onTabletScreenChanged() - // - // Description: - // -Called when the TabletScriptingInterface::screenChanged() signal is emitted. The "type" argument can be either the string - // value of "Home", "Web", "Menu", "QML", or "Closed". The "url" argument is only valid for Web and QML. - var onWalletScreen = false; - var onCommerceScreen = false; - function onTabletScreenChanged(type, url) { - onMarketplaceScreen = type === "Web" && url.indexOf(MARKETPLACE_URL) !== -1; - var onWalletScreenNow = url.indexOf(MARKETPLACE_WALLET_QML_PATH) !== -1; - var onCommerceScreenNow = type === "QML" && (url.indexOf(MARKETPLACE_CHECKOUT_QML_PATH) !== -1 || url === MARKETPLACE_PURCHASES_QML_PATH - || url.indexOf(MARKETPLACE_INSPECTIONCERTIFICATE_QML_PATH) !== -1); - - if ((!onWalletScreenNow && onWalletScreen) || (!onCommerceScreenNow && onCommerceScreen)) { // exiting wallet or commerce screen - maybeEnableHMDPreview(); + return; + } else if (message.type === CLARA_IO_DOWNLOAD) { + if (messageBox !== null) { + Window.closeMessageBox(messageBox); + messageBox = null; } - - onCommerceScreen = onCommerceScreenNow; - onWalletScreen = onWalletScreenNow; - wireEventBridge(onMarketplaceScreen || onCommerceScreen || onWalletScreen); - - if (url === MARKETPLACE_PURCHASES_QML_PATH) { - sendToQml({ - method: 'updatePurchases', - referrerURL: referrerURL, - filterText: filterText - }); - } - - // for toolbar mode: change button to active when window is first openend, false otherwise. - if (marketplaceButton) { - marketplaceButton.editProperties({ isActive: (onMarketplaceScreen || onCommerceScreen) && !onWalletScreen }); - } - if (type === "Web" && url.indexOf(MARKETPLACE_URL) !== -1) { - ContextOverlay.isInMarketplaceInspectionMode = true; - } else { - ContextOverlay.isInMarketplaceInspectionMode = false; - } - - if (onCommerceScreen) { - if (!isWired) { - Users.usernameFromIDReply.connect(usernameFromIDReply); - Controller.mousePressEvent.connect(handleMouseEvent); - Controller.mouseMoveEvent.connect(handleMouseMoveEvent); - triggerMapping.enable(); - triggerPressMapping.enable(); - } - isWired = true; - Wallet.refreshWalletStatus(); - } else { - off(); - sendToQml({ - method: 'inspectionCertificate_resetCert' - }); - } - } - - // - // Manage the connection between the button and the window. - // - var marketplaceButton; - var buttonName = "MARKET"; - var tablet = null; - var NORMAL_ICON = "icons/tablet-icons/market-i.svg"; - var NORMAL_ACTIVE = "icons/tablet-icons/market-a.svg"; - var WAITING_ICON = "icons/tablet-icons/market-i-msg.svg"; - var WAITING_ACTIVE = "icons/tablet-icons/market-a-msg.svg"; - function startup() { - tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); - marketplaceButton = tablet.addButton({ - icon: NORMAL_ICON, - activeIcon: NORMAL_ACTIVE, - text: buttonName, - sortOrder: 9 + return; + } else if (message.type === CLARA_IO_CANCELLED_DOWNLOAD) { + isDownloadBeingCancelled = false; + } else if (message.type === "CHECKOUT") { + wireQmlEventBridge(true); + ui.open(MARKETPLACE_CHECKOUT_QML_PATH); + ui.tablet.sendToQml({ + method: 'updateCheckoutQML', + params: message + }); + } else if (message.type === "REQUEST_SETTING") { + sendCommerceSettings(); + } else if (message.type === "PURCHASES") { + referrerURL = message.referrerURL; + filterText = ""; + ui.open(MARKETPLACE_PURCHASES_QML_PATH); + } else if (message.type === "LOGIN") { + openLoginWindow(); + } else if (message.type === "WALLET_SETUP") { + setupWallet('marketplace cta'); + } else if (message.type === "MY_ITEMS") { + referrerURL = MARKETPLACE_URL_INITIAL; + filterText = ""; + ui.open(MARKETPLACE_PURCHASES_QML_PATH); + wireQmlEventBridge(true); + ui.tablet.sendToQml({ + method: 'purchases_showMyItems' }); - - ContextOverlay.contextOverlayClicked.connect(setCertificateInfo); - Entities.canWriteAssetsChanged.connect(onCanWriteAssetsChanged); - GlobalServices.myUsernameChanged.connect(onUsernameChanged); - marketplaceButton.clicked.connect(onButtonClicked); - tablet.screenChanged.connect(onTabletScreenChanged); - tablet.webEventReceived.connect(onMessage); - Wallet.walletStatusChanged.connect(sendCommerceSettings); - Window.messageBoxClosed.connect(onMessageBoxClosed); - - Wallet.refreshWalletStatus(); } - var isWired = false; - var isUpdateOverlaysWired = false; - function off() { - if (isWired) { - Users.usernameFromIDReply.disconnect(usernameFromIDReply); - Controller.mousePressEvent.disconnect(handleMouseEvent); - Controller.mouseMoveEvent.disconnect(handleMouseMoveEvent); - triggerMapping.disable(); - triggerPressMapping.disable(); +} - isWired = false; - } - if (isUpdateOverlaysWired) { - Script.update.disconnect(updateOverlays); - isUpdateOverlaysWired = false; - } - removeOverlays(); +var savedDisablePreviewOption = Menu.isOptionChecked("Disable Preview"); +var UI_FADE_TIMEOUT_MS = 150; +function maybeEnableHMDPreview() { + // Set a small timeout to prevent sensitive data from being shown during UI fade + Script.setTimeout(function () { + setTabletVisibleInSecondaryCamera(true); + DesktopPreviewProvider.setPreviewDisabledReason("USER"); + Menu.setIsOptionChecked("Disable Preview", savedDisablePreviewOption); + }, UI_FADE_TIMEOUT_MS); +} + +var resourceObjectsInTest = []; +function signalNewResourceObjectInTest(resourceObject) { + ui.tablet.sendToQml({ + method: "newResourceObjectInTest", + resourceObject: resourceObject + }); +} + +var onQmlMessageReceived = function onQmlMessageReceived(message) { + if (message.messageSrc === "HTML") { + return; } - function shutdown() { + switch (message.method) { + case 'gotoBank': + ui.close(); + if (Account.metaverseServerURL.indexOf("staging") >= 0) { + Window.location = "hifi://hifiqa-master-metaverse-staging"; // So that we can test in staging. + } else { + Window.location = "hifi://BankOfHighFidelity"; + } + break; + case 'checkout_openRecentActivity': + ui.open(MARKETPLACE_WALLET_QML_PATH); + wireQmlEventBridge(true); + ui.tablet.sendToQml({ + method: 'checkout_openRecentActivity' + }); + break; + case 'checkout_setUpClicked': + openWallet(); + break; + case 'checkout_walletNotSetUp': + wireQmlEventBridge(true); + ui.tablet.sendToQml({ + method: 'updateWalletReferrer', + referrer: message.referrer === "itemPage" ? message.itemId : message.referrer + }); + openWallet(); + break; + case 'checkout_cancelClicked': + openMarketplace(message.params); + break; + case 'header_goToPurchases': + case 'checkout_goToPurchases': + referrerURL = MARKETPLACE_URL_INITIAL; + filterText = message.filterText; + ui.open(MARKETPLACE_PURCHASES_QML_PATH); + break; + case 'checkout_itemLinkClicked': + openMarketplace(message.itemId); + break; + case 'checkout_continue': + openMarketplace(); + break; + case 'checkout_rezClicked': + case 'purchases_rezClicked': + case 'tester_rezClicked': + rezEntity(message.itemHref, message.itemType, message.itemId); + break; + case 'tester_newResourceObject': + var resourceObject = message.resourceObject; + resourceObjectsInTest = []; // REMOVE THIS once we support specific referrers + resourceObject.currentlyRecordingResources = false; + resourceObject.resourceAccessEventText = ""; + resourceObjectsInTest[resourceObject.resourceObjectId] = resourceObject; + resourceObjectsInTest[resourceObject.resourceObjectId].resourceDataArray = []; + signalNewResourceObjectInTest(resourceObject); + break; + case 'tester_updateResourceObjectAssetType': + resourceObjectsInTest[message.objectId].assetType = message.assetType; + break; + case 'tester_deleteResourceObject': + delete resourceObjectsInTest[message.objectId]; + break; + case 'tester_updateResourceRecordingStatus': + resourceObjectsInTest[message.objectId].currentlyRecordingResources = message.status; + if (message.status) { + resourceObjectsInTest[message.objectId].resourceDataArray = []; + resourceObjectsInTest[message.objectId].resourceAccessEventText = ""; + } + break; + case 'header_marketplaceImageClicked': + openMarketplace(message.referrerURL); + break; + case 'purchases_goToMarketplaceClicked': + openMarketplace(); + break; + case 'updateItemClicked': + openMarketplace(message.upgradeUrl + "?edition=" + message.itemEdition); + break; + case 'passphrasePopup_cancelClicked': + case 'needsLogIn_cancelClicked': + // Should/must NOT check for wallet setup. + ui.open(MARKETPLACE_URL_INITIAL, MARKETPLACES_INJECT_SCRIPT_URL); + break; + case 'needsLogIn_loginClicked': + openLoginWindow(); + break; + case 'disableHmdPreview': + if (!savedDisablePreviewOption) { + DesktopPreviewProvider.setPreviewDisabledReason("SECURE_SCREEN"); + Menu.setIsOptionChecked("Disable Preview", true); + setTabletVisibleInSecondaryCamera(false); + } + break; + case 'maybeEnableHmdPreview': maybeEnableHMDPreview(); - deleteSendAssetParticleEffect(); + break; + case 'checkout_openGoTo': + ui.open("hifi/tablet/TabletAddressDialog.qml"); + break; + case 'inspectionCertificate_closeClicked': + ui.close(); + break; + case 'inspectionCertificate_requestOwnershipVerification': + ContextOverlay.requestOwnershipVerification(message.entity); + break; + case 'inspectionCertificate_showInMarketplaceClicked': + openMarketplace(message.marketplaceUrl); + break; + case 'header_myItemsClicked': + referrerURL = MARKETPLACE_URL_INITIAL; + filterText = ""; + ui.open(MARKETPLACE_PURCHASES_QML_PATH); + wireQmlEventBridge(true); + ui.tablet.sendToQml({ + method: 'purchases_showMyItems' + }); + break; + case 'http.request': + // Handled elsewhere, don't log. + break; + // All of these are handled by wallet.js + case 'purchases_updateWearables': + case 'enable_ChooseRecipientNearbyMode': + case 'disable_ChooseRecipientNearbyMode': + case 'sendAsset_sendPublicly': + case 'refreshConnections': + case 'transactionHistory_goToBank': + case 'purchases_walletNotSetUp': + case 'purchases_openGoTo': + case 'purchases_itemInfoClicked': + case 'purchases_itemCertificateClicked': + case 'clearShouldShowDotHistory': + case 'giftAsset': + break; + default: + print('marketplaces.js: Unrecognized message from Checkout.qml'); + } +}; - ContextOverlay.contextOverlayClicked.disconnect(setCertificateInfo); - Entities.canWriteAssetsChanged.disconnect(onCanWriteAssetsChanged); - GlobalServices.myUsernameChanged.disconnect(onUsernameChanged); - marketplaceButton.clicked.disconnect(onButtonClicked); - tablet.removeButton(marketplaceButton); - tablet.webEventReceived.disconnect(onMessage); - Wallet.walletStatusChanged.disconnect(sendCommerceSettings); - Window.messageBoxClosed.disconnect(onMessageBoxClosed); - - if (tablet) { - tablet.screenChanged.disconnect(onTabletScreenChanged); - if (onMarketplaceScreen) { - tablet.gotoHomeScreen(); - } +function pushResourceObjectsInTest() { + var maxResourceObjectId = -1; + var length = resourceObjectsInTest.length; + for (var i = 0; i < length; i++) { + if (i in resourceObjectsInTest) { + signalNewResourceObjectInTest(resourceObjectsInTest[i]); + var resourceObjectId = resourceObjectsInTest[i].resourceObjectId; + maxResourceObjectId = (maxResourceObjectId < resourceObjectId) ? parseInt(resourceObjectId) : maxResourceObjectId; } + } + // N.B. Thinking about removing the following sendToQml? Be sure + // that the marketplace item tester QML has heard from us, at least + // so that it can indicate to the user that all of the resoruce + // objects in test have been transmitted to it. + //ui.tablet.sendToQml({ method: "nextObjectIdInTest", id: maxResourceObjectId + 1 }); + // Since, for now, we only support 1 object in test, always send id: 0 + ui.tablet.sendToQml({ method: "nextObjectIdInTest", id: 0 }); +} +// Function Name: onTabletScreenChanged() +// +// Description: +// -Called when the TabletScriptingInterface::screenChanged() signal is emitted. The "type" argument can be either the string +// value of "Home", "Web", "Menu", "QML", or "Closed". The "url" argument is only valid for Web and QML. + +var onCommerceScreen = false; +var onInspectionCertificateScreen = false; +var onMarketplaceItemTesterScreen = false; +var onMarketplaceScreen = false; +var onWalletScreen = false; +var onTabletScreenChanged = function onTabletScreenChanged(type, url) { + ui.setCurrentVisibleScreenMetadata(type, url); + onMarketplaceScreen = type === "Web" && url.indexOf(MARKETPLACE_URL) !== -1; + onInspectionCertificateScreen = type === "QML" && url.indexOf(MARKETPLACE_INSPECTIONCERTIFICATE_QML_PATH) !== -1; + var onWalletScreenNow = url.indexOf(MARKETPLACE_WALLET_QML_PATH) !== -1; + var onCommerceScreenNow = type === "QML" && ( + url.indexOf(MARKETPLACE_CHECKOUT_QML_PATH) !== -1 || + url === MARKETPLACE_PURCHASES_QML_PATH || + url.indexOf(MARKETPLACE_INSPECTIONCERTIFICATE_QML_PATH) !== -1); + var onMarketplaceItemTesterScreenNow = ( + url.indexOf(MARKETPLACE_ITEM_TESTER_QML_PATH) !== -1 || + url === MARKETPLACE_ITEM_TESTER_QML_PATH); + + if ((!onWalletScreenNow && onWalletScreen) || + (!onCommerceScreenNow && onCommerceScreen) || + (!onMarketplaceItemTesterScreenNow && onMarketplaceScreen) + ) { + // exiting wallet, commerce, or marketplace item tester screen + maybeEnableHMDPreview(); + } + + onCommerceScreen = onCommerceScreenNow; + onWalletScreen = onWalletScreenNow; + onMarketplaceItemTesterScreen = onMarketplaceItemTesterScreenNow; + wireQmlEventBridge(onCommerceScreen || onWalletScreen || onMarketplaceItemTesterScreen); + + if (url === MARKETPLACE_PURCHASES_QML_PATH) { + // FIXME? Is there a race condition here in which the event + // bridge may not be up yet? If so, Script.setTimeout(..., 750) + // may help avoid the condition. + ui.tablet.sendToQml({ + method: 'updatePurchases', + referrerURL: referrerURL, + filterText: filterText + }); + referrerURL = ""; + filterText = ""; + } + + var wasIsOpen = ui.isOpen; + ui.isOpen = (onMarketplaceScreen || onCommerceScreen) && !onWalletScreen; + ui.buttonActive(ui.isOpen); + + if (wasIsOpen !== ui.isOpen && Keyboard.raised) { + Keyboard.raised = false; + } + + if (type === "Web" && url.indexOf(MARKETPLACE_URL) !== -1) { + ContextOverlay.isInMarketplaceInspectionMode = true; + } else { + ContextOverlay.isInMarketplaceInspectionMode = false; + } + + if (onInspectionCertificateScreen) { + setCertificateInfo(contextOverlayEntity); + } + + if (onCommerceScreen) { + WalletScriptingInterface.refreshWalletStatus(); + } else { + if (onMarketplaceScreen) { + onMarketplaceOpen('marketplace cta'); + } + ui.tablet.sendToQml({ + method: 'inspectionCertificate_resetCert' + }); off(); } - // - // Run the functions. - // - startup(); - Script.scriptEnding.connect(shutdown); + if (onMarketplaceItemTesterScreen) { + // Why setTimeout? The QML event bridge, wired above, requires a + // variable amount of time to come up, in practice less than + // 750ms. + Script.setTimeout(pushResourceObjectsInTest, 750); + } + console.debug(ui.buttonName + " app reports: Tablet screen changed.\nNew screen type: " + type + + "\nNew screen URL: " + url + "\nCurrent app open status: " + ui.isOpen + "\n"); +}; + + +var BUTTON_NAME = "MARKET"; +var MARKETPLACE_URL = METAVERSE_SERVER_URL + "/marketplace"; +// Append "?" if necessary to signal injected script that it's the initial page. +var MARKETPLACE_URL_INITIAL = MARKETPLACE_URL + (MARKETPLACE_URL.indexOf("?") > -1 ? "" : "?"); +var ui; +function startup() { + ui = new AppUi({ + buttonName: BUTTON_NAME, + sortOrder: 9, + inject: MARKETPLACES_INJECT_SCRIPT_URL, + home: MARKETPLACE_URL_INITIAL, + onScreenChanged: onTabletScreenChanged, + onMessage: onQmlMessageReceived + }); + ContextOverlay.contextOverlayClicked.connect(openInspectionCertificateQML); + Entities.canWriteAssetsChanged.connect(onCanWriteAssetsChanged); + GlobalServices.myUsernameChanged.connect(onUsernameChanged); + ui.tablet.webEventReceived.connect(onWebEventReceived); + WalletScriptingInterface.walletStatusChanged.connect(sendCommerceSettings); + Window.messageBoxClosed.connect(onMessageBoxClosed); + ResourceRequestObserver.resourceRequestEvent.connect(onResourceRequestEvent); + + WalletScriptingInterface.refreshWalletStatus(); +} + +function off() { +} +function shutdown() { + maybeEnableHMDPreview(); + + Window.messageBoxClosed.disconnect(onMessageBoxClosed); + WalletScriptingInterface.walletStatusChanged.disconnect(sendCommerceSettings); + ui.tablet.webEventReceived.disconnect(onWebEventReceived); + GlobalServices.myUsernameChanged.disconnect(onUsernameChanged); + Entities.canWriteAssetsChanged.disconnect(onCanWriteAssetsChanged); + ContextOverlay.contextOverlayClicked.disconnect(openInspectionCertificateQML); + ResourceRequestObserver.resourceRequestEvent.disconnect(onResourceRequestEvent); + + off(); +} + +// +// Run the functions. +// +startup(); +Script.scriptEnding.connect(shutdown); }()); // END LOCAL_SCOPE diff --git a/scripts/system/miniTablet.js b/scripts/system/miniTablet.js new file mode 100644 index 0000000000..cc201f58f7 --- /dev/null +++ b/scripts/system/miniTablet.js @@ -0,0 +1,1094 @@ +// +// miniTablet.js +// +// Created by David Rowe on 9 Aug 2018. +// Copyright 2018 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +/* global getTabletWidthFromSettings, TRIGGER_OFF_VALUE */ + +(function () { + + "use strict"; + + Script.include("./libraries/utils.js"); + Script.include("./libraries/controllerDispatcherUtils.js"); + + var UI, + ui = null, + State, + miniState = null, + miniTabletEnabled = true, + + // Hands. + NO_HAND = -1, + LEFT_HAND = 0, + RIGHT_HAND = 1, + HAND_NAMES = ["LeftHand", "RightHand"], + + // Track controller grabbing state. + HIFI_OBJECT_MANIPULATION_CHANNEL = "Hifi-Object-Manipulation", + grabbingHand = NO_HAND, + grabbedItem = null, + + // Miscellaneous. + tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"), + DEBUG = false; + + + function debug(message) { + if (!DEBUG) { + return; + } + print("DEBUG: " + message); + } + + function error(message) { + print("ERROR: " + message); + } + + function handJointName(hand) { + var jointName; + if (hand === LEFT_HAND) { + if (Camera.mode === "first person") { + jointName = "_CONTROLLER_LEFTHAND"; + } else if (Camera.mode === "third person") { + jointName = "_CAMERA_RELATIVE_CONTROLLER_LEFTHAND"; + } else { + jointName = "LeftHand"; + } + } else { + if (Camera.mode === "first person") { + jointName = "_CONTROLLER_RIGHTHAND"; + } else if (Camera.mode === "third person") { + jointName = "_CAMERA_RELATIVE_CONTROLLER_RIGHTHAND"; + } else { + jointName = "RightHand"; + } + } + return jointName; + } + + function handJointIndex(hand) { + return MyAvatar.getJointIndex(handJointName(hand)); + } + + function otherHand(hand) { + return hand === LEFT_HAND ? RIGHT_HAND : LEFT_HAND; + } + + + UI = function () { + + if (!(this instanceof UI)) { + return new UI(); + } + + var // Base overlay + miniOverlay = null, + MINI_MODEL = Script.resolvePath("./assets/models/miniTabletBlank.fbx"), + MINI_DIMENSIONS = { x: 0.0637, y: 0.0965, z: 0.0045 }, // Proportional to tablet proper. + MINI_POSITIONS = [ + { + x: -0.01, // Distance across hand. + y: 0.08, // Distance from joint. + z: 0.06 // Distance above palm. + }, + { + x: 0.01, // Distance across hand. + y: 0.08, // Distance from joint. + z: 0.06 // Distance above palm. + } + ], + DEGREES_180 = 180, + MINI_PICTH = 40, + MINI_ROTATIONS = [ + Quat.fromVec3Degrees({ x: 0, y: DEGREES_180 - MINI_PICTH, z: 90 }), + Quat.fromVec3Degrees({ x: 0, y: DEGREES_180 + MINI_PICTH, z: -90 }) + ], + + // UI overlay. + uiHand = LEFT_HAND, + miniUIOverlay = null, + MINI_UI_HTML = Script.resolvePath("./html/miniTablet.html"), + MINI_UI_DIMENSIONS = { x: 0.059, y: 0.0865 }, + MINI_UI_WIDTH_PIXELS = 150, + METERS_TO_INCHES = 39.3701, + MINI_UI_DPI = MINI_UI_WIDTH_PIXELS / (MINI_UI_DIMENSIONS.x * METERS_TO_INCHES), + MINI_UI_OFFSET = 0.001, // Above model surface. + MINI_UI_LOCAL_POSITION = { x: 0.0002, y: 0.0024, z: -(MINI_DIMENSIONS.z / 2 + MINI_UI_OFFSET) }, + MINI_UI_LOCAL_ROTATION = Quat.fromVec3Degrees({ x: 0, y: 180, z: 0 }), + miniUIOverlayEnabled = false, + MINI_UI_OVERLAY_ENABLED_DELAY = 500, + miniOverlayObject = null, + + // Button icons. + MUTE_ON_ICON = Script.resourcesPath() + "icons/tablet-icons/mic-mute-a.svg", + MUTE_OFF_ICON = Script.resourcesPath() + "icons/tablet-icons/mic-unmute-i.svg", + GOTO_ICON = Script.resourcesPath() + "icons/tablet-icons/goto-i.svg", + + // Expansion to tablet. + MINI_EXPAND_HANDLES = [ // Normalized coordinates in range [-0.5, 0.5] about center of mini tablet. + { x: 0.5, y: -0.65, z: 0 }, + { x: -0.5, y: -0.65, z: 0 } + ], + MINI_EXPAND_DELTA_ROTATION = Quat.fromVec3Degrees({ x: -5, y: 0, z: 0 }), + MINI_EXPAND_HANDLES_OTHER = [ // Different handles when expanding after being grabbed by other hand, + { x: 0.5, y: -0.4, z: 0 }, + { x: -0.5, y: -0.4, z: 0 } + ], + MINI_EXPAND_DELTA_ROTATION_OTHER = Quat.IDENTITY, + miniExpandHand, + miniExpandHandles = MINI_EXPAND_HANDLES, + miniExpandDeltaRotation = MINI_EXPAND_HANDLES_OTHER, + miniExpandLocalPosition, + miniExpandLocalRotation = Quat.IDENTITY, + miniInitialWidth, + miniTargetWidth, + miniTargetLocalRotation, + + // Laser pointing at. + isBodyHovered = false, + + // EventBridge. + READY_MESSAGE = "ready", // Engine <== Dialog + HOVER_MESSAGE = "hover", // Engine <== Dialog + UNHOVER_MESSAGE = "unhover", // Engine <== Dialog + MUTE_MESSAGE = "mute", // Engine <=> Dialog + GOTO_MESSAGE = "goto", // Engine <=> Dialog + EXPAND_MESSAGE = "expand", // Engine <== Dialog + + // Sounds. + HOVER_SOUND = "./assets/sounds/button-hover.wav", + HOVER_VOLUME = 0.5, + CLICK_SOUND = "./assets/sounds/button-click.wav", + CLICK_VOLUME = 0.8, + hoverSound = SoundCache.getSound(Script.resolvePath(HOVER_SOUND)), + clickSound = SoundCache.getSound(Script.resolvePath(CLICK_SOUND)); + + + function updateMutedStatus() { + var isMuted = Audio.muted; + miniOverlayObject.emitScriptEvent(JSON.stringify({ + type: MUTE_MESSAGE, + on: isMuted, + icon: isMuted ? MUTE_ON_ICON : MUTE_OFF_ICON + })); + } + + function setGotoIcon() { + miniOverlayObject.emitScriptEvent(JSON.stringify({ + type: GOTO_MESSAGE, + icon: GOTO_ICON + })); + } + + function onWebEventReceived(data) { + var message; + + try { + message = JSON.parse(data); + } catch (e) { + console.error("EventBridge message error"); + return; + } + + switch (message.type) { + case READY_MESSAGE: + // Send initial button statuses. + updateMutedStatus(); + setGotoIcon(); + break; + case HOVER_MESSAGE: + if (message.target === "body") { + // Laser status. + isBodyHovered = true; + } else if (message.target === "button") { + // Audio feedback. + playSound(hoverSound, HOVER_VOLUME); + } + break; + case UNHOVER_MESSAGE: + if (message.target === "body") { + // Laser status. + isBodyHovered = false; + } + break; + case MUTE_MESSAGE: + // Toggle mute. + playSound(clickSound, CLICK_VOLUME); + Audio.muted = !Audio.muted; + break; + case GOTO_MESSAGE: + // Goto. + playSound(clickSound, CLICK_VOLUME); + miniState.setState(miniState.MINI_EXPANDING, { hand: uiHand, goto: true }); + break; + case EXPAND_MESSAGE: + // Expand tablet; + playSound(clickSound, CLICK_VOLUME); + miniState.setState(miniState.MINI_EXPANDING, { hand: uiHand, goto: false }); + break; + } + } + + + function updateMiniTabletID() { + HMD.miniTabletID = miniOverlay; + HMD.miniTabletScreenID = miniUIOverlay; + HMD.miniTabletHand = miniOverlay ? uiHand : -1; + } + + function playSound(sound, volume) { + Audio.playSound(sound, { + position: uiHand === LEFT_HAND ? MyAvatar.getLeftPalmPosition() : MyAvatar.getRightPalmPosition(), + volume: volume, + localOnly: true + }); + } + + + function getUIPositionAndRotation(hand) { + return { + position: MINI_POSITIONS[hand], + rotation: MINI_ROTATIONS[hand] + }; + } + + function getMiniTabletID() { + return miniOverlay; + } + + function getMiniTabletProperties() { + var properties = Overlays.getProperties(miniOverlay, ["position", "orientation"]); + return { + position: properties.position, + orientation: properties.orientation + }; + } + + function isLaserPointingAt() { + return isBodyHovered; + } + + + function show(hand) { + var initialScale = 0.01; // Start very small. + + uiHand = hand; + + Overlays.editOverlay(miniOverlay, { + parentID: MyAvatar.SELF_ID, + parentJointIndex: handJointIndex(hand), + localPosition: Vec3.multiply(MyAvatar.sensorToWorldScale, MINI_POSITIONS[hand]), + localRotation: MINI_ROTATIONS[hand], + dimensions: Vec3.multiply(initialScale, MINI_DIMENSIONS), + grabbable: true, + visible: true + }); + Overlays.editOverlay(miniUIOverlay, { + localPosition: Vec3.multiply(MyAvatar.sensorToWorldScale, MINI_UI_LOCAL_POSITION), + localRotation: MINI_UI_LOCAL_ROTATION, + dimensions: Vec3.multiply(initialScale, MINI_UI_DIMENSIONS), + dpi: MINI_UI_DPI / initialScale, + visible: true + }); + + updateMiniTabletID(); + + if (!miniUIOverlayEnabled) { + // Overlay content is created the first time it is visible to the user. The initial creation displays artefacts. + // Delay showing UI overlay until after giving it time for its content to be created. + Script.setTimeout(function () { + Overlays.editOverlay(miniUIOverlay, { alpha: 1.0 }); + }, MINI_UI_OVERLAY_ENABLED_DELAY); + } + } + + function size(scaleFactor) { + // Scale UI in place. + Overlays.editOverlay(miniOverlay, { + dimensions: Vec3.multiply(scaleFactor, MINI_DIMENSIONS) + }); + Overlays.editOverlay(miniUIOverlay, { + localPosition: Vec3.multiply(scaleFactor, MINI_UI_LOCAL_POSITION), + dimensions: Vec3.multiply(scaleFactor, MINI_UI_DIMENSIONS), + dpi: MINI_UI_DPI / scaleFactor + }); + updateRotation(); + } + + function startExpandingTablet(hand) { + // Expansion details. + if (hand === uiHand) { + miniExpandHandles = MINI_EXPAND_HANDLES; + miniExpandDeltaRotation = MINI_EXPAND_DELTA_ROTATION; + } else { + miniExpandHandles = MINI_EXPAND_HANDLES_OTHER; + miniExpandDeltaRotation = MINI_EXPAND_DELTA_ROTATION_OTHER; + } + + // Grab details. + var properties = Overlays.getProperties(miniOverlay, ["localPosition", "localRotation"]); + miniExpandHand = hand; + miniExpandLocalRotation = properties.localRotation; + miniExpandLocalPosition = Vec3.sum(properties.localPosition, + Vec3.multiplyQbyV(miniExpandLocalRotation, + Vec3.multiplyVbyV(miniExpandHandles[miniExpandHand], MINI_DIMENSIONS))); + + // Start expanding. + miniInitialWidth = MINI_DIMENSIONS.x; + miniTargetWidth = getTabletWidthFromSettings(); + miniTargetLocalRotation = Quat.multiply(miniExpandLocalRotation, miniExpandDeltaRotation); + } + + function sizeAboutHandles(scaleFactor) { + // Scale UI and move per handles. + var tabletScaleFactor, + dimensions, + localRotation, + localPosition; + + tabletScaleFactor = MyAvatar.sensorToWorldScale + * (1 + scaleFactor * (miniTargetWidth - miniInitialWidth) / miniInitialWidth); + dimensions = Vec3.multiply(tabletScaleFactor, MINI_DIMENSIONS); + localRotation = Quat.mix(miniExpandLocalRotation, miniTargetLocalRotation, scaleFactor); + localPosition = + Vec3.sum(miniExpandLocalPosition, + Vec3.multiplyQbyV(miniExpandLocalRotation, + Vec3.multiply(-tabletScaleFactor, + Vec3.multiplyVbyV(miniExpandHandles[miniExpandHand], MINI_DIMENSIONS))) + ); + localPosition = Vec3.sum(localPosition, + Vec3.multiplyQbyV(miniExpandLocalRotation, { x: 0, y: 0.5 * -dimensions.y, z: 0 })); + localPosition = Vec3.sum(localPosition, + Vec3.multiplyQbyV(localRotation, { x: 0, y: 0.5 * dimensions.y, z: 0 })); + Overlays.editOverlay(miniOverlay, { + localPosition: localPosition, + localRotation: localRotation, + dimensions: dimensions + }); + // FIXME: Temporary code change to try not displaying UI when mini tablet is expanding to become the tablet proper. + Overlays.editOverlay(miniUIOverlay, { + /* + localPosition: Vec3.multiply(tabletScaleFactor, MINI_UI_LOCAL_POSITION), + dimensions: Vec3.multiply(tabletScaleFactor, MINI_UI_DIMENSIONS), + dpi: MINI_UI_DPI / tabletScaleFactor + */ + visible: false + }); + } + + function updateRotation() { + // Update the rotation of the tablet about its face normal so that its base is horizontal. + var COS_5_DEGREES = 0.996, + RADIANS_TO_DEGREES = DEGREES_180 / Math.PI, + defaultLocalRotation, + handOrientation, + defaultOrientation, + faceNormal, + desiredOrientation, + defaultYAxis, + desiredYAxis, + cross, + dot, + deltaAngle, + deltaRotation, + localRotation; + + if (Overlays.getProperty(miniOverlay, "parentJointIndex") !== handJointIndex(uiHand)) { + // Overlay has been grabbed by other hand but this script hasn't received notification yet. + return; + } + + defaultLocalRotation = MINI_ROTATIONS[uiHand]; + handOrientation = + Quat.multiply(MyAvatar.orientation, MyAvatar.getAbsoluteJointRotationInObjectFrame(handJointIndex(uiHand))); + defaultOrientation = Quat.multiply(handOrientation, defaultLocalRotation); + faceNormal = Vec3.multiplyQbyV(defaultOrientation, Vec3.UNIT_Z); + + if (Math.abs(Vec3.dot(faceNormal, Vec3.UNIT_Y)) > COS_5_DEGREES) { + // Don't rotate mini tablet if almost flat in the x-z plane. + return; + } else { + // Rotate the tablet so that its base is parallel with the x-z plane. + desiredOrientation = Quat.lookAt(Vec3.ZERO, Vec3.multiplyQbyV(defaultOrientation, Vec3.UNIT_Z), Vec3.UNIT_Y); + defaultYAxis = Vec3.multiplyQbyV(defaultOrientation, Vec3.UNIT_Y); + desiredYAxis = Vec3.multiplyQbyV(desiredOrientation, Vec3.UNIT_Y); + cross = Vec3.cross(defaultYAxis, desiredYAxis); + dot = Vec3.dot(defaultYAxis, desiredYAxis); + deltaAngle = Math.atan2(Vec3.length(cross), dot) * RADIANS_TO_DEGREES; + if (Vec3.dot(cross, Vec3.multiplyQbyV(desiredOrientation, Vec3.UNIT_Z)) > 0) { + deltaAngle = -deltaAngle; + } + deltaRotation = Quat.angleAxis(deltaAngle, Vec3.multiplyQbyV(defaultLocalRotation, Vec3.UNIT_Z)); + localRotation = Quat.multiply(deltaRotation, defaultLocalRotation); + } + Overlays.editOverlay(miniOverlay, { + localRotation: localRotation + }); + } + + function release() { + Overlays.editOverlay(miniOverlay, { + parentID: Uuid.NULL, // Release hold so that hand can grab tablet proper. + grabbable: false + }); + } + + function hide() { + Overlays.editOverlay(miniOverlay, { + visible: false + }); + Overlays.editOverlay(miniUIOverlay, { + visible: false + }); + } + + function create() { + miniOverlay = Overlays.addOverlay("model", { + url: MINI_MODEL, + dimensions: Vec3.multiply(MyAvatar.sensorToWorldScale, MINI_DIMENSIONS), + solid: true, + grabbable: true, + showKeyboardFocusHighlight: false, + displayInFront: true, + visible: false + }); + miniUIOverlay = Overlays.addOverlay("web3d", { + url: MINI_UI_HTML, + parentID: miniOverlay, + localPosition: Vec3.multiply(MyAvatar.sensorToWorldScale, MINI_UI_LOCAL_POSITION), + localRotation: MINI_UI_LOCAL_ROTATION, + dimensions: Vec3.multiply(MyAvatar.sensorToWorldScale, MINI_UI_DIMENSIONS), + dpi: MINI_UI_DPI / MyAvatar.sensorToWorldScale, + alpha: 0, // Hide overlay while its content is being created. + grabbable: false, + showKeyboardFocusHighlight: false, + displayInFront: true, + visible: false + }); + + miniUIOverlayEnabled = false; // This and alpha = 0 hides overlay while its content is being created. + + miniOverlayObject = Overlays.getOverlayObject(miniUIOverlay); + miniOverlayObject.webEventReceived.connect(onWebEventReceived); + } + + function destroy() { + if (miniOverlayObject) { + miniOverlayObject.webEventReceived.disconnect(onWebEventReceived); + Overlays.deleteOverlay(miniUIOverlay); + Overlays.deleteOverlay(miniOverlay); + miniOverlayObject = null; + miniUIOverlay = null; + miniOverlay = null; + updateMiniTabletID(); + } + } + + create(); + + return { + getUIPositionAndRotation: getUIPositionAndRotation, + getMiniTabletID: getMiniTabletID, + getMiniTabletProperties: getMiniTabletProperties, + isLaserPointingAt: isLaserPointingAt, + updateMutedStatus: updateMutedStatus, + show: show, + size: size, + startExpandingTablet: startExpandingTablet, + sizeAboutHandles: sizeAboutHandles, + updateRotation: updateRotation, + release: release, + hide: hide, + destroy: destroy + }; + + }; + + + State = function () { + + if (!(this instanceof State)) { + return new State(); + } + + var + // States. + MINI_DISABLED = 0, + MINI_HIDDEN = 1, + MINI_HIDING = 2, + MINI_SHOWING = 3, + MINI_VISIBLE = 4, + MINI_EXPANDING = 5, + TABLET_OPEN = 6, + STATE_STRINGS = ["MINI_DISABLED", "MINI_HIDDEN", "MINI_HIDING", "MINI_SHOWING", "MINI_VISIBLE", "MINI_EXPANDING", + "TABLET_OPEN"], + STATE_MACHINE, + miniState = MINI_DISABLED, + miniHand, + + // Mini tablet scaling. + MINI_SCALE_DURATION = 250, + MINI_SCALE_TIMEOUT = 20, + miniScaleTimer = null, + miniScaleStart, + + // Expansion to tablet. + MINI_EXPAND_DURATION = 250, + MINI_EXPAND_TIMEOUT = 20, + miniExpandTimer = null, + miniExpandStart, + miniExpandedStartTime, + + // Tablet targets. + isGoto = false, + TABLET_ADDRESS_DIALOG = "hifi/tablet/TabletAddressDialog.qml", + + // Trigger values. + leftTriggerOn = 0, + rightTriggerOn = 0, + MAX_TRIGGER_ON_TIME = 100, + + // Visibility. + MAX_HAND_CAMERA_ANGLE = 30, + MAX_CAMERA_HAND_ANGLE = 30, + DEGREES_180 = 180, + MAX_HAND_CAMERA_ANGLE_COS = Math.cos(Math.PI * MAX_HAND_CAMERA_ANGLE / DEGREES_180), + MAX_CAMERA_HAND_ANGLE_COS = Math.cos(Math.PI * MAX_CAMERA_HAND_ANGLE / DEGREES_180), + HIDING_DELAY = 1000, // ms + lastVisible = [0, 0]; + + + function enterMiniDisabled() { + // Stop updates. + Script.update.disconnect(updateState); + + // Stop monitoring mute changes. + Audio.mutedChanged.disconnect(ui.updateMutedStatus); + + // Don't keep overlays prepared if in desktop mode. + ui.destroy(); + ui = null; + } + + function exitMiniDisabled() { + // Create UI so that it's ready to be displayed without seeing artefacts from creating the UI. + ui = new UI(); + + // Start monitoring mute changes. + Audio.mutedChanged.connect(ui.updateMutedStatus); + + // Start updates. + Script.update.connect(updateState); + } + + function shouldShowMini(hand) { + // Should show mini tablet if it would be oriented toward the camera. + var show, + pose, + isLeftTriggerOff, + isRightTriggerOff, + wasLeftTriggerOff = true, + wasRightTriggerOff = true, + jointIndex, + handPosition, + handOrientation, + uiPositionAndOrientation, + miniPosition, + miniOrientation, + miniToCameraDirection, + cameraToHand; + + // Shouldn't show mini tablet if hand isn't being controlled. + pose = Controller.getPoseValue(hand === LEFT_HAND ? Controller.Standard.LeftHand : Controller.Standard.RightHand); + show = pose.valid; + + // Shouldn't show mini tablet on hand if that hand's trigger or grip are pressed (i.e., laser is searching or hand + // is grabbing something) or the other hand's trigger is pressed unless it is pointing at the mini tablet. Allow + // the triggers to be pressed briefly to allow for the grabbing process. + if (show) { + isLeftTriggerOff = Controller.getValue(Controller.Standard.LT) < TRIGGER_OFF_VALUE + && Controller.getValue(Controller.Standard.LeftGrip) < TRIGGER_OFF_VALUE; + if (!isLeftTriggerOff) { + if (leftTriggerOn === 0) { + leftTriggerOn = Date.now(); + } else { + wasLeftTriggerOff = Date.now() - leftTriggerOn < MAX_TRIGGER_ON_TIME; + } + } else { + leftTriggerOn = 0; + } + isRightTriggerOff = Controller.getValue(Controller.Standard.RT) < TRIGGER_OFF_VALUE + && Controller.getValue(Controller.Standard.RightGrip) < TRIGGER_OFF_VALUE; + if (!isRightTriggerOff) { + if (rightTriggerOn === 0) { + rightTriggerOn = Date.now(); + } else { + wasRightTriggerOff = Date.now() - rightTriggerOn < MAX_TRIGGER_ON_TIME; + } + } else { + rightTriggerOn = 0; + } + + show = (hand === LEFT_HAND ? wasLeftTriggerOff : wasRightTriggerOff) + && ((hand === LEFT_HAND ? wasRightTriggerOff : wasLeftTriggerOff) || ui.isLaserPointingAt()); + } + + // Should show mini tablet if it would be oriented toward the camera. + if (show) { + // Calculate current visibility of mini tablet. + jointIndex = handJointIndex(hand); + handPosition = Vec3.sum(MyAvatar.position, + Vec3.multiplyQbyV(MyAvatar.orientation, MyAvatar.getAbsoluteJointTranslationInObjectFrame(jointIndex))); + handOrientation = + Quat.multiply(MyAvatar.orientation, MyAvatar.getAbsoluteJointRotationInObjectFrame(jointIndex)); + uiPositionAndOrientation = ui.getUIPositionAndRotation(hand); + miniPosition = Vec3.sum(handPosition, Vec3.multiply(MyAvatar.sensorToWorldScale, + Vec3.multiplyQbyV(handOrientation, uiPositionAndOrientation.position))); + miniOrientation = Quat.multiply(handOrientation, uiPositionAndOrientation.rotation); + miniToCameraDirection = Vec3.normalize(Vec3.subtract(Camera.position, miniPosition)); + show = Vec3.dot(miniToCameraDirection, Quat.getForward(miniOrientation)) > MAX_HAND_CAMERA_ANGLE_COS; + show = show || (-Vec3.dot(miniToCameraDirection, Quat.getForward(handOrientation)) > MAX_HAND_CAMERA_ANGLE_COS); + cameraToHand = -Vec3.dot(miniToCameraDirection, Quat.getForward(Camera.orientation)); + show = show && (cameraToHand > MAX_CAMERA_HAND_ANGLE_COS); + + // Persist showing for a while after it would otherwise be hidden. + if (show) { + lastVisible[hand] = Date.now(); + } else { + show = Date.now() - lastVisible[hand] <= HIDING_DELAY; + } + } + + return { + show: show, + cameraToHand: cameraToHand + }; + } + + function enterMiniHidden() { + ui.release(); + ui.hide(); + } + + function updateMiniHidden() { + var showLeft, + showRight; + + // Don't show mini tablet if tablet proper is already displayed, in toolbar mode, or away. + if (HMD.showTablet || tablet.toolbarMode || MyAvatar.isAway) { + return; + } + + // Show mini tablet if it would be pointing at the camera. + showLeft = shouldShowMini(LEFT_HAND); + showRight = shouldShowMini(RIGHT_HAND); + if (showLeft.show && showRight.show) { + // Both hands would be pointing at camera; show the one the camera is gazing at. + if (showLeft.cameraToHand > showRight.cameraToHand) { + setState(MINI_SHOWING, LEFT_HAND); + } else { + setState(MINI_SHOWING, RIGHT_HAND); + } + } else if (showLeft.show) { + setState(MINI_SHOWING, LEFT_HAND); + } else if (showRight.show) { + setState(MINI_SHOWING, RIGHT_HAND); + } + } + + function scaleMiniDown() { + var scaleFactor = (Date.now() - miniScaleStart) / MINI_SCALE_DURATION; + if (scaleFactor < 1) { + ui.size((1 - scaleFactor) * MyAvatar.sensorToWorldScale); + miniScaleTimer = Script.setTimeout(scaleMiniDown, MINI_SCALE_TIMEOUT); + return; + } + miniScaleTimer = null; + setState(MINI_HIDDEN); + } + + function enterMiniHiding() { + miniScaleStart = Date.now(); + miniScaleTimer = Script.setTimeout(scaleMiniDown, MINI_SCALE_TIMEOUT); + } + + function updateMiniHiding() { + if (HMD.showTablet) { + setState(MINI_HIDDEN); + } + } + + function exitMiniHiding() { + if (miniScaleTimer) { + Script.clearTimeout(miniScaleTimer); + miniScaleTimer = null; + } + } + + function scaleMiniUp() { + var scaleFactor = (Date.now() - miniScaleStart) / MINI_SCALE_DURATION; + if (scaleFactor < 1) { + ui.size(scaleFactor * MyAvatar.sensorToWorldScale); + miniScaleTimer = Script.setTimeout(scaleMiniUp, MINI_SCALE_TIMEOUT); + return; + } + miniScaleTimer = null; + ui.size(MyAvatar.sensorToWorldScale); + setState(MINI_VISIBLE); + } + + function checkMiniVisibility() { + var showLeft, + showRight; + + // Check that the mini tablet should still be visible and if so then ensure it's on the hand that the camera is + // gazing at. + showLeft = shouldShowMini(LEFT_HAND); + showRight = shouldShowMini(RIGHT_HAND); + if (showLeft.show && showRight.show) { + if (showLeft.cameraToHand > showRight.cameraToHand) { + if (miniHand !== LEFT_HAND) { + setState(MINI_HIDING); + } + } else { + if (miniHand !== RIGHT_HAND) { + setState(MINI_HIDING); + } + } + } else if (showLeft.show) { + if (miniHand !== LEFT_HAND) { + setState(MINI_HIDING); + } + } else if (showRight.show) { + if (miniHand !== RIGHT_HAND) { + setState(MINI_HIDING); + } + } else { + if (grabbedItem === null || grabbingHand !== miniHand) { + setState(MINI_HIDING); + } else { + setState(MINI_HIDDEN); + } + } + } + + function enterMiniShowing(hand) { + miniHand = hand; + ui.show(miniHand); + miniScaleStart = Date.now(); + miniScaleTimer = Script.setTimeout(scaleMiniUp, MINI_SCALE_TIMEOUT); + } + + function updateMiniShowing() { + // Hide mini tablet if tablet proper has been displayed by other means. + if (HMD.showTablet) { + setState(MINI_HIDDEN); + } + + // Hide mini tablet if it should no longer be visible. + checkMiniVisibility(); + } + + function exitMiniShowing() { + if (miniScaleTimer) { + Script.clearTimeout(miniScaleTimer); + miniScaleTimer = null; + } + } + + function updateMiniVisible() { + // Hide mini tablet if tablet proper has been displayed by other means. + if (HMD.showTablet) { + setState(MINI_HIDDEN); + return; + } + + // Hide mini tablet if it should no longer be visible. + checkMiniVisibility(); + + // If state hasn't changed, update mini tablet rotation. + if (miniState === MINI_VISIBLE) { + ui.updateRotation(); + } + } + + function expandMini() { + var scaleFactor = (Date.now() - miniExpandStart) / MINI_EXPAND_DURATION; + if (scaleFactor < 1) { + ui.sizeAboutHandles(scaleFactor); + miniExpandTimer = Script.setTimeout(expandMini, MINI_EXPAND_TIMEOUT); + return; + } + miniExpandTimer = null; + setState(TABLET_OPEN); + } + + function enterMiniExpanding(data) { + isGoto = data.goto; + ui.startExpandingTablet(data.hand); + miniExpandStart = Date.now(); + miniExpandTimer = Script.setTimeout(expandMini, MINI_EXPAND_TIMEOUT); + } + + function updateMiniExanding() { + // Hide mini tablet immediately if tablet proper has been displayed by other means. + if (HMD.showTablet) { + setState(MINI_HIDDEN); + } + } + + function exitMiniExpanding() { + if (miniExpandTimer !== null) { + Script.clearTimeout(miniExpandTimer); + miniExpandTimer = null; + } + } + + function enterTabletOpen() { + var miniTabletProperties; + + if (isGoto) { + tablet.loadQMLSource(TABLET_ADDRESS_DIALOG); + } else { + tablet.gotoHomeScreen(); + } + + ui.release(); + miniTabletProperties = ui.getMiniTabletProperties(); + + Overlays.editOverlay(HMD.tabletID, { + position: miniTabletProperties.position, + orientation: miniTabletProperties.orientation + }); + + HMD.openTablet(true); + + miniExpandedStartTime = Date.now(); + } + + function updateTabletOpen() { + // Give the tablet proper time to rez before hiding expanded mini tablet. + // The mini tablet is also hidden elsewhere if the tablet proper is grabbed. + var TABLET_OPENING_DELAY = 500; + if (Date.now() >= miniExpandedStartTime + TABLET_OPENING_DELAY) { + setState(MINI_HIDDEN); + } + } + + STATE_MACHINE = { + MINI_DISABLED: { // Mini tablet cannot be shown because in desktop mode. + enter: enterMiniDisabled, + update: null, + exit: exitMiniDisabled + }, + MINI_HIDDEN: { // Mini tablet could be shown but isn't because hand is oriented to show it or aren't in HMD mode. + enter: enterMiniHidden, + update: updateMiniHidden, + exit: null + }, + MINI_HIDING: { // Mini tablet is reducing from MINI_VISIBLE to MINI_HIDDEN. + enter: enterMiniHiding, + update: updateMiniHiding, + exit: exitMiniHiding + }, + MINI_SHOWING: { // Mini tablet is expanding from MINI_HIDDN to MINI_VISIBLE. + enter: enterMiniShowing, + update: updateMiniShowing, + exit: exitMiniShowing + }, + MINI_VISIBLE: { // Mini tablet is visible and attached to hand. + enter: null, + update: updateMiniVisible, + exit: null + }, + MINI_EXPANDING: { // Mini tablet is expanding before showing tablet proper. + enter: enterMiniExpanding, + update: updateMiniExanding, + exit: exitMiniExpanding + }, + TABLET_OPEN: { // Tablet proper is being displayed. + enter: enterTabletOpen, + update: updateTabletOpen, + exit: null + } + }; + + function setState(state, data) { + if (state !== miniState) { + debug("State transition from " + STATE_STRINGS[miniState] + " to " + STATE_STRINGS[state] + + ( data ? " " + JSON.stringify(data) : "")); + if (STATE_MACHINE[STATE_STRINGS[miniState]].exit) { + STATE_MACHINE[STATE_STRINGS[miniState]].exit(data); + } + if (STATE_MACHINE[STATE_STRINGS[state]].enter) { + STATE_MACHINE[STATE_STRINGS[state]].enter(data); + } + miniState = state; + } else { + error("Null state transition: " + state + "!"); + } + } + + function getState() { + return miniState; + } + + function getHand() { + return miniHand; + } + + function updateState() { + if (STATE_MACHINE[STATE_STRINGS[miniState]].update) { + STATE_MACHINE[STATE_STRINGS[miniState]].update(); + } + } + + function create() { + // Nothing to do. + } + + function destroy() { + if (miniState !== MINI_DISABLED) { + setState(MINI_DISABLED); + } + } + + create(); + + return { + MINI_DISABLED: MINI_DISABLED, + MINI_HIDDEN: MINI_HIDDEN, + MINI_HIDING: MINI_HIDING, + MINI_SHOWING: MINI_SHOWING, + MINI_VISIBLE: MINI_VISIBLE, + MINI_EXPANDING: MINI_EXPANDING, + TABLET_OPEN: TABLET_OPEN, + setState: setState, + getState: getState, + getHand: getHand, + destroy: destroy + }; + }; + + + function onMessageReceived(channel, data, senderID, localOnly) { + var message, + miniHand, + hand; + + if (channel !== HIFI_OBJECT_MANIPULATION_CHANNEL) { + return; + } + + try { + message = JSON.parse(data); + } catch (e) { + return; + } + + // Track grabbed state and item. + switch (message.action) { + case "grab": + grabbingHand = HAND_NAMES.indexOf(message.joint); + grabbedItem = message.grabbedEntity; + break; + case "release": + grabbingHand = NO_HAND; + grabbedItem = null; + break; + default: + error("Unexpected grab message!"); + return; + } + + if (miniState.getState() === miniState.MINI_DISABLED + || (message.grabbedEntity !== HMD.tabletID && message.grabbedEntity !== ui.getMiniTabletID())) { + return; + } + + if (message.action === "grab" && message.grabbedEntity === HMD.tabletID && HMD.active) { + // Tablet may have been grabbed after it replaced expanded mini tablet. + if (miniTabletEnabled) { + miniState.setState(miniState.MINI_HIDDEN); + } + } else if (message.action === "grab" && miniState.getState() === miniState.MINI_VISIBLE) { + if (miniTabletEnabled) { + miniHand = miniState.getHand(); + hand = message.joint === HAND_NAMES[miniHand] ? miniHand : otherHand(miniHand); + miniState.setState(miniState.MINI_EXPANDING, { hand: hand, goto: false }); + } + } + } + + function onWentAway() { + // Mini tablet only available when user is not away. + if (HMD.active && miniTabletEnabled) { + miniState.setState(miniState.MINI_HIDDEN); + } + } + + function onDisplayModeChanged() { + // Mini tablet only available when HMD is active. + if (HMD.active) { + if (miniTabletEnabled && miniState.getState() !== miniState.MINI_HIDDEN) { + miniState.setState(miniState.MINI_HIDDEN); + } + } else if (miniState.getState() !== miniState.MINI_DISABLED) { + miniState.setState(miniState.MINI_DISABLED); + } + } + + function onMiniTabletEnabledChanged(enabled) { + miniTabletEnabled = enabled; + if (miniTabletEnabled) { + if (HMD.active && miniState.getState() !== miniState.MINI_HIDDEN) { + miniState.setState(miniState.MINI_HIDDEN); + } + } else if (miniState.getState() !== miniState.MINI_DISABLED) { + miniState.setState(miniState.MINI_DISABLED); + } + } + + + function setUp() { + miniState = new State(); + + HMD.miniTabletEnabledChanged.connect(onMiniTabletEnabledChanged); + miniTabletEnabled = HMD.miniTabletEnabled; + + Messages.subscribe(HIFI_OBJECT_MANIPULATION_CHANNEL); + Messages.messageReceived.connect(onMessageReceived); + + MyAvatar.wentAway.connect(onWentAway); + HMD.displayModeChanged.connect(onDisplayModeChanged); + + if (HMD.active && miniTabletEnabled) { + miniState.setState(miniState.MINI_HIDDEN); + } + } + + function tearDown() { + miniState.setState(miniState.MINI_DISABLED); + + HMD.displayModeChanged.disconnect(onDisplayModeChanged); + MyAvatar.wentAway.disconnect(onWentAway); + + Messages.messageReceived.disconnect(onMessageReceived); + Messages.unsubscribe(HIFI_OBJECT_MANIPULATION_CHANNEL); + + HMD.miniTabletEnabledChanged.disconnect(onMiniTabletEnabledChanged); + + miniState.destroy(); + miniState = null; + } + + setUp(); + Script.scriptEnding.connect(tearDown); + +}()); diff --git a/scripts/system/modules/createWindow.js b/scripts/system/modules/createWindow.js index 7369cf91f8..0c4412abfb 100644 --- a/scripts/system/modules/createWindow.js +++ b/scripts/system/modules/createWindow.js @@ -125,6 +125,9 @@ module.exports = (function() { Script.scriptEnding.connect(this, function() { this.window.close(); + // FIXME: temp solution for reload crash (MS18269), + // we should decide on proper object ownership strategy for InteractiveWindow API + this.window = null; }); }, setVisible: function(visible) { diff --git a/scripts/system/notifications.js b/scripts/system/notifications.js index 0778e2a44b..1d6b4dada3 100644 --- a/scripts/system/notifications.js +++ b/scripts/system/notifications.js @@ -343,6 +343,7 @@ } var CLOSE_NOTIFICATION_ICON = Script.resolvePath("assets/images/close-small-light.svg"); + var TEXT_OVERLAY_FONT_SIZE_IN_PIXELS = 18.0; // taken from TextOverlay::textSize // This function creates and sizes the overlays function createNotification(text, notificationType, imageProperties) { @@ -362,7 +363,7 @@ if (text.length >= breakPoint) { breaks = count; } - extraLine = breaks * 16.0; + extraLine = breaks * TEXT_OVERLAY_FONT_SIZE_IN_PIXELS; for (i = 0; i < heights.length; i += 1) { stack = stack + heights[i]; } @@ -564,7 +565,7 @@ } function walletNotSetup() { - createNotification("Your wallet isn't set up. Open the WALLET app.", NotificationType.WALLET); + createNotification("Your wallet isn't activated yet. Open the WALLET app.", NotificationType.WALLET); } function connectionAdded(connectionName) { @@ -634,7 +635,7 @@ Window.notifyEditError = onEditError; Window.notify = onNotify; Tablet.tabletNotification.connect(tabletNotification); - Wallet.walletNotSetup.connect(walletNotSetup); + WalletScriptingInterface.walletNotSetup.connect(walletNotSetup); Messages.subscribe(NOTIFICATIONS_MESSAGE_CHANNEL); Messages.messageReceived.connect(onMessageReceived); diff --git a/scripts/system/pal.js b/scripts/system/pal.js index ebb45130e5..141ea03330 100644 --- a/scripts/system/pal.js +++ b/scripts/system/pal.js @@ -1,6 +1,9 @@ "use strict"; -/*jslint vars:true, plusplus:true, forin:true*/ -/*global Tablet, Settings, Script, AvatarList, Users, Entities, MyAvatar, Camera, Overlays, Vec3, Quat, HMD, Controller, Account, UserActivityLogger, Messages, Window, XMLHttpRequest, print, location, getControllerWorldLocation*/ +/* jslint vars:true, plusplus:true, forin:true */ +/* global Tablet, Settings, Script, AvatarList, Users, Entities, + MyAvatar, Camera, Overlays, Vec3, Quat, HMD, Controller, Account, + UserActivityLogger, Messages, Window, XMLHttpRequest, print, location, getControllerWorldLocation +*/ /* eslint indent: ["error", 4, { "outerIIFEBody": 0 }] */ // // pal.js @@ -20,7 +23,7 @@ var AppUi = Script.require('appUi'); var populateNearbyUserList, color, textures, removeOverlays, controllerComputePickRay, off, receiveMessage, avatarDisconnected, clearLocalQMLDataAndClosePAL, - CHANNEL, getConnectionData, findableByChanged, + CHANNEL, getConnectionData, avatarAdded, avatarRemoved, avatarSessionChanged; // forward references; // hardcoding these as it appears we cannot traverse the originalTextures in overlays??? Maybe I've missed @@ -318,8 +321,12 @@ function fromQml(message) { // messages are {method, params}, like json-rpc. See break; case 'http.request': break; // Handled by request-service. + case 'hideNotificationDot': + shouldShowDot = false; + ui.messagesWaiting(shouldShowDot); + break; default: - print('Unrecognized message from Pal.qml:', JSON.stringify(message)); + print('Unrecognized message from Pal.qml'); } } @@ -340,7 +347,7 @@ function requestJSON(url, callback) { // callback(data) if successfull. Logs oth uri: url }, function (error, response) { if (error || (response.status !== 'success')) { - print("Error: unable to get", url, error || response.status); + print("Error: unable to get request", error || response.status); return; } callback(response.data); @@ -361,8 +368,8 @@ function getProfilePicture(username, callback) { // callback(url) if successfull }); } var SAFETY_LIMIT = 400; -function getAvailableConnections(domain, callback) { // callback([{usename, location}...]) if successfull. (Logs otherwise) - var url = METAVERSE_BASE + '/api/v1/users?per_page=' + SAFETY_LIMIT + '&'; +function getAvailableConnections(domain, callback, numResultsPerPage) { // callback([{usename, location}...]) if successfull. (Logs otherwise) + var url = METAVERSE_BASE + '/api/v1/users?per_page=' + (numResultsPerPage || SAFETY_LIMIT) + '&'; if (domain) { url += 'status=' + domain.slice(1, -1); // without curly braces } else { @@ -713,7 +720,7 @@ function tabletVisibilityChanged() { if (!ui.tablet.tabletShown && ui.isOpen) { ui.close(); } - } +} var UPDATE_INTERVAL_MS = 100; var updateInterval; @@ -725,10 +732,14 @@ function createUpdateInterval() { var previousContextOverlay = ContextOverlay.enabled; var previousRequestsDomainListData = Users.requestsDomainListData; -function on() { +function palOpened() { + ui.sendMessage({ + method: 'changeConnectionsDotStatus', + shouldShowDot: shouldShowDot + }); previousContextOverlay = ContextOverlay.enabled; - previousRequestsDomainListData = Users.requestsDomainListData + previousRequestsDomainListData = Users.requestsDomainListData; ContextOverlay.enabled = false; Users.requestsDomainListData = true; @@ -807,14 +818,83 @@ function avatarSessionChanged(avatarID) { sendToQml({ method: 'palIsStale', params: [avatarID, 'avatarSessionChanged'] }); } +function notificationDataProcessPage(data) { + return data.data.users; +} + +var shouldShowDot = false; +var pingPong = false; +var storedOnlineUsers = {}; +function notificationPollCallback(connectionsArray) { + // + // START logic for handling online/offline user changes + // + pingPong = !pingPong; + var newOnlineUsers = 0; + var message; + + connectionsArray.forEach(function (user) { + var stored = storedOnlineUsers[user.username]; + var storedOrNew = stored || user; + storedOrNew.pingPong = pingPong; + if (stored) { + return; + } + + newOnlineUsers++; + storedOnlineUsers[user.username] = user; + + if (!ui.isOpen && ui.notificationInitialCallbackMade[0]) { + message = user.username + " is available in " + + user.location.root.name + ". Open PEOPLE to join them."; + ui.notificationDisplayBanner(message); + } + }); + var key; + for (key in storedOnlineUsers) { + if (storedOnlineUsers[key].pingPong !== pingPong) { + delete storedOnlineUsers[key]; + } + } + shouldShowDot = newOnlineUsers > 0 || (Object.keys(storedOnlineUsers).length > 0 && shouldShowDot); + // + // END logic for handling online/offline user changes + // + + if (!ui.isOpen) { + ui.messagesWaiting(shouldShowDot); + ui.sendMessage({ + method: 'changeConnectionsDotStatus', + shouldShowDot: shouldShowDot + }); + + if (newOnlineUsers > 0 && !ui.notificationInitialCallbackMade[0]) { + message = newOnlineUsers + " of your connections " + + (newOnlineUsers === 1 ? "is" : "are") + " available online. Open PEOPLE to join them."; + ui.notificationDisplayBanner(message); + } + } +} + +function isReturnedDataEmpty(data) { + var usersArray = data.data.users; + return usersArray.length === 0; +} + function startup() { ui = new AppUi({ buttonName: "PEOPLE", sortOrder: 7, home: "hifi/Pal.qml", - onOpened: on, + onOpened: palOpened, onClosed: off, - onMessage: fromQml + onMessage: fromQml, + notificationPollEndpoint: ["/api/v1/users?filter=connections&status=online&per_page=10"], + notificationPollTimeoutMs: [60000], + notificationDataProcessPage: [notificationDataProcessPage], + notificationPollCallback: [notificationPollCallback], + notificationPollStopPaginatingConditionMet: [isReturnedDataEmpty], + notificationPollCaresAboutSince: [false] }); Window.domainChanged.connect(clearLocalQMLDataAndClosePAL); Window.domainConnectionRefused.connect(clearLocalQMLDataAndClosePAL); diff --git a/scripts/system/particle_explorer/hifi-entity-ui.js b/scripts/system/particle_explorer/hifi-entity-ui.js deleted file mode 100644 index 62a0aadc86..0000000000 --- a/scripts/system/particle_explorer/hifi-entity-ui.js +++ /dev/null @@ -1,709 +0,0 @@ -/* global window, document, print, alert, console,setTimeout, clearTimeout, _ $ */ -/* eslint no-console: 0 */ - -/** -UI Builder V1.0 - -Created by Matti 'Menithal' Lahtinen -24/5/2017 -Copyright 2017 High Fidelity, Inc. - -This can eventually be expanded to all of Edit, for now, starting -with Particles Only. - -This is created for the sole purpose of streamliming the bridge, and to simplify -the logic between an inputfield in WebView and Entities in High Fidelity. - -We also do not need anything as heavy as jquery or any other platform, -as we are mostly only building for QT (while, all the other JS frameworks usually do alot of polyfilling) - -Available Types: - - JSONInputField - Accepts JSON input, once one presses Save, it will be propegated. - Button- A Button that listens for a custom event as defined by callback - Boolean - Creates a checkbox that the user can either check or uncheck - SliderFloat - Creates a slider (with input) that has Float values from min to max. - Default is min 0, max 1 - SliderInteger - Creates a slider (with input) that has a Integer value from min to max. - Default is min 1, max 10000 - SliderRadian - Creates a slider (with input) that has Float values in degrees, - that are converted to radians. default is min 0, max Math.PI. - Texture - Creates a Image with an url input field that points to texture. - If image cannot form, show "cannot find image" - VecQuaternion - Creates a 3D Vector field that converts to quaternions. - Checkbox exists to show quaternions instead. - Color - Create field color button, that when pressed, opens the color picker. - Vector - Create a 3D Vector field that has one to one correspondence. - -The script will use this structure to build a UI that is connected The -id fields within High Fidelity - -This should make editing, and everything related much more simpler to maintain, -and If there is any changes to either the Entities or properties of - -**/ - -var RADIANS_PER_DEGREE = Math.PI / 180; -var DEBOUNCE_TIMEOUT = 125; - -var roundFloat = function (input, round) { - round = round ? round : 1000; - var sanitizedInput; - if (typeof input === "string") { - sanitizedInput = parseFloat(input); - } else { - sanitizedInput = input; - } - return Math.round(sanitizedInput * round) / round; -}; - -function HifiEntityUI(parent) { - this.parent = parent; - - var self = this; - this.sendPackage = {}; - this.settingsUpdateLock = false; - this.webBridgeSync = function(id, val) { - if (!this.settingsUpdateLock) { - this.sendPackage[id] = val; - this.webBridgeSyncDebounce(); - } - }; - this.webBridgeSyncDebounce = _.debounce(function () { - if (self.EventBridge) { - self.submitChanges(self.sendPackage); - self.sendPackage = {}; - } - }, DEBOUNCE_TIMEOUT); -} - -HifiEntityUI.prototype = { - setOnSelect: function (callback) { - this.onSelect = callback; - }, - submitChanges: function (structure) { - var message = { - messageType: "settings_update", - updatedSettings: structure - }; - this.EventBridge.emitWebEvent(JSON.stringify(message)); - }, - setUI: function (structure) { - this.structure = structure; - }, - disableFields: function () { - var fields = document.getElementsByTagName("input"); - for (var i = 0; i < fields.length; i++) { - if (fields[i].getAttribute("type") !== "button") { - fields[i].value = ""; - } - - fields[i].setAttribute("disabled", true); - } - var textures = document.getElementsByTagName("img"); - for (i = 0; i < textures.length; i++) { - textures[i].src = ""; - } - - textures = document.getElementsByClassName("with-texture"); - for (i = 0; i < textures.length; i++) { - textures[i].classList.remove("with-textures"); - textures[i].classList.add("no-texture"); - } - - var textareas = document.getElementsByTagName("textarea"); - for (var x = 0; x < textareas.length; x++) { - textareas[x].remove(); - } - }, - getSettings: function () { - var self = this; - var json = {}; - var keys = Object.keys(self.builtRows); - for (var i = 0; i < keys.length; i++) { - var key = keys[i]; - var el = self.builtRows[key]; - if (el.className.indexOf("checkbox") !== -1) { - json[key] = document.getElementById(key) - .checked ? true : false; - } else if (el.className.indexOf("vector-section") !== -1) { - var vector = {}; - if (el.className.indexOf("rgb") !== -1) { - var red = document.getElementById(key + "-red"); - var blue = document.getElementById(key + "-blue"); - var green = document.getElementById(key + "-green"); - vector.red = red.value; - vector.blue = blue.value; - vector.green = green.value; - } else if (el.className.indexOf("pyr") !== -1) { - var p = document.getElementById(key + "-Pitch"); - var y = document.getElementById(key + "-Yaw"); - var r = document.getElementById(key + "-Roll"); - vector.x = p.value; - vector.y = y.value; - vector.z = r.value; - } else { - var x = document.getElementById(key + "-x"); - var ey = document.getElementById(key + "-y"); - var z = document.getElementById(key + "-z"); - vector.x = x.value; - vector.y = ey.value; - vector.z = z.value; - } - json[key] = vector; - } else if (el.className.indexOf("radian") !== -1) { - json[key] = document.getElementById(key).value * RADIANS_PER_DEGREE; - } else if (el.className.length > 0) { - json[key] = document.getElementById(key).value; - } - } - - - return json; - }, - fillFields: function (currentProperties) { - var self = this; - var fields = document.getElementsByTagName("input"); - - if (!currentProperties.locked) { - for (var i = 0; i < fields.length; i++) { - fields[i].removeAttribute("disabled"); - if (fields[i].hasAttribute("data-max")) { - // Reset Max to original max - fields[i].setAttribute("max", fields[i].getAttribute("data-max")); - } - } - } - - if (self.onSelect) { - self.onSelect(); - } - var keys = Object.keys(currentProperties); - - - for (var e in keys) { - if (keys.hasOwnProperty(e)) { - var value = keys[e]; - - var property = currentProperties[value]; - var field = self.builtRows[value]; - if (field) { - var el = document.getElementById(value); - - if (field.className.indexOf("radian") !== -1) { - el.value = property / RADIANS_PER_DEGREE; - el.onchange({ - target: el - }); - } else if (field.className.indexOf("range") !== -1 || field.className.indexOf("texture") !== -1) { - el.value = property; - el.onchange({ - target: el - }); - } else if (field.className.indexOf("checkbox") !== -1) { - if (property) { - el.setAttribute("checked", property); - } else { - el.removeAttribute("checked"); - } - } else if (field.className.indexOf("vector-section") !== -1) { - if (field.className.indexOf("rgb") !== -1) { - var red = document.getElementById(value + "-red"); - var blue = document.getElementById(value + "-blue"); - var green = document.getElementById(value + "-green"); - red.value = parseInt(property.red); - blue.value = parseInt(property.blue); - green.value = parseInt(property.green); - - red.oninput({ - target: red - }); - } else if (field.className.indexOf("xyz") !== -1) { - var x = document.getElementById(value + "-x"); - var y = document.getElementById(value + "-y"); - var z = document.getElementById(value + "-z"); - - x.value = roundFloat(property.x, 100); - y.value = roundFloat(property.y, 100); - z.value = roundFloat(property.z, 100); - } else if (field.className.indexOf("pyr") !== -1) { - var pitch = document.getElementById(value + "-Pitch"); - var yaw = document.getElementById(value + "-Yaw"); - var roll = document.getElementById(value + "-Roll"); - - pitch.value = roundFloat(property.x, 100); - yaw.value = roundFloat(property.y, 100); - roll.value = roundFloat(property.z, 100); - - } - } - } - } - } - }, - connect: function (EventBridge) { - this.EventBridge = EventBridge; - - var self = this; - - EventBridge.emitWebEvent(JSON.stringify({ - messageType: 'page_loaded' - })); - - EventBridge.scriptEventReceived.connect(function (data) { - data = JSON.parse(data); - - if (data.messageType === 'particle_settings') { - self.settingsUpdateLock = true; - self.fillFields(data.currentProperties); - self.settingsUpdateLock = false; - // Do expected property match with structure; - } else if (data.messageType === 'particle_close') { - self.disableFields(); - } - }); - }, - build: function () { - var self = this; - var sections = Object.keys(this.structure); - this.builtRows = {}; - sections.forEach(function (section, index) { - var properties = self.structure[section]; - self.addSection(self.parent, section, properties, index); - }); - }, - addSection: function (parent, section, properties, index) { - var self = this; - - var sectionDivHeader = document.createElement("fieldset"); - var title = document.createElement("legend"); - var dropDown = document.createElement("span"); - - dropDown.className = "arrow"; - sectionDivHeader.className = "major"; - title.className = "section-header"; - title.id = section + "-section"; - title.innerHTML = section; - title.appendChild(dropDown); - sectionDivHeader.appendChild(title); - - var collapsed = index !== 0; - - dropDown.innerHTML = collapsed ? "L" : "M"; - sectionDivHeader.setAttribute("collapsed", collapsed); - parent.appendChild(sectionDivHeader); - - var sectionDivBody = document.createElement("div"); - sectionDivBody.className = "property-group"; - - var animationWrapper = document.createElement("div"); - animationWrapper.className = "section-wrap"; - - for (var property in properties) { - if (properties.hasOwnProperty(property)) { - var builtRow = self.addElement(animationWrapper, properties[property]); - var id = properties[property].id; - if (id) { - self.builtRows[id] = builtRow; - } - } - } - sectionDivBody.appendChild(animationWrapper); - sectionDivHeader.appendChild(sectionDivBody); - _.defer(function () { - var height = (animationWrapper.clientHeight) + "px"; - if (collapsed) { - sectionDivBody.classList.remove("visible"); - sectionDivBody.style.maxHeight = "0px"; - } else { - sectionDivBody.classList.add("visible"); - sectionDivBody.style.maxHeight = height; - } - - title.onclick = function () { - collapsed = !collapsed; - if (collapsed) { - sectionDivBody.classList.remove("visible"); - sectionDivBody.style.maxHeight = "0px"; - } else { - sectionDivBody.classList.add("visible"); - sectionDivBody.style.maxHeight = (animationWrapper.clientHeight) + "px"; - } - // sectionDivBody.style.display = collapsed ? "none": "block"; - dropDown.innerHTML = collapsed ? "L" : "M"; - title.setAttribute("collapsed", collapsed); - }; - }); - }, - addLabel: function (parent, group) { - var label = document.createElement("label"); - label.innerHTML = group.name; - parent.appendChild(label); - if (group.unit) { - var span = document.createElement("span"); - span.innerHTML = group.unit; - span.className = "unit"; - label.appendChild(span); - } - return label; - }, - addVector: function (parent, group, labels, domArray) { - var self = this; - var inputs = labels ? labels : ["x", "y", "z"]; - domArray = domArray ? domArray : []; - parent.id = group.id; - for (var index in inputs) { - var element = document.createElement("input"); - - element.setAttribute("type", "number"); - element.className = inputs[index]; - element.id = group.id + "-" + inputs[index]; - - if (group.defaultRange) { - if (group.defaultRange.min) { - element.setAttribute("min", group.defaultRange.min); - } - if (group.defaultRange.max) { - element.setAttribute("max", group.defaultRange.max); - } - if (group.defaultRange.step) { - element.setAttribute("step", group.defaultRange.step); - } - } - if (group.oninput) { - element.oninput = group.oninput; - } else { - element.oninput = function (event) { - self.webBridgeSync(group.id, { - x: domArray[0].value, - y: domArray[1].value, - z: domArray[2].value - }); - }; - } - element.onchange = element.oninput; - domArray.push(element); - } - - this.addLabel(parent, group); - var className = ""; - for (var i = 0; i < inputs.length; i++) { - className += inputs[i].charAt(0) - .toLowerCase(); - } - parent.className += " property vector-section " + className; - - // Add Tuple and the rest - var tupleContainer = document.createElement("div"); - tupleContainer.className = "tuple"; - for (var domIndex in domArray) { - var container = domArray[domIndex]; - var div = document.createElement("div"); - var label = document.createElement("label"); - label.innerHTML = inputs[domIndex] + ":"; - label.setAttribute("for", container.id); - div.appendChild(container); - div.appendChild(label); - tupleContainer.appendChild(div); - } - parent.appendChild(tupleContainer); - }, - addVectorQuaternion: function (parent, group) { - this.addVector(parent, group, ["Pitch", "Yaw", "Roll"]); - }, - addColorPicker: function (parent, group) { - var self = this; - var $colPickContainer = $('
      ', { - id: group.id, - class: "color-picker" - }); - var updateColors = function (red, green, blue) { - $colPickContainer.css('background-color', "rgb(" + - red + "," + - green + "," + - blue + ")"); - }; - - var inputs = ["red", "green", "blue"]; - var domArray = []; - group.oninput = function (event) { - $colPickContainer.colpickSetColor( - { - r: domArray[0].value, - g: domArray[1].value, - b: domArray[2].value - }, - true); - }; - group.defaultRange = { - min: 0, - max: 255, - step: 1 - }; - - parent.appendChild($colPickContainer[0]); - self.addVector(parent, group, inputs, domArray); - - updateColors(domArray[0].value, domArray[1].value, domArray[2].value); - - // Could probably write a custom one for this to completely write out jquery, - // but for now, using the same as earlier. - - /* Color Picker Logic Here */ - - - $colPickContainer.colpick({ - colorScheme: (group.layoutColorScheme === undefined ? 'dark' : group.layoutColorScheme), - layout: (group.layoutType === undefined ? 'hex' : group.layoutType), - submit: (group.useSubmitButton === undefined ? true : group.useSubmitButton), - color: { - r: domArray[0].value, - g: domArray[1].value, - b: domArray[2].value - }, - onChange: function (hsb, hex, rgb, el) { - updateColors(rgb.r, rgb.g, rgb.b); - - domArray[0].value = rgb.r; - domArray[1].value = rgb.g; - domArray[2].value = rgb.b; - self.webBridgeSync(group.id, { - red: rgb.r, - green: rgb.g, - blue: rgb.b - }); - }, - onSubmit: function (hsb, hex, rgb, el) { - $(el) - .css('background-color', '#' + hex); - $(el) - .colpickHide(); - domArray[0].value = rgb.r; - domArray[1].value = rgb.g; - domArray[2].value = rgb.b; - self.webBridgeSync(group.id, { - red: rgb.r, - green: rgb.g, - blue: rgb.b - }); - } - }); - }, - addTextureField: function (parent, group) { - var self = this; - this.addLabel(parent, group); - parent.className += " property texture"; - var textureImage = document.createElement("div"); - var textureUrl = document.createElement("input"); - textureUrl.setAttribute("type", "text"); - textureUrl.id = group.id; - textureImage.className = "texture-image no-texture"; - var image = document.createElement("img"); - var imageLoad = _.debounce(function (url) { - if (url.slice(0, 5).toLowerCase() === "atp:/") { - image.src = ""; - image.style.display = "none"; - textureImage.classList.remove("with-texture"); - textureImage.classList.remove("no-texture"); - textureImage.classList.add("no-preview"); - } else if (url.length > 0) { - textureImage.classList.remove("no-texture"); - textureImage.classList.remove("no-preview"); - textureImage.classList.add("with-texture"); - image.src = url; - image.style.display = "block"; - } else { - image.src = ""; - image.style.display = "none"; - textureImage.classList.remove("with-texture"); - textureImage.classList.remove("no-preview"); - textureImage.classList.add("no-texture"); - } - }, DEBOUNCE_TIMEOUT * 2); - - textureUrl.oninput = function (event) { - // Add throttle - var url = event.target.value; - imageLoad(url); - self.webBridgeSync(group.id, url); - }; - textureUrl.onchange = textureUrl.oninput; - textureImage.appendChild(image); - parent.appendChild(textureImage); - parent.appendChild(textureUrl); - }, - addSlider: function (parent, group) { - var self = this; - this.addLabel(parent, group); - parent.className += " property range"; - var container = document.createElement("div"); - container.className = "slider-wrapper"; - var slider = document.createElement("input"); - slider.setAttribute("type", "range"); - - var inputField = document.createElement("input"); - inputField.setAttribute("type", "number"); - - container.appendChild(slider); - container.appendChild(inputField); - parent.appendChild(container); - - if (group.type === "SliderInteger") { - inputField.setAttribute("min", group.min !== undefined ? group.min : 0); - inputField.setAttribute("step", 1); - - slider.setAttribute("min", group.min !== undefined ? group.min : 0); - slider.setAttribute("max", group.max !== undefined ? group.max : 10000); - slider.setAttribute("data-max", group.max !== undefined ? group.max : 10000); - slider.setAttribute("step", 1); - - inputField.oninput = function (event) { - // TODO: Remove this functionality? Alan finds it confusing - if (parseInt(event.target.value) > parseInt(slider.getAttribute("max")) && group.max !== 1) { - slider.setAttribute("max", event.target.value); - } - slider.value = event.target.value; - self.webBridgeSync(group.id, slider.value); - }; - inputField.onchange = inputField.oninput; - slider.oninput = function (event) { - inputField.value = event.target.value; - self.webBridgeSync(group.id, inputField.value); - }; - - inputField.id = group.id; - } else if (group.type === "SliderRadian") { - slider.setAttribute("min", group.min !== undefined ? group.min : 0); - slider.setAttribute("max", group.max !== undefined ? group.max : 180); - slider.setAttribute("step", 1); - parent.className += " radian"; - inputField.setAttribute("min", (group.min !== undefined ? group.min : 0)); - inputField.setAttribute("max", (group.max !== undefined ? group.max : 180)); - - inputField.oninput = function (event) { - slider.value = event.target.value; - self.webBridgeSync(group.id, slider.value * RADIANS_PER_DEGREE); - }; - inputField.onchange = inputField.oninput; - - inputField.id = group.id; - slider.oninput = function (event) { - if (event.target.value > 0) { - inputField.value = Math.floor(event.target.value); - } else { - inputField.value = Math.ceil(event.target.value); - } - self.webBridgeSync(group.id, inputField.value * RADIANS_PER_DEGREE); - }; - var degrees = document.createElement("label"); - degrees.innerHTML = "°"; - degrees.style.fontSize = "1.4rem"; - degrees.style.display = "inline"; - degrees.style.verticalAlign = "top"; - degrees.style.paddingLeft = "0.4rem"; - container.appendChild(degrees); - - } else { - // Must then be Float - inputField.setAttribute("min", group.min !== undefined ? group.min : 0); - slider.setAttribute("step", 0.01); - - slider.setAttribute("min", group.min !== undefined ? group.min : 0); - slider.setAttribute("max", group.max !== undefined ? group.max : 1); - slider.setAttribute("data-max", group.max !== undefined ? group.max : 1); - slider.setAttribute("step", 0.01); - - inputField.oninput = function (event) { - // TODO: Remove this functionality? Alan finds it confusing - if (parseFloat(event.target.value) > parseFloat(slider.getAttribute("max")) && group.max !== 1) { - slider.setAttribute("max", event.target.value); - } - - slider.value = event.target.value; - self.webBridgeSync(group.id, slider.value); - // bind web sock update here. - }; - inputField.onchange = inputField.oninput; - slider.oninput = function (event) { - inputField.value = event.target.value; - self.webBridgeSync(group.id, inputField.value); - }; - - inputField.id = group.id; - } - - // UpdateBinding - }, - addCheckBox: function (parent, group) { - var checkBox = document.createElement("input"); - checkBox.setAttribute("type", "checkbox"); - var self = this; - checkBox.onchange = function (event) { - self.webBridgeSync(group.id, event.target.checked); - }; - checkBox.id = group.id; - parent.appendChild(checkBox); - var label = this.addLabel(parent, group); - label.setAttribute("for", checkBox.id); - parent.className += " property checkbox"; - }, - addElement: function (parent, group) { - var self = this; - var property = document.createElement("div"); - property.id = group.id; - - var row = document.createElement("div"); - switch (group.type) { - case "Button": - var button = document.createElement("input"); - button.setAttribute("type", "button"); - button.id = group.id; - if (group.disabled) { - button.disabled = group.disabled; - } - button.className = group.class; - button.value = group.name; - - button.onclick = group.callback; - parent.appendChild(button); - break; - case "Row": - var hr = document.createElement("hr"); - hr.className = "splitter"; - if (group.id) { - hr.id = group.id; - } - parent.appendChild(hr); - break; - case "Boolean": - self.addCheckBox(row, group); - parent.appendChild(row); - break; - case "SliderFloat": - case "SliderInteger": - case "SliderRadian": - self.addSlider(row, group); - parent.appendChild(row); - break; - case "Texture": - self.addTextureField(row, group); - parent.appendChild(row); - break; - case "Color": - self.addColorPicker(row, group); - parent.appendChild(row); - break; - case "Vector": - self.addVector(row, group); - parent.appendChild(row); - break; - case "VectorQuaternion": - self.addVectorQuaternion(row, group); - parent.appendChild(row); - break; - default: - console.log("not defined"); - } - return row; - } -}; \ No newline at end of file diff --git a/scripts/system/particle_explorer/particle-style.css b/scripts/system/particle_explorer/particle-style.css deleted file mode 100644 index cde325f6c6..0000000000 --- a/scripts/system/particle_explorer/particle-style.css +++ /dev/null @@ -1,140 +0,0 @@ -/* -// particle-style.css -// -// Created by Matti 'Menithal' Lahtinen on 21 May 2017 -// Copyright 2017 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -*/ - - -.property-group { - max-height: 0; - -webkit-transition: max-height 0.15s ease-out; - transition: max-height 0.15s ease-out; - overflow: hidden; -} -.property-group.visible { - transition: max-height 0.25s ease-in; -} -.section-wrap { - width: 100%; -} -.property { - padding: 0.4rem 0; - margin: 0; -} -.property.checkbox { - margin: 0; -} -.property.range label{ - display: block; -} - -input[type="button"] { - margin: 0.4rem; - min-width: 6rem; -} -input[type="text"] { - margin: 0; -} -.property.range input[type=number]{ - margin-left: 0.8rem; - width: 5.4rem; - height: 1.8rem; -} -input[type=range] { - -webkit-appearance: none; - background: #2e2e2e; - height: 1.8rem; - border-radius: 1rem; -} -input[type=range]::-webkit-slider-thumb { - -webkit-appearance:none; - width: 0.6rem; - height: 1.8rem; - padding:0; - margin: 0; - background-color: #696969; - border-radius: 1rem; -} -input[type=range]::-webkit-slider-thumb:hover { - background-color: white; -} -input[type=range]:focus { /*#252525*/ - outline: none; -} -.tuple label { - text-transform: capitalize; -} -.slider-wrapper { - display: table; - padding: 0.4rem 0; -} -hr.splitter{ - width: 100%; - padding: 0.2rem 0 0 0; - margin: 0; - position: relative; - clear: both; -} -hr.splitter:last-of-type{ - padding:0; -} -#rem { - height: 1rem; - width: 1rem; -} -.property { - min-height: 2rem; -} -.property.vector-section{ - - width: 24rem; -} - -.property.texture { - display: block; -} -.property.texture input{ - margin: 0.4rem 0; -} -.texture-image img{ - padding: 0; - margin: 0; - width: 100%; - height: 100%; - display: none; -} -.texture-image { - display: block; - position: relative; - background-repeat: no-repeat; - background-position: center; - background-size: 100% 100%; - margin-top: 0.4rem; - height:128px; - width: 128px; - background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAYAAADDPmHLAAAACXBIWXMAAAsTAAALEwEAmpwYAAAKT2lDQ1BQaG90b3Nob3AgSUNDIHByb2ZpbGUAAHjanVNnVFPpFj333vRCS4iAlEtvUhUIIFJCi4AUkSYqIQkQSoghodkVUcERRUUEG8igiAOOjoCMFVEsDIoK2AfkIaKOg6OIisr74Xuja9a89+bN/rXXPues852zzwfACAyWSDNRNYAMqUIeEeCDx8TG4eQuQIEKJHAAEAizZCFz/SMBAPh+PDwrIsAHvgABeNMLCADATZvAMByH/w/qQplcAYCEAcB0kThLCIAUAEB6jkKmAEBGAYCdmCZTAKAEAGDLY2LjAFAtAGAnf+bTAICd+Jl7AQBblCEVAaCRACATZYhEAGg7AKzPVopFAFgwABRmS8Q5ANgtADBJV2ZIALC3AMDOEAuyAAgMADBRiIUpAAR7AGDIIyN4AISZABRG8lc88SuuEOcqAAB4mbI8uSQ5RYFbCC1xB1dXLh4ozkkXKxQ2YQJhmkAuwnmZGTKBNA/g88wAAKCRFRHgg/P9eM4Ors7ONo62Dl8t6r8G/yJiYuP+5c+rcEAAAOF0ftH+LC+zGoA7BoBt/qIl7gRoXgugdfeLZrIPQLUAoOnaV/Nw+H48PEWhkLnZ2eXk5NhKxEJbYcpXff5nwl/AV/1s+X48/Pf14L7iJIEyXYFHBPjgwsz0TKUcz5IJhGLc5o9H/LcL//wd0yLESWK5WCoU41EScY5EmozzMqUiiUKSKcUl0v9k4t8s+wM+3zUAsGo+AXuRLahdYwP2SycQWHTA4vcAAPK7b8HUKAgDgGiD4c93/+8//UegJQCAZkmScQAAXkQkLlTKsz/HCAAARKCBKrBBG/TBGCzABhzBBdzBC/xgNoRCJMTCQhBCCmSAHHJgKayCQiiGzbAdKmAv1EAdNMBRaIaTcA4uwlW4Dj1wD/phCJ7BKLyBCQRByAgTYSHaiAFiilgjjggXmYX4IcFIBBKLJCDJiBRRIkuRNUgxUopUIFVIHfI9cgI5h1xGupE7yAAygvyGvEcxlIGyUT3UDLVDuag3GoRGogvQZHQxmo8WoJvQcrQaPYw2oefQq2gP2o8+Q8cwwOgYBzPEbDAuxsNCsTgsCZNjy7EirAyrxhqwVqwDu4n1Y8+xdwQSgUXACTYEd0IgYR5BSFhMWE7YSKggHCQ0EdoJNwkDhFHCJyKTqEu0JroR+cQYYjIxh1hILCPWEo8TLxB7iEPENyQSiUMyJ7mQAkmxpFTSEtJG0m5SI+ksqZs0SBojk8naZGuyBzmULCAryIXkneTD5DPkG+Qh8lsKnWJAcaT4U+IoUspqShnlEOU05QZlmDJBVaOaUt2ooVQRNY9aQq2htlKvUYeoEzR1mjnNgxZJS6WtopXTGmgXaPdpr+h0uhHdlR5Ol9BX0svpR+iX6AP0dwwNhhWDx4hnKBmbGAcYZxl3GK+YTKYZ04sZx1QwNzHrmOeZD5lvVVgqtip8FZHKCpVKlSaVGyovVKmqpqreqgtV81XLVI+pXlN9rkZVM1PjqQnUlqtVqp1Q61MbU2epO6iHqmeob1Q/pH5Z/YkGWcNMw09DpFGgsV/jvMYgC2MZs3gsIWsNq4Z1gTXEJrHN2Xx2KruY/R27iz2qqaE5QzNKM1ezUvOUZj8H45hx+Jx0TgnnKKeX836K3hTvKeIpG6Y0TLkxZVxrqpaXllirSKtRq0frvTau7aedpr1Fu1n7gQ5Bx0onXCdHZ4/OBZ3nU9lT3acKpxZNPTr1ri6qa6UbobtEd79up+6Ynr5egJ5Mb6feeb3n+hx9L/1U/W36p/VHDFgGswwkBtsMzhg8xTVxbzwdL8fb8VFDXcNAQ6VhlWGX4YSRudE8o9VGjUYPjGnGXOMk423GbcajJgYmISZLTepN7ppSTbmmKaY7TDtMx83MzaLN1pk1mz0x1zLnm+eb15vft2BaeFostqi2uGVJsuRaplnutrxuhVo5WaVYVVpds0atna0l1rutu6cRp7lOk06rntZnw7Dxtsm2qbcZsOXYBtuutm22fWFnYhdnt8Wuw+6TvZN9un2N/T0HDYfZDqsdWh1+c7RyFDpWOt6azpzuP33F9JbpL2dYzxDP2DPjthPLKcRpnVOb00dnF2e5c4PziIuJS4LLLpc+Lpsbxt3IveRKdPVxXeF60vWdm7Obwu2o26/uNu5p7ofcn8w0nymeWTNz0MPIQ+BR5dE/C5+VMGvfrH5PQ0+BZ7XnIy9jL5FXrdewt6V3qvdh7xc+9j5yn+M+4zw33jLeWV/MN8C3yLfLT8Nvnl+F30N/I/9k/3r/0QCngCUBZwOJgUGBWwL7+Hp8Ib+OPzrbZfay2e1BjKC5QRVBj4KtguXBrSFoyOyQrSH355jOkc5pDoVQfujW0Adh5mGLw34MJ4WHhVeGP45wiFga0TGXNXfR3ENz30T6RJZE3ptnMU85ry1KNSo+qi5qPNo3ujS6P8YuZlnM1VidWElsSxw5LiquNm5svt/87fOH4p3iC+N7F5gvyF1weaHOwvSFpxapLhIsOpZATIhOOJTwQRAqqBaMJfITdyWOCnnCHcJnIi/RNtGI2ENcKh5O8kgqTXqS7JG8NXkkxTOlLOW5hCepkLxMDUzdmzqeFpp2IG0yPTq9MYOSkZBxQqohTZO2Z+pn5mZ2y6xlhbL+xW6Lty8elQfJa7OQrAVZLQq2QqboVFoo1yoHsmdlV2a/zYnKOZarnivN7cyzytuQN5zvn//tEsIS4ZK2pYZLVy0dWOa9rGo5sjxxedsK4xUFK4ZWBqw8uIq2Km3VT6vtV5eufr0mek1rgV7ByoLBtQFr6wtVCuWFfevc1+1dT1gvWd+1YfqGnRs+FYmKrhTbF5cVf9go3HjlG4dvyr+Z3JS0qavEuWTPZtJm6ebeLZ5bDpaql+aXDm4N2dq0Dd9WtO319kXbL5fNKNu7g7ZDuaO/PLi8ZafJzs07P1SkVPRU+lQ27tLdtWHX+G7R7ht7vPY07NXbW7z3/T7JvttVAVVN1WbVZftJ+7P3P66Jqun4lvttXa1ObXHtxwPSA/0HIw6217nU1R3SPVRSj9Yr60cOxx++/p3vdy0NNg1VjZzG4iNwRHnk6fcJ3/ceDTradox7rOEH0x92HWcdL2pCmvKaRptTmvtbYlu6T8w+0dbq3nr8R9sfD5w0PFl5SvNUyWna6YLTk2fyz4ydlZ19fi753GDborZ752PO32oPb++6EHTh0kX/i+c7vDvOXPK4dPKy2+UTV7hXmq86X23qdOo8/pPTT8e7nLuarrlca7nuer21e2b36RueN87d9L158Rb/1tWeOT3dvfN6b/fF9/XfFt1+cif9zsu72Xcn7q28T7xf9EDtQdlD3YfVP1v+3Njv3H9qwHeg89HcR/cGhYPP/pH1jw9DBY+Zj8uGDYbrnjg+OTniP3L96fynQ89kzyaeF/6i/suuFxYvfvjV69fO0ZjRoZfyl5O/bXyl/erA6xmv28bCxh6+yXgzMV70VvvtwXfcdx3vo98PT+R8IH8o/2j5sfVT0Kf7kxmTk/8EA5jz/GMzLdsAAAAgY0hSTQAAeiUAAICDAAD5/wAAgOkAAHUwAADqYAAAOpgAABdvkl/FRgAABhNJREFUeNrsnVFy4joQRVsSCwAqBMwqsrRsIavMEkICoeAf2+8j1R5ZGDBgpLzoUDVVmTT2dc8It/paOpi3t7faOSciImVZyn6/l6qqRETEWivj8VistYPFd7ud1HUtIiLGGBmPx5JKX0RkMplIzvmPnHNijBERaS7Ef1lrB40bY1oXgH5a/ZH+8P7+LlVVycfHR/MGa60URdGcYOi4MUaKomhGaGx9EZHlcplMP2X+Ly8vPwOgLEtxzklVVVJVVfOznqAsy9YFXhuvqqq5AF/Lj+srtr7+LpV+yvz1mNF+vxcRkdVqJdZaeXp6ap1ws9m0TjibzVoj6lJ8vV6fjJdlKev1ujViU+j7t8tc8p9Op1KWpYw06L9JL0Av0r9l+jXl3nhd11JV1VE8tn5YM3PI3xjzoxVOGvyDU7zQj6s/0tGmI++amtNV087F9Wf/FnVPzRtCXz8RdV1nlb/efUbaJy4Wi0FqzjU1yRgjs9ls0Jp3jb6IyPPzczL9lPkvFot/dwCtB/om/x9oyJoXxps65NW8mPpdNTeX/JtBEtYE/+AUL/Tj6g/qA3TVnD41a6g++Bp9rYOp9FPnH80HOBcvy1I2m81D++BL+o/2AX5r/vgA+AD4AOif8AH8EdpVcy71sX3jWp/8W2AKff/TkUv+Oufr9AF0YuKc66xJ18T7eNP3nP9WfZ0EzufzJPqp8y+KQuq67vYBdETqCDpVU/rEw5oUnr+rD46h73/qUuinzh8fAP22D6AjxznXcqq6akrf+KmaFB6vf4+t7/sAelfIJf/GB9jtdmKMkdVq1dQM3zg4VVNU/NY+1Bgjh8Oh6YM1+dj6X19fzXwgp/wbH0DFtS7oyf0RdKqmhPFr+1RdseKfP7a+Px/IKX98APTbPoDOJrv60L417d54TH3V8lfS5pT/yfUA6/X6qOZcqkm3xrUm6X9CTH3fB0ihnzr/Ix9A/3T1qbfWpGvjMfX9T0UK/dT54wOg/88H8EfGPTVr6D740frhLDmn/Hv5AH1qku9t31KTzh3/aP1LPsBfzr+XDxCO0K6ack/N6qp5MfUv+QB/Of/ePsCQfWmfc6EfV3/kjzZrrRwOh9YtKHSm/LjOH3yrMTzej4c1y//51PHoP0a/tR7AOSdFURw9rz5VU049zw7jl2qWrosP++BY+iI/+wJS6afMv9kXoA6gvimsieHzZr/m6MTp3PPuc3G9SP95OPpx9JtOgT4cHwA+QCJ9+ADwAeADsC+AfQHo/4b1APAB4APAB4APAB8APgB9OD4AfAD4AFFqEnwA+AD4APgA6P86HwA+AHyAZhIBHwA+AHwA+AD04X/eB4APAB8APgB8APgA8AHow/P0AeADwAeADwAfAD4AfAD68Px8APgA8AHgA8AHgA8AH0DO70/v6lHvjaOfVn8U/iLcXx5OUML96X49vRTX3/nPw9FPo9+sB5hMJuKck+VyeVRTrLWtdfNdcf95eldNCuOfn5+tSYy/Pz+2voi0fICc8p/P5z93gJAPEN4+wufN4evaePj99eH+ePTj6p/1Abp60kt9Ksf/v46HDwAfAD6A/6gUPgD7AtgXwPP4DNcDwAeADwAfAD4AfAD4ADyPz289AHyA+Pqp84cPIPAB8AHwAfAB8AHgA7Q+HfAB4APAB4APAB+APjw3HwA+AHwA+ADwAeADwAegD8/TB4APAB8APgB8APgA8AHow/PzAeADwAeADwAfAD4AfACJ//316KfVH/mjLeb31+vx/kWhH0+/tR7AOSdFUUT9/nq9oK4+OJa+iLT25+eUf7MvIOQDxPr+en2F++PRj6PfdAr04fgA8AES6cMHgA8AH4B9AewLQP83rAeADwAfAD4AfAD4APAB6MPxAeADwAeIUpPgA8AHwAfAB0D/1/kA8AHgAzSTCPgA8AHgA8AHoA//8z4AfAD4APAB4APAB4APQB+epw8AHwA+AHwA+ADwAeAD0Ifn5wPAB4APAB8APgB8gBz5AOb19bX2TYLpdNpqQ7bbbctJGjJeVZVst9vWLSu2/vf3t+Sc/yicFIRr0C7Fu76f/lw8XBePflr9/wYAqWwWUSLcO54AAAAASUVORK5CYII='); -} - -.texture-image.no-texture { - background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAYAAADDPmHLAAAACXBIWXMAAAsTAAALEwEAmpwYAAAKT2lDQ1BQaG90b3Nob3AgSUNDIHByb2ZpbGUAAHjanVNnVFPpFj333vRCS4iAlEtvUhUIIFJCi4AUkSYqIQkQSoghodkVUcERRUUEG8igiAOOjoCMFVEsDIoK2AfkIaKOg6OIisr74Xuja9a89+bN/rXXPues852zzwfACAyWSDNRNYAMqUIeEeCDx8TG4eQuQIEKJHAAEAizZCFz/SMBAPh+PDwrIsAHvgABeNMLCADATZvAMByH/w/qQplcAYCEAcB0kThLCIAUAEB6jkKmAEBGAYCdmCZTAKAEAGDLY2LjAFAtAGAnf+bTAICd+Jl7AQBblCEVAaCRACATZYhEAGg7AKzPVopFAFgwABRmS8Q5ANgtADBJV2ZIALC3AMDOEAuyAAgMADBRiIUpAAR7AGDIIyN4AISZABRG8lc88SuuEOcqAAB4mbI8uSQ5RYFbCC1xB1dXLh4ozkkXKxQ2YQJhmkAuwnmZGTKBNA/g88wAAKCRFRHgg/P9eM4Ors7ONo62Dl8t6r8G/yJiYuP+5c+rcEAAAOF0ftH+LC+zGoA7BoBt/qIl7gRoXgugdfeLZrIPQLUAoOnaV/Nw+H48PEWhkLnZ2eXk5NhKxEJbYcpXff5nwl/AV/1s+X48/Pf14L7iJIEyXYFHBPjgwsz0TKUcz5IJhGLc5o9H/LcL//wd0yLESWK5WCoU41EScY5EmozzMqUiiUKSKcUl0v9k4t8s+wM+3zUAsGo+AXuRLahdYwP2SycQWHTA4vcAAPK7b8HUKAgDgGiD4c93/+8//UegJQCAZkmScQAAXkQkLlTKsz/HCAAARKCBKrBBG/TBGCzABhzBBdzBC/xgNoRCJMTCQhBCCmSAHHJgKayCQiiGzbAdKmAv1EAdNMBRaIaTcA4uwlW4Dj1wD/phCJ7BKLyBCQRByAgTYSHaiAFiilgjjggXmYX4IcFIBBKLJCDJiBRRIkuRNUgxUopUIFVIHfI9cgI5h1xGupE7yAAygvyGvEcxlIGyUT3UDLVDuag3GoRGogvQZHQxmo8WoJvQcrQaPYw2oefQq2gP2o8+Q8cwwOgYBzPEbDAuxsNCsTgsCZNjy7EirAyrxhqwVqwDu4n1Y8+xdwQSgUXACTYEd0IgYR5BSFhMWE7YSKggHCQ0EdoJNwkDhFHCJyKTqEu0JroR+cQYYjIxh1hILCPWEo8TLxB7iEPENyQSiUMyJ7mQAkmxpFTSEtJG0m5SI+ksqZs0SBojk8naZGuyBzmULCAryIXkneTD5DPkG+Qh8lsKnWJAcaT4U+IoUspqShnlEOU05QZlmDJBVaOaUt2ooVQRNY9aQq2htlKvUYeoEzR1mjnNgxZJS6WtopXTGmgXaPdpr+h0uhHdlR5Ol9BX0svpR+iX6AP0dwwNhhWDx4hnKBmbGAcYZxl3GK+YTKYZ04sZx1QwNzHrmOeZD5lvVVgqtip8FZHKCpVKlSaVGyovVKmqpqreqgtV81XLVI+pXlN9rkZVM1PjqQnUlqtVqp1Q61MbU2epO6iHqmeob1Q/pH5Z/YkGWcNMw09DpFGgsV/jvMYgC2MZs3gsIWsNq4Z1gTXEJrHN2Xx2KruY/R27iz2qqaE5QzNKM1ezUvOUZj8H45hx+Jx0TgnnKKeX836K3hTvKeIpG6Y0TLkxZVxrqpaXllirSKtRq0frvTau7aedpr1Fu1n7gQ5Bx0onXCdHZ4/OBZ3nU9lT3acKpxZNPTr1ri6qa6UbobtEd79up+6Ynr5egJ5Mb6feeb3n+hx9L/1U/W36p/VHDFgGswwkBtsMzhg8xTVxbzwdL8fb8VFDXcNAQ6VhlWGX4YSRudE8o9VGjUYPjGnGXOMk423GbcajJgYmISZLTepN7ppSTbmmKaY7TDtMx83MzaLN1pk1mz0x1zLnm+eb15vft2BaeFostqi2uGVJsuRaplnutrxuhVo5WaVYVVpds0atna0l1rutu6cRp7lOk06rntZnw7Dxtsm2qbcZsOXYBtuutm22fWFnYhdnt8Wuw+6TvZN9un2N/T0HDYfZDqsdWh1+c7RyFDpWOt6azpzuP33F9JbpL2dYzxDP2DPjthPLKcRpnVOb00dnF2e5c4PziIuJS4LLLpc+Lpsbxt3IveRKdPVxXeF60vWdm7Obwu2o26/uNu5p7ofcn8w0nymeWTNz0MPIQ+BR5dE/C5+VMGvfrH5PQ0+BZ7XnIy9jL5FXrdewt6V3qvdh7xc+9j5yn+M+4zw33jLeWV/MN8C3yLfLT8Nvnl+F30N/I/9k/3r/0QCngCUBZwOJgUGBWwL7+Hp8Ib+OPzrbZfay2e1BjKC5QRVBj4KtguXBrSFoyOyQrSH355jOkc5pDoVQfujW0Adh5mGLw34MJ4WHhVeGP45wiFga0TGXNXfR3ENz30T6RJZE3ptnMU85ry1KNSo+qi5qPNo3ujS6P8YuZlnM1VidWElsSxw5LiquNm5svt/87fOH4p3iC+N7F5gvyF1weaHOwvSFpxapLhIsOpZATIhOOJTwQRAqqBaMJfITdyWOCnnCHcJnIi/RNtGI2ENcKh5O8kgqTXqS7JG8NXkkxTOlLOW5hCepkLxMDUzdmzqeFpp2IG0yPTq9MYOSkZBxQqohTZO2Z+pn5mZ2y6xlhbL+xW6Lty8elQfJa7OQrAVZLQq2QqboVFoo1yoHsmdlV2a/zYnKOZarnivN7cyzytuQN5zvn//tEsIS4ZK2pYZLVy0dWOa9rGo5sjxxedsK4xUFK4ZWBqw8uIq2Km3VT6vtV5eufr0mek1rgV7ByoLBtQFr6wtVCuWFfevc1+1dT1gvWd+1YfqGnRs+FYmKrhTbF5cVf9go3HjlG4dvyr+Z3JS0qavEuWTPZtJm6ebeLZ5bDpaql+aXDm4N2dq0Dd9WtO319kXbL5fNKNu7g7ZDuaO/PLi8ZafJzs07P1SkVPRU+lQ27tLdtWHX+G7R7ht7vPY07NXbW7z3/T7JvttVAVVN1WbVZftJ+7P3P66Jqun4lvttXa1ObXHtxwPSA/0HIw6217nU1R3SPVRSj9Yr60cOxx++/p3vdy0NNg1VjZzG4iNwRHnk6fcJ3/ceDTradox7rOEH0x92HWcdL2pCmvKaRptTmvtbYlu6T8w+0dbq3nr8R9sfD5w0PFl5SvNUyWna6YLTk2fyz4ydlZ19fi753GDborZ752PO32oPb++6EHTh0kX/i+c7vDvOXPK4dPKy2+UTV7hXmq86X23qdOo8/pPTT8e7nLuarrlca7nuer21e2b36RueN87d9L158Rb/1tWeOT3dvfN6b/fF9/XfFt1+cif9zsu72Xcn7q28T7xf9EDtQdlD3YfVP1v+3Njv3H9qwHeg89HcR/cGhYPP/pH1jw9DBY+Zj8uGDYbrnjg+OTniP3L96fynQ89kzyaeF/6i/suuFxYvfvjV69fO0ZjRoZfyl5O/bXyl/erA6xmv28bCxh6+yXgzMV70VvvtwXfcdx3vo98PT+R8IH8o/2j5sfVT0Kf7kxmTk/8EA5jz/GMzLdsAAAAgY0hSTQAAeiUAAICDAAD5/wAAgOkAAHUwAADqYAAAOpgAABdvkl/FRgAAB81JREFUeNrsnTGPm0oXht97FWm2Ch2pTEeHpUihsyvTuXO67Ta/IPkr+Qfp3MWdO7Zad0SKZDo6XIWOrTzV9xVXZ8SygGHXG4/t96lW68GGw8vMmZlzDv98+/btfyBXy780wXXzTv74/fs3rXFFfPz4kT0AoQAoAJqAAiAUAKEACAVAKABCARAKgFAAhAIgFAChAAgFQCgAQgEQCoBQAIQCIBQAoQAIBUAoAHLmvDv3C7i7u4PjOMiyDOv1+mC75XKJoiga2wRBAN/34TgOHMdBWZYoigJpmiLPcwrARhzHAQD4vg/P81pvlLRrwvM8zGYz00ZrbY5xHAe+7yPPc9zf36MsSwrAVmazGX78+DHoGM/zsFgsAAB5nmOz2ZgeQimF8XiMMAxNu+VyaQRCH8Ai8jyH4zgIw7D3MUopzOdzAECaplitVk+GB601kiTBz58/obWG4ziIoohOoI38+vULABCGYWd3X2U6nUIphbIsEcdxa7uiKPDw8GCGGtd1KQDbKMsSWZZBKYXJZNLrGN/3zdN/iDRNTdcvx1EAFqGUwmazeeIQduG6LpRSAIAsy3r9hrRjD2BxL5AkiXEI+8wetNa9PXtp13eIoQBOQJIkxmHrcgjlJkov8JKpJwVgIVpr47CFYdh6g/f7/ZM5/9CehgKwmDRNURQFlFKYTqeNN/rx8dH0AH2faBn7KYAzQKZ1QRCYZd0qf/78MX+PRqNe3ymO5W63owBsR9bwZShoGirEq++zeBQEweBZAwVwYh4eHqC1RhAErQ6jOHVdK3yu65qhJE1TDgHn5BDKTW6auxdFYdYOgiDAYrF40k4phTAM8fnzZyilUBRF54rhOfIOF06SJMYPaPt8v99jOp3C8zx4nget9bPZQ5ZlF3fzL0IAZVke9OLv7+/Njl/brCHLMozHY4xGI3z48MH0EEVRIMuyi40H+EdqBbNS6HXBSqGEAiAUAAVAE1AAhAIgFAChAAgFQCgAQgGQq+Eom0GLxeJgGHYVSdCUhM02yrI0qV5hGGIymaAsy9b0LNd1cXt7CwDYbDa98wOA/zKLVquVSQGr/nYTbe2iKDIh53JtZVmiLEvsdjtst9tn5z7EDmfXA3QFXdaTMbvYbrdm568tgkdueJ7njbt3QwJA+8YJ1tsFQQDXdXFzc2N2E0Uwk8kEX758eXbMEDtY2QOsVqtn//v69SsAYL1eH9xK7dNGgjuiKMJ4PH4WmSN7+QBMFu/3798bn1oAzz47NvVrqmYgz2azRpv1scNV+wDVaN969y6JIEmSWBmyJenlIgZbcgvOzgmUqJxqkmY18ldCvGwkz/MntQcogBcgETrVMV98Aptvfh1JTKEAXsBms4HWGp7nYT6fw3Ec5Hlufbi253lQSkFr3VqmhgLoQVmW2G63ZigQx8/2my/FKCR17WLWAV7LfD5vzOFLkqS1W0/T1HT9RVFY5/jNZjMz3ouvorVGHMet9QheYoer7AGq478Y2LaiDTc3N3Bd90megSwG2YQVPcDQ+a/ccK01ttutWSWsetl/i7bfq16TzP1lGFgul0exw9X2AJLGJV3joRXCl3rnXbUDhmQKl2WJ9XoNrbV1vdXZCUCWWqvVQGR8HFIgqmuaKUiCSJcA+nrzWmvzdA/ZN6EAKlTz/eXmA3iSuXOoNEzfBRsA+PTpU+PnUjxSfnvo9/ZNR6cAakjFj2rqd3VtQJ6u1z5h1e+SdYbqdK5aWHLImC0OoFQgpRN4YPoD/LfRVC8C2TQlkhVC3/dfVDG0/l1xHCOKIvi+b572atJoURSdtYnbfAHxV0aj0TP/oY8dzqYH6OscHXK26tO+rqcujmNTIKqtJkDfc0vTFMvl8smu436/R57niOO4NSbh0HfLkFHtpYbY4dgwOfRKYXIooQAIBUAB0AQUAKEACAVAKABCARAKgFAA5Gp4s93AKIrw/v17ExsnFEWB/X6P3W6HLMtaN0+GJkwOad+W2FlPLq3GHFSRdq85h2PYyGoByG6cvJOnHiEryZJSg7e+s1ZNmOyzSza0ffWYJsIwbMzk7Tp+6Dm81kZWC0BoCnSU7dowDE2K12q1alT60EDJYwVWKqUQRdHgPf9jnfMQG52dDyA5fLKnLlGztiB5Bn1eP3fuNvr31IaWZM9jhHIdEwk5G1Jk4hxtdPJZQJZlJrLWlnBpx3FMmrnrup3RReduIyumgXJxtryRUxw4mQXIO4Yv0UZWCMDWN3I2vX7u0mxk1RtDmp6yoQmTbe27kjK7iOMYt7e3CIIA2+22VyLIWyZ5Hrsnsmol0Jac+fo51QtSXJKNrOgBuvLsTrUOUO8FxAP3ff/gTXiLc3irt5aevAdQSpmpja0vZqq+fm4ymfz18i5vaaOTC0DSvapv8rQRmRY6joPxeHwxNjqpAGSpUwx8ikKJQ5AyNFKb4BJsdBIfwPM8BEFgFjXSNG3debMJSUv7GyuWf8tGby6Aaq2c+qvaJce/a3p2ioTJQ73A3d3di6aBbef8WhtZKQDJ6K1fTJ7neHx8PFjWTcbbvvPePm8QbVtc6ft/+UwKUdfbDT3n19roGDA59EphciihAAgFQAHQBBQAoQAIBUAoAEIBEAqAUACEAiAUAKEACAVAKABCARAKgFAAhAIgFAChAAgFQC4CkxgiceKEPQC5Iv4/APgB2O7x8IXXAAAAAElFTkSuQmCC'); -} - -.texture-image.no-preview { - background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAIAAABMXPacAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAA8sSURBVHhe7Z3rbxXFG8d7B9SWthRabLmIYlHkIEXKJdXYBEXxHtEXprwxxsR3/jG+8PLCaDDGeAkmKsTEoCUVKoVCA6WNtLS2UEUKBSy0tKW/D+eZM9nu7tmz55z+mC2Zz4tl9tk5c2bnO/PMM2dnS+6nn36aYzFH7vvvv6+SFhMoAY4fPy7nljvG448/zjFPTiymsAIYxgpgGCuAYawAhrECGMYKYBgrgGGsAIaxAhjGCmAYK4BhrACGsQIYxgpgGCuAYawAhrECGMYKYBgrgGGsAIaxAhjmLhQgPz+/pKRk3rx56jzaRHFf0ObNmxctWkTi7Nmzp0+fFqNm+/btRUVFP/30kzp3UFtbu27duqVLl+bl3e5Y169f7+rqam1tvXnzpmSIFNHdF1RTU7M6TkNDQ0FBgbImWLVqFZfUSQKyvfzyy88991x1dfXU1NSFCxdGRkbuueeeurq6pqam0tJSlS96RNcFSQvSo9V5IC+88MIDDzwwOjr6448/fvTRR19++eVnn322Z8+ev//+u7i4+M0331ywYIHKGjGiK8Aff/zBMRaL5ebmiiUZjz322MqVK/Ez33333ZkzZxgBYh8eHt67d++lS5do/W3btokxakRXANxIf38/3mPNmjXKlARxpkeOHKGtxaIZHx9vaWkhwfTg9WZRILoCgIQG0r7JKC8vlxm7s7NTLC6YyW/cuFFYWIiPUqYoEWkB+vr6cOJLlizBwyiTB2l9vA0xj1hcTE9PDw4OkiA6EkukiLQAcOzYMY4bN26UUy8LFy7k+O+//8qpL1euXOF43333yWmkiLoATKqEQwSmlZWVyjQTIiWOwZG+npYjSNQFwIG0tbWRqK+vF4sL1r0qlZzJyUmOYXLeeaIuAHR3d+PfmQbE27hgguUY3LgS/0RzHMwBAei/R48ezcvL8x0EOCiOxEJy6osoJ1JFjTkgAHR0dExMTBDLexe0EvsTKQUMgsWLF3OUWChqzA0BGARoQBN7wyHWa6Ojo1x6+OGHlWkmaEOoeuvWrXPnzilTlJgbAgBeiEEQi8W8Pf3kyZMct27d6v0JGsf15JNPkmA5lmyhYJY5IwAenNmYBW1RUZEyJSBMYiYoLi7etWtXWVmZsubkkHPHjh2EsCjX3NysrBFjzggANDSeRJ04wEF9//33rLYqKip27979yiuvNDY2Pvvss2+//TZ+ieBn//79V69eVbkjRv6WLVv4hxW/nEcB+iyuo6ura3x8XJnicIqToV8zGpgSlDXO2NhYZ2cnV+WnIVZtTLxEn+fPn9+3b180p9+qqiqOd9ub8ihH67M8xuPT65mf1YXocXe+KY+PGhoa6unp4Rjl1tfcbQLMOawAhrECGMYKYBgrgGGsAIaxAhjGCmAYK4BhrACGyfy3oNdff72mpkadJLh27Vpvb29LS8vExIRYdu7c6dpLOz09ffPmTXLypadOnVLWnJzGxsZYLKZOPHR0dDQ3N7/33nv5+fkff/yx7/PFBQsWvPPOO5T/4YcfLly4sKmpaXBw8Ntvv5Wr7777bsAOUbINDw+Th5IpX1kTyGcPHz7c2tqqTHG4NW7wzz//9N2tHczs/BY0NjZ2PQFVLy4uXr9+/UsvvaQuJxgfH1eZ4tkKCwsrKiq2b9/u3XbozOkEzaamps6ePUueZHvcsOfl5ZFHtkH4oorzQOFU7MqVKzS0S6fy8nKxeDvckiVLOGbza2u22yW/+eYbOo46ie9Te/XVV5ctW7Z8+fK//vpLWXNyfvjhB2ctaaaGhoYNGzZs3bq1q6tLWeP88ssvdCh14oFLDz30EA3tuxFRhBGRkvHJJ5+olB8XLlxg6NCs/f39ypRo93/++Wfp0qWMP+fuCnna7N2TGp5ZngMQ48iRIyQefPBBsfhy69atgwcPjo6OlpSU+G42SQaicv80tPfBJBbslBwsQDBDQ0McpVk1CMBAx2HyFa79jUhFfeRTmTH7k7DsEky5DxBPffHiRRKytS0kNMTAwAAN4d0tigX7+fPnfaeHkEjlxbFoEIAvlTFRXV0tRhBnNTIy4hwT6TL7Asgz2zBvBUlO/K+chkQc1IoVK+RUI5YzZ87IaWZIX3buMpIJAP+Jroxv5zQgOmW52WL2BZDtyv/995+cJkMeHHJX6T42wcPgZ5gJ1HkCsWTjf4C+TCuXlpZqFyctLl6etpZpIH5F6eScAjNglgVg+n3iiSdIuHoiI/f2S19xamtrN23a9NprrzEVt7W1uSKWtWvXPu2HuhzfHkF/pFfef//9ypSTQxoLPi3lw3dV3Ez4UnU5/nicJpZuBAigvTzfyyU9DWQfAkG2UdCLL76oPeC99947f/58Et3d3cQMYhTk0b8TejGhfXt7uzpPgCfxuhf49ddfVSonp6enhyhr1apVeHyxkOYYxv8QJauUA9yaXpEQCKEH8zAJThGA1pd7lLamM0mCPNhl73vGZDsCGK10FgGffvnyZZYqP//8s7qcgCY7EUemMvz+F198ceDAAaZiyaA5duwYixov6nIcaWhpdEHSfIucBqCKm4m8hSDIBhHp3URoMgHEr9wefHoaYChw71qbjMlWgK+//pp1o/DBBx98/vnnLBfp3epyAmI4ujDs3bv3t99+I/J5/vnnfd++4/7pj17U5TjohzsuKysTL8yRNM5HwqpgVHEzce7KoYlpUynZO83qaYAOxzGbFYCQrQAsXOkXgrc7+4IYuA5WwgHvvaSEVuMoKy859vb23r6QNbQ+zof2Je2cAAQ9DYhCWU4AMPtRUBhko2B9fX1aiwAnEu3IakCOYfxPSFgN4HnwP7h7xHA6GT0NyFScZQgEZgRgimYyKCwsrKurU6Y0weHIbwO0FEfGX5bxuBPp8kR0jAPX22d8EY2Oa6qqqiJt3gVlzKFDhzjGYjFaUCzpgs/BGzQ2NnJkWg7pAMMg8Y/8Wul1Mn19fUiONtl3fzAmAP0XN8IgcM0EGzZs2JkElSOBTAMsLDiGnwBUWR74XpUjvuxiJS/TgK8AdBpUz34CAGMCgPy27hoEdC5Zr3lRORIQ8krYMzExMTAwIMaUqLI8iE/XyCCgj+NnxKLRoWf2/gcyfyBDGDNv3jw6csCP70C0QPvSUq6tzgKelK5EUxJZElazlFMX/PB6efkIJXsD0IKCgsrKSuclmpi1t6S9uBy6lJzMy1My5ae892DExdn/R8wYd+fu6DmHFcAwVgDDWAEMYwUwjBXAMFYAw1gBDGMFMIwVwDBp/xSxZs2aqqqqsbGxw4cPK1PiD2W0t7cne0K9ePHitWvXXr9+Xf4aKFRWVj7yyCMkKIfSxKgpLS1lpT4yMqIrxinGU6dOBf95OGH16tXV1dWuSmrkmbs6iTM5OXnjxo2enh7560Oap+O7MZz7AVzIF6kTPwI+m+FPEbT1+vXrN2/eXFJSokzxfXAYH330UXXuYd26dWRw/uoZi8WwgPPZukYKdO5vJI0FDdR5IL6V1KxYseL2FzvYuHFjQ0NDU1OTa7uRXFUnftTU1EieZKh8yUlPALott3T58mXSiC9GkJ/mA/aDyo1JNsjPz6fdr169OjU15SxnVqioqCgrK/NW0oXefrF///4DBw5QN2r1zDPPFBcXqxyhOXnypBTlReVITnoCyP20tLS4Gq6/v58hvGjRIudfi9HIrqnR0VG9jWfZsmXz58/nnoeGhiQt9llBVxIXFCCA3n7R3d3d0dFBY3EXRUVF4hjTAq8oRXlROZKTtgATExN9fX0DAwMyGsQ+PT0te3V8b1iMztqIpbe3l6JkNIh9VtCVpEGdlUyJPOjnI3J6Z0hDALkZbozuL63pbG6vReMSQFqcEcOACPhUZoj/kUrKPonwhcvTlTDbimeRNASQt1mkp9N5uUPn+y2Dg4M4Ge7f1eOQTR4taf+zcuVKfI6UI5sbli9f7pyfs0GaWwpnmLoqGYxswwr/dHNWSEMA7o37kfdecK+4b+luchUv5NudnS0iiEU/Rmfg5+XlBb/QEZ7gSjoh0CpPwOy1adMmQrVz58653tgJAz1MFTQT79+w8xJWACZSvobeoWN2r9MXAWSfmkb8u8v/UIjuaOk6igCkrYMrqXnqqad2JyAA3bZtG8N037593n2VKamvr1cFzaS2tlblSE5YAeQenLvPpJc57w0ng0thYaL3u0mLcGN6Bwf+p7CwkOmRfiqWixcv4rsIqLP3QmEqqRkeHqZWQK8njMH1U+233nor5FLDCcs3KcpFypckIOz2dLkHhiqrG7EAlZYmlqAb6Oksaoj65W+6iWOhG+pdU1IOGjjLQSGGF5nlD1BmTMhKCq2trXpcAkOT5RuV37Fjx1dffaWs4Whvb3f9DbvwhBoBdE8aiASr5y0O5B0j519MlVvSDt21/iooKBCPxFEVEYcGwhhmwAYgrUwiZSV9YUQeOnQI31VVVZXWe4NZEkoAqT3tyIrRibwQ6Ww4Qho6mvgTmoNG4ZZ0/EO70/cZ7+rzDojc+VTGe3VBur+3kvq/MInnCgINqD+JDLxQxqQWIDc3VzoyHYSB5uT333/HfUtDS2agCYhqWN8CpxKwyiVpI/XhmUhQJBkyQz7rrWRbWxvu3lXJZMhw0RW+A6QWQLoz9+DyoYI3hmFlzxHN+CAJp/+RAMk5SWqyjIXE/ySrJOsyjikLp+OzaiEKohxl+v+TWgCpt2+rgTfOu3TpEoENrQ/OcBP/w0RHyMGUKxYnrAbod84IyheCa/K4YH4KrqSvAK6i6urq3njjDcbu6dOnXTVUOWZCf1KX48opqweZOwNIEQVp/6PXTS7w77SyDHC9C5NeT0RBorOz0+V/5PcWL5OTk0hFkEq2EydOKKsHJlWVcoCjl8KTVVJUd1XStyjmp4MHD6qTBLt27VIpB3v27NEDZUMcSbugbrhBdeJHij9dTDyAvFQrWaMQXyLS+Pj4tWvX9PAn/kV5hgJhJXYxMgLIQDm+u3SBeZgOKJM2/YuhwJSoN+SWlJTQiJTphTZlzRlQSXBWkjUwsan6cBy+iLD9+PHjzc3Nzv22RLQqhwfEphBukx6mTH6wEEn2kOru/NPFc4gMn4hZZhcrgGGsAIaxAhjGCmAYK4BhrACGsQIYxgpgGCuAYawAhrECGMYKYBgrgGGsAIaxAhjGCmAYK4BhrACGsQIYxgpgGCuAYdS2FIsp7AgwSk7O/wCqCi/+JioQYgAAAABJRU5ErkJggg=='); -} - -#properties-list > fieldset { - margin-top: 0px; -} - -#main-header { - margin-bottom: 21px; -} - -.section-wrap { - padding: 21px 0px; -} \ No newline at end of file diff --git a/scripts/system/particle_explorer/particleExplorer.html b/scripts/system/particle_explorer/particleExplorer.html deleted file mode 100644 index ab4c249cc3..0000000000 --- a/scripts/system/particle_explorer/particleExplorer.html +++ /dev/null @@ -1,43 +0,0 @@ - - - - - - - - - - - - - - - - - - - -
      -
      - -
      - -
      -
      - - - diff --git a/scripts/system/particle_explorer/particleExplorer.js b/scripts/system/particle_explorer/particleExplorer.js deleted file mode 100644 index f1b7c8600f..0000000000 --- a/scripts/system/particle_explorer/particleExplorer.js +++ /dev/null @@ -1,485 +0,0 @@ -// -// particleExplorer.js -// -// Created by James B. Pollack @imgntn on 9/26/2015 -// Copyright 2017 High Fidelity, Inc. -// -// Reworked by Menithal on 20/5/2017 -// Reworked by Daniela Fontes and Artur Gomes (Mimicry) on 12/18/2017 -// -// Web app side of the App - contains GUI. -// This is an example of a new, easy way to do two way bindings between dynamically created GUI and in-world entities. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// -/* global HifiEntityUI, openEventBridge, console, EventBridge, document, window */ -/* eslint no-console: 0, no-global-assign: 0 */ - -(function () { - - var root = document.getElementById("properties-list"); - - window.onload = function () { - var ui = new HifiEntityUI(root); - var textarea = document.createElement("textarea"); - var properties = ""; - var menuStructure = { - General: [ - { - type: "Row", - id: "export-import-field" - }, - { - id: "show-properties-button", - name: "Show Properties", - type: "Button", - class: "blue", - disabled: true, - callback: function (event) { - var insertZone = document.getElementById("export-import-field"); - var json = ui.getSettings(); - properties = JSON.stringify(json); - textarea.value = properties; - if (!insertZone.contains(textarea)) { - insertZone.appendChild(textarea); - insertZone.parentNode.parentNode.style.maxHeight = - insertZone.parentNode.clientHeight + "px"; - document.getElementById("export-properties-button").removeAttribute("disabled"); - textarea.onchange = function (e) { - if (e.target.value !== properties) { - document.getElementById("import-properties-button").removeAttribute("disabled"); - } - }; - textarea.oninput = textarea.onchange; - document.getElementById("show-properties-button").value = "Hide Properties"; - } else { - textarea.onchange = function () {}; - textarea.oninput = textarea.onchange; - textarea.value = ""; - textarea.remove(); - insertZone.parentNode.parentNode.style.maxHeight = - insertZone.parentNode.clientHeight + "px"; - document.getElementById("export-properties-button").setAttribute("disabled", true); - document.getElementById("import-properties-button").setAttribute("disabled", true); - document.getElementById("show-properties-button").value = "Show Properties"; - } - } - }, - { - id: "import-properties-button", - name: "Import", - type: "Button", - class: "blue", - disabled: true, - callback: function (event) { - ui.fillFields(JSON.parse(textarea.value)); - ui.submitChanges(JSON.parse(textarea.value)); - } - }, - { - id: "export-properties-button", - name: "Export", - type: "Button", - class: "red", - disabled: true, - callback: function (event) { - textarea.select(); - try { - var success = document.execCommand('copy'); - if (!success) { - throw "Not success :("; - } - } catch (e) { - print("couldnt copy field"); - } - } - }, - { - type: "Row" - }, - { - id: "isEmitting", - name: "Is Emitting", - type: "Boolean" - }, - { - type: "Row" - }, - { - id: "lifespan", - name: "Lifespan", - type: "SliderFloat", - min: 0.01, - max: 10 - }, - { - type: "Row" - }, - { - id: "maxParticles", - name: "Max Particles", - type: "SliderInteger", - min: 1, - max: 10000 - }, - { - type: "Row" - }, - { - id: "textures", - name: "Textures", - type: "Texture" - }, - { - type: "Row" - } - ], - Emit: [ - { - id: "emitRate", - name: "Emit Rate", - type: "SliderInteger", - max: 1000, - min: 1 - }, - { - type: "Row" - }, - { - id: "emitSpeed", - name: "Emit Speed", - type: "SliderFloat", - max: 5 - }, - { - id: "speedSpread", - name: "Speed Spread", - type: "SliderFloat", - max: 5 - }, - { - type: "Row" - }, - { - id: "emitDimensions", - name: "Emit Dimension", - type: "Vector", - defaultRange: { - min: 0, - step: 0.01 - } - }, - { - type: "Row" - }, - { - id: "emitOrientation", - unit: "deg", - name: "Emit Orientation", - type: "VectorQuaternion", - defaultRange: { - min: 0, - step: 0.01 - } - }, - { - type: "Row" - }, - { - id: "emitterShouldTrail", - name: "Emitter Should Trail", - type: "Boolean" - }, - { - type: "Row" - } - ], - Radius: [ - { - id: "particleRadius", - name: "Particle Radius", - type: "SliderFloat", - max: 4.0 - }, - { - type: "Row" - }, - { - id: "radiusSpread", - name: "Radius Spread", - type: "SliderFloat", - max: 4.0 - }, - { - type: "Row" - }, - { - id: "radiusStart", - name: "Radius Start", - type: "SliderFloat", - max: 4.0 - }, - { - type: "Row" - }, - { - id: "radiusFinish", - name: "Radius Finish", - type: "SliderFloat", - max: 4.0 - }, - { - type: "Row" - } - ], - Color: [ - { - id: "color", - name: "Color", - type: "Color", - defaultColor: { - red: 255, - green: 255, - blue: 255 - }, - layoutType: "hex", - layoutColorScheme: "dark", - useSubmitButton: false - }, - { - type: "Row" - }, - { - id: "colorSpread", - name: "Color Spread", - type: "Color", - defaultColor: { - red: 0, - green: 0, - blue: 0 - }, - layoutType: "hex", - layoutColorScheme: "dark", - useSubmitButton: false - }, - { - type: "Row" - }, - { - id: "colorStart", - name: "Color Start", - type: "Color", - defaultColor: { - red: 255, - green: 255, - blue: 255 - }, - layoutType: "hex", - layoutColorScheme: "dark", - useSubmitButton: false - }, - { - type: "Row" - }, - { - id: "colorFinish", - name: "Color Finish", - type: "Color", - defaultColor: { - red: 255, - green: 255, - blue: 255 - }, - layoutType: "hex", - layoutColorScheme: "dark", - useSubmitButton: false - }, - { - type: "Row" - } - ], - Acceleration: [ - { - id: "emitAcceleration", - name: "Emit Acceleration", - type: "Vector", - defaultRange: { - step: 0.01 - } - }, - { - type: "Row" - }, - { - id: "accelerationSpread", - name: "Acceleration Spread", - type: "Vector", - defaultRange: { - step: 0.01 - } - }, - { - type: "Row" - } - ], - Alpha: [ - { - id: "alpha", - name: "Alpha", - type: "SliderFloat", - max: 1.0 - }, - { - type: "Row" - }, - { - id: "alphaSpread", - name: "Alpha Spread", - type: "SliderFloat", - max: 1.0 - }, - { - type: "Row" - }, - { - id: "alphaStart", - name: "Alpha Start", - type: "SliderFloat", - max: 1.0 - }, - { - type: "Row" - }, - { - id: "alphaFinish", - name: "Alpha Finish", - type: "SliderFloat", - max: 1.0 - }, - { - type: "Row" - } - ], - Spin: [ - { - id: "particleSpin", - name: "Particle Spin", - type: "SliderRadian", - min: -360.0, - max: 360.0 - }, - { - type: "Row" - }, - { - id: "spinSpread", - name: "Spin Spread", - type: "SliderRadian", - max: 360.0 - }, - { - type: "Row" - }, - { - id: "spinStart", - name: "Spin Start", - type: "SliderRadian", - min: -360.0, - max: 360.0 - }, - { - type: "Row" - }, - { - id: "spinFinish", - name: "Spin Finish", - type: "SliderRadian", - min: -360.0, - max: 360.0 - }, - { - type: "Row" - }, - { - id: "rotateWithEntity", - name: "Rotate with Entity", - type: "Boolean" - }, - { - type: "Row" - } - ], - Polar: [ - { - id: "polarStart", - name: "Polar Start", - unit: "deg", - type: "SliderRadian" - }, - { - type: "Row" - }, - { - id: "polarFinish", - name: "Polar Finish", - unit: "deg", - type: "SliderRadian" - }, - { - type: "Row" - } - ], - Azimuth: [ - { - id: "azimuthStart", - name: "Azimuth Start", - unit: "deg", - type: "SliderRadian", - min: -180, - max: 0 - }, - { - type: "Row" - }, - { - id: "azimuthFinish", - name: "Azimuth Finish", - unit: "deg", - type: "SliderRadian" - }, - { - type: "Row" - } - ] - }; - ui.setUI(menuStructure); - ui.setOnSelect(function () { - document.getElementById("show-properties-button").removeAttribute("disabled"); - document.getElementById("export-properties-button").setAttribute("disabled", true); - document.getElementById("import-properties-button").setAttribute("disabled", true); - }); - ui.build(); - var overrideLoad = false; - if (openEventBridge === undefined) { - overrideLoad = true, - openEventBridge = function (callback) { - callback({ - emitWebEvent: function () {}, - submitChanges: function () {}, - scriptEventReceived: { - connect: function () { - - } - } - }); - }; - } - openEventBridge(function (EventBridge) { - ui.connect(EventBridge); - }); - if (overrideLoad) { - openEventBridge(); - } - }; -})(); diff --git a/scripts/system/particle_explorer/particleExplorerTool.js b/scripts/system/particle_explorer/particleExplorerTool.js deleted file mode 100644 index a3be004329..0000000000 --- a/scripts/system/particle_explorer/particleExplorerTool.js +++ /dev/null @@ -1,144 +0,0 @@ -// -// particleExplorerTool.js -// -// Created by Eric Levin on 2/15/16 -// Copyright 2016 High Fidelity, Inc. -// Adds particleExplorer tool to the edit panel when a user selects a particle entity from the edit tool window -// This is an example of a new, easy way to do two way bindings between dynamically created GUI and in-world entities. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// -/* global ParticleExplorerTool */ - - -var PARTICLE_EXPLORER_HTML_URL = Script.resolvePath('particleExplorer.html'); - -ParticleExplorerTool = function(createToolsWindow) { - var that = {}; - that.activeParticleEntity = 0; - that.updatedActiveParticleProperties = {}; - - that.createWebView = function() { - that.webView = Tablet.getTablet("com.highfidelity.interface.tablet.system"); - that.webView.setVisible = function(value) {}; - that.webView.webEventReceived.connect(that.webEventReceived); - createToolsWindow.webEventReceived.addListener(this, that.webEventReceived); - }; - - function emitScriptEvent(data) { - var messageData = JSON.stringify(data); - that.webView.emitScriptEvent(messageData); - createToolsWindow.emitScriptEvent(messageData); - } - - that.destroyWebView = function() { - if (!that.webView) { - return; - } - that.activeParticleEntity = 0; - that.updatedActiveParticleProperties = {}; - - emitScriptEvent({ - messageType: "particle_close" - }); - }; - - function sendParticleProperties(properties) { - emitScriptEvent({ - messageType: "particle_settings", - currentProperties: properties - }); - } - - function sendActiveParticleProperties() { - var properties = Entities.getEntityProperties(that.activeParticleEntity); - if (properties.emitOrientation) { - properties.emitOrientation = Quat.safeEulerAngles(properties.emitOrientation); - } - // Update uninitialized variables - if (isNaN(properties.alphaStart)) { - properties.alphaStart = properties.alpha; - } - if (isNaN(properties.alphaFinish)) { - properties.alphaFinish = properties.alpha; - } - if (isNaN(properties.radiusStart)) { - properties.radiusStart = properties.particleRadius; - } - if (isNaN(properties.radiusFinish)) { - properties.radiusFinish = properties.particleRadius; - } - if (isNaN(properties.colorStart.red)) { - properties.colorStart = properties.color; - } - if (isNaN(properties.colorFinish.red)) { - properties.colorFinish = properties.color; - } - if (isNaN(properties.spinStart)) { - properties.spinStart = properties.particleSpin; - } - if (isNaN(properties.spinFinish)) { - properties.spinFinish = properties.particleSpin; - } - sendParticleProperties(properties); - } - - function sendUpdatedActiveParticleProperties() { - sendParticleProperties(that.updatedActiveParticleProperties); - that.updatedActiveParticleProperties = {}; - } - - that.webEventReceived = function(message) { - var data = JSON.parse(message); - if (data.messageType === "settings_update") { - var updatedSettings = data.updatedSettings; - - var optionalProps = ["alphaStart", "alphaFinish", "radiusStart", "radiusFinish", "colorStart", "colorFinish", "spinStart", "spinFinish"]; - var fallbackProps = ["alpha", "particleRadius", "color", "particleSpin"]; - for (var i = 0; i < optionalProps.length; i++) { - var fallbackProp = fallbackProps[Math.floor(i / 2)]; - var optionalValue = updatedSettings[optionalProps[i]]; - var fallbackValue = updatedSettings[fallbackProp]; - if (optionalValue && fallbackValue) { - delete updatedSettings[optionalProps[i]]; - } - } - - if (updatedSettings.emitOrientation) { - updatedSettings.emitOrientation = Quat.fromVec3Degrees(updatedSettings.emitOrientation); - } - - Entities.editEntity(that.activeParticleEntity, updatedSettings); - - var entityProps = Entities.getEntityProperties(that.activeParticleEntity, optionalProps); - - var needsUpdate = false; - for (var i = 0; i < optionalProps.length; i++) { - var fallbackProp = fallbackProps[Math.floor(i / 2)]; - var fallbackValue = updatedSettings[fallbackProp]; - if (fallbackValue) { - var optionalProp = optionalProps[i]; - if ((fallbackProp !== "color" && isNaN(entityProps[optionalProp])) || (fallbackProp === "color" && isNaN(entityProps[optionalProp].red))) { - that.updatedActiveParticleProperties[optionalProp] = fallbackValue; - needsUpdate = true; - } - } - } - - if (needsUpdate) { - sendUpdatedActiveParticleProperties(); - } - - } else if (data.messageType === "page_loaded") { - sendActiveParticleProperties(); - } - }; - - that.setActiveParticleEntity = function(id) { - that.activeParticleEntity = id; - sendActiveParticleProperties(); - }; - - return that; -}; diff --git a/scripts/system/progress.js b/scripts/system/progress.js index db09d40608..b373612790 100644 --- a/scripts/system/progress.js +++ b/scripts/system/progress.js @@ -14,11 +14,11 @@ // (function () { // BEGIN LOCAL_SCOPE - function debug() { //print.apply(null, arguments); } + Script.include("/~/system/libraries/globals.js"); var rawProgress = 100, // % raw value. displayProgress = 100, // % smoothed value to display. alpha = 0.0, @@ -83,7 +83,9 @@ // The initial delay cooldown keeps us from tracking progress before the allotted time // has passed. INITIAL_DELAY_COOLDOWN_TIME = 1000, - initialDelayCooldown = 0; + initialDelayCooldown = 0, + + isInInterstitialMode = false; function fade() { @@ -265,7 +267,7 @@ // Update state if (!visible) { // Not visible because no recent downloads - if (displayProgress < 100 || gpuTextures > 0) { // Have started downloading so fade in + if ((displayProgress < 100 || gpuTextures > 0) && !isInInterstitialMode && !isInterstitialOverlaysVisible) { // Have started downloading so fade in visible = true; alphaDelta = ALPHA_DELTA_IN; fadeTimer = Script.setInterval(fade, FADE_INTERVAL); @@ -305,10 +307,13 @@ } else { x = x * BAR_HMD_REPEAT; } + if (isInInterstitialMode || isInterstitialOverlaysVisible) { + visible = false; + } // Update progress bar Overlays.editOverlay(barDesktop.overlay, { - visible: !isHMD, + visible: !isHMD && visible, bounds: { x: barDesktop.repeat - x, y: windowHeight - barDesktop.height, @@ -318,7 +323,7 @@ }); Overlays.editOverlay(barHMD.overlay, { - visible: isHMD, + visible: isHMD && visible, bounds: { x: BAR_HMD_REPEAT - x, y: windowHeight - BAR_HMD_HEIGHT, @@ -328,11 +333,11 @@ }); Overlays.editOverlay(textDesktop.overlay, { - visible: !isHMD + visible: !isHMD && visible }); Overlays.editOverlay(textHMD.overlay, { - visible: isHMD + visible: isHMD && visible }); // Update 2D overlays to maintain positions at bottom middle of window @@ -344,6 +349,10 @@ } } + function interstitialModeChanged(inMode) { + isInInterstitialMode = inMode; + } + function setUp() { var is4k = Window.innerWidth > 3000; @@ -369,6 +378,7 @@ } setUp(); + Window.interstitialModeChanged.connect(interstitialModeChanged); GlobalServices.downloadInfoChanged.connect(onDownloadInfoChanged); GlobalServices.updateDownloadInfo(); Script.setInterval(update, 1000 / 60); diff --git a/scripts/system/redirectOverlays.js b/scripts/system/redirectOverlays.js new file mode 100644 index 0000000000..bb537bee0e --- /dev/null +++ b/scripts/system/redirectOverlays.js @@ -0,0 +1,263 @@ +"use strict"; +(function() { + + var ERROR_MESSAGE_MAP = [ + "Oops! Protocol version mismatch.", + "Oops! Not authorized to join domain.", + "Oops! Connection timed out.", + "Oops! The domain is full.", + "Oops! Something went wrong." + ]; + + var PROTOCOL_VERSION_MISMATCH = 1; + var NOT_AUTHORIZED = 3; + var DOMAIN_FULL = 4; + var TIMEOUT = 5; + var hardRefusalErrors = [ + PROTOCOL_VERSION_MISMATCH, + NOT_AUTHORIZED, + TIMEOUT, + DOMAIN_FULL + ]; + var timer = null; + var isErrorState = false; + + function getOopsText() { + var error = Window.getLastDomainConnectionError(); + var errorMessageMapIndex = hardRefusalErrors.indexOf(error); + if (error === -1) { + // not an error. + return ""; + } else if (errorMessageMapIndex >= 0) { + return ERROR_MESSAGE_MAP[errorMessageMapIndex]; + } else { + // some other text. + return ERROR_MESSAGE_MAP[ERROR_MESSAGE_MAP.length - 1]; + } + }; + + var oopsDimensions = {x: 4.2, y: 0.8}; + + var redirectOopsText = Overlays.addOverlay("text3d", { + name: "oopsText", + position: {x: 0, y: 1.6763916015625, z: 1.45927095413208}, + rotation: {x: -4.57763671875e-05, y: 0.4957197904586792, z: -7.62939453125e-05, w: 0.8684672117233276}, + text: getOopsText(), + textAlpha: 1, + backgroundColor: {x: 0, y: 0, z:0}, + backgroundAlpha: 0, + lineHeight: 0.10, + leftMargin: 0.538373570564886, + visible: false, + emissive: true, + ignoreRayIntersection: true, + dimensions: oopsDimensions, + grabbable: false, + }); + + var tryAgainImageNeutral = Overlays.addOverlay("image3d", { + name: "tryAgainImage", + localPosition: {x: -0.6, y: -0.6, z: 0.0}, + url: Script.resourcesPath() + "images/interstitialPage/button.png", + alpha: 1, + visible: false, + emissive: true, + ignoreRayIntersection: false, + grabbable: false, + orientation: Overlays.getProperty(redirectOopsText, "orientation"), + parentID: redirectOopsText + }); + + var tryAgainImageHover = Overlays.addOverlay("image3d", { + name: "tryAgainImageHover", + localPosition: {x: -0.6, y: -0.6, z: 0.0}, + url: Script.resourcesPath() + "images/interstitialPage/button_hover.png", + alpha: 1, + visible: false, + emissive: true, + ignoreRayIntersection: false, + grabbable: false, + orientation: Overlays.getProperty(redirectOopsText, "orientation"), + parentID: redirectOopsText + }); + + var tryAgainText = Overlays.addOverlay("text3d", { + name: "tryAgainText", + localPosition: {x: -0.6, y: -0.962, z: 0.0}, + text: "Try Again", + textAlpha: 1, + backgroundAlpha: 0.00393, + lineHeight: 0.08, + visible: false, + emissive: true, + ignoreRayIntersection: true, + grabbable: false, + orientation: Overlays.getProperty(redirectOopsText, "orientation"), + parentID: redirectOopsText + }); + + var backImageNeutral = Overlays.addOverlay("image3d", { + name: "backImage", + localPosition: {x: 0.6, y: -0.6, z: 0.0}, + url: Script.resourcesPath() + "images/interstitialPage/button.png", + alpha: 1, + visible: false, + emissive: true, + ignoreRayIntersection: false, + grabbable: false, + orientation: Overlays.getProperty(redirectOopsText, "orientation"), + parentID: redirectOopsText + }); + + var backImageHover = Overlays.addOverlay("image3d", { + name: "backImageHover", + localPosition: {x: 0.6, y: -0.6, z: 0.0}, + url: Script.resourcesPath() + "images/interstitialPage/button_hover.png", + alpha: 1, + visible: false, + emissive: true, + ignoreRayIntersection: false, + grabbable: false, + orientation: Overlays.getProperty(redirectOopsText, "orientation"), + parentID: redirectOopsText + }); + + var backText = Overlays.addOverlay("text3d", { + name: "backText", + localPosition: {x: 0.6, y: -0.962, z: 0.0}, + text: "Back", + textAlpha: 1, + backgroundAlpha: 0.00393, + lineHeight: 0.08, + visible: false, + emissive: true, + ignoreRayIntersection: true, + grabbable: false, + orientation: Overlays.getProperty(redirectOopsText, "orientation"), + parentID: redirectOopsText + }); + + function toggleOverlays(isInErrorState) { + isErrorState = isInErrorState; + if (!isInErrorState) { + var properties = { + visible: false + }; + + Overlays.editOverlay(redirectOopsText, properties); + Overlays.editOverlay(tryAgainImageNeutral, properties); + Overlays.editOverlay(tryAgainImageHover, properties); + Overlays.editOverlay(backImageNeutral, properties); + Overlays.editOverlay(backImageHover, properties); + Overlays.editOverlay(tryAgainText, properties); + Overlays.editOverlay(backText, properties); + return; + } + var oopsText = getOopsText(); + // if oopsText === "", it was a success. + var overlaysVisible = (oopsText !== ""); + // for catching init or if error state were to be different. + isErrorState = overlaysVisible; + var properties = { + visible: overlaysVisible + }; + + var textWidth = Overlays.textSize(redirectOopsText, oopsText).width; + var textOverlayWidth = oopsDimensions.x; + + var oopsTextProperties = { + visible: overlaysVisible, + text: oopsText, + textAlpha: overlaysVisible, + // either visible or invisible. 0 doesn't work in Mac. + backgroundAlpha: overlaysVisible * 0.00393, + leftMargin: (textOverlayWidth - textWidth) / 2 + }; + + var tryAgainTextWidth = Overlays.textSize(tryAgainText, "Try Again").width; + var tryAgainImageWidth = Overlays.getProperty(tryAgainImageNeutral, "dimensions").x; + + var tryAgainTextProperties = { + visible: overlaysVisible, + leftMargin: (tryAgainImageWidth - tryAgainTextWidth) / 2 + }; + + var backTextWidth = Overlays.textSize(backText, "Back").width; + var backImageWidth = Overlays.getProperty(backImageNeutral, "dimensions").x; + + var backTextProperties = { + visible: overlaysVisible, + leftMargin: (backImageWidth - backTextWidth) / 2 + }; + + Overlays.editOverlay(redirectOopsText, oopsTextProperties); + Overlays.editOverlay(tryAgainImageNeutral, properties); + Overlays.editOverlay(backImageNeutral, properties); + Overlays.editOverlay(tryAgainImageHover, {visible: false}); + Overlays.editOverlay(backImageHover, {visible: false}); + Overlays.editOverlay(tryAgainText, tryAgainTextProperties); + Overlays.editOverlay(backText, backTextProperties); + + } + + function clickedOnOverlay(overlayID, event) { + if (event.isRightButton) { + // don't allow right-clicks. + return; + } + if (tryAgainImageHover === overlayID) { + location.goToLastAddress(); + } else if (backImageHover === overlayID && location.canGoBack()) { + location.goBack(); + } + } + + function cleanup() { + Script.clearInterval(timer); + timer = null; + Overlays.deleteOverlay(redirectOopsText); + Overlays.deleteOverlay(tryAgainImageNeutral); + Overlays.deleteOverlay(backImageNeutral); + Overlays.deleteOverlay(tryAgainImageHover); + Overlays.deleteOverlay(backImageHover); + Overlays.deleteOverlay(tryAgainText); + Overlays.deleteOverlay(backText); + } + + toggleOverlays(true); + + Overlays.mouseReleaseOnOverlay.connect(clickedOnOverlay); + Overlays.hoverEnterOverlay.connect(function(overlayID, event) { + if (!isErrorState) { + // don't allow hover overlay events to get caught if it's not in error state anymore. + return; + } + if (overlayID === backImageNeutral && location.canGoBack()) { + Overlays.editOverlay(backImageNeutral, {visible: false}); + Overlays.editOverlay(backImageHover, {visible: true}); + } + if (overlayID === tryAgainImageNeutral) { + Overlays.editOverlay(tryAgainImageNeutral, {visible: false}); + Overlays.editOverlay(tryAgainImageHover, {visible: true}); + } + }); + + Overlays.hoverLeaveOverlay.connect(function(overlayID, event) { + if (!isErrorState) { + // don't allow hover overlay events to get caught if it's not in error state anymore. + return; + } + if (overlayID === backImageHover) { + Overlays.editOverlay(backImageHover, {visible: false}); + Overlays.editOverlay(backImageNeutral, {visible: true}); + } + if (overlayID === tryAgainImageHover) { + Overlays.editOverlay(tryAgainImageHover, {visible: false}); + Overlays.editOverlay(tryAgainImageNeutral, {visible: true}); + } + }); + + Window.redirectErrorStateChanged.connect(toggleOverlays); + + Script.scriptEnding.connect(cleanup); +}()); diff --git a/scripts/system/snapshot.js b/scripts/system/snapshot.js index 3e58bc61ad..946de6df0f 100644 --- a/scripts/system/snapshot.js +++ b/scripts/system/snapshot.js @@ -7,28 +7,19 @@ // Distributed under the Apache License, Version 2.0 // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -/* globals Tablet, Script, HMD, Settings, DialogsManager, Menu, Reticle, OverlayWebWindow, Desktop, Account, MyAvatar, Snapshot */ +/* globals Tablet, Script, HMD, Settings, DialogsManager, Menu, Reticle, + OverlayWebWindow, Desktop, Account, MyAvatar, Snapshot */ /* eslint indent: ["error", 4, { "outerIIFEBody": 0 }] */ (function () { // BEGIN LOCAL_SCOPE Script.include("/~/system/libraries/accountUtils.js"); +var AppUi = Script.require('appUi'); var SNAPSHOT_DELAY = 500; // 500ms var FINISH_SOUND_DELAY = 350; var resetOverlays; var reticleVisible; -var buttonName = "SNAP"; -var buttonConnected = false; - -var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); -var button = tablet.addButton({ - icon: "icons/tablet-icons/snap-i.svg", - activeIcon: "icons/tablet-icons/snap-a.svg", - text: buttonName, - sortOrder: 5 -}); - var snapshotOptions = {}; var imageData = []; var storyIDsToMaybeDelete = []; @@ -52,10 +43,11 @@ try { print('Failed to resolve request api, error: ' + err); } - - function removeFromStoryIDsToMaybeDelete(story_id) { - storyIDsToMaybeDelete.splice(storyIDsToMaybeDelete.indexOf(story_id), 1); + story_id = parseInt(story_id); + if (storyIDsToMaybeDelete.indexOf(story_id) > -1) { + storyIDsToMaybeDelete.splice(storyIDsToMaybeDelete.indexOf(story_id), 1); + } print('storyIDsToMaybeDelete[] now:', JSON.stringify(storyIDsToMaybeDelete)); } @@ -73,33 +65,32 @@ function onMessage(message) { // 2. Although we currently use a single image, we would like to take snapshot, a selfie, a 360 etc. all at the // same time, show the user all of them, and have the user deselect any that they do not want to share. // So we'll ultimately be receiving a set of objects, perhaps with different post processing for each. - message = JSON.parse(message); if (message.type !== "snapshot") { return; } switch (message.action) { case 'ready': // DOM is ready and page has loaded - tablet.emitScriptEvent(JSON.stringify({ + ui.sendMessage({ type: "snapshot", action: "captureSettings", setting: Settings.getValue("alsoTakeAnimatedSnapshot", true) - })); + }); if (Snapshot.getSnapshotsLocation() !== "") { isDomainOpen(Settings.getValue("previousSnapshotDomainID"), function (canShare) { - tablet.emitScriptEvent(JSON.stringify({ + ui.sendMessage({ type: "snapshot", action: "showPreviousImages", options: snapshotOptions, image_data: imageData, canShare: canShare - })); + }); }); } else { - tablet.emitScriptEvent(JSON.stringify({ + ui.sendMessage({ type: "snapshot", action: "showSetupInstructions" - })); + }); Settings.setValue("previousStillSnapPath", ""); Settings.setValue("previousStillSnapStoryID", ""); Settings.setValue("previousStillSnapBlastingDisabled", false); @@ -124,7 +115,7 @@ function onMessage(message) { || (!HMD.active && Settings.getValue("desktopTabletBecomesToolbar", true))) { Desktop.show("hifi/dialogs/GeneralPreferencesDialog.qml", "GeneralPreferencesDialog"); } else { - tablet.loadQMLOnTop("hifi/tablet/TabletGeneralPreferences.qml"); + ui.openNewAppOnTop("hifi/tablet/TabletGeneralPreferences.qml"); } break; case 'captureStillAndGif': @@ -284,7 +275,6 @@ var POLAROID_RATE_LIMIT_MS = 1000; var polaroidPrintingIsRateLimited = false; function printToPolaroid(image_url) { - // Rate-limit printing if (polaroidPrintingIsRateLimited) { return; @@ -325,9 +315,7 @@ function printToPolaroid(image_url) { "dynamic": true, "collisionsWillMove": true, - "userData": { - "grabbableKey": { "grabbable" : true } - } + "grab": { "grabbable": true } }; var polaroid = Entities.addEntity(properties); @@ -348,11 +336,13 @@ function fillImageDataFromPrevious() { var previousAnimatedSnapStoryID = Settings.getValue("previousAnimatedSnapStoryID"); var previousAnimatedSnapBlastingDisabled = Settings.getValue("previousAnimatedSnapBlastingDisabled"); var previousAnimatedSnapHifiSharingDisabled = Settings.getValue("previousAnimatedSnapHifiSharingDisabled"); + snapshotOptions = { containsGif: previousAnimatedSnapPath !== "", processingGif: false, shouldUpload: false, - canBlast: location.domainID === Settings.getValue("previousSnapshotDomainID"), + canBlast: snapshotDomainID === Settings.getValue("previousSnapshotDomainID") && + snapshotDomainID === location.domainID, isLoggedIn: isLoggedIn }; imageData = []; @@ -376,19 +366,6 @@ function fillImageDataFromPrevious() { } } -var SNAPSHOT_REVIEW_URL = Script.resolvePath("html/SnapshotReview.html"); -var isInSnapshotReview = false; -function onButtonClicked() { - if (isInSnapshotReview){ - // for toolbar-mode: go back to home screen, this will close the window. - tablet.gotoHomeScreen(); - } else { - fillImageDataFromPrevious(); - tablet.gotoWebScreen(SNAPSHOT_REVIEW_URL); - HMD.openTablet(); - } -} - function snapshotUploaded(isError, reply) { if (!isError) { var replyJson = JSON.parse(reply), @@ -397,7 +374,8 @@ function snapshotUploaded(isError, reply) { isGif = fileExtensionMatches(imageURL, "gif"), ignoreGifSnapshotData = false, ignoreStillSnapshotData = false; - storyIDsToMaybeDelete.push(storyID); + storyIDsToMaybeDelete.push(parseInt(storyID)); + print('storyIDsToMaybeDelete[] now:', JSON.stringify(storyIDsToMaybeDelete)); if (isGif) { if (mostRecentGifSnapshotFilename !== replyJson.user_story.details.original_image_file_name) { ignoreGifSnapshotData = true; @@ -409,12 +387,12 @@ function snapshotUploaded(isError, reply) { } if ((isGif && !ignoreGifSnapshotData) || (!isGif && !ignoreStillSnapshotData)) { print('SUCCESS: Snapshot uploaded! Story with audience:for_url created! ID:', storyID); - tablet.emitScriptEvent(JSON.stringify({ + ui.sendMessage({ type: "snapshot", action: "snapshotUploadComplete", story_id: storyID, image_url: imageURL, - })); + }); if (isGif) { Settings.setValue("previousAnimatedSnapStoryID", storyID); } else { @@ -427,12 +405,12 @@ function snapshotUploaded(isError, reply) { } isUploadingPrintableStill = false; } -var href, domainID; +var href, snapshotDomainID; function takeSnapshot() { - tablet.emitScriptEvent(JSON.stringify({ + ui.sendMessage({ type: "snapshot", action: "clearPreviousImages" - })); + }); Settings.setValue("previousStillSnapPath", ""); Settings.setValue("previousStillSnapStoryID", ""); Settings.setValue("previousStillSnapBlastingDisabled", false); @@ -452,8 +430,8 @@ function takeSnapshot() { // Even the domainID could change (e.g., if the user falls into a teleporter while recording). href = location.href; Settings.setValue("previousSnapshotHref", href); - domainID = location.domainID; - Settings.setValue("previousSnapshotDomainID", domainID); + snapshotDomainID = location.domainID; + Settings.setValue("previousSnapshotDomainID", snapshotDomainID); maybeDeleteSnapshotStories(); @@ -471,10 +449,6 @@ function takeSnapshot() { } else { Window.stillSnapshotTaken.connect(stillSnapshotTaken); } - if (buttonConnected) { - button.clicked.disconnect(onButtonClicked); - buttonConnected = false; - } // hide overlays if they are on if (resetOverlays) { @@ -538,10 +512,6 @@ function stillSnapshotTaken(pathStillSnapshot, notify) { Menu.setIsOptionChecked("Show Overlays", true); } Window.stillSnapshotTaken.disconnect(stillSnapshotTaken); - if (!buttonConnected) { - button.clicked.connect(onButtonClicked); - buttonConnected = true; - } // A Snapshot Review dialog might be left open indefinitely after taking the picture, // during which time the user may have moved. So stash that info in the dialog so that @@ -551,7 +521,7 @@ function stillSnapshotTaken(pathStillSnapshot, notify) { HMD.openTablet(); - isDomainOpen(domainID, function (canShare) { + isDomainOpen(snapshotDomainID, function (canShare) { snapshotOptions = { containsGif: false, processingGif: false, @@ -559,12 +529,12 @@ function stillSnapshotTaken(pathStillSnapshot, notify) { isLoggedIn: isLoggedIn }; imageData = [{ localPath: pathStillSnapshot, href: href }]; - tablet.emitScriptEvent(JSON.stringify({ + ui.sendMessage({ type: "snapshot", action: "addImages", options: snapshotOptions, image_data: imageData - })); + }); }); } @@ -572,10 +542,10 @@ function snapshotDirChanged(snapshotPath) { Window.browseDirChanged.disconnect(snapshotDirChanged); if (snapshotPath !== "") { // not cancelled Snapshot.setSnapshotsLocation(snapshotPath); - tablet.emitScriptEvent(JSON.stringify({ + ui.sendMessage({ type: "snapshot", action: "snapshotLocationChosen" - })); + }); } } @@ -594,7 +564,7 @@ function processingGifStarted(pathStillSnapshot) { HMD.openTablet(); - isDomainOpen(domainID, function (canShare) { + isDomainOpen(snapshotDomainID, function (canShare) { snapshotOptions = { containsGif: true, processingGif: true, @@ -603,26 +573,22 @@ function processingGifStarted(pathStillSnapshot) { isLoggedIn: isLoggedIn }; imageData = [{ localPath: pathStillSnapshot, href: href }]; - tablet.emitScriptEvent(JSON.stringify({ + ui.sendMessage({ type: "snapshot", action: "addImages", options: snapshotOptions, image_data: imageData - })); + }); }); } function processingGifCompleted(pathAnimatedSnapshot) { isLoggedIn = Account.isLoggedIn(); Window.processingGifCompleted.disconnect(processingGifCompleted); - if (!buttonConnected) { - button.clicked.connect(onButtonClicked); - buttonConnected = true; - } Settings.setValue("previousAnimatedSnapPath", pathAnimatedSnapshot); - isDomainOpen(domainID, function (canShare) { + isDomainOpen(snapshotDomainID, function (canShare) { snapshotOptions = { containsGif: true, processingGif: false, @@ -631,12 +597,12 @@ function processingGifCompleted(pathAnimatedSnapshot) { canBlast: location.domainID === Settings.getValue("previousSnapshotDomainID"), }; imageData = [{ localPath: pathAnimatedSnapshot, href: href }]; - tablet.emitScriptEvent(JSON.stringify({ + ui.sendMessage({ type: "snapshot", action: "addImages", options: snapshotOptions, image_data: imageData - })); + }); }); } function maybeDeleteSnapshotStories() { @@ -655,28 +621,16 @@ function maybeDeleteSnapshotStories() { }); storyIDsToMaybeDelete = []; } -function onTabletScreenChanged(type, url) { - var wasInSnapshotReview = isInSnapshotReview; - isInSnapshotReview = (type === "Web" && url === SNAPSHOT_REVIEW_URL); - button.editProperties({ isActive: isInSnapshotReview }); - if (isInSnapshotReview !== wasInSnapshotReview) { - if (isInSnapshotReview) { - tablet.webEventReceived.connect(onMessage); - } else { - tablet.webEventReceived.disconnect(onMessage); - } - } -} function onUsernameChanged() { fillImageDataFromPrevious(); isDomainOpen(Settings.getValue("previousSnapshotDomainID"), function (canShare) { - tablet.emitScriptEvent(JSON.stringify({ + ui.sendMessage({ type: "snapshot", action: "showPreviousImages", options: snapshotOptions, image_data: imageData, canShare: canShare - })); + }); }); if (isLoggedIn) { if (shareAfterLogin) { @@ -705,10 +659,10 @@ function onUsernameChanged() { function snapshotLocationSet(location) { if (location !== "") { - tablet.emitScriptEvent(JSON.stringify({ + ui.sendMessage({ type: "snapshot", action: "snapshotLocationChosen" - })); + }); } } @@ -733,36 +687,36 @@ function processRezPermissionChange(canRez) { action = 'setPrintButtonDisabled'; } - tablet.emitScriptEvent(JSON.stringify({ + ui.sendMessage({ type: "snapshot", action : action - })); + }); } -button.clicked.connect(onButtonClicked); -buttonConnected = true; +function startup() { + ui = new AppUi({ + buttonName: "SNAP", + sortOrder: 5, + home: Script.resolvePath("html/SnapshotReview.html"), + onOpened: fillImageDataFromPrevious, + onMessage: onMessage + }); -Window.snapshotShared.connect(snapshotUploaded); -tablet.screenChanged.connect(onTabletScreenChanged); -GlobalServices.myUsernameChanged.connect(onUsernameChanged); -Snapshot.snapshotLocationSet.connect(snapshotLocationSet); + Entities.canRezChanged.connect(updatePrintPermissions); + Entities.canRezTmpChanged.connect(updatePrintPermissions); + GlobalServices.myUsernameChanged.connect(onUsernameChanged); + Snapshot.snapshotLocationSet.connect(snapshotLocationSet); + Window.snapshotShared.connect(snapshotUploaded); +} +startup(); -Entities.canRezChanged.connect(updatePrintPermissions); -Entities.canRezTmpChanged.connect(updatePrintPermissions); - -Script.scriptEnding.connect(function () { - if (buttonConnected) { - button.clicked.disconnect(onButtonClicked); - buttonConnected = false; - } - if (tablet) { - tablet.removeButton(button); - tablet.screenChanged.disconnect(onTabletScreenChanged); - } +function shutdown() { Window.snapshotShared.disconnect(snapshotUploaded); Snapshot.snapshotLocationSet.disconnect(snapshotLocationSet); + GlobalServices.myUsernameChanged.disconnect(onUsernameChanged); Entities.canRezChanged.disconnect(updatePrintPermissions); Entities.canRezTmpChanged.disconnect(updatePrintPermissions); -}); +} +Script.scriptEnding.connect(shutdown); }()); // END LOCAL_SCOPE diff --git a/scripts/system/tablet-goto.js b/scripts/system/tablet-goto.js index 46ddeb2bab..94117fd9ea 100644 --- a/scripts/system/tablet-goto.js +++ b/scripts/system/tablet-goto.js @@ -1,9 +1,10 @@ "use strict"; -/*jslint vars:true, plusplus:true, forin:true*/ -/*global Window, Script, Tablet, HMD, Controller, Account, XMLHttpRequest, location, print*/ +/* jslint vars:true, plusplus:true, forin:true */ +/* eslint indent: ["error", 4, { "outerIIFEBody": 0 }] */ +/* global Window, Script, Tablet, HMD, Controller, Account, XMLHttpRequest, location, print */ // -// goto.js +// tablet-goto.js // scripts/system/ // // Created by Dante Ruiz on 8 February 2017 @@ -14,148 +15,121 @@ // (function () { // BEGIN LOCAL_SCOPE +var AppUi = Script.require('appUi'); - var request = Script.require('request').request; - var DEBUG = false; - function debug() { - if (!DEBUG) { +function gotoOpened() { + shouldShowDot = false; + ui.messagesWaiting(shouldShowDot); +} + +function notificationDataProcessPage(data) { + return data.user_stories; +} + +var shouldShowDot = false; +var pingPong = false; +var storedAnnouncements = {}; +var storedFeaturedStories = {}; +var message; +function notificationPollCallback(userStoriesArray) { + // + // START logic for keeping track of new info + // + pingPong = !pingPong; + var totalNewStories = 0; + var shouldNotifyIndividually = !ui.isOpen && ui.notificationInitialCallbackMade[0]; + userStoriesArray.forEach(function (story) { + if (story.audience !== "for_connections" && + story.audience !== "for_feed") { return; } - print('tablet-goto.js:', [].map.call(arguments, JSON.stringify)); - } - var gotoQmlSource = "hifi/tablet/TabletAddressDialog.qml"; - var buttonName = "GOTO"; - var onGotoScreen = false; - var shouldActivateButton = false; - function ignore() { } + var stored = storedAnnouncements[story.id] || storedFeaturedStories[story.id]; + var storedOrNew = stored || story; + storedOrNew.pingPong = pingPong; + if (stored) { + return; + } - var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); - var NORMAL_ICON = "icons/tablet-icons/goto-i.svg"; - var NORMAL_ACTIVE = "icons/tablet-icons/goto-a.svg"; - var WAITING_ICON = "icons/tablet-icons/goto-msg.svg"; - var button = tablet.addButton({ - icon: NORMAL_ICON, - activeIcon: NORMAL_ACTIVE, - text: buttonName, - sortOrder: 8 + totalNewStories++; + + if (story.audience === "for_connections") { + storedAnnouncements[story.id] = story; + + if (shouldNotifyIndividually) { + message = story.username + " says something is happening in " + + story.place_name + ". Open GOTO to join them."; + ui.notificationDisplayBanner(message); + } + } else if (story.audience === "for_feed") { + storedFeaturedStories[story.id] = story; + + if (shouldNotifyIndividually) { + message = story.username + " invites you to an event in " + + story.place_name + ". Open GOTO to join them."; + ui.notificationDisplayBanner(message); + } + } }); - - function messagesWaiting(isWaiting) { - button.editProperties({ - icon: isWaiting ? WAITING_ICON : NORMAL_ICON - // No need for a different activeIcon, because we issue messagesWaiting(false) when the button goes active anyway. - }); - } - - function onClicked() { - if (onGotoScreen) { - // for toolbar-mode: go back to home screen, this will close the window. - tablet.gotoHomeScreen(); - } else { - shouldActivateButton = true; - tablet.loadQMLSource(gotoQmlSource); - onGotoScreen = true; + var key; + for (key in storedAnnouncements) { + if (storedAnnouncements[key].pingPong !== pingPong) { + delete storedAnnouncements[key]; } } - - function onScreenChanged(type, url) { - ignore(type); - if (url === gotoQmlSource) { - onGotoScreen = true; - shouldActivateButton = true; - button.editProperties({isActive: shouldActivateButton}); - messagesWaiting(false); - } else { - shouldActivateButton = false; - onGotoScreen = false; - button.editProperties({isActive: shouldActivateButton}); + for (key in storedFeaturedStories) { + if (storedFeaturedStories[key].pingPong !== pingPong) { + delete storedFeaturedStories[key]; } } - button.clicked.connect(onClicked); - tablet.screenChanged.connect(onScreenChanged); + // + // END logic for keeping track of new info + // - var stories = {}, pingPong = false; - function expire(id) { - var options = { - uri: Account.metaverseServerURL + '/api/v1/user_stories/' + id, - method: 'PUT', - json: true, - body: {expire: "true"} - }; - request(options, function (error, response) { - debug('expired story', options, 'error:', error, 'response:', response); - if (error || (response.status !== 'success')) { - print("ERROR expiring story: ", error || response.status); - } - }); - } - function pollForAnnouncements() { - // We could bail now if !Account.isLoggedIn(), but what if we someday have system-wide announcments? - var actions = 'announcement'; - var count = DEBUG ? 10 : 100; - var options = [ - 'now=' + new Date().toISOString(), - 'include_actions=' + actions, - 'restriction=' + (Account.isLoggedIn() ? 'open,hifi' : 'open'), - 'require_online=true', - 'protocol=' + encodeURIComponent(Window.protocolSignature()), - 'per_page=' + count - ]; - var url = Account.metaverseServerURL + '/api/v1/user_stories?' + options.join('&'); - request({ - uri: url - }, function (error, data) { - debug(url, error, data); - if (error || (data.status !== 'success')) { - print("Error: unable to get", url, error || data.status); - return; - } - var didNotify = false, key; - pingPong = !pingPong; - data.user_stories.forEach(function (story) { - var stored = stories[story.id], storedOrNew = stored || story; - debug('story exists:', !!stored, storedOrNew); - if ((storedOrNew.username === Account.username) && (storedOrNew.place_name !== location.placename)) { - if (storedOrNew.audience == 'for_connections') { // Only expire if we haven't already done so. - expire(story.id); - } - return; // before marking - } - storedOrNew.pingPong = pingPong; - if (stored) { // already seen - return; - } - stories[story.id] = story; - var message = story.username + " says something is happening in " + story.place_name + ". Open GOTO to join them."; - Window.displayAnnouncement(message); - didNotify = true; - }); - for (key in stories) { // Any story we were tracking that was not marked, has expired. - if (stories[key].pingPong !== pingPong) { - debug('removing story', key); - delete stories[key]; - } - } - if (didNotify) { - messagesWaiting(true); - if (HMD.isHandControllerAvailable()) { - var STRENGTH = 1.0, DURATION_MS = 60, HAND = 2; // both hands - Controller.triggerHapticPulse(STRENGTH, DURATION_MS, HAND); - } - } else if (!Object.keys(stories).length) { // If there's nothing being tracked, then any messageWaiting has expired. - messagesWaiting(false); - } - }); - } - var ANNOUNCEMENTS_POLL_TIME_MS = (DEBUG ? 10 : 60) * 1000; - var pollTimer = Script.setInterval(pollForAnnouncements, ANNOUNCEMENTS_POLL_TIME_MS); + var totalStories = Object.keys(storedAnnouncements).length + + Object.keys(storedFeaturedStories).length; + shouldShowDot = totalNewStories > 0 || (totalStories > 0 && shouldShowDot); + ui.messagesWaiting(shouldShowDot && !ui.isOpen); - Script.scriptEnding.connect(function () { - Script.clearInterval(pollTimer); - button.clicked.disconnect(onClicked); - tablet.removeButton(button); - tablet.screenChanged.disconnect(onScreenChanged); + if (totalStories > 0 && !ui.isOpen && !ui.notificationInitialCallbackMade[0]) { + message = "There " + (totalStories === 1 ? "is " : "are ") + totalStories + " event" + + (totalStories === 1 ? "" : "s") + " to know about. " + + "Open GOTO to see " + (totalStories === 1 ? "it" : "them") + "."; + ui.notificationDisplayBanner(message); + } +} + +function isReturnedDataEmpty(data) { + var storiesArray = data.user_stories; + return storiesArray.length === 0; +} + +var ui; +var GOTO_QML_SOURCE = "hifi/tablet/TabletAddressDialog.qml"; +var BUTTON_NAME = "GOTO"; +function startup() { + var options = [ + 'include_actions=announcement', + 'restriction=open,hifi', + 'require_online=true', + 'protocol=' + encodeURIComponent(Window.protocolSignature()), + 'per_page=10' + ]; + var endpoint = '/api/v1/user_stories?' + options.join('&'); + + ui = new AppUi({ + buttonName: BUTTON_NAME, + sortOrder: 8, + onOpened: gotoOpened, + home: GOTO_QML_SOURCE, + notificationPollEndpoint: [endpoint], + notificationPollTimeoutMs: [60000], + notificationDataProcessPage: [notificationDataProcessPage], + notificationPollCallback: [notificationPollCallback], + notificationPollStopPaginatingConditionMet: [isReturnedDataEmpty], + notificationPollCaresAboutSince: [false] }); +} +startup(); }()); // END LOCAL_SCOPE diff --git a/scripts/system/tablet-ui/tabletUI.js b/scripts/system/tablet-ui/tabletUI.js index f339475f72..9a22014895 100644 --- a/scripts/system/tablet-ui/tabletUI.js +++ b/scripts/system/tablet-ui/tabletUI.js @@ -19,11 +19,12 @@ var tabletRezzed = false; var activeHand = null; var DEFAULT_WIDTH = 0.4375; - var DEFAULT_TABLET_SCALE = 70; + var DEFAULT_DESKTOP_TABLET_SCALE = 75; + var DEFAULT_HMD_TABLET_SCALE = 60; var preMakeTime = Date.now(); var validCheckTime = Date.now(); var debugTablet = false; - var tabletScalePercentage = 70.0; + var tabletScalePercentage = DEFAULT_HMD_TABLET_SCALE; var UIWebTablet = null; var MSECS_PER_SEC = 1000.0; var MUTE_MICROPHONE_MENU_ITEM = "Mute Microphone"; @@ -66,14 +67,14 @@ } function getTabletScalePercentageFromSettings() { - checkTablet() + checkTablet(); var toolbarMode = gTablet.toolbarMode; - var tabletScalePercentage = DEFAULT_TABLET_SCALE; + var tabletScalePercentage = DEFAULT_HMD_TABLET_SCALE; if (!toolbarMode) { if (HMD.active) { - tabletScalePercentage = Settings.getValue("hmdTabletScale") || DEFAULT_TABLET_SCALE; + tabletScalePercentage = Settings.getValue("hmdTabletScale") || DEFAULT_HMD_TABLET_SCALE; } else { - tabletScalePercentage = Settings.getValue("desktopTabletScale") || DEFAULT_TABLET_SCALE; + tabletScalePercentage = Settings.getValue("desktopTabletScale") || DEFAULT_DESKTOP_TABLET_SCALE; } } return tabletScalePercentage; @@ -306,7 +307,7 @@ } wantsMenu = clicked; }); - + clickMapping.from(Controller.Standard.Start).peek().to(function (clicked) { if (clicked) { //activeHudPoint2dGamePad(); diff --git a/scripts/tutorials/createCow.js b/scripts/tutorials/createCow.js index 16498e0e8c..0f034ecefa 100644 --- a/scripts/tutorials/createCow.js +++ b/scripts/tutorials/createCow.js @@ -45,11 +45,7 @@ var cow = Entities.addEntity({ lifetime: 3600, shapeType: "box", script: SCRIPT_URL, - userData: JSON.stringify({ - grabbableKey: { - grabbable: true - } - }) + grab: { grabbable: true } }); -Script.stop(); \ No newline at end of file +Script.stop(); diff --git a/scripts/tutorials/createFlashlight.js b/scripts/tutorials/createFlashlight.js index f3e1e72182..329be56af7 100644 --- a/scripts/tutorials/createFlashlight.js +++ b/scripts/tutorials/createFlashlight.js @@ -37,11 +37,6 @@ var flashlight = Entities.addEntity({ shapeType: 'box', lifetime: 3600, script: SCRIPT_URL, - userData: JSON.stringify({ - grabbableKey: { - invertSolidWhileHeld: true - } - }) }); -Script.stop(); \ No newline at end of file +Script.stop(); diff --git a/scripts/tutorials/createGolfClub.js b/scripts/tutorials/createGolfClub.js index 21e60f26ef..4135b6680d 100644 --- a/scripts/tutorials/createGolfClub.js +++ b/scripts/tutorials/createGolfClub.js @@ -53,35 +53,34 @@ var golfClubProperties = { y: -5.0, z: 0 }, - userData: JSON.stringify({ - wearable: { - joints: { - LeftHand: [{ - x: -0.1631782054901123, - y: 0.44648152589797974, - z: 0.10100018978118896 - }, { - x: -0.9181621670722961, - y: -0.0772884339094162, - z: -0.3870723247528076, - w: -0.0343472845852375 - }], - RightHand: [{ - x: 0.16826771199703217, - y: 0.4757269620895386, - z: 0.07139724493026733 - }, { - x: -0.7976328134536743, - y: -0.0011603273451328278, - z: 0.6030101776123047, - w: -0.012610925361514091 - }] - } + grab: { + equippable: true, + equippableLeftPosition: { + x: -0.1631782054901123, + y: 0.44648152589797974, + z: 0.10100018978118896 + }, + equippableLeftRotation: { + x: -0.9181621670722961, + y: -0.0772884339094162, + z: -0.3870723247528076, + w: -0.0343472845852375 + }, + equippableRightPosition: { + x: 0.16826771199703217, + y: 0.4757269620895386, + z: 0.07139724493026733 + }, + equippableRightRotation: { + x: -0.7976328134536743, + y: -0.0011603273451328278, + z: 0.6030101776123047, + w: -0.012610925361514091 } - }) -} + } +}; var golfClub = Entities.addEntity(golfClubProperties); -Script.stop(); \ No newline at end of file +Script.stop(); diff --git a/scripts/tutorials/createPingPongGun.js b/scripts/tutorials/createPingPongGun.js index c86a78e96d..927738f29e 100644 --- a/scripts/tutorials/createPingPongGun.js +++ b/scripts/tutorials/createPingPongGun.js @@ -37,35 +37,31 @@ var pingPongGunProperties = { }, lifetime: 3600, dynamic: true, - userData: JSON.stringify({ - grabbableKey: { - invertSolidWhileHeld: true + grab: { + equippable: true, + equippableLeftPosition: { + x: 0.09151676297187805, + y: 0.13639454543590546, + z: 0.09354984760284424 }, - wearable: { - joints: { - RightHand: [{ - x: 0.1177130937576294, - y: 0.12922893464565277, - z: 0.08307232707738876 - }, { - x: 0.4934672713279724, - y: 0.3605862259864807, - z: 0.6394805908203125, - w: -0.4664038419723511 - }], - LeftHand: [{ - x: 0.09151676297187805, - y: 0.13639454543590546, - z: 0.09354984760284424 - }, { - x: -0.19628101587295532, - y: 0.6418180465698242, - z: 0.2830369472503662, - w: 0.6851521730422974 - }] - } + equippableLeftRotation: { + x: -0.19628101587295532, + y: 0.6418180465698242, + z: 0.2830369472503662, + w: 0.6851521730422974 + }, + equippableRightPosition: { + x: 0.1177130937576294, + y: 0.12922893464565277, + z: 0.08307232707738876 + }, + equippableRightRotation: { + x: 0.4934672713279724, + y: 0.3605862259864807, + z: 0.6394805908203125, + w: -0.4664038419723511 } - }) + } } var pingPongGun = Entities.addEntity(pingPongGunProperties); diff --git a/scripts/tutorials/createPistol.js b/scripts/tutorials/createPistol.js index 8851f53d09..0912645c35 100644 --- a/scripts/tutorials/createPistol.js +++ b/scripts/tutorials/createPistol.js @@ -37,38 +37,34 @@ var pistolProperties = { restitution: 0, damping: 0.5, collisionSoundURL: COLLISION_SOUND_URL, - userData: JSON.stringify({ - grabbableKey: { - invertSolidWhileHeld: true + grab: { + equippable: true, + equippableLeftPosition: { + x: 0.1802254319190979, + y: 0.13442856073379517, + z: 0.08504903316497803 }, - wearable: { - joints: { - RightHand: [{ - x: 0.07079616189002991, - y: 0.20177987217903137, - z: 0.06374628841876984 - }, { - x: -0.5863648653030396, - y: -0.46007341146469116, - z: 0.46949487924575806, - w: -0.4733745753765106 - }], - LeftHand: [{ - x: 0.1802254319190979, - y: 0.13442856073379517, - z: 0.08504903316497803 - }, { - x: 0.2198076844215393, - y: -0.7377811074256897, - z: 0.2780133783817291, - w: 0.574519157409668 - }] - } + equippableLeftRotation: { + x: 0.2198076844215393, + y: -0.7377811074256897, + z: 0.2780133783817291, + w: 0.574519157409668 + }, + equippableRightPosition: { + x: 0.07079616189002991, + y: 0.20177987217903137, + z: 0.06374628841876984 + }, + equippableRightRotation: { + x: -0.5863648653030396, + y: -0.46007341146469116, + z: 0.46949487924575806, + w: -0.4733745753765106 } - }) + } }; var pistol = Entities.addEntity(pistolProperties); -Script.stop(); \ No newline at end of file +Script.stop(); diff --git a/scripts/tutorials/createSwords.js b/scripts/tutorials/createSwords.js index 749df3c5ed..f375939dc8 100644 --- a/scripts/tutorials/createSwords.js +++ b/scripts/tutorials/createSwords.js @@ -29,11 +29,7 @@ var sword1 = Entities.addEntity({ angularDamping: 0, damping: 0, script: SCRIPT_URL, - userData:JSON.stringify({ - grabbableKey:{ - grabbable:true - } - }) + grab: { grabbable: true } }); var sword2 = Entities.addEntity({ @@ -45,11 +41,7 @@ var sword2 = Entities.addEntity({ angularDamping: 0, damping: 0, script: SCRIPT_URL, - userData:JSON.stringify({ - grabbableKey:{ - grabbable:true - } - }) + grab: { grabbable: true } }); Script.scriptEnding.connect(function scriptEnding() { diff --git a/scripts/tutorials/createTetherballStick.js b/scripts/tutorials/createTetherballStick.js index 1b5bc36932..b940762137 100644 --- a/scripts/tutorials/createTetherballStick.js +++ b/scripts/tutorials/createTetherballStick.js @@ -1,6 +1,6 @@ "use strict"; /* jslint vars: true, plusplus: true, forin: true*/ -/* globals Tablet, Script, AvatarList, Users, Entities, MyAvatar, Camera, Overlays, Vec3, Quat, Controller, print, getControllerWorldLocation */ +/* globals Script, Entities, MyAvatar, Vec3, Quat */ /* eslint indent: ["error", 4, { "outerIIFEBody": 0 }] */ // // createTetherballStick.js @@ -59,11 +59,7 @@ var ballID = Entities.addEntity({ restitution: BALL_RESTITUTION, dynamic: true, collidesWith: "static,dynamic,otherAvatar,", - userData: JSON.stringify({ - grabbableKey: { - grabbable: false - } - }) + grab: { grabbable: false } }); var lineID = Entities.addEntity({ @@ -109,43 +105,38 @@ var STICK_PROPERTIES = { }, shapeType: 'box', lifetime: LIFETIME, - userData: JSON.stringify({ - grabbableKey: { - invertSolidWhileHeld: true, - ignoreIK: false + grab: { + grabbable: false, + grabFollowsController: false, + equippable: true, + equippableLeftPosition: { + x: -0.14998853206634521, + y: 0.17033983767032623, + z: 0.023199155926704407 }, - wearable: { - joints: { - RightHand: [{ - x: 0.15539926290512085, - y: 0.14493153989315033, - z: 0.023641478270292282 - }, { - x: 0.5481458902359009, - y: -0.4470711946487427, - z: -0.3148134648799896, - w: 0.6328644752502441 - }], - LeftHand: [{ - x: -0.14998853206634521, - y: 0.17033983767032623, - z: 0.023199155926704407 - }, - { - x: 0.6623835563659668, - y: -0.1671387255191803, - z: 0.7071226835250854, - w: 0.1823924481868744 - }] - } + equippableLeftRotation: { + x: 0.6623835563659668, + y: -0.1671387255191803, + z: 0.7071226835250854, + w: 0.1823924481868744 }, - ownerID: MyAvatar.sessionUUID, - ballID: ballID, - lineID: lineID, - actionID: actionID, - lifetime: LIFETIME, - maxDistanceBetweenBallAndStick: ACTION_DISTANCE * MAX_DISTANCE_MULTIPLIER - }) + equippableRightPosition: { + x: 0.15539926290512085, + y: 0.14493153989315033, + z: 0.023641478270292282 + }, + equippableRightRotation: { + x: 0.5481458902359009, + y: -0.4470711946487427, + z: -0.3148134648799896, + w: 0.6328644752502441 + } + }, + ownerID: MyAvatar.sessionUUID, + ballID: ballID, + lineID: lineID, + actionID: actionID, + maxDistanceBetweenBallAndStick: ACTION_DISTANCE * MAX_DISTANCE_MULTIPLIER }; Entities.addEntity(STICK_PROPERTIES); diff --git a/scripts/tutorials/entity_scripts/ambientSound.js b/scripts/tutorials/entity_scripts/ambientSound.js index dd964fb4e0..cbd94c22e4 100644 --- a/scripts/tutorials/entity_scripts/ambientSound.js +++ b/scripts/tutorials/entity_scripts/ambientSound.js @@ -32,7 +32,7 @@ range: DEFAULT_RANGE, maxVolume: DEFAULT_VOLUME, disabled: true, - grabbableKey: { wantsTrigger: true }, + grab: { triggerable: true } }; var soundURL = ""; @@ -71,13 +71,13 @@ try { var data = JSON.parse(props.userData); } catch(e) { - debugPrint("unable to parse userData JSON string: " + props.userData); + debugPrint("unable to parse userData JSON string"); this.cleanup(); return; } if (data.soundURL && !(soundURL === data.soundURL)) { soundURL = data.soundURL; - debugPrint("Read ambient sound URL: " + soundURL); + debugPrint("Read ambient sound URL"); } if (data.range && !(range === data.range)) { range = data.range; @@ -113,7 +113,7 @@ ambientSound = SoundCache.getSound(soundURL); } else if (resource.state === Resource.State.FAILED) { resource.stateChanged.disconnect(onStateChanged); - debugPrint("Failed to download ambient sound: " + soundURL); + debugPrint("Failed to download ambient sound"); } } resource.stateChanged.connect(onStateChanged); @@ -151,7 +151,7 @@ var data = JSON.parse(props.userData); data.disabled = !data.disabled; - debugPrint(hint + " -- triggering ambient sound " + (data.disabled ? "OFF" : "ON") + " (" + data.soundURL + ")"); + debugPrint(hint + " -- triggering ambient sound " + (data.disabled ? "OFF" : "ON")); this.cleanup(); @@ -182,7 +182,7 @@ entity = entityID; _this = this; - var props = Entities.getEntityProperties(entity, [ "userData" ]); + var props = Entities.getEntityProperties(entity, [ "userData", "grab.triggerable" ]); var data = {}; if (props.userData) { data = JSON.parse(props.userData); @@ -194,14 +194,15 @@ changed = true; } } - if (!data.grabbableKey.wantsTrigger) { - data.grabbableKey.wantsTrigger = true; - changed = true; - } if (changed) { debugPrint("applying default values to userData"); Entities.editEntity(entity, { userData: JSON.stringify(data) }); } + + if (!props.grab.triggerable) { + Entities.editEntity(entity, { grab: { triggerable: true } }); + } + this._updateColor(data.disabled); this.updateSettings(); @@ -235,7 +236,7 @@ soundOptions.orientation = rotation; soundOptions.volume = volume; if (!soundPlaying && ambientSound && ambientSound.downloaded) { - debugPrint("Starting ambient sound: " + soundURL + " (duration: " + ambientSound.duration + ")"); + debugPrint("Starting ambient sound: (duration: " + ambientSound.duration + ")"); soundPlaying = Audio.playSound(ambientSound, soundOptions); } else if (soundPlaying && soundPlaying.playing) { soundPlaying.setOptions(soundOptions); @@ -243,7 +244,7 @@ } else if (soundPlaying && soundPlaying.playing && (distance > range * HYSTERESIS_FRACTION)) { soundPlaying.stop(); soundPlaying = false; - debugPrint("Out of range, stopping ambient sound: " + soundURL); + debugPrint("Out of range, stopping ambient sound"); } }; diff --git a/scripts/tutorials/entity_scripts/magneticBlock.js b/scripts/tutorials/entity_scripts/magneticBlock.js index 1ec5f2a6c6..2f073a5f41 100644 --- a/scripts/tutorials/entity_scripts/magneticBlock.js +++ b/scripts/tutorials/entity_scripts/magneticBlock.js @@ -52,32 +52,8 @@ It will behave as the constructor */ preload: function(id) { - /* - We will now override any existing userdata with the grabbable property. - Only retrieving userData - */ - var entityProperties = Entities.getEntityProperties(id, ['userData']); - var userData = { - grabbableKey: {} - }; - // Check if existing userData field exists. - if (entityProperties.userData && entityProperties.userData.length > 0) { - try { - userData = JSON.parse(entityProperties.userData); - if (!userData.grabbableKey) { - userData.grabbableKey = {}; // If by random change there is no grabbableKey in the userData. - } - } catch (e) { - // if user data is not valid json, we will simply overwrite it. - } - } - // Object must be triggerable inorder to bind releaseGrabEvent - userData.grabbableKey.grabbable = true; - // Apply the new properties to entity of id - Entities.editEntity(id, { - userData: JSON.stringify(userData) - }); + Entities.editEntity(id, { grab: { grabbable: true } }); Script.scriptEnding.connect(function() { Script.removeEventHandler(id, "releaseGrab", this.releaseGrab); }); diff --git a/scripts/tutorials/entity_scripts/springHold.js b/scripts/tutorials/entity_scripts/springHold.js index 059ea2cc6f..dbf0ceafeb 100644 --- a/scripts/tutorials/entity_scripts/springHold.js +++ b/scripts/tutorials/entity_scripts/springHold.js @@ -62,11 +62,7 @@ position: originalProps.position, shapeType: originalProps.shapeType, visible: true, - userData:JSON.stringify({ - grabbableKey:{ - grabbable:false - } - }) + grab: { grabbable: false } }; _this.copy = Entities.addEntity(props); } diff --git a/scripts/tutorials/entity_scripts/touch.js b/scripts/tutorials/entity_scripts/touch.js index 1d0586b350..6e1974a4fd 100644 --- a/scripts/tutorials/entity_scripts/touch.js +++ b/scripts/tutorials/entity_scripts/touch.js @@ -142,11 +142,7 @@ position: originalProps.position, shapeType: originalProps.shapeType, visible: true, - userData:JSON.stringify({ - grabbableKey:{ - grabbable:false - } - }) + grab: { grabbable: false } }; _this.copy = Entities.addEntity(props); } diff --git a/scripts/tutorials/makeBlocks.js b/scripts/tutorials/makeBlocks.js index 432f7444c4..c7d0425863 100644 --- a/scripts/tutorials/makeBlocks.js +++ b/scripts/tutorials/makeBlocks.js @@ -53,14 +53,10 @@ y: SIZE, z: SIZE }, - userData: JSON.stringify({ - grabbableKey: { - cloneable: true, - grabbable: true, - cloneLifetime: LIFETIME, - cloneLimit: 9999 - } - }), + grab: { grabbable: true }, + cloneable: true, + cloneLifetime: LIFETIME, + cloneLimit: 9999 position: Vec3.sum(MyAvatar.position, Vec3.sum(forwardOffset, forwardVector)), color: newColor(), script: SCRIPT_URL diff --git a/server-console/CMakeLists.txt b/server-console/CMakeLists.txt index 1c6e40c582..49742cacf0 100644 --- a/server-console/CMakeLists.txt +++ b/server-console/CMakeLists.txt @@ -4,13 +4,17 @@ if (PRODUCTION_BUILD) set(PRODUCTION_OPTION "--production") endif() +if (CLIENT_ONLY) + set(CLIENT_ONLY_OPTION "--client_only") +endif() + # add a target that will package the console add_custom_target(${TARGET_NAME}-npm-install COMMAND npm install WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} ) add_custom_target(${TARGET_NAME} - COMMAND npm run packager -- --out ${CMAKE_CURRENT_BINARY_DIR} ${PRODUCTION_OPTION} + COMMAND npm run packager -- --out ${CMAKE_CURRENT_BINARY_DIR} ${PRODUCTION_OPTION} ${CLIENT_ONLY_OPTION} WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} DEPENDS ${TARGET_NAME}-npm-install ) @@ -19,11 +23,21 @@ set_target_properties(${TARGET_NAME} PROPERTIES FOLDER "Server Console") set_target_properties(${TARGET_NAME}-npm-install PROPERTIES FOLDER "hidden/Server Console") # add a dependency from the package target to the server components -add_dependencies(${TARGET_NAME} assignment-client domain-server) +if (BUILD_CLIENT) + add_dependencies(${TARGET_NAME} interface) +endif() + +if (BUILD_SERVER) + add_dependencies(${TARGET_NAME} assignment-client domain-server) +endif() # set the packaged console folder depending on platform, so we can copy it if (APPLE) - set(PACKAGED_CONSOLE_FOLDER "Sandbox-darwin-x64/${CONSOLE_EXEC_NAME}") + if (CLIENT_ONLY) + set(PACKAGED_CONSOLE_FOLDER "Console-darwin-x64/${CONSOLE_EXEC_NAME}") + else () + set(PACKAGED_CONSOLE_FOLDER "Sandbox-darwin-x64/${CONSOLE_EXEC_NAME}") + endif() elseif (WIN32) set(PACKAGED_CONSOLE_FOLDER "server-console-win32-x64") elseif (UNIX) @@ -33,9 +47,9 @@ endif () # install the packaged Server Console in our install directory if (APPLE) install( - PROGRAMS "${CMAKE_CURRENT_BINARY_DIR}/${PACKAGED_CONSOLE_FOLDER}" + DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/${PACKAGED_CONSOLE_FOLDER}" + USE_SOURCE_PERMISSIONS DESTINATION ${CONSOLE_INSTALL_DIR} - COMPONENT ${SERVER_COMPONENT} ) elseif (WIN32) set(CONSOLE_DESTINATION "${CMAKE_CURRENT_BINARY_DIR}/${PACKAGED_CONSOLE_FOLDER}") @@ -43,7 +57,6 @@ elseif (WIN32) install( DIRECTORY "${CONSOLE_DESTINATION}/" DESTINATION ${CONSOLE_INSTALL_DIR} - COMPONENT ${SERVER_COMPONENT} ) # sign the copied server console executable after install diff --git a/server-console/package-lock.json b/server-console/package-lock.json index 4f12f2fa00..e27c3815f6 100644 --- a/server-console/package-lock.json +++ b/server-console/package-lock.json @@ -5,9 +5,9 @@ "requires": true, "dependencies": { "@types/node": { - "version": "8.10.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-8.10.2.tgz", - "integrity": "sha512-A6Uv1anbsCvrRDtaUXS2xZ5tlzD+Kg7yMRlSLFDy3z0r7KlGXDzL14vELXIAgpk2aJbU3XeZZQRcEkLkowT92g==", + "version": "8.10.29", + "resolved": "https://registry.npmjs.org/@types/node/-/node-8.10.29.tgz", + "integrity": "sha512-zbteaWZ2mdduacm0byELwtRyhYE40aK+pAanQk415gr1eRuu67x7QGOLmn8jz5zI8LDK7d0WI/oT6r5Trz4rzQ==", "dev": true }, "abbrev": { @@ -21,10 +21,10 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz", "integrity": "sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU=", "requires": { - "co": "4.6.0", - "fast-deep-equal": "1.0.0", - "fast-json-stable-stringify": "2.0.0", - "json-schema-traverse": "0.3.1" + "co": "^4.6.0", + "fast-deep-equal": "^1.0.0", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.3.0" } }, "always-tail": { @@ -32,7 +32,14 @@ "resolved": "https://registry.npmjs.org/always-tail/-/always-tail-0.2.0.tgz", "integrity": "sha1-M5sa9E1QJQqgeg6H7Mw6JOxET/4=", "requires": { - "debug": "0.7.4" + "debug": "~0.7.2" + }, + "dependencies": { + "debug": { + "version": "0.7.4", + "resolved": "http://registry.npmjs.org/debug/-/debug-0.7.4.tgz", + "integrity": "sha1-BuHqgILCyxTjmAbiLi9vdX+Srzk=" + } } }, "ansi-regex": { @@ -47,70 +54,48 @@ "dev": true }, "asar": { - "version": "0.14.2", - "resolved": "https://registry.npmjs.org/asar/-/asar-0.14.2.tgz", - "integrity": "sha512-eKo4ywQDq9dC/0Pu6UJsX4PxNi5ZlC4/NQ1JORUW4xkMRrEWpoLPpkngmQ6K7ZkioVjE2ZafLMmHPAQKMO0BdA==", + "version": "0.14.3", + "resolved": "https://registry.npmjs.org/asar/-/asar-0.14.3.tgz", + "integrity": "sha512-+hNnVVDmYbv05We/a9knj/98w171+A94A9DNHj+3kXUr3ENTQoSEcfbJRvBBRHyOh4vukBYWujmHvvaMmQoQbg==", "dev": true, "requires": { - "chromium-pickle-js": "0.2.0", - "commander": "2.9.0", - "cuint": "0.2.2", - "glob": "6.0.4", - "minimatch": "3.0.4", - "mkdirp": "0.5.1", - "mksnapshot": "0.3.1", + "chromium-pickle-js": "^0.2.0", + "commander": "^2.9.0", + "cuint": "^0.2.1", + "glob": "^6.0.4", + "minimatch": "^3.0.3", + "mkdirp": "^0.5.0", + "mksnapshot": "^0.3.0", "tmp": "0.0.28" }, "dependencies": { - "balanced-match": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", - "dev": true - }, - "brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "requires": { - "balanced-match": "1.0.0", - "concat-map": "0.0.1" - } - }, "glob": { "version": "6.0.4", "resolved": "https://registry.npmjs.org/glob/-/glob-6.0.4.tgz", "integrity": "sha1-DwiGD2oVUSey+t1PnOJLGqtuTSI=", "dev": true, "requires": { - "inflight": "1.0.4", - "inherits": "2.0.1", - "minimatch": "3.0.4", - "once": "1.3.3", - "path-is-absolute": "1.0.0" - } - }, - "minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "dev": true, - "requires": { - "brace-expansion": "1.1.11" + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "2 || 3", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" } } } }, "asn1": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz", - "integrity": "sha1-2sh4dxPJlmhJ/IGAd36+nB3fO4Y=" + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", + "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", + "requires": { + "safer-buffer": "~2.1.0" + } }, "assert-plus": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-0.2.0.tgz", - "integrity": "sha1-104bh+ev/A24qttwIfP+SBAasjQ=" + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" }, "asynckit": { "version": "0.4.0", @@ -129,9 +114,15 @@ "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=" }, "aws4": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.7.0.tgz", - "integrity": "sha512-32NDda82rhwD9/JBCCkB+MRYDp0oSvlo2IL6rQWA10PQi7tDUM3eqMSltXmY+Oyl/7N3P3qNtAlv7X0d9bI28w==" + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz", + "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==" + }, + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "dev": true }, "base64-js": { "version": "1.2.0", @@ -139,14 +130,23 @@ "integrity": "sha1-o5mS1yNYSBGYK+XikLtqU9hnAPE=", "dev": true }, + "bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", + "optional": true, + "requires": { + "tweetnacl": "^0.14.3" + } + }, "binary": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/binary/-/binary-0.3.0.tgz", "integrity": "sha1-n2BVO8XOjDOG87VTz/R0Yq3sqnk=", "dev": true, "requires": { - "buffers": "0.1.1", - "chainsaw": "0.1.0" + "buffers": "~0.1.1", + "chainsaw": "~0.1.0" } }, "bl": { @@ -154,7 +154,7 @@ "resolved": "https://registry.npmjs.org/bl/-/bl-1.1.2.tgz", "integrity": "sha1-/cqHGplxOqANGeO7ukHER4emU5g=", "requires": { - "readable-stream": "2.0.6" + "readable-stream": "~2.0.5" }, "dependencies": { "isarray": { @@ -167,20 +167,20 @@ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.0.6.tgz", "integrity": "sha1-j5A0HmilPMySh4jaz80Rs265t44=", "requires": { - "core-util-is": "1.0.2", - "inherits": "2.0.1", - "isarray": "1.0.0", - "process-nextick-args": "1.0.6", - "string_decoder": "0.10.31", - "util-deprecate": "1.0.2" + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "~1.0.0", + "process-nextick-args": "~1.0.6", + "string_decoder": "~0.10.x", + "util-deprecate": "~1.0.1" } } } }, "bluebird": { - "version": "3.5.1", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.1.tgz", - "integrity": "sha512-MKiLiV+I1AA596t9w1sQJ8jkiSr5+ZKi0WKrYGUn6d1Fx+Ij4tIj+m2WMQSGczs5jZVxV339chE8iwk6F64wjA==", + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.2.tgz", + "integrity": "sha512-dhHTWMI7kMx5whMQntl7Vr9C6BvV10lFXDAasnqnrMYhXVCzzk6IO9Fo2L75jXHT07WrOngL1WDXOp+yYS91Yg==", "dev": true }, "boolbase": { @@ -188,14 +188,44 @@ "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", "integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24=" }, - "boom": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/boom/-/boom-4.3.1.tgz", - "integrity": "sha1-T4owBctKfjiJ90kDD9JbluAdLjE=", + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, "requires": { - "hoek": "4.2.1" + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" } }, + "buffer-alloc": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/buffer-alloc/-/buffer-alloc-1.2.0.tgz", + "integrity": "sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==", + "dev": true, + "requires": { + "buffer-alloc-unsafe": "^1.1.0", + "buffer-fill": "^1.0.0" + } + }, + "buffer-alloc-unsafe": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz", + "integrity": "sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==", + "dev": true + }, + "buffer-fill": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-fill/-/buffer-fill-1.0.0.tgz", + "integrity": "sha1-+PeLdniYiO858gXNY39o5wISKyw=", + "dev": true + }, + "buffer-from": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", + "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==", + "dev": true + }, "buffers": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/buffers/-/buffers-0.1.1.tgz", @@ -219,8 +249,8 @@ "integrity": "sha1-MIvur/3ygRkFHvodkyITyRuPkuc=", "dev": true, "requires": { - "camelcase": "2.1.1", - "map-obj": "1.0.1" + "camelcase": "^2.0.0", + "map-obj": "^1.0.0" } }, "caseless": { @@ -234,19 +264,30 @@ "integrity": "sha1-XqtQsor+WAdNDVgpE4iCi15fvJg=", "dev": true, "requires": { - "traverse": "0.3.9" + "traverse": ">=0.3.0 <0.4" } }, "cheerio": { - "version": "0.19.0", - "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-0.19.0.tgz", - "integrity": "sha1-dy5wFfLuKZZQltcepBdbdas1SSU=", + "version": "0.22.0", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-0.22.0.tgz", + "integrity": "sha1-qbqoYKP5tZWmuBsahocxIe06Jp4=", "requires": { - "css-select": "1.0.0", - "dom-serializer": "0.1.0", - "entities": "1.1.1", - "htmlparser2": "3.8.3", - "lodash": "3.10.1" + "css-select": "~1.2.0", + "dom-serializer": "~0.1.0", + "entities": "~1.1.1", + "htmlparser2": "^3.9.1", + "lodash.assignin": "^4.0.9", + "lodash.bind": "^4.1.4", + "lodash.defaults": "^4.0.1", + "lodash.filter": "^4.4.0", + "lodash.flatten": "^4.2.0", + "lodash.foreach": "^4.3.0", + "lodash.map": "^4.4.0", + "lodash.merge": "^4.4.0", + "lodash.pick": "^4.2.1", + "lodash.reduce": "^4.4.0", + "lodash.reject": "^4.4.0", + "lodash.some": "^4.4.0" } }, "chromium-pickle-js": { @@ -260,9 +301,9 @@ "resolved": "https://registry.npmjs.org/cliui/-/cliui-3.2.0.tgz", "integrity": "sha1-EgYBU3qRbSmUD5NNo7SNWFo5IT0=", "requires": { - "string-width": "1.0.1", - "strip-ansi": "3.0.1", - "wrap-ansi": "2.0.0" + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1", + "wrap-ansi": "^2.0.0" } }, "co": { @@ -275,25 +316,22 @@ "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.0.0.tgz", "integrity": "sha1-9psZLT99keOC5Lcb3bd4eGGasMY=", "requires": { - "number-is-nan": "1.0.0" + "number-is-nan": "^1.0.0" } }, "combined-stream": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.5.tgz", - "integrity": "sha1-k4NwpXtKUd6ix3wV1cX9+JUWQAk=", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.7.tgz", + "integrity": "sha512-brWl9y6vOB1xYPZcpZde3N9zDByXTosAeMDo4p1wzo6UMOX4vumB+TP1RZ76sfE6Md68Q0NJSrE/gbezd4Ul+w==", "requires": { - "delayed-stream": "1.0.0" + "delayed-stream": "~1.0.0" } }, "commander": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.9.0.tgz", - "integrity": "sha1-nJkJQXbhIkDLItbFFGCYQA/g99Q=", - "dev": true, - "requires": { - "graceful-readlink": "1.0.1" - } + "version": "2.18.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.18.0.tgz", + "integrity": "sha512-6CYPa+JP2ftfRU2qkDK+UTVeQYosOg/2GbcjIcKPHfinyOLPVGXu/ovN86RP49Re5ndJK1N0kuiidFFuepc4ZQ==", + "dev": true }, "compare-version": { "version": "0.1.2", @@ -308,34 +346,57 @@ "dev": true }, "concat-stream": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.5.0.tgz", - "integrity": "sha1-U/fUPFHF5D+ByP3QMyHGMb5o1hE=", + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", "dev": true, "requires": { - "inherits": "2.0.1", - "readable-stream": "2.0.6", - "typedarray": "0.0.6" + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" }, "dependencies": { + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "dev": true + }, "isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", "dev": true }, + "process-nextick-args": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", + "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==", + "dev": true + }, "readable-stream": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.0.6.tgz", - "integrity": "sha1-j5A0HmilPMySh4jaz80Rs265t44=", + "version": "2.3.6", + "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", "dev": true, "requires": { - "core-util-is": "1.0.2", - "inherits": "2.0.1", - "isarray": "1.0.0", - "process-nextick-args": "1.0.6", - "string_decoder": "0.10.31", - "util-deprecate": "1.0.2" + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.0" } } } @@ -345,39 +406,21 @@ "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" }, - "cryptiles": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-3.1.2.tgz", - "integrity": "sha1-qJ+7Ig9c4l7FboxKqKT9e1sNKf4=", - "requires": { - "boom": "5.2.0" - }, - "dependencies": { - "boom": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/boom/-/boom-5.2.0.tgz", - "integrity": "sha512-Z5BTk6ZRe4tXXQlkqftmsAUANpXmuwlsF5Oov8ThoMbQRzdGTA1ngYRW160GexgOgjsFOKJz0LYhoNi+2AMBUw==", - "requires": { - "hoek": "4.2.1" - } - } - } - }, "css-select": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/css-select/-/css-select-1.0.0.tgz", - "integrity": "sha1-sRIcpRhI3SZOIkTQWM7iVN7rRLA=", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-1.2.0.tgz", + "integrity": "sha1-KzoRBTnFNV8c2NMUYj6HCxIeyFg=", "requires": { - "boolbase": "1.0.0", - "css-what": "1.0.0", - "domutils": "1.4.3", - "nth-check": "1.0.1" + "boolbase": "~1.0.0", + "css-what": "2.1", + "domutils": "1.5.1", + "nth-check": "~1.0.1" } }, "css-what": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/css-what/-/css-what-1.0.0.tgz", - "integrity": "sha1-18wt9FGAZm+Z0rFEYmOUaeAPc2w=" + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-2.1.0.tgz", + "integrity": "sha1-lGfQMsOM+u+58teVASUwYvh/ob0=" }, "cuint": { "version": "0.2.2", @@ -386,24 +429,27 @@ "dev": true }, "dashdash": { - "version": "1.13.0", - "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.13.0.tgz", - "integrity": "sha1-parm/Z2OFWYk6w3ZJZ6xK6JFOFo=", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", "requires": { - "assert-plus": "1.0.0" - }, - "dependencies": { - "assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" - } + "assert-plus": "^1.0.0" } }, "debug": { - "version": "0.7.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-0.7.4.tgz", - "integrity": "sha1-BuHqgILCyxTjmAbiLi9vdX+Srzk=" + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.0.1.tgz", + "integrity": "sha512-K23FHJ/Mt404FSlp6gSZCevIbTMLX0j3fmHhUEhQ3Wq0FMODW3+cUSoLdy1Gx4polAf4t/lphhmHH35BB8cLYw==", + "requires": { + "ms": "^2.1.1" + }, + "dependencies": { + "ms": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" + } + } }, "decamelize": { "version": "1.2.0", @@ -416,19 +462,19 @@ "integrity": "sha1-rjvLfjTGWHmt/nfhnDD4ZgK0vbA=", "dev": true, "requires": { - "binary": "0.3.0", - "graceful-fs": "4.1.3", - "mkpath": "0.1.0", - "nopt": "3.0.6", - "q": "1.5.1", - "readable-stream": "1.1.14", + "binary": "^0.3.0", + "graceful-fs": "^4.1.3", + "mkpath": "^0.1.0", + "nopt": "^3.0.1", + "q": "^1.1.2", + "readable-stream": "^1.1.8", "touch": "0.0.3" } }, "deep-extend": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.4.1.tgz", - "integrity": "sha1-7+QRPQgIX05vlod1mBD4B0aeIlM=", + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", "dev": true }, "delayed-stream": { @@ -441,8 +487,8 @@ "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.1.0.tgz", "integrity": "sha1-BzxpdUbOB4DOI75KKOKT5AvDDII=", "requires": { - "domelementtype": "1.1.3", - "entities": "1.1.1" + "domelementtype": "~1.1.1", + "entities": "~1.1.1" }, "dependencies": { "domelementtype": { @@ -458,95 +504,109 @@ "integrity": "sha1-sXrtguirWeUt2cGbF1bg/BhyBMI=" }, "domhandler": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.3.0.tgz", - "integrity": "sha1-LeWaCCLVAn+r/28DLCsloqir5zg=", + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.4.2.tgz", + "integrity": "sha512-JiK04h0Ht5u/80fdLMCEmV4zkNh2BcoMFBmZ/91WtYZ8qVXSKjiw7fXMgFPnHcSZgOo3XdinHvmnDUeMf5R4wA==", "requires": { - "domelementtype": "1.3.0" + "domelementtype": "1" } }, "domutils": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.4.3.tgz", - "integrity": "sha1-CGVRN5bGswYDGFDhdVFrr4C3Km8=", + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.5.1.tgz", + "integrity": "sha1-3NhIiib1Y9YQeeSMn3t+Mjc2gs8=", "requires": { - "domelementtype": "1.3.0" + "dom-serializer": "0", + "domelementtype": "1" } }, "ecc-jsbn": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz", - "integrity": "sha1-D8c6ntXw1Tw4GTOYUj735UN3dQU=", + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", "optional": true, "requires": { - "jsbn": "0.1.0" + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" } }, "electron": { - "version": "1.8.4", - "resolved": "https://registry.npmjs.org/electron/-/electron-1.8.4.tgz", - "integrity": "sha512-2f1cx0G3riMFODXFftF5AHXy+oHfhpntZHTDN66Hxtl09gmEr42B3piNEod9MEmw72f75LX2JfeYceqq1PF8cA==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/electron/-/electron-3.0.0.tgz", + "integrity": "sha512-QN9X5vYa4kzJKniwhXlJwioX9qw2fDehdqxN/00KCLz/qnOz/IHLAHGikFjRwfEF2xnkmHxf61F8wn2LePPXXQ==", "dev": true, "requires": { - "@types/node": "8.10.2", - "electron-download": "3.3.0", - "extract-zip": "1.5.0" + "@types/node": "^8.0.24", + "electron-download": "^4.1.0", + "extract-zip": "^1.0.3" } }, "electron-download": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/electron-download/-/electron-download-3.3.0.tgz", - "integrity": "sha1-LP1U1pZsAZxNSa1l++Zcyc3vaMg=", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/electron-download/-/electron-download-4.1.1.tgz", + "integrity": "sha512-FjEWG9Jb/ppK/2zToP+U5dds114fM1ZOJqMAR4aXXL5CvyPE9fiqBK/9YcwC9poIFQTEJk/EM/zyRwziziRZrg==", "dev": true, "requires": { - "debug": "2.6.9", - "fs-extra": "0.30.0", - "home-path": "1.0.5", - "minimist": "1.2.0", - "nugget": "2.0.1", - "path-exists": "2.1.0", - "rc": "1.1.6", - "semver": "5.5.0", - "sumchecker": "1.3.1" + "debug": "^3.0.0", + "env-paths": "^1.0.0", + "fs-extra": "^4.0.1", + "minimist": "^1.2.0", + "nugget": "^2.0.1", + "path-exists": "^3.0.0", + "rc": "^1.2.1", + "semver": "^5.4.1", + "sumchecker": "^2.0.2" }, "dependencies": { "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.5.tgz", + "integrity": "sha512-D61LaDQPQkxJ5AUM2mbSJRbPkNs/TmdmOeLAi1hgDkpDfIfetSrjmWhccwtuResSwMbACjx/xXQofvM9CE/aeg==", "dev": true, "requires": { - "ms": "2.0.0" + "ms": "^2.1.1" } }, "fs-extra": { - "version": "0.30.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-0.30.0.tgz", - "integrity": "sha1-8jP/zAjU2n1DLapEl3aYnbHfk/A=", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-4.0.3.tgz", + "integrity": "sha512-q6rbdDd1o2mAnQreO7YADIxf/Whx4AHBiRf6d+/cVT8h44ss+lHgxf1FemcqDnQt9X3ct4McHr+JMGlYSsK7Cg==", "dev": true, "requires": { - "graceful-fs": "4.1.3", - "jsonfile": "2.2.3", - "klaw": "1.3.1", - "path-is-absolute": "1.0.0", - "rimraf": "2.6.2" + "graceful-fs": "^4.1.2", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" } }, - "semver": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz", - "integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA==", + "jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.6" + }, + "dependencies": { + "graceful-fs": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", + "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=", + "dev": true, + "optional": true + } + } + }, + "ms": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", "dev": true }, - "sumchecker": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-1.3.1.tgz", - "integrity": "sha1-ebs7RFbdBPGOvbwNcDodHa7FEF0=", - "dev": true, - "requires": { - "debug": "2.6.9", - "es6-promise": "4.2.4" - } + "semver": { + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.1.tgz", + "integrity": "sha512-PqpAxfrEhlSUWge8dwIp4tZnQ25DIOthpiaHNIthsjEFQD6EvqUKUDM7L8O2rShkFccYo1VjJR0coWfNkCubRw==", + "dev": true } } }, @@ -556,17 +616,17 @@ "integrity": "sha1-DboCXtM9DkW/j0DG6b487i+YbCg=" }, "electron-osx-sign": { - "version": "0.4.8", - "resolved": "https://registry.npmjs.org/electron-osx-sign/-/electron-osx-sign-0.4.8.tgz", - "integrity": "sha1-8Ln63e2eHlTsNfqJh3tcbDTHvEA=", + "version": "0.4.10", + "resolved": "https://registry.npmjs.org/electron-osx-sign/-/electron-osx-sign-0.4.10.tgz", + "integrity": "sha1-vk87ibKnWh3F8eckkIGrKSnKOiY=", "dev": true, "requires": { - "bluebird": "3.5.1", - "compare-version": "0.1.2", - "debug": "2.6.9", - "isbinaryfile": "3.0.2", - "minimist": "1.2.0", - "plist": "2.1.0" + "bluebird": "^3.5.0", + "compare-version": "^0.1.2", + "debug": "^2.6.8", + "isbinaryfile": "^3.0.2", + "minimist": "^1.2.0", + "plist": "^2.1.0" }, "dependencies": { "debug": { @@ -577,89 +637,75 @@ "requires": { "ms": "2.0.0" } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", - "dev": true } } }, "electron-packager": { - "version": "12.0.0", - "resolved": "https://registry.npmjs.org/electron-packager/-/electron-packager-12.0.0.tgz", - "integrity": "sha1-uC0k14ovIUA7v9FmpbFWmJTVzQw=", + "version": "12.1.2", + "resolved": "https://registry.npmjs.org/electron-packager/-/electron-packager-12.1.2.tgz", + "integrity": "sha512-7UiTNquZqhQm+L0Oqn7bR/7Ry/7zGO/PKwFpSNqHbWxydoN2aNahKyWjOPhcxHCAz+C1uu+tdyRe7wEN0BaJsA==", "dev": true, "requires": { - "asar": "0.14.2", - "debug": "3.1.0", - "electron-download": "4.1.0", - "electron-osx-sign": "0.4.8", - "extract-zip": "1.5.0", - "fs-extra": "5.0.0", - "galactus": "0.2.0", - "get-package-info": "1.0.0", - "nodeify": "1.0.1", - "parse-author": "2.0.0", - "pify": "3.0.0", - "plist": "2.1.0", - "rcedit": "1.0.0", - "resolve": "1.5.0", - "sanitize-filename": "1.6.1", - "semver": "5.5.0", - "yargs-parser": "9.0.2" + "asar": "^0.14.0", + "debug": "^3.0.0", + "electron-download": "^4.1.1", + "electron-osx-sign": "^0.4.1", + "extract-zip": "^1.0.3", + "fs-extra": "^5.0.0", + "galactus": "^0.2.1", + "get-package-info": "^1.0.0", + "nodeify": "^1.0.1", + "parse-author": "^2.0.0", + "pify": "^3.0.0", + "plist": "^2.0.0", + "rcedit": "^1.0.0", + "resolve": "^1.1.6", + "sanitize-filename": "^1.6.0", + "semver": "^5.3.0", + "yargs-parser": "^10.0.0" }, "dependencies": { - "camelcase": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", - "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=", - "dev": true - }, "debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.5.tgz", + "integrity": "sha512-D61LaDQPQkxJ5AUM2mbSJRbPkNs/TmdmOeLAi1hgDkpDfIfetSrjmWhccwtuResSwMbACjx/xXQofvM9CE/aeg==", "dev": true, "requires": { - "ms": "2.0.0" + "ms": "^2.1.1" } }, + "deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "dev": true + }, "electron-download": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/electron-download/-/electron-download-4.1.0.tgz", - "integrity": "sha1-v5MsdG8vh//MCdHdRy8v9rkYeEU=", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/electron-download/-/electron-download-4.1.1.tgz", + "integrity": "sha512-FjEWG9Jb/ppK/2zToP+U5dds114fM1ZOJqMAR4aXXL5CvyPE9fiqBK/9YcwC9poIFQTEJk/EM/zyRwziziRZrg==", "dev": true, "requires": { - "debug": "2.6.9", - "env-paths": "1.0.0", - "fs-extra": "2.1.2", - "minimist": "1.2.0", - "nugget": "2.0.1", - "path-exists": "3.0.0", - "rc": "1.1.6", - "semver": "5.5.0", - "sumchecker": "2.0.2" + "debug": "^3.0.0", + "env-paths": "^1.0.0", + "fs-extra": "^4.0.1", + "minimist": "^1.2.0", + "nugget": "^2.0.1", + "path-exists": "^3.0.0", + "rc": "^1.2.1", + "semver": "^5.4.1", + "sumchecker": "^2.0.2" }, "dependencies": { - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, "fs-extra": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-2.1.2.tgz", - "integrity": "sha1-BGxwFjzvmq1GsOSn+kZ/si1x3jU=", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-4.0.3.tgz", + "integrity": "sha512-q6rbdDd1o2mAnQreO7YADIxf/Whx4AHBiRf6d+/cVT8h44ss+lHgxf1FemcqDnQt9X3ct4McHr+JMGlYSsK7Cg==", "dev": true, "requires": { - "graceful-fs": "4.1.3", - "jsonfile": "2.2.3" + "graceful-fs": "^4.1.2", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" } } } @@ -670,63 +716,35 @@ "integrity": "sha512-66Pm4RYbjzdyeuqudYqhFiNBbCIuI9kgRqLPSHIlXHidW8NIQtVdkM1yeZ4lXwuhbTETv3EUGMNHAAw6hiundQ==", "dev": true, "requires": { - "graceful-fs": "4.1.3", - "jsonfile": "4.0.0", - "universalify": "0.1.1" + "graceful-fs": "^4.1.2", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + } + }, + "jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.6" }, "dependencies": { - "jsonfile": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", - "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", + "graceful-fs": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", + "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=", "dev": true, - "requires": { - "graceful-fs": "4.1.11" - }, - "dependencies": { - "graceful-fs": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", - "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=", - "dev": true, - "optional": true - } - } + "optional": true } } }, "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", "dev": true }, - "nugget": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/nugget/-/nugget-2.0.1.tgz", - "integrity": "sha1-IBCVpIfhrTYIGzQy+jytpPjQcbA=", - "dev": true, - "requires": { - "debug": "2.6.9", - "minimist": "1.2.0", - "pretty-bytes": "1.0.4", - "progress-stream": "1.2.0", - "request": "2.85.0", - "single-line-log": "1.1.2", - "throttleit": "0.0.2" - }, - "dependencies": { - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - } - } - }, "path-exists": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", @@ -739,41 +757,29 @@ "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", "dev": true }, - "rcedit": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/rcedit/-/rcedit-1.0.0.tgz", - "integrity": "sha512-W7DNa34x/3OgWyDHsI172AG/Lr/lZ+PkavFkHj0QhhkBRcV9QTmRJE1tDKrWkx8XHPSBsmZkNv9OKue6pncLFQ==", - "dev": true + "rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "dev": true, + "requires": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + } }, "semver": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz", - "integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA==", + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.1.tgz", + "integrity": "sha512-PqpAxfrEhlSUWge8dwIp4tZnQ25DIOthpiaHNIthsjEFQD6EvqUKUDM7L8O2rShkFccYo1VjJR0coWfNkCubRw==", "dev": true }, - "single-line-log": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/single-line-log/-/single-line-log-1.1.2.tgz", - "integrity": "sha1-wvg/Jzo+GhbtsJlWYdoO1e8DM2Q=", - "dev": true, - "requires": { - "string-width": "1.0.1" - } - }, - "throttleit": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/throttleit/-/throttleit-0.0.2.tgz", - "integrity": "sha1-z+34jmDADdlpe2H90qg0OptoDq8=", + "strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", "dev": true - }, - "yargs-parser": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-9.0.2.tgz", - "integrity": "sha1-nM9qQ0YP5O1Aqbto9I1DuKaMwHc=", - "dev": true, - "requires": { - "camelcase": "4.1.0" - } } } }, @@ -782,7 +788,7 @@ "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.1.0.tgz", "integrity": "sha1-6TUyWLqpEIll78QcsO+K3i88+wc=", "requires": { - "once": "1.3.3" + "once": "~1.3.0" } }, "entities": { @@ -802,58 +808,46 @@ "integrity": "sha1-5ntD8+gsluo6WE/+4Ln8MyXYAtk=", "dev": true, "requires": { - "is-arrayish": "0.2.1" + "is-arrayish": "^0.2.1" } }, - "es6-promise": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.4.tgz", - "integrity": "sha512-/NdNZVJg+uZgtm9eS3O6lrOLYmQag2DjdEXuPaHlZ6RuVqgqaVZfgYCepEIKsLqwdQArOPtC3XzRLqGGfT8KQQ==", - "dev": true - }, "extend": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.0.tgz", "integrity": "sha1-WkdDU7nzNT3dgXbf03uRyDpG8dQ=" }, "extract-zip": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-1.5.0.tgz", - "integrity": "sha1-ksz22B73Cp+kwXRxFMzvbYaIpsQ=", + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-1.6.7.tgz", + "integrity": "sha1-qEC0uK9kAyZMjbV/Txp0Mz74H+k=", "dev": true, "requires": { - "concat-stream": "1.5.0", - "debug": "0.7.4", - "mkdirp": "0.5.0", + "concat-stream": "1.6.2", + "debug": "2.6.9", + "mkdirp": "0.5.1", "yauzl": "2.4.1" }, "dependencies": { - "minimist": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", - "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", - "dev": true - }, - "mkdirp": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.0.tgz", - "integrity": "sha1-HXMHam35hs2TROFecfzAWkyavxI=", + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "dev": true, "requires": { - "minimist": "0.0.8" + "ms": "2.0.0" } } } }, "extsprintf": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.0.2.tgz", - "integrity": "sha1-4QgOBljjALBilJkMxw4VAiNf1VA=" + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" }, "fast-deep-equal": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.0.0.tgz", - "integrity": "sha1-liVqO8l1WV6zbYLpkp0GDYk0Of8=" + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz", + "integrity": "sha1-wFNHeBfIa1HaqFPIHgWbcz0CNhQ=" }, "fast-json-stable-stringify": { "version": "2.0.0", @@ -866,7 +860,7 @@ "integrity": "sha1-i1vL2ewyfFBBv5qwI/1nUPEXfmU=", "dev": true, "requires": { - "pend": "1.2.0" + "pend": "~1.2.0" } }, "find-up": { @@ -875,8 +869,8 @@ "integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=", "dev": true, "requires": { - "path-exists": "2.1.0", - "pinkie-promise": "2.0.1" + "path-exists": "^2.0.0", + "pinkie-promise": "^2.0.0" }, "dependencies": { "path-exists": { @@ -885,28 +879,28 @@ "integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=", "dev": true, "requires": { - "pinkie-promise": "2.0.1" + "pinkie-promise": "^2.0.0" } } } }, "flora-colossus": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/flora-colossus/-/flora-colossus-0.0.2.tgz", - "integrity": "sha1-fRvimh8X+k8isb1hSC+Gw04HuQE=", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/flora-colossus/-/flora-colossus-1.0.0.tgz", + "integrity": "sha1-VHKcNh7ezuAU3UQWeeGjfB13OkU=", "dev": true, "requires": { - "debug": "3.1.0", - "fs-extra": "4.0.3" + "debug": "^3.1.0", + "fs-extra": "^4.0.0" }, "dependencies": { "debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.5.tgz", + "integrity": "sha512-D61LaDQPQkxJ5AUM2mbSJRbPkNs/TmdmOeLAi1hgDkpDfIfetSrjmWhccwtuResSwMbACjx/xXQofvM9CE/aeg==", "dev": true, "requires": { - "ms": "2.0.0" + "ms": "^2.1.1" } }, "fs-extra": { @@ -915,9 +909,9 @@ "integrity": "sha512-q6rbdDd1o2mAnQreO7YADIxf/Whx4AHBiRf6d+/cVT8h44ss+lHgxf1FemcqDnQt9X3ct4McHr+JMGlYSsK7Cg==", "dev": true, "requires": { - "graceful-fs": "4.1.3", - "jsonfile": "4.0.0", - "universalify": "0.1.1" + "graceful-fs": "^4.1.2", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" } }, "jsonfile": { @@ -926,7 +920,7 @@ "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", "dev": true, "requires": { - "graceful-fs": "4.1.11" + "graceful-fs": "^4.1.6" }, "dependencies": { "graceful-fs": { @@ -939,9 +933,9 @@ } }, "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", "dev": true } } @@ -956,9 +950,9 @@ "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.2.tgz", "integrity": "sha1-SXBJi+YEwgwAXU9cI67NIda0kJk=", "requires": { - "asynckit": "0.4.0", + "asynckit": "^0.4.0", "combined-stream": "1.0.6", - "mime-types": "2.1.18" + "mime-types": "^2.1.12" }, "dependencies": { "combined-stream": { @@ -966,19 +960,37 @@ "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.6.tgz", "integrity": "sha1-cj599ugBrFYTETp+RFqbactjKBg=", "requires": { - "delayed-stream": "1.0.0" + "delayed-stream": "~1.0.0" } } } }, "fs-extra": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-1.0.0.tgz", - "integrity": "sha1-zTzl9+fLYUWIP8rjGR6Yd/hYeVA=", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-6.0.1.tgz", + "integrity": "sha512-GnyIkKhhzXZUWFCaJzvyDLEEgDkPfb4/TPvJCJVuS8MWZgoSsErf++QpiAlDnKFcqhRlm+tIOcencCjyJE6ZCA==", "requires": { - "graceful-fs": "4.1.3", - "jsonfile": "2.2.3", - "klaw": "1.3.1" + "graceful-fs": "^4.1.2", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "dependencies": { + "jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", + "requires": { + "graceful-fs": "^4.1.6" + }, + "dependencies": { + "graceful-fs": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", + "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=", + "optional": true + } + } + } } }, "fs.realpath": { @@ -988,23 +1000,23 @@ "dev": true }, "galactus": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/galactus/-/galactus-0.2.0.tgz", - "integrity": "sha1-w9Y7pVAkZv5A6mfMaJCFs90kqPw=", + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/galactus/-/galactus-0.2.1.tgz", + "integrity": "sha1-y+0tIKQMH1Z5o1kI4rlBVzPnjbk=", "dev": true, "requires": { - "debug": "3.1.0", - "flora-colossus": "0.0.2", - "fs-extra": "4.0.3" + "debug": "^3.1.0", + "flora-colossus": "^1.0.0", + "fs-extra": "^4.0.0" }, "dependencies": { "debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.5.tgz", + "integrity": "sha512-D61LaDQPQkxJ5AUM2mbSJRbPkNs/TmdmOeLAi1hgDkpDfIfetSrjmWhccwtuResSwMbACjx/xXQofvM9CE/aeg==", "dev": true, "requires": { - "ms": "2.0.0" + "ms": "^2.1.1" } }, "fs-extra": { @@ -1013,9 +1025,9 @@ "integrity": "sha512-q6rbdDd1o2mAnQreO7YADIxf/Whx4AHBiRf6d+/cVT8h44ss+lHgxf1FemcqDnQt9X3ct4McHr+JMGlYSsK7Cg==", "dev": true, "requires": { - "graceful-fs": "4.1.3", - "jsonfile": "4.0.0", - "universalify": "0.1.1" + "graceful-fs": "^4.1.2", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" } }, "jsonfile": { @@ -1024,7 +1036,7 @@ "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", "dev": true, "requires": { - "graceful-fs": "4.1.11" + "graceful-fs": "^4.1.6" }, "dependencies": { "graceful-fs": { @@ -1037,9 +1049,9 @@ } }, "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", "dev": true } } @@ -1050,10 +1062,10 @@ "integrity": "sha1-ZDJ5ZWPigRPNlHTbvQAFKYWkmZw=", "dev": true, "requires": { - "bluebird": "3.5.1", - "debug": "2.6.9", - "lodash.get": "4.4.2", - "read-pkg-up": "2.0.0" + "bluebird": "^3.1.1", + "debug": "^2.2.0", + "lodash.get": "^4.0.0", + "read-pkg-up": "^2.0.0" }, "dependencies": { "debug": { @@ -1071,7 +1083,7 @@ "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", "dev": true, "requires": { - "locate-path": "2.0.0" + "locate-path": "^2.0.0" } }, "load-json-file": { @@ -1080,25 +1092,19 @@ "integrity": "sha1-eUfkIUmvgNaWy/eXvKq8/h/inKg=", "dev": true, "requires": { - "graceful-fs": "4.1.3", - "parse-json": "2.2.0", - "pify": "2.3.0", - "strip-bom": "3.0.0" + "graceful-fs": "^4.1.2", + "parse-json": "^2.2.0", + "pify": "^2.0.0", + "strip-bom": "^3.0.0" } }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", - "dev": true - }, "path-type": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-2.0.0.tgz", "integrity": "sha1-8BLMuEFbcJb8LaoQVMPXI4lZTHM=", "dev": true, "requires": { - "pify": "2.3.0" + "pify": "^2.0.0" } }, "read-pkg": { @@ -1107,9 +1113,9 @@ "integrity": "sha1-jvHAYjxqbbDcZxPEv6xGMysjaPg=", "dev": true, "requires": { - "load-json-file": "2.0.0", - "normalize-package-data": "2.3.5", - "path-type": "2.0.0" + "load-json-file": "^2.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^2.0.0" } }, "read-pkg-up": { @@ -1118,8 +1124,8 @@ "integrity": "sha1-a3KoBImE4MQeeVEP1en6mbO1Sb4=", "dev": true, "requires": { - "find-up": "2.1.0", - "read-pkg": "2.0.0" + "find-up": "^2.0.0", + "read-pkg": "^2.0.0" } }, "strip-bom": { @@ -1136,18 +1142,26 @@ "integrity": "sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4=", "dev": true }, + "getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", + "requires": { + "assert-plus": "^1.0.0" + } + }, "glob": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", "dev": true, "requires": { - "fs.realpath": "1.0.0", - "inflight": "1.0.4", - "inherits": "2.0.1", - "minimatch": "3.0.4", - "once": "1.3.3", - "path-is-absolute": "1.0.0" + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" }, "dependencies": { "balanced-match": { @@ -1162,7 +1176,7 @@ "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "dev": true, "requires": { - "balanced-match": "1.0.0", + "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, @@ -1172,7 +1186,7 @@ "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", "dev": true, "requires": { - "brace-expansion": "1.1.11" + "brace-expansion": "^1.1.7" } } } @@ -1182,12 +1196,6 @@ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.3.tgz", "integrity": "sha1-kgM84RETxB4mKNYf36QLwQ3AFVw=" }, - "graceful-readlink": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/graceful-readlink/-/graceful-readlink-1.0.1.tgz", - "integrity": "sha1-TK+tdrxi8C+gObL5Tpo906ORpyU=", - "dev": true - }, "growly": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/growly/-/growly-1.3.0.tgz", @@ -1199,36 +1207,14 @@ "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=" }, "har-validator": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.0.3.tgz", - "integrity": "sha1-ukAsJmGU8VlW7xXg/PJCmT9qff0=", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.0.tgz", + "integrity": "sha512-+qnmNjI4OfH2ipQ9VQOw23bBd/ibtfbVdK2fYbY4acTDqKTW/YDp9McimZdDbG8iV9fZizUqQMD5xvriB146TA==", "requires": { - "ajv": "5.5.2", - "har-schema": "2.0.0" + "ajv": "^5.3.0", + "har-schema": "^2.0.0" } }, - "hawk": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/hawk/-/hawk-6.0.2.tgz", - "integrity": "sha512-miowhl2+U7Qle4vdLqDdPt9m09K6yZhkLDTWGoUiUzrQCn+mHHSmfJgAyGaLRZbPmTqfFFjRV1QWCW0VWUJBbQ==", - "requires": { - "boom": "4.3.1", - "cryptiles": "3.1.2", - "hoek": "4.2.1", - "sntp": "2.1.0" - } - }, - "hoek": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/hoek/-/hoek-4.2.1.tgz", - "integrity": "sha512-QLg82fGkfnJ/4iy1xZ81/9SIJiq1NGFUMGs6ParyjBZr6jW2Ufj/snDqTHixNlHdPNwN2RLVD0Pi3igeK9+JfA==" - }, - "home-path": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/home-path/-/home-path-1.0.5.tgz", - "integrity": "sha1-eIspgVsS1Tus9XVkhHbm+QQdEz8=", - "dev": true - }, "hosted-git-info": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.1.4.tgz", @@ -1236,30 +1222,56 @@ "dev": true }, "htmlparser2": { - "version": "3.8.3", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.8.3.tgz", - "integrity": "sha1-mWwosZFRaovoZQGn15dX5ccMEGg=", + "version": "3.9.2", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.9.2.tgz", + "integrity": "sha1-G9+HrMoPP55T+k/M6w9LTLsAszg=", "requires": { - "domelementtype": "1.3.0", - "domhandler": "2.3.0", - "domutils": "1.5.1", - "entities": "1.0.0", - "readable-stream": "1.1.14" + "domelementtype": "^1.3.0", + "domhandler": "^2.3.0", + "domutils": "^1.5.1", + "entities": "^1.1.1", + "inherits": "^2.0.1", + "readable-stream": "^2.0.2" }, "dependencies": { - "domutils": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.5.1.tgz", - "integrity": "sha1-3NhIiib1Y9YQeeSMn3t+Mjc2gs8=", + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + }, + "process-nextick-args": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", + "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==" + }, + "readable-stream": { + "version": "2.3.6", + "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", "requires": { - "dom-serializer": "0.1.0", - "domelementtype": "1.3.0" + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + }, + "dependencies": { + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + } } }, - "entities": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-1.0.0.tgz", - "integrity": "sha1-sph6o4ITR/zeZCsk/fyeT7cSvyY=" + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } } } }, @@ -1268,16 +1280,9 @@ "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", "requires": { - "assert-plus": "1.0.0", - "jsprim": "1.2.2", - "sshpk": "1.7.4" - }, - "dependencies": { - "assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" - } + "assert-plus": "^1.0.0", + "jsprim": "^1.2.2", + "sshpk": "^1.7.0" } }, "indent-string": { @@ -1286,7 +1291,7 @@ "integrity": "sha1-ji1INIdCEhtKghi3oTfppSBJ3IA=", "dev": true, "requires": { - "repeating": "2.0.1" + "repeating": "^2.0.0" }, "dependencies": { "repeating": { @@ -1295,7 +1300,7 @@ "integrity": "sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo=", "dev": true, "requires": { - "is-finite": "1.0.1" + "is-finite": "^1.0.0" } } } @@ -1306,8 +1311,8 @@ "integrity": "sha1-bLtFIevVHODsCpNr/XZX736bFyo=", "dev": true, "requires": { - "once": "1.3.3", - "wrappy": "1.0.1" + "once": "^1.3.0", + "wrappy": "1" } }, "inherits": { @@ -1338,7 +1343,7 @@ "integrity": "sha1-VAVy0096wxGfj3bDDLwbHgN6/74=", "dev": true, "requires": { - "builtin-modules": "1.1.1" + "builtin-modules": "^1.0.0" } }, "is-finite": { @@ -1347,7 +1352,7 @@ "integrity": "sha1-ZDhgPq6+J5OUj/SkJi7I2z1iWXs=", "dev": true, "requires": { - "number-is-nan": "1.0.0" + "number-is-nan": "^1.0.0" } }, "is-fullwidth-code-point": { @@ -1355,7 +1360,7 @@ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", "requires": { - "number-is-nan": "1.0.0" + "number-is-nan": "^1.0.0" } }, "is-promise": { @@ -1378,13 +1383,17 @@ "isarray": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", + "dev": true }, "isbinaryfile": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-3.0.2.tgz", - "integrity": "sha1-Sj6XTsDLqQBNP8bN5yCeppNopiE=", - "dev": true + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-3.0.3.tgz", + "integrity": "sha512-8cJBL5tTd2OS0dM4jz07wQd5g0dCCqIhUxPIGtZfa5L6hWlvV5MHTITy/DBAsF+Oe2LS1X3krBUhNwaGUWpWxw==", + "dev": true, + "requires": { + "buffer-alloc": "^1.2.0" + } }, "isexe": { "version": "2.0.0", @@ -1396,25 +1405,16 @@ "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" }, - "jodid25519": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/jodid25519/-/jodid25519-1.0.2.tgz", - "integrity": "sha1-BtSRIlUJNBlHfUJWM2BuDpB4KWc=", - "optional": true, - "requires": { - "jsbn": "0.1.0" - } - }, "jsbn": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.0.tgz", - "integrity": "sha1-ZQmH2g3XT06/WhE3eiqi0nPpff0=", + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", "optional": true }, "json-schema": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.2.tgz", - "integrity": "sha1-UDVPGfYDkXxpX3C4Wvp3w7DyNQY=" + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", + "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=" }, "json-schema-traverse": { "version": "0.3.1", @@ -1429,30 +1429,34 @@ "jsonfile": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-2.2.3.tgz", - "integrity": "sha1-4lK5mmr5AdPsQfMyWJyQUJp7xgU=" + "integrity": "sha1-4lK5mmr5AdPsQfMyWJyQUJp7xgU=", + "dev": true }, "jsprim": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.2.2.tgz", - "integrity": "sha1-8gyQaskqvVjjt5rIvHCkiDJRLaE=", + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", + "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", "requires": { - "extsprintf": "1.0.2", - "json-schema": "0.2.2", - "verror": "1.3.6" + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.2.3", + "verror": "1.10.0" } }, "klaw": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/klaw/-/klaw-1.3.1.tgz", "integrity": "sha1-QIhDO0azsbolnXh4XY6W9zugJDk=", + "dev": true, "requires": { - "graceful-fs": "4.1.11" + "graceful-fs": "^4.1.9" }, "dependencies": { "graceful-fs": { "version": "4.1.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=", + "dev": true, "optional": true } } @@ -1462,7 +1466,7 @@ "resolved": "https://registry.npmjs.org/lcid/-/lcid-1.0.0.tgz", "integrity": "sha1-MIrMr6C8SDo4Z7S28rlQYlHRuDU=", "requires": { - "invert-kv": "1.0.0" + "invert-kv": "^1.0.0" } }, "load-json-file": { @@ -1471,11 +1475,11 @@ "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=", "dev": true, "requires": { - "graceful-fs": "4.1.3", - "parse-json": "2.2.0", - "pify": "2.3.0", - "pinkie-promise": "2.0.1", - "strip-bom": "2.0.0" + "graceful-fs": "^4.1.2", + "parse-json": "^2.2.0", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0", + "strip-bom": "^2.0.0" } }, "locate-path": { @@ -1484,8 +1488,8 @@ "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", "dev": true, "requires": { - "p-locate": "2.0.0", - "path-exists": "3.0.0" + "p-locate": "^2.0.0", + "path-exists": "^3.0.0" }, "dependencies": { "path-exists": { @@ -1496,10 +1500,35 @@ } } }, - "lodash": { - "version": "3.10.1", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-3.10.1.tgz", - "integrity": "sha1-W/Rejkm6QYnhfUgnid/RW9FAt7Y=" + "lodash.assignin": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.assignin/-/lodash.assignin-4.2.0.tgz", + "integrity": "sha1-uo31+4QesKPoBEIysOJjqNxqKKI=" + }, + "lodash.bind": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/lodash.bind/-/lodash.bind-4.2.1.tgz", + "integrity": "sha1-euMBfpOWIqwxt9fX3LGzTbFpDTU=" + }, + "lodash.defaults": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha1-0JF4cW/+pN3p5ft7N/bwgCJ0WAw=" + }, + "lodash.filter": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.filter/-/lodash.filter-4.6.0.tgz", + "integrity": "sha1-ZosdSYFgOuHMWm+nYBQ+SAtMSs4=" + }, + "lodash.flatten": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", + "integrity": "sha1-8xwiIlqWMtK7+OSt2+8kCqdlph8=" + }, + "lodash.foreach": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.foreach/-/lodash.foreach-4.5.0.tgz", + "integrity": "sha1-Gmo16s5AEoDH8G3d7DUWWrJ+PlM=" }, "lodash.get": { "version": "4.4.2", @@ -1507,14 +1536,44 @@ "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=", "dev": true }, + "lodash.map": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.map/-/lodash.map-4.6.0.tgz", + "integrity": "sha1-dx7Hg540c9nEzeKLGTlMNWL09tM=" + }, + "lodash.merge": { + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.1.tgz", + "integrity": "sha512-AOYza4+Hf5z1/0Hztxpm2/xiPZgi/cjMqdnKTUWTBSKchJlxXXuUSxCCl8rJlf4g6yww/j6mA8nC8Hw/EZWxKQ==" + }, + "lodash.pick": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.pick/-/lodash.pick-4.4.0.tgz", + "integrity": "sha1-UvBWEP/53tQiYRRB7R/BI6AwAbM=" + }, + "lodash.reduce": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.reduce/-/lodash.reduce-4.6.0.tgz", + "integrity": "sha1-8atrg5KZrUj3hKu/R2WW8DuRTTs=" + }, + "lodash.reject": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.reject/-/lodash.reject-4.6.0.tgz", + "integrity": "sha1-gNZJLcFHCGS79YNTO2UfQqn1JBU=" + }, + "lodash.some": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.some/-/lodash.some-4.6.0.tgz", + "integrity": "sha1-G7nzFO9ri63tE7VJFpsqlF62jk0=" + }, "loud-rejection": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/loud-rejection/-/loud-rejection-1.3.0.tgz", "integrity": "sha1-8omjkvF9K6rPGU0KZzAEOUQzsRU=", "dev": true, "requires": { - "array-find-index": "1.0.1", - "signal-exit": "2.1.2" + "array-find-index": "^1.0.0", + "signal-exit": "^2.1.2" } }, "map-obj": { @@ -1529,29 +1588,38 @@ "integrity": "sha1-cstmi0JSKCkKu/qFaJJYcwioAfs=", "dev": true, "requires": { - "camelcase-keys": "2.1.0", - "decamelize": "1.2.0", - "loud-rejection": "1.3.0", - "map-obj": "1.0.1", - "minimist": "1.2.0", - "normalize-package-data": "2.3.5", - "object-assign": "4.0.1", - "read-pkg-up": "1.0.1", - "redent": "1.0.0", - "trim-newlines": "1.0.0" + "camelcase-keys": "^2.0.0", + "decamelize": "^1.1.2", + "loud-rejection": "^1.0.0", + "map-obj": "^1.0.1", + "minimist": "^1.1.3", + "normalize-package-data": "^2.3.4", + "object-assign": "^4.0.1", + "read-pkg-up": "^1.0.1", + "redent": "^1.0.0", + "trim-newlines": "^1.0.0" } }, "mime-db": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.33.0.tgz", - "integrity": "sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ==" + "version": "1.36.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.36.0.tgz", + "integrity": "sha512-L+xvyD9MkoYMXb1jAmzI/lWYAxAMCPvIBSWur0PZ5nOf5euahRLVqH//FKW9mWp2lkqUgYiXPgkzfMUFi4zVDw==" }, "mime-types": { - "version": "2.1.18", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.18.tgz", - "integrity": "sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ==", + "version": "2.1.20", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.20.tgz", + "integrity": "sha512-HrkrPaP9vGuWbLK1B1FfgAkbqNjIuy4eHlIYnFi7kamZyLLrGlo2mpcx0bBmNpKqBtYtAfGbodDddIgddSJC2A==", "requires": { - "mime-db": "1.33.0" + "mime-db": "~1.36.0" + } + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" } }, "minimist": { @@ -1589,220 +1657,21 @@ "requires": { "decompress-zip": "0.3.0", "fs-extra": "0.26.7", - "request": "2.83.0" + "request": "^2.79.0" }, "dependencies": { - "assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", - "dev": true - }, - "aws-sign2": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", - "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=", - "dev": true - }, - "aws4": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.6.0.tgz", - "integrity": "sha1-g+9cqGCysy5KDe7e6MdxudtXRx4=", - "dev": true - }, - "boom": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/boom/-/boom-4.3.1.tgz", - "integrity": "sha1-T4owBctKfjiJ90kDD9JbluAdLjE=", - "dev": true, - "requires": { - "hoek": "4.2.0" - } - }, - "caseless": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", - "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=", - "dev": true - }, - "cryptiles": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-3.1.2.tgz", - "integrity": "sha1-qJ+7Ig9c4l7FboxKqKT9e1sNKf4=", - "dev": true, - "requires": { - "boom": "5.2.0" - }, - "dependencies": { - "boom": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/boom/-/boom-5.2.0.tgz", - "integrity": "sha512-Z5BTk6ZRe4tXXQlkqftmsAUANpXmuwlsF5Oov8ThoMbQRzdGTA1ngYRW160GexgOgjsFOKJz0LYhoNi+2AMBUw==", - "dev": true, - "requires": { - "hoek": "4.2.0" - } - } - } - }, - "extend": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.1.tgz", - "integrity": "sha1-p1Xqe8Gt/MWjHOfnYtuq3F5jZEQ=", - "dev": true - }, - "form-data": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.1.tgz", - "integrity": "sha1-b7lPvXGIUwbXPRXMSX/kzE7NRL8=", - "dev": true, - "requires": { - "asynckit": "0.4.0", - "combined-stream": "1.0.5", - "mime-types": "2.1.17" - } - }, "fs-extra": { "version": "0.26.7", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-0.26.7.tgz", "integrity": "sha1-muH92UiXeY7at20JGM9C0MMYT6k=", "dev": true, "requires": { - "graceful-fs": "4.1.3", - "jsonfile": "2.2.3", - "klaw": "1.3.1", - "path-is-absolute": "1.0.0", - "rimraf": "2.6.2" + "graceful-fs": "^4.1.2", + "jsonfile": "^2.1.0", + "klaw": "^1.0.0", + "path-is-absolute": "^1.0.0", + "rimraf": "^2.2.8" } - }, - "har-validator": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.0.3.tgz", - "integrity": "sha1-ukAsJmGU8VlW7xXg/PJCmT9qff0=", - "dev": true, - "requires": { - "ajv": "5.5.2", - "har-schema": "2.0.0" - } - }, - "hawk": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/hawk/-/hawk-6.0.2.tgz", - "integrity": "sha512-miowhl2+U7Qle4vdLqDdPt9m09K6yZhkLDTWGoUiUzrQCn+mHHSmfJgAyGaLRZbPmTqfFFjRV1QWCW0VWUJBbQ==", - "dev": true, - "requires": { - "boom": "4.3.1", - "cryptiles": "3.1.2", - "hoek": "4.2.0", - "sntp": "2.1.0" - } - }, - "hoek": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/hoek/-/hoek-4.2.0.tgz", - "integrity": "sha512-v0XCLxICi9nPfYrS9RL8HbYnXi9obYAeLbSP00BmnZwCK9+Ih9WOjoZ8YoHCoav2csqn4FOz4Orldsy2dmDwmQ==", - "dev": true - }, - "http-signature": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", - "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", - "dev": true, - "requires": { - "assert-plus": "1.0.0", - "jsprim": "1.2.2", - "sshpk": "1.7.4" - } - }, - "mime-db": { - "version": "1.30.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.30.0.tgz", - "integrity": "sha1-dMZD2i3Z1qRTmZY0ZbJtXKfXHwE=", - "dev": true - }, - "mime-types": { - "version": "2.1.17", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.17.tgz", - "integrity": "sha1-Cdejk/A+mVp5+K+Fe3Cp4KsWVXo=", - "dev": true, - "requires": { - "mime-db": "1.30.0" - } - }, - "oauth-sign": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz", - "integrity": "sha1-Rqarfwrq2N6unsBWV4C31O/rnUM=", - "dev": true - }, - "qs": { - "version": "6.5.1", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.1.tgz", - "integrity": "sha512-eRzhrN1WSINYCDCbrz796z37LOe3m5tmW7RQf6oBntukAG1nmovJvhnwHHRMAfeoItc1m2Hk02WER2aQ/iqs+A==", - "dev": true - }, - "request": { - "version": "2.83.0", - "resolved": "https://registry.npmjs.org/request/-/request-2.83.0.tgz", - "integrity": "sha512-lR3gD69osqm6EYLk9wB/G1W/laGWjzH90t1vEa2xuxHD5KUrSzp9pUSfTm+YC5Nxt2T8nMPEvKlhbQayU7bgFw==", - "dev": true, - "requires": { - "aws-sign2": "0.7.0", - "aws4": "1.6.0", - "caseless": "0.12.0", - "combined-stream": "1.0.5", - "extend": "3.0.1", - "forever-agent": "0.6.1", - "form-data": "2.3.1", - "har-validator": "5.0.3", - "hawk": "6.0.2", - "http-signature": "1.2.0", - "is-typedarray": "1.0.0", - "isstream": "0.1.2", - "json-stringify-safe": "5.0.1", - "mime-types": "2.1.17", - "oauth-sign": "0.8.2", - "performance-now": "2.1.0", - "qs": "6.5.1", - "safe-buffer": "5.1.1", - "stringstream": "0.0.5", - "tough-cookie": "2.3.3", - "tunnel-agent": "0.6.0", - "uuid": "3.2.1" - } - }, - "sntp": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/sntp/-/sntp-2.1.0.tgz", - "integrity": "sha512-FL1b58BDrqS3A11lJ0zEdnJ3UOKqVxawAkF3k7F0CVN7VQ34aZrV+G8BZ1WC9ZL7NyrwsW0oviwsWDgRuVYtJg==", - "dev": true, - "requires": { - "hoek": "4.2.0" - } - }, - "tough-cookie": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.3.tgz", - "integrity": "sha1-C2GKVWW23qkL80JdBNVe3EdadWE=", - "dev": true, - "requires": { - "punycode": "1.4.1" - } - }, - "tunnel-agent": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", - "dev": true, - "requires": { - "safe-buffer": "5.1.1" - } - }, - "uuid": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.2.1.tgz", - "integrity": "sha512-jZnMwlb9Iku/O3smGWvZhauCf6cvvpKi4BKRiliS3cxnI+Gz9j5MEpTz2UFuXiKPJocb7gnsLHwiS05ige5BEA==", - "dev": true } } }, @@ -1817,10 +1686,10 @@ "resolved": "https://registry.npmjs.org/node-notifier/-/node-notifier-5.2.1.tgz", "integrity": "sha512-MIBs+AAd6dJ2SklbbE8RUDRlIVhU8MaNLh1A9SUZDUHPiZkWLFde6UNwG41yQHZEToHgJMXqyVZ9UcS/ReOVTg==", "requires": { - "growly": "1.3.0", - "semver": "5.5.0", - "shellwords": "0.1.1", - "which": "1.3.0" + "growly": "^1.3.0", + "semver": "^5.4.1", + "shellwords": "^0.1.1", + "which": "^1.3.0" }, "dependencies": { "semver": { @@ -1836,8 +1705,8 @@ "integrity": "sha1-ZKtpp7268DzhB7TwM1yHwLnpGx0=", "dev": true, "requires": { - "is-promise": "1.0.1", - "promise": "1.3.0" + "is-promise": "~1.0.0", + "promise": "~1.3.0" } }, "nopt": { @@ -1846,7 +1715,7 @@ "integrity": "sha1-xkZdvwirzU2zWTF/eaxopkayj/k=", "dev": true, "requires": { - "abbrev": "1.1.1" + "abbrev": "1" } }, "normalize-package-data": { @@ -1855,10 +1724,10 @@ "integrity": "sha1-jZJPFClg4Xd+f/4XBUNjHMfLAt8=", "dev": true, "requires": { - "hosted-git-info": "2.1.4", - "is-builtin-module": "1.0.0", - "semver": "5.1.0", - "validate-npm-package-license": "3.0.1" + "hosted-git-info": "^2.1.4", + "is-builtin-module": "^1.0.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" } }, "nth-check": { @@ -1866,7 +1735,7 @@ "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-1.0.1.tgz", "integrity": "sha1-mSms32KPwsQQmN6rgqxYDPFJquQ=", "requires": { - "boolbase": "1.0.0" + "boolbase": "~1.0.0" } }, "nugget": { @@ -1875,12 +1744,12 @@ "integrity": "sha1-IBCVpIfhrTYIGzQy+jytpPjQcbA=", "dev": true, "requires": { - "debug": "2.6.9", - "minimist": "1.2.0", - "pretty-bytes": "1.0.4", - "progress-stream": "1.2.0", - "request": "2.85.0", - "single-line-log": "1.1.2", + "debug": "^2.1.3", + "minimist": "^1.1.0", + "pretty-bytes": "^1.0.2", + "progress-stream": "^1.1.0", + "request": "^2.45.0", + "single-line-log": "^1.1.2", "throttleit": "0.0.2" }, "dependencies": { @@ -1907,9 +1776,9 @@ "integrity": "sha1-wCD1KcUoKt/dIz2R1LGBw9aG3Es=" }, "oauth-sign": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz", - "integrity": "sha1-Rqarfwrq2N6unsBWV4C31O/rnUM=" + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", + "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==" }, "object-assign": { "version": "4.0.1", @@ -1928,7 +1797,7 @@ "resolved": "https://registry.npmjs.org/once/-/once-1.3.3.tgz", "integrity": "sha1-suJhVXzkwxTsgwTz+oJmPkKXyiA=", "requires": { - "wrappy": "1.0.1" + "wrappy": "1" } }, "os-homedir": { @@ -1941,7 +1810,7 @@ "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-1.4.0.tgz", "integrity": "sha1-IPnxeuKe00XoveWDsT0gCYA8FNk=", "requires": { - "lcid": "1.0.0" + "lcid": "^1.0.0" } }, "os-tmpdir": { @@ -1951,12 +1820,12 @@ "dev": true }, "p-limit": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.2.0.tgz", - "integrity": "sha512-Y/OtIaXtUPr4/YpMv1pCL5L5ed0rumAaAeBSj12F+bSlMdys7i8oQF/GUJmfpTS/QoaRrS/k6pma29haJpsMng==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", + "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", "dev": true, "requires": { - "p-try": "1.0.0" + "p-try": "^1.0.0" } }, "p-locate": { @@ -1965,7 +1834,7 @@ "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", "dev": true, "requires": { - "p-limit": "1.2.0" + "p-limit": "^1.1.0" } }, "p-try": { @@ -1980,7 +1849,7 @@ "integrity": "sha1-00YL8d3Q367tQtp1QkLmX7aEqB8=", "dev": true, "requires": { - "author-regex": "1.0.0" + "author-regex": "^1.0.0" } }, "parse-json": { @@ -1989,17 +1858,14 @@ "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=", "dev": true, "requires": { - "error-ex": "1.3.0" + "error-ex": "^1.2.0" } }, "path-exists": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz", - "integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=", - "dev": true, - "requires": { - "pinkie-promise": "2.0.1" - } + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", + "dev": true }, "path-is-absolute": { "version": "1.0.0", @@ -2008,9 +1874,9 @@ "dev": true }, "path-parse": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.5.tgz", - "integrity": "sha1-PBrfhx6pzWyUMbbqK9dKD/BVxME=", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", + "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==", "dev": true }, "path-type": { @@ -2019,9 +1885,9 @@ "integrity": "sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE=", "dev": true, "requires": { - "graceful-fs": "4.1.3", - "pify": "2.3.0", - "pinkie-promise": "2.0.1" + "graceful-fs": "^4.1.2", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0" } }, "pend": { @@ -2053,7 +1919,7 @@ "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", "dev": true, "requires": { - "pinkie": "2.0.4" + "pinkie": "^2.0.0" } }, "plist": { @@ -2064,7 +1930,7 @@ "requires": { "base64-js": "1.2.0", "xmlbuilder": "8.2.2", - "xmldom": "0.1.27" + "xmldom": "0.1.x" } }, "pretty-bytes": { @@ -2073,8 +1939,8 @@ "integrity": "sha1-CiLoIQYJrTVUL4yNXSFZr/B1HIQ=", "dev": true, "requires": { - "get-stdin": "4.0.1", - "meow": "3.7.0" + "get-stdin": "^4.0.1", + "meow": "^3.1.0" } }, "process-nextick-args": { @@ -2088,8 +1954,8 @@ "integrity": "sha1-LNPP6jO6OonJwSHsM0er6asSX3c=", "dev": true, "requires": { - "speedometer": "0.1.4", - "through2": "0.2.3" + "speedometer": "~0.1.2", + "through2": "~0.2.3" } }, "promise": { @@ -2098,16 +1964,21 @@ "integrity": "sha1-5cyaTIJ45GZP/twBx9qEhCsEAXU=", "dev": true, "requires": { - "is-promise": "1.0.1" + "is-promise": "~1" } }, + "psl": { + "version": "1.1.29", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.1.29.tgz", + "integrity": "sha512-AeUmQ0oLN02flVHXWh9sSJF7mcdFq0ppid/JkErufc3hGIV/AMa8Fo9VgDo/cT2jFdOWoFvHp90qqBH54W+gjQ==" + }, "pump": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/pump/-/pump-1.0.1.tgz", "integrity": "sha1-8fFAn7m9EIW721drQ7hOxLXq3Bo=", "requires": { - "end-of-stream": "1.1.0", - "once": "1.3.3" + "end-of-stream": "^1.1.0", + "once": "^1.3.1" } }, "punycode": { @@ -2122,31 +1993,37 @@ "dev": true }, "qs": { - "version": "6.5.1", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.1.tgz", - "integrity": "sha512-eRzhrN1WSINYCDCbrz796z37LOe3m5tmW7RQf6oBntukAG1nmovJvhnwHHRMAfeoItc1m2Hk02WER2aQ/iqs+A==" + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", + "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" }, "rc": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/rc/-/rc-1.1.6.tgz", - "integrity": "sha1-Q2UbdrauU7XIAvEVH6P8OwWZack=", + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", "dev": true, "requires": { - "deep-extend": "0.4.1", - "ini": "1.3.4", - "minimist": "1.2.0", - "strip-json-comments": "1.0.4" + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" } }, + "rcedit": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/rcedit/-/rcedit-1.1.0.tgz", + "integrity": "sha512-JkXJ0IrUcdupLoIx6gE4YcFaMVSGtu7kQf4NJoDJUnfBZGuATmJ2Yal2v55KTltp+WV8dGr7A0RtOzx6jmtM6Q==", + "dev": true + }, "read-pkg": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", "integrity": "sha1-9f+qXs0pyzHAR0vKfXVra7KePyg=", "dev": true, "requires": { - "load-json-file": "1.1.0", - "normalize-package-data": "2.3.5", - "path-type": "1.1.0" + "load-json-file": "^1.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^1.0.0" } }, "read-pkg-up": { @@ -2155,19 +2032,20 @@ "integrity": "sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI=", "dev": true, "requires": { - "find-up": "1.1.2", - "read-pkg": "1.1.0" + "find-up": "^1.0.0", + "read-pkg": "^1.0.0" } }, "readable-stream": { "version": "1.1.14", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", + "dev": true, "requires": { - "core-util-is": "1.0.2", - "inherits": "2.0.1", + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", "isarray": "0.0.1", - "string_decoder": "0.10.31" + "string_decoder": "~0.10.x" } }, "redent": { @@ -2176,43 +2054,41 @@ "integrity": "sha1-z5Fqsf1fHxbfsggi3W7H9zDCr94=", "dev": true, "requires": { - "indent-string": "2.1.0", - "strip-indent": "1.0.1" + "indent-string": "^2.1.0", + "strip-indent": "^1.0.1" } }, "request": { - "version": "2.85.0", - "resolved": "https://registry.npmjs.org/request/-/request-2.85.0.tgz", - "integrity": "sha512-8H7Ehijd4js+s6wuVPLjwORxD4zeuyjYugprdOXlPSqaApmL/QOy+EB/beICHVCHkGMKNh5rvihb5ov+IDw4mg==", + "version": "2.88.0", + "resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz", + "integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==", "requires": { - "aws-sign2": "0.7.0", - "aws4": "1.7.0", - "caseless": "0.12.0", - "combined-stream": "1.0.5", - "extend": "3.0.1", - "forever-agent": "0.6.1", - "form-data": "2.3.2", - "har-validator": "5.0.3", - "hawk": "6.0.2", - "http-signature": "1.2.0", - "is-typedarray": "1.0.0", - "isstream": "0.1.2", - "json-stringify-safe": "5.0.1", - "mime-types": "2.1.18", - "oauth-sign": "0.8.2", - "performance-now": "2.1.0", - "qs": "6.5.1", - "safe-buffer": "5.1.1", - "stringstream": "0.0.5", - "tough-cookie": "2.3.4", - "tunnel-agent": "0.6.0", - "uuid": "3.2.1" + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "form-data": "~2.3.2", + "har-validator": "~5.1.0", + "http-signature": "~1.2.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.19", + "oauth-sign": "~0.9.0", + "performance-now": "^2.1.0", + "qs": "~6.5.2", + "safe-buffer": "^5.1.2", + "tough-cookie": "~2.4.3", + "tunnel-agent": "^0.6.0", + "uuid": "^3.3.2" }, "dependencies": { "extend": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.1.tgz", - "integrity": "sha1-p1Xqe8Gt/MWjHOfnYtuq3F5jZEQ=" + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" } } }, @@ -2221,16 +2097,16 @@ "resolved": "https://registry.npmjs.org/request-progress/-/request-progress-1.0.2.tgz", "integrity": "sha1-XUBvCBMJ32G0qKqDzVc032Pxi/U=", "requires": { - "throttleit": "1.0.0" + "throttleit": "^1.0.0" } }, "resolve": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.5.0.tgz", - "integrity": "sha512-hgoSGrc3pjzAPHNBg+KnFcK2HwlHTs/YrAGUr6qgTVUZmXv1UEXXl0bZNBKMA9fud6lRYFdPGz0xXxycPzmmiw==", + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.8.1.tgz", + "integrity": "sha512-AicPrAC7Qu1JxPCZ9ZgCZlY35QgFnNqc+0LtbRNxnVw4TXvjQ72wnuL9JQcEBgXkI9JM8MsT9kaQoHcpCRJOYA==", "dev": true, "requires": { - "path-parse": "1.0.5" + "path-parse": "^1.0.5" } }, "rimraf": { @@ -2239,13 +2115,18 @@ "integrity": "sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w==", "dev": true, "requires": { - "glob": "7.1.2" + "glob": "^7.0.5" } }, "safe-buffer": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", - "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==" + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, "sanitize-filename": { "version": "1.6.1", @@ -2253,7 +2134,7 @@ "integrity": "sha1-YS2hyWRz+gLczaktzVtKsWSmdyo=", "dev": true, "requires": { - "truncate-utf8-bytes": "1.0.2" + "truncate-utf8-bytes": "^1.0.0" } }, "semver": { @@ -2279,15 +2160,7 @@ "integrity": "sha1-wvg/Jzo+GhbtsJlWYdoO1e8DM2Q=", "dev": true, "requires": { - "string-width": "1.0.1" - } - }, - "sntp": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/sntp/-/sntp-2.1.0.tgz", - "integrity": "sha512-FL1b58BDrqS3A11lJ0zEdnJ3UOKqVxawAkF3k7F0CVN7VQ34aZrV+G8BZ1WC9ZL7NyrwsW0oviwsWDgRuVYtJg==", - "requires": { - "hoek": "4.2.1" + "string-width": "^1.0.1" } }, "spdx-correct": { @@ -2296,7 +2169,7 @@ "integrity": "sha1-SzBz2TP/UfORLwOsVRlJikFQ20A=", "dev": true, "requires": { - "spdx-license-ids": "1.2.1" + "spdx-license-ids": "^1.0.2" } }, "spdx-exceptions": { @@ -2311,8 +2184,8 @@ "integrity": "sha1-1SsUtelnB3FECvIlvLVjEirEUvY=", "dev": true, "requires": { - "spdx-exceptions": "1.0.4", - "spdx-license-ids": "1.2.1" + "spdx-exceptions": "^1.0.4", + "spdx-license-ids": "^1.0.0" } }, "spdx-license-ids": { @@ -2328,17 +2201,19 @@ "dev": true }, "sshpk": { - "version": "1.7.4", - "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.7.4.tgz", - "integrity": "sha1-rXtH3vymHIQV2WQkO2KwzmD7yjg=", + "version": "1.14.2", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.14.2.tgz", + "integrity": "sha1-xvxhZIo9nE52T9P8306hBeSSupg=", "requires": { - "asn1": "0.2.3", - "assert-plus": "0.2.0", - "dashdash": "1.13.0", - "ecc-jsbn": "0.1.1", - "jodid25519": "1.0.2", - "jsbn": "0.1.0", - "tweetnacl": "0.14.3" + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "safer-buffer": "^2.0.2", + "tweetnacl": "~0.14.0" } }, "string-width": { @@ -2346,9 +2221,9 @@ "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.1.tgz", "integrity": "sha1-ySEptvHX9SrPmvQkom44ZKBc6wo=", "requires": { - "code-point-at": "1.0.0", - "is-fullwidth-code-point": "1.0.0", - "strip-ansi": "3.0.1" + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" } }, "string_decoder": { @@ -2356,17 +2231,12 @@ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" }, - "stringstream": { - "version": "0.0.5", - "resolved": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.5.tgz", - "integrity": "sha1-TkhM1N5aC7vuGORjB3EKioFiGHg=" - }, "strip-ansi": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", "requires": { - "ansi-regex": "2.0.0" + "ansi-regex": "^2.0.0" } }, "strip-bom": { @@ -2375,7 +2245,7 @@ "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=", "dev": true, "requires": { - "is-utf8": "0.2.1" + "is-utf8": "^0.2.0" } }, "strip-indent": { @@ -2384,13 +2254,13 @@ "integrity": "sha1-DHlipq3vp7vUrDZkYKY4VSrhoKI=", "dev": true, "requires": { - "get-stdin": "4.0.1" + "get-stdin": "^4.0.1" } }, "strip-json-comments": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-1.0.4.tgz", - "integrity": "sha1-HhX7ysl9Pumb8tc7TGVrCCu6+5E=", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", "dev": true }, "sumchecker": { @@ -2399,7 +2269,7 @@ "integrity": "sha1-D0LBDl0F2l1C7qPlbDOZo31sWz4=", "dev": true, "requires": { - "debug": "2.6.9" + "debug": "^2.2.0" }, "dependencies": { "debug": { @@ -2410,12 +2280,6 @@ "requires": { "ms": "2.0.0" } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", - "dev": true } } }, @@ -2424,9 +2288,9 @@ "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-1.12.0.tgz", "integrity": "sha1-pqgFU9ilTHPeHQrg553ncDVgXh0=", "requires": { - "mkdirp": "0.5.1", - "pump": "1.0.1", - "tar-stream": "1.5.1" + "mkdirp": "^0.5.0", + "pump": "^1.0.0", + "tar-stream": "^1.1.2" } }, "tar-stream": { @@ -2434,10 +2298,10 @@ "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-1.5.1.tgz", "integrity": "sha1-UWx00b6j4THMC5NIkpyag/CirRE=", "requires": { - "bl": "1.1.2", - "end-of-stream": "1.1.0", - "readable-stream": "2.0.6", - "xtend": "4.0.1" + "bl": "^1.0.0", + "end-of-stream": "^1.0.0", + "readable-stream": "^2.0.0", + "xtend": "^4.0.0" }, "dependencies": { "isarray": { @@ -2450,12 +2314,12 @@ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.0.6.tgz", "integrity": "sha1-j5A0HmilPMySh4jaz80Rs265t44=", "requires": { - "core-util-is": "1.0.2", - "inherits": "2.0.1", - "isarray": "1.0.0", - "process-nextick-args": "1.0.6", - "string_decoder": "0.10.31", - "util-deprecate": "1.0.2" + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "~1.0.0", + "process-nextick-args": "~1.0.6", + "string_decoder": "~0.10.x", + "util-deprecate": "~1.0.1" } } } @@ -2471,8 +2335,8 @@ "integrity": "sha1-6zKE2k6jEbbMis42U3SKUqvyWj8=", "dev": true, "requires": { - "readable-stream": "1.1.14", - "xtend": "2.1.2" + "readable-stream": "~1.1.9", + "xtend": "~2.1.1" }, "dependencies": { "xtend": { @@ -2481,7 +2345,7 @@ "integrity": "sha1-bv7MKk2tjmlixJAbM3znuoe10os=", "dev": true, "requires": { - "object-keys": "0.4.0" + "object-keys": "~0.4.0" } } } @@ -2492,7 +2356,7 @@ "integrity": "sha1-Fyc1t/YU6nrzlmT6hM8N5OUV0SA=", "dev": true, "requires": { - "os-tmpdir": "1.0.2" + "os-tmpdir": "~1.0.1" } }, "touch": { @@ -2501,7 +2365,7 @@ "integrity": "sha1-Ua7z1ElXHU8oel2Hyci0kYGg2x0=", "dev": true, "requires": { - "nopt": "1.0.10" + "nopt": "~1.0.10" }, "dependencies": { "nopt": { @@ -2510,17 +2374,18 @@ "integrity": "sha1-bd0hvSoxQXuScn3Vhfim83YI6+4=", "dev": true, "requires": { - "abbrev": "1.1.1" + "abbrev": "1" } } } }, "tough-cookie": { - "version": "2.3.4", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.4.tgz", - "integrity": "sha512-TZ6TTfI5NtZnuyy/Kecv+CnoROnyXn2DN97LontgQpCwsX2XyLYCC0ENhYkehSOwAp8rTQKc/NUIF7BkQ5rKLA==", + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz", + "integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==", "requires": { - "punycode": "1.4.1" + "psl": "^1.1.24", + "punycode": "^1.4.1" } }, "traverse": { @@ -2541,7 +2406,7 @@ "integrity": "sha1-QFkjkJWS1W94pYGENLC3hInKXys=", "dev": true, "requires": { - "utf8-byte-length": "1.0.4" + "utf8-byte-length": "^1.0.1" } }, "tunnel-agent": { @@ -2549,13 +2414,13 @@ "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", "requires": { - "safe-buffer": "5.1.1" + "safe-buffer": "^5.0.1" } }, "tweetnacl": { - "version": "0.14.3", - "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.3.tgz", - "integrity": "sha1-PaOC9nDyXe1417PReSEZvKC3Ey0=", + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", "optional": true }, "typedarray": { @@ -2567,8 +2432,7 @@ "universalify": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.1.tgz", - "integrity": "sha1-+nG63UQ3r0wUiEHjs7Fl+enlkLc=", - "dev": true + "integrity": "sha1-+nG63UQ3r0wUiEHjs7Fl+enlkLc=" }, "utf8-byte-length": { "version": "1.0.4", @@ -2582,9 +2446,9 @@ "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" }, "uuid": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.2.1.tgz", - "integrity": "sha512-jZnMwlb9Iku/O3smGWvZhauCf6cvvpKi4BKRiliS3cxnI+Gz9j5MEpTz2UFuXiKPJocb7gnsLHwiS05ige5BEA==" + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", + "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==" }, "validate-npm-package-license": { "version": "3.0.1", @@ -2592,16 +2456,18 @@ "integrity": "sha1-KAS6vnEq0zeUWaz74kdGqywwP7w=", "dev": true, "requires": { - "spdx-correct": "1.0.2", - "spdx-expression-parse": "1.0.2" + "spdx-correct": "~1.0.0", + "spdx-expression-parse": "~1.0.0" } }, "verror": { - "version": "1.3.6", - "resolved": "https://registry.npmjs.org/verror/-/verror-1.3.6.tgz", - "integrity": "sha1-z/XfEpRtKX0rqu+qJoniW+AcAFw=", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", "requires": { - "extsprintf": "1.0.2" + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" } }, "which": { @@ -2609,7 +2475,7 @@ "resolved": "https://registry.npmjs.org/which/-/which-1.3.0.tgz", "integrity": "sha512-xcJpopdamTuY5duC/KnTTNBraPK54YwpenP4lzxU8H91GudWpFv38u0CKjclE1Wi2EH2EDz5LRcHcKbCIzqGyg==", "requires": { - "isexe": "2.0.0" + "isexe": "^2.0.0" } }, "window-size": { @@ -2622,7 +2488,7 @@ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.0.0.tgz", "integrity": "sha1-fTD4+HP5pbvDpk2ryNF34HGuQm8=", "requires": { - "string-width": "1.0.1" + "string-width": "^1.0.1" } }, "wrappy": { @@ -2657,13 +2523,30 @@ "resolved": "https://registry.npmjs.org/yargs/-/yargs-3.32.0.tgz", "integrity": "sha1-AwiOnr+edWtpdRYR0qXvWRSCyZU=", "requires": { - "camelcase": "2.1.1", - "cliui": "3.2.0", - "decamelize": "1.2.0", - "os-locale": "1.4.0", - "string-width": "1.0.1", - "window-size": "0.1.4", - "y18n": "3.2.1" + "camelcase": "^2.0.1", + "cliui": "^3.0.3", + "decamelize": "^1.1.1", + "os-locale": "^1.4.0", + "string-width": "^1.0.1", + "window-size": "^0.1.4", + "y18n": "^3.2.0" + } + }, + "yargs-parser": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-10.1.0.tgz", + "integrity": "sha512-VCIyR1wJoEBZUqk5PA+oOBF6ypbwh5aNB3I50guxAL/quggdfs4TtNHQrSazFA3fYZ+tEqfs0zIGlv0c/rgjbQ==", + "dev": true, + "requires": { + "camelcase": "^4.1.0" + }, + "dependencies": { + "camelcase": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", + "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=", + "dev": true + } } }, "yauzl": { @@ -2672,7 +2555,7 @@ "integrity": "sha1-lSj0QtqxsihOWLQ3m7GU4i4MQAU=", "dev": true, "requires": { - "fd-slicer": "1.0.1" + "fd-slicer": "~1.0.1" } } } diff --git a/server-console/package.json b/server-console/package.json index 565658702b..6824d1d9cf 100644 --- a/server-console/package.json +++ b/server-console/package.json @@ -1,6 +1,6 @@ { - "name": "HighFidelitySandbox", - "description": "High Fidelity Sandbox", + "name": "HighFidelityConsole", + "description": "High Fidelity Console", "author": "High Fidelity", "license": "Apache-2.0", "version": "1.0.0", @@ -8,8 +8,8 @@ "" ], "devDependencies": { - "electron-packager": "^12.0.0", - "electron": "1.8.4" + "electron": "^3.0.0", + "electron-packager": "^12.1.2" }, "repository": { "type": "git", @@ -23,14 +23,15 @@ "packager": "node packager.js" }, "dependencies": { - "always-tail": "0.2.0", - "cheerio": "^0.19.0", + "always-tail": "^0.2.0", + "cheerio": "^0.22.0", + "debug": "^4.0.1", "electron-log": "1.1.1", "extend": "^3.0.0", "fs-extra": "^6.0.0", "node-notifier": "^5.2.1", "os-homedir": "^1.0.1", - "request": "^2.85.0", + "request": "^2.88.0", "request-progress": "1.0.2", "tar-fs": "^1.12.0", "yargs": "^3.30.0" diff --git a/server-console/packager.js b/server-console/packager.js index 89bcd7cb71..00866ee1be 100644 --- a/server-console/packager.js +++ b/server-console/packager.js @@ -27,8 +27,8 @@ var options = { } const EXEC_NAME = "server-console"; -const SHORT_NAME = "Sandbox"; -const FULL_NAME = "High Fidelity Sandbox"; +var SHORT_NAME = argv.client_only ? "Console" : "Sandbox"; +var FULL_NAME = argv.client_only ? "High Fidelity Console" : "High Fidelity Sandbox"; // setup per OS options if (osType == "Darwin") { diff --git a/server-console/resources/console-notification.png b/server-console/resources/console-notification.png index 0fd22b8900..d506a3d2e8 100644 Binary files a/server-console/resources/console-notification.png and b/server-console/resources/console-notification.png differ diff --git a/server-console/resources/console-tray-osx-stopped.png b/server-console/resources/console-tray-osx-stopped.png index 3255d43f60..53589b60b0 100644 Binary files a/server-console/resources/console-tray-osx-stopped.png and b/server-console/resources/console-tray-osx-stopped.png differ diff --git a/server-console/resources/console-tray-osx-stopped@2x.png b/server-console/resources/console-tray-osx-stopped@2x.png index 6a75dad5bf..5356afb93e 100644 Binary files a/server-console/resources/console-tray-osx-stopped@2x.png and b/server-console/resources/console-tray-osx-stopped@2x.png differ diff --git a/server-console/resources/tray-menu-notification.png b/server-console/resources/tray-menu-notification.png new file mode 100644 index 0000000000..0d6e15752f Binary files /dev/null and b/server-console/resources/tray-menu-notification.png differ diff --git a/server-console/src/main.js b/server-console/src/main.js index 92ebdbf36c..5adb4be4cb 100644 --- a/server-console/src/main.js +++ b/server-console/src/main.js @@ -29,92 +29,47 @@ const updater = require('./modules/hf-updater.js'); const Config = require('./modules/config').Config; const hfprocess = require('./modules/hf-process.js'); + +global.log = require('electron-log'); + const Process = hfprocess.Process; const ACMonitorProcess = hfprocess.ACMonitorProcess; const ProcessStates = hfprocess.ProcessStates; const ProcessGroup = hfprocess.ProcessGroup; const ProcessGroupStates = hfprocess.ProcessGroupStates; +const hfApp = require('./modules/hf-app.js'); +const GetBuildInfo = hfApp.getBuildInfo; +const StartInterface = hfApp.startInterface; +const getRootHifiDataDirectory = hfApp.getRootHifiDataDirectory; +const getDomainServerClientResourcesDirectory = hfApp.getDomainServerClientResourcesDirectory; +const getAssignmentClientResourcesDirectory = hfApp.getAssignmentClientResourcesDirectory; +const getApplicationDataDirectory = hfApp.getApplicationDataDirectory; + + const osType = os.type(); const appIcon = path.join(__dirname, '../resources/console.png'); +const menuNotificationIcon = path.join(__dirname, '../resources/tray-menu-notification.png'); + const DELETE_LOG_FILES_OLDER_THAN_X_SECONDS = 60 * 60 * 24 * 7; // 7 Days const LOG_FILE_REGEX = /(domain-server|ac-monitor|ac)-.*-std(out|err).txt/; const HOME_CONTENT_URL = "http://cdn.highfidelity.com/content-sets/home-tutorial-RC40.tar.gz"; -function getBuildInfo() { - var buildInfoPath = null; +const buildInfo = GetBuildInfo(); - if (osType == 'Windows_NT') { - buildInfoPath = path.join(path.dirname(process.execPath), 'build-info.json'); - } else if (osType == 'Darwin') { - var contentPath = ".app/Contents/"; - var contentEndIndex = __dirname.indexOf(contentPath); - - if (contentEndIndex != -1) { - // this is an app bundle - var appPath = __dirname.substring(0, contentEndIndex) + ".app"; - buildInfoPath = path.join(appPath, "/Contents/Resources/build-info.json"); - } - } - - const DEFAULT_BUILD_INFO = { - releaseType: "", - buildIdentifier: "dev", - buildNumber: "0", - stableBuild: "0", - organization: "High Fidelity - dev", - appUserModelId: "com.highfidelity.sandbox-dev" - }; - var buildInfo = DEFAULT_BUILD_INFO; - - if (buildInfoPath) { - try { - buildInfo = JSON.parse(fs.readFileSync(buildInfoPath)); - } catch (e) { - buildInfo = DEFAULT_BUILD_INFO; - } - } - - return buildInfo; -} -const buildInfo = getBuildInfo(); - -function getRootHifiDataDirectory(local) { - var organization = buildInfo.organization; - if (osType == 'Windows_NT') { - if (local) { - return path.resolve(osHomeDir(), 'AppData/Local', organization); - } else { - return path.resolve(osHomeDir(), 'AppData/Roaming', organization); - } - } else if (osType == 'Darwin') { - return path.resolve(osHomeDir(), 'Library/Application Support', organization); - } else { - return path.resolve(osHomeDir(), '.local/share/', organization); - } -} - -function getDomainServerClientResourcesDirectory() { - return path.join(getRootHifiDataDirectory(), '/domain-server'); -} - -function getAssignmentClientResourcesDirectory() { - return path.join(getRootHifiDataDirectory(), '/assignment-client'); -} - -function getApplicationDataDirectory(local) { - return path.join(getRootHifiDataDirectory(local), '/Server Console'); -} +const NotificationState = { + UNNOTIFIED: 'unnotified', + NOTIFIED: 'notified' +}; // Update lock filepath const UPDATER_LOCK_FILENAME = ".updating"; const UPDATER_LOCK_FULL_PATH = getRootHifiDataDirectory() + "/" + UPDATER_LOCK_FILENAME; // Configure log -global.log = require('electron-log'); const oldLogFile = path.join(getApplicationDataDirectory(), '/log.txt'); const logFile = path.join(getApplicationDataDirectory(true), '/log.txt'); if (oldLogFile != logFile && fs.existsSync(oldLogFile)) { @@ -149,15 +104,32 @@ const configPath = path.join(getApplicationDataDirectory(), 'config.json'); var userConfig = new Config(); userConfig.load(configPath); - const ipcMain = electron.ipcMain; + +function isInterfaceInstalled () { + if (osType == "Darwin") { + // In OSX Sierra, the app translocation process moves + // the executable to a random location before starting it + // which makes finding the interface near impossible using + // relative paths. For now, as there are no server-only + // installs, we just assume the interface is installed here + return true; + } else { + return interfacePath; + } +} + +function isServerInstalled () { + return dsPath && acPath; +} + var isShuttingDown = false; function shutdown() { log.debug("Normal shutdown (isShuttingDown: " + isShuttingDown + ")"); if (!isShuttingDown) { // if the home server is running, show a prompt before quit to ask if the user is sure - if (homeServer.state == ProcessGroupStates.STARTED) { + if (isServerInstalled() && homeServer.state == ProcessGroupStates.STARTED) { log.debug("Showing shutdown dialog."); dialog.showMessageBox({ type: 'question', @@ -184,6 +156,9 @@ function shutdownCallback(idx) { if (idx == 0 && !isShuttingDown) { isShuttingDown = true; + log.debug("Stop tray polling."); + trayNotifications.stopPolling(); + log.debug("Saving user config"); userConfig.save(configPath); @@ -191,31 +166,37 @@ function shutdownCallback(idx) { log.debug("Closing log window"); logWindow.close(); } - if (homeServer) { - log.debug("Stoping home server"); - homeServer.stop(); - } - updateTrayMenu(homeServer.state); + if (isServerInstalled()) { + if (homeServer) { + log.debug("Stoping home server"); + homeServer.stop(); - if (homeServer.state == ProcessGroupStates.STOPPED) { - // if the home server is already down, take down the server console now - log.debug("Quitting."); - app.exit(0); - } else { - // if the home server is still running, wait until we get a state change or timeout - // before quitting the app - log.debug("Server still shutting down. Waiting"); - var timeoutID = setTimeout(function() { - app.exit(0); - }, 5000); - homeServer.on('state-update', function(processGroup) { - if (processGroup.state == ProcessGroupStates.STOPPED) { - clearTimeout(timeoutID); + updateTrayMenu(homeServer.state); + + if (homeServer.state == ProcessGroupStates.STOPPED) { + // if the home server is already down, take down the server console now log.debug("Quitting."); app.exit(0); + } else { + // if the home server is still running, wait until we get a state change or timeout + // before quitting the app + log.debug("Server still shutting down. Waiting"); + var timeoutID = setTimeout(function() { + app.exit(0); + }, 5000); + homeServer.on('state-update', function(processGroup) { + if (processGroup.state == ProcessGroupStates.STOPPED) { + clearTimeout(timeoutID); + log.debug("Quitting."); + app.exit(0); + } + }); } - }); + } + } + else { + app.exit(0); } } } @@ -266,15 +247,12 @@ process.on('uncaughtException', function(err) { log.error(err.stack); }); -var shouldQuit = app.makeSingleInstance(function(commandLine, workingDirectory) { - // Someone tried to run a second instance, focus the window (if there is one) - return true; -}); +const gotTheLock = app.requestSingleInstanceLock() -if (shouldQuit) { - log.warn("Another instance of the Sandbox is already running - this instance will quit."); - app.exit(0); - return; +if (!gotTheLock) { + log.warn("Another instance of the Sandbox is already running - this instance will quit."); + app.exit(0); + return; } // Check command line arguments to see how to find binaries @@ -290,10 +268,14 @@ var debug = argv.debug; var binaryType = argv.binaryType; -interfacePath = pathFinder.discoveredPath("Interface", binaryType, buildInfo.releaseType); +interfacePath = pathFinder.discoveredPath("interface", binaryType, buildInfo.releaseType); dsPath = pathFinder.discoveredPath("domain-server", binaryType, buildInfo.releaseType); acPath = pathFinder.discoveredPath("assignment-client", binaryType, buildInfo.releaseType); +console.log("Domain Server Path: " + dsPath); +console.log("Assignment Client Path: " + acPath); +console.log("Interface Path: " + interfacePath); + function binaryMissingMessage(displayName, executableName, required) { var message = "The " + displayName + " executable was not found.\n"; @@ -317,18 +299,6 @@ function binaryMissingMessage(displayName, executableName, required) { return message; } -// if at this point any of the paths are null, we're missing something we wanted to find - -if (!dsPath) { - dialog.showErrorBox("Domain Server Not Found", binaryMissingMessage("domain-server", "domain-server", true)); - app.exit(0); -} - -if (!acPath) { - dialog.showErrorBox("Assignment Client Not Found", binaryMissingMessage("assignment-client", "assignment-client", true)); - app.exit(0); -} - function openFileBrowser(path) { // Add quotes around path path = '"' + path + '"'; @@ -351,20 +321,6 @@ function openLogDirectory() { app.on('window-all-closed', function() { }); -function startInterface(url) { - var argArray = []; - - // check if we have a url parameter to include - if (url) { - argArray = ["--url", url]; - } - - // create a new Interface instance - Interface makes sure only one is running at a time - var pInterface = new Process('interface', interfacePath, argArray); - pInterface.detached = true; - pInterface.start(); -} - var tray = null; global.homeServer = null; global.domainServer = null; @@ -372,6 +328,39 @@ global.acMonitor = null; global.userConfig = userConfig; global.openLogDirectory = openLogDirectory; +const hfNotifications = require('./modules/hf-notifications.js'); +const HifiNotifications = hfNotifications.HifiNotifications; +const HifiNotificationType = hfNotifications.NotificationType; + +var pendingNotifications = {} +var notificationState = NotificationState.UNNOTIFIED; + +function setNotificationState (notificationType, pending = undefined) { + if (pending !== undefined) { + if ((notificationType === HifiNotificationType.TRANSACTIONS || + notificationType === HifiNotificationType.ITEMS)) { + // special case, because we want to clear the indicator light + // on INVENTORY when either Transactions or Items are + // clicked on in the notification popup, we detect that case + // here and force both to be unnotified. + pendingNotifications[HifiNotificationType.TRANSACTIONS] = pending; + pendingNotifications[HifiNotificationType.ITEMS] = pending; + } else { + pendingNotifications[notificationType] = pending; + } + notificationState = NotificationState.UNNOTIFIED; + for (var key in pendingNotifications) { + if (pendingNotifications[key]) { + notificationState = NotificationState.NOTIFIED; + break; + } + } + } + updateTrayMenu(homeServer ? homeServer.state : ProcessGroupStates.STOPPED); +} + +var trayNotifications = new HifiNotifications(userConfig, setNotificationState); + var LogWindow = function(ac, ds) { this.ac = ac; this.ds = ds; @@ -406,8 +395,8 @@ LogWindow.prototype = { }; function visitSandboxClicked() { - if (interfacePath) { - startInterface('hifi://localhost'); + if (isInterfaceInstalled()) { + StartInterface('hifi://localhost'); } else { // show an error to say that we can't go home without an interface instance dialog.showErrorBox("Client Not Found", binaryMissingMessage("High Fidelity client", "Interface", false)); @@ -425,6 +414,38 @@ var labels = { label: 'Version - ' + buildInfo.buildIdentifier, enabled: false }, + showNotifications: { + label: 'Show Notifications', + type: 'checkbox', + checked: true, + click: function () { + trayNotifications.enable(!trayNotifications.enabled(), setNotificationState); + userConfig.save(configPath); + updateTrayMenu(homeServer ? homeServer.state : ProcessGroupStates.STOPPED); + } + }, + goto: { + label: 'GoTo', + click: function () { + StartInterface("hifiapp:GOTO"); + setNotificationState(HifiNotificationType.GOTO, false); + } + }, + people: { + label: 'People', + click: function () { + StartInterface("hifiapp:PEOPLE"); + setNotificationState(HifiNotificationType.PEOPLE, false); + } + }, + inventory: { + label: 'Inventory', + click: function () { + StartInterface("hifiapp:INVENTORY"); + setNotificationState(HifiNotificationType.ITEMS, false); + setNotificationState(HifiNotificationType.TRANSACTIONS, false); + } + }, restart: { label: 'Start Server', click: function() { @@ -489,22 +510,37 @@ function buildMenuArray(serverState) { if (isShuttingDown) { menuArray.push(labels.shuttingDown); } else { - menuArray.push(labels.serverState); - menuArray.push(labels.version); - menuArray.push(separator); - menuArray.push(labels.goHome); - menuArray.push(separator); - menuArray.push(labels.restart); - menuArray.push(labels.stopServer); - menuArray.push(labels.settings); - menuArray.push(labels.viewLogs); - menuArray.push(separator); + if (isServerInstalled()) { + menuArray.push(labels.serverState); + menuArray.push(labels.version); + menuArray.push(separator); + } + if (isServerInstalled() && isInterfaceInstalled()) { + menuArray.push(labels.goHome); + menuArray.push(separator); + } + if (isServerInstalled()) { + menuArray.push(labels.restart); + menuArray.push(labels.stopServer); + menuArray.push(labels.settings); + menuArray.push(labels.viewLogs); + menuArray.push(separator); + } menuArray.push(labels.share); menuArray.push(separator); + if (isInterfaceInstalled()) { + if (trayNotifications.enabled()) { + menuArray.push(labels.goto); + menuArray.push(labels.people); + menuArray.push(labels.inventory); + menuArray.push(separator); + } + menuArray.push(labels.showNotifications); + menuArray.push(separator); + } menuArray.push(labels.quit); } - return menuArray; } @@ -528,12 +564,53 @@ function updateLabels(serverState) { labels.restart.label = "Restart Server"; labels.restart.enabled = false; } + + labels.showNotifications.checked = trayNotifications.enabled(); + labels.goto.icon = pendingNotifications[HifiNotificationType.GOTO] ? menuNotificationIcon : null; + labels.people.icon = pendingNotifications[HifiNotificationType.PEOPLE] ? menuNotificationIcon : null; + labels.inventory.icon = pendingNotifications[HifiNotificationType.ITEMS] || pendingNotifications[HifiNotificationType.TRANSACTIONS]? menuNotificationIcon : null; + var onlineUsers = trayNotifications.getOnlineUsers(); + delete labels.people.submenu; + if (onlineUsers) { + for (var name in onlineUsers) { + if(labels.people.submenu == undefined) { + labels.people.submenu = []; + } + labels.people.submenu.push({ + label: name, + enabled: (onlineUsers[name].location != undefined), + click: function (item) { + setNotificationState(HifiNotificationType.PEOPLE, false); + if(onlineUsers[item.label] && onlineUsers[item.label].location) { + StartInterface("hifi://" + onlineUsers[item.label].location.root.name + onlineUsers[item.label].location.path); + } + } + }); + } + } + var currentStories = trayNotifications.getCurrentStories(); + delete labels.goto.submenu; + if (currentStories) { + for (var location in currentStories) { + if(labels.goto.submenu == undefined) { + labels.goto.submenu = []; + } + labels.goto.submenu.push({ + label: "event in " + location, + location: location, + click: function (item) { + setNotificationState(HifiNotificationType.GOTO, false); + StartInterface("hifi://" + item.location + currentStories[item.location].path); + } + }); + } + } } function updateTrayMenu(serverState) { if (tray) { var menuArray = buildMenuArray(isShuttingDown ? null : serverState); - tray.setImage(trayIcons[serverState]); + tray.setImage(trayIcons[notificationState]); tray.setContextMenu(Menu.buildFromTemplate(menuArray)); if (isShuttingDown) { tray.setToolTip('High Fidelity - Shutting Down'); @@ -753,9 +830,8 @@ function maybeShowSplash() { const trayIconOS = (osType == "Darwin") ? "osx" : "win"; var trayIcons = {}; -trayIcons[ProcessGroupStates.STARTED] = "console-tray-" + trayIconOS + ".png"; -trayIcons[ProcessGroupStates.STOPPED] = "console-tray-" + trayIconOS + "-stopped.png"; -trayIcons[ProcessGroupStates.STOPPING] = "console-tray-" + trayIconOS + "-stopping.png"; +trayIcons[NotificationState.UNNOTIFIED] = "console-tray-" + trayIconOS + ".png"; +trayIcons[NotificationState.NOTIFIED] = "console-tray-" + trayIconOS + "-stopped.png"; for (var key in trayIcons) { var fullPath = path.join(__dirname, '../resources/' + trayIcons[key]); var img = nativeImage.createFromPath(fullPath); @@ -781,33 +857,29 @@ function onContentLoaded() { // Disable splash window for now. // maybeShowSplash(); - if (buildInfo.releaseType == 'PRODUCTION' && !argv.noUpdater) { + if (isServerInstalled()) { + if (buildInfo.releaseType == 'PRODUCTION' && !argv.noUpdater) { - const CHECK_FOR_UPDATES_INTERVAL_SECONDS = 60 * 30; - var hasShownUpdateNotification = false; - const updateChecker = new updater.UpdateChecker(buildInfo, CHECK_FOR_UPDATES_INTERVAL_SECONDS); - updateChecker.on('update-available', function(latestVersion, url) { - if (!hasShownUpdateNotification) { - notifier.notify({ - icon: notificationIcon, - title: 'An update is available!', - message: 'High Fidelity version ' + latestVersion + ' is available', - wait: true, - appID: buildInfo.appUserModelId, - url: url - }); - hasShownUpdateNotification = true; - } - }); - notifier.on('click', function(notifierObject, options) { - log.debug("Got click", options.url); - shell.openExternal(options.url); - }); - } + const CHECK_FOR_UPDATES_INTERVAL_SECONDS = 60 * 30; + var hasShownUpdateNotification = false; + const updateChecker = new updater.UpdateChecker(buildInfo, CHECK_FOR_UPDATES_INTERVAL_SECONDS); + updateChecker.on('update-available', function(latestVersion, url) { + if (!hasShownUpdateNotification) { + notifier.notify({ + icon: notificationIcon, + title: 'An update is available!', + message: 'High Fidelity version ' + latestVersion + ' is available', + wait: true, + appID: buildInfo.appUserModelId, + url: url + }); + hasShownUpdateNotification = true; + } + }); + } - deleteOldFiles(logPath, DELETE_LOG_FILES_OLDER_THAN_X_SECONDS, LOG_FILE_REGEX); + deleteOldFiles(logPath, DELETE_LOG_FILES_OLDER_THAN_X_SECONDS, LOG_FILE_REGEX); - if (dsPath && acPath) { var dsArguments = ['--get-temp-name', '--parent-pid', process.pid]; domainServer = new Process('domain-server', dsPath, dsArguments, logPath); @@ -838,7 +910,7 @@ function onContentLoaded() { // If we were launched with the launchInterface option, then we need to launch interface now if (argv.launchInterface) { log.debug("Interface launch requested... argv.launchInterface:", argv.launchInterface); - startInterface(); + StartInterface(); } // If we were launched with the shutdownWith option, then we need to shutdown when that process (pid) @@ -868,14 +940,19 @@ app.on('ready', function() { } // Create tray icon - tray = new Tray(trayIcons[ProcessGroupStates.STOPPED]); - tray.setToolTip('High Fidelity Sandbox'); + tray = new Tray(trayIcons[NotificationState.UNNOTIFIED]); + tray.setToolTip('High Fidelity'); tray.on('click', function() { tray.popUpContextMenu(tray.menu); }); + if (isInterfaceInstalled()) { + trayNotifications.startPolling(); + } updateTrayMenu(ProcessGroupStates.STOPPED); - - maybeInstallDefaultContentSet(onContentLoaded); + + if (isServerInstalled()) { + maybeInstallDefaultContentSet(onContentLoaded); + } }); diff --git a/server-console/src/modules/hf-acctinfo.js b/server-console/src/modules/hf-acctinfo.js new file mode 100644 index 0000000000..d20748544f --- /dev/null +++ b/server-console/src/modules/hf-acctinfo.js @@ -0,0 +1,138 @@ +'use strict' + +const request = require('request'); +const extend = require('extend'); +const util = require('util'); +const events = require('events'); +const childProcess = require('child_process'); +const fs = require('fs-extra'); +const os = require('os'); +const path = require('path'); + +const hfApp = require('./hf-app.js'); +const getInterfaceDataDirectory = hfApp.getInterfaceDataDirectory; + + +const VariantTypes = { + USER_TYPE: 1024 +} + +function AccountInfo() { + + var accountInfoPath = path.join(getInterfaceDataDirectory(), '/AccountInfo.bin'); + this.rawData = null; + this.parseOffset = 0; + try { + this.rawData = fs.readFileSync(accountInfoPath); + + this.data = this._parseMap(); + + } catch(e) { + console.log(e); + log.debug("AccountInfo file not found: " + accountInfoPath); + } +} + +AccountInfo.prototype = { + + accessToken: function (metaverseUrl) { + if (this.data && this.data[metaverseUrl] && this.data[metaverseUrl]["accessToken"]) { + return this.data[metaverseUrl]["accessToken"]["token"]; + } + return null; + }, + _parseUInt32: function () { + if (!this.rawData || (this.rawData.length - this.parseOffset < 4)) { + throw "Expected uint32"; + } + var result = this.rawData.readUInt32BE(this.parseOffset); + this.parseOffset += 4; + return result; + }, + _parseMap: function () { + var result = {}; + var n = this._parseUInt32(); + for (var i = 0; i < n; i++) { + var key = this._parseQString(); + result[key] = this._parseVariant(); + } + return result; + }, + _parseVariant: function () { + var varType = this._parseUInt32(); + var isNull = this.rawData[this.parseOffset++]; + + switch (varType) { + case VariantTypes.USER_TYPE: + //user type + var userTypeName = this._parseByteArray().toString('ascii').slice(0,-1); + if (userTypeName === "DataServerAccountInfo") { + return this._parseDataServerAccountInfo(); + } + else { + throw "Unknown custom type " + userTypeName; + } + break; + } + }, + _parseByteArray: function () { + var length = this._parseUInt32(); + if (length === 0xffffffff) { + return null; + } + var result = this.rawData.slice(this.parseOffset, this.parseOffset+length); + this.parseOffset += length; + return result; + + }, + _parseQString: function () { + if (!this.rawData || (this.rawData.length <= this.parseOffset)) { + throw "Expected QString"; + } + // length in bytes; + var length = this._parseUInt32(); + if (length === 0xFFFFFFFF) { + return null; + } + + if (this.rawData.length - this.parseOffset < length) { + throw "Insufficient buffer length for QString parsing"; + } + + // Convert from BE UTF16 to LE + var resultBuffer = this.rawData.slice(this.parseOffset, this.parseOffset+length); + resultBuffer.swap16(); + var result = resultBuffer.toString('utf16le'); + this.parseOffset += length; + return result; + }, + _parseDataServerAccountInfo: function () { + return { + accessToken: this._parseOAuthAccessToken(), + username: this._parseQString(), + xmppPassword: this._parseQString(), + discourseApiKey: this._parseQString(), + walletId: this._parseUUID(), + privateKey: this._parseByteArray(), + domainId: this._parseUUID(), + tempDomainId: this._parseUUID(), + tempDomainApiKey: this._parseQString() + + } + }, + _parseOAuthAccessToken: function () { + return { + token: this._parseQString(), + timestampHigh: this._parseUInt32(), + timestampLow: this._parseUInt32(), + tokenType: this._parseQString(), + refreshToken: this._parseQString() + } + }, + _parseUUID: function () { + this.parseOffset += 16; + return null; + } +} + +exports.AccountInfo = AccountInfo; \ No newline at end of file diff --git a/server-console/src/modules/hf-app.js b/server-console/src/modules/hf-app.js new file mode 100644 index 0000000000..af6c38cdb3 --- /dev/null +++ b/server-console/src/modules/hf-app.js @@ -0,0 +1,127 @@ +const fs = require('fs'); +const extend = require('extend'); +const Config = require('./config').Config +const os = require('os'); +const pathFinder = require('./path-finder'); +const path = require('path'); +const argv = require('yargs').argv; +const hfprocess = require('./hf-process'); +const osHomeDir = require('os-homedir'); +const childProcess = require('child_process'); +const Process = hfprocess.Process; + +const binaryType = argv.binaryType; +const osType = os.type(); + +exports.getBuildInfo = function() { + var buildInfoPath = null; + + if (osType === 'Windows_NT') { + buildInfoPath = path.join(path.dirname(process.execPath), 'build-info.json'); + } else if (osType === 'Darwin') { + var contentPath = ".app/Contents/"; + var contentEndIndex = __dirname.indexOf(contentPath); + + if (contentEndIndex != -1) { + // this is an app bundle + var appPath = __dirname.substring(0, contentEndIndex) + ".app"; + buildInfoPath = path.join(appPath, "/Contents/Resources/build-info.json"); + } + } + + const DEFAULT_BUILD_INFO = { + releaseType: "", + buildIdentifier: "dev", + buildNumber: "0", + stableBuild: "0", + organization: "High Fidelity - dev", + appUserModelId: "com.highfidelity.console" + }; + var buildInfo = DEFAULT_BUILD_INFO; + + if (buildInfoPath) { + try { + buildInfo = JSON.parse(fs.readFileSync(buildInfoPath)); + } catch (e) { + buildInfo = DEFAULT_BUILD_INFO; + } + } + + return buildInfo; +} + +const buildInfo = exports.getBuildInfo(); +const interfacePath = pathFinder.discoveredPath("interface", binaryType, buildInfo.releaseType); + +exports.startInterface = function(url) { + + if (osType === 'Darwin') { + if (!url) { + log.debug("No URL given for startInterface"); + return; + } + + // do this as a workaround for app translocation on osx, which makes + // it nearly impossible to find the interface executable + var bundle_id = 'com.highfidelity.interface-dev'; + if (buildInfo.releaseType == 'PR') { + bundle_id = 'com.highfidelity.interface-pr'; + } else if (buildInfo.releaseType == 'PRODUCTION') { + bundle_id = 'com.highfidelity.interface'; + } + childProcess.exec('open -b ' + bundle_id + ' --args --url ' + url); + } else { + var argArray = []; + + // check if we have a url parameter to include + if (url) { + argArray = ["--url", url]; + } + console.log("Starting with " + url); + // create a new Interface instance - Interface makes sure only one is running at a time + var pInterface = new Process('Interface', interfacePath, argArray); + pInterface.detached = true; + pInterface.start(); + } +} + +exports.isInterfaceRunning = function(done) { + if (osType === 'Windows_NT') { + var pInterface = new Process('interface', 'interface.exe'); + } else if (osType === 'Darwin') { + var pInterface = new Process('interface', 'interface'); + } + return pInterface.isRunning(done); +} + + +exports.getRootHifiDataDirectory = function(local) { + var organization = buildInfo.organization; + if (osType === 'Windows_NT') { + if (local) { + return path.resolve(osHomeDir(), 'AppData/Local', organization); + } else { + return path.resolve(osHomeDir(), 'AppData/Roaming', organization); + } + } else if (osType === 'Darwin') { + return path.resolve(osHomeDir(), 'Library/Application Support', organization); + } else { + return path.resolve(osHomeDir(), '.local/share/', organization); + } +} + +exports.getDomainServerClientResourcesDirectory = function() { + return path.join(exports.getRootHifiDataDirectory(), '/domain-server'); +} + +exports.getAssignmentClientResourcesDirectory = function() { + return path.join(exports.getRootHifiDataDirectory(), '/assignment-client'); +} + +exports.getApplicationDataDirectory = function(local) { + return path.join(exports.getRootHifiDataDirectory(local), '/Server Console'); +} + +exports.getInterfaceDataDirectory = function(local) { + return path.join(exports.getRootHifiDataDirectory(local), '/Interface'); +} \ No newline at end of file diff --git a/server-console/src/modules/hf-notifications.js b/server-console/src/modules/hf-notifications.js new file mode 100644 index 0000000000..1ddbd1d307 --- /dev/null +++ b/server-console/src/modules/hf-notifications.js @@ -0,0 +1,519 @@ +const request = require('request'); +const notifier = require('node-notifier'); +const os = require('os'); +const process = require('process'); +const hfApp = require('./hf-app'); +const path = require('path'); +const AccountInfo = require('./hf-acctinfo').AccountInfo; +const url = require('url'); +const shell = require('electron').shell; +const GetBuildInfo = hfApp.getBuildInfo; +const buildInfo = GetBuildInfo(); +const osType = os.type(); + +const notificationIcon = path.join(__dirname, '../../resources/console-notification.png'); +const STORIES_NOTIFICATION_POLL_TIME_MS = 120 * 1000; +const PEOPLE_NOTIFICATION_POLL_TIME_MS = 120 * 1000; +const WALLET_NOTIFICATION_POLL_TIME_MS = 600 * 1000; +const MARKETPLACE_NOTIFICATION_POLL_TIME_MS = 600 * 1000; +const OSX_CLICK_DELAY_TIMEOUT = 500; + + +const METAVERSE_SERVER_URL= process.env.HIFI_METAVERSE_URL ? process.env.HIFI_METAVERSE_URL : 'https://metaverse.highfidelity.com' +const STORIES_URL= '/api/v1/user_stories'; +const USERS_URL= '/api/v1/users'; +const ECONOMIC_ACTIVITY_URL= '/api/v1/commerce/history'; +const UPDATES_URL= '/api/v1/commerce/available_updates'; +const MAX_NOTIFICATION_ITEMS=30 +const STARTUP_MAX_NOTIFICATION_ITEMS=1 + + +const StartInterface=hfApp.startInterface; +const IsInterfaceRunning=hfApp.isInterfaceRunning; + +const NotificationType = { + GOTO: 'goto', + PEOPLE: 'people', + ITEMS: 'items', + TRANSACTIONS: 'transactions' +}; + + +function HifiNotification(notificationType, notificationData, menuNotificationCallback) { + this.type = notificationType; + this.data = notificationData; +} + +HifiNotification.prototype = { + show: function (finished) { + var text = ""; + var message = ""; + var url = null; + var app = null; + switch (this.type) { + case NotificationType.GOTO: + if (typeof(this.data) === "number") { + if (this.data === 1) { + text = "You have " + this.data + " event invitation pending." + } else { + text = "You have " + this.data + " event invitations pending." + } + message = "Click to open GOTO."; + url="hifiapp:GOTO" + } else { + text = this.data.username + " " + this.data.action_string + " in " + this.data.place_name + "."; + message = "Click to go to " + this.data.place_name + "."; + url = "hifi://" + this.data.place_name + this.data.path; + } + break; + + case NotificationType.PEOPLE: + if (typeof(this.data) === "number") { + if (this.data === 1) { + text = this.data + " of your connections is online." + } else { + text = this.data + " of your connections are online." + } + message = "Click to open PEOPLE."; + url="hifiapp:PEOPLE"; + } else { + if (this.data.location) { + text = this.data.username + " is available in " + this.data.location.root.name + "."; + message = "Click to join them."; + url="hifi://" + this.data.location.root.name + this.data.location.path; + } else { + text = this.data.username + " is online."; + message = "Click to open PEOPLE."; + url="hifiapp:PEOPLE"; + } + } + break; + + case NotificationType.TRANSACTIONS: + if (typeof(this.data) === "number") { + if (this.data === 1) { + text = "You have " + this.data + " unread transaction."; + } else { + text = "You have " + this.data + " unread transactions."; + } + message = "Click to open INVENTORY." + url = "hifiapp:INVENTORY"; + break; + } + text = this.data.message.replace(/<\/?[^>]+(>|$)/g, ""); + message = "Click to open INVENTORY."; + url = "hifiapp:INVENTORY"; + break; + + case NotificationType.ITEMS: + if (typeof(this.data) === "number") { + if (this.data === 1) { + text = this.data + " of your items has an update available."; + } else { + text = this.data + " of your items have updates available."; + } + } else { + text = "Update available for " + this.data.base_item_title + "."; + } + message = "Click to open INVENTORY."; + url = "hifiapp:INVENTORY"; + break; + } + notifier.notify({ + notificationType: this.type, + icon: notificationIcon, + title: text, + message: message, + wait: true, + appID: buildInfo.appUserModelId, + url: url, + timeout: 5 + }, + function (error, reason, metadata) { + if (finished) { + if (osType === 'Darwin') { + setTimeout(finished, OSX_CLICK_DELAY_TIMEOUT); + } else { + finished(); + } + } + }); + } +} + +function HifiNotifications(config, menuNotificationCallback) { + this.config = config; + this.menuNotificationCallback = menuNotificationCallback; + this.onlineUsers = {}; + this.currentStories = {}; + this.storiesSince = new Date(this.config.get("storiesNotifySince", "1970-01-01T00:00:00.000Z")); + this.peopleSince = new Date(this.config.get("peopleNotifySince", "1970-01-01T00:00:00.000Z")); + this.walletSince = new Date(this.config.get("walletNotifySince", "1970-01-01T00:00:00.000Z")); + this.marketplaceSince = new Date(this.config.get("marketplaceNotifySince", "1970-01-01T00:00:00.000Z")); + + this.pendingNotifications = []; + + + var _menuNotificationCallback = menuNotificationCallback; + notifier.on('click', function (notifierObject, options) { + const optUrl = url.parse(options.url); + if ((optUrl.protocol === "hifi:") || (optUrl.protocol === "hifiapp:")) { + StartInterface(options.url); + _menuNotificationCallback(options.notificationType, false); + } else { + shell.openExternal(options.url); + } + }); +} + +HifiNotifications.prototype = { + enable: function (enabled) { + this.config.set("disableTrayNotifications", !enabled); + if (enabled) { + var _this = this; + this.storiesPollTimer = setInterval(function () { + var _since = _this.storiesSince; + _this.storiesSince = new Date(); + _this.pollForStories(_since); + }, + STORIES_NOTIFICATION_POLL_TIME_MS); + + this.peoplePollTimer = setInterval(function () { + var _since = _this.peopleSince; + _this.peopleSince = new Date(); + _this.pollForConnections(_since); + }, + PEOPLE_NOTIFICATION_POLL_TIME_MS); + + this.walletPollTimer = setInterval(function () { + var _since = _this.walletSince; + _this.walletSince = new Date(); + _this.pollForEconomicActivity(_since); + }, + WALLET_NOTIFICATION_POLL_TIME_MS); + + this.marketplacePollTimer = setInterval(function () { + var _since = _this.marketplaceSince; + _this.marketplaceSince = new Date(); + _this.pollForMarketplaceUpdates(_since); + }, + MARKETPLACE_NOTIFICATION_POLL_TIME_MS); + } else { + this.stopPolling(); + } + }, + enabled: function () { + return !this.config.get("disableTrayNotifications", false); + }, + startPolling: function () { + this.enable(this.enabled()); + }, + stopPolling: function () { + this.config.set("storiesNotifySince", this.storiesSince.toISOString()); + this.config.set("peopleNotifySince", this.peopleSince.toISOString()); + this.config.set("walletNotifySince", this.walletSince.toISOString()); + this.config.set("marketplaceNotifySince", this.marketplaceSince.toISOString()); + + if (this.storiesPollTimer) { + clearInterval(this.storiesPollTimer); + } + if (this.peoplePollTimer) { + clearInterval(this.peoplePollTimer); + } + if (this.walletPollTimer) { + clearInterval(this.walletPollTimer); + } + if (this.marketplacePollTimer) { + clearInterval(this.marketplacePollTimer); + } + }, + getOnlineUsers: function () { + return this.onlineUsers; + }, + getCurrentStories: function () { + return this.currentStories; + }, + _showNotification: function () { + var _this = this; + if (osType === 'Darwin') { + this.pendingNotifications[0].show(function () { + // For OSX + // Don't attempt to show the next notification + // until the current is clicked or times out + // as the OSX Notifier stuff will dismiss the + // previous notification immediately when a + // new one is submitted + _this.pendingNotifications.shift(); + if (_this.pendingNotifications.length > 0) { + _this._showNotification(); + } + }); + } else { + // For Windows + // All notifications are sent immediately as they are queued + // by windows in Tray Notifications and can be bulk seen and + // dismissed + _this.pendingNotifications.shift().show(); + } + }, + _addNotification: function (notification) { + this.pendingNotifications.push(notification); + if (this.pendingNotifications.length === 1) { + this._showNotification(); + } + }, + _pollToDisableHighlight: function (notifyType, error, data) { + if (error || !data.body) { + console.log("Error: unable to get " + url); + return false; + } + var content = JSON.parse(data.body); + if (!content || content.status != 'success') { + console.log("Error: unable to get " + url); + return false; + } + if (!content.total_entries) { + this.menuNotificationCallback(notifyType, false); + } + }, + _pollCommon: function (notifyType, url, since, finished) { + + var _this = this; + IsInterfaceRunning(function (running) { + if (running) { + finished(false); + return; + } + var acctInfo = new AccountInfo(); + var token = acctInfo.accessToken(METAVERSE_SERVER_URL); + if (!token) { + return; + } + request.get({ + uri: url, + 'auth': { + 'bearer': token + } + }, function (error, data) { + + var maxNotificationItemCount = since.getTime() ? MAX_NOTIFICATION_ITEMS : STARTUP_MAX_NOTIFICATION_ITEMS; + if (error || !data.body) { + console.log("Error: unable to get " + url); + finished(false); + return; + } + var content = JSON.parse(data.body); + if (!content || content.status != 'success') { + console.log("Error: unable to get " + url); + finished(false); + return; + } + if (!content.total_entries) { + finished(true, token); + return; + } + _this.menuNotificationCallback(notifyType, true); + if (content.total_entries >= maxNotificationItemCount) { + _this._addNotification(new HifiNotification(notifyType, content.total_entries)); + } else { + var notifyData = [] + switch (notifyType) { + case NotificationType.GOTO: + notifyData = content.user_stories; + break; + case NotificationType.PEOPLE: + notifyData = content.data.users; + break; + case NotificationType.TRANSACTIONS: + notifyData = content.data.history; + break; + case NotificationType.ITEMS: + notifyData = content.data.updates; + break; + } + notifyData.forEach(function (notifyDataEntry) { + _this._addNotification(new HifiNotification(notifyType, notifyDataEntry)); + }); + } + finished(true, token); + }); + }); + }, + pollForStories: function (since) { + var _this = this; + var actions = 'announcement'; + var options = [ + 'since=' + since.getTime() / 1000, + 'include_actions=announcement', + 'restriction=open,hifi', + 'require_online=true', + 'per_page='+MAX_NOTIFICATION_ITEMS + ]; + console.log("Polling for stories"); + var url = METAVERSE_SERVER_URL + STORIES_URL + '?' + options.join('&'); + console.log(url); + + _this._pollCommon(NotificationType.GOTO, + url, + since, + function (success, token) { + if (success) { + var options = [ + 'now=' + new Date().toISOString(), + 'include_actions=announcement', + 'restriction=open,hifi', + 'require_online=true', + 'per_page=' + MAX_NOTIFICATION_ITEMS + ]; + var url = METAVERSE_SERVER_URL + STORIES_URL + '?' + options.join('&'); + // call a second time to determine if there are no more stories and we should + // put out the light. + request.get({ + uri: url, + 'auth': { + 'bearer': token + } + }, function (error, data) { + if (error || !data.body) { + console.log("Error: " + error + ": unable to get " + url); + return; + } + var content = JSON.parse(data.body); + if (!content || content.status != 'success') { + console.log("Error: unable to get " + url); + return; + } + + if (!content.total_entries) { + return; + } + if (!content.total_entries) { + _this.menuNotificationCallback(NotificationType.GOTO, false); + } + _this.currentStories = {}; + content.user_stories.forEach(function (story) { + // only show a single instance of each story location + // in the menu. This may cause issues with domains + // where the story locations are significantly different + // for each story. + _this.currentStories[story.place_name] = story; + }); + _this.menuNotificationCallback(NotificationType.GOTO); + }); + } + }); + }, + pollForConnections: function (since) { + var _this = this; + var _since = since; + IsInterfaceRunning(function (running) { + if (running) { + return; + } + var options = [ + 'filter=connections', + 'status=online', + 'page=1', + 'per_page=' + MAX_NOTIFICATION_ITEMS + ]; + console.log("Polling for connections"); + var url = METAVERSE_SERVER_URL + USERS_URL + '?' + options.join('&'); + console.log(url); + var acctInfo = new AccountInfo(); + var token = acctInfo.accessToken(METAVERSE_SERVER_URL); + if (!token) { + return; + } + request.get({ + uri: url, + 'auth': { + 'bearer': token + } + }, function (error, data) { + // Users is a special case as we keep track of online users locally. + var maxNotificationItemCount = _since.getTime() ? MAX_NOTIFICATION_ITEMS : STARTUP_MAX_NOTIFICATION_ITEMS; + if (error || !data.body) { + console.log("Error: unable to get " + url); + return false; + } + var content = JSON.parse(data.body); + if (!content || content.status != 'success') { + console.log("Error: unable to get " + url); + return false; + } + if (!content.total_entries) { + _this.menuNotificationCallback(NotificationType.PEOPLE, false); + _this.onlineUsers = {}; + return; + } + + var currentUsers = {}; + var newUsers = new Set([]); + content.data.users.forEach(function (user) { + currentUsers[user.username] = user; + if (!(user.username in _this.onlineUsers)) { + newUsers.add(user); + _this.onlineUsers[user.username] = user; + } + }); + _this.onlineUsers = currentUsers; + if (newUsers.size) { + _this.menuNotificationCallback(NotificationType.PEOPLE, true); + } + + if (newUsers.size >= maxNotificationItemCount) { + _this._addNotification(new HifiNotification(NotificationType.PEOPLE, newUsers.size)); + return; + } + newUsers.forEach(function (user) { + _this._addNotification(new HifiNotification(NotificationType.PEOPLE, user)); + }); + }); + }); + }, + pollForEconomicActivity: function (since) { + var _this = this; + var options = [ + 'since=' + since.getTime() / 1000, + 'page=1', + 'per_page=' + 1000 // total_entries is incorrect for wallet queries if results + // aren't all on one page, so grab them all on a single page + // for now. + ]; + console.log("Polling for economic activity"); + var url = METAVERSE_SERVER_URL + ECONOMIC_ACTIVITY_URL + '?' + options.join('&'); + console.log(url); + _this._pollCommon(NotificationType.TRANSACTIONS, url, since, function () {}); + }, + pollForMarketplaceUpdates: function (since) { + var _this = this; + var options = [ + 'since=' + since.getTime() / 1000, + 'page=1', + 'per_page=' + MAX_NOTIFICATION_ITEMS + ]; + console.log("Polling for marketplace update"); + var url = METAVERSE_SERVER_URL + UPDATES_URL + '?' + options.join('&'); + console.log(url); + _this._pollCommon(NotificationType.ITEMS, url, since, function (success, token) { + if (success) { + var options = [ + 'page=1', + 'per_page=1' + ]; + var url = METAVERSE_SERVER_URL + UPDATES_URL + '?' + options.join('&'); + request.get({ + uri: url, + 'auth': { + 'bearer': token + } + }, function (error, data) { + _this._pollToDisableHighlight(NotificationType.ITEMS, error, data); + }); + } + }); + } +}; + +exports.HifiNotifications = HifiNotifications; +exports.NotificationType = NotificationType; \ No newline at end of file diff --git a/server-console/src/modules/hf-process.js b/server-console/src/modules/hf-process.js index 797ee38a0d..7736de0e55 100644 --- a/server-console/src/modules/hf-process.js +++ b/server-console/src/modules/hf-process.js @@ -259,6 +259,26 @@ Process.prototype = extend(Process.prototype, { }; return logs; }, + isRunning: function (done) { + var _command = this.command; + if (os.type == 'Windows_NT') { + childProcess.exec('tasklist /FO CSV', function (err, stdout, stderr) { + var running = false; + stdout.split("\n").forEach(function (line) { + var exeData = line.split(","); + var executable = exeData[0].replace(/\"/g, "").toLowerCase(); + if (executable == _command) { + running = true; + } + }); + done(running); + }); + } else if (os.type == 'Darwin') { + childProcess.exec('ps cax | grep ' + _command, function (err, stdout, stderr) { + done(stdout.length > 0); + }); + } + }, // Events onChildStartError: function(error) { diff --git a/server-console/src/modules/path-finder.js b/server-console/src/modules/path-finder.js index 5ecd2c74ff..a7ccee2631 100644 --- a/server-console/src/modules/path-finder.js +++ b/server-console/src/modules/path-finder.js @@ -1,8 +1,9 @@ var fs = require('fs'); var path = require('path'); +const { app } = require('electron'); function platformExtension(name) { - if (name == "Interface") { + if (name == "interface") { if (process.platform == "darwin") { return ".app/Contents/MacOS/" + name } else if (process.platform == "win32") { @@ -53,9 +54,9 @@ exports.searchPaths = function(name, binaryType, releaseType) { var componentsPath = appPath + "/Contents/MacOS/Components.app/Contents/MacOS/"; paths.push(componentsPath + name + extension); - // check beside the app bundle for the binaries - paths.push(path.join(path.dirname(appPath), name + extension)); } + // check beside the app bundle for the binaries + paths.push(path.join(path.dirname(app.getAppPath()), "../../..", name + extension)); } } diff --git a/tests-manual/controllers/CMakeLists.txt b/tests-manual/controllers/CMakeLists.txt index ce1c150ed4..03043c79f2 100644 --- a/tests-manual/controllers/CMakeLists.txt +++ b/tests-manual/controllers/CMakeLists.txt @@ -11,14 +11,8 @@ setup_memory_debugger() # link in the shared libraries link_hifi_libraries(shared gl script-engine plugins render-utils ui-plugins input-plugins display-plugins controllers) - -if (WIN32) - add_dependency_external_projects(OpenVR) - find_package(OpenVR REQUIRED) - target_include_directories(${TARGET_NAME} PRIVATE ${OPENVR_INCLUDE_DIRS}) - target_link_libraries(${TARGET_NAME} ${OPENVR_LIBRARIES}) -endif() if (WIN32) + target_openvr() target_link_libraries(${TARGET_NAME} Winmm.lib) add_dependency_external_projects(wasapi) endif() diff --git a/tests-manual/entities/src/main.cpp b/tests-manual/entities/src/main.cpp index 43a5d2e48d..e38298ba2c 100644 --- a/tests-manual/entities/src/main.cpp +++ b/tests-manual/entities/src/main.cpp @@ -169,5 +169,3 @@ int main(int argc, char** argv) { qDebug() << (duration / 1000.0f); return 0; } - -#include "main.moc" diff --git a/tests-manual/gpu-textures/CMakeLists.txt b/tests-manual/gpu-textures/CMakeLists.txt index 84f5027411..907690748a 100644 --- a/tests-manual/gpu-textures/CMakeLists.txt +++ b/tests-manual/gpu-textures/CMakeLists.txt @@ -4,7 +4,7 @@ setup_hifi_project(Quick Gui Script) setup_memory_debugger() set_target_properties(${TARGET_NAME} PROPERTIES FOLDER "Tests/manual-tests/") link_hifi_libraries( - shared task networking gl + shared shaders task networking gl ktx gpu octree ${PLATFORM_GL_BACKEND} ) diff --git a/tests-manual/gpu-textures/src/TestTextures.cpp b/tests-manual/gpu-textures/src/TestTextures.cpp index 701e60fab8..5d5ddce6fa 100644 --- a/tests-manual/gpu-textures/src/TestTextures.cpp +++ b/tests-manual/gpu-textures/src/TestTextures.cpp @@ -81,8 +81,10 @@ TexturesTest::TexturesTest() { connect(&stats, &TextureTestStats::prevTexture, this, &TexturesTest::onPrevTexture); connect(&stats, &TextureTestStats::maxTextureMemory, this, &TexturesTest::onMaxTextureMemory); { - auto VS = gpu::Shader::createVertex({ vertexShaderSource, {} }); - auto PS = gpu::Shader::createPixel({ fragmentShaderSource, {} }); + shader::Source vertexSource; + + auto VS = gpu::Shader::createVertex(shader::Source::generate(vertexShaderSource)); + auto PS = gpu::Shader::createPixel(shader::Source::generate(fragmentShaderSource)); auto program = gpu::Shader::createProgram(VS, PS); // If the pipeline did not exist, make it auto state = std::make_shared(); diff --git a/tests-manual/gpu-textures/src/main.cpp b/tests-manual/gpu-textures/src/main.cpp index 3d0051dc1d..d8454c6c22 100644 --- a/tests-manual/gpu-textures/src/main.cpp +++ b/tests-manual/gpu-textures/src/main.cpp @@ -85,14 +85,12 @@ class MyTestWindow : public TestWindow { void updateCamera() { float t = _time.elapsed() * 1e-3f; - glm::vec3 unitscale{ 1.0f }; - glm::vec3 up{ 0.0f, 1.0f, 0.0f }; + static const glm::vec3 up{ 0.0f, 1.0f, 0.0f }; float distance = 3.0f; glm::vec3 camera_position{ distance * sinf(t), 0.5f, distance * cosf(t) }; static const vec3 camera_focus(0); - static const vec3 camera_up(0, 1, 0); _camera = glm::inverse(glm::lookAt(camera_position, camera_focus, up)); ViewFrustum frustum; diff --git a/tests-manual/gpu/CMakeLists.txt b/tests-manual/gpu/CMakeLists.txt index 30218f3f97..2b04bdc3f2 100644 --- a/tests-manual/gpu/CMakeLists.txt +++ b/tests-manual/gpu/CMakeLists.txt @@ -5,8 +5,8 @@ setup_memory_debugger() set_target_properties(${TARGET_NAME} PROPERTIES FOLDER "Tests/manual-tests/") link_hifi_libraries( shared task networking gl - ktx gpu procedural octree image - graphics model-networking fbx animation + ktx shaders gpu procedural octree image + graphics model-networking fbx hfm animation script-engine render render-utils ${PLATFORM_GL_BACKEND} ) diff --git a/tests-manual/gpu/src/TestFbx.cpp b/tests-manual/gpu/src/TestFbx.cpp index 538bb0a973..9253f8bc91 100644 --- a/tests-manual/gpu/src/TestFbx.cpp +++ b/tests-manual/gpu/src/TestFbx.cpp @@ -100,12 +100,12 @@ bool TestFbx::isReady() const { void TestFbx::parseFbx(const QByteArray& fbxData) { QVariantHash mapping; - FBXGeometry* fbx = readFBX(fbxData, mapping); + HFMModel* hfmModel = readFBX(fbxData, mapping); size_t totalVertexCount = 0; size_t totalIndexCount = 0; size_t totalPartCount = 0; size_t highestIndex = 0; - for (const auto& mesh : fbx->meshes) { + for (const auto& mesh : hfmModel->meshes) { size_t vertexCount = mesh.vertices.size(); totalVertexCount += mesh.vertices.size(); highestIndex = std::max(highestIndex, vertexCount); @@ -123,7 +123,7 @@ void TestFbx::parseFbx(const QByteArray& fbxData) { std::vector parts; parts.reserve(totalPartCount); _partCount = totalPartCount; - for (const auto& mesh : fbx->meshes) { + for (const auto& mesh : hfmModel->meshes) { baseVertex = vertices.size(); vec3 color; @@ -133,7 +133,7 @@ void TestFbx::parseFbx(const QByteArray& fbxData) { partIndirect.firstIndex = (uint)indices.size(); partIndirect.baseInstance = (uint)parts.size(); _partTransforms.push_back(mesh.modelTransform); - auto material = fbx->materials[part.materialID]; + auto material = hfmModel->materials[part.materialID]; color = material.diffuseColor; for (auto index : part.quadTrianglesIndices) { indices.push_back(index); @@ -163,7 +163,7 @@ void TestFbx::parseFbx(const QByteArray& fbxData) { _vertexBuffer->append(vertices); _indexBuffer->append(indices); _indirectBuffer->append(parts); - delete fbx; + delete hfmModel; } void TestFbx::renderTest(size_t testId, RenderArgs* args) { diff --git a/tests-manual/gpu/src/TestFbx.h b/tests-manual/gpu/src/TestFbx.h index 391fff1091..9ca2c67c3b 100644 --- a/tests-manual/gpu/src/TestFbx.h +++ b/tests-manual/gpu/src/TestFbx.h @@ -11,8 +11,6 @@ #include -class FBXGeometry; - class TestFbx : public GpuTestBase { size_t _partCount { 0 }; graphics::Material _material; diff --git a/tests-manual/gpu/src/main.cpp b/tests-manual/gpu/src/main.cpp index 41d84ca026..7cf941daea 100644 --- a/tests-manual/gpu/src/main.cpp +++ b/tests-manual/gpu/src/main.cpp @@ -102,14 +102,12 @@ class MyTestWindow : public TestWindow { #ifdef INTERACTIVE t = _time.elapsed() * 1e-3f; #endif - glm::vec3 unitscale { 1.0f }; - glm::vec3 up { 0.0f, 1.0f, 0.0f }; + static const glm::vec3 up { 0.0f, 1.0f, 0.0f }; float distance = 3.0f; glm::vec3 camera_position { distance * sinf(t), 0.5f, distance * cosf(t) }; static const vec3 camera_focus(0); - static const vec3 camera_up(0, 1, 0); _camera = glm::inverse(glm::lookAt(camera_position, camera_focus, up)); ViewFrustum frustum; diff --git a/tests-manual/qml/qml/MacQml.qml b/tests-manual/qml/qml/MacQml.qml new file mode 100644 index 0000000000..bb7e3a0dff --- /dev/null +++ b/tests-manual/qml/qml/MacQml.qml @@ -0,0 +1,77 @@ +/**************************************************************************** +** +** Copyright (C) 2016 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the examples of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:BSD$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** BSD License Usage +** Alternatively, you may use this file under the terms of the BSD license +** as follows: +** +** "Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions are +** met: +** * Redistributions of source code must retain the above copyright +** notice, this list of conditions and the following disclaimer. +** * Redistributions in binary form must reproduce the above copyright +** notice, this list of conditions and the following disclaimer in +** the documentation and/or other materials provided with the +** distribution. +** * Neither the name of The Qt Company Ltd nor the names of its +** contributors may be used to endorse or promote products derived +** from this software without specific prior written permission. +** +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +import QtQuick 2.2 +import QtQuick.Layouts 1.1 +import QtQuick.Dialogs 1.1 +import QtQuick.Controls 1.2 +import QtWebEngine 1.5 + +Item { + width: 640 + height: 480 + + Rectangle { + width: 5 + height: 5 + color: "red" + ColorAnimation on color { loops: Animation.Infinite; from: "red"; to: "yellow"; duration: 1000 } + } + + + WebEngineView { + id: root + url: "https://google.com/" + x: 6; y: 6; + width: parent.width * 0.8 + height: parent.height * 0.8 + + } +} diff --git a/tests-manual/qml/src/MacQml.cpp b/tests-manual/qml/src/MacQml.cpp new file mode 100644 index 0000000000..9c5f91041e --- /dev/null +++ b/tests-manual/qml/src/MacQml.cpp @@ -0,0 +1,60 @@ +#include "MacQml.h" + +#include + +#include + +#include + +using TextureAndFence = hifi::qml::OffscreenSurface::TextureAndFence; + +void MacQml::update() { + auto rootItem =_surface->getRootItem(); + float now = sinf(secTimestampNow()); + rootItem->setProperty("level", fabs(now)); + rootItem->setProperty("muted", now > 0.0f); + rootItem->setProperty("statsValue", rand()); + + // Fetch any new textures + TextureAndFence newTextureAndFence; + if (_surface->fetchTexture(newTextureAndFence)) { + if (_texture != 0) { + auto readFence = _glf.glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0); + glFlush(); + _discardLamdba(_texture, readFence); + } + _texture = newTextureAndFence.first; + _glf.glWaitSync((GLsync)newTextureAndFence.second, 0, GL_TIMEOUT_IGNORED); + } +} + +void MacQml::init() { + Parent::init(); + _glf.glGenFramebuffers(1, &_fbo); + _surface.reset(new hifi::qml::OffscreenSurface()); + //QUrl url =getTestResource("qml/main.qml"); + QUrl url = getTestResource("qml/MacQml.qml"); + hifi::qml::QmlContextObjectCallback callback =[](QQmlContext* context, QQuickItem* item) { + }; + _surface->load(url, callback); + _surface->resize(_window->size()); + _surface->resume(); + +} + +void MacQml::draw() { + auto size = _window->geometry().size(); + if (_texture) { + _glf.glBindFramebuffer(GL_READ_FRAMEBUFFER, _fbo); + _glf.glFramebufferTexture2D(GL_READ_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, _texture, 0); + _glf.glBlitFramebuffer( + // src coordinates + 0, 0, size.width(), size.height(), + // dst coordinates + 0, 0, size.width(), size.height(), + // blit mask and filter + GL_COLOR_BUFFER_BIT, GL_NEAREST); + _glf.glFramebufferTexture2D(GL_READ_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, 0, 0); + _glf.glBindFramebuffer(GL_READ_FRAMEBUFFER, 0); + } +} diff --git a/tests-manual/qml/src/MacQml.h b/tests-manual/qml/src/MacQml.h new file mode 100644 index 0000000000..50f71cb72e --- /dev/null +++ b/tests-manual/qml/src/MacQml.h @@ -0,0 +1,16 @@ +#include "TestCase.h" + +#include + +class MacQml : public TestCase { + using Parent = TestCase; +public: + GLuint _texture{ 0 }; + QmlPtr _surface; + GLuint _fbo{ 0 }; + + MacQml(const QWindow* window) : Parent(window) {} + void update() override; + void init() override; + void draw() override; +}; diff --git a/tests-manual/qml/src/StressWeb.cpp b/tests-manual/qml/src/StressWeb.cpp new file mode 100644 index 0000000000..71293feb9a --- /dev/null +++ b/tests-manual/qml/src/StressWeb.cpp @@ -0,0 +1,131 @@ +#include "StressWeb.h" + +#include + +#include + +using TextureAndFence = hifi::qml::OffscreenSurface::TextureAndFence; + +static const int DEFAULT_MAX_FPS = 10; +static const QString CONTROL_URL{ "/qml/controls/WebEntityView.qml" }; +static const char* URL_PROPERTY{ "url" }; + +QString StressWeb::getSourceUrl(bool video) { + static const std::vector SOURCE_URLS{ + "https://www.reddit.com/wiki/random", + "https://en.wikipedia.org/wiki/Wikipedia:Random", + "https://slashdot.org/", + }; + + static const std::vector VIDEO_SOURCE_URLS{ + "https://www.youtube.com/watch?v=gDXwhHm4GhM", + "https://www.youtube.com/watch?v=Ch_hoYPPeGc", + }; + + const auto& sourceUrls = video ? VIDEO_SOURCE_URLS : SOURCE_URLS; + auto index = rand() % sourceUrls.size(); + return sourceUrls[index]; +} + + + +void StressWeb::buildSurface(QmlInfo& qmlInfo, bool video) { + ++_surfaceCount; + auto lifetimeSecs = (uint32_t)(5.0f + (randFloat() * 10.0f)); + auto lifetimeUsecs = (USECS_PER_SECOND * lifetimeSecs); + qmlInfo.lifetime = lifetimeUsecs + usecTimestampNow(); + qmlInfo.texture = 0; + qmlInfo.surface.reset(new hifi::qml::OffscreenSurface()); + qmlInfo.surface->load(getTestResource(CONTROL_URL), [video](QQmlContext* context, QQuickItem* item) { + item->setProperty(URL_PROPERTY, getSourceUrl(video)); + }); + qmlInfo.surface->setMaxFps(DEFAULT_MAX_FPS); + qmlInfo.surface->resize(_qmlSize); + qmlInfo.surface->resume(); +} + +void StressWeb::destroySurface(QmlInfo& qmlInfo) { + auto& surface = qmlInfo.surface; + auto& currentTexture = qmlInfo.texture; + if (currentTexture) { + auto readFence = _glf.glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0); + glFlush(); + _discardLamdba(currentTexture, readFence); + } + auto webView = surface->getRootItem(); + if (webView) { + // stop loading + QMetaObject::invokeMethod(webView, "stop"); + webView->setProperty(URL_PROPERTY, "about:blank"); + } + surface->pause(); + surface.reset(); +} + +void StressWeb::update() { + auto now = usecTimestampNow(); + // Fetch any new textures + for (size_t x = 0; x < DIVISIONS_X; ++x) { + for (size_t y = 0; y < DIVISIONS_Y; ++y) { + auto& qmlInfo = _surfaces[x][y]; + if (!qmlInfo.surface) { + if (now < _createStopTime && randFloat() > 0.99f) { + buildSurface(qmlInfo, x == 0 && y == 0); + } else { + continue; + } + } + + if (now > qmlInfo.lifetime) { + destroySurface(qmlInfo); + continue; + } + + auto& surface = qmlInfo.surface; + auto& currentTexture = qmlInfo.texture; + + TextureAndFence newTextureAndFence; + if (surface->fetchTexture(newTextureAndFence)) { + if (currentTexture != 0) { + auto readFence = _glf.glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0); + glFlush(); + _discardLamdba(currentTexture, readFence); + } + currentTexture = newTextureAndFence.first; + _glf.glWaitSync((GLsync)newTextureAndFence.second, 0, GL_TIMEOUT_IGNORED); + } + } + } +} + +void StressWeb::init() { + Parent::init(); + _createStopTime = usecTimestampNow() + (3000u * USECS_PER_SECOND); + _glf.glGenFramebuffers(1, &_fbo); +} + +void StressWeb::draw() { + auto size = _window->geometry().size(); + auto incrementX = size.width() / DIVISIONS_X; + auto incrementY = size.height() / DIVISIONS_Y; + + for (uint32_t x = 0; x < DIVISIONS_X; ++x) { + for (uint32_t y = 0; y < DIVISIONS_Y; ++y) { + auto& qmlInfo = _surfaces[x][y]; + if (!qmlInfo.surface || !qmlInfo.texture) { + continue; + } + _glf.glBindFramebuffer(GL_READ_FRAMEBUFFER, _fbo); + _glf.glFramebufferTexture2D(GL_READ_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, qmlInfo.texture, 0); + _glf.glBlitFramebuffer( + // src coordinates + 0, 0, _qmlSize.width() - 1, _qmlSize.height() - 1, + // dst coordinates + incrementX * x, incrementY * y, incrementX * (x + 1), incrementY * (y + 1), + // blit mask and filter + GL_COLOR_BUFFER_BIT, GL_NEAREST); + } + } + _glf.glFramebufferTexture2D(GL_READ_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, 0, 0); + _glf.glBindFramebuffer(GL_READ_FRAMEBUFFER, 0); +} diff --git a/tests-manual/qml/src/StressWeb.h b/tests-manual/qml/src/StressWeb.h new file mode 100644 index 0000000000..a68e34d0c1 --- /dev/null +++ b/tests-manual/qml/src/StressWeb.h @@ -0,0 +1,34 @@ +#include "TestCase.h" + +#include + +#include + +#define DIVISIONS_X 5 +#define DIVISIONS_Y 5 + +class StressWeb : public TestCase { + using Parent = TestCase; +public: + using QmlPtr = QSharedPointer; + + struct QmlInfo { + QmlPtr surface; + GLuint texture{ 0 }; + uint64_t lifetime{ 0 }; + }; + + size_t _surfaceCount{ 0 }; + uint64_t _createStopTime{ 0 }; + const QSize _qmlSize{ 640, 480 }; + std::array, DIVISIONS_X> _surfaces; + GLuint _fbo{ 0 }; + + StressWeb(const QWindow* window) : Parent(window) {} + static QString getSourceUrl(bool video); + void buildSurface(QmlInfo& qmlInfo, bool video); + void destroySurface(QmlInfo& qmlInfo); + void update() override; + void init() override; + void draw() override; +}; diff --git a/tests-manual/qml/src/TestCase.cpp b/tests-manual/qml/src/TestCase.cpp new file mode 100644 index 0000000000..534de71e51 --- /dev/null +++ b/tests-manual/qml/src/TestCase.cpp @@ -0,0 +1,25 @@ +#include "TestCase.h" + +#include +#include + +void TestCase::destroy() { +} +void TestCase::update() { +} + +void TestCase::init() { + _glf.initializeOpenGLFunctions(); + _discardLamdba = hifi::qml::OffscreenSurface::getDiscardLambda(); +} + +QUrl TestCase::getTestResource(const QString& relativePath) { + static QString dir; + if (dir.isEmpty()) { + QDir path(__FILE__); + path.cdUp(); + dir = path.cleanPath(path.absoluteFilePath("../")) + "/"; + qDebug() << "Resources Path: " << dir; + } + return QUrl::fromLocalFile(dir + relativePath); +} diff --git a/tests-manual/qml/src/TestCase.h b/tests-manual/qml/src/TestCase.h new file mode 100644 index 0000000000..191eecb408 --- /dev/null +++ b/tests-manual/qml/src/TestCase.h @@ -0,0 +1,23 @@ +#pragma once + +#include +#include +#include +#include + +class TestCase { +public: + using QmlPtr = QSharedPointer; + using Builder = std::function; + TestCase(const QWindow* window) : _window(window) {} + virtual void init(); + virtual void destroy(); + virtual void update(); + virtual void draw() = 0; + static QUrl getTestResource(const QString& relativePath); + +protected: + QOpenGLFunctions_4_1_Core _glf; + const QWindow* _window; + std::function _discardLamdba; +}; diff --git a/tests-manual/qml/src/main.cpp b/tests-manual/qml/src/main.cpp index d70bb52dde..1d98ebf8c8 100644 --- a/tests-manual/qml/src/main.cpp +++ b/tests-manual/qml/src/main.cpp @@ -43,6 +43,11 @@ #include #include #include +#include +#include + +#include "TestCase.h" +#include "MacQml.h" namespace gl { extern void initModuleGl(); @@ -67,53 +72,37 @@ QUrl getTestResource(const QString& relativePath) { return QUrl::fromLocalFile(dir + relativePath); } -#define DIVISIONS_X 5 -#define DIVISIONS_Y 5 - using QmlPtr = QSharedPointer; using TextureAndFence = hifi::qml::OffscreenSurface::TextureAndFence; -struct QmlInfo { - QmlPtr surface; - GLuint texture{ 0 }; - uint64_t lifetime{ 0 }; -}; - class TestWindow : public QWindow { public: - TestWindow(); + TestWindow(const TestCase::Builder& caseBuilder); private: QOpenGLContext _glContext; OffscreenGLCanvas _sharedContext; - std::array, DIVISIONS_X> _surfaces; + TestCase* _testCase{ nullptr }; QOpenGLFunctions_4_1_Core _glf; - std::function _discardLamdba; QSize _size; - size_t _surfaceCount{ 0 }; - GLuint _fbo{ 0 }; - const QSize _qmlSize{ 640, 480 }; bool _aboutToQuit{ false }; - uint64_t _createStopTime; void initGl(); - void updateSurfaces(); - void buildSurface(QmlInfo& qmlInfo, bool allowVideo); - void destroySurface(QmlInfo& qmlInfo); void resizeWindow(const QSize& size); void draw(); void resizeEvent(QResizeEvent* ev) override; }; -TestWindow::TestWindow() { +TestWindow::TestWindow(const TestCase::Builder& builder) { Setting::init(); + + _testCase = builder(this); setSurfaceType(QSurface::OpenGLSurface); qmlRegisterType("Hifi", 1, 0, "TestItem"); show(); - _createStopTime = usecTimestampNow() + (3000u * USECS_PER_SECOND); resize(QSize(800, 600)); @@ -129,162 +118,84 @@ TestWindow::TestWindow() { }); } +Q_GUI_EXPORT void qt_gl_set_global_share_context(QOpenGLContext *context); +Q_GUI_EXPORT QOpenGLContext *qt_gl_global_share_context(); +OffscreenGLCanvas* _chromiumShareContext{ nullptr}; void TestWindow::initGl() { _glContext.setFormat(format()); + + auto globalShareContext = qt_gl_global_share_context(); + if (globalShareContext) { + _glContext.setShareContext(globalShareContext); + globalShareContext->makeCurrent(this); + gl::Context::setupDebugLogging(globalShareContext); + globalShareContext->doneCurrent(); + } + if (!_glContext.create() || !_glContext.makeCurrent(this)) { qFatal("Unable to intialize Window GL context"); } + gl::Context::setupDebugLogging(&_glContext); gl::initModuleGl(); _glf.initializeOpenGLFunctions(); - _glf.glGenFramebuffers(1, &_fbo); if (!_sharedContext.create(&_glContext) || !_sharedContext.makeCurrent()) { qFatal("Unable to intialize Shared GL context"); } hifi::qml::OffscreenSurface::setSharedContext(_sharedContext.getContext()); - _discardLamdba = hifi::qml::OffscreenSurface::getDiscardLambda(); + + if (!globalShareContext) { + _chromiumShareContext = new OffscreenGLCanvas(); + _chromiumShareContext->setObjectName("ChromiumShareContext"); + _chromiumShareContext->create(&_glContext); + if (!_chromiumShareContext->makeCurrent()) { + qFatal("Unable to make chromium shared context current"); + } + + qt_gl_set_global_share_context(_chromiumShareContext->getContext()); + _chromiumShareContext->doneCurrent(); + } + + // Restore the GL widget context + if (!_glContext.makeCurrent(this)) { + qFatal("Unable to make window context current"); + } + + _testCase->init(); } void TestWindow::resizeWindow(const QSize& size) { _size = size; } -static const int DEFAULT_MAX_FPS = 10; -static const QString CONTROL_URL{ "/qml/controls/WebEntityView.qml" }; -static const char* URL_PROPERTY{ "url" }; - -QString getSourceUrl(bool video) { - static const std::vector SOURCE_URLS{ - "https://www.reddit.com/wiki/random", - "https://en.wikipedia.org/wiki/Wikipedia:Random", - "https://slashdot.org/", - }; - - static const std::vector VIDEO_SOURCE_URLS{ - "https://www.youtube.com/watch?v=gDXwhHm4GhM", - "https://www.youtube.com/watch?v=Ch_hoYPPeGc", - }; - - const auto& sourceUrls = video ? VIDEO_SOURCE_URLS : SOURCE_URLS; - auto index = rand() % sourceUrls.size(); - return sourceUrls[index]; -} - -void TestWindow::buildSurface(QmlInfo& qmlInfo, bool video) { - ++_surfaceCount; - auto lifetimeSecs = (uint32_t)(5.0f + (randFloat() * 10.0f)); - auto lifetimeUsecs = (USECS_PER_SECOND * lifetimeSecs); - qmlInfo.lifetime = lifetimeUsecs + usecTimestampNow(); - qmlInfo.texture = 0; - qmlInfo.surface.reset(new hifi::qml::OffscreenSurface()); - qmlInfo.surface->load(getTestResource(CONTROL_URL), [video](QQmlContext* context, QQuickItem* item) { - item->setProperty(URL_PROPERTY, getSourceUrl(video)); - }); - qmlInfo.surface->setMaxFps(DEFAULT_MAX_FPS); - qmlInfo.surface->resize(_qmlSize); - qmlInfo.surface->resume(); -} - -void TestWindow::destroySurface(QmlInfo& qmlInfo) { - auto& surface = qmlInfo.surface; - auto& currentTexture = qmlInfo.texture; - if (currentTexture) { - auto readFence = _glf.glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0); - glFlush(); - _discardLamdba(currentTexture, readFence); - } - auto webView = surface->getRootItem(); - if (webView) { - // stop loading - QMetaObject::invokeMethod(webView, "stop"); - webView->setProperty(URL_PROPERTY, "about:blank"); - } - surface->pause(); - surface.reset(); -} - -void TestWindow::updateSurfaces() { - auto now = usecTimestampNow(); - // Fetch any new textures - for (size_t x = 0; x < DIVISIONS_X; ++x) { - for (size_t y = 0; y < DIVISIONS_Y; ++y) { - auto& qmlInfo = _surfaces[x][y]; - if (!qmlInfo.surface) { - if (now < _createStopTime && randFloat() > 0.99f) { - buildSurface(qmlInfo, x == 0 && y == 0); - } else { - continue; - } - } - - if (now > qmlInfo.lifetime) { - destroySurface(qmlInfo); - continue; - } - - auto& surface = qmlInfo.surface; - auto& currentTexture = qmlInfo.texture; - - TextureAndFence newTextureAndFence; - if (surface->fetchTexture(newTextureAndFence)) { - if (currentTexture != 0) { - auto readFence = _glf.glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0); - glFlush(); - _discardLamdba(currentTexture, readFence); - } - currentTexture = newTextureAndFence.first; - _glf.glWaitSync((GLsync)newTextureAndFence.second, 0, GL_TIMEOUT_IGNORED); - } - } - } -} - void TestWindow::draw() { if (_aboutToQuit) { return; } - + // Attempting to draw before we're visible and have a valid size will // produce GL errors. if (!isVisible() || _size.width() <= 0 || _size.height() <= 0) { return; } - + static std::once_flag once; std::call_once(once, [&] { initGl(); }); - + if (!_glContext.makeCurrent(this)) { return; } - - updateSurfaces(); - - auto size = this->geometry().size(); - auto incrementX = size.width() / DIVISIONS_X; - auto incrementY = size.height() / DIVISIONS_Y; + + _testCase->update(); + + auto size = geometry().size(); _glf.glViewport(0, 0, size.width(), size.height()); _glf.glClearColor(1, 0, 0, 1); _glf.glClear(GL_COLOR_BUFFER_BIT); - for (uint32_t x = 0; x < DIVISIONS_X; ++x) { - for (uint32_t y = 0; y < DIVISIONS_Y; ++y) { - auto& qmlInfo = _surfaces[x][y]; - if (!qmlInfo.surface || !qmlInfo.texture) { - continue; - } - _glf.glBindFramebuffer(GL_READ_FRAMEBUFFER, _fbo); - _glf.glFramebufferTexture2D(GL_READ_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, qmlInfo.texture, 0); - _glf.glBlitFramebuffer( - // src coordinates - 0, 0, _qmlSize.width() - 1, _qmlSize.height() - 1, - // dst coordinates - incrementX * x, incrementY * y, incrementX * (x + 1), incrementY * (y + 1), - // blit mask and filter - GL_COLOR_BUFFER_BIT, GL_NEAREST); - } - } - _glf.glFramebufferTexture2D(GL_READ_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, 0, 0); - _glf.glBindFramebuffer(GL_READ_FRAMEBUFFER, 0); + + _testCase->draw(); + _glContext.swapBuffers(this); } @@ -292,19 +203,15 @@ void TestWindow::resizeEvent(QResizeEvent* ev) { resizeWindow(ev->size()); } -int main(int argc, char** argv) { - QSurfaceFormat format; - format.setDepthBufferSize(24); - format.setStencilBufferSize(8); +int main(int argc, char** argv) { + auto format = getDefaultOpenGLSurfaceFormat(); format.setVersion(4, 1); - format.setProfile(QSurfaceFormat::OpenGLContextProfile::CoreProfile); - format.setOption(QSurfaceFormat::DebugContext); QSurfaceFormat::setDefaultFormat(format); - // setFormat(format); QGuiApplication app(argc, argv); - TestWindow window; + TestCase::Builder builder = [](const QWindow* window)->TestCase*{ return new MacQml(window); }; + TestWindow window(builder); return app.exec(); } diff --git a/tests-manual/render-perf/CMakeLists.txt b/tests-manual/render-perf/CMakeLists.txt index 0cc3f87bd9..9e7d826d03 100644 --- a/tests-manual/render-perf/CMakeLists.txt +++ b/tests-manual/render-perf/CMakeLists.txt @@ -17,7 +17,7 @@ link_hifi_libraries( ktx image octree shaders gl gpu ${PLATFORM_GL_BACKEND} render render-utils - graphics fbx model-networking graphics-scripting + graphics hfm fbx model-networking graphics-scripting entities entities-renderer audio avatars script-engine physics procedural midi qml ui ${PLATFORM_GL_BACKEND} diff --git a/tests-manual/render-perf/src/main.cpp b/tests-manual/render-perf/src/main.cpp index 9238e0dc1c..357d40cfb4 100644 --- a/tests-manual/render-perf/src/main.cpp +++ b/tests-manual/render-perf/src/main.cpp @@ -659,7 +659,7 @@ private: update(); _initContext.makeCurrent(); - RenderArgs renderArgs(_renderThread._gpuContext, DEFAULT_OCTREE_SIZE_SCALE, 0, RenderArgs::DEFAULT_RENDER_MODE, + RenderArgs renderArgs(_renderThread._gpuContext, DEFAULT_OCTREE_SIZE_SCALE, 0, getPerspectiveAccuracyAngleTan(DEFAULT_OCTREE_SIZE_SCALE, 0), RenderArgs::DEFAULT_RENDER_MODE, RenderArgs::MONO, RenderArgs::RENDER_DEBUG_NONE); QSize windowSize = _size; @@ -1103,5 +1103,3 @@ int main(int argc, char** argv) { app.exec(); return 0; } - -#include "main.moc" diff --git a/tests-manual/render-texture-load/CMakeLists.txt b/tests-manual/render-texture-load/CMakeLists.txt index 36526ecd42..af6c5b44b5 100644 --- a/tests-manual/render-texture-load/CMakeLists.txt +++ b/tests-manual/render-texture-load/CMakeLists.txt @@ -15,7 +15,7 @@ set_target_properties(${TARGET_NAME} PROPERTIES FOLDER "Tests/manual-tests/") link_hifi_libraries( shared task networking octree shaders gl gpu render ktx image animation - graphics fbx model-networking + graphics hfm fbx model-networking render-utils entities entities-renderer audio avatars script-engine physics diff --git a/tests-manual/render-texture-load/src/GLIHelpers.cpp b/tests-manual/render-texture-load/src/GLIHelpers.cpp index be39e7f1c8..abdd648485 100644 --- a/tests-manual/render-texture-load/src/GLIHelpers.cpp +++ b/tests-manual/render-texture-load/src/GLIHelpers.cpp @@ -10,6 +10,7 @@ #include #include +#if 0 #include #include #include @@ -68,3 +69,4 @@ gpu::TexturePointer processTexture(const QString& sourceFile) { // FIXME load the actual KTX texture return gpu::TexturePointer(); } +#endif \ No newline at end of file diff --git a/tests-manual/render-texture-load/src/GLIHelpers.h b/tests-manual/render-texture-load/src/GLIHelpers.h index a5d4f71bb3..b9332d7ba2 100644 --- a/tests-manual/render-texture-load/src/GLIHelpers.h +++ b/tests-manual/render-texture-load/src/GLIHelpers.h @@ -12,9 +12,8 @@ #include #include +#if 0 // Work around for a bug in the MSVC compiler that chokes when you use GLI and Qt headers together. -#define gli glm - #if defined(__clang__) #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wunused-variable" @@ -46,3 +45,5 @@ gpu::TexturePointer processTexture(const QString& file); #endif + +#endif diff --git a/tests-manual/render-utils/CMakeLists.txt b/tests-manual/render-utils/CMakeLists.txt index be75c53f2e..9f575ee8ca 100644 --- a/tests-manual/render-utils/CMakeLists.txt +++ b/tests-manual/render-utils/CMakeLists.txt @@ -8,7 +8,7 @@ set_target_properties(${TARGET_NAME} PROPERTIES FOLDER "Tests/manual-tests/") setup_memory_debugger() # link in the shared libraries -link_hifi_libraries(render-utils gl gpu shared ${PLATFORM_GL_BACKEND}) +link_hifi_libraries(render-utils shaders gl gpu shared ${PLATFORM_GL_BACKEND}) target_link_libraries(${TARGET_NAME} ${CMAKE_THREAD_LIBS_INIT}) if (WIN32) diff --git a/tests-manual/ui/qml/ControlsGalleryWindow.qml b/tests-manual/ui/qml/ControlsGalleryWindow.qml new file mode 100644 index 0000000000..32fd62da36 --- /dev/null +++ b/tests-manual/ui/qml/ControlsGalleryWindow.qml @@ -0,0 +1,14 @@ +import QtQuick 2.0 +import QtQuick.Window 2.3 +import QtQuick.Controls 1.4 +import '../../../scripts/developer/tests' as Tests + +ApplicationWindow { + width: 640 + height: 480 + visible: true + + Tests.ControlsGallery { + anchors.fill: parent + } +} diff --git a/tests-manual/ui/qml/Palettes.qml b/tests-manual/ui/qml/Palettes.qml deleted file mode 100644 index 2bdf6eba8b..0000000000 --- a/tests-manual/ui/qml/Palettes.qml +++ /dev/null @@ -1,150 +0,0 @@ -import QtQuick 2.3 -import QtQuick.Controls 1.2 - -Rectangle { - color: "teal" - height: 512 - width: 192 - SystemPalette { id: sp; colorGroup: SystemPalette.Active } - SystemPalette { id: spi; colorGroup: SystemPalette.Inactive } - SystemPalette { id: spd; colorGroup: SystemPalette.Disabled } - - Column { - anchors.margins: 8 - anchors.fill: parent - spacing: 8 - Row { - width: parent.width - height: 16 - Text { height: parent.height; width: 128; text: "base" } - Rectangle { height: parent.height; width: 16; color: sp.base } - Rectangle { height: parent.height; width: 16; color: spi.base } - Rectangle { height: parent.height; width: 16; color: spd.base } - } - Row { - width: parent.width - height: 16 - Text { height: parent.height; width: 128; text: "alternateBase" } - Rectangle { height: parent.height; width: 16; color: sp.alternateBase } - Rectangle { height: parent.height; width: 16; color: spi.alternateBase } - Rectangle { height: parent.height; width: 16; color: spd.alternateBase } - } - Item { - height: 16 - width:parent.width - } - Row { - width: parent.width - height: 16 - Text { height: parent.height; width: 128; text: "dark" } - Rectangle { height: parent.height; width: 16; color: sp.dark } - Rectangle { height: parent.height; width: 16; color: spi.dark } - Rectangle { height: parent.height; width: 16; color: spd.dark } - } - Row { - width: parent.width - height: 16 - Text { height: parent.height; width: 128; text: "mid" } - Rectangle { height: parent.height; width: 16; color: sp.mid } - Rectangle { height: parent.height; width: 16; color: spi.mid } - Rectangle { height: parent.height; width: 16; color: spd.mid } - } - Row { - width: parent.width - height: 16 - Text { height: parent.height; width: 128; text: "mid light" } - Rectangle { height: parent.height; width: 16; color: sp.midlight } - Rectangle { height: parent.height; width: 16; color: spi.midlight } - Rectangle { height: parent.height; width: 16; color: spd.midlight } - } - Row { - width: parent.width - height: 16 - Text { height: parent.height; width: 128; text: "light" } - Rectangle { height: parent.height; width: 16; color: sp.light} - Rectangle { height: parent.height; width: 16; color: spi.light} - Rectangle { height: parent.height; width: 16; color: spd.light} - } - Row { - width: parent.width - height: 16 - Text { height: parent.height; width: 128; text: "shadow" } - Rectangle { height: parent.height; width: 16; color: sp.shadow} - Rectangle { height: parent.height; width: 16; color: spi.shadow} - Rectangle { height: parent.height; width: 16; color: spd.shadow} - } - Item { - height: 16 - width:parent.width - } - - Row { - width: parent.width - height: 16 - Text { height: parent.height; width: 128; text: "text" } - Rectangle { height: parent.height; width: 16; color: sp.text } - Rectangle { height: parent.height; width: 16; color: spi.text } - Rectangle { height: parent.height; width: 16; color: spd.text } - } - Item { - height: 16 - width:parent.width - } - Row { - width: parent.width - height: 16 - Text { height: parent.height; width: 128; text: "window" } - Rectangle { height: parent.height; width: 16; color: sp.window } - Rectangle { height: parent.height; width: 16; color: spi.window } - Rectangle { height: parent.height; width: 16; color: spd.window } - } - Row { - width: parent.width - height: 16 - Text { height: parent.height; width: 128; text: "window text" } - Rectangle { height: parent.height; width: 16; color: sp.windowText } - Rectangle { height: parent.height; width: 16; color: spi.windowText } - Rectangle { height: parent.height; width: 16; color: spd.windowText } - } - Item { - height: 16 - width:parent.width - } - Row { - width: parent.width - height: 16 - Text { height: parent.height; width: 128; text: "button" } - Rectangle { height: parent.height; width: 16; color: sp.button } - Rectangle { height: parent.height; width: 16; color: spi.button } - Rectangle { height: parent.height; width: 16; color: spd.button } - } - Row { - width: parent.width - height: 16 - Text { height: parent.height; width: 128; text: "buttonText" } - Rectangle { height: parent.height; width: 16; color: sp.buttonText } - Rectangle { height: parent.height; width: 16; color: spi.buttonText } - Rectangle { height: parent.height; width: 16; color: spd.buttonText } - } - Item { - height: 16 - width:parent.width - } - Row { - width: parent.width - height: 16 - Text { height: parent.height; width: 128; text: "highlight" } - Rectangle { height: parent.height; width: 16; color: sp.highlight } - Rectangle { height: parent.height; width: 16; color: spi.highlight } - Rectangle { height: parent.height; width: 16; color: spd.highlight } - } - Row { - width: parent.width - height: 16 - Text { height: parent.height; width: 128; text: "highlighted text" } - Rectangle { height: parent.height; width: 16; color: sp.highlightedText} - Rectangle { height: parent.height; width: 16; color: spi.highlightedText} - Rectangle { height: parent.height; width: 16; color: spd.highlightedText} - } - } -} diff --git a/tests-manual/ui/qml/ScrollingGraph.qml b/tests-manual/ui/qml/ScrollingGraph.qml deleted file mode 100644 index 55523a23f4..0000000000 --- a/tests-manual/ui/qml/ScrollingGraph.qml +++ /dev/null @@ -1,111 +0,0 @@ -import QtQuick 2.1 -import QtQuick.Controls 1.0 -import QtQuick.Layouts 1.0 -import QtQuick.Dialogs 1.0 - -Rectangle { - id: root - property int size: 64 - width: size - height: size - color: 'black' - property int controlId: 0 - property real value: 0.5 - property int scrollWidth: 1 - property real min: 0.0 - property real max: 1.0 - property bool log: false - property real range: max - min - property color lineColor: 'yellow' - property bool bar: false - property real lastHeight: -1 - property string label: "" - - function update() { - value = Controller.getValue(controlId); - if (log) { - var log = Math.log(10) / Math.log(Math.abs(value)); - var sign = Math.sign(value); - value = log * sign; - } - canvas.requestPaint(); - } - - function drawHeight() { - if (value < min) { - return 0; - } - if (value > max) { - return height; - } - return ((value - min) / range) * height; - } - - Timer { - interval: 50; running: true; repeat: true - onTriggered: root.update() - } - - Canvas { - id: canvas - anchors.fill: parent - antialiasing: false - - Text { - anchors.top: parent.top - text: root.label - color: 'white' - } - - Text { - anchors.right: parent.right - anchors.top: parent.top - text: root.max - color: 'white' - } - - Text { - anchors.right: parent.right - anchors.bottom: parent.bottom - text: root.min - color: 'white' - } - - function scroll() { - var ctx = canvas.getContext('2d'); - var image = ctx.getImageData(0, 0, canvas.width, canvas.height); - ctx.beginPath(); - ctx.clearRect(0, 0, canvas.width, canvas.height); - ctx.drawImage(image, -root.scrollWidth, 0, canvas.width, canvas.height) - ctx.restore() - } - - onPaint: { - scroll(); - var ctx = canvas.getContext('2d'); - ctx.save(); - var currentHeight = root.drawHeight(); - if (root.lastHeight == -1) { - root.lastHeight = currentHeight - } - -// var x = canvas.width - root.drawWidth; -// var y = canvas.height - drawHeight; -// ctx.fillStyle = root.color -// ctx.fillRect(x, y, root.drawWidth, root.bar ? drawHeight : 1) -// ctx.fill(); -// ctx.restore() - - - ctx.beginPath(); - ctx.lineWidth = 1 - ctx.strokeStyle = root.lineColor - ctx.moveTo(canvas.width - root.scrollWidth, root.lastHeight).lineTo(canvas.width, currentHeight) - ctx.stroke() - ctx.restore() - root.lastHeight = currentHeight - } - } -} - - diff --git a/tests-manual/ui/qml/StubMenu.qml b/tests-manual/ui/qml/StubMenu.qml deleted file mode 100644 index fd0298988a..0000000000 --- a/tests-manual/ui/qml/StubMenu.qml +++ /dev/null @@ -1,730 +0,0 @@ -import QtQuick 2.5 -import QtQuick.Controls 1.4 - -import "../../../interface/resources/qml/hifi" - -Menu { - property var menuOption: MenuOption {} - Item { - Action { - id: login; - text: menuOption.login - } - Action { - id: update; - text: "Update"; - enabled: false - } - Action { - id: crashReporter; - text: "Crash Reporter..."; - enabled: false - } - Action { - id: help; - text: menuOption.help - onTriggered: Application.showHelp() - } - Action { - id: aboutApp; - text: menuOption.aboutApp - } - Action { - id: quit; - text: menuOption.quit - } - - ExclusiveGroup { id: renderResolutionGroup } - Action { - id: renderResolutionOne; - exclusiveGroup: renderResolutionGroup; - text: menuOption.renderResolutionOne; - checkable: true; - checked: true - } - Action { - id: renderResolutionTwoThird; - exclusiveGroup: renderResolutionGroup; - text: menuOption.renderResolutionTwoThird; - checkable: true - } - Action { - id: renderResolutionHalf; - exclusiveGroup: renderResolutionGroup; - text: menuOption.renderResolutionHalf; - checkable: true - } - Action { - id: renderResolutionThird; - exclusiveGroup: renderResolutionGroup; - text: menuOption.renderResolutionThird; - checkable: true - } - Action { - id: renderResolutionQuarter; - exclusiveGroup: renderResolutionGroup; - text: menuOption.renderResolutionQuarter; - checkable: true - } - - ExclusiveGroup { id: ambientLightGroup } - Action { - id: renderAmbientLightGlobal; - exclusiveGroup: ambientLightGroup; - text: menuOption.renderAmbientLightGlobal; - checkable: true; - checked: true - } - Action { - id: renderAmbientLight0; - exclusiveGroup: ambientLightGroup; - text: menuOption.renderAmbientLight0; - checkable: true; - checked: true - } - Action { - id: renderAmbientLight1; - exclusiveGroup: ambientLightGroup; - text: menuOption.renderAmbientLight1; - checkable: true; - checked: true - } - Action { - id: renderAmbientLight2; - exclusiveGroup: ambientLightGroup; - text: menuOption.renderAmbientLight2; - checkable: true; - checked: true - } - Action { - id: renderAmbientLight3; - exclusiveGroup: ambientLightGroup; - text: menuOption.renderAmbientLight3; - checkable: true; - checked: true - } - Action { - id: renderAmbientLight4; - exclusiveGroup: ambientLightGroup; - text: menuOption.renderAmbientLight4; - checkable: true; - checked: true - } - Action { - id: renderAmbientLight5; - exclusiveGroup: ambientLightGroup; - text: menuOption.renderAmbientLight5; - checkable: true; - checked: true - } - Action { - id: renderAmbientLight6; - exclusiveGroup: ambientLightGroup; - text: menuOption.renderAmbientLight6; - checkable: true; - checked: true - } - Action { - id: renderAmbientLight7; - exclusiveGroup: ambientLightGroup; - text: menuOption.renderAmbientLight7; - checkable: true; - checked: true - } - Action { - id: renderAmbientLight8; - exclusiveGroup: ambientLightGroup; - text: menuOption.renderAmbientLight8; - checkable: true; - checked: true - } - Action { - id: renderAmbientLight9; - exclusiveGroup: ambientLightGroup; - text: menuOption.renderAmbientLight9; - checkable: true; - checked: true - } - Action { - id: preferences - shortcut: StandardKey.Preferences - text: menuOption.preferences - onTriggered: dialogsManager.editPreferences() - } - - } - - Menu { - title: "File" - MenuItem { - action: login - } - MenuItem { - action: update - } - MenuItem { - action: help - } - MenuItem { - action: crashReporter - } - MenuItem { - action: aboutApp - } - MenuItem { - action: quit - } - } - - Menu { - title: "Edit" - MenuItem { - text: "Undo" } - MenuItem { - text: "Redo" } - MenuItem { - text: menuOption.runningScripts - } - MenuItem { - text: menuOption.loadScript - } - MenuItem { - text: menuOption.loadScriptURL - } - MenuItem { - text: menuOption.stopAllScripts - } - MenuItem { - text: menuOption.reloadAllScripts - } - MenuItem { - text: menuOption.scriptEditor - } - MenuItem { - text: menuOption.console_ - } - MenuItem { - text: menuOption.reloadContent - } - MenuItem { - text: menuOption.packageModel - } - } - - Menu { - title: "Audio" - MenuItem { - text: menuOption.muteAudio; - checkable: true - } - MenuItem { - text: menuOption.audioTools; - checkable: true - } - } - Menu { - title: "Avatar" - // Avatar > Attachments... - MenuItem { - text: menuOption.attachments - } - Menu { - title: "Size" - // Avatar > Size > Increase - MenuItem { - text: menuOption.increaseAvatarSize - } - // Avatar > Size > Decrease - MenuItem { - text: menuOption.decreaseAvatarSize - } - // Avatar > Size > Reset - MenuItem { - text: menuOption.resetAvatarSize - } - } - // Avatar > Reset Sensors - MenuItem { - text: menuOption.resetSensors - } - } - Menu { - title: "Display" - } - Menu { - title: "View" - ExclusiveGroup { - id: cameraModeGroup - } - - MenuItem { - text: menuOption.firstPerson; - checkable: true; - exclusiveGroup: cameraModeGroup - } - MenuItem { - text: menuOption.thirdPerson; - checkable: true; - exclusiveGroup: cameraModeGroup - } - MenuItem { - text: menuOption.fullscreenMirror; - checkable: true; - exclusiveGroup: cameraModeGroup - } - MenuItem { - text: menuOption.independentMode; - checkable: true; - exclusiveGroup: cameraModeGroup - } - MenuItem { - text: menuOption.cameraEntityMode; - checkable: true; - exclusiveGroup: cameraModeGroup - } - MenuSeparator{} - MenuItem { - text: menuOption.miniMirror; - checkable: true - } - } - Menu { - title: "Navigate" - MenuItem { - text: "Home" } - MenuItem { - text: menuOption.addressBar - } - MenuItem { - text: "Directory" } - MenuItem { - text: menuOption.copyAddress - } - MenuItem { - text: menuOption.copyPath - } - } - Menu { - title: "Settings" - MenuItem { - text: "Advanced Menus" } - MenuItem { - text: "Developer Menus" } - MenuItem { - text: menuOption.preferences - } - MenuItem { - text: "Avatar..." } - MenuItem { - text: "Audio..." } - MenuItem { - text: "LOD..." } - MenuItem { - text: menuOption.inputMenu - } - } - Menu { - title: "Developer" - Menu { - title: "Render" - MenuItem { - text: menuOption.atmosphere; - checkable: true - } - MenuItem { - text: menuOption.worldAxes; - checkable: true - } - MenuItem { - text: menuOption.debugAmbientOcclusion; - checkable: true - } - MenuItem { - text: menuOption.antialiasing; - checkable: true - } - MenuItem { - text: menuOption.stars; - checkable: true - } - Menu { - title: menuOption.renderAmbientLight - MenuItem { - action: renderAmbientLightGlobal; } - MenuItem { - action: renderAmbientLight0; } - MenuItem { - action: renderAmbientLight1; } - MenuItem { - action: renderAmbientLight2; } - MenuItem { - action: renderAmbientLight3; } - MenuItem { - action: renderAmbientLight4; } - MenuItem { - action: renderAmbientLight5; } - MenuItem { - action: renderAmbientLight6; } - MenuItem { - action: renderAmbientLight7; } - MenuItem { - action: renderAmbientLight8; } - MenuItem { - action: renderAmbientLight9; } - } - MenuItem { - text: menuOption.throttleFPSIfNotFocus; - checkable: true - } - Menu { - title: menuOption.renderResolution - MenuItem { - action: renderResolutionOne - } - MenuItem { - action: renderResolutionTwoThird - } - MenuItem { - action: renderResolutionHalf - } - MenuItem { - action: renderResolutionThird - } - MenuItem { - action: renderResolutionQuarter - } - } - MenuItem { - text: menuOption.lodTools - } - } - Menu { - title: "Assets" - MenuItem { - text: menuOption.uploadAsset - } - MenuItem { - text: menuOption.assetMigration - } - } - Menu { - title: "Avatar" - Menu { - title: "Face Tracking" - MenuItem { - text: menuOption.noFaceTracking; - checkable: true - } - MenuItem { - text: menuOption.faceshift; - checkable: true - } - MenuItem { - text: menuOption.useCamera; - checkable: true - } - MenuSeparator{} - MenuItem { - text: menuOption.binaryEyelidControl; - checkable: true - } - MenuItem { - text: menuOption.coupleEyelids; - checkable: true - } - MenuItem { - text: menuOption.useAudioForMouth; - checkable: true - } - MenuItem { - text: menuOption.velocityFilter; - checkable: true - } - MenuItem { - text: menuOption.calibrateCamera - } - MenuSeparator{} - MenuItem { - text: menuOption.muteFaceTracking; - checkable: true - } - MenuItem { - text: menuOption.autoMuteAudio; - checkable: true - } - } - Menu { - title: "Eye Tracking" - MenuItem { - text: menuOption.sMIEyeTracking; - checkable: true - } - Menu { - title: "Calibrate" - MenuItem { - text: menuOption.onePointCalibration - } - MenuItem { - text: menuOption.threePointCalibration - } - MenuItem { - text: menuOption.fivePointCalibration - } - } - MenuItem { - text: menuOption.simulateEyeTracking; - checkable: true - } - } - MenuItem { - text: menuOption.avatarReceiveStats; - checkable: true - } - MenuItem { - text: menuOption.renderBoundingCollisionShapes; - checkable: true - } - MenuItem { - text: menuOption.renderLookAtVectors; - checkable: true - } - MenuItem { - text: menuOption.renderLookAtTargets; - checkable: true - } - MenuItem { - text: menuOption.renderFocusIndicator; - checkable: true - } - MenuItem { - text: menuOption.showWhosLookingAtMe; - checkable: true - } - MenuItem { - text: menuOption.fixGaze; - checkable: true - } - MenuItem { - text: menuOption.animDebugDrawDefaultPose; - checkable: true - } - MenuItem { - text: menuOption.animDebugDrawAnimPose; - checkable: true - } - MenuItem { - text: menuOption.animDebugDrawPosition; - checkable: true - } - MenuItem { - text: menuOption.meshVisible; - checkable: true - } - MenuItem { - text: menuOption.disableEyelidAdjustment; - checkable: true - } - MenuItem { - text: menuOption.turnWithHead; - checkable: true - } - MenuItem { - text: menuOption.keyboardMotorControl; - checkable: true - } - MenuItem { - text: menuOption.scriptedMotorControl; - checkable: true - } - MenuItem { - text: menuOption.enableCharacterController; - checkable: true - } - } - Menu { - title: "Hands" - MenuItem { - text: menuOption.displayHandTargets; - checkable: true - } - MenuItem { - text: menuOption.lowVelocityFilter; - checkable: true - } - Menu { - title: "Leap Motion" - MenuItem { - text: menuOption.leapMotionOnHMD; - checkable: true - } - } - } - Menu { - title: "Entities" - MenuItem { - text: menuOption.octreeStats - } - MenuItem { - text: menuOption.showRealtimeEntityStats; - checkable: true - } - } - Menu { - title: "Network" - MenuItem { - text: menuOption.reloadContent - } - MenuItem { - text: menuOption.disableNackPackets; - checkable: true - } - MenuItem { - text: menuOption.disableActivityLogger; - checkable: true - } - MenuItem { - text: menuOption.cachesSize - } - MenuItem { - text: menuOption.diskCacheEditor - } - MenuItem { - text: menuOption.showDSConnectTable - } - MenuItem { - text: menuOption.bandwidthDetails - } - } - Menu { - title: "Timing" - Menu { - title: "Performance Timer" - MenuItem { - text: menuOption.displayDebugTimingDetails; - checkable: true - } - MenuItem { - text: menuOption.onlyDisplayTopTen; - checkable: true - } - MenuItem { - text: menuOption.expandUpdateTiming; - checkable: true - } - MenuItem { - text: menuOption.expandMyAvatarTiming; - checkable: true - } - MenuItem { - text: menuOption.expandMyAvatarSimulateTiming; - checkable: true - } - MenuItem { - text: menuOption.expandOtherAvatarTiming; - checkable: true - } - MenuItem { - text: menuOption.expandPaintGLTiming; - checkable: true - } - } - MenuItem { - text: menuOption.frameTimer; - checkable: true - } - MenuItem { - text: menuOption.runTimingTests - } - MenuItem { - text: menuOption.pipelineWarnings; - checkable: true - } - MenuItem { - text: menuOption.logExtraTimings; - checkable: true - } - MenuItem { - text: menuOption.suppressShortTimings; - checkable: true - } - } - Menu { - title: "Audio" - MenuItem { - text: menuOption.audioNoiseReduction; - checkable: true - } - MenuItem { - text: menuOption.echoServerAudio; - checkable: true - } - MenuItem { - text: menuOption.echoLocalAudio; - checkable: true - } - MenuItem { - text: menuOption.muteEnvironment - } - Menu { - title: "Audio" - MenuItem { - text: menuOption.audioScope; - checkable: true - } - MenuItem { - text: menuOption.audioScopePause; - checkable: true - } - Menu { - title: "Display Frames" - ExclusiveGroup { - id: audioScopeFramesGroup - } - MenuItem { - exclusiveGroup: audioScopeFramesGroup; - text: menuOption.audioScopeFiveFrames; - checkable: true - } - MenuItem { - exclusiveGroup: audioScopeFramesGroup; - text: menuOption.audioScopeTwentyFrames; - checkable: true - } - MenuItem { - exclusiveGroup: audioScopeFramesGroup; - text: menuOption.audioScopeFiftyFrames; - checkable: true - } - } - MenuItem { - text: menuOption.audioNetworkStats - } - } - } - Menu { - title: "Physics" - MenuItem { - text: menuOption.physicsShowOwned; - checkable: true - } - MenuItem { - text: menuOption.physicsShowHulls; - checkable: true - } - } - MenuItem { - text: menuOption.displayCrashOptions; - checkable: true - } - MenuItem { - text: menuOption.crashInterface - } - MenuItem { - text: menuOption.log - } - MenuItem { - text: menuOption.stats; - checkable: true - } - } -} diff --git a/tests-manual/ui/qml/Stubs.qml b/tests-manual/ui/qml/Stubs.qml deleted file mode 100644 index 8c1465d54c..0000000000 --- a/tests-manual/ui/qml/Stubs.qml +++ /dev/null @@ -1,346 +0,0 @@ -import QtQuick 2.5 -import QtQuick.Controls 1.4 - -// Stubs for the global service objects set by Interface.cpp when creating the UI -// This is useful for testing inside Qt creator where these services don't actually exist. -Item { - - Item { - objectName: "offscreenFlags" - property bool navigationFocused: false - } - - Item { - objectName: "urlHandler" - function fixupUrl(url) { return url; } - function canHandleUrl(url) { return false; } - function handleUrl(url) { return true; } - } - - Item { - objectName: "Account" - function isLoggedIn() { return true; } - function getUsername() { return "Jherico"; } - } - - Item { - objectName: "GL" - property string vendor: "" - } - - Item { - objectName: "ApplicationCompositor" - property bool reticleOverDesktop: true - } - - Item { - objectName: "Controller" - function getRecommendedOverlayRect() { - return Qt.rect(0, 0, 1920, 1080); - } - } - - Item { - objectName: "Preferences" - // List of categories obtained by logging categories as they are added in Interface in Preferences::addPreference(). - property var categories: [ - "Avatar Basics", "Snapshots", "Scripts", "Privacy", "Level of Detail Tuning", "Avatar Tuning", "Avatar Camera", - "Audio", "Octree", "HMD", "Sixense Controllers", "Graphics" - ] - } - - Item { - objectName: "ScriptDiscoveryService" - //property var scriptsModelFilter: scriptsModel - signal scriptCountChanged() - property var _runningScripts:[ - { name: "wireFrameTest.js", url: "foo/wireframetest.js", path: "foo/wireframetest.js", local: true }, - { name: "edit.js", url: "foo/edit.js", path: "foo/edit.js", local: false }, - { name: "listAllScripts.js", url: "foo/listAllScripts.js", path: "foo/listAllScripts.js", local: false }, - { name: "users.js", url: "foo/users.js", path: "foo/users.js", local: false }, - ] - - function getRunning() { - return _runningScripts; - } - } - - Item { - objectName: "HMD" - property bool active: false - } - - Item { - id: menuHelper - objectName: "MenuHelper" - - Component { - id: modelMaker - ListModel { } - } - - function toModel(menu, parent) { - if (!parent) { parent = menuHelper } - var result = modelMaker.createObject(parent); - if (menu.type !== MenuItemType.Menu) { - console.warn("Not a menu: " + menu); - return result; - } - - var items = menu.items; - for (var i = 0; i < items.length; ++i) { - var item = items[i]; - switch (item.type) { - case 2: - result.append({"name": item.title, "item": item}) - break; - case 1: - result.append({"name": item.text, "item": item}) - break; - case 0: - result.append({"name": "", "item": item}) - break; - } - } - return result; - } - - } - - Item { - objectName: "Desktop" - - property string _OFFSCREEN_ROOT_OBJECT_NAME: "desktopRoot"; - property string _OFFSCREEN_DIALOG_OBJECT_NAME: "topLevelWindow"; - - - function findChild(item, name) { - for (var i = 0; i < item.children.length; ++i) { - if (item.children[i].objectName === name) { - return item.children[i]; - } - } - return null; - } - - function findParent(item, name) { - while (item) { - if (item.objectName === name) { - return item; - } - item = item.parent; - } - return null; - } - - function findDialog(item) { - item = findParent(item, _OFFSCREEN_DIALOG_OBJECT_NAME); - return item; - } - - function closeDialog(item) { - item = findDialog(item); - if (item) { - item.visible = false - } else { - console.warn("Could not find top level dialog") - } - } - - function getDesktop(item) { - while (item) { - if (item.desktopRoot) { - break; - } - item = item.parent; - } - return item - } - - function raise(item) { - var desktop = getDesktop(item); - if (desktop) { - desktop.raise(item); - } - } - } - - Menu { - id: root - objectName: "rootMenu" - - Menu { - title: "Audio" - } - - Menu { - title: "Avatar" - } - - Menu { - title: "Display" - ExclusiveGroup { id: displayMode } - Menu { - title: "More Stuff" - - Menu { title: "Empty" } - - MenuItem { - text: "Do Nothing" - onTriggered: console.log("Nothing") - } - } - MenuItem { - text: "Oculus" - exclusiveGroup: displayMode - checkable: true - } - MenuItem { - text: "OpenVR" - exclusiveGroup: displayMode - checkable: true - } - MenuItem { - text: "OSVR" - exclusiveGroup: displayMode - checkable: true - } - MenuItem { - text: "2D Screen" - exclusiveGroup: displayMode - checkable: true - checked: true - } - MenuItem { - text: "3D Screen (Active)" - exclusiveGroup: displayMode - checkable: true - } - MenuItem { - text: "3D Screen (Passive)" - exclusiveGroup: displayMode - checkable: true - } - } - - Menu { - title: "View" - Menu { - title: "Camera Mode" - ExclusiveGroup { id: cameraMode } - MenuItem { - exclusiveGroup: cameraMode - text: "First Person"; - onTriggered: console.log(text + " checked " + checked) - checkable: true - checked: true - } - MenuItem { - exclusiveGroup: cameraMode - text: "Third Person"; - onTriggered: console.log(text) - checkable: true - } - MenuItem { - exclusiveGroup: cameraMode - text: "Independent Mode"; - onTriggered: console.log(text) - checkable: true - } - MenuItem { - exclusiveGroup: cameraMode - text: "Entity Mode"; - onTriggered: console.log(text) - enabled: false - checkable: true - } - MenuItem { - exclusiveGroup: cameraMode - text: "Fullscreen Mirror"; - onTriggered: console.log(text) - checkable: true - } - } - } - - Menu { - title: "Edit" - - MenuItem { - text: "Undo" - shortcut: "Ctrl+Z" - enabled: false - onTriggered: console.log(text) - } - - MenuItem { - text: "Redo" - shortcut: "Ctrl+Shift+Z" - enabled: false - onTriggered: console.log(text) - } - - MenuSeparator { } - - MenuItem { - text: "Cut" - shortcut: "Ctrl+X" - onTriggered: console.log(text) - } - - MenuItem { - text: "Copy" - shortcut: "Ctrl+C" - onTriggered: console.log(text) - } - - MenuItem { - text: "Paste" - shortcut: "Ctrl+V" - visible: false - onTriggered: console.log("Paste") - } - } - - Menu { - title: "Navigate" - } - - Menu { - title: "Market" - } - - Menu { - title: "Settings" - } - - Menu { - title: "Developer" - } - - Menu { - title: "Quit" - } - - Menu { - title: "File" - - Action { - id: login - text: "Login" - } - - Action { - id: quit - text: "Quit" - shortcut: "Ctrl+Q" - onTriggered: Qt.quit(); - } - - MenuItem { action: quit } - MenuItem { action: login } - } - } - -} - diff --git a/tests-manual/ui/qml/TestControllers.qml b/tests-manual/ui/qml/TestControllers.qml deleted file mode 100644 index e9a7fb49e5..0000000000 --- a/tests-manual/ui/qml/TestControllers.qml +++ /dev/null @@ -1,160 +0,0 @@ -import QtQuick 2.3 -import QtQuick.Controls 1.2 -import QtQuick.Layouts 1.0 -import QtQuick.Dialogs 1.0 - -import "controller" -import "controls" as HifiControls -import "styles" - -HifiControls.VrDialog { - id: root - HifiConstants { id: hifi } - title: "Controller Test" - resizable: true - contentImplicitWidth: clientArea.implicitWidth - contentImplicitHeight: clientArea.implicitHeight - backgroundColor: "beige" - - property var actions: Controller.Actions - property var standard: Controller.Standard - property var hydra: null - property var testMapping: null - property bool testMappingEnabled: false - property var xbox: null - - function buildMapping() { - testMapping = Controller.newMapping(); - testMapping.fromQml(standard.RY).invert().toQml(actions.Pitch); - testMapping.fromQml(function(){ - return Math.sin(Date.now() / 250); - }).toQml(actions.Yaw); - //testMapping.makeAxis(standard.LB, standard.RB).to(actions.Yaw); - // Step yaw takes a number of degrees - testMapping.fromQml(standard.LB).pulse(0.10).invert().scale(40.0).toQml(actions.StepYaw); - testMapping.fromQml(standard.RB).pulse(0.10).scale(15.0).toQml(actions.StepYaw); - testMapping.fromQml(standard.RX).scale(15.0).toQml(actions.StepYaw); - } - - function toggleMapping() { - testMapping.enable(!testMappingEnabled); - testMappingEnabled = !testMappingEnabled; - } - - Component.onCompleted: { - var xboxRegex = /^GamePad/; - var hydraRegex = /^Hydra/; - for (var prop in Controller.Hardware) { - if(xboxRegex.test(prop)) { - root.xbox = Controller.Hardware[prop] - print("found xbox") - continue - } - if (hydraRegex.test(prop)) { - root.hydra = Controller.Hardware[prop] - print("found hydra") - continue - } - } - } - - Column { - id: clientArea - spacing: 12 - x: root.clientX - y: root.clientY - - Row { - spacing: 8 - - Button { - text: !root.testMapping ? "Build Mapping" : (root.testMappingEnabled ? "Disable Mapping" : "Enable Mapping") - onClicked: { - - if (!root.testMapping) { - root.buildMapping() - } else { - root.toggleMapping(); - } - } - } - } - - Row { - Standard { device: root.standard; label: "Standard"; width: 180 } - } - - Row { - spacing: 8 - Xbox { device: root.xbox; label: "XBox"; width: 180 } - Hydra { device: root.hydra; width: 180 } - } - - Row { - spacing: 4 - ScrollingGraph { - controlId: Controller.Actions.Yaw - label: "Yaw" - min: -2.0 - max: 2.0 - size: 64 - } - - ScrollingGraph { - controlId: Controller.Actions.YawLeft - label: "Yaw Left" - min: -2.0 - max: 2.0 - size: 64 - } - - ScrollingGraph { - controlId: Controller.Actions.YawRight - label: "Yaw Right" - min: -2.0 - max: 2.0 - size: 64 - } - - ScrollingGraph { - controlId: Controller.Actions.StepYaw - label: "StepYaw" - min: -20.0 - max: 20.0 - size: 64 - } - } - - Row { - ScrollingGraph { - controlId: Controller.Actions.TranslateZ - label: "TranslateZ" - min: -2.0 - max: 2.0 - size: 64 - } - - ScrollingGraph { - controlId: Controller.Actions.Forward - label: "Forward" - min: -2.0 - max: 2.0 - size: 64 - } - - ScrollingGraph { - controlId: Controller.Actions.Backward - label: "Backward" - min: -2.0 - max: 2.0 - size: 64 - } - - } - } -} // dialog - - - - - diff --git a/tests-manual/ui/qml/TestDialog.qml b/tests-manual/ui/qml/TestDialog.qml deleted file mode 100644 index e6675b7282..0000000000 --- a/tests-manual/ui/qml/TestDialog.qml +++ /dev/null @@ -1,94 +0,0 @@ -import QtQuick 2.3 -import QtQuick.Controls 1.2 -import QtQuick.Controls.Styles 1.3 -import "controls" - -VrDialog { - title: "Test Dialog" - id: testDialog - objectName: "TestDialog" - width: 512 - height: 512 - animationDuration: 200 - - onEnabledChanged: { - if (enabled) { - edit.forceActiveFocus(); - } - } - - Item { - id: clientArea - // The client area - anchors.fill: parent - anchors.margins: parent.margins - anchors.topMargin: parent.topMargin - - Rectangle { - property int d: 100 - id: square - objectName: "testRect" - width: d - height: d - anchors.centerIn: parent - color: "red" - NumberAnimation on rotation { from: 0; to: 360; duration: 2000; loops: Animation.Infinite; } - } - - - TextEdit { - id: edit - anchors.left: parent.left - anchors.leftMargin: 12 - anchors.right: parent.right - anchors.rightMargin: 12 - clip: true - text: "test edit" - anchors.top: parent.top - anchors.topMargin: 12 - } - - Button { - x: 128 - y: 192 - text: "Test" - anchors.bottom: parent.bottom - anchors.bottomMargin: 12 - anchors.right: parent.right - anchors.rightMargin: 12 - onClicked: { - console.log("Click"); - - if (square.visible) { - square.visible = false - } else { - square.visible = true - } - } - } - - Button { - id: customButton2 - y: 192 - text: "Move" - anchors.left: parent.left - anchors.leftMargin: 12 - anchors.bottom: parent.bottom - anchors.bottomMargin: 12 - onClicked: { - onClicked: testDialog.x == 0 ? testDialog.x = 200 : testDialog.x = 0 - } - } - - Keys.onPressed: { - console.log("Key " + event.key); - switch (event.key) { - case Qt.Key_Q: - if (Qt.ControlModifier == event.modifiers) { - event.accepted = true; - break; - } - } - } - } -} diff --git a/tests-manual/ui/qml/TestMenu.qml b/tests-manual/ui/qml/TestMenu.qml deleted file mode 100644 index fe8a26e234..0000000000 --- a/tests-manual/ui/qml/TestMenu.qml +++ /dev/null @@ -1,10 +0,0 @@ -import QtQuick 2.4 -import QtQuick.Controls 1.3 -import Hifi 1.0 - -// Currently for testing a pure QML replacement menu -Item { - Menu { - objectName: "rootMenu"; - } -} diff --git a/tests-manual/ui/qml/TestRoot.qml b/tests-manual/ui/qml/TestRoot.qml deleted file mode 100644 index bd38c696bf..0000000000 --- a/tests-manual/ui/qml/TestRoot.qml +++ /dev/null @@ -1,43 +0,0 @@ -import Hifi 1.0 -import QtQuick 2.3 -import QtQuick.Controls 1.3 -// Import local folder last so that our own control customizations override -// the built in ones -import "controls" - -Root { - id: root - anchors.fill: parent - onParentChanged: { - forceActiveFocus(); - } - Button { - id: messageBox - anchors.right: createDialog.left - anchors.rightMargin: 24 - anchors.bottom: parent.bottom - anchors.bottomMargin: 24 - text: "Message" - onClicked: { - console.log("Foo") - root.information("a") - console.log("Bar") - } - } - Button { - id: createDialog - anchors.right: parent.right - anchors.rightMargin: 24 - anchors.bottom: parent.bottom - anchors.bottomMargin: 24 - text: "Create" - onClicked: { - root.loadChild("MenuTest.qml"); - } - } - - Keys.onPressed: { - console.log("Key press root") - } -} - diff --git a/tests-manual/ui/qml/controlDemo/ButtonPage.qml b/tests-manual/ui/qml/controlDemo/ButtonPage.qml deleted file mode 100644 index 0ed7e2d6ad..0000000000 --- a/tests-manual/ui/qml/controlDemo/ButtonPage.qml +++ /dev/null @@ -1,128 +0,0 @@ -/**************************************************************************** -** -** Copyright (C) 2014 Digia Plc and/or its subsidiary(-ies). -** Contact: http://www.qt-project.org/legal -** -** This file is part of the Qt Quick Controls module of the Qt Toolkit. -** -** $QT_BEGIN_LICENSE:BSD$ -** You may use this file under the terms of the BSD license as follows: -** -** "Redistribution and use in source and binary forms, with or without -** modification, are permitted provided that the following conditions are -** met: -** * Redistributions of source code must retain the above copyright -** notice, this list of conditions and the following disclaimer. -** * Redistributions in binary form must reproduce the above copyright -** notice, this list of conditions and the following disclaimer in -** the documentation and/or other materials provided with the -** distribution. -** * Neither the name of Digia Plc and its Subsidiary(-ies) nor the names -** of its contributors may be used to endorse or promote products derived -** from this software without specific prior written permission. -** -** -** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." -** -** $QT_END_LICENSE$ -** -****************************************************************************/ - -import QtQuick 2.2 -import QtQuick.Layouts 1.1 -import QtQuick.Controls 1.2 - -ScrollView { - id: page - - horizontalScrollBarPolicy: Qt.ScrollBarAlwaysOff - - Item { - id: content - - width: Math.max(page.viewport.width, grid.implicitWidth + 2 * grid.rowSpacing) - height: Math.max(page.viewport.height, grid.implicitHeight + 2 * grid.columnSpacing) - - GridLayout { - id: grid - - anchors.top: parent.top - anchors.left: parent.left - anchors.right: parent.right - anchors.leftMargin: grid.rowSpacing - anchors.rightMargin: grid.rowSpacing - anchors.topMargin: grid.columnSpacing - - columns: page.width < page.height ? 1 : 2 - - GroupBox { - title: "Button" - Layout.fillWidth: true - Layout.columnSpan: grid.columns - RowLayout { - anchors.fill: parent - Button { text: "OK"; isDefault: true } - Button { text: "Cancel" } - Item { Layout.fillWidth: true } - Button { - text: "Attach" - menu: Menu { - MenuItem { text: "Image" } - MenuItem { text: "Document" } - } - } - } - } - - GroupBox { - title: "CheckBox" - Layout.fillWidth: true - ColumnLayout { - anchors.fill: parent - CheckBox { text: "E-mail"; checked: true } - CheckBox { text: "Calendar"; checked: true } - CheckBox { text: "Contacts" } - } - } - - GroupBox { - title: "RadioButton" - Layout.fillWidth: true - ColumnLayout { - anchors.fill: parent - ExclusiveGroup { id: radioGroup } - RadioButton { text: "Portrait"; exclusiveGroup: radioGroup } - RadioButton { text: "Landscape"; exclusiveGroup: radioGroup } - RadioButton { text: "Automatic"; exclusiveGroup: radioGroup; checked: true } - } - } - - GroupBox { - title: "Switch" - Layout.fillWidth: true - Layout.columnSpan: grid.columns - ColumnLayout { - anchors.fill: parent - RowLayout { - Label { text: "Wi-Fi"; Layout.fillWidth: true } - Switch { checked: true } - } - RowLayout { - Label { text: "Bluetooth"; Layout.fillWidth: true } - Switch { checked: false } - } - } - } - } - } -} diff --git a/tests-manual/ui/qml/controlDemo/InputPage.qml b/tests-manual/ui/qml/controlDemo/InputPage.qml deleted file mode 100644 index cb1878d023..0000000000 --- a/tests-manual/ui/qml/controlDemo/InputPage.qml +++ /dev/null @@ -1,114 +0,0 @@ -/**************************************************************************** -** -** Copyright (C) 2014 Digia Plc and/or its subsidiary(-ies). -** Contact: http://www.qt-project.org/legal -** -** This file is part of the Qt Quick Controls module of the Qt Toolkit. -** -** $QT_BEGIN_LICENSE:BSD$ -** You may use this file under the terms of the BSD license as follows: -** -** "Redistribution and use in source and binary forms, with or without -** modification, are permitted provided that the following conditions are -** met: -** * Redistributions of source code must retain the above copyright -** notice, this list of conditions and the following disclaimer. -** * Redistributions in binary form must reproduce the above copyright -** notice, this list of conditions and the following disclaimer in -** the documentation and/or other materials provided with the -** distribution. -** * Neither the name of Digia Plc and its Subsidiary(-ies) nor the names -** of its contributors may be used to endorse or promote products derived -** from this software without specific prior written permission. -** -** -** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." -** -** $QT_END_LICENSE$ -** -****************************************************************************/ - -import QtQuick 2.2 -import QtQuick.Layouts 1.1 -import QtQuick.Controls 1.2 - -ScrollView { - id: page - - horizontalScrollBarPolicy: Qt.ScrollBarAlwaysOff - - Item { - id: content - - width: Math.max(page.viewport.width, column.implicitWidth + 2 * column.spacing) - height: Math.max(page.viewport.height, column.implicitHeight + 2 * column.spacing) - - ColumnLayout { - id: column - - anchors.top: parent.top - anchors.left: parent.left - anchors.right: parent.right - anchors.margins: column.spacing - - GroupBox { - title: "TextField" - Layout.fillWidth: true - ColumnLayout { - anchors.fill: parent - TextField { placeholderText: "..."; Layout.fillWidth: true; z: 1 } - TextField { placeholderText: "Password"; echoMode: TextInput.Password; Layout.fillWidth: true } - } - } - - GroupBox { - title: "ComboBox" - Layout.fillWidth: true - ColumnLayout { - anchors.fill: parent - ComboBox { - model: Qt.fontFamilies() - Layout.fillWidth: true - } - ComboBox { - editable: true - model: ListModel { - id: listModel - ListElement { text: "Apple" } - ListElement { text: "Banana" } - ListElement { text: "Coconut" } - ListElement { text: "Orange" } - } - onAccepted: { - if (find(currentText) === -1) { - listModel.append({text: editText}) - currentIndex = find(editText) - } - } - Layout.fillWidth: true - } - } - } - - GroupBox { - title: "SpinBox" - Layout.fillWidth: true - ColumnLayout { - anchors.fill: parent - SpinBox { value: 99; Layout.fillWidth: true; z: 1 } - SpinBox { decimals: 2; Layout.fillWidth: true } - } - } - } - } -} diff --git a/tests-manual/ui/qml/controlDemo/ProgressPage.qml b/tests-manual/ui/qml/controlDemo/ProgressPage.qml deleted file mode 100644 index a1fa596f79..0000000000 --- a/tests-manual/ui/qml/controlDemo/ProgressPage.qml +++ /dev/null @@ -1,90 +0,0 @@ -/**************************************************************************** -** -** Copyright (C) 2014 Digia Plc and/or its subsidiary(-ies). -** Contact: http://www.qt-project.org/legal -** -** This file is part of the Qt Quick Controls module of the Qt Toolkit. -** -** $QT_BEGIN_LICENSE:BSD$ -** You may use this file under the terms of the BSD license as follows: -** -** "Redistribution and use in source and binary forms, with or without -** modification, are permitted provided that the following conditions are -** met: -** * Redistributions of source code must retain the above copyright -** notice, this list of conditions and the following disclaimer. -** * Redistributions in binary form must reproduce the above copyright -** notice, this list of conditions and the following disclaimer in -** the documentation and/or other materials provided with the -** distribution. -** * Neither the name of Digia Plc and its Subsidiary(-ies) nor the names -** of its contributors may be used to endorse or promote products derived -** from this software without specific prior written permission. -** -** -** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." -** -** $QT_END_LICENSE$ -** -****************************************************************************/ - -import QtQuick 2.2 -import QtQuick.Layouts 1.1 -import QtQuick.Controls 1.2 - -ScrollView { - id: page - - horizontalScrollBarPolicy: Qt.ScrollBarAlwaysOff - - Item { - id: content - - width: Math.max(page.viewport.width, column.implicitWidth + 2 * column.spacing) - height: Math.max(page.viewport.height, column.implicitHeight + 2 * column.spacing) - - ColumnLayout { - id: column - - anchors.top: parent.top - anchors.left: parent.left - anchors.right: parent.right - anchors.margins: column.spacing - - GroupBox { - title: "ProgressBar" - Layout.fillWidth: true - ColumnLayout { - anchors.fill: parent - ProgressBar { indeterminate: true; Layout.fillWidth: true } - ProgressBar { value: slider.value; Layout.fillWidth: true } - } - } - - GroupBox { - title: "Slider" - Layout.fillWidth: true - ColumnLayout { - anchors.fill: parent - Slider { id: slider; value: 0.5; Layout.fillWidth: true } - } - } - - GroupBox { - title: "BusyIndicator" - Layout.fillWidth: true - BusyIndicator { running: true } - } - } - } -} diff --git a/tests-manual/ui/qml/controlDemo/main.qml b/tests-manual/ui/qml/controlDemo/main.qml deleted file mode 100644 index 168b9fb291..0000000000 --- a/tests-manual/ui/qml/controlDemo/main.qml +++ /dev/null @@ -1,161 +0,0 @@ -/**************************************************************************** -** -** Copyright (C) 2014 Digia Plc and/or its subsidiary(-ies). -** Contact: http://www.qt-project.org/legal -** -** This file is part of the Qt Quick Controls module of the Qt Toolkit. -** -** $QT_BEGIN_LICENSE:BSD$ -** You may use this file under the terms of the BSD license as follows: -** -** "Redistribution and use in source and binary forms, with or without -** modification, are permitted provided that the following conditions are -** met: -** * Redistributions of source code must retain the above copyright -** notice, this list of conditions and the following disclaimer. -** * Redistributions in binary form must reproduce the above copyright -** notice, this list of conditions and the following disclaimer in -** the documentation and/or other materials provided with the -** distribution. -** * Neither the name of Digia Plc and its Subsidiary(-ies) nor the names -** of its contributors may be used to endorse or promote products derived -** from this software without specific prior written permission. -** -** -** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." -** -** $QT_END_LICENSE$ -** -****************************************************************************/ - -import QtQuick 2.2 -import QtQuick.Layouts 1.1 -import QtQuick.Dialogs 1.1 -import QtQuick.Controls 1.2 -import "qml/UI.js" as UI -import "qml" -//import "/Users/bdavis/Git/hifi/interface/resources/qml" - -Item { - anchors.fill: parent - visible: true - //title: "Qt Quick Controls Gallery" - - MessageDialog { - id: aboutDialog - icon: StandardIcon.Information - title: "About" - text: "Qt Quick Controls Gallery" - informativeText: "This example demonstrates most of the available Qt Quick Controls." - } - - Action { - id: copyAction - text: "&Copy" - shortcut: StandardKey.Copy - iconName: "edit-copy" - enabled: (!!activeFocusItem && !!activeFocusItem["copy"]) - onTriggered: activeFocusItem.copy() - } - - Action { - id: cutAction - text: "Cu&t" - shortcut: StandardKey.Cut - iconName: "edit-cut" - enabled: (!!activeFocusItem && !!activeFocusItem["cut"]) - onTriggered: activeFocusItem.cut() - } - - Action { - id: pasteAction - text: "&Paste" - shortcut: StandardKey.Paste - iconName: "edit-paste" - enabled: (!!activeFocusItem && !!activeFocusItem["paste"]) - onTriggered: activeFocusItem.paste() - } - -// toolBar: ToolBar { -// RowLayout { -// anchors.fill: parent -// anchors.margins: spacing -// Label { -// text: UI.label -// } -// Item { Layout.fillWidth: true } -// CheckBox { -// id: enabler -// text: "Enabled" -// checked: true -// } -// } -// } - -// menuBar: MenuBar { -// Menu { -// title: "&File" -// MenuItem { -// text: "E&xit" -// shortcut: StandardKey.Quit -// onTriggered: Qt.quit() -// } -// } -// Menu { -// title: "&Edit" -// visible: tabView.currentIndex == 2 -// MenuItem { action: cutAction } -// MenuItem { action: copyAction } -// MenuItem { action: pasteAction } -// } -// Menu { -// title: "&Help" -// MenuItem { -// text: "About..." -// onTriggered: aboutDialog.open() -// } -// } -// } - - TabView { - id: tabView - - anchors.fill: parent - anchors.margins: UI.margin - tabPosition: UI.tabPosition - - Layout.minimumWidth: 360 - Layout.minimumHeight: 360 - Layout.preferredWidth: 480 - Layout.preferredHeight: 640 - - Tab { - title: "Buttons" - ButtonPage { - enabled: enabler.checked - } - } - Tab { - title: "Progress" - ProgressPage { - enabled: enabler.checked - } - } - Tab { - title: "Input" - InputPage { - enabled: enabler.checked - } - } - } -} diff --git a/tests-manual/ui/qml/main.qml b/tests-manual/ui/qml/main.qml deleted file mode 100644 index 607bd624a1..0000000000 --- a/tests-manual/ui/qml/main.qml +++ /dev/null @@ -1,461 +0,0 @@ -import QtQuick 2.5 -import QtQuick.Controls 1.4 -import QtQuick.Dialogs 1.2 as OriginalDialogs -import Qt.labs.settings 1.0 - -import "../../../interface/resources/qml" -//import "../../../interface/resources/qml/windows" -import "../../../interface/resources/qml/windows" -import "../../../interface/resources/qml/dialogs" -import "../../../interface/resources/qml/hifi" -import "../../../interface/resources/qml/hifi/dialogs" -import "../../../interface/resources/qml/styles-uit" - -ApplicationWindow { - id: appWindow - objectName: "MainWindow" - visible: true - width: 1280 - height: 800 - title: qsTr("Scratch App") - toolBar: Row { - id: testButtons - anchors { margins: 8; left: parent.left; top: parent.top } - spacing: 8 - property int count: 0 - - property var tabs: []; - property var urls: []; - property var toolbar; - property var lastButton; - - Button { - text: HMD.active ? "Disable HMD" : "Enable HMD" - onClicked: HMD.active = !HMD.active - } - - Button { - text: desktop.hmdHandMouseActive ? "Disable HMD HandMouse" : "Enable HMD HandMouse" - onClicked: desktop.hmdHandMouseActive = !desktop.hmdHandMouseActive - } - - // Window visibility - Button { - text: "toggle desktop" - onClicked: desktop.togglePinned() - } - - Button { - text: "Create Toolbar" - onClicked: testButtons.toolbar = desktop.getToolbar("com.highfidelity.interface.toolbar.system"); - } - - Button { - text: "Toggle Toolbar Direction" - onClicked: testButtons.toolbar.horizontal = !testButtons.toolbar.horizontal - } - - Button { - readonly property var icons: [ - "edit-01.svg", - "model-01.svg", - "cube-01.svg", - "sphere-01.svg", - "light-01.svg", - "text-01.svg", - "web-01.svg", - "zone-01.svg", - "particle-01.svg", - ] - property int iconIndex: 0 - readonly property string toolIconUrl: "../../../../../scripts/system/assets/images/tools/" - text: "Create Button" - onClicked: { - var name = icons[iconIndex]; - var url = toolIconUrl + name; - iconIndex = (iconIndex + 1) % icons.length; - var button = testButtons.lastButton = testButtons.toolbar.addButton({ - imageURL: url, - objectName: name, - subImage: { - y: 50, - }, - alpha: 0.9 - }); - - button.clicked.connect(function(){ - console.log("Clicked on button " + button.imageURL + " alpha " + button.alpha) - }); - } - } - - Button { - text: "Toggle Button Visible" - onClicked: testButtons.lastButton.visible = !testButtons.lastButton.visible - } - - // Error alerts - /* - Button { - // Message without title. - text: "Show Error" - onClicked: { - var messageBox = desktop.messageBox({ - text: "Diagnostic cycle will be complete in 30 seconds", - icon: hifi.icons.critical, - }); - messageBox.selected.connect(function(button) { - console.log("You clicked " + button) - }) - } - } - Button { - // detailedText is not currently used anywhere in Interface but it is easier to leave in and style good enough. - text: "Show Long Error" - onClicked: { - desktop.messageBox({ - informativeText: "Diagnostic cycle will be complete in 30 seconds Diagnostic cycle will be complete in 30 seconds Diagnostic cycle will be complete in 30 seconds Diagnostic cycle will be complete in 30 seconds Diagnostic cycle will be complete in 30 seconds Diagnostic cycle will be complete in 30 seconds Diagnostic cycle will be complete in 30 seconds Diagnostic cycle will be complete in 30 seconds ", - text: "Baloney", - icon: hifi.icons.warning, - detailedText: "sakjd;laskj dksa;dl jka;lsd j;lkjas ;dlkaj s;dlakjd ;alkjda; slkjda; lkjda;lksjd ;alksjd; alksjd ;alksjd; alksjd; alksdjas;ldkjas;lkdja ;kj ;lkasjd; lkj as;dlka jsd;lka jsd;laksjd a" - }); - } - } - */ - - // query - /* - // There is no such desktop.queryBox() function; may need to update test to cover QueryDialog.qml? - Button { - text: "Show Query" - onClicked: { - var queryBox = desktop.queryBox({ - text: "Have you stopped beating your wife?", - placeholderText: "Are you sure?", - // icon: hifi.icons.critical, - }); - queryBox.selected.connect(function(result) { - console.log("User responded with " + result); - }); - - queryBox.canceled.connect(function() { - console.log("User cancelled query box "); - }) - } - } - */ - - // Browser - /* - Button { - text: "Open Browser" - onClicked: builder.createObject(desktop); - property var builder: Component { - Browser {} - } - } - */ - - - // file dialog - /* - - Button { - text: "Open Directory" - property var builder: Component { - FileDialog { selectDirectory: true } - } - - onClicked: { - var fileDialog = builder.createObject(desktop); - fileDialog.canceled.connect(function(){ - console.log("Cancelled") - }) - fileDialog.selectedFile.connect(function(file){ - console.log("Selected " + file) - }) - } - } - Button { - text: "Open File" - property var builder: Component { - FileDialog { - title: "Open File" - filter: "All Files (*.*)" - //filter: "HTML files (*.html);;Other(*.png)" - } - } - - onClicked: { - var fileDialog = builder.createObject(desktop); - fileDialog.canceled.connect(function(){ - console.log("Cancelled") - }) - fileDialog.selectedFile.connect(function(file){ - console.log("Selected " + file) - }) - } - } - */ - - // tabs - /* - Button { - text: "Add Tab" - onClicked: { - console.log(desktop.toolWindow); - desktop.toolWindow.addWebTab({ source: "Foo" }); - desktop.toolWindow.showTabForUrl("Foo", true); - } - } - - Button { - text: "Add Tab 2" - onClicked: { - console.log(desktop.toolWindow); - desktop.toolWindow.addWebTab({ source: "Foo 2" }); - desktop.toolWindow.showTabForUrl("Foo 2", true); - } - } - - Button { - text: "Add Tab 3" - onClicked: { - console.log(desktop.toolWindow); - desktop.toolWindow.addWebTab({ source: "Foo 3" }); - desktop.toolWindow.showTabForUrl("Foo 3", true); - } - } - - Button { - text: "Destroy Tab" - onClicked: { - console.log(desktop.toolWindow); - desktop.toolWindow.removeTabForUrl("Foo"); - } - } - */ - - // Hifi specific stuff - /* - Button { - // Shows the dialog with preferences sections but not each section's preference items - // because Preferences.preferencesByCategory() method is not stubbed out. - text: "Settings > General..." - property var builder: Component { - GeneralPreferencesDialog { } - } - onClicked: { - var runningScripts = builder.createObject(desktop); - } - } - - Button { - text: "Running Scripts" - property var builder: Component { - RunningScripts { } - } - onClicked: { - var runningScripts = builder.createObject(desktop); - } - } - - Button { - text: "Attachments" - property var builder: Component { - AttachmentsDialog { } - } - onClicked: { - var attachmentsDialog = builder.createObject(desktop); - } - } - Button { - // Replicates message box that pops up after selecting new avatar. Includes title. - text: "Confirm Avatar" - onClicked: { - var messageBox = desktop.messageBox({ - title: "Set Avatar", - text: "Would you like to use 'Albert' for your avatar?", - icon: hifi.icons.question, // Test question icon - //icon: hifi.icons.information, // Test informaton icon - //icon: hifi.icons.warning, // Test warning icon - //icon: hifi.icons.critical, // Test critical icon - //icon: hifi.icons.none, // Test no icon - buttons: OriginalDialogs.StandardButton.Ok + OriginalDialogs.StandardButton.Cancel, - defaultButton: OriginalDialogs.StandardButton.Ok - }); - messageBox.selected.connect(function(button) { - console.log("You clicked " + button) - }) - } - } - */ - // bookmarks - /* - Button { - text: "Bookmark Location" - onClicked: { - desktop.inputDialog({ - title: "Bookmark Location", - icon: hifi.icons.placemark, - label: "Name" - }); - } - } - Button { - text: "Delete Bookmark" - onClicked: { - desktop.inputDialog({ - title: "Delete Bookmark", - icon: hifi.icons.placemark, - label: "Select the bookmark to delete", - items: ["Bookmark A", "Bookmark B", "Bookmark C"] - }); - } - } - Button { - text: "Duplicate Bookmark" - onClicked: { - desktop.messageBox({ - title: "Duplicate Bookmark", - icon: hifi.icons.warning, - text: "The bookmark name you entered alread exists in yoru list.", - informativeText: "Would you like to overwrite it?", - buttons: OriginalDialogs.StandardButton.Yes + OriginalDialogs.StandardButton.No, - defaultButton: OriginalDialogs.StandardButton.Yes - }); - } - } - */ - - } - - - HifiConstants { id: hifi } - - Desktop { - id: desktop - anchors.fill: parent - - //rootMenu: StubMenu { id: rootMenu } - //Component.onCompleted: offscreenWindow = appWindow - - /* - MouseArea { - anchors.fill: parent - acceptedButtons: Qt.RightButton - onClicked: desktop.popupMenu(Qt.vector2d(mouseX, mouseY)); - } - */ - - Browser { - url: "http://s3.amazonaws.com/DreamingContent/testUiDelegates.html" - } - - - Window { - id: blue - closable: true - visible: true - resizable: true - destroyOnHidden: false - title: "Blue" - - width: 100; height: 100 - x: 1280 / 2; y: 720 / 2 - Settings { - category: "TestWindow.Blue" - property alias x: blue.x - property alias y: blue.y - property alias width: blue.width - property alias height: blue.height - } - - Rectangle { - anchors.fill: parent - visible: true - color: "blue" - Component.onDestruction: console.log("Blue destroyed") - } - } - - Window { - id: green - closable: true - visible: true - resizable: true - title: "Green" - destroyOnHidden: false - - width: 100; height: 100 - x: 1280 / 2; y: 720 / 2 - Settings { - category: "TestWindow.Green" - property alias x: green.x - property alias y: green.y - property alias width: green.width - property alias height: green.height - } - - Rectangle { - anchors.fill: parent - visible: true - color: "green" - Component.onDestruction: console.log("Green destroyed") - } - } - -/* - Rectangle { width: 100; height: 100; x: 100; y: 100; color: "#00f" } - - Window { - id: green - alwaysOnTop: true - frame: HiddenFrame{} - hideBackground: true - closable: true - visible: true - resizable: false - x: 1280 / 2; y: 720 / 2 - width: 100; height: 100 - Rectangle { - color: "#0f0" - width: green.width; - height: green.height; - } - } - */ - -/* - Window { - id: yellow - closable: true - visible: true - resizable: true - x: 100; y: 100 - width: 100; height: 100 - Rectangle { - anchors.fill: parent - visible: true - color: "yellow" - } - } -*/ - } - - Action { - id: openBrowserAction - text: "Open Browser" - shortcut: "Ctrl+Shift+X" - onTriggered: { - builder.createObject(desktop); - } - property var builder: Component { - ModelBrowserDialog{} - } - } -} - - - - diff --git a/tests-manual/ui/qmlscratch.pro b/tests-manual/ui/qmlscratch.pro index 5c9b91ee52..3287180d26 100644 --- a/tests-manual/ui/qmlscratch.pro +++ b/tests-manual/ui/qmlscratch.pro @@ -4,34 +4,10 @@ QT += gui qml quick xml webengine widgets CONFIG += c++11 -SOURCES += src/main.cpp \ - ../../libraries/ui/src/FileDialogHelper.cpp - -HEADERS += \ - ../../libraries/ui/src/FileDialogHelper.h +SOURCES += src/main.cpp # Additional import path used to resolve QML modules in Qt Creator's code model QML_IMPORT_PATH = ../../interface/resources/qml - DISTFILES += \ - qml/*.qml \ - ../../interface/resources/QtWebEngine/UIDelegates/original/*.qml \ - ../../interface/resources/QtWebEngine/UIDelegates/*.qml \ - ../../interface/resources/qml/*.qml \ - ../../interface/resources/qml/controls/*.qml \ - ../../interface/resources/qml/controls-uit/*.qml \ - ../../interface/resources/qml/dialogs/*.qml \ - ../../interface/resources/qml/dialogs/fileDialog/*.qml \ - ../../interface/resources/qml/dialogs/preferences/*.qml \ - ../../interface/resources/qml/dialogs/messageDialog/*.qml \ - ../../interface/resources/qml/desktop/*.qml \ - ../../interface/resources/qml/menus/*.qml \ - ../../interface/resources/qml/styles/*.qml \ - ../../interface/resources/qml/styles-uit/*.qml \ - ../../interface/resources/qml/windows/*.qml \ - ../../interface/resources/qml/hifi/*.qml \ - ../../interface/resources/qml/hifi/toolbars/*.qml \ - ../../interface/resources/qml/hifi/dialogs/*.qml \ - ../../interface/resources/qml/hifi/dialogs/preferences/*.qml \ - ../../interface/resources/qml/hifi/overlays/*.qml + qml/*.qml diff --git a/tests-manual/ui/src/main.cpp b/tests-manual/ui/src/main.cpp index 312b5f3823..a5061f4d01 100644 --- a/tests-manual/ui/src/main.cpp +++ b/tests-manual/ui/src/main.cpp @@ -3,88 +3,31 @@ #include #include -#include "../../../libraries/ui/src/FileDialogHelper.h" - - -class Preference : public QObject { - Q_OBJECT - Q_PROPERTY(QString category READ getCategory() CONSTANT) - Q_PROPERTY(QString name READ getName() CONSTANT) - Q_PROPERTY(Type type READ getType() CONSTANT) - Q_ENUMS(Type) -public: - enum Type { - Editable, - Browsable, - Spinner, - Checkbox, - }; - - Preference(QObject* parent = nullptr) : QObject(parent) {} - - Preference(const QString& category, const QString& name, QObject* parent = nullptr) - : QObject(parent), _category(category), _name(name) { } - const QString& getCategory() const { return _category; } - const QString& getName() const { return _name; } - virtual Type getType() { return Editable; } - -protected: - const QString _category; - const QString _name; -}; - -class Reticle : public QObject { - Q_OBJECT - Q_PROPERTY(QPoint position READ getPosition CONSTANT) -public: - - Reticle(QObject* parent) : QObject(parent) { - } - - QPoint getPosition() { - if (!_window) { - return QPoint(0, 0); - } - return _window->mapFromGlobal(QCursor::pos()); - } - - void setWindow(QWindow* window) { - _window = window; - } - -private: - QWindow* _window{nullptr}; -}; - QString getRelativeDir(const QString& relativePath = ".") { - QDir path(__FILE__); path.cdUp(); + QDir path(__FILE__); + path.cdUp(); + path.cdUp(); auto result = path.absoluteFilePath(relativePath); result = path.cleanPath(result) + "/"; return result; } -QString getTestQmlDir() { - return getRelativeDir("../qml"); +QString getResourcesDir() { + return getRelativeDir("../../interface/resources"); } -QString getInterfaceQmlDir() { - return getRelativeDir("/"); +QString getQmlDir() { + return getRelativeDir("../../interface/resources/qml"); } - -void setChild(QQmlApplicationEngine& engine, const char* name) { - for (auto obj : engine.rootObjects()) { - auto child = obj->findChild(QString(name)); - if (child) { - engine.rootContext()->setContextProperty(name, child); - return; - } - } - qWarning() << "Could not find object named " << name; +QString getScriptsDir() { + return getRelativeDir("../../scripts"); } void addImportPath(QQmlApplicationEngine& engine, const QString& relativePath, bool insert = false) { QString resolvedPath = getRelativeDir(relativePath); + + qDebug() << "adding import path: " << QDir::toNativeSeparators(resolvedPath); engine.addImportPath(resolvedPath); } @@ -93,44 +36,24 @@ int main(int argc, char *argv[]) { app.setOrganizationName("Some Company"); app.setOrganizationDomain("somecompany.com"); app.setApplicationName("Amazing Application"); - QDir::setCurrent(getRelativeDir("..")); - QtWebEngine::initialize(); - qmlRegisterType("Hifi", 1, 0, "Preference"); + auto scriptsDir = getScriptsDir(); + auto resourcesDir = getResourcesDir(); QQmlApplicationEngine engine; + addImportPath(engine, "."); addImportPath(engine, "qml"); - addImportPath(engine, "../../interface/resources/qml"); - addImportPath(engine, "../../interface/resources"); - engine.load(QUrl(QStringLiteral("qml/Stubs.qml"))); + addImportPath(engine, resourcesDir); + addImportPath(engine, resourcesDir + "/qml"); + addImportPath(engine, scriptsDir); + addImportPath(engine, scriptsDir + "/developer/tests"); - setChild(engine, "offscreenFlags"); - setChild(engine, "Account"); - setChild(engine, "ApplicationCompositor"); - setChild(engine, "Controller"); - setChild(engine, "Desktop"); - setChild(engine, "ScriptDiscoveryService"); - setChild(engine, "HMD"); - setChild(engine, "GL"); - setChild(engine, "MenuHelper"); - setChild(engine, "Preferences"); - setChild(engine, "urlHandler"); - engine.rootContext()->setContextProperty("DebugQML", true); - engine.rootContext()->setContextProperty("fileDialogHelper", new FileDialogHelper()); + QFontDatabase::addApplicationFont(resourcesDir + "/fonts/FiraSans-Regular.ttf"); + QFontDatabase::addApplicationFont(resourcesDir + "/fonts/FiraSans-SemiBold.ttf"); + QFontDatabase::addApplicationFont(resourcesDir + "/fonts/hifi-glyphs.ttf"); - //engine.load(QUrl(QStringLiteral("qrc:/qml/gallery/main.qml"))); - engine.load(QUrl(QStringLiteral("qml/main.qml"))); - for (QObject* rootObject : engine.rootObjects()) { - if (rootObject->objectName() == "MainWindow") { - Reticle* reticle = new Reticle(rootObject); - reticle->setWindow((QWindow*)rootObject); - engine.rootContext()->setContextProperty("Reticle", reticle); - engine.rootContext()->setContextProperty("Window", rootObject); - break; - } - } + auto url = getRelativeDir(".") + "qml/ControlsGalleryWindow.qml"; + + engine.load(url); return app.exec(); } - -#include "main.moc" - diff --git a/tests/animation/src/AnimInverseKinematicsTests.cpp b/tests/animation/src/AnimInverseKinematicsTests.cpp index f5d3597f56..910770bb0d 100644 --- a/tests/animation/src/AnimInverseKinematicsTests.cpp +++ b/tests/animation/src/AnimInverseKinematicsTests.cpp @@ -28,8 +28,8 @@ const glm::quat identity = glm::quat(); const glm::quat quaterTurnAroundZ = glm::angleAxis(0.5f * PI, zAxis); -void makeTestFBXJoints(FBXGeometry& geometry) { - FBXJoint joint; +void makeTestFBXJoints(HFMModel& hfmModel) { + HFMJoint joint; joint.isFree = false; joint.freeLineage.clear(); joint.parentIndex = -1; @@ -60,29 +60,29 @@ void makeTestFBXJoints(FBXGeometry& geometry) { joint.name = "A"; joint.parentIndex = -1; joint.translation = origin; - geometry.joints.push_back(joint); + hfmModel.joints.push_back(joint); joint.name = "B"; joint.parentIndex = 0; joint.translation = xAxis; - geometry.joints.push_back(joint); + hfmModel.joints.push_back(joint); joint.name = "C"; joint.parentIndex = 1; joint.translation = xAxis; - geometry.joints.push_back(joint); + hfmModel.joints.push_back(joint); joint.name = "D"; joint.parentIndex = 2; joint.translation = xAxis; - geometry.joints.push_back(joint); + hfmModel.joints.push_back(joint); // compute each joint's transform - for (int i = 1; i < (int)geometry.joints.size(); ++i) { - FBXJoint& j = geometry.joints[i]; + for (int i = 1; i < (int)hfmModel.joints.size(); ++i) { + HFMJoint& j = hfmModel.joints[i]; int parentIndex = j.parentIndex; // World = ParentWorld * T * (Roff * Rp) * Rpre * R * Rpost * (Rp-1 * Soff * Sp * S * Sp-1) - j.transform = geometry.joints[parentIndex].transform * + j.transform = hfmModel.joints[parentIndex].transform * glm::translate(j.translation) * j.preTransform * glm::mat4_cast(j.preRotation * j.rotation * j.postRotation) * @@ -96,12 +96,12 @@ void AnimInverseKinematicsTests::testSingleChain() { AnimContext context(false, false, false, glm::mat4(), glm::mat4()); - FBXGeometry geometry; - makeTestFBXJoints(geometry); + HFMModel hfmModel; + makeTestFBXJoints(hfmModel); // create a skeleton and doll AnimPose offset; - AnimSkeleton::Pointer skeletonPtr = std::make_shared(geometry); + AnimSkeleton::Pointer skeletonPtr = std::make_shared(hfmModel); AnimInverseKinematics ikDoll("doll"); ikDoll.setSkeleton(skeletonPtr); @@ -119,7 +119,7 @@ void AnimInverseKinematicsTests::testSingleChain() { poses.push_back(pose); pose.trans() = xAxis; - for (int i = 1; i < (int)geometry.joints.size(); ++i) { + for (int i = 1; i < (int)hfmModel.joints.size(); ++i) { poses.push_back(pose); } ikDoll.loadPoses(poses); diff --git a/tests/recording/src/main.cpp b/tests/recording/src/main.cpp index 1b4e5adf6d..6181906ff0 100644 --- a/tests/recording/src/main.cpp +++ b/tests/recording/src/main.cpp @@ -1,8 +1,28 @@ +// +// main.cpp +// tests/recording/src +// +// Created by Bradley Austin Davis on 11/06/15. +// Copyright 2018 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#if defined(__clang__) +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wunused-private-field" +#endif + #include #include #include #include +#if defined(__clang__) +#pragma clang diagnostic pop +#endif + #ifdef Q_OS_WIN32 #include #endif diff --git a/tests/shaders/CMakeLists.txt b/tests/shaders/CMakeLists.txt index 08678c1c26..a2bba2a425 100644 --- a/tests/shaders/CMakeLists.txt +++ b/tests/shaders/CMakeLists.txt @@ -3,7 +3,17 @@ macro (setup_testcase_dependencies) # link in the shared libraries link_hifi_libraries(shared test-utils gpu shaders gl ${PLATFORM_GL_BACKEND}) + #target_spirv() + + find_package(spirv_cross_core REQUIRED) + find_package(spirv_cross_reflect REQUIRED) + find_package(spirv_cross_glsl REQUIRED) + target_include_directories(${TARGET_NAME} PRIVATE $) + target_include_directories(${TARGET_NAME} PRIVATE $ENV{VULKAN_SDK}/Include) + + package_libraries_for_deployment() endmacro () setup_hifi_testcase(Gui) + diff --git a/tests/shaders/src/ShaderTests.cpp b/tests/shaders/src/ShaderTests.cpp index 4dd15710f9..56b92ed047 100644 --- a/tests/shaders/src/ShaderTests.cpp +++ b/tests/shaders/src/ShaderTests.cpp @@ -31,22 +31,39 @@ #include #include +#define RUNTIME_SHADER_COMPILE_TEST 1 + +#if RUNTIME_SHADER_COMPILE_TEST +#include +#include +#include +#include +#include +#include + +#ifdef DEBUG +#define VCPKG_LIB_DIR "D:/hifi/vcpkg/hifi/installed/x64-windows/debug/lib/" +#else +#define VCPKG_LIB_DIR "D:/hifi/vcpkg/hifi/installed/x64-windows/lib/" +#endif + +#pragma comment(lib, VCPKG_LIB_DIR "shaderc_combined.lib") +#pragma comment(lib, VCPKG_LIB_DIR "spirv-cross-core.lib") +#pragma comment(lib, VCPKG_LIB_DIR "spirv-cross-glsl.lib") +#pragma comment(lib, VCPKG_LIB_DIR "spirv-cross-reflect.lib") + +#endif + QTEST_MAIN(ShaderTests) -#pragma optimize("", off) void ShaderTests::initTestCase() { - _window = new QWindow(); - _window->setSurfaceType(QSurface::SurfaceType::OpenGLSurface); - _context = new ::gl::Context(_window); + _context = new ::gl::OffscreenContext(); getDefaultOpenGLSurfaceFormat(); _context->create(); if (!_context->makeCurrent()) { qFatal("Unable to make test GL context current"); } - QOpenGLContextWrapper(_context->qglContext()).makeCurrent(_window); - if (!_context->makeCurrent()) { - qFatal("Unable to make test GL context current"); - } + QOpenGLContextWrapper(_context->qglContext()).makeCurrent(_context->getWindow()); gl::initModuleGl(); if (!_context->makeCurrent()) { qFatal("Unable to make test GL context current"); @@ -62,6 +79,8 @@ void ShaderTests::cleanupTestCase() { qDebug() << "Done"; } +#if RUNTIME_SHADER_COMPILE_TEST + template QStringList toQStringList(const C& c) { QStringList result; @@ -80,7 +99,7 @@ std::unordered_set toStringSet(const C& c, F f) { return result; } -template +template bool isSubset(const C& parent, const C& child) { for (const auto& v : child) { if (0 == parent.count(v)) { @@ -90,6 +109,7 @@ bool isSubset(const C& parent, const C& child) { return true; } +#if 0 gpu::Shader::ReflectionMap mergeReflection(const std::initializer_list& list) { gpu::Shader::ReflectionMap result; std::unordered_map> usedLocationsByType; @@ -120,65 +140,7 @@ gpu::Shader::ReflectionMap mergeReflection(const std::initializer_list -std::unordered_map invertMap(const std::unordered_map& map) { - std::unordered_map result; - for (const auto& entry : map) { - result[entry.second] = entry.first; - } - return result; -} - -static void verifyBindings(const gpu::Shader::Source& source) { - const auto reflection = source.getReflection(); - for (const auto& entry : reflection) { - const auto& map = entry.second; - const auto reverseMap = invertMap(map); - if (map.size() != reverseMap.size()) { - QFAIL("Bindings are not unique"); - } - } - -} - - -static void verifyInterface(const gpu::Shader::Source& vertexSource, const gpu::Shader::Source& fragmentSource) { - if (0 == fragmentSource.getReflection().count(gpu::Shader::BindingType::INPUT)) { - return; - } - auto fragIn = fragmentSource.getReflection().at(gpu::Shader::BindingType::INPUT); - if (0 == vertexSource.getReflection().count(gpu::Shader::BindingType::OUTPUT)) { - qDebug() << "No vertex output for fragment input"; - //QFAIL("No vertex output for fragment input"); - return; - } - auto vout = vertexSource.getReflection().at(gpu::Shader::BindingType::OUTPUT); - auto vrev = invertMap(vout); - static const std::string IN_STEREO_SIDE_STRING = "_inStereoSide"; - for (const auto entry : fragIn) { - const auto& name = entry.first; - // The presence of "_inStereoSide" in fragment shaders is a bug due to the way we do reflection - // and use preprocessor macros in the shaders - if (name == IN_STEREO_SIDE_STRING) { - continue; - } - if (0 == vout.count(name)) { - qDebug() << "Vertex output missing"; - //QFAIL("Vertex output missing"); - continue; - } - const auto& inLocation = entry.second; - const auto& outLocation = vout.at(name); - if (inLocation != outLocation) { - qDebug() << "Mismatch in vertex / fragment interface"; - //QFAIL("Mismatch in vertex / fragment interface"); - continue; - } - } -} - -template +template bool compareBindings(const C& actual, const gpu::Shader::LocationMap& expected) { if (actual.size() != expected.size()) { auto actualNames = toStringSet(actual, [](const auto& v) { return v.name; }); @@ -192,124 +154,363 @@ bool compareBindings(const C& actual, const gpu::Shader::LocationMap& expected) return true; } -void ShaderTests::testShaderLoad() { - std::set usedShaders; - uint32_t maxShader = 0; - try { - -#if 0 - uint32_t testPrograms[] = { - shader::render_utils::program::parabola, - shader::INVALID_PROGRAM, - }; -#else - const auto& testPrograms = shader::all_programs; #endif - size_t index = 0; - while (shader::INVALID_PROGRAM != testPrograms[index]) { - auto programId = testPrograms[index]; - ++index; +#endif - uint32_t vertexId = shader::getVertexId(programId); - uint32_t fragmentId = shader::getFragmentId(programId); - usedShaders.insert(vertexId); - usedShaders.insert(fragmentId); - maxShader = std::max(maxShader, std::max(fragmentId, vertexId)); - auto vertexSource = gpu::Shader::getShaderSource(vertexId); - QVERIFY(!vertexSource.getCode().empty()); - verifyBindings(vertexSource); - auto fragmentSource = gpu::Shader::getShaderSource(fragmentId); - QVERIFY(!fragmentSource.getCode().empty()); - verifyBindings(fragmentSource); +template +std::unordered_map invertMap(const std::unordered_map& map) { + std::unordered_map result; + for (const auto& entry : map) { + result[entry.second] = entry.first; + } + if (result.size() != map.size()) { + throw std::runtime_error("Map inversion failure, result size does not match input size"); + } + return result; +} + +static void verifyInterface(const gpu::Shader::Source& vertexSource, + const gpu::Shader::Source& fragmentSource, + shader::Dialect dialect, + shader::Variant variant) { + const auto& fragmentReflection = fragmentSource.getReflection(dialect, variant); + if (fragmentReflection.inputs.empty()) { + return; + } + + const auto& vertexReflection = vertexSource.getReflection(dialect, variant); + const auto& fragIn = fragmentReflection.inputs; + if (vertexReflection.outputs.empty()) { + throw std::runtime_error("No vertex outputs for fragment inputs"); + } + + const auto& vout = vertexReflection.outputs; + auto vrev = invertMap(vout); + for (const auto entry : fragIn) { + const auto& name = entry.first; + if (0 == vout.count(name)) { + throw std::runtime_error("Vertex outputs missing"); + } + const auto& inLocation = entry.second; + const auto& outLocation = vout.at(name); + if (inLocation != outLocation) { + throw std::runtime_error("Mismatch in vertex / fragment interface"); + } + } +} + +static void verifyInterface(const gpu::Shader::Source& vertexSource, const gpu::Shader::Source& fragmentSource) { + for (const auto& dialect : shader::allDialects()) { + for (const auto& variant : shader::allVariants()) { + verifyInterface(vertexSource, fragmentSource, dialect, variant); + } + } +} + +#if RUNTIME_SHADER_COMPILE_TEST + +void configureGLSLCompilerResources(TBuiltInResource* glslCompilerResources) { + glslCompilerResources->maxLights = 32; + glslCompilerResources->maxClipPlanes = 6; + glslCompilerResources->maxTextureUnits = 32; + glslCompilerResources->maxTextureCoords = 32; + glslCompilerResources->maxVertexAttribs = 64; + glslCompilerResources->maxVertexUniformComponents = 4096; + glslCompilerResources->maxVaryingFloats = 64; + glslCompilerResources->maxVertexTextureImageUnits = 32; + glslCompilerResources->maxCombinedTextureImageUnits = 80; + glslCompilerResources->maxTextureImageUnits = 32; + glslCompilerResources->maxFragmentUniformComponents = 4096; + glslCompilerResources->maxDrawBuffers = 32; + glslCompilerResources->maxVertexUniformVectors = 128; + glslCompilerResources->maxVaryingVectors = 8; + glslCompilerResources->maxFragmentUniformVectors = 16; + glslCompilerResources->maxVertexOutputVectors = 16; + glslCompilerResources->maxFragmentInputVectors = 15; + glslCompilerResources->minProgramTexelOffset = -8; + glslCompilerResources->maxProgramTexelOffset = 7; + glslCompilerResources->maxClipDistances = 8; + glslCompilerResources->maxComputeWorkGroupCountX = 65535; + glslCompilerResources->maxComputeWorkGroupCountY = 65535; + glslCompilerResources->maxComputeWorkGroupCountZ = 65535; + glslCompilerResources->maxComputeWorkGroupSizeX = 1024; + glslCompilerResources->maxComputeWorkGroupSizeY = 1024; + glslCompilerResources->maxComputeWorkGroupSizeZ = 64; + glslCompilerResources->maxComputeUniformComponents = 1024; + glslCompilerResources->maxComputeTextureImageUnits = 16; + glslCompilerResources->maxComputeImageUniforms = 8; + glslCompilerResources->maxComputeAtomicCounters = 8; + glslCompilerResources->maxComputeAtomicCounterBuffers = 1; + glslCompilerResources->maxVaryingComponents = 60; + glslCompilerResources->maxVertexOutputComponents = 64; + glslCompilerResources->maxGeometryInputComponents = 64; + glslCompilerResources->maxGeometryOutputComponents = 128; + glslCompilerResources->maxFragmentInputComponents = 128; + glslCompilerResources->maxImageUnits = 8; + glslCompilerResources->maxCombinedImageUnitsAndFragmentOutputs = 8; + glslCompilerResources->maxCombinedShaderOutputResources = 8; + glslCompilerResources->maxImageSamples = 0; + glslCompilerResources->maxVertexImageUniforms = 0; + glslCompilerResources->maxTessControlImageUniforms = 0; + glslCompilerResources->maxTessEvaluationImageUniforms = 0; + glslCompilerResources->maxGeometryImageUniforms = 0; + glslCompilerResources->maxFragmentImageUniforms = 8; + glslCompilerResources->maxCombinedImageUniforms = 8; + glslCompilerResources->maxGeometryTextureImageUnits = 16; + glslCompilerResources->maxGeometryOutputVertices = 256; + glslCompilerResources->maxGeometryTotalOutputComponents = 1024; + glslCompilerResources->maxGeometryUniformComponents = 1024; + glslCompilerResources->maxGeometryVaryingComponents = 64; + glslCompilerResources->maxTessControlInputComponents = 128; + glslCompilerResources->maxTessControlOutputComponents = 128; + glslCompilerResources->maxTessControlTextureImageUnits = 16; + glslCompilerResources->maxTessControlUniformComponents = 1024; + glslCompilerResources->maxTessControlTotalOutputComponents = 4096; + glslCompilerResources->maxTessEvaluationInputComponents = 128; + glslCompilerResources->maxTessEvaluationOutputComponents = 128; + glslCompilerResources->maxTessEvaluationTextureImageUnits = 16; + glslCompilerResources->maxTessEvaluationUniformComponents = 1024; + glslCompilerResources->maxTessPatchComponents = 120; + glslCompilerResources->maxPatchVertices = 32; + glslCompilerResources->maxTessGenLevel = 64; + glslCompilerResources->maxViewports = 16; + glslCompilerResources->maxVertexAtomicCounters = 0; + glslCompilerResources->maxTessControlAtomicCounters = 0; + glslCompilerResources->maxTessEvaluationAtomicCounters = 0; + glslCompilerResources->maxGeometryAtomicCounters = 0; + glslCompilerResources->maxFragmentAtomicCounters = 8; + glslCompilerResources->maxCombinedAtomicCounters = 8; + glslCompilerResources->maxAtomicCounterBindings = 1; + glslCompilerResources->maxVertexAtomicCounterBuffers = 0; + glslCompilerResources->maxTessControlAtomicCounterBuffers = 0; + glslCompilerResources->maxTessEvaluationAtomicCounterBuffers = 0; + glslCompilerResources->maxGeometryAtomicCounterBuffers = 0; + glslCompilerResources->maxFragmentAtomicCounterBuffers = 1; + glslCompilerResources->maxCombinedAtomicCounterBuffers = 1; + glslCompilerResources->maxAtomicCounterBufferSize = 16384; + glslCompilerResources->maxTransformFeedbackBuffers = 4; + glslCompilerResources->maxTransformFeedbackInterleavedComponents = 64; + glslCompilerResources->maxCullDistances = 8; + glslCompilerResources->maxCombinedClipAndCullDistances = 8; + glslCompilerResources->maxSamples = 4; + glslCompilerResources->limits.nonInductiveForLoops = 1; + glslCompilerResources->limits.whileLoops = 1; + glslCompilerResources->limits.doWhileLoops = 1; + glslCompilerResources->limits.generalUniformIndexing = 1; + glslCompilerResources->limits.generalAttributeMatrixVectorIndexing = 1; + glslCompilerResources->limits.generalVaryingIndexing = 1; + glslCompilerResources->limits.generalSamplerIndexing = 1; + glslCompilerResources->limits.generalVariableIndexing = 1; + glslCompilerResources->limits.generalConstantMatrixVectorIndexing = 1; +} + +void writeSpirv(const std::string& filename, const std::vector& spirv) { + std::ofstream o(filename, std::ios::trunc | std::ios::binary); + for (const auto& word : spirv) { + o.write((const char*)&word, sizeof(word)); + } + o.close(); +} + +void write(const std::string& filename, const std::string& text) { + std::ofstream o(filename, std::ios::trunc); + o.write(text.c_str(), text.size()); + o.close(); +} + +bool endsWith(const std::string& s, const std::string& f) { + auto end = s.substr(s.size() - f.size()); + return (end == f); +} + +EShLanguage getShaderStage(const std::string& shaderName) { + static const std::string VERT_EXT{ ".vert" }; + static const std::string FRAG_EXT{ ".frag" }; + static const std::string GEOM_EXT{ ".geom" }; + static const size_t EXT_SIZE = VERT_EXT.size(); + if (shaderName.size() < EXT_SIZE) { + throw std::runtime_error("Invalid shader name"); + } + std::string ext = shaderName.substr(shaderName.size() - EXT_SIZE); + if (ext == VERT_EXT) { + return EShLangVertex; + } else if (ext == FRAG_EXT) { + return EShLangFragment; + } else if (ext == GEOM_EXT) { + return EShLangGeometry; + } + throw std::runtime_error("Invalid shader name"); +} + +void rebuildSource(shader::Dialect dialect, shader::Variant variant, const shader::Source& source) { + try { + using namespace glslang; + static const EShMessages messages = (EShMessages)(EShMsgDefault | EShMsgSpvRules | EShMsgVulkanRules); + auto shaderName = source.name; + const std::string baseOutName = "d:/shaders/" + shaderName; + auto stage = getShaderStage(shaderName); + + TShader shader(stage); + std::vector strings; + const auto& dialectVariantSource = source.dialectSources.at(dialect).variantSources.at(variant); + strings.push_back(dialectVariantSource.scribe.c_str()); + shader.setStrings(strings.data(), (int)strings.size()); + shader.setEnvInput(EShSourceGlsl, stage, EShClientOpenGL, 450); + shader.setEnvClient(EShClientVulkan, EShTargetVulkan_1_1); + shader.setEnvTarget(EShTargetSpv, EShTargetSpv_1_3); + + bool success = shader.parse(&glslCompilerResources, 450, false, messages); + if (!success) { + qWarning() << "Failed to parse shader " << shaderName.c_str(); + qWarning() << shader.getInfoLog(); + qWarning() << shader.getInfoDebugLog(); + throw std::runtime_error("Wrong"); + } + + // Create and link a shader program containing the single shader + glslang::TProgram program; + program.addShader(&shader); + if (!program.link(messages)) { + qWarning() << "Failed to compile shader " << shaderName.c_str(); + qWarning() << program.getInfoLog(); + qWarning() << program.getInfoDebugLog(); + throw std::runtime_error("Wrong"); + } + + // Output the SPIR-V code from the shader program + std::vector spirv; + glslang::GlslangToSpv(*program.getIntermediate(stage), spirv); + + spvtools::SpirvTools core(SPV_ENV_VULKAN_1_1); + spvtools::Optimizer opt(SPV_ENV_VULKAN_1_1); + + auto outputLambda = [](spv_message_level_t, const char*, const spv_position_t&, const char* m) { qWarning() << m; }; + core.SetMessageConsumer(outputLambda); + opt.SetMessageConsumer(outputLambda); + + if (!core.Validate(spirv)) { + throw std::runtime_error("invalid spirv"); + } + writeSpirv(baseOutName + ".spv", spirv); + + opt.RegisterPass(spvtools::CreateFreezeSpecConstantValuePass()) + .RegisterPass(spvtools::CreateStrengthReductionPass()) + .RegisterPass(spvtools::CreateEliminateDeadConstantPass()) + .RegisterPass(spvtools::CreateEliminateDeadFunctionsPass()) + .RegisterPass(spvtools::CreateUnifyConstantPass()); + + std::vector optspirv; + if (!opt.Run(spirv.data(), spirv.size(), &optspirv)) { + throw std::runtime_error("bad optimize run"); + } + writeSpirv(baseOutName + ".opt.spv", optspirv); + + std::string disassembly; + if (!core.Disassemble(optspirv, &disassembly)) { + throw std::runtime_error("bad disassembly"); + } + + write(baseOutName + ".spv.txt", disassembly); + } catch (const std::runtime_error& error) { + qWarning() << error.what(); + } +} +#endif + +void validateDialectVariantSource(const shader::DialectVariantSource& source) { + if (source.scribe.empty()) { + throw std::runtime_error("Missing scribe source"); + } + + if (source.spirv.empty()) { + throw std::runtime_error("Missing SPIRV"); + } + + if (source.glsl.empty()) { + throw std::runtime_error("Missing GLSL"); + } +} + +void validaDialectSource(const shader::DialectSource& dialectSource) { + for (const auto& variant : shader::allVariants()) { + validateDialectVariantSource(dialectSource.variantSources.find(variant)->second); + } +} + +void validateSource(const shader::Source& shader) { + if (shader.id == shader::INVALID_SHADER) { + throw std::runtime_error("Missing stored shader ID"); + } + + if (shader.name.empty()) { + throw std::runtime_error("Missing shader name"); + } + + static const auto& dialects = shader::allDialects(); + for (const auto dialect : dialects) { + if (!shader.dialectSources.count(dialect)) { + throw std::runtime_error("Missing platform shader"); + } + const auto& platformShader = shader.dialectSources.find(dialect)->second; + validaDialectSource(platformShader); + } +} + +void ShaderTests::testShaderLoad() { + try { + const auto& shaderIds = shader::allShaders(); + + // For debugging compile or link failures on individual programs, enable the following block and change the array values + // Be sure to end with the sentinal value of shader::INVALID_PROGRAM + const auto& programIds = shader::allPrograms(); + + for (auto shaderId : shaderIds) { + validateSource(shader::Source::get(shaderId)); + } + + { + std::unordered_set programUsedShaders; +#pragma omp parallel for + for (auto programId : programIds) { + auto vertexId = shader::getVertexId(programId); + shader::Source::get(vertexId); + programUsedShaders.insert(vertexId); + auto fragmentId = shader::getFragmentId(programId); + shader::Source::get(fragmentId); + programUsedShaders.insert(fragmentId); + } + + for (const auto& shaderId : shaderIds) { + if (programUsedShaders.count(shaderId)) { + continue; + } + const auto& shader = shader::Source::get(shaderId); + qDebug() << "Unused shader found" << shader.name.c_str(); + } + } + + // Traverse all programs again to do program level tests + for (const auto& programId : programIds) { + auto vertexId = shader::getVertexId(programId); + const auto& vertexSource = shader::Source::get(vertexId); + auto fragmentId = shader::getFragmentId(programId); + const auto& fragmentSource = shader::Source::get(fragmentId); verifyInterface(vertexSource, fragmentSource); - auto expectedBindings = mergeReflection({ vertexSource, fragmentSource }); - auto program = gpu::Shader::createProgram(programId); auto glBackend = std::static_pointer_cast(_gpuContext->getBackend()); auto glshader = gpu::gl::GLShader::sync(*glBackend, *program); + if (!glshader) { - qDebug() << "Failed to compile or link vertex " << vertexId << " fragment " << fragmentId; - continue; - } - - QVERIFY(glshader != nullptr); - for (const auto& shaderObject : glshader->_shaderObjects) { - const auto& program = shaderObject.glprogram; - - // Uniforms - { - auto uniforms = gl::Uniform::load(program); - const auto& uniformRemap = shaderObject.uniformRemap; - auto expectedUniforms = expectedBindings[gpu::Shader::BindingType::UNIFORM]; - if (!compareBindings(uniforms, expectedUniforms)) { - qDebug() << "Uniforms mismatch"; - } - for (const auto& uniform : uniforms) { - if (0 != expectedUniforms.count(uniform.name)) { - auto expectedLocation = expectedUniforms[uniform.name]; - if (0 != uniformRemap.count(expectedLocation)) { - expectedLocation = uniformRemap.at(expectedLocation); - } - QVERIFY(expectedLocation == uniform.binding); - } - } - } - - // Textures - { - auto textures = gl::Uniform::loadTextures(program); - auto expiredBegin = std::remove_if(textures.begin(), textures.end(), [&](const gl::Uniform& uniform) -> bool { - return uniform.name == "transformObjectBuffer"; - }); - textures.erase(expiredBegin, textures.end()); - - const auto expectedTextures = expectedBindings[gpu::Shader::BindingType::TEXTURE]; - if (!compareBindings(textures, expectedTextures)) { - qDebug() << "Textures mismatch"; - } - for (const auto& texture : textures) { - if (0 != expectedTextures.count(texture.name)) { - const auto& location = texture.binding; - const auto& expectedUnit = expectedTextures.at(texture.name); - GLint actualUnit = -1; - glGetUniformiv(program, location, &actualUnit); - QVERIFY(expectedUnit == actualUnit); - } - } - } - - // UBOs - { - auto ubos = gl::UniformBlock::load(program); - auto expectedUbos = expectedBindings[gpu::Shader::BindingType::UNIFORM_BUFFER]; - if (!compareBindings(ubos, expectedUbos)) { - qDebug() << "UBOs mismatch"; - } - for (const auto& ubo : ubos) { - if (0 != expectedUbos.count(ubo.name)) { - QVERIFY(expectedUbos[ubo.name] == ubo.binding); - } - } - } - - // FIXME add storage buffer validation + qWarning() << "Failed to compile or link vertex " << vertexSource.name.c_str() << " fragment " + << fragmentSource.name.c_str(); + QFAIL("Program link error"); } } } catch (const std::runtime_error& error) { QFAIL(error.what()); } - for (uint32_t i = 0; i <= maxShader; ++i) { - auto used = usedShaders.count(i); - if (0 != usedShaders.count(i)) { - continue; - } - auto reflectionJson = shader::loadShaderReflection(i); - auto name = QJsonDocument::fromJson(reflectionJson.c_str()).object()["name"].toString(); - qDebug() << "Unused shader" << name; - } - qDebug() << "Completed all shaders"; } diff --git a/tests/shaders/src/ShaderTests.h b/tests/shaders/src/ShaderTests.h index d109341c1f..fd361d9e60 100644 --- a/tests/shaders/src/ShaderTests.h +++ b/tests/shaders/src/ShaderTests.h @@ -23,9 +23,8 @@ private slots: void testShaderLoad(); private: - QWindow* _window{ nullptr }; - gl::Context* _context{ nullptr }; + gl::OffscreenContext* _context{ nullptr }; gpu::ContextPointer _gpuContext; }; -#endif // hifi_ViewFruxtumTests_h +#endif // hifi_ViewFruxtumTests_h diff --git a/tests/shared/src/AACubeTests.cpp b/tests/shared/src/AACubeTests.cpp index 95a4d7f9f0..4ed3ee2813 100644 --- a/tests/shared/src/AACubeTests.cpp +++ b/tests/shared/src/AACubeTests.cpp @@ -168,12 +168,13 @@ void AACubeTests::rayVsParabolaPerformance() { glm::vec3 origin(0.0f); glm::vec3 direction = glm::normalize(glm::vec3(1.0f)); + glm::vec3 invDirection = 1.0f / direction; float distance; BoxFace face; glm::vec3 normal; auto start = std::chrono::high_resolution_clock::now(); for (auto& cube : cubes) { - if (cube.findRayIntersection(origin, direction, distance, face, normal)) { + if (cube.findRayIntersection(origin, direction, invDirection, distance, face, normal)) { numRayHits++; } } diff --git a/tests/shared/src/GLMHelpersTests.cpp b/tests/shared/src/GLMHelpersTests.cpp index 93c4735a6d..71877e89f6 100644 --- a/tests/shared/src/GLMHelpersTests.cpp +++ b/tests/shared/src/GLMHelpersTests.cpp @@ -214,3 +214,39 @@ void GLMHelpersTests::testGenerateBasisVectors() { QCOMPARE_WITH_ABS_ERROR(w, z, EPSILON); } } + +void GLMHelpersTests::roundPerf() { + const int NUM_VECS = 1000000; + const float MAX_VEC = 500.0f; + std::vector vecs; + vecs.reserve(NUM_VECS); + for (int i = 0; i < NUM_VECS; i++) { + vecs.emplace_back(randFloatInRange(-MAX_VEC, MAX_VEC), randFloatInRange(-MAX_VEC, MAX_VEC), randFloatInRange(-MAX_VEC, MAX_VEC)); + } + std::vector vecs2 = vecs; + std::vector originalVecs = vecs; + + auto start = std::chrono::high_resolution_clock::now(); + for (auto& vec : vecs) { + vec = glm::round(vec); + } + + auto glmTime = std::chrono::high_resolution_clock::now() - start; + start = std::chrono::high_resolution_clock::now(); + for (auto& vec : vecs2) { + vec = glm::vec3(fastLrintf(vec.x), fastLrintf(vec.y), fastLrintf(vec.z)); + } + auto manualTime = std::chrono::high_resolution_clock::now() - start; + + bool identical = true; + for (int i = 0; i < vecs.size(); i++) { + identical &= vecs[i] == vecs2[i]; + if (vecs[i] != vecs2[i]) { + qDebug() << "glm: " << vecs[i].x << vecs[i].y << vecs[i].z << ", manual: " << vecs2[i].x << vecs2[i].y << vecs2[i].z; + qDebug() << "original: " << originalVecs[i].x << originalVecs[i].y << originalVecs[i].z; + break; + } + } + + qDebug() << "ratio: " << (float)glmTime.count() / (float)manualTime.count() << ", identical: " << identical; +} \ No newline at end of file diff --git a/tests/shared/src/GLMHelpersTests.h b/tests/shared/src/GLMHelpersTests.h index 030f2d477f..4d9bd0bb60 100644 --- a/tests/shared/src/GLMHelpersTests.h +++ b/tests/shared/src/GLMHelpersTests.h @@ -22,6 +22,7 @@ private slots: void testSixByteOrientationCompression(); void testSimd(); void testGenerateBasisVectors(); + void roundPerf(); }; float getErrorDifference(const float& a, const float& b); diff --git a/tools/CMakeLists.txt b/tools/CMakeLists.txt index 9b36180bc2..6ec9125ce8 100644 --- a/tools/CMakeLists.txt +++ b/tools/CMakeLists.txt @@ -1,11 +1,6 @@ # add the tool directories -add_subdirectory(scribe) -set_target_properties(scribe PROPERTIES FOLDER "Tools") - -add_subdirectory(shreflect) -set_target_properties(shreflect PROPERTIES FOLDER "Tools") - find_npm() + if (NPM_EXECUTABLE) add_subdirectory(jsdoc) set_target_properties(jsdoc PROPERTIES FOLDER "Tools") diff --git a/tools/auto-tester/AppDataHighFidelity/Interface.json b/tools/auto-tester/AppDataHighFidelity/Interface.json new file mode 100644 index 0000000000..ca91a092c8 --- /dev/null +++ b/tools/auto-tester/AppDataHighFidelity/Interface.json @@ -0,0 +1,285 @@ +{ + "AddressManager/address": "hifi://localhost/1.07449e-05,0,-0.0174923/0,0,0,1", + "Audio/Desktop/INPUT": "Microphone (Realtek Audio)", + "Audio/Desktop/OUTPUT": "Speakers / Headphones (Realtek Audio)", + "Audio/VR/INPUT": "Microphone (Realtek Audio)", + "Audio/VR/OUTPUT": "Speakers / Headphones (Realtek Audio)", + "Avatar/Avatar/fullAvatarURL": "", + "Avatar/Debug Draw Animation": false, + "Avatar/Debug Draw Base of Support": false, + "Avatar/Debug Draw Default Pose": false, + "Avatar/Debug Draw Position": false, + "Avatar/Disable Eyelid Adjustment": false, + "Avatar/Draw Mesh": true, + "Avatar/Enable Default Motor Control": true, + "Avatar/Enable Inverse Kinematics": true, + "Avatar/Enable LookAt Snapping": true, + "Avatar/Enable Scripted Motor Control": true, + "Avatar/Face Tracking/Auto Mute Microphone": false, + "Avatar/Face Tracking/Binary Eyelid Control": true, + "Avatar/Face Tracking/Couple Eyelids": true, + "Avatar/Face Tracking/Mute Face Tracking": true, + "Avatar/Face Tracking/None": false, + "Avatar/Face Tracking/Use Audio for Mouth": true, + "Avatar/Face Tracking/Use Camera": true, + "Avatar/Face Tracking/Velocity Filter": true, + "Avatar/Fix Gaze (no saccade)": false, + "Avatar/Show Bounding Collision Shapes": false, + "Avatar/Show Detailed Collision": false, + "Avatar/Show IK Chains": false, + "Avatar/Show IK Constraints": false, + "Avatar/Show IK Targets": false, + "Avatar/Show My Eye Vectors": false, + "Avatar/Show Other Eye Vectors": false, + "Avatar/Show Receive Stats": false, + "Avatar/Show SensorToWorld Matrix": false, + "Avatar/Toggle Hips Following": false, + "Avatar/Turn using Head": false, + "Avatar/animGraphURL": "", + "Avatar/attachmentData/size": 0, + "Avatar/avatarEntityData/size": 0, + "Avatar/collisionSoundURL": "https://hifi-public.s3.amazonaws.com/sounds/Collisions-otherorganic/Body_Hits_Impact.wav", + "Avatar/displayName": "", + "Avatar/dominantHand": "right", + "Avatar/flyingHMD": false, + "Avatar/fullAvatarModelName": "Default", + "Avatar/fullAvatarURL": "", + "Avatar/headPitch": 0, + "Avatar/pitchSpeed": 75, + "Avatar/scale": 1, + "Avatar/useSnapTurn": true, + "Avatar/userHeight": 1.7549999952316284, + "Avatar/yawSpeed": 100, + "Developer/Avatar/Debug Draw Animation": false, + "Developer/Avatar/Debug Draw Base of Support": false, + "Developer/Avatar/Debug Draw Default Pose": false, + "Developer/Avatar/Debug Draw Position": false, + "Developer/Avatar/Disable Eyelid Adjustment": false, + "Developer/Avatar/Draw Mesh": true, + "Developer/Avatar/Enable Default Motor Control": true, + "Developer/Avatar/Enable Inverse Kinematics": true, + "Developer/Avatar/Enable LookAt Snapping": true, + "Developer/Avatar/Enable Scripted Motor Control": true, + "Developer/Avatar/Face Tracking/Auto Mute Microphone": false, + "Developer/Avatar/Face Tracking/Binary Eyelid Control": true, + "Developer/Avatar/Face Tracking/Couple Eyelids": true, + "Developer/Avatar/Face Tracking/Mute Face Tracking": true, + "Developer/Avatar/Face Tracking/None": false, + "Developer/Avatar/Face Tracking/Use Audio for Mouth": true, + "Developer/Avatar/Face Tracking/Use Camera": true, + "Developer/Avatar/Face Tracking/Velocity Filter": true, + "Developer/Avatar/Fix Gaze (no saccade)": false, + "Developer/Avatar/Show Bounding Collision Shapes": false, + "Developer/Avatar/Show Detailed Collision": false, + "Developer/Avatar/Show IK Chains": false, + "Developer/Avatar/Show IK Constraints": false, + "Developer/Avatar/Show IK Targets": false, + "Developer/Avatar/Show My Eye Vectors": false, + "Developer/Avatar/Show Other Eye Vectors": false, + "Developer/Avatar/Show Receive Stats": false, + "Developer/Avatar/Show SensorToWorld Matrix": false, + "Developer/Avatar/Toggle Hips Following": false, + "Developer/Avatar/Turn using Head": false, + "Developer/Debug defaultScripts.js": false, + "Developer/Display Crash Options": true, + "Developer/Enable Speech Control API": false, + "Developer/Entities/Show Realtime Entity Stats": false, + "Developer/Hands/Show Hand Targets": false, + "Developer/Network/Disable Activity Logger": false, + "Developer/Network/Send wrong DS connect version": false, + "Developer/Network/Send wrong protocol version": false, + "Developer/Physics/Highlight Simulation Ownership": false, + "Developer/Physics/Show Bullet Bounding Boxes": false, + "Developer/Physics/Show Bullet Collision": false, + "Developer/Physics/Show Bullet Constraint Limits": false, + "Developer/Physics/Show Bullet Constraints": false, + "Developer/Physics/Show Bullet Contact Points": false, + "Developer/Picking/Force Coarse Picking": false, + "Developer/Render/Ambient Occlusion": false, + "Developer/Render/Compute Blendshapes": true, + "Developer/Render/Decimate Textures": false, + "Developer/Render/Default Skybox": true, + "Developer/Render/Enable Sparse Texture Management": true, + "Developer/Render/Maximum Texture Memory/1024 MB": false, + "Developer/Render/Maximum Texture Memory/2048 MB": false, + "Developer/Render/Maximum Texture Memory/256 MB": false, + "Developer/Render/Maximum Texture Memory/4 MB": false, + "Developer/Render/Maximum Texture Memory/4096 MB": false, + "Developer/Render/Maximum Texture Memory/512 MB": false, + "Developer/Render/Maximum Texture Memory/6144 MB": false, + "Developer/Render/Maximum Texture Memory/64 MB": false, + "Developer/Render/Maximum Texture Memory/8192 MB": false, + "Developer/Render/Maximum Texture Memory/Automatic Texture Memory": true, + "Developer/Render/OpenVR Threaded Submit": true, + "Developer/Render/Scale Resolution/1": true, + "Developer/Render/Scale Resolution/1/2": false, + "Developer/Render/Scale Resolution/1/3": false, + "Developer/Render/Scale Resolution/1/4": false, + "Developer/Render/Scale Resolution/2/3": false, + "Developer/Render/Shadows": true, + "Developer/Render/Temporal Antialiasing (FXAA if disabled)": true, + "Developer/Render/Throttle FPS If Not Focus": true, + "Developer/Render/World Axes": false, + "Developer/Show Animation Stats": false, + "Developer/Show Overlays": true, + "Developer/Show Statistics": false, + "Developer/Timing/Log Extra Timing Details": false, + "Developer/Timing/Log Render Pipeline Warnings": false, + "Developer/Timing/Performance Timer/Display Timing Details": false, + "Developer/Timing/Performance Timer/Expand /myAvatar": false, + "Developer/Timing/Performance Timer/Expand /myAvatar/simulation": false, + "Developer/Timing/Performance Timer/Expand /otherAvatar": false, + "Developer/Timing/Performance Timer/Expand /paintGL": false, + "Developer/Timing/Performance Timer/Expand /physics": false, + "Developer/Timing/Performance Timer/Expand /simulation": false, + "Developer/Timing/Performance Timer/Expand /update": false, + "Developer/Timing/Performance Timer/Only Display Top Ten": true, + "Developer/Timing/Show Timer": false, + "Developer/Timing/Suppress Timings Less than 10ms": false, + "Developer/UI/Desktop Tablet Becomes Toolbar": true, + "Developer/UI/HMD Tablet Becomes Toolbar": false, + "Developer/Verbose Logging": false, + "Display/3D TV - Interleaved": false, + "Display/3D TV - Side by Side Stereo": false, + "Display/Desktop": true, + "Display/Fullscreen": false, + "Display/Oculus Rift": false, + "Display/Oculus Rift (Simulator)": false, + "Edit/Allow Selecting of Large Models": true, + "Edit/Allow Selecting of Lights": true, + "Edit/Allow Selecting of Small Models": true, + "Edit/Auto Focus on Select": false, + "Edit/Create Entities As Grabbable (except Zones, Particles, and Lights)": true, + "Edit/Ease Orientation on Focus": false, + "Edit/Show Lights and Particle Systems in Create Mode": true, + "Edit/Show Zones in Create Mode": true, + "Entities/Show Realtime Entity Stats": false, + "Face Tracking/Auto Mute Microphone": false, + "Face Tracking/Binary Eyelid Control": true, + "Face Tracking/Couple Eyelids": true, + "Face Tracking/Mute Face Tracking": true, + "Face Tracking/None": false, + "Face Tracking/Use Audio for Mouth": true, + "Face Tracking/Use Camera": true, + "Face Tracking/Velocity Filter": true, + "Hands/Show Hand Targets": false, + "Leap Motion/desktopHeightOffset": 0.20000000298023224, + "Leap Motion/enabled": false, + "Leap Motion/sensorLocation": "Desktop", + "Maximum Texture Memory/1024 MB": false, + "Maximum Texture Memory/2048 MB": false, + "Maximum Texture Memory/256 MB": false, + "Maximum Texture Memory/4 MB": false, + "Maximum Texture Memory/4096 MB": false, + "Maximum Texture Memory/512 MB": false, + "Maximum Texture Memory/6144 MB": false, + "Maximum Texture Memory/64 MB": false, + "Maximum Texture Memory/8192 MB": false, + "Maximum Texture Memory/Automatic Texture Memory": true, + "Network/Disable Activity Logger": false, + "Network/Send wrong DS connect version": false, + "Network/Send wrong protocol version": false, + "Perception Neuron/enabled": false, + "Perception Neuron/serverAddress": "localhost", + "Perception Neuron/serverPort": 7001, + "Performance Timer/Display Timing Details": false, + "Performance Timer/Expand /myAvatar": false, + "Performance Timer/Expand /myAvatar/simulation": false, + "Performance Timer/Expand /otherAvatar": false, + "Performance Timer/Expand /paintGL": false, + "Performance Timer/Expand /physics": false, + "Performance Timer/Expand /simulation": false, + "Performance Timer/Expand /update": false, + "Performance Timer/Only Display Top Ten": true, + "Physics/Highlight Simulation Ownership": false, + "Physics/Show Bullet Bounding Boxes": false, + "Physics/Show Bullet Collision": false, + "Physics/Show Bullet Constraint Limits": false, + "Physics/Show Bullet Constraints": false, + "Physics/Show Bullet Contact Points": false, + "Picking/Force Coarse Picking": false, + "Render/Ambient Occlusion": false, + "Render/Compute Blendshapes": true, + "Render/Decimate Textures": false, + "Render/Default Skybox": true, + "Render/Enable Sparse Texture Management": true, + "Render/Maximum Texture Memory/1024 MB": false, + "Render/Maximum Texture Memory/2048 MB": false, + "Render/Maximum Texture Memory/256 MB": false, + "Render/Maximum Texture Memory/4 MB": false, + "Render/Maximum Texture Memory/4096 MB": false, + "Render/Maximum Texture Memory/512 MB": false, + "Render/Maximum Texture Memory/6144 MB": false, + "Render/Maximum Texture Memory/64 MB": false, + "Render/Maximum Texture Memory/8192 MB": false, + "Render/Maximum Texture Memory/Automatic Texture Memory": true, + "Render/OpenVR Threaded Submit": true, + "Render/Scale Resolution/1": true, + "Render/Scale Resolution/1/2": false, + "Render/Scale Resolution/1/3": false, + "Render/Scale Resolution/1/4": false, + "Render/Scale Resolution/2/3": false, + "Render/Shadows": true, + "Render/Temporal Antialiasing (FXAA if disabled)": true, + "Render/Throttle FPS If Not Focus": true, + "Render/World Axes": false, + "RunningScripts": [ + "file:///~//defaultScripts.js" + ], + "SDL2/enabled": true, + "Scale Resolution/1": true, + "Scale Resolution/1/2": false, + "Scale Resolution/1/3": false, + "Scale Resolution/1/4": false, + "Scale Resolution/2/3": false, + "Settings/Ask To Reset Settings on Start": false, + "Settings/Developer Menu": false, + "TabletSounds": "@Variant(\u0000\u0000\u0000\u000b\u0000\u0000\u0000\u0005\u0000\u0000\u0000(\u0000/\u0000s\u0000o\u0000u\u0000n\u0000d\u0000s\u0000/\u0000B\u0000u\u0000t\u0000t\u0000o\u0000n\u00000\u00006\u0000.\u0000w\u0000a\u0000v\u0000\u0000\u0000(\u0000/\u0000s\u0000o\u0000u\u0000n\u0000d\u0000s\u0000/\u0000B\u0000u\u0000t\u0000t\u0000o\u0000n\u00000\u00004\u0000.\u0000w\u0000a\u0000v\u0000\u0000\u0000(\u0000/\u0000s\u0000o\u0000u\u0000n\u0000d\u0000s\u0000/\u0000B\u0000u\u0000t\u0000t\u0000o\u0000n\u00000\u00007\u0000.\u0000w\u0000a\u0000v\u0000\u0000\u0000\"\u0000/\u0000s\u0000o\u0000u\u0000n\u0000d\u0000s\u0000/\u0000T\u0000a\u0000b\u00000\u00001\u0000.\u0000w\u0000a\u0000v\u0000\u0000\u0000\"\u0000/\u0000s\u0000o\u0000u\u0000n\u0000d\u0000s\u0000/\u0000T\u0000a\u0000b\u00000\u00002\u0000.\u0000w\u0000a\u0000v)", + "Timing/Log Extra Timing Details": false, + "Timing/Log Render Pipeline Warnings": false, + "Timing/Performance Timer/Display Timing Details": false, + "Timing/Performance Timer/Expand /myAvatar": false, + "Timing/Performance Timer/Expand /myAvatar/simulation": false, + "Timing/Performance Timer/Expand /otherAvatar": false, + "Timing/Performance Timer/Expand /paintGL": false, + "Timing/Performance Timer/Expand /physics": false, + "Timing/Performance Timer/Expand /simulation": false, + "Timing/Performance Timer/Expand /update": false, + "Timing/Performance Timer/Only Display Top Ten": true, + "Timing/Show Timer": false, + "Timing/Suppress Timings Less than 10ms": false, + "UI/Desktop Tablet Becomes Toolbar": true, + "UI/HMD Tablet Becomes Toolbar": false, + "UserActivityLoggerDisabled": false, + "View/Center Player In View": true, + "View/Enter First Person Mode in HMD": true, + "View/Entity Mode": false, + "View/First Person": true, + "View/Independent Mode": false, + "View/Mirror": false, + "View/Third Person": false, + "WindowGeometry": "@Rect(0 0 1920 1080)", + "WindowRoot.Windows/height": 706, + "WindowRoot.Windows/width": 480, + "WindowState": 0, + "activeDisplayPlugin": "Desktop", + "autoFocusOnSelect": true, + "cameraEaseOnFocus": true, + "desktopLODDecreaseFPS": 30.000001907348633, + "dynamicJitterBuffersEnabled": true, + "firstRun": false, + "hifi.ktx.cache_version": 1, + "hmdLODDecreaseFPS": 34, + "io.highfidelity.attachPoints": "{}", + "io.highfidelity.isEditing": false, + "loginDialogPoppedUp": false, + "sessionRunTime": 42, + "showLightsAndParticlesInEditMode": true, + "showZonesInEditMode": true, + "staticJitterBufferFrames": 1, + "toolbar/com.highfidelity.interface.toolbar.system/desktopHeight": 1059, + "toolbar/com.highfidelity.interface.toolbar.system/x": 655, + "toolbar/com.highfidelity.interface.toolbar.system/y": 953, + "toolbar/constrainToolbarToCenterX": true, + "wallet/autoLogout": true +} diff --git a/tools/auto-tester/AppDataHighFidelity/Interface/AccountInfo.bin b/tools/auto-tester/AppDataHighFidelity/Interface/AccountInfo.bin new file mode 100644 index 0000000000..65c971ea79 Binary files /dev/null and b/tools/auto-tester/AppDataHighFidelity/Interface/AccountInfo.bin differ diff --git a/tools/auto-tester/AppDataHighFidelity/Interface/avatarbookmarks.json b/tools/auto-tester/AppDataHighFidelity/Interface/avatarbookmarks.json new file mode 100644 index 0000000000..9976036f8e --- /dev/null +++ b/tools/auto-tester/AppDataHighFidelity/Interface/avatarbookmarks.json @@ -0,0 +1,861 @@ +{ + "Anime boy": { + "attachments": [ + ], + "avatarEntites": [ + { + "properties": { + "acceleration": { + "x": 0, + "y": 0, + "z": 0 + }, + "actionData": "", + "age": 6.915350914001465, + "ageAsText": "0 hours 0 minutes 6 seconds", + "angularDamping": 0.39346998929977417, + "angularVelocity": { + "x": 0, + "y": 0, + "z": 0 + }, + "animation": { + "allowTranslation": true, + "currentFrame": 0, + "firstFrame": 0, + "fps": 30, + "hold": false, + "lastFrame": 100000, + "loop": true, + "running": false, + "url": "" + }, + "boundingBox": { + "brn": { + "x": -0.10961885005235672, + "y": -0.19444090127944946, + "z": -0.15760529041290283 + }, + "center": { + "x": 2.6226043701171875e-06, + "y": -0.13999652862548828, + "z": -0.04999971389770508 + }, + "dimensions": { + "x": 0.21924294531345367, + "y": 0.10888873785734177, + "z": 0.2152111530303955 + }, + "tfl": { + "x": 0.10962409526109695, + "y": -0.0855521634221077, + "z": 0.057605862617492676 + } + }, + "canCastShadow": true, + "certificateID": "", + "clientOnly": true, + "cloneAvatarEntity": false, + "cloneDynamic": false, + "cloneLifetime": 300, + "cloneLimit": 0, + "cloneOriginID": "{00000000-0000-0000-0000-000000000000}", + "cloneable": false, + "collidesWith": "", + "collisionMask": 0, + "collisionSoundURL": "", + "collisionless": false, + "collisionsWillMove": false, + "compoundShapeURL": "", + "created": "2018-06-06T17:27:53Z", + "damping": 0.39346998929977417, + "density": 1000, + "description": "", + "dimensions": { + "x": 0.21924294531345367, + "y": 0.07768379896879196, + "z": 0.2055898904800415 + }, + "dynamic": false, + "editionNumber": 15, + "entityInstanceNumber": 0, + "friction": 0.5, + "gravity": { + "x": 0, + "y": 0, + "z": 0 + }, + "href": "", + "id": "{5d20c775-a0d7-4163-b158-4e0a784a4625}", + "ignoreForCollisions": false, + "itemArtist": "jyoum", + "itemCategories": "Wearables", + "itemDescription": "Wear these, and others will respect your authoritah.", + "itemLicense": "", + "itemName": "Aviators", + "jointRotations": [ + ], + "jointRotationsSet": [ + ], + "jointTranslations": [ + ], + "jointTranslationsSet": [ + ], + "lastEdited": 1528306178314655, + "lastEditedBy": "{439a2669-4626-487f-9dcf-2d15e77c69a2}", + "lifetime": -1, + "limitedRun": 4294967295, + "localPosition": { + "x": 2.6226043701171875e-06, + "y": -0.13999652862548828, + "z": -0.04999971389770508 + }, + "localRotation": { + "w": 0.9969173073768616, + "x": -0.07845909893512726, + "y": 0, + "z": 0 + }, + "locked": false, + "marketplaceID": "40d879ec-93f0-4b4a-8c58-dd6349bdb058", + "modelURL": "http://mpassets.highfidelity.com/40d879ec-93f0-4b4a-8c58-dd6349bdb058-v1/Aviator.fbx", + "name": "", + "naturalDimensions": { + "x": 0.1660931408405304, + "y": 0.05885136127471924, + "z": 0.15574991703033447 + }, + "naturalPosition": { + "x": 0, + "y": 1.6633577346801758, + "z": 0.048884183168411255 + }, + "originalTextures": "{\n \"aviator:Eyewear2F\": \"http://mpassets.highfidelity.com/40d879ec-93f0-4b4a-8c58-dd6349bdb058-v1/Aviator.fbx/Aviator.fbm/aviator_Eyewear_Diffuse.png\",\n \"aviator:Eyewear2F1\": \"http://mpassets.highfidelity.com/40d879ec-93f0-4b4a-8c58-dd6349bdb058-v1/Aviator.fbx/Aviator.fbm/aviator_Eyewear_Specular.png\"\n}\n", + "owningAvatarID": "{439a2669-4626-487f-9dcf-2d15e77c69a2}", + "parentID": "{439a2669-4626-487f-9dcf-2d15e77c69a2}", + "parentJointIndex": 66, + "position": { + "x": 2.6226043701171875e-06, + "y": -0.13999652862548828, + "z": -0.04999971389770508 + }, + "queryAACube": { + "scale": 0.9313028454780579, + "x": -1.4091639518737793, + "y": -10.133878707885742, + "z": 1.9983724355697632 + }, + "registrationPoint": { + "x": 0.5, + "y": 0.5, + "z": 0.5 + }, + "relayParentJoints": false, + "renderInfo": { + "drawCalls": 1, + "hasTransparent": false, + "texturesCount": 2, + "texturesSize": 1310720, + "verticesCount": 982 + }, + "restitution": 0.5, + "rotation": { + "w": 0.9969173073768616, + "x": -0.07845909893512726, + "y": 0, + "z": 0 + }, + "script": "", + "scriptTimestamp": 0, + "serverScripts": "", + "shapeType": "box", + "staticCertificateVersion": 0, + "textures": "", + "type": "Model", + "userData": "{\"Attachment\":{\"action\":\"attach\",\"joint\":\"HeadTop_End\",\"attached\":false,\"options\":{\"translation\":{\"x\":0,\"y\":0,\"z\":0},\"scale\":1}},\"grabbableKey\":{\"cloneable\":false,\"grabbable\":true}}", + "velocity": { + "x": 0, + "y": 0, + "z": 0 + }, + "visible": true + } + } + ], + "avatarScale": 1, + "avatarUrl": "http://mpassets.highfidelity.com/46e0fd52-3cff-462f-ba97-927338d88295-v1/AnimeBoy2.fst", + "version": 3 + }, + "Anime girl": { + "attachments": [ + ], + "avatarEntites": [ + { + "properties": { + "acceleration": { + "x": 0, + "y": 0, + "z": 0 + }, + "actionData": "", + "age": 19.66267967224121, + "ageAsText": "0 hours 0 minutes 19 seconds", + "angularDamping": 0.39346998929977417, + "angularVelocity": { + "x": 0, + "y": 0, + "z": 0 + }, + "animation": { + "allowTranslation": true, + "currentFrame": 0, + "firstFrame": 0, + "fps": 30, + "hold": false, + "lastFrame": 100000, + "loop": true, + "running": false, + "url": "" + }, + "boundingBox": { + "brn": { + "x": -0.10536206513643265, + "y": -0.16647332906723022, + "z": -0.12632352113723755 + }, + "center": { + "x": 0, + "y": -0.12999999523162842, + "z": -0.030000001192092896 + }, + "dimensions": { + "x": 0.2107241302728653, + "y": 0.07294666767120361, + "z": 0.1926470398902893 + }, + "tfl": { + "x": 0.10536206513643265, + "y": -0.09352666139602661, + "z": 0.06632351875305176 + } + }, + "canCastShadow": true, + "certificateID": "", + "clientOnly": true, + "cloneAvatarEntity": false, + "cloneDynamic": false, + "cloneLifetime": 300, + "cloneLimit": 0, + "cloneOriginID": "{00000000-0000-0000-0000-000000000000}", + "cloneable": false, + "collidesWith": "", + "collisionMask": 0, + "collisionSoundURL": "", + "collisionless": false, + "collisionsWillMove": false, + "compoundShapeURL": "", + "created": "2018-06-05T00:10:37Z", + "damping": 0.39346998929977417, + "density": 1000, + "description": "", + "dimensions": { + "x": 0.2107241302728653, + "y": 0.07294666767120361, + "z": 0.1926470398902893 + }, + "dynamic": false, + "editionNumber": 5, + "entityInstanceNumber": 0, + "friction": 0.5, + "gravity": { + "x": 0, + "y": 0, + "z": 0 + }, + "href": "", + "id": "{1586b83a-2af7-4532-9bfb-82fe3f5d5ce9}", + "ignoreForCollisions": false, + "itemArtist": "moam_00", + "itemCategories": "Wearables", + "itemDescription": "Perfect for side-glancin'.", + "itemLicense": "", + "itemName": "Blacker Fem Glasses", + "jointRotations": [ + ], + "jointRotationsSet": [ + ], + "jointTranslations": [ + ], + "jointTranslationsSet": [ + ], + "lastEdited": 1528157470041658, + "lastEditedBy": "{425df1a8-289b-42fc-819c-c3b2a12d7165}", + "lifetime": -1, + "limitedRun": 4294967295, + "localPosition": { + "x": 0, + "y": -0.12999999523162842, + "z": -0.029999999329447746 + }, + "localRotation": { + "w": 1, + "x": -2.2351741790771484e-08, + "y": 3.4924596548080444e-10, + "z": 3.725290298461914e-09 + }, + "locked": false, + "marketplaceID": "06781d12-9139-48f4-ac2a-417dde090981", + "modelURL": "http://mpassets.highfidelity.com/06781d12-9139-48f4-ac2a-417dde090981-v1/FemGlasses03.fbx", + "name": "Female Glasses 3 by Mario Andrade", + "naturalDimensions": { + "x": 0.16209548711776733, + "y": 0.05611282214522362, + "z": 0.14819003641605377 + }, + "naturalPosition": { + "x": 0, + "y": -7.636845111846924e-08, + "z": 0 + }, + "originalTextures": "{\n \"file49\": \"http://mpassets.highfidelity.com/06781d12-9139-48f4-ac2a-417dde090981-v1/FemGlasses03.fbx/FemGlasses03.fbm/FemGlasses03Mat_Mixed_AO.jpg\",\n \"file81\": \"http://mpassets.highfidelity.com/06781d12-9139-48f4-ac2a-417dde090981-v1/FemGlasses03.fbx/FemGlasses03.fbm/FemGlasses03Mat_Metallic.jpg\",\n \"file84\": \"http://mpassets.highfidelity.com/06781d12-9139-48f4-ac2a-417dde090981-v1/FemGlasses03.fbx/FemGlasses03.fbm/FemGlasses03Mat_Roughness.jpg\",\n \"file86\": \"http://mpassets.highfidelity.com/06781d12-9139-48f4-ac2a-417dde090981-v1/FemGlasses03.fbx/FemGlasses03.fbm/FemGlasses03Mat_Base_Color.jpg\",\n \"file87\": \"http://mpassets.highfidelity.com/06781d12-9139-48f4-ac2a-417dde090981-v1/FemGlasses03.fbx/FemGlasses03.fbm/FemGlasses03Mat_Normal_DirectX.jpg\"\n}\n", + "owningAvatarID": "{1277f725-fbb4-478b-ae79-1241fd90e508}", + "parentID": "{1277f725-fbb4-478b-ae79-1241fd90e508}", + "parentJointIndex": 66, + "position": { + "x": 0, + "y": -0.12999999523162842, + "z": -0.029999999329447746 + }, + "queryAACube": { + "scale": 0.8840523958206177, + "x": -2.6587564945220947, + "y": -10.162277221679688, + "z": -0.9548344016075134 + }, + "registrationPoint": { + "x": 0.5, + "y": 0.5, + "z": 0.5 + }, + "relayParentJoints": false, + "renderInfo": { + "drawCalls": 1, + "hasTransparent": false, + "texturesCount": 5, + "texturesSize": 0, + "verticesCount": 1156 + }, + "restitution": 0.5, + "rotation": { + "w": 1, + "x": -2.2351741790771484e-08, + "y": 3.4924596548080444e-10, + "z": 3.725290298461914e-09 + }, + "script": "", + "scriptTimestamp": 0, + "serverScripts": "", + "shapeType": "box", + "staticCertificateVersion": 0, + "textures": "", + "type": "Model", + "userData": "{\"Attachment\":{\"action\":\"attach\",\"joint\":\"HeadTop_End\",\"attached\":false,\"options\":{\"translation\":{\"x\":0,\"y\":0,\"z\":0},\"scale\":1}},\"grabbableKey\":{\"cloneable\":false,\"grabbable\":true}}", + "velocity": { + "x": 0, + "y": 0, + "z": 0 + }, + "visible": true + } + } + ], + "avatarScale": 1, + "avatarUrl": "http://mpassets.highfidelity.com/0dce3426-55c8-4641-8dd5-d76eb575b64a-v1/Anime_F_Outfit.fst", + "version": 3 + }, + "Last Legends: Male": { + "attachments": [ + ], + "avatarEntites": [ + { + "properties": { + "acceleration": { + "blue": 0, + "green": 0, + "red": 0, + "x": 0, + "y": 0, + "z": 0 + }, + "actionData": "", + "age": 321.8835144042969, + "ageAsText": "0 hours 5 minutes 21 seconds", + "angularDamping": 0.39346998929977417, + "angularVelocity": { + "blue": 0, + "green": 0, + "red": 0, + "x": 0, + "y": 0, + "z": 0 + }, + "animation": { + "allowTranslation": true, + "currentFrame": 0, + "firstFrame": 0, + "fps": 30, + "hold": false, + "lastFrame": 100000, + "loop": true, + "running": false, + "url": "" + }, + "boundingBox": { + "brn": { + "blue": -0.03950843587517738, + "green": 0.20785385370254517, + "red": -0.04381325840950012, + "x": -0.04381325840950012, + "y": 0.20785385370254517, + "z": -0.03950843587517738 + }, + "center": { + "blue": 0, + "green": 0.23000000417232513, + "red": 0, + "x": 0, + "y": 0.23000000417232513, + "z": 0 + }, + "dimensions": { + "blue": 0.07901687175035477, + "green": 0.044292300939559937, + "red": 0.08762651681900024, + "x": 0.08762651681900024, + "y": 0.044292300939559937, + "z": 0.07901687175035477 + }, + "tfl": { + "blue": 0.03950843587517738, + "green": 0.2521461546421051, + "red": 0.04381325840950012, + "x": 0.04381325840950012, + "y": 0.2521461546421051, + "z": 0.03950843587517738 + } + }, + "canCastShadow": true, + "certificateID": "", + "clientOnly": true, + "cloneAvatarEntity": false, + "cloneDynamic": false, + "cloneLifetime": 300, + "cloneLimit": 0, + "cloneOriginID": "{00000000-0000-0000-0000-000000000000}", + "cloneable": false, + "collidesWith": "", + "collisionMask": 0, + "collisionSoundURL": "", + "collisionless": false, + "collisionsWillMove": false, + "compoundShapeURL": "", + "created": "2018-07-26T23:56:46Z", + "damping": 0.39346998929977417, + "density": 1000, + "description": "", + "dimensions": { + "blue": 0.07229919731616974, + "green": 0.06644226610660553, + "red": 0.03022606298327446, + "x": 0.03022606298327446, + "y": 0.06644226610660553, + "z": 0.07229919731616974 + }, + "dynamic": false, + "editionNumber": 58, + "entityInstanceNumber": 0, + "friction": 0.5, + "gravity": { + "blue": 0, + "green": 0, + "red": 0, + "x": 0, + "y": 0, + "z": 0 + }, + "href": "", + "id": "{03053239-bb37-4c51-a013-a1772baaeed5}", + "ignoreForCollisions": false, + "itemArtist": "jyoum", + "itemCategories": "Wearables", + "itemDescription": "A cool scifi watch for your avatar!", + "itemLicense": "", + "itemName": "Scifi Watch", + "jointRotations": [ + ], + "jointRotationsSet": [ + ], + "jointTranslations": [ + ], + "jointTranslationsSet": [ + ], + "lastEdited": 1532649569894305, + "lastEditedBy": "{042ac463-7879-40f0-8126-e2e56c4345ca}", + "lifetime": -1, + "limitedRun": 4294967295, + "localPosition": { + "blue": 0, + "green": 0.23000000417232513, + "red": 0, + "x": 0, + "y": 0.23000000417232513, + "z": 0 + }, + "localRotation": { + "w": 0.5910986065864563, + "x": -0.48726415634155273, + "y": -0.4088630974292755, + "z": 0.49599072337150574 + }, + "locked": false, + "marketplaceID": "0685794d-fddb-4bad-a608-6d7789ceda90", + "modelURL": "http://mpassets.highfidelity.com/0685794d-fddb-4bad-a608-6d7789ceda90-v1/ScifiWatch.fbx", + "name": "Scifi Watch by Jimi", + "naturalDimensions": { + "blue": 0.055614765733480453, + "green": 0.0511094331741333, + "red": 0.023250818252563477, + "x": 0.023250818252563477, + "y": 0.0511094331741333, + "z": 0.055614765733480453 + }, + "naturalPosition": { + "blue": -0.06031447649002075, + "green": 1.4500460624694824, + "red": 0.6493338942527771, + "x": 0.6493338942527771, + "y": 1.4500460624694824, + "z": -0.06031447649002075 + }, + "originalTextures": "{\n \"file4\": \"http://mpassets.highfidelity.com/0685794d-fddb-4bad-a608-6d7789ceda90-v1/ScifiWatch.fbx/ScifiWatch/texture/lambert1_Base_Color.png\",\n \"file5\": \"http://mpassets.highfidelity.com/0685794d-fddb-4bad-a608-6d7789ceda90-v1/ScifiWatch.fbx/ScifiWatch/texture/lambert1_Normal_OpenGL.png\",\n \"file6\": \"http://mpassets.highfidelity.com/0685794d-fddb-4bad-a608-6d7789ceda90-v1/ScifiWatch.fbx/ScifiWatch/texture/lambert1_Metallic.png\",\n \"file7\": \"http://mpassets.highfidelity.com/0685794d-fddb-4bad-a608-6d7789ceda90-v1/ScifiWatch.fbx/ScifiWatch/texture/lambert1_Roughness.png\",\n \"file8\": \"http://mpassets.highfidelity.com/0685794d-fddb-4bad-a608-6d7789ceda90-v1/ScifiWatch.fbx/ScifiWatch/texture/lambert1_Emissive.png\"\n}\n", + "owningAvatarID": "{042ac463-7879-40f0-8126-e2e56c4345ca}", + "parentID": "{042ac463-7879-40f0-8126-e2e56c4345ca}", + "parentJointIndex": 16, + "position": { + "blue": 0, + "green": 0.23000000417232513, + "red": 0, + "x": 0, + "y": 0.23000000417232513, + "z": 0 + }, + "queryAACube": { + "scale": 0.3082179129123688, + "x": 495.7716979980469, + "y": 498.345703125, + "z": 498.52044677734375 + }, + "registrationPoint": { + "blue": 0.5, + "green": 0.5, + "red": 0.5, + "x": 0.5, + "y": 0.5, + "z": 0.5 + }, + "relayParentJoints": false, + "renderInfo": { + "drawCalls": 1, + "hasTransparent": false, + "texturesCount": 5, + "texturesSize": 786432, + "verticesCount": 273 + }, + "restitution": 0.5, + "rotation": { + "w": 0.5910986065864563, + "x": -0.48726415634155273, + "y": -0.4088630974292755, + "z": 0.49599072337150574 + }, + "script": "", + "scriptTimestamp": 0, + "serverScripts": "", + "shapeType": "box", + "staticCertificateVersion": 0, + "textures": "", + "type": "Model", + "userData": "{\"Attachment\":{\"action\":\"attach\",\"joint\":\"[LR]ForeArm\",\"attached\":false,\"options\":{\"translation\":{\"x\":0,\"y\":0,\"z\":0},\"scale\":1}},\"grabbableKey\":{\"cloneable\":false,\"grabbable\":true}}", + "velocity": { + "blue": 0, + "green": 0, + "red": 0, + "x": 0, + "y": 0, + "z": 0 + }, + "visible": true + } + }, + { + "properties": { + "acceleration": { + "blue": 0, + "green": 0, + "red": 0, + "x": 0, + "y": 0, + "z": 0 + }, + "actionData": "", + "age": 308.8044128417969, + "ageAsText": "0 hours 5 minutes 8 seconds", + "angularDamping": 0.39346998929977417, + "angularVelocity": { + "blue": 0, + "green": 0, + "red": 0, + "x": 0, + "y": 0, + "z": 0 + }, + "animation": { + "allowTranslation": true, + "currentFrame": 0, + "firstFrame": 0, + "fps": 30, + "hold": false, + "lastFrame": 100000, + "loop": true, + "running": false, + "url": "" + }, + "boundingBox": { + "brn": { + "blue": -0.2340194433927536, + "green": -0.07067721337080002, + "red": -0.17002610862255096, + "x": -0.17002610862255096, + "y": -0.07067721337080002, + "z": -0.2340194433927536 + }, + "center": { + "blue": -0.039825439453125, + "green": 0.02001953125, + "red": 0.0001678466796875, + "x": 0.0001678466796875, + "y": 0.02001953125, + "z": -0.039825439453125 + }, + "dimensions": { + "blue": 0.3883880078792572, + "green": 0.18139348924160004, + "red": 0.34038791060447693, + "x": 0.34038791060447693, + "y": 0.18139348924160004, + "z": 0.3883880078792572 + }, + "tfl": { + "blue": 0.1543685644865036, + "green": 0.11071627587080002, + "red": 0.17036180198192596, + "x": 0.17036180198192596, + "y": 0.11071627587080002, + "z": 0.1543685644865036 + } + }, + "canCastShadow": true, + "certificateID": "", + "clientOnly": true, + "cloneAvatarEntity": false, + "cloneDynamic": false, + "cloneLifetime": 300, + "cloneLimit": 0, + "cloneOriginID": "{00000000-0000-0000-0000-000000000000}", + "cloneable": false, + "collidesWith": "", + "collisionMask": 0, + "collisionSoundURL": "", + "collisionless": false, + "collisionsWillMove": false, + "compoundShapeURL": "", + "created": "2018-07-26T23:56:46Z", + "damping": 0.39346998929977417, + "density": 1000, + "description": "", + "dimensions": { + "blue": 0.38838762044906616, + "green": 0.16981728374958038, + "red": 0.33466479182243347, + "x": 0.33466479182243347, + "y": 0.16981728374958038, + "z": 0.38838762044906616 + }, + "dynamic": false, + "editionNumber": 18, + "entityInstanceNumber": 0, + "friction": 0.5, + "gravity": { + "blue": 0, + "green": 0, + "red": 0, + "x": 0, + "y": 0, + "z": 0 + }, + "href": "", + "id": "{1bf231ce-3913-4c53-be3c-b1f4094dac51}", + "ignoreForCollisions": false, + "itemArtist": "jyoum", + "itemCategories": "Wearables", + "itemDescription": "A stylish and classic piece of headwear for your avatar.", + "itemLicense": "", + "itemName": "Fedora", + "jointRotations": [ + ], + "jointRotationsSet": [ + ], + "jointTranslations": [ + ], + "jointTranslationsSet": [ + ], + "lastEdited": 1532649698129709, + "lastEditedBy": "{042ac463-7879-40f0-8126-e2e56c4345ca}", + "lifetime": -1, + "limitedRun": 4294967295, + "localPosition": { + "blue": -0.039825439453125, + "green": 0.02001953125, + "red": 0.0001678466796875, + "x": 0.0001678466796875, + "y": 0.02001953125, + "z": -0.039825439453125 + }, + "localRotation": { + "w": 0.9998477101325989, + "x": -9.898545982878204e-09, + "y": 5.670873406415922e-07, + "z": 0.017452405765652657 + }, + "locked": false, + "marketplaceID": "11c4208d-15d7-4449-9758-a08da6dbd3dc", + "modelURL": "http://mpassets.highfidelity.com/11c4208d-15d7-4449-9758-a08da6dbd3dc-v1/Fedora.fbx", + "name": "", + "naturalDimensions": { + "blue": 0.320981502532959, + "green": 0.14034485816955566, + "red": 0.2765824794769287, + "x": 0.2765824794769287, + "y": 0.14034485816955566, + "z": 0.320981502532959 + }, + "naturalPosition": { + "blue": 0.022502630949020386, + "green": 1.7460365295410156, + "red": 0.000143393874168396, + "x": 0.000143393874168396, + "y": 1.7460365295410156, + "z": 0.022502630949020386 + }, + "originalTextures": "{\n \"file5\": \"http://mpassets.highfidelity.com/11c4208d-15d7-4449-9758-a08da6dbd3dc-v1/Fedora.fbx/Texture/Fedora_Hat1_Base_Color.png\",\n \"file7\": \"http://mpassets.highfidelity.com/11c4208d-15d7-4449-9758-a08da6dbd3dc-v1/Fedora.fbx/Texture/Fedora_Hat1_Roughness.png\"\n}\n", + "owningAvatarID": "{042ac463-7879-40f0-8126-e2e56c4345ca}", + "parentID": "{042ac463-7879-40f0-8126-e2e56c4345ca}", + "parentJointIndex": 66, + "position": { + "blue": -0.039825439453125, + "green": 0.02001953125, + "red": 0.0001678466796875, + "x": 0.0001678466796875, + "y": 0.02001953125, + "z": -0.039825439453125 + }, + "queryAACube": { + "scale": 1.6202316284179688, + "x": 495.21051025390625, + "y": 498.5577697753906, + "z": 497.6370849609375 + }, + "registrationPoint": { + "blue": 0.5, + "green": 0.5, + "red": 0.5, + "x": 0.5, + "y": 0.5, + "z": 0.5 + }, + "relayParentJoints": false, + "renderInfo": { + "drawCalls": 1, + "hasTransparent": false, + "texturesCount": 2, + "texturesSize": 327680, + "verticesCount": 719 + }, + "restitution": 0.5, + "rotation": { + "w": 0.9998477101325989, + "x": -9.898545982878204e-09, + "y": 5.670873406415922e-07, + "z": 0.017452405765652657 + }, + "script": "", + "scriptTimestamp": 0, + "serverScripts": "", + "shapeType": "box", + "staticCertificateVersion": 0, + "textures": "", + "type": "Model", + "userData": "{\"Attachment\":{\"action\":\"attach\",\"joint\":\"HeadTop_End\",\"attached\":false,\"options\":{\"translation\":{\"x\":0,\"y\":0,\"z\":0},\"scale\":1}},\"grabbableKey\":{\"cloneable\":false,\"grabbable\":true}}", + "velocity": { + "blue": 0, + "green": 0, + "red": 0, + "x": 0, + "y": 0, + "z": 0 + }, + "visible": true + } + } + ], + "avatarScale": 1, + "avatarUrl": "http://mpassets.highfidelity.com/28569047-6f1a-4100-af67-8054ec397cc3-v1/LLMale2.fst", + "version": 3 + }, + "Last legends Female": { + "attachments": [ + ], + "avatarEntites": [ + ], + "avatarScale": 1, + "avatarUrl": "http://mpassets.highfidelity.com/8d823be5-6197-4418-b984-eb94160ed956-v1/LLFemale_Clothes.fst", + "version": 3 + }, + "Matthew": { + "attachments": [ + ], + "avatarEntites": [ + ], + "avatarScale": 1, + "avatarUrl": "http://mpassets.highfidelity.com/b652081b-a199-425e-ae5c-7815721bdc09-v1/matthew.fst", + "version": 3 + }, + "Priscilla": { + "attachments": [ + ], + "avatarEntites": [ + ], + "avatarScale": 1, + "avatarUrl": "http://mpassets.highfidelity.com/e7565f93-8bc5-47c2-b6eb-b3b31d4a1339-v1/priscilla.fst", + "version": 3 + }, + "Woody": { + "attachments": [ + ], + "avatarEntites": [ + ], + "avatarScale": 1, + "avatarUrl": "http://mpassets.highfidelity.com/ad348528-de38-420c-82bb-054cb22163f5-v1/mannequin.fst", + "version": 3 + } +} diff --git a/tools/auto-tester/AppDataHighFidelity/assignment-client/entities/models.json.gz b/tools/auto-tester/AppDataHighFidelity/assignment-client/entities/models.json.gz new file mode 100644 index 0000000000..1eeeac50ac Binary files /dev/null and b/tools/auto-tester/AppDataHighFidelity/assignment-client/entities/models.json.gz differ diff --git a/tools/auto-tester/AppDataHighFidelity/domain-server/AccountInfo.bin b/tools/auto-tester/AppDataHighFidelity/domain-server/AccountInfo.bin new file mode 100644 index 0000000000..645e34895e Binary files /dev/null and b/tools/auto-tester/AppDataHighFidelity/domain-server/AccountInfo.bin differ diff --git a/tools/auto-tester/AppDataHighFidelity/domain-server/config.json b/tools/auto-tester/AppDataHighFidelity/domain-server/config.json new file mode 100644 index 0000000000..d662d89f28 --- /dev/null +++ b/tools/auto-tester/AppDataHighFidelity/domain-server/config.json @@ -0,0 +1,7 @@ +{ + "metaverse": { + "automatic_networking": "full", + "id": "17b1cb9c-08c4-45aa-9257-163ad3913529" + }, + "version": 2.2 +} diff --git a/tools/auto-tester/AppDataHighFidelity/domain-server/entities/models.json.gz b/tools/auto-tester/AppDataHighFidelity/domain-server/entities/models.json.gz new file mode 100644 index 0000000000..1eeeac50ac Binary files /dev/null and b/tools/auto-tester/AppDataHighFidelity/domain-server/entities/models.json.gz differ diff --git a/tools/auto-tester/CMakeLists.txt b/tools/auto-tester/CMakeLists.txt index 1546a35f4c..c8c0a336d2 100644 --- a/tools/auto-tester/CMakeLists.txt +++ b/tools/auto-tester/CMakeLists.txt @@ -48,4 +48,19 @@ if (WIN32) POST_BUILD COMMAND CMD /C "SET PATH=%PATH%;${QT_DIR}/bin && ${WINDEPLOYQT_COMMAND} ${EXTRA_DEPLOY_OPTIONS} $<$,$,$>:--release> \"$\"" ) + + # add a custom command to copy the empty Apps/Data High Fidelity folder (i.e. - a valid folder with no entities) + add_custom_command( + TARGET ${TARGET_NAME} + POST_BUILD + COMMAND "${CMAKE_COMMAND}" -E copy_directory "${CMAKE_CURRENT_SOURCE_DIR}/AppDataHighFidelity" "$/AppDataHighFidelity" + ) + + # add a custom command to copy the SSL DLLs + add_custom_command( + TARGET ${TARGET_NAME} + POST_BUILD + COMMAND "${CMAKE_COMMAND}" -E copy_directory "$ENV{VCPKG_ROOT}/installed/x64-windows/bin" "$" + ) + endif () \ No newline at end of file diff --git a/tools/auto-tester/Create.PNG b/tools/auto-tester/Create.PNG index d82d4873a2..85d70e59ed 100644 Binary files a/tools/auto-tester/Create.PNG and b/tools/auto-tester/Create.PNG differ diff --git a/tools/auto-tester/Evaluate.PNG b/tools/auto-tester/Evaluate.PNG index d530dec994..2f711f61a3 100644 Binary files a/tools/auto-tester/Evaluate.PNG and b/tools/auto-tester/Evaluate.PNG differ diff --git a/tools/auto-tester/README.md b/tools/auto-tester/README.md index 0924e77f8b..e029955edc 100644 --- a/tools/auto-tester/README.md +++ b/tools/auto-tester/README.md @@ -5,14 +5,16 @@ The auto-tester is a stand alone application that provides a mechanism for regre * The snapshots are compared to a 'canonical' set of images that have been produced beforehand. * The result, if any test failed, is a zipped folder describing the failure. -Auto-tester has 4 functions, separated into 4 tabs: +Auto-tester has 5 functions, separated into 4 tabs: 1. Creating tests, MD files and recursive scripts -2. Evaluating the results of running tests -3. TestRail interface -4. Windows task bar utility (Windows only) +1. Windows task bar utility (Windows only) +1. Running tests +1. Evaluating the results of running tests +1. Web interface + ## Installation ### Executable -1. Download the installer by browsing to [here](). +1. Download the installer by browsing to [here](). 2. Double click on the installer and install to a convenient location ![](./setup_7z.PNG) 3. To run the auto-tester, double click **auto-tester.exe**. @@ -25,6 +27,17 @@ Python 3 can be downloaded from: 3. Mac (**macOS 64-bit/32-bit installer** or **macOS 64-bit/32-bit installer**) After installation - create an environment variable called PYTHON_PATH and set it to the folder containing the Python executable. +### AWS interface +#### Windows +1. Download the AWS CLI from `https://aws.amazon.com/cli/` +1. Install (installer is named `AWSCLI64PY3.msi`) +1. Open a new command prompt and run `aws configure` +1. Enter the AWS account number +1. Enter the secret key +1. Leave region name and ouput format as default [None] + +1. Install the latest release of Boto3 via pip: +>pip install boto3 # Create ![](./Create.PNG) @@ -36,6 +49,9 @@ This function is used to create/update Expected Images after a successful run of The user will be asked for the snapshot folder and then the tests root folder. All snapshots located in the snapshot folder will be used to create or update the expected images in the relevant tests. ### Details As an example - if the snapshots folder contains an image named `tests.content.entity.zone.zoneOrientation.00003.png`, then this file will be copied to `tests/contente/enity/zone/zoneOrientation/ExpectedImage0003.png`. +## Create Tests Outline +### Usage +This function creates an MD file in the (user-selected) tests root folder. The file provides links to both the tests and the MD files. ## Create MD file ### Usage This function creates a file named `test.md` from a `test.js` script. The user will be asked for the folder containing the test script: @@ -54,13 +70,20 @@ The process to produce the MD file is a simplistic parse of the test script. ### Usage This function creates all MD files recursively from the user-selected root folder. This can be any folder in the tests hierarchy (e.g. all engine\material tests). -The file provides a hierarchial list of all the tests -## Create Tests Outline +The file provides a hierarchal list of all the tests +## Create testAuto script ### Usage -This function creates an MD file in the (user-selected) tests root folder. The file provides links to both the tests and the MD files. +This function creates a script named `testAuto.js` in a user-selected test folder. +### Details +The script created runs the `test.js` script in the folder in automatic mode. The script is the same for all tests. +## Create all testAuto scripts +### Usage +This function creates all testAuto scripts recursively from the user-selected root folder. This can be any folder in the tests hierarchy (e.g. all engine\material tests). + +The file provides a hierarchical list of all the tests ## Create Recursive Script ### Usage -After the user selects a folder within the tests hierarchy, a script is created, named `testRecursive.js`. This script calls all `test.js` scripts in the subfolders. +After the user selects a folder within the tests hierarchy, a script is created, named `testRecursive.js`. This script calls all `test.js` scripts in the sub-folders. ### Details The various scripts are called in alphabetical order. @@ -92,6 +115,34 @@ autoTester.runRecursive(); In this case all recursive scripts, from the selected folder down, are created. Running this function in the tests root folder will create (or update) all the recursive scripts. +# Windows +![](./Windows.PNG) + +This tab is Windows-specific. It provides buttons to hide and show the task bar. + +The task bar should be hidden for all tests that use the primary camera. This is required to ensure that the snapshots are the right size. +# Run +![](./Run.PNG) +The run tab is used to run tests in automatic mode. The tests require the location of a folder to store files in; this folder can safely be re-used for any number of runs (the "Working Folder"). +The test script that is run is `https://github.com/highfidelity/hifi_tests/blob/master/tests/testRecursive.js`. The user can use a different branch' or even repository, if required. +Tests can be run server-less, or with the local host. In the second case, the domain-server and assignment-clients are run before starting Interface. +The default is to run the latest build. The user can select a specific build or PR if desired. +Testing can be started immediately, or run on a schedule. + +A test run is performed in a number of steps: +1. If the latest run has been selected then the `dev-builds.xml` file is downloaded to identify the latest build. +1. The installer is then downloaded. +1. After downloading the High Fidelity application is installed. This requires that UAC be disabled! +1. Any instances of the server-console, assignment-client, domain-server or Interface are killed before running the tests. +1. Interface is run with the appropriate command line parameters. +1. The expected images are then downloaded from GitHub and compared to the actual images from the tests. + +The working folder will ultimately contain the following: +1. A folder named `High Fidelity`. This is where High Fidelity is installed. +1. A folder named `snapshots`. This folder contains the zipped results folders (one for each run). It also contains both the actual images and the expected images from the last run; note that these are deleted before running tests (All PNG files in the folder are deleted. In addition - a text file named `tests_completed.txt` is created at the end of the test script - this signals that Interface did not crash during the test run. +1. The `dev-builds.xml` file, if it was downloaded. +1. The HighFidelity installer. Note that this is always named `HighFidelity-Beta-latest-dev` so as not to store too many installers over time. +1. A log file describing the runs. This file is appended to after each run. # Evaluate ![](./Evaluate.PNG) @@ -103,13 +154,13 @@ If any tests have failed, then a zipped folder will be created in the snapshots ### Usage Before starting the evaluation, make sure the GitHub user and branch are set correctly. The user should not normally be changed, but the branch may need to be set to the appropriate RC. -After setting the checkbox as required and pressing Evaluate - the user will be asked for the snapshots folder. +After setting the check-box as required and pressing Evaluate - the user will be asked for the snapshots folder. ### Details Evaluation proceeds in a number of steps: 1. A folder is created to store any failures -1. The expecetd images are download from GitHub. They are named slightly differently from the snapshots (e.g. `tests.engine.render.effect.highlight.coverage.00000.png` and `tests.engine.render.effect.highlight.coverage.00000_EI.png`). +1. The expected images are download from GitHub. They are named slightly differently from the snapshots (e.g. `tests.engine.render.effect.highlight.coverage.00000.png` and `tests.engine.render.effect.highlight.coverage.00000_EI.png`). 1. The images are then pair-wise compared, using the SSIM algorithm. A fixed threshold is used to define a mismatch. @@ -119,8 +170,10 @@ Evaluation proceeds in a number of steps: 1. If not in interactive mode, or the user has defined the results as an error, an error is written into the error folder. The error itself is a folder with the 3 images and a small text file containing details. 1. At the end of the test, the folder is zipped and the original folder is deleted. If there are no errors then the zipped folder will be empty. -# TestRail -![](./TestRail.PNG) + +# Web Interface +![](./WebInterface.PNG) +This tab has two functions: updating the TestRail cases, runs and results, and creating web page reports that are stored on AWS. Before updating TestRail, make sure the GitHub user and branch are set correctly. The user should not normally be changed, but the branch may need to be set to the appropriate RC. @@ -129,9 +182,9 @@ Any access to TestRail will require the TestRail account (default is High Fideli ![](./TestRailSelector.PNG) - The default test rail user is shown, and can be changed as needed. -- The username is usually the user's email. +- The user-name is usually the user's email. - The Project ID defaults to 14 - Interface. -- The Suite ID defaults to 1147 - Renderong. +- The Suite ID defaults to 1147 - Rendering. - The TestRail page provides 3 functions for writing to TestRail. ## Create Test Cases ### Usage @@ -188,10 +241,7 @@ A number of Python scripts are created: - `getRuns.py` reads the release names from TestRail - `addRun` is the script that writes to TestRail. -In addition - a file containing all the releases will be created - `runs.txt` -# Windows -![](./Windows.PNG) - -This tab is Windows-specific. It provides buttons to hide and show the task bar. - -The task bar should be hidden for all tests that use the primary camera. This is required to ensure that the snapshots are the right size. \ No newline at end of file +In addition - a file containing all the releases will be created - `runs.txt`. +## Create Web Page +This function requests a zipped results folder and converts it to a web page. The page is created in a user-selecetd working folder. +If the `Update AWS` checkbox is checked then the page will also be copied to AWS, and the appropriate URL will be displayed in the window below the button. diff --git a/tools/auto-tester/Run.PNG b/tools/auto-tester/Run.PNG new file mode 100644 index 0000000000..29f81299c8 Binary files /dev/null and b/tools/auto-tester/Run.PNG differ diff --git a/tools/auto-tester/TestRail.PNG b/tools/auto-tester/TestRail.PNG deleted file mode 100644 index 042d0cf1cb..0000000000 Binary files a/tools/auto-tester/TestRail.PNG and /dev/null differ diff --git a/tools/auto-tester/Web Interface.PNG b/tools/auto-tester/Web Interface.PNG new file mode 100644 index 0000000000..d7216e3c18 Binary files /dev/null and b/tools/auto-tester/Web Interface.PNG differ diff --git a/tools/auto-tester/WebInterface.PNG b/tools/auto-tester/WebInterface.PNG new file mode 100644 index 0000000000..d9d3df3fc6 Binary files /dev/null and b/tools/auto-tester/WebInterface.PNG differ diff --git a/tools/auto-tester/Windows.PNG b/tools/auto-tester/Windows.PNG index bf7b76ba02..32e3bfd53e 100644 Binary files a/tools/auto-tester/Windows.PNG and b/tools/auto-tester/Windows.PNG differ diff --git a/tools/auto-tester/src/AWSInterface.cpp b/tools/auto-tester/src/AWSInterface.cpp new file mode 100644 index 0000000000..628db5329c --- /dev/null +++ b/tools/auto-tester/src/AWSInterface.cpp @@ -0,0 +1,413 @@ +// +// AWSInterface.cpp +// +// Created by Nissim Hadar on 3 Oct 2018. +// Copyright 2013 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// +#include "AWSInterface.h" + +#include +#include +#include + +#include +#include + +AWSInterface::AWSInterface(QObject* parent) : QObject(parent) { + _pythonInterface = new PythonInterface(); + _pythonCommand = _pythonInterface->getPythonCommand(); +} + +void AWSInterface::createWebPageFromResults(const QString& testResults, + const QString& workingDirectory, + QCheckBox* updateAWSCheckBox, + QLineEdit* urlLineEdit) { + _testResults = testResults; + _workingDirectory = workingDirectory; + + _urlLineEdit = urlLineEdit; + _urlLineEdit->setEnabled(false); + + extractTestFailuresFromZippedFolder(); + createHTMLFile(); + + if (updateAWSCheckBox->isChecked()) { + updateAWS(); + } +} + +void AWSInterface::extractTestFailuresFromZippedFolder() { + // For a test results zip file called `D:/tt/TestResults--2018-10-02_16-54-11(9426)[DESKTOP-PMKNLSQ].zip` + // the folder will be called `TestResults--2018-10-02_16-54-11(9426)[DESKTOP-PMKNLSQ]` + // and, this folder will be in the workign directory + QStringList parts =_testResults.split('/'); + QString zipFolderName = _workingDirectory + "/" + parts[parts.length() - 1].split('.')[0]; + if (QDir(zipFolderName).exists()) { + QDir dir = zipFolderName; + dir.removeRecursively(); + } + + QDir().mkdir(_workingDirectory); + JlCompress::extractDir(_testResults, _workingDirectory); +} + +void AWSInterface::createHTMLFile() { + // For file named `D:/tt/snapshots/TestResults--2018-10-03_15-35-28(9433)[DESKTOP-PMKNLSQ].zip` + // - the HTML will be in a folder named `TestResults--2018-10-03_15-35-28(9433)[DESKTOP-PMKNLSQ]` + QStringList pathComponents = _testResults.split('/'); + QString filename = pathComponents[pathComponents.length() - 1]; + _resultsFolder = filename.left(filename.length() - 4); + + QString resultsPath = _workingDirectory + "/" + _resultsFolder + "/"; + QDir().mkdir(resultsPath); + _htmlFilename = resultsPath + HTML_FILENAME; + + QFile file(_htmlFilename); + if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) { + QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), + "Could not create '" + _htmlFilename + "'"); + exit(-1); + } + + QTextStream stream(&file); + + startHTMLpage(stream); + writeHead(stream); + writeBody(stream); + finishHTMLpage(stream); + + file.close(); +} + +void AWSInterface::startHTMLpage(QTextStream& stream) { + stream << "\n"; + stream << "\n"; +} + +void AWSInterface::writeHead(QTextStream& stream) { + stream << "\t" << "\n"; + stream << "\t" << "\t" << "\n"; + stream << "\t" << "\n"; +} + +void AWSInterface::writeBody(QTextStream& stream) { + stream << "\t" << "\n"; + writeTitle(stream); + writeTable(stream); + stream << "\t" << "\n"; +} + +void AWSInterface::finishHTMLpage(QTextStream& stream) { + stream << "\n"; +} + +void AWSInterface::writeTitle(QTextStream& stream) { + // Separate relevant components from the results name + // The expected format is as follows: `D:/tt/snapshots/TestResults--2018-10-04_11-09-41(PR14128)[DESKTOP-PMKNLSQ].zip` + QStringList tokens = _testResults.split('/'); + + // date_buildorPR_hostName will be 2018-10-03_15-35-28(9433)[DESKTOP-PMKNLSQ] + QString date_buildorPR_hostName = tokens[tokens.length() - 1].split("--")[1].split(".")[0]; + + QString buildorPR = date_buildorPR_hostName.split('(')[1].split(')')[0]; + QString hostName = date_buildorPR_hostName.split('[')[1].split(']')[0]; + + QStringList dateList = date_buildorPR_hostName.split('(')[0].split('_')[0].split('-'); + QString year = dateList[0]; + QString month = dateList[1]; + QString day = dateList[2]; + + QStringList timeList = date_buildorPR_hostName.split('(')[0].split('_')[1].split('-'); + QString hour = timeList[0]; + QString minute = timeList[1]; + QString second = timeList[2]; + + const QString months[] = { "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" }; + + stream << "\t" << "\t" << "\n"; + stream << "\t" << "\t" << "

      Failures for "; + stream << months[month.toInt() - 1] << " " << day << ", " << year << ", "; + stream << hour << ":" << minute << ":" << second << ", "; + + if (buildorPR.left(2) == "PR") { + stream << "PR " << buildorPR.right(buildorPR.length() - 2) << ", "; + } else { + stream << "build " << buildorPR << ", "; + } + + stream << "run on " << hostName << "

      \n"; +} + +void AWSInterface::writeTable(QTextStream& stream) { + QString previousTestName{ "" }; + + // Loop over all entries in directory. This is done in stages, as the names are not in the order of the tests + // The first stage reads the directory names into a list + // The second stage renames the tests by removing everything up to "--tests." + // The third stage renames the directories + // The fourth and lasts stage creates the HTML entries + // + // Note that failures are processed first, then successes + QStringList originalNamesFailures; + QStringList originalNamesSuccesses; + QDirIterator it1(_workingDirectory.toStdString().c_str()); + while (it1.hasNext()) { + QString nextDirectory = it1.next(); + + // Skip `.` and `..` directories + if (nextDirectory.right(1) == ".") { + continue; + } + + // Only process failure folders + if (!nextDirectory.contains("--tests.")) { + continue; + } + + // Look at the filename at the end of the path + QStringList parts = nextDirectory.split('/'); + QString name = parts[parts.length() - 1]; + if (name.left(7) == "Failure") { + originalNamesFailures.append(nextDirectory); + } else { + originalNamesSuccesses.append(nextDirectory); + } + } + + QStringList newNamesFailures; + for (int i = 0; i < originalNamesFailures.length(); ++i) { + newNamesFailures.append(originalNamesFailures[i].split("--tests.")[1]); + } + + QStringList newNamesSuccesses; + for (int i = 0; i < originalNamesSuccesses.length(); ++i) { + newNamesSuccesses.append(originalNamesSuccesses[i].split("--tests.")[1]); + } + + _htmlFailuresFolder = _workingDirectory + "/" + _resultsFolder + "/" + FAILURES_FOLDER; + QDir().mkdir(_htmlFailuresFolder); + + _htmlSuccessesFolder = _workingDirectory + "/" + _resultsFolder + "/" + SUCCESSES_FOLDER; + QDir().mkdir(_htmlSuccessesFolder); + + for (int i = 0; i < newNamesFailures.length(); ++i) { + QDir().rename(originalNamesFailures[i], _htmlFailuresFolder + "/" + newNamesFailures[i]); + } + + for (int i = 0; i < newNamesSuccesses.length(); ++i) { + QDir().rename(originalNamesSuccesses[i], _htmlSuccessesFolder + "/" + newNamesSuccesses[i]); + } + + QDirIterator it2((_htmlFailuresFolder).toStdString().c_str()); + while (it2.hasNext()) { + QString nextDirectory = it2.next(); + + // Skip `.` and `..` directories, as well as the HTML directory + if (nextDirectory.right(1) == "." || nextDirectory.contains(QString("/") + _resultsFolder + "/TestResults--")) { + continue; + } + + QStringList pathComponents = nextDirectory.split('/'); + QString filename = pathComponents[pathComponents.length() - 1]; + int splitIndex = filename.lastIndexOf("."); + QString testName = filename.left(splitIndex).replace(".", " / "); + QString testNumber = filename.right(filename.length() - (splitIndex + 1)); + + // The failures are ordered lexicographically, so we know that we can rely on the testName changing to create a new table + if (testName != previousTestName) { + if (!previousTestName.isEmpty()) { + closeTable(stream); + } + + previousTestName = testName; + + stream << "\t\t

      " << testName << "

      \n"; + + openTable(stream); + } + + createEntry(testNumber.toInt(), filename, stream, true); + } + + closeTable(stream); + stream << "\t" << "\t" << "\n"; + stream << "\t" << "\t" << "

      The following tests passed:

      "; + + QDirIterator it3((_htmlSuccessesFolder).toStdString().c_str()); + while (it3.hasNext()) { + QString nextDirectory = it3.next(); + + // Skip `.` and `..` directories, as well as the HTML directory + if (nextDirectory.right(1) == "." || nextDirectory.contains(QString("/") + _resultsFolder + "/TestResults--")) { + continue; + } + + QStringList pathComponents = nextDirectory.split('/'); + QString filename = pathComponents[pathComponents.length() - 1]; + int splitIndex = filename.lastIndexOf("."); + QString testName = filename.left(splitIndex).replace(".", " / "); + QString testNumber = filename.right(filename.length() - (splitIndex + 1)); + + // The failures are ordered lexicographically, so we know that we can rely on the testName changing to create a new table + if (testName != previousTestName) { + if (!previousTestName.isEmpty()) { + closeTable(stream); + } + + previousTestName = testName; + + stream << "\t\t

      " << testName << "

      \n"; + + openTable(stream); + } + + createEntry(testNumber.toInt(), filename, stream, false); + } + + closeTable(stream); +} + +void AWSInterface::openTable(QTextStream& stream) { + stream << "\t\t\n"; + stream << "\t\t\t\n"; + stream << "\t\t\t\t\n"; + stream << "\t\t\t\t\n"; + stream << "\t\t\t\t\n"; + stream << "\t\t\t\t\n"; + stream << "\t\t\t\n"; +} + +void AWSInterface::closeTable(QTextStream& stream) { + stream << "\t\t

      Test

      Actual Image

      Expected Image

      Difference Image

      \n"; +} + +void AWSInterface::createEntry(int index, const QString& testResult, QTextStream& stream, const bool isFailure) { + stream << "\t\t\t\n"; + stream << "\t\t\t\t

      " << QString::number(index) << "

      \n"; + + // For a test named `D:/t/fgadhcUDHSFaidsfh3478JJJFSDFIUSOEIrf/Failure_1--tests.engine.interaction.pick.collision.many.00000` + // we need `Failure_1--tests.engine.interaction.pick.collision.many.00000` + QStringList resultNameComponents = testResult.split('/'); + QString resultName = resultNameComponents[resultNameComponents.length() - 1]; + + QString folder; + bool differenceFileFound; + if (isFailure) { + folder = FAILURES_FOLDER; + differenceFileFound = QFile::exists(_htmlFailuresFolder + "/" + resultName + "/Difference Image.png"); + } else { + folder = SUCCESSES_FOLDER; + differenceFileFound = QFile::exists(_htmlSuccessesFolder + "/" + resultName + "/Difference Image.png"); + } + + + stream << "\t\t\t\t\n"; + stream << "\t\t\t\t\n"; + + if (differenceFileFound) { + stream << "\t\t\t\t\n"; + } else { + stream << "\t\t\t\t

      No Image Found

      \n"; + } + + stream << "\t\t\t\n"; +} + +void AWSInterface::updateAWS() { + QString filename = _workingDirectory + "/updateAWS.py"; + if (QFile::exists(filename)) { + QFile::remove(filename); + } + QFile file(filename); + + if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) { + QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), + "Could not create 'addTestCases.py'"); + exit(-1); + } + + QTextStream stream(&file); + + stream << "import boto3\n"; + stream << "s3 = boto3.resource('s3')\n\n"; + + QDirIterator it1(_htmlFailuresFolder.toStdString().c_str()); + while (it1.hasNext()) { + QString nextDirectory = it1.next(); + + // Skip `.` and `..` directories + if (nextDirectory.right(1) == ".") { + continue; + } + + // nextDirectory looks like `D:/t/TestResults--2018-10-02_16-54-11(9426)[DESKTOP-PMKNLSQ]/failures/engine.render.effect.bloom.00000` + // We need to concatenate the last 3 components, to get `TestResults--2018-10-02_16-54-11(9426)[DESKTOP-PMKNLSQ]/failures/engine.render.effect.bloom.00000` + QStringList parts = nextDirectory.split('/'); + QString filename = parts[parts.length() - 3] + "/" + parts[parts.length() - 2] + "/" + parts[parts.length() - 1]; + + stream << "data = open('" << _workingDirectory << "/" << filename << "/" << "Actual Image.png" << "', 'rb')\n"; + stream << "s3.Bucket('hifi-content').put_object(Bucket='" << AWS_BUCKET << "', Key='" << filename << "/" << "Actual Image.png" << "', Body=data)\n\n"; + + stream << "data = open('" << _workingDirectory << "/" << filename << "/" << "Expected Image.png" << "', 'rb')\n"; + stream << "s3.Bucket('hifi-content').put_object(Bucket='" << AWS_BUCKET << "', Key='" << filename << "/" << "Expected Image.png" << "', Body=data)\n\n"; + + if (QFile::exists(_htmlFailuresFolder + "/" + parts[parts.length() - 1] + "/Difference Image.png")) { + stream << "data = open('" << _workingDirectory << "/" << filename << "/" << "Difference Image.png" << "', 'rb')\n"; + stream << "s3.Bucket('hifi-content').put_object(Bucket='" << AWS_BUCKET << "', Key='" << filename << "/" << "Difference Image.png" << "', Body=data)\n\n"; + } + } + + QDirIterator it2(_htmlSuccessesFolder.toStdString().c_str()); + while (it2.hasNext()) { + QString nextDirectory = it2.next(); + + // Skip `.` and `..` directories + if (nextDirectory.right(1) == ".") { + continue; + } + + // nextDirectory looks like `D:/t/TestResults--2018-10-02_16-54-11(9426)[DESKTOP-PMKNLSQ]/successes/engine.render.effect.bloom.00000` + // We need to concatenate the last 3 components, to get `TestResults--2018-10-02_16-54-11(9426)[DESKTOP-PMKNLSQ]/successes/engine.render.effect.bloom.00000` + QStringList parts = nextDirectory.split('/'); + QString filename = parts[parts.length() - 3] + "/" + parts[parts.length() - 2] + "/" + parts[parts.length() - 1]; + + stream << "data = open('" << _workingDirectory << "/" << filename << "/" << "Actual Image.png" << "', 'rb')\n"; + stream << "s3.Bucket('hifi-content').put_object(Bucket='" << AWS_BUCKET << "', Key='" << filename << "/" << "Actual Image.png" << "', Body=data)\n\n"; + + stream << "data = open('" << _workingDirectory << "/" << filename << "/" << "Expected Image.png" << "', 'rb')\n"; + stream << "s3.Bucket('hifi-content').put_object(Bucket='" << AWS_BUCKET << "', Key='" << filename << "/" << "Expected Image.png" << "', Body=data)\n\n"; + + if (QFile::exists(_htmlSuccessesFolder + "/" + parts[parts.length() - 1] + "/Difference Image.png")) { + stream << "data = open('" << _workingDirectory << "/" << filename << "/" << "Difference Image.png" << "', 'rb')\n"; + stream << "s3.Bucket('hifi-content').put_object(Bucket='" << AWS_BUCKET << "', Key='" << filename << "/" << "Difference Image.png" << "', Body=data)\n\n"; + } + } + + stream << "data = open('" << _workingDirectory << "/" << _resultsFolder << "/" << HTML_FILENAME << "', 'rb')\n"; + stream << "s3.Bucket('hifi-content').put_object(Bucket='" << AWS_BUCKET << "', Key='" << _resultsFolder << "/" + << HTML_FILENAME << "', Body=data, ContentType='text/html')\n"; + + file.close(); + + // Show user the URL + _urlLineEdit->setEnabled(true); + _urlLineEdit->setText(QString("https://") + AWS_BUCKET + ".s3.amazonaws.com/" + _resultsFolder + "/" + HTML_FILENAME); + _urlLineEdit->setCursorPosition(0); + + QProcess* process = new QProcess(); + + connect(process, &QProcess::started, this, [=]() { _busyWindow.exec(); }); + connect(process, SIGNAL(finished(int)), process, SLOT(deleteLater())); + connect(process, static_cast(&QProcess::finished), this, + [=](int exitCode, QProcess::ExitStatus exitStatus) { _busyWindow.hide(); }); + + QStringList parameters = QStringList() << filename ; + process->start(_pythonCommand, parameters); +} diff --git a/tools/auto-tester/src/AWSInterface.h b/tools/auto-tester/src/AWSInterface.h new file mode 100644 index 0000000000..c5be5f35bb --- /dev/null +++ b/tools/auto-tester/src/AWSInterface.h @@ -0,0 +1,72 @@ +// +// AWSInterface.h +// +// Created by Nissim Hadar on 3 Oct 2018. +// Copyright 2013 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_AWSInterface_h +#define hifi_AWSInterface_h + +#include +#include +#include +#include + +#include "ui/BusyWindow.h" + +#include "PythonInterface.h" + +class AWSInterface : public QObject { + Q_OBJECT +public: + explicit AWSInterface(QObject* parent = 0); + + void createWebPageFromResults(const QString& testResults, + const QString& workingDirectory, + QCheckBox* updateAWSCheckBox, + QLineEdit* urlLineEdit); + + void extractTestFailuresFromZippedFolder(); + void createHTMLFile(); + + void startHTMLpage(QTextStream& stream); + void writeHead(QTextStream& stream); + void writeBody(QTextStream& stream); + void finishHTMLpage(QTextStream& stream); + + void writeTitle(QTextStream& stream); + void writeTable(QTextStream& stream); + void openTable(QTextStream& stream); + void closeTable(QTextStream& stream); + + void createEntry(int index, const QString& testResult, QTextStream& stream, const bool isFailure); + + void updateAWS(); + +private: + QString _testResults; + QString _workingDirectory; + QString _resultsFolder; + QString _htmlFailuresFolder; + QString _htmlSuccessesFolder; + QString _htmlFilename; + + const QString FAILURES_FOLDER{ "failures" }; + const QString SUCCESSES_FOLDER{ "successes" }; + const QString HTML_FILENAME{ "TestResults.html" }; + + BusyWindow _busyWindow; + + PythonInterface* _pythonInterface; + QString _pythonCommand; + + QString AWS_BUCKET{ "hifi-qa" }; + + QLineEdit* _urlLineEdit; +}; + +#endif // hifi_AWSInterface_h \ No newline at end of file diff --git a/tools/auto-tester/src/Downloader.cpp b/tools/auto-tester/src/Downloader.cpp index 530a3b61bd..cb9863f34d 100644 --- a/tools/auto-tester/src/Downloader.cpp +++ b/tools/auto-tester/src/Downloader.cpp @@ -11,20 +11,20 @@ #include -Downloader::Downloader(QUrl imageUrl, QObject *parent) : QObject(parent) { +Downloader::Downloader(QUrl fileURL, QObject *parent) : QObject(parent) { connect( &_networkAccessManager, SIGNAL (finished(QNetworkReply*)), this, SLOT (fileDownloaded(QNetworkReply*)) ); - QNetworkRequest request(imageUrl); + QNetworkRequest request(fileURL); _networkAccessManager.get(request); } void Downloader::fileDownloaded(QNetworkReply* reply) { QNetworkReply::NetworkError error = reply->error(); if (error != QNetworkReply::NetworkError::NoError) { - QMessageBox::information(0, "Test Aborted", "Failed to download image: " + reply->errorString()); + QMessageBox::information(0, "Test Aborted", "Failed to download file: " + reply->errorString()); return; } diff --git a/tools/auto-tester/src/Downloader.h b/tools/auto-tester/src/Downloader.h index b0ad58fac5..6d1029698f 100644 --- a/tools/auto-tester/src/Downloader.h +++ b/tools/auto-tester/src/Downloader.h @@ -30,7 +30,7 @@ class Downloader : public QObject { Q_OBJECT public: - explicit Downloader(QUrl imageUrl, QObject *parent = 0); + explicit Downloader(QUrl fileURL, QObject *parent = 0); QByteArray downloadedData() const; diff --git a/tools/auto-tester/src/PythonInterface.cpp b/tools/auto-tester/src/PythonInterface.cpp new file mode 100644 index 0000000000..4922b8a8df --- /dev/null +++ b/tools/auto-tester/src/PythonInterface.cpp @@ -0,0 +1,32 @@ +// +// PythonInterface.cpp +// +// Created by Nissim Hadar on Oct 6, 2018. +// Copyright 2013 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// +#include "PythonInterface.h" + +#include +#include +#include + +PythonInterface::PythonInterface() { + if (QProcessEnvironment::systemEnvironment().contains("PYTHON_PATH")) { + QString _pythonPath = QProcessEnvironment::systemEnvironment().value("PYTHON_PATH"); + if (!QFile::exists(_pythonPath + "/" + _pythonExe)) { + QMessageBox::critical(0, _pythonExe, QString("Python executable not found in ") + _pythonPath); + } + _pythonCommand = _pythonPath + "/" + _pythonExe; + } else { + QMessageBox::critical(0, "PYTHON_PATH not defined", + "Please set PYTHON_PATH to directory containing the Python executable"); + exit(-1); + } +} + +QString PythonInterface::getPythonCommand() { + return _pythonCommand; +} diff --git a/tools/auto-tester/src/PythonInterface.h b/tools/auto-tester/src/PythonInterface.h new file mode 100644 index 0000000000..f32a39a644 --- /dev/null +++ b/tools/auto-tester/src/PythonInterface.h @@ -0,0 +1,26 @@ +// +// PythonInterface.h +// +// Created by Nissim Hadar on Oct 6, 2018. +// Copyright 2013 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// +#ifndef hifi_PythonInterface_h +#define hifi_PythonInterface_h + +#include + +class PythonInterface { +public: + PythonInterface(); + + QString getPythonCommand(); + +private: + const QString _pythonExe{ "python.exe" }; + QString _pythonCommand; +}; + +#endif // hifi_PythonInterface_h diff --git a/tools/auto-tester/src/Test.cpp b/tools/auto-tester/src/Test.cpp index 3da789f405..582f6209af 100644 --- a/tools/auto-tester/src/Test.cpp +++ b/tools/auto-tester/src/Test.cpp @@ -12,6 +12,7 @@ #include #include #include +#include #include #include @@ -23,7 +24,10 @@ extern AutoTester* autoTester; #include -Test::Test() { +Test::Test(QProgressBar* progressBar, QCheckBox* checkBoxInteractiveMode) { + _progressBar = progressBar; + _checkBoxInteractiveMode = checkBoxInteractiveMode; + _mismatchWindow.setModal(true); if (autoTester) { @@ -34,17 +38,17 @@ Test::Test() { bool Test::createTestResultsFolderPath(const QString& directory) { QDateTime now = QDateTime::currentDateTime(); - _testResultsFolderPath = directory + "/" + TEST_RESULTS_FOLDER + "--" + now.toString(DATETIME_FORMAT); + _testResultsFolderPath = directory + "/" + TEST_RESULTS_FOLDER + "--" + now.toString(DATETIME_FORMAT) + "(local)[" + QHostInfo::localHostName() + "]"; QDir testResultsFolder(_testResultsFolderPath); // Create a new test results folder return QDir().mkdir(_testResultsFolderPath); } -void Test::zipAndDeleteTestResultsFolder() { +QString Test::zipAndDeleteTestResultsFolder() { QString zippedResultsFileName { _testResultsFolderPath + ".zip" }; QFileInfo fileInfo(zippedResultsFileName); - if (!fileInfo.exists()) { + if (fileInfo.exists()) { QFile::remove(zippedResultsFileName); } @@ -55,25 +59,30 @@ void Test::zipAndDeleteTestResultsFolder() { //In all cases, for the next evaluation _testResultsFolderPath = ""; - _index = 1; + _failureIndex = 1; + _successIndex = 1; + + return zippedResultsFileName; } -bool Test::compareImageLists(bool isInteractiveMode, QProgressBar* progressBar) { - progressBar->setMinimum(0); - progressBar->setMaximum(_expectedImagesFullFilenames.length() - 1); - progressBar->setValue(0); - progressBar->setVisible(true); +int Test::compareImageLists() { + _progressBar->setMinimum(0); + _progressBar->setMaximum(_expectedImagesFullFilenames.length() - 1); + _progressBar->setValue(0); + _progressBar->setVisible(true); // Loop over both lists and compare each pair of images // Quit loop if user has aborted due to a failed test. - bool success{ true }; bool keepOn{ true }; + int numberOfFailures{ 0 }; for (int i = 0; keepOn && i < _expectedImagesFullFilenames.length(); ++i) { // First check that images are the same size QImage resultImage(_resultImagesFullFilenames[i]); QImage expectedImage(_expectedImagesFullFilenames[i]); double similarityIndex; // in [-1.0 .. 1.0], where 1.0 means images are identical + + bool isInteractiveMode = (!_isRunningFromCommandLine && _checkBoxInteractiveMode->isChecked() && !_isRunningInAutomaticTestRun); // similarityIndex is set to -100.0 to indicate images are not the same size if (isInteractiveMode && (resultImage.width() != expectedImage.width() || resultImage.height() != expectedImage.height())) { @@ -83,19 +92,20 @@ bool Test::compareImageLists(bool isInteractiveMode, QProgressBar* progressBar) similarityIndex = _imageComparer.compareImages(resultImage, expectedImage); } - if (similarityIndex < THRESHOLD) { - TestFailure testFailure = TestFailure{ - (float)similarityIndex, - _expectedImagesFullFilenames[i].left(_expectedImagesFullFilenames[i].lastIndexOf("/") + 1), // path to the test (including trailing /) - QFileInfo(_expectedImagesFullFilenames[i].toStdString().c_str()).fileName(), // filename of expected image - QFileInfo(_resultImagesFullFilenames[i].toStdString().c_str()).fileName() // filename of result image - }; + TestResult testResult = TestResult{ + (float)similarityIndex, + _expectedImagesFullFilenames[i].left(_expectedImagesFullFilenames[i].lastIndexOf("/") + 1), // path to the test (including trailing /) + QFileInfo(_expectedImagesFullFilenames[i].toStdString().c_str()).fileName(), // filename of expected image + QFileInfo(_resultImagesFullFilenames[i].toStdString().c_str()).fileName() // filename of result image + }; - _mismatchWindow.setTestFailure(testFailure); + _mismatchWindow.setTestResult(testResult); + + if (similarityIndex < THRESHOLD) { + ++numberOfFailures; if (!isInteractiveMode) { - appendTestResultsToFile(_testResultsFolderPath, testFailure, _mismatchWindow.getComparisonImage()); - success = false; + appendTestResultsToFile(_testResultsFolderPath, testResult, _mismatchWindow.getComparisonImage(), true); } else { _mismatchWindow.exec(); @@ -103,41 +113,53 @@ bool Test::compareImageLists(bool isInteractiveMode, QProgressBar* progressBar) case USER_RESPONSE_PASS: break; case USE_RESPONSE_FAIL: - appendTestResultsToFile(_testResultsFolderPath, testFailure, _mismatchWindow.getComparisonImage()); - success = false; + appendTestResultsToFile(_testResultsFolderPath, testResult, _mismatchWindow.getComparisonImage(), true); break; case USER_RESPONSE_ABORT: keepOn = false; - success = false; break; default: assert(false); break; } } + } else { + appendTestResultsToFile(_testResultsFolderPath, testResult, _mismatchWindow.getComparisonImage(), false); } - progressBar->setValue(i); + _progressBar->setValue(i); } - progressBar->setVisible(false); - return success; + _progressBar->setVisible(false); + return numberOfFailures; } -void Test::appendTestResultsToFile(const QString& _testResultsFolderPath, TestFailure testFailure, QPixmap comparisonImage) { +void Test::appendTestResultsToFile(const QString& _testResultsFolderPath, TestResult testResult, QPixmap comparisonImage, bool hasFailed) { if (!QDir().exists(_testResultsFolderPath)) { QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), "Folder " + _testResultsFolderPath + " not found"); exit(-1); } - QString failureFolderPath { _testResultsFolderPath + "/Failure_" + QString::number(_index) + "--" + testFailure._actualImageFilename.left(testFailure._actualImageFilename.length() - 4) }; - if (!QDir().mkdir(failureFolderPath)) { - QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), "Failed to create folder " + failureFolderPath); + QString resultFolderPath; + if (hasFailed) { + resultFolderPath = _testResultsFolderPath + "/Failure_" + QString::number(_failureIndex) + "--" + + testResult._actualImageFilename.left(testResult._actualImageFilename.length() - 4); + + ++_failureIndex; + } else { + resultFolderPath = _testResultsFolderPath + "/Success_" + QString::number(_successIndex) + "--" + + testResult._actualImageFilename.left(testResult._actualImageFilename.length() - 4); + + ++_successIndex; + } + + if (!QDir().mkdir(resultFolderPath)) { + QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), + "Failed to create folder " + resultFolderPath); exit(-1); } - ++_index; - QFile descriptionFile(failureFolderPath + "/" + TEST_RESULTS_FILENAME); + QFile descriptionFile(resultFolderPath + "/" + TEST_RESULTS_FILENAME); if (!descriptionFile.open(QIODevice::ReadWrite)) { QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), "Failed to create file " + TEST_RESULTS_FILENAME); exit(-1); @@ -145,10 +167,10 @@ void Test::appendTestResultsToFile(const QString& _testResultsFolderPath, TestFa // Create text file describing the failure QTextStream stream(&descriptionFile); - stream << "Test failed in folder " << testFailure._pathname.left(testFailure._pathname.length() - 1) << endl; // remove trailing '/' - stream << "Expected image was " << testFailure._expectedImageFilename << endl; - stream << "Actual image was " << testFailure._actualImageFilename << endl; - stream << "Similarity _index was " << testFailure._error << endl; + stream << "Test failed in folder " << testResult._pathname.left(testResult._pathname.length() - 1) << endl; // remove trailing '/' + stream << "Expected image was " << testResult._expectedImageFilename << endl; + stream << "Actual image was " << testResult._actualImageFilename << endl; + stream << "Similarity index was " << testResult._error << endl; descriptionFile.close(); @@ -156,26 +178,34 @@ void Test::appendTestResultsToFile(const QString& _testResultsFolderPath, TestFa QString sourceFile; QString destinationFile; - sourceFile = testFailure._pathname + testFailure._expectedImageFilename; - destinationFile = failureFolderPath + "/" + "Expected Image.png"; + sourceFile = testResult._pathname + testResult._expectedImageFilename; + destinationFile = resultFolderPath + "/" + "Expected Image.png"; if (!QFile::copy(sourceFile, destinationFile)) { QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), "Failed to copy " + sourceFile + " to " + destinationFile); exit(-1); } - sourceFile = testFailure._pathname + testFailure._actualImageFilename; - destinationFile = failureFolderPath + "/" + "Actual Image.png"; + sourceFile = testResult._pathname + testResult._actualImageFilename; + destinationFile = resultFolderPath + "/" + "Actual Image.png"; if (!QFile::copy(sourceFile, destinationFile)) { QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), "Failed to copy " + sourceFile + " to " + destinationFile); exit(-1); } - comparisonImage.save(failureFolderPath + "/" + "Difference Image.png"); + comparisonImage.save(resultFolderPath + "/" + "Difference Image.png"); } -void Test::startTestsEvaluation(const QString& testFolder, const QString& branchFromCommandLine, const QString& userFromCommandLine) { - if (testFolder.isNull()) { - // Get list of JPEG images in folder, sorted by name +void Test::startTestsEvaluation(const bool isRunningFromCommandLine, + const bool isRunningInAutomaticTestRun, + const QString& snapshotDirectory, + const QString& branchFromCommandLine, + const QString& userFromCommandLine +) { + _isRunningFromCommandLine = isRunningFromCommandLine; + _isRunningInAutomaticTestRun = isRunningInAutomaticTestRun; + + if (snapshotDirectory.isNull()) { + // Get list of PNG images in folder, sorted by name QString previousSelection = _snapshotDirectory; QString parent = previousSelection.left(previousSelection.lastIndexOf('/')); if (!parent.isNull() && parent.right(1) != "/") { @@ -184,14 +214,14 @@ void Test::startTestsEvaluation(const QString& testFolder, const QString& branch _snapshotDirectory = QFileDialog::getExistingDirectory(nullptr, "Please select folder containing the test images", parent, QFileDialog::ShowDirsOnly); - // If user cancelled then restore previous selection and return + // If user canceled then restore previous selection and return if (_snapshotDirectory == "") { _snapshotDirectory = previousSelection; return; } } else { - _snapshotDirectory = testFolder; - _exitWhenComplete = true; + _snapshotDirectory = snapshotDirectory; + _exitWhenComplete = (isRunningFromCommandLine && !isRunningInAutomaticTestRun); } // Quit if test results folder could not be created @@ -238,25 +268,28 @@ void Test::startTestsEvaluation(const QString& testFolder, const QString& branch } } - autoTester->downloadImages(expectedImagesURLs, _snapshotDirectory, _expectedImagesFilenames); + autoTester->downloadFiles(expectedImagesURLs, _snapshotDirectory, _expectedImagesFilenames, (void *)this); } - -void Test::finishTestsEvaluation(bool isRunningFromCommandline, bool interactiveMode, QProgressBar* progressBar) { - bool success = compareImageLists((!isRunningFromCommandline && interactiveMode), progressBar); +void Test::finishTestsEvaluation() { + int numberOfFailures = compareImageLists(); - if (interactiveMode && !isRunningFromCommandline) { - if (success) { + if (!_isRunningFromCommandLine && !_isRunningInAutomaticTestRun) { + if (numberOfFailures == 0) { QMessageBox::information(0, "Success", "All images are as expected"); } else { QMessageBox::information(0, "Failure", "One or more images are not as expected"); } } - zipAndDeleteTestResultsFolder(); + QString zippedFolderName = zipAndDeleteTestResultsFolder(); if (_exitWhenComplete) { exit(0); } + + if (_isRunningInAutomaticTestRun) { + autoTester->automaticTestRunEvaluationComplete(zippedFolderName, numberOfFailures); + } } bool Test::isAValidDirectory(const QString& pathname) { @@ -302,176 +335,8 @@ void Test::includeTest(QTextStream& textStream, const QString& testPathname) { textStream << "Script.include(testsRootPath + \"" << partialPathWithoutTests + "\");" << endl; } -// Creates a single script in a user-selected folder. -// This script will run all text.js scripts in every applicable sub-folder -void Test::createRecursiveScript() { - // Select folder to start recursing from - QString previousSelection = _testDirectory; - QString parent = previousSelection.left(previousSelection.lastIndexOf('/')); - if (!parent.isNull() && parent.right(1) != "/") { - parent += "/"; - } - - _testDirectory = - QFileDialog::getExistingDirectory(nullptr, "Please select folder that will contain the top level test script", parent, - QFileDialog::ShowDirsOnly); - - // If user cancelled then restore previous selection and return - if (_testDirectory == "") { - _testDirectory = previousSelection; - return; - } - - createRecursiveScript(_testDirectory, true); -} - -// This method creates a `testRecursive.js` script in every sub-folder. -void Test::createAllRecursiveScripts() { - // Select folder to start recursing from - QString previousSelection = _testsRootDirectory; - QString parent = previousSelection.left(previousSelection.lastIndexOf('/')); - if (!parent.isNull() && parent.right(1) != "/") { - parent += "/"; - } - - _testsRootDirectory = QFileDialog::getExistingDirectory(nullptr, "Please select the root folder for the recursive scripts", - parent, QFileDialog::ShowDirsOnly); - - // If user cancelled then restore previous selection and return - if (_testsRootDirectory == "") { - _testsRootDirectory = previousSelection; - return; - } - - createRecursiveScript(_testsRootDirectory, false); - - QDirIterator it(_testsRootDirectory.toStdString().c_str(), QDirIterator::Subdirectories); - while (it.hasNext()) { - QString directory = it.next(); - - // Only process directories - QDir dir; - if (!isAValidDirectory(directory)) { - continue; - } - - // Only process directories that have sub-directories - bool hasNoSubDirectories{ true }; - QDirIterator it2(directory.toStdString().c_str(), QDirIterator::Subdirectories); - while (it2.hasNext()) { - QString directory2 = it2.next(); - - // Only process directories - QDir dir; - if (isAValidDirectory(directory2)) { - hasNoSubDirectories = false; - break; - } - } - - if (!hasNoSubDirectories) { - createRecursiveScript(directory, false); - } - } - - QMessageBox::information(0, "Success", "Scripts have been created"); -} - -void Test::createRecursiveScript(const QString& topLevelDirectory, bool interactiveMode) { - const QString recursiveTestsFilename("testRecursive.js"); - QFile allTestsFilename(topLevelDirectory + "/" + recursiveTestsFilename); - if (!allTestsFilename.open(QIODevice::WriteOnly | QIODevice::Text)) { - QMessageBox::critical(0, - "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), - "Failed to create \"" + recursiveTestsFilename + "\" in directory \"" + topLevelDirectory + "\"" - ); - - exit(-1); - } - - QTextStream textStream(&allTestsFilename); - - const QString DATE_TIME_FORMAT("MMM d yyyy, h:mm"); - textStream << "// This is an automatically generated file, created by auto-tester on " << QDateTime::currentDateTime().toString(DATE_TIME_FORMAT) << endl << endl; - - // Include 'autoTest.js' - QString branch = autoTester->getSelectedBranch(); - QString user = autoTester->getSelectedUser(); - - textStream << "PATH_TO_THE_REPO_PATH_UTILS_FILE = \"https://raw.githubusercontent.com/" + user + "/hifi_tests/" + branch + "/tests/utils/branchUtils.js\";" << endl; - textStream << "Script.include(PATH_TO_THE_REPO_PATH_UTILS_FILE);" << endl; - textStream << "var autoTester = createAutoTester(Script.resolvePath(\".\"));" << endl << endl; - - textStream << "var testsRootPath = autoTester.getTestsRootPath();" << endl << endl; - - // Wait 10 seconds before starting - textStream << "if (typeof Test !== 'undefined') {" << endl; - textStream << " Test.wait(10000);" << endl; - textStream << "};" << endl << endl; - - textStream << "autoTester.enableRecursive();" << endl; - textStream << "autoTester.enableAuto();" << endl << endl; - - // This is used to verify that the recursive test contains at least one test - bool testFound{ false }; - - // Directories are included in reverse order. The autoTester scripts use a stack mechanism, - // so this ensures that the tests run in alphabetical order (a convenience when debugging) - QStringList directories; - - // First test if top-level folder has a test.js file - const QString testPathname{ topLevelDirectory + "/" + TEST_FILENAME }; - QFileInfo fileInfo(testPathname); - if (fileInfo.exists()) { - // Current folder contains a test - directories.push_front(testPathname); - - testFound = true; - } - - QDirIterator it(topLevelDirectory.toStdString().c_str(), QDirIterator::Subdirectories); - while (it.hasNext()) { - QString directory = it.next(); - - // Only process directories - QDir dir(directory); - if (!isAValidDirectory(directory)) { - continue; - } - - const QString testPathname { directory + "/" + TEST_FILENAME }; - QFileInfo fileInfo(testPathname); - if (fileInfo.exists()) { - // Current folder contains a test - directories.push_front(testPathname); - - testFound = true; - } - } - - if (interactiveMode && !testFound) { - QMessageBox::information(0, "Failure", "No \"" + TEST_FILENAME + "\" files found"); - allTestsFilename.close(); - return; - } - - // Now include the test scripts - for (int i = 0; i < directories.length(); ++i) { - includeTest(textStream, directories.at(i)); - } - - textStream << endl; - textStream << "autoTester.runRecursive();" << endl; - - allTestsFilename.close(); - - if (interactiveMode) { - QMessageBox::information(0, "Success", "Script has been created"); - } -} - void Test::createTests() { - // Rename files sequentially, as ExpectedResult_00000.jpeg, ExpectedResult_00001.jpg and so on + // Rename files sequentially, as ExpectedResult_00000.png, ExpectedResult_00001.png and so on // Any existing expected result images will be deleted QString previousSelection = _snapshotDirectory; QString parent = previousSelection.left(previousSelection.lastIndexOf('/')); @@ -482,7 +347,7 @@ void Test::createTests() { _snapshotDirectory = QFileDialog::getExistingDirectory(nullptr, "Please select folder containing the test images", parent, QFileDialog::ShowDirsOnly); - // If user cancelled then restore previous selection and return + // If user canceled then restore previous selection and return if (_snapshotDirectory == "") { _snapshotDirectory = previousSelection; return; @@ -497,7 +362,7 @@ void Test::createTests() { _testsRootDirectory = QFileDialog::getExistingDirectory(nullptr, "Please select test root folder", parent, QFileDialog::ShowDirsOnly); - // If user cancelled then restore previous selection and return + // If user canceled then restore previous selection and return if (_testsRootDirectory == "") { _testsRootDirectory = previousSelection; return; @@ -613,9 +478,7 @@ ExtractedText Test::getTestScriptLines(QString testFileName) { return relevantTextFromTest; } -// Create an MD file for a user-selected test. -// The folder selected must contain a script named "test.js", the file produced is named "test.md" -void Test::createMDFile() { +bool Test::createFileSetup() { // Folder selection QString previousSelection = _testDirectory; QString parent = previousSelection.left(previousSelection.lastIndexOf('/')); @@ -624,20 +487,18 @@ void Test::createMDFile() { } _testDirectory = QFileDialog::getExistingDirectory(nullptr, "Please select folder containing the test", parent, - QFileDialog::ShowDirsOnly); + QFileDialog::ShowDirsOnly); - // If user cancelled then restore previous selection and return + // If user canceled then restore previous selection and return if (_testDirectory == "") { _testDirectory = previousSelection; - return; + return false; } - createMDFile(_testDirectory); - - QMessageBox::information(0, "Success", "MD file has been created"); + return true; } -void Test::createAllMDFiles() { +bool Test::createAllFilesSetup() { // Select folder to start recursing from QString previousSelection = _testsRootDirectory; QString parent = previousSelection.left(previousSelection.lastIndexOf('/')); @@ -645,12 +506,32 @@ void Test::createAllMDFiles() { parent += "/"; } - _testsRootDirectory = QFileDialog::getExistingDirectory(nullptr, "Please select the root folder for the MD files", parent, - QFileDialog::ShowDirsOnly); + _testsRootDirectory = QFileDialog::getExistingDirectory(nullptr, "Please select the tests root folder", parent, + QFileDialog::ShowDirsOnly); // If user cancelled then restore previous selection and return if (_testsRootDirectory == "") { _testsRootDirectory = previousSelection; + return false; + } + + return true; +} + +// Create an MD file for a user-selected test. +// The folder selected must contain a script named "test.js", the file produced is named "test.md" +void Test::createMDFile() { + if (!createFileSetup()) { + return; + } + + if (createMDFile(_testDirectory)) { + QMessageBox::information(0, "Success", "MD file has been created"); + } +} + +void Test::createAllMDFiles() { + if (!createAllFilesSetup()) { return; } @@ -681,18 +562,18 @@ void Test::createAllMDFiles() { QMessageBox::information(0, "Success", "MD files have been created"); } -void Test::createMDFile(const QString& _testDirectory) { +bool Test::createMDFile(const QString& directory) { // Verify folder contains test.js file - QString testFileName(_testDirectory + "/" + TEST_FILENAME); + QString testFileName(directory + "/" + TEST_FILENAME); QFileInfo testFileInfo(testFileName); if (!testFileInfo.exists()) { QMessageBox::critical(0, "Error", "Could not find file: " + TEST_FILENAME); - return; + return false; } ExtractedText testScriptLines = getTestScriptLines(testFileName); - QString mdFilename(_testDirectory + "/" + "test.md"); + QString mdFilename(directory + "/" + "test.md"); QFile mdFile(mdFilename); if (!mdFile.open(QIODevice::WriteOnly)) { QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), "Failed to create file " + mdFilename); @@ -705,9 +586,6 @@ void Test::createMDFile(const QString& _testDirectory) { QString testName = testScriptLines.title; stream << "# " << testName << "\n"; - // Find the relevant part of the path to the test (i.e. from "tests" down - QString partialPath = extractPathFromTestsDown(_testDirectory); - stream << "## Run this script URL: [Manual](./test.js?raw=true) [Auto](./testAuto.js?raw=true)(from menu/Edit/Open and Run scripts from URL...)." << "\n\n"; stream << "## Preconditions" << "\n"; @@ -727,6 +605,223 @@ void Test::createMDFile(const QString& _testDirectory) { } mdFile.close(); + + foreach (auto test, testScriptLines.stepList) { + delete test; + } + testScriptLines.stepList.clear(); + + return true; +} + +void Test::createTestAutoScript() { + if (!createFileSetup()) { + return; + } + + if (createTestAutoScript(_testDirectory)) { + QMessageBox::information(0, "Success", "'autoTester.js` script has been created"); + } +} + +void Test::createAllTestAutoScripts() { + if (!createAllFilesSetup()) { + return; + } + + // First test if top-level folder has a test.js file + const QString testPathname{ _testsRootDirectory + "/" + TEST_FILENAME }; + QFileInfo fileInfo(testPathname); + if (fileInfo.exists()) { + createTestAutoScript(_testsRootDirectory); + } + + QDirIterator it(_testsRootDirectory.toStdString().c_str(), QDirIterator::Subdirectories); + while (it.hasNext()) { + QString directory = it.next(); + + // Only process directories + QDir dir; + if (!isAValidDirectory(directory)) { + continue; + } + + const QString testPathname{ directory + "/" + TEST_FILENAME }; + QFileInfo fileInfo(testPathname); + if (fileInfo.exists()) { + createTestAutoScript(directory); + } + } + + QMessageBox::information(0, "Success", "'autoTester.js' scripts have been created"); +} + +bool Test::createTestAutoScript(const QString& directory) { + // Verify folder contains test.js file + QString testFileName(directory + "/" + TEST_FILENAME); + QFileInfo testFileInfo(testFileName); + if (!testFileInfo.exists()) { + QMessageBox::critical(0, "Error", "Could not find file: " + TEST_FILENAME); + return false; + } + + QString testAutoScriptFilename(directory + "/" + "testAuto.js"); + QFile testAutoScriptFile(testAutoScriptFilename); + if (!testAutoScriptFile.open(QIODevice::WriteOnly)) { + QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), + "Failed to create file " + testAutoScriptFilename); + exit(-1); + } + + QTextStream stream(&testAutoScriptFile); + + stream << "if (typeof PATH_TO_THE_REPO_PATH_UTILS_FILE === 'undefined') PATH_TO_THE_REPO_PATH_UTILS_FILE = 'https://raw.githubusercontent.com/highfidelity/hifi_tests/master/tests/utils/branchUtils.js';\n"; + stream << "Script.include(PATH_TO_THE_REPO_PATH_UTILS_FILE);\n"; + stream << "var autoTester = createAutoTester(Script.resolvePath('.'));\n\n"; + stream << "autoTester.enableAuto();\n\n"; + stream << "Script.include('./test.js?raw=true');\n"; + + testAutoScriptFile.close(); + return true; +} + +// Creates a single script in a user-selected folder. +// This script will run all text.js scripts in every applicable sub-folder +void Test::createRecursiveScript() { + if (!createFileSetup()) { + return; + } + + createRecursiveScript(_testDirectory, true); + QMessageBox::information(0, "Success", "'testRecursive.js` script has been created"); +} + +// This method creates a `testRecursive.js` script in every sub-folder. +void Test::createAllRecursiveScripts() { + if (!createAllFilesSetup()) { + return; + } + + createRecursiveScript(_testsRootDirectory, false); + + QDirIterator it(_testsRootDirectory.toStdString().c_str(), QDirIterator::Subdirectories); + while (it.hasNext()) { + QString directory = it.next(); + + // Only process directories + QDir dir; + if (!isAValidDirectory(directory)) { + continue; + } + + // Only process directories that have sub-directories + bool hasNoSubDirectories{ true }; + QDirIterator it2(directory.toStdString().c_str(), QDirIterator::Subdirectories); + while (it2.hasNext()) { + QString directory2 = it2.next(); + + // Only process directories + QDir dir; + if (isAValidDirectory(directory2)) { + hasNoSubDirectories = false; + break; + } + } + + if (!hasNoSubDirectories) { + createRecursiveScript(directory, false); + } + } + + QMessageBox::information(0, "Success", "Scripts have been created"); +} + +void Test::createRecursiveScript(const QString& topLevelDirectory, bool interactiveMode) { + const QString recursiveTestsFilename("testRecursive.js"); + QFile allTestsFilename(topLevelDirectory + "/" + recursiveTestsFilename); + if (!allTestsFilename.open(QIODevice::WriteOnly | QIODevice::Text)) { + QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), + "Failed to create \"" + recursiveTestsFilename + "\" in directory \"" + topLevelDirectory + "\""); + + exit(-1); + } + + QTextStream textStream(&allTestsFilename); + + textStream << "// This is an automatically generated file, created by auto-tester" << endl; + + // Include 'autoTest.js' + QString branch = autoTester->getSelectedBranch(); + QString user = autoTester->getSelectedUser(); + + textStream << "PATH_TO_THE_REPO_PATH_UTILS_FILE = \"https://raw.githubusercontent.com/" + user + "/hifi_tests/" + branch + + "/tests/utils/branchUtils.js\";" + << endl; + textStream << "Script.include(PATH_TO_THE_REPO_PATH_UTILS_FILE);" << endl; + textStream << "var autoTester = createAutoTester(Script.resolvePath(\".\"));" << endl << endl; + + textStream << "var testsRootPath = autoTester.getTestsRootPath();" << endl << endl; + + // Wait 10 seconds before starting + textStream << "if (typeof Test !== 'undefined') {" << endl; + textStream << " Test.wait(10000);" << endl; + textStream << "};" << endl << endl; + + textStream << "autoTester.enableRecursive();" << endl; + textStream << "autoTester.enableAuto();" << endl << endl; + + // This is used to verify that the recursive test contains at least one test + bool testFound{ false }; + + // Directories are included in reverse order. The autoTester scripts use a stack mechanism, + // so this ensures that the tests run in alphabetical order (a convenience when debugging) + QStringList directories; + + // First test if top-level folder has a test.js file + const QString testPathname{ topLevelDirectory + "/" + TEST_FILENAME }; + QFileInfo fileInfo(testPathname); + if (fileInfo.exists()) { + // Current folder contains a test + directories.push_front(testPathname); + + testFound = true; + } + + QDirIterator it(topLevelDirectory.toStdString().c_str(), QDirIterator::Subdirectories); + while (it.hasNext()) { + QString directory = it.next(); + + // Only process directories + QDir dir(directory); + if (!isAValidDirectory(directory)) { + continue; + } + + const QString testPathname{ directory + "/" + TEST_FILENAME }; + QFileInfo fileInfo(testPathname); + if (fileInfo.exists()) { + // Current folder contains a test + directories.push_front(testPathname); + + testFound = true; + } + } + + if (interactiveMode && !testFound) { + QMessageBox::information(0, "Failure", "No \"" + TEST_FILENAME + "\" files found"); + allTestsFilename.close(); + return; + } + + // Now include the test scripts + for (int i = 0; i < directories.length(); ++i) { + includeTest(textStream, directories.at(i)); + } + + textStream << endl; + textStream << "autoTester.runRecursive();" << endl; + + allTestsFilename.close(); } void Test::createTestsOutline() { @@ -739,7 +834,7 @@ void Test::createTestsOutline() { _testDirectory = QFileDialog::getExistingDirectory(nullptr, "Please select the tests root folder", parent, QFileDialog::ShowDirsOnly); - // If user cancelled then restore previous selection and return + // If user canceled then restore previous selection and return if (_testDirectory == "") { _testDirectory = previousSelection; return; @@ -949,3 +1044,19 @@ QString Test::getExpectedImagePartialSourceDirectory(const QString& filename) { void Test::setTestRailCreateMode(TestRailCreateMode testRailCreateMode) { _testRailCreateMode = testRailCreateMode; } + +void Test::createWebPage(QCheckBox* updateAWSCheckBox, QLineEdit* urlLineEdit) { + QString testResults = QFileDialog::getOpenFileName(nullptr, "Please select the zipped test results to update from", nullptr, + "Zipped Test Results (*.zip)"); + if (testResults.isNull()) { + return; + } + + QString tempDirectory = QFileDialog::getExistingDirectory(nullptr, "Please select a folder to store temporary files in", + nullptr, QFileDialog::ShowDirsOnly); + if (tempDirectory.isNull()) { + return; + } + + _awsInterface.createWebPageFromResults(testResults, tempDirectory, updateAWSCheckBox, urlLineEdit); +} \ No newline at end of file diff --git a/tools/auto-tester/src/Test.h b/tools/auto-tester/src/Test.h index 853e9c98e2..f653a91782 100644 --- a/tools/auto-tester/src/Test.h +++ b/tools/auto-tester/src/Test.h @@ -16,6 +16,7 @@ #include #include +#include "AWSInterface.h" #include "ImageComparer.h" #include "ui/MismatchWindow.h" #include "TestRailInterface.h" @@ -41,28 +42,41 @@ enum TestRailCreateMode { class Test { public: - Test(); + Test(QProgressBar* progressBar, QCheckBox* checkBoxInteractiveMode); - void startTestsEvaluation(const QString& testFolder = QString(), const QString& branchFromCommandLine = QString(), const QString& userFromCommandLine = QString()); - void finishTestsEvaluation(bool isRunningFromCommandline, bool interactiveMode, QProgressBar* progressBar); + void startTestsEvaluation(const bool isRunningFromCommandLine, + const bool isRunningInAutomaticTestRun, + const QString& snapshotDirectory = QString(), + const QString& branchFromCommandLine = QString(), + const QString& userFromCommandLine = QString()); - void createRecursiveScript(); - void createAllRecursiveScripts(); - void createRecursiveScript(const QString& topLevelDirectory, bool interactiveMode); + void finishTestsEvaluation(); void createTests(); void createTestsOutline(); + bool createFileSetup(); + bool createAllFilesSetup(); + void createMDFile(); void createAllMDFiles(); - void createMDFile(const QString& topLevelDirectory); + bool createMDFile(const QString& directory); + + void createTestAutoScript(); + void createAllTestAutoScripts(); + bool createTestAutoScript(const QString& directory); void createTestRailTestCases(); void createTestRailRun(); + void updateTestRailRunResult(); - bool compareImageLists(bool isInteractiveMode, QProgressBar* progressBar); + void createRecursiveScript(); + void createAllRecursiveScripts(); + void createRecursiveScript(const QString& topLevelDirectory, bool interactiveMode); + + int compareImageLists(); QStringList createListOfAll_imagesInDirectory(const QString& imageFormat, const QString& pathToImageDirectory); @@ -70,10 +84,10 @@ public: void includeTest(QTextStream& textStream, const QString& testPathname); - void appendTestResultsToFile(const QString& testResultsFolderPath, TestFailure testFailure, QPixmap comparisonImage); + void appendTestResultsToFile(const QString& testResultsFolderPath, TestResult testResult, QPixmap comparisonImage, bool hasFailed); bool createTestResultsFolderPath(const QString& directory); - void zipAndDeleteTestResultsFolder(); + QString zipAndDeleteTestResultsFolder(); static bool isAValidDirectory(const QString& pathname); QString extractPathFromTestsDown(const QString& fullPath); @@ -84,12 +98,20 @@ public: void setTestRailCreateMode(TestRailCreateMode testRailCreateMode); + void createWebPage(QCheckBox* updateAWSCheckBox, QLineEdit* urlLineEdit); + private: + QProgressBar* _progressBar; + QCheckBox* _checkBoxInteractiveMode; + + bool _isRunningFromCommandLine{ false }; + bool _isRunningInAutomaticTestRun{ false }; + const QString TEST_FILENAME { "test.js" }; const QString TEST_RESULTS_FOLDER { "TestResults" }; const QString TEST_RESULTS_FILENAME { "TestResults.txt" }; - const double THRESHOLD{ 0.96 }; + const double THRESHOLD{ 0.935 }; QDir _imageDirectory; @@ -98,7 +120,8 @@ private: ImageComparer _imageComparer; QString _testResultsFolderPath; - int _index { 1 }; + int _failureIndex{ 1 }; + int _successIndex{ 1 }; // Expected images are in the format ExpectedImage_dddd.jpg (d == decimal digit) const int NUM_DIGITS { 5 }; @@ -132,8 +155,9 @@ private: bool _exitWhenComplete{ false }; TestRailInterface _testRailInterface; - TestRailCreateMode _testRailCreateMode { PYTHON }; + + AWSInterface _awsInterface; }; #endif // hifi_test_h \ No newline at end of file diff --git a/tools/auto-tester/src/TestRailInterface.cpp b/tools/auto-tester/src/TestRailInterface.cpp index 93559490e5..f943935539 100644 --- a/tools/auto-tester/src/TestRailInterface.cpp +++ b/tools/auto-tester/src/TestRailInterface.cpp @@ -16,66 +16,40 @@ #include #include +#include #include #include TestRailInterface::TestRailInterface() { _testRailTestCasesSelectorWindow.setURL("https://highfidelity.testrail.net"); - ////_testRailTestCasesSelectorWindow.setURL("https://nissimhadar.testrail.io"); _testRailTestCasesSelectorWindow.setUser("@highfidelity.io"); - ////_testRailTestCasesSelectorWindow.setUser("nissim.hadar@gmail.com"); - _testRailTestCasesSelectorWindow.setProjectID(INTERFACE_PROJECT_ID); - ////_testRailTestCasesSelectorWindow.setProjectID(2); + _testRailTestCasesSelectorWindow.setProjectID(INTERFACE_AUTOMATION_PROJECT_ID); _testRailTestCasesSelectorWindow.setSuiteID(INTERFACE_SUITE_ID); - ////_testRailTestCasesSelectorWindow.setSuiteID(2); _testRailRunSelectorWindow.setURL("https://highfidelity.testrail.net"); - ////_testRailRunSelectorWindow.setURL("https://nissimhadar.testrail.io"); _testRailRunSelectorWindow.setUser("@highfidelity.io"); - ////_testRailRunSelectorWindow.setUser("nissim.hadar@gmail.com"); - _testRailRunSelectorWindow.setProjectID(INTERFACE_PROJECT_ID); - ////_testRailRunSelectorWindow.setProjectID(2); + _testRailRunSelectorWindow.setProjectID(INTERFACE_AUTOMATION_PROJECT_ID); _testRailRunSelectorWindow.setSuiteID(INTERFACE_SUITE_ID); - ////_testRailRunSelectorWindow.setSuiteID(2); _testRailResultsSelectorWindow.setURL("https://highfidelity.testrail.net"); - ////_testRailResultsSelectorWindow.setURL("https://nissimhadar.testrail.io"); _testRailResultsSelectorWindow.setUser("@highfidelity.io"); - ////_testRailResultsSelectorWindow.setUser("nissim.hadar@gmail.com"); - _testRailResultsSelectorWindow.setProjectID(INTERFACE_PROJECT_ID); - ////_testRailResultsSelectorWindow.setProjectID(2); + _testRailResultsSelectorWindow.setProjectID(INTERFACE_AUTOMATION_PROJECT_ID); _testRailResultsSelectorWindow.setSuiteID(INTERFACE_SUITE_ID); - ////_testRailResultsSelectorWindow.setSuiteID(2); + + _pythonInterface = new PythonInterface(); + _pythonCommand = _pythonInterface->getPythonCommand(); } QString TestRailInterface::getObject(const QString& path) { return path.right(path.length() - path.lastIndexOf("/") - 1); } - -bool TestRailInterface::setPythonCommand() { - if (QProcessEnvironment::systemEnvironment().contains("PYTHON_PATH")) { - QString _pythonPath = QProcessEnvironment::systemEnvironment().value("PYTHON_PATH"); - if (!QFile::exists(_pythonPath + "/" + pythonExe)) { - QMessageBox::critical(0, pythonExe, QString("Python executable not found in ") + _pythonPath); - } - _pythonCommand = _pythonPath + "/" + pythonExe; - return true; - } else { - QMessageBox::critical(0, "PYTHON_PATH not defined", - "Please set PYTHON_PATH to directory containing the Python executable"); - return false; - } - - return false; -} - // Creates the testrail.py script // This is the file linked to from http://docs.gurock.com/testrail-api2/bindings-python void TestRailInterface::createTestRailDotPyScript() { @@ -240,7 +214,7 @@ bool TestRailInterface::requestTestRailTestCasesDataFromUser() { _url = _testRailTestCasesSelectorWindow.getURL() + "/"; _user = _testRailTestCasesSelectorWindow.getUser(); _password = _testRailTestCasesSelectorWindow.getPassword(); - ////_password = "tutKA76";//// + _projectID = QString::number(_testRailTestCasesSelectorWindow.getProjectID()); _suiteID = QString::number(_testRailTestCasesSelectorWindow.getSuiteID()); @@ -258,7 +232,7 @@ bool TestRailInterface::requestTestRailRunDataFromUser() { _url = _testRailRunSelectorWindow.getURL() + "/"; _user = _testRailRunSelectorWindow.getUser(); _password = _testRailRunSelectorWindow.getPassword(); - ////_password = "tutKA76";//// + _projectID = QString::number(_testRailRunSelectorWindow.getProjectID()); _suiteID = QString::number(_testRailRunSelectorWindow.getSuiteID()); @@ -276,7 +250,7 @@ bool TestRailInterface::requestTestRailResultsDataFromUser() { _url = _testRailResultsSelectorWindow.getURL() + "/"; _user = _testRailResultsSelectorWindow.getUser(); _password = _testRailResultsSelectorWindow.getPassword(); - ////_password = "tutKA76";//// + _projectID = QString::number(_testRailResultsSelectorWindow.getProjectID()); _suiteID = QString::number(_testRailResultsSelectorWindow.getSuiteID()); @@ -377,8 +351,9 @@ void TestRailInterface::createAddTestCasesPythonScript(const QString& testDirect QMessageBox::Yes | QMessageBox::No).exec() ) { QProcess* process = new QProcess(); - connect(process, &QProcess::started, this, [=]() { _busyWindow.exec(); }); + connect(process, &QProcess::started, this, [=]() { _busyWindow.exec(); }); + connect(process, SIGNAL(finished(int)), process, SLOT(deleteLater())); connect(process, static_cast(&QProcess::finished), this, [=](int exitCode, QProcess::ExitStatus exitStatus) { _busyWindow.hide(); }); @@ -489,8 +464,8 @@ void TestRailInterface::addRun() { stream << "\tcase_ids.append(case['id'])\n\n"; // Now, we can create the run - stream << "data = { 'name': '" + _sectionNames[_testRailRunSelectorWindow.getSectionID()].replace("Section", "Run") + - "', 'suite_id': " + _suiteID + + stream << "data = { 'name': '" + _sectionNames[_testRailRunSelectorWindow.getSectionID()].replace("Section", "Run") + "[" + + QHostInfo::localHostName() + "]" + "', 'suite_id': " + _suiteID + ", 'include_all': False, 'case_ids': case_ids}\n"; stream << "run = client.send_post('add_run/" + _projectID + "', data)\n"; @@ -503,7 +478,7 @@ void TestRailInterface::addRun() { ) { QProcess* process = new QProcess(); connect(process, &QProcess::started, this, [=]() { _busyWindow.exec(); }); - + connect(process, SIGNAL(finished(int)), process, SLOT(deleteLater())); connect(process, static_cast(&QProcess::finished), this, [=](int exitCode, QProcess::ExitStatus exitStatus) { _busyWindow.hide(); }); @@ -511,6 +486,7 @@ void TestRailInterface::addRun() { process->start(_pythonCommand, parameters); } } + void TestRailInterface::updateRunWithResults() { QString filename = _outputDirectory + "/updateRunWithResults.py"; if (QFile::exists(filename)) { @@ -536,17 +512,27 @@ void TestRailInterface::updateRunWithResults() { // The failed tests are read, formatted and inserted into a set // A failure named 'Failure_1--tests.content.entity.material.apply.avatars.00000' is formatted to 'content/entity/material/apply/avatars' // This is the name of the test in TestRail + // + // A success is named `Success_-tests. ... stream << "from os import listdir\n"; stream << "failed_tests = set()\n"; - stream << "for entry in listdir('" + _outputDirectory + "/" + tempName + "'):\n"; - stream << "\tparts = entry.split('--tests.')[1].split('.')\n"; - stream << "\tfailed_test = parts[0]\n"; - stream << "\tfor i in range(1, len(parts) - 1):\n"; - stream << "\t\tfailed_test = failed_test + '/' + parts[i]\n"; + QDir dir(_outputDirectory + "/" + TEMP_NAME); + if (dir.exists()) { + stream << "for entry in listdir('" + _outputDirectory + "/" + TEMP_NAME + "'):\n"; + + // skip over successes + stream << "\tif entry.split('_')[0] == 'Success':\n"; + stream << "\t\tcontinue\n"; + + stream << "\tparts = entry.split('--tests.')[1].split('.')\n"; + stream << "\tfailed_test = parts[0]\n"; + stream << "\tfor i in range(1, len(parts) - 1):\n"; + stream << "\t\tfailed_test = failed_test + '/' + parts[i]\n"; - stream << "\tfailed_tests.add(failed_test)\n\n"; + stream << "\tfailed_tests.add(failed_test)\n\n"; + } // Initialize the array of results that will be eventually used to update TestRail stream << "status_ids = []\n"; @@ -580,7 +566,13 @@ void TestRailInterface::updateRunWithResults() { stream << "\tresults.append({'case_id': case_ids[i], 'status_id': status_ids[i] })\n\n"; stream << "data = { 'results': results }\n"; - stream << "section = client.send_post('add_results_for_cases/' + str(" << runID << "), data)\n"; + stream << "client.send_post('add_results_for_cases/' + str(" << runID << "), data)\n"; + + // Also update the run + QStringList parts = _testResults.split('/'); + QString resultName = parts[parts.length() - 1].split('.')[0]; + stream << "client.send_post('update_run/' + str(" << runID << ")," + << " { 'description' : 'https://hifi-qa.s3.amazonaws.com/" << resultName << "/TestResults.html' })\n"; file.close(); @@ -590,7 +582,7 @@ void TestRailInterface::updateRunWithResults() { ) { QProcess* process = new QProcess(); connect(process, &QProcess::started, this, [=]() { _busyWindow.exec(); }); - + connect(process, SIGNAL(finished(int)), process, SLOT(deleteLater())); connect(process, static_cast(&QProcess::finished), this, [=](int exitCode, QProcess::ExitStatus exitStatus) { _busyWindow.hide(); }); @@ -765,6 +757,7 @@ void TestRailInterface::getReleasesFromTestRail() { QProcess* process = new QProcess(); connect(process, static_cast(&QProcess::finished), this, [=](int exitCode, QProcess::ExitStatus exitStatus) { updateReleasesComboData(exitCode, exitStatus); }); + connect(process, SIGNAL(finished(int)), process, SLOT(deleteLater())); QStringList parameters = QStringList() << filename; process->start(_pythonCommand, parameters); @@ -779,10 +772,6 @@ void TestRailInterface::createTestSuitePython(const QString& testDirectory, _userGitHub = userGitHub; _branchGitHub = branchGitHub; - if (!setPythonCommand()) { - return; - } - if (!requestTestRailTestCasesDataFromUser()) { return; } @@ -812,6 +801,7 @@ void TestRailInterface::createTestSuiteXML(const QString& testDirectory, QDomElement suiteName = _document.createElement("name"); suiteName.appendChild( _document.createTextNode("Test Suite - " + QDateTime::currentDateTime().toString("yyyy-MM-ddTHH:mm"))); + topLevelSection.appendChild(suiteName); // This is the first call to 'process'. This is then called recursively to build the full XML tree @@ -908,7 +898,7 @@ QDomElement TestRailInterface::processTestXML(const QString& fullDirectory, ++i; QString title{ words[i] }; for (++i; i < words.length() - 1; ++i) { - title += " / " + words[i]; + title += "/" + words[i]; } QDomElement titleElement = _document.createElement("title"); @@ -1036,7 +1026,6 @@ void TestRailInterface::processTestPython(const QString& fullDirectory, QString testContent = QString("Execute instructions in [THIS TEST](") + testMDName + ")"; QString testExpected = QString("Refer to the expected result in the linked description."); - stream << "data = {\n" << "\t'title': '" << title << "',\n" << "\t'template_id': 2,\n" @@ -1087,6 +1076,7 @@ void TestRailInterface::getTestSectionsFromTestRail() { QProcess* process = new QProcess(); connect(process, static_cast(&QProcess::finished), this, [=](int exitCode, QProcess::ExitStatus exitStatus) { updateSectionsComboData(exitCode, exitStatus); }); + connect(process, SIGNAL(finished(int)), process, SLOT(deleteLater())); QStringList parameters = QStringList() << filename; process->start(_pythonCommand, parameters); @@ -1125,6 +1115,7 @@ void TestRailInterface::getRunsFromTestRail() { QProcess* process = new QProcess(); connect(process, static_cast(&QProcess::finished), this, [=](int exitCode, QProcess::ExitStatus exitStatus) { updateRunsComboData(exitCode, exitStatus); }); + connect(process, SIGNAL(finished(int)), process, SLOT(deleteLater())); QStringList parameters = QStringList() << filename; @@ -1134,10 +1125,6 @@ void TestRailInterface::getRunsFromTestRail() { void TestRailInterface::createTestRailRun(const QString& outputDirectory) { _outputDirectory = outputDirectory; - if (!setPythonCommand()) { - return; - } - if (!requestTestRailRunDataFromUser()) { return; } @@ -1151,10 +1138,7 @@ void TestRailInterface::createTestRailRun(const QString& outputDirectory) { void TestRailInterface::updateTestRailRunResults(const QString& testResults, const QString& tempDirectory) { _outputDirectory = tempDirectory; - - if (!setPythonCommand()) { - return; - } + _testResults = testResults; if (!requestTestRailResultsDataFromUser()) { return; @@ -1164,11 +1148,20 @@ void TestRailInterface::updateTestRailRunResults(const QString& testResults, con createTestRailDotPyScript(); // Extract test failures from zipped folder - QString tempSubDirectory = tempDirectory + "/" + tempName; + QString tempSubDirectory = tempDirectory + "/" + TEMP_NAME; QDir dir = tempSubDirectory; dir.mkdir(tempSubDirectory); JlCompress::extractDir(testResults, tempSubDirectory); // TestRail will be updated after the process initiated by getTestRunFromTestRail has completed getRunsFromTestRail(); + + dir.rmdir(tempSubDirectory); +} + +void TestRailInterface::extractTestFailuresFromZippedFolder(const QString& testResults, const QString& tempDirectory) { + QString tempSubDirectory = tempDirectory + "/" + TEMP_NAME; + QDir dir = tempSubDirectory; + dir.mkdir(tempSubDirectory); + JlCompress::extractDir(testResults, tempSubDirectory); } \ No newline at end of file diff --git a/tools/auto-tester/src/TestRailInterface.h b/tools/auto-tester/src/TestRailInterface.h index 6f250dfbba..6843ca0142 100644 --- a/tools/auto-tester/src/TestRailInterface.h +++ b/tools/auto-tester/src/TestRailInterface.h @@ -12,7 +12,6 @@ #define hifi_test_testrail_interface_h #include "ui/BusyWindow.h" - #include "ui/TestRailTestCasesSelectorWindow.h" #include "ui/TestRailRunSelectorWindow.h" #include "ui/TestRailResultsSelectorWindow.h" @@ -22,7 +21,9 @@ #include #include -class TestRailInterface : public QObject{ +#include "PythonInterface.h" + +class TestRailInterface : public QObject { Q_OBJECT public: @@ -65,9 +66,7 @@ public: bool requestTestRailRunDataFromUser(); bool requestTestRailResultsDataFromUser(); - void createAddTestCasesPythonScript(const QString& testDirectory, - const QString& userGitHub, - const QString& branchGitHub); + void createAddTestCasesPythonScript(const QString& testDirectory, const QString& userGitHub, const QString& branchGitHub); void processDirectoryPython(const QString& directory, QTextStream& stream, @@ -88,14 +87,14 @@ public: void addRun(); void updateRunWithResults(); - bool setPythonCommand(); + void extractTestFailuresFromZippedFolder(const QString& testResults, const QString& tempDirectory); private: // HighFidelity Interface project ID in TestRail - const int INTERFACE_PROJECT_ID{ 24 }; + const int INTERFACE_AUTOMATION_PROJECT_ID{ 26 }; // Rendering suite ID - const int INTERFACE_SUITE_ID{ 1147 }; + const int INTERFACE_SUITE_ID{ 1312 }; QDomDocument _document; @@ -111,13 +110,11 @@ private: QString _suiteID; QString _testDirectory; + QString _testResults; QString _outputDirectory; QString _userGitHub; QString _branchGitHub; - const QString pythonExe{ "python.exe" }; - QString _pythonCommand; - QStringList _releaseNames; QStringList _sectionNames; @@ -126,7 +123,10 @@ private: QStringList _runNames; std::vector _runIDs; - QString tempName{ "fgadhcUDHSFaidsfh3478JJJFSDFIUSOEIrf" }; + QString TEMP_NAME{ "fgadhcUDHSFaidsfh3478JJJFSDFIUSOEIrf" }; + + PythonInterface* _pythonInterface; + QString _pythonCommand; }; #endif \ No newline at end of file diff --git a/tools/auto-tester/src/TestRunner.cpp b/tools/auto-tester/src/TestRunner.cpp new file mode 100644 index 0000000000..01ec04f254 --- /dev/null +++ b/tools/auto-tester/src/TestRunner.cpp @@ -0,0 +1,608 @@ +// +// TestRunner.cpp +// +// Created by Nissim Hadar on 1 Sept 2018. +// Copyright 2013 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// +#include "TestRunner.h" + +#include +#include +#include + +#include "ui/AutoTester.h" +extern AutoTester* autoTester; + +#ifdef Q_OS_WIN +#include +#include +#endif + +TestRunner::TestRunner(std::vector dayCheckboxes, + std::vector timeEditCheckboxes, + std::vector timeEdits, + QLabel* workingFolderLabel, + QCheckBox* runServerless, + QCheckBox* runLatest, + QLineEdit* url, + QPushButton* runNow, + QObject* parent) : + QObject(parent) { + _dayCheckboxes = dayCheckboxes; + _timeEditCheckboxes = timeEditCheckboxes; + _timeEdits = timeEdits; + _workingFolderLabel = workingFolderLabel; + _runServerless = runServerless; + _runLatest = runLatest; + _url = url; + _runNow = runNow; + + installerThread = new QThread(); + installerWorker = new Worker(); + installerWorker->moveToThread(installerThread); + installerThread->start(); + connect(this, SIGNAL(startInstaller()), installerWorker, SLOT(runCommand())); + connect(installerWorker, SIGNAL(commandComplete()), this, SLOT(installationComplete())); + + interfaceThread = new QThread(); + interfaceWorker = new Worker(); + interfaceThread->start(); + interfaceWorker->moveToThread(interfaceThread); + connect(this, SIGNAL(startInterface()), interfaceWorker, SLOT(runCommand())); + connect(interfaceWorker, SIGNAL(commandComplete()), this, SLOT(interfaceExecutionComplete())); +} + +TestRunner::~TestRunner() { + delete installerThread; + delete interfaceThread; + + delete interfaceThread; + delete interfaceWorker; + + if (_timer) { + delete _timer; + } +} + +void TestRunner::setWorkingFolder() { + // Everything will be written to this folder + QString previousSelection = _workingFolder; + QString parent = previousSelection.left(previousSelection.lastIndexOf('/')); + if (!parent.isNull() && parent.right(1) != "/") { + parent += "/"; + } + + _workingFolder = QFileDialog::getExistingDirectory(nullptr, "Please select a temporary folder for installation", parent, + QFileDialog::ShowDirsOnly); + + // If user canceled then restore previous selection and return + if (_workingFolder == "") { + _workingFolder = previousSelection; + return; + } + + _installationFolder = _workingFolder + "/High Fidelity"; + _logFile.setFileName(_workingFolder + "/log.txt"); + + autoTester->enableRunTabControls(); + _workingFolderLabel->setText(QDir::toNativeSeparators(_workingFolder)); + + _timer = new QTimer(this); + connect(_timer, SIGNAL(timeout()), this, SLOT(checkTime())); + _timer->start(30 * 1000); //time specified in ms +} + +void TestRunner::run() { + _runNow->setEnabled(false); + + _testStartDateTime = QDateTime::currentDateTime(); + _automatedTestIsRunning = true; + + // Initial setup + _branch = autoTester->getSelectedBranch(); + _user = autoTester->getSelectedUser(); + + // This will be restored at the end of the tests + saveExistingHighFidelityAppDataFolder(); + + // Download the latest High Fidelity build XML. + // Note that this is not needed for PR builds (or whenever `Run Latest` is unchecked) + // It is still downloaded, to simplify the flow + QStringList urls; + QStringList filenames; + + urls << DEV_BUILD_XML_URL; + filenames << DEV_BUILD_XML_FILENAME; + + updateStatusLabel("Downloading Build XML"); + + buildXMLDownloaded = false; + autoTester->downloadFiles(urls, _workingFolder, filenames, (void*)this); + + // `downloadComplete` will run after download has completed +} + +void TestRunner::downloadComplete() { + if (!buildXMLDownloaded) { + // Download of Build XML has completed + buildXMLDownloaded = true; + + // Download the High Fidelity installer + QStringList urls; + QStringList filenames; + if (_runLatest->isChecked()) { + parseBuildInformation(); + + _installerFilename = INSTALLER_FILENAME_LATEST; + + urls << _buildInformation.url; + filenames << _installerFilename; + } else { + QString urlText = _url->text(); + urls << urlText; + _installerFilename = getInstallerNameFromURL(urlText); + filenames << _installerFilename; + } + + updateStatusLabel("Downloading installer"); + + autoTester->downloadFiles(urls, _workingFolder, filenames, (void*)this); + + // `downloadComplete` will run again after download has completed + + } else { + // Download of Installer has completed + appendLog(QString("Tests started at ") + QString::number(_testStartDateTime.time().hour()) + ":" + + QString("%1").arg(_testStartDateTime.time().minute(), 2, 10, QChar('0')) + ", on " + + _testStartDateTime.date().toString("ddd, MMM d, yyyy")); + + updateStatusLabel("Installing"); + + // Kill any existing processes that would interfere with installation + killProcesses(); + + runInstaller(); + } +} + +void TestRunner::runInstaller() { + // Qt cannot start an installation process using QProcess::start (Qt Bug 9761) + // To allow installation, the installer is run using the `system` command + + QStringList arguments{ QStringList() << QString("/S") << QString("/D=") + QDir::toNativeSeparators(_installationFolder) }; + + QString installerFullPath = _workingFolder + "/" + _installerFilename; + + QString commandLine = + "\"" + QDir::toNativeSeparators(installerFullPath) + "\"" + " /S /D=" + QDir::toNativeSeparators(_installationFolder); + + installerWorker->setCommandLine(commandLine); + emit startInstaller(); +} + +void TestRunner::installationComplete() { + verifyInstallationSucceeded(); + + createSnapshotFolder(); + + updateStatusLabel("Running tests"); + + if (!_runServerless->isChecked()) { + startLocalServerProcesses(); + } + + runInterfaceWithTestScript(); +} + +void TestRunner::verifyInstallationSucceeded() { + // Exit if the executables are missing. + // On Windows, the reason is probably that UAC has blocked the installation. This is treated as a critical error +#ifdef Q_OS_WIN + QFileInfo interfaceExe(QDir::toNativeSeparators(_installationFolder) + "\\interface.exe"); + QFileInfo assignmentClientExe(QDir::toNativeSeparators(_installationFolder) + "\\assignment-client.exe"); + QFileInfo domainServerExe(QDir::toNativeSeparators(_installationFolder) + "\\domain-server.exe"); + + if (!interfaceExe.exists() || !assignmentClientExe.exists() || !domainServerExe.exists()) { + QMessageBox::critical(0, "Installation of High Fidelity has failed", "Please verify that UAC has been disabled"); + exit(-1); + } +#endif +} + +void TestRunner::saveExistingHighFidelityAppDataFolder() { + QString dataDirectory{ "NOT FOUND" }; + +#ifdef Q_OS_WIN + dataDirectory = qgetenv("USERPROFILE") + "\\AppData\\Roaming"; +#endif + + if (_runLatest->isChecked()) { + _appDataFolder = dataDirectory + "\\High Fidelity"; + } else { + // We are running a PR build + _appDataFolder = dataDirectory + "\\High Fidelity - " + getPRNumberFromURL(_url->text()); + } + + _savedAppDataFolder = dataDirectory + "/" + UNIQUE_FOLDER_NAME; + if (_savedAppDataFolder.exists()) { + _savedAppDataFolder.removeRecursively(); + } + + if (_appDataFolder.exists()) { + // The original folder is saved in a unique name + _appDataFolder.rename(_appDataFolder.path(), _savedAppDataFolder.path()); + } + + // Copy an "empty" AppData folder (i.e. no entities) + copyFolder(QDir::currentPath() + "/AppDataHighFidelity", _appDataFolder.path()); +} + +void TestRunner::createSnapshotFolder() { + _snapshotFolder = _workingFolder + "/" + SNAPSHOT_FOLDER_NAME; + + // Just delete all PNGs from the folder if it already exists + if (QDir(_snapshotFolder).exists()) { + // Note that we cannot use just a `png` filter, as the filenames include periods + // Also, delete any `jpg` and `txt` files + // The idea is to leave only previous zipped result folders + QDirIterator it(_snapshotFolder.toStdString().c_str()); + while (it.hasNext()) { + QString filename = it.next(); + if (filename.right(4) == ".png" || filename.right(4) == ".jpg" || filename.right(4) == ".txt") { + QFile::remove(filename); + } + } + + } else { + QDir().mkdir(_snapshotFolder); + } +} + +void TestRunner::killProcesses() { +#ifdef Q_OS_WIN + try { + QStringList processesToKill = QStringList() << "interface.exe" + << "assignment-client.exe" + << "domain-server.exe" + << "server-console.exe"; + + // Loop until all pending processes to kill have actually died + QStringList pendingProcessesToKill; + do { + pendingProcessesToKill.clear(); + + // Get list of running tasks + HANDLE processSnapHandle = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); + if (processSnapHandle == INVALID_HANDLE_VALUE) { + throw("Process snapshot creation failure"); + } + + PROCESSENTRY32 processEntry32; + processEntry32.dwSize = sizeof(PROCESSENTRY32); + if (!Process32First(processSnapHandle, &processEntry32)) { + CloseHandle(processSnapHandle); + throw("Process32First failed"); + } + + // Kill any task in the list + do { + foreach (QString process, processesToKill) + if (QString(processEntry32.szExeFile) == process) { + QString commandLine = "taskkill /im " + process + " /f >nul"; + system(commandLine.toStdString().c_str()); + pendingProcessesToKill << process; + } + } while (Process32Next(processSnapHandle, &processEntry32)); + + QThread::sleep(2); + } while (!pendingProcessesToKill.isEmpty()); + + } catch (QString errorMessage) { + QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), errorMessage); + exit(-1); + } catch (...) { + QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), "unknown error"); + exit(-1); + } +#endif +} + +void TestRunner::startLocalServerProcesses() { +#ifdef Q_OS_WIN + QString commandLine; + + commandLine = "start \"domain-server.exe\" \"" + QDir::toNativeSeparators(_installationFolder) + "\\domain-server.exe\""; + system(commandLine.toStdString().c_str()); + + commandLine = + "start \"assignment-client.exe\" \"" + QDir::toNativeSeparators(_installationFolder) + "\\assignment-client.exe\" -n 6"; + system(commandLine.toStdString().c_str()); +#endif + // Give server processes time to stabilize + QThread::sleep(20); +} + +void TestRunner::runInterfaceWithTestScript() { + QString exeFile = QString("\"") + QDir::toNativeSeparators(_installationFolder) + "\\interface.exe\""; + QString snapshotFolder = QString("\"") + QDir::toNativeSeparators(_snapshotFolder) + "\""; + + QString url = QString("hifi://localhost"); + if (_runServerless->isChecked()) { + // Move to an empty area + url = "file:///~serverless/tutorial.json"; + } else { + url = "hifi://localhost"; + } + + QString testScript = + QString("https://raw.githubusercontent.com/") + _user + "/hifi_tests/" + _branch + "/tests/testRecursive.js"; + + QString commandLine = exeFile + " --url " + url + " --no-updater --no-login-suggestion" + " --testScript " + testScript + + " quitWhenFinished --testResultsLocation " + snapshotFolder; + + interfaceWorker->setCommandLine(commandLine); + emit startInterface(); +} + +void TestRunner::interfaceExecutionComplete() { + killProcesses(); + + QFileInfo testCompleted(QDir::toNativeSeparators(_snapshotFolder) +"/tests_completed.txt"); + if (!testCompleted.exists()) { + QMessageBox::critical(0, "Tests not completed", "Interface seems to have crashed before completion of the test scripts"); + _runNow->setEnabled(true); + return; + } + + evaluateResults(); + + // The High Fidelity AppData folder will be restored after evaluation has completed +} + +void TestRunner::evaluateResults() { + updateStatusLabel("Evaluating results"); + autoTester->startTestsEvaluation(false, true, _snapshotFolder, _branch, _user); +} + +void TestRunner::automaticTestRunEvaluationComplete(QString zippedFolder, int numberOfFailures) { + addBuildNumberToResults(zippedFolder); + restoreHighFidelityAppDataFolder(); + + updateStatusLabel("Testing complete"); + + QDateTime currentDateTime = QDateTime::currentDateTime(); + + QString completionText = QString("Tests completed at ") + QString::number(currentDateTime.time().hour()) + ":" + + QString("%1").arg(currentDateTime.time().minute(), 2, 10, QChar('0')) + ", on " + + currentDateTime.date().toString("ddd, MMM d, yyyy"); + + if (numberOfFailures == 0) { + completionText += "; no failures"; + } else if (numberOfFailures == 1) { + completionText += "; 1 failure"; + } else { + completionText += QString("; ") + QString::number(numberOfFailures) + " failures"; + } + appendLog(completionText); + + _automatedTestIsRunning = false; + + _runNow->setEnabled(true); +} + +void TestRunner::addBuildNumberToResults(QString zippedFolderName) { + QString augmentedFilename; + if (!_runLatest->isChecked()) { + augmentedFilename = zippedFolderName.replace("local", getPRNumberFromURL(_url->text())); + } else { + augmentedFilename = zippedFolderName.replace("local", _buildInformation.build); + } + QFile::rename(zippedFolderName, augmentedFilename); +} + +void TestRunner::restoreHighFidelityAppDataFolder() { + _appDataFolder.removeRecursively(); + + if (_savedAppDataFolder != QDir()) { + _appDataFolder.rename(_savedAppDataFolder.path(), _appDataFolder.path()); + } +} + +// Copies a folder recursively +void TestRunner::copyFolder(const QString& source, const QString& destination) { + try { + if (!QFileInfo(source).isDir()) { + // just a file copy + QFile::copy(source, destination); + } else { + QDir destinationDir(destination); + if (!destinationDir.cdUp()) { + throw("'source '" + source + "'seems to be a root folder"); + } + + if (!destinationDir.mkdir(QFileInfo(destination).fileName())) { + throw("Could not create destination folder '" + destination + "'"); + } + + QStringList fileNames = + QDir(source).entryList(QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot | QDir::Hidden | QDir::System); + + foreach (const QString& fileName, fileNames) { + copyFolder(QString(source + "/" + fileName), QString(destination + "/" + fileName)); + } + } + } catch (QString errorMessage) { + QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), errorMessage); + exit(-1); + } catch (...) { + QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), "unknown error"); + exit(-1); + } +} + +void TestRunner::checkTime() { + // No processing is done if a test is running + if (_automatedTestIsRunning) { + return; + } + + QDateTime now = QDateTime::currentDateTime(); + + // Check day of week + if (!_dayCheckboxes.at(now.date().dayOfWeek() - 1)->isChecked()) { + return; + } + + // Check the time + bool timeToRun{ false }; + + for (size_t i = 0; i < std::min(_timeEditCheckboxes.size(), _timeEdits.size()); ++i) { + if (_timeEditCheckboxes[i]->isChecked() && (_timeEdits[i]->time().hour() == now.time().hour()) && + (_timeEdits[i]->time().minute() == now.time().minute())) { + timeToRun = true; + break; + } + } + + if (timeToRun) { + run(); + } +} + +void TestRunner::updateStatusLabel(const QString& message) { + autoTester->updateStatusLabel(message); +} + +void TestRunner::appendLog(const QString& message) { + if (!_logFile.open(QIODevice::Append | QIODevice::Text)) { + QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), + "Could not open the log file"); + exit(-1); + } + + _logFile.write(message.toStdString().c_str()); + _logFile.write("\n"); + _logFile.close(); + + autoTester->appendLogWindow(message); +} + +QString TestRunner::getInstallerNameFromURL(const QString& url) { + // An example URL: https://deployment.highfidelity.com/jobs/pr-build/label%3Dwindows/13023/HighFidelity-Beta-Interface-PR14006-be76c43.exe + try { + QStringList urlParts = url.split("/"); + return urlParts[urlParts.size() - 1]; + } catch (QString errorMessage) { + QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), errorMessage); + exit(-1); + } catch (...) { + QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), "unknown error"); + exit(-1); + } +} + +QString TestRunner::getPRNumberFromURL(const QString& url) { + try { + QStringList urlParts = url.split("/"); + QStringList filenameParts = urlParts[urlParts.size() - 1].split("-"); + if (filenameParts.size() <= 3) { + throw "URL not in expected format, should look like `https://deployment.highfidelity.com/jobs/pr-build/label%3Dwindows/13023/HighFidelity-Beta-Interface-PR14006-be76c43.exe`"; + } + return filenameParts[filenameParts.size() - 2]; + } catch (QString errorMessage) { + QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), errorMessage); + exit(-1); + } catch (...) { + QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), "unknown error"); + exit(-1); + } +} + +void TestRunner::parseBuildInformation() { + try { + QDomDocument domDocument; + QString filename{ _workingFolder + "/" + DEV_BUILD_XML_FILENAME }; + QFile file(filename); + if (!file.open(QIODevice::ReadOnly) || !domDocument.setContent(&file)) { + throw QString("Could not open " + filename); + } + + QString platformOfInterest; +#ifdef Q_OS_WIN + platformOfInterest = "windows"; +#elif defined(Q_OS_MAC) + platformOfInterest = "mac"; +#endif + QDomElement element = domDocument.documentElement(); + + // Verify first element is "projects" + if (element.tagName() != "projects") { + throw("File seems to be in wrong format"); + } + + element = element.firstChild().toElement(); + if (element.tagName() != "project") { + throw("File seems to be in wrong format"); + } + + if (element.attribute("name") != "interface") { + throw("File is not from 'interface' build"); + } + + // Now loop over the platforms + while (!element.isNull()) { + element = element.firstChild().toElement(); + if (element.tagName() != "platform" || element.attribute("name") != platformOfInterest) { + continue; + } + + // Next element should be the build + element = element.firstChild().toElement(); + if (element.tagName() != "build") { + throw("File seems to be in wrong format"); + } + + // Next element should be the version + element = element.firstChild().toElement(); + if (element.tagName() != "version") { + throw("File seems to be in wrong format"); + } + + // Add the build number to the end of the filename + _buildInformation.build = element.text(); + + // First sibling should be stable_version + element = element.nextSibling().toElement(); + if (element.tagName() != "stable_version") { + throw("File seems to be in wrong format"); + } + + // Next sibling should be url + element = element.nextSibling().toElement(); + if (element.tagName() != "url") { + throw("File seems to be in wrong format"); + } + _buildInformation.url = element.text(); + } + + } catch (QString errorMessage) { + QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), errorMessage); + exit(-1); + } catch (...) { + QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), "unknown error"); + exit(-1); + } +} + +void Worker::setCommandLine(const QString& commandLine) { + _commandLine = commandLine; +} + +int Worker::runCommand() { + int result = system(_commandLine.toStdString().c_str()); + emit commandComplete(); + return result; +} \ No newline at end of file diff --git a/tools/auto-tester/src/TestRunner.h b/tools/auto-tester/src/TestRunner.h new file mode 100644 index 0000000000..e6cb7cd764 --- /dev/null +++ b/tools/auto-tester/src/TestRunner.h @@ -0,0 +1,151 @@ +// +// TestRunner.h +// +// Created by Nissim Hadar on 1 Sept 2018. +// Copyright 2013 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_testRunner_h +#define hifi_testRunner_h + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +class BuildInformation { +public: + QString build; + QString url; +}; + +class Worker; + +class TestRunner : public QObject { + Q_OBJECT +public: + explicit TestRunner(std::vector dayCheckboxes, + std::vector timeEditCheckboxes, + std::vector timeEdits, + QLabel* workingFolderLabel, + QCheckBox* runServerless, + QCheckBox* runLatest, + QLineEdit* url, + QPushButton* runNow, + QObject* parent = 0); + + ~TestRunner(); + + void setWorkingFolder(); + + void run(); + + void downloadComplete(); + void runInstaller(); + void verifyInstallationSucceeded(); + + void saveExistingHighFidelityAppDataFolder(); + void restoreHighFidelityAppDataFolder(); + + void createSnapshotFolder(); + + void killProcesses(); + void startLocalServerProcesses(); + + void runInterfaceWithTestScript(); + + void evaluateResults(); + void automaticTestRunEvaluationComplete(QString zippedFolderName, int numberOfFailures); + void addBuildNumberToResults(QString zippedFolderName); + + void copyFolder(const QString& source, const QString& destination); + + void updateStatusLabel(const QString& message); + void appendLog(const QString& message); + + QString getInstallerNameFromURL(const QString& url); + QString getPRNumberFromURL(const QString& url); + + void parseBuildInformation(); + +private slots: + void checkTime(); + void installationComplete(); + void interfaceExecutionComplete(); + +signals: + void startInstaller(); + void startInterface(); + +private: + bool _automatedTestIsRunning{ false }; + + const QString INSTALLER_FILENAME_LATEST{ "HighFidelity-Beta-latest-dev.exe" }; + + QString _installerURL; + QString _installerFilename; + const QString DEV_BUILD_XML_URL{ "https://highfidelity.com/dev-builds.xml" }; + const QString DEV_BUILD_XML_FILENAME{ "dev-builds.xml" }; + + bool buildXMLDownloaded; + + QDir _appDataFolder; + QDir _savedAppDataFolder; + + QString _workingFolder; + QString _installationFolder; + QString _snapshotFolder; + + const QString UNIQUE_FOLDER_NAME{ "fgadhcUDHSFaidsfh3478JJJFSDFIUSOEIrf" }; + const QString SNAPSHOT_FOLDER_NAME{ "snapshots" }; + + QString _branch; + QString _user; + + std::vector _dayCheckboxes; + std::vector _timeEditCheckboxes; + std::vector _timeEdits; + QLabel* _workingFolderLabel; + QCheckBox* _runServerless; + QCheckBox* _runLatest; + QLineEdit* _url; + QPushButton* _runNow; + QTimer* _timer; + + QFile _logFile; + + QDateTime _testStartDateTime; + + QThread* installerThread; + QThread* interfaceThread; + Worker* installerWorker; + Worker* interfaceWorker; + + BuildInformation _buildInformation; +}; + +class Worker : public QObject { + Q_OBJECT +public: + void setCommandLine(const QString& commandLine); + +public slots: + int runCommand(); + +signals: + void commandComplete(); + void startInstaller(); + void startInterface(); + +private: + QString _commandLine; +}; +#endif // hifi_testRunner_h \ No newline at end of file diff --git a/tools/auto-tester/src/common.h b/tools/auto-tester/src/common.h index 939814df62..5df4e9c921 100644 --- a/tools/auto-tester/src/common.h +++ b/tools/auto-tester/src/common.h @@ -12,9 +12,9 @@ #include -class TestFailure { +class TestResult { public: - TestFailure(float error, QString pathname, QString expectedImageFilename, QString actualImageFilename) : + TestResult(float error, QString pathname, QString expectedImageFilename, QString actualImageFilename) : _error(error), _pathname(pathname), _expectedImageFilename(expectedImageFilename), diff --git a/tools/auto-tester/src/main.cpp b/tools/auto-tester/src/main.cpp index 03b8cf51ff..ac4b4593c5 100644 --- a/tools/auto-tester/src/main.cpp +++ b/tools/auto-tester/src/main.cpp @@ -66,7 +66,7 @@ int main(int argc, char *argv[]) { autoTester->setup(); if (!testFolder.isNull()) { - autoTester->runFromCommandLine(testFolder, branch, user); + autoTester->startTestsEvaluation(true ,false, testFolder, branch, user); } else { autoTester->show(); } diff --git a/tools/auto-tester/src/ui/AutoTester.cpp b/tools/auto-tester/src/ui/AutoTester.cpp index 13bda4853f..32457c2224 100644 --- a/tools/auto-tester/src/ui/AutoTester.cpp +++ b/tools/auto-tester/src/ui/AutoTester.cpp @@ -28,34 +28,86 @@ AutoTester::AutoTester(QWidget* parent) : QMainWindow(parent) { connect(_ui.actionAbout, &QAction::triggered, this, &AutoTester::about); connect(_ui.actionContent, &QAction::triggered, this, &AutoTester::content); + // The second tab hides and shows the Windows task bar #ifndef Q_OS_WIN - _ui.tabWidget->setTabEnabled(3, false); + _ui.tabWidget->removeTab(1); #endif - // helpWindow.textBrowser->setText() + _ui.statusLabel->setText(""); + _ui.plainTextEdit->setReadOnly(true); + + setWindowTitle("Auto Tester - v6.7"); + + // Coming soon to an auto-tester near you... + //// _helpWindow.textBrowser->setText() +} + +AutoTester::~AutoTester() { + delete _signalMapper; + + if (_test) { + delete _test; + } + + if (_testRunner) { + delete _testRunner; + } } void AutoTester::setup() { - _test = new Test(); + if (_test) { + delete _test; + } + _test = new Test(_ui.progressBar, _ui.checkBoxInteractiveMode); + + std::vector dayCheckboxes; + dayCheckboxes.emplace_back(_ui.mondayCheckBox); + dayCheckboxes.emplace_back(_ui.tuesdayCheckBox); + dayCheckboxes.emplace_back(_ui.wednesdayCheckBox); + dayCheckboxes.emplace_back(_ui.thursdayCheckBox); + dayCheckboxes.emplace_back(_ui.fridayCheckBox); + dayCheckboxes.emplace_back(_ui.saturdayCheckBox); + dayCheckboxes.emplace_back(_ui.sundayCheckBox); + + std::vector timeEditCheckboxes; + timeEditCheckboxes.emplace_back(_ui.timeEdit1checkBox); + timeEditCheckboxes.emplace_back(_ui.timeEdit2checkBox); + timeEditCheckboxes.emplace_back(_ui.timeEdit3checkBox); + timeEditCheckboxes.emplace_back(_ui.timeEdit4checkBox); + + std::vector timeEdits; + timeEdits.emplace_back(_ui.timeEdit1); + timeEdits.emplace_back(_ui.timeEdit2); + timeEdits.emplace_back(_ui.timeEdit3); + timeEdits.emplace_back(_ui.timeEdit4); + + if (_testRunner) { + delete _testRunner; + } + _testRunner = new TestRunner(dayCheckboxes, timeEditCheckboxes, timeEdits, _ui.workingFolderLabel, _ui.checkBoxServerless, _ui.checkBoxRunLatest, _ui.urlLineEdit, _ui.runNowButton); } -void AutoTester::runFromCommandLine(const QString& testFolder, const QString& branch, const QString& user) { - _isRunningFromCommandline = true; - _test->startTestsEvaluation(testFolder, branch, user); +void AutoTester::startTestsEvaluation(const bool isRunningFromCommandLine, + const bool isRunningInAutomaticTestRun, + const QString& snapshotDirectory, + const QString& branch, + const QString& user +) { + _test->startTestsEvaluation(isRunningFromCommandLine, isRunningInAutomaticTestRun, snapshotDirectory, branch, user); } void AutoTester::on_tabWidget_currentChanged(int index) { - if (index == 1 || index == 2) { - _ui.userTextEdit->setDisabled(false); - _ui.branchTextEdit->setDisabled(false); + if (index == 0 || index == 2 || index == 3) { + _ui.userLineEdit->setDisabled(false); + _ui.branchLineEdit->setDisabled(false); } else { - _ui.userTextEdit->setDisabled(true); - _ui.branchTextEdit->setDisabled(true); + _ui.userLineEdit->setDisabled(true); + _ui.branchLineEdit->setDisabled(true); } } void AutoTester::on_evaluateTestsButton_clicked() { - _test->startTestsEvaluation(); + _test->startTestsEvaluation(false, false); } void AutoTester::on_createRecursiveScriptButton_clicked() { @@ -78,6 +130,14 @@ void AutoTester::on_createAllMDFilesButton_clicked() { _test->createAllMDFiles(); } +void AutoTester::on_createTestAutoScriptButton_clicked() { + _test->createTestAutoScript(); +} + +void AutoTester::on_createAllTestAutoScriptsButton_clicked() { + _test->createAllTestAutoScripts(); +} + void AutoTester::on_createTestsOutlineButton_clicked() { _test->createTestsOutline(); } @@ -90,6 +150,28 @@ void AutoTester::on_createTestRailRunButton_clicked() { _test->createTestRailRun(); } +void AutoTester::on_setWorkingFolderButton_clicked() { + _testRunner->setWorkingFolder(); +} + +void AutoTester::enableRunTabControls() { + _ui.runNowButton->setEnabled(true); + _ui.daysGroupBox->setEnabled(true); + _ui.timesGroupBox->setEnabled(true); +} + +void AutoTester::on_runNowButton_clicked() { + _testRunner->run(); +} + +void AutoTester::on_checkBoxRunLatest_clicked() { + _ui.urlLineEdit->setEnabled(!_ui.checkBoxRunLatest->isChecked()); +} + +void AutoTester::automaticTestRunEvaluationComplete(QString zippedFolderName, int numberOfFailures) { + _testRunner->automaticTestRunEvaluationComplete(zippedFolderName, numberOfFailures); +} + void AutoTester::on_updateTestRailRunResultsButton_clicked() { _test->updateTestRailRunResult(); } @@ -130,7 +212,11 @@ void AutoTester::on_createXMLScriptRadioButton_clicked() { _test->setTestRailCreateMode(XML); } -void AutoTester::downloadImage(const QUrl& url) { +void AutoTester::on_createWebPagePushButton_clicked() { + _test->createWebPage(_ui.updateAWSCheckBox, _ui.awsURLLineEdit); +} + +void AutoTester::downloadFile(const QUrl& url) { _downloaders.emplace_back(new Downloader(url, this)); connect(_downloaders[_index], SIGNAL(downloaded()), _signalMapper, SLOT(map())); @@ -139,70 +225,86 @@ void AutoTester::downloadImage(const QUrl& url) { ++_index; } -void AutoTester::downloadImages(const QStringList& URLs, const QString& directoryName, const QStringList& filenames) { +void AutoTester::downloadFiles(const QStringList& URLs, const QString& directoryName, const QStringList& filenames, void *caller) { + connect(_signalMapper, SIGNAL(mapped(int)), this, SLOT(saveFile(int))); + _directoryName = directoryName; _filenames = filenames; + _caller = caller; - _numberOfImagesToDownload = URLs.size(); - _numberOfImagesDownloaded = 0; + _numberOfFilesToDownload = URLs.size(); + _numberOfFilesDownloaded = 0; _index = 0; _ui.progressBar->setMinimum(0); - _ui.progressBar->setMaximum(_numberOfImagesToDownload - 1); + _ui.progressBar->setMaximum(_numberOfFilesToDownload - 1); _ui.progressBar->setValue(0); _ui.progressBar->setVisible(true); - _downloaders.clear(); - for (int i = 0; i < _numberOfImagesToDownload; ++i) { - QUrl imageURL(URLs[i]); - downloadImage(imageURL); + foreach (auto downloader, _downloaders) { + delete downloader; } - connect(_signalMapper, SIGNAL(mapped(int)), this, SLOT(saveImage(int))); + _downloaders.clear(); + for (int i = 0; i < _numberOfFilesToDownload; ++i) { + downloadFile(URLs[i]); + } } -void AutoTester::saveImage(int index) { +void AutoTester::saveFile(int index) { try { QFile file(_directoryName + "/" + _filenames[index]); file.open(QIODevice::WriteOnly); file.write(_downloaders[index]->downloadedData()); file.close(); } catch (...) { - QMessageBox::information(0, "Test Aborted", "Failed to save image: " + _filenames[index]); + QMessageBox::information(0, "Test Aborted", "Failed to save file: " + _filenames[index]); _ui.progressBar->setVisible(false); return; } - ++_numberOfImagesDownloaded; + ++_numberOfFilesDownloaded; - if (_numberOfImagesDownloaded == _numberOfImagesToDownload) { - disconnect(_signalMapper, SIGNAL(mapped(int)), this, SLOT(saveImage(int))); - _test->finishTestsEvaluation(_isRunningFromCommandline, _ui.checkBoxInteractiveMode->isChecked(), _ui.progressBar); + if (_numberOfFilesDownloaded == _numberOfFilesToDownload) { + disconnect(_signalMapper, SIGNAL(mapped(int)), this, SLOT(saveFile(int))); + if (_caller == _test) { + _test->finishTestsEvaluation(); + } else if (_caller == _testRunner) { + _testRunner->downloadComplete(); + } } else { - _ui.progressBar->setValue(_numberOfImagesDownloaded); + _ui.progressBar->setValue(_numberOfFilesDownloaded); } } void AutoTester::about() { - QMessageBox::information(0, "About", QString("Built ") + __DATE__ + " : " + __TIME__); + QMessageBox::information(0, "About", QString("Built ") + __DATE__ + ", " + __TIME__); } void AutoTester::content() { - helpWindow.show(); + _helpWindow.show(); } void AutoTester::setUserText(const QString& user) { - _ui.userTextEdit->setText(user); + _ui.userLineEdit->setText(user); } QString AutoTester::getSelectedUser() { - return _ui.userTextEdit->toPlainText(); + return _ui.userLineEdit->text(); } void AutoTester::setBranchText(const QString& branch) { - _ui.branchTextEdit->setText(branch); + _ui.branchLineEdit->setText(branch); } QString AutoTester::getSelectedBranch() { - return _ui.branchTextEdit->toPlainText(); + return _ui.branchLineEdit->text(); } + +void AutoTester::updateStatusLabel(const QString& status) { + _ui.statusLabel->setText(status); +} + +void AutoTester::appendLogWindow(const QString& message) { + _ui.plainTextEdit->appendPlainText(message); +} \ No newline at end of file diff --git a/tools/auto-tester/src/ui/AutoTester.h b/tools/auto-tester/src/ui/AutoTester.h index e29da5b716..429a8b60e1 100644 --- a/tools/auto-tester/src/ui/AutoTester.h +++ b/tools/auto-tester/src/ui/AutoTester.h @@ -19,19 +19,28 @@ #include "../Test.h" #include "HelpWindow.h" +#include "../TestRunner.h" +#include "../AWSInterface.h" class AutoTester : public QMainWindow { Q_OBJECT public: - AutoTester(QWidget *parent = Q_NULLPTR); + AutoTester(QWidget* parent = Q_NULLPTR); + ~AutoTester(); void setup(); - void runFromCommandLine(const QString& testFolder, const QString& branch, const QString& user); + void startTestsEvaluation(const bool isRunningFromCommandLine, + const bool isRunningInAutomaticTestRun, + const QString& snapshotDirectory, + const QString& branch, + const QString& user); - void downloadImage(const QUrl& url); - void downloadImages(const QStringList& URLs, const QString& directoryName, const QStringList& filenames); + void automaticTestRunEvaluationComplete(QString zippedFolderName, int numberOfFailures); + + void downloadFile(const QUrl& url); + void downloadFiles(const QStringList& URLs, const QString& directoryName, const QStringList& filenames, void* caller); void setUserText(const QString& user); QString getSelectedUser(); @@ -39,21 +48,35 @@ public: void setBranchText(const QString& branch); QString getSelectedBranch(); + void enableRunTabControls(); + + void updateStatusLabel(const QString& status); + void appendLogWindow(const QString& message); + private slots: void on_tabWidget_currentChanged(int index); void on_evaluateTestsButton_clicked(); void on_createRecursiveScriptButton_clicked(); void on_createAllRecursiveScriptsButton_clicked(); - void on_createTestsButton_clicked(); + void on_createTestsButton_clicked(); void on_createMDFileButton_clicked(); void on_createAllMDFilesButton_clicked(); + void on_createTestAutoScriptButton_clicked(); + void on_createAllTestAutoScriptsButton_clicked(); + void on_createTestsOutlineButton_clicked(); void on_createTestRailTestCasesButton_clicked(); void on_createTestRailRunButton_clicked(); + + void on_setWorkingFolderButton_clicked(); + void on_runNowButton_clicked(); + + void on_checkBoxRunLatest_clicked(); + void on_updateTestRailRunResultsButton_clicked(); void on_hideTaskbarButton_clicked(); @@ -62,16 +85,21 @@ private slots: void on_createPythonScriptRadioButton_clicked(); void on_createXMLScriptRadioButton_clicked(); + void on_createWebPagePushButton_clicked(); + void on_closeButton_clicked(); - void saveImage(int index); + void saveFile(int index); void about(); void content(); private: Ui::AutoTesterClass _ui; - Test* _test; + Test* _test{ nullptr }; + TestRunner* _testRunner{ nullptr }; + + AWSInterface _awsInterface; std::vector _downloaders; @@ -82,13 +110,15 @@ private: // Used to enable passing a parameter to slots QSignalMapper* _signalMapper; - int _numberOfImagesToDownload { 0 }; - int _numberOfImagesDownloaded { 0 }; - int _index { 0 }; + int _numberOfFilesToDownload{ 0 }; + int _numberOfFilesDownloaded{ 0 }; + int _index{ 0 }; - bool _isRunningFromCommandline { false }; + bool _isRunningFromCommandline{ false }; - HelpWindow helpWindow; + HelpWindow _helpWindow; + + void* _caller; }; -#endif // hifi_AutoTester_h \ No newline at end of file +#endif // hifi_AutoTester_h \ No newline at end of file diff --git a/tools/auto-tester/src/ui/AutoTester.ui b/tools/auto-tester/src/ui/AutoTester.ui index ac8fcf5e86..b277fbdb2a 100644 --- a/tools/auto-tester/src/ui/AutoTester.ui +++ b/tools/auto-tester/src/ui/AutoTester.ui @@ -6,8 +6,8 @@ 0 0 - 432 - 734 + 720 + 870 @@ -23,8 +23,8 @@ - 166 - 610 + 470 + 750 100 40 @@ -36,10 +36,10 @@ - 12 + 45 140 - 408 - 461 + 630 + 580 @@ -52,8 +52,8 @@ - 96 - 20 + 195 + 60 220 40 @@ -65,8 +65,8 @@ - 96 - 100 + 70 + 180 220 40 @@ -78,8 +78,8 @@ - 96 - 150 + 320 + 180 220 40 @@ -91,8 +91,8 @@ - 96 - 230 + 195 + 120 220 40 @@ -104,8 +104,8 @@ - 96 - 310 + 70 + 300 220 40 @@ -117,8 +117,8 @@ - 96 - 360 + 320 + 300 220 40 @@ -127,30 +127,436 @@ Create all Recursive Scripts + + + + 70 + 240 + 220 + 40 + + + + Create testAuto script + + + + + + 320 + 240 + 220 + 40 + + + + Create all testAuto scripts + + + + + + Windows + + + + + 200 + 130 + 211 + 40 + + + + Hide Windows Taskbar + + + + + + 200 + 200 + 211 + 40 + + + + Show Windows Taskbar + + + + + + Run + + + + false + + + + 10 + 160 + 161 + 28 + + + + Run now + + + + + false + + + + 20 + 240 + 91 + 241 + + + + Days + + + + + 10 + 210 + 80 + 17 + + + + Sunday + + + + + + 10 + 90 + 80 + 17 + + + + Wednesday + + + + + + 10 + 60 + 80 + 17 + + + + Tuesday + + + + + + 10 + 120 + 80 + 17 + + + + Thursday + + + + + + 10 + 150 + 80 + 17 + + + + Friday + + + + + + 10 + 180 + 80 + 17 + + + + Saturday + + + + + + 10 + 30 + 80 + 17 + + + + Monday + + + + + + false + + + + 130 + 240 + 161 + 191 + + + + Times + + + + + 30 + 20 + 118 + 22 + + + + + + + 30 + 60 + 118 + 22 + + + + + + + 30 + 100 + 118 + 22 + + + + + + + 30 + 140 + 118 + 22 + + + + + + + 10 + 23 + 21 + 17 + + + + + + + + + + 10 + 63 + 21 + 17 + + + + + + + + + + 10 + 103 + 21 + 17 + + + + + + + + + + 10 + 143 + 21 + 17 + + + + + + + + + + + 10 + 20 + 161 + 28 + + + + Set Working Folder + + + + + + 190 + 20 + 321 + 31 + + + + (not set...) + + + + + + 300 + 210 + 311 + 331 + + + + + + + 300 + 170 + 41 + 31 + + + + Status: + + + + + + 350 + 170 + 271 + 31 + + + + ####### + + + + + + 20 + 70 + 120 + 20 + + + + <html><head/><body><p>If unchecked, will not show results during evaluation</p></body></html> + + + Server-less + + + false + + + + + + 20 + 100 + 120 + 20 + + + + <html><head/><body><p>If unchecked, will not show results during evaluation</p></body></html> + + + Run Latest + + + true + + + + + + 128 + 95 + 21 + 31 + + + + URL + + + + + false + + + + 160 + 100 + 451 + 21 + + + Evaluate - - - - 90 - 100 - 255 - 23 - - - - 24 - - - 90 - 50 - 131 + 200 + 180 + 120 20 @@ -164,8 +570,8 @@ - 200 - 40 + 330 + 170 101 40 @@ -177,14 +583,14 @@ - TestRail + Web Interface - 180 - 160 - 161 + 240 + 220 + 160 40 @@ -195,8 +601,8 @@ - 80 - 40 + 170 + 100 95 20 @@ -211,9 +617,9 @@ - 180 - 100 - 161 + 240 + 160 + 160 40 @@ -224,9 +630,9 @@ - 180 - 40 - 161 + 240 + 100 + 160 40 @@ -237,8 +643,8 @@ - 80 - 60 + 170 + 120 95 20 @@ -247,45 +653,86 @@ XML - - - - Windows - - + - 100 - 100 - 211 - 40 + 10 + 30 + 601 + 300 - - Hide Windows Taskbar + + TestRail - + - 100 - 170 - 211 - 40 + 10 + 350 + 601 + 151 - - Show Windows Taskbar + + Amazon Web Services + + + true + + + + 240 + 30 + 160 + 40 + + + + Create Web Page + + + + + + 150 + 42 + 81 + 17 + + + + Update AWS + + + + + + 20 + 90 + 561 + 21 + + + + groupBox + updateTestRailRunResultsButton + createPythonScriptRadioButton + createTestRailRunButton + createTestRailTestCasesButton + createXMLScriptRadioButton + groupBox_2 - 110 - 90 - 81 + 120 + 80 + 110 16 @@ -298,32 +745,12 @@ GitHub Branch - - - - 200 - 85 - 140 - 24 - - - - - - - 200 - 47 - 140 - 24 - - - - 110 - 50 - 81 + 120 + 40 + 110 16 @@ -336,13 +763,46 @@ GitHub User + + + + 80 + 760 + 255 + 23 + + + + 24 + + + + + + 220 + 40 + 161 + 21 + + + + + + + 220 + 80 + 161 + 21 + + + 0 0 - 432 + 720 21 @@ -388,6 +848,53 @@ + + userLineEdit + branchLineEdit + createTestsButton + createMDFileButton + createAllMDFilesButton + createTestsOutlineButton + createRecursiveScriptButton + createAllRecursiveScriptsButton + createTestAutoScriptButton + createAllTestAutoScriptsButton + hideTaskbarButton + showTaskbarButton + runNowButton + sundayCheckBox + wednesdayCheckBox + tuesdayCheckBox + thursdayCheckBox + fridayCheckBox + saturdayCheckBox + mondayCheckBox + timeEdit1 + timeEdit2 + timeEdit3 + timeEdit4 + timeEdit1checkBox + timeEdit2checkBox + timeEdit3checkBox + timeEdit4checkBox + setWorkingFolderButton + plainTextEdit + checkBoxServerless + checkBoxRunLatest + urlLineEdit + checkBoxInteractiveMode + evaluateTestsButton + updateTestRailRunResultsButton + createPythonScriptRadioButton + createTestRailRunButton + createTestRailTestCasesButton + createXMLScriptRadioButton + createWebPagePushButton + updateAWSCheckBox + awsURLLineEdit + closeButton + tabWidget + diff --git a/tools/auto-tester/src/ui/MismatchWindow.cpp b/tools/auto-tester/src/ui/MismatchWindow.cpp index 79d2ce9f61..58189b4795 100644 --- a/tools/auto-tester/src/ui/MismatchWindow.cpp +++ b/tools/auto-tester/src/ui/MismatchWindow.cpp @@ -22,6 +22,11 @@ MismatchWindow::MismatchWindow(QWidget *parent) : QDialog(parent) { } QPixmap MismatchWindow::computeDiffPixmap(QImage expectedImage, QImage resultImage) { + // Create an empty difference image if the images differ in size + if (expectedImage.height() != resultImage.height() || expectedImage.width() != resultImage.width()) { + return QPixmap(); + } + // This is an optimization, as QImage.setPixel() is embarrassingly slow unsigned char* buffer = new unsigned char[expectedImage.height() * expectedImage.width() * 3]; @@ -55,20 +60,20 @@ QPixmap MismatchWindow::computeDiffPixmap(QImage expectedImage, QImage resultIma return resultPixmap; } -void MismatchWindow::setTestFailure(TestFailure testFailure) { - errorLabel->setText("Similarity: " + QString::number(testFailure._error)); +void MismatchWindow::setTestResult(TestResult testResult) { + errorLabel->setText("Similarity: " + QString::number(testResult._error)); - imagePath->setText("Path to test: " + testFailure._pathname); + imagePath->setText("Path to test: " + testResult._pathname); - expectedFilename->setText(testFailure._expectedImageFilename); - resultFilename->setText(testFailure._actualImageFilename); + expectedFilename->setText(testResult._expectedImageFilename); + resultFilename->setText(testResult._actualImageFilename); - QPixmap expectedPixmap = QPixmap(testFailure._pathname + testFailure._expectedImageFilename); - QPixmap actualPixmap = QPixmap(testFailure._pathname + testFailure._actualImageFilename); + QPixmap expectedPixmap = QPixmap(testResult._pathname + testResult._expectedImageFilename); + QPixmap actualPixmap = QPixmap(testResult._pathname + testResult._actualImageFilename); _diffPixmap = computeDiffPixmap( - QImage(testFailure._pathname + testFailure._expectedImageFilename), - QImage(testFailure._pathname + testFailure._actualImageFilename) + QImage(testResult._pathname + testResult._expectedImageFilename), + QImage(testResult._pathname + testResult._actualImageFilename) ); expectedImage->setPixmap(expectedPixmap); diff --git a/tools/auto-tester/src/ui/MismatchWindow.h b/tools/auto-tester/src/ui/MismatchWindow.h index f203a2be6a..30c29076b3 100644 --- a/tools/auto-tester/src/ui/MismatchWindow.h +++ b/tools/auto-tester/src/ui/MismatchWindow.h @@ -20,7 +20,7 @@ class MismatchWindow : public QDialog, public Ui::MismatchWindow { public: MismatchWindow(QWidget *parent = Q_NULLPTR); - void setTestFailure(TestFailure testFailure); + void setTestResult(TestResult testResult); UserResponse getUserResponse() { return _userResponse; } diff --git a/tools/auto-tester/src/ui/TestRailResultsSelectorWindow.cpp b/tools/auto-tester/src/ui/TestRailResultsSelectorWindow.cpp index 414e4fca79..505e04b33e 100644 --- a/tools/auto-tester/src/ui/TestRailResultsSelectorWindow.cpp +++ b/tools/auto-tester/src/ui/TestRailResultsSelectorWindow.cpp @@ -25,6 +25,7 @@ void TestRailResultsSelectorWindow::reset() { userLineEdit->setDisabled(false); passwordLineEdit->setDisabled(false); projectIDLineEdit->setDisabled(false); + suiteIDLineEdit->setDisabled(false); OKButton->setDisabled(true); @@ -37,6 +38,7 @@ void TestRailResultsSelectorWindow::on_acceptButton_clicked() { userLineEdit->setDisabled(true); passwordLineEdit->setDisabled(true); projectIDLineEdit->setDisabled(true); + suiteIDLineEdit->setDisabled(true); OKButton->setDisabled(false); diff --git a/tools/auto-tester/src/ui/TestRailRunSelectorWindow.cpp b/tools/auto-tester/src/ui/TestRailRunSelectorWindow.cpp index 54a3985a8b..ac3419d46f 100644 --- a/tools/auto-tester/src/ui/TestRailRunSelectorWindow.cpp +++ b/tools/auto-tester/src/ui/TestRailRunSelectorWindow.cpp @@ -19,12 +19,12 @@ TestRailRunSelectorWindow::TestRailRunSelectorWindow(QWidget *parent) { projectIDLineEdit->setValidator(new QIntValidator(1, 999, this)); } - void TestRailRunSelectorWindow::reset() { urlLineEdit->setDisabled(false); userLineEdit->setDisabled(false); passwordLineEdit->setDisabled(false); projectIDLineEdit->setDisabled(false); + suiteIDLineEdit->setDisabled(false); OKButton->setDisabled(true); sectionsComboBox->setDisabled(true); @@ -35,6 +35,7 @@ void TestRailRunSelectorWindow::on_acceptButton_clicked() { userLineEdit->setDisabled(true); passwordLineEdit->setDisabled(true); projectIDLineEdit->setDisabled(true); + suiteIDLineEdit->setDisabled(true); OKButton->setDisabled(false); sectionsComboBox->setDisabled(false); diff --git a/tools/auto-tester/src/ui/TestRailTestCasesSelectorWindow.cpp b/tools/auto-tester/src/ui/TestRailTestCasesSelectorWindow.cpp index abb873ea14..638fe71819 100644 --- a/tools/auto-tester/src/ui/TestRailTestCasesSelectorWindow.cpp +++ b/tools/auto-tester/src/ui/TestRailTestCasesSelectorWindow.cpp @@ -25,6 +25,7 @@ void TestRailTestCasesSelectorWindow::reset() { userLineEdit->setDisabled(false); passwordLineEdit->setDisabled(false); projectIDLineEdit->setDisabled(false); + suiteIDLineEdit->setDisabled(false); OKButton->setDisabled(true); @@ -37,6 +38,7 @@ void TestRailTestCasesSelectorWindow::on_acceptButton_clicked() { userLineEdit->setDisabled(true); passwordLineEdit->setDisabled(true); projectIDLineEdit->setDisabled(true); + suiteIDLineEdit->setDisabled(true); OKButton->setDisabled(false); diff --git a/tools/ktx-tool/CMakeLists.txt b/tools/ktx-tool/CMakeLists.txt index 9daf8e0a1a..6bb09e0a9e 100644 --- a/tools/ktx-tool/CMakeLists.txt +++ b/tools/ktx-tool/CMakeLists.txt @@ -2,7 +2,7 @@ set(TARGET_NAME ktx-tool) setup_hifi_project(Quick Gui Concurrent) -link_hifi_libraries(shared networking image gl gpu ktx) +link_hifi_libraries(shared networking image gl shaders gpu ktx) target_gli() diff --git a/tools/oven/CMakeLists.txt b/tools/oven/CMakeLists.txt index 1b77a2585f..022c9769fe 100644 --- a/tools/oven/CMakeLists.txt +++ b/tools/oven/CMakeLists.txt @@ -2,7 +2,7 @@ set(TARGET_NAME oven) setup_hifi_project(Widgets Gui Concurrent) -link_hifi_libraries(networking shared image gpu ktx fbx baking graphics) +link_hifi_libraries(networking shared image gpu ktx fbx hfm baking graphics) setup_memory_debugger() diff --git a/tools/oven/src/Oven.cpp b/tools/oven/src/Oven.cpp index 52b6db1aa5..af98376034 100644 --- a/tools/oven/src/Oven.cpp +++ b/tools/oven/src/Oven.cpp @@ -19,6 +19,7 @@ #include #include #include +#include Oven* Oven::_staticInstance { nullptr }; @@ -31,6 +32,7 @@ Oven::Oven() { // Initialize dependencies for OBJ Baker DependencyManager::set(); DependencyManager::set(false); + DependencyManager::set(); } Oven::~Oven() { diff --git a/tools/scribe/CMakeLists.txt b/tools/scribe/CMakeLists.txt deleted file mode 100755 index 9c71aeec9c..0000000000 --- a/tools/scribe/CMakeLists.txt +++ /dev/null @@ -1,8 +0,0 @@ -set(TARGET_NAME scribe) - -# don't use the setup_hifi_project macro as we don't want Qt or GLM dependencies -file(GLOB TARGET_SRCS src/*) -add_executable(${TARGET_NAME} ${TARGET_SRCS}) -if (WIN32) - set_property(TARGET ${TARGET_NAME} APPEND_STRING PROPERTY LINK_FLAGS_DEBUG "/OPT:NOREF /OPT:NOICF") -endif() diff --git a/tools/scribe/src/TextTemplate.cpp b/tools/scribe/src/TextTemplate.cpp deleted file mode 100755 index 89937c4da6..0000000000 --- a/tools/scribe/src/TextTemplate.cpp +++ /dev/null @@ -1,1005 +0,0 @@ -// -// TextTemplate.cpp -// tools/shaderScribe/src -// -// Created by Sam Gateau on 12/15/2014. -// Copyright 2013 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -#include "TextTemplate.h" - -#include -#include -#include -#include - -typedef TextTemplate::Block::Pointer BlockPointer; -typedef TextTemplate::Config::Pointer ConfigPointer; -typedef TextTemplate::Pointer TextTemplatePointer; - -const std::string TextTemplate::Tag::NULL_VAR = "_SCRIBE_NULL"; - -//----------------------------------------------------------------------------- -TextTemplate::Config::Config() : - _includes(), - _funcs(), - _logStream(&std::cout), - _numErrors(0), - _includerCallback(TextTemplate::loadFile) { - _paths.push_back(""); -} - -const TextTemplatePointer TextTemplate::Config::addInclude(const ConfigPointer& config, const char* include) { - if (!config) { - return TextTemplatePointer(); - } - - TextTemplatePointer included = config->findInclude(include); - if (included) { - return included; - } - - // INcluded doest exist yet so let's try to create it - String includeStream; - if (config->_includerCallback(config, include, includeStream)) { - // ok, then create a new Template on the include file with this as lib - included = std::make_shared(include, config); - - std::stringstream src(includeStream); - - int nbErrors = included->parse(src); - if (nbErrors > 0) { - included->logError(included->_root, "File failed to parse, not included"); - return TextTemplatePointer(); - } - config->_includes.insert(Includes::value_type(include, included)); - return included; - } - - return TextTemplatePointer(); -} - -const TextTemplatePointer TextTemplate::Config::findInclude(const char* include) { - Includes::iterator includeIt = _includes.find(String(include)); - if (includeIt != _includes.end()) { - return (*includeIt).second; - } else { - return TextTemplatePointer(); - } -} - -void TextTemplate::Config::addIncludePath(const char* path) { - _paths.push_back(String(path)); -} - -bool TextTemplate::loadFile(const ConfigPointer& config, const char* filename, String& source) { - String sourceFile(filename); - String fullfilename; - for (unsigned int i = 0; i < config->_paths.size(); i++) { - fullfilename = config->_paths[i] + sourceFile; - std::ifstream ifs; - ifs.open(fullfilename.c_str()); - if (ifs.is_open()) { - std::string str((std::istreambuf_iterator(ifs)), std::istreambuf_iterator()); - source = str; - ifs.close(); - return (source.length() > 0); - } - } - - return false; -} - -TextTemplate::Funcs::Funcs() : - _funcs() { -} - -TextTemplate::Funcs::~Funcs() { -} - - -const BlockPointer TextTemplate::Funcs::findFunc(const char* func) { - map::iterator it = _funcs.find(String(func)); - if (it != _funcs.end()) { - return (*it).second; - } else { - return BlockPointer(); - } -} - -const BlockPointer TextTemplate::Funcs::addFunc(const char* func, const BlockPointer& funcBlock) { - BlockPointer included = findFunc(func); - if (! included) { - _funcs.insert(map::value_type(func, funcBlock)); - } - return included; -} - -void TextTemplate::Block::addNewBlock(const Pointer& parent, const Pointer& block) { - if (parent) { - parent->blocks.push_back(block); - block->parent = parent; - } -} - -const BlockPointer& TextTemplate::Block::getCurrentBlock(const Pointer& block) { - if (block && block->command.isBlockEnd()) { - return block->parent; - } else { - return block; - } -} - -void TextTemplate::logError(const Block::Pointer& block, const char* fmt, ...) { - va_list argp; - va_start(argp, fmt); - - char buff[256]; - sprintf(buff, fmt, argp); - - _numErrors++; - log() << block->sourceName << " Error >>" << buff << std::endl; - - int level = 1; - displayTree(std::cerr, level); - -} - -bool TextTemplate::grabUntilBeginTag(std::istream* str, std::string& grabbed, Tag::Type& tagType) { - std::stringstream dst; - while (!str->eof()) { - - std::string datatoken; - getline((*str), datatoken, Tag::BEGIN); - dst << datatoken; - - char next = str->peek(); - if (next == Tag::VAR) { - tagType = Tag::VARIABLE; - grabbed = dst.str(); - str->get(); // skip tag char - return true; - } else if (next == Tag::COM) { - tagType = Tag::COMMAND; - grabbed = dst.str(); - str->get(); // skip tag char - return true; - } else if (next == Tag::REM) { - tagType = Tag::REMARK; - grabbed = dst.str(); - str->get(); // skip tag char - return true; - } else { - if (!str->eof()) { - // false positive, just found the Tag::BEGIN without consequence - dst << Tag::BEGIN; - // keep searching - } else { - // end of the file finishing with no tag - tagType = Tag::INVALID; - grabbed = dst.str(); - return true; - } - } - } - - return false; -} - -bool TextTemplate::grabUntilEndTag(std::istream* str, std::string& grabbed, Tag::Type& tagType) { - std::stringstream dst; - - // preEnd char depends on tag type - char preEnd = Tag::COM; - if (tagType == Tag::VARIABLE) { - preEnd = Tag::VAR; - } else if (tagType == Tag::REMARK) { - preEnd = Tag::REM; - } - - while (!str->eof() && !str->fail()) { - // looking for the end of the tag means find the next preEnd - std::string dataToken; - getline((*str), dataToken, preEnd); - char end = str->peek(); - - dst << dataToken; - - // and if the next char is Tag::END then that's it - if (end == Tag::END) { - grabbed = dst.str(); - str->get(); // eat the Tag::END - return true; - } else { - // false positive, keep on searching - dst << preEnd; - } - } - grabbed = dst.str(); - return false; -} - -bool TextTemplate::stepForward(std::istream* str, std::string& grabbed, std::string& tag, Tag::Type& tagType, - Tag::Type& nextTagType) { - if (str->eof()) { - return false; - } - - if (!_steppingStarted) { - _steppingStarted = true; - return grabUntilBeginTag(str, grabbed, nextTagType); - } - - // Read from the last opening Tag::BEGIN captured to the next Tag::END - if (grabUntilEndTag(str, tag, tagType)) { - // skip trailing space and new lines only after Command or Remark tag block - if ((tagType == Tag::COMMAND) || (tagType == Tag::REMARK)) { - while (!str->eof() && !str->fail()) { - char c = str->peek(); - if ((c == ' ') || (c == '\t') || (c == '\n')) { - str->get(); - } else { - break; - } - } - } - - grabUntilBeginTag(str, grabbed, nextTagType); - } - - return true; //hasElement; -} - -const BlockPointer TextTemplate::processStep(const BlockPointer& block, std::string& grabbed, std::string& tag, - Tag::Type& tagType) { - switch (tagType) { - case Tag::INVALID: - block->ostr << grabbed; - return block; - break; - case Tag::VARIABLE: - return processStepVar(block, grabbed, tag); - break; - case Tag::COMMAND: - return processStepCommand(block, grabbed, tag); - break; - case Tag::REMARK: - return processStepRemark(block, grabbed, tag); - break; - } - - logError(block, "Invalid tag"); - return block; -} - -bool TextTemplate::grabFirstToken(String& src, String& token, String& reminder) { - bool goOn = true; - std::string::size_type i = 0; - while (goOn && (i < src.length())) { - char c = src[i]; - if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || (c == '_') || (c == '.') || (c == '[') || (c == ']')) { - token += c; - } else { - if (!token.empty()) { - reminder = src.substr(i); - return true; - } - } - i++; - } - - return (!token.empty()); -} - -bool TextTemplate::convertExpressionToArguments(String& src, std::vector< String >& arguments) { - std::stringstream str(src); - String token; - - while (!str.eof()) { - str >> token; - if (!str.fail()) { - arguments.push_back(token); - } - } - - return true; -} - -bool TextTemplate::convertExpressionToDefArguments(String& src, std::vector< String >& arguments) { - if (src.empty()) { - return false; - } - - std::stringstream argstr(src); - std::stringstream dest; - bool inVar = false; - while (!argstr.eof()) { - // parse the value of a var, try to find a VAR, so look for the pattern BEGIN,VAR ... VAR,END - String token; - char tag; - if (!inVar) { - getline(argstr, token, Tag::BEGIN); - dest << token; - tag = argstr.peek(); - } else { - getline(argstr, token, Tag::END); - dest << token; - tag = token.back(); - } - - if (tag == Tag::VAR) { - if (!inVar) { - // real var coming: - arguments.push_back(dest.str()); - inVar = true; - } else { - // real var over - arguments.push_back(dest.str()); - inVar = false; - } - } else { - if (argstr.eof()) { - arguments.push_back(dest.str()); - } else { - // put back the tag char stolen - dest << (!inVar ? Tag::BEGIN : Tag::END); - } - } - } - - return true; -} - -bool TextTemplate::convertExpressionToFuncArguments(String& src, std::vector< String >& arguments) { - if (src.empty()) { - return false; - } - std::stringstream streamSrc(src); - String params; - getline(streamSrc, params, '('); - getline(streamSrc, params, ')'); - if (params.empty()) { - return false; - } - - std::stringstream str(params); - String token; - int nbTokens = 0; - while (!str.eof()) { - char c = str.peek(); - if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || (c == '_') || (c == '.') || (c == Tag::VAR) || (c == '[') || (c == ']')) { - token += c; - } else if (c == ',') { - if (!token.empty()) { - if (token == Tag::NULL_VAR) { - arguments.push_back(Tag::NULL_VAR); - } else { - arguments.push_back(token); - } - nbTokens++; - } - token.clear(); - } - - str.get(); - } - - if (token.size()) { - arguments.push_back(token); - nbTokens++; - } - - return (nbTokens > 0); -} - -const BlockPointer TextTemplate::processStepVar(const BlockPointer& block, String& grabbed, String& tag) { - // then look at the define - String var = tag; - String varName; - String val; - - if (grabFirstToken(var, varName, val)) { - if (!varName.empty()) { - BlockPointer parent = Block::getCurrentBlock(block); - - // Add a new BLock - BlockPointer newBlock = std::make_shared(_root->sourceName); - (newBlock->ostr) << grabbed; - - newBlock->command.type = Command::VAR; - - newBlock->command.arguments.push_back(varName); - - if (!val.empty()) { - convertExpressionToFuncArguments(val, newBlock->command.arguments); - } - - Block::addNewBlock(parent, newBlock); - - // dive in the new block - return newBlock; - } - } - - return block; -} - -const BlockPointer TextTemplate::processStepCommand(const BlockPointer& block, String& grabbed, String& tag) { - // Grab the command name - String command = tag; - String commandName; - - if (grabFirstToken(command, commandName, command)) { - if (commandName.compare("def") == 0) { - return processStepDef(block, grabbed, command); - } else if (commandName.compare("if") == 0) { - return processStepCommandIf(block, grabbed, command); - } else if (commandName.compare("endif") == 0) { - return processStepCommandEndIf(block, grabbed, command); - } else if (commandName.compare("else") == 0) { - return processStepCommandElse(block, grabbed, command); - } else if (commandName.compare("elif") == 0) { - return processStepCommandElif(block, grabbed, command); - } else if (commandName.compare("include") == 0) { - return processStepInclude(block, grabbed, command); - } else if (commandName.compare("func") == 0) { - return processStepFunc(block, grabbed, command); - } else if (commandName.compare("endfunc") == 0) { - return processStepEndFunc(block, grabbed, command); - } - } - - return block; -} - -const BlockPointer TextTemplate::processStepDef(const BlockPointer& block, String& grabbed, String& def) { - // then look at the define - String varName; - String val; - - if (!grabFirstToken(def, varName, val)) { - logError(block, "Invalid Var name in a tag"); - return block; - } - - BlockPointer parent = Block::getCurrentBlock(block); - - // Add a new BLock - BlockPointer newBlock = std::make_shared(_root->sourceName); - (newBlock->ostr) << grabbed; - - newBlock->command.type = Command::DEF; - newBlock->command.arguments.push_back(varName); - if (!val.empty()) { - // loose first character which should be a white space - val = val.substr(val.find_first_not_of(' ')); - convertExpressionToDefArguments(val, newBlock->command.arguments); - } - - Block::addNewBlock(parent, newBlock); - - // dive in the new block - return newBlock; -} - - -const BlockPointer TextTemplate::processStepCommandIf(const BlockPointer& block, String& grabbed, String& expression) { - BlockPointer parent = Block::getCurrentBlock(block); - - // Add a new BLock depth - BlockPointer newIfBlock = std::make_shared(_root->sourceName); - newIfBlock->command.type = Command::IFBLOCK; - - Block::addNewBlock(parent, newIfBlock); - - BlockPointer newBlock = std::make_shared(_root->sourceName); - (newBlock->ostr) << grabbed; - - newBlock->command.type = Command::IF; - convertExpressionToArguments(expression, newBlock->command.arguments); - - Block::addNewBlock(newIfBlock, newBlock); - - // dive in the new If branch - return newBlock; -} - -const BlockPointer TextTemplate::processStepCommandEndIf(const BlockPointer& block, String& grabbed, String& expression) { - BlockPointer parent = Block::getCurrentBlock(block); - - // are we in a if block ? - if ((parent->command.type == Command::IF) - || (parent->command.type == Command::ELIF) - || (parent->command.type == Command::ELSE)) { - BlockPointer newBlock = std::make_shared(_root->sourceName); - (newBlock->ostr) << grabbed; - - newBlock->command.type = Command::ENDIF; - newBlock->command.arguments.push_back(expression); - - Block::addNewBlock(parent->parent->parent, newBlock); - - return newBlock; - } else { - logError(block, "Invalid block, not in a block"); - return block; - } - - return block; -} - -const BlockPointer TextTemplate::processStepCommandElse(const BlockPointer& block, String& grabbed, String& expression) { - BlockPointer parent = Block::getCurrentBlock(block); - - // are we in a if block ? - if ((parent->command.type == Command::IF) - || (parent->command.type == Command::ELIF)) { - // All good go back to the IfBlock - parent = parent->parent; - - // Add a new BLock depth - BlockPointer newBlock = std::make_shared(_root->sourceName); - newBlock->ostr << grabbed; - newBlock->command.type = Command::ELSE; - newBlock->command.arguments.push_back(expression); - - Block::addNewBlock(parent, newBlock); - - // dive in the new block - return newBlock; - - } else if ((block)->command.type == Command::ELSE) { - logError(block, "Invalid block, in a block but after a block"); - return block; - } else { - logError(block, "Invalid block, not in a block"); - return block; - } -} - -const BlockPointer TextTemplate::processStepCommandElif(const BlockPointer& block, String& grabbed, String& expression) { - BlockPointer parent = Block::getCurrentBlock(block); - - // are we in a if block ? - if ((parent->command.type == Command::IF) - || (parent->command.type == Command::ELIF)) { - // All good go back to the IfBlock - parent = parent->parent; - - // Add a new BLock depth - BlockPointer newBlock = std::make_shared(_root->sourceName); - (newBlock->ostr) << grabbed; - - newBlock->command.type = Command::ELIF; - convertExpressionToArguments(expression, newBlock->command.arguments); - - Block::addNewBlock(parent, newBlock); - - // dive in the new block - return newBlock; - - } else if (parent->command.type == Command::ELSE) { - logError(block, "Invalid block, in a block but after a block"); - return block; - } else { - logError(block, "Invalid block, not in a block"); - return block; - } -} - -const BlockPointer TextTemplate::processStepRemark(const BlockPointer& block, String& grabbed, String& tag) { - // nothing to do :) - - // no need to think, let's just add the grabbed text - (block->ostr) << grabbed; - - return block; -} - -const BlockPointer TextTemplate::processStepInclude(const BlockPointer& block, String& grabbed, String& include) { - BlockPointer parent = Block::getCurrentBlock(block); - - // Add a new BLock - BlockPointer newBlock = std::make_shared(_root->sourceName); - (newBlock->ostr) << grabbed; - - newBlock->command.type = Command::INCLUDE; - convertExpressionToArguments(include, newBlock->command.arguments); - - Block::addNewBlock(parent, newBlock); - - TextTemplatePointer inc = Config::addInclude(_config, newBlock->command.arguments.front().c_str()); - if (!inc) { - logError(newBlock, "Failed to include %s", newBlock->command.arguments.front().c_str()); - } - - // dive in the new block - return newBlock; -} - -const BlockPointer TextTemplate::processStepFunc(const BlockPointer& block, String& grabbed, String& func) { - // then look at the define - String varName; - String var; - - if (!grabFirstToken(func, varName, var)) { - logError(block, "Invalid func name <%s> in a block", func.c_str()); - return block; - } - - // DOes the func already exists ? - if (_config->_funcs.findFunc(varName.c_str())) { - logError(block, "Declaring a new func named <%s> already exists", func.c_str()); - return block; - } - - BlockPointer parent = Block::getCurrentBlock(block); - - // Add a new BLock - BlockPointer newBlock = std::make_shared(_root->sourceName); - (newBlock->ostr) << grabbed; - - newBlock->command.type = Command::FUNC; - newBlock->command.arguments.push_back(varName); - convertExpressionToFuncArguments(var, newBlock->command.arguments); - - _config->_funcs.addFunc(varName.c_str(), newBlock); - - Block::addNewBlock(parent, newBlock); - - // dive in the new block - return newBlock; -} - -const BlockPointer TextTemplate::processStepEndFunc(const BlockPointer& block, String& grabbed, String& tag) { - BlockPointer parent = Block::getCurrentBlock(block); - - // are we in a func block ? - if (parent->command.type == Command::FUNC) { - // Make sure the FUnc has been declared properly - BlockPointer funcBlock = _config->_funcs.findFunc(parent->command.arguments.front().c_str()); - if (funcBlock != parent) { - logError(block, "Mismatching blocks"); - return BlockPointer(); - } - - // Everything is cool , so let's unplugg the FUnc block from this tree and just put the EndFunc block - - BlockPointer newBlock = std::make_shared(_root->sourceName); - (newBlock->ostr) << grabbed; - - newBlock->command.type = Command::ENDFUNC; - convertExpressionToArguments(tag, newBlock->command.arguments); - - newBlock->parent = parent->parent; - - parent->parent->blocks.back() = newBlock; - - parent->parent = 0; - - // dive in the new block - return newBlock; - } else { - logError(block, "Invalid block, not in a block"); - return BlockPointer(); - } - - return block; -} - -TextTemplate::TextTemplate(const String& name, const ConfigPointer& externalConfig) : - _config(externalConfig), - _root(new Block(name)), - _numErrors(0), - _steppingStarted(false) { -} - -TextTemplate::~TextTemplate() { -} - -int TextTemplate::scribe(std::ostream& dst, std::istream& src, Vars& vars) { - int nbErrors = parse(src); - - if (nbErrors == 0) { - return generate(dst, vars); - } - return nbErrors; -} - -int TextTemplate::generateTree(std::ostream& dst, const BlockPointer& block, Vars& vars) { - BlockPointer newCurrentBlock; - int numPasses = evalBlockGeneration(dst, block, vars, newCurrentBlock); - for (int passNum= 0; passNum < numPasses; passNum++) { - dst << newCurrentBlock->ostr.str(); - - for (auto child : newCurrentBlock->blocks) { - generateTree(dst, child, vars); - } - } - - return _numErrors; -} - -int TextTemplate::evalBlockGeneration(std::ostream& dst, const BlockPointer& block, Vars& vars, BlockPointer& branch) { - switch (block->command.type) { - case Command::BLOCK: { - branch = block; - return 1; - } - break; - case Command::VAR: { - Vars::iterator it = vars.find(block->command.arguments.front()); - if (it != vars.end()) { - dst << (*it).second; - } else { - BlockPointer funcBlock = _config->_funcs.findFunc(block->command.arguments.front().c_str()); - if (funcBlock) { - // before diving in the func tree, let's modify the vars with the local defs: - int nbParams = (int)std::min(block->command.arguments.size(), - funcBlock->command.arguments.size()); - std::vector< String > paramCache; - paramCache.push_back(""); - String val; - for (int i = 1; i < nbParams; i++) { - val = block->command.arguments[i]; - if ((val[0] == Tag::VAR) && (val[val.length()-1] == Tag::VAR)) { - val = val.substr(1, val.length()-2); - Vars::iterator it = vars.find(val); - if (it != vars.end()) { - val = (*it).second; - } else { - val = Tag::NULL_VAR; - } - } - - Vars::iterator it = vars.find(funcBlock->command.arguments[i]); - if (it != vars.end()) { - paramCache.push_back((*it).second); - (*it).second = val; - } else { - if (val != Tag::NULL_VAR) { - vars.insert(Vars::value_type(funcBlock->command.arguments[i], val)); - } - - paramCache.push_back(Tag::NULL_VAR); - } - } - - generateTree(dst, funcBlock, vars); - - for (int i = 1; i < nbParams; i++) { - if (paramCache[i] == Tag::NULL_VAR) { - vars.erase(funcBlock->command.arguments[i]); - } else { - vars[funcBlock->command.arguments[i]] = paramCache[i]; - } - } - } - } - branch = block; - return 1; - } - break; - case Command::IFBLOCK: { - // ok, go through the branches and pick the first one that goes - for (auto child: block->blocks) { - int numPasses = evalBlockGeneration(dst, child, vars, branch); - if (numPasses > 0) { - return numPasses; - } - } - } - break; - case Command::IF: - case Command::ELIF: { - if (!block->command.arguments.empty()) { - // Just one argument means check for the var beeing defined - if (block->command.arguments.size() == 1) { - Vars::iterator it = vars.find(block->command.arguments.front()); - if (it != vars.end()) { - branch = block; - return 1; - } - } else if (block->command.arguments.size() == 2) { - if (block->command.arguments[0].compare("not") == 0) { - Vars::iterator it = vars.find(block->command.arguments[1]); - if (it == vars.end()) { - branch = block; - return 1; - } - } - } else if (block->command.arguments.size() == 3) { - if (block->command.arguments[1].compare("and") == 0) { - Vars::iterator itL = vars.find(block->command.arguments[0]); - Vars::iterator itR = vars.find(block->command.arguments[2]); - if ((itL != vars.end()) && (itR != vars.end())) { - branch = block; - return 1; - } - } else if (block->command.arguments[1].compare("or") == 0) { - Vars::iterator itL = vars.find(block->command.arguments[0]); - Vars::iterator itR = vars.find(block->command.arguments[2]); - if ((itL != vars.end()) || (itR != vars.end())) { - branch = block; - return 1; - } - } else if (block->command.arguments[1].compare("==") == 0) { - Vars::iterator itL = vars.find(block->command.arguments[0]); - if (itL != vars.end()) { - if ((*itL).second.compare(block->command.arguments[2]) == 0) { - branch = block; - return 1; - } - } - } - } - - } - return 0; - } - break; - case Command::ELSE: { - branch = block; - return 1; - } - break; - case Command::ENDIF: { - branch = block; - return 1; - } - break; - case Command::DEF: { - if (block->command.arguments.size()) { - // THe actual value of the var defined sneeds to be evaluated: - String val; - for (unsigned int t = 1; t < block->command.arguments.size(); t++) { - // detect if a param is a var - auto len = block->command.arguments[t].length(); - if ((block->command.arguments[t][0] == Tag::VAR) - && (block->command.arguments[t][len - 1] == Tag::VAR)) { - String var = block->command.arguments[t].substr(1, len - 2); - Vars::iterator it = vars.find(var); - if (it != vars.end()) { - val += (*it).second; - } - } else { - val += block->command.arguments[t]; - } - } - - Vars::iterator it = vars.find(block->command.arguments.front()); - if (it == vars.end()) { - vars.insert(Vars::value_type(block->command.arguments.front(), val)); - } else { - (*it).second = val; - } - - branch = block; - return 1; - } else { - branch = block; - return 0; - } - } - break; - - case Command::INCLUDE: { - TextTemplatePointer include = _config->findInclude(block->command.arguments.front().c_str()); - if (include && !include->_root->blocks.empty()) { - if (include->_root) { - generateTree(dst, include->_root, vars); - } - } - - branch = block; - return 1; - } - break; - - case Command::FUNC: { - branch = block; - return 1; - } - break; - - case Command::ENDFUNC: { - branch = block; - return 1; - } - break; - - default: { - } - } - - return 0; -} - - -int TextTemplate::parse(std::istream& src) { - _root->command.type = Command::BLOCK; - BlockPointer currentBlock = _root; - _numErrors = 0; - - // First Parse the input file - int nbLoops = 0; - bool goOn = true; - Tag::Type tagType = Tag::INVALID; - Tag::Type nextTagType = Tag::INVALID; - while (goOn) { - std::string data; - std::string tag; - if (stepForward(&src, data, tag, tagType, nextTagType)) { - currentBlock = processStep(currentBlock, data, tag, tagType); - } else { - goOn = false; - } - - tagType = nextTagType; - nbLoops++; - } - - return _numErrors; -} - -int TextTemplate::generate(std::ostream& dst, Vars& vars) { - return generateTree(dst, _root, vars); -} - -void TextTemplate::displayTree(std::ostream& dst, int& level) const { - Block::displayTree(_root, dst, level); -} - -void TextTemplate::Block::displayTree(const BlockPointer& block, std::ostream& dst, int& level) { - String tab(level * 2, ' '); - - const String BLOCK_TYPE_NAMES[] = { - "VAR", - "BLOCK", - "FUNC", - "ENDFUNC", - "IFBLOCK", - "IF", - "ELIF", - "ELSE", - "ENDIF", - "FOR", - "ENDFOR", - "INCLUDE", - "DEF" - }; - - dst << tab << "{ " << BLOCK_TYPE_NAMES[block->command.type] << ":"; - if (!block->command.arguments.empty()) { - for (auto arg: block->command.arguments) { - dst << " " << arg; - } - } - dst << std::endl; - - level++; - for (auto sub: block->blocks) { - displayTree(sub, dst, level); - } - level--; - - dst << tab << "}" << std::endl; -} - -void TextTemplate::Config::displayTree(std::ostream& dst, int& level) const { - String tab(level * 2, ' '); - - level++; - dst << tab << "Includes:" << std::endl; - for (auto inc: _includes) { - dst << tab << tab << inc.first << std::endl; - inc.second->displayTree(dst, level); - } - dst << tab << "Funcs:" << std::endl; - for (auto func: _funcs._funcs) { - dst << tab << tab << func.first << std::endl; - TextTemplate::Block::displayTree( func.second, dst, level); - } - level--; -} diff --git a/tools/scribe/src/TextTemplate.h b/tools/scribe/src/TextTemplate.h deleted file mode 100755 index 44edc23c12..0000000000 --- a/tools/scribe/src/TextTemplate.h +++ /dev/null @@ -1,206 +0,0 @@ - -// -// TextTemplate.cpp -// tools/shaderScribe/src -// -// Created by Sam Gateau on 12/15/2014. -// Copyright 2013 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -#ifndef hifi_TEXT_TEMPLATE_H -#define hifi_TEXT_TEMPLATE_H - -#include -#include -#include -#include -#include -#include -#include - -class TextTemplate { -public: - typedef std::shared_ptr< TextTemplate > Pointer; - typedef std::string String; - typedef std::vector< String > StringVector; - typedef std::map< String, String > Vars; - typedef std::map< String, TextTemplate::Pointer > Includes; - - class Tag { - public: - enum Type { - VARIABLE = 0, - COMMAND, - REMARK, - INVALID = -1, - }; - - static const char BEGIN = '<'; - static const char END = '>'; - - static const char VAR = '$'; - static const char COM = '@'; - static const char REM = '!'; - - static const std::string NULL_VAR; - }; - - class Command { - public: - typedef std::vector< Command > vector; - - enum Type { - VAR = 0, - BLOCK, - FUNC, - ENDFUNC, - IFBLOCK, - IF, - ELIF, - ELSE, - ENDIF, - FOR, - ENDFOR, - INCLUDE, - DEF, - }; - - Type type; - std::vector< String > arguments; - - bool isBlockEnd() { - switch (type) { - case ENDFUNC: - case ENDIF: - case ENDFOR: - case INCLUDE: - case DEF: - case VAR: - return true; - default: - return false; - } - } - }; - - class Block { - public: - typedef std::shared_ptr Pointer; - typedef std::vector< Block::Pointer > Vector; - - Block::Pointer parent; - Command command; - Vector blocks; - std::stringstream ostr; - - String sourceName; - - Block(const String& sourceFilename) : - sourceName(sourceFilename) {} - - static void addNewBlock(const Block::Pointer& parent, const Block::Pointer& block); - static const Block::Pointer& getCurrentBlock(const Block::Pointer& block); - - static void displayTree(const Block::Pointer& block, std::ostream& dst, int& level); - }; - - class Funcs { - public: - typedef std::map< String, Block::Pointer > map; - - Funcs(); - ~Funcs(); - - const Block::Pointer findFunc(const char* func); - const Block::Pointer addFunc(const char* func, const Block::Pointer& root); - - map _funcs; - protected: - }; - - class Config { - public: - typedef std::shared_ptr< Config > Pointer; - typedef bool (*IncluderCallback) (const Config::Pointer& config, const char* filename, String& source); - - Includes _includes; - Funcs _funcs; - std::ostream* _logStream; - int _numErrors; - IncluderCallback _includerCallback; - StringVector _paths; - - Config(); - - static const TextTemplate::Pointer addInclude(const Config::Pointer& config, const char* include); - const TextTemplate::Pointer findInclude(const char* include); - - void addIncludePath(const char* path); - - void displayTree(std::ostream& dst, int& level) const; - }; - - static bool loadFile(const Config::Pointer& config, const char* filename, String& source); - - TextTemplate(const String& name, const Config::Pointer& config = std::make_shared()); - ~TextTemplate(); - - // Scibe does all the job of parsing an inout template stream and then gneerating theresulting stream using the vars - int scribe(std::ostream& dst, std::istream& src, Vars& vars); - - int parse(std::istream& src); - int generate(std::ostream& dst, Vars& vars); - - const Config::Pointer config() { return _config; } - - void displayTree(std::ostream& dst, int& level) const; - -protected: - Config::Pointer _config; - Block::Pointer _root; - int _numErrors; - bool _steppingStarted; - - bool grabUntilBeginTag(std::istream* str, String& grabbed, Tag::Type& tagType); - bool grabUntilEndTag(std::istream* str, String& grabbed, Tag::Type& tagType); - - bool stepForward(std::istream* str, String& grabbed, String& tag, Tag::Type& tagType, Tag::Type& nextTagType); - - bool grabFirstToken(String& src, String& token, String& reminder); - bool convertExpressionToArguments(String& src, std::vector< String >& arguments); - bool convertExpressionToDefArguments(String& src, std::vector< String >& arguments); - bool convertExpressionToFuncArguments(String& src, std::vector< String >& arguments); - - // Filter between var, command or comments - const Block::Pointer processStep(const Block::Pointer& block, String& grabbed, String& tag, Tag::Type& tagType); - const Block::Pointer processStepVar(const Block::Pointer& block, String& grabbed, String& tag); - const Block::Pointer processStepCommand(const Block::Pointer& block, String& grabbed, String& tag); - const Block::Pointer processStepRemark(const Block::Pointer& block, String& grabbed, String& tag); - - // Define command - const Block::Pointer processStepDef(const Block::Pointer& block, String& grabbed, String& tag); - - // If commands - const Block::Pointer processStepCommandIf(const Block::Pointer& block, String& grabbed, String& expression); - const Block::Pointer processStepCommandEndIf(const Block::Pointer& block, String& grabbed, String& expression); - const Block::Pointer processStepCommandElif(const Block::Pointer& block, String& grabbed, String& expression); - const Block::Pointer processStepCommandElse(const Block::Pointer& block, String& grabbed, String& expression); - - // Include command - const Block::Pointer processStepInclude(const Block::Pointer& block, String& grabbed, String& tag); - - // Function command - const Block::Pointer processStepFunc(const Block::Pointer& block, String& grabbed, String& tag); - const Block::Pointer processStepEndFunc(const Block::Pointer& block, String& grabbed, String& tag); - - // Generation - int generateTree(std::ostream& dst, const Block::Pointer& block, Vars& vars); - int evalBlockGeneration(std::ostream& dst, const Block::Pointer& block, Vars& vars, Block::Pointer& branch); - - // Errors - std::ostream& log() { return (* _config->_logStream); } - void logError(const Block::Pointer& block, const char* error, ...); -}; - -#endif diff --git a/tools/scribe/src/main.cpp b/tools/scribe/src/main.cpp deleted file mode 100755 index c8c540c362..0000000000 --- a/tools/scribe/src/main.cpp +++ /dev/null @@ -1,352 +0,0 @@ -// -// main.cpp -// tools/shaderScribe/src -// -// Created by Sam Gateau on 12/15/2014. -// Copyright 2013 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html - - -#include "TextTemplate.h" - -#include -#include -#include -#include - -using namespace std; - -int main (int argc, char** argv) { - // process the command line arguments - std::vector< std::string > inputs; - - std::string srcFilename; - std::string destFilename; - std::string targetName; - TextTemplate::Vars vars; - - std::string lastVarName; - bool listVars = false; - bool showParseTree = false; - bool makeCPlusPlus = false; - - auto config = std::make_shared(); - - enum Mode { - READY = 0, - GRAB_OUTPUT, - GRAB_VAR_NAME, - GRAB_VAR_VALUE, - GRAB_INCLUDE_PATH, - GRAB_TARGET_NAME, - GRAB_SHADER_TYPE, - EXIT, - } mode = READY; - - enum Type { - VERTEX = 0, - FRAGMENT, - GEOMETRY, - } type = VERTEX; - static const char* shaderTypeString[] = { - "VERTEX", "PIXEL", "GEOMETRY" - }; - static const char* shaderCreateString[] = { - "Vertex", "Pixel", "Geometry" - }; - std::string shaderStage{ "vert" }; - - for (int ii = 1; (mode != EXIT) && (ii < argc); ii++) { - inputs.push_back(argv[ii]); - - switch (mode) { - case READY: { - if (inputs.back() == "-o") { - mode = GRAB_OUTPUT; - } else if (inputs.back() == "-t") { - mode = GRAB_TARGET_NAME; - } else if (inputs.back() == "-D") { - mode = GRAB_VAR_NAME; - } else if (inputs.back() == "-I") { - mode = GRAB_INCLUDE_PATH; - } else if (inputs.back() == "-listVars") { - listVars = true; - mode = READY; - } else if (inputs.back() == "-showParseTree") { - showParseTree = true; - mode = READY; - } else if (inputs.back() == "-c++") { - makeCPlusPlus = true; - mode = READY; - } else if (inputs.back() == "-T") { - mode = GRAB_SHADER_TYPE; - } else { - // just grabbed the source filename, stop parameter parsing - srcFilename = inputs.back(); - mode = EXIT; - } - } - break; - - case GRAB_OUTPUT: { - destFilename = inputs.back(); - mode = READY; - } - break; - - case GRAB_TARGET_NAME: { - targetName = inputs.back(); - mode = READY; - } - break; - - case GRAB_VAR_NAME: { - // grab first the name of the var - lastVarName = inputs.back(); - mode = GRAB_VAR_VALUE; - } - break; - case GRAB_VAR_VALUE: { - // and then the value - vars.insert(TextTemplate::Vars::value_type(lastVarName, inputs.back())); - - mode = READY; - } - break; - - case GRAB_INCLUDE_PATH: { - config->addIncludePath(inputs.back().c_str()); - mode = READY; - } - break; - - case GRAB_SHADER_TYPE: - { - if (inputs.back() == "frag") { - shaderStage = inputs.back(); - type = FRAGMENT; - } else if (inputs.back() == "geom") { - shaderStage = inputs.back(); - type = GEOMETRY; - } else if (inputs.back() == "vert") { - shaderStage = inputs.back(); - type = VERTEX; - } else { - cerr << "Unrecognized shader type. Supported is vert, frag or geom" << endl; - } - mode = READY; - } - break; - - case EXIT: { - // THis shouldn't happen - } - break; - } - } - - if (srcFilename.empty()) { - cerr << "Usage: shaderScribe [OPTION]... inputFilename" << endl; - cerr << "Where options include:" << endl; - cerr << " -o filename: Send output to filename rather than standard output." << endl; - cerr << " -t targetName: Set the targetName used, if not defined use the output filename 'name' and if not defined use the inputFilename 'name'" << endl; - cerr << " -I include_directory: Declare a directory to be added to the includes search pool." << endl; - cerr << " -D varname varvalue: Declare a var used to generate the output file." << endl; - cerr << " varname and varvalue must be made of alpha numerical characters with no spaces." << endl; - cerr << " -listVars : Will list the vars name and value in the standard output." << endl; - cerr << " -showParseTree : Draw the tree obtained while parsing the source" << endl; - cerr << " -c++ : Generate a c++ source file containing the output file stream stored as a char[] variable" << endl; - cerr << " -T vert/frag/geom : define the type of the shader. Defaults to VERTEX if not specified." << endl; - cerr << " This is necessary if the -c++ option is used." << endl; - return 0; - } - - // Define targetName: if none, get destFilename, if none get srcFilename - if (targetName.empty()) { - if (destFilename.empty()) { - targetName = srcFilename; - } else { - targetName = destFilename; - } - } - // no clean it to have just a descent c var name - if (!targetName.empty()) { - // trim anything before '/' or '\' - targetName = targetName.substr(targetName.find_last_of('/') + 1); - targetName = targetName.substr(targetName.find_last_of('\\') + 1); - - // trim anything after '.' - targetName = targetName.substr(0, targetName.find_first_of('.')); - } - - // Add built in vars - time_t endTime = chrono::system_clock::to_time_t(chrono::system_clock::now()); - std::string endTimStr(ctime(&endTime)); - vars["_SCRIBE_DATE"] = endTimStr.substr(0, endTimStr.length() - 1); - - // List vars? - if (listVars) { - cerr << "Vars:" << endl; - for (auto v : vars) { - cerr << " " << v.first << " = " << v.second << endl; - } - } - - // Open up source - std::fstream srcStream; - srcStream.open(srcFilename, std::fstream::in); - if (!srcStream.is_open()) { - cerr << "Failed to open source file <" << srcFilename << ">" << endl; - return 1; - } - - auto scribe = std::make_shared(srcFilename, config); - - // ready to parse and generate - std::stringstream destStringStream; - int numErrors = scribe->scribe(destStringStream, srcStream, vars); - if (numErrors) { - cerr << "Scribe " << srcFilename << "> failed: " << numErrors << " errors." << endl; - return 1; - }; - - - if (showParseTree) { - int level = 1; - cerr << "Config trees:" << std::endl; - config->displayTree(cerr, level); - - cerr << srcFilename << " tree:" << std::endl; - scribe->displayTree(cerr, level); - } - - if (makeCPlusPlus) { - // Because there is a maximum size for literal strings declared in source we need to partition the - // full source string stream into pages that seems to be around that value... - const int MAX_STRING_LITERAL = 10000; - std::string lineToken; - auto pageSize = lineToken.length(); - std::vector> pages(1, std::make_shared()); - while (!destStringStream.eof()) { - std::getline(destStringStream, lineToken); - auto lineSize = lineToken.length() + 1; - - if (pageSize + lineSize > MAX_STRING_LITERAL) { - pages.push_back(std::make_shared()); - // reset pageStringStream - pageSize = 0; - } - - (*pages.back()) << lineToken << std::endl; - pageSize += lineSize; - } - - std::stringstream headerStringStream; - std::stringstream sourceStringStream; - std::string headerFileName = destFilename + ".h"; - std::string sourceFileName = destFilename + ".cpp"; - - sourceStringStream << "// File generated by Scribe " << vars["_SCRIBE_DATE"] << std::endl; - - // Write header file - headerStringStream << "#ifndef " << targetName << "_h" << std::endl; - headerStringStream << "#define " << targetName << "_h\n" << std::endl; - headerStringStream << "#include \n" << std::endl; - headerStringStream << "class " << targetName << " {" << std::endl; - headerStringStream << "public:" << std::endl; - headerStringStream << "\tstatic gpu::Shader::Type getType() { return gpu::Shader::" << shaderTypeString[type] << "; }" << std::endl; - headerStringStream << "\tstatic const std::string& getSource() { return _source; }" << std::endl; - headerStringStream << "\tstatic gpu::ShaderPointer getShader();" << std::endl; - headerStringStream << "private:" << std::endl; - headerStringStream << "\tstatic const std::string _source;" << std::endl; - headerStringStream << "\tstatic gpu::ShaderPointer _shader;" << std::endl; - headerStringStream << "};\n" << std::endl; - headerStringStream << "#endif // " << targetName << "_h" << std::endl; - - bool mustOutputHeader = destFilename.empty(); - // Compare with existing file - { - std::fstream headerFile; - headerFile.open(headerFileName, std::fstream::in); - if (headerFile.is_open()) { - // Skip first line - std::string line; - std::stringstream previousHeaderStringStream; - std::getline(headerFile, line); - - previousHeaderStringStream << headerFile.rdbuf(); - mustOutputHeader = mustOutputHeader || previousHeaderStringStream.str() != headerStringStream.str(); - } else { - mustOutputHeader = true; - } - } - - if (mustOutputHeader) { - if (!destFilename.empty()) { - // File content has changed so write it - std::fstream headerFile; - headerFile.open(headerFileName, std::fstream::out); - if (headerFile.is_open()) { - // First line contains the date of modification - headerFile << sourceStringStream.str(); - headerFile << headerStringStream.str(); - } else { - cerr << "Scribe output file <" << headerFileName << "> failed to open." << endl; - return 1; - } - } else { - cerr << sourceStringStream.str(); - cerr << headerStringStream.str(); - } - } - - // Write source file - sourceStringStream << "#include \"" << targetName << ".h\"\n" << std::endl; - sourceStringStream << "gpu::ShaderPointer " << targetName << "::_shader;" << std::endl; - sourceStringStream << "const std::string " << targetName << "::_source = std::string()"; - // Write the pages content - for (auto page : pages) { - sourceStringStream << "+ std::string(R\"SCRIBE(\n" << page->str() << "\n)SCRIBE\")\n"; - } - sourceStringStream << ";\n" << std::endl << std::endl; - - sourceStringStream << "gpu::ShaderPointer " << targetName << "::getShader() {" << std::endl; - sourceStringStream << "\tif (_shader==nullptr) {" << std::endl; - sourceStringStream << "\t\t_shader = gpu::Shader::create" << shaderCreateString[type] << "(std::string(_source));" << std::endl; - sourceStringStream << "\t}" << std::endl; - sourceStringStream << "\treturn _shader;" << std::endl; - sourceStringStream << "}\n" << std::endl; - - // Destination stream - if (!destFilename.empty()) { - std::fstream sourceFile; - sourceFile.open(sourceFileName, std::fstream::out); - if (!sourceFile.is_open()) { - cerr << "Scribe output file <" << sourceFileName << "> failed to open." << endl; - return 1; - } - sourceFile << sourceStringStream.str(); - } else { - cerr << sourceStringStream.str(); - } - } else { - // Destination stream - if (!destFilename.empty()) { - std::fstream destFileStream; - destFileStream.open(destFilename, std::fstream::out); - if (!destFileStream.is_open()) { - cerr << "Scribe output file <" << destFilename << "> failed to open." << endl; - return 1; - } - - destFileStream << destStringStream.str(); - } else { - cerr << destStringStream.str(); - } - } - - return 0; - -} diff --git a/tools/shadergen.py b/tools/shadergen.py new file mode 100644 index 0000000000..ffbe1662ec --- /dev/null +++ b/tools/shadergen.py @@ -0,0 +1,251 @@ +import re +import subprocess +import sys +import time +import os +import json +import argparse +import concurrent +from os.path import expanduser +from concurrent.futures import ThreadPoolExecutor +from argparse import ArgumentParser +from pathlib import Path +from threading import Lock + + # # Target dependant Custom rule on the SHADER_FILE + # if (ANDROID) + # set(GLPROFILE LINUX_GL) + # else() + # if (APPLE) + # set(GLPROFILE MAC_GL) + # elseif(UNIX) + # set(GLPROFILE LINUX_GL) + # else() + # set(GLPROFILE PC_GL) + # endif() + # endif() + +def getTypeForScribeFile(scribefilename): + last = scribefilename.rfind('.') + extension = scribefilename[last:] + switcher = { + '.slv': 'vert', + '.slf': 'frag', + '.slg': 'geom', + '.slc': 'comp', + } + if not extension in switcher: + raise ValueError("Unknown scribe file type for " + scribefilename) + return switcher.get(extension) + +def getCommonScribeArgs(scribefile, includeLibs): + scribeArgs = [os.path.join(args.tools_dir, 'scribe')] + # FIXME use the sys.platform to set the correct value + scribeArgs.extend(['-D', 'GLPROFILE', 'PC_GL']) + scribeArgs.extend(['-T', getTypeForScribeFile(scribefile)]) + for lib in includeLibs: + scribeArgs.extend(['-I', args.source_dir + '/libraries/' + lib + '/src/' + lib + '/']) + scribeArgs.extend(['-I', args.source_dir + '/libraries/' + lib + '/src/']) + scribeArgs.append(scribefile) + return scribeArgs + +def getDialectAndVariantHeaders(dialect, variant): + headerPath = args.source_dir + '/libraries/shaders/headers/' + variantHeader = headerPath + ('stereo.glsl' if (variant == 'stereo') else 'mono.glsl') + dialectHeader = headerPath + dialect + '/header.glsl' + return [dialectHeader, variantHeader] + +class ScribeDependenciesCache: + cache = {} + lock = Lock() + filename = '' + + def __init__(self, filename): + self.filename = filename + + def load(self): + jsonstr = '{}' + if (os.path.exists(self.filename)): + with open(self.filename) as f: + jsonstr = f.read() + self.cache = json.loads(jsonstr) + + def save(self): + with open(self.filename, "w") as f: + f.write(json.dumps(self.cache)) + + def get(self, scribefile, dialect, variant): + self.lock.acquire() + key = self.key(scribefile, dialect, variant) + try: + if key in self.cache: + return self.cache[key].copy() + finally: + self.lock.release() + return None + + def key(self, scribeFile, dialect, variant): + return ':'.join([scribeFile, dialect, variant]) + + def getOrGen(self, scribefile, includeLibs, dialect, variant): + result = self.get(scribefile, dialect, variant) + if (None == result): + result = self.gen(scribefile, includeLibs, dialect, variant) + return result + + def gen(self, scribefile, includeLibs, dialect, variant): + scribeArgs = getCommonScribeArgs(scribefile, includeLibs) + scribeArgs.extend(['-M']) + processResult = subprocess.run(scribeArgs, stdout=subprocess.PIPE) + if (0 != processResult.returncode): + raise RuntimeError("Unable to parse scribe dependencies") + result = processResult.stdout.decode("utf-8").splitlines(False) + result.append(scribefile) + result.extend(getDialectAndVariantHeaders(dialect, variant)) + key = self.key(scribefile, dialect, variant) + self.lock.acquire() + self.cache[key] = result.copy() + self.lock.release() + return result + +def getFileTimes(files): + if isinstance(files, str): + files = [files] + return list(map(lambda f: os.path.getmtime(f) if os.path.isfile(f) else -1, files)) + +def outOfDate(inputs, output): + oldestInput = max(getFileTimes(inputs)) + youngestOutput = min(getFileTimes(output)) + diff = youngestOutput - oldestInput + return oldestInput >= youngestOutput + +def executeSubprocess(processArgs): + processResult = subprocess.run(processArgs, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + if (0 != processResult.returncode): + raise RuntimeError('Call to "{}" failed.\n\narguments:\n{}\n\nstdout:\n{}\n\nstderr:\n{}'.format( + processArgs[0], + ' '.join(processArgs[1:]), + processResult.stdout.decode('utf-8'), + processResult.stderr.decode('utf-8'))) + +folderMutex = Lock() + +def processCommand(line): + global args + global scribeDepCache + glslangExec = args.tools_dir + '/glslangValidator' + spirvCrossExec = args.tools_dir + '/spirv-cross' + spirvOptExec = args.tools_dir + '/spirv-opt' + params = line.split(';') + dialect = params.pop(0) + variant = params.pop(0) + scribeFile = args.source_dir + '/' + params.pop(0) + unoptGlslFile = args.source_dir + '/' + params.pop(0) + libs = params + + upoptSpirvFile = unoptGlslFile + '.spv' + spirvFile = unoptGlslFile + '.opt.spv' + reflectionFile = unoptGlslFile + '.json' + glslFile = unoptGlslFile + '.glsl' + outputFiles = [unoptGlslFile, spirvFile, reflectionFile, glslFile] + + scribeOutputDir = os.path.abspath(os.path.join(unoptGlslFile, os.pardir)) + + # Serialize checking and creation of the output directory to avoid occasional + # crashes + global folderMutex + folderMutex.acquire() + if not os.path.exists(scribeOutputDir): + os.makedirs(scribeOutputDir) + folderMutex.release() + + scribeDeps = scribeDepCache.getOrGen(scribeFile, libs, dialect, variant) + + # if the scribe sources (slv, slf, slh, etc), or the dialect/ variant headers are out of date + # regenerate the scribe GLSL output + if args.force or outOfDate(scribeDeps, outputFiles): + print('Processing file {} dialect {} variant {}'.format(scribeFile, dialect, variant)) + if args.dry_run: + return True + + scribeDepCache.gen(scribeFile, libs, dialect, variant) + scribeArgs = getCommonScribeArgs(scribeFile, libs) + for header in getDialectAndVariantHeaders(dialect, variant): + scribeArgs.extend(['-H', header]) + scribeArgs.extend(['-o', unoptGlslFile]) + executeSubprocess(scribeArgs) + + # Generate the un-optimized output + executeSubprocess([glslangExec, '-V110', '-o', upoptSpirvFile, unoptGlslFile]) + + # Optimize the SPIRV + executeSubprocess([spirvOptExec, '-O', '-o', spirvFile, upoptSpirvFile]) + + # Generation JSON reflection + executeSubprocess([spirvCrossExec, '--reflect', 'json', '--output', reflectionFile, spirvFile]) + + # Generate the optimized GLSL output + spirvCrossDialect = dialect + # 310es causes spirv-cross to inject "#extension GL_OES_texture_buffer : require" into the output + if (dialect == '310es'): spirvCrossDialect = '320es' + spirvCrossArgs = [spirvCrossExec, '--output', glslFile, spirvFile, '--version', spirvCrossDialect] + if (dialect == '410'): spirvCrossArgs.append('--no-420pack-extension') + executeSubprocess(spirvCrossArgs) + else: + # This logic is necessary because cmake will agressively keep re-executing the shadergen + # code otherwise + Path(unoptGlslFile).touch() + Path(upoptSpirvFile).touch() + Path(spirvFile).touch() + Path(glslFile).touch() + Path(reflectionFile).touch() + return True + + + +def main(): + commands = args.commands.read().splitlines(False) + if args.debug: + for command in commands: + processCommand(command) + else: + workers = max(1, os.cpu_count() - 2) + with ThreadPoolExecutor(max_workers=workers) as executor: + for result in executor.map(processCommand, commands): + if not result: + raise RuntimeError("Failed to execute all subprocesses") + executor.shutdown() + + +parser = ArgumentParser(description='Generate shader artifacts.') +parser.add_argument('--commands', type=argparse.FileType('r'), help='list of commands to execute') +parser.add_argument('--tools-dir', type=str, help='location of the host compatible binaries') +parser.add_argument('--build-dir', type=str, help='The build directory base path') +parser.add_argument('--source-dir', type=str, help='The root directory of the git repository') +parser.add_argument('--debug', action='store_true') +parser.add_argument('--force', action='store_true', help='Ignore timestamps and force regeneration of all files') +parser.add_argument('--dry-run', action='store_true', help='Report the files that would be process, but do not output') + +args = None +if len(sys.argv) == 1: + # for debugging + sourceDir = expanduser('~/git/hifi') + toolsDir = os.path.join(expanduser('~/git/vcpkg'), 'installed', 'x64-windows', 'tools') + buildPath = sourceDir + '/build' + commandsPath = buildPath + '/libraries/shaders/shadergen.txt' + shaderDir = buildPath + '/libraries/shaders' + testArgs = '--commands {} --tools-dir {} --build-dir {} --source-dir {}'.format( + commandsPath, toolsDir, shaderDir, sourceDir + ).split() + testArgs.append('--debug') + testArgs.append('--force') + #testArgs.append('--dry-run') + args = parser.parse_args(testArgs) +else: + args = parser.parse_args() + +scribeDepCache = ScribeDependenciesCache(args.build_dir + '/shaderDeps.json') +scribeDepCache.load() +main() +scribeDepCache.save() + diff --git a/tools/shreflect/CMakeLists.txt b/tools/shreflect/CMakeLists.txt deleted file mode 100644 index 0748f59d31..0000000000 --- a/tools/shreflect/CMakeLists.txt +++ /dev/null @@ -1,10 +0,0 @@ -set(TARGET_NAME shreflect) - -# don't use the setup_hifi_project macro as we don't want Qt or GLM dependencies -file(GLOB TARGET_SRCS src/*) -add_executable(${TARGET_NAME} ${TARGET_SRCS}) -target_json() - -if (WIN32) - set_property(TARGET ${TARGET_NAME} APPEND_STRING PROPERTY LINK_FLAGS_DEBUG "/OPT:NOREF /OPT:NOICF") -endif() diff --git a/tools/shreflect/src/main.cpp b/tools/shreflect/src/main.cpp deleted file mode 100644 index e13f937102..0000000000 --- a/tools/shreflect/src/main.cpp +++ /dev/null @@ -1,204 +0,0 @@ -// -// Bradley Austin Davis on 2018/05/24 -// Copyright 2013 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html - - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -using json = nlohmann::json; - -std::vector splitStringIntoLines(const std::string& s) { - std::stringstream ss(s); - std::vector result; - std::string line; - while (std::getline(ss, line, '\n')) { - result.push_back(line); - } - return result; -} - -std::string readFile(const std::string& file) { - std::ifstream t(file); - std::string str((std::istreambuf_iterator(t)), - std::istreambuf_iterator()); - return str; -} - -void writeFile(const std::string& file, const std::string& out) { - std::ofstream t(file, std::ios::trunc); - t << out; - t.close(); -} - -// Convert a Perl style multi-line commented regex into a C++ style regex -// All whitespace will be removed and lines with '#' comments will have the comments removed -std::string getUnformattedRegex(const std::string& formatted) { - static const std::regex WHITESPACE = std::regex("\\s+"); - static const std::string EMPTY; - std::string result; - result.reserve(formatted.size()); - auto lines = splitStringIntoLines(formatted); - for (auto line : lines) { - auto commentStart = line.find('#'); - if (std::string::npos != commentStart) { - line = line.substr(0, commentStart); - } - line = std::regex_replace(line, WHITESPACE, EMPTY); - result += line; - } - - return result; -} - -static std::string LAYOUT_REGEX_STRING{ R"REGEX( -^layout\( # BEGIN layout declaration block - (\s*std140\s*,\s*)? # Optional std140 marker - (binding|location) # binding / location - \s*=\s* - (?: - (\b\d+\b) # literal numeric binding like binding=1 - | - (\b[A-Z_0-9]+\b) # Preprocessor macro binding like binding=GPU_TEXTURE_FOO - ) -\)\s* # END layout declaration block -(?: - ( # Texture or simple uniform like `layout(binding=0) uniform sampler2D originalTexture;` - uniform\s+ - (\b\w+\b)\s+ - (\b\w+\b)\s* - (?:\[\d*\])? - (?:\s*=.*)? - \s*;.*$ - ) - | - ( # UBO or SSBO like `layout(std140, binding=GPU_STORAGE_TRANSFORM_OBJECT) buffer transformObjectBuffer {` - \b(uniform|buffer)\b\s+ - \b(\w+\b) - \s*\{.*$ - ) - | - ( # Input or output attribute like `layout(location=GPU_ATTR_POSITION) in vec4 inPosition;` - \b(in|out)\b\s+ - \b(\w+)\b\s+ - \b(\w+)\b\s*;\s*$ - ) -) -)REGEX" }; - -enum Groups { - STD140 = 1, - LOCATION_TYPE = 2, - LOCATION_LITERAL = 3, - LOCATION_DEFINE = 4, - DECL_SIMPLE = 5, - SIMPLE_TYPE = 6, - SIMPLE_NAME = 7, - DECL_STRUCT = 8, - STRUCT_TYPE = 9, - STRUCT_NAME = 10, - DECL_INOUT = 11, - INOUT_DIRECTION = 12, - INOUT_TYPE = 13, - INOUT_NAME = 14, -}; - -json reflectShader(const std::string& shaderPath) { - static const std::regex DEFINE("^#define\\s+([_A-Z0-9]+)\\s+(\\d+)\\s*$"); - static const std::regex LAYOUT_QUALIFIER{ getUnformattedRegex(LAYOUT_REGEX_STRING) }; - - - auto shaderSource = readFile(shaderPath); - std::vector lines = splitStringIntoLines(shaderSource); - using Map = std::unordered_map; - - json inputs; - json outputs; - json textures; - json textureTypes; - json uniforms; - json storageBuffers; - json uniformBuffers; - Map locationDefines; - for (const auto& line : lines) { - std::cmatch m; - if (std::regex_match(line.c_str(), m, DEFINE)) { - locationDefines[m[1].str()] = std::stoi(m[2].first); - } else if (std::regex_match(line.c_str(), m, LAYOUT_QUALIFIER)) { - int binding = -1; - if (m[LOCATION_LITERAL].matched) { - binding = std::stoi(m[LOCATION_LITERAL].str()); - } else { - binding = locationDefines[m[LOCATION_DEFINE].str()]; - } - if (m[DECL_SIMPLE].matched) { - auto name = m[SIMPLE_NAME].str(); - auto type = m[SIMPLE_TYPE].str(); - bool isTexture = 0 == type.find("sampler"); - auto& map = isTexture ? textures : uniforms; - map[name] = binding; - if (isTexture) { - textureTypes[name] = type; - } - } else if (m[DECL_STRUCT].matched) { - auto name = m[STRUCT_NAME].str(); - auto type = m[STRUCT_TYPE].str(); - auto& map = (type == "buffer") ? storageBuffers : uniformBuffers; - map[name] = binding; - } else if (m[DECL_INOUT].matched) { - auto name = m[INOUT_NAME].str(); - auto& map = (m[INOUT_DIRECTION].str() == "in") ? inputs : outputs; - map[name] = binding; - } - } - } - - json result; - if (!inputs.empty()) { - result["inputs"] = inputs; - } - if (!outputs.empty()) { - result["outputs"] = outputs; - } - if (!textures.empty()) { - result["textures"] = textures; - } - if (!textureTypes.empty()) { - result["texturesTypes"] = textureTypes; - } - if (!uniforms.empty()) { - result["uniforms"] = uniforms; - } - if (!storageBuffers.empty()) { - result["storageBuffers"] = storageBuffers; - } - if (!uniformBuffers.empty()) { - result["uniformBuffers"] = uniformBuffers; - } - - result["name"] = shaderPath; - - return result; -} - -int main (int argc, char** argv) { - auto path = std::string(argv[1]); - auto shaderReflection = reflectShader(path); - writeFile(path + ".json", shaderReflection.dump(4)); - return 0; -} diff --git a/tools/skeleton-dump/CMakeLists.txt b/tools/skeleton-dump/CMakeLists.txt index d0a6475c59..baec1d163b 100644 --- a/tools/skeleton-dump/CMakeLists.txt +++ b/tools/skeleton-dump/CMakeLists.txt @@ -1,5 +1,4 @@ set(TARGET_NAME skeleton-dump) setup_hifi_project(Core) setup_memory_debugger() -link_hifi_libraries(shared fbx graphics gpu gl animation) - +link_hifi_libraries(shared fbx hfm graphics gpu gl animation) diff --git a/tools/skeleton-dump/src/SkeletonDumpApp.cpp b/tools/skeleton-dump/src/SkeletonDumpApp.cpp index e9d8243e38..10b13aef36 100644 --- a/tools/skeleton-dump/src/SkeletonDumpApp.cpp +++ b/tools/skeleton-dump/src/SkeletonDumpApp.cpp @@ -54,8 +54,8 @@ SkeletonDumpApp::SkeletonDumpApp(int argc, char* argv[]) : QCoreApplication(argc return; } QByteArray blob = file.readAll(); - std::unique_ptr fbxGeometry(readFBX(blob, QVariantHash())); - std::unique_ptr skeleton(new AnimSkeleton(*fbxGeometry)); + std::unique_ptr geometry(readFBX(blob, QVariantHash())); + std::unique_ptr skeleton(new AnimSkeleton(*geometry)); skeleton->dump(verbose); } diff --git a/tools/vhacd-util/CMakeLists.txt b/tools/vhacd-util/CMakeLists.txt index 5ba8b7d971..aa6642c610 100644 --- a/tools/vhacd-util/CMakeLists.txt +++ b/tools/vhacd-util/CMakeLists.txt @@ -1,6 +1,6 @@ set(TARGET_NAME vhacd-util) setup_hifi_project(Core) -link_hifi_libraries(shared fbx graphics gpu gl) +link_hifi_libraries(shared fbx hfm graphics gpu gl) add_dependency_external_projects(vhacd) diff --git a/tools/vhacd-util/src/VHACDUtil.cpp b/tools/vhacd-util/src/VHACDUtil.cpp index a52e948f01..8de9c39da9 100644 --- a/tools/vhacd-util/src/VHACDUtil.cpp +++ b/tools/vhacd-util/src/VHACDUtil.cpp @@ -19,16 +19,16 @@ // FBXReader jumbles the order of the meshes by reading them back out of a hashtable. This will put // them back in the order in which they appeared in the file. -bool FBXGeometryLessThan(const FBXMesh& e1, const FBXMesh& e2) { +bool HFMModelLessThan(const HFMMesh& e1, const HFMMesh& e2) { return e1.meshIndex < e2.meshIndex; } -void reSortFBXGeometryMeshes(FBXGeometry& geometry) { - qSort(geometry.meshes.begin(), geometry.meshes.end(), FBXGeometryLessThan); +void reSortHFMModelMeshes(HFMModel& hfmModel) { + qSort(hfmModel.meshes.begin(), hfmModel.meshes.end(), HFMModelLessThan); } // Read all the meshes from provided FBX file -bool vhacd::VHACDUtil::loadFBX(const QString filename, FBXGeometry& result) { +bool vhacd::VHACDUtil::loadFBX(const QString filename, HFMModel& result) { if (_verbose) { qDebug() << "reading FBX file =" << filename << "..."; } @@ -41,19 +41,19 @@ bool vhacd::VHACDUtil::loadFBX(const QString filename, FBXGeometry& result) { } try { QByteArray fbxContents = fbx.readAll(); - FBXGeometry::Pointer geom; + HFMModel::Pointer hfmModel; if (filename.toLower().endsWith(".obj")) { bool combineParts = false; - geom = OBJReader().readOBJ(fbxContents, QVariantHash(), combineParts); + hfmModel = OBJReader().readOBJ(fbxContents, QVariantHash(), combineParts); } else if (filename.toLower().endsWith(".fbx")) { - geom.reset(readFBX(fbxContents, QVariantHash(), filename)); + hfmModel.reset(readFBX(fbxContents, QVariantHash(), filename)); } else { qWarning() << "file has unknown extension" << filename; return false; } - result = *geom; + result = *hfmModel; - reSortFBXGeometryMeshes(result); + reSortHFMModelMeshes(result); } catch (const QString& error) { qWarning() << "error reading" << filename << ":" << error; return false; @@ -63,7 +63,7 @@ bool vhacd::VHACDUtil::loadFBX(const QString filename, FBXGeometry& result) { } -void getTrianglesInMeshPart(const FBXMeshPart &meshPart, std::vector& triangleIndices) { +void getTrianglesInMeshPart(const HFMMeshPart &meshPart, std::vector& triangleIndices) { // append triangle indices triangleIndices.reserve(triangleIndices.size() + (size_t)meshPart.triangleIndices.size()); for (auto index : meshPart.triangleIndices) { @@ -88,12 +88,12 @@ void getTrianglesInMeshPart(const FBXMeshPart &meshPart, std::vector& trian } } -void vhacd::VHACDUtil::fattenMesh(const FBXMesh& mesh, const glm::mat4& geometryOffset, FBXMesh& result) const { +void vhacd::VHACDUtil::fattenMesh(const HFMMesh& mesh, const glm::mat4& modelOffset, HFMMesh& result) const { // this is used to make meshes generated from a highfield collidable. each triangle // is converted into a tetrahedron and made into its own mesh-part. std::vector triangleIndices; - foreach (const FBXMeshPart &meshPart, mesh.parts) { + foreach (const HFMMeshPart &meshPart, mesh.parts) { getTrianglesInMeshPart(meshPart, triangleIndices); } @@ -104,7 +104,7 @@ void vhacd::VHACDUtil::fattenMesh(const FBXMesh& mesh, const glm::mat4& geometry int indexStartOffset = result.vertices.size(); // new mesh gets the transformed points from the original - glm::mat4 totalTransform = geometryOffset * mesh.modelTransform; + glm::mat4 totalTransform = modelOffset * mesh.modelTransform; for (int i = 0; i < mesh.vertices.size(); i++) { // apply the source mesh's transform to the points glm::vec4 v = totalTransform * glm::vec4(mesh.vertices[i], 1.0f); @@ -145,7 +145,7 @@ void vhacd::VHACDUtil::fattenMesh(const FBXMesh& mesh, const glm::mat4& geometry int index3 = result.vertices.size(); result.vertices << p3; // add the new point to the result mesh - FBXMeshPart newMeshPart; + HFMMeshPart newMeshPart; setMeshPartDefaults(newMeshPart, "unknown"); newMeshPart.triangleIndices << index0 << index1 << index2; newMeshPart.triangleIndices << index0 << index3 << index1; @@ -155,7 +155,7 @@ void vhacd::VHACDUtil::fattenMesh(const FBXMesh& mesh, const glm::mat4& geometry } } -AABox getAABoxForMeshPart(const FBXMesh& mesh, const FBXMeshPart &meshPart) { +AABox getAABoxForMeshPart(const HFMMesh& mesh, const HFMMeshPart &meshPart) { AABox aaBox; const int TRIANGLE_STRIDE = 3; for (int i = 0; i < meshPart.triangleIndices.size(); i += TRIANGLE_STRIDE) { @@ -242,7 +242,7 @@ bool isClosedManifold(const std::vector& triangleIndices) { return true; } -void vhacd::VHACDUtil::getConvexResults(VHACD::IVHACD* convexifier, FBXMesh& resultMesh) const { +void vhacd::VHACDUtil::getConvexResults(VHACD::IVHACD* convexifier, HFMMesh& resultMesh) const { // Number of hulls for this input meshPart uint32_t numHulls = convexifier->GetNConvexHulls(); if (_verbose) { @@ -256,8 +256,8 @@ void vhacd::VHACDUtil::getConvexResults(VHACD::IVHACD* convexifier, FBXMesh& res VHACD::IVHACD::ConvexHull hull; convexifier->GetConvexHull(j, hull); - resultMesh.parts.append(FBXMeshPart()); - FBXMeshPart& resultMeshPart = resultMesh.parts.last(); + resultMesh.parts.append(HFMMeshPart()); + HFMMeshPart& resultMeshPart = resultMesh.parts.last(); int hullIndexStart = resultMesh.vertices.size(); resultMesh.vertices.reserve(hullIndexStart + hull.m_nPoints); @@ -288,17 +288,17 @@ float computeDt(uint64_t start) { return (float)(usecTimestampNow() - start) / (float)USECS_PER_SECOND; } -bool vhacd::VHACDUtil::computeVHACD(FBXGeometry& geometry, +bool vhacd::VHACDUtil::computeVHACD(HFMModel& hfmModel, VHACD::IVHACD::Parameters params, - FBXGeometry& result, + HFMModel& result, float minimumMeshSize, float maximumMeshSize) { if (_verbose) { - qDebug() << "meshes =" << geometry.meshes.size(); + qDebug() << "meshes =" << hfmModel.meshes.size(); } // count the mesh-parts int numParts = 0; - foreach (const FBXMesh& mesh, geometry.meshes) { + foreach (const HFMMesh& mesh, hfmModel.meshes) { numParts += mesh.parts.size(); } if (_verbose) { @@ -308,15 +308,15 @@ bool vhacd::VHACDUtil::computeVHACD(FBXGeometry& geometry, VHACD::IVHACD * convexifier = VHACD::CreateVHACD(); result.meshExtents.reset(); - result.meshes.append(FBXMesh()); - FBXMesh &resultMesh = result.meshes.last(); + result.meshes.append(HFMMesh()); + HFMMesh &resultMesh = result.meshes.last(); const uint32_t POINT_STRIDE = 3; const uint32_t TRIANGLE_STRIDE = 3; int meshIndex = 0; int validPartsFound = 0; - foreach (const FBXMesh& mesh, geometry.meshes) { + foreach (const HFMMesh& mesh, hfmModel.meshes) { // find duplicate points int numDupes = 0; @@ -337,7 +337,7 @@ bool vhacd::VHACDUtil::computeVHACD(FBXGeometry& geometry, // each mesh has its own transform to move it to model-space std::vector vertices; - glm::mat4 totalTransform = geometry.offset * mesh.modelTransform; + glm::mat4 totalTransform = hfmModel.offset * mesh.modelTransform; foreach (glm::vec3 vertex, mesh.vertices) { vertices.push_back(glm::vec3(totalTransform * glm::vec4(vertex, 1.0f))); } @@ -354,7 +354,7 @@ bool vhacd::VHACDUtil::computeVHACD(FBXGeometry& geometry, int partIndex = 0; std::vector triangleIndices; - foreach (const FBXMeshPart &meshPart, mesh.parts) { + foreach (const HFMMeshPart &meshPart, mesh.parts) { triangleIndices.clear(); getTrianglesInMeshPart(meshPart, triangleIndices); @@ -421,7 +421,7 @@ bool vhacd::VHACDUtil::computeVHACD(FBXGeometry& geometry, triangleIndices.clear(); for (auto index : openParts) { - const FBXMeshPart &meshPart = mesh.parts[index]; + const HFMMeshPart &meshPart = mesh.parts[index]; getTrianglesInMeshPart(meshPart, triangleIndices); } diff --git a/tools/vhacd-util/src/VHACDUtil.h b/tools/vhacd-util/src/VHACDUtil.h index 35ec3ef56b..dd8f606756 100644 --- a/tools/vhacd-util/src/VHACDUtil.h +++ b/tools/vhacd-util/src/VHACDUtil.h @@ -27,16 +27,16 @@ namespace vhacd { public: void setVerbose(bool verbose) { _verbose = verbose; } - bool loadFBX(const QString filename, FBXGeometry& result); + bool loadFBX(const QString filename, HFMModel& result); - void fattenMesh(const FBXMesh& mesh, const glm::mat4& gometryOffset, FBXMesh& result) const; + void fattenMesh(const HFMMesh& mesh, const glm::mat4& modelOffset, HFMMesh& result) const; - bool computeVHACD(FBXGeometry& geometry, + bool computeVHACD(HFMModel& hfmModel, VHACD::IVHACD::Parameters params, - FBXGeometry& result, + HFMModel& result, float minimumMeshSize, float maximumMeshSize); - void getConvexResults(VHACD::IVHACD* convexifier, FBXMesh& resultMesh) const; + void getConvexResults(VHACD::IVHACD* convexifier, HFMMesh& resultMesh) const; ~VHACDUtil(); @@ -55,6 +55,6 @@ namespace vhacd { }; } -AABox getAABoxForMeshPart(const FBXMeshPart &meshPart); +AABox getAABoxForMeshPart(const HFMMeshPart &meshPart); #endif //hifi_VHACDUtil_h diff --git a/tools/vhacd-util/src/VHACDUtilApp.cpp b/tools/vhacd-util/src/VHACDUtilApp.cpp index c263dce609..3d675f8baf 100644 --- a/tools/vhacd-util/src/VHACDUtilApp.cpp +++ b/tools/vhacd-util/src/VHACDUtilApp.cpp @@ -36,7 +36,7 @@ QString formatFloat(double n) { } -bool VHACDUtilApp::writeOBJ(QString outFileName, FBXGeometry& geometry, bool outputCentimeters, int whichMeshPart) { +bool VHACDUtilApp::writeOBJ(QString outFileName, HFMModel& hfmModel, bool outputCentimeters, int whichMeshPart) { QFile file(outFileName); if (!file.open(QIODevice::WriteOnly)) { qWarning() << "unable to write to" << outFileName; @@ -56,9 +56,9 @@ bool VHACDUtilApp::writeOBJ(QString outFileName, FBXGeometry& geometry, bool out int vertexIndexOffset = 0; - foreach (const FBXMesh& mesh, geometry.meshes) { + foreach (const HFMMesh& mesh, hfmModel.meshes) { bool verticesHaveBeenOutput = false; - foreach (const FBXMeshPart &meshPart, mesh.parts) { + foreach (const HFMMeshPart &meshPart, mesh.parts) { if (whichMeshPart >= 0 && nth != (unsigned int) whichMeshPart) { nth++; continue; @@ -297,7 +297,7 @@ VHACDUtilApp::VHACDUtilApp(int argc, char* argv[]) : } // load the mesh - FBXGeometry fbx; + HFMModel fbx; auto begin = std::chrono::high_resolution_clock::now(); if (!vUtil.loadFBX(inputFilename, fbx)){ _returnCode = VHACD_RETURN_CODE_FAILURE_TO_READ; @@ -315,8 +315,8 @@ VHACDUtilApp::VHACDUtilApp(int argc, char* argv[]) : QVector infileExtensions = {"fbx", "obj"}; QString baseFileName = fileNameWithoutExtension(outputFilename, infileExtensions); int count = 0; - foreach (const FBXMesh& mesh, fbx.meshes) { - foreach (const FBXMeshPart &meshPart, mesh.parts) { + foreach (const HFMMesh& mesh, fbx.meshes) { + foreach (const HFMMeshPart &meshPart, mesh.parts) { QString outputFileName = baseFileName + "-" + QString::number(count) + ".obj"; writeOBJ(outputFileName, fbx, outputCentimeters, count); count++; @@ -358,7 +358,7 @@ VHACDUtilApp::VHACDUtilApp(int argc, char* argv[]) : } begin = std::chrono::high_resolution_clock::now(); - FBXGeometry result; + HFMModel result; bool success = vUtil.computeVHACD(fbx, params, result, minimumMeshSize, maximumMeshSize); end = std::chrono::high_resolution_clock::now(); @@ -377,9 +377,9 @@ VHACDUtilApp::VHACDUtilApp(int argc, char* argv[]) : int totalVertices = 0; int totalTriangles = 0; - foreach (const FBXMesh& mesh, result.meshes) { + foreach (const HFMMesh& mesh, result.meshes) { totalVertices += mesh.vertices.size(); - foreach (const FBXMeshPart &meshPart, mesh.parts) { + foreach (const HFMMeshPart &meshPart, mesh.parts) { totalTriangles += meshPart.triangleIndices.size() / 3; // each quad was made into two triangles totalTriangles += 2 * meshPart.quadIndices.size() / 4; @@ -398,17 +398,17 @@ VHACDUtilApp::VHACDUtilApp(int argc, char* argv[]) : } if (fattenFaces) { - FBXGeometry newFbx; - FBXMesh result; + HFMModel newFbx; + HFMMesh result; // count the mesh-parts unsigned int meshCount = 0; - foreach (const FBXMesh& mesh, fbx.meshes) { + foreach (const HFMMesh& mesh, fbx.meshes) { meshCount += mesh.parts.size(); } result.modelTransform = glm::mat4(); // Identity matrix - foreach (const FBXMesh& mesh, fbx.meshes) { + foreach (const HFMMesh& mesh, fbx.meshes) { vUtil.fattenMesh(mesh, fbx.offset, result); } diff --git a/tools/vhacd-util/src/VHACDUtilApp.h b/tools/vhacd-util/src/VHACDUtilApp.h index 0d75275802..7dadad20b3 100644 --- a/tools/vhacd-util/src/VHACDUtilApp.h +++ b/tools/vhacd-util/src/VHACDUtilApp.h @@ -28,7 +28,7 @@ public: VHACDUtilApp(int argc, char* argv[]); ~VHACDUtilApp(); - bool writeOBJ(QString outFileName, FBXGeometry& geometry, bool outputCentimeters, int whichMeshPart = -1); + bool writeOBJ(QString outFileName, HFMModel& hfmModel, bool outputCentimeters, int whichMeshPart = -1); int getReturnCode() const { return _returnCode; } diff --git a/unpublishedScripts/marketplace/record/record.js b/unpublishedScripts/marketplace/record/record.js index 8e49589e3c..4786356fee 100644 --- a/unpublishedScripts/marketplace/record/record.js +++ b/unpublishedScripts/marketplace/record/record.js @@ -395,7 +395,7 @@ Script.clearInterval(updateTimer); Messages.messageReceived.disconnect(onMessageReceived); - Messages.subscribe(HIFI_RECORDER_CHANNEL); + Messages.unsubscribe(HIFI_RECORDER_CHANNEL); } return { diff --git a/unpublishedScripts/marketplace/spectator-camera/SpectatorCamera.qml b/unpublishedScripts/marketplace/spectator-camera/SpectatorCamera.qml index 033039b87d..753771987a 100644 --- a/unpublishedScripts/marketplace/spectator-camera/SpectatorCamera.qml +++ b/unpublishedScripts/marketplace/spectator-camera/SpectatorCamera.qml @@ -16,8 +16,8 @@ import QtQuick 2.7 import QtQuick.Controls 2.2 import QtGraphicalEffects 1.0 -import "qrc:////qml//styles-uit" as HifiStylesUit -import "qrc:////qml//controls-uit" as HifiControlsUit +import stylesUit 1.0 as HifiStylesUit +import controlsUit 1.0 as HifiControlsUit import "qrc:////qml//controls" as HifiControls import "qrc:////qml//hifi" as Hifi