mirror of
https://github.com/lubosz/overte.git
synced 2025-04-16 15:30:11 +02:00
Merge branch 'master' into fix_stored_size
This commit is contained in:
commit
7be40a48a1
118 changed files with 7432 additions and 1889 deletions
|
@ -34,6 +34,7 @@ module.exports = {
|
|||
"Quat": false,
|
||||
"Rates": false,
|
||||
"Recording": false,
|
||||
"Resource": false,
|
||||
"Reticle": false,
|
||||
"Scene": false,
|
||||
"Script": false,
|
||||
|
|
|
@ -30,6 +30,7 @@
|
|||
#include <ShutdownEventListener.h>
|
||||
#include <SoundCache.h>
|
||||
#include <ResourceScriptingInterface.h>
|
||||
#include <UserActivityLoggerScriptingInterface.h>
|
||||
|
||||
#include "AssignmentFactory.h"
|
||||
#include "AssignmentDynamicFactory.h"
|
||||
|
@ -66,6 +67,7 @@ AssignmentClient::AssignmentClient(Assignment::Type requestAssignmentType, QStri
|
|||
DependencyManager::registerInheritance<EntityDynamicFactoryInterface, AssignmentDynamicFactory>();
|
||||
auto dynamicFactory = DependencyManager::set<AssignmentDynamicFactory>();
|
||||
DependencyManager::set<ResourceScriptingInterface>();
|
||||
DependencyManager::set<UserActivityLoggerScriptingInterface>();
|
||||
|
||||
// setup a thread for the NodeList and its PacketReceiver
|
||||
QThread* nodeThread = new QThread(this);
|
||||
|
|
|
@ -11,6 +11,8 @@
|
|||
|
||||
#include "SendAssetTask.h"
|
||||
|
||||
#include <cmath>
|
||||
|
||||
#include <QFile>
|
||||
|
||||
#include <DependencyManager.h>
|
||||
|
@ -21,6 +23,7 @@
|
|||
#include <udt/Packet.h>
|
||||
|
||||
#include "AssetUtils.h"
|
||||
#include "ByteRange.h"
|
||||
#include "ClientServerUtils.h"
|
||||
|
||||
SendAssetTask::SendAssetTask(QSharedPointer<ReceivedMessage> message, const SharedNodePointer& sendToNode, const QDir& resourcesDir) :
|
||||
|
@ -34,20 +37,21 @@ SendAssetTask::SendAssetTask(QSharedPointer<ReceivedMessage> message, const Shar
|
|||
|
||||
void SendAssetTask::run() {
|
||||
MessageID messageID;
|
||||
DataOffset start, end;
|
||||
|
||||
ByteRange byteRange;
|
||||
|
||||
_message->readPrimitive(&messageID);
|
||||
QByteArray assetHash = _message->read(SHA256_HASH_LENGTH);
|
||||
|
||||
// `start` and `end` indicate the range of data to retrieve for the asset identified by `assetHash`.
|
||||
// `start` is inclusive, `end` is exclusive. Requesting `start` = 1, `end` = 10 will retrieve 9 bytes of data,
|
||||
// starting at index 1.
|
||||
_message->readPrimitive(&start);
|
||||
_message->readPrimitive(&end);
|
||||
_message->readPrimitive(&byteRange.fromInclusive);
|
||||
_message->readPrimitive(&byteRange.toExclusive);
|
||||
|
||||
QString hexHash = assetHash.toHex();
|
||||
|
||||
qDebug() << "Received a request for the file (" << messageID << "): " << hexHash << " from " << start << " to " << end;
|
||||
qDebug() << "Received a request for the file (" << messageID << "): " << hexHash << " from "
|
||||
<< byteRange.fromInclusive << " to " << byteRange.toExclusive;
|
||||
|
||||
qDebug() << "Starting task to send asset: " << hexHash << " for messageID " << messageID;
|
||||
auto replyPacketList = NLPacketList::create(PacketType::AssetGetReply, QByteArray(), true, true);
|
||||
|
@ -56,7 +60,7 @@ void SendAssetTask::run() {
|
|||
|
||||
replyPacketList->writePrimitive(messageID);
|
||||
|
||||
if (end <= start) {
|
||||
if (!byteRange.isValid()) {
|
||||
replyPacketList->writePrimitive(AssetServerError::InvalidByteRange);
|
||||
} else {
|
||||
QString filePath = _resourcesDir.filePath(QString(hexHash));
|
||||
|
@ -64,15 +68,40 @@ void SendAssetTask::run() {
|
|||
QFile file { filePath };
|
||||
|
||||
if (file.open(QIODevice::ReadOnly)) {
|
||||
if (file.size() < end) {
|
||||
|
||||
// first fixup the range based on the now known file size
|
||||
byteRange.fixupRange(file.size());
|
||||
|
||||
// check if we're being asked to read data that we just don't have
|
||||
// because of the file size
|
||||
if (file.size() < byteRange.fromInclusive || file.size() < byteRange.toExclusive) {
|
||||
replyPacketList->writePrimitive(AssetServerError::InvalidByteRange);
|
||||
qCDebug(networking) << "Bad byte range: " << hexHash << " " << start << ":" << end;
|
||||
qCDebug(networking) << "Bad byte range: " << hexHash << " "
|
||||
<< byteRange.fromInclusive << ":" << byteRange.toExclusive;
|
||||
} else {
|
||||
auto size = end - start;
|
||||
file.seek(start);
|
||||
replyPacketList->writePrimitive(AssetServerError::NoError);
|
||||
replyPacketList->writePrimitive(size);
|
||||
replyPacketList->write(file.read(size));
|
||||
// we have a valid byte range, handle it and send the asset
|
||||
auto size = byteRange.size();
|
||||
|
||||
if (byteRange.fromInclusive >= 0) {
|
||||
|
||||
// this range is positive, meaning we just need to seek into the file and then read from there
|
||||
file.seek(byteRange.fromInclusive);
|
||||
replyPacketList->writePrimitive(AssetServerError::NoError);
|
||||
replyPacketList->writePrimitive(size);
|
||||
replyPacketList->write(file.read(size));
|
||||
} else {
|
||||
// this range is negative, at least the first part of the read will be back into the end of the file
|
||||
|
||||
// seek to the part of the file where the negative range begins
|
||||
file.seek(file.size() + byteRange.fromInclusive);
|
||||
|
||||
replyPacketList->writePrimitive(AssetServerError::NoError);
|
||||
replyPacketList->writePrimitive(size);
|
||||
|
||||
// first write everything from the negative range to the end of the file
|
||||
replyPacketList->write(file.read(size));
|
||||
}
|
||||
|
||||
qCDebug(networking) << "Sending asset: " << hexHash;
|
||||
}
|
||||
file.close();
|
||||
|
|
|
@ -481,14 +481,14 @@ void EntityScriptServer::deletingEntity(const EntityItemID& entityID) {
|
|||
}
|
||||
}
|
||||
|
||||
void EntityScriptServer::entityServerScriptChanging(const EntityItemID& entityID, const bool reload) {
|
||||
void EntityScriptServer::entityServerScriptChanging(const EntityItemID& entityID, bool reload) {
|
||||
if (_entityViewer.getTree() && !_shuttingDown) {
|
||||
_entitiesScriptEngine->unloadEntityScript(entityID, true);
|
||||
checkAndCallPreload(entityID, reload);
|
||||
}
|
||||
}
|
||||
|
||||
void EntityScriptServer::checkAndCallPreload(const EntityItemID& entityID, const bool reload) {
|
||||
void EntityScriptServer::checkAndCallPreload(const EntityItemID& entityID, bool reload) {
|
||||
if (_entityViewer.getTree() && !_shuttingDown && _entitiesScriptEngine) {
|
||||
|
||||
EntityItemPointer entity = _entityViewer.getTree()->findEntityByEntityItemID(entityID);
|
||||
|
|
|
@ -67,8 +67,8 @@ private:
|
|||
|
||||
void addingEntity(const EntityItemID& entityID);
|
||||
void deletingEntity(const EntityItemID& entityID);
|
||||
void entityServerScriptChanging(const EntityItemID& entityID, const bool reload);
|
||||
void checkAndCallPreload(const EntityItemID& entityID, const bool reload = false);
|
||||
void entityServerScriptChanging(const EntityItemID& entityID, bool reload);
|
||||
void checkAndCallPreload(const EntityItemID& entityID, bool reload = false);
|
||||
|
||||
void cleanupOldKilledListeners();
|
||||
|
||||
|
|
BIN
interface/resources/icons/loader-red-countdown-ring.gif
Normal file
BIN
interface/resources/icons/loader-red-countdown-ring.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 38 KiB |
109
interface/resources/icons/tablet-icons/avatar-record-a.svg
Normal file
109
interface/resources/icons/tablet-icons/avatar-record-a.svg
Normal file
|
@ -0,0 +1,109 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Generator: Adobe Illustrator 19.2.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
version="1.1"
|
||||
id="Layer_1"
|
||||
x="0px"
|
||||
y="0px"
|
||||
viewBox="0 0 50 50"
|
||||
style="enable-background:new 0 0 50 50;"
|
||||
xml:space="preserve"
|
||||
sodipodi:docname="avatar-record-a.svg"
|
||||
inkscape:version="0.92.1 r15371"><metadata
|
||||
id="metadata36"><rdf:RDF><cc:Work
|
||||
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
|
||||
id="defs34" /><sodipodi:namedview
|
||||
pagecolor="#ff0000"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1"
|
||||
objecttolerance="10"
|
||||
gridtolerance="10"
|
||||
guidetolerance="10"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:window-width="1829"
|
||||
inkscape:window-height="1057"
|
||||
id="namedview32"
|
||||
showgrid="false"
|
||||
inkscape:zoom="4.72"
|
||||
inkscape:cx="-9.4279661"
|
||||
inkscape:cy="25"
|
||||
inkscape:window-x="83"
|
||||
inkscape:window-y="-8"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="Layer_1" /><style
|
||||
type="text/css"
|
||||
id="style2">
|
||||
.st0{fill:#FFFFFF;}
|
||||
</style><g
|
||||
id="Layer_2" /><g
|
||||
id="g879"><path
|
||||
class="st0"
|
||||
d="m 23.2,20.5 c -1,0.8 -1.8,1.4 -2.7,2.1 -0.2,0.1 -0.2,0.4 -0.2,0.7 -0.3,1.7 -0.6,3.4 -0.9,5.1 -0.1,0.8 -0.6,1.2 -1.3,1.1 -0.7,-0.1 -1.2,-0.7 -1.1,-1.4 0.3,-2.2 0.6,-4.4 1,-6.6 0.1,-0.3 0.3,-0.7 0.6,-0.9 1.4,-1.3 2.8,-2.5 4.2,-3.7 0.7,-0.6 1.5,-1 2.4,-0.9 0.3,0 0.7,0 1,0 1,-0.1 1.7,0.4 2.1,1.3 0.7,1.4 1.4,2.8 1.9,4.3 0.5,1.3 1.2,2.1 2.4,2.6 1,0.4 2,1 3,1.5 0.2,0.1 0.5,0.3 0.7,0.5 0.4,0.4 0.5,1 0.3,1.4 C 36.4,28 36,28.1 35.5,28 35.1,27.9 34.7,27.8 34.3,27.6 33,27 31.8,26.4 30.6,25.8 29.8,25.5 29.2,25 28.8,24.2 c -0.2,-0.3 -0.4,-0.6 -0.7,-1 -0.1,0.3 -0.1,0.5 -0.2,0.7 -0.3,1.2 -0.5,2.4 -0.8,3.6 -0.1,0.4 0,0.7 0.2,1 2.2,3.7 4.4,7.4 6.6,11.1 0.3,0.4 0.4,1 0.5,1.5 0.1,0.7 -0.1,1.3 -0.7,1.6 C 33,43.1 32.3,43.1 31.8,42.6 31.4,42.2 31,41.8 30.7,41.3 28.2,37.4 25.7,33.4 23.2,29.5 22.8,28.8 22.4,28 22.1,27.3 22,26.9 22,26.4 22.1,26 c 0.4,-1.8 0.7,-3.6 1.1,-5.5 z"
|
||||
id="path5"
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#000000;fill-opacity:1" /><path
|
||||
class="st0"
|
||||
d="M 23.2,33.9 C 23.1,33.8 23,33.7 23,33.6 c 0,0 0,0 0,0 -0.2,-0.2 -0.3,-0.5 -0.5,-0.7 -0.3,-0.4 -0.6,-0.8 -0.9,-1.1 -0.3,1 -0.5,2 -0.8,3 -0.1,0.3 -0.3,0.7 -0.4,1 -1,1.5 -2,3.1 -3,4.6 -0.2,0.4 -0.4,0.8 -0.6,1.3 -0.2,0.9 0.7,1.9 1.6,1.5 0.5,-0.2 1,-0.7 1.3,-1.1 0.9,-1.1 1.6,-2.3 2.5,-3.3 0.8,-1 1.4,-2.2 1.8,-3.4 -0.2,-0.7 -0.5,-1.1 -0.8,-1.5 z"
|
||||
id="path7"
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#000000;fill-opacity:1" /><path
|
||||
class="st0"
|
||||
d="M 29,11.6 C 29,12.9 27.9,14 26.6,14 H 26.4 C 25.1,14 24,12.9 24,11.6 V 10.4 C 24,9.1 25.1,8 26.4,8 h 0.2 c 1.3,0 2.4,1.1 2.4,2.4 z"
|
||||
id="path9"
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#000000;fill-opacity:1" /><path
|
||||
class="st0"
|
||||
d="m 43.4,24.1 c -0.5,0.3 -0.9,0.5 -1.4,0.8 v 6.3 h 2.3 v -7.6 c -0.3,0.2 -0.6,0.3 -0.9,0.5 z"
|
||||
id="path11"
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#000000;fill-opacity:1" /><path
|
||||
class="st0"
|
||||
d="M 42,38.6 V 39 c 0,1.2 -1,2.1 -2.1,2.1 h -0.8 v 2.3 h 0.8 c 2.5,0 4.5,-2 4.5,-4.5 V 38.5 H 42 Z"
|
||||
id="path13"
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#000000;fill-opacity:1" /><path
|
||||
class="st0"
|
||||
d="m 9.7,12.2 v -0.4 c 0,-1.2 1,-2.1 2.1,-2.1 h 2 V 7.3 h -2 c -2.5,0 -4.5,2 -4.5,4.5 v 0.4 z"
|
||||
id="path15"
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#000000;fill-opacity:1" /><rect
|
||||
x="7.4000001"
|
||||
y="18.299999"
|
||||
class="st0"
|
||||
width="2.3"
|
||||
height="12.9"
|
||||
id="rect17"
|
||||
style="fill:#000000;fill-opacity:1" /><path
|
||||
class="st0"
|
||||
d="M 9.7,38.9 V 38.5 H 7.4 v 0.4 c 0,2.5 2,4.5 4.5,4.5 h 2 v -2.3 h -2 c -1.2,0 -2.2,-1 -2.2,-2.2 z"
|
||||
id="path19"
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#000000;fill-opacity:1" /><g
|
||||
style="fill:#000000;fill-opacity:1"
|
||||
id="g25"><circle
|
||||
class="st0"
|
||||
cx="38.599998"
|
||||
cy="13.3"
|
||||
r="2.2"
|
||||
id="circle21"
|
||||
style="fill:#000000;fill-opacity:1" /><path
|
||||
class="st0"
|
||||
d="m 38.6,15.5 c -1.2,0 -2.2,-1 -2.2,-2.2 0,-1.2 1,-2.2 2.2,-2.2 1.2,0 2.2,1 2.2,2.2 0,1.2 -1,2.2 -2.2,2.2 z m 0,-4.3 c -1.1,0 -2.1,0.9 -2.1,2.1 0,1.2 0.9,2.1 2.1,2.1 1.1,0 2.1,-0.9 2.1,-2.1 0,-1.2 -1,-2.1 -2.1,-2.1 z"
|
||||
id="path23"
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#000000;fill-opacity:1" /></g><path
|
||||
class="st0"
|
||||
d="m 38.6,19.7 c -3.6,0 -6.4,-2.9 -6.4,-6.4 0,-3.5 2.9,-6.4 6.4,-6.4 3.6,0 6.4,2.9 6.4,6.4 0,3.5 -2.9,6.4 -6.4,6.4 z m 0,-10.6 c -2.3,0 -4.2,1.9 -4.2,4.2 0,2.3 1.9,4.2 4.2,4.2 2.3,0 4.2,-1.9 4.2,-4.2 0,-2.3 -1.9,-4.2 -4.2,-4.2 z"
|
||||
id="path27"
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#000000;fill-opacity:1" /></g></svg>
|
After Width: | Height: | Size: 5.4 KiB |
36
interface/resources/icons/tablet-icons/avatar-record-i.svg
Normal file
36
interface/resources/icons/tablet-icons/avatar-record-i.svg
Normal file
|
@ -0,0 +1,36 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 19.2.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 50 50" style="enable-background:new 0 0 50 50;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#FFFFFF;}
|
||||
</style>
|
||||
<g id="Layer_2">
|
||||
</g>
|
||||
<g>
|
||||
<path class="st0" d="M23.2,20.5c-1,0.8-1.8,1.4-2.7,2.1c-0.2,0.1-0.2,0.4-0.2,0.7c-0.3,1.7-0.6,3.4-0.9,5.1
|
||||
c-0.1,0.8-0.6,1.2-1.3,1.1c-0.7-0.1-1.2-0.7-1.1-1.4c0.3-2.2,0.6-4.4,1-6.6c0.1-0.3,0.3-0.7,0.6-0.9c1.4-1.3,2.8-2.5,4.2-3.7
|
||||
c0.7-0.6,1.5-1,2.4-0.9c0.3,0,0.7,0,1,0c1-0.1,1.7,0.4,2.1,1.3c0.7,1.4,1.4,2.8,1.9,4.3c0.5,1.3,1.2,2.1,2.4,2.6c1,0.4,2,1,3,1.5
|
||||
c0.2,0.1,0.5,0.3,0.7,0.5c0.4,0.4,0.5,1,0.3,1.4C36.4,28,36,28.1,35.5,28c-0.4-0.1-0.8-0.2-1.2-0.4c-1.3-0.6-2.5-1.2-3.7-1.8
|
||||
c-0.8-0.3-1.4-0.8-1.8-1.6c-0.2-0.3-0.4-0.6-0.7-1c-0.1,0.3-0.1,0.5-0.2,0.7c-0.3,1.2-0.5,2.4-0.8,3.6c-0.1,0.4,0,0.7,0.2,1
|
||||
c2.2,3.7,4.4,7.4,6.6,11.1c0.3,0.4,0.4,1,0.5,1.5c0.1,0.7-0.1,1.3-0.7,1.6c-0.7,0.4-1.4,0.4-1.9-0.1c-0.4-0.4-0.8-0.8-1.1-1.3
|
||||
c-2.5-3.9-5-7.9-7.5-11.8c-0.4-0.7-0.8-1.5-1.1-2.2c-0.1-0.4-0.1-0.9,0-1.3C22.5,24.2,22.8,22.4,23.2,20.5z"/>
|
||||
<path class="st0" d="M23.2,33.9c-0.1-0.1-0.2-0.2-0.2-0.3c0,0,0,0,0,0c-0.2-0.2-0.3-0.5-0.5-0.7c-0.3-0.4-0.6-0.8-0.9-1.1
|
||||
c-0.3,1-0.5,2-0.8,3c-0.1,0.3-0.3,0.7-0.4,1c-1,1.5-2,3.1-3,4.6c-0.2,0.4-0.4,0.8-0.6,1.3c-0.2,0.9,0.7,1.9,1.6,1.5
|
||||
c0.5-0.2,1-0.7,1.3-1.1c0.9-1.1,1.6-2.3,2.5-3.3c0.8-1,1.4-2.2,1.8-3.4C23.8,34.7,23.5,34.3,23.2,33.9z"/>
|
||||
<path class="st0" d="M29,11.6c0,1.3-1.1,2.4-2.4,2.4h-0.2c-1.3,0-2.4-1.1-2.4-2.4v-1.2C24,9.1,25.1,8,26.4,8h0.2
|
||||
c1.3,0,2.4,1.1,2.4,2.4V11.6z"/>
|
||||
<path class="st0" d="M43.4,24.1c-0.5,0.3-0.9,0.5-1.4,0.8v6.3h2.3v-7.6C44,23.8,43.7,23.9,43.4,24.1z"/>
|
||||
<path class="st0" d="M42,38.6v0.4c0,1.2-1,2.1-2.1,2.1h-0.8v2.3h0.8c2.5,0,4.5-2,4.5-4.5v-0.4H42z"/>
|
||||
<path class="st0" d="M9.7,12.2v-0.4c0-1.2,1-2.1,2.1-2.1h2V7.3h-2c-2.5,0-4.5,2-4.5,4.5v0.4H9.7z"/>
|
||||
<rect x="7.4" y="18.3" class="st0" width="2.3" height="12.9"/>
|
||||
<path class="st0" d="M9.7,38.9v-0.4H7.4v0.4c0,2.5,2,4.5,4.5,4.5h2v-2.3h-2C10.7,41.1,9.7,40.1,9.7,38.9z"/>
|
||||
<g>
|
||||
<circle class="st0" cx="38.6" cy="13.3" r="2.2"/>
|
||||
<path class="st0" d="M38.6,15.5c-1.2,0-2.2-1-2.2-2.2s1-2.2,2.2-2.2c1.2,0,2.2,1,2.2,2.2S39.8,15.5,38.6,15.5z M38.6,11.2
|
||||
c-1.1,0-2.1,0.9-2.1,2.1s0.9,2.1,2.1,2.1c1.1,0,2.1-0.9,2.1-2.1S39.7,11.2,38.6,11.2z"/>
|
||||
</g>
|
||||
<path class="st0" d="M38.6,19.7c-3.6,0-6.4-2.9-6.4-6.4s2.9-6.4,6.4-6.4c3.6,0,6.4,2.9,6.4,6.4S42.1,19.7,38.6,19.7z M38.6,9.1
|
||||
c-2.3,0-4.2,1.9-4.2,4.2s1.9,4.2,4.2,4.2c2.3,0,4.2-1.9,4.2-4.2S40.9,9.1,38.6,9.1z"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 2.6 KiB |
94
interface/resources/icons/tablet-icons/doppleganger-a.svg
Normal file
94
interface/resources/icons/tablet-icons/doppleganger-a.svg
Normal file
|
@ -0,0 +1,94 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Generator: Adobe Illustrator 19.2.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
|
||||
<svg
|
||||
xmlns:osb="http://www.openswatchbook.org/uri/2009/osb"
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
version="1.1"
|
||||
x="0px"
|
||||
y="0px"
|
||||
viewBox="0 0 50 50"
|
||||
style="enable-background:new 0 0 50 50;"
|
||||
xml:space="preserve"
|
||||
id="svg2"
|
||||
inkscape:version="0.91 r13725"
|
||||
sodipodi:docname="doppleganger-a.svg"><metadata
|
||||
id="metadata36"><rdf:RDF><cc:Work
|
||||
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title /></cc:Work></rdf:RDF></metadata><defs
|
||||
id="defs34"><linearGradient
|
||||
id="linearGradient8353"
|
||||
osb:paint="solid"><stop
|
||||
style="stop-color:#000000;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop8355" /></linearGradient></defs><sodipodi:namedview
|
||||
pagecolor="#ff4900"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1"
|
||||
objecttolerance="10"
|
||||
gridtolerance="10"
|
||||
guidetolerance="10"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1004"
|
||||
id="namedview32"
|
||||
showgrid="false"
|
||||
inkscape:zoom="9.44"
|
||||
inkscape:cx="-3.2806499"
|
||||
inkscape:cy="20.640561"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="24"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="g4308" /><style
|
||||
type="text/css"
|
||||
id="style4">
|
||||
.st0{fill:#FFFFFF;}
|
||||
</style><g
|
||||
id="Layer_2" /><g
|
||||
id="Layer_1"
|
||||
style="fill:#000000;fill-opacity:1"><g
|
||||
id="g8375"
|
||||
transform="matrix(1.0667546,0,0,1.0667546,-2.1894733,-1.7818707)"><g
|
||||
id="g4308"
|
||||
transform="translate(1.0333645e-6,0)"><g
|
||||
id="g8"
|
||||
style="fill:#000000;fill-opacity:1"
|
||||
transform="matrix(1.1059001,0,0,1.1059001,-17.342989,-7.9561147)"><path
|
||||
class="st0"
|
||||
d="m 23.2,24.1 c -0.8,0.9 -1.5,1.8 -2.2,2.6 -0.1,0.2 -0.1,0.5 -0.1,0.7 0.1,1.7 0.2,3.4 0.2,5.1 0,0.8 -0.4,1.2 -1.1,1.3 -0.7,0.1 -1.3,-0.4 -1.4,-1.1 -0.2,-2.2 -0.3,-4.3 -0.5,-6.5 0,-0.3 0.1,-0.7 0.4,-1 1.1,-1.5 2.3,-3 3.4,-4.5 0.6,-0.7 1.6,-1.6 2.6,-1.6 0.3,0 1.1,0 1.4,0 0.8,-0.1 1.3,0.1 1.9,0.9 1,1.2 1.5,2.3 2.4,3.6 0.7,1.1 1.4,1.6 2.9,1.9 1.1,0.2 2.2,0.5 3.3,0.8 0.3,0.1 0.6,0.2 0.8,0.3 0.5,0.3 0.7,0.8 0.6,1.3 -0.1,0.5 -0.5,0.7 -1,0.8 -0.4,0 -0.9,0 -1.3,-0.1 -1.4,-0.3 -2.7,-0.6 -4.1,-0.9 -0.8,-0.2 -1.5,-0.6 -2.1,-1.1 -0.3,-0.3 -0.6,-0.5 -0.9,-0.8 0,0.3 0,0.5 0,0.7 0,1.2 0,2.4 0,3.6 0,0.4 -0.3,12.6 -0.1,16.8 0,0.5 -0.1,1 -0.2,1.5 -0.2,0.7 -0.6,1 -1.4,1.1 -0.8,0 -1.4,-0.3 -1.7,-1 C 24.8,48 24.7,47.4 24.6,46.9 24.2,42.3 23.7,34 23.5,33.1 23.4,32.3 23.3,32 23.2,31 c -0.1,-0.5 -0.1,-0.9 -0.1,-1.3 0.2,-1.8 0.1,-3.6 0.1,-5.6 z"
|
||||
id="path10"
|
||||
style="fill:#000000;fill-opacity:1"
|
||||
inkscape:connector-curvature="0" /><path
|
||||
class="st0"
|
||||
d="m 28.2,14.6 c 0,1.4 -1.1,2.6 -2.6,2.6 l 0,0 C 24.2,17.2 23,16.1 23,14.6 L 23,13 c 0,-1.4 1.1,-2.6 2.6,-2.6 l 0,0 c 1.4,0 2.6,1.1 2.6,2.6 l 0,1.6 z"
|
||||
id="path12"
|
||||
style="fill:#000000;fill-opacity:1"
|
||||
inkscape:connector-curvature="0" /></g><g
|
||||
id="g8-3"
|
||||
style="opacity:0.5;fill:#808080;fill-opacity:1;stroke:#000000;stroke-width:0.59335912;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:0.29667956,0.29667956;stroke-dashoffset:0;stroke-opacity:1"
|
||||
transform="matrix(-1.1059001,0,0,1.1059001,67.821392,-7.9561147)"><path
|
||||
class="st0"
|
||||
d="m 23.2,24.1 c -0.8,0.9 -1.5,1.8 -2.2,2.6 -0.1,0.2 -0.1,0.5 -0.1,0.7 0.1,1.7 0.2,3.4 0.2,5.1 0,0.8 -0.4,1.2 -1.1,1.3 -0.7,0.1 -1.3,-0.4 -1.4,-1.1 -0.2,-2.2 -0.3,-4.3 -0.5,-6.5 0,-0.3 0.1,-0.7 0.4,-1 1.1,-1.5 2.3,-3 3.4,-4.5 0.6,-0.7 1.6,-1.6 2.6,-1.6 0.3,0 1.1,0 1.4,0 0.8,-0.1 1.3,0.1 1.9,0.9 1,1.2 1.5,2.3 2.4,3.6 0.7,1.1 1.4,1.6 2.9,1.9 1.1,0.2 2.2,0.5 3.3,0.8 0.3,0.1 0.6,0.2 0.8,0.3 0.5,0.3 0.7,0.8 0.6,1.3 -0.1,0.5 -0.5,0.7 -1,0.8 -0.4,0 -0.9,0 -1.3,-0.1 -1.4,-0.3 -2.7,-0.6 -4.1,-0.9 -0.8,-0.2 -1.5,-0.6 -2.1,-1.1 -0.3,-0.3 -0.6,-0.5 -0.9,-0.8 0,0.3 0,0.5 0,0.7 0,1.2 0,2.4 0,3.6 0,0.4 -0.3,12.6 -0.1,16.8 0,0.5 -0.1,1 -0.2,1.5 -0.2,0.7 -0.6,1 -1.4,1.1 -0.8,0 -1.4,-0.3 -1.7,-1 C 24.8,48 24.7,47.4 24.6,46.9 24.2,42.3 23.7,34 23.5,33.1 23.4,32.3 23.3,32 23.2,31 c -0.1,-0.5 -0.1,-0.9 -0.1,-1.3 0.2,-1.8 0.1,-3.6 0.1,-5.6 z"
|
||||
id="path10-6"
|
||||
style="fill:#808080;fill-opacity:1;stroke:#000000;stroke-width:0.59335912;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:0.29667956,0.29667956;stroke-dashoffset:0;stroke-opacity:1"
|
||||
inkscape:connector-curvature="0" /><path
|
||||
class="st0"
|
||||
d="m 28.2,14.6 c 0,1.4 -1.1,2.6 -2.6,2.6 l 0,0 C 24.2,17.2 23,16.1 23,14.6 L 23,13 c 0,-1.4 1.1,-2.6 2.6,-2.6 l 0,0 c 1.4,0 2.6,1.1 2.6,2.6 l 0,1.6 z"
|
||||
id="path12-7"
|
||||
style="fill:#808080;fill-opacity:1;stroke:#000000;stroke-width:0.59335912;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:0.29667956,0.29667956;stroke-dashoffset:0;stroke-opacity:1"
|
||||
inkscape:connector-curvature="0" /></g></g><rect
|
||||
style="opacity:0.5;fill:#808080;fill-opacity:1;stroke:#000000;stroke-width:0.15729524;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:0.62918094, 1.25836187;stroke-dashoffset:0;stroke-opacity:1"
|
||||
id="rect4306"
|
||||
width="0.12393159"
|
||||
height="46.498554"
|
||||
x="25.227457"
|
||||
y="1.8070068"
|
||||
rx="0"
|
||||
ry="0.9407174" /></g></g></svg>
|
After Width: | Height: | Size: 5.8 KiB |
94
interface/resources/icons/tablet-icons/doppleganger-i.svg
Normal file
94
interface/resources/icons/tablet-icons/doppleganger-i.svg
Normal file
|
@ -0,0 +1,94 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Generator: Adobe Illustrator 19.2.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
|
||||
<svg
|
||||
xmlns:osb="http://www.openswatchbook.org/uri/2009/osb"
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
version="1.1"
|
||||
x="0px"
|
||||
y="0px"
|
||||
viewBox="0 0 50 50"
|
||||
style="enable-background:new 0 0 50 50;"
|
||||
xml:space="preserve"
|
||||
id="svg2"
|
||||
inkscape:version="0.91 r13725"
|
||||
sodipodi:docname="doppleganger-i.svg"><metadata
|
||||
id="metadata36"><rdf:RDF><cc:Work
|
||||
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
|
||||
id="defs34"><linearGradient
|
||||
id="linearGradient8353"
|
||||
osb:paint="solid"><stop
|
||||
style="stop-color:#000000;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop8355" /></linearGradient></defs><sodipodi:namedview
|
||||
pagecolor="#ff4900"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1"
|
||||
objecttolerance="10"
|
||||
gridtolerance="10"
|
||||
guidetolerance="10"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1004"
|
||||
id="namedview32"
|
||||
showgrid="false"
|
||||
inkscape:zoom="9.44"
|
||||
inkscape:cx="-3.2806499"
|
||||
inkscape:cy="20.640561"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="24"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="g4308" /><style
|
||||
type="text/css"
|
||||
id="style4">
|
||||
.st0{fill:#FFFFFF;}
|
||||
</style><g
|
||||
id="Layer_2" /><g
|
||||
id="Layer_1"
|
||||
style="fill:#000000;fill-opacity:1"><g
|
||||
id="g8375"
|
||||
transform="matrix(1.0667546,0,0,1.0667546,-2.1894733,-1.7818707)"><g
|
||||
id="g4308"
|
||||
transform="translate(1.0333645e-6,0)"><g
|
||||
id="g8"
|
||||
style="fill:#ffffff;fill-opacity:1"
|
||||
transform="matrix(1.1059001,0,0,1.1059001,-17.342989,-7.9561147)"><path
|
||||
class="st0"
|
||||
d="m 23.2,24.1 c -0.8,0.9 -1.5,1.8 -2.2,2.6 -0.1,0.2 -0.1,0.5 -0.1,0.7 0.1,1.7 0.2,3.4 0.2,5.1 0,0.8 -0.4,1.2 -1.1,1.3 -0.7,0.1 -1.3,-0.4 -1.4,-1.1 -0.2,-2.2 -0.3,-4.3 -0.5,-6.5 0,-0.3 0.1,-0.7 0.4,-1 1.1,-1.5 2.3,-3 3.4,-4.5 0.6,-0.7 1.6,-1.6 2.6,-1.6 0.3,0 1.1,0 1.4,0 0.8,-0.1 1.3,0.1 1.9,0.9 1,1.2 1.5,2.3 2.4,3.6 0.7,1.1 1.4,1.6 2.9,1.9 1.1,0.2 2.2,0.5 3.3,0.8 0.3,0.1 0.6,0.2 0.8,0.3 0.5,0.3 0.7,0.8 0.6,1.3 -0.1,0.5 -0.5,0.7 -1,0.8 -0.4,0 -0.9,0 -1.3,-0.1 -1.4,-0.3 -2.7,-0.6 -4.1,-0.9 -0.8,-0.2 -1.5,-0.6 -2.1,-1.1 -0.3,-0.3 -0.6,-0.5 -0.9,-0.8 0,0.3 0,0.5 0,0.7 0,1.2 0,2.4 0,3.6 0,0.4 -0.3,12.6 -0.1,16.8 0,0.5 -0.1,1 -0.2,1.5 -0.2,0.7 -0.6,1 -1.4,1.1 -0.8,0 -1.4,-0.3 -1.7,-1 C 24.8,48 24.7,47.4 24.6,46.9 24.2,42.3 23.7,34 23.5,33.1 23.4,32.3 23.3,32 23.2,31 c -0.1,-0.5 -0.1,-0.9 -0.1,-1.3 0.2,-1.8 0.1,-3.6 0.1,-5.6 z"
|
||||
id="path10"
|
||||
style="fill:#ffffff;fill-opacity:1"
|
||||
inkscape:connector-curvature="0" /><path
|
||||
class="st0"
|
||||
d="m 28.2,14.6 c 0,1.4 -1.1,2.6 -2.6,2.6 l 0,0 C 24.2,17.2 23,16.1 23,14.6 L 23,13 c 0,-1.4 1.1,-2.6 2.6,-2.6 l 0,0 c 1.4,0 2.6,1.1 2.6,2.6 l 0,1.6 z"
|
||||
id="path12"
|
||||
style="fill:#ffffff;fill-opacity:1"
|
||||
inkscape:connector-curvature="0" /></g><g
|
||||
id="g8-3"
|
||||
style="opacity:0.5;fill:#808080;fill-opacity:1;stroke:#ffffff;stroke-width:0.59335912;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:0.29667956, 0.29667956000000001;stroke-dashoffset:0;stroke-opacity:1"
|
||||
transform="matrix(-1.1059001,0,0,1.1059001,67.821392,-7.9561147)"><path
|
||||
class="st0"
|
||||
d="m 23.2,24.1 c -0.8,0.9 -1.5,1.8 -2.2,2.6 -0.1,0.2 -0.1,0.5 -0.1,0.7 0.1,1.7 0.2,3.4 0.2,5.1 0,0.8 -0.4,1.2 -1.1,1.3 -0.7,0.1 -1.3,-0.4 -1.4,-1.1 -0.2,-2.2 -0.3,-4.3 -0.5,-6.5 0,-0.3 0.1,-0.7 0.4,-1 1.1,-1.5 2.3,-3 3.4,-4.5 0.6,-0.7 1.6,-1.6 2.6,-1.6 0.3,0 1.1,0 1.4,0 0.8,-0.1 1.3,0.1 1.9,0.9 1,1.2 1.5,2.3 2.4,3.6 0.7,1.1 1.4,1.6 2.9,1.9 1.1,0.2 2.2,0.5 3.3,0.8 0.3,0.1 0.6,0.2 0.8,0.3 0.5,0.3 0.7,0.8 0.6,1.3 -0.1,0.5 -0.5,0.7 -1,0.8 -0.4,0 -0.9,0 -1.3,-0.1 -1.4,-0.3 -2.7,-0.6 -4.1,-0.9 -0.8,-0.2 -1.5,-0.6 -2.1,-1.1 -0.3,-0.3 -0.6,-0.5 -0.9,-0.8 0,0.3 0,0.5 0,0.7 0,1.2 0,2.4 0,3.6 0,0.4 -0.3,12.6 -0.1,16.8 0,0.5 -0.1,1 -0.2,1.5 -0.2,0.7 -0.6,1 -1.4,1.1 -0.8,0 -1.4,-0.3 -1.7,-1 C 24.8,48 24.7,47.4 24.6,46.9 24.2,42.3 23.7,34 23.5,33.1 23.4,32.3 23.3,32 23.2,31 c -0.1,-0.5 -0.1,-0.9 -0.1,-1.3 0.2,-1.8 0.1,-3.6 0.1,-5.6 z"
|
||||
id="path10-6"
|
||||
style="fill:#808080;fill-opacity:1;stroke:#ffffff;stroke-width:0.59335912;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:0.29667956, 0.29667956000000001;stroke-dashoffset:0;stroke-opacity:1"
|
||||
inkscape:connector-curvature="0" /><path
|
||||
class="st0"
|
||||
d="m 28.2,14.6 c 0,1.4 -1.1,2.6 -2.6,2.6 l 0,0 C 24.2,17.2 23,16.1 23,14.6 L 23,13 c 0,-1.4 1.1,-2.6 2.6,-2.6 l 0,0 c 1.4,0 2.6,1.1 2.6,2.6 l 0,1.6 z"
|
||||
id="path12-7"
|
||||
style="fill:#808080;fill-opacity:1;stroke:#ffffff;stroke-width:0.59335912;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:0.29667956, 0.29667956000000001;stroke-dashoffset:0;stroke-opacity:1"
|
||||
inkscape:connector-curvature="0" /></g></g><rect
|
||||
style="opacity:0.5;fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-width:0.15729524;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:0.62918094, 1.25836187000000010;stroke-dashoffset:0;stroke-opacity:1"
|
||||
id="rect4306"
|
||||
width="0.12393159"
|
||||
height="46.498554"
|
||||
x="25.227457"
|
||||
y="1.8070068"
|
||||
rx="0"
|
||||
ry="0.9407174" /></g></g></svg>
|
After Width: | Height: | Size: 5.9 KiB |
|
@ -466,6 +466,11 @@ FocusScope {
|
|||
return fileDialogBuilder.createObject(desktop, properties);
|
||||
}
|
||||
|
||||
Component { id: assetDialogBuilder; AssetDialog { } }
|
||||
function assetDialog(properties) {
|
||||
return assetDialogBuilder.createObject(desktop, properties);
|
||||
}
|
||||
|
||||
function unfocusWindows() {
|
||||
// First find the active focus item, and unfocus it, all the way
|
||||
// up the parent chain to the window
|
||||
|
|
58
interface/resources/qml/dialogs/AssetDialog.qml
Normal file
58
interface/resources/qml/dialogs/AssetDialog.qml
Normal file
|
@ -0,0 +1,58 @@
|
|||
//
|
||||
// AssetDialog.qml
|
||||
//
|
||||
// Created by David Rowe on 18 Apr 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.5
|
||||
import QtQuick.Controls 1.4
|
||||
import Qt.labs.settings 1.0
|
||||
|
||||
import "../styles-uit"
|
||||
import "../windows"
|
||||
|
||||
import "assetDialog"
|
||||
|
||||
ModalWindow {
|
||||
id: root
|
||||
resizable: true
|
||||
implicitWidth: 480
|
||||
implicitHeight: 360
|
||||
|
||||
minSize: Qt.vector2d(360, 240)
|
||||
draggable: true
|
||||
|
||||
Settings {
|
||||
category: "AssetDialog"
|
||||
property alias width: root.width
|
||||
property alias height: root.height
|
||||
property alias x: root.x
|
||||
property alias y: root.y
|
||||
}
|
||||
|
||||
// Set from OffscreenUi::assetDialog().
|
||||
property alias caption: root.title
|
||||
property alias dir: assetDialogContent.dir
|
||||
property alias filter: assetDialogContent.filter
|
||||
property alias options: assetDialogContent.options
|
||||
|
||||
// Dialog results.
|
||||
signal selectedAsset(var asset);
|
||||
signal canceled();
|
||||
|
||||
property int titleWidth: 0 // For ModalFrame.
|
||||
|
||||
HifiConstants { id: hifi }
|
||||
|
||||
AssetDialogContent {
|
||||
id: assetDialogContent
|
||||
|
||||
width: pane.width
|
||||
height: pane.height
|
||||
anchors.margins: 0
|
||||
}
|
||||
}
|
53
interface/resources/qml/dialogs/TabletAssetDialog.qml
Normal file
53
interface/resources/qml/dialogs/TabletAssetDialog.qml
Normal file
|
@ -0,0 +1,53 @@
|
|||
//
|
||||
// TabletAssetDialog.qml
|
||||
//
|
||||
// Created by David Rowe on 18 Apr 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.5
|
||||
import QtQuick.Controls 1.4
|
||||
|
||||
import "../styles-uit"
|
||||
import "../windows"
|
||||
|
||||
import "assetDialog"
|
||||
|
||||
TabletModalWindow {
|
||||
id: root
|
||||
anchors.fill: parent
|
||||
width: parent.width
|
||||
height: parent.height
|
||||
|
||||
// Set from OffscreenUi::assetDialog().
|
||||
property alias caption: root.title
|
||||
property alias dir: assetDialogContent.dir
|
||||
property alias filter: assetDialogContent.filter
|
||||
property alias options: assetDialogContent.options
|
||||
|
||||
// Dialog results.
|
||||
signal selectedAsset(var asset);
|
||||
signal canceled();
|
||||
|
||||
property int titleWidth: 0 // For TabletModalFrame.
|
||||
|
||||
TabletModalFrame {
|
||||
id: frame
|
||||
anchors.fill: parent
|
||||
|
||||
AssetDialogContent {
|
||||
id: assetDialogContent
|
||||
singleClickNavigate: true
|
||||
width: parent.width - 12
|
||||
height: parent.height - frame.frameMarginTop - 12
|
||||
anchors {
|
||||
horizontalCenter: parent.horizontalCenter
|
||||
top: parent.top
|
||||
topMargin: parent.height - height - 6
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,536 @@
|
|||
//
|
||||
// AssetDialogContent.qml
|
||||
//
|
||||
// Created by David Rowe on 19 Apr 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.5
|
||||
import QtQuick.Controls 1.4
|
||||
|
||||
import "../../controls-uit"
|
||||
import "../../styles-uit"
|
||||
|
||||
import "../fileDialog"
|
||||
|
||||
Item {
|
||||
// Set from OffscreenUi::assetDialog()
|
||||
property alias dir: assetTableModel.folder
|
||||
property alias filter: selectionType.filtersString // FIXME: Currently only supports simple filters, "*.xxx".
|
||||
property int options // Not used.
|
||||
|
||||
property bool selectDirectory: false
|
||||
|
||||
// Not implemented.
|
||||
//property bool saveDialog: false;
|
||||
//property bool multiSelect: false;
|
||||
|
||||
property bool singleClickNavigate: false
|
||||
|
||||
HifiConstants { id: hifi }
|
||||
|
||||
Component.onCompleted: {
|
||||
homeButton.destination = dir;
|
||||
|
||||
if (selectDirectory) {
|
||||
d.currentSelectionIsFolder = true;
|
||||
d.currentSelectionPath = assetTableModel.folder;
|
||||
}
|
||||
|
||||
assetTableView.forceActiveFocus();
|
||||
}
|
||||
|
||||
Item {
|
||||
id: assetDialogItem
|
||||
anchors.fill: parent
|
||||
clip: true
|
||||
|
||||
MouseArea {
|
||||
// Clear selection when click on internal unused area.
|
||||
anchors.fill: parent
|
||||
drag.target: root
|
||||
onClicked: {
|
||||
d.clearSelection();
|
||||
frame.forceActiveFocus();
|
||||
assetTableView.forceActiveFocus();
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
id: navControls
|
||||
anchors {
|
||||
top: parent.top
|
||||
topMargin: hifi.dimensions.contentMargin.y
|
||||
left: parent.left
|
||||
}
|
||||
spacing: hifi.dimensions.contentSpacing.x
|
||||
|
||||
GlyphButton {
|
||||
id: upButton
|
||||
glyph: hifi.glyphs.levelUp
|
||||
width: height
|
||||
size: 30
|
||||
enabled: assetTableModel.parentFolder !== ""
|
||||
onClicked: d.navigateUp();
|
||||
}
|
||||
|
||||
GlyphButton {
|
||||
id: homeButton
|
||||
property string destination: ""
|
||||
glyph: hifi.glyphs.home
|
||||
size: 28
|
||||
width: height
|
||||
enabled: destination !== ""
|
||||
//onClicked: d.navigateHome();
|
||||
onClicked: assetTableModel.folder = destination;
|
||||
}
|
||||
}
|
||||
|
||||
ComboBox {
|
||||
id: pathSelector
|
||||
anchors {
|
||||
top: parent.top
|
||||
topMargin: hifi.dimensions.contentMargin.y
|
||||
left: navControls.right
|
||||
leftMargin: hifi.dimensions.contentSpacing.x
|
||||
right: parent.right
|
||||
}
|
||||
z: 10
|
||||
|
||||
property string lastValidFolder: assetTableModel.folder
|
||||
|
||||
function calculatePathChoices(folder) {
|
||||
var folders = folder.split("/"),
|
||||
choices = [],
|
||||
i, length;
|
||||
|
||||
if (folders[folders.length - 1] === "") {
|
||||
folders.pop();
|
||||
}
|
||||
|
||||
choices.push(folders[0]);
|
||||
|
||||
for (i = 1, length = folders.length; i < length; i++) {
|
||||
choices.push(choices[i - 1] + "/" + folders[i]);
|
||||
}
|
||||
|
||||
if (folders[0] === "") {
|
||||
choices[0] = "/";
|
||||
}
|
||||
|
||||
choices.reverse();
|
||||
|
||||
if (choices.length > 0) {
|
||||
pathSelector.model = choices;
|
||||
}
|
||||
}
|
||||
|
||||
onLastValidFolderChanged: {
|
||||
var folder = lastValidFolder;
|
||||
calculatePathChoices(folder);
|
||||
}
|
||||
|
||||
onCurrentTextChanged: {
|
||||
var folder = currentText;
|
||||
|
||||
if (folder !== "/") {
|
||||
folder += "/";
|
||||
}
|
||||
|
||||
if (folder !== assetTableModel.folder) {
|
||||
if (root.selectDirectory) {
|
||||
currentSelection.text = currentText;
|
||||
d.currentSelectionPath = currentText;
|
||||
}
|
||||
assetTableModel.folder = folder;
|
||||
assetTableView.forceActiveFocus();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
QtObject {
|
||||
id: d
|
||||
|
||||
property string currentSelectionPath
|
||||
property bool currentSelectionIsFolder
|
||||
property var tableViewConnection: Connections { target: assetTableView; onCurrentRowChanged: d.update(); }
|
||||
|
||||
function update() {
|
||||
var row = assetTableView.currentRow;
|
||||
|
||||
if (row === -1) {
|
||||
if (!root.selectDirectory) {
|
||||
currentSelection.text = "";
|
||||
currentSelectionIsFolder = false;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
var rowInfo = assetTableModel.get(row);
|
||||
currentSelectionPath = rowInfo.filePath;
|
||||
currentSelectionIsFolder = rowInfo.fileIsDir;
|
||||
if (root.selectDirectory || !currentSelectionIsFolder) {
|
||||
currentSelection.text = currentSelectionPath;
|
||||
} else {
|
||||
currentSelection.text = "";
|
||||
}
|
||||
}
|
||||
|
||||
function navigateUp() {
|
||||
if (assetTableModel.parentFolder !== "") {
|
||||
assetTableModel.folder = assetTableModel.parentFolder;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function navigateHome() {
|
||||
assetTableModel.folder = homeButton.destination;
|
||||
return true;
|
||||
}
|
||||
|
||||
function clearSelection() {
|
||||
assetTableView.selection.clear();
|
||||
assetTableView.currentRow = -1;
|
||||
update();
|
||||
}
|
||||
}
|
||||
|
||||
ListModel {
|
||||
id: assetTableModel
|
||||
|
||||
property string folder
|
||||
property string parentFolder: ""
|
||||
readonly property string rootFolder: "/"
|
||||
|
||||
onFolderChanged: {
|
||||
parentFolder = calculateParentFolder();
|
||||
update();
|
||||
}
|
||||
|
||||
function calculateParentFolder() {
|
||||
if (folder !== "/") {
|
||||
return folder.slice(0, folder.slice(0, -1).lastIndexOf("/") + 1);
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
function isFolder(row) {
|
||||
if (row === -1) {
|
||||
return false;
|
||||
}
|
||||
return get(row).fileIsDir;
|
||||
}
|
||||
|
||||
function onGetAllMappings(error, map) {
|
||||
var mappings,
|
||||
fileTypeFilter,
|
||||
index,
|
||||
path,
|
||||
fileName,
|
||||
fileType,
|
||||
fileIsDir,
|
||||
isValid,
|
||||
subDirectory,
|
||||
subDirectories = [],
|
||||
fileNameSort,
|
||||
rows = 0,
|
||||
lower,
|
||||
middle,
|
||||
upper,
|
||||
i,
|
||||
length;
|
||||
|
||||
clear();
|
||||
|
||||
if (error === "") {
|
||||
mappings = Object.keys(map);
|
||||
fileTypeFilter = filter.replace("*", "").toLowerCase();
|
||||
|
||||
for (i = 0, length = mappings.length; i < length; i++) {
|
||||
index = mappings[i].lastIndexOf("/");
|
||||
|
||||
path = mappings[i].slice(0, mappings[i].lastIndexOf("/") + 1);
|
||||
fileName = mappings[i].slice(path.length);
|
||||
fileType = fileName.slice(fileName.lastIndexOf("."));
|
||||
fileIsDir = false;
|
||||
isValid = false;
|
||||
|
||||
if (fileType.toLowerCase() === fileTypeFilter) {
|
||||
if (path === folder) {
|
||||
isValid = !selectDirectory;
|
||||
} else if (path.length > folder.length) {
|
||||
subDirectory = path.slice(folder.length);
|
||||
index = subDirectory.indexOf("/");
|
||||
if (index === subDirectory.lastIndexOf("/")) {
|
||||
fileName = subDirectory.slice(0, index);
|
||||
if (subDirectories.indexOf(fileName) === -1) {
|
||||
fileIsDir = true;
|
||||
isValid = true;
|
||||
subDirectories.push(fileName);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isValid) {
|
||||
fileNameSort = (fileIsDir ? "*" : "") + fileName.toLowerCase();
|
||||
|
||||
lower = 0;
|
||||
upper = rows;
|
||||
while (lower < upper) {
|
||||
middle = Math.floor((lower + upper) / 2);
|
||||
var lessThan;
|
||||
if (fileNameSort < get(middle)["fileNameSort"]) {
|
||||
lessThan = true;
|
||||
upper = middle;
|
||||
} else {
|
||||
lessThan = false;
|
||||
lower = middle + 1;
|
||||
}
|
||||
}
|
||||
|
||||
insert(lower, {
|
||||
fileName: fileName,
|
||||
filePath: path + (fileIsDir ? "" : fileName),
|
||||
fileIsDir: fileIsDir,
|
||||
fileNameSort: fileNameSort
|
||||
});
|
||||
|
||||
rows++;
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
console.log("Error getting mappings from Asset Server");
|
||||
}
|
||||
}
|
||||
|
||||
function update() {
|
||||
d.clearSelection();
|
||||
clear();
|
||||
Assets.getAllMappings(onGetAllMappings);
|
||||
}
|
||||
}
|
||||
|
||||
Table {
|
||||
id: assetTableView
|
||||
colorScheme: hifi.colorSchemes.light
|
||||
anchors {
|
||||
top: navControls.bottom
|
||||
topMargin: hifi.dimensions.contentSpacing.y
|
||||
left: parent.left
|
||||
right: parent.right
|
||||
bottom: currentSelection.top
|
||||
bottomMargin: hifi.dimensions.contentSpacing.y + currentSelection.controlHeight - currentSelection.height
|
||||
}
|
||||
|
||||
model: assetTableModel
|
||||
|
||||
focus: true
|
||||
|
||||
onClicked: {
|
||||
if (singleClickNavigate) {
|
||||
navigateToRow(row);
|
||||
}
|
||||
}
|
||||
|
||||
onDoubleClicked: navigateToRow(row);
|
||||
Keys.onReturnPressed: navigateToCurrentRow();
|
||||
Keys.onEnterPressed: navigateToCurrentRow();
|
||||
|
||||
itemDelegate: Item {
|
||||
clip: true
|
||||
|
||||
FontLoader { id: firaSansSemiBold; source: "../../../fonts/FiraSans-SemiBold.ttf"; }
|
||||
FontLoader { id: firaSansRegular; source: "../../../fonts/FiraSans-Regular.ttf"; }
|
||||
|
||||
FiraSansSemiBold {
|
||||
text: styleData.value
|
||||
elide: styleData.elideMode
|
||||
anchors {
|
||||
left: parent.left
|
||||
leftMargin: hifi.dimensions.tablePadding
|
||||
right: parent.right
|
||||
rightMargin: hifi.dimensions.tablePadding
|
||||
verticalCenter: parent.verticalCenter
|
||||
}
|
||||
size: hifi.fontSizes.tableText
|
||||
color: hifi.colors.baseGrayHighlight
|
||||
font.family: (styleData.row !== -1 && assetTableView.model.get(styleData.row).fileIsDir)
|
||||
? firaSansSemiBold.name : firaSansRegular.name
|
||||
}
|
||||
}
|
||||
|
||||
TableViewColumn {
|
||||
id: fileNameColumn
|
||||
role: "fileName"
|
||||
title: "Name"
|
||||
width: assetTableView.width
|
||||
movable: false
|
||||
resizable: false
|
||||
}
|
||||
|
||||
function navigateToRow(row) {
|
||||
currentRow = row;
|
||||
navigateToCurrentRow();
|
||||
}
|
||||
|
||||
function navigateToCurrentRow() {
|
||||
if (model.isFolder(currentRow)) {
|
||||
model.folder = model.get(currentRow).filePath;
|
||||
} else {
|
||||
okAction.trigger();
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: prefixClearTimer
|
||||
interval: 1000
|
||||
repeat: false
|
||||
running: false
|
||||
onTriggered: assetTableView.prefix = "";
|
||||
}
|
||||
|
||||
property string prefix: ""
|
||||
|
||||
function addToPrefix(event) {
|
||||
if (!event.text || event.text === "") {
|
||||
return false;
|
||||
}
|
||||
var newPrefix = prefix + event.text.toLowerCase();
|
||||
var matchedIndex = -1;
|
||||
for (var i = 0; i < model.count; ++i) {
|
||||
var name = model.get(i).fileName.toLowerCase();
|
||||
if (0 === name.indexOf(newPrefix)) {
|
||||
matchedIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (matchedIndex !== -1) {
|
||||
assetTableView.selection.clear();
|
||||
assetTableView.selection.select(matchedIndex);
|
||||
assetTableView.currentRow = matchedIndex;
|
||||
assetTableView.prefix = newPrefix;
|
||||
}
|
||||
prefixClearTimer.restart();
|
||||
return true;
|
||||
}
|
||||
|
||||
Keys.onPressed: {
|
||||
switch (event.key) {
|
||||
case Qt.Key_Backspace:
|
||||
case Qt.Key_Tab:
|
||||
case Qt.Key_Backtab:
|
||||
event.accepted = false;
|
||||
break;
|
||||
|
||||
default:
|
||||
if (addToPrefix(event)) {
|
||||
event.accepted = true
|
||||
} else {
|
||||
event.accepted = false;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TextField {
|
||||
id: currentSelection
|
||||
label: selectDirectory ? "Directory:" : "File name:"
|
||||
anchors {
|
||||
left: parent.left
|
||||
right: selectionType.visible ? selectionType.left: parent.right
|
||||
rightMargin: selectionType.visible ? hifi.dimensions.contentSpacing.x : 0
|
||||
bottom: buttonRow.top
|
||||
bottomMargin: hifi.dimensions.contentSpacing.y
|
||||
}
|
||||
readOnly: true
|
||||
activeFocusOnTab: !readOnly
|
||||
onActiveFocusChanged: if (activeFocus) { selectAll(); }
|
||||
onAccepted: okAction.trigger();
|
||||
}
|
||||
|
||||
FileTypeSelection {
|
||||
id: selectionType
|
||||
anchors {
|
||||
top: currentSelection.top
|
||||
left: buttonRow.left
|
||||
right: parent.right
|
||||
}
|
||||
visible: !selectDirectory && filtersCount > 1
|
||||
KeyNavigation.left: assetTableView
|
||||
KeyNavigation.right: openButton
|
||||
}
|
||||
|
||||
Action {
|
||||
id: okAction
|
||||
text: currentSelection.text && root.selectDirectory && assetTableView.currentRow === -1 ? "Choose" : "Open"
|
||||
enabled: currentSelection.text || !root.selectDirectory && d.currentSelectionIsFolder ? true : false
|
||||
onTriggered: {
|
||||
if (!root.selectDirectory && !d.currentSelectionIsFolder
|
||||
|| root.selectDirectory && assetTableView.currentRow === -1) {
|
||||
selectedAsset(d.currentSelectionPath);
|
||||
root.destroy();
|
||||
} else {
|
||||
assetTableView.navigateToCurrentRow();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Action {
|
||||
id: cancelAction
|
||||
text: "Cancel"
|
||||
onTriggered: {
|
||||
canceled();
|
||||
root.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
id: buttonRow
|
||||
anchors {
|
||||
right: parent.right
|
||||
bottom: parent.bottom
|
||||
}
|
||||
spacing: hifi.dimensions.contentSpacing.y
|
||||
|
||||
Button {
|
||||
id: openButton
|
||||
color: hifi.buttons.blue
|
||||
action: okAction
|
||||
Keys.onReturnPressed: okAction.trigger()
|
||||
KeyNavigation.up: selectionType
|
||||
KeyNavigation.left: selectionType
|
||||
KeyNavigation.right: cancelButton
|
||||
}
|
||||
|
||||
Button {
|
||||
id: cancelButton
|
||||
action: cancelAction
|
||||
KeyNavigation.up: selectionType
|
||||
KeyNavigation.left: openButton
|
||||
KeyNavigation.right: assetTableView.contentItem
|
||||
Keys.onReturnPressed: { canceled(); root.enabled = false }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Keys.onPressed: {
|
||||
switch (event.key) {
|
||||
case Qt.Key_Backspace:
|
||||
event.accepted = d.navigateUp();
|
||||
break;
|
||||
|
||||
case Qt.Key_Home:
|
||||
event.accepted = d.navigateHome();
|
||||
break;
|
||||
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,19 +1,11 @@
|
|||
import QtQuick 2.5
|
||||
import QtQuick.Controls 1.4
|
||||
import QtWebEngine 1.1
|
||||
import QtWebChannel 1.0
|
||||
import QtQuick.Controls.Styles 1.4
|
||||
import "../../controls"
|
||||
import "../toolbars"
|
||||
import HFWebEngineProfile 1.0
|
||||
import QtGraphicalEffects 1.0
|
||||
import "../../controls-uit" as HifiControls
|
||||
import "../../styles-uit"
|
||||
|
||||
StackView {
|
||||
id: editRoot
|
||||
objectName: "stack"
|
||||
initialItem: editBasePage
|
||||
initialItem: Qt.resolvedUrl('EditTabView.qml')
|
||||
|
||||
property var eventBridge;
|
||||
signal sendToScript(var message);
|
||||
|
@ -30,270 +22,10 @@ StackView {
|
|||
editRoot.pop();
|
||||
}
|
||||
|
||||
|
||||
Component {
|
||||
id: editBasePage
|
||||
TabView {
|
||||
id: editTabView
|
||||
// anchors.fill: parent
|
||||
height: 60
|
||||
|
||||
Tab {
|
||||
title: "CREATE"
|
||||
active: true
|
||||
enabled: true
|
||||
property string originalUrl: ""
|
||||
|
||||
Rectangle {
|
||||
color: "#404040"
|
||||
|
||||
Text {
|
||||
color: "#ffffff"
|
||||
text: "Choose an Entity Type to Create:"
|
||||
font.pixelSize: 14
|
||||
font.bold: true
|
||||
anchors.top: parent.top
|
||||
anchors.topMargin: 28
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: 28
|
||||
}
|
||||
|
||||
Flow {
|
||||
id: createEntitiesFlow
|
||||
spacing: 35
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: 55
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: 55
|
||||
anchors.top: parent.top
|
||||
anchors.topMargin: 70
|
||||
|
||||
|
||||
NewEntityButton {
|
||||
icon: "icons/create-icons/94-model-01.svg"
|
||||
text: "MODEL"
|
||||
onClicked: {
|
||||
editRoot.sendToScript({
|
||||
method: "newEntityButtonClicked", params: { buttonName: "newModelButton" }
|
||||
});
|
||||
editTabView.currentIndex = 2
|
||||
}
|
||||
}
|
||||
|
||||
NewEntityButton {
|
||||
icon: "icons/create-icons/21-cube-01.svg"
|
||||
text: "CUBE"
|
||||
onClicked: {
|
||||
editRoot.sendToScript({
|
||||
method: "newEntityButtonClicked", params: { buttonName: "newCubeButton" }
|
||||
});
|
||||
editTabView.currentIndex = 2
|
||||
}
|
||||
}
|
||||
|
||||
NewEntityButton {
|
||||
icon: "icons/create-icons/22-sphere-01.svg"
|
||||
text: "SPHERE"
|
||||
onClicked: {
|
||||
editRoot.sendToScript({
|
||||
method: "newEntityButtonClicked", params: { buttonName: "newSphereButton" }
|
||||
});
|
||||
editTabView.currentIndex = 2
|
||||
}
|
||||
}
|
||||
|
||||
NewEntityButton {
|
||||
icon: "icons/create-icons/24-light-01.svg"
|
||||
text: "LIGHT"
|
||||
onClicked: {
|
||||
editRoot.sendToScript({
|
||||
method: "newEntityButtonClicked", params: { buttonName: "newLightButton" }
|
||||
});
|
||||
editTabView.currentIndex = 2
|
||||
}
|
||||
}
|
||||
|
||||
NewEntityButton {
|
||||
icon: "icons/create-icons/20-text-01.svg"
|
||||
text: "TEXT"
|
||||
onClicked: {
|
||||
editRoot.sendToScript({
|
||||
method: "newEntityButtonClicked", params: { buttonName: "newTextButton" }
|
||||
});
|
||||
editTabView.currentIndex = 2
|
||||
}
|
||||
}
|
||||
|
||||
NewEntityButton {
|
||||
icon: "icons/create-icons/25-web-1-01.svg"
|
||||
text: "WEB"
|
||||
onClicked: {
|
||||
editRoot.sendToScript({
|
||||
method: "newEntityButtonClicked", params: { buttonName: "newWebButton" }
|
||||
});
|
||||
editTabView.currentIndex = 2
|
||||
}
|
||||
}
|
||||
|
||||
NewEntityButton {
|
||||
icon: "icons/create-icons/23-zone-01.svg"
|
||||
text: "ZONE"
|
||||
onClicked: {
|
||||
editRoot.sendToScript({
|
||||
method: "newEntityButtonClicked", params: { buttonName: "newZoneButton" }
|
||||
});
|
||||
editTabView.currentIndex = 2
|
||||
}
|
||||
}
|
||||
|
||||
NewEntityButton {
|
||||
icon: "icons/create-icons/90-particles-01.svg"
|
||||
text: "PARTICLE"
|
||||
onClicked: {
|
||||
editRoot.sendToScript({
|
||||
method: "newEntityButtonClicked", params: { buttonName: "newParticleButton" }
|
||||
});
|
||||
editTabView.currentIndex = 2
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
HifiControls.Button {
|
||||
id: assetServerButton
|
||||
text: "Open This Domain's Asset Server"
|
||||
color: hifi.buttons.black
|
||||
colorScheme: hifi.colorSchemes.dark
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: 55
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: 55
|
||||
anchors.top: createEntitiesFlow.bottom
|
||||
anchors.topMargin: 35
|
||||
onClicked: {
|
||||
editRoot.sendToScript({
|
||||
method: "newEntityButtonClicked", params: { buttonName: "openAssetBrowserButton" }
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
HifiControls.Button {
|
||||
text: "Import Entities (.json)"
|
||||
color: hifi.buttons.black
|
||||
colorScheme: hifi.colorSchemes.dark
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: 55
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: 55
|
||||
anchors.top: assetServerButton.bottom
|
||||
anchors.topMargin: 20
|
||||
onClicked: {
|
||||
editRoot.sendToScript({
|
||||
method: "newEntityButtonClicked", params: { buttonName: "importEntitiesButton" }
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Tab {
|
||||
title: "LIST"
|
||||
active: true
|
||||
enabled: true
|
||||
property string originalUrl: ""
|
||||
|
||||
WebView {
|
||||
id: entityListToolWebView
|
||||
url: "../../../../../scripts/system/html/entityList.html"
|
||||
eventBridge: editRoot.eventBridge
|
||||
anchors.fill: parent
|
||||
enabled: true
|
||||
}
|
||||
}
|
||||
|
||||
Tab {
|
||||
title: "PROPERTIES"
|
||||
active: true
|
||||
enabled: true
|
||||
property string originalUrl: ""
|
||||
|
||||
WebView {
|
||||
id: entityPropertiesWebView
|
||||
url: "../../../../../scripts/system/html/entityProperties.html"
|
||||
eventBridge: editRoot.eventBridge
|
||||
anchors.fill: parent
|
||||
enabled: true
|
||||
}
|
||||
}
|
||||
|
||||
Tab {
|
||||
title: "GRID"
|
||||
active: true
|
||||
enabled: true
|
||||
property string originalUrl: ""
|
||||
|
||||
WebView {
|
||||
id: gridControlsWebView
|
||||
url: "../../../../../scripts/system/html/gridControls.html"
|
||||
eventBridge: editRoot.eventBridge
|
||||
anchors.fill: parent
|
||||
enabled: true
|
||||
}
|
||||
}
|
||||
|
||||
Tab {
|
||||
title: "P"
|
||||
active: true
|
||||
enabled: true
|
||||
property string originalUrl: ""
|
||||
|
||||
WebView {
|
||||
id: particleExplorerWebView
|
||||
url: "../../../../../scripts/system/particle_explorer/particleExplorer.html"
|
||||
eventBridge: editRoot.eventBridge
|
||||
anchors.fill: parent
|
||||
enabled: true
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
style: TabViewStyle {
|
||||
frameOverlap: 1
|
||||
tab: Rectangle {
|
||||
color: styleData.selected ? "#404040" :"black"
|
||||
implicitWidth: text.width + 42
|
||||
implicitHeight: 40
|
||||
Text {
|
||||
id: text
|
||||
anchors.centerIn: parent
|
||||
text: styleData.title
|
||||
font.pixelSize: 16
|
||||
font.bold: true
|
||||
color: styleData.selected ? "white" : "white"
|
||||
property string glyphtext: ""
|
||||
HiFiGlyphs {
|
||||
anchors.centerIn: parent
|
||||
size: 30
|
||||
color: "#ffffff"
|
||||
text: text.glyphtext
|
||||
}
|
||||
Component.onCompleted: if (styleData.title == "P") {
|
||||
text.text = " ";
|
||||
text.glyphtext = "\ue004";
|
||||
}
|
||||
}
|
||||
}
|
||||
tabBar: Rectangle {
|
||||
color: "black"
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: 0
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: 0
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.bottomMargin: 0
|
||||
anchors.top: parent.top
|
||||
anchors.topMargin: 0
|
||||
}
|
||||
}
|
||||
}
|
||||
// Passes script messages to the item on the top of the stack
|
||||
function fromScript(message) {
|
||||
var currentItem = editRoot.currentItem;
|
||||
if (currentItem && currentItem.fromScript)
|
||||
currentItem.fromScript(message);
|
||||
}
|
||||
}
|
||||
|
|
318
interface/resources/qml/hifi/tablet/EditTabView.qml
Normal file
318
interface/resources/qml/hifi/tablet/EditTabView.qml
Normal file
|
@ -0,0 +1,318 @@
|
|||
import QtQuick 2.5
|
||||
import QtQuick.Controls 1.4
|
||||
import QtWebEngine 1.1
|
||||
import QtWebChannel 1.0
|
||||
import QtQuick.Controls.Styles 1.4
|
||||
import "../../controls"
|
||||
import "../toolbars"
|
||||
import HFWebEngineProfile 1.0
|
||||
import QtGraphicalEffects 1.0
|
||||
import "../../controls-uit" as HifiControls
|
||||
import "../../styles-uit"
|
||||
|
||||
|
||||
TabView {
|
||||
id: editTabView
|
||||
// anchors.fill: parent
|
||||
height: 60
|
||||
|
||||
Tab {
|
||||
title: "CREATE"
|
||||
active: true
|
||||
enabled: true
|
||||
property string originalUrl: ""
|
||||
|
||||
Rectangle {
|
||||
color: "#404040"
|
||||
|
||||
Text {
|
||||
color: "#ffffff"
|
||||
text: "Choose an Entity Type to Create:"
|
||||
font.pixelSize: 14
|
||||
font.bold: true
|
||||
anchors.top: parent.top
|
||||
anchors.topMargin: 28
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: 28
|
||||
}
|
||||
|
||||
Flow {
|
||||
id: createEntitiesFlow
|
||||
spacing: 35
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: 55
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: 55
|
||||
anchors.top: parent.top
|
||||
anchors.topMargin: 70
|
||||
|
||||
|
||||
NewEntityButton {
|
||||
icon: "icons/create-icons/94-model-01.svg"
|
||||
text: "MODEL"
|
||||
onClicked: {
|
||||
editRoot.sendToScript({
|
||||
method: "newEntityButtonClicked", params: { buttonName: "newModelButton" }
|
||||
});
|
||||
editTabView.currentIndex = 2
|
||||
}
|
||||
}
|
||||
|
||||
NewEntityButton {
|
||||
icon: "icons/create-icons/21-cube-01.svg"
|
||||
text: "CUBE"
|
||||
onClicked: {
|
||||
editRoot.sendToScript({
|
||||
method: "newEntityButtonClicked", params: { buttonName: "newCubeButton" }
|
||||
});
|
||||
editTabView.currentIndex = 2
|
||||
}
|
||||
}
|
||||
|
||||
NewEntityButton {
|
||||
icon: "icons/create-icons/22-sphere-01.svg"
|
||||
text: "SPHERE"
|
||||
onClicked: {
|
||||
editRoot.sendToScript({
|
||||
method: "newEntityButtonClicked", params: { buttonName: "newSphereButton" }
|
||||
});
|
||||
editTabView.currentIndex = 2
|
||||
}
|
||||
}
|
||||
|
||||
NewEntityButton {
|
||||
icon: "icons/create-icons/24-light-01.svg"
|
||||
text: "LIGHT"
|
||||
onClicked: {
|
||||
editRoot.sendToScript({
|
||||
method: "newEntityButtonClicked", params: { buttonName: "newLightButton" }
|
||||
});
|
||||
editTabView.currentIndex = 2
|
||||
}
|
||||
}
|
||||
|
||||
NewEntityButton {
|
||||
icon: "icons/create-icons/20-text-01.svg"
|
||||
text: "TEXT"
|
||||
onClicked: {
|
||||
editRoot.sendToScript({
|
||||
method: "newEntityButtonClicked", params: { buttonName: "newTextButton" }
|
||||
});
|
||||
editTabView.currentIndex = 2
|
||||
}
|
||||
}
|
||||
|
||||
NewEntityButton {
|
||||
icon: "icons/create-icons/25-web-1-01.svg"
|
||||
text: "WEB"
|
||||
onClicked: {
|
||||
editRoot.sendToScript({
|
||||
method: "newEntityButtonClicked", params: { buttonName: "newWebButton" }
|
||||
});
|
||||
editTabView.currentIndex = 2
|
||||
}
|
||||
}
|
||||
|
||||
NewEntityButton {
|
||||
icon: "icons/create-icons/23-zone-01.svg"
|
||||
text: "ZONE"
|
||||
onClicked: {
|
||||
editRoot.sendToScript({
|
||||
method: "newEntityButtonClicked", params: { buttonName: "newZoneButton" }
|
||||
});
|
||||
editTabView.currentIndex = 2
|
||||
}
|
||||
}
|
||||
|
||||
NewEntityButton {
|
||||
icon: "icons/create-icons/90-particles-01.svg"
|
||||
text: "PARTICLE"
|
||||
onClicked: {
|
||||
editRoot.sendToScript({
|
||||
method: "newEntityButtonClicked", params: { buttonName: "newParticleButton" }
|
||||
});
|
||||
editTabView.currentIndex = 4
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
HifiControls.Button {
|
||||
id: assetServerButton
|
||||
text: "Open This Domain's Asset Server"
|
||||
color: hifi.buttons.black
|
||||
colorScheme: hifi.colorSchemes.dark
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: 55
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: 55
|
||||
anchors.top: createEntitiesFlow.bottom
|
||||
anchors.topMargin: 35
|
||||
onClicked: {
|
||||
editRoot.sendToScript({
|
||||
method: "newEntityButtonClicked", params: { buttonName: "openAssetBrowserButton" }
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
HifiControls.Button {
|
||||
text: "Import Entities (.json)"
|
||||
color: hifi.buttons.black
|
||||
colorScheme: hifi.colorSchemes.dark
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: 55
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: 55
|
||||
anchors.top: assetServerButton.bottom
|
||||
anchors.topMargin: 20
|
||||
onClicked: {
|
||||
editRoot.sendToScript({
|
||||
method: "newEntityButtonClicked", params: { buttonName: "importEntitiesButton" }
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Tab {
|
||||
title: "LIST"
|
||||
active: true
|
||||
enabled: true
|
||||
property string originalUrl: ""
|
||||
|
||||
WebView {
|
||||
id: entityListToolWebView
|
||||
url: "../../../../../scripts/system/html/entityList.html"
|
||||
eventBridge: editRoot.eventBridge
|
||||
anchors.fill: parent
|
||||
enabled: true
|
||||
}
|
||||
}
|
||||
|
||||
Tab {
|
||||
title: "PROPERTIES"
|
||||
active: true
|
||||
enabled: true
|
||||
property string originalUrl: ""
|
||||
|
||||
WebView {
|
||||
id: entityPropertiesWebView
|
||||
url: "../../../../../scripts/system/html/entityProperties.html"
|
||||
eventBridge: editRoot.eventBridge
|
||||
anchors.fill: parent
|
||||
enabled: true
|
||||
}
|
||||
}
|
||||
|
||||
Tab {
|
||||
title: "GRID"
|
||||
active: true
|
||||
enabled: true
|
||||
property string originalUrl: ""
|
||||
|
||||
WebView {
|
||||
id: gridControlsWebView
|
||||
url: "../../../../../scripts/system/html/gridControls.html"
|
||||
eventBridge: editRoot.eventBridge
|
||||
anchors.fill: parent
|
||||
enabled: true
|
||||
}
|
||||
}
|
||||
|
||||
Tab {
|
||||
title: "P"
|
||||
active: true
|
||||
enabled: true
|
||||
property string originalUrl: ""
|
||||
|
||||
WebView {
|
||||
id: particleExplorerWebView
|
||||
url: "../../../../../scripts/system/particle_explorer/particleExplorer.html"
|
||||
eventBridge: editRoot.eventBridge
|
||||
anchors.fill: parent
|
||||
enabled: true
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
style: TabViewStyle {
|
||||
frameOverlap: 1
|
||||
tab: Rectangle {
|
||||
color: styleData.selected ? "#404040" :"black"
|
||||
implicitWidth: text.width + 42
|
||||
implicitHeight: 40
|
||||
Text {
|
||||
id: text
|
||||
anchors.centerIn: parent
|
||||
text: styleData.title
|
||||
font.pixelSize: 16
|
||||
font.bold: true
|
||||
color: styleData.selected ? "white" : "white"
|
||||
property string glyphtext: ""
|
||||
HiFiGlyphs {
|
||||
anchors.centerIn: parent
|
||||
size: 30
|
||||
color: "#ffffff"
|
||||
text: text.glyphtext
|
||||
}
|
||||
Component.onCompleted: if (styleData.title == "P") {
|
||||
text.text = " ";
|
||||
text.glyphtext = "\ue004";
|
||||
}
|
||||
}
|
||||
}
|
||||
tabBar: Rectangle {
|
||||
color: "black"
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: 0
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: 0
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.bottomMargin: 0
|
||||
anchors.top: parent.top
|
||||
anchors.topMargin: 0
|
||||
}
|
||||
}
|
||||
|
||||
function fromScript(message) {
|
||||
switch (message.method) {
|
||||
case 'selectTab':
|
||||
selectTab(message.params.id);
|
||||
break;
|
||||
default:
|
||||
console.warn('Unrecognized message:', JSON.stringify(message));
|
||||
}
|
||||
}
|
||||
|
||||
// Changes the current tab based on tab index or title as input
|
||||
function selectTab(id) {
|
||||
if (typeof id === 'number') {
|
||||
if (id >= 0 && id <= 4) {
|
||||
editTabView.currentIndex = id;
|
||||
} else {
|
||||
console.warn('Attempt to switch to invalid tab:', id);
|
||||
}
|
||||
} else if (typeof id === 'string'){
|
||||
switch (id.toLowerCase()) {
|
||||
case 'create':
|
||||
editTabView.currentIndex = 0;
|
||||
break;
|
||||
case 'list':
|
||||
editTabView.currentIndex = 1;
|
||||
break;
|
||||
case 'properties':
|
||||
editTabView.currentIndex = 2;
|
||||
break;
|
||||
case 'grid':
|
||||
editTabView.currentIndex = 3;
|
||||
break;
|
||||
case 'particle':
|
||||
editTabView.currentIndex = 4;
|
||||
break;
|
||||
default:
|
||||
console.warn('Attempt to switch to invalid tab:', id);
|
||||
}
|
||||
} else {
|
||||
console.warn('Attempt to switch tabs with invalid input:', JSON.stringify(id));
|
||||
}
|
||||
}
|
||||
}
|
170
interface/resources/qml/hifi/tablet/InputRecorder.qml
Normal file
170
interface/resources/qml/hifi/tablet/InputRecorder.qml
Normal file
|
@ -0,0 +1,170 @@
|
|||
//
|
||||
// Created by Dante Ruiz 2017/04/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
|
||||
import Hifi 1.0
|
||||
import QtQuick.Controls 1.4
|
||||
import QtQuick.Dialogs 1.2 as OriginalDialogs
|
||||
|
||||
import "../../styles-uit"
|
||||
import "../../controls-uit" as HifiControls
|
||||
import "../../windows"
|
||||
import "../../dialogs"
|
||||
|
||||
Rectangle {
|
||||
id: inputRecorder
|
||||
property var eventBridge;
|
||||
HifiConstants { id: hifi }
|
||||
signal sendToScript(var message);
|
||||
color: hifi.colors.baseGray;
|
||||
property string path: ""
|
||||
property string dir: ""
|
||||
property var dialog: null;
|
||||
property bool recording: false;
|
||||
|
||||
Component { id: fileDialog; TabletFileDialog { } }
|
||||
Row {
|
||||
id: topButtons
|
||||
width: parent.width
|
||||
height: 40
|
||||
spacing: 40
|
||||
anchors {
|
||||
left: parent.left
|
||||
right: parent.right
|
||||
top: parent.top
|
||||
topMargin: 10
|
||||
}
|
||||
|
||||
HifiControls.Button {
|
||||
id: start
|
||||
text: "Start Recoring"
|
||||
color: hifi.buttons.black
|
||||
enabled: true
|
||||
onClicked: {
|
||||
if (inputRecorder.recording) {
|
||||
sendToScript({method: "Stop"});
|
||||
inputRecorder.recording = false;
|
||||
start.text = "Start Recording";
|
||||
selectedFile.text = "Current recording is not saved";
|
||||
} else {
|
||||
sendToScript({method: "Start"});
|
||||
inputRecorder.recording = true;
|
||||
start.text = "Stop Recording";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
HifiControls.Button {
|
||||
id: save
|
||||
text: "Save Recording"
|
||||
color: hifi.buttons.black
|
||||
enabled: true
|
||||
onClicked: {
|
||||
sendToScript({method: "Save"});
|
||||
selectedFile.text = "";
|
||||
}
|
||||
}
|
||||
|
||||
HifiControls.Button {
|
||||
id: playBack
|
||||
anchors.right: browse.left
|
||||
anchors.top: selectedFile.bottom
|
||||
anchors.topMargin: 10
|
||||
|
||||
text: "Play Recording"
|
||||
color: hifi.buttons.black
|
||||
enabled: true
|
||||
onClicked: {
|
||||
sendToScript({method: "playback"});
|
||||
HMD.closeTablet();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
HifiControls.VerticalSpacer {}
|
||||
|
||||
HifiControls.TextField {
|
||||
id: selectedFile
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: topButtons.top
|
||||
anchors.topMargin: 40
|
||||
|
||||
colorScheme: hifi.colorSchemes.dark
|
||||
readOnly: true
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
HifiControls.Button {
|
||||
id: browse
|
||||
anchors.right: parent.right
|
||||
anchors.top: selectedFile.bottom
|
||||
anchors.topMargin: 10
|
||||
|
||||
text: "Load..."
|
||||
color: hifi.buttons.black
|
||||
enabled: true
|
||||
onClicked: {
|
||||
dialog = fileDialog.createObject(inputRecorder);
|
||||
dialog.caption = "InputRecorder";
|
||||
console.log(dialog.dir);
|
||||
dialog.dir = "file:///" + inputRecorder.dir;
|
||||
dialog.selectedFile.connect(getFileSelected);
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
id: notes
|
||||
anchors.centerIn: parent;
|
||||
spacing: 20
|
||||
|
||||
Text {
|
||||
text: "All files are saved under the folder 'hifi-input-recording' in AppData directory";
|
||||
color: "white"
|
||||
font.pointSize: 10
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "To cancel a recording playback press Alt-B"
|
||||
color: "white"
|
||||
font.pointSize: 10
|
||||
}
|
||||
}
|
||||
|
||||
function getFileSelected(file) {
|
||||
selectedFile.text = file;
|
||||
inputRecorder.path = file;
|
||||
sendToScript({method: "Load", params: {file: path }});
|
||||
}
|
||||
|
||||
function fromScript(message) {
|
||||
switch (message.method) {
|
||||
case "update":
|
||||
updateButtonStatus(message.params);
|
||||
break;
|
||||
case "path":
|
||||
console.log(message.params);
|
||||
inputRecorder.dir = message.params;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function updateButtonStatus(status) {
|
||||
inputRecorder.recording = status;
|
||||
|
||||
if (inputRecorder.recording) {
|
||||
start.text = "Stop Recording";
|
||||
} else {
|
||||
start.text = "Start Recording";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -23,7 +23,7 @@ StackView {
|
|||
signal sendToScript(var message);
|
||||
|
||||
function pushSource(path) {
|
||||
profileRoot.push(Qt.reslovedUrl(path));
|
||||
profileRoot.push(Qt.resolvedUrl(path));
|
||||
}
|
||||
|
||||
function popSource() {
|
||||
|
|
|
@ -23,7 +23,7 @@ StackView {
|
|||
signal sendToScript(var message);
|
||||
|
||||
function pushSource(path) {
|
||||
profileRoot.push(Qt.reslovedUrl(path));
|
||||
profileRoot.push(Qt.resolvedUrl(path));
|
||||
}
|
||||
|
||||
function popSource() {
|
||||
|
|
|
@ -23,7 +23,7 @@ StackView {
|
|||
signal sendToScript(var message);
|
||||
|
||||
function pushSource(path) {
|
||||
profileRoot.push(Qt.reslovedUrl(path));
|
||||
profileRoot.push(Qt.resolvedUrl(path));
|
||||
}
|
||||
|
||||
function popSource() {
|
||||
|
|
|
@ -23,7 +23,7 @@ StackView {
|
|||
signal sendToScript(var message);
|
||||
|
||||
function pushSource(path) {
|
||||
profileRoot.push(Qt.reslovedUrl(path));
|
||||
profileRoot.push(Qt.resolvedUrl(path));
|
||||
}
|
||||
|
||||
function popSource() {
|
||||
|
|
|
@ -23,7 +23,7 @@ StackView {
|
|||
signal sendToScript(var message);
|
||||
|
||||
function pushSource(path) {
|
||||
profileRoot.push(Qt.reslovedUrl(path));
|
||||
profileRoot.push(Qt.resolvedUrl(path));
|
||||
}
|
||||
|
||||
function popSource() {
|
||||
|
|
|
@ -44,6 +44,12 @@ Item {
|
|||
return openModal;
|
||||
}
|
||||
|
||||
Component { id: assetDialogBuilder; TabletAssetDialog { } }
|
||||
function assetDialog(properties) {
|
||||
openModal = assetDialogBuilder.createObject(tabletRoot, properties);
|
||||
return openModal;
|
||||
}
|
||||
|
||||
function setMenuProperties(rootMenu, subMenu) {
|
||||
tabletRoot.rootMenu = rootMenu;
|
||||
tabletRoot.subMenu = subMenu;
|
||||
|
|
|
@ -78,6 +78,7 @@
|
|||
#include <InfoView.h>
|
||||
#include <input-plugins/InputPlugin.h>
|
||||
#include <controllers/UserInputMapper.h>
|
||||
#include <controllers/InputRecorder.h>
|
||||
#include <controllers/ScriptingInterface.h>
|
||||
#include <controllers/StateController.h>
|
||||
#include <UserActivityLoggerScriptingInterface.h>
|
||||
|
@ -1465,46 +1466,53 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo
|
|||
const auto testScript = property(hifi::properties::TEST).toUrl();
|
||||
scriptEngines->loadScript(testScript, false);
|
||||
} else {
|
||||
// Get sandbox content set version, if available
|
||||
enum HandControllerType {
|
||||
Vive,
|
||||
Oculus
|
||||
};
|
||||
static const std::map<HandControllerType, int> MIN_CONTENT_VERSION = {
|
||||
{ Vive, 1 },
|
||||
{ Oculus, 27 }
|
||||
};
|
||||
|
||||
// Get sandbox content set version
|
||||
auto acDirPath = PathUtils::getAppDataPath() + "../../" + BuildInfo::MODIFIED_ORGANIZATION + "/assignment-client/";
|
||||
auto contentVersionPath = acDirPath + "content-version.txt";
|
||||
qCDebug(interfaceapp) << "Checking " << contentVersionPath << " for content version";
|
||||
auto contentVersion = 0;
|
||||
int contentVersion = 0;
|
||||
QFile contentVersionFile(contentVersionPath);
|
||||
if (contentVersionFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
|
||||
QString line = contentVersionFile.readAll();
|
||||
// toInt() returns 0 if the conversion fails, so we don't need to specifically check for failure
|
||||
contentVersion = line.toInt();
|
||||
contentVersion = line.toInt(); // returns 0 if conversion fails
|
||||
}
|
||||
qCDebug(interfaceapp) << "Server content version: " << contentVersion;
|
||||
|
||||
static const int MIN_VIVE_CONTENT_VERSION = 1;
|
||||
static const int MIN_OCULUS_TOUCH_CONTENT_VERSION = 27;
|
||||
|
||||
bool hasSufficientTutorialContent = false;
|
||||
// Get controller availability
|
||||
bool hasHandControllers = false;
|
||||
|
||||
// Only specific hand controllers are currently supported, so only send users to the tutorial
|
||||
// if they have one of those hand controllers.
|
||||
HandControllerType handControllerType = Vive;
|
||||
if (PluginUtils::isViveControllerAvailable()) {
|
||||
hasHandControllers = true;
|
||||
hasSufficientTutorialContent = contentVersion >= MIN_VIVE_CONTENT_VERSION;
|
||||
handControllerType = Vive;
|
||||
} else if (PluginUtils::isOculusTouchControllerAvailable()) {
|
||||
hasHandControllers = true;
|
||||
hasSufficientTutorialContent = contentVersion >= MIN_OCULUS_TOUCH_CONTENT_VERSION;
|
||||
handControllerType = Oculus;
|
||||
}
|
||||
|
||||
// Check tutorial content versioning
|
||||
bool hasTutorialContent = contentVersion >= MIN_CONTENT_VERSION.at(handControllerType);
|
||||
|
||||
// Check HMD use (may be technically available without being in use)
|
||||
bool hasHMD = PluginUtils::isHMDAvailable();
|
||||
bool isUsingHMD = hasHMD && hasHandControllers && _displayPlugin->isHmd();
|
||||
|
||||
Setting::Handle<bool> tutorialComplete { "tutorialComplete", false };
|
||||
Setting::Handle<bool> firstRun { Settings::firstRun, true };
|
||||
|
||||
bool hasHMDAndHandControllers = PluginUtils::isHMDAvailable() && hasHandControllers;
|
||||
Setting::Handle<bool> tutorialComplete { "tutorialComplete", false };
|
||||
bool isTutorialComplete = tutorialComplete.get();
|
||||
bool shouldGoToTutorial = isUsingHMD && hasTutorialContent && !isTutorialComplete;
|
||||
|
||||
bool shouldGoToTutorial = hasHMDAndHandControllers && hasSufficientTutorialContent && !tutorialComplete.get();
|
||||
|
||||
qCDebug(interfaceapp) << "Has HMD + Hand Controllers: " << hasHMDAndHandControllers << ", current plugin: " << _displayPlugin->getName();
|
||||
qCDebug(interfaceapp) << "Has sufficient tutorial content (" << contentVersion << ") : " << hasSufficientTutorialContent;
|
||||
qCDebug(interfaceapp) << "Tutorial complete: " << tutorialComplete.get();
|
||||
qCDebug(interfaceapp) << "Should go to tutorial: " << shouldGoToTutorial;
|
||||
qCDebug(interfaceapp) << "HMD:" << hasHMD << ", Hand Controllers: " << hasHandControllers << ", Using HMD: " << isUsingHMD;
|
||||
qCDebug(interfaceapp) << "Tutorial version:" << contentVersion << ", sufficient:" << hasTutorialContent <<
|
||||
", complete:" << isTutorialComplete << ", should go:" << shouldGoToTutorial;
|
||||
|
||||
// when --url in command line, teleport to location
|
||||
const QString HIFI_URL_COMMAND_LINE_KEY = "--url";
|
||||
|
@ -1541,7 +1549,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo
|
|||
|
||||
// If this is a first run we short-circuit the address passed in
|
||||
if (isFirstRun) {
|
||||
if (hasHMDAndHandControllers) {
|
||||
if (isUsingHMD) {
|
||||
if (sandboxIsRunning) {
|
||||
qCDebug(interfaceapp) << "Home sandbox appears to be running, going to Home.";
|
||||
DependencyManager::get<AddressManager>()->goToLocalSandbox();
|
||||
|
@ -2746,6 +2754,9 @@ void Application::keyPressEvent(QKeyEvent* event) {
|
|||
if (isMeta) {
|
||||
auto offscreenUi = DependencyManager::get<OffscreenUi>();
|
||||
offscreenUi->load("Browser.qml");
|
||||
} else if (isOption) {
|
||||
controller::InputRecorder* inputRecorder = controller::InputRecorder::getInstance();
|
||||
inputRecorder->stopPlayback();
|
||||
}
|
||||
break;
|
||||
|
||||
|
|
|
@ -929,6 +929,17 @@ QVector<glm::quat> Avatar::getJointRotations() const {
|
|||
return jointRotations;
|
||||
}
|
||||
|
||||
QVector<glm::vec3> Avatar::getJointTranslations() const {
|
||||
if (QThread::currentThread() != thread()) {
|
||||
return AvatarData::getJointTranslations();
|
||||
}
|
||||
QVector<glm::vec3> jointTranslations(_skeletonModel->getJointStateCount());
|
||||
for (int i = 0; i < _skeletonModel->getJointStateCount(); ++i) {
|
||||
_skeletonModel->getJointTranslation(i, jointTranslations[i]);
|
||||
}
|
||||
return jointTranslations;
|
||||
}
|
||||
|
||||
glm::quat Avatar::getJointRotation(int index) const {
|
||||
glm::quat rotation;
|
||||
_skeletonModel->getJointRotation(index, rotation);
|
||||
|
|
|
@ -112,6 +112,7 @@ public:
|
|||
|
||||
virtual QVector<glm::quat> getJointRotations() const override;
|
||||
virtual glm::quat getJointRotation(int index) const override;
|
||||
virtual QVector<glm::vec3> getJointTranslations() const override;
|
||||
virtual glm::vec3 getJointTranslation(int index) const override;
|
||||
virtual int getJointIndex(const QString& name) const override;
|
||||
virtual QStringList getJointNames() const override;
|
||||
|
|
|
@ -28,6 +28,7 @@
|
|||
|
||||
static const QString DESKTOP_LOCATION = QStandardPaths::writableLocation(QStandardPaths::DesktopLocation);
|
||||
static const QString LAST_BROWSE_LOCATION_SETTING = "LastBrowseLocation";
|
||||
static const QString LAST_BROWSE_ASSETS_LOCATION_SETTING = "LastBrowseAssetsLocation";
|
||||
|
||||
|
||||
QScriptValue CustomPromptResultToScriptValue(QScriptEngine* engine, const CustomPromptResult& result) {
|
||||
|
@ -149,6 +150,15 @@ void WindowScriptingInterface::setPreviousBrowseLocation(const QString& location
|
|||
Setting::Handle<QVariant>(LAST_BROWSE_LOCATION_SETTING).set(location);
|
||||
}
|
||||
|
||||
QString WindowScriptingInterface::getPreviousBrowseAssetLocation() const {
|
||||
QString ASSETS_ROOT_PATH = "/";
|
||||
return Setting::Handle<QString>(LAST_BROWSE_ASSETS_LOCATION_SETTING, ASSETS_ROOT_PATH).get();
|
||||
}
|
||||
|
||||
void WindowScriptingInterface::setPreviousBrowseAssetLocation(const QString& location) {
|
||||
Setting::Handle<QVariant>(LAST_BROWSE_ASSETS_LOCATION_SETTING).set(location);
|
||||
}
|
||||
|
||||
/// Makes sure that the reticle is visible, use this in blocking forms that require a reticle and
|
||||
/// might be in same thread as a script that sets the reticle to invisible
|
||||
void WindowScriptingInterface::ensureReticleVisible() const {
|
||||
|
@ -202,6 +212,31 @@ 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
|
||||
/// 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`
|
||||
/// \return QScriptValue asset path as a string if one was selected, otherwise `QScriptValue::NullValue`
|
||||
QScriptValue WindowScriptingInterface::browseAssets(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 + "/";
|
||||
}
|
||||
QString result = OffscreenUi::getOpenAssetName(nullptr, title, path, nameFilter);
|
||||
if (!result.isEmpty()) {
|
||||
setPreviousBrowseAssetLocation(QFileInfo(result).absolutePath());
|
||||
}
|
||||
return result.isEmpty() ? QScriptValue::NullValue : QScriptValue(result);
|
||||
}
|
||||
|
||||
void WindowScriptingInterface::showAssetServer(const QString& upload) {
|
||||
QMetaObject::invokeMethod(qApp, "showAssetServerWidget", Qt::QueuedConnection, Q_ARG(QString, upload));
|
||||
}
|
||||
|
|
|
@ -53,6 +53,7 @@ public slots:
|
|||
CustomPromptResult customPrompt(const QVariant& config);
|
||||
QScriptValue browse(const QString& title = "", const QString& directory = "", const QString& nameFilter = "");
|
||||
QScriptValue save(const QString& title = "", const QString& directory = "", const QString& nameFilter = "");
|
||||
QScriptValue browseAssets(const QString& title = "", const QString& directory = "", const QString& nameFilter = "");
|
||||
void showAssetServer(const QString& upload = "");
|
||||
void copyToClipboard(const QString& text);
|
||||
void takeSnapshot(bool notify = true, bool includeAnimated = false, float aspectRatio = 0.0f);
|
||||
|
@ -88,6 +89,9 @@ private:
|
|||
QString getPreviousBrowseLocation() const;
|
||||
void setPreviousBrowseLocation(const QString& location);
|
||||
|
||||
QString getPreviousBrowseAssetLocation() const;
|
||||
void setPreviousBrowseAssetLocation(const QString& location);
|
||||
|
||||
void ensureReticleVisible() const;
|
||||
|
||||
int createMessageBox(QString title, QString text, int buttons, int defaultButton);
|
||||
|
|
|
@ -126,6 +126,55 @@ void ModelOverlay::setProperties(const QVariantMap& properties) {
|
|||
QMetaObject::invokeMethod(_model.get(), "setTextures", Qt::AutoConnection,
|
||||
Q_ARG(const QVariantMap&, textureMap));
|
||||
}
|
||||
|
||||
// relative
|
||||
auto jointTranslationsValue = properties["jointTranslations"];
|
||||
if (jointTranslationsValue.canConvert(QVariant::List)) {
|
||||
const QVariantList& jointTranslations = jointTranslationsValue.toList();
|
||||
int translationCount = jointTranslations.size();
|
||||
int jointCount = _model->getJointStateCount();
|
||||
if (translationCount < jointCount) {
|
||||
jointCount = translationCount;
|
||||
}
|
||||
for (int i=0; i < jointCount; i++) {
|
||||
const auto& translationValue = jointTranslations[i];
|
||||
if (translationValue.isValid()) {
|
||||
_model->setJointTranslation(i, true, vec3FromVariant(translationValue), 1.0f);
|
||||
}
|
||||
}
|
||||
_updateModel = true;
|
||||
}
|
||||
|
||||
// relative
|
||||
auto jointRotationsValue = properties["jointRotations"];
|
||||
if (jointRotationsValue.canConvert(QVariant::List)) {
|
||||
const QVariantList& jointRotations = jointRotationsValue.toList();
|
||||
int rotationCount = jointRotations.size();
|
||||
int jointCount = _model->getJointStateCount();
|
||||
if (rotationCount < jointCount) {
|
||||
jointCount = rotationCount;
|
||||
}
|
||||
for (int i=0; i < jointCount; i++) {
|
||||
const auto& rotationValue = jointRotations[i];
|
||||
if (rotationValue.isValid()) {
|
||||
_model->setJointRotation(i, true, quatFromVariant(rotationValue), 1.0f);
|
||||
}
|
||||
}
|
||||
_updateModel = true;
|
||||
}
|
||||
}
|
||||
|
||||
template <typename vectorType, typename itemType>
|
||||
vectorType ModelOverlay::mapJoints(mapFunction<itemType> function) const {
|
||||
vectorType result;
|
||||
if (_model && _model->isActive()) {
|
||||
const int jointCount = _model->getJointStateCount();
|
||||
result.reserve(jointCount);
|
||||
for (int i = 0; i < jointCount; i++) {
|
||||
result << function(i);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
QVariant ModelOverlay::getProperty(const QString& property) {
|
||||
|
@ -150,6 +199,58 @@ QVariant ModelOverlay::getProperty(const QString& property) {
|
|||
}
|
||||
}
|
||||
|
||||
if (property == "jointNames") {
|
||||
if (_model && _model->isActive()) {
|
||||
// note: going through Rig because Model::getJointNames() (which proxies to FBXGeometry) was always empty
|
||||
const RigPointer rig = _model->getRig();
|
||||
if (rig) {
|
||||
return mapJoints<QStringList, QString>([rig](int jointIndex) -> QString {
|
||||
return rig->nameOfJoint(jointIndex);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// relative
|
||||
if (property == "jointRotations") {
|
||||
return mapJoints<QVariantList, QVariant>(
|
||||
[this](int jointIndex) -> QVariant {
|
||||
glm::quat rotation;
|
||||
_model->getJointRotation(jointIndex, rotation);
|
||||
return quatToVariant(rotation);
|
||||
});
|
||||
}
|
||||
|
||||
// relative
|
||||
if (property == "jointTranslations") {
|
||||
return mapJoints<QVariantList, QVariant>(
|
||||
[this](int jointIndex) -> QVariant {
|
||||
glm::vec3 translation;
|
||||
_model->getJointTranslation(jointIndex, translation);
|
||||
return vec3toVariant(translation);
|
||||
});
|
||||
}
|
||||
|
||||
// absolute
|
||||
if (property == "jointOrientations") {
|
||||
return mapJoints<QVariantList, QVariant>(
|
||||
[this](int jointIndex) -> QVariant {
|
||||
glm::quat orientation;
|
||||
_model->getJointRotationInWorldFrame(jointIndex, orientation);
|
||||
return quatToVariant(orientation);
|
||||
});
|
||||
}
|
||||
|
||||
// absolute
|
||||
if (property == "jointPositions") {
|
||||
return mapJoints<QVariantList, QVariant>(
|
||||
[this](int jointIndex) -> QVariant {
|
||||
glm::vec3 position;
|
||||
_model->getJointPositionInWorldFrame(jointIndex, position);
|
||||
return vec3toVariant(position);
|
||||
});
|
||||
}
|
||||
|
||||
return Volume3DOverlay::getProperty(property);
|
||||
}
|
||||
|
||||
|
|
|
@ -41,6 +41,12 @@ public:
|
|||
|
||||
void locationChanged(bool tellPhysics) override;
|
||||
|
||||
protected:
|
||||
// helper to extract metadata from our Model's rigged joints
|
||||
template <typename itemType> using mapFunction = std::function<itemType(int jointIndex)>;
|
||||
template <typename vectorType, typename itemType>
|
||||
vectorType mapJoints(mapFunction<itemType> function) const;
|
||||
|
||||
private:
|
||||
|
||||
ModelPointer _model;
|
||||
|
|
|
@ -14,6 +14,8 @@
|
|||
#include "AudioLogging.h"
|
||||
#include "SoundCache.h"
|
||||
|
||||
static const int SOUNDS_LOADING_PRIORITY { -7 }; // Make sure sounds load after the low rez texture mips
|
||||
|
||||
int soundPointerMetaTypeId = qRegisterMetaType<SharedSoundPointer>();
|
||||
|
||||
SoundCache::SoundCache(QObject* parent) :
|
||||
|
@ -37,5 +39,7 @@ SharedSoundPointer SoundCache::getSound(const QUrl& url) {
|
|||
QSharedPointer<Resource> SoundCache::createResource(const QUrl& url, const QSharedPointer<Resource>& fallback,
|
||||
const void* extra) {
|
||||
qCDebug(audio) << "Requesting sound at" << url.toString();
|
||||
return QSharedPointer<Resource>(new Sound(url), &Resource::deleter);
|
||||
auto resource = QSharedPointer<Resource>(new Sound(url), &Resource::deleter);
|
||||
resource->setLoadPriority(this, SOUNDS_LOADING_PRIORITY);
|
||||
return resource;
|
||||
}
|
||||
|
|
|
@ -1392,6 +1392,22 @@ void AvatarData::setJointRotations(QVector<glm::quat> jointRotations) {
|
|||
}
|
||||
}
|
||||
|
||||
QVector<glm::vec3> AvatarData::getJointTranslations() const {
|
||||
if (QThread::currentThread() != thread()) {
|
||||
QVector<glm::vec3> result;
|
||||
QMetaObject::invokeMethod(const_cast<AvatarData*>(this),
|
||||
"getJointTranslations", Qt::BlockingQueuedConnection,
|
||||
Q_RETURN_ARG(QVector<glm::vec3>, result));
|
||||
return result;
|
||||
}
|
||||
QReadLocker readLock(&_jointDataLock);
|
||||
QVector<glm::vec3> jointTranslations(_jointData.size());
|
||||
for (int i = 0; i < _jointData.size(); ++i) {
|
||||
jointTranslations[i] = _jointData[i].translation;
|
||||
}
|
||||
return jointTranslations;
|
||||
}
|
||||
|
||||
void AvatarData::setJointTranslations(QVector<glm::vec3> jointTranslations) {
|
||||
if (QThread::currentThread() != thread()) {
|
||||
QVector<glm::quat> result;
|
||||
|
|
|
@ -497,6 +497,7 @@ public:
|
|||
Q_INVOKABLE glm::vec3 getJointTranslation(const QString& name) const;
|
||||
|
||||
Q_INVOKABLE virtual QVector<glm::quat> getJointRotations() const;
|
||||
Q_INVOKABLE virtual QVector<glm::vec3> getJointTranslations() const;
|
||||
Q_INVOKABLE virtual void setJointRotations(QVector<glm::quat> jointRotations);
|
||||
Q_INVOKABLE virtual void setJointTranslations(QVector<glm::vec3> jointTranslations);
|
||||
|
||||
|
|
|
@ -210,6 +210,13 @@ QVector<glm::quat> ScriptAvatarData::getJointRotations() const {
|
|||
return QVector<glm::quat>();
|
||||
}
|
||||
}
|
||||
QVector<glm::vec3> ScriptAvatarData::getJointTranslations() const {
|
||||
if (AvatarSharedPointer sharedAvatarData = _avatarData.lock()) {
|
||||
return sharedAvatarData->getJointTranslations();
|
||||
} else {
|
||||
return QVector<glm::vec3>();
|
||||
}
|
||||
}
|
||||
bool ScriptAvatarData::isJointDataValid(const QString& name) const {
|
||||
if (AvatarSharedPointer sharedAvatarData = _avatarData.lock()) {
|
||||
return sharedAvatarData->isJointDataValid(name);
|
||||
|
|
|
@ -106,6 +106,7 @@ public:
|
|||
Q_INVOKABLE glm::quat getJointRotation(const QString& name) const;
|
||||
Q_INVOKABLE glm::vec3 getJointTranslation(const QString& name) const;
|
||||
Q_INVOKABLE QVector<glm::quat> getJointRotations() const;
|
||||
Q_INVOKABLE QVector<glm::vec3> getJointTranslations() const;
|
||||
Q_INVOKABLE bool isJointDataValid(const QString& name) const;
|
||||
Q_INVOKABLE int getJointIndex(const QString& name) const;
|
||||
Q_INVOKABLE QStringList getJointNames() const;
|
||||
|
|
|
@ -10,4 +10,4 @@ GroupSources("src/controllers")
|
|||
|
||||
add_dependency_external_projects(glm)
|
||||
find_package(GLM REQUIRED)
|
||||
target_include_directories(${TARGET_NAME} PUBLIC ${GLM_INCLUDE_DIRS})
|
||||
target_include_directories(${TARGET_NAME} PUBLIC ${GLM_INCLUDE_DIRS} "${CMAKE_BINARY_DIR}/includes")
|
||||
|
|
290
libraries/controllers/src/controllers/InputRecorder.cpp
Normal file
290
libraries/controllers/src/controllers/InputRecorder.cpp
Normal file
|
@ -0,0 +1,290 @@
|
|||
//
|
||||
// Created by Dante Ruiz 2017/04/16
|
||||
// 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 "InputRecorder.h"
|
||||
|
||||
#include <QJsonArray>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonDocument>
|
||||
#include <QFile>
|
||||
#include <QDir>
|
||||
#include <QDirIterator>
|
||||
#include <QStandardPaths>
|
||||
#include <QDateTime>
|
||||
#include <QByteArray>
|
||||
#include <QStandardPaths>
|
||||
#include <PathUtils.h>
|
||||
|
||||
#include <BuildInfo.h>
|
||||
#include <GLMHelpers.h>
|
||||
|
||||
QString SAVE_DIRECTORY = QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation) + "/" + BuildInfo::MODIFIED_ORGANIZATION + "/" + BuildInfo::INTERFACE_NAME + "/hifi-input-recordings/";
|
||||
QString FILE_PREFIX_NAME = "input-recording-";
|
||||
QString COMPRESS_EXTENSION = ".tar.gz";
|
||||
namespace controller {
|
||||
|
||||
QJsonObject poseToJsonObject(const Pose pose) {
|
||||
QJsonObject newPose;
|
||||
|
||||
QJsonArray translation;
|
||||
translation.append(pose.translation.x);
|
||||
translation.append(pose.translation.y);
|
||||
translation.append(pose.translation.z);
|
||||
|
||||
QJsonArray rotation;
|
||||
rotation.append(pose.rotation.x);
|
||||
rotation.append(pose.rotation.y);
|
||||
rotation.append(pose.rotation.z);
|
||||
rotation.append(pose.rotation.w);
|
||||
|
||||
QJsonArray velocity;
|
||||
velocity.append(pose.velocity.x);
|
||||
velocity.append(pose.velocity.y);
|
||||
velocity.append(pose.velocity.z);
|
||||
|
||||
QJsonArray angularVelocity;
|
||||
angularVelocity.append(pose.angularVelocity.x);
|
||||
angularVelocity.append(pose.angularVelocity.y);
|
||||
angularVelocity.append(pose.angularVelocity.z);
|
||||
|
||||
newPose["translation"] = translation;
|
||||
newPose["rotation"] = rotation;
|
||||
newPose["velocity"] = velocity;
|
||||
newPose["angularVelocity"] = angularVelocity;
|
||||
newPose["valid"] = pose.valid;
|
||||
|
||||
return newPose;
|
||||
}
|
||||
|
||||
Pose jsonObjectToPose(const QJsonObject object) {
|
||||
Pose pose;
|
||||
QJsonArray translation = object["translation"].toArray();
|
||||
QJsonArray rotation = object["rotation"].toArray();
|
||||
QJsonArray velocity = object["velocity"].toArray();
|
||||
QJsonArray angularVelocity = object["angularVelocity"].toArray();
|
||||
|
||||
pose.valid = object["valid"].toBool();
|
||||
|
||||
pose.translation.x = translation[0].toDouble();
|
||||
pose.translation.y = translation[1].toDouble();
|
||||
pose.translation.z = translation[2].toDouble();
|
||||
|
||||
pose.rotation.x = rotation[0].toDouble();
|
||||
pose.rotation.y = rotation[1].toDouble();
|
||||
pose.rotation.z = rotation[2].toDouble();
|
||||
pose.rotation.w = rotation[3].toDouble();
|
||||
|
||||
pose.velocity.x = velocity[0].toDouble();
|
||||
pose.velocity.y = velocity[1].toDouble();
|
||||
pose.velocity.z = velocity[2].toDouble();
|
||||
|
||||
pose.angularVelocity.x = angularVelocity[0].toDouble();
|
||||
pose.angularVelocity.y = angularVelocity[1].toDouble();
|
||||
pose.angularVelocity.z = angularVelocity[2].toDouble();
|
||||
|
||||
return pose;
|
||||
}
|
||||
|
||||
|
||||
void exportToFile(QJsonObject& object) {
|
||||
if (!QDir(SAVE_DIRECTORY).exists()) {
|
||||
QDir().mkdir(SAVE_DIRECTORY);
|
||||
}
|
||||
|
||||
QString timeStamp = QDateTime::currentDateTime().toString(Qt::ISODate);
|
||||
timeStamp.replace(":", "-");
|
||||
QString fileName = SAVE_DIRECTORY + FILE_PREFIX_NAME + timeStamp + COMPRESS_EXTENSION;
|
||||
qDebug() << fileName;
|
||||
QFile saveFile (fileName);
|
||||
if (!saveFile.open(QIODevice::WriteOnly)) {
|
||||
qWarning() << "could not open file: " << fileName;
|
||||
return;
|
||||
}
|
||||
QJsonDocument saveData(object);
|
||||
QByteArray compressedData = qCompress(saveData.toJson(QJsonDocument::Compact));
|
||||
saveFile.write(compressedData);
|
||||
}
|
||||
|
||||
QJsonObject openFile(const QString& file, bool& status) {
|
||||
QJsonObject object;
|
||||
QFile openFile(file);
|
||||
if (!openFile.open(QIODevice::ReadOnly)) {
|
||||
qWarning() << "could not open file: " << file;
|
||||
status = false;
|
||||
return object;
|
||||
}
|
||||
QByteArray compressedData = qUncompress(openFile.readAll());
|
||||
QJsonDocument jsonDoc = QJsonDocument::fromJson(compressedData);
|
||||
object = jsonDoc.object();
|
||||
status = true;
|
||||
return object;
|
||||
}
|
||||
|
||||
InputRecorder::InputRecorder() {}
|
||||
|
||||
InputRecorder::~InputRecorder() {}
|
||||
|
||||
InputRecorder* InputRecorder::getInstance() {
|
||||
static InputRecorder inputRecorder;
|
||||
return &inputRecorder;
|
||||
}
|
||||
|
||||
QString InputRecorder::getSaveDirectory() {
|
||||
return SAVE_DIRECTORY;
|
||||
}
|
||||
|
||||
void InputRecorder::startRecording() {
|
||||
_recording = true;
|
||||
_playback = false;
|
||||
_framesRecorded = 0;
|
||||
_poseStateList.clear();
|
||||
_actionStateList.clear();
|
||||
}
|
||||
|
||||
void InputRecorder::saveRecording() {
|
||||
QJsonObject data;
|
||||
data["frameCount"] = _framesRecorded;
|
||||
|
||||
QJsonArray actionArrayList;
|
||||
QJsonArray poseArrayList;
|
||||
for(const ActionStates actionState: _actionStateList) {
|
||||
QJsonArray actionArray;
|
||||
for (const float value: actionState) {
|
||||
actionArray.append(value);
|
||||
}
|
||||
actionArrayList.append(actionArray);
|
||||
}
|
||||
|
||||
for (const PoseStates poseState: _poseStateList) {
|
||||
QJsonArray poseArray;
|
||||
for (const Pose pose: poseState) {
|
||||
poseArray.append(poseToJsonObject(pose));
|
||||
}
|
||||
poseArrayList.append(poseArray);
|
||||
}
|
||||
|
||||
data["actionList"] = actionArrayList;
|
||||
data["poseList"] = poseArrayList;
|
||||
exportToFile(data);
|
||||
}
|
||||
|
||||
void InputRecorder::loadRecording(const QString& path) {
|
||||
_recording = false;
|
||||
_playback = false;
|
||||
_loading = true;
|
||||
_playCount = 0;
|
||||
resetFrame();
|
||||
_poseStateList.clear();
|
||||
_actionStateList.clear();
|
||||
QString filePath = path;
|
||||
filePath.remove(0,8);
|
||||
QFileInfo info(filePath);
|
||||
QString extension = info.suffix();
|
||||
if (extension != "gz") {
|
||||
qWarning() << "can not load file with exentsion of " << extension;
|
||||
return;
|
||||
}
|
||||
bool success = false;
|
||||
QJsonObject data = openFile(info.absoluteFilePath(), success);
|
||||
if (success) {
|
||||
_framesRecorded = data["frameCount"].toInt();
|
||||
QJsonArray actionArrayList = data["actionList"].toArray();
|
||||
QJsonArray poseArrayList = data["poseList"].toArray();
|
||||
|
||||
for (int actionIndex = 0; actionIndex < actionArrayList.size(); actionIndex++) {
|
||||
QJsonArray actionState = actionArrayList[actionIndex].toArray();
|
||||
for (int index = 0; index < actionState.size(); index++) {
|
||||
_currentFrameActions[index] = actionState[index].toInt();
|
||||
}
|
||||
_actionStateList.push_back(_currentFrameActions);
|
||||
_currentFrameActions = ActionStates(toInt(Action::NUM_ACTIONS));
|
||||
}
|
||||
|
||||
for (int poseIndex = 0; poseIndex < poseArrayList.size(); poseIndex++) {
|
||||
QJsonArray poseState = poseArrayList[poseIndex].toArray();
|
||||
for (int index = 0; index < poseState.size(); index++) {
|
||||
_currentFramePoses[index] = jsonObjectToPose(poseState[index].toObject());
|
||||
}
|
||||
_poseStateList.push_back(_currentFramePoses);
|
||||
_currentFramePoses = PoseStates(toInt(Action::NUM_ACTIONS));
|
||||
}
|
||||
}
|
||||
|
||||
_loading = false;
|
||||
}
|
||||
|
||||
void InputRecorder::stopRecording() {
|
||||
_recording = false;
|
||||
}
|
||||
|
||||
void InputRecorder::startPlayback() {
|
||||
_playback = true;
|
||||
_recording = false;
|
||||
_playCount = 0;
|
||||
}
|
||||
|
||||
void InputRecorder::stopPlayback() {
|
||||
_playback = false;
|
||||
_playCount = 0;
|
||||
}
|
||||
|
||||
void InputRecorder::setActionState(controller::Action action, float value) {
|
||||
if (_recording) {
|
||||
_currentFrameActions[toInt(action)] += value;
|
||||
}
|
||||
}
|
||||
|
||||
void InputRecorder::setActionState(controller::Action action, const controller::Pose pose) {
|
||||
if (_recording) {
|
||||
_currentFramePoses[toInt(action)] = pose;
|
||||
}
|
||||
}
|
||||
|
||||
void InputRecorder::resetFrame() {
|
||||
if (_recording) {
|
||||
for(auto& channel : _currentFramePoses) {
|
||||
channel = Pose();
|
||||
}
|
||||
|
||||
for(auto& channel : _currentFrameActions) {
|
||||
channel = 0.0f;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
float InputRecorder::getActionState(controller::Action action) {
|
||||
if (_actionStateList.size() > 0 ) {
|
||||
return _actionStateList[_playCount][toInt(action)];
|
||||
}
|
||||
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
controller::Pose InputRecorder::getPoseState(controller::Action action) {
|
||||
if (_poseStateList.size() > 0) {
|
||||
return _poseStateList[_playCount][toInt(action)];
|
||||
}
|
||||
|
||||
return Pose();
|
||||
}
|
||||
|
||||
void InputRecorder::frameTick() {
|
||||
if (_recording) {
|
||||
_framesRecorded++;
|
||||
_poseStateList.push_back(_currentFramePoses);
|
||||
_actionStateList.push_back(_currentFrameActions);
|
||||
}
|
||||
|
||||
if (_playback) {
|
||||
_playCount++;
|
||||
if (_playCount == _framesRecorded) {
|
||||
_playCount = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
62
libraries/controllers/src/controllers/InputRecorder.h
Normal file
62
libraries/controllers/src/controllers/InputRecorder.h
Normal file
|
@ -0,0 +1,62 @@
|
|||
//
|
||||
// Created by Dante Ruiz on 2017/04/16
|
||||
// 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_InputRecorder_h
|
||||
#define hifi_InputRecorder_h
|
||||
|
||||
#include <mutex>
|
||||
#include <atomic>
|
||||
#include <vector>
|
||||
|
||||
#include <QString>
|
||||
|
||||
#include "Pose.h"
|
||||
#include "Actions.h"
|
||||
|
||||
namespace controller {
|
||||
class InputRecorder {
|
||||
public:
|
||||
using PoseStates = std::vector<Pose>;
|
||||
using ActionStates = std::vector<float>;
|
||||
|
||||
InputRecorder();
|
||||
~InputRecorder();
|
||||
|
||||
static InputRecorder* getInstance();
|
||||
|
||||
void saveRecording();
|
||||
void loadRecording(const QString& path);
|
||||
void startRecording();
|
||||
void startPlayback();
|
||||
void stopPlayback();
|
||||
void stopRecording();
|
||||
void toggleRecording() { _recording = !_recording; }
|
||||
void togglePlayback() { _playback = !_playback; }
|
||||
void resetFrame();
|
||||
bool isRecording() { return _recording; }
|
||||
bool isPlayingback() { return (_playback && !_loading); }
|
||||
void setActionState(controller::Action action, float value);
|
||||
void setActionState(controller::Action action, const controller::Pose pose);
|
||||
float getActionState(controller::Action action);
|
||||
controller::Pose getPoseState(controller::Action action);
|
||||
QString getSaveDirectory();
|
||||
void frameTick();
|
||||
private:
|
||||
bool _recording { false };
|
||||
bool _playback { false };
|
||||
bool _loading { false };
|
||||
std::vector<PoseStates> _poseStateList = std::vector<PoseStates>();
|
||||
std::vector<ActionStates> _actionStateList = std::vector<ActionStates>();
|
||||
PoseStates _currentFramePoses = PoseStates(toInt(Action::NUM_ACTIONS));
|
||||
ActionStates _currentFrameActions = ActionStates(toInt(Action::NUM_ACTIONS));
|
||||
|
||||
int _framesRecorded { 0 };
|
||||
int _playCount { 0 };
|
||||
};
|
||||
}
|
||||
#endif
|
|
@ -23,6 +23,7 @@
|
|||
#include "impl/MappingBuilderProxy.h"
|
||||
#include "Logging.h"
|
||||
#include "InputDevice.h"
|
||||
#include "InputRecorder.h"
|
||||
|
||||
|
||||
static QRegularExpression SANITIZE_NAME_EXPRESSION{ "[\\(\\)\\.\\s]" };
|
||||
|
@ -154,6 +155,41 @@ namespace controller {
|
|||
return DependencyManager::get<UserInputMapper>()->triggerHapticPulse(strength, SHORT_HAPTIC_DURATION_MS, hand);
|
||||
}
|
||||
|
||||
void ScriptingInterface::startInputRecording() {
|
||||
InputRecorder* inputRecorder = InputRecorder::getInstance();
|
||||
inputRecorder->startRecording();
|
||||
}
|
||||
|
||||
void ScriptingInterface::stopInputRecording() {
|
||||
InputRecorder* inputRecorder = InputRecorder::getInstance();
|
||||
inputRecorder->stopRecording();
|
||||
}
|
||||
|
||||
void ScriptingInterface::startInputPlayback() {
|
||||
InputRecorder* inputRecorder = InputRecorder::getInstance();
|
||||
inputRecorder->startPlayback();
|
||||
}
|
||||
|
||||
void ScriptingInterface::stopInputPlayback() {
|
||||
InputRecorder* inputRecorder = InputRecorder::getInstance();
|
||||
inputRecorder->stopPlayback();
|
||||
}
|
||||
|
||||
void ScriptingInterface::saveInputRecording() {
|
||||
InputRecorder* inputRecorder = InputRecorder::getInstance();
|
||||
inputRecorder->saveRecording();
|
||||
}
|
||||
|
||||
void ScriptingInterface::loadInputRecording(const QString& file) {
|
||||
InputRecorder* inputRecorder = InputRecorder::getInstance();
|
||||
inputRecorder->loadRecording(file);
|
||||
}
|
||||
|
||||
QString ScriptingInterface::getInputRecorderSaveDirectory() {
|
||||
InputRecorder* inputRecorder = InputRecorder::getInstance();
|
||||
return inputRecorder->getSaveDirectory();
|
||||
}
|
||||
|
||||
bool ScriptingInterface::triggerHapticPulseOnDevice(unsigned int device, float strength, float duration, controller::Hand hand) const {
|
||||
return DependencyManager::get<UserInputMapper>()->triggerHapticPulseOnDevice(device, strength, duration, hand);
|
||||
}
|
||||
|
|
|
@ -99,6 +99,13 @@ namespace controller {
|
|||
Q_INVOKABLE const QVariantMap& getHardware() { return _hardware; }
|
||||
Q_INVOKABLE const QVariantMap& getActions() { return _actions; }
|
||||
Q_INVOKABLE const QVariantMap& getStandard() { return _standard; }
|
||||
Q_INVOKABLE void startInputRecording();
|
||||
Q_INVOKABLE void stopInputRecording();
|
||||
Q_INVOKABLE void startInputPlayback();
|
||||
Q_INVOKABLE void stopInputPlayback();
|
||||
Q_INVOKABLE void saveInputRecording();
|
||||
Q_INVOKABLE void loadInputRecording(const QString& file);
|
||||
Q_INVOKABLE QString getInputRecorderSaveDirectory();
|
||||
|
||||
bool isMouseCaptured() const { return _mouseCaptured; }
|
||||
bool isTouchCaptured() const { return _touchCaptured; }
|
||||
|
|
|
@ -22,7 +22,7 @@
|
|||
|
||||
#include "StandardController.h"
|
||||
#include "StateController.h"
|
||||
|
||||
#include "InputRecorder.h"
|
||||
#include "Logging.h"
|
||||
|
||||
#include "impl/conditionals/AndConditional.h"
|
||||
|
@ -243,10 +243,11 @@ void fixBisectedAxis(float& full, float& negative, float& positive) {
|
|||
|
||||
void UserInputMapper::update(float deltaTime) {
|
||||
Locker locker(_lock);
|
||||
|
||||
InputRecorder* inputRecorder = InputRecorder::getInstance();
|
||||
static uint64_t updateCount = 0;
|
||||
++updateCount;
|
||||
|
||||
inputRecorder->resetFrame();
|
||||
// Reset the axis state for next loop
|
||||
for (auto& channel : _actionStates) {
|
||||
channel = 0.0f;
|
||||
|
@ -298,6 +299,7 @@ void UserInputMapper::update(float deltaTime) {
|
|||
emit inputEvent(input.id, value);
|
||||
}
|
||||
}
|
||||
inputRecorder->frameTick();
|
||||
}
|
||||
|
||||
Input::NamedVector UserInputMapper::getAvailableInputs(uint16 deviceID) const {
|
||||
|
|
|
@ -11,19 +11,32 @@
|
|||
#include <DependencyManager.h>
|
||||
|
||||
#include "../../UserInputMapper.h"
|
||||
#include "../../InputRecorder.h"
|
||||
|
||||
using namespace controller;
|
||||
|
||||
void ActionEndpoint::apply(float newValue, const Pointer& source) {
|
||||
InputRecorder* inputRecorder = InputRecorder::getInstance();
|
||||
if(inputRecorder->isPlayingback()) {
|
||||
newValue = inputRecorder->getActionState(Action(_input.getChannel()));
|
||||
}
|
||||
|
||||
_currentValue += newValue;
|
||||
if (_input != Input::INVALID_INPUT) {
|
||||
auto userInputMapper = DependencyManager::get<UserInputMapper>();
|
||||
userInputMapper->deltaActionState(Action(_input.getChannel()), newValue);
|
||||
}
|
||||
inputRecorder->setActionState(Action(_input.getChannel()), newValue);
|
||||
}
|
||||
|
||||
void ActionEndpoint::apply(const Pose& value, const Pointer& source) {
|
||||
_currentPose = value;
|
||||
InputRecorder* inputRecorder = InputRecorder::getInstance();
|
||||
inputRecorder->setActionState(Action(_input.getChannel()), _currentPose);
|
||||
if (inputRecorder->isPlayingback()) {
|
||||
_currentPose = inputRecorder->getPoseState(Action(_input.getChannel()));
|
||||
}
|
||||
|
||||
if (!_currentPose.isValid()) {
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -1015,11 +1015,11 @@ void EntityTreeRenderer::addEntityToScene(EntityItemPointer entity) {
|
|||
}
|
||||
|
||||
|
||||
void EntityTreeRenderer::entityScriptChanging(const EntityItemID& entityID, const bool reload) {
|
||||
void EntityTreeRenderer::entityScriptChanging(const EntityItemID& entityID, bool reload) {
|
||||
checkAndCallPreload(entityID, reload, true);
|
||||
}
|
||||
|
||||
void EntityTreeRenderer::checkAndCallPreload(const EntityItemID& entityID, const bool reload, const bool unloadFirst) {
|
||||
void EntityTreeRenderer::checkAndCallPreload(const EntityItemID& entityID, bool reload, bool unloadFirst) {
|
||||
if (_tree && !_shuttingDown) {
|
||||
EntityItemPointer entity = getTree()->findEntityByEntityItemID(entityID);
|
||||
if (!entity) {
|
||||
|
@ -1027,11 +1027,11 @@ void EntityTreeRenderer::checkAndCallPreload(const EntityItemID& entityID, const
|
|||
}
|
||||
bool shouldLoad = entity->shouldPreloadScript() && _entitiesScriptEngine;
|
||||
QString scriptUrl = entity->getScript();
|
||||
if (shouldLoad && (unloadFirst || scriptUrl.isEmpty())) {
|
||||
if ((shouldLoad && unloadFirst) || scriptUrl.isEmpty()) {
|
||||
_entitiesScriptEngine->unloadEntityScript(entityID);
|
||||
entity->scriptHasUnloaded();
|
||||
}
|
||||
if (shouldLoad && !scriptUrl.isEmpty()) {
|
||||
if (shouldLoad) {
|
||||
scriptUrl = ResourceManager::normalizeURL(scriptUrl);
|
||||
_entitiesScriptEngine->loadEntityScript(entityID, scriptUrl, reload);
|
||||
entity->scriptHasPreloaded();
|
||||
|
|
|
@ -152,7 +152,7 @@ private:
|
|||
bool applySkyboxAndHasAmbient();
|
||||
bool applyLayeredZones();
|
||||
|
||||
void checkAndCallPreload(const EntityItemID& entityID, const bool reload = false, const bool unloadFirst = false);
|
||||
void checkAndCallPreload(const EntityItemID& entityID, bool reload = false, bool unloadFirst = false);
|
||||
|
||||
QList<ModelPointer> _releasedModels;
|
||||
RayToEntityIntersectionResult findRayIntersectionWorker(const PickRay& ray, Octree::lockType lockType,
|
||||
|
|
|
@ -448,10 +448,10 @@ void GLVariableAllocationSupport::updateMemoryPressure() {
|
|||
float pressure = (float)totalVariableMemoryAllocation / (float)allowedMemoryAllocation;
|
||||
|
||||
auto newState = MemoryPressureState::Idle;
|
||||
if (pressure > OVERSUBSCRIBED_PRESSURE_VALUE && canDemote) {
|
||||
newState = MemoryPressureState::Oversubscribed;
|
||||
} else if (pressure < UNDERSUBSCRIBED_PRESSURE_VALUE && unallocated != 0 && canPromote) {
|
||||
if (pressure < UNDERSUBSCRIBED_PRESSURE_VALUE && (unallocated != 0 && canPromote)) {
|
||||
newState = MemoryPressureState::Undersubscribed;
|
||||
} else if (pressure > OVERSUBSCRIBED_PRESSURE_VALUE && canDemote) {
|
||||
newState = MemoryPressureState::Oversubscribed;
|
||||
} else if (hasTransfers) {
|
||||
newState = MemoryPressureState::Transfer;
|
||||
}
|
||||
|
@ -529,6 +529,7 @@ void GLVariableAllocationSupport::processWorkQueues() {
|
|||
}
|
||||
|
||||
if (workQueue.empty()) {
|
||||
_memoryPressureState = MemoryPressureState::Idle;
|
||||
_memoryPressureStateStale = true;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -113,7 +113,7 @@ protected:
|
|||
static void manageMemory();
|
||||
|
||||
//bool canPromoteNoAllocate() const { return _allocatedMip < _populatedMip; }
|
||||
bool canPromote() const { return _allocatedMip > 0; }
|
||||
bool canPromote() const { return _allocatedMip > _minAllocatedMip; }
|
||||
bool canDemote() const { return _allocatedMip < _maxAllocatedMip; }
|
||||
bool hasPendingTransfers() const { return _populatedMip > _allocatedMip; }
|
||||
void executeNextTransfer(const TexturePointer& currentTexture);
|
||||
|
@ -131,6 +131,9 @@ protected:
|
|||
// The highest (lowest resolution) mip that we will support, relative to the number
|
||||
// of mips in the gpu::Texture object
|
||||
uint16 _maxAllocatedMip { 0 };
|
||||
// The lowest (highest resolution) mip that we will support, relative to the number
|
||||
// of mips in the gpu::Texture object
|
||||
uint16 _minAllocatedMip { 0 };
|
||||
// Contains a series of lambdas that when executed will transfer data to the GPU, modify
|
||||
// the _populatedMip and update the sampler in order to fully populate the allocated texture
|
||||
// until _populatedMip == _allocatedMip
|
||||
|
|
|
@ -55,6 +55,18 @@ GLTexture* GL41Backend::syncGPUObject(const TexturePointer& texturePointer) {
|
|||
default:
|
||||
Q_UNREACHABLE();
|
||||
}
|
||||
} else {
|
||||
if (texture.getUsageType() == TextureUsageType::RESOURCE) {
|
||||
auto varTex = static_cast<GL41VariableAllocationTexture*> (object);
|
||||
|
||||
if (varTex->_minAllocatedMip > 0) {
|
||||
auto minAvailableMip = texture.minAvailableMipLevel();
|
||||
if (minAvailableMip < varTex->_minAllocatedMip) {
|
||||
varTex->_minAllocatedMip = minAvailableMip;
|
||||
GL41VariableAllocationTexture::_memoryPressureStateStale = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return object;
|
||||
|
@ -231,15 +243,20 @@ using GL41VariableAllocationTexture = GL41Backend::GL41VariableAllocationTexture
|
|||
GL41VariableAllocationTexture::GL41VariableAllocationTexture(const std::weak_ptr<GLBackend>& backend, const Texture& texture) : GL41Texture(backend, texture) {
|
||||
auto mipLevels = texture.getNumMips();
|
||||
_allocatedMip = mipLevels;
|
||||
_maxAllocatedMip = _populatedMip = mipLevels;
|
||||
_minAllocatedMip = texture.minAvailableMipLevel();
|
||||
|
||||
uvec3 mipDimensions;
|
||||
for (uint16_t mip = 0; mip < mipLevels; ++mip) {
|
||||
for (uint16_t mip = _minAllocatedMip; mip < mipLevels; ++mip) {
|
||||
if (glm::all(glm::lessThanEqual(texture.evalMipDimensions(mip), INITIAL_MIP_TRANSFER_DIMENSIONS))) {
|
||||
_maxAllocatedMip = _populatedMip = mip;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
uint16_t allocatedMip = _populatedMip - std::min<uint16_t>(_populatedMip, 2);
|
||||
auto targetMip = _populatedMip - std::min<uint16_t>(_populatedMip, 2);
|
||||
uint16_t allocatedMip = std::max<uint16_t>(_minAllocatedMip, targetMip);
|
||||
|
||||
allocateStorage(allocatedMip);
|
||||
_memoryPressureStateStale = true;
|
||||
size_t maxFace = GLTexture::getFaceCount(_target);
|
||||
|
@ -292,6 +309,10 @@ void GL41VariableAllocationTexture::syncSampler() const {
|
|||
void GL41VariableAllocationTexture::promote() {
|
||||
PROFILE_RANGE(render_gpu_gl, __FUNCTION__);
|
||||
Q_ASSERT(_allocatedMip > 0);
|
||||
|
||||
uint16_t targetAllocatedMip = _allocatedMip - std::min<uint16_t>(_allocatedMip, 2);
|
||||
targetAllocatedMip = std::max<uint16_t>(_minAllocatedMip, targetAllocatedMip);
|
||||
|
||||
GLuint oldId = _id;
|
||||
auto oldSize = _size;
|
||||
// create new texture
|
||||
|
@ -299,7 +320,7 @@ void GL41VariableAllocationTexture::promote() {
|
|||
uint16_t oldAllocatedMip = _allocatedMip;
|
||||
|
||||
// allocate storage for new level
|
||||
allocateStorage(_allocatedMip - std::min<uint16_t>(_allocatedMip, 2));
|
||||
allocateStorage(targetAllocatedMip);
|
||||
|
||||
withPreservedTexture([&] {
|
||||
GLuint fbo { 0 };
|
||||
|
|
|
@ -80,6 +80,19 @@ GLTexture* GL45Backend::syncGPUObject(const TexturePointer& texturePointer) {
|
|||
default:
|
||||
Q_UNREACHABLE();
|
||||
}
|
||||
} else {
|
||||
|
||||
if (texture.getUsageType() == TextureUsageType::RESOURCE) {
|
||||
auto varTex = static_cast<GL45VariableAllocationTexture*> (object);
|
||||
|
||||
if (varTex->_minAllocatedMip > 0) {
|
||||
auto minAvailableMip = texture.minAvailableMipLevel();
|
||||
if (minAvailableMip < varTex->_minAllocatedMip) {
|
||||
varTex->_minAllocatedMip = minAvailableMip;
|
||||
GL45VariableAllocationTexture::_memoryPressureStateStale = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return object;
|
||||
|
@ -109,6 +122,10 @@ GL45Texture::GL45Texture(const std::weak_ptr<GLBackend>& backend, const Texture&
|
|||
GLuint GL45Texture::allocate(const Texture& texture) {
|
||||
GLuint result;
|
||||
glCreateTextures(getGLTextureType(texture), 1, &result);
|
||||
#ifdef DEBUG
|
||||
auto source = texture.source();
|
||||
glObjectLabel(GL_TEXTURE, result, source.length(), source.data());
|
||||
#endif
|
||||
return result;
|
||||
}
|
||||
|
||||
|
|
|
@ -43,16 +43,22 @@ using GL45ResourceTexture = GL45Backend::GL45ResourceTexture;
|
|||
GL45ResourceTexture::GL45ResourceTexture(const std::weak_ptr<GLBackend>& backend, const Texture& texture) : GL45VariableAllocationTexture(backend, texture) {
|
||||
auto mipLevels = texture.getNumMips();
|
||||
_allocatedMip = mipLevels;
|
||||
_maxAllocatedMip = _populatedMip = mipLevels;
|
||||
_minAllocatedMip = texture.minAvailableMipLevel();
|
||||
|
||||
uvec3 mipDimensions;
|
||||
for (uint16_t mip = 0; mip < mipLevels; ++mip) {
|
||||
for (uint16_t mip = _minAllocatedMip; mip < mipLevels; ++mip) {
|
||||
if (glm::all(glm::lessThanEqual(texture.evalMipDimensions(mip), INITIAL_MIP_TRANSFER_DIMENSIONS))) {
|
||||
_maxAllocatedMip = _populatedMip = mip;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
uint16_t allocatedMip = _populatedMip - std::min<uint16_t>(_populatedMip, 2);
|
||||
auto targetMip = _populatedMip - std::min<uint16_t>(_populatedMip, 2);
|
||||
uint16_t allocatedMip = std::max<uint16_t>(_minAllocatedMip, targetMip);
|
||||
|
||||
allocateStorage(allocatedMip);
|
||||
_memoryPressureStateStale = true;
|
||||
copyMipsFromTexture();
|
||||
syncSampler();
|
||||
|
||||
|
@ -70,6 +76,7 @@ void GL45ResourceTexture::allocateStorage(uint16 allocatedMip) {
|
|||
for (uint16_t mip = _allocatedMip; mip < mipLevels; ++mip) {
|
||||
_size += _gpuObject.evalMipSize(mip);
|
||||
}
|
||||
|
||||
Backend::updateTextureGPUMemoryUsage(0, _size);
|
||||
|
||||
}
|
||||
|
@ -93,13 +100,17 @@ void GL45ResourceTexture::syncSampler() const {
|
|||
void GL45ResourceTexture::promote() {
|
||||
PROFILE_RANGE(render_gpu_gl, __FUNCTION__);
|
||||
Q_ASSERT(_allocatedMip > 0);
|
||||
|
||||
uint16_t targetAllocatedMip = _allocatedMip - std::min<uint16_t>(_allocatedMip, 2);
|
||||
targetAllocatedMip = std::max<uint16_t>(_minAllocatedMip, targetAllocatedMip);
|
||||
|
||||
GLuint oldId = _id;
|
||||
auto oldSize = _size;
|
||||
// create new texture
|
||||
const_cast<GLuint&>(_id) = allocate(_gpuObject);
|
||||
uint16_t oldAllocatedMip = _allocatedMip;
|
||||
// allocate storage for new level
|
||||
allocateStorage(_allocatedMip - std::min<uint16_t>(_allocatedMip, 2));
|
||||
allocateStorage(targetAllocatedMip);
|
||||
uint16_t mips = _gpuObject.getNumMips();
|
||||
// copy pre-existing mips
|
||||
for (uint16_t mip = _populatedMip; mip < mips; ++mip) {
|
||||
|
|
|
@ -118,6 +118,7 @@ Texture::Size Texture::getAllowedGPUMemoryUsage() {
|
|||
return _allowedCPUMemoryUsage;
|
||||
}
|
||||
|
||||
|
||||
void Texture::setAllowedGPUMemoryUsage(Size size) {
|
||||
qCDebug(gpulogging) << "New MAX texture memory " << BYTES_TO_MB(size) << " MB";
|
||||
_allowedCPUMemoryUsage = size;
|
||||
|
@ -411,6 +412,7 @@ const Element& Texture::getStoredMipFormat() const {
|
|||
}
|
||||
|
||||
void Texture::assignStoredMip(uint16 level, Size size, const Byte* bytes) {
|
||||
// TODO Skip the extra allocation here
|
||||
storage::StoragePointer storage = std::make_shared<storage::MemoryStorage>(size, bytes);
|
||||
assignStoredMip(level, storage);
|
||||
}
|
||||
|
@ -474,6 +476,10 @@ void Texture::assignStoredMipFace(uint16 level, uint8 face, storage::StoragePoin
|
|||
}
|
||||
}
|
||||
|
||||
bool Texture::isStoredMipFaceAvailable(uint16 level, uint8 face) const {
|
||||
return _storage->isMipAvailable(level, face);
|
||||
}
|
||||
|
||||
void Texture::setAutoGenerateMips(bool enable) {
|
||||
bool changed = false;
|
||||
if (!_autoGenerateMips) {
|
||||
|
|
|
@ -28,10 +28,17 @@ namespace ktx {
|
|||
struct KTXDescriptor;
|
||||
using KTXDescriptorPointer = std::unique_ptr<KTXDescriptor>;
|
||||
struct Header;
|
||||
struct KeyValue;
|
||||
using KeyValues = std::list<KeyValue>;
|
||||
}
|
||||
|
||||
namespace gpu {
|
||||
|
||||
|
||||
const std::string SOURCE_HASH_KEY { "hifi.sourceHash" };
|
||||
|
||||
const uint8 SOURCE_HASH_BYTES = 16;
|
||||
|
||||
// THe spherical harmonics is a nice tool for cubemap, so if required, the irradiance SH can be automatically generated
|
||||
// with the cube texture
|
||||
class Texture;
|
||||
|
@ -150,7 +157,7 @@ protected:
|
|||
Desc _desc;
|
||||
};
|
||||
|
||||
enum class TextureUsageType {
|
||||
enum class TextureUsageType : uint8 {
|
||||
RENDERBUFFER, // Used as attachments to a framebuffer
|
||||
RESOURCE, // Resource textures, like materials... subject to memory manipulation
|
||||
STRICT_RESOURCE, // Resource textures not subject to manipulation, like the normal fitting texture
|
||||
|
@ -271,6 +278,7 @@ public:
|
|||
virtual void assignMipData(uint16 level, const storage::StoragePointer& storage) = 0;
|
||||
virtual void assignMipFaceData(uint16 level, uint8 face, const storage::StoragePointer& storage) = 0;
|
||||
virtual bool isMipAvailable(uint16 level, uint8 face = 0) const = 0;
|
||||
virtual uint16 minAvailableMipLevel() const { return 0; }
|
||||
Texture::Type getType() const { return _type; }
|
||||
|
||||
Stamp getStamp() const { return _stamp; }
|
||||
|
@ -308,24 +316,30 @@ public:
|
|||
KtxStorage(const std::string& filename);
|
||||
PixelsPointer getMipFace(uint16 level, uint8 face = 0) const override;
|
||||
Size getMipFaceSize(uint16 level, uint8 face = 0) const override;
|
||||
// By convention, all mip levels and faces MUST be populated when using KTX backing
|
||||
bool isMipAvailable(uint16 level, uint8 face = 0) const override { return true; }
|
||||
bool isMipAvailable(uint16 level, uint8 face = 0) const override;
|
||||
void assignMipData(uint16 level, const storage::StoragePointer& storage) override;
|
||||
void assignMipFaceData(uint16 level, uint8 face, const storage::StoragePointer& storage) override;
|
||||
uint16 minAvailableMipLevel() const override;
|
||||
|
||||
void assignMipData(uint16 level, const storage::StoragePointer& storage) override {
|
||||
throw std::runtime_error("Invalid call");
|
||||
}
|
||||
|
||||
void assignMipFaceData(uint16 level, uint8 face, const storage::StoragePointer& storage) override {
|
||||
throw std::runtime_error("Invalid call");
|
||||
}
|
||||
void reset() override { }
|
||||
|
||||
protected:
|
||||
std::shared_ptr<storage::FileStorage> maybeOpenFile();
|
||||
|
||||
std::mutex _cacheFileCreateMutex;
|
||||
std::mutex _cacheFileWriteMutex;
|
||||
std::weak_ptr<storage::FileStorage> _cacheFile;
|
||||
|
||||
std::string _filename;
|
||||
std::atomic<uint8_t> _minMipLevelAvailable;
|
||||
size_t _offsetToMinMipKV;
|
||||
|
||||
ktx::KTXDescriptorPointer _ktxDescriptor;
|
||||
friend class Texture;
|
||||
};
|
||||
|
||||
uint16 minAvailableMipLevel() const { return _storage->minAvailableMipLevel(); };
|
||||
|
||||
static const uint16 MAX_NUM_MIPS = 0;
|
||||
static const uint16 SINGLE_MIP = 1;
|
||||
static TexturePointer create1D(const Element& texelFormat, uint16 width, uint16 numMips = SINGLE_MIP, const Sampler& sampler = Sampler());
|
||||
|
@ -469,7 +483,7 @@ public:
|
|||
|
||||
// Access the stored mips and faces
|
||||
const PixelsPointer accessStoredMipFace(uint16 level, uint8 face = 0) const { return _storage->getMipFace(level, face); }
|
||||
bool isStoredMipFaceAvailable(uint16 level, uint8 face = 0) const { return _storage->isMipAvailable(level, face); }
|
||||
bool isStoredMipFaceAvailable(uint16 level, uint8 face = 0) const;
|
||||
Size getStoredMipFaceSize(uint16 level, uint8 face = 0) const { return _storage->getMipFaceSize(level, face); }
|
||||
Size getStoredMipSize(uint16 level) const;
|
||||
Size getStoredSize() const;
|
||||
|
@ -503,9 +517,12 @@ public:
|
|||
|
||||
ExternalUpdates getUpdates() const;
|
||||
|
||||
// Textures can be serialized directly to ktx data file, here is how
|
||||
// Serialize a texture into a KTX file
|
||||
static ktx::KTXUniquePointer serialize(const Texture& texture);
|
||||
static TexturePointer unserialize(const std::string& ktxFile, TextureUsageType usageType = TextureUsageType::RESOURCE, Usage usage = Usage(), const Sampler::Desc& sampler = Sampler::Desc());
|
||||
|
||||
static TexturePointer unserialize(const std::string& ktxFile);
|
||||
static TexturePointer unserialize(const std::string& ktxFile, const ktx::KTXDescriptor& descriptor);
|
||||
|
||||
static bool evalKTXFormat(const Element& mipFormat, const Element& texelFormat, ktx::Header& header);
|
||||
static bool evalTextureFormat(const ktx::Header& header, Element& mipFormat, Element& texelFormat);
|
||||
|
||||
|
|
|
@ -12,44 +12,114 @@
|
|||
|
||||
#include "Texture.h"
|
||||
|
||||
#include <QtCore/QByteArray>
|
||||
|
||||
#include <ktx/KTX.h>
|
||||
|
||||
#include "GPULogging.h"
|
||||
|
||||
using namespace gpu;
|
||||
|
||||
using PixelsPointer = Texture::PixelsPointer;
|
||||
using KtxStorage = Texture::KtxStorage;
|
||||
|
||||
struct GPUKTXPayload {
|
||||
using Version = uint8;
|
||||
|
||||
static const std::string KEY;
|
||||
static const Version CURRENT_VERSION { 1 };
|
||||
static const size_t PADDING { 2 };
|
||||
static const size_t SIZE { sizeof(Version) + sizeof(Sampler::Desc) + sizeof(uint32) + sizeof(TextureUsageType) + PADDING };
|
||||
static_assert(GPUKTXPayload::SIZE == 36, "Packing size may differ between platforms");
|
||||
static_assert(GPUKTXPayload::SIZE % 4 == 0, "GPUKTXPayload is not 4 bytes aligned");
|
||||
|
||||
Sampler::Desc _samplerDesc;
|
||||
Texture::Usage _usage;
|
||||
TextureUsageType _usageType;
|
||||
|
||||
Byte* serialize(Byte* data) const {
|
||||
*(Version*)data = CURRENT_VERSION;
|
||||
data += sizeof(Version);
|
||||
|
||||
memcpy(data, &_samplerDesc, sizeof(Sampler::Desc));
|
||||
data += sizeof(Sampler::Desc);
|
||||
|
||||
// We can't copy the bitset in Texture::Usage in a crossplateform manner
|
||||
// So serialize it manually
|
||||
*(uint32*)data = _usage._flags.to_ulong();
|
||||
data += sizeof(uint32);
|
||||
|
||||
*(TextureUsageType*)data = _usageType;
|
||||
data += sizeof(TextureUsageType);
|
||||
|
||||
return data + PADDING;
|
||||
}
|
||||
|
||||
bool unserialize(const Byte* data, size_t size) {
|
||||
if (size != SIZE) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Version version = *(const Version*)data;
|
||||
if (version != CURRENT_VERSION) {
|
||||
glm::vec4 borderColor(1.0f);
|
||||
if (memcmp(&borderColor, data, sizeof(glm::vec4)) == 0) {
|
||||
memcpy(this, data, sizeof(GPUKTXPayload));
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
data += sizeof(Version);
|
||||
|
||||
memcpy(&_samplerDesc, data, sizeof(Sampler::Desc));
|
||||
data += sizeof(Sampler::Desc);
|
||||
|
||||
// We can't copy the bitset in Texture::Usage in a crossplateform manner
|
||||
// So unserialize it manually
|
||||
_usage = Texture::Usage(*(const uint32*)data);
|
||||
data += sizeof(uint32);
|
||||
|
||||
_usageType = *(const TextureUsageType*)data;
|
||||
return true;
|
||||
}
|
||||
|
||||
static std::string KEY;
|
||||
static bool isGPUKTX(const ktx::KeyValue& val) {
|
||||
return (val._key.compare(KEY) == 0);
|
||||
}
|
||||
|
||||
static bool findInKeyValues(const ktx::KeyValues& keyValues, GPUKTXPayload& payload) {
|
||||
auto found = std::find_if(keyValues.begin(), keyValues.end(), isGPUKTX);
|
||||
auto found = std::find_if(keyValues.begin(), keyValues.end(), isGPUKTX);
|
||||
if (found != keyValues.end()) {
|
||||
if ((*found)._value.size() == sizeof(GPUKTXPayload)) {
|
||||
memcpy(&payload, (*found)._value.data(), sizeof(GPUKTXPayload));
|
||||
return true;
|
||||
}
|
||||
auto value = found->_value;
|
||||
return payload.unserialize(value.data(), value.size());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
std::string GPUKTXPayload::KEY { "hifi.gpu" };
|
||||
const std::string GPUKTXPayload::KEY { "hifi.gpu" };
|
||||
|
||||
KtxStorage::KtxStorage(const std::string& filename) : _filename(filename) {
|
||||
{
|
||||
ktx::StoragePointer storage { new storage::FileStorage(_filename.c_str()) };
|
||||
// We are doing a lot of work here just to get descriptor data
|
||||
ktx::StoragePointer storage{ new storage::FileStorage(_filename.c_str()) };
|
||||
auto ktxPointer = ktx::KTX::create(storage);
|
||||
_ktxDescriptor.reset(new ktx::KTXDescriptor(ktxPointer->toDescriptor()));
|
||||
if (_ktxDescriptor->images.size() < _ktxDescriptor->header.numberOfMipmapLevels) {
|
||||
qWarning() << "Bad images found in ktx";
|
||||
}
|
||||
|
||||
_offsetToMinMipKV = _ktxDescriptor->getValueOffsetForKey(ktx::HIFI_MIN_POPULATED_MIP_KEY);
|
||||
if (_offsetToMinMipKV) {
|
||||
auto data = storage->data() + ktx::KTX_HEADER_SIZE + _offsetToMinMipKV;
|
||||
_minMipLevelAvailable = *data;
|
||||
} else {
|
||||
// Assume all mip levels are available
|
||||
_minMipLevelAvailable = 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// now that we know the ktx, let's get the header info to configure this Texture::Storage:
|
||||
Format mipFormat = Format::COLOR_BGRA_32;
|
||||
Format texelFormat = Format::COLOR_SRGBA_32;
|
||||
|
@ -58,6 +128,27 @@ KtxStorage::KtxStorage(const std::string& filename) : _filename(filename) {
|
|||
}
|
||||
}
|
||||
|
||||
std::shared_ptr<storage::FileStorage> KtxStorage::maybeOpenFile() {
|
||||
std::shared_ptr<storage::FileStorage> file = _cacheFile.lock();
|
||||
if (file) {
|
||||
return file;
|
||||
}
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock{ _cacheFileCreateMutex };
|
||||
|
||||
file = _cacheFile.lock();
|
||||
if (file) {
|
||||
return file;
|
||||
}
|
||||
|
||||
file = std::make_shared<storage::FileStorage>(_filename.c_str());
|
||||
_cacheFile = file;
|
||||
}
|
||||
|
||||
return file;
|
||||
}
|
||||
|
||||
PixelsPointer KtxStorage::getMipFace(uint16 level, uint8 face) const {
|
||||
storage::StoragePointer result;
|
||||
auto faceOffset = _ktxDescriptor->getMipFaceTexelsOffset(level, face);
|
||||
|
@ -72,6 +163,58 @@ Size KtxStorage::getMipFaceSize(uint16 level, uint8 face) const {
|
|||
return _ktxDescriptor->getMipFaceTexelsSize(level, face);
|
||||
}
|
||||
|
||||
|
||||
bool KtxStorage::isMipAvailable(uint16 level, uint8 face) const {
|
||||
return level >= _minMipLevelAvailable;
|
||||
}
|
||||
|
||||
uint16 KtxStorage::minAvailableMipLevel() const {
|
||||
return _minMipLevelAvailable;
|
||||
}
|
||||
|
||||
void KtxStorage::assignMipData(uint16 level, const storage::StoragePointer& storage) {
|
||||
if (level != _minMipLevelAvailable - 1) {
|
||||
qWarning() << "Invalid level to be stored, expected: " << (_minMipLevelAvailable - 1) << ", got: " << level << " " << _filename.c_str();
|
||||
return;
|
||||
}
|
||||
|
||||
if (level >= _ktxDescriptor->images.size()) {
|
||||
throw std::runtime_error("Invalid level");
|
||||
}
|
||||
|
||||
if (storage->size() != _ktxDescriptor->images[level]._imageSize) {
|
||||
qWarning() << "Invalid image size: " << storage->size() << ", expected: " << _ktxDescriptor->images[level]._imageSize
|
||||
<< ", level: " << level << ", filename: " << QString::fromStdString(_filename);
|
||||
return;
|
||||
}
|
||||
|
||||
auto file = maybeOpenFile();
|
||||
|
||||
auto imageData = file->mutableData();
|
||||
imageData += ktx::KTX_HEADER_SIZE + _ktxDescriptor->header.bytesOfKeyValueData + _ktxDescriptor->images[level]._imageOffset;
|
||||
imageData += ktx::IMAGE_SIZE_WIDTH;
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock { _cacheFileWriteMutex };
|
||||
|
||||
if (level != _minMipLevelAvailable - 1) {
|
||||
qWarning() << "Invalid level to be stored";
|
||||
return;
|
||||
}
|
||||
|
||||
memcpy(imageData, storage->data(), _ktxDescriptor->images[level]._imageSize);
|
||||
_minMipLevelAvailable = level;
|
||||
if (_offsetToMinMipKV > 0) {
|
||||
auto minMipKeyData = file->mutableData() + ktx::KTX_HEADER_SIZE + _offsetToMinMipKV;
|
||||
memcpy(minMipKeyData, (void*)&_minMipLevelAvailable, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void KtxStorage::assignMipFaceData(uint16 level, uint8 face, const storage::StoragePointer& storage) {
|
||||
throw std::runtime_error("Invalid call");
|
||||
}
|
||||
|
||||
void Texture::setKtxBacking(const std::string& filename) {
|
||||
// Check the KTX file for validity before using it as backing storage
|
||||
{
|
||||
|
@ -86,6 +229,7 @@ void Texture::setKtxBacking(const std::string& filename) {
|
|||
setStorage(newBacking);
|
||||
}
|
||||
|
||||
|
||||
ktx::KTXUniquePointer Texture::serialize(const Texture& texture) {
|
||||
ktx::Header header;
|
||||
|
||||
|
@ -141,19 +285,21 @@ ktx::KTXUniquePointer Texture::serialize(const Texture& texture) {
|
|||
header.numberOfMipmapLevels = texture.getNumMips();
|
||||
|
||||
ktx::Images images;
|
||||
uint32_t imageOffset = 0;
|
||||
for (uint32_t level = 0; level < header.numberOfMipmapLevels; level++) {
|
||||
auto mip = texture.accessStoredMipFace(level);
|
||||
if (mip) {
|
||||
if (numFaces == 1) {
|
||||
images.emplace_back(ktx::Image((uint32_t)mip->getSize(), 0, mip->readData()));
|
||||
images.emplace_back(ktx::Image(imageOffset, (uint32_t)mip->getSize(), 0, mip->readData()));
|
||||
} else {
|
||||
ktx::Image::FaceBytes cubeFaces(Texture::CUBE_FACE_COUNT);
|
||||
cubeFaces[0] = mip->readData();
|
||||
for (uint32_t face = 1; face < Texture::CUBE_FACE_COUNT; face++) {
|
||||
cubeFaces[face] = texture.accessStoredMipFace(level, face)->readData();
|
||||
}
|
||||
images.emplace_back(ktx::Image((uint32_t)mip->getSize(), 0, cubeFaces));
|
||||
images.emplace_back(ktx::Image(imageOffset, (uint32_t)mip->getSize(), 0, cubeFaces));
|
||||
}
|
||||
imageOffset += static_cast<uint32_t>(mip->getSize()) + ktx::IMAGE_SIZE_WIDTH;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -161,13 +307,18 @@ ktx::KTXUniquePointer Texture::serialize(const Texture& texture) {
|
|||
keyval._samplerDesc = texture.getSampler().getDesc();
|
||||
keyval._usage = texture.getUsage();
|
||||
keyval._usageType = texture.getUsageType();
|
||||
ktx::KeyValues keyValues;
|
||||
keyValues.emplace_back(ktx::KeyValue(GPUKTXPayload::KEY, sizeof(GPUKTXPayload), (ktx::Byte*) &keyval));
|
||||
Byte keyvalPayload[GPUKTXPayload::SIZE];
|
||||
keyval.serialize(keyvalPayload);
|
||||
|
||||
ktx::KeyValues keyValues;
|
||||
keyValues.emplace_back(GPUKTXPayload::KEY, (uint32)GPUKTXPayload::SIZE, (ktx::Byte*) &keyvalPayload);
|
||||
|
||||
static const std::string SOURCE_HASH_KEY = "hifi.sourceHash";
|
||||
auto hash = texture.sourceHash();
|
||||
if (!hash.empty()) {
|
||||
keyValues.emplace_back(ktx::KeyValue(SOURCE_HASH_KEY, static_cast<uint32>(hash.size()), (ktx::Byte*) hash.c_str()));
|
||||
// the sourceHash is an std::string in hex
|
||||
// we use QByteArray to take the hex and turn it into the smaller binary representation (16 bytes)
|
||||
auto binaryHash = QByteArray::fromHex(QByteArray::fromStdString(hash));
|
||||
keyValues.emplace_back(SOURCE_HASH_KEY, static_cast<uint32>(binaryHash.size()), (ktx::Byte*) binaryHash.data());
|
||||
}
|
||||
|
||||
auto ktxBuffer = ktx::KTX::create(header, images, keyValues);
|
||||
|
@ -200,13 +351,17 @@ ktx::KTXUniquePointer Texture::serialize(const Texture& texture) {
|
|||
return ktxBuffer;
|
||||
}
|
||||
|
||||
TexturePointer Texture::unserialize(const std::string& ktxfile, TextureUsageType usageType, Usage usage, const Sampler::Desc& sampler) {
|
||||
std::unique_ptr<ktx::KTX> ktxPointer = ktx::KTX::create(ktx::StoragePointer { new storage::FileStorage(ktxfile.c_str()) });
|
||||
TexturePointer Texture::unserialize(const std::string& ktxfile) {
|
||||
std::unique_ptr<ktx::KTX> ktxPointer = ktx::KTX::create(std::make_shared<storage::FileStorage>(ktxfile.c_str()));
|
||||
if (!ktxPointer) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
ktx::KTXDescriptor descriptor { ktxPointer->toDescriptor() };
|
||||
return unserialize(ktxfile, ktxPointer->toDescriptor());
|
||||
}
|
||||
|
||||
TexturePointer Texture::unserialize(const std::string& ktxfile, const ktx::KTXDescriptor& descriptor) {
|
||||
const auto& header = descriptor.header;
|
||||
|
||||
Format mipFormat = Format::COLOR_BGRA_32;
|
||||
|
@ -232,28 +387,28 @@ TexturePointer Texture::unserialize(const std::string& ktxfile, TextureUsageType
|
|||
type = TEX_3D;
|
||||
}
|
||||
|
||||
|
||||
// If found, use the
|
||||
GPUKTXPayload gpuktxKeyValue;
|
||||
bool isGPUKTXPayload = GPUKTXPayload::findInKeyValues(descriptor.keyValues, gpuktxKeyValue);
|
||||
if (!GPUKTXPayload::findInKeyValues(descriptor.keyValues, gpuktxKeyValue)) {
|
||||
qCWarning(gpulogging) << "Could not find GPUKTX key values.";
|
||||
return TexturePointer();
|
||||
}
|
||||
|
||||
auto tex = Texture::create( (isGPUKTXPayload ? gpuktxKeyValue._usageType : usageType),
|
||||
type,
|
||||
texelFormat,
|
||||
header.getPixelWidth(),
|
||||
header.getPixelHeight(),
|
||||
header.getPixelDepth(),
|
||||
1, // num Samples
|
||||
header.getNumberOfSlices(),
|
||||
header.getNumberOfLevels(),
|
||||
(isGPUKTXPayload ? gpuktxKeyValue._samplerDesc : sampler));
|
||||
|
||||
tex->setUsage((isGPUKTXPayload ? gpuktxKeyValue._usage : usage));
|
||||
auto texture = create(gpuktxKeyValue._usageType,
|
||||
type,
|
||||
texelFormat,
|
||||
header.getPixelWidth(),
|
||||
header.getPixelHeight(),
|
||||
header.getPixelDepth(),
|
||||
1, // num Samples
|
||||
header.getNumberOfSlices(),
|
||||
header.getNumberOfLevels(),
|
||||
gpuktxKeyValue._samplerDesc);
|
||||
texture->setUsage(gpuktxKeyValue._usage);
|
||||
|
||||
// Assing the mips availables
|
||||
tex->setStoredMipFormat(mipFormat);
|
||||
tex->setKtxBacking(ktxfile);
|
||||
return tex;
|
||||
texture->setStoredMipFormat(mipFormat);
|
||||
texture->setKtxBacking(ktxfile);
|
||||
return texture;
|
||||
}
|
||||
|
||||
bool Texture::evalKTXFormat(const Element& mipFormat, const Element& texelFormat, ktx::Header& header) {
|
||||
|
|
|
@ -12,6 +12,7 @@
|
|||
#include "KTX.h"
|
||||
|
||||
#include <algorithm> //min max and more
|
||||
#include <QDebug>
|
||||
|
||||
using namespace ktx;
|
||||
|
||||
|
@ -34,30 +35,80 @@ uint32_t Header::evalMaxDimension() const {
|
|||
return std::max(getPixelWidth(), std::max(getPixelHeight(), getPixelDepth()));
|
||||
}
|
||||
|
||||
uint32_t Header::evalPixelWidth(uint32_t level) const {
|
||||
return std::max(getPixelWidth() >> level, 1U);
|
||||
uint32_t Header::evalPixelOrBlockWidth(uint32_t level) const {
|
||||
auto pixelWidth = std::max(getPixelWidth() >> level, 1U);
|
||||
if (getGLType() == GLType::COMPRESSED_TYPE) {
|
||||
return (pixelWidth + 3) / 4;
|
||||
} else {
|
||||
return pixelWidth;
|
||||
}
|
||||
}
|
||||
uint32_t Header::evalPixelHeight(uint32_t level) const {
|
||||
return std::max(getPixelHeight() >> level, 1U);
|
||||
uint32_t Header::evalPixelOrBlockHeight(uint32_t level) const {
|
||||
auto pixelWidth = std::max(getPixelHeight() >> level, 1U);
|
||||
if (getGLType() == GLType::COMPRESSED_TYPE) {
|
||||
auto format = getGLInternaFormat_Compressed();
|
||||
switch (format) {
|
||||
case GLInternalFormat_Compressed::COMPRESSED_SRGB_S3TC_DXT1_EXT: // BC1
|
||||
case GLInternalFormat_Compressed::COMPRESSED_SRGB_ALPHA_S3TC_DXT1_EXT: // BC1A
|
||||
case GLInternalFormat_Compressed::COMPRESSED_SRGB_ALPHA_S3TC_DXT5_EXT: // BC3
|
||||
case GLInternalFormat_Compressed::COMPRESSED_RED_RGTC1: // BC4
|
||||
case GLInternalFormat_Compressed::COMPRESSED_RG_RGTC2: // BC5
|
||||
return (pixelWidth + 3) / 4;
|
||||
default:
|
||||
throw std::runtime_error("Unknown format");
|
||||
}
|
||||
} else {
|
||||
return pixelWidth;
|
||||
}
|
||||
}
|
||||
uint32_t Header::evalPixelDepth(uint32_t level) const {
|
||||
uint32_t Header::evalPixelOrBlockDepth(uint32_t level) const {
|
||||
return std::max(getPixelDepth() >> level, 1U);
|
||||
}
|
||||
|
||||
size_t Header::evalPixelSize() const {
|
||||
return glTypeSize; // Really we should generate the size from the FOrmat etc
|
||||
size_t Header::evalPixelOrBlockSize() const {
|
||||
if (getGLType() == GLType::COMPRESSED_TYPE) {
|
||||
auto format = getGLInternaFormat_Compressed();
|
||||
if (format == GLInternalFormat_Compressed::COMPRESSED_SRGB_S3TC_DXT1_EXT) {
|
||||
return 8;
|
||||
} else if (format == GLInternalFormat_Compressed::COMPRESSED_SRGB_ALPHA_S3TC_DXT1_EXT) {
|
||||
return 8;
|
||||
} else if (format == GLInternalFormat_Compressed::COMPRESSED_SRGB_ALPHA_S3TC_DXT5_EXT) {
|
||||
return 16;
|
||||
} else if (format == GLInternalFormat_Compressed::COMPRESSED_RED_RGTC1) {
|
||||
return 8;
|
||||
} else if (format == GLInternalFormat_Compressed::COMPRESSED_RG_RGTC2) {
|
||||
return 16;
|
||||
}
|
||||
} else {
|
||||
auto baseFormat = getGLBaseInternalFormat();
|
||||
if (baseFormat == GLBaseInternalFormat::RED) {
|
||||
return 1;
|
||||
} else if (baseFormat == GLBaseInternalFormat::RG) {
|
||||
return 2;
|
||||
} else if (baseFormat == GLBaseInternalFormat::RGB) {
|
||||
return 3;
|
||||
} else if (baseFormat == GLBaseInternalFormat::RGBA) {
|
||||
return 4;
|
||||
}
|
||||
}
|
||||
|
||||
qWarning() << "Unknown ktx format: " << glFormat << " " << glBaseInternalFormat << " " << glInternalFormat;
|
||||
return 0;
|
||||
}
|
||||
|
||||
size_t Header::evalRowSize(uint32_t level) const {
|
||||
auto pixWidth = evalPixelWidth(level);
|
||||
auto pixSize = evalPixelSize();
|
||||
auto pixWidth = evalPixelOrBlockWidth(level);
|
||||
auto pixSize = evalPixelOrBlockSize();
|
||||
if (pixSize == 0) {
|
||||
return 0;
|
||||
}
|
||||
auto netSize = pixWidth * pixSize;
|
||||
auto padding = evalPadding(netSize);
|
||||
return netSize + padding;
|
||||
}
|
||||
size_t Header::evalFaceSize(uint32_t level) const {
|
||||
auto pixHeight = evalPixelHeight(level);
|
||||
auto pixDepth = evalPixelDepth(level);
|
||||
auto pixHeight = evalPixelOrBlockHeight(level);
|
||||
auto pixDepth = evalPixelOrBlockDepth(level);
|
||||
auto rowSize = evalRowSize(level);
|
||||
return pixDepth * pixHeight * rowSize;
|
||||
}
|
||||
|
@ -71,6 +122,47 @@ size_t Header::evalImageSize(uint32_t level) const {
|
|||
}
|
||||
|
||||
|
||||
size_t KTXDescriptor::getValueOffsetForKey(const std::string& key) const {
|
||||
size_t offset { 0 };
|
||||
for (auto& kv : keyValues) {
|
||||
if (kv._key == key) {
|
||||
return offset + ktx::KV_SIZE_WIDTH + kv._key.size() + 1;
|
||||
}
|
||||
offset += kv.serializedByteSize();
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
ImageDescriptors Header::generateImageDescriptors() const {
|
||||
ImageDescriptors descriptors;
|
||||
|
||||
size_t imageOffset = 0;
|
||||
for (uint32_t level = 0; level < numberOfMipmapLevels; ++level) {
|
||||
auto imageSize = static_cast<uint32_t>(evalImageSize(level));
|
||||
if (imageSize == 0) {
|
||||
return ImageDescriptors();
|
||||
}
|
||||
ImageHeader header {
|
||||
numberOfFaces == NUM_CUBEMAPFACES,
|
||||
imageOffset,
|
||||
imageSize,
|
||||
0
|
||||
};
|
||||
|
||||
imageOffset += (imageSize * numberOfFaces) + ktx::IMAGE_SIZE_WIDTH;
|
||||
|
||||
ImageHeader::FaceOffsets offsets;
|
||||
// TODO Add correct face offsets
|
||||
for (uint32_t i = 0; i < numberOfFaces; ++i) {
|
||||
offsets.push_back(0);
|
||||
}
|
||||
descriptors.push_back(ImageDescriptor(header, offsets));
|
||||
}
|
||||
|
||||
return descriptors;
|
||||
}
|
||||
|
||||
|
||||
KeyValue::KeyValue(const std::string& key, uint32_t valueByteSize, const Byte* value) :
|
||||
_byteSize((uint32_t) key.size() + 1 + valueByteSize), // keyString size + '\0' ending char + the value size
|
||||
_key(key),
|
||||
|
@ -209,4 +301,4 @@ KTXDescriptor KTX::toDescriptor() const {
|
|||
|
||||
KTX::KTX(const StoragePointer& storage, const Header& header, const KeyValues& keyValues, const Images& images)
|
||||
: _header(header), _storage(storage), _keyValues(keyValues), _images(images) {
|
||||
}
|
||||
}
|
||||
|
|
|
@ -71,6 +71,8 @@ end
|
|||
|
||||
namespace ktx {
|
||||
const uint32_t PACKING_SIZE { sizeof(uint32_t) };
|
||||
const std::string HIFI_MIN_POPULATED_MIP_KEY{ "hifi.minMip" };
|
||||
|
||||
using Byte = uint8_t;
|
||||
|
||||
enum class GLType : uint32_t {
|
||||
|
@ -292,6 +294,11 @@ namespace ktx {
|
|||
using Storage = storage::Storage;
|
||||
using StoragePointer = std::shared_ptr<Storage>;
|
||||
|
||||
struct ImageDescriptor;
|
||||
using ImageDescriptors = std::vector<ImageDescriptor>;
|
||||
|
||||
bool checkIdentifier(const Byte* identifier);
|
||||
|
||||
// Header
|
||||
struct Header {
|
||||
static const size_t IDENTIFIER_LENGTH = 12;
|
||||
|
@ -330,11 +337,11 @@ namespace ktx {
|
|||
uint32_t getNumberOfLevels() const { return (numberOfMipmapLevels ? numberOfMipmapLevels : 1); }
|
||||
|
||||
uint32_t evalMaxDimension() const;
|
||||
uint32_t evalPixelWidth(uint32_t level) const;
|
||||
uint32_t evalPixelHeight(uint32_t level) const;
|
||||
uint32_t evalPixelDepth(uint32_t level) const;
|
||||
uint32_t evalPixelOrBlockWidth(uint32_t level) const;
|
||||
uint32_t evalPixelOrBlockHeight(uint32_t level) const;
|
||||
uint32_t evalPixelOrBlockDepth(uint32_t level) const;
|
||||
|
||||
size_t evalPixelSize() const;
|
||||
size_t evalPixelOrBlockSize() const;
|
||||
size_t evalRowSize(uint32_t level) const;
|
||||
size_t evalFaceSize(uint32_t level) const;
|
||||
size_t evalImageSize(uint32_t level) const;
|
||||
|
@ -378,7 +385,12 @@ namespace ktx {
|
|||
void setCube(uint32_t width, uint32_t height) { setDimensions(width, height, 0, 0, NUM_CUBEMAPFACES); }
|
||||
void setCubeArray(uint32_t width, uint32_t height, uint32_t numSlices) { setDimensions(width, height, 0, (numSlices > 0 ? numSlices : 1), NUM_CUBEMAPFACES); }
|
||||
|
||||
ImageDescriptors generateImageDescriptors() const;
|
||||
};
|
||||
static const size_t KTX_HEADER_SIZE = 64;
|
||||
static_assert(sizeof(Header) == KTX_HEADER_SIZE, "KTX Header size is static and should not change from the spec");
|
||||
static const size_t KV_SIZE_WIDTH = 4; // Number of bytes for keyAndValueByteSize
|
||||
static const size_t IMAGE_SIZE_WIDTH = 4; // Number of bytes for imageSize
|
||||
|
||||
// Key Values
|
||||
struct KeyValue {
|
||||
|
@ -405,12 +417,17 @@ namespace ktx {
|
|||
struct ImageHeader {
|
||||
using FaceOffsets = std::vector<size_t>;
|
||||
using FaceBytes = std::vector<const Byte*>;
|
||||
|
||||
// This is the byte offset from the _start_ of the image region. For example, level 0
|
||||
// will have a byte offset of 0.
|
||||
const uint32_t _numFaces;
|
||||
const size_t _imageOffset;
|
||||
const uint32_t _imageSize;
|
||||
const uint32_t _faceSize;
|
||||
const uint32_t _padding;
|
||||
ImageHeader(bool cube, uint32_t imageSize, uint32_t padding) :
|
||||
ImageHeader(bool cube, size_t imageOffset, uint32_t imageSize, uint32_t padding) :
|
||||
_numFaces(cube ? NUM_CUBEMAPFACES : 1),
|
||||
_imageOffset(imageOffset),
|
||||
_imageSize(imageSize * _numFaces),
|
||||
_faceSize(imageSize),
|
||||
_padding(padding) {
|
||||
|
@ -419,22 +436,22 @@ namespace ktx {
|
|||
|
||||
struct Image;
|
||||
|
||||
// Image without the image data itself
|
||||
struct ImageDescriptor : public ImageHeader {
|
||||
const FaceOffsets _faceOffsets;
|
||||
ImageDescriptor(const ImageHeader& header, const FaceOffsets& offsets) : ImageHeader(header), _faceOffsets(offsets) {}
|
||||
Image toImage(const ktx::StoragePointer& storage) const;
|
||||
};
|
||||
|
||||
using ImageDescriptors = std::vector<ImageDescriptor>;
|
||||
|
||||
// Image with the image data itself
|
||||
struct Image : public ImageHeader {
|
||||
FaceBytes _faceBytes;
|
||||
Image(const ImageHeader& header, const FaceBytes& faces) : ImageHeader(header), _faceBytes(faces) {}
|
||||
Image(uint32_t imageSize, uint32_t padding, const Byte* bytes) :
|
||||
ImageHeader(false, imageSize, padding),
|
||||
Image(size_t imageOffset, uint32_t imageSize, uint32_t padding, const Byte* bytes) :
|
||||
ImageHeader(false, imageOffset, imageSize, padding),
|
||||
_faceBytes(1, bytes) {}
|
||||
Image(uint32_t pageSize, uint32_t padding, const FaceBytes& cubeFaceBytes) :
|
||||
ImageHeader(true, pageSize, padding)
|
||||
Image(size_t imageOffset, uint32_t pageSize, uint32_t padding, const FaceBytes& cubeFaceBytes) :
|
||||
ImageHeader(true, imageOffset, pageSize, padding)
|
||||
{
|
||||
if (cubeFaceBytes.size() == NUM_CUBEMAPFACES) {
|
||||
_faceBytes = cubeFaceBytes;
|
||||
|
@ -457,6 +474,7 @@ namespace ktx {
|
|||
const ImageDescriptors images;
|
||||
size_t getMipFaceTexelsSize(uint16_t mip = 0, uint8_t face = 0) const;
|
||||
size_t getMipFaceTexelsOffset(uint16_t mip = 0, uint8_t face = 0) const;
|
||||
size_t getValueOffsetForKey(const std::string& key) const;
|
||||
};
|
||||
|
||||
class KTX {
|
||||
|
@ -471,6 +489,7 @@ namespace ktx {
|
|||
// This path allocate the Storage where to store header, keyvalues and copy mips
|
||||
// Then COPY all the data
|
||||
static std::unique_ptr<KTX> create(const Header& header, const Images& images, const KeyValues& keyValues = KeyValues());
|
||||
static std::unique_ptr<KTX> createBare(const Header& header, const KeyValues& keyValues = KeyValues());
|
||||
|
||||
// Instead of creating a full Copy of the src data in a KTX object, the write serialization can be performed with the
|
||||
// following two functions
|
||||
|
@ -484,10 +503,14 @@ namespace ktx {
|
|||
//
|
||||
// This is exactly what is done in the create function
|
||||
static size_t evalStorageSize(const Header& header, const Images& images, const KeyValues& keyValues = KeyValues());
|
||||
static size_t evalStorageSize(const Header& header, const ImageDescriptors& images, const KeyValues& keyValues = KeyValues());
|
||||
static size_t write(Byte* destBytes, size_t destByteSize, const Header& header, const Images& images, const KeyValues& keyValues = KeyValues());
|
||||
static size_t writeWithoutImages(Byte* destBytes, size_t destByteSize, const Header& header, const ImageDescriptors& descriptors, const KeyValues& keyValues = KeyValues());
|
||||
static size_t writeKeyValues(Byte* destBytes, size_t destByteSize, const KeyValues& keyValues);
|
||||
static Images writeImages(Byte* destBytes, size_t destByteSize, const Images& images);
|
||||
|
||||
void writeMipData(uint16_t level, const Byte* sourceBytes, size_t source_size);
|
||||
|
||||
// Parse a block of memory and create a KTX object from it
|
||||
static std::unique_ptr<KTX> create(const StoragePointer& src);
|
||||
|
||||
|
|
|
@ -144,6 +144,7 @@ namespace ktx {
|
|||
while ((currentPtr - srcBytes) + sizeof(uint32_t) <= (srcSize)) {
|
||||
|
||||
// Grab the imageSize coming up
|
||||
uint32_t imageOffset = currentPtr - srcBytes;
|
||||
size_t imageSize = *reinterpret_cast<const uint32_t*>(currentPtr);
|
||||
currentPtr += sizeof(uint32_t);
|
||||
|
||||
|
@ -158,10 +159,10 @@ namespace ktx {
|
|||
faces[face] = currentPtr;
|
||||
currentPtr += faceSize;
|
||||
}
|
||||
images.emplace_back(Image((uint32_t) faceSize, padding, faces));
|
||||
images.emplace_back(Image(imageOffset, (uint32_t) faceSize, padding, faces));
|
||||
currentPtr += padding;
|
||||
} else {
|
||||
images.emplace_back(Image((uint32_t) imageSize, padding, currentPtr));
|
||||
images.emplace_back(Image(imageOffset, (uint32_t) imageSize, padding, currentPtr));
|
||||
currentPtr += imageSize + padding;
|
||||
}
|
||||
} else {
|
||||
|
|
|
@ -40,6 +40,24 @@ namespace ktx {
|
|||
return create(storagePointer);
|
||||
}
|
||||
|
||||
std::unique_ptr<KTX> KTX::createBare(const Header& header, const KeyValues& keyValues) {
|
||||
auto descriptors = header.generateImageDescriptors();
|
||||
|
||||
Byte minMip = header.numberOfMipmapLevels;
|
||||
auto newKeyValues = keyValues;
|
||||
newKeyValues.emplace_back(KeyValue(HIFI_MIN_POPULATED_MIP_KEY, sizeof(Byte), &minMip));
|
||||
|
||||
StoragePointer storagePointer;
|
||||
{
|
||||
auto storageSize = ktx::KTX::evalStorageSize(header, descriptors, newKeyValues);
|
||||
auto memoryStorage = new storage::MemoryStorage(storageSize);
|
||||
qDebug() << "Memory storage size is: " << storageSize;
|
||||
ktx::KTX::writeWithoutImages(memoryStorage->data(), memoryStorage->size(), header, descriptors, newKeyValues);
|
||||
storagePointer.reset(memoryStorage);
|
||||
}
|
||||
return create(storagePointer);
|
||||
}
|
||||
|
||||
size_t KTX::evalStorageSize(const Header& header, const Images& images, const KeyValues& keyValues) {
|
||||
size_t storageSize = sizeof(Header);
|
||||
|
||||
|
@ -59,6 +77,25 @@ namespace ktx {
|
|||
return storageSize;
|
||||
}
|
||||
|
||||
size_t KTX::evalStorageSize(const Header& header, const ImageDescriptors& imageDescriptors, const KeyValues& keyValues) {
|
||||
size_t storageSize = sizeof(Header);
|
||||
|
||||
if (!keyValues.empty()) {
|
||||
size_t keyValuesSize = KeyValue::serializedKeyValuesByteSize(keyValues);
|
||||
storageSize += keyValuesSize;
|
||||
}
|
||||
|
||||
auto numMips = header.getNumberOfLevels();
|
||||
for (uint32_t l = 0; l < numMips; l++) {
|
||||
if (imageDescriptors.size() > l) {
|
||||
storageSize += sizeof(uint32_t);
|
||||
storageSize += imageDescriptors[l]._imageSize;
|
||||
storageSize += Header::evalPadding(imageDescriptors[l]._imageSize);
|
||||
}
|
||||
}
|
||||
return storageSize;
|
||||
}
|
||||
|
||||
size_t KTX::write(Byte* destBytes, size_t destByteSize, const Header& header, const Images& srcImages, const KeyValues& keyValues) {
|
||||
// Check again that we have enough destination capacity
|
||||
if (!destBytes || (destByteSize < evalStorageSize(header, srcImages, keyValues))) {
|
||||
|
@ -87,6 +124,43 @@ namespace ktx {
|
|||
return destByteSize;
|
||||
}
|
||||
|
||||
size_t KTX::writeWithoutImages(Byte* destBytes, size_t destByteSize, const Header& header, const ImageDescriptors& descriptors, const KeyValues& keyValues) {
|
||||
// Check again that we have enough destination capacity
|
||||
if (!destBytes || (destByteSize < evalStorageSize(header, descriptors, keyValues))) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
auto currentDestPtr = destBytes;
|
||||
// Header
|
||||
auto destHeader = reinterpret_cast<Header*>(currentDestPtr);
|
||||
memcpy(currentDestPtr, &header, sizeof(Header));
|
||||
currentDestPtr += sizeof(Header);
|
||||
|
||||
|
||||
// KeyValues
|
||||
if (!keyValues.empty()) {
|
||||
destHeader->bytesOfKeyValueData = (uint32_t) writeKeyValues(currentDestPtr, destByteSize - sizeof(Header), keyValues);
|
||||
} else {
|
||||
// Make sure the header contains the right bytesOfKeyValueData size
|
||||
destHeader->bytesOfKeyValueData = 0;
|
||||
}
|
||||
currentDestPtr += destHeader->bytesOfKeyValueData;
|
||||
|
||||
for (size_t i = 0; i < descriptors.size(); ++i) {
|
||||
auto ptr = reinterpret_cast<uint32_t*>(currentDestPtr);
|
||||
*ptr = descriptors[i]._imageSize;
|
||||
ptr++;
|
||||
#ifdef DEBUG
|
||||
for (size_t k = 0; k < descriptors[i]._imageSize/4; k++) {
|
||||
*(ptr + k) = 0xFFFFFFFF;
|
||||
}
|
||||
#endif
|
||||
currentDestPtr += descriptors[i]._imageSize + sizeof(uint32_t);
|
||||
}
|
||||
|
||||
return destByteSize;
|
||||
}
|
||||
|
||||
uint32_t KeyValue::writeSerializedKeyAndValue(Byte* destBytes, uint32_t destByteSize, const KeyValue& keyval) {
|
||||
uint32_t keyvalSize = keyval.serializedByteSize();
|
||||
if (keyvalSize > destByteSize) {
|
||||
|
@ -134,6 +208,7 @@ namespace ktx {
|
|||
|
||||
for (uint32_t l = 0; l < srcImages.size(); l++) {
|
||||
if (currentDataSize + sizeof(uint32_t) < allocatedImagesDataSize) {
|
||||
uint32_t imageOffset = currentPtr - destBytes;
|
||||
size_t imageSize = srcImages[l]._imageSize;
|
||||
*(reinterpret_cast<uint32_t*> (currentPtr)) = (uint32_t) imageSize;
|
||||
currentPtr += sizeof(uint32_t);
|
||||
|
@ -146,7 +221,7 @@ namespace ktx {
|
|||
// Single face vs cubes
|
||||
if (srcImages[l]._numFaces == 1) {
|
||||
memcpy(currentPtr, srcImages[l]._faceBytes[0], imageSize);
|
||||
destImages.emplace_back(Image((uint32_t) imageSize, padding, currentPtr));
|
||||
destImages.emplace_back(Image(imageOffset, (uint32_t) imageSize, padding, currentPtr));
|
||||
currentPtr += imageSize;
|
||||
} else {
|
||||
Image::FaceBytes faceBytes(NUM_CUBEMAPFACES);
|
||||
|
@ -156,7 +231,7 @@ namespace ktx {
|
|||
faceBytes[face] = currentPtr;
|
||||
currentPtr += faceSize;
|
||||
}
|
||||
destImages.emplace_back(Image(faceSize, padding, faceBytes));
|
||||
destImages.emplace_back(Image(imageOffset, faceSize, padding, faceBytes));
|
||||
}
|
||||
|
||||
currentPtr += padding;
|
||||
|
@ -168,4 +243,11 @@ namespace ktx {
|
|||
return destImages;
|
||||
}
|
||||
|
||||
void KTX::writeMipData(uint16_t level, const Byte* sourceBytes, size_t sourceSize) {
|
||||
Q_ASSERT(level > 0);
|
||||
Q_ASSERT(level < _images.size());
|
||||
Q_ASSERT(sourceSize == _images[level]._imageSize);
|
||||
|
||||
//memcpy(reinterpret_cast<void*>(_images[level]._faceBytes[0]), sourceBytes, sourceSize);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -30,8 +30,6 @@
|
|||
|
||||
#include <gpu/Batch.h>
|
||||
|
||||
#include <ktx/KTX.h>
|
||||
|
||||
#include <image/Image.h>
|
||||
|
||||
#include <NumericalConstants.h>
|
||||
|
@ -40,6 +38,7 @@
|
|||
#include <Finally.h>
|
||||
#include <Profile.h>
|
||||
|
||||
#include "NetworkLogging.h"
|
||||
#include "ModelNetworkingLogging.h"
|
||||
#include <Trace.h>
|
||||
#include <StatTracker.h>
|
||||
|
@ -51,6 +50,8 @@ Q_LOGGING_CATEGORY(trace_resource_parse_image_ktx, "trace.resource.parse.image.k
|
|||
const std::string TextureCache::KTX_DIRNAME { "ktx_cache" };
|
||||
const std::string TextureCache::KTX_EXT { "ktx" };
|
||||
|
||||
static const int SKYBOX_LOAD_PRIORITY { 10 }; // Make sure skybox loads first
|
||||
|
||||
TextureCache::TextureCache() :
|
||||
_ktxCache(KTX_DIRNAME, KTX_EXT) {
|
||||
setUnusedResourceCacheSize(0);
|
||||
|
@ -260,15 +261,20 @@ QSharedPointer<Resource> TextureCache::createResource(const QUrl& url, const QSh
|
|||
auto content = textureExtra ? textureExtra->content : QByteArray();
|
||||
auto maxNumPixels = textureExtra ? textureExtra->maxNumPixels : ABSOLUTE_MAX_TEXTURE_NUM_PIXELS;
|
||||
NetworkTexture* texture = new NetworkTexture(url, type, content, maxNumPixels);
|
||||
if (type == image::TextureUsage::CUBE_TEXTURE) {
|
||||
texture->setLoadPriority(this, SKYBOX_LOAD_PRIORITY);
|
||||
}
|
||||
return QSharedPointer<Resource>(texture, &Resource::deleter);
|
||||
}
|
||||
|
||||
NetworkTexture::NetworkTexture(const QUrl& url, image::TextureUsage::Type type, const QByteArray& content, int maxNumPixels) :
|
||||
Resource(url),
|
||||
_type(type),
|
||||
_sourceIsKTX(url.path().endsWith(".ktx")),
|
||||
_maxNumPixels(maxNumPixels)
|
||||
{
|
||||
_textureSource = std::make_shared<gpu::TextureSource>();
|
||||
_lowestRequestedMipLevel = 0;
|
||||
|
||||
if (!url.isValid()) {
|
||||
_loaded = true;
|
||||
|
@ -324,11 +330,333 @@ private:
|
|||
int _maxNumPixels;
|
||||
};
|
||||
|
||||
const uint16_t NetworkTexture::NULL_MIP_LEVEL = std::numeric_limits<uint16_t>::max();
|
||||
void NetworkTexture::makeRequest() {
|
||||
if (!_sourceIsKTX) {
|
||||
Resource::makeRequest();
|
||||
return;
|
||||
}
|
||||
|
||||
// We special-handle ktx requests to run 2 concurrent requests right off the bat
|
||||
PROFILE_ASYNC_BEGIN(resource, "Resource:" + getType(), QString::number(_requestID), { { "url", _url.toString() }, { "activeURL", _activeUrl.toString() } });
|
||||
|
||||
if (_ktxResourceState == PENDING_INITIAL_LOAD) {
|
||||
_ktxResourceState = LOADING_INITIAL_DATA;
|
||||
|
||||
// Add a fragment to the base url so we can identify the section of the ktx being requested when debugging
|
||||
// The actual requested url is _activeUrl and will not contain the fragment
|
||||
_url.setFragment("head");
|
||||
_ktxHeaderRequest = ResourceManager::createResourceRequest(this, _activeUrl);
|
||||
|
||||
if (!_ktxHeaderRequest) {
|
||||
qCDebug(networking).noquote() << "Failed to get request for" << _url.toDisplayString();
|
||||
|
||||
PROFILE_ASYNC_END(resource, "Resource:" + getType(), QString::number(_requestID));
|
||||
return;
|
||||
}
|
||||
|
||||
ByteRange range;
|
||||
range.fromInclusive = 0;
|
||||
range.toExclusive = 1000;
|
||||
_ktxHeaderRequest->setByteRange(range);
|
||||
|
||||
emit loading();
|
||||
|
||||
connect(_ktxHeaderRequest, &ResourceRequest::progress, this, &NetworkTexture::ktxHeaderRequestProgress);
|
||||
connect(_ktxHeaderRequest, &ResourceRequest::finished, this, &NetworkTexture::ktxHeaderRequestFinished);
|
||||
|
||||
_bytesReceived = _bytesTotal = _bytes = 0;
|
||||
|
||||
_ktxHeaderRequest->send();
|
||||
|
||||
startMipRangeRequest(NULL_MIP_LEVEL, NULL_MIP_LEVEL);
|
||||
} else if (_ktxResourceState == PENDING_MIP_REQUEST) {
|
||||
if (_lowestKnownPopulatedMip > 0) {
|
||||
_ktxResourceState = REQUESTING_MIP;
|
||||
|
||||
// Add a fragment to the base url so we can identify the section of the ktx being requested when debugging
|
||||
// The actual requested url is _activeUrl and will not contain the fragment
|
||||
uint16_t nextMip = _lowestKnownPopulatedMip - 1;
|
||||
_url.setFragment(QString::number(nextMip));
|
||||
startMipRangeRequest(nextMip, nextMip);
|
||||
}
|
||||
} else {
|
||||
qWarning(networking) << "NetworkTexture::makeRequest() called while not in a valid state: " << _ktxResourceState;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void NetworkTexture::startRequestForNextMipLevel() {
|
||||
if (_lowestKnownPopulatedMip == 0) {
|
||||
qWarning(networking) << "Requesting next mip level but all have been fulfilled: " << _lowestKnownPopulatedMip
|
||||
<< " " << _textureSource->getGPUTexture()->minAvailableMipLevel() << " " << _url;
|
||||
return;
|
||||
}
|
||||
|
||||
if (_ktxResourceState == WAITING_FOR_MIP_REQUEST) {
|
||||
_ktxResourceState = PENDING_MIP_REQUEST;
|
||||
|
||||
init();
|
||||
setLoadPriority(this, -static_cast<int>(_originalKtxDescriptor->header.numberOfMipmapLevels) + _lowestKnownPopulatedMip);
|
||||
_url.setFragment(QString::number(_lowestKnownPopulatedMip - 1));
|
||||
TextureCache::attemptRequest(_self);
|
||||
}
|
||||
}
|
||||
|
||||
// Load mips in the range [low, high] (inclusive)
|
||||
void NetworkTexture::startMipRangeRequest(uint16_t low, uint16_t high) {
|
||||
if (_ktxMipRequest) {
|
||||
return;
|
||||
}
|
||||
|
||||
bool isHighMipRequest = low == NULL_MIP_LEVEL && high == NULL_MIP_LEVEL;
|
||||
|
||||
_ktxMipRequest = ResourceManager::createResourceRequest(this, _activeUrl);
|
||||
|
||||
if (!_ktxMipRequest) {
|
||||
qCWarning(networking).noquote() << "Failed to get request for" << _url.toDisplayString();
|
||||
|
||||
PROFILE_ASYNC_END(resource, "Resource:" + getType(), QString::number(_requestID));
|
||||
return;
|
||||
}
|
||||
|
||||
_ktxMipLevelRangeInFlight = { low, high };
|
||||
if (isHighMipRequest) {
|
||||
static const int HIGH_MIP_MAX_SIZE = 5516;
|
||||
// This is a special case where we load the high 7 mips
|
||||
ByteRange range;
|
||||
range.fromInclusive = -HIGH_MIP_MAX_SIZE;
|
||||
_ktxMipRequest->setByteRange(range);
|
||||
} else {
|
||||
ByteRange range;
|
||||
range.fromInclusive = ktx::KTX_HEADER_SIZE + _originalKtxDescriptor->header.bytesOfKeyValueData
|
||||
+ _originalKtxDescriptor->images[low]._imageOffset + ktx::IMAGE_SIZE_WIDTH;
|
||||
range.toExclusive = ktx::KTX_HEADER_SIZE + _originalKtxDescriptor->header.bytesOfKeyValueData
|
||||
+ _originalKtxDescriptor->images[high + 1]._imageOffset;
|
||||
_ktxMipRequest->setByteRange(range);
|
||||
}
|
||||
|
||||
connect(_ktxMipRequest, &ResourceRequest::progress, this, &NetworkTexture::ktxMipRequestProgress);
|
||||
connect(_ktxMipRequest, &ResourceRequest::finished, this, &NetworkTexture::ktxMipRequestFinished);
|
||||
|
||||
_ktxMipRequest->send();
|
||||
}
|
||||
|
||||
|
||||
void NetworkTexture::ktxHeaderRequestFinished() {
|
||||
Q_ASSERT(_ktxResourceState == LOADING_INITIAL_DATA);
|
||||
|
||||
_ktxHeaderRequestFinished = true;
|
||||
maybeHandleFinishedInitialLoad();
|
||||
}
|
||||
|
||||
void NetworkTexture::ktxMipRequestFinished() {
|
||||
Q_ASSERT(_ktxResourceState == LOADING_INITIAL_DATA || _ktxResourceState == REQUESTING_MIP);
|
||||
|
||||
if (_ktxResourceState == LOADING_INITIAL_DATA) {
|
||||
_ktxHighMipRequestFinished = true;
|
||||
maybeHandleFinishedInitialLoad();
|
||||
} else if (_ktxResourceState == REQUESTING_MIP) {
|
||||
Q_ASSERT(_ktxMipLevelRangeInFlight.first != NULL_MIP_LEVEL);
|
||||
TextureCache::requestCompleted(_self);
|
||||
|
||||
if (_ktxMipRequest->getResult() == ResourceRequest::Success) {
|
||||
Q_ASSERT(_ktxMipLevelRangeInFlight.second - _ktxMipLevelRangeInFlight.first == 0);
|
||||
|
||||
auto texture = _textureSource->getGPUTexture();
|
||||
if (texture) {
|
||||
texture->assignStoredMip(_ktxMipLevelRangeInFlight.first,
|
||||
_ktxMipRequest->getData().size(), reinterpret_cast<uint8_t*>(_ktxMipRequest->getData().data()));
|
||||
_lowestKnownPopulatedMip = _textureSource->getGPUTexture()->minAvailableMipLevel();
|
||||
}
|
||||
else {
|
||||
qWarning(networking) << "Trying to update mips but texture is null";
|
||||
}
|
||||
finishedLoading(true);
|
||||
_ktxResourceState = WAITING_FOR_MIP_REQUEST;
|
||||
}
|
||||
else {
|
||||
finishedLoading(false);
|
||||
if (handleFailedRequest(_ktxMipRequest->getResult())) {
|
||||
_ktxResourceState = PENDING_MIP_REQUEST;
|
||||
}
|
||||
else {
|
||||
qWarning(networking) << "Failed to load mip: " << _url;
|
||||
_ktxResourceState = FAILED_TO_LOAD;
|
||||
}
|
||||
}
|
||||
|
||||
_ktxMipRequest->deleteLater();
|
||||
_ktxMipRequest = nullptr;
|
||||
|
||||
if (_ktxResourceState == WAITING_FOR_MIP_REQUEST && _lowestRequestedMipLevel < _lowestKnownPopulatedMip) {
|
||||
startRequestForNextMipLevel();
|
||||
}
|
||||
}
|
||||
else {
|
||||
qWarning() << "Mip request finished in an unexpected state: " << _ktxResourceState;
|
||||
}
|
||||
}
|
||||
|
||||
// This is called when the header or top mips have been loaded
|
||||
void NetworkTexture::maybeHandleFinishedInitialLoad() {
|
||||
Q_ASSERT(_ktxResourceState == LOADING_INITIAL_DATA);
|
||||
|
||||
if (_ktxHeaderRequestFinished && _ktxHighMipRequestFinished) {
|
||||
|
||||
TextureCache::requestCompleted(_self);
|
||||
|
||||
if (_ktxHeaderRequest->getResult() != ResourceRequest::Success || _ktxMipRequest->getResult() != ResourceRequest::Success) {
|
||||
if (handleFailedRequest(_ktxMipRequest->getResult())) {
|
||||
_ktxResourceState = PENDING_INITIAL_LOAD;
|
||||
}
|
||||
else {
|
||||
_ktxResourceState = FAILED_TO_LOAD;
|
||||
}
|
||||
|
||||
_ktxHeaderRequest->deleteLater();
|
||||
_ktxHeaderRequest = nullptr;
|
||||
_ktxMipRequest->deleteLater();
|
||||
_ktxMipRequest = nullptr;
|
||||
} else {
|
||||
// create ktx...
|
||||
auto ktxHeaderData = _ktxHeaderRequest->getData();
|
||||
auto ktxHighMipData = _ktxMipRequest->getData();
|
||||
|
||||
auto header = reinterpret_cast<const ktx::Header*>(ktxHeaderData.data());
|
||||
|
||||
if (!ktx::checkIdentifier(header->identifier)) {
|
||||
qWarning() << "Cannot load " << _url << ", invalid header identifier";
|
||||
_ktxResourceState = FAILED_TO_LOAD;
|
||||
finishedLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
auto kvSize = header->bytesOfKeyValueData;
|
||||
if (kvSize > (ktxHeaderData.size() - ktx::KTX_HEADER_SIZE)) {
|
||||
qWarning() << "Cannot load " << _url << ", did not receive all kv data with initial request";
|
||||
_ktxResourceState = FAILED_TO_LOAD;
|
||||
finishedLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
auto keyValues = ktx::KTX::parseKeyValues(header->bytesOfKeyValueData, reinterpret_cast<const ktx::Byte*>(ktxHeaderData.data()) + ktx::KTX_HEADER_SIZE);
|
||||
|
||||
auto imageDescriptors = header->generateImageDescriptors();
|
||||
if (imageDescriptors.size() == 0) {
|
||||
qWarning(networking) << "Failed to process ktx file " << _url;
|
||||
_ktxResourceState = FAILED_TO_LOAD;
|
||||
finishedLoading(false);
|
||||
}
|
||||
_originalKtxDescriptor.reset(new ktx::KTXDescriptor(*header, keyValues, imageDescriptors));
|
||||
|
||||
// Create bare ktx in memory
|
||||
auto found = std::find_if(keyValues.begin(), keyValues.end(), [](const ktx::KeyValue& val) -> bool {
|
||||
return val._key.compare(gpu::SOURCE_HASH_KEY) == 0;
|
||||
});
|
||||
std::string filename;
|
||||
std::string hash;
|
||||
if (found == keyValues.end() || found->_value.size() != gpu::SOURCE_HASH_BYTES) {
|
||||
qWarning("Invalid source hash key found, bailing");
|
||||
_ktxResourceState = FAILED_TO_LOAD;
|
||||
finishedLoading(false);
|
||||
return;
|
||||
} else {
|
||||
// at this point the source hash is in binary 16-byte form
|
||||
// and we need it in a hexadecimal string
|
||||
auto binaryHash = QByteArray(reinterpret_cast<char*>(found->_value.data()), gpu::SOURCE_HASH_BYTES);
|
||||
hash = filename = binaryHash.toHex().toStdString();
|
||||
}
|
||||
|
||||
auto textureCache = DependencyManager::get<TextureCache>();
|
||||
|
||||
gpu::TexturePointer texture = textureCache->getTextureByHash(hash);
|
||||
|
||||
if (!texture) {
|
||||
KTXFilePointer ktxFile = textureCache->_ktxCache.getFile(hash);
|
||||
if (ktxFile) {
|
||||
texture = gpu::Texture::unserialize(ktxFile->getFilepath());
|
||||
if (texture) {
|
||||
texture = textureCache->cacheTextureByHash(hash, texture);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!texture) {
|
||||
|
||||
auto memKtx = ktx::KTX::createBare(*header, keyValues);
|
||||
if (!memKtx) {
|
||||
qWarning() << " Ktx could not be created, bailing";
|
||||
finishedLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Move ktx to file
|
||||
const char* data = reinterpret_cast<const char*>(memKtx->_storage->data());
|
||||
size_t length = memKtx->_storage->size();
|
||||
KTXFilePointer file;
|
||||
auto& ktxCache = textureCache->_ktxCache;
|
||||
if (!memKtx || !(file = ktxCache.writeFile(data, KTXCache::Metadata(filename, length)))) {
|
||||
qCWarning(modelnetworking) << _url << " failed to write cache file";
|
||||
_ktxResourceState = FAILED_TO_LOAD;
|
||||
finishedLoading(false);
|
||||
return;
|
||||
} else {
|
||||
_file = file;
|
||||
}
|
||||
|
||||
auto newKtxDescriptor = memKtx->toDescriptor();
|
||||
|
||||
texture = gpu::Texture::unserialize(_file->getFilepath(), newKtxDescriptor);
|
||||
texture->setKtxBacking(file->getFilepath());
|
||||
texture->setSource(filename);
|
||||
|
||||
auto& images = _originalKtxDescriptor->images;
|
||||
size_t imageSizeRemaining = ktxHighMipData.size();
|
||||
uint8_t* ktxData = reinterpret_cast<uint8_t*>(ktxHighMipData.data());
|
||||
ktxData += ktxHighMipData.size();
|
||||
// TODO Move image offset calculation to ktx ImageDescriptor
|
||||
for (int level = static_cast<int>(images.size()) - 1; level >= 0; --level) {
|
||||
auto& image = images[level];
|
||||
if (image._imageSize > imageSizeRemaining) {
|
||||
break;
|
||||
}
|
||||
ktxData -= image._imageSize;
|
||||
texture->assignStoredMip(static_cast<gpu::uint16>(level), image._imageSize, ktxData);
|
||||
ktxData -= ktx::IMAGE_SIZE_WIDTH;
|
||||
imageSizeRemaining -= (image._imageSize + ktx::IMAGE_SIZE_WIDTH);
|
||||
}
|
||||
|
||||
// We replace the texture with the one stored in the cache. This deals with the possible race condition of two different
|
||||
// images with the same hash being loaded concurrently. Only one of them will make it into the cache by hash first and will
|
||||
// be the winner
|
||||
texture = textureCache->cacheTextureByHash(filename, texture);
|
||||
}
|
||||
|
||||
_lowestKnownPopulatedMip = texture->minAvailableMipLevel();
|
||||
|
||||
_ktxResourceState = WAITING_FOR_MIP_REQUEST;
|
||||
setImage(texture, header->getPixelWidth(), header->getPixelHeight());
|
||||
|
||||
_ktxHeaderRequest->deleteLater();
|
||||
_ktxHeaderRequest = nullptr;
|
||||
_ktxMipRequest->deleteLater();
|
||||
_ktxMipRequest = nullptr;
|
||||
}
|
||||
startRequestForNextMipLevel();
|
||||
}
|
||||
}
|
||||
|
||||
void NetworkTexture::downloadFinished(const QByteArray& data) {
|
||||
loadContent(data);
|
||||
}
|
||||
|
||||
void NetworkTexture::loadContent(const QByteArray& content) {
|
||||
if (_sourceIsKTX) {
|
||||
assert(false);
|
||||
return;
|
||||
}
|
||||
|
||||
QThreadPool::globalInstance()->start(new ImageReader(_self, _url, content, _maxNumPixels));
|
||||
}
|
||||
|
||||
|
@ -451,6 +779,7 @@ void ImageReader::read() {
|
|||
if (texture && textureCache) {
|
||||
auto memKtx = gpu::Texture::serialize(*texture);
|
||||
|
||||
// Move the texture into a memory mapped file
|
||||
if (memKtx) {
|
||||
const char* data = reinterpret_cast<const char*>(memKtx->_storage->data());
|
||||
size_t length = memKtx->_storage->size();
|
||||
|
|
|
@ -23,6 +23,7 @@
|
|||
#include <ResourceCache.h>
|
||||
#include <model/TextureMap.h>
|
||||
#include <image/Image.h>
|
||||
#include <ktx/KTX.h>
|
||||
|
||||
#include "KTXCache.h"
|
||||
|
||||
|
@ -59,7 +60,16 @@ public:
|
|||
signals:
|
||||
void networkTextureCreated(const QWeakPointer<NetworkTexture>& self);
|
||||
|
||||
public slots:
|
||||
void ktxHeaderRequestProgress(uint64_t bytesReceived, uint64_t bytesTotal) { }
|
||||
void ktxHeaderRequestFinished();
|
||||
|
||||
void ktxMipRequestProgress(uint64_t bytesReceived, uint64_t bytesTotal) { }
|
||||
void ktxMipRequestFinished();
|
||||
|
||||
protected:
|
||||
void makeRequest() override;
|
||||
|
||||
virtual bool isCacheable() const override { return _loaded; }
|
||||
|
||||
virtual void downloadFinished(const QByteArray& data) override;
|
||||
|
@ -67,12 +77,51 @@ protected:
|
|||
Q_INVOKABLE void loadContent(const QByteArray& content);
|
||||
Q_INVOKABLE void setImage(gpu::TexturePointer texture, int originalWidth, int originalHeight);
|
||||
|
||||
void startRequestForNextMipLevel();
|
||||
|
||||
void startMipRangeRequest(uint16_t low, uint16_t high);
|
||||
void maybeHandleFinishedInitialLoad();
|
||||
|
||||
private:
|
||||
friend class KTXReader;
|
||||
friend class ImageReader;
|
||||
|
||||
image::TextureUsage::Type _type;
|
||||
|
||||
static const uint16_t NULL_MIP_LEVEL;
|
||||
enum KTXResourceState {
|
||||
PENDING_INITIAL_LOAD = 0,
|
||||
LOADING_INITIAL_DATA, // Loading KTX Header + Low Resolution Mips
|
||||
WAITING_FOR_MIP_REQUEST, // Waiting for the gpu layer to report that it needs higher resolution mips
|
||||
PENDING_MIP_REQUEST, // We have added ourselves to the ResourceCache queue
|
||||
REQUESTING_MIP, // We have a mip in flight
|
||||
FAILED_TO_LOAD
|
||||
};
|
||||
|
||||
bool _sourceIsKTX { false };
|
||||
KTXResourceState _ktxResourceState { PENDING_INITIAL_LOAD };
|
||||
|
||||
// TODO Can this be removed?
|
||||
KTXFilePointer _file;
|
||||
|
||||
// The current mips that are currently being requested w/ _ktxMipRequest
|
||||
std::pair<uint16_t, uint16_t> _ktxMipLevelRangeInFlight{ NULL_MIP_LEVEL, NULL_MIP_LEVEL };
|
||||
|
||||
ResourceRequest* _ktxHeaderRequest { nullptr };
|
||||
ResourceRequest* _ktxMipRequest { nullptr };
|
||||
bool _ktxHeaderRequestFinished{ false };
|
||||
bool _ktxHighMipRequestFinished{ false };
|
||||
|
||||
uint16_t _lowestRequestedMipLevel { NULL_MIP_LEVEL };
|
||||
uint16_t _lowestKnownPopulatedMip { NULL_MIP_LEVEL };
|
||||
|
||||
// This is a copy of the original KTX descriptor from the source url.
|
||||
// We need this because the KTX that will be cached will likely include extra data
|
||||
// in its key/value data, and so will not match up with the original, causing
|
||||
// mip offsets to change.
|
||||
ktx::KTXDescriptorPointer _originalKtxDescriptor;
|
||||
|
||||
|
||||
int _originalWidth { 0 };
|
||||
int _originalHeight { 0 };
|
||||
int _width { 0 };
|
||||
|
|
|
@ -67,7 +67,6 @@ void AssetClient::init() {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
void AssetClient::cacheInfoRequest(QObject* reciever, QString slot) {
|
||||
if (QThread::currentThread() != thread()) {
|
||||
QMetaObject::invokeMethod(this, "cacheInfoRequest", Qt::QueuedConnection,
|
||||
|
@ -182,8 +181,8 @@ RenameMappingRequest* AssetClient::createRenameMappingRequest(const AssetPath& o
|
|||
return request;
|
||||
}
|
||||
|
||||
AssetRequest* AssetClient::createRequest(const AssetHash& hash) {
|
||||
auto request = new AssetRequest(hash);
|
||||
AssetRequest* AssetClient::createRequest(const AssetHash& hash, const ByteRange& byteRange) {
|
||||
auto request = new AssetRequest(hash, byteRange);
|
||||
|
||||
// Move to the AssetClient thread in case we are not currently on that thread (which will usually be the case)
|
||||
request->moveToThread(thread());
|
||||
|
|
|
@ -21,6 +21,7 @@
|
|||
#include <DependencyManager.h>
|
||||
|
||||
#include "AssetUtils.h"
|
||||
#include "ByteRange.h"
|
||||
#include "ClientServerUtils.h"
|
||||
#include "LimitedNodeList.h"
|
||||
#include "Node.h"
|
||||
|
@ -55,7 +56,7 @@ public:
|
|||
Q_INVOKABLE DeleteMappingsRequest* createDeleteMappingsRequest(const AssetPathList& paths);
|
||||
Q_INVOKABLE SetMappingRequest* createSetMappingRequest(const AssetPath& path, const AssetHash& hash);
|
||||
Q_INVOKABLE RenameMappingRequest* createRenameMappingRequest(const AssetPath& oldPath, const AssetPath& newPath);
|
||||
Q_INVOKABLE AssetRequest* createRequest(const AssetHash& hash);
|
||||
Q_INVOKABLE AssetRequest* createRequest(const AssetHash& hash, const ByteRange& byteRange = ByteRange());
|
||||
Q_INVOKABLE AssetUpload* createUpload(const QString& filename);
|
||||
Q_INVOKABLE AssetUpload* createUpload(const QByteArray& data);
|
||||
|
||||
|
|
|
@ -23,10 +23,12 @@
|
|||
|
||||
static int requestID = 0;
|
||||
|
||||
AssetRequest::AssetRequest(const QString& hash) :
|
||||
AssetRequest::AssetRequest(const QString& hash, const ByteRange& byteRange) :
|
||||
_requestID(++requestID),
|
||||
_hash(hash)
|
||||
_hash(hash),
|
||||
_byteRange(byteRange)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
AssetRequest::~AssetRequest() {
|
||||
|
@ -34,9 +36,6 @@ AssetRequest::~AssetRequest() {
|
|||
if (_assetRequestID) {
|
||||
assetClient->cancelGetAssetRequest(_assetRequestID);
|
||||
}
|
||||
if (_assetInfoRequestID) {
|
||||
assetClient->cancelGetAssetInfoRequest(_assetInfoRequestID);
|
||||
}
|
||||
}
|
||||
|
||||
void AssetRequest::start() {
|
||||
|
@ -62,108 +61,74 @@ void AssetRequest::start() {
|
|||
// Try to load from cache
|
||||
_data = loadFromCache(getUrl());
|
||||
if (!_data.isNull()) {
|
||||
_info.hash = _hash;
|
||||
_info.size = _data.size();
|
||||
_error = NoError;
|
||||
|
||||
_state = Finished;
|
||||
emit finished(this);
|
||||
return;
|
||||
}
|
||||
|
||||
_state = WaitingForInfo;
|
||||
|
||||
|
||||
_state = WaitingForData;
|
||||
|
||||
auto assetClient = DependencyManager::get<AssetClient>();
|
||||
_assetInfoRequestID = assetClient->getAssetInfo(_hash,
|
||||
[this](bool responseReceived, AssetServerError serverError, AssetInfo info) {
|
||||
auto that = QPointer<AssetRequest>(this); // Used to track the request's lifetime
|
||||
auto hash = _hash;
|
||||
|
||||
_assetInfoRequestID = INVALID_MESSAGE_ID;
|
||||
_assetRequestID = assetClient->getAsset(_hash, _byteRange.fromInclusive, _byteRange.toExclusive,
|
||||
[this, that, hash](bool responseReceived, AssetServerError serverError, const QByteArray& data) {
|
||||
|
||||
_info = info;
|
||||
if (!that) {
|
||||
qCWarning(asset_client) << "Got reply for dead asset request " << hash << "- error code" << _error;
|
||||
// If the request is dead, return
|
||||
return;
|
||||
}
|
||||
_assetRequestID = INVALID_MESSAGE_ID;
|
||||
|
||||
if (!responseReceived) {
|
||||
_error = NetworkError;
|
||||
} else if (serverError != AssetServerError::NoError) {
|
||||
switch(serverError) {
|
||||
switch (serverError) {
|
||||
case AssetServerError::AssetNotFound:
|
||||
_error = NotFound;
|
||||
break;
|
||||
case AssetServerError::InvalidByteRange:
|
||||
_error = InvalidByteRange;
|
||||
break;
|
||||
default:
|
||||
_error = UnknownError;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (_byteRange.isSet()) {
|
||||
// we had a byte range, the size of the data does not match what we expect, so we return an error
|
||||
if (data.size() != _byteRange.size()) {
|
||||
_error = SizeVerificationFailed;
|
||||
}
|
||||
} else if (hashData(data).toHex() != _hash) {
|
||||
// the hash of the received data does not match what we expect, so we return an error
|
||||
_error = HashVerificationFailed;
|
||||
}
|
||||
|
||||
if (_error == NoError) {
|
||||
_data = data;
|
||||
_totalReceived += data.size();
|
||||
emit progress(_totalReceived, data.size());
|
||||
|
||||
saveToCache(getUrl(), data);
|
||||
}
|
||||
}
|
||||
|
||||
if (_error != NoError) {
|
||||
qCWarning(asset_client) << "Got error retrieving asset info for" << _hash;
|
||||
_state = Finished;
|
||||
emit finished(this);
|
||||
|
||||
qCWarning(asset_client) << "Got error retrieving asset" << _hash << "- error code" << _error;
|
||||
}
|
||||
|
||||
_state = Finished;
|
||||
emit finished(this);
|
||||
}, [this, that](qint64 totalReceived, qint64 total) {
|
||||
if (!that) {
|
||||
// If the request is dead, return
|
||||
return;
|
||||
}
|
||||
|
||||
_state = WaitingForData;
|
||||
_data.resize(info.size);
|
||||
|
||||
qCDebug(asset_client) << "Got size of " << _hash << " : " << info.size << " bytes";
|
||||
|
||||
int start = 0, end = _info.size;
|
||||
|
||||
auto assetClient = DependencyManager::get<AssetClient>();
|
||||
auto that = QPointer<AssetRequest>(this); // Used to track the request's lifetime
|
||||
auto hash = _hash;
|
||||
_assetRequestID = assetClient->getAsset(_hash, start, end,
|
||||
[this, that, hash, start, end](bool responseReceived, AssetServerError serverError, const QByteArray& data) {
|
||||
if (!that) {
|
||||
qCWarning(asset_client) << "Got reply for dead asset request " << hash << "- error code" << _error;
|
||||
// If the request is dead, return
|
||||
return;
|
||||
}
|
||||
_assetRequestID = INVALID_MESSAGE_ID;
|
||||
|
||||
if (!responseReceived) {
|
||||
_error = NetworkError;
|
||||
} else if (serverError != AssetServerError::NoError) {
|
||||
switch (serverError) {
|
||||
case AssetServerError::AssetNotFound:
|
||||
_error = NotFound;
|
||||
break;
|
||||
case AssetServerError::InvalidByteRange:
|
||||
_error = InvalidByteRange;
|
||||
break;
|
||||
default:
|
||||
_error = UnknownError;
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
Q_ASSERT(data.size() == (end - start));
|
||||
|
||||
// we need to check the hash of the received data to make sure it matches what we expect
|
||||
if (hashData(data).toHex() == _hash) {
|
||||
memcpy(_data.data() + start, data.constData(), data.size());
|
||||
_totalReceived += data.size();
|
||||
emit progress(_totalReceived, _info.size);
|
||||
|
||||
saveToCache(getUrl(), data);
|
||||
} else {
|
||||
// hash doesn't match - we have an error
|
||||
_error = HashVerificationFailed;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if (_error != NoError) {
|
||||
qCWarning(asset_client) << "Got error retrieving asset" << _hash << "- error code" << _error;
|
||||
}
|
||||
|
||||
_state = Finished;
|
||||
emit finished(this);
|
||||
}, [this, that](qint64 totalReceived, qint64 total) {
|
||||
if (!that) {
|
||||
// If the request is dead, return
|
||||
return;
|
||||
}
|
||||
emit progress(totalReceived, total);
|
||||
});
|
||||
emit progress(totalReceived, total);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -17,15 +17,15 @@
|
|||
#include <QString>
|
||||
|
||||
#include "AssetClient.h"
|
||||
|
||||
#include "AssetUtils.h"
|
||||
|
||||
#include "ByteRange.h"
|
||||
|
||||
class AssetRequest : public QObject {
|
||||
Q_OBJECT
|
||||
public:
|
||||
enum State {
|
||||
NotStarted = 0,
|
||||
WaitingForInfo,
|
||||
WaitingForData,
|
||||
Finished
|
||||
};
|
||||
|
@ -36,11 +36,12 @@ public:
|
|||
InvalidByteRange,
|
||||
InvalidHash,
|
||||
HashVerificationFailed,
|
||||
SizeVerificationFailed,
|
||||
NetworkError,
|
||||
UnknownError
|
||||
};
|
||||
|
||||
AssetRequest(const QString& hash);
|
||||
AssetRequest(const QString& hash, const ByteRange& byteRange = ByteRange());
|
||||
virtual ~AssetRequest() override;
|
||||
|
||||
Q_INVOKABLE void start();
|
||||
|
@ -59,13 +60,12 @@ private:
|
|||
int _requestID;
|
||||
State _state = NotStarted;
|
||||
Error _error = NoError;
|
||||
AssetInfo _info;
|
||||
uint64_t _totalReceived { 0 };
|
||||
QString _hash;
|
||||
QByteArray _data;
|
||||
int _numPendingRequests { 0 };
|
||||
MessageID _assetRequestID { INVALID_MESSAGE_ID };
|
||||
MessageID _assetInfoRequestID { INVALID_MESSAGE_ID };
|
||||
const ByteRange _byteRange;
|
||||
};
|
||||
|
||||
#endif
|
||||
|
|
|
@ -114,7 +114,7 @@ void AssetResourceRequest::requestMappingForPath(const AssetPath& path) {
|
|||
void AssetResourceRequest::requestHash(const AssetHash& hash) {
|
||||
// Make request to atp
|
||||
auto assetClient = DependencyManager::get<AssetClient>();
|
||||
_assetRequest = assetClient->createRequest(hash);
|
||||
_assetRequest = assetClient->createRequest(hash, _byteRange);
|
||||
|
||||
connect(_assetRequest, &AssetRequest::progress, this, &AssetResourceRequest::onDownloadProgress);
|
||||
connect(_assetRequest, &AssetRequest::finished, this, [this](AssetRequest* req) {
|
||||
|
|
53
libraries/networking/src/ByteRange.h
Normal file
53
libraries/networking/src/ByteRange.h
Normal file
|
@ -0,0 +1,53 @@
|
|||
//
|
||||
// ByteRange.h
|
||||
// libraries/networking/src
|
||||
//
|
||||
// Created by Stephen Birarda on 4/17/17.
|
||||
// Copyright 2017 High Fidelity, Inc.
|
||||
//
|
||||
// Distributed under the Apache License, Version 2.0.
|
||||
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
|
||||
//
|
||||
|
||||
#ifndef hifi_ByteRange_h
|
||||
#define hifi_ByteRange_h
|
||||
|
||||
struct ByteRange {
|
||||
int64_t fromInclusive { 0 };
|
||||
int64_t toExclusive { 0 };
|
||||
|
||||
bool isSet() const { return fromInclusive < 0 || fromInclusive < toExclusive; }
|
||||
int64_t size() const { return toExclusive - fromInclusive; }
|
||||
|
||||
// byte ranges are invalid if:
|
||||
// (1) the toExclusive of the range is negative
|
||||
// (2) the toExclusive of the range is less than the fromInclusive, and isn't zero
|
||||
// (3) the fromExclusive of the range is negative, and the toExclusive isn't zero
|
||||
bool isValid() {
|
||||
return toExclusive >= 0
|
||||
&& (toExclusive >= fromInclusive || toExclusive == 0)
|
||||
&& (fromInclusive >= 0 || toExclusive == 0);
|
||||
}
|
||||
|
||||
void fixupRange(int64_t fileSize) {
|
||||
if (!isSet()) {
|
||||
// if the byte range is not set, force it to be from 0 to the end of the file
|
||||
fromInclusive = 0;
|
||||
toExclusive = fileSize;
|
||||
}
|
||||
|
||||
if (fromInclusive > 0 && toExclusive == 0) {
|
||||
// we have a left side of the range that is non-zero
|
||||
// if the RHS of the range is zero, set it to the end of the file now
|
||||
toExclusive = fileSize;
|
||||
} else if (-fromInclusive >= fileSize) {
|
||||
// we have a negative range that is equal or greater than the full size of the file
|
||||
// so we just set this to be a range across the entire file, from 0
|
||||
fromInclusive = 0;
|
||||
toExclusive = fileSize;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
#endif // hifi_ByteRange_h
|
|
@ -11,6 +11,8 @@
|
|||
|
||||
#include "FileResourceRequest.h"
|
||||
|
||||
#include <cstdlib>
|
||||
|
||||
#include <QFile>
|
||||
|
||||
void FileResourceRequest::doSend() {
|
||||
|
@ -21,17 +23,39 @@ void FileResourceRequest::doSend() {
|
|||
if (filename.isEmpty()) {
|
||||
filename = _url.toString();
|
||||
}
|
||||
|
||||
QFile file(filename);
|
||||
if (file.exists()) {
|
||||
if (file.open(QFile::ReadOnly)) {
|
||||
_data = file.readAll();
|
||||
_result = ResourceRequest::Success;
|
||||
} else {
|
||||
_result = ResourceRequest::AccessDenied;
|
||||
}
|
||||
|
||||
if (!_byteRange.isValid()) {
|
||||
_result = ResourceRequest::InvalidByteRange;
|
||||
} else {
|
||||
_result = ResourceRequest::NotFound;
|
||||
QFile file(filename);
|
||||
if (file.exists()) {
|
||||
if (file.open(QFile::ReadOnly)) {
|
||||
|
||||
if (file.size() < _byteRange.fromInclusive || file.size() < _byteRange.toExclusive) {
|
||||
_result = ResourceRequest::InvalidByteRange;
|
||||
} else {
|
||||
// fix it up based on the known size of the file
|
||||
_byteRange.fixupRange(file.size());
|
||||
|
||||
if (_byteRange.fromInclusive >= 0) {
|
||||
// this is a positive byte range, simply skip to that part of the file and read from there
|
||||
file.seek(_byteRange.fromInclusive);
|
||||
_data = file.read(_byteRange.size());
|
||||
} else {
|
||||
// this is a negative byte range, we'll need to grab data from the end of the file first
|
||||
file.seek(file.size() + _byteRange.fromInclusive);
|
||||
_data = file.read(_byteRange.size());
|
||||
}
|
||||
|
||||
_result = ResourceRequest::Success;
|
||||
}
|
||||
|
||||
} else {
|
||||
_result = ResourceRequest::AccessDenied;
|
||||
}
|
||||
} else {
|
||||
_result = ResourceRequest::NotFound;
|
||||
}
|
||||
}
|
||||
|
||||
_state = Finished;
|
||||
|
|
|
@ -59,6 +59,18 @@ void HTTPResourceRequest::doSend() {
|
|||
networkRequest.setAttribute(QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::AlwaysNetwork);
|
||||
}
|
||||
|
||||
if (_byteRange.isSet()) {
|
||||
QString byteRange;
|
||||
if (_byteRange.fromInclusive < 0) {
|
||||
byteRange = QString("bytes=%1").arg(_byteRange.fromInclusive);
|
||||
} else {
|
||||
// HTTP byte ranges are inclusive on the `to` end: [from, to]
|
||||
byteRange = QString("bytes=%1-%2").arg(_byteRange.fromInclusive).arg(_byteRange.toExclusive - 1);
|
||||
}
|
||||
networkRequest.setRawHeader("Range", byteRange.toLatin1());
|
||||
}
|
||||
networkRequest.setAttribute(QNetworkRequest::HttpPipeliningAllowedAttribute, false);
|
||||
|
||||
_reply = NetworkAccessManager::getInstance().get(networkRequest);
|
||||
|
||||
connect(_reply, &QNetworkReply::finished, this, &HTTPResourceRequest::onRequestFinished);
|
||||
|
@ -72,12 +84,60 @@ void HTTPResourceRequest::onRequestFinished() {
|
|||
Q_ASSERT(_reply);
|
||||
|
||||
cleanupTimer();
|
||||
|
||||
|
||||
// Content-Range headers have the form:
|
||||
//
|
||||
// Content-Range: <unit> <range-start>-<range-end>/<size>
|
||||
// Content-Range: <unit> <range-start>-<range-end>/*
|
||||
// Content-Range: <unit> */<size>
|
||||
//
|
||||
auto parseContentRangeHeader = [](QString contentRangeHeader) -> std::pair<bool, uint64_t> {
|
||||
auto unitRangeParts = contentRangeHeader.split(' ');
|
||||
if (unitRangeParts.size() != 2) {
|
||||
return { false, 0 };
|
||||
}
|
||||
|
||||
auto rangeSizeParts = unitRangeParts[1].split('/');
|
||||
if (rangeSizeParts.size() != 2) {
|
||||
return { false, 0 };
|
||||
}
|
||||
|
||||
auto sizeStr = rangeSizeParts[1];
|
||||
if (sizeStr == "*") {
|
||||
return { true, 0 };
|
||||
} else {
|
||||
bool ok;
|
||||
auto size = sizeStr.toLong(&ok);
|
||||
return { ok, size };
|
||||
}
|
||||
};
|
||||
|
||||
switch(_reply->error()) {
|
||||
case QNetworkReply::NoError:
|
||||
_data = _reply->readAll();
|
||||
_loadedFromCache = _reply->attribute(QNetworkRequest::SourceIsFromCacheAttribute).toBool();
|
||||
_result = Success;
|
||||
|
||||
if (_byteRange.isSet()) {
|
||||
auto statusCode = _reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
|
||||
if (statusCode == 206) {
|
||||
_rangeRequestSuccessful = true;
|
||||
auto contentRangeHeader = _reply->rawHeader("Content-Range");
|
||||
bool success;
|
||||
uint64_t size;
|
||||
std::tie(success, size) = parseContentRangeHeader(contentRangeHeader);
|
||||
if (success) {
|
||||
_totalSizeOfResource = size;
|
||||
} else {
|
||||
qWarning(networking) << "Error parsing content-range header: " << contentRangeHeader;
|
||||
_totalSizeOfResource = 0;
|
||||
}
|
||||
} else {
|
||||
_rangeRequestSuccessful = false;
|
||||
_totalSizeOfResource = _data.size();
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case QNetworkReply::TimeoutError:
|
||||
|
@ -130,6 +190,7 @@ void HTTPResourceRequest::onDownloadProgress(qint64 bytesReceived, qint64 bytesT
|
|||
}
|
||||
|
||||
void HTTPResourceRequest::onTimeout() {
|
||||
qDebug() << "Timeout: " << _url << ":" << _reply->isFinished();
|
||||
Q_ASSERT(_state == InProgress);
|
||||
_reply->disconnect(this);
|
||||
_reply->abort();
|
||||
|
|
|
@ -13,6 +13,7 @@
|
|||
|
||||
#include "AtpReply.h"
|
||||
#include "NetworkAccessManager.h"
|
||||
#include <QtNetwork/QNetworkProxy>
|
||||
|
||||
QThreadStorage<QNetworkAccessManager*> networkAccessManagers;
|
||||
|
||||
|
|
|
@ -474,8 +474,9 @@ int ResourceCache::getLoadingRequestCount() {
|
|||
|
||||
bool ResourceCache::attemptRequest(QSharedPointer<Resource> resource) {
|
||||
Q_ASSERT(!resource.isNull());
|
||||
auto sharedItems = DependencyManager::get<ResourceCacheSharedItems>();
|
||||
|
||||
|
||||
auto sharedItems = DependencyManager::get<ResourceCacheSharedItems>();
|
||||
if (_requestsActive >= _requestLimit) {
|
||||
// wait until a slot becomes available
|
||||
sharedItems->appendPendingRequest(resource);
|
||||
|
@ -490,6 +491,7 @@ bool ResourceCache::attemptRequest(QSharedPointer<Resource> resource) {
|
|||
|
||||
void ResourceCache::requestCompleted(QWeakPointer<Resource> resource) {
|
||||
auto sharedItems = DependencyManager::get<ResourceCacheSharedItems>();
|
||||
|
||||
sharedItems->removeRequest(resource);
|
||||
--_requestsActive;
|
||||
|
||||
|
@ -553,6 +555,10 @@ void Resource::clearLoadPriority(const QPointer<QObject>& owner) {
|
|||
}
|
||||
|
||||
float Resource::getLoadPriority() {
|
||||
if (_loadPriorities.size() == 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
float highestPriority = -FLT_MAX;
|
||||
for (QHash<QPointer<QObject>, float>::iterator it = _loadPriorities.begin(); it != _loadPriorities.end(); ) {
|
||||
if (it.key().isNull()) {
|
||||
|
@ -637,12 +643,12 @@ void Resource::attemptRequest() {
|
|||
void Resource::finishedLoading(bool success) {
|
||||
if (success) {
|
||||
qCDebug(networking).noquote() << "Finished loading:" << _url.toDisplayString();
|
||||
_loadPriorities.clear();
|
||||
_loaded = true;
|
||||
} else {
|
||||
qCDebug(networking).noquote() << "Failed to load:" << _url.toDisplayString();
|
||||
_failedToLoad = true;
|
||||
}
|
||||
_loadPriorities.clear();
|
||||
emit finished(success);
|
||||
}
|
||||
|
||||
|
@ -676,6 +682,8 @@ void Resource::makeRequest() {
|
|||
return;
|
||||
}
|
||||
|
||||
_request->setByteRange(_requestByteRange);
|
||||
|
||||
qCDebug(resourceLog).noquote() << "Starting request for:" << _url.toDisplayString();
|
||||
emit loading();
|
||||
|
||||
|
@ -722,34 +730,7 @@ void Resource::handleReplyFinished() {
|
|||
emit loaded(data);
|
||||
downloadFinished(data);
|
||||
} else {
|
||||
switch (result) {
|
||||
case ResourceRequest::Result::Timeout: {
|
||||
qCDebug(networking) << "Timed out loading" << _url << "received" << _bytesReceived << "total" << _bytesTotal;
|
||||
// Fall through to other cases
|
||||
}
|
||||
case ResourceRequest::Result::ServerUnavailable: {
|
||||
// retry with increasing delays
|
||||
const int BASE_DELAY_MS = 1000;
|
||||
if (_attempts++ < MAX_ATTEMPTS) {
|
||||
auto waitTime = BASE_DELAY_MS * (int)pow(2.0, _attempts);
|
||||
|
||||
qCDebug(networking).noquote() << "Server unavailable for" << _url << "- may retry in" << waitTime << "ms"
|
||||
<< "if resource is still needed";
|
||||
|
||||
QTimer::singleShot(waitTime, this, &Resource::attemptRequest);
|
||||
break;
|
||||
}
|
||||
// fall through to final failure
|
||||
}
|
||||
default: {
|
||||
qCDebug(networking) << "Error loading " << _url;
|
||||
auto error = (result == ResourceRequest::Timeout) ? QNetworkReply::TimeoutError
|
||||
: QNetworkReply::UnknownNetworkError;
|
||||
emit failed(error);
|
||||
finishedLoading(false);
|
||||
break;
|
||||
}
|
||||
}
|
||||
handleFailedRequest(result);
|
||||
}
|
||||
|
||||
_request->disconnect(this);
|
||||
|
@ -757,6 +738,41 @@ void Resource::handleReplyFinished() {
|
|||
_request = nullptr;
|
||||
}
|
||||
|
||||
bool Resource::handleFailedRequest(ResourceRequest::Result result) {
|
||||
bool willRetry = false;
|
||||
switch (result) {
|
||||
case ResourceRequest::Result::Timeout: {
|
||||
qCDebug(networking) << "Timed out loading" << _url << "received" << _bytesReceived << "total" << _bytesTotal;
|
||||
// Fall through to other cases
|
||||
}
|
||||
case ResourceRequest::Result::ServerUnavailable: {
|
||||
// retry with increasing delays
|
||||
const int BASE_DELAY_MS = 1000;
|
||||
if (_attempts++ < MAX_ATTEMPTS) {
|
||||
auto waitTime = BASE_DELAY_MS * (int)pow(2.0, _attempts);
|
||||
|
||||
qCDebug(networking).noquote() << "Server unavailable for" << _url << "- may retry in" << waitTime << "ms"
|
||||
<< "if resource is still needed";
|
||||
|
||||
QTimer::singleShot(waitTime, this, &Resource::attemptRequest);
|
||||
willRetry = true;
|
||||
break;
|
||||
}
|
||||
// fall through to final failure
|
||||
}
|
||||
default: {
|
||||
qCDebug(networking) << "Error loading " << _url;
|
||||
auto error = (result == ResourceRequest::Timeout) ? QNetworkReply::TimeoutError
|
||||
: QNetworkReply::UnknownNetworkError;
|
||||
emit failed(error);
|
||||
willRetry = false;
|
||||
finishedLoading(false);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return willRetry;
|
||||
}
|
||||
|
||||
uint qHash(const QPointer<QObject>& value, uint seed) {
|
||||
return qHash(value.data(), seed);
|
||||
}
|
||||
|
|
|
@ -424,6 +424,11 @@ protected slots:
|
|||
protected:
|
||||
virtual void init();
|
||||
|
||||
/// Called by ResourceCache to begin loading this Resource.
|
||||
/// This method can be overriden to provide custom request functionality. If this is done,
|
||||
/// downloadFinished and ResourceCache::requestCompleted must be called.
|
||||
virtual void makeRequest();
|
||||
|
||||
/// Checks whether the resource is cacheable.
|
||||
virtual bool isCacheable() const { return true; }
|
||||
|
||||
|
@ -440,16 +445,27 @@ protected:
|
|||
|
||||
Q_INVOKABLE void allReferencesCleared();
|
||||
|
||||
/// Return true if the resource will be retried
|
||||
bool handleFailedRequest(ResourceRequest::Result result);
|
||||
|
||||
QUrl _url;
|
||||
QUrl _activeUrl;
|
||||
ByteRange _requestByteRange;
|
||||
bool _startedLoading = false;
|
||||
bool _failedToLoad = false;
|
||||
bool _loaded = false;
|
||||
QHash<QPointer<QObject>, float> _loadPriorities;
|
||||
QWeakPointer<Resource> _self;
|
||||
QPointer<ResourceCache> _cache;
|
||||
|
||||
private slots:
|
||||
|
||||
qint64 _bytesReceived{ 0 };
|
||||
qint64 _bytesTotal{ 0 };
|
||||
qint64 _bytes{ 0 };
|
||||
|
||||
int _requestID;
|
||||
ResourceRequest* _request{ nullptr };
|
||||
|
||||
public slots:
|
||||
void handleDownloadProgress(uint64_t bytesReceived, uint64_t bytesTotal);
|
||||
void handleReplyFinished();
|
||||
|
||||
|
@ -459,20 +475,14 @@ private:
|
|||
|
||||
void setLRUKey(int lruKey) { _lruKey = lruKey; }
|
||||
|
||||
void makeRequest();
|
||||
void retry();
|
||||
void reinsert();
|
||||
|
||||
bool isInScript() const { return _isInScript; }
|
||||
void setInScript(bool isInScript) { _isInScript = isInScript; }
|
||||
|
||||
int _requestID;
|
||||
ResourceRequest* _request{ nullptr };
|
||||
int _lruKey{ 0 };
|
||||
QTimer* _replyTimer{ nullptr };
|
||||
qint64 _bytesReceived{ 0 };
|
||||
qint64 _bytesTotal{ 0 };
|
||||
qint64 _bytes{ 0 };
|
||||
int _attempts{ 0 };
|
||||
bool _isInScript{ false };
|
||||
};
|
||||
|
|
|
@ -26,6 +26,7 @@ const QString URL_SCHEME_ATP = "atp";
|
|||
|
||||
class ResourceManager {
|
||||
public:
|
||||
|
||||
static void setUrlPrefixOverride(const QString& prefix, const QString& replacement);
|
||||
static QString normalizeURL(const QString& urlString);
|
||||
static QUrl normalizeURL(const QUrl& url);
|
||||
|
|
|
@ -17,6 +17,8 @@
|
|||
|
||||
#include <cstdint>
|
||||
|
||||
#include "ByteRange.h"
|
||||
|
||||
class ResourceRequest : public QObject {
|
||||
Q_OBJECT
|
||||
public:
|
||||
|
@ -35,6 +37,7 @@ public:
|
|||
Timeout,
|
||||
ServerUnavailable,
|
||||
AccessDenied,
|
||||
InvalidByteRange,
|
||||
InvalidURL,
|
||||
NotFound
|
||||
};
|
||||
|
@ -46,8 +49,11 @@ public:
|
|||
QString getResultString() const;
|
||||
QUrl getUrl() const { return _url; }
|
||||
bool loadedFromCache() const { return _loadedFromCache; }
|
||||
bool getRangeRequestSuccessful() const { return _rangeRequestSuccessful; }
|
||||
bool getTotalSizeOfResource() const { return _totalSizeOfResource; }
|
||||
|
||||
void setCacheEnabled(bool value) { _cacheEnabled = value; }
|
||||
void setByteRange(ByteRange byteRange) { _byteRange = byteRange; }
|
||||
|
||||
public slots:
|
||||
void send();
|
||||
|
@ -65,6 +71,9 @@ protected:
|
|||
QByteArray _data;
|
||||
bool _cacheEnabled { true };
|
||||
bool _loadedFromCache { false };
|
||||
ByteRange _byteRange;
|
||||
bool _rangeRequestSuccessful { false };
|
||||
uint64_t _totalSizeOfResource { 0 };
|
||||
};
|
||||
|
||||
#endif
|
||||
|
|
|
@ -13,28 +13,28 @@
|
|||
#include "UserActivityLogger.h"
|
||||
|
||||
void UserActivityLoggerScriptingInterface::enabledEdit() {
|
||||
logAction("enabled_edit");
|
||||
doLogAction("enabled_edit");
|
||||
}
|
||||
|
||||
void UserActivityLoggerScriptingInterface::openedTablet(bool visibleToOthers) {
|
||||
logAction("opened_tablet", { { "visible_to_others", visibleToOthers } });
|
||||
doLogAction("opened_tablet", { { "visible_to_others", visibleToOthers } });
|
||||
}
|
||||
|
||||
void UserActivityLoggerScriptingInterface::closedTablet() {
|
||||
logAction("closed_tablet");
|
||||
doLogAction("closed_tablet");
|
||||
}
|
||||
|
||||
void UserActivityLoggerScriptingInterface::openedMarketplace() {
|
||||
logAction("opened_marketplace");
|
||||
doLogAction("opened_marketplace");
|
||||
}
|
||||
|
||||
void UserActivityLoggerScriptingInterface::toggledAway(bool isAway) {
|
||||
logAction("toggled_away", { { "is_away", isAway } });
|
||||
doLogAction("toggled_away", { { "is_away", isAway } });
|
||||
}
|
||||
|
||||
void UserActivityLoggerScriptingInterface::tutorialProgress( QString stepName, int stepNumber, float secondsToComplete,
|
||||
float tutorialElapsedTime, QString tutorialRunID, int tutorialVersion, QString controllerType) {
|
||||
logAction("tutorial_progress", {
|
||||
doLogAction("tutorial_progress", {
|
||||
{ "tutorial_run_id", tutorialRunID },
|
||||
{ "tutorial_version", tutorialVersion },
|
||||
{ "step", stepName },
|
||||
|
@ -52,11 +52,11 @@ void UserActivityLoggerScriptingInterface::palAction(QString action, QString tar
|
|||
if (target.length() > 0) {
|
||||
payload["target"] = target;
|
||||
}
|
||||
logAction("pal_activity", payload);
|
||||
doLogAction("pal_activity", payload);
|
||||
}
|
||||
|
||||
void UserActivityLoggerScriptingInterface::palOpened(float secondsOpened) {
|
||||
logAction("pal_opened", {
|
||||
doLogAction("pal_opened", {
|
||||
{ "seconds_opened", secondsOpened }
|
||||
});
|
||||
}
|
||||
|
@ -68,10 +68,14 @@ void UserActivityLoggerScriptingInterface::makeUserConnection(QString otherID, b
|
|||
if (detailsString.length() > 0) {
|
||||
payload["details"] = detailsString;
|
||||
}
|
||||
logAction("makeUserConnection", payload);
|
||||
doLogAction("makeUserConnection", payload);
|
||||
}
|
||||
|
||||
void UserActivityLoggerScriptingInterface::logAction(QString action, QJsonObject details) {
|
||||
void UserActivityLoggerScriptingInterface::logAction(QString action, QVariantMap details) {
|
||||
doLogAction(action, QJsonObject::fromVariantMap(details));
|
||||
}
|
||||
|
||||
void UserActivityLoggerScriptingInterface::doLogAction(QString action, QJsonObject details) {
|
||||
QMetaObject::invokeMethod(&UserActivityLogger::getInstance(), "logAction",
|
||||
Q_ARG(QString, action),
|
||||
Q_ARG(QJsonObject, details));
|
||||
|
|
|
@ -29,9 +29,10 @@ public:
|
|||
float tutorialElapsedTime, QString tutorialRunID = "", int tutorialVersion = 0, QString controllerType = "");
|
||||
Q_INVOKABLE void palAction(QString action, QString target);
|
||||
Q_INVOKABLE void palOpened(float secondsOpen);
|
||||
Q_INVOKABLE void makeUserConnection(QString otherUser, bool success, QString details="");
|
||||
Q_INVOKABLE void makeUserConnection(QString otherUser, bool success, QString details = "");
|
||||
Q_INVOKABLE void logAction(QString action, QVariantMap details = QVariantMap{});
|
||||
private:
|
||||
void logAction(QString action, QJsonObject details = {});
|
||||
void doLogAction(QString action, QJsonObject details = {});
|
||||
};
|
||||
|
||||
#endif // hifi_UserActivityLoggerScriptingInterface_h
|
||||
|
|
|
@ -64,7 +64,7 @@ PacketVersion versionForPacketType(PacketType packetType) {
|
|||
case PacketType::AssetGetInfo:
|
||||
case PacketType::AssetGet:
|
||||
case PacketType::AssetUpload:
|
||||
return static_cast<PacketVersion>(AssetServerPacketVersion::VegasCongestionControl);
|
||||
return static_cast<PacketVersion>(AssetServerPacketVersion::RangeRequestSupport);
|
||||
case PacketType::NodeIgnoreRequest:
|
||||
return 18; // Introduction of node ignore request (which replaced an unused packet tpye)
|
||||
|
||||
|
|
|
@ -214,7 +214,8 @@ enum class EntityQueryPacketVersion: PacketVersion {
|
|||
};
|
||||
|
||||
enum class AssetServerPacketVersion: PacketVersion {
|
||||
VegasCongestionControl = 19
|
||||
VegasCongestionControl = 19,
|
||||
RangeRequestSupport
|
||||
};
|
||||
|
||||
enum class AvatarMixerPacketVersion : PacketVersion {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
set(TARGET_NAME procedural)
|
||||
AUTOSCRIBE_SHADER_LIB(gpu model)
|
||||
setup_hifi_library()
|
||||
link_hifi_libraries(shared gpu gpu-gl networking model model-networking image)
|
||||
link_hifi_libraries(shared gpu gpu-gl networking model model-networking ktx image)
|
||||
|
||||
|
|
|
@ -118,7 +118,7 @@ void MeshPartPayload::drawCall(gpu::Batch& batch) const {
|
|||
batch.drawIndexed(gpu::TRIANGLES, _drawPart._numIndices, _drawPart._startIndex);
|
||||
}
|
||||
|
||||
void MeshPartPayload::bindMesh(gpu::Batch& batch) const {
|
||||
void MeshPartPayload::bindMesh(gpu::Batch& batch) {
|
||||
batch.setIndexBuffer(gpu::UINT32, (_drawMesh->getIndexBuffer()._buffer), 0);
|
||||
|
||||
batch.setInputFormat((_drawMesh->getVertexFormat()));
|
||||
|
@ -255,7 +255,7 @@ void MeshPartPayload::bindTransform(gpu::Batch& batch, const ShapePipeline::Loca
|
|||
}
|
||||
|
||||
|
||||
void MeshPartPayload::render(RenderArgs* args) const {
|
||||
void MeshPartPayload::render(RenderArgs* args) {
|
||||
PerformanceTimer perfTimer("MeshPartPayload::render");
|
||||
|
||||
gpu::Batch& batch = *(args->_batch);
|
||||
|
@ -485,7 +485,7 @@ ShapeKey ModelMeshPartPayload::getShapeKey() const {
|
|||
return builder.build();
|
||||
}
|
||||
|
||||
void ModelMeshPartPayload::bindMesh(gpu::Batch& batch) const {
|
||||
void ModelMeshPartPayload::bindMesh(gpu::Batch& batch) {
|
||||
if (!_isBlendShaped) {
|
||||
batch.setIndexBuffer(gpu::UINT32, (_drawMesh->getIndexBuffer()._buffer), 0);
|
||||
|
||||
|
@ -517,7 +517,7 @@ void ModelMeshPartPayload::bindTransform(gpu::Batch& batch, const ShapePipeline:
|
|||
batch.setModelTransform(_transform);
|
||||
}
|
||||
|
||||
float ModelMeshPartPayload::computeFadeAlpha() const {
|
||||
float ModelMeshPartPayload::computeFadeAlpha() {
|
||||
if (_fadeState == FADE_WAITING_TO_START) {
|
||||
return 0.0f;
|
||||
}
|
||||
|
@ -536,7 +536,7 @@ float ModelMeshPartPayload::computeFadeAlpha() const {
|
|||
return Interpolate::simpleNonLinearBlend(fadeAlpha);
|
||||
}
|
||||
|
||||
void ModelMeshPartPayload::render(RenderArgs* args) const {
|
||||
void ModelMeshPartPayload::render(RenderArgs* args) {
|
||||
PerformanceTimer perfTimer("ModelMeshPartPayload::render");
|
||||
|
||||
if (!_model->addedToScene() || !_model->isVisible()) {
|
||||
|
@ -544,7 +544,7 @@ void ModelMeshPartPayload::render(RenderArgs* args) const {
|
|||
}
|
||||
|
||||
if (_fadeState == FADE_WAITING_TO_START) {
|
||||
if (_model->isLoaded() && _model->getGeometry()->areTexturesLoaded()) {
|
||||
if (_model->isLoaded()) {
|
||||
if (EntityItem::getEntitiesShouldFadeFunction()()) {
|
||||
_fadeStartTime = usecTimestampNow();
|
||||
_fadeState = FADE_IN_PROGRESS;
|
||||
|
@ -557,6 +557,11 @@ void ModelMeshPartPayload::render(RenderArgs* args) const {
|
|||
}
|
||||
}
|
||||
|
||||
if (_materialNeedsUpdate && _model->getGeometry()->areTexturesLoaded()) {
|
||||
_model->setRenderItemsNeedUpdate();
|
||||
_materialNeedsUpdate = false;
|
||||
}
|
||||
|
||||
if (!args) {
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -46,11 +46,11 @@ public:
|
|||
virtual render::ItemKey getKey() const;
|
||||
virtual render::Item::Bound getBound() const;
|
||||
virtual render::ShapeKey getShapeKey() const; // shape interface
|
||||
virtual void render(RenderArgs* args) const;
|
||||
virtual void render(RenderArgs* args);
|
||||
|
||||
// ModelMeshPartPayload functions to perform render
|
||||
void drawCall(gpu::Batch& batch) const;
|
||||
virtual void bindMesh(gpu::Batch& batch) const;
|
||||
virtual void bindMesh(gpu::Batch& batch);
|
||||
virtual void bindMaterial(gpu::Batch& batch, const render::ShapePipeline::LocationsPointer locations, bool enableTextures) const;
|
||||
virtual void bindTransform(gpu::Batch& batch, const render::ShapePipeline::LocationsPointer locations, RenderArgs::RenderMode renderMode) const;
|
||||
|
||||
|
@ -93,16 +93,16 @@ public:
|
|||
const Transform& boundTransform,
|
||||
const gpu::BufferPointer& buffer);
|
||||
|
||||
float computeFadeAlpha() const;
|
||||
float computeFadeAlpha();
|
||||
|
||||
// Render Item interface
|
||||
render::ItemKey getKey() const override;
|
||||
int getLayer() const;
|
||||
render::ShapeKey getShapeKey() const override; // shape interface
|
||||
void render(RenderArgs* args) const override;
|
||||
void render(RenderArgs* args) override;
|
||||
|
||||
// ModelMeshPartPayload functions to perform render
|
||||
void bindMesh(gpu::Batch& batch) const override;
|
||||
void bindMesh(gpu::Batch& batch) override;
|
||||
void bindTransform(gpu::Batch& batch, const render::ShapePipeline::LocationsPointer locations, RenderArgs::RenderMode renderMode) const override;
|
||||
|
||||
void initCache();
|
||||
|
@ -116,11 +116,12 @@ public:
|
|||
int _shapeID;
|
||||
|
||||
bool _isSkinned{ false };
|
||||
bool _isBlendShaped{ false };
|
||||
bool _isBlendShaped { false };
|
||||
bool _materialNeedsUpdate { true };
|
||||
|
||||
private:
|
||||
mutable quint64 _fadeStartTime { 0 };
|
||||
mutable uint8_t _fadeState { FADE_WAITING_TO_START };
|
||||
quint64 _fadeStartTime { 0 };
|
||||
uint8_t _fadeState { FADE_WAITING_TO_START };
|
||||
};
|
||||
|
||||
namespace render {
|
||||
|
|
|
@ -16,6 +16,6 @@ if (NOT ANDROID)
|
|||
|
||||
endif ()
|
||||
|
||||
link_hifi_libraries(shared networking octree gpu ui procedural model model-networking recording avatars fbx entities controllers animation audio physics image)
|
||||
link_hifi_libraries(shared networking octree gpu ui procedural model model-networking ktx recording avatars fbx entities controllers animation audio physics image)
|
||||
# ui includes gl, but link_hifi_libraries does not use transitive includes, so gl must be explicit
|
||||
include_hifi_library_headers(gl)
|
||||
|
|
|
@ -44,7 +44,8 @@ void AssetScriptingInterface::setMapping(QString path, QString hash, QScriptValu
|
|||
|
||||
QObject::connect(setMappingRequest, &SetMappingRequest::finished, this, [this, callback](SetMappingRequest* request) mutable {
|
||||
if (callback.isFunction()) {
|
||||
QScriptValueList args { };
|
||||
QString error = request->getErrorString();
|
||||
QScriptValueList args { error };
|
||||
callback.call(_engine->currentContext()->thisObject(), args);
|
||||
}
|
||||
request->deleteLater();
|
||||
|
|
|
@ -72,6 +72,7 @@ public:
|
|||
/**jsdoc
|
||||
* Called when setMapping is complete
|
||||
* @callback Assets~setMappingCallback
|
||||
* @param {string} error
|
||||
*/
|
||||
Q_INVOKABLE void setMapping(QString path, QString hash, QScriptValue callback);
|
||||
|
||||
|
|
|
@ -210,9 +210,11 @@ bool RecordingScriptingInterface::saveRecordingToAsset(QScriptValue getClipAtpUr
|
|||
}
|
||||
|
||||
if (QThread::currentThread() != thread()) {
|
||||
bool result;
|
||||
QMetaObject::invokeMethod(this, "saveRecordingToAsset", Qt::BlockingQueuedConnection,
|
||||
Q_RETURN_ARG(bool, result),
|
||||
Q_ARG(QScriptValue, getClipAtpUrl));
|
||||
return false;
|
||||
return result;
|
||||
}
|
||||
|
||||
if (!_lastClip) {
|
||||
|
|
|
@ -43,6 +43,7 @@
|
|||
#include <NetworkAccessManager.h>
|
||||
#include <PathUtils.h>
|
||||
#include <ResourceScriptingInterface.h>
|
||||
#include <UserActivityLoggerScriptingInterface.h>
|
||||
#include <NodeList.h>
|
||||
#include <ScriptAvatarData.h>
|
||||
#include <udt/PacketHeaders.h>
|
||||
|
@ -678,6 +679,8 @@ void ScriptEngine::init() {
|
|||
registerGlobalObject("Model", new ModelScriptingInterface(this));
|
||||
qScriptRegisterMetaType(this, meshToScriptValue, meshFromScriptValue);
|
||||
qScriptRegisterMetaType(this, meshesToScriptValue, meshesFromScriptValue);
|
||||
|
||||
registerGlobalObject("UserActivityLogger", DependencyManager::get<UserActivityLoggerScriptingInterface>().data());
|
||||
}
|
||||
|
||||
void ScriptEngine::registerValue(const QString& valueName, QScriptValue value) {
|
||||
|
@ -2317,6 +2320,8 @@ void ScriptEngine::unloadEntityScript(const EntityItemID& entityID, bool shouldR
|
|||
|
||||
if (_entityScripts.contains(entityID)) {
|
||||
const EntityScriptDetails &oldDetails = _entityScripts[entityID];
|
||||
auto scriptText = oldDetails.scriptText;
|
||||
|
||||
if (isEntityScriptRunning(entityID)) {
|
||||
callEntityScriptMethod(entityID, "unload");
|
||||
}
|
||||
|
@ -2334,14 +2339,14 @@ void ScriptEngine::unloadEntityScript(const EntityItemID& entityID, bool shouldR
|
|||
newDetails.status = EntityScriptStatus::UNLOADED;
|
||||
newDetails.lastModified = QDateTime::currentMSecsSinceEpoch();
|
||||
// keep scriptText populated for the current need to "debouce" duplicate calls to unloadEntityScript
|
||||
newDetails.scriptText = oldDetails.scriptText;
|
||||
newDetails.scriptText = scriptText;
|
||||
setEntityScriptDetails(entityID, newDetails);
|
||||
}
|
||||
|
||||
stopAllTimersForEntityScript(entityID);
|
||||
{
|
||||
// FIXME: shouldn't have to do this here, but currently something seems to be firing unloads moments after firing initial load requests
|
||||
processDeferredEntityLoads(oldDetails.scriptText, entityID);
|
||||
processDeferredEntityLoads(scriptText, entityID);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -68,7 +68,7 @@ StoragePointer FileStorage::create(const QString& filename, size_t size, const u
|
|||
}
|
||||
|
||||
FileStorage::FileStorage(const QString& filename) : _file(filename) {
|
||||
if (_file.open(QFile::ReadOnly)) {
|
||||
if (_file.open(QFile::ReadWrite)) {
|
||||
_mapped = _file.map(0, _file.size());
|
||||
if (_mapped) {
|
||||
_valid = true;
|
||||
|
|
|
@ -20,10 +20,12 @@ namespace storage {
|
|||
class Storage;
|
||||
using StoragePointer = std::shared_ptr<const Storage>;
|
||||
|
||||
// Abstract class to represent memory that stored _somewhere_ (in system memory or in a file, for example)
|
||||
class Storage : public std::enable_shared_from_this<Storage> {
|
||||
public:
|
||||
virtual ~Storage() {}
|
||||
virtual const uint8_t* data() const = 0;
|
||||
virtual uint8_t* mutableData() = 0;
|
||||
virtual size_t size() const = 0;
|
||||
virtual operator bool() const { return true; }
|
||||
|
||||
|
@ -41,6 +43,7 @@ namespace storage {
|
|||
MemoryStorage(size_t size, const uint8_t* data = nullptr);
|
||||
const uint8_t* data() const override { return _data.data(); }
|
||||
uint8_t* data() { return _data.data(); }
|
||||
uint8_t* mutableData() override { return _data.data(); }
|
||||
size_t size() const override { return _data.size(); }
|
||||
operator bool() const override { return true; }
|
||||
private:
|
||||
|
@ -57,6 +60,7 @@ namespace storage {
|
|||
FileStorage& operator=(const FileStorage& other) = delete;
|
||||
|
||||
const uint8_t* data() const override { return _mapped; }
|
||||
uint8_t* mutableData() override { return _mapped; }
|
||||
size_t size() const override { return _file.size(); }
|
||||
operator bool() const override { return _valid; }
|
||||
private:
|
||||
|
@ -69,6 +73,7 @@ namespace storage {
|
|||
public:
|
||||
ViewStorage(const storage::StoragePointer& owner, size_t size, const uint8_t* data);
|
||||
const uint8_t* data() const override { return _data; }
|
||||
uint8_t* mutableData() override { throw std::runtime_error("Cannot modify ViewStorage"); }
|
||||
size_t size() const override { return _size; }
|
||||
operator bool() const override { return *_owner; }
|
||||
private:
|
||||
|
|
|
@ -26,6 +26,10 @@ QStringList FileDialogHelper::standardPath(StandardLocation location) {
|
|||
return QStandardPaths::standardLocations(static_cast<QStandardPaths::StandardLocation>(location));
|
||||
}
|
||||
|
||||
QString FileDialogHelper::writableLocation(StandardLocation location) {
|
||||
return QStandardPaths::writableLocation(static_cast<QStandardPaths::StandardLocation>(location));
|
||||
}
|
||||
|
||||
QString FileDialogHelper::urlToPath(const QUrl& url) {
|
||||
return url.toLocalFile();
|
||||
}
|
||||
|
|
|
@ -48,6 +48,7 @@ public:
|
|||
|
||||
Q_INVOKABLE QUrl home();
|
||||
Q_INVOKABLE QStringList standardPath(StandardLocation location);
|
||||
Q_INVOKABLE QString writableLocation(StandardLocation location);
|
||||
Q_INVOKABLE QStringList drives();
|
||||
Q_INVOKABLE QString urlToPath(const QUrl& url);
|
||||
Q_INVOKABLE bool urlIsDir(const QUrl& url);
|
||||
|
|
|
@ -716,6 +716,86 @@ QString OffscreenUi::getExistingDirectory(void* ignored, const QString &caption,
|
|||
return DependencyManager::get<OffscreenUi>()->existingDirectoryDialog(caption, dir, filter, selectedFilter, options);
|
||||
}
|
||||
|
||||
class AssetDialogListener : public ModalDialogListener {
|
||||
// ATP equivalent of FileDialogListener.
|
||||
Q_OBJECT
|
||||
|
||||
friend class OffscreenUi;
|
||||
AssetDialogListener(QQuickItem* messageBox) : ModalDialogListener(messageBox) {
|
||||
if (_finished) {
|
||||
return;
|
||||
}
|
||||
connect(_dialog, SIGNAL(selectedAsset(QVariant)), this, SLOT(onSelectedAsset(QVariant)));
|
||||
}
|
||||
|
||||
private slots:
|
||||
void onSelectedAsset(QVariant asset) {
|
||||
_result = asset;
|
||||
_finished = true;
|
||||
disconnect(_dialog);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
QString OffscreenUi::assetDialog(const QVariantMap& properties) {
|
||||
// ATP equivalent of fileDialog().
|
||||
QVariant buildDialogResult;
|
||||
bool invokeResult;
|
||||
auto tabletScriptingInterface = DependencyManager::get<TabletScriptingInterface>();
|
||||
TabletProxy* tablet = dynamic_cast<TabletProxy*>(tabletScriptingInterface->getTablet("com.highfidelity.interface.tablet.system"));
|
||||
if (tablet->getToolbarMode()) {
|
||||
invokeResult = QMetaObject::invokeMethod(_desktop, "assetDialog",
|
||||
Q_RETURN_ARG(QVariant, buildDialogResult),
|
||||
Q_ARG(QVariant, QVariant::fromValue(properties)));
|
||||
} else {
|
||||
QQuickItem* tabletRoot = tablet->getTabletRoot();
|
||||
invokeResult = QMetaObject::invokeMethod(tabletRoot, "assetDialog",
|
||||
Q_RETURN_ARG(QVariant, buildDialogResult),
|
||||
Q_ARG(QVariant, QVariant::fromValue(properties)));
|
||||
emit tabletScriptingInterface->tabletNotification();
|
||||
}
|
||||
|
||||
if (!invokeResult) {
|
||||
qWarning() << "Failed to create asset open dialog";
|
||||
return QString();
|
||||
}
|
||||
|
||||
QVariant result = AssetDialogListener(qvariant_cast<QQuickItem*>(buildDialogResult)).waitForResult();
|
||||
if (!result.isValid()) {
|
||||
return QString();
|
||||
}
|
||||
qCDebug(uiLogging) << result.toString();
|
||||
return result.toUrl().toString();
|
||||
}
|
||||
|
||||
QString OffscreenUi::assetOpenDialog(const QString& caption, const QString& dir, const QString& filter, QString* selectedFilter, QFileDialog::Options options) {
|
||||
// ATP equivalent of fileOpenDialog().
|
||||
if (QThread::currentThread() != thread()) {
|
||||
QString result;
|
||||
QMetaObject::invokeMethod(this, "assetOpenDialog", Qt::BlockingQueuedConnection,
|
||||
Q_RETURN_ARG(QString, result),
|
||||
Q_ARG(QString, caption),
|
||||
Q_ARG(QString, dir),
|
||||
Q_ARG(QString, filter),
|
||||
Q_ARG(QString*, selectedFilter),
|
||||
Q_ARG(QFileDialog::Options, options));
|
||||
return result;
|
||||
}
|
||||
|
||||
// FIXME support returning the selected filter... somehow?
|
||||
QVariantMap map;
|
||||
map.insert("caption", caption);
|
||||
map.insert("dir", dir);
|
||||
map.insert("filter", filter);
|
||||
map.insert("options", static_cast<int>(options));
|
||||
return assetDialog(map);
|
||||
}
|
||||
|
||||
QString OffscreenUi::getOpenAssetName(void* ignored, const QString &caption, const QString &dir, const QString &filter, QString *selectedFilter, QFileDialog::Options options) {
|
||||
// ATP equivalent of getOpenFileName().
|
||||
return DependencyManager::get<OffscreenUi>()->assetOpenDialog(caption, dir, filter, selectedFilter, options);
|
||||
}
|
||||
|
||||
bool OffscreenUi::eventFilter(QObject* originalDestination, QEvent* event) {
|
||||
if (!filterEnabled(originalDestination, event)) {
|
||||
return false;
|
||||
|
|
|
@ -118,6 +118,8 @@ public:
|
|||
Q_INVOKABLE QString fileSaveDialog(const QString &caption = QString(), const QString &dir = QString(), const QString &filter = QString(), QString *selectedFilter = 0, QFileDialog::Options options = 0);
|
||||
Q_INVOKABLE QString existingDirectoryDialog(const QString &caption = QString(), const QString &dir = QString(), const QString &filter = QString(), QString *selectedFilter = 0, QFileDialog::Options options = 0);
|
||||
|
||||
Q_INVOKABLE QString assetOpenDialog(const QString &caption = QString(), const QString &dir = QString(), const QString &filter = QString(), QString *selectedFilter = 0, QFileDialog::Options options = 0);
|
||||
|
||||
// Compatibility with QFileDialog::getOpenFileName
|
||||
static QString getOpenFileName(void* ignored, const QString &caption = QString(), const QString &dir = QString(), const QString &filter = QString(), QString *selectedFilter = 0, QFileDialog::Options options = 0);
|
||||
// Compatibility with QFileDialog::getSaveFileName
|
||||
|
@ -125,6 +127,8 @@ public:
|
|||
// Compatibility with QFileDialog::getExistingDirectory
|
||||
static QString getExistingDirectory(void* ignored, const QString &caption = QString(), const QString &dir = QString(), const QString &filter = QString(), QString *selectedFilter = 0, QFileDialog::Options options = 0);
|
||||
|
||||
static QString getOpenAssetName(void* ignored, const QString &caption = QString(), const QString &dir = QString(), const QString &filter = QString(), QString *selectedFilter = 0, QFileDialog::Options options = 0);
|
||||
|
||||
Q_INVOKABLE QVariant inputDialog(const Icon icon, const QString& title, const QString& label = QString(), const QVariant& current = QVariant());
|
||||
Q_INVOKABLE QVariant customInputDialog(const Icon icon, const QString& title, const QVariantMap& config);
|
||||
QQuickItem* createInputDialog(const Icon icon, const QString& title, const QString& label, const QVariant& current);
|
||||
|
@ -155,6 +159,7 @@ signals:
|
|||
|
||||
private:
|
||||
QString fileDialog(const QVariantMap& properties);
|
||||
QString assetDialog(const QVariantMap& properties);
|
||||
|
||||
QQuickItem* _desktop { nullptr };
|
||||
QQuickItem* _toolWindow { nullptr };
|
||||
|
|
|
@ -13,7 +13,7 @@ if (WIN32)
|
|||
setup_hifi_plugin(OpenGL Script Qml Widgets)
|
||||
link_hifi_libraries(shared gl networking controllers ui
|
||||
plugins display-plugins ui-plugins input-plugins script-engine
|
||||
render-utils model gpu gpu-gl render model-networking fbx image)
|
||||
render-utils model gpu gpu-gl render model-networking fbx ktx image)
|
||||
|
||||
include_hifi_library_headers(octree)
|
||||
|
||||
|
|
103
scripts/developer/inputRecording.js
Normal file
103
scripts/developer/inputRecording.js
Normal file
|
@ -0,0 +1,103 @@
|
|||
//
|
||||
// Created by Dante Ruiz 2017/04/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
|
||||
//
|
||||
|
||||
(function() {
|
||||
var recording = false;
|
||||
var onRecordingScreen = false;
|
||||
var passedSaveDirectory = false;
|
||||
var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system");
|
||||
var button = tablet.addButton({
|
||||
text: "IRecord"
|
||||
});
|
||||
function onClick() {
|
||||
if (onRecordingScreen) {
|
||||
tablet.gotoHomeScreen();
|
||||
onRecordingScreen = false;
|
||||
} else {
|
||||
tablet.loadQMLSource("InputRecorder.qml");
|
||||
onRecordingScreen = true;
|
||||
}
|
||||
}
|
||||
|
||||
function onScreenChanged(type, url) {
|
||||
onRecordingScreen = false;
|
||||
passedSaveDirectory = false;
|
||||
}
|
||||
|
||||
button.clicked.connect(onClick);
|
||||
tablet.fromQml.connect(fromQml);
|
||||
tablet.screenChanged.connect(onScreenChanged);
|
||||
function fromQml(message) {
|
||||
switch (message.method) {
|
||||
case "Start":
|
||||
startRecording();
|
||||
break;
|
||||
case "Stop":
|
||||
stopRecording();
|
||||
break;
|
||||
case "Save":
|
||||
saveRecording();
|
||||
break;
|
||||
case "Load":
|
||||
loadRecording(message.params.file);
|
||||
break;
|
||||
case "playback":
|
||||
startPlayback();
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function startRecording() {
|
||||
Controller.startInputRecording();
|
||||
recording = true;
|
||||
}
|
||||
|
||||
function stopRecording() {
|
||||
Controller.stopInputRecording();
|
||||
recording = false;
|
||||
}
|
||||
|
||||
function saveRecording() {
|
||||
Controller.saveInputRecording();
|
||||
}
|
||||
|
||||
function loadRecording(file) {
|
||||
Controller.loadInputRecording(file);
|
||||
}
|
||||
|
||||
function startPlayback() {
|
||||
Controller.startInputPlayback();
|
||||
}
|
||||
|
||||
function sendToQml(message) {
|
||||
tablet.sendToQml(message);
|
||||
}
|
||||
|
||||
function update() {
|
||||
|
||||
if (!passedSaveDirectory) {
|
||||
var directory = Controller.getInputRecorderSaveDirectory();
|
||||
sendToQml({method: "path", params: directory});
|
||||
passedSaveDirectory = true;
|
||||
}
|
||||
sendToQml({method: "update", params: recording});
|
||||
}
|
||||
|
||||
Script.setInterval(update, 60);
|
||||
|
||||
Script.scriptEnding.connect(function () {
|
||||
button.clicked.disconnect(onClick);
|
||||
if (tablet) {
|
||||
tablet.removeButton(button);
|
||||
}
|
||||
|
||||
Controller.stopInputRecording();
|
||||
});
|
||||
|
||||
}());
|
85
scripts/system/app-doppleganger.js
Normal file
85
scripts/system/app-doppleganger.js
Normal file
|
@ -0,0 +1,85 @@
|
|||
// doppleganger-app.js
|
||||
//
|
||||
// Created by Timothy Dedischew on 04/21/2017.
|
||||
// Copyright 2017 High Fidelity, Inc.
|
||||
//
|
||||
// This Client script creates an instance of a Doppleganger that can be toggled on/off via tablet button.
|
||||
// (for more info see doppleganger.js)
|
||||
//
|
||||
// Distributed under the Apache License, Version 2.0.
|
||||
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
|
||||
//
|
||||
|
||||
var DopplegangerClass = Script.require('./doppleganger.js');
|
||||
|
||||
var tablet = Tablet.getTablet('com.highfidelity.interface.tablet.system'),
|
||||
button = tablet.addButton({
|
||||
icon: "icons/tablet-icons/doppleganger-i.svg",
|
||||
activeIcon: "icons/tablet-icons/doppleganger-a.svg",
|
||||
text: 'MIRROR'
|
||||
});
|
||||
|
||||
Script.scriptEnding.connect(function() {
|
||||
tablet.removeButton(button);
|
||||
button = null;
|
||||
});
|
||||
|
||||
var doppleganger = new DopplegangerClass({
|
||||
avatar: MyAvatar,
|
||||
mirrored: true,
|
||||
autoUpdate: true
|
||||
});
|
||||
|
||||
// hide the doppleganger if this client script is unloaded
|
||||
Script.scriptEnding.connect(doppleganger, 'stop');
|
||||
|
||||
// hide the doppleganger if the user switches domains (which might place them arbitrarily far away in world space)
|
||||
function onDomainChanged() {
|
||||
if (doppleganger.active) {
|
||||
doppleganger.stop('domain_changed');
|
||||
}
|
||||
}
|
||||
Window.domainChanged.connect(onDomainChanged);
|
||||
Window.domainConnectionRefused.connect(onDomainChanged);
|
||||
Script.scriptEnding.connect(function() {
|
||||
Window.domainChanged.disconnect(onDomainChanged);
|
||||
Window.domainConnectionRefused.disconnect(onDomainChanged);
|
||||
});
|
||||
|
||||
// toggle on/off via tablet button
|
||||
button.clicked.connect(doppleganger, 'toggle');
|
||||
|
||||
// highlight tablet button based on current doppleganger state
|
||||
doppleganger.activeChanged.connect(function(active, reason) {
|
||||
if (button) {
|
||||
button.editProperties({ isActive: active });
|
||||
print('doppleganger.activeChanged', active, reason);
|
||||
}
|
||||
});
|
||||
|
||||
// alert the user if there was an error applying their skeletonModelURL
|
||||
doppleganger.modelOverlayLoaded.connect(function(error, result) {
|
||||
if (doppleganger.active && error) {
|
||||
Window.alert('doppleganger | ' + error + '\n' + doppleganger.skeletonModelURL);
|
||||
}
|
||||
});
|
||||
|
||||
// add debug indicators, but only if the user has configured the settings value
|
||||
if (Settings.getValue('debug.doppleganger', false)) {
|
||||
DopplegangerClass.addDebugControls(doppleganger);
|
||||
}
|
||||
|
||||
UserActivityLogger.logAction('doppleganger_app_load');
|
||||
doppleganger.activeChanged.connect(function(active, reason) {
|
||||
if (active) {
|
||||
UserActivityLogger.logAction('doppleganger_enable');
|
||||
} else {
|
||||
if (reason === 'stop') {
|
||||
// user intentionally toggled the doppleganger
|
||||
UserActivityLogger.logAction('doppleganger_disable');
|
||||
} else {
|
||||
print('doppleganger stopped:', reason);
|
||||
UserActivityLogger.logAction('doppleganger_autodisable', { reason: reason });
|
||||
}
|
||||
}
|
||||
});
|
BIN
scripts/system/assets/sounds/countdown-tick.wav
Normal file
BIN
scripts/system/assets/sounds/countdown-tick.wav
Normal file
Binary file not shown.
BIN
scripts/system/assets/sounds/finish-recording.wav
Normal file
BIN
scripts/system/assets/sounds/finish-recording.wav
Normal file
Binary file not shown.
BIN
scripts/system/assets/sounds/start-recording.wav
Normal file
BIN
scripts/system/assets/sounds/start-recording.wav
Normal file
Binary file not shown.
|
@ -1330,7 +1330,7 @@ function MyController(hand) {
|
|||
if (this.stylus) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
var stylusProperties = {
|
||||
name: "stylus",
|
||||
url: Script.resourcesPath() + "meshes/tablet-stylus-fat.fbx",
|
||||
|
@ -1420,6 +1420,14 @@ function MyController(hand) {
|
|||
}
|
||||
};
|
||||
|
||||
// Turns off indicators used for searching. Overlay line and sphere.
|
||||
this.searchIndicatorOff = function() {
|
||||
this.searchSphereOff();
|
||||
if (PICK_WITH_HAND_RAY) {
|
||||
this.overlayLineOff();
|
||||
}
|
||||
}
|
||||
|
||||
this.otherGrabbingLineOn = function(avatarPosition, entityPosition, color) {
|
||||
if (this.otherGrabbingLine === null) {
|
||||
var lineProperties = {
|
||||
|
@ -1791,6 +1799,15 @@ function MyController(hand) {
|
|||
}
|
||||
|
||||
this.processStylus();
|
||||
|
||||
if (isInEditMode() && !this.isNearStylusTarget) {
|
||||
// Always showing lasers while in edit mode and hands/stylus is not active.
|
||||
var rayPickInfo = this.calcRayPickInfo(this.hand);
|
||||
this.intersectionDistance = (rayPickInfo.entityID || rayPickInfo.overlayID) ? rayPickInfo.distance : 0;
|
||||
this.searchIndicatorOn(rayPickInfo.searchRay);
|
||||
} else {
|
||||
this.searchIndicatorOff();
|
||||
}
|
||||
};
|
||||
|
||||
this.handleLaserOnHomeButton = function(rayPickInfo) {
|
||||
|
@ -2237,15 +2254,22 @@ function MyController(hand) {
|
|||
return;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (isInEditMode()) {
|
||||
this.searchIndicatorOn(rayPickInfo.searchRay);
|
||||
if (this.triggerSmoothedGrab()) {
|
||||
if (!this.editTriggered && rayPickInfo.entityID) {
|
||||
Messages.sendLocalMessage("entityToolUpdates", JSON.stringify({
|
||||
method: "selectEntity",
|
||||
entityID: rayPickInfo.entityID
|
||||
}));
|
||||
if (!this.editTriggered){
|
||||
if (rayPickInfo.entityID) {
|
||||
Messages.sendLocalMessage("entityToolUpdates", JSON.stringify({
|
||||
method: "selectEntity",
|
||||
entityID: rayPickInfo.entityID
|
||||
}));
|
||||
} else if (rayPickInfo.overlayID) {
|
||||
Messages.sendLocalMessage("entityToolUpdates", JSON.stringify({
|
||||
method: "selectOverlay",
|
||||
overlayID: rayPickInfo.overlayID
|
||||
}));
|
||||
}
|
||||
}
|
||||
this.editTriggered = true;
|
||||
}
|
||||
|
@ -2274,7 +2298,7 @@ function MyController(hand) {
|
|||
if (this.getOtherHandController().state === STATE_DISTANCE_HOLDING) {
|
||||
this.setState(STATE_DISTANCE_ROTATING, "distance rotate '" + name + "'");
|
||||
} else {
|
||||
this.setState(STATE_DISTANCE_HOLDING, "distance hold '" + name + "'");
|
||||
this.setState(STATE_DISTANCE_HOLDING, "distance hold '" + name + "'");
|
||||
}
|
||||
return;
|
||||
} else {
|
||||
|
@ -3341,7 +3365,14 @@ function MyController(hand) {
|
|||
};
|
||||
|
||||
this.offEnter = function() {
|
||||
// Reuse the existing search distance if lasers were active since
|
||||
// they will be shown in OFF state while in edit mode.
|
||||
var existingSearchDistance = this.searchSphereDistance;
|
||||
this.release();
|
||||
|
||||
if (isInEditMode()) {
|
||||
this.searchSphereDistance = existingSearchDistance;
|
||||
}
|
||||
};
|
||||
|
||||
this.entityLaserTouchingEnter = function() {
|
||||
|
|
494
scripts/system/doppleganger.js
Normal file
494
scripts/system/doppleganger.js
Normal file
|
@ -0,0 +1,494 @@
|
|||
"use strict";
|
||||
|
||||
// doppleganger.js
|
||||
//
|
||||
// Created by Timothy Dedischew on 04/21/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
|
||||
//
|
||||
|
||||
/* global module */
|
||||
// @module doppleganger
|
||||
//
|
||||
// This module contains the `Doppleganger` class implementation for creating an inspectable replica of
|
||||
// an Avatar (as a model directly in front of and facing them). Joint positions and rotations are copied
|
||||
// over in an update thread, so that the model automatically mirrors the Avatar's joint movements.
|
||||
// An Avatar can then for example walk around "themselves" and examine from the back, etc.
|
||||
//
|
||||
// This should be helpful for inspecting your own look and debugging avatars, etc.
|
||||
//
|
||||
// The doppleganger is created as an overlay so that others do not see it -- and this also allows for the
|
||||
// highest possible update rate when keeping joint data in sync.
|
||||
|
||||
module.exports = Doppleganger;
|
||||
|
||||
// @property {bool} - when set true, Script.update will be used instead of setInterval for syncing joint data
|
||||
Doppleganger.USE_SCRIPT_UPDATE = false;
|
||||
|
||||
// @property {int} - the frame rate to target when using setInterval for joint updates
|
||||
Doppleganger.TARGET_FPS = 60;
|
||||
|
||||
// @property {int} - the maximum time in seconds to wait for the model overlay to finish loading
|
||||
Doppleganger.MAX_WAIT_SECS = 10;
|
||||
|
||||
// @function - derive mirrored joint names from a list of regular joint names
|
||||
// @param {Array} - list of joint names to mirror
|
||||
// @return {Array} - list of mirrored joint names (note: entries for non-mirrored joints will be `undefined`)
|
||||
Doppleganger.getMirroredJointNames = function(jointNames) {
|
||||
return jointNames.map(function(name, i) {
|
||||
if (/Left/.test(name)) {
|
||||
return name.replace('Left', 'Right');
|
||||
}
|
||||
if (/Right/.test(name)) {
|
||||
return name.replace('Right', 'Left');
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
};
|
||||
|
||||
// @class Doppleganger - Creates a new instance of a Doppleganger.
|
||||
// @param {Avatar} [options.avatar=MyAvatar] - Avatar used to retrieve position and joint data.
|
||||
// @param {bool} [options.mirrored=true] - Apply "symmetric mirroring" of Left/Right joints.
|
||||
// @param {bool} [options.autoUpdate=true] - Automatically sync joint data.
|
||||
function Doppleganger(options) {
|
||||
options = options || {};
|
||||
this.avatar = options.avatar || MyAvatar;
|
||||
this.mirrored = 'mirrored' in options ? options.mirrored : true;
|
||||
this.autoUpdate = 'autoUpdate' in options ? options.autoUpdate : true;
|
||||
|
||||
// @public
|
||||
this.active = false; // whether doppleganger is currently being displayed/updated
|
||||
this.overlayID = null; // current doppleganger's Overlay id
|
||||
this.frame = 0; // current joint update frame
|
||||
|
||||
// @signal - emitted when .active state changes
|
||||
this.activeChanged = signal(function(active, reason) {});
|
||||
// @signal - emitted once model overlay is either loaded or errors out
|
||||
this.modelOverlayLoaded = signal(function(error, result){});
|
||||
// @signal - emitted each time the model overlay's joint data has been synchronized
|
||||
this.jointsUpdated = signal(function(overlayID){});
|
||||
}
|
||||
|
||||
Doppleganger.prototype = {
|
||||
// @public @method - toggles doppleganger on/off
|
||||
toggle: function() {
|
||||
if (this.active) {
|
||||
log('toggling off');
|
||||
this.stop();
|
||||
} else {
|
||||
log('toggling on');
|
||||
this.start();
|
||||
}
|
||||
return this.active;
|
||||
},
|
||||
|
||||
// @public @method - synchronize the joint data between Avatar / doppleganger
|
||||
update: function() {
|
||||
this.frame++;
|
||||
try {
|
||||
if (!this.overlayID) {
|
||||
throw new Error('!this.overlayID');
|
||||
}
|
||||
|
||||
if (this.avatar.skeletonModelURL !== this.skeletonModelURL) {
|
||||
return this.stop('avatar_changed');
|
||||
}
|
||||
|
||||
var rotations = this.avatar.getJointRotations();
|
||||
var translations = this.avatar.getJointTranslations();
|
||||
var size = rotations.length;
|
||||
|
||||
// note: this mismatch can happen when the avatar's model is actively changing
|
||||
if (size !== translations.length ||
|
||||
(this.jointStateCount && size !== this.jointStateCount)) {
|
||||
log('mismatched joint counts (avatar model likely changed)', size, translations.length, this.jointStateCount);
|
||||
this.stop('avatar_changed_joints');
|
||||
return;
|
||||
}
|
||||
this.jointStateCount = size;
|
||||
|
||||
if (this.mirrored) {
|
||||
var mirroredIndexes = this.mirroredIndexes;
|
||||
var outRotations = new Array(size);
|
||||
var outTranslations = new Array(size);
|
||||
for (var i=0; i < size; i++) {
|
||||
var index = mirroredIndexes[i];
|
||||
if (index < 0 || index === false) {
|
||||
index = i;
|
||||
}
|
||||
var rot = rotations[index];
|
||||
var trans = translations[index];
|
||||
trans.x *= -1;
|
||||
rot.y *= -1;
|
||||
rot.z *= -1;
|
||||
outRotations[i] = rot;
|
||||
outTranslations[i] = trans;
|
||||
}
|
||||
rotations = outRotations;
|
||||
translations = outTranslations;
|
||||
}
|
||||
Overlays.editOverlay(this.overlayID, {
|
||||
jointRotations: rotations,
|
||||
jointTranslations: translations
|
||||
});
|
||||
|
||||
this.jointsUpdated(this.overlayID);
|
||||
} catch (e) {
|
||||
log('.update error: '+ e, index);
|
||||
this.stop('update_error');
|
||||
}
|
||||
},
|
||||
|
||||
// @public @method - show the doppleganger (and start the update thread, if options.autoUpdate was specified).
|
||||
// @param {vec3} [options.position=(in front of avatar)] - starting position
|
||||
// @param {quat} [options.orientation=avatar.orientation] - starting orientation
|
||||
start: function(options) {
|
||||
options = options || {};
|
||||
if (this.overlayID) {
|
||||
log('start() called but overlay model already exists', this.overlayID);
|
||||
return;
|
||||
}
|
||||
var avatar = this.avatar;
|
||||
if (!avatar.jointNames.length) {
|
||||
return this.stop('joints_unavailable');
|
||||
}
|
||||
|
||||
this.frame = 0;
|
||||
this.position = options.position || Vec3.sum(avatar.position, Quat.getForward(avatar.orientation));
|
||||
this.orientation = options.orientation || avatar.orientation;
|
||||
this.skeletonModelURL = avatar.skeletonModelURL;
|
||||
this.jointStateCount = 0;
|
||||
this.jointNames = avatar.jointNames;
|
||||
this.mirroredNames = Doppleganger.getMirroredJointNames(this.jointNames);
|
||||
this.mirroredIndexes = this.mirroredNames.map(function(name) {
|
||||
return name ? avatar.getJointIndex(name) : false;
|
||||
});
|
||||
|
||||
this.overlayID = Overlays.addOverlay('model', {
|
||||
visible: false,
|
||||
url: this.skeletonModelURL,
|
||||
position: this.position,
|
||||
rotation: this.orientation
|
||||
});
|
||||
|
||||
this.onModelOverlayLoaded = function(error, result) {
|
||||
if (error) {
|
||||
return this.stop(error);
|
||||
}
|
||||
log('ModelOverlay is ready; # joints == ' + result.jointNames.length);
|
||||
Overlays.editOverlay(this.overlayID, { visible: true });
|
||||
if (!options.position) {
|
||||
this.syncVerticalPosition();
|
||||
}
|
||||
if (this.autoUpdate) {
|
||||
this._createUpdateThread();
|
||||
}
|
||||
};
|
||||
this.modelOverlayLoaded.connect(this, 'onModelOverlayLoaded');
|
||||
|
||||
log('doppleganger created; overlayID =', this.overlayID);
|
||||
|
||||
// trigger clean up (and stop updates) if the overlay gets deleted
|
||||
this.onDeletedOverlay = function(uuid) {
|
||||
if (uuid === this.overlayID) {
|
||||
log('onDeletedOverlay', uuid);
|
||||
this.stop('overlay_deleted');
|
||||
}
|
||||
};
|
||||
Overlays.overlayDeleted.connect(this, 'onDeletedOverlay');
|
||||
|
||||
if ('onLoadComplete' in avatar) {
|
||||
// stop the current doppleganger if Avatar loads a different model URL
|
||||
this.onLoadComplete = function() {
|
||||
if (avatar.skeletonModelURL !== this.skeletonModelURL) {
|
||||
this.stop('avatar_changed_load');
|
||||
}
|
||||
};
|
||||
avatar.onLoadComplete.connect(this, 'onLoadComplete');
|
||||
}
|
||||
|
||||
this.activeChanged(this.active = true, 'start');
|
||||
this._waitForModel(ModelCache.prefetch(this.skeletonModelURL));
|
||||
},
|
||||
|
||||
// @public @method - hide the doppleganger
|
||||
// @param {String} [reason=stop] - the reason stop was called
|
||||
stop: function(reason) {
|
||||
reason = reason || 'stop';
|
||||
if (this.onUpdate) {
|
||||
Script.update.disconnect(this, 'onUpdate');
|
||||
delete this.onUpdate;
|
||||
}
|
||||
if (this._interval) {
|
||||
Script.clearInterval(this._interval);
|
||||
this._interval = undefined;
|
||||
}
|
||||
if (this.onDeletedOverlay) {
|
||||
Overlays.overlayDeleted.disconnect(this, 'onDeletedOverlay');
|
||||
delete this.onDeletedOverlay;
|
||||
}
|
||||
if (this.onLoadComplete) {
|
||||
this.avatar.onLoadComplete.disconnect(this, 'onLoadComplete');
|
||||
delete this.onLoadComplete;
|
||||
}
|
||||
if (this.onModelOverlayLoaded) {
|
||||
this.modelOverlayLoaded.disconnect(this, 'onModelOverlayLoaded');
|
||||
}
|
||||
if (this.overlayID) {
|
||||
Overlays.deleteOverlay(this.overlayID);
|
||||
this.overlayID = undefined;
|
||||
}
|
||||
if (this.active) {
|
||||
this.activeChanged(this.active = false, reason);
|
||||
} else if (reason) {
|
||||
log('already stopped so not triggering another activeChanged; latest reason was:', reason);
|
||||
}
|
||||
},
|
||||
|
||||
// @public @method - Reposition the doppleganger so it sees "eye to eye" with the Avatar.
|
||||
// @param {String} [byJointName=Hips] - the reference joint used to align the Doppleganger and Avatar
|
||||
syncVerticalPosition: function(byJointName) {
|
||||
byJointName = byJointName || 'Hips';
|
||||
var names = Overlays.getProperty(this.overlayID, 'jointNames'),
|
||||
positions = Overlays.getProperty(this.overlayID, 'jointPositions'),
|
||||
dopplePosition = Overlays.getProperty(this.overlayID, 'position'),
|
||||
doppleJointIndex = names.indexOf(byJointName),
|
||||
doppleJointPosition = positions[doppleJointIndex];
|
||||
|
||||
var avatarPosition = this.avatar.position,
|
||||
avatarJointIndex = this.avatar.getJointIndex(byJointName),
|
||||
avatarJointPosition = this.avatar.getJointPosition(avatarJointIndex);
|
||||
|
||||
var offset = avatarJointPosition.y - doppleJointPosition.y;
|
||||
log('adjusting for offset', offset);
|
||||
dopplePosition.y = avatarPosition.y + offset;
|
||||
this.position = dopplePosition;
|
||||
Overlays.editOverlay(this.overlayID, { position: this.position });
|
||||
},
|
||||
|
||||
// @private @method - creates the update thread to synchronize joint data
|
||||
_createUpdateThread: function() {
|
||||
if (Doppleganger.USE_SCRIPT_UPDATE) {
|
||||
log('creating Script.update thread');
|
||||
this.onUpdate = this.update;
|
||||
Script.update.connect(this, 'onUpdate');
|
||||
} else {
|
||||
log('creating Script.setInterval thread @ ~', Doppleganger.TARGET_FPS +'fps');
|
||||
var timeout = 1000 / Doppleganger.TARGET_FPS;
|
||||
this._interval = Script.setInterval(bind(this, 'update'), timeout);
|
||||
}
|
||||
},
|
||||
|
||||
// @private @method - waits for model to load and handles timeouts
|
||||
// @param {ModelResource} resource - a prefetched resource to monitor loading state against
|
||||
_waitForModel: function(resource) {
|
||||
var RECHECK_MS = 50;
|
||||
var id = this.overlayID,
|
||||
watchdogTimer = null;
|
||||
|
||||
function waitForJointNames() {
|
||||
var error = null, result = null;
|
||||
if (!watchdogTimer) {
|
||||
error = 'joints_unavailable';
|
||||
} else if (resource.state === Resource.State.FAILED) {
|
||||
error = 'prefetch_failed';
|
||||
} else if (resource.state === Resource.State.FINISHED) {
|
||||
var names = Overlays.getProperty(id, 'jointNames');
|
||||
if (Array.isArray(names) && names.length) {
|
||||
result = { overlayID: id, jointNames: names };
|
||||
}
|
||||
}
|
||||
if (error || result !== null) {
|
||||
Script.clearInterval(this._interval);
|
||||
this._interval = null;
|
||||
if (watchdogTimer) {
|
||||
Script.clearTimeout(watchdogTimer);
|
||||
}
|
||||
this.modelOverlayLoaded(error, result);
|
||||
}
|
||||
}
|
||||
watchdogTimer = Script.setTimeout(function() {
|
||||
watchdogTimer = null;
|
||||
}, Doppleganger.MAX_WAIT_SECS * 1000);
|
||||
this._interval = Script.setInterval(bind(this, waitForJointNames), RECHECK_MS);
|
||||
}
|
||||
};
|
||||
|
||||
// @function - bind a function to a `this` context
|
||||
// @param {Object} - the `this` context
|
||||
// @param {Function|String} - function or method name
|
||||
function bind(thiz, method) {
|
||||
method = thiz[method] || method;
|
||||
return function() {
|
||||
return method.apply(thiz, arguments);
|
||||
};
|
||||
}
|
||||
|
||||
// @function - Qt signal polyfill
|
||||
function signal(template) {
|
||||
var callbacks = [];
|
||||
return Object.defineProperties(function() {
|
||||
var args = [].slice.call(arguments);
|
||||
callbacks.forEach(function(obj) {
|
||||
obj.handler.apply(obj.scope, args);
|
||||
});
|
||||
}, {
|
||||
connect: { value: function(scope, handler) {
|
||||
callbacks.push({scope: scope, handler: scope[handler] || handler || scope});
|
||||
}},
|
||||
disconnect: { value: function(scope, handler) {
|
||||
var match = {scope: scope, handler: scope[handler] || handler || scope};
|
||||
callbacks = callbacks.filter(function(obj) {
|
||||
return !(obj.scope === match.scope && obj.handler === match.handler);
|
||||
});
|
||||
}}
|
||||
});
|
||||
}
|
||||
|
||||
// @function - debug logging
|
||||
function log() {
|
||||
print('doppleganger | ' + [].slice.call(arguments).join(' '));
|
||||
}
|
||||
|
||||
// -- ADVANCED DEBUGGING --
|
||||
// @function - Add debug joint indicators / extra debugging info.
|
||||
// @param {Doppleganger} - existing Doppleganger instance to add controls to
|
||||
//
|
||||
// @note:
|
||||
// * rightclick toggles mirror mode on/off
|
||||
// * shift-rightclick toggles the debug indicators on/off
|
||||
// * clicking on an indicator displays the joint name and mirrored joint name in the debug log.
|
||||
//
|
||||
// Example use:
|
||||
// var doppleganger = new Doppleganger();
|
||||
// Doppleganger.addDebugControls(doppleganger);
|
||||
Doppleganger.addDebugControls = function(doppleganger) {
|
||||
DebugControls.COLOR_DEFAULT = { red: 255, blue: 255, green: 255 };
|
||||
DebugControls.COLOR_SELECTED = { red: 0, blue: 255, green: 0 };
|
||||
|
||||
function DebugControls() {
|
||||
this.enableIndicators = true;
|
||||
this.selectedJointName = null;
|
||||
this.debugOverlayIDs = undefined;
|
||||
this.jointSelected = signal(function(result) {});
|
||||
}
|
||||
DebugControls.prototype = {
|
||||
start: function() {
|
||||
if (!this.onMousePressEvent) {
|
||||
this.onMousePressEvent = this._onMousePressEvent;
|
||||
Controller.mousePressEvent.connect(this, 'onMousePressEvent');
|
||||
}
|
||||
},
|
||||
|
||||
stop: function() {
|
||||
this.removeIndicators();
|
||||
if (this.onMousePressEvent) {
|
||||
Controller.mousePressEvent.disconnect(this, 'onMousePressEvent');
|
||||
delete this.onMousePressEvent;
|
||||
}
|
||||
},
|
||||
|
||||
createIndicators: function(jointNames) {
|
||||
this.jointNames = jointNames;
|
||||
return jointNames.map(function(name, i) {
|
||||
return Overlays.addOverlay('shape', {
|
||||
shape: 'Icosahedron',
|
||||
scale: 0.1,
|
||||
solid: false,
|
||||
alpha: 0.5
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
removeIndicators: function() {
|
||||
if (this.debugOverlayIDs) {
|
||||
this.debugOverlayIDs.forEach(Overlays.deleteOverlay);
|
||||
this.debugOverlayIDs = undefined;
|
||||
}
|
||||
},
|
||||
|
||||
onJointsUpdated: function(overlayID) {
|
||||
if (!this.enableIndicators) {
|
||||
return;
|
||||
}
|
||||
var jointNames = Overlays.getProperty(overlayID, 'jointNames'),
|
||||
jointOrientations = Overlays.getProperty(overlayID, 'jointOrientations'),
|
||||
jointPositions = Overlays.getProperty(overlayID, 'jointPositions'),
|
||||
selectedIndex = jointNames.indexOf(this.selectedJointName);
|
||||
|
||||
if (!this.debugOverlayIDs) {
|
||||
this.debugOverlayIDs = this.createIndicators(jointNames);
|
||||
}
|
||||
|
||||
// batch all updates into a single call (using the editOverlays({ id: {props...}, ... }) API)
|
||||
var updatedOverlays = this.debugOverlayIDs.reduce(function(updates, id, i) {
|
||||
updates[id] = {
|
||||
position: jointPositions[i],
|
||||
rotation: jointOrientations[i],
|
||||
color: i === selectedIndex ? DebugControls.COLOR_SELECTED : DebugControls.COLOR_DEFAULT,
|
||||
solid: i === selectedIndex
|
||||
};
|
||||
return updates;
|
||||
}, {});
|
||||
Overlays.editOverlays(updatedOverlays);
|
||||
},
|
||||
|
||||
_onMousePressEvent: function(evt) {
|
||||
if (!evt.isLeftButton || !this.enableIndicators || !this.debugOverlayIDs) {
|
||||
return;
|
||||
}
|
||||
var ray = Camera.computePickRay(evt.x, evt.y),
|
||||
hit = Overlays.findRayIntersection(ray, true, this.debugOverlayIDs);
|
||||
|
||||
hit.jointIndex = this.debugOverlayIDs.indexOf(hit.overlayID);
|
||||
hit.jointName = this.jointNames[hit.jointIndex];
|
||||
this.jointSelected(hit);
|
||||
}
|
||||
};
|
||||
|
||||
if ('$debugControls' in doppleganger) {
|
||||
throw new Error('only one set of debug controls can be added per doppleganger');
|
||||
}
|
||||
var debugControls = new DebugControls();
|
||||
doppleganger.$debugControls = debugControls;
|
||||
|
||||
function onMousePressEvent(evt) {
|
||||
if (evt.isRightButton) {
|
||||
if (evt.isShifted) {
|
||||
debugControls.enableIndicators = !debugControls.enableIndicators;
|
||||
if (!debugControls.enableIndicators) {
|
||||
debugControls.removeIndicators();
|
||||
}
|
||||
} else {
|
||||
doppleganger.mirrored = !doppleganger.mirrored;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
doppleganger.activeChanged.connect(function(active) {
|
||||
if (active) {
|
||||
debugControls.start();
|
||||
doppleganger.jointsUpdated.connect(debugControls, 'onJointsUpdated');
|
||||
Controller.mousePressEvent.connect(onMousePressEvent);
|
||||
} else {
|
||||
Controller.mousePressEvent.disconnect(onMousePressEvent);
|
||||
doppleganger.jointsUpdated.disconnect(debugControls, 'onJointsUpdated');
|
||||
debugControls.stop();
|
||||
}
|
||||
});
|
||||
|
||||
debugControls.jointSelected.connect(function(hit) {
|
||||
debugControls.selectedJointName = hit.jointName;
|
||||
if (hit.jointIndex < 0) {
|
||||
return;
|
||||
}
|
||||
hit.mirroredJointName = Doppleganger.getMirroredJointNames([hit.jointName])[0];
|
||||
log('selected joint:', JSON.stringify(hit, 0, 2));
|
||||
});
|
||||
|
||||
Script.scriptEnding.connect(debugControls, 'removeIndicators');
|
||||
|
||||
return doppleganger;
|
||||
};
|
|
@ -12,7 +12,8 @@
|
|||
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
|
||||
//
|
||||
|
||||
/* global Script, SelectionDisplay, LightOverlayManager, CameraManager, Grid, GridTool, EntityListTool, Vec3, SelectionManager, Overlays, OverlayWebWindow, UserActivityLogger, Settings, Entities, Tablet, Toolbars, Messages, Menu, Camera, progressDialog, tooltip, MyAvatar, Quat, Controller, Clipboard, HMD, UndoStack, ParticleExplorerTool */
|
||||
/* global Script, SelectionDisplay, LightOverlayManager, CameraManager, Grid, GridTool, EntityListTool, Vec3, SelectionManager, Overlays, OverlayWebWindow, UserActivityLogger,
|
||||
Settings, Entities, Tablet, Toolbars, Messages, Menu, Camera, progressDialog, tooltip, MyAvatar, Quat, Controller, Clipboard, HMD, UndoStack, ParticleExplorerTool */
|
||||
|
||||
(function() { // BEGIN LOCAL_SCOPE
|
||||
|
||||
|
@ -96,6 +97,10 @@ selectionManager.addEventListener(function () {
|
|||
particleExplorerTool.webView.emitScriptEvent(JSON.stringify(particleData));
|
||||
}
|
||||
});
|
||||
|
||||
// Switch to particle explorer
|
||||
var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system");
|
||||
tablet.sendToQml({method: 'selectTab', params: {id: 'particle'}});
|
||||
} else {
|
||||
needToDestroyParticleExplorer = true;
|
||||
}
|
||||
|
@ -213,6 +218,8 @@ function hideMarketplace() {
|
|||
// }
|
||||
|
||||
var TOOLS_PATH = Script.resolvePath("assets/images/tools/");
|
||||
var GRABBABLE_ENTITIES_MENU_CATEGORY = "Edit";
|
||||
var GRABBABLE_ENTITIES_MENU_ITEM = "Create Entities As Grabbable";
|
||||
|
||||
var toolBar = (function () {
|
||||
var EDIT_SETTING = "io.highfidelity.isEditting"; // for communication with other scripts
|
||||
|
@ -227,8 +234,11 @@ var toolBar = (function () {
|
|||
var position = getPositionToCreateEntity();
|
||||
var entityID = null;
|
||||
if (position !== null && position !== undefined) {
|
||||
position = grid.snapToSurface(grid.snapToGrid(position, false, dimensions), dimensions),
|
||||
properties.position = position;
|
||||
position = grid.snapToSurface(grid.snapToGrid(position, false, dimensions), dimensions);
|
||||
properties.position = position;
|
||||
if (Menu.isOptionChecked(GRABBABLE_ENTITIES_MENU_ITEM)) {
|
||||
properties.userData = JSON.stringify({ grabbableKey: { grabbable: true } });
|
||||
}
|
||||
entityID = Entities.addEntity(properties);
|
||||
if (properties.type == "ParticleEffect") {
|
||||
selectParticleEntity(entityID);
|
||||
|
@ -253,6 +263,7 @@ var toolBar = (function () {
|
|||
if (systemToolbar) {
|
||||
systemToolbar.removeButton(EDIT_TOGGLE_BUTTON);
|
||||
}
|
||||
Menu.removeMenuItem(GRABBABLE_ENTITIES_MENU_CATEGORY, GRABBABLE_ENTITIES_MENU_ITEM);
|
||||
}
|
||||
|
||||
var buttonHandlers = {}; // only used to tablet mode
|
||||
|
@ -638,6 +649,27 @@ function findClickedEntity(event) {
|
|||
};
|
||||
}
|
||||
|
||||
// Handles selections on overlays while in edit mode by querying entities from
|
||||
// entityIconOverlayManager.
|
||||
function handleOverlaySelectionToolUpdates(channel, message, sender) {
|
||||
if (sender !== MyAvatar.sessionUUID || channel !== 'entityToolUpdates')
|
||||
return;
|
||||
|
||||
var data = JSON.parse(message);
|
||||
|
||||
if (data.method === "selectOverlay") {
|
||||
print("setting selection to overlay " + data.overlayID);
|
||||
var entity = entityIconOverlayManager.findEntity(data.overlayID);
|
||||
|
||||
if (entity !== null) {
|
||||
selectionManager.setSelections([entity]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Messages.subscribe("entityToolUpdates");
|
||||
Messages.messageReceived.connect(handleOverlaySelectionToolUpdates);
|
||||
|
||||
var mouseHasMovedSincePress = false;
|
||||
var mousePressStartTime = 0;
|
||||
var mousePressStartPosition = {
|
||||
|
@ -903,11 +935,21 @@ function setupModelMenus() {
|
|||
afterItem: "Parent Entity to Last",
|
||||
grouping: "Advanced"
|
||||
});
|
||||
|
||||
Menu.addMenuItem({
|
||||
menuName: GRABBABLE_ENTITIES_MENU_CATEGORY,
|
||||
menuItemName: GRABBABLE_ENTITIES_MENU_ITEM,
|
||||
afterItem: "Unparent Entity",
|
||||
isCheckable: true,
|
||||
isChecked: true,
|
||||
grouping: "Advanced"
|
||||
});
|
||||
|
||||
Menu.addMenuItem({
|
||||
menuName: "Edit",
|
||||
menuItemName: "Allow Selecting of Large Models",
|
||||
shortcutKey: "CTRL+META+L",
|
||||
afterItem: "Unparent Entity",
|
||||
afterItem: GRABBABLE_ENTITIES_MENU_ITEM,
|
||||
isCheckable: true,
|
||||
isChecked: true,
|
||||
grouping: "Advanced"
|
||||
|
@ -1047,6 +1089,13 @@ Script.scriptEnding.connect(function () {
|
|||
|
||||
Controller.keyReleaseEvent.disconnect(keyReleaseEvent);
|
||||
Controller.keyPressEvent.disconnect(keyPressEvent);
|
||||
|
||||
Controller.mousePressEvent.disconnect(mousePressEvent);
|
||||
Controller.mouseMoveEvent.disconnect(mouseMoveEventBuffered);
|
||||
Controller.mouseReleaseEvent.disconnect(mouseReleaseEvent);
|
||||
|
||||
Messages.messageReceived.disconnect(handleOverlaySelectionToolUpdates);
|
||||
Messages.unsubscribe("entityToolUpdates");
|
||||
});
|
||||
|
||||
var lastOrientation = null;
|
||||
|
@ -2013,7 +2062,11 @@ function selectParticleEntity(entityID) {
|
|||
|
||||
selectedParticleEntity = entityID;
|
||||
particleExplorerTool.setActiveParticleEntity(entityID);
|
||||
particleExplorerTool.webView.emitScriptEvent(JSON.stringify(particleData));
|
||||
particleExplorerTool.webView.emitScriptEvent(JSON.stringify(particleData));
|
||||
|
||||
// Switch to particle explorer
|
||||
var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system");
|
||||
tablet.sendToQml({method: 'selectTab', params: {id: 'particle'}});
|
||||
}
|
||||
|
||||
entityListTool.webView.webEventReceived.connect(function (data) {
|
||||
|
|
218
scripts/system/html/css/record.css
Normal file
218
scripts/system/html/css/record.css
Normal file
|
@ -0,0 +1,218 @@
|
|||
/*
|
||||
// record.css
|
||||
//
|
||||
// Created by David Rowe on 5 Apr 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
|
||||
*/
|
||||
|
||||
|
||||
body {
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.title {
|
||||
padding-left: 21px;
|
||||
}
|
||||
|
||||
.title label {
|
||||
font-size: 18px;
|
||||
position: relative;
|
||||
top: 12px;
|
||||
}
|
||||
|
||||
|
||||
#recordings {
|
||||
height: 100%;
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
padding: 51px 0 185px 0;
|
||||
margin: 0 21px 0 21px;
|
||||
}
|
||||
|
||||
#recordings #table-container {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
box-sizing: border-box;
|
||||
border-left: 2px solid #575757;
|
||||
border-right: 2px solid #575757;
|
||||
background-color: #2e2e2e;
|
||||
}
|
||||
|
||||
#recordings table {
|
||||
border: none;
|
||||
}
|
||||
|
||||
#recordings thead {
|
||||
position: absolute;
|
||||
top: 21px;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
border: 2px solid #575757;
|
||||
border-top-left-radius: 7px;
|
||||
border-top-right-radius: 7px;
|
||||
border-bottom: 1px solid #575757;
|
||||
position: absolute;
|
||||
word-wrap: nowrap;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#recordings table col#unload-column {
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
#recordings thead th:last-child {
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
#recordings table td {
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
#recordings table td:nth-child(2) {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#recordings tbody tr.filler td {
|
||||
height: auto;
|
||||
border-top: 1px solid #1c1c1c;
|
||||
}
|
||||
|
||||
#recordings-list input {
|
||||
height: 22px;
|
||||
width: 22px;
|
||||
min-width: 22px;
|
||||
font-size: 16px;
|
||||
padding: 0 1px 0 0;
|
||||
}
|
||||
|
||||
#recordings tfoot {
|
||||
position: absolute;
|
||||
bottom: 159px;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
border: 2px solid #575757;
|
||||
border-bottom-left-radius: 7px;
|
||||
border-bottom-right-radius: 7px;
|
||||
border-top: 1px solid #575757;
|
||||
}
|
||||
|
||||
#recordings tfoot tr, #recordings tfoot td {
|
||||
background: none;
|
||||
}
|
||||
|
||||
|
||||
#spinner {
|
||||
text-align: center;
|
||||
margin-top: 25%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
#spinner span {
|
||||
display: block;
|
||||
position: relative;
|
||||
top: -101px;
|
||||
color: #e2334d;
|
||||
font-size: 60px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
|
||||
#recordings tfoot tr {
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
|
||||
#instructions td {
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
#instructions h1 {
|
||||
font-size: 16px;
|
||||
margin-top: 28px;
|
||||
}
|
||||
|
||||
#instructions h1 + p {
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
#instructions p, #instructions ul {
|
||||
margin-top: 21px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
#instructions p {
|
||||
font-family: Raleway-Bold;
|
||||
}
|
||||
|
||||
#instructions ul {
|
||||
font-family: Raleway-SemiBold;
|
||||
margin-left: 21px;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
#instructions li {
|
||||
margin-top: 7px;
|
||||
}
|
||||
|
||||
#instructions ul input {
|
||||
margin-left: 1px;
|
||||
margin-top: 6px;
|
||||
font-size: 14px;
|
||||
padding: 0 7px;
|
||||
}
|
||||
|
||||
|
||||
#show-info-button {
|
||||
font-family: HiFi-Glyphs;
|
||||
font-size: 32px;
|
||||
height: 16px;
|
||||
line-height: 16px;
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
top: 15px;
|
||||
right: 5px;
|
||||
margin-top: -11px;
|
||||
margin-left: 7px;
|
||||
}
|
||||
|
||||
#show-info-button:hover {
|
||||
color: #00b4ef;
|
||||
}
|
||||
|
||||
|
||||
#record-controls {
|
||||
position: absolute;
|
||||
bottom: 7px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#record-controls #load-container {
|
||||
position: absolute;
|
||||
left: 21px;
|
||||
}
|
||||
|
||||
#record-controls #record-container {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#record-controls #checkbox-container {
|
||||
margin-top: 31px;
|
||||
}
|
||||
|
||||
#record-controls div.property {
|
||||
padding-left: 21px;
|
||||
}
|
||||
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
298
scripts/system/html/js/record.js
Normal file
298
scripts/system/html/js/record.js
Normal file
|
@ -0,0 +1,298 @@
|
|||
"use strict";
|
||||
|
||||
//
|
||||
// record.js
|
||||
//
|
||||
// Created by David Rowe on 5 Apr 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
|
||||
//
|
||||
|
||||
var isUsingToolbar = false,
|
||||
isDisplayingInstructions = false,
|
||||
isRecording = false,
|
||||
numberOfPlayers = 0,
|
||||
recordingsBeingPlayed = [],
|
||||
elRecordings,
|
||||
elRecordingsTable,
|
||||
elRecordingsList,
|
||||
elInstructions,
|
||||
elPlayersUnused,
|
||||
elHideInfoButton,
|
||||
elShowInfoButton,
|
||||
elLoadButton,
|
||||
elSpinner,
|
||||
elCountdownNumber,
|
||||
elRecordButton,
|
||||
elFinishOnOpen,
|
||||
elFinishOnOpenLabel,
|
||||
EVENT_BRIDGE_TYPE = "record",
|
||||
BODY_LOADED_ACTION = "bodyLoaded",
|
||||
USING_TOOLBAR_ACTION = "usingToolbar",
|
||||
RECORDINGS_BEING_PLAYED_ACTION = "recordingsBeingPlayed",
|
||||
NUMBER_OF_PLAYERS_ACTION = "numberOfPlayers",
|
||||
STOP_PLAYING_RECORDING_ACTION = "stopPlayingRecording",
|
||||
LOAD_RECORDING_ACTION = "loadRecording",
|
||||
START_RECORDING_ACTION = "startRecording",
|
||||
SET_COUNTDOWN_NUMBER_ACTION = "setCountdownNumber",
|
||||
STOP_RECORDING_ACTION = "stopRecording",
|
||||
FINISH_ON_OPEN_ACTION = "finishOnOpen";
|
||||
|
||||
function stopPlayingRecording(event) {
|
||||
var playerID = event.target.getAttribute("playerID");
|
||||
EventBridge.emitWebEvent(JSON.stringify({
|
||||
type: EVENT_BRIDGE_TYPE,
|
||||
action: STOP_PLAYING_RECORDING_ACTION,
|
||||
value: playerID
|
||||
}));
|
||||
}
|
||||
|
||||
function updatePlayersUnused() {
|
||||
elPlayersUnused.innerHTML = numberOfPlayers - recordingsBeingPlayed.length;
|
||||
}
|
||||
|
||||
function orderRecording(a, b) {
|
||||
return a.filename > b.filename ? 1 : -1;
|
||||
}
|
||||
|
||||
function updateRecordings() {
|
||||
var tbody,
|
||||
tr,
|
||||
td,
|
||||
input,
|
||||
ths,
|
||||
tds,
|
||||
length,
|
||||
i,
|
||||
HIFI_GLYPH_CLOSE = "w";
|
||||
|
||||
recordingsBeingPlayed.sort(orderRecording);
|
||||
|
||||
tbody = document.createElement("tbody");
|
||||
tbody.id = "recordings-list";
|
||||
|
||||
|
||||
// <tr><td>Filename</td><td><input type="button" class="glyph red" value="w" playerID=id /></td></tr>
|
||||
for (i = 0, length = recordingsBeingPlayed.length; i < length; i += 1) {
|
||||
tr = document.createElement("tr");
|
||||
td = document.createElement("td");
|
||||
td.innerHTML = recordingsBeingPlayed[i].filename.slice(4);
|
||||
tr.appendChild(td);
|
||||
td = document.createElement("td");
|
||||
input = document.createElement("input");
|
||||
input.setAttribute("type", "button");
|
||||
input.setAttribute("class", "glyph red");
|
||||
input.setAttribute("value", HIFI_GLYPH_CLOSE);
|
||||
input.setAttribute("playerID", recordingsBeingPlayed[i].playerID);
|
||||
input.addEventListener("click", stopPlayingRecording);
|
||||
td.appendChild(input);
|
||||
tr.appendChild(td);
|
||||
tbody.appendChild(tr);
|
||||
}
|
||||
|
||||
// Empty rows representing available players.
|
||||
for (i = recordingsBeingPlayed.length, length = numberOfPlayers; i < length; i += 1) {
|
||||
tr = document.createElement("tr");
|
||||
td = document.createElement("td");
|
||||
td.colSpan = 2;
|
||||
tr.appendChild(td);
|
||||
tbody.appendChild(tr);
|
||||
}
|
||||
|
||||
// Filler row for extra table space.
|
||||
tr = document.createElement("tr");
|
||||
tr.classList.add("filler");
|
||||
td = document.createElement("td");
|
||||
td.colSpan = 2;
|
||||
tr.appendChild(td);
|
||||
tbody.appendChild(tr);
|
||||
|
||||
// Update table content.
|
||||
elRecordingsTable.replaceChild(tbody, elRecordingsList);
|
||||
elRecordingsList = document.getElementById("recordings-list");
|
||||
|
||||
// Update header cell widths to match content widths.
|
||||
ths = document.querySelectorAll("#recordings-table thead th");
|
||||
tds = document.querySelectorAll("#recordings-table tbody tr:first-child td");
|
||||
for (i = 0; i < ths.length; i += 1) {
|
||||
ths[i].width = tds[i].offsetWidth;
|
||||
}
|
||||
}
|
||||
|
||||
function updateInstructions() {
|
||||
// Display show/hide instructions buttons if players are available.
|
||||
if (numberOfPlayers === 0) {
|
||||
elHideInfoButton.classList.add("hidden");
|
||||
elShowInfoButton.classList.add("hidden");
|
||||
} else {
|
||||
elHideInfoButton.classList.remove("hidden");
|
||||
elShowInfoButton.classList.remove("hidden");
|
||||
}
|
||||
|
||||
// Display instructions if user requested or no players available.
|
||||
if (isDisplayingInstructions || numberOfPlayers === 0) {
|
||||
elRecordingsList.classList.add("hidden");
|
||||
elInstructions.classList.remove("hidden");
|
||||
} else {
|
||||
elInstructions.classList.add("hidden");
|
||||
elRecordingsList.classList.remove("hidden");
|
||||
}
|
||||
}
|
||||
|
||||
function showInstructions() {
|
||||
isDisplayingInstructions = true;
|
||||
updateInstructions();
|
||||
}
|
||||
|
||||
function hideInstructions() {
|
||||
isDisplayingInstructions = false;
|
||||
updateInstructions();
|
||||
}
|
||||
|
||||
function updateLoadButton() {
|
||||
if (isRecording || numberOfPlayers <= recordingsBeingPlayed.length) {
|
||||
elLoadButton.setAttribute("disabled", "disabled");
|
||||
} else {
|
||||
elLoadButton.removeAttribute("disabled");
|
||||
}
|
||||
}
|
||||
|
||||
function updateSpinner() {
|
||||
if (isRecording) {
|
||||
elRecordings.classList.add("hidden");
|
||||
elSpinner.classList.remove("hidden");
|
||||
} else {
|
||||
elSpinner.classList.add("hidden");
|
||||
elRecordings.classList.remove("hidden");
|
||||
}
|
||||
}
|
||||
|
||||
function updateFinishOnOpenLabel() {
|
||||
var WINDOW_FINISH_ON_OPEN_LABEL = "Stop recording automatically when reopen this window",
|
||||
TABLET_FINISH_ON_OPEN_LABEL = "Stop recording automatically when reopen tablet or window";
|
||||
|
||||
elFinishOnOpenLabel.innerHTML = isUsingToolbar ? WINDOW_FINISH_ON_OPEN_LABEL : TABLET_FINISH_ON_OPEN_LABEL;
|
||||
}
|
||||
|
||||
function onScriptEventReceived(data) {
|
||||
var message = JSON.parse(data);
|
||||
if (message.type === EVENT_BRIDGE_TYPE) {
|
||||
switch (message.action) {
|
||||
case USING_TOOLBAR_ACTION:
|
||||
isUsingToolbar = message.value;
|
||||
updateFinishOnOpenLabel();
|
||||
break;
|
||||
case FINISH_ON_OPEN_ACTION:
|
||||
elFinishOnOpen.checked = message.value;
|
||||
break;
|
||||
case START_RECORDING_ACTION:
|
||||
isRecording = true;
|
||||
elRecordButton.value = "Stop";
|
||||
updateSpinner();
|
||||
updateLoadButton();
|
||||
break;
|
||||
case SET_COUNTDOWN_NUMBER_ACTION:
|
||||
elCountdownNumber.innerHTML = message.value;
|
||||
break;
|
||||
case STOP_RECORDING_ACTION:
|
||||
isRecording = false;
|
||||
elRecordButton.value = "Record";
|
||||
updateSpinner();
|
||||
updateLoadButton();
|
||||
break;
|
||||
case RECORDINGS_BEING_PLAYED_ACTION:
|
||||
recordingsBeingPlayed = JSON.parse(message.value);
|
||||
updateRecordings();
|
||||
updatePlayersUnused();
|
||||
updateInstructions();
|
||||
updateLoadButton();
|
||||
break;
|
||||
case NUMBER_OF_PLAYERS_ACTION:
|
||||
numberOfPlayers = message.value;
|
||||
updateRecordings();
|
||||
updatePlayersUnused();
|
||||
updateInstructions();
|
||||
updateLoadButton();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function onLoadButtonClicked() {
|
||||
EventBridge.emitWebEvent(JSON.stringify({
|
||||
type: EVENT_BRIDGE_TYPE,
|
||||
action: LOAD_RECORDING_ACTION
|
||||
}));
|
||||
}
|
||||
|
||||
function onRecordButtonClicked() {
|
||||
if (!isRecording) {
|
||||
elRecordButton.value = "Stop";
|
||||
EventBridge.emitWebEvent(JSON.stringify({
|
||||
type: EVENT_BRIDGE_TYPE,
|
||||
action: START_RECORDING_ACTION
|
||||
}));
|
||||
isRecording = true;
|
||||
updateSpinner();
|
||||
updateLoadButton();
|
||||
} else {
|
||||
elRecordButton.value = "Record";
|
||||
EventBridge.emitWebEvent(JSON.stringify({
|
||||
type: EVENT_BRIDGE_TYPE,
|
||||
action: STOP_RECORDING_ACTION
|
||||
}));
|
||||
isRecording = false;
|
||||
updateSpinner();
|
||||
updateLoadButton();
|
||||
}
|
||||
}
|
||||
|
||||
function onFinishOnOpenClicked() {
|
||||
EventBridge.emitWebEvent(JSON.stringify({
|
||||
type: EVENT_BRIDGE_TYPE,
|
||||
action: FINISH_ON_OPEN_ACTION,
|
||||
value: elFinishOnOpen.checked
|
||||
}));
|
||||
}
|
||||
|
||||
function signalBodyLoaded() {
|
||||
EventBridge.emitWebEvent(JSON.stringify({
|
||||
type: EVENT_BRIDGE_TYPE,
|
||||
action: BODY_LOADED_ACTION
|
||||
}));
|
||||
}
|
||||
|
||||
function onBodyLoaded() {
|
||||
|
||||
EventBridge.scriptEventReceived.connect(onScriptEventReceived);
|
||||
|
||||
elRecordings = document.getElementById("recordings");
|
||||
|
||||
elRecordingsTable = document.getElementById("recordings-table");
|
||||
elRecordingsList = document.getElementById("recordings-list");
|
||||
elInstructions = document.getElementById("instructions");
|
||||
elPlayersUnused = document.getElementById("players-unused");
|
||||
|
||||
elHideInfoButton = document.getElementById("hide-info-button");
|
||||
elHideInfoButton.onclick = hideInstructions;
|
||||
elShowInfoButton = document.getElementById("show-info-button");
|
||||
elShowInfoButton.onclick = showInstructions;
|
||||
|
||||
elLoadButton = document.getElementById("load-button");
|
||||
elLoadButton.onclick = onLoadButtonClicked;
|
||||
|
||||
elSpinner = document.getElementById("spinner");
|
||||
elCountdownNumber = document.getElementById("countdown-number");
|
||||
|
||||
elRecordButton = document.getElementById("record-button");
|
||||
elRecordButton.onclick = onRecordButtonClicked;
|
||||
|
||||
elFinishOnOpen = document.getElementById("finish-on-open");
|
||||
elFinishOnOpen.onclick = onFinishOnOpenClicked;
|
||||
|
||||
elFinishOnOpenLabel = document.getElementById("finish-on-open-label");
|
||||
|
||||
signalBodyLoaded();
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue