DRTVWR-534: Batch of modifications to 360 capture project after moving from internal repo to public one.

master
Callum Prentice 2021-08-20 08:28:48 -07:00
parent c40b8310b0
commit 0c89e67eb5
34 changed files with 1892 additions and 68 deletions

1
.gitignore vendored
View File

@ -56,6 +56,7 @@ indra/newview/search_history.txt
indra/newview/teleport_history.txt
indra/newview/typed_locations.txt
indra/newview/vivox-runtime
indra/newview/skins/default/html/common/equirectangular/js
indra/server-linux-*
indra/temp
indra/test/linden_file.dat

View File

@ -367,6 +367,58 @@
<key>version</key>
<string>2.3.545362</string>
</map>
<key>cubemaptoequirectangular</key>
<map>
<key>copyright</key>
<string>Copyright (c) 2017 Jaume Sanchez Elias, http://www.clicktorelease.com</string>
<key>license</key>
<string>MIT</string>
<key>license_file</key>
<string>LICENSES/CUBEMAPTOEQUIRECTANGULAR_LICENSE.txt</string>
<key>name</key>
<string>cubemaptoequirectangular</string>
<key>platforms</key>
<map>
<key>darwin64</key>
<map>
<key>archive</key>
<map>
<key>hash</key>
<string>e6da43ea831b2416a5a8b388a511e70b</string>
<key>url</key>
<string>https://automated-builds-secondlife-com.s3.amazonaws.com/ct2/85446/792080/cubemaptoequirectangular-1.1.0-darwin64-562268.tar.bz2</string>
</map>
<key>name</key>
<string>darwin64</string>
</map>
<key>windows</key>
<map>
<key>archive</key>
<map>
<key>hash</key>
<string>f26636666a056cacea0618bc2648d935</string>
<key>url</key>
<string>https://automated-builds-secondlife-com.s3.amazonaws.com/ct2/85451/792109/cubemaptoequirectangular-1.1.0-windows-562268.tar.bz2</string>
</map>
<key>name</key>
<string>windows</string>
</map>
<key>windows64</key>
<map>
<key>archive</key>
<map>
<key>hash</key>
<string>a59ba299bc94e915a8b55e9eef681253</string>
<key>url</key>
<string>https://automated-builds-secondlife-com.s3.amazonaws.com/ct2/85449/792108/cubemaptoequirectangular-1.1.0-windows64-562268.tar.bz2</string>
</map>
<key>name</key>
<string>windows64</string>
</map>
</map>
<key>version</key>
<string>1.1.0</string>
</map>
<key>curl</key>
<map>
<key>copyright</key>
@ -1491,6 +1543,58 @@
<key>version</key>
<string>2012.1-2</string>
</map>
<key>jpegencoderbasic</key>
<map>
<key>copyright</key>
<string>Andreas Ritter, www.bytestrom.eu, 11/2009</string>
<key>license</key>
<string>NONE</string>
<key>license_file</key>
<string>LICENSES/JPEG_ENCODER_BASIC_LICENSE.txt</string>
<key>name</key>
<string>jpegencoderbasic</string>
<key>platforms</key>
<map>
<key>darwin64</key>
<map>
<key>archive</key>
<map>
<key>hash</key>
<string>d07453bf10bae71013209874477640bb</string>
<key>url</key>
<string>https://automated-builds-secondlife-com.s3.amazonaws.com/ct2/85447/792085/jpegencoderbasic-1.0-darwin64-562269.tar.bz2</string>
</map>
<key>name</key>
<string>darwin64</string>
</map>
<key>windows</key>
<map>
<key>archive</key>
<map>
<key>hash</key>
<string>e422fee0c82507ec218597654f8cadbf</string>
<key>url</key>
<string>https://automated-builds-secondlife-com.s3.amazonaws.com/ct2/85450/792107/jpegencoderbasic-1.0-windows-562269.tar.bz2</string>
</map>
<key>name</key>
<string>windows</string>
</map>
<key>windows64</key>
<map>
<key>archive</key>
<map>
<key>hash</key>
<string>fbe07040cecc4e8760acc2cdf2436468</string>
<key>url</key>
<string>https://automated-builds-secondlife-com.s3.amazonaws.com/ct2/85448/792106/jpegencoderbasic-1.0-windows64-562269.tar.bz2</string>
</map>
<key>name</key>
<string>windows64</string>
</map>
</map>
<key>version</key>
<string>1.0</string>
</map>
<key>jpeglib</key>
<map>
<key>copyright</key>
@ -3067,6 +3171,58 @@ Copyright (c) 2012, 2014, 2015, 2016 nghttp2 contributors</string>
<key>version</key>
<string>4.10.0000.32327.5fc3fe7c.539691</string>
</map>
<key>threejs</key>
<map>
<key>copyright</key>
<string>Copyright © 2010-2021 three.js authors</string>
<key>license</key>
<string>MIT</string>
<key>license_file</key>
<string>LICENSES/THREEJS_LICENSE.txt</string>
<key>name</key>
<string>threejs</string>
<key>platforms</key>
<map>
<key>darwin64</key>
<map>
<key>archive</key>
<map>
<key>hash</key>
<string>313a852ddc87be24235319987a42f0f2</string>
<key>url</key>
<string>https://automated-builds-secondlife-com.s3.amazonaws.com/ct2/86208/797009/threejs-0.131.3-darwin64-562835.tar.bz2</string>
</map>
<key>name</key>
<string>darwin64</string>
</map>
<key>windows</key>
<map>
<key>archive</key>
<map>
<key>hash</key>
<string>850b2400f91e7704653fe3fd4171ce94</string>
<key>url</key>
<string>https://automated-builds-secondlife-com.s3.amazonaws.com/ct2/86210/797031/threejs-0.131.3-windows-562835.tar.bz2</string>
</map>
<key>name</key>
<string>windows</string>
</map>
<key>windows64</key>
<map>
<key>archive</key>
<map>
<key>hash</key>
<string>821f398169c1743987ac1b99376f767f</string>
<key>url</key>
<string>https://automated-builds-secondlife-com.s3.amazonaws.com/ct2/86209/797030/threejs-0.131.3-windows64-562835.tar.bz2</string>
</map>
<key>name</key>
<string>windows64</string>
</map>
</map>
<key>version</key>
<string>0.131.3</string>
</map>
<key>tut</key>
<map>
<key>copyright</key>

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -714,6 +714,15 @@ void LLPluginClassMedia::loadURI(const std::string &uri)
sendMessage(message);
}
void LLPluginClassMedia::executeJavaScript(const std::string &code)
{
LLPluginMessage message(LLPLUGIN_MESSAGE_CLASS_MEDIA, "execute_javascript");
message.setValue("code", code);
sendMessage(message);
}
const char* LLPluginClassMedia::priorityToString(EPriority priority)
{
const char* result = "UNKNOWN";
@ -891,6 +900,19 @@ void LLPluginClassMedia::setJavascriptEnabled(const bool enabled)
sendMessage(message);
}
void LLPluginClassMedia::setWebSecurityDisabled(const bool disabled)
{
LLPluginMessage message(LLPLUGIN_MESSAGE_CLASS_MEDIA_BROWSER, "web_security_disabled");
message.setValueBoolean("disabled", disabled);
sendMessage(message);
}
void LLPluginClassMedia::setFileAccessFromFileUrlsEnabled(const bool enabled)
{
LLPluginMessage message(LLPLUGIN_MESSAGE_CLASS_MEDIA_BROWSER, "file_access_from_file_urls");
message.setValueBoolean("enabled", enabled);
sendMessage(message);
}
void LLPluginClassMedia::enableMediaPluginDebugging( bool enable )
{

View File

@ -139,6 +139,8 @@ public:
void loadURI(const std::string &uri);
void executeJavaScript(const std::string &code);
// "Loading" means uninitialized or any state prior to fully running (processing commands)
bool isPluginLoading(void) { return mPlugin?mPlugin->isLoading():false; };
@ -199,6 +201,8 @@ public:
void setLanguageCode(const std::string &language_code);
void setPluginsEnabled(const bool enabled);
void setJavascriptEnabled(const bool enabled);
void setWebSecurityDisabled(const bool disabled);
void setFileAccessFromFileUrlsEnabled(const bool enabled);
void setTarget(const std::string &target);
///////////////////////////////////

View File

@ -65,7 +65,7 @@ private:
void onTooltipCallback(std::string text);
void onLoadStartCallback();
void onRequestExitCallback();
void onLoadEndCallback(int httpStatusCode);
void onLoadEndCallback(int httpStatusCode, std::string url);
void onLoadError(int status, const std::string error_text);
void onAddressChangeCallback(std::string url);
void onOpenPopupCallback(std::string url, std::string target);
@ -92,6 +92,8 @@ private:
bool mDisableGPU;
bool mDisableNetworkService;
bool mUseMockKeyChain;
bool mDisableWebSecurity;
bool mFileAccessFromFileUrls;
std::string mUserAgentSubtring;
std::string mAuthUsername;
std::string mAuthPassword;
@ -127,6 +129,8 @@ MediaPluginBase(host_send_func, host_user_data)
mDisableGPU = false;
mDisableNetworkService = true;
mUseMockKeyChain = true;
mDisableWebSecurity = false;
mFileAccessFromFileUrls = false;
mUserAgentSubtring = "";
mAuthUsername = "";
mAuthPassword = "";
@ -260,13 +264,14 @@ void MediaPluginCEF::onRequestExitCallback()
////////////////////////////////////////////////////////////////////////////////
//
void MediaPluginCEF::onLoadEndCallback(int httpStatusCode)
void MediaPluginCEF::onLoadEndCallback(int httpStatusCode, std::string url)
{
LLPluginMessage message(LLPLUGIN_MESSAGE_CLASS_MEDIA_BROWSER, "navigate_complete");
//message.setValue("uri", event.getEventUri()); // not easily available here in CEF - needed?
message.setValueS32("result_code", httpStatusCode);
message.setValueBoolean("history_back_available", mCEFLib->canGoBack());
message.setValueBoolean("history_forward_available", mCEFLib->canGoForward());
message.setValue("uri", url);
sendMessage(message);
}
@ -355,14 +360,16 @@ const std::vector<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>();
@ -523,7 +530,7 @@ void MediaPluginCEF::receiveMessage(const char* message_string)
mCEFLib->setOnTitleChangeCallback(std::bind(&MediaPluginCEF::onTitleChangeCallback, this, std::placeholders::_1));
mCEFLib->setOnTooltipCallback(std::bind(&MediaPluginCEF::onTooltipCallback, this, std::placeholders::_1));
mCEFLib->setOnLoadStartCallback(std::bind(&MediaPluginCEF::onLoadStartCallback, this));
mCEFLib->setOnLoadEndCallback(std::bind(&MediaPluginCEF::onLoadEndCallback, this, std::placeholders::_1));
mCEFLib->setOnLoadEndCallback(std::bind(&MediaPluginCEF::onLoadEndCallback, this, std::placeholders::_1, std::placeholders::_2));
mCEFLib->setOnLoadErrorCallback(std::bind(&MediaPluginCEF::onLoadError, this, std::placeholders::_1, std::placeholders::_2));
mCEFLib->setOnAddressChangeCallback(std::bind(&MediaPluginCEF::onAddressChangeCallback, this, std::placeholders::_1));
mCEFLib->setOnOpenPopupCallback(std::bind(&MediaPluginCEF::onOpenPopupCallback, this, std::placeholders::_1, std::placeholders::_2));
@ -562,6 +569,19 @@ void MediaPluginCEF::receiveMessage(const char* message_string)
settings.disable_network_service = mDisableNetworkService;
settings.use_mock_keychain = mUseMockKeyChain;
#endif
// these were added to facilitate loading images directly into a local
// web page for the prototype 360 project in 2017 - something that is
// disallowed normally by the browser security model. Now the the source
// (cubemap) images are stores as JavaScript, we can avoid opening up
// this security hole (it was only set for the 360 floater but still
// a concern). Leaving them here, explicitly turn off vs removing
// entirely from this source file so that others are aware of them
// in the future.
settings.disable_web_security = false;
settings.file_access_from_file_urls = false;
settings.flash_enabled = mPluginsEnabled;
// This setting applies to all plugins, not just Flash
// Regarding, SL-15559 PDF files do not load in CEF v91,
// it turns out that on Windows, PDF support is treated
@ -688,6 +708,11 @@ void MediaPluginCEF::receiveMessage(const char* message_string)
std::string uri = message_in.getValue("uri");
mCEFLib->navigate(uri);
}
else if (message_name == "execute_javascript")
{
std::string code = message_in.getValue("code");
mCEFLib->executeJavaScript(code);
}
else if (message_name == "set_cookie")
{
std::string uri = message_in.getValue("uri");
@ -883,6 +908,14 @@ void MediaPluginCEF::receiveMessage(const char* message_string)
{
mDisableGPU = message_in.getValueBoolean("disable");
}
else if (message_name == "web_security_disabled")
{
mDisableWebSecurity = message_in.getValueBoolean("disabled");
}
else if (message_name == "file_access_from_file_urls")
{
mFileAccessFromFileUrls = message_in.getValueBoolean("enabled");
}
}
else if (message_class == LLPLUGIN_MESSAGE_CLASS_MEDIA_TIME)
{

View File

@ -12,12 +12,14 @@ include(bugsplat)
include(BuildPackagesInfo)
include(BuildVersion)
include(CMakeCopyIfDifferent)
include(CubemapToEquirectangularJS)
include(DBusGlib)
include(DragDrop)
include(EXPAT)
include(FMODSTUDIO)
include(GLOD)
include(Hunspell)
include(JPEGEncoderBasic)
include(JsonCpp)
include(LLAppearance)
include(LLAudio)
@ -47,6 +49,7 @@ include(OpenGL)
include(OpenSSL)
include(PNG)
include(TemplateCheck)
include(ThreeJS)
include(UI)
include(UnixInstall)
include(ViewerMiscLibs)
@ -205,6 +208,7 @@ set(viewer_SOURCE_FILES
llfilteredwearablelist.cpp
llfirstuse.cpp
llflexibleobject.cpp
llfloater360capture.cpp
llfloaterabout.cpp
llfloaterbvhpreview.cpp
llfloateraddpaymentmethod.cpp
@ -278,7 +282,7 @@ set(viewer_SOURCE_FILES
llfloaternamedesc.cpp
llfloaternotificationsconsole.cpp
llfloaternotificationstabbed.cpp
llfloateroutfitphotopreview.cpp
llfloateroutfitphotopreview.cpp
llfloateroutfitsnapshot.cpp
llfloaterobjectweights.cpp
llfloateropenobject.cpp
@ -844,6 +848,7 @@ set(viewer_HEADER_FILES
llfilteredwearablelist.h
llfirstuse.h
llflexibleobject.h
llfloater360capture.h
llfloaterabout.h
llfloaterbvhpreview.h
llfloateraddpaymentmethod.h
@ -1381,7 +1386,7 @@ file(WRITE "${CMAKE_CURRENT_BINARY_DIR}/viewer_version.txt"
"${VIEWER_SHORT_VERSION}.${VIEWER_VERSION_REVISION}\n")
set_source_files_properties(
llversioninfo.cpp tests/llversioninfo_test.cpp
llversioninfo.cpp tests/llversioninfo_test.cpp
PROPERTIES
COMPILE_DEFINITIONS "${VIEWER_CHANNEL_VERSION_DEFINES}" # see BuildVersion.cmake
)
@ -2004,7 +2009,7 @@ endif (WINDOWS)
# one of these being libz where you can find four or more versions in play
# at once. On Linux, libz can be found at link and run time via a number
# of paths:
#
#
# => -lfreetype
# => libz.so.1 (on install machine, not build)
# => -lSDL
@ -2175,7 +2180,7 @@ if (DARWIN)
# https://blog.kitware.com/upcoming-in-cmake-2-8-12-osx-rpath-support/
set(CMAKE_MACOSX_RPATH 1)
set_target_properties(
${VIEWER_BINARY_NAME}
PROPERTIES
@ -2436,7 +2441,7 @@ if (LL_TESTS)
llworldmap.cpp
llworldmipmap.cpp
PROPERTIES
LL_TEST_ADDITIONAL_SOURCE_FILES
LL_TEST_ADDITIONAL_SOURCE_FILES
tests/llviewertexture_stub.cpp
#llviewertexturelist.cpp
)
@ -2470,7 +2475,7 @@ if (LL_TESTS)
llworldmap.cpp
llworldmipmap.cpp
PROPERTIES
LL_TEST_ADDITIONAL_SOURCE_FILES
LL_TEST_ADDITIONAL_SOURCE_FILES
tests/llviewertexture_stub.cpp
#llviewertexturelist.cpp
LL_TEST_ADDITIONAL_LIBRARIES "${BOOST_SYSTEM_LIBRARY}"

View File

@ -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>

View File

@ -2076,6 +2076,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>
@ -3539,7 +3561,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>
@ -8858,7 +8880,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>
@ -11580,7 +11602,7 @@
<string>Boolean</string>
<key>Value</key>
<integer>0</integer>
</map>
</map>
<key>NearbyListShowMap</key>
<map>
<key>Comment</key>
@ -16632,30 +16654,83 @@
<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>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>

View File

@ -0,0 +1,904 @@
/**
* @file llfloater360capture.cpp
* @author Callum Prentice (callum@lindenlab.com)
* @brief Floater code for the 360 Capture feature
*
* $LicenseInfo:firstyear=2011&license=viewerlgpl$
* Second Life Viewer Source Code
* Copyright (C) 2011, Linden Research, Inc.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation;
* version 2.1 of the License only.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*
* Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA
* $/LicenseInfo$
*/
#include "llviewerprecompiledheaders.h"
#include "llfloater360capture.h"
#include "llagent.h"
#include "llagentui.h"
#include "llbase64.h"
#include "llcallbacklist.h"
#include "llenvironment.h"
#include "llimagejpeg.h"
#include "llmediactrl.h"
#include "llradiogroup.h"
#include "llslurl.h"
#include "lltextbox.h"
#include "lltrans.h"
#include "lluictrlfactory.h"
#include "llversioninfo.h"
#include "llviewercamera.h"
#include "llviewercontrol.h"
#include "llviewerpartsim.h"
#include "llviewerregion.h"
#include "llviewerwindow.h"
#include "pipeline.h"
#include <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();
while (mSourceImageSize > window_width || mSourceImageSize > window_height)
{
mSourceImageSize /= 2;
LL_INFOS("360Capture") << "Deferred rendering is forcing a smaller capture size: " << mSourceImageSize << LL_ENDL;
}
// there has to be an easier way than this to get the value
// from the radio group item at index 0. Why doesn't
// LLRadioGroup::getSelectedValue(int index) exist?
int index = mQualityRadioGroup->getSelectedIndex();
mQualityRadioGroup->setSelectedIndex(0);
int min_size = mQualityRadioGroup->getSelectedValue().asInteger();
mQualityRadioGroup->setSelectedIndex(index);
// If the maximum size we can support falls below a threshold then
// we should display a message in the log so we can try to debug
// why this is happening
if (mSourceImageSize < min_size)
{
LL_INFOS("360Capture") << "Small snapshot size due to deferred rendering and small app window" << LL_ENDL;
}
}
}
// This function shouldn't exist! We use the tooltip text from
// the corresponding XUI file (floater_360capture.xml) as the
// overlay text for the final web page to inform the user
// about the quality level in play. There ouught to be a
// UI function like LLView* getSelectedItemView() or similar
// but as far as I can tell, there isn't so we have to resort
// to finding it ourselves with this icky code..
const std::string LLFloater360Capture::getSelectedQualityTooltip()
{
// safey (or bravery?)
if (mQualityRadioGroup != nullptr)
{
// for all the child widgets for the radio group
// (just the radio buttons themselves I think)
for (child_list_const_reverse_iter_t iter = mQualityRadioGroup->getChildList()->rbegin();
iter != mQualityRadioGroup->getChildList()->rend();
++iter)
{
// if we match the selected index (which we can get easily)
// with our position in the list of children
if (mQualityRadioGroup->getSelectedIndex() ==
std::distance(mQualityRadioGroup->getChildList()->rend(), iter) - 1)
{
// return the plain old tooltip text
return (*iter)->getToolTip();
}
}
}
// if it's not found or not available, return an empty string
return std::string();
}
// Some of the 'magic' happens via a web page in an HTML directory
// and this code provides a single point of reference for its' location
const std::string LLFloater360Capture::getHTMLBaseFolder()
{
std::string folder_name = gDirUtilp->getSkinDir();
folder_name += gDirUtilp->getDirDelimiter();
folder_name += "html";
folder_name += gDirUtilp->getDirDelimiter();
folder_name += "common";
folder_name += gDirUtilp->getDirDelimiter();
folder_name += "equirectangular";
folder_name += gDirUtilp->getDirDelimiter();
return folder_name;
}
// triggered when the 'capture' button in the UI is pressed
void LLFloater360Capture::onCapture360ImagesBtn()
{
// launch the main capture code in a coroutine so we can
// yield/suspend at some points to give the main UI
// thread a look-in occasionally.
LLCoros::instance().launch("capture360cap", [this]()
{
capture360Images();
});
}
// Gets the full path name for a given JavaScript file in the HTML folder. We
// use this ultimately as a parameter to the main web app so it knows where to find
// the JavaScript array containing the 6 cube map images, stored as data URLs
const std::string LLFloater360Capture::makeFullPathToJS(const std::string filename)
{
std::string full_js_path = mImageSaveDir;
full_js_path += gDirUtilp->getDirDelimiter();
full_js_path += filename;
return full_js_path;
}
// Write the header/prequel portion of the JavaScript array of data urls
// that we use to store the cube map images in (so the web code can load
// them without tweaking browser security - we'd have to do this if they
// we stored as plain old images) This deliberately overwrites the old
// one, if it exists
void LLFloater360Capture::writeDataURLHeader(const std::string filename)
{
std::ofstream file_handle(filename.c_str());
if (file_handle.is_open())
{
file_handle << "// cube map images for Second Life Viewer panorama 360 images" << std::endl;
file_handle.close();
}
}
// Write the footer/sequel portion of the JavaScript image code. When this is
// called, the current file on disk will contain the header and the 6 data
// URLs, each with a well specified name. This final piece of JavaScript code
// creates an array from those data URLs that the main application can
// reference and read.
void LLFloater360Capture::writeDataURLFooter(const std::string filename)
{
std::ofstream file_handle(filename.c_str(), std::ios_base::app);
if (file_handle.is_open())
{
file_handle << "var cubemap_img_js = [" << std::endl;
file_handle << " img_posx, img_negx," << std::endl;
file_handle << " img_posy, img_negy," << std::endl;
file_handle << " img_posz, img_negz," << std::endl;
file_handle << "];" << std::endl;
file_handle.close();
}
}
// Given a filename, a chunk of data (representing an image file) and the size
// of the buffer, we create a BASE64 encoded string and use it to build a JavaScript
// data URL that represents the image in a web browser environment
bool LLFloater360Capture::writeDataURL(const std::string filename, const std::string prefix, U8* data, unsigned int data_len)
{
LL_INFOS("360Capture") << "Writing data URL for " << prefix << " to " << filename << LL_ENDL;
const std::string data_url = LLBase64::encode(data, data_len);
std::ofstream file_handle(filename.c_str(), std::ios_base::app);
if (file_handle.is_open())
{
file_handle << "var img_";
file_handle << prefix;
file_handle << " = '";
file_handle << "data:image/jpeg; base64,";
file_handle << data_url;
file_handle << "'";
file_handle << std::endl;
file_handle.close();
return true;
}
return false;
}
// Encode the image from each of the 6 snapshots and save it out to
// the JavaScript array of data URLs
void LLFloater360Capture::encodeAndSave(LLPointer<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);
// determine whether or not to include avatar in the scene as we capture the 360 panorama
if (gSavedSettings.getBOOL("360CaptureHideAvatars"))
{
// Turn off the avatar if UI tells us to hide it.
// Note: the original call to gAvatar.hide(FALSE) did *not* hide
// attachments and so for most residents, there would be some debris
// left behind in the snapshot.
// Note: this toggles so if it set to on, this will turn it off and
// the subsequent call to the same thing after capture is finished
// will turn it back on again. Similarly, for the case where it
// was set to off - I think this is what we need
LLPipeline::toggleRenderTypeControl(LLPipeline::RENDER_TYPE_AVATAR);
}
// these are the 6 directions we will point the camera - essentially,
// North, South, East, West, Up, Down
LLVector3 look_dirs[6] = { LLVector3(1, 0, 0), LLVector3(0, 1, 0), LLVector3(0, 0, 1), LLVector3(-1, 0, 0), LLVector3(0, -1, 0), LLVector3(0, 0, -1) };
LLVector3 look_upvecs[6] = { LLVector3(0, 0, 1), LLVector3(0, 0, 1), LLVector3(0, -1, 0), LLVector3(0, 0, 1), LLVector3(0, 0, 1), LLVector3(0, 1, 0) };
// save current view/camera settings so we can restore them afterwards
S32 old_occlusion = LLPipeline::sUseOcclusion;
LLPipeline::sUseOcclusion = 0;
LLViewerCamera* camera = LLViewerCamera::getInstance();
F32 old_fov = camera->getView();
F32 old_aspect = camera->getAspect();
F32 old_yaw = camera->getYaw();
// stop the motion of as much of the world moving as much as we can
freezeWorld(true);
// Save the direction (in degrees) the camera is looking when we
// take the shot since that is what we write to image metadata
// 'GPano:InitialViewHeadingDegrees' field.
// We need to convert from the angle getYaw() gives us into something
// the XMP data field wants (N=0, E=90, S=180, W= 270 etc.)
mInitialHeadingDeg = (360 + 90 - (int)(camera->getYaw() * RAD_TO_DEG)) % 360;
LL_INFOS("360Capture") << "Recording a heading of " << (int)(mInitialHeadingDeg) << LL_ENDL;
// camera constants for the square, cube map capture image
camera->setAspect(1.0); // must set aspect ratio first to avoid undesirable clamping of vertical FoV
camera->setView(F_PI_BY_TWO);
camera->yaw(0.0);
// record how many times we changed camera to try to understand the "all shots are the same issue"
unsigned int camera_changed_times = 0;
// the name of the JavaScript file written out that contains the 6 cube map images
// stored as a JavaScript array of data URLs. If you change this filename, you must
// also change the corresponding entry in the HTML file that uses it -
// (newview/skins/default/html/common/equirectangular/display_eqr.html)
const std::string cumemap_js_filename("cubemap_img.js");
// construct the full path to this file - typically stored in the users'
// Second Life settings / username / eqrimg folder.
const std::string cubemap_js_full_path = makeFullPathToJS(cumemap_js_filename);
// Write the JavaScript file header (the top of the file before the
// declarations of the actual data URLs array). In practice, all this writes
// is a comment - it's main purpose is to reset the file from the last time
// it was written
writeDataURLHeader(cubemap_js_full_path);
// the names of the prefixes we assign as the name to each data URL and are then
// consumed by the WebGL application. Nominally, they stand for positive and
// negative in the X/Y/Z directions.
static const std::string prefixes[6] =
{
"posx", "posz", "posy",
"negx", "negz", "negy",
};
// time the encode process for later optimization
auto encode_time_total = 0.0;
// for each of the 6 directions we shoot...
for (int i = 0; i < 6; i++)
{
// these buffers are where the raw, captured pixels are stored and
// the first time we use them, we have to make a new one
if (mRawImages[i] == nullptr)
{
mRawImages[i] = new LLImageRaw(mSourceImageSize, mSourceImageSize, 3);
}
else
// subsequent capture with floater open so we resize the buffer from
// the previous run
{
// LLImageRaw deletes the old one via operator= but just to be
// sure, we delete its' large data member first...
mRawImages[i]->deleteData();
mRawImages[i] = new LLImageRaw(mSourceImageSize, mSourceImageSize, 3);
}
// set up camera to look in each direction
camera->lookDir(look_dirs[i], look_upvecs[i]);
// record if camera changed to try to understand the "all shots are the same issue"
if (camera->isChanged())
{
++camera_changed_times;
}
// call the (very) simplified snapshot code that simply deals
// with a single image, no sub-images etc. but is very fast
gViewerWindow->simpleSnapshot(mRawImages[i], mSourceImageSize, mSourceImageSize);
// encode each image and write to disk while saving how long it took to do so
auto t_start = std::chrono::high_resolution_clock::now();
encodeAndSave(mRawImages[i], cubemap_js_full_path, prefixes[i]);
auto t_end = std::chrono::high_resolution_clock::now();
auto duration = std::chrono::duration_cast<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);
}
// record that we missed some shots in the log for later debugging
// note: we use 5 and not 6 because the first shot isn't regarded
// as a change - only the subsequent 5 are
if (camera_changed_times < 5)
{
LL_INFOS("360Capture") << "Warning: we only captured " << camera_changed_times << " images." << LL_ENDL;
}
// now we have the 6 shots saved in a well specified location,
// we can load the web content that uses them
std::string url = "file:///" + getHTMLBaseFolder() + mEqrGenHTML;
mWebBrowser->navigateTo(url);
// allow the UI to update by suspending and waiting for the
// main render loop to update the UI
suspendForAFrame();
// page is loaded and ready so we can turn on the buttons again
mCaptureBtn->setEnabled(true);
mSaveLocalBtn->setEnabled(true);
}
// once the request is made to navigate to the web page containing the code
// to process the 6 images into an EQR one, we have to wait for it to finish
// loaded - we get a "navigate complete" event when that happens that we can act on
void LLFloater360Capture::handleMediaEvent(LLPluginClassMedia* self, EMediaEvent event)
{
switch (event)
{
// not used right now but retaining because this event might
// be useful for a feature I am hoping to add
case MEDIA_EVENT_LOCATION_CHANGED:
break;
// navigation in the browser completed
case MEDIA_EVENT_NAVIGATE_COMPLETE:
{
// Confirm that the navigation event does indeed apply to the
// page we are looking for. At the moment, this is the only
// one we care about so the test is superfluous but that might change.
std::string navigate_url = self->getNavigateURI();
if (navigate_url.find(mEqrGenHTML) != std::string::npos)
{
// this string is being passed across to the web so replace all the windows backslash
// characters with forward slashes or (I think) the backslashes are treated as escapes
std::replace(mImageSaveDir.begin(), mImageSaveDir.end(), '\\', '/');
// we store the camera FOV (field of view) in a saved setting since this feels
// like something it would be interesting to change and experiment with
int camera_fov = gSavedSettings.getU32("360CaptureCameraFOV");
// compose the overlay for the final web page that tells the user
// what level of quality the capture was taken with
std::string overlay_label = "'" + getSelectedQualityTooltip() + "'";
// so now our page is loaded and images are in place - call
// the JavaScript init script with some parameters to initialize
// the WebGL based preview
const std::string cmd = STRINGIZE(
"init("
<< mOutputImageWidth
<< ", "
<< mOutputImageHeight
<< ", "
<< "'"
<< mImageSaveDir
<< "'"
<< ", "
<< camera_fov
<< ", "
<< LLViewerCamera::getInstance()->getYaw()
<< ", "
<< overlay_label
<< ")"
);
// execute the command on the page
mWebBrowser->getMediaPlugin()->executeJavaScript(cmd);
}
}
break;
default:
break;
}
}
// called when the user wants to save the cube maps off to the final EQR image
void LLFloater360Capture::onSaveLocalBtn()
{
// region name and URL
std::string region_name; // no sensible default
std::string region_url("http://secondlife.com");
LLViewerRegion* region = gAgent.getRegion();
if (region)
{
// region names can (and do) contain characters that would make passing
// them into a JavaScript function problematic - single quotes for example
// so we must escape/encode both
region_name = region->getName();
// escaping/encoding is a minefield - let's just remove any offending characters from the region name
region_name.erase(std::remove(region_name.begin(), region_name.end(), '\''), region_name.end());
region_name.erase(std::remove(region_name.begin(), region_name.end(), '\"'), region_name.end());
// fortunately there is already an escaping function built into the SLURL generation code
LLSLURL slurl;
bool is_escaped = true;
LLAgentUI::buildSLURL(slurl, is_escaped);
region_url = slurl.getSLURLString();
}
// build suggested filename (the one that appears as the default
// in the Save dialog box)
const std::string suggested_filename = generate_proposed_filename();
// This string (the name of the product plus a truncated version number (no build))
// is used in the XMP block as the name of the generating and stitching software.
// We save the version number here and not in the more generic 'software' item
// because that might help us determine something about the image in the future.
const std::string client_version = STRINGIZE(
LLVersionInfo::instance().getChannel() <<
" " <<
LLVersionInfo::instance().getShortVersion()
);
// save the time the image was created. I don't know if this should be
// UTC/ZULU or the users' local time. It probably doesn't matter.
std::time_t result = std::time(nullptr);
std::string ctime_str = std::ctime(&result);
std::string time_str = ctime_str.substr(0, ctime_str.length() - 1);
// build the JavaScript data structure that is used to pass all the
// variables into the JavaScript function on the web page loaded into
// the embedded browser component of the floater.
const std::string xmp_details = STRINGIZE(
"{ " <<
"pano_version: '" << "2.2.1" << "', " <<
"software: '" << LLVersionInfo::instance().getChannel() << "', " <<
"capture_software: '" << client_version << "', " <<
"stitching_software: '" << client_version << "', " <<
"width: " << mOutputImageWidth << ", " <<
"height: " << mOutputImageHeight << ", " <<
"heading: " << mInitialHeadingDeg << ", " <<
"actual_source_image_size: " << mQualityRadioGroup->getSelectedValue().asInteger() << ", " <<
"scaled_source_image_size: " << mSourceImageSize << ", " <<
"first_photo_date: '" << time_str << "', " <<
"last_photo_date: '" << time_str << "', " <<
"region_name: '" << region_name << "', " <<
"region_url: '" << region_url << "', " <<
" }"
);
// build the JavaScript command to send to the web browser
const std::string cmd = "saveAsEqrImage(\"" + suggested_filename + "\", " + xmp_details + ")";
// send it to the browser instance, triggering the equirectangular capture
// process and complimentary offer to save the image
mWebBrowser->getMediaPlugin()->executeJavaScript(cmd);
}
// We capture all 6 images sequentially and if parts of the world are moving
// E.G. clouds, water, objects - then we may get seams or discontinuities
// when the images are combined to form the EQR image. This code tries to
// stop everything so we can shoot for seamless shots. There is probably more
// we can do here - e.g. waves in the water probably won't line up.
void LLFloater360Capture::freezeWorld(bool enable)
{
static bool clouds_scroll_paused = false;
if (enable)
{
// record the cloud scroll current value so we can restore it
clouds_scroll_paused = LLEnvironment::instance().isCloudScrollPaused();
// stop the clouds moving
LLEnvironment::instance().pauseCloudScroll();
// freeze all avatars
LLCharacter* avatarp;
for (std::vector<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();
}

View File

@ -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

View File

@ -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)
{

View File

@ -153,6 +153,7 @@ public:
static void update();
void onExtendFloater();
void on360Snapshot();
static LLFloaterSnapshot* getInstance();
static LLFloaterSnapshot* findInstance();

View File

@ -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>);

View File

@ -1746,8 +1746,16 @@ LLPluginClassMedia* LLViewerMediaImpl::newSourceFromMediaType(std::string media_
media_source->cookies_enabled( cookies_enabled || clean_browser);
// collect 'javascript enabled' setting from prefs and send to embedded browser
bool javascript_enabled = gSavedSettings.getBOOL( "BrowserJavascriptEnabled" );
media_source->setJavascriptEnabled( javascript_enabled || clean_browser);
bool javascript_enabled = gSavedSettings.getBOOL("BrowserJavascriptEnabled");
media_source->setJavascriptEnabled(javascript_enabled || clean_browser);
// collect 'web security disabled' (see Chrome --web-security-disabled) setting from prefs and send to embedded browser
bool web_security_disabled = gSavedSettings.getBOOL("BrowserWebSecurityDisabled");
media_source->setWebSecurityDisabled(web_security_disabled || clean_browser);
// collect setting indicates if local file access from file URLs is allowed from prefs and send to embedded browser
bool file_access_from_file_urls = gSavedSettings.getBOOL("BrowserFileAccessFromFileUrls");
media_source->setFileAccessFromFileUrlsEnabled(file_access_from_file_urls || clean_browser);
// As of SL-15559 PDF files do not load in CEF v91 we enable plugins
// but explicitly disable Flash (PDF support in CEF is now treated as a plugin)
@ -1907,6 +1915,15 @@ void LLViewerMediaImpl::loadURI()
}
}
//////////////////////////////////////////////////////////////////////////////////////////
void LLViewerMediaImpl::executeJavaScript(const std::string& code)
{
if (mMediaSource)
{
mMediaSource->executeJavaScript(code);
}
}
//////////////////////////////////////////////////////////////////////////////////////////
void LLViewerMediaImpl::setSize(int width, int height)
{
@ -3212,8 +3229,19 @@ void LLViewerMediaImpl::handleMediaEvent(LLPluginClassMedia* plugin, LLPluginCla
case LLViewerMediaObserver::MEDIA_EVENT_FILE_DOWNLOAD:
{
//llinfos << "Media event - file download requested - filename is " << self->getFileDownloadFilename() << llendl;
LLNotificationsUtil::add("MediaFileDownloadUnsupported");
LL_DEBUGS("Media") << "Media event - file download requested - filename is " << plugin->getFileDownloadFilename() << LL_ENDL;
// pick a file from SAVE FILE dialog
// need a better algorithm that this or else, pass in type of save type
// from event that initiated it - this is okay for now - only thing
// that saves is 360s
std::string suggested_filename = plugin->getFileDownloadFilename();
LLFilePicker::ESaveFilter filter = LLFilePicker::FFSAVE_ALL;
if (suggested_filename.find(".jpg") != std::string::npos || suggested_filename.find(".jpeg") != std::string::npos)
filter = LLFilePicker::FFSAVE_JPEG;
if (suggested_filename.find(".png") != std::string::npos)
filter = LLFilePicker::FFSAVE_PNG;
init_threaded_picker_save_dialog(plugin, filter, suggested_filename);
}
break;

View File

@ -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);

View File

@ -1263,12 +1263,55 @@ class LLAdvancedDumpScriptedCamera : public view_listener_t
class LLAdvancedDumpRegionObjectCache : public view_listener_t
{
bool handleEvent(const LLSD& userdata)
{
{
handle_dump_region_object_cache(NULL);
return true;
}
};
class LLAdvancedInterestListFullUpdate : public view_listener_t
{
bool handleEvent(const LLSD& userdata)
{
LLSD request;
LLSD body;
static bool using_360 = false;
if (using_360)
{
body["mode"] = LLSD::String("default");
}
else
{
body["mode"] = LLSD::String("360");
}
using_360 = !using_360;
if (gAgent.requestPostCapability("InterestList", body, [](const LLSD& response)
{
LL_INFOS("360Capture") <<
"InterestList capability responded: \n" <<
ll_pretty_print_sd(response) <<
LL_ENDL;
}))
{
LL_INFOS("360Capture") <<
"Successfully posted an InterestList capability request with payload: \n" <<
ll_pretty_print_sd(body) <<
LL_ENDL;
return true;
}
else
{
LL_INFOS("360Capture") <<
"Unable to post an InterestList capability request with payload: \n" <<
ll_pretty_print_sd(body) <<
LL_ENDL;
return false;
}
}
};
class LLAdvancedBuyCurrencyTest : public view_listener_t
{
bool handleEvent(const LLSD& userdata)
@ -2309,11 +2352,10 @@ class LLAdvancedLeaveAdminStatus : public view_listener_t
// Advanced > Debugging //
//////////////////////////
class LLAdvancedForceErrorBreakpoint : public view_listener_t
{
bool handleEvent(const LLSD& userdata)
{
{
force_error_breakpoint(NULL);
return true;
}
@ -9189,6 +9231,7 @@ void initialize_menus()
// Advanced > World
view_listener_t::addMenu(new LLAdvancedDumpScriptedCamera(), "Advanced.DumpScriptedCamera");
view_listener_t::addMenu(new LLAdvancedDumpRegionObjectCache(), "Advanced.DumpRegionObjectCache");
view_listener_t::addMenu(new LLAdvancedInterestListFullUpdate(), "Advanced.InterestListFullUpdate");
// Advanced > UI
commit.add("Advanced.WebBrowserTest", boost::bind(&handle_web_browser_test, _2)); // sigh! this one opens the MEDIA browser

View File

@ -3745,6 +3745,7 @@ void process_kill_object(LLMessageSystem *mesgsys, void **user_data)
{
LLColor4 color(0.f,1.f,0.f,1.f);
gPipeline.addDebugBlip(objectp->getPositionAgent(), color);
LL_DEBUGS("MessageBlip") << "Kill blip for local " << local_id << " at " << objectp->getPositionAgent() << LL_ENDL;
}
// Do the kill

View File

@ -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;

View File

@ -2956,6 +2956,8 @@ void LLViewerRegionImpl::buildCapabilityNames(LLSD& capabilityNames)
capabilityNames.append("IncrementCOFVersion");
AISAPI::getCapNames(capabilityNames);
capabilityNames.append("InterestList");
capabilityNames.append("GetDisplayNames");
capabilityNames.append("GetExperiences");
capabilityNames.append("AgentExperiences");

View File

@ -5160,6 +5160,83 @@ BOOL LLViewerWindow::rawSnapshot(LLImageRaw *raw, S32 image_width, S32 image_hei
return ret;
}
BOOL LLViewerWindow::simpleSnapshot(LLImageRaw* raw, S32 image_width, S32 image_height)
{
gDisplaySwapBuffers = FALSE;
glClear(GL_DEPTH_BUFFER_BIT | GL_COLOR_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
setCursor(UI_CURSOR_WAIT);
BOOL prev_draw_ui = gPipeline.hasRenderDebugFeatureMask(LLPipeline::RENDER_DEBUG_FEATURE_UI) ? TRUE : FALSE;
if (prev_draw_ui != false)
{
LLPipeline::toggleRenderDebugFeature(LLPipeline::RENDER_DEBUG_FEATURE_UI);
}
LLPipeline::sShowHUDAttachments = FALSE;
LLRect window_rect = getWorldViewRectRaw();
S32 original_width = LLPipeline::sRenderDeferred ? gPipeline.mDeferredScreen.getWidth() : gViewerWindow->getWorldViewWidthRaw();
S32 original_height = LLPipeline::sRenderDeferred ? gPipeline.mDeferredScreen.getHeight() : gViewerWindow->getWorldViewHeightRaw();
LLRenderTarget scratch_space;
U32 color_fmt = GL_RGBA;
const bool use_depth_buffer = true;
const bool use_stencil_buffer = true;
if (scratch_space.allocate(image_width, image_height, color_fmt, use_depth_buffer, use_stencil_buffer))
{
if (gPipeline.allocateScreenBuffer(image_width, image_height))
{
mWorldViewRectRaw.set(0, image_height, image_width, 0);
scratch_space.bindTarget();
}
else
{
scratch_space.release();
gPipeline.allocateScreenBuffer(original_width, original_height);
}
}
const U32 subfield = 0;
const bool do_rebuild = true;
const F32 zoom = 1.0;
const bool for_snapshot = TRUE;
display(do_rebuild, zoom, subfield, for_snapshot);
glReadPixels(
0, 0,
image_width,
image_height,
GL_RGB, GL_UNSIGNED_BYTE,
raw->getData()
);
stop_glerror();
gDisplaySwapBuffers = FALSE;
gDepthDirty = TRUE;
if (!gPipeline.hasRenderDebugFeatureMask(LLPipeline::RENDER_DEBUG_FEATURE_UI))
{
if (prev_draw_ui != false)
{
LLPipeline::toggleRenderDebugFeature(LLPipeline::RENDER_DEBUG_FEATURE_UI);
}
}
LLPipeline::sShowHUDAttachments = TRUE;
setCursor(UI_CURSOR_ARROW);
gPipeline.resetDrawOrders();
mWorldViewRectRaw = window_rect;
scratch_space.flush();
scratch_space.release();
gPipeline.allocateScreenBuffer(original_width, original_height);
return true;
}
void LLViewerWindow::destroyWindow()
{
if (mWindow)

View File

@ -357,7 +357,10 @@ public:
BOOL saveSnapshot(const std::string& filename, S32 image_width, S32 image_height, BOOL show_ui = TRUE, BOOL show_hud = TRUE, BOOL do_rebuild = FALSE, LLSnapshotModel::ESnapshotLayerType type = LLSnapshotModel::SNAPSHOT_TYPE_COLOR, LLSnapshotModel::ESnapshotFormat format = LLSnapshotModel::SNAPSHOT_FORMAT_BMP);
BOOL rawSnapshot(LLImageRaw *raw, S32 image_width, S32 image_height, BOOL keep_window_aspect = TRUE, BOOL is_texture = FALSE,
BOOL show_ui = TRUE, BOOL show_hud = TRUE, BOOL do_rebuild = FALSE, LLSnapshotModel::ESnapshotLayerType type = LLSnapshotModel::SNAPSHOT_TYPE_COLOR, S32 max_size = MAX_SNAPSHOT_IMAGE_SIZE);
BOOL thumbnailSnapshot(LLImageRaw *raw, S32 preview_width, S32 preview_height, BOOL show_ui, BOOL show_hud, BOOL do_rebuild, LLSnapshotModel::ESnapshotLayerType type);
BOOL simpleSnapshot(LLImageRaw *raw, S32 image_width, S32 image_height);
BOOL thumbnailSnapshot(LLImageRaw *raw, S32 preview_width, S32 preview_height, BOOL show_ui, BOOL show_hud, BOOL do_rebuild, LLSnapshotModel::ESnapshotLayerType type);
BOOL isSnapshotLocSet() const;
void resetSnapshotLoc() const;

View File

@ -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>

View File

@ -0,0 +1,149 @@
<!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 {
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>

View File

@ -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" />

Binary file not shown.

After

Width:  |  Height:  |  Size: 793 B

View File

@ -0,0 +1,124 @@
<?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 Capture"
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">
<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="Capture 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>
<web_browser top="0"
follows="all"
bg_opaque_color="0.225 0.225 0.225 1"
left="200"
width="600"
height="380"
name="360capture_contents"
trusted_content="true" />
<text follows="bottom"
layout="topleft"
name="statusbar"
height="17"
left="210"
top="383"
width="600">
Click and drag on the image to pan
</text>
</floater>

View File

@ -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>

View File

@ -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>

View File

@ -733,6 +733,15 @@
function="Floater.Show"
parameter="snapshot" />
</menu_item_call>
<menu_item_call
label="Capture 360"
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"

View File

@ -4124,6 +4124,8 @@ Try enclosing path to the editor with double quotes.
<!-- commands -->
<string
name="Command_360_Capture_Label">360 Capture</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 panorama 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>

View File

@ -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