Merge branch 'master' into fix_stored_size

This commit is contained in:
Brad Davis 2017-04-26 19:08:04 -07:00 committed by GitHub
commit 7be40a48a1
118 changed files with 7432 additions and 1889 deletions

View file

@ -34,6 +34,7 @@ module.exports = {
"Quat": false,
"Rates": false,
"Recording": false,
"Resource": false,
"Reticle": false,
"Scene": false,
"Script": false,

View file

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

View file

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

View file

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

View file

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

View 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

View 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

View 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

View 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

View file

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

View 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
}
}

View 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
}
}
}
}

View file

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

View file

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

View 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));
}
}
}

View 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";
}
}
}

View file

@ -23,7 +23,7 @@ StackView {
signal sendToScript(var message);
function pushSource(path) {
profileRoot.push(Qt.reslovedUrl(path));
profileRoot.push(Qt.resolvedUrl(path));
}
function popSource() {

View file

@ -23,7 +23,7 @@ StackView {
signal sendToScript(var message);
function pushSource(path) {
profileRoot.push(Qt.reslovedUrl(path));
profileRoot.push(Qt.resolvedUrl(path));
}
function popSource() {

View file

@ -23,7 +23,7 @@ StackView {
signal sendToScript(var message);
function pushSource(path) {
profileRoot.push(Qt.reslovedUrl(path));
profileRoot.push(Qt.resolvedUrl(path));
}
function popSource() {

View file

@ -23,7 +23,7 @@ StackView {
signal sendToScript(var message);
function pushSource(path) {
profileRoot.push(Qt.reslovedUrl(path));
profileRoot.push(Qt.resolvedUrl(path));
}
function popSource() {

View file

@ -23,7 +23,7 @@ StackView {
signal sendToScript(var message);
function pushSource(path) {
profileRoot.push(Qt.reslovedUrl(path));
profileRoot.push(Qt.resolvedUrl(path));
}
function popSource() {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View 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;
}
}
}
}

View 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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View 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

View file

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

View file

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

View file

@ -13,6 +13,7 @@
#include "AtpReply.h"
#include "NetworkAccessManager.h"
#include <QtNetwork/QNetworkProxy>
QThreadStorage<QNetworkAccessManager*> networkAccessManagers;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -214,7 +214,8 @@ enum class EntityQueryPacketVersion: PacketVersion {
};
enum class AssetServerPacketVersion: PacketVersion {
VegasCongestionControl = 19
VegasCongestionControl = 19,
RangeRequestSupport
};
enum class AvatarMixerPacketVersion : PacketVersion {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -68,7 +68,7 @@ StoragePointer FileStorage::create(const QString& filename, size_t size, const u
}
FileStorage::FileStorage(const QString& filename) : _file(filename) {
if (_file.open(QFile::ReadOnly)) {
if (_file.open(QFile::ReadWrite)) {
_mapped = _file.map(0, _file.size());
if (_mapped) {
_valid = true;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View 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();
});
}());

View 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 });
}
}
});

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

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

View 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;
};

View file

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

View 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;
}

View 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