Merge branch 'master' of github.com:highfidelity/hifi into motor-action

This commit is contained in:
Seth Alves 2017-05-12 13:03:58 -07:00
commit 4af0a85aa0
46 changed files with 4664 additions and 14 deletions

114
cmake/modules/FindFBX.cmake Normal file
View file

@ -0,0 +1,114 @@
# Locate the FBX SDK
#
# Defines the following variables:
#
# FBX_FOUND - Found the FBX SDK
# FBX_VERSION - Version number
# FBX_INCLUDE_DIRS - Include directories
# FBX_LIBRARIES - The libraries to link to
#
# Accepts the following variables as input:
#
# FBX_VERSION - as a CMake variable, e.g. 2017.0.1
# FBX_ROOT - (as a CMake or environment variable)
# The root directory of the FBX SDK install
# adapted from https://github.com/ufz-vislab/VtkFbxConverter/blob/master/FindFBX.cmake
# which uses the MIT license (https://github.com/ufz-vislab/VtkFbxConverter/blob/master/LICENSE.txt)
if (NOT FBX_VERSION)
if (WIN32)
set(FBX_VERSION 2017.1)
else()
set(FBX_VERSION 2017.0.1)
endif()
endif()
string(REGEX REPLACE "^([0-9]+).*$" "\\1" FBX_VERSION_MAJOR "${FBX_VERSION}")
string(REGEX REPLACE "^[0-9]+\\.([0-9]+).*$" "\\1" FBX_VERSION_MINOR "${FBX_VERSION}")
string(REGEX REPLACE "^[0-9]+\\.[0-9]+\\.([0-9]+).*$" "\\1" FBX_VERSION_PATCH "${FBX_VERSION}")
set(FBX_MAC_LOCATIONS "/Applications/Autodesk/FBX\ SDK/${FBX_VERSION}")
if (WIN32)
string(REGEX REPLACE "\\\\" "/" WIN_PROGRAM_FILES_X64_DIRECTORY $ENV{ProgramW6432})
endif()
set(FBX_WIN_LOCATIONS "${WIN_PROGRAM_FILES_X64_DIRECTORY}/Autodesk/FBX/FBX SDK/${FBX_VERSION}")
set(FBX_SEARCH_LOCATIONS $ENV{FBX_ROOT} ${FBX_ROOT} ${FBX_MAC_LOCATIONS} ${FBX_WIN_LOCATIONS})
function(_fbx_append_debugs _endvar _library)
if (${_library} AND ${_library}_DEBUG)
set(_output optimized ${${_library}} debug ${${_library}_DEBUG})
else()
set(_output ${${_library}})
endif()
set(${_endvar} ${_output} PARENT_SCOPE)
endfunction()
if (${CMAKE_CXX_COMPILER_ID} MATCHES "Clang")
set(fbx_compiler clang)
elseif (${CMAKE_CXX_COMPILER_ID} MATCHES "GNU")
set(fbx_compiler gcc4)
endif()
function(_fbx_find_library _name _lib _suffix)
if (MSVC12)
set(VS_PREFIX vs2013)
endif()
if (MSVC11)
set(VS_PREFIX vs2012)
endif()
if (MSVC10)
set(VS_PREFIX vs2010)
endif()
if (MSVC90)
set(VS_PREFIX vs2008)
endif()
find_library(${_name}
NAMES ${_lib}
HINTS ${FBX_SEARCH_LOCATIONS}
PATH_SUFFIXES lib/${fbx_compiler}/${_suffix} lib/${fbx_compiler}/ub/${_suffix} lib/${VS_PREFIX}/x64/${_suffix}
)
mark_as_advanced(${_name})
endfunction()
find_path(FBX_INCLUDE_DIR fbxsdk.h
PATHS ${FBX_SEARCH_LOCATIONS}
PATH_SUFFIXES include
)
mark_as_advanced(FBX_INCLUDE_DIR)
if (WIN32)
_fbx_find_library(FBX_LIBRARY libfbxsdk-md release)
_fbx_find_library(FBX_LIBRARY_DEBUG libfbxsdk-md debug)
elseif (APPLE)
find_library(CARBON NAMES Carbon)
find_library(SYSTEM_CONFIGURATION NAMES SystemConfiguration)
_fbx_find_library(FBX_LIBRARY libfbxsdk.a release)
_fbx_find_library(FBX_LIBRARY_DEBUG libfbxsdk.a debug)
endif()
include(FindPackageHandleStandardArgs)
FIND_PACKAGE_HANDLE_STANDARD_ARGS(FBX DEFAULT_MSG FBX_LIBRARY FBX_INCLUDE_DIR)
if (FBX_FOUND)
set(FBX_INCLUDE_DIRS ${FBX_INCLUDE_DIR})
_fbx_append_debugs(FBX_LIBRARIES FBX_LIBRARY)
add_definitions(-DFBXSDK_NEW_API)
if (WIN32)
add_definitions(-DK_PLUGIN -DK_FBXSDK -DK_NODLL)
set(CMAKE_EXE_LINKER_FLAGS /NODEFAULTLIB:\"LIBCMT\")
set(FBX_LIBRARIES ${FBX_LIBRARIES} Wininet.lib)
elseif (APPLE)
set(FBX_LIBRARIES ${FBX_LIBRARIES} ${CARBON} ${SYSTEM_CONFIGURATION})
endif()
endif()

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 394 KiB

Binary file not shown.

View file

@ -1438,15 +1438,17 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo
connect(_window, SIGNAL(windowMinimizedChanged(bool)), this, SLOT(windowMinimizedChanged(bool)));
qCDebug(interfaceapp, "Startup time: %4.2f seconds.", (double)startupTimer.elapsed() / 1000.0);
auto textureCache = DependencyManager::get<TextureCache>();
{
PROFILE_RANGE(render, "Process Default Skybox");
auto textureCache = DependencyManager::get<TextureCache>();
QString skyboxUrl { PathUtils::resourcesPath() + "images/Default-Sky-9-cubemap.jpg" };
QString skyboxAmbientUrl { PathUtils::resourcesPath() + "images/Default-Sky-9-ambient.jpg" };
auto skyboxUrl = PathUtils::resourcesPath().toStdString() + "images/Default-Sky-9-cubemap.ktx";
_defaultSkyboxTexture = textureCache->getImageTexture(skyboxUrl, image::TextureUsage::CUBE_TEXTURE, { { "generateIrradiance", false } });
_defaultSkyboxAmbientTexture = textureCache->getImageTexture(skyboxAmbientUrl, image::TextureUsage::CUBE_TEXTURE, { { "generateIrradiance", true } });
_defaultSkyboxTexture = gpu::Texture::unserialize(skyboxUrl);
_defaultSkyboxAmbientTexture = _defaultSkyboxTexture;
_defaultSkybox->setCubemap(_defaultSkyboxTexture);
_defaultSkybox->setCubemap(_defaultSkyboxTexture);
}
EntityItem::setEntitiesShouldFadeFunction([this]() {
SharedNodePointer entityServerNode = DependencyManager::get<NodeList>()->soloNodeOfType(NodeType::EntityServer);

View file

@ -22,6 +22,8 @@
#include "Forward.h"
#include "Resource.h"
const int ABSOLUTE_MAX_TEXTURE_NUM_PIXELS = 8192 * 8192;
namespace ktx {
class KTX;
using KTXUniquePointer = std::unique_ptr<KTX>;

View file

@ -37,7 +37,8 @@ enum Type {
CUBE_TEXTURE,
OCCLUSION_TEXTURE,
SCATTERING_TEXTURE = OCCLUSION_TEXTURE,
LIGHTMAP_TEXTURE
LIGHTMAP_TEXTURE,
UNUSED_TEXTURE
};
using TextureLoader = std::function<gpu::TexturePointer(const QImage&, const std::string&)>;

View file

@ -27,8 +27,6 @@
#include "KTXCache.h"
const int ABSOLUTE_MAX_TEXTURE_NUM_PIXELS = 8192 * 8192;
namespace gpu {
class Batch;
}

View file

@ -313,6 +313,9 @@ void AddressManager::handleAPIResponse(QNetworkReply& requestReply) {
QJsonObject responseObject = QJsonDocument::fromJson(requestReply.readAll()).object();
QJsonObject dataObject = responseObject["data"].toObject();
// Lookup succeeded, don't keep re-trying it (especially on server restarts)
_previousLookup.clear();
if (!dataObject.isEmpty()) {
goToAddressFromObject(dataObject.toVariantMap(), requestReply);
} else if (responseObject.contains(DATA_OBJECT_DOMAIN_KEY)) {
@ -739,6 +742,8 @@ void AddressManager::refreshPreviousLookup() {
// if we have a non-empty previous lookup, fire it again now (but don't re-store it in the history)
if (!_previousLookup.isEmpty()) {
handleUrl(_previousLookup, LookupTrigger::AttemptedRefresh);
} else {
handleUrl(currentAddress(), LookupTrigger::AttemptedRefresh);
}
}

View file

@ -34,7 +34,7 @@ Q_LOGGING_CATEGORY(trace_simulation_physics_detail, "trace.simulation.physics.de
#endif
static bool tracingEnabled() {
return DependencyManager::get<tracing::Tracer>()->isEnabled();
return DependencyManager::isSet<tracing::Tracer>() && DependencyManager::get<tracing::Tracer>()->isEnabled();
}
Duration::Duration(const QLoggingCategory& category, const QString& name, uint32_t argbColor, uint64_t payload, const QVariantMap& baseArgs) : _name(name), _category(category) {

View file

@ -106,6 +106,10 @@ namespace Setting {
return (_isSet) ? _value : other;
}
bool isSet() const {
return _isSet;
}
const T& getDefault() const {
return _defaultValue;
}

View file

@ -21,7 +21,6 @@ namespace Setting {
class Manager;
void init();
void cleanupSettings();
class Interface {
public:

View file

@ -68,7 +68,7 @@ StoragePointer FileStorage::create(const QString& filename, size_t size, const u
}
FileStorage::FileStorage(const QString& filename) : _file(filename) {
if (_file.open(QFile::ReadWrite)) {
if (_file.open(QFile::ReadOnly)) {
_mapped = _file.map(0, _file.size());
if (_mapped) {
_valid = true;
@ -90,3 +90,34 @@ FileStorage::~FileStorage() {
_file.close();
}
}
void FileStorage::ensureWriteAccess() {
if (_hasWriteAccess) {
return;
}
if (_mapped) {
if (!_file.unmap(_mapped)) {
throw std::runtime_error("Unable to unmap file");
}
}
if (_file.isOpen()) {
_file.close();
}
_valid = false;
_mapped = nullptr;
if (_file.open(QFile::ReadWrite)) {
_mapped = _file.map(0, _file.size());
if (_mapped) {
_valid = true;
_hasWriteAccess = true;
} else {
qCWarning(storagelogging) << "Failed to map file " << _file.fileName();
throw std::runtime_error("Failed to map file");
}
} else {
qCWarning(storagelogging) << "Failed to open file " << _file.fileName();
throw std::runtime_error("Failed to open file");
}
}

View file

@ -60,11 +60,14 @@ namespace storage {
FileStorage& operator=(const FileStorage& other) = delete;
const uint8_t* data() const override { return _mapped; }
uint8_t* mutableData() override { return _mapped; }
uint8_t* mutableData() override { ensureWriteAccess(); return _mapped; }
size_t size() const override { return _file.size(); }
operator bool() const override { return _valid; }
private:
void ensureWriteAccess();
bool _valid { false };
bool _hasWriteAccess { false };
QFile _file;
uint8_t* _mapped { nullptr };
};

View file

@ -415,7 +415,7 @@ function updateShareInfo(containerID, storyID) {
facebookButton.setAttribute("href", 'https://www.facebook.com/dialog/feed?app_id=1585088821786423&link=' + shareURL);
twitterButton.setAttribute("target", "_blank");
twitterButton.setAttribute("href", 'https://twitter.com/intent/tweet?text=I%20just%20took%20a%20snapshot!&url=' + shareURL + '&via=highfidelity&hashtags=VR,HiFi');
twitterButton.setAttribute("href", 'https://twitter.com/intent/tweet?text=I%20just%20took%20a%20snapshot!&url=' + shareURL + '&via=highfidelityinc&hashtags=VR,HiFi');
hideUploadingMessageAndShare(containerID, storyID);
}

View file

@ -19,3 +19,6 @@ set_target_properties(skeleton-dump PROPERTIES FOLDER "Tools")
add_subdirectory(atp-get)
set_target_properties(atp-get PROPERTIES FOLDER "Tools")
add_subdirectory(oven)
set_target_properties(oven PROPERTIES FOLDER "Tools")

19
tools/oven/CMakeLists.txt Normal file
View file

@ -0,0 +1,19 @@
set(TARGET_NAME oven)
setup_hifi_project(Widgets Gui Concurrent)
link_hifi_libraries(networking shared image gpu ktx)
if (WIN32)
package_libraries_for_deployment()
endif ()
# try to find the FBX SDK but fail silently if we don't
# because this tool is not built by default
find_package(FBX)
if (FBX_FOUND)
target_link_libraries(${TARGET_NAME} ${FBX_LIBRARIES})
target_include_directories(${TARGET_NAME} SYSTEM PRIVATE ${FBX_INCLUDE_DIR})
endif ()
set_target_properties(${TARGET_NAME} PROPERTIES EXCLUDE_FROM_ALL TRUE EXCLUDE_FROM_DEFAULT_BUILD TRUE)

32
tools/oven/src/Baker.cpp Normal file
View file

@ -0,0 +1,32 @@
//
// Baker.cpp
// tools/oven/src
//
// Created by Stephen Birarda on 4/14/17.
// Copyright 2017 High Fidelity, Inc.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
#include "ModelBakingLoggingCategory.h"
#include "Baker.h"
void Baker::handleError(const QString& error) {
qCCritical(model_baking).noquote() << error;
_errorList.append(error);
emit finished();
}
void Baker::handleErrors(const QStringList& errors) {
// we're appending errors, presumably from a baking operation we called
// add those to our list and emit that we are finished
_errorList.append(errors);
emit finished();
}
void Baker::handleWarning(const QString& warning) {
qCWarning(model_baking).noquote() << warning;
_warningList.append(warning);
}

43
tools/oven/src/Baker.h Normal file
View file

@ -0,0 +1,43 @@
//
// Baker.h
// tools/oven/src
//
// Created by Stephen Birarda on 4/14/17.
// Copyright 2017 High Fidelity, Inc.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
#ifndef hifi_Baker_h
#define hifi_Baker_h
#include <QtCore/QObject>
class Baker : public QObject {
Q_OBJECT
public:
bool hasErrors() const { return !_errorList.isEmpty(); }
QStringList getErrors() const { return _errorList; }
bool hasWarnings() const { return !_warningList.isEmpty(); }
QStringList getWarnings() const { return _warningList; }
public slots:
virtual void bake() = 0;
signals:
void finished();
protected:
void handleError(const QString& error);
void handleWarning(const QString& warning);
void handleErrors(const QStringList& errors);
QStringList _errorList;
QStringList _warningList;
};
#endif // hifi_Baker_h

View file

@ -0,0 +1,475 @@
//
// DomainBaker.cpp
// tools/oven/src
//
// Created by Stephen Birarda on 4/12/17.
// Copyright 2017 High Fidelity, Inc.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
#include <QtConcurrent>
#include <QtCore/QEventLoop>
#include <QtCore/QFile>
#include <QtCore/QFileInfo>
#include <QtCore/QJsonDocument>
#include <QtCore/QJsonObject>
#include "Gzip.h"
#include "Oven.h"
#include "DomainBaker.h"
DomainBaker::DomainBaker(const QUrl& localModelFileURL, const QString& domainName,
const QString& baseOutputPath, const QUrl& destinationPath) :
_localEntitiesFileURL(localModelFileURL),
_domainName(domainName),
_baseOutputPath(baseOutputPath)
{
// make sure the destination path has a trailing slash
if (!destinationPath.toString().endsWith('/')) {
_destinationPath = destinationPath.toString() + '/';
} else {
_destinationPath = destinationPath;
}
}
void DomainBaker::bake() {
setupOutputFolder();
if (hasErrors()) {
return;
}
loadLocalFile();
if (hasErrors()) {
return;
}
enumerateEntities();
if (hasErrors()) {
return;
}
// in case we've baked and re-written all of our entities already, check if we're done
checkIfRewritingComplete();
}
void DomainBaker::setupOutputFolder() {
// in order to avoid overwriting previous bakes, we create a special output folder with the domain name and timestamp
// first, construct the directory name
auto domainPrefix = !_domainName.isEmpty() ? _domainName + "-" : "";
auto timeNow = QDateTime::currentDateTime();
static const QString FOLDER_TIMESTAMP_FORMAT = "yyyyMMdd-hhmmss";
QString outputDirectoryName = domainPrefix + timeNow.toString(FOLDER_TIMESTAMP_FORMAT);
// make sure we can create that directory
QDir outputDir { _baseOutputPath };
if (!outputDir.mkpath(outputDirectoryName)) {
// add an error to specify that the output directory could not be created
handleError("Could not create output folder");
return;
}
// store the unique output path so we can re-use it when saving baked models
outputDir.cd(outputDirectoryName);
_uniqueOutputPath = outputDir.absolutePath();
// add a content folder inside the unique output folder
static const QString CONTENT_OUTPUT_FOLDER_NAME = "content";
if (!outputDir.mkpath(CONTENT_OUTPUT_FOLDER_NAME)) {
// add an error to specify that the content output directory could not be created
handleError("Could not create content folder");
return;
}
_contentOutputPath = outputDir.absoluteFilePath(CONTENT_OUTPUT_FOLDER_NAME);
}
const QString ENTITIES_OBJECT_KEY = "Entities";
void DomainBaker::loadLocalFile() {
// load up the local entities file
QFile entitiesFile { _localEntitiesFileURL.toLocalFile() };
if (!entitiesFile.open(QIODevice::ReadOnly)) {
// add an error to our list to specify that the file could not be read
handleError("Could not open entities file");
// return to stop processing
return;
}
// grab a byte array from the file
auto fileContents = entitiesFile.readAll();
// check if we need to inflate a gzipped models file or if this was already decompressed
static const QString GZIPPED_ENTITIES_FILE_SUFFIX = "gz";
if (QFileInfo(_localEntitiesFileURL.toLocalFile()).suffix() == "gz") {
// this was a gzipped models file that we need to decompress
QByteArray uncompressedContents;
gunzip(fileContents, uncompressedContents);
fileContents = uncompressedContents;
}
// read the file contents to a JSON document
auto jsonDocument = QJsonDocument::fromJson(fileContents);
// grab the entities object from the root JSON object
_entities = jsonDocument.object()[ENTITIES_OBJECT_KEY].toArray();
if (_entities.isEmpty()) {
// add an error to our list stating that the models file was empty
// return to stop processing
return;
}
}
const QString ENTITY_MODEL_URL_KEY = "modelURL";
const QString ENTITY_SKYBOX_KEY = "skybox";
const QString ENTITY_SKYBOX_URL_KEY = "url";
const QString ENTITY_KEYLIGHT_KEY = "keyLight";
const QString ENTITY_KEYLIGHT_AMBIENT_URL_KEY = "ambientURL";
void DomainBaker::enumerateEntities() {
qDebug() << "Enumerating" << _entities.size() << "entities from domain";
for (auto it = _entities.begin(); it != _entities.end(); ++it) {
// make sure this is a JSON object
if (it->isObject()) {
auto entity = it->toObject();
// check if this is an entity with a model URL or is a skybox texture
if (entity.contains(ENTITY_MODEL_URL_KEY)) {
// grab a QUrl for the model URL
QUrl modelURL { entity[ENTITY_MODEL_URL_KEY].toString() };
// check if the file pointed to by this URL is a bakeable model, by comparing extensions
auto modelFileName = modelURL.fileName();
static const QStringList BAKEABLE_MODEL_EXTENSIONS { ".fbx" };
auto completeLowerExtension = modelFileName.mid(modelFileName.indexOf('.')).toLower();
if (BAKEABLE_MODEL_EXTENSIONS.contains(completeLowerExtension)) {
// grab a clean version of the URL without a query or fragment
modelURL = modelURL.adjusted(QUrl::RemoveQuery | QUrl::RemoveFragment);
// setup an FBXBaker for this URL, as long as we don't already have one
if (!_modelBakers.contains(modelURL)) {
QSharedPointer<FBXBaker> baker {
new FBXBaker(modelURL, _contentOutputPath, []() -> QThread* {
return qApp->getNextWorkerThread();
}), &FBXBaker::deleteLater
};
// make sure our handler is called when the baker is done
connect(baker.data(), &Baker::finished, this, &DomainBaker::handleFinishedModelBaker);
// insert it into our bakers hash so we hold a strong pointer to it
_modelBakers.insert(modelURL, baker);
// move the baker to the baker thread
// and kickoff the bake
baker->moveToThread(qApp->getFBXBakerThread());
QMetaObject::invokeMethod(baker.data(), "bake");
// keep track of the total number of baking entities
++_totalNumberOfSubBakes;
}
// add this QJsonValueRef to our multi hash so that we can easily re-write
// the model URL to the baked version once the baker is complete
_entitiesNeedingRewrite.insert(modelURL, *it);
}
} else {
// // We check now to see if we have either a texture for a skybox or a keylight, or both.
// if (entity.contains(ENTITY_SKYBOX_KEY)) {
// auto skyboxObject = entity[ENTITY_SKYBOX_KEY].toObject();
// if (skyboxObject.contains(ENTITY_SKYBOX_URL_KEY)) {
// // we have a URL to a skybox, grab it
// QUrl skyboxURL { skyboxObject[ENTITY_SKYBOX_URL_KEY].toString() };
//
// // setup a bake of the skybox
// bakeSkybox(skyboxURL, *it);
// }
// }
//
// if (entity.contains(ENTITY_KEYLIGHT_KEY)) {
// auto keyLightObject = entity[ENTITY_KEYLIGHT_KEY].toObject();
// if (keyLightObject.contains(ENTITY_KEYLIGHT_AMBIENT_URL_KEY)) {
// // we have a URL to a skybox, grab it
// QUrl skyboxURL { keyLightObject[ENTITY_KEYLIGHT_AMBIENT_URL_KEY].toString() };
//
// // setup a bake of the skybox
// bakeSkybox(skyboxURL, *it);
// }
// }
}
}
}
// emit progress now to say we're just starting
emit bakeProgress(0, _totalNumberOfSubBakes);
}
void DomainBaker::bakeSkybox(QUrl skyboxURL, QJsonValueRef entity) {
auto skyboxFileName = skyboxURL.fileName();
static const QStringList BAKEABLE_SKYBOX_EXTENSIONS {
".jpg", ".png", ".gif", ".bmp", ".pbm", ".pgm", ".ppm", ".xbm", ".xpm", ".svg"
};
auto completeLowerExtension = skyboxFileName.mid(skyboxFileName.indexOf('.')).toLower();
if (BAKEABLE_SKYBOX_EXTENSIONS.contains(completeLowerExtension)) {
// grab a clean version of the URL without a query or fragment
skyboxURL = skyboxURL.adjusted(QUrl::RemoveQuery | QUrl::RemoveFragment);
// setup a texture baker for this URL, as long as we aren't baking a skybox already
if (!_skyboxBakers.contains(skyboxURL)) {
// setup a baker for this skybox
QSharedPointer<TextureBaker> skyboxBaker {
new TextureBaker(skyboxURL, image::TextureUsage::CUBE_TEXTURE, _contentOutputPath),
&TextureBaker::deleteLater
};
// make sure our handler is called when the skybox baker is done
connect(skyboxBaker.data(), &TextureBaker::finished, this, &DomainBaker::handleFinishedSkyboxBaker);
// insert it into our bakers hash so we hold a strong pointer to it
_skyboxBakers.insert(skyboxURL, skyboxBaker);
// move the baker to a worker thread and kickoff the bake
skyboxBaker->moveToThread(qApp->getNextWorkerThread());
QMetaObject::invokeMethod(skyboxBaker.data(), "bake");
// keep track of the total number of baking entities
++_totalNumberOfSubBakes;
}
// add this QJsonValueRef to our multi hash so that it can re-write the skybox URL
// to the baked version once the baker is complete
_entitiesNeedingRewrite.insert(skyboxURL, entity);
}
}
void DomainBaker::handleFinishedModelBaker() {
auto baker = qobject_cast<FBXBaker*>(sender());
if (baker) {
if (!baker->hasErrors()) {
// this FBXBaker is done and everything went according to plan
qDebug() << "Re-writing entity references to" << baker->getFBXUrl();
// enumerate the QJsonRef values for the URL of this FBX from our multi hash of
// entity objects needing a URL re-write
for (QJsonValueRef entityValue : _entitiesNeedingRewrite.values(baker->getFBXUrl())) {
// convert the entity QJsonValueRef to a QJsonObject so we can modify its URL
auto entity = entityValue.toObject();
// grab the old URL
QUrl oldModelURL { entity[ENTITY_MODEL_URL_KEY].toString() };
// setup a new URL using the prefix we were passed
QUrl newModelURL = _destinationPath.resolved(baker->getBakedFBXRelativePath());
// copy the fragment and query, and user info from the old model URL
newModelURL.setQuery(oldModelURL.query());
newModelURL.setFragment(oldModelURL.fragment());
newModelURL.setUserInfo(oldModelURL.userInfo());
// set the new model URL as the value in our temp QJsonObject
entity[ENTITY_MODEL_URL_KEY] = newModelURL.toString();
// check if the entity also had an animation at the same URL
// in which case it should be replaced with our baked model URL too
const QString ENTITY_ANIMATION_KEY = "animation";
const QString ENTITIY_ANIMATION_URL_KEY = "url";
if (entity.contains(ENTITY_ANIMATION_KEY)) {
auto animationObject = entity[ENTITY_ANIMATION_KEY].toObject();
if (animationObject.contains(ENTITIY_ANIMATION_URL_KEY)) {
// grab the old animation URL
QUrl oldAnimationURL { animationObject[ENTITIY_ANIMATION_URL_KEY].toString() };
// check if its stripped down version matches our stripped down model URL
if (oldAnimationURL.matches(oldModelURL, QUrl::RemoveQuery | QUrl::RemoveFragment)) {
// the animation URL matched the old model URL, so make the animation URL point to the baked FBX
// with its original query and fragment
auto newAnimationURL = _destinationPath.resolved(baker->getBakedFBXRelativePath());
newAnimationURL.setQuery(oldAnimationURL.query());
newAnimationURL.setFragment(oldAnimationURL.fragment());
newAnimationURL.setUserInfo(oldAnimationURL.userInfo());
animationObject[ENTITIY_ANIMATION_URL_KEY] = newAnimationURL.toString();
// replace the animation object in the entity object
entity[ENTITY_ANIMATION_KEY] = animationObject;
}
}
}
// replace our temp object with the value referenced by our QJsonValueRef
entityValue = entity;
}
} else {
// this model failed to bake - this doesn't fail the entire bake but we need to add
// the errors from the model to our warnings
_warningList << baker->getErrors();
}
// remove the baked URL from the multi hash of entities needing a re-write
_entitiesNeedingRewrite.remove(baker->getFBXUrl());
// drop our shared pointer to this baker so that it gets cleaned up
_modelBakers.remove(baker->getFBXUrl());
// emit progress to tell listeners how many models we have baked
emit bakeProgress(++_completedSubBakes, _totalNumberOfSubBakes);
// check if this was the last model we needed to re-write and if we are done now
checkIfRewritingComplete();
}
}
void DomainBaker::handleFinishedSkyboxBaker() {
auto baker = qobject_cast<TextureBaker*>(sender());
if (baker) {
if (!baker->hasErrors()) {
// this FBXBaker is done and everything went according to plan
qDebug() << "Re-writing entity references to" << baker->getTextureURL();
// enumerate the QJsonRef values for the URL of this FBX from our multi hash of
// entity objects needing a URL re-write
for (QJsonValueRef entityValue : _entitiesNeedingRewrite.values(baker->getTextureURL())) {
// convert the entity QJsonValueRef to a QJsonObject so we can modify its URL
auto entity = entityValue.toObject();
if (entity.contains(ENTITY_SKYBOX_KEY)) {
auto skyboxObject = entity[ENTITY_SKYBOX_KEY].toObject();
if (skyboxObject.contains(ENTITY_SKYBOX_URL_KEY)) {
if (rewriteSkyboxURL(skyboxObject[ENTITY_SKYBOX_URL_KEY], baker)) {
// we re-wrote the URL, replace the skybox object referenced by the entity object
entity[ENTITY_SKYBOX_KEY] = skyboxObject;
}
}
}
if (entity.contains(ENTITY_KEYLIGHT_KEY)) {
auto ambientObject = entity[ENTITY_KEYLIGHT_KEY].toObject();
if (ambientObject.contains(ENTITY_KEYLIGHT_AMBIENT_URL_KEY)) {
if (rewriteSkyboxURL(ambientObject[ENTITY_KEYLIGHT_AMBIENT_URL_KEY], baker)) {
// we re-wrote the URL, replace the ambient object referenced by the entity object
entity[ENTITY_KEYLIGHT_KEY] = ambientObject;
}
}
}
// replace our temp object with the value referenced by our QJsonValueRef
entityValue = entity;
}
} else {
// this skybox failed to bake - this doesn't fail the entire bake but we need to add the errors from
// the model to our warnings
_warningList << baker->getWarnings();
}
// remove the baked URL from the multi hash of entities needing a re-write
_entitiesNeedingRewrite.remove(baker->getTextureURL());
// drop our shared pointer to this baker so that it gets cleaned up
_skyboxBakers.remove(baker->getTextureURL());
// emit progress to tell listeners how many models we have baked
emit bakeProgress(++_completedSubBakes, _totalNumberOfSubBakes);
// check if this was the last model we needed to re-write and if we are done now
checkIfRewritingComplete();
}
}
bool DomainBaker::rewriteSkyboxURL(QJsonValueRef urlValue, TextureBaker* baker) {
// grab the old skybox URL
QUrl oldSkyboxURL { urlValue.toString() };
if (oldSkyboxURL.matches(baker->getTextureURL(), QUrl::RemoveQuery | QUrl::RemoveFragment)) {
// change the URL to point to the baked texture with its original query and fragment
auto newSkyboxURL = _destinationPath.resolved(baker->getBakedTextureFileName());
newSkyboxURL.setQuery(oldSkyboxURL.query());
newSkyboxURL.setFragment(oldSkyboxURL.fragment());
newSkyboxURL.setUserInfo(oldSkyboxURL.userInfo());
urlValue = newSkyboxURL.toString();
return true;
} else {
return false;
}
}
void DomainBaker::checkIfRewritingComplete() {
if (_entitiesNeedingRewrite.isEmpty()) {
writeNewEntitiesFile();
if (hasErrors()) {
return;
}
// we've now written out our new models file - time to say that we are finished up
emit finished();
}
}
void DomainBaker::writeNewEntitiesFile() {
// we've enumerated all of our entities and re-written all the URLs we'll be able to re-write
// time to write out a main models.json.gz file
// first setup a document with the entities array below the entities key
QJsonDocument entitiesDocument;
QJsonObject rootObject;
rootObject[ENTITIES_OBJECT_KEY] = _entities;
entitiesDocument.setObject(rootObject);
// turn that QJsonDocument into a byte array ready for compression
QByteArray jsonByteArray = entitiesDocument.toJson();
// compress the json byte array using gzip
QByteArray compressedJson;
gzip(jsonByteArray, compressedJson);
// write the gzipped json to a new models file
static const QString MODELS_FILE_NAME = "models.json.gz";
auto bakedEntitiesFilePath = QDir(_uniqueOutputPath).filePath(MODELS_FILE_NAME);
QFile compressedEntitiesFile { bakedEntitiesFilePath };
if (!compressedEntitiesFile.open(QIODevice::WriteOnly)
|| (compressedEntitiesFile.write(compressedJson) == -1)) {
// add an error to our list to state that the output models file could not be created or could not be written to
handleError("Failed to export baked entities file");
return;
}
qDebug() << "Exported entities file with baked model URLs to" << bakedEntitiesFilePath;
}

View file

@ -0,0 +1,70 @@
//
// DomainBaker.h
// tools/oven/src
//
// Created by Stephen Birarda on 4/12/17.
// Copyright 2017 High Fidelity, Inc.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
#ifndef hifi_DomainBaker_h
#define hifi_DomainBaker_h
#include <QtCore/QJsonArray>
#include <QtCore/QObject>
#include <QtCore/QUrl>
#include <QtCore/QThread>
#include "Baker.h"
#include "FBXBaker.h"
#include "TextureBaker.h"
class DomainBaker : public Baker {
Q_OBJECT
public:
// This is a real bummer, but the FBX SDK is not thread safe - even with separate FBXManager objects.
// This means that we need to put all of the FBX importing/exporting from the same process on the same thread.
// That means you must pass a usable running QThread when constructing a domain baker.
DomainBaker(const QUrl& localEntitiesFileURL, const QString& domainName,
const QString& baseOutputPath, const QUrl& destinationPath);
signals:
void allModelsFinished();
void bakeProgress(int baked, int total);
private slots:
virtual void bake() override;
void handleFinishedModelBaker();
void handleFinishedSkyboxBaker();
private:
void setupOutputFolder();
void loadLocalFile();
void enumerateEntities();
void checkIfRewritingComplete();
void writeNewEntitiesFile();
void bakeSkybox(QUrl skyboxURL, QJsonValueRef entity);
bool rewriteSkyboxURL(QJsonValueRef urlValue, TextureBaker* baker);
QUrl _localEntitiesFileURL;
QString _domainName;
QString _baseOutputPath;
QString _uniqueOutputPath;
QString _contentOutputPath;
QUrl _destinationPath;
QJsonArray _entities;
QHash<QUrl, QSharedPointer<FBXBaker>> _modelBakers;
QHash<QUrl, QSharedPointer<TextureBaker>> _skyboxBakers;
QMultiHash<QUrl, QJsonValueRef> _entitiesNeedingRewrite;
int _totalNumberOfSubBakes { 0 };
int _completedSubBakes { 0 };
};
#endif // hifi_DomainBaker_h

554
tools/oven/src/FBXBaker.cpp Normal file
View file

@ -0,0 +1,554 @@
//
// FBXBaker.cpp
// tools/oven/src
//
// Created by Stephen Birarda on 3/30/17.
// Copyright 2017 High Fidelity, Inc.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
#include <cmath> // need this include so we don't get an error looking for std::isnan
#include <fbxsdk.h>
#include <QtConcurrent>
#include <QtCore/QCoreApplication>
#include <QtCore/QDir>
#include <QtCore/QEventLoop>
#include <QtCore/QFileInfo>
#include <QtCore/QThread>
#include <mutex>
#include <NetworkAccessManager.h>
#include <SharedUtil.h>
#include "ModelBakingLoggingCategory.h"
#include "TextureBaker.h"
#include "FBXBaker.h"
std::once_flag onceFlag;
FBXSDKManagerUniquePointer FBXBaker::_sdkManager { nullptr };
FBXBaker::FBXBaker(const QUrl& fbxURL, const QString& baseOutputPath,
TextureBakerThreadGetter textureThreadGetter, bool copyOriginals) :
_fbxURL(fbxURL),
_baseOutputPath(baseOutputPath),
_textureThreadGetter(textureThreadGetter),
_copyOriginals(copyOriginals)
{
std::call_once(onceFlag, [](){
// create the static FBX SDK manager
_sdkManager = FBXSDKManagerUniquePointer(FbxManager::Create(), [](FbxManager* manager){
manager->Destroy();
});
});
// grab the name of the FBX from the URL, this is used for folder output names
auto fileName = fbxURL.fileName();
_fbxName = fileName.left(fileName.lastIndexOf('.'));
}
static const QString BAKED_OUTPUT_SUBFOLDER = "baked/";
static const QString ORIGINAL_OUTPUT_SUBFOLDER = "original/";
QString FBXBaker::pathToCopyOfOriginal() const {
return _uniqueOutputPath + ORIGINAL_OUTPUT_SUBFOLDER + _fbxURL.fileName();
}
void FBXBaker::bake() {
qCDebug(model_baking) << "Baking" << _fbxURL;
// setup the output folder for the results of this bake
setupOutputFolder();
if (hasErrors()) {
return;
}
connect(this, &FBXBaker::sourceCopyReadyToLoad, this, &FBXBaker::bakeSourceCopy);
// make a local copy of the FBX file
loadSourceFBX();
}
void FBXBaker::bakeSourceCopy() {
// load the scene from the FBX file
importScene();
if (hasErrors()) {
return;
}
// enumerate the textures found in the scene and start a bake for them
rewriteAndBakeSceneTextures();
if (hasErrors()) {
return;
}
// export the FBX with re-written texture references
exportScene();
if (hasErrors()) {
return;
}
// check if we're already done with textures (in case we had none to re-write)
checkIfTexturesFinished();
}
void FBXBaker::setupOutputFolder() {
// construct the output path using the name of the fbx and the base output path
_uniqueOutputPath = _baseOutputPath + "/" + _fbxName + "/";
// make sure there isn't already an output directory using the same name
int iteration = 0;
while (QDir(_uniqueOutputPath).exists()) {
_uniqueOutputPath = _baseOutputPath + "/" + _fbxName + "-" + QString::number(++iteration) + "/";
}
qCDebug(model_baking) << "Creating FBX output folder" << _uniqueOutputPath;
// attempt to make the output folder
if (!QDir().mkdir(_uniqueOutputPath)) {
handleError("Failed to create FBX output folder " + _uniqueOutputPath);
return;
}
// make the baked and original sub-folders used during export
QDir uniqueOutputDir = _uniqueOutputPath;
if (!uniqueOutputDir.mkdir(BAKED_OUTPUT_SUBFOLDER) || !uniqueOutputDir.mkdir(ORIGINAL_OUTPUT_SUBFOLDER)) {
handleError("Failed to create baked/original subfolders in " + _uniqueOutputPath);
return;
}
}
void FBXBaker::loadSourceFBX() {
// check if the FBX is local or first needs to be downloaded
if (_fbxURL.isLocalFile()) {
// load up the local file
QFile localFBX { _fbxURL.toLocalFile() };
// make a copy in the output folder
localFBX.copy(pathToCopyOfOriginal());
// emit our signal to start the import of the FBX source copy
emit sourceCopyReadyToLoad();
} else {
// remote file, kick off a download
auto& networkAccessManager = NetworkAccessManager::getInstance();
QNetworkRequest networkRequest;
// setup the request to follow re-directs and always hit the network
networkRequest.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true);
networkRequest.setAttribute(QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::AlwaysNetwork);
networkRequest.setHeader(QNetworkRequest::UserAgentHeader, HIGH_FIDELITY_USER_AGENT);
networkRequest.setUrl(_fbxURL);
qCDebug(model_baking) << "Downloading" << _fbxURL;
auto networkReply = networkAccessManager.get(networkRequest);
connect(networkReply, &QNetworkReply::finished, this, &FBXBaker::handleFBXNetworkReply);
}
}
void FBXBaker::handleFBXNetworkReply() {
auto requestReply = qobject_cast<QNetworkReply*>(sender());
if (requestReply->error() == QNetworkReply::NoError) {
qCDebug(model_baking) << "Downloaded" << _fbxURL;
// grab the contents of the reply and make a copy in the output folder
QFile copyOfOriginal(pathToCopyOfOriginal());
qDebug(model_baking) << "Writing copy of original FBX to" << copyOfOriginal.fileName();
if (!copyOfOriginal.open(QIODevice::WriteOnly) || (copyOfOriginal.write(requestReply->readAll()) == -1)) {
// add an error to the error list for this FBX stating that a duplicate of the original FBX could not be made
handleError("Could not create copy of " + _fbxURL.toString());
return;
}
// close that file now that we are done writing to it
copyOfOriginal.close();
// emit our signal to start the import of the FBX source copy
emit sourceCopyReadyToLoad();
} else {
// add an error to our list stating that the FBX could not be downloaded
handleError("Failed to download " + _fbxURL.toString());
}
}
void FBXBaker::importScene() {
// create an FBX SDK importer
FbxImporter* importer = FbxImporter::Create(_sdkManager.get(), "");
// import the copy of the original FBX file
QString originalCopyPath = pathToCopyOfOriginal();
bool importStatus = importer->Initialize(originalCopyPath.toLocal8Bit().data());
if (!importStatus) {
// failed to initialize importer, print an error and return
handleError("Failed to import " + _fbxURL.toString() + " - " + importer->GetStatus().GetErrorString());
return;
} else {
qCDebug(model_baking) << "Imported" << _fbxURL << "to FbxScene";
}
// setup a new scene to hold the imported file
_scene = FbxScene::Create(_sdkManager.get(), "bakeScene");
// import the file to the created scene
importer->Import(_scene);
// destroy the importer that is no longer needed
importer->Destroy();
}
QString texturePathRelativeToFBX(QUrl fbxURL, QUrl textureURL) {
auto fbxPath = fbxURL.toString(QUrl::RemoveFilename | QUrl::RemoveQuery | QUrl::RemoveFragment);
auto texturePath = textureURL.toString(QUrl::RemoveFilename | QUrl::RemoveQuery | QUrl::RemoveFragment);
if (texturePath.startsWith(fbxPath)) {
// texture path is a child of the FBX path, return the texture path without the fbx path
return texturePath.mid(fbxPath.length());
} else {
// the texture path was not a child of the FBX path, return the empty string
return "";
}
}
QString FBXBaker::createBakedTextureFileName(const QFileInfo& textureFileInfo) {
// first make sure we have a unique base name for this texture
// in case another texture referenced by this model has the same base name
auto nameMatches = _textureNameMatchCount[textureFileInfo.baseName()];
QString bakedTextureFileName { textureFileInfo.completeBaseName() };
if (nameMatches > 0) {
// there are already nameMatches texture with this name
// append - and that number to our baked texture file name so that it is unique
bakedTextureFileName += "-" + QString::number(nameMatches);
}
bakedTextureFileName += BAKED_TEXTURE_EXT;
// increment the number of name matches
++nameMatches;
return bakedTextureFileName;
}
QUrl FBXBaker::getTextureURL(const QFileInfo& textureFileInfo, FbxFileTexture* fileTexture) {
QUrl urlToTexture;
if (textureFileInfo.exists() && textureFileInfo.isFile()) {
// set the texture URL to the local texture that we have confirmed exists
urlToTexture = QUrl::fromLocalFile(textureFileInfo.absoluteFilePath());
} else {
// external texture that we'll need to download or find
// first check if it the RelativePath to the texture in the FBX was relative
QString relativeFileName = fileTexture->GetRelativeFileName();
auto apparentRelativePath = QFileInfo(relativeFileName.replace("\\", "/"));
// this is a relative file path which will require different handling
// depending on the location of the original FBX
if (_fbxURL.isLocalFile() && apparentRelativePath.exists() && apparentRelativePath.isFile()) {
// the absolute path we ran into for the texture in the FBX exists on this machine
// so use that file
urlToTexture = QUrl::fromLocalFile(apparentRelativePath.absoluteFilePath());
} else {
// we didn't find the texture on this machine at the absolute path
// so assume that it is right beside the FBX to match the behaviour of interface
urlToTexture = _fbxURL.resolved(apparentRelativePath.fileName());
}
}
return urlToTexture;
}
image::TextureUsage::Type textureTypeForMaterialProperty(FbxProperty& property, FbxSurfaceMaterial* material) {
using namespace image::TextureUsage;
// this is a property we know has a texture, we need to match it to a High Fidelity known texture type
// since that information is passed to the baking process
// grab the hierarchical name for this property and lowercase it for case-insensitive compare
auto propertyName = QString(property.GetHierarchicalName()).toLower();
// figure out the type of the property based on what known value string it matches
if ((propertyName.contains("diffuse") && !propertyName.contains("tex_global_diffuse"))
|| propertyName.contains("tex_color_map")) {
return ALBEDO_TEXTURE;
} else if (propertyName.contains("transparentcolor") || propertyName.contains("transparencyfactor")) {
return ALBEDO_TEXTURE;
} else if (propertyName.contains("bump")) {
return BUMP_TEXTURE;
} else if (propertyName.contains("normal")) {
return NORMAL_TEXTURE;
} else if ((propertyName.contains("specular") && !propertyName.contains("tex_global_specular"))
|| propertyName.contains("reflection")) {
return SPECULAR_TEXTURE;
} else if (propertyName.contains("tex_metallic_map")) {
return METALLIC_TEXTURE;
} else if (propertyName.contains("shininess")) {
return GLOSS_TEXTURE;
} else if (propertyName.contains("tex_roughness_map")) {
return ROUGHNESS_TEXTURE;
} else if (propertyName.contains("emissive")) {
return EMISSIVE_TEXTURE;
} else if (propertyName.contains("ambientcolor")) {
return LIGHTMAP_TEXTURE;
} else if (propertyName.contains("ambientfactor")) {
// we need to check what the ambient factor is, since that tells Interface to process this texture
// either as an occlusion texture or a light map
auto lambertMaterial = FbxCast<FbxSurfaceLambert>(material);
if (lambertMaterial->AmbientFactor == 0) {
return LIGHTMAP_TEXTURE;
} else if (lambertMaterial->AmbientFactor > 0) {
return OCCLUSION_TEXTURE;
} else {
return UNUSED_TEXTURE;
}
} else if (propertyName.contains("tex_ao_map")) {
return OCCLUSION_TEXTURE;
}
return UNUSED_TEXTURE;
}
void FBXBaker::rewriteAndBakeSceneTextures() {
// enumerate the surface materials to find the textures used in the scene
int numMaterials = _scene->GetMaterialCount();
for (int i = 0; i < numMaterials; i++) {
FbxSurfaceMaterial* material = _scene->GetMaterial(i);
if (material) {
// enumerate the properties of this material to see what texture channels it might have
FbxProperty property = material->GetFirstProperty();
while (property.IsValid()) {
// first check if this property has connected textures, if not we don't need to bother with it here
if (property.GetSrcObjectCount<FbxTexture>() > 0) {
// figure out the type of texture from the material property
auto textureType = textureTypeForMaterialProperty(property, material);
if (textureType != image::TextureUsage::UNUSED_TEXTURE) {
int numTextures = property.GetSrcObjectCount<FbxFileTexture>();
for (int j = 0; j < numTextures; j++) {
FbxFileTexture* fileTexture = property.GetSrcObject<FbxFileTexture>(j);
// use QFileInfo to easily split up the existing texture filename into its components
QString fbxFileName { fileTexture->GetFileName() };
QFileInfo textureFileInfo { fbxFileName.replace("\\", "/") };
// make sure this texture points to something and isn't one we've already re-mapped
if (!textureFileInfo.filePath().isEmpty()
&& textureFileInfo.suffix() != BAKED_TEXTURE_EXT.mid(1)) {
// construct the new baked texture file name and file path
// ensuring that the baked texture will have a unique name
// even if there was another texture with the same name at a different path
auto bakedTextureFileName = createBakedTextureFileName(textureFileInfo);
QString bakedTextureFilePath {
_uniqueOutputPath + BAKED_OUTPUT_SUBFOLDER + bakedTextureFileName
};
qCDebug(model_baking).noquote() << "Re-mapping" << fileTexture->GetFileName()
<< "to" << bakedTextureFilePath;
// write the new filename into the FBX scene
fileTexture->SetFileName(bakedTextureFilePath.toLocal8Bit());
// write the relative filename to be the baked texture file name since it will
// be right beside the FBX
fileTexture->SetRelativeFileName(bakedTextureFileName.toLocal8Bit().constData());
// figure out the URL to this texture, embedded or external
auto urlToTexture = getTextureURL(textureFileInfo, fileTexture);
if (!_bakingTextures.contains(urlToTexture)) {
// bake this texture asynchronously
bakeTexture(urlToTexture, textureType, _uniqueOutputPath + BAKED_OUTPUT_SUBFOLDER);
}
}
}
}
}
property = material->GetNextProperty(property);
}
}
}
}
void FBXBaker::bakeTexture(const QUrl& textureURL, image::TextureUsage::Type textureType, const QDir& outputDir) {
// start a bake for this texture and add it to our list to keep track of
QSharedPointer<TextureBaker> bakingTexture {
new TextureBaker(textureURL, textureType, outputDir),
&TextureBaker::deleteLater
};
// make sure we hear when the baking texture is done
connect(bakingTexture.data(), &Baker::finished, this, &FBXBaker::handleBakedTexture);
// keep a shared pointer to the baking texture
_bakingTextures.insert(textureURL, bakingTexture);
// start baking the texture on one of our available worker threads
bakingTexture->moveToThread(_textureThreadGetter());
QMetaObject::invokeMethod(bakingTexture.data(), "bake");
}
void FBXBaker::handleBakedTexture() {
TextureBaker* bakedTexture = qobject_cast<TextureBaker*>(sender());
// make sure we haven't already run into errors, and that this is a valid texture
if (bakedTexture) {
if (!hasErrors()) {
if (!bakedTexture->hasErrors()) {
if (_copyOriginals) {
// we've been asked to make copies of the originals, so we need to make copies of this if it is a linked texture
// use the path to the texture being baked to determine if this was an embedded or a linked texture
// it is embeddded if the texure being baked was inside the original output folder
// since that is where the FBX SDK places the .fbm folder it generates when importing the FBX
auto originalOutputFolder = QUrl::fromLocalFile(_uniqueOutputPath + ORIGINAL_OUTPUT_SUBFOLDER);
if (!originalOutputFolder.isParentOf(bakedTexture->getTextureURL())) {
// for linked textures we want to save a copy of original texture beside the original FBX
qCDebug(model_baking) << "Saving original texture for" << bakedTexture->getTextureURL();
// check if we have a relative path to use for the texture
auto relativeTexturePath = texturePathRelativeToFBX(_fbxURL, bakedTexture->getTextureURL());
QFile originalTextureFile {
_uniqueOutputPath + ORIGINAL_OUTPUT_SUBFOLDER + relativeTexturePath + bakedTexture->getTextureURL().fileName()
};
if (relativeTexturePath.length() > 0) {
// make the folders needed by the relative path
}
if (originalTextureFile.open(QIODevice::WriteOnly) && originalTextureFile.write(bakedTexture->getOriginalTexture()) != -1) {
qCDebug(model_baking) << "Saved original texture file" << originalTextureFile.fileName()
<< "for" << _fbxURL;
} else {
handleError("Could not save original external texture " + originalTextureFile.fileName()
+ " for " + _fbxURL.toString());
return;
}
}
}
// now that this texture has been baked and handled, we can remove that TextureBaker from our hash
_bakingTextures.remove(bakedTexture->getTextureURL());
checkIfTexturesFinished();
} else {
// there was an error baking this texture - add it to our list of errors
_errorList.append(bakedTexture->getErrors());
// we don't emit finished yet so that the other textures can finish baking first
_pendingErrorEmission = true;
// now that this texture has been baked, even though it failed, we can remove that TextureBaker from our list
_bakingTextures.remove(bakedTexture->getTextureURL());
checkIfTexturesFinished();
}
} else {
// we have errors to attend to, so we don't do extra processing for this texture
// but we do need to remove that TextureBaker from our list
// and then check if we're done with all textures
_bakingTextures.remove(bakedTexture->getTextureURL());
checkIfTexturesFinished();
}
}
}
void FBXBaker::exportScene() {
// setup the exporter
FbxExporter* exporter = FbxExporter::Create(_sdkManager.get(), "");
auto rewrittenFBXPath = _uniqueOutputPath + BAKED_OUTPUT_SUBFOLDER + _fbxName + BAKED_FBX_EXTENSION;
// save the relative path to this FBX inside our passed output folder
_bakedFBXRelativePath = rewrittenFBXPath;
_bakedFBXRelativePath.remove(_baseOutputPath + "/");
bool exportStatus = exporter->Initialize(rewrittenFBXPath.toLocal8Bit().data());
if (!exportStatus) {
// failed to initialize exporter, print an error and return
handleError("Failed to export FBX file at " + _fbxURL.toString() + " to " + rewrittenFBXPath
+ "- error: " + exporter->GetStatus().GetErrorString());
}
// export the scene
exporter->Export(_scene);
qCDebug(model_baking) << "Exported" << _fbxURL << "with re-written paths to" << rewrittenFBXPath;
}
void FBXBaker::removeEmbeddedMediaFolder() {
// now that the bake is complete, remove the embedded media folder produced by the FBX SDK when it imports an FBX
auto embeddedMediaFolderName = _fbxURL.fileName().replace(".fbx", ".fbm");
QDir(_uniqueOutputPath + ORIGINAL_OUTPUT_SUBFOLDER + embeddedMediaFolderName).removeRecursively();
}
void FBXBaker::possiblyCleanupOriginals() {
if (!_copyOriginals) {
// caller did not ask us to keep the original around, so delete the original output folder now
QDir(_uniqueOutputPath + ORIGINAL_OUTPUT_SUBFOLDER).removeRecursively();
}
}
void FBXBaker::checkIfTexturesFinished() {
// check if we're done everything we need to do for this FBX
// and emit our finished signal if we're done
if (_bakingTextures.isEmpty()) {
// remove the embedded media folder that the FBX SDK produces when reading the original
removeEmbeddedMediaFolder();
// cleanup the originals if we weren't asked to keep them around
possiblyCleanupOriginals();
if (hasErrors()) {
// if we're checking for completion but we have errors
// that means one or more of our texture baking operations failed
if (_pendingErrorEmission) {
emit finished();
}
return;
} else {
qCDebug(model_baking) << "Finished baking" << _fbxURL;
emit finished();
}
}
}

101
tools/oven/src/FBXBaker.h Normal file
View file

@ -0,0 +1,101 @@
//
// FBXBaker.h
// tools/oven/src
//
// Created by Stephen Birarda on 3/30/17.
// Copyright 2017 High Fidelity, Inc.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
#ifndef hifi_FBXBaker_h
#define hifi_FBXBaker_h
#include <QtCore/QFutureSynchronizer>
#include <QtCore/QDir>
#include <QtCore/QUrl>
#include <QtNetwork/QNetworkReply>
#include "Baker.h"
#include "TextureBaker.h"
#include <gpu/Texture.h>
namespace fbxsdk {
class FbxManager;
class FbxProperty;
class FbxScene;
class FbxFileTexture;
}
static const QString BAKED_FBX_EXTENSION = ".baked.fbx";
using FBXSDKManagerUniquePointer = std::unique_ptr<fbxsdk::FbxManager, std::function<void (fbxsdk::FbxManager *)>>;
using TextureBakerThreadGetter = std::function<QThread*()>;
class FBXBaker : public Baker {
Q_OBJECT
public:
FBXBaker(const QUrl& fbxURL, const QString& baseOutputPath,
TextureBakerThreadGetter textureThreadGetter, bool copyOriginals = true);
QUrl getFBXUrl() const { return _fbxURL; }
QString getBakedFBXRelativePath() const { return _bakedFBXRelativePath; }
public slots:
// all calls to FBXBaker::bake for FBXBaker instances must be from the same thread
// because the Autodesk SDK will cause a crash if it is called from multiple threads
virtual void bake() override;
signals:
void sourceCopyReadyToLoad();
private slots:
void bakeSourceCopy();
void handleFBXNetworkReply();
void handleBakedTexture();
private:
void setupOutputFolder();
void loadSourceFBX();
void bakeCopiedFBX();
void importScene();
void rewriteAndBakeSceneTextures();
void exportScene();
void removeEmbeddedMediaFolder();
void possiblyCleanupOriginals();
void checkIfTexturesFinished();
QString createBakedTextureFileName(const QFileInfo& textureFileInfo);
QUrl getTextureURL(const QFileInfo& textureFileInfo, fbxsdk::FbxFileTexture* fileTexture);
void bakeTexture(const QUrl& textureURL, image::TextureUsage::Type textureType, const QDir& outputDir);
QString pathToCopyOfOriginal() const;
QUrl _fbxURL;
QString _fbxName;
QString _baseOutputPath;
QString _uniqueOutputPath;
QString _bakedFBXRelativePath;
static FBXSDKManagerUniquePointer _sdkManager;
fbxsdk::FbxScene* _scene { nullptr };
QMultiHash<QUrl, QSharedPointer<TextureBaker>> _bakingTextures;
QHash<QString, int> _textureNameMatchCount;
TextureBakerThreadGetter _textureThreadGetter;
bool _copyOriginals { true };
bool _pendingErrorEmission { false };
};
#endif // hifi_FBXBaker_h

View file

@ -0,0 +1,14 @@
//
// ModelBakingLoggingCategory.cpp
// tools/oven/src
//
// Created by Stephen Birarda on 4/5/17.
// Copyright 2017 High Fidelity, Inc.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
#include "ModelBakingLoggingCategory.h"
Q_LOGGING_CATEGORY(model_baking, "hifi.model-baking");

View file

@ -0,0 +1,19 @@
//
// ModelBakingLoggingCategory.h
// tools/oven/src
//
// Created by Stephen Birarda on 4/5/17.
// Copyright 2017 High Fidelity, Inc.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
#ifndef hifi_ModelBakingLoggingCategory_h
#define hifi_ModelBakingLoggingCategory_h
#include <QtCore/QLoggingCategory>
Q_DECLARE_LOGGING_CATEGORY(model_baking)
#endif // hifi_ModelBakingLoggingCategory_h

100
tools/oven/src/Oven.cpp Normal file
View file

@ -0,0 +1,100 @@
//
// Oven.cpp
// tools/oven/src
//
// Created by Stephen Birarda on 4/5/17.
// Copyright 2017 High Fidelity, Inc.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
#include <QtCore/QDebug>
#include <QtCore/QThread>
#include <SettingInterface.h>
#include "ui/OvenMainWindow.h"
#include "Oven.h"
static const QString OUTPUT_FOLDER = "/Users/birarda/code/hifi/lod/test-oven/export";
Oven::Oven(int argc, char* argv[]) :
QApplication(argc, argv)
{
QCoreApplication::setOrganizationName("High Fidelity");
QCoreApplication::setApplicationName("Oven");
// init the settings interface so we can save and load settings
Setting::init();
// check if we were passed any command line arguments that would tell us just to run without the GUI
// setup the GUI
_mainWindow = new OvenMainWindow;
_mainWindow->show();
// setup our worker threads
setupWorkerThreads(QThread::idealThreadCount() - 1);
// Autodesk's SDK means that we need a single thread for all FBX importing/exporting in the same process
// setup the FBX Baker thread
setupFBXBakerThread();
}
Oven::~Oven() {
// cleanup the worker threads
for (auto i = 0; i < _workerThreads.size(); ++i) {
_workerThreads[i]->quit();
_workerThreads[i]->wait();
}
// cleanup the FBX Baker thread
_fbxBakerThread->quit();
_fbxBakerThread->wait();
}
void Oven::setupWorkerThreads(int numWorkerThreads) {
for (auto i = 0; i < numWorkerThreads; ++i) {
// setup a worker thread yet and add it to our concurrent vector
auto newThread = new QThread(this);
newThread->setObjectName("Oven Worker Thread " + QString::number(i + 1));
_workerThreads.push_back(newThread);
}
}
void Oven::setupFBXBakerThread() {
// we're being asked for the FBX baker thread, but we don't have one yet
// so set that up now
_fbxBakerThread = new QThread(this);
_fbxBakerThread->setObjectName("Oven FBX Baker Thread");
}
QThread* Oven::getFBXBakerThread() {
if (!_fbxBakerThread->isRunning()) {
// start the FBX baker thread if it isn't running yet
_fbxBakerThread->start();
}
return _fbxBakerThread;
}
QThread* Oven::getNextWorkerThread() {
// Here we replicate some of the functionality of QThreadPool by giving callers an available worker thread to use.
// We can't use QThreadPool because we want to put QObjects with signals/slots on these threads.
// So instead we setup our own list of threads, up to one less than the ideal thread count
// (for the FBX Baker Thread to have room), and cycle through them to hand a usable running thread back to our callers.
auto nextIndex = ++_nextWorkerThreadIndex;
auto nextThread = _workerThreads[nextIndex % _workerThreads.size()];
// start the thread if it isn't running yet
if (!nextThread->isRunning()) {
nextThread->start();
}
return nextThread;
}

53
tools/oven/src/Oven.h Normal file
View file

@ -0,0 +1,53 @@
//
// Oven.h
// tools/oven/src
//
// Created by Stephen Birarda on 4/5/17.
// Copyright 2017 High Fidelity, Inc.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
#ifndef hifi_Oven_h
#define hifi_Oven_h
#include <QtWidgets/QApplication>
#include <tbb/concurrent_vector.h>
#include <atomic>
#if defined(qApp)
#undef qApp
#endif
#define qApp (static_cast<Oven*>(QCoreApplication::instance()))
class OvenMainWindow;
class Oven : public QApplication {
Q_OBJECT
public:
Oven(int argc, char* argv[]);
~Oven();
OvenMainWindow* getMainWindow() const { return _mainWindow; }
QThread* getFBXBakerThread();
QThread* getNextWorkerThread();
private:
void setupWorkerThreads(int numWorkerThreads);
void setupFBXBakerThread();
OvenMainWindow* _mainWindow;
QThread* _fbxBakerThread;
QList<QThread*> _workerThreads;
std::atomic<uint> _nextWorkerThreadIndex;
int _numWorkerThreads;
};
#endif // hifi_Oven_h

View file

@ -0,0 +1,131 @@
//
// TextureBaker.cpp
// tools/oven/src
//
// Created by Stephen Birarda on 4/5/17.
// Copyright 2017 High Fidelity, Inc.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
#include <QtCore/QDir>
#include <QtCore/QEventLoop>
#include <QtCore/QFile>
#include <QtNetwork/QNetworkReply>
#include <image/Image.h>
#include <ktx/KTX.h>
#include <NetworkAccessManager.h>
#include <SharedUtil.h>
#include "ModelBakingLoggingCategory.h"
#include "TextureBaker.h"
const QString BAKED_TEXTURE_EXT = ".ktx";
TextureBaker::TextureBaker(const QUrl& textureURL, image::TextureUsage::Type textureType, const QDir& outputDirectory) :
_textureURL(textureURL),
_textureType(textureType),
_outputDirectory(outputDirectory)
{
// figure out the baked texture filename
auto originalFilename = textureURL.fileName();
_bakedTextureFileName = originalFilename.left(originalFilename.lastIndexOf('.')) + BAKED_TEXTURE_EXT;
}
void TextureBaker::bake() {
// once our texture is loaded, kick off a the processing
connect(this, &TextureBaker::originalTextureLoaded, this, &TextureBaker::processTexture);
// first load the texture (either locally or remotely)
loadTexture();
}
void TextureBaker::loadTexture() {
// check if the texture is local or first needs to be downloaded
if (_textureURL.isLocalFile()) {
// load up the local file
QFile localTexture { _textureURL.toLocalFile() };
if (!localTexture.open(QIODevice::ReadOnly)) {
handleError("Unable to open texture " + _textureURL.toString());
return;
}
_originalTexture = localTexture.readAll();
emit originalTextureLoaded();
} else {
// remote file, kick off a download
auto& networkAccessManager = NetworkAccessManager::getInstance();
QNetworkRequest networkRequest;
// setup the request to follow re-directs and always hit the network
networkRequest.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true);
networkRequest.setAttribute(QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::AlwaysNetwork);
networkRequest.setHeader(QNetworkRequest::UserAgentHeader, HIGH_FIDELITY_USER_AGENT);
networkRequest.setUrl(_textureURL);
qCDebug(model_baking) << "Downloading" << _textureURL;
// kickoff the download, wait for slot to tell us it is done
auto networkReply = networkAccessManager.get(networkRequest);
connect(networkReply, &QNetworkReply::finished, this, &TextureBaker::handleTextureNetworkReply);
}
}
void TextureBaker::handleTextureNetworkReply() {
auto requestReply = qobject_cast<QNetworkReply*>(sender());
if (requestReply->error() == QNetworkReply::NoError) {
qCDebug(model_baking) << "Downloaded texture" << _textureURL;
// store the original texture so it can be passed along for the bake
_originalTexture = requestReply->readAll();
emit originalTextureLoaded();
} else {
// add an error to our list stating that this texture could not be downloaded
handleError("Error downloading " + _textureURL.toString() + " - " + requestReply->errorString());
}
}
void TextureBaker::processTexture() {
auto processedTexture = image::processImage(_originalTexture, _textureURL.toString().toStdString(),
ABSOLUTE_MAX_TEXTURE_NUM_PIXELS, _textureType);
if (!processedTexture) {
handleError("Could not process texture " + _textureURL.toString());
return;
}
// the baked textures need to have the source hash added for cache checks in Interface
// so we add that to the processed texture before handling it off to be serialized
auto hashData = QCryptographicHash::hash(_originalTexture, QCryptographicHash::Md5);
std::string hash = hashData.toHex().toStdString();
processedTexture->setSourceHash(hash);
auto memKTX = gpu::Texture::serialize(*processedTexture);
if (!memKTX) {
handleError("Could not serialize " + _textureURL.toString() + " to KTX");
return;
}
const char* data = reinterpret_cast<const char*>(memKTX->_storage->data());
const size_t length = memKTX->_storage->size();
// attempt to write the baked texture to the destination file path
QFile bakedTextureFile { _outputDirectory.absoluteFilePath(_bakedTextureFileName) };
if (!bakedTextureFile.open(QIODevice::WriteOnly) || bakedTextureFile.write(data, length) == -1) {
handleError("Could not write baked texture for " + _textureURL.toString());
}
qCDebug(model_baking) << "Baked texture" << _textureURL;
emit finished();
}

View file

@ -0,0 +1,59 @@
//
// TextureBaker.h
// tools/oven/src
//
// Created by Stephen Birarda on 4/5/17.
// Copyright 2017 High Fidelity, Inc.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
#ifndef hifi_TextureBaker_h
#define hifi_TextureBaker_h
#include <QtCore/QObject>
#include <QtCore/QUrl>
#include <QtCore/QRunnable>
#include <image/Image.h>
#include "Baker.h"
extern const QString BAKED_TEXTURE_EXT;
class TextureBaker : public Baker {
Q_OBJECT
public:
TextureBaker(const QUrl& textureURL, image::TextureUsage::Type textureType, const QDir& outputDirectory);
const QByteArray& getOriginalTexture() const { return _originalTexture; }
QUrl getTextureURL() const { return _textureURL; }
QString getDestinationFilePath() const { return _outputDirectory.absoluteFilePath(_bakedTextureFileName); }
QString getBakedTextureFileName() const { return _bakedTextureFileName; }
public slots:
virtual void bake() override;
signals:
void originalTextureLoaded();
private slots:
void processTexture();
private:
void loadTexture();
void handleTextureNetworkReply();
QUrl _textureURL;
QByteArray _originalTexture;
image::TextureUsage::Type _textureType;
QDir _outputDirectory;
QString _bakedTextureFileName;
};
#endif // hifi_TextureBaker_h

16
tools/oven/src/main.cpp Normal file
View file

@ -0,0 +1,16 @@
//
// main.cpp
// tools/oven/src
//
// Created by Stephen Birarda on 3/28/2017.
// Copyright 2017 High Fidelity, Inc.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
#include "Oven.h"
int main (int argc, char** argv) {
Oven app(argc, argv);
return app.exec();
}

View file

@ -0,0 +1,46 @@
//
// BakeWidget.cpp
// tools/oven/src/ui
//
// Created by Stephen Birarda on 4/17/17.
// Copyright 2017 High Fidelity, Inc.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
#include <QtWidgets/QStackedWidget>
#include "../Oven.h"
#include "OvenMainWindow.h"
#include "BakeWidget.h"
BakeWidget::BakeWidget(QWidget* parent, Qt::WindowFlags flags) :
QWidget(parent, flags)
{
}
BakeWidget::~BakeWidget() {
// if we're going down, our bakers are about to too
// enumerate them, send a cancelled status to the results table, and remove them
auto it = _bakers.begin();
while (it != _bakers.end()) {
auto resultRow = it->second;
auto resultsWindow = qApp->getMainWindow()->showResultsWindow();
resultsWindow->changeStatusForRow(resultRow, "Cancelled");
it = _bakers.erase(it);
}
}
void BakeWidget::cancelButtonClicked() {
// the user wants to go back to the mode selection screen
// remove ourselves from the stacked widget and call delete later so we'll be cleaned up
auto stackedWidget = qobject_cast<QStackedWidget*>(parentWidget());
stackedWidget->removeWidget(this);
this->deleteLater();
}

View file

@ -0,0 +1,33 @@
//
// BakeWidget.h
// tools/oven/src/ui
//
// Created by Stephen Birarda on 4/17/17.
// Copyright 2017 High Fidelity, Inc.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
#ifndef hifi_BakeWidget_h
#define hifi_BakeWidget_h
#include <QtWidgets/QWidget>
#include "../Baker.h"
class BakeWidget : public QWidget {
Q_OBJECT
public:
BakeWidget(QWidget* parent = nullptr, Qt::WindowFlags flags = Qt::WindowFlags());
~BakeWidget();
void cancelButtonClicked();
protected:
using BakerRowPair = std::pair<std::unique_ptr<Baker>, int>;
using BakerRowPairList = std::list<BakerRowPair>;
BakerRowPairList _bakers;
};
#endif // hifi_BakeWidget_h

View file

@ -0,0 +1,286 @@
//
// DomainBakeWidget.cpp
// tools/oven/src/ui
//
// Created by Stephen Birarda on 4/12/17.
// Copyright 2017 High Fidelity, Inc.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
#include <QtConcurrent>
#include <QtWidgets/QFileDialog>
#include <QtWidgets/QGridLayout>
#include <QtWidgets/QLabel>
#include <QtWidgets/QLineEdit>
#include <QtWidgets/QPushButton>
#include <QtCore/QDir>
#include <QtCore/QDebug>
#include "../Oven.h"
#include "OvenMainWindow.h"
#include "DomainBakeWidget.h"
static const QString DOMAIN_NAME_SETTING_KEY = "domain_name";
static const QString EXPORT_DIR_SETTING_KEY = "domain_export_directory";
static const QString BROWSE_START_DIR_SETTING_KEY = "domain_search_directory";
static const QString DESTINATION_PATH_SETTING_KEY = "destination_path";
DomainBakeWidget::DomainBakeWidget(QWidget* parent, Qt::WindowFlags flags) :
BakeWidget(parent, flags),
_domainNameSetting(DOMAIN_NAME_SETTING_KEY),
_exportDirectory(EXPORT_DIR_SETTING_KEY),
_browseStartDirectory(BROWSE_START_DIR_SETTING_KEY),
_destinationPathSetting(DESTINATION_PATH_SETTING_KEY)
{
setupUI();
}
void DomainBakeWidget::setupUI() {
// setup a grid layout to hold everything
QGridLayout* gridLayout = new QGridLayout;
int rowIndex = 0;
// setup a section to enter the name of the domain being baked
QLabel* domainNameLabel = new QLabel("Domain Name");
_domainNameLineEdit = new QLineEdit;
_domainNameLineEdit->setPlaceholderText("welcome");
// set the text of the domain name from whatever was used during last bake
if (!_domainNameSetting.get().isEmpty()) {
_domainNameLineEdit->setText(_domainNameSetting.get());
}
gridLayout->addWidget(domainNameLabel);
gridLayout->addWidget(_domainNameLineEdit, rowIndex, 1, 1, -1);
++rowIndex;
// setup a section to choose the file being baked
QLabel* entitiesFileLabel = new QLabel("Entities File");
_entitiesFileLineEdit = new QLineEdit;
_entitiesFileLineEdit->setPlaceholderText("File");
QPushButton* chooseFileButton = new QPushButton("Browse...");
connect(chooseFileButton, &QPushButton::clicked, this, &DomainBakeWidget::chooseFileButtonClicked);
// add the components for the entities file picker to the layout
gridLayout->addWidget(entitiesFileLabel, rowIndex, 0);
gridLayout->addWidget(_entitiesFileLineEdit, rowIndex, 1, 1, 3);
gridLayout->addWidget(chooseFileButton, rowIndex, 4);
// start a new row for next component
++rowIndex;
// setup a section to choose the output directory
QLabel* outputDirectoryLabel = new QLabel("Output Directory");
_outputDirLineEdit = new QLineEdit;
// set the current export directory to whatever was last used
_outputDirLineEdit->setText(_exportDirectory.get());
// whenever the output directory line edit changes, update the value in settings
connect(_outputDirLineEdit, &QLineEdit::textChanged, this, &DomainBakeWidget::outputDirectoryChanged);
QPushButton* chooseOutputDirectoryButton = new QPushButton("Browse...");
connect(chooseOutputDirectoryButton, &QPushButton::clicked, this, &DomainBakeWidget::chooseOutputDirButtonClicked);
// add the components for the output directory picker to the layout
gridLayout->addWidget(outputDirectoryLabel, rowIndex, 0);
gridLayout->addWidget(_outputDirLineEdit, rowIndex, 1, 1, 3);
gridLayout->addWidget(chooseOutputDirectoryButton, rowIndex, 4);
// start a new row for the next component
++rowIndex;
// setup a section to choose the upload prefix - the URL where baked models will be made available
QLabel* uploadPrefixLabel = new QLabel("Destination URL Path");
_destinationPathLineEdit = new QLineEdit;
_destinationPathLineEdit->setPlaceholderText("http://cdn.example.com/baked-domain/");
if (!_destinationPathSetting.get().isEmpty()) {
_destinationPathLineEdit->setText(_destinationPathSetting.get());
}
gridLayout->addWidget(uploadPrefixLabel, rowIndex, 0);
gridLayout->addWidget(_destinationPathLineEdit, rowIndex, 1, 1, -1);
// start a new row for the next component
++rowIndex;
// add a horizontal line to split the bake/cancel buttons off
QFrame* lineFrame = new QFrame;
lineFrame->setFrameShape(QFrame::HLine);
lineFrame->setFrameShadow(QFrame::Sunken);
gridLayout->addWidget(lineFrame, rowIndex, 0, 1, -1);
// start a new row for the next component
++rowIndex;
// add a button that will kickoff the bake
QPushButton* bakeButton = new QPushButton("Bake");
connect(bakeButton, &QPushButton::clicked, this, &DomainBakeWidget::bakeButtonClicked);
gridLayout->addWidget(bakeButton, rowIndex, 3);
// add a cancel button to go back to the modes page
QPushButton* cancelButton = new QPushButton("Cancel");
connect(cancelButton, &QPushButton::clicked, this, &DomainBakeWidget::cancelButtonClicked);
gridLayout->addWidget(cancelButton, rowIndex, 4);
setLayout(gridLayout);
}
void DomainBakeWidget::chooseFileButtonClicked() {
// pop a file dialog so the user can select the entities file
// if we have picked an FBX before, start in the folder that matches the last path
// otherwise start in the home directory
auto startDir = _browseStartDirectory.get();
if (startDir.isEmpty()) {
startDir = QDir::homePath();
}
auto selectedFile = QFileDialog::getOpenFileName(this, "Choose Entities File", startDir,
"Entities File (*.json *.gz)");
if (!selectedFile.isEmpty()) {
// set the contents of the entities file text box to be the path to the selected file
_entitiesFileLineEdit->setText(selectedFile);
auto directoryOfEntitiesFile = QFileInfo(selectedFile).absolutePath();
// save the directory containing this entities file so we can default to it next time we show the file dialog
_browseStartDirectory.set(directoryOfEntitiesFile);
}
}
void DomainBakeWidget::chooseOutputDirButtonClicked() {
// pop a file dialog so the user can select the output directory
// if we have a previously selected output directory, use that as the initial path in the choose dialog
// otherwise use the user's home directory
auto startDir = _exportDirectory.get();
if (startDir.isEmpty()) {
startDir = QDir::homePath();
}
auto selectedDir = QFileDialog::getExistingDirectory(this, "Choose Output Directory", startDir);
if (!selectedDir.isEmpty()) {
// set the contents of the output directory text box to be the path to the directory
_outputDirLineEdit->setText(selectedDir);
}
}
void DomainBakeWidget::outputDirectoryChanged(const QString& newDirectory) {
// update the export directory setting so we can re-use it next time
_exportDirectory.set(newDirectory);
}
void DomainBakeWidget::bakeButtonClicked() {
// save whatever the current domain name is in settings, we'll re-use it next time the widget is shown
_domainNameSetting.set(_domainNameLineEdit->text());
// save whatever the current destination path is in settings, we'll re-use it next time the widget is shown
_destinationPathSetting.set(_destinationPathLineEdit->text());
// make sure we have a valid output directory
QDir outputDirectory(_outputDirLineEdit->text());
if (!outputDirectory.exists()) {
return;
}
// make sure we have a non empty URL to an entities file to bake
if (!_entitiesFileLineEdit->text().isEmpty()) {
// everything seems to be in place, kick off a bake for this entities file now
auto fileToBakeURL = QUrl::fromLocalFile(_entitiesFileLineEdit->text());
auto domainBaker = std::unique_ptr<DomainBaker> {
new DomainBaker(fileToBakeURL, _domainNameLineEdit->text(),
outputDirectory.absolutePath(), _destinationPathLineEdit->text())
};
// make sure we hear from the baker when it is done
connect(domainBaker.get(), &DomainBaker::finished, this, &DomainBakeWidget::handleFinishedBaker);
// watch the baker's progress so that we can put its progress in the results table
connect(domainBaker.get(), &DomainBaker::bakeProgress, this, &DomainBakeWidget::handleBakerProgress);
// move the baker to the next available Oven worker thread
auto nextThread = qApp->getNextWorkerThread();
domainBaker->moveToThread(nextThread);
// kickoff the domain baker on its thread
QMetaObject::invokeMethod(domainBaker.get(), "bake");
// add a pending row to the results window to show that this bake is in process
auto resultsWindow = qApp->getMainWindow()->showResultsWindow();
auto resultsRowName = _domainNameLineEdit->text().isEmpty() ? fileToBakeURL.fileName() : _domainNameLineEdit->text();
auto resultsRow = resultsWindow->addPendingResultRow(resultsRowName, outputDirectory);
// keep the unique ptr to the domain baker and the index to the row representing it in the results table
_bakers.emplace_back(std::move(domainBaker), resultsRow);
}
}
void DomainBakeWidget::handleBakerProgress(int baked, int total) {
if (auto baker = qobject_cast<DomainBaker*>(sender())) {
// add the results of this bake to the results window
auto it = std::find_if(_bakers.begin(), _bakers.end(), [baker](const BakerRowPair& value) {
return value.first.get() == baker;
});
if (it != _bakers.end()) {
auto resultRow = it->second;
// grab the results window, don't force it to be brought to the top
auto resultsWindow = qApp->getMainWindow()->showResultsWindow(false);
int percentage = roundf(float(baked) / float(total) * 100.0f);
auto statusString = QString("Baking - %1 of %2 - %3%").arg(baked).arg(total).arg(percentage);
resultsWindow->changeStatusForRow(resultRow, statusString);
}
}
}
void DomainBakeWidget::handleFinishedBaker() {
if (auto baker = qobject_cast<DomainBaker*>(sender())) {
// add the results of this bake to the results window
auto it = std::find_if(_bakers.begin(), _bakers.end(), [baker](const BakerRowPair& value) {
return value.first.get() == baker;
});
if (it != _bakers.end()) {
auto resultRow = it->second;
auto resultsWindow = qApp->getMainWindow()->showResultsWindow();
if (baker->hasErrors()) {
auto errors = baker->getErrors();
errors.removeDuplicates();
resultsWindow->changeStatusForRow(resultRow, errors.join("\n"));
} else if (baker->hasWarnings()) {
auto warnings = baker->getWarnings();
warnings.removeDuplicates();
resultsWindow->changeStatusForRow(resultRow, warnings.join("\n"));
} else {
resultsWindow->changeStatusForRow(resultRow, "Success");
}
// remove the DomainBaker now that it has completed
_bakers.erase(it);
}
}
}

View file

@ -0,0 +1,54 @@
//
// DomainBakeWidget.h
// tools/oven/src/ui
//
// Created by Stephen Birarda on 4/12/17.
// Copyright 2017 High Fidelity, Inc.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
#ifndef hifi_DomainBakeWidget_h
#define hifi_DomainBakeWidget_h
#include <QtWidgets/QWidget>
#include <SettingHandle.h>
#include "../DomainBaker.h"
#include "BakeWidget.h"
class QLineEdit;
class DomainBakeWidget : public BakeWidget {
Q_OBJECT
public:
DomainBakeWidget(QWidget* parent = nullptr, Qt::WindowFlags flags = Qt::WindowFlags());
private slots:
void chooseFileButtonClicked();
void chooseOutputDirButtonClicked();
void bakeButtonClicked();
void outputDirectoryChanged(const QString& newDirectory);
void handleBakerProgress(int baked, int total);
void handleFinishedBaker();
private:
void setupUI();
QLineEdit* _domainNameLineEdit;
QLineEdit* _entitiesFileLineEdit;
QLineEdit* _outputDirLineEdit;
QLineEdit* _destinationPathLineEdit;
Setting::Handle<QString> _domainNameSetting;
Setting::Handle<QString> _exportDirectory;
Setting::Handle<QString> _browseStartDirectory;
Setting::Handle<QString> _destinationPathSetting;
};
#endif // hifi_ModelBakeWidget_h

View file

@ -0,0 +1,227 @@
//
// ModelBakeWidget.cpp
// tools/oven/src/ui
//
// Created by Stephen Birarda on 4/6/17.
// Copyright 2017 High Fidelity, Inc.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
#include <QtWidgets/QFileDialog>
#include <QtWidgets/QGridLayout>
#include <QtWidgets/QLabel>
#include <QtWidgets/QLineEdit>
#include <QtWidgets/QPushButton>
#include <QtWidgets/QStackedWidget>
#include <QtCore/QDir>
#include <QtCore/QDebug>
#include <QtCore/QThread>
#include "../Oven.h"
#include "OvenMainWindow.h"
#include "ModelBakeWidget.h"
static const auto EXPORT_DIR_SETTING_KEY = "model_export_directory";
static const auto MODEL_START_DIR_SETTING_KEY = "model_search_directory";
ModelBakeWidget::ModelBakeWidget(QWidget* parent, Qt::WindowFlags flags) :
BakeWidget(parent, flags),
_exportDirectory(EXPORT_DIR_SETTING_KEY),
_modelStartDirectory(MODEL_START_DIR_SETTING_KEY)
{
setupUI();
}
void ModelBakeWidget::setupUI() {
// setup a grid layout to hold everything
QGridLayout* gridLayout = new QGridLayout;
int rowIndex = 0;
// setup a section to choose the file being baked
QLabel* modelFileLabel = new QLabel("Model File(s)");
_modelLineEdit = new QLineEdit;
_modelLineEdit->setPlaceholderText("File or URL");
QPushButton* chooseFileButton = new QPushButton("Browse...");
connect(chooseFileButton, &QPushButton::clicked, this, &ModelBakeWidget::chooseFileButtonClicked);
// add the components for the model file picker to the layout
gridLayout->addWidget(modelFileLabel, rowIndex, 0);
gridLayout->addWidget(_modelLineEdit, rowIndex, 1, 1, 3);
gridLayout->addWidget(chooseFileButton, rowIndex, 4);
// start a new row for next component
++rowIndex;
// setup a section to choose the output directory
QLabel* outputDirectoryLabel = new QLabel("Output Directory");
_outputDirLineEdit = new QLineEdit;
// set the current export directory to whatever was last used
_outputDirLineEdit->setText(_exportDirectory.get());
// whenever the output directory line edit changes, update the value in settings
connect(_outputDirLineEdit, &QLineEdit::textChanged, this, &ModelBakeWidget::outputDirectoryChanged);
QPushButton* chooseOutputDirectoryButton = new QPushButton("Browse...");
connect(chooseOutputDirectoryButton, &QPushButton::clicked, this, &ModelBakeWidget::chooseOutputDirButtonClicked);
// add the components for the output directory picker to the layout
gridLayout->addWidget(outputDirectoryLabel, rowIndex, 0);
gridLayout->addWidget(_outputDirLineEdit, rowIndex, 1, 1, 3);
gridLayout->addWidget(chooseOutputDirectoryButton, rowIndex, 4);
// start a new row for the next component
++rowIndex;
// add a horizontal line to split the bake/cancel buttons off
QFrame* lineFrame = new QFrame;
lineFrame->setFrameShape(QFrame::HLine);
lineFrame->setFrameShadow(QFrame::Sunken);
gridLayout->addWidget(lineFrame, rowIndex, 0, 1, -1);
// start a new row for the next component
++rowIndex;
// add a button that will kickoff the bake
QPushButton* bakeButton = new QPushButton("Bake");
connect(bakeButton, &QPushButton::clicked, this, &ModelBakeWidget::bakeButtonClicked);
gridLayout->addWidget(bakeButton, rowIndex, 3);
// add a cancel button to go back to the modes page
QPushButton* cancelButton = new QPushButton("Cancel");
connect(cancelButton, &QPushButton::clicked, this, &ModelBakeWidget::cancelButtonClicked);
gridLayout->addWidget(cancelButton, rowIndex, 4);
setLayout(gridLayout);
}
void ModelBakeWidget::chooseFileButtonClicked() {
// pop a file dialog so the user can select the model file
// if we have picked an FBX before, start in the folder that matches the last path
// otherwise start in the home directory
auto startDir = _modelStartDirectory.get();
if (startDir.isEmpty()) {
startDir = QDir::homePath();
}
auto selectedFiles = QFileDialog::getOpenFileNames(this, "Choose Model", startDir, "Models (*.fbx)");
if (!selectedFiles.isEmpty()) {
// set the contents of the model file text box to be the path to the selected file
_modelLineEdit->setText(selectedFiles.join(','));
auto directoryOfModel = QFileInfo(selectedFiles[0]).absolutePath();
if (_outputDirLineEdit->text().isEmpty()) {
// if our output directory is not yet set, set it to the directory of this model
_outputDirLineEdit->setText(directoryOfModel);
}
// save the directory containing the file(s) so we can default to it next time we show the file dialog
_modelStartDirectory.set(directoryOfModel);
}
}
void ModelBakeWidget::chooseOutputDirButtonClicked() {
// pop a file dialog so the user can select the output directory
// if we have a previously selected output directory, use that as the initial path in the choose dialog
// otherwise use the user's home directory
auto startDir = _exportDirectory.get();
if (startDir.isEmpty()) {
startDir = QDir::homePath();
}
auto selectedDir = QFileDialog::getExistingDirectory(this, "Choose Output Directory", startDir);
if (!selectedDir.isEmpty()) {
// set the contents of the output directory text box to be the path to the directory
_outputDirLineEdit->setText(selectedDir);
}
}
void ModelBakeWidget::outputDirectoryChanged(const QString& newDirectory) {
// update the export directory setting so we can re-use it next time
_exportDirectory.set(newDirectory);
}
void ModelBakeWidget::bakeButtonClicked() {
// make sure we have a valid output directory
QDir outputDirectory(_outputDirLineEdit->text());
if (!outputDirectory.exists()) {
return;
}
// make sure we have a non empty URL to a model to bake
if (_modelLineEdit->text().isEmpty()) {
return;
}
// split the list from the model line edit to see how many models we need to bake
auto fileURLStrings = _modelLineEdit->text().split(',');
foreach (QString fileURLString, fileURLStrings) {
// construct a URL from the path in the model file text box
QUrl modelToBakeURL(fileURLString);
// if the URL doesn't have a scheme, assume it is a local file
if (modelToBakeURL.scheme() != "http" && modelToBakeURL.scheme() != "https" && modelToBakeURL.scheme() != "ftp") {
modelToBakeURL.setScheme("file");
}
// everything seems to be in place, kick off a bake for this model now
auto baker = std::unique_ptr<FBXBaker> {
new FBXBaker(modelToBakeURL, outputDirectory.absolutePath(), []() -> QThread* {
return qApp->getNextWorkerThread();
}, false)
};
// move the baker to the FBX baker thread
baker->moveToThread(qApp->getFBXBakerThread());
// invoke the bake method on the baker thread
QMetaObject::invokeMethod(baker.get(), "bake");
// make sure we hear about the results of this baker when it is done
connect(baker.get(), &FBXBaker::finished, this, &ModelBakeWidget::handleFinishedBaker);
// add a pending row to the results window to show that this bake is in process
auto resultsWindow = qApp->getMainWindow()->showResultsWindow();
auto resultsRow = resultsWindow->addPendingResultRow(modelToBakeURL.fileName(), outputDirectory);
// keep a unique_ptr to this baker
// and remember the row that represents it in the results table
_bakers.emplace_back(std::move(baker), resultsRow);
}
}
void ModelBakeWidget::handleFinishedBaker() {
if (auto baker = qobject_cast<FBXBaker*>(sender())) {
// add the results of this bake to the results window
auto it = std::find_if(_bakers.begin(), _bakers.end(), [baker](const BakerRowPair& value) {
return value.first.get() == baker;
});
if (it != _bakers.end()) {
auto resultRow = it->second;
auto resultsWindow = qApp->getMainWindow()->showResultsWindow();
if (baker->hasErrors()) {
resultsWindow->changeStatusForRow(resultRow, baker->getErrors().join("\n"));
} else {
resultsWindow->changeStatusForRow(resultRow, "Success");
}
_bakers.erase(it);
}
}
}

View file

@ -0,0 +1,51 @@
//
// ModelBakeWidget.h
// tools/oven/src/ui
//
// Created by Stephen Birarda on 4/6/17.
// Copyright 2017 High Fidelity, Inc.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
#ifndef hifi_ModelBakeWidget_h
#define hifi_ModelBakeWidget_h
#include <QtWidgets/QWidget>
#include <SettingHandle.h>
#include "../FBXBaker.h"
#include "BakeWidget.h"
class QLineEdit;
class QThread;
class ModelBakeWidget : public BakeWidget {
Q_OBJECT
public:
ModelBakeWidget(QWidget* parent = nullptr, Qt::WindowFlags flags = Qt::WindowFlags());
private slots:
void chooseFileButtonClicked();
void chooseOutputDirButtonClicked();
void bakeButtonClicked();
void outputDirectoryChanged(const QString& newDirectory);
void handleFinishedBaker();
private:
void setupUI();
QLineEdit* _modelLineEdit;
QLineEdit* _outputDirLineEdit;
Setting::Handle<QString> _exportDirectory;
Setting::Handle<QString> _modelStartDirectory;
};
#endif // hifi_ModelBakeWidget_h

View file

@ -0,0 +1,69 @@
//
// ModesWidget.cpp
// tools/oven/src/ui
//
// Created by Stephen Birarda on 4/7/17.
// Copyright 2017 High Fidelity, Inc.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
#include <QtWidgets/QHBoxLayout>
#include <QtWidgets/QPushButton>
#include <QtWidgets/QStackedWidget>
#include "DomainBakeWidget.h"
#include "ModelBakeWidget.h"
#include "SkyboxBakeWidget.h"
#include "ModesWidget.h"
ModesWidget::ModesWidget(QWidget* parent, Qt::WindowFlags flags) :
QWidget(parent, flags)
{
setupUI();
}
void ModesWidget::setupUI() {
// setup a horizontal box layout to hold our mode buttons
QHBoxLayout* horizontalLayout = new QHBoxLayout;
// add a button for domain baking
QPushButton* domainButton = new QPushButton("Bake Domain");
connect(domainButton, &QPushButton::clicked, this, &ModesWidget::showDomainBakingWidget);
horizontalLayout->addWidget(domainButton);
// add a button for model baking
QPushButton* modelsButton = new QPushButton("Bake Models");
connect(modelsButton, &QPushButton::clicked, this, &ModesWidget::showModelBakingWidget);
horizontalLayout->addWidget(modelsButton);
// add a button for skybox baking
QPushButton* skyboxButton = new QPushButton("Bake Skyboxes");
connect(skyboxButton, &QPushButton::clicked, this, &ModesWidget::showSkyboxBakingWidget);
horizontalLayout->addWidget(skyboxButton);
setLayout(horizontalLayout);
}
void ModesWidget::showModelBakingWidget() {
auto stackedWidget = qobject_cast<QStackedWidget*>(parentWidget());
// add a new widget for model baking to the stack, and switch to it
stackedWidget->setCurrentIndex(stackedWidget->addWidget(new ModelBakeWidget));
}
void ModesWidget::showDomainBakingWidget() {
auto stackedWidget = qobject_cast<QStackedWidget*>(parentWidget());
// add a new widget for domain baking to the stack, and switch to it
stackedWidget->setCurrentIndex(stackedWidget->addWidget(new DomainBakeWidget));
}
void ModesWidget::showSkyboxBakingWidget() {
auto stackedWidget = qobject_cast<QStackedWidget*>(parentWidget());
// add a new widget for skybox baking to the stack, and switch to it
stackedWidget->setCurrentIndex(stackedWidget->addWidget(new SkyboxBakeWidget));
}

View file

@ -0,0 +1,31 @@
//
// ModesWidget.h
// tools/oven/src/ui
//
// Created by Stephen Birarda on 4/7/17.
// Copyright 2017 High Fidelity, Inc.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
#ifndef hifi_ModesWidget_h
#define hifi_ModesWidget_h
#include <QtWidgets/QWidget>
class ModesWidget : public QWidget {
Q_OBJECT
public:
ModesWidget(QWidget* parent = nullptr, Qt::WindowFlags flags = Qt::WindowFlags());
private slots:
void showModelBakingWidget();
void showDomainBakingWidget();
void showSkyboxBakingWidget();
private:
void setupUI();
};
#endif // hifi_ModesWidget_h

View file

@ -0,0 +1,61 @@
//
// OvenMainWindow.cpp
// tools/oven/src/ui
//
// Created by Stephen Birarda on 4/6/17.
// Copyright 2017 High Fidelity, Inc.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
#include <QtWidgets/QStackedWidget>
#include "ModesWidget.h"
#include "OvenMainWindow.h"
OvenMainWindow::OvenMainWindow(QWidget *parent, Qt::WindowFlags flags) :
QMainWindow(parent, flags)
{
setWindowTitle("High Fidelity Oven");
// give the window a fixed width that will never change
setFixedWidth(FIXED_WINDOW_WIDTH);
// setup a stacked layout for the main "modes" menu and subseq
QStackedWidget* stackedWidget = new QStackedWidget(this);
stackedWidget->addWidget(new ModesWidget);
setCentralWidget(stackedWidget);
}
OvenMainWindow::~OvenMainWindow() {
if (_resultsWindow) {
_resultsWindow->close();
_resultsWindow->deleteLater();
}
}
ResultsWindow* OvenMainWindow::showResultsWindow(bool shouldRaise) {
if (!_resultsWindow) {
// we don't have a results window right now, so make a new one
_resultsWindow = new ResultsWindow;
// even though we're about to show the results window, we do it here so that the move below works
_resultsWindow->show();
// place the results window initially below our window
_resultsWindow->move(_resultsWindow->x(), this->frameGeometry().bottom());
}
// show the results window and make sure it is in front
_resultsWindow->show();
if (shouldRaise) {
_resultsWindow->raise();
}
// return a pointer to the results window the caller can use
return _resultsWindow;
}

View file

@ -0,0 +1,34 @@
//
// OvenMainWindow.h
// tools/oven/src/ui
//
// Created by Stephen Birarda on 4/6/17.
// Copyright 2017 High Fidelity, Inc.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
#ifndef hifi_OvenMainWindow_h
#define hifi_OvenMainWindow_h
#include <QtCore/QPointer>
#include <QtWidgets/QMainWindow>
#include "ResultsWindow.h"
const int FIXED_WINDOW_WIDTH = 640;
class OvenMainWindow : public QMainWindow {
Q_OBJECT
public:
OvenMainWindow(QWidget *parent = Q_NULLPTR, Qt::WindowFlags flags = Qt::WindowFlags());
~OvenMainWindow();
ResultsWindow* showResultsWindow(bool shouldRaise = true);
private:
QPointer<ResultsWindow> _resultsWindow;
};
#endif // hifi_OvenMainWindow_h

View file

@ -0,0 +1,100 @@
//
// ResultsWindow.cpp
// tools/oven/src/ui
//
// Created by Stephen Birarda on 4/14/17.
// Copyright 2017 High Fidelity, Inc.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
#include <QtCore/QUrl>
#include <QtGui/QDesktopServices>
#include <QtWidgets/QHeaderView>
#include <QtWidgets/QTableWidget>
#include <QtWidgets/QVBoxLayout>
#include "OvenMainWindow.h"
#include "ResultsWindow.h"
ResultsWindow::ResultsWindow(QWidget* parent) :
QWidget(parent)
{
// add a title to this window to identify it
setWindowTitle("High Fidelity Oven - Bake Results");
// give this dialog the same starting width as the main application window
resize(FIXED_WINDOW_WIDTH, size().height());
// have the window delete itself when closed
setAttribute(Qt::WA_DeleteOnClose);
setupUI();
}
void ResultsWindow::setupUI() {
QVBoxLayout* resultsLayout = new QVBoxLayout(this);
// add a results table to the widget
_resultsTable = new QTableWidget(0, 2, this);
// add the header to the table widget
_resultsTable->setHorizontalHeaderLabels({"File", "Status"});
// add that table widget to the vertical box layout, so we can make it stretch to the size of the parent
resultsLayout->insertWidget(0, _resultsTable);
// make the filename column hold 25% of the total width
// strech the last column of the table (that holds the results) to fill up the remaining available size
_resultsTable->horizontalHeader()->resizeSection(0, 0.25 * FIXED_WINDOW_WIDTH);
_resultsTable->horizontalHeader()->setSectionResizeMode(1, QHeaderView::ResizeToContents);
// make sure we hear about cell clicks so that we can show the output directory for the given row
connect(_resultsTable, &QTableWidget::cellClicked, this, &ResultsWindow::handleCellClicked);
// set the layout of this widget to the created layout
setLayout(resultsLayout);
}
void ResultsWindow::handleCellClicked(int rowIndex, int columnIndex) {
// make sure this click was on the file/domain being baked
if (columnIndex == 0) {
// use QDesktopServices to show the output directory for this row
auto directory = _outputDirectories[rowIndex];
QDesktopServices::openUrl(QUrl::fromLocalFile(directory.absolutePath()));
}
}
int ResultsWindow::addPendingResultRow(const QString& fileName, const QDir& outputDirectory) {
int rowIndex = _resultsTable->rowCount();
_resultsTable->insertRow(rowIndex);
// add a new item for the filename, make it non-editable
auto fileNameItem = new QTableWidgetItem(fileName);
fileNameItem->setFlags(fileNameItem->flags() & ~Qt::ItemIsEditable);
_resultsTable->setItem(rowIndex, 0, fileNameItem);
auto statusItem = new QTableWidgetItem("Baking...");
statusItem->setFlags(statusItem->flags() & ~Qt::ItemIsEditable);
_resultsTable->setItem(rowIndex, 1, statusItem);
// push an output directory to our list so we can show it if the user clicks on this bake in the results table
_outputDirectories.push_back(outputDirectory);
return rowIndex;
}
void ResultsWindow::changeStatusForRow(int rowIndex, const QString& result) {
const int STATUS_COLUMN = 1;
auto statusItem = new QTableWidgetItem(result);
statusItem->setFlags(statusItem->flags() & ~Qt::ItemIsEditable);
_resultsTable->setItem(rowIndex, STATUS_COLUMN, statusItem);
// resize the row for the new contents
_resultsTable->resizeRowToContents(rowIndex);
// reszie the column for the new contents
_resultsTable->resizeColumnToContents(STATUS_COLUMN);
}

View file

@ -0,0 +1,39 @@
//
// ResultsWindow.h
// tools/oven/src/ui
//
// Created by Stephen Birarda on 4/14/17.
// Copyright 2017 High Fidelity, Inc.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
#ifndef hifi_ResultsWindow_h
#define hifi_ResultsWindow_h
#include <QtCore/QDir>
#include <QtWidgets/QWidget>
class QTableWidget;
class ResultsWindow : public QWidget {
Q_OBJECT
public:
ResultsWindow(QWidget* parent = nullptr);
void setupUI();
int addPendingResultRow(const QString& fileName, const QDir& outputDirectory);
void changeStatusForRow(int rowIndex, const QString& result);
private slots:
void handleCellClicked(int rowIndex, int columnIndex);
private:
QTableWidget* _resultsTable { nullptr };
QList<QDir> _outputDirectories;
};
#endif // hifi_ResultsWindow_h

View file

@ -0,0 +1,223 @@
//
// SkyboxBakeWidget.cpp
// tools/oven/src/ui
//
// Created by Stephen Birarda on 4/17/17.
// Copyright 2017 High Fidelity, Inc.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
#include <QtWidgets/QFileDialog>
#include <QtWidgets/QGridLayout>
#include <QtWidgets/QLabel>
#include <QtWidgets/QLineEdit>
#include <QtWidgets/QPushButton>
#include <QtWidgets/QStackedWidget>
#include <QtCore/QDir>
#include <QtCore/QDebug>
#include <QtCore/QThread>
#include "../Oven.h"
#include "OvenMainWindow.h"
#include "SkyboxBakeWidget.h"
static const auto EXPORT_DIR_SETTING_KEY = "skybox_export_directory";
static const auto SELECTION_START_DIR_SETTING_KEY = "skybox_search_directory";
SkyboxBakeWidget::SkyboxBakeWidget(QWidget* parent, Qt::WindowFlags flags) :
BakeWidget(parent, flags),
_exportDirectory(EXPORT_DIR_SETTING_KEY),
_selectionStartDirectory(SELECTION_START_DIR_SETTING_KEY)
{
setupUI();
}
void SkyboxBakeWidget::setupUI() {
// setup a grid layout to hold everything
QGridLayout* gridLayout = new QGridLayout;
int rowIndex = 0;
// setup a section to choose the file being baked
QLabel* skyboxFileLabel = new QLabel("Skybox File(s)");
_selectionLineEdit = new QLineEdit;
_selectionLineEdit->setPlaceholderText("File or URL");
QPushButton* chooseFileButton = new QPushButton("Browse...");
connect(chooseFileButton, &QPushButton::clicked, this, &SkyboxBakeWidget::chooseFileButtonClicked);
// add the components for the skybox file picker to the layout
gridLayout->addWidget(skyboxFileLabel, rowIndex, 0);
gridLayout->addWidget(_selectionLineEdit, rowIndex, 1, 1, 3);
gridLayout->addWidget(chooseFileButton, rowIndex, 4);
// start a new row for next component
++rowIndex;
// setup a section to choose the output directory
QLabel* outputDirectoryLabel = new QLabel("Output Directory");
_outputDirLineEdit = new QLineEdit;
// set the current export directory to whatever was last used
_outputDirLineEdit->setText(_exportDirectory.get());
// whenever the output directory line edit changes, update the value in settings
connect(_outputDirLineEdit, &QLineEdit::textChanged, this, &SkyboxBakeWidget::outputDirectoryChanged);
QPushButton* chooseOutputDirectoryButton = new QPushButton("Browse...");
connect(chooseOutputDirectoryButton, &QPushButton::clicked, this, &SkyboxBakeWidget::chooseOutputDirButtonClicked);
// add the components for the output directory picker to the layout
gridLayout->addWidget(outputDirectoryLabel, rowIndex, 0);
gridLayout->addWidget(_outputDirLineEdit, rowIndex, 1, 1, 3);
gridLayout->addWidget(chooseOutputDirectoryButton, rowIndex, 4);
// start a new row for the next component
++rowIndex;
// add a horizontal line to split the bake/cancel buttons off
QFrame* lineFrame = new QFrame;
lineFrame->setFrameShape(QFrame::HLine);
lineFrame->setFrameShadow(QFrame::Sunken);
gridLayout->addWidget(lineFrame, rowIndex, 0, 1, -1);
// start a new row for the next component
++rowIndex;
// add a button that will kickoff the bake
QPushButton* bakeButton = new QPushButton("Bake");
connect(bakeButton, &QPushButton::clicked, this, &SkyboxBakeWidget::bakeButtonClicked);
gridLayout->addWidget(bakeButton, rowIndex, 3);
// add a cancel button to go back to the modes page
QPushButton* cancelButton = new QPushButton("Cancel");
connect(cancelButton, &QPushButton::clicked, this, &SkyboxBakeWidget::cancelButtonClicked);
gridLayout->addWidget(cancelButton, rowIndex, 4);
setLayout(gridLayout);
}
void SkyboxBakeWidget::chooseFileButtonClicked() {
// pop a file dialog so the user can select the skybox file(s)
// if we have picked a skybox before, start in the folder that matches the last path
// otherwise start in the home directory
auto startDir = _selectionStartDirectory.get();
if (startDir.isEmpty()) {
startDir = QDir::homePath();
}
auto selectedFiles = QFileDialog::getOpenFileNames(this, "Choose Skybox", startDir);
if (!selectedFiles.isEmpty()) {
// set the contents of the file select text box to be the path to the selected file
_selectionLineEdit->setText(selectedFiles.join(','));
if (_outputDirLineEdit->text().isEmpty()) {
auto directoryOfSkybox = QFileInfo(selectedFiles[0]).absolutePath();
// if our output directory is not yet set, set it to the directory of this skybox
_outputDirLineEdit->setText(directoryOfSkybox);
}
}
}
void SkyboxBakeWidget::chooseOutputDirButtonClicked() {
// pop a file dialog so the user can select the output directory
// if we have a previously selected output directory, use that as the initial path in the choose dialog
// otherwise use the user's home directory
auto startDir = _exportDirectory.get();
if (startDir.isEmpty()) {
startDir = QDir::homePath();
}
auto selectedDir = QFileDialog::getExistingDirectory(this, "Choose Output Directory", startDir);
if (!selectedDir.isEmpty()) {
// set the contents of the output directory text box to be the path to the directory
_outputDirLineEdit->setText(selectedDir);
}
}
void SkyboxBakeWidget::outputDirectoryChanged(const QString& newDirectory) {
// update the export directory setting so we can re-use it next time
_exportDirectory.set(newDirectory);
}
void SkyboxBakeWidget::bakeButtonClicked() {
// make sure we have a valid output directory
QDir outputDirectory(_outputDirLineEdit->text());
if (!outputDirectory.exists()) {
return;
}
// make sure we have a non empty URL to a skybox to bake
if (_selectionLineEdit->text().isEmpty()) {
return;
}
// split the list from the selection line edit to see how many skyboxes we need to bake
auto fileURLStrings = _selectionLineEdit->text().split(',');
foreach (QString fileURLString, fileURLStrings) {
// construct a URL from the path in the skybox file text box
QUrl skyboxToBakeURL(fileURLString);
// if the URL doesn't have a scheme, assume it is a local file
if (skyboxToBakeURL.scheme() != "http" && skyboxToBakeURL.scheme() != "https" && skyboxToBakeURL.scheme() != "ftp") {
skyboxToBakeURL.setScheme("file");
}
// everything seems to be in place, kick off a bake for this skybox now
auto baker = std::unique_ptr<TextureBaker> {
new TextureBaker(skyboxToBakeURL, image::TextureUsage::CUBE_TEXTURE, outputDirectory.absolutePath())
};
// move the baker to a worker thread
baker->moveToThread(qApp->getNextWorkerThread());
// invoke the bake method on the baker thread
QMetaObject::invokeMethod(baker.get(), "bake");
// make sure we hear about the results of this baker when it is done
connect(baker.get(), &TextureBaker::finished, this, &SkyboxBakeWidget::handleFinishedBaker);
// add a pending row to the results window to show that this bake is in process
auto resultsWindow = qApp->getMainWindow()->showResultsWindow();
auto resultsRow = resultsWindow->addPendingResultRow(skyboxToBakeURL.fileName(), outputDirectory);
// keep a unique_ptr to this baker
// and remember the row that represents it in the results table
_bakers.emplace_back(std::move(baker), resultsRow);
}
}
void SkyboxBakeWidget::handleFinishedBaker() {
if (auto baker = qobject_cast<TextureBaker*>(sender())) {
// add the results of this bake to the results window
auto it = std::find_if(_bakers.begin(), _bakers.end(), [baker](const BakerRowPair& value) {
return value.first.get() == baker;
});
if (it != _bakers.end()) {
auto resultRow = it->second;
auto resultsWindow = qApp->getMainWindow()->showResultsWindow();
if (baker->hasErrors()) {
resultsWindow->changeStatusForRow(resultRow, baker->getErrors().join("\n"));
} else {
resultsWindow->changeStatusForRow(resultRow, "Success");
}
// drop our strong pointer to the baker now that we are done with it
_bakers.erase(it);
}
}
}

View file

@ -0,0 +1,50 @@
//
// SkyboxBakeWidget.h
// tools/oven/src/ui
//
// Created by Stephen Birarda on 4/17/17.
// Copyright 2017 High Fidelity, Inc.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
#ifndef hifi_SkyboxBakeWidget_h
#define hifi_SkyboxBakeWidget_h
#include <QtWidgets/QWidget>
#include <SettingHandle.h>
#include "../TextureBaker.h"
#include "BakeWidget.h"
class QLineEdit;
class SkyboxBakeWidget : public BakeWidget {
Q_OBJECT
public:
SkyboxBakeWidget(QWidget* parent = nullptr, Qt::WindowFlags flags = Qt::WindowFlags());
private slots:
void chooseFileButtonClicked();
void chooseOutputDirButtonClicked();
void bakeButtonClicked();
void outputDirectoryChanged(const QString& newDirectory);
void handleFinishedBaker();
private:
void setupUI();
QLineEdit* _selectionLineEdit;
QLineEdit* _outputDirLineEdit;
Setting::Handle<QString> _exportDirectory;
Setting::Handle<QString> _selectionStartDirectory;
};
#endif // hifi_SkyboxBakeWidget_h

View file

@ -0,0 +1,987 @@
"use strict";
// Chat.js
// By Don Hopkins (dhopkins@donhopkins.com)
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
(function() {
var webPageURL = "ChatPage.html"; // URL of tablet web page.
var randomizeWebPageURL = true; // Set to true for debugging.
var lastWebPageURL = ""; // Last random URL of tablet web page.
var onChatPage = false; // True when chat web page is opened.
var webHandlerConnected = false; // True when the web handler has been connected.
var channelName = "Chat"; // Unique name for channel that we listen to.
var tabletButtonName = "CHAT"; // Tablet button label.
var tabletButtonIcon = "icons/tablet-icons/menu-i.svg"; // Icon for chat button.
var tabletButtonActiveIcon = "icons/tablet-icons/menu-a.svg"; // Active icon for chat button.
var tabletButton = null; // The button we create in the tablet.
var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); // The awesome tablet.
var chatLog = []; // Array of chat messages in the form of [avatarID, displayName, message, data].
var avatarIdentifiers = {}; // Map of avatar ids to dict of identifierParams.
var speechBubbleShowing = false; // Is the speech bubble visible?
var speechBubbleMessage = null; // The message shown in the speech bubble.
var speechBubbleData = null; // The data of the speech bubble message.
var speechBubbleTextID = null; // The id of the speech bubble local text entity.
var speechBubbleTimer = null; // The timer to pop down the speech bubble.
var speechBubbleParams = null; // The params used to create or edit the speech bubble.
// Persistent variables saved in the Settings.
var chatName = ''; // The user's name shown in chat.
var chatLogMaxSize = 100; // The maximum number of chat messages we remember.
var sendTyping = true; // Send typing begin and end notification.
var identifyAvatarDuration = 10; // How long to leave the avatar identity line up, in seconds.
var identifyAvatarLineColor = { red: 0, green: 255, blue: 0 }; // The color of the avatar identity line.
var identifyAvatarMyJointName = 'Head'; // My bone from which to draw the avatar identity line.
var identifyAvatarYourJointName = 'Head'; // Your bone to which to draw the avatar identity line.
var speechBubbleDuration = 10; // How long to leave the speech bubble up, in seconds.
var speechBubbleTextColor = {red: 255, green: 255, blue: 255}; // The text color of the speech bubble.
var speechBubbleBackgroundColor = {red: 0, green: 0, blue: 0}; // The background color of the speech bubble.
var speechBubbleOffset = {x: 0, y: 0.3, z: 0.0}; // The offset from the joint to whic the speech bubble is attached.
var speechBubbleJointName = 'Head'; // The name of the joint to which the speech bubble is attached.
var speechBubbleLineHeight = 0.05; // The height of a line of text in the speech bubble.
// Load the persistent variables from the Settings, with defaults.
function loadSettings() {
chatName = Settings.getValue('Chat_chatName', MyAvatar.displayName);
if (!chatName) {
chatName = randomAvatarName();
}
chatLogMaxSize = Settings.getValue('Chat_chatLogMaxSize', 100);
sendTyping = Settings.getValue('Chat_sendTyping', true);
identifyAvatarDuration = Settings.getValue('Chat_identifyAvatarDuration', 10);
identifyAvatarLineColor = Settings.getValue('Chat_identifyAvatarLineColor', { red: 0, green: 255, blue: 0 });
identifyAvatarMyJointName = Settings.getValue('Chat_identifyAvatarMyJointName', 'Head');
identifyAvatarYourJointName = Settings.getValue('Chat_identifyAvatarYourJointName', 'Head');
speechBubbleDuration = Settings.getValue('Chat_speechBubbleDuration', 10);
speechBubbleTextColor = Settings.getValue('Chat_speechBubbleTextColor', {red: 255, green: 255, blue: 255});
speechBubbleBackgroundColor = Settings.getValue('Chat_speechBubbleBackgroundColor', {red: 0, green: 0, blue: 0});
speechBubbleOffset = Settings.getValue('Chat_speechBubbleOffset', {x: 0.0, y: 0.3, z:0.0});
speechBubbleJointName = Settings.getValue('Chat_speechBubbleJointName', 'Head');
speechBubbleLineHeight = Settings.getValue('Chat_speechBubbleLineHeight', 0.05);
saveSettings();
}
// Save the persistent variables to the Settings.
function saveSettings() {
Settings.setValue('Chat_chatName', chatName);
Settings.setValue('Chat_chatLogMaxSize', chatLogMaxSize);
Settings.setValue('Chat_sendTyping', sendTyping);
Settings.setValue('Chat_identifyAvatarDuration', identifyAvatarDuration);
Settings.setValue('Chat_identifyAvatarLineColor', identifyAvatarLineColor);
Settings.setValue('Chat_identifyAvatarMyJointName', identifyAvatarMyJointName);
Settings.setValue('Chat_identifyAvatarYourJointName', identifyAvatarYourJointName);
Settings.setValue('Chat_speechBubbleDuration', speechBubbleDuration);
Settings.setValue('Chat_speechBubbleTextColor', speechBubbleTextColor);
Settings.setValue('Chat_speechBubbleBackgroundColor', speechBubbleBackgroundColor);
Settings.setValue('Chat_speechBubbleOffset', speechBubbleOffset);
Settings.setValue('Chat_speechBubbleJointName', speechBubbleJointName);
Settings.setValue('Chat_speechBubbleLineHeight', speechBubbleLineHeight);
}
// Reset the Settings and persistent variables to the defaults.
function resetSettings() {
Settings.setValue('Chat_chatName', null);
Settings.setValue('Chat_chatLogMaxSize', null);
Settings.setValue('Chat_sendTyping', null);
Settings.setValue('Chat_identifyAvatarDuration', null);
Settings.setValue('Chat_identifyAvatarLineColor', null);
Settings.setValue('Chat_identifyAvatarMyJointName', null);
Settings.setValue('Chat_identifyAvatarYourJointName', null);
Settings.setValue('Chat_speechBubbleDuration', null);
Settings.setValue('Chat_speechBubbleTextColor', null);
Settings.setValue('Chat_speechBubbleBackgroundColor', null);
Settings.setValue('Chat_speechBubbleOffset', null);
Settings.setValue('Chat_speechBubbleJointName', null);
Settings.setValue('Chat_speechBubbleLineHeight', null);
loadSettings();
}
// Update anything that might depend on the settings.
function updateSettings() {
updateSpeechBubble();
trimChatLog();
updateChatPage();
}
// Trim the chat log so it is no longer than chatLogMaxSize lines.
function trimChatLog() {
if (chatLog.length > chatLogMaxSize) {
chatLog.splice(0, chatLogMaxSize - chatLog.length);
}
}
// Clear the local chat log.
function clearChatLog() {
//print("clearChatLog");
chatLog = [];
updateChatPage();
}
// We got a chat message from the channel.
// Trim the chat log, save the latest message in the chat log,
// and show the message on the tablet, if the chat page is showing.
function handleTransmitChatMessage(avatarID, displayName, message, data) {
//print("receiveChat", "avatarID", avatarID, "displayName", displayName, "message", message, "data", data);
trimChatLog();
chatLog.push([avatarID, displayName, message, data]);
if (onChatPage) {
tablet.emitScriptEvent(
JSON.stringify({
type: "ReceiveChatMessage",
avatarID: avatarID,
displayName: displayName,
message: message,
data: data
}));
}
}
// Trim the chat log, save the latest log message in the chat log,
// and show the message on the tablet, if the chat page is showing.
function logMessage(message, data) {
//print("logMessage", message, data);
trimChatLog();
chatLog.push([null, null, message, data]);
if (onChatPage) {
tablet.emitScriptEvent(
JSON.stringify({
type: "LogMessage",
message: message,
data: data
}));
}
}
// An empty chat message was entered.
// Hide our speech bubble.
function emptyChatMessage(data) {
popDownSpeechBubble();
}
// Notification that we typed a keystroke.
function type() {
//print("type");
}
// Notification that we began typing.
// Notify everyone that we started typing.
function beginTyping() {
//print("beginTyping");
if (!sendTyping) {
return;
}
Messages.sendMessage(
channelName,
JSON.stringify({
type: 'AvatarBeginTyping',
avatarID: MyAvatar.sessionUUID,
displayName: chatName
}));
}
// Notification that somebody started typing.
function handleAvatarBeginTyping(avatarID, displayName) {
//print("handleAvatarBeginTyping:", "avatarID", avatarID, displayName);
}
// Notification that we stopped typing.
// Notify everyone that we stopped typing.
function endTyping() {
//print("endTyping");
if (!sendTyping) {
return;
}
Messages.sendMessage(
channelName,
JSON.stringify({
type: 'AvatarEndTyping',
avatarID: MyAvatar.sessionUUID,
displayName: chatName
}));
}
// Notification that somebody stopped typing.
function handleAvatarEndTyping(avatarID, displayName) {
//print("handleAvatarEndTyping:", "avatarID", avatarID, displayName);
}
// Identify an avatar by drawing a line from our head to their head.
// If the avatar is our own, then just draw a line up into the sky.
function identifyAvatar(yourAvatarID) {
//print("identifyAvatar", yourAvatarID);
unidentifyAvatars();
var myAvatarID = MyAvatar.sessionUUID;
var myJointIndex = MyAvatar.getJointIndex(identifyAvatarMyJointName);
var myJointRotation =
Quat.multiply(
MyAvatar.orientation,
MyAvatar.getAbsoluteJointRotationInObjectFrame(myJointIndex));
var myJointPosition =
Vec3.sum(
MyAvatar.position,
Vec3.multiplyQbyV(
MyAvatar.orientation,
MyAvatar.getAbsoluteJointTranslationInObjectFrame(myJointIndex)));
var yourJointIndex = -1;
var yourJointPosition;
if (yourAvatarID == myAvatarID) {
// You pointed at your own name, so draw a line up from your head.
yourJointPosition = {
x: myJointPosition.x,
y: myJointPosition.y + 1000.0,
z: myJointPosition.z
};
} else {
// You pointed at somebody else's name, so draw a line from your head to their head.
var yourAvatar = AvatarList.getAvatar(yourAvatarID);
if (!yourAvatar) {
return;
}
yourJointIndex = yourAvatar.getJointIndex(identifyAvatarMyJointName)
var yourJointRotation =
Quat.multiply(
yourAvatar.orientation,
yourAvatar.getAbsoluteJointRotationInObjectFrame(yourJointIndex));
yourJointPosition =
Vec3.sum(
yourAvatar.position,
Vec3.multiplyQbyV(
yourAvatar.orientation,
yourAvatar.getAbsoluteJointTranslationInObjectFrame(yourJointIndex)));
}
var identifierParams = {
parentID: myAvatarID,
parentJointIndex: myJointIndex,
lifetime: identifyAvatarDuration,
start: myJointPosition,
endParentID: yourAvatarID,
endParentJointIndex: yourJointIndex,
end: yourJointPosition,
color: identifyAvatarLineColor,
alpha: 1,
lineWidth: 1
};
avatarIdentifiers[yourAvatarID] = identifierParams;
identifierParams.lineID = Overlays.addOverlay("line3d", identifierParams);
//print("ADDOVERLAY lineID", lineID, "myJointPosition", JSON.stringify(myJointPosition), "yourJointPosition", JSON.stringify(yourJointPosition), "lineData", JSON.stringify(lineData));
identifierParams.timer =
Script.setTimeout(function() {
//print("DELETEOVERLAY lineID");
unidentifyAvatar(yourAvatarID);
}, identifyAvatarDuration * 1000);
}
// Stop identifying an avatar.
function unidentifyAvatar(yourAvatarID) {
//print("unidentifyAvatar", yourAvatarID);
var identifierParams = avatarIdentifiers[yourAvatarID];
if (!identifierParams) {
return;
}
if (identifierParams.timer) {
Script.clearTimeout(identifierParams.timer);
}
if (identifierParams.lineID) {
Overlays.deleteOverlay(identifierParams.lineID);
}
delete avatarIdentifiers[yourAvatarID];
}
// Stop identifying all avatars.
function unidentifyAvatars() {
var ids = [];
for (var avatarID in avatarIdentifiers) {
ids.push(avatarID);
}
for (var i = 0, n = ids.length; i < n; i++) {
var avatarID = ids[i];
unidentifyAvatar(avatarID);
}
}
// Turn to face another avatar.
function faceAvatar(yourAvatarID, displayName) {
//print("faceAvatar:", yourAvatarID, displayName);
var myAvatarID = MyAvatar.sessionUUID;
if (yourAvatarID == myAvatarID) {
// You clicked on yourself.
return;
}
var yourAvatar = AvatarList.getAvatar(yourAvatarID);
if (!yourAvatar) {
logMessage(displayName + ' is not here!', null);
return;
}
// Project avatar positions to the floor and get the direction between those points,
// then face my avatar towards your avatar.
var yourPosition = yourAvatar.position;
yourPosition.y = 0;
var myPosition = MyAvatar.position;
myPosition.y = 0;
var myOrientation = Quat.lookAtSimple(myPosition, yourPosition);
MyAvatar.orientation = myOrientation;
}
// Make a hopefully unique random anonymous avatar name.
function randomAvatarName() {
return 'Anon_' + Math.floor(Math.random() * 1000000);
}
// Change the avatar size to bigger.
function biggerSize() {
//print("biggerSize");
logMessage("Increasing avatar size bigger!", null);
MyAvatar.increaseSize();
}
// Change the avatar size to smaller.
function smallerSize() {
//print("smallerSize");
logMessage("Decreasing avatar size smaler!", null);
MyAvatar.decreaseSize();
}
// Set the avatar size to normal.
function normalSize() {
//print("normalSize");
logMessage("Resetting avatar size to normal!", null);
MyAvatar.resetSize();
}
// Send out a "Who" message, including our avatarID as myAvatarID,
// which will be sent in the response, so we can tell the reply
// is to our request.
function transmitWho() {
//print("transmitWho");
logMessage("Who is here?", null);
Messages.sendMessage(
channelName,
JSON.stringify({
type: 'Who',
myAvatarID: MyAvatar.sessionUUID
}));
}
// Send a reply to a "Who" message, with a friendly message,
// our avatarID and our displayName. myAvatarID is the id
// of the avatar who send the Who message, to whom we're
// responding.
function handleWho(myAvatarID) {
var avatarID = MyAvatar.sessionUUID;
if (myAvatarID == avatarID) {
// Don't reply to myself.
return;
}
var message = "I'm here!";
var data = {};
Messages.sendMessage(
channelName,
JSON.stringify({
type: 'ReplyWho',
myAvatarID: myAvatarID,
avatarID: avatarID,
displayName: chatName,
message: message,
data: data
}));
}
// Receive the reply to a "Who" message. Ignore it unless we were the one
// who sent it out (if myAvatarIS is our avatar's id).
function handleReplyWho(myAvatarID, avatarID, displayName, message, data) {
if (myAvatarID != MyAvatar.sessionUUID) {
return;
}
handleTransmitChatMessage(avatarID, displayName, message, data);
}
// Handle input form the user, possibly multiple lines separated by newlines.
// Each line may be a chat command starting with "/", or a chat message.
function handleChatMessage(message, data) {
var messageLines = message.trim().split('\n');
for (var i = 0, n = messageLines.length; i < n; i++) {
var messageLine = messageLines[i];
if (messageLine.substr(0, 1) == '/') {
handleChatCommand(messageLine, data);
} else {
transmitChatMessage(messageLine, data);
}
}
}
// Handle a chat command prefixed by "/".
function handleChatCommand(message, data) {
var commandLine = message.substr(1);
var tokens = commandLine.trim().split(' ');
var command = tokens[0];
var rest = commandLine.substr(command.length + 1).trim();
//print("commandLine", commandLine, "command", command, "tokens", tokens, "rest", rest);
switch (command) {
case '?':
case 'help':
logMessage('Type "/?" or "/help" for help, which is this!', null);
logMessage('Type "/name <name>" to set your chat name, or "/name" to use your display name, or a random name if that is not defined.', null);
logMessage('Type "/shutup" to shut up your overhead chat message.', null);
logMessage('Type "/say <something>" to say something.', null);
logMessage('Type "/clear" to clear your cha, nullt log.', null);
logMessage('Type "/who" to ask who is h, nullere to chat.', null);
logMessage('Type "/bigger", "/smaller" or "/normal" to change, null your avatar size.', null);
logMessage('(Sorry, that\'s all there is so far!)', null);
break;
case 'name':
if (rest == '') {
if (MyAvatar.displayName) {
chatName = MyAvatar.displayName;
saveSettings();
logMessage('Your chat name has been set to your display name "' + chatName + '".', null);
} else {
chatName = randomAvatarName();
saveSettings();
logMessage('Your avatar\'s display name is not defined, so your chat name has been set to "' + chatName + '".', null);
}
} else {
chatName = rest;
saveSettings();
logMessage('Your chat name has been set to "' + chatName + '".', null);
}
break;
case 'shutup':
popDownSpeechBubble();
logMessage('Overhead chat message shut up.', null);
break;
case 'say':
if (rest == '') {
emptyChatMessage(data);
} else {
transmitChatMessage(rest, data);
}
break;
case 'who':
transmitWho();
break;
case 'clear':
clearChatLog();
break;
case 'bigger':
biggerSize();
break;
case 'smaller':
smallerSize();
break;
case 'normal':
normalSize();
break;
case 'resetsettings':
resetSettings();
updateSettings();
break;
case 'speechbubbleheight':
var y = parseInt(rest);
if (!isNaN(y)) {
speechBubbleOffset.y = y;
}
saveSettings();
updateSettings();
break;
case 'speechbubbleduration':
var duration = parseFloat(rest);
if (!isNaN(duration)) {
speechBubbleDuration = duration;
}
saveSettings();
updateSettings();
break;
default:
logMessage('Unknown chat command. Type "/help" or "/?" for help.', null);
break;
}
}
// Send out a chat message to everyone.
function transmitChatMessage(message, data) {
//print("transmitChatMessage", 'avatarID', avatarID, 'displayName', displayName, 'message', message, 'data', data);
popUpSpeechBubble(message, data);
Messages.sendMessage(
channelName,
JSON.stringify({
type: 'TransmitChatMessage',
avatarID: MyAvatar.sessionUUID,
displayName: chatName,
message: message,
data: data
}));
}
// Show the speech bubble.
function popUpSpeechBubble(message, data) {
//print("popUpSpeechBubble", message, data);
popDownSpeechBubble();
speechBubbleShowing = true;
speechBubbleMessage = message;
speechBubbleData = data;
updateSpeechBubble();
if (speechBubbleDuration > 0) {
speechBubbleTimer = Script.setTimeout(
function () {
popDownSpeechBubble();
},
speechBubbleDuration * 1000);
}
}
// Update the speech bubble.
// This is factored out so we can update an existing speech bubble if any settings change.
function updateSpeechBubble() {
if (!speechBubbleShowing) {
return;
}
var jointIndex = MyAvatar.getJointIndex(speechBubbleJointName);
var dimensions = {
x: 100.0,
y: 100.0,
z: 0.1
};
speechBubbleParams = {
type: "Text",
lifetime: speechBubbleDuration,
parentID: MyAvatar.sessionUUID,
jointIndex: jointIndex,
dimensions: dimensions,
lineHeight: speechBubbleLineHeight,
leftMargin: 0,
topMargin: 0,
rightMargin: 0,
bottomMargin: 0,
faceCamera: true,
drawInFront: true,
ignoreRayIntersection: true,
text: speechBubbleMessage,
textColor: speechBubbleTextColor,
color: speechBubbleTextColor,
backgroundColor: speechBubbleBackgroundColor
};
// Only overlay text3d has a way to measure the text, not entities.
// So we make a temporary one just for measuring text, then delete it.
var speechBubbleTextOverlayID = Overlays.addOverlay("text3d", speechBubbleParams);
var textSize = Overlays.textSize(speechBubbleTextOverlayID, speechBubbleMessage);
try {
Overlays.deleteOverlay(speechBubbleTextOverlayID);
} catch (e) {}
//print("updateSpeechBubble:", "speechBubbleMessage", speechBubbleMessage, "textSize", textSize.width, textSize.height);
var fudge = 0.02;
var width = textSize.width + fudge;
var height = textSize.height + fudge;
dimensions = {
x: width,
y: height,
z: 0.1
};
speechBubbleParams.dimensions = dimensions;
var headRotation =
Quat.multiply(
MyAvatar.orientation,
MyAvatar.getAbsoluteJointRotationInObjectFrame(jointIndex));
var headPosition =
Vec3.sum(
MyAvatar.position,
Vec3.multiplyQbyV(
MyAvatar.orientation,
MyAvatar.getAbsoluteJointTranslationInObjectFrame(jointIndex)));
var rotatedOffset =
Vec3.multiplyQbyV(
headRotation,
speechBubbleOffset);
var position =
Vec3.sum(
headPosition,
rotatedOffset);
speechBubbleParams.position = position;
if (!speechBubbleTextID) {
speechBubbleTextID =
Entities.addEntity(speechBubbleParams, true);
} else {
Entities.editEntity(speechBubbleTextID, speechBubbleParams);
}
//print("speechBubbleTextID:", speechBubbleTextID, "speechBubbleParams", JSON.stringify(speechBubbleParams));
}
// Hide the speech bubble.
function popDownSpeechBubble() {
cancelSpeechBubbleTimer();
speechBubbleShowing = false;
//print("popDownSpeechBubble speechBubbleTextID", speechBubbleTextID);
if (speechBubbleTextID) {
try {
Entities.deleteEntity(speechBubbleTextID);
} catch (e) {}
speechBubbleTextID = null;
}
}
// Cancel the speech bubble popup timer.
function cancelSpeechBubbleTimer() {
if (speechBubbleTimer) {
Script.clearTimeout(speechBubbleTimer);
speechBubbleTimer = null;
}
}
// Show the tablet web page and connect the web handler.
function showTabletWebPage() {
var url = Script.resolvePath(webPageURL);
if (randomizeWebPageURL) {
url += '?rand=' + Math.random();
}
lastWebPageURL = url;
onChatPage = true;
tablet.gotoWebScreen(lastWebPageURL);
// Connect immediately so we don't miss anything.
connectWebHandler();
}
// Update the tablet web page with the chat log.
function updateChatPage() {
if (!onChatPage) {
return;
}
tablet.emitScriptEvent(
JSON.stringify({
type: "Update",
chatLog: chatLog
}));
}
function onChatMessageReceived(channel, message, senderID) {
// Ignore messages to any other channel than mine.
if (channel != channelName) {
return;
}
// Parse the message and pull out the message parameters.
var messageData = JSON.parse(message);
var messageType = messageData.type;
//print("MESSAGE", message);
//print("MESSAGEDATA", messageData, JSON.stringify(messageData));
switch (messageType) {
case 'TransmitChatMessage':
handleTransmitChatMessage(messageData.avatarID, messageData.displayName, messageData.message, messageData.data);
break;
case 'AvatarBeginTyping':
handleAvatarBeginTyping(messageData.avatarID, messageData.displayName);
break;
case 'AvatarEndTyping':
handleAvatarEndTyping(messageData.avatarID, messageData.displayName);
break;
case 'Who':
handleWho(messageData.myAvatarID);
break;
case 'ReplyWho':
handleReplyWho(messageData.myAvatarID, messageData.avatarID, messageData.displayName, messageData.message, messageData.data);
break;
default:
print("onChatMessageReceived: unknown messageType", messageType, "message", message);
break;
}
}
// Handle events from the tablet web page.
function onWebEventReceived(event) {
if (!onChatPage) {
return;
}
//print("onWebEventReceived: event", event);
var eventData = JSON.parse(event);
var eventType = eventData.type;
switch (eventType) {
case 'Ready':
updateChatPage();
break;
case 'Update':
updateChatPage();
break;
case 'HandleChatMessage':
var message = eventData.message;
var data = eventData.data;
//print("onWebEventReceived: HandleChatMessage:", 'message', message, 'data', data);
handleChatMessage(message, data);
break;
case 'PopDownSpeechBubble':
popDownSpeechBubble();
break;
case 'EmptyChatMessage':
emptyChatMessage();
break;
case 'Type':
type();
break;
case 'BeginTyping':
beginTyping();
break;
case 'EndTyping':
endTyping();
break;
case 'IdentifyAvatar':
identifyAvatar(eventData.avatarID);
break;
case 'UnidentifyAvatar':
unidentifyAvatar(eventData.avatarID);
break;
case 'FaceAvatar':
faceAvatar(eventData.avatarID, eventData.displayName);
break;
case 'ClearChatLog':
clearChatLog();
break;
case 'Who':
transmitWho();
break;
case 'Bigger':
biggerSize();
break;
case 'Smaller':
smallerSize();
break;
case 'Normal':
normalSize();
break;
default:
print("onWebEventReceived: unexpected eventType", eventType);
break;
}
}
function onScreenChanged(type, url) {
//print("onScreenChanged", "type", type, "url", url, "lastWebPageURL", lastWebPageURL);
if ((type === "Web") &&
(url === lastWebPageURL)) {
if (!onChatPage) {
onChatPage = true;
connectWebHandler();
}
} else {
if (onChatPage) {
onChatPage = false;
disconnectWebHandler();
}
}
}
function connectWebHandler() {
if (webHandlerConnected) {
return;
}
try {
tablet.webEventReceived.connect(onWebEventReceived);
} catch (e) {
print("connectWebHandler: error connecting: " + e);
return;
}
webHandlerConnected = true;
//print("connectWebHandler connected");
updateChatPage();
}
function disconnectWebHandler() {
if (!webHandlerConnected) {
return;
}
try {
tablet.webEventReceived.disconnect(onWebEventReceived);
} catch (e) {
print("disconnectWebHandler: error disconnecting web handler: " + e);
return;
}
webHandlerConnected = false;
//print("disconnectWebHandler: disconnected");
}
// Show the tablet web page when the chat button on the tablet is clicked.
function onTabletButtonClicked() {
showTabletWebPage();
}
// Shut down the chat application when the tablet button is destroyed.
function onTabletButtonDestroyed() {
shutDown();
}
// Start up the chat application.
function startUp() {
//print("startUp");
loadSettings();
tabletButton = tablet.addButton({
icon: tabletButtonIcon,
activeIcon: tabletButtonActiveIcon,
text: tabletButtonName,
sortOrder: 0
});
Messages.subscribe(channelName);
tablet.screenChanged.connect(onScreenChanged);
Messages.messageReceived.connect(onChatMessageReceived);
tabletButton.clicked.connect(onTabletButtonClicked);
Script.scriptEnding.connect(onTabletButtonDestroyed);
logMessage('Type "/?" or "/help" for help with chat.', null);
//print("Added chat button to tablet.");
}
// Shut down the chat application.
function shutDown() {
//print("shutDown");
popDownSpeechBubble();
unidentifyAvatars();
disconnectWebHandler();
if (onChatPage) {
tablet.gotoHomeScreen();
onChatPage = false;
}
tablet.screenChanged.disconnect(onScreenChanged);
Messages.messageReceived.disconnect(onChatMessageReceived);
// Clean up the tablet button we made.
tabletButton.clicked.disconnect(onTabletButtonClicked);
tablet.removeButton(tabletButton);
tabletButton = null;
//print("Removed chat button from tablet.");
}
// Kick off the chat application!
startUp();
}());

View file

@ -0,0 +1,511 @@
<!--
// ChatPage.html
//
// Created by Faye Li on 3 Feb 2017
// Modified by Don Hopkins (dhopkins@donhopkins.com).
// Copyright 2017 High Fidelity, Inc.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
-->
<html>
<head>
<title>Chat</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="https://fonts.googleapis.com/css?family=Raleway:300,400,600,700"" rel="stylesheet">
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.1.1/jquery.min.js"></script>
<style>
input[type=button],
button {
font-family: 'Raleway';
font-weight: bold;
font-size: 20px;
vertical-align: top;
width: 100%;
height: 40px;
min-width: 120px;
padding: 0px 18px;
margin-top: 5px;
margin-right: 6px;
border-radius: 5px;
border: none;
color: #fff;
background-color: #000;
background: linear-gradient(#343434 20%, #000 100%);
cursor: pointer;
}
.commandButton {
width: 100px !important;
}
input[type=text] {
font-family: 'Raleway';
font-weight: bold;
font-size: 20px;
vertical-align: top;
height: 40px;
color: #000;
width: 100%;
background-color: #fff;
background: linear-gradient(#343434 20%, #fff 100%);
}
input[type=button].red {
color: #fff;
background-color: #94132e;
background: linear-gradient(#d42043 20%, #94132e 100%);
}
input[type=button].blue {
color: #fff;
background-color: #1080b8;
background: linear-gradient(#00b4ef 20%, #1080b8 100%);
}
input[type=button].white {
color: #121212;
background-color: #afafaf;
background: linear-gradient(#fff 20%, #afafaf 100%);
}
input[type=button]:enabled:hover {
background: linear-gradient(#000, #000);
border: none;
}
input[type=button].red:enabled:hover {
background: linear-gradient(#d42043, #d42043);
border: none;
}
input[type=button].blue:enabled:hover {
background: linear-gradient(#00b4ef, #00b4ef);
border: none;
}
input[type=button].white:enabled:hover {
background: linear-gradient(#fff, #fff);
border: none;
}
input[type=button]:active {
background: linear-gradient(#343434, #343434);
}
input[type=button].red:active {
background: linear-gradient(#94132e, #94132e);
}
input[type=button].blue:active {
background: linear-gradient(#1080b8, #1080b8);
}
input[type=button].white:active {
background: linear-gradient(#afafaf, #afafaf);
}
input[type=button]:disabled {
color: #252525;
background: linear-gradient(#575757 20%, #252525 100%);
}
input[type=button][pressed=pressed] {
color: #00b4ef;
}
body {
width: 100%;
overflow-x: hidden;
overflow-y: hidden;
margin: 0;
font-family: 'Raleway', sans-serif;
color: white;
background: linear-gradient(#2b2b2b, #0f212e);
}
.Content {
font-size: 20px;
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
}
.TopBar {
height: 40px;
background: linear-gradient(#2b2b2b, #1e1e1e);
font-weight: bold;
padding: 10px 10px 10px 10px;
display: flex;
align-items: center;
width: 100%;
font-size: 28px;
flex-grow: 0;
}
.ChatLog {
padding: 20px;
font-size: 20px;
flex-grow: 1;
color: white;
background-color: black;
overflow-x: hidden;
overflow-y: scroll;
word-wrap: break-word;
}
.ChatLogLine {
margin-bottom: 15px;
}
.ChatLogLineDisplayName {
font-weight: bold;
}
.ChatLogLineMessage {
}
.LogLogLine {
margin-bottom: 15px;
}
.LogLogLineMessage {
font-style: italic;
}
.ChatInput {
display: flex;
flex-direction: row;
flex-grow: 0;
}
.ChatInputText {
padding: 20px 20px 20px 20px;
height: 60px !important;
font-size: 20px !important;
flex-grow: 1;
display: flex;
flex-direction: column;
}
</style>
</head>
<body>
<div class="Content">
<div class="TopBar">
<b>Chat</b>
</div>
<div class="ChatLog" id="ChatLog"></div>
<div class="ChatInput">
<input type="text" class="ChatInputText" id="ChatInputText" size="256"/>
</div>
</div>
</body>
<script>
//console.log("ChatPage: loading script...");
var messageData = {}; // The data that is sent along with the message.
var typing = false; // True while the user is typing.
var typingTimerDuration = 1; // How long to wait before ending typing, in seconds.
var typingTimer = null; // The timer to end typing.
var $ChatLog; // The scrolling chat log.
var $ChatInputText; // The text field for entering text.
// Recreate the lines in chatLog as the DOM in $ChatLog.
function updateChatLog() {
$ChatLog.html('');
for (var i = 0, n = chatLog.length; i < n; i++) {
var a = chatLog[i];
var avatarID = a[0];
var displayName = a[1];
var message = a[2];
var data = a[3];
//console.log("updateChatLog", i, a, displayName, message);
if (avatarID) {
receiveChatMessage(avatarID, displayName, message, data);
} else {
logMessage(message);
}
}
}
// Call this no every keystroke.
function type() {
beginTyping();
handleType();
}
// Reset the typing timer, and notify if we're starting.
function beginTyping() {
if (typingTimer) {
clearTimeout(typingTimer);
}
typingTimer = setTimeout(function() {
typing = false;
handleEndTyping();
}, typingTimerDuration * 1000);
if (typing) {
return;
}
typing = true;
handleBeginTyping();
}
// Clear the typing timer and notify if we're finished.
function endTyping() {
if (typingTimer) {
clearTimeout(typingTimer);
typingTimer = null;
}
if (!typing) {
return;
}
typing = false;
handleEndTyping();
}
// Notify the interface script on every keystroke.
function handleType() {
EventBridge.emitWebEvent(
JSON.stringify({
type: "Type"
}));
}
// Notify the interface script when we begin typing.
function handleBeginTyping() {
EventBridge.emitWebEvent(
JSON.stringify({
type: "BeginTyping"
}));
}
// Notify the interface script when we end typing.
function handleEndTyping() {
EventBridge.emitWebEvent(
JSON.stringify({
type: "EndTyping"
}));
}
// Append a chat message to $ChatLog.
function receiveChatMessage(avatarID, displayName, message, data) {
var $logLine =
$('<div/>')
.addClass('ChatLogLine')
.data('chat-avatarID', avatarID)
.data('chat-displayName', displayName)
.data('chat-message', message)
.data('chat-data', data)
.appendTo($ChatLog);
var $logLineDisplayName =
$('<span/>')
.addClass('ChatLogLineDisplayName')
.text(displayName + ': ')
.on('mouseenter', function(event) {
identifyAvatar(avatarID);
//event.cancelBubble();
$logLineDisplayName.css({
color: '#00ff00',
'text-decoration': 'underline'
});
})
.on('mouseleave', function(event) {
unidentifyAvatar(avatarID);
//event.cancelBubble();
$logLineDisplayName.css({
color: 'white',
'text-decoration': 'none'
});
})
.click(function(event) {
faceAvatar(avatarID, displayName);
//event.cancelBubble();
})
.appendTo($logLine);
var $logLineMessage =
$('<span/>')
.addClass('ChatLogLineMessage')
.text(message)
.appendTo($logLine);
}
// Append a log message to $ChatLog.
function logMessage(message) {
var $logLine =
$('<div/>')
.addClass('LogLogLine')
.data('chat-message', message)
.appendTo($ChatLog);
var $logLineMessage =
$('<span/>')
.addClass('LogLogLineMessage')
.text(message)
.appendTo($logLine);
}
// Scroll $ChatLog so the last line is visible.
function scrollChatLog() {
var $logLine = $ChatLog.children().last();
if (!$logLine || !$logLine.length) {
return;
}
var chatLogHeight = $ChatLog.outerHeight(true);
var logLineTop = ($logLine.offset().top - $ChatLog.offset().top);
var logLineBottom = logLineTop + $logLine.outerHeight(true);
var scrollUp = logLineBottom - chatLogHeight;
if (scrollUp > 0) {
$ChatLog.scrollTop($ChatLog.scrollTop() + scrollUp);
}
}
// Tell the interface script we have initialized.
function emitReadyEvent() {
EventBridge.emitWebEvent(
JSON.stringify({
type: "Ready"
}));
}
// The user entered an empty chat message.
function emptyChatMessage() {
EventBridge.emitWebEvent(
JSON.stringify({
type: "EmptyChatMessage",
data: null
}));
}
// The user entered a non-empty chat message.
function handleChatMessage(message, data) {
//console.log("handleChatMessage", message);
EventBridge.emitWebEvent(
JSON.stringify({
type: "HandleChatMessage",
message: message,
data: data
}));
}
// Clear the chat log, of course.
function clearChatLog() {
EventBridge.emitWebEvent(
JSON.stringify({
type: "ClearChatLog"
}));
}
// Identify an avatar.
function identifyAvatar(avatarID) {
EventBridge.emitWebEvent(
JSON.stringify({
type: "IdentifyAvatar",
avatarID: avatarID
}));
}
// Stop identifying an avatar.
function unidentifyAvatar(avatarID) {
EventBridge.emitWebEvent(
JSON.stringify({
type: "UnidentifyAvatar",
avatarID: avatarID
}));
}
// Face an avatar.
function faceAvatar(avatarID, displayName) {
EventBridge.emitWebEvent(
JSON.stringify({
type: "FaceAvatar",
avatarID: avatarID,
displayName: displayName
}));
}
// Let's get this show on the road!
function main() {
//console.log("ChatPage: main");
$ChatLog = $('#ChatLog');
$ChatInputText = $('#ChatInputText');
// Whenever the chat log resizes, or the input text gets or loses focus,
// scroll the chat log to the last line.
$ChatLog.on('resize', function(event) {
//console.log("ChatLog resize", $ChatLog, event);
scrollChatLog();
});
$ChatInputText.on('focus blur', function(event) {
//console.log("ChatInputText focus blur", $ChatInputText, event);
scrollChatLog();
});
// Track when the user is typing, and handle the message when the user hits return.
$ChatInputText.on('keydown', function(event) {
type();
if (event.keyCode == 13) {
var message = $ChatInputText.val().substr(0, 256);
$ChatInputText.val('');
if (message == '') {
emptyChatMessage();
} else {
handleChatMessage(message, messageData);
}
endTyping();
}
});
// Start out with the input text in focus.
$ChatInputText.focus();
// Hook up a handler for events that come from hifi.
EventBridge.scriptEventReceived.connect(function (message) {
//console.log("ChatPage: main: scriptEventReceived", message);
var messageData = JSON.parse(message);
var messageType = messageData['type'];
switch (messageType) {
case "Update":
chatLog = messageData['chatLog'];
updateChatLog();
scrollChatLog();
break;
case "ReceiveChatMessage":
receiveChatMessage(messageData['avatarID'], messageData['displayName'], messageData['message'], message['data']);
scrollChatLog();
break;
case "LogMessage":
logMessage(messageData['message']);
scrollChatLog();
break;
default:
console.log("WEB: received unexpected script event message: " + message);
break;
}
});
emitReadyEvent();
}
// Start up once the document is loaded.
$(document).ready(main);
</script>
</html>