mirror of
https://thingvellir.net/git/overte
synced 2025-03-27 23:52:03 +01:00
Merge branch 'master' into android_new
This commit is contained in:
commit
e42068f681
211 changed files with 9083 additions and 4103 deletions
2
BUILD.md
2
BUILD.md
|
@ -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
|
||||
|
|
|
@ -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
|
@ -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
|
||||
|
|
14
assignment-client/src/assets/AssetServerLogging.cpp
Normal file
14
assignment-client/src/assets/AssetServerLogging.cpp
Normal 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")
|
19
assignment-client/src/assets/AssetServerLogging.h
Normal file
19
assignment-client/src/assets/AssetServerLogging.h
Normal 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
|
79
assignment-client/src/assets/BakeAssetTask.cpp
Normal file
79
assignment-client/src/assets/BakeAssetTask.cpp
Normal 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();
|
||||
}
|
||||
}
|
52
assignment-client/src/assets/BakeAssetTask.h
Normal file
52
assignment-client/src/assets/BakeAssetTask.h
Normal 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
40
cmake/externals/draco/CMakeLists.txt
vendored
Normal 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")
|
30
cmake/modules/FindDraco.cmake
Normal file
30
cmake/modules/FindDraco.cmake
Normal 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)
|
|
@ -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()
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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" }
|
||||
]
|
||||
}
|
||||
}
|
Binary file not shown.
1056
interface/resources/fonts/hifi-glyphs/icons-reference.html
Normal file
1056
interface/resources/fonts/hifi-glyphs/icons-reference.html
Normal file
File diff suppressed because it is too large
Load diff
421
interface/resources/fonts/hifi-glyphs/styles.css
Normal file
421
interface/resources/fonts/hifi-glyphs/styles.css
Normal 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";
|
||||
}
|
|
@ -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"
|
||||
|
|
113
interface/resources/qml/controls-uit/CheckBoxQQC2.qml
Normal file
113
interface/resources/qml/controls-uit/CheckBoxQQC2.qml
Normal 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
|
||||
}
|
||||
}
|
||||
|
|
@ -34,7 +34,10 @@ Item {
|
|||
|
||||
onClicked: {
|
||||
mouse.accepted = true;
|
||||
|
||||
webEntity.synthesizeKeyPress(glyph);
|
||||
webEntity.synthesizeKeyPress(glyph, mirrorText);
|
||||
|
||||
if (toggle) {
|
||||
toggled = !toggled;
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
51
interface/resources/qml/controls-uit/ToolTip.qml
Normal file
51
interface/resources/qml/controls-uit/ToolTip.qml
Normal 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; }
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
35
interface/resources/qml/hifi/audio/AudioTabButton.qml
Normal file
35
interface/resources/qml/hifi/audio/AudioTabButton.qml
Normal 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"
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(); }
|
||||
|
||||
}
|
||||
|
|
|
@ -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(); }
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -9,8 +9,6 @@
|
|||
//
|
||||
|
||||
import QtQuick 2.5
|
||||
import QtQuick.Controls 1.4
|
||||
import QtQuick.Controls.Styles 1.4
|
||||
|
||||
Text {
|
||||
id: root
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
||||
};
|
||||
|
|
|
@ -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)) {
|
||||
|
|
|
@ -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;
|
||||
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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); }
|
||||
|
|
|
@ -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)); }
|
||||
|
|
|
@ -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)) {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
10
libraries/baking/CMakeLists.txt
Normal file
10
libraries/baking/CMakeLists.txt
Normal 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})
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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
|
821
libraries/baking/src/FBXBaker.cpp
Normal file
821
libraries/baking/src/FBXBaker.cpp
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 };
|
||||
};
|
||||
|
|
@ -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;
|
||||
}
|
|
@ -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
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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) { }
|
||||
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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
345
libraries/fbx/src/FBX.h
Normal 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_
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
200
libraries/fbx/src/FBXWriter.cpp
Normal file
200
libraries/fbx/src/FBXWriter.cpp
Normal 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
Loading…
Reference in a new issue