From 0c89e67eb5bf2d82596d0c697831787cbbdf1e7f Mon Sep 17 00:00:00 2001 From: Callum Prentice Date: Fri, 20 Aug 2021 08:28:48 -0700 Subject: DRTVWR-534: Batch of modifications to 360 capture project after moving from internal repo to public one. --- .gitignore | 1 + autobuild.xml | 156 ++++ indra/cmake/CubemapToEquirectangularJS.cmake | 5 + indra/cmake/JPEGEncoderBasic.cmake | 5 + indra/cmake/ThreeJS.cmake | 8 + indra/llplugin/llpluginclassmedia.cpp | 22 + indra/llplugin/llpluginclassmedia.h | 4 + indra/media_plugins/cef/media_plugin_cef.cpp | 41 +- indra/newview/CMakeLists.txt | 17 +- indra/newview/app_settings/commands.xml | 11 + indra/newview/app_settings/settings.xml | 129 ++- indra/newview/llfloater360capture.cpp | 904 +++++++++++++++++++++ indra/newview/llfloater360capture.h | 97 +++ indra/newview/llfloatersnapshot.cpp | 14 + indra/newview/llfloatersnapshot.h | 1 + indra/newview/llviewerfloaterreg.cpp | 2 + indra/newview/llviewermedia.cpp | 36 +- indra/newview/llviewermedia.h | 1 + indra/newview/llviewermenu.cpp | 49 +- indra/newview/llviewermessage.cpp | 1 + indra/newview/llviewerobject.cpp | 1 + indra/newview/llviewerregion.cpp | 2 + indra/newview/llviewerwindow.cpp | 77 ++ indra/newview/llviewerwindow.h | 5 +- .../html/common/equirectangular/default.html | 22 + .../html/common/equirectangular/eqr_gen.html | 149 ++++ indra/newview/skins/default/textures/textures.xml | 3 +- .../default/textures/toolbar_icons/360_capture.png | Bin 0 -> 793 bytes .../skins/default/xui/en/floater_360capture.xml | 124 +++ .../skins/default/xui/en/floater_snapshot.xml | 22 +- .../skins/default/xui/en/floater_toybox.xml | 12 +- indra/newview/skins/default/xui/en/menu_viewer.xml | 17 + indra/newview/skins/default/xui/en/strings.xml | 4 + indra/newview/viewer_manifest.py | 18 +- 34 files changed, 1892 insertions(+), 68 deletions(-) create mode 100644 indra/cmake/CubemapToEquirectangularJS.cmake create mode 100644 indra/cmake/JPEGEncoderBasic.cmake create mode 100644 indra/cmake/ThreeJS.cmake create mode 100644 indra/newview/llfloater360capture.cpp create mode 100644 indra/newview/llfloater360capture.h create mode 100644 indra/newview/skins/default/html/common/equirectangular/default.html create mode 100644 indra/newview/skins/default/html/common/equirectangular/eqr_gen.html create mode 100644 indra/newview/skins/default/textures/toolbar_icons/360_capture.png create mode 100644 indra/newview/skins/default/xui/en/floater_360capture.xml diff --git a/.gitignore b/.gitignore index 4af34870cf..355c39de70 100755 --- a/.gitignore +++ b/.gitignore @@ -56,6 +56,7 @@ indra/newview/search_history.txt indra/newview/teleport_history.txt indra/newview/typed_locations.txt indra/newview/vivox-runtime +indra/newview/skins/default/html/common/equirectangular/js indra/server-linux-* indra/temp indra/test/linden_file.dat diff --git a/autobuild.xml b/autobuild.xml index 4768bd25c6..d44dc2ac7c 100644 --- a/autobuild.xml +++ b/autobuild.xml @@ -367,6 +367,58 @@ version 2.3.545362 + cubemaptoequirectangular + + copyright + Copyright (c) 2017 Jaume Sanchez Elias, http://www.clicktorelease.com + license + MIT + license_file + LICENSES/CUBEMAPTOEQUIRECTANGULAR_LICENSE.txt + name + cubemaptoequirectangular + platforms + + darwin64 + + archive + + hash + e6da43ea831b2416a5a8b388a511e70b + url + https://automated-builds-secondlife-com.s3.amazonaws.com/ct2/85446/792080/cubemaptoequirectangular-1.1.0-darwin64-562268.tar.bz2 + + name + darwin64 + + windows + + archive + + hash + f26636666a056cacea0618bc2648d935 + url + https://automated-builds-secondlife-com.s3.amazonaws.com/ct2/85451/792109/cubemaptoequirectangular-1.1.0-windows-562268.tar.bz2 + + name + windows + + windows64 + + archive + + hash + a59ba299bc94e915a8b55e9eef681253 + url + https://automated-builds-secondlife-com.s3.amazonaws.com/ct2/85449/792108/cubemaptoequirectangular-1.1.0-windows64-562268.tar.bz2 + + name + windows64 + + + version + 1.1.0 + curl copyright @@ -1491,6 +1543,58 @@ version 2012.1-2 + jpegencoderbasic + + copyright + Andreas Ritter, www.bytestrom.eu, 11/2009 + license + NONE + license_file + LICENSES/JPEG_ENCODER_BASIC_LICENSE.txt + name + jpegencoderbasic + platforms + + darwin64 + + archive + + hash + d07453bf10bae71013209874477640bb + url + https://automated-builds-secondlife-com.s3.amazonaws.com/ct2/85447/792085/jpegencoderbasic-1.0-darwin64-562269.tar.bz2 + + name + darwin64 + + windows + + archive + + hash + e422fee0c82507ec218597654f8cadbf + url + https://automated-builds-secondlife-com.s3.amazonaws.com/ct2/85450/792107/jpegencoderbasic-1.0-windows-562269.tar.bz2 + + name + windows + + windows64 + + archive + + hash + fbe07040cecc4e8760acc2cdf2436468 + url + https://automated-builds-secondlife-com.s3.amazonaws.com/ct2/85448/792106/jpegencoderbasic-1.0-windows64-562269.tar.bz2 + + name + windows64 + + + version + 1.0 + jpeglib copyright @@ -3067,6 +3171,58 @@ Copyright (c) 2012, 2014, 2015, 2016 nghttp2 contributors version 4.10.0000.32327.5fc3fe7c.539691 + threejs + + copyright + Copyright © 2010-2021 three.js authors + license + MIT + license_file + LICENSES/THREEJS_LICENSE.txt + name + threejs + platforms + + darwin64 + + archive + + hash + 313a852ddc87be24235319987a42f0f2 + url + https://automated-builds-secondlife-com.s3.amazonaws.com/ct2/86208/797009/threejs-0.131.3-darwin64-562835.tar.bz2 + + name + darwin64 + + windows + + archive + + hash + 850b2400f91e7704653fe3fd4171ce94 + url + https://automated-builds-secondlife-com.s3.amazonaws.com/ct2/86210/797031/threejs-0.131.3-windows-562835.tar.bz2 + + name + windows + + windows64 + + archive + + hash + 821f398169c1743987ac1b99376f767f + url + https://automated-builds-secondlife-com.s3.amazonaws.com/ct2/86209/797030/threejs-0.131.3-windows64-562835.tar.bz2 + + name + windows64 + + + version + 0.131.3 + tut copyright diff --git a/indra/cmake/CubemapToEquirectangularJS.cmake b/indra/cmake/CubemapToEquirectangularJS.cmake new file mode 100644 index 0000000000..bfe2926005 --- /dev/null +++ b/indra/cmake/CubemapToEquirectangularJS.cmake @@ -0,0 +1,5 @@ +# -*- cmake -*- +use_prebuilt_binary(cubemaptoequirectangular) + +# Main JS file +configure_file("${AUTOBUILD_INSTALL_DIR}/js/CubemapToEquirectangular.js" "${CMAKE_SOURCE_DIR}/newview/skins/default/html/common/equirectangular/js/CubemapToEquirectangular.js" COPYONLY) diff --git a/indra/cmake/JPEGEncoderBasic.cmake b/indra/cmake/JPEGEncoderBasic.cmake new file mode 100644 index 0000000000..0d2a3231bb --- /dev/null +++ b/indra/cmake/JPEGEncoderBasic.cmake @@ -0,0 +1,5 @@ +# -*- cmake -*- +use_prebuilt_binary(jpegencoderbasic) + +# Main JS file +configure_file("${AUTOBUILD_INSTALL_DIR}/js/jpeg_encoder_basic.js" "${CMAKE_SOURCE_DIR}/newview/skins/default/html/common/equirectangular/js/jpeg_encoder_basic.js" COPYONLY) diff --git a/indra/cmake/ThreeJS.cmake b/indra/cmake/ThreeJS.cmake new file mode 100644 index 0000000000..528adcbb25 --- /dev/null +++ b/indra/cmake/ThreeJS.cmake @@ -0,0 +1,8 @@ +# -*- cmake -*- +use_prebuilt_binary(threejs) + +# Main three.js file +configure_file("${AUTOBUILD_INSTALL_DIR}/js/three.min.js" "${CMAKE_SOURCE_DIR}/newview/skins/default/html/common/equirectangular/js/three.min.js" COPYONLY) + +# Controls to move around the scene using mouse or keyboard +configure_file("${AUTOBUILD_INSTALL_DIR}/js/OrbitControls.js" "${CMAKE_SOURCE_DIR}/newview/skins/default/html/common/equirectangular/js/OrbitControls.js" COPYONLY) diff --git a/indra/llplugin/llpluginclassmedia.cpp b/indra/llplugin/llpluginclassmedia.cpp index 6d51adc685..da352e8dd4 100644 --- a/indra/llplugin/llpluginclassmedia.cpp +++ b/indra/llplugin/llpluginclassmedia.cpp @@ -714,6 +714,15 @@ void LLPluginClassMedia::loadURI(const std::string &uri) sendMessage(message); } +void LLPluginClassMedia::executeJavaScript(const std::string &code) +{ + LLPluginMessage message(LLPLUGIN_MESSAGE_CLASS_MEDIA, "execute_javascript"); + + message.setValue("code", code); + + sendMessage(message); +} + const char* LLPluginClassMedia::priorityToString(EPriority priority) { const char* result = "UNKNOWN"; @@ -891,6 +900,19 @@ void LLPluginClassMedia::setJavascriptEnabled(const bool enabled) sendMessage(message); } +void LLPluginClassMedia::setWebSecurityDisabled(const bool disabled) +{ + LLPluginMessage message(LLPLUGIN_MESSAGE_CLASS_MEDIA_BROWSER, "web_security_disabled"); + message.setValueBoolean("disabled", disabled); + sendMessage(message); +} + +void LLPluginClassMedia::setFileAccessFromFileUrlsEnabled(const bool enabled) +{ + LLPluginMessage message(LLPLUGIN_MESSAGE_CLASS_MEDIA_BROWSER, "file_access_from_file_urls"); + message.setValueBoolean("enabled", enabled); + sendMessage(message); +} void LLPluginClassMedia::enableMediaPluginDebugging( bool enable ) { diff --git a/indra/llplugin/llpluginclassmedia.h b/indra/llplugin/llpluginclassmedia.h index 382f891e0c..69622c62db 100644 --- a/indra/llplugin/llpluginclassmedia.h +++ b/indra/llplugin/llpluginclassmedia.h @@ -139,6 +139,8 @@ public: void loadURI(const std::string &uri); + void executeJavaScript(const std::string &code); + // "Loading" means uninitialized or any state prior to fully running (processing commands) bool isPluginLoading(void) { return mPlugin?mPlugin->isLoading():false; }; @@ -199,6 +201,8 @@ public: void setLanguageCode(const std::string &language_code); void setPluginsEnabled(const bool enabled); void setJavascriptEnabled(const bool enabled); + void setWebSecurityDisabled(const bool disabled); + void setFileAccessFromFileUrlsEnabled(const bool enabled); void setTarget(const std::string &target); /////////////////////////////////// diff --git a/indra/media_plugins/cef/media_plugin_cef.cpp b/indra/media_plugins/cef/media_plugin_cef.cpp index 8465285d2b..2f2f25b612 100644 --- a/indra/media_plugins/cef/media_plugin_cef.cpp +++ b/indra/media_plugins/cef/media_plugin_cef.cpp @@ -65,7 +65,7 @@ private: void onTooltipCallback(std::string text); void onLoadStartCallback(); void onRequestExitCallback(); - void onLoadEndCallback(int httpStatusCode); + void onLoadEndCallback(int httpStatusCode, std::string url); void onLoadError(int status, const std::string error_text); void onAddressChangeCallback(std::string url); void onOpenPopupCallback(std::string url, std::string target); @@ -92,6 +92,8 @@ private: bool mDisableGPU; bool mDisableNetworkService; bool mUseMockKeyChain; + bool mDisableWebSecurity; + bool mFileAccessFromFileUrls; std::string mUserAgentSubtring; std::string mAuthUsername; std::string mAuthPassword; @@ -127,6 +129,8 @@ MediaPluginBase(host_send_func, host_user_data) mDisableGPU = false; mDisableNetworkService = true; mUseMockKeyChain = true; + mDisableWebSecurity = false; + mFileAccessFromFileUrls = false; mUserAgentSubtring = ""; mAuthUsername = ""; mAuthPassword = ""; @@ -260,13 +264,14 @@ void MediaPluginCEF::onRequestExitCallback() //////////////////////////////////////////////////////////////////////////////// // -void MediaPluginCEF::onLoadEndCallback(int httpStatusCode) +void MediaPluginCEF::onLoadEndCallback(int httpStatusCode, std::string url) { LLPluginMessage message(LLPLUGIN_MESSAGE_CLASS_MEDIA_BROWSER, "navigate_complete"); //message.setValue("uri", event.getEventUri()); // not easily available here in CEF - needed? message.setValueS32("result_code", httpStatusCode); message.setValueBoolean("history_back_available", mCEFLib->canGoBack()); message.setValueBoolean("history_forward_available", mCEFLib->canGoForward()); + message.setValue("uri", url); sendMessage(message); } @@ -355,14 +360,16 @@ const std::vector MediaPluginCEF::onFileDialog(dullahan::EFileDialo } else if (dialog_type == dullahan::FD_SAVE_FILE) { + mPickedFiles.clear(); mAuthOK = false; LLPluginMessage message(LLPLUGIN_MESSAGE_CLASS_MEDIA, "file_download"); + message.setValueBoolean("blocking_request", true); message.setValue("filename", default_file); sendMessage(message); - return std::vector(); + return mPickedFiles; } return std::vector(); @@ -523,7 +530,7 @@ void MediaPluginCEF::receiveMessage(const char* message_string) mCEFLib->setOnTitleChangeCallback(std::bind(&MediaPluginCEF::onTitleChangeCallback, this, std::placeholders::_1)); mCEFLib->setOnTooltipCallback(std::bind(&MediaPluginCEF::onTooltipCallback, this, std::placeholders::_1)); mCEFLib->setOnLoadStartCallback(std::bind(&MediaPluginCEF::onLoadStartCallback, this)); - mCEFLib->setOnLoadEndCallback(std::bind(&MediaPluginCEF::onLoadEndCallback, this, std::placeholders::_1)); + mCEFLib->setOnLoadEndCallback(std::bind(&MediaPluginCEF::onLoadEndCallback, this, std::placeholders::_1, std::placeholders::_2)); mCEFLib->setOnLoadErrorCallback(std::bind(&MediaPluginCEF::onLoadError, this, std::placeholders::_1, std::placeholders::_2)); mCEFLib->setOnAddressChangeCallback(std::bind(&MediaPluginCEF::onAddressChangeCallback, this, std::placeholders::_1)); mCEFLib->setOnOpenPopupCallback(std::bind(&MediaPluginCEF::onOpenPopupCallback, this, std::placeholders::_1, std::placeholders::_2)); @@ -562,6 +569,19 @@ void MediaPluginCEF::receiveMessage(const char* message_string) settings.disable_network_service = mDisableNetworkService; settings.use_mock_keychain = mUseMockKeyChain; #endif + // these were added to facilitate loading images directly into a local + // web page for the prototype 360 project in 2017 - something that is + // disallowed normally by the browser security model. Now the the source + // (cubemap) images are stores as JavaScript, we can avoid opening up + // this security hole (it was only set for the 360 floater but still + // a concern). Leaving them here, explicitly turn off vs removing + // entirely from this source file so that others are aware of them + // in the future. + settings.disable_web_security = false; + settings.file_access_from_file_urls = false; + + settings.flash_enabled = mPluginsEnabled; + // This setting applies to all plugins, not just Flash // Regarding, SL-15559 PDF files do not load in CEF v91, // it turns out that on Windows, PDF support is treated @@ -688,6 +708,11 @@ void MediaPluginCEF::receiveMessage(const char* message_string) std::string uri = message_in.getValue("uri"); mCEFLib->navigate(uri); } + else if (message_name == "execute_javascript") + { + std::string code = message_in.getValue("code"); + mCEFLib->executeJavaScript(code); + } else if (message_name == "set_cookie") { std::string uri = message_in.getValue("uri"); @@ -883,6 +908,14 @@ void MediaPluginCEF::receiveMessage(const char* message_string) { mDisableGPU = message_in.getValueBoolean("disable"); } + else if (message_name == "web_security_disabled") + { + mDisableWebSecurity = message_in.getValueBoolean("disabled"); + } + else if (message_name == "file_access_from_file_urls") + { + mFileAccessFromFileUrls = message_in.getValueBoolean("enabled"); + } } else if (message_class == LLPLUGIN_MESSAGE_CLASS_MEDIA_TIME) { diff --git a/indra/newview/CMakeLists.txt b/indra/newview/CMakeLists.txt index 87caca56af..08462d044b 100644 --- a/indra/newview/CMakeLists.txt +++ b/indra/newview/CMakeLists.txt @@ -12,12 +12,14 @@ include(bugsplat) include(BuildPackagesInfo) include(BuildVersion) include(CMakeCopyIfDifferent) +include(CubemapToEquirectangularJS) include(DBusGlib) include(DragDrop) include(EXPAT) include(FMODSTUDIO) include(GLOD) include(Hunspell) +include(JPEGEncoderBasic) include(JsonCpp) include(LLAppearance) include(LLAudio) @@ -47,6 +49,7 @@ include(OpenGL) include(OpenSSL) include(PNG) include(TemplateCheck) +include(ThreeJS) include(UI) include(UnixInstall) include(ViewerMiscLibs) @@ -205,6 +208,7 @@ set(viewer_SOURCE_FILES llfilteredwearablelist.cpp llfirstuse.cpp llflexibleobject.cpp + llfloater360capture.cpp llfloaterabout.cpp llfloaterbvhpreview.cpp llfloateraddpaymentmethod.cpp @@ -278,7 +282,7 @@ set(viewer_SOURCE_FILES llfloaternamedesc.cpp llfloaternotificationsconsole.cpp llfloaternotificationstabbed.cpp - llfloateroutfitphotopreview.cpp + llfloateroutfitphotopreview.cpp llfloateroutfitsnapshot.cpp llfloaterobjectweights.cpp llfloateropenobject.cpp @@ -844,6 +848,7 @@ set(viewer_HEADER_FILES llfilteredwearablelist.h llfirstuse.h llflexibleobject.h + llfloater360capture.h llfloaterabout.h llfloaterbvhpreview.h llfloateraddpaymentmethod.h @@ -1381,7 +1386,7 @@ file(WRITE "${CMAKE_CURRENT_BINARY_DIR}/viewer_version.txt" "${VIEWER_SHORT_VERSION}.${VIEWER_VERSION_REVISION}\n") set_source_files_properties( - llversioninfo.cpp tests/llversioninfo_test.cpp + llversioninfo.cpp tests/llversioninfo_test.cpp PROPERTIES COMPILE_DEFINITIONS "${VIEWER_CHANNEL_VERSION_DEFINES}" # see BuildVersion.cmake ) @@ -2004,7 +2009,7 @@ endif (WINDOWS) # one of these being libz where you can find four or more versions in play # at once. On Linux, libz can be found at link and run time via a number # of paths: -# +# # => -lfreetype # => libz.so.1 (on install machine, not build) # => -lSDL @@ -2175,7 +2180,7 @@ if (DARWIN) # https://blog.kitware.com/upcoming-in-cmake-2-8-12-osx-rpath-support/ set(CMAKE_MACOSX_RPATH 1) - + set_target_properties( ${VIEWER_BINARY_NAME} PROPERTIES @@ -2436,7 +2441,7 @@ if (LL_TESTS) llworldmap.cpp llworldmipmap.cpp PROPERTIES - LL_TEST_ADDITIONAL_SOURCE_FILES + LL_TEST_ADDITIONAL_SOURCE_FILES tests/llviewertexture_stub.cpp #llviewertexturelist.cpp ) @@ -2470,7 +2475,7 @@ if (LL_TESTS) llworldmap.cpp llworldmipmap.cpp PROPERTIES - LL_TEST_ADDITIONAL_SOURCE_FILES + LL_TEST_ADDITIONAL_SOURCE_FILES tests/llviewertexture_stub.cpp #llviewertexturelist.cpp LL_TEST_ADDITIONAL_LIBRARIES "${BOOST_SYSTEM_LIBRARY}" diff --git a/indra/newview/app_settings/commands.xml b/indra/newview/app_settings/commands.xml index d0480ca47e..3dfe3f6634 100644 --- a/indra/newview/app_settings/commands.xml +++ b/indra/newview/app_settings/commands.xml @@ -276,4 +276,15 @@ is_running_function="Floater.IsOpen" is_running_parameters="my_environments" /> + diff --git a/indra/newview/app_settings/settings.xml b/indra/newview/app_settings/settings.xml index b1120c18b2..5c140fb724 100644 --- a/indra/newview/app_settings/settings.xml +++ b/indra/newview/app_settings/settings.xml @@ -2076,6 +2076,28 @@ Value 1 + BrowserFileAccessFromFileUrls + + Comment + Allow access to local files via file urls in the embedded browser + Persist + 1 + Type + Boolean + Value + 1 + + BrowserPluginsEnabled + + Comment + Enable Web plugins in the built-in Web browser? + Persist + 1 + Type + Boolean + Value + 1 + ChatBarCustomWidth Comment @@ -3539,7 +3561,7 @@ Value 0 - DoubleClickTeleport + DoubleClickTeleport Comment Enable double-click to teleport where allowed (afects minimap and people panel) @@ -8858,7 +8880,7 @@ Value 0 - RenderHiDPI + RenderHiDPI Comment Enable support for HiDPI displays, like Retina (MacOS X ONLY, requires restart) @@ -11580,7 +11602,7 @@ Boolean Value 0 - + NearbyListShowMap Comment @@ -16632,30 +16654,83 @@ Boolean Value 1 - - CefVerboseLog - - Comment - Enable/disable CEF verbose loggingk - Persist - 1 - Type - Boolean - Value - 0 - - ResetUIScaleOnFirstRun - - Comment - Resets the UI scale factor on first run due to changed display scaling behavior - Persist - 1 - Type - Boolean - Value - 1 + + 360CaptureUseInterestListCap + + Comment + Flag if set, uses the new InterestList cap to ask the simulator for full content + Persist + 1 + Type + Boolean + Value + 1 + + 360CaptureJPEGEncodeQuality + + Comment + Quality value to use in the JPEG encoder (0..100) + Persist + 1 + Type + U32 + Value + 95 + + 360CaptureDebugSaveImage + + Comment + Flag if set, saves off each cube map as an image, as well as the JavaScript data URL, for debugging purposes + Persist + 1 + Type + Boolean + Value + 0 + + 360CaptureOutputImageWidth + + Comment + Width of the output 360 equirectangular image + Persist + 1 + Type + U32 + Value + 4096 + + 360CaptureHideAvatars + + Comment + Flag if set, removes all the avatars from the 360 snapshot + Persist + 1 + Type + Boolean + Value + 0 + 360CaptureCameraFOV + + Comment + Field of view of the WebGL camera that converts the cubemap to an equirectangular image + Persist + 1 + Type + U32 + Value + 75 + + ResetUIScaleOnFirstRun + + Comment + Resets the UI scale factor on first run due to changed display scaling behavior + Persist + 1 + Type + Boolean + Value + 1 + - - diff --git a/indra/newview/llfloater360capture.cpp b/indra/newview/llfloater360capture.cpp new file mode 100644 index 0000000000..aa226981aa --- /dev/null +++ b/indra/newview/llfloater360capture.cpp @@ -0,0 +1,904 @@ +/** + * @file llfloater360capture.cpp + * @author Callum Prentice (callum@lindenlab.com) + * @brief Floater code for the 360 Capture feature + * + * $LicenseInfo:firstyear=2011&license=viewerlgpl$ + * Second Life Viewer Source Code + * Copyright (C) 2011, Linden Research, Inc. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; + * version 2.1 of the License only. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + * Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA + * $/LicenseInfo$ + */ + +#include "llviewerprecompiledheaders.h" + +#include "llfloater360capture.h" + +#include "llagent.h" +#include "llagentui.h" +#include "llbase64.h" +#include "llcallbacklist.h" +#include "llenvironment.h" +#include "llimagejpeg.h" +#include "llmediactrl.h" +#include "llradiogroup.h" +#include "llslurl.h" +#include "lltextbox.h" +#include "lltrans.h" +#include "lluictrlfactory.h" +#include "llversioninfo.h" +#include "llviewercamera.h" +#include "llviewercontrol.h" +#include "llviewerpartsim.h" +#include "llviewerregion.h" +#include "llviewerwindow.h" +#include "pipeline.h" + +#include + +LLFloater360Capture::LLFloater360Capture(const LLSD& key) + : LLFloater(key) +{ + // The handle to embedded browser that we use to + // render the WebGL preview as we as host the + // Cube Map to Equirectangular image code + mWebBrowser = nullptr; + + // Ask the simulator to send us everything (and not just + // what it thinks the connected Viewer can see) until + // such time as we ask it not to (the dtor). If we crash or + // otherwise, exit before this is turned off, the Simulator + // will take care of cleaning up for us. + if (gSavedSettings.getBOOL("360CaptureUseInterestListCap")) + { + // send everything to us for as long as this floater is open + const bool send_everything = true; + changeInterestListMode(send_everything); + } +} + +LLFloater360Capture::~LLFloater360Capture() +{ + if (mWebBrowser) + { + mWebBrowser->navigateStop(); + mWebBrowser->clearCache(); + mWebBrowser->unloadMediaSource(); + } + + // Tell the Simulator not to send us everything anymore + // and revert to the regular "keyhole" frustum of interest + // list updates. + if (gSavedSettings.getBOOL("360CaptureUseInterestListCap")) + { + const bool send_everything = false; + changeInterestListMode(send_everything); + } +} + +BOOL LLFloater360Capture::postBuild() +{ + mCaptureBtn = getChild("capture_button"); + mCaptureBtn->setCommitCallback(boost::bind(&LLFloater360Capture::onCapture360ImagesBtn, this)); + + mSaveLocalBtn = getChild("save_local_button"); + mSaveLocalBtn->setCommitCallback(boost::bind(&LLFloater360Capture::onSaveLocalBtn, this)); + mSaveLocalBtn->setEnabled(false); + + mWebBrowser = getChild("360capture_contents"); + mWebBrowser->addObserver(this); + + // There is a group of radio buttons that define the quality + // by each having a 'value' that is returns equal to the pixel + // size (width == height) + mQualityRadioGroup = getChild("360_quality_selection"); + mQualityRadioGroup->setCommitCallback(boost::bind(&LLFloater360Capture::onChooseQualityRadioGroup, this)); + + // UX/UI called for preview mode (always the first index/option) + // by default each time vs restoring the last value + mQualityRadioGroup->setSelectedIndex(0); + + // Construct a URL pointing to the first page to load. Although + // we do not use this page for anything (after some significant + // design changes), we retain the code to load the start page + // in case that changes again one day. It also makes sure the + // embedded browser is active and ready to go for when the real + // page with the 360 preview is navigated to. + std::string url = STRINGIZE( + "file:///" << + getHTMLBaseFolder() << + mDefaultHTML + ); + mWebBrowser->navigateTo(url); + + // initial pass at determining what size (width == height since + // the cube map images are square) we should capture at. + setSourceImageSize(); + + // the size of the output equirectangular image. The height of an EQR image + // is always 1/2 of the width so we should not store it but rather, + // calculate it from the width directly + mOutputImageWidth = gSavedSettings.getU32("360CaptureOutputImageWidth"); + mOutputImageHeight = mOutputImageWidth / 2; + + // enable resizing and enable for width and for height + enableResizeCtrls(true, true, true); + + // initial heading that consumers of the equirectangular image + // (such as Facebook or Flickr) use to position initial view - + // we set during capture - stored as degrees (0..359) + mInitialHeadingDeg = 0.0; + + // save directory in which to store the images (must obliviously be + // writable by the viewer). Also create it for users who haven't + // used the 360 feature before. + mImageSaveDir = gDirUtilp->getLindenUserDir() + gDirUtilp->getDirDelimiter() + "eqrimg"; + LLFile::mkdir(mImageSaveDir); + + // We do an initial capture when the floater is opened, albeit at a 'preview' + // quality level (really low resolution, but really fast) + onCapture360ImagesBtn(); + + return true; +} + +// called when the user choose a quality level using +// the buttons in the radio group +void LLFloater360Capture::onChooseQualityRadioGroup() +{ + // set the size of the captured cube map images based + // on the quality level chosen + setSourceImageSize(); +} + +// Using a new capability, tell the simulator that we want it to send everything +// it knows about and not just what is in front of the camera, in its view +// frustum. We need this feature so that the contents of the region that appears +// in the 6 snapshots which we cannot see and is normally not "considered", is +// also rendered. Typically, this is turned on when the 360 capture floater is +// opened and turned off when it is closed. +// Note: for this version, we do not have a way to determine when "everything" +// has arrived and has been rendered so for now, the proposal is that users +// will need to experiment with the low resolution version and wait for some +// (hopefully) small period of time while the full contents resolves. +// Pass in a flag to ask the simulator/interest list to "send everything" or +// not (the default mode) +void LLFloater360Capture::changeInterestListMode(bool send_everything) +{ + LLSD body; + + if (send_everything) + { + body["mode"] = LLSD::String("360"); + } + else + { + body["mode"] = LLSD::String("default"); + } + + if (gAgent.requestPostCapability("InterestList", body, [](const LLSD & response) + { + LL_INFOS("360Capture") << + "InterestList capability responded: \n" << + ll_pretty_print_sd(response) << + LL_ENDL; + })) + { + LL_INFOS("360Capture") << + "Successfully posted an InterestList capability request with payload: \n" << + ll_pretty_print_sd(body) << + LL_ENDL; + } + else + { + LL_INFOS("360Capture") << + "Unable to post an InterestList capability request with payload: \n" << + ll_pretty_print_sd(body) << + LL_ENDL; + } +} + +// There is is a setting (360CaptureSourceImageSize) that holds the size +// (width == height since it's a square) of each of the 6 source snapshots. +// However there are some known (and I dare say, some more unknown conditions +// where the specified size is not possible and this function tries to figure it +// out and change that setting to the optimal value for the current conditions. +void LLFloater360Capture::setSourceImageSize() +{ + mSourceImageSize = mQualityRadioGroup->getSelectedValue().asInteger(); + + // If deferred rendering is off, we need to shrink the window we capture + // until it's smaller than the Viewer window dimensions. + if (!LLPipeline::sRenderDeferred) + { + LLRect window_rect = gViewerWindow->getWindowRectRaw(); + S32 window_width = window_rect.getWidth(); + S32 window_height = window_rect.getHeight(); + + while (mSourceImageSize > window_width || mSourceImageSize > window_height) + { + mSourceImageSize /= 2; + LL_INFOS("360Capture") << "Deferred rendering is forcing a smaller capture size: " << mSourceImageSize << LL_ENDL; + } + + // there has to be an easier way than this to get the value + // from the radio group item at index 0. Why doesn't + // LLRadioGroup::getSelectedValue(int index) exist? + int index = mQualityRadioGroup->getSelectedIndex(); + mQualityRadioGroup->setSelectedIndex(0); + int min_size = mQualityRadioGroup->getSelectedValue().asInteger(); + mQualityRadioGroup->setSelectedIndex(index); + + // If the maximum size we can support falls below a threshold then + // we should display a message in the log so we can try to debug + // why this is happening + if (mSourceImageSize < min_size) + { + LL_INFOS("360Capture") << "Small snapshot size due to deferred rendering and small app window" << LL_ENDL; + } + } +} + +// This function shouldn't exist! We use the tooltip text from +// the corresponding XUI file (floater_360capture.xml) as the +// overlay text for the final web page to inform the user +// about the quality level in play. There ouught to be a +// UI function like LLView* getSelectedItemView() or similar +// but as far as I can tell, there isn't so we have to resort +// to finding it ourselves with this icky code.. +const std::string LLFloater360Capture::getSelectedQualityTooltip() +{ + // safey (or bravery?) + if (mQualityRadioGroup != nullptr) + { + // for all the child widgets for the radio group + // (just the radio buttons themselves I think) + for (child_list_const_reverse_iter_t iter = mQualityRadioGroup->getChildList()->rbegin(); + iter != mQualityRadioGroup->getChildList()->rend(); + ++iter) + { + // if we match the selected index (which we can get easily) + // with our position in the list of children + if (mQualityRadioGroup->getSelectedIndex() == + std::distance(mQualityRadioGroup->getChildList()->rend(), iter) - 1) + { + // return the plain old tooltip text + return (*iter)->getToolTip(); + } + } + } + + // if it's not found or not available, return an empty string + return std::string(); +} + +// Some of the 'magic' happens via a web page in an HTML directory +// and this code provides a single point of reference for its' location +const std::string LLFloater360Capture::getHTMLBaseFolder() +{ + std::string folder_name = gDirUtilp->getSkinDir(); + folder_name += gDirUtilp->getDirDelimiter(); + folder_name += "html"; + folder_name += gDirUtilp->getDirDelimiter(); + folder_name += "common"; + folder_name += gDirUtilp->getDirDelimiter(); + folder_name += "equirectangular"; + folder_name += gDirUtilp->getDirDelimiter(); + + return folder_name; +} + +// triggered when the 'capture' button in the UI is pressed +void LLFloater360Capture::onCapture360ImagesBtn() +{ + // launch the main capture code in a coroutine so we can + // yield/suspend at some points to give the main UI + // thread a look-in occasionally. + LLCoros::instance().launch("capture360cap", [this]() + { + capture360Images(); + }); +} + +// Gets the full path name for a given JavaScript file in the HTML folder. We +// use this ultimately as a parameter to the main web app so it knows where to find +// the JavaScript array containing the 6 cube map images, stored as data URLs +const std::string LLFloater360Capture::makeFullPathToJS(const std::string filename) +{ + std::string full_js_path = mImageSaveDir; + full_js_path += gDirUtilp->getDirDelimiter(); + full_js_path += filename; + + return full_js_path; +} + +// Write the header/prequel portion of the JavaScript array of data urls +// that we use to store the cube map images in (so the web code can load +// them without tweaking browser security - we'd have to do this if they +// we stored as plain old images) This deliberately overwrites the old +// one, if it exists +void LLFloater360Capture::writeDataURLHeader(const std::string filename) +{ + std::ofstream file_handle(filename.c_str()); + if (file_handle.is_open()) + { + file_handle << "// cube map images for Second Life Viewer panorama 360 images" << std::endl; + file_handle.close(); + } +} + +// Write the footer/sequel portion of the JavaScript image code. When this is +// called, the current file on disk will contain the header and the 6 data +// URLs, each with a well specified name. This final piece of JavaScript code +// creates an array from those data URLs that the main application can +// reference and read. +void LLFloater360Capture::writeDataURLFooter(const std::string filename) +{ + std::ofstream file_handle(filename.c_str(), std::ios_base::app); + if (file_handle.is_open()) + { + file_handle << "var cubemap_img_js = [" << std::endl; + file_handle << " img_posx, img_negx," << std::endl; + file_handle << " img_posy, img_negy," << std::endl; + file_handle << " img_posz, img_negz," << std::endl; + file_handle << "];" << std::endl; + + file_handle.close(); + } +} + +// Given a filename, a chunk of data (representing an image file) and the size +// of the buffer, we create a BASE64 encoded string and use it to build a JavaScript +// data URL that represents the image in a web browser environment +bool LLFloater360Capture::writeDataURL(const std::string filename, const std::string prefix, U8* data, unsigned int data_len) +{ + LL_INFOS("360Capture") << "Writing data URL for " << prefix << " to " << filename << LL_ENDL; + + const std::string data_url = LLBase64::encode(data, data_len); + + std::ofstream file_handle(filename.c_str(), std::ios_base::app); + if (file_handle.is_open()) + { + file_handle << "var img_"; + file_handle << prefix; + file_handle << " = '"; + file_handle << "data:image/jpeg; base64,"; + file_handle << data_url; + file_handle << "'"; + file_handle << std::endl; + file_handle.close(); + + return true; + } + + return false; +} + +// Encode the image from each of the 6 snapshots and save it out to +// the JavaScript array of data URLs +void LLFloater360Capture::encodeAndSave(LLPointer raw_image, const std::string filename, const std::string prefix) +{ + // the default quality for the JPEG encoding is set quite high + // but this still seems to be a reasonable compromise for + // quality/size and is still much smaller than equivalent PNGs + int jpeg_encode_quality = gSavedSettings.getU32("360CaptureJPEGEncodeQuality"); + LLPointer jpeg_image = new LLImageJPEG(jpeg_encode_quality); + + // Actually encode the JPEG image. This is where a lot of time + // is spent now that the snapshot capture process has been + // optimized. The encode_time parameter doesn't appear to be + // used anymore. + const int encode_time = 0; + bool resultjpeg = jpeg_image->encode(raw_image, encode_time); + + if (resultjpeg) + { + // save individual cube map images as real JPEG files + // for debugging or curiosity) based on debug settings + if (gSavedSettings.getBOOL("360CaptureDebugSaveImage")) + { + const std::string jpeg_filename = STRINGIZE( + gDirUtilp->getLindenUserDir() << + gDirUtilp->getDirDelimiter() << + "eqrimg" << + gDirUtilp->getDirDelimiter() << + prefix << + "." << + jpeg_image->getExtension() + ); + + LL_INFOS("360Capture") << "Saving debug JPEG image as " << jpeg_filename << LL_ENDL; + jpeg_image->save(jpeg_filename); + } + + // actually write the JPEG image to disk as a data URL + writeDataURL(filename, prefix, jpeg_image->getData(), jpeg_image->getDataSize()); + } +} + +// Defer back to the main loop for a single rendered frame to give +// the renderer a chance to update the UI if it is needed +void LLFloater360Capture::suspendForAFrame() +{ + const U32 frame_count_delta = 1; + U32 curr_frame_count = LLFrameTimer::getFrameCount(); + while (LLFrameTimer::getFrameCount() <= curr_frame_count + frame_count_delta) + { + llcoro::suspend(); + } +} + +// A debug version of the snapshot code that simply fills the +// buffer with a pattern that can be used to investigate +// issues with encoding and saving off each RAW image. +// Probably not needed anymore but saving here just in case. +void LLFloater360Capture::mockSnapShot(LLImageRaw* raw) +{ + unsigned int width = raw->getWidth(); + unsigned int height = raw->getHeight(); + unsigned int depth = raw->getComponents(); + unsigned char* pixels = raw->getData(); + + for (int y = 0; y < height; y++) + { + for (int x = 0; x < width; x++) + { + unsigned long offset = y * width * depth + x * depth; + unsigned char red = x * 256 / width; + unsigned char green = y * 256 / height; + unsigned char blue = ((x + y) / 2) * 256 / (width + height) / 2; + pixels[offset + 0] = red; + pixels[offset + 1] = green; + pixels[offset + 2] = blue; + } + } +} + +// The main code that actually captures all 6 images and then saves them out to +// disk before navigating the embedded web browser to the page with the WebGL +// application that consumes them and creates an EQR image. This code runs as a +// coroutine so it can be suspended at certain points. +void LLFloater360Capture::capture360Images() +{ + // recheck the size of the cube map source images in case it changed + // since it was set when we opened the floater + setSourceImageSize(); + + // disable buttons while we are capturing + mCaptureBtn->setEnabled(false); + mSaveLocalBtn->setEnabled(false); + + // determine whether or not to include avatar in the scene as we capture the 360 panorama + if (gSavedSettings.getBOOL("360CaptureHideAvatars")) + { + // Turn off the avatar if UI tells us to hide it. + // Note: the original call to gAvatar.hide(FALSE) did *not* hide + // attachments and so for most residents, there would be some debris + // left behind in the snapshot. + // Note: this toggles so if it set to on, this will turn it off and + // the subsequent call to the same thing after capture is finished + // will turn it back on again. Similarly, for the case where it + // was set to off - I think this is what we need + LLPipeline::toggleRenderTypeControl(LLPipeline::RENDER_TYPE_AVATAR); + } + + // these are the 6 directions we will point the camera - essentially, + // North, South, East, West, Up, Down + LLVector3 look_dirs[6] = { LLVector3(1, 0, 0), LLVector3(0, 1, 0), LLVector3(0, 0, 1), LLVector3(-1, 0, 0), LLVector3(0, -1, 0), LLVector3(0, 0, -1) }; + LLVector3 look_upvecs[6] = { LLVector3(0, 0, 1), LLVector3(0, 0, 1), LLVector3(0, -1, 0), LLVector3(0, 0, 1), LLVector3(0, 0, 1), LLVector3(0, 1, 0) }; + + // save current view/camera settings so we can restore them afterwards + S32 old_occlusion = LLPipeline::sUseOcclusion; + LLPipeline::sUseOcclusion = 0; + LLViewerCamera* camera = LLViewerCamera::getInstance(); + F32 old_fov = camera->getView(); + F32 old_aspect = camera->getAspect(); + F32 old_yaw = camera->getYaw(); + + // stop the motion of as much of the world moving as much as we can + freezeWorld(true); + + // Save the direction (in degrees) the camera is looking when we + // take the shot since that is what we write to image metadata + // 'GPano:InitialViewHeadingDegrees' field. + // We need to convert from the angle getYaw() gives us into something + // the XMP data field wants (N=0, E=90, S=180, W= 270 etc.) + mInitialHeadingDeg = (360 + 90 - (int)(camera->getYaw() * RAD_TO_DEG)) % 360; + LL_INFOS("360Capture") << "Recording a heading of " << (int)(mInitialHeadingDeg) << LL_ENDL; + + // camera constants for the square, cube map capture image + camera->setAspect(1.0); // must set aspect ratio first to avoid undesirable clamping of vertical FoV + camera->setView(F_PI_BY_TWO); + camera->yaw(0.0); + + // record how many times we changed camera to try to understand the "all shots are the same issue" + unsigned int camera_changed_times = 0; + + // the name of the JavaScript file written out that contains the 6 cube map images + // stored as a JavaScript array of data URLs. If you change this filename, you must + // also change the corresponding entry in the HTML file that uses it - + // (newview/skins/default/html/common/equirectangular/display_eqr.html) + const std::string cumemap_js_filename("cubemap_img.js"); + + // construct the full path to this file - typically stored in the users' + // Second Life settings / username / eqrimg folder. + const std::string cubemap_js_full_path = makeFullPathToJS(cumemap_js_filename); + + // Write the JavaScript file header (the top of the file before the + // declarations of the actual data URLs array). In practice, all this writes + // is a comment - it's main purpose is to reset the file from the last time + // it was written + writeDataURLHeader(cubemap_js_full_path); + + // the names of the prefixes we assign as the name to each data URL and are then + // consumed by the WebGL application. Nominally, they stand for positive and + // negative in the X/Y/Z directions. + static const std::string prefixes[6] = + { + "posx", "posz", "posy", + "negx", "negz", "negy", + }; + + // time the encode process for later optimization + auto encode_time_total = 0.0; + + // for each of the 6 directions we shoot... + for (int i = 0; i < 6; i++) + { + // these buffers are where the raw, captured pixels are stored and + // the first time we use them, we have to make a new one + if (mRawImages[i] == nullptr) + { + mRawImages[i] = new LLImageRaw(mSourceImageSize, mSourceImageSize, 3); + } + else + // subsequent capture with floater open so we resize the buffer from + // the previous run + { + // LLImageRaw deletes the old one via operator= but just to be + // sure, we delete its' large data member first... + mRawImages[i]->deleteData(); + mRawImages[i] = new LLImageRaw(mSourceImageSize, mSourceImageSize, 3); + } + + // set up camera to look in each direction + camera->lookDir(look_dirs[i], look_upvecs[i]); + + // record if camera changed to try to understand the "all shots are the same issue" + if (camera->isChanged()) + { + ++camera_changed_times; + } + + // call the (very) simplified snapshot code that simply deals + // with a single image, no sub-images etc. but is very fast + gViewerWindow->simpleSnapshot(mRawImages[i], mSourceImageSize, mSourceImageSize); + + // encode each image and write to disk while saving how long it took to do so + auto t_start = std::chrono::high_resolution_clock::now(); + encodeAndSave(mRawImages[i], cubemap_js_full_path, prefixes[i]); + auto t_end = std::chrono::high_resolution_clock::now(); + auto duration = std::chrono::duration_cast>(t_end - t_start); + encode_time_total += duration.count(); + + // ping the main loop in case the snapshot process takes a really long + // time and we get disconnected + LLAppViewer::instance()->pingMainloopTimeout("LLFloater360Capture::capture360Images"); + } + + // display time to encode all 6 images. It tends to be a fairly linear + // time for each so we don't need to worry about displaying the time + // for each - this gives us plenty to use for optimizing + LL_INFOS("360Capture") << + "Time to encode and save 6 images was " << + encode_time_total << + " seconds" << + LL_ENDL; + + // Write the JavaScript file footer (the bottom of the file after the + // declarations of the actual data URLs array). The footer comprises of + // a JavaScript array declaration that references the 6 data URLs generated + // previously and is what is referred to in the display HTML file + // (newview/skins/default/html/common/equirectangular/display_eqr.html) + writeDataURLFooter(cubemap_js_full_path); + + // unfreeze the world now we have our shots + freezeWorld(false); + + // restore original view/camera/avatar settings settings + camera->setAspect(old_aspect); + camera->setView(old_fov); + camera->yaw(old_yaw); + LLPipeline::sUseOcclusion = old_occlusion; + + // if we toggled off the avatar because the Hide check box was ticked, + // we should toggle it back to where it was before we started the capture + if (gSavedSettings.getBOOL("360CaptureHideAvatars")) + { + LLPipeline::toggleRenderTypeControl(LLPipeline::RENDER_TYPE_AVATAR); + } + + // record that we missed some shots in the log for later debugging + // note: we use 5 and not 6 because the first shot isn't regarded + // as a change - only the subsequent 5 are + if (camera_changed_times < 5) + { + LL_INFOS("360Capture") << "Warning: we only captured " << camera_changed_times << " images." << LL_ENDL; + } + + // now we have the 6 shots saved in a well specified location, + // we can load the web content that uses them + std::string url = "file:///" + getHTMLBaseFolder() + mEqrGenHTML; + mWebBrowser->navigateTo(url); + + // allow the UI to update by suspending and waiting for the + // main render loop to update the UI + suspendForAFrame(); + + // page is loaded and ready so we can turn on the buttons again + mCaptureBtn->setEnabled(true); + mSaveLocalBtn->setEnabled(true); +} + +// once the request is made to navigate to the web page containing the code +// to process the 6 images into an EQR one, we have to wait for it to finish +// loaded - we get a "navigate complete" event when that happens that we can act on +void LLFloater360Capture::handleMediaEvent(LLPluginClassMedia* self, EMediaEvent event) +{ + switch (event) + { + // not used right now but retaining because this event might + // be useful for a feature I am hoping to add + case MEDIA_EVENT_LOCATION_CHANGED: + break; + + // navigation in the browser completed + case MEDIA_EVENT_NAVIGATE_COMPLETE: + { + // Confirm that the navigation event does indeed apply to the + // page we are looking for. At the moment, this is the only + // one we care about so the test is superfluous but that might change. + std::string navigate_url = self->getNavigateURI(); + if (navigate_url.find(mEqrGenHTML) != std::string::npos) + { + // this string is being passed across to the web so replace all the windows backslash + // characters with forward slashes or (I think) the backslashes are treated as escapes + std::replace(mImageSaveDir.begin(), mImageSaveDir.end(), '\\', '/'); + + // we store the camera FOV (field of view) in a saved setting since this feels + // like something it would be interesting to change and experiment with + int camera_fov = gSavedSettings.getU32("360CaptureCameraFOV"); + + // compose the overlay for the final web page that tells the user + // what level of quality the capture was taken with + std::string overlay_label = "'" + getSelectedQualityTooltip() + "'"; + + // so now our page is loaded and images are in place - call + // the JavaScript init script with some parameters to initialize + // the WebGL based preview + const std::string cmd = STRINGIZE( + "init(" + << mOutputImageWidth + << ", " + << mOutputImageHeight + << ", " + << "'" + << mImageSaveDir + << "'" + << ", " + << camera_fov + << ", " + << LLViewerCamera::getInstance()->getYaw() + << ", " + << overlay_label + << ")" + ); + + // execute the command on the page + mWebBrowser->getMediaPlugin()->executeJavaScript(cmd); + } + } + break; + + default: + break; + } +} + +// called when the user wants to save the cube maps off to the final EQR image +void LLFloater360Capture::onSaveLocalBtn() +{ + // region name and URL + std::string region_name; // no sensible default + std::string region_url("http://secondlife.com"); + LLViewerRegion* region = gAgent.getRegion(); + if (region) + { + // region names can (and do) contain characters that would make passing + // them into a JavaScript function problematic - single quotes for example + // so we must escape/encode both + region_name = region->getName(); + + // escaping/encoding is a minefield - let's just remove any offending characters from the region name + region_name.erase(std::remove(region_name.begin(), region_name.end(), '\''), region_name.end()); + region_name.erase(std::remove(region_name.begin(), region_name.end(), '\"'), region_name.end()); + + // fortunately there is already an escaping function built into the SLURL generation code + LLSLURL slurl; + bool is_escaped = true; + LLAgentUI::buildSLURL(slurl, is_escaped); + region_url = slurl.getSLURLString(); + } + + // build suggested filename (the one that appears as the default + // in the Save dialog box) + const std::string suggested_filename = generate_proposed_filename(); + + // This string (the name of the product plus a truncated version number (no build)) + // is used in the XMP block as the name of the generating and stitching software. + // We save the version number here and not in the more generic 'software' item + // because that might help us determine something about the image in the future. + const std::string client_version = STRINGIZE( + LLVersionInfo::instance().getChannel() << + " " << + LLVersionInfo::instance().getShortVersion() + ); + + // save the time the image was created. I don't know if this should be + // UTC/ZULU or the users' local time. It probably doesn't matter. + std::time_t result = std::time(nullptr); + std::string ctime_str = std::ctime(&result); + std::string time_str = ctime_str.substr(0, ctime_str.length() - 1); + + // build the JavaScript data structure that is used to pass all the + // variables into the JavaScript function on the web page loaded into + // the embedded browser component of the floater. + const std::string xmp_details = STRINGIZE( + "{ " << + "pano_version: '" << "2.2.1" << "', " << + "software: '" << LLVersionInfo::instance().getChannel() << "', " << + "capture_software: '" << client_version << "', " << + "stitching_software: '" << client_version << "', " << + "width: " << mOutputImageWidth << ", " << + "height: " << mOutputImageHeight << ", " << + "heading: " << mInitialHeadingDeg << ", " << + "actual_source_image_size: " << mQualityRadioGroup->getSelectedValue().asInteger() << ", " << + "scaled_source_image_size: " << mSourceImageSize << ", " << + "first_photo_date: '" << time_str << "', " << + "last_photo_date: '" << time_str << "', " << + "region_name: '" << region_name << "', " << + "region_url: '" << region_url << "', " << + " }" + ); + + // build the JavaScript command to send to the web browser + const std::string cmd = "saveAsEqrImage(\"" + suggested_filename + "\", " + xmp_details + ")"; + + // send it to the browser instance, triggering the equirectangular capture + // process and complimentary offer to save the image + mWebBrowser->getMediaPlugin()->executeJavaScript(cmd); +} + +// We capture all 6 images sequentially and if parts of the world are moving +// E.G. clouds, water, objects - then we may get seams or discontinuities +// when the images are combined to form the EQR image. This code tries to +// stop everything so we can shoot for seamless shots. There is probably more +// we can do here - e.g. waves in the water probably won't line up. +void LLFloater360Capture::freezeWorld(bool enable) +{ + static bool clouds_scroll_paused = false; + if (enable) + { + // record the cloud scroll current value so we can restore it + clouds_scroll_paused = LLEnvironment::instance().isCloudScrollPaused(); + + // stop the clouds moving + LLEnvironment::instance().pauseCloudScroll(); + + // freeze all avatars + LLCharacter* avatarp; + for (std::vector::iterator iter = LLCharacter::sInstances.begin(); + iter != LLCharacter::sInstances.end(); ++iter) + { + avatarp = *iter; + mAvatarPauseHandles.push_back(avatarp->requestPause()); + } + + // freeze everything else + gSavedSettings.setBOOL("FreezeTime", true); + + // disable particle system + LLViewerPartSim::getInstance()->enable(false); + } + else // turning off freeze world mode, either temporarily or not. + { + // restart the clouds moving if they were not paused before + // we starting using the 360 capture floater + if (clouds_scroll_paused == false) + { + LLEnvironment::instance().resumeCloudScroll(); + } + + // thaw all avatars + mAvatarPauseHandles.clear(); + + // thaw everything else + gSavedSettings.setBOOL("FreezeTime", false); + + //enable particle system + LLViewerPartSim::getInstance()->enable(true); + } +} + +// Build the default filename that appears in the Save dialog box. We try +// to encode some metadata about too (region name, EQR dimensions, capture +// time) but the user if free to replace this with anything else before +// the images is saved. +const std::string LLFloater360Capture::generate_proposed_filename() +{ + std::ostringstream filename(""); + + // base name + filename << "sl360_"; + + LLViewerRegion* region = gAgent.getRegion(); + if (region) + { + // this looks complex but it's straightforward - removes all non-alpha chars from a string + // which in this case is the SL region name - we use it as a proposed filename but the user is free to change + std::string region_name = region->getName(); + std::replace_if(region_name.begin(), region_name.end(), std::not1(std::ptr_fun(isalnum)), '_'); + if (region_name.length() > 0) + { + filename << region_name; + filename << "_"; + } + } + + // add in resolution to make it easier to tell what you captured later + filename << mOutputImageWidth; + filename << "x"; + filename << mOutputImageHeight; + filename << "_"; + + // Add in the size of the source image (width == height since it was square) + // Might be useful later for quality comparisons + filename << mSourceImageSize; + filename << "_"; + + // add in the current HH-MM-SS (with leading 0's) so users can easily save many shots in same folder + std::time_t cur_epoch = std::time(nullptr); + std::tm* tm_time = std::localtime(&cur_epoch); + filename << std::setfill('0') << std::setw(4) << (tm_time->tm_year + 1900); + filename << std::setfill('0') << std::setw(2) << (tm_time->tm_mon + 1); + filename << std::setfill('0') << std::setw(2) << tm_time->tm_mday; + filename << "_"; + filename << std::setfill('0') << std::setw(2) << tm_time->tm_hour; + filename << std::setfill('0') << std::setw(2) << tm_time->tm_min; + filename << std::setfill('0') << std::setw(2) << tm_time->tm_sec; + + // the unusual way we save the output image (originates in the + // embedded browser and not the C++ code) means that the system + // appends ".jpeg" to the file automatically on macOS at least, + // so we only need to do it ourselves for windows. +#if LL_WINDOWS + filename << ".jpg"; +#endif + + return filename.str(); +} diff --git a/indra/newview/llfloater360capture.h b/indra/newview/llfloater360capture.h new file mode 100644 index 0000000000..6da7ee074a --- /dev/null +++ b/indra/newview/llfloater360capture.h @@ -0,0 +1,97 @@ +/** + * @file llfloater360capture.h + * @author Callum Prentice (callum@lindenlab.com) + * @brief Floater for the 360 capture feature + * + * $LicenseInfo:firstyear=2011&license=viewerlgpl$ + * Second Life Viewer Source Code + * Copyright (C) 2011, Linden Research, Inc. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; + * version 2.1 of the License only. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + * Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA + * $/LicenseInfo$ + */ + +#ifndef LL_FLOATER_360CAPTURE_H +#define LL_FLOATER_360CAPTURE_H + +#include "llfloater.h" +#include "llmediactrl.h" +#include "llcharacter.h" + +class LLImageRaw; +class LLTextBox; +class LLRadioGroup; + +class LLFloater360Capture: + public LLFloater, + public LLViewerMediaObserver +{ + friend class LLFloaterReg; + + private: + LLFloater360Capture(const LLSD& key); + + ~LLFloater360Capture(); + BOOL postBuild() override; + void handleMediaEvent(LLPluginClassMedia* self, EMediaEvent event) override; + + void changeInterestListMode(bool send_everything); + + const std::string getHTMLBaseFolder(); + void capture360Images(); + + const std::string makeFullPathToJS(const std::string filename); + void writeDataURLHeader(const std::string filename); + void writeDataURLFooter(const std::string filename); + bool writeDataURL(const std::string filename, const std::string prefix, U8* data, unsigned int data_len); + void encodeAndSave(LLPointer raw_image, const std::string filename, const std::string prefix); + + std::vector mAvatarPauseHandles; + void freezeWorld(bool enable); + + void mockSnapShot(LLImageRaw* raw); + + void suspendForAFrame(); + + const std::string generate_proposed_filename(); + + void setSourceImageSize(); + + LLMediaCtrl* mWebBrowser; + const std::string mDefaultHTML = "default.html"; + const std::string mEqrGenHTML = "eqr_gen.html"; + + LLUICtrl* mCaptureBtn; + void onCapture360ImagesBtn(); + + void onSaveLocalBtn(); + LLUICtrl* mSaveLocalBtn; + + LLRadioGroup* mQualityRadioGroup; + void onChooseQualityRadioGroup(); + const std::string getSelectedQualityTooltip(); + + int mSourceImageSize; + float mInitialHeadingDeg; + int mOutputImageWidth; + int mOutputImageHeight; + std::string mImageSaveDir; + + LLPointer mRawImages[6]; +}; + +#endif // LL_FLOATER_360CAPTURE_H diff --git a/indra/newview/llfloatersnapshot.cpp b/indra/newview/llfloatersnapshot.cpp index ef7a9fd536..83212230e5 100644 --- a/indra/newview/llfloatersnapshot.cpp +++ b/indra/newview/llfloatersnapshot.cpp @@ -179,6 +179,10 @@ void LLFloaterSnapshotBase::ImplBase::updateLayout(LLFloaterSnapshotBase* floate thumbnail_placeholder->reshape(panel_width, thumbnail_placeholder->getRect().getHeight()); floaterp->getChild("image_res_text")->setVisible(mAdvanced); floaterp->getChild("file_size_label")->setVisible(mAdvanced); + if (floaterp->hasChild("360_label", TRUE)) + { + floaterp->getChild("360_label")->setVisible(mAdvanced); + } if(!floaterp->isMinimized()) { floaterp->reshape(floater_width, floaterp->getRect().getHeight()); @@ -992,6 +996,10 @@ BOOL LLFloaterSnapshot::postBuild() getChild("retract_btn")->setCommitCallback(boost::bind(&LLFloaterSnapshot::onExtendFloater, this)); getChild("extend_btn")->setCommitCallback(boost::bind(&LLFloaterSnapshot::onExtendFloater, this)); + getChild("360_label")->setSoundFlags(LLView::MOUSE_UP); + getChild("360_label")->setShowCursorHand(false); + getChild("360_label")->setClickedCallback(boost::bind(&LLFloaterSnapshot::on360Snapshot, this)); + // Filters LLComboBox* filterbox = getChild("filters_combobox"); std::vector filter_list = LLImageFiltersManager::getInstance()->getFiltersList(); @@ -1118,6 +1126,12 @@ void LLFloaterSnapshot::onExtendFloater() impl->setAdvanced(gSavedSettings.getBOOL("AdvanceSnapshot")); } +void LLFloaterSnapshot::on360Snapshot() +{ + LLFloaterReg::showInstance("360capture"); + closeFloater(); +} + //virtual void LLFloaterSnapshotBase::onClose(bool app_quitting) { diff --git a/indra/newview/llfloatersnapshot.h b/indra/newview/llfloatersnapshot.h index 8221b0a637..7ec133ff45 100644 --- a/indra/newview/llfloatersnapshot.h +++ b/indra/newview/llfloatersnapshot.h @@ -153,6 +153,7 @@ public: static void update(); void onExtendFloater(); + void on360Snapshot(); static LLFloaterSnapshot* getInstance(); static LLFloaterSnapshot* findInstance(); diff --git a/indra/newview/llviewerfloaterreg.cpp b/indra/newview/llviewerfloaterreg.cpp index 5a05f89758..62d73063aa 100644 --- a/indra/newview/llviewerfloaterreg.cpp +++ b/indra/newview/llviewerfloaterreg.cpp @@ -33,6 +33,7 @@ #include "llcommandhandler.h" #include "llcompilequeue.h" #include "llfasttimerview.h" +#include "llfloater360capture.h" #include "llfloaterabout.h" #include "llfloateraddpaymentmethod.h" #include "llfloaterauction.h" @@ -195,6 +196,7 @@ void LLViewerFloaterReg::registerFloaters() // *NOTE: Please keep these alphabetized for easier merges LLFloaterAboutUtil::registerFloater(); + LLFloaterReg::add("360capture", "floater_360capture.xml", (LLFloaterBuildFunc)&LLFloaterReg::build); LLFloaterReg::add("block_timers", "floater_fast_timers.xml", (LLFloaterBuildFunc)&LLFloaterReg::build); LLFloaterReg::add("about_land", "floater_about_land.xml", (LLFloaterBuildFunc)&LLFloaterReg::build); LLFloaterReg::add("add_payment_method", "floater_add_payment_method.xml", (LLFloaterBuildFunc)&LLFloaterReg::build); diff --git a/indra/newview/llviewermedia.cpp b/indra/newview/llviewermedia.cpp index d35dbda907..c428e73a6e 100644 --- a/indra/newview/llviewermedia.cpp +++ b/indra/newview/llviewermedia.cpp @@ -1746,8 +1746,16 @@ LLPluginClassMedia* LLViewerMediaImpl::newSourceFromMediaType(std::string media_ media_source->cookies_enabled( cookies_enabled || clean_browser); // collect 'javascript enabled' setting from prefs and send to embedded browser - bool javascript_enabled = gSavedSettings.getBOOL( "BrowserJavascriptEnabled" ); - media_source->setJavascriptEnabled( javascript_enabled || clean_browser); + bool javascript_enabled = gSavedSettings.getBOOL("BrowserJavascriptEnabled"); + media_source->setJavascriptEnabled(javascript_enabled || clean_browser); + + // collect 'web security disabled' (see Chrome --web-security-disabled) setting from prefs and send to embedded browser + bool web_security_disabled = gSavedSettings.getBOOL("BrowserWebSecurityDisabled"); + media_source->setWebSecurityDisabled(web_security_disabled || clean_browser); + + // collect setting indicates if local file access from file URLs is allowed from prefs and send to embedded browser + bool file_access_from_file_urls = gSavedSettings.getBOOL("BrowserFileAccessFromFileUrls"); + media_source->setFileAccessFromFileUrlsEnabled(file_access_from_file_urls || clean_browser); // As of SL-15559 PDF files do not load in CEF v91 we enable plugins // but explicitly disable Flash (PDF support in CEF is now treated as a plugin) @@ -1907,6 +1915,15 @@ void LLViewerMediaImpl::loadURI() } } +////////////////////////////////////////////////////////////////////////////////////////// +void LLViewerMediaImpl::executeJavaScript(const std::string& code) +{ + if (mMediaSource) + { + mMediaSource->executeJavaScript(code); + } +} + ////////////////////////////////////////////////////////////////////////////////////////// void LLViewerMediaImpl::setSize(int width, int height) { @@ -3212,8 +3229,19 @@ void LLViewerMediaImpl::handleMediaEvent(LLPluginClassMedia* plugin, LLPluginCla case LLViewerMediaObserver::MEDIA_EVENT_FILE_DOWNLOAD: { - //llinfos << "Media event - file download requested - filename is " << self->getFileDownloadFilename() << llendl; - LLNotificationsUtil::add("MediaFileDownloadUnsupported"); + LL_DEBUGS("Media") << "Media event - file download requested - filename is " << plugin->getFileDownloadFilename() << LL_ENDL; + // pick a file from SAVE FILE dialog + + // need a better algorithm that this or else, pass in type of save type + // from event that initiated it - this is okay for now - only thing + // that saves is 360s + std::string suggested_filename = plugin->getFileDownloadFilename(); + LLFilePicker::ESaveFilter filter = LLFilePicker::FFSAVE_ALL; + if (suggested_filename.find(".jpg") != std::string::npos || suggested_filename.find(".jpeg") != std::string::npos) + filter = LLFilePicker::FFSAVE_JPEG; + if (suggested_filename.find(".png") != std::string::npos) + filter = LLFilePicker::FFSAVE_PNG; + init_threaded_picker_save_dialog(plugin, filter, suggested_filename); } break; diff --git a/indra/newview/llviewermedia.h b/indra/newview/llviewermedia.h index 8bf1ad2441..71cec5125d 100644 --- a/indra/newview/llviewermedia.h +++ b/indra/newview/llviewermedia.h @@ -207,6 +207,7 @@ public: bool initializeMedia(const std::string& mime_type); bool initializePlugin(const std::string& media_type); void loadURI(); + void executeJavaScript(const std::string& code); LLPluginClassMedia* getMediaPlugin() { return mMediaSource.get(); } void setSize(int width, int height); diff --git a/indra/newview/llviewermenu.cpp b/indra/newview/llviewermenu.cpp index ad81cb07c1..0eda1523ff 100644 --- a/indra/newview/llviewermenu.cpp +++ b/indra/newview/llviewermenu.cpp @@ -1263,12 +1263,55 @@ class LLAdvancedDumpScriptedCamera : public view_listener_t class LLAdvancedDumpRegionObjectCache : public view_listener_t { bool handleEvent(const LLSD& userdata) -{ + { handle_dump_region_object_cache(NULL); return true; } }; +class LLAdvancedInterestListFullUpdate : public view_listener_t +{ + bool handleEvent(const LLSD& userdata) + { + LLSD request; + LLSD body; + static bool using_360 = false; + + if (using_360) + { + body["mode"] = LLSD::String("default"); + } + else + { + body["mode"] = LLSD::String("360"); + } + using_360 = !using_360; + + if (gAgent.requestPostCapability("InterestList", body, [](const LLSD& response) + { + LL_INFOS("360Capture") << + "InterestList capability responded: \n" << + ll_pretty_print_sd(response) << + LL_ENDL; + })) + { + LL_INFOS("360Capture") << + "Successfully posted an InterestList capability request with payload: \n" << + ll_pretty_print_sd(body) << + LL_ENDL; + return true; + } + else + { + LL_INFOS("360Capture") << + "Unable to post an InterestList capability request with payload: \n" << + ll_pretty_print_sd(body) << + LL_ENDL; + return false; + } + } +}; + class LLAdvancedBuyCurrencyTest : public view_listener_t { bool handleEvent(const LLSD& userdata) @@ -2309,11 +2352,10 @@ class LLAdvancedLeaveAdminStatus : public view_listener_t // Advanced > Debugging // ////////////////////////// - class LLAdvancedForceErrorBreakpoint : public view_listener_t { bool handleEvent(const LLSD& userdata) - { + { force_error_breakpoint(NULL); return true; } @@ -9189,6 +9231,7 @@ void initialize_menus() // Advanced > World view_listener_t::addMenu(new LLAdvancedDumpScriptedCamera(), "Advanced.DumpScriptedCamera"); view_listener_t::addMenu(new LLAdvancedDumpRegionObjectCache(), "Advanced.DumpRegionObjectCache"); + view_listener_t::addMenu(new LLAdvancedInterestListFullUpdate(), "Advanced.InterestListFullUpdate"); // Advanced > UI commit.add("Advanced.WebBrowserTest", boost::bind(&handle_web_browser_test, _2)); // sigh! this one opens the MEDIA browser diff --git a/indra/newview/llviewermessage.cpp b/indra/newview/llviewermessage.cpp index 39c891c9c1..7737a2753f 100644 --- a/indra/newview/llviewermessage.cpp +++ b/indra/newview/llviewermessage.cpp @@ -3745,6 +3745,7 @@ void process_kill_object(LLMessageSystem *mesgsys, void **user_data) { LLColor4 color(0.f,1.f,0.f,1.f); gPipeline.addDebugBlip(objectp->getPositionAgent(), color); + LL_DEBUGS("MessageBlip") << "Kill blip for local " << local_id << " at " << objectp->getPositionAgent() << LL_ENDL; } // Do the kill diff --git a/indra/newview/llviewerobject.cpp b/indra/newview/llviewerobject.cpp index b88baf6aa7..31e80eb865 100644 --- a/indra/newview/llviewerobject.cpp +++ b/indra/newview/llviewerobject.cpp @@ -2402,6 +2402,7 @@ U32 LLViewerObject::processUpdateMessage(LLMessageSystem *mesgsys, color.setVec(1.f, 0.f, 0.f, 1.f); } gPipeline.addDebugBlip(getPositionAgent(), color); + LL_DEBUGS("MessageBlip") << "Update type " << (S32)update_type << " blip for local " << mLocalID << " at " << getPositionAgent() << LL_ENDL; } const F32 MAG_CUTOFF = F_APPROXIMATELY_ZERO; diff --git a/indra/newview/llviewerregion.cpp b/indra/newview/llviewerregion.cpp index 7628a6c7ef..df84938eac 100644 --- a/indra/newview/llviewerregion.cpp +++ b/indra/newview/llviewerregion.cpp @@ -2956,6 +2956,8 @@ void LLViewerRegionImpl::buildCapabilityNames(LLSD& capabilityNames) capabilityNames.append("IncrementCOFVersion"); AISAPI::getCapNames(capabilityNames); + capabilityNames.append("InterestList"); + capabilityNames.append("GetDisplayNames"); capabilityNames.append("GetExperiences"); capabilityNames.append("AgentExperiences"); diff --git a/indra/newview/llviewerwindow.cpp b/indra/newview/llviewerwindow.cpp index 1d13a306ef..81e19e9fe2 100644 --- a/indra/newview/llviewerwindow.cpp +++ b/indra/newview/llviewerwindow.cpp @@ -5160,6 +5160,83 @@ BOOL LLViewerWindow::rawSnapshot(LLImageRaw *raw, S32 image_width, S32 image_hei return ret; } +BOOL LLViewerWindow::simpleSnapshot(LLImageRaw* raw, S32 image_width, S32 image_height) +{ + gDisplaySwapBuffers = FALSE; + + glClear(GL_DEPTH_BUFFER_BIT | GL_COLOR_BUFFER_BIT | GL_STENCIL_BUFFER_BIT); + setCursor(UI_CURSOR_WAIT); + + BOOL prev_draw_ui = gPipeline.hasRenderDebugFeatureMask(LLPipeline::RENDER_DEBUG_FEATURE_UI) ? TRUE : FALSE; + if (prev_draw_ui != false) + { + LLPipeline::toggleRenderDebugFeature(LLPipeline::RENDER_DEBUG_FEATURE_UI); + } + + LLPipeline::sShowHUDAttachments = FALSE; + LLRect window_rect = getWorldViewRectRaw(); + + S32 original_width = LLPipeline::sRenderDeferred ? gPipeline.mDeferredScreen.getWidth() : gViewerWindow->getWorldViewWidthRaw(); + S32 original_height = LLPipeline::sRenderDeferred ? gPipeline.mDeferredScreen.getHeight() : gViewerWindow->getWorldViewHeightRaw(); + + LLRenderTarget scratch_space; + U32 color_fmt = GL_RGBA; + const bool use_depth_buffer = true; + const bool use_stencil_buffer = true; + if (scratch_space.allocate(image_width, image_height, color_fmt, use_depth_buffer, use_stencil_buffer)) + { + if (gPipeline.allocateScreenBuffer(image_width, image_height)) + { + mWorldViewRectRaw.set(0, image_height, image_width, 0); + + scratch_space.bindTarget(); + } + else + { + scratch_space.release(); + gPipeline.allocateScreenBuffer(original_width, original_height); + } + } + + const U32 subfield = 0; + const bool do_rebuild = true; + const F32 zoom = 1.0; + const bool for_snapshot = TRUE; + display(do_rebuild, zoom, subfield, for_snapshot); + + glReadPixels( + 0, 0, + image_width, + image_height, + GL_RGB, GL_UNSIGNED_BYTE, + raw->getData() + ); + stop_glerror(); + + gDisplaySwapBuffers = FALSE; + gDepthDirty = TRUE; + + if (!gPipeline.hasRenderDebugFeatureMask(LLPipeline::RENDER_DEBUG_FEATURE_UI)) + { + if (prev_draw_ui != false) + { + LLPipeline::toggleRenderDebugFeature(LLPipeline::RENDER_DEBUG_FEATURE_UI); + } + } + + LLPipeline::sShowHUDAttachments = TRUE; + + setCursor(UI_CURSOR_ARROW); + + gPipeline.resetDrawOrders(); + mWorldViewRectRaw = window_rect; + scratch_space.flush(); + scratch_space.release(); + gPipeline.allocateScreenBuffer(original_width, original_height); + + return true; +} + void LLViewerWindow::destroyWindow() { if (mWindow) diff --git a/indra/newview/llviewerwindow.h b/indra/newview/llviewerwindow.h index 8a6df613dc..dd1a32edef 100644 --- a/indra/newview/llviewerwindow.h +++ b/indra/newview/llviewerwindow.h @@ -357,7 +357,10 @@ public: BOOL saveSnapshot(const std::string& filename, S32 image_width, S32 image_height, BOOL show_ui = TRUE, BOOL show_hud = TRUE, BOOL do_rebuild = FALSE, LLSnapshotModel::ESnapshotLayerType type = LLSnapshotModel::SNAPSHOT_TYPE_COLOR, LLSnapshotModel::ESnapshotFormat format = LLSnapshotModel::SNAPSHOT_FORMAT_BMP); BOOL rawSnapshot(LLImageRaw *raw, S32 image_width, S32 image_height, BOOL keep_window_aspect = TRUE, BOOL is_texture = FALSE, BOOL show_ui = TRUE, BOOL show_hud = TRUE, BOOL do_rebuild = FALSE, LLSnapshotModel::ESnapshotLayerType type = LLSnapshotModel::SNAPSHOT_TYPE_COLOR, S32 max_size = MAX_SNAPSHOT_IMAGE_SIZE); - BOOL thumbnailSnapshot(LLImageRaw *raw, S32 preview_width, S32 preview_height, BOOL show_ui, BOOL show_hud, BOOL do_rebuild, LLSnapshotModel::ESnapshotLayerType type); + + BOOL simpleSnapshot(LLImageRaw *raw, S32 image_width, S32 image_height); + + BOOL thumbnailSnapshot(LLImageRaw *raw, S32 preview_width, S32 preview_height, BOOL show_ui, BOOL show_hud, BOOL do_rebuild, LLSnapshotModel::ESnapshotLayerType type); BOOL isSnapshotLocSet() const; void resetSnapshotLoc() const; diff --git a/indra/newview/skins/default/html/common/equirectangular/default.html b/indra/newview/skins/default/html/common/equirectangular/default.html new file mode 100644 index 0000000000..227b306590 --- /dev/null +++ b/indra/newview/skins/default/html/common/equirectangular/default.html @@ -0,0 +1,22 @@ + + + + + + + + \ No newline at end of file diff --git a/indra/newview/skins/default/html/common/equirectangular/eqr_gen.html b/indra/newview/skins/default/html/common/equirectangular/eqr_gen.html new file mode 100644 index 0000000000..855c26c651 --- /dev/null +++ b/indra/newview/skins/default/html/common/equirectangular/eqr_gen.html @@ -0,0 +1,149 @@ + + + + + + + + + + + + + +
UNABLE TO LOAD EQR IMAGE
+
Preview Quality
+ + \ No newline at end of file diff --git a/indra/newview/skins/default/textures/textures.xml b/indra/newview/skins/default/textures/textures.xml index 03878d9fe7..a36b859b6c 100644 --- a/indra/newview/skins/default/textures/textures.xml +++ b/indra/newview/skins/default/textures/textures.xml @@ -131,7 +131,8 @@ with the same filename but different name - + + diff --git a/indra/newview/skins/default/textures/toolbar_icons/360_capture.png b/indra/newview/skins/default/textures/toolbar_icons/360_capture.png new file mode 100644 index 0000000000..163cebe29f Binary files /dev/null and b/indra/newview/skins/default/textures/toolbar_icons/360_capture.png differ diff --git a/indra/newview/skins/default/xui/en/floater_360capture.xml b/indra/newview/skins/default/xui/en/floater_360capture.xml new file mode 100644 index 0000000000..c2959b9853 --- /dev/null +++ b/indra/newview/skins/default/xui/en/floater_360capture.xml @@ -0,0 +1,124 @@ + + + + + Quality level + + + + + + + + + @@ -106,7 +106,7 @@ layout="topleft" left="335" name="btn_restore_defaults" - top="330" + top="385" width="130"> diff --git a/indra/newview/skins/default/xui/en/menu_viewer.xml b/indra/newview/skins/default/xui/en/menu_viewer.xml index 72cce2208f..52afe9164e 100644 --- a/indra/newview/skins/default/xui/en/menu_viewer.xml +++ b/indra/newview/skins/default/xui/en/menu_viewer.xml @@ -733,6 +733,15 @@ function="Floater.Show" parameter="snapshot" /> + + + + + + + + + 360 Capture About land Outfits Complete avatars @@ -4154,6 +4156,8 @@ Try enclosing path to the editor with double quotes. Camera controls Voice settings + Capture a 360 panorama image Information about the land you're visiting Change your avatar Choose a complete avatar diff --git a/indra/newview/viewer_manifest.py b/indra/newview/viewer_manifest.py index 41da8fa328..7e24c1f800 100755 --- a/indra/newview/viewer_manifest.py +++ b/indra/newview/viewer_manifest.py @@ -158,18 +158,12 @@ class ViewerManifest(LLManifest): self.path("*/xui/*/widgets/*.xml") self.path("*/*.xml") - # Local HTML files (e.g. loading screen) - # The claim is that we never use local html files any - # longer. But rather than commenting out this block, let's - # rename every html subdirectory as html.old. That way, if - # we're wrong, a user actually does have the relevant - # files; s/he just needs to rename every html.old - # directory back to html to recover them. - with self.prefix(src="*/html", dst="*/html.old"): - self.path("*.png") - self.path("*/*/*.html") - self.path("*/*/*.gif") - + # Update: 2017-11-01 CP Now we store app code in the html folder + # Initially the HTML/JS code to render equirectangular + # images for the 360 capture feature but more to follow. + with self.prefix(src="*/html", dst="*/html"): + self.path("*/*/*/*.js") + self.path("*/*/*.html") #build_data.json. Standard with exception handling is fine. If we can't open a new file for writing, we have worse problems #platform is computed above with other arg parsing -- cgit v1.2.3