diff --git a/.gitignore b/.gitignore index 09cf59afe9..5a965b494c 100644 --- a/.gitignore +++ b/.gitignore @@ -14,11 +14,13 @@ Makefile # Android Studio *.iml +*.class local.properties android/gradle* android/.gradle android/**/src/main/jniLibs android/**/libs +android/**/bin android/**/src/main/res/values/libs.xml android/**/src/main/assets android/**/gradle* diff --git a/CMakeLists.txt b/CMakeLists.txt index 7802eac29c..d3ac2bff50 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -12,7 +12,7 @@ target_python() if (HIFI_ANDROID ) execute_process( - COMMAND ${HIFI_PYTHON_EXEC} ${CMAKE_CURRENT_SOURCE_DIR}/prebuild.py --android --build-root ${CMAKE_BINARY_DIR} + COMMAND ${HIFI_PYTHON_EXEC} ${CMAKE_CURRENT_SOURCE_DIR}/prebuild.py --android ${HIFI_ANDROID_APP} --build-root ${CMAKE_BINARY_DIR} WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} ) else() @@ -174,7 +174,7 @@ set_packaging_parameters() # FIXME hack to work on the proper Android toolchain if (ANDROID) - add_subdirectory(android/app) + add_subdirectory(android/apps/${HIFI_ANDROID_APP}) return() endif() diff --git a/INSTALL.md b/INSTALL.md index 985a76fe89..bcbf93eef3 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -32,8 +32,8 @@ To produce an executable installer on Windows, the following are required: 1. [InetC Plug-in for Nullsoft](http://nsis.sourceforge.net/Inetc_plug-in) - 1.0 1. Extract Zip - 1. Copy `Plugins\x86-ansi\InetC.dll` to `C:\Program Files (x86)\NSIS\Plugins\x86-ansi\` - 1. Copy `Plugins\x86-unicode\InetC.dll` to `C:\Program Files (x86)\NSIS\Plugins\x86-unicode\` + 1. Copy `Plugin\x86-ansi\InetC.dll` to `C:\Program Files (x86)\NSIS\Plugins\x86-ansi\` + 1. Copy `Plugin\x86-unicode\InetC.dll` to `C:\Program Files (x86)\NSIS\Plugins\x86-unicode\` 1. [NSISpcre Plug-in for Nullsoft](http://nsis.sourceforge.net/NSISpcre_plug-in) - 1.0 1. Extract Zip diff --git a/android/app/CMakeLists.txt b/android/apps/interface/CMakeLists.txt similarity index 61% rename from android/app/CMakeLists.txt rename to android/apps/interface/CMakeLists.txt index 19dce330c1..500d555915 100644 --- a/android/app/CMakeLists.txt +++ b/android/apps/interface/CMakeLists.txt @@ -4,10 +4,10 @@ link_hifi_libraries(shared task networking gl gpu qml image fbx hfm render-utils target_opengl() target_bullet() -set(INTERFACE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../../interface") +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") +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) @@ -15,16 +15,3 @@ target_link_libraries(native-lib android log m interface) set(GVR_ROOT "${HIFI_ANDROID_PRECOMPILED}/gvr/gvr-android-sdk-1.101.0/") target_include_directories(native-lib PRIVATE "${GVR_ROOT}/libraries/headers" "libraries/ui/src") target_link_libraries(native-lib "${GVR_ROOT}/libraries/libgvr.so" ui) - -# finished libraries -# core -> qt -# networking -> openssl, tbb -# fbx -> draco -# physics -> bullet -# entities-renderer -> polyvox - -# unfinished libraries -# image -> nvtt (doesn't look good, but can be made optional) -# script-engine -> quazip (probably not required for the android client) - - diff --git a/android/app/build.gradle b/android/apps/interface/build.gradle similarity index 83% rename from android/app/build.gradle rename to android/apps/interface/build.gradle index e3c6989baf..4163df03b7 100644 --- a/android/app/build.gradle +++ b/android/apps/interface/build.gradle @@ -1,5 +1,37 @@ import org.apache.tools.ant.taskdefs.condition.Os +buildscript { + repositories { + jcenter() + google() + } + dependencies { + classpath 'com.android.tools.build:gradle:3.2.1' + } +} + +allprojects { + repositories { + jcenter() + google() + } +} + +task renameHifiACTaskDebug() { + doLast { + def sourceFile = new File("${appDir}/build/intermediates/cmake/debug/obj/arm64-v8a/","libhifiCodec.so") + def destinationFile = new File("${appDir}/src/main/jniLibs/arm64-v8a", "libplugins_libhifiCodec.so") + copy { from sourceFile; into destinationFile.parent; rename(sourceFile.name, destinationFile.name) } + } +} +task renameHifiACTaskRelease(type: Copy) { + doLast { + def sourceFile = new File("${appDir}/build/intermediates/cmake/release/obj/arm64-v8a/","libhifiCodec.so") + def destinationFile = new File("${appDir}/src/main/jniLibs/arm64-v8a", "libplugins_libhifiCodec.so") + copy { from sourceFile; into destinationFile.parent; rename(sourceFile.name, destinationFile.name) } + } +} + apply plugin: 'com.android.application' android { @@ -19,17 +51,17 @@ android { externalNativeBuild { cmake { arguments '-DHIFI_ANDROID=1', + '-DHIFI_ANDROID_APP=interface', '-DANDROID_PLATFORM=android-24', '-DANDROID_TOOLCHAIN=clang', '-DANDROID_STL=c++_shared', - '-DQT_CMAKE_PREFIX_PATH=' + HIFI_ANDROID_PRECOMPILED + '/qt/lib/cmake', - '-DHIFI_ANDROID_PRECOMPILED=' + HIFI_ANDROID_PRECOMPILED, '-DRELEASE_NUMBER=' + RELEASE_NUMBER, '-DRELEASE_TYPE=' + RELEASE_TYPE, '-DSTABLE_BUILD=' + STABLE_BUILD, '-DDISABLE_QML=OFF', '-DDISABLE_KTX_CACHE=OFF', '-DUSE_BREAKPAD=' + (System.getenv("CMAKE_BACKTRACE_URL") && System.getenv("CMAKE_BACKTRACE_TOKEN") ? 'ON' : 'OFF'); + targets = ['native-lib'] } } signingConfigs { @@ -72,7 +104,7 @@ android { externalNativeBuild { cmake { - path '../../CMakeLists.txt' + path '../../../CMakeLists.txt' } } @@ -82,6 +114,7 @@ android { variant.externalNativeBuildTasks.each { task -> variant.mergeResources.dependsOn(task) if (Os.isFamily(Os.FAMILY_UNIX)) { + // FIXME 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() @@ -97,7 +130,7 @@ android { // Copy the compiled resources generated by the external native build copy { - from new File(projectDir, "../../interface/compiledResources") + from new File(projectDir, "../../../interface/compiledResources") into outputDir duplicatesStrategy DuplicatesStrategy.INCLUDE eachFile { details -> @@ -108,7 +141,7 @@ android { // Copy the scripts directory copy { - from new File(projectDir, "../../scripts") + from new File(projectDir, "../../../scripts") into new File(outputDir, "scripts") duplicatesStrategy DuplicatesStrategy.INCLUDE eachFile { details-> @@ -123,12 +156,6 @@ android { assetList.each { file -> out.println(file) } } } - - variant.outputs.all { - if (RELEASE_NUMBER != '0') { - outputFileName = "app_" + RELEASE_NUMBER + "_" + RELEASE_TYPE + ".apk" - } - } } } @@ -157,5 +184,6 @@ dependencies { api 'com.sothree.slidinguppanel:library:3.4.0' - implementation fileTree(include: ['*.jar'], dir: 'libs') + implementation fileTree(include: ['*.jar'], dir: '../../libraries/qt/libs') + implementation project(':qt') } diff --git a/android/app/proguard-rules.pro b/android/apps/interface/proguard-rules.pro similarity index 100% rename from android/app/proguard-rules.pro rename to android/apps/interface/proguard-rules.pro diff --git a/android/app/src/main/AndroidManifest.xml b/android/apps/interface/src/main/AndroidManifest.xml similarity index 100% rename from android/app/src/main/AndroidManifest.xml rename to android/apps/interface/src/main/AndroidManifest.xml diff --git a/android/app/src/main/assets/privacy_policy.html b/android/apps/interface/src/main/assets/privacy_policy.html similarity index 100% rename from android/app/src/main/assets/privacy_policy.html rename to android/apps/interface/src/main/assets/privacy_policy.html diff --git a/android/app/src/main/cpp/native.cpp b/android/apps/interface/src/main/cpp/native.cpp similarity index 99% rename from android/app/src/main/cpp/native.cpp rename to android/apps/interface/src/main/cpp/native.cpp index f9c7751a3e..2bb851bb85 100644 --- a/android/app/src/main/cpp/native.cpp +++ b/android/apps/interface/src/main/cpp/native.cpp @@ -149,7 +149,7 @@ void unpackAndroidAssets() { extern "C" { -JNIEXPORT void Java_io_highfidelity_hifiinterface_InterfaceActivity_nativeOnCreate(JNIEnv* env, jobject obj, jobject instance, jobject asset_mgr) { +JNIEXPORT void Java_io_highfidelity_hifiinterface_InterfaceActivity_nativeOnCreate(JNIEnv* env, jobject instance, jobject asset_mgr) { g_assetManager = AAssetManager_fromJava(env, asset_mgr); qRegisterMetaType("QAndroidJniObject"); __interfaceActivity = QAndroidJniObject(instance); diff --git a/android/app/src/main/java/io/highfidelity/gvrinterface/InterfaceActivity.java b/android/apps/interface/src/main/java/io/highfidelity/gvrinterface/InterfaceActivity.java similarity index 100% rename from android/app/src/main/java/io/highfidelity/gvrinterface/InterfaceActivity.java rename to android/apps/interface/src/main/java/io/highfidelity/gvrinterface/InterfaceActivity.java diff --git a/android/app/src/main/java/io/highfidelity/hifiinterface/BreakpadUploaderService.java b/android/apps/interface/src/main/java/io/highfidelity/hifiinterface/BreakpadUploaderService.java similarity index 100% rename from android/app/src/main/java/io/highfidelity/hifiinterface/BreakpadUploaderService.java rename to android/apps/interface/src/main/java/io/highfidelity/hifiinterface/BreakpadUploaderService.java diff --git a/android/app/src/main/java/io/highfidelity/hifiinterface/HifiUtils.java b/android/apps/interface/src/main/java/io/highfidelity/hifiinterface/HifiUtils.java similarity index 100% rename from android/app/src/main/java/io/highfidelity/hifiinterface/HifiUtils.java rename to android/apps/interface/src/main/java/io/highfidelity/hifiinterface/HifiUtils.java diff --git a/android/app/src/main/java/io/highfidelity/hifiinterface/InterfaceActivity.java b/android/apps/interface/src/main/java/io/highfidelity/hifiinterface/InterfaceActivity.java similarity index 98% rename from android/app/src/main/java/io/highfidelity/hifiinterface/InterfaceActivity.java rename to android/apps/interface/src/main/java/io/highfidelity/hifiinterface/InterfaceActivity.java index 50aea59663..b7d2157737 100644 --- a/android/app/src/main/java/io/highfidelity/hifiinterface/InterfaceActivity.java +++ b/android/apps/interface/src/main/java/io/highfidelity/hifiinterface/InterfaceActivity.java @@ -61,7 +61,7 @@ public class InterfaceActivity extends QtActivity implements WebViewFragment.OnW private HeadsetStateReceiver headsetStateReceiver; //public static native void handleHifiURL(String hifiURLString); - private native long nativeOnCreate(InterfaceActivity instance, AssetManager assetManager); + private native void nativeOnCreate(AssetManager assetManager); private native void nativeOnDestroy(); private native void nativeGotoUrl(String url); private native void nativeGoToUser(String username); @@ -114,7 +114,7 @@ public class InterfaceActivity extends QtActivity implements WebViewFragment.OnW assetManager = getResources().getAssets(); //nativeGvrApi = - nativeOnCreate(this, assetManager /*, gvrApi.getNativeGvrContext()*/); + nativeOnCreate(assetManager /*, gvrApi.getNativeGvrContext()*/); final View rootView = getWindow().getDecorView().findViewById(android.R.id.content); diff --git a/android/app/src/main/java/io/highfidelity/hifiinterface/LoginMenuActivity.java b/android/apps/interface/src/main/java/io/highfidelity/hifiinterface/LoginMenuActivity.java similarity index 100% rename from android/app/src/main/java/io/highfidelity/hifiinterface/LoginMenuActivity.java rename to android/apps/interface/src/main/java/io/highfidelity/hifiinterface/LoginMenuActivity.java diff --git a/android/app/src/main/java/io/highfidelity/hifiinterface/MainActivity.java b/android/apps/interface/src/main/java/io/highfidelity/hifiinterface/MainActivity.java similarity index 100% rename from android/app/src/main/java/io/highfidelity/hifiinterface/MainActivity.java rename to android/apps/interface/src/main/java/io/highfidelity/hifiinterface/MainActivity.java diff --git a/android/app/src/main/java/io/highfidelity/hifiinterface/PermissionChecker.java b/android/apps/interface/src/main/java/io/highfidelity/hifiinterface/PermissionChecker.java similarity index 100% rename from android/app/src/main/java/io/highfidelity/hifiinterface/PermissionChecker.java rename to android/apps/interface/src/main/java/io/highfidelity/hifiinterface/PermissionChecker.java diff --git a/android/app/src/main/java/io/highfidelity/hifiinterface/SplashActivity.java b/android/apps/interface/src/main/java/io/highfidelity/hifiinterface/SplashActivity.java similarity index 100% rename from android/app/src/main/java/io/highfidelity/hifiinterface/SplashActivity.java rename to android/apps/interface/src/main/java/io/highfidelity/hifiinterface/SplashActivity.java diff --git a/android/app/src/main/java/io/highfidelity/hifiinterface/WebViewActivity.java b/android/apps/interface/src/main/java/io/highfidelity/hifiinterface/WebViewActivity.java similarity index 100% rename from android/app/src/main/java/io/highfidelity/hifiinterface/WebViewActivity.java rename to android/apps/interface/src/main/java/io/highfidelity/hifiinterface/WebViewActivity.java diff --git a/android/app/src/main/java/io/highfidelity/hifiinterface/fragment/FriendsFragment.java b/android/apps/interface/src/main/java/io/highfidelity/hifiinterface/fragment/FriendsFragment.java similarity index 100% rename from android/app/src/main/java/io/highfidelity/hifiinterface/fragment/FriendsFragment.java rename to android/apps/interface/src/main/java/io/highfidelity/hifiinterface/fragment/FriendsFragment.java diff --git a/android/app/src/main/java/io/highfidelity/hifiinterface/fragment/HomeFragment.java b/android/apps/interface/src/main/java/io/highfidelity/hifiinterface/fragment/HomeFragment.java similarity index 100% rename from android/app/src/main/java/io/highfidelity/hifiinterface/fragment/HomeFragment.java rename to android/apps/interface/src/main/java/io/highfidelity/hifiinterface/fragment/HomeFragment.java diff --git a/android/app/src/main/java/io/highfidelity/hifiinterface/fragment/LoginFragment.java b/android/apps/interface/src/main/java/io/highfidelity/hifiinterface/fragment/LoginFragment.java similarity index 100% rename from android/app/src/main/java/io/highfidelity/hifiinterface/fragment/LoginFragment.java rename to android/apps/interface/src/main/java/io/highfidelity/hifiinterface/fragment/LoginFragment.java diff --git a/android/app/src/main/java/io/highfidelity/hifiinterface/fragment/OnBackPressedListener.java b/android/apps/interface/src/main/java/io/highfidelity/hifiinterface/fragment/OnBackPressedListener.java similarity index 100% rename from android/app/src/main/java/io/highfidelity/hifiinterface/fragment/OnBackPressedListener.java rename to android/apps/interface/src/main/java/io/highfidelity/hifiinterface/fragment/OnBackPressedListener.java diff --git a/android/app/src/main/java/io/highfidelity/hifiinterface/fragment/PolicyFragment.java b/android/apps/interface/src/main/java/io/highfidelity/hifiinterface/fragment/PolicyFragment.java similarity index 100% rename from android/app/src/main/java/io/highfidelity/hifiinterface/fragment/PolicyFragment.java rename to android/apps/interface/src/main/java/io/highfidelity/hifiinterface/fragment/PolicyFragment.java diff --git a/android/app/src/main/java/io/highfidelity/hifiinterface/fragment/SettingsFragment.java b/android/apps/interface/src/main/java/io/highfidelity/hifiinterface/fragment/SettingsFragment.java similarity index 100% rename from android/app/src/main/java/io/highfidelity/hifiinterface/fragment/SettingsFragment.java rename to android/apps/interface/src/main/java/io/highfidelity/hifiinterface/fragment/SettingsFragment.java diff --git a/android/app/src/main/java/io/highfidelity/hifiinterface/fragment/SignupFragment.java b/android/apps/interface/src/main/java/io/highfidelity/hifiinterface/fragment/SignupFragment.java similarity index 100% rename from android/app/src/main/java/io/highfidelity/hifiinterface/fragment/SignupFragment.java rename to android/apps/interface/src/main/java/io/highfidelity/hifiinterface/fragment/SignupFragment.java diff --git a/android/app/src/main/java/io/highfidelity/hifiinterface/fragment/StartMenuFragment.java b/android/apps/interface/src/main/java/io/highfidelity/hifiinterface/fragment/StartMenuFragment.java similarity index 100% rename from android/app/src/main/java/io/highfidelity/hifiinterface/fragment/StartMenuFragment.java rename to android/apps/interface/src/main/java/io/highfidelity/hifiinterface/fragment/StartMenuFragment.java diff --git a/android/app/src/main/java/io/highfidelity/hifiinterface/fragment/WebViewFragment.java b/android/apps/interface/src/main/java/io/highfidelity/hifiinterface/fragment/WebViewFragment.java similarity index 100% rename from android/app/src/main/java/io/highfidelity/hifiinterface/fragment/WebViewFragment.java rename to android/apps/interface/src/main/java/io/highfidelity/hifiinterface/fragment/WebViewFragment.java diff --git a/android/app/src/main/java/io/highfidelity/hifiinterface/provider/Callback.java b/android/apps/interface/src/main/java/io/highfidelity/hifiinterface/provider/Callback.java similarity index 100% rename from android/app/src/main/java/io/highfidelity/hifiinterface/provider/Callback.java rename to android/apps/interface/src/main/java/io/highfidelity/hifiinterface/provider/Callback.java diff --git a/android/app/src/main/java/io/highfidelity/hifiinterface/provider/DomainProvider.java b/android/apps/interface/src/main/java/io/highfidelity/hifiinterface/provider/DomainProvider.java similarity index 100% rename from android/app/src/main/java/io/highfidelity/hifiinterface/provider/DomainProvider.java rename to android/apps/interface/src/main/java/io/highfidelity/hifiinterface/provider/DomainProvider.java diff --git a/android/app/src/main/java/io/highfidelity/hifiinterface/provider/EndpointUsersProvider.java b/android/apps/interface/src/main/java/io/highfidelity/hifiinterface/provider/EndpointUsersProvider.java similarity index 100% rename from android/app/src/main/java/io/highfidelity/hifiinterface/provider/EndpointUsersProvider.java rename to android/apps/interface/src/main/java/io/highfidelity/hifiinterface/provider/EndpointUsersProvider.java diff --git a/android/app/src/main/java/io/highfidelity/hifiinterface/provider/UserStoryDomainProvider.java b/android/apps/interface/src/main/java/io/highfidelity/hifiinterface/provider/UserStoryDomainProvider.java similarity index 100% rename from android/app/src/main/java/io/highfidelity/hifiinterface/provider/UserStoryDomainProvider.java rename to android/apps/interface/src/main/java/io/highfidelity/hifiinterface/provider/UserStoryDomainProvider.java diff --git a/android/app/src/main/java/io/highfidelity/hifiinterface/provider/UsersProvider.java b/android/apps/interface/src/main/java/io/highfidelity/hifiinterface/provider/UsersProvider.java similarity index 100% rename from android/app/src/main/java/io/highfidelity/hifiinterface/provider/UsersProvider.java rename to android/apps/interface/src/main/java/io/highfidelity/hifiinterface/provider/UsersProvider.java diff --git a/android/app/src/main/java/io/highfidelity/hifiinterface/receiver/HeadsetStateReceiver.java b/android/apps/interface/src/main/java/io/highfidelity/hifiinterface/receiver/HeadsetStateReceiver.java similarity index 100% rename from android/app/src/main/java/io/highfidelity/hifiinterface/receiver/HeadsetStateReceiver.java rename to android/apps/interface/src/main/java/io/highfidelity/hifiinterface/receiver/HeadsetStateReceiver.java diff --git a/android/app/src/main/java/io/highfidelity/hifiinterface/task/DownloadProfileImageTask.java b/android/apps/interface/src/main/java/io/highfidelity/hifiinterface/task/DownloadProfileImageTask.java similarity index 100% rename from android/app/src/main/java/io/highfidelity/hifiinterface/task/DownloadProfileImageTask.java rename to android/apps/interface/src/main/java/io/highfidelity/hifiinterface/task/DownloadProfileImageTask.java diff --git a/android/app/src/main/java/io/highfidelity/hifiinterface/view/DomainAdapter.java b/android/apps/interface/src/main/java/io/highfidelity/hifiinterface/view/DomainAdapter.java similarity index 100% rename from android/app/src/main/java/io/highfidelity/hifiinterface/view/DomainAdapter.java rename to android/apps/interface/src/main/java/io/highfidelity/hifiinterface/view/DomainAdapter.java diff --git a/android/app/src/main/java/io/highfidelity/hifiinterface/view/UserListAdapter.java b/android/apps/interface/src/main/java/io/highfidelity/hifiinterface/view/UserListAdapter.java similarity index 100% rename from android/app/src/main/java/io/highfidelity/hifiinterface/view/UserListAdapter.java rename to android/apps/interface/src/main/java/io/highfidelity/hifiinterface/view/UserListAdapter.java diff --git a/android/app/src/main/res/drawable/default_profile_avatar.xml b/android/apps/interface/src/main/res/drawable/default_profile_avatar.xml similarity index 100% rename from android/app/src/main/res/drawable/default_profile_avatar.xml rename to android/apps/interface/src/main/res/drawable/default_profile_avatar.xml diff --git a/android/app/src/main/res/drawable/domain_placeholder.png b/android/apps/interface/src/main/res/drawable/domain_placeholder.png similarity index 100% rename from android/app/src/main/res/drawable/domain_placeholder.png rename to android/apps/interface/src/main/res/drawable/domain_placeholder.png diff --git a/android/app/src/main/res/drawable/encourage_login_background.jpg b/android/apps/interface/src/main/res/drawable/encourage_login_background.jpg similarity index 100% rename from android/app/src/main/res/drawable/encourage_login_background.jpg rename to android/apps/interface/src/main/res/drawable/encourage_login_background.jpg diff --git a/android/app/src/main/res/drawable/hifi_header.xml b/android/apps/interface/src/main/res/drawable/hifi_header.xml similarity index 100% rename from android/app/src/main/res/drawable/hifi_header.xml rename to android/apps/interface/src/main/res/drawable/hifi_header.xml diff --git a/android/app/src/main/res/drawable/hifi_logo_header.xml b/android/apps/interface/src/main/res/drawable/hifi_logo_header.xml similarity index 100% rename from android/app/src/main/res/drawable/hifi_logo_header.xml rename to android/apps/interface/src/main/res/drawable/hifi_logo_header.xml diff --git a/android/app/src/main/res/drawable/hifi_logo_splash.xml b/android/apps/interface/src/main/res/drawable/hifi_logo_splash.xml similarity index 100% rename from android/app/src/main/res/drawable/hifi_logo_splash.xml rename to android/apps/interface/src/main/res/drawable/hifi_logo_splash.xml diff --git a/android/app/src/main/res/drawable/ic_bookmark.xml b/android/apps/interface/src/main/res/drawable/ic_bookmark.xml similarity index 100% rename from android/app/src/main/res/drawable/ic_bookmark.xml rename to android/apps/interface/src/main/res/drawable/ic_bookmark.xml diff --git a/android/app/src/main/res/drawable/ic_clear.xml b/android/apps/interface/src/main/res/drawable/ic_clear.xml similarity index 100% rename from android/app/src/main/res/drawable/ic_clear.xml rename to android/apps/interface/src/main/res/drawable/ic_clear.xml diff --git a/android/app/src/main/res/drawable/ic_close.xml b/android/apps/interface/src/main/res/drawable/ic_close.xml similarity index 100% rename from android/app/src/main/res/drawable/ic_close.xml rename to android/apps/interface/src/main/res/drawable/ic_close.xml diff --git a/android/app/src/main/res/drawable/ic_close_black_24dp.xml b/android/apps/interface/src/main/res/drawable/ic_close_black_24dp.xml similarity index 100% rename from android/app/src/main/res/drawable/ic_close_black_24dp.xml rename to android/apps/interface/src/main/res/drawable/ic_close_black_24dp.xml diff --git a/android/app/src/main/res/drawable/ic_delete_black_24dp.xml b/android/apps/interface/src/main/res/drawable/ic_delete_black_24dp.xml similarity index 100% rename from android/app/src/main/res/drawable/ic_delete_black_24dp.xml rename to android/apps/interface/src/main/res/drawable/ic_delete_black_24dp.xml diff --git a/android/app/src/main/res/drawable/ic_expand.xml b/android/apps/interface/src/main/res/drawable/ic_expand.xml similarity index 100% rename from android/app/src/main/res/drawable/ic_expand.xml rename to android/apps/interface/src/main/res/drawable/ic_expand.xml diff --git a/android/app/src/main/res/drawable/ic_eye_noshow.xml b/android/apps/interface/src/main/res/drawable/ic_eye_noshow.xml similarity index 100% rename from android/app/src/main/res/drawable/ic_eye_noshow.xml rename to android/apps/interface/src/main/res/drawable/ic_eye_noshow.xml diff --git a/android/app/src/main/res/drawable/ic_eye_show.xml b/android/apps/interface/src/main/res/drawable/ic_eye_show.xml similarity index 100% rename from android/app/src/main/res/drawable/ic_eye_show.xml rename to android/apps/interface/src/main/res/drawable/ic_eye_show.xml diff --git a/android/app/src/main/res/drawable/ic_launcher.xml b/android/apps/interface/src/main/res/drawable/ic_launcher.xml similarity index 100% rename from android/app/src/main/res/drawable/ic_launcher.xml rename to android/apps/interface/src/main/res/drawable/ic_launcher.xml diff --git a/android/app/src/main/res/drawable/ic_menu.xml b/android/apps/interface/src/main/res/drawable/ic_menu.xml similarity index 100% rename from android/app/src/main/res/drawable/ic_menu.xml rename to android/apps/interface/src/main/res/drawable/ic_menu.xml diff --git a/android/app/src/main/res/drawable/ic_person.xml b/android/apps/interface/src/main/res/drawable/ic_person.xml similarity index 100% rename from android/app/src/main/res/drawable/ic_person.xml rename to android/apps/interface/src/main/res/drawable/ic_person.xml diff --git a/android/app/src/main/res/drawable/ic_right_arrow.xml b/android/apps/interface/src/main/res/drawable/ic_right_arrow.xml similarity index 100% rename from android/app/src/main/res/drawable/ic_right_arrow.xml rename to android/apps/interface/src/main/res/drawable/ic_right_arrow.xml diff --git a/android/app/src/main/res/drawable/ic_search.xml b/android/apps/interface/src/main/res/drawable/ic_search.xml similarity index 100% rename from android/app/src/main/res/drawable/ic_search.xml rename to android/apps/interface/src/main/res/drawable/ic_search.xml diff --git a/android/app/src/main/res/drawable/ic_share.xml b/android/apps/interface/src/main/res/drawable/ic_share.xml similarity index 100% rename from android/app/src/main/res/drawable/ic_share.xml rename to android/apps/interface/src/main/res/drawable/ic_share.xml diff --git a/android/app/src/main/res/drawable/ic_star.xml b/android/apps/interface/src/main/res/drawable/ic_star.xml similarity index 100% rename from android/app/src/main/res/drawable/ic_star.xml rename to android/apps/interface/src/main/res/drawable/ic_star.xml diff --git a/android/app/src/main/res/drawable/ic_steam.xml b/android/apps/interface/src/main/res/drawable/ic_steam.xml similarity index 100% rename from android/app/src/main/res/drawable/ic_steam.xml rename to android/apps/interface/src/main/res/drawable/ic_steam.xml diff --git a/android/app/src/main/res/drawable/ic_teleporticon.xml b/android/apps/interface/src/main/res/drawable/ic_teleporticon.xml similarity index 100% rename from android/app/src/main/res/drawable/ic_teleporticon.xml rename to android/apps/interface/src/main/res/drawable/ic_teleporticon.xml diff --git a/android/app/src/main/res/drawable/launch_screen.xml b/android/apps/interface/src/main/res/drawable/launch_screen.xml similarity index 100% rename from android/app/src/main/res/drawable/launch_screen.xml rename to android/apps/interface/src/main/res/drawable/launch_screen.xml diff --git a/android/app/src/main/res/drawable/rounded_button_color1.xml b/android/apps/interface/src/main/res/drawable/rounded_button_color1.xml similarity index 100% rename from android/app/src/main/res/drawable/rounded_button_color1.xml rename to android/apps/interface/src/main/res/drawable/rounded_button_color1.xml diff --git a/android/app/src/main/res/drawable/rounded_button_color3.xml b/android/apps/interface/src/main/res/drawable/rounded_button_color3.xml similarity index 100% rename from android/app/src/main/res/drawable/rounded_button_color3.xml rename to android/apps/interface/src/main/res/drawable/rounded_button_color3.xml diff --git a/android/app/src/main/res/drawable/rounded_button_color4.xml b/android/apps/interface/src/main/res/drawable/rounded_button_color4.xml similarity index 100% rename from android/app/src/main/res/drawable/rounded_button_color4.xml rename to android/apps/interface/src/main/res/drawable/rounded_button_color4.xml diff --git a/android/app/src/main/res/drawable/rounded_secondary_button.xml b/android/apps/interface/src/main/res/drawable/rounded_secondary_button.xml similarity index 100% rename from android/app/src/main/res/drawable/rounded_secondary_button.xml rename to android/apps/interface/src/main/res/drawable/rounded_secondary_button.xml diff --git a/android/app/src/main/res/drawable/search_bg.xml b/android/apps/interface/src/main/res/drawable/search_bg.xml similarity index 100% rename from android/app/src/main/res/drawable/search_bg.xml rename to android/apps/interface/src/main/res/drawable/search_bg.xml diff --git a/android/app/src/main/res/drawable/selector_show_password.xml b/android/apps/interface/src/main/res/drawable/selector_show_password.xml similarity index 100% rename from android/app/src/main/res/drawable/selector_show_password.xml rename to android/apps/interface/src/main/res/drawable/selector_show_password.xml diff --git a/android/app/src/main/res/font/raleway.ttf b/android/apps/interface/src/main/res/font/raleway.ttf similarity index 100% rename from android/app/src/main/res/font/raleway.ttf rename to android/apps/interface/src/main/res/font/raleway.ttf diff --git a/android/app/src/main/res/font/raleway_bold.xml b/android/apps/interface/src/main/res/font/raleway_bold.xml similarity index 100% rename from android/app/src/main/res/font/raleway_bold.xml rename to android/apps/interface/src/main/res/font/raleway_bold.xml diff --git a/android/app/src/main/res/font/raleway_italic.xml b/android/apps/interface/src/main/res/font/raleway_italic.xml similarity index 100% rename from android/app/src/main/res/font/raleway_italic.xml rename to android/apps/interface/src/main/res/font/raleway_italic.xml diff --git a/android/app/src/main/res/font/raleway_light_italic.xml b/android/apps/interface/src/main/res/font/raleway_light_italic.xml similarity index 100% rename from android/app/src/main/res/font/raleway_light_italic.xml rename to android/apps/interface/src/main/res/font/raleway_light_italic.xml diff --git a/android/app/src/main/res/font/raleway_medium.xml b/android/apps/interface/src/main/res/font/raleway_medium.xml similarity index 100% rename from android/app/src/main/res/font/raleway_medium.xml rename to android/apps/interface/src/main/res/font/raleway_medium.xml diff --git a/android/app/src/main/res/font/raleway_semibold.xml b/android/apps/interface/src/main/res/font/raleway_semibold.xml similarity index 100% rename from android/app/src/main/res/font/raleway_semibold.xml rename to android/apps/interface/src/main/res/font/raleway_semibold.xml diff --git a/android/app/src/main/res/layout/activity_encourage_login.xml b/android/apps/interface/src/main/res/layout/activity_encourage_login.xml similarity index 100% rename from android/app/src/main/res/layout/activity_encourage_login.xml rename to android/apps/interface/src/main/res/layout/activity_encourage_login.xml diff --git a/android/app/src/main/res/layout/activity_main.xml b/android/apps/interface/src/main/res/layout/activity_main.xml similarity index 100% rename from android/app/src/main/res/layout/activity_main.xml rename to android/apps/interface/src/main/res/layout/activity_main.xml diff --git a/android/app/src/main/res/layout/activity_splash.xml b/android/apps/interface/src/main/res/layout/activity_splash.xml similarity index 100% rename from android/app/src/main/res/layout/activity_splash.xml rename to android/apps/interface/src/main/res/layout/activity_splash.xml diff --git a/android/app/src/main/res/layout/activity_web_view.xml b/android/apps/interface/src/main/res/layout/activity_web_view.xml similarity index 100% rename from android/app/src/main/res/layout/activity_web_view.xml rename to android/apps/interface/src/main/res/layout/activity_web_view.xml diff --git a/android/app/src/main/res/layout/domain_view.xml b/android/apps/interface/src/main/res/layout/domain_view.xml similarity index 100% rename from android/app/src/main/res/layout/domain_view.xml rename to android/apps/interface/src/main/res/layout/domain_view.xml diff --git a/android/app/src/main/res/layout/fragment_friends.xml b/android/apps/interface/src/main/res/layout/fragment_friends.xml similarity index 100% rename from android/app/src/main/res/layout/fragment_friends.xml rename to android/apps/interface/src/main/res/layout/fragment_friends.xml diff --git a/android/app/src/main/res/layout/fragment_home.xml b/android/apps/interface/src/main/res/layout/fragment_home.xml similarity index 100% rename from android/app/src/main/res/layout/fragment_home.xml rename to android/apps/interface/src/main/res/layout/fragment_home.xml diff --git a/android/app/src/main/res/layout/fragment_login.xml b/android/apps/interface/src/main/res/layout/fragment_login.xml similarity index 100% rename from android/app/src/main/res/layout/fragment_login.xml rename to android/apps/interface/src/main/res/layout/fragment_login.xml diff --git a/android/app/src/main/res/layout/fragment_login_menu.xml b/android/apps/interface/src/main/res/layout/fragment_login_menu.xml similarity index 100% rename from android/app/src/main/res/layout/fragment_login_menu.xml rename to android/apps/interface/src/main/res/layout/fragment_login_menu.xml diff --git a/android/app/src/main/res/layout/fragment_policy.xml b/android/apps/interface/src/main/res/layout/fragment_policy.xml similarity index 100% rename from android/app/src/main/res/layout/fragment_policy.xml rename to android/apps/interface/src/main/res/layout/fragment_policy.xml diff --git a/android/app/src/main/res/layout/fragment_signup.xml b/android/apps/interface/src/main/res/layout/fragment_signup.xml similarity index 100% rename from android/app/src/main/res/layout/fragment_signup.xml rename to android/apps/interface/src/main/res/layout/fragment_signup.xml diff --git a/android/app/src/main/res/layout/fragment_web_view.xml b/android/apps/interface/src/main/res/layout/fragment_web_view.xml similarity index 100% rename from android/app/src/main/res/layout/fragment_web_view.xml rename to android/apps/interface/src/main/res/layout/fragment_web_view.xml diff --git a/android/app/src/main/res/layout/navigation_header.xml b/android/apps/interface/src/main/res/layout/navigation_header.xml similarity index 100% rename from android/app/src/main/res/layout/navigation_header.xml rename to android/apps/interface/src/main/res/layout/navigation_header.xml diff --git a/android/app/src/main/res/layout/user_item.xml b/android/apps/interface/src/main/res/layout/user_item.xml similarity index 100% rename from android/app/src/main/res/layout/user_item.xml rename to android/apps/interface/src/main/res/layout/user_item.xml diff --git a/android/app/src/main/res/layout/web_drawer.xml b/android/apps/interface/src/main/res/layout/web_drawer.xml similarity index 100% rename from android/app/src/main/res/layout/web_drawer.xml rename to android/apps/interface/src/main/res/layout/web_drawer.xml diff --git a/android/app/src/main/res/menu/menu_navigation.xml b/android/apps/interface/src/main/res/menu/menu_navigation.xml similarity index 100% rename from android/app/src/main/res/menu/menu_navigation.xml rename to android/apps/interface/src/main/res/menu/menu_navigation.xml diff --git a/android/app/src/main/res/menu/web_view_menu.xml b/android/apps/interface/src/main/res/menu/web_view_menu.xml similarity index 100% rename from android/app/src/main/res/menu/web_view_menu.xml rename to android/apps/interface/src/main/res/menu/web_view_menu.xml diff --git a/android/app/src/main/res/values-w385dp/dimens.xml b/android/apps/interface/src/main/res/values-w385dp/dimens.xml similarity index 100% rename from android/app/src/main/res/values-w385dp/dimens.xml rename to android/apps/interface/src/main/res/values-w385dp/dimens.xml diff --git a/android/app/src/main/res/values/colors.xml b/android/apps/interface/src/main/res/values/colors.xml similarity index 100% rename from android/app/src/main/res/values/colors.xml rename to android/apps/interface/src/main/res/values/colors.xml diff --git a/android/app/src/main/res/values/dimens.xml b/android/apps/interface/src/main/res/values/dimens.xml similarity index 100% rename from android/app/src/main/res/values/dimens.xml rename to android/apps/interface/src/main/res/values/dimens.xml diff --git a/android/app/src/main/res/values/font_certs.xml b/android/apps/interface/src/main/res/values/font_certs.xml similarity index 100% rename from android/app/src/main/res/values/font_certs.xml rename to android/apps/interface/src/main/res/values/font_certs.xml diff --git a/android/app/src/main/res/values/preloaded_fonts.xml b/android/apps/interface/src/main/res/values/preloaded_fonts.xml similarity index 100% rename from android/app/src/main/res/values/preloaded_fonts.xml rename to android/apps/interface/src/main/res/values/preloaded_fonts.xml diff --git a/android/app/src/main/res/values/strings.xml b/android/apps/interface/src/main/res/values/strings.xml similarity index 100% rename from android/app/src/main/res/values/strings.xml rename to android/apps/interface/src/main/res/values/strings.xml diff --git a/android/app/src/main/res/values/styles.xml b/android/apps/interface/src/main/res/values/styles.xml similarity index 100% rename from android/app/src/main/res/values/styles.xml rename to android/apps/interface/src/main/res/values/styles.xml diff --git a/android/app/src/main/res/xml/settings.xml b/android/apps/interface/src/main/res/xml/settings.xml similarity index 100% rename from android/app/src/main/res/xml/settings.xml rename to android/apps/interface/src/main/res/xml/settings.xml diff --git a/android/build.gradle b/android/build.gradle index 8d03b9f6b3..ed2ca1c47e 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -10,8 +10,8 @@ import java.util.regex.Pattern buildscript { repositories { - jcenter() google() + jcenter() } dependencies { classpath 'com.android.tools.build:gradle:3.2.1' @@ -26,8 +26,8 @@ plugins { allprojects { repositories { - jcenter() google() + jcenter() mavenCentral() } } @@ -42,378 +42,13 @@ ext { RELEASE_TYPE = project.hasProperty('RELEASE_TYPE') ? project.getProperty('RELEASE_TYPE') : 'DEV' STABLE_BUILD = project.hasProperty('STABLE_BUILD') ? project.getProperty('STABLE_BUILD') : '0' EXEC_SUFFIX = Os.isFamily(Os.FAMILY_WINDOWS) ? '.exe' : '' - QT5_DEPS = [ - 'Qt5Concurrent', - 'Qt5Core', - 'Qt5Gui', - 'Qt5Multimedia', - 'Qt5Network', - 'Qt5OpenGL', - 'Qt5Qml', - 'Qt5Quick', - 'Qt5QuickControls2', - 'Qt5QuickTemplates2', - 'Qt5Script', - 'Qt5ScriptTools', - 'Qt5Svg', - 'Qt5WebChannel', - 'Qt5WebSockets', - 'Qt5Widgets', - 'Qt5XmlPatterns', - // Android specific - 'Qt5AndroidExtras', - 'Qt5WebView', - ] } -def baseFolder = new File(HIFI_ANDROID_PRECOMPILED) -def appDir = new File(projectDir, 'app') +def appDir = new File(projectDir, 'apps/interface') def jniFolder = new File(appDir, 'src/main/jniLibs/arm64-v8a') def baseUrl = 'https://hifi-public.s3.amazonaws.com/dependencies/android/' def breakpadDumpSymsDir = new File("${appDir}/build/tmp/breakpadDumpSyms") -def qtFile='qt-5.11.1_linux_armv8-libcpp_openssl_patched.tgz' -def qtChecksum='aa449d4bfa963f3bc9a9dfe558ba29df' -def qtVersionId='3S97HBM5G5Xw9EfE52sikmgdN3t6C2MN' -if (Os.isFamily(Os.FAMILY_MAC)) { - qtFile = 'qt-5.11.1_osx_armv8-libcpp_openssl_patched.tgz' - qtChecksum='c83cc477c08a892e00c71764dca051a0' - qtVersionId='OxBD7iKINv1HbyOXmAmDrBb8AF3N.Kup' -} else if (Os.isFamily(Os.FAMILY_WINDOWS)) { - qtFile = 'qt-5.11.1_win_armv8-libcpp_openssl_patched.tgz' - qtChecksum='0582191cc55431aa4f660848a542883e' - qtVersionId='JfWM0P_Mz5Qp0LwpzhrsRwN3fqlLSFeT' -} - -def packages = [ - qt: [ - file: qtFile, - versionId: qtVersionId, - checksum: qtChecksum, - ], - bullet: [ - file: 'bullet-2.88_armv8-libcpp.tgz', - versionId: 'S8YaoED0Cl8sSb8fSV7Q2G1lQJSNDxqg', - checksum: '81642779ccb110f8c7338e8739ac38a0', - ], - draco: [ - file: 'draco_armv8-libcpp.tgz', - versionId: '3.B.uBj31kWlgND3_R2xwQzT_TP6Dz_8', - checksum: '617a80d213a5ec69fbfa21a1f2f738cd', - ], - glad: [ - file: 'glad_armv8-libcpp.zip', - versionId: 'r5Zran.JSCtvrrB6Q4KaqfIoALPw3lYY', - checksum: 'a8ee8584cf1ccd34766c7ddd9d5e5449', - ], - gvr: [ - file: 'gvrsdk_v1.101.0.tgz', - versionId: 'nqBV_j81Uc31rC7bKIrlya_Hah4v3y5r', - checksum: '57fd02baa069176ba18597a29b6b4fc7', - ], - nvtt: [ - file: 'nvtt_armv8-libcpp.zip', - versionId: 'lmkBVR5t4UF1UUwMwEirnk9H_8Nt90IO', - checksum: 'eb46d0b683e66987190ed124aabf8910', - sharedLibFolder: 'lib', - includeLibs: ['libnvtt.so', 'libnvmath.so', 'libnvimage.so', 'libnvcore.so'], - ], - openssl: [ - file: 'openssl-1.1.0g_armv8.tgz', - versionId: 'AiiPjmgUZTgNj7YV1EEx2lL47aDvvvAW', - checksum: 'cabb681fbccd79594f65fcc266e02f32', - ], - polyvox: [ - file: 'polyvox_armv8-libcpp.tgz', - versionId: 'A2kbKiNhpIenGq23bKRRzg7IMAI5BI92', - checksum: 'dba88b3a098747af4bb169e9eb9af57e', - sharedLibFolder: 'lib', - includeLibs: ['Release/libPolyVoxCore.so', 'libPolyVoxUtil.so'], - ], - tbb: [ - file: 'tbb-2018_U1_armv8_libcpp.tgz', - versionId: 'mrRbWnv4O4evcM1quRH43RJqimlRtaKB', - checksum: '20768f298f53b195e71b414b0ae240c4', - sharedLibFolder: 'lib/release', - includeLibs: ['libtbb.so', 'libtbbmalloc.so'], - ], - hifiAC: [ - baseUrl: 'http://s3.amazonaws.com/hifi-public/dependencies/', - file: 'codecSDK-android_armv8-2.0.zip', - checksum: '1cbef929675818fc64c4101b72f84a6a' - ], - etc2comp: [ - file: 'etc2comp-patched-armv8-libcpp.tgz', - versionId: 'bHhGECRAQR1vkpshBcK6ByNc1BQIM8gU', - checksum: '14b02795d774457a33bbc60e00a786bc' - ], - breakpad: [ - file: 'breakpad.tgz', - versionId: '8VrYXz7oyc.QBxNia0BVJOUBvrFO61jI', - checksum: 'ddcb23df336b08017042ba4786db1d9e', - sharedLibFolder: 'lib', - includeLibs: ['libbreakpad_client.a'] - ] -] - -def options = [ - files: new TreeSet(), - features: new HashSet(), - permissions: new HashSet() -] - -def qmlRoot = new File(HIFI_ANDROID_PRECOMPILED, 'qt') - -def captureOutput = { String command, List commandArgs -> - def result - new ByteArrayOutputStream().withStream { os -> - def execResult = exec { - executable = command - args = commandArgs - standardOutput = os - errorOutput = new ByteArrayOutputStream() - } - result = os.toString() - } - return result; -} - -def relativize = { File root, File absolute -> - def relativeURI = root.toURI().relativize(absolute.toURI()) - return new File(relativeURI.toString()) -} - -def scanQmlImports = { File qmlRootPath -> - def qmlImportCommandFile = new File(qmlRoot, 'bin/qmlimportscanner' + EXEC_SUFFIX) - if (!qmlImportCommandFile.exists()) { - throw new GradleException('Unable to find required qmlimportscanner executable at ' + qmlImportCommandFile.parent.toString()) - } - - def command = qmlImportCommandFile.absolutePath - def args = [ - '-rootPath', qmlRootPath.absolutePath, - '-importPath', "${qmlRoot.absolutePath}/qml" - ] - - def commandResult = captureOutput(command, args) - new JsonSlurper().parseText(commandResult).each { - if (!it.containsKey('path')) { - println "Warning: QML import could not be resolved in any of the import paths: ${it.name}" - return - } - def file = new File(it.path) - // Ignore non-existent files - if (!file.exists()) { - return - } - // Ignore files in the import path - if (file.canonicalPath.startsWith(qmlRootPath.canonicalPath)) { - return - } - if (file.isFile()) { - options.files.add(file) - } else { - file.eachFileRecurse(FileType.FILES, { - options.files.add(it) - }) - } - } -} - -def parseQtDependencies = { List qtLibs -> - qtLibs.each({ - def libFile = new File(qmlRoot, "lib/lib${it}.so") - options.files.add(libFile) - - def androidDeps = new File(qmlRoot, "lib/${it}-android-dependencies.xml") - if (!libFile.exists()) return - if (!androidDeps.exists()) return - - new XmlSlurper().parse(androidDeps).dependencies.lib.depends.'*'.each{ node -> - switch (node.name()) { - case 'lib': - case 'bundled': - def relativeFilename = node.@file.toString() - - // Special case, since this is handled by qmlimportscanner instead - if (relativeFilename.startsWith('qml')) - return - - def file = new File(qmlRoot, relativeFilename) - - if (!file.exists()) - return - - if (file.isFile()) { - options.files.add(file) - } else { - file.eachFileRecurse(FileType.FILES, { options.files.add(it) }) - } - break - - - case 'jar': - if (node.@bundling == "1") { - def jar = new File(qmlRoot, node.@file.toString()) - if (!jar.exists()) { - throw new GradleException('Unable to find required JAR ' + jar.path) - } - options.files.add(jar) - } - break - - case 'permission': - options.permissions.add(node.@name) - break - - case 'feature': - options.features.add(node.@name) - break - - default: - throw new GradleException('Unhandled Android Dependency node ' + node.name()) - } - } - }) -} - -def generateLibsXml = { - def libDestinationDirectory = jniFolder - def jarDestinationDirectory = new File(appDir, 'libs') - def assetDestinationDirectory = new File(appDir, 'src/main/assets/--Added-by-androiddeployqt--'); - def libsXmlFile = new File(appDir, 'src/main/res/values/libs.xml') - def libPrefix = 'lib' + File.separator - def jarPrefix = 'jar' + File.separator - - def xmlParser = new XmlParser() - def libsXmlRoot = xmlParser.parseText('') - def qtLibsNode = xmlParser.createNode(libsXmlRoot, 'array', [name: 'qt_libs']) - def bundledLibsNode = xmlParser.createNode(libsXmlRoot, 'array', [name: 'bundled_in_lib']) - def bundledAssetsNode = xmlParser.createNode(libsXmlRoot, 'array', [name: 'bundled_in_assets']) - - options.files.each { - def sourceFile = it - if (!sourceFile.exists()) { - throw new GradleException("Unable to find dependency file " + sourceFile.toString()) - } - - def relativePath = relativize( qmlRoot, sourceFile ).toString() - def destinationFile - if (relativePath.endsWith('.so')) { - def garbledFileName - if (relativePath.startsWith(libPrefix)) { - garbledFileName = relativePath.substring(libPrefix.size()) - Pattern p = ~/lib(Qt5.*).so/ - Matcher m = p.matcher(garbledFileName) - assert m.matches() - def libName = m.group(1) - xmlParser.createNode(qtLibsNode, 'item', [:]).setValue(libName) - } else { - garbledFileName = 'lib' + relativePath.replace(File.separator, '_'[0]) - xmlParser.createNode(bundledLibsNode, 'item', [:]).setValue("${garbledFileName}:${relativePath}".replace(File.separator, '/')) - } - destinationFile = new File(libDestinationDirectory, garbledFileName) - } else if (relativePath.startsWith('jar')) { - destinationFile = new File(jarDestinationDirectory, relativePath.substring(jarPrefix.size())) - } else { - xmlParser.createNode(bundledAssetsNode, 'item', [:]).setValue("--Added-by-androiddeployqt--/${relativePath}:${relativePath}".replace(File.separator, '/')) - destinationFile = new File(assetDestinationDirectory, relativePath) - } - - copy { from sourceFile; into destinationFile.parent; rename(sourceFile.name, destinationFile.name) } - assert destinationFile.exists() && destinationFile.isFile() - } - def xml = XmlUtil.serialize(libsXmlRoot) - new FileWriter(libsXmlFile).withPrintWriter { writer -> - writer.write(xml) - } -} - -task downloadDependencies { - doLast { - packages.each { entry -> - def filename = entry.value['file']; - def dependencyBaseUrl = entry.value['baseUrl'] - def url = (dependencyBaseUrl?.trim() ? dependencyBaseUrl : baseUrl) + filename; - if (entry.value.containsKey('versionId')) { - url = url + '?versionId=' + entry.value['versionId'] - } - download { - src url - dest new File(baseFolder, filename) - onlyIfNewer true - } - } - } -} - -task verifyQt(type: Verify) { def p = packages['qt']; src new File(baseFolder, p['file']); checksum p['checksum']; } -task verifyBullet(type: Verify) { def p = packages['bullet']; src new File(baseFolder, p['file']); checksum p['checksum'] } -task verifyDraco(type: Verify) { def p = packages['draco']; src new File(baseFolder, p['file']); checksum p['checksum'] } -task verifyGvr(type: Verify) { def p = packages['gvr']; src new File(baseFolder, p['file']); checksum p['checksum'] } -task verifyOpenSSL(type: Verify) { def p = packages['openssl']; src new File(baseFolder, p['file']); checksum p['checksum'] } -task verifyPolyvox(type: Verify) { def p = packages['polyvox']; src new File(baseFolder, p['file']); checksum p['checksum'] } -task verifyTBB(type: Verify) { def p = packages['tbb']; src new File(baseFolder, p['file']); checksum p['checksum'] } -task verifyHifiAC(type: Verify) { def p = packages['hifiAC']; src new File(baseFolder, p['file']); checksum p['checksum'] } -task verifyEtc2Comp(type: Verify) { def p = packages['etc2comp']; src new File(baseFolder, p['file']); checksum p['checksum'] } -task verifyBreakpad(type: Verify) { def p = packages['breakpad']; src new File(baseFolder, p['file']); checksum p['checksum'] } - -task verifyDependencyDownloads(dependsOn: downloadDependencies) { } -verifyDependencyDownloads.dependsOn verifyQt -verifyDependencyDownloads.dependsOn verifyBullet -verifyDependencyDownloads.dependsOn verifyDraco -verifyDependencyDownloads.dependsOn verifyGvr -verifyDependencyDownloads.dependsOn verifyOpenSSL -verifyDependencyDownloads.dependsOn verifyPolyvox -verifyDependencyDownloads.dependsOn verifyTBB -verifyDependencyDownloads.dependsOn verifyHifiAC -verifyDependencyDownloads.dependsOn verifyEtc2Comp -verifyDependencyDownloads.dependsOn verifyBreakpad - -task extractDependencies(dependsOn: verifyDependencyDownloads) { - doLast { - packages.each { entry -> - def folder = entry.key - def filename = entry.value['file'] - def localFile = new File(HIFI_ANDROID_PRECOMPILED, filename) - def localFolder = new File(HIFI_ANDROID_PRECOMPILED, folder) - def fileTree; - if (filename.endsWith('zip')) { - fileTree = zipTree(localFile) - } else { - fileTree = tarTree(resources.gzip(localFile)) - } - copy { - from fileTree - into localFolder - } - } - } -} - -// Copies the non Qt dependencies. Qt dependencies (primary libraries and plugins) are handled by the qtBundle task -task copyDependencies() { - doLast { - packages.each { entry -> - def packageName = entry.key - def currentPackage = entry.value; - if (currentPackage.containsKey('sharedLibFolder')) { - def localFolder = new File(baseFolder, packageName + '/' + currentPackage['sharedLibFolder']) - def tree = fileTree(localFolder); - if (currentPackage.containsKey('includeLibs')) { - currentPackage['includeLibs'].each { includeSpec -> tree.include includeSpec } - } - tree.visit { element -> - if (!element.file.isDirectory()) { - println "Copying " + element.file + " to " + jniFolder - copy { from element.file; into jniFolder } - } - } - } - } - } -} - task extractGvrBinaries() { doLast { def gvrLibFolder = new File(HIFI_ANDROID_PRECOMPILED, 'gvr/gvr-android-sdk-1.101.0/libraries'); @@ -500,13 +135,11 @@ task qtBundle { } } -task setupDependencies(dependsOn: [copyDependencies, extractGvrBinaries, qtBundle]) { } +task setupDependencies() { + // migrated to python +} task cleanDependencies(type: Delete) { - delete HIFI_ANDROID_PRECOMPILED - delete 'app/src/main/jniLibs/arm64-v8a' - delete 'app/src/main/assets/--Added-by-androiddeployqt--' - delete 'app/src/main/res/values/libs.xml' } def runBreakpadDumpSyms = { buildType -> diff --git a/android/build_android.sh b/android/build_android.sh index 189e6099a8..9c68b8969b 100755 --- a/android/build_android.sh +++ b/android/build_android.sh @@ -1,4 +1,11 @@ #!/usr/bin/env bash set -xeuo pipefail -./gradlew -PHIFI_ANDROID_PRECOMPILED=${HIFI_ANDROID_PRECOMPILED} -PVERSION_CODE=${VERSION_CODE} -PRELEASE_NUMBER=${RELEASE_NUMBER} -PRELEASE_TYPE=${RELEASE_TYPE} setupDependencies -./gradlew -PHIFI_ANDROID_PRECOMPILED=${HIFI_ANDROID_PRECOMPILED} -PVERSION_CODE=${VERSION_CODE} -PRELEASE_NUMBER=${RELEASE_NUMBER} -PRELEASE_TYPE=${RELEASE_TYPE} app:${ANDROID_BUILD_TARGET} \ No newline at end of file +./gradlew -PHIFI_ANDROID_PRECOMPILED=${HIFI_ANDROID_PRECOMPILED} -PVERSION_CODE=${VERSION_CODE} -PRELEASE_NUMBER=${RELEASE_NUMBER} -PRELEASE_TYPE=${RELEASE_TYPE} ${ANDROID_APP}:${ANDROID_BUILD_TARGET} + +# This is the actual output from gradle, which no longer attempts to muck with the naming of the APK +OUTPUT_APK=./apps/${ANDROID_APP}/build/outputs/apk/${ANDROID_BUILD_DIR}/${ANDROID_APP}-${ANDROID_BUILD_DIR}.apk +# This is the APK name requested by Jenkins +TARGET_APK=./${ANDROID_APK_NAME} +# Make sure this matches up with the new ARTIFACT_EXPRESSION for jenkins builds, which should be "android/*.apk" +cp ${OUTPUT_APK} ${TARGET_APK} + diff --git a/android/containerized_build.sh b/android/containerized_build.sh index e5ec895146..42118a8e38 100755 --- a/android/containerized_build.sh +++ b/android/containerized_build.sh @@ -5,12 +5,21 @@ DOCKER_IMAGE_NAME="hifi_androidbuild" docker build --build-arg BUILD_UID=`id -u` -t "${DOCKER_IMAGE_NAME}" -f docker/Dockerfile docker +# The Jenkins PR builds use VERSION_CODE, but the release builds use VERSION +# So make sure we use VERSION_CODE consistently +if [-z "$VERSION_CODE"]; then + export VERSION_CODE=$VERSION +fi + docker run \ --rm \ - --security-opt seccomp:unconfined \ + --security-opt seccomp:unconfined \ -v "${WORKSPACE}":/home/jenkins/hifi \ + -v /home/jenkins/.gradle:/home/jenkins/.gradle \ -e RELEASE_NUMBER \ -e RELEASE_TYPE \ + -e ANDROID_APP \ + -e ANDROID_APK_NAME \ -e ANDROID_BUILD_TARGET \ -e ANDROID_BUILD_DIR \ -e CMAKE_BACKTRACE_URL \ diff --git a/android/docker/Dockerfile b/android/docker/Dockerfile index 2a6943cbc2..c37f73cb2a 100644 --- a/android/docker/Dockerfile +++ b/android/docker/Dockerfile @@ -26,10 +26,9 @@ RUN mkdir -p "$ANDROID_HOME" "$ANDROID_SDK_HOME" && \ curl -s -S -o sdk.zip -L "${SDK_URL}" && \ unzip sdk.zip && \ rm sdk.zip && \ - yes | $ANDROID_HOME/tools/bin/sdkmanager --licenses + yes | $ANDROID_HOME/tools/bin/sdkmanager --licenses && yes | $ANDROID_HOME/tools/bin/sdkmanager --update # Install Android Build Tool and Libraries -RUN $ANDROID_HOME/tools/bin/sdkmanager --update RUN $ANDROID_HOME/tools/bin/sdkmanager "build-tools;${ANDROID_BUILD_TOOLS_VERSION}" \ "platforms;android-${ANDROID_VERSION}" \ "platform-tools" @@ -52,11 +51,14 @@ ENV PATH ${PATH}:${ANDROID_NDK_HOME} RUN apt-get -y install \ g++ \ gcc \ + sudo \ + emacs-nox \ - # --- Gradle ARG BUILD_UID=1001 RUN useradd -ms /bin/bash -u $BUILD_UID jenkins +RUN echo "jenkins ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers USER jenkins WORKDIR /home/jenkins @@ -71,22 +73,21 @@ RUN mkdir "$HIFI_BASE" && \ RUN git clone https://github.com/jherico/hifi.git && \ cd ~/hifi && \ - git checkout feature/build/gradle-wrapper - + git checkout feature/quest_move_interface WORKDIR /home/jenkins/hifi -RUN touch .test4 && \ - git fetch && git reset origin/feature/build/gradle-wrapper --hard +RUN touch .test6 && \ + git fetch && git reset origin/feature/quest_move_interface --hard RUN mkdir build # Pre-cache the vcpkg managed dependencies WORKDIR /home/jenkins/hifi/build -RUN python3 ../prebuild.py --build-root `pwd` --android +RUN python3 ../prebuild.py --build-root `pwd` --android interface # Pre-cache the gradle dependencies WORKDIR /home/jenkins/hifi/android RUN ./gradlew -m tasks -PHIFI_ANDROID_PRECOMPILED=$HIFI_ANDROID_PRECOMPILED -RUN ./gradlew extractDependencies -PHIFI_ANDROID_PRECOMPILED=$HIFI_ANDROID_PRECOMPILED +#RUN ./gradlew extractDependencies -PHIFI_ANDROID_PRECOMPILED=$HIFI_ANDROID_PRECOMPILED diff --git a/android/docker/update.txt b/android/docker/update.txt new file mode 100644 index 0000000000..a12c215a06 --- /dev/null +++ b/android/docker/update.txt @@ -0,0 +1,13 @@ +git fetch +git checkout feature/quest_move_interface +export VERSION_CODE=1 +export RELEASE_NUMBER=1 +export RELEASE_TYPE=DEV +export ANDROID_APP=interface +touch ~/.gradle/gradle.properties +echo HIFI_ANDROID_KEYSTORE=/home/jenkins/keystore.jks > ~/.gradle/gradle.properties +echo HIFI_ANDROID_KEYSTORE_PASSWORD=password >> ~/.gradle/gradle.properties +echo HIFI_ANDROID_KEY_ALIAS=key0 >> ~/.gradle/gradle.properties +echo HIFI_ANDROID_KEY_PASSWORD=password >> ~/.gradle/gradle.properties +./build_android.sh +cp ./apps/${ANDROID_APP}/build/outputs/apk/release/${ANDROID_APP}-release.apk ${ANDROID_APP}.apk \ No newline at end of file diff --git a/android/libraries/qt/build.gradle b/android/libraries/qt/build.gradle new file mode 100644 index 0000000000..e6141f4cdf --- /dev/null +++ b/android/libraries/qt/build.gradle @@ -0,0 +1,22 @@ +apply plugin: 'com.android.library' + +android { + compileSdkVersion 28 + + defaultConfig { + minSdkVersion 24 + targetSdkVersion 28 + versionCode 1 + versionName "1.0" + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } +} + +dependencies { + implementation fileTree(dir: 'libs', include: ['*.jar']) + api 'com.google.guava:guava:23.0' +} diff --git a/android/libraries/qt/src/main/AndroidManifest.xml b/android/libraries/qt/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..c6638c09e8 --- /dev/null +++ b/android/libraries/qt/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + diff --git a/android/libraries/qt/src/main/java/io/highfidelity/utils/HifiUtils.java b/android/libraries/qt/src/main/java/io/highfidelity/utils/HifiUtils.java new file mode 100644 index 0000000000..e8e9f04d9f --- /dev/null +++ b/android/libraries/qt/src/main/java/io/highfidelity/utils/HifiUtils.java @@ -0,0 +1,69 @@ + +package io.highfidelity.utils; + +import android.content.res.AssetManager; + +import com.google.common.io.ByteStreams; +import com.google.common.io.Files; + +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.util.LinkedList; + +public class HifiUtils { + + private static LinkedList readAssetLines(AssetManager assetManager, String asset) throws IOException { + LinkedList assets = new LinkedList<>(); + InputStream is = assetManager.open(asset); + BufferedReader in = new BufferedReader(new InputStreamReader(is, "UTF-8")); + String line; + while ((line=in.readLine()) != null) { + assets.add(line); + } + in.close(); + return assets; + } + + private static void copyAsset(AssetManager assetManager, String asset, String destFileName) throws IOException { + try (InputStream is = assetManager.open(asset)) { + try (OutputStream os = Files.asByteSink(new File(destFileName)).openStream()) { + ByteStreams.copy(is, os); + } + } + } + + public static void upackAssets(AssetManager assetManager, String destDir) { + try { + if (!destDir.endsWith("/")) + destDir = destDir + "/"; + LinkedList assets = readAssetLines(assetManager, "cache_assets.txt"); + String dateStamp = assets.poll(); + String dateStampFilename = destDir + dateStamp; + File dateStampFile = new File(dateStampFilename); + if (dateStampFile.exists()) { + return; + } + for (String fileToCopy : assets) { + String destFileName = destDir + fileToCopy; + { + File destFile = new File(destFileName); + File destFolder = destFile.getParentFile(); + if (!destFolder.exists()) { + destFolder.mkdirs(); + } + if (destFile.exists()) { + destFile.delete(); + } + } + copyAsset(assetManager, fileToCopy, destFileName); + } + Files.write("touch".getBytes(), dateStampFile); + } catch (IOException e){ + throw new RuntimeException(e); + } + } +} diff --git a/android/app/src/main/java/org/qtproject/qt5/android/bindings/QtActivity.java b/android/libraries/qt/src/main/java/org/qtproject/qt5/android/bindings/QtActivity.java similarity index 100% rename from android/app/src/main/java/org/qtproject/qt5/android/bindings/QtActivity.java rename to android/libraries/qt/src/main/java/org/qtproject/qt5/android/bindings/QtActivity.java diff --git a/android/app/src/main/java/org/qtproject/qt5/android/bindings/QtActivityLoader.java b/android/libraries/qt/src/main/java/org/qtproject/qt5/android/bindings/QtActivityLoader.java similarity index 100% rename from android/app/src/main/java/org/qtproject/qt5/android/bindings/QtActivityLoader.java rename to android/libraries/qt/src/main/java/org/qtproject/qt5/android/bindings/QtActivityLoader.java diff --git a/android/app/src/main/java/org/qtproject/qt5/android/bindings/QtApplication.java b/android/libraries/qt/src/main/java/org/qtproject/qt5/android/bindings/QtApplication.java similarity index 100% rename from android/app/src/main/java/org/qtproject/qt5/android/bindings/QtApplication.java rename to android/libraries/qt/src/main/java/org/qtproject/qt5/android/bindings/QtApplication.java diff --git a/android/settings.gradle b/android/settings.gradle index e7b4def49c..40b5eb44bf 100644 --- a/android/settings.gradle +++ b/android/settings.gradle @@ -1 +1,5 @@ -include ':app' +include ':qt' +project(':qt').projectDir = new File(settingsDir, 'libraries/qt') + +include ':interface' +project(':interface').projectDir = new File(settingsDir, 'apps/interface') diff --git a/assignment-client/src/Agent.cpp b/assignment-client/src/Agent.cpp index cc2973f61d..5c644cb132 100644 --- a/assignment-client/src/Agent.cpp +++ b/assignment-client/src/Agent.cpp @@ -196,7 +196,8 @@ void Agent::run() { connect(nodeList.data(), &LimitedNodeList::nodeKilled, this, &Agent::nodeKilled); nodeList->addSetOfNodeTypesToNodeInterestSet({ - NodeType::AudioMixer, NodeType::AvatarMixer, NodeType::EntityServer, NodeType::MessagesMixer, NodeType::AssetServer + NodeType::AudioMixer, NodeType::AvatarMixer, NodeType::EntityServer, + NodeType::MessagesMixer, NodeType::AssetServer, NodeType::EntityScriptServer }); } @@ -505,16 +506,6 @@ void Agent::executeScript() { DependencyManager::set(_entityViewer.getTree()); - // Agents should run at 45hz - static const int AVATAR_DATA_HZ = 45; - static const int AVATAR_DATA_IN_MSECS = MSECS_PER_SECOND / AVATAR_DATA_HZ; - QTimer* avatarDataTimer = new QTimer(this); - connect(avatarDataTimer, &QTimer::timeout, this, &Agent::processAgentAvatar); - avatarDataTimer->setSingleShot(false); - avatarDataTimer->setInterval(AVATAR_DATA_IN_MSECS); - avatarDataTimer->setTimerType(Qt::PreciseTimer); - avatarDataTimer->start(); - _scriptEngine->run(); Frame::clearFrameHandler(AUDIO_FRAME_TYPE); @@ -528,8 +519,6 @@ void Agent::executeScript() { recordingInterface->stopRecording(); } - avatarDataTimer->stop(); - setIsAvatar(false); // will stop timers for sending identity packets } @@ -584,20 +573,16 @@ void Agent::setIsAvatar(bool isAvatar) { auto scriptableAvatar = DependencyManager::get(); if (_isAvatar) { - if (!_avatarIdentityTimer) { + if (!_avatarQueryTimer) { // set up the avatar timers - _avatarIdentityTimer = new QTimer(this); _avatarQueryTimer = new QTimer(this); // connect our slot - connect(_avatarIdentityTimer, &QTimer::timeout, this, &Agent::sendAvatarIdentityPacket); connect(_avatarQueryTimer, &QTimer::timeout, this, &Agent::queryAvatars); - static const int AVATAR_IDENTITY_PACKET_SEND_INTERVAL_MSECS = 1000; static const int AVATAR_VIEW_PACKET_SEND_INTERVAL_MSECS = 1000; - // start the timers - _avatarIdentityTimer->start(AVATAR_IDENTITY_PACKET_SEND_INTERVAL_MSECS); // FIXME - we shouldn't really need to constantly send identity packets + // start the timer _avatarQueryTimer->start(AVATAR_VIEW_PACKET_SEND_INTERVAL_MSECS); connect(_scriptEngine.data(), &ScriptEngine::update, @@ -609,11 +594,7 @@ void Agent::setIsAvatar(bool isAvatar) { _entityEditSender.setMyAvatar(scriptableAvatar.data()); } else { - if (_avatarIdentityTimer) { - _avatarIdentityTimer->stop(); - delete _avatarIdentityTimer; - _avatarIdentityTimer = nullptr; - + if (_avatarQueryTimer) { _avatarQueryTimer->stop(); delete _avatarQueryTimer; _avatarQueryTimer = nullptr; @@ -646,14 +627,6 @@ void Agent::setIsAvatar(bool isAvatar) { } } -void Agent::sendAvatarIdentityPacket() { - if (_isAvatar) { - auto scriptedAvatar = DependencyManager::get(); - scriptedAvatar->markIdentityDataChanged(); - scriptedAvatar->sendIdentityPacket(); - } -} - void Agent::queryAvatars() { auto scriptedAvatar = DependencyManager::get(); @@ -681,44 +654,6 @@ void Agent::queryAvatars() { { NodeType::AvatarMixer }); } -void Agent::processAgentAvatar() { - if (!_scriptEngine->isFinished() && _isAvatar) { - auto scriptedAvatar = DependencyManager::get(); - - AvatarData::AvatarDataDetail dataDetail = (randFloat() < AVATAR_SEND_FULL_UPDATE_RATIO) ? AvatarData::SendAllData : AvatarData::CullSmallData; - QByteArray avatarByteArray = scriptedAvatar->toByteArrayStateful(dataDetail); - - int maximumByteArraySize = NLPacket::maxPayloadSize(PacketType::AvatarData) - sizeof(AvatarDataSequenceNumber); - - if (avatarByteArray.size() > maximumByteArraySize) { - qWarning() << " scriptedAvatar->toByteArrayStateful() resulted in very large buffer:" << avatarByteArray.size() << "... attempt to drop facial data"; - avatarByteArray = scriptedAvatar->toByteArrayStateful(dataDetail, true); - - if (avatarByteArray.size() > maximumByteArraySize) { - qWarning() << " scriptedAvatar->toByteArrayStateful() without facial data resulted in very large buffer:" << avatarByteArray.size() << "... reduce to MinimumData"; - avatarByteArray = scriptedAvatar->toByteArrayStateful(AvatarData::MinimumData, true); - - if (avatarByteArray.size() > maximumByteArraySize) { - qWarning() << " scriptedAvatar->toByteArrayStateful() MinimumData resulted in very large buffer:" << avatarByteArray.size() << "... FAIL!!"; - return; - } - } - } - - scriptedAvatar->doneEncoding(true); - - static AvatarDataSequenceNumber sequenceNumber = 0; - auto avatarPacket = NLPacket::create(PacketType::AvatarData, avatarByteArray.size() + sizeof(sequenceNumber)); - avatarPacket->writePrimitive(sequenceNumber++); - - avatarPacket->write(avatarByteArray); - - auto nodeList = DependencyManager::get(); - - nodeList->broadcastToNodes(std::move(avatarPacket), NodeSet() << NodeType::AvatarMixer); - } -} - void Agent::encodeFrameOfZeros(QByteArray& encodedZeros) { _flushEncoder = false; static const QByteArray zeros(AudioConstants::NETWORK_FRAME_BYTES_PER_CHANNEL, 0); diff --git a/assignment-client/src/Agent.h b/assignment-client/src/Agent.h index 244f72e624..b8e7652cca 100644 --- a/assignment-client/src/Agent.h +++ b/assignment-client/src/Agent.h @@ -81,7 +81,6 @@ private slots: void nodeActivated(SharedNodePointer activatedNode); void nodeKilled(SharedNodePointer killedNode); - void processAgentAvatar(); void processAgentAvatarAudio(); private: @@ -99,7 +98,6 @@ private: void setAvatarSound(SharedSoundPointer avatarSound) { _avatarSound = avatarSound; } - void sendAvatarIdentityPacket(); void queryAvatars(); QString _scriptContents; @@ -110,7 +108,6 @@ private: bool _shouldMuteRecordingAudio { false }; int _numAvatarSoundSentBytes = 0; bool _isAvatar = false; - QTimer* _avatarIdentityTimer = nullptr; QTimer* _avatarQueryTimer = nullptr; QHash _outgoingScriptAudioSequenceNumbers; diff --git a/assignment-client/src/audio/AudioMixer.cpp b/assignment-client/src/audio/AudioMixer.cpp index 004e4ad2ea..bdec17bd8d 100644 --- a/assignment-client/src/audio/AudioMixer.cpp +++ b/assignment-client/src/audio/AudioMixer.cpp @@ -68,6 +68,13 @@ AudioMixer::AudioMixer(ReceivedMessage& message) : // hash the available codecs (on the mixer) _availableCodecs.clear(); // Make sure struct is clean auto pluginManager = DependencyManager::set(); + // Only load codec plugins; for now assume codec plugins have 'codec' in their name. + auto codecPluginFilter = [](const QJsonObject& metaData) { + QJsonValue nameValue = metaData["MetaData"]["name"]; + return nameValue.toString().contains("codec", Qt::CaseInsensitive); + }; + pluginManager->setPluginFilter(codecPluginFilter); + auto codecPlugins = pluginManager->getCodecPlugins(); for_each(codecPlugins.cbegin(), codecPlugins.cend(), [&](const CodecPluginPointer& codec) { diff --git a/assignment-client/src/avatars/AvatarMixer.cpp b/assignment-client/src/avatars/AvatarMixer.cpp index 6b90a8fbbd..500772c1b5 100644 --- a/assignment-client/src/avatars/AvatarMixer.cpp +++ b/assignment-client/src/avatars/AvatarMixer.cpp @@ -54,7 +54,6 @@ AvatarMixer::AvatarMixer(ReceivedMessage& message) : packetReceiver.registerListener(PacketType::NodeIgnoreRequest, this, "handleNodeIgnoreRequestPacket"); packetReceiver.registerListener(PacketType::RadiusIgnoreRequest, this, "handleRadiusIgnoreRequestPacket"); packetReceiver.registerListener(PacketType::RequestsDomainListData, this, "handleRequestsDomainListDataPacket"); - packetReceiver.registerListener(PacketType::AvatarIdentityRequest, this, "handleAvatarIdentityRequestPacket"); packetReceiver.registerListener(PacketType::SetAvatarTraits, this, "queueIncomingPacket"); packetReceiver.registerListener(PacketType::BulkAvatarTraitsAck, this, "queueIncomingPacket"); @@ -582,36 +581,6 @@ void AvatarMixer::handleAvatarIdentityPacket(QSharedPointer mes _handleAvatarIdentityPacketElapsedTime += (end - start); } -void AvatarMixer::handleAvatarIdentityRequestPacket(QSharedPointer message, SharedNodePointer senderNode) { - if (message->getSize() < NUM_BYTES_RFC4122_UUID) { - qCDebug(avatars) << "Malformed AvatarIdentityRequest received from" << message->getSenderSockAddr().toString(); - return; - } - - QUuid avatarID(QUuid::fromRfc4122(message->getMessage()) ); - if (!avatarID.isNull()) { - auto nodeList = DependencyManager::get(); - auto requestedNode = nodeList->nodeWithUUID(avatarID); - - if (requestedNode) { - AvatarMixerClientData* avatarClientData = static_cast(requestedNode->getLinkedData()); - if (avatarClientData) { - const AvatarData& avatarData = avatarClientData->getAvatar(); - QByteArray serializedAvatar = avatarData.identityByteArray(); - auto identityPackets = NLPacketList::create(PacketType::AvatarIdentity, QByteArray(), true, true); - identityPackets->write(serializedAvatar); - nodeList->sendPacketList(std::move(identityPackets), *senderNode); - ++_sumIdentityPackets; - } - - AvatarMixerClientData* senderData = static_cast(senderNode->getLinkedData()); - if (senderData) { - senderData->resetSentTraitData(requestedNode->getLocalID()); - } - } - } -} - void AvatarMixer::handleKillAvatarPacket(QSharedPointer message, SharedNodePointer node) { auto start = usecTimestampNow(); handleAvatarKilled(node); diff --git a/assignment-client/src/avatars/AvatarMixer.h b/assignment-client/src/avatars/AvatarMixer.h index 8ae7fc9931..764656a2d5 100644 --- a/assignment-client/src/avatars/AvatarMixer.h +++ b/assignment-client/src/avatars/AvatarMixer.h @@ -54,7 +54,6 @@ private slots: void handleRequestsDomainListDataPacket(QSharedPointer message, SharedNodePointer senderNode); void handleReplicatedPacket(QSharedPointer message); void handleReplicatedBulkAvatarPacket(QSharedPointer message); - void handleAvatarIdentityRequestPacket(QSharedPointer message, SharedNodePointer senderNode); void domainSettingsRequestComplete(); void handlePacketVersionMismatch(PacketType type, const HifiSockAddr& senderSockAddr, const QUuid& senderUUID); void start(); diff --git a/assignment-client/src/avatars/ScriptableAvatar.cpp b/assignment-client/src/avatars/ScriptableAvatar.cpp index c61e41fbbe..044ab86942 100644 --- a/assignment-client/src/avatars/ScriptableAvatar.cpp +++ b/assignment-client/src/avatars/ScriptableAvatar.cpp @@ -91,6 +91,39 @@ void ScriptableAvatar::setSkeletonModelURL(const QUrl& skeletonModelURL) { updateJointMappings(); } +int ScriptableAvatar::sendAvatarDataPacket(bool sendAll) { + using namespace std::chrono; + auto now = Clock::now(); + + int MAX_DATA_RATE_MBPS = 3; + int maxDataRateBytesPerSeconds = MAX_DATA_RATE_MBPS * BYTES_PER_KILOBYTE * KILO_PER_MEGA / BITS_IN_BYTE; + int maxDataRateBytesPerMilliseconds = maxDataRateBytesPerSeconds / MSECS_PER_SECOND; + + auto bytesSent = 0; + + if (now > _nextTraitsSendWindow) { + if (getIdentityDataChanged()) { + bytesSent += sendIdentityPacket(); + } + + bytesSent += _clientTraitsHandler->sendChangedTraitsToMixer(); + + // Compute the next send window based on how much data we sent and what + // data rate we're trying to max at. + milliseconds timeUntilNextSend { bytesSent / maxDataRateBytesPerMilliseconds }; + _nextTraitsSendWindow += timeUntilNextSend; + + // Don't let the next send window lag behind if we're not sending a lot of data. + if (_nextTraitsSendWindow < now) { + _nextTraitsSendWindow = now; + } + } + + bytesSent += AvatarData::sendAvatarDataPacket(sendAll); + + return bytesSent; +} + static AnimPose composeAnimPose(const HFMJoint& joint, const glm::quat rotation, const glm::vec3 translation) { glm::mat4 translationMat = glm::translate(translation); glm::mat4 rotationMat = glm::mat4_cast(joint.preRotation * rotation * joint.postRotation); @@ -161,7 +194,13 @@ void ScriptableAvatar::update(float deltatime) { } } - _clientTraitsHandler->sendChangedTraitsToMixer(); + quint64 now = usecTimestampNow(); + quint64 dt = now - _lastSendAvatarDataTime; + + if (dt > MIN_TIME_BETWEEN_MY_AVATAR_DATA_SENDS) { + sendAvatarDataPacket(); + _lastSendAvatarDataTime = now; + } } void ScriptableAvatar::updateJointMappings() { diff --git a/assignment-client/src/avatars/ScriptableAvatar.h b/assignment-client/src/avatars/ScriptableAvatar.h index df949f8bff..e93be897d5 100644 --- a/assignment-client/src/avatars/ScriptableAvatar.h +++ b/assignment-client/src/avatars/ScriptableAvatar.h @@ -123,6 +123,10 @@ class ScriptableAvatar : public AvatarData, public Dependency { Q_OBJECT + + using Clock = std::chrono::system_clock; + using TimePoint = Clock::time_point; + public: ScriptableAvatar(); @@ -177,6 +181,8 @@ public: virtual void setSkeletonModelURL(const QUrl& skeletonModelURL) override; + int sendAvatarDataPacket(bool sendAll = false) override; + virtual QByteArray toByteArrayStateful(AvatarDataDetail dataDetail, bool dropFaceTracking = false) override; void setHasProceduralBlinkFaceMovement(bool hasProceduralBlinkFaceMovement); @@ -228,6 +234,10 @@ private: /// Loads the joint indices, names from the FST file (if any) void updateJointMappings(); + + quint64 _lastSendAvatarDataTime { 0 }; + + TimePoint _nextTraitsSendWindow; }; #endif // hifi_ScriptableAvatar_h diff --git a/cmake/macros/FixupNitpick.cmake b/cmake/macros/FixupNitpick.cmake index 8685b27997..8477b17823 100644 --- a/cmake/macros/FixupNitpick.cmake +++ b/cmake/macros/FixupNitpick.cmake @@ -19,7 +19,7 @@ macro(fixup_nitpick) if (NOT MACDEPLOYQT_COMMAND AND (PRODUCTION_BUILD OR PR_BUILD)) message(FATAL_ERROR "Could not find macdeployqt at ${QT_DIR}/bin.\ - It is required to produce an relocatable interface application.\ + It is required to produce a relocatable nitpick application.\ Check that the environment variable QT_DIR points to your Qt installation.\ ") endif () diff --git a/domain-server/src/AssetsBackupHandler.cpp b/domain-server/src/AssetsBackupHandler.cpp index b3ba74a985..6c879aca70 100644 --- a/domain-server/src/AssetsBackupHandler.cpp +++ b/domain-server/src/AssetsBackupHandler.cpp @@ -246,6 +246,7 @@ void AssetsBackupHandler::createBackup(const QString& backupName, QuaZip& zip) { if (_assetServerEnabled && _lastMappingsRefresh.time_since_epoch().count() == 0) { qCWarning(asset_backup) << "Current mappings not yet loaded."; + _backups.emplace_back(backupName, AssetUtils::Mappings(), true); return; } diff --git a/hifi_android.py b/hifi_android.py index e3944cda9a..13c9cdccf2 100644 --- a/hifi_android.py +++ b/hifi_android.py @@ -6,6 +6,7 @@ import re import shutil import xml.etree.ElementTree as ET import functools +import zipfile print = functools.partial(print, flush=True) @@ -163,6 +164,31 @@ def copyAndroidLibs(packagePath, appPath): print("Copying {}".format(lib)) shutil.copy(sourceFile, destFile) + gvrLibFolder = os.path.join(packagePath, 'gvr/gvr-android-sdk-1.101.0/libraries') + audioSoOut = os.path.join(gvrLibFolder, 'libgvr_audio.so') + if not os.path.isfile(audioSoOut): + audioAar = os.path.join(gvrLibFolder, 'sdk-audio-1.101.0.aar') + with zipfile.ZipFile(audioAar) as z: + with z.open('jni/arm64-v8a/libgvr_audio.so') as f: + with open(audioSoOut, 'wb') as of: + shutil.copyfileobj(f, of) + + audioSoOut2 = os.path.join(jniPath, 'libgvr_audio.so') + if not os.path.isfile(audioSoOut2): + shutil.copy(audioSoOut, audioSoOut2) + + baseSoOut = os.path.join(gvrLibFolder, 'libgvr.so') + if not os.path.isfile(baseSoOut): + baseAar = os.path.join(gvrLibFolder, 'sdk-base-1.101.0.aar') + with zipfile.ZipFile(baseAar) as z: + with z.open('jni/arm64-v8a/libgvr.so') as f: + with open(baseSoOut, 'wb') as of: + shutil.copyfileobj(f, of) + + baseSoOut2 = os.path.join(jniPath, 'libgvr.so') + if not os.path.isfile(baseSoOut2): + shutil.copy(baseSoOut, baseSoOut2) + class QtPackager: def __init__(self, appPath, qtRootPath): self.appPath = appPath @@ -170,6 +196,7 @@ class QtPackager: self.jniPath = os.path.join(self.appPath, 'src/main/jniLibs/arm64-v8a') self.assetPath = os.path.join(self.appPath, 'src/main/assets') self.qtAssetPath = os.path.join(self.assetPath, '--Added-by-androiddeployqt--') + self.qtAssetCacheList = os.path.join(self.qtAssetPath, 'qt_cache_pregenerated_file_list') # Jars go into the qt library self.jarPath = os.path.realpath(os.path.join(self.appPath, '../../libraries/qt/libs')) self.xmlFile = os.path.join(self.appPath, 'src/main/res/values/libs.xml') @@ -195,7 +222,7 @@ class QtPackager: if (relativeFilename.startswith('qml')): continue filename = os.path.join(self.qtRootPath, relativeFilename) - self.files.extend(hifi_utils.recursiveFileList(filename)) + self.files.extend(hifi_utils.recursiveFileList(filename, excludeNamePattern=r"^\.")) elif item.tag == 'jar' and 'bundling' in item.attrib and item.attrib['bundling'] == "1": self.files.append(os.path.join(self.qtRootPath, item.attrib['file'])) elif item.tag == 'permission': @@ -220,7 +247,6 @@ class QtPackager: qmlImportResults = json.loads(commandResult) for item in qmlImportResults: if 'path' not in item: - print("Warning: QML import could not be resolved in any of the import paths: {}".format(item['name'])) continue path = os.path.realpath(item['path']) if not os.path.exists(path): @@ -231,7 +257,7 @@ class QtPackager: basePath = os.path.normcase(basePath) if basePath.startswith(qmlRootPath): continue - self.files.extend(hifi_utils.recursiveFileList(path)) + self.files.extend(hifi_utils.recursiveFileList(path, excludeNamePattern=r"^\.")) def processFiles(self): self.files = list(set(self.files)) @@ -244,7 +270,7 @@ class QtPackager: for sourceFile in self.files: if not os.path.isfile(sourceFile): raise RuntimeError("Unable to find dependency file " + sourceFile) - relativePath = os.path.relpath(sourceFile, self.qtRootPath) + relativePath = os.path.relpath(sourceFile, self.qtRootPath).replace('\\', '/') destinationFile = None if relativePath.endswith('.so'): garbledFileName = None @@ -257,7 +283,7 @@ class QtPackager: libName = m.group(1) ET.SubElement(qtLibsNode, 'item').text = libName else: - garbledFileName = 'lib' + relativePath.replace('\\', '_'[0]) + garbledFileName = 'lib' + relativePath.replace('/', '_'[0]) value = "{}:{}".format(garbledFileName, relativePath).replace('\\', '/') ET.SubElement(bundledLibsNode, 'item').text = value destinationFile = os.path.join(self.jniPath, garbledFileName) @@ -277,10 +303,44 @@ class QtPackager: tree = ET.ElementTree(libsXmlRoot) tree.write(self.xmlFile, 'UTF-8', True) + def generateAssetsFileList(self): + print("Implement asset file list") + # outputFilename = os.path.join(self.qtAssetPath, "qt_cache_pregenerated_file_list") + # fileList = hifi_utils.recursiveFileList(self.qtAssetPath) + # fileMap = {} + # for fileName in fileList: + # relativeFileName = os.path.relpath(fileName, self.assetPath) + # dirName, localFileName = os.path.split(relativeFileName) + # if not dirName in fileMap: + # fileMap[dirName] = [] + # fileMap[dirName].append(localFileName) + + # for dirName in fileMap: + # for localFileName in fileMap[dirName]: + # ???? + + # + # Gradle version + # + # DataOutputStream fos = new DataOutputStream(new FileOutputStream(outputFile)); + # for (Map.Entry> e: directoryContents.entrySet()) { + # def entryList = e.getValue() + # fos.writeInt(e.key.length()*2); // 2 bytes per char + # fos.writeChars(e.key); + # fos.writeInt(entryList.size()); + # for (String entry: entryList) { + # fos.writeInt(entry.length()*2); + # fos.writeChars(entry); + # } + # } + def bundle(self): - if not os.path.isfile(self.xmlFile) or True: + if not os.path.isfile(self.xmlFile): + print("Bundling Qt info into {}".format(self.xmlFile)) self.copyQtDeps() self.scanQmlImports() self.processFiles() + # if not os.path.isfile(self.qtAssetCacheList): + # self.generateAssetsFileList() diff --git a/hifi_utils.py b/hifi_utils.py index f53258d4f6..24e43dc83c 100644 --- a/hifi_utils.py +++ b/hifi_utils.py @@ -6,6 +6,7 @@ import ssl import subprocess import sys import tarfile +import re import urllib import urllib.request import zipfile @@ -23,13 +24,15 @@ def scriptRelative(*paths): return result -def recursiveFileList(startPath): +def recursiveFileList(startPath, excludeNamePattern=None ): result = [] if os.path.isfile(startPath): result.append(startPath) elif os.path.isdir(startPath): for dirName, subdirList, fileList in os.walk(startPath): for fname in fileList: + if excludeNamePattern and re.match(excludeNamePattern, fname): + continue result.append(os.path.realpath(os.path.join(startPath, dirName, fname))) result.sort() return result @@ -97,16 +100,12 @@ def downloadFile(url, hash=None, hasher=hashlib.sha512(), retries=3): else: tempFileName, headers = urllib.request.urlretrieve(url) - # for some reason the hash we get back from the downloaded file is sometimes wrong if we check it right away - # but if we examine the file later, it is correct. - time.sleep(3) downloadHash = hashFile(tempFileName, hasher) # Verify the hash if hash is not None and hash != downloadHash: print("Try {}: Downloaded file {} hash {} does not match expected hash {} for url {}".format(i + 1, tempFileName, downloadHash, hash, url)) os.remove(tempFileName) continue - return tempFileName raise RuntimeError("Downloaded file hash {} does not match expected hash {} for\n{}".format(downloadHash, hash, url)) diff --git a/hifi_vcpkg.py b/hifi_vcpkg.py index 5492109864..e062b40d86 100644 --- a/hifi_vcpkg.py +++ b/hifi_vcpkg.py @@ -85,7 +85,7 @@ endif() if self.args.android: self.triplet = 'arm64-android' - self.androidPackagePath = os.path.join(self.path, 'android') + self.androidPackagePath = os.getenv('HIFI_ANDROID_PRECOMPILED', os.path.join(self.path, 'android')) else: self.triplet = self.hostTriplet @@ -189,6 +189,18 @@ endif() #hifi_utils.downloadAndExtract(url, dest, hash) hifi_utils.downloadAndExtract(url, dest) + print("Installing additional android archives") + androidPackages = hifi_android.getPlatformPackages() + for packageName in androidPackages: + package = androidPackages[packageName] + dest = os.path.join(self.androidPackagePath, packageName) + if os.path.isdir(dest): + continue + url = hifi_android.getPackageUrl(package) + zipFile = package['file'].endswith('.zip') + print("Android archive {}".format(package['file'])) + hifi_utils.downloadAndExtract(url, dest, isZip=zipFile, hash=package['checksum'], hasher=hashlib.md5()) + def writeTag(self): print("Writing tag {} to {}".format(self.tagContents, self.tagFile)) with open(self.tagFile, 'w') as f: @@ -203,6 +215,12 @@ endif() cmakeTemplate = VcpkgRepo.CMAKE_TEMPLATE if not self.args.android: cmakeTemplate += VcpkgRepo.CMAKE_TEMPLATE_NON_ANDROID + else: + precompiled = os.path.realpath(self.androidPackagePath) + qtCmakePrefix = os.path.realpath(os.path.join(precompiled, 'qt/lib/cmake')) + cmakeTemplate += 'set(HIFI_ANDROID_PRECOMPILED "{}")\n'.format(precompiled) + cmakeTemplate += 'set(QT_CMAKE_PREFIX_PATH "{}")\n'.format(qtCmakePrefix) + cmakeConfig = cmakeTemplate.format(cmakeScript, cmakeScript, installPath, toolsPath).replace('\\', '/') with open(self.configFilePath, 'w') as f: f.write(cmakeConfig) diff --git a/interface/resources/qml/+android/Web3DOverlay.qml b/interface/resources/qml/+android/Web3DSurface.qml similarity index 100% rename from interface/resources/qml/+android/Web3DOverlay.qml rename to interface/resources/qml/+android/Web3DSurface.qml diff --git a/interface/resources/qml/LoginDialog.qml b/interface/resources/qml/LoginDialog.qml index 0f3db48925..2d5c68c0e8 100644 --- a/interface/resources/qml/LoginDialog.qml +++ b/interface/resources/qml/LoginDialog.qml @@ -54,7 +54,8 @@ FocusScope { Image { z: -10 id: loginDialogBackground - source: "LoginDialog/images/background.jpg" + fillMode: Image.PreserveAspectCrop + source: "LoginDialog/images/background.png" anchors.fill: parent } @@ -119,6 +120,6 @@ FocusScope { } Component.onCompleted: { - bodyLoader.setSource("LoginDialog/LinkAccountBody.qml", { "loginDialog": loginDialog, "root": root, "bodyLoader": bodyLoader, "linkSteam": false }); + bodyLoader.setSource("LoginDialog/LinkAccountBody.qml", { "loginDialog": loginDialog, "root": root, "bodyLoader": bodyLoader, "linkSteam": false, "linkOculus": false }); } } diff --git a/interface/resources/qml/LoginDialog/CompleteProfileBody.qml b/interface/resources/qml/LoginDialog/CompleteProfileBody.qml index 6859b7ab3d..ebc677fb00 100644 --- a/interface/resources/qml/LoginDialog/CompleteProfileBody.qml +++ b/interface/resources/qml/LoginDialog/CompleteProfileBody.qml @@ -22,13 +22,20 @@ Item { width: root.width height: root.height readonly property string termsContainerText: qsTr("By creating this user profile, you agree to High Fidelity's Terms of Service") + readonly property string termsContainerOculusText: qsTr("By signing up, you agree to High Fidelity's Terms of Service") + readonly property int textFieldHeight: 31 readonly property string fontFamily: "Raleway" readonly property int fontSize: 15 readonly property bool fontBold: true + readonly property int textFieldFontSize: 18 + readonly property var passwordImageRatio: 16 / 23 - readonly property bool withSteam: withSteam + property bool withOculus: withOculus + property bool withSteam: withSteam property string errorString: errorString + readonly property bool loginDialogPoppedUp: loginDialog.getLoginDialogPoppedUp() + QtObject { id: d readonly property int minWidth: 480 @@ -59,15 +66,20 @@ Item { Item { id: contentItem - anchors.fill: parent + width: parent.width + height: errorContainer.height + fields.height + buttons.height + additionalTextContainer.height + + termsContainer.height + anchors.top: parent.top + anchors.topMargin: root.bannerHeight + 0.25 * parent.height + anchors.left: parent.left Item { id: errorContainer - width: parent.width + width: root.bannerWidth height: loginErrorMessageTextMetrics.height anchors { - bottom: buttons.top; - bottomMargin: hifi.dimensions.contentSpacing.y; + bottom: completeProfileBody.withOculus ? fields.top : buttons.top; + bottomMargin: 1.5 * hifi.dimensions.contentSpacing.y; left: buttons.left; } TextMetrics { @@ -77,8 +89,8 @@ Item { } Text { id: loginErrorMessage; - width: root.bannerWidth color: "red"; + width: root.bannerWidth; font.family: completeProfileBody.fontFamily font.pixelSize: 18 font.bold: completeProfileBody.fontBold @@ -86,13 +98,196 @@ Item { horizontalAlignment: Text.AlignHCenter text: completeProfileBody.errorString visible: true + onTextChanged: { + mainContainer.recalculateErrorMessage(); + } + Component.onCompleted: { + mainContainer.recalculateErrorMessage(); + } } - Component.onCompleted: { - if (loginErrorMessageTextMetrics.width > root.bannerWidth && root.isTablet) { - loginErrorMessage.wrapMode = Text.WordWrap; - loginErrorMessage.verticalAlignment = Text.AlignLeft; - loginErrorMessage.horizontalAlignment = Text.AlignLeft; - errorContainer.height = 3 * loginErrorMessageTextMetrics.height; + } + + Item { + id: fields + width: root.bannerWidth + height: 3 * completeProfileBody.textFieldHeight + 2 * hifi.dimensions.contentSpacing.y + visible: completeProfileBody.withOculus + anchors { + left: parent.left + leftMargin: (parent.width - root.bannerWidth) / 2 + bottom: buttons.top + bottomMargin: hifi.dimensions.contentSpacing.y + } + + HifiControlsUit.TextField { + id: usernameField + width: root.bannerWidth + height: completeProfileBody.textFieldHeight + placeholderText: "Username" + font.pixelSize: completeProfileBody.textFieldFontSize + styleRenderType: Text.QtRendering + anchors { + top: parent.top + } + Keys.onPressed: { + if (!usernameField.visible) { + return; + } + switch (event.key) { + case Qt.Key_Tab: + event.accepted = true; + if (event.modifiers === Qt.ShiftModifier) { + passwordField.focus = true; + } else { + emailField.focus = true; + } + break; + case Qt.Key_Backtab: + event.accepted = true; + passwordField.focus = true; + break; + case Qt.Key_Enter: + case Qt.Key_Return: + event.accepted = true; + loginDialog.createAccountFromOculus(emailField.text, usernameField.text, passwordField.text); + bodyLoader.setSource("LoggingInBody.qml", { "loginDialog": loginDialog, "root": root, "bodyLoader": bodyLoader, "withSteam": completeProfileBody.withSteam, + "linkSteam": false, "withOculus": completeProfileBody.withOculus, "linkOculus": false, "createOculus": true }); + break; + } + } + onFocusChanged: { + root.text = ""; + if (focus) { + root.isPassword = false; + } + } + Component.onCompleted: { + var userID = ""; + if (completeProfileBody.withOculus) { + userID = loginDialog.oculusUserID(); + } + usernameField.text = userID; + } + } + HifiControlsUit.TextField { + id: emailField + width: root.bannerWidth + height: completeProfileBody.textFieldHeight + anchors { + top: usernameField.bottom + topMargin: hifi.dimensions.contentSpacing.y + } + placeholderText: "Email" + font.pixelSize: completeProfileBody.textFieldFontSize + styleRenderType: Text.QtRendering + activeFocusOnPress: true + Keys.onPressed: { + switch (event.key) { + case Qt.Key_Tab: + event.accepted = true; + if (event.modifiers === Qt.ShiftModifier) { + usernameField.focus = true; + } else { + passwordField.focus = true; + } + break; + case Qt.Key_Backtab: + event.accepted = true; + usernameField.focus = true; + break; + case Qt.Key_Enter: + case Qt.Key_Return: + event.accepted = true; + loginDialog.createAccountFromOculus(emailField.text, usernameField.text, passwordField.text); + bodyLoader.setSource("LoggingInBody.qml", { "loginDialog": loginDialog, "root": root, "bodyLoader": bodyLoader, "withSteam": completeProfileBody.withSteam, + "linkSteam": false, "withOculus": completeProfileBody.withOculus, "linkOculus": false, "createOculus": true }); + break; + } + } + onFocusChanged: { + root.text = ""; + if (focus) { + root.isPassword = false; + } + } + } + HifiControlsUit.TextField { + id: passwordField + width: root.bannerWidth + height: completeProfileBody.textFieldHeight + placeholderText: "Password (optional)" + font.pixelSize: completeProfileBody.textFieldFontSize + styleRenderType: Text.QtRendering + activeFocusOnPress: true + echoMode: passwordFieldMouseArea.showPassword ? TextInput.Normal : TextInput.Password + anchors { + top: emailField.bottom + topMargin: hifi.dimensions.contentSpacing.y + } + + onFocusChanged: { + root.text = ""; + root.isPassword = focus; + } + + Item { + id: showPasswordContainer + z: 10 + // width + image's rightMargin + width: showPasswordImage.width + 8 + height: parent.height + anchors { + right: parent.right + } + + Image { + id: showPasswordImage + width: passwordField.height * passwordImageRatio + height: passwordField.height * passwordImageRatio + anchors { + right: parent.right + rightMargin: 8 + top: parent.top + topMargin: passwordFieldMouseArea.showPassword ? 6 : 8 + bottom: parent.bottom + bottomMargin: passwordFieldMouseArea.showPassword ? 5 : 8 + } + source: passwordFieldMouseArea.showPassword ? "../../images/eyeClosed.svg" : "../../images/eyeOpen.svg" + MouseArea { + id: passwordFieldMouseArea + anchors.fill: parent + acceptedButtons: Qt.LeftButton + property bool showPassword: false + onClicked: { + showPassword = !showPassword; + } + } + } + } + Keys.onPressed: { + switch (event.key) { + case Qt.Key_Tab: + event.accepted = true; + if (event.modifiers === Qt.ShiftModifier) { + emailField.focus = true; + } else if (usernameField.visible) { + usernameField.focus = true; + } else { + emailField.focus = true; + } + break; + case Qt.Key_Backtab: + event.accepted = true; + emailField.focus = true; + break; + case Qt.Key_Enter: + case Qt.Key_Return: + event.accepted = true; + loginDialog.createAccountFromOculus(emailField.text, usernameField.text, passwordField.text); + bodyLoader.setSource("LoggingInBody.qml", { "loginDialog": loginDialog, "root": root, "bodyLoader": bodyLoader, "withSteam": completeProfileBody.withSteam, + "linkSteam": false, "withOculus": completeProfileBody.withOculus, "linkOculus": false, "createOculus": true }); + break; + } } } } @@ -103,7 +298,7 @@ Item { height: d.minHeightButton anchors { top: parent.top - topMargin: (parent.height - additionalTextContainer.height) / 2 - hifi.dimensions.contentSpacing.y + topMargin: (parent.height - additionalTextContainer.height + fields.height) / 2 - hifi.dimensions.contentSpacing.y left: parent.left leftMargin: (parent.width - root.bannerWidth) / 2 } @@ -123,6 +318,13 @@ Item { fontSize: completeProfileBody.fontSize fontBold: completeProfileBody.fontBold onClicked: { + if (completeProfileBody.loginDialogPoppedUp) { + var data = { + "action": "user clicked cancel on the complete profile screen" + } + UserActivityLogger.logAction("encourageLoginDialog", data); + } + bodyLoader.setSource("LinkAccountBody.qml", { "loginDialog": loginDialog, "root": root, "bodyLoader": bodyLoader }); } } @@ -135,63 +337,26 @@ Item { width: (parent.width - hifi.dimensions.contentSpacing.x) / 2 height: d.minHeightButton - text: qsTr("Create your profile") + text: completeProfileBody.withOculus ? qsTr("Sign Up") : qsTr("Create your profile") color: hifi.buttons.blue fontFamily: completeProfileBody.fontFamily fontSize: completeProfileBody.fontSize fontBold: completeProfileBody.fontBold onClicked: { + if (completeProfileBody.loginDialogPoppedUp) { + var data = { + "action": "user clicked create profile" + } + UserActivityLogger.logAction("encourageLoginDialog", data); + } loginErrorMessage.visible = false; - loginDialog.createAccountFromSteam(); - } - } - } - - Item { - id: additionalTextContainer - width: parent.width - height: additionalTextMetrics.height - anchors { - top: buttons.bottom - horizontalCenter: parent.horizontalCenter - topMargin: hifi.dimensions.contentSpacing.y - left: parent.left - } - - TextMetrics { - id: additionalTextMetrics - font: additionalText.font - text: "Already have a High Fidelity profile? Link to an existing profile here." - } - - HifiStylesUit.ShortcutText { - id: additionalText - text: "Already have a High Fidelity profile? Link to an existing profile here." - - font.family: completeProfileBody.fontFamily - font.pixelSize: completeProfileBody.fontSize - font.bold: completeProfileBody.fontBold - wrapMode: Text.NoWrap - lineHeight: 1 - lineHeightMode: Text.ProportionalHeight - horizontalAlignment: Text.AlignHCenter - linkColor: hifi.colors.blueAccent - - onLinkActivated: { - loginDialog.isLogIn = true; - bodyLoader.setSource("LinkAccountBody.qml", { "loginDialog": loginDialog, "root": root, "bodyLoader": bodyLoader, "errorString": "", "withSteam": true, "linkSteam": true }); - } - Component.onCompleted: { - if (additionalTextMetrics.width > root.bannerWidth && root.isTablet) { - additionalText.width = root.bannerWidth; - additionalText.wrapMode = Text.WordWrap; - additionalText.verticalAlignment = Text.AlignLeft; - additionalText.horizontalAlignment = Text.AlignLeft; - additionalTextContainer.height = (additionalTextMetrics.width / root.bannerWidth) * additionalTextMetrics.height; - additionalTextContainer.anchors.left = buttons.left; - } else { - additionalText.anchors.centerIn = additionalTextContainer; + if (completeProfileBody.withOculus) { + loginDialog.createAccountFromOculus(emailField.text, usernameField.text, passwordField.text); + bodyLoader.setSource("LoggingInBody.qml", { "loginDialog": loginDialog, "root": root, "bodyLoader": bodyLoader, "withSteam": completeProfileBody.withSteam, + "linkSteam": false, "withOculus": completeProfileBody.withOculus, "linkOculus": false, "createOculus": true }); + } else if (completeProfileBody.withSteam) { + loginDialog.createAccountFromSteam(); } } } @@ -202,29 +367,33 @@ Item { width: parent.width height: termsTextMetrics.height anchors { - top: additionalTextContainer.bottom + top: buttons.bottom horizontalCenter: parent.horizontalCenter - topMargin: 2 * hifi.dimensions.contentSpacing.y + topMargin: hifi.dimensions.contentSpacing.y left: parent.left } TextMetrics { id: termsTextMetrics font: termsText.font - text: completeProfileBody.termsContainerText + text: completeProfileBody.withOculus ? completeProfileBody.termsContainerOculusText : completeProfileBody.termsContainerText Component.onCompleted: { // with the link. - termsText.text = qsTr("By creating this user profile, you agree to High Fidelity's Terms of Service") + if (completeProfileBody.withOculus) { + termsText.text = qsTr("By signing up, you agree to High Fidelity's Terms of Service") + } else { + termsText.text = qsTr("By creating this user profile, you agree to High Fidelity's Terms of Service") + } } } HifiStylesUit.InfoItem { id: termsText - text: completeProfileBody.termsContainerText + text: completeProfileBody.withOculus ? completeProfileBody.termsContainerOculusText : completeProfileBody.termsContainerText font.family: completeProfileBody.fontFamily font.pixelSize: completeProfileBody.fontSize font.bold: completeProfileBody.fontBold wrapMode: Text.WordWrap - color: hifi.colors.lightGray + color: hifi.colors.white linkColor: hifi.colors.blueAccent lineHeight: 1 lineHeightMode: Text.ProportionalHeight @@ -232,7 +401,7 @@ Item { onLinkActivated: loginDialog.openUrl(link); Component.onCompleted: { - if (termsTextMetrics.width > root.bannerWidth && root.isTablet) { + if (termsTextMetrics.width > root.bannerWidth) { termsText.width = root.bannerWidth; termsText.wrapMode = Text.WordWrap; additionalText.verticalAlignment = Text.AlignLeft; @@ -245,21 +414,113 @@ Item { } } } + + Item { + id: additionalTextContainer + width: parent.width + height: additionalTextMetrics.height + anchors { + top: termsContainer.bottom + horizontalCenter: parent.horizontalCenter + topMargin: 2 * hifi.dimensions.contentSpacing.y + left: parent.left + } + + TextMetrics { + id: additionalTextMetrics + font: additionalText.font + text: "Already have a High Fidelity profile? Link to an existing profile here." + } + + HifiStylesUit.ShortcutText { + id: additionalText + text: "Already have a High Fidelity profile? Link to an existing profile here." + width: root.bannerWidth; + font.family: completeProfileBody.fontFamily + font.pixelSize: completeProfileBody.fontSize + font.bold: completeProfileBody.fontBold + wrapMode: Text.NoWrap + lineHeight: 1 + lineHeightMode: Text.ProportionalHeight + horizontalAlignment: Text.AlignHCenter + linkColor: hifi.colors.blueAccent + + onLinkActivated: { + bodyLoader.setSource("LinkAccountBody.qml", { "loginDialog": loginDialog, "root": root, "bodyLoader": bodyLoader, "errorString": "", + "withSteam": completeProfileBody.withSteam, "linkSteam": completeProfileBody.withSteam, "withOculus": completeProfileBody.withOculus, + "linkOculus": completeProfileBody.withOculus }); + } + Component.onCompleted: { + if (additionalTextMetrics.width > root.bannerWidth) { + additionalText.wrapMode = Text.WordWrap; + additionalText.verticalAlignment = Text.AlignLeft; + additionalText.horizontalAlignment = Text.AlignLeft; + additionalTextContainer.height = (additionalTextMetrics.width / root.bannerWidth) * additionalTextMetrics.height; + additionalTextContainer.anchors.left = buttons.left; + } else { + additionalText.anchors.centerIn = additionalTextContainer; + } + } + } + } + } + function recalculateErrorMessage() { + if (completeProfileBody.errorString !== "") { + loginErrorMessage.visible = true; + var errorLength = completeProfileBody.errorString.split(/\r\n|\r|\n/).length; + var errorStringEdited = completeProfileBody.errorString.replace(/[\n\r]+/g, "\n"); + loginErrorMessage.text = errorStringEdited; + if (errorLength > 1.0) { + loginErrorMessage.wrapMode = Text.WordWrap; + loginErrorMessage.verticalAlignment = Text.AlignLeft; + loginErrorMessage.horizontalAlignment = Text.AlignLeft; + errorContainer.height = errorLength * loginErrorMessageTextMetrics.height; + } else if (loginErrorMessageTextMetrics.width > root.bannerWidth) { + loginErrorMessage.wrapMode = Text.WordWrap; + loginErrorMessage.verticalAlignment = Text.AlignLeft; + loginErrorMessage.horizontalAlignment = Text.AlignLeft; + errorContainer.height = (loginErrorMessageTextMetrics.width / root.bannerWidth) * loginErrorMessageTextMetrics.height; + } else { + loginErrorMessage.wrapMode = Text.NoWrap; + loginErrorMessage.verticalAlignment = Text.AlignVCenter; + loginErrorMessage.horizontalAlignment = Text.AlignHCenter; + errorContainer.height = loginErrorMessageTextMetrics.height; + } + } } } Connections { target: loginDialog onHandleCreateCompleted: { - console.log("Create Succeeded") - - loginDialog.loginThroughSteam(); - bodyLoader.setSource("LoggingInBody.qml", { "loginDialog": loginDialog, "root": root, "bodyLoader": bodyLoader, "withSteam": true, "linkSteam": false }); + console.log("Create Succeeded"); + if (completeProfileBody.withSteam) { + if (completeProfileBody.loginDialogPoppedUp) { + var data = { + "action": "user created a profile with Steam successfully from the complete profile screen" + } + UserActivityLogger.logAction("encourageLoginDialog", data); + } + loginDialog.loginThroughSteam(); + } + bodyLoader.setSource("LoggingInBody.qml", { "loginDialog": loginDialog, "root": root, "bodyLoader": bodyLoader, "withSteam": completeProfileBody.withSteam, "linkSteam": false, + "withOculus": completeProfileBody.withOculus, "linkOculus": false }); } onHandleCreateFailed: { console.log("Create Failed: " + error); - - bodyLoader.setSource("UsernameCollisionBody.qml", { "loginDialog": loginDialog, "root": root, "bodyLoader": bodyLoader }); + if (completeProfileBody.withSteam || completeProfileBody.withOculus) { + if (completeProfileBody.loginDialogPoppedUp) { + action = completeProfileBody.withSteam ? "Steam" : "Oculus"; + var data = { + "action": "user failed to create a profile with " + action + " from the complete profile screen" + } + UserActivityLogger.logAction("encourageLoginDialog", data); + } + } + if (!completeProfileBody.withOculus) { + bodyLoader.setSource("UsernameCollisionBody.qml", { "loginDialog": loginDialog, "root": root, "bodyLoader": bodyLoader, "withSteam": completeProfileBody.withSteam, + "withOculus": completeProfileBody.withOculus }); + } } } @@ -271,5 +532,6 @@ Item { } d.resize(); root.text = ""; + usernameField.forceActiveFocus(); } } diff --git a/interface/resources/qml/LoginDialog/LinkAccountBody.qml b/interface/resources/qml/LoginDialog/LinkAccountBody.qml index 987c5b08e4..4dd05f594d 100644 --- a/interface/resources/qml/LoginDialog/LinkAccountBody.qml +++ b/interface/resources/qml/LoginDialog/LinkAccountBody.qml @@ -36,12 +36,15 @@ Item { property bool keyboardRaised: false property bool punctuationMode: false - property bool withSteam: false + property bool withSteam: withSteam property bool linkSteam: linkSteam - property bool withOculus: false + property bool withOculus: withOculus + property bool linkOculus: linkOculus property string errorString: errorString property bool lostFocus: false + readonly property bool loginDialogPoppedUp: loginDialog.getLoginDialogPoppedUp() + QtObject { id: d readonly property int minWidth: 480 @@ -68,22 +71,37 @@ Item { function login() { loginDialog.login(emailField.text, passwordField.text); - bodyLoader.setSource("LoggingInBody.qml", { "loginDialog": loginDialog, "root": root, "bodyLoader": bodyLoader, "withSteam": linkAccountBody.withSteam, "withOculus": linkAccountBody.withOculus, "linkSteam": linkAccountBody.linkSteam }); + if (linkAccountBody.loginDialogPoppedUp) { + var data; + if (linkAccountBody.linkSteam) { + data = { + "action": "user linking hifi account with Steam" + }; + } else { + data = { + "action": "user logging in" + }; + } + UserActivityLogger.logAction("encourageLoginDialog", data); + } + bodyLoader.setSource("LoggingInBody.qml", { "loginDialog": loginDialog, "root": root, "bodyLoader": bodyLoader, "withSteam": linkAccountBody.withSteam, + "withOculus": linkAccountBody.withOculus, "linkSteam": linkAccountBody.linkSteam, "linkOculus": linkAccountBody.linkOculus }); } function init() { // going to/from sign in/up dialog. - loginDialog.isLogIn = true; loginErrorMessage.text = linkAccountBody.errorString; loginErrorMessage.visible = (linkAccountBody.errorString !== ""); - loginButton.text = !linkAccountBody.linkSteam ? "Log In" : "Link Account"; + if (loginErrorMessageTextMetrics.width > emailField.width) { + loginErrorMessage.wrapMode = Text.WordWrap; + errorContainer.height = (loginErrorMessageTextMetrics.width / emailField.width) * loginErrorMessageTextMetrics.height; + } + loginButton.text = (!linkAccountBody.linkSteam && !linkAccountBody.linkOculus) ? "Log In" : "Link Account"; loginButton.color = hifi.buttons.blue; emailField.placeholderText = "Username or Email"; var savedUsername = Settings.getValue("keepMeLoggedIn/savedUsername", ""); emailField.text = keepMeLoggedInCheckbox.checked ? savedUsername === "Unknown user" ? "" : savedUsername : ""; - if (linkAccountBody.linkSteam) { - steamInfoText.anchors.top = passwordField.bottom; - keepMeLoggedInCheckbox.anchors.top = steamInfoText.bottom; + if (linkAccountBody.linkSteam || linkAccountBody.linkOculus) { loginButton.width = (passwordField.width - hifi.dimensions.contentSpacing.x) / 2; loginButton.anchors.right = emailField.right; } else { @@ -109,7 +127,7 @@ Item { id: loginContainer width: emailField.width height: errorContainer.height + emailField.height + passwordField.height + 5.5 * hifi.dimensions.contentSpacing.y + - keepMeLoggedInCheckbox.height + loginButton.height + cantAccessTextMetrics.height + continueButton.height + steamInfoTextMetrics.height + keepMeLoggedInCheckbox.height + loginButton.height + cantAccessTextMetrics.height + continueButton.height anchors { top: parent.top topMargin: root.bannerHeight + 0.25 * parent.height @@ -119,7 +137,7 @@ Item { Item { id: errorContainer - width: loginErrorMessageTextMetrics.width + width: parent.width height: loginErrorMessageTextMetrics.height anchors { bottom: emailField.top; @@ -288,13 +306,20 @@ Item { fontSize: linkAccountBody.fontSize fontBold: linkAccountBody.fontBold color: hifi.buttons.noneBorderlessWhite; - visible: linkAccountBody.linkSteam + visible: linkAccountBody.linkSteam || linkAccountBody.linkOculus anchors { top: keepMeLoggedInCheckbox.bottom topMargin: hifi.dimensions.contentSpacing.y } onClicked: { - bodyLoader.setSource("CompleteProfileBody.qml", { "loginDialog": loginDialog, "root": root, "bodyLoader": bodyLoader, "withSteam": linkAccountBody.withSteam, "errorString": "" }); + if (linkAccountBody.loginDialogPoppedUp) { + var data = { + "action": "user clicked cancel at link account screen" + }; + UserActivityLogger.logAction("encourageLoginDialog", data); + } + bodyLoader.setSource("CompleteProfileBody.qml", { "loginDialog": loginDialog, "root": root, "bodyLoader": bodyLoader, "withSteam": linkAccountBody.withSteam, + "withOculus": linkAccountBody.withOculus, "errorString": "" }); } } HifiControlsUit.Button { @@ -310,34 +335,7 @@ Item { topMargin: hifi.dimensions.contentSpacing.y } onClicked: { - linkAccountBody.login() - } - } - TextMetrics { - id: steamInfoTextMetrics - font: steamInfoText.font - text: steamInfoText.text - } - Text { - id: steamInfoText - width: root.bannerWidth - visible: linkAccountBody.linkSteam - anchors { - top: loginButton.bottom - topMargin: hifi.dimensions.contentSpacing.y - left: emailField.left - } - - font.family: linkAccountBody.fontFamily - font.pixelSize: linkAccountBody.textFieldFontSize - color: "white" - text: qsTr("Your Steam account information will not be exposed to others."); - verticalAlignment: Text.AlignVCenter - horizontalAlignment: Text.AlignHCenter - Component.onCompleted: { - if (steamInfoTextMetrics.width > root.bannerWidth) { - steamInfoText.wrapMode = Text.WordWrap; - } + linkAccountBody.login(); } } TextMetrics { @@ -348,7 +346,7 @@ Item { HifiStylesUit.ShortcutText { id: cantAccessText z: 10 - visible: !linkAccountBody.linkSteam + visible: !linkAccountBody.linkSteam && !linkAccountBody.linkOculus anchors { top: loginButton.bottom topMargin: hifi.dimensions.contentSpacing.y @@ -373,7 +371,12 @@ Item { lightboxPopup.visible = false; } lightboxPopup.visible = true; - // bodyLoader.setSource("CantAccessBody.qml", { "loginDialog": loginDialog, "root": root, "bodyLoader": bodyLoader }); + if (linkAccountBody.loginDialogPoppedUp) { + var data = { + "action": "user clicked can't access account" + }; + UserActivityLogger.logAction("encourageLoginDialog", data); + } } } HifiControlsUit.Button { @@ -394,28 +397,40 @@ Item { buttonGlyphSize: 24 buttonGlyphRightMargin: 10 onClicked: { - // if (loginDialog.isOculusStoreRunning()) { - // linkAccountBody.withOculus = true; - // loginDialog.loginThroughSteam(); - // } else + if (loginDialog.isOculusRunning()) { + linkAccountBody.withOculus = true; + loginDialog.loginThroughOculus(); + } else if (loginDialog.isSteamRunning()) { linkAccountBody.withSteam = true; loginDialog.loginThroughSteam(); } + if (linkAccountBody.loginDialogPoppedUp) { + var data; + if (linkAccountBody.withOculus) { + data = { + "action": "user clicked login through Oculus" + }; + } else if (linkAccountBody.withSteam) { + data = { + "action": "user clicked login through Steam" + }; + } + UserActivityLogger.logAction("encourageLoginDialog", data); + } bodyLoader.setSource("LoggingInBody.qml", { "loginDialog": loginDialog, "root": root, "bodyLoader": bodyLoader, - "withSteam": linkAccountBody.withSteam, "withOculus": linkAccountBody.withOculus, "linkSteam": linkAccountBody.linkSteam }); + "withSteam": linkAccountBody.withSteam, "withOculus": linkAccountBody.withOculus, "linkSteam": linkAccountBody.linkSteam, "linkOculus": linkAccountBody.linkOculus }); } Component.onCompleted: { - if (linkAccountBody.linkSteam) { + if (linkAccountBody.linkSteam || linkAccountBody.linkOculus) { continueButton.visible = false; return; } - // if (loginDialog.isOculusStoreRunning()) { - // continueButton.text = qsTr("CONTINUE WITH OCULUS"); - // continueButton.buttonGlyph = hifi.glyphs.oculus; - // } else - if (loginDialog.isSteamRunning()) { + if (loginDialog.isOculusRunning()) { + continueButton.text = qsTr("CONTINUE WITH OCULUS"); + continueButton.buttonGlyph = hifi.glyphs.oculus; + } else if (loginDialog.isSteamRunning()) { continueButton.text = qsTr("CONTINUE WITH STEAM"); continueButton.buttonGlyph = hifi.glyphs.steamSquare; } else { @@ -428,7 +443,7 @@ Item { id: signUpContainer width: loginContainer.width height: signUpTextMetrics.height - visible: !linkAccountBody.linkSteam + visible: !linkAccountBody.linkSteam && !linkAccountBody.linkOculus anchors { left: loginContainer.left top: loginContainer.bottom @@ -470,8 +485,14 @@ Item { linkColor: hifi.colors.blueAccent onLinkActivated: { Tablet.playSound(TabletEnums.ButtonClick); + if (linkAccountBody.loginDialogPoppedUp) { + var data = { + "action": "user clicked sign up button" + }; + UserActivityLogger.logAction("encourageLoginDialog", data); + } bodyLoader.setSource("SignUpBody.qml", { "loginDialog": loginDialog, "root": root, "bodyLoader": bodyLoader, - "errorString": "", "linkSteam": linkAccountBody.linkSteam }); + "errorString": "" }); } } } @@ -495,10 +516,9 @@ Item { fontFamily: linkAccountBody.fontFamily fontSize: linkAccountBody.fontSize fontBold: linkAccountBody.fontBold - visible: loginDialog.getLoginDialogPoppedUp() && !linkAccountBody.linkSteam; + visible: loginDialog.getLoginDialogPoppedUp() && !linkAccountBody.linkSteam && !linkAccountBody.linkOculus; onClicked: { - if (loginDialog.getLoginDialogPoppedUp()) { - console.log("[ENCOURAGELOGINDIALOG]: user dismissed login screen") + if (linkAccountBody.loginDialogPoppedUp) { var data = { "action": "user dismissed login screen" }; diff --git a/interface/resources/qml/LoginDialog/LoggingInBody.qml b/interface/resources/qml/LoginDialog/LoggingInBody.qml index 355f89667b..583f00583b 100644 --- a/interface/resources/qml/LoginDialog/LoggingInBody.qml +++ b/interface/resources/qml/LoginDialog/LoggingInBody.qml @@ -29,6 +29,10 @@ Item { property bool withSteam: withSteam property bool withOculus: withOculus property bool linkSteam: linkSteam + property bool linkOculus: linkOculus + property bool createOculus: createOculus + + readonly property bool loginDialogPoppedUp: loginDialog.getLoginDialogPoppedUp() QtObject { id: d @@ -62,22 +66,36 @@ Item { running: false; repeat: false; onTriggered: { - if (loginDialog.getLoginDialogPoppedUp()) { + if (loggingInBody.loginDialogPoppedUp) { loginDialog.dismissLoginDialog(); + var data = { + "action": "user logged in successfully" + }; + UserActivityLogger.logAction("encourageLoginDialog", data); } root.tryDestroy(); } } + Timer { + id: oculusSuccessTimer + interval: 500; + running: false; + repeat: false; + onTriggered: { + loginDialog.loginThroughOculus(); + init(); + } + } + function init() { // For the process of logging in. loggingInText.wrapMode = Text.NoWrap; - - if (loggingInBody.linkSteam) { + if (loggingInBody.createOculus) { + loggingInGlyph.text = hifi.glyphs.oculus; loggingInGlyph.visible = true; - loggingInText.text = "Linking to Steam"; + loggingInText.text = "Creating account with Oculus"; loggingInText.x = loggingInHeader.width/2 - loggingInTextMetrics.width/2 + loggingInGlyphTextMetrics.width/2; - loginDialog.linkSteam(); } else if (loggingInBody.withSteam) { loggingInGlyph.visible = true; loggingInText.text = "Logging in to Steam"; @@ -94,12 +112,18 @@ Item { loggingInSpinner.visible = true; } function loadingSuccess() { - loggingInSpinner.visible = false; if (loggingInBody.linkSteam) { loggingInText.text = "Linking to Steam"; + loggingInText.x = loggingInHeader.width/2 - loggingInTextMetrics.width/2 + loggingInGlyphTextMetrics.width/2; loginDialog.linkSteam(); return; + } else if (loggingInBody.linkOculus) { + loggingInText.text = "Linking to Oculus"; + loggingInText.x = loggingInHeader.width/2 - loggingInTextMetrics.width/2 + loggingInGlyphTextMetrics.width/2; + loginDialog.linkOculus(); + return; } + loggingInSpinner.visible = false; if (loggingInBody.withSteam) { // reset the flag. loggingInGlyph.visible = false; @@ -107,6 +131,12 @@ Item { loggingInText.x = 0; loggingInText.anchors.centerIn = loggingInHeader; loggedInGlyph.visible = true; + if (loggingInBody.loginDialogPoppedUp) { + var data = { + "action": "user logged in with Steam successfully" + }; + UserActivityLogger.logAction("encourageLoginDialog", data); + } } else if (loggingInBody.withOculus) { // reset the flag. loggingInGlyph.visible = false; @@ -115,8 +145,21 @@ Item { loggingInText.anchors.centerIn = loggingInHeader; loggedInGlyph.text = hifi.glyphs.oculus; loggedInGlyph.visible = true; + if (loggingInBody.loginDialogPoppedUp) { + var data = { + "action": "user logged in with Oculus successfully" + }; + UserActivityLogger.logAction("encourageLoginDialog", data); + } } else { loggingInText.text = "You are now logged in!"; + if (loggingInBody.loginDialogPoppedUp) { + var data = { + "action": "user logged in successfully" + }; + UserActivityLogger.logAction("encourageLoginDialog", data); + } + } successTimer.start(); } @@ -221,6 +264,26 @@ Item { verticalAlignment: Text.AlignVCenter; visible: false; } + HifiControlsUit.Button { + id: okButton; + width: d.minWidthButton + height: d.minHeightButton + text: qsTr("OK") + color: hifi.buttons.white + anchors { + top: loggedInGlyph.bottom + topMargin: 3 * hifi.dimensions.contentSpacing.y + left: parent.left + leftMargin: (parent.width - width) / 2; + } + onClicked: { + root.tryDestroy(); + if (loginDialog.getLoginDialogPoppedUp()) { + loginDialog.dismissLoginDialog(); + } + } + visible: false + } } } } @@ -232,14 +295,78 @@ Item { Connections { target: loginDialog + onHandleCreateCompleted: { + console.log("Create Succeeded") + if (loggingInBody.withOculus) { + if (loggingInBody.loginDialogPoppedUp) { + var data = { + "action": "user created Oculus account successfully" + }; + UserActivityLogger.logAction("encourageLoginDialog", data); + } + loggingInBody.createOculus = false; + loggingInText.text = "Account created!"; + loggingInText.x = loggingInHeader.width/2 - loggingInTextMetrics.width/2 + loggingInGlyphTextMetrics.width/2; + oculusSuccessTimer.start(); + } + } + onHandleCreateFailed: { + console.log("Create Failed: " + error); + if (loggingInBody.withOculus) { + if (loggingInBody.loginDialogPoppedUp) { + var data = { + "action": "user created Oculus account unsuccessfully" + }; + UserActivityLogger.logAction("encourageLoginDialog", data); + } + bodyLoader.setSource("CompleteProfileBody.qml", { "loginDialog": loginDialog, "root": root, "bodyLoader": bodyLoader, "withSteam": loggingInBody.withSteam, + "withOculus": loggingInBody.withOculus, "errorString": error }); + } + } onHandleLinkCompleted: { console.log("Link Succeeded"); - loggingInBody.linkSteam = false; + if (loggingInBody.linkSteam) { + loggingInBody.linkSteam = false; + if (loggingInBody.loginDialogPoppedUp) { + var data = { + "action": "user linked Steam with their hifi account credentials successfully" + }; + UserActivityLogger.logAction("encourageLoginDialog", data); + } + } else if (loggingInBody.linkOculus) { + loggingInBody.linkOculus = false; + if (loggingInBody.loginDialogPoppedUp) { + var data = { + "action": "user linked Oculus with their hifi account credentials successfully" + }; + UserActivityLogger.logAction("encourageLoginDialog", data); + } + } loggingInBody.loadingSuccess(); } onHandleLinkFailed: { console.log("Link Failed: " + error); - bodyLoader.setSource("LinkAccountBody.qml", { "loginDialog": loginDialog, "root": root, "bodyLoader": bodyLoader, "linkSteam": true, "errorString": error }); + loggingInSpinner.visible = false; + if (loggingInBody.linkOculus) { + loggingInText.text = "Oculus failed to link"; + if (loggingInBody.loginDialogPoppedUp) { + var data = { + "action": "user linked Oculus unsuccessfully" + }; + UserActivityLogger.logAction("encourageLoginDialog", data); + } + okButton.visible = true; + } else if (loggingInBody.linkSteam){ + if (loggingInBody.loginDialogPoppedUp) { + var data = { + "action": "user linked Steam unsuccessfully" + }; + UserActivityLogger.logAction("encourageLoginDialog", data); + } + } else { + bodyLoader.setSource("LinkAccountBody.qml", { "loginDialog": loginDialog, "root": root, "bodyLoader": bodyLoader, "linkSteam": loggingInBody.linkSteam, + "linkOculus": loggingInBody.linkOculus, "errorString": error }); + } } onHandleLoginCompleted: { @@ -250,21 +377,57 @@ Item { onHandleLoginFailed: { console.log("Login Failed") loggingInSpinner.visible = false; + loggingInGlyph.visible = false; var errorString = ""; - if (loggingInBody.linkSteam && loggingInBody.withSteam) { + if (loggingInBody.linkOculus && loggingInBody.withOculus) { errorString = "Username or password is incorrect."; - bodyLoader.setSource("LinkAccountBody.qml", { "loginDialog": loginDialog, "root": root, "bodyLoader": bodyLoader, "withSteam": loggingInBody.withSteam, "linkSteam": loggingInBody.linkSteam, "errorString": errorString }); + if (loggingInBody.loginDialogPoppedUp) { + var data = { + "action": "user failed to link Oculus with their hifi account credentials" + }; + UserActivityLogger.logAction("encourageLoginDialog", data); + } + bodyLoader.setSource("LinkAccountBody.qml", { "loginDialog": loginDialog, "root": root, "bodyLoader": bodyLoader, "withSteam": loggingInBody.withSteam, + "withOculus": loggingInBody.withOculus, "linkSteam": loggingInBody.linkSteam, "linkOculus": loggingInBody.linkOculus, "errorString": errorString }); + } else if (loggingInBody.linkSteam && loggingInBody.withSteam) { + errorString = "Username or password is incorrect."; + if (loggingInBody.loginDialogPoppedUp) { + var data = { + "action": "user failed to link Steam with their hifi account credentials" + }; + UserActivityLogger.logAction("encourageLoginDialog", data); + } + bodyLoader.setSource("LinkAccountBody.qml", { "loginDialog": loginDialog, "root": root, "bodyLoader": bodyLoader, "withSteam": loggingInBody.withSteam, + "withOculus": loggingInBody.withOculus, "linkSteam": loggingInBody.linkSteam, "linkOculus": loggingInBody.linkOculus, "errorString": errorString }); } else if (loggingInBody.withSteam) { - loggingInGlyph.visible = false; errorString = "Your Steam authentication has failed. Please make sure you are logged into Steam and try again."; - bodyLoader.setSource("CompleteProfileBody.qml", { "loginDialog": loginDialog, "root": root, "bodyLoader": bodyLoader, "withSteam": loggingInBody.withSteam, "errorString": errorString }); + if (loggingInBody.loginDialogPoppedUp) { + var data = { + "action": "user failed to authenticate with Steam to log in" + }; + UserActivityLogger.logAction("encourageLoginDialog", data); + } + bodyLoader.setSource("CompleteProfileBody.qml", { "loginDialog": loginDialog, "root": root, "bodyLoader": bodyLoader, "withSteam": loggingInBody.withSteam, + "withOculus": loggingInBody.withOculus, "linkSteam": loggingInBody.linkSteam, "linkOculus": loggingInBody.linkOculus, "errorString": errorString }); } else if (loggingInBody.withOculus) { - loggingInGlyph.visible = false; - errorString = "Your Oculus authentication has failed. Please make sure you are logged into Oculus and try again." - bodyLoader.setSource("LinkAccountBody.qml", { "loginDialog": loginDialog, "root": root, "bodyLoader": bodyLoader, "errorString": errorString }); - } - else { + errorString = "Your Oculus account is not connected to an existing High Fidelity account. Please create a new one." + if (loggingInBody.loginDialogPoppedUp) { + var data = { + "action": "user failed to authenticate with Oculus to log in" + }; + UserActivityLogger.logAction("encourageLoginDialog", data); + } + bodyLoader.setSource("CompleteProfileBody.qml", { "loginDialog": loginDialog, "root": root, "bodyLoader": bodyLoader, "withSteam": loggingInBody.withSteam, + "withOculus": loggingInBody.withOculus, "linkSteam": loggingInBody.linkSteam, "linkOculus": loggingInBody.linkOculus, "errorString": errorString }); + } else { errorString = "Username or password is incorrect."; + if (loggingInBody.loginDialogPoppedUp) { + var data = { + "action": "user failed at logging in" + }; + UserActivityLogger.logAction("encourageLoginDialog", data); + } + bodyLoader.setSource("LinkAccountBody.qml", { "loginDialog": loginDialog, "root": root, "bodyLoader": bodyLoader, "errorString": errorString }); } } diff --git a/interface/resources/qml/LoginDialog/SignUpBody.qml b/interface/resources/qml/LoginDialog/SignUpBody.qml index 5e0e955330..64df9089a1 100644 --- a/interface/resources/qml/LoginDialog/SignUpBody.qml +++ b/interface/resources/qml/LoginDialog/SignUpBody.qml @@ -23,6 +23,7 @@ Item { clip: true height: root.height width: root.width + readonly property string termsContainerText: qsTr("By signing up, you agree to High Fidelity's Terms of Service") property int textFieldHeight: 31 property string fontFamily: "Raleway" property int fontSize: 15 @@ -37,9 +38,10 @@ Item { onKeyboardRaisedChanged: d.resize(); property string errorString: errorString - property bool linkSteam: linkSteam property bool lostFocus: false + readonly property bool loginDialogPoppedUp: loginDialog.getLoginDialogPoppedUp() + QtObject { id: d readonly property int minWidth: 480 @@ -71,7 +73,6 @@ Item { function init() { // going to/from sign in/up dialog. - loginDialog.isLogIn = false; emailField.placeholderText = "Email"; emailField.text = ""; emailField.anchors.top = usernameField.bottom; @@ -345,7 +346,13 @@ Item { fontSize: signUpBody.fontSize fontBold: signUpBody.fontBold onClicked: { - bodyLoader.setSource("LinkAccountBody.qml", { "loginDialog": loginDialog, "root": root, "bodyLoader": bodyLoader, "linkSteam": signUpBody.linkSteam }); + if (signUpBody.loginDialogPoppedUp) { + var data = { + "action": "user clicked cancel button at sign up screen" + } + UserActivityLogger.logAction("encourageLoginDialog", data); + } + bodyLoader.setSource("LinkAccountBody.qml", { "loginDialog": loginDialog, "root": root, "bodyLoader": bodyLoader, "linkSteam": false }); } } HifiControlsUit.Button { @@ -363,19 +370,62 @@ Item { } onClicked: { + if (signUpBody.loginDialogPoppedUp) { + var data = { + "action": "user clicked sign up button" + } + UserActivityLogger.logAction("encourageLoginDialog", data); + } signUpBody.signup(); } } - } - } + Item { + id: termsContainer + width: parent.width + height: termsTextMetrics.height + anchors { + top: signUpButton.bottom + horizontalCenter: parent.horizontalCenter + topMargin: 2 * hifi.dimensions.contentSpacing.y + left: parent.left + } + TextMetrics { + id: termsTextMetrics + font: termsText.font + text: signUpBody.termsContainerText + Component.onCompleted: { + // with the link. + termsText.text = qsTr("By signing up, you agree to High Fidelity's Terms of Service") + } + } - MouseArea { - z: -2 - anchors.fill: parent - acceptedButtons: Qt.LeftButton - onClicked: { - if (!usernameField.focus && !emailField.focus && !passwordField.focus) { - usernameField.focus = true; + HifiStylesUit.InfoItem { + id: termsText + text: signUpBody.termsContainerText + font.family: signUpBody.fontFamily + font.pixelSize: signUpBody.fontSize + font.bold: signUpBody.fontBold + wrapMode: Text.WordWrap + color: hifi.colors.white + linkColor: hifi.colors.blueAccent + lineHeight: 1 + lineHeightMode: Text.ProportionalHeight + + onLinkActivated: loginDialog.openUrl(link); + + Component.onCompleted: { + if (termsTextMetrics.width > root.bannerWidth) { + termsText.width = root.bannerWidth; + termsText.wrapMode = Text.WordWrap; + additionalText.verticalAlignment = Text.AlignLeft; + additionalText.horizontalAlignment = Text.AlignLeft; + termsContainer.height = (termsTextMetrics.width / root.bannerWidth) * termsTextMetrics.height; + termsContainer.anchors.left = buttons.left; + } else { + termsText.anchors.centerIn = termsContainer; + } + } + } } } } @@ -408,22 +458,37 @@ Item { onHandleSignupCompleted: { console.log("Sign Up Completed"); + if (signUpBody.loginDialogPoppedUp) { + var data = { + "action": "user signed up successfully" + } + UserActivityLogger.logAction("encourageLoginDialog", data); + } + loginDialog.login(usernameField.text, passwordField.text); bodyLoader.setSource("LoggingInBody.qml", { "loginDialog": loginDialog, "root": root, "bodyLoader": bodyLoader, "withSteam": false, "linkSteam": false }); } onHandleSignupFailed: { console.log("Sign Up Failed") + if (signUpBody.loginDialogPoppedUp) { + var data = { + "action": "user signed up unsuccessfully" + } + UserActivityLogger.logAction("encourageLoginDialog", data); + } + if (errorString !== "") { loginErrorMessage.visible = true; + var errorLength = errorString.split(/\r\n|\r|\n/).length; var errorStringEdited = errorString.replace(/[\n\r]+/g, "\n"); loginErrorMessage.text = errorStringEdited; - loginErrorMessageTextMetrics.text = errorString; - if (loginErrorMessageTextMetrics.width > usernameField.width) { + if (errorLength > 1.0) { + loginErrorMessage.width = root.bannerWidth; loginErrorMessage.wrapMode = Text.WordWrap; loginErrorMessage.verticalAlignment = Text.AlignLeft; loginErrorMessage.horizontalAlignment = Text.AlignLeft; - errorContainer.height = (loginErrorMessageTextMetrics.width / usernameField.width) * loginErrorMessageTextMetrics.height; + errorContainer.height = errorLength * loginErrorMessageTextMetrics.height; } errorContainer.anchors.bottom = usernameField.top; errorContainer.anchors.bottomMargin = hifi.dimensions.contentSpacing.y; diff --git a/interface/resources/qml/LoginDialog/UsernameCollisionBody.qml b/interface/resources/qml/LoginDialog/UsernameCollisionBody.qml index d44f5b733c..2c8e61a29a 100644 --- a/interface/resources/qml/LoginDialog/UsernameCollisionBody.qml +++ b/interface/resources/qml/LoginDialog/UsernameCollisionBody.qml @@ -19,6 +19,7 @@ import TabletScriptingInterface 1.0 Item { id: usernameCollisionBody clip: true + readonly property string termsContainerText: qsTr("By creating this user profile, you agree to High Fidelity's Terms of Service") width: root.width height: root.height readonly property string fontFamily: "Raleway" @@ -26,9 +27,18 @@ Item { readonly property int textFieldFontSize: 18 readonly property bool fontBold: true + property bool withSteam: withSteam + property bool withOculus: withOculus + + readonly property bool loginDialogPoppedUp: loginDialog.getLoginDialogPoppedUp() + function create() { mainTextContainer.visible = false - loginDialog.createAccountFromSteam(textField.text); + if (usernameCollisionBody.withOculus) { + loginDialog.createAccountFromOculus(textField.text); + } else if (usernameCollisionBody.withSteam) { + loginDialog.createAccountFromSteam(textField.text); + } } property bool keyboardEnabled: false @@ -86,12 +96,19 @@ Item { font.family: usernameCollisionBody.fontFamily font.pixelSize: usernameCollisionBody.fontSize font.bold: usernameCollisionBody.fontBold - text: qsTr("Your Steam username is not available."); + text: qsTr(""); wrapMode: Text.WordWrap color: hifi.colors.redAccent lineHeight: 1 lineHeightMode: Text.ProportionalHeight horizontalAlignment: Text.AlignHCenter + Component.onCompleted: { + if (usernameCollisionBody.withOculus) { + text = qsTr("Your Oculus username is not available."); + } else if (usernameCollisionBody.withSteam) { + text = qsTr("Your Steam username is not available."); + } + } } @@ -160,7 +177,8 @@ Item { fontSize: usernameCollisionBody.fontSize fontBold: usernameCollisionBody.fontBold onClicked: { - bodyLoader.setSource("CompleteProfileBody.qml", { "loginDialog": loginDialog, "root": root, "bodyLoader": bodyLoader, "errorString": "" }); + bodyLoader.setSource("CompleteProfileBody.qml", { "loginDialog": loginDialog, "root": root, "bodyLoader": bodyLoader, "withSteam": usernameCollisionBody.withSteam, + "withOculus": usernameCollisionBody.withOculus, "errorString": "" }); } } HifiControlsUit.Button { @@ -183,6 +201,55 @@ Item { } } } + Item { + id: termsContainer + width: parent.width + height: termsTextMetrics.height + anchors { + top: buttons.bottom + horizontalCenter: parent.horizontalCenter + topMargin: 2 * hifi.dimensions.contentSpacing.y + left: parent.left + leftMargin: (parent.width - buttons.width) / 2 + } + TextMetrics { + id: termsTextMetrics + font: termsText.font + text: usernameCollisionBody.termsContainerText + Component.onCompleted: { + // with the link. + termsText.text = qsTr("By creating this user profile, you agree to High Fidelity's Terms of Service") + } + } + + HifiStylesUit.InfoItem { + id: termsText + text: usernameCollisionBody.termsContainerText + font.family: usernameCollisionBody.fontFamily + font.pixelSize: usernameCollisionBody.fontSize + font.bold: usernameCollisionBody.fontBold + wrapMode: Text.WordWrap + color: hifi.colors.white + linkColor: hifi.colors.blueAccent + lineHeight: 1 + lineHeightMode: Text.ProportionalHeight + + onLinkActivated: loginDialog.openUrl(link); + + Component.onCompleted: { + if (termsTextMetrics.width > root.bannerWidth) { + termsText.width = root.bannerWidth; + termsText.wrapMode = Text.WordWrap; + additionalText.verticalAlignment = Text.AlignLeft; + additionalText.horizontalAlignment = Text.AlignLeft; + termsContainer.height = (termsTextMetrics.width / root.bannerWidth) * termsTextMetrics.height; + termsContainer.anchors.left = buttons.left; + } else { + termsText.anchors.centerIn = termsContainer; + } + } + } + } } Component.onCompleted: { @@ -197,18 +264,47 @@ Item { target: loginDialog onHandleCreateCompleted: { console.log("Create Succeeded"); - loginDialog.loginThroughSteam(); - bodyLoader.setSource("LoggingInBody.qml", { "loginDialog": loginDialog, "root": root, "bodyLoader": bodyLoader, "withSteam": true, "linkSteam": false }) + if (usernameCollisionBody.withOculus) { + if (usernameCollisionBody.loginDialogPoppedUp) { + var data = { + "action": "user created a profile with Oculus successfully in the username collision screen" + } + UserActivityLogger.logAction("encourageLoginDialog", data); + } + loginDialog.loginThroughOculus(); + } else if (usernameCollisionBody.withSteam) { + if (usernameCollisionBody.loginDialogPoppedUp) { + var data = { + "action": "user created a profile with Steam successfully in the username collision screen" + } + UserActivityLogger.logAction("encourageLoginDialog", data); + } + loginDialog.loginThroughSteam(); + } + bodyLoader.setSource("LoggingInBody.qml", { "loginDialog": loginDialog, "root": root, "bodyLoader": bodyLoader, "withSteam": usernameCollisionBody.withSteam, + "withOculus": usernameCollisionBody.withOculus, "linkSteam": false, "linkOculus": false }) } onHandleCreateFailed: { console.log("Create Failed: " + error) + if (usernameCollisionBody.loginDialogPoppedUp) { + var data = { + "action": "user failed to create account from the username collision screen" + } + UserActivityLogger.logAction("encourageLoginDialog", data); + } + mainTextContainer.visible = true mainTextContainer.text = "\"" + textField.text + qsTr("\" is invalid or already taken."); } onHandleLoginCompleted: { console.log("Login Succeeded"); - if (loginDialog.getLoginDialogPoppedUp()) { + if (usernameCollisionBody.loginDialogPoppedUp) { + var data = { + "action": "user logged in successfully from the username collision screen" + } + UserActivityLogger.logAction("encourageLoginDialog", data); + loginDialog.dismissLoginDialog(); } root.tryDestroy(); @@ -216,6 +312,13 @@ Item { onHandleLoginFailed: { console.log("Login Failed") + if (usernameCollisionBody.loginDialogPoppedUp) { + var data = { + "action": "user failed to log in from the username collision screen" + } + UserActivityLogger.logAction("encourageLoginDialog", data); + } + mainTextContainer.text = "Login Failed"; } diff --git a/interface/resources/qml/LoginDialog/images/background.jpg b/interface/resources/qml/LoginDialog/images/background.jpg deleted file mode 100644 index db2798a4a1..0000000000 Binary files a/interface/resources/qml/LoginDialog/images/background.jpg and /dev/null differ diff --git a/interface/resources/qml/LoginDialog/images/background.png b/interface/resources/qml/LoginDialog/images/background.png new file mode 100644 index 0000000000..cd107b13eb Binary files /dev/null and b/interface/resources/qml/LoginDialog/images/background.png differ diff --git a/interface/resources/qml/LoginDialog/images/background_tablet.jpg b/interface/resources/qml/LoginDialog/images/background_tablet.jpg deleted file mode 100644 index a46c052c04..0000000000 Binary files a/interface/resources/qml/LoginDialog/images/background_tablet.jpg and /dev/null differ diff --git a/interface/resources/qml/LoginDialog/images/background_tablet.png b/interface/resources/qml/LoginDialog/images/background_tablet.png new file mode 100644 index 0000000000..5c288590ab Binary files /dev/null and b/interface/resources/qml/LoginDialog/images/background_tablet.png differ diff --git a/interface/resources/qml/OverlayLoginDialog.qml b/interface/resources/qml/OverlayLoginDialog.qml index 3de3f68942..0ad2c57e5f 100644 --- a/interface/resources/qml/OverlayLoginDialog.qml +++ b/interface/resources/qml/OverlayLoginDialog.qml @@ -55,7 +55,8 @@ FocusScope { Image { z: -10 id: loginDialogBackground - source: "LoginDialog/images/background.jpg" + fillMode: Image.PreserveAspectCrop + source: "LoginDialog/images/background.png" anchors.fill: parent } @@ -149,6 +150,6 @@ FocusScope { Component.onCompleted: { keyboardTimer.start(); - bodyLoader.setSource("LoginDialog/LinkAccountBody.qml", { "loginDialog": loginDialog, "root": root, "bodyLoader": bodyLoader, "linkSteam": false }); + bodyLoader.setSource("LoginDialog/LinkAccountBody.qml", { "loginDialog": loginDialog, "root": root, "bodyLoader": bodyLoader, "linkSteam": false, "linkOculus": false }); } } diff --git a/interface/resources/qml/Web3DOverlay.qml b/interface/resources/qml/Web3DSurface.qml similarity index 100% rename from interface/resources/qml/Web3DOverlay.qml rename to interface/resources/qml/Web3DSurface.qml diff --git a/interface/resources/qml/controls/+android/WebEntityView.qml b/interface/resources/qml/controls/+android/WebEntityView.qml deleted file mode 100644 index 848077cea0..0000000000 --- a/interface/resources/qml/controls/+android/WebEntityView.qml +++ /dev/null @@ -1,76 +0,0 @@ -// -// Web3DOverlay.qml -// -// Created by Gabriel Calero & Cristian Duarte on Jun 22, 2018 -// Copyright 2016 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -import QtQuick 2.5 -import QtGraphicalEffects 1.0 - -Item { - - property string url - RadialGradient { - anchors.fill: parent - gradient: Gradient { - GradientStop { position: 0.0; color: "#262626" } - GradientStop { position: 1.0; color: "#000000" } - } - } - - function shortUrl(url) { - var hostBegin = url.indexOf("://"); - if (hostBegin > -1) { - url = url.substring(hostBegin + 3); - } - - var portBegin = url.indexOf(":"); - if (portBegin > -1) { - url = url.substring(0, portBegin); - } - - var pathBegin = url.indexOf("/"); - if (pathBegin > -1) { - url = url.substring(0, pathBegin); - } - - if (url.length > 45) { - url = url.substring(0, 45); - } - - return url; - } - - Text { - id: urlText - text: shortUrl(url) - verticalAlignment: Text.AlignVCenter - horizontalAlignment: Text.AlignHCenter - anchors.fill: parent - anchors.rightMargin: 10 - anchors.leftMargin: 10 - font.family: "Cairo" - font.weight: Font.DemiBold - font.pointSize: 48 - fontSizeMode: Text.Fit - color: "#FFFFFF" - minimumPixelSize: 5 - } - - Image { - id: hand - source: "../../../icons/hand.svg" - width: 300 - height: 300 - anchors.bottom: parent.bottom - anchors.right: parent.right - anchors.bottomMargin: 100 - anchors.rightMargin: 100 - } - - -} diff --git a/interface/resources/qml/controls/WebEntityView.qml b/interface/resources/qml/controls/WebEntityView.qml deleted file mode 100644 index 3bd6aad053..0000000000 --- a/interface/resources/qml/controls/WebEntityView.qml +++ /dev/null @@ -1,18 +0,0 @@ -// -// WebEntityView.qml -// -// Created by Kunal Gosar on 16 March 2017 -// Copyright 2017 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -import QtQuick 2.5 -import "." - -WebView { - viewProfile: FileTypeProfile; - - urlTag: "noDownload=true"; -} diff --git a/interface/resources/qml/controlsUit/Key.qml b/interface/resources/qml/controlsUit/Key.qml index dd77fc92dc..b8d95acf1c 100644 --- a/interface/resources/qml/controlsUit/Key.qml +++ b/interface/resources/qml/controlsUit/Key.qml @@ -58,7 +58,7 @@ Item { keyItem.state = "mouseOver"; var globalPosition = keyItem.mapToGlobal(mouseArea1.mouseX, mouseArea1.mouseY); - var pointerID = Web3DOverlay.deviceIdByTouchPoint(globalPosition.x, globalPosition.y); + var pointerID = QmlSurface.deviceIdByTouchPoint(globalPosition.x, globalPosition.y); if (Pointers.isLeftHand(pointerID)) { Controller.triggerHapticPulse(_HAPTIC_STRENGTH, _HAPTIC_DURATION, leftHand); diff --git a/interface/resources/qml/controlsUit/SpinBox.qml b/interface/resources/qml/controlsUit/SpinBox.qml index 34794d80c7..564157efb0 100644 --- a/interface/resources/qml/controlsUit/SpinBox.qml +++ b/interface/resources/qml/controlsUit/SpinBox.qml @@ -57,16 +57,23 @@ SpinBox { locale: Qt.locale("en_US") - onValueModified: realValue = value/factor - onValueChanged: realValue = value/factor + onValueModified: { + realValue = value / factor + } + + onValueChanged: { + realValue = value / factor + spinBox.editingFinished(); + } + onRealValueChanged: { - var newValue = Math.round(realValue*factor); + var newValue = Math.round(realValue * factor); if(value != newValue) { value = newValue; } } - stepSize: realStepSize*factor + stepSize: realStepSize * factor to : realTo*factor from : realFrom*factor @@ -90,11 +97,11 @@ SpinBox { } textFromValue: function(value, locale) { - return parseFloat(value/factor).toFixed(decimals); + return parseFloat(value / factor).toFixed(decimals); } valueFromText: function(text, locale) { - return Number.fromLocaleString(locale, text)*factor; + return Number.fromLocaleString(locale, text) * factor; } @@ -102,7 +109,7 @@ SpinBox { id: spinboxText z: 2 color: isLightColorScheme - ? (spinBox.activeFocus ? hifi.colors.black : hifi.colors.lightGray) + ? (spinBox.activeFocus ? hifi.colors.black : hifi.colors.faintGray) : (spinBox.activeFocus ? hifi.colors.white : hifi.colors.lightGrayText) selectedTextColor: hifi.colors.black selectionColor: hifi.colors.primaryHighlight @@ -112,8 +119,6 @@ SpinBox { verticalAlignment: Qt.AlignVCenter leftPadding: spinBoxLabelInside.visible ? 30 : hifi.dimensions.textPadding width: spinBox.width - hifi.dimensions.spinnerSize - onEditingFinished: spinBox.editingFinished() - Text { id: suffixText x: metrics.advanceWidth(spinboxText.text + '*') @@ -125,7 +130,7 @@ SpinBox { } color: isLightColorScheme - ? (spinBox.activeFocus ? hifi.colors.black : hifi.colors.lightGray) + ? (spinBox.activeFocus ? hifi.colors.black : hifi.colors.faintGray) : (spinBox.activeFocus ? hifi.colors.white : hifi.colors.lightGrayText) text: suffix verticalAlignment: Qt.AlignVCenter @@ -170,6 +175,22 @@ SpinBox { } } + Keys.onPressed: { + if (event.key === Qt.Key_Return) { + if (!spinboxText.acceptableInput) { + var number = spinBox.valueFromText(spinboxText.text, spinBox.locale) / spinBox.factor + + if (number < spinBox.minimumValue) { + number = spinBox.minimumValue; + } else if (number > maximumValue) { + number = spinBox.maximumValue; + } + + spinboxText.text = spinBox.textFromValue(Math.round(number * factor), spinBox.locale) + } + } + } + HifiControls.Label { id: spinBoxLabel text: spinBox.label diff --git a/interface/resources/qml/dialogs/TabletLoginDialog.qml b/interface/resources/qml/dialogs/TabletLoginDialog.qml index 7bffcbe75c..8d6444bc0e 100644 --- a/interface/resources/qml/dialogs/TabletLoginDialog.qml +++ b/interface/resources/qml/dialogs/TabletLoginDialog.qml @@ -14,6 +14,8 @@ import QtQuick 2.5 import controlsUit 1.0 as HifiControlsUit import stylesUit 1.0 as HifiStylesUit +import TabletScriptingInterface 1.0 + import "../LoginDialog" FocusScope { @@ -25,10 +27,9 @@ FocusScope { width: parent.width height: parent.height - signal sendToScript(var message); - signal canceled(); + property var tabletProxy: Tablet.getTablet("com.highfidelity.interface.tablet.system"); - property bool isHMD: false + property bool isHMD: HMD.active property bool gotoPreviousApp: false; property bool keyboardEnabled: false @@ -52,6 +53,7 @@ FocusScope { } function tryDestroy() { + tabletProxy.gotoHomeScreen(); } MouseArea { @@ -76,7 +78,7 @@ FocusScope { interval: 200 onTriggered: { - if (MenuInterface.isOptionChecked("Use 3D Keyboard")) { + if (MenuInterface.isOptionChecked("Use 3D Keyboard") && root.isHMD) { KeyboardScriptingInterface.raised = true; } } @@ -95,7 +97,8 @@ FocusScope { Image { z: -10 id: loginDialogBackground - source: "../LoginDialog/images/background_tablet.jpg" + fillMode: Image.PreserveAspectCrop + source: "../LoginDialog/images/background_tablet.png" anchors.fill: parent } @@ -168,11 +171,13 @@ FocusScope { Component.onDestruction: { loginKeyboard.raised = false; - KeyboardScriptingInterface.raised = false; + if (root.isHMD) { + KeyboardScriptingInterface.raised = false; + } } Component.onCompleted: { keyboardTimer.start(); - bodyLoader.setSource("../LoginDialog/LinkAccountBody.qml", { "loginDialog": loginDialog, "root": root, "bodyLoader": bodyLoader, "linkSteam": false }); + bodyLoader.setSource("../LoginDialog/LinkAccountBody.qml", { "loginDialog": loginDialog, "root": root, "bodyLoader": bodyLoader, "linkSteam": false, "linkOculus": false }); } } diff --git a/interface/resources/qml/hifi/AssetServer.qml b/interface/resources/qml/hifi/AssetServer.qml index 247a42428a..1abd4f45ff 100644 --- a/interface/resources/qml/hifi/AssetServer.qml +++ b/interface/resources/qml/hifi/AssetServer.qml @@ -148,7 +148,7 @@ Windows.ScrollingWindow { } function canAddToWorld(path) { - var supportedExtensions = [/\.fbx\b/i, /\.obj\b/i, /\.jpg\b/i, /\.png\b/i]; + var supportedExtensions = [/\.fbx\b/i, /\.obj\b/i, /\.jpg\b/i, /\.png\b/i, /\.gltf\b/i]; if (selectedItemCount > 1) { return false; diff --git a/interface/resources/qml/hifi/AvatarApp.qml b/interface/resources/qml/hifi/AvatarApp.qml index 171ea4fd15..753b9c5a81 100644 --- a/interface/resources/qml/hifi/AvatarApp.qml +++ b/interface/resources/qml/hifi/AvatarApp.qml @@ -254,6 +254,7 @@ Rectangle { onSaveClicked: function() { var avatarSettings = { dominantHand : settings.dominantHandIsLeft ? 'left' : 'right', + hmdAvatarAlignmentType : settings.hmdAvatarAlignmentTypeIsEyes ? 'eyes' : 'head', collisionsEnabled : settings.environmentCollisionsOn, otherAvatarsCollisionsEnabled : settings.otherAvatarsCollisionsOn, animGraphOverrideUrl : settings.avatarAnimationOverrideJSON, diff --git a/interface/resources/qml/hifi/avatarapp/AdjustWearables.qml b/interface/resources/qml/hifi/avatarapp/AdjustWearables.qml index a537c65b23..136d535b3f 100644 --- a/interface/resources/qml/hifi/avatarapp/AdjustWearables.qml +++ b/interface/resources/qml/hifi/avatarapp/AdjustWearables.qml @@ -177,8 +177,9 @@ Rectangle { repeat: true onTriggered: { var currentWearable = getCurrentWearable(); - var soft = currentWearable ? currentWearable.relayParentJoints : false; - var softEnabled = currentWearable ? entityHasAvatarJoints(currentWearable.id) : false; + var hasSoft = currentWearable && currentWearable.relayParentJoints !== undefined; + var soft = hasSoft ? currentWearable.relayParentJoints : false; + var softEnabled = hasSoft ? entityHasAvatarJoints(currentWearable.id) : false; isSoft.set(soft); isSoft.enabled = softEnabled; } @@ -511,7 +512,7 @@ Rectangle { function set(value) { notify = false; - checked = value + checked = value; notify = true; } diff --git a/interface/resources/qml/hifi/avatarapp/Settings.qml b/interface/resources/qml/hifi/avatarapp/Settings.qml index 668a950d0d..39c48646d3 100644 --- a/interface/resources/qml/hifi/avatarapp/Settings.qml +++ b/interface/resources/qml/hifi/avatarapp/Settings.qml @@ -35,8 +35,9 @@ Rectangle { property real scaleValue: scaleSlider.value / 10 property alias dominantHandIsLeft: leftHandRadioButton.checked - property alias otherAvatarsCollisionsOn: otherAvatarsCollisionsEnabledCheckBox.checked - property alias environmentCollisionsOn: environmentCollisionsEnabledCheckBox.checked + property alias otherAvatarsCollisionsOn: otherAvatarsCollisionsEnabledRadiobutton.checked + property alias environmentCollisionsOn: environmentCollisionsEnabledRadiobutton.checked + property alias hmdAvatarAlignmentTypeIsEyes: eyesRadioButton.checked property alias avatarAnimationOverrideJSON: avatarAnimationUrlInputText.text property alias avatarAnimationJSON: avatarAnimationUrlInputText.placeholderText property alias avatarCollisionSoundUrl: avatarCollisionSoundUrlInputText.text @@ -56,10 +57,19 @@ Rectangle { rightHandRadioButton.checked = true; } if (settings.otherAvatarsCollisionsEnabled) { - otherAvatarsCollisionsEnabledCheckBox.checked = true; + otherAvatarsCollisionsEnabledRadiobutton.checked = true; + } else { + otherAvatarsCollisionsDisabledRadiobutton.checked = true; } if (settings.collisionsEnabled) { - environmentCollisionsEnabledCheckBox.checked = true; + environmentCollisionsEnabledRadiobutton.checked = true; + } else { + environmentCollisionsDisabledRadiobutton.checked = true; + } + if (settings.hmdAvatarAlignmentType === 'eyes') { + eyesRadioButton.checked = true; + } else { + headRadioButton.checked = true; } avatarAnimationJSON = settings.animGraphUrl; @@ -206,7 +216,7 @@ Rectangle { anchors.left: parent.left anchors.right: parent.right - rows: 2 + rows: 4 rowSpacing: 25 columns: 3 @@ -229,7 +239,7 @@ Rectangle { Layout.row: 0 Layout.column: 1 - Layout.leftMargin: -40 + Layout.leftMargin: -15 ButtonGroup.group: leftRight checked: true @@ -246,7 +256,7 @@ Rectangle { Layout.row: 0 Layout.column: 2 - Layout.rightMargin: 20 + Layout.rightMargin: -15 ButtonGroup.group: leftRight @@ -256,7 +266,7 @@ Rectangle { text: "Right" boxSize: 20 } - + HifiConstants { id: hifi } @@ -266,16 +276,43 @@ Rectangle { size: 17; Layout.row: 1 Layout.column: 0 - text: "Avatar collides with other avatars" + text: "Avatar to avatar collision" } - HifiControlsUit.CheckBox { - id: otherAvatarsCollisionsEnabledCheckBox; - boxSize: 20; + ButtonGroup { + id: otherAvatarsOnOff + } + + HifiControlsUit.RadioButton { + id: otherAvatarsCollisionsEnabledRadiobutton + + Layout.row: 1 + Layout.column: 1 + Layout.leftMargin: -15 + + ButtonGroup.group: otherAvatarsOnOff + + colorScheme: hifi.colorSchemes.light + fontSize: 17 + letterSpacing: 1.4 + text: "On" + boxSize: 20 + } + + HifiControlsUit.RadioButton { + id: otherAvatarsCollisionsDisabledRadiobutton + Layout.row: 1 Layout.column: 2 - Layout.leftMargin: 60 + Layout.rightMargin: -15 + + ButtonGroup.group: otherAvatarsOnOff + colorScheme: hifi.colorSchemes.light + fontSize: 17 + letterSpacing: 1.4 + text: "Off" + boxSize: 20 } // TextStyle9 @@ -283,17 +320,90 @@ Rectangle { size: 17; Layout.row: 2 Layout.column: 0 - text: "Avatar collides with environment" + text: "Avatar to environment collision" } - HifiControlsUit.CheckBox { - id: environmentCollisionsEnabledCheckBox; - boxSize: 20; + ButtonGroup { + id: worldOnOff + } + + HifiControlsUit.RadioButton { + id: environmentCollisionsEnabledRadiobutton + + Layout.row: 2 + Layout.column: 1 + Layout.leftMargin: -15 + + ButtonGroup.group: worldOnOff + + colorScheme: hifi.colorSchemes.light + fontSize: 17 + letterSpacing: 1.4 + text: "On" + boxSize: 20 + } + + HifiControlsUit.RadioButton { + id: environmentCollisionsDisabledRadiobutton + Layout.row: 2 Layout.column: 2 - Layout.leftMargin: 60 + Layout.rightMargin: -15 + + ButtonGroup.group: worldOnOff + colorScheme: hifi.colorSchemes.light + fontSize: 17 + letterSpacing: 1.4 + text: "Off" + boxSize: 20 } + + // TextStyle9 + RalewaySemiBold { + size: 17; + Layout.row: 3 + Layout.column: 0 + text: "HMD Alignment" + } + + ButtonGroup { + id: headEyes + } + + HifiControlsUit.RadioButton { + id: headRadioButton + + Layout.row: 3 + Layout.column: 1 + Layout.leftMargin: -15 + + ButtonGroup.group: headEyes + checked: true + + colorScheme: hifi.colorSchemes.light + fontSize: 17 + letterSpacing: 1.4 + text: "Head" + boxSize: 20 + } + + HifiControlsUit.RadioButton { + id: eyesRadioButton + + Layout.row: 3 + Layout.column: 2 + Layout.rightMargin: -15 + + ButtonGroup.group: headEyes + + colorScheme: hifi.colorSchemes.light + fontSize: 17 + letterSpacing: 1.4 + text: "Eyes" + boxSize: 20 + } + } ColumnLayout { diff --git a/interface/resources/qml/hifi/dialogs/TabletAssetServer.qml b/interface/resources/qml/hifi/dialogs/TabletAssetServer.qml index b5374b2fe0..62ec264fc9 100644 --- a/interface/resources/qml/hifi/dialogs/TabletAssetServer.qml +++ b/interface/resources/qml/hifi/dialogs/TabletAssetServer.qml @@ -148,7 +148,7 @@ Rectangle { } function canAddToWorld(path) { - var supportedExtensions = [/\.fbx\b/i, /\.obj\b/i, /\.jpg\b/i, /\.png\b/i]; + var supportedExtensions = [/\.fbx\b/i, /\.obj\b/i, /\.jpg\b/i, /\.png\b/i, /\.gltf\b/i]; if (selectedItemCount > 1) { return false; diff --git a/interface/resources/qml/hifi/tablet/OpenVrConfiguration.qml b/interface/resources/qml/hifi/tablet/OpenVrConfiguration.qml index a2104826c3..a9d67fec35 100644 --- a/interface/resources/qml/hifi/tablet/OpenVrConfiguration.qml +++ b/interface/resources/qml/hifi/tablet/OpenVrConfiguration.qml @@ -70,7 +70,7 @@ Flickable { readonly property bool hmdDesktop: hmdInDesktop.checked property int state: buttonState.disabled - property var lastConfiguration: null + property var lastConfiguration: null HifiConstants { id: hifi } @@ -90,7 +90,6 @@ Flickable { anchors.fill: parent propagateComposedEvents: true onPressed: { - parent.forceActiveFocus() mouse.accepted = false; } } @@ -169,9 +168,7 @@ Flickable { boxRadius: 7 visible: viveInDesktop.checked - anchors.top: viveInDesktop.bottom anchors.topMargin: 5 - anchors.left: openVrConfiguration.left anchors.leftMargin: leftMargin + 10 onClicked: { @@ -214,13 +211,13 @@ Flickable { onRealValueChanged: { sendConfigurationSettings(); - openVrConfiguration.forceActiveFocus(); } } HifiControls.SpinBox { id: headZOffset + z: 10 width: 112 label: "Z Offset" minimumValue: -50 @@ -232,7 +229,6 @@ Flickable { onRealValueChanged: { sendConfigurationSettings(); - openVrConfiguration.forceActiveFocus(); } } } @@ -326,7 +322,6 @@ Flickable { onRealValueChanged: { sendConfigurationSettings(); - openVrConfiguration.forceActiveFocus(); } } @@ -344,7 +339,6 @@ Flickable { onRealValueChanged: { sendConfigurationSettings(); - openVrConfiguration.forceActiveFocus(); } } } @@ -578,7 +572,6 @@ Flickable { onRealValueChanged: { sendConfigurationSettings(); - openVrConfiguration.forceActiveFocus(); } } @@ -596,7 +589,6 @@ Flickable { onRealValueChanged: { sendConfigurationSettings(); - openVrConfiguration.forceActiveFocus(); } } } @@ -747,8 +739,8 @@ Flickable { } Component.onCompleted: { - InputConfiguration.calibrationStatus.connect(calibrationStatusInfo); lastConfiguration = composeConfigurationSettings(); + InputConfiguration.calibrationStatus.connect(calibrationStatusInfo); } Component.onDestruction: { @@ -777,7 +769,6 @@ Flickable { calibrationTimer.interval = realValue * 1000; openVrConfiguration.countDown = realValue; numberAnimation.duration = calibrationTimer.interval; - openVrConfiguration.forceActiveFocus(); } } @@ -1048,6 +1039,9 @@ Flickable { } function updateButtonState() { + if (lastConfiguration === null) { + lastConfiguration = composeConfigurationSettings(); + } var settings = composeConfigurationSettings(); var bodySetting = settings["bodyConfiguration"]; var headSetting = settings["headConfiguration"]; diff --git a/interface/resources/qml/hifi/tablet/TabletRoot.qml b/interface/resources/qml/hifi/tablet/TabletRoot.qml index a01d978b2f..b19dcbb919 100644 --- a/interface/resources/qml/hifi/tablet/TabletRoot.qml +++ b/interface/resources/qml/hifi/tablet/TabletRoot.qml @@ -134,8 +134,7 @@ Item { if (isWebPage) { var webUrl = tabletApps.get(currentApp).appWebUrl; var scriptUrl = tabletApps.get(currentApp).scriptUrl; - loadSource("hifi/tablet/TabletWebView.qml"); - loadWebUrl(webUrl, scriptUrl); + loadWebBase(webUrl, scriptUrl); } else { loader.load(tabletApps.get(currentApp).appUrl); } @@ -150,16 +149,6 @@ Item { tabletRoot.openBrowser = newWindow; } - function loadWebUrl(url, injectedJavaScriptUrl) { - tabletApps.clear(); - loader.item.url = url; - loader.item.scriptURL = injectedJavaScriptUrl; - tabletApps.append({"appUrl": "TabletWebView.qml", "isWebUrl": true, "scriptUrl": injectedJavaScriptUrl, "appWebUrl": url}); - if (loader.item.hasOwnProperty("closeButtonVisible")) { - loader.item.closeButtonVisible = false; - } - } - // used to send a message from qml to interface script. signal sendToScript(var message); diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 1954e373f7..47250a1215 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -120,6 +120,7 @@ #include #include #include +#include #include #include #include @@ -151,6 +152,7 @@ #include #include #include +#include #include #include "recording/ClipCache.h" @@ -799,7 +801,6 @@ bool setupEssentials(int& argc, char** argv, bool runningMarkerExisted) { if (auto steamClient = pluginManager->getSteamClientPlugin()) { steamClient->init(); } - PROFILE_SET_THREAD_NAME("Main Thread"); #if defined(Q_OS_WIN) @@ -919,6 +920,7 @@ bool setupEssentials(int& argc, char** argv, bool runningMarkerExisted) { DependencyManager::set(); DependencyManager::set(); DependencyManager::set(); + DependencyManager::set(); DependencyManager::set(); DependencyManager::set(); @@ -1200,6 +1202,10 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo connect(&domainHandler, SIGNAL(connectedToDomain(QUrl)), SLOT(updateWindowTitle())); connect(&domainHandler, SIGNAL(disconnectedFromDomain()), SLOT(updateWindowTitle())); connect(&domainHandler, &DomainHandler::disconnectedFromDomain, this, [this]() { + auto tabletScriptingInterface = DependencyManager::get(); + if (tabletScriptingInterface) { + tabletScriptingInterface->setQmlTabletRoot(SYSTEM_TABLET, nullptr); + } getOverlays().deleteOverlay(getTabletScreenID()); getOverlays().deleteOverlay(getTabletHomeButtonID()); getOverlays().deleteOverlay(getTabletFrameID()); @@ -2201,7 +2207,6 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo || ((rightHandPose.valid || lastRightHandPose.valid) && (rightHandPose != lastRightHandPose)); lastLeftHandPose = leftHandPose; lastRightHandPose = rightHandPose; - properties["avatar_identity_requests_sent"] = DependencyManager::get()->getIdentityRequestsSent(); UserActivityLogger::getInstance().logAction("stats", properties); }); @@ -2316,6 +2321,76 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo return DependencyManager::get()->getMyAvatar()->getWorldOrientation() * Vectors::UP; }); + render::entities::WebEntityRenderer::setAcquireWebSurfaceOperator([this](const QString& url, bool htmlContent, QSharedPointer& webSurface, bool& cachedWebSurface) { + bool isTablet = url == TabletScriptingInterface::QML; + if (htmlContent) { + webSurface = DependencyManager::get()->acquire(render::entities::WebEntityRenderer::QML); + cachedWebSurface = true; + auto rootItemLoadedFunctor = [url, webSurface] { + webSurface->getRootItem()->setProperty(render::entities::WebEntityRenderer::URL_PROPERTY, url); + }; + if (webSurface->getRootItem()) { + rootItemLoadedFunctor(); + } else { + QObject::connect(webSurface.data(), &hifi::qml::OffscreenSurface::rootContextCreated, rootItemLoadedFunctor); + } + } else { + // FIXME: the tablet should use the OffscreenQmlSurfaceCache + webSurface = QSharedPointer(new OffscreenQmlSurface(), [](OffscreenQmlSurface* webSurface) { + AbstractViewStateInterface::instance()->sendLambdaEvent([webSurface] { + // WebEngineView may run other threads (wasapi), so they must be deleted for a clean shutdown + // if the application has already stopped its event loop, delete must be explicit + delete webSurface; + }); + }); + auto rootItemLoadedFunctor = [webSurface, url, isTablet] { + Application::setupQmlSurface(webSurface->getSurfaceContext(), isTablet || url == OVERLAY_LOGIN_DIALOG.toString()); + }; + if (webSurface->getRootItem()) { + rootItemLoadedFunctor(); + } else { + QObject::connect(webSurface.data(), &hifi::qml::OffscreenSurface::rootContextCreated, rootItemLoadedFunctor); + } + webSurface->load(url); + cachedWebSurface = false; + } + const uint8_t DEFAULT_MAX_FPS = 10; + const uint8_t TABLET_FPS = 90; + webSurface->setMaxFps(isTablet ? TABLET_FPS : DEFAULT_MAX_FPS); + }); + render::entities::WebEntityRenderer::setReleaseWebSurfaceOperator([this](QSharedPointer& webSurface, bool& cachedWebSurface, std::vector& connections) { + QQuickItem* rootItem = webSurface->getRootItem(); + + // Fix for crash in QtWebEngineCore when rapidly switching domains + // Call stop on the QWebEngineView before destroying OffscreenQMLSurface. + if (rootItem) { + // stop loading + QMetaObject::invokeMethod(rootItem, "stop"); + } + + webSurface->pause(); + + for (auto& connection : connections) { + QObject::disconnect(connection); + } + connections.clear(); + + // If the web surface was fetched out of the cache, release it back into the cache + if (cachedWebSurface) { + // If it's going back into the cache make sure to explicitly set the URL to a blank page + // in order to stop any resource consumption or audio related to the page. + if (rootItem) { + rootItem->setProperty("url", "about:blank"); + } + auto offscreenCache = DependencyManager::get(); + if (offscreenCache) { + offscreenCache->release(render::entities::WebEntityRenderer::QML, webSurface); + } + cachedWebSurface = false; + } + webSurface.reset(); + }); + // Preload Tablet sounds DependencyManager::get()->preloadSounds(); DependencyManager::get()->createKeyboard(); @@ -2599,6 +2674,7 @@ void Application::cleanupBeforeQuit() { DependencyManager::destroy(); // Must be destroyed before TabletScriptingInterface // stop QML + DependencyManager::destroy(); DependencyManager::destroy(); DependencyManager::destroy(); DependencyManager::destroy(); @@ -2661,6 +2737,7 @@ Application::~Application() { if (auto steamClient = PluginManager::getInstance()->getSteamClientPlugin()) { steamClient->shutdown(); } + DependencyManager::destroy(); DependencyManager::destroy(); // must be destroyed before the FramebufferCache @@ -2867,7 +2944,7 @@ void Application::showLoginScreen() { dialogsManager->showLoginDialog(); emit loginDialogFocusEnabled(); QJsonObject loginData = {}; - loginData["action"] = "login dialog shown"; + loginData["action"] = "login dialog popped up"; UserActivityLogger::getInstance().logAction("encourageLoginDialog", loginData); _window->setWindowTitle("High Fidelity Interface"); } else { @@ -2887,7 +2964,7 @@ void Application::initializeUi() { Tooltip::registerType(); UpdateDialog::registerType(); QmlContextCallback commerceCallback = [](QQmlContext* context) { - context->setContextProperty("Commerce", new QmlCommerce()); + context->setContextProperty("Commerce", DependencyManager::get().data()); }; OffscreenQmlSurface::addWhitelistContextHandler({ QUrl{ "hifi/commerce/checkout/Checkout.qml" }, @@ -2912,6 +2989,7 @@ void Application::initializeUi() { QUrl{ "hifi/dialogs/security/SecurityImageChange.qml" }, QUrl{ "hifi/dialogs/security/SecurityImageModel.qml" }, QUrl{ "hifi/dialogs/security/SecurityImageSelection.qml" }, + QUrl{ "hifi/tablet/TabletMenu.qml" }, }, commerceCallback); QmlContextCallback ttsCallback = [](QQmlContext* context) { context->setContextProperty("TextToSpeech", DependencyManager::get().data()); @@ -3015,7 +3093,7 @@ void Application::initializeUi() { }); offscreenSurfaceCache->reserve(TabletScriptingInterface::QML, 1); - offscreenSurfaceCache->reserve(Web3DOverlay::QML, 2); + offscreenSurfaceCache->reserve(render::entities::WebEntityRenderer::QML, 2); #endif flushMenuUpdates(); @@ -3155,6 +3233,61 @@ void Application::onDesktopRootItemCreated(QQuickItem* rootItem) { #endif } +void Application::setupQmlSurface(QQmlContext* surfaceContext, bool setAdditionalContextProperties) { + surfaceContext->setContextProperty("Users", DependencyManager::get().data()); + surfaceContext->setContextProperty("HMD", DependencyManager::get().data()); + surfaceContext->setContextProperty("UserActivityLogger", DependencyManager::get().data()); + surfaceContext->setContextProperty("Preferences", DependencyManager::get().data()); + surfaceContext->setContextProperty("Vec3", new Vec3()); + surfaceContext->setContextProperty("Quat", new Quat()); + surfaceContext->setContextProperty("MyAvatar", DependencyManager::get()->getMyAvatar().get()); + surfaceContext->setContextProperty("Entities", DependencyManager::get().data()); + surfaceContext->setContextProperty("Snapshot", DependencyManager::get().data()); + + if (setAdditionalContextProperties) { + auto tabletScriptingInterface = DependencyManager::get(); + auto flags = tabletScriptingInterface->getFlags(); + + surfaceContext->setContextProperty("offscreenFlags", flags); + surfaceContext->setContextProperty("AddressManager", DependencyManager::get().data()); + + surfaceContext->setContextProperty("Settings", SettingsScriptingInterface::getInstance()); + surfaceContext->setContextProperty("MenuInterface", MenuScriptingInterface::getInstance()); + surfaceContext->setContextProperty("KeyboardScriptingInterface", DependencyManager::get().data()); + + surfaceContext->setContextProperty("Account", AccountServicesScriptingInterface::getInstance()); // DEPRECATED - TO BE REMOVED + surfaceContext->setContextProperty("GlobalServices", AccountServicesScriptingInterface::getInstance()); // DEPRECATED - TO BE REMOVED + surfaceContext->setContextProperty("AccountServices", AccountServicesScriptingInterface::getInstance()); + + // in Qt 5.10.0 there is already an "Audio" object in the QML context + // though I failed to find it (from QtMultimedia??). So.. let it be "AudioScriptingInterface" + surfaceContext->setContextProperty("AudioScriptingInterface", DependencyManager::get().data()); + + surfaceContext->setContextProperty("AudioStats", DependencyManager::get()->getStats().data()); + surfaceContext->setContextProperty("fileDialogHelper", new FileDialogHelper()); + surfaceContext->setContextProperty("ScriptDiscoveryService", DependencyManager::get().data()); + surfaceContext->setContextProperty("Assets", DependencyManager::get().data()); + surfaceContext->setContextProperty("LODManager", DependencyManager::get().data()); + surfaceContext->setContextProperty("OctreeStats", DependencyManager::get().data()); + surfaceContext->setContextProperty("DCModel", DependencyManager::get().data()); + surfaceContext->setContextProperty("AvatarInputs", AvatarInputs::getInstance()); + surfaceContext->setContextProperty("AvatarList", DependencyManager::get().data()); + surfaceContext->setContextProperty("DialogsManager", DialogsManagerScriptingInterface::getInstance()); + surfaceContext->setContextProperty("InputConfiguration", DependencyManager::get().data()); + surfaceContext->setContextProperty("SoundCache", DependencyManager::get().data()); + surfaceContext->setContextProperty("AvatarBookmarks", DependencyManager::get().data()); + surfaceContext->setContextProperty("Render", AbstractViewStateInterface::instance()->getRenderEngine()->getConfiguration().get()); + surfaceContext->setContextProperty("Workload", qApp->getGameWorkload()._engine->getConfiguration().get()); + surfaceContext->setContextProperty("Controller", DependencyManager::get().data()); + surfaceContext->setContextProperty("Pointers", DependencyManager::get().data()); + surfaceContext->setContextProperty("Window", DependencyManager::get().data()); + surfaceContext->setContextProperty("Reticle", qApp->getApplicationCompositor().getReticleInterface()); + surfaceContext->setContextProperty("HiFiAbout", AboutUtil::getInstance()); + surfaceContext->setContextProperty("WalletScriptingInterface", DependencyManager::get().data()); + surfaceContext->setContextProperty("ResourceRequestObserver", DependencyManager::get().data()); + } +} + void Application::updateCamera(RenderArgs& renderArgs, float deltaTime) { PROFILE_RANGE(render, __FUNCTION__); PerformanceTimer perfTimer("updateCamera"); @@ -4741,6 +4874,10 @@ void Application::idle() { steamClient->runCallbacks(); } + if (auto oculusPlugin = PluginManager::getInstance()->getOculusPlatformPlugin()) { + oculusPlugin->handleOVREvents(); + } + float secondsSinceLastUpdate = (float)_lastTimeUpdated.nsecsElapsed() / NSECS_PER_MSEC / MSECS_PER_SECOND; _lastTimeUpdated.start(); @@ -5925,6 +6062,13 @@ void Application::update(float deltaTime) { auto userInputMapper = DependencyManager::get(); + controller::HmdAvatarAlignmentType hmdAvatarAlignmentType; + if (myAvatar->getHmdAvatarAlignmentType() == "eyes") { + hmdAvatarAlignmentType = controller::HmdAvatarAlignmentType::Eyes; + } else { + hmdAvatarAlignmentType = controller::HmdAvatarAlignmentType::Head; + } + controller::InputCalibrationData calibrationData = { myAvatar->getSensorToWorldMatrix(), createMatFromQuatAndPos(myAvatar->getWorldOrientation(), myAvatar->getWorldPosition()), @@ -5938,7 +6082,8 @@ void Application::update(float deltaTime) { myAvatar->getRightArmCalibrationMat(), myAvatar->getLeftArmCalibrationMat(), myAvatar->getRightHandCalibrationMat(), - myAvatar->getLeftHandCalibrationMat() + myAvatar->getLeftHandCalibrationMat(), + hmdAvatarAlignmentType }; InputPluginPointer keyboardMousePlugin; @@ -6274,7 +6419,7 @@ void Application::update(float deltaTime) { // TODO: Fix this by modeling the way the secondary camera works on how the main camera works // ie. Use a camera object stored in the game logic and informs the Engine on where the secondary // camera should be. - // updateSecondaryCameraViewFrustum(); + updateSecondaryCameraViewFrustum(); } quint64 now = usecTimestampNow(); @@ -6757,7 +6902,7 @@ void Application::updateWindowTitle() const { DependencyManager::get< MessagesClient >()->sendLocalMessage("Toolbar-DomainChanged", ""); } -void Application::clearDomainOctreeDetails() { +void Application::clearDomainOctreeDetails(bool clearAll) { // before we delete all entities get MyAvatar's AvatarEntityData ready getMyAvatar()->prepareAvatarEntityDataForReload(); @@ -6776,7 +6921,7 @@ void Application::clearDomainOctreeDetails() { }); // reset the model renderer - getEntities()->clear(); + clearAll ? getEntities()->clear() : getEntities()->clearNonLocalEntities(); auto skyStage = DependencyManager::get()->getSkyStage(); @@ -6814,7 +6959,7 @@ void Application::goToErrorDomainURL(QUrl errorDomainURL) { void Application::resettingDomain() { _notifiedPacketVersionMismatchThisDomain = false; - clearDomainOctreeDetails(); + clearDomainOctreeDetails(false); } void Application::nodeAdded(SharedNodePointer node) const { @@ -6900,7 +7045,7 @@ void Application::nodeKilled(SharedNodePointer node) { QMetaObject::invokeMethod(DependencyManager::get().data(), "audioMixerKilled"); } else if (node->getType() == NodeType::EntityServer) { // we lost an entity server, clear all of the domain octree details - clearDomainOctreeDetails(); + clearDomainOctreeDetails(false); } else if (node->getType() == NodeType::AssetServer) { // asset server going away - check if we have the asset browser showing @@ -8015,8 +8160,7 @@ void Application::openUrl(const QUrl& url) const { if (url.scheme() == URL_SCHEME_HIFI) { DependencyManager::get()->handleLookupString(url.toString()); } else if (url.scheme() == URL_SCHEME_HIFIAPP) { - QmlCommerce commerce; - commerce.openSystemApp(url.path()); + DependencyManager::get()->openSystemApp(url.path()); } else { // address manager did not handle - ask QDesktopServices to handle QDesktopServices::openUrl(url); diff --git a/interface/src/Application.h b/interface/src/Application.h index 4c6d45b8c3..fbf6e8bc9c 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -469,7 +469,7 @@ private slots: void onDesktopRootItemCreated(QQuickItem* qmlContext); void onDesktopRootContextCreated(QQmlContext* qmlContext); void showDesktop(); - void clearDomainOctreeDetails(); + void clearDomainOctreeDetails(bool clearAll = true); void onAboutToQuit(); void onPresent(quint32 frameCount); @@ -589,6 +589,8 @@ private: void maybeToggleMenuVisible(QMouseEvent* event) const; void toggleTabletUI(bool shouldOpen = false) const; + static void setupQmlSurface(QQmlContext* surfaceContext, bool setAdditionalContextProperties); + MainWindow* _window; QElapsedTimer& _sessionRunTimer; diff --git a/interface/src/AvatarBookmarks.cpp b/interface/src/AvatarBookmarks.cpp index 1d003f19c1..5fe35bd23f 100644 --- a/interface/src/AvatarBookmarks.cpp +++ b/interface/src/AvatarBookmarks.cpp @@ -55,7 +55,7 @@ void addAvatarEntities(const QVariantList& avatarEntities) { QVariantMap asMap = variantProperties.toMap(); QScriptValue scriptProperties = variantMapToScriptValue(asMap, scriptEngine); EntityItemProperties entityProperties; - EntityItemPropertiesFromScriptValueHonorReadOnly(scriptProperties, entityProperties); + EntityItemPropertiesFromScriptValueIgnoreReadOnly(scriptProperties, entityProperties); entityProperties.setParentID(myNodeID); entityProperties.setEntityHostType(entity::HostType::AVATAR); @@ -151,8 +151,29 @@ void AvatarBookmarks::removeBookmark(const QString& bookmarkName) { void AvatarBookmarks::updateAvatarEntities(const QVariantList &avatarEntities) { auto myAvatar = DependencyManager::get()->getMyAvatar(); - myAvatar->removeWearableAvatarEntities(); - addAvatarEntities(avatarEntities); + auto currentAvatarEntities = myAvatar->getAvatarEntityData(); + std::set newAvatarEntities; + + // Update or add all the new avatar entities + for (auto& avatarEntityVariant : avatarEntities) { + auto avatarEntityVariantMap = avatarEntityVariant.toMap(); + auto idItr = avatarEntityVariantMap.find("id"); + if (idItr != avatarEntityVariantMap.end()) { + auto propertiesItr = avatarEntityVariantMap.find("properties"); + if (propertiesItr != avatarEntityVariantMap.end()) { + EntityItemID id = idItr.value().toUuid(); + newAvatarEntities.insert(id); + myAvatar->updateAvatarEntity(id, QJsonDocument::fromVariant(propertiesItr.value()).toBinaryData()); + } + } + } + + // Remove any old entities not in the new list + for (auto& avatarEntityID : currentAvatarEntities.keys()) { + if (newAvatarEntities.find(avatarEntityID) == newAvatarEntities.end()) { + myAvatar->removeWornAvatarEntity(avatarEntityID); + } + } } void AvatarBookmarks::loadBookmark(const QString& bookmarkName) { @@ -176,7 +197,7 @@ void AvatarBookmarks::loadBookmark(const QString& bookmarkName) { auto myAvatar = DependencyManager::get()->getMyAvatar(); auto treeRenderer = DependencyManager::get(); EntityTreePointer entityTree = treeRenderer ? treeRenderer->getTree() : nullptr; - myAvatar->removeWearableAvatarEntities(); + myAvatar->clearWornAvatarEntities(); const QString& avatarUrl = bookmark.value(ENTRY_AVATAR_URL, "").toString(); myAvatar->useFullAvatarURL(avatarUrl); qCDebug(interfaceapp) << "Avatar On"; diff --git a/interface/src/avatar/AvatarManager.cpp b/interface/src/avatar/AvatarManager.cpp old mode 100644 new mode 100755 index 53c16c8a61..5e4f02742e --- a/interface/src/avatar/AvatarManager.cpp +++ b/interface/src/avatar/AvatarManager.cpp @@ -48,8 +48,6 @@ // 50 times per second - target is 45hz, but this helps account for any small deviations // in the update loop - this also results in ~30hz when in desktop mode which is essentially // what we want -const int CLIENT_TO_AVATAR_MIXER_BROADCAST_FRAMES_PER_SECOND = 50; -static const quint64 MIN_TIME_BETWEEN_MY_AVATAR_DATA_SENDS = USECS_PER_SECOND / CLIENT_TO_AVATAR_MIXER_BROADCAST_FRAMES_PER_SECOND; // We add _myAvatar into the hash with all the other AvatarData, and we use the default NULL QUid as the key. const QUuid MY_AVATAR_KEY; // NULL key @@ -67,6 +65,11 @@ AvatarManager::AvatarManager(QObject* parent) : connect(nodeList.data(), &NodeList::ignoredNode, this, [this](const QUuid& nodeID, bool enabled) { if (enabled) { removeAvatar(nodeID, KillAvatarReason::AvatarIgnored); + } else { + auto avatar = std::static_pointer_cast(getAvatarBySessionID(nodeID)); + if (avatar) { + avatar->createOrb(); + } } }); @@ -267,7 +270,6 @@ void AvatarManager::updateOtherAvatars(float deltaTime) { if (avatar->getSkeletonModel()->isLoaded()) { // remove the orb if it is there avatar->removeOrb(); - avatar->updateCollisionGroup(_myAvatar->getOtherAvatarsCollisionsEnabled()); if (avatar->needsPhysicsUpdate()) { _avatarsToChangeInPhysics.insert(avatar); } @@ -347,25 +349,6 @@ void AvatarManager::postUpdate(float deltaTime, const render::ScenePointer& scen } } -void AvatarManager::sendIdentityRequest(const QUuid& avatarID) const { - auto nodeList = DependencyManager::get(); - QWeakPointer nodeListWeak = nodeList; - nodeList->eachMatchingNode( - [](const SharedNodePointer& node)->bool { - return node->getType() == NodeType::AvatarMixer && node->getActiveSocket(); - }, - [this, avatarID, nodeListWeak](const SharedNodePointer& node) { - auto nodeList = nodeListWeak.lock(); - if (nodeList) { - auto packet = NLPacket::create(PacketType::AvatarIdentityRequest, NUM_BYTES_RFC4122_UUID, true); - packet->write(avatarID.toRfc4122()); - nodeList->sendPacket(std::move(packet), *node); - ++_identityRequestsSent; - } - } - ); -} - void AvatarManager::simulateAvatarFades(float deltaTime) { if (_avatarsToFadeOut.empty()) { return; @@ -391,8 +374,14 @@ void AvatarManager::simulateAvatarFades(float deltaTime) { scene->enqueueTransaction(transaction); } -AvatarSharedPointer AvatarManager::newSharedAvatar() { - return AvatarSharedPointer(new OtherAvatar(qApp->thread()), [](OtherAvatar* ptr) { ptr->deleteLater(); }); +AvatarSharedPointer AvatarManager::newSharedAvatar(const QUuid& sessionUUID) { + auto otherAvatar = new OtherAvatar(qApp->thread()); + otherAvatar->setSessionUUID(sessionUUID); + auto nodeList = DependencyManager::get(); + if (!nodeList || !nodeList->isIgnoringNode(sessionUUID)) { + otherAvatar->createOrb(); + } + return AvatarSharedPointer(otherAvatar, [](OtherAvatar* ptr) { ptr->deleteLater(); }); } void AvatarManager::queuePhysicsChange(const OtherAvatarPointer& avatar) { diff --git a/interface/src/avatar/AvatarManager.h b/interface/src/avatar/AvatarManager.h index 359af8e361..7bd4e8236a 100644 --- a/interface/src/avatar/AvatarManager.h +++ b/interface/src/avatar/AvatarManager.h @@ -92,7 +92,6 @@ public: void updateMyAvatar(float deltaTime); void updateOtherAvatars(float deltaTime); - void sendIdentityRequest(const QUuid& avatarID) const; void setMyAvatarDataPacketsPaused(bool puase); @@ -191,7 +190,6 @@ public: Q_INVOKABLE QVariantMap getPalData(const QStringList& specificAvatarIdentifiers = QStringList()); float getMyAvatarSendRate() const { return _myAvatarSendRate.rate(); } - int getIdentityRequestsSent() const { return _identityRequestsSent; } void queuePhysicsChange(const OtherAvatarPointer& avatar); void buildPhysicsTransaction(PhysicsEngine::Transaction& transaction); @@ -216,7 +214,7 @@ private: void simulateAvatarFades(float deltaTime); - AvatarSharedPointer newSharedAvatar() override; + AvatarSharedPointer newSharedAvatar(const QUuid& sessionUUID) override; // called only from the AvatarHashMap thread - cannot be called while this thread holds the // hash lock, since handleRemovedAvatar needs a write lock on the entity tree and the entity tree @@ -241,7 +239,6 @@ private: float _avatarSimulationTime { 0.0f }; bool _shouldRender { true }; bool _myAvatarDataPacketsPaused { false }; - mutable int _identityRequestsSent { 0 }; mutable std::mutex _spaceLock; workload::SpacePointer _space; diff --git a/interface/src/avatar/AvatarMotionState.cpp b/interface/src/avatar/AvatarMotionState.cpp old mode 100644 new mode 100755 index 3fa59ea967..77fc81fa04 --- a/interface/src/avatar/AvatarMotionState.cpp +++ b/interface/src/avatar/AvatarMotionState.cpp @@ -15,7 +15,6 @@ #include #include - AvatarMotionState::AvatarMotionState(OtherAvatarPointer avatar, const btCollisionShape* shape) : ObjectMotionState(shape), _avatar(avatar) { assert(_avatar); _type = MOTIONSTATE_TYPE_AVATAR; @@ -172,7 +171,10 @@ QUuid AvatarMotionState::getSimulatorID() const { // virtual void AvatarMotionState::computeCollisionGroupAndMask(int32_t& group, int32_t& mask) const { group = _collisionGroup; - mask = _collisionGroup == BULLET_COLLISION_GROUP_COLLISIONLESS ? 0 : Physics::getDefaultCollisionMask(group); + mask = Physics::getDefaultCollisionMask(group); + if (!_avatar->getCollideWithOtherAvatars()) { + mask &= ~(BULLET_COLLISION_GROUP_MY_AVATAR | BULLET_COLLISION_GROUP_OTHER_AVATAR); + } } // virtual diff --git a/interface/src/avatar/GrabManager.cpp b/interface/src/avatar/GrabManager.cpp index c41435d67e..db1337b64d 100644 --- a/interface/src/avatar/GrabManager.cpp +++ b/interface/src/avatar/GrabManager.cpp @@ -18,6 +18,8 @@ void GrabManager::simulateGrabs() { // Update grabbed objects auto entityTreeRenderer = DependencyManager::get(); auto entityTree = entityTreeRenderer->getTree(); + auto sessionID = DependencyManager::get()->getSessionUUID(); + EntityEditPacketSender* packetSender = entityTreeRenderer ? entityTreeRenderer->getPacketSender() : nullptr; entityTree->withReadLock([&] { PROFILE_RANGE(simulation, "Grabs"); @@ -33,6 +35,8 @@ void GrabManager::simulateGrabs() { glm::vec3 finalPosition = acc.finalizePosition(); glm::quat finalOrientation = acc.finalizeOrientation(); grabbedThing->setTransform(createMatFromQuatAndPos(finalOrientation, finalPosition)); + bool iShouldTellServer = grabbedThing->getEditSenderID() == sessionID; + entityTree->updateEntityQueryAACube(grabbedThing, packetSender, false, iShouldTellServer); } } }); diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 4e5c143919..8016bf840e 100755 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -95,6 +95,37 @@ const float CENTIMETERS_PER_METER = 100.0f; const QString AVATAR_SETTINGS_GROUP_NAME { "Avatar" }; +static const QString USER_RECENTER_MODEL_FORCE_SIT = QStringLiteral("ForceSit"); +static const QString USER_RECENTER_MODEL_FORCE_STAND = QStringLiteral("ForceStand"); +static const QString USER_RECENTER_MODEL_AUTO = QStringLiteral("Auto"); +static const QString USER_RECENTER_MODEL_DISABLE_HMD_LEAN = QStringLiteral("DisableHMDLean"); + +MyAvatar::SitStandModelType stringToUserRecenterModel(const QString& str) { + if (str == USER_RECENTER_MODEL_FORCE_SIT) { + return MyAvatar::ForceSit; + } else if (str == USER_RECENTER_MODEL_FORCE_STAND) { + return MyAvatar::ForceStand; + } else if (str == USER_RECENTER_MODEL_DISABLE_HMD_LEAN) { + return MyAvatar::DisableHMDLean; + } else { + return MyAvatar::Auto; + } +} + +QString userRecenterModelToString(MyAvatar::SitStandModelType model) { + switch (model) { + case MyAvatar::ForceSit: + return USER_RECENTER_MODEL_FORCE_SIT; + case MyAvatar::ForceStand: + return USER_RECENTER_MODEL_FORCE_STAND; + case MyAvatar::DisableHMDLean: + return USER_RECENTER_MODEL_DISABLE_HMD_LEAN; + case MyAvatar::Auto: + default: + return USER_RECENTER_MODEL_AUTO; + } +} + MyAvatar::MyAvatar(QThread* thread) : Avatar(thread), _yawSpeed(YAW_SPEED_DEFAULT), @@ -125,6 +156,7 @@ MyAvatar::MyAvatar(QThread* thread) : _prevShouldDrawHead(true), _audioListenerMode(FROM_HEAD), _dominantHandSetting(QStringList() << AVATAR_SETTINGS_GROUP_NAME << "dominantHand", DOMINANT_RIGHT_HAND), + _hmdAvatarAlignmentTypeSetting(QStringList() << AVATAR_SETTINGS_GROUP_NAME << "hmdAvatarAlignmentType", DEFAULT_HMD_AVATAR_ALIGNMENT_TYPE), _headPitchSetting(QStringList() << AVATAR_SETTINGS_GROUP_NAME << "", 0.0f), _scaleSetting(QStringList() << AVATAR_SETTINGS_GROUP_NAME << "scale", _targetScale), _yawSpeedSetting(QStringList() << AVATAR_SETTINGS_GROUP_NAME << "yawSpeed", _yawSpeed), @@ -138,7 +170,8 @@ MyAvatar::MyAvatar(QThread* thread) : _useSnapTurnSetting(QStringList() << AVATAR_SETTINGS_GROUP_NAME << "useSnapTurn", _useSnapTurn), _userHeightSetting(QStringList() << AVATAR_SETTINGS_GROUP_NAME << "userHeight", DEFAULT_AVATAR_HEIGHT), _flyingHMDSetting(QStringList() << AVATAR_SETTINGS_GROUP_NAME << "flyingHMD", _flyingPrefHMD), - _avatarEntityCountSetting(QStringList() << AVATAR_SETTINGS_GROUP_NAME << "avatarEntityData" << "size", 0) + _avatarEntityCountSetting(QStringList() << AVATAR_SETTINGS_GROUP_NAME << "avatarEntityData" << "size", 0), + _userRecenterModelSetting(QStringList() << AVATAR_SETTINGS_GROUP_NAME << "userRecenterModel", USER_RECENTER_MODEL_AUTO) { _clientTraitsHandler.reset(new ClientTraitsHandler(this)); @@ -205,12 +238,12 @@ MyAvatar::MyAvatar(QThread* thread) : if (recordingInterface->getPlayFromCurrentLocation()) { setRecordingBasis(); } - _previousCollisionGroup = _characterController.computeCollisionGroup(); + _previousCollisionMask = _characterController.computeCollisionMask(); _characterController.setCollisionless(true); } else { clearRecordingBasis(); useFullAvatarURL(_fullAvatarURLFromPreferences, _fullAvatarModelName); - if (_previousCollisionGroup != BULLET_COLLISION_GROUP_COLLISIONLESS) { + if (_previousCollisionMask != BULLET_COLLISION_MASK_COLLISIONLESS) { _characterController.setCollisionless(false); } } @@ -286,10 +319,25 @@ MyAvatar::~MyAvatar() { _myScriptEngine = nullptr; } +QString MyAvatar::getDominantHand() const { + return _dominantHand.get(); +} + void MyAvatar::setDominantHand(const QString& hand) { if (hand == DOMINANT_LEFT_HAND || hand == DOMINANT_RIGHT_HAND) { - _dominantHand = hand; - emit dominantHandChanged(_dominantHand); + _dominantHand.set(hand); + emit dominantHandChanged(hand); + } +} + +QString MyAvatar::getHmdAvatarAlignmentType() const { + return _hmdAvatarAlignmentType.get(); +} + +void MyAvatar::setHmdAvatarAlignmentType(const QString& type) { + if (type != _hmdAvatarAlignmentType.get()) { + _hmdAvatarAlignmentType.set(type); + emit hmdAvatarAlignmentTypeChanged(type); } } @@ -377,6 +425,7 @@ void MyAvatar::resetSensorsAndBody() { if (QThread::currentThread() != thread()) { QMetaObject::invokeMethod(this, "resetSensorsAndBody"); return; + } qApp->getActiveDisplayPlugin()->resetSensors(); @@ -668,12 +717,6 @@ void MyAvatar::update(float deltaTime) { Q_ARG(glm::vec3, (getWorldPosition() - halfBoundingBoxDimensions)), Q_ARG(glm::vec3, (halfBoundingBoxDimensions*2.0f))); - if (getIdentityDataChanged()) { - sendIdentityPacket(); - } - - _clientTraitsHandler->sendChangedTraitsToMixer(); - simulate(deltaTime, true); currentEnergy += energyChargeRate; @@ -773,7 +816,7 @@ void MyAvatar::simulate(float deltaTime, bool inView) { auto headBoneSet = _skeletonModel->getCauterizeBoneSet(); forEachChild([&](SpatiallyNestablePointer object) { bool isChildOfHead = headBoneSet.find(object->getParentJointIndex()) != headBoneSet.end(); - if (isChildOfHead) { + if (isChildOfHead && !object->hasGrabs()) { // Cauterize or display children of head per head drawing state. updateChildCauterization(object, !_prevShouldDrawHead); object->forEachDescendant([&](SpatiallyNestablePointer descendant) { @@ -823,7 +866,9 @@ void MyAvatar::simulate(float deltaTime, bool inView) { // and all of its joints, now update our attachements. Avatar::simulateAttachments(deltaTime); relayJointDataToChildren(); - updateGrabs(); + if (updateGrabs()) { + _cauterizationNeedsUpdate = true; + } if (!_skeletonModel->hasSkeleton()) { // All the simulation that can be done has been done @@ -879,9 +924,13 @@ void MyAvatar::simulate(float deltaTime, bool inView) { collisionlessAllowed = zone->getGhostingAllowed(); } EntityEditPacketSender* packetSender = qApp->getEntityEditPacketSender(); - bool force = false; - bool iShouldTellServer = true; forEachDescendant([&](SpatiallyNestablePointer object) { + // we need to update attached queryAACubes in our own local tree so point-select always works + // however we don't want to flood the update pipeline with AvatarEntity updates, so we assume + // others have all info required to properly update queryAACube of AvatarEntities on their end + EntityItemPointer entity = std::dynamic_pointer_cast(object); + bool iShouldTellServer = !(entity && entity->isAvatarEntity()); + const bool force = false; entityTree->updateEntityQueryAACube(object, packetSender, force, iShouldTellServer); }); }); @@ -1277,7 +1326,8 @@ void MyAvatar::resizeAvatarEntitySettingHandles(uint32_t maxIndex) { } void MyAvatar::saveData() { - _dominantHandSetting.set(_dominantHand); + _dominantHandSetting.set(getDominantHand()); + _hmdAvatarAlignmentTypeSetting.set(getHmdAvatarAlignmentType()); _headPitchSetting.set(getHead()->getBasePitch()); _scaleSetting.set(_targetScale); _yawSpeedSetting.set(_yawSpeed); @@ -1300,6 +1350,7 @@ void MyAvatar::saveData() { _useSnapTurnSetting.set(_useSnapTurn); _userHeightSetting.set(getUserHeight()); _flyingHMDSetting.set(getFlyingHMDPref()); + _userRecenterModelSetting.set(userRecenterModelToString(getUserRecenterModel())); auto hmdInterface = DependencyManager::get(); saveAvatarEntityDataToSettings(); @@ -1610,10 +1661,12 @@ void MyAvatar::handleChangedAvatarEntityData() { skip = true; } }); - sanitizeAvatarEntityProperties(properties); - entityTree->withWriteLock([&] { - entityTree->updateEntity(id, properties); - }); + if (!skip) { + sanitizeAvatarEntityProperties(properties); + entityTree->withWriteLock([&] { + entityTree->updateEntity(id, properties); + }); + } } // DELETE cached blobs @@ -1810,19 +1863,27 @@ void MyAvatar::setAvatarEntityData(const AvatarEntityMap& avatarEntityData) { void MyAvatar::updateAvatarEntity(const QUuid& entityID, const QByteArray& entityData) { // NOTE: this is an invokable Script call - // TODO: we should handle the case where entityData is corrupt or invalid - // BEFORE we store into _cachedAvatarEntityBlobs - _needToSaveAvatarEntitySettings = true; + bool changed = false; _avatarEntitiesLock.withWriteLock([&] { - AvatarEntityMap::iterator itr = _cachedAvatarEntityBlobs.find(entityID); - if (itr != _cachedAvatarEntityBlobs.end()) { - _entitiesToUpdate.push_back(entityID); - itr.value() = entityData; + auto data = QJsonDocument::fromBinaryData(entityData); + if (data.isEmpty() || data.isNull() || !data.isObject() || data.object().isEmpty()) { + qDebug() << "ERROR! Trying to update with invalid avatar entity data. Skipping." << data; } else { - _entitiesToAdd.push_back(entityID); - _cachedAvatarEntityBlobs.insert(entityID, entityData); + auto itr = _cachedAvatarEntityBlobs.find(entityID); + if (itr == _cachedAvatarEntityBlobs.end()) { + _entitiesToAdd.push_back(entityID); + _cachedAvatarEntityBlobs.insert(entityID, entityData); + changed = true; + } else { + _entitiesToUpdate.push_back(entityID); + itr.value() = entityData; + changed = true; + } } }); + if (changed) { + _needToSaveAvatarEntitySettings = true; + } } void MyAvatar::avatarEntityDataToJson(QJsonObject& root) const { @@ -1872,9 +1933,12 @@ void MyAvatar::loadData() { setCollisionSoundURL(_collisionSoundURLSetting.get(QUrl(DEFAULT_AVATAR_COLLISION_SOUND_URL)).toString()); setSnapTurn(_useSnapTurnSetting.get()); setDominantHand(_dominantHandSetting.get(DOMINANT_RIGHT_HAND).toLower()); + setHmdAvatarAlignmentType(_hmdAvatarAlignmentTypeSetting.get(DEFAULT_HMD_AVATAR_ALIGNMENT_TYPE).toLower()); setUserHeight(_userHeightSetting.get(DEFAULT_AVATAR_HEIGHT)); setTargetScale(_scaleSetting.get()); + setUserRecenterModel(stringToUserRecenterModel(_userRecenterModelSetting.get(USER_RECENTER_MODEL_AUTO))); + setEnableMeshVisible(Menu::getInstance()->isOptionChecked(MenuOption::MeshVisible)); _follow.setToggleHipsFollowing (Menu::getInstance()->isOptionChecked(MenuOption::ToggleHipsFollowing)); setEnableDebugDrawBaseOfSupport(Menu::getInstance()->isOptionChecked(MenuOption::AnimDebugDrawBaseOfSupport)); @@ -2362,50 +2426,36 @@ bool isWearableEntity(const EntityItemPointer& entity) { || entity->getParentID() == AVATAR_SELF_ID); } -void MyAvatar::clearAvatarEntities() { +void MyAvatar::removeWornAvatarEntity(const EntityItemID& entityID) { auto treeRenderer = DependencyManager::get(); EntityTreePointer entityTree = treeRenderer ? treeRenderer->getTree() : nullptr; - QList avatarEntityIDs; - _avatarEntitiesLock.withReadLock([&] { - avatarEntityIDs = _packedAvatarEntityData.keys(); - }); - for (const auto& entityID : avatarEntityIDs) { - entityTree->withWriteLock([&entityID, &entityTree] { - // remove this entity first from the entity tree - entityTree->deleteEntity(entityID, true, true); - }); - - // remove the avatar entity from our internal list - // (but indicate it doesn't need to be pulled from the tree) - clearAvatarEntity(entityID, false); - } -} - -void MyAvatar::removeWearableAvatarEntities() { - auto treeRenderer = DependencyManager::get(); - EntityTreePointer entityTree = treeRenderer ? treeRenderer->getTree() : nullptr; if (entityTree) { - QList avatarEntityIDs; - _avatarEntitiesLock.withReadLock([&] { - avatarEntityIDs = _packedAvatarEntityData.keys(); - }); - for (const auto& entityID : avatarEntityIDs) { - auto entity = entityTree->findEntityByID(entityID); - if (entity && isWearableEntity(entity)) { - entityTree->withWriteLock([&entityID, &entityTree] { - // remove this entity first from the entity tree - entityTree->deleteEntity(entityID, true, true); - }); + auto entity = entityTree->findEntityByID(entityID); + if (entity && isWearableEntity(entity)) { + entityTree->withWriteLock([&entityID, &entityTree] { + // remove this entity first from the entity tree + entityTree->deleteEntity(entityID, true, true); + }); - // remove the avatar entity from our internal list - // (but indicate it doesn't need to be pulled from the tree) - clearAvatarEntity(entityID, false); - } + // remove the avatar entity from our internal list + // (but indicate it doesn't need to be pulled from the tree) + clearAvatarEntity(entityID, false); } } } +void MyAvatar::clearWornAvatarEntities() { + QList avatarEntityIDs; + _avatarEntitiesLock.withReadLock([&] { + avatarEntityIDs = _packedAvatarEntityData.keys(); + }); + for (auto entityID : avatarEntityIDs) { + removeWornAvatarEntity(entityID); + } +} + + QVariantList MyAvatar::getAvatarEntitiesVariant() { // NOTE: this method is NOT efficient QVariantList avatarEntitiesData; @@ -2428,6 +2478,7 @@ QVariantList MyAvatar::getAvatarEntitiesVariant() { desiredProperties += PROP_LOCAL_ROTATION; EntityItemProperties entityProperties = entity->getProperties(desiredProperties); QScriptValue scriptProperties = EntityItemPropertiesToScriptValue(_myScriptEngine, entityProperties); + avatarEntityData["id"] = entityID; avatarEntityData["properties"] = scriptProperties.toVariant(); avatarEntitiesData.append(QVariant(avatarEntityData)); } @@ -2537,7 +2588,7 @@ void MyAvatar::updateMotors() { float verticalMotorTimescale; if (_characterController.getState() == CharacterController::State::Hover || - _characterController.computeCollisionGroup() == BULLET_COLLISION_GROUP_COLLISIONLESS) { + _characterController.computeCollisionMask() == BULLET_COLLISION_MASK_COLLISIONLESS) { horizontalMotorTimescale = FLYING_MOTOR_TIMESCALE; verticalMotorTimescale = FLYING_MOTOR_TIMESCALE; } else { @@ -2547,7 +2598,7 @@ void MyAvatar::updateMotors() { if (_motionBehaviors & AVATAR_MOTION_ACTION_MOTOR_ENABLED) { if (_characterController.getState() == CharacterController::State::Hover || - _characterController.computeCollisionGroup() == BULLET_COLLISION_GROUP_COLLISIONLESS) { + _characterController.computeCollisionMask() == BULLET_COLLISION_MASK_COLLISIONLESS) { motorRotation = getMyHead()->getHeadOrientation(); } else { // non-hovering = walking: follow camera twist about vertical but not lift @@ -2602,7 +2653,7 @@ void MyAvatar::prepareForPhysicsSimulation() { qDebug() << "Warning: getParentVelocity failed" << getID(); parentVelocity = glm::vec3(); } - _characterController.handleChangedCollisionGroup(); + _characterController.handleChangedCollisionMask(); _characterController.setParentVelocity(parentVelocity); _characterController.setScaleFactor(getSensorToWorldScale()); @@ -2806,8 +2857,8 @@ void MyAvatar::setAttachmentData(const QVector& attachmentData) newEntitiesProperties.push_back(properties); } - // clear any existing avatar entities - clearAvatarEntities(); + // clear any existing wearables + clearWornAvatarEntities(); for (auto& properties : newEntitiesProperties) { DependencyManager::get()->addEntity(properties, true); @@ -3106,6 +3157,39 @@ void MyAvatar::preDisplaySide(const RenderArgs* renderArgs) { _prevShouldDrawHead = shouldDrawHead; } +int MyAvatar::sendAvatarDataPacket(bool sendAll) { + using namespace std::chrono; + auto now = Clock::now(); + + int MAX_DATA_RATE_MBPS = 3; + int maxDataRateBytesPerSeconds = MAX_DATA_RATE_MBPS * BYTES_PER_KILOBYTE * KILO_PER_MEGA / BITS_IN_BYTE; + int maxDataRateBytesPerMilliseconds = maxDataRateBytesPerSeconds / MSECS_PER_SECOND; + + auto bytesSent = 0; + + if (now > _nextTraitsSendWindow) { + if (getIdentityDataChanged()) { + bytesSent += sendIdentityPacket(); + } + + bytesSent += _clientTraitsHandler->sendChangedTraitsToMixer(); + + // Compute the next send window based on how much data we sent and what + // data rate we're trying to max at. + milliseconds timeUntilNextSend { bytesSent / maxDataRateBytesPerMilliseconds }; + _nextTraitsSendWindow += timeUntilNextSend; + + // Don't let the next send window lag behind if we're not sending a lot of data. + if (_nextTraitsSendWindow < now) { + _nextTraitsSendWindow = now; + } + } + + bytesSent += Avatar::sendAvatarDataPacket(sendAll); + + return bytesSent; +} + const float RENDER_HEAD_CUTOFF_DISTANCE = 0.47f; bool MyAvatar::cameraInsideHead(const glm::vec3& cameraPosition) const { @@ -3249,7 +3333,7 @@ void MyAvatar::updateOrientation(float deltaTime) { head->setBaseRoll(ROLL(euler)); } else { head->setBaseYaw(0.0f); - head->setBasePitch(getHead()->getBasePitch() + getDriveKey(PITCH) * _pitchSpeed * deltaTime + head->setBasePitch(getHead()->getBasePitch() + getDriveKey(PITCH) * _pitchSpeed * deltaTime + getDriveKey(DELTA_PITCH) * _pitchSpeed / PITCH_SPEED_DEFAULT); head->setBaseRoll(0.0f); } @@ -3295,7 +3379,7 @@ void MyAvatar::updateActionMotor(float deltaTime) { glm::vec3 direction = forward + right; if (state == CharacterController::State::Hover || - _characterController.computeCollisionGroup() == BULLET_COLLISION_GROUP_COLLISIONLESS) { + _characterController.computeCollisionMask() == BULLET_COLLISION_MASK_COLLISIONLESS) { glm::vec3 up = (getDriveKey(TRANSLATE_Y)) * IDENTITY_UP; direction += up; } @@ -3851,7 +3935,7 @@ void MyAvatar::setCollisionsEnabled(bool enabled) { bool MyAvatar::getCollisionsEnabled() { // may return 'false' even though the collisionless option was requested // because the zone may disallow collisionless avatars - return _characterController.computeCollisionGroup() != BULLET_COLLISION_GROUP_COLLISIONLESS; + return _characterController.computeCollisionMask() != BULLET_COLLISION_MASK_COLLISIONLESS; } void MyAvatar::setOtherAvatarsCollisionsEnabled(bool enabled) { @@ -3860,7 +3944,11 @@ void MyAvatar::setOtherAvatarsCollisionsEnabled(bool enabled) { QMetaObject::invokeMethod(this, "setOtherAvatarsCollisionsEnabled", Q_ARG(bool, enabled)); return; } + bool change = _collideWithOtherAvatars != enabled; _collideWithOtherAvatars = enabled; + if (change) { + setCollisionWithOtherAvatarsFlags(); + } emit otherAvatarsCollisionsEnabledChanged(enabled); } @@ -3868,6 +3956,11 @@ bool MyAvatar::getOtherAvatarsCollisionsEnabled() { return _collideWithOtherAvatars; } +void MyAvatar::setCollisionWithOtherAvatarsFlags() { + _characterController.setCollideWithOtherAvatars(_collideWithOtherAvatars); + _characterController.setPendingFlagsUpdateCollisionMask(); +} + void MyAvatar::updateCollisionCapsuleCache() { glm::vec3 start, end; float radius; diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h old mode 100644 new mode 100755 index da7f7c47f9..91e1b67fcc --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -252,6 +252,10 @@ class MyAvatar : public Avatar { const QString DOMINANT_LEFT_HAND = "left"; const QString DOMINANT_RIGHT_HAND = "right"; + const QString DEFAULT_HMD_AVATAR_ALIGNMENT_TYPE = "head"; + + using Clock = std::chrono::system_clock; + using TimePoint = Clock::time_point; public: enum DriveKeys { @@ -294,6 +298,8 @@ public: void reset(bool andRecenter = false, bool andReload = true, bool andHead = true); + void setCollisionWithOtherAvatarsFlags() override; + /**jsdoc * @function MyAvatar.resetSensorsAndBody */ @@ -514,7 +520,18 @@ public: * @function MyAvatar.getDominantHand * @returns {string} */ - Q_INVOKABLE QString getDominantHand() const { return _dominantHand; } + Q_INVOKABLE QString getDominantHand() const; + + /**jsdoc + * @function MyAvatar.setHmdAvatarAlignmentType + * @param {string} hand + */ + Q_INVOKABLE void setHmdAvatarAlignmentType(const QString& hand); + /**jsdoc + * @function MyAvatar.setHmdAvatarAlignmentType + * @returns {string} + */ + Q_INVOKABLE QString getHmdAvatarAlignmentType() const; /**jsdoc * @function MyAvatar.setCenterOfGravityModelEnabled @@ -970,8 +987,8 @@ public: * @returns {object[]} */ Q_INVOKABLE QVariantList getAvatarEntitiesVariant(); - void clearAvatarEntities(); - void removeWearableAvatarEntities(); + void removeWornAvatarEntity(const EntityItemID& entityID); + void clearWornAvatarEntities(); /**jsdoc * Check whether your avatar is flying or not. @@ -1213,6 +1230,7 @@ public: void setAvatarEntityData(const AvatarEntityMap& avatarEntityData) override; void updateAvatarEntity(const QUuid& entityID, const QByteArray& entityData) override; void avatarEntityDataToJson(QJsonObject& root) const override; + int sendAvatarDataPacket(bool sendAll = false) override; public slots: @@ -1579,6 +1597,13 @@ signals: */ void dominantHandChanged(const QString& hand); + /**jsdoc + * @function MyAvatar.hmdAvatarAlignmentTypeChanged + * @param {string} type + * @returns {Signal} + */ + void hmdAvatarAlignmentTypeChanged(const QString& type); + /**jsdoc * @function MyAvatar.sensorToWorldScaleChanged * @param {number} scale @@ -1728,7 +1753,7 @@ private: SharedSoundPointer _collisionSound; MyCharacterController _characterController; - int32_t _previousCollisionGroup { BULLET_COLLISION_GROUP_MY_AVATAR }; + int32_t _previousCollisionMask { BULLET_COLLISION_MASK_MY_AVATAR }; AvatarWeakPointer _lookAtTargetAvatar; glm::vec3 _targetAvatarPosition; @@ -1767,7 +1792,8 @@ private: ThreadSafeValueCache _prefOverrideAnimGraphUrl; QUrl _fstAnimGraphOverrideUrl; bool _useSnapTurn { true }; - QString _dominantHand { DOMINANT_RIGHT_HAND }; + ThreadSafeValueCache _dominantHand { DOMINANT_RIGHT_HAND }; + ThreadSafeValueCache _hmdAvatarAlignmentType { DEFAULT_HMD_AVATAR_ALIGNMENT_TYPE }; const float ROLL_CONTROL_DEAD_ZONE_DEFAULT = 8.0f; // degrees const float ROLL_CONTROL_RATE_DEFAULT = 114.0f; // degrees / sec @@ -1937,7 +1963,10 @@ private: bool _skeletonModelLoaded { false }; bool _reloadAvatarEntityDataFromSettings { true }; + TimePoint _nextTraitsSendWindow; + Setting::Handle _dominantHandSetting; + Setting::Handle _hmdAvatarAlignmentTypeSetting; Setting::Handle _headPitchSetting; Setting::Handle _scaleSetting; Setting::Handle _yawSpeedSetting; @@ -1954,6 +1983,7 @@ private: Setting::Handle _allowTeleportingSetting { "allowTeleporting", true }; std::vector> _avatarEntityIDSettings; std::vector> _avatarEntityDataSettings; + Setting::Handle _userRecenterModelSetting; // AvatarEntities stuff: // We cache the "map of unfortunately-formatted-binary-blobs" because they are expensive to compute diff --git a/interface/src/avatar/MyCharacterController.cpp b/interface/src/avatar/MyCharacterController.cpp index 798dbc91ed..821b01c2c6 100755 --- a/interface/src/avatar/MyCharacterController.cpp +++ b/interface/src/avatar/MyCharacterController.cpp @@ -202,6 +202,29 @@ bool MyCharacterController::testRayShotgun(const glm::vec3& position, const glm: return result.hitFraction < 1.0f; } +int32_t MyCharacterController::computeCollisionMask() const { + int32_t collisionMask = BULLET_COLLISION_MASK_MY_AVATAR; + if (_collisionless && _collisionlessAllowed) { + collisionMask = BULLET_COLLISION_MASK_COLLISIONLESS; + } else if (!_collideWithOtherAvatars) { + collisionMask &= ~BULLET_COLLISION_GROUP_OTHER_AVATAR; + } + return collisionMask; +} + +void MyCharacterController::handleChangedCollisionMask() { + if (_pendingFlags & PENDING_FLAG_UPDATE_COLLISION_MASK) { + // ATM the easiest way to update collision groups/masks is to remove/re-add the RigidBody + if (_dynamicsWorld) { + _dynamicsWorld->removeRigidBody(_rigidBody); + int32_t collisionMask = computeCollisionMask(); + _dynamicsWorld->addRigidBody(_rigidBody, BULLET_COLLISION_GROUP_MY_AVATAR, collisionMask); + } + _pendingFlags &= ~PENDING_FLAG_UPDATE_COLLISION_MASK; + updateCurrentGravity(); + } +} + btConvexHullShape* MyCharacterController::computeShape() const { // HACK: the avatar collides using convex hull with a collision margin equal to // the old capsule radius. Two points define a capsule and additional points are diff --git a/interface/src/avatar/MyCharacterController.h b/interface/src/avatar/MyCharacterController.h old mode 100644 new mode 100755 index fd9caface2..76fe588e71 --- a/interface/src/avatar/MyCharacterController.h +++ b/interface/src/avatar/MyCharacterController.h @@ -42,6 +42,12 @@ public: void setDensity(btScalar density) { _density = density; } + int32_t computeCollisionMask() const override; + void handleChangedCollisionMask() override; + + bool _collideWithOtherAvatars{ true }; + void setCollideWithOtherAvatars(bool collideWithOtherAvatars) { _collideWithOtherAvatars = collideWithOtherAvatars; } + protected: void initRayShotgun(const btCollisionWorld* world); void updateMassProperties() override; diff --git a/interface/src/avatar/MySkeletonModel.cpp b/interface/src/avatar/MySkeletonModel.cpp old mode 100644 new mode 100755 index 356b365f93..26d69841d0 --- a/interface/src/avatar/MySkeletonModel.cpp +++ b/interface/src/avatar/MySkeletonModel.cpp @@ -187,7 +187,7 @@ void MySkeletonModel::updateRig(float deltaTime, glm::mat4 parentTransform) { } } - bool isFlying = (myAvatar->getCharacterController()->getState() == CharacterController::State::Hover || myAvatar->getCharacterController()->computeCollisionGroup() == BULLET_COLLISION_GROUP_COLLISIONLESS); + bool isFlying = (myAvatar->getCharacterController()->getState() == CharacterController::State::Hover || myAvatar->getCharacterController()->computeCollisionMask() == BULLET_COLLISION_MASK_COLLISIONLESS); if (isFlying != _prevIsFlying) { const float FLY_TO_IDLE_HIPS_TRANSITION_TIME = 0.5f; _flyIdleTimer = FLY_TO_IDLE_HIPS_TRANSITION_TIME; @@ -198,7 +198,7 @@ void MySkeletonModel::updateRig(float deltaTime, glm::mat4 parentTransform) { // if hips are not under direct control, estimate the hips position. if (avatarHeadPose.isValid() && !(params.primaryControllerFlags[Rig::PrimaryControllerType_Hips] & (uint8_t)Rig::ControllerFlags::Enabled)) { - bool isFlying = (myAvatar->getCharacterController()->getState() == CharacterController::State::Hover || myAvatar->getCharacterController()->computeCollisionGroup() == BULLET_COLLISION_GROUP_COLLISIONLESS); + bool isFlying = (myAvatar->getCharacterController()->getState() == CharacterController::State::Hover || myAvatar->getCharacterController()->computeCollisionMask() == BULLET_COLLISION_MASK_COLLISIONLESS); // timescale in seconds const float TRANS_HORIZ_TIMESCALE = 0.15f; diff --git a/interface/src/avatar/OtherAvatar.cpp b/interface/src/avatar/OtherAvatar.cpp old mode 100644 new mode 100755 index 0dfc349e18..754d914135 --- a/interface/src/avatar/OtherAvatar.cpp +++ b/interface/src/avatar/OtherAvatar.cpp @@ -46,9 +46,6 @@ OtherAvatar::OtherAvatar(QThread* thread) : Avatar(thread) { connect(_skeletonModel.get(), &Model::setURLFinished, this, &Avatar::setModelURLFinished); connect(_skeletonModel.get(), &Model::rigReady, this, &Avatar::rigReady); connect(_skeletonModel.get(), &Model::rigReset, this, &Avatar::rigReset); - - // add the purple orb - createOrb(); } OtherAvatar::~OtherAvatar() { @@ -138,17 +135,9 @@ void OtherAvatar::rebuildCollisionShape() { } } -void OtherAvatar::updateCollisionGroup(bool myAvatarCollide) { +void OtherAvatar::setCollisionWithOtherAvatarsFlags() { if (_motionState) { - bool collides = _motionState->getCollisionGroup() == BULLET_COLLISION_GROUP_OTHER_AVATAR && myAvatarCollide; - if (_collideWithOtherAvatars != collides) { - if (!myAvatarCollide) { - _collideWithOtherAvatars = false; - } - auto newCollisionGroup = _collideWithOtherAvatars ? BULLET_COLLISION_GROUP_OTHER_AVATAR : BULLET_COLLISION_GROUP_COLLISIONLESS; - _motionState->setCollisionGroup(newCollisionGroup); - _motionState->addDirtyFlags(Simulation::DIRTY_COLLISION_GROUP); - } + _motionState->addDirtyFlags(Simulation::DIRTY_COLLISION_GROUP); } } diff --git a/interface/src/avatar/OtherAvatar.h b/interface/src/avatar/OtherAvatar.h old mode 100644 new mode 100755 index a1dc5724a9..969f551783 --- a/interface/src/avatar/OtherAvatar.h +++ b/interface/src/avatar/OtherAvatar.h @@ -46,7 +46,9 @@ public: bool shouldBeInPhysicsSimulation() const; bool needsPhysicsUpdate() const; - void updateCollisionGroup(bool myAvatarCollide); + bool getCollideWithOtherAvatars() const { return _collideWithOtherAvatars; } + + void setCollisionWithOtherAvatarsFlags() override; void simulate(float deltaTime, bool inView) override; diff --git a/interface/src/commerce/QmlCommerce.cpp b/interface/src/commerce/QmlCommerce.cpp index 00acd40e70..5236c5a7fb 100644 --- a/interface/src/commerce/QmlCommerce.cpp +++ b/interface/src/commerce/QmlCommerce.cpp @@ -22,7 +22,9 @@ #include #include "scripting/HMDScriptingInterface.h" -QmlCommerce::QmlCommerce() { +QmlCommerce::QmlCommerce() : + _appsPath(PathUtils::getAppDataPath() + "Apps/") +{ auto ledger = DependencyManager::get(); auto wallet = DependencyManager::get(); connect(ledger.data(), &Ledger::buyResult, this, &QmlCommerce::buyResult); @@ -44,22 +46,18 @@ QmlCommerce::QmlCommerce() { auto accountManager = DependencyManager::get(); connect(accountManager.data(), &AccountManager::usernameChanged, this, [&]() { setPassphrase(""); }); - - _appsPath = PathUtils::getAppDataPath() + "Apps/"; } - - void QmlCommerce::openSystemApp(const QString& appName) { - static QMap systemApps { + static const QMap systemApps { {"GOTO", "hifi/tablet/TabletAddressDialog.qml"}, {"PEOPLE", "hifi/Pal.qml"}, {"WALLET", "hifi/commerce/wallet/Wallet.qml"}, {"MARKET", "/marketplace.html"} }; - static QMap systemInject{ + static const QMap systemInject{ {"MARKET", "/scripts/system/html/js/marketplacesInject.js"} }; diff --git a/interface/src/commerce/QmlCommerce.h b/interface/src/commerce/QmlCommerce.h index ad21899ebf..3217b8a1f9 100644 --- a/interface/src/commerce/QmlCommerce.h +++ b/interface/src/commerce/QmlCommerce.h @@ -19,7 +19,9 @@ #include -class QmlCommerce : public QObject { +#include + +class QmlCommerce : public QObject, public Dependency { Q_OBJECT public: @@ -98,7 +100,7 @@ protected: Q_INVOKABLE void updateItem(const QString& certificateId); private: - QString _appsPath; + const QString _appsPath; }; #endif // hifi_QmlCommerce_h diff --git a/interface/src/ui/LoginDialog.cpp b/interface/src/ui/LoginDialog.cpp index 6e9c91785f..4359318833 100644 --- a/interface/src/ui/LoginDialog.cpp +++ b/interface/src/ui/LoginDialog.cpp @@ -18,6 +18,7 @@ #include #include +#include #include #include #include @@ -109,8 +110,16 @@ bool LoginDialog::isSteamRunning() const { return steamClient && steamClient->isRunning(); } -bool LoginDialog::isOculusStoreRunning() const { - return qApp->property(hifi::properties::OCULUS_STORE).toBool(); +bool LoginDialog::isOculusRunning() const { + auto oculusPlatformPlugin = PluginManager::getInstance()->getOculusPlatformPlugin(); + return (oculusPlatformPlugin && oculusPlatformPlugin->isRunning()); +} + +QString LoginDialog::oculusUserID() const { + if (auto oculusPlatformPlugin = PluginManager::getInstance()->getOculusPlatformPlugin()) { + return oculusPlatformPlugin->getOculusUserID(); + } + return ""; } void LoginDialog::dismissLoginDialog() { @@ -126,6 +135,79 @@ void LoginDialog::login(const QString& username, const QString& password) const DependencyManager::get()->requestAccessToken(username, password); } +void LoginDialog::loginThroughOculus() { + qDebug() << "Attempting to login through Oculus"; + if (auto oculusPlatformPlugin = PluginManager::getInstance()->getOculusPlatformPlugin()) { + oculusPlatformPlugin->requestNonceAndUserID([this] (QString nonce, QString oculusID) { + DependencyManager::get()->requestAccessTokenWithOculus(nonce, oculusID); + }); + } +} + +void LoginDialog::linkOculus() { + qDebug() << "Attempting to link Oculus account"; + if (auto oculusPlatformPlugin = PluginManager::getInstance()->getOculusPlatformPlugin()) { + oculusPlatformPlugin->requestNonceAndUserID([this] (QString nonce, QString oculusID) { + if (nonce.isEmpty() || oculusID.isEmpty()) { + emit handleLoginFailed(); + return; + } + + JSONCallbackParameters callbackParams; + callbackParams.callbackReceiver = this; + callbackParams.jsonCallbackMethod = "linkCompleted"; + callbackParams.errorCallbackMethod = "linkFailed"; + const QString LINK_OCULUS_PATH = "api/v1/user/oculus/link"; + + QJsonObject payload; + payload["oculus_nonce"] = nonce; + payload["oculus_id"] = oculusID; + + auto accountManager = DependencyManager::get(); + accountManager->sendRequest(LINK_OCULUS_PATH, AccountManagerAuth::Required, + QNetworkAccessManager::PostOperation, callbackParams, + QJsonDocument(payload).toJson()); + }); + } +} + +void LoginDialog::createAccountFromOculus(QString email, QString username, QString password) { + qDebug() << "Attempting to create account from Oculus info"; + if (auto oculusPlatformPlugin = PluginManager::getInstance()->getOculusPlatformPlugin()) { + oculusPlatformPlugin->requestNonceAndUserID([this, email, username, password] (QString nonce, QString oculusID) { + if (nonce.isEmpty() || oculusID.isEmpty()) { + emit handleLoginFailed(); + return; + } + + JSONCallbackParameters callbackParams; + callbackParams.callbackReceiver = this; + callbackParams.jsonCallbackMethod = "createCompleted"; + callbackParams.errorCallbackMethod = "createFailed"; + + const QString CREATE_ACCOUNT_FROM_OCULUS_PATH = "api/v1/user/oculus/create"; + + QJsonObject payload; + payload["oculus_nonce"] = nonce; + payload["oculus_id"] = oculusID; + if (!email.isEmpty()) { + payload["email"] = email; + } + if (!username.isEmpty()) { + payload["username"] = username; + } + if (!password.isEmpty()) { + payload["password"] = password; + } + + auto accountManager = DependencyManager::get(); + accountManager->sendRequest(CREATE_ACCOUNT_FROM_OCULUS_PATH, AccountManagerAuth::None, + QNetworkAccessManager::PostOperation, callbackParams, + QJsonDocument(payload).toJson()); + }); + } +} + void LoginDialog::loginThroughSteam() { qDebug() << "Attempting to login through Steam"; if (auto steamClient = PluginManager::getInstance()->getSteamClientPlugin()) { @@ -157,7 +239,7 @@ void LoginDialog::linkSteam() { const QString LINK_STEAM_PATH = "api/v1/user/steam/link"; QJsonObject payload; - payload.insert("steam_auth_ticket", QJsonValue::fromVariant(QVariant(ticket))); + payload["steam_auth_ticket"] = QJsonValue::fromVariant(QVariant(ticket)); auto accountManager = DependencyManager::get(); accountManager->sendRequest(LINK_STEAM_PATH, AccountManagerAuth::Required, @@ -184,9 +266,9 @@ void LoginDialog::createAccountFromSteam(QString username) { const QString CREATE_ACCOUNT_FROM_STEAM_PATH = "api/v1/user/steam/create"; QJsonObject payload; - payload.insert("steam_auth_ticket", QJsonValue::fromVariant(QVariant(ticket))); + payload["steam_auth_ticket"] = QJsonValue::fromVariant(QVariant(ticket)); if (!username.isEmpty()) { - payload.insert("username", QJsonValue::fromVariant(QVariant(username))); + payload["username"] = username; } auto accountManager = DependencyManager::get(); @@ -214,6 +296,45 @@ void LoginDialog::createCompleted(QNetworkReply* reply) { } void LoginDialog::createFailed(QNetworkReply* reply) { + if (isOculusRunning()) { + auto replyData = reply->readAll(); + QJsonParseError parseError; + auto doc = QJsonDocument::fromJson(replyData, &parseError); + if (parseError.error != QJsonParseError::NoError) { + emit handleCreateFailed(reply->errorString()); + return; + } + auto data = doc["data"]; + auto error = data["error"]; + auto oculusError = data["oculus"]; + auto user = error["username"].toArray(); + auto uid = error["uid"].toArray(); + auto email = error["email"].toArray(); + auto password = error["password"].toArray(); + QString reply; + if (uid[0].isString()) { + emit handleCreateFailed("Oculus ID " + uid[0].toString() + "."); + return; + } + if (user[0].isString()) { + reply = "Username " + user[0].toString() + "."; + } + if (email[0].isString()) { + reply.append((!reply.isEmpty()) ? "\n" : ""); + reply.append("Email " + email[0].toString() + "."); + } + if (password[0].isString()) { + reply.append((!reply.isEmpty()) ? "\n" : ""); + reply.append("Password " + password[0].toString() + "."); + } + if (!oculusError.isNull() && !oculusError.isUndefined()) { + emit handleCreateFailed("Could not verify token with Oculus. Please try again."); + return; + } else { + emit handleCreateFailed(reply); + return; + } + } emit handleCreateFailed(reply->errorString()); } diff --git a/interface/src/ui/LoginDialog.h b/interface/src/ui/LoginDialog.h index 2714d654bf..7c932932cf 100644 --- a/interface/src/ui/LoginDialog.h +++ b/interface/src/ui/LoginDialog.h @@ -22,7 +22,6 @@ extern const QUrl OVERLAY_LOGIN_DIALOG; class LoginDialog : public OffscreenQmlDialog { Q_OBJECT - Q_PROPERTY(bool isLogIn READ getIsLogIn WRITE setIsLogIn) HIFI_QML_DECL public: @@ -67,24 +66,23 @@ protected slots: Q_INVOKABLE void dismissLoginDialog(); Q_INVOKABLE bool isSteamRunning() const; - Q_INVOKABLE bool isOculusStoreRunning() const; + Q_INVOKABLE bool isOculusRunning() const; + + Q_INVOKABLE QString oculusUserID() const; Q_INVOKABLE void login(const QString& username, const QString& password) const; Q_INVOKABLE void loginThroughSteam(); Q_INVOKABLE void linkSteam(); Q_INVOKABLE void createAccountFromSteam(QString username = QString()); + Q_INVOKABLE void loginThroughOculus(); + Q_INVOKABLE void linkOculus(); + Q_INVOKABLE void createAccountFromOculus(QString email = QString(), QString username = QString(), QString password = QString()); Q_INVOKABLE void signup(const QString& email, const QString& username, const QString& password); Q_INVOKABLE void openUrl(const QString& url) const; Q_INVOKABLE bool getLoginDialogPoppedUp() const; - -private: - bool getIsLogIn() const { return _isLogIn; } - void setIsLogIn(const bool isLogIn) { _isLogIn = isLogIn; } - - bool _isLogIn{ false }; }; #endif // hifi_LoginDialog_h diff --git a/interface/src/ui/overlays/ModelOverlay.cpp b/interface/src/ui/overlays/ModelOverlay.cpp index 14b8182abf..14e5cdc7f5 100644 --- a/interface/src/ui/overlays/ModelOverlay.cpp +++ b/interface/src/ui/overlays/ModelOverlay.cpp @@ -114,14 +114,9 @@ void ModelOverlay::update(float deltatime) { _model->setVisibleInScene(getVisible(), scene); metaDirty = true; } - if (_drawInFrontDirty) { - _drawInFrontDirty = false; - _model->setLayeredInFront(getDrawInFront(), scene); - metaDirty = true; - } - if (_drawInHUDDirty) { - _drawInHUDDirty = false; - _model->setLayeredInHUD(getDrawHUDLayer(), scene); + if (_renderLayerDirty) { + _renderLayerDirty = false; + _model->setHifiRenderLayer(_drawHUDLayer ? render::hifi::LAYER_3D_HUD : (_drawInFront ? render::hifi::LAYER_3D_FRONT : render::hifi::LAYER_3D), scene); metaDirty = true; } if (_groupCulledDirty) { @@ -175,14 +170,14 @@ void ModelOverlay::setVisible(bool visible) { void ModelOverlay::setDrawInFront(bool drawInFront) { if (drawInFront != getDrawInFront()) { Base3DOverlay::setDrawInFront(drawInFront); - _drawInFrontDirty = true; + _renderLayerDirty = true; } } void ModelOverlay::setDrawHUDLayer(bool drawHUDLayer) { if (drawHUDLayer != getDrawHUDLayer()) { Base3DOverlay::setDrawHUDLayer(drawHUDLayer); - _drawInHUDDirty = true; + _renderLayerDirty = true; } } diff --git a/interface/src/ui/overlays/ModelOverlay.h b/interface/src/ui/overlays/ModelOverlay.h index bd922e258a..17a2327d02 100644 --- a/interface/src/ui/overlays/ModelOverlay.h +++ b/interface/src/ui/overlays/ModelOverlay.h @@ -126,8 +126,7 @@ private: QVector _jointMapping; // domain is index into model-joints, range is index into animation-joints bool _visibleDirty { true }; - bool _drawInFrontDirty { false }; - bool _drawInHUDDirty { false }; + bool _renderLayerDirty { false }; bool _isGroupCulled { false }; bool _groupCulledDirty { false }; diff --git a/interface/src/ui/overlays/Web3DOverlay.cpp b/interface/src/ui/overlays/Web3DOverlay.cpp index ec6b62e237..eb61ca7281 100644 --- a/interface/src/ui/overlays/Web3DOverlay.cpp +++ b/interface/src/ui/overlays/Web3DOverlay.cpp @@ -64,20 +64,13 @@ #include "AboutUtil.h" #include "ResourceRequestObserver.h" +#include + static int MAX_WINDOW_SIZE = 4096; static const float METERS_TO_INCHES = 39.3701f; static const float OPAQUE_ALPHA_THRESHOLD = 0.99f; const QString Web3DOverlay::TYPE = "web3d"; -const QString Web3DOverlay::QML = "Web3DOverlay.qml"; - -static auto qmlSurfaceDeleter = [](OffscreenQmlSurface* surface) { - AbstractViewStateInterface::instance()->sendLambdaEvent([surface] { - // WebEngineView may run other threads (wasapi), so they must be deleted for a clean shutdown - // if the application has already stopped its event loop, delete must be explicit - delete surface; - }); -}; Web3DOverlay::Web3DOverlay() { _touchDevice.setCapabilities(QTouchDevice::Position); @@ -87,34 +80,21 @@ Web3DOverlay::Web3DOverlay() { _geometryId = DependencyManager::get()->allocateID(); connect(this, &Web3DOverlay::requestWebSurface, this, &Web3DOverlay::buildWebSurface); - connect(this, &Web3DOverlay::releaseWebSurface, this, &Web3DOverlay::destroyWebSurface); connect(this, &Web3DOverlay::resizeWebSurface, this, &Web3DOverlay::onResizeWebSurface); - //need to be intialized before Tablet 1st open - _webSurface = DependencyManager::get()->acquire(QML); - _cachedWebSurface = true; - _webSurface->getSurfaceContext()->setContextProperty("HMD", DependencyManager::get().data()); - _webSurface->getSurfaceContext()->setContextProperty("Account", AccountServicesScriptingInterface::getInstance()); // DEPRECATED - TO BE REMOVED - _webSurface->getSurfaceContext()->setContextProperty("GlobalServices", AccountServicesScriptingInterface::getInstance()); // DEPRECATED - TO BE REMOVED - _webSurface->getSurfaceContext()->setContextProperty("AccountServices", AccountServicesScriptingInterface::getInstance()); - _webSurface->getSurfaceContext()->setContextProperty("AddressManager", DependencyManager::get().data()); + buildWebSurface(true); } Web3DOverlay::Web3DOverlay(const Web3DOverlay* Web3DOverlay) : Billboard3DOverlay(Web3DOverlay), _url(Web3DOverlay->_url), _scriptURL(Web3DOverlay->_scriptURL), - _dpi(Web3DOverlay->_dpi), - _showKeyboardFocusHighlight(Web3DOverlay->_showKeyboardFocusHighlight) + _dpi(Web3DOverlay->_dpi) { _geometryId = DependencyManager::get()->allocateID(); } Web3DOverlay::~Web3DOverlay() { - disconnect(this, &Web3DOverlay::requestWebSurface, this, nullptr); - disconnect(this, &Web3DOverlay::releaseWebSurface, this, nullptr); - disconnect(this, &Web3DOverlay::resizeWebSurface, this, nullptr); - destroyWebSurface(); auto geometryCache = DependencyManager::get(); if (geometryCache) { @@ -128,81 +108,22 @@ void Web3DOverlay::rebuildWebSurface() { } void Web3DOverlay::destroyWebSurface() { - if (!_webSurface) { - return; + if (_webSurface) { + render::entities::WebEntityRenderer::releaseWebSurface(_webSurface, _cachedWebSurface, _connections); } - - QQuickItem* rootItem = _webSurface->getRootItem(); - - if (rootItem && rootItem->objectName() == "tabletRoot") { - auto tabletScriptingInterface = DependencyManager::get(); - if (tabletScriptingInterface) { - tabletScriptingInterface->setQmlTabletRoot("com.highfidelity.interface.tablet.system", nullptr); - } - } - - // Fix for crash in QtWebEngineCore when rapidly switching domains - // Call stop on the QWebEngineView before destroying OffscreenQMLSurface. - if (rootItem) { - // stop loading - QMetaObject::invokeMethod(rootItem, "stop"); - } - - _webSurface->pause(); - - QObject::disconnect(this, &Web3DOverlay::scriptEventReceived, _webSurface.data(), &OffscreenQmlSurface::emitScriptEvent); - QObject::disconnect(_webSurface.data(), &OffscreenQmlSurface::webEventReceived, this, &Web3DOverlay::webEventReceived); - - // If the web surface was fetched out of the cache, release it back into the cache - if (_cachedWebSurface) { - // If it's going back into the cache make sure to explicitly set the URL to a blank page - // in order to stop any resource consumption or audio related to the page. - if (rootItem) { - rootItem->setProperty("url", "about:blank"); - } - auto offscreenCache = DependencyManager::get(); - // FIXME prevents crash on shutdown, but we shoudln't have to do this check - if (offscreenCache) { - offscreenCache->release(QML, _webSurface); - } - _cachedWebSurface = false; - } - _webSurface.reset(); } -void Web3DOverlay::buildWebSurface() { +void Web3DOverlay::buildWebSurface(bool overrideWeb) { if (_webSurface) { return; } - // FIXME the context save here is most likely unecessary since the QML surfaces now render - // off the main thread, and all GL context work is done off the main thread (I *think*) - gl::withSavedContext([&] { - // FIXME, the max FPS could be better managed by being dynamic (based on the number of current surfaces - // and the current rendering load) - if (_currentMaxFPS != _desiredMaxFPS) { - setMaxFPS(_desiredMaxFPS); - } - if (isWebContent()) { - _webSurface = DependencyManager::get()->acquire(QML); - _cachedWebSurface = true; - _webSurface->getRootItem()->setProperty("url", _url); - _webSurface->getRootItem()->setProperty("scriptURL", _scriptURL); - } else { - _webSurface = QSharedPointer(new OffscreenQmlSurface(), qmlSurfaceDeleter); - connect(_webSurface.data(), &hifi::qml::OffscreenSurface::rootContextCreated, [this](QQmlContext* surfaceContext) { - setupQmlSurface(_url == TabletScriptingInterface::QML, _url == OVERLAY_LOGIN_DIALOG.toString()); - }); - _webSurface->load(_url); - _cachedWebSurface = false; - } - _webSurface->getSurfaceContext()->setContextProperty("globalPosition", vec3toVariant(getWorldPosition())); - onResizeWebSurface(); - _webSurface->resume(); - }); + render::entities::WebEntityRenderer::acquireWebSurface(_url, overrideWeb || isWebContent(), _webSurface, _cachedWebSurface); + onResizeWebSurface(); + _webSurface->resume(); - QObject::connect(this, &Web3DOverlay::scriptEventReceived, _webSurface.data(), &OffscreenQmlSurface::emitScriptEvent); - QObject::connect(_webSurface.data(), &OffscreenQmlSurface::webEventReceived, this, &Web3DOverlay::webEventReceived); + _connections.push_back(QObject::connect(this, &Web3DOverlay::scriptEventReceived, _webSurface.data(), &OffscreenQmlSurface::emitScriptEvent)); + _connections.push_back(QObject::connect(_webSurface.data(), &OffscreenQmlSurface::webEventReceived, this, &Web3DOverlay::webEventReceived)); } void Web3DOverlay::update(float deltatime) { @@ -222,69 +143,8 @@ bool Web3DOverlay::isWebContent() const { return false; } -void Web3DOverlay::setupQmlSurface(bool isTablet, bool isLoginDialog) { - _webSurface->getSurfaceContext()->setContextProperty("Users", DependencyManager::get().data()); - _webSurface->getSurfaceContext()->setContextProperty("HMD", DependencyManager::get().data()); - _webSurface->getSurfaceContext()->setContextProperty("UserActivityLogger", DependencyManager::get().data()); - _webSurface->getSurfaceContext()->setContextProperty("Preferences", DependencyManager::get().data()); - _webSurface->getSurfaceContext()->setContextProperty("Vec3", new Vec3()); - _webSurface->getSurfaceContext()->setContextProperty("Quat", new Quat()); - _webSurface->getSurfaceContext()->setContextProperty("MyAvatar", DependencyManager::get()->getMyAvatar().get()); - _webSurface->getSurfaceContext()->setContextProperty("Entities", DependencyManager::get().data()); - _webSurface->getSurfaceContext()->setContextProperty("Snapshot", DependencyManager::get().data()); - - if (isTablet || isLoginDialog) { - _webSurface->getSurfaceContext()->setContextProperty("Account", AccountServicesScriptingInterface::getInstance()); // DEPRECATED - TO BE REMOVED - _webSurface->getSurfaceContext()->setContextProperty("Settings", SettingsScriptingInterface::getInstance()); - _webSurface->getSurfaceContext()->setContextProperty("MenuInterface", MenuScriptingInterface::getInstance()); - _webSurface->getSurfaceContext()->setContextProperty("KeyboardScriptingInterface", DependencyManager::get().data()); - } - if (isTablet) { - auto tabletScriptingInterface = DependencyManager::get(); - auto flags = tabletScriptingInterface->getFlags(); - - _webSurface->getSurfaceContext()->setContextProperty("offscreenFlags", flags); - _webSurface->getSurfaceContext()->setContextProperty("AddressManager", DependencyManager::get().data()); - - _webSurface->getSurfaceContext()->setContextProperty("GlobalServices", AccountServicesScriptingInterface::getInstance()); // DEPRECATED - TO BE REMOVED - _webSurface->getSurfaceContext()->setContextProperty("AccountServices", AccountServicesScriptingInterface::getInstance()); - - // in Qt 5.10.0 there is already an "Audio" object in the QML context - // though I failed to find it (from QtMultimedia??). So.. let it be "AudioScriptingInterface" - _webSurface->getSurfaceContext()->setContextProperty("AudioScriptingInterface", DependencyManager::get().data()); - - _webSurface->getSurfaceContext()->setContextProperty("AudioStats", DependencyManager::get()->getStats().data()); - _webSurface->getSurfaceContext()->setContextProperty("HMD", DependencyManager::get().data()); - _webSurface->getSurfaceContext()->setContextProperty("fileDialogHelper", new FileDialogHelper()); - _webSurface->getSurfaceContext()->setContextProperty("MyAvatar", DependencyManager::get()->getMyAvatar().get()); - _webSurface->getSurfaceContext()->setContextProperty("ScriptDiscoveryService", DependencyManager::get().data()); - _webSurface->getSurfaceContext()->setContextProperty("Assets", DependencyManager::get().data()); - _webSurface->getSurfaceContext()->setContextProperty("LODManager", DependencyManager::get().data()); - _webSurface->getSurfaceContext()->setContextProperty("OctreeStats", DependencyManager::get().data()); - _webSurface->getSurfaceContext()->setContextProperty("DCModel", DependencyManager::get().data()); - _webSurface->getSurfaceContext()->setContextProperty("AvatarInputs", AvatarInputs::getInstance()); - _webSurface->getSurfaceContext()->setContextProperty("AvatarList", DependencyManager::get().data()); - _webSurface->getSurfaceContext()->setContextProperty("DialogsManager", DialogsManagerScriptingInterface::getInstance()); - _webSurface->getSurfaceContext()->setContextProperty("InputConfiguration", DependencyManager::get().data()); - _webSurface->getSurfaceContext()->setContextProperty("SoundCache", DependencyManager::get().data()); - _webSurface->getSurfaceContext()->setContextProperty("AvatarBookmarks", DependencyManager::get().data()); - _webSurface->getSurfaceContext()->setContextProperty("Render", AbstractViewStateInterface::instance()->getRenderEngine()->getConfiguration().get()); - _webSurface->getSurfaceContext()->setContextProperty("Workload", qApp->getGameWorkload()._engine->getConfiguration().get()); - _webSurface->getSurfaceContext()->setContextProperty("Controller", DependencyManager::get().data()); - _webSurface->getSurfaceContext()->setContextProperty("Pointers", DependencyManager::get().data()); - _webSurface->getSurfaceContext()->setContextProperty("Web3DOverlay", this); - _webSurface->getSurfaceContext()->setContextProperty("Window", DependencyManager::get().data()); - _webSurface->getSurfaceContext()->setContextProperty("Reticle", qApp->getApplicationCompositor().getReticleInterface()); - _webSurface->getSurfaceContext()->setContextProperty("HiFiAbout", AboutUtil::getInstance()); - _webSurface->getSurfaceContext()->setContextProperty("WalletScriptingInterface", DependencyManager::get().data()); - _webSurface->getSurfaceContext()->setContextProperty("ResourceRequestObserver", DependencyManager::get().data()); - - // Override min fps for tablet UI, for silky smooth scrolling - setMaxFPS(90); - } -} - void Web3DOverlay::setMaxFPS(uint8_t maxFPS) { + // FIXME, the max FPS could be better managed by being dynamic (based on the number of current surfaces and the current rendering load) _desiredMaxFPS = maxFPS; if (_webSurface) { _webSurface->setMaxFps(_desiredMaxFPS); @@ -305,21 +165,13 @@ void Web3DOverlay::onResizeWebSurface() { _webSurface->resize(QSize(dims.x, dims.y)); } -unsigned int Web3DOverlay::deviceIdByTouchPoint(qreal x, qreal y) { - if (_webSurface) { - return _webSurface->deviceIdByTouchPoint(x, y); - } else { - return PointerEvent::INVALID_POINTER_ID; - } -} - void Web3DOverlay::render(RenderArgs* args) { if (!_renderVisible || !getParentVisible()) { return; } if (!_webSurface) { - emit requestWebSurface(); + emit requestWebSurface(false); return; } @@ -506,11 +358,6 @@ void Web3DOverlay::setProperties(const QVariantMap& properties) { _desiredMaxFPS = maxFPS.toInt(); } - auto showKeyboardFocusHighlight = properties["showKeyboardFocusHighlight"]; - if (showKeyboardFocusHighlight.isValid()) { - _showKeyboardFocusHighlight = showKeyboardFocusHighlight.toBool(); - } - auto inputModeValue = properties["inputMode"]; if (inputModeValue.isValid()) { QString inputModeStr = inputModeValue.toString(); @@ -573,8 +420,6 @@ void Web3DOverlay::setProperties(const QVariantMap& properties) { * @property {Vec2} dimensions=1,1 - The size of the overlay to display the Web page on, in meters. Synonyms: * scale, size. * @property {number} maxFPS=10 - The maximum update rate for the Web overlay content, in frames/second. - * @property {boolean} showKeyboardFocusHighlight=true - If true, the Web overlay is highlighted when it has - * keyboard focus. * @property {string} inputMode=Touch - The user input mode to use - either "Touch" or "Mouse". */ QVariant Web3DOverlay::getProperty(const QString& property) { @@ -590,9 +435,6 @@ QVariant Web3DOverlay::getProperty(const QString& property) { if (property == "maxFPS") { return _desiredMaxFPS; } - if (property == "showKeyboardFocusHighlight") { - return _showKeyboardFocusHighlight; - } if (property == "inputMode") { if (_inputMode == Mouse) { @@ -612,14 +454,14 @@ void Web3DOverlay::setURL(const QString& url) { if (wasWebContent && isWebContent()) { // If we're just targeting a new web URL, then switch to that without messing around // with the underlying QML - AbstractViewStateInterface::instance()->postLambdaEvent([this, url] { - _webSurface->getRootItem()->setProperty("url", _url); + AbstractViewStateInterface::instance()->postLambdaEvent([this] { + _webSurface->getRootItem()->setProperty(render::entities::WebEntityRenderer::URL_PROPERTY, _url); _webSurface->getRootItem()->setProperty("scriptURL", _scriptURL); }); } else { // If we're switching to or from web content, or between different QML content // we need to destroy and rebuild the entire QML surface - AbstractViewStateInterface::instance()->postLambdaEvent([this, url] { + AbstractViewStateInterface::instance()->postLambdaEvent([this] { rebuildWebSurface(); }); } diff --git a/interface/src/ui/overlays/Web3DOverlay.h b/interface/src/ui/overlays/Web3DOverlay.h index 548ad7abe0..4265c35699 100644 --- a/interface/src/ui/overlays/Web3DOverlay.h +++ b/interface/src/ui/overlays/Web3DOverlay.h @@ -22,8 +22,6 @@ class Web3DOverlay : public Billboard3DOverlay { using Parent = Billboard3DOverlay; public: - - static const QString QML; static QString const TYPE; virtual QString getType() const override { return TYPE; } @@ -59,12 +57,10 @@ public: Mouse }; - void buildWebSurface(); + void buildWebSurface(bool overrideWeb = false); void destroyWebSurface(); void onResizeWebSurface(); - Q_INVOKABLE unsigned int deviceIdByTouchPoint(qreal x, qreal y); - public slots: void emitScriptEvent(const QVariant& scriptMessage); @@ -72,26 +68,23 @@ signals: void scriptEventReceived(const QVariant& message); void webEventReceived(const QVariant& message); void resizeWebSurface(); - void requestWebSurface(); - void releaseWebSurface(); + void requestWebSurface(bool overrideWeb); protected: Transform evalRenderTransform() override; private: - void setupQmlSurface(bool isTablet, bool isLoginDialog); void rebuildWebSurface(); bool isWebContent() const; InputMode _inputMode { Touch }; QSharedPointer _webSurface; - bool _cachedWebSurface{ false }; + bool _cachedWebSurface { false }; gpu::TexturePointer _texture; QString _url; QString _scriptURL; float _dpi { 30.0f }; int _geometryId { 0 }; - bool _showKeyboardFocusHighlight { true }; QTouchDevice _touchDevice; @@ -99,6 +92,8 @@ private: uint8_t _currentMaxFPS { 0 }; bool _mayNeedResize { false }; + + std::vector _connections; }; #endif // hifi_Web3DOverlay_h diff --git a/libraries/animation/src/AnimSkeleton.cpp b/libraries/animation/src/AnimSkeleton.cpp index 16c2c1cc7e..cc48308f17 100644 --- a/libraries/animation/src/AnimSkeleton.cpp +++ b/libraries/animation/src/AnimSkeleton.cpp @@ -237,8 +237,17 @@ void AnimSkeleton::buildSkeletonFromJoints(const std::vector& joints, _relativeDefaultPoses = _absoluteDefaultPoses; convertAbsolutePosesToRelative(_relativeDefaultPoses); + // build _jointIndicesByName hash for (int i = 0; i < _jointsSize; i++) { - _jointIndicesByName[_joints[i].name] = i; + auto iter = _jointIndicesByName.find(_joints[i].name); + if (iter != _jointIndicesByName.end()) { + // prefer joints over meshes if there is a name collision. + if (_joints[i].isSkeletonJoint && !_joints[iter.value()].isSkeletonJoint) { + iter.value() = i; + } + } else { + _jointIndicesByName.insert(_joints[i].name, i); + } } // build mirror map. diff --git a/libraries/animation/src/Rig.cpp b/libraries/animation/src/Rig.cpp index 6e27bee06f..bc4dca54f2 100644 --- a/libraries/animation/src/Rig.cpp +++ b/libraries/animation/src/Rig.cpp @@ -1984,11 +1984,10 @@ void Rig::copyJointsIntoJointData(QVector& jointDataVec) const { data.rotation = !_sendNetworkNode ? _internalPoseSet._absolutePoses[i].rot() : _networkPoseSet._absolutePoses[i].rot(); data.rotationIsDefaultPose = isEqual(data.rotation, defaultAbsRot); - // translations are in relative frame but scaled so that they are in meters, - // instead of model units. + // translations are in relative frame. glm::vec3 defaultRelTrans = _animSkeleton->getRelativeDefaultPose(i).trans(); glm::vec3 currentRelTrans = _sendNetworkNode ? _networkPoseSet._relativePoses[i].trans() : _internalPoseSet._relativePoses[i].trans(); - data.translation = geometryToRigScale * currentRelTrans; + data.translation = currentRelTrans; data.translationIsDefaultPose = isEqual(currentRelTrans, defaultRelTrans); } else { data.translationIsDefaultPose = true; @@ -2015,7 +2014,6 @@ void Rig::copyJointsFromJointData(const QVector& jointDataVec) { std::vector rotations; rotations.reserve(numJoints); const glm::quat rigToGeometryRot(glmExtractRotation(_rigToGeometryTransform)); - const glm::vec3 rigToGeometryScale(extractScale(_rigToGeometryTransform)); for (int i = 0; i < numJoints; i++) { const JointData& data = jointDataVec.at(i); @@ -2041,8 +2039,8 @@ void Rig::copyJointsFromJointData(const QVector& jointDataVec) { if (data.translationIsDefaultPose) { _internalPoseSet._relativePoses[i].trans() = relativeDefaultPoses[i].trans(); } else { - // JointData translations are in scaled relative-frame so we scale back to regular relative-frame - _internalPoseSet._relativePoses[i].trans() = rigToGeometryScale * data.translation; + // JointData translations are in relative-frame + _internalPoseSet._relativePoses[i].trans() = data.translation; } } } diff --git a/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp b/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp index a6185d7e79..17d10cdf49 100644 --- a/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp +++ b/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp @@ -324,8 +324,8 @@ void Avatar::removeAvatarEntitiesFromTree() { } } -void Avatar::updateGrabs() { - +bool Avatar::updateGrabs() { + bool grabAddedOrRemoved = false; // update the Grabs according to any changes in _avatarGrabData _avatarGrabsLock.withWriteLock([&] { if (_avatarGrabDataChanged) { @@ -385,6 +385,7 @@ void Avatar::updateGrabs() { entityTree->updateEntityQueryAACube(target, packetSender, force, iShouldTellServer); }); } + grabAddedOrRemoved = true; } _avatarGrabs.remove(grabID); _changedAvatarGrabs.remove(grabID); @@ -402,9 +403,11 @@ void Avatar::updateGrabs() { target->addGrab(grab); // only clear this entry from the _changedAvatarGrabs if we found the entity. changeItr.remove(); + grabAddedOrRemoved = true; } } }); + return grabAddedOrRemoved; } void Avatar::accumulateGrabPositions(std::map& grabAccumulators) { @@ -1375,7 +1378,7 @@ void Avatar::setSkeletonModelURL(const QUrl& skeletonModelURL) { AvatarData::setSkeletonModelURL(skeletonModelURL); if (QThread::currentThread() == thread()) { - if (!isMyAvatar()) { + if (!isMyAvatar() && !DependencyManager::get()->isIgnoringNode(getSessionUUID())) { createOrb(); } diff --git a/libraries/avatars-renderer/src/avatars-renderer/Avatar.h b/libraries/avatars-renderer/src/avatars-renderer/Avatar.h index 4ff3e9cc13..d5431ad2d2 100644 --- a/libraries/avatars-renderer/src/avatars-renderer/Avatar.h +++ b/libraries/avatars-renderer/src/avatars-renderer/Avatar.h @@ -538,7 +538,7 @@ protected: // protected methods... bool isLookingAtMe(AvatarSharedPointer avatar) const; - void updateGrabs(); + bool updateGrabs(); void relayJointDataToChildren(); void fade(render::Transaction& transaction, render::Transition::Type type); diff --git a/libraries/avatars/src/AssociatedTraitValues.h b/libraries/avatars/src/AssociatedTraitValues.h index cf1edef7f7..e3060a8097 100644 --- a/libraries/avatars/src/AssociatedTraitValues.h +++ b/libraries/avatars/src/AssociatedTraitValues.h @@ -14,16 +14,33 @@ #include "AvatarTraits.h" +// This templated class is admittedly fairly confusing to look at. It is used +// to hold some associated value of type T for both simple (non-instanced) and instanced traits. +// Most of the complexity comes from the fact that simple and instanced trait types are +// handled differently. For each simple trait type there can be a value T, but for +// each instance of each instanced trait +// (keyed by a TraitInstanceID, which at the time of this writing is a UUID) there can be a value T. +// There are separate methods in most cases for simple traits and instanced traits +// because of this different behaviour. This class is not used to hold the values +// of the traits themselves, but instead an associated value like the latest version +// of each trait (see TraitVersions) or a state associated with each trait (like added/changed/deleted). + namespace AvatarTraits { template class AssociatedTraitValues { public: + // constructor that pre-fills _simpleTypes with the default value specified by the template AssociatedTraitValues() : _simpleTypes(FirstInstancedTrait, defaultValue) {} + /// inserts the given value for the given simple trait type void insert(TraitType type, T value) { _simpleTypes[type] = value; } + /// resets the simple trait type value to the default void erase(TraitType type) { _simpleTypes[type] = defaultValue; } + /// returns a reference to the value for a given instance for a given instanced trait type T& getInstanceValueRef(TraitType traitType, TraitInstanceID instanceID); + + /// inserts the passed value for the given instance for the given instanced trait type void instanceInsert(TraitType traitType, TraitInstanceID instanceID, T value); struct InstanceIDValuePair { @@ -34,24 +51,30 @@ namespace AvatarTraits { }; using InstanceIDValuePairs = std::vector; - + + /// returns a vector of InstanceIDValuePair objects for the given instanced trait type InstanceIDValuePairs& getInstanceIDValuePairs(TraitType traitType); + /// erases the a given instance for a given instanced trait type void instanceErase(TraitType traitType, TraitInstanceID instanceID); + /// erases the value for all instances for a given instanced trait type void eraseAllInstances(TraitType traitType); - // will return defaultValue for instanced traits + /// value getters for simple trait types, will be default value if value has been erased or not set T operator[](TraitType traitType) const { return _simpleTypes[traitType]; } T& operator[](TraitType traitType) { return _simpleTypes[traitType]; } + /// resets all simple trait types to the default value and erases all values for instanced trait types void reset() { std::fill(_simpleTypes.begin(), _simpleTypes.end(), defaultValue); _instancedTypes.clear(); } + /// const iterators for the vector of simple type values typename std::vector::const_iterator simpleCBegin() const { return _simpleTypes.cbegin(); } typename std::vector::const_iterator simpleCEnd() const { return _simpleTypes.cend(); } + /// non-const iterators for the vector of simple type values typename std::vector::iterator simpleBegin() { return _simpleTypes.begin(); } typename std::vector::iterator simpleEnd() { return _simpleTypes.end(); } @@ -64,15 +87,18 @@ namespace AvatarTraits { traitType(traitType), instances({{ instanceID, value }}) {}; }; + /// const iterators for the vector of TraitWithInstances objects typename std::vector::const_iterator instancedCBegin() const { return _instancedTypes.cbegin(); } typename std::vector::const_iterator instancedCEnd() const { return _instancedTypes.cend(); } + /// non-const iterators for the vector of TraitWithInstances objects typename std::vector::iterator instancedBegin() { return _instancedTypes.begin(); } typename std::vector::iterator instancedEnd() { return _instancedTypes.end(); } private: std::vector _simpleTypes; + /// return the iterator to the matching TraitWithInstances object for a given instanced trait type typename std::vector::iterator instancesForTrait(TraitType traitType) { return std::find_if(_instancedTypes.begin(), _instancedTypes.end(), [traitType](TraitWithInstances& traitWithInstances){ @@ -83,25 +109,34 @@ namespace AvatarTraits { std::vector _instancedTypes; }; + /// returns a reference to the InstanceIDValuePairs object for a given instanced trait type template inline typename AssociatedTraitValues::InstanceIDValuePairs& AssociatedTraitValues::getInstanceIDValuePairs(TraitType traitType) { + // first check if we already have some values for instances of this trait type auto it = instancesForTrait(traitType); if (it != _instancedTypes.end()) { return it->instances; } else { + // if we didn't have any values for instances of the instanced trait type + // add an empty InstanceIDValuePairs object first and then return the reference to it _instancedTypes.emplace_back(traitType); return _instancedTypes.back().instances; } } + // returns a reference to value for the given instance of the given instanced trait type template inline T& AssociatedTraitValues::getInstanceValueRef(TraitType traitType, TraitInstanceID instanceID) { + // first check if we already have some values for instances of this trait type auto it = instancesForTrait(traitType); if (it != _instancedTypes.end()) { + // grab the matching vector of instances auto& instancesVector = it->instances; + + // check if we have a value for this specific instance ID auto instanceIt = std::find_if(instancesVector.begin(), instancesVector.end(), [instanceID](InstanceIDValuePair& idValuePair){ return idValuePair.id == instanceID; @@ -109,40 +144,53 @@ namespace AvatarTraits { if (instanceIt != instancesVector.end()) { return instanceIt->value; } else { + // no value for this specific instance ID, insert the default value and return it instancesVector.emplace_back(instanceID, defaultValue); return instancesVector.back().value; } } else { + // no values for any instances of this trait type + // insert the default value for the specific instance for the instanced trait type _instancedTypes.emplace_back(traitType, instanceID, defaultValue); return _instancedTypes.back().instances.back().value; } } + /// inserts the passed value for the specific instance of the given instanced trait type template inline void AssociatedTraitValues::instanceInsert(TraitType traitType, TraitInstanceID instanceID, T value) { + // first check if we already have some instances for this trait type auto it = instancesForTrait(traitType); if (it != _instancedTypes.end()) { + // found some instances for the instanced trait type, check if our specific instance is one of them auto& instancesVector = it->instances; auto instanceIt = std::find_if(instancesVector.begin(), instancesVector.end(), [instanceID](InstanceIDValuePair& idValuePair){ return idValuePair.id == instanceID; }); if (instanceIt != instancesVector.end()) { + // the instance already existed, update the value instanceIt->value = value; } else { + // the instance was not present, emplace the new value instancesVector.emplace_back(instanceID, value); } } else { + // there were no existing instances for the given trait type + // setup the container for instances and insert the passed value for this instance ID _instancedTypes.emplace_back(traitType, instanceID, value); } } + /// erases the value for a specific instance of the given instanced trait type template inline void AssociatedTraitValues::instanceErase(TraitType traitType, TraitInstanceID instanceID) { + // check if we have any instances at all for this instanced trait type auto it = instancesForTrait(traitType); if (it != _instancedTypes.end()) { + // we have some instances, erase the value for the passed instance ID if it is present auto& instancesVector = it->instances; instancesVector.erase(std::remove_if(instancesVector.begin(), instancesVector.end(), diff --git a/libraries/avatars/src/AvatarData.cpp b/libraries/avatars/src/AvatarData.cpp old mode 100644 new mode 100755 index 21e0a6aba2..17dad715f9 --- a/libraries/avatars/src/AvatarData.cpp +++ b/libraries/avatars/src/AvatarData.cpp @@ -54,7 +54,8 @@ using namespace std; const QString AvatarData::FRAME_NAME = "com.highfidelity.recording.AvatarData"; -static const int TRANSLATION_COMPRESSION_RADIX = 12; +static const int TRANSLATION_COMPRESSION_RADIX = 14; +static const int FAUX_JOINT_COMPRESSION_RADIX = 12; static const int SENSOR_TO_WORLD_SCALE_RADIX = 10; static const float AUDIO_LOUDNESS_SCALE = 1024.0f; static const float DEFAULT_AVATAR_DENSITY = 1000.0f; // density of water @@ -73,6 +74,7 @@ size_t AvatarDataPacket::maxJointDataSize(size_t numJoints, bool hasGrabJoints) totalSize += validityBitsSize; // Orientations mask totalSize += numJoints * sizeof(SixByteQuat); // Orientations totalSize += validityBitsSize; // Translations mask + totalSize += sizeof(float); // maxTranslationDimension totalSize += numJoints * sizeof(SixByteTrans); // Translations size_t NUM_FAUX_JOINT = 2; @@ -85,6 +87,23 @@ size_t AvatarDataPacket::maxJointDataSize(size_t numJoints, bool hasGrabJoints) return totalSize; } +size_t AvatarDataPacket::minJointDataSize(size_t numJoints) { + const size_t validityBitsSize = calcBitVectorSize((int)numJoints); + + size_t totalSize = sizeof(uint8_t); // numJoints + + totalSize += validityBitsSize; // Orientations mask + // assume no valid rotations + totalSize += validityBitsSize; // Translations mask + totalSize += sizeof(float); // maxTranslationDimension + // assume no valid translations + + size_t NUM_FAUX_JOINT = 2; + totalSize += NUM_FAUX_JOINT * (sizeof(SixByteQuat) + sizeof(SixByteTrans)); // faux joints + + return totalSize; +} + size_t AvatarDataPacket::maxJointDefaultPoseFlagsSize(size_t numJoints) { const size_t bitVectorSize = calcBitVectorSize((int)numJoints); size_t totalSize = sizeof(uint8_t); // numJoints @@ -611,13 +630,24 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent assert(numJoints <= 255); const int jointBitVectorSize = calcBitVectorSize(numJoints); - // Start joints if room for at least the faux joints. - IF_AVATAR_SPACE(PACKET_HAS_JOINT_DATA, 1 + 2 * jointBitVectorSize + AvatarDataPacket::FAUX_JOINTS_SIZE) { + // include jointData if there is room for the most minimal section. i.e. no translations or rotations. + IF_AVATAR_SPACE(PACKET_HAS_JOINT_DATA, AvatarDataPacket::minJointDataSize(numJoints)) { // Allow for faux joints + translation bit-vector: const ptrdiff_t minSizeForJoint = sizeof(AvatarDataPacket::SixByteQuat) + jointBitVectorSize + AvatarDataPacket::FAUX_JOINTS_SIZE; auto startSection = destinationBuffer; + // compute maxTranslationDimension before we send any joint data. + float maxTranslationDimension = 0.001f; + for (int i = sendStatus.translationsSent; i < numJoints; ++i) { + const JointData& data = jointData[i]; + if (!data.translationIsDefaultPose) { + maxTranslationDimension = glm::max(fabsf(data.translation.x), maxTranslationDimension); + maxTranslationDimension = glm::max(fabsf(data.translation.y), maxTranslationDimension); + maxTranslationDimension = glm::max(fabsf(data.translation.z), maxTranslationDimension); + } + } + // joint rotation data *destinationBuffer++ = (uint8_t)numJoints; @@ -684,9 +714,11 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent memset(destinationBuffer, 0, jointBitVectorSize); destinationBuffer += jointBitVectorSize; // Move pointer past the validity bytes + // write maxTranslationDimension + AVATAR_MEMCPY(maxTranslationDimension); + float minTranslation = (distanceAdjust && cullSmallChanges) ? getDistanceBasedMinTranslationDistance(viewerPosition) : AVATAR_MIN_TRANSLATION; - float maxTranslationDimension = 0.0; i = sendStatus.translationsSent; for (; i < numJoints; ++i) { const JointData& data = joints[i]; @@ -700,12 +732,8 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent #ifdef WANT_DEBUG translationSentCount++; #endif - maxTranslationDimension = glm::max(fabsf(data.translation.x), maxTranslationDimension); - maxTranslationDimension = glm::max(fabsf(data.translation.y), maxTranslationDimension); - maxTranslationDimension = glm::max(fabsf(data.translation.z), maxTranslationDimension); - - destinationBuffer += - packFloatVec3ToSignedTwoByteFixed(destinationBuffer, data.translation, TRANSLATION_COMPRESSION_RADIX); + destinationBuffer += packFloatVec3ToSignedTwoByteFixed(destinationBuffer, data.translation / maxTranslationDimension, + TRANSLATION_COMPRESSION_RADIX); if (sentJoints) { sentJoints[i].translation = data.translation; @@ -727,12 +755,12 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent Transform controllerLeftHandTransform = Transform(getControllerLeftHandMatrix()); destinationBuffer += packOrientationQuatToSixBytes(destinationBuffer, controllerLeftHandTransform.getRotation()); destinationBuffer += packFloatVec3ToSignedTwoByteFixed(destinationBuffer, controllerLeftHandTransform.getTranslation(), - TRANSLATION_COMPRESSION_RADIX); + FAUX_JOINT_COMPRESSION_RADIX); Transform controllerRightHandTransform = Transform(getControllerRightHandMatrix()); destinationBuffer += packOrientationQuatToSixBytes(destinationBuffer, controllerRightHandTransform.getRotation()); destinationBuffer += packFloatVec3ToSignedTwoByteFixed(destinationBuffer, controllerRightHandTransform.getTranslation(), - TRANSLATION_COMPRESSION_RADIX); + FAUX_JOINT_COMPRESSION_RADIX); IF_AVATAR_SPACE(PACKET_HAS_GRAB_JOINTS, sizeof (AvatarDataPacket::FarGrabJoints)) { // the far-grab joints may range further than 3 meters, so we can't use packFloatVec3ToSignedTwoByteFixed etc @@ -785,7 +813,7 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent outboundDataRateOut->jointDataRate.increment(numBytes); } } - + IF_AVATAR_SPACE(PACKET_HAS_JOINT_DEFAULT_POSE_FLAGS, 1 + 2 * jointBitVectorSize) { auto startSection = destinationBuffer; @@ -871,7 +899,7 @@ const unsigned char* unpackFauxJoint(const unsigned char* sourceBuffer, ThreadSa glm::vec3 position; Transform transform; sourceBuffer += unpackOrientationQuatFromSixBytes(sourceBuffer, orientation); - sourceBuffer += unpackFloatVec3FromSignedTwoByteFixed(sourceBuffer, position, TRANSLATION_COMPRESSION_RADIX); + sourceBuffer += unpackFloatVec3FromSignedTwoByteFixed(sourceBuffer, position, FAUX_JOINT_COMPRESSION_RADIX); transform.setTranslation(position); transform.setRotation(orientation); matrixCache.set(transform.getMatrix()); @@ -1144,6 +1172,9 @@ int AvatarData::parseDataFromBuffer(const QByteArray& buffer) { sourceBuffer += sizeof(AvatarDataPacket::AdditionalFlags); + if (collideWithOtherAvatarsChanged) { + setCollisionWithOtherAvatarsFlags(); + } if (somethingChanged) { _additionalFlagsChanged = now; } @@ -1280,6 +1311,12 @@ int AvatarData::parseDataFromBuffer(const QByteArray& buffer) { } } // 1 + bytesOfValidity bytes + // read maxTranslationDimension + float maxTranslationDimension; + PACKET_READ_CHECK(JointMaxTranslationDimension, sizeof(float)); + memcpy(&maxTranslationDimension, sourceBuffer, sizeof(float)); + sourceBuffer += sizeof(float); + // each joint translation component is stored in 6 bytes. const int COMPRESSED_TRANSLATION_SIZE = 6; PACKET_READ_CHECK(JointTranslation, numValidJointTranslations * COMPRESSED_TRANSLATION_SIZE); @@ -1288,6 +1325,7 @@ int AvatarData::parseDataFromBuffer(const QByteArray& buffer) { JointData& data = _jointData[i]; if (validTranslations[i]) { sourceBuffer += unpackFloatVec3FromSignedTwoByteFixed(sourceBuffer, data.translation, TRANSLATION_COMPRESSION_RADIX); + data.translation *= maxTranslationDimension; _hasNewJointData = true; data.translationIsDefaultPose = false; } @@ -2157,7 +2195,7 @@ void AvatarData::detachAll(const QString& modelURL, const QString& jointName) { setAttachmentData(attachmentData); } -void AvatarData::sendAvatarDataPacket(bool sendAll) { +int AvatarData::sendAvatarDataPacket(bool sendAll) { auto nodeList = DependencyManager::get(); // about 2% of the time, we send a full update (meaning, we transmit all the joint data), even if nothing has changed. @@ -2170,16 +2208,14 @@ void AvatarData::sendAvatarDataPacket(bool sendAll) { int maximumByteArraySize = NLPacket::maxPayloadSize(PacketType::AvatarData) - sizeof(AvatarDataSequenceNumber); if (avatarByteArray.size() > maximumByteArraySize) { - qCWarning(avatars) << "toByteArrayStateful() resulted in very large buffer:" << avatarByteArray.size() << "... attempt to drop facial data"; avatarByteArray = toByteArrayStateful(dataDetail, true); if (avatarByteArray.size() > maximumByteArraySize) { - qCWarning(avatars) << "toByteArrayStateful() without facial data resulted in very large buffer:" << avatarByteArray.size() << "... reduce to MinimumData"; avatarByteArray = toByteArrayStateful(MinimumData, true); if (avatarByteArray.size() > maximumByteArraySize) { qCWarning(avatars) << "toByteArrayStateful() MinimumData resulted in very large buffer:" << avatarByteArray.size() << "... FAIL!!"; - return; + return 0; } } } @@ -2191,18 +2227,20 @@ void AvatarData::sendAvatarDataPacket(bool sendAll) { auto avatarPacket = NLPacket::create(PacketType::AvatarData, avatarByteArray.size() + sizeof(sequenceNumber)); avatarPacket->writePrimitive(sequenceNumber++); avatarPacket->write(avatarByteArray); + auto packetSize = avatarPacket->getWireSize(); nodeList->broadcastToNodes(std::move(avatarPacket), NodeSet() << NodeType::AvatarMixer); + + return packetSize; } -void AvatarData::sendIdentityPacket() { +int AvatarData::sendIdentityPacket() { auto nodeList = DependencyManager::get(); if (_identityDataChanged) { // if the identity data has changed, push the sequence number forwards ++_identitySequenceNumber; } - QByteArray identityData = identityByteArray(); auto packetList = NLPacketList::create(PacketType::AvatarIdentity, QByteArray(), true, true); @@ -2216,6 +2254,7 @@ void AvatarData::sendIdentityPacket() { }); _identityDataChanged = false; + return identityData.size(); } static const QString JSON_ATTACHMENT_URL = QStringLiteral("modelUrl"); @@ -2392,7 +2431,8 @@ static const QString JSON_AVATAR_VERSION = QStringLiteral("version"); enum class JsonAvatarFrameVersion : int { JointRotationsInRelativeFrame = 0, JointRotationsInAbsoluteFrame, - JointDefaultPoseBits + JointDefaultPoseBits, + JointUnscaledTranslations, }; QJsonValue toJsonValue(const JointData& joint) { @@ -2409,7 +2449,16 @@ JointData jointDataFromJsonValue(int version, const QJsonValue& json) { if (json.isArray()) { QJsonArray array = json.toArray(); result.rotation = quatFromJsonValue(array[0]); + result.translation = vec3FromJsonValue(array[1]); + + // In old recordings, translations are scaled by _geometryOffset. Undo that scaling. + if (version < (int)JsonAvatarFrameVersion::JointUnscaledTranslations) { + // because we don't have access to the actual _geometryOffset used. we have to guess. + // most avatar FBX files were authored in centimeters. + const float METERS_TO_CENTIMETERS = 100.0f; + result.translation *= METERS_TO_CENTIMETERS; + } if (version >= (int)JsonAvatarFrameVersion::JointDefaultPoseBits) { result.rotationIsDefaultPose = array[2].toBool(); result.translationIsDefaultPose = array[3].toBool(); @@ -2428,7 +2477,7 @@ void AvatarData::avatarEntityDataToJson(QJsonObject& root) const { QJsonObject AvatarData::toJson() const { QJsonObject root; - root[JSON_AVATAR_VERSION] = (int)JsonAvatarFrameVersion::JointDefaultPoseBits; + root[JSON_AVATAR_VERSION] = (int)JsonAvatarFrameVersion::JointUnscaledTranslations; if (!getSkeletonModelURL().isEmpty()) { root[JSON_AVATAR_BODY_MODEL] = getSkeletonModelURL().toString(); @@ -2744,23 +2793,28 @@ void AvatarData::setAttachmentsVariant(const QVariantList& variant) { } void AvatarData::storeAvatarEntityDataPayload(const QUuid& entityID, const QByteArray& data) { + bool changed = false; _avatarEntitiesLock.withWriteLock([&] { - PackedAvatarEntityMap::iterator itr = _packedAvatarEntityData.find(entityID); + auto itr = _packedAvatarEntityData.find(entityID); if (itr == _packedAvatarEntityData.end()) { if (_packedAvatarEntityData.size() < MAX_NUM_AVATAR_ENTITIES) { _packedAvatarEntityData.insert(entityID, data); + changed = true; } } else { itr.value() = data; + changed = true; } }); - _avatarEntityDataChanged = true; + if (changed) { + _avatarEntityDataChanged = true; - if (_clientTraitsHandler) { - // we have a client traits handler, so we need to mark this instanced trait as changed - // so that changes will be sent next frame - _clientTraitsHandler->markInstancedTraitUpdated(AvatarTraits::AvatarEntity, entityID); + if (_clientTraitsHandler) { + // we have a client traits handler, so we need to mark this instanced trait as changed + // so that changes will be sent next frame + _clientTraitsHandler->markInstancedTraitUpdated(AvatarTraits::AvatarEntity, entityID); + } } } diff --git a/libraries/avatars/src/AvatarData.h b/libraries/avatars/src/AvatarData.h old mode 100644 new mode 100755 index ec5ea4a5d0..9128c2dbf9 --- a/libraries/avatars/src/AvatarData.h +++ b/libraries/avatars/src/AvatarData.h @@ -277,8 +277,8 @@ namespace AvatarDataPacket { uint8_t rotationValidityBits[ceil(numJoints / 8)]; // one bit per joint, if true then a compressed rotation follows. SixByteQuat rotation[numValidRotations]; // encodeded and compressed by packOrientationQuatToSixBytes() uint8_t translationValidityBits[ceil(numJoints / 8)]; // one bit per joint, if true then a compressed translation follows. - SixByteTrans translation[numValidTranslations]; // encodeded and compressed by packFloatVec3ToSignedTwoByteFixed() - + float maxTranslationDimension; // used to normalize fixed point translation values. + SixByteTrans translation[numValidTranslations]; // normalized and compressed by packFloatVec3ToSignedTwoByteFixed() SixByteQuat leftHandControllerRotation; SixByteTrans leftHandControllerTranslation; SixByteQuat rightHandControllerRotation; @@ -286,6 +286,7 @@ namespace AvatarDataPacket { }; */ size_t maxJointDataSize(size_t numJoints, bool hasGrabJoints); + size_t minJointDataSize(size_t numJoints); /* struct JointDefaultPoseFlags { @@ -465,8 +466,6 @@ public: static const QUrl& defaultFullAvatarModelUrl(); - virtual bool isMyAvatar() const { return false; } - const QUuid getSessionUUID() const { return getID(); } glm::vec3 getHandPosition() const; @@ -497,6 +496,8 @@ public: /// \return number of bytes parsed virtual int parseDataFromBuffer(const QByteArray& buffer); + virtual void setCollisionWithOtherAvatarsFlags() {}; + // Body Rotation (degrees) float getBodyYaw() const; void setBodyYaw(float bodyYaw); @@ -1273,12 +1274,12 @@ public slots: * @function MyAvatar.sendAvatarDataPacket * @param {boolean} [sendAll=false] */ - void sendAvatarDataPacket(bool sendAll = false); + virtual int sendAvatarDataPacket(bool sendAll = false); /**jsdoc * @function MyAvatar.sendIdentityPacket */ - void sendIdentityPacket(); + int sendIdentityPacket(); /**jsdoc * @function MyAvatar.setSessionUUID diff --git a/libraries/avatars/src/AvatarHashMap.cpp b/libraries/avatars/src/AvatarHashMap.cpp index 6a67ef6638..5f30d98ed6 100644 --- a/libraries/avatars/src/AvatarHashMap.cpp +++ b/libraries/avatars/src/AvatarHashMap.cpp @@ -195,21 +195,22 @@ int AvatarHashMap::numberOfAvatarsInRange(const glm::vec3& position, float range return count; } -AvatarSharedPointer AvatarHashMap::newSharedAvatar() { - return std::make_shared(); +AvatarSharedPointer AvatarHashMap::newSharedAvatar(const QUuid& sessionUUID) { + auto avatarData = std::make_shared(); + avatarData->setSessionUUID(sessionUUID); + return avatarData; } AvatarSharedPointer AvatarHashMap::addAvatar(const QUuid& sessionUUID, const QWeakPointer& mixerWeakPointer) { qCDebug(avatars) << "Adding avatar with sessionUUID " << sessionUUID << "to AvatarHashMap."; - auto avatar = newSharedAvatar(); + auto avatar = newSharedAvatar(sessionUUID); avatar->setSessionUUID(sessionUUID); avatar->setOwningAvatarMixer(mixerWeakPointer); // addAvatar is only called from newOrExistingAvatar, which already locks _hashLock _avatarHash.insert(sessionUUID, avatar); emit avatarAddedEvent(sessionUUID); - return avatar; } diff --git a/libraries/avatars/src/AvatarHashMap.h b/libraries/avatars/src/AvatarHashMap.h index 3bb38dd081..8395651d6b 100644 --- a/libraries/avatars/src/AvatarHashMap.h +++ b/libraries/avatars/src/AvatarHashMap.h @@ -32,6 +32,9 @@ #include "AvatarData.h" #include "AssociatedTraitValues.h" +const int CLIENT_TO_AVATAR_MIXER_BROADCAST_FRAMES_PER_SECOND = 50; +const quint64 MIN_TIME_BETWEEN_MY_AVATAR_DATA_SENDS = USECS_PER_SECOND / CLIENT_TO_AVATAR_MIXER_BROADCAST_FRAMES_PER_SECOND; + /**jsdoc * Note: An AvatarList API is also provided for Interface and client entity scripts: it is a * synonym for the {@link AvatarManager} API. @@ -179,7 +182,7 @@ protected: AvatarHashMap(); virtual AvatarSharedPointer parseAvatarData(QSharedPointer message, SharedNodePointer sendingNode); - virtual AvatarSharedPointer newSharedAvatar(); + virtual AvatarSharedPointer newSharedAvatar(const QUuid& sessionUUID); virtual AvatarSharedPointer addAvatar(const QUuid& sessionUUID, const QWeakPointer& mixerWeakPointer); AvatarSharedPointer newOrExistingAvatar(const QUuid& sessionUUID, const QWeakPointer& mixerWeakPointer, bool& isNew); diff --git a/libraries/avatars/src/ClientTraitsHandler.cpp b/libraries/avatars/src/ClientTraitsHandler.cpp index 3e24c1f9ad..bcbe5308c7 100644 --- a/libraries/avatars/src/ClientTraitsHandler.cpp +++ b/libraries/avatars/src/ClientTraitsHandler.cpp @@ -65,8 +65,9 @@ void ClientTraitsHandler::resetForNewMixer() { _owningAvatar->prepareResetTraitInstances(); } -void ClientTraitsHandler::sendChangedTraitsToMixer() { +int ClientTraitsHandler::sendChangedTraitsToMixer() { std::unique_lock lock(_traitLock); + int bytesWritten = 0; if (hasChangedTraits() || _shouldPerformInitialSend) { // we have at least one changed trait to send @@ -75,7 +76,7 @@ void ClientTraitsHandler::sendChangedTraitsToMixer() { auto avatarMixer = nodeList->soloNodeOfType(NodeType::AvatarMixer); if (!avatarMixer || !avatarMixer->getActiveSocket()) { // we don't have an avatar mixer with an active socket, we can't send changed traits at this time - return; + return 0; } // we have a mixer to send to, setup our set traits packet @@ -106,7 +107,7 @@ void ClientTraitsHandler::sendChangedTraitsToMixer() { if (initialSend || *simpleIt == Updated) { if (traitType == AvatarTraits::SkeletonModelURL) { - _owningAvatar->packTrait(traitType, *traitsPacketList); + bytesWritten += _owningAvatar->packTrait(traitType, *traitsPacketList); // keep track of our skeleton version in case we get an override back _currentSkeletonVersion = _currentTraitVersion; @@ -123,10 +124,10 @@ void ClientTraitsHandler::sendChangedTraitsToMixer() { || instanceIDValuePair.value == Updated) { // this is a changed trait we need to send or we haven't send out trait information yet // ask the owning avatar to pack it - _owningAvatar->packTraitInstance(instancedIt->traitType, instanceIDValuePair.id, *traitsPacketList); + bytesWritten += _owningAvatar->packTraitInstance(instancedIt->traitType, instanceIDValuePair.id, *traitsPacketList); } else if (!initialSend && instanceIDValuePair.value == Deleted) { // pack delete for this trait instance - AvatarTraits::packInstancedTraitDelete(instancedIt->traitType, instanceIDValuePair.id, + bytesWritten += AvatarTraits::packInstancedTraitDelete(instancedIt->traitType, instanceIDValuePair.id, *traitsPacketList); } } @@ -136,6 +137,8 @@ void ClientTraitsHandler::sendChangedTraitsToMixer() { nodeList->sendPacketList(std::move(traitsPacketList), *avatarMixer); } + + return bytesWritten; } void ClientTraitsHandler::processTraitOverride(QSharedPointer message, SharedNodePointer sendingNode) { diff --git a/libraries/avatars/src/ClientTraitsHandler.h b/libraries/avatars/src/ClientTraitsHandler.h index 3900268101..35499fd2cf 100644 --- a/libraries/avatars/src/ClientTraitsHandler.h +++ b/libraries/avatars/src/ClientTraitsHandler.h @@ -24,7 +24,7 @@ class ClientTraitsHandler : public QObject { public: ClientTraitsHandler(AvatarData* owningAvatar); - void sendChangedTraitsToMixer(); + int sendChangedTraitsToMixer(); bool hasChangedTraits() const { return _hasChangedTraits; } diff --git a/libraries/controllers/src/controllers/Input.h b/libraries/controllers/src/controllers/Input.h index 3c01ee0942..f2a5ca1296 100644 --- a/libraries/controllers/src/controllers/Input.h +++ b/libraries/controllers/src/controllers/Input.h @@ -15,6 +15,11 @@ namespace controller { +enum class HmdAvatarAlignmentType { + Eyes = 0, // align the user's eyes with the avatars eyes + Head // align the user's head with the avatars head +}; + struct InputCalibrationData { glm::mat4 sensorToWorldMat; // sensor to world glm::mat4 avatarMat; // avatar to world @@ -29,6 +34,7 @@ struct InputCalibrationData { glm::mat4 defaultLeftArm; // default pose for leftArm joint in sensor space glm::mat4 defaultRightHand; // default pose for rightHand joint in sensor space glm::mat4 defaultLeftHand; // default pose for leftHand joint in sensor space + HmdAvatarAlignmentType hmdAvatarAlignmentType; }; enum class ChannelType { diff --git a/libraries/display-plugins/src/display-plugins/Basic2DWindowOpenGLDisplayPlugin.h b/libraries/display-plugins/src/display-plugins/Basic2DWindowOpenGLDisplayPlugin.h index a061a4c923..944d5e89d1 100644 --- a/libraries/display-plugins/src/display-plugins/Basic2DWindowOpenGLDisplayPlugin.h +++ b/libraries/display-plugins/src/display-plugins/Basic2DWindowOpenGLDisplayPlugin.h @@ -31,6 +31,8 @@ public: virtual void compositeExtra() override; + virtual void pluginUpdate() override {}; + protected: mutable bool _isThrottled = false; diff --git a/libraries/display-plugins/src/display-plugins/NullDisplayPlugin.h b/libraries/display-plugins/src/display-plugins/NullDisplayPlugin.h index 11563b3798..e4ff1b8b37 100644 --- a/libraries/display-plugins/src/display-plugins/NullDisplayPlugin.h +++ b/libraries/display-plugins/src/display-plugins/NullDisplayPlugin.h @@ -20,6 +20,7 @@ public: QImage getScreenshot(float aspectRatio = 0.0f) const override; QImage getSecondaryCameraScreenshot() const override; void copyTextureToQuickFramebuffer(NetworkTexturePointer source, QOpenGLFramebufferObject* target, GLsync* fenceSync) override {}; + void pluginUpdate() override {}; private: static const QString NAME; }; diff --git a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.h b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.h index a56daaad83..4aeacbe05c 100644 --- a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.h +++ b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.h @@ -46,6 +46,8 @@ public: virtual bool onDisplayTextureReset() override { _clearPreviewFlag = true; return true; }; + void pluginUpdate() override {}; + signals: void hmdMountedChanged(); void hmdVisibleChanged(bool visible); diff --git a/libraries/display-plugins/src/display-plugins/stereo/StereoDisplayPlugin.h b/libraries/display-plugins/src/display-plugins/stereo/StereoDisplayPlugin.h index 5a7ca24059..a55bde0f4e 100644 --- a/libraries/display-plugins/src/display-plugins/stereo/StereoDisplayPlugin.h +++ b/libraries/display-plugins/src/display-plugins/stereo/StereoDisplayPlugin.h @@ -28,6 +28,8 @@ public: // to the HMD plugins. //virtual glm::mat4 getEyeToHeadTransform(Eye eye) const override; + virtual void pluginUpdate() override {}; + protected: virtual bool internalActivate() override; virtual void internalDeactivate() override; diff --git a/libraries/entities-renderer/src/EntityTreeRenderer.cpp b/libraries/entities-renderer/src/EntityTreeRenderer.cpp index 380998321f..c71b296a74 100644 --- a/libraries/entities-renderer/src/EntityTreeRenderer.cpp +++ b/libraries/entities-renderer/src/EntityTreeRenderer.cpp @@ -197,9 +197,57 @@ void EntityTreeRenderer::resetEntitiesScriptEngine() { }); } +void EntityTreeRenderer::stopNonLocalEntityScripts() { + leaveNonLocalEntities(); + // unload and stop the engine + if (_entitiesScriptEngine) { + QList entitiesWithEntityScripts = _entitiesScriptEngine->getListOfEntityScriptIDs(); + + foreach (const EntityItemID& entityID, entitiesWithEntityScripts) { + EntityItemPointer entityItem = getTree()->findEntityByEntityItemID(entityID); + + if (entityItem) { + if (!entityItem->isLocalEntity()) { + _entitiesScriptEngine->unloadEntityScript(entityID, true); + } + } + } + } +} + +void EntityTreeRenderer::clearNonLocalEntities() { + stopNonLocalEntityScripts(); + + std::unordered_map savedEntities; + // remove all entities from the scene + _space->clear(); + auto scene = _viewState->getMain3DScene(); + if (scene) { + render::Transaction transaction; + for (const auto& entry : _entitiesInScene) { + const auto& renderer = entry.second; + const EntityItemPointer& entityItem = renderer->getEntity(); + if (!entityItem->isLocalEntity()) { + renderer->removeFromScene(scene, transaction); + } else { + savedEntities[entry.first] = entry.second; + } + } + scene->enqueueTransaction(transaction); + } else { + qCWarning(entitiesrenderer) << "EntitityTreeRenderer::clear(), Unexpected null scene, possibly during application shutdown"; + } + + _renderablesToUpdate = savedEntities; + _entitiesInScene = savedEntities; + + _layeredZones.clearNonLocalLayeredZones(); + + OctreeProcessor::clearNonLocalEntities(); +} + void EntityTreeRenderer::clear() { leaveAllEntities(); - // unload and stop the engine if (_entitiesScriptEngine) { // do this here (instead of in deleter) to avoid marshalling unload signals back to this thread @@ -211,8 +259,8 @@ void EntityTreeRenderer::clear() { if (_wantScripts && !_shuttingDown) { resetEntitiesScriptEngine(); } - // remove all entities from the scene + _space->clear(); auto scene = _viewState->getMain3DScene(); if (scene) { @@ -507,8 +555,7 @@ bool EntityTreeRenderer::findBestZoneAndMaybeContainingEntities(QVector(_tree); // FIXME - if EntityTree had a findEntitiesContainingPoint() this could theoretically be a little faster - entityTree->evalEntitiesInSphere(_avatarPosition, radius, - PickFilter(PickFilter::getBitMask(PickFilter::FlagBit::DOMAIN_ENTITIES) | PickFilter::getBitMask(PickFilter::FlagBit::AVATAR_ENTITIES)), entityIDs); + entityTree->evalEntitiesInSphere(_avatarPosition, radius, PickFilter(), entityIDs); LayeredZones oldLayeredZones(std::move(_layeredZones)); _layeredZones.clear(); @@ -614,6 +661,26 @@ bool EntityTreeRenderer::checkEnterLeaveEntities() { return didUpdate; } +void EntityTreeRenderer::leaveNonLocalEntities() { + if (_tree && !_shuttingDown) { + QVector currentLocalEntitiesInside; + foreach (const EntityItemID& entityID, _currentEntitiesInside) { + EntityItemPointer entityItem = getTree()->findEntityByEntityItemID(entityID); + if (!entityItem->isLocalEntity()) { + emit leaveEntity(entityID); + if (_entitiesScriptEngine) { + _entitiesScriptEngine->callEntityScriptMethod(entityID, "leaveEntity"); + } + } else { + currentLocalEntitiesInside.push_back(entityID); + } + } + + _currentEntitiesInside = currentLocalEntitiesInside; + forceRecheckEntities(); + } +} + void EntityTreeRenderer::leaveAllEntities() { if (_tree && !_shuttingDown) { @@ -1136,6 +1203,29 @@ EntityTreeRenderer::LayeredZones::LayeredZones(LayeredZones&& other) { } } +void EntityTreeRenderer::LayeredZones::clearNonLocalLayeredZones() { + std::set localLayeredZones; + std::map newMap; + + for (auto iter = begin(); iter != end(); iter++) { + LayeredZone layeredZone = *iter; + + if (layeredZone.zone->isLocalEntity()) { + bool success; + iterator it; + std::tie(it, success) = localLayeredZones.insert(layeredZone); + + if (success) { + newMap.emplace(layeredZone.id, it); + } + } + } + + std::set::operator=(localLayeredZones); + _map = newMap; + _skyboxLayer = empty() ? end() : begin(); +} + void EntityTreeRenderer::LayeredZones::clear() { std::set::clear(); _map.clear(); diff --git a/libraries/entities-renderer/src/EntityTreeRenderer.h b/libraries/entities-renderer/src/EntityTreeRenderer.h index b4f0bda703..d9f594a20b 100644 --- a/libraries/entities-renderer/src/EntityTreeRenderer.h +++ b/libraries/entities-renderer/src/EntityTreeRenderer.h @@ -86,6 +86,7 @@ public: virtual void init() override; /// clears the tree + virtual void clearNonLocalEntities() override; virtual void clear() override; /// reloads the entity scripts, calling unload and preload @@ -161,6 +162,7 @@ private: bool findBestZoneAndMaybeContainingEntities(QVector* entitiesContainingAvatar = nullptr); bool applyLayeredZones(); + void stopNonLocalEntityScripts(); void checkAndCallPreload(const EntityItemID& entityID, bool reload = false, bool unloadFirst = false); @@ -169,6 +171,7 @@ private: QScriptValueList createEntityArgs(const EntityItemID& entityID); bool checkEnterLeaveEntities(); + void leaveNonLocalEntities(); void leaveAllEntities(); void forceRecheckEntities(); @@ -219,6 +222,7 @@ private: LayeredZones& operator=(LayeredZones&&) = delete; void clear(); + void clearNonLocalLayeredZones(); std::pair insert(const LayeredZone& layer); void update(std::shared_ptr zone); bool contains(const LayeredZones& other); diff --git a/libraries/entities-renderer/src/RenderableEntityItem.cpp b/libraries/entities-renderer/src/RenderableEntityItem.cpp index 5fb5a15d2c..53e62ee35f 100644 --- a/libraries/entities-renderer/src/RenderableEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableEntityItem.cpp @@ -159,20 +159,40 @@ Item::Bound EntityRenderer::getBound() { return _bound; } +ShapeKey EntityRenderer::getShapeKey() { + if (_primitiveMode == PrimitiveMode::LINES) { + return ShapeKey::Builder().withOwnPipeline().withWireframe(); + } + return ShapeKey::Builder().withOwnPipeline(); +} + render::hifi::Tag EntityRenderer::getTagMask() const { return _isVisibleInSecondaryCamera ? render::hifi::TAG_ALL_VIEWS : render::hifi::TAG_MAIN_VIEW; } +render::hifi::Layer EntityRenderer::getHifiRenderLayer() const { + switch (_renderLayer) { + case RenderLayer::WORLD: + return render::hifi::LAYER_3D; + case RenderLayer::FRONT: + return render::hifi::LAYER_3D_FRONT; + case RenderLayer::HUD: + return render::hifi::LAYER_3D_HUD; + default: + return render::hifi::LAYER_3D; + } +} + ItemKey EntityRenderer::getKey() { if (isTransparent()) { - return ItemKey::Builder::transparentShape().withTypeMeta().withTagBits(getTagMask()); + return ItemKey::Builder::transparentShape().withTypeMeta().withTagBits(getTagMask()).withLayer(getHifiRenderLayer()); } // This allows shapes to cast shadows if (_canCastShadow) { - return ItemKey::Builder::opaqueShape().withTypeMeta().withTagBits(getTagMask()).withShadowCaster(); + return ItemKey::Builder::opaqueShape().withTypeMeta().withTagBits(getTagMask()).withShadowCaster().withLayer(getHifiRenderLayer()); } else { - return ItemKey::Builder::opaqueShape().withTypeMeta().withTagBits(getTagMask()); + return ItemKey::Builder::opaqueShape().withTypeMeta().withTagBits(getTagMask()).withLayer(getHifiRenderLayer()); } } @@ -411,6 +431,8 @@ void EntityRenderer::doRenderUpdateSynchronous(const ScenePointer& scene, Transa _moving = entity->isMovingRelativeToParent(); _visible = entity->getVisible(); setIsVisibleInSecondaryCamera(entity->isVisibleInSecondaryCamera()); + setRenderLayer(entity->getRenderLayer()); + setPrimitiveMode(entity->getPrimitiveMode()); _canCastShadow = entity->getCanCastShadow(); _cauterized = entity->getCauterized(); _needsRenderUpdate = false; diff --git a/libraries/entities-renderer/src/RenderableEntityItem.h b/libraries/entities-renderer/src/RenderableEntityItem.h index 9c4d10190c..fde63f78fa 100644 --- a/libraries/entities-renderer/src/RenderableEntityItem.h +++ b/libraries/entities-renderer/src/RenderableEntityItem.h @@ -72,11 +72,12 @@ protected: // Implementing the PayloadProxyInterface methods virtual ItemKey getKey() override; - virtual ShapeKey getShapeKey() override { return ShapeKey::Builder::ownPipeline(); } + virtual ShapeKey getShapeKey() override; virtual Item::Bound getBound() override; virtual void render(RenderArgs* args) override final; virtual uint32_t metaFetchMetaSubItems(ItemIDs& subItems) override; virtual render::hifi::Tag getTagMask() const; + virtual render::hifi::Layer getHifiRenderLayer() const; // Returns true if the item in question needs to have updateInScene called because of internal rendering state changes virtual bool needsRenderUpdate() const; @@ -103,6 +104,8 @@ protected: inline bool isValidRenderItem() const { return _renderItemID != Item::INVALID_ITEM_ID; } virtual void setIsVisibleInSecondaryCamera(bool value) { _isVisibleInSecondaryCamera = value; } + virtual void setRenderLayer(RenderLayer value) { _renderLayer = value; } + virtual void setPrimitiveMode(PrimitiveMode value) { _primitiveMode = value; } template T withReadLockResult(const std::function& f) { @@ -136,6 +139,8 @@ protected: bool _visible { false }; bool _isVisibleInSecondaryCamera { false }; bool _canCastShadow { false }; + RenderLayer _renderLayer { RenderLayer::WORLD }; + PrimitiveMode _primitiveMode { PrimitiveMode::SOLID }; bool _cauterized { false }; bool _moving { false }; bool _needsRenderUpdate { false }; diff --git a/libraries/entities-renderer/src/RenderableGridEntityItem.cpp b/libraries/entities-renderer/src/RenderableGridEntityItem.cpp index 22cf72cec6..4576358699 100644 --- a/libraries/entities-renderer/src/RenderableGridEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableGridEntityItem.cpp @@ -97,6 +97,10 @@ ShapeKey GridEntityRenderer::getShapeKey() { builder.withTranslucent(); } + if (_primitiveMode == PrimitiveMode::LINES) { + builder.withWireframe(); + } + return builder.build(); } diff --git a/libraries/entities-renderer/src/RenderableImageEntityItem.cpp b/libraries/entities-renderer/src/RenderableImageEntityItem.cpp index 7c5b7fc0da..c1d6d3211d 100644 --- a/libraries/entities-renderer/src/RenderableImageEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableImageEntityItem.cpp @@ -123,6 +123,10 @@ ShapeKey ImageEntityRenderer::getShapeKey() { if (_emissive) { builder.withUnlit(); } + + if (_primitiveMode == PrimitiveMode::LINES) { + builder.withWireframe(); + } }); return builder.build(); diff --git a/libraries/entities-renderer/src/RenderableMaterialEntityItem.cpp b/libraries/entities-renderer/src/RenderableMaterialEntityItem.cpp index 6451e873c9..483f9ffe1c 100644 --- a/libraries/entities-renderer/src/RenderableMaterialEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableMaterialEntityItem.cpp @@ -55,7 +55,7 @@ void MaterialEntityRenderer::doRenderUpdateSynchronousTyped(const ScenePointer& ItemKey MaterialEntityRenderer::getKey() { ItemKey::Builder builder; - builder.withTypeShape().withTagBits(getTagMask()); + builder.withTypeShape().withTagBits(getTagMask()).withLayer(getHifiRenderLayer()); if (!_visible) { builder.withInvisible(); @@ -98,6 +98,10 @@ ShapeKey MaterialEntityRenderer::getShapeKey() { builder.withUnlit(); } + if (_primitiveMode == PrimitiveMode::LINES) { + builder.withWireframe(); + } + return builder.build(); } diff --git a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp index aa449b8919..7e01af04dd 100644 --- a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp @@ -1074,10 +1074,16 @@ ModelEntityRenderer::ModelEntityRenderer(const EntityItemPointer& entity) : Pare } void ModelEntityRenderer::setKey(bool didVisualGeometryRequestSucceed) { + auto builder = ItemKey::Builder().withTypeMeta().withTagBits(getTagMask()).withLayer(getHifiRenderLayer()); + + if (_model && _model->isGroupCulled()) { + builder.withMetaCullGroup(); + } + if (didVisualGeometryRequestSucceed) { - _itemKey = ItemKey::Builder().withTypeMeta().withTagBits(getTagMask()); + _itemKey = builder.build(); } else { - _itemKey = ItemKey::Builder().withTypeMeta().withTypeShape().withTagBits(getTagMask()); + _itemKey = builder.withTypeShape().build(); } } @@ -1295,6 +1301,10 @@ bool ModelEntityRenderer::needsRenderUpdateFromTypedEntity(const TypedEntityPoin model->getRegistrationPoint() != entity->getRegistrationPoint()) { return true; } + + if (model->isGroupCulled() != entity->getGroupCulled()) { + return true; + } } return false; @@ -1351,6 +1361,8 @@ void ModelEntityRenderer::doRenderUpdateSynchronousTyped(const ScenePointer& sce connect(model.get(), &Model::requestRenderUpdate, this, &ModelEntityRenderer::requestRenderUpdate); connect(model.get(), &Model::setURLFinished, this, [&](bool didVisualGeometryRequestSucceed) { setKey(didVisualGeometryRequestSucceed); + _model->setTagMask(getTagMask()); + _model->setHifiRenderLayer(getHifiRenderLayer()); emit requestRenderUpdate(); if(didVisualGeometryRequestSucceed) { emit DependencyManager::get()-> @@ -1437,6 +1449,14 @@ void ModelEntityRenderer::doRenderUpdateSynchronousTyped(const ScenePointer& sce model->setCanCastShadow(_canCastShadow, scene); } + { + bool groupCulled = entity->getGroupCulled(); + if (model->isGroupCulled() != groupCulled) { + model->setGroupCulled(groupCulled); + setKey(_didLastVisualGeometryRequestSucceed); + } + } + { DETAILED_PROFILE_RANGE(simulation_physics, "Fixup"); if (model->needsFixupInScene()) { @@ -1494,6 +1514,24 @@ void ModelEntityRenderer::doRenderUpdateSynchronousTyped(const ScenePointer& sce void ModelEntityRenderer::setIsVisibleInSecondaryCamera(bool value) { Parent::setIsVisibleInSecondaryCamera(value); setKey(_didLastVisualGeometryRequestSucceed); + if (_model) { + _model->setTagMask(getTagMask()); + } +} + +void ModelEntityRenderer::setRenderLayer(RenderLayer value) { + Parent::setRenderLayer(value); + setKey(_didLastVisualGeometryRequestSucceed); + if (_model) { + _model->setHifiRenderLayer(getHifiRenderLayer()); + } +} + +void ModelEntityRenderer::setPrimitiveMode(PrimitiveMode value) { + Parent::setPrimitiveMode(value); + if (_model) { + _model->setPrimitiveMode(_primitiveMode); + } } // NOTE: this only renders the "meta" portion of the Model, namely it renders debugging items diff --git a/libraries/entities-renderer/src/RenderableModelEntityItem.h b/libraries/entities-renderer/src/RenderableModelEntityItem.h index 725c1d96c3..16c3664f28 100644 --- a/libraries/entities-renderer/src/RenderableModelEntityItem.h +++ b/libraries/entities-renderer/src/RenderableModelEntityItem.h @@ -169,6 +169,8 @@ protected: render::hifi::Tag getTagMask() const override; void setIsVisibleInSecondaryCamera(bool value) override; + void setRenderLayer(RenderLayer value) override; + void setPrimitiveMode(PrimitiveMode value) override; private: void animate(const TypedEntityPointer& entity); diff --git a/libraries/entities-renderer/src/RenderableParticleEffectEntityItem.cpp b/libraries/entities-renderer/src/RenderableParticleEffectEntityItem.cpp index 38027a80ed..351d72baf5 100644 --- a/libraries/entities-renderer/src/RenderableParticleEffectEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableParticleEffectEntityItem.cpp @@ -159,14 +159,18 @@ void ParticleEffectEntityRenderer::doRenderUpdateAsynchronousTyped(const TypedEn ItemKey ParticleEffectEntityRenderer::getKey() { if (_visible) { - return ItemKey::Builder::transparentShape().withTagBits(getTagMask()); + return ItemKey::Builder::transparentShape().withTagBits(getTagMask()).withLayer(getHifiRenderLayer()); } else { - return ItemKey::Builder().withInvisible().withTagBits(getTagMask()).build(); + return ItemKey::Builder().withInvisible().withTagBits(getTagMask()).withLayer(getHifiRenderLayer()).build(); } } ShapeKey ParticleEffectEntityRenderer::getShapeKey() { - return ShapeKey::Builder().withCustom(CUSTOM_PIPELINE_NUMBER).withTranslucent().build(); + auto builder = ShapeKey::Builder().withCustom(CUSTOM_PIPELINE_NUMBER).withTranslucent(); + if (_primitiveMode == PrimitiveMode::LINES) { + builder.withWireframe(); + } + return builder.build(); } Item::Bound ParticleEffectEntityRenderer::getBound() { diff --git a/libraries/entities-renderer/src/RenderablePolyLineEntityItem.cpp b/libraries/entities-renderer/src/RenderablePolyLineEntityItem.cpp index d4a10e551d..68371e4e13 100644 --- a/libraries/entities-renderer/src/RenderablePolyLineEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderablePolyLineEntityItem.cpp @@ -55,11 +55,15 @@ void PolyLineEntityRenderer::buildPipeline() { } ItemKey PolyLineEntityRenderer::getKey() { - return ItemKey::Builder::transparentShape().withTypeMeta().withTagBits(getTagMask()); + return ItemKey::Builder::transparentShape().withTypeMeta().withTagBits(getTagMask()).withLayer(getHifiRenderLayer()); } ShapeKey PolyLineEntityRenderer::getShapeKey() { - return ShapeKey::Builder().withOwnPipeline().withTranslucent().withoutCullFace(); + auto builder = ShapeKey::Builder().withOwnPipeline().withTranslucent().withoutCullFace(); + if (_primitiveMode == PrimitiveMode::LINES) { + builder.withWireframe(); + } + return builder.build(); } bool PolyLineEntityRenderer::needsRenderUpdate() const { diff --git a/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.cpp b/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.cpp index 514ed3cec1..183d2881f3 100644 --- a/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.cpp @@ -1613,7 +1613,11 @@ PolyVoxEntityRenderer::PolyVoxEntityRenderer(const EntityItemPointer& entity) : } ShapeKey PolyVoxEntityRenderer::getShapeKey() { - return ShapeKey::Builder().withCustom(CUSTOM_PIPELINE_NUMBER).build(); + auto builder = ShapeKey::Builder().withCustom(CUSTOM_PIPELINE_NUMBER); + if (_primitiveMode == PrimitiveMode::LINES) { + builder.withWireframe(); + } + return builder.build(); } bool PolyVoxEntityRenderer::needsRenderUpdateFromTypedEntity(const TypedEntityPointer& entity) const { diff --git a/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.h b/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.h index 366a3fdc70..7aea87535e 100644 --- a/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.h +++ b/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.h @@ -173,7 +173,7 @@ public: } protected: - virtual ItemKey getKey() override { return ItemKey::Builder::opaqueShape().withTagBits(getTagMask()); } + virtual ItemKey getKey() override { return ItemKey::Builder::opaqueShape().withTagBits(getTagMask()).withLayer(getHifiRenderLayer()); } virtual ShapeKey getShapeKey() override; virtual bool needsRenderUpdateFromTypedEntity(const TypedEntityPointer& entity) const override; virtual void doRenderUpdateSynchronousTyped(const ScenePointer& scene, Transaction& transaction, const TypedEntityPointer& entity) override; diff --git a/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp b/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp index 1569c75eec..d5a8aaafe2 100644 --- a/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp @@ -42,10 +42,23 @@ ShapeEntityRenderer::ShapeEntityRenderer(const EntityItemPointer& entity) : Pare // TODO: move into Procedural.cpp PrepareStencil::testMaskDrawShape(*_procedural._opaqueState); PrepareStencil::testMask(*_procedural._transparentState); + + addMaterial(graphics::MaterialLayer(_material, 0), "0"); } bool ShapeEntityRenderer::needsRenderUpdate() const { - if (_procedural.isEnabled() && _procedural.isFading()) { + if (resultWithReadLock([&] { + if (_procedural.isEnabled() && _procedural.isFading()) { + return true; + } + + auto mat = _materials.find("0"); + if (mat != _materials.end() && mat->second.needsUpdate()) { + return true; + } + + return false; + })) { return true; } @@ -56,7 +69,11 @@ bool ShapeEntityRenderer::needsRenderUpdateFromTypedEntity(const TypedEntityPoin if (_lastUserData != entity->getUserData()) { return true; } - if (_material != entity->getMaterial()) { + + if (_color != entity->getColor()) { + return true; + } + if (_alpha != entity->getAlpha()) { return true; } @@ -79,10 +96,6 @@ void ShapeEntityRenderer::doRenderUpdateSynchronousTyped(const ScenePointer& sce _procedural.setProceduralData(ProceduralData::parse(_lastUserData)); } - removeMaterial(_material, "0"); - _material = entity->getMaterial(); - addMaterial(graphics::MaterialLayer(_material, 0), "0"); - _shape = entity->getShape(); }); @@ -111,6 +124,20 @@ void ShapeEntityRenderer::doRenderUpdateAsynchronousTyped(const TypedEntityPoint _procedural.setIsFading(isFading); } }); + + glm::u8vec3 color = entity->getColor(); + float alpha = entity->getAlpha(); + if (_color != color || _alpha != alpha) { + _color = color; + _alpha = alpha; + _material->setAlbedo(toGlm(_color)); + _material->setOpacity(_alpha); + + auto materials = _materials.find("0"); + if (materials != _materials.end()) { + materials->second.setNeedsUpdate(true); + } + } } bool ShapeEntityRenderer::isTransparent() const { @@ -120,18 +147,15 @@ bool ShapeEntityRenderer::isTransparent() const { auto mat = _materials.find("0"); if (mat != _materials.end()) { - if (mat->second.top().material) { - auto matKey = mat->second.top().material->getKey(); - if (matKey.isTranslucent()) { - return true; - } + if (mat->second.getMaterialKey().isTranslucent()) { + return true; } } return Parent::isTransparent(); } -bool ShapeEntityRenderer::useMaterialPipeline() const { +bool ShapeEntityRenderer::useMaterialPipeline(const graphics::MultiMaterial& materials) const { bool proceduralReady = resultWithReadLock([&] { return _procedural.isReady(); }); @@ -139,12 +163,7 @@ bool ShapeEntityRenderer::useMaterialPipeline() const { return false; } - graphics::MaterialKey drawMaterialKey; - auto mat = _materials.find("0"); - if (mat != _materials.end() && mat->second.top().material) { - drawMaterialKey = mat->second.top().material->getKey(); - } - + graphics::MaterialKey drawMaterialKey = materials.getMaterialKey(); if (drawMaterialKey.isEmissive() || drawMaterialKey.isUnlit() || drawMaterialKey.isMetallic() || drawMaterialKey.isScattering()) { return true; } @@ -159,11 +178,13 @@ bool ShapeEntityRenderer::useMaterialPipeline() const { } ShapeKey ShapeEntityRenderer::getShapeKey() { - if (useMaterialPipeline()) { - graphics::MaterialKey drawMaterialKey; - if (_materials["0"].top().material) { - drawMaterialKey = _materials["0"].top().material->getKey(); - } + auto mat = _materials.find("0"); + if (mat != _materials.end() && mat->second.needsUpdate()) { + RenderPipelines::updateMultiMaterial(mat->second); + } + + if (mat != _materials.end() && useMaterialPipeline(mat->second)) { + graphics::MaterialKey drawMaterialKey = mat->second.getMaterialKey(); bool isTranslucent = drawMaterialKey.isTranslucent(); bool hasTangents = drawMaterialKey.isNormalMap(); @@ -186,6 +207,10 @@ ShapeKey ShapeEntityRenderer::getShapeKey() { builder.withUnlit(); } + if (_primitiveMode == PrimitiveMode::LINES) { + builder.withWireframe(); + } + return builder.build(); } else { ShapeKey::Builder builder; @@ -198,6 +223,10 @@ ShapeKey ShapeEntityRenderer::getShapeKey() { if (isTransparent()) { builder.withTranslucent(); } + + if (_primitiveMode == PrimitiveMode::LINES) { + builder.withWireframe(); + } return builder.build(); } } @@ -208,7 +237,7 @@ void ShapeEntityRenderer::doRender(RenderArgs* args) { gpu::Batch& batch = *args->_batch; - std::shared_ptr mat; + graphics::MultiMaterial materials; auto geometryCache = DependencyManager::get(); GeometryCache::Shape geometryShape; bool proceduralRender = false; @@ -216,40 +245,40 @@ void ShapeEntityRenderer::doRender(RenderArgs* args) { withReadLock([&] { geometryShape = geometryCache->getShapeForEntityShape(_shape); batch.setModelTransform(_renderTransform); // use a transform with scale, rotation, registration point and translation - mat = _materials["0"].top().material; - if (mat) { - outColor = glm::vec4(mat->getAlbedo(), mat->getOpacity()); - if (_procedural.isReady()) { - outColor = _procedural.getColor(outColor); - outColor.a *= _procedural.isFading() ? Interpolate::calculateFadeRatio(_procedural.getFadeStartTime()) : 1.0f; - _procedural.prepare(batch, _position, _dimensions, _orientation, ProceduralProgramKey(outColor.a < 1.0f)); - proceduralRender = true; - } + materials = _materials["0"]; + auto& schema = materials.getSchemaBuffer().get(); + outColor = glm::vec4(schema._albedo, schema._opacity); + if (_procedural.isReady()) { + outColor = _procedural.getColor(outColor); + outColor.a *= _procedural.isFading() ? Interpolate::calculateFadeRatio(_procedural.getFadeStartTime()) : 1.0f; + _procedural.prepare(batch, _position, _dimensions, _orientation, ProceduralProgramKey(outColor.a < 1.0f)); + proceduralRender = true; } }); - if (!mat) { - return; - } - if (proceduralRender) { if (render::ShapeKey(args->_globalShapeKey).isWireframe()) { geometryCache->renderWireShape(batch, geometryShape, outColor); } else { geometryCache->renderShape(batch, geometryShape, outColor); } - } else if (!useMaterialPipeline()) { + } else if (!useMaterialPipeline(materials)) { // FIXME, support instanced multi-shape rendering using multidraw indirect outColor.a *= _isFading ? Interpolate::calculateFadeRatio(_fadeStartTime) : 1.0f; - auto pipeline = outColor.a < 1.0f ? geometryCache->getTransparentShapePipeline() : geometryCache->getOpaqueShapePipeline(); - if (render::ShapeKey(args->_globalShapeKey).isWireframe()) { + render::ShapePipelinePointer pipeline; + if (_renderLayer == RenderLayer::WORLD) { + pipeline = outColor.a < 1.0f ? geometryCache->getTransparentShapePipeline() : geometryCache->getOpaqueShapePipeline(); + } else { + pipeline = outColor.a < 1.0f ? geometryCache->getForwardTransparentShapePipeline() : geometryCache->getForwardOpaqueShapePipeline(); + } + if (render::ShapeKey(args->_globalShapeKey).isWireframe() || _primitiveMode == PrimitiveMode::LINES) { geometryCache->renderWireShapeInstance(args, batch, geometryShape, outColor, pipeline); } else { geometryCache->renderSolidShapeInstance(args, batch, geometryShape, outColor, pipeline); } } else { if (args->_renderMode != render::Args::RenderMode::SHADOW_RENDER_MODE) { - RenderPipelines::bindMaterial(mat, batch, args->_enableTexturing); + RenderPipelines::bindMaterials(materials, batch, args->_enableTexturing); args->_details._materialSwitches++; } @@ -268,8 +297,9 @@ scriptable::ScriptableModelBase ShapeEntityRenderer::getScriptableModel() { { std::lock_guard lock(_materialsLock); result.appendMaterials(_materials); - if (_materials["0"].top().material) { - vertexColor = _materials["0"].top().material->getAlbedo(); + auto materials = _materials.find("0"); + if (materials != _materials.end()) { + vertexColor = materials->second.getSchemaBuffer().get()._albedo; } } if (auto mesh = geometryCache->meshFromShape(geometryShape, vertexColor)) { diff --git a/libraries/entities-renderer/src/RenderableShapeEntityItem.h b/libraries/entities-renderer/src/RenderableShapeEntityItem.h index a33f023213..ece13fabe9 100644 --- a/libraries/entities-renderer/src/RenderableShapeEntityItem.h +++ b/libraries/entities-renderer/src/RenderableShapeEntityItem.h @@ -35,12 +35,14 @@ private: virtual void doRender(RenderArgs* args) override; virtual bool isTransparent() const override; - bool useMaterialPipeline() const; + bool useMaterialPipeline(const graphics::MultiMaterial& materials) const; Procedural _procedural; QString _lastUserData; entity::Shape _shape { entity::Sphere }; - std::shared_ptr _material; + std::shared_ptr _material { std::make_shared() }; + glm::u8vec3 _color; + float _alpha; glm::vec3 _position; glm::vec3 _dimensions; glm::quat _orientation; diff --git a/libraries/entities-renderer/src/RenderableTextEntityItem.cpp b/libraries/entities-renderer/src/RenderableTextEntityItem.cpp index 4ddb398fbf..d7da8e7e1a 100644 --- a/libraries/entities-renderer/src/RenderableTextEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableTextEntityItem.cpp @@ -49,6 +49,9 @@ ShapeKey TextEntityRenderer::getShapeKey() { if (isTransparent()) { builder.withTranslucent(); } + if (_primitiveMode == PrimitiveMode::LINES) { + builder.withWireframe(); + } return builder.build(); } diff --git a/libraries/entities-renderer/src/RenderableWebEntityItem.cpp b/libraries/entities-renderer/src/RenderableWebEntityItem.cpp index 2942de0ba4..38d1320aa2 100644 --- a/libraries/entities-renderer/src/RenderableWebEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableWebEntityItem.cpp @@ -30,22 +30,26 @@ using namespace render; using namespace render::entities; -static const QString WEB_ENTITY_QML = "controls/WebEntityView.qml"; +const QString WebEntityRenderer::QML = "Web3DSurface.qml"; +const char* WebEntityRenderer::URL_PROPERTY = "url"; +std::function&, bool&)> WebEntityRenderer::_acquireWebSurfaceOperator = nullptr; +std::function&, bool&, std::vector&)> WebEntityRenderer::_releaseWebSurfaceOperator = nullptr; + +static int MAX_WINDOW_SIZE = 4096; const float METERS_TO_INCHES = 39.3701f; -static uint32_t _currentWebCount{ 0 }; -// Don't allow more than 20 concurrent web views -static const uint32_t MAX_CONCURRENT_WEB_VIEWS = 20; +static float OPAQUE_ALPHA_THRESHOLD = 0.99f; + // If a web-view hasn't been rendered for 30 seconds, de-allocate the framebuffer static uint64_t MAX_NO_RENDER_INTERVAL = 30 * USECS_PER_SECOND; -static int MAX_WINDOW_SIZE = 4096; -static float OPAQUE_ALPHA_THRESHOLD = 0.99f; -static int DEFAULT_MAX_FPS = 10; -static int YOUTUBE_MAX_FPS = 30; +static uint8_t YOUTUBE_MAX_FPS = 30; + +// Don't allow more than 20 concurrent web views +static uint32_t _currentWebCount { 0 }; +static const uint32_t MAX_CONCURRENT_WEB_VIEWS = 20; static QTouchDevice _touchDevice; -static const char* URL_PROPERTY = "url"; WebEntityRenderer::ContentType WebEntityRenderer::getContentType(const QString& urlString) { if (urlString.isEmpty()) { @@ -71,13 +75,18 @@ WebEntityRenderer::WebEntityRenderer(const EntityItemPointer& entity) : Parent(e _touchDevice.setMaximumTouchPoints(4); }); _geometryId = DependencyManager::get()->allocateID(); + _texture = gpu::Texture::createExternal(OffscreenQmlSurface::getDiscardLambda()); _texture->setSource(__FUNCTION__); + + _contentType = ContentType::HtmlContent; + buildWebSurface(entity, ""); + _timer.setInterval(MSECS_PER_SECOND); connect(&_timer, &QTimer::timeout, this, &WebEntityRenderer::onTimeout); } -void WebEntityRenderer::onRemoveFromSceneTyped(const TypedEntityPointer& entity) { +WebEntityRenderer::~WebEntityRenderer() { destroyWebSurface(); auto geometryCache = DependencyManager::get(); @@ -86,6 +95,11 @@ void WebEntityRenderer::onRemoveFromSceneTyped(const TypedEntityPointer& entity) } } +bool WebEntityRenderer::isTransparent() const { + float fadeRatio = _isFading ? Interpolate::calculateFadeRatio(_fadeStartTime) : 1.0f; + return fadeRatio < OPAQUE_ALPHA_THRESHOLD || _alpha < 1.0f; +} + bool WebEntityRenderer::needsRenderUpdateFromTypedEntity(const TypedEntityPointer& entity) const { if (_contextPosition != entity->getWorldPosition()) { return true; @@ -101,11 +115,31 @@ bool WebEntityRenderer::needsRenderUpdateFromTypedEntity(const TypedEntityPointe } } - if (_lastSourceUrl != entity->getSourceUrl()) { + if (_color != entity->getColor()) { return true; } - if (_lastDPI != entity->getDPI()) { + if (_alpha != entity->getAlpha()) { + return true; + } + + if (_sourceURL != entity->getSourceUrl()) { + return true; + } + + if (_dpi != entity->getDPI()) { + return true; + } + + if (_scriptURL != entity->getScriptURL()) { + return true; + } + + if (_maxFPS != entity->getMaxFPS()) { + return true; + } + + if (_inputMode != entity->getInputMode()) { return true; } @@ -113,35 +147,25 @@ bool WebEntityRenderer::needsRenderUpdateFromTypedEntity(const TypedEntityPointe } bool WebEntityRenderer::needsRenderUpdate() const { - { - QSharedPointer webSurface; - withReadLock([&] { - webSurface = _webSurface; - }); - if (!webSurface) { - // If we have rendered recently, and there is no web surface, we're going to create one - return true; - } + if (resultWithReadLock([this] { + return !_webSurface; + })) { + return true; } return Parent::needsRenderUpdate(); } void WebEntityRenderer::onTimeout() { - bool needsCheck = resultWithReadLock([&] { + uint64_t lastRenderTime; + if (!resultWithReadLock([&] { + lastRenderTime = _lastRenderTime; return (_lastRenderTime != 0 && (bool)_webSurface); - }); - - if (!needsCheck) { + })) { return; } - uint64_t interval; - withReadLock([&] { - interval = usecTimestampNow() - _lastRenderTime; - }); - - if (interval > MAX_NO_RENDER_INTERVAL) { + if (usecTimestampNow() - lastRenderTime > MAX_NO_RENDER_INTERVAL) { destroyWebSurface(); } } @@ -151,80 +175,100 @@ void WebEntityRenderer::doRenderUpdateSynchronousTyped(const ScenePointer& scene // destroy the existing surface (because surfaces don't support changing the root // object, so subsequent loads of content just overlap the existing content bool urlChanged = false; + auto newSourceURL = entity->getSourceUrl(); { - auto newSourceUrl = entity->getSourceUrl(); - auto newContentType = getContentType(newSourceUrl); - auto currentContentType = ContentType::NoContent; + auto newContentType = getContentType(newSourceURL); + ContentType currentContentType; withReadLock([&] { - urlChanged = _lastSourceUrl != newSourceUrl; + urlChanged = _sourceURL != newSourceURL; currentContentType = _contentType; }); if (urlChanged) { - if (newContentType != ContentType::HtmlContent || currentContentType != ContentType::HtmlContent) { - destroyWebSurface(); - // If we destroyed the surface, the URL change will be implicitly handled by the re-creation - urlChanged = false; - } - withWriteLock([&] { - _lastSourceUrl = newSourceUrl; _contentType = newContentType; }); + + if (newContentType != ContentType::HtmlContent || currentContentType != ContentType::HtmlContent) { + destroyWebSurface(); + } } } withWriteLock([&] { + _inputMode = entity->getInputMode(); + _dpi = entity->getDPI(); + _color = entity->getColor(); + _alpha = entity->getAlpha(); + if (_contentType == ContentType::NoContent) { return; } // This work must be done on the main thread - // If we couldn't create a new web surface, exit - if (!hasWebSurface() && !buildWebSurface(entity)) { - return; + if (!_webSurface) { + buildWebSurface(entity, newSourceURL); } - if (urlChanged && _contentType == ContentType::HtmlContent) { - _webSurface->getRootItem()->setProperty(URL_PROPERTY, _lastSourceUrl); - } - - void* key = (void*)this; - AbstractViewStateInterface::instance()->pushPostUpdateLambda(key, [this, entity] () { - withWriteLock([&] { - if (_contextPosition != entity->getWorldPosition()) { - // update globalPosition - _contextPosition = entity->getWorldPosition(); - _webSurface->getSurfaceContext()->setContextProperty("globalPosition", vec3toVariant(_contextPosition)); + if (_webSurface && _webSurface->getRootItem()) { + if (_webSurface->getRootItem()) { + if (_contentType == ContentType::HtmlContent && urlChanged) { + _webSurface->getRootItem()->setProperty(URL_PROPERTY, newSourceURL); + _sourceURL = newSourceURL; } - _lastDPI = entity->getDPI(); + { + auto scriptURL = entity->getScriptURL(); + if (_scriptURL != scriptURL) { + _webSurface->getRootItem()->setProperty("scriptURL", _scriptURL); + _scriptURL = scriptURL; + } + } - glm::vec2 windowSize = getWindowSize(entity); - _webSurface->resize(QSize(windowSize.x, windowSize.y)); - updateModelTransformAndBound(); - _renderTransform = getModelTransform(); - _renderTransform.postScale(entity->getScaledDimensions()); + { + auto maxFPS = entity->getMaxFPS(); + if (_maxFPS != maxFPS) { + // We special case YouTube URLs since we know they are videos that we should play with at least 30 FPS. + // FIXME this doesn't handle redirects or shortened URLs, consider using a signaling method from the web entity + if (QUrl(_sourceURL).host().endsWith("youtube.com", Qt::CaseInsensitive)) { + _webSurface->setMaxFps(YOUTUBE_MAX_FPS); + } else { + _webSurface->setMaxFps(maxFPS); + } + _maxFPS = maxFPS; + } + } + + { + auto contextPosition = entity->getWorldPosition(); + if (_contextPosition != contextPosition) { + _webSurface->getSurfaceContext()->setContextProperty("globalPosition", vec3toVariant(contextPosition)); + _contextPosition = contextPosition; + } + } + } + + void* key = (void*)this; + AbstractViewStateInterface::instance()->pushPostUpdateLambda(key, [this, entity]() { + withWriteLock([&] { + glm::vec2 windowSize = getWindowSize(entity); + _webSurface->resize(QSize(windowSize.x, windowSize.y)); + updateModelTransformAndBound(); + _renderTransform = getModelTransform(); + _renderTransform.postScale(entity->getScaledDimensions()); + }); }); - }); + } }); } void WebEntityRenderer::doRender(RenderArgs* args) { + PerformanceTimer perfTimer("WebEntityRenderer::render"); withWriteLock([&] { _lastRenderTime = usecTimestampNow(); }); -#ifdef WANT_EXTRA_DEBUGGING - { - gpu::Batch& batch = *args->_batch; - batch.setModelTransform(getTransformToCenter()); // we want to include the scale as well - glm::vec4 cubeColor{ 1.0f, 0.0f, 0.0f, 1.0f }; - DependencyManager::get()->renderWireCube(batch, 1.0f, cubeColor); - } -#endif - // Try to update the texture { QSharedPointer webSurface; @@ -242,111 +286,61 @@ void WebEntityRenderer::doRender(RenderArgs* args) { } } - PerformanceTimer perfTimer("WebEntityRenderer::render"); static const glm::vec2 texMin(0.0f), texMax(1.0f), topLeft(-0.5f), bottomRight(0.5f); gpu::Batch& batch = *args->_batch; + glm::vec4 color; withReadLock([&] { + float fadeRatio = _isFading ? Interpolate::calculateFadeRatio(_fadeStartTime) : 1.0f; + color = glm::vec4(toGlm(_color), _alpha * fadeRatio); batch.setModelTransform(_renderTransform); }); batch.setResourceTexture(0, _texture); - float fadeRatio = _isFading ? Interpolate::calculateFadeRatio(_fadeStartTime) : 1.0f; // Turn off jitter for these entities batch.pushProjectionJitter(); - DependencyManager::get()->bindWebBrowserProgram(batch, fadeRatio < OPAQUE_ALPHA_THRESHOLD); - DependencyManager::get()->renderQuad(batch, topLeft, bottomRight, texMin, texMax, glm::vec4(1.0f, 1.0f, 1.0f, fadeRatio), _geometryId); + DependencyManager::get()->bindWebBrowserProgram(batch, color.a < OPAQUE_ALPHA_THRESHOLD); + DependencyManager::get()->renderQuad(batch, topLeft, bottomRight, texMin, texMax, color, _geometryId); batch.popProjectionJitter(); batch.setResourceTexture(0, nullptr); } -bool WebEntityRenderer::hasWebSurface() { - return (bool)_webSurface && _webSurface->getRootItem(); -} - -static const auto WebSurfaceDeleter = [](OffscreenQmlSurface* webSurface) { - AbstractViewStateInterface::instance()->sendLambdaEvent([webSurface] { - // WebEngineView may run other threads (wasapi), so they must be deleted for a clean shutdown - // if the application has already stopped its event loop, delete must be explicit - delete webSurface; - }); -}; - -bool WebEntityRenderer::buildWebSurface(const TypedEntityPointer& entity) { +void WebEntityRenderer::buildWebSurface(const EntityItemPointer& entity, const QString& newSourceURL) { if (_currentWebCount >= MAX_CONCURRENT_WEB_VIEWS) { qWarning() << "Too many concurrent web views to create new view"; - return false; + return; } ++_currentWebCount; - - // FIXME use the surface cache instead of explicit creation - _webSurface = QSharedPointer(new OffscreenQmlSurface(), WebSurfaceDeleter); - // FIXME, the max FPS could be better managed by being dynamic (based on the number of current surfaces - // and the current rendering load) - _webSurface->setMaxFps(DEFAULT_MAX_FPS); - QObject::connect(_webSurface.data(), &OffscreenQmlSurface::rootContextCreated, [](QQmlContext* surfaceContext) { - // FIXME - Keyboard HMD only: Possibly add "HMDinfo" object to context for WebView.qml. - surfaceContext->setContextProperty("desktop", QVariant()); - // Let us interact with the keyboard - surfaceContext->setContextProperty("tabletInterface", DependencyManager::get().data()); - }); - - // forward web events to EntityScriptingInterface - auto entities = DependencyManager::get(); - const EntityItemID entityItemID = entity->getID(); - QObject::connect(_webSurface.data(), &OffscreenQmlSurface::webEventReceived, [=](const QVariant& message) { - emit entities->webEventReceived(entityItemID, message); - }); - - if (_contentType == ContentType::HtmlContent) { - // We special case YouTube URLs since we know they are videos that we should play with at least 30 FPS. - // FIXME this doesn't handle redirects or shortened URLs, consider using a signaling method from the - // web entity - if (QUrl(_lastSourceUrl).host().endsWith("youtube.com", Qt::CaseInsensitive)) { - _webSurface->setMaxFps(YOUTUBE_MAX_FPS); - } else { - _webSurface->setMaxFps(DEFAULT_MAX_FPS); - } - _webSurface->load("controls/WebEntityView.qml", [this](QQmlContext* context, QObject* item) { - item->setProperty(URL_PROPERTY, _lastSourceUrl); - }); - } else if (_contentType == ContentType::QmlContent) { - _webSurface->load(_lastSourceUrl); - } + WebEntityRenderer::acquireWebSurface(newSourceURL, _contentType == ContentType::HtmlContent, _webSurface, _cachedWebSurface); _fadeStartTime = usecTimestampNow(); _webSurface->resume(); - return _webSurface->getRootItem(); + _connections.push_back(QObject::connect(this, &WebEntityRenderer::scriptEventReceived, _webSurface.data(), &OffscreenQmlSurface::emitScriptEvent)); + _connections.push_back(QObject::connect(_webSurface.data(), &OffscreenQmlSurface::webEventReceived, this, &WebEntityRenderer::webEventReceived)); + const EntityItemID entityItemID = entity->getID(); + _connections.push_back(QObject::connect(_webSurface.data(), &OffscreenQmlSurface::webEventReceived, this, [entityItemID](const QVariant& message) { + emit DependencyManager::get()->webEventReceived(entityItemID, message); + })); } void WebEntityRenderer::destroyWebSurface() { QSharedPointer webSurface; - ContentType contentType{ ContentType::NoContent }; + ContentType contentType = ContentType::NoContent; withWriteLock([&] { webSurface.swap(_webSurface); - std::swap(contentType, _contentType); + _contentType = contentType; }); if (webSurface) { --_currentWebCount; - QQuickItem* rootItem = webSurface->getRootItem(); - - // Fix for crash in QtWebEngineCore when rapidly switching domains - // Call stop on the QWebEngineView before destroying OffscreenQMLSurface. - if (rootItem && contentType == ContentType::HtmlContent) { - // stop loading - QMetaObject::invokeMethod(rootItem, "stop"); - } - - webSurface->pause(); - webSurface.reset(); + WebEntityRenderer::releaseWebSurface(webSurface, _cachedWebSurface, _connections); } } glm::vec2 WebEntityRenderer::getWindowSize(const TypedEntityPointer& entity) const { glm::vec2 dims = glm::vec2(entity->getScaledDimensions()); - dims *= METERS_TO_INCHES * _lastDPI; + dims *= METERS_TO_INCHES * _dpi; // ensure no side is never larger then MAX_WINDOW_SIZE float max = (dims.x > dims.y) ? dims.x : dims.y; @@ -358,29 +352,84 @@ glm::vec2 WebEntityRenderer::getWindowSize(const TypedEntityPointer& entity) con } void WebEntityRenderer::hoverEnterEntity(const PointerEvent& event) { - if (_webSurface) { + if (_inputMode == WebInputMode::MOUSE) { + handlePointerEvent(event); + } else if (_webSurface) { + qDebug() << "boop5" << this << _webSurface << _webSurface->getRootItem(); PointerEvent webEvent = event; - webEvent.setPos2D(event.getPos2D() * (METERS_TO_INCHES * _lastDPI)); + webEvent.setPos2D(event.getPos2D() * (METERS_TO_INCHES * _dpi)); _webSurface->hoverBeginEvent(webEvent, _touchDevice); } } void WebEntityRenderer::hoverLeaveEntity(const PointerEvent& event) { - if (_webSurface) { + if (_inputMode == WebInputMode::MOUSE) { + PointerEvent endEvent(PointerEvent::Release, event.getID(), event.getPos2D(), event.getPos3D(), event.getNormal(), event.getDirection(), + event.getButton(), event.getButtons(), event.getKeyboardModifiers()); + handlePointerEvent(endEvent); + // QML onReleased is only triggered if a click has happened first. We need to send this "fake" mouse move event to properly trigger an onExited. + PointerEvent endMoveEvent(PointerEvent::Move, event.getID()); + handlePointerEvent(endMoveEvent); + } else if (_webSurface) { PointerEvent webEvent = event; - webEvent.setPos2D(event.getPos2D() * (METERS_TO_INCHES * _lastDPI)); + webEvent.setPos2D(event.getPos2D() * (METERS_TO_INCHES * _dpi)); _webSurface->hoverEndEvent(webEvent, _touchDevice); } } void WebEntityRenderer::handlePointerEvent(const PointerEvent& event) { + if (_inputMode == WebInputMode::TOUCH) { + handlePointerEventAsTouch(event); + } else { + handlePointerEventAsMouse(event); + } +} + +void WebEntityRenderer::handlePointerEventAsTouch(const PointerEvent& event) { if (_webSurface) { PointerEvent webEvent = event; - webEvent.setPos2D(event.getPos2D() * (METERS_TO_INCHES * _lastDPI)); + webEvent.setPos2D(event.getPos2D() * (METERS_TO_INCHES * _dpi)); _webSurface->handlePointerEvent(webEvent, _touchDevice); } } +void WebEntityRenderer::handlePointerEventAsMouse(const PointerEvent& event) { + if (!_webSurface) { + return; + } + + glm::vec2 windowPos = event.getPos2D() * (METERS_TO_INCHES * _dpi); + QPointF windowPoint(windowPos.x, windowPos.y); + + Qt::MouseButtons buttons = Qt::NoButton; + if (event.getButtons() & PointerEvent::PrimaryButton) { + buttons |= Qt::LeftButton; + } + + Qt::MouseButton button = Qt::NoButton; + if (event.getButton() == PointerEvent::PrimaryButton) { + button = Qt::LeftButton; + } + + QEvent::Type type; + switch (event.getType()) { + case PointerEvent::Press: + type = QEvent::MouseButtonPress; + break; + case PointerEvent::Release: + type = QEvent::MouseButtonRelease; + break; + case PointerEvent::Move: + type = QEvent::MouseMove; + break; + default: + return; + } + + QMouseEvent mouseEvent(type, windowPoint, windowPoint, windowPoint, button, buttons, event.getKeyboardModifiers()); + QCoreApplication::sendEvent(_webSurface->getWindow(), &mouseEvent); +} + void WebEntityRenderer::setProxyWindow(QWindow* proxyWindow) { if (_webSurface) { _webSurface->setProxyWindow(proxyWindow); @@ -394,8 +443,6 @@ QObject* WebEntityRenderer::getEventHandler() { return _webSurface->getEventHandler(); } -bool WebEntityRenderer::isTransparent() const { - float fadeRatio = _isFading ? Interpolate::calculateFadeRatio(_fadeStartTime) : 1.0f; - return fadeRatio < OPAQUE_ALPHA_THRESHOLD; -} - +void WebEntityRenderer::emitScriptEvent(const QVariant& message) { + QMetaObject::invokeMethod(this, "scriptEventReceived", Q_ARG(QVariant, message)); +} \ No newline at end of file diff --git a/libraries/entities-renderer/src/RenderableWebEntityItem.h b/libraries/entities-renderer/src/RenderableWebEntityItem.h index 12640f697d..4affb5819d 100644 --- a/libraries/entities-renderer/src/RenderableWebEntityItem.h +++ b/libraries/entities-renderer/src/RenderableWebEntityItem.h @@ -24,13 +24,30 @@ class WebEntityRenderer : public TypedEntityRenderer { public: WebEntityRenderer(const EntityItemPointer& entity); + ~WebEntityRenderer(); Q_INVOKABLE void hoverEnterEntity(const PointerEvent& event); Q_INVOKABLE void hoverLeaveEntity(const PointerEvent& event); Q_INVOKABLE void handlePointerEvent(const PointerEvent& event); + static const QString QML; + static const char* URL_PROPERTY; + + static void setAcquireWebSurfaceOperator(std::function&, bool&)> acquireWebSurfaceOperator) { _acquireWebSurfaceOperator = acquireWebSurfaceOperator; } + static void acquireWebSurface(const QString& url, bool htmlContent, QSharedPointer& webSurface, bool& cachedWebSurface) { + if (_acquireWebSurfaceOperator) { + _acquireWebSurfaceOperator(url, htmlContent, webSurface, cachedWebSurface); + } + } + + static void setReleaseWebSurfaceOperator(std::function&, bool&, std::vector&)> releaseWebSurfaceOperator) { _releaseWebSurfaceOperator = releaseWebSurfaceOperator; } + static void releaseWebSurface(QSharedPointer& webSurface, bool& cachedWebSurface, std::vector& connections) { + if (_releaseWebSurfaceOperator) { + _releaseWebSurfaceOperator(webSurface, cachedWebSurface, connections); + } + } + protected: - virtual void onRemoveFromSceneTyped(const TypedEntityPointer& entity) override; virtual bool needsRenderUpdate() const override; virtual bool needsRenderUpdateFromTypedEntity(const TypedEntityPointer& entity) const override; virtual void doRenderUpdateSynchronousTyped(const ScenePointer& scene, Transaction& transaction, const TypedEntityPointer& entity) override; @@ -39,40 +56,59 @@ protected: virtual bool wantsHandControllerPointerEvents() const override { return true; } virtual bool wantsKeyboardFocus() const override { return true; } + virtual void setProxyWindow(QWindow* proxyWindow) override; virtual QObject* getEventHandler() override; + void handlePointerEventAsTouch(const PointerEvent& event); + void handlePointerEventAsMouse(const PointerEvent& event); + private: void onTimeout(); - bool buildWebSurface(const TypedEntityPointer& entity); + void buildWebSurface(const EntityItemPointer& entity, const QString& newSourceURL); void destroyWebSurface(); - bool hasWebSurface(); glm::vec2 getWindowSize(const TypedEntityPointer& entity) const; - int _geometryId{ 0 }; enum class ContentType { NoContent, HtmlContent, QmlContent }; - static ContentType getContentType(const QString& urlString); + ContentType _contentType { ContentType::NoContent }; - ContentType _contentType{ ContentType::NoContent }; - QSharedPointer _webSurface; - glm::vec3 _contextPosition; + QSharedPointer _webSurface { nullptr }; + bool _cachedWebSurface { false }; gpu::TexturePointer _texture; - QString _lastSourceUrl; - uint16_t _lastDPI; + + glm::u8vec3 _color; + float _alpha { 1.0f }; + + QString _sourceURL; + uint16_t _dpi; + QString _scriptURL; + uint8_t _maxFPS; + WebInputMode _inputMode; + + glm::vec3 _contextPosition; + QTimer _timer; uint64_t _lastRenderTime { 0 }; + + std::vector _connections; + + static std::function&, bool&)> _acquireWebSurfaceOperator; + static std::function&, bool&, std::vector&)> _releaseWebSurfaceOperator; + +public slots: + void emitScriptEvent(const QVariant& scriptMessage); + +signals: + void scriptEventReceived(const QVariant& message); + void webEventReceived(const QVariant& message); }; -} } // namespace - -#if 0 - virtual void emitScriptEvent(const QVariant& message) override; -#endif +} } #endif // hifi_RenderableWebEntityItem_h diff --git a/libraries/entities/src/EntityItem.cpp b/libraries/entities/src/EntityItem.cpp index 498f0ff066..871fbe9cf3 100644 --- a/libraries/entities/src/EntityItem.cpp +++ b/libraries/entities/src/EntityItem.cpp @@ -95,6 +95,9 @@ EntityPropertyFlags EntityItem::getEntityProperties(EncodeBitstreamParams& param requestedProperties += PROP_QUERY_AA_CUBE; requestedProperties += PROP_CAN_CAST_SHADOW; // requestedProperties += PROP_VISIBLE_IN_SECONDARY_CAMERA; // not sent over the wire + requestedProperties += PROP_RENDER_LAYER; + requestedProperties += PROP_PRIMITIVE_MODE; + requestedProperties += PROP_IGNORE_PICK_INTERSECTION; withReadLock([&] { requestedProperties += _grabProperties.getEntityProperties(params); }); @@ -263,8 +266,8 @@ OctreeElement::AppendState EntityItem::appendEntityData(OctreePacketData* packet APPEND_ENTITY_PROPERTY(PROP_REGISTRATION_POINT, getRegistrationPoint()); APPEND_ENTITY_PROPERTY(PROP_CREATED, getCreated()); APPEND_ENTITY_PROPERTY(PROP_LAST_EDITED_BY, getLastEditedBy()); - // APPEND_ENTITY_PROPERTY(PROP_ENTITY_HOST_TYPE, getEntityHostType()); // not sent over the wire - // APPEND_ENTITY_PROPERTY(PROP_OWNING_AVATAR_ID, getOwningAvatarID()); // not sent over the wire + // APPEND_ENTITY_PROPERTY(PROP_ENTITY_HOST_TYPE, (uint32_t)getEntityHostType()); // not sent over the wire + // APPEND_ENTITY_PROPERTY(PROP_OWNING_AVATAR_ID, getOwningAvatarID()); // not sent over the wire // convert AVATAR_SELF_ID to actual sessionUUID. QUuid actualParentID = getParentID(); if (actualParentID == AVATAR_SELF_ID) { @@ -276,6 +279,9 @@ OctreeElement::AppendState EntityItem::appendEntityData(OctreePacketData* packet APPEND_ENTITY_PROPERTY(PROP_QUERY_AA_CUBE, getQueryAACube()); APPEND_ENTITY_PROPERTY(PROP_CAN_CAST_SHADOW, getCanCastShadow()); // APPEND_ENTITY_PROPERTY(PROP_VISIBLE_IN_SECONDARY_CAMERA, getIsVisibleInSecondaryCamera()); // not sent over the wire + APPEND_ENTITY_PROPERTY(PROP_RENDER_LAYER, (uint32_t)getRenderLayer()); + APPEND_ENTITY_PROPERTY(PROP_PRIMITIVE_MODE, (uint32_t)getPrimitiveMode()); + APPEND_ENTITY_PROPERTY(PROP_IGNORE_PICK_INTERSECTION, getIgnorePickIntersection()); withReadLock([&] { _grabProperties.appendSubclassData(packetData, params, entityTreeElementExtraEncodeData, requestedProperties, propertyFlags, propertiesDidntFit, propertyCount, appendState); @@ -770,7 +776,10 @@ int EntityItem::readEntityDataFromBuffer(const unsigned char* data, int bytesLef auto lastEdited = lastEditedFromBufferAdjusted; bool otherOverwrites = overwriteLocalData && !weOwnSimulation; - auto shouldUpdate = [lastEdited, otherOverwrites, filterRejection](quint64 updatedTimestamp, bool valueChanged) { + auto shouldUpdate = [this, lastEdited, otherOverwrites, filterRejection](quint64 updatedTimestamp, bool valueChanged) { + if (stillHasGrabActions()) { + return false; + } bool simulationChanged = lastEdited > updatedTimestamp; return otherOverwrites && simulationChanged && (valueChanged || filterRejection); }; @@ -839,6 +848,9 @@ int EntityItem::readEntityDataFromBuffer(const unsigned char* data, int bytesLef } READ_ENTITY_PROPERTY(PROP_CAN_CAST_SHADOW, bool, setCanCastShadow); // READ_ENTITY_PROPERTY(PROP_VISIBLE_IN_SECONDARY_CAMERA, bool, setIsVisibleInSecondaryCamera); // not sent over the wire + READ_ENTITY_PROPERTY(PROP_RENDER_LAYER, RenderLayer, setRenderLayer); + READ_ENTITY_PROPERTY(PROP_PRIMITIVE_MODE, PrimitiveMode, setPrimitiveMode); + READ_ENTITY_PROPERTY(PROP_IGNORE_PICK_INTERSECTION, bool, setIgnorePickIntersection); withWriteLock([&] { int bytesFromGrab = _grabProperties.readEntitySubclassDataFromBuffer(dataAt, (bytesLeftToRead - bytesRead), args, propertyFlags, overwriteLocalData, @@ -1310,6 +1322,9 @@ EntityItemProperties EntityItem::getProperties(const EntityPropertyFlags& desire COPY_ENTITY_PROPERTY_TO_PROPERTIES(queryAACube, getQueryAACube); COPY_ENTITY_PROPERTY_TO_PROPERTIES(canCastShadow, getCanCastShadow); COPY_ENTITY_PROPERTY_TO_PROPERTIES(isVisibleInSecondaryCamera, isVisibleInSecondaryCamera); + COPY_ENTITY_PROPERTY_TO_PROPERTIES(renderLayer, getRenderLayer); + COPY_ENTITY_PROPERTY_TO_PROPERTIES(primitiveMode, getPrimitiveMode); + COPY_ENTITY_PROPERTY_TO_PROPERTIES(ignorePickIntersection, getIgnorePickIntersection); withReadLock([&] { _grabProperties.getProperties(properties); }); @@ -1454,6 +1469,9 @@ bool EntityItem::setProperties(const EntityItemProperties& properties) { SET_ENTITY_PROPERTY_FROM_PROPERTIES(queryAACube, setQueryAACube); SET_ENTITY_PROPERTY_FROM_PROPERTIES(canCastShadow, setCanCastShadow); SET_ENTITY_PROPERTY_FROM_PROPERTIES(isVisibleInSecondaryCamera, setIsVisibleInSecondaryCamera); + SET_ENTITY_PROPERTY_FROM_PROPERTIES(renderLayer, setRenderLayer); + SET_ENTITY_PROPERTY_FROM_PROPERTIES(primitiveMode, setPrimitiveMode); + SET_ENTITY_PROPERTY_FROM_PROPERTIES(ignorePickIntersection, setIgnorePickIntersection); withWriteLock([&] { bool grabPropertiesChanged = _grabProperties.setProperties(properties); somethingChanged |= grabPropertiesChanged; @@ -2931,6 +2949,58 @@ void EntityItem::setIsVisibleInSecondaryCamera(bool value) { } } +RenderLayer EntityItem::getRenderLayer() const { + return resultWithReadLock([&] { + return _renderLayer; + }); +} + +void EntityItem::setRenderLayer(RenderLayer value) { + bool changed = false; + withWriteLock([&] { + if (_renderLayer != value) { + changed = true; + _renderLayer = value; + } + }); + + if (changed) { + emit requestRenderUpdate(); + } +} + +PrimitiveMode EntityItem::getPrimitiveMode() const { + return resultWithReadLock([&] { + return _primitiveMode; + }); +} + +void EntityItem::setPrimitiveMode(PrimitiveMode value) { + bool changed = false; + withWriteLock([&] { + if (_primitiveMode != value) { + changed = true; + _primitiveMode = value; + } + }); + + if (changed) { + emit requestRenderUpdate(); + } +} + +bool EntityItem::getIgnorePickIntersection() const { + return resultWithReadLock([&] { + return _ignorePickIntersection; + }); +} + +void EntityItem::setIgnorePickIntersection(bool value) { + withWriteLock([&] { + _ignorePickIntersection = value; + }); +} + bool EntityItem::getCanCastShadow() const { bool result; withReadLock([&] { @@ -3349,7 +3419,8 @@ void EntityItem::prepareForSimulationOwnershipBid(EntityItemProperties& properti } bool EntityItem::isWearable() const { - return isVisible() && (getParentID() == DependencyManager::get()->getSessionUUID() || getParentID() == AVATAR_SELF_ID); + return isVisible() && + (getParentID() == DependencyManager::get()->getSessionUUID() || getParentID() == AVATAR_SELF_ID); } void EntityItem::addGrab(GrabPointer grab) { @@ -3368,7 +3439,8 @@ void EntityItem::addGrab(GrabPointer grab) { EntityDynamicType dynamicType; QVariantMap arguments; int grabParentJointIndex =grab->getParentJointIndex(); - if (grabParentJointIndex == FARGRAB_RIGHTHAND_INDEX || grabParentJointIndex == FARGRAB_LEFTHAND_INDEX) { + if (grabParentJointIndex == FARGRAB_RIGHTHAND_INDEX || grabParentJointIndex == FARGRAB_LEFTHAND_INDEX || + grabParentJointIndex == FARGRAB_MOUSE_INDEX) { // add a far-grab action dynamicType = DYNAMIC_TYPE_FAR_GRAB; arguments["otherID"] = grab->getOwnerID(); diff --git a/libraries/entities/src/EntityItem.h b/libraries/entities/src/EntityItem.h index 826a9c34a0..ec7ad78313 100644 --- a/libraries/entities/src/EntityItem.h +++ b/libraries/entities/src/EntityItem.h @@ -293,6 +293,15 @@ public: bool isVisibleInSecondaryCamera() const; void setIsVisibleInSecondaryCamera(bool value); + RenderLayer getRenderLayer() const; + void setRenderLayer(RenderLayer value); + + PrimitiveMode getPrimitiveMode() const; + void setPrimitiveMode(PrimitiveMode value); + + bool getIgnorePickIntersection() const; + void setIgnorePickIntersection(bool value); + bool getCanCastShadow() const; void setCanCastShadow(bool value); @@ -621,7 +630,10 @@ protected: float _angularDamping { ENTITY_ITEM_DEFAULT_ANGULAR_DAMPING }; bool _visible { ENTITY_ITEM_DEFAULT_VISIBLE }; bool _isVisibleInSecondaryCamera { ENTITY_ITEM_DEFAULT_VISIBLE_IN_SECONDARY_CAMERA }; + RenderLayer _renderLayer { RenderLayer::WORLD }; + PrimitiveMode _primitiveMode { PrimitiveMode::SOLID }; bool _canCastShadow{ ENTITY_ITEM_DEFAULT_CAN_CAST_SHADOW }; + bool _ignorePickIntersection { false }; bool _collisionless { ENTITY_ITEM_DEFAULT_COLLISIONLESS }; uint16_t _collisionMask { ENTITY_COLLISION_MASK_DEFAULT }; bool _dynamic { ENTITY_ITEM_DEFAULT_DYNAMIC }; diff --git a/libraries/entities/src/EntityItemProperties.cpp b/libraries/entities/src/EntityItemProperties.cpp index 3b1e9a3e2b..a79ba40a49 100644 --- a/libraries/entities/src/EntityItemProperties.cpp +++ b/libraries/entities/src/EntityItemProperties.cpp @@ -340,17 +340,96 @@ QString EntityItemProperties::getBillboardModeAsString() const { return BillboardModeHelpers::getNameForBillboardMode(_billboardMode); } -void EntityItemProperties::setBillboardModeFromString(const QString& materialMappingMode) { +void EntityItemProperties::setBillboardModeFromString(const QString& billboardMode) { if (stringToBillboardModeLookup.empty()) { buildStringToBillboardModeLookup(); } - auto billboardModeItr = stringToBillboardModeLookup.find(materialMappingMode.toLower()); + auto billboardModeItr = stringToBillboardModeLookup.find(billboardMode.toLower()); if (billboardModeItr != stringToBillboardModeLookup.end()) { _billboardMode = billboardModeItr.value(); _billboardModeChanged = true; } } +QHash stringToRenderLayerLookup; + +void addRenderLayer(RenderLayer mode) { + stringToRenderLayerLookup[RenderLayerHelpers::getNameForRenderLayer(mode)] = mode; +} + +void buildStringToRenderLayerLookup() { + addRenderLayer(RenderLayer::WORLD); + addRenderLayer(RenderLayer::FRONT); + addRenderLayer(RenderLayer::HUD); +} + +QString EntityItemProperties::getRenderLayerAsString() const { + return RenderLayerHelpers::getNameForRenderLayer(_renderLayer); +} + +void EntityItemProperties::setRenderLayerFromString(const QString& renderLayer) { + if (stringToRenderLayerLookup.empty()) { + buildStringToRenderLayerLookup(); + } + auto renderLayerItr = stringToRenderLayerLookup.find(renderLayer.toLower()); + if (renderLayerItr != stringToRenderLayerLookup.end()) { + _renderLayer = renderLayerItr.value(); + _renderLayerChanged = true; + } +} + +QHash stringToPrimitiveModeLookup; + +void addPrimitiveMode(PrimitiveMode mode) { + stringToPrimitiveModeLookup[PrimitiveModeHelpers::getNameForPrimitiveMode(mode)] = mode; +} + +void buildStringToPrimitiveModeLookup() { + addPrimitiveMode(PrimitiveMode::SOLID); + addPrimitiveMode(PrimitiveMode::LINES); +} + +QString EntityItemProperties::getPrimitiveModeAsString() const { + return PrimitiveModeHelpers::getNameForPrimitiveMode(_primitiveMode); +} + +void EntityItemProperties::setPrimitiveModeFromString(const QString& primitiveMode) { + if (stringToPrimitiveModeLookup.empty()) { + buildStringToPrimitiveModeLookup(); + } + auto primitiveModeItr = stringToPrimitiveModeLookup.find(primitiveMode.toLower()); + if (primitiveModeItr != stringToPrimitiveModeLookup.end()) { + _primitiveMode = primitiveModeItr.value(); + _primitiveModeChanged = true; + } +} + +QHash stringToWebInputModeLookup; + +void addWebInputMode(WebInputMode mode) { + stringToWebInputModeLookup[WebInputModeHelpers::getNameForWebInputMode(mode)] = mode; +} + +void buildStringToWebInputModeLookup() { + addWebInputMode(WebInputMode::TOUCH); + addWebInputMode(WebInputMode::MOUSE); +} + +QString EntityItemProperties::getInputModeAsString() const { + return WebInputModeHelpers::getNameForWebInputMode(_inputMode); +} + +void EntityItemProperties::setInputModeFromString(const QString& webInputMode) { + if (stringToWebInputModeLookup.empty()) { + buildStringToWebInputModeLookup(); + } + auto webInputModeItr = stringToWebInputModeLookup.find(webInputMode.toLower()); + if (webInputModeItr != stringToWebInputModeLookup.end()) { + _inputMode = webInputModeItr.value(); + _inputModeChanged = true; + } +} + EntityPropertyFlags EntityItemProperties::getChangedProperties() const { EntityPropertyFlags changedProperties; @@ -375,6 +454,9 @@ EntityPropertyFlags EntityItemProperties::getChangedProperties() const { CHECK_PROPERTY_CHANGE(PROP_QUERY_AA_CUBE, queryAACube); CHECK_PROPERTY_CHANGE(PROP_CAN_CAST_SHADOW, canCastShadow); CHECK_PROPERTY_CHANGE(PROP_VISIBLE_IN_SECONDARY_CAMERA, isVisibleInSecondaryCamera); + CHECK_PROPERTY_CHANGE(PROP_RENDER_LAYER, renderLayer); + CHECK_PROPERTY_CHANGE(PROP_PRIMITIVE_MODE, primitiveMode); + CHECK_PROPERTY_CHANGE(PROP_IGNORE_PICK_INTERSECTION, ignorePickIntersection); changedProperties += _grab.getChangedProperties(); // Physics @@ -474,6 +556,7 @@ EntityPropertyFlags EntityItemProperties::getChangedProperties() const { CHECK_PROPERTY_CHANGE(PROP_JOINT_TRANSLATIONS_SET, jointTranslationsSet); CHECK_PROPERTY_CHANGE(PROP_JOINT_TRANSLATIONS, jointTranslations); CHECK_PROPERTY_CHANGE(PROP_RELAY_PARENT_JOINTS, relayParentJoints); + CHECK_PROPERTY_CHANGE(PROP_GROUP_CULLED, groupCulled); changedProperties += _animation.getChangedProperties(); // Light @@ -528,6 +611,9 @@ EntityPropertyFlags EntityItemProperties::getChangedProperties() const { // Web CHECK_PROPERTY_CHANGE(PROP_SOURCE_URL, sourceUrl); CHECK_PROPERTY_CHANGE(PROP_DPI, dpi); + CHECK_PROPERTY_CHANGE(PROP_SCRIPT_URL, scriptURL); + CHECK_PROPERTY_CHANGE(PROP_MAX_FPS, maxFPS); + CHECK_PROPERTY_CHANGE(PROP_INPUT_MODE, inputMode); // Polyline CHECK_PROPERTY_CHANGE(PROP_LINE_POINTS, linePoints); @@ -579,15 +665,15 @@ EntityPropertyFlags EntityItemProperties::getChangedProperties() const { * the shape property set for entities of these types.) Read-only. * @property {EntityHostType} entityHostType="domain" - How this entity will behave, including if and how it is sent to other people. * The value can only be set at entity creation by using the entityHostType parameter in - * {@link Entities.addEntity}. + * {@link Entities.addEntity}. Read-only. * @property {boolean} avatarEntity=false - If true then the entity is an avatar entity; An avatar entity follows you to each domain you visit, * rendering at the same world coordinates unless it's parented to your avatar. Value cannot be changed after the entity is created.
* The value can only be set at entity creation by using the entityHostType parameter in - * {@link Entities.addEntity}. clientOnly is an alias. + * {@link Entities.addEntity}. clientOnly is an alias. Read-only. * @property {boolean} localEntity=false - If true then the entity is a local entity; Local entities only render for you and are not sent over the wire. * Value cannot be changed after the entity is created.
* The value can only be set at entity creation by using the entityHostType parameter in - * {@link Entities.addEntity}. + * {@link Entities.addEntity}. Read-only. * @property {Uuid} owningAvatarID=Uuid.NULL - The session ID of the owning avatar if avatarEntity is * true, otherwise {@link Uuid|Uuid.NULL}. Read-only. * @@ -611,6 +697,9 @@ EntityPropertyFlags EntityItemProperties::getChangedProperties() const { * {@link Entities.EntityType|Zone} entity with castShadows enabled in its * {@link Entities.EntityProperties-Zone|keyLight} property. * @property {boolean} isVisibleInSecondaryCamera=true - Whether or not the entity is rendered in the secondary camera. If true then the entity is rendered. + * @property {RenderLayer} renderLayer="world" - In which layer this entity renders. + * @property {PrimitiveMode} primitiveMode="solid" - How this entity's geometry is rendered. + * @property {boolean} ignorePickIntersection=false - If true, picks ignore the entity. * * @property {Vec3} position=0,0,0 - The position of the entity. * @property {Quat} rotation=0,0,0,1 - The orientation of the entity with respect to world coordinates. @@ -841,10 +930,14 @@ EntityPropertyFlags EntityItemProperties::getChangedProperties() const { * @property {number} priority=0 - The priority for applying the material to its parent. Only the highest priority material is * applied, with materials of the same priority randomly assigned. Materials that come with the model have a priority of * 0. - * @property {string|number} parentMaterialName="0" - Selects the submesh or submeshes within the parent to apply the material - * to. If in the format "mat::string", all submeshes with material name "string" are replaced. - * Otherwise the property value is parsed as an unsigned integer, specifying the mesh index to modify. Invalid values are - * parsed to 0. + * @property {string} parentMaterialName="0" - Selects the mesh part or parts within the parent to which to apply the material. + * If in the format "mat::string", all mesh parts with material name "string" are replaced. + * Otherwise the property value is parsed as an unsigned integer, specifying the mesh part index to modify. If "all", + * all mesh parts will be replaced. If an array (starts with "[" and ends with "]"), the string will be + * split at each "," and each element will be parsed as either a number or a string if it starts with + * "mat::". In other words, "[0,1,mat::string,mat::string2]" will replace mesh parts 0 and 1, and any + * mesh parts with material "string" or "string2". Do not put spaces around the commas. Invalid values + * are parsed to 0. * @property {string} materialMappingMode="uv" - How the material is mapped to the entity. Either "uv" or * "projected". In "uv" mode, the material will be evaluated within the UV space of the mesh it is applied to. In * "projected" mode, the 3D transform of the Material Entity will be used to evaluate the texture coordinates for the material. @@ -900,7 +993,7 @@ EntityPropertyFlags EntityItemProperties::getChangedProperties() const { * parse the JSON string into a JavaScript object of name, URL pairs. Read-only. * * @property {ShapeType} shapeType="none" - The shape of the collision hull used if collisions are enabled. - * @property {string} compoundShapeURL="" - The OBJ file to use for the compound shape if shapeType is + * @property {string} compoundShapeURL="" - The model file to use for the compound shape if shapeType is * "compound". * * @property {Entities.AnimationProperties} animation - An animation to play on the model. @@ -925,6 +1018,8 @@ EntityPropertyFlags EntityItemProperties::getChangedProperties() const { * {@link Entities.getJointIndex|getJointIndex}. * @property {boolean} relayParentJoints=false - If true and the entity is parented to an avatar, then the * avatar's joint rotations are applied to the entity's joints. + * @property {boolean} groupCulled=false - If true, the mesh parts of the model are LOD culled as a group. + * If false, separate mesh parts will be LOD culled individually. * * @example Rez a Vive tracker puck. * var entity = Entities.addEntity({ @@ -1205,11 +1300,16 @@ EntityPropertyFlags EntityItemProperties::getChangedProperties() const { * The entity has properties in addition to the common {@link Entities.EntityProperties|EntityProperties}. * @typedef {object} Entities.EntityProperties-Web * @property {Vec3} dimensions=0.1,0.1,0.01 - The dimensions of the entity. + * @property {Color} color=255,255,255 - The color of the web surface. + * @property {number} alpha=1 - The alpha of the web surface. * @property {string} sourceUrl="" - The URL of the Web page to display. This value does not change as you or others navigate * on the Web entity. * @property {number} dpi=30 - The resolution to display the page at, in dots per inch. If you convert this to dots per meter * (multiply by 1 / 0.0254 = 39.3701) then multiply dimensions.x and dimensions.y by that value * you get the resolution in pixels. + * @property {string} scriptURL="" - The URL of a JavaScript file to inject into the Web page. + * @property {number} maxFPS=10 - The maximum update rate for the Web content, in frames/second. + * @property {WebInputMode} inputMode="touch" - The user input mode to use. * @example Create a Web entity displaying at 1920 x 1080 resolution. * var METERS_TO_INCHES = 39.3701; * var entity = Entities.addEntity({ @@ -1238,7 +1338,7 @@ EntityPropertyFlags EntityItemProperties::getChangedProperties() const { * @property {ShapeType} shapeType="box" - The shape of the volume in which the zone's lighting effects and avatar * permissions have effect. Reverts to the default value if set to "none", or set to "compound" * and compoundShapeURL is "". - * @property {string} compoundShapeURL="" - The OBJ file to use for the compound shape if shapeType is + * @property {string} compoundShapeURL="" - The model file to use for the compound shape if shapeType is * "compound". * * @property {string} keyLightMode="inherit" - Configures the key light in the zone. Possible values:
@@ -1416,6 +1516,9 @@ QScriptValue EntityItemProperties::copyToScriptValue(QScriptEngine* engine, bool COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_QUERY_AA_CUBE, queryAACube); COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_CAN_CAST_SHADOW, canCastShadow); COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_VISIBLE_IN_SECONDARY_CAMERA, isVisibleInSecondaryCamera); + COPY_PROPERTY_TO_QSCRIPTVALUE_GETTER(PROP_RENDER_LAYER, renderLayer, getRenderLayerAsString()); + COPY_PROPERTY_TO_QSCRIPTVALUE_GETTER(PROP_PRIMITIVE_MODE, primitiveMode, getPrimitiveModeAsString()); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_IGNORE_PICK_INTERSECTION, ignorePickIntersection); _grab.copyToScriptValue(_desiredProperties, properties, engine, skipDefaults, defaultEntityProperties); // Physics @@ -1532,6 +1635,7 @@ QScriptValue EntityItemProperties::copyToScriptValue(QScriptEngine* engine, bool COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_JOINT_TRANSLATIONS_SET, jointTranslationsSet); COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_JOINT_TRANSLATIONS, jointTranslations); COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_RELAY_PARENT_JOINTS, relayParentJoints); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_GROUP_CULLED, groupCulled); if (!psuedoPropertyFlagsButDesiredEmpty) { _animation.copyToScriptValue(_desiredProperties, properties, engine, skipDefaults, defaultEntityProperties); } @@ -1602,8 +1706,14 @@ QScriptValue EntityItemProperties::copyToScriptValue(QScriptEngine* engine, bool // Web only if (_type == EntityTypes::Web) { + COPY_PROPERTY_TO_QSCRIPTVALUE_TYPED(PROP_COLOR, color, u8vec3Color); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_ALPHA, alpha); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_SOURCE_URL, sourceUrl); COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_DPI, dpi); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_SCRIPT_URL, scriptURL); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_MAX_FPS, maxFPS); + COPY_PROPERTY_TO_QSCRIPTVALUE_GETTER(PROP_INPUT_MODE, inputMode, getInputModeAsString()); } // PolyVoxel only @@ -1797,6 +1907,9 @@ void EntityItemProperties::copyFromScriptValue(const QScriptValue& object, bool COPY_PROPERTY_FROM_QSCRIPTVALUE(queryAACube, AACube, setQueryAACube); // TODO: should scripts be able to set this? COPY_PROPERTY_FROM_QSCRIPTVALUE(canCastShadow, bool, setCanCastShadow); COPY_PROPERTY_FROM_QSCRIPTVALUE(isVisibleInSecondaryCamera, bool, setIsVisibleInSecondaryCamera); + COPY_PROPERTY_FROM_QSCRIPTVALUE_ENUM(renderLayer, RenderLayer); + COPY_PROPERTY_FROM_QSCRIPTVALUE_ENUM(primitiveMode, PrimitiveMode); + COPY_PROPERTY_FROM_QSCRIPTVALUE(ignorePickIntersection, bool, setIgnorePickIntersection); _grab.copyFromScriptValue(object, _defaultSettings); // Physics @@ -1901,6 +2014,7 @@ void EntityItemProperties::copyFromScriptValue(const QScriptValue& object, bool COPY_PROPERTY_FROM_QSCRIPTVALUE(jointTranslationsSet, qVectorBool, setJointTranslationsSet); COPY_PROPERTY_FROM_QSCRIPTVALUE(jointTranslations, qVectorVec3, setJointTranslations); COPY_PROPERTY_FROM_QSCRIPTVALUE(relayParentJoints, bool, setRelayParentJoints); + COPY_PROPERTY_FROM_QSCRIPTVALUE(groupCulled, bool, setGroupCulled); _animation.copyFromScriptValue(object, _defaultSettings); // Light @@ -1955,6 +2069,9 @@ void EntityItemProperties::copyFromScriptValue(const QScriptValue& object, bool // Web COPY_PROPERTY_FROM_QSCRIPTVALUE(sourceUrl, QString, setSourceUrl); COPY_PROPERTY_FROM_QSCRIPTVALUE(dpi, uint16_t, setDPI); + COPY_PROPERTY_FROM_QSCRIPTVALUE(scriptURL, QString, setScriptURL); + COPY_PROPERTY_FROM_QSCRIPTVALUE(maxFPS, uint8_t, setMaxFPS); + COPY_PROPERTY_FROM_QSCRIPTVALUE_ENUM(inputMode, InputMode); // Polyline COPY_PROPERTY_FROM_QSCRIPTVALUE(linePoints, qVectorVec3, setLinePoints); @@ -2068,6 +2185,9 @@ void EntityItemProperties::merge(const EntityItemProperties& other) { COPY_PROPERTY_IF_CHANGED(queryAACube); COPY_PROPERTY_IF_CHANGED(canCastShadow); COPY_PROPERTY_IF_CHANGED(isVisibleInSecondaryCamera); + COPY_PROPERTY_IF_CHANGED(renderLayer); + COPY_PROPERTY_IF_CHANGED(primitiveMode); + COPY_PROPERTY_IF_CHANGED(ignorePickIntersection); _grab.merge(other._grab); // Physics @@ -2167,6 +2287,7 @@ void EntityItemProperties::merge(const EntityItemProperties& other) { COPY_PROPERTY_IF_CHANGED(jointTranslationsSet); COPY_PROPERTY_IF_CHANGED(jointTranslations); COPY_PROPERTY_IF_CHANGED(relayParentJoints); + COPY_PROPERTY_IF_CHANGED(groupCulled); _animation.merge(other._animation); // Light @@ -2221,6 +2342,9 @@ void EntityItemProperties::merge(const EntityItemProperties& other) { // Web COPY_PROPERTY_IF_CHANGED(sourceUrl); COPY_PROPERTY_IF_CHANGED(dpi); + COPY_PROPERTY_IF_CHANGED(scriptURL); + COPY_PROPERTY_IF_CHANGED(maxFPS); + COPY_PROPERTY_IF_CHANGED(inputMode); // Polyline COPY_PROPERTY_IF_CHANGED(linePoints); @@ -2338,6 +2462,9 @@ bool EntityItemProperties::getPropertyInfo(const QString& propertyName, EntityPr ADD_PROPERTY_TO_MAP(PROP_QUERY_AA_CUBE, QueryAACube, queryAACube, AACube); ADD_PROPERTY_TO_MAP(PROP_CAN_CAST_SHADOW, CanCastShadow, canCastShadow, bool); ADD_PROPERTY_TO_MAP(PROP_VISIBLE_IN_SECONDARY_CAMERA, IsVisibleInSecondaryCamera, isVisibleInSecondaryCamera, bool); + ADD_PROPERTY_TO_MAP(PROP_RENDER_LAYER, RenderLayer, renderLayer, RenderLayer); + ADD_PROPERTY_TO_MAP(PROP_PRIMITIVE_MODE, PrimitiveMode, primitiveMode, PrimitiveMode); + ADD_PROPERTY_TO_MAP(PROP_IGNORE_PICK_INTERSECTION, IgnorePickIntersection, ignorePickIntersection, bool); { // Grab ADD_GROUP_PROPERTY_TO_MAP(PROP_GRAB_GRABBABLE, Grab, grab, Grabbable, grabbable); ADD_GROUP_PROPERTY_TO_MAP(PROP_GRAB_KINEMATIC, Grab, grab, GrabKinematic, grabKinematic); @@ -2491,6 +2618,7 @@ bool EntityItemProperties::getPropertyInfo(const QString& propertyName, EntityPr ADD_PROPERTY_TO_MAP(PROP_JOINT_TRANSLATIONS_SET, JointTranslationsSet, jointTranslationsSet, QVector); ADD_PROPERTY_TO_MAP(PROP_JOINT_TRANSLATIONS, JointTranslations, jointTranslations, QVector); ADD_PROPERTY_TO_MAP(PROP_RELAY_PARENT_JOINTS, RelayParentJoints, relayParentJoints, bool); + ADD_PROPERTY_TO_MAP(PROP_GROUP_CULLED, GroupCulled, groupCulled, bool); { // Animation ADD_GROUP_PROPERTY_TO_MAP(PROP_ANIMATION_URL, Animation, animation, URL, url); ADD_GROUP_PROPERTY_TO_MAP(PROP_ANIMATION_ALLOW_TRANSLATION, Animation, animation, AllowTranslation, allowTranslation); @@ -2587,6 +2715,9 @@ bool EntityItemProperties::getPropertyInfo(const QString& propertyName, EntityPr // Web ADD_PROPERTY_TO_MAP(PROP_SOURCE_URL, SourceUrl, sourceUrl, QString); ADD_PROPERTY_TO_MAP(PROP_DPI, DPI, dpi, uint16_t); + ADD_PROPERTY_TO_MAP(PROP_SCRIPT_URL, ScriptURL, scriptURL, QString); + ADD_PROPERTY_TO_MAP(PROP_MAX_FPS, MaxFPS, maxFPS, uint8_t); + ADD_PROPERTY_TO_MAP(PROP_INPUT_MODE, InputMode, inputMode, WebInputMode); // Polyline ADD_PROPERTY_TO_MAP(PROP_LINE_POINTS, LinePoints, linePoints, QVector); @@ -2767,6 +2898,9 @@ OctreeElement::AppendState EntityItemProperties::encodeEntityEditPacket(PacketTy APPEND_ENTITY_PROPERTY(PROP_QUERY_AA_CUBE, properties.getQueryAACube()); APPEND_ENTITY_PROPERTY(PROP_CAN_CAST_SHADOW, properties.getCanCastShadow()); // APPEND_ENTITY_PROPERTY(PROP_VISIBLE_IN_SECONDARY_CAMERA, properties.getIsVisibleInSecondaryCamera()); // not sent over the wire + APPEND_ENTITY_PROPERTY(PROP_RENDER_LAYER, (uint32_t)properties.getRenderLayer()); + APPEND_ENTITY_PROPERTY(PROP_PRIMITIVE_MODE, (uint32_t)properties.getPrimitiveMode()); + APPEND_ENTITY_PROPERTY(PROP_IGNORE_PICK_INTERSECTION, properties.getIgnorePickIntersection()); _staticGrab.setProperties(properties); _staticGrab.appendToEditPacket(packetData, requestedProperties, propertyFlags, propertiesDidntFit, propertyCount, appendState); @@ -2873,6 +3007,7 @@ OctreeElement::AppendState EntityItemProperties::encodeEntityEditPacket(PacketTy APPEND_ENTITY_PROPERTY(PROP_JOINT_TRANSLATIONS_SET, properties.getJointTranslationsSet()); APPEND_ENTITY_PROPERTY(PROP_JOINT_TRANSLATIONS, properties.getJointTranslations()); APPEND_ENTITY_PROPERTY(PROP_RELAY_PARENT_JOINTS, properties.getRelayParentJoints()); + APPEND_ENTITY_PROPERTY(PROP_GROUP_CULLED, properties.getGroupCulled()); _staticAnimation.setProperties(properties); _staticAnimation.appendToEditPacket(packetData, requestedProperties, propertyFlags, propertiesDidntFit, propertyCount, appendState); @@ -2947,8 +3082,14 @@ OctreeElement::AppendState EntityItemProperties::encodeEntityEditPacket(PacketTy } if (properties.getType() == EntityTypes::Web) { + APPEND_ENTITY_PROPERTY(PROP_COLOR, properties.getColor()); + APPEND_ENTITY_PROPERTY(PROP_ALPHA, properties.getAlpha()); + APPEND_ENTITY_PROPERTY(PROP_SOURCE_URL, properties.getSourceUrl()); APPEND_ENTITY_PROPERTY(PROP_DPI, properties.getDPI()); + APPEND_ENTITY_PROPERTY(PROP_SCRIPT_URL, properties.getScriptURL()); + APPEND_ENTITY_PROPERTY(PROP_MAX_FPS, properties.getMaxFPS()); + APPEND_ENTITY_PROPERTY(PROP_INPUT_MODE, (uint32_t)properties.getInputMode()); } if (properties.getType() == EntityTypes::Line) { @@ -3208,6 +3349,9 @@ bool EntityItemProperties::decodeEntityEditPacket(const unsigned char* data, int READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_QUERY_AA_CUBE, AACube, setQueryAACube); READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_CAN_CAST_SHADOW, bool, setCanCastShadow); // READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_VISIBLE_IN_SECONDARY_CAMERA, bool, setIsVisibleInSecondaryCamera); // not sent over the wire + READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_RENDER_LAYER, RenderLayer, setRenderLayer); + READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_PRIMITIVE_MODE, PrimitiveMode, setPrimitiveMode); + READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_IGNORE_PICK_INTERSECTION, bool, setIgnorePickIntersection); properties.getGrab().decodeFromEditPacket(propertyFlags, dataAt, processedBytes); // Physics @@ -3312,6 +3456,7 @@ bool EntityItemProperties::decodeEntityEditPacket(const unsigned char* data, int READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_JOINT_TRANSLATIONS_SET, QVector, setJointTranslationsSet); READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_JOINT_TRANSLATIONS, QVector, setJointTranslations); READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_RELAY_PARENT_JOINTS, bool, setRelayParentJoints); + READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_GROUP_CULLED, bool, setGroupCulled); properties.getAnimation().decodeFromEditPacket(propertyFlags, dataAt, processedBytes); } @@ -3377,8 +3522,14 @@ bool EntityItemProperties::decodeEntityEditPacket(const unsigned char* data, int } if (properties.getType() == EntityTypes::Web) { + READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_COLOR, u8vec3Color, setColor); + READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_ALPHA, float, setAlpha); + READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_SOURCE_URL, QString, setSourceUrl); READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_DPI, uint16_t, setDPI); + READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_SCRIPT_URL, QString, setScriptURL); + READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_MAX_FPS, uint8_t, setMaxFPS); + READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_INPUT_MODE, WebInputMode, setInputMode); } if (properties.getType() == EntityTypes::Line) { @@ -3590,6 +3741,9 @@ void EntityItemProperties::markAllChanged() { _queryAACubeChanged = true; _canCastShadowChanged = true; _isVisibleInSecondaryCameraChanged = true; + _renderLayerChanged = true; + _primitiveModeChanged = true; + _ignorePickIntersectionChanged = true; _grab.markAllChanged(); // Physics @@ -3682,6 +3836,7 @@ void EntityItemProperties::markAllChanged() { _jointTranslationsSetChanged = true; _jointTranslationsChanged = true; _relayParentJointsChanged = true; + _groupCulledChanged = true; _animation.markAllChanged(); // Light @@ -3736,6 +3891,9 @@ void EntityItemProperties::markAllChanged() { // Web _sourceUrlChanged = true; _dpiChanged = true; + _scriptURLChanged = true; + _maxFPSChanged = true; + _inputModeChanged = true; // Polyline _linePointsChanged = true; @@ -3962,6 +4120,15 @@ QList EntityItemProperties::listChangedProperties() { if (isVisibleInSecondaryCameraChanged()) { out += "isVisibleInSecondaryCamera"; } + if (renderLayerChanged()) { + out += "renderLayer"; + } + if (primitiveModeChanged()) { + out += "primitiveMode"; + } + if (ignorePickIntersectionChanged()) { + out += "ignorePickIntersection"; + } getGrab().listChangedProperties(out); // Physics @@ -4208,6 +4375,9 @@ QList EntityItemProperties::listChangedProperties() { if (relayParentJointsChanged()) { out += "relayParentJoints"; } + if (groupCulledChanged()) { + out += "groupCulled"; + } getAnimation().listChangedProperties(out); // Light @@ -4338,6 +4508,15 @@ QList EntityItemProperties::listChangedProperties() { if (dpiChanged()) { out += "dpi"; } + if (scriptURLChanged()) { + out += "scriptURL"; + } + if (maxFPSChanged()) { + out += "maxFPS"; + } + if (inputModeChanged()) { + out += "inputMode"; + } // Polyline if (linePointsChanged()) { @@ -4616,14 +4795,14 @@ bool EntityItemProperties::blobToProperties(QScriptEngine& scriptEngine, const Q // DANGER: this method is NOT efficient. // begin recipe for converting unfortunately-formatted-binary-blob to EntityItemProperties QJsonDocument jsonProperties = QJsonDocument::fromBinaryData(blob); - if (!jsonProperties.isObject()) { + if (jsonProperties.isEmpty() || jsonProperties.isNull() || !jsonProperties.isObject() || jsonProperties.object().isEmpty()) { qCDebug(entities) << "bad avatarEntityData json" << QString(blob.toHex()); return false; } QVariant variant = jsonProperties.toVariant(); QVariantMap variantMap = variant.toMap(); QScriptValue scriptValue = variantMapToScriptValue(variantMap, scriptEngine); - EntityItemPropertiesFromScriptValueHonorReadOnly(scriptValue, properties); + EntityItemPropertiesFromScriptValueIgnoreReadOnly(scriptValue, properties); // end recipe return true; } diff --git a/libraries/entities/src/EntityItemProperties.h b/libraries/entities/src/EntityItemProperties.h index bb4d8c5878..654c8e2ab8 100644 --- a/libraries/entities/src/EntityItemProperties.h +++ b/libraries/entities/src/EntityItemProperties.h @@ -30,27 +30,33 @@ #include #include -#include "AnimationPropertyGroup.h" #include "EntityItemID.h" #include "EntityItemPropertiesDefaults.h" #include "EntityItemPropertiesMacros.h" #include "EntityTypes.h" #include "EntityPropertyFlags.h" #include "EntityPsuedoPropertyFlags.h" -#include "LightEntityItem.h" -#include "LineEntityItem.h" -#include "ParticleEffectEntityItem.h" -#include "PolyVoxEntityItem.h" #include "SimulationOwner.h" + +#include "TextEntityItem.h" +#include "WebEntityItem.h" +#include "ParticleEffectEntityItem.h" +#include "LineEntityItem.h" +#include "PolyVoxEntityItem.h" +#include "GridEntityItem.h" +#include "LightEntityItem.h" +#include "ZoneEntityItem.h" + +#include "AnimationPropertyGroup.h" #include "SkyboxPropertyGroup.h" #include "HazePropertyGroup.h" #include "BloomPropertyGroup.h" -#include "TextEntityItem.h" -#include "ZoneEntityItem.h" -#include "GridEntityItem.h" #include "MaterialMappingMode.h" #include "BillboardMode.h" +#include "RenderLayer.h" +#include "PrimitiveMode.h" +#include "WebInputMode.h" const quint64 UNKNOWN_CREATED_TIME = 0; @@ -169,6 +175,9 @@ public: DEFINE_PROPERTY_REF(PROP_QUERY_AA_CUBE, QueryAACube, queryAACube, AACube, AACube()); DEFINE_PROPERTY(PROP_CAN_CAST_SHADOW, CanCastShadow, canCastShadow, bool, ENTITY_ITEM_DEFAULT_CAN_CAST_SHADOW); DEFINE_PROPERTY(PROP_VISIBLE_IN_SECONDARY_CAMERA, IsVisibleInSecondaryCamera, isVisibleInSecondaryCamera, bool, ENTITY_ITEM_DEFAULT_VISIBLE_IN_SECONDARY_CAMERA); + DEFINE_PROPERTY_REF_ENUM(PROP_RENDER_LAYER, RenderLayer, renderLayer, RenderLayer, RenderLayer::WORLD); + DEFINE_PROPERTY_REF_ENUM(PROP_PRIMITIVE_MODE, PrimitiveMode, primitiveMode, PrimitiveMode, PrimitiveMode::SOLID); + DEFINE_PROPERTY(PROP_IGNORE_PICK_INTERSECTION, IgnorePickIntersection, ignorePickIntersection, bool, false); DEFINE_PROPERTY_GROUP(Grab, grab, GrabPropertyGroup); // Physics @@ -268,6 +277,7 @@ public: DEFINE_PROPERTY_REF(PROP_JOINT_TRANSLATIONS_SET, JointTranslationsSet, jointTranslationsSet, QVector, QVector()); DEFINE_PROPERTY_REF(PROP_JOINT_TRANSLATIONS, JointTranslations, jointTranslations, QVector, ENTITY_ITEM_DEFAULT_EMPTY_VEC3_QVEC); DEFINE_PROPERTY(PROP_RELAY_PARENT_JOINTS, RelayParentJoints, relayParentJoints, bool, ENTITY_ITEM_DEFAULT_RELAY_PARENT_JOINTS); + DEFINE_PROPERTY_REF(PROP_GROUP_CULLED, GroupCulled, groupCulled, bool, false); DEFINE_PROPERTY_GROUP(Animation, animation, AnimationPropertyGroup); // Light @@ -320,8 +330,11 @@ public: DEFINE_PROPERTY_REF(PROP_Z_P_NEIGHBOR_ID, ZPNeighborID, zPNeighborID, EntityItemID, UNKNOWN_ENTITY_ID); // Web - DEFINE_PROPERTY_REF(PROP_SOURCE_URL, SourceUrl, sourceUrl, QString, ""); + DEFINE_PROPERTY_REF(PROP_SOURCE_URL, SourceUrl, sourceUrl, QString, WebEntityItem::DEFAULT_SOURCE_URL); DEFINE_PROPERTY_REF(PROP_DPI, DPI, dpi, uint16_t, ENTITY_ITEM_DEFAULT_DPI); + DEFINE_PROPERTY_REF(PROP_SCRIPT_URL, ScriptURL, scriptURL, QString, ""); + DEFINE_PROPERTY_REF(PROP_MAX_FPS, MaxFPS, maxFPS, uint8_t, WebEntityItem::DEFAULT_MAX_FPS); + DEFINE_PROPERTY_REF_ENUM(PROP_INPUT_MODE, InputMode, inputMode, WebInputMode, WebInputMode::TOUCH); // Polyline DEFINE_PROPERTY_REF(PROP_LINE_POINTS, LinePoints, linePoints, QVector, ENTITY_ITEM_DEFAULT_EMPTY_VEC3_QVEC); diff --git a/libraries/entities/src/EntityPropertyFlags.h b/libraries/entities/src/EntityPropertyFlags.h index 5e4b27858c..5c136c4820 100644 --- a/libraries/entities/src/EntityPropertyFlags.h +++ b/libraries/entities/src/EntityPropertyFlags.h @@ -39,6 +39,9 @@ enum EntityPropertyList { PROP_QUERY_AA_CUBE, PROP_CAN_CAST_SHADOW, PROP_VISIBLE_IN_SECONDARY_CAMERA, // not sent over the wire + PROP_RENDER_LAYER, + PROP_PRIMITIVE_MODE, + PROP_IGNORE_PICK_INTERSECTION, // Grab PROP_GRAB_GRABBABLE, PROP_GRAB_KINEMATIC, @@ -198,16 +201,17 @@ enum EntityPropertyList { PROP_JOINT_TRANSLATIONS_SET = PROP_DERIVED_3, PROP_JOINT_TRANSLATIONS = PROP_DERIVED_4, PROP_RELAY_PARENT_JOINTS = PROP_DERIVED_5, + PROP_GROUP_CULLED = PROP_DERIVED_6, // Animation - PROP_ANIMATION_URL = PROP_DERIVED_6, - PROP_ANIMATION_ALLOW_TRANSLATION = PROP_DERIVED_7, - PROP_ANIMATION_FPS = PROP_DERIVED_8, - PROP_ANIMATION_FRAME_INDEX = PROP_DERIVED_9, - PROP_ANIMATION_PLAYING = PROP_DERIVED_10, - PROP_ANIMATION_LOOP = PROP_DERIVED_11, - PROP_ANIMATION_FIRST_FRAME = PROP_DERIVED_12, - PROP_ANIMATION_LAST_FRAME = PROP_DERIVED_13, - PROP_ANIMATION_HOLD = PROP_DERIVED_14, + PROP_ANIMATION_URL = PROP_DERIVED_7, + PROP_ANIMATION_ALLOW_TRANSLATION = PROP_DERIVED_8, + PROP_ANIMATION_FPS = PROP_DERIVED_9, + PROP_ANIMATION_FRAME_INDEX = PROP_DERIVED_10, + PROP_ANIMATION_PLAYING = PROP_DERIVED_11, + PROP_ANIMATION_LOOP = PROP_DERIVED_12, + PROP_ANIMATION_FIRST_FRAME = PROP_DERIVED_13, + PROP_ANIMATION_LAST_FRAME = PROP_DERIVED_14, + PROP_ANIMATION_HOLD = PROP_DERIVED_15, // Light PROP_IS_SPOTLIGHT = PROP_DERIVED_0, @@ -284,6 +288,9 @@ enum EntityPropertyList { // Web PROP_SOURCE_URL = PROP_DERIVED_0, PROP_DPI = PROP_DERIVED_1, + PROP_SCRIPT_URL = PROP_DERIVED_2, + PROP_MAX_FPS = PROP_DERIVED_3, + PROP_INPUT_MODE = PROP_DERIVED_4, // Polyline PROP_LINE_POINTS = PROP_DERIVED_0, diff --git a/libraries/entities/src/EntityScriptingInterface.cpp b/libraries/entities/src/EntityScriptingInterface.cpp index 697d583de7..286f0dd650 100644 --- a/libraries/entities/src/EntityScriptingInterface.cpp +++ b/libraries/entities/src/EntityScriptingInterface.cpp @@ -986,7 +986,7 @@ void EntityScriptingInterface::deleteEntity(QUuid id) { shouldSendDeleteToServer = false; } else { // only delete local entities, server entities will round trip through the server filters - if (entity->isAvatarEntity() || _entityTree->isServerlessMode()) { + if (!entity->isDomainEntity() || _entityTree->isServerlessMode()) { shouldSendDeleteToServer = false; _entityTree->deleteEntity(entityID); diff --git a/libraries/entities/src/EntityTree.cpp b/libraries/entities/src/EntityTree.cpp index fb1a11d43f..954462a9f2 100644 --- a/libraries/entities/src/EntityTree.cpp +++ b/libraries/entities/src/EntityTree.cpp @@ -70,6 +70,49 @@ OctreeElementPointer EntityTree::createNewElement(unsigned char* octalCode) { return std::static_pointer_cast(newElement); } +void EntityTree::eraseNonLocalEntities() { + emit clearingEntities(); + + if (_simulation) { + // This will clear all entities host types including local entities, because local entities + // are not in the physics simulation + _simulation->clearEntities(); + } + _staleProxies.clear(); + QHash localMap; + localMap.swap(_entityMap); + QHash savedEntities; + this->withWriteLock([&] { + foreach(EntityItemPointer entity, localMap) { + EntityTreeElementPointer element = entity->getElement(); + if (element) { + element->cleanupNonLocalEntities(); + } + + if (entity->isLocalEntity()) { + savedEntities[entity->getEntityItemID()] = entity; + } + } + }); + localMap.clear(); + _entityMap = savedEntities; + + resetClientEditStats(); + clearDeletedEntities(); + + { + QWriteLocker locker(&_needsParentFixupLock); + QVector localEntitiesNeedsParentFixup; + + foreach (EntityItemWeakPointer entityItem, _needsParentFixup) { + if (!entityItem.expired() && entityItem.lock()->isLocalEntity()) { + localEntitiesNeedsParentFixup.push_back(entityItem); + } + } + + _needsParentFixup = localEntitiesNeedsParentFixup; + } +} void EntityTree::eraseAllOctreeElements(bool createNewRoot) { emit clearingEntities(); @@ -2664,7 +2707,6 @@ void convertGrabUserDataToProperties(EntityItemProperties& properties) { bool EntityTree::readFromMap(QVariantMap& map) { // These are needed to deal with older content (before adding inheritance modes) int contentVersion = map["Version"].toInt(); - bool needsConversion = (contentVersion < (int)EntityVersion::ZoneLightInheritModes); if (map.contains("Id")) { _persistID = map["Id"].toUuid(); @@ -2739,7 +2781,7 @@ bool EntityTree::readFromMap(QVariantMap& map) { } // Fix for older content not containing mode fields in the zones - if (needsConversion && (properties.getType() == EntityTypes::EntityType::Zone)) { + if (contentVersion < (int)EntityVersion::ZoneLightInheritModes && (properties.getType() == EntityTypes::EntityType::Zone)) { // The legacy version had no keylight mode - this is set to on properties.setKeyLightMode(COMPONENT_MODE_ENABLED); @@ -2965,8 +3007,8 @@ void EntityTree::updateEntityQueryAACubeWorker(SpatiallyNestablePointer object, // if the queryBox has changed, tell the entity-server EntityItemPointer entity = std::dynamic_pointer_cast(object); if (entity) { - bool tellServerThis = tellServer && (entity->getEntityHostType() != entity::HostType::AVATAR); - if ((entity->updateQueryAACube() || force)) { + // NOTE: we rely on side-effects of the entity->updateQueryAACube() call in the following if() conditional: + if (entity->updateQueryAACube() || force) { bool success; AACube newCube = entity->getQueryAACube(success); if (success) { @@ -2974,7 +3016,7 @@ void EntityTree::updateEntityQueryAACubeWorker(SpatiallyNestablePointer object, } // send an edit packet to update the entity-server about the queryAABox. We do this for domain-hosted // entities as well as for avatar-entities; the packet-sender will route the update accordingly - if (tellServerThis && packetSender && (entity->isDomainEntity() || entity->isAvatarEntity())) { + if (tellServer && packetSender && (entity->isDomainEntity() || entity->isAvatarEntity())) { quint64 now = usecTimestampNow(); EntityItemProperties properties = entity->getProperties(); properties.setQueryAACubeDirty(); @@ -2982,6 +3024,7 @@ void EntityTree::updateEntityQueryAACubeWorker(SpatiallyNestablePointer object, properties.setLastEdited(now); packetSender->queueEditEntityMessage(PacketType::EntityEdit, getThisPointer(), entity->getID(), properties); + entity->setLastEdited(now); // so we ignore the echo from the server entity->setLastBroadcast(now); // for debug/physics status icons } diff --git a/libraries/entities/src/EntityTree.h b/libraries/entities/src/EntityTree.h index 9181a4851c..f9b7b8d67f 100644 --- a/libraries/entities/src/EntityTree.h +++ b/libraries/entities/src/EntityTree.h @@ -74,6 +74,8 @@ public: return std::static_pointer_cast(_rootElement); } + + virtual void eraseNonLocalEntities() override; virtual void eraseAllOctreeElements(bool createNewRoot = true) override; virtual void readBitstreamToTree(const unsigned char* bitstream, diff --git a/libraries/entities/src/EntityTreeElement.cpp b/libraries/entities/src/EntityTreeElement.cpp index 2ece6835ea..3b085457d1 100644 --- a/libraries/entities/src/EntityTreeElement.cpp +++ b/libraries/entities/src/EntityTreeElement.cpp @@ -187,6 +187,10 @@ EntityItemID EntityTreeElement::evalDetailedRayIntersection(const glm::vec3& ori // only called if we do intersect our bounding cube, but find if we actually intersect with entities... EntityItemID entityID; forEachEntity([&](EntityItemPointer entity) { + if (entity->getIgnorePickIntersection()) { + return; + } + // use simple line-sphere for broadphase check // (this is faster and more likely to cull results than the filter check below so we do it first) bool success; @@ -327,6 +331,10 @@ EntityItemID EntityTreeElement::evalDetailedParabolaIntersection(const glm::vec3 // only called if we do intersect our bounding cube, but find if we actually intersect with entities... EntityItemID entityID; forEachEntity([&](EntityItemPointer entity) { + if (entity->getIgnorePickIntersection()) { + return; + } + // use simple line-sphere for broadphase check // (this is faster and more likely to cull results than the filter check below so we do it first) bool success; @@ -683,6 +691,23 @@ EntityItemPointer EntityTreeElement::getEntityWithEntityItemID(const EntityItemI return foundEntity; } +void EntityTreeElement::cleanupNonLocalEntities() { + withWriteLock([&] { + EntityItems savedEntities; + foreach(EntityItemPointer entity, _entityItems) { + if (!entity->isLocalEntity()) { + entity->preDelete(); + entity->_element = NULL; + } else { + savedEntities.push_back(entity); + } + } + + _entityItems = savedEntities; + }); + bumpChangedContent(); +} + void EntityTreeElement::cleanupEntities() { withWriteLock([&] { foreach(EntityItemPointer entity, _entityItems) { diff --git a/libraries/entities/src/EntityTreeElement.h b/libraries/entities/src/EntityTreeElement.h index aed19eed15..3f1fda57bd 100644 --- a/libraries/entities/src/EntityTreeElement.h +++ b/libraries/entities/src/EntityTreeElement.h @@ -189,6 +189,7 @@ public: EntityItemPointer getEntityWithEntityItemID(const EntityItemID& id) const; void getEntitiesInside(const AACube& box, QVector& foundEntities); + void cleanupNonLocalEntities(); void cleanupEntities(); /// called by EntityTree on cleanup this will free all entities bool removeEntityItem(EntityItemPointer entity, bool deletion = false); diff --git a/libraries/entities/src/ModelEntityItem.cpp b/libraries/entities/src/ModelEntityItem.cpp index 55ae1c6c3b..ddbb028b6e 100644 --- a/libraries/entities/src/ModelEntityItem.cpp +++ b/libraries/entities/src/ModelEntityItem.cpp @@ -67,6 +67,7 @@ EntityItemProperties ModelEntityItem::getProperties(const EntityPropertyFlags& d COPY_ENTITY_PROPERTY_TO_PROPERTIES(jointTranslationsSet, getJointTranslationsSet); COPY_ENTITY_PROPERTY_TO_PROPERTIES(jointTranslations, getJointTranslations); COPY_ENTITY_PROPERTY_TO_PROPERTIES(relayParentJoints, getRelayParentJoints); + COPY_ENTITY_PROPERTY_TO_PROPERTIES(groupCulled, getGroupCulled); withReadLock([&] { _animationProperties.getProperties(properties); }); @@ -88,6 +89,7 @@ bool ModelEntityItem::setProperties(const EntityItemProperties& properties) { SET_ENTITY_PROPERTY_FROM_PROPERTIES(jointTranslationsSet, setJointTranslationsSet); SET_ENTITY_PROPERTY_FROM_PROPERTIES(jointTranslations, setJointTranslations); SET_ENTITY_PROPERTY_FROM_PROPERTIES(relayParentJoints, setRelayParentJoints); + SET_ENTITY_PROPERTY_FROM_PROPERTIES(groupCulled, setGroupCulled); withWriteLock([&] { AnimationPropertyGroup animationProperties = _animationProperties; @@ -130,6 +132,7 @@ int ModelEntityItem::readEntitySubclassDataFromBuffer(const unsigned char* data, READ_ENTITY_PROPERTY(PROP_JOINT_TRANSLATIONS_SET, QVector, setJointTranslationsSet); READ_ENTITY_PROPERTY(PROP_JOINT_TRANSLATIONS, QVector, setJointTranslations); READ_ENTITY_PROPERTY(PROP_RELAY_PARENT_JOINTS, bool, setRelayParentJoints); + READ_ENTITY_PROPERTY(PROP_GROUP_CULLED, bool, setGroupCulled); // grab a local copy of _animationProperties to avoid multiple locks int bytesFromAnimation; @@ -166,6 +169,7 @@ EntityPropertyFlags ModelEntityItem::getEntityProperties(EncodeBitstreamParams& requestedProperties += PROP_JOINT_TRANSLATIONS_SET; requestedProperties += PROP_JOINT_TRANSLATIONS; requestedProperties += PROP_RELAY_PARENT_JOINTS; + requestedProperties += PROP_GROUP_CULLED; requestedProperties += _animationProperties.getEntityProperties(params); return requestedProperties; @@ -192,6 +196,7 @@ void ModelEntityItem::appendSubclassData(OctreePacketData* packetData, EncodeBit APPEND_ENTITY_PROPERTY(PROP_JOINT_TRANSLATIONS_SET, getJointTranslationsSet()); APPEND_ENTITY_PROPERTY(PROP_JOINT_TRANSLATIONS, getJointTranslations()); APPEND_ENTITY_PROPERTY(PROP_RELAY_PARENT_JOINTS, getRelayParentJoints()); + APPEND_ENTITY_PROPERTY(PROP_GROUP_CULLED, getGroupCulled()); withReadLock([&] { _animationProperties.appendSubclassData(packetData, params, entityTreeElementExtraEncodeData, requestedProperties, @@ -548,6 +553,18 @@ bool ModelEntityItem::getRelayParentJoints() const { }); } +void ModelEntityItem::setGroupCulled(bool value) { + withWriteLock([&] { + _groupCulled = value; + }); +} + +bool ModelEntityItem::getGroupCulled() const { + return resultWithReadLock([&] { + return _groupCulled; + }); +} + QString ModelEntityItem::getCompoundShapeURL() const { return _compoundShapeURL.get(); } diff --git a/libraries/entities/src/ModelEntityItem.h b/libraries/entities/src/ModelEntityItem.h index 5ca3e2caa1..8c9fbdc45f 100644 --- a/libraries/entities/src/ModelEntityItem.h +++ b/libraries/entities/src/ModelEntityItem.h @@ -100,6 +100,9 @@ public: void setRelayParentJoints(bool relayJoints); bool getRelayParentJoints() const; + void setGroupCulled(bool value); + bool getGroupCulled() const; + bool getAnimationIsPlaying() const; float getAnimationCurrentFrame() const; float getAnimationFPS() const; @@ -154,6 +157,7 @@ protected: glm::u8vec3 _color; QString _modelURL; bool _relayParentJoints; + bool _groupCulled { false }; ThreadSafeValueCache _compoundShapeURL; diff --git a/libraries/entities/src/ShapeEntityItem.cpp b/libraries/entities/src/ShapeEntityItem.cpp index 88612c8be6..819cbf9dad 100644 --- a/libraries/entities/src/ShapeEntityItem.cpp +++ b/libraries/entities/src/ShapeEntityItem.cpp @@ -112,7 +112,6 @@ EntityItemPointer ShapeEntityItem::sphereFactory(const EntityItemID& entityID, c ShapeEntityItem::ShapeEntityItem(const EntityItemID& entityItemID) : EntityItem(entityItemID) { _type = EntityTypes::Shape; _volumeMultiplier *= PI / 6.0f; - _material = std::make_shared(); } EntityItemProperties ShapeEntityItem::getProperties(const EntityPropertyFlags& desiredProperties, bool allowEmptyDesiredProperties) const { @@ -215,7 +214,6 @@ void ShapeEntityItem::appendSubclassData(OctreePacketData* packetData, EncodeBit void ShapeEntityItem::setColor(const glm::u8vec3& value) { withWriteLock([&] { _color = value; - _material->setAlbedo(toGlm(_color)); }); } @@ -228,7 +226,12 @@ glm::u8vec3 ShapeEntityItem::getColor() const { void ShapeEntityItem::setAlpha(float alpha) { withWriteLock([&] { _alpha = alpha; - _material->setOpacity(alpha); + }); +} + +float ShapeEntityItem::getAlpha() const { + return resultWithReadLock([&] { + return _alpha; }); } diff --git a/libraries/entities/src/ShapeEntityItem.h b/libraries/entities/src/ShapeEntityItem.h index c89a8934f8..417f0c6173 100644 --- a/libraries/entities/src/ShapeEntityItem.h +++ b/libraries/entities/src/ShapeEntityItem.h @@ -74,7 +74,7 @@ public: void setShape(const entity::Shape& shape); void setShape(const QString& shape) { setShape(entity::shapeFromString(shape)); } - float getAlpha() const { return _alpha; }; + float getAlpha() const; void setAlpha(float alpha); glm::u8vec3 getColor() const; @@ -99,20 +99,15 @@ public: virtual void computeShapeInfo(ShapeInfo& info) override; virtual ShapeType getShapeType() const override; - std::shared_ptr getMaterial() { return _material; } - protected: - - float _alpha { 1.0f }; glm::u8vec3 _color; + float _alpha { 1.0f }; entity::Shape _shape { entity::Shape::Sphere }; //! This is SHAPE_TYPE_ELLIPSOID rather than SHAPE_TYPE_NONE to maintain //! prior functionality where new or unsupported shapes are treated as //! ellipsoids. ShapeType _collisionShapeType{ ShapeType::SHAPE_TYPE_ELLIPSOID }; - - std::shared_ptr _material; }; #endif // hifi_ShapeEntityItem_h diff --git a/libraries/entities/src/WebEntityItem.cpp b/libraries/entities/src/WebEntityItem.cpp index d20d36cd71..c0cdb081a2 100644 --- a/libraries/entities/src/WebEntityItem.cpp +++ b/libraries/entities/src/WebEntityItem.cpp @@ -21,7 +21,8 @@ #include "EntityTree.h" #include "EntityTreeElement.h" -const QString WebEntityItem::DEFAULT_SOURCE_URL("http://www.google.com"); +const QString WebEntityItem::DEFAULT_SOURCE_URL = "http://www.google.com"; +const uint8_t WebEntityItem::DEFAULT_MAX_FPS = 10; EntityItemPointer WebEntityItem::factory(const EntityItemID& entityID, const EntityItemProperties& properties) { EntityItemPointer entity(new WebEntityItem(entityID), [](EntityItem* ptr) { ptr->deleteLater(); }); @@ -31,20 +32,25 @@ EntityItemPointer WebEntityItem::factory(const EntityItemID& entityID, const Ent WebEntityItem::WebEntityItem(const EntityItemID& entityItemID) : EntityItem(entityItemID) { _type = EntityTypes::Web; - _dpi = ENTITY_ITEM_DEFAULT_DPI; } -const float WEB_ENTITY_ITEM_FIXED_DEPTH = 0.01f; - void WebEntityItem::setUnscaledDimensions(const glm::vec3& value) { // NOTE: Web Entities always have a "depth" of 1cm. + const float WEB_ENTITY_ITEM_FIXED_DEPTH = 0.01f; EntityItem::setUnscaledDimensions(glm::vec3(value.x, value.y, WEB_ENTITY_ITEM_FIXED_DEPTH)); } EntityItemProperties WebEntityItem::getProperties(const EntityPropertyFlags& desiredProperties, bool allowEmptyDesiredProperties) const { EntityItemProperties properties = EntityItem::getProperties(desiredProperties, allowEmptyDesiredProperties); // get the properties from our base class + + COPY_ENTITY_PROPERTY_TO_PROPERTIES(color, getColor); + COPY_ENTITY_PROPERTY_TO_PROPERTIES(alpha, getAlpha); + COPY_ENTITY_PROPERTY_TO_PROPERTIES(sourceUrl, getSourceUrl); COPY_ENTITY_PROPERTY_TO_PROPERTIES(dpi, getDPI); + COPY_ENTITY_PROPERTY_TO_PROPERTIES(scriptURL, getScriptURL); + COPY_ENTITY_PROPERTY_TO_PROPERTIES(maxFPS, getMaxFPS); + COPY_ENTITY_PROPERTY_TO_PROPERTIES(inputMode, getInputMode); return properties; } @@ -52,8 +58,14 @@ bool WebEntityItem::setProperties(const EntityItemProperties& properties) { bool somethingChanged = false; somethingChanged = EntityItem::setProperties(properties); // set the properties in our base class + SET_ENTITY_PROPERTY_FROM_PROPERTIES(color, setColor); + SET_ENTITY_PROPERTY_FROM_PROPERTIES(alpha, setAlpha); + SET_ENTITY_PROPERTY_FROM_PROPERTIES(sourceUrl, setSourceUrl); SET_ENTITY_PROPERTY_FROM_PROPERTIES(dpi, setDPI); + SET_ENTITY_PROPERTY_FROM_PROPERTIES(scriptURL, setScriptURL); + SET_ENTITY_PROPERTY_FROM_PROPERTIES(maxFPS, setMaxFPS); + SET_ENTITY_PROPERTY_FROM_PROPERTIES(inputMode, setInputMode); if (somethingChanged) { bool wantDebug = false; @@ -77,16 +89,28 @@ int WebEntityItem::readEntitySubclassDataFromBuffer(const unsigned char* data, i int bytesRead = 0; const unsigned char* dataAt = data; + READ_ENTITY_PROPERTY(PROP_COLOR, glm::u8vec3, setColor); + READ_ENTITY_PROPERTY(PROP_ALPHA, float, setAlpha); + READ_ENTITY_PROPERTY(PROP_SOURCE_URL, QString, setSourceUrl); READ_ENTITY_PROPERTY(PROP_DPI, uint16_t, setDPI); + READ_ENTITY_PROPERTY(PROP_SCRIPT_URL, QString, setScriptURL); + READ_ENTITY_PROPERTY(PROP_MAX_FPS, uint8_t, setMaxFPS); + READ_ENTITY_PROPERTY(PROP_INPUT_MODE, WebInputMode, setInputMode); return bytesRead; } EntityPropertyFlags WebEntityItem::getEntityProperties(EncodeBitstreamParams& params) const { EntityPropertyFlags requestedProperties = EntityItem::getEntityProperties(params); + requestedProperties += PROP_COLOR; + requestedProperties += PROP_ALPHA; + requestedProperties += PROP_SOURCE_URL; requestedProperties += PROP_DPI; + requestedProperties += PROP_SCRIPT_URL; + requestedProperties += PROP_MAX_FPS; + requestedProperties += PROP_INPUT_MODE; return requestedProperties; } @@ -99,8 +123,14 @@ void WebEntityItem::appendSubclassData(OctreePacketData* packetData, EncodeBitst OctreeElement::AppendState& appendState) const { bool successPropertyFits = true; - APPEND_ENTITY_PROPERTY(PROP_SOURCE_URL, _sourceUrl); - APPEND_ENTITY_PROPERTY(PROP_DPI, _dpi); + APPEND_ENTITY_PROPERTY(PROP_COLOR, getColor()); + APPEND_ENTITY_PROPERTY(PROP_ALPHA, getAlpha()); + + APPEND_ENTITY_PROPERTY(PROP_SOURCE_URL, getSourceUrl()); + APPEND_ENTITY_PROPERTY(PROP_DPI, getDPI()); + APPEND_ENTITY_PROPERTY(PROP_SCRIPT_URL, getScriptURL()); + APPEND_ENTITY_PROPERTY(PROP_MAX_FPS, getMaxFPS()); + APPEND_ENTITY_PROPERTY(PROP_INPUT_MODE, (uint32_t)getInputMode()); } bool WebEntityItem::findDetailedRayIntersection(const glm::vec3& origin, const glm::vec3& direction, @@ -157,6 +187,30 @@ bool WebEntityItem::findDetailedParabolaIntersection(const glm::vec3& origin, co } } +void WebEntityItem::setColor(const glm::u8vec3& value) { + withWriteLock([&] { + _color = value; + }); +} + +glm::u8vec3 WebEntityItem::getColor() const { + return resultWithReadLock([&] { + return _color; + }); +} + +void WebEntityItem::setAlpha(float alpha) { + withWriteLock([&] { + _alpha = alpha; + }); +} + +float WebEntityItem::getAlpha() const { + return resultWithReadLock([&] { + return _alpha; + }); +} + void WebEntityItem::setSourceUrl(const QString& value) { withWriteLock([&] { if (_sourceUrl != value) { @@ -172,17 +226,63 @@ void WebEntityItem::setSourceUrl(const QString& value) { } QString WebEntityItem::getSourceUrl() const { - QString result; - withReadLock([&] { - result = _sourceUrl; + return resultWithReadLock([&] { + return _sourceUrl; }); - return result; } void WebEntityItem::setDPI(uint16_t value) { - _dpi = value; + withWriteLock([&] { + _dpi = value; + }); } uint16_t WebEntityItem::getDPI() const { - return _dpi; + return resultWithReadLock([&] { + return _dpi; + }); } + +void WebEntityItem::setScriptURL(const QString& value) { + withWriteLock([&] { + if (_scriptURL != value) { + auto newURL = QUrl::fromUserInput(value); + + if (newURL.isValid()) { + _scriptURL = newURL.toDisplayString(); + } else { + qCDebug(entities) << "Clearing web entity source URL since" << value << "cannot be parsed to a valid URL."; + } + } + }); +} + +QString WebEntityItem::getScriptURL() const { + return resultWithReadLock([&] { + return _scriptURL; + }); +} + +void WebEntityItem::setMaxFPS(uint8_t value) { + withWriteLock([&] { + _maxFPS = value; + }); +} + +uint8_t WebEntityItem::getMaxFPS() const { + return resultWithReadLock([&] { + return _maxFPS; + }); +} + +void WebEntityItem::setInputMode(const WebInputMode& value) { + withWriteLock([&] { + _inputMode = value; + }); +} + +WebInputMode WebEntityItem::getInputMode() const { + return resultWithReadLock([&] { + return _inputMode; + }); +} \ No newline at end of file diff --git a/libraries/entities/src/WebEntityItem.h b/libraries/entities/src/WebEntityItem.h index 10d06e6f4a..c566bcb5f7 100644 --- a/libraries/entities/src/WebEntityItem.h +++ b/libraries/entities/src/WebEntityItem.h @@ -13,8 +13,6 @@ class WebEntityItem : public EntityItem { public: - static const QString DEFAULT_SOURCE_URL; - static EntityItemPointer factory(const EntityItemID& entityID, const EntityItemProperties& properties); WebEntityItem(const EntityItemID& entityItemID); @@ -54,15 +52,38 @@ public: BoxFace& face, glm::vec3& surfaceNormal, QVariantMap& extraInfo, bool precisionPicking) const override; - virtual void setSourceUrl(const QString& value); + glm::u8vec3 getColor() const; + void setColor(const glm::u8vec3& value); + + float getAlpha() const; + void setAlpha(float alpha); + + static const QString DEFAULT_SOURCE_URL; + void setSourceUrl(const QString& value); QString getSourceUrl() const; void setDPI(uint16_t value); uint16_t getDPI() const; + void setScriptURL(const QString& value); + QString getScriptURL() const; + + static const uint8_t DEFAULT_MAX_FPS; + void setMaxFPS(uint8_t value); + uint8_t getMaxFPS() const; + + void setInputMode(const WebInputMode& value); + WebInputMode getInputMode() const; + protected: + glm::u8vec3 _color; + float _alpha { 1.0f }; + QString _sourceUrl; uint16_t _dpi; + QString _scriptURL; + uint8_t _maxFPS; + WebInputMode _inputMode; }; #endif // hifi_WebEntityItem_h diff --git a/libraries/fbx/src/FBXSerializer.cpp b/libraries/fbx/src/FBXSerializer.cpp index 68019c2f4b..07102253e4 100644 --- a/libraries/fbx/src/FBXSerializer.cpp +++ b/libraries/fbx/src/FBXSerializer.cpp @@ -131,6 +131,7 @@ public: glm::vec3 geometricTranslation; glm::quat geometricRotation; glm::vec3 geometricScaling; + bool isLimbNode; // is this FBXModel transform is a "LimbNode" i.e. a joint }; glm::mat4 getGlobalTransform(const QMultiMap& _connectionParentMap, @@ -559,9 +560,11 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr glm::vec3 geometricRotation; glm::vec3 rotationMin, rotationMax; + + bool isLimbNode = object.properties.size() >= 3 && object.properties.at(2) == "LimbNode"; FBXModel fbxModel = { name, -1, glm::vec3(), glm::mat4(), glm::quat(), glm::quat(), glm::quat(), - glm::mat4(), glm::vec3(), glm::vec3(), - false, glm::vec3(), glm::quat(), glm::vec3(1.0f) }; + glm::mat4(), glm::vec3(), glm::vec3(), + false, glm::vec3(), glm::quat(), glm::vec3(1.0f), isLimbNode }; ExtractedMesh* mesh = NULL; QVector blendshapes; foreach (const FBXNode& subobject, object.children) { @@ -752,17 +755,17 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr } else if (subobject.name == "Texture_Alpha_Source" && subobject.properties.length() >= TEXTURE_ALPHA_SOURCE_MIN_SIZE) { tex.assign(tex.alphaSource, subobject.properties.at(0).value()); } else if (subobject.name == "ModelUVTranslation" && subobject.properties.length() >= MODEL_UV_TRANSLATION_MIN_SIZE) { - tex.assign(tex.UVTranslation, glm::vec2(subobject.properties.at(0).value(), - subobject.properties.at(1).value())); + auto newTranslation = glm::vec3(subobject.properties.at(0).value(), subobject.properties.at(1).value(), 0.0); + tex.assign(tex.translation, tex.translation + newTranslation); } else if (subobject.name == "ModelUVScaling" && subobject.properties.length() >= MODEL_UV_SCALING_MIN_SIZE) { - tex.assign(tex.UVScaling, glm::vec2(subobject.properties.at(0).value(), - subobject.properties.at(1).value())); - if (tex.UVScaling.x == 0.0f) { - tex.UVScaling.x = 1.0f; + auto newScaling = glm::vec3(subobject.properties.at(0).value(), subobject.properties.at(1).value(), 1.0); + if (newScaling.x == 0.0f) { + newScaling.x = 1.0f; } - if (tex.UVScaling.y == 0.0f) { - tex.UVScaling.y = 1.0f; + if (newScaling.y == 0.0f) { + newScaling.y = 1.0f; } + tex.assign(tex.scaling, tex.scaling * newScaling); } else if (subobject.name == "Cropping" && subobject.properties.length() >= CROPPING_MIN_SIZE) { tex.assign(tex.cropping, glm::vec4(subobject.properties.at(0).value(), subobject.properties.at(1).value(), @@ -790,20 +793,21 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr } else if (property.properties.at(0) == USE_MATERIAL) { tex.assign(tex.useMaterial, property.properties.at(index).value()); } else if (property.properties.at(0) == TRANSLATION) { - tex.assign(tex.translation, getVec3(property.properties, index)); + tex.assign(tex.translation, tex.translation + getVec3(property.properties, index)); } else if (property.properties.at(0) == ROTATION) { tex.assign(tex.rotation, getVec3(property.properties, index)); } else if (property.properties.at(0) == SCALING) { - tex.assign(tex.scaling, getVec3(property.properties, index)); - if (tex.scaling.x == 0.0f) { - tex.scaling.x = 1.0f; + auto newScaling = getVec3(property.properties, index); + if (newScaling.x == 0.0f) { + newScaling.x = 1.0f; } - if (tex.scaling.y == 0.0f) { - tex.scaling.y = 1.0f; + if (newScaling.y == 0.0f) { + newScaling.y = 1.0f; } - if (tex.scaling.z == 0.0f) { - tex.scaling.z = 1.0f; + if (newScaling.z == 0.0f) { + newScaling.z = 1.0f; } + tex.assign(tex.scaling, tex.scaling * newScaling); } #if defined(DEBUG_FBXSERIALIZER) else { @@ -848,6 +852,7 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr } } else if (object.name == "Material") { HFMMaterial material; + MaterialParam materialParam; material.name = (object.properties.at(1).toString()); foreach (const FBXNode& subobject, object.children) { bool properties = false; @@ -892,6 +897,10 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr static const QVariant MAYA_EMISSIVE_INTENSITY = QByteArray("Maya|emissive_intensity"); static const QVariant MAYA_USE_EMISSIVE_MAP = QByteArray("Maya|use_emissive_map"); static const QVariant MAYA_USE_AO_MAP = QByteArray("Maya|use_ao_map"); + static const QVariant MAYA_UV_SCALE = QByteArray("Maya|uv_scale"); + static const QVariant MAYA_UV_OFFSET = QByteArray("Maya|uv_offset"); + static const int MAYA_UV_OFFSET_PROPERTY_LENGTH = 6; + static const int MAYA_UV_SCALE_PROPERTY_LENGTH = 6; @@ -980,6 +989,27 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr material.isPBSMaterial = true; material.useOcclusionMap = (bool)property.properties.at(index).value(); + } else if (property.properties.at(0) == MAYA_UV_SCALE) { + if (property.properties.size() == MAYA_UV_SCALE_PROPERTY_LENGTH) { + // properties: { "Maya|uv_scale", "Vector2D", "Vector2", nothing, double, double } + glm::vec3 scale = glm::vec3(property.properties.at(4).value(), property.properties.at(5).value(), 1.0); + if (scale.x == 0.0f) { + scale.x = 1.0f; + } + if (scale.y == 0.0f) { + scale.y = 1.0f; + } + if (scale.z == 0.0f) { + scale.z = 1.0f; + } + materialParam.scaling *= scale; + } + } else if (property.properties.at(0) == MAYA_UV_OFFSET) { + if (property.properties.size() == MAYA_UV_OFFSET_PROPERTY_LENGTH) { + // properties: { "Maya|uv_offset", "Vector2D", "Vector2", nothing, double, double } + glm::vec3 translation = glm::vec3(property.properties.at(4).value(), property.properties.at(5).value(), 1.0); + materialParam.translation += translation; + } } else { const QString propname = property.properties.at(0).toString(); unknowns.push_back(propname.toStdString()); @@ -1001,6 +1031,7 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr } material.materialID = getID(object.properties); _hfmMaterials.insert(material.materialID, material); + _materialParams.insert(material.materialID, materialParam); } else if (object.name == "NodeAttribute") { @@ -1258,6 +1289,7 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr // convert the models to joints QVariantList freeJoints = mapping.values("freeJoint"); hfmModel.hasSkeletonJoints = false; + foreach (const QString& modelID, modelIDs) { const FBXModel& fbxModel = fbxModels[modelID]; HFMJoint joint; @@ -1288,6 +1320,8 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr joint.geometricTranslation = fbxModel.geometricTranslation; joint.geometricRotation = fbxModel.geometricRotation; joint.geometricScaling = fbxModel.geometricScaling; + joint.isSkeletonJoint = fbxModel.isLimbNode; + hfmModel.hasSkeletonJoints = (hfmModel.hasSkeletonJoints || joint.isSkeletonJoint); glm::quat combinedRotation = joint.preRotation * joint.rotation * joint.postRotation; @@ -1311,14 +1345,6 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr joint.name = hfmModel.hfmToHifiJointNameMapping.key(fbxModel.name); } - foreach (const QString& childID, _connectionChildMap.values(modelID)) { - QString type = typeFlags.value(childID); - if (!type.isEmpty()) { - hfmModel.hasSkeletonJoints |= (joint.isSkeletonJoint = type.toLower().contains("Skeleton")); - break; - } - } - joint.bindTransformFoundInCluster = false; hfmModel.joints.append(joint); @@ -1439,7 +1465,9 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr materialIndex++; } else if (_textureFilenames.contains(childID)) { - HFMTexture texture = getTexture(childID); + // NOTE (Sabrina 2019/01/11): getTextures now takes in the materialID as a second parameter, because FBX material nodes can sometimes have uv transform information (ex: "Maya|uv_scale") + // I'm leaving the second parameter blank right now as this code may never be used. + HFMTexture texture = getTexture(childID, ""); for (int j = 0; j < extracted.partMaterialTextures.size(); j++) { int partTexture = extracted.partMaterialTextures.at(j).second; if (partTexture == textureIndex && !(partTexture == 0 && materialsHaveTextures)) { diff --git a/libraries/fbx/src/FBXSerializer.h b/libraries/fbx/src/FBXSerializer.h index 31ca301522..b95bb729e7 100644 --- a/libraries/fbx/src/FBXSerializer.h +++ b/libraries/fbx/src/FBXSerializer.h @@ -37,8 +37,6 @@ class FBXNode; class TextureParam { public: - glm::vec2 UVTranslation; - glm::vec2 UVScaling; glm::vec4 cropping; QString UVSet; @@ -63,8 +61,6 @@ public: bool isDefault; TextureParam() : - UVTranslation(0.0f), - UVScaling(1.0f), cropping(0.0f), UVSet("map1"), translation(0.0f), @@ -77,8 +73,6 @@ public: {} TextureParam(const TextureParam& src) : - UVTranslation(src.UVTranslation), - UVScaling(src.UVScaling), cropping(src.cropping), UVSet(src.UVSet), translation(src.translation), @@ -92,6 +86,22 @@ public: }; +class MaterialParam { +public: + glm::vec3 translation; + glm::vec3 scaling; + + MaterialParam() : + translation(0.0), + scaling(1.0) + {} + + MaterialParam(const MaterialParam& src) : + translation(src.translation), + scaling(src.scaling) + {} +}; + class ExtractedMesh; class FBXSerializer : public HFMSerializer { @@ -114,7 +124,7 @@ public: static ExtractedMesh extractMesh(const FBXNode& object, unsigned int& meshIndex, bool deduplicate = true); QHash meshes; - HFMTexture getTexture(const QString& textureID); + HFMTexture getTexture(const QString& textureID, const QString& materialID); QHash _textureNames; // Hashes the original RelativeFilename of textures @@ -141,6 +151,7 @@ public: QHash occlusionTextures; QHash _hfmMaterials; + QHash _materialParams; void consolidateHFMMaterials(const QVariantHash& mapping); diff --git a/libraries/fbx/src/FBXSerializer_Material.cpp b/libraries/fbx/src/FBXSerializer_Material.cpp index 7713b36e57..0a1d15b72a 100644 --- a/libraries/fbx/src/FBXSerializer_Material.cpp +++ b/libraries/fbx/src/FBXSerializer_Material.cpp @@ -27,7 +27,7 @@ #include -HFMTexture FBXSerializer::getTexture(const QString& textureID) { +HFMTexture FBXSerializer::getTexture(const QString& textureID, const QString& materialID) { HFMTexture texture; const QByteArray& filepath = _textureFilepaths.value(textureID); texture.content = _textureContent.value(filepath); @@ -45,8 +45,8 @@ HFMTexture FBXSerializer::getTexture(const QString& textureID) { if (_textureParams.contains(textureID)) { auto p = _textureParams.value(textureID); - texture.transform.setTranslation(p.translation); - texture.transform.setRotation(glm::quat(glm::radians(p.rotation))); + texture.transform.postTranslate(p.translation); + texture.transform.postRotate(glm::quat(glm::radians(p.rotation))); auto scaling = p.scaling; // Protect from bad scaling which should never happen @@ -59,13 +59,19 @@ HFMTexture FBXSerializer::getTexture(const QString& textureID) { if (scaling.z == 0.0f) { scaling.z = 1.0f; } - texture.transform.setScale(scaling); + texture.transform.postScale(scaling); if ((p.UVSet != "map1") && (p.UVSet != "UVSet0")) { texture.texcoordSet = 1; } texture.texcoordSetName = p.UVSet; } + auto materialParamItr = _materialParams.find(materialID); + if (materialParamItr != _materialParams.end()) { + auto& materialParam = materialParamItr.value(); + texture.transform.postTranslate(materialParam.translation); + texture.transform.postScale(materialParam.scaling); + } return texture; } @@ -102,12 +108,12 @@ void FBXSerializer::consolidateHFMMaterials(const QVariantHash& mapping) { material.diffuseFactor = 1.0; } - diffuseTexture = getTexture(diffuseTextureID); + diffuseTexture = getTexture(diffuseTextureID, material.materialID); // FBX files generated by 3DSMax have an intermediate texture parent, apparently foreach(const QString& childTextureID, _connectionChildMap.values(diffuseTextureID)) { if (_textureFilenames.contains(childTextureID)) { - diffuseTexture = getTexture(diffuseTextureID); + diffuseTexture = getTexture(diffuseTextureID, material.materialID); } } @@ -122,7 +128,7 @@ void FBXSerializer::consolidateHFMMaterials(const QVariantHash& mapping) { transparentTextureID = diffuseTextureID; } if (!transparentTextureID.isNull()) { - transparentTexture = getTexture(transparentTextureID); + transparentTexture = getTexture(transparentTextureID, material.materialID); material.opacityTexture = transparentTexture; detectDifferentUVs |= (transparentTexture.texcoordSet != 0) || (!transparentTexture.transform.isIdentity()); } @@ -131,13 +137,13 @@ void FBXSerializer::consolidateHFMMaterials(const QVariantHash& mapping) { QString bumpTextureID = bumpTextures.value(material.materialID); QString normalTextureID = normalTextures.value(material.materialID); if (!normalTextureID.isNull()) { - normalTexture = getTexture(normalTextureID); + normalTexture = getTexture(normalTextureID, material.materialID); normalTexture.isBumpmap = false; material.normalTexture = normalTexture; detectDifferentUVs |= (normalTexture.texcoordSet != 0) || (!normalTexture.transform.isIdentity()); } else if (!bumpTextureID.isNull()) { - normalTexture = getTexture(bumpTextureID); + normalTexture = getTexture(bumpTextureID, material.materialID); normalTexture.isBumpmap = true; material.normalTexture = normalTexture; @@ -147,7 +153,7 @@ void FBXSerializer::consolidateHFMMaterials(const QVariantHash& mapping) { HFMTexture specularTexture; QString specularTextureID = specularTextures.value(material.materialID); if (!specularTextureID.isNull()) { - specularTexture = getTexture(specularTextureID); + specularTexture = getTexture(specularTextureID, material.materialID); detectDifferentUVs |= (specularTexture.texcoordSet != 0) || (!specularTexture.transform.isIdentity()); material.specularTexture = specularTexture; } @@ -155,7 +161,7 @@ void FBXSerializer::consolidateHFMMaterials(const QVariantHash& mapping) { HFMTexture metallicTexture; QString metallicTextureID = metallicTextures.value(material.materialID); if (!metallicTextureID.isNull()) { - metallicTexture = getTexture(metallicTextureID); + metallicTexture = getTexture(metallicTextureID, material.materialID); detectDifferentUVs |= (metallicTexture.texcoordSet != 0) || (!metallicTexture.transform.isIdentity()); material.metallicTexture = metallicTexture; } @@ -163,7 +169,7 @@ void FBXSerializer::consolidateHFMMaterials(const QVariantHash& mapping) { HFMTexture roughnessTexture; QString roughnessTextureID = roughnessTextures.value(material.materialID); if (!roughnessTextureID.isNull()) { - roughnessTexture = getTexture(roughnessTextureID); + roughnessTexture = getTexture(roughnessTextureID, material.materialID); material.roughnessTexture = roughnessTexture; detectDifferentUVs |= (roughnessTexture.texcoordSet != 0) || (!roughnessTexture.transform.isIdentity()); } @@ -171,7 +177,7 @@ void FBXSerializer::consolidateHFMMaterials(const QVariantHash& mapping) { HFMTexture shininessTexture; QString shininessTextureID = shininessTextures.value(material.materialID); if (!shininessTextureID.isNull()) { - shininessTexture = getTexture(shininessTextureID); + shininessTexture = getTexture(shininessTextureID, material.materialID); material.glossTexture = shininessTexture; detectDifferentUVs |= (shininessTexture.texcoordSet != 0) || (!shininessTexture.transform.isIdentity()); } @@ -179,7 +185,7 @@ void FBXSerializer::consolidateHFMMaterials(const QVariantHash& mapping) { HFMTexture emissiveTexture; QString emissiveTextureID = emissiveTextures.value(material.materialID); if (!emissiveTextureID.isNull()) { - emissiveTexture = getTexture(emissiveTextureID); + emissiveTexture = getTexture(emissiveTextureID, material.materialID); detectDifferentUVs |= (emissiveTexture.texcoordSet != 0) || (!emissiveTexture.transform.isIdentity()); material.emissiveTexture = emissiveTexture; @@ -202,7 +208,7 @@ void FBXSerializer::consolidateHFMMaterials(const QVariantHash& mapping) { } if (!occlusionTextureID.isNull()) { - occlusionTexture = getTexture(occlusionTextureID); + occlusionTexture = getTexture(occlusionTextureID, material.materialID); detectDifferentUVs |= (occlusionTexture.texcoordSet != 0) || (!emissiveTexture.transform.isIdentity()); material.occlusionTexture = occlusionTexture; } @@ -222,7 +228,7 @@ void FBXSerializer::consolidateHFMMaterials(const QVariantHash& mapping) { } if (_loadLightmaps && !ambientTextureID.isNull()) { - ambientTexture = getTexture(ambientTextureID); + ambientTexture = getTexture(ambientTextureID, material.materialID); detectDifferentUVs |= (ambientTexture.texcoordSet != 0) || (!ambientTexture.transform.isIdentity()); material.lightmapTexture = ambientTexture; material.lightmapParams = lightmapParams; diff --git a/libraries/graphics-scripting/src/graphics-scripting/Forward.h b/libraries/graphics-scripting/src/graphics-scripting/Forward.h index 104674eddc..43b0daf407 100644 --- a/libraries/graphics-scripting/src/graphics-scripting/Forward.h +++ b/libraries/graphics-scripting/src/graphics-scripting/Forward.h @@ -40,13 +40,13 @@ namespace scriptable { * @typedef {object} Graphics.Material * @property {string} name * @property {string} model - * @property {number} opacity - * @property {number} roughness - * @property {number} metallic - * @property {number} scattering - * @property {boolean} unlit - * @propety {Vec3} emissive - * @propety {Vec3} albedo + * @property {number|string} opacity + * @property {number|string} roughness + * @property {number|string} metallic + * @property {number|string} scattering + * @property {boolean|string} unlit + * @propety {Vec3|string} emissive + * @propety {Vec3|string} albedo * @property {string} emissiveMap * @property {string} albedoMap * @property {string} opacityMap @@ -59,6 +59,11 @@ namespace scriptable { * @property {string} occlusionMap * @property {string} lightmapMap * @property {string} scatteringMap + * @property {string} texCoordTransform0 + * @property {string} texCoordTransform1 + * @property {string} lightmapParams + * @property {string} materialParams + * @property {boolean} defaultFallthrough */ class ScriptableMaterial { public: @@ -88,6 +93,9 @@ namespace scriptable { QString occlusionMap; QString lightmapMap; QString scatteringMap; + + bool defaultFallthrough; + std::unordered_map propertyFallthroughs; // not actually exposed to script }; /**jsdoc diff --git a/libraries/graphics-scripting/src/graphics-scripting/GraphicsScriptingInterface.cpp b/libraries/graphics-scripting/src/graphics-scripting/GraphicsScriptingInterface.cpp index 67f79db6b7..3293d294d8 100644 --- a/libraries/graphics-scripting/src/graphics-scripting/GraphicsScriptingInterface.cpp +++ b/libraries/graphics-scripting/src/graphics-scripting/GraphicsScriptingInterface.cpp @@ -362,25 +362,64 @@ namespace scriptable { QScriptValue obj = engine->newObject(); obj.setProperty("name", material.name); obj.setProperty("model", material.model); - obj.setProperty("opacity", material.opacity); - obj.setProperty("roughness", material.roughness); - obj.setProperty("metallic", material.metallic); - obj.setProperty("scattering", material.scattering); - obj.setProperty("unlit", material.unlit); - obj.setProperty("emissive", vec3ColorToScriptValue(engine, material.emissive)); - obj.setProperty("albedo", vec3ColorToScriptValue(engine, material.albedo)); - obj.setProperty("emissiveMap", material.emissiveMap); - obj.setProperty("albedoMap", material.albedoMap); + + const QScriptValue FALLTHROUGH("fallthrough"); + obj.setProperty("opacity", material.propertyFallthroughs.at(graphics::MaterialKey::OPACITY_VAL_BIT) ? FALLTHROUGH : material.opacity); + obj.setProperty("roughness", material.propertyFallthroughs.at(graphics::MaterialKey::GLOSSY_VAL_BIT) ? FALLTHROUGH : material.roughness); + obj.setProperty("metallic", material.propertyFallthroughs.at(graphics::MaterialKey::METALLIC_VAL_BIT) ? FALLTHROUGH : material.metallic); + obj.setProperty("scattering", material.propertyFallthroughs.at(graphics::MaterialKey::SCATTERING_VAL_BIT) ? FALLTHROUGH : material.scattering); + obj.setProperty("unlit", material.propertyFallthroughs.at(graphics::MaterialKey::UNLIT_VAL_BIT) ? FALLTHROUGH : material.unlit); + obj.setProperty("emissive", material.propertyFallthroughs.at(graphics::MaterialKey::EMISSIVE_VAL_BIT) ? FALLTHROUGH : vec3ColorToScriptValue(engine, material.emissive)); + obj.setProperty("albedo", material.propertyFallthroughs.at(graphics::MaterialKey::ALBEDO_VAL_BIT) ? FALLTHROUGH : vec3ColorToScriptValue(engine, material.albedo)); + + obj.setProperty("emissiveMap", material.propertyFallthroughs.at(graphics::MaterialKey::EMISSIVE_MAP_BIT) ? FALLTHROUGH : material.emissiveMap); + obj.setProperty("albedoMap", material.propertyFallthroughs.at(graphics::MaterialKey::ALBEDO_MAP_BIT) ? FALLTHROUGH : material.albedoMap); obj.setProperty("opacityMap", material.opacityMap); - obj.setProperty("metallicMap", material.metallicMap); - obj.setProperty("specularMap", material.specularMap); - obj.setProperty("roughnessMap", material.roughnessMap); - obj.setProperty("glossMap", material.glossMap); - obj.setProperty("normalMap", material.normalMap); - obj.setProperty("bumpMap", material.bumpMap); - obj.setProperty("occlusionMap", material.occlusionMap); - obj.setProperty("lightmapMap", material.lightmapMap); - obj.setProperty("scatteringMap", material.scatteringMap); + obj.setProperty("occlusionMap", material.propertyFallthroughs.at(graphics::MaterialKey::OCCLUSION_MAP_BIT) ? FALLTHROUGH : material.occlusionMap); + obj.setProperty("lightmapMap", material.propertyFallthroughs.at(graphics::MaterialKey::LIGHTMAP_MAP_BIT) ? FALLTHROUGH : material.lightmapMap); + obj.setProperty("scatteringMap", material.propertyFallthroughs.at(graphics::MaterialKey::SCATTERING_MAP_BIT) ? FALLTHROUGH : material.scatteringMap); + + // Only set one of each of these + if (material.propertyFallthroughs.at(graphics::MaterialKey::METALLIC_MAP_BIT)) { + obj.setProperty("metallicMap", FALLTHROUGH); + } else if (!material.metallicMap.isEmpty()) { + obj.setProperty("metallicMap", material.metallicMap); + } else if (!material.specularMap.isEmpty()) { + obj.setProperty("specularMap", material.specularMap); + } + + if (material.propertyFallthroughs.at(graphics::MaterialKey::ROUGHNESS_MAP_BIT)) { + obj.setProperty("roughnessMap", FALLTHROUGH); + } else if (!material.roughnessMap.isEmpty()) { + obj.setProperty("roughnessMap", material.roughnessMap); + } else if (!material.glossMap.isEmpty()) { + obj.setProperty("glossMap", material.glossMap); + } + + if (material.propertyFallthroughs.at(graphics::MaterialKey::NORMAL_MAP_BIT)) { + obj.setProperty("normalMap", FALLTHROUGH); + } else if (!material.normalMap.isEmpty()) { + obj.setProperty("normalMap", material.normalMap); + } else if (!material.bumpMap.isEmpty()) { + obj.setProperty("bumpMap", material.bumpMap); + } + + // These need to be implemented, but set the fallthrough for now + if (material.propertyFallthroughs.at(graphics::Material::TEXCOORDTRANSFORM0)) { + obj.setProperty("texCoordTransform0", FALLTHROUGH); + } + if (material.propertyFallthroughs.at(graphics::Material::TEXCOORDTRANSFORM1)) { + obj.setProperty("texCoordTransform1", FALLTHROUGH); + } + if (material.propertyFallthroughs.at(graphics::Material::LIGHTMAP_PARAMS)) { + obj.setProperty("lightmapParams", FALLTHROUGH); + } + if (material.propertyFallthroughs.at(graphics::Material::MATERIAL_PARAMS)) { + obj.setProperty("materialParams", FALLTHROUGH); + } + + obj.setProperty("defaultFallthrough", material.defaultFallthrough); + return obj; } diff --git a/libraries/graphics-scripting/src/graphics-scripting/ScriptableModel.cpp b/libraries/graphics-scripting/src/graphics-scripting/ScriptableModel.cpp index 51085561c3..4ff751782c 100644 --- a/libraries/graphics-scripting/src/graphics-scripting/ScriptableModel.cpp +++ b/libraries/graphics-scripting/src/graphics-scripting/ScriptableModel.cpp @@ -42,6 +42,9 @@ scriptable::ScriptableMaterial& scriptable::ScriptableMaterial::operator=(const lightmapMap = material.lightmapMap; scatteringMap = material.scatteringMap; + defaultFallthrough = material.defaultFallthrough; + propertyFallthroughs = material.propertyFallthroughs; + return *this; } @@ -54,7 +57,9 @@ scriptable::ScriptableMaterial::ScriptableMaterial(const graphics::MaterialPoint scattering(material->getScattering()), unlit(material->isUnlit()), emissive(material->getEmissive()), - albedo(material->getAlbedo()) + albedo(material->getAlbedo()), + defaultFallthrough(material->getDefaultFallthrough()), + propertyFallthroughs(material->getPropertyFallthroughs()) { auto map = material->getTextureMap(graphics::Material::MapChannel::EMISSIVE_MAP); if (map && map->getTextureSource()) { diff --git a/libraries/graphics/src/graphics/Material.cpp b/libraries/graphics/src/graphics/Material.cpp index be99cea681..7befb7e053 100755 --- a/libraries/graphics/src/graphics/Material.cpp +++ b/libraries/graphics/src/graphics/Material.cpp @@ -17,125 +17,125 @@ using namespace graphics; using namespace gpu; -Material::Material() : - _key(0), - _schemaBuffer(), - _textureMaps() -{ - // created from nothing: create the Buffer to store the properties - Schema schema; - _schemaBuffer = gpu::BufferView(std::make_shared(sizeof(Schema), (const gpu::Byte*) &schema, sizeof(Schema))); +const float Material::DEFAULT_EMISSIVE { 0.0f }; +const float Material::DEFAULT_OPACITY { 1.0f }; +const float Material::DEFAULT_ALBEDO { 0.5f }; +const float Material::DEFAULT_METALLIC { 0.0f }; +const float Material::DEFAULT_ROUGHNESS { 1.0f }; +const float Material::DEFAULT_SCATTERING { 0.0f }; + +Material::Material() { + for (int i = 0; i < NUM_TOTAL_FLAGS; i++) { + _propertyFallthroughs[i] = false; + } } Material::Material(const Material& material) : _name(material._name), + _model(material._model), _key(material._key), - _textureMaps(material._textureMaps) + _emissive(material._emissive), + _opacity(material._opacity), + _albedo(material._albedo), + _roughness(material._roughness), + _metallic(material._metallic), + _scattering(material._scattering), + _texcoordTransforms(material._texcoordTransforms), + _lightmapParams(material._lightmapParams), + _materialParams(material._materialParams), + _textureMaps(material._textureMaps), + _defaultFallthrough(material._defaultFallthrough), + _propertyFallthroughs(material._propertyFallthroughs) { - // copied: create the Buffer to store the properties, avoid holding a ref to the old Buffer - Schema schema; - _schemaBuffer = gpu::BufferView(std::make_shared(sizeof(Schema), (const gpu::Byte*) &schema, sizeof(Schema))); - _schemaBuffer.edit() = material._schemaBuffer.get(); } -Material& Material::operator= (const Material& material) { +Material& Material::operator=(const Material& material) { QMutexLocker locker(&_textureMapsMutex); _name = material._name; + _model = material._model; + _key = material._key; + _emissive = material._emissive; + _opacity = material._opacity; + _albedo = material._albedo; + _roughness = material._roughness; + _metallic = material._metallic; + _scattering = material._scattering; + _texcoordTransforms = material._texcoordTransforms; + _lightmapParams = material._lightmapParams; + _materialParams = material._materialParams; + _textureMaps = material._textureMaps; - _key = (material._key); - _textureMaps = (material._textureMaps); - _hasCalculatedTextureInfo = false; - - // copied: create the Buffer to store the properties, avoid holding a ref to the old Buffer - Schema schema; - _schemaBuffer = gpu::BufferView(std::make_shared(sizeof(Schema), (const gpu::Byte*) &schema, sizeof(Schema))); - _schemaBuffer.edit() = material._schemaBuffer.get(); + _defaultFallthrough = material._defaultFallthrough; + _propertyFallthroughs = material._propertyFallthroughs; return (*this); } -Material::~Material() { -} - -void Material::setEmissive(const Color& emissive, bool isSRGB) { - _key.setEmissive(glm::any(glm::greaterThan(emissive, Color(0.0f)))); - _schemaBuffer.edit()._key = (uint32) _key._flags.to_ulong(); - _schemaBuffer.edit()._emissive = (isSRGB ? ColorUtils::sRGBToLinearVec3(emissive) : emissive); +void Material::setEmissive(const glm::vec3& emissive, bool isSRGB) { + _key.setEmissive(glm::any(glm::greaterThan(emissive, glm::vec3(0.0f)))); + _emissive = (isSRGB ? ColorUtils::sRGBToLinearVec3(emissive) : emissive); } void Material::setOpacity(float opacity) { _key.setTranslucentFactor((opacity < 1.0f)); - _schemaBuffer.edit()._key = (uint32)_key._flags.to_ulong(); - _schemaBuffer.edit()._opacity = opacity; + _opacity = opacity; } void Material::setUnlit(bool value) { _key.setUnlit(value); - _schemaBuffer.edit()._key = (uint32)_key._flags.to_ulong(); } -void Material::setAlbedo(const Color& albedo, bool isSRGB) { - _key.setAlbedo(glm::any(glm::greaterThan(albedo, Color(0.0f)))); - _schemaBuffer.edit()._key = (uint32)_key._flags.to_ulong(); - _schemaBuffer.edit()._albedo = (isSRGB ? ColorUtils::sRGBToLinearVec3(albedo) : albedo); +void Material::setAlbedo(const glm::vec3& albedo, bool isSRGB) { + _key.setAlbedo(glm::any(glm::greaterThan(albedo, glm::vec3(0.0f)))); + _albedo = (isSRGB ? ColorUtils::sRGBToLinearVec3(albedo) : albedo); } void Material::setRoughness(float roughness) { roughness = std::min(1.0f, std::max(roughness, 0.0f)); - _key.setGlossy((roughness < 1.0f)); - _schemaBuffer.edit()._key = (uint32)_key._flags.to_ulong(); - _schemaBuffer.edit()._roughness = roughness; + _key.setGlossy(roughness < 1.0f); + _roughness = roughness; } void Material::setMetallic(float metallic) { metallic = glm::clamp(metallic, 0.0f, 1.0f); _key.setMetallic(metallic > 0.0f); - _schemaBuffer.edit()._key = (uint32)_key._flags.to_ulong(); - _schemaBuffer.edit()._metallic = metallic; + _metallic = metallic; } void Material::setScattering(float scattering) { scattering = glm::clamp(scattering, 0.0f, 1.0f); _key.setMetallic(scattering > 0.0f); - _schemaBuffer.edit()._key = (uint32)_key._flags.to_ulong(); - _schemaBuffer.edit()._scattering = scattering; + _scattering = scattering; } void Material::setTextureMap(MapChannel channel, const TextureMapPointer& textureMap) { QMutexLocker locker(&_textureMapsMutex); if (textureMap) { - _key.setMapChannel(channel, (true)); + _key.setMapChannel(channel, true); _textureMaps[channel] = textureMap; } else { - _key.setMapChannel(channel, (false)); + _key.setMapChannel(channel, false); _textureMaps.erase(channel); } - _hasCalculatedTextureInfo = false; - - _schemaBuffer.edit()._key = (uint32)_key._flags.to_ulong(); if (channel == MaterialKey::ALBEDO_MAP) { resetOpacityMap(); - - // update the texcoord0 with albedo - _schemaBuffer.edit()._texcoordTransforms[0] = (textureMap ? textureMap->getTextureTransform().getMatrix() : glm::mat4()); + _texcoordTransforms[0] = (textureMap ? textureMap->getTextureTransform().getMatrix() : glm::mat4()); } if (channel == MaterialKey::OCCLUSION_MAP) { - _schemaBuffer.edit()._texcoordTransforms[1] = (textureMap ? textureMap->getTextureTransform().getMatrix() : glm::mat4()); + _texcoordTransforms[1] = (textureMap ? textureMap->getTextureTransform().getMatrix() : glm::mat4()); } if (channel == MaterialKey::LIGHTMAP_MAP) { // update the texcoord1 with lightmap - _schemaBuffer.edit()._texcoordTransforms[1] = (textureMap ? textureMap->getTextureTransform().getMatrix() : glm::mat4()); - _schemaBuffer.edit()._lightmapParams = (textureMap ? glm::vec2(textureMap->getLightmapOffsetScale()) : glm::vec2(0.0, 1.0)); + _texcoordTransforms[1] = (textureMap ? textureMap->getTextureTransform().getMatrix() : glm::mat4()); + _lightmapParams = (textureMap ? glm::vec2(textureMap->getLightmapOffsetScale()) : glm::vec2(0.0, 1.0)); } - _schemaBuffer.edit()._materialParams = (textureMap ? glm::vec2(textureMap->getMappingMode(), textureMap->getRepeat()) : glm::vec2(MaterialMappingMode::UV, 1.0)); - - _schemaBuffer.edit()._key = (uint32)_key._flags.to_ulong(); + _materialParams = (textureMap ? glm::vec2(textureMap->getMappingMode(), textureMap->getRepeat()) : glm::vec2(MaterialMappingMode::UV, 1.0)); } @@ -163,11 +163,8 @@ void Material::resetOpacityMap() const { } } } - - _schemaBuffer.edit()._key = (uint32)_key._flags.to_ulong(); } - const TextureMapPointer Material::getTextureMap(MapChannel channel) const { QMutexLocker locker(&_textureMapsMutex); @@ -179,40 +176,6 @@ const TextureMapPointer Material::getTextureMap(MapChannel channel) const { } } - -bool Material::calculateMaterialInfo() const { - if (!_hasCalculatedTextureInfo) { - QMutexLocker locker(&_textureMapsMutex); - - bool allTextures = true; // assume we got this... - _textureSize = 0; - _textureCount = 0; - - for (auto const &textureMapItem : _textureMaps) { - auto textureMap = textureMapItem.second; - if (textureMap) { - auto textureSoure = textureMap->getTextureSource(); - if (textureSoure) { - auto texture = textureSoure->getGPUTexture(); - if (texture) { - auto size = texture->getSize(); - _textureSize += size; - _textureCount++; - } else { - allTextures = false; - } - } else { - allTextures = false; - } - } else { - allTextures = false; - } - } - _hasCalculatedTextureInfo = allTextures; - } - return _hasCalculatedTextureInfo; -} - void Material::setTextureTransforms(const Transform& transform, MaterialMappingMode mode, bool repeat) { for (auto &textureMapItem : _textureMaps) { if (textureMapItem.second) { @@ -222,7 +185,32 @@ void Material::setTextureTransforms(const Transform& transform, MaterialMappingM } } for (int i = 0; i < NUM_TEXCOORD_TRANSFORMS; i++) { - _schemaBuffer.edit()._texcoordTransforms[i] = transform.getMatrix(); + _texcoordTransforms[i] = transform.getMatrix(); } - _schemaBuffer.edit()._materialParams = glm::vec2(mode, repeat); + _materialParams = glm::vec2(mode, repeat); } + +MultiMaterial::MultiMaterial() { + Schema schema; + _schemaBuffer = gpu::BufferView(std::make_shared(sizeof(Schema), (const gpu::Byte*) &schema, sizeof(Schema))); +} + +void MultiMaterial::calculateMaterialInfo() const { + if (!_hasCalculatedTextureInfo) { + bool allTextures = true; // assume we got this... + _textureSize = 0; + _textureCount = 0; + + auto textures = _textureTable->getTextures(); + for (auto const &texture : textures) { + if (texture && texture->isDefined()) { + auto size = texture->getSize(); + _textureSize += size; + _textureCount++; + } else { + allTextures = false; + } + } + _hasCalculatedTextureInfo = allTextures; + } +} \ No newline at end of file diff --git a/libraries/graphics/src/graphics/Material.h b/libraries/graphics/src/graphics/Material.h index 9711bd9000..fdddf3640a 100755 --- a/libraries/graphics/src/graphics/Material.h +++ b/libraries/graphics/src/graphics/Material.h @@ -15,6 +15,7 @@ #include #include +#include #include #include @@ -176,7 +177,6 @@ public: bool isTexelOpaque() const { return isOpaque() && isOpacityMaskMap(); } }; - class MaterialFilter { public: MaterialKey::Flags _value{ 0 }; @@ -266,84 +266,44 @@ public: class Material { public: - typedef gpu::BufferView UniformBufferView; - - typedef glm::vec3 Color; - - // Texture Map Array Schema - static const int NUM_TEXCOORD_TRANSFORMS{ 2 }; - typedef MaterialKey::MapChannel MapChannel; typedef std::map TextureMaps; - typedef std::bitset MapFlags; Material(); Material(const Material& material); Material& operator= (const Material& material); - virtual ~Material(); const MaterialKey& getKey() const { return _key; } - void setEmissive(const Color& emissive, bool isSRGB = true); - Color getEmissive(bool SRGB = true) const { return (SRGB ? ColorUtils::tosRGBVec3(_schemaBuffer.get()._emissive) : _schemaBuffer.get()._emissive); } + static const float DEFAULT_EMISSIVE; + void setEmissive(const glm::vec3& emissive, bool isSRGB = true); + glm::vec3 getEmissive(bool SRGB = true) const { return (SRGB ? ColorUtils::tosRGBVec3(_emissive) : _emissive); } + static const float DEFAULT_OPACITY; void setOpacity(float opacity); - float getOpacity() const { return _schemaBuffer.get()._opacity; } + float getOpacity() const { return _opacity; } void setUnlit(bool value); bool isUnlit() const { return _key.isUnlit(); } - void setAlbedo(const Color& albedo, bool isSRGB = true); - Color getAlbedo(bool SRGB = true) const { return (SRGB ? ColorUtils::tosRGBVec3(_schemaBuffer.get()._albedo) : _schemaBuffer.get()._albedo); } + static const float DEFAULT_ALBEDO; + void setAlbedo(const glm::vec3& albedo, bool isSRGB = true); + glm::vec3 getAlbedo(bool SRGB = true) const { return (SRGB ? ColorUtils::tosRGBVec3(_albedo) : _albedo); } + static const float DEFAULT_METALLIC; void setMetallic(float metallic); - float getMetallic() const { return _schemaBuffer.get()._metallic; } + float getMetallic() const { return _metallic; } + static const float DEFAULT_ROUGHNESS; void setRoughness(float roughness); - float getRoughness() const { return _schemaBuffer.get()._roughness; } + float getRoughness() const { return _roughness; } + static const float DEFAULT_SCATTERING; void setScattering(float scattering); - float getScattering() const { return _schemaBuffer.get()._scattering; } - - // Schema to access the attribute values of the material - class Schema { - public: - glm::vec3 _emissive { 0.0f }; // No Emissive - float _opacity { 1.0f }; // Opacity = 1 => Not Transparent - - glm::vec3 _albedo { 0.5f }; // Grey albedo => isAlbedo - float _roughness { 1.0f }; // Roughness = 1 => Not Glossy - - float _metallic { 0.0f }; // Not Metallic - float _scattering { 0.0f }; // Scattering info -#if defined(__clang__) - __attribute__((unused)) -#endif - glm::vec2 _spare { 0.0f }; // Padding - - uint32_t _key { 0 }; // a copy of the materialKey -#if defined(__clang__) - __attribute__((unused)) -#endif - glm::vec3 _spare2 { 0.0f }; - - // for alignment beauty, Material size == Mat4x4 - - // Texture Coord Transform Array - glm::mat4 _texcoordTransforms[NUM_TEXCOORD_TRANSFORMS]; - - glm::vec2 _lightmapParams { 0.0, 1.0 }; - - // x: material mode (0 for UV, 1 for PROJECTED) - // y: 1 for texture repeat, 0 for discard outside of 0 - 1 - glm::vec2 _materialParams { 0.0, 1.0 }; - - Schema() {} - }; - - const UniformBufferView& getSchemaBuffer() const { return _schemaBuffer; } + float getScattering() const { return _scattering; } // The texture map to channel association + static const int NUM_TEXCOORD_TRANSFORMS { 2 }; void setTextureMap(MapChannel channel, const TextureMapPointer& textureMap); const TextureMaps& getTextureMaps() const { return _textureMaps; } // FIXME - not thread safe... const TextureMapPointer getTextureMap(MapChannel channel) const; @@ -355,10 +315,6 @@ public: // conversion from legacy material properties to PBR equivalent static float shininessToRoughness(float shininess) { return 1.0f - shininess / 100.0f; } - int getTextureCount() const { calculateMaterialInfo(); return _textureCount; } - size_t getTextureSize() const { calculateMaterialInfo(); return _textureSize; } - bool hasTextureInfo() const { return _hasCalculatedTextureInfo; } - void setTextureTransforms(const Transform& transform, MaterialMappingMode mode, bool repeat); const std::string& getName() const { return _name; } @@ -366,28 +322,50 @@ public: const std::string& getModel() const { return _model; } void setModel(const std::string& model) { _model = model; } - const gpu::TextureTablePointer& getTextureTable() const { return _textureTable; } + glm::mat4 getTexCoordTransform(uint i) const { return _texcoordTransforms[i]; } + glm::vec2 getLightmapParams() const { return _lightmapParams; } + glm::vec2 getMaterialParams() const { return _materialParams; } + + bool getDefaultFallthrough() const { return _defaultFallthrough; } + void setDefaultFallthrough(bool defaultFallthrough) { _defaultFallthrough = defaultFallthrough; } + + enum ExtraFlagBit { + TEXCOORDTRANSFORM0 = MaterialKey::NUM_FLAGS, + TEXCOORDTRANSFORM1, + LIGHTMAP_PARAMS, + MATERIAL_PARAMS, + + NUM_TOTAL_FLAGS + }; + std::unordered_map getPropertyFallthroughs() { return _propertyFallthroughs; } + bool getPropertyFallthrough(uint property) { return _propertyFallthroughs[property]; } + void setPropertyDoesFallthrough(uint property) { _propertyFallthroughs[property] = true; } protected: std::string _name { "" }; private: - mutable MaterialKey _key; - mutable UniformBufferView _schemaBuffer; - mutable gpu::TextureTablePointer _textureTable{ std::make_shared() }; + std::string _model { "hifi_pbr" }; + mutable MaterialKey _key { 0 }; + // Material properties + glm::vec3 _emissive { DEFAULT_EMISSIVE }; + float _opacity { DEFAULT_OPACITY }; + glm::vec3 _albedo { DEFAULT_ALBEDO }; + float _roughness { DEFAULT_ROUGHNESS }; + float _metallic { DEFAULT_METALLIC }; + float _scattering { DEFAULT_SCATTERING }; + std::array _texcoordTransforms; + glm::vec2 _lightmapParams { 0.0, 1.0 }; + glm::vec2 _materialParams { 0.0, 1.0 }; TextureMaps _textureMaps; + bool _defaultFallthrough { false }; + std::unordered_map _propertyFallthroughs { NUM_TOTAL_FLAGS }; + mutable QMutex _textureMapsMutex { QMutex::Recursive }; - mutable size_t _textureSize { 0 }; - mutable int _textureCount { 0 }; - mutable bool _hasCalculatedTextureInfo { false }; - bool calculateMaterialInfo() const; - - std::string _model { "hifi_pbr" }; - }; -typedef std::shared_ptr< Material > MaterialPointer; +typedef std::shared_ptr MaterialPointer; class MaterialLayer { public: @@ -403,9 +381,18 @@ public: return left.priority < right.priority; } }; +typedef std::priority_queue, MaterialLayerCompare> MaterialLayerQueue; -class MultiMaterial : public std::priority_queue, MaterialLayerCompare> { +class MultiMaterial : public MaterialLayerQueue { public: + MultiMaterial(); + + void push(const MaterialLayer& value) { + MaterialLayerQueue::push(value); + _hasCalculatedTextureInfo = false; + _needsUpdate = true; + } + bool remove(const MaterialPointer& value) { auto it = c.begin(); while (it != c.end()) { @@ -417,11 +404,78 @@ public: if (it != c.end()) { c.erase(it); std::make_heap(c.begin(), c.end(), comp); + _hasCalculatedTextureInfo = false; + _needsUpdate = true; return true; } else { return false; } } + + // Schema to access the attribute values of the material + class Schema { + public: + glm::vec3 _emissive { Material::DEFAULT_EMISSIVE }; // No Emissive + float _opacity { Material::DEFAULT_OPACITY }; // Opacity = 1 => Not Transparent + + glm::vec3 _albedo { Material::DEFAULT_ALBEDO }; // Grey albedo => isAlbedo + float _roughness { Material::DEFAULT_ROUGHNESS }; // Roughness = 1 => Not Glossy + + float _metallic { Material::DEFAULT_METALLIC }; // Not Metallic + float _scattering { Material::DEFAULT_SCATTERING }; // Scattering info +#if defined(__clang__) + __attribute__((unused)) +#endif + glm::vec2 _spare { 0.0f }; // Padding + + uint32_t _key { 0 }; // a copy of the materialKey +#if defined(__clang__) + __attribute__((unused)) +#endif + glm::vec3 _spare2 { 0.0f }; + + // for alignment beauty, Material size == Mat4x4 + + // Texture Coord Transform Array + glm::mat4 _texcoordTransforms[Material::NUM_TEXCOORD_TRANSFORMS]; + + glm::vec2 _lightmapParams { 0.0, 1.0 }; + + // x: material mode (0 for UV, 1 for PROJECTED) + // y: 1 for texture repeat, 0 for discard outside of 0 - 1 + glm::vec2 _materialParams { 0.0, 1.0 }; + + Schema() { + for (auto& transform : _texcoordTransforms) { + transform = glm::mat4(); + } + } + }; + + gpu::BufferView& getSchemaBuffer() { return _schemaBuffer; } + graphics::MaterialKey getMaterialKey() const { return graphics::MaterialKey(_schemaBuffer.get()._key); } + const gpu::TextureTablePointer& getTextureTable() const { return _textureTable; } + + bool needsUpdate() const { return _needsUpdate; } + void setNeedsUpdate(bool needsUpdate) { _needsUpdate = needsUpdate; } + + void setTexturesLoading(bool value) { _texturesLoading = value; } + bool areTexturesLoading() const { return _texturesLoading; } + + int getTextureCount() const { calculateMaterialInfo(); return _textureCount; } + size_t getTextureSize() const { calculateMaterialInfo(); return _textureSize; } + bool hasTextureInfo() const { return _hasCalculatedTextureInfo; } + +private: + gpu::BufferView _schemaBuffer; + gpu::TextureTablePointer _textureTable { std::make_shared() }; + bool _needsUpdate { false }; + bool _texturesLoading { false }; + + mutable size_t _textureSize { 0 }; + mutable int _textureCount { 0 }; + mutable bool _hasCalculatedTextureInfo { false }; + void calculateMaterialInfo() const; }; }; diff --git a/libraries/hfm/src/hfm/HFM.cpp b/libraries/hfm/src/hfm/HFM.cpp index b9e630456d..e247984bbf 100644 --- a/libraries/hfm/src/hfm/HFM.cpp +++ b/libraries/hfm/src/hfm/HFM.cpp @@ -205,6 +205,8 @@ bool HFMModel::convexHullContains(const glm::vec3& point) const { auto checkEachPrimitive = [=](HFMMesh& mesh, QVector indices, int primitiveSize) -> bool { // Check whether the point is "behind" all the primitives. + // But first must transform from model-frame into mesh-frame + glm::vec3 transformedPoint = glm::vec3(glm::inverse(mesh.modelTransform) * glm::vec4(point, 1.0f)); int verticesSize = mesh.vertices.size(); for (int j = 0; j < indices.size() - 2; // -2 in case the vertices aren't the right size -- we access j + 2 below @@ -212,7 +214,7 @@ bool HFMModel::convexHullContains(const glm::vec3& point) const { if (indices[j] < verticesSize && indices[j + 1] < verticesSize && indices[j + 2] < verticesSize && - !isPointBehindTrianglesPlane(point, + !isPointBehindTrianglesPlane(transformedPoint, mesh.vertices[indices[j]], mesh.vertices[indices[j + 1]], mesh.vertices[indices[j + 2]])) { diff --git a/libraries/model-networking/src/model-networking/MaterialCache.cpp b/libraries/model-networking/src/model-networking/MaterialCache.cpp index e6e3b0e812..b6550a5e9e 100644 --- a/libraries/model-networking/src/model-networking/MaterialCache.cpp +++ b/libraries/model-networking/src/model-networking/MaterialCache.cpp @@ -111,146 +111,300 @@ NetworkMaterialResource::ParsedMaterials NetworkMaterialResource::parseJSONMater /**jsdoc * A material such as may be used by a {@link Entities.EntityType|Material} entity. * @typedef {object} Material - * @property {string} name="" - A name for the material. - * @property {string} model="hifi_pbr" - Currently not used. - * @property {Color|RGBS} emissive - The emissive color, i.e., the color that the material emits. A {@link Color} value - * is treated as sRGB. A {@link RGBS} value can be either RGB or sRGB. - * @property {number} opacity=1.0 - The opacity, 0.01.0. - * @property {boolean} unlit=false - If true, the material is not lit. - * @property {Color|RGBS} albedo - The albedo color. A {@link Color} value is treated as sRGB. A {@link RGBS} value can - * be either RGB or sRGB. - * @property {number} roughness - The roughness, 0.01.0. - * @property {number} metallic - The metallicness, 0.01.0. - * @property {number} scattering - The scattering, 0.01.0. - * @property {string} emissiveMap - URL of emissive texture image. - * @property {string} albedoMap - URL of albedo texture image. + * @property {string} model="hifi_pbr" - Different material models support different properties and rendering modes. + * Supported models are: "hifi_pbr" + * @property {string} name="" - A name for the material. Supported by all material models. + * @property {Color|RGBS|string} emissive - The emissive color, i.e., the color that the material emits. A {@link Color} value + * is treated as sRGB. A {@link RGBS} value can be either RGB or sRGB. Set to "fallthrough" to fallthrough to + * the material below. "hifi_pbr" model only. + * @property {number|string} opacity=1.0 - The opacity, 0.01.0. Set to "fallthrough" to fallthrough to + * the material below. "hifi_pbr" model only. + * @property {boolean|string} unlit=false - If true, the material is not lit. Set to "fallthrough" to fallthrough to + * the material below. "hifi_pbr" model only. + * @property {Color|RGBS|string} albedo - The albedo color. A {@link Color} value is treated as sRGB. A {@link RGBS} value can + * be either RGB or sRGB. Set to "fallthrough" to fallthrough to the material below. Set to "fallthrough" to fallthrough to + * the material below. "hifi_pbr" model only. + * @property {number|string} roughness - The roughness, 0.01.0. Set to "fallthrough" to fallthrough to + * the material below. "hifi_pbr" model only. + * @property {number|string} metallic - The metallicness, 0.01.0. Set to "fallthrough" to fallthrough to + * the material below. "hifi_pbr" model only. + * @property {number|string} scattering - The scattering, 0.01.0. Set to "fallthrough" to fallthrough to + * the material below. "hifi_pbr" model only. + * @property {string} emissiveMap - URL of emissive texture image. Set to "fallthrough" to fallthrough to + * the material below. "hifi_pbr" model only. + * @property {string} albedoMap - URL of albedo texture image. Set to "fallthrough" to fallthrough to + * the material below. "hifi_pbr" model only. * @property {string} opacityMap - URL of opacity texture image. Set value the same as the albedoMap value for - * transparency. - * @property {string} roughnessMap - URL of roughness texture image. Can use this or glossMap, but not both. - * @property {string} glossMap - URL of gloss texture image. Can use this or roughnessMap, but not both. - * @property {string} metallicMap - URL of metallic texture image. Can use this or specularMap, but not both. - * @property {string} specularMap - URL of specular texture image. Can use this or metallicMap, but not both. - * @property {string} normalMap - URL of normal texture image. Can use this or bumpMap, but not both. - * @property {string} bumpMap - URL of bump texture image. Can use this or normalMap, but not both. - * @property {string} occlusionMap - URL of occlusion texture image. + * transparency. "hifi_pbr" model only. + * @property {string} roughnessMap - URL of roughness texture image. Can use this or glossMap, but not both. Set to "fallthrough" + * to fallthrough to the material below. "hifi_pbr" model only. + * @property {string} glossMap - URL of gloss texture image. Can use this or roughnessMap, but not both. Set to "fallthrough" + * to fallthrough to the material below. "hifi_pbr" model only. + * @property {string} metallicMap - URL of metallic texture image. Can use this or specularMap, but not both. Set to "fallthrough" + * to fallthrough to the material below. "hifi_pbr" model only. + * @property {string} specularMap - URL of specular texture image. Can use this or metallicMap, but not both. Set to "fallthrough" + * to fallthrough to the material below. "hifi_pbr" model only. + * @property {string} normalMap - URL of normal texture image. Can use this or bumpMap, but not both. Set to "fallthrough" + * to fallthrough to the material below. "hifi_pbr" model only. + * @property {string} bumpMap - URL of bump texture image. Can use this or normalMap, but not both. Set to "fallthrough" + * to fallthrough to the material below. "hifi_pbr" model only. + * @property {string} occlusionMap - URL of occlusion texture image. Set to "fallthrough" to fallthrough to the material below. "hifi_pbr" model only. * @property {string} scatteringMap - URL of scattering texture image. Only used if normalMap or - * bumpMap is specified. - * @property {string} lightMap - URL of light map texture image. Currently not used. + * bumpMap is specified. Set to "fallthrough" to fallthrough to the material below. "hifi_pbr" model only. + * @property {string} lightMap - URL of light map texture image. Currently not used.. Set to "fallthrough" + * to fallthrough to the material below. "hifi_pbr" model only. + * @property {string} texCoordTransform0 - The transform to use for all of the maps besides occlusionMap and lightMap. Currently unused. Set to + * "fallthrough" to fallthrough to the material below. "hifi_pbr" model only. + * @property {string} texCoordTransform1 - The transform to use for occlusionMap and lightMap. Currently unused. Set to "fallthrough" + * to fallthrough to the material below. "hifi_pbr" model only. + * @property {string} lightmapParams - Parameters for controlling how lightMap is used. Currently unused. Set to "fallthrough" + * to fallthrough to the material below. "hifi_pbr" model only. + * @property {string} materialParams - Parameters for controlling the material projection and repition. Currently unused. Set to "fallthrough" + * to fallthrough to the material below. "hifi_pbr" model only. + * @property {bool} defaultFallthrough=false - If true, all properties will fallthrough to the material below unless they are set. If + * false, they will respect the individual properties' fallthrough state. "hifi_pbr" model only. */ // Note: See MaterialEntityItem.h for default values used in practice. std::pair> NetworkMaterialResource::parseJSONMaterial(const QJsonObject& materialJSON, const QUrl& baseUrl) { std::string name = ""; std::shared_ptr material = std::make_shared(); - for (auto& key : materialJSON.keys()) { - if (key == "name") { - auto nameJSON = materialJSON.value(key); - if (nameJSON.isString()) { - name = nameJSON.toString().toStdString(); - } - } else if (key == "model") { - auto modelJSON = materialJSON.value(key); - if (modelJSON.isString()) { - material->setModel(modelJSON.toString().toStdString()); - } - } else if (key == "emissive") { - glm::vec3 color; - bool isSRGB; - bool valid = parseJSONColor(materialJSON.value(key), color, isSRGB); - if (valid) { - material->setEmissive(color, isSRGB); - } - } else if (key == "opacity") { - auto value = materialJSON.value(key); - if (value.isDouble()) { - material->setOpacity(value.toDouble()); - } - } else if (key == "unlit") { - auto value = materialJSON.value(key); - if (value.isBool()) { - material->setUnlit(value.toBool()); - } - } else if (key == "albedo") { - glm::vec3 color; - bool isSRGB; - bool valid = parseJSONColor(materialJSON.value(key), color, isSRGB); - if (valid) { - material->setAlbedo(color, isSRGB); - } - } else if (key == "roughness") { - auto value = materialJSON.value(key); - if (value.isDouble()) { - material->setRoughness(value.toDouble()); - } - } else if (key == "metallic") { - auto value = materialJSON.value(key); - if (value.isDouble()) { - material->setMetallic(value.toDouble()); - } - } else if (key == "scattering") { - auto value = materialJSON.value(key); - if (value.isDouble()) { - material->setScattering(value.toDouble()); - } - } else if (key == "emissiveMap") { - auto value = materialJSON.value(key); - if (value.isString()) { - material->setEmissiveMap(baseUrl.resolved(value.toString())); - } - } else if (key == "albedoMap") { - auto value = materialJSON.value(key); - if (value.isString()) { - QString urlString = value.toString(); - bool useAlphaChannel = false; - auto opacityMap = materialJSON.find("opacityMap"); - if (opacityMap != materialJSON.end() && opacityMap->isString() && opacityMap->toString() == urlString) { - useAlphaChannel = true; + + const std::string HIFI_PBR = "hifi_pbr"; + std::string modelString = HIFI_PBR; + auto modelJSONIter = materialJSON.find("model"); + if (modelJSONIter != materialJSON.end() && modelJSONIter.value().isString()) { + modelString = modelJSONIter.value().toString().toStdString(); + material->setModel(modelString); + } + + if (modelString == HIFI_PBR) { + const QString FALLTHROUGH("fallthrough"); + for (auto& key : materialJSON.keys()) { + if (key == "name") { + auto nameJSON = materialJSON.value(key); + if (nameJSON.isString()) { + name = nameJSON.toString().toStdString(); + } + } else if (key == "model") { + auto modelJSON = materialJSON.value(key); + if (modelJSON.isString()) { + material->setModel(modelJSON.toString().toStdString()); + } + } else if (key == "emissive") { + auto value = materialJSON.value(key); + if (value.isString() && value.toString() == FALLTHROUGH) { + material->setPropertyDoesFallthrough(graphics::MaterialKey::FlagBit::EMISSIVE_VAL_BIT); + } else { + glm::vec3 color; + bool isSRGB; + bool valid = parseJSONColor(value, color, isSRGB); + if (valid) { + material->setEmissive(color, isSRGB); + } + } + } else if (key == "opacity") { + auto value = materialJSON.value(key); + if (value.isString() && value.toString() == FALLTHROUGH) { + material->setPropertyDoesFallthrough(graphics::MaterialKey::FlagBit::OPACITY_VAL_BIT); + } else if (value.isDouble()) { + material->setOpacity(value.toDouble()); + } + } else if (key == "unlit") { + auto value = materialJSON.value(key); + if (value.isString() && value.toString() == FALLTHROUGH) { + material->setPropertyDoesFallthrough(graphics::MaterialKey::FlagBit::UNLIT_VAL_BIT); + } else if (value.isBool()) { + material->setUnlit(value.toBool()); + } + } else if (key == "albedo") { + auto value = materialJSON.value(key); + if (value.isString() && value.toString() == FALLTHROUGH) { + material->setPropertyDoesFallthrough(graphics::MaterialKey::FlagBit::ALBEDO_VAL_BIT); + } else { + glm::vec3 color; + bool isSRGB; + bool valid = parseJSONColor(value, color, isSRGB); + if (valid) { + material->setAlbedo(color, isSRGB); + } + } + } else if (key == "roughness") { + auto value = materialJSON.value(key); + if (value.isString() && value.toString() == FALLTHROUGH) { + material->setPropertyDoesFallthrough(graphics::MaterialKey::FlagBit::GLOSSY_VAL_BIT); + } else if (value.isDouble()) { + material->setRoughness(value.toDouble()); + } + } else if (key == "metallic") { + auto value = materialJSON.value(key); + if (value.isString() && value.toString() == FALLTHROUGH) { + material->setPropertyDoesFallthrough(graphics::MaterialKey::FlagBit::METALLIC_VAL_BIT); + } else if (value.isDouble()) { + material->setMetallic(value.toDouble()); + } + } else if (key == "scattering") { + auto value = materialJSON.value(key); + if (value.isString() && value.toString() == FALLTHROUGH) { + material->setPropertyDoesFallthrough(graphics::MaterialKey::FlagBit::SCATTERING_VAL_BIT); + } else if (value.isDouble()) { + material->setScattering(value.toDouble()); + } + } else if (key == "emissiveMap") { + auto value = materialJSON.value(key); + if (value.isString()) { + auto valueString = value.toString(); + if (valueString == FALLTHROUGH) { + material->setPropertyDoesFallthrough(graphics::MaterialKey::FlagBit::EMISSIVE_MAP_BIT); + } else { + material->setEmissiveMap(baseUrl.resolved(valueString)); + } + } + } else if (key == "albedoMap") { + auto value = materialJSON.value(key); + if (value.isString()) { + QString valueString = value.toString(); + if (valueString == FALLTHROUGH) { + material->setPropertyDoesFallthrough(graphics::MaterialKey::FlagBit::ALBEDO_MAP_BIT); + } else { + bool useAlphaChannel = false; + auto opacityMap = materialJSON.find("opacityMap"); + if (opacityMap != materialJSON.end() && opacityMap->isString() && opacityMap->toString() == valueString) { + useAlphaChannel = true; + } + material->setAlbedoMap(baseUrl.resolved(valueString), useAlphaChannel); + } + } + } else if (key == "roughnessMap") { + auto value = materialJSON.value(key); + if (value.isString()) { + auto valueString = value.toString(); + if (valueString == FALLTHROUGH) { + material->setPropertyDoesFallthrough(graphics::MaterialKey::FlagBit::ROUGHNESS_MAP_BIT); + } else { + material->setRoughnessMap(baseUrl.resolved(valueString), false); + } + } + } else if (key == "glossMap") { + auto value = materialJSON.value(key); + if (value.isString()) { + auto valueString = value.toString(); + if (valueString == FALLTHROUGH) { + material->setPropertyDoesFallthrough(graphics::MaterialKey::FlagBit::ROUGHNESS_MAP_BIT); + } else { + material->setRoughnessMap(baseUrl.resolved(valueString), true); + } + } + } else if (key == "metallicMap") { + auto value = materialJSON.value(key); + if (value.isString()) { + auto valueString = value.toString(); + if (valueString == FALLTHROUGH) { + material->setPropertyDoesFallthrough(graphics::MaterialKey::FlagBit::METALLIC_MAP_BIT); + } else { + material->setMetallicMap(baseUrl.resolved(valueString), false); + } + } + } else if (key == "specularMap") { + auto value = materialJSON.value(key); + if (value.isString()) { + auto valueString = value.toString(); + if (valueString == FALLTHROUGH) { + material->setPropertyDoesFallthrough(graphics::MaterialKey::FlagBit::METALLIC_MAP_BIT); + } else { + material->setMetallicMap(baseUrl.resolved(valueString), true); + } + } + } else if (key == "normalMap") { + auto value = materialJSON.value(key); + if (value.isString()) { + auto valueString = value.toString(); + if (valueString == FALLTHROUGH) { + material->setPropertyDoesFallthrough(graphics::MaterialKey::FlagBit::NORMAL_MAP_BIT); + } else { + material->setNormalMap(baseUrl.resolved(valueString), false); + } + } + } else if (key == "bumpMap") { + auto value = materialJSON.value(key); + if (value.isString()) { + auto valueString = value.toString(); + if (valueString == FALLTHROUGH) { + material->setPropertyDoesFallthrough(graphics::MaterialKey::FlagBit::NORMAL_MAP_BIT); + } else { + material->setNormalMap(baseUrl.resolved(valueString), true); + } + } + } else if (key == "occlusionMap") { + auto value = materialJSON.value(key); + if (value.isString()) { + auto valueString = value.toString(); + if (valueString == FALLTHROUGH) { + material->setPropertyDoesFallthrough(graphics::MaterialKey::FlagBit::OCCLUSION_MAP_BIT); + } else { + material->setOcclusionMap(baseUrl.resolved(valueString)); + } + } + } else if (key == "scatteringMap") { + auto value = materialJSON.value(key); + if (value.isString()) { + auto valueString = value.toString(); + if (valueString == FALLTHROUGH) { + material->setPropertyDoesFallthrough(graphics::MaterialKey::FlagBit::SCATTERING_MAP_BIT); + } else { + material->setScatteringMap(baseUrl.resolved(valueString)); + } + } + } else if (key == "lightMap") { + auto value = materialJSON.value(key); + if (value.isString()) { + auto valueString = value.toString(); + if (valueString == FALLTHROUGH) { + material->setPropertyDoesFallthrough(graphics::MaterialKey::FlagBit::LIGHTMAP_MAP_BIT); + } else { + material->setLightmapMap(baseUrl.resolved(valueString)); + } + } + } else if (key == "texCoordTransform0") { + auto value = materialJSON.value(key); + if (value.isString()) { + auto valueString = value.toString(); + if (valueString == FALLTHROUGH) { + material->setPropertyDoesFallthrough(graphics::Material::ExtraFlagBit::TEXCOORDTRANSFORM0); + } + } + // TODO: implement texCoordTransform0 + } else if (key == "texCoordTransform1") { + auto value = materialJSON.value(key); + if (value.isString()) { + auto valueString = value.toString(); + if (valueString == FALLTHROUGH) { + material->setPropertyDoesFallthrough(graphics::Material::ExtraFlagBit::TEXCOORDTRANSFORM1); + } + } + // TODO: implement texCoordTransform1 + } else if (key == "lightmapParams") { + auto value = materialJSON.value(key); + if (value.isString()) { + auto valueString = value.toString(); + if (valueString == FALLTHROUGH) { + material->setPropertyDoesFallthrough(graphics::Material::ExtraFlagBit::LIGHTMAP_PARAMS); + } + } + // TODO: implement lightmapParams + } else if (key == "materialParams") { + auto value = materialJSON.value(key); + if (value.isString()) { + auto valueString = value.toString(); + if (valueString == FALLTHROUGH) { + material->setPropertyDoesFallthrough(graphics::Material::ExtraFlagBit::MATERIAL_PARAMS); + } + } + // TODO: implement materialParams + } else if (key == "defaultFallthrough") { + auto value = materialJSON.value(key); + if (value.isBool()) { + material->setDefaultFallthrough(value.toBool()); } - material->setAlbedoMap(baseUrl.resolved(urlString), useAlphaChannel); - } - } else if (key == "roughnessMap") { - auto value = materialJSON.value(key); - if (value.isString()) { - material->setRoughnessMap(baseUrl.resolved(value.toString()), false); - } - } else if (key == "glossMap") { - auto value = materialJSON.value(key); - if (value.isString()) { - material->setRoughnessMap(baseUrl.resolved(value.toString()), true); - } - } else if (key == "metallicMap") { - auto value = materialJSON.value(key); - if (value.isString()) { - material->setMetallicMap(baseUrl.resolved(value.toString()), false); - } - } else if (key == "specularMap") { - auto value = materialJSON.value(key); - if (value.isString()) { - material->setMetallicMap(baseUrl.resolved(value.toString()), true); - } - } else if (key == "normalMap") { - auto value = materialJSON.value(key); - if (value.isString()) { - material->setNormalMap(baseUrl.resolved(value.toString()), false); - } - } else if (key == "bumpMap") { - auto value = materialJSON.value(key); - if (value.isString()) { - material->setNormalMap(baseUrl.resolved(value.toString()), true); - } - } else if (key == "occlusionMap") { - auto value = materialJSON.value(key); - if (value.isString()) { - material->setOcclusionMap(baseUrl.resolved(value.toString())); - } - } else if (key == "scatteringMap") { - auto value = materialJSON.value(key); - if (value.isString()) { - material->setScatteringMap(baseUrl.resolved(value.toString())); - } - } else if (key == "lightMap") { - auto value = materialJSON.value(key); - if (value.isString()) { - material->setLightmapMap(baseUrl.resolved(value.toString())); } } } diff --git a/libraries/networking/src/AccountManager.cpp b/libraries/networking/src/AccountManager.cpp index 989661cb81..f74b337ee7 100644 --- a/libraries/networking/src/AccountManager.cpp +++ b/libraries/networking/src/AccountManager.cpp @@ -588,6 +588,29 @@ void AccountManager::requestAccessTokenWithSteam(QByteArray authSessionTicket) { connect(requestReply, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(requestAccessTokenError(QNetworkReply::NetworkError))); } +void AccountManager::requestAccessTokenWithOculus(const QString& nonce, const QString &oculusID) { + QNetworkAccessManager& networkAccessManager = NetworkAccessManager::getInstance(); + + QNetworkRequest request; + request.setHeader(QNetworkRequest::UserAgentHeader, _userAgentGetter()); + + QUrl grantURL = _authURL; + grantURL.setPath("/oauth/token"); + + QByteArray postData; + postData.append("grant_type=password&"); + postData.append("oculus_nonce=" + nonce + "&"); + postData.append("oculus_id=" + oculusID + "&"); + postData.append("scope=" + ACCOUNT_MANAGER_REQUESTED_SCOPE); + + request.setUrl(grantURL); + request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded"); + + QNetworkReply* requestReply = networkAccessManager.post(request, postData); + connect(requestReply, &QNetworkReply::finished, this, &AccountManager::requestAccessTokenFinished); + connect(requestReply, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(requestAccessTokenError(QNetworkReply::NetworkError))); +} + void AccountManager::refreshAccessToken() { // we can't refresh our access token if we don't have a refresh token, so check for that first diff --git a/libraries/networking/src/AccountManager.h b/libraries/networking/src/AccountManager.h index ca2b826c98..477488031d 100644 --- a/libraries/networking/src/AccountManager.h +++ b/libraries/networking/src/AccountManager.h @@ -106,6 +106,7 @@ public: public slots: void requestAccessToken(const QString& login, const QString& password); void requestAccessTokenWithSteam(QByteArray authSessionTicket); + void requestAccessTokenWithOculus(const QString& nonce, const QString& oculusID); void requestAccessTokenWithAuthCode(const QString& authCode, const QString& clientId, const QString& clientSecret, diff --git a/libraries/networking/src/udt/Connection.cpp b/libraries/networking/src/udt/Connection.cpp index 418dc8f417..7ab2296935 100644 --- a/libraries/networking/src/udt/Connection.cpp +++ b/libraries/networking/src/udt/Connection.cpp @@ -31,6 +31,7 @@ using namespace udt; using namespace std::chrono; Connection::Connection(Socket* parentSocket, HifiSockAddr destination, std::unique_ptr congestionControl) : + QObject(parentSocket), _parentSocket(parentSocket), _destination(destination), _congestionControl(move(congestionControl)) diff --git a/libraries/networking/src/udt/PacketHeaders.cpp b/libraries/networking/src/udt/PacketHeaders.cpp index 642914cd56..7f20f881da 100644 --- a/libraries/networking/src/udt/PacketHeaders.cpp +++ b/libraries/networking/src/udt/PacketHeaders.cpp @@ -33,15 +33,15 @@ PacketVersion versionForPacketType(PacketType packetType) { case PacketType::EntityEdit: case PacketType::EntityData: case PacketType::EntityPhysics: - return static_cast(EntityVersion::FixProtocolVersionBumpMismatch); + return static_cast(EntityVersion::LAST_PACKET_TYPE); case PacketType::EntityQuery: return static_cast(EntityQueryPacketVersion::ConicalFrustums); case PacketType::AvatarIdentity: case PacketType::AvatarData: - return static_cast(AvatarMixerPacketVersion::CollisionFlag); + return static_cast(AvatarMixerPacketVersion::SendMaxTranslationDimension); case PacketType::BulkAvatarData: case PacketType::KillAvatar: - return static_cast(AvatarMixerPacketVersion::FasterAvatarEntities); + return static_cast(AvatarMixerPacketVersion::SendMaxTranslationDimension); case PacketType::MessagesData: return static_cast(MessageDataVersion::TextOrBinaryData); // ICE packets @@ -93,8 +93,6 @@ PacketVersion versionForPacketType(PacketType packetType) { return static_cast(PingVersion::IncludeConnectionID); case PacketType::AvatarQuery: return static_cast(AvatarQueryVersion::ConicalFrustums); - case PacketType::AvatarIdentityRequest: - return 22; case PacketType::EntityQueryInitialResultsComplete: return static_cast(EntityVersion::ParticleSpin); case PacketType::BulkAvatarTraitsAck: diff --git a/libraries/networking/src/udt/PacketHeaders.h b/libraries/networking/src/udt/PacketHeaders.h index f53a287d71..68830a2f6b 100644 --- a/libraries/networking/src/udt/PacketHeaders.h +++ b/libraries/networking/src/udt/PacketHeaders.h @@ -57,7 +57,7 @@ public: ICEServerQuery, OctreeStats, SetAvatarTraits, - AvatarIdentityRequest, + UNUSED_PACKET_TYPE, AssignmentClientStatus, NoisyMute, AvatarIdentity, @@ -255,7 +255,13 @@ enum class EntityVersion : PacketVersion { MorePropertiesCleanup, FixPropertiesFromCleanup, UpdatedPolyLines, - FixProtocolVersionBumpMismatch + FixProtocolVersionBumpMismatch, + MigrateOverlayRenderProperties, + MissingWebEntityProperties, + + // Add new versions above here + NUM_PACKET_TYPE, + LAST_PACKET_TYPE = NUM_PACKET_TYPE - 1 }; enum class EntityScriptCallMethodVersion : PacketVersion { @@ -312,7 +318,8 @@ enum class AvatarMixerPacketVersion : PacketVersion { GrabTraits, CollisionFlag, AvatarTraitsAck, - FasterAvatarEntities + FasterAvatarEntities, + SendMaxTranslationDimension }; enum class DomainConnectRequestVersion : PacketVersion { diff --git a/libraries/octree/src/Octree.h b/libraries/octree/src/Octree.h index eef23493f6..aac29201f1 100644 --- a/libraries/octree/src/Octree.h +++ b/libraries/octree/src/Octree.h @@ -149,6 +149,7 @@ public: OctreeElementPointer getRoot() { return _rootElement; } + virtual void eraseNonLocalEntities() { _isDirty = true; }; virtual void eraseAllOctreeElements(bool createNewRoot = true); virtual void readBitstreamToTree(const unsigned char* bitstream, uint64_t bufferSizeBytes, ReadBitstreamToTreeParams& args); diff --git a/libraries/octree/src/OctreePacketData.cpp b/libraries/octree/src/OctreePacketData.cpp old mode 100644 new mode 100755 index fd57f2fa3a..a79f0a0c2b --- a/libraries/octree/src/OctreePacketData.cpp +++ b/libraries/octree/src/OctreePacketData.cpp @@ -77,8 +77,8 @@ bool OctreePacketData::append(const unsigned char* data, int length) { _bytesAvailable -= length; success = true; _dirty = true; - } - + } + #ifdef WANT_DEBUG if (!success) { qCDebug(octree) << "OctreePacketData::append(const unsigned char* data, int length) FAILING...."; @@ -97,7 +97,7 @@ bool OctreePacketData::append(unsigned char byte) { if (_bytesAvailable > 0) { _uncompressed[_bytesInUse] = byte; _bytesInUse++; - _bytesAvailable--; + _bytesAvailable--; success = true; _dirty = true; } @@ -110,13 +110,13 @@ bool OctreePacketData::reserveBitMask() { bool OctreePacketData::reserveBytes(int numberOfBytes) { bool success = false; - + if (_bytesAvailable >= numberOfBytes) { _bytesReserved += numberOfBytes; _bytesAvailable -= numberOfBytes; success = true; } - + return success; } @@ -188,7 +188,7 @@ bool OctreePacketData::startSubTree(const unsigned char* octcode) { const unsigned char* OctreePacketData::getFinalizedData() { if (!_enableCompression) { - return &_uncompressed[0]; + return &_uncompressed[0]; } if (_dirty) { @@ -197,7 +197,7 @@ const unsigned char* OctreePacketData::getFinalizedData() { } compressContent(); } - return &_compressed[0]; + return &_compressed[0]; } int OctreePacketData::getFinalizedSize() { @@ -223,7 +223,7 @@ void OctreePacketData::endSubTree() { void OctreePacketData::discardSubTree() { int bytesInSubTree = _bytesInUse - _subTreeAt; _bytesInUse -= bytesInSubTree; - _bytesAvailable += bytesInSubTree; + _bytesAvailable += bytesInSubTree; _subTreeAt = _bytesInUse; // should be the same actually... _dirty = true; @@ -231,7 +231,7 @@ void OctreePacketData::discardSubTree() { int reduceBytesOfOctalCodes = _bytesOfOctalCodes - _bytesOfOctalCodesCurrentSubTree; _bytesOfOctalCodes = _bytesOfOctalCodesCurrentSubTree; _totalBytesOfOctalCodes -= reduceBytesOfOctalCodes; - + // if we discard the subtree then reset reserved bytes to the value when we started the subtree _bytesReserved = _subTreeBytesReserved; } @@ -243,7 +243,7 @@ LevelDetails OctreePacketData::startLevel() { void OctreePacketData::discardLevel(LevelDetails key) { int bytesInLevel = _bytesInUse - key._startIndex; - + // reset statistics... int reduceBytesOfOctalCodes = _bytesOfOctalCodes - key._bytesOfOctalCodes; int reduceBytesOfBitMasks = _bytesOfBitMasks - key._bytesOfBitmasks; @@ -261,11 +261,11 @@ void OctreePacketData::discardLevel(LevelDetails key) { qCDebug(octree, "discardLevel() BEFORE _dirty=%s bytesInLevel=%d _compressedBytes=%d _bytesInUse=%d", debug::valueOf(_dirty), bytesInLevel, _compressedBytes, _bytesInUse); } - + _bytesInUse -= bytesInLevel; - _bytesAvailable += bytesInLevel; + _bytesAvailable += bytesInLevel; _dirty = true; - + // reserved bytes are reset to the value when the level started _bytesReserved = key._bytesReservedAtStart; @@ -333,7 +333,7 @@ bool OctreePacketData::appendValue(uint8_t value) { bool OctreePacketData::appendValue(uint16_t value) { const unsigned char* data = (const unsigned char*)&value; - + int length = sizeof(value); bool success = append(data, length); if (success) { @@ -506,10 +506,11 @@ bool OctreePacketData::appendValue(bool value) { bool OctreePacketData::appendValue(const QString& string) { // TODO: make this a ByteCountCoded leading byte - uint16_t length = string.size() + 1; // include NULL + QByteArray utf8Array = string.toUtf8(); + uint16_t length = utf8Array.length(); // no NULL bool success = appendValue(length); if (success) { - success = appendRawData((const unsigned char*)qPrintable(string), length); + success = appendRawData((const unsigned char*)utf8Array.constData(), length); } return success; } @@ -666,7 +667,7 @@ void OctreePacketData::debugContent() { } } printf("\n"); - + qCDebug(octree, "OctreePacketData::debugContent()... UNCOMPRESSED DATA.... size=%d",_bytesInUse); perline=0; for (int i = 0; i < _bytesInUse; i++) { @@ -702,16 +703,16 @@ int OctreePacketData::unpackDataFromBytes(const unsigned char* dataBytes, glm::u return sizeof(result); } -int OctreePacketData::unpackDataFromBytes(const unsigned char* dataBytes, QString& result) { +int OctreePacketData::unpackDataFromBytes(const unsigned char* dataBytes, QString& result) { uint16_t length; memcpy(&length, dataBytes, sizeof(length)); dataBytes += sizeof(length); - QString value((const char*)dataBytes); + QString value = QString::fromUtf8((const char*)dataBytes, length); result = value; return sizeof(length) + length; } -int OctreePacketData::unpackDataFromBytes(const unsigned char* dataBytes, QUuid& result) { +int OctreePacketData::unpackDataFromBytes(const unsigned char* dataBytes, QUuid& result) { uint16_t length; memcpy(&length, dataBytes, sizeof(length)); dataBytes += sizeof(length); @@ -819,4 +820,4 @@ int OctreePacketData::unpackDataFromBytes(const unsigned char* dataBytes, AACube int OctreePacketData::unpackDataFromBytes(const unsigned char* dataBytes, QRect& result) { memcpy(&result, dataBytes, sizeof(result)); return sizeof(result); -} \ No newline at end of file +} diff --git a/libraries/octree/src/OctreePacketData.h b/libraries/octree/src/OctreePacketData.h index bd1abf8744..1dbb564fe9 100644 --- a/libraries/octree/src/OctreePacketData.h +++ b/libraries/octree/src/OctreePacketData.h @@ -35,6 +35,9 @@ #include "MaterialMappingMode.h" #include "BillboardMode.h" +#include "RenderLayer.h" +#include "PrimitiveMode.h" +#include "WebInputMode.h" #include "OctreeConstants.h" #include "OctreeElement.h" @@ -263,6 +266,9 @@ public: static int unpackDataFromBytes(const unsigned char* dataBytes, ShapeType& result) { memcpy(&result, dataBytes, sizeof(result)); return sizeof(result); } static int unpackDataFromBytes(const unsigned char* dataBytes, MaterialMappingMode& result) { memcpy(&result, dataBytes, sizeof(result)); return sizeof(result); } static int unpackDataFromBytes(const unsigned char* dataBytes, BillboardMode& result) { memcpy(&result, dataBytes, sizeof(result)); return sizeof(result); } + static int unpackDataFromBytes(const unsigned char* dataBytes, RenderLayer& result) { memcpy(&result, dataBytes, sizeof(result)); return sizeof(result); } + static int unpackDataFromBytes(const unsigned char* dataBytes, PrimitiveMode& result) { memcpy(&result, dataBytes, sizeof(result)); return sizeof(result); } + static int unpackDataFromBytes(const unsigned char* dataBytes, WebInputMode& result) { memcpy(&result, dataBytes, sizeof(result)); return sizeof(result); } static int unpackDataFromBytes(const unsigned char* dataBytes, glm::vec2& result); static int unpackDataFromBytes(const unsigned char* dataBytes, glm::vec3& result); static int unpackDataFromBytes(const unsigned char* dataBytes, glm::u8vec3& result); diff --git a/libraries/octree/src/OctreeProcessor.cpp b/libraries/octree/src/OctreeProcessor.cpp index 206ff399d9..18c8630391 100644 --- a/libraries/octree/src/OctreeProcessor.cpp +++ b/libraries/octree/src/OctreeProcessor.cpp @@ -51,15 +51,15 @@ void OctreeProcessor::processDatagram(ReceivedMessage& message, SharedNodePointe bool showTimingDetails = false; // Menu::getInstance()->isOptionChecked(MenuOption::PipelineWarnings); PerformanceWarning warn(showTimingDetails, "OctreeProcessor::processDatagram()", showTimingDetails); - + if (message.getType() == getExpectedPacketType()) { PerformanceWarning warn(showTimingDetails, "OctreeProcessor::processDatagram expected PacketType", showTimingDetails); // if we are getting inbound packets, then our tree is also viewing, and we should remember that fact. _tree->setIsViewing(true); - + OCTREE_PACKET_FLAGS flags; message.readPrimitive(&flags); - + OCTREE_PACKET_SEQUENCE sequence; message.readPrimitive(&sequence); @@ -68,7 +68,7 @@ void OctreeProcessor::processDatagram(ReceivedMessage& message, SharedNodePointe bool packetIsColored = oneAtBit(flags, PACKET_IS_COLOR_BIT); bool packetIsCompressed = oneAtBit(flags, PACKET_IS_COMPRESSED_BIT); - + OCTREE_PACKET_SENT_TIME arrivedAt = usecTimestampNow(); qint64 clockSkew = sourceNode ? sourceNode->getClockSkewUsec() : 0; qint64 flightTime = arrivedAt - sentAt + clockSkew; @@ -79,27 +79,27 @@ void OctreeProcessor::processDatagram(ReceivedMessage& message, SharedNodePointe qCDebug(octree) << "OctreeProcessor::processDatagram() ... " "Got Packet Section color:" << packetIsColored << "compressed:" << packetIsCompressed << - "sequence: " << sequence << + "sequence: " << sequence << "flight: " << flightTime << " usec" << "size:" << message.getSize() << "data:" << message.getBytesLeftToRead(); } - + _packetsInLastWindow++; - + int elementsPerPacket = 0; int entitiesPerPacket = 0; - + quint64 totalWaitingForLock = 0; quint64 totalUncompress = 0; quint64 totalReadBitsteam = 0; const QUuid& sourceUUID = sourceNode->getUUID(); - + int subsection = 1; - + bool error = false; - + while (message.getBytesLeftToRead() > 0 && !error) { if (packetIsCompressed) { if (message.getBytesLeftToRead() > (qint64) sizeof(OCTREE_PACKET_INTERNAL_SECTION_SIZE)) { @@ -111,7 +111,7 @@ void OctreeProcessor::processDatagram(ReceivedMessage& message, SharedNodePointe } else { sectionLength = message.getBytesLeftToRead(); } - + if (sectionLength) { // ask the VoxelTree to read the bitstream into the tree ReadBitstreamToTreeParams args(WANT_EXISTS_BITS, NULL, @@ -149,7 +149,7 @@ void OctreeProcessor::processDatagram(ReceivedMessage& message, SharedNodePointe qCDebug(octree) << "OctreeProcessor::processDatagram() ******* END _tree->readBitstreamToTree()..."; } }); - + // seek forwards in packet message.seek(message.getPosition() + sectionLength); @@ -172,13 +172,13 @@ void OctreeProcessor::processDatagram(ReceivedMessage& message, SharedNodePointe _waitLockPerPacket.updateAverage(totalWaitingForLock); _uncompressPerPacket.updateAverage(totalUncompress); _readBitstreamPerPacket.updateAverage(totalReadBitsteam); - + quint64 now = usecTimestampNow(); if (_lastWindowAt == 0) { _lastWindowAt = now; } quint64 sinceLastWindow = now - _lastWindowAt; - + if (sinceLastWindow > USECS_PER_SECOND) { float packetsPerSecondInWindow = (float)_packetsInLastWindow / (float)(sinceLastWindow / USECS_PER_SECOND); float elementsPerSecondInWindow = (float)_elementsInLastWindow / (float)(sinceLastWindow / USECS_PER_SECOND); @@ -197,6 +197,14 @@ void OctreeProcessor::processDatagram(ReceivedMessage& message, SharedNodePointe } } + +void OctreeProcessor::clearNonLocalEntities() { + if (_tree) { + _tree->withWriteLock([&] { + _tree->eraseNonLocalEntities(); + }); + } +} void OctreeProcessor::clear() { if (_tree) { _tree->withWriteLock([&] { diff --git a/libraries/octree/src/OctreeProcessor.h b/libraries/octree/src/OctreeProcessor.h index 1bc3bd10f9..bc5618e657 100644 --- a/libraries/octree/src/OctreeProcessor.h +++ b/libraries/octree/src/OctreeProcessor.h @@ -43,6 +43,7 @@ public: virtual void init(); /// clears the tree + virtual void clearNonLocalEntities(); virtual void clear(); float getAverageElementsPerPacket() const { return _elementsPerPacket.getAverage(); } diff --git a/libraries/physics/src/CharacterController.cpp b/libraries/physics/src/CharacterController.cpp index 8fd6d4eada..d5ded6f909 100755 --- a/libraries/physics/src/CharacterController.cpp +++ b/libraries/physics/src/CharacterController.cpp @@ -109,7 +109,8 @@ void CharacterController::setDynamicsWorld(btDynamicsWorld* world) { } _dynamicsWorld = nullptr; } - int32_t collisionGroup = computeCollisionGroup(); + int32_t collisionMask = computeCollisionMask(); + int32_t collisionGroup = BULLET_COLLISION_GROUP_MY_AVATAR; if (_rigidBody) { updateMassProperties(); } @@ -117,7 +118,7 @@ void CharacterController::setDynamicsWorld(btDynamicsWorld* world) { // add to new world _dynamicsWorld = world; _pendingFlags &= ~PENDING_FLAG_JUMP; - _dynamicsWorld->addRigidBody(_rigidBody, collisionGroup, BULLET_COLLISION_MASK_MY_AVATAR); + _dynamicsWorld->addRigidBody(_rigidBody, collisionGroup, collisionMask); _dynamicsWorld->addAction(this); // restore gravity settings because adding an object to the world overwrites its gravity setting _rigidBody->setGravity(_currentGravity * _currentUp); @@ -127,7 +128,7 @@ void CharacterController::setDynamicsWorld(btDynamicsWorld* world) { assert(shape && shape->getShapeType() == CONVEX_HULL_SHAPE_PROXYTYPE); _ghost.setCharacterShape(static_cast(shape)); } - _ghost.setCollisionGroupAndMask(collisionGroup, BULLET_COLLISION_MASK_MY_AVATAR & (~ collisionGroup)); + _ghost.setCollisionGroupAndMask(collisionGroup, collisionMask & (~ collisionGroup)); _ghost.setCollisionWorld(_dynamicsWorld); _ghost.setRadiusAndHalfHeight(_radius, _halfHeight); if (_rigidBody) { @@ -384,8 +385,8 @@ static const char* stateToStr(CharacterController::State state) { #endif // #ifdef DEBUG_STATE_CHANGE void CharacterController::updateCurrentGravity() { - int32_t collisionGroup = computeCollisionGroup(); - if (_state == State::Hover || collisionGroup == BULLET_COLLISION_GROUP_COLLISIONLESS) { + int32_t collisionMask = computeCollisionMask(); + if (_state == State::Hover || collisionMask == BULLET_COLLISION_MASK_COLLISIONLESS) { _currentGravity = 0.0f; } else { _currentGravity = _gravity; @@ -458,28 +459,7 @@ void CharacterController::setLocalBoundingBox(const glm::vec3& minCorner, const void CharacterController::setCollisionless(bool collisionless) { if (collisionless != _collisionless) { _collisionless = collisionless; - _pendingFlags |= PENDING_FLAG_UPDATE_COLLISION_GROUP; - } -} - -int32_t CharacterController::computeCollisionGroup() const { - if (_collisionless) { - return _collisionlessAllowed ? BULLET_COLLISION_GROUP_COLLISIONLESS : BULLET_COLLISION_GROUP_MY_AVATAR; - } else { - return BULLET_COLLISION_GROUP_MY_AVATAR; - } -} - -void CharacterController::handleChangedCollisionGroup() { - if (_pendingFlags & PENDING_FLAG_UPDATE_COLLISION_GROUP) { - // ATM the easiest way to update collision groups is to remove/re-add the RigidBody - if (_dynamicsWorld) { - _dynamicsWorld->removeRigidBody(_rigidBody); - int32_t collisionGroup = computeCollisionGroup(); - _dynamicsWorld->addRigidBody(_rigidBody, collisionGroup, BULLET_COLLISION_MASK_MY_AVATAR); - } - _pendingFlags &= ~PENDING_FLAG_UPDATE_COLLISION_GROUP; - updateCurrentGravity(); + _pendingFlags |= PENDING_FLAG_UPDATE_COLLISION_MASK; } } @@ -567,8 +547,8 @@ void CharacterController::applyMotor(int index, btScalar dt, btVector3& worldVel btScalar angle = motor.rotation.getAngle(); btVector3 velocity = worldVelocity.rotate(axis, -angle); - int32_t collisionGroup = computeCollisionGroup(); - if (collisionGroup == BULLET_COLLISION_GROUP_COLLISIONLESS || + int32_t collisionMask = computeCollisionMask(); + if (collisionMask == BULLET_COLLISION_MASK_COLLISIONLESS || _state == State::Hover || motor.hTimescale == motor.vTimescale) { // modify velocity btScalar tau = dt / motor.hTimescale; @@ -708,11 +688,11 @@ void CharacterController::updateState() { btVector3 rayStart = _position; btScalar rayLength = _radius; - int32_t collisionGroup = computeCollisionGroup(); - if (collisionGroup == BULLET_COLLISION_GROUP_MY_AVATAR) { - rayLength += _scaleFactor * DEFAULT_AVATAR_FALL_HEIGHT; - } else { + int32_t collisionMask = computeCollisionMask(); + if (collisionMask == BULLET_COLLISION_MASK_COLLISIONLESS) { rayLength += MIN_HOVER_HEIGHT; + } else { + rayLength += _scaleFactor * DEFAULT_AVATAR_FALL_HEIGHT; } btVector3 rayEnd = rayStart - rayLength * _currentUp; @@ -746,69 +726,7 @@ void CharacterController::updateState() { // disable normal state transitions while collisionless const btScalar MAX_WALKING_SPEED = 2.65f; - if (collisionGroup == BULLET_COLLISION_GROUP_MY_AVATAR) { - switch (_state) { - case State::Ground: - if (!rayHasHit && !_hasSupport) { - SET_STATE(State::Hover, "no ground detected"); - } else if (_pendingFlags & PENDING_FLAG_JUMP && _jumpButtonDownCount != _takeoffJumpButtonID) { - _takeoffJumpButtonID = _jumpButtonDownCount; - _takeoffToInAirStartTime = now; - SET_STATE(State::Takeoff, "jump pressed"); - } else if (rayHasHit && !_hasSupport && _floorDistance > GROUND_TO_FLY_THRESHOLD) { - SET_STATE(State::InAir, "falling"); - } - break; - case State::Takeoff: - if (!rayHasHit && !_hasSupport) { - SET_STATE(State::Hover, "no ground"); - } else if ((now - _takeoffToInAirStartTime) > TAKE_OFF_TO_IN_AIR_PERIOD) { - SET_STATE(State::InAir, "takeoff done"); - - // compute jumpSpeed based on the scaled jump height for the default avatar in default gravity. - const float jumpHeight = std::max(_scaleFactor * DEFAULT_AVATAR_JUMP_HEIGHT, DEFAULT_AVATAR_MIN_JUMP_HEIGHT); - const float jumpSpeed = sqrtf(2.0f * -DEFAULT_AVATAR_GRAVITY * jumpHeight); - velocity += jumpSpeed * _currentUp; - _rigidBody->setLinearVelocity(velocity); - } - break; - case State::InAir: { - const float jumpHeight = std::max(_scaleFactor * DEFAULT_AVATAR_JUMP_HEIGHT, DEFAULT_AVATAR_MIN_JUMP_HEIGHT); - const float jumpSpeed = sqrtf(2.0f * -DEFAULT_AVATAR_GRAVITY * jumpHeight); - if ((velocity.dot(_currentUp) <= (jumpSpeed / 2.0f)) && ((_floorDistance < FLY_TO_GROUND_THRESHOLD) || _hasSupport)) { - SET_STATE(State::Ground, "hit ground"); - } else if (_flyingAllowed) { - btVector3 desiredVelocity = _targetVelocity; - if (desiredVelocity.length2() < MIN_TARGET_SPEED_SQUARED) { - desiredVelocity = btVector3(0.0f, 0.0f, 0.0f); - } - bool vertTargetSpeedIsNonZero = desiredVelocity.dot(_currentUp) > MIN_TARGET_SPEED; - if ((jumpButtonHeld || vertTargetSpeedIsNonZero) && (_takeoffJumpButtonID != _jumpButtonDownCount)) { - SET_STATE(State::Hover, "double jump button"); - } else if ((jumpButtonHeld || vertTargetSpeedIsNonZero) && (now - _jumpButtonDownStartTime) > JUMP_TO_HOVER_PERIOD) { - SET_STATE(State::Hover, "jump button held"); - } else if (_floorDistance > _scaleFactor * DEFAULT_AVATAR_FALL_HEIGHT) { - // Transition to hover if we are above the fall threshold - SET_STATE(State::Hover, "above fall threshold"); - } - } else if (!rayHasHit && !_hasSupport) { - SET_STATE(State::Hover, "no ground detected"); - } - break; - } - case State::Hover: - btScalar horizontalSpeed = (velocity - velocity.dot(_currentUp) * _currentUp).length(); - bool flyingFast = horizontalSpeed > (MAX_WALKING_SPEED * 0.75f); - if (!_flyingAllowed && rayHasHit) { - SET_STATE(State::InAir, "flying not allowed"); - } else if ((_floorDistance < MIN_HOVER_HEIGHT) && !jumpButtonHeld && !flyingFast) { - SET_STATE(State::InAir, "near ground"); - } else if (((_floorDistance < FLY_TO_GROUND_THRESHOLD) || _hasSupport) && !flyingFast) { - SET_STATE(State::Ground, "touching ground"); - } - break; - } - } else { + if (collisionMask == BULLET_COLLISION_MASK_COLLISIONLESS) { // when collisionless: only switch between State::Ground and State::Hover // and bypass state debugging if (rayHasHit) { @@ -820,6 +738,68 @@ void CharacterController::updateState() { } else { _state = State::Hover; } + } else { + switch (_state) { + case State::Ground: + if (!rayHasHit && !_hasSupport) { + SET_STATE(State::Hover, "no ground detected"); + } else if (_pendingFlags & PENDING_FLAG_JUMP && _jumpButtonDownCount != _takeoffJumpButtonID) { + _takeoffJumpButtonID = _jumpButtonDownCount; + _takeoffToInAirStartTime = now; + SET_STATE(State::Takeoff, "jump pressed"); + } else if (rayHasHit && !_hasSupport && _floorDistance > GROUND_TO_FLY_THRESHOLD) { + SET_STATE(State::InAir, "falling"); + } + break; + case State::Takeoff: + if (!rayHasHit && !_hasSupport) { + SET_STATE(State::Hover, "no ground"); + } else if ((now - _takeoffToInAirStartTime) > TAKE_OFF_TO_IN_AIR_PERIOD) { + SET_STATE(State::InAir, "takeoff done"); + + // compute jumpSpeed based on the scaled jump height for the default avatar in default gravity. + const float jumpHeight = std::max(_scaleFactor * DEFAULT_AVATAR_JUMP_HEIGHT, DEFAULT_AVATAR_MIN_JUMP_HEIGHT); + const float jumpSpeed = sqrtf(2.0f * -DEFAULT_AVATAR_GRAVITY * jumpHeight); + velocity += jumpSpeed * _currentUp; + _rigidBody->setLinearVelocity(velocity); + } + break; + case State::InAir: { + const float jumpHeight = std::max(_scaleFactor * DEFAULT_AVATAR_JUMP_HEIGHT, DEFAULT_AVATAR_MIN_JUMP_HEIGHT); + const float jumpSpeed = sqrtf(2.0f * -DEFAULT_AVATAR_GRAVITY * jumpHeight); + if ((velocity.dot(_currentUp) <= (jumpSpeed / 2.0f)) && ((_floorDistance < FLY_TO_GROUND_THRESHOLD) || _hasSupport)) { + SET_STATE(State::Ground, "hit ground"); + } else if (_flyingAllowed) { + btVector3 desiredVelocity = _targetVelocity; + if (desiredVelocity.length2() < MIN_TARGET_SPEED_SQUARED) { + desiredVelocity = btVector3(0.0f, 0.0f, 0.0f); + } + bool vertTargetSpeedIsNonZero = desiredVelocity.dot(_currentUp) > MIN_TARGET_SPEED; + if ((jumpButtonHeld || vertTargetSpeedIsNonZero) && (_takeoffJumpButtonID != _jumpButtonDownCount)) { + SET_STATE(State::Hover, "double jump button"); + } else if ((jumpButtonHeld || vertTargetSpeedIsNonZero) && (now - _jumpButtonDownStartTime) > JUMP_TO_HOVER_PERIOD) { + SET_STATE(State::Hover, "jump button held"); + } else if (_floorDistance > _scaleFactor * DEFAULT_AVATAR_FALL_HEIGHT) { + // Transition to hover if we are above the fall threshold + SET_STATE(State::Hover, "above fall threshold"); + } + } else if (!rayHasHit && !_hasSupport) { + SET_STATE(State::Hover, "no ground detected"); + } + break; + } + case State::Hover: + btScalar horizontalSpeed = (velocity - velocity.dot(_currentUp) * _currentUp).length(); + bool flyingFast = horizontalSpeed > (MAX_WALKING_SPEED * 0.75f); + if (!_flyingAllowed && rayHasHit) { + SET_STATE(State::InAir, "flying not allowed"); + } else if ((_floorDistance < MIN_HOVER_HEIGHT) && !jumpButtonHeld && !flyingFast) { + SET_STATE(State::InAir, "near ground"); + } else if (((_floorDistance < FLY_TO_GROUND_THRESHOLD) || _hasSupport) && !flyingFast) { + SET_STATE(State::Ground, "touching ground"); + } + break; + } } } @@ -866,6 +846,6 @@ void CharacterController::setFlyingAllowed(bool value) { void CharacterController::setCollisionlessAllowed(bool value) { if (value != _collisionlessAllowed) { _collisionlessAllowed = value; - _pendingFlags |= PENDING_FLAG_UPDATE_COLLISION_GROUP; + _pendingFlags |= PENDING_FLAG_UPDATE_COLLISION_MASK; } } diff --git a/libraries/physics/src/CharacterController.h b/libraries/physics/src/CharacterController.h old mode 100644 new mode 100755 index 50db2bea12..cac37da0b9 --- a/libraries/physics/src/CharacterController.h +++ b/libraries/physics/src/CharacterController.h @@ -30,7 +30,7 @@ const uint32_t PENDING_FLAG_ADD_TO_SIMULATION = 1U << 0; const uint32_t PENDING_FLAG_REMOVE_FROM_SIMULATION = 1U << 1; const uint32_t PENDING_FLAG_UPDATE_SHAPE = 1U << 2; const uint32_t PENDING_FLAG_JUMP = 1U << 3; -const uint32_t PENDING_FLAG_UPDATE_COLLISION_GROUP = 1U << 4; +const uint32_t PENDING_FLAG_UPDATE_COLLISION_MASK = 1U << 4; const uint32_t PENDING_FLAG_RECOMPUTE_FLYING = 1U << 5; const float DEFAULT_MIN_FLOOR_NORMAL_DOT_UP = cosf(PI / 3.0f); @@ -120,14 +120,16 @@ public: bool isStuck() const { return _isStuck; } void setCollisionless(bool collisionless); - int32_t computeCollisionGroup() const; - void handleChangedCollisionGroup(); + + virtual int32_t computeCollisionMask() const = 0; + virtual void handleChangedCollisionMask() = 0; bool getRigidBodyLocation(glm::vec3& avatarRigidBodyPosition, glm::quat& avatarRigidBodyRotation); void setFlyingAllowed(bool value); void setCollisionlessAllowed(bool value); + void setPendingFlagsUpdateCollisionMask(){ _pendingFlags |= PENDING_FLAG_UPDATE_COLLISION_MASK; } protected: #ifdef DEBUG_STATE_CHANGE diff --git a/libraries/plugins/src/plugins/DisplayPlugin.h b/libraries/plugins/src/plugins/DisplayPlugin.h index ad49ceafe6..fde43e7a5b 100644 --- a/libraries/plugins/src/plugins/DisplayPlugin.h +++ b/libraries/plugins/src/plugins/DisplayPlugin.h @@ -217,6 +217,9 @@ public: static const QString& MENU_PATH(); + // for updating plugin-related commands. Mimics the input plugin. + virtual void pluginUpdate() = 0; + signals: void recommendedFramebufferSizeChanged(const QSize& size); void resetSensorsRequested(); diff --git a/libraries/plugins/src/plugins/Forward.h b/libraries/plugins/src/plugins/Forward.h index 90746d648e..fc1e12b639 100644 --- a/libraries/plugins/src/plugins/Forward.h +++ b/libraries/plugins/src/plugins/Forward.h @@ -21,6 +21,7 @@ class DisplayPlugin; class InputPlugin; class CodecPlugin; class SteamClientPlugin; +class OculusPlatformPlugin; class Plugin; class PluginContainer; class PluginManager; @@ -35,4 +36,5 @@ using CodecPluginPointer = std::shared_ptr; using CodecPluginList = std::vector; using CodecPluginProvider = std::function; using SteamClientPluginPointer = std::shared_ptr; +using OculusPlatformPluginPointer = std::shared_ptr; using InputPluginSettingsPersister = std::function; diff --git a/libraries/plugins/src/plugins/OculusPlatformPlugin.h b/libraries/plugins/src/plugins/OculusPlatformPlugin.h new file mode 100644 index 0000000000..93bf534c6e --- /dev/null +++ b/libraries/plugins/src/plugins/OculusPlatformPlugin.h @@ -0,0 +1,28 @@ +// +// Created by Wayne Chen on 2018/12/20 +// 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 +// +#pragma once + +#include + +#include + +using NonceUserIDCallback = std::function; + +class OculusPlatformPlugin { +public: + virtual ~OculusPlatformPlugin() = default; + + virtual QString getName() const = 0; + virtual QString getOculusUserID() const = 0; + + virtual bool isRunning() const = 0; + + virtual void requestNonceAndUserID(NonceUserIDCallback callback) = 0; + + virtual void handleOVREvents() = 0; +}; diff --git a/libraries/plugins/src/plugins/PluginManager.cpp b/libraries/plugins/src/plugins/PluginManager.cpp index a75ede3f03..0d0209e35f 100644 --- a/libraries/plugins/src/plugins/PluginManager.cpp +++ b/libraries/plugins/src/plugins/PluginManager.cpp @@ -14,6 +14,11 @@ #include #include +//#define HIFI_PLUGINMANAGER_DEBUG +#if defined(HIFI_PLUGINMANAGER_DEBUG) +#include +#endif + #include #include @@ -79,10 +84,7 @@ bool isDisabled(QJsonObject metaData) { return false; } -using Loader = QSharedPointer; -using LoaderList = QList; - -const LoaderList& getLoadedPlugins() { + auto PluginManager::getLoadedPlugins() const -> const LoaderList& { static std::once_flag once; static LoaderList loadedPlugins; std::call_once(once, [&] { @@ -106,15 +108,25 @@ const LoaderList& getLoadedPlugins() { for (auto plugin : candidates) { qCDebug(plugins) << "Attempting plugin" << qPrintable(plugin); QSharedPointer loader(new QPluginLoader(pluginPath + plugin)); - - if (isDisabled(loader->metaData())) { + const QJsonObject pluginMetaData = loader->metaData(); +#if defined(HIFI_PLUGINMANAGER_DEBUG) + QJsonDocument metaDataDoc(pluginMetaData); + qCInfo(plugins) << "Metadata for " << qPrintable(plugin) << ": " << QString(metaDataDoc.toJson()); +#endif + if (isDisabled(pluginMetaData)) { qCWarning(plugins) << "Plugin" << qPrintable(plugin) << "is disabled"; // Skip this one, it's disabled continue; } - if (getPluginInterfaceVersionFromMetaData(loader->metaData()) != HIFI_PLUGIN_INTERFACE_VERSION) { + + if (!_pluginFilter(pluginMetaData)) { + qCDebug(plugins) << "Plugin" << qPrintable(plugin) << "doesn't pass provided filter"; + continue; + } + + if (getPluginInterfaceVersionFromMetaData(pluginMetaData) != HIFI_PLUGIN_INTERFACE_VERSION) { qCWarning(plugins) << "Plugin" << qPrintable(plugin) << "interface version doesn't match, not loading:" - << getPluginInterfaceVersionFromMetaData(loader->metaData()) + << getPluginInterfaceVersionFromMetaData(pluginMetaData) << "doesn't match" << HIFI_PLUGIN_INTERFACE_VERSION; continue; } @@ -176,6 +188,22 @@ const SteamClientPluginPointer PluginManager::getSteamClientPlugin() { return steamClientPlugin; } +const OculusPlatformPluginPointer PluginManager::getOculusPlatformPlugin() { + static OculusPlatformPluginPointer oculusPlatformPlugin; + static std::once_flag once; + std::call_once(once, [&] { + // Now grab the dynamic plugins + for (auto loader : getLoadedPlugins()) { + OculusPlatformProvider* oculusPlatformProvider = qobject_cast(loader->instance()); + if (oculusPlatformProvider) { + oculusPlatformPlugin = oculusPlatformProvider->getOculusPlatformPlugin(); + break; + } + } + }); + return oculusPlatformPlugin; +} + const DisplayPluginList& PluginManager::getDisplayPlugins() { static std::once_flag once; static auto deviceAddedCallback = [](QString deviceName) { diff --git a/libraries/plugins/src/plugins/PluginManager.h b/libraries/plugins/src/plugins/PluginManager.h index 2a002577a4..1a578c7406 100644 --- a/libraries/plugins/src/plugins/PluginManager.h +++ b/libraries/plugins/src/plugins/PluginManager.h @@ -13,8 +13,7 @@ #include "Forward.h" - -class PluginManager; +class QPluginLoader; using PluginManagerPointer = QSharedPointer; class PluginManager : public QObject, public Dependency { @@ -28,6 +27,7 @@ public: const InputPluginList& getInputPlugins(); const CodecPluginList& getCodecPlugins(); const SteamClientPluginPointer getSteamClientPlugin(); + const OculusPlatformPluginPointer getOculusPlatformPlugin(); DisplayPluginList getPreferredDisplayPlugins(); void setPreferredDisplayPlugins(const QStringList& displays); @@ -47,6 +47,9 @@ public: void setInputPluginSettingsPersister(const InputPluginSettingsPersister& persister); QStringList getRunningInputDeviceNames() const; + using PluginFilter = std::function; + void setPluginFilter(PluginFilter pluginFilter) { _pluginFilter = pluginFilter; } + signals: void inputDeviceRunningChanged(const QString& pluginName, bool isRunning, const QStringList& runningDevices); @@ -60,6 +63,12 @@ private: PluginContainer* _container { nullptr }; DisplayPluginList _displayPlugins; InputPluginList _inputPlugins; + PluginFilter _pluginFilter { [](const QJsonObject&) { return true; } }; + + using Loader = QSharedPointer; + using LoaderList = QList; + + const LoaderList& getLoadedPlugins() const; }; // TODO: we should define this value in CMake, and then use CMake diff --git a/libraries/plugins/src/plugins/RuntimePlugin.h b/libraries/plugins/src/plugins/RuntimePlugin.h index 9a7d6e0638..756b4ff585 100644 --- a/libraries/plugins/src/plugins/RuntimePlugin.h +++ b/libraries/plugins/src/plugins/RuntimePlugin.h @@ -51,5 +51,13 @@ public: virtual SteamClientPluginPointer getSteamClientPlugin() = 0; }; +class OculusPlatformProvider { +public: + virtual OculusPlatformPluginPointer getOculusPlatformPlugin() = 0; +}; + #define SteamClientProvider_iid "com.highfidelity.plugins.steamclient" Q_DECLARE_INTERFACE(SteamClientProvider, SteamClientProvider_iid) + +#define OculusPlatformProvider_iid "com.highfidelity.plugins.oculusplatform" +Q_DECLARE_INTERFACE(OculusPlatformProvider, OculusPlatformProvider_iid) \ No newline at end of file diff --git a/libraries/qml/src/qml/impl/RenderEventHandler.cpp b/libraries/qml/src/qml/impl/RenderEventHandler.cpp index 46fc3490a7..97c222337b 100644 --- a/libraries/qml/src/qml/impl/RenderEventHandler.cpp +++ b/libraries/qml/src/qml/impl/RenderEventHandler.cpp @@ -66,6 +66,7 @@ void RenderEventHandler::onInitalize() { qFatal("Unable to make QML rendering context current on render thread"); } _shared->initializeRenderControl(_canvas.getContext()); + _initialized = true; } void RenderEventHandler::resize() { @@ -150,22 +151,24 @@ void RenderEventHandler::onRender() { } void RenderEventHandler::onQuit() { - if (_canvas.getContext() != QOpenGLContextWrapper::currentContext()) { - qFatal("QML rendering context not current on render thread"); - } + if (_initialized) { + if (_canvas.getContext() != QOpenGLContextWrapper::currentContext()) { + qFatal("QML rendering context not current on render thread"); + } - if (_depthStencil) { - glDeleteRenderbuffers(1, &_depthStencil); - _depthStencil = 0; - } + if (_depthStencil) { + glDeleteRenderbuffers(1, &_depthStencil); + _depthStencil = 0; + } - if (_fbo) { - glDeleteFramebuffers(1, &_fbo); - _fbo = 0; - } + if (_fbo) { + glDeleteFramebuffers(1, &_fbo); + _fbo = 0; + } - _shared->shutdownRendering(_canvas, _currentSize); - _canvas.doneCurrent(); + _shared->shutdownRendering(_canvas, _currentSize); + _canvas.doneCurrent(); + } _canvas.moveToThreadWithContext(qApp->thread()); moveToThread(qApp->thread()); QThread::currentThread()->quit(); diff --git a/libraries/qml/src/qml/impl/RenderEventHandler.h b/libraries/qml/src/qml/impl/RenderEventHandler.h index 1e2f9945f3..86046c3721 100644 --- a/libraries/qml/src/qml/impl/RenderEventHandler.h +++ b/libraries/qml/src/qml/impl/RenderEventHandler.h @@ -53,6 +53,8 @@ private: uint32_t _fbo{ 0 }; uint32_t _depthStencil{ 0 }; + + bool _initialized { false }; }; }}} // namespace hifi::qml::impl diff --git a/libraries/qml/src/qml/impl/SharedObject.cpp b/libraries/qml/src/qml/impl/SharedObject.cpp index 5bcca0821f..a064be79bd 100644 --- a/libraries/qml/src/qml/impl/SharedObject.cpp +++ b/libraries/qml/src/qml/impl/SharedObject.cpp @@ -500,6 +500,9 @@ void SharedObject::onTimer() { } { + if (_maxFps == 0) { + return; + } auto minRenderInterval = USECS_PER_SECOND / _maxFps; auto lastInterval = usecTimestampNow() - _lastRenderTime; // Don't exceed the framerate limit diff --git a/libraries/render-utils/src/CauterizedModel.cpp b/libraries/render-utils/src/CauterizedModel.cpp index 3e32d19b49..81a81c5602 100644 --- a/libraries/render-utils/src/CauterizedModel.cpp +++ b/libraries/render-utils/src/CauterizedModel.cpp @@ -215,7 +215,7 @@ void CauterizedModel::updateRenderItems() { modelTransform.setTranslation(self->getTranslation()); modelTransform.setRotation(self->getRotation()); - bool isWireframe = self->isWireframe(); + PrimitiveMode primitiveMode = self->getPrimitiveMode(); auto renderItemKeyGlobalFlags = self->getRenderItemKeyGlobalFlags(); bool enableCauterization = self->getEnableCauterization(); @@ -232,7 +232,7 @@ void CauterizedModel::updateRenderItems() { bool useDualQuaternionSkinning = self->getUseDualQuaternionSkinning(); transaction.updateItem(itemID, [modelTransform, meshState, useDualQuaternionSkinning, cauterizedMeshState, invalidatePayloadShapeKey, - isWireframe, renderItemKeyGlobalFlags, enableCauterization](ModelMeshPartPayload& mmppData) { + primitiveMode, renderItemKeyGlobalFlags, enableCauterization](ModelMeshPartPayload& mmppData) { CauterizedMeshPartPayload& data = static_cast(mmppData); if (useDualQuaternionSkinning) { data.updateClusterBuffer(meshState.clusterDualQuaternions, @@ -276,7 +276,7 @@ void CauterizedModel::updateRenderItems() { data.setEnableCauterization(enableCauterization); data.updateKey(renderItemKeyGlobalFlags); - data.setShapeKey(invalidatePayloadShapeKey, isWireframe, useDualQuaternionSkinning); + data.setShapeKey(invalidatePayloadShapeKey, primitiveMode, useDualQuaternionSkinning); }); } diff --git a/libraries/render-utils/src/GeometryCache.cpp b/libraries/render-utils/src/GeometryCache.cpp index d6d6f4903e..02f51e558a 100644 --- a/libraries/render-utils/src/GeometryCache.cpp +++ b/libraries/render-utils/src/GeometryCache.cpp @@ -721,11 +721,16 @@ QHash GeometryCache::_simplePrograms; gpu::ShaderPointer GeometryCache::_simpleShader; gpu::ShaderPointer GeometryCache::_transparentShader; gpu::ShaderPointer GeometryCache::_unlitShader; +gpu::ShaderPointer GeometryCache::_forwardSimpleShader; +gpu::ShaderPointer GeometryCache::_forwardTransparentShader; +gpu::ShaderPointer GeometryCache::_forwardUnlitShader; gpu::ShaderPointer GeometryCache::_simpleFadeShader; gpu::ShaderPointer GeometryCache::_unlitFadeShader; render::ShapePipelinePointer GeometryCache::_simpleOpaquePipeline; render::ShapePipelinePointer GeometryCache::_simpleTransparentPipeline; +render::ShapePipelinePointer GeometryCache::_forwardSimpleOpaquePipeline; +render::ShapePipelinePointer GeometryCache::_forwardSimpleTransparentPipeline; render::ShapePipelinePointer GeometryCache::_simpleOpaqueFadePipeline; render::ShapePipelinePointer GeometryCache::_simpleTransparentFadePipeline; render::ShapePipelinePointer GeometryCache::_simpleWirePipeline; @@ -805,6 +810,8 @@ void GeometryCache::initializeShapePipelines() { if (!_simpleOpaquePipeline) { _simpleOpaquePipeline = getShapePipeline(false, false, true, false); _simpleTransparentPipeline = getShapePipeline(false, true, true, false); + _forwardSimpleOpaquePipeline = getShapePipeline(false, false, true, false, false, true); + _forwardSimpleTransparentPipeline = getShapePipeline(false, true, true, false, false, true); _simpleOpaqueFadePipeline = getFadingShapePipeline(false, false, false, false, false); _simpleTransparentFadePipeline = getFadingShapePipeline(false, true, false, false, false); _simpleWirePipeline = getShapePipeline(false, false, true, true); @@ -812,9 +819,9 @@ void GeometryCache::initializeShapePipelines() { } render::ShapePipelinePointer GeometryCache::getShapePipeline(bool textured, bool transparent, bool culled, - bool unlit, bool depthBias) { + bool unlit, bool depthBias, bool forward) { - return std::make_shared(getSimplePipeline(textured, transparent, culled, unlit, depthBias, false, true), nullptr, + return std::make_shared(getSimplePipeline(textured, transparent, culled, unlit, depthBias, false, true, forward), nullptr, [](const render::ShapePipeline& pipeline, gpu::Batch& batch, render::Args* args) { batch.setResourceTexture(gr::Texture::MaterialAlbedo, DependencyManager::get()->getWhiteTexture()); DependencyManager::get()->setupKeyLightBatch(args, batch); @@ -2165,6 +2172,7 @@ public: HAS_DEPTH_BIAS_FLAG, IS_FADING_FLAG, IS_ANTIALIASED_FLAG, + IS_FORWARD_FLAG, NUM_FLAGS, }; @@ -2177,6 +2185,7 @@ public: HAS_DEPTH_BIAS = (1 << HAS_DEPTH_BIAS_FLAG), IS_FADING = (1 << IS_FADING_FLAG), IS_ANTIALIASED = (1 << IS_ANTIALIASED_FLAG), + IS_FORWARD = (1 << IS_FORWARD_FLAG), }; typedef unsigned short Flags; @@ -2189,6 +2198,7 @@ public: bool hasDepthBias() const { return isFlag(HAS_DEPTH_BIAS); } bool isFading() const { return isFlag(IS_FADING); } bool isAntiAliased() const { return isFlag(IS_ANTIALIASED); } + bool isForward() const { return isFlag(IS_FORWARD); } Flags _flags = 0; #if defined(__clang__) @@ -2200,9 +2210,9 @@ public: SimpleProgramKey(bool textured = false, bool transparent = false, bool culled = true, - bool unlit = false, bool depthBias = false, bool fading = false, bool isAntiAliased = true) { + bool unlit = false, bool depthBias = false, bool fading = false, bool isAntiAliased = true, bool forward = false) { _flags = (textured ? IS_TEXTURED : 0) | (transparent ? IS_TRANSPARENT : 0) | (culled ? IS_CULLED : 0) | - (unlit ? IS_UNLIT : 0) | (depthBias ? HAS_DEPTH_BIAS : 0) | (fading ? IS_FADING : 0) | (isAntiAliased ? IS_ANTIALIASED : 0); + (unlit ? IS_UNLIT : 0) | (depthBias ? HAS_DEPTH_BIAS : 0) | (fading ? IS_FADING : 0) | (isAntiAliased ? IS_ANTIALIASED : 0) | (forward ? IS_FORWARD : 0); } SimpleProgramKey(int bitmask) : _flags(bitmask) {} @@ -2255,8 +2265,8 @@ void GeometryCache::bindSimpleProgram(gpu::Batch& batch, bool textured, bool tra } } -gpu::PipelinePointer GeometryCache::getSimplePipeline(bool textured, bool transparent, bool culled, bool unlit, bool depthBiased, bool fading, bool isAntiAliased) { - SimpleProgramKey config { textured, transparent, culled, unlit, depthBiased, fading, isAntiAliased }; +gpu::PipelinePointer GeometryCache::getSimplePipeline(bool textured, bool transparent, bool culled, bool unlit, bool depthBiased, bool fading, bool isAntiAliased, bool forward) { + SimpleProgramKey config { textured, transparent, culled, unlit, depthBiased, fading, isAntiAliased, forward }; // If the pipeline already exists, return it auto it = _simplePrograms.find(config); @@ -2269,13 +2279,20 @@ gpu::PipelinePointer GeometryCache::getSimplePipeline(bool textured, bool transp static std::once_flag once; std::call_once(once, [&]() { using namespace shader::render_utils::program; - auto PS = DISABLE_DEFERRED ? forward_simple_textured : simple_textured; - // Use the forward pipeline for both here, otherwise transparents will be unlit - auto PSTransparent = DISABLE_DEFERRED ? forward_simple_textured_transparent : forward_simple_textured_transparent; - auto PSUnlit = DISABLE_DEFERRED ? forward_simple_textured_unlit : simple_textured_unlit; - _simpleShader = gpu::Shader::createProgram(PS); - _transparentShader = gpu::Shader::createProgram(PSTransparent); - _unlitShader = gpu::Shader::createProgram(PSUnlit); + + _forwardSimpleShader = gpu::Shader::createProgram(forward_simple_textured); + _forwardTransparentShader = gpu::Shader::createProgram(forward_simple_textured_transparent); + _forwardUnlitShader = gpu::Shader::createProgram(forward_simple_textured_unlit); + if (DISABLE_DEFERRED) { + _simpleShader = _forwardSimpleShader; + _transparentShader = _forwardTransparentShader; + _unlitShader = _forwardUnlitShader; + } else { + _simpleShader = gpu::Shader::createProgram(simple_textured); + // Use the forward pipeline for both here, otherwise transparents will be unlit + _transparentShader = gpu::Shader::createProgram(forward_simple_textured_transparent); + _unlitShader = gpu::Shader::createProgram(simple_textured_unlit); + } }); } else { static std::once_flag once; @@ -2308,8 +2325,14 @@ gpu::PipelinePointer GeometryCache::getSimplePipeline(bool textured, bool transp PrepareStencil::testMaskDrawShapeNoAA(*state); } - gpu::ShaderPointer program = (config.isUnlit()) ? (config.isFading() ? _unlitFadeShader : _unlitShader) : - (config.isFading() ? _simpleFadeShader : (config.isTransparent() ? _transparentShader : _simpleShader)); + gpu::ShaderPointer program; + if (config.isForward()) { + program = (config.isUnlit()) ? (config.isFading() ? _unlitFadeShader : _forwardUnlitShader) : + (config.isFading() ? _simpleFadeShader : (config.isTransparent() ? _forwardTransparentShader : _forwardSimpleShader)); + } else { + program = (config.isUnlit()) ? (config.isFading() ? _unlitFadeShader : _unlitShader) : + (config.isFading() ? _simpleFadeShader : (config.isTransparent() ? _transparentShader : _simpleShader)); + } gpu::PipelinePointer pipeline = gpu::Pipeline::create(program, state); _simplePrograms.insert(config, pipeline); return pipeline; diff --git a/libraries/render-utils/src/GeometryCache.h b/libraries/render-utils/src/GeometryCache.h index 589be4dcc1..e46bb4a899 100644 --- a/libraries/render-utils/src/GeometryCache.h +++ b/libraries/render-utils/src/GeometryCache.h @@ -174,7 +174,7 @@ public: bool unlit = false, bool depthBias = false, bool isAntiAliased = true); // Get the pipeline to render static geometry static gpu::PipelinePointer getSimplePipeline(bool textured = false, bool transparent = false, bool culled = true, - bool unlit = false, bool depthBias = false, bool fading = false, bool isAntiAliased = true); + bool unlit = false, bool depthBias = false, bool fading = false, bool isAntiAliased = true, bool forward = false); void bindWebBrowserProgram(gpu::Batch& batch, bool transparent = false); gpu::PipelinePointer getWebBrowserProgram(bool transparent); @@ -183,6 +183,8 @@ public: render::ShapePipelinePointer getOpaqueShapePipeline() { assert(_simpleOpaquePipeline != nullptr); return _simpleOpaquePipeline; } render::ShapePipelinePointer getTransparentShapePipeline() { assert(_simpleTransparentPipeline != nullptr); return _simpleTransparentPipeline; } + render::ShapePipelinePointer getForwardOpaqueShapePipeline() { assert(_forwardSimpleOpaquePipeline != nullptr); return _forwardSimpleOpaquePipeline; } + render::ShapePipelinePointer getForwardTransparentShapePipeline() { assert(_forwardSimpleTransparentPipeline != nullptr); return _forwardSimpleTransparentPipeline; } render::ShapePipelinePointer getOpaqueFadeShapePipeline() { assert(_simpleOpaqueFadePipeline != nullptr); return _simpleOpaqueFadePipeline; } render::ShapePipelinePointer getTransparentFadeShapePipeline() { assert(_simpleTransparentFadePipeline != nullptr); return _simpleTransparentFadePipeline; } render::ShapePipelinePointer getOpaqueShapePipeline(bool isFading); @@ -465,13 +467,19 @@ private: QHash _lastRegisteredGridBuffer; QHash _registeredGridBuffers; + // FIXME: clean these up static gpu::ShaderPointer _simpleShader; static gpu::ShaderPointer _transparentShader; static gpu::ShaderPointer _unlitShader; + static gpu::ShaderPointer _forwardSimpleShader; + static gpu::ShaderPointer _forwardTransparentShader; + static gpu::ShaderPointer _forwardUnlitShader; static gpu::ShaderPointer _simpleFadeShader; static gpu::ShaderPointer _unlitFadeShader; static render::ShapePipelinePointer _simpleOpaquePipeline; static render::ShapePipelinePointer _simpleTransparentPipeline; + static render::ShapePipelinePointer _forwardSimpleOpaquePipeline; + static render::ShapePipelinePointer _forwardSimpleTransparentPipeline; static render::ShapePipelinePointer _simpleOpaqueFadePipeline; static render::ShapePipelinePointer _simpleTransparentFadePipeline; static render::ShapePipelinePointer _simpleWirePipeline; @@ -485,7 +493,7 @@ private: gpu::PipelinePointer _simpleTransparentWebBrowserPipeline; static render::ShapePipelinePointer getShapePipeline(bool textured = false, bool transparent = false, bool culled = true, - bool unlit = false, bool depthBias = false); + bool unlit = false, bool depthBias = false, bool forward = false); static render::ShapePipelinePointer getFadingShapePipeline(bool textured = false, bool transparent = false, bool culled = true, bool unlit = false, bool depthBias = false); }; diff --git a/libraries/render-utils/src/MeshPartPayload.cpp b/libraries/render-utils/src/MeshPartPayload.cpp index ca2e56862d..bb2e784807 100644 --- a/libraries/render-utils/src/MeshPartPayload.cpp +++ b/libraries/render-utils/src/MeshPartPayload.cpp @@ -49,8 +49,6 @@ template <> void payloadRender(const MeshPartPayload::Pointer& payload, RenderAr } } -const graphics::MaterialPointer MeshPartPayload::DEFAULT_MATERIAL = std::make_shared(); - MeshPartPayload::MeshPartPayload(const std::shared_ptr& mesh, int partIndex, graphics::MaterialPointer material) { updateMeshPart(mesh, partIndex); addMaterial(graphics::MaterialLayer(material, 0)); @@ -85,11 +83,13 @@ void MeshPartPayload::updateKey(const render::ItemKey& key) { ItemKey::Builder builder(key); builder.withTypeShape(); - if (topMaterialExists()) { - auto matKey = _drawMaterials.top().material->getKey(); - if (matKey.isTranslucent()) { - builder.withTransparent(); - } + if (_drawMaterials.needsUpdate()) { + RenderPipelines::updateMultiMaterial(_drawMaterials); + } + + auto matKey = _drawMaterials.getMaterialKey(); + if (matKey.isTranslucent()) { + builder.withTransparent(); } _itemKey = builder.build(); @@ -104,10 +104,7 @@ Item::Bound MeshPartPayload::getBound() const { } ShapeKey MeshPartPayload::getShapeKey() const { - graphics::MaterialKey drawMaterialKey; - if (topMaterialExists()) { - drawMaterialKey = _drawMaterials.top().material->getKey(); - } + graphics::MaterialKey drawMaterialKey = _drawMaterials.getMaterialKey(); ShapeKey::Builder builder; builder.withMaterial(); @@ -158,7 +155,7 @@ void MeshPartPayload::render(RenderArgs* args) { // apply material properties if (args->_renderMode != render::Args::RenderMode::SHADOW_RENDER_MODE) { - RenderPipelines::bindMaterial(!_drawMaterials.empty() ? _drawMaterials.top().material : DEFAULT_MATERIAL, batch, args->_enableTexturing); + RenderPipelines::bindMaterials(_drawMaterials, batch, args->_enableTexturing); args->_details._materialSwitches++; } @@ -332,33 +329,37 @@ void ModelMeshPartPayload::updateKey(const render::ItemKey& key) { builder.withDeformed(); } - if (topMaterialExists()) { - auto matKey = _drawMaterials.top().material->getKey(); - if (matKey.isTranslucent()) { - builder.withTransparent(); - } + if (_drawMaterials.needsUpdate()) { + RenderPipelines::updateMultiMaterial(_drawMaterials); + } + + auto matKey = _drawMaterials.getMaterialKey(); + if (matKey.isTranslucent()) { + builder.withTransparent(); } _itemKey = builder.build(); } -void ModelMeshPartPayload::setShapeKey(bool invalidateShapeKey, bool isWireframe, bool useDualQuaternionSkinning) { +void ModelMeshPartPayload::setShapeKey(bool invalidateShapeKey, PrimitiveMode primitiveMode, bool useDualQuaternionSkinning) { if (invalidateShapeKey) { _shapeKey = ShapeKey::Builder::invalid(); return; } - graphics::MaterialKey drawMaterialKey; - if (topMaterialExists()) { - drawMaterialKey = _drawMaterials.top().material->getKey(); + if (_drawMaterials.needsUpdate()) { + RenderPipelines::updateMultiMaterial(_drawMaterials); } + graphics::MaterialKey drawMaterialKey = _drawMaterials.getMaterialKey(); + bool isTranslucent = drawMaterialKey.isTranslucent(); bool hasTangents = drawMaterialKey.isNormalMap() && _hasTangents; bool hasLightmap = drawMaterialKey.isLightmapMap(); bool isUnlit = drawMaterialKey.isUnlit(); bool isDeformed = _isBlendShaped || _isSkinned; + bool isWireframe = primitiveMode == PrimitiveMode::LINES; if (isWireframe) { isTranslucent = hasTangents = hasLightmap = false; @@ -434,7 +435,7 @@ void ModelMeshPartPayload::render(RenderArgs* args) { // apply material properties if (args->_renderMode != render::Args::RenderMode::SHADOW_RENDER_MODE) { - RenderPipelines::bindMaterial(!_drawMaterials.empty() ? _drawMaterials.top().material : DEFAULT_MATERIAL, batch, args->_enableTexturing); + RenderPipelines::bindMaterials(_drawMaterials, batch, args->_enableTexturing); args->_details._materialSwitches++; } diff --git a/libraries/render-utils/src/MeshPartPayload.h b/libraries/render-utils/src/MeshPartPayload.h index 29c0091f11..deae91dda9 100644 --- a/libraries/render-utils/src/MeshPartPayload.h +++ b/libraries/render-utils/src/MeshPartPayload.h @@ -66,18 +66,15 @@ public: graphics::Mesh::Part _drawPart; size_t getVerticesCount() const { return _drawMesh ? _drawMesh->getNumVertices() : 0; } - size_t getMaterialTextureSize() { return topMaterialExists() ? _drawMaterials.top().material->getTextureSize() : 0; } - int getMaterialTextureCount() { return topMaterialExists() ? _drawMaterials.top().material->getTextureCount() : 0; } - bool hasTextureInfo() const { return topMaterialExists() ? _drawMaterials.top().material->hasTextureInfo() : false; } + size_t getMaterialTextureSize() { return _drawMaterials.getTextureSize(); } + int getMaterialTextureCount() { return _drawMaterials.getTextureCount(); } + bool hasTextureInfo() const { return _drawMaterials.hasTextureInfo(); } void addMaterial(graphics::MaterialLayer material); void removeMaterial(graphics::MaterialPointer material); protected: - static const graphics::MaterialPointer DEFAULT_MATERIAL; render::ItemKey _itemKey{ render::ItemKey::Builder::opaqueShape().build() }; - - bool topMaterialExists() const { return !_drawMaterials.empty() && _drawMaterials.top().material; } }; namespace render { @@ -109,7 +106,7 @@ public: render::ShapeKey getShapeKey() const override; // shape interface void render(RenderArgs* args) override; - void setShapeKey(bool invalidateShapeKey, bool isWireframe, bool useDualQuaternionSkinning); + void setShapeKey(bool invalidateShapeKey, PrimitiveMode primitiveMode, bool useDualQuaternionSkinning); // ModelMeshPartPayload functions to perform render void bindMesh(gpu::Batch& batch) override; diff --git a/libraries/render-utils/src/Model.cpp b/libraries/render-utils/src/Model.cpp index ec29fb009e..da8dceb176 100644 --- a/libraries/render-utils/src/Model.cpp +++ b/libraries/render-utils/src/Model.cpp @@ -62,7 +62,6 @@ Model::Model(QObject* parent, SpatiallyNestable* spatiallyNestableOverride) : _snapModelToRegistrationPoint(false), _snappedToRegistrationPoint(false), _url(HTTP_INVALID_COM), - _isWireframe(false), _renderItemKeyGlobalFlags(render::ItemKey::Builder().withVisible().withTagBits(render::hifi::TAG_ALL_VIEWS).build()) { // we may have been created in the network thread, but we live in the main thread @@ -223,7 +222,7 @@ void Model::updateRenderItems() { Transform modelTransform = self->getTransform(); modelTransform.setScale(glm::vec3(1.0f)); - bool isWireframe = self->isWireframe(); + PrimitiveMode primitiveMode = self->getPrimitiveMode(); auto renderItemKeyGlobalFlags = self->getRenderItemKeyGlobalFlags(); render::Transaction transaction; @@ -238,7 +237,7 @@ void Model::updateRenderItems() { bool useDualQuaternionSkinning = self->getUseDualQuaternionSkinning(); transaction.updateItem(itemID, [modelTransform, meshState, useDualQuaternionSkinning, - invalidatePayloadShapeKey, isWireframe, renderItemKeyGlobalFlags](ModelMeshPartPayload& data) { + invalidatePayloadShapeKey, primitiveMode, renderItemKeyGlobalFlags](ModelMeshPartPayload& data) { if (useDualQuaternionSkinning) { data.updateClusterBuffer(meshState.clusterDualQuaternions); } else { @@ -263,7 +262,7 @@ void Model::updateRenderItems() { data.updateTransformForSkinnedMesh(renderTransform, modelTransform); data.updateKey(renderItemKeyGlobalFlags); - data.setShapeKey(invalidatePayloadShapeKey, isWireframe, useDualQuaternionSkinning); + data.setShapeKey(invalidatePayloadShapeKey, primitiveMode, useDualQuaternionSkinning); }); } @@ -276,6 +275,11 @@ void Model::setRenderItemsNeedUpdate() { emit requestRenderUpdate(); } +void Model::setPrimitiveMode(PrimitiveMode primitiveMode) { + _primitiveMode = primitiveMode; + setRenderItemsNeedUpdate(); +} + void Model::reset() { if (isLoaded()) { const HFMModel& hfmModel = getHFMModel(); @@ -878,30 +882,14 @@ bool Model::canCastShadow() const { return _renderItemKeyGlobalFlags.isShadowCaster(); } -void Model::setLayeredInFront(bool layeredInFront, const render::ScenePointer& scene) { - if (Model::isLayeredInFront() != layeredInFront) { +void Model::setHifiRenderLayer(render::hifi::Layer renderLayer, const render::ScenePointer& scene) { + if (_renderItemKeyGlobalFlags.getLayer() != renderLayer) { auto keyBuilder = render::ItemKey::Builder(_renderItemKeyGlobalFlags); - _renderItemKeyGlobalFlags = (layeredInFront ? keyBuilder.withLayer(render::hifi::LAYER_3D_FRONT) : keyBuilder.withoutLayer()); + _renderItemKeyGlobalFlags = keyBuilder.withLayer(renderLayer); updateRenderItemsKey(scene); } } -bool Model::isLayeredInFront() const { - return _renderItemKeyGlobalFlags.isLayer(render::hifi::LAYER_3D_FRONT); -} - -void Model::setLayeredInHUD(bool layeredInHUD, const render::ScenePointer& scene) { - if (Model::isLayeredInHUD() != layeredInHUD) { - auto keyBuilder = render::ItemKey::Builder(_renderItemKeyGlobalFlags); - _renderItemKeyGlobalFlags = (layeredInHUD ? keyBuilder.withLayer(render::hifi::LAYER_3D_HUD) : keyBuilder.withoutLayer()); - updateRenderItemsKey(scene); - } -} - -bool Model::isLayeredInHUD() const { - return _renderItemKeyGlobalFlags.isLayer(render::hifi::LAYER_3D_HUD); -} - void Model::setTagMask(uint8_t mask, const render::ScenePointer& scene) { if (Model::getTagMask() != mask) { auto keyBuilder = render::ItemKey::Builder(_renderItemKeyGlobalFlags); @@ -920,6 +908,7 @@ void Model::setGroupCulled(bool groupCulled, const render::ScenePointer& scene) updateRenderItemsKey(scene); } } + bool Model::isGroupCulled() const { return _renderItemKeyGlobalFlags.isSubMetaCulled(); } @@ -1486,44 +1475,71 @@ bool Model::isRenderable() const { return !_meshStates.empty() || (isLoaded() && _renderGeometry->getMeshes().empty()); } -std::vector Model::getMeshIDsFromMaterialID(QString parentMaterialName) { - // try to find all meshes with materials that match parentMaterialName as a string - // if none, return parentMaterialName as a uint - std::vector toReturn; - const QString MATERIAL_NAME_PREFIX = "mat::"; - if (parentMaterialName.startsWith(MATERIAL_NAME_PREFIX)) { - parentMaterialName.replace(0, MATERIAL_NAME_PREFIX.size(), QString("")); - for (unsigned int i = 0; i < (unsigned int)_modelMeshMaterialNames.size(); i++) { - if (_modelMeshMaterialNames[i] == parentMaterialName.toStdString()) { - toReturn.push_back(i); - } - } - } +std::set Model::getMeshIDsFromMaterialID(QString parentMaterialName) { + std::set toReturn; - if (toReturn.empty()) { - toReturn.push_back(parentMaterialName.toUInt()); + const QString all("all"); + if (parentMaterialName == all) { + for (unsigned int i = 0; i < (unsigned int)_modelMeshRenderItemIDs.size(); i++) { + toReturn.insert(i); + } + } else if (!parentMaterialName.isEmpty()) { + auto parseFunc = [this, &toReturn] (QString& target) { + if (target.isEmpty()) { + return; + } + // if target starts with "mat::", try to find all meshes with materials that match target as a string + // otherwise, return target as a uint + const QString MATERIAL_NAME_PREFIX("mat::"); + if (target.startsWith(MATERIAL_NAME_PREFIX)) { + std::string targetStdString = target.replace(0, MATERIAL_NAME_PREFIX.size(), "").toStdString(); + for (unsigned int i = 0; i < (unsigned int)_modelMeshMaterialNames.size(); i++) { + if (_modelMeshMaterialNames[i] == targetStdString) { + toReturn.insert(i); + } + } + return; + } + toReturn.insert(target.toUInt()); + }; + + if (parentMaterialName.length() > 2 && parentMaterialName.startsWith("[") && parentMaterialName.endsWith("]")) { + QStringList list = parentMaterialName.split(",", QString::SkipEmptyParts); + for (int i = 0; i < list.length(); i++) { + auto& target = list[i]; + if (i == 0) { + target = target.replace(0, 1, ""); + } + if (i == list.length() - 1) { + target = target.replace(target.length() - 1, 1, ""); + } + parseFunc(target); + } + } else { + parseFunc(parentMaterialName); + } } return toReturn; } void Model::addMaterial(graphics::MaterialLayer material, const std::string& parentMaterialName) { - std::vector shapeIDs = getMeshIDsFromMaterialID(QString(parentMaterialName.c_str())); + std::set shapeIDs = getMeshIDsFromMaterialID(QString(parentMaterialName.c_str())); render::Transaction transaction; for (auto shapeID : shapeIDs) { if (shapeID < _modelMeshRenderItemIDs.size()) { auto itemID = _modelMeshRenderItemIDs[shapeID]; auto renderItemsKey = _renderItemKeyGlobalFlags; - bool wireframe = isWireframe(); + PrimitiveMode primitiveMode = getPrimitiveMode(); auto meshIndex = _modelMeshRenderItemShapes[shapeID].meshIndex; bool invalidatePayloadShapeKey = shouldInvalidatePayloadShapeKey(meshIndex); bool useDualQuaternionSkinning = _useDualQuaternionSkinning; transaction.updateItem(itemID, [material, renderItemsKey, - invalidatePayloadShapeKey, wireframe, useDualQuaternionSkinning](ModelMeshPartPayload& data) { + invalidatePayloadShapeKey, primitiveMode, useDualQuaternionSkinning](ModelMeshPartPayload& data) { data.addMaterial(material); // if the material changed, we might need to update our item key or shape key data.updateKey(renderItemsKey); - data.setShapeKey(invalidatePayloadShapeKey, wireframe, useDualQuaternionSkinning); + data.setShapeKey(invalidatePayloadShapeKey, primitiveMode, useDualQuaternionSkinning); }); } } @@ -1531,22 +1547,22 @@ void Model::addMaterial(graphics::MaterialLayer material, const std::string& par } void Model::removeMaterial(graphics::MaterialPointer material, const std::string& parentMaterialName) { - std::vector shapeIDs = getMeshIDsFromMaterialID(QString(parentMaterialName.c_str())); + std::set shapeIDs = getMeshIDsFromMaterialID(QString(parentMaterialName.c_str())); render::Transaction transaction; for (auto shapeID : shapeIDs) { if (shapeID < _modelMeshRenderItemIDs.size()) { auto itemID = _modelMeshRenderItemIDs[shapeID]; auto renderItemsKey = _renderItemKeyGlobalFlags; - bool wireframe = isWireframe(); + PrimitiveMode primitiveMode = getPrimitiveMode(); auto meshIndex = _modelMeshRenderItemShapes[shapeID].meshIndex; bool invalidatePayloadShapeKey = shouldInvalidatePayloadShapeKey(meshIndex); bool useDualQuaternionSkinning = _useDualQuaternionSkinning; transaction.updateItem(itemID, [material, renderItemsKey, - invalidatePayloadShapeKey, wireframe, useDualQuaternionSkinning](ModelMeshPartPayload& data) { + invalidatePayloadShapeKey, primitiveMode, useDualQuaternionSkinning](ModelMeshPartPayload& data) { data.removeMaterial(material); // if the material changed, we might need to update our item key or shape key data.updateKey(renderItemsKey); - data.setShapeKey(invalidatePayloadShapeKey, wireframe, useDualQuaternionSkinning); + data.setShapeKey(invalidatePayloadShapeKey, primitiveMode, useDualQuaternionSkinning); }); } } diff --git a/libraries/render-utils/src/Model.h b/libraries/render-utils/src/Model.h index 0f8eb782c3..16e08c2b23 100644 --- a/libraries/render-utils/src/Model.h +++ b/libraries/render-utils/src/Model.h @@ -37,6 +37,7 @@ #include "GeometryCache.h" #include "TextureCache.h" #include "Rig.h" +#include "PrimitiveMode.h" // Use dual quaternion skinning! // Must match define in Skinning.slh @@ -123,11 +124,7 @@ public: bool canCastShadow() const; void setCanCastShadow(bool canCastShadow, const render::ScenePointer& scene = nullptr); - void setLayeredInFront(bool isLayeredInFront, const render::ScenePointer& scene = nullptr); - void setLayeredInHUD(bool isLayeredInHUD, const render::ScenePointer& scene = nullptr); - - bool isLayeredInFront() const; - bool isLayeredInHUD() const; + void setHifiRenderLayer(render::hifi::Layer layer, const render::ScenePointer& scene = nullptr); // Access the current RenderItemKey Global Flags used by the model and applied to the render items representing the parts of the model. const render::ItemKey getRenderItemKeyGlobalFlags() const; @@ -166,8 +163,8 @@ public: bool isLoaded() const { return (bool)_renderGeometry && _renderGeometry->isHFMModelLoaded(); } bool isAddedToScene() const { return _addedToScene; } - void setIsWireframe(bool isWireframe) { _isWireframe = isWireframe; } - bool isWireframe() const { return _isWireframe; } + void setPrimitiveMode(PrimitiveMode primitiveMode); + PrimitiveMode getPrimitiveMode() const { return _primitiveMode; } void reset(); @@ -455,7 +452,7 @@ protected: virtual void createRenderItemSet(); - bool _isWireframe; + PrimitiveMode _primitiveMode { PrimitiveMode::SOLID }; bool _useDualQuaternionSkinning { false }; // debug rendering support @@ -513,7 +510,7 @@ private: void calculateTextureInfo(); - std::vector getMeshIDsFromMaterialID(QString parentMaterialName); + std::set getMeshIDsFromMaterialID(QString parentMaterialName); }; Q_DECLARE_METATYPE(ModelPointer) diff --git a/libraries/render-utils/src/RenderPipelines.cpp b/libraries/render-utils/src/RenderPipelines.cpp index cd685a54a1..07dc683719 100644 --- a/libraries/render-utils/src/RenderPipelines.cpp +++ b/libraries/render-utils/src/RenderPipelines.cpp @@ -308,24 +308,33 @@ void addPlumberPipeline(ShapePlumber& plumber, void batchSetter(const ShapePipeline& pipeline, gpu::Batch& batch, RenderArgs* args) { // Set a default albedo map - batch.setResourceTexture(gr::Texture::MaterialAlbedo, - DependencyManager::get()->getWhiteTexture()); + batch.setResourceTexture(gr::Texture::MaterialAlbedo, DependencyManager::get()->getWhiteTexture()); // Set a default material if (pipeline.locations->materialBufferUnit) { // Create a default schema - static bool isMaterialSet = false; - static graphics::Material material; - if (!isMaterialSet) { - material.setAlbedo(vec3(1.0f)); - material.setOpacity(1.0f); - material.setMetallic(0.1f); - material.setRoughness(0.9f); - isMaterialSet = true; - } + static gpu::BufferView schemaBuffer; + static std::once_flag once; + std::call_once(once, [] { + graphics::MultiMaterial::Schema schema; + graphics::MaterialKey schemaKey; - // Set a default schema - batch.setUniformBuffer(gr::Buffer::Material, material.getSchemaBuffer()); + schema._albedo = vec3(1.0f); + schema._opacity = 1.0f; + schema._metallic = 0.1f; + schema._roughness = 0.9f; + + schemaKey.setAlbedo(true); + schemaKey.setTranslucentFactor(false); + schemaKey.setMetallic(true); + schemaKey.setGlossy(true); + schema._key = (uint32_t)schemaKey._flags.to_ulong(); + + auto schemaSize = sizeof(graphics::MultiMaterial::Schema); + schemaBuffer = gpu::BufferView(std::make_shared(schemaSize, (const gpu::Byte*) &schema, schemaSize)); + }); + + batch.setUniformBuffer(gr::Buffer::Material, schemaBuffer); } } @@ -364,103 +373,400 @@ void initZPassPipelines(ShapePlumber& shapePlumber, gpu::StatePointer state, con gpu::Shader::createProgram(deformed_model_shadow_fade_dq), state, extraBatchSetter, itemSetter); } -// FIXME find a better way to setup the default textures -void RenderPipelines::bindMaterial(const graphics::MaterialPointer& material, gpu::Batch& batch, bool enableTextures) { - if (!material) { +void RenderPipelines::bindMaterial(graphics::MaterialPointer& material, gpu::Batch& batch, bool enableTextures) { + graphics::MultiMaterial multiMaterial; + multiMaterial.push(graphics::MaterialLayer(material, 0)); + bindMaterials(multiMaterial, batch, enableTextures); +} + +void RenderPipelines::updateMultiMaterial(graphics::MultiMaterial& multiMaterial) { + auto& schemaBuffer = multiMaterial.getSchemaBuffer(); + + if (multiMaterial.size() == 0) { + schemaBuffer.edit() = graphics::MultiMaterial::Schema(); return; } + auto& drawMaterialTextures = multiMaterial.getTextureTable(); + multiMaterial.setTexturesLoading(false); + + // The total list of things we need to look for + static std::set allFlags; + static std::once_flag once; + std::call_once(once, [] { + for (int i = 0; i < graphics::Material::NUM_TOTAL_FLAGS; i++) { + // The opacity mask/map are derived from the albedo map + if (i != graphics::MaterialKey::OPACITY_MASK_MAP_BIT && + i != graphics::MaterialKey::OPACITY_TRANSLUCENT_MAP_BIT) { + allFlags.insert(i); + } + } + }); + + graphics::MultiMaterial materials = multiMaterial; + graphics::MultiMaterial::Schema schema; + graphics::MaterialKey schemaKey; + + std::set flagsToCheck = allFlags; + std::set flagsToSetDefault; + + while (!materials.empty()) { + auto material = materials.top().material; + if (!material) { + break; + } + materials.pop(); + + bool defaultFallthrough = material->getDefaultFallthrough(); + const auto& materialKey = material->getKey(); + const auto& textureMaps = material->getTextureMaps(); + + auto it = flagsToCheck.begin(); + while (it != flagsToCheck.end()) { + auto flag = *it; + bool fallthrough = defaultFallthrough || material->getPropertyFallthrough(flag); + + bool wasSet = false; + bool forceDefault = false; + switch (flag) { + case graphics::MaterialKey::EMISSIVE_VAL_BIT: + if (materialKey.isEmissive()) { + schema._emissive = material->getEmissive(false); + schemaKey.setEmissive(true); + wasSet = true; + } + break; + case graphics::MaterialKey::UNLIT_VAL_BIT: + if (materialKey.isUnlit()) { + schemaKey.setUnlit(true); + wasSet = true; + } + break; + case graphics::MaterialKey::ALBEDO_VAL_BIT: + if (materialKey.isAlbedo()) { + schema._albedo = material->getAlbedo(false); + schemaKey.setAlbedo(true); + wasSet = true; + } + break; + case graphics::MaterialKey::METALLIC_VAL_BIT: + if (materialKey.isMetallic()) { + schema._metallic = material->getMetallic(); + schemaKey.setMetallic(true); + wasSet = true; + } + break; + case graphics::MaterialKey::GLOSSY_VAL_BIT: + if (materialKey.isRough() || materialKey.isGlossy()) { + schema._roughness = material->getRoughness(); + schemaKey.setGlossy(materialKey.isGlossy()); + wasSet = true; + } + break; + case graphics::MaterialKey::OPACITY_VAL_BIT: + if (materialKey.isTranslucentFactor()) { + schema._opacity = material->getOpacity(); + schemaKey.setTranslucentFactor(true); + wasSet = true; + } + break; + case graphics::MaterialKey::SCATTERING_VAL_BIT: + if (materialKey.isScattering()) { + schema._scattering = material->getScattering(); + schemaKey.setScattering(true); + wasSet = true; + } + break; + case graphics::MaterialKey::ALBEDO_MAP_BIT: + if (materialKey.isAlbedoMap()) { + auto itr = textureMaps.find(graphics::MaterialKey::ALBEDO_MAP); + if (itr != textureMaps.end()) { + if (itr->second->isDefined()) { + drawMaterialTextures->setTexture(gr::Texture::MaterialAlbedo, itr->second->getTextureView()); + wasSet = true; + } else { + multiMaterial.setTexturesLoading(true); + forceDefault = true; + } + } else { + forceDefault = true; + } + schemaKey.setAlbedoMap(true); + schemaKey.setOpacityMaskMap(materialKey.isOpacityMaskMap()); + schemaKey.setTranslucentMap(materialKey.isTranslucentMap()); + } + break; + case graphics::MaterialKey::METALLIC_MAP_BIT: + if (materialKey.isMetallicMap()) { + auto itr = textureMaps.find(graphics::MaterialKey::METALLIC_MAP); + if (itr != textureMaps.end()) { + if (itr->second->isDefined()) { + drawMaterialTextures->setTexture(gr::Texture::MaterialMetallic, itr->second->getTextureView()); + wasSet = true; + } else { + multiMaterial.setTexturesLoading(true); + forceDefault = true; + } + } else { + forceDefault = true; + } + schemaKey.setMetallicMap(true); + } + break; + case graphics::MaterialKey::ROUGHNESS_MAP_BIT: + if (materialKey.isRoughnessMap()) { + auto itr = textureMaps.find(graphics::MaterialKey::ROUGHNESS_MAP); + if (itr != textureMaps.end()) { + if (itr->second->isDefined()) { + drawMaterialTextures->setTexture(gr::Texture::MaterialRoughness, itr->second->getTextureView()); + wasSet = true; + } else { + multiMaterial.setTexturesLoading(true); + forceDefault = true; + } + } else { + forceDefault = true; + } + schemaKey.setRoughnessMap(true); + } + break; + case graphics::MaterialKey::NORMAL_MAP_BIT: + if (materialKey.isNormalMap()) { + auto itr = textureMaps.find(graphics::MaterialKey::NORMAL_MAP); + if (itr != textureMaps.end()) { + if (itr->second->isDefined()) { + drawMaterialTextures->setTexture(gr::Texture::MaterialNormal, itr->second->getTextureView()); + wasSet = true; + } else { + multiMaterial.setTexturesLoading(true); + forceDefault = true; + } + } else { + forceDefault = true; + } + schemaKey.setNormalMap(true); + } + break; + case graphics::MaterialKey::OCCLUSION_MAP_BIT: + if (materialKey.isOcclusionMap()) { + auto itr = textureMaps.find(graphics::MaterialKey::OCCLUSION_MAP); + if (itr != textureMaps.end()) { + if (itr->second->isDefined()) { + drawMaterialTextures->setTexture(gr::Texture::MaterialOcclusion, itr->second->getTextureView()); + wasSet = true; + } else { + multiMaterial.setTexturesLoading(true); + forceDefault = true; + } + } else { + forceDefault = true; + } + schemaKey.setOcclusionMap(true); + } + break; + case graphics::MaterialKey::SCATTERING_MAP_BIT: + if (materialKey.isScatteringMap()) { + auto itr = textureMaps.find(graphics::MaterialKey::SCATTERING_MAP); + if (itr != textureMaps.end()) { + if (itr->second->isDefined()) { + drawMaterialTextures->setTexture(gr::Texture::MaterialScattering, itr->second->getTextureView()); + wasSet = true; + } else { + multiMaterial.setTexturesLoading(true); + forceDefault = true; + } + } else { + forceDefault = true; + } + schemaKey.setScattering(true); + } + break; + case graphics::MaterialKey::EMISSIVE_MAP_BIT: + // Lightmap takes precendence over emissive map for legacy reasons + if (materialKey.isEmissiveMap() && !materialKey.isLightmapMap()) { + auto itr = textureMaps.find(graphics::MaterialKey::EMISSIVE_MAP); + if (itr != textureMaps.end()) { + if (itr->second->isDefined()) { + drawMaterialTextures->setTexture(gr::Texture::MaterialEmissiveLightmap, itr->second->getTextureView()); + wasSet = true; + } else { + multiMaterial.setTexturesLoading(true); + forceDefault = true; + } + } else { + forceDefault = true; + } + schemaKey.setEmissiveMap(true); + } else if (materialKey.isLightmapMap()) { + // We'll set this later when we check the lightmap + wasSet = true; + } + break; + case graphics::MaterialKey::LIGHTMAP_MAP_BIT: + if (materialKey.isLightmapMap()) { + auto itr = textureMaps.find(graphics::MaterialKey::LIGHTMAP_MAP); + if (itr != textureMaps.end()) { + if (itr->second->isDefined()) { + drawMaterialTextures->setTexture(gr::Texture::MaterialEmissiveLightmap, itr->second->getTextureView()); + wasSet = true; + } else { + multiMaterial.setTexturesLoading(true); + forceDefault = true; + } + } else { + forceDefault = true; + } + schemaKey.setLightmapMap(true); + } + break; + case graphics::Material::TEXCOORDTRANSFORM0: + if (!fallthrough) { + schema._texcoordTransforms[0] = material->getTexCoordTransform(0); + wasSet = true; + } + break; + case graphics::Material::TEXCOORDTRANSFORM1: + if (!fallthrough) { + schema._texcoordTransforms[1] = material->getTexCoordTransform(1); + wasSet = true; + } + break; + case graphics::Material::LIGHTMAP_PARAMS: + if (!fallthrough) { + schema._lightmapParams = material->getLightmapParams(); + wasSet = true; + } + break; + case graphics::Material::MATERIAL_PARAMS: + if (!fallthrough) { + schema._materialParams = material->getMaterialParams(); + wasSet = true; + } + break; + default: + break; + } + + if (wasSet) { + flagsToCheck.erase(it++); + } else if (forceDefault || !fallthrough) { + flagsToSetDefault.insert(flag); + flagsToCheck.erase(it++); + } else { + ++it; + } + } + + if (flagsToCheck.empty()) { + break; + } + } + + for (auto flagBit : flagsToCheck) { + flagsToSetDefault.insert(flagBit); + } + + auto textureCache = DependencyManager::get(); + // Handle defaults + for (auto flag : flagsToSetDefault) { + switch (flag) { + case graphics::MaterialKey::EMISSIVE_VAL_BIT: + case graphics::MaterialKey::UNLIT_VAL_BIT: + case graphics::MaterialKey::ALBEDO_VAL_BIT: + case graphics::MaterialKey::METALLIC_VAL_BIT: + case graphics::MaterialKey::GLOSSY_VAL_BIT: + case graphics::MaterialKey::OPACITY_VAL_BIT: + case graphics::MaterialKey::SCATTERING_VAL_BIT: + case graphics::Material::TEXCOORDTRANSFORM0: + case graphics::Material::TEXCOORDTRANSFORM1: + case graphics::Material::LIGHTMAP_PARAMS: + case graphics::Material::MATERIAL_PARAMS: + // these are initialized to the correct default values in Schema() + break; + case graphics::MaterialKey::ALBEDO_MAP_BIT: + if (schemaKey.isAlbedoMap()) { + drawMaterialTextures->setTexture(gr::Texture::MaterialAlbedo, textureCache->getWhiteTexture()); + } + break; + case graphics::MaterialKey::METALLIC_MAP_BIT: + if (schemaKey.isMetallicMap()) { + drawMaterialTextures->setTexture(gr::Texture::MaterialMetallic, textureCache->getBlackTexture()); + } + break; + case graphics::MaterialKey::ROUGHNESS_MAP_BIT: + if (schemaKey.isRoughnessMap()) { + drawMaterialTextures->setTexture(gr::Texture::MaterialRoughness, textureCache->getWhiteTexture()); + } + break; + case graphics::MaterialKey::NORMAL_MAP_BIT: + if (schemaKey.isNormalMap()) { + drawMaterialTextures->setTexture(gr::Texture::MaterialNormal, textureCache->getBlueTexture()); + } + break; + case graphics::MaterialKey::OCCLUSION_MAP_BIT: + if (schemaKey.isOcclusionMap()) { + drawMaterialTextures->setTexture(gr::Texture::MaterialOcclusion, textureCache->getWhiteTexture()); + } + break; + case graphics::MaterialKey::SCATTERING_MAP_BIT: + if (schemaKey.isScatteringMap()) { + drawMaterialTextures->setTexture(gr::Texture::MaterialScattering, textureCache->getWhiteTexture()); + } + break; + case graphics::MaterialKey::EMISSIVE_MAP_BIT: + if (schemaKey.isEmissiveMap() && !schemaKey.isLightmapMap()) { + drawMaterialTextures->setTexture(gr::Texture::MaterialEmissiveLightmap, textureCache->getGrayTexture()); + } + break; + case graphics::MaterialKey::LIGHTMAP_MAP_BIT: + if (schemaKey.isLightmapMap()) { + drawMaterialTextures->setTexture(gr::Texture::MaterialEmissiveLightmap, textureCache->getBlackTexture()); + } + break; + default: + break; + } + } + + schema._key = (uint32_t)schemaKey._flags.to_ulong(); + schemaBuffer.edit() = schema; + multiMaterial.setNeedsUpdate(false); +} + +void RenderPipelines::bindMaterials(graphics::MultiMaterial& multiMaterial, gpu::Batch& batch, bool enableTextures) { + if (multiMaterial.size() == 0) { + return; + } + + if (multiMaterial.needsUpdate() || multiMaterial.areTexturesLoading()) { + updateMultiMaterial(multiMaterial); + } + auto textureCache = DependencyManager::get(); - batch.setUniformBuffer(gr::Buffer::Material, material->getSchemaBuffer()); + static gpu::TextureTablePointer defaultMaterialTextures = std::make_shared(); + static std::once_flag once; + std::call_once(once, [textureCache] { + defaultMaterialTextures->setTexture(gr::Texture::MaterialAlbedo, textureCache->getWhiteTexture()); + defaultMaterialTextures->setTexture(gr::Texture::MaterialMetallic, textureCache->getBlackTexture()); + defaultMaterialTextures->setTexture(gr::Texture::MaterialRoughness, textureCache->getWhiteTexture()); + defaultMaterialTextures->setTexture(gr::Texture::MaterialNormal, textureCache->getBlueTexture()); + defaultMaterialTextures->setTexture(gr::Texture::MaterialOcclusion, textureCache->getWhiteTexture()); + defaultMaterialTextures->setTexture(gr::Texture::MaterialScattering, textureCache->getWhiteTexture()); + // MaterialEmissiveLightmap has to be set later + }); - const auto& materialKey = material->getKey(); - const auto& textureMaps = material->getTextureMaps(); - - int numUnlit = 0; - if (materialKey.isUnlit()) { - numUnlit++; - } - - const auto& drawMaterialTextures = material->getTextureTable(); - - // Albedo - if (materialKey.isAlbedoMap()) { - auto itr = textureMaps.find(graphics::MaterialKey::ALBEDO_MAP); - if (enableTextures && itr != textureMaps.end() && itr->second->isDefined()) { - drawMaterialTextures->setTexture(gr::Texture::MaterialAlbedo, itr->second->getTextureView()); - } else { - drawMaterialTextures->setTexture(gr::Texture::MaterialAlbedo, textureCache->getWhiteTexture()); + auto& schemaBuffer = multiMaterial.getSchemaBuffer(); + batch.setUniformBuffer(gr::Buffer::Material, schemaBuffer); + if (enableTextures) { + batch.setResourceTextureTable(multiMaterial.getTextureTable()); + } else { + auto key = multiMaterial.getMaterialKey(); + if (key.isLightmapMap()) { + defaultMaterialTextures->setTexture(gr::Texture::MaterialEmissiveLightmap, textureCache->getBlackTexture()); + } else if (key.isEmissiveMap()) { + defaultMaterialTextures->setTexture(gr::Texture::MaterialEmissiveLightmap, textureCache->getGrayTexture()); } + batch.setResourceTextureTable(defaultMaterialTextures); } - - // Roughness map - if (materialKey.isRoughnessMap()) { - auto itr = textureMaps.find(graphics::MaterialKey::ROUGHNESS_MAP); - if (enableTextures && itr != textureMaps.end() && itr->second->isDefined()) { - drawMaterialTextures->setTexture(gr::Texture::MaterialRoughness, itr->second->getTextureView()); - } else { - drawMaterialTextures->setTexture(gr::Texture::MaterialRoughness, textureCache->getWhiteTexture()); - } - } - - // Normal map - if (materialKey.isNormalMap()) { - auto itr = textureMaps.find(graphics::MaterialKey::NORMAL_MAP); - if (enableTextures && itr != textureMaps.end() && itr->second->isDefined()) { - drawMaterialTextures->setTexture(gr::Texture::MaterialNormal, itr->second->getTextureView()); - } else { - drawMaterialTextures->setTexture(gr::Texture::MaterialNormal, textureCache->getBlueTexture()); - } - } - - // Metallic map - if (materialKey.isMetallicMap()) { - auto itr = textureMaps.find(graphics::MaterialKey::METALLIC_MAP); - if (enableTextures && itr != textureMaps.end() && itr->second->isDefined()) { - drawMaterialTextures->setTexture(gr::Texture::MaterialMetallic, itr->second->getTextureView()); - } else { - drawMaterialTextures->setTexture(gr::Texture::MaterialMetallic, textureCache->getBlackTexture()); - } - } - - // Occlusion map - if (materialKey.isOcclusionMap()) { - auto itr = textureMaps.find(graphics::MaterialKey::OCCLUSION_MAP); - if (enableTextures && itr != textureMaps.end() && itr->second->isDefined()) { - drawMaterialTextures->setTexture(gr::Texture::MaterialOcclusion, itr->second->getTextureView()); - } else { - drawMaterialTextures->setTexture(gr::Texture::MaterialOcclusion, textureCache->getWhiteTexture()); - } - } - - // Scattering map - if (materialKey.isScatteringMap()) { - auto itr = textureMaps.find(graphics::MaterialKey::SCATTERING_MAP); - if (enableTextures && itr != textureMaps.end() && itr->second->isDefined()) { - drawMaterialTextures->setTexture(gr::Texture::MaterialScattering, itr->second->getTextureView()); - } else { - drawMaterialTextures->setTexture(gr::Texture::MaterialScattering, textureCache->getWhiteTexture()); - } - } - - // Emissive / Lightmap - if (materialKey.isLightmapMap()) { - auto itr = textureMaps.find(graphics::MaterialKey::LIGHTMAP_MAP); - - if (enableTextures && itr != textureMaps.end() && itr->second->isDefined()) { - drawMaterialTextures->setTexture(gr::Texture::MaterialEmissiveLightmap, itr->second->getTextureView()); - } else { - drawMaterialTextures->setTexture(gr::Texture::MaterialEmissiveLightmap, textureCache->getGrayTexture()); - } - } else if (materialKey.isEmissiveMap()) { - auto itr = textureMaps.find(graphics::MaterialKey::EMISSIVE_MAP); - if (enableTextures && itr != textureMaps.end() && itr->second->isDefined()) { - drawMaterialTextures->setTexture(gr::Texture::MaterialEmissiveLightmap, itr->second->getTextureView()); - } else { - drawMaterialTextures->setTexture(gr::Texture::MaterialEmissiveLightmap, textureCache->getBlackTexture()); - } - } - - batch.setResourceTextureTable(material->getTextureTable()); } diff --git a/libraries/render-utils/src/RenderPipelines.h b/libraries/render-utils/src/RenderPipelines.h index b7d22bc72d..0f3d1160ef 100644 --- a/libraries/render-utils/src/RenderPipelines.h +++ b/libraries/render-utils/src/RenderPipelines.h @@ -15,7 +15,9 @@ class RenderPipelines { public: - static void bindMaterial(const graphics::MaterialPointer& material, gpu::Batch& batch, bool enableTextures); + static void bindMaterial(graphics::MaterialPointer& material, gpu::Batch& batch, bool enableTextures); + static void updateMultiMaterial(graphics::MultiMaterial& multiMaterial); + static void bindMaterials(graphics::MultiMaterial& multiMaterial, gpu::Batch& batch, bool enableTextures); }; diff --git a/libraries/render-utils/src/simple.slf b/libraries/render-utils/src/simple.slf index 582549ade1..469c0976aa 100644 --- a/libraries/render-utils/src/simple.slf +++ b/libraries/render-utils/src/simple.slf @@ -91,14 +91,14 @@ void main(void) { position.xyz, #endif normal, - vec3(0.0), + diffuse, DEFAULT_SPECULAR, - DEFAULT_EMISSIVE, + emissive, 1.0, - DEFAULT_ROUGHNESS, - DEFAULT_METALLIC, - DEFAULT_OCCLUSION, - DEFAULT_SCATTERING + roughness, + metallic, + occlusion, + scattering ); #if defined(PROCEDURAL_V3) diff --git a/libraries/render-utils/src/simple_transparent.slf b/libraries/render-utils/src/simple_transparent.slf index ea444d6113..6d8348f50c 100644 --- a/libraries/render-utils/src/simple_transparent.slf +++ b/libraries/render-utils/src/simple_transparent.slf @@ -92,22 +92,22 @@ void main(void) { emissive = vec3(clamp(emissiveAmount, 0.0, 1.0)); #elif defined(PROCEDURAL_V3) || defined(PROCEDURAL_V4) #if defined(PROCEDURAL_V3) - ProceduralFragment proceduralData = { + ProceduralFragment proceduralData = ProceduralFragment( #else vec4 position = cam._viewInverse * _positionES; - ProceduralFragmentWithPosition proceduralData = { + ProceduralFragmentWithPosition proceduralData = ProceduralFragmentWithPosition( position.xyz, #endif normal, - vec3(0.0), - DEFAULT_SPECULAR, - DEFAULT_EMISSIVE, - 1.0, - DEFAULT_ROUGHNESS, - DEFAULT_METALLIC, - DEFAULT_OCCLUSION, + diffuse, + fresnel, + emissive, + alpha, + roughness, + metallic, + occlusion, DEFAULT_SCATTERING - }; + ); #if defined(PROCEDURAL_V3) emissiveAmount = getProceduralFragment(proceduralData); diff --git a/libraries/script-engine/src/ScriptEngine.cpp b/libraries/script-engine/src/ScriptEngine.cpp index 47ce29f9a3..fa7a8e1114 100644 --- a/libraries/script-engine/src/ScriptEngine.cpp +++ b/libraries/script-engine/src/ScriptEngine.cpp @@ -2407,6 +2407,11 @@ void ScriptEngine::unloadEntityScript(const EntityItemID& entityID, bool shouldR } } +QList ScriptEngine::getListOfEntityScriptIDs() { + QReadLocker locker{ &_entityScriptsLock }; + return _entityScripts.keys(); +} + void ScriptEngine::unloadAllEntityScripts() { if (QThread::currentThread() != thread()) { #ifdef THREAD_DEBUGGING diff --git a/libraries/script-engine/src/ScriptEngine.h b/libraries/script-engine/src/ScriptEngine.h index 0d6d45e594..8753010089 100644 --- a/libraries/script-engine/src/ScriptEngine.h +++ b/libraries/script-engine/src/ScriptEngine.h @@ -145,6 +145,9 @@ public: QString getFilename() const; + + QList getListOfEntityScriptIDs(); + /**jsdoc * Stop the current script. * @function Script.stop diff --git a/libraries/shared/src/AvatarConstants.h b/libraries/shared/src/AvatarConstants.h index 87da47a27a..103782bd3f 100644 --- a/libraries/shared/src/AvatarConstants.h +++ b/libraries/shared/src/AvatarConstants.h @@ -44,7 +44,7 @@ const float DEFAULT_AVATAR_RIGHTHAND_MASS = 2.0f; // Used when avatar is missing joints... (avatar space) const glm::quat DEFAULT_AVATAR_MIDDLE_EYE_ROT { Quaternions::Y_180 }; -const glm::vec3 DEFAULT_AVATAR_HEAD_TO_MIDDLE_EYE_OFFSET = { 0.0f, 0.06f, -0.09f }; +const glm::vec3 DEFAULT_AVATAR_HEAD_TO_MIDDLE_EYE_OFFSET = { 0.0f, 0.064f, 0.084f }; const glm::vec3 DEFAULT_AVATAR_HEAD_POS { 0.0f, 0.53f, 0.0f }; const glm::quat DEFAULT_AVATAR_HEAD_ROT { Quaternions::Y_180 }; const glm::vec3 DEFAULT_AVATAR_RIGHTARM_POS { -0.134824f, 0.396348f, -0.0515777f }; diff --git a/libraries/shared/src/BillboardMode.cpp b/libraries/shared/src/BillboardMode.cpp index 56251f53f2..4b6af5db33 100644 --- a/libraries/shared/src/BillboardMode.cpp +++ b/libraries/shared/src/BillboardMode.cpp @@ -14,10 +14,10 @@ const char* billboardModeNames[] = { "full" }; -static const size_t MATERIAL_MODE_NAMES = (sizeof(billboardModeNames) / sizeof((billboardModeNames)[0])); +static const size_t BILLBOARD_MODE_NAMES = (sizeof(billboardModeNames) / sizeof(billboardModeNames[0])); QString BillboardModeHelpers::getNameForBillboardMode(BillboardMode mode) { - if (((int)mode <= 0) || ((int)mode >= (int)MATERIAL_MODE_NAMES)) { + if (((int)mode <= 0) || ((int)mode >= (int)BILLBOARD_MODE_NAMES)) { mode = (BillboardMode)0; } diff --git a/libraries/shared/src/JointData.h b/libraries/shared/src/JointData.h index f4c8b89e7a..7a2420262a 100644 --- a/libraries/shared/src/JointData.h +++ b/libraries/shared/src/JointData.h @@ -14,7 +14,7 @@ public: }; // Used by the avatar mixer to describe a single joint -// Translations relative to their parent and are in meters. +// Translations relative to their parent joint // Rotations are absolute (i.e. not relative to parent) and are in rig space. class JointData { public: diff --git a/libraries/shared/src/MaterialMappingMode.cpp b/libraries/shared/src/MaterialMappingMode.cpp index 1ddad178a2..09a7cfd6d9 100644 --- a/libraries/shared/src/MaterialMappingMode.cpp +++ b/libraries/shared/src/MaterialMappingMode.cpp @@ -13,7 +13,7 @@ const char* materialMappingModeNames[] = { "projected" }; -static const size_t MATERIAL_MODE_NAMES = (sizeof(materialMappingModeNames) / sizeof((materialMappingModeNames)[0])); +static const size_t MATERIAL_MODE_NAMES = (sizeof(materialMappingModeNames) / sizeof(materialMappingModeNames[0])); QString MaterialMappingModeHelpers::getNameForMaterialMappingMode(MaterialMappingMode mode) { if (((int)mode <= 0) || ((int)mode >= (int)MATERIAL_MODE_NAMES)) { diff --git a/libraries/shared/src/PrimitiveMode.cpp b/libraries/shared/src/PrimitiveMode.cpp new file mode 100644 index 0000000000..c426f896b9 --- /dev/null +++ b/libraries/shared/src/PrimitiveMode.cpp @@ -0,0 +1,24 @@ +// +// Created by Sam Gondelman on 1/7/19 +// Copyright 2019 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include "PrimitiveMode.h" + +const char* primitiveModeNames[] = { + "solid", + "lines" +}; + +static const size_t PRIMITIVE_MODE_NAMES = (sizeof(primitiveModeNames) / sizeof(primitiveModeNames[0])); + +QString PrimitiveModeHelpers::getNameForPrimitiveMode(PrimitiveMode mode) { + if (((int)mode <= 0) || ((int)mode >= (int)PRIMITIVE_MODE_NAMES)) { + mode = (PrimitiveMode)0; + } + + return primitiveModeNames[(int)mode]; +} \ No newline at end of file diff --git a/libraries/shared/src/PrimitiveMode.h b/libraries/shared/src/PrimitiveMode.h new file mode 100644 index 0000000000..6072f24fb9 --- /dev/null +++ b/libraries/shared/src/PrimitiveMode.h @@ -0,0 +1,39 @@ +// +// Created by Sam Gondelman on 1/7/19. +// Copyright 2019 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_PrimitiveMode_h +#define hifi_PrimitiveMode_h + +#include "QString" + +/**jsdoc + *

How the geometry of the entity is rendered.

+ * + * + * + * + * + * + * + * + *
ValueDescription
solidThe entity will be drawn as a solid shape.
linesThe entity will be drawn as wireframe.
+ * @typedef {string} PrimitiveMode + */ + +enum class PrimitiveMode { + SOLID = 0, + LINES +}; + +class PrimitiveModeHelpers { +public: + static QString getNameForPrimitiveMode(PrimitiveMode mode); +}; + +#endif // hifi_PrimitiveMode_h + diff --git a/libraries/shared/src/RenderLayer.cpp b/libraries/shared/src/RenderLayer.cpp new file mode 100644 index 0000000000..7e072631e0 --- /dev/null +++ b/libraries/shared/src/RenderLayer.cpp @@ -0,0 +1,25 @@ +// +// Created by Sam Gondelman on 1/3/19 +// Copyright 2019 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include "RenderLayer.h" + +const char* renderLayerNames[] = { + "world", + "front", + "hud" +}; + +static const size_t RENDER_LAYER_NAMES = (sizeof(renderLayerNames) / sizeof(renderLayerNames[0])); + +QString RenderLayerHelpers::getNameForRenderLayer(RenderLayer mode) { + if (((int)mode <= 0) || ((int)mode >= (int)RENDER_LAYER_NAMES)) { + mode = (RenderLayer)0; + } + + return renderLayerNames[(int)mode]; +} \ No newline at end of file diff --git a/libraries/shared/src/RenderLayer.h b/libraries/shared/src/RenderLayer.h new file mode 100644 index 0000000000..b5bf885616 --- /dev/null +++ b/libraries/shared/src/RenderLayer.h @@ -0,0 +1,41 @@ +// +// Created by Sam Gondelman on 1/3/19. +// Copyright 2019 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_RenderLayer_h +#define hifi_RenderLayer_h + +#include "QString" + +/**jsdoc + *

In which layer an entity is rendered.

+ * + * + * + * + * + * + * + * + * + *
ValueDescription
worldThe entity will be drawn in the world with everything else.
frontThe entity will be drawn on top of the world layer, but behind the HUD sphere.
hudThe entity will be drawn on top of other layers and the HUD sphere.
+ * @typedef {string} RenderLayer + */ + +enum class RenderLayer { + WORLD = 0, + FRONT, + HUD +}; + +class RenderLayerHelpers { +public: + static QString getNameForRenderLayer(RenderLayer mode); +}; + +#endif // hifi_RenderLayer_h + diff --git a/libraries/shared/src/SpatiallyNestable.cpp b/libraries/shared/src/SpatiallyNestable.cpp index 8805205361..c524e3183b 100644 --- a/libraries/shared/src/SpatiallyNestable.cpp +++ b/libraries/shared/src/SpatiallyNestable.cpp @@ -108,7 +108,8 @@ SpatiallyNestablePointer SpatiallyNestable::getParentPointer(bool& success) cons return nullptr; } - if (parent && parent->getID() == parentID) { + if (parent && (parent->getID() == parentID || + (parentID == AVATAR_SELF_ID && parent->isMyAvatar()))) { // parent pointer is up-to-date if (!_parentKnowsMe) { SpatialParentTree* parentTree = parent->getParentTree(); diff --git a/libraries/shared/src/SpatiallyNestable.h b/libraries/shared/src/SpatiallyNestable.h index a55f32896b..319f07236b 100644 --- a/libraries/shared/src/SpatiallyNestable.h +++ b/libraries/shared/src/SpatiallyNestable.h @@ -47,6 +47,8 @@ public: virtual const QUuid getParentID() const; virtual void setParentID(const QUuid& parentID); + virtual bool isMyAvatar() const { return false; } + virtual quint16 getParentJointIndex() const { return _parentJointIndex; } virtual void setParentJointIndex(quint16 parentJointIndex); diff --git a/libraries/shared/src/WebInputMode.cpp b/libraries/shared/src/WebInputMode.cpp new file mode 100644 index 0000000000..292fb50763 --- /dev/null +++ b/libraries/shared/src/WebInputMode.cpp @@ -0,0 +1,24 @@ +// +// Created by Sam Gondelman on 1/9/19 +// Copyright 2019 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include "WebInputMode.h" + +const char* webInputModeNames[] = { + "touch", + "mouse" +}; + +static const size_t WEB_INPUT_MODE_NAMES = (sizeof(webInputModeNames) / sizeof(webInputModeNames[0])); + +QString WebInputModeHelpers::getNameForWebInputMode(WebInputMode mode) { + if (((int)mode <= 0) || ((int)mode >= (int)WEB_INPUT_MODE_NAMES)) { + mode = (WebInputMode)0; + } + + return webInputModeNames[(int)mode]; +} \ No newline at end of file diff --git a/libraries/shared/src/WebInputMode.h b/libraries/shared/src/WebInputMode.h new file mode 100644 index 0000000000..d97ccef519 --- /dev/null +++ b/libraries/shared/src/WebInputMode.h @@ -0,0 +1,39 @@ +// +// Created by Sam Gondelman on 1/9/19. +// Copyright 2019 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_WebInputMode_h +#define hifi_WebInputMode_h + +#include "QString" + +/**jsdoc + *

Controls how the web surface processed PointerEvents

+ * + * + * + * + * + * + * + * + *
ValueDescription
touchEvents are processed as touch events.
mouseEvents are processed as mouse events.
+ * @typedef {string} WebInputMode + */ + +enum class WebInputMode { + TOUCH = 0, + MOUSE, +}; + +class WebInputModeHelpers { +public: + static QString getNameForWebInputMode(WebInputMode mode); +}; + +#endif // hifi_WebInputMode_h + diff --git a/libraries/ui/src/ui/OffscreenQmlSurface.h b/libraries/ui/src/ui/OffscreenQmlSurface.h index b95a8f117d..b8c6808afa 100644 --- a/libraries/ui/src/ui/OffscreenQmlSurface.h +++ b/libraries/ui/src/ui/OffscreenQmlSurface.h @@ -35,7 +35,7 @@ public: Q_INVOKABLE void synthesizeKeyPress(QString key, QObject* targetOverride = nullptr); Q_INVOKABLE void lowerKeyboard(); PointerEvent::EventType choosePointerEventType(QEvent::Type type); - unsigned int deviceIdByTouchPoint(qreal x, qreal y); + Q_INVOKABLE unsigned int deviceIdByTouchPoint(qreal x, qreal y); signals: void focusObjectChanged(QObject* newFocus); diff --git a/plugins/oculus/src/OculusBaseDisplayPlugin.cpp b/plugins/oculus/src/OculusBaseDisplayPlugin.cpp index f10aba7920..a67e3127e5 100644 --- a/plugins/oculus/src/OculusBaseDisplayPlugin.cpp +++ b/plugins/oculus/src/OculusBaseDisplayPlugin.cpp @@ -12,6 +12,7 @@ #include #include #include +#include #include "OculusHelpers.h" @@ -30,7 +31,7 @@ bool OculusBaseDisplayPlugin::beginFrameRender(uint32_t frameIndex) { return false; } - if (ovr::quitRequested(status) || ovr::displayLost(status) || !ovr::handleOVREvents()) { + if (ovr::quitRequested(status) || ovr::displayLost(status)) { QMetaObject::invokeMethod(qApp, "quit"); return false; } diff --git a/plugins/oculus/src/OculusBaseDisplayPlugin.h b/plugins/oculus/src/OculusBaseDisplayPlugin.h index 547d3ee5fe..1abb7cdad7 100644 --- a/plugins/oculus/src/OculusBaseDisplayPlugin.h +++ b/plugins/oculus/src/OculusBaseDisplayPlugin.h @@ -13,6 +13,9 @@ #include +#define OVRPL_DISABLED +#include + class OculusBaseDisplayPlugin : public HmdDisplayPlugin { using Parent = HmdDisplayPlugin; public: @@ -30,7 +33,7 @@ public: QRectF getPlayAreaRect() override; QVector getSensorPositions() override; - + protected: void customizeContext() override; void uncustomizeContext() override; diff --git a/plugins/oculus/src/OculusControllerManager.cpp b/plugins/oculus/src/OculusControllerManager.cpp index 76ff4a1755..c2b9145f3c 100644 --- a/plugins/oculus/src/OculusControllerManager.cpp +++ b/plugins/oculus/src/OculusControllerManager.cpp @@ -17,6 +17,7 @@ #include #include +#include #include #include #include @@ -327,8 +328,14 @@ void OculusControllerManager::TouchDevice::handleHeadPose(float deltaTime, ovr::toGlm(headPose.AngularVelocity)); glm::mat4 sensorToAvatar = glm::inverse(inputCalibrationData.avatarMat) * inputCalibrationData.sensorToWorldMat; - glm::mat4 defaultHeadOffset = glm::inverse(inputCalibrationData.defaultCenterEyeMat) * - inputCalibrationData.defaultHeadMat; + glm::mat4 defaultHeadOffset; + if (inputCalibrationData.hmdAvatarAlignmentType == controller::HmdAvatarAlignmentType::Eyes) { + // align the eyes of the user with the eyes of the avatar + defaultHeadOffset = glm::inverse(inputCalibrationData.defaultCenterEyeMat) * inputCalibrationData.defaultHeadMat; + } else { + // align the head of the user with the head of the avatar + defaultHeadOffset = createMatFromQuatAndPos(Quaternions::IDENTITY, -DEFAULT_AVATAR_HEAD_TO_MIDDLE_EYE_OFFSET); + } pose.valid = true; _poseStateMap[controller::HEAD] = pose.postTransform(defaultHeadOffset).transform(sensorToAvatar); diff --git a/plugins/oculus/src/OculusHelpers.cpp b/plugins/oculus/src/OculusHelpers.cpp index 29691e73a5..2693b9ee7e 100644 --- a/plugins/oculus/src/OculusHelpers.cpp +++ b/plugins/oculus/src/OculusHelpers.cpp @@ -82,15 +82,18 @@ private: } #ifdef OCULUS_APP_ID - if (qApp->property(hifi::properties::OCULUS_STORE).toBool()) { - if (ovr_PlatformInitializeWindows(OCULUS_APP_ID) != ovrPlatformInitialize_Success) { - qCWarning(oculusLog) << "Unable to initialize the platform for entitlement check - fail the check" << ovr::getError(); - return; - } else { - qCDebug(oculusLog) << "Performing Oculus Platform entitlement check"; - ovr_Entitlement_GetIsViewerEntitled(); + static std::once_flag once; + std::call_once(once, []() { + if (qApp->property(hifi::properties::OCULUS_STORE).toBool()) { + if (ovr_PlatformInitializeWindows(OCULUS_APP_ID) != ovrPlatformInitialize_Success) { + qCWarning(oculusLog) << "Unable to initialize the platform for entitlement check - fail the check" << ovr::getError(); + return; + } else { + qCDebug(oculusLog) << "Performing Oculus Platform entitlement check"; + ovr_Entitlement_GetIsViewerEntitled(); + } } - } + }); #endif ovrGraphicsLuid luid; @@ -293,34 +296,3 @@ controller::Pose hifi::ovr::toControllerPose(ovrHandType hand, pose.valid = true; return pose; } - -bool hifi::ovr::handleOVREvents() { -#ifdef OCULUS_APP_ID - if (qApp->property(hifi::properties::OCULUS_STORE).toBool()) { - // pop messages to see if we got a return for an entitlement check - ovrMessageHandle message = ovr_PopMessage(); - - while (message) { - switch (ovr_Message_GetType(message)) { - case ovrMessage_Entitlement_GetIsViewerEntitled: { - if (!ovr_Message_IsError(message)) { - // this viewer is entitled, no need to flag anything - qCDebug(oculusLog) << "Oculus Platform entitlement check succeeded, proceeding normally"; - } else { - // we failed the entitlement check, quit - qCDebug(oculusLog) << "Oculus Platform entitlement check failed, app will now quit" << OCULUS_APP_ID; - return false; - } - } - } - - // free the message handle to cleanup and not leak - ovr_FreeMessage(message); - - // pop the next message to check, if there is one - message = ovr_PopMessage(); - } - } -#endif - return true; -} diff --git a/plugins/oculus/src/OculusHelpers.h b/plugins/oculus/src/OculusHelpers.h index bdfc4434bb..3587117825 100644 --- a/plugins/oculus/src/OculusHelpers.h +++ b/plugins/oculus/src/OculusHelpers.h @@ -30,7 +30,6 @@ struct ovr { static ovrSessionStatus getStatus(ovrResult& result); static ovrTrackingState getTrackingState(double absTime = 0.0, ovrBool latencyMarker = ovrFalse); static QString getError(); - static bool handleOVREvents(); static inline bool quitRequested() { return quitRequested(getStatus()); } static inline bool reorientRequested() { return reorientRequested(getStatus()); } diff --git a/plugins/oculus/src/OculusPlatformPlugin.cpp b/plugins/oculus/src/OculusPlatformPlugin.cpp new file mode 100644 index 0000000000..27fb98c8b5 --- /dev/null +++ b/plugins/oculus/src/OculusPlatformPlugin.cpp @@ -0,0 +1,109 @@ +// +// Created by Wayne Chen on 2019/01/08 +// Copyright 2019 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include "OculusPlatformPlugin.h" + +#include + +#include + +#include "OculusHelpers.h" + +QString OculusAPIPlugin::NAME { "Oculus Rift" }; + +OculusAPIPlugin::OculusAPIPlugin() { + _session = hifi::ovr::acquireRenderSession(); +} + +OculusAPIPlugin::~OculusAPIPlugin() { + hifi::ovr::releaseRenderSession(_session); +} + +bool OculusAPIPlugin::isRunning() const { + return (qApp->property(hifi::properties::OCULUS_STORE).toBool()); +} + +void OculusAPIPlugin::requestNonceAndUserID(NonceUserIDCallback callback) { +#ifdef OCULUS_APP_ID + _nonceUserIDCallback = callback; + ovr_User_GetUserProof(); + ovr_User_GetLoggedInUser(); +#endif +} + +void OculusAPIPlugin::handleOVREvents() { +#ifdef OCULUS_APP_ID + if (qApp->property(hifi::properties::OCULUS_STORE).toBool()) { + // pop messages to see if we got a return for an entitlement check + ovrMessageHandle message { nullptr }; + + // pop the next message to check, if there is one + while ((message = ovr_PopMessage())) { + switch (ovr_Message_GetType(message)) { + case ovrMessage_Entitlement_GetIsViewerEntitled: { + if (!ovr_Message_IsError(message)) { + // this viewer is entitled, no need to flag anything + qCDebug(oculusLog) << "Oculus Platform entitlement check succeeded, proceeding normally"; + } else { + // we failed the entitlement check, quit + qCDebug(oculusLog) << "Oculus Platform entitlement check failed, app will now quit" << OCULUS_APP_ID; + QMetaObject::invokeMethod(qApp, "quit"); + } + break; + } + case ovrMessage_User_Get: { + if (!ovr_Message_IsError(message)) { + qCDebug(oculusLog) << "Oculus Platform user retrieval succeeded"; + ovrUserHandle user = ovr_Message_GetUser(message); + _user = ovr_User_GetOculusID(user); + // went all the way through the `requestNonceAndUserID()` pipeline successfully. + } else { + qCDebug(oculusLog) << "Oculus Platform user retrieval failed" << QString(ovr_Error_GetMessage(ovr_Message_GetError(message))); + // emit the signal so we don't hang for it anywhere else. + _user = ""; + } + break; + } + case ovrMessage_User_GetLoggedInUser: { + if (!ovr_Message_IsError(message)) { + ovrUserHandle user = ovr_Message_GetUser(message); + _userID = ovr_User_GetID(user); + ovr_User_Get(_userID); + } else { + qCDebug(oculusLog) << "Oculus Platform user ID retrieval failed" << QString(ovr_Error_GetMessage(ovr_Message_GetError(message))); + // emit the signal so we don't hang for it anywhere else. + } + _userIDChanged = true; + break; + } + case ovrMessage_User_GetUserProof: { + if (!ovr_Message_IsError(message)) { + ovrUserProofHandle userProof = ovr_Message_GetUserProof(message); + _nonce = ovr_UserProof_GetNonce(userProof); + qCDebug(oculusLog) << "Oculus Platform nonce retrieval succeeded: " << _nonce; + } else { + qCDebug(oculusLog) << "Oculus Platform nonce retrieval failed" << QString(ovr_Error_GetMessage(ovr_Message_GetError(message))); + _nonce = ""; + // emit the signal so we don't hang for it anywhere else. + } + _nonceChanged = true; + break; + } + } + + if (_nonceChanged && _userIDChanged) { + _nonceUserIDCallback(_nonce, QString::number(_userID)); + _nonceChanged = _userIDChanged = false; + } + + // free the message handle to cleanup and not leak + ovr_FreeMessage(message); + } + } +#endif +} diff --git a/plugins/oculus/src/OculusPlatformPlugin.h b/plugins/oculus/src/OculusPlatformPlugin.h new file mode 100644 index 0000000000..3d80540419 --- /dev/null +++ b/plugins/oculus/src/OculusPlatformPlugin.h @@ -0,0 +1,39 @@ +// +// Created by Wayne Chen on 2019/01/08 +// Copyright 2019 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// +#pragma once + +#include + +#include + +#define OVRPL_DISABLED +#include + +class OculusAPIPlugin : public OculusPlatformPlugin { +public: + OculusAPIPlugin(); + virtual ~OculusAPIPlugin(); + QString getName() const { return NAME; } + QString getOculusUserID() const { return _user; }; + + bool isRunning() const; + + virtual void requestNonceAndUserID(NonceUserIDCallback callback); + + virtual void handleOVREvents(); + +private: + static QString NAME; + NonceUserIDCallback _nonceUserIDCallback; + QString _nonce; + bool _nonceChanged{ false }; + bool _userIDChanged{ false }; + QString _user; + ovrID _userID; + ovrSession _session; +}; diff --git a/plugins/oculus/src/OculusProvider.cpp b/plugins/oculus/src/OculusProvider.cpp index 47ccc5304e..67871b8610 100644 --- a/plugins/oculus/src/OculusProvider.cpp +++ b/plugins/oculus/src/OculusProvider.cpp @@ -18,15 +18,18 @@ #include "OculusDisplayPlugin.h" #include "OculusDebugDisplayPlugin.h" +#include "OculusPlatformPlugin.h" #include "OculusControllerManager.h" -class OculusProvider : public QObject, public DisplayProvider, InputProvider +class OculusProvider : public QObject, public DisplayProvider, InputProvider, OculusPlatformProvider { Q_OBJECT Q_PLUGIN_METADATA(IID DisplayProvider_iid FILE "oculus.json") Q_INTERFACES(DisplayProvider) Q_PLUGIN_METADATA(IID InputProvider_iid FILE "oculus.json") Q_INTERFACES(InputProvider) + Q_PLUGIN_METADATA(IID OculusPlatformProvider_iid FILE "oculus.json") + Q_INTERFACES(OculusPlatformProvider) public: OculusProvider(QObject* parent = nullptr) : QObject(parent) {} @@ -62,6 +65,15 @@ public: return _inputPlugins; } + virtual OculusPlatformPluginPointer getOculusPlatformPlugin() override { + static std::once_flag once; + std::call_once(once, [&] { + _oculusPlatformPlugin = std::make_shared(); + + }); + return _oculusPlatformPlugin; + } + virtual void destroyInputPlugins() override { _inputPlugins.clear(); } @@ -73,6 +85,7 @@ public: private: DisplayPluginList _displayPlugins; InputPluginList _inputPlugins; + OculusPlatformPluginPointer _oculusPlatformPlugin; }; #include "OculusProvider.moc" diff --git a/plugins/openvr/src/ViveControllerManager.cpp b/plugins/openvr/src/ViveControllerManager.cpp index 3aea5f1ce0..34ebb73fda 100644 --- a/plugins/openvr/src/ViveControllerManager.cpp +++ b/plugins/openvr/src/ViveControllerManager.cpp @@ -27,6 +27,7 @@ #include #include #include +#include #include #include #include @@ -1024,7 +1025,16 @@ void ViveControllerManager::InputDevice::handleHeadPoseEvent(const controller::I //perform a 180 flip to make the HMD face the +z instead of -z, beacuse the head faces +z glm::mat4 matYFlip = mat * Matrices::Y_180; controller::Pose pose(extractTranslation(matYFlip), glmExtractRotation(matYFlip), linearVelocity, angularVelocity); - glm::mat4 defaultHeadOffset = glm::inverse(inputCalibrationData.defaultCenterEyeMat) * inputCalibrationData.defaultHeadMat; + + glm::mat4 defaultHeadOffset; + if (inputCalibrationData.hmdAvatarAlignmentType == controller::HmdAvatarAlignmentType::Eyes) { + // align the eyes of the user with the eyes of the avatar + defaultHeadOffset = glm::inverse(inputCalibrationData.defaultCenterEyeMat) * inputCalibrationData.defaultHeadMat; + } else { + // align the head of the user with the head of the avatar + defaultHeadOffset = createMatFromQuatAndPos(Quaternions::IDENTITY, -DEFAULT_AVATAR_HEAD_TO_MIDDLE_EYE_OFFSET); + } + glm::mat4 sensorToAvatar = glm::inverse(inputCalibrationData.avatarMat) * inputCalibrationData.sensorToWorldMat; _poseStateMap[controller::HEAD] = pose.postTransform(defaultHeadOffset).transform(sensorToAvatar); } diff --git a/prebuild.py b/prebuild.py index a758dcbea2..fb54b8d6fe 100644 --- a/prebuild.py +++ b/prebuild.py @@ -43,8 +43,7 @@ def parse_args(): defaultPortsPath = hifi_utils.scriptRelative('cmake', 'ports') from argparse import ArgumentParser parser = ArgumentParser(description='Prepare build dependencies.') - parser.add_argument('--android', action='store_true') - #parser.add_argument('--android', type=str) + parser.add_argument('--android', type=str) parser.add_argument('--debug', action='store_true') parser.add_argument('--force-bootstrap', action='store_true') parser.add_argument('--force-build', action='store_true') @@ -87,6 +86,17 @@ def main(): # here shouldn't invalidte the vcpkg install) pm.cleanBuilds() + # If we're running in android mode, we also need to grab a bunch of additional binaries + # (this logic is all migrated from the old setupDependencies tasks in gradle) + if args.android: + # Find the target location + appPath = hifi_utils.scriptRelative('android/apps/' + args.android) + # Copy the non-Qt libraries specified in the config in hifi_android.py + hifi_android.copyAndroidLibs(pm.androidPackagePath, appPath) + # Determine the Qt package path + qtPath = os.path.join(pm.androidPackagePath, 'qt') + hifi_android.QtPackager(appPath, qtPath).bundle() + # Write the vcpkg config to the build directory last pm.writeConfig() diff --git a/scripts/developer/utilities/render/deferredLighting.qml b/scripts/developer/utilities/render/deferredLighting.qml index 9737fb7f1a..aef533b7a3 100644 --- a/scripts/developer/utilities/render/deferredLighting.qml +++ b/scripts/developer/utilities/render/deferredLighting.qml @@ -14,7 +14,6 @@ import QtQuick.Layouts 1.3 import stylesUit 1.0 import controlsUit 1.0 as HifiControls import "configSlider" -import "../lib/jet/qml" as Jet Rectangle { HifiConstants { id: hifi;} @@ -249,12 +248,6 @@ Rectangle { checked: render.mainViewTask.getConfig("DrawOverlayHUDOpaqueBounds")["enabled"] onCheckedChanged: { render.mainViewTask.getConfig("DrawOverlayHUDOpaqueBounds")["enabled"] = checked } } - HifiControls.CheckBox { - boxSize: 20 - text: "Transparents in HUD" - checked: render.mainViewTask.getConfig("DrawOverlayHUDTransparentBounds")["enabled"] - onCheckedChanged: { render.mainViewTask.getConfig("DrawOverlayHUDTransparentBounds")["enabled"] = checked } - } } Column { @@ -277,6 +270,12 @@ Rectangle { checked: render.mainViewTask.getConfig("DrawZones")["enabled"] onCheckedChanged: { render.mainViewTask.getConfig("ZoneRenderer")["enabled"] = checked; render.mainViewTask.getConfig("DrawZones")["enabled"] = checked; } } + HifiControls.CheckBox { + boxSize: 20 + text: "Transparents in HUD" + checked: render.mainViewTask.getConfig("DrawOverlayHUDTransparentBounds")["enabled"] + onCheckedChanged: { render.mainViewTask.getConfig("DrawOverlayHUDTransparentBounds")["enabled"] = checked } + } } } Separator {} @@ -303,5 +302,13 @@ Rectangle { } } } + Row { + HifiControls.Button { + text: "Material" + onClicked: { + sendToScript({method: "openMaterialInspectorView"}); + } + } + } } } diff --git a/scripts/developer/utilities/render/luci.js b/scripts/developer/utilities/render/luci.js index cffeb615c9..bae5c4646d 100644 --- a/scripts/developer/utilities/render/luci.js +++ b/scripts/developer/utilities/render/luci.js @@ -12,6 +12,8 @@ (function() { var AppUi = Script.require('appUi'); + + var MaterialInspector = Script.require('./materialInspector.js'); var moveDebugCursor = false; var onMousePressEvent = function (e) { @@ -41,11 +43,12 @@ Render.getConfig("RenderMainView").getConfig("DebugDeferredBuffer").size = { x: nx, y: ny, z: 1.0, w: 1.0 }; } - function Page(title, qmlurl, width, height) { + function Page(title, qmlurl, width, height, handleWindowFunc) { this.title = title; this.qml = qmlurl; this.width = width; this.height = height; + this.handleWindowFunc = handleWindowFunc; this.window; @@ -73,8 +76,10 @@ presentationMode: Desktop.PresentationMode.NATIVE, size: {x: this.width, y: this.height} }); + this.handleWindowFunc(this.window); this.window.closed.connect(function () { that.killView(); + this.handleWindowFunc(undefined); }); } }; @@ -84,8 +89,12 @@ this._pages = {}; }; - Pages.prototype.addPage = function (command, title, qmlurl, width, height) { - this._pages[command] = new Page(title, qmlurl, width, height); + Pages.prototype.addPage = function (command, title, qmlurl, width, height, handleWindowFunc) { + if (handleWindowFunc === undefined) { + // Workaround for bad linter + handleWindowFunc = function(window){}; + } + this._pages[command] = new Page(title, qmlurl, width, height, handleWindowFunc); }; Pages.prototype.open = function (command) { @@ -110,6 +119,7 @@ pages.addPage('openEngineView', 'Render Engine', 'engineInspector.qml', 300, 400); pages.addPage('openEngineLODView', 'Render LOD', 'lod.qml', 300, 400); pages.addPage('openCullInspectorView', 'Cull Inspector', 'culling.qml', 300, 400); + pages.addPage('openMaterialInspectorView', 'Material Inspector', 'materialInspector.qml', 300, 400, MaterialInspector.setWindow); function fromQml(message) { if (pages.open(message.method)) { diff --git a/scripts/developer/utilities/render/materialInspector.js b/scripts/developer/utilities/render/materialInspector.js new file mode 100644 index 0000000000..76e5da5cd0 --- /dev/null +++ b/scripts/developer/utilities/render/materialInspector.js @@ -0,0 +1,165 @@ +// +// materialInspector.js +// +// Created by Sabrina Shanman on 2019-01-17 +// Copyright 2019 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or https://www.apache.org/licenses/LICENSE-2.0.html +// +"use strict"; + +var activeWindow; + +// Adapted from Samuel G's material painting script +function getTopMaterial(multiMaterial) { + // For non-models: multiMaterial[0] will be the top material + // For models, multiMaterial[0] is the base material, and multiMaterial[1] is the highest priority applied material + if (multiMaterial.length > 1) { + if (multiMaterial[1].priority > multiMaterial[0].priority) { + return multiMaterial[1]; + } + } + + return multiMaterial[0]; +} + +function updateMaterial(type, id, meshPart) { + var mesh = Graphics.getModel(id); + var meshPartString = meshPart.toString(); + if (!mesh) { + return; + } + var materials = mesh.materialLayers; + if (!materials[meshPartString] || materials[meshPartString].length <= 0) { + return; + } + + var topMaterial = getTopMaterial(materials[meshPartString]); + var materialJSONText = JSON.stringify({ + materialVersion: 1, + materials: topMaterial.material + }, null, 2); + + toQml({method: "setObjectInfo", params: {id: id, type: type, meshPart: meshPart}}); + toQml({method: "setMaterialJSON", params: {materialJSONText: materialJSONText}}); +} + +// Adapted from Samuel G's material painting script +function getHoveredMaterialLocation(event) { + var pickRay = Camera.computePickRay(event.x, event.y); + var closest; + var id; + var type = "Entity"; + var avatar = AvatarManager.findRayIntersection(pickRay); + var entity = Entities.findRayIntersection(pickRay, true); + var overlay = Overlays.findRayIntersection(pickRay, true); + + closest = entity; + id = entity.entityID; + + if (avatar.intersects && avatar.distance < closest.distance) { + closest = avatar; + id = avatar.avatarID; + type = "Avatar"; + } else if (overlay.intersects && overlay.distance < closest.distance) { + closest = overlay; + id = overlay.overlayID; + type = "Overlay"; + } + + if (closest.intersects) { + return { + type: type, + id: id, + meshPart: (closest.extraInfo.shapeID ? closest.extraInfo.shapeID : 0) + }; + } else { + return undefined; + } +} + +var pressedID; +var pressedMeshPart; + +function mousePressEvent(event) { + if (!event.isLeftButton) { + return; + } + + var result = getHoveredMaterialLocation(event); + + if (result !== undefined) { + pressedID = result.id; + pressedMeshPart = result.meshPart; + } +} + +function mouseReleaseEvent(event) { + if (!event.isLeftButton) { + return; + } + + var result = getHoveredMaterialLocation(event); + + if (result !== undefined && result.id === pressedID && result.meshPart === pressedMeshPart) { + updateMaterial(result.type, result.id, result.meshPart); + setSelectedObject(result.id, result.type); + } +} + +function killWindow() { + setWindow(undefined); +} + +function toQml(message) { + if (activeWindow === undefined) { + return; // Shouldn't happen + } + + activeWindow.sendToQml(message); +} + +function fromQml(message) { + // No cases currently +} + +var SELECT_LIST = "luci_materialInspector_SelectionList"; +Selection.enableListHighlight(SELECT_LIST, { + outlineUnoccludedColor: { red: 255, green: 255, blue: 255 } +}); +function setSelectedObject(id, type) { + Selection.clearSelectedItemsList(SELECT_LIST); + if (id !== undefined && !Uuid.isNull(id)) { + Selection.addToSelectedItemsList(SELECT_LIST, type.toLowerCase(), id); + } +} + +function setWindow(window) { + if (activeWindow !== undefined) { + setSelectedObject(Uuid.NULL, ""); + activeWindow.closed.disconnect(killWindow); + activeWindow.fromQml.disconnect(fromQml); + Controller.mousePressEvent.disconnect(mousePressEvent); + Controller.mouseReleaseEvent.disconnect(mouseReleaseEvent); + activeWindow.close(); + } + if (window !== undefined) { + window.closed.connect(killWindow); + window.fromQml.connect(fromQml); + Controller.mousePressEvent.connect(mousePressEvent); + Controller.mouseReleaseEvent.connect(mouseReleaseEvent); + } + activeWindow = window; +} + +function cleanup() { + setWindow(undefined); + Selection.disableListHighlight(SELECT_LIST); +} + +Script.scriptEnding.connect(cleanup); + +module.exports = { + setWindow: setWindow +}; diff --git a/scripts/developer/utilities/render/materialInspector.qml b/scripts/developer/utilities/render/materialInspector.qml new file mode 100644 index 0000000000..d4dad203cd --- /dev/null +++ b/scripts/developer/utilities/render/materialInspector.qml @@ -0,0 +1,65 @@ +// +// materialInspector.qml +// +// Created by Sabrina Shanman on 2019-01-16 +// Copyright 2019 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or https://www.apache.org/licenses/LICENSE-2.0.html +// +import QtQuick 2.7 +import QtQuick.Controls 2.3 as Original +import QtQuick.Layouts 1.3 + +import stylesUit 1.0 +import controlsUit 1.0 as HifiControls + +Rectangle { + HifiConstants { id: hifi;} + color: Qt.rgba(hifi.colors.baseGray.r, hifi.colors.baseGray.g, hifi.colors.baseGray.b, 0.8); + id: root; + + function fromScript(message) { + switch (message.method) { + case "setObjectInfo": + entityIDInfo.text = "Type: " + message.params.type + "\nID: " + message.params.id + "\nMesh Part: " + message.params.meshPart; + break; + case "setMaterialJSON": + materialJSONText.text = message.params.materialJSONText; + break; + } + } + + Rectangle { + id: entityIDContainer + height: 52 + width: root.width + color: Qt.rgba(root.color.r * 0.7, root.color.g * 0.7, root.color.b * 0.7, 0.8); + TextEdit { + id: entityIDInfo + text: "Type: Unknown\nID: None\nMesh Part: Unknown" + font.pointSize: 9 + color: "#FFFFFF" + readOnly: true + selectByMouse: true + } + } + + Original.ScrollView { + anchors.top: entityIDContainer.bottom + height: root.height - entityIDContainer.height + width: root.width + clip: true + Original.ScrollBar.horizontal.policy: Original.ScrollBar.AlwaysOff + TextEdit { + id: materialJSONText + text: "Click an object to get material JSON" + width: root.width + font.pointSize: 10 + color: "#FFFFFF" + readOnly: true + selectByMouse: true + wrapMode: Text.WordWrap + } + } +} diff --git a/scripts/system/assets/data/createAppTooltips.json b/scripts/system/assets/data/createAppTooltips.json index cb194c9d66..bf3ff3f324 100644 --- a/scripts/system/assets/data/createAppTooltips.json +++ b/scripts/system/assets/data/createAppTooltips.json @@ -39,6 +39,14 @@ "leftMargin": { "tooltip": "The left margin, in meters." }, + "zoneShapeType": { + "tooltip": "The shape of the volume in which the zone's lighting effects and avatar permissions have effect.", + "jsPropertyName": "shapeType" + }, + "zoneCompoundShapeURL": { + "tooltip": "The model file to use for the compound shape if Shape Type is \"Use Compound Shape URL\".", + "jsPropertyName": "compoundShapeURL" + }, "flyingAllowed": { "tooltip": "If enabled, users can fly in the zone." }, @@ -133,7 +141,7 @@ "tooltip": "The shape of the collision hull used if collisions are enabled. This affects how an entity collides." }, "compoundShapeURL": { - "tooltip": "The OBJ file to use for the compound shape if Collision Shape is \"compound\"." + "tooltip": "The model file to use for the compound shape if Collision Shape is \"Compound\"." }, "animation.url": { "tooltip": "An animation to play on the model." diff --git a/scripts/system/avatarapp.js b/scripts/system/avatarapp.js index 2b9a738202..fb61b914a3 100644 --- a/scripts/system/avatarapp.js +++ b/scripts/system/avatarapp.js @@ -62,6 +62,7 @@ function getMyAvatar() { function getMyAvatarSettings() { return { dominantHand: MyAvatar.getDominantHand(), + hmdAvatarAlignmentType: MyAvatar.getHmdAvatarAlignmentType(), collisionsEnabled: MyAvatar.getCollisionsEnabled(), otherAvatarsCollisionsEnabled: MyAvatar.getOtherAvatarsCollisionsEnabled(), collisionSoundUrl : MyAvatar.collisionSoundURL, @@ -70,9 +71,9 @@ function getMyAvatarSettings() { } } -function updateAvatarWearables(avatar, callback) { +function updateAvatarWearables(avatar, callback, wearablesOverride) { executeLater(function() { - var wearables = getMyAvatarWearables(); + var wearables = wearablesOverride ? wearablesOverride : getMyAvatarWearables(); avatar[ENTRY_AVATAR_ENTITIES] = wearables; sendToQml({'method' : 'wearablesUpdated', 'wearables' : wearables}) @@ -129,6 +130,13 @@ function onDominantHandChanged(dominantHand) { } } +function onHmdAvatarAlignmentTypeChanged(type) { + if (currentAvatarSettings.hmdAvatarAlignmentType !== type) { + currentAvatarSettings.hmdAvatarAlignmentType = type; + sendToQml({'method' : 'settingChanged', 'name' : 'hmdAvatarAlignmentType', 'value' : type}); + } +} + function onCollisionsEnabledChanged(enabled) { if(currentAvatarSettings.collisionsEnabled !== enabled) { currentAvatarSettings.collisionsEnabled = enabled; @@ -210,7 +218,7 @@ function fromQml(message) { // messages are {method, params}, like json-rpc. See break; case 'adjustWearable': if(message.properties.localRotationAngles) { - message.properties.localRotation = Quat.fromVec3Degrees(message.properties.localRotationAngles) + message.properties.localRotation = Quat.fromVec3Degrees(message.properties.localRotationAngles); } Entities.editEntity(message.entityID, message.properties); @@ -235,7 +243,7 @@ function fromQml(message) { // messages are {method, params}, like json-rpc. See // revert changes using snapshot of wearables if(currentAvatarWearablesBackup !== null) { AvatarBookmarks.updateAvatarEntities(currentAvatarWearablesBackup); - updateAvatarWearables(currentAvatar); + updateAvatarWearables(currentAvatar, null, currentAvatarWearablesBackup); } } else { sendToQml({'method' : 'updateAvatarInBookmarks'}); @@ -331,6 +339,7 @@ function fromQml(message) { // messages are {method, params}, like json-rpc. See currentAvatar.avatarScale = message.avatarScale; MyAvatar.setDominantHand(message.settings.dominantHand); + MyAvatar.setHmdAvatarAlignmentType(message.settings.hmdAvatarAlignmentType); MyAvatar.setOtherAvatarsCollisionsEnabled(message.settings.otherAvatarsCollisionsEnabled); MyAvatar.setCollisionsEnabled(message.settings.collisionsEnabled); MyAvatar.collisionSoundURL = message.settings.collisionSoundUrl; @@ -521,6 +530,7 @@ function off() { Entities.deletingWearable.disconnect(onDeletingWearable); MyAvatar.skeletonModelURLChanged.disconnect(onSkeletonModelURLChanged); MyAvatar.dominantHandChanged.disconnect(onDominantHandChanged); + MyAvatar.hmdAvatarAlignmentTypeChanged.disconnect(onHmdAvatarAlignmentTypeChanged); MyAvatar.collisionsEnabledChanged.disconnect(onCollisionsEnabledChanged); MyAvatar.otherAvatarsCollisionsEnabledChanged.disconnect(onOtherAvatarsCollisionsEnabledChanged); MyAvatar.newCollisionSoundURL.disconnect(onNewCollisionSoundUrl); @@ -542,6 +552,7 @@ function on() { Entities.deletingWearable.connect(onDeletingWearable); MyAvatar.skeletonModelURLChanged.connect(onSkeletonModelURLChanged); MyAvatar.dominantHandChanged.connect(onDominantHandChanged); + MyAvatar.hmdAvatarAlignmentTypeChanged.connect(onHmdAvatarAlignmentTypeChanged); MyAvatar.collisionsEnabledChanged.connect(onCollisionsEnabledChanged); MyAvatar.otherAvatarsCollisionsEnabledChanged.connect(onOtherAvatarsCollisionsEnabledChanged); MyAvatar.newCollisionSoundURL.connect(onNewCollisionSoundUrl); diff --git a/scripts/system/controllers/controllerModules/equipEntity.js b/scripts/system/controllers/controllerModules/equipEntity.js index c61e46c8eb..b1c1bc7765 100644 --- a/scripts/system/controllers/controllerModules/equipEntity.js +++ b/scripts/system/controllers/controllerModules/equipEntity.js @@ -6,11 +6,11 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -/* global Script, Entities, MyAvatar, Controller, RIGHT_HAND, LEFT_HAND, Camera, print, - getControllerJointIndex, enableDispatcherModule, disableDispatcherModule, entityIsFarGrabbedByOther, - Messages, makeDispatcherModuleParameters, makeRunningValues, Settings, entityHasActions, - Vec3, Overlays, flatten, Xform, getControllerWorldLocation, ensureDynamic, entityIsCloneable, - cloneEntity, DISPATCHER_PROPERTIES, Uuid, unhighlightTargetEntity, isInEditMode, getGrabbableData +/* global Script, Entities, MyAvatar, Controller, RIGHT_HAND, LEFT_HAND, Camera, print, getControllerJointIndex, + enableDispatcherModule, disableDispatcherModule, entityIsFarGrabbedByOther, Messages, makeDispatcherModuleParameters, + makeRunningValues, Settings, entityHasActions, Vec3, Overlays, flatten, Xform, getControllerWorldLocation, ensureDynamic, + entityIsCloneable, cloneEntity, DISPATCHER_PROPERTIES, Uuid, unhighlightTargetEntity, isInEditMode, getGrabbableData, + entityIsEquippable */ Script.include("/~/system/libraries/Xform.js"); @@ -767,7 +767,7 @@ EquipHotspotBuddy.prototype.update = function(deltaTime, timestamp, controllerDa var entityProperties = Entities.getEntityProperties(entityID, DISPATCHER_PROPERTIES); entityProperties.id = entityID; var hasEquipData = getWearableData(entityProperties); - if (hasEquipData && entityProperties.parentID === EMPTY_PARENT_ID && !entityIsFarGrabbedByOther(entityID)) { + if (hasEquipData && entityIsEquippable(entityProperties)) { entityProperties.id = entityID; var rightHandPosition = MyAvatar.getJointPosition("RightHand"); var leftHandPosition = MyAvatar.getJointPosition("LeftHand"); diff --git a/scripts/system/controllers/controllerModules/farActionGrabEntityDynOnly.js b/scripts/system/controllers/controllerModules/farActionGrabEntityDynOnly.js deleted file mode 100644 index 0ba3dd6e6b..0000000000 --- a/scripts/system/controllers/controllerModules/farActionGrabEntityDynOnly.js +++ /dev/null @@ -1,572 +0,0 @@ -"use strict"; - -// farActionGrabEntity.js -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html - -/* jslint bitwise: true */ - -/* global Script, Controller, RIGHT_HAND, LEFT_HAND, Mat4, MyAvatar, Vec3, Camera, Quat, getEnabledModuleByName, - makeRunningValues, Entities, enableDispatcherModule, disableDispatcherModule, entityIsGrabbable, - makeDispatcherModuleParameters, MSECS_PER_SEC, HAPTIC_PULSE_STRENGTH, HAPTIC_PULSE_DURATION, TRIGGER_OFF_VALUE, - TRIGGER_ON_VALUE, ZERO_VEC, getControllerWorldLocation, projectOntoEntityXYPlane, ContextOverlay, HMD, - Picks, makeLaserLockInfo, makeLaserParams, AddressManager, getEntityParents, Selection, DISPATCHER_HOVERING_LIST, - Uuid, worldPositionToRegistrationFrameMatrix, DISPATCHER_PROPERTIES -*/ - -Script.include("/~/system/libraries/controllerDispatcherUtils.js"); -Script.include("/~/system/libraries/controllers.js"); - -(function() { - - var MARGIN = 25; - - function TargetObject(entityID, entityProps) { - this.entityID = entityID; - this.entityProps = entityProps; - this.targetEntityID = null; - this.targetEntityProps = null; - - this.getTargetEntity = function() { - var parentPropsLength = this.parentProps.length; - if (parentPropsLength !== 0) { - var targetEntity = { - id: this.parentProps[parentPropsLength - 1].id, - props: this.parentProps[parentPropsLength - 1]}; - this.targetEntityID = targetEntity.id; - this.targetEntityProps = targetEntity.props; - return targetEntity; - } - this.targetEntityID = this.entityID; - this.targetEntityProps = this.entityProps; - return { - id: this.entityID, - props: this.entityProps}; - }; - } - - function FarActionGrabEntity(hand) { - this.hand = hand; - this.grabbing = false; - this.grabbedThingID = null; - this.targetObject = null; - this.actionID = null; // action this script created... - this.entityToLockOnto = null; - this.potentialEntityWithContextOverlay = false; - this.entityWithContextOverlay = false; - this.contextOverlayTimer = false; - this.locked = false; - this.highlightedEntity = null; - this.reticleMinX = MARGIN; - this.reticleMaxX = 0; - this.reticleMinY = MARGIN; - this.reticleMaxY = 0; - - var ACTION_TTL = 15; // seconds - - var DISTANCE_HOLDING_RADIUS_FACTOR = 3.5; // multiplied by distance between hand and object - var DISTANCE_HOLDING_ACTION_TIMEFRAME = 0.1; // how quickly objects move to their new position - var DISTANCE_HOLDING_UNITY_MASS = 1200; // The mass at which the distance holding action timeframe is unmodified - var DISTANCE_HOLDING_UNITY_DISTANCE = 6; // The distance at which the distance holding action timeframe is unmodified - - this.parameters = makeDispatcherModuleParameters( - 550, - this.hand === RIGHT_HAND ? ["rightHand"] : ["leftHand"], - [], - 100, - makeLaserParams(this.hand, false)); - - - this.handToController = function() { - return (this.hand === RIGHT_HAND) ? Controller.Standard.RightHand : Controller.Standard.LeftHand; - }; - - this.distanceGrabTimescale = function(mass, distance) { - var timeScale = DISTANCE_HOLDING_ACTION_TIMEFRAME * mass / - DISTANCE_HOLDING_UNITY_MASS * distance / - DISTANCE_HOLDING_UNITY_DISTANCE; - if (timeScale < DISTANCE_HOLDING_ACTION_TIMEFRAME) { - timeScale = DISTANCE_HOLDING_ACTION_TIMEFRAME; - } - return timeScale; - }; - - this.getMass = function(dimensions, density) { - return (dimensions.x * dimensions.y * dimensions.z) * density; - }; - - this.startFarGrabAction = function (controllerData, grabbedProperties) { - var controllerLocation = controllerData.controllerLocations[this.hand]; - var worldControllerPosition = controllerLocation.position; - var worldControllerRotation = controllerLocation.orientation; - - // transform the position into room space - var worldToSensorMat = Mat4.inverse(MyAvatar.getSensorToWorldMatrix()); - var roomControllerPosition = Mat4.transformPoint(worldToSensorMat, worldControllerPosition); - - var now = Date.now(); - - // add the action and initialize some variables - this.currentObjectPosition = grabbedProperties.position; - this.currentObjectRotation = grabbedProperties.rotation; - this.currentObjectTime = now; - this.currentCameraOrientation = Camera.orientation; - - this.grabRadius = this.grabbedDistance; - this.grabRadialVelocity = 0.0; - - // offset between controller vector at the grab radius and the entity position - var targetPosition = Vec3.multiply(this.grabRadius, Quat.getUp(worldControllerRotation)); - targetPosition = Vec3.sum(targetPosition, worldControllerPosition); - this.offsetPosition = Vec3.subtract(this.currentObjectPosition, targetPosition); - - // compute a constant based on the initial conditions which we use below to exaggerate hand motion - // onto the held object - this.radiusScalar = Math.log(this.grabRadius + 1.0); - if (this.radiusScalar < 1.0) { - this.radiusScalar = 1.0; - } - - // compute the mass for the purpose of energy and how quickly to move object - this.mass = this.getMass(grabbedProperties.dimensions, grabbedProperties.density); - var distanceToObject = Vec3.length(Vec3.subtract(MyAvatar.position, grabbedProperties.position)); - var timeScale = this.distanceGrabTimescale(this.mass, distanceToObject); - this.linearTimeScale = timeScale; - this.actionID = Entities.addAction("far-grab", this.grabbedThingID, { - targetPosition: this.currentObjectPosition, - linearTimeScale: timeScale, - targetRotation: this.currentObjectRotation, - angularTimeScale: timeScale, - tag: "far-grab-" + MyAvatar.sessionUUID, - ttl: ACTION_TTL - }); - if (this.actionID === Uuid.NULL) { - this.actionID = null; - } - - if (this.actionID !== null) { - var args = [this.hand === RIGHT_HAND ? "right" : "left", MyAvatar.sessionUUID]; - Entities.callEntityMethod(this.grabbedThingID, "startDistanceGrab", args); - } - - Controller.triggerHapticPulse(HAPTIC_PULSE_STRENGTH, HAPTIC_PULSE_DURATION, this.hand); - this.previousRoomControllerPosition = roomControllerPosition; - this.grabbing = true; - }; - - this.continueDistanceHolding = function(controllerData) { - var controllerLocation = controllerData.controllerLocations[this.hand]; - var worldControllerPosition = controllerLocation.position; - var worldControllerRotation = controllerLocation.orientation; - - // also transform the position into room space - var worldToSensorMat = Mat4.inverse(MyAvatar.getSensorToWorldMatrix()); - var roomControllerPosition = Mat4.transformPoint(worldToSensorMat, worldControllerPosition); - - var grabbedProperties = Entities.getEntityProperties(this.grabbedThingID, DISPATCHER_PROPERTIES); - var now = Date.now(); - var deltaObjectTime = (now - this.currentObjectTime) / MSECS_PER_SEC; // convert to seconds - this.currentObjectTime = now; - - // the action was set up when this.distanceHolding was called. update the targets. - var radius = Vec3.distance(this.currentObjectPosition, worldControllerPosition) * - this.radiusScalar * DISTANCE_HOLDING_RADIUS_FACTOR; - if (radius < 1.0) { - radius = 1.0; - } - - var roomHandDelta = Vec3.subtract(roomControllerPosition, this.previousRoomControllerPosition); - var worldHandDelta = Mat4.transformVector(MyAvatar.getSensorToWorldMatrix(), roomHandDelta); - var handMoved = Vec3.multiply(worldHandDelta, radius); - this.currentObjectPosition = Vec3.sum(this.currentObjectPosition, handMoved); - - var args = [this.hand === RIGHT_HAND ? "right" : "left", MyAvatar.sessionUUID]; - Entities.callEntityMethod(this.grabbedThingID, "continueDistanceGrab", args); - - // Update radialVelocity - var lastVelocity = Vec3.multiply(worldHandDelta, 1.0 / deltaObjectTime); - var delta = Vec3.normalize(Vec3.subtract(grabbedProperties.position, worldControllerPosition)); - var newRadialVelocity = Vec3.dot(lastVelocity, delta); - - var VELOCITY_AVERAGING_TIME = 0.016; - var blendFactor = deltaObjectTime / VELOCITY_AVERAGING_TIME; - if (blendFactor < 0.0) { - blendFactor = 0.0; - } else if (blendFactor > 1.0) { - blendFactor = 1.0; - } - this.grabRadialVelocity = blendFactor * newRadialVelocity + (1.0 - blendFactor) * this.grabRadialVelocity; - - var RADIAL_GRAB_AMPLIFIER = 10.0; - if (Math.abs(this.grabRadialVelocity) > 0.0) { - this.grabRadius = this.grabRadius + (this.grabRadialVelocity * deltaObjectTime * - this.grabRadius * RADIAL_GRAB_AMPLIFIER); - } - - // don't let grabRadius go all the way to zero, because it can't come back from that - var MINIMUM_GRAB_RADIUS = 0.1; - if (this.grabRadius < MINIMUM_GRAB_RADIUS) { - this.grabRadius = MINIMUM_GRAB_RADIUS; - } - var newTargetPosition = Vec3.multiply(this.grabRadius, Quat.getUp(worldControllerRotation)); - newTargetPosition = Vec3.sum(newTargetPosition, worldControllerPosition); - newTargetPosition = Vec3.sum(newTargetPosition, this.offsetPosition); - - // XXX - // this.maybeScale(grabbedProperties); - - var distanceToObject = Vec3.length(Vec3.subtract(MyAvatar.position, this.currentObjectPosition)); - - this.linearTimeScale = (this.linearTimeScale / 2); - if (this.linearTimeScale <= DISTANCE_HOLDING_ACTION_TIMEFRAME) { - this.linearTimeScale = DISTANCE_HOLDING_ACTION_TIMEFRAME; - } - var success = Entities.updateAction(this.grabbedThingID, this.actionID, { - targetPosition: newTargetPosition, - linearTimeScale: this.linearTimeScale, - targetRotation: this.currentObjectRotation, - angularTimeScale: this.distanceGrabTimescale(this.mass, distanceToObject), - ttl: ACTION_TTL - }); - if (!success) { - print("farActionGrabEntity continueDistanceHolding -- updateAction failed: " + this.actionID); - this.actionID = null; - } - - this.previousRoomControllerPosition = roomControllerPosition; - }; - - this.endFarGrabAction = function () { - this.distanceHolding = false; - this.distanceRotating = false; - Entities.deleteAction(this.grabbedThingID, this.actionID); - - var args = [this.hand === RIGHT_HAND ? "right" : "left", MyAvatar.sessionUUID]; - Entities.callEntityMethod(this.grabbedThingID, "releaseGrab", args); - this.actionID = null; - this.grabbedThingID = null; - this.targetObject = null; - this.potentialEntityWithContextOverlay = false; - this.grabbing = false; - }; - - this.updateRecommendedArea = function() { - var dims = Controller.getViewportDimensions(); - this.reticleMaxX = dims.x - MARGIN; - this.reticleMaxY = dims.y - MARGIN; - }; - - this.calculateNewReticlePosition = function(intersection) { - this.updateRecommendedArea(); - var point2d = HMD.overlayFromWorldPoint(intersection); - point2d.x = Math.max(this.reticleMinX, Math.min(point2d.x, this.reticleMaxX)); - point2d.y = Math.max(this.reticleMinY, Math.min(point2d.y, this.reticleMaxY)); - return point2d; - }; - - this.notPointingAtEntity = function(controllerData) { - var intersection = controllerData.rayPicks[this.hand]; - var entityProperty = Entities.getEntityProperties(intersection.objectID, DISPATCHER_PROPERTIES); - var entityType = entityProperty.type; - var hudRayPick = controllerData.hudRayPicks[this.hand]; - var point2d = this.calculateNewReticlePosition(hudRayPick.intersection); - if ((intersection.type === Picks.INTERSECTED_ENTITY && entityType === "Web") || - intersection.type === Picks.INTERSECTED_OVERLAY || Window.isPointOnDesktopWindow(point2d)) { - return true; - } - return false; - }; - - this.distanceRotate = function(otherFarGrabModule) { - this.distanceRotating = true; - this.distanceHolding = false; - - var worldControllerRotation = getControllerWorldLocation(this.handToController(), true).orientation; - var controllerRotationDelta = - Quat.multiply(worldControllerRotation, Quat.inverse(this.previousWorldControllerRotation)); - // Rotate entity by twice the delta rotation. - controllerRotationDelta = Quat.multiply(controllerRotationDelta, controllerRotationDelta); - - // Perform the rotation in the translation controller's action update. - otherFarGrabModule.currentObjectRotation = Quat.multiply(controllerRotationDelta, - otherFarGrabModule.currentObjectRotation); - - this.previousWorldControllerRotation = worldControllerRotation; - }; - - this.prepareDistanceRotatingData = function(controllerData) { - var intersection = controllerData.rayPicks[this.hand]; - - var controllerLocation = getControllerWorldLocation(this.handToController(), true); - var worldControllerPosition = controllerLocation.position; - var worldControllerRotation = controllerLocation.orientation; - - var grabbedProperties = Entities.getEntityProperties(intersection.objectID, DISPATCHER_PROPERTIES); - this.currentObjectPosition = grabbedProperties.position; - this.grabRadius = intersection.distance; - - // Offset between controller vector at the grab radius and the entity position. - var targetPosition = Vec3.multiply(this.grabRadius, Quat.getUp(worldControllerRotation)); - targetPosition = Vec3.sum(targetPosition, worldControllerPosition); - this.offsetPosition = Vec3.subtract(this.currentObjectPosition, targetPosition); - - // Initial controller rotation. - this.previousWorldControllerRotation = worldControllerRotation; - }; - - this.destroyContextOverlay = function(controllerData) { - if (this.entityWithContextOverlay) { - ContextOverlay.destroyContextOverlay(this.entityWithContextOverlay); - this.entityWithContextOverlay = false; - this.potentialEntityWithContextOverlay = false; - } - }; - - this.targetIsNull = function() { - var properties = Entities.getEntityProperties(this.grabbedThingID, DISPATCHER_PROPERTIES); - if (Object.keys(properties).length === 0 && this.distanceHolding) { - return true; - } - return false; - }; - - this.getTargetProps = function (controllerData) { - var targetEntityID = controllerData.rayPicks[this.hand].objectID; - if (targetEntityID) { - return Entities.getEntityProperties(targetEntityID, DISPATCHER_PROPERTIES); - } - return null; - }; - - this.isReady = function (controllerData) { - if (HMD.active) { - if (this.notPointingAtEntity(controllerData)) { - return makeRunningValues(false, [], []); - } - - this.distanceHolding = false; - this.distanceRotating = false; - - if (controllerData.triggerValues[this.hand] > TRIGGER_ON_VALUE) { - this.prepareDistanceRotatingData(controllerData); - return makeRunningValues(true, [], []); - } else { - this.destroyContextOverlay(); - return makeRunningValues(false, [], []); - } - } - return makeRunningValues(false, [], []); - }; - - this.run = function (controllerData) { - if (controllerData.triggerValues[this.hand] < TRIGGER_OFF_VALUE || this.targetIsNull()) { - this.endFarGrabAction(); - Selection.removeFromSelectedItemsList(DISPATCHER_HOVERING_LIST, "entity", - this.highlightedEntity); - this.highlightedEntity = null; - return makeRunningValues(false, [], []); - } - this.intersectionDistance = controllerData.rayPicks[this.hand].distance; - - var otherModuleName = this.hand === RIGHT_HAND ? "LeftFarActionGrabEntity" : "RightFarActionGrabEntity"; - var otherFarGrabModule = getEnabledModuleByName(otherModuleName); - - // gather up the readiness of the near-grab modules - var nearGrabNames = [ - this.hand === RIGHT_HAND ? "RightScaleAvatar" : "LeftScaleAvatar", - this.hand === RIGHT_HAND ? "RightFarTriggerEntity" : "LeftFarTriggerEntity", - this.hand === RIGHT_HAND ? "RightNearActionGrabEntity" : "LeftNearActionGrabEntity", - this.hand === RIGHT_HAND ? "RightNearParentingGrabEntity" : "LeftNearParentingGrabEntity" - ]; - if (!this.grabbing) { - nearGrabNames.push(this.hand === RIGHT_HAND ? "RightNearParentingGrabOverlay" : "LeftNearParentingGrabOverlay"); - nearGrabNames.push(this.hand === RIGHT_HAND ? "RightNearTabletHighlight" : "LeftNearTabletHighlight"); - } - - var nearGrabReadiness = []; - for (var i = 0; i < nearGrabNames.length; i++) { - var nearGrabModule = getEnabledModuleByName(nearGrabNames[i]); - var ready = nearGrabModule ? nearGrabModule.isReady(controllerData) : makeRunningValues(false, [], []); - nearGrabReadiness.push(ready); - } - - if (this.actionID) { - // if we are doing a distance grab and the object or tablet gets close enough to the controller, - // stop the far-grab so the near-grab or equip can take over. - for (var k = 0; k < nearGrabReadiness.length; k++) { - if (nearGrabReadiness[k].active && (nearGrabReadiness[k].targets[0] === this.grabbedThingID || - HMD.tabletID && nearGrabReadiness[k].targets[0] === HMD.tabletID)) { - this.endFarGrabAction(); - return makeRunningValues(false, [], []); - } - } - - this.continueDistanceHolding(controllerData); - } else { - // if we are doing a distance search and this controller moves into a position - // where it could near-grab something, stop searching. - for (var j = 0; j < nearGrabReadiness.length; j++) { - if (nearGrabReadiness[j].active) { - this.endFarGrabAction(); - return makeRunningValues(false, [], []); - } - } - - var rayPickInfo = controllerData.rayPicks[this.hand]; - if (rayPickInfo.type === Picks.INTERSECTED_ENTITY) { - if (controllerData.triggerClicks[this.hand]) { - var entityID = rayPickInfo.objectID; - Selection.removeFromSelectedItemsList(DISPATCHER_HOVERING_LIST, "entity", - this.highlightedEntity); - this.highlightedEntity = null; - var targetProps = Entities.getEntityProperties(entityID, DISPATCHER_PROPERTIES); - if (targetProps.href !== "") { - AddressManager.handleLookupString(targetProps.href); - return makeRunningValues(false, [], []); - } - - this.targetObject = new TargetObject(entityID, targetProps); - this.targetObject.parentProps = getEntityParents(targetProps); - - if (this.contextOverlayTimer) { - Script.clearTimeout(this.contextOverlayTimer); - } - this.contextOverlayTimer = false; - if (entityID === this.entityWithContextOverlay) { - this.destroyContextOverlay(); - } else { - Selection.removeFromSelectedItemsList("contextOverlayHighlightList", "entity", entityID); - } - - var targetEntity = this.targetObject.getTargetEntity(); - entityID = targetEntity.id; - targetProps = targetEntity.props; - - if (!targetProps.dynamic && !this.targetObject.entityProps.dynamic) { - // let farParentGrabEntity handle it - return makeRunningValues(false, [], []); - } - - if (entityIsGrabbable(targetProps) || entityIsGrabbable(this.targetObject.entityProps)) { - if (!this.distanceRotating) { - this.grabbedThingID = entityID; - this.grabbedDistance = rayPickInfo.distance; - } - - if (otherFarGrabModule.grabbedThingID === this.grabbedThingID && - otherFarGrabModule.distanceHolding) { - this.prepareDistanceRotatingData(controllerData); - this.distanceRotate(otherFarGrabModule); - } else { - this.distanceHolding = true; - this.distanceRotating = false; - this.startFarGrabAction(controllerData, targetProps); - } - } - } else { - var targetEntityID = rayPickInfo.objectID; - if (this.highlightedEntity !== targetEntityID) { - Selection.removeFromSelectedItemsList(DISPATCHER_HOVERING_LIST, "entity", - this.highlightedEntity); - var selectionTargetProps = Entities.getEntityProperties(targetEntityID, DISPATCHER_PROPERTIES); - - var selectionTargetObject = new TargetObject(targetEntityID, selectionTargetProps); - selectionTargetObject.parentProps = getEntityParents(selectionTargetProps); - var selectionTargetEntity = selectionTargetObject.getTargetEntity(); - - if (entityIsGrabbable(selectionTargetEntity.props) || - entityIsGrabbable(selectionTargetObject.entityProps)) { - - Selection.addToSelectedItemsList(DISPATCHER_HOVERING_LIST, "entity", rayPickInfo.objectID); - } - this.highlightedEntity = rayPickInfo.objectID; - } - - if (!this.entityWithContextOverlay) { - var _this = this; - - if (_this.potentialEntityWithContextOverlay !== rayPickInfo.objectID) { - if (_this.contextOverlayTimer) { - Script.clearTimeout(_this.contextOverlayTimer); - } - _this.contextOverlayTimer = false; - _this.potentialEntityWithContextOverlay = rayPickInfo.objectID; - } - - if (!_this.contextOverlayTimer) { - _this.contextOverlayTimer = Script.setTimeout(function () { - if (!_this.entityWithContextOverlay && - _this.contextOverlayTimer && - _this.potentialEntityWithContextOverlay === rayPickInfo.objectID) { - var pEvProps = Entities.getEntityProperties(rayPickInfo.objectID, - DISPATCHER_PROPERTIES); - var pointerEvent = { - type: "Move", - id: _this.hand + 1, // 0 is reserved for hardware mouse - pos2D: projectOntoEntityXYPlane(rayPickInfo.objectID, - rayPickInfo.intersection, pEvProps), - pos3D: rayPickInfo.intersection, - normal: rayPickInfo.surfaceNormal, - direction: Vec3.subtract(ZERO_VEC, rayPickInfo.surfaceNormal), - button: "Secondary" - }; - if (ContextOverlay.createOrDestroyContextOverlay(rayPickInfo.objectID, pointerEvent)) { - _this.entityWithContextOverlay = rayPickInfo.objectID; - } - } - _this.contextOverlayTimer = false; - }, 500); - } - } - } - } else if (this.distanceRotating) { - this.distanceRotate(otherFarGrabModule); - } else if (this.highlightedEntity) { - Selection.removeFromSelectedItemsList(DISPATCHER_HOVERING_LIST, "entity", this.highlightedEntity); - this.highlightedEntity = null; - } - } - return this.exitIfDisabled(controllerData); - }; - - this.exitIfDisabled = function(controllerData) { - var moduleName = this.hand === RIGHT_HAND ? "RightDisableModules" : "LeftDisableModules"; - var disableModule = getEnabledModuleByName(moduleName); - if (disableModule) { - if (disableModule.disableModules) { - this.endFarGrabAction(); - Selection.removeFromSelectedItemsList(DISPATCHER_HOVERING_LIST, "entity", - this.highlightedEntity); - this.highlightedEntity = null; - return makeRunningValues(false, [], []); - } - } - var grabbedThing = (this.distanceHolding || this.distanceRotating) ? this.targetObject.entityID : null; - var offset = this.calculateOffset(controllerData); - var laserLockInfo = makeLaserLockInfo(grabbedThing, false, this.hand, offset); - return makeRunningValues(true, [], [], laserLockInfo); - }; - - this.calculateOffset = function(controllerData) { - if (this.distanceHolding || this.distanceRotating) { - var targetProps = Entities.getEntityProperties(this.targetObject.entityID, - [ "position", "rotation", "registrationPoint", "dimensions" ]); - return worldPositionToRegistrationFrameMatrix(targetProps, controllerData.rayPicks[this.hand].intersection); - } - return undefined; - }; - } - - var leftFarActionGrabEntity = new FarActionGrabEntity(LEFT_HAND); - var rightFarActionGrabEntity = new FarActionGrabEntity(RIGHT_HAND); - - enableDispatcherModule("LeftFarActionGrabEntity", leftFarActionGrabEntity); - enableDispatcherModule("RightFarActionGrabEntity", rightFarActionGrabEntity); - - function cleanup() { - disableDispatcherModule("LeftFarActionGrabEntity"); - disableDispatcherModule("RightFarActionGrabEntity"); - } - Script.scriptEnding.connect(cleanup); -}()); diff --git a/scripts/system/controllers/controllerModules/farGrabEntity.js b/scripts/system/controllers/controllerModules/farGrabEntity.js index dab1aa97af..197a809e91 100644 --- a/scripts/system/controllers/controllerModules/farGrabEntity.js +++ b/scripts/system/controllers/controllerModules/farGrabEntity.js @@ -11,7 +11,7 @@ Entities, enableDispatcherModule, disableDispatcherModule, entityIsGrabbable, makeDispatcherModuleParameters, MSECS_PER_SEC, HAPTIC_PULSE_STRENGTH, HAPTIC_PULSE_DURATION, TRIGGER_OFF_VALUE, TRIGGER_ON_VALUE, ZERO_VEC, projectOntoEntityXYPlane, ContextOverlay, HMD, Picks, makeLaserLockInfo, makeLaserParams, AddressManager, - getEntityParents, Selection, DISPATCHER_HOVERING_LIST, unhighlightTargetEntity, Messages, findGroupParent, + getEntityParents, Selection, DISPATCHER_HOVERING_LIST, unhighlightTargetEntity, Messages, findGrabbableGroupParent, worldPositionToRegistrationFrameMatrix, DISPATCHER_PROPERTIES */ @@ -308,7 +308,7 @@ Script.include("/~/system/libraries/controllers.js"); var gtProps = Entities.getEntityProperties(targetEntity, DISPATCHER_PROPERTIES); if (entityIsGrabbable(gtProps)) { // if we've attempted to grab a child, roll up to the root of the tree - var groupRootProps = findGroupParent(controllerData, gtProps); + var groupRootProps = findGrabbableGroupParent(controllerData, gtProps); if (entityIsGrabbable(groupRootProps)) { return groupRootProps; } diff --git a/scripts/system/controllers/controllerModules/farParentGrabEntity.js b/scripts/system/controllers/controllerModules/farParentGrabEntity.js deleted file mode 100644 index 9960b08292..0000000000 --- a/scripts/system/controllers/controllerModules/farParentGrabEntity.js +++ /dev/null @@ -1,664 +0,0 @@ -"use strict"; - -// farParentGrabEntity.js -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html - -/* jslint bitwise: true */ - -/* global Script, Controller, RIGHT_HAND, LEFT_HAND, Mat4, MyAvatar, Vec3, Quat, getEnabledModuleByName, makeRunningValues, - Entities, enableDispatcherModule, disableDispatcherModule, entityIsGrabbable, makeDispatcherModuleParameters, MSECS_PER_SEC, - HAPTIC_PULSE_STRENGTH, HAPTIC_PULSE_DURATION, TRIGGER_OFF_VALUE, TRIGGER_ON_VALUE, ZERO_VEC, getControllerWorldLocation, - projectOntoEntityXYPlane, ContextOverlay, HMD, Picks, makeLaserLockInfo, makeLaserParams, AddressManager, - getEntityParents, Selection, DISPATCHER_HOVERING_LIST, unhighlightTargetEntity, Messages, Uuid, findGroupParent, - worldPositionToRegistrationFrameMatrix, DISPATCHER_PROPERTIES, findFarGrabJointChildEntities -*/ - -Script.include("/~/system/libraries/controllerDispatcherUtils.js"); -Script.include("/~/system/libraries/controllers.js"); - -(function() { - var MARGIN = 25; - - function TargetObject(entityID, entityProps) { - this.entityID = entityID; - this.entityProps = entityProps; - this.targetEntityID = null; - this.targetEntityProps = null; - - this.getTargetEntity = function() { - var parentPropsLength = this.parentProps.length; - if (parentPropsLength !== 0) { - var targetEntity = { - id: this.parentProps[parentPropsLength - 1].id, - props: this.parentProps[parentPropsLength - 1]}; - this.targetEntityID = targetEntity.id; - this.targetEntityProps = targetEntity.props; - return targetEntity; - } - this.targetEntityID = this.entityID; - this.targetEntityProps = this.entityProps; - return { - id: this.entityID, - props: this.entityProps}; - }; - } - - function FarParentGrabEntity(hand) { - this.hand = hand; - this.grabbing = false; - this.targetEntityID = null; - this.targetObject = null; - this.previouslyUnhooked = {}; - this.previousParentID = {}; - this.previousParentJointIndex = {}; - this.potentialEntityWithContextOverlay = false; - this.entityWithContextOverlay = false; - this.contextOverlayTimer = false; - this.highlightedEntity = null; - this.reticleMinX = MARGIN; - this.reticleMaxX = 0; - this.reticleMinY = MARGIN; - this.reticleMaxY = 0; - this.lastUnexpectedChildrenCheckTime = 0; - this.endedGrab = 0; - this.MIN_HAPTIC_PULSE_INTERVAL = 500; // ms - - var FAR_GRAB_JOINTS = [65527, 65528]; // FARGRAB_LEFTHAND_INDEX, FARGRAB_RIGHTHAND_INDEX - - var DISTANCE_HOLDING_RADIUS_FACTOR = 3.5; // multiplied by distance between hand and object - var DISTANCE_HOLDING_ACTION_TIMEFRAME = 0.1; // how quickly objects move to their new position - var DISTANCE_HOLDING_UNITY_MASS = 1200; // The mass at which the distance holding action timeframe is unmodified - var DISTANCE_HOLDING_UNITY_DISTANCE = 6; // The distance at which the distance holding action timeframe is unmodified - - this.parameters = makeDispatcherModuleParameters( - 540, - this.hand === RIGHT_HAND ? ["rightHand"] : ["leftHand"], - [], - 100, - makeLaserParams(this.hand, false)); - - - this.handToController = function() { - return (this.hand === RIGHT_HAND) ? Controller.Standard.RightHand : Controller.Standard.LeftHand; - }; - - this.distanceGrabTimescale = function(mass, distance) { - var timeScale = DISTANCE_HOLDING_ACTION_TIMEFRAME * mass / - DISTANCE_HOLDING_UNITY_MASS * distance / - DISTANCE_HOLDING_UNITY_DISTANCE; - if (timeScale < DISTANCE_HOLDING_ACTION_TIMEFRAME) { - timeScale = DISTANCE_HOLDING_ACTION_TIMEFRAME; - } - return timeScale; - }; - - this.getMass = function(dimensions, density) { - return (dimensions.x * dimensions.y * dimensions.z) * density; - }; - - this.thisFarGrabJointIsParent = function(isParentProps) { - if (!isParentProps) { - return false; - } - - if (isParentProps.parentID !== MyAvatar.sessionUUID && isParentProps.parentID !== MyAvatar.SELF_ID) { - return false; - } - - if (isParentProps.parentJointIndex === FAR_GRAB_JOINTS[this.hand]) { - return true; - } - - return false; - }; - - this.startFarParentGrab = function (controllerData, grabbedProperties) { - var controllerLocation = controllerData.controllerLocations[this.hand]; - var worldControllerPosition = controllerLocation.position; - var worldControllerRotation = controllerLocation.orientation; - // transform the position into room space - var worldToSensorMat = Mat4.inverse(MyAvatar.getSensorToWorldMatrix()); - var roomControllerPosition = Mat4.transformPoint(worldToSensorMat, worldControllerPosition); - - var now = Date.now(); - - // add the action and initialize some variables - this.currentObjectPosition = grabbedProperties.position; - this.currentObjectRotation = grabbedProperties.rotation; - this.currentObjectTime = now; - - this.grabRadius = this.grabbedDistance; - this.grabRadialVelocity = 0.0; - - // offset between controller vector at the grab radius and the entity position - var targetPosition = Vec3.multiply(this.grabRadius, Quat.getUp(worldControllerRotation)); - targetPosition = Vec3.sum(targetPosition, worldControllerPosition); - this.offsetPosition = Vec3.subtract(this.currentObjectPosition, targetPosition); - - // compute a constant based on the initial conditions which we use below to exaggerate hand motion - // onto the held object - this.radiusScalar = Math.log(this.grabRadius + 1.0); - if (this.radiusScalar < 1.0) { - this.radiusScalar = 1.0; - } - - // compute the mass for the purpose of energy and how quickly to move object - this.mass = this.getMass(grabbedProperties.dimensions, grabbedProperties.density); - - // Debounce haptic pules. Can occur as near grab controller module vacillates between being ready or not due to - // changing positions and floating point rounding. - if (Date.now() - this.endedGrab > this.MIN_HAPTIC_PULSE_INTERVAL) { - Controller.triggerHapticPulse(HAPTIC_PULSE_STRENGTH, HAPTIC_PULSE_DURATION, this.hand); - } - - unhighlightTargetEntity(this.targetEntityID); - var message = { - hand: this.hand, - entityID: this.targetEntityID - }; - - Messages.sendLocalMessage('Hifi-unhighlight-entity', JSON.stringify(message)); - - var newTargetPosLocal = MyAvatar.worldToJointPoint(grabbedProperties.position); - MyAvatar.setJointTranslation(FAR_GRAB_JOINTS[this.hand], newTargetPosLocal); - - var args = [this.hand === RIGHT_HAND ? "right" : "left", MyAvatar.sessionUUID]; - Entities.callEntityMethod(grabbedProperties.id, "startNearGrab", args); - - var reparentProps = { - parentID: MyAvatar.SELF_ID, - parentJointIndex: FAR_GRAB_JOINTS[this.hand], - localVelocity: {x: 0, y: 0, z: 0}, - localAngularVelocity: {x: 0, y: 0, z: 0} - }; - - if (this.thisFarGrabJointIsParent(grabbedProperties)) { - // this should never happen, but if it does, don't set previous parent to be this hand. - this.previousParentID[grabbedProperties.id] = null; - this.previousParentJointIndex[grabbedProperties.id] = -1; - } else { - this.previousParentID[grabbedProperties.id] = grabbedProperties.parentID; - this.previousParentJointIndex[grabbedProperties.id] = grabbedProperties.parentJointIndex; - } - - this.targetEntityID = grabbedProperties.id; - Entities.editEntity(grabbedProperties.id, reparentProps); - - Messages.sendMessage('Hifi-Object-Manipulation', JSON.stringify({ - action: 'grab', - grabbedEntity: grabbedProperties.id, - joint: this.hand === RIGHT_HAND ? "RightHand" : "LeftHand" - })); - this.grabbing = true; - - this.previousRoomControllerPosition = roomControllerPosition; - }; - - this.continueDistanceHolding = function(controllerData) { - var controllerLocation = controllerData.controllerLocations[this.hand]; - var worldControllerPosition = controllerLocation.position; - var worldControllerRotation = controllerLocation.orientation; - - // also transform the position into room space - var worldToSensorMat = Mat4.inverse(MyAvatar.getSensorToWorldMatrix()); - var roomControllerPosition = Mat4.transformPoint(worldToSensorMat, worldControllerPosition); - - var grabbedProperties = Entities.getEntityProperties(this.targetEntityID, DISPATCHER_PROPERTIES); - var now = Date.now(); - var deltaObjectTime = (now - this.currentObjectTime) / MSECS_PER_SEC; // convert to seconds - this.currentObjectTime = now; - - // the action was set up when this.distanceHolding was called. update the targets. - var radius = Vec3.distance(this.currentObjectPosition, worldControllerPosition) * - this.radiusScalar * DISTANCE_HOLDING_RADIUS_FACTOR; - if (radius < 1.0) { - radius = 1.0; - } - - var roomHandDelta = Vec3.subtract(roomControllerPosition, this.previousRoomControllerPosition); - var worldHandDelta = Mat4.transformVector(MyAvatar.getSensorToWorldMatrix(), roomHandDelta); - var handMoved = Vec3.multiply(worldHandDelta, radius); - this.currentObjectPosition = Vec3.sum(this.currentObjectPosition, handMoved); - - var args = [this.hand === RIGHT_HAND ? "right" : "left", MyAvatar.sessionUUID]; - Entities.callEntityMethod(this.targetEntityID, "continueDistanceGrab", args); - - // Update radialVelocity - var lastVelocity = Vec3.multiply(worldHandDelta, 1.0 / deltaObjectTime); - var delta = Vec3.normalize(Vec3.subtract(grabbedProperties.position, worldControllerPosition)); - var newRadialVelocity = Vec3.dot(lastVelocity, delta); - - var VELOCITY_AVERAGING_TIME = 0.016; - var blendFactor = deltaObjectTime / VELOCITY_AVERAGING_TIME; - if (blendFactor < 0.0) { - blendFactor = 0.0; - } else if (blendFactor > 1.0) { - blendFactor = 1.0; - } - this.grabRadialVelocity = blendFactor * newRadialVelocity + (1.0 - blendFactor) * this.grabRadialVelocity; - - var RADIAL_GRAB_AMPLIFIER = 10.0; - if (Math.abs(this.grabRadialVelocity) > 0.0) { - this.grabRadius = this.grabRadius + (this.grabRadialVelocity * deltaObjectTime * - this.grabRadius * RADIAL_GRAB_AMPLIFIER); - } - - // don't let grabRadius go all the way to zero, because it can't come back from that - var MINIMUM_GRAB_RADIUS = 0.1; - if (this.grabRadius < MINIMUM_GRAB_RADIUS) { - this.grabRadius = MINIMUM_GRAB_RADIUS; - } - var newTargetPosition = Vec3.multiply(this.grabRadius, Quat.getUp(worldControllerRotation)); - newTargetPosition = Vec3.sum(newTargetPosition, worldControllerPosition); - newTargetPosition = Vec3.sum(newTargetPosition, this.offsetPosition); - - // MyAvatar.setJointTranslation(FAR_GRAB_JOINTS[this.hand], MyAvatar.worldToJointPoint(newTargetPosition)); - - // var newTargetPosLocal = Mat4.transformPoint(MyAvatar.getSensorToWorldMatrix(), newTargetPosition); - var newTargetPosLocal = MyAvatar.worldToJointPoint(newTargetPosition); - MyAvatar.setJointTranslation(FAR_GRAB_JOINTS[this.hand], newTargetPosLocal); - - this.previousRoomControllerPosition = roomControllerPosition; - }; - - this.endFarParentGrab = function (controllerData) { - this.endedGrab = Date.now(); - // var endProps = controllerData.nearbyEntityPropertiesByID[this.targetEntityID]; - var endProps = Entities.getEntityProperties(this.targetEntityID, DISPATCHER_PROPERTIES); - if (this.thisFarGrabJointIsParent(endProps)) { - Entities.editEntity(this.targetEntityID, { - parentID: this.previousParentID[this.targetEntityID], - parentJointIndex: this.previousParentJointIndex[this.targetEntityID], - localVelocity: {x: 0, y: 0, z: 0}, - localAngularVelocity: {x: 0, y: 0, z: 0} - }); - } - - var args = [this.hand === RIGHT_HAND ? "right" : "left", MyAvatar.sessionUUID]; - Entities.callEntityMethod(this.targetEntityID, "releaseGrab", args); - Messages.sendMessage('Hifi-Object-Manipulation', JSON.stringify({ - action: 'release', - grabbedEntity: this.targetEntityID, - joint: this.hand === RIGHT_HAND ? "RightHand" : "LeftHand" - })); - unhighlightTargetEntity(this.targetEntityID); - this.grabbing = false; - this.targetEntityID = null; - this.potentialEntityWithContextOverlay = false; - MyAvatar.clearJointData(FAR_GRAB_JOINTS[this.hand]); - }; - - this.updateRecommendedArea = function() { - var dims = Controller.getViewportDimensions(); - this.reticleMaxX = dims.x - MARGIN; - this.reticleMaxY = dims.y - MARGIN; - }; - - this.calculateNewReticlePosition = function(intersection) { - this.updateRecommendedArea(); - var point2d = HMD.overlayFromWorldPoint(intersection); - point2d.x = Math.max(this.reticleMinX, Math.min(point2d.x, this.reticleMaxX)); - point2d.y = Math.max(this.reticleMinY, Math.min(point2d.y, this.reticleMaxY)); - return point2d; - }; - - this.notPointingAtEntity = function(controllerData) { - var intersection = controllerData.rayPicks[this.hand]; - var entityProperty = Entities.getEntityProperties(intersection.objectID, DISPATCHER_PROPERTIES); - var entityType = entityProperty.type; - var hudRayPick = controllerData.hudRayPicks[this.hand]; - var point2d = this.calculateNewReticlePosition(hudRayPick.intersection); - if ((intersection.type === Picks.INTERSECTED_ENTITY && entityType === "Web") || - intersection.type === Picks.INTERSECTED_OVERLAY || Window.isPointOnDesktopWindow(point2d)) { - return true; - } - return false; - }; - - this.distanceRotate = function(otherFarGrabModule) { - this.distanceRotating = true; - this.distanceHolding = false; - - var worldControllerRotation = getControllerWorldLocation(this.handToController(), true).orientation; - var controllerRotationDelta = - Quat.multiply(worldControllerRotation, Quat.inverse(this.previousWorldControllerRotation)); - // Rotate entity by twice the delta rotation. - controllerRotationDelta = Quat.multiply(controllerRotationDelta, controllerRotationDelta); - - // Perform the rotation in the translation controller's action update. - otherFarGrabModule.currentObjectRotation = Quat.multiply(controllerRotationDelta, - otherFarGrabModule.currentObjectRotation); - - this.previousWorldControllerRotation = worldControllerRotation; - }; - - this.prepareDistanceRotatingData = function(controllerData) { - var intersection = controllerData.rayPicks[this.hand]; - - var controllerLocation = getControllerWorldLocation(this.handToController(), true); - var worldControllerPosition = controllerLocation.position; - var worldControllerRotation = controllerLocation.orientation; - - var grabbedProperties = Entities.getEntityProperties(intersection.objectID, DISPATCHER_PROPERTIES); - this.currentObjectPosition = grabbedProperties.position; - this.grabRadius = intersection.distance; - - // Offset between controller vector at the grab radius and the entity position. - var targetPosition = Vec3.multiply(this.grabRadius, Quat.getUp(worldControllerRotation)); - targetPosition = Vec3.sum(targetPosition, worldControllerPosition); - this.offsetPosition = Vec3.subtract(this.currentObjectPosition, targetPosition); - - // Initial controller rotation. - this.previousWorldControllerRotation = worldControllerRotation; - }; - - this.destroyContextOverlay = function(controllerData) { - if (this.entityWithContextOverlay) { - ContextOverlay.destroyContextOverlay(this.entityWithContextOverlay); - this.entityWithContextOverlay = false; - this.potentialEntityWithContextOverlay = false; - } - }; - - this.checkForUnexpectedChildren = function (controllerData) { - // sometimes things can get parented to a hand and this script is unaware. Search for such entities and - // unhook them. - - var now = Date.now(); - var UNEXPECTED_CHILDREN_CHECK_TIME = 0.1; // seconds - if (now - this.lastUnexpectedChildrenCheckTime > MSECS_PER_SEC * UNEXPECTED_CHILDREN_CHECK_TIME) { - this.lastUnexpectedChildrenCheckTime = now; - - var children = findFarGrabJointChildEntities(this.hand); - var _this = this; - - children.forEach(function(childID) { - // we appear to be holding something and this script isn't in a state that would be holding something. - // unhook it. if we previously took note of this entity's parent, put it back where it was. This - // works around some problems that happen when more than one hand or avatar is passing something around. - if (_this.previousParentID[childID]) { - var previousParentID = _this.previousParentID[childID]; - var previousParentJointIndex = _this.previousParentJointIndex[childID]; - - // The main flaw with keeping track of previous parentage in individual scripts is: - // (1) A grabs something (2) B takes it from A (3) A takes it from B (4) A releases it - // now A and B will take turns passing it back to the other. Detect this and stop the loop here... - var UNHOOK_LOOP_DETECT_MS = 200; - if (_this.previouslyUnhooked[childID]) { - if (now - _this.previouslyUnhooked[childID] < UNHOOK_LOOP_DETECT_MS) { - previousParentID = Uuid.NULL; - previousParentJointIndex = -1; - } - } - _this.previouslyUnhooked[childID] = now; - - Entities.editEntity(childID, { - parentID: previousParentID, - parentJointIndex: previousParentJointIndex - }); - } else { - Entities.editEntity(childID, { parentID: Uuid.NULL }); - } - }); - } - }; - - this.targetIsNull = function() { - var properties = Entities.getEntityProperties(this.targetEntityID, DISPATCHER_PROPERTIES); - if (Object.keys(properties).length === 0 && this.distanceHolding) { - return true; - } - return false; - }; - - this.getTargetProps = function (controllerData) { - var targetEntity = controllerData.rayPicks[this.hand].objectID; - if (targetEntity) { - var gtProps = Entities.getEntityProperties(targetEntity, DISPATCHER_PROPERTIES); - if (entityIsGrabbable(gtProps)) { - // if we've attempted to grab a child, roll up to the root of the tree - var groupRootProps = findGroupParent(controllerData, gtProps); - if (entityIsGrabbable(groupRootProps)) { - return groupRootProps; - } - return gtProps; - } - } - return null; - }; - - this.isReady = function (controllerData) { - if (HMD.active) { - if (this.notPointingAtEntity(controllerData)) { - return makeRunningValues(false, [], []); - } - - this.distanceHolding = false; - this.distanceRotating = false; - - if (controllerData.triggerValues[this.hand] > TRIGGER_ON_VALUE) { - var targetProps = this.getTargetProps(controllerData); - if (targetProps && (targetProps.dynamic && targetProps.parentID === Uuid.NULL)) { - return makeRunningValues(false, [], []); // let farActionGrabEntity handle it - } else { - this.prepareDistanceRotatingData(controllerData); - return makeRunningValues(true, [], []); - } - } else { - this.checkForUnexpectedChildren(controllerData); - this.destroyContextOverlay(); - return makeRunningValues(false, [], []); - } - } - return makeRunningValues(false, [], []); - }; - - this.run = function (controllerData) { - if (controllerData.triggerValues[this.hand] < TRIGGER_OFF_VALUE || this.targetIsNull()) { - this.endFarParentGrab(controllerData); - Selection.removeFromSelectedItemsList(DISPATCHER_HOVERING_LIST, "entity", this.highlightedEntity); - this.highlightedEntity = null; - return makeRunningValues(false, [], []); - } - this.intersectionDistance = controllerData.rayPicks[this.hand].distance; - - var otherModuleName = this.hand === RIGHT_HAND ? "LeftFarParentGrabEntity" : "RightFarParentGrabEntity"; - var otherFarGrabModule = getEnabledModuleByName(otherModuleName); - - // gather up the readiness of the near-grab modules - var nearGrabNames = [ - this.hand === RIGHT_HAND ? "RightScaleAvatar" : "LeftScaleAvatar", - this.hand === RIGHT_HAND ? "RightFarTriggerEntity" : "LeftFarTriggerEntity", - this.hand === RIGHT_HAND ? "RightNearActionGrabEntity" : "LeftNearActionGrabEntity", - this.hand === RIGHT_HAND ? "RightNearParentingGrabEntity" : "LeftNearParentingGrabEntity" - ]; - if (!this.grabbing) { - nearGrabNames.push(this.hand === RIGHT_HAND ? "RightNearParentingGrabOverlay" : "LeftNearParentingGrabOverlay"); - nearGrabNames.push(this.hand === RIGHT_HAND ? "RightNearTabletHighlight" : "LeftNearTabletHighlight"); - } - - var nearGrabReadiness = []; - for (var i = 0; i < nearGrabNames.length; i++) { - var nearGrabModule = getEnabledModuleByName(nearGrabNames[i]); - var ready = nearGrabModule ? nearGrabModule.isReady(controllerData) : makeRunningValues(false, [], []); - nearGrabReadiness.push(ready); - } - - if (this.targetEntityID) { - // if we are doing a distance grab and the object gets close enough to the controller, - // stop the far-grab so the near-grab or equip can take over. - for (var k = 0; k < nearGrabReadiness.length; k++) { - if (nearGrabReadiness[k].active && (nearGrabReadiness[k].targets[0] === this.targetEntityID)) { - this.endFarParentGrab(controllerData); - return makeRunningValues(false, [], []); - } - } - - this.continueDistanceHolding(controllerData); - } else { - // if we are doing a distance search and this controller moves into a position - // where it could near-grab something, stop searching. - for (var j = 0; j < nearGrabReadiness.length; j++) { - if (nearGrabReadiness[j].active) { - this.endFarParentGrab(controllerData); - return makeRunningValues(false, [], []); - } - } - - var rayPickInfo = controllerData.rayPicks[this.hand]; - if (rayPickInfo.type === Picks.INTERSECTED_ENTITY) { - if (controllerData.triggerClicks[this.hand]) { - var entityID = rayPickInfo.objectID; - Selection.removeFromSelectedItemsList(DISPATCHER_HOVERING_LIST, "entity", this.highlightedEntity); - this.highlightedEntity = null; - var targetProps = Entities.getEntityProperties(entityID, DISPATCHER_PROPERTIES); - if (targetProps.href !== "") { - AddressManager.handleLookupString(targetProps.href); - return makeRunningValues(false, [], []); - } - - this.targetObject = new TargetObject(entityID, targetProps); - this.targetObject.parentProps = getEntityParents(targetProps); - - if (this.contextOverlayTimer) { - Script.clearTimeout(this.contextOverlayTimer); - } - this.contextOverlayTimer = false; - if (entityID === this.entityWithContextOverlay) { - this.destroyContextOverlay(); - } else { - Selection.removeFromSelectedItemsList("contextOverlayHighlightList", "entity", entityID); - } - - var targetEntity = this.targetObject.getTargetEntity(); - entityID = targetEntity.id; - targetProps = targetEntity.props; - - if (targetProps.dynamic || this.targetObject.entityProps.dynamic) { - // let farActionGrabEntity handle it - return makeRunningValues(false, [], []); - } - - if (entityIsGrabbable(targetProps) || entityIsGrabbable(this.targetObject.entityProps)) { - - if (!this.distanceRotating) { - this.targetEntityID = entityID; - this.grabbedDistance = rayPickInfo.distance; - } - - if (otherFarGrabModule.targetEntityID === this.targetEntityID && - otherFarGrabModule.distanceHolding) { - this.prepareDistanceRotatingData(controllerData); - this.distanceRotate(otherFarGrabModule); - } else { - this.distanceHolding = true; - this.distanceRotating = false; - this.startFarParentGrab(controllerData, targetProps); - } - } - } else { - var targetEntityID = rayPickInfo.objectID; - if (this.highlightedEntity !== targetEntityID) { - Selection.removeFromSelectedItemsList(DISPATCHER_HOVERING_LIST, "entity", this.highlightedEntity); - var selectionTargetProps = Entities.getEntityProperties(targetEntityID, DISPATCHER_PROPERTIES); - - var selectionTargetObject = new TargetObject(targetEntityID, selectionTargetProps); - selectionTargetObject.parentProps = getEntityParents(selectionTargetProps); - var selectionTargetEntity = selectionTargetObject.getTargetEntity(); - - if (entityIsGrabbable(selectionTargetEntity.props) || - entityIsGrabbable(selectionTargetObject.entityProps)) { - - Selection.addToSelectedItemsList(DISPATCHER_HOVERING_LIST, "entity", rayPickInfo.objectID); - } - this.highlightedEntity = rayPickInfo.objectID; - } - - if (!this.entityWithContextOverlay) { - var _this = this; - - if (_this.potentialEntityWithContextOverlay !== rayPickInfo.objectID) { - if (_this.contextOverlayTimer) { - Script.clearTimeout(_this.contextOverlayTimer); - } - _this.contextOverlayTimer = false; - _this.potentialEntityWithContextOverlay = rayPickInfo.objectID; - } - - if (!_this.contextOverlayTimer) { - _this.contextOverlayTimer = Script.setTimeout(function () { - if (!_this.entityWithContextOverlay && - _this.contextOverlayTimer && - _this.potentialEntityWithContextOverlay === rayPickInfo.objectID) { - var cotProps = Entities.getEntityProperties(rayPickInfo.objectID, - DISPATCHER_PROPERTIES); - var pointerEvent = { - type: "Move", - id: _this.hand + 1, // 0 is reserved for hardware mouse - pos2D: projectOntoEntityXYPlane(rayPickInfo.objectID, - rayPickInfo.intersection, cotProps), - pos3D: rayPickInfo.intersection, - normal: rayPickInfo.surfaceNormal, - direction: Vec3.subtract(ZERO_VEC, rayPickInfo.surfaceNormal), - button: "Secondary" - }; - if (ContextOverlay.createOrDestroyContextOverlay(rayPickInfo.objectID, pointerEvent)) { - _this.entityWithContextOverlay = rayPickInfo.objectID; - } - } - _this.contextOverlayTimer = false; - }, 500); - } - } - } - } else if (this.distanceRotating) { - this.distanceRotate(otherFarGrabModule); - } else if (this.highlightedEntity) { - Selection.removeFromSelectedItemsList(DISPATCHER_HOVERING_LIST, "entity", this.highlightedEntity); - this.highlightedEntity = null; - } - } - return this.exitIfDisabled(controllerData); - }; - - this.exitIfDisabled = function(controllerData) { - var moduleName = this.hand === RIGHT_HAND ? "RightDisableModules" : "LeftDisableModules"; - var disableModule = getEnabledModuleByName(moduleName); - if (disableModule) { - if (disableModule.disableModules) { - this.endFarParentGrab(controllerData); - Selection.removeFromSelectedItemsList(DISPATCHER_HOVERING_LIST, "entity", this.highlightedEntity); - this.highlightedEntity = null; - return makeRunningValues(false, [], []); - } - } - var grabbedThing = (this.distanceHolding || this.distanceRotating) ? this.targetObject.entityID : null; - var offset = this.calculateOffset(controllerData); - var laserLockInfo = makeLaserLockInfo(grabbedThing, false, this.hand, offset); - return makeRunningValues(true, [], [], laserLockInfo); - }; - - this.calculateOffset = function(controllerData) { - if (this.distanceHolding || this.distanceRotating) { - var targetProps = Entities.getEntityProperties(this.targetObject.entityID, - [ "position", "rotation", "registrationPoint", "dimensions" ]); - return worldPositionToRegistrationFrameMatrix(targetProps, controllerData.rayPicks[this.hand].intersection); - } - return undefined; - }; - } - - var leftFarParentGrabEntity = new FarParentGrabEntity(LEFT_HAND); - var rightFarParentGrabEntity = new FarParentGrabEntity(RIGHT_HAND); - - enableDispatcherModule("LeftFarParentGrabEntity", leftFarParentGrabEntity); - enableDispatcherModule("RightFarParentGrabEntity", rightFarParentGrabEntity); - - function cleanup() { - disableDispatcherModule("LeftFarParentGrabEntity"); - disableDispatcherModule("RightFarParentGrabEntity"); - } - Script.scriptEnding.connect(cleanup); -}()); diff --git a/scripts/system/controllers/controllerModules/nearActionGrabEntity.js b/scripts/system/controllers/controllerModules/nearActionGrabEntity.js deleted file mode 100644 index ddff35b9e7..0000000000 --- a/scripts/system/controllers/controllerModules/nearActionGrabEntity.js +++ /dev/null @@ -1,250 +0,0 @@ -"use strict"; - -// nearActionGrabEntity.js -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html - -/* global Script, Entities, MyAvatar, Controller, RIGHT_HAND, LEFT_HAND, - getControllerJointIndex, getGrabbableData, enableDispatcherModule, disableDispatcherModule, - propsArePhysical, Messages, HAPTIC_PULSE_STRENGTH, HAPTIC_PULSE_DURATION, entityIsGrabbable, - MSECS_PER_SEC, makeDispatcherModuleParameters, makeRunningValues, - TRIGGER_OFF_VALUE, NEAR_GRAB_RADIUS, findGroupParent, entityIsCloneable, propsAreCloneDynamic, cloneEntity, - HAPTIC_PULSE_STRENGTH, HAPTIC_PULSE_DURATION, BUMPER_ON_VALUE, unhighlightTargetEntity, Uuid, - DISPATCHER_PROPERTIES, HMD -*/ - -Script.include("/~/system/libraries/controllerDispatcherUtils.js"); -Script.include("/~/system/libraries/controllers.js"); -Script.include("/~/system/libraries/cloneEntityUtils.js"); - -(function() { - - function NearActionGrabEntity(hand) { - this.hand = hand; - this.targetEntityID = null; - this.actionID = null; // action this script created... - - this.parameters = makeDispatcherModuleParameters( - 500, - this.hand === RIGHT_HAND ? ["rightHand"] : ["leftHand"], - [], - 100); - - var NEAR_GRABBING_ACTION_TIMEFRAME = 0.05; // how quickly objects move to their new position - var ACTION_TTL = 15; // seconds - var ACTION_TTL_REFRESH = 5; - - // XXX does handJointIndex change if the avatar changes? - this.handJointIndex = MyAvatar.getJointIndex(this.hand === RIGHT_HAND ? "RightHand" : "LeftHand"); - this.controllerJointIndex = getControllerJointIndex(this.hand); - - - // handPosition is where the avatar's hand appears to be, in-world. - this.getHandPosition = function () { - if (this.hand === RIGHT_HAND) { - return MyAvatar.getRightPalmPosition(); - } else { - return MyAvatar.getLeftPalmPosition(); - } - }; - - this.getHandRotation = function () { - if (this.hand === RIGHT_HAND) { - return MyAvatar.getRightPalmRotation(); - } else { - return MyAvatar.getLeftPalmRotation(); - } - }; - - - this.startNearGrabAction = function (controllerData, targetProps) { - Controller.triggerHapticPulse(HAPTIC_PULSE_STRENGTH, HAPTIC_PULSE_DURATION, this.hand); - - var grabbableData = getGrabbableData(targetProps); - this.grabFollowsController = grabbableData.grabFollowsController; - this.kinematicGrab = grabbableData.grabKinematic; - - var handJointIndex; - if (HMD.mounted && HMD.isHandControllerAvailable() && grabbableData.grabFollowsController) { - handJointIndex = getControllerJointIndex(this.hand); - } else { - handJointIndex = MyAvatar.getJointIndex(this.hand === RIGHT_HAND ? "RightHand" : "LeftHand"); - } - this.offsetPosition = Entities.worldToLocalPosition(targetProps.position, MyAvatar.SELF_ID, handJointIndex); - this.offsetRotation = Entities.worldToLocalRotation(targetProps.rotation, MyAvatar.SELF_ID, handJointIndex); - - var now = Date.now(); - this.actionTimeout = now + (ACTION_TTL * MSECS_PER_SEC); - - if (this.actionID) { - Entities.deleteAction(this.targetEntityID, this.actionID); - } - this.actionID = Entities.addAction("hold", this.targetEntityID, { - hand: this.hand === RIGHT_HAND ? "right" : "left", - timeScale: NEAR_GRABBING_ACTION_TIMEFRAME, - relativePosition: this.offsetPosition, - relativeRotation: this.offsetRotation, - ttl: ACTION_TTL, - kinematic: this.kinematicGrab, - kinematicSetVelocity: true, - ignoreIK: this.grabFollowsController - }); - if (this.actionID === Uuid.NULL) { - this.actionID = null; - return; - } - - Messages.sendMessage('Hifi-Object-Manipulation', JSON.stringify({ - action: 'grab', - grabbedEntity: this.targetEntityID, - joint: this.hand === RIGHT_HAND ? "RightHand" : "LeftHand" - })); - - var args = [this.hand === RIGHT_HAND ? "right" : "left", MyAvatar.sessionUUID]; - Entities.callEntityMethod(this.targetEntityID, "startNearGrab", args); - }; - - // this is for when the action is going to time-out - this.refreshNearGrabAction = function (controllerData) { - var now = Date.now(); - if (this.actionID && this.actionTimeout - now < ACTION_TTL_REFRESH * MSECS_PER_SEC) { - // if less than a 5 seconds left, refresh the actions ttl - var success = Entities.updateAction(this.targetEntityID, this.actionID, { - hand: this.hand === RIGHT_HAND ? "right" : "left", - timeScale: NEAR_GRABBING_ACTION_TIMEFRAME, - relativePosition: this.offsetPosition, - relativeRotation: this.offsetRotation, - ttl: ACTION_TTL, - kinematic: this.kinematicGrab, - kinematicSetVelocity: true, - ignoreIK: this.grabFollowsController - }); - if (success) { - this.actionTimeout = now + (ACTION_TTL * MSECS_PER_SEC); - } - } - }; - - this.endNearGrabAction = function () { - var args = [this.hand === RIGHT_HAND ? "right" : "left", MyAvatar.sessionUUID]; - Entities.callEntityMethod(this.targetEntityID, "releaseGrab", args); - - Entities.deleteAction(this.targetEntityID, this.actionID); - this.actionID = null; - - Messages.sendMessage('Hifi-Object-Manipulation', JSON.stringify({ - action: 'release', - grabbedEntity: this.targetEntityID, - joint: this.hand === RIGHT_HAND ? "RightHand" : "LeftHand" - })); - - this.targetEntityID = null; - }; - - this.getTargetProps = function (controllerData) { - // nearbyEntityProperties is already sorted by distance from controller - var nearbyEntityProperties = controllerData.nearbyEntityProperties[this.hand]; - var sensorScaleFactor = MyAvatar.sensorToWorldScale; - for (var i = 0; i < nearbyEntityProperties.length; i++) { - var props = nearbyEntityProperties[i]; - if (props.distance > NEAR_GRAB_RADIUS * sensorScaleFactor) { - break; - } - if (entityIsGrabbable(props) || entityIsCloneable(props)) { - if (!entityIsCloneable(props)) { - // if we've attempted to grab a non-cloneable child, roll up to the root of the tree - var groupRootProps = findGroupParent(controllerData, props); - if (entityIsGrabbable(groupRootProps)) { - return groupRootProps; - } - } - return props; - } - } - return null; - }; - - this.isReady = function (controllerData) { - this.targetEntityID = null; - - var targetProps = this.getTargetProps(controllerData); - if (controllerData.triggerValues[this.hand] < TRIGGER_OFF_VALUE && - controllerData.secondaryValues[this.hand] < TRIGGER_OFF_VALUE) { - return makeRunningValues(false, [], []); - } - - if (targetProps) { - if ((!propsArePhysical(targetProps) && !propsAreCloneDynamic(targetProps)) || - targetProps.parentID !== Uuid.NULL) { - return makeRunningValues(false, [], []); // let nearParentGrabEntity handle it - } else { - this.targetEntityID = targetProps.id; - return makeRunningValues(true, [this.targetEntityID], []); - } - } else { - return makeRunningValues(false, [], []); - } - }; - - this.run = function (controllerData) { - if (this.actionID) { - if (controllerData.triggerClicks[this.hand] < TRIGGER_OFF_VALUE && - controllerData.secondaryValues[this.hand] < TRIGGER_OFF_VALUE) { - this.endNearGrabAction(); - return makeRunningValues(false, [], []); - } - - this.refreshNearGrabAction(controllerData); - var args = [this.hand === RIGHT_HAND ? "right" : "left", MyAvatar.sessionUUID]; - Entities.callEntityMethod(this.targetEntityID, "continueNearGrab", args); - } else { - - // still searching / highlighting - var readiness = this.isReady (controllerData); - if (!readiness.active) { - return readiness; - } - - var targetProps = this.getTargetProps(controllerData); - if (targetProps) { - if (controllerData.triggerClicks[this.hand] || - controllerData.secondaryValues[this.hand] > BUMPER_ON_VALUE) { - // switch to grabbing - var targetCloneable = entityIsCloneable(targetProps); - if (targetCloneable) { - var cloneID = cloneEntity(targetProps); - var cloneProps = Entities.getEntityProperties(cloneID, DISPATCHER_PROPERTIES); - this.targetEntityID = cloneID; - this.startNearGrabAction(controllerData, cloneProps); - } else { - this.startNearGrabAction(controllerData, targetProps); - } - } - } - } - - return makeRunningValues(true, [this.targetEntityID], []); - }; - - this.cleanup = function () { - if (this.targetEntityID) { - this.endNearGrabAction(); - } - }; - } - - var leftNearActionGrabEntity = new NearActionGrabEntity(LEFT_HAND); - var rightNearActionGrabEntity = new NearActionGrabEntity(RIGHT_HAND); - - enableDispatcherModule("LeftNearActionGrabEntity", leftNearActionGrabEntity); - enableDispatcherModule("RightNearActionGrabEntity", rightNearActionGrabEntity); - - function cleanup() { - leftNearActionGrabEntity.cleanup(); - rightNearActionGrabEntity.cleanup(); - disableDispatcherModule("LeftNearActionGrabEntity"); - disableDispatcherModule("RightNearActionGrabEntity"); - } - Script.scriptEnding.connect(cleanup); -}()); diff --git a/scripts/system/controllers/controllerModules/nearGrabEntity.js b/scripts/system/controllers/controllerModules/nearGrabEntity.js index 60a5781ca4..0f8071677c 100644 --- a/scripts/system/controllers/controllerModules/nearGrabEntity.js +++ b/scripts/system/controllers/controllerModules/nearGrabEntity.js @@ -8,9 +8,10 @@ /* global Script, Entities, MyAvatar, Controller, RIGHT_HAND, LEFT_HAND, getControllerJointIndex, enableDispatcherModule, disableDispatcherModule, Messages, HAPTIC_PULSE_STRENGTH, HAPTIC_PULSE_DURATION, TRIGGER_OFF_VALUE, - makeDispatcherModuleParameters, entityIsGrabbable, makeRunningValues, NEAR_GRAB_RADIUS, findGroupParent, Vec3, cloneEntity, - entityIsCloneable, HAPTIC_PULSE_STRENGTH, HAPTIC_PULSE_DURATION, BUMPER_ON_VALUE, distanceBetweenPointAndEntityBoundingBox, - getGrabbableData, getEnabledModuleByName, DISPATCHER_PROPERTIES, HMD, NEAR_GRAB_DISTANCE + makeDispatcherModuleParameters, entityIsGrabbable, makeRunningValues, NEAR_GRAB_RADIUS, findGrabbableGroupParent, Vec3, + cloneEntity, entityIsCloneable, HAPTIC_PULSE_STRENGTH, HAPTIC_PULSE_DURATION, BUMPER_ON_VALUE, + distanceBetweenPointAndEntityBoundingBox, getGrabbableData, getEnabledModuleByName, DISPATCHER_PROPERTIES, HMD, + NEAR_GRAB_DISTANCE */ Script.include("/~/system/libraries/controllerDispatcherUtils.js"); @@ -80,9 +81,6 @@ Script.include("/~/system/libraries/controllers.js"); this.endNearGrabEntity = function () { this.endGrab(); - this.grabbing = false; - this.targetEntityID = null; - var args = [this.hand === RIGHT_HAND ? "right" : "left", MyAvatar.sessionUUID]; Entities.callEntityMethod(this.targetEntityID, "releaseGrab", args); Messages.sendMessage('Hifi-Object-Manipulation', JSON.stringify({ @@ -90,6 +88,9 @@ Script.include("/~/system/libraries/controllers.js"); grabbedEntity: this.targetEntityID, joint: this.hand === RIGHT_HAND ? "RightHand" : "LeftHand" })); + + this.grabbing = false; + this.targetEntityID = null; }; this.getTargetProps = function (controllerData) { @@ -110,7 +111,7 @@ Script.include("/~/system/libraries/controllers.js"); if (entityIsGrabbable(props) || entityIsCloneable(props)) { if (!entityIsCloneable(props)) { // if we've attempted to grab a non-cloneable child, roll up to the root of the tree - var groupRootProps = findGroupParent(controllerData, props); + var groupRootProps = findGrabbableGroupParent(controllerData, props); if (entityIsGrabbable(groupRootProps)) { return groupRootProps; } diff --git a/scripts/system/controllers/controllerModules/nearParentGrabEntity.js b/scripts/system/controllers/controllerModules/nearParentGrabEntity.js deleted file mode 100644 index 13557bdb7e..0000000000 --- a/scripts/system/controllers/controllerModules/nearParentGrabEntity.js +++ /dev/null @@ -1,359 +0,0 @@ -"use strict"; - -// nearParentGrabEntity.js -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html - - -/* global Script, Entities, MyAvatar, Controller, RIGHT_HAND, LEFT_HAND, getControllerJointIndex, - enableDispatcherModule, disableDispatcherModule, propsArePhysical, Messages, HAPTIC_PULSE_STRENGTH, HAPTIC_PULSE_DURATION, - TRIGGER_OFF_VALUE, makeDispatcherModuleParameters, entityIsGrabbable, makeRunningValues, NEAR_GRAB_RADIUS, - findGroupParent, Vec3, cloneEntity, entityIsCloneable, propsAreCloneDynamic, HAPTIC_PULSE_STRENGTH, - HAPTIC_PULSE_DURATION, BUMPER_ON_VALUE, findHandChildEntities, TEAR_AWAY_DISTANCE, MSECS_PER_SEC, TEAR_AWAY_CHECK_TIME, - TEAR_AWAY_COUNT, distanceBetweenPointAndEntityBoundingBox, print, Uuid, NEAR_GRAB_DISTANCE, - distanceBetweenEntityLocalPositionAndBoundingBox, getGrabbableData, getGrabPointSphereOffset, DISPATCHER_PROPERTIES -*/ - -Script.include("/~/system/libraries/controllerDispatcherUtils.js"); -Script.include("/~/system/libraries/cloneEntityUtils.js"); -Script.include("/~/system/libraries/controllers.js"); - -(function() { - - // XXX this.ignoreIK = (grabbableData.ignoreIK !== undefined) ? grabbableData.ignoreIK : true; - // XXX this.kinematicGrab = (grabbableData.kinematic !== undefined) ? grabbableData.kinematic : NEAR_GRABBING_KINEMATIC; - - function NearParentingGrabEntity(hand) { - this.hand = hand; - this.targetEntityID = null; - this.grabbing = false; - this.previousParentID = {}; - this.previousParentJointIndex = {}; - this.previouslyUnhooked = {}; - this.lastUnequipCheckTime = 0; - this.autoUnequipCounter = 0; - this.lastUnexpectedChildrenCheckTime = 0; - this.robbed = false; - this.cloneAllowed = true; - - this.parameters = makeDispatcherModuleParameters( - 500, - this.hand === RIGHT_HAND ? ["rightHand"] : ["leftHand"], - [], - 100); - - this.thisHandIsParent = function(props) { - if (!props) { - return false; - } - - if (props.parentID !== MyAvatar.sessionUUID && props.parentID !== MyAvatar.SELF_ID) { - return false; - } - - var handJointIndex = MyAvatar.getJointIndex(this.hand === RIGHT_HAND ? "RightHand" : "LeftHand"); - if (props.parentJointIndex === handJointIndex) { - return true; - } - - if (props.parentJointIndex === getControllerJointIndex(this.hand)) { - return true; - } - - var controllerCRJointIndex = MyAvatar.getJointIndex(this.hand === RIGHT_HAND ? - "_CAMERA_RELATIVE_CONTROLLER_RIGHTHAND" : - "_CAMERA_RELATIVE_CONTROLLER_LEFTHAND"); - - if (props.parentJointIndex === controllerCRJointIndex) { - return true; - } - - return false; - }; - - this.getOtherModule = function() { - return this.hand === RIGHT_HAND ? leftNearParentingGrabEntity : rightNearParentingGrabEntity; - }; - - this.otherHandIsParent = function(props) { - var otherModule = this.getOtherModule(); - return (otherModule.thisHandIsParent(props) && otherModule.grabbing); - }; - - this.startNearParentingGrabEntity = function (controllerData, targetProps) { - var grabData = getGrabbableData(targetProps); - Controller.triggerHapticPulse(HAPTIC_PULSE_STRENGTH, HAPTIC_PULSE_DURATION, this.hand); - - var handJointIndex; - if (grabData.grabFollowsController) { - handJointIndex = getControllerJointIndex(this.hand); - } else { - handJointIndex = MyAvatar.getJointIndex(this.hand === RIGHT_HAND ? "RightHand" : "LeftHand"); - } - - var args = [this.hand === RIGHT_HAND ? "right" : "left", MyAvatar.sessionUUID]; - Entities.callEntityMethod(targetProps.id, "startNearGrab", args); - - var reparentProps = { - parentID: MyAvatar.SELF_ID, - parentJointIndex: handJointIndex, - localVelocity: {x: 0, y: 0, z: 0}, - localAngularVelocity: {x: 0, y: 0, z: 0} - }; - - if (this.thisHandIsParent(targetProps)) { - // this should never happen, but if it does, don't set previous parent to be this hand. - this.previousParentID[targetProps.id] = null; - this.previousParentJointIndex[targetProps.id] = -1; - } else if (this.otherHandIsParent(targetProps)) { - var otherModule = this.getOtherModule(); - this.previousParentID[this.grabbedThingID] = otherModule.previousParentID[this.grabbedThingID]; - this.previousParentJointIndex[this.grabbedThingID] = otherModule.previousParentJointIndex[this.grabbedThingID]; - otherModule.robbed = true; - } else { - this.previousParentID[targetProps.id] = targetProps.parentID; - this.previousParentJointIndex[targetProps.id] = targetProps.parentJointIndex; - } - - this.targetEntityID = targetProps.id; - Entities.editEntity(targetProps.id, reparentProps); - - Messages.sendMessage('Hifi-Object-Manipulation', JSON.stringify({ - action: 'grab', - grabbedEntity: targetProps.id, - joint: this.hand === RIGHT_HAND ? "RightHand" : "LeftHand" - })); - this.grabbing = true; - }; - - this.endNearParentingGrabEntity = function (controllerData) { - var props = controllerData.nearbyEntityPropertiesByID[this.targetEntityID]; - if (this.thisHandIsParent(props) && !this.robbed) { - Entities.editEntity(this.targetEntityID, { - parentID: this.previousParentID[this.targetEntityID], - parentJointIndex: this.previousParentJointIndex[this.targetEntityID], - localVelocity: {x: 0, y: 0, z: 0}, - localAngularVelocity: {x: 0, y: 0, z: 0} - }); - } - - var args = [this.hand === RIGHT_HAND ? "right" : "left", MyAvatar.sessionUUID]; - Entities.callEntityMethod(this.targetEntityID, "releaseGrab", args); - Messages.sendMessage('Hifi-Object-Manipulation', JSON.stringify({ - action: 'release', - grabbedEntity: this.targetEntityID, - joint: this.hand === RIGHT_HAND ? "RightHand" : "LeftHand" - })); - - this.grabbing = false; - this.targetEntityID = null; - this.robbed = false; - }; - - this.checkForChildTooFarAway = function (controllerData) { - var props = controllerData.nearbyEntityPropertiesByID[this.targetEntityID]; - var now = Date.now(); - if (now - this.lastUnequipCheckTime > MSECS_PER_SEC * TEAR_AWAY_CHECK_TIME) { - this.lastUnequipCheckTime = now; - if (props.parentID === MyAvatar.SELF_ID) { - var tearAwayDistance = TEAR_AWAY_DISTANCE * MyAvatar.sensorToWorldScale; - var controllerIndex = - this.hand === LEFT_HAND ? Controller.Standard.LeftHand : Controller.Standard.RightHand; - var controllerGrabOffset = getGrabPointSphereOffset(controllerIndex, true); - controllerGrabOffset = Vec3.multiply(-MyAvatar.sensorToWorldScale, controllerGrabOffset); - var distance = distanceBetweenEntityLocalPositionAndBoundingBox(props, controllerGrabOffset); - if (distance > tearAwayDistance) { - this.autoUnequipCounter++; - } else { - this.autoUnequipCounter = 0; - } - if (this.autoUnequipCounter >= TEAR_AWAY_COUNT) { - return true; - } - } - } - return false; - }; - - - this.checkForUnexpectedChildren = function (controllerData) { - // sometimes things can get parented to a hand and this script is unaware. Search for such entities and - // unhook them. - - var now = Date.now(); - var UNEXPECTED_CHILDREN_CHECK_TIME = 0.1; // seconds - if (now - this.lastUnexpectedChildrenCheckTime > MSECS_PER_SEC * UNEXPECTED_CHILDREN_CHECK_TIME) { - this.lastUnexpectedChildrenCheckTime = now; - - var children = findHandChildEntities(this.hand); - var _this = this; - - children.forEach(function(childID) { - // we appear to be holding something and this script isn't in a state that would be holding something. - // unhook it. if we previously took note of this entity's parent, put it back where it was. This - // works around some problems that happen when more than one hand or avatar is passing something around. - if (_this.previousParentID[childID]) { - var previousParentID = _this.previousParentID[childID]; - var previousParentJointIndex = _this.previousParentJointIndex[childID]; - - // The main flaw with keeping track of previous parentage in individual scripts is: - // (1) A grabs something (2) B takes it from A (3) A takes it from B (4) A releases it - // now A and B will take turns passing it back to the other. Detect this and stop the loop here... - var UNHOOK_LOOP_DETECT_MS = 200; - if (_this.previouslyUnhooked[childID]) { - if (now - _this.previouslyUnhooked[childID] < UNHOOK_LOOP_DETECT_MS) { - previousParentID = Uuid.NULL; - previousParentJointIndex = -1; - } - } - _this.previouslyUnhooked[childID] = now; - - Entities.editEntity(childID, { - parentID: previousParentID, - parentJointIndex: previousParentJointIndex - }); - } else { - Entities.editEntity(childID, { parentID: Uuid.NULL }); - } - }); - } - }; - - this.getTargetProps = function (controllerData) { - // nearbyEntityProperties is already sorted by length from controller - var nearbyEntityProperties = controllerData.nearbyEntityProperties[this.hand]; - var sensorScaleFactor = MyAvatar.sensorToWorldScale; - var nearGrabDistance = NEAR_GRAB_DISTANCE * sensorScaleFactor; - var nearGrabRadius = NEAR_GRAB_RADIUS * sensorScaleFactor; - for (var i = 0; i < nearbyEntityProperties.length; i++) { - var props = nearbyEntityProperties[i]; - var grabPosition = controllerData.controllerLocations[this.hand].position; // Is offset from hand position. - var dist = distanceBetweenPointAndEntityBoundingBox(grabPosition, props); - var distance = Vec3.distance(grabPosition, props.position); - if ((dist > nearGrabDistance) || - (distance > nearGrabRadius)) { // Only smallish entities can be near grabbed. - continue; - } - if (entityIsGrabbable(props) || entityIsCloneable(props)) { - if (!entityIsCloneable(props)) { - // if we've attempted to grab a non-cloneable child, roll up to the root of the tree - var groupRootProps = findGroupParent(controllerData, props); - if (entityIsGrabbable(groupRootProps)) { - return groupRootProps; - } - } - return props; - } - } - return null; - }; - - this.isReady = function (controllerData, deltaTime) { - this.targetEntityID = null; - this.grabbing = false; - - var targetProps = this.getTargetProps(controllerData); - if (controllerData.triggerValues[this.hand] < TRIGGER_OFF_VALUE && - controllerData.secondaryValues[this.hand] < TRIGGER_OFF_VALUE) { - this.checkForUnexpectedChildren(controllerData); - this.robbed = false; - this.cloneAllowed = true; - return makeRunningValues(false, [], []); - } - - if (targetProps) { - if ((propsArePhysical(targetProps) || propsAreCloneDynamic(targetProps)) && - targetProps.parentID === Uuid.NULL) { - this.robbed = false; - return makeRunningValues(false, [], []); // let nearActionGrabEntity handle it - } else { - this.targetEntityID = targetProps.id; - return makeRunningValues(true, [this.targetEntityID], []); - } - } else { - this.robbed = false; - return makeRunningValues(false, [], []); - } - }; - - this.run = function (controllerData, deltaTime) { - if (this.grabbing) { - if (controllerData.triggerClicks[this.hand] < TRIGGER_OFF_VALUE && - controllerData.secondaryValues[this.hand] < TRIGGER_OFF_VALUE) { - this.endNearParentingGrabEntity(controllerData); - return makeRunningValues(false, [], []); - } - - var props = controllerData.nearbyEntityPropertiesByID[this.targetEntityID]; - if (!props) { - // entity was deleted - this.grabbing = false; - this.targetEntityID = null; - this.robbed = false; - return makeRunningValues(false, [], []); - } - - if (this.checkForChildTooFarAway(controllerData)) { - // if the held entity moves too far from the hand, release it - print("nearParentGrabEntity -- autoreleasing held item because it is far from hand"); - this.endNearParentingGrabEntity(controllerData); - return makeRunningValues(false, [], []); - } - - var args = [this.hand === RIGHT_HAND ? "right" : "left", MyAvatar.sessionUUID]; - Entities.callEntityMethod(this.targetEntityID, "continueNearGrab", args); - } else { - // still searching - var readiness = this.isReady(controllerData); - if (!readiness.active) { - this.robbed = false; - return readiness; - } - if (controllerData.triggerClicks[this.hand] || controllerData.secondaryValues[this.hand] > BUMPER_ON_VALUE) { - // switch to grab - var targetProps = this.getTargetProps(controllerData); - var targetCloneable = entityIsCloneable(targetProps); - - if (targetCloneable) { - if (this.cloneAllowed) { - var cloneID = cloneEntity(targetProps); - if (cloneID !== null) { - var cloneProps = Entities.getEntityProperties(cloneID, DISPATCHER_PROPERTIES); - this.grabbing = true; - this.targetEntityID = cloneID; - this.startNearParentingGrabEntity(controllerData, cloneProps); - this.cloneAllowed = false; // prevent another clone call until inputs released - } - } - } else if (targetProps) { - this.grabbing = true; - this.startNearParentingGrabEntity(controllerData, targetProps); - } - } - } - - return makeRunningValues(true, [this.targetEntityID], []); - }; - - this.cleanup = function () { - if (this.targetEntityID) { - this.endNearParentingGrabEntity(); - } - }; - } - - var leftNearParentingGrabEntity = new NearParentingGrabEntity(LEFT_HAND); - var rightNearParentingGrabEntity = new NearParentingGrabEntity(RIGHT_HAND); - - enableDispatcherModule("LeftNearParentingGrabEntity", leftNearParentingGrabEntity); - enableDispatcherModule("RightNearParentingGrabEntity", rightNearParentingGrabEntity); - - function cleanup() { - leftNearParentingGrabEntity.cleanup(); - rightNearParentingGrabEntity.cleanup(); - disableDispatcherModule("LeftNearParentingGrabEntity"); - disableDispatcherModule("RightNearParentingGrabEntity"); - } - Script.scriptEnding.connect(cleanup); -}()); diff --git a/scripts/system/controllers/controllerScripts.js b/scripts/system/controllers/controllerScripts.js index 2114f2c0b2..86ff7701c3 100644 --- a/scripts/system/controllers/controllerScripts.js +++ b/scripts/system/controllers/controllerScripts.js @@ -32,22 +32,13 @@ var CONTOLLER_SCRIPTS = [ "controllerModules/mouseHMD.js", "controllerModules/scaleEntity.js", "controllerModules/nearGrabHyperLinkEntity.js", - "controllerModules/nearTabletHighlight.js" + "controllerModules/nearTabletHighlight.js", + "controllerModules/nearGrabEntity.js", + "controllerModules/farGrabEntity.js" ]; -if (Settings.getValue("useTraitsGrab", true)) { - CONTOLLER_SCRIPTS.push("controllerModules/nearGrabEntity.js"); - CONTOLLER_SCRIPTS.push("controllerModules/farGrabEntity.js"); -} else { - CONTOLLER_SCRIPTS.push("controllerModules/nearParentGrabEntity.js"); - CONTOLLER_SCRIPTS.push("controllerModules/nearActionGrabEntity.js"); - CONTOLLER_SCRIPTS.push("controllerModules/farActionGrabEntityDynOnly.js"); - CONTOLLER_SCRIPTS.push("controllerModules/farParentGrabEntity.js"); -} - var DEBUG_MENU_ITEM = "Debug defaultScripts.js"; - function runDefaultsTogether() { for (var j in CONTOLLER_SCRIPTS) { if (CONTOLLER_SCRIPTS.hasOwnProperty(j)) { diff --git a/scripts/system/controllers/grab.js b/scripts/system/controllers/grab.js index a78a2971e9..1fb82d3843 100644 --- a/scripts/system/controllers/grab.js +++ b/scripts/system/controllers/grab.js @@ -14,79 +14,25 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -/* global MyAvatar, Entities, Script, HMD, Camera, Vec3, Reticle, Overlays, getEntityCustomData, Messages, Quat, Controller, +/* global MyAvatar, Entities, Script, HMD, Camera, Vec3, Reticle, Overlays, Messages, Quat, Controller, isInEditMode, entityIsGrabbable, Picks, PickType, Pointers, unhighlightTargetEntity, DISPATCHER_PROPERTIES, - entityIsGrabbable, entityIsEquipped, getMainTabletIDs + entityIsGrabbable, getMainTabletIDs */ /* jslint bitwise: true */ (function() { // BEGIN LOCAL_SCOPE - Script.include("/~/system/libraries/utils.js"); - Script.include("/~/system/libraries/controllerDispatcherUtils.js"); +Script.include("/~/system/libraries/utils.js"); +Script.include("/~/system/libraries/controllerDispatcherUtils.js"); + +var MOUSE_GRAB_JOINT = 65526; // FARGRAB_MOUSE_INDEX + var MAX_SOLID_ANGLE = 0.01; // objects that appear smaller than this can't be grabbed var DELAY_FOR_30HZ = 33; // milliseconds -var ZERO_VEC3 = { - x: 0, - y: 0, - z: 0 -}; -var IDENTITY_QUAT = { - x: 0, - y: 0, - z: 0, - w: 0 -}; - -var DEFAULT_GRABBABLE_DATA = { - grabbable: true, - invertSolidWhileHeld: false -}; - - -var ACTION_TTL = 10; // seconds - -function getTag() { - return "grab-" + MyAvatar.sessionUUID; -} - -var DISTANCE_HOLDING_ACTION_TIMEFRAME = 0.1; // how quickly objects move to their new position -var DISTANCE_HOLDING_UNITY_MASS = 1200; // The mass at which the distance holding action timeframe is unmodified -var DISTANCE_HOLDING_UNITY_DISTANCE = 6; // The distance at which the distance holding action timeframe is unmodified - -function distanceGrabTimescale(mass, distance) { - var timeScale = DISTANCE_HOLDING_ACTION_TIMEFRAME * mass / - DISTANCE_HOLDING_UNITY_MASS * distance / - DISTANCE_HOLDING_UNITY_DISTANCE; - if (timeScale < DISTANCE_HOLDING_ACTION_TIMEFRAME) { - timeScale = DISTANCE_HOLDING_ACTION_TIMEFRAME; - } - return timeScale; -} -function getMass(dimensions, density) { - return (dimensions.x * dimensions.y * dimensions.z) * density; -} - -function entityIsGrabbedByOther(entityID) { - // by convention, a distance grab sets the tag of its action to be grab-*owner-session-id*. - var actionIDs = Entities.getActionIDs(entityID); - for (var actionIndex = 0; actionIndex < actionIDs.length; actionIndex++) { - var actionID = actionIDs[actionIndex]; - var actionArguments = Entities.getActionArguments(entityID, actionID); - var tag = actionArguments.tag; - if (tag == getTag()) { - // we see a grab-*uuid* shaped tag, but it's our tag, so that's okay. - continue; - } - if (tag.slice(0, 5) == "grab-") { - // we see a grab-*uuid* shaped tag and it's not ours, so someone else is grabbing it. - return true; - } - } - return false; -} +var ZERO_VEC3 = { x: 0, y: 0, z: 0 }; +var IDENTITY_QUAT = { x: 0, y: 0, z: 0, w: 1 }; // helper function function mouseIntersectionWithPlane(pointOnPlane, planeNormal, event, maxDistance) { @@ -227,7 +173,6 @@ var beacon = { function Grabber() { this.isGrabbing = false; this.entityID = null; - this.actionID = null; this.startPosition = ZERO_VEC3; this.lastRotation = IDENTITY_QUAT; this.currentPosition = ZERO_VEC3; @@ -253,9 +198,6 @@ function Grabber() { z: 0 }; - this.targetPosition = null; - this.targetRotation = null; - this.liftKey = false; // SHIFT this.rotateKey = false; // CONTROL @@ -305,7 +247,7 @@ Grabber.prototype.computeNewGrabPlane = function() { } } - this.pointOnPlane = Vec3.sum(this.currentPosition, this.offset); + this.pointOnPlane = Vec3.subtract(this.currentPosition, this.offset); var xzOffset = Vec3.subtract(this.pointOnPlane, Camera.getPosition()); xzOffset.y = 0; this.xzDistanceToGrab = Vec3.length(xzOffset); @@ -315,15 +257,12 @@ Grabber.prototype.pressEvent = function(event) { if (isInEditMode() || HMD.active) { return; } - if (event.button !== "LEFT") { return; } - if (event.isAlt || event.isMeta) { return; } - if (Overlays.getOverlayAtPoint(Reticle.position) > 0) { // the mouse is pointing at an overlay; don't look for entities underneath the overlay. return; @@ -341,13 +280,12 @@ Grabber.prototype.pressEvent = function(event) { } var props = Entities.getEntityProperties(pickResults.objectID, DISPATCHER_PROPERTIES); - var isDynamic = props.dynamic; if (!entityIsGrabbable(props)) { // only grab grabbable objects return; } - - if (!props.grab.grabbable) { + if (props.grab.equippable) { + // don't mouse-grab click-to-equip entities (let equipEntity.js handle these) return; } @@ -361,7 +299,6 @@ Grabber.prototype.pressEvent = function(event) { var entityProperties = Entities.getEntityProperties(clickedEntity, DISPATCHER_PROPERTIES); this.startPosition = entityProperties.position; this.lastRotation = entityProperties.rotation; - this.madeDynamic = false; var cameraPosition = Camera.getPosition(); var objectBoundingDiameter = Vec3.length(entityProperties.dimensions); @@ -373,21 +310,10 @@ Grabber.prototype.pressEvent = function(event) { return; } - if (entityIsGrabbable(props) && !isDynamic) { - entityProperties.dynamic = true; - Entities.editEntity(clickedEntity, entityProperties); - this.madeDynamic = true; - } - // this.activateEntity(clickedEntity, entityProperties); this.isGrabbing = true; this.entityID = clickedEntity; this.currentPosition = entityProperties.position; - this.targetPosition = { - x: this.startPosition.x, - y: this.startPosition.y, - z: this.startPosition.z - }; // compute the grab point var pickRay = Camera.computePickRay(event.x, event.y); @@ -396,14 +322,13 @@ Grabber.prototype.pressEvent = function(event) { nearestPoint = Vec3.multiply(distanceToGrab, pickRay.direction); this.pointOnPlane = Vec3.sum(cameraPosition, nearestPoint); - // compute the grab offset (points from object center to point of grab) - this.offset = Vec3.subtract(this.pointOnPlane, this.startPosition); + // compute the grab offset (points from point of grab to object center) + this.offset = Vec3.subtract(this.startPosition, this.pointOnPlane); // offset in world-space + MyAvatar.setJointTranslation(MOUSE_GRAB_JOINT, MyAvatar.worldToJointPoint(this.startPosition)); + MyAvatar.setJointRotation(MOUSE_GRAB_JOINT, MyAvatar.worldToJointRotation(this.lastRotation)); this.computeNewGrabPlane(); - - if (!entityIsGrabbedByOther(this.entityID)) { - this.moveEvent(event); - } + this.moveEvent(event); var args = "mouse"; Entities.callEntityMethod(this.entityID, "startDistanceGrab", args); @@ -413,6 +338,12 @@ Grabber.prototype.pressEvent = function(event) { grabbedEntity: this.entityID })); + if (this.grabID) { + MyAvatar.releaseGrab(this.grabID); + this.grabID = null; + } + this.grabID = MyAvatar.grab(this.entityID, MOUSE_GRAB_JOINT, ZERO_VEC3, IDENTITY_QUAT); + // TODO: play sounds again when we aren't leaking AudioInjector threads //Audio.playSound(grabSound, { position: entityProperties.position, volume: VOLUME }); }; @@ -428,20 +359,7 @@ Grabber.prototype.releaseEvent = function(event) { } if (this.isGrabbing) { - // this.deactivateEntity(this.entityID); this.isGrabbing = false; - if (this.actionID) { - Entities.deleteAction(this.entityID, this.actionID); - } - - if (this.madeDynamic) { - var entityProps = {}; - entityProps.dynamic = false; - entityProps.localVelocity = {x: 0, y: 0, z: 0}; - Entities.editEntity(this.entityID, entityProps); - } - - this.actionID = null; Pointers.setRenderState(this.mouseRayEntities, ""); Pointers.setLockEndUUID(this.mouseRayEntities, null, false); @@ -455,6 +373,13 @@ Grabber.prototype.releaseEvent = function(event) { joint: "mouse" })); + if (this.grabID) { + MyAvatar.releaseGrab(this.grabID); + this.grabID = null; + } + + MyAvatar.clearJointData(MOUSE_GRAB_JOINT); + // TODO: play sounds again when we aren't leaking AudioInjector threads //Audio.playSound(releaseSound, { position: entityProperties.position, volume: VOLUME }); } @@ -482,23 +407,12 @@ Grabber.prototype.moveEvent = function(event) { Grabber.prototype.moveEventProcess = function() { this.moveEventTimer = null; - // see if something added/restored gravity var entityProperties = Entities.getEntityProperties(this.entityID, DISPATCHER_PROPERTIES); - if (!entityProperties || !entityProperties.gravity || HMD.active) { + if (!entityProperties || HMD.active) { return; } - if (Vec3.length(entityProperties.gravity) !== 0.0) { - this.originalGravity = entityProperties.gravity; - } this.currentPosition = entityProperties.position; - this.mass = getMass(entityProperties.dimensions, entityProperties.density); - var cameraPosition = Camera.getPosition(); - - var actionArgs = { - tag: getTag(), - ttl: ACTION_TTL - }; if (this.mode === "rotate") { var drag = mouse.getDrag(); @@ -510,19 +424,9 @@ Grabber.prototype.moveEventProcess = function() { var ROTATE_STRENGTH = 0.4; // magic number tuned by hand var angle = ROTATE_STRENGTH * Math.sqrt((drag.x * drag.x) + (drag.y * drag.y)); var deltaQ = Quat.angleAxis(angle, axis); - // var qZero = entityProperties.rotation; - //var qZero = this.lastRotation; + this.lastRotation = Quat.multiply(deltaQ, this.lastRotation); - - var distanceToCameraR = Vec3.length(Vec3.subtract(this.currentPosition, cameraPosition)); - var angularTimeScale = distanceGrabTimescale(this.mass, distanceToCameraR); - - actionArgs = { - targetRotation: this.lastRotation, - angularTimeScale: angularTimeScale, - tag: getTag(), - ttl: ACTION_TTL - }; + MyAvatar.setJointRotation(MOUSE_GRAB_JOINT, MyAvatar.worldToJointRotation(this.lastRotation)); } else { var newPointOnPlane; @@ -534,17 +438,10 @@ Grabber.prototype.moveEventProcess = function() { planeNormal = Vec3.normalize(planeNormal); var pointOnCylinder = Vec3.multiply(planeNormal, this.xzDistanceToGrab); pointOnCylinder = Vec3.sum(Camera.getPosition(), pointOnCylinder); - this.pointOnPlane = mouseIntersectionWithPlane(pointOnCylinder, planeNormal, mouse.current, this.maxDistance); - newPointOnPlane = { - x: this.pointOnPlane.x, - y: this.pointOnPlane.y, - z: this.pointOnPlane.z - }; - + newPointOnPlane = mouseIntersectionWithPlane(pointOnCylinder, planeNormal, mouse.current, this.maxDistance); } else { - - newPointOnPlane = mouseIntersectionWithPlane( - this.pointOnPlane, this.planeNormal, mouse.current, this.maxDistance); + var cameraPosition = Camera.getPosition(); + newPointOnPlane = mouseIntersectionWithPlane(this.pointOnPlane, this.planeNormal, mouse.current, this.maxDistance); var relativePosition = Vec3.subtract(newPointOnPlane, cameraPosition); var distance = Vec3.length(relativePosition); if (distance > this.maxDistance) { @@ -553,26 +450,8 @@ Grabber.prototype.moveEventProcess = function() { newPointOnPlane = Vec3.sum(relativePosition, cameraPosition); } } - this.targetPosition = Vec3.subtract(newPointOnPlane, this.offset); - var distanceToCameraL = Vec3.length(Vec3.subtract(this.targetPosition, cameraPosition)); - var linearTimeScale = distanceGrabTimescale(this.mass, distanceToCameraL); - - actionArgs = { - targetPosition: this.targetPosition, - linearTimeScale: linearTimeScale, - tag: getTag(), - ttl: ACTION_TTL - }; - - } - - if (!this.actionID) { - if (!entityIsGrabbedByOther(this.entityID) && !entityIsEquipped(this.entityID)) { - this.actionID = Entities.addAction("far-grab", this.entityID, actionArgs); - } - } else { - Entities.updateAction(this.entityID, this.actionID, actionArgs); + MyAvatar.setJointTranslation(MOUSE_GRAB_JOINT, MyAvatar.worldToJointPoint(Vec3.sum(newPointOnPlane, this.offset))); } this.scheduleMouseMoveProcessor(); @@ -601,6 +480,10 @@ Grabber.prototype.keyPressEvent = function(event) { Grabber.prototype.cleanup = function() { Pointers.removePointer(this.mouseRayEntities); Picks.removePick(this.mouseRayOverlays); + if (this.grabID) { + MyAvatar.releaseGrab(this.grabID); + this.grabID = null; + } }; var grabber = new Grabber(); diff --git a/scripts/system/html/js/entityProperties.js b/scripts/system/html/js/entityProperties.js index 8b7264eeb1..0bec77aa41 100644 --- a/scripts/system/html/js/entityProperties.js +++ b/scripts/system/html/js/entityProperties.js @@ -184,6 +184,20 @@ const GROUPS = [ id: "zone", addToGroup: "base", properties: [ + { + label: "Shape Type", + type: "dropdown", + options: { "box": "Box", "sphere": "Sphere", "ellipsoid": "Ellipsoid", + "cylinder-y": "Cylinder", "compound": "Use Compound Shape URL" }, + propertyID: "zoneShapeType", + propertyName: "shapeType", // actual entity property name + }, + { + label: "Compound Shape URL", + type: "string", + propertyID: "zoneCompoundShapeURL", + propertyName: "compoundShapeURL", // actual entity property name + }, { label: "Flying Allowed", type: "bool", @@ -1345,24 +1359,6 @@ const GROUPS = [ }, ] }, - { - id: "zone_shape", - label: "ZONE SHAPE", - properties: [ - { - label: "Shape Type", - type: "dropdown", - options: { "box": "Box", "sphere": "Sphere", "ellipsoid": "Ellipsoid", - "cylinder-y": "Cylinder", "compound": "Use Compound Shape URL" }, - propertyID: "shapeType", - }, - { - label: "Compound Shape URL", - type: "string", - propertyID: "compoundShapeURL", - }, - ] - }, { id: "physics", label: "PHYSICS", @@ -1454,7 +1450,7 @@ 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', 'zone_shape', 'physics' ], + Zone: [ 'base', 'zone', 'spatial', 'behavior', 'physics' ], Model: [ 'base', 'model', 'spatial', 'behavior', 'collision', 'physics' ], Image: [ 'base', 'image', 'spatial', 'behavior', 'collision', 'physics' ], Web: [ 'base', 'web', 'spatial', 'behavior', 'collision', 'physics' ], @@ -3317,7 +3313,8 @@ function loaded() { } } - let doSelectElement = lastEntityID === '"' + selectedEntityProperties.id + '"'; + let hasSelectedEntityChanged = lastEntityID !== '"' + selectedEntityProperties.id + '"'; + let doSelectElement = !hasSelectedEntityChanged; // the event bridge and json parsing handle our avatar id string differently. lastEntityID = '"' + selectedEntityProperties.id + '"'; @@ -3437,7 +3434,7 @@ function loaded() { property.elColorPicker.style.backgroundColor = "rgb(" + propertyValue.red + "," + propertyValue.green + "," + propertyValue.blue + ")"; - if ($(property.elColorPicker).attr('active') === 'true') { + if (hasSelectedEntityChanged && $(property.elColorPicker).attr('active') === 'true') { // Set the color picker inactive before setting the color, // otherwise an update will be sent directly after setting it here. $(property.elColorPicker).attr('active', 'false'); diff --git a/scripts/system/libraries/controllerDispatcherUtils.js b/scripts/system/libraries/controllerDispatcherUtils.js index 221af07474..385ed954b0 100644 --- a/scripts/system/libraries/controllerDispatcherUtils.js +++ b/scripts/system/libraries/controllerDispatcherUtils.js @@ -33,6 +33,7 @@ getGrabbableData:true, isAnothersAvatarEntity:true, isAnothersChildEntity:true, + entityIsEquippable:true, entityIsGrabbable:true, entityIsDistanceGrabbable:true, getControllerJointIndexCacheTime:true, @@ -46,7 +47,7 @@ makeLaserLockInfo:true, entityHasActions:true, ensureDynamic:true, - findGroupParent:true, + findGrabbableGroupParent:true, BUMPER_ON_VALUE:true, getEntityParents:true, findHandChildEntities:true, @@ -58,7 +59,6 @@ NEAR_GRAB_DISTANCE: true, distanceBetweenPointAndEntityBoundingBox:true, entityIsEquipped:true, - entityIsFarGrabbedByOther:true, highlightTargetEntity:true, clearHighlightedEntities:true, unhighlightTargetEntity:true, @@ -323,6 +323,18 @@ isAnothersChildEntity = function (iaceProps) { return false; }; + +entityIsEquippable = function (eieProps) { + var grabbable = getGrabbableData(eieProps).grabbable; + if (!grabbable || + isAnothersAvatarEntity(eieProps) || + isAnothersChildEntity(eieProps) || + FORBIDDEN_GRAB_TYPES.indexOf(eieProps.type) >= 0) { + return false; + } + return true; +}; + entityIsGrabbable = function (eigProps) { var grabbable = getGrabbableData(eigProps).grabbable; if (!grabbable || @@ -439,7 +451,7 @@ ensureDynamic = function (entityID) { } }; -findGroupParent = function (controllerData, targetProps) { +findGrabbableGroupParent = function (controllerData, targetProps) { while (targetProps.grab.grabDelegateToParent && targetProps.parentID && targetProps.parentID !== Uuid.NULL && @@ -448,6 +460,9 @@ findGroupParent = function (controllerData, targetProps) { if (!parentProps) { break; } + if (!entityIsGrabbable(parentProps)) { + break; + } parentProps.id = targetProps.parentID; targetProps = parentProps; controllerData.nearbyEntityPropertiesByID[targetProps.id] = targetProps; @@ -561,27 +576,6 @@ entityIsEquipped = function(entityID) { return equippedInRightHand || equippedInLeftHand; }; -entityIsFarGrabbedByOther = function(entityID) { - // by convention, a far grab sets the tag of its action to be far-grab-*owner-session-id*. - var actionIDs = Entities.getActionIDs(entityID); - var myFarGrabTag = "far-grab-" + MyAvatar.sessionUUID; - for (var actionIndex = 0; actionIndex < actionIDs.length; actionIndex++) { - var actionID = actionIDs[actionIndex]; - var actionArguments = Entities.getActionArguments(entityID, actionID); - var tag = actionArguments.tag; - if (tag == myFarGrabTag) { - // we see a far-grab-*uuid* shaped tag, but it's our tag, so that's okay. - continue; - } - if (tag.slice(0, 9) == "far-grab-") { - // we see a far-grab-*uuid* shaped tag and it's not ours, so someone else is grabbing it. - return true; - } - } - 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); @@ -614,12 +608,13 @@ if (typeof module !== 'undefined') { unhighlightTargetEntity: unhighlightTargetEntity, clearHighlightedEntities: clearHighlightedEntities, makeRunningValues: makeRunningValues, - findGroupParent: findGroupParent, + findGrabbableGroupParent: findGrabbableGroupParent, LEFT_HAND: LEFT_HAND, RIGHT_HAND: RIGHT_HAND, BUMPER_ON_VALUE: BUMPER_ON_VALUE, TEAR_AWAY_DISTANCE: TEAR_AWAY_DISTANCE, propsArePhysical: propsArePhysical, + entityIsEquippable: entityIsEquippable, entityIsGrabbable: entityIsGrabbable, NEAR_GRAB_RADIUS: NEAR_GRAB_RADIUS, projectOntoOverlayXYPlane: projectOntoOverlayXYPlane, diff --git a/tools/dissectors/3-hf-avatar.lua b/tools/dissectors/3-hf-avatar.lua index f4172b01cb..bc449770f5 100644 --- a/tools/dissectors/3-hf-avatar.lua +++ b/tools/dissectors/3-hf-avatar.lua @@ -379,6 +379,9 @@ function decode_avatar_data_packet(buf) i = i + num_validity_bytes result["valid_translations"] = "Valid Translations: " .. string.format("(%d/%d) {", #indices, num_joints) .. table.concat(indices, ", ") .. "}" + -- TODO: skip maxTranslationDimension + i = i + 4 + -- TODO: skip translations for now i = i + #indices * 6 diff --git a/tools/nitpick/CMakeLists.txt b/tools/nitpick/CMakeLists.txt index 8073edaa85..9d5d38ec1f 100644 --- a/tools/nitpick/CMakeLists.txt +++ b/tools/nitpick/CMakeLists.txt @@ -1,47 +1,27 @@ set(TARGET_NAME nitpick) project(${TARGET_NAME}) -function(JOIN VALUES GLUE OUTPUT) - string (REGEX REPLACE "([^\\]|^);" "\\1${GLUE}" _TMP_STR "${VALUES}") - string (REGEX REPLACE "[\\](.)" "\\1" _TMP_STR "${_TMP_STR}") #fixes escaping - set (${OUTPUT} "${_TMP_STR}" PARENT_SCOPE) -endfunction() - set(CUSTOM_NITPICK_QRC_PATHS "") find_npm() -if (BUILD_TOOLS AND NPM_EXECUTABLE) - add_custom_qrc_path(CUSTOM_NITPICK_QRC_PATHS "${CMAKE_SOURCE_DIR}/tools/jsdoc/out/hifiJSDoc.json" "auto-complete/hifiJSDoc.json") -endif () - set(RESOURCES_QRC ${CMAKE_CURRENT_BINARY_DIR}/resources.qrc) set(RESOURCES_RCC ${CMAKE_CURRENT_SOURCE_DIR}/compiledResources/resources.rcc) generate_qrc(OUTPUT ${RESOURCES_QRC} PATH ${CMAKE_CURRENT_SOURCE_DIR}/resources CUSTOM_PATHS ${CUSTOM_NITPICK_QRC_PATHS} GLOBS *) add_custom_command( - OUTPUT ${RESOURCES_RCC} - DEPENDS ${RESOURCES_QRC} ${GENERATE_QRC_DEPENDS} - COMMAND "${QT_DIR}/bin/rcc" - ARGS ${RESOURCES_QRC} -binary -o ${RESOURCES_RCC} + OUTPUT ${RESOURCES_RCC} + DEPENDS ${RESOURCES_QRC} ${GENERATE_QRC_DEPENDS} + COMMAND "${QT_DIR}/bin/rcc" + ARGS ${RESOURCES_QRC} -binary -o ${RESOURCES_RCC} ) -if (WIN32) - set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -bigobj") - add_definitions(-D_USE_MATH_DEFINES) # apparently needed to get M_PI and other defines from cmath/math.h - add_definitions(-DWINDOWS_LEAN_AND_MEAN) # needed to make sure windows doesn't go to crazy with its defines -endif() - # grab the implementation and header files from src dirs file(GLOB_RECURSE NITPICK_SRCS "src/*.cpp" "src/*.h") GroupSources("src") list(APPEND NITPICK_SRCS ${RESOURCES_RCC}) -find_package( - Qt5 COMPONENTS - Gui Widgets Multimedia Network Qml Quick Script Svg Xml - ${PLATFORM_QT_COMPONENTS} -) +find_package(Qt5 COMPONENTS Widgets) # grab the ui files in ui file (GLOB_RECURSE QT_UI_FILES ui/*.ui) @@ -54,59 +34,50 @@ qt5_wrap_ui(QT_UI_HEADERS "${QT_UI_FILES}") set(NITPICK_SRCS ${NITPICK_SRCS} "${QT_UI_HEADERS}" "${QT_RESOURCES}") if (APPLE) - # configure CMake to use a custom Info.plist - set_target_properties(${this_target} PROPERTIES MACOSX_BUNDLE_INFO_PLIST MacOSXBundleInfo.plist.in) + # configure CMake to use a custom Info.plist + set_target_properties(${this_target} PROPERTIES MACOSX_BUNDLE_INFO_PLIST MacOSXBundleInfo.plist.in) - if (PRODUCTION_BUILD) - set(MACOSX_BUNDLE_GUI_IDENTIFIER com.highfidelity.nitpick) - else () - if (DEV_BUILD) - set(MACOSX_BUNDLE_GUI_IDENTIFIER com.highfidelity.nitpick-dev) - elseif (PR_BUILD) - set(MACOSX_BUNDLE_GUI_IDENTIFIER com.highfidelity.nitpick-pr) + if (PRODUCTION_BUILD) + set(MACOSX_BUNDLE_GUI_IDENTIFIER com.highfidelity.nitpick) + else () + if (DEV_BUILD) + set(MACOSX_BUNDLE_GUI_IDENTIFIER com.highfidelity.nitpick-dev) + elseif (PR_BUILD) + set(MACOSX_BUNDLE_GUI_IDENTIFIER com.highfidelity.nitpick-pr) + endif () endif () - endif () - # set how the icon shows up in the Info.plist file - set(MACOSX_BUNDLE_ICON_FILE "${NITPICK_ICON_FILENAME}") + # set how the icon shows up in the Info.plist file + set(MACOSX_BUNDLE_ICON_FILE "${NITPICK_ICON_FILENAME}") - # set where in the bundle to put the resources file - set_source_files_properties(${CMAKE_CURRENT_SOURCE_DIR}/icon/${NITPICK_ICON_FILENAME} PROPERTIES MACOSX_PACKAGE_LOCATION Resources) + # set where in the bundle to put the resources file + set_source_files_properties(${CMAKE_CURRENT_SOURCE_DIR}/icon/${NITPICK_ICON_FILENAME} PROPERTIES MACOSX_PACKAGE_LOCATION Resources) - # append the discovered resources to our list of nitpick sources - list(APPEND NITPICK_SRCS ${CMAKE_CURRENT_SOURCE_DIR}/icon/${NITPICK_ICON_FILENAME}) + # append the discovered resources to our list of nitpick sources + list(APPEND NITPICK_SRCS ${CMAKE_CURRENT_SOURCE_DIR}/icon/${NITPICK_ICON_FILENAME}) endif() # create the executable, make it a bundle on OS X if (APPLE) - add_executable(${TARGET_NAME} MACOSX_BUNDLE ${NITPICK_SRCS} ${QM}) + add_executable(${TARGET_NAME} MACOSX_BUNDLE ${NITPICK_SRCS} ${QM}) - # make sure the output name for the .app bundle is correct - # Fix up the rpath so macdeployqt works - set_target_properties(${TARGET_NAME} PROPERTIES INSTALL_RPATH "@executable_path/../Frameworks") + # make sure the output name for the .app bundle is correct + # Fix up the rpath so macdeployqt works + set_target_properties(${TARGET_NAME} PROPERTIES INSTALL_RPATH "@executable_path/../Frameworks") elseif (WIN32) - # configure an rc file for the chosen icon - set(CONFIGURE_ICON_PATH "${CMAKE_CURRENT_SOURCE_DIR}/icon/${NITPICK_ICON_FILENAME}") - set(CONFIGURE_ICON_RC_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/Icon.rc") - configure_file("${HF_CMAKE_DIR}/templates/Icon.rc.in" ${CONFIGURE_ICON_RC_OUTPUT}) + # configure an rc file for the chosen icon + set(CONFIGURE_ICON_PATH "${CMAKE_CURRENT_SOURCE_DIR}/icon/${NITPICK_ICON_FILENAME}") + set(CONFIGURE_ICON_RC_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/Icon.rc") + configure_file("${HF_CMAKE_DIR}/templates/Icon.rc.in" ${CONFIGURE_ICON_RC_OUTPUT}) - set(APP_FULL_NAME "High Fidelity Nitpick") - set(CONFIGURE_VERSION_INFO_RC_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/VersionInfo.rc") - configure_file("${HF_CMAKE_DIR}/templates/VersionInfo.rc.in" ${CONFIGURE_VERSION_INFO_RC_OUTPUT}) + set(APP_FULL_NAME "High Fidelity Nitpick") + set(CONFIGURE_VERSION_INFO_RC_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/VersionInfo.rc") + configure_file("${HF_CMAKE_DIR}/templates/VersionInfo.rc.in" ${CONFIGURE_VERSION_INFO_RC_OUTPUT}) - # add an executable that also has the icon itself and the configured rc file as resources - add_executable(${TARGET_NAME} WIN32 ${NITPICK_SRCS} ${QM} ${CONFIGURE_ICON_RC_OUTPUT} ${CONFIGURE_VERSION_INFO_RC_OUTPUT}) - - if (NOT DEV_BUILD) - add_custom_command( - TARGET ${TARGET_NAME} - POST_BUILD - COMMAND "mt.exe" -manifest "${CMAKE_CURRENT_SOURCE_DIR}/nitpick.exe.manifest" -inputresource:"$"\;\#1 -outputresource:"$"\;\#1 - COMMENT "Adding OS version support manifest to exe" - ) - endif() + # add an executable that also has the icon itself and the configured rc file as resources + add_executable(${TARGET_NAME} WIN32 ${NITPICK_SRCS} ${QM} ${CONFIGURE_ICON_RC_OUTPUT} ${CONFIGURE_VERSION_INFO_RC_OUTPUT}) else () - add_executable(${TARGET_NAME} ${NITPICK_SRCS} ${QM}) + add_executable(${TARGET_NAME} ${NITPICK_SRCS} ${QM}) endif () add_dependencies(${TARGET_NAME} resources) @@ -115,46 +86,46 @@ add_dependencies(${TARGET_NAME} resources) # This will prevent the following linker warnings # LINK : warning LNK4075: ignoring '/INCREMENTAL' due to '/OPT:ICF' specification if (WIN32) - set_property(TARGET ${TARGET_NAME} APPEND_STRING PROPERTY LINK_FLAGS_DEBUG "/OPT:NOREF /OPT:NOICF") + set_property(TARGET ${TARGET_NAME} APPEND_STRING PROPERTY LINK_FLAGS_DEBUG "/OPT:NOREF /OPT:NOICF") endif() link_hifi_libraries(entities-renderer) # perform standard include and linking for found externals foreach(EXTERNAL ${OPTIONAL_EXTERNALS}) - if (${${EXTERNAL}_UPPERCASE}_REQUIRED) - find_package(${EXTERNAL} REQUIRED) - else () - find_package(${EXTERNAL}) - endif () - - if (${${EXTERNAL}_UPPERCASE}_FOUND AND NOT DISABLE_${${EXTERNAL}_UPPERCASE}) - add_definitions(-DHAVE_${${EXTERNAL}_UPPERCASE}) - - # include the library directories (ignoring warnings) - if (NOT ${${EXTERNAL}_UPPERCASE}_INCLUDE_DIRS) - set(${${EXTERNAL}_UPPERCASE}_INCLUDE_DIRS ${${${EXTERNAL}_UPPERCASE}_INCLUDE_DIR}) + if (${${EXTERNAL}_UPPERCASE}_REQUIRED) + find_package(${EXTERNAL} REQUIRED) + else () + find_package(${EXTERNAL}) endif () - include_directories(SYSTEM ${${${EXTERNAL}_UPPERCASE}_INCLUDE_DIRS}) + if (${${EXTERNAL}_UPPERCASE}_FOUND AND NOT DISABLE_${${EXTERNAL}_UPPERCASE}) + add_definitions(-DHAVE_${${EXTERNAL}_UPPERCASE}) - # perform the system include hack for OS X to ignore warnings - if (APPLE) - foreach(EXTERNAL_INCLUDE_DIR ${${${EXTERNAL}_UPPERCASE}_INCLUDE_DIRS}) - SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -isystem ${EXTERNAL_INCLUDE_DIR}") - endforeach() - endif () + # include the library directories (ignoring warnings) + if (NOT ${${EXTERNAL}_UPPERCASE}_INCLUDE_DIRS) + set(${${EXTERNAL}_UPPERCASE}_INCLUDE_DIRS ${${${EXTERNAL}_UPPERCASE}_INCLUDE_DIR}) + endif () - if (NOT ${${EXTERNAL}_UPPERCASE}_LIBRARIES) - set(${${EXTERNAL}_UPPERCASE}_LIBRARIES ${${${EXTERNAL}_UPPERCASE}_LIBRARY}) - endif () + include_directories(SYSTEM ${${${EXTERNAL}_UPPERCASE}_INCLUDE_DIRS}) - if (NOT APPLE OR NOT ${${EXTERNAL}_UPPERCASE} MATCHES "SIXENSE") - target_link_libraries(${TARGET_NAME} ${${${EXTERNAL}_UPPERCASE}_LIBRARIES}) - elseif (APPLE AND NOT INSTALLER_BUILD) - add_definitions(-DSIXENSE_LIB_FILENAME=\"${${${EXTERNAL}_UPPERCASE}_LIBRARY_RELEASE}\") + # perform the system include hack for OS X to ignore warnings + if (APPLE) + foreach(EXTERNAL_INCLUDE_DIR ${${${EXTERNAL}_UPPERCASE}_INCLUDE_DIRS}) + SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -isystem ${EXTERNAL_INCLUDE_DIR}") + endforeach() + endif () + + if (NOT ${${EXTERNAL}_UPPERCASE}_LIBRARIES) + set(${${EXTERNAL}_UPPERCASE}_LIBRARIES ${${${EXTERNAL}_UPPERCASE}_LIBRARY}) + endif () + + if (NOT APPLE OR NOT ${${EXTERNAL}_UPPERCASE} MATCHES "SIXENSE") + target_link_libraries(${TARGET_NAME} ${${${EXTERNAL}_UPPERCASE}_LIBRARIES}) + elseif (APPLE AND NOT INSTALLER_BUILD) + add_definitions(-DSIXENSE_LIB_FILENAME=\"${${${EXTERNAL}_UPPERCASE}_LIBRARY_RELEASE}\") + endif () endif () - endif () endforeach() # include headers for nitpick and NitpickConfig. @@ -170,59 +141,50 @@ if (UNIX AND NOT ANDROID) endif () endif() -# assume we are using a Qt build without bearer management -add_definitions(-DQT_NO_BEARERMANAGEMENT) +# add a custom command to copy the empty AppData High Fidelity folder (i.e. - a valid folder with no entities) +if (WIN32) + add_custom_command( + TARGET ${TARGET_NAME} + POST_BUILD + COMMAND "${CMAKE_COMMAND}" -E copy_directory "${CMAKE_CURRENT_SOURCE_DIR}/AppDataHighFidelity" "$/AppDataHighFidelity" + ) + + if (RELEASE_TYPE STREQUAL "DEV") + # This to enable running from the IDE + add_custom_command( + TARGET ${TARGET_NAME} + POST_BUILD + COMMAND "${CMAKE_COMMAND}" -E copy_directory "${CMAKE_CURRENT_SOURCE_DIR}/AppDataHighFidelity" "AppDataHighFidelity" + ) + endif () +elseif (APPLE) + add_custom_command( + TARGET ${TARGET_NAME} + POST_BUILD + COMMAND "${CMAKE_COMMAND}" -E copy_directory "${CMAKE_CURRENT_SOURCE_DIR}/AppDataHighFidelity" "$/AppDataHighFidelity" + ) +endif() if (APPLE) - # link in required OS X frameworks and include the right GL headers - find_library(OpenGL OpenGL) - find_library(AppKit AppKit) - - target_link_libraries(${TARGET_NAME} ${OpenGL} ${AppKit}) - - # setup install of OS X nitpick bundle - install(TARGETS ${TARGET_NAME} - BUNDLE DESTINATION ${NITPICK_INSTALL_DIR} - COMPONENT ${CLIENT_COMPONENT} - ) - - # call the fixup_nitpick macro to add required bundling commands for installation - fixup_nitpick() - -else() - set(NITPICK_EXEC_DIR "$") - set(RESOURCES_DEV_DIR "${NITPICK_EXEC_DIR}/resources") - - # copy the resources files beside the executable - add_custom_command(TARGET ${TARGET_NAME} POST_BUILD - COMMAND "${CMAKE_COMMAND}" -E copy_if_different - "${RESOURCES_RCC}" - "${NITPICK_EXEC_DIR}" - # FIXME, the edit script code loads HTML from the scripts folder - # which in turn relies on CSS that refers to the fonts. In theory - # we should be able to modify the CSS to reference the QRC path to - # the ttf files, but doing so generates a CORS policy violation, - # so we have to retain a copy of the fonts outside of the resources binary - ) - - # link target to external libraries - if (WIN32) - # setup install of executable and things copied by fixup/windeployqt - install( - DIRECTORY "$/" - DESTINATION ${NITPICK_INSTALL_DIR} - COMPONENT ${CLIENT_COMPONENT} - PATTERN "*.pdb" EXCLUDE - PATTERN "*.lib" EXCLUDE - PATTERN "*.exp" EXCLUDE + # setup install of OS X nitpick bundle + install(TARGETS ${TARGET_NAME} + BUNDLE DESTINATION ${NITPICK_INSTALL_DIR} + COMPONENT ${CLIENT_COMPONENT} ) - set(SCRIPTS_INSTALL_DIR "${NITPICK_INSTALL_DIR}") - - set(EXECUTABLE_COMPONENT ${CLIENT_COMPONENT}) - - optional_win_executable_signing() - endif() + # call the fixup_nitpick macro to add required bundling commands for installation + fixup_nitpick() +elseif (WIN32) + # link target to external libraries + # setup install of executable and things copied by fixup/windeployqt + install( + DIRECTORY "$/" + DESTINATION ${NITPICK_INSTALL_DIR} + COMPONENT ${CLIENT_COMPONENT} + PATTERN "*.pdb" EXCLUDE + PATTERN "*.lib" EXCLUDE + PATTERN "*.exp" EXCLUDE + ) endif() if (WIN32) @@ -230,10 +192,6 @@ if (WIN32) set(TARGET_INSTALL_DIR ${NITPICK_INSTALL_DIR}) set(TARGET_INSTALL_COMPONENT ${CLIENT_COMPONENT}) - manually_install_openssl_for_qt() package_libraries_for_deployment() endif() - -# tell CMake to exclude ui_console.h for policy CMP0071 -set_property(SOURCE ui_console.h PROPERTY SKIP_AUTOMOC ON) diff --git a/tools/nitpick/README.md b/tools/nitpick/README.md index 23105a0e02..b027f222d3 100644 --- a/tools/nitpick/README.md +++ b/tools/nitpick/README.md @@ -6,42 +6,16 @@ Nitpick is a stand alone application that provides a mechanism for regression te * The result, if any test failed, is a zipped folder describing the failure. Nitpick has 5 functions, separated into separate tabs: + 1. Creating tests, MD files and recursive scripts 1. Windows task bar utility (Windows only) 1. Running tests 1. Evaluating the results of running tests 1. Web interface -## Build (for developers) -Nitpick is built as part of the High Fidelity build. -### Creating installers -#### Windows -1. Verify that 7Zip is installed. -1. cd to the `build\tools\nitpick\Release` directory -1. Delete any existing installers (named nitpick-installer-###.exe) -1. Select all, right-click and select 7-Zip->Add to archive... -1. Set Archive format to 7z -1. Check "Create SFX archive -1. Enter installer name (i.e. `nitpick-installer-v1.2.exe`) -1. Click "OK" -1. Copy created installer to https://hifi-qa.s3.amazonaws.com/nitpick/Windows/nitpick-installer-v1.2.exe: aws s3 cp nitpick-installer-v1.2.exe s3://hifi-qa/nitpick/Mac/nitpick-installer-v1.2.exe -#### Mac -These steps assume the hifi repository has been cloned to `~/hifi`. -1. (first time) Install brew - In a terminal: `/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)` -1. (First time) install create-dmg: - In a terminal: `brew install create-dmg` -1. In a terminal: cd to the `build/tools/nitpick/Release` folder -1. Copy the quazip dynamic library (note final period): - In a terminal: `cp ~/hifi/build/ext/Xcode/quazip/project/lib/libquazip5.1.dylib .` -1. Change the loader instruction to find the dynamic library locally - In a terminal: `install_name_tool -change ~/hifi/build/ext/Xcode/quazip/project/lib/libquazip5.1.dylib libquazip5.1.dylib nitpick` -1. Delete any existing disk images. In a terminal: `rm *.dmg` -1. Create installer (note final period).In a terminal: `create-dmg --volname nitpick-installer-v1.2 nitpick-installer-v1.2.dmg .` - Make sure to wait for completion. -1. Copy created installer to AWS: `~/Library/Python/3.7/bin/aws s3 cp nitpick-installer-v1.2.dmg s3://hifi-qa/nitpick/Mac/nitpick-installer-v1.2.dmg` -### Installation -#### Windows +## Installation +`nitpick` is packaged with High Fidelity PR and Development builds. +### Windows 1. (First time) download and install vc_redist.x64.exe (available at https://hifi-qa.s3.amazonaws.com/nitpick/Windows/nitpick-installer-v1.2.exe) 1. (First time) download and install Python 3 from https://hifi-qa.s3.amazonaws.com/nitpick/Windows/python-3.7.0-amd64.exe (also located at https://www.python.org/downloads/) 1. After installation - create an environment variable called PYTHON_PATH and set it to the folder containing the Python executable. @@ -52,12 +26,9 @@ These steps assume the hifi repository has been cloned to `~/hifi`. 1. Leave region name and ouput format as default [None] 1. Install the latest release of Boto3 via pip: `pip install boto3` -1. Download the installer by browsing to [here]() -1. Double click on the installer and install to a convenient location -![](./setup_7z.PNG) - -1. __To run nitpick, double click **nitpick.exe**__ -#### Mac +1. (First time) Install adb (Android Debug Bridge) from `https://dl.google.com/android/repository/platform-tools-latest-windows.zip` + 1. Create and environment variable named ADB_PATH and set its value to the installation location (e.g. **C:\adb**) +### Mac 1. (first time) Install brew In a terminal: `/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)` 1. (First time) install Qt: @@ -75,17 +46,8 @@ In a terminal: `python3 get-pip.py --user` 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: pip3 install boto3 -1. Download the installer by browsing to [here](). -1. Double-click on the downloaded image to mount it -1. Create a folder for the nitpick files (e.g. ~/nitpick) - If this folder exists then delete all it's contents. -1. Copy the downloaded files to the folder - In a terminal: - `cd ~/nitpick` - `cp -r /Volumes/nitpick-installer-v1.2/* .` - -1. __To run nitpick, cd to the folder that you copied to and run `./nitpick`__ + 1. Install the latest release of Boto3 via pip: pip3 install boto3 +1. (First time)Install adb (Android Debug Bridge) from `https://dl.google.com/android/repository/platform-tools-latest-darwin.zip` # Usage ## Create ![](./Create.PNG) @@ -164,7 +126,7 @@ nitpick.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 (only) ![](./Windows.PNG) This tab is Windows-specific. It provides buttons to hide and show the task bar. diff --git a/tools/nitpick/compiledResources/resources.rcc b/tools/nitpick/compiledResources/resources.rcc index 29a6647b40..15f51ed7f4 100644 Binary files a/tools/nitpick/compiledResources/resources.rcc and b/tools/nitpick/compiledResources/resources.rcc differ diff --git a/tools/nitpick/src/Nitpick.cpp b/tools/nitpick/src/Nitpick.cpp index 0bd397715b..9e385bcd4d 100644 --- a/tools/nitpick/src/Nitpick.cpp +++ b/tools/nitpick/src/Nitpick.cpp @@ -26,7 +26,7 @@ Nitpick::Nitpick(QWidget* parent) : QMainWindow(parent) { _signalMapper = new QSignalMapper(); - connect(_ui.actionClose, &QAction::triggered, this, &Nitpick::on_closeButton_clicked); + connect(_ui.actionClose, &QAction::triggered, this, &Nitpick::on_closePushbutton_clicked); connect(_ui.actionAbout, &QAction::triggered, this, &Nitpick::about); connect(_ui.actionContent, &QAction::triggered, this, &Nitpick::content); @@ -35,10 +35,12 @@ Nitpick::Nitpick(QWidget* parent) : QMainWindow(parent) { _ui.tabWidget->removeTab(1); #endif - _ui.statusLabel->setText(""); - _ui.plainTextEdit->setReadOnly(true); + _ui.statusLabelOnDesktop->setText(""); + _ui.statusLabelOnMobile->setText(""); + + _ui.plainTextEdit->setReadOnly(true); - setWindowTitle("Nitpick - v1.3.2"); + setWindowTitle("Nitpick - v2.0.1"); } Nitpick::~Nitpick() { @@ -48,8 +50,12 @@ Nitpick::~Nitpick() { delete _test; } - if (_testRunner) { - delete _testRunner; + if (_testRunnerDesktop) { + delete _testRunnerDesktop; + } + + if (_testRunnerMobile) { + delete _testRunnerMobile; } } @@ -80,10 +86,37 @@ void Nitpick::setup() { timeEdits.emplace_back(_ui.timeEdit3); timeEdits.emplace_back(_ui.timeEdit4); - if (_testRunner) { - delete _testRunner; + // Create the two test runners + if (_testRunnerDesktop) { + delete _testRunnerDesktop; } - _testRunner = new TestRunner(dayCheckboxes, timeEditCheckboxes, timeEdits, _ui.workingFolderLabel, _ui.checkBoxServerless, _ui.checkBoxRunLatest, _ui.urlLineEdit, _ui.runNowButton); + _testRunnerDesktop = new TestRunnerDesktop( + dayCheckboxes, + timeEditCheckboxes, + timeEdits, + _ui.workingFolderRunOnDesktopLabel, + _ui.checkBoxServerless, + _ui.runLatestOnDesktopCheckBox, + _ui.urlOnDesktopLineEdit, + _ui.runNowPushbutton, + _ui.statusLabelOnDesktop + ); + + if (_testRunnerMobile) { + delete _testRunnerMobile; + } + _testRunnerMobile = new TestRunnerMobile( + _ui.workingFolderRunOnMobileLabel, + _ui.connectDevicePushbutton, + _ui.pullFolderPushbutton, + _ui.detectedDeviceLabel, + _ui.folderLineEdit, + _ui.downloadAPKPushbutton, + _ui.installAPKPushbutton, + _ui.runLatestOnMobileCheckBox, + _ui.urlOnMobileLineEdit, + _ui.statusLabelOnMobile + ); } void Nitpick::startTestsEvaluation(const bool isRunningFromCommandLine, @@ -98,9 +131,9 @@ void Nitpick::startTestsEvaluation(const bool isRunningFromCommandLine, void Nitpick::on_tabWidget_currentChanged(int index) { // Enable the GitHub edit boxes as required #ifdef Q_OS_WIN - if (index == 0 || index == 2 || index == 3) { + if (index == 0 || index == 2 || index == 3 || index == 4) { #else - if (index == 0 || index == 1 || index == 2) { + if (index == 0 || index == 1 || index == 2 || index == 3) { #endif _ui.userLineEdit->setDisabled(false); _ui.branchLineEdit->setDisabled(false); @@ -110,43 +143,43 @@ void Nitpick::on_tabWidget_currentChanged(int index) { } } -void Nitpick::on_evaluateTestsButton_clicked() { +void Nitpick::on_evaluateTestsPushbutton_clicked() { _test->startTestsEvaluation(false, false); } -void Nitpick::on_createRecursiveScriptButton_clicked() { +void Nitpick::on_createRecursiveScriptPushbutton_clicked() { _test->createRecursiveScript(); } -void Nitpick::on_createAllRecursiveScriptsButton_clicked() { +void Nitpick::on_createAllRecursiveScriptsPushbutton_clicked() { _test->createAllRecursiveScripts(); } -void Nitpick::on_createTestsButton_clicked() { +void Nitpick::on_createTestsPushbutton_clicked() { _test->createTests(); } -void Nitpick::on_createMDFileButton_clicked() { +void Nitpick::on_createMDFilePushbutton_clicked() { _test->createMDFile(); } -void Nitpick::on_createAllMDFilesButton_clicked() { +void Nitpick::on_createAllMDFilesPushbutton_clicked() { _test->createAllMDFiles(); } -void Nitpick::on_createTestAutoScriptButton_clicked() { +void Nitpick::on_createTestAutoScriptPushbutton_clicked() { _test->createTestAutoScript(); } -void Nitpick::on_createAllTestAutoScriptsButton_clicked() { +void Nitpick::on_createAllTestAutoScriptsPushbutton_clicked() { _test->createAllTestAutoScripts(); } -void Nitpick::on_createTestsOutlineButton_clicked() { +void Nitpick::on_createTestsOutlinePushbutton_clicked() { _test->createTestsOutline(); } -void Nitpick::on_createTestRailTestCasesButton_clicked() { +void Nitpick::on_createTestRailTestCasesPushbutton_clicked() { _test->createTestRailTestCases(); } @@ -154,29 +187,29 @@ void Nitpick::on_createTestRailRunButton_clicked() { _test->createTestRailRun(); } -void Nitpick::on_setWorkingFolderButton_clicked() { - _testRunner->setWorkingFolder(); +void Nitpick::on_setWorkingFolderRunOnDesktopPushbutton_clicked() { + _testRunnerDesktop->setWorkingFolderAndEnableControls(); } void Nitpick::enableRunTabControls() { - _ui.runNowButton->setEnabled(true); + _ui.runNowPushbutton->setEnabled(true); _ui.daysGroupBox->setEnabled(true); _ui.timesGroupBox->setEnabled(true); } -void Nitpick::on_runNowButton_clicked() { - _testRunner->run(); +void Nitpick::on_runNowPushbutton_clicked() { + _testRunnerDesktop->run(); } -void Nitpick::on_checkBoxRunLatest_clicked() { - _ui.urlLineEdit->setEnabled(!_ui.checkBoxRunLatest->isChecked()); +void Nitpick::on_runLatestOnDesktopCheckBox_clicked() { + _ui.urlOnDesktopLineEdit->setEnabled(!_ui.runLatestOnDesktopCheckBox->isChecked()); } void Nitpick::automaticTestRunEvaluationComplete(QString zippedFolderName, int numberOfFailures) { - _testRunner->automaticTestRunEvaluationComplete(zippedFolderName, numberOfFailures); + _testRunnerDesktop->automaticTestRunEvaluationComplete(zippedFolderName, numberOfFailures); } -void Nitpick::on_updateTestRailRunResultsButton_clicked() { +void Nitpick::on_updateTestRailRunResultsPushbutton_clicked() { _test->updateTestRailRunResult(); } @@ -184,7 +217,7 @@ void Nitpick::on_updateTestRailRunResultsButton_clicked() { // if (uState & ABS_AUTOHIDE) on_showTaskbarButton_clicked(); // else on_hideTaskbarButton_clicked(); // -void Nitpick::on_hideTaskbarButton_clicked() { +void Nitpick::on_hideTaskbarPushbutton_clicked() { #ifdef Q_OS_WIN APPBARDATA abd = { sizeof abd }; UINT uState = (UINT)SHAppBarMessage(ABM_GETSTATE, &abd); @@ -194,7 +227,7 @@ void Nitpick::on_hideTaskbarButton_clicked() { #endif } -void Nitpick::on_showTaskbarButton_clicked() { +void Nitpick::on_showTaskbarPushbutton_clicked() { #ifdef Q_OS_WIN APPBARDATA abd = { sizeof abd }; UINT uState = (UINT)SHAppBarMessage(ABM_GETSTATE, &abd); @@ -204,7 +237,7 @@ void Nitpick::on_showTaskbarButton_clicked() { #endif } -void Nitpick::on_closeButton_clicked() { +void Nitpick::on_closePushbutton_clicked() { exit(0); } @@ -216,7 +249,7 @@ void Nitpick::on_createXMLScriptRadioButton_clicked() { _test->setTestRailCreateMode(XML); } -void Nitpick::on_createWebPagePushButton_clicked() { +void Nitpick::on_createWebPagePushbutton_clicked() { _test->createWebPage(_ui.updateAWSCheckBox, _ui.awsURLLineEdit); } @@ -273,9 +306,13 @@ void Nitpick::saveFile(int index) { disconnect(_signalMapper, SIGNAL(mapped(int)), this, SLOT(saveFile(int))); if (_caller == _test) { _test->finishTestsEvaluation(); - } else if (_caller == _testRunner) { - _testRunner->downloadComplete(); + } else if (_caller == _testRunnerDesktop) { + _testRunnerDesktop->downloadComplete(); + } else if (_caller == _testRunnerMobile) { + _testRunnerMobile->downloadComplete(); } + + _ui.progressBar->setVisible(false); } else { _ui.progressBar->setValue(_numberOfFilesDownloaded); } @@ -305,10 +342,31 @@ QString Nitpick::getSelectedBranch() { return _ui.branchLineEdit->text(); } -void Nitpick::updateStatusLabel(const QString& status) { - _ui.statusLabel->setText(status); -} - void Nitpick::appendLogWindow(const QString& message) { _ui.plainTextEdit->appendPlainText(message); } + +// Test on Mobile +void Nitpick::on_setWorkingFolderRunOnMobilePushbutton_clicked() { + _testRunnerMobile->setWorkingFolderAndEnableControls(); +} + +void Nitpick::on_connectDevicePushbutton_clicked() { + _testRunnerMobile->connectDevice(); +} + +void Nitpick::on_runLatestOnMobileCheckBox_clicked() { + _ui.urlOnMobileLineEdit->setEnabled(!_ui.runLatestOnMobileCheckBox->isChecked()); +} + +void Nitpick::on_downloadAPKPushbutton_clicked() { + _testRunnerMobile->downloadAPK(); +} + +void Nitpick::on_installAPKPushbutton_clicked() { + _testRunnerMobile->installAPK(); +} + +void Nitpick::on_pullFolderPushbutton_clicked() { + _testRunnerMobile->pullFolder(); +} diff --git a/tools/nitpick/src/Nitpick.h b/tools/nitpick/src/Nitpick.h index 7b95081d66..00516d1e76 100644 --- a/tools/nitpick/src/Nitpick.h +++ b/tools/nitpick/src/Nitpick.h @@ -18,7 +18,9 @@ #include "Downloader.h" #include "Test.h" -#include "TestRunner.h" +#include "TestRunnerDesktop.h" +#include "TestRunnerMobile.h" + #include "AWSInterface.h" class Nitpick : public QMainWindow { @@ -49,54 +51,65 @@ public: void enableRunTabControls(); - void updateStatusLabel(const QString& status); void appendLogWindow(const QString& message); private slots: + void on_closePushbutton_clicked(); + void on_tabWidget_currentChanged(int index); - void on_evaluateTestsButton_clicked(); - void on_createRecursiveScriptButton_clicked(); - void on_createAllRecursiveScriptsButton_clicked(); - void on_createTestsButton_clicked(); + void on_evaluateTestsPushbutton_clicked(); + void on_createRecursiveScriptPushbutton_clicked(); + void on_createAllRecursiveScriptsPushbutton_clicked(); + void on_createTestsPushbutton_clicked(); - void on_createMDFileButton_clicked(); - void on_createAllMDFilesButton_clicked(); + void on_createMDFilePushbutton_clicked(); + void on_createAllMDFilesPushbutton_clicked(); - void on_createTestAutoScriptButton_clicked(); - void on_createAllTestAutoScriptsButton_clicked(); + void on_createTestAutoScriptPushbutton_clicked(); + void on_createAllTestAutoScriptsPushbutton_clicked(); - void on_createTestsOutlineButton_clicked(); + void on_createTestsOutlinePushbutton_clicked(); - void on_createTestRailTestCasesButton_clicked(); + void on_createTestRailTestCasesPushbutton_clicked(); void on_createTestRailRunButton_clicked(); - void on_setWorkingFolderButton_clicked(); - void on_runNowButton_clicked(); + void on_setWorkingFolderRunOnDesktopPushbutton_clicked(); + void on_runNowPushbutton_clicked(); - void on_checkBoxRunLatest_clicked(); + void on_runLatestOnDesktopCheckBox_clicked(); - void on_updateTestRailRunResultsButton_clicked(); + void on_updateTestRailRunResultsPushbutton_clicked(); - void on_hideTaskbarButton_clicked(); - void on_showTaskbarButton_clicked(); + void on_hideTaskbarPushbutton_clicked(); + void on_showTaskbarPushbutton_clicked(); void on_createPythonScriptRadioButton_clicked(); void on_createXMLScriptRadioButton_clicked(); - void on_createWebPagePushButton_clicked(); - - void on_closeButton_clicked(); + void on_createWebPagePushbutton_clicked(); void saveFile(int index); void about(); void content(); + // Run on Mobile controls + void on_setWorkingFolderRunOnMobilePushbutton_clicked(); + void on_connectDevicePushbutton_clicked(); + void on_runLatestOnMobileCheckBox_clicked(); + + void on_downloadAPKPushbutton_clicked(); + void on_installAPKPushbutton_clicked(); + + void on_pullFolderPushbutton_clicked(); + private: Ui::NitpickClass _ui; Test* _test{ nullptr }; - TestRunner* _testRunner{ nullptr }; + + TestRunnerDesktop* _testRunnerDesktop{ nullptr }; + TestRunnerMobile* _testRunnerMobile{ nullptr }; AWSInterface _awsInterface; diff --git a/tools/nitpick/src/PythonInterface.cpp b/tools/nitpick/src/PythonInterface.cpp index 9832ac9f8d..9e2fec005f 100644 --- a/tools/nitpick/src/PythonInterface.cpp +++ b/tools/nitpick/src/PythonInterface.cpp @@ -16,13 +16,13 @@ PythonInterface::PythonInterface() { #ifdef Q_OS_WIN 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); + QString pythonPath = QProcessEnvironment::systemEnvironment().value("PYTHON_PATH"); + if (!QFile::exists(pythonPath + "/" + _pythonExe)) { + QMessageBox::critical(0, _pythonExe, QString("Python executable not found in ") + pythonPath); exit(-1); } - _pythonCommand = _pythonPath + "/" + _pythonExe; + _pythonCommand = pythonPath + "/" + _pythonExe; } else { QMessageBox::critical(0, "PYTHON_PATH not defined", "Please set PYTHON_PATH to directory containing the Python executable"); @@ -31,7 +31,7 @@ PythonInterface::PythonInterface() { #elif defined Q_OS_MAC _pythonCommand = "/usr/local/bin/python3"; if (!QFile::exists(_pythonCommand)) { - QMessageBox::critical(0, "PYTHON_PATH not defined", + QMessageBox::critical(0, "python not found", "python3 not found at " + _pythonCommand); exit(-1); } diff --git a/tools/nitpick/src/TestRunner.cpp b/tools/nitpick/src/TestRunner.cpp index a895774712..54246de80b 100644 --- a/tools/nitpick/src/TestRunner.cpp +++ b/tools/nitpick/src/TestRunner.cpp @@ -1,7 +1,7 @@ // // TestRunner.cpp // -// Created by Nissim Hadar on 1 Sept 2018. +// Created by Nissim Hadar on 23 Jan 2019. // Copyright 2013 High Fidelity, Inc. // // Distributed under the Apache License, Version 2.0. @@ -9,70 +9,12 @@ // #include "TestRunner.h" -#include -#include -#include +#include #include "Nitpick.h" extern Nitpick* nitpick; -#ifdef Q_OS_WIN -#include -#include -#endif - -// TODO: for debug -#include - -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 _installerWorker; - - delete _interfaceThread; - delete _interfaceWorker; - - if (_timer) { - delete _timer; - } -} - -void TestRunner::setWorkingFolder() { +void TestRunner::setWorkingFolder(QLabel* workingFolderLabel) { // Everything will be written to this folder QString previousSelection = _workingFolder; QString parent = previousSelection.left(previousSelection.lastIndexOf('/')); @@ -80,8 +22,8 @@ void TestRunner::setWorkingFolder() { parent += "/"; } - _workingFolder = QFileDialog::getExistingDirectory(nullptr, "Please select a temporary folder for installation", parent, - QFileDialog::ShowDirsOnly); + _workingFolder = QFileDialog::getExistingDirectory(nullptr, "Please select a working folder for temporary files", parent, + QFileDialog::ShowDirsOnly); // If user canceled then restore previous selection and return if (_workingFolder == "") { @@ -89,643 +31,25 @@ void TestRunner::setWorkingFolder() { return; } -#ifdef Q_OS_WIN - _installationFolder = _workingFolder + "/High Fidelity"; -#elif defined Q_OS_MAC - _installationFolder = _workingFolder + "/High_Fidelity"; -#endif + workingFolderLabel->setText(QDir::toNativeSeparators(_workingFolder)); + // This file is used for debug purposes. _logFile.setFileName(_workingFolder + "/log.txt"); - - nitpick->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 - -#ifdef Q_OS_MAC - // Create MAC shell scripts - QFile script; - - // This script waits for a process to start - script.setFileName(_workingFolder + "/waitForStart.sh"); - if (!script.open(QIODevice::WriteOnly | QIODevice::Text)) { - QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), - "Could not open 'waitForStart.sh'"); - exit(-1); - } - - script.write("#!/bin/sh\n\n"); - script.write("PROCESS=\"$1\"\n"); - script.write("until (pgrep -x $PROCESS >nul)\n"); - script.write("do\n"); - script.write("\techo waiting for \"$1\" to start\n"); - script.write("\tsleep 2\n"); - script.write("done\n"); - script.write("echo \"$1\" \"started\"\n"); - script.close(); - script.setPermissions(QFile::ReadOwner | QFile::WriteOwner | QFile::ExeOwner); - - // The Mac shell command returns immediately. This little script waits for a process to finish - script.setFileName(_workingFolder + "/waitForFinish.sh"); - if (!script.open(QIODevice::WriteOnly | QIODevice::Text)) { - QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), - "Could not open 'waitForFinish.sh'"); - exit(-1); - } - - script.write("#!/bin/sh\n\n"); - script.write("PROCESS=\"$1\"\n"); - script.write("while (pgrep -x $PROCESS >nul)\n"); - script.write("do\n"); - script.write("\techo waiting for \"$1\" to finish\n"); - script.write("\tsleep 2\n"); - script.write("done\n"); - script.write("echo \"$1\" \"finished\"\n"); - script.close(); - script.setPermissions(QFile::ReadOwner | QFile::WriteOwner | QFile::ExeOwner); - - // Create an AppleScript to resize Interface. This is needed so that snapshots taken - // with the primary camera will be the correct size. - // This will be run from a normal shell script - script.setFileName(_workingFolder + "/setInterfaceSizeAndPosition.scpt"); - if (!script.open(QIODevice::WriteOnly | QIODevice::Text)) { - QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), - "Could not open 'setInterfaceSizeAndPosition.scpt'"); - exit(-1); - } - - script.write("set width to 960\n"); - script.write("set height to 540\n"); - script.write("set x to 100\n"); - script.write("set y to 100\n\n"); - script.write("tell application \"System Events\" to tell application process \"interface\" to tell window 1 to set {size, position} to {{width, height}, {x, y}}\n"); - - script.close(); - script.setPermissions(QFile::ReadOwner | QFile::WriteOwner | QFile::ExeOwner); - - script.setFileName(_workingFolder + "/setInterfaceSizeAndPosition.sh"); - if (!script.open(QIODevice::WriteOnly | QIODevice::Text)) { - QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), - "Could not open 'setInterfaceSizeAndPosition.sh'"); - exit(-1); - } - - script.write("#!/bin/sh\n\n"); - script.write("echo resizing interface\n"); - script.write(("osascript " + _workingFolder + "/setInterfaceSizeAndPosition.scpt\n").toStdString().c_str()); - script.write("echo resize complete\n"); - script.close(); - script.setPermissions(QFile::ReadOwner | QFile::WriteOwner | QFile::ExeOwner); -#endif } -void TestRunner::run() { - _runNow->setEnabled(false); - - _testStartDateTime = QDateTime::currentDateTime(); - _automatedTestIsRunning = true; - - // Initial setup - _branch = nitpick->getSelectedBranch(); - _user = nitpick->getSelectedUser(); - - // This will be restored at the end of the tests - saveExistingHighFidelityAppDataFolder(); - +void TestRunner::downloadBuildXml(void* caller) { // 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 + buildXMLDownloaded = false; + QStringList urls; QStringList filenames; urls << DEV_BUILD_XML_URL; filenames << DEV_BUILD_XML_FILENAME; - updateStatusLabel("Downloading Build XML"); - - buildXMLDownloaded = false; - nitpick->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"); - - nitpick->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; -#ifdef Q_OS_WIN - commandLine = "\"" + QDir::toNativeSeparators(installerFullPath) + "\"" + " /S /D=" + QDir::toNativeSeparators(_installationFolder); -#elif defined Q_OS_MAC - // Create installation shell script - QFile script; - script.setFileName(_workingFolder + "/install_app.sh"); - if (!script.open(QIODevice::WriteOnly | QIODevice::Text)) { - QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), - "Could not open 'install_app.sh'"); - exit(-1); - } - - if (!QDir().exists(_installationFolder)) { - QDir().mkdir(_installationFolder); - } - - // This script installs High Fidelity. It is run as "yes | install_app.sh... so "yes" is killed at the end - script.write("#!/bin/sh\n\n"); - script.write("VOLUME=`hdiutil attach \"$1\" | grep Volumes | awk '{print $3}'`\n"); - - QString folderName {"High Fidelity"}; - if (!_runLatest->isChecked()) { - folderName += QString(" - ") + getPRNumberFromURL(_url->text()); - } - - script.write((QString("cp -rf \"$VOLUME/") + folderName + "/interface.app\" \"" + _workingFolder + "/High_Fidelity/\"\n").toStdString().c_str()); - script.write((QString("cp -rf \"$VOLUME/") + folderName + "/Sandbox.app\" \"" + _workingFolder + "/High_Fidelity/\"\n").toStdString().c_str()); - - script.write("hdiutil detach \"$VOLUME\"\n"); - script.write("killall yes\n"); - script.close(); - script.setPermissions(QFile::ReadOwner | QFile::WriteOwner | QFile::ExeOwner); - commandLine = "yes | " + _workingFolder + "/install_app.sh " + installerFullPath; -#endif - appendLog(commandLine); - _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"; -#elif defined Q_OS_MAC - dataDirectory = QDir::homePath() + "/Library/Application Support"; -#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 (QDir(_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); - 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); - } -#elif defined Q_OS_MAC - QString commandLine; - - commandLine = QString("killall interface") + "; " + _workingFolder +"/waitForFinish.sh interface"; - system(commandLine.toStdString().c_str()); - - commandLine = QString("killall Sandbox") + "; " + _workingFolder +"/waitForFinish.sh Sandbox"; - system(commandLine.toStdString().c_str()); - - commandLine = QString("killall Console") + "; " + _workingFolder +"/waitForFinish.sh Console"; - system(commandLine.toStdString().c_str()); -#endif -} - -void TestRunner::startLocalServerProcesses() { - QString commandLine; - -#ifdef Q_OS_WIN - 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()); - -#elif defined Q_OS_MAC - commandLine = "open \"" +_installationFolder + "/Sandbox.app\""; - system(commandLine.toStdString().c_str()); -#endif - - // Give server processes time to stabilize - QThread::sleep(20); -} - -void TestRunner::runInterfaceWithTestScript() { - QString url = QString("hifi://localhost"); - if (_runServerless->isChecked()) { - // Move to an empty area - url = "file:///~serverless/tutorial.json"; - } else { - url = "hifi://localhost"; - } - - QString deleteScript = - QString("https://raw.githubusercontent.com/") + _user + "/hifi_tests/" + _branch + "/tests/utils/deleteNearbyEntities.js"; - - QString testScript = - QString("https://raw.githubusercontent.com/") + _user + "/hifi_tests/" + _branch + "/tests/testRecursive.js"; - - QString commandLine; -#ifdef Q_OS_WIN - QString exeFile; - // First, run script to delete any entities in test area - // Note that this will run to completion before continuing - exeFile = QString("\"") + QDir::toNativeSeparators(_installationFolder) + "\\interface.exe\""; - commandLine = "start /wait \"\" " + exeFile + - " --url " + url + - " --no-updater" + - " --no-login-suggestion" - " --testScript " + deleteScript + " quitWhenFinished"; - - system(commandLine.toStdString().c_str()); - - // Now run the test suite - exeFile = QString("\"") + QDir::toNativeSeparators(_installationFolder) + "\\interface.exe\""; - commandLine = exeFile + - " --url " + url + - " --no-updater" + - " --no-login-suggestion" - " --testScript " + testScript + " quitWhenFinished" + - " --testResultsLocation " + _snapshotFolder; - - _interfaceWorker->setCommandLine(commandLine); - emit startInterface(); -#elif defined Q_OS_MAC - QFile script; - script.setFileName(_workingFolder + "/runInterfaceTests.sh"); - if (!script.open(QIODevice::WriteOnly | QIODevice::Text)) { - QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), - "Could not open 'runInterfaceTests.sh'"); - exit(-1); - } - - script.write("#!/bin/sh\n\n"); - - // First, run script to delete any entities in test area - commandLine = - "open -W \"" +_installationFolder + "/interface.app\" --args" + - " --url " + url + - " --no-updater" + - " --no-login-suggestion" - " --testScript " + deleteScript + " quitWhenFinished\n"; - - script.write(commandLine.toStdString().c_str()); - - // On The Mac, we need to resize Interface. The Interface window opens a few seconds after the process - // has started. - // Before starting interface, start a process that will resize interface 10s after it opens - commandLine = _workingFolder +"/waitForStart.sh interface && sleep 10 && " + _workingFolder +"/setInterfaceSizeAndPosition.sh &\n"; - script.write(commandLine.toStdString().c_str()); - - commandLine = - "open \"" +_installationFolder + "/interface.app\" --args" + - " --url " + url + - " --no-updater" + - " --no-login-suggestion" - " --testScript " + testScript + " quitWhenFinished" + - " --testResultsLocation " + _snapshotFolder + - " && " + _workingFolder +"/waitForFinish.sh interface\n"; - - script.write(commandLine.toStdString().c_str()); - - script.close(); - script.setPermissions(QFile::ReadOwner | QFile::WriteOwner | QFile::ExeOwner); - - commandLine = _workingFolder + "/runInterfaceTests.sh"; - _interfaceWorker->setCommandLine(commandLine); - - emit startInterface(); -#endif - - // Helpful for debugging - appendLog(commandLine); -} - -void TestRunner::interfaceExecutionComplete() { - 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\nExisting images will be evaluated"); - } - - evaluateResults(); - - killProcesses(); - - // The High Fidelity AppData folder will be restored after evaluation has completed -} - -void TestRunner::evaluateResults() { - updateStatusLabel("Evaluating results"); - nitpick->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) { - nitpick->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(); - - nitpick->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 - // On Mac, replace `exe` with `dmg` - 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) { -#ifdef Q_OS_WIN - 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`"; -#elif defined Q_OS_MAC - throw "URL not in expected format, should look like `https://deployment.highfidelity.com/jobs/pr-build/label%3Dwindows/13023/HighFidelity-Beta-Interface-PR14006-be76c43.dmg`"; -#endif - } - 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); - } + nitpick->downloadFiles(urls, _workingFolder, filenames, caller); } void TestRunner::parseBuildInformation() { @@ -802,15 +126,48 @@ void TestRunner::parseBuildInformation() { } _buildInformation.url = element.text(); - } catch (QString errorMessage) { + } + catch (QString errorMessage) { QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), errorMessage); exit(-1); - } catch (...) { + } + catch (...) { QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), "unknown error"); exit(-1); } } +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 + // On Mac, replace `exe` with `dmg` + 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); + } +} + +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(); + + nitpick->appendLogWindow(message); +} + void Worker::setCommandLine(const QString& commandLine) { _commandLine = commandLine; } diff --git a/tools/nitpick/src/TestRunner.h b/tools/nitpick/src/TestRunner.h index 00f0f66ecf..d2468ec2fa 100644 --- a/tools/nitpick/src/TestRunner.h +++ b/tools/nitpick/src/TestRunner.h @@ -1,7 +1,7 @@ // // TestRunner.h // -// Created by Nissim Hadar on 1 Sept 2018. +// Created by Nissim Hadar on 23 Jan 2019. // Copyright 2013 High Fidelity, Inc. // // Distributed under the Apache License, Version 2.0. @@ -16,10 +16,9 @@ #include #include #include -#include -#include #include -#include + +class Worker; class BuildInformation { public: @@ -27,67 +26,28 @@ public: QString url; }; -class Worker; - -class TestRunner : public QObject { - Q_OBJECT +class TestRunner { 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); + void setWorkingFolder(QLabel* workingFolderLabel); + void downloadBuildXml(void* caller); + void parseBuildInformation(); + QString getInstallerNameFromURL(const QString& url); - ~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); +protected: + QLabel* _workingFolderLabel; + QLabel* _statusLabel; + QLineEdit* _url; + QCheckBox* _runLatest; - void parseBuildInformation(); + QString _workingFolder; -private slots: - void checkTime(); - void installationComplete(); - void interfaceExecutionComplete(); + const QString DEV_BUILD_XML_URL{ "https://highfidelity.com/dev-builds.xml" }; + const QString DEV_BUILD_XML_FILENAME{ "dev-builds.xml" }; -signals: - void startInstaller(); - void startInterface(); - void startResize(); - -private: - bool _automatedTestIsRunning{ false }; + bool buildXMLDownloaded; + BuildInformation _buildInformation; #ifdef Q_OS_WIN const QString INSTALLER_FILENAME_LATEST{ "HighFidelity-Beta-latest-dev.exe" }; @@ -97,47 +57,10 @@ private: const QString INSTALLER_FILENAME_LATEST{ "" }; #endif - 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; +private: + QFile _logFile; }; class Worker : public QObject { @@ -150,10 +73,9 @@ public slots: signals: void commandComplete(); - void startInstaller(); - void startInterface(); - + private: QString _commandLine; }; -#endif // hifi_testRunner_h + +#endif diff --git a/tools/nitpick/src/TestRunnerDesktop.cpp b/tools/nitpick/src/TestRunnerDesktop.cpp new file mode 100644 index 0000000000..50cb6f9a07 --- /dev/null +++ b/tools/nitpick/src/TestRunnerDesktop.cpp @@ -0,0 +1,681 @@ +// +// TestRunnerDesktop.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 "TestRunnerDesktop.h" + +#include +#include +#include + +#ifdef Q_OS_WIN +#include +#include +#endif + +#include "Nitpick.h" +extern Nitpick* nitpick; + +TestRunnerDesktop::TestRunnerDesktop( + std::vector dayCheckboxes, + std::vector timeEditCheckboxes, + std::vector timeEdits, + QLabel* workingFolderLabel, + QCheckBox* runServerless, + QCheckBox* runLatest, + QLineEdit* url, + QPushButton* runNow, + QLabel* statusLabel, + + QObject* parent +) : QObject(parent) +{ + _dayCheckboxes = dayCheckboxes; + _timeEditCheckboxes = timeEditCheckboxes; + _timeEdits = timeEdits; + _workingFolderLabel = workingFolderLabel; + _runServerless = runServerless; + _runLatest = runLatest; + _url = url; + _runNow = runNow; + _statusLabel = statusLabel; + + _installerThread = new QThread(); + _installerWorker = new InstallerWorker(); + + _installerWorker->moveToThread(_installerThread); + _installerThread->start(); + connect(this, SIGNAL(startInstaller()), _installerWorker, SLOT(runCommand())); + connect(_installerWorker, SIGNAL(commandComplete()), this, SLOT(installationComplete())); + + _interfaceThread = new QThread(); + _interfaceWorker = new InterfaceWorker(); + + _interfaceThread->start(); + _interfaceWorker->moveToThread(_interfaceThread); + connect(this, SIGNAL(startInterface()), _interfaceWorker, SLOT(runCommand())); + connect(_interfaceWorker, SIGNAL(commandComplete()), this, SLOT(interfaceExecutionComplete())); +} + +TestRunnerDesktop::~TestRunnerDesktop() { + delete _installerThread; + delete _installerWorker; + + delete _interfaceThread; + delete _interfaceWorker; + + if (_timer) { + delete _timer; + } +} + +void TestRunnerDesktop::setWorkingFolderAndEnableControls() { + setWorkingFolder(_workingFolderLabel); + +#ifdef Q_OS_WIN + _installationFolder = _workingFolder + "/High Fidelity"; +#elif defined Q_OS_MAC + _installationFolder = _workingFolder + "/High_Fidelity"; +#endif + + nitpick->enableRunTabControls(); + + _timer = new QTimer(this); + connect(_timer, SIGNAL(timeout()), this, SLOT(checkTime())); + _timer->start(30 * 1000); //time specified in ms + +#ifdef Q_OS_MAC + // Create MAC shell scripts + QFile script; + + // This script waits for a process to start + script.setFileName(_workingFolder + "/waitForStart.sh"); + if (!script.open(QIODevice::WriteOnly | QIODevice::Text)) { + QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), + "Could not open 'waitForStart.sh'"); + exit(-1); + } + + script.write("#!/bin/sh\n\n"); + script.write("PROCESS=\"$1\"\n"); + script.write("until (pgrep -x $PROCESS >nul)\n"); + script.write("do\n"); + script.write("\techo waiting for \"$1\" to start\n"); + script.write("\tsleep 2\n"); + script.write("done\n"); + script.write("echo \"$1\" \"started\"\n"); + script.close(); + script.setPermissions(QFile::ReadOwner | QFile::WriteOwner | QFile::ExeOwner); + + // The Mac shell command returns immediately. This little script waits for a process to finish + script.setFileName(_workingFolder + "/waitForFinish.sh"); + if (!script.open(QIODevice::WriteOnly | QIODevice::Text)) { + QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), + "Could not open 'waitForFinish.sh'"); + exit(-1); + } + + script.write("#!/bin/sh\n\n"); + script.write("PROCESS=\"$1\"\n"); + script.write("while (pgrep -x $PROCESS >nul)\n"); + script.write("do\n"); + script.write("\techo waiting for \"$1\" to finish\n"); + script.write("\tsleep 2\n"); + script.write("done\n"); + script.write("echo \"$1\" \"finished\"\n"); + script.close(); + script.setPermissions(QFile::ReadOwner | QFile::WriteOwner | QFile::ExeOwner); + + // Create an AppleScript to resize Interface. This is needed so that snapshots taken + // with the primary camera will be the correct size. + // This will be run from a normal shell script + script.setFileName(_workingFolder + "/setInterfaceSizeAndPosition.scpt"); + if (!script.open(QIODevice::WriteOnly | QIODevice::Text)) { + QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), + "Could not open 'setInterfaceSizeAndPosition.scpt'"); + exit(-1); + } + + script.write("set width to 960\n"); + script.write("set height to 540\n"); + script.write("set x to 100\n"); + script.write("set y to 100\n\n"); + script.write("tell application \"System Events\" to tell application process \"interface\" to tell window 1 to set {size, position} to {{width, height}, {x, y}}\n"); + + script.close(); + script.setPermissions(QFile::ReadOwner | QFile::WriteOwner | QFile::ExeOwner); + + script.setFileName(_workingFolder + "/setInterfaceSizeAndPosition.sh"); + if (!script.open(QIODevice::WriteOnly | QIODevice::Text)) { + QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), + "Could not open 'setInterfaceSizeAndPosition.sh'"); + exit(-1); + } + + script.write("#!/bin/sh\n\n"); + script.write("echo resizing interface\n"); + script.write(("osascript " + _workingFolder + "/setInterfaceSizeAndPosition.scpt\n").toStdString().c_str()); + script.write("echo resize complete\n"); + script.close(); + script.setPermissions(QFile::ReadOwner | QFile::WriteOwner | QFile::ExeOwner); +#endif +} + +void TestRunnerDesktop::run() { + _runNow->setEnabled(false); + + _testStartDateTime = QDateTime::currentDateTime(); + _automatedTestIsRunning = true; + + // Initial setup + _branch = nitpick->getSelectedBranch(); + _user = nitpick->getSelectedUser(); + + // This will be restored at the end of the tests + saveExistingHighFidelityAppDataFolder(); + + _statusLabel->setText("Downloading Build XML"); + downloadBuildXml((void*)this); + + // `downloadComplete` will run after download has completed +} + +void TestRunnerDesktop::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; + } + + _statusLabel->setText("Downloading installer"); + + nitpick->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")); + + _statusLabel->setText("Installing"); + + // Kill any existing processes that would interfere with installation + killProcesses(); + + runInstaller(); + } +} + +void TestRunnerDesktop::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; +#ifdef Q_OS_WIN + commandLine = "\"" + QDir::toNativeSeparators(installerFullPath) + "\"" + " /S /D=" + QDir::toNativeSeparators(_installationFolder); +#elif defined Q_OS_MAC + // Create installation shell script + QFile script; + script.setFileName(_workingFolder + "/install_app.sh"); + if (!script.open(QIODevice::WriteOnly | QIODevice::Text)) { + QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), + "Could not open 'install_app.sh'"); + exit(-1); + } + + if (!QDir().exists(_installationFolder)) { + QDir().mkdir(_installationFolder); + } + + // This script installs High Fidelity. It is run as "yes | install_app.sh... so "yes" is killed at the end + script.write("#!/bin/sh\n\n"); + script.write("VOLUME=`hdiutil attach \"$1\" | grep Volumes | awk '{print $3}'`\n"); + + QString folderName {"High Fidelity"}; + if (!_runLatest->isChecked()) { + folderName += QString(" - ") + getPRNumberFromURL(_url->text()); + } + + script.write((QString("cp -rf \"$VOLUME/") + folderName + "/interface.app\" \"" + _workingFolder + "/High_Fidelity/\"\n").toStdString().c_str()); + script.write((QString("cp -rf \"$VOLUME/") + folderName + "/Sandbox.app\" \"" + _workingFolder + "/High_Fidelity/\"\n").toStdString().c_str()); + + script.write("hdiutil detach \"$VOLUME\"\n"); + script.write("killall yes\n"); + script.close(); + script.setPermissions(QFile::ReadOwner | QFile::WriteOwner | QFile::ExeOwner); + commandLine = "yes | " + _workingFolder + "/install_app.sh " + installerFullPath; +#endif + appendLog(commandLine); + _installerWorker->setCommandLine(commandLine); + emit startInstaller(); +} + +void TestRunnerDesktop::installationComplete() { + verifyInstallationSucceeded(); + + createSnapshotFolder(); + + _statusLabel->setText("Running tests"); + + if (!_runServerless->isChecked()) { + startLocalServerProcesses(); + } + + runInterfaceWithTestScript(); +} + +void TestRunnerDesktop::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 TestRunnerDesktop::saveExistingHighFidelityAppDataFolder() { + QString dataDirectory{ "NOT FOUND" }; +#ifdef Q_OS_WIN + dataDirectory = qgetenv("USERPROFILE") + "\\AppData\\Roaming"; +#elif defined Q_OS_MAC + dataDirectory = QDir::homePath() + "/Library/Application Support"; +#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 (QDir(_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) + QDir canonicalAppDataFolder; +#ifdef Q_OS_WIN + canonicalAppDataFolder = QDir::currentPath() + "/AppDataHighFidelity"; +#elif defined Q_OS_MAC + canonicalAppDataFolder = QCoreApplication::applicationDirPath() + "/AppDataHighFidelity"; +#endif + if (canonicalAppDataFolder.exists()) { + copyFolder(canonicalAppDataFolder.path(), _appDataFolder.path()); + } else { + QMessageBox::critical(0, "Internal error", "The nitpick AppData folder cannot be found at:\n" + canonicalAppDataFolder.path()); + exit(-1); + } +} + +void TestRunnerDesktop::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); + 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 TestRunnerDesktop::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); + } +#elif defined Q_OS_MAC + QString commandLine; + + commandLine = QString("killall interface") + "; " + _workingFolder +"/waitForFinish.sh interface"; + system(commandLine.toStdString().c_str()); + + commandLine = QString("killall Sandbox") + "; " + _workingFolder +"/waitForFinish.sh Sandbox"; + system(commandLine.toStdString().c_str()); + + commandLine = QString("killall Console") + "; " + _workingFolder +"/waitForFinish.sh Console"; + system(commandLine.toStdString().c_str()); +#endif +} + +void TestRunnerDesktop::startLocalServerProcesses() { + QString commandLine; + +#ifdef Q_OS_WIN + 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()); + +#elif defined Q_OS_MAC + commandLine = "open \"" +_installationFolder + "/Sandbox.app\""; + system(commandLine.toStdString().c_str()); +#endif + + // Give server processes time to stabilize + QThread::sleep(20); +} + +void TestRunnerDesktop::runInterfaceWithTestScript() { + QString url = QString("hifi://localhost"); + if (_runServerless->isChecked()) { + // Move to an empty area + url = "file:///~serverless/tutorial.json"; + } else { + url = "hifi://localhost"; + } + + QString deleteScript = + QString("https://raw.githubusercontent.com/") + _user + "/hifi_tests/" + _branch + "/tests/utils/deleteNearbyEntities.js"; + + QString testScript = + QString("https://raw.githubusercontent.com/") + _user + "/hifi_tests/" + _branch + "/tests/testRecursive.js"; + + QString commandLine; +#ifdef Q_OS_WIN + QString exeFile; + // First, run script to delete any entities in test area + // Note that this will run to completion before continuing + exeFile = QString("\"") + QDir::toNativeSeparators(_installationFolder) + "\\interface.exe\""; + commandLine = "start /wait \"\" " + exeFile + + " --url " + url + + " --no-updater" + + " --no-login-suggestion" + " --testScript " + deleteScript + " quitWhenFinished"; + + system(commandLine.toStdString().c_str()); + + // Now run the test suite + exeFile = QString("\"") + QDir::toNativeSeparators(_installationFolder) + "\\interface.exe\""; + commandLine = exeFile + + " --url " + url + + " --no-updater" + + " --no-login-suggestion" + " --testScript " + testScript + " quitWhenFinished" + + " --testResultsLocation " + _snapshotFolder; + + _interfaceWorker->setCommandLine(commandLine); + emit startInterface(); +#elif defined Q_OS_MAC + QFile script; + script.setFileName(_workingFolder + "/runInterfaceTests.sh"); + if (!script.open(QIODevice::WriteOnly | QIODevice::Text)) { + QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), + "Could not open 'runInterfaceTests.sh'"); + exit(-1); + } + + script.write("#!/bin/sh\n\n"); + + // First, run script to delete any entities in test area + commandLine = + "open -W \"" +_installationFolder + "/interface.app\" --args" + + " --url " + url + + " --no-updater" + + " --no-login-suggestion" + " --testScript " + deleteScript + " quitWhenFinished\n"; + + script.write(commandLine.toStdString().c_str()); + + // On The Mac, we need to resize Interface. The Interface window opens a few seconds after the process + // has started. + // Before starting interface, start a process that will resize interface 10s after it opens + commandLine = _workingFolder +"/waitForStart.sh interface && sleep 10 && " + _workingFolder +"/setInterfaceSizeAndPosition.sh &\n"; + script.write(commandLine.toStdString().c_str()); + + commandLine = + "open \"" +_installationFolder + "/interface.app\" --args" + + " --url " + url + + " --no-updater" + + " --no-login-suggestion" + " --testScript " + testScript + " quitWhenFinished" + + " --testResultsLocation " + _snapshotFolder + + " && " + _workingFolder +"/waitForFinish.sh interface\n"; + + script.write(commandLine.toStdString().c_str()); + + script.close(); + script.setPermissions(QFile::ReadOwner | QFile::WriteOwner | QFile::ExeOwner); + + commandLine = _workingFolder + "/runInterfaceTests.sh"; + _interfaceWorker->setCommandLine(commandLine); + + emit startInterface(); +#endif + + // Helpful for debugging + appendLog(commandLine); +} + +void TestRunnerDesktop::interfaceExecutionComplete() { + 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\nExisting images will be evaluated"); + } + + evaluateResults(); + + killProcesses(); + + // The High Fidelity AppData folder will be restored after evaluation has completed +} + +void TestRunnerDesktop::evaluateResults() { + _statusLabel->setText("Evaluating results"); + nitpick->startTestsEvaluation(false, true, _snapshotFolder, _branch, _user); +} + +void TestRunnerDesktop::automaticTestRunEvaluationComplete(QString zippedFolder, int numberOfFailures) { + addBuildNumberToResults(zippedFolder); + restoreHighFidelityAppDataFolder(); + + _statusLabel->setText("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 TestRunnerDesktop::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 TestRunnerDesktop::restoreHighFidelityAppDataFolder() { + _appDataFolder.removeRecursively(); + + if (_savedAppDataFolder != QDir()) { + _appDataFolder.rename(_savedAppDataFolder.path(), _appDataFolder.path()); + } +} + +// Copies a folder recursively +void TestRunnerDesktop::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 TestRunnerDesktop::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(); + } +} + +QString TestRunnerDesktop::getPRNumberFromURL(const QString& url) { + try { + QStringList urlParts = url.split("/"); + QStringList filenameParts = urlParts[urlParts.size() - 1].split("-"); + if (filenameParts.size() <= 3) { +#ifdef Q_OS_WIN + 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`"; +#elif defined Q_OS_MAC + throw "URL not in expected format, should look like `https://deployment.highfidelity.com/jobs/pr-build/label%3Dwindows/13023/HighFidelity-Beta-Interface-PR14006-be76c43.dmg`"; +#endif + } + 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); + } +} diff --git a/tools/nitpick/src/TestRunnerDesktop.h b/tools/nitpick/src/TestRunnerDesktop.h new file mode 100644 index 0000000000..a8f828b9d4 --- /dev/null +++ b/tools/nitpick/src/TestRunnerDesktop.h @@ -0,0 +1,124 @@ +// +// TestRunnerDesktop.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_testRunnerDesktop_h +#define hifi_testRunnerDesktop_h + +#include +#include +#include +#include +#include +#include + +#include "TestRunner.h" + +class InterfaceWorker; +class InstallerWorker; + +class TestRunnerDesktop : public QObject, public TestRunner { + Q_OBJECT +public: + explicit TestRunnerDesktop( + std::vector dayCheckboxes, + std::vector timeEditCheckboxes, + std::vector timeEdits, + QLabel* workingFolderLabel, + QCheckBox* runServerless, + QCheckBox* runLatest, + QLineEdit* url, + QPushButton* runNow, + QLabel* statusLabel, + + QObject* parent = 0 + ); + + ~TestRunnerDesktop(); + + void setWorkingFolderAndEnableControls(); + + 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); + + QString getPRNumberFromURL(const QString& url); + +private slots: + void checkTime(); + void installationComplete(); + void interfaceExecutionComplete(); + +signals: + void startInstaller(); + void startInterface(); + void startResize(); + +private: + bool _automatedTestIsRunning{ false }; + + QString _installerURL; + QString _installerFilename; + + QDir _appDataFolder; + QDir _savedAppDataFolder; + + 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; + QPushButton* _runNow; + QTimer* _timer; + QThread* _installerThread; + QThread* _interfaceThread; + + InstallerWorker* _installerWorker; + InterfaceWorker* _interfaceWorker; +}; + +class InstallerWorker : public Worker { + Q_OBJECT +signals: + void startInstaller(); +}; + +class InterfaceWorker : public Worker { + Q_OBJECT +signals: + void startInterface(); +}; +#endif diff --git a/tools/nitpick/src/TestRunnerMobile.cpp b/tools/nitpick/src/TestRunnerMobile.cpp new file mode 100644 index 0000000000..6bc34f26a8 --- /dev/null +++ b/tools/nitpick/src/TestRunnerMobile.cpp @@ -0,0 +1,172 @@ +// +// TestRunnerMobile.cpp +// +// Created by Nissim Hadar on 22 Jan 2019. +// 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 "TestRunnerMobile.h" + +#include +#include +#include + +#include "Nitpick.h" +extern Nitpick* nitpick; + +TestRunnerMobile::TestRunnerMobile( + QLabel* workingFolderLabel, + QPushButton *connectDeviceButton, + QPushButton *pullFolderButton, + QLabel* detectedDeviceLabel, + QLineEdit *folderLineEdit, + QPushButton* downloadAPKPushbutton, + QPushButton* installAPKPushbutton, + QCheckBox* runLatest, + QLineEdit* url, + QLabel* statusLabel, + + QObject* parent +) : QObject(parent) +{ + _workingFolderLabel = workingFolderLabel; + _connectDeviceButton = connectDeviceButton; + _pullFolderButton = pullFolderButton; + _detectedDeviceLabel = detectedDeviceLabel; + _folderLineEdit = folderLineEdit; + _downloadAPKPushbutton = downloadAPKPushbutton; + _installAPKPushbutton = installAPKPushbutton; + _runLatest = runLatest; + _url = url; + _statusLabel = statusLabel; + + folderLineEdit->setText("/sdcard/DCIM/TEST"); +} + +TestRunnerMobile::~TestRunnerMobile() { +} + +void TestRunnerMobile::setWorkingFolderAndEnableControls() { + setWorkingFolder(_workingFolderLabel); + + _connectDeviceButton->setEnabled(true); + + // Find ADB (Android Debugging Bridge) before continuing +#ifdef Q_OS_WIN + if (QProcessEnvironment::systemEnvironment().contains("ADB_PATH")) { + QString adbExePath = QProcessEnvironment::systemEnvironment().value("ADB_PATH") + "/platform-tools"; + if (!QFile::exists(adbExePath + "/" + _adbExe)) { + QMessageBox::critical(0, _adbExe, QString("Python executable not found in ") + adbExePath); + exit(-1); + } + + _adbCommand = adbExePath + "/" + _adbExe; + } else { + QMessageBox::critical(0, "PYTHON_PATH not defined", + "Please set PYTHON_PATH to directory containing the Python executable"); + exit(-1); + } +#elif defined Q_OS_MAC + _adbCommand = "/usr/local/bin/adb"; + if (!QFile::exists(_adbCommand)) { + QMessageBox::critical(0, "adb not found", + "python3 not found at " + _adbCommand); + exit(-1); + } +#endif +} + +void TestRunnerMobile::connectDevice() { +#if defined Q_OS_WIN || defined Q_OS_MAC + QString devicesFullFilename{ _workingFolder + "/devices.txt" }; + QString command = _adbCommand + " devices > " + devicesFullFilename; + system(command.toStdString().c_str()); + + if (!QFile::exists(devicesFullFilename)) { + QMessageBox::critical(0, "Internal error", "devicesFullFilename not found"); + exit (-1); + } + + // Device should be in second line + QFile devicesFile(devicesFullFilename); + devicesFile.open(QIODevice::ReadOnly | QIODevice::Text); + QString line1 = devicesFile.readLine(); + QString line2 = devicesFile.readLine(); + + const QString DEVICE{ "device" }; + if (line2.contains(DEVICE)) { + // Make sure only 1 device + QString line3 = devicesFile.readLine(); + if (line3.contains(DEVICE)) { + QMessageBox::critical(0, "Too many devices detected", "Tests will run only if a single device is attached"); + + } else { + _detectedDeviceLabel->setText(line2.remove(DEVICE)); + _pullFolderButton->setEnabled(true); + _folderLineEdit->setEnabled(true); + _downloadAPKPushbutton->setEnabled(true); + } + } +#endif +} + +void TestRunnerMobile::downloadAPK() { + downloadBuildXml((void*)this); +} + + +void TestRunnerMobile::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; + + + // Replace the `exe` extension with `apk` + _installerFilename = _installerFilename.replace(_installerFilename.length() - 3, 3, "apk"); + _buildInformation.url = _buildInformation.url.replace(_buildInformation.url.length() - 3, 3, "apk"); + + urls << _buildInformation.url; + filenames << _installerFilename; + } else { + QString urlText = _url->text(); + urls << urlText; + _installerFilename = getInstallerNameFromURL(urlText); + filenames << _installerFilename; + } + + _statusLabel->setText("Downloading installer"); + + nitpick->downloadFiles(urls, _workingFolder, filenames, (void*)this); + } else { + _statusLabel->setText("Installer download complete"); + _installAPKPushbutton->setEnabled(true); + } +} + +void TestRunnerMobile::installAPK() { +#if defined Q_OS_WIN || defined Q_OS_MAC + _statusLabel->setText("Installing"); + QString command = _adbCommand + " install -r -d " + _workingFolder + "/" + _installerFilename + " >" + _workingFolder + "/installOutput.txt"; + system(command.toStdString().c_str()); + _statusLabel->setText("Installation complete"); +#endif +} + +void TestRunnerMobile::pullFolder() { +#if defined Q_OS_WIN || defined Q_OS_MAC + _statusLabel->setText("Pulling folder"); + QString command = _adbCommand + " pull " + _folderLineEdit->text() + " " + _workingFolder + _installerFilename; + system(command.toStdString().c_str()); + _statusLabel->setText("Pull complete"); +#endif +} diff --git a/tools/nitpick/src/TestRunnerMobile.h b/tools/nitpick/src/TestRunnerMobile.h new file mode 100644 index 0000000000..4cf31f6bd4 --- /dev/null +++ b/tools/nitpick/src/TestRunnerMobile.h @@ -0,0 +1,69 @@ +// +// TestRunnerMobile.h +// +// Created by Nissim Hadar on 22 Jan 2019. +// 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_testRunnerMobile_h +#define hifi_testRunnerMobile_h + +#include +#include +#include + +#include "TestRunner.h" + +class TestRunnerMobile : public QObject, public TestRunner { + Q_OBJECT +public: + explicit TestRunnerMobile( + QLabel* workingFolderLabel, + QPushButton *connectDeviceButton, + QPushButton *pullFolderButton, + QLabel* detectedDeviceLabel, + QLineEdit *folderLineEdit, + QPushButton* downloadAPKPushbutton, + QPushButton* installAPKPushbutton, + QCheckBox* runLatest, + QLineEdit* url, + QLabel* statusLabel, + + QObject* parent = 0 + ); + ~TestRunnerMobile(); + + void setWorkingFolderAndEnableControls(); + void connectDevice(); + + void downloadComplete(); + void downloadAPK(); + + void installAPK(); + + void pullFolder(); + +private: + QPushButton* _connectDeviceButton; + QPushButton* _pullFolderButton; + QLabel* _detectedDeviceLabel; + QLineEdit* _folderLineEdit; + QPushButton* _downloadAPKPushbutton; + QPushButton* _installAPKPushbutton; + +#ifdef Q_OS_WIN + const QString _adbExe{ "adb.exe" }; +#else + // Both Mac and Linux use "adb" + const QString _adbExe{ "adb" }; +#endif + + QString _installerFilename; + + QString _adbCommand; + +}; +#endif diff --git a/tools/nitpick/ui/Nitpick.ui b/tools/nitpick/ui/Nitpick.ui index 78f7dcf2bf..8d69317369 100644 --- a/tools/nitpick/ui/Nitpick.ui +++ b/tools/nitpick/ui/Nitpick.ui @@ -20,7 +20,7 @@ Nitpick - + 470 @@ -49,10 +49,10 @@ Create - + - 195 + 210 60 220 40 @@ -62,7 +62,7 @@ Create Tests - + 70 @@ -75,7 +75,7 @@ Create MD file - + 320 @@ -88,10 +88,10 @@ Create all MD files - + - 195 + 210 120 220 40 @@ -101,7 +101,7 @@ Create Tests Outline - + 70 @@ -114,7 +114,7 @@ Create Recursive Script - + 320 @@ -127,7 +127,7 @@ Create all Recursive Scripts - + 70 @@ -140,7 +140,7 @@ Create testAuto script - + 320 @@ -158,7 +158,7 @@ Windows - + 200 @@ -171,7 +171,7 @@ Hide Windows Taskbar - + 200 @@ -187,9 +187,9 @@ - Run + Test on Desktop - + false @@ -420,26 +420,26 @@ - + 10 20 - 161 - 28 + 160 + 30 Set Working Folder - + 190 20 - 321 - 31 + 320 + 30 @@ -469,7 +469,7 @@ Status: - + 350 @@ -501,7 +501,7 @@ false - + 20 @@ -533,7 +533,7 @@ URL - + false @@ -547,6 +547,185 @@ + + + Test on Mobile + + + + false + + + + 10 + 90 + 160 + 30 + + + + Connect Device + + + + + + 190 + 96 + 320 + 30 + + + + (not detected) + + + + + + 10 + 20 + 160 + 30 + + + + Set Working Folder + + + + + + 190 + 20 + 320 + 30 + + + + (not set...) + + + + + false + + + + 460 + 350 + 160 + 30 + + + + Pull folder + + + + + false + + + + 10 + 350 + 440 + 30 + + + + + + false + + + + 170 + 170 + 451 + 21 + + + + + + + 20 + 170 + 120 + 20 + + + + <html><head/><body><p>If unchecked, will not show results during evaluation</p></body></html> + + + Run Latest + + + true + + + + + false + + + + 10 + 210 + 160 + 30 + + + + Download APK + + + + + + 290 + 20 + 41 + 31 + + + + Status: + + + + + + 340 + 20 + 271 + 31 + + + + ####### + + + + + false + + + + 10 + 250 + 160 + 30 + + + + Install APK + + + Evaluate @@ -567,7 +746,7 @@ Interactive Mode - + 330 @@ -585,7 +764,7 @@ Web Interface - + 240 @@ -614,7 +793,7 @@ true - + 240 @@ -627,7 +806,7 @@ Create Run - + 240 @@ -678,7 +857,7 @@ Amazon Web Services - + true @@ -719,10 +898,10 @@ groupBox - updateTestRailRunResultsButton + updateTestRailRunResultsPushbutton createPythonScriptRadioButton - createTestRailRunButton - createTestRailTestCasesButton + createTestRailRunPushbutton + createTestRailTestCasesPushbutton createXMLScriptRadioButton groupBox_2 @@ -851,17 +1030,17 @@ userLineEdit branchLineEdit - createTestsButton - createMDFileButton - createAllMDFilesButton - createTestsOutlineButton - createRecursiveScriptButton - createAllRecursiveScriptsButton - createTestAutoScriptButton - createAllTestAutoScriptsButton - hideTaskbarButton - showTaskbarButton - runNowButton + createTestsPushbutton + createMDFilePushbutton + createAllMDFilesPushbutton + createTestsOutlinePushbutton + createRecursiveScriptPushbutton + createAllRecursiveScriptsPushbutton + createTestAutoScriptPushbutton + createAllTestAutoScriptsPushbutton + hideTaskbarPushbutton + showTaskbarPushbutton + runNowPushbutton sundayCheckBox wednesdayCheckBox tuesdayCheckBox @@ -877,22 +1056,22 @@ timeEdit2checkBox timeEdit3checkBox timeEdit4checkBox - setWorkingFolderButton + setWorkingFolderRunOnDesktopPushbutton plainTextEdit checkBoxServerless - checkBoxRunLatest - urlLineEdit + runLatestOnDesktopCheckBox + urlOnDesktopLineEdit checkBoxInteractiveMode - evaluateTestsButton - updateTestRailRunResultsButton + evaluateTestsPushbutton + updateTestRailRunResultsPushbutton createPythonScriptRadioButton - createTestRailRunButton - createTestRailTestCasesButton + createTestRailRunPushbutton + createTestRailTestCasesPushbutton createXMLScriptRadioButton - createWebPagePushButton + createWebPagePushbutton updateAWSCheckBox awsURLLineEdit - closeButton + closePushbutton tabWidget