diff options
Diffstat (limited to 'indra')
36 files changed, 1829 insertions, 75 deletions
| 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 a436452461..6f88232c1d 100644 --- a/indra/llplugin/llpluginclassmedia.cpp +++ b/indra/llplugin/llpluginclassmedia.cpp @@ -774,6 +774,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"; @@ -951,6 +960,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 a09145cf50..adbb93f789 100644 --- a/indra/llplugin/llpluginclassmedia.h +++ b/indra/llplugin/llpluginclassmedia.h @@ -153,6 +153,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; }; @@ -213,6 +215,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 11dec63cd3..cd8e5800b8 100644 --- a/indra/media_plugins/cef/media_plugin_cef.cpp +++ b/indra/media_plugins/cef/media_plugin_cef.cpp @@ -62,7 +62,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); @@ -89,6 +89,8 @@ private:  	bool mDisableGPU;  	bool mDisableNetworkService;  	bool mUseMockKeyChain; +	bool mDisableWebSecurity; +	bool mFileAccessFromFileUrls;  	std::string mUserAgentSubtring;  	std::string mAuthUsername;  	std::string mAuthPassword; @@ -124,6 +126,8 @@ MediaPluginBase(host_send_func, host_user_data)  	mDisableGPU = false;  	mDisableNetworkService = true;  	mUseMockKeyChain = true; +	mDisableWebSecurity = false; +	mFileAccessFromFileUrls = false;  	mUserAgentSubtring = "";  	mAuthUsername = "";  	mAuthPassword = ""; @@ -257,13 +261,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);  } @@ -352,14 +357,16 @@ const std::vector<std::string> 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<std::string>(); +		return mPickedFiles;  	}  	return std::vector<std::string>(); @@ -520,7 +527,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)); @@ -559,6 +566,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 @@ -685,6 +705,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"); @@ -880,6 +905,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 1969c498f0..7af405befd 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     ) @@ -2002,7 +2007,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 @@ -2173,7 +2178,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 @@ -2375,7 +2380,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    ) @@ -2409,7 +2414,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"             /> +  <command name="360capture" +         available_in_toybox="true" +         is_flashing_allowed="true" +         icon="Command_360_Capture_Icon" +         label_ref="Command_360_Capture_Label" +         tooltip_ref="Command_360_Capture_Tooltip" +         execute_function="Floater.ToggleOrBringToFront" +         execute_parameters="360capture" +         is_running_function="Floater.IsOpen" +         is_running_parameters="360capture" +           />  </commands> diff --git a/indra/newview/app_settings/settings.xml b/indra/newview/app_settings/settings.xml index eeb7e6f0aa..07dc8a143c 100644 --- a/indra/newview/app_settings/settings.xml +++ b/indra/newview/app_settings/settings.xml @@ -2077,6 +2077,28 @@        <key>Value</key>        <integer>1</integer>      </map> +    <key>BrowserFileAccessFromFileUrls</key> +    <map> +      <key>Comment</key> +      <string>Allow access to local files via file urls in the embedded browser</string> +      <key>Persist</key> +      <integer>1</integer> +      <key>Type</key> +      <string>Boolean</string> +      <key>Value</key> +      <integer>1</integer> +    </map> +    <key>BrowserPluginsEnabled</key> +    <map> +      <key>Comment</key> +      <string>Enable Web plugins in the built-in Web browser?</string> +      <key>Persist</key> +      <integer>1</integer> +      <key>Type</key> +      <string>Boolean</string> +      <key>Value</key> +      <integer>1</integer> +    </map>      <key>ChatBarCustomWidth</key>      <map>        <key>Comment</key> @@ -3541,7 +3563,7 @@        <key>Value</key>        <integer>0</integer>      </map> -    <key>DoubleClickTeleport</key>  +    <key>DoubleClickTeleport</key>      <map>        <key>Comment</key>        <string>Enable double-click to teleport where allowed (afects minimap and people panel)</string> @@ -8873,7 +8895,7 @@      <key>Value</key>      <integer>0</integer>    </map> -   <key>RenderHiDPI</key> +    <key>RenderHiDPI</key>    <map>      <key>Comment</key>      <string>Enable support for HiDPI displays, like Retina (MacOS X ONLY, requires restart)</string> @@ -11595,7 +11617,7 @@              <string>Boolean</string>          <key>Value</key>              <integer>0</integer> -    </map>  +    </map>      <key>NearbyListShowMap</key>      <map>        <key>Comment</key> @@ -16660,30 +16682,94 @@      <string>Boolean</string>      <key>Value</key>      <integer>1</integer> -   </map> -    <key>CefVerboseLog</key> -    <map> -      <key>Comment</key> -      <string>Enable/disable CEF verbose loggingk</string> -      <key>Persist</key> -      <integer>1</integer> -      <key>Type</key> -      <string>Boolean</string> -      <key>Value</key> -      <integer>0</integer> -    </map> -    <key>ResetUIScaleOnFirstRun</key> -    <map> -      <key>Comment</key> -      <string>Resets the UI scale factor on first run due to changed display scaling behavior</string> -      <key>Persist</key> -      <integer>1</integer> -      <key>Type</key> -      <string>Boolean</string> -      <key>Value</key> -      <integer>1</integer>         +  </map> +  <key>360CaptureUseInterestListCap</key> +  <map> +    <key>Comment</key> +    <string>Flag if set, uses the new InterestList cap to ask the simulator for full content</string> +    <key>Persist</key> +    <integer>1</integer> +    <key>Type</key> +    <string>Boolean</string> +    <key>Value</key> +    <integer>1</integer> +  </map> +  <key>360CaptureJPEGEncodeQuality</key> +  <map> +    <key>Comment</key> +    <string>Quality value to use in the JPEG encoder (0..100)</string> +    <key>Persist</key> +    <integer>1</integer> +    <key>Type</key> +    <string>U32</string> +    <key>Value</key> +    <integer>95</integer> +  </map> +  <key>360CaptureDebugSaveImage</key> +  <map> +    <key>Comment</key> +    <string>Flag if set, saves off each cube map as an image, as well as the JavaScript data URL, for debugging purposes</string> +    <key>Persist</key> +    <integer>1</integer> +    <key>Type</key> +    <string>Boolean</string> +    <key>Value</key> +    <integer>0</integer> +  </map> +  <key>360CaptureOutputImageWidth</key> +  <map> +    <key>Comment</key> +    <string>Width of the output 360 equirectangular image</string> +    <key>Persist</key> +    <integer>1</integer> +    <key>Type</key> +    <string>U32</string> +    <key>Value</key> +    <integer>4096</integer> +  </map> +  <key>360CaptureHideAvatars</key> +  <map> +    <key>Comment</key> +    <string>Flag if set, removes all the avatars from the 360 snapshot</string> +    <key>Persist</key> +    <integer>1</integer> +    <key>Type</key> +    <string>Boolean</string> +    <key>Value</key> +    <integer>0</integer>      </map> +  <key>360CaptureCameraFOV</key> +  <map> +    <key>Comment</key> +    <string>Field of view of the WebGL camera that converts the cubemap to an equirectangular image</string> +    <key>Persist</key> +    <integer>1</integer> +    <key>Type</key> +    <string>U32</string> +    <key>Value</key> +    <integer>75</integer> +  </map> +  <key>360CaptureNumRenderPasses</key> +  <map> +    <key>Comment</key> +    <string>Number of times to render the scene while taking a snapshot</string> +    <key>Persist</key> +    <integer>1</integer> +    <key>Type</key> +    <string>U32</string> +    <key>Value</key> +    <integer>3</integer> +  </map> +  <key>ResetUIScaleOnFirstRun</key> +  <map> +  <key>Comment</key> +  <string>Resets the UI scale factor on first run due to changed display scaling behavior</string> +  <key>Persist</key> +  <integer>1</integer> +  <key>Type</key> +  <string>Boolean</string> +  <key>Value</key> +  <integer>1</integer> +  </map>  </map>  </llsd> - - diff --git a/indra/newview/llagent.cpp b/indra/newview/llagent.cpp index 389448654a..1e7711e577 100644 --- a/indra/newview/llagent.cpp +++ b/indra/newview/llagent.cpp @@ -2895,7 +2895,7 @@ bool LLAgent::requestGetCapability(const std::string &capName, httpCallback_t cb  {      std::string url; -    url = getRegion()->getCapability(capName); +    url = getRegionCapability(capName);      if (url.empty())      { diff --git a/indra/newview/llappviewer.cpp b/indra/newview/llappviewer.cpp index f668dc754d..fb016bbb5a 100644 --- a/indra/newview/llappviewer.cpp +++ b/indra/newview/llappviewer.cpp @@ -608,10 +608,10 @@ static void settings_to_globals()  static void settings_modify()  { -	LLRenderTarget::sUseFBO				= gSavedSettings.getBOOL("RenderDeferred");  	LLPipeline::sRenderTransparentWater	= gSavedSettings.getBOOL("RenderTransparentWater");  	LLPipeline::sRenderBump				= gSavedSettings.getBOOL("RenderObjectBump"); -	LLPipeline::sRenderDeferred		= LLPipeline::sRenderTransparentWater && LLPipeline::sRenderBump && gSavedSettings.getBOOL("RenderDeferred"); +	LLPipeline::sRenderDeferred = LLPipeline::sRenderTransparentWater && LLPipeline::sRenderBump && gSavedSettings.getBOOL("RenderDeferred"); +    LLRenderTarget::sUseFBO = LLPipeline::sRenderDeferred && gSavedSettings.getBOOL("RenderAvatarVP");  	LLVOSurfacePatch::sLODFactor		= gSavedSettings.getF32("RenderTerrainLODFactor");  	LLVOSurfacePatch::sLODFactor *= LLVOSurfacePatch::sLODFactor; //square lod factor to get exponential range of [1,4]  	gDebugGL = gSavedSettings.getBOOL("RenderDebugGL") || gDebugSession; diff --git a/indra/newview/lldrawpoolavatar.cpp b/indra/newview/lldrawpoolavatar.cpp index 687b13d2c8..eb0bd0de4e 100644 --- a/indra/newview/lldrawpoolavatar.cpp +++ b/indra/newview/lldrawpoolavatar.cpp @@ -1526,7 +1526,7 @@ void LLDrawPoolAvatar::renderAvatars(LLVOAvatar* single_avatar, S32 pass)  	}  	LLVOAvatar *attached_av = avatarp->getAttachedAvatar(); -	if (attached_av && LLVOAvatar::AOA_NORMAL != attached_av->getOverallAppearance()) +	if (attached_av && (LLVOAvatar::AOA_NORMAL != attached_av->getOverallAppearance() || !gPipeline.hasRenderType(LLPipeline::RENDER_TYPE_AVATAR)))  	{  		// Animesh attachment of a jellydolled or invisible parent - don't show  		return; diff --git a/indra/newview/llfloater360capture.cpp b/indra/newview/llfloater360capture.cpp new file mode 100644 index 0000000000..745f154535 --- /dev/null +++ b/indra/newview/llfloater360capture.cpp @@ -0,0 +1,930 @@ +/** + * @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 <iterator> + +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<LLUICtrl>("capture_button"); +    mCaptureBtn->setCommitCallback(boost::bind(&LLFloater360Capture::onCapture360ImagesBtn, this)); + +    mSaveLocalBtn = getChild<LLUICtrl>("save_local_button"); +    mSaveLocalBtn->setCommitCallback(boost::bind(&LLFloater360Capture::onSaveLocalBtn, this)); +    mSaveLocalBtn->setEnabled(false); + +    mWebBrowser = getChild<LLMediaCtrl>("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<LLRadioGroup>("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(); + +        // It's not possible (as I had hoped) to always render to an off screen +        // buffer regardless of deferred rendering status so the next best +        // option is to render to a buffer that is the size of the users app +        // window.  Note, this was changed - before it chose the smallest +        // power of 2 less than the window size - but since that meant a +        // 1023 height app window would result in a 512 pixel capture, Maxim +        // tried this and it does indeed appear to work.  Mayb need to revisit +        // after the project viewer pass if people on low end graphics systems +        // after having issues. +        if (mSourceImageSize > window_width || mSourceImageSize > window_height) +        { +            mSourceImageSize = llmin(window_width, window_height, mSourceImageSize); +            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 ought 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) +{ +    llofstream 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) +{ +    llofstream 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); + +    llofstream 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<LLImageRaw> 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<LLImageJPEG> 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); + +    bool render_attached_lights = LLPipeline::sRenderAttachedLights; +    // 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); +        LLPipeline::toggleRenderTypeControl(LLPipeline::RENDER_TYPE_PARTICLES); +        LLPipeline::sRenderAttachedLights = FALSE; +    } + +    // 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; + +    // set new parameters specific to the 360 requirements +    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", +    }; + +    // number of times to render the scene (display(..) inside +    // the simple snapshot function in llViewerWindow. +    // Note: rendering even just 1 more time (for a total of 2) +    // has a dramatic effect on the scene contents and *much* +    // less of it is missing. More investigation required +    // but for the moment, this helps with missing content +    // because of interest list issues. +    int num_render_passes = gSavedSettings.getU32("360CaptureNumRenderPasses"); + +    // 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, num_render_passes); + +        // 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<std::chrono::duration<double>>(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); +        LLPipeline::toggleRenderTypeControl(LLPipeline::RENDER_TYPE_PARTICLES); +        LLPipeline::sRenderAttachedLights = render_attached_lights; +    } + +    // 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); + +    // page is loaded and ready so we can turn on the buttons again +    mCaptureBtn->setEnabled(true); +    mSaveLocalBtn->setEnabled(true); + +    // allow the UI to update by suspending and waiting for the +    // main render loop to update the UI +    suspendForAFrame(); +} + +// 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<LLCharacter*>::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<LLImageRaw> raw_image, const std::string filename, const std::string prefix); + +        std::vector<LLAnimPauseRequest> 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<LLImageRaw> 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<LLUICtrl>("image_res_text")->setVisible(mAdvanced);  	floaterp->getChild<LLUICtrl>("file_size_label")->setVisible(mAdvanced); +    if (floaterp->hasChild("360_label", TRUE)) +    {  +        floaterp->getChild<LLUICtrl>("360_label")->setVisible(mAdvanced); +    }  	if(!floaterp->isMinimized())  	{  		floaterp->reshape(floater_width, floaterp->getRect().getHeight()); @@ -992,6 +996,10 @@ BOOL LLFloaterSnapshot::postBuild()      getChild<LLButton>("retract_btn")->setCommitCallback(boost::bind(&LLFloaterSnapshot::onExtendFloater, this));      getChild<LLButton>("extend_btn")->setCommitCallback(boost::bind(&LLFloaterSnapshot::onExtendFloater, this)); +    getChild<LLTextBox>("360_label")->setSoundFlags(LLView::MOUSE_UP); +    getChild<LLTextBox>("360_label")->setShowCursorHand(false); +    getChild<LLTextBox>("360_label")->setClickedCallback(boost::bind(&LLFloaterSnapshot::on360Snapshot, this)); +  	// Filters  	LLComboBox* filterbox = getChild<LLComboBox>("filters_combobox");  	std::vector<std::string> 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/llviewercontrol.cpp b/indra/newview/llviewercontrol.cpp index 89c600fc2c..bc425123e1 100644 --- a/indra/newview/llviewercontrol.cpp +++ b/indra/newview/llviewercontrol.cpp @@ -162,6 +162,17 @@ static bool handleSetShaderChanged(const LLSD& newvalue)  	return true;  } +static bool handleAvatarVPChanged(const LLSD& newvalue) +{ +    LLRenderTarget::sUseFBO = newvalue.asBoolean() +                                && gSavedSettings.getBOOL("RenderObjectBump") +                                && gSavedSettings.getBOOL("RenderTransparentWater") +                                && gSavedSettings.getBOOL("RenderDeferred"); + +    handleSetShaderChanged(LLSD()); +    return true; +} +  static bool handleRenderPerfTestChanged(const LLSD& newvalue)  {         bool status = !newvalue.asBoolean(); @@ -201,7 +212,10 @@ static bool handleRenderPerfTestChanged(const LLSD& newvalue)  bool handleRenderTransparentWaterChanged(const LLSD& newvalue)  { -	LLRenderTarget::sUseFBO = newvalue.asBoolean(); +    LLRenderTarget::sUseFBO = newvalue.asBoolean()  +                                && gSavedSettings.getBOOL("RenderObjectBump")  +                                && gSavedSettings.getBOOL("RenderAvatarVP")  +                                && gSavedSettings.getBOOL("RenderDeferred");  	if (gPipeline.isInit())  	{  		gPipeline.updateRenderTransparentWater(); @@ -454,7 +468,10 @@ static bool handleRenderDeferredChanged(const LLSD& newvalue)  //  static bool handleRenderBumpChanged(const LLSD& newval)  { -	LLRenderTarget::sUseFBO = newval.asBoolean(); +    LLRenderTarget::sUseFBO = newval.asBoolean()  +                                && gSavedSettings.getBOOL("RenderTransparentWater")  +                                && gSavedSettings.getBOOL("RenderAvatarVP") +                                && gSavedSettings.getBOOL("RenderDeferred");  	if (gPipeline.isInit())  	{  		gPipeline.updateRenderBump(); @@ -648,7 +665,7 @@ void settings_setup_listeners()  	gSavedSettings.getControl("OctreeAttachmentSizeFactor")->getSignal()->connect(boost::bind(&handleRepartition, _2));  	gSavedSettings.getControl("RenderMaxTextureIndex")->getSignal()->connect(boost::bind(&handleSetShaderChanged, _2));  	gSavedSettings.getControl("RenderUseTriStrips")->getSignal()->connect(boost::bind(&handleResetVertexBuffersChanged, _2)); -	gSavedSettings.getControl("RenderAvatarVP")->getSignal()->connect(boost::bind(&handleSetShaderChanged, _2)); +	gSavedSettings.getControl("RenderAvatarVP")->getSignal()->connect(boost::bind(&handleAvatarVPChanged, _2));  	gSavedSettings.getControl("RenderUIBuffer")->getSignal()->connect(boost::bind(&handleWindowResized, _2));  	gSavedSettings.getControl("RenderDepthOfField")->getSignal()->connect(boost::bind(&handleReleaseGLBufferChanged, _2));  	gSavedSettings.getControl("RenderFSAASamples")->getSignal()->connect(boost::bind(&handleReleaseGLBufferChanged, _2)); 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<LLFloater360Capture>);  	LLFloaterReg::add("block_timers", "floater_fast_timers.xml", (LLFloaterBuildFunc)&LLFloaterReg::build<LLFastTimerView>);  	LLFloaterReg::add("about_land", "floater_about_land.xml", (LLFloaterBuildFunc)&LLFloaterReg::build<LLFloaterLand>);  	LLFloaterReg::add("add_payment_method", "floater_add_payment_method.xml", (LLFloaterBuildFunc)&LLFloaterReg::build<LLFloaterAddPaymentMethod>); diff --git a/indra/newview/llviewermedia.cpp b/indra/newview/llviewermedia.cpp index d0cf8ea407..54c2099ac9 100644 --- a/indra/newview/llviewermedia.cpp +++ b/indra/newview/llviewermedia.cpp @@ -1752,8 +1752,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) @@ -1915,6 +1923,15 @@ void LLViewerMediaImpl::loadURI()  }  ////////////////////////////////////////////////////////////////////////////////////////// +void LLViewerMediaImpl::executeJavaScript(const std::string& code) +{ +	if (mMediaSource) +	{ +		mMediaSource->executeJavaScript(code); +	} +} + +//////////////////////////////////////////////////////////////////////////////////////////  void LLViewerMediaImpl::setSize(int width, int height)  {  	mMediaWidth = width; @@ -3219,8 +3236,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 635b731193..01a4bd587d 100644 --- a/indra/newview/llviewermenu.cpp +++ b/indra/newview/llviewermenu.cpp @@ -1273,12 +1273,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) @@ -2319,11 +2362,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;  	} @@ -9203,6 +9245,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 9d85586dae..b4db57b141 100644 --- a/indra/newview/llviewermessage.cpp +++ b/indra/newview/llviewermessage.cpp @@ -3746,6 +3746,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 198fe1563c..0f409701d1 100644 --- a/indra/newview/llviewerregion.cpp +++ b/indra/newview/llviewerregion.cpp @@ -2968,6 +2968,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 c20021d4c7..92e8f8026d 100644 --- a/indra/newview/llviewerwindow.cpp +++ b/indra/newview/llviewerwindow.cpp @@ -5170,6 +5170,104 @@ BOOL LLViewerWindow::rawSnapshot(LLImageRaw *raw, S32 image_width, S32 image_hei  	return ret;  } +BOOL LLViewerWindow::simpleSnapshot(LLImageRaw* raw, S32 image_width, S32 image_height, const int num_render_passes) +{ +    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); +        } +    } + +    // we render the scene more than once since this helps +    // greatly with the objects not being drawn in the  +    // snapshot when they are drawn in the scene. This is  +    // evident when you set this value via the debug setting +    // called 360CaptureNumRenderPasses to 1. The theory is +    // that the missing objects are caused by the sUseOcclusion +    // property in pipeline but that the use in pipeline.cpp +    // lags by a frame or two so rendering more than once +    // appears to help a lot. +    for (int i = 0; i < num_render_passes; ++i) +    { +        // turning this flag off here prohibits the screen swap +        // to present the new page to the viewer - this stops +        // the black flash in between captures when the number +        // of render passes is more than 1. We need to also +        // set it here because code in LLViewerDisplay resets +        // it to TRUE each time. +        gDisplaySwapBuffers = FALSE; + +        // actually render the scene +        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..713cc51bf7 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, const int num_render_passes); + +    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 @@ +<html> +<head> +<style> +body { +  background-color:#000; +  background-image: linear-gradient(white 2px, transparent 2px), +  linear-gradient(90deg, white 2px, transparent 2px), +  linear-gradient(rgba(255,255,255,.3) 1px, transparent 1px), +  linear-gradient(90deg, rgba(255,255,255,.3) 1px, transparent 1px); +  background-size: 100px 100px, 100px 100px, 20px 20px, 20px 20px; +  background-position:-2px -2px, -2px -2px, -1px -1px, -1px -1px; +} +</style> +</head> +<body> +<script> +function start() { +} +document.addEventListener('DOMContentLoaded', start); +</script> +</body> +</html>
\ 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..2675d37727 --- /dev/null +++ b/indra/newview/skins/default/html/common/equirectangular/eqr_gen.html @@ -0,0 +1,150 @@ +<!DOCTYPE html> +<html lang="en"> +<head> +    <meta charset="utf-8"> +    <meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0"> +    <style> +        body { +            background: #333; +            padding: 0; +            margin: 0; +            overflow: hidden; +        } +        #error_message { +            z-index: 2; +            background-color: #aa3333; +            overflow: hidden; +            display:  none; +            pointer-events:none; +            font-family: monospace; +            font-size: 3em; +            color: white; +            border-radius: 1em; +            padding: 1em; +            position: absolute; +            top: 50%; +            left: 50%; +            margin-right: -50%; +            transform: translate(-50%, -50%) +        } +        #quality_window { +            user-select: none; +            z-index: 100; +            position:  absolute; +            left: 8px; +            top: 8px; +            width: auto; +            border-radius: 16px; +            height: auto; +            font-size: 1.5em; +            text-align: center; +            font-family: monospace; +            background-color: rgba(200,200,200,0.35); +            color: #000; +            padding-left: 16px; +            padding-right: 16px; +            padding-top: 8px; +            padding-bottom: 8px; +        } +    </style> +</head> +<body> +    <script src="js/three.min.js"></script> +    <script src="js/OrbitControls.js"></script> +    <script src="js/jpeg_encoder_basic.js" type="text/javascript"></script> +    <script src="js/CubemapToEquirectangular.js"></script> +    <script> +        var controls, camera, scene, renderer, equiManaged; + +        function init(eqr_width, eqr_height, img_path, camera_fov, initial_heading, overlay_label) { + +            camera = new THREE.PerspectiveCamera(camera_fov, window.innerWidth / window.innerHeight, 0.1, 100); +            camera.position.x = 0.01; + +            scene = new THREE.Scene(); + +            renderer = new THREE.WebGLRenderer(); +            renderer.autoClear = false; +            renderer.setPixelRatio(window.devicePixelRatio); +            renderer.setSize(window.innerWidth, window.innerHeight); + +            var cubemap_img_js_url = img_path + '/cubemap_img.js'; +            var cubemap_image_js = document.createElement('script'); +            cubemap_image_js.setAttribute('type', 'text/javascript'); +            cubemap_image_js.setAttribute('src', cubemap_img_js_url); +            document.getElementsByTagName('head')[0].appendChild(cubemap_image_js); +            cubemap_image_js.onload = function () { +                document.getElementById("error_message").style.display = 'none'  +                scene.background = new THREE.CubeTextureLoader().load(cubemap_img_js); +                equiManaged = new CubemapToEquirectangular(renderer, true, eqr_width, eqr_height); +            }; +            cubemap_image_js.onerror = function () { +                document.getElementById("error_message").style.display = 'inline-block'  +            }; + +            document.body.appendChild(renderer.domElement); +            window.addEventListener('resize', onWindowResize, false); + +            controls = new THREE.OrbitControls(camera, renderer.domElement); +            controls.autoRotate = true; +            controls.autoRotateSpeed = 0.2; +            controls.enableZoom = false; +            controls.enablePan = false; +            controls.enableDamping = true; +            controls.dampingFactor = 0.15; +            controls.rotateSpeed = -0.5; + +            // initial direction the camera faces +            // We cannot edit camera rotation directly as the OrbitControls will +            // immediately reset it so we need some math to tell the controls +            // there to look at initially. Note there is also an offset of π/2 since +            // the Viewer and three.js have slightly different coordinate systems  +            var spherical_target = new THREE.Spherical(1, Math.PI / 2, initial_heading + Math.PI / 2) +            var target = new THREE.Vector3().setFromSpherical(spherical_target)  +            camera.position.set(target.x, target.y, target.z); +            controls.update(); +            controls.saveState(); + +            // update the text that gets passed in from the C++ app for +            // the translucent overlay label that tells us what we are seeing +            document.getElementById('quality_window').innerHTML = overlay_label; + +            animate(); +        } + +        window.addEventListener( +            'mousedown', +            function (event) { +                controls.autoRotate = false; +            }, +            false +        ); + +        window.addEventListener( +            'dblclick', +            function (event) { +                controls.autoRotate = true; +            }, +            false +        ); + +        function saveAsEqrImage(filename, xmp_details) { +            equiManaged.update(camera, scene, filename, xmp_details); +        } + +        function onWindowResize() { +            camera.aspect = window.innerWidth / window.innerHeight; +            camera.updateProjectionMatrix(); +            renderer.setSize(window.innerWidth, window.innerHeight); +        } + +        function animate() { +            requestAnimationFrame(animate); +            controls.update(); +            renderer.render(scene, camera); +        } +    </script> +    <div id="error_message">UNABLE TO LOAD EQR IMAGE</div> +    <div id="quality_window">Preview Quality</div> +</body> +</html>
\ 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    <texture name="Check_Mark" file_name="icons/check_mark.png" preload="true" />    <texture name="Checker" file_name="checker.png" preload="false" /> -   + +  <texture name="Command_360_Capture_Icon"  file_name="toolbar_icons/360_capture.png"  preload="true" />    <texture name="Command_AboutLand_Icon"    file_name="toolbar_icons/land.png"         preload="true" />    <texture name="Command_Appearance_Icon"   file_name="toolbar_icons/appearance.png"   preload="true" />    <texture name="Command_Avatar_Icon"       file_name="toolbar_icons/avatars.png"      preload="true" /> diff --git a/indra/newview/skins/default/textures/toolbar_icons/360_capture.png b/indra/newview/skins/default/textures/toolbar_icons/360_capture.pngBinary files differ new file mode 100644 index 0000000000..163cebe29f --- /dev/null +++ b/indra/newview/skins/default/textures/toolbar_icons/360_capture.png 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..c89489d145 --- /dev/null +++ b/indra/newview/skins/default/xui/en/floater_360capture.xml @@ -0,0 +1,135 @@ +<?xml version="1.0" encoding="UTF-8"?> +<floater can_resize="true" +         height="400" +         layout="topleft" +         min_height="300" +         min_width="400" +         name="360capture" +         help_topic="360capture" +         save_rect="true" +         title="360 SNAPSHOT" +         width="800"> +  <panel layout="topleft" +       background_visible="true" +       top="0" +       follows="left|bottom|top" +       left="0" +       width="200" +       bg_opaque_color="0.195 0.195 0.195 1" +       background_opaque="true" +       height="400" +       name="ui_panel_left"> +    <text +       follows="top|left|right" +       height="16" +       layout="topleft" +       left="10" +       top="10" +       width="100"> +      Quality level +    </text> +    <radio_group +      control_name="360QualitySelection" +      follows="left|top" +      height="94" +      layout="topleft" +      left_delta="20" +      name="360_quality_selection" +      top_pad="0" +      width="180"> +      <radio_item +        height="20" +        label="Preview (fast)" +        layout="topleft" +        left="0" +        name="preview_quality" +        value="128" +        tool_tip="Preview quality" +        top="0" +        width="100" /> +      <radio_item +         height="20" +         label="Medium" +         layout="topleft" +         left_delta="0" +         name="medium_quality" +         value="512" +         tool_tip="Medium quality" +         top_delta="20" +         width="100" /> +      <radio_item +         height="20" +         label="High" +         layout="topleft" +         left_delta="0" +         name="high_quality" +         value="1024" +         tool_tip="High quality" +         top_delta="20" +         width="100" /> +      <radio_item +         height="20" +         label="Maximum" +         layout="topleft" +         left_delta="0" +         name="maximum_quality" +         value="2048" +         tool_tip="Maximum quality" +         top_delta="20" +         width="100" /> +    </radio_group> +    <check_box control_name="360CaptureHideAvatars" +               follows="left|top" +               height="15" +               label="Hide all avatars" +               layout="left" +               left="10" +               name="360_hide_avatar" +               top_delta="0" +               width="100"/> +    <button follows="left|top" +            height="20" +            label="Create 360 image" +            layout="topleft" +            left="10" +            name="capture_button" +            top_delta="32" +            width="180" /> +    <button follows="left|top" +            height="20" +            label="Save as..." +            layout="topleft" +            left="10" +            name="save_local_button" +            top_delta="35" +            width="180" /> +  </panel> +  <panel layout="topleft" +     background_visible="true" +     top="0" +     follows="all" +     left="200" +     width="600" +     bg_opaque_color="0.195 0.195 0.195 1" +     background_opaque="true" +     height="400" +     name="ui_panel_right"> +    <web_browser top="10" +                 follows="all" +                 bg_opaque_color="0.225 0.225 0.225 1" +                 left="0" +                 width="590" +                 height="368" +                 name="360capture_contents" +                 trusted_content="true" /> +    <text follows="bottom" +          layout="topleft" +          name="statusbar" +          height="17" +          left="0" +          top="382" +          width="590"> +      Click and drag on the image to pan +    </text> +  </panel> +</floater>
\ No newline at end of file diff --git a/indra/newview/skins/default/xui/en/floater_snapshot.xml b/indra/newview/skins/default/xui/en/floater_snapshot.xml index 832c2ee7da..f441e3cbd7 100644 --- a/indra/newview/skins/default/xui/en/floater_snapshot.xml +++ b/indra/newview/skins/default/xui/en/floater_snapshot.xml @@ -400,7 +400,7 @@     layout="topleft"     name="img_info_border"     top_pad="0" -   right="-10" +   right="-130"     follows="left|top|right"     left_delta="0"/>     <text @@ -411,11 +411,10 @@      height="14"      layout="topleft"      left="220" -	right="-20"      halign="left"      name="image_res_text"      top_delta="5" -    width="200"> +    width="250">         [WIDTH]px (width) x [HEIGHT]px (height)     </text>     <text @@ -423,7 +422,7 @@      font="SansSerifSmall"      height="14"      layout="topleft" -    left="-65" +    left="-185"      length="1"      halign="right"      name="file_size_label" @@ -432,4 +431,19 @@      width="50">         [SIZE] KB     </text> +   <text +    follows="right|top" +    font="SansSerifSmall" +    height="14" +    layout="topleft" +    left="-130" +    length="1" +    halign="right" +    name="360_label" +    text_color="0.3 0.82 1 1" +    top_delta="0" +    type="string" +    width="115"> +    Take a 360 snapshot +   </text>  </floater> diff --git a/indra/newview/skins/default/xui/en/floater_toybox.xml b/indra/newview/skins/default/xui/en/floater_toybox.xml index bc19d6e79f..bdc04a8a78 100644 --- a/indra/newview/skins/default/xui/en/floater_toybox.xml +++ b/indra/newview/skins/default/xui/en/floater_toybox.xml @@ -4,7 +4,7 @@    can_dock="false"    can_minimize="false"    can_resize="false" -  height="375" +  height="430"    help_topic="toybox"    layout="topleft"    legacy_header_height="18" @@ -45,7 +45,7 @@        Buttons will appear as shown or as icon-only depending on each toolbar's settings.    </text>    <toolbar -    bottom="310" +    bottom="365"      button_display_mode="icons_with_text"      follows="all"      left="20" @@ -81,11 +81,11 @@    <panel      bevel_style="none"      border="true" -    bottom="311" +    bottom="366"      follows="left|bottom|right"      left="20"      right="-20" -    top="311" /> +    top="366" />    <button      follows="left|bottom|right"      height="23" @@ -94,7 +94,7 @@      layout="topleft"      left="185"      name="btn_clear_all" -    top="330" +    top="385"      width="130">      <button.commit_callback function="Toybox.ClearAll" />    </button> @@ -106,7 +106,7 @@      layout="topleft"      left="335"      name="btn_restore_defaults" -    top="330" +    top="385"      width="130">      <button.commit_callback function="Toybox.RestoreDefaults" />    </button> diff --git a/indra/newview/skins/default/xui/en/menu_viewer.xml b/indra/newview/skins/default/xui/en/menu_viewer.xml index 72cce2208f..24d06722b9 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" />          </menu_item_call> +         +<menu_item_call +         label="360 snapshot" +         name="Capture 360" +         shortcut="control|alt|shift|s"> +            <menu_item_call.on_click +             function="Floater.Show" +             parameter="360capture" /> +        </menu_item_call>          <menu_item_separator/>          <menu_item_call           label="Place profile" @@ -3427,6 +3436,14 @@ function="World.EnvPreset"                  <menu_item_call.on_click                   function="Advanced.DumpRegionObjectCache" />              </menu_item_call> +         +<menu_item_call +             label="Interest List: Full Update" +             name="Interest List: Full Update" +             shortcut="alt|shift|I"> +                <menu_item_call.on_click +                 function="Advanced.InterestListFullUpdate" /> +            </menu_item_call>          </menu>          <menu           create_jump_keys="true" diff --git a/indra/newview/skins/default/xui/en/strings.xml b/indra/newview/skins/default/xui/en/strings.xml index d115e09d5b..db813a56b1 100644 --- a/indra/newview/skins/default/xui/en/strings.xml +++ b/indra/newview/skins/default/xui/en/strings.xml @@ -4124,6 +4124,8 @@ Try enclosing path to the editor with double quotes.    <!-- commands --> +  <string  +name="Command_360_Capture_Label">360 snapshot</string>    <string name="Command_AboutLand_Label">About land</string>    <string name="Command_Appearance_Label">Outfits</string>    <string name="Command_Avatar_Label">Complete avatars</string> @@ -4154,6 +4156,8 @@ Try enclosing path to the editor with double quotes.    <string name="Command_View_Label">Camera controls</string>    <string name="Command_Voice_Label">Voice settings</string> +  <string  +name="Command_360_Capture_Tooltip">Capture a 360 equirectangular image</string>    <string name="Command_AboutLand_Tooltip">Information about the land you're visiting</string>    <string name="Command_Appearance_Tooltip">Change your avatar</string>    <string name="Command_Avatar_Tooltip">Choose a complete avatar</string> diff --git a/indra/newview/viewer_manifest.py b/indra/newview/viewer_manifest.py index 6fcd1e84e8..b6d62a0d5f 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 | 
