Merge branch 'master' into android_new

This commit is contained in:
Bradley Austin Davis 2017-09-26 14:31:29 -07:00 committed by GitHub
commit e42068f681
211 changed files with 9083 additions and 4103 deletions

View file

@ -2,7 +2,7 @@
- [cmake](https://cmake.org/download/): 3.9
- [Qt](https://www.qt.io/download-open-source): 5.9.1
- [OpenSSL](https://www.openssl.org/): Use the latest available version of OpenSSL to avoid security vulnerabilities.
- [OpenSSL](https://www.openssl.org/): Use the latest available 1.0 version (**NOT** 1.1) of OpenSSL to avoid security vulnerabilities.
- [VHACD](https://github.com/virneo/v-hacd)(clone this repository)(Optional)
### CMake External Project Dependencies

View file

@ -13,7 +13,7 @@ setup_memory_debugger()
link_hifi_libraries(
audio avatars octree gpu model fbx entities
networking animation recording shared script-engine embedded-webserver
controllers physics plugins midi
controllers physics plugins midi baking image
)
if (WIN32)

File diff suppressed because it is too large Load diff

View file

@ -14,17 +14,39 @@
#include <QtCore/QDir>
#include <QtCore/QThreadPool>
#include <QRunnable>
#include <ThreadedAssignment.h>
#include "AssetUtils.h"
#include "ReceivedMessage.h"
namespace std {
template <>
struct hash<QString> {
size_t operator()(const QString& v) const { return qHash(v); }
};
}
struct AssetMeta {
AssetMeta() {
}
int bakeVersion { 0 };
bool failedLastBake { false };
QString lastBakeErrors;
};
class BakeAssetTask;
class AssetServer : public ThreadedAssignment {
Q_OBJECT
public:
AssetServer(ReceivedMessage& message);
void aboutToFinish() override;
public slots:
void run() override;
@ -39,13 +61,14 @@ private slots:
void sendStatsPacket() override;
private:
using Mappings = QVariantHash;
using Mappings = std::unordered_map<QString, QString>;
void handleGetMappingOperation(ReceivedMessage& message, SharedNodePointer senderNode, NLPacketList& replyPacket);
void handleGetAllMappingOperation(ReceivedMessage& message, SharedNodePointer senderNode, NLPacketList& replyPacket);
void handleSetMappingOperation(ReceivedMessage& message, SharedNodePointer senderNode, NLPacketList& replyPacket);
void handleDeleteMappingsOperation(ReceivedMessage& message, SharedNodePointer senderNode, NLPacketList& replyPacket);
void handleRenameMappingOperation(ReceivedMessage& message, SharedNodePointer senderNode, NLPacketList& replyPacket);
void handleSetBakingEnabledOperation(ReceivedMessage& message, SharedNodePointer senderNode, NLPacketList& replyPacket);
// Mapping file operations must be called from main assignment thread only
bool loadMappingsFromFile();
@ -55,19 +78,54 @@ private:
bool setMapping(AssetPath path, AssetHash hash);
/// Delete mapping `path`. Returns `true` if deletion of mappings succeeds, else `false`.
bool deleteMappings(AssetPathList& paths);
bool deleteMappings(const AssetPathList& paths);
/// Rename mapping from `oldPath` to `newPath`. Returns true if successful
bool renameMapping(AssetPath oldPath, AssetPath newPath);
// deletes any unmapped files from the local asset directory
bool setBakingEnabled(const AssetPathList& paths, bool enabled);
/// Delete any unmapped files from the local asset directory
void cleanupUnmappedFiles();
QString getPathToAssetHash(const AssetHash& assetHash);
std::pair<BakingStatus, QString> getAssetStatus(const AssetPath& path, const AssetHash& hash);
void bakeAssets();
void maybeBake(const AssetPath& path, const AssetHash& hash);
void createEmptyMetaFile(const AssetHash& hash);
bool hasMetaFile(const AssetHash& hash);
bool needsToBeBaked(const AssetPath& path, const AssetHash& assetHash);
void bakeAsset(const AssetHash& assetHash, const AssetPath& assetPath, const QString& filePath);
/// Move baked content for asset to baked directory and update baked status
void handleCompletedBake(QString originalAssetHash, QString assetPath, QVector<QString> bakedFilePaths);
void handleFailedBake(QString originalAssetHash, QString assetPath, QString errors);
void handleAbortedBake(QString originalAssetHash, QString assetPath);
/// Create meta file to describe baked content for original asset
std::pair<bool, AssetMeta> readMetaFile(AssetHash hash);
bool writeMetaFile(AssetHash originalAssetHash, const AssetMeta& meta = AssetMeta());
/// Remove baked paths when the original asset is deleteds
void removeBakedPathsForDeletedAsset(AssetHash originalAssetHash);
Mappings _fileMappings;
QDir _resourcesDirectory;
QDir _filesDirectory;
QThreadPool _taskPool;
/// Task pool for handling uploads and downloads of assets
QThreadPool _transferTaskPool;
QHash<AssetHash, std::shared_ptr<BakeAssetTask>> _pendingBakes;
QThreadPool _bakingTaskPool;
bool _wasColorTextureCompressionEnabled { false };
bool _wasGrayscaleTextureCompressionEnabled { false };
bool _wasNormalTextureCompressionEnabled { false };
bool _wasCubeTextureCompressionEnabled { false };
};
#endif

View file

@ -0,0 +1,14 @@
//
// AssetServerLogging.cpp
// assignment-client/src/assets
//
// Created by Clement Brisset on 8/9/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 "AssetServerLogging.h"
Q_LOGGING_CATEGORY(asset_server, "hifi.asset-server")

View file

@ -0,0 +1,19 @@
//
// AssetServerLogging.h
// assignment-client/src/assets
//
// Created by Clement Brisset on 8/9/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_AssetServerLogging_h
#define hifi_AssetServerLogging_h
#include <QLoggingCategory>
Q_DECLARE_LOGGING_CATEGORY(asset_server)
#endif // hifi_AssetServerLogging_h

View file

@ -0,0 +1,79 @@
//
// BakeAssetTask.cpp
// assignment-client/src/assets
//
// Created by Stephen Birarda on 9/18/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 "BakeAssetTask.h"
#include <QtCore/QThread>
#include <FBXBaker.h>
#include <PathUtils.h>
BakeAssetTask::BakeAssetTask(const AssetHash& assetHash, const AssetPath& assetPath, const QString& filePath) :
_assetHash(assetHash),
_assetPath(assetPath),
_filePath(filePath)
{
}
void BakeAssetTask::run() {
_isBaking.store(true);
qRegisterMetaType<QVector<QString> >("QVector<QString>");
TextureBakerThreadGetter fn = []() -> QThread* { return QThread::currentThread(); };
if (_assetPath.endsWith(".fbx")) {
_baker = std::unique_ptr<FBXBaker> {
new FBXBaker(QUrl("file:///" + _filePath), fn, PathUtils::generateTemporaryDir())
};
} else {
_baker = std::unique_ptr<TextureBaker> {
new TextureBaker(QUrl("file:///" + _filePath), image::TextureUsage::CUBE_TEXTURE,
PathUtils::generateTemporaryDir())
};
}
QEventLoop loop;
connect(_baker.get(), &Baker::finished, &loop, &QEventLoop::quit);
connect(_baker.get(), &Baker::aborted, &loop, &QEventLoop::quit);
QMetaObject::invokeMethod(_baker.get(), "bake", Qt::QueuedConnection);
loop.exec();
if (_baker->wasAborted()) {
qDebug() << "Aborted baking: " << _assetHash << _assetPath;
_wasAborted.store(true);
emit bakeAborted(_assetHash, _assetPath);
} else if (_baker->hasErrors()) {
qDebug() << "Failed to bake: " << _assetHash << _assetPath << _baker->getErrors();
auto errors = _baker->getErrors().join('\n'); // Join error list into a single string for convenience
_didFinish.store(true);
emit bakeFailed(_assetHash, _assetPath, errors);
} else {
auto vectorOutputFiles = QVector<QString>::fromStdVector(_baker->getOutputFiles());
qDebug() << "Finished baking: " << _assetHash << _assetPath << vectorOutputFiles;
_didFinish.store(true);
emit bakeComplete(_assetHash, _assetPath, vectorOutputFiles);
}
}
void BakeAssetTask::abort() {
if (_baker) {
_baker->abort();
}
}

View file

@ -0,0 +1,52 @@
//
// BakeAssetTask.h
// assignment-client/src/assets
//
// Created by Stephen Birarda on 9/18/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_BakeAssetTask_h
#define hifi_BakeAssetTask_h
#include <memory>
#include <QtCore/QDebug>
#include <QtCore/QObject>
#include <QtCore/QRunnable>
#include <AssetUtils.h>
#include <Baker.h>
class BakeAssetTask : public QObject, public QRunnable {
Q_OBJECT
public:
BakeAssetTask(const AssetHash& assetHash, const AssetPath& assetPath, const QString& filePath);
bool isBaking() { return _isBaking.load(); }
void run() override;
void abort();
bool wasAborted() const { return _wasAborted.load(); }
bool didFinish() const { return _didFinish.load(); }
signals:
void bakeComplete(QString assetHash, QString assetPath, QVector<QString> outputFiles);
void bakeFailed(QString assetHash, QString assetPath, QString errors);
void bakeAborted(QString assetHash, QString assetPath);
private:
std::atomic<bool> _isBaking { false };
AssetHash _assetHash;
AssetPath _assetPath;
QString _filePath;
std::unique_ptr<Baker> _baker;
std::atomic<bool> _wasAborted { false };
std::atomic<bool> _didFinish { false };
};
#endif // hifi_BakeAssetTask_h

40
cmake/externals/draco/CMakeLists.txt vendored Normal file
View file

@ -0,0 +1,40 @@
set(EXTERNAL_NAME draco)
if (ANDROID)
set(ANDROID_CMAKE_ARGS "-DCMAKE_TOOLCHAIN_FILE=${CMAKE_TOOLCHAIN_FILE}" "-DANDROID_NATIVE_API_LEVEL=19")
endif ()
if (APPLE)
set(EXTRA_CMAKE_FLAGS -DCMAKE_CXX_FLAGS=-stdlib=libc++ -DCMAKE_EXE_LINKER_FLAGS=-stdlib=libc++)
endif ()
include(ExternalProject)
ExternalProject_Add(
${EXTERNAL_NAME}
URL http://hifi-public.s3.amazonaws.com/dependencies/draco-1.1.0.zip
URL_MD5 208f8b04c91d5f1c73d731a3ea37c5bb
CONFIGURE_COMMAND CMAKE_ARGS ${ANDROID_CMAKE_ARGS} -DCMAKE_BUILD_TYPE=${CMAKE_BUILD_TYPE} -DCMAKE_INSTALL_PREFIX:PATH=<INSTALL_DIR> ${EXTRA_CMAKE_FLAGS}
LOG_DOWNLOAD 1
LOG_CONFIGURE 1
LOG_BUILD 1
)
# Hide this external target (for ide users)
set_target_properties(${EXTERNAL_NAME} PROPERTIES FOLDER "hidden/externals")
ExternalProject_Get_Property(${EXTERNAL_NAME} INSTALL_DIR)
string(TOUPPER ${EXTERNAL_NAME} EXTERNAL_NAME_UPPER)
set(${EXTERNAL_NAME_UPPER}_INCLUDE_DIRS ${INSTALL_DIR}/include CACHE PATH "List of Draco include directories")
if (UNIX)
set(LIB_PREFIX "lib")
set(LIB_EXT "a")
elseif (WIN32)
set(LIB_EXT "lib")
endif ()
set(${EXTERNAL_NAME_UPPER}_LIBRARY ${INSTALL_DIR}/lib/${LIB_PREFIX}draco.${LIB_EXT} CACHE FILEPATH "Path to Draco release library")
set(${EXTERNAL_NAME_UPPER}_ENCODER_LIBRARY ${INSTALL_DIR}/lib/${LIB_PREFIX}dracoenc.${LIB_EXT} CACHE FILEPATH "Path to Draco encoder release library")
set(${EXTERNAL_NAME_UPPER}_DECODER_LIBRARY ${INSTALL_DIR}/lib/${LIB_PREFIX}dracodec.${LIB_EXT} CACHE FILEPATH "Path to Draco decoder release library")

View file

@ -0,0 +1,30 @@
#
# FindDraco.cmake
#
# Try to find Draco libraries and include path.
# Once done this will define
#
# DRACO_FOUND
# DRACO_INCLUDE_DIRS
# DRACO_LIBRARY
# DRACO_ENCODER_LIBRARY
# DRACO_DECODER_LIBRARY
#
# Created on 8/8/2017 by Stephen Birarda
# 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("${MACRO_DIR}/HifiLibrarySearchHints.cmake")
hifi_library_search_hints("draco")
find_path(DRACO_INCLUDE_DIRS draco/core/draco_types.h PATH_SUFFIXES include/draco/src include HINTS ${DRACO_SEARCH_DIRS})
find_library(DRACO_LIBRARY draco PATH_SUFFIXES "lib" HINTS ${DRACO_SEARCH_DIRS})
find_library(DRACO_ENCODER_LIBRARY draco PATH_SUFFIXES "lib" HINTS ${DRACO_SEARCH_DIRS})
find_library(DRACO_DECODER_LIBRARY draco PATH_SUFFIXES "lib" HINTS ${DRACO_SEARCH_DIRS})
include(FindPackageHandleStandardArgs)
find_package_handle_standard_args(DRACO DEFAULT_MSG DRACO_INCLUDE_DIRS DRACO_LIBRARY DRACO_ENCODER_LIBRARY DRACO_DECODER_LIBRARY)

View file

@ -1,115 +0,0 @@
# 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}")
set(FBX_LINUX_LOCATIONS "/usr/local/lib/gcc4/x64/debug/")
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} ${FBX_LINUX_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 (MSVC_VERSION EQUAL 1910)
set(VS_PREFIX vs2017)
elseif (MSVC_VERSION EQUAL 1900)
set(VS_PREFIX vs2015)
elseif (MSVC_VERSION EQUAL 1800)
set(VS_PREFIX vs2013)
elseif (MSVC_VERSION EQUAL 1700)
set(VS_PREFIX vs2012)
elseif (MSVC_VERSION EQUAL 1600)
set(VS_PREFIX vs2010)
elseif (MSVC_VERSION EQUAL 1500)
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)
else ()
_fbx_find_library(FBX_LIBRARY libfbxsdk.a release)
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()

View file

@ -836,6 +836,9 @@ Section "-Core installation"
Delete "$INSTDIR\ui_resources_200_percent.pak"
Delete "$INSTDIR\vccorlib120.dll"
Delete "$INSTDIR\version"
Delete "$INSTDIR\msvcr140.dll"
Delete "$INSTDIR\msvcp140.dll"
Delete "$INSTDIR\vcruntime140.dll"
Delete "$INSTDIR\xinput1_3.dll"
; Delete old desktop shortcuts before they were renamed during Sandbox rename

View file

@ -272,22 +272,22 @@ void DomainGatekeeper::updateNodePermissions() {
userPerms.permissions |= NodePermissions::Permission::canWriteToAssetServer;
userPerms.permissions |= NodePermissions::Permission::canReplaceDomainContent;
} else {
// this node is an agent
const QHostAddress& addr = node->getLocalSocket().getAddress();
bool isLocalUser = (addr == limitedNodeList->getLocalSockAddr().getAddress() ||
addr == QHostAddress::LocalHost);
// at this point we don't have a sending socket for packets from this node - assume it is the active socket
// or the public socket if we haven't activated a socket for the node yet
HifiSockAddr connectingAddr = node->getActiveSocket() ? *node->getActiveSocket() : node->getPublicSocket();
QString hardwareAddress;
QUuid machineFingerprint;
bool isLocalUser { false };
DomainServerNodeData* nodeData = static_cast<DomainServerNodeData*>(node->getLinkedData());
if (nodeData) {
hardwareAddress = nodeData->getHardwareAddress();
machineFingerprint = nodeData->getMachineFingerprint();
auto sendingAddress = nodeData->getSendingSockAddr().getAddress();
isLocalUser = (sendingAddress == limitedNodeList->getLocalSockAddr().getAddress() ||
sendingAddress == QHostAddress::LocalHost);
}
userPerms = setPermissionsForUser(isLocalUser, verifiedUsername, connectingAddr.getAddress(), hardwareAddress, machineFingerprint);

View file

@ -25,7 +25,7 @@
},
{ "from": "Standard.RX",
"when": [ "Application.InHMD", "Application.SnapTurn" ],
"when": [ "Application.SnapTurn" ],
"to": "Actions.StepYaw",
"filters":
[
@ -128,4 +128,4 @@
{ "from": "Standard.TrackedObject14", "to" : "Actions.TrackedObject14" },
{ "from": "Standard.TrackedObject15", "to" : "Actions.TrackedObject15" }
]
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,421 @@
@charset "UTF-8";
@font-face {
font-family: "hifi-glyphs";
src:url("fonts/hifi-glyphs.eot");
src:url("fonts/hifi-glyphs.eot?#iefix") format("embedded-opentype"),
url("fonts/hifi-glyphs.woff") format("woff"),
url("fonts/hifi-glyphs.ttf") format("truetype"),
url("fonts/hifi-glyphs.svg#hifi-glyphs") format("svg");
font-weight: normal;
font-style: normal;
}
[data-icon]:before {
font-family: "hifi-glyphs" !important;
content: attr(data-icon);
font-style: normal !important;
font-weight: normal !important;
font-variant: normal !important;
text-transform: none !important;
speak: none;
line-height: 1;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
[class^="icon-"]:before,
[class*=" icon-"]:before {
font-family: "hifi-glyphs" !important;
font-style: normal !important;
font-weight: normal !important;
font-variant: normal !important;
text-transform: none !important;
speak: none;
line-height: 1;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.icon-hmd:before {
content: "\62";
}
.icon-2d-screen:before {
content: "\63";
}
.icon-keyboard:before {
content: "\64";
}
.icon-hand-controllers:before {
content: "\65";
}
.icon-headphones-mic:before {
content: "\66";
}
.icon-gamepad:before {
content: "\67";
}
.icon-headphones:before {
content: "\68";
}
.icon-mic:before {
content: "\69";
}
.icon-upload:before {
content: "\6a";
}
.icon-script:before {
content: "\6b";
}
.icon-text:before {
content: "\6c";
}
.icon-cube:before {
content: "\6d";
}
.icon-sphere:before {
content: "\6e";
}
.icon-zone:before {
content: "\6f";
}
.icon-light:before {
content: "\70";
}
.icon-web:before {
content: "\71";
}
.icon-web-2:before {
content: "\72";
}
.icon-edit:before {
content: "\73";
}
.icon-market:before {
content: "\74";
}
.icon-directory:before {
content: "\75";
}
.icon-menu:before {
content: "\76";
}
.icon-close:before {
content: "\77";
}
.icon-close-inverted:before {
content: "\78";
}
.icon-pin:before {
content: "\79";
}
.icon-pin-inverted:before {
content: "\7a";
}
.icon-resize-handle:before {
content: "\41";
}
.icon-diclosure-expand:before {
content: "\42";
}
.icon-reload-small:before {
content: "\61";
}
.icon-close-small:before {
content: "\43";
}
.icon-backward:before {
content: "\45";
}
.icon-reload:before {
content: "\46";
}
.icon-minimize:before {
content: "\49";
}
.icon-maximize:before {
content: "\4a";
}
.icon-maximize-inverted:before {
content: "\4b";
}
.icon-disclosure-button-expand:before {
content: "\4c";
}
.icon-disclosure-button-collapse:before {
content: "\4d";
}
.icon-script-stop:before {
content: "\4e";
}
.icon-script-reload:before {
content: "\4f";
}
.icon-script-run:before {
content: "\50";
}
.icon-script-new:before {
content: "\51";
}
.icon-hifi-forum:before {
content: "\32";
}
.icon-hifi-logo-small:before {
content: "\53";
}
.icon-avatar-1:before {
content: "\54";
}
.icon-placemark:before {
content: "\55";
}
.icon-box:before {
content: "\56";
}
.icon-community:before {
content: "\30";
}
.icon-grab-handle:before {
content: "\58";
}
.icon-search:before {
content: "\59";
}
.icon-disclosure-collapse:before {
content: "\5a";
}
.icon-script-upload:before {
content: "\52";
}
.icon-code:before {
content: "\57";
}
.icon-avatar:before {
content: "\3c";
}
.icon-arrows-h:before {
content: "\3a";
}
.icon-arrows-v:before {
content: "\3b";
}
.icon-arrows:before {
content: "\60";
}
.icon-compress:before {
content: "\21";
}
.icon-expand:before {
content: "\22";
}
.icon-placemark-1:before {
content: "\23";
}
.icon-circle:before {
content: "\24";
}
.icon-hand-pointer:before {
content: "\39";
}
.icon-plus-square-o:before {
content: "\25";
}
.icon-square:before {
content: "\27";
}
.icon-align-center:before {
content: "\38";
}
.icon-align-justify:before {
content: "\29";
}
.icon-align-left:before {
content: "\2a";
}
.icon-align-right:before {
content: "\5e";
}
.icon-bars:before {
content: "\37";
}
.icon-circle-slash:before {
content: "\2c";
}
.icon-sync:before {
content: "\28";
}
.icon-key:before {
content: "\2d";
}
.icon-link:before {
content: "\2e";
}
.icon-location:before {
content: "\2f";
}
.icon-carat-r:before {
content: "\33";
}
.icon-carat-l:before {
content: "\34";
}
.icon-folder-lg:before {
content: "\3e";
}
.icon-folder-sm:before {
content: "\3f";
}
.icon-level-up:before {
content: "\31";
}
.icon-info:before {
content: "\5b";
}
.icon-question:before {
content: "\5d";
}
.icon-alert:before {
content: "\2b";
}
.icon-home:before {
content: "\5f";
}
.icon-error:before {
content: "\3d";
}
.icon-settings:before {
content: "\40";
}
.icon-trash:before {
content: "\7b";
}
.icon-object-group:before {
content: "\e000";
}
.icon-cm:before {
content: "\7d";
}
.icon-msvg:before {
content: "\7e";
}
.icon-deg:before {
content: "\5c";
}
.icon-px:before {
content: "\7c";
}
.icon-m-sq:before {
content: "\e001";
}
.icon-m-cubed:before {
content: "\e002";
}
.icon-acceleration:before {
content: "\e003";
}
.icon-particles:before {
content: "\e004";
}
.icon-voxels:before {
content: "\e005";
}
.icon-lock:before {
content: "\e006";
}
.icon-visible:before {
content: "\e007";
}
.icon-model:before {
content: "\e008";
}
.icon-forward:before {
content: "\44";
}
.icon-avatar-2:before {
content: "\e009";
}
.icon-arrow-dn:before {
content: "\35";
}
.icon-arrow-up:before {
content: "\36";
}
.icon-time:before {
content: "\e00a";
}
.icon-transparency:before {
content: "\e00b";
}
.icon-unmuted:before {
content: "\47";
}
.icon-user:before {
content: "\e00c";
}
.icon-edit-pencil:before {
content: "\e00d";
}
.icon-muted:before {
content: "\48";
}
.icon-vol-0:before {
content: "\e00e";
}
.icon-vol-1:before {
content: "\e00f";
}
.icon-vol-2:before {
content: "\e010";
}
.icon-vol-3:before {
content: "\e011";
}
.icon-vol-4:before {
content: "\e012";
}
.icon-vol-x-0:before {
content: "\e013";
}
.icon-vol-x-1:before {
content: "\e014";
}
.icon-vol-x-2:before {
content: "\e015";
}
.icon-vol-x-3:before {
content: "\e016";
}
.icon-vol-x-4:before {
content: "\e017";
}
.icon-share-ext:before {
content: "\e018";
}
.icon-ellipsis:before {
content: "\e019";
}
.icon-check:before {
content: "\e01a";
}
.icon-sliders:before {
content: "\26";
}
.icon-polyline:before {
content: "\e01b";
}
.icon-source:before {
content: "\e01c";
}
.icon-playback-play:before {
content: "\e01d";
}
.icon-stop-square:before {
content: "\e01e";
}
.icon-avatar-t-pose:before {
content: "\e01f";
}
.icon-check-2-01:before {
content: "\e020";
}

View file

@ -10,6 +10,7 @@
import QtQuick 2.5
import QtQuick.Controls 1.4
import QtQuick.Controls.Styles 1.4
import QtQuick.Dialogs 1.2 as OriginalDialogs
import Qt.labs.settings 1.0
@ -48,8 +49,14 @@ ScrollingWindow {
Component.onCompleted: {
ApplicationInterface.uploadRequest.connect(uploadClicked);
assetMappingsModel.errorGettingMappings.connect(handleGetMappingsError);
assetMappingsModel.autoRefreshEnabled = true;
reload();
}
Component.onDestruction: {
assetMappingsModel.autoRefreshEnabled = false;
}
function doDeleteFile(path) {
console.log("Deleting " + path);
@ -70,11 +77,11 @@ ScrollingWindow {
function doRenameFile(oldPath, newPath) {
if (newPath[0] != "/") {
if (newPath[0] !== "/") {
newPath = "/" + newPath;
}
if (oldPath[oldPath.length - 1] == '/' && newPath[newPath.length - 1] != '/') {
if (oldPath[oldPath.length - 1] === '/' && newPath[newPath.length - 1] != '/') {
// this is a folder rename but the user neglected to add a trailing slash when providing a new path
newPath = newPath + "/";
}
@ -144,7 +151,6 @@ ScrollingWindow {
function reload() {
Assets.mappingModel.refresh();
treeView.selection.clear();
}
function handleGetMappingsError(errorString) {
@ -300,7 +306,7 @@ ScrollingWindow {
object.selected.connect(function(destinationPath) {
destinationPath = destinationPath.trim();
if (path == destinationPath) {
if (path === destinationPath) {
return;
}
if (fileExists(destinationPath)) {
@ -361,7 +367,7 @@ ScrollingWindow {
running: false
}
property var uploadOpen: false;
property bool uploadOpen: false;
Timer {
id: timer
}
@ -464,20 +470,10 @@ ScrollingWindow {
HifiControls.VerticalSpacer {}
Row {
id: buttonRow
anchors.left: parent.left
anchors.right: parent.right
spacing: hifi.dimensions.contentSpacing.x
HifiControls.GlyphButton {
glyph: hifi.glyphs.reload
color: hifi.buttons.black
colorScheme: root.colorScheme
width: hifi.dimensions.controlLineHeight
onClicked: root.reload()
}
HifiControls.Button {
text: "Add To World"
color: hifi.buttons.black
@ -511,7 +507,182 @@ ScrollingWindow {
enabled: treeView.selection.hasSelection
}
}
}
HifiControls.Tree {
id: treeView
anchors.top: assetDirectory.bottom
anchors.bottom: infoRow.top
anchors.margins: hifi.dimensions.contentMargin.x + 2 // Extra for border
anchors.left: parent.left
anchors.right: parent.right
treeModel: assetProxyModel
selectionMode: SelectionMode.ExtendedSelection
headerVisible: true
sortIndicatorVisible: true
colorScheme: root.colorScheme
modifyEl: renameEl
TableViewColumn {
id: nameColumn
title: "Name:"
role: "name"
width: treeView.width - bakedColumn.width;
}
TableViewColumn {
id: bakedColumn
title: "Use Baked?"
role: "baked"
width: 100
}
itemDelegate: Loader {
id: itemDelegateLoader
anchors {
left: parent ? parent.left : undefined
leftMargin: (styleData.column === 0 ? (2 + styleData.depth) : 1) * hifi.dimensions.tablePadding
right: parent ? parent.right : undefined
rightMargin: hifi.dimensions.tablePadding
verticalCenter: parent ? parent.verticalCenter : undefined
}
function convertToGlyph(text) {
switch (text) {
case "Not Baked":
return hifi.glyphs.circleSlash;
case "Baked":
return hifi.glyphs.check_2_01;
case "Error":
return hifi.glyphs.alert;
default:
return "";
}
}
function getComponent() {
if ((styleData.column === 0) && styleData.selected) {
return textFieldComponent;
} else if (convertToGlyph(styleData.value) != "") {
return glyphComponent;
} else {
return labelComponent;
}
}
sourceComponent: getComponent()
Component {
id: labelComponent
FiraSansSemiBold {
text: styleData.value
size: hifi.fontSizes.tableText
color: colorScheme == hifi.colorSchemes.light
? (styleData.selected ? hifi.colors.black : hifi.colors.baseGrayHighlight)
: (styleData.selected ? hifi.colors.black : hifi.colors.lightGrayText)
elide: Text.ElideRight
horizontalAlignment: styleData.column === 1 ? TextInput.AlignHCenter : TextInput.AlignLeft
}
}
Component {
id: glyphComponent
HiFiGlyphs {
text: convertToGlyph(styleData.value)
size: hifi.dimensions.frameIconSize
color: colorScheme == hifi.colorSchemes.light
? (styleData.selected ? hifi.colors.black : hifi.colors.baseGrayHighlight)
: (styleData.selected ? hifi.colors.black : hifi.colors.lightGrayText)
elide: Text.ElideRight
horizontalAlignment: TextInput.AlignHCenter
HifiControls.ToolTip {
anchors.fill: parent
visible: styleData.value === "Error"
toolTip: assetProxyModel.data(styleData.index, 0x106)
}
}
}
Component {
id: textFieldComponent
TextField {
id: textField
readOnly: !activeFocus
text: styleData.value
FontLoader { id: firaSansSemiBold; source: "../../fonts/FiraSans-SemiBold.ttf"; }
font.family: firaSansSemiBold.name
font.pixelSize: hifi.fontSizes.textFieldInput
height: hifi.dimensions.tableRowHeight
style: TextFieldStyle {
textColor: readOnly
? hifi.colors.black
: (treeView.isLightColorScheme ? hifi.colors.black : hifi.colors.white)
background: Rectangle {
visible: !readOnly
color: treeView.isLightColorScheme ? hifi.colors.white : hifi.colors.black
border.color: hifi.colors.primaryHighlight
border.width: 1
}
selectedTextColor: hifi.colors.black
selectionColor: hifi.colors.primaryHighlight
padding.left: readOnly ? 0 : hifi.dimensions.textPadding
padding.right: readOnly ? 0 : hifi.dimensions.textPadding
}
validator: RegExpValidator {
regExp: /[^/]+/
}
Keys.onPressed: {
if (event.key == Qt.Key_Escape) {
text = styleData.value;
unfocusHelper.forceActiveFocus();
event.accepted = true;
}
}
onAccepted: {
if (acceptableInput && styleData.selected) {
if (!treeView.modifyEl(treeView.selection.currentIndex, text)) {
text = styleData.value;
}
unfocusHelper.forceActiveFocus();
}
}
onReadOnlyChanged: {
// Have to explicily set keyboardRaised because automatic setting fails because readOnly is true at the time.
keyboardRaised = activeFocus;
}
}
}
}
MouseArea {
propagateComposedEvents: true
anchors.fill: parent
acceptedButtons: Qt.RightButton
onClicked: {
if (!HMD.active) { // Popup only displays properly on desktop
var index = treeView.indexAt(mouse.x, mouse.y);
treeView.selection.setCurrentIndex(index, 0x0002);
contextMenu.currentIndex = index;
contextMenu.popup();
}
}
}
Menu {
id: contextMenu
title: "Edit"
@ -541,36 +712,49 @@ ScrollingWindow {
}
}
HifiControls.Tree {
id: treeView
anchors.top: assetDirectory.bottom
Row {
id: infoRow
anchors.left: treeView.left
anchors.right: treeView.right
anchors.bottom: uploadSection.top
anchors.margins: hifi.dimensions.contentMargin.x + 2 // Extra for border
anchors.left: parent.left
anchors.right: parent.right
treeModel: assetProxyModel
canEdit: true
colorScheme: root.colorScheme
selectionMode: SelectionMode.ExtendedSelection
modifyEl: renameEl
MouseArea {
propagateComposedEvents: true
anchors.fill: parent
acceptedButtons: Qt.RightButton
onClicked: {
if (!HMD.active) { // Popup only displays properly on desktop
var index = treeView.indexAt(mouse.x, mouse.y);
treeView.selection.setCurrentIndex(index, 0x0002);
contextMenu.currentIndex = index;
contextMenu.popup();
}
}
anchors.bottomMargin: hifi.dimensions.contentSpacing.y
spacing: hifi.dimensions.contentSpacing.x
RalewayRegular {
size: hifi.fontSizes.sectionName
font.capitalization: Font.AllUppercase
text: selectedItems + " items selected"
color: hifi.colors.lightGrayText
}
HifiControls.CheckBox {
function isChecked() {
var status = assetProxyModel.data(treeView.selection.currentIndex, 0x105);
var bakingDisabled = (status === "Not Baked" || status === "--");
return selectedItems === 1 && !bakingDisabled;
}
text: "Use baked (optimized) versions"
colorScheme: root.colorScheme
enabled: selectedItems === 1 && assetProxyModel.data(treeView.selection.currentIndex, 0x105) !== "--"
checked: isChecked()
onClicked: {
var mappings = [];
for (var i in treeView.selection.selectedIndexes) {
var index = treeView.selection.selectedIndexes[i];
var path = assetProxyModel.data(index, 0x100);
mappings.push(path);
}
print("Setting baking enabled:" + mappings + " " + checked);
Assets.setBakingEnabled(mappings, checked, function() {
reload();
});
checked = Qt.binding(isChecked);
}
}
}
HifiControls.ContentSection {
id: uploadSection
name: "Upload A File"

View file

@ -0,0 +1,113 @@
//
// CheckBox2.qml
//
// Created by Vlad Stelmahovsky on 10 Aug 2017
// Copyright 2017 High Fidelity, Inc.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
import QtQuick 2.7
import QtQuick.Controls 2.2
import "../styles-uit"
import "../controls-uit" as HiFiControls
CheckBox {
id: checkBox
HifiConstants { id: hifi; }
padding: 0
leftPadding: 0
property int colorScheme: hifi.colorSchemes.light
property string color: hifi.colors.lightGrayText
readonly property bool isLightColorScheme: colorScheme === hifi.colorSchemes.light
property bool isRedCheck: false
property bool isRound: false
property int boxSize: 14
property int boxRadius: isRound ? boxSize : 3
property bool wrap: true;
readonly property int checkSize: Math.max(boxSize - 8, 10)
readonly property int checkRadius: isRound ? checkSize / 2 : 2
focusPolicy: Qt.ClickFocus
indicator: Rectangle {
id: box
implicitWidth: boxSize
implicitHeight: boxSize
radius: boxRadius
x: checkBox.leftPadding
y: parent.height / 2 - height / 2
border.width: 1
border.color: pressed || hovered
? hifi.colors.checkboxCheckedBorder
: (checkBox.isLightColorScheme ? hifi.colors.checkboxLightFinish : hifi.colors.checkboxDarkFinish)
gradient: Gradient {
GradientStop {
position: 0.2
color: pressed || hovered
? (checkBox.isLightColorScheme ? hifi.colors.checkboxChecked : hifi.colors.checkboxLightStart)
: (checkBox.isLightColorScheme ? hifi.colors.checkboxLightStart : hifi.colors.checkboxDarkStart)
}
GradientStop {
position: 1.0
color: pressed || hovered
? (checkBox.isLightColorScheme ? hifi.colors.checkboxChecked : hifi.colors.checkboxLightFinish)
: (checkBox.isLightColorScheme ? hifi.colors.checkboxLightFinish : hifi.colors.checkboxDarkFinish)
}
}
Rectangle {
visible: pressed || hovered
anchors.centerIn: parent
id: innerBox
width: checkSize - 4
height: width
radius: checkRadius
color: hifi.colors.checkboxCheckedBorder
}
Rectangle {
id: check
width: checkSize
height: checkSize
radius: checkRadius
anchors.centerIn: parent
color: isRedCheck ? hifi.colors.checkboxCheckedRed : hifi.colors.checkboxChecked
border.width: 2
border.color: isRedCheck? hifi.colors.checkboxCheckedBorderRed : hifi.colors.checkboxCheckedBorder
visible: checked && !pressed || !checked && pressed
}
Rectangle {
id: disabledOverlay
visible: !enabled
width: boxSize
height: boxSize
radius: boxRadius
border.width: 1
border.color: hifi.colors.baseGrayHighlight
color: hifi.colors.baseGrayHighlight
opacity: 0.5
}
}
contentItem: Text {
id: root
FontLoader { id: ralewaySemiBold; source: pathToFonts + "fonts/Raleway-SemiBold.ttf"; }
font.pixelSize: hifi.fontSizes.inputLabel
font.family: ralewaySemiBold.name
text: checkBox.text
color: checkBox.color
x: 2
wrapMode: checkBox.wrap ? Text.Wrap : Text.NoWrap
elide: checkBox.wrap ? Text.ElideNone : Text.ElideRight
enabled: checkBox.enabled
verticalAlignment: Text.AlignVCenter
leftPadding: checkBox.indicator.width + checkBox.spacing
}
}

View file

@ -34,7 +34,10 @@ Item {
onClicked: {
mouse.accepted = true;
webEntity.synthesizeKeyPress(glyph);
webEntity.synthesizeKeyPress(glyph, mirrorText);
if (toggle) {
toggled = !toggled;
}

View file

@ -9,6 +9,7 @@
//
import QtQuick 2.0
import "."
Item {
id: keyboardBase
@ -16,9 +17,15 @@ Item {
property bool raised: false
property bool numeric: false
readonly property int keyboardRowHeight: 50
readonly property int keyboardWidth: 480
readonly property int mirrorTextHeight: keyboardRowHeight
property bool showMirrorText: true
readonly property int raisedHeight: 200
height: enabled && raised ? raisedHeight : 0
height: enabled && raised ? raisedHeight + (showMirrorText ? keyboardRowHeight : 0) : 0
visible: enabled && raised
property bool shiftMode: false
@ -93,24 +100,35 @@ Item {
}
Rectangle {
id: leftRect
y: 0
height: 200
x: 0
height: showMirrorText ? mirrorTextHeight : 0
width: keyboardWidth
color: "#252525"
anchors.right: keyboardRect.left
anchors.rightMargin: 0
anchors.bottom: parent.bottom
anchors.bottomMargin: 0
anchors.left: parent.left
anchors.leftMargin: 0
TextEdit {
id: mirrorText
visible: showMirrorText
size: 13.5
horizontalAlignment: Text.AlignHCenter
color: "#FFFFFF";
anchors.fill: parent
wrapMode: Text.WordWrap
readOnly: false // we need to leave this property read-only to allow control to accept QKeyEvent
selectByMouse: false
}
MouseArea { // ... and we need this mouse area to prevent mirrorText from getting mouse events to ensure it will never get focus
anchors.fill: parent
}
}
Rectangle {
id: keyboardRect
x: 206
y: 0
width: 480
height: 200
x: 0
y: showMirrorText ? mirrorTextHeight : 0
width: keyboardWidth
height: raisedHeight
color: "#252525"
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: parent.bottom
@ -118,13 +136,13 @@ Item {
Column {
id: columnAlpha
width: 480
height: 200
width: keyboardWidth
height: raisedHeight
visible: !numeric
Row {
width: 480
height: 50
width: keyboardWidth
height: keyboardRowHeight
anchors.left: parent.left
anchors.leftMargin: 4
@ -142,8 +160,8 @@ Item {
}
Row {
width: 480
height: 50
width: keyboardWidth
height: keyboardRowHeight
anchors.left: parent.left
anchors.leftMargin: 20
@ -160,8 +178,8 @@ Item {
}
Row {
width: 480
height: 50
width: keyboardWidth
height: keyboardRowHeight
anchors.left: parent.left
anchors.leftMargin: 4
@ -185,8 +203,8 @@ Item {
}
Row {
width: 480
height: 50
width: keyboardWidth
height: keyboardRowHeight
anchors.left: parent.left
anchors.leftMargin: 4
@ -205,13 +223,13 @@ Item {
Column {
id: columnNumeric
width: 480
height: 200
width: keyboardWidth
height: raisedHeight
visible: numeric
Row {
width: 480
height: 50
width: keyboardWidth
height: keyboardRowHeight
anchors.left: parent.left
anchors.leftMargin: 4
@ -229,8 +247,8 @@ Item {
}
Row {
width: 480
height: 50
width: keyboardWidth
height: keyboardRowHeight
anchors.left: parent.left
anchors.leftMargin: 4
@ -248,8 +266,8 @@ Item {
}
Row {
width: 480
height: 50
width: keyboardWidth
height: keyboardRowHeight
anchors.left: parent.left
anchors.leftMargin: 4
@ -273,8 +291,8 @@ Item {
}
Row {
width: 480
height: 50
width: keyboardWidth
height: keyboardRowHeight
anchors.left: parent.left
anchors.leftMargin: 4
@ -291,31 +309,4 @@ Item {
}
}
}
Rectangle {
id: rightRect
y: 280
height: 200
color: "#252525"
border.width: 0
anchors.left: keyboardRect.right
anchors.leftMargin: 0
anchors.right: parent.right
anchors.rightMargin: 0
anchors.bottom: parent.bottom
anchors.bottomMargin: 0
}
Rectangle {
id: rectangle1
color: "#ffffff"
anchors.bottom: keyboardRect.top
anchors.bottomMargin: 0
anchors.left: parent.left
anchors.leftMargin: 0
anchors.right: parent.right
anchors.rightMargin: 0
anchors.top: parent.top
anchors.topMargin: 0
}
}

View file

@ -0,0 +1,51 @@
//
// ToolTip.qml
//
// Created by Clement on 9/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
//
import QtQuick 2.5
Item {
property string toolTip
property bool showToolTip: false
Rectangle {
id: toolTipRectangle
anchors.right: parent.right
width: toolTipText.width + 4
height: toolTipText.height + 4
opacity: (toolTip != "" && showToolTip) ? 1 : 0
color: "#ffffaa"
border.color: "#0a0a0a"
Text {
id: toolTipText
text: toolTip
color: "black"
anchors.centerIn: parent
}
Behavior on opacity {
PropertyAnimation {
easing.type: Easing.InOutQuad
duration: 250
}
}
}
MouseArea {
id: mouseArea
anchors.fill: parent
onEntered: showTimer.start()
onExited: { showToolTip = false; showTimer.stop(); }
hoverEnabled: true
}
Timer {
id: showTimer
interval: 250
onTriggered: { showToolTip = true; }
}
}

View file

@ -19,7 +19,7 @@ TreeView {
id: treeView
property var treeModel: ListModel { }
property var canEdit: false
property bool centerHeaderText: false
property int colorScheme: hifi.colorSchemes.light
readonly property bool isLightColorScheme: colorScheme == hifi.colorSchemes.light
@ -31,14 +31,9 @@ TreeView {
model: treeModel
}
TableViewColumn {
role: "display";
}
anchors { left: parent.left; right: parent.right }
headerVisible: false
headerDelegate: Item { } // Fix OSX QML bug that displays scrollbar starting too low.
// Use rectangle to draw border with rounded corners.
frameVisible: false
@ -61,6 +56,64 @@ TreeView {
// Needed in order for rows to keep displaying rows after end of table entries.
backgroundColor: parent.isLightColorScheme ? hifi.colors.tableRowLightEven : hifi.colors.tableRowDarkEven
alternateBackgroundColor: parent.isLightColorScheme ? hifi.colors.tableRowLightOdd : hifi.colors.tableRowDarkOdd
headerDelegate: Rectangle {
height: hifi.dimensions.tableHeaderHeight
color: isLightColorScheme ? hifi.colors.tableBackgroundLight : hifi.colors.tableBackgroundDark
RalewayRegular {
id: titleText
text: styleData.value
size: hifi.fontSizes.tableHeading
font.capitalization: Font.AllUppercase
color: hifi.colors.baseGrayHighlight
horizontalAlignment: (centerHeaderText ? Text.AlignHCenter : Text.AlignLeft)
elide: Text.ElideRight
anchors {
left: parent.left
leftMargin: hifi.dimensions.tablePadding
right: sortIndicatorVisible && sortIndicatorColumn === styleData.column ? titleSort.left : parent.right
rightMargin: hifi.dimensions.tablePadding
verticalCenter: parent.verticalCenter
}
}
HiFiGlyphs {
id: titleSort
text: sortIndicatorOrder == Qt.AscendingOrder ? hifi.glyphs.caratUp : hifi.glyphs.caratDn
color: isLightColorScheme ? hifi.colors.darkGray : hifi.colors.baseGrayHighlight
opacity: 0.6;
size: hifi.fontSizes.tableHeadingIcon
anchors {
right: parent.right
verticalCenter: titleText.verticalCenter
}
visible: sortIndicatorVisible && sortIndicatorColumn === styleData.column
}
Rectangle {
width: 1
anchors {
left: parent.left
top: parent.top
topMargin: 1
bottom: parent.bottom
bottomMargin: 2
}
color: isLightColorScheme ? hifi.colors.lightGrayText : hifi.colors.baseGrayHighlight
visible: styleData.column > 0
}
Rectangle {
height: 1
anchors {
left: parent.left
right: parent.right
bottom: parent.bottom
}
color: isLightColorScheme ? hifi.colors.lightGrayText : hifi.colors.baseGrayHighlight
}
}
branchDelegate: HiFiGlyphs {
text: styleData.isExpanded ? hifi.glyphs.caratDn : hifi.glyphs.caratR
@ -76,28 +129,53 @@ TreeView {
handle: Item {
id: scrollbarHandle
implicitWidth: 6
implicitWidth: hifi.dimensions.scrollbarHandleWidth
Rectangle {
anchors {
fill: parent
leftMargin: 2 // Move it right
rightMargin: -2 // ""
topMargin: 3 // Shrink vertically
topMargin: treeView.headerVisible ? hifi.dimensions.tableHeaderHeight + 3 : 3
bottomMargin: 3 // ""
leftMargin: 1 // Move it right
rightMargin: -1 // ""
}
radius: 3
color: hifi.colors.tableScrollHandleDark
radius: hifi.dimensions.scrollbarHandleWidth / 2
color: treeView.isLightColorScheme ? hifi.colors.tableScrollHandleLight : hifi.colors.tableScrollHandleDark
}
}
scrollBarBackground: Item {
implicitWidth: 9
implicitWidth: hifi.dimensions.scrollbarBackgroundWidth
Rectangle {
anchors {
fill: parent
topMargin: treeView.headerVisible ? hifi.dimensions.tableHeaderHeight - 1 : -1
margins: -1 // Expand
}
color: hifi.colors.tableBackgroundDark
color: treeView.isLightColorScheme ? hifi.colors.tableScrollBackgroundLight : hifi.colors.tableScrollBackgroundDark
// Extend header color above scrollbar background
Rectangle {
anchors {
top: parent.top
topMargin: -hifi.dimensions.tableHeaderHeight
left: parent.left
right: parent.right
}
height: hifi.dimensions.tableHeaderHeight
color: treeView.isLightColorScheme ? hifi.colors.tableBackgroundLight : hifi.colors.tableBackgroundDark
visible: treeView.headerVisible
}
Rectangle {
// Extend header bottom border
anchors {
top: parent.top
left: parent.left
right: parent.right
}
height: 1
color: treeView.isLightColorScheme ? hifi.colors.lightGrayText : hifi.colors.baseGrayHighlight
visible: treeView.headerVisible
}
}
}
@ -119,9 +197,7 @@ TreeView {
: (styleData.alternate ? hifi.colors.tableRowDarkEven : hifi.colors.tableRowDarkOdd)
}
itemDelegate: Loader {
id: itemDelegateLoader
itemDelegate: FiraSansSemiBold {
anchors {
left: parent ? parent.left : undefined
leftMargin: (2 + styleData.depth) * hifi.dimensions.tablePadding
@ -130,83 +206,13 @@ TreeView {
verticalCenter: parent ? parent.verticalCenter : undefined
}
function getComponent() {
if (treeView.canEdit && styleData.selected) {
return textFieldComponent;
} else {
return labelComponent;
}
}
sourceComponent: getComponent()
Component {
id: labelComponent
FiraSansSemiBold {
text: styleData.value
size: hifi.fontSizes.tableText
color: colorScheme == hifi.colorSchemes.light
? (styleData.selected ? hifi.colors.black : hifi.colors.baseGrayHighlight)
: (styleData.selected ? hifi.colors.black : hifi.colors.lightGrayText)
}
}
Component {
id: textFieldComponent
TextField {
id: textField
readOnly: !activeFocus
text: styleData.value
FontLoader { id: firaSansSemiBold; source: "../../fonts/FiraSans-SemiBold.ttf"; }
font.family: firaSansSemiBold.name
font.pixelSize: hifi.fontSizes.textFieldInput
height: hifi.dimensions.tableRowHeight
style: TextFieldStyle {
textColor: readOnly
? hifi.colors.black
: (treeView.isLightColorScheme ? hifi.colors.black : hifi.colors.white)
background: Rectangle {
visible: !readOnly
color: treeView.isLightColorScheme ? hifi.colors.white : hifi.colors.black
border.color: hifi.colors.primaryHighlight
border.width: 1
}
selectedTextColor: hifi.colors.black
selectionColor: hifi.colors.primaryHighlight
padding.left: readOnly ? 0 : hifi.dimensions.textPadding
padding.right: readOnly ? 0 : hifi.dimensions.textPadding
}
validator: RegExpValidator {
regExp: /[^/]+/
}
Keys.onPressed: {
if (event.key == Qt.Key_Escape) {
text = styleData.value;
unfocusHelper.forceActiveFocus();
event.accepted = true;
}
}
onAccepted: {
if (acceptableInput && styleData.selected) {
if (!modifyEl(selection.currentIndex, text)) {
text = styleData.value;
}
unfocusHelper.forceActiveFocus();
}
}
onReadOnlyChanged: {
// Have to explicily set keyboardRaised because automatic setting fails because readOnly is true at the time.
keyboardRaised = activeFocus;
}
}
}
text: styleData.value
size: hifi.fontSizes.tableText
color: colorScheme == hifi.colorSchemes.light
? (styleData.selected ? hifi.colors.black : hifi.colors.baseGrayHighlight)
: (styleData.selected ? hifi.colors.black : hifi.colors.lightGrayText)
elide: Text.ElideRight
}
Item {
@ -219,11 +225,4 @@ TreeView {
onClicked: {
selectionModel.setCurrentIndex(index, ItemSelectionModel.ClearAndSelect);
}
onActivated: {
var path = scriptsModel.data(index, 0x100)
if (path) {
loadScript(path)
}
}
}

View file

@ -12,7 +12,7 @@
//
import QtQuick 2.5
import QtQuick.Controls 1.4
import QtQuick.Controls 2.2
import QtQuick.Layouts 1.3
import "../../styles-uit"
@ -36,7 +36,41 @@ Rectangle {
return (root.parent !== null) && root.parent.objectName == "loader";
}
property bool isVR: Audio.context === "VR"
property real rightMostInputLevelPos: 0
//placeholder for control sizes and paddings
//recalculates dynamically in case of UI size is changed
QtObject {
id: margins
property real paddings: root.width / 20.25
property real sizeCheckBox: root.width / 13.5
property real sizeText: root.width / 2.5
property real sizeLevel: root.width / 5.8
property real sizeDesktop: root.width / 5.8
property real sizeVR: root.width / 13.5
}
TabBar {
id: bar
spacing: 0
width: parent.width
height: 42
currentIndex: isVR ? 1 : 0
AudioControls.AudioTabButton {
height: parent.height
text: qsTr("Desktop")
}
AudioControls.AudioTabButton {
height: parent.height
text: qsTr("VR")
}
}
property bool showPeaks: true;
function enablePeakValues() {
Audio.devices.input.peakValuesEnabled = true;
Audio.devices.input.peakValuesEnabledChanged.connect(function(enabled) {
@ -45,6 +79,7 @@ Rectangle {
}
});
}
function disablePeakValues() {
root.showPeaks = false;
Audio.devices.input.peakValuesEnabled = false;
@ -55,29 +90,32 @@ Rectangle {
onVisibleChanged: visible ? enablePeakValues() : disablePeakValues();
Column {
y: 16; // padding does not work
spacing: 16;
spacing: 12;
anchors.top: bar.bottom
anchors.bottom: parent.bottom
anchors.bottomMargin: 5
width: parent.width;
Separator { }
RalewayRegular {
x: 16; // padding does not work
x: margins.paddings + muteMic.boxSize + muteMic.spacing;
size: 16;
color: "white";
text: root.title;
visible: root.showTitle();
text: qsTr("Input Device Settings")
}
Separator { visible: root.showTitle() }
ColumnLayout {
x: 16; // padding does not work
x: margins.paddings;
spacing: 16;
width: parent.width;
// mute is in its own row
RowLayout {
AudioControls.CheckBox {
id: muteMic
text: qsTr("Mute microphone");
spacing: margins.sizeCheckBox - boxSize
isRedCheck: true;
checked: Audio.muted;
onClicked: {
@ -88,8 +126,9 @@ Rectangle {
}
RowLayout {
spacing: 16;
spacing: muteMic.spacing*2; //make it visually distinguish
AudioControls.CheckBox {
spacing: muteMic.spacing
text: qsTr("Enable noise reduction");
checked: Audio.noiseReduction;
onClicked: {
@ -98,24 +137,33 @@ Rectangle {
}
}
AudioControls.CheckBox {
spacing: muteMic.spacing
text: qsTr("Show audio level meter");
checked: AvatarInputs.showAudioTools;
onClicked: {
AvatarInputs.showAudioTools = checked;
checked = Qt.binding(function() { return AvatarInputs.showAudioTools; }); // restore binding
}
onXChanged: rightMostInputLevelPos = x + width
}
}
}
Separator {}
RowLayout {
Item {
x: margins.paddings;
width: parent.width - margins.paddings*2
height: 36
HiFiGlyphs {
width: margins.sizeCheckBox
text: hifi.glyphs.mic;
color: hifi.colors.primaryHighlight;
anchors.left: parent.left
anchors.leftMargin: -size/4 //the glyph has empty space at left about 25%
anchors.verticalCenter: parent.verticalCenter;
size: 28;
size: 30;
}
RalewayRegular {
anchors.verticalCenter: parent.verticalCenter;
@ -126,90 +174,114 @@ Rectangle {
}
ListView {
anchors { left: parent.left; right: parent.right; leftMargin: 70 }
height: 125;
spacing: 0;
id: inputView
width: parent.width - margins.paddings*2
x: margins.paddings
height: Math.min(150, contentHeight);
spacing: 4;
snapMode: ListView.SnapToItem;
clip: true;
model: Audio.devices.input;
delegate: Item {
width: parent.width;
height: 36;
width: rightMostInputLevelPos
height: margins.sizeCheckBox > checkBoxInput.implicitHeight ?
margins.sizeCheckBox : checkBoxInput.implicitHeight
AudioControls.CheckBox {
id: checkbox
anchors.verticalCenter: parent.verticalCenter
id: checkBoxInput
anchors.left: parent.left
text: display;
wrap: false;
checked: selected;
enabled: false;
spacing: margins.sizeCheckBox - boxSize
anchors.verticalCenter: parent.verticalCenter
width: parent.width - inputLevel.width
clip: true
checkable: !checked
checked: bar.currentIndex === 0 ? selectedDesktop : selectedHMD;
boxSize: margins.sizeCheckBox / 2
isRound: true
text: devicename
onPressed: {
if (!checked) {
Audio.setInputDevice(info, bar.currentIndex === 1);
}
}
}
MouseArea {
anchors.fill: checkbox
onClicked: Audio.setInputDevice(info);
}
InputPeak {
id: inputPeak;
visible: Audio.devices.input.peakValuesAvailable;
id: inputLevel
anchors.right: parent.right
peak: model.peak;
anchors.verticalCenter: parent.verticalCenter
anchors.right: parent.right
anchors.rightMargin: 30
visible: (bar.currentIndex === 1 && selectedHMD && isVR) ||
(bar.currentIndex === 0 && selectedDesktop && !isVR) &&
Audio.devices.input.peakValuesAvailable;
}
}
}
Separator {}
RowLayout {
Column {
RowLayout {
HiFiGlyphs {
text: hifi.glyphs.unmuted;
color: hifi.colors.primaryHighlight;
anchors.verticalCenter: parent.verticalCenter;
size: 36;
}
RalewayRegular {
anchors.verticalCenter: parent.verticalCenter;
size: 16;
color: hifi.colors.lightGrayText;
text: qsTr("CHOOSE OUTPUT DEVICE");
}
}
Item {
x: margins.paddings;
width: parent.width - margins.paddings*2
height: 36
PlaySampleSound { anchors { left: parent.left; leftMargin: 60 }}
HiFiGlyphs {
anchors.left: parent.left
anchors.leftMargin: -size/4 //the glyph has empty space at left about 25%
anchors.verticalCenter: parent.verticalCenter;
width: margins.sizeCheckBox
text: hifi.glyphs.unmuted;
color: hifi.colors.primaryHighlight;
size: 36;
}
RalewayRegular {
width: margins.sizeText + margins.sizeLevel
anchors.left: parent.left
anchors.leftMargin: margins.sizeCheckBox
anchors.verticalCenter: parent.verticalCenter;
size: 16;
color: hifi.colors.lightGrayText;
text: qsTr("CHOOSE OUTPUT DEVICE");
}
}
ListView {
anchors { left: parent.left; right: parent.right; leftMargin: 70 }
height: Math.min(250, contentHeight);
spacing: 0;
id: outputView
width: parent.width - margins.paddings*2
x: margins.paddings
height: Math.min(360 - inputView.height, contentHeight);
spacing: 4;
snapMode: ListView.SnapToItem;
clip: true;
model: Audio.devices.output;
delegate: Item {
width: parent.width;
height: 36;
width: rightMostInputLevelPos
height: margins.sizeCheckBox > checkBoxOutput.implicitHeight ?
margins.sizeCheckBox : checkBoxOutput.implicitHeight
AudioControls.CheckBox {
id: checkbox
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
text: display;
checked: selected;
enabled: false;
}
MouseArea {
anchors.fill: checkbox
onClicked: Audio.setOutputDevice(info);
id: checkBoxOutput
width: parent.width
spacing: margins.sizeCheckBox - boxSize
boxSize: margins.sizeCheckBox / 2
isRound: true
checked: bar.currentIndex === 0 ? selectedDesktop : selectedHMD;
checkable: !checked
text: devicename
onPressed: {
if (!checked) {
Audio.setOutputDevice(info, bar.currentIndex === 1);
}
}
}
}
}
PlaySampleSound {
x: margins.paddings
visible: (bar.currentIndex === 1 && isVR) ||
(bar.currentIndex === 0 && !isVR);
anchors { left: parent.left; leftMargin: margins.paddings }
}
}
}

View file

@ -0,0 +1,35 @@
//
// AudioTabButton.qml
// qml/hifi/audio
//
// Created by Vlad Stelmahovsky on 8/16/2017
// Copyright 2017 High Fidelity, Inc.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
import QtQuick 2.7
import QtQuick.Controls 2.2
import "../../controls-uit" as HifiControls
import "../../styles-uit"
TabButton {
id: control
font.pixelSize: height / 2
HifiConstants { id: hifi; }
contentItem: RalewaySemiBold {
text: control.text
font: control.font
color: "white"
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
elide: Text.ElideRight
}
background: Rectangle {
color: control.checked ? hifi.colors.baseGray : "black"
}
}

View file

@ -9,10 +9,10 @@
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
import QtQuick 2.5
import QtQuick 2.7
import "../../controls-uit" as HifiControls
HifiControls.CheckBox {
HifiControls.CheckBoxQQC2 {
color: "white"
}

View file

@ -371,6 +371,17 @@ ScrollingWindow {
colorScheme: hifi.colorSchemes.dark
anchors.left: parent.left
anchors.right: parent.right
TableViewColumn {
role: "display";
}
onActivated: {
var path = scriptsModel.data(index, 0x100)
if (path) {
loadScript(path)
}
}
}
HifiControls.VerticalSpacer {

View file

@ -10,6 +10,7 @@
import QtQuick 2.5
import QtQuick.Controls 1.4
import QtQuick.Controls.Styles 1.4
import QtQuick.Dialogs 1.2 as OriginalDialogs
import Qt.labs.settings 1.0
@ -48,9 +49,15 @@ Rectangle {
isHMD = HMD.active;
ApplicationInterface.uploadRequest.connect(uploadClicked);
assetMappingsModel.errorGettingMappings.connect(handleGetMappingsError);
assetMappingsModel.autoRefreshEnabled = true;
reload();
}
Component.onDestruction: {
assetMappingsModel.autoRefreshEnabled = false;
}
function doDeleteFile(path) {
console.log("Deleting " + path);
@ -105,12 +112,12 @@ Rectangle {
function askForOverwrite(path, callback) {
var object = tabletRoot.messageBox({
icon: hifi.icons.question,
buttons: OriginalDialogs.StandardButton.Yes | OriginalDialogs.StandardButton.No,
defaultButton: OriginalDialogs.StandardButton.No,
title: "Overwrite File",
text: path + "\n" + "This file already exists. Do you want to overwrite it?"
});
icon: hifi.icons.question,
buttons: OriginalDialogs.StandardButton.Yes | OriginalDialogs.StandardButton.No,
defaultButton: OriginalDialogs.StandardButton.No,
title: "Overwrite File",
text: path + "\n" + "This file already exists. Do you want to overwrite it?"
});
object.selected.connect(function(button) {
if (button === OriginalDialogs.StandardButton.Yes) {
callback();
@ -144,14 +151,13 @@ Rectangle {
function reload() {
Assets.mappingModel.refresh();
treeView.selection.clear();
}
function handleGetMappingsError(errorString) {
errorMessageBox(
"There was a problem retreiving the list of assets from your Asset Server.\n"
+ errorString
);
"There was a problem retreiving the list of assets from your Asset Server.\n"
+ errorString
);
}
function addToWorld() {
@ -179,25 +185,25 @@ Rectangle {
var SHAPE_TYPE_DEFAULT = SHAPE_TYPE_STATIC_MESH;
var DYNAMIC_DEFAULT = false;
var prompt = tabletRoot.customInputDialog({
textInput: {
label: "Model URL",
text: defaultURL
},
comboBox: {
label: "Automatic Collisions",
index: SHAPE_TYPE_DEFAULT,
items: SHAPE_TYPES
},
checkBox: {
label: "Dynamic",
checked: DYNAMIC_DEFAULT,
disableForItems: [
SHAPE_TYPE_STATIC_MESH
],
checkStateOnDisable: false,
warningOnDisable: "Models with 'Exact' automatic collisions cannot be dynamic, and should not be used as floors"
}
});
textInput: {
label: "Model URL",
text: defaultURL
},
comboBox: {
label: "Automatic Collisions",
index: SHAPE_TYPE_DEFAULT,
items: SHAPE_TYPES
},
checkBox: {
label: "Dynamic",
checked: DYNAMIC_DEFAULT,
disableForItems: [
SHAPE_TYPE_STATIC_MESH
],
checkStateOnDisable: false,
warningOnDisable: "Models with 'Exact' automatic collisions cannot be dynamic, and should not be used as floors"
}
});
prompt.selected.connect(function (jsonResult) {
if (jsonResult) {
@ -205,23 +211,23 @@ Rectangle {
var url = result.textInput.trim();
var shapeType;
switch (result.comboBox) {
case SHAPE_TYPE_SIMPLE_HULL:
shapeType = "simple-hull";
break;
case SHAPE_TYPE_SIMPLE_COMPOUND:
shapeType = "simple-compound";
break;
case SHAPE_TYPE_STATIC_MESH:
shapeType = "static-mesh";
break;
case SHAPE_TYPE_BOX:
shapeType = "box";
break;
case SHAPE_TYPE_SPHERE:
shapeType = "sphere";
break;
default:
shapeType = "none";
case SHAPE_TYPE_SIMPLE_HULL:
shapeType = "simple-hull";
break;
case SHAPE_TYPE_SIMPLE_COMPOUND:
shapeType = "simple-compound";
break;
case SHAPE_TYPE_STATIC_MESH:
shapeType = "static-mesh";
break;
case SHAPE_TYPE_BOX:
shapeType = "box";
break;
case SHAPE_TYPE_SPHERE:
shapeType = "sphere";
break;
default:
shapeType = "none";
}
var dynamic = result.checkBox !== null ? result.checkBox : DYNAMIC_DEFAULT;
@ -230,7 +236,7 @@ Rectangle {
print("Error: model cannot be both static mesh and dynamic. This should never happen.");
} else if (url) {
var name = assetProxyModel.data(treeView.selection.currentIndex);
var addPosition = Vec3.sum(MyAvatar.position, Vec3.multiply(2, Quat.getFront(MyAvatar.orientation)));
var addPosition = Vec3.sum(MyAvatar.position, Vec3.multiply(2, Quat.getForward(MyAvatar.orientation)));
var gravity;
if (dynamic) {
// Create a vector <0, -10, 0>. { x: 0, y: -10, z: 0 } won't work because Qt is dumb and this is a
@ -293,10 +299,10 @@ Rectangle {
}
var object = tabletRoot.inputDialog({
label: "Enter new path:",
current: path,
placeholderText: "Enter path here"
});
label: "Enter new path:",
current: path,
placeholderText: "Enter path here"
});
object.selected.connect(function(destinationPath) {
destinationPath = destinationPath.trim();
@ -339,12 +345,12 @@ Rectangle {
}
var object = tabletRoot.messageBox({
icon: hifi.icons.question,
buttons: OriginalDialogs.StandardButton.Yes + OriginalDialogs.StandardButton.No,
defaultButton: OriginalDialogs.StandardButton.Yes,
title: "Delete",
text: modalMessage
});
icon: hifi.icons.question,
buttons: OriginalDialogs.StandardButton.Yes + OriginalDialogs.StandardButton.No,
defaultButton: OriginalDialogs.StandardButton.Yes,
title: "Delete",
text: modalMessage
});
object.selected.connect(function(button) {
if (button === OriginalDialogs.StandardButton.Yes) {
doDeleteFile(path);
@ -379,38 +385,38 @@ Rectangle {
var filename = fileUrl.slice(fileUrl.lastIndexOf('/') + 1);
Assets.uploadFile(fileUrl, directory + filename,
function() {
// Upload started
uploadSpinner.visible = true;
uploadButton.enabled = false;
uploadProgressLabel.text = "In progress...";
},
function(err, path) {
print(err, path);
if (err === "") {
uploadProgressLabel.text = "Upload Complete";
timer.interval = 1000;
timer.repeat = false;
timer.triggered.connect(function() {
uploadSpinner.visible = false;
uploadButton.enabled = true;
uploadOpen = false;
});
timer.start();
console.log("Asset Browser - finished uploading: ", fileUrl);
reload();
} else {
uploadSpinner.visible = false;
uploadButton.enabled = true;
uploadOpen = false;
function() {
// Upload started
uploadSpinner.visible = true;
uploadButton.enabled = false;
uploadProgressLabel.text = "In progress...";
},
function(err, path) {
print(err, path);
if (err === "") {
uploadProgressLabel.text = "Upload Complete";
timer.interval = 1000;
timer.repeat = false;
timer.triggered.connect(function() {
uploadSpinner.visible = false;
uploadButton.enabled = true;
uploadOpen = false;
});
timer.start();
console.log("Asset Browser - finished uploading: ", fileUrl);
reload();
} else {
uploadSpinner.visible = false;
uploadButton.enabled = true;
uploadOpen = false;
if (err !== -1) {
console.log("Asset Browser - error uploading: ", fileUrl, " - error ", err);
var box = errorMessageBox("There was an error uploading:\n" + fileUrl + "\n" + err);
box.selected.connect(reload);
}
}
}, dropping);
if (err !== -1) {
console.log("Asset Browser - error uploading: ", fileUrl, " - error ", err);
var box = errorMessageBox("There was an error uploading:\n" + fileUrl + "\n" + err);
box.selected.connect(reload);
}
}
}, dropping);
}
function initiateUpload(url) {
@ -421,9 +427,9 @@ Rectangle {
doUpload(fileUrl, true);
} else {
var browser = tabletRoot.fileDialog({
selectDirectory: false,
dir: currentDirectory
});
selectDirectory: false,
dir: currentDirectory
});
browser.canceled.connect(function() {
uploadOpen = false;
@ -445,11 +451,11 @@ Rectangle {
function errorMessageBox(message) {
return tabletRoot.messageBox({
icon: hifi.icons.warning,
defaultButton: OriginalDialogs.StandardButton.Ok,
title: "Error",
text: message
});
icon: hifi.icons.warning,
defaultButton: OriginalDialogs.StandardButton.Ok,
title: "Error",
text: message
});
}
Column {
@ -469,15 +475,6 @@ Rectangle {
height: 30
spacing: hifi.dimensions.contentSpacing.x
HifiControls.GlyphButton {
glyph: hifi.glyphs.reload
color: hifi.buttons.black
colorScheme: root.colorScheme
width: hifi.dimensions.controlLineHeight
onClicked: root.reload()
}
HifiControls.Button {
text: "Add To World"
color: hifi.buttons.black
@ -511,7 +508,180 @@ Rectangle {
enabled: treeView.selection.hasSelection
}
}
}
HifiControls.Tree {
id: treeView
anchors.margins: hifi.dimensions.contentMargin.x + 2 // Extra for border
anchors.left: parent.left
anchors.right: parent.right
treeModel: assetProxyModel
selectionMode: SelectionMode.ExtendedSelection
headerVisible: true
sortIndicatorVisible: true
colorScheme: root.colorScheme
modifyEl: renameEl
TableViewColumn {
id: nameColumn
title: "Name:"
role: "name"
width: treeView.width - bakedColumn.width;
}
TableViewColumn {
id: bakedColumn
title: "Use Baked?"
role: "baked"
width: 100
}
itemDelegate: Loader {
id: itemDelegateLoader
anchors {
left: parent ? parent.left : undefined
leftMargin: (styleData.column === 0 ? (2 + styleData.depth) : 1) * hifi.dimensions.tablePadding
right: parent ? parent.right : undefined
rightMargin: hifi.dimensions.tablePadding
verticalCenter: parent ? parent.verticalCenter : undefined
}
function convertToGlyph(text) {
switch (text) {
case "Not Baked":
return hifi.glyphs.circleSlash;
case "Baked":
return hifi.glyphs.check_2_01;
case "Error":
return hifi.glyphs.alert;
default:
return "";
}
}
function getComponent() {
if ((styleData.column === 0) && styleData.selected) {
return textFieldComponent;
} else if (convertToGlyph(styleData.value) != "") {
return glyphComponent;
} else {
return labelComponent;
}
}
sourceComponent: getComponent()
Component {
id: labelComponent
FiraSansSemiBold {
text: styleData.value
size: hifi.fontSizes.tableText
color: colorScheme == hifi.colorSchemes.light
? (styleData.selected ? hifi.colors.black : hifi.colors.baseGrayHighlight)
: (styleData.selected ? hifi.colors.black : hifi.colors.lightGrayText)
elide: Text.ElideRight
horizontalAlignment: styleData.column === 1 ? TextInput.AlignHCenter : TextInput.AlignLeft
}
}
Component {
id: glyphComponent
HiFiGlyphs {
text: convertToGlyph(styleData.value)
size: hifi.dimensions.frameIconSize
color: colorScheme == hifi.colorSchemes.light
? (styleData.selected ? hifi.colors.black : hifi.colors.baseGrayHighlight)
: (styleData.selected ? hifi.colors.black : hifi.colors.lightGrayText)
elide: Text.ElideRight
horizontalAlignment: TextInput.AlignHCenter
HifiControls.ToolTip {
anchors.fill: parent
visible: styleData.value === "Error"
toolTip: assetProxyModel.data(styleData.index, 0x106)
}
}
}
Component {
id: textFieldComponent
TextField {
id: textField
readOnly: !activeFocus
text: styleData.value
FontLoader { id: firaSansSemiBold; source: "../../fonts/FiraSans-SemiBold.ttf"; }
font.family: firaSansSemiBold.name
font.pixelSize: hifi.fontSizes.textFieldInput
height: hifi.dimensions.tableRowHeight
style: TextFieldStyle {
textColor: readOnly
? hifi.colors.black
: (treeView.isLightColorScheme ? hifi.colors.black : hifi.colors.white)
background: Rectangle {
visible: !readOnly
color: treeView.isLightColorScheme ? hifi.colors.white : hifi.colors.black
border.color: hifi.colors.primaryHighlight
border.width: 1
}
selectedTextColor: hifi.colors.black
selectionColor: hifi.colors.primaryHighlight
padding.left: readOnly ? 0 : hifi.dimensions.textPadding
padding.right: readOnly ? 0 : hifi.dimensions.textPadding
}
validator: RegExpValidator {
regExp: /[^/]+/
}
Keys.onPressed: {
if (event.key == Qt.Key_Escape) {
text = styleData.value;
unfocusHelper.forceActiveFocus();
event.accepted = true;
}
}
onAccepted: {
if (acceptableInput && styleData.selected) {
if (!treeView.modifyEl(treeView.selection.currentIndex, text)) {
text = styleData.value;
}
unfocusHelper.forceActiveFocus();
}
}
onReadOnlyChanged: {
// Have to explicily set keyboardRaised because automatic setting fails because readOnly is true at the time.
keyboardRaised = activeFocus;
}
}
}
}
MouseArea {
propagateComposedEvents: true
anchors.fill: parent
acceptedButtons: Qt.RightButton
onClicked: {
if (!HMD.active) { // Popup only displays properly on desktop
var index = treeView.indexAt(mouse.x, mouse.y);
treeView.selection.setCurrentIndex(index, 0x0002);
contextMenu.currentIndex = index;
contextMenu.popup();
}
}
}
Menu {
id: contextMenu
title: "Edit"
@ -539,39 +709,50 @@ Rectangle {
}
}
}
}
HifiControls.Tree {
id: treeView
height: 290
anchors.leftMargin: hifi.dimensions.contentMargin.x + 2 // Extra for border
anchors.rightMargin: hifi.dimensions.contentMargin.x + 2 // Extra for border
anchors.left: parent.left
anchors.right: parent.right
treeModel: assetProxyModel
canEdit: true
colorScheme: root.colorScheme
selectionMode: SelectionMode.ExtendedSelection
Row {
id: infoRow
anchors.left: treeView.left
anchors.right: treeView.right
anchors.bottomMargin: hifi.dimensions.contentSpacing.y
spacing: hifi.dimensions.contentSpacing.x
RalewayRegular {
size: hifi.fontSizes.sectionName
font.capitalization: Font.AllUppercase
text: selectedItems + " items selected"
color: hifi.colors.lightGrayText
}
modifyEl: renameEl
HifiControls.CheckBox {
function isChecked() {
var status = assetProxyModel.data(treeView.selection.currentIndex, 0x105);
var bakingDisabled = (status === "Not Baked" || status === "--");
return selectedItems === 1 && !bakingDisabled;
}
MouseArea {
propagateComposedEvents: true
anchors.fill: parent
acceptedButtons: Qt.RightButton
text: "Use baked (optimized) versions"
colorScheme: root.colorScheme
enabled: selectedItems === 1 && assetProxyModel.data(treeView.selection.currentIndex, 0x105) !== "--"
checked: isChecked()
onClicked: {
if (!HMD.active) { // Popup only displays properly on desktop
var index = treeView.indexAt(mouse.x, mouse.y);
treeView.selection.setCurrentIndex(index, 0x0002);
contextMenu.currentIndex = index;
contextMenu.popup();
var mappings = [];
for (var i in treeView.selection.selectedIndexes) {
var index = treeView.selection.selectedIndexes[i];
var path = assetProxyModel.data(index, 0x100);
mappings.push(path);
}
print("Setting baking enabled:" + mappings + " " + checked);
Assets.setBakingEnabled(mappings, checked, function() {
reload();
});
checked = Qt.binding(isChecked);
}
}
}
HifiControls.TabletContentSection {
id: uploadSection
name: "Upload A File"

View file

@ -400,6 +400,17 @@ Rectangle {
colorScheme: hifi.colorSchemes.dark
anchors.left: parent.left
anchors.right: parent.right
TableViewColumn {
role: "display";
}
onActivated: {
var path = scriptsModel.data(index, 0x100)
if (path) {
loadScript(path)
}
}
}
HifiControls.VerticalSpacer {

View file

@ -21,9 +21,9 @@ Item {
property alias text: label.text
property var source
implicitHeight: source.visible ? 2 * label.implicitHeight : 0
implicitHeight: source !== null ? source.visible ? 2 * label.implicitHeight : 0 : 0
implicitWidth: 2 * hifi.dimensions.menuPadding.x + check.width + label.width + tail.width
visible: source.visible
visible: source !== null ? source.visible : false
width: parent.width
Item {
@ -42,7 +42,9 @@ Item {
id: checkbox
// FIXME: Should use radio buttons if source.exclusiveGroup.
width: 20
visible: source.visible && source.type === 1 && source.checkable && !source.exclusiveGroup
visible: source !== null ?
source.visible && source.type === 1 && source.checkable && !source.exclusiveGroup :
false
checked: setChecked()
function setChecked() {
if (!source || source.type !== 1 || !source.checkable) {
@ -58,7 +60,9 @@ Item {
id: radiobutton
// FIXME: Should use radio buttons if source.exclusiveGroup.
width: 20
visible: source.visible && source.type === 1 && source.checkable && source.exclusiveGroup
visible: source !== null ?
source.visible && source.type === 1 && source.checkable && source.exclusiveGroup :
false
checked: setChecked()
function setChecked() {
if (!source || source.type !== 1 || !source.checkable) {
@ -80,9 +84,13 @@ Item {
anchors.left: check.right
anchors.verticalCenter: parent.verticalCenter
verticalAlignment: Text.AlignVCenter
color: source.enabled ? hifi.colors.baseGrayShadow : hifi.colors.baseGrayShadow50
enabled: source.visible && (source.type !== 0 ? source.enabled : false)
visible: source.visible
color: source !== null ?
source.enabled ? hifi.colors.baseGrayShadow :
hifi.colors.baseGrayShadow50 :
"transparent"
enabled: source !== null ? source.visible && (source.type !== 0 ? source.enabled : false) : false
visible: source !== null ? source.visible : false
wrapMode: Text.WordWrap
}
@ -93,7 +101,7 @@ Item {
leftMargin: hifi.dimensions.menuPadding.x + check.width
rightMargin: hifi.dimensions.menuPadding.x + tail.width
}
visible: source.type === MenuItemType.Separator
visible: source !== null ? source.type === MenuItemType.Separator : false
Rectangle {
anchors {
@ -117,23 +125,23 @@ Item {
RalewayLight {
id: shortcut
text: source.shortcut ? source.shortcut : ""
text: source !== null ? source.shortcut ? source.shortcut : "" : ""
size: hifi.fontSizes.shortcutText
color: hifi.colors.baseGrayShadow
anchors.verticalCenter: parent.verticalCenter
anchors.right: parent.right
anchors.rightMargin: 15
visible: source.visible && text != ""
visible: source !== null ? source.visible && text != "" : false
}
HiFiGlyphs {
text: hifi.glyphs.disclosureExpand
color: source.enabled ? hifi.colors.baseGrayShadow : hifi.colors.baseGrayShadow25
color: source !== null ? source.enabled ? hifi.colors.baseGrayShadow : hifi.colors.baseGrayShadow25 : "transparent"
size: 70
anchors.verticalCenter: parent.verticalCenter
anchors.right: parent.right
horizontalAlignment: Text.AlignRight
visible: source.visible && (source.type === 2)
visible: source !== null ? source.visible && (source.type === 2) : false
}
}
}

View file

@ -70,7 +70,6 @@ Item {
for (var i = 0; i < items.length; ++i) {
var item = items[i];
if (!item.visible) continue;
switch (item.type) {
case MenuItemType.Menu:
result.append({"name": item.title, "item": item})
@ -216,5 +215,4 @@ Item {
function nextItem() { d.topMenu.nextItem(); }
function selectCurrentItem() { d.topMenu.selectCurrentItem(); }
function previousPage() { d.topMenu.previousPage(); }
}

View file

@ -9,8 +9,6 @@
//
import QtQuick 2.5
import QtQuick.Controls 1.4
import QtQuick.Controls.Styles 1.4
import "../../styles-uit"
import "."
@ -36,7 +34,6 @@ FocusScope {
//color: isSubMenu ? hifi.colors.faintGray : hifi.colors.faintGray80
}
ListView {
id: listView
x: 0
@ -68,8 +65,8 @@ FocusScope {
delegate: TabletMenuItem {
text: name
source: item
onImplicitHeightChanged: listView.recalcSize()
onImplicitWidthChanged: listView.recalcSize()
onImplicitHeightChanged: listView !== null ? listView.recalcSize() : 0
onImplicitWidthChanged: listView !== null ? listView.recalcSize() : 0
MouseArea {
anchors.fill: parent
@ -124,8 +121,6 @@ FocusScope {
function nextItem() { listView.currentIndex = (listView.currentIndex + listView.count + 1) % listView.count; }
function selectCurrentItem() { if (listView.currentIndex != -1) root.selected(currentItem.source); }
function previousPage() { root.parent.pop(); }
}

View file

@ -9,8 +9,6 @@
//
import QtQuick 2.5
import QtQuick.Controls 1.4
import QtQuick.Controls.Styles 1.4
Text {
id: root

View file

@ -338,5 +338,6 @@ Item {
readonly property string stop_square: "\ue01e"
readonly property string avatarTPose: "\ue01f"
readonly property string lock: "\ue006"
readonly property string check_2_01: "\ue020"
}
}

View file

@ -980,7 +980,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo
connect(scriptEngines, &ScriptEngines::scriptLoadError,
scriptEngines, [](const QString& filename, const QString& error){
OffscreenUi::warning(nullptr, "Error Loading Script", filename + " failed to load.");
OffscreenUi::asyncWarning(nullptr, "Error Loading Script", filename + " failed to load.");
}, Qt::QueuedConnection);
#ifdef _WIN32
@ -1845,7 +1845,7 @@ void Application::domainConnectionRefused(const QString& reasonMessage, int reas
case DomainHandler::ConnectionRefusedReason::Unknown: {
QString message = "Unable to connect to the location you are visiting.\n";
message += reasonMessage;
OffscreenUi::warning("", message);
OffscreenUi::asyncWarning("", message);
break;
}
default:
@ -2057,6 +2057,7 @@ void Application::cleanupBeforeQuit() {
// this must happen after QML, as there are unexplained audio crashes originating in qtwebengine
DependencyManager::destroy<AudioClient>();
DependencyManager::destroy<AudioInjectorManager>();
DependencyManager::destroy<AudioScriptingInterface>();
qCDebug(interfaceapp) << "Application::cleanupBeforeQuit() complete";
}
@ -2580,7 +2581,6 @@ void Application::paintGL() {
// scale IPD by sensorToWorldScale, to make the world seem larger or smaller accordingly.
ipdScale *= sensorToWorldScale;
mat4 eyeProjections[2];
{
PROFILE_RANGE(render, "/mainRender");
PerformanceTimer perfTimer("mainRender");
@ -2637,17 +2637,8 @@ void Application::paintGL() {
PerformanceTimer perfTimer("postComposite");
renderArgs._batch = &postCompositeBatch;
renderArgs._batch->setViewportTransform(ivec4(0, 0, finalFramebufferSize.width(), finalFramebufferSize.height()));
for_each_eye([&](Eye eye) {
// apply eye offset and IPD scale to the view matrix
mat4 eyeToHead = displayPlugin->getEyeToHeadTransform(eye);
vec3 eyeOffset = glm::vec3(eyeToHead[3]);
mat4 eyeOffsetTransform = glm::translate(mat4(), eyeOffset * -1.0f * ipdScale);
renderArgs._batch->setViewTransform(renderArgs.getViewFrustum().getView() * eyeOffsetTransform);
renderArgs._batch->setProjectionTransform(eyeProjections[eye]);
_overlays.render3DHUDOverlays(&renderArgs);
});
renderArgs._batch->setViewTransform(renderArgs.getViewFrustum().getView());
_overlays.render3DHUDOverlays(&renderArgs);
}
auto frame = _gpuContext->endFrame();
@ -3750,7 +3741,7 @@ bool Application::acceptSnapshot(const QString& urlString) {
DependencyManager::get<AddressManager>()->handleLookupString(snapshotData->getURL().toString());
}
} else {
OffscreenUi::warning("", "No location details were found in the file\n" +
OffscreenUi::asyncWarning("", "No location details were found in the file\n" +
snapshotPath + "\nTry dragging in an authentic Hifi snapshot.");
}
return true;
@ -5159,12 +5150,6 @@ void Application::update(float deltaTime) {
}
}
{
PROFILE_RANGE_EX(app, "Overlays", 0xffff0000, (uint64_t)getActiveDisplayPlugin()->presentCount());
PerformanceTimer perfTimer("overlays");
_overlays.update(deltaTime);
}
{
PROFILE_RANGE(app, "RayPickManager");
_rayPickManager.update();
@ -5175,6 +5160,12 @@ void Application::update(float deltaTime) {
_laserPointerManager.update();
}
{
PROFILE_RANGE_EX(app, "Overlays", 0xffff0000, (uint64_t)getActiveDisplayPlugin()->presentCount());
PerformanceTimer perfTimer("overlays");
_overlays.update(deltaTime);
}
// Update _viewFrustum with latest camera and view frustum data...
// NOTE: we get this from the view frustum, to make it simpler, since the
// loadViewFrumstum() method will get the correct details from the camera
@ -6150,7 +6141,7 @@ void Application::setSessionUUID(const QUuid& sessionUUID) const {
bool Application::askToSetAvatarUrl(const QString& url) {
QUrl realUrl(url);
if (realUrl.isLocalFile()) {
OffscreenUi::warning("", "You can not use local files for avatar components.");
OffscreenUi::asyncWarning("", "You can not use local files for avatar components.");
return false;
}
@ -6162,41 +6153,55 @@ bool Application::askToSetAvatarUrl(const QString& url) {
QString modelName = fstMapping["name"].toString();
QString modelLicense = fstMapping["license"].toString();
bool agreeToLicence = true; // assume true
bool agreeToLicense = true; // assume true
//create set avatar callback
auto setAvatar = [=] (QString url, QString modelName) {
ModalDialogListener* dlg = OffscreenUi::asyncQuestion("Set Avatar",
"Would you like to use '" + modelName + "' for your avatar?",
QMessageBox::Ok | QMessageBox::Cancel, QMessageBox::Ok);
QObject::connect(dlg, &ModalDialogListener::response, this, [=] (QVariant answer) {
QObject::disconnect(dlg, &ModalDialogListener::response, this, nullptr);
bool ok = (QMessageBox::Ok == static_cast<QMessageBox::StandardButton>(answer.toInt()));
if (ok) {
getMyAvatar()->useFullAvatarURL(url, modelName);
emit fullAvatarURLChanged(url, modelName);
} else {
qCDebug(interfaceapp) << "Declined to use the avatar: " << url;
}
});
};
if (!modelLicense.isEmpty()) {
// word wrap the licence text to fit in a reasonable shaped message box.
// word wrap the license text to fit in a reasonable shaped message box.
const int MAX_CHARACTERS_PER_LINE = 90;
modelLicense = simpleWordWrap(modelLicense, MAX_CHARACTERS_PER_LINE);
agreeToLicence = QMessageBox::Yes == OffscreenUi::question("Avatar Usage License",
modelLicense + "\nDo you agree to these terms?",
QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes);
}
ModalDialogListener* dlg = OffscreenUi::asyncQuestion("Avatar Usage License",
modelLicense + "\nDo you agree to these terms?",
QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes);
QObject::connect(dlg, &ModalDialogListener::response, this, [=, &agreeToLicense] (QVariant answer) {
QObject::disconnect(dlg, &ModalDialogListener::response, this, nullptr);
bool ok = false;
agreeToLicense = (static_cast<QMessageBox::StandardButton>(answer.toInt()) == QMessageBox::Yes);
if (agreeToLicense) {
switch (modelType) {
case FSTReader::HEAD_AND_BODY_MODEL: {
setAvatar(url, modelName);
break;
}
default:
OffscreenUi::asyncWarning("", modelName + "Does not support a head and body as required.");
break;
}
} else {
qCDebug(interfaceapp) << "Declined to agree to avatar license: " << url;
}
if (!agreeToLicence) {
qCDebug(interfaceapp) << "Declined to agree to avatar license: " << url;
//auto offscreenUi = DependencyManager::get<OffscreenUi>();
});
} else {
switch (modelType) {
case FSTReader::HEAD_AND_BODY_MODEL:
ok = QMessageBox::Ok == OffscreenUi::question("Set Avatar",
"Would you like to use '" + modelName + "' for your avatar?",
QMessageBox::Ok | QMessageBox::Cancel, QMessageBox::Ok);
break;
default:
OffscreenUi::warning("", modelName + "Does not support a head and body as required.");
break;
}
}
if (ok) {
getMyAvatar()->useFullAvatarURL(url, modelName);
emit fullAvatarURLChanged(url, modelName);
} else {
qCDebug(interfaceapp) << "Declined to use the avatar: " << url;
setAvatar(url, modelName);
}
return true;
@ -6204,8 +6209,6 @@ bool Application::askToSetAvatarUrl(const QString& url) {
bool Application::askToLoadScript(const QString& scriptFilenameOrURL) {
QMessageBox::StandardButton reply;
QString shortName = scriptFilenameOrURL;
QUrl scriptURL { scriptFilenameOrURL };
@ -6217,15 +6220,20 @@ bool Application::askToLoadScript(const QString& scriptFilenameOrURL) {
}
QString message = "Would you like to run this script:\n" + shortName;
ModalDialogListener* dlg = OffscreenUi::asyncQuestion(getWindow(), "Run Script", message,
QMessageBox::Yes | QMessageBox::No);
reply = OffscreenUi::question(getWindow(), "Run Script", message, QMessageBox::Yes | QMessageBox::No);
QObject::connect(dlg, &ModalDialogListener::response, this, [=] (QVariant answer) {
const QString& fileName = scriptFilenameOrURL;
if (static_cast<QMessageBox::StandardButton>(answer.toInt()) == QMessageBox::Yes) {
qCDebug(interfaceapp) << "Chose to run the script: " << fileName;
DependencyManager::get<ScriptEngines>()->loadScript(fileName);
} else {
qCDebug(interfaceapp) << "Declined to run the script: " << scriptFilenameOrURL;
}
QObject::disconnect(dlg, &ModalDialogListener::response, this, nullptr);
});
if (reply == QMessageBox::Yes) {
qCDebug(interfaceapp) << "Chose to run the script: " << scriptFilenameOrURL;
DependencyManager::get<ScriptEngines>()->loadScript(scriptFilenameOrURL);
} else {
qCDebug(interfaceapp) << "Declined to run the script: " << scriptFilenameOrURL;
}
return true;
}
@ -6262,22 +6270,26 @@ bool Application::askToWearAvatarAttachmentUrl(const QString& url) {
name = nameValue.toString();
}
// display confirmation dialog
if (displayAvatarAttachmentConfirmationDialog(name)) {
// add attachment to avatar
auto myAvatar = getMyAvatar();
assert(myAvatar);
auto attachmentDataVec = myAvatar->getAttachmentData();
AttachmentData attachmentData;
attachmentData.fromJson(jsonObject);
attachmentDataVec.push_back(attachmentData);
myAvatar->setAttachmentData(attachmentDataVec);
} else {
qCDebug(interfaceapp) << "User declined to wear the avatar attachment: " << url;
}
auto avatarAttachmentConfirmationTitle = tr("Avatar Attachment Confirmation");
auto avatarAttachmentConfirmationMessage = tr("Would you like to wear '%1' on your avatar?").arg(name);
ModalDialogListener* dlg = OffscreenUi::asyncQuestion(avatarAttachmentConfirmationTitle,
avatarAttachmentConfirmationMessage,
QMessageBox::Ok | QMessageBox::Cancel);
QObject::connect(dlg, &ModalDialogListener::response, this, [=] (QVariant answer) {
QObject::disconnect(dlg, &ModalDialogListener::response, this, nullptr);
if (static_cast<QMessageBox::StandardButton>(answer.toInt()) == QMessageBox::Yes) {
// add attachment to avatar
auto myAvatar = getMyAvatar();
assert(myAvatar);
auto attachmentDataVec = myAvatar->getAttachmentData();
AttachmentData attachmentData;
attachmentData.fromJson(jsonObject);
attachmentDataVec.push_back(attachmentData);
myAvatar->setAttachmentData(attachmentDataVec);
} else {
qCDebug(interfaceapp) << "User declined to wear the avatar attachment: " << url;
}
});
} else {
// json parse error
auto avatarAttachmentParseErrorString = tr("Error parsing attachment JSON from url: \"%1\"");
@ -6350,20 +6362,7 @@ bool Application::askToReplaceDomainContent(const QString& url) {
void Application::displayAvatarAttachmentWarning(const QString& message) const {
auto avatarAttachmentWarningTitle = tr("Avatar Attachment Failure");
OffscreenUi::warning(avatarAttachmentWarningTitle, message);
}
bool Application::displayAvatarAttachmentConfirmationDialog(const QString& name) const {
auto avatarAttachmentConfirmationTitle = tr("Avatar Attachment Confirmation");
auto avatarAttachmentConfirmationMessage = tr("Would you like to wear '%1' on your avatar?").arg(name);
auto reply = OffscreenUi::question(avatarAttachmentConfirmationTitle,
avatarAttachmentConfirmationMessage,
QMessageBox::Ok | QMessageBox::Cancel);
if (QMessageBox::Ok == reply) {
return true;
} else {
return false;
}
OffscreenUi::asyncWarning(avatarAttachmentWarningTitle, message);
}
void Application::showDialog(const QUrl& widgetUrl, const QUrl& tabletUrl, const QString& name) const {
@ -6685,7 +6684,7 @@ void Application::addAssetToWorldCheckModelSize() {
if (dimensions != DEFAULT_DIMENSIONS) {
// Scale model so that its maximum is exactly specific size.
const float MAXIMUM_DIMENSION = 1.0f * getMyAvatar()->getSensorToWorldScale();
const float MAXIMUM_DIMENSION = getMyAvatar()->getSensorToWorldScale();
auto previousDimensions = dimensions;
auto scale = std::min(MAXIMUM_DIMENSION / dimensions.x, std::min(MAXIMUM_DIMENSION / dimensions.y,
MAXIMUM_DIMENSION / dimensions.z));
@ -6944,12 +6943,17 @@ void Application::openUrl(const QUrl& url) const {
void Application::loadDialog() {
auto scriptEngines = DependencyManager::get<ScriptEngines>();
QString fileNameString = OffscreenUi::getOpenFileName(
_glWidget, tr("Open Script"), getPreviousScriptLocation(), tr("JavaScript Files (*.js)"));
if (!fileNameString.isEmpty() && QFile(fileNameString).exists()) {
setPreviousScriptLocation(QFileInfo(fileNameString).absolutePath());
DependencyManager::get<ScriptEngines>()->loadScript(fileNameString, true, false, false, true); // Don't load from cache
}
ModalDialogListener* dlg = OffscreenUi::getOpenFileNameAsync(_glWidget, tr("Open Script"),
getPreviousScriptLocation(),
tr("JavaScript Files (*.js)"));
connect(dlg, &ModalDialogListener::response, this, [=] (QVariant answer) {
disconnect(dlg, &ModalDialogListener::response, this, nullptr);
const QString& response = answer.toString();
if (!response.isEmpty() && QFile(response).exists()) {
setPreviousScriptLocation(QFileInfo(response).absolutePath());
DependencyManager::get<ScriptEngines>()->loadScript(response, true, false, false, true); // Don't load from cache
}
});
}
QString Application::getPreviousScriptLocation() {
@ -6962,12 +6966,16 @@ void Application::setPreviousScriptLocation(const QString& location) {
}
void Application::loadScriptURLDialog() const {
QString newScript = OffscreenUi::getText(OffscreenUi::ICON_NONE, "Open and Run Script", "Script URL");
if (QUrl(newScript).scheme() == "atp") {
OffscreenUi::warning("Error Loading Script", "Cannot load client script over ATP");
} else if (!newScript.isEmpty()) {
DependencyManager::get<ScriptEngines>()->loadScript(newScript.trimmed());
}
ModalDialogListener* dlg = OffscreenUi::getTextAsync(OffscreenUi::ICON_NONE, "Open and Run Script", "Script URL");
connect(dlg, &ModalDialogListener::response, this, [=] (QVariant response) {
disconnect(dlg, &ModalDialogListener::response, this, nullptr);
const QString& newScript = response.toString();
if (QUrl(newScript).scheme() == "atp") {
OffscreenUi::asyncWarning("Error Loading Script", "Cannot load client script over ATP");
} else if (!newScript.isEmpty()) {
DependencyManager::get<ScriptEngines>()->loadScript(newScript.trimmed());
}
});
}
void Application::loadLODToolsDialog() {
@ -7084,7 +7092,7 @@ void Application::notifyPacketVersionMismatch() {
QString message = "The location you are visiting is running an incompatible server version.\n";
message += "Content may not display properly.";
OffscreenUi::warning("", message);
OffscreenUi::asyncWarning("", message);
}
}
@ -7093,7 +7101,7 @@ void Application::checkSkeleton() const {
qCDebug(interfaceapp) << "MyAvatar model has no skeleton";
QString message = "Your selected avatar body has no skeleton.\n\nThe default body will be loaded...";
OffscreenUi::warning("", message);
OffscreenUi::asyncWarning("", message);
getMyAvatar()->useFullAvatarURL(AvatarData::defaultFullAvatarModelUrl(), DEFAULT_FULL_AVATAR_MODEL_NAME);
} else {

View file

@ -429,7 +429,6 @@ private slots:
bool askToWearAvatarAttachmentUrl(const QString& url);
void displayAvatarAttachmentWarning(const QString& message) const;
bool displayAvatarAttachmentConfirmationDialog(const QString& name) const;
bool askToReplaceDomainContent(const QString& url);

View file

@ -106,30 +106,30 @@ void AvatarBookmarks::changeToBookmarkedAvatar() {
}
void AvatarBookmarks::addBookmark() {
bool ok = false;
auto bookmarkName = OffscreenUi::getText(OffscreenUi::ICON_PLACEMARK, "Bookmark Avatar", "Name", QString(), &ok);
if (!ok) {
return;
}
ModalDialogListener* dlg = OffscreenUi::getTextAsync(OffscreenUi::ICON_PLACEMARK, "Bookmark Avatar", "Name", QString());
connect(dlg, &ModalDialogListener::response, this, [=] (QVariant response) {
disconnect(dlg, &ModalDialogListener::response, this, nullptr);
auto bookmarkName = response.toString();
bookmarkName = bookmarkName.trimmed().replace(QRegExp("(\r\n|[\r\n\t\v ])+"), " ");
if (bookmarkName.length() == 0) {
return;
}
bookmarkName = bookmarkName.trimmed().replace(QRegExp("(\r\n|[\r\n\t\v ])+"), " ");
if (bookmarkName.length() == 0) {
return;
}
auto myAvatar = DependencyManager::get<AvatarManager>()->getMyAvatar();
auto myAvatar = DependencyManager::get<AvatarManager>()->getMyAvatar();
const QString& avatarUrl = myAvatar->getSkeletonModelURL().toString();
const QVariant& avatarScale = myAvatar->getAvatarScale();
const QString& avatarUrl = myAvatar->getSkeletonModelURL().toString();
const QVariant& avatarScale = myAvatar->getAvatarScale();
// If Avatar attachments ever change, this is where to update them, when saving remember to also append to AVATAR_BOOKMARK_VERSION
QVariantMap *bookmark = new QVariantMap;
bookmark->insert(ENTRY_VERSION, AVATAR_BOOKMARK_VERSION);
bookmark->insert(ENTRY_AVATAR_URL, avatarUrl);
bookmark->insert(ENTRY_AVATAR_SCALE, avatarScale);
bookmark->insert(ENTRY_AVATAR_ATTACHMENTS, myAvatar->getAttachmentsVariant());
Bookmarks::addBookmarkToFile(bookmarkName, *bookmark);
});
// If Avatar attachments ever change, this is where to update them, when saving remember to also append to AVATAR_BOOKMARK_VERSION
QVariantMap *bookmark = new QVariantMap;
bookmark->insert(ENTRY_VERSION, AVATAR_BOOKMARK_VERSION);
bookmark->insert(ENTRY_AVATAR_URL, avatarUrl);
bookmark->insert(ENTRY_AVATAR_SCALE, avatarScale);
bookmark->insert(ENTRY_AVATAR_ATTACHMENTS, myAvatar->getAttachmentsVariant());
Bookmarks::addBookmarkToFile(bookmarkName, *bookmark);
}
void AvatarBookmarks::addBookmarkToMenu(Menu* menubar, const QString& name, const QVariant& bookmark) {

View file

@ -58,20 +58,25 @@ void Bookmarks::addBookmarkToFile(const QString& bookmarkName, const QVariant& b
Menu* menubar = Menu::getInstance();
if (contains(bookmarkName)) {
auto offscreenUi = DependencyManager::get<OffscreenUi>();
auto duplicateBookmarkMessage = offscreenUi->createMessageBox(OffscreenUi::ICON_WARNING, "Duplicate Bookmark",
"The bookmark name you entered already exists in your list.",
QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes);
duplicateBookmarkMessage->setProperty("informativeText", "Would you like to overwrite it?");
auto result = offscreenUi->waitForMessageBoxResult(duplicateBookmarkMessage);
if (result != QMessageBox::Yes) {
return;
}
removeBookmarkFromMenu(menubar, bookmarkName);
}
ModalDialogListener* dlg = OffscreenUi::asyncWarning("Duplicate Bookmark",
"The bookmark name you entered already exists in your list.",
QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes);
dlg->setProperty("informativeText", "Would you like to overwrite it?");
QObject::connect(dlg, &ModalDialogListener::response, this, [=] (QVariant answer) {
QObject::disconnect(dlg, &ModalDialogListener::response, this, nullptr);
addBookmarkToMenu(menubar, bookmarkName, bookmark);
insert(bookmarkName, bookmark); // Overwrites any item with the same bookmarkName.
enableMenuItems(true);
if (QMessageBox::Yes == static_cast<QMessageBox::StandardButton>(answer.toInt())) {
removeBookmarkFromMenu(menubar, bookmarkName);
addBookmarkToMenu(menubar, bookmarkName, bookmark);
insert(bookmarkName, bookmark); // Overwrites any item with the same bookmarkName.
enableMenuItems(true);
}
});
} else {
addBookmarkToMenu(menubar, bookmarkName, bookmark);
insert(bookmarkName, bookmark); // Overwrites any item with the same bookmarkName.
enableMenuItems(true);
}
}
void Bookmarks::insert(const QString& name, const QVariant& bookmark) {

View file

@ -74,20 +74,21 @@ void LocationBookmarks::teleportToBookmark() {
}
void LocationBookmarks::addBookmark() {
bool ok = false;
auto bookmarkName = OffscreenUi::getText(OffscreenUi::ICON_PLACEMARK, "Bookmark Location", "Name", QString(), &ok);
if (!ok) {
return;
}
ModalDialogListener* dlg = OffscreenUi::getTextAsync(OffscreenUi::ICON_PLACEMARK, "Bookmark Location", "Name", QString());
bookmarkName = bookmarkName.trimmed().replace(QRegExp("(\r\n|[\r\n\t\v ])+"), " ");
if (bookmarkName.length() == 0) {
return;
}
connect(dlg, &ModalDialogListener::response, this, [=] (QVariant response) {
disconnect(dlg, &ModalDialogListener::response, this, nullptr);
auto bookmarkName = response.toString();
auto addressManager = DependencyManager::get<AddressManager>();
QString bookmarkAddress = addressManager->currentAddress().toString();
Bookmarks::addBookmarkToFile(bookmarkName, bookmarkAddress);
bookmarkName = bookmarkName.trimmed().replace(QRegExp("(\r\n|[\r\n\t\v ])+"), " ");
if (bookmarkName.length() == 0) {
return;
}
auto addressManager = DependencyManager::get<AddressManager>();
QString bookmarkAddress = addressManager->currentAddress().toString();
Bookmarks::addBookmarkToFile(bookmarkName, bookmarkAddress);
});
}
void LocationBookmarks::addBookmarkToMenu(Menu* menubar, const QString& name, const QVariant& address) {
@ -101,4 +102,4 @@ void LocationBookmarks::addBookmarkToMenu(Menu* menubar, const QString& name, co
menubar->addActionToQMenuAndActionHash(_bookmarksMenu, teleportAction, name, 0, QAction::NoRole);
Bookmarks::sortActions(menubar, _bookmarksMenu);
}
}
}

View file

@ -367,6 +367,20 @@ Menu::Menu() {
QString("../../hifi/tablet/TabletGraphicsPreferences.qml"), "GraphicsPreferencesDialog");
});
// Developer > UI >>>
MenuWrapper* uiOptionsMenu = developerMenu->addMenu("UI");
action = addCheckableActionToQMenuAndActionHash(uiOptionsMenu, MenuOption::DesktopTabletToToolbar, 0,
qApp->getDesktopTabletBecomesToolbarSetting());
connect(action, &QAction::triggered, [action] {
qApp->setDesktopTabletBecomesToolbarSetting(action->isChecked());
});
action = addCheckableActionToQMenuAndActionHash(uiOptionsMenu, MenuOption::HMDTabletToToolbar, 0,
qApp->getHmdTabletBecomesToolbarSetting());
connect(action, &QAction::triggered, [action] {
qApp->setHmdTabletBecomesToolbarSetting(action->isChecked());
});
// Developer > Render >>>
MenuWrapper* renderOptionsMenu = developerMenu->addMenu("Render");
addCheckableActionToQMenuAndActionHash(renderOptionsMenu, MenuOption::WorldAxes);

View file

@ -200,6 +200,8 @@ namespace MenuOption {
const QString VisibleToFriends = "Friends";
const QString VisibleToNoOne = "No one";
const QString WorldAxes = "World Axes";
const QString DesktopTabletToToolbar = "Desktop Tablet Becomes Toolbar";
const QString HMDTabletToToolbar = "HMD Tablet Becomes Toolbar";
}
#endif // hifi_Menu_h

View file

@ -79,7 +79,7 @@ bool ModelPackager::loadModel() {
if (_modelFile.completeSuffix().contains("fst")) {
QFile fst(_modelFile.filePath());
if (!fst.open(QFile::ReadOnly | QFile::Text)) {
OffscreenUi::warning(NULL,
OffscreenUi::asyncWarning(NULL,
QString("ModelPackager::loadModel()"),
QString("Could not open FST file %1").arg(_modelFile.filePath()),
QMessageBox::Ok);
@ -98,7 +98,7 @@ bool ModelPackager::loadModel() {
// open the fbx file
QFile fbx(_fbxInfo.filePath());
if (!_fbxInfo.exists() || !_fbxInfo.isFile() || !fbx.open(QIODevice::ReadOnly)) {
OffscreenUi::warning(NULL,
OffscreenUi::asyncWarning(NULL,
QString("ModelPackager::loadModel()"),
QString("Could not open FBX file %1").arg(_fbxInfo.filePath()),
QMessageBox::Ok);
@ -408,7 +408,7 @@ bool ModelPackager::copyTextures(const QString& oldDir, const QDir& newDir) {
}
if (!errors.isEmpty()) {
OffscreenUi::warning(nullptr, "ModelPackager::copyTextures()",
OffscreenUi::asyncWarning(nullptr, "ModelPackager::copyTextures()",
"Missing textures:" + errors);
qCDebug(interfaceapp) << "ModelPackager::copyTextures():" << errors;
return false;

View file

@ -201,7 +201,7 @@ void ModelPropertiesDialog::chooseTextureDirectory() {
return;
}
if (!directory.startsWith(_basePath)) {
OffscreenUi::warning(NULL, "Invalid texture directory", "Texture directory must be child of base path.");
OffscreenUi::asyncWarning(NULL, "Invalid texture directory", "Texture directory must be child of base path.");
return;
}
_textureDirectory->setText(directory.length() == _basePath.length() ? "." : directory.mid(_basePath.length() + 1));

View file

@ -42,117 +42,161 @@ static const QString COMPOUND_SHAPE_URL_KEY = "compoundShapeURL";
static const QString MESSAGE_BOX_TITLE = "ATP Asset Migration";
void ATPAssetMigrator::loadEntityServerFile() {
auto filename = OffscreenUi::getOpenFileName(_dialogParent, tr("Select an entity-server content file to migrate"), QString(), tr("Entity-Server Content (*.gz)"));
if (!filename.isEmpty()) {
qCDebug(asset_migrator) << "Selected filename for ATP asset migration: " << filename;
static const QString MIGRATION_CONFIRMATION_TEXT {
"The ATP Asset Migration process will scan the selected entity-server file,\nupload discovered resources to the"\
" current asset-server\nand then save a new entity-server file with the ATP URLs.\n\nAre you ready to"\
" continue?\n\nMake sure you are connected to the right domain."
};
auto button = OffscreenUi::question(_dialogParent, MESSAGE_BOX_TITLE, MIGRATION_CONFIRMATION_TEXT,
QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes);
if (button == QMessageBox::No) {
return;
}
// try to open the file at the given filename
QFile modelsFile { filename };
if (modelsFile.open(QIODevice::ReadOnly)) {
QByteArray compressedJsonData = modelsFile.readAll();
QByteArray jsonData;
if (!gunzip(compressedJsonData, jsonData)) {
OffscreenUi::warning(_dialogParent, "Error", "The file at" + filename + "was not in gzip format.");
}
QJsonDocument modelsJSON = QJsonDocument::fromJson(jsonData);
_entitiesArray = modelsJSON.object()["Entities"].toArray();
for (auto jsonValue : _entitiesArray) {
QJsonObject entityObject = jsonValue.toObject();
QString modelURLString = entityObject.value(MODEL_URL_KEY).toString();
QString compoundURLString = entityObject.value(COMPOUND_SHAPE_URL_KEY).toString();
ModalDialogListener* dlg = OffscreenUi::getOpenFileNameAsync(_dialogParent, tr("Select an entity-server content file to migrate"),
QString(), tr("Entity-Server Content (*.gz)"));
for (int i = 0; i < 2; ++i) {
bool isModelURL = (i == 0);
quint8 replacementType = i;
auto migrationURLString = (isModelURL) ? modelURLString : compoundURLString;
connect(dlg, &ModalDialogListener::response, this, [=] (QVariant response) {
const QString& filename = response.toString();
disconnect(dlg, &ModalDialogListener::response, this, nullptr);
if (!filename.isEmpty()) {
qCDebug(asset_migrator) << "Selected filename for ATP asset migration: " << filename;
if (!migrationURLString.isEmpty()) {
QUrl migrationURL = QUrl(migrationURLString);
auto migrateResources = [=](QUrl migrationURL, QJsonValueRef jsonValue, bool isModelURL) {
auto request =
DependencyManager::get<ResourceManager>()->createResourceRequest(this, migrationURL);
if (!_ignoredUrls.contains(migrationURL)
&& (migrationURL.scheme() == URL_SCHEME_HTTP || migrationURL.scheme() == URL_SCHEME_HTTPS
|| migrationURL.scheme() == URL_SCHEME_FILE || migrationURL.scheme() == URL_SCHEME_FTP)) {
if (request) {
qCDebug(asset_migrator) << "Requesting" << migrationURL << "for ATP asset migration";
if (_pendingReplacements.contains(migrationURL)) {
// we already have a request out for this asset, just store the QJsonValueRef
// so we can do the hash replacement when the request comes back
_pendingReplacements.insert(migrationURL, { jsonValue, replacementType });
} else if (_uploadedAssets.contains(migrationURL)) {
// we already have a hash for this asset
// so just do the replacement immediately
if (isModelURL) {
entityObject[MODEL_URL_KEY] = _uploadedAssets.value(migrationURL).toString();
} else {
entityObject[COMPOUND_SHAPE_URL_KEY] = _uploadedAssets.value(migrationURL).toString();
}
// add this combination of QUrl and QJsonValueRef to our multi hash so we can change the URL
// to an ATP one once ready
_pendingReplacements.insert(migrationURL, { jsonValue, (isModelURL ? 0 : 1)});
jsonValue = entityObject;
} else if (wantsToMigrateResource(migrationURL)) {
auto request =
DependencyManager::get<ResourceManager>()->createResourceRequest(this, migrationURL);
connect(request, &ResourceRequest::finished, this, [=]() {
if (request->getResult() == ResourceRequest::Success) {
migrateResource(request);
} else {
++_errorCount;
_pendingReplacements.remove(migrationURL);
qWarning() << "Could not retrieve asset at" << migrationURL.toString();
if (request) {
qCDebug(asset_migrator) << "Requesting" << migrationURL << "for ATP asset migration";
checkIfFinished();
}
request->deleteLater();
});
// add this combination of QUrl and QJsonValueRef to our multi hash so we can change the URL
// to an ATP one once ready
_pendingReplacements.insert(migrationURL, { jsonValue, (isModelURL ? 0 : 1)});
connect(request, &ResourceRequest::finished, this, [=]() {
if (request->getResult() == ResourceRequest::Success) {
migrateResource(request);
} else {
++_errorCount;
_pendingReplacements.remove(migrationURL);
qWarning() << "Could not retrieve asset at" << migrationURL.toString();
checkIfFinished();
}
request->deleteLater();
});
request->send();
} else {
++_errorCount;
qWarning() << "Count not create request for asset at" << migrationURL.toString();
}
} else {
_ignoredUrls.insert(migrationURL);
}
}
}
request->send();
} else {
++_errorCount;
qWarning() << "Count not create request for asset at" << migrationURL.toString();
}
}
_doneReading = true;
};
static const QString MIGRATION_CONFIRMATION_TEXT {
"The ATP Asset Migration process will scan the selected entity-server file,\nupload discovered resources to the"\
" current asset-server\nand then save a new entity-server file with the ATP URLs.\n\nAre you ready to"\
" continue?\n\nMake sure you are connected to the right domain."
};
ModalDialogListener* migrationConfirmDialog = OffscreenUi::asyncQuestion(_dialogParent, MESSAGE_BOX_TITLE, MIGRATION_CONFIRMATION_TEXT,
QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes);
QObject::connect(migrationConfirmDialog, &ModalDialogListener::response, this, [=] (QVariant answer) {
QObject::disconnect(migrationConfirmDialog, &ModalDialogListener::response, this, nullptr);
checkIfFinished();
} else {
OffscreenUi::warning(_dialogParent, "Error",
"There was a problem loading that entity-server file for ATP asset migration. Please try again");
if (QMessageBox::Yes == static_cast<QMessageBox::StandardButton>(answer.toInt())) {
// try to open the file at the given filename
QFile modelsFile { filename };
if (modelsFile.open(QIODevice::ReadOnly)) {
QByteArray compressedJsonData = modelsFile.readAll();
QByteArray jsonData;
if (!gunzip(compressedJsonData, jsonData)) {
OffscreenUi::asyncWarning(_dialogParent, "Error", "The file at" + filename + "was not in gzip format.");
}
QJsonDocument modelsJSON = QJsonDocument::fromJson(jsonData);
_entitiesArray = modelsJSON.object()["Entities"].toArray();
for (auto jsonValue : _entitiesArray) {
QJsonObject entityObject = jsonValue.toObject();
QString modelURLString = entityObject.value(MODEL_URL_KEY).toString();
QString compoundURLString = entityObject.value(COMPOUND_SHAPE_URL_KEY).toString();
for (int i = 0; i < 2; ++i) {
bool isModelURL = (i == 0);
quint8 replacementType = i;
auto migrationURLString = (isModelURL) ? modelURLString : compoundURLString;
if (!migrationURLString.isEmpty()) {
QUrl migrationURL = QUrl(migrationURLString);
if (!_ignoredUrls.contains(migrationURL)
&& (migrationURL.scheme() == URL_SCHEME_HTTP || migrationURL.scheme() == URL_SCHEME_HTTPS
|| migrationURL.scheme() == URL_SCHEME_FILE || migrationURL.scheme() == URL_SCHEME_FTP)) {
if (_pendingReplacements.contains(migrationURL)) {
// we already have a request out for this asset, just store the QJsonValueRef
// so we can do the hash replacement when the request comes back
_pendingReplacements.insert(migrationURL, { jsonValue, replacementType });
} else if (_uploadedAssets.contains(migrationURL)) {
// we already have a hash for this asset
// so just do the replacement immediately
if (isModelURL) {
entityObject[MODEL_URL_KEY] = _uploadedAssets.value(migrationURL).toString();
} else {
entityObject[COMPOUND_SHAPE_URL_KEY] = _uploadedAssets.value(migrationURL).toString();
}
jsonValue = entityObject;
} else {
static bool hasAskedForCompleteMigration { false };
static bool wantsCompleteMigration { false };
if (!hasAskedForCompleteMigration) {
// this is the first resource migration - ask the user if they just want to migrate everything
static const QString COMPLETE_MIGRATION_TEXT { "Do you want to migrate all assets found in this entity-server file?\n"\
"Select \"Yes\" to upload all discovered assets to the current asset-server immediately.\n"\
"Select \"No\" to be prompted for each discovered asset."
};
ModalDialogListener* migrationConfirmDialog1 = OffscreenUi::asyncQuestion(_dialogParent, MESSAGE_BOX_TITLE,
"Would you like to migrate the following resource?\n" + migrationURL.toString(),
QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes);
QObject::connect(migrationConfirmDialog1, &ModalDialogListener::response, this, [=] (QVariant answer) {
QObject::disconnect(migrationConfirmDialog1, &ModalDialogListener::response, this, nullptr);
if (static_cast<QMessageBox::StandardButton>(answer.toInt()) ==
QMessageBox::Yes) {
wantsCompleteMigration = true;
migrateResources(migrationURL, jsonValue, isModelURL);
} else {
ModalDialogListener* migrationConfirmDialog2 = OffscreenUi::asyncQuestion(_dialogParent, MESSAGE_BOX_TITLE, COMPLETE_MIGRATION_TEXT,
QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes);
QObject::connect(migrationConfirmDialog2, &ModalDialogListener::response, this, [=] (QVariant answer) {
QObject::disconnect(migrationConfirmDialog2, &ModalDialogListener::response, this, nullptr);
if (static_cast<QMessageBox::StandardButton>(answer.toInt()) ==
QMessageBox::Yes) {
migrateResources(migrationURL, jsonValue, isModelURL);
} else {
_ignoredUrls.insert(migrationURL);
}
});
}
});
hasAskedForCompleteMigration = true;
}
if (wantsCompleteMigration) {
migrateResources(migrationURL, jsonValue, isModelURL);
}
}
}
}
}
}
_doneReading = true;
checkIfFinished();
} else {
OffscreenUi::asyncWarning(_dialogParent, "Error",
"There was a problem loading that entity-server file for ATP asset migration. Please try again");
}
}
});
}
}
});
}
void ATPAssetMigrator::migrateResource(ResourceRequest* request) {
@ -248,9 +292,6 @@ void ATPAssetMigrator::checkIfFinished() {
// are we out of pending replacements? if so it is time to save the entity-server file
if (_doneReading && _pendingReplacements.empty()) {
saveEntityServerFile();
// reset after the attempted save, success or fail
reset();
}
}
@ -288,39 +329,43 @@ bool ATPAssetMigrator::wantsToMigrateResource(const QUrl& url) {
void ATPAssetMigrator::saveEntityServerFile() {
// show a dialog to ask the user where they want to save the file
QString saveName = OffscreenUi::getSaveFileName(_dialogParent, "Save Migrated Entities File");
QFile saveFile { saveName };
if (saveFile.open(QIODevice::WriteOnly)) {
QJsonObject rootObject;
rootObject[ENTITIES_OBJECT_KEY] = _entitiesArray;
QJsonDocument newDocument { rootObject };
QByteArray jsonDataForFile;
if (gzip(newDocument.toJson(), jsonDataForFile, -1)) {
saveFile.write(jsonDataForFile);
saveFile.close();
ModalDialogListener* dlg = OffscreenUi::getSaveFileNameAsync(_dialogParent, "Save Migrated Entities File");
connect(dlg, &ModalDialogListener::response, this, [=] (QVariant response) {
const QString& saveName = response.toString();
QFile saveFile { saveName };
QString infoMessage = QString("Your new entities file has been saved at\n%1.").arg(saveName);
if (saveFile.open(QIODevice::WriteOnly)) {
QJsonObject rootObject;
rootObject[ENTITIES_OBJECT_KEY] = _entitiesArray;
if (_errorCount > 0) {
infoMessage += QString("\nThere were %1 models that could not be migrated.\n").arg(_errorCount);
infoMessage += "Check the warnings in your log for details.\n";
infoMessage += "You can re-attempt migration on those models\nby restarting this process with the newly saved file.";
QJsonDocument newDocument { rootObject };
QByteArray jsonDataForFile;
if (gzip(newDocument.toJson(), jsonDataForFile, -1)) {
saveFile.write(jsonDataForFile);
saveFile.close();
QString infoMessage = QString("Your new entities file has been saved at\n%1.").arg(saveName);
if (_errorCount > 0) {
infoMessage += QString("\nThere were %1 models that could not be migrated.\n").arg(_errorCount);
infoMessage += "Check the warnings in your log for details.\n";
infoMessage += "You can re-attempt migration on those models\nby restarting this process with the newly saved file.";
}
OffscreenUi::asyncInformation(_dialogParent, "Success", infoMessage);
} else {
OffscreenUi::asyncWarning(_dialogParent, "Error", "Could not gzip JSON data for new entities file.");
}
OffscreenUi::information(_dialogParent, "Success", infoMessage);
} else {
OffscreenUi::warning(_dialogParent, "Error", "Could not gzip JSON data for new entities file.");
OffscreenUi::asyncWarning(_dialogParent, "Error",
QString("Could not open file at %1 to write new entities file to.").arg(saveName));
}
} else {
OffscreenUi::warning(_dialogParent, "Error",
QString("Could not open file at %1 to write new entities file to.").arg(saveName));
}
// reset after the attempted save, success or fail
reset();
});
}
void ATPAssetMigrator::reset() {

View file

@ -2196,6 +2196,14 @@ float MyAvatar::getDomainMaxScale() {
return _domainMaximumScale;
}
void MyAvatar::setGravity(float gravity) {
_characterController.setGravity(gravity);
}
float MyAvatar::getGravity() {
return _characterController.getGravity();
}
void MyAvatar::increaseSize() {
// make sure we're starting from an allowable scale
clampTargetScaleToDomainLimits();
@ -3035,9 +3043,9 @@ glm::mat4 MyAvatar::getCenterEyeCalibrationMat() const {
if (rightEyeIndex >= 0 && leftEyeIndex >= 0) {
auto centerEyePos = (getAbsoluteDefaultJointTranslationInObjectFrame(rightEyeIndex) + getAbsoluteDefaultJointTranslationInObjectFrame(leftEyeIndex)) * 0.5f;
auto centerEyeRot = Quaternions::Y_180;
return createMatFromQuatAndPos(centerEyeRot, centerEyePos);
return createMatFromQuatAndPos(centerEyeRot, centerEyePos / getSensorToWorldScale());
} else {
return createMatFromQuatAndPos(DEFAULT_AVATAR_MIDDLE_EYE_ROT, DEFAULT_AVATAR_MIDDLE_EYE_POS);
return createMatFromQuatAndPos(DEFAULT_AVATAR_MIDDLE_EYE_ROT, DEFAULT_AVATAR_MIDDLE_EYE_POS / getSensorToWorldScale());
}
}
@ -3047,9 +3055,9 @@ glm::mat4 MyAvatar::getHeadCalibrationMat() const {
if (headIndex >= 0) {
auto headPos = getAbsoluteDefaultJointTranslationInObjectFrame(headIndex);
auto headRot = getAbsoluteDefaultJointRotationInObjectFrame(headIndex);
return createMatFromQuatAndPos(headRot, headPos);
return createMatFromQuatAndPos(headRot, headPos / getSensorToWorldScale());
} else {
return createMatFromQuatAndPos(DEFAULT_AVATAR_HEAD_ROT, DEFAULT_AVATAR_HEAD_POS);
return createMatFromQuatAndPos(DEFAULT_AVATAR_HEAD_ROT, DEFAULT_AVATAR_HEAD_POS / getSensorToWorldScale());
}
}
@ -3059,9 +3067,9 @@ glm::mat4 MyAvatar::getSpine2CalibrationMat() const {
if (spine2Index >= 0) {
auto spine2Pos = getAbsoluteDefaultJointTranslationInObjectFrame(spine2Index);
auto spine2Rot = getAbsoluteDefaultJointRotationInObjectFrame(spine2Index);
return createMatFromQuatAndPos(spine2Rot, spine2Pos);
return createMatFromQuatAndPos(spine2Rot, spine2Pos / getSensorToWorldScale());
} else {
return createMatFromQuatAndPos(DEFAULT_AVATAR_SPINE2_ROT, DEFAULT_AVATAR_SPINE2_POS);
return createMatFromQuatAndPos(DEFAULT_AVATAR_SPINE2_ROT, DEFAULT_AVATAR_SPINE2_POS / getSensorToWorldScale());
}
}
@ -3071,9 +3079,9 @@ glm::mat4 MyAvatar::getHipsCalibrationMat() const {
if (hipsIndex >= 0) {
auto hipsPos = getAbsoluteDefaultJointTranslationInObjectFrame(hipsIndex);
auto hipsRot = getAbsoluteDefaultJointRotationInObjectFrame(hipsIndex);
return createMatFromQuatAndPos(hipsRot, hipsPos);
return createMatFromQuatAndPos(hipsRot, hipsPos / getSensorToWorldScale());
} else {
return createMatFromQuatAndPos(DEFAULT_AVATAR_HIPS_ROT, DEFAULT_AVATAR_HIPS_POS);
return createMatFromQuatAndPos(DEFAULT_AVATAR_HIPS_ROT, DEFAULT_AVATAR_HIPS_POS / getSensorToWorldScale());
}
}
@ -3083,9 +3091,9 @@ glm::mat4 MyAvatar::getLeftFootCalibrationMat() const {
if (leftFootIndex >= 0) {
auto leftFootPos = getAbsoluteDefaultJointTranslationInObjectFrame(leftFootIndex);
auto leftFootRot = getAbsoluteDefaultJointRotationInObjectFrame(leftFootIndex);
return createMatFromQuatAndPos(leftFootRot, leftFootPos);
return createMatFromQuatAndPos(leftFootRot, leftFootPos / getSensorToWorldScale());
} else {
return createMatFromQuatAndPos(DEFAULT_AVATAR_LEFTFOOT_ROT, DEFAULT_AVATAR_LEFTFOOT_POS);
return createMatFromQuatAndPos(DEFAULT_AVATAR_LEFTFOOT_ROT, DEFAULT_AVATAR_LEFTFOOT_POS / getSensorToWorldScale());
}
}
@ -3095,9 +3103,9 @@ glm::mat4 MyAvatar::getRightFootCalibrationMat() const {
if (rightFootIndex >= 0) {
auto rightFootPos = getAbsoluteDefaultJointTranslationInObjectFrame(rightFootIndex);
auto rightFootRot = getAbsoluteDefaultJointRotationInObjectFrame(rightFootIndex);
return createMatFromQuatAndPos(rightFootRot, rightFootPos);
return createMatFromQuatAndPos(rightFootRot, rightFootPos / getSensorToWorldScale());
} else {
return createMatFromQuatAndPos(DEFAULT_AVATAR_RIGHTFOOT_ROT, DEFAULT_AVATAR_RIGHTFOOT_POS);
return createMatFromQuatAndPos(DEFAULT_AVATAR_RIGHTFOOT_ROT, DEFAULT_AVATAR_RIGHTFOOT_POS / getSensorToWorldScale());
}
}
@ -3107,9 +3115,9 @@ glm::mat4 MyAvatar::getRightArmCalibrationMat() const {
if (rightArmIndex >= 0) {
auto rightArmPos = getAbsoluteDefaultJointTranslationInObjectFrame(rightArmIndex);
auto rightArmRot = getAbsoluteDefaultJointRotationInObjectFrame(rightArmIndex);
return createMatFromQuatAndPos(rightArmRot, rightArmPos);
return createMatFromQuatAndPos(rightArmRot, rightArmPos / getSensorToWorldScale());
} else {
return createMatFromQuatAndPos(DEFAULT_AVATAR_RIGHTARM_ROT, DEFAULT_AVATAR_RIGHTARM_POS);
return createMatFromQuatAndPos(DEFAULT_AVATAR_RIGHTARM_ROT, DEFAULT_AVATAR_RIGHTARM_POS / getSensorToWorldScale());
}
}
@ -3118,9 +3126,9 @@ glm::mat4 MyAvatar::getLeftArmCalibrationMat() const {
if (leftArmIndex >= 0) {
auto leftArmPos = getAbsoluteDefaultJointTranslationInObjectFrame(leftArmIndex);
auto leftArmRot = getAbsoluteDefaultJointRotationInObjectFrame(leftArmIndex);
return createMatFromQuatAndPos(leftArmRot, leftArmPos);
return createMatFromQuatAndPos(leftArmRot, leftArmPos / getSensorToWorldScale());
} else {
return createMatFromQuatAndPos(DEFAULT_AVATAR_LEFTARM_ROT, DEFAULT_AVATAR_LEFTARM_POS);
return createMatFromQuatAndPos(DEFAULT_AVATAR_LEFTARM_ROT, DEFAULT_AVATAR_LEFTARM_POS / getSensorToWorldScale());
}
}
@ -3129,9 +3137,9 @@ glm::mat4 MyAvatar::getRightHandCalibrationMat() const {
if (rightHandIndex >= 0) {
auto rightHandPos = getAbsoluteDefaultJointTranslationInObjectFrame(rightHandIndex);
auto rightHandRot = getAbsoluteDefaultJointRotationInObjectFrame(rightHandIndex);
return createMatFromQuatAndPos(rightHandRot, rightHandPos);
return createMatFromQuatAndPos(rightHandRot, rightHandPos / getSensorToWorldScale());
} else {
return createMatFromQuatAndPos(DEFAULT_AVATAR_RIGHTHAND_ROT, DEFAULT_AVATAR_RIGHTHAND_POS);
return createMatFromQuatAndPos(DEFAULT_AVATAR_RIGHTHAND_ROT, DEFAULT_AVATAR_RIGHTHAND_POS / getSensorToWorldScale());
}
}
@ -3140,9 +3148,9 @@ glm::mat4 MyAvatar::getLeftHandCalibrationMat() const {
if (leftHandIndex >= 0) {
auto leftHandPos = getAbsoluteDefaultJointTranslationInObjectFrame(leftHandIndex);
auto leftHandRot = getAbsoluteDefaultJointRotationInObjectFrame(leftHandIndex);
return createMatFromQuatAndPos(leftHandRot, leftHandPos);
return createMatFromQuatAndPos(leftHandRot, leftHandPos / getSensorToWorldScale());
} else {
return createMatFromQuatAndPos(DEFAULT_AVATAR_LEFTHAND_ROT, DEFAULT_AVATAR_LEFTHAND_POS);
return createMatFromQuatAndPos(DEFAULT_AVATAR_LEFTHAND_ROT, DEFAULT_AVATAR_LEFTHAND_POS / getSensorToWorldScale());
}
}

View file

@ -152,7 +152,7 @@ class MyAvatar : public Avatar {
Q_PROPERTY(float userHeight READ getUserHeight WRITE setUserHeight)
Q_PROPERTY(float userEyeHeight READ getUserEyeHeight)
const QString DOMINANT_LEFT_HAND = "left";
const QString DOMINANT_RIGHT_HAND = "right";
@ -551,6 +551,9 @@ public slots:
float getDomainMinScale();
float getDomainMaxScale();
void setGravity(float gravity);
float getGravity();
void goToLocation(const glm::vec3& newPosition,
bool hasOrientation = false, const glm::quat& newOrientation = glm::quat(),
bool shouldFaceLocation = false);

View file

@ -86,12 +86,13 @@ void LaserPointer::editRenderState(const std::string& state, const QVariant& sta
void LaserPointer::updateRenderStateOverlay(const OverlayID& id, const QVariant& props) {
if (!id.isNull() && props.isValid()) {
qApp->getOverlays().editOverlay(id, props);
QVariantMap propMap = props.toMap();
propMap.remove("visible");
qApp->getOverlays().editOverlay(id, propMap);
}
}
void LaserPointer::updateRenderState(const RenderState& renderState, const IntersectionType type, const float distance, const QUuid& objectID, const bool defaultState) {
PickRay pickRay = qApp->getRayPickManager().getPickRay(_rayPickUID);
void LaserPointer::updateRenderState(const RenderState& renderState, const IntersectionType type, const float distance, const QUuid& objectID, const PickRay& pickRay, const bool defaultState) {
if (!renderState.getStartID().isNull()) {
QVariantMap startProps;
startProps.insert("position", vec3toVariant(pickRay.origin));
@ -183,12 +184,14 @@ void LaserPointer::disableRenderState(const RenderState& renderState) {
void LaserPointer::update() {
RayPickResult prevRayPickResult = DependencyManager::get<RayPickScriptingInterface>()->getPrevRayPickResult(_rayPickUID);
if (_renderingEnabled && !_currentRenderState.empty() && _renderStates.find(_currentRenderState) != _renderStates.end() && prevRayPickResult.type != IntersectionType::NONE) {
updateRenderState(_renderStates[_currentRenderState], prevRayPickResult.type, prevRayPickResult.distance, prevRayPickResult.objectID, false);
if (_renderingEnabled && !_currentRenderState.empty() && _renderStates.find(_currentRenderState) != _renderStates.end() &&
(prevRayPickResult.type != IntersectionType::NONE || _laserLength > 0.0f || !_objectLockEnd.first.isNull())) {
float distance = _laserLength > 0.0f ? _laserLength : prevRayPickResult.distance;
updateRenderState(_renderStates[_currentRenderState], prevRayPickResult.type, distance, prevRayPickResult.objectID, prevRayPickResult.searchRay, false);
disableRenderState(_defaultRenderStates[_currentRenderState].second);
} else if (_renderingEnabled && !_currentRenderState.empty() && _defaultRenderStates.find(_currentRenderState) != _defaultRenderStates.end()) {
disableRenderState(_renderStates[_currentRenderState]);
updateRenderState(_defaultRenderStates[_currentRenderState].second, IntersectionType::NONE, _defaultRenderStates[_currentRenderState].first, QUuid(), true);
updateRenderState(_defaultRenderStates[_currentRenderState].second, IntersectionType::NONE, _defaultRenderStates[_currentRenderState].first, QUuid(), prevRayPickResult.searchRay, true);
} else if (!_currentRenderState.empty()) {
disableRenderState(_renderStates[_currentRenderState]);
disableRenderState(_defaultRenderStates[_currentRenderState].second);

View file

@ -65,6 +65,7 @@ public:
void editRenderState(const std::string& state, const QVariant& startProps, const QVariant& pathProps, const QVariant& endProps);
void setPrecisionPicking(const bool precisionPicking) { DependencyManager::get<RayPickScriptingInterface>()->setPrecisionPicking(_rayPickUID, precisionPicking); }
void setLaserLength(const float laserLength) { _laserLength = laserLength; }
void setIgnoreEntities(const QScriptValue& ignoreEntities) { DependencyManager::get<RayPickScriptingInterface>()->setIgnoreEntities(_rayPickUID, ignoreEntities); }
void setIncludeEntities(const QScriptValue& includeEntities) { DependencyManager::get<RayPickScriptingInterface>()->setIncludeEntities(_rayPickUID, includeEntities); }
void setIgnoreOverlays(const QScriptValue& ignoreOverlays) { DependencyManager::get<RayPickScriptingInterface>()->setIgnoreOverlays(_rayPickUID, ignoreOverlays); }
@ -78,6 +79,7 @@ public:
private:
bool _renderingEnabled;
float _laserLength { 0.0f };
std::string _currentRenderState { "" };
RenderStateMap _renderStates;
DefaultRenderStateMap _defaultRenderStates;
@ -89,7 +91,7 @@ private:
QUuid _rayPickUID;
void updateRenderStateOverlay(const OverlayID& id, const QVariant& props);
void updateRenderState(const RenderState& renderState, const IntersectionType type, const float distance, const QUuid& objectID, const bool defaultState);
void updateRenderState(const RenderState& renderState, const IntersectionType type, const float distance, const QUuid& objectID, const PickRay& pickRay, const bool defaultState);
void disableRenderState(const RenderState& renderState);
};

View file

@ -14,17 +14,19 @@ QUuid LaserPointerManager::createLaserPointer(const QVariant& rayProps, const La
const bool faceAvatar, const bool centerEndY, const bool lockEnd, const bool enabled) {
std::shared_ptr<LaserPointer> laserPointer = std::make_shared<LaserPointer>(rayProps, renderStates, defaultRenderStates, faceAvatar, centerEndY, lockEnd, enabled);
if (!laserPointer->getRayUID().isNull()) {
QWriteLocker lock(&_addLock);
QWriteLocker containsLock(&_containsLock);
QUuid id = QUuid::createUuid();
_laserPointersToAdd.push(std::pair<QUuid, std::shared_ptr<LaserPointer>>(id, laserPointer));
_laserPointers[id] = laserPointer;
_laserPointerLocks[id] = std::make_shared<QReadWriteLock>();
return id;
}
return QUuid();
}
void LaserPointerManager::removeLaserPointer(const QUuid uid) {
QWriteLocker lock(&_removeLock);
_laserPointersToRemove.push(uid);
QWriteLocker lock(&_containsLock);
_laserPointers.remove(uid);
_laserPointerLocks.remove(uid);
}
void LaserPointerManager::enableLaserPointer(const QUuid uid) {
@ -69,32 +71,12 @@ const RayPickResult LaserPointerManager::getPrevRayPickResult(const QUuid uid) {
}
void LaserPointerManager::update() {
QReadLocker lock(&_containsLock);
for (QUuid& uid : _laserPointers.keys()) {
// This only needs to be a read lock because update won't change any of the properties that can be modified from scripts
QReadLocker laserLock(_laserPointerLocks[uid].get());
_laserPointers[uid]->update();
}
QWriteLocker containsLock(&_containsLock);
{
QWriteLocker lock(&_addLock);
while (!_laserPointersToAdd.empty()) {
std::pair<QUuid, std::shared_ptr<LaserPointer>> laserPointerToAdd = _laserPointersToAdd.front();
_laserPointersToAdd.pop();
_laserPointers[laserPointerToAdd.first] = laserPointerToAdd.second;
_laserPointerLocks[laserPointerToAdd.first] = std::make_shared<QReadWriteLock>();
}
}
{
QWriteLocker lock(&_removeLock);
while (!_laserPointersToRemove.empty()) {
QUuid uid = _laserPointersToRemove.front();
_laserPointersToRemove.pop();
_laserPointers.remove(uid);
_laserPointerLocks.remove(uid);
}
}
}
void LaserPointerManager::setPrecisionPicking(QUuid uid, const bool precisionPicking) {
@ -105,6 +87,14 @@ void LaserPointerManager::setPrecisionPicking(QUuid uid, const bool precisionPic
}
}
void LaserPointerManager::setLaserLength(QUuid uid, const float laserLength) {
QReadLocker lock(&_containsLock);
if (_laserPointers.contains(uid)) {
QWriteLocker laserLock(_laserPointerLocks[uid].get());
_laserPointers[uid]->setLaserLength(laserLength);
}
}
void LaserPointerManager::setIgnoreEntities(QUuid uid, const QScriptValue& ignoreEntities) {
QReadLocker lock(&_containsLock);
if (_laserPointers.contains(uid)) {

View file

@ -32,6 +32,7 @@ public:
const RayPickResult getPrevRayPickResult(const QUuid uid);
void setPrecisionPicking(QUuid uid, const bool precisionPicking);
void setLaserLength(QUuid uid, const float laserLength);
void setIgnoreEntities(QUuid uid, const QScriptValue& ignoreEntities);
void setIncludeEntities(QUuid uid, const QScriptValue& includeEntities);
void setIgnoreOverlays(QUuid uid, const QScriptValue& ignoreOverlays);
@ -46,10 +47,6 @@ public:
private:
QHash<QUuid, std::shared_ptr<LaserPointer>> _laserPointers;
QHash<QUuid, std::shared_ptr<QReadWriteLock>> _laserPointerLocks;
QReadWriteLock _addLock;
std::queue<std::pair<QUuid, std::shared_ptr<LaserPointer>>> _laserPointersToAdd;
QReadWriteLock _removeLock;
std::queue<QUuid> _laserPointersToRemove;
QReadWriteLock _containsLock;
};

View file

@ -95,6 +95,7 @@ const RenderState LaserPointerScriptingInterface::buildRenderState(const QVarian
if (propMap["start"].isValid()) {
QVariantMap startMap = propMap["start"].toMap();
if (startMap["type"].isValid()) {
startMap.remove("visible");
startID = qApp->getOverlays().addOverlay(startMap["type"].toString(), startMap);
}
}
@ -104,6 +105,7 @@ const RenderState LaserPointerScriptingInterface::buildRenderState(const QVarian
QVariantMap pathMap = propMap["path"].toMap();
// right now paths must be line3ds
if (pathMap["type"].isValid() && pathMap["type"].toString() == "line3d") {
pathMap.remove("visible");
pathID = qApp->getOverlays().addOverlay(pathMap["type"].toString(), pathMap);
}
}
@ -112,6 +114,7 @@ const RenderState LaserPointerScriptingInterface::buildRenderState(const QVarian
if (propMap["end"].isValid()) {
QVariantMap endMap = propMap["end"].toMap();
if (endMap["type"].isValid()) {
endMap.remove("visible");
endID = qApp->getOverlays().addOverlay(endMap["type"].toString(), endMap);
}
}

View file

@ -31,6 +31,7 @@ public slots:
Q_INVOKABLE RayPickResult getPrevRayPickResult(QUuid uid) { return qApp->getLaserPointerManager().getPrevRayPickResult(uid); }
Q_INVOKABLE void setPrecisionPicking(QUuid uid, const bool precisionPicking) { qApp->getLaserPointerManager().setPrecisionPicking(uid, precisionPicking); }
Q_INVOKABLE void setLaserLength(QUuid uid, const float laserLength) { qApp->getLaserPointerManager().setLaserLength(uid, laserLength); }
Q_INVOKABLE void setIgnoreEntities(QUuid uid, const QScriptValue& ignoreEntities) { qApp->getLaserPointerManager().setIgnoreEntities(uid, ignoreEntities); }
Q_INVOKABLE void setIncludeEntities(QUuid uid, const QScriptValue& includeEntities) { qApp->getLaserPointerManager().setIncludeEntities(uid, includeEntities); }
Q_INVOKABLE void setIgnoreOverlays(QUuid uid, const QScriptValue& ignoreOverlays) { qApp->getLaserPointerManager().setIgnoreOverlays(uid, ignoreOverlays); }

View file

@ -70,6 +70,9 @@ public:
if (doesPickNonCollidable()) {
toReturn |= getBitMask(PICK_INCLUDE_NONCOLLIDABLE);
}
if (doesPickCourse()) {
toReturn |= getBitMask(PICK_COURSE);
}
return Flags(toReturn);
}
Flags getOverlayFlags() const {
@ -80,6 +83,9 @@ public:
if (doesPickNonCollidable()) {
toReturn |= getBitMask(PICK_INCLUDE_NONCOLLIDABLE);
}
if (doesPickCourse()) {
toReturn |= getBitMask(PICK_COURSE);
}
return Flags(toReturn);
}
Flags getAvatarFlags() const { return Flags(getBitMask(PICK_AVATARS)); }

View file

@ -38,11 +38,12 @@ void RayPickManager::cacheResult(const bool intersects, const RayPickResult& res
res = resTemp;
}
} else {
cache[ray][mask] = RayPickResult();
cache[ray][mask] = RayPickResult(res.searchRay);
}
}
void RayPickManager::update() {
QReadLocker lock(&_containsLock);
RayPickCache results;
for (auto& uid : _rayPicks.keys()) {
std::shared_ptr<RayPick> rayPick = _rayPicks[uid];
@ -58,7 +59,7 @@ void RayPickManager::update() {
}
QPair<glm::vec3, glm::vec3> rayKey = QPair<glm::vec3, glm::vec3>(ray.origin, ray.direction);
RayPickResult res;
RayPickResult res = RayPickResult(ray);
if (rayPick->getFilter().doesPickEntities()) {
RayToEntityIntersectionResult entityRes;
@ -73,7 +74,7 @@ void RayPickManager::update() {
}
if (!fromCache) {
cacheResult(entityRes.intersects, RayPickResult(IntersectionType::ENTITY, entityRes.entityID, entityRes.distance, entityRes.intersection, entityRes.surfaceNormal),
cacheResult(entityRes.intersects, RayPickResult(IntersectionType::ENTITY, entityRes.entityID, entityRes.distance, entityRes.intersection, ray, entityRes.surfaceNormal),
entityMask, res, rayKey, results);
}
}
@ -91,7 +92,7 @@ void RayPickManager::update() {
}
if (!fromCache) {
cacheResult(overlayRes.intersects, RayPickResult(IntersectionType::OVERLAY, overlayRes.overlayID, overlayRes.distance, overlayRes.intersection, overlayRes.surfaceNormal),
cacheResult(overlayRes.intersects, RayPickResult(IntersectionType::OVERLAY, overlayRes.overlayID, overlayRes.distance, overlayRes.intersection, ray, overlayRes.surfaceNormal),
overlayMask, res, rayKey, results);
}
}
@ -100,7 +101,7 @@ void RayPickManager::update() {
RayPickFilter::Flags avatarMask = rayPick->getFilter().getAvatarFlags();
if (!checkAndCompareCachedResults(rayKey, results, res, avatarMask)) {
RayToAvatarIntersectionResult avatarRes = DependencyManager::get<AvatarManager>()->findRayIntersectionVector(ray, rayPick->getIncludeAvatars(), rayPick->getIgnoreAvatars());
cacheResult(avatarRes.intersects, RayPickResult(IntersectionType::AVATAR, avatarRes.avatarID, avatarRes.distance, avatarRes.intersection), avatarMask, res, rayKey, results);
cacheResult(avatarRes.intersects, RayPickResult(IntersectionType::AVATAR, avatarRes.avatarID, avatarRes.distance, avatarRes.intersection, ray), avatarMask, res, rayKey, results);
}
}
@ -109,7 +110,7 @@ void RayPickManager::update() {
RayPickFilter::Flags hudMask = rayPick->getFilter().getHUDFlags();
if (!checkAndCompareCachedResults(rayKey, results, res, hudMask)) {
glm::vec3 hudRes = DependencyManager::get<HMDScriptingInterface>()->calculateRayUICollisionPoint(ray.origin, ray.direction);
cacheResult(true, RayPickResult(IntersectionType::HUD, 0, glm::distance(ray.origin, hudRes), hudRes), hudMask, res, rayKey, results);
cacheResult(true, RayPickResult(IntersectionType::HUD, 0, glm::distance(ray.origin, hudRes), hudRes, ray), hudMask, res, rayKey, results);
}
}
@ -117,56 +118,39 @@ void RayPickManager::update() {
if (rayPick->getMaxDistance() == 0.0f || (rayPick->getMaxDistance() > 0.0f && res.distance < rayPick->getMaxDistance())) {
rayPick->setRayPickResult(res);
} else {
rayPick->setRayPickResult(RayPickResult());
}
}
QWriteLocker containsLock(&_containsLock);
{
QWriteLocker lock(&_addLock);
while (!_rayPicksToAdd.empty()) {
std::pair<QUuid, std::shared_ptr<RayPick>> rayPickToAdd = _rayPicksToAdd.front();
_rayPicksToAdd.pop();
_rayPicks[rayPickToAdd.first] = rayPickToAdd.second;
_rayPickLocks[rayPickToAdd.first] = std::make_shared<QReadWriteLock>();
}
}
{
QWriteLocker lock(&_removeLock);
while (!_rayPicksToRemove.empty()) {
QUuid uid = _rayPicksToRemove.front();
_rayPicksToRemove.pop();
_rayPicks.remove(uid);
_rayPickLocks.remove(uid);
rayPick->setRayPickResult(RayPickResult(ray));
}
}
}
QUuid RayPickManager::createRayPick(const std::string& jointName, const glm::vec3& posOffset, const glm::vec3& dirOffset, const RayPickFilter& filter, const float maxDistance, const bool enabled) {
QWriteLocker lock(&_addLock);
QWriteLocker lock(&_containsLock);
QUuid id = QUuid::createUuid();
_rayPicksToAdd.push(std::pair<QUuid, std::shared_ptr<RayPick>>(id, std::make_shared<JointRayPick>(jointName, posOffset, dirOffset, filter, maxDistance, enabled)));
_rayPicks[id] = std::make_shared<JointRayPick>(jointName, posOffset, dirOffset, filter, maxDistance, enabled);
_rayPickLocks[id] = std::make_shared<QReadWriteLock>();
return id;
}
QUuid RayPickManager::createRayPick(const RayPickFilter& filter, const float maxDistance, const bool enabled) {
QWriteLocker lock(&_addLock);
QWriteLocker lock(&_containsLock);
QUuid id = QUuid::createUuid();
_rayPicksToAdd.push(std::pair<QUuid, std::shared_ptr<RayPick>>(id, std::make_shared<MouseRayPick>(filter, maxDistance, enabled)));
_rayPicks[id] = std::make_shared<MouseRayPick>(filter, maxDistance, enabled);
_rayPickLocks[id] = std::make_shared<QReadWriteLock>();
return id;
}
QUuid RayPickManager::createRayPick(const glm::vec3& position, const glm::vec3& direction, const RayPickFilter& filter, const float maxDistance, const bool enabled) {
QWriteLocker lock(&_addLock);
QWriteLocker lock(&_containsLock);
QUuid id = QUuid::createUuid();
_rayPicksToAdd.push(std::pair<QUuid, std::shared_ptr<RayPick>>(id, std::make_shared<StaticRayPick>(position, direction, filter, maxDistance, enabled)));
_rayPicks[id] = std::make_shared<StaticRayPick>(position, direction, filter, maxDistance, enabled);
_rayPickLocks[id] = std::make_shared<QReadWriteLock>();
return id;
}
void RayPickManager::removeRayPick(const QUuid uid) {
QWriteLocker lock(&_removeLock);
_rayPicksToRemove.push(uid);
QWriteLocker lock(&_containsLock);
_rayPicks.remove(uid);
_rayPickLocks.remove(uid);
}
void RayPickManager::enableRayPick(const QUuid uid) {
@ -185,18 +169,6 @@ void RayPickManager::disableRayPick(const QUuid uid) {
}
}
const PickRay RayPickManager::getPickRay(const QUuid uid) {
QReadLocker containsLock(&_containsLock);
if (_rayPicks.contains(uid)) {
bool valid;
PickRay pickRay = _rayPicks[uid]->getPickRay(valid);
if (valid) {
return pickRay;
}
}
return PickRay();
}
const RayPickResult RayPickManager::getPrevRayPickResult(const QUuid uid) {
QReadLocker containsLock(&_containsLock);
if (_rayPicks.contains(uid)) {

View file

@ -28,7 +28,6 @@ class RayPickManager {
public:
void update();
const PickRay getPickRay(const QUuid uid);
QUuid createRayPick(const std::string& jointName, const glm::vec3& posOffset, const glm::vec3& dirOffset, const RayPickFilter& filter, const float maxDistance, const bool enabled);
QUuid createRayPick(const RayPickFilter& filter, const float maxDistance, const bool enabled);
@ -49,10 +48,6 @@ public:
private:
QHash<QUuid, std::shared_ptr<RayPick>> _rayPicks;
QHash<QUuid, std::shared_ptr<QReadWriteLock>> _rayPickLocks;
QReadWriteLock _addLock;
std::queue<std::pair<QUuid, std::shared_ptr<RayPick>>> _rayPicksToAdd;
QReadWriteLock _removeLock;
std::queue<QUuid> _rayPicksToRemove;
QReadWriteLock _containsLock;
typedef QHash<QPair<glm::vec3, glm::vec3>, std::unordered_map<RayPickFilter::Flags, RayPickResult>> RayPickCache;

View file

@ -19,20 +19,12 @@
#include <AssetUpload.h>
#include <MappingRequest.h>
#include <NetworkLogging.h>
#include <NodeList.h>
#include <OffscreenUi.h>
void AssetMappingModel::clear() {
// make sure we are on the same thread before we touch the hash
if (thread() != QThread::currentThread()) {
QMetaObject::invokeMethod(this, "clear");
return;
}
static const int AUTO_REFRESH_INTERVAL = 1000;
qDebug() << "Clearing loaded asset mappings for Asset Browser";
_pathToItemMap.clear();
QStandardItemModel::clear();
}
int assetMappingModelMetatypeId = qRegisterMetaType<AssetMappingModel*>("AssetMappingModel*");
AssetMappingsScriptingInterface::AssetMappingsScriptingInterface() {
_proxyModel.setSourceModel(&_assetMappingModel);
@ -154,7 +146,7 @@ void AssetMappingsScriptingInterface::getAllMappings(QJSValue callback) {
auto map = callback.engine()->newObject();
for (auto& kv : mappings ) {
map.setProperty(kv.first, kv.second);
map.setProperty(kv.first, kv.second.hash);
}
if (callback.isCallable()) {
@ -174,7 +166,7 @@ void AssetMappingsScriptingInterface::renameMapping(QString oldPath, QString new
connect(request, &RenameMappingRequest::finished, this, [this, callback](RenameMappingRequest* request) mutable {
if (callback.isCallable()) {
QJSValueList args { request->getErrorString() };
QJSValueList args{ request->getErrorString() };
callback.call(args);
}
@ -184,6 +176,49 @@ void AssetMappingsScriptingInterface::renameMapping(QString oldPath, QString new
request->start();
}
void AssetMappingsScriptingInterface::setBakingEnabled(QStringList paths, bool enabled, QJSValue callback) {
auto assetClient = DependencyManager::get<AssetClient>();
auto request = assetClient->createSetBakingEnabledRequest(paths, enabled);
connect(request, &SetBakingEnabledRequest::finished, this, [this, callback](SetBakingEnabledRequest* request) mutable {
if (callback.isCallable()) {
QJSValueList args{ request->getErrorString() };
callback.call(args);
}
request->deleteLater();
});
request->start();
}
AssetMappingModel::AssetMappingModel() {
setupRoles();
connect(&_autoRefreshTimer, &QTimer::timeout, this, [this] {
auto nodeList = DependencyManager::get<NodeList>();
auto assetServer = nodeList->soloNodeOfType(NodeType::AssetServer);
if (assetServer) {
refresh();
}
});
_autoRefreshTimer.setInterval(AUTO_REFRESH_INTERVAL);
}
bool AssetMappingModel::isAutoRefreshEnabled() {
return _autoRefreshTimer.isActive();
}
void AssetMappingModel::setAutoRefreshEnabled(bool enabled) {
if (enabled != _autoRefreshTimer.isActive()) {
if (enabled) {
_autoRefreshTimer.start();
} else {
_autoRefreshTimer.stop();
}
}
}
bool AssetMappingModel::isKnownFolder(QString path) const {
if (!path.endsWith("/")) {
return false;
@ -198,10 +233,7 @@ bool AssetMappingModel::isKnownFolder(QString path) const {
return false;
}
int assetMappingModelMetatypeId = qRegisterMetaType<AssetMappingModel*>("AssetMappingModel*");
void AssetMappingModel::refresh() {
qDebug() << "Refreshing asset mapping model";
auto assetClient = DependencyManager::get<AssetClient>();
auto request = assetClient->createGetAllMappingsRequest();
@ -211,6 +243,12 @@ void AssetMappingModel::refresh() {
auto existingPaths = _pathToItemMap.keys();
for (auto& mapping : mappings) {
auto& path = mapping.first;
if (path.startsWith(HIDDEN_BAKED_CONTENT_FOLDER)) {
// Hide baked mappings
continue;
}
auto parts = path.split("/");
auto length = parts.length();
@ -223,27 +261,32 @@ void AssetMappingModel::refresh() {
// start index at 1 to avoid empty string from leading slash
for (int i = 1; i < length; ++i) {
fullPath += (i == 1 ? "" : "/") + parts[i];
bool isFolder = i < length - 1;
auto it = _pathToItemMap.find(fullPath);
if (it == _pathToItemMap.end()) {
auto item = new QStandardItem(parts[i]);
bool isFolder = i < length - 1;
item->setData(isFolder ? fullPath + "/" : fullPath, Qt::UserRole);
item->setData(isFolder, Qt::UserRole + 1);
item->setData(parts[i], Qt::UserRole + 2);
item->setData("atp:" + fullPath, Qt::UserRole + 3);
item->setData(fullPath, Qt::UserRole + 4);
if (lastItem) {
lastItem->setChild(lastItem->rowCount(), 0, item);
lastItem->appendRow(item);
} else {
appendRow(item);
}
lastItem = item;
_pathToItemMap[fullPath] = lastItem;
} else {
lastItem = it.value();
}
// update status
auto statusString = isFolder ? "--" : bakingStatusToString(mapping.second.status);
lastItem->setData(statusString, Qt::UserRole + 5);
lastItem->setData(mapping.second.bakingErrors, Qt::UserRole + 6);
}
Q_ASSERT(fullPath == path);
@ -295,8 +338,30 @@ void AssetMappingModel::refresh() {
emit errorGettingMappings(request->getErrorString());
}
emit updated();
request->deleteLater();
});
request->start();
}
void AssetMappingModel::clear() {
// make sure we are on the same thread before we touch the hash
if (thread() != QThread::currentThread()) {
QMetaObject::invokeMethod(this, "clear");
return;
}
qDebug() << "Clearing loaded asset mappings for Asset Browser";
_pathToItemMap.clear();
QStandardItemModel::clear();
}
void AssetMappingModel::setupRoles() {
QHash<int, QByteArray> roleNames;
roleNames[Qt::DisplayRole] = "name";
roleNames[Qt::UserRole + 5] = "baked";
setItemRoleNames(roleNames);
}

View file

@ -25,9 +25,16 @@
class AssetMappingModel : public QStandardItemModel {
Q_OBJECT
Q_PROPERTY(bool autoRefreshEnabled READ isAutoRefreshEnabled WRITE setAutoRefreshEnabled)
public:
AssetMappingModel();
Q_INVOKABLE void refresh();
bool isAutoRefreshEnabled();
void setAutoRefreshEnabled(bool enabled);
bool isKnownMapping(QString path) const { return _pathToItemMap.contains(path); }
bool isKnownFolder(QString path) const;
@ -36,9 +43,13 @@ public slots:
signals:
void errorGettingMappings(QString errorString);
void updated();
private:
void setupRoles();
QHash<QString, QStandardItem*> _pathToItemMap;
QTimer _autoRefreshTimer;
};
Q_DECLARE_METATYPE(AssetMappingModel*)
@ -61,10 +72,11 @@ public:
Q_INVOKABLE void setMapping(QString path, QString hash, QJSValue callback = QJSValue());
Q_INVOKABLE void getMapping(QString path, QJSValue callback = QJSValue());
Q_INVOKABLE void uploadFile(QString path, QString mapping, QJSValue startedCallback = QJSValue(), QJSValue completedCallback = QJSValue(), bool dropEvent = false);
Q_INVOKABLE void deleteMappings(QStringList paths, QJSValue callback);
Q_INVOKABLE void deleteMappings(QStringList paths, QJSValue callback = QJSValue());
Q_INVOKABLE void deleteMapping(QString path, QJSValue callback) { deleteMappings(QStringList(path), callback = QJSValue()); }
Q_INVOKABLE void getAllMappings(QJSValue callback = QJSValue());
Q_INVOKABLE void renameMapping(QString oldPath, QString newPath, QJSValue callback = QJSValue());
Q_INVOKABLE void setBakingEnabled(QStringList paths, bool enabled, QJSValue callback = QJSValue());
protected:
QSet<AssetRequest*> _pendingRequests;

View file

@ -135,10 +135,10 @@ void Audio::setReverbOptions(const AudioEffectOptions* options) {
DependencyManager::get<AudioClient>()->setReverbOptions(options);
}
void Audio::setInputDevice(const QAudioDeviceInfo& device) {
_devices.chooseInputDevice(device);
void Audio::setInputDevice(const QAudioDeviceInfo& device, bool isHMD) {
_devices.chooseInputDevice(device, isHMD);
}
void Audio::setOutputDevice(const QAudioDeviceInfo& device) {
_devices.chooseOutputDevice(device);
void Audio::setOutputDevice(const QAudioDeviceInfo& device, bool isHMD) {
_devices.chooseOutputDevice(device, isHMD);
}

View file

@ -50,8 +50,8 @@ public:
void showMicMeter(bool show);
void setInputVolume(float volume);
Q_INVOKABLE void setInputDevice(const QAudioDeviceInfo& device);
Q_INVOKABLE void setOutputDevice(const QAudioDeviceInfo& device);
Q_INVOKABLE void setInputDevice(const QAudioDeviceInfo& device, bool isHMD);
Q_INVOKABLE void setOutputDevice(const QAudioDeviceInfo& device, bool isHMD);
Q_INVOKABLE void setReverb(bool enable);
Q_INVOKABLE void setReverbOptions(const AudioEffectOptions* options);

View file

@ -38,15 +38,17 @@ Setting::Handle<QString>& getSetting(bool contextIsHMD, QAudio::Mode mode) {
}
enum AudioDeviceRole {
DisplayRole = Qt::DisplayRole,
CheckStateRole = Qt::CheckStateRole,
PeakRole = Qt::UserRole,
InfoRole = Qt::UserRole + 1
DeviceNameRole = Qt::UserRole,
SelectedDesktopRole,
SelectedHMDRole,
PeakRole,
InfoRole
};
QHash<int, QByteArray> AudioDeviceList::_roles {
{ DisplayRole, "display" },
{ CheckStateRole, "selected" },
{ DeviceNameRole, "devicename" },
{ SelectedDesktopRole, "selectedDesktop" },
{ SelectedHMDRole, "selectedHMD" },
{ PeakRole, "peak" },
{ InfoRole, "info" }
};
@ -68,15 +70,64 @@ static QString getTargetDevice(bool hmd, QAudio::Mode mode) {
Qt::ItemFlags AudioDeviceList::_flags { Qt::ItemIsSelectable | Qt::ItemIsEnabled };
AudioDeviceList::AudioDeviceList(QAudio::Mode mode) : _mode(mode) {
auto& setting1 = getSetting(true, QAudio::AudioInput);
if (setting1.isSet()) {
qDebug() << "Device name in settings for HMD, Input" << setting1.get();
} else {
qDebug() << "Device name in settings for HMD, Input not set";
}
auto& setting2 = getSetting(true, QAudio::AudioOutput);
if (setting2.isSet()) {
qDebug() << "Device name in settings for HMD, Output" << setting2.get();
} else {
qDebug() << "Device name in settings for HMD, Output not set";
}
auto& setting3 = getSetting(false, QAudio::AudioInput);
if (setting3.isSet()) {
qDebug() << "Device name in settings for Desktop, Input" << setting3.get();
} else {
qDebug() << "Device name in settings for Desktop, Input not set";
}
auto& setting4 = getSetting(false, QAudio::AudioOutput);
if (setting4.isSet()) {
qDebug() << "Device name in settings for Desktop, Output" << setting4.get();
} else {
qDebug() << "Device name in settings for Desktop, Output not set";
}
}
AudioDeviceList::~AudioDeviceList() {
//save all selected devices
auto& settingHMD = getSetting(true, _mode);
auto& settingDesktop = getSetting(false, _mode);
// store the selected device
foreach(std::shared_ptr<AudioDevice> adevice, _devices) {
if (adevice->selectedDesktop) {
qDebug() << "Saving Desktop for" << _mode << "name" << adevice->info.deviceName();
settingDesktop.set(adevice->info.deviceName());
}
if (adevice->selectedHMD) {
qDebug() << "Saving HMD for" << _mode << "name" << adevice->info.deviceName();
settingHMD.set(adevice->info.deviceName());
}
}
}
QVariant AudioDeviceList::data(const QModelIndex& index, int role) const {
if (!index.isValid() || index.row() >= rowCount()) {
return QVariant();
}
if (role == DisplayRole) {
if (role == DeviceNameRole) {
return _devices.at(index.row())->display;
} else if (role == CheckStateRole) {
return _devices.at(index.row())->selected;
} else if (role == SelectedDesktopRole) {
return _devices.at(index.row())->selectedDesktop;
} else if (role == SelectedHMDRole) {
return _devices.at(index.row())->selectedHMD;
} else if (role == InfoRole) {
return QVariant::fromValue<QAudioDeviceInfo>(_devices.at(index.row())->info);
} else {
@ -130,37 +181,48 @@ void AudioDeviceList::resetDevice(bool contextIsHMD) {
#endif
}
void AudioDeviceList::onDeviceChanged(const QAudioDeviceInfo& device) {
auto oldDevice = _selectedDevice;
_selectedDevice = device;
void AudioDeviceList::onDeviceChanged(const QAudioDeviceInfo& device, bool isHMD) {
auto oldDevice = isHMD ? _selectedHMDDevice : _selectedDesktopDevice;
QAudioDeviceInfo& selectedDevice = isHMD ? _selectedHMDDevice : _selectedDesktopDevice;
selectedDevice = device;
for (auto i = 0; i < rowCount(); ++i) {
AudioDevice& device = *_devices[i];
if (device.selected && device.info != _selectedDevice) {
device.selected = false;
} else if (device.info == _selectedDevice) {
device.selected = true;
for (auto i = 0; i < _devices.size(); ++i) {
std::shared_ptr<AudioDevice> device = _devices[i];
bool &isSelected = isHMD ? device->selectedHMD : device->selectedDesktop;
if (isSelected && device->info != selectedDevice) {
isSelected = false;
} else if (device->info == selectedDevice) {
isSelected = true;
}
}
emit deviceChanged(_selectedDevice);
emit deviceChanged(selectedDevice);
emit dataChanged(createIndex(0, 0), createIndex(rowCount() - 1, 0));
}
void AudioDeviceList::onDevicesChanged(const QList<QAudioDeviceInfo>& devices) {
void AudioDeviceList::onDevicesChanged(const QList<QAudioDeviceInfo>& devices, bool isHMD) {
QAudioDeviceInfo& selectedDevice = isHMD ? _selectedHMDDevice : _selectedDesktopDevice;
const QString& savedDeviceName = isHMD ? _hmdSavedDeviceName : _desktopSavedDeviceName;
beginResetModel();
_devices.clear();
foreach(const QAudioDeviceInfo& deviceInfo, devices) {
AudioDevice device;
bool &isSelected = isHMD ? device.selectedHMD : device.selectedDesktop;
device.info = deviceInfo;
device.display = device.info.deviceName()
.replace("High Definition", "HD")
.remove("Device")
.replace(" )", ")");
device.selected = (device.info == _selectedDevice);
if (!selectedDevice.isNull()) {
isSelected = (device.info == selectedDevice);
} else {
//no selected device for context. fallback to saved
isSelected = (device.info.deviceName() == savedDeviceName);
}
qDebug() << "adding audio device:" << device.display << device.selectedDesktop << device.selectedHMD << _mode;
_devices.push_back(newDevice(device));
}
@ -203,22 +265,32 @@ AudioDevices::AudioDevices(bool& contextIsHMD) : _contextIsHMD(contextIsHMD) {
connect(client.data(), &AudioClient::devicesChanged, this, &AudioDevices::onDevicesChanged, Qt::QueuedConnection);
connect(client.data(), &AudioClient::peakValueListChanged, &_inputs, &AudioInputDeviceList::onPeakValueListChanged, Qt::QueuedConnection);
_inputs.onDeviceChanged(client->getActiveAudioDevice(QAudio::AudioInput), contextIsHMD);
_outputs.onDeviceChanged(client->getActiveAudioDevice(QAudio::AudioOutput), contextIsHMD);
// connections are made after client is initialized, so we must also fetch the devices
_inputs.onDeviceChanged(client->getActiveAudioDevice(QAudio::AudioInput));
_outputs.onDeviceChanged(client->getActiveAudioDevice(QAudio::AudioOutput));
_inputs.onDevicesChanged(client->getAudioDevices(QAudio::AudioInput));
_outputs.onDevicesChanged(client->getAudioDevices(QAudio::AudioOutput));
const QList<QAudioDeviceInfo>& devicesInput = client->getAudioDevices(QAudio::AudioInput);
const QList<QAudioDeviceInfo>& devicesOutput = client->getAudioDevices(QAudio::AudioOutput);
//setup HMD devices
_inputs.onDevicesChanged(devicesInput, true);
_outputs.onDevicesChanged(devicesOutput, true);
//setup Desktop devices
_inputs.onDevicesChanged(devicesInput, false);
_outputs.onDevicesChanged(devicesOutput, false);
}
AudioDevices::~AudioDevices() {}
void AudioDevices::onContextChanged(const QString& context) {
_inputs.resetDevice(_contextIsHMD);
_outputs.resetDevice(_contextIsHMD);
}
void AudioDevices::onDeviceSelected(QAudio::Mode mode, const QAudioDeviceInfo& device, const QAudioDeviceInfo& previousDevice) {
void AudioDevices::onDeviceSelected(QAudio::Mode mode, const QAudioDeviceInfo& device,
const QAudioDeviceInfo& previousDevice, bool isHMD) {
QString deviceName = device.isNull() ? QString() : device.deviceName();
auto& setting = getSetting(_contextIsHMD, mode);
auto& setting = getSetting(isHMD, mode);
// check for a previous device
auto wasDefault = setting.get().isNull();
@ -254,42 +326,94 @@ void AudioDevices::onDeviceSelected(QAudio::Mode mode, const QAudioDeviceInfo& d
void AudioDevices::onDeviceChanged(QAudio::Mode mode, const QAudioDeviceInfo& device) {
if (mode == QAudio::AudioInput) {
if (_requestedInputDevice == device) {
onDeviceSelected(QAudio::AudioInput, device, _inputs._selectedDevice);
onDeviceSelected(QAudio::AudioInput, device,
_contextIsHMD ? _inputs._selectedHMDDevice : _inputs._selectedDesktopDevice,
_contextIsHMD);
_requestedInputDevice = QAudioDeviceInfo();
}
_inputs.onDeviceChanged(device);
_inputs.onDeviceChanged(device, _contextIsHMD);
} else { // if (mode == QAudio::AudioOutput)
if (_requestedOutputDevice == device) {
onDeviceSelected(QAudio::AudioOutput, device, _outputs._selectedDevice);
onDeviceSelected(QAudio::AudioOutput, device,
_contextIsHMD ? _outputs._selectedHMDDevice : _outputs._selectedDesktopDevice,
_contextIsHMD);
_requestedOutputDevice = QAudioDeviceInfo();
}
_outputs.onDeviceChanged(device);
_outputs.onDeviceChanged(device, _contextIsHMD);
}
}
void AudioDevices::onDevicesChanged(QAudio::Mode mode, const QList<QAudioDeviceInfo>& devices) {
static std::once_flag once;
std::call_once(once, [&] {
//readout settings
auto client = DependencyManager::get<AudioClient>();
_inputs._hmdSavedDeviceName = getTargetDevice(true, QAudio::AudioInput);
_inputs._desktopSavedDeviceName = getTargetDevice(false, QAudio::AudioInput);
//fallback to default device
if (_inputs._desktopSavedDeviceName.isEmpty()) {
_inputs._desktopSavedDeviceName = client->getActiveAudioDevice(QAudio::AudioInput).deviceName();
}
//fallback to desktop device
if (_inputs._hmdSavedDeviceName.isEmpty()) {
_inputs._hmdSavedDeviceName = _inputs._desktopSavedDeviceName;
}
_outputs._hmdSavedDeviceName = getTargetDevice(true, QAudio::AudioOutput);
_outputs._desktopSavedDeviceName = getTargetDevice(false, QAudio::AudioOutput);
if (_outputs._desktopSavedDeviceName.isEmpty()) {
_outputs._desktopSavedDeviceName = client->getActiveAudioDevice(QAudio::AudioOutput).deviceName();
}
if (_outputs._hmdSavedDeviceName.isEmpty()) {
_outputs._hmdSavedDeviceName = _outputs._desktopSavedDeviceName;
}
onContextChanged(QString());
});
//set devices for both contexts
if (mode == QAudio::AudioInput) {
_inputs.onDevicesChanged(devices);
_inputs.onDevicesChanged(devices, _contextIsHMD);
_inputs.onDevicesChanged(devices, !_contextIsHMD);
} else { // if (mode == QAudio::AudioOutput)
_outputs.onDevicesChanged(devices);
_outputs.onDevicesChanged(devices, _contextIsHMD);
_outputs.onDevicesChanged(devices, !_contextIsHMD);
}
std::call_once(once, [&] { onContextChanged(QString()); });
}
void AudioDevices::chooseInputDevice(const QAudioDeviceInfo& device) {
auto client = DependencyManager::get<AudioClient>();
_requestedInputDevice = device;
QMetaObject::invokeMethod(client.data(), "switchAudioDevice",
Q_ARG(QAudio::Mode, QAudio::AudioInput),
Q_ARG(const QAudioDeviceInfo&, device));
void AudioDevices::chooseInputDevice(const QAudioDeviceInfo& device, bool isHMD) {
//check if current context equals device to change
if (_contextIsHMD == isHMD) {
auto client = DependencyManager::get<AudioClient>();
_requestedInputDevice = device;
QMetaObject::invokeMethod(client.data(), "switchAudioDevice",
Q_ARG(QAudio::Mode, QAudio::AudioInput),
Q_ARG(const QAudioDeviceInfo&, device));
} else {
//context is different. just save device in settings
onDeviceSelected(QAudio::AudioInput, device,
isHMD ? _inputs._selectedHMDDevice : _inputs._selectedDesktopDevice,
isHMD);
_inputs.onDeviceChanged(device, isHMD);
}
}
void AudioDevices::chooseOutputDevice(const QAudioDeviceInfo& device) {
auto client = DependencyManager::get<AudioClient>();
_requestedOutputDevice = device;
QMetaObject::invokeMethod(client.data(), "switchAudioDevice",
Q_ARG(QAudio::Mode, QAudio::AudioOutput),
Q_ARG(const QAudioDeviceInfo&, device));
void AudioDevices::chooseOutputDevice(const QAudioDeviceInfo& device, bool isHMD) {
//check if current context equals device to change
if (_contextIsHMD == isHMD) {
auto client = DependencyManager::get<AudioClient>();
_requestedOutputDevice = device;
QMetaObject::invokeMethod(client.data(), "switchAudioDevice",
Q_ARG(QAudio::Mode, QAudio::AudioOutput),
Q_ARG(const QAudioDeviceInfo&, device));
} else {
//context is different. just save device in settings
onDeviceSelected(QAudio::AudioOutput, device,
isHMD ? _outputs._selectedHMDDevice : _outputs._selectedDesktopDevice,
isHMD);
_outputs.onDeviceChanged(device, isHMD);
}
}

View file

@ -25,15 +25,16 @@ class AudioDevice {
public:
QAudioDeviceInfo info;
QString display;
bool selected { false };
bool selectedDesktop { false };
bool selectedHMD { false };
};
class AudioDeviceList : public QAbstractListModel {
Q_OBJECT
public:
AudioDeviceList(QAudio::Mode mode = QAudio::AudioOutput) : _mode(mode) {}
~AudioDeviceList() = default;
AudioDeviceList(QAudio::Mode mode = QAudio::AudioOutput);
virtual ~AudioDeviceList();
virtual std::shared_ptr<AudioDevice> newDevice(const AudioDevice& device)
{ return std::make_shared<AudioDevice>(device); }
@ -52,8 +53,8 @@ signals:
void deviceChanged(const QAudioDeviceInfo& device);
protected slots:
void onDeviceChanged(const QAudioDeviceInfo& device);
void onDevicesChanged(const QList<QAudioDeviceInfo>& devices);
void onDeviceChanged(const QAudioDeviceInfo& device, bool isHMD);
void onDevicesChanged(const QList<QAudioDeviceInfo>& devices, bool isHMD);
protected:
friend class AudioDevices;
@ -61,8 +62,11 @@ protected:
static QHash<int, QByteArray> _roles;
static Qt::ItemFlags _flags;
const QAudio::Mode _mode;
QAudioDeviceInfo _selectedDevice;
QAudioDeviceInfo _selectedDesktopDevice;
QAudioDeviceInfo _selectedHMDDevice;
QList<std::shared_ptr<AudioDevice>> _devices;
QString _hmdSavedDeviceName;
QString _desktopSavedDeviceName;
};
class AudioInputDevice : public AudioDevice {
@ -102,7 +106,6 @@ protected:
void setPeakValuesEnabled(bool enable);
bool _peakValuesEnabled { false };
};
class Audio;
class AudioDevices : public QObject {
@ -112,15 +115,18 @@ class AudioDevices : public QObject {
public:
AudioDevices(bool& contextIsHMD);
void chooseInputDevice(const QAudioDeviceInfo& device);
void chooseOutputDevice(const QAudioDeviceInfo& device);
virtual ~AudioDevices();
void chooseInputDevice(const QAudioDeviceInfo& device, bool isHMD);
void chooseOutputDevice(const QAudioDeviceInfo& device, bool isHMD);
signals:
void nop();
private slots:
void onContextChanged(const QString& context);
void onDeviceSelected(QAudio::Mode mode, const QAudioDeviceInfo& device, const QAudioDeviceInfo& previousDevice);
void onDeviceSelected(QAudio::Mode mode, const QAudioDeviceInfo& device,
const QAudioDeviceInfo& previousDevice, bool isHMD);
void onDeviceChanged(QAudio::Mode mode, const QAudioDeviceInfo& device);
void onDevicesChanged(QAudio::Mode mode, const QList<QAudioDeviceInfo>& devices);

View file

@ -59,7 +59,7 @@ WindowScriptingInterface::WindowScriptingInterface() {
QUrl url(urlString);
emit svoImportRequested(url.url());
} else {
OffscreenUi::warning("Import SVO Error", "You need to be running edit.js to import entities.");
OffscreenUi::asyncWarning("Import SVO Error", "You need to be running edit.js to import entities.");
}
});
@ -103,7 +103,7 @@ void WindowScriptingInterface::raiseMainWindow() {
/// \param const QString& message message to display
/// \return QScriptValue::UndefinedValue
void WindowScriptingInterface::alert(const QString& message) {
OffscreenUi::warning("", message);
OffscreenUi::asyncWarning("", message);
}
/// Display a confirmation box with the options 'Yes' and 'No'
@ -123,6 +123,17 @@ QScriptValue WindowScriptingInterface::prompt(const QString& message, const QStr
return ok ? QScriptValue(result) : QScriptValue::NullValue;
}
/// Display a prompt with a text box
/// \param const QString& message message to display
/// \param const QString& defaultText default text in the text box
void WindowScriptingInterface::promptAsync(const QString& message, const QString& defaultText) {
ModalDialogListener* dlg = OffscreenUi::getTextAsync(nullptr, "", message, QLineEdit::Normal, defaultText);
connect(dlg, &ModalDialogListener::response, this, [=] (QVariant result) {
disconnect(dlg, &ModalDialogListener::response, this, nullptr);
emit promptTextChanged(result.toString());
});
}
CustomPromptResult WindowScriptingInterface::customPrompt(const QVariant& config) {
CustomPromptResult result;
bool ok = false;
@ -191,8 +202,31 @@ QScriptValue WindowScriptingInterface::browseDir(const QString& title, const QSt
return result.isEmpty() ? QScriptValue::NullValue : QScriptValue(result);
}
/// Display an open file dialog. If `directory` is an invalid file or directory the browser will start at the current
/// Display a "browse to directory" dialog. If `directory` is an invalid file or directory the browser will start at the current
/// working directory.
/// \param const QString& title title of the window
/// \param const QString& directory directory to start the file browser at
/// \param const QString& nameFilter filter to filter filenames by - see `QFileDialog`
void WindowScriptingInterface::browseDirAsync(const QString& title, const QString& directory) {
ensureReticleVisible();
QString path = directory;
if (path.isEmpty()) {
path = getPreviousBrowseLocation();
}
#ifndef Q_OS_WIN
path = fixupPathForMac(directory);
#endif
ModalDialogListener* dlg = OffscreenUi::getExistingDirectoryAsync(nullptr, title, path);
connect(dlg, &ModalDialogListener::response, this, [=] (QVariant response) {
const QString& result = response.toString();
disconnect(dlg, &ModalDialogListener::response, this, nullptr);
if (!result.isEmpty()) {
setPreviousBrowseLocation(QFileInfo(result).absolutePath());
}
emit browseDirChanged(result);
});
}
/// \param const QString& title title of the window
/// \param const QString& directory directory to start the file browser at
/// \param const QString& nameFilter filter to filter filenames by - see `QFileDialog`
@ -213,6 +247,31 @@ QScriptValue WindowScriptingInterface::browse(const QString& title, const QStrin
return result.isEmpty() ? QScriptValue::NullValue : QScriptValue(result);
}
/// Display an open file dialog. If `directory` is an invalid file or directory the browser will start at the current
/// working directory.
/// \param const QString& title title of the window
/// \param const QString& directory directory to start the file browser at
/// \param const QString& nameFilter filter to filter filenames by - see `QFileDialog`
void WindowScriptingInterface::browseAsync(const QString& title, const QString& directory, const QString& nameFilter) {
ensureReticleVisible();
QString path = directory;
if (path.isEmpty()) {
path = getPreviousBrowseLocation();
}
#ifndef Q_OS_WIN
path = fixupPathForMac(directory);
#endif
ModalDialogListener* dlg = OffscreenUi::getOpenFileNameAsync(nullptr, title, path, nameFilter);
connect(dlg, &ModalDialogListener::response, this, [=] (QVariant response) {
const QString& result = response.toString();
disconnect(dlg, &ModalDialogListener::response, this, nullptr);
if (!result.isEmpty()) {
setPreviousBrowseLocation(QFileInfo(result).absolutePath());
}
emit openFileChanged(result);
});
}
/// Display a save file dialog. If `directory` is an invalid file or directory the browser will start at the current
/// working directory.
/// \param const QString& title title of the window
@ -235,7 +294,32 @@ QScriptValue WindowScriptingInterface::save(const QString& title, const QString&
return result.isEmpty() ? QScriptValue::NullValue : QScriptValue(result);
}
/// Display a select asset dialog that lets the user select an asset from the Asset Server. If `directory` is an invalid
/// Display a save file dialog. If `directory` is an invalid file or directory the browser will start at the current
/// working directory.
/// \param const QString& title title of the window
/// \param const QString& directory directory to start the file browser at
/// \param const QString& nameFilter filter to filter filenames by - see `QFileDialog`
void WindowScriptingInterface::saveAsync(const QString& title, const QString& directory, const QString& nameFilter) {
ensureReticleVisible();
QString path = directory;
if (path.isEmpty()) {
path = getPreviousBrowseLocation();
}
#ifndef Q_OS_WIN
path = fixupPathForMac(directory);
#endif
ModalDialogListener* dlg = OffscreenUi::getSaveFileNameAsync(nullptr, title, path, nameFilter);
connect(dlg, &ModalDialogListener::response, this, [=] (QVariant response) {
const QString& result = response.toString();
disconnect(dlg, &ModalDialogListener::response, this, nullptr);
if (!result.isEmpty()) {
setPreviousBrowseLocation(QFileInfo(result).absolutePath());
}
emit saveFileChanged(result);
});
}
/// Display a select asset dialog that lets the user select an asset from the Asset Server. If `directory` is an invalid
/// directory the browser will start at the root directory.
/// \param const QString& title title of the window
/// \param const QString& directory directory to start the asset browser at
@ -260,6 +344,35 @@ QScriptValue WindowScriptingInterface::browseAssets(const QString& title, const
return result.isEmpty() ? QScriptValue::NullValue : QScriptValue(result);
}
/// Display a select asset dialog that lets the user select an asset from the Asset Server. If `directory` is an invalid
/// directory the browser will start at the root directory.
/// \param const QString& title title of the window
/// \param const QString& directory directory to start the asset browser at
/// \param const QString& nameFilter filter to filter asset names by - see `QFileDialog`
void WindowScriptingInterface::browseAssetsAsync(const QString& title, const QString& directory, const QString& nameFilter) {
ensureReticleVisible();
QString path = directory;
if (path.isEmpty()) {
path = getPreviousBrowseAssetLocation();
}
if (path.left(1) != "/") {
path = "/" + path;
}
if (path.right(1) != "/") {
path = path + "/";
}
ModalDialogListener* dlg = OffscreenUi::getOpenAssetNameAsync(nullptr, title, path, nameFilter);
connect(dlg, &ModalDialogListener::response, this, [=] (QVariant response) {
const QString& result = response.toString();
disconnect(dlg, &ModalDialogListener::response, this, nullptr);
if (!result.isEmpty()) {
setPreviousBrowseAssetLocation(QFileInfo(result).absolutePath());
}
emit assetsDirChanged(result);
});
}
void WindowScriptingInterface::showAssetServer(const QString& upload) {
QMetaObject::invokeMethod(qApp, "showAssetServerWidget", Qt::QueuedConnection, Q_ARG(QString, upload));
}

View file

@ -51,12 +51,17 @@ public slots:
void raiseMainWindow();
void alert(const QString& message = "");
QScriptValue confirm(const QString& message = "");
QScriptValue prompt(const QString& message = "", const QString& defaultText = "");
QScriptValue prompt(const QString& message, const QString& defaultText);
void promptAsync(const QString& message = "", const QString& defaultText = "");
CustomPromptResult customPrompt(const QVariant& config);
QScriptValue browseDir(const QString& title = "", const QString& directory = "");
void browseDirAsync(const QString& title = "", const QString& directory = "");
QScriptValue browse(const QString& title = "", const QString& directory = "", const QString& nameFilter = "");
void browseAsync(const QString& title = "", const QString& directory = "", const QString& nameFilter = "");
QScriptValue save(const QString& title = "", const QString& directory = "", const QString& nameFilter = "");
void saveAsync(const QString& title = "", const QString& directory = "", const QString& nameFilter = "");
QScriptValue browseAssets(const QString& title = "", const QString& directory = "", const QString& nameFilter = "");
void browseAssetsAsync(const QString& title = "", const QString& directory = "", const QString& nameFilter = "");
void showAssetServer(const QString& upload = "");
QString checkVersion();
void copyToClipboard(const QString& text);
@ -89,6 +94,11 @@ signals:
void announcement(const QString& message);
void messageBoxClosed(int id, int button);
void browseDirChanged(QString browseDir);
void assetsDirChanged(QString assetsDir);
void saveFileChanged(QString filename);
void openFileChanged(QString filename);
void promptTextChanged(QString text);
// triggered when window size or position changes
void geometryChanged(QRect geometry);

View file

@ -73,11 +73,11 @@ void AddressBarDialog::loadForward() {
}
void AddressBarDialog::displayAddressOfflineMessage() {
OffscreenUi::critical("", "That user or place is currently offline");
OffscreenUi::asyncCritical("", "That user or place is currently offline");
}
void AddressBarDialog::displayAddressNotFoundMessage() {
OffscreenUi::critical("", "There is no address information for that user or place");
OffscreenUi::asyncCritical("", "There is no address information for that user or place");
}
void AddressBarDialog::observeShownChanged(bool visible) {

View file

@ -91,16 +91,6 @@ void setupPreferences() {
preference->setMax(500);
preferences->addPreference(preference);
}
{
auto getter = []()->bool { return qApp->getDesktopTabletBecomesToolbarSetting(); };
auto setter = [](bool value) { qApp->setDesktopTabletBecomesToolbarSetting(value); };
preferences->addPreference(new CheckPreference(UI_CATEGORY, "Desktop Tablet Becomes Toolbar", getter, setter));
}
{
auto getter = []()->bool { return qApp->getHmdTabletBecomesToolbarSetting(); };
auto setter = [](bool value) { qApp->setHmdTabletBecomesToolbarSetting(value); };
preferences->addPreference(new CheckPreference(UI_CATEGORY, "HMD Tablet Becomes Toolbar", getter, setter));
}
{
auto getter = []()->bool { return qApp->getPreferAvatarFingerOverStylus(); };
auto setter = [](bool value) { qApp->setPreferAvatarFingerOverStylus(value); };
@ -343,30 +333,6 @@ void setupPreferences() {
preferences->addPreference(preference);
}
}
{
auto getter = []()->bool { return image::isColorTexturesCompressionEnabled(); };
auto setter = [](bool value) { return image::setColorTexturesCompressionEnabled(value); };
auto preference = new CheckPreference(RENDER, "Compress Color Textures", getter, setter);
preferences->addPreference(preference);
}
{
auto getter = []()->bool { return image::isNormalTexturesCompressionEnabled(); };
auto setter = [](bool value) { return image::setNormalTexturesCompressionEnabled(value); };
auto preference = new CheckPreference(RENDER, "Compress Normal Textures", getter, setter);
preferences->addPreference(preference);
}
{
auto getter = []()->bool { return image::isGrayscaleTexturesCompressionEnabled(); };
auto setter = [](bool value) { return image::setGrayscaleTexturesCompressionEnabled(value); };
auto preference = new CheckPreference(RENDER, "Compress Grayscale Textures", getter, setter);
preferences->addPreference(preference);
}
{
auto getter = []()->bool { return image::isCubeTexturesCompressionEnabled(); };
auto setter = [](bool value) { return image::setCubeTexturesCompressionEnabled(value); };
auto preference = new CheckPreference(RENDER, "Compress Cube Textures", getter, setter);
preferences->addPreference(preference);
}
}
{
static const QString RENDER("Networking");

View file

@ -15,6 +15,10 @@
#include <EntityTreeRenderer.h>
#include <NetworkingConstants.h>
#ifndef MIN
#define MIN(a,b) ((a) < (b) ? (a) : (b))
#endif
static const float CONTEXT_OVERLAY_TABLET_OFFSET = 30.0f; // Degrees
static const float CONTEXT_OVERLAY_TABLET_ORIENTATION = 210.0f; // Degrees
static const float CONTEXT_OVERLAY_TABLET_DISTANCE = 0.65F; // Meters
@ -38,11 +42,6 @@ ContextOverlayInterface::ContextOverlayInterface() {
_entityPropertyFlags += PROP_DIMENSIONS;
_entityPropertyFlags += PROP_REGISTRATION_POINT;
// initially, set _enabled to match the switch. Later we enable/disable via the getter/setters
// if we are in edit or pal (for instance). Note this is temporary, as we expect to enable this all
// the time after getting edge highlighting, etc...
_enabled = _settingSwitch.get();
auto entityTreeRenderer = DependencyManager::get<EntityTreeRenderer>().data();
connect(entityTreeRenderer, SIGNAL(mousePressOnEntity(const EntityItemID&, const PointerEvent&)), this, SLOT(createOrDestroyContextOverlay(const EntityItemID&, const PointerEvent&)));
connect(entityTreeRenderer, SIGNAL(hoverEnterEntity(const EntityItemID&, const PointerEvent&)), this, SLOT(contextOverlays_hoverEnterEntity(const EntityItemID&, const PointerEvent&)));
@ -65,40 +64,36 @@ ContextOverlayInterface::ContextOverlayInterface() {
connect(_selectionScriptingInterface.data(), &SelectionScriptingInterface::selectedItemsListChanged, &_selectionToSceneHandler, &SelectionToSceneHandler::selectedItemsListChanged);
}
static const uint32_t MOUSE_HW_ID = 0;
static const uint32_t LEFT_HAND_HW_ID = 1;
static const xColor CONTEXT_OVERLAY_COLOR = { 255, 255, 255 };
static const float CONTEXT_OVERLAY_INSIDE_DISTANCE = 1.0f; // in meters
static const float CONTEXT_OVERLAY_CLOSE_DISTANCE = 1.5f; // in meters
static const float CONTEXT_OVERLAY_CLOSE_SIZE = 0.12f; // in meters, same x and y dims
static const float CONTEXT_OVERLAY_FAR_SIZE = 0.08f; // in meters, same x and y dims
static const float CONTEXT_OVERLAY_CLOSE_OFFSET_ANGLE = 20.0f;
static const float CONTEXT_OVERLAY_SIZE = 0.09f; // in meters, same x and y dims
static const float CONTEXT_OVERLAY_OFFSET_DISTANCE = 0.1f;
static const float CONTEXT_OVERLAY_OFFSET_ANGLE = 5.0f;
static const float CONTEXT_OVERLAY_UNHOVERED_ALPHA = 0.85f;
static const float CONTEXT_OVERLAY_HOVERED_ALPHA = 1.0f;
static const float CONTEXT_OVERLAY_UNHOVERED_PULSEMIN = 0.6f;
static const float CONTEXT_OVERLAY_UNHOVERED_PULSEMAX = 1.0f;
static const float CONTEXT_OVERLAY_UNHOVERED_PULSEPERIOD = 1.0f;
static const float CONTEXT_OVERLAY_UNHOVERED_COLORPULSE = 1.0f;
static const float CONTEXT_OVERLAY_FAR_OFFSET = 0.1f;
void ContextOverlayInterface::setEnabled(bool enabled) {
// only enable/disable if the setting in 'on'. If it is 'off',
// make sure _enabled is always false.
if (_settingSwitch.get()) {
_enabled = enabled;
} else {
_enabled = false;
}
_enabled = enabled;
}
bool ContextOverlayInterface::createOrDestroyContextOverlay(const EntityItemID& entityItemID, const PointerEvent& event) {
if (_enabled && event.getButton() == PointerEvent::SecondaryButton) {
if (contextOverlayFilterPassed(entityItemID)) {
if (event.getID() == MOUSE_HW_ID) {
enableEntityHighlight(entityItemID);
}
qCDebug(context_overlay) << "Creating Context Overlay on top of entity with ID: " << entityItemID;
// Add all necessary variables to the stack
EntityItemProperties entityProperties = _entityScriptingInterface->getEntityProperties(entityItemID, _entityPropertyFlags);
glm::vec3 cameraPosition = qApp->getCamera().getPosition();
float distanceFromCameraToEntity = glm::distance(entityProperties.getPosition(), cameraPosition);
glm::vec3 entityDimensions = entityProperties.getDimensions();
glm::vec3 entityPosition = entityProperties.getPosition();
glm::vec3 contextOverlayPosition = entityProperties.getPosition();
@ -131,27 +126,22 @@ bool ContextOverlayInterface::createOrDestroyContextOverlay(const EntityItemID&
// If the camera is inside the box...
// ...position the Context Overlay 1 meter in front of the camera.
contextOverlayPosition = cameraPosition + CONTEXT_OVERLAY_INSIDE_DISTANCE * (qApp->getCamera().getOrientation() * Vectors::FRONT);
contextOverlayDimensions = glm::vec2(CONTEXT_OVERLAY_CLOSE_SIZE, CONTEXT_OVERLAY_CLOSE_SIZE) * glm::distance(contextOverlayPosition, cameraPosition);
} else if (distanceFromCameraToEntity < CONTEXT_OVERLAY_CLOSE_DISTANCE) {
// Else if the entity is too close to the camera...
// ...rotate the Context Overlay to the right of the entity.
// This makes it easy to inspect things you're holding.
float offsetAngle = -CONTEXT_OVERLAY_CLOSE_OFFSET_ANGLE;
if (event.getID() == LEFT_HAND_HW_ID) {
offsetAngle *= -1;
}
contextOverlayPosition = (glm::quat(glm::radians(glm::vec3(0.0f, offsetAngle, 0.0f))) * (entityPosition - cameraPosition)) + cameraPosition;
contextOverlayDimensions = glm::vec2(CONTEXT_OVERLAY_CLOSE_SIZE, CONTEXT_OVERLAY_CLOSE_SIZE) * glm::distance(contextOverlayPosition, cameraPosition);
contextOverlayDimensions = glm::vec2(CONTEXT_OVERLAY_SIZE, CONTEXT_OVERLAY_SIZE) * glm::distance(contextOverlayPosition, cameraPosition);
} else {
// Else, place the Context Overlay some offset away from the entity's bounding
// box in the direction of the camera.
// Rotate the Context Overlay some number of degrees offset from the entity
// along the line cast from your head to the entity's bounding box.
glm::vec3 direction = glm::normalize(entityPosition - cameraPosition);
float distance;
BoxFace face;
glm::vec3 normal;
boundingBox.findRayIntersection(cameraPosition, direction, distance, face, normal);
contextOverlayPosition = (cameraPosition + direction * distance) - direction * CONTEXT_OVERLAY_FAR_OFFSET;
contextOverlayDimensions = glm::vec2(CONTEXT_OVERLAY_FAR_SIZE, CONTEXT_OVERLAY_FAR_SIZE) * glm::distance(contextOverlayPosition, cameraPosition);
float offsetAngle = -CONTEXT_OVERLAY_OFFSET_ANGLE;
if (event.getID() == LEFT_HAND_HW_ID) {
offsetAngle *= -1;
}
contextOverlayPosition = (glm::quat(glm::radians(glm::vec3(0.0f, offsetAngle, 0.0f)))) *
((cameraPosition + direction * (distance - CONTEXT_OVERLAY_OFFSET_DISTANCE)));
contextOverlayDimensions = glm::vec2(CONTEXT_OVERLAY_SIZE, CONTEXT_OVERLAY_SIZE) * glm::distance(contextOverlayPosition, cameraPosition);
}
// Finally, setup and draw the Context Overlay
@ -176,6 +166,7 @@ bool ContextOverlayInterface::createOrDestroyContextOverlay(const EntityItemID&
}
} else {
if (!_currentEntityWithContextOverlay.isNull()) {
disableEntityHighlight(_currentEntityWithContextOverlay);
return destroyContextOverlay(_currentEntityWithContextOverlay, event);
}
return false;
@ -237,13 +228,13 @@ void ContextOverlayInterface::contextOverlays_hoverLeaveOverlay(const OverlayID&
}
void ContextOverlayInterface::contextOverlays_hoverEnterEntity(const EntityItemID& entityID, const PointerEvent& event) {
if (contextOverlayFilterPassed(entityID)) {
if (contextOverlayFilterPassed(entityID) && _enabled && event.getID() != MOUSE_HW_ID) {
enableEntityHighlight(entityID);
}
}
void ContextOverlayInterface::contextOverlays_hoverLeaveEntity(const EntityItemID& entityID, const PointerEvent& event) {
if (_currentEntityWithContextOverlay != entityID) {
if (_currentEntityWithContextOverlay != entityID && _enabled && event.getID() != MOUSE_HW_ID) {
disableEntityHighlight(entityID);
}
}

View file

@ -76,8 +76,6 @@ private:
bool _isInMarketplaceInspectionMode { false };
Setting::Handle<bool> _settingSwitch { "inspectionMode", false };
void openMarketplace();
void enableEntityHighlight(const EntityItemID& entityItemID);
void disableEntityHighlight(const EntityItemID& entityItemID);

View file

@ -47,9 +47,7 @@ void ModelOverlay::update(float deltatime) {
_updateModel = false;
_model->setSnapModelToCenter(true);
Transform transform = getTransform();
#ifndef USE_SN_SCALE
transform.setScale(1.0f); // disable inherited scale
#endif
if (_scaleToFit) {
_model->setScaleToFit(true, transform.getScale() * getDimensions());
} else {

View file

@ -41,9 +41,7 @@ void Sphere3DOverlay::render(RenderArgs* args) {
if (batch) {
// FIXME Start using the _renderTransform instead of calling for Transform and Dimensions from here, do the custom things needed in evalRenderTransform()
Transform transform = getTransform();
#ifndef USE_SN_SCALE
transform.setScale(1.0f); // ignore inherited scale from SpatiallyNestable
#endif
transform.postScale(getDimensions() * SPHERE_OVERLAY_SCALE);
batch->setModelTransform(transform);

View file

@ -17,6 +17,8 @@
#include <RenderDeferredTask.h>
#include <TextRenderer3D.h>
#include <AbstractViewStateInterface.h>
const int FIXED_FONT_POINT_SIZE = 40;
const int FIXED_FONT_SCALING_RATIO = FIXED_FONT_POINT_SIZE * 80.0f; // this is a ratio determined through experimentation
const float LINE_SCALE_RATIO = 1.2f;
@ -114,6 +116,7 @@ void Text3DOverlay::render(RenderArgs* args) {
glm::vec3 topLeft(-halfDimensions.x, -halfDimensions.y, SLIGHTLY_BEHIND);
glm::vec3 bottomRight(halfDimensions.x, halfDimensions.y, SLIGHTLY_BEHIND);
DependencyManager::get<GeometryCache>()->bindSimpleProgram(batch, false, quadColor.a < 1.0f, false, false, false, false);
DependencyManager::get<GeometryCache>()->renderQuad(batch, topLeft, bottomRight, quadColor, _geometryId);
// Same font properties as textSize()
@ -133,13 +136,8 @@ void Text3DOverlay::render(RenderArgs* args) {
glm::vec4 textColor = { _color.red / MAX_COLOR, _color.green / MAX_COLOR,
_color.blue / MAX_COLOR, getTextAlpha() };
// FIXME: Factor out textRenderer so that Text3DOverlay overlay parts can be grouped by pipeline
// for a gpu performance increase. Currently,
// Text renderer sets its own pipeline,
_textRenderer->draw(batch, 0, 0, getText(), textColor, glm::vec2(-1.0f), getDrawInFront());
// so before we continue, we must reset the pipeline
batch.setPipeline(args->_shapePipeline->pipeline);
args->_shapePipeline->prepare(batch, args);
// FIXME: Factor out textRenderer so that Text3DOverlay overlay parts can be grouped by pipeline for a gpu performance increase.
_textRenderer->draw(batch, 0, 0, getText(), textColor, glm::vec2(-1.0f), true);
}
const render::ShapeKey Text3DOverlay::getShapeKey() {
@ -160,7 +158,18 @@ void Text3DOverlay::setProperties(const QVariantMap& properties) {
auto textAlpha = properties["textAlpha"];
if (textAlpha.isValid()) {
float prevTextAlpha = getTextAlpha();
setTextAlpha(textAlpha.toFloat());
// Update our payload key if necessary to handle transparency
if ((prevTextAlpha < 1.0f && _textAlpha >= 1.0f) || (prevTextAlpha >= 1.0f && _textAlpha < 1.0f)) {
auto itemID = getRenderItemID();
if (render::Item::isValidID(itemID)) {
render::ScenePointer scene = AbstractViewStateInterface::instance()->getMain3DScene();
render::Transaction transaction;
transaction.updateItem(itemID);
scene->enqueueTransaction(transaction);
}
}
}
bool valid;

View file

@ -43,6 +43,7 @@ public:
xColor getBackgroundColor();
float getTextAlpha() { return _textAlpha; }
float getBackgroundAlpha() { return getAlpha(); }
bool isTransparent() override { return Overlay::isTransparent() || _textAlpha < 1.0f; }
// setters
void setText(const QString& text);

View file

@ -57,9 +57,7 @@ bool Volume3DOverlay::findRayIntersection(const glm::vec3& origin, const glm::ve
// extents is the entity relative, scaled, centered extents of the entity
glm::mat4 worldToEntityMatrix;
Transform transform = getTransform();
#ifndef USE_SN_SCALE
transform.setScale(1.0f); // ignore any inherited scale from SpatiallyNestable
#endif
transform.getInverseMatrix(worldToEntityMatrix);
glm::vec3 overlayFrameOrigin = glm::vec3(worldToEntityMatrix * glm::vec4(origin, 1.0f));

View file

@ -1755,14 +1755,6 @@ int AudioClient::setOutputBufferSize(int numFrames, bool persist) {
if (persist) {
_outputBufferSizeFrames.set(numFrames);
}
if (_audioOutput) {
// The buffer size can't be adjusted after QAudioOutput::start() has been called, so
// recreate the device by switching to the default.
QAudioDeviceInfo outputDeviceInfo = defaultAudioDeviceForMode(QAudio::AudioOutput);
qCDebug(audioclient) << __FUNCTION__ << "about to send changeDevice signal outputDeviceInfo: [" << outputDeviceInfo.deviceName() << "]";
emit changeDevice(outputDeviceInfo); // On correct thread, please, as setOutputBufferSize can be called from main thread.
}
}
return numFrames;
}

View file

@ -0,0 +1,10 @@
set(TARGET_NAME baking)
setup_hifi_library(Concurrent)
link_hifi_libraries(shared model networking ktx image fbx)
include_hifi_library_headers(gpu)
add_dependency_external_projects(draco)
find_package(Draco REQUIRED)
target_include_directories(${TARGET_NAME} SYSTEM PRIVATE ${DRACO_INCLUDE_DIRS})
target_link_libraries(${TARGET_NAME} ${DRACO_LIBRARY} ${DRACO_ENCODER_LIBRARY})

View file

@ -13,20 +13,49 @@
#include "Baker.h"
bool Baker::shouldStop() {
if (_shouldAbort) {
setWasAborted(true);
return true;
}
if (hasErrors()) {
return true;
}
return false;
}
void Baker::handleError(const QString& error) {
qCCritical(model_baking).noquote() << error;
_errorList.append(error);
emit finished();
setIsFinished(true);
}
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();
setIsFinished(true);
}
void Baker::handleWarning(const QString& warning) {
qCWarning(model_baking).noquote() << warning;
_warningList.append(warning);
}
void Baker::setIsFinished(bool isFinished) {
_isFinished.store(isFinished);
if (isFinished) {
emit finished();
}
}
void Baker::setWasAborted(bool wasAborted) {
_wasAborted.store(wasAborted);
if (wasAborted) {
emit aborted();
}
}

View file

@ -18,17 +18,30 @@ class Baker : public QObject {
Q_OBJECT
public:
bool shouldStop();
bool hasErrors() const { return !_errorList.isEmpty(); }
QStringList getErrors() const { return _errorList; }
bool hasWarnings() const { return !_warningList.isEmpty(); }
QStringList getWarnings() const { return _warningList; }
std::vector<QString> getOutputFiles() const { return _outputFiles; }
virtual void setIsFinished(bool isFinished);
bool isFinished() const { return _isFinished.load(); }
virtual void setWasAborted(bool wasAborted);
bool wasAborted() const { return _wasAborted.load(); }
public slots:
virtual void bake() = 0;
virtual void abort() { _shouldAbort.store(true); }
signals:
void finished();
void aborted();
protected:
void handleError(const QString& error);
@ -36,8 +49,17 @@ protected:
void handleErrors(const QStringList& errors);
// List of baked output files. For instance, for an FBX this would
// include the .fbx and all of its texture files.
std::vector<QString> _outputFiles;
QStringList _errorList;
QStringList _warningList;
std::atomic<bool> _isFinished { false };
std::atomic<bool> _shouldAbort { false };
std::atomic<bool> _wasAborted { false };
};
#endif // hifi_Baker_h

View file

@ -0,0 +1,821 @@
//
// FBXBaker.cpp
// tools/baking/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 <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 <PathUtils.h>
#include <FBXReader.h>
#include <FBXWriter.h>
#include "ModelBakingLoggingCategory.h"
#include "TextureBaker.h"
#include "FBXBaker.h"
#ifdef _WIN32
#pragma warning( push )
#pragma warning( disable : 4267 )
#endif
#include <draco/mesh/triangle_soup_mesh_builder.h>
#include <draco/compression/encode.h>
#ifdef _WIN32
#pragma warning( pop )
#endif
FBXBaker::FBXBaker(const QUrl& fbxURL, TextureBakerThreadGetter textureThreadGetter,
const QString& bakedOutputDir, const QString& originalOutputDir) :
_fbxURL(fbxURL),
_bakedOutputDir(bakedOutputDir),
_originalOutputDir(originalOutputDir),
_textureThreadGetter(textureThreadGetter)
{
}
void FBXBaker::abort() {
Baker::abort();
// tell our underlying TextureBaker instances to abort
// the FBXBaker will wait until all are aborted before emitting its own abort signal
for (auto& textureBaker : _bakingTextures) {
textureBaker->abort();
}
}
void FBXBaker::bake() {
qDebug() << "FBXBaker" << _fbxURL << "bake starting";
auto tempDir = PathUtils::generateTemporaryDir();
if (tempDir.isEmpty()) {
handleError("Failed to create a temporary directory.");
return;
}
_tempDir = tempDir;
_originalFBXFilePath = _tempDir.filePath(_fbxURL.fileName());
qDebug() << "Made temporary dir " << _tempDir;
qDebug() << "Origin file path: " << _originalFBXFilePath;
// setup the output folder for the results of this bake
setupOutputFolder();
if (shouldStop()) {
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 (shouldStop()) {
return;
}
// enumerate the models and textures found in the scene and start a bake for them
rewriteAndBakeSceneTextures();
if (shouldStop()) {
return;
}
rewriteAndBakeSceneModels();
if (shouldStop()) {
return;
}
// export the FBX with re-written texture references
exportScene();
if (shouldStop()) {
return;
}
// check if we're already done with textures (in case we had none to re-write)
checkIfTexturesFinished();
}
void FBXBaker::setupOutputFolder() {
// make sure there isn't already an output directory using the same name
if (QDir(_bakedOutputDir).exists()) {
qWarning() << "Output path" << _bakedOutputDir << "already exists. Continuing.";
} else {
qCDebug(model_baking) << "Creating FBX output folder" << _bakedOutputDir;
// attempt to make the output folder
if (!QDir().mkpath(_bakedOutputDir)) {
handleError("Failed to create FBX output folder " + _bakedOutputDir);
return;
}
// attempt to make the output folder
if (!QDir().mkpath(_originalOutputDir)) {
handleError("Failed to create FBX output folder " + _bakedOutputDir);
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() };
qDebug() << "Local file url: " << _fbxURL << _fbxURL.toString() << _fbxURL.toLocalFile() << ", copying to: " << _originalFBXFilePath;
if (!localFBX.exists()) {
//QMessageBox::warning(this, "Could not find " + _fbxURL.toString(), "");
handleError("Could not find " + _fbxURL.toString());
return;
}
// make a copy in the output folder
if (!_originalOutputDir.isEmpty()) {
qDebug() << "Copying to: " << _originalOutputDir << "/" << _fbxURL.fileName();
localFBX.copy(_originalOutputDir + "/" + _fbxURL.fileName());
}
localFBX.copy(_originalFBXFilePath);
// 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(_originalFBXFilePath);
qDebug(model_baking) << "Writing copy of original FBX to" << _originalFBXFilePath << copyOfOriginal.fileName();
if (!copyOfOriginal.open(QIODevice::WriteOnly)) {
// 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() + " (Failed to open " + _originalFBXFilePath + ")");
return;
}
if (copyOfOriginal.write(requestReply->readAll()) == -1) {
handleError("Could not create copy of " + _fbxURL.toString() + " (Failed to write)");
return;
}
// close that file now that we are done writing to it
copyOfOriginal.close();
if (!_originalOutputDir.isEmpty()) {
copyOfOriginal.copy(_originalOutputDir + "/" + _fbxURL.fileName());
}
// 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() {
qDebug() << "file path: " << _originalFBXFilePath.toLocal8Bit().data() << QDir(_originalFBXFilePath).exists();
QFile fbxFile(_originalFBXFilePath);
if (!fbxFile.open(QIODevice::ReadOnly)) {
handleError("Error opening " + _originalFBXFilePath + " for reading");
return;
}
FBXReader reader;
qCDebug(model_baking) << "Parsing" << _fbxURL;
_rootNode = reader._rootNode = reader.parseFBX(&fbxFile);
_geometry = reader.extractFBXGeometry({}, _fbxURL.toString());
_textureContent = reader._textureContent;
}
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, QString relativeFileName, bool isEmbedded) {
QUrl urlToTexture;
auto apparentRelativePath = QFileInfo(relativeFileName.replace("\\", "/"));
if (isEmbedded) {
urlToTexture = _fbxURL.toString() + "/" + apparentRelativePath.filePath();
} else {
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
// 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;
}
void FBXBaker::rewriteAndBakeSceneModels() {
unsigned int meshIndex = 0;
bool hasDeformers { false };
for (FBXNode& rootChild : _rootNode.children) {
if (rootChild.name == "Objects") {
for (FBXNode& objectChild : rootChild.children) {
if (objectChild.name == "Deformer") {
hasDeformers = true;
break;
}
}
}
if (hasDeformers) {
break;
}
}
for (FBXNode& rootChild : _rootNode.children) {
if (rootChild.name == "Objects") {
for (FBXNode& objectChild : rootChild.children) {
if (objectChild.name == "Geometry") {
// TODO Pull this out of _geometry instead so we don't have to reprocess it
auto extractedMesh = FBXReader::extractMesh(objectChild, meshIndex, false);
auto& mesh = extractedMesh.mesh;
if (mesh.wasCompressed) {
handleError("Cannot re-bake a file that contains compressed mesh");
return;
}
Q_ASSERT(mesh.normals.size() == 0 || mesh.normals.size() == mesh.vertices.size());
Q_ASSERT(mesh.colors.size() == 0 || mesh.colors.size() == mesh.vertices.size());
Q_ASSERT(mesh.texCoords.size() == 0 || mesh.texCoords.size() == mesh.vertices.size());
int64_t numTriangles { 0 };
for (auto& part : mesh.parts) {
if ((part.quadTrianglesIndices.size() % 3) != 0 || (part.triangleIndices.size() % 3) != 0) {
handleWarning("Found a mesh part with invalid index data, skipping");
continue;
}
numTriangles += part.quadTrianglesIndices.size() / 3;
numTriangles += part.triangleIndices.size() / 3;
}
if (numTriangles == 0) {
continue;
}
draco::TriangleSoupMeshBuilder meshBuilder;
meshBuilder.Start(numTriangles);
bool hasNormals { mesh.normals.size() > 0 };
bool hasColors { mesh.colors.size() > 0 };
bool hasTexCoords { mesh.texCoords.size() > 0 };
bool hasTexCoords1 { mesh.texCoords1.size() > 0 };
bool hasPerFaceMaterials { mesh.parts.size() > 1 };
bool needsOriginalIndices { hasDeformers };
int normalsAttributeID { -1 };
int colorsAttributeID { -1 };
int texCoordsAttributeID { -1 };
int texCoords1AttributeID { -1 };
int faceMaterialAttributeID { -1 };
int originalIndexAttributeID { -1 };
const int positionAttributeID = meshBuilder.AddAttribute(draco::GeometryAttribute::POSITION,
3, draco::DT_FLOAT32);
if (needsOriginalIndices) {
originalIndexAttributeID = meshBuilder.AddAttribute(
(draco::GeometryAttribute::Type)DRACO_ATTRIBUTE_ORIGINAL_INDEX,
1, draco::DT_INT32);
}
if (hasNormals) {
normalsAttributeID = meshBuilder.AddAttribute(draco::GeometryAttribute::NORMAL,
3, draco::DT_FLOAT32);
}
if (hasColors) {
colorsAttributeID = meshBuilder.AddAttribute(draco::GeometryAttribute::COLOR,
3, draco::DT_FLOAT32);
}
if (hasTexCoords) {
texCoordsAttributeID = meshBuilder.AddAttribute(draco::GeometryAttribute::TEX_COORD,
2, draco::DT_FLOAT32);
}
if (hasTexCoords1) {
texCoords1AttributeID = meshBuilder.AddAttribute(
(draco::GeometryAttribute::Type)DRACO_ATTRIBUTE_TEX_COORD_1,
2, draco::DT_FLOAT32);
}
if (hasPerFaceMaterials) {
faceMaterialAttributeID = meshBuilder.AddAttribute(
(draco::GeometryAttribute::Type)DRACO_ATTRIBUTE_MATERIAL_ID,
1, draco::DT_UINT16);
}
auto partIndex = 0;
draco::FaceIndex face;
for (auto& part : mesh.parts) {
const auto& matTex = extractedMesh.partMaterialTextures[partIndex];
uint16_t materialID = matTex.first;
auto addFace = [&](QVector<int>& indices, int index, draco::FaceIndex face) {
int32_t idx0 = indices[index];
int32_t idx1 = indices[index + 1];
int32_t idx2 = indices[index + 2];
if (hasPerFaceMaterials) {
meshBuilder.SetPerFaceAttributeValueForFace(faceMaterialAttributeID, face, &materialID);
}
meshBuilder.SetAttributeValuesForFace(positionAttributeID, face,
&mesh.vertices[idx0], &mesh.vertices[idx1],
&mesh.vertices[idx2]);
if (needsOriginalIndices) {
meshBuilder.SetAttributeValuesForFace(originalIndexAttributeID, face,
&mesh.originalIndices[idx0],
&mesh.originalIndices[idx1],
&mesh.originalIndices[idx2]);
}
if (hasNormals) {
meshBuilder.SetAttributeValuesForFace(normalsAttributeID, face,
&mesh.normals[idx0], &mesh.normals[idx1],
&mesh.normals[idx2]);
}
if (hasColors) {
meshBuilder.SetAttributeValuesForFace(colorsAttributeID, face,
&mesh.colors[idx0], &mesh.colors[idx1],
&mesh.colors[idx2]);
}
if (hasTexCoords) {
meshBuilder.SetAttributeValuesForFace(texCoordsAttributeID, face,
&mesh.texCoords[idx0], &mesh.texCoords[idx1],
&mesh.texCoords[idx2]);
}
if (hasTexCoords1) {
meshBuilder.SetAttributeValuesForFace(texCoords1AttributeID, face,
&mesh.texCoords1[idx0], &mesh.texCoords1[idx1],
&mesh.texCoords1[idx2]);
}
};
for (int i = 0; (i + 2) < part.quadTrianglesIndices.size(); i += 3) {
addFace(part.quadTrianglesIndices, i, face++);
}
for (int i = 0; (i + 2) < part.triangleIndices.size(); i += 3) {
addFace(part.triangleIndices, i, face++);
}
partIndex++;
}
auto dracoMesh = meshBuilder.Finalize();
if (!dracoMesh) {
handleWarning("Failed to finalize the baking of a draco Geometry node");
continue;
}
// we need to modify unique attribute IDs for custom attributes
// so the attributes are easily retrievable on the other side
if (hasPerFaceMaterials) {
dracoMesh->attribute(faceMaterialAttributeID)->set_unique_id(DRACO_ATTRIBUTE_MATERIAL_ID);
}
if (hasTexCoords1) {
dracoMesh->attribute(texCoords1AttributeID)->set_unique_id(DRACO_ATTRIBUTE_TEX_COORD_1);
}
if (needsOriginalIndices) {
dracoMesh->attribute(originalIndexAttributeID)->set_unique_id(DRACO_ATTRIBUTE_ORIGINAL_INDEX);
}
draco::Encoder encoder;
encoder.SetAttributeQuantization(draco::GeometryAttribute::POSITION, 14);
encoder.SetAttributeQuantization(draco::GeometryAttribute::TEX_COORD, 12);
encoder.SetAttributeQuantization(draco::GeometryAttribute::NORMAL, 10);
encoder.SetSpeedOptions(0, 5);
draco::EncoderBuffer buffer;
encoder.EncodeMeshToBuffer(*dracoMesh, &buffer);
FBXNode dracoMeshNode;
dracoMeshNode.name = "DracoMesh";
auto value = QVariant::fromValue(QByteArray(buffer.data(), (int) buffer.size()));
dracoMeshNode.properties.append(value);
objectChild.children.push_back(dracoMeshNode);
static const std::vector<QString> nodeNamesToDelete {
// Node data that is packed into the draco mesh
"Vertices",
"PolygonVertexIndex",
"LayerElementNormal",
"LayerElementColor",
"LayerElementUV",
"LayerElementMaterial",
"LayerElementTexture",
// Node data that we don't support
"Edges",
"LayerElementTangent",
"LayerElementBinormal",
"LayerElementSmoothing"
};
auto& children = objectChild.children;
auto it = children.begin();
while (it != children.end()) {
auto begin = nodeNamesToDelete.begin();
auto end = nodeNamesToDelete.end();
if (find(begin, end, it->name) != end) {
it = children.erase(it);
} else {
++it;
}
}
}
}
}
}
}
void FBXBaker::rewriteAndBakeSceneTextures() {
using namespace image::TextureUsage;
QHash<QString, image::TextureUsage::Type> textureTypes;
// enumerate the materials in the extracted geometry so we can determine the texture type for each texture ID
for (const auto& material : _geometry->materials) {
if (material.normalTexture.isBumpmap) {
textureTypes[material.normalTexture.id] = BUMP_TEXTURE;
} else {
textureTypes[material.normalTexture.id] = NORMAL_TEXTURE;
}
textureTypes[material.albedoTexture.id] = ALBEDO_TEXTURE;
textureTypes[material.glossTexture.id] = GLOSS_TEXTURE;
textureTypes[material.roughnessTexture.id] = ROUGHNESS_TEXTURE;
textureTypes[material.specularTexture.id] = SPECULAR_TEXTURE;
textureTypes[material.metallicTexture.id] = METALLIC_TEXTURE;
textureTypes[material.emissiveTexture.id] = EMISSIVE_TEXTURE;
textureTypes[material.occlusionTexture.id] = OCCLUSION_TEXTURE;
textureTypes[material.lightmapTexture.id] = LIGHTMAP_TEXTURE;
}
// enumerate the children of the root node
for (FBXNode& rootChild : _rootNode.children) {
if (rootChild.name == "Objects") {
// enumerate the objects
auto object = rootChild.children.begin();
while (object != rootChild.children.end()) {
if (object->name == "Texture") {
// double check that we didn't get an abort while baking the last texture
if (shouldStop()) {
return;
}
// enumerate the texture children
for (FBXNode& textureChild : object->children) {
if (textureChild.name == "RelativeFilename") {
// use QFileInfo to easily split up the existing texture filename into its components
QString fbxTextureFileName { textureChild.properties.at(0).toByteArray() };
QFileInfo textureFileInfo { fbxTextureFileName.replace("\\", "/") };
if (textureFileInfo.suffix() == BAKED_TEXTURE_EXT.mid(1)) {
// re-baking an FBX that already references baked textures is a fail
// so we add an error and return from here
handleError("Cannot re-bake a file that references compressed textures");
return;
}
// make sure this texture points to something and isn't one we've already re-mapped
if (!textureFileInfo.filePath().isEmpty()) {
// check if this was an embedded texture we have already have in-memory content for
auto textureContent = _textureContent.value(fbxTextureFileName.toLocal8Bit());
// figure out the URL to this texture, embedded or external
auto urlToTexture = getTextureURL(textureFileInfo, fbxTextureFileName,
!textureContent.isNull());
QString bakedTextureFileName;
if (_remappedTexturePaths.contains(urlToTexture)) {
bakedTextureFileName = _remappedTexturePaths[urlToTexture];
} else {
// 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
bakedTextureFileName = createBakedTextureFileName(textureFileInfo);
_remappedTexturePaths[urlToTexture] = bakedTextureFileName;
}
qCDebug(model_baking).noquote() << "Re-mapping" << fbxTextureFileName
<< "to" << bakedTextureFileName;
QString bakedTextureFilePath {
_bakedOutputDir + "/" + bakedTextureFileName
};
// write the new filename into the FBX scene
textureChild.properties[0] = bakedTextureFileName.toLocal8Bit();
if (!_bakingTextures.contains(urlToTexture)) {
_outputFiles.push_back(bakedTextureFilePath);
// grab the ID for this texture so we can figure out the
// texture type from the loaded materials
QString textureID { object->properties[0].toByteArray() };
auto textureType = textureTypes[textureID];
// bake this texture asynchronously
bakeTexture(urlToTexture, textureType, _bakedOutputDir, bakedTextureFileName, textureContent);
}
}
}
}
++object;
} else if (object->name == "Video") {
// this is an embedded texture, we need to remove it from the FBX
object = rootChild.children.erase(object);
} else {
++object;
}
}
}
}
}
void FBXBaker::bakeTexture(const QUrl& textureURL, image::TextureUsage::Type textureType,
const QDir& outputDir, const QString& bakedFilename, const QByteArray& textureContent) {
// start a bake for this texture and add it to our list to keep track of
QSharedPointer<TextureBaker> bakingTexture {
new TextureBaker(textureURL, textureType, outputDir, bakedFilename, textureContent),
&TextureBaker::deleteLater
};
// make sure we hear when the baking texture is done or aborted
connect(bakingTexture.data(), &Baker::finished, this, &FBXBaker::handleBakedTexture);
connect(bakingTexture.data(), &TextureBaker::aborted, this, &FBXBaker::handleAbortedTexture);
// 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 (!shouldStop()) {
if (!bakedTexture->hasErrors()) {
if (!_originalOutputDir.isEmpty()) {
// 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 a folder with the name of the FBX
// since that is the fake URL we provide when baking external textures
if (!_fbxURL.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 {
_originalOutputDir + "/" + 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());
// abort any other ongoing texture bakes since we know we'll end up failing
for (auto& bakingTexture : _bakingTextures) {
bakingTexture->abort();
}
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::handleAbortedTexture() {
// grab the texture bake that was aborted and remove it from our hash since we don't need to track it anymore
TextureBaker* bakedTexture = qobject_cast<TextureBaker*>(sender());
if (bakedTexture) {
_bakingTextures.remove(bakedTexture->getTextureURL());
}
// since a texture we were baking aborted, our status is also aborted
_shouldAbort.store(true);
// abort any other ongoing texture bakes since we know we'll end up failing
for (auto& bakingTexture : _bakingTextures) {
bakingTexture->abort();
}
checkIfTexturesFinished();
}
void FBXBaker::exportScene() {
// save the relative path to this FBX inside our passed output folder
auto fileName = _fbxURL.fileName();
auto baseName = fileName.left(fileName.lastIndexOf('.'));
auto bakedFilename = baseName + BAKED_FBX_EXTENSION;
_bakedFBXFilePath = _bakedOutputDir + "/" + bakedFilename;
auto fbxData = FBXWriter::encodeFBX(_rootNode);
QFile bakedFile(_bakedFBXFilePath);
if (!bakedFile.open(QIODevice::WriteOnly)) {
handleError("Error opening " + _bakedFBXFilePath + " for writing");
return;
}
bakedFile.write(fbxData);
_outputFiles.push_back(_bakedFBXFilePath);
qCDebug(model_baking) << "Exported" << _fbxURL << "with re-written paths to" << _bakedFBXFilePath;
}
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()) {
if (shouldStop()) {
// if we're checking for completion but we have errors
// that means one or more of our texture baking operations failed
if (_pendingErrorEmission) {
setIsFinished(true);
}
return;
} else {
qCDebug(model_baking) << "Finished baking, emitting finsihed" << _fbxURL;
setIsFinished(true);
}
}
}
void FBXBaker::setWasAborted(bool wasAborted) {
if (wasAborted != _wasAborted.load()) {
Baker::setWasAborted(wasAborted);
if (wasAborted) {
qCDebug(model_baking) << "Aborted baking" << _fbxURL;
}
}
}

View file

@ -1,6 +1,6 @@
//
// FBXBaker.h
// tools/oven/src
// tools/baking/src
//
// Created by Stephen Birarda on 3/30/17.
// Copyright 2017 High Fidelity, Inc.
@ -20,33 +20,30 @@
#include "Baker.h"
#include "TextureBaker.h"
#include "ModelBakingLoggingCategory.h"
#include <gpu/Texture.h>
namespace fbxsdk {
class FbxManager;
class FbxProperty;
class FbxScene;
class FbxFileTexture;
}
#include <FBX.h>
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);
FBXBaker(const QUrl& fbxURL, TextureBakerThreadGetter textureThreadGetter,
const QString& bakedOutputDir, const QString& originalOutputDir = "");
QUrl getFBXUrl() const { return _fbxURL; }
QString getBakedFBXRelativePath() const { return _bakedFBXRelativePath; }
QString getBakedFBXFilePath() const { return _bakedFBXFilePath; }
virtual void setWasAborted(bool wasAborted) override;
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;
virtual void abort() override;
signals:
void sourceCopyReadyToLoad();
@ -55,46 +52,49 @@ private slots:
void bakeSourceCopy();
void handleFBXNetworkReply();
void handleBakedTexture();
void handleAbortedTexture();
private:
void setupOutputFolder();
void loadSourceFBX();
void bakeCopiedFBX();
void importScene();
void rewriteAndBakeSceneModels();
void rewriteAndBakeSceneTextures();
void exportScene();
void removeEmbeddedMediaFolder();
void possiblyCleanupOriginals();
void checkIfTexturesFinished();
QString createBakedTextureFileName(const QFileInfo& textureFileInfo);
QUrl getTextureURL(const QFileInfo& textureFileInfo, fbxsdk::FbxFileTexture* fileTexture);
QUrl getTextureURL(const QFileInfo& textureFileInfo, QString relativeFileName, bool isEmbedded = false);
void bakeTexture(const QUrl& textureURL, image::TextureUsage::Type textureType, const QDir& outputDir);
QString pathToCopyOfOriginal() const;
void bakeTexture(const QUrl& textureURL, image::TextureUsage::Type textureType, const QDir& outputDir,
const QString& bakedFilename, const QByteArray& textureContent = QByteArray());
QUrl _fbxURL;
QString _fbxName;
QString _baseOutputPath;
QString _uniqueOutputPath;
QString _bakedFBXRelativePath;
static FBXSDKManagerUniquePointer _sdkManager;
fbxsdk::FbxScene* _scene { nullptr };
FBXNode _rootNode;
FBXGeometry* _geometry;
QHash<QByteArray, QByteArray> _textureContent;
QString _bakedFBXFilePath;
QString _bakedOutputDir;
// If set, the original FBX and textures will also be copied here
QString _originalOutputDir;
QDir _tempDir;
QString _originalFBXFilePath;
QMultiHash<QUrl, QSharedPointer<TextureBaker>> _bakingTextures;
QHash<QString, int> _textureNameMatchCount;
QHash<QUrl, QString> _remappedTexturePaths;
TextureBakerThreadGetter _textureThreadGetter;
bool _copyOriginals { true };
bool _pendingErrorEmission { false };
};

View file

@ -25,22 +25,48 @@
const QString BAKED_TEXTURE_EXT = ".ktx";
TextureBaker::TextureBaker(const QUrl& textureURL, image::TextureUsage::Type textureType, const QDir& outputDirectory) :
TextureBaker::TextureBaker(const QUrl& textureURL, image::TextureUsage::Type textureType,
const QDir& outputDirectory, const QString& bakedFilename,
const QByteArray& textureContent) :
_textureURL(textureURL),
_originalTexture(textureContent),
_textureType(textureType),
_outputDirectory(outputDirectory)
_outputDirectory(outputDirectory),
_bakedTextureFileName(bakedFilename)
{
// figure out the baked texture filename
auto originalFilename = textureURL.fileName();
_bakedTextureFileName = originalFilename.left(originalFilename.lastIndexOf('.')) + BAKED_TEXTURE_EXT;
if (bakedFilename.isEmpty()) {
// 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();
if (_originalTexture.isEmpty()) {
// first load the texture (either locally or remotely)
loadTexture();
} else {
// we already have a texture passed to us, use that
emit originalTextureLoaded();
}
}
void TextureBaker::abort() {
Baker::abort();
// flip our atomic bool so any ongoing texture processing is stopped
_abortProcessing.store(true);
}
const QStringList TextureBaker::getSupportedFormats() {
auto formats = QImageReader::supportedImageFormats();
QStringList stringFormats;
std::transform(formats.begin(), formats.end(), std::back_inserter(stringFormats),
[](QByteArray& format) -> QString { return format; });
return stringFormats;
}
void TextureBaker::loadTexture() {
@ -96,7 +122,11 @@ void TextureBaker::handleTextureNetworkReply() {
void TextureBaker::processTexture() {
auto processedTexture = image::processImage(_originalTexture, _textureURL.toString().toStdString(),
ABSOLUTE_MAX_TEXTURE_NUM_PIXELS, _textureType);
ABSOLUTE_MAX_TEXTURE_NUM_PIXELS, _textureType, _abortProcessing);
if (shouldStop()) {
return;
}
if (!processedTexture) {
handleError("Could not process texture " + _textureURL.toString());
@ -120,12 +150,21 @@ void TextureBaker::processTexture() {
const size_t length = memKTX->_storage->size();
// attempt to write the baked texture to the destination file path
QFile bakedTextureFile { _outputDirectory.absoluteFilePath(_bakedTextureFileName) };
auto filePath = _outputDirectory.absoluteFilePath(_bakedTextureFileName);
QFile bakedTextureFile { filePath };
if (!bakedTextureFile.open(QIODevice::WriteOnly) || bakedTextureFile.write(data, length) == -1) {
handleError("Could not write baked texture for " + _textureURL.toString());
} else {
_outputFiles.push_back(filePath);
}
qCDebug(model_baking) << "Baked texture" << _textureURL;
emit finished();
setIsFinished(true);
}
void TextureBaker::setWasAborted(bool wasAborted) {
Baker::setWasAborted(wasAborted);
qCDebug(model_baking) << "Aborted baking" << _textureURL;
}

View file

@ -15,6 +15,7 @@
#include <QtCore/QObject>
#include <QtCore/QUrl>
#include <QtCore/QRunnable>
#include <QImageReader>
#include <image/Image.h>
@ -26,7 +27,11 @@ class TextureBaker : public Baker {
Q_OBJECT
public:
TextureBaker(const QUrl& textureURL, image::TextureUsage::Type textureType, const QDir& outputDirectory);
TextureBaker(const QUrl& textureURL, image::TextureUsage::Type textureType,
const QDir& outputDirectory, const QString& bakedFilename = QString(),
const QByteArray& textureContent = QByteArray());
static const QStringList getSupportedFormats();
const QByteArray& getOriginalTexture() const { return _originalTexture; }
@ -35,8 +40,11 @@ public:
QString getDestinationFilePath() const { return _outputDirectory.absoluteFilePath(_bakedTextureFileName); }
QString getBakedTextureFileName() const { return _bakedTextureFileName; }
virtual void setWasAborted(bool wasAborted) override;
public slots:
virtual void bake() override;
virtual void abort() override;
signals:
void originalTextureLoaded();
@ -54,6 +62,8 @@ private:
QDir _outputDirectory;
QString _bakedTextureFileName;
std::atomic<bool> _abortProcessing { false };
};
#endif // hifi_TextureBaker_h

View file

@ -441,7 +441,7 @@ void HmdDisplayPlugin::OverlayRenderer::updatePipeline() {
this->uniformsLocation = program->getUniformBuffers().findLocation("overlayBuffer");
gpu::StatePointer state = gpu::StatePointer(new gpu::State());
state->setDepthTest(gpu::State::DepthTest(true, true, gpu::LESS_EQUAL));
state->setDepthTest(gpu::State::DepthTest(false, false, gpu::LESS_EQUAL));
state->setBlendFunction(true,
gpu::State::SRC_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::INV_SRC_ALPHA,
gpu::State::FACTOR_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::ONE);

View file

@ -159,6 +159,88 @@ void EntityTreeRenderer::shutdown() {
clear(); // always clear() on shutdown
}
void EntityTreeRenderer::addPendingEntities(const render::ScenePointer& scene, render::Transaction& transaction) {
// Clear any expired entities
// FIXME should be able to use std::remove_if, but it fails due to some
// weird compilation error related to EntityItemID assignment operators
for (auto itr = _entitiesToAdd.begin(); _entitiesToAdd.end() != itr; ) {
if (itr->second.expired()) {
_entitiesToAdd.erase(itr++);
} else {
++itr;
}
}
if (!_entitiesToAdd.empty()) {
std::unordered_set<EntityItemID> processedIds;
for (const auto& entry : _entitiesToAdd) {
auto entity = entry.second.lock();
if (!entity) {
continue;
}
// Path to the parent transforms is not valid,
// don't add to the scene graph yet
if (!entity->isParentPathComplete()) {
continue;
}
auto entityID = entity->getEntityItemID();
processedIds.insert(entityID);
auto renderable = EntityRenderer::addToScene(*this, entity, scene, transaction);
if (renderable) {
_entitiesInScene.insert({ entityID, renderable });
}
}
if (!processedIds.empty()) {
for (const auto& processedId : processedIds) {
_entitiesToAdd.erase(processedId);
}
}
}
}
void EntityTreeRenderer::updateChangedEntities(const render::ScenePointer& scene, render::Transaction& transaction) {
std::unordered_set<EntityItemID> changedEntities;
_changedEntitiesGuard.withWriteLock([&] {
#if 0
// FIXME Weird build failure in latest VC update that fails to compile when using std::swap
changedEntities.swap(_changedEntities);
#else
changedEntities.insert(_changedEntities.begin(), _changedEntities.end());
_changedEntities.clear();
#endif
});
for (const auto& entityId : changedEntities) {
auto renderable = renderableForEntityId(entityId);
if (!renderable) {
continue;
}
_renderablesToUpdate.insert({ entityId, renderable });
}
// NOTE: Looping over all the entity renderers is likely to be a bottleneck in the future
// Currently, this is necessary because the model entity loading logic requires constant polling
// This was working fine because the entity server used to send repeated updates as your view changed,
// but with the improved entity server logic (PR 11141), updateInScene (below) would not be triggered enough
for (const auto& entry : _entitiesInScene) {
const auto& renderable = entry.second;
if (renderable) {
renderable->update(scene, transaction);
}
}
if (!_renderablesToUpdate.empty()) {
for (const auto& entry : _renderablesToUpdate) {
const auto& renderable = entry.second;
renderable->updateInScene(scene, transaction);
}
_renderablesToUpdate.clear();
}
}
void EntityTreeRenderer::update(bool simulate) {
PerformanceTimer perfTimer("ETRupdate");
if (_tree && !_shuttingDown) {
@ -178,30 +260,11 @@ void EntityTreeRenderer::update(bool simulate) {
}
}
std::unordered_set<EntityItemID> changedEntities;
// FIXME Weird build failure in latest VC update that fails to compile when using std::swap
_changedEntitiesGuard.withWriteLock([&] {
changedEntities.insert(_changedEntities.begin(), _changedEntities.end());
_changedEntities.clear();
});
for (const auto& entityId : changedEntities) {
auto renderable = renderableForEntityId(entityId);
if (!renderable) {
continue;
}
_renderablesToUpdate.insert({ entityId, renderable });
}
auto scene = _viewState->getMain3DScene();
if (scene && !_renderablesToUpdate.empty()) {
if (scene) {
render::Transaction transaction;
for (const auto& entry : _renderablesToUpdate) {
const auto& renderable = entry.second;
renderable->updateInScene(scene, transaction);
}
_renderablesToUpdate.clear();
addPendingEntities(scene, transaction);
updateChangedEntities(scene, transaction);
scene->enqueueTransaction(transaction);
}
}
@ -689,8 +752,12 @@ void EntityTreeRenderer::mouseMoveEvent(QMouseEvent* event) {
}
void EntityTreeRenderer::deletingEntity(const EntityItemID& entityID) {
// If it's in the pending queue, remove it
_entitiesToAdd.erase(entityID);
auto itr = _entitiesInScene.find(entityID);
if (_entitiesInScene.end() == itr) {
// Not in the scene, and no longer potentially in the pending queue, we're done
return;
}
@ -725,25 +792,10 @@ void EntityTreeRenderer::addingEntity(const EntityItemID& entityID) {
checkAndCallPreload(entityID);
auto entity = std::static_pointer_cast<EntityTree>(_tree)->findEntityByID(entityID);
if (entity) {
addEntityToScene(entity);
_entitiesToAdd.insert({ entity->getEntityItemID(), entity });
}
}
void EntityTreeRenderer::addEntityToScene(const EntityItemPointer& entity) {
// here's where we add the entity payload to the scene
auto scene = _viewState->getMain3DScene();
if (!scene) {
qCWarning(entitiesrenderer) << "EntityTreeRenderer::addEntityToScene(), Unexpected null scene, possibly during application shutdown";
return;
}
auto renderable = EntityRenderer::addToScene(*this, entity, scene);
if (renderable) {
_entitiesInScene[entity->getEntityItemID()] = renderable;
}
}
void EntityTreeRenderer::entityScriptChanging(const EntityItemID& entityID, bool reload) {
checkAndCallPreload(entityID, reload, true);
}

View file

@ -36,6 +36,7 @@ namespace render { namespace entities {
class EntityRenderer;
using EntityRendererPointer = std::shared_ptr<EntityRenderer>;
using EntityRendererWeakPointer = std::weak_ptr<EntityRenderer>;
} }
// Allow the use of std::unordered_map with QUuid keys
@ -157,12 +158,13 @@ protected:
}
private:
void addPendingEntities(const render::ScenePointer& scene, render::Transaction& transaction);
void updateChangedEntities(const render::ScenePointer& scene, render::Transaction& transaction);
EntityRendererPointer renderableForEntity(const EntityItemPointer& entity) const { return renderableForEntityId(entity->getID()); }
render::ItemID renderableIdForEntity(const EntityItemPointer& entity) const { return renderableIdForEntityId(entity->getID()); }
void resetEntitiesScriptEngine();
void addEntityToScene(const EntityItemPointer& entity);
bool findBestZoneAndMaybeContainingEntities(QVector<EntityItemID>* entitiesContainingAvatar = nullptr);
bool applyLayeredZones();
@ -260,6 +262,7 @@ private:
std::unordered_map<EntityItemID, EntityRendererPointer> _renderablesToUpdate;
std::unordered_map<EntityItemID, EntityRendererPointer> _entitiesInScene;
std::unordered_map<EntityItemID, EntityItemWeakPointer> _entitiesToAdd;
// For Scene.shouldRenderEntities
QList<EntityItemID> _entityIDsLastInScene;

View file

@ -187,7 +187,7 @@ void EntityRenderer::render(RenderArgs* args) {
// Methods called by the EntityTreeRenderer
//
EntityRenderer::Pointer EntityRenderer::addToScene(EntityTreeRenderer& renderer, const EntityItemPointer& entity, const ScenePointer& scene) {
EntityRenderer::Pointer EntityRenderer::addToScene(EntityTreeRenderer& renderer, const EntityItemPointer& entity, const ScenePointer& scene, Transaction& transaction) {
EntityRenderer::Pointer result;
if (!entity) {
return result;
@ -245,9 +245,7 @@ EntityRenderer::Pointer EntityRenderer::addToScene(EntityTreeRenderer& renderer,
}
if (result) {
Transaction transaction;
result->addToScene(scene, transaction);
scene->enqueueTransaction(transaction);
}
return result;
@ -293,6 +291,18 @@ void EntityRenderer::updateInScene(const ScenePointer& scene, Transaction& trans
});
}
void EntityRenderer::update(const ScenePointer& scene, Transaction& transaction) {
if (!isValidRenderItem()) {
return;
}
if (!needsUpdate()) {
return;
}
doUpdate(scene, transaction, _entity);
}
//
// Internal methods
//
@ -306,6 +316,11 @@ bool EntityRenderer::needsRenderUpdate() const {
return needsRenderUpdateFromEntity(_entity);
}
// Returns true if the item needs to have update called
bool EntityRenderer::needsUpdate() const {
return needsUpdateFromEntity(_entity);
}
// Returns true if the item in question needs to have updateInScene called because of changes in the entity
bool EntityRenderer::needsRenderUpdateFromEntity(const EntityItemPointer& entity) const {
bool success = false;

View file

@ -30,7 +30,7 @@ class EntityRenderer : public QObject, public std::enable_shared_from_this<Entit
public:
static void initEntityRenderers();
static Pointer addToScene(EntityTreeRenderer& renderer, const EntityItemPointer& entity, const ScenePointer& scene);
static Pointer addToScene(EntityTreeRenderer& renderer, const EntityItemPointer& entity, const ScenePointer& scene, Transaction& transaction);
// Allow classes to override this to interact with the user
virtual bool wantsHandControllerPointerEvents() const { return false; }
@ -49,6 +49,8 @@ public:
virtual bool addToScene(const ScenePointer& scene, Transaction& transaction) final;
virtual void removeFromScene(const ScenePointer& scene, Transaction& transaction);
virtual void update(const ScenePointer& scene, Transaction& transaction);
protected:
virtual bool needsRenderUpdateFromEntity() const final { return needsRenderUpdateFromEntity(_entity); }
virtual void onAddToScene(const EntityItemPointer& entity);
@ -71,6 +73,12 @@ protected:
// Returns true if the item in question needs to have updateInScene called because of changes in the entity
virtual bool needsRenderUpdateFromEntity(const EntityItemPointer& entity) const;
// Returns true if the item in question needs to have update called
virtual bool needsUpdate() const;
// Returns true if the item in question needs to have update called because of changes in the entity
virtual bool needsUpdateFromEntity(const EntityItemPointer& entity) const { return false; }
// Will be called on the main thread from updateInScene. This can be used to fetch things like
// network textures or model geometry from resource caches
virtual void doRenderUpdateSynchronous(const ScenePointer& scene, Transaction& transaction, const EntityItemPointer& entity) { }
@ -80,6 +88,8 @@ protected:
// data in this method if using multi-threaded rendering
virtual void doRenderUpdateAsynchronous(const EntityItemPointer& entity);
virtual void doUpdate(const ScenePointer& scene, Transaction& transaction, const EntityItemPointer& entity) { }
// Called by the `render` method after `needsRenderUpdate`
virtual void doRender(RenderArgs* args) = 0;
@ -148,6 +158,15 @@ protected:
onRemoveFromSceneTyped(_typedEntity);
}
using Parent::needsUpdateFromEntity;
// Returns true if the item in question needs to have update called because of changes in the entity
virtual bool needsUpdateFromEntity(const EntityItemPointer& entity) const override final {
if (Parent::needsUpdateFromEntity(entity)) {
return true;
}
return needsUpdateFromTypedEntity(_typedEntity);
}
using Parent::needsRenderUpdateFromEntity;
// Returns true if the item in question needs to have updateInScene called because of changes in the entity
virtual bool needsRenderUpdateFromEntity(const EntityItemPointer& entity) const override final {
@ -162,6 +181,11 @@ protected:
doRenderUpdateSynchronousTyped(scene, transaction, _typedEntity);
}
virtual void doUpdate(const ScenePointer& scene, Transaction& transaction, const EntityItemPointer& entity) override final {
Parent::doUpdate(scene, transaction, entity);
doUpdateTyped(scene, transaction, _typedEntity);
}
virtual void doRenderUpdateAsynchronous(const EntityItemPointer& entity) override final {
Parent::doRenderUpdateAsynchronous(entity);
doRenderUpdateAsynchronousTyped(_typedEntity);
@ -170,6 +194,8 @@ protected:
virtual bool needsRenderUpdateFromTypedEntity(const TypedEntityPointer& entity) const { return false; }
virtual void doRenderUpdateSynchronousTyped(const ScenePointer& scene, Transaction& transaction, const TypedEntityPointer& entity) { }
virtual void doRenderUpdateAsynchronousTyped(const TypedEntityPointer& entity) { }
virtual bool needsUpdateFromTypedEntity(const TypedEntityPointer& entity) const { return false; }
virtual void doUpdateTyped(const ScenePointer& scene, Transaction& transaction, const TypedEntityPointer& entity) { }
virtual void onAddToSceneTyped(const TypedEntityPointer& entity) { }
virtual void onRemoveFromSceneTyped(const TypedEntityPointer& entity) { }

View file

@ -318,8 +318,8 @@ void RenderableModelEntityItem::computeShapeInfo(ShapeInfo& shapeInfo) {
updateModelBounds();
// should never fall in here when collision model not fully loaded
// hence we assert that all geometries exist and are loaded
assert(_model && _model->isLoaded() && _compoundShapeResource && _compoundShapeResource->isLoaded());
// TODO: assert that all geometries exist and are loaded
//assert(_model && _model->isLoaded() && _compoundShapeResource && _compoundShapeResource->isLoaded());
const FBXGeometry& collisionGeometry = _compoundShapeResource->getFBXGeometry();
ShapeInfo::PointCollection& pointCollection = shapeInfo.getPointCollection();
@ -407,8 +407,8 @@ void RenderableModelEntityItem::computeShapeInfo(ShapeInfo& shapeInfo) {
}
shapeInfo.setParams(type, dimensions, getCompoundShapeURL());
} else if (type >= SHAPE_TYPE_SIMPLE_HULL && type <= SHAPE_TYPE_STATIC_MESH) {
// should never fall in here when model not fully loaded
assert(_model && _model->isLoaded());
// TODO: assert we never fall in here when model not fully loaded
//assert(_model && _model->isLoaded());
updateModelBounds();
model->updateGeometry();
@ -904,7 +904,7 @@ using namespace render;
using namespace render::entities;
ItemKey ModelEntityRenderer::getKey() {
return ItemKey::Builder::opaqueShape().withTypeMeta();
return ItemKey::Builder().withTypeMeta();
}
uint32_t ModelEntityRenderer::metaFetchMetaSubItems(ItemIDs& subItems) {
@ -1026,12 +1026,16 @@ void ModelEntityRenderer::animate(const TypedEntityPointer& entity) {
entity->copyAnimationJointDataToModel();
}
bool ModelEntityRenderer::needsRenderUpdate() const {
bool ModelEntityRenderer::needsUpdate() const {
ModelPointer model;
withReadLock([&] {
model = _model;
});
if (_modelJustLoaded) {
return true;
}
if (model) {
if (_needsJointSimulation || _moving || _animating) {
return true;
@ -1057,10 +1061,10 @@ bool ModelEntityRenderer::needsRenderUpdate() const {
return true;
}
}
return Parent::needsRenderUpdate();
return Parent::needsUpdate();
}
bool ModelEntityRenderer::needsRenderUpdateFromTypedEntity(const TypedEntityPointer& entity) const {
bool ModelEntityRenderer::needsUpdateFromTypedEntity(const TypedEntityPointer& entity) const {
if (resultWithReadLock<bool>([&] {
if (entity->hasModel() != _hasModel) {
return true;
@ -1122,7 +1126,7 @@ bool ModelEntityRenderer::needsRenderUpdateFromTypedEntity(const TypedEntityPoin
return false;
}
void ModelEntityRenderer::doRenderUpdateSynchronousTyped(const ScenePointer& scene, Transaction& transaction, const TypedEntityPointer& entity) {
void ModelEntityRenderer::doUpdateTyped(const ScenePointer& scene, Transaction& transaction, const TypedEntityPointer& entity) {
if (_hasModel != entity->hasModel()) {
_hasModel = entity->hasModel();
}
@ -1148,9 +1152,11 @@ void ModelEntityRenderer::doRenderUpdateSynchronousTyped(const ScenePointer& sce
return;
}
_modelJustLoaded = false;
// Check for addition
if (_hasModel && !(bool)_model) {
model = std::make_shared<Model>(nullptr, entity.get());
connect(model.get(), &Model::setURLFinished, this, &ModelEntityRenderer::handleModelLoaded);
model->setLoadingPriority(EntityTreeRenderer::getEntityLoadingPriority(*entity));
model->init();
entity->setModel(model);
@ -1175,8 +1181,8 @@ void ModelEntityRenderer::doRenderUpdateSynchronousTyped(const ScenePointer& sce
properties.setLastEdited(usecTimestampNow()); // we must set the edit time since we're editing it
auto extents = model->getMeshExtents();
properties.setDimensions(extents.maximum - extents.minimum);
qCDebug(entitiesrenderer) << "Autoresizing"
<< (!entity->getName().isEmpty() ? entity->getName() : entity->getModelURL())
qCDebug(entitiesrenderer) << "Autoresizing"
<< (!entity->getName().isEmpty() ? entity->getName() : entity->getModelURL())
<< "from mesh extents";
QMetaObject::invokeMethod(DependencyManager::get<EntityScriptingInterface>().data(), "editEntity",
@ -1203,7 +1209,6 @@ void ModelEntityRenderer::doRenderUpdateSynchronousTyped(const ScenePointer& sce
entity->updateModelBounds();
}
if (model->isVisible() != _visible) {
// FIXME: this seems like it could be optimized if we tracked our last known visible state in
// the renderable item. As it stands now the model checks it's visible/invisible state
@ -1234,7 +1239,6 @@ void ModelEntityRenderer::doRenderUpdateSynchronousTyped(const ScenePointer& sce
});
}
if (_animating) {
if (!jointsMapped()) {
mapJoints(entity, model->getJointNames());
@ -1243,6 +1247,12 @@ void ModelEntityRenderer::doRenderUpdateSynchronousTyped(const ScenePointer& sce
}
}
void ModelEntityRenderer::handleModelLoaded(bool success) {
if (success) {
_modelJustLoaded = true;
}
}
// NOTE: this only renders the "meta" portion of the Model, namely it renders debugging items
void ModelEntityRenderer::doRender(RenderArgs* args) {
PROFILE_RANGE(render_detail, "MetaModelRender");

View file

@ -138,10 +138,10 @@ protected:
virtual ItemKey getKey() override;
virtual uint32_t metaFetchMetaSubItems(ItemIDs& subItems) override;
virtual bool needsRenderUpdateFromTypedEntity(const TypedEntityPointer& entity) const override;
virtual bool needsRenderUpdate() const override;
virtual bool needsUpdateFromTypedEntity(const TypedEntityPointer& entity) const override;
virtual bool needsUpdate() const override;
virtual void doRender(RenderArgs* args) override;
virtual void doRenderUpdateSynchronousTyped(const ScenePointer& scene, Transaction& transaction, const TypedEntityPointer& entity) override;
virtual void doUpdateTyped(const ScenePointer& scene, Transaction& transaction, const TypedEntityPointer& entity) override;
private:
void animate(const TypedEntityPointer& entity);
@ -151,6 +151,7 @@ private:
// Transparency is handled in ModelMeshPartPayload
virtual bool isTransparent() const override { return false; }
bool _modelJustLoaded { false };
bool _hasModel { false };
::ModelPointer _model;
GeometryResource::Pointer _compoundShapeResource;
@ -178,6 +179,9 @@ private:
bool _animating { false };
uint64_t _lastAnimated { 0 };
float _currentFrame { 0 };
private slots:
void handleModelLoaded(bool success);
};
} } // namespace

View file

@ -102,7 +102,7 @@ void TextEntityRenderer::doRender(RenderArgs* args) {
glm::quat orientation = glm::quat(glm::vec3(0.0f, yawRotation, 0.0f));
transformToTopLeft.setRotation(orientation);
}
transformToTopLeft.postTranslate(glm::vec3(-0.5f, 0.5f, 0.0f)); // Go to the top left
transformToTopLeft.postTranslate(dimensions * glm::vec3(-0.5f, 0.5f, 0.0f)); // Go to the top left
transformToTopLeft.setScale(1.0f); // Use a scale of one so that the text is not deformed
batch.setModelTransform(transformToTopLeft);
@ -110,7 +110,7 @@ void TextEntityRenderer::doRender(RenderArgs* args) {
if (!_geometryID) {
_geometryID = geometryCache->allocateID();
}
geometryCache->bindSimpleProgram(batch, false, transparent, false, false, true);
geometryCache->bindSimpleProgram(batch, false, transparent, false, false, false, false);
geometryCache->renderQuad(batch, minCorner, maxCorner, backgroundColor, _geometryID);
float scale = _lineHeight / _textRenderer->getFontSize();

View file

@ -140,6 +140,11 @@ void WebEntityRenderer::doRenderUpdateSynchronousTyped(const ScenePointer& scene
_webSurface->resize(QSize(windowSize.x, windowSize.y));
}
void WebEntityRenderer::doRenderUpdateAsynchronousTyped(const TypedEntityPointer& entity) {
Parent::doRenderUpdateAsynchronousTyped(entity);
_modelTransform.postScale(entity->getDimensions());
}
void WebEntityRenderer::doRender(RenderArgs* args) {
withWriteLock([&] {
_lastRenderTime = usecTimestampNow();
@ -171,8 +176,6 @@ void WebEntityRenderer::doRender(RenderArgs* args) {
}
}
//_timer.singleShot
PerformanceTimer perfTimer("WebEntityRenderer::render");
static const glm::vec2 texMin(0.0f), texMax(1.0f), topLeft(-0.5f), bottomRight(0.5f);
@ -182,11 +185,7 @@ void WebEntityRenderer::doRender(RenderArgs* args) {
float fadeRatio = _isFading ? Interpolate::calculateFadeRatio(_fadeStartTime) : 1.0f;
batch._glColor4f(1.0f, 1.0f, 1.0f, fadeRatio);
if (fadeRatio < OPAQUE_ALPHA_THRESHOLD) {
DependencyManager::get<GeometryCache>()->bindWebBrowserProgram(batch, true);
} else {
DependencyManager::get<GeometryCache>()->bindWebBrowserProgram(batch);
}
DependencyManager::get<GeometryCache>()->bindWebBrowserProgram(batch, fadeRatio < OPAQUE_ALPHA_THRESHOLD);
DependencyManager::get<GeometryCache>()->renderQuad(batch, topLeft, bottomRight, texMin, texMax, glm::vec4(1.0f, 1.0f, 1.0f, fadeRatio), _geometryId);
}

View file

@ -29,6 +29,7 @@ protected:
virtual bool needsRenderUpdate() const override;
virtual bool needsRenderUpdateFromTypedEntity(const TypedEntityPointer& entity) const override;
virtual void doRenderUpdateSynchronousTyped(const ScenePointer& scene, Transaction& transaction, const TypedEntityPointer& entity) override;
virtual void doRenderUpdateAsynchronousTyped(const TypedEntityPointer& entity) override;
virtual void doRender(RenderArgs* args) override;
virtual bool isTransparent() const override;

View file

@ -1,4 +1,10 @@
set(TARGET_NAME fbx)
setup_hifi_library()
link_hifi_libraries(shared model networking)
include_hifi_library_headers(gpu)
link_hifi_libraries(shared model networking image)
include_hifi_library_headers(gpu image)
add_dependency_external_projects(draco)
find_package(Draco REQUIRED)
target_include_directories(${TARGET_NAME} SYSTEM PRIVATE ${DRACO_INCLUDE_DIRS})
target_link_libraries(${TARGET_NAME} ${DRACO_LIBRARY} ${DRACO_ENCODER_LIBRARY})

345
libraries/fbx/src/FBX.h Normal file
View file

@ -0,0 +1,345 @@
//
// FBX.h
// libraries/fbx/src
//
// Created by Ryan Huffman on 9/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_FBX_h_
#define hifi_FBX_h_
#include <QMetaType>
#include <QSet>
#include <QUrl>
#include <QVarLengthArray>
#include <QVariant>
#include <QVector>
#include <glm/glm.hpp>
#include <glm/gtc/quaternion.hpp>
#include <Extents.h>
#include <Transform.h>
#include <model/Geometry.h>
#include <model/Material.h>
static const QByteArray FBX_BINARY_PROLOG = "Kaydara FBX Binary ";
static const int FBX_HEADER_BYTES_BEFORE_VERSION = 23;
static const quint32 FBX_VERSION_2015 = 7400;
static const quint32 FBX_VERSION_2016 = 7500;
static const int DRACO_BEGIN_CUSTOM_HIFI_ATTRIBUTES = 1000;
static const int DRACO_ATTRIBUTE_MATERIAL_ID = DRACO_BEGIN_CUSTOM_HIFI_ATTRIBUTES;
static const int DRACO_ATTRIBUTE_TEX_COORD_1 = DRACO_BEGIN_CUSTOM_HIFI_ATTRIBUTES + 1;
static const int DRACO_ATTRIBUTE_ORIGINAL_INDEX = DRACO_BEGIN_CUSTOM_HIFI_ATTRIBUTES + 2;
static const int32_t FBX_PROPERTY_UNCOMPRESSED_FLAG = 0;
static const int32_t FBX_PROPERTY_COMPRESSED_FLAG = 1;
class FBXNode;
using FBXNodeList = QList<FBXNode>;
/// A node within an FBX document.
class FBXNode {
public:
QByteArray name;
QVariantList properties;
FBXNodeList children;
};
/// A single blendshape extracted from an FBX document.
class FBXBlendshape {
public:
QVector<int> indices;
QVector<glm::vec3> vertices;
QVector<glm::vec3> normals;
};
struct FBXJointShapeInfo {
// same units and frame as FBXJoint.translation
glm::vec3 avgPoint;
std::vector<float> dots;
std::vector<glm::vec3> points;
std::vector<glm::vec3> debugLines;
};
/// A single joint (transformation node) extracted from an FBX document.
class FBXJoint {
public:
FBXJointShapeInfo shapeInfo;
QVector<int> freeLineage;
bool isFree;
int parentIndex;
float distanceToParent;
// http://download.autodesk.com/us/fbx/20112/FBX_SDK_HELP/SDKRef/a00209.html
glm::vec3 translation; // T
glm::mat4 preTransform; // Roff * Rp
glm::quat preRotation; // Rpre
glm::quat rotation; // R
glm::quat postRotation; // Rpost
glm::mat4 postTransform; // Rp-1 * Soff * Sp * S * Sp-1
// World = ParentWorld * T * (Roff * Rp) * Rpre * R * Rpost * (Rp-1 * Soff * Sp * S * Sp-1)
glm::mat4 transform;
glm::vec3 rotationMin; // radians
glm::vec3 rotationMax; // radians
glm::quat inverseDefaultRotation;
glm::quat inverseBindRotation;
glm::mat4 bindTransform;
QString name;
bool isSkeletonJoint;
bool bindTransformFoundInCluster;
// geometric offset is applied in local space but does NOT affect children.
bool hasGeometricOffset;
glm::vec3 geometricTranslation;
glm::quat geometricRotation;
glm::vec3 geometricScaling;
};
/// A single binding to a joint in an FBX document.
class FBXCluster {
public:
int jointIndex;
glm::mat4 inverseBindMatrix;
};
const int MAX_NUM_PIXELS_FOR_FBX_TEXTURE = 2048 * 2048;
/// A texture map in an FBX document.
class FBXTexture {
public:
QString id;
QString name;
QByteArray filename;
QByteArray content;
Transform transform;
int maxNumPixels { MAX_NUM_PIXELS_FOR_FBX_TEXTURE };
int texcoordSet;
QString texcoordSetName;
bool isBumpmap{ false };
bool isNull() const { return name.isEmpty() && filename.isEmpty() && content.isEmpty(); }
};
/// A single part of a mesh (with the same material).
class FBXMeshPart {
public:
QVector<int> quadIndices; // original indices from the FBX mesh
QVector<int> quadTrianglesIndices; // original indices from the FBX mesh of the quad converted as triangles
QVector<int> triangleIndices; // original indices from the FBX mesh
QString materialID;
};
class FBXMaterial {
public:
FBXMaterial() {};
FBXMaterial(const glm::vec3& diffuseColor, const glm::vec3& specularColor, const glm::vec3& emissiveColor,
float shininess, float opacity) :
diffuseColor(diffuseColor),
specularColor(specularColor),
emissiveColor(emissiveColor),
shininess(shininess),
opacity(opacity) {}
void getTextureNames(QSet<QString>& textureList) const;
void setMaxNumPixelsPerTexture(int maxNumPixels);
glm::vec3 diffuseColor{ 1.0f };
float diffuseFactor{ 1.0f };
glm::vec3 specularColor{ 0.02f };
float specularFactor{ 1.0f };
glm::vec3 emissiveColor{ 0.0f };
float emissiveFactor{ 0.0f };
float shininess{ 23.0f };
float opacity{ 1.0f };
float metallic{ 0.0f };
float roughness{ 1.0f };
float emissiveIntensity{ 1.0f };
float ambientFactor{ 1.0f };
QString materialID;
QString name;
QString shadingModel;
model::MaterialPointer _material;
FBXTexture normalTexture;
FBXTexture albedoTexture;
FBXTexture opacityTexture;
FBXTexture glossTexture;
FBXTexture roughnessTexture;
FBXTexture specularTexture;
FBXTexture metallicTexture;
FBXTexture emissiveTexture;
FBXTexture occlusionTexture;
FBXTexture scatteringTexture;
FBXTexture lightmapTexture;
glm::vec2 lightmapParams{ 0.0f, 1.0f };
bool isPBSMaterial{ false };
// THe use XXXMap are not really used to drive which map are going or not, debug only
bool useNormalMap{ false };
bool useAlbedoMap{ false };
bool useOpacityMap{ false };
bool useRoughnessMap{ false };
bool useSpecularMap{ false };
bool useMetallicMap{ false };
bool useEmissiveMap{ false };
bool useOcclusionMap{ false };
bool needTangentSpace() const;
};
/// A single mesh (with optional blendshapes) extracted from an FBX document.
class FBXMesh {
public:
QVector<FBXMeshPart> parts;
QVector<glm::vec3> vertices;
QVector<glm::vec3> normals;
QVector<glm::vec3> tangents;
QVector<glm::vec3> colors;
QVector<glm::vec2> texCoords;
QVector<glm::vec2> texCoords1;
QVector<uint16_t> clusterIndices;
QVector<uint8_t> clusterWeights;
QVector<int32_t> originalIndices;
QVector<FBXCluster> clusters;
Extents meshExtents;
glm::mat4 modelTransform;
QVector<FBXBlendshape> blendshapes;
unsigned int meshIndex; // the order the meshes appeared in the object file
model::MeshPointer _mesh;
bool wasCompressed { false };
};
class ExtractedMesh {
public:
FBXMesh mesh;
QMultiHash<int, int> newIndices;
QVector<QHash<int, int> > blendshapeIndexMaps;
QVector<QPair<int, int> > partMaterialTextures;
QHash<QString, size_t> texcoordSetMap;
};
/// A single animation frame extracted from an FBX document.
class FBXAnimationFrame {
public:
QVector<glm::quat> rotations;
QVector<glm::vec3> translations;
};
/// A light in an FBX document.
class FBXLight {
public:
QString name;
Transform transform;
float intensity;
float fogValue;
glm::vec3 color;
FBXLight() :
name(),
transform(),
intensity(1.0f),
fogValue(0.0f),
color(1.0f)
{}
};
Q_DECLARE_METATYPE(FBXAnimationFrame)
Q_DECLARE_METATYPE(QVector<FBXAnimationFrame>)
/// A set of meshes extracted from an FBX document.
class FBXGeometry {
public:
using Pointer = std::shared_ptr<FBXGeometry>;
QString originalURL;
QString author;
QString applicationName; ///< the name of the application that generated the model
QVector<FBXJoint> joints;
QHash<QString, int> jointIndices; ///< 1-based, so as to more easily detect missing indices
bool hasSkeletonJoints;
QVector<FBXMesh> meshes;
QHash<QString, FBXMaterial> materials;
glm::mat4 offset; // This includes offset, rotation, and scale as specified by the FST file
int leftEyeJointIndex = -1;
int rightEyeJointIndex = -1;
int neckJointIndex = -1;
int rootJointIndex = -1;
int leanJointIndex = -1;
int headJointIndex = -1;
int leftHandJointIndex = -1;
int rightHandJointIndex = -1;
int leftToeJointIndex = -1;
int rightToeJointIndex = -1;
float leftEyeSize = 0.0f; // Maximum mesh extents dimension
float rightEyeSize = 0.0f;
QVector<int> humanIKJointIndices;
glm::vec3 palmDirection;
glm::vec3 neckPivot;
Extents bindExtents;
Extents meshExtents;
QVector<FBXAnimationFrame> animationFrames;
int getJointIndex(const QString& name) const { return jointIndices.value(name) - 1; }
QStringList getJointNames() const;
bool hasBlendedMeshes() const;
/// Returns the unscaled extents of the model's mesh
Extents getUnscaledMeshExtents() const;
bool convexHullContains(const glm::vec3& point) const;
QHash<int, QString> meshIndicesToModelNames;
/// given a meshIndex this will return the name of the model that mesh belongs to if known
QString getModelNameOfMesh(int meshIndex) const;
QList<QString> blendshapeChannelNames;
};
Q_DECLARE_METATYPE(FBXGeometry)
Q_DECLARE_METATYPE(FBXGeometry::Pointer)
#endif // hifi_FBX_h_

View file

@ -168,7 +168,8 @@ QString getID(const QVariantList& properties, int index = 0) {
return processID(properties.at(index).toString());
}
const char* HUMANIK_JOINTS[] = {
/// The names of the joints in the Maya HumanIK rig
static const std::array<const char*, 16> HUMANIK_JOINTS = {{
"RightHand",
"RightForeArm",
"RightArm",
@ -184,9 +185,8 @@ const char* HUMANIK_JOINTS[] = {
"RightLeg",
"LeftLeg",
"RightFoot",
"LeftFoot",
""
};
"LeftFoot"
}};
class FBXModel {
public:
@ -468,7 +468,7 @@ QByteArray fileOnUrl(const QByteArray& filepath, const QString& url) {
}
FBXGeometry* FBXReader::extractFBXGeometry(const QVariantHash& mapping, const QString& url) {
const FBXNode& node = _fbxNode;
const FBXNode& node = _rootNode;
QMap<QString, ExtractedMesh> meshes;
QHash<QString, QString> modelIDsToNames;
QHash<QString, int> meshIDsToMeshIndices;
@ -512,11 +512,8 @@ FBXGeometry* FBXReader::extractFBXGeometry(const QVariantHash& mapping, const QS
QVector<QString> humanIKJointNames;
for (int i = 0;; i++) {
for (int i = 0; i < (int) HUMANIK_JOINTS.size(); i++) {
QByteArray jointName = HUMANIK_JOINTS[i];
if (jointName.isEmpty()) {
break;
}
humanIKJointNames.append(processID(getString(joints.value(jointName, jointName))));
}
QVector<QString> humanIKJointIDs(humanIKJointNames.size());
@ -846,12 +843,14 @@ FBXGeometry* FBXReader::extractFBXGeometry(const QVariantHash& mapping, const QS
QByteArray filename = subobject.properties.at(0).toByteArray();
QByteArray filepath = filename.replace('\\', '/');
filename = fileOnUrl(filepath, url);
qDebug() << "Filename" << filepath << filename;
_textureFilepaths.insert(getID(object.properties), filepath);
_textureFilenames.insert(getID(object.properties), filename);
} else if (subobject.name == "TextureName" && subobject.properties.length() >= TEXTURE_NAME_MIN_SIZE) {
// trim the name from the timestamp
QString name = QString(subobject.properties.at(0).toByteArray());
name = name.left(name.indexOf('['));
qDebug() << "Filename" << name;
_textureNames.insert(getID(object.properties), name);
} else if (subobject.name == "Texture_Alpha_Source" && subobject.properties.length() >= TEXTURE_ALPHA_SOURCE_MIN_SIZE) {
tex.assign<uint8_t>(tex.alphaSource, subobject.properties.at(0).value<int>());
@ -940,7 +939,7 @@ FBXGeometry* FBXReader::extractFBXGeometry(const QVariantHash& mapping, const QS
QByteArray content;
foreach (const FBXNode& subobject, object.children) {
if (subobject.name == "RelativeFilename") {
filepath= subobject.properties.at(0).toByteArray();
filepath = subobject.properties.at(0).toByteArray();
filepath = filepath.replace('\\', '/');
} else if (subobject.name == "Content" && !subobject.properties.isEmpty()) {
@ -1840,9 +1839,11 @@ FBXGeometry* readFBX(const QByteArray& model, const QVariantHash& mapping, const
FBXGeometry* readFBX(QIODevice* device, const QVariantHash& mapping, const QString& url, bool loadLightmaps, float lightmapLevel) {
FBXReader reader;
reader._fbxNode = FBXReader::parseFBX(device);
reader._rootNode = FBXReader::parseFBX(device);
reader._loadLightmaps = loadLightmaps;
reader._lightmapLevel = lightmapLevel;
qDebug() << "Reading FBX: " << url;
return reader.extractFBXGeometry(mapping, url);
}

View file

@ -12,6 +12,8 @@
#ifndef hifi_FBXReader_h
#define hifi_FBXReader_h
#include "FBX.h"
#include <QMetaType>
#include <QSet>
#include <QUrl>
@ -31,305 +33,6 @@
class QIODevice;
class FBXNode;
typedef QList<FBXNode> FBXNodeList;
/// The names of the joints in the Maya HumanIK rig, terminated with an empty string.
extern const char* HUMANIK_JOINTS[];
/// A node within an FBX document.
class FBXNode {
public:
QByteArray name;
QVariantList properties;
FBXNodeList children;
};
/// A single blendshape extracted from an FBX document.
class FBXBlendshape {
public:
QVector<int> indices;
QVector<glm::vec3> vertices;
QVector<glm::vec3> normals;
};
struct FBXJointShapeInfo {
// same units and frame as FBXJoint.translation
glm::vec3 avgPoint;
std::vector<float> dots;
std::vector<glm::vec3> points;
std::vector<glm::vec3> debugLines;
};
/// A single joint (transformation node) extracted from an FBX document.
class FBXJoint {
public:
FBXJointShapeInfo shapeInfo;
QVector<int> freeLineage;
bool isFree;
int parentIndex;
float distanceToParent;
// http://download.autodesk.com/us/fbx/20112/FBX_SDK_HELP/SDKRef/a00209.html
glm::vec3 translation; // T
glm::mat4 preTransform; // Roff * Rp
glm::quat preRotation; // Rpre
glm::quat rotation; // R
glm::quat postRotation; // Rpost
glm::mat4 postTransform; // Rp-1 * Soff * Sp * S * Sp-1
// World = ParentWorld * T * (Roff * Rp) * Rpre * R * Rpost * (Rp-1 * Soff * Sp * S * Sp-1)
glm::mat4 transform;
glm::vec3 rotationMin; // radians
glm::vec3 rotationMax; // radians
glm::quat inverseDefaultRotation;
glm::quat inverseBindRotation;
glm::mat4 bindTransform;
QString name;
bool isSkeletonJoint;
bool bindTransformFoundInCluster;
// geometric offset is applied in local space but does NOT affect children.
bool hasGeometricOffset;
glm::vec3 geometricTranslation;
glm::quat geometricRotation;
glm::vec3 geometricScaling;
};
/// A single binding to a joint in an FBX document.
class FBXCluster {
public:
int jointIndex;
glm::mat4 inverseBindMatrix;
};
const int MAX_NUM_PIXELS_FOR_FBX_TEXTURE = 2048 * 2048;
/// A texture map in an FBX document.
class FBXTexture {
public:
QString name;
QByteArray filename;
QByteArray content;
Transform transform;
int maxNumPixels { MAX_NUM_PIXELS_FOR_FBX_TEXTURE };
int texcoordSet;
QString texcoordSetName;
bool isBumpmap{ false };
bool isNull() const { return name.isEmpty() && filename.isEmpty() && content.isEmpty(); }
};
/// A single part of a mesh (with the same material).
class FBXMeshPart {
public:
QVector<int> quadIndices; // original indices from the FBX mesh
QVector<int> quadTrianglesIndices; // original indices from the FBX mesh of the quad converted as triangles
QVector<int> triangleIndices; // original indices from the FBX mesh
QString materialID;
};
class FBXMaterial {
public:
FBXMaterial() {};
FBXMaterial(const glm::vec3& diffuseColor, const glm::vec3& specularColor, const glm::vec3& emissiveColor,
float shininess, float opacity) :
diffuseColor(diffuseColor),
specularColor(specularColor),
emissiveColor(emissiveColor),
shininess(shininess),
opacity(opacity) {}
void getTextureNames(QSet<QString>& textureList) const;
void setMaxNumPixelsPerTexture(int maxNumPixels);
glm::vec3 diffuseColor{ 1.0f };
float diffuseFactor{ 1.0f };
glm::vec3 specularColor{ 0.02f };
float specularFactor{ 1.0f };
glm::vec3 emissiveColor{ 0.0f };
float emissiveFactor{ 0.0f };
float shininess{ 23.0f };
float opacity{ 1.0f };
float metallic{ 0.0f };
float roughness{ 1.0f };
float emissiveIntensity{ 1.0f };
float ambientFactor{ 1.0f };
QString materialID;
QString name;
QString shadingModel;
model::MaterialPointer _material;
FBXTexture normalTexture;
FBXTexture albedoTexture;
FBXTexture opacityTexture;
FBXTexture glossTexture;
FBXTexture roughnessTexture;
FBXTexture specularTexture;
FBXTexture metallicTexture;
FBXTexture emissiveTexture;
FBXTexture occlusionTexture;
FBXTexture scatteringTexture;
FBXTexture lightmapTexture;
glm::vec2 lightmapParams{ 0.0f, 1.0f };
bool isPBSMaterial{ false };
// THe use XXXMap are not really used to drive which map are going or not, debug only
bool useNormalMap{ false };
bool useAlbedoMap{ false };
bool useOpacityMap{ false };
bool useRoughnessMap{ false };
bool useSpecularMap{ false };
bool useMetallicMap{ false };
bool useEmissiveMap{ false };
bool useOcclusionMap{ false };
bool needTangentSpace() const;
};
/// A single mesh (with optional blendshapes) extracted from an FBX document.
class FBXMesh {
public:
QVector<FBXMeshPart> parts;
QVector<glm::vec3> vertices;
QVector<glm::vec3> normals;
QVector<glm::vec3> tangents;
QVector<glm::vec3> colors;
QVector<glm::vec2> texCoords;
QVector<glm::vec2> texCoords1;
QVector<uint16_t> clusterIndices;
QVector<uint8_t> clusterWeights;
QVector<FBXCluster> clusters;
Extents meshExtents;
glm::mat4 modelTransform;
QVector<FBXBlendshape> blendshapes;
unsigned int meshIndex; // the order the meshes appeared in the object file
model::MeshPointer _mesh;
};
class ExtractedMesh {
public:
FBXMesh mesh;
QMultiHash<int, int> newIndices;
QVector<QHash<int, int> > blendshapeIndexMaps;
QVector<QPair<int, int> > partMaterialTextures;
QHash<QString, size_t> texcoordSetMap;
};
/// A single animation frame extracted from an FBX document.
class FBXAnimationFrame {
public:
QVector<glm::quat> rotations;
QVector<glm::vec3> translations;
};
/// A light in an FBX document.
class FBXLight {
public:
QString name;
Transform transform;
float intensity;
float fogValue;
glm::vec3 color;
FBXLight() :
name(),
transform(),
intensity(1.0f),
fogValue(0.0f),
color(1.0f)
{}
};
Q_DECLARE_METATYPE(FBXAnimationFrame)
Q_DECLARE_METATYPE(QVector<FBXAnimationFrame>)
/// A set of meshes extracted from an FBX document.
class FBXGeometry {
public:
using Pointer = std::shared_ptr<FBXGeometry>;
QString originalURL;
QString author;
QString applicationName; ///< the name of the application that generated the model
QVector<FBXJoint> joints;
QHash<QString, int> jointIndices; ///< 1-based, so as to more easily detect missing indices
bool hasSkeletonJoints;
QVector<FBXMesh> meshes;
QHash<QString, FBXMaterial> materials;
glm::mat4 offset; // This includes offset, rotation, and scale as specified by the FST file
int leftEyeJointIndex = -1;
int rightEyeJointIndex = -1;
int neckJointIndex = -1;
int rootJointIndex = -1;
int leanJointIndex = -1;
int headJointIndex = -1;
int leftHandJointIndex = -1;
int rightHandJointIndex = -1;
int leftToeJointIndex = -1;
int rightToeJointIndex = -1;
float leftEyeSize = 0.0f; // Maximum mesh extents dimension
float rightEyeSize = 0.0f;
QVector<int> humanIKJointIndices;
glm::vec3 palmDirection;
glm::vec3 neckPivot;
Extents bindExtents;
Extents meshExtents;
QVector<FBXAnimationFrame> animationFrames;
int getJointIndex(const QString& name) const { return jointIndices.value(name) - 1; }
QStringList getJointNames() const;
bool hasBlendedMeshes() const;
/// Returns the unscaled extents of the model's mesh
Extents getUnscaledMeshExtents() const;
bool convexHullContains(const glm::vec3& point) const;
QHash<int, QString> meshIndicesToModelNames;
/// given a meshIndex this will return the name of the model that mesh belongs to if known
QString getModelNameOfMesh(int meshIndex) const;
QList<QString> blendshapeChannelNames;
};
Q_DECLARE_METATYPE(FBXGeometry)
Q_DECLARE_METATYPE(FBXGeometry::Pointer)
/// Reads FBX geometry from the supplied model and mapping data.
/// \exception QString if an error occurs in parsing
@ -402,12 +105,12 @@ class FBXReader {
public:
FBXGeometry* _fbxGeometry;
FBXNode _fbxNode;
FBXNode _rootNode;
static FBXNode parseFBX(QIODevice* device);
FBXGeometry* extractFBXGeometry(const QVariantHash& mapping, const QString& url);
ExtractedMesh extractMesh(const FBXNode& object, unsigned int& meshIndex);
static ExtractedMesh extractMesh(const FBXNode& object, unsigned int& meshIndex, bool deduplicate = true);
QHash<QString, ExtractedMesh> meshes;
static void buildModelMesh(FBXMesh& extractedMesh, const QString& url);

View file

@ -92,6 +92,7 @@ FBXTexture FBXReader::getTexture(const QString& textureID) {
texture.filename = filepath;
}
texture.id = textureID;
texture.name = _textureNames.value(textureID);
texture.transform.setIdentity();
texture.texcoordSet = 0;

View file

@ -9,6 +9,17 @@
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
#ifdef _WIN32
#pragma warning( push )
#pragma warning( disable : 4267 )
#endif
#include <draco/compression/decode.h>
#ifdef _WIN32
#pragma warning( pop )
#endif
#include <iostream>
#include <QBuffer>
#include <QDataStream>
@ -73,7 +84,7 @@ public:
};
void appendIndex(MeshData& data, QVector<int>& indices, int index) {
void appendIndex(MeshData& data, QVector<int>& indices, int index, bool deduplicate) {
if (index >= data.polygonIndices.size()) {
return;
}
@ -145,12 +156,13 @@ void appendIndex(MeshData& data, QVector<int>& indices, int index) {
}
QHash<Vertex, int>::const_iterator it = data.indices.find(vertex);
if (it == data.indices.constEnd()) {
if (!deduplicate || it == data.indices.constEnd()) {
int newIndex = data.extracted.mesh.vertices.size();
indices.append(newIndex);
data.indices.insert(vertex, newIndex);
data.extracted.newIndices.insert(vertexIndex, newIndex);
data.extracted.mesh.vertices.append(position);
data.extracted.mesh.originalIndices.append(vertexIndex);
data.extracted.mesh.normals.append(normal);
data.extracted.mesh.texCoords.append(vertex.texCoord);
if (hasColors) {
@ -165,14 +177,20 @@ void appendIndex(MeshData& data, QVector<int>& indices, int index) {
}
}
ExtractedMesh FBXReader::extractMesh(const FBXNode& object, unsigned int& meshIndex) {
ExtractedMesh FBXReader::extractMesh(const FBXNode& object, unsigned int& meshIndex, bool deduplicate) {
MeshData data;
data.extracted.mesh.meshIndex = meshIndex++;
QVector<int> materials;
QVector<int> textures;
bool isMaterialPerPolygon = false;
static const QVariant BY_VERTICE = QByteArray("ByVertice");
static const QVariant INDEX_TO_DIRECT = QByteArray("IndexToDirect");
bool isDracoMesh = false;
foreach (const FBXNode& child, object.children) {
if (child.name == "Vertices") {
data.vertices = createVec3Vector(getDoubleVector(child));
@ -304,8 +322,9 @@ ExtractedMesh FBXReader::extractMesh(const FBXNode& object, unsigned int& meshIn
if (subdata.name == "Materials") {
materials = getIntVector(subdata);
} else if (subdata.name == "MappingInformationType") {
if (subdata.properties.at(0) == BY_POLYGON)
if (subdata.properties.at(0) == BY_POLYGON) {
isMaterialPerPolygon = true;
}
} else {
isMaterialPerPolygon = false;
}
@ -318,70 +337,206 @@ ExtractedMesh FBXReader::extractMesh(const FBXNode& object, unsigned int& meshIn
textures = getIntVector(subdata);
}
}
}
}
} else if (child.name == "DracoMesh") {
isDracoMesh = true;
data.extracted.mesh.wasCompressed = true;
bool isMultiMaterial = false;
if (isMaterialPerPolygon) {
isMultiMaterial = true;
}
// TODO: make excellent use of isMultiMaterial
Q_UNUSED(isMultiMaterial);
// load the draco mesh from the FBX and create a draco::Mesh
draco::Decoder decoder;
draco::DecoderBuffer decodedBuffer;
QByteArray dracoArray = child.properties.at(0).value<QByteArray>();
decodedBuffer.Init(dracoArray.data(), dracoArray.size());
// convert the polygons to quads and triangles
int polygonIndex = 0;
QHash<QPair<int, int>, int> materialTextureParts;
for (int beginIndex = 0; beginIndex < data.polygonIndices.size(); polygonIndex++) {
int endIndex = beginIndex;
while (endIndex < data.polygonIndices.size() && data.polygonIndices.at(endIndex++) >= 0);
std::unique_ptr<draco::Mesh> dracoMesh(new draco::Mesh());
decoder.DecodeBufferToGeometry(&decodedBuffer, dracoMesh.get());
QPair<int, int> materialTexture((polygonIndex < materials.size()) ? materials.at(polygonIndex) : 0,
(polygonIndex < textures.size()) ? textures.at(polygonIndex) : 0);
int& partIndex = materialTextureParts[materialTexture];
if (partIndex == 0) {
data.extracted.partMaterialTextures.append(materialTexture);
data.extracted.mesh.parts.resize(data.extracted.mesh.parts.size() + 1);
partIndex = data.extracted.mesh.parts.size();
}
FBXMeshPart& part = data.extracted.mesh.parts[partIndex - 1];
if (endIndex - beginIndex == 4) {
appendIndex(data, part.quadIndices, beginIndex++);
appendIndex(data, part.quadIndices, beginIndex++);
appendIndex(data, part.quadIndices, beginIndex++);
appendIndex(data, part.quadIndices, beginIndex++);
// prepare attributes for this mesh
auto positionAttribute = dracoMesh->GetNamedAttribute(draco::GeometryAttribute::POSITION);
auto normalAttribute = dracoMesh->GetNamedAttribute(draco::GeometryAttribute::NORMAL);
auto texCoordAttribute = dracoMesh->GetNamedAttribute(draco::GeometryAttribute::TEX_COORD);
auto extraTexCoordAttribute = dracoMesh->GetAttributeByUniqueId(DRACO_ATTRIBUTE_TEX_COORD_1);
auto colorAttribute = dracoMesh->GetNamedAttribute(draco::GeometryAttribute::COLOR);
auto materialIDAttribute = dracoMesh->GetAttributeByUniqueId(DRACO_ATTRIBUTE_MATERIAL_ID);
auto originalIndexAttribute = dracoMesh->GetAttributeByUniqueId(DRACO_ATTRIBUTE_ORIGINAL_INDEX);
int quadStartIndex = part.quadIndices.size() - 4;
int i0 = part.quadIndices[quadStartIndex + 0];
int i1 = part.quadIndices[quadStartIndex + 1];
int i2 = part.quadIndices[quadStartIndex + 2];
int i3 = part.quadIndices[quadStartIndex + 3];
// setup extracted mesh data structures given number of points
auto numVertices = dracoMesh->num_points();
// Sam's recommended triangle slices
// Triangle tri1 = { v0, v1, v3 };
// Triangle tri2 = { v1, v2, v3 };
// NOTE: Random guy on the internet's recommended triangle slices
// Triangle tri1 = { v0, v1, v2 };
// Triangle tri2 = { v2, v3, v0 };
QHash<QPair<int, int>, int> materialTextureParts;
part.quadTrianglesIndices.append(i0);
part.quadTrianglesIndices.append(i1);
part.quadTrianglesIndices.append(i3);
data.extracted.mesh.vertices.resize(numVertices);
part.quadTrianglesIndices.append(i1);
part.quadTrianglesIndices.append(i2);
part.quadTrianglesIndices.append(i3);
if (normalAttribute) {
data.extracted.mesh.normals.resize(numVertices);
}
} else {
for (int nextIndex = beginIndex + 1;; ) {
appendIndex(data, part.triangleIndices, beginIndex);
appendIndex(data, part.triangleIndices, nextIndex++);
appendIndex(data, part.triangleIndices, nextIndex);
if (nextIndex >= data.polygonIndices.size() || data.polygonIndices.at(nextIndex) < 0) {
break;
if (texCoordAttribute) {
data.extracted.mesh.texCoords.resize(numVertices);
}
if (extraTexCoordAttribute) {
data.extracted.mesh.texCoords1.resize(numVertices);
}
if (colorAttribute) {
data.extracted.mesh.colors.resize(numVertices);
}
// enumerate the vertices and construct the extracted mesh
for (int i = 0; i < numVertices; ++i) {
draco::PointIndex vertexIndex(i);
if (positionAttribute) {
// read position from draco mesh to extracted mesh
auto mappedIndex = positionAttribute->mapped_index(vertexIndex);
positionAttribute->ConvertValue<float, 3>(mappedIndex,
reinterpret_cast<float*>(&data.extracted.mesh.vertices[i]));
}
if (normalAttribute) {
// read normals from draco mesh to extracted mesh
auto mappedIndex = normalAttribute->mapped_index(vertexIndex);
normalAttribute->ConvertValue<float, 3>(mappedIndex,
reinterpret_cast<float*>(&data.extracted.mesh.normals[i]));
}
if (texCoordAttribute) {
// read UVs from draco mesh to extracted mesh
auto mappedIndex = texCoordAttribute->mapped_index(vertexIndex);
texCoordAttribute->ConvertValue<float, 2>(mappedIndex,
reinterpret_cast<float*>(&data.extracted.mesh.texCoords[i]));
}
if (extraTexCoordAttribute) {
// some meshes have a second set of UVs, read those to extracted mesh
auto mappedIndex = extraTexCoordAttribute->mapped_index(vertexIndex);
extraTexCoordAttribute->ConvertValue<float, 2>(mappedIndex,
reinterpret_cast<float*>(&data.extracted.mesh.texCoords1[i]));
}
if (colorAttribute) {
// read vertex colors from draco mesh to extracted mesh
auto mappedIndex = colorAttribute->mapped_index(vertexIndex);
colorAttribute->ConvertValue<float, 3>(mappedIndex,
reinterpret_cast<float*>(&data.extracted.mesh.colors[i]));
}
if (originalIndexAttribute) {
auto mappedIndex = originalIndexAttribute->mapped_index(vertexIndex);
int32_t originalIndex;
originalIndexAttribute->ConvertValue<int32_t, 1>(mappedIndex, &originalIndex);
data.extracted.newIndices.insert(originalIndex, i);
} else {
data.extracted.newIndices.insert(i, i);
}
}
beginIndex = endIndex;
for (int i = 0; i < dracoMesh->num_faces(); ++i) {
// grab the material ID and texture ID for this face, if we have it
auto& dracoFace = dracoMesh->face(draco::FaceIndex(i));
auto& firstCorner = dracoFace[0];
uint16_t materialID { 0 };
if (materialIDAttribute) {
// read material ID and texture ID mappings into materials and texture vectors
auto mappedIndex = materialIDAttribute->mapped_index(firstCorner);
materialIDAttribute->ConvertValue<uint16_t, 1>(mappedIndex, &materialID);
}
QPair<int, int> materialTexture(materialID, 0);
// grab or setup the FBXMeshPart for the part this face belongs to
int& partIndexPlusOne = materialTextureParts[materialTexture];
if (partIndexPlusOne == 0) {
data.extracted.partMaterialTextures.append(materialTexture);
data.extracted.mesh.parts.resize(data.extracted.mesh.parts.size() + 1);
partIndexPlusOne = data.extracted.mesh.parts.size();
}
// give the mesh part this index
FBXMeshPart& part = data.extracted.mesh.parts[partIndexPlusOne - 1];
part.triangleIndices.append(firstCorner.value());
part.triangleIndices.append(dracoFace[1].value());
part.triangleIndices.append(dracoFace[2].value());
}
}
}
// when we have a draco mesh, we've already built the extracted mesh, so we don't need to do the
// processing we do for normal meshes below
if (!isDracoMesh) {
bool isMultiMaterial = false;
if (isMaterialPerPolygon) {
isMultiMaterial = true;
}
// TODO: make excellent use of isMultiMaterial
Q_UNUSED(isMultiMaterial);
// convert the polygons to quads and triangles
int polygonIndex = 0;
QHash<QPair<int, int>, int> materialTextureParts;
for (int beginIndex = 0; beginIndex < data.polygonIndices.size(); polygonIndex++) {
int endIndex = beginIndex;
while (endIndex < data.polygonIndices.size() && data.polygonIndices.at(endIndex++) >= 0);
QPair<int, int> materialTexture((polygonIndex < materials.size()) ? materials.at(polygonIndex) : 0,
(polygonIndex < textures.size()) ? textures.at(polygonIndex) : 0);
int& partIndex = materialTextureParts[materialTexture];
if (partIndex == 0) {
data.extracted.partMaterialTextures.append(materialTexture);
data.extracted.mesh.parts.resize(data.extracted.mesh.parts.size() + 1);
partIndex = data.extracted.mesh.parts.size();
}
FBXMeshPart& part = data.extracted.mesh.parts[partIndex - 1];
if (endIndex - beginIndex == 4) {
appendIndex(data, part.quadIndices, beginIndex++, deduplicate);
appendIndex(data, part.quadIndices, beginIndex++, deduplicate);
appendIndex(data, part.quadIndices, beginIndex++, deduplicate);
appendIndex(data, part.quadIndices, beginIndex++, deduplicate);
int quadStartIndex = part.quadIndices.size() - 4;
int i0 = part.quadIndices[quadStartIndex + 0];
int i1 = part.quadIndices[quadStartIndex + 1];
int i2 = part.quadIndices[quadStartIndex + 2];
int i3 = part.quadIndices[quadStartIndex + 3];
// Sam's recommended triangle slices
// Triangle tri1 = { v0, v1, v3 };
// Triangle tri2 = { v1, v2, v3 };
// NOTE: Random guy on the internet's recommended triangle slices
// Triangle tri1 = { v0, v1, v2 };
// Triangle tri2 = { v2, v3, v0 };
part.quadTrianglesIndices.append(i0);
part.quadTrianglesIndices.append(i1);
part.quadTrianglesIndices.append(i3);
part.quadTrianglesIndices.append(i1);
part.quadTrianglesIndices.append(i2);
part.quadTrianglesIndices.append(i3);
} else {
for (int nextIndex = beginIndex + 1;; ) {
appendIndex(data, part.triangleIndices, beginIndex, deduplicate);
appendIndex(data, part.triangleIndices, nextIndex++, deduplicate);
appendIndex(data, part.triangleIndices, nextIndex, deduplicate);
if (nextIndex >= data.polygonIndices.size() || data.polygonIndices.at(nextIndex) < 0) {
break;
}
}
beginIndex = endIndex;
}
}
}

View file

@ -24,15 +24,18 @@
#include <shared/NsightHelpers.h>
#include "ModelFormatLogging.h"
template<class T> int streamSize() {
template<class T>
int streamSize() {
return sizeof(T);
}
template<bool> int streamSize() {
template<bool>
int streamSize() {
return 1;
}
template<class T> QVariant readBinaryArray(QDataStream& in, int& position) {
template<class T>
QVariant readBinaryArray(QDataStream& in, int& position) {
quint32 arrayLength;
quint32 encoding;
quint32 compressedLength;
@ -45,9 +48,8 @@ template<class T> QVariant readBinaryArray(QDataStream& in, int& position) {
QVector<T> values;
if ((int)QSysInfo::ByteOrder == (int)in.byteOrder()) {
values.resize(arrayLength);
const unsigned int DEFLATE_ENCODING = 1;
QByteArray arrayData;
if (encoding == DEFLATE_ENCODING) {
if (encoding == FBX_PROPERTY_COMPRESSED_FLAG) {
// preface encoded data with uncompressed length
QByteArray compressed(sizeof(quint32) + compressedLength, 0);
*((quint32*)compressed.data()) = qToBigEndian<quint32>(arrayLength * sizeof(T));
@ -69,8 +71,7 @@ template<class T> QVariant readBinaryArray(QDataStream& in, int& position) {
}
} else {
values.reserve(arrayLength);
const unsigned int DEFLATE_ENCODING = 1;
if (encoding == DEFLATE_ENCODING) {
if (encoding == FBX_PROPERTY_COMPRESSED_FLAG) {
// preface encoded data with uncompressed length
QByteArray compressed(sizeof(quint32) + compressedLength, 0);
*((quint32*)compressed.data()) = qToBigEndian<quint32>(arrayLength * sizeof(T));
@ -350,8 +351,7 @@ FBXNode parseTextFBXNode(Tokenizer& tokenizer) {
FBXNode FBXReader::parseFBX(QIODevice* device) {
PROFILE_RANGE_EX(resource_parse, __FUNCTION__, 0xff0000ff, device);
// verify the prolog
const QByteArray BINARY_PROLOG = "Kaydara FBX Binary ";
if (device->peek(BINARY_PROLOG.size()) != BINARY_PROLOG) {
if (device->peek(FBX_BINARY_PROLOG.size()) != FBX_BINARY_PROLOG) {
// parse as a text file
FBXNode top;
Tokenizer tokenizer(device);
@ -377,15 +377,13 @@ FBXNode FBXReader::parseFBX(QIODevice* device) {
// Bytes 0 - 20: Kaydara FBX Binary \x00(file - magic, with 2 spaces at the end, then a NULL terminator).
// Bytes 21 - 22: [0x1A, 0x00](unknown but all observed files show these bytes).
// Bytes 23 - 26 : unsigned int, the version number. 7300 for version 7.3 for example.
const int HEADER_BEFORE_VERSION = 23;
const quint32 VERSION_FBX2016 = 7500;
in.skipRawData(HEADER_BEFORE_VERSION);
int position = HEADER_BEFORE_VERSION;
in.skipRawData(FBX_HEADER_BYTES_BEFORE_VERSION);
int position = FBX_HEADER_BYTES_BEFORE_VERSION;
quint32 fileVersion;
in >> fileVersion;
position += sizeof(fileVersion);
qCDebug(modelformat) << "fileVersion:" << fileVersion;
bool has64BitPositions = (fileVersion >= VERSION_FBX2016);
bool has64BitPositions = (fileVersion >= FBX_VERSION_2016);
// parse the top-level node
FBXNode top;

View file

@ -0,0 +1,200 @@
//
// FBXWriter.cpp
// libraries/fbx/src
//
// Created by Ryan Huffman on 9/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 "FBXWriter.h"
#include <QDebug>
#ifdef USE_FBX_2016_FORMAT
using FBXEndOffset = int64_t;
using FBXPropertyCount = uint64_t;
using FBXListLength = uint64_t;
#else
using FBXEndOffset = int32_t;
using FBXPropertyCount = uint32_t;
using FBXListLength = uint32_t;
#endif
template <typename T>
void writeVector(QDataStream& out, char ch, QVector<T> vec) {
// Minimum number of bytes to consider compressing
const int ATTEMPT_COMPRESSION_THRESHOLD_BYTES = 2000;
out.device()->write(&ch, 1);
out << (int32_t)vec.length();
auto data { QByteArray::fromRawData((const char*)vec.constData(), vec.length() * sizeof(T)) };
if (data.size() >= ATTEMPT_COMPRESSION_THRESHOLD_BYTES) {
auto compressedDataWithLength { qCompress(data) };
// qCompress packs a length uint32 at the beginning of the buffer, but the FBX format
// does not expect it. This removes it.
auto compressedData = QByteArray::fromRawData(
compressedDataWithLength.constData() + sizeof(uint32_t), compressedDataWithLength.size() - sizeof(uint32_t));
if (compressedData.size() < data.size()) {
out << FBX_PROPERTY_COMPRESSED_FLAG;
out << (int32_t)compressedData.size();
out.writeRawData(compressedData.constData(), compressedData.size());
return;
}
}
out << FBX_PROPERTY_UNCOMPRESSED_FLAG;
out << (int32_t)0;
out.writeRawData(data.constData(), data.size());
}
QByteArray FBXWriter::encodeFBX(const FBXNode& root) {
QByteArray data;
QDataStream out(&data, QIODevice::WriteOnly);
out.setByteOrder(QDataStream::LittleEndian);
out.setVersion(QDataStream::Qt_4_5);
out.writeRawData(FBX_BINARY_PROLOG, FBX_BINARY_PROLOG.size());
auto bytes = QByteArray(FBX_HEADER_BYTES_BEFORE_VERSION - FBX_BINARY_PROLOG.size(), '\0');
out.writeRawData(bytes, bytes.size());
#ifdef USE_FBX_2016_FORMAT
out << FBX_VERSION_2016;
#else
out << FBX_VERSION_2015;
#endif
for (auto& child : root.children) {
encodeNode(out, child);
}
encodeNode(out, FBXNode());
return data;
}
void FBXWriter::encodeNode(QDataStream& out, const FBXNode& node) {
auto device = out.device();
auto nodeStartPos = device->pos();
// endOffset (temporary, updated later)
out << (FBXEndOffset)0;
// Property count
out << (FBXPropertyCount)node.properties.size();
// Property list length (temporary, updated later)
out << (FBXListLength)0;
out << (quint8)node.name.size();
out.writeRawData(node.name, node.name.size());
auto nodePropertiesStartPos = device->pos();
for (const auto& prop : node.properties) {
encodeFBXProperty(out, prop);
}
// Go back and write property list length
auto nodePropertiesEndPos = device->pos();
device->seek(nodeStartPos + sizeof(FBXEndOffset) + sizeof(FBXPropertyCount));
out << (FBXListLength)(nodePropertiesEndPos - nodePropertiesStartPos);
device->seek(nodePropertiesEndPos);
for (auto& child : node.children) {
encodeNode(out, child);
}
if (node.children.length() > 0) {
encodeNode(out, FBXNode());
}
// Go back and write actual endOffset
auto nodeEndPos = device->pos();
device->seek(nodeStartPos);
out << (FBXEndOffset)(nodeEndPos);
device->seek(nodeEndPos);
}
void FBXWriter::encodeFBXProperty(QDataStream& out, const QVariant& prop) {
auto type = prop.userType();
switch (type) {
case QMetaType::Short:
out.device()->write("Y", 1);
out << prop.value<int16_t>();
break;
case QVariant::Type::Bool:
out.device()->write("C", 1);
out << prop.toBool();
break;
case QMetaType::Int:
out.device()->write("I", 1);
out << prop.toInt();
break;
encodeNode(out, FBXNode());
case QMetaType::Float:
out.device()->write("F", 1);
out << prop.toFloat();
break;
case QMetaType::Double:
out.device()->write("D", 1);
out << prop.toDouble();
break;
case QMetaType::LongLong:
out.device()->write("L", 1);
out << prop.toLongLong();
break;
case QMetaType::QString:
{
auto bytes = prop.toString().toUtf8();
out << 'S';
out << bytes.length();
out << bytes;
out << (int32_t)bytes.size();
out.writeRawData(bytes, bytes.size());
break;
}
case QMetaType::QByteArray:
{
auto bytes = prop.toByteArray();
out.device()->write("S", 1);
out << (int32_t)bytes.size();
out.writeRawData(bytes, bytes.size());
break;
}
default:
{
if (prop.canConvert<QVector<float>>()) {
writeVector(out, 'f', prop.value<QVector<float>>());
} else if (prop.canConvert<QVector<double>>()) {
writeVector(out, 'd', prop.value<QVector<double>>());
} else if (prop.canConvert<QVector<qint64>>()) {
writeVector(out, 'l', prop.value<QVector<qint64>>());
} else if (prop.canConvert<QVector<qint32>>()) {
writeVector(out, 'i', prop.value<QVector<qint32>>());
} else if (prop.canConvert<QVector<bool>>()) {
writeVector(out, 'b', prop.value<QVector<bool>>());
} else {
qDebug() << "Unsupported property type in FBXWriter::encodeNode: " << type << prop;
throw("Unsupported property type in FBXWriter::encodeNode: " + QString::number(type) + " " + prop.toString());
}
}
}
}

Some files were not shown because too many files have changed in this diff Show more