mirror of
https://github.com/overte-org/overte.git
synced 2025-08-09 14:47:41 +02:00
Merge branch 'master' into file_dialog_crash_qt59
This commit is contained in:
commit
0d4328fa51
47 changed files with 1692 additions and 97 deletions
|
@ -229,7 +229,7 @@ void AvatarMixer::start() {
|
||||||
auto start = usecTimestampNow();
|
auto start = usecTimestampNow();
|
||||||
nodeList->nestedEach([&](NodeList::const_iterator cbegin, NodeList::const_iterator cend) {
|
nodeList->nestedEach([&](NodeList::const_iterator cbegin, NodeList::const_iterator cend) {
|
||||||
std::for_each(cbegin, cend, [&](const SharedNodePointer& node) {
|
std::for_each(cbegin, cend, [&](const SharedNodePointer& node) {
|
||||||
if (node->getType() == NodeType::Agent && !node->isUpstream()) {
|
if (node->getType() == NodeType::Agent) {
|
||||||
manageIdentityData(node);
|
manageIdentityData(node);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -325,8 +325,8 @@ void AvatarMixer::manageIdentityData(const SharedNodePointer& node) {
|
||||||
sendIdentity = true;
|
sendIdentity = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (sendIdentity) {
|
if (sendIdentity && !node->isUpstream()) {
|
||||||
|
sendIdentityPacket(nodeData, node); // Tell node whose name changed about its new session display name or avatar.
|
||||||
// since this packet includes a change to either the skeleton model URL or the display name
|
// since this packet includes a change to either the skeleton model URL or the display name
|
||||||
// it needs a new sequence number
|
// it needs a new sequence number
|
||||||
nodeData->getAvatar().pushIdentitySequenceNumber();
|
nodeData->getAvatar().pushIdentitySequenceNumber();
|
||||||
|
|
|
@ -81,7 +81,7 @@ int AvatarMixerSlave::sendIdentityPacket(const AvatarMixerClientData* nodeData,
|
||||||
|
|
||||||
int AvatarMixerSlave::sendReplicatedIdentityPacket(const Node& agentNode, const AvatarMixerClientData* nodeData, const Node& destinationNode) {
|
int AvatarMixerSlave::sendReplicatedIdentityPacket(const Node& agentNode, const AvatarMixerClientData* nodeData, const Node& destinationNode) {
|
||||||
if (AvatarMixer::shouldReplicateTo(agentNode, destinationNode)) {
|
if (AvatarMixer::shouldReplicateTo(agentNode, destinationNode)) {
|
||||||
QByteArray individualData = nodeData->getConstAvatarData()->identityByteArray();
|
QByteArray individualData = nodeData->getConstAvatarData()->identityByteArray(true);
|
||||||
individualData.replace(0, NUM_BYTES_RFC4122_UUID, nodeData->getNodeID().toRfc4122()); // FIXME, this looks suspicious
|
individualData.replace(0, NUM_BYTES_RFC4122_UUID, nodeData->getNodeID().toRfc4122()); // FIXME, this looks suspicious
|
||||||
auto identityPacket = NLPacketList::create(PacketType::ReplicatedAvatarIdentity, QByteArray(), true, true);
|
auto identityPacket = NLPacketList::create(PacketType::ReplicatedAvatarIdentity, QByteArray(), true, true);
|
||||||
identityPacket->write(individualData);
|
identityPacket->write(individualData);
|
||||||
|
|
|
@ -217,7 +217,7 @@ FocusScope {
|
||||||
anchors.leftMargin: hifi.dimensions.textPadding
|
anchors.leftMargin: hifi.dimensions.textPadding
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
id: popupText
|
id: popupText
|
||||||
text: listView.model[index] ? listView.model[index] : (listView.model.get(index).text ? listView.model.get(index).text : "")
|
text: listView.model[index] ? listView.model[index] : (listView.model.get && listView.model.get(index).text ? listView.model.get(index).text : "")
|
||||||
size: hifi.fontSizes.textFieldInput
|
size: hifi.fontSizes.textFieldInput
|
||||||
color: hifi.colors.baseGray
|
color: hifi.colors.baseGray
|
||||||
}
|
}
|
||||||
|
|
|
@ -43,6 +43,7 @@ Item {
|
||||||
property bool selected: false
|
property bool selected: false
|
||||||
property bool isAdmin: false
|
property bool isAdmin: false
|
||||||
property bool isPresent: true
|
property bool isPresent: true
|
||||||
|
property bool isReplicated: false
|
||||||
property string placeName: ""
|
property string placeName: ""
|
||||||
property string profilePicBorderColor: (connectionStatus == "connection" ? hifi.colors.indigoAccent : (connectionStatus == "friend" ? hifi.colors.greenHighlight : "transparent"))
|
property string profilePicBorderColor: (connectionStatus == "connection" ? hifi.colors.indigoAccent : (connectionStatus == "friend" ? hifi.colors.greenHighlight : "transparent"))
|
||||||
property alias avImage: avatarImage
|
property alias avImage: avatarImage
|
||||||
|
|
|
@ -473,6 +473,7 @@ Rectangle {
|
||||||
visible: !isCheckBox && !isButton && !isAvgAudio;
|
visible: !isCheckBox && !isButton && !isAvgAudio;
|
||||||
uuid: model ? model.sessionId : "";
|
uuid: model ? model.sessionId : "";
|
||||||
selected: styleData.selected;
|
selected: styleData.selected;
|
||||||
|
isReplicated: model.isReplicated;
|
||||||
isAdmin: model && model.admin;
|
isAdmin: model && model.admin;
|
||||||
isPresent: model && model.isPresent;
|
isPresent: model && model.isPresent;
|
||||||
// Size
|
// Size
|
||||||
|
@ -553,6 +554,7 @@ Rectangle {
|
||||||
id: actionButton;
|
id: actionButton;
|
||||||
color: 2; // Red
|
color: 2; // Red
|
||||||
visible: isButton;
|
visible: isButton;
|
||||||
|
enabled: !nameCard.isReplicated;
|
||||||
anchors.centerIn: parent;
|
anchors.centerIn: parent;
|
||||||
width: 32;
|
width: 32;
|
||||||
height: 32;
|
height: 32;
|
||||||
|
|
|
@ -23,11 +23,13 @@ import "../../../windows"
|
||||||
import "../../../dialogs/fileDialog"
|
import "../../../dialogs/fileDialog"
|
||||||
|
|
||||||
//FIXME implement shortcuts for favorite location
|
//FIXME implement shortcuts for favorite location
|
||||||
Item {
|
Rectangle {
|
||||||
id: root
|
id: root
|
||||||
anchors.top: parent.top
|
anchors.top: parent ? parent.top : undefined
|
||||||
HifiConstants { id: hifi }
|
HifiConstants { id: hifi }
|
||||||
|
|
||||||
|
color: hifi.colors.baseGray;
|
||||||
|
|
||||||
Settings {
|
Settings {
|
||||||
category: "FileDialog"
|
category: "FileDialog"
|
||||||
property alias width: root.width
|
property alias width: root.width
|
||||||
|
|
|
@ -37,8 +37,6 @@
|
||||||
#include <QtQml/QQmlEngine>
|
#include <QtQml/QQmlEngine>
|
||||||
#include <QtQuick/QQuickWindow>
|
#include <QtQuick/QQuickWindow>
|
||||||
|
|
||||||
#include <QtWebEngineWidgets/QWebEngineProfile>
|
|
||||||
|
|
||||||
#include <QtWidgets/QDesktopWidget>
|
#include <QtWidgets/QDesktopWidget>
|
||||||
#include <QtWidgets/QMessageBox>
|
#include <QtWidgets/QMessageBox>
|
||||||
|
|
||||||
|
@ -723,9 +721,13 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo
|
||||||
updateHeartbeat();
|
updateHeartbeat();
|
||||||
|
|
||||||
// setup a timer for domain-server check ins
|
// setup a timer for domain-server check ins
|
||||||
QTimer* domainCheckInTimer = new QTimer(nodeList.data());
|
QTimer* domainCheckInTimer = new QTimer(this);
|
||||||
connect(domainCheckInTimer, &QTimer::timeout, nodeList.data(), &NodeList::sendDomainServerCheckIn);
|
connect(domainCheckInTimer, &QTimer::timeout, nodeList.data(), &NodeList::sendDomainServerCheckIn);
|
||||||
domainCheckInTimer->start(DOMAIN_SERVER_CHECK_IN_MSECS);
|
domainCheckInTimer->start(DOMAIN_SERVER_CHECK_IN_MSECS);
|
||||||
|
connect(this, &QCoreApplication::aboutToQuit, [domainCheckInTimer] {
|
||||||
|
domainCheckInTimer->stop();
|
||||||
|
domainCheckInTimer->deleteLater();
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
auto audioIO = DependencyManager::get<AudioClient>();
|
auto audioIO = DependencyManager::get<AudioClient>();
|
||||||
|
|
|
@ -297,6 +297,7 @@ public:
|
||||||
void setAvatarOverrideUrl(const QUrl& url, bool save);
|
void setAvatarOverrideUrl(const QUrl& url, bool save);
|
||||||
QUrl getAvatarOverrideUrl() { return _avatarOverrideUrl; }
|
QUrl getAvatarOverrideUrl() { return _avatarOverrideUrl; }
|
||||||
bool getSaveAvatarOverrideUrl() { return _saveAvatarOverrideUrl; }
|
bool getSaveAvatarOverrideUrl() { return _saveAvatarOverrideUrl; }
|
||||||
|
void setCacheOverrideDir(const QString& dirName) { _cacheDir = dirName; }
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void svoImportRequested(const QString& url);
|
void svoImportRequested(const QString& url);
|
||||||
|
@ -688,5 +689,7 @@ private:
|
||||||
|
|
||||||
QUrl _avatarOverrideUrl;
|
QUrl _avatarOverrideUrl;
|
||||||
bool _saveAvatarOverrideUrl { false };
|
bool _saveAvatarOverrideUrl { false };
|
||||||
|
|
||||||
|
QString _cacheDir;
|
||||||
};
|
};
|
||||||
#endif // hifi_Application_h
|
#endif // hifi_Application_h
|
||||||
|
|
|
@ -23,6 +23,7 @@
|
||||||
#include <gl/OpenGLVersionChecker.h>
|
#include <gl/OpenGLVersionChecker.h>
|
||||||
#include <SandboxUtils.h>
|
#include <SandboxUtils.h>
|
||||||
#include <SharedUtil.h>
|
#include <SharedUtil.h>
|
||||||
|
#include <NetworkAccessManager.h>
|
||||||
|
|
||||||
#include "AddressManager.h"
|
#include "AddressManager.h"
|
||||||
#include "Application.h"
|
#include "Application.h"
|
||||||
|
@ -71,15 +72,17 @@ int main(int argc, const char* argv[]) {
|
||||||
QCommandLineOption runServerOption("runServer", "Whether to run the server");
|
QCommandLineOption runServerOption("runServer", "Whether to run the server");
|
||||||
QCommandLineOption serverContentPathOption("serverContentPath", "Where to find server content", "serverContentPath");
|
QCommandLineOption serverContentPathOption("serverContentPath", "Where to find server content", "serverContentPath");
|
||||||
QCommandLineOption allowMultipleInstancesOption("allowMultipleInstances", "Allow multiple instances to run");
|
QCommandLineOption allowMultipleInstancesOption("allowMultipleInstances", "Allow multiple instances to run");
|
||||||
|
QCommandLineOption overrideAppLocalDataPathOption("cache", "set test cache <dir>", "dir");
|
||||||
parser.addOption(urlOption);
|
parser.addOption(urlOption);
|
||||||
parser.addOption(noUpdaterOption);
|
parser.addOption(noUpdaterOption);
|
||||||
parser.addOption(checkMinSpecOption);
|
parser.addOption(checkMinSpecOption);
|
||||||
parser.addOption(runServerOption);
|
parser.addOption(runServerOption);
|
||||||
parser.addOption(serverContentPathOption);
|
parser.addOption(serverContentPathOption);
|
||||||
|
parser.addOption(overrideAppLocalDataPathOption);
|
||||||
parser.addOption(allowMultipleInstancesOption);
|
parser.addOption(allowMultipleInstancesOption);
|
||||||
parser.parse(arguments);
|
parser.parse(arguments);
|
||||||
|
|
||||||
|
|
||||||
const QString& applicationName = getInterfaceSharedMemoryName();
|
const QString& applicationName = getInterfaceSharedMemoryName();
|
||||||
bool instanceMightBeRunning = true;
|
bool instanceMightBeRunning = true;
|
||||||
#ifdef Q_OS_WIN
|
#ifdef Q_OS_WIN
|
||||||
|
@ -96,6 +99,19 @@ int main(int argc, const char* argv[]) {
|
||||||
if (allowMultipleInstances) {
|
if (allowMultipleInstances) {
|
||||||
instanceMightBeRunning = false;
|
instanceMightBeRunning = false;
|
||||||
}
|
}
|
||||||
|
if (parser.isSet(overrideAppLocalDataPathOption)) {
|
||||||
|
// get dir to use for cache
|
||||||
|
QString cacheDir = parser.value(overrideAppLocalDataPathOption);
|
||||||
|
if (!cacheDir.isEmpty()) {
|
||||||
|
// tell everyone to use the right cache location
|
||||||
|
//
|
||||||
|
// this handles data8 and prepared
|
||||||
|
ResourceManager::setCacheDir(cacheDir);
|
||||||
|
|
||||||
|
// this does the ktx_cache
|
||||||
|
PathUtils::getAppLocalDataPath(cacheDir);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (instanceMightBeRunning) {
|
if (instanceMightBeRunning) {
|
||||||
// Try to connect and send message to existing interface instance
|
// Try to connect and send message to existing interface instance
|
||||||
|
@ -179,7 +195,7 @@ int main(int argc, const char* argv[]) {
|
||||||
QString openvrDllPath = appPath + "/plugins/openvr.dll";
|
QString openvrDllPath = appPath + "/plugins/openvr.dll";
|
||||||
HMODULE openvrDll;
|
HMODULE openvrDll;
|
||||||
CHECKMINSPECPROC checkMinSpecPtr;
|
CHECKMINSPECPROC checkMinSpecPtr;
|
||||||
if ((openvrDll = LoadLibrary(openvrDllPath.toLocal8Bit().data())) &&
|
if ((openvrDll = LoadLibrary(openvrDllPath.toLocal8Bit().data())) &&
|
||||||
(checkMinSpecPtr = (CHECKMINSPECPROC)GetProcAddress(openvrDll, "CheckMinSpec"))) {
|
(checkMinSpecPtr = (CHECKMINSPECPROC)GetProcAddress(openvrDll, "CheckMinSpec"))) {
|
||||||
if (!checkMinSpecPtr()) {
|
if (!checkMinSpecPtr()) {
|
||||||
return -1;
|
return -1;
|
||||||
|
|
|
@ -1509,6 +1509,7 @@ void AvatarData::processAvatarIdentity(const QByteArray& identityData, bool& ide
|
||||||
>> identity.attachmentData
|
>> identity.attachmentData
|
||||||
>> identity.displayName
|
>> identity.displayName
|
||||||
>> identity.sessionDisplayName
|
>> identity.sessionDisplayName
|
||||||
|
>> identity.isReplicated
|
||||||
>> identity.avatarEntityData;
|
>> identity.avatarEntityData;
|
||||||
|
|
||||||
// set the store identity sequence number to match the incoming identity
|
// set the store identity sequence number to match the incoming identity
|
||||||
|
@ -1531,6 +1532,11 @@ void AvatarData::processAvatarIdentity(const QByteArray& identityData, bool& ide
|
||||||
}
|
}
|
||||||
maybeUpdateSessionDisplayNameFromTransport(identity.sessionDisplayName);
|
maybeUpdateSessionDisplayNameFromTransport(identity.sessionDisplayName);
|
||||||
|
|
||||||
|
if (identity.isReplicated != _isReplicated) {
|
||||||
|
_isReplicated = identity.isReplicated;
|
||||||
|
identityChanged = true;
|
||||||
|
}
|
||||||
|
|
||||||
if (identity.attachmentData != _attachmentData) {
|
if (identity.attachmentData != _attachmentData) {
|
||||||
setAttachmentData(identity.attachmentData);
|
setAttachmentData(identity.attachmentData);
|
||||||
identityChanged = true;
|
identityChanged = true;
|
||||||
|
@ -1561,7 +1567,7 @@ void AvatarData::processAvatarIdentity(const QByteArray& identityData, bool& ide
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
QByteArray AvatarData::identityByteArray() const {
|
QByteArray AvatarData::identityByteArray(bool setIsReplicated) const {
|
||||||
QByteArray identityData;
|
QByteArray identityData;
|
||||||
QDataStream identityStream(&identityData, QIODevice::Append);
|
QDataStream identityStream(&identityData, QIODevice::Append);
|
||||||
const QUrl& urlToSend = cannonicalSkeletonModelURL(emptyURL); // depends on _skeletonModelURL
|
const QUrl& urlToSend = cannonicalSkeletonModelURL(emptyURL); // depends on _skeletonModelURL
|
||||||
|
@ -1576,6 +1582,7 @@ QByteArray AvatarData::identityByteArray() const {
|
||||||
<< _attachmentData
|
<< _attachmentData
|
||||||
<< _displayName
|
<< _displayName
|
||||||
<< getSessionDisplayNameForTransport() // depends on _sessionDisplayName
|
<< getSessionDisplayNameForTransport() // depends on _sessionDisplayName
|
||||||
|
<< (_isReplicated || setIsReplicated)
|
||||||
<< _avatarEntityData;
|
<< _avatarEntityData;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -531,6 +531,7 @@ public:
|
||||||
QVector<AttachmentData> attachmentData;
|
QVector<AttachmentData> attachmentData;
|
||||||
QString displayName;
|
QString displayName;
|
||||||
QString sessionDisplayName;
|
QString sessionDisplayName;
|
||||||
|
bool isReplicated;
|
||||||
AvatarEntityMap avatarEntityData;
|
AvatarEntityMap avatarEntityData;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -539,7 +540,7 @@ public:
|
||||||
void processAvatarIdentity(const QByteArray& identityData, bool& identityChanged,
|
void processAvatarIdentity(const QByteArray& identityData, bool& identityChanged,
|
||||||
bool& displayNameChanged, bool& skeletonModelUrlChanged);
|
bool& displayNameChanged, bool& skeletonModelUrlChanged);
|
||||||
|
|
||||||
QByteArray identityByteArray() const;
|
QByteArray identityByteArray(bool setIsReplicated = false) const;
|
||||||
|
|
||||||
const QUrl& getSkeletonModelURL() const { return _skeletonModelURL; }
|
const QUrl& getSkeletonModelURL() const { return _skeletonModelURL; }
|
||||||
const QString& getDisplayName() const { return _displayName; }
|
const QString& getDisplayName() const { return _displayName; }
|
||||||
|
@ -629,6 +630,8 @@ public:
|
||||||
|
|
||||||
float getDensity() const { return _density; }
|
float getDensity() const { return _density; }
|
||||||
|
|
||||||
|
bool getIsReplicated() const { return _isReplicated; }
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void displayNameChanged();
|
void displayNameChanged();
|
||||||
|
|
||||||
|
@ -665,6 +668,10 @@ protected:
|
||||||
bool hasParent() const { return !getParentID().isNull(); }
|
bool hasParent() const { return !getParentID().isNull(); }
|
||||||
bool hasFaceTracker() const { return _headData ? _headData->_isFaceTrackerConnected : false; }
|
bool hasFaceTracker() const { return _headData ? _headData->_isFaceTrackerConnected : false; }
|
||||||
|
|
||||||
|
// isReplicated will be true on downstream Avatar Mixers and their clients, but false on the upstream "master"
|
||||||
|
// Audio Mixer that the replicated avatar is connected to.
|
||||||
|
bool _isReplicated{ false };
|
||||||
|
|
||||||
glm::vec3 _handPosition;
|
glm::vec3 _handPosition;
|
||||||
virtual const QString& getSessionDisplayNameForTransport() const { return _sessionDisplayName; }
|
virtual const QString& getSessionDisplayNameForTransport() const { return _sessionDisplayName; }
|
||||||
virtual void maybeUpdateSessionDisplayNameFromTransport(const QString& sessionDisplayName) { } // No-op in AvatarMixer
|
virtual void maybeUpdateSessionDisplayNameFromTransport(const QString& sessionDisplayName) { } // No-op in AvatarMixer
|
||||||
|
|
|
@ -152,6 +152,15 @@ QString ScriptAvatarData::getSessionDisplayName() const {
|
||||||
return QString();
|
return QString();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool ScriptAvatarData::getIsReplicated() const {
|
||||||
|
if (AvatarSharedPointer sharedAvatarData = _avatarData.lock()) {
|
||||||
|
return sharedAvatarData->getIsReplicated();
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
// IDENTIFIER PROPERTIES
|
// IDENTIFIER PROPERTIES
|
||||||
// END
|
// END
|
||||||
|
|
|
@ -45,6 +45,7 @@ class ScriptAvatarData : public QObject {
|
||||||
Q_PROPERTY(QUuid sessionUUID READ getSessionUUID)
|
Q_PROPERTY(QUuid sessionUUID READ getSessionUUID)
|
||||||
Q_PROPERTY(QString displayName READ getDisplayName NOTIFY displayNameChanged)
|
Q_PROPERTY(QString displayName READ getDisplayName NOTIFY displayNameChanged)
|
||||||
Q_PROPERTY(QString sessionDisplayName READ getSessionDisplayName)
|
Q_PROPERTY(QString sessionDisplayName READ getSessionDisplayName)
|
||||||
|
Q_PROPERTY(bool isReplicated READ getIsReplicated)
|
||||||
|
|
||||||
//
|
//
|
||||||
// ATTACHMENT AND JOINT PROPERTIES
|
// ATTACHMENT AND JOINT PROPERTIES
|
||||||
|
@ -95,6 +96,7 @@ public:
|
||||||
QUuid getSessionUUID() const;
|
QUuid getSessionUUID() const;
|
||||||
QString getDisplayName() const;
|
QString getDisplayName() const;
|
||||||
QString getSessionDisplayName() const;
|
QString getSessionDisplayName() const;
|
||||||
|
bool getIsReplicated() const;
|
||||||
|
|
||||||
//
|
//
|
||||||
// ATTACHMENT AND JOINT PROPERTIES
|
// ATTACHMENT AND JOINT PROPERTIES
|
||||||
|
|
|
@ -178,6 +178,11 @@ void GLBackend::init() {
|
||||||
int swapInterval = wglGetSwapIntervalEXT();
|
int swapInterval = wglGetSwapIntervalEXT();
|
||||||
qCDebug(gpugllogging, "V-Sync is %s\n", (swapInterval > 0 ? "ON" : "OFF"));
|
qCDebug(gpugllogging, "V-Sync is %s\n", (swapInterval > 0 ? "ON" : "OFF"));
|
||||||
}*/
|
}*/
|
||||||
|
#endif
|
||||||
|
#if THREADED_TEXTURE_BUFFERING
|
||||||
|
// This has to happen on the main thread in order to give the thread
|
||||||
|
// pool a reasonable parent object
|
||||||
|
GLVariableAllocationSupport::TransferJob::startBufferingThread();
|
||||||
#endif
|
#endif
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -461,11 +461,6 @@ void GLVariableAllocationSupport::updateMemoryPressure() {
|
||||||
|
|
||||||
if (newState != _memoryPressureState) {
|
if (newState != _memoryPressureState) {
|
||||||
_memoryPressureState = newState;
|
_memoryPressureState = newState;
|
||||||
#if THREADED_TEXTURE_BUFFERING
|
|
||||||
if (MemoryPressureState::Transfer == _memoryPressureState) {
|
|
||||||
TransferJob::startBufferingThread();
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
// Clear the existing queue
|
// Clear the existing queue
|
||||||
_transferQueue = WorkQueue();
|
_transferQueue = WorkQueue();
|
||||||
_promoteQueue = WorkQueue();
|
_promoteQueue = WorkQueue();
|
||||||
|
|
|
@ -31,7 +31,7 @@
|
||||||
|
|
||||||
MessageID AssetClient::_currentID = 0;
|
MessageID AssetClient::_currentID = 0;
|
||||||
|
|
||||||
AssetClient::AssetClient() {
|
AssetClient::AssetClient(const QString& cacheDir) : _cacheDir(cacheDir) {
|
||||||
setCustomDeleter([](Dependency* dependency){
|
setCustomDeleter([](Dependency* dependency){
|
||||||
static_cast<AssetClient*>(dependency)->deleteLater();
|
static_cast<AssetClient*>(dependency)->deleteLater();
|
||||||
});
|
});
|
||||||
|
@ -55,14 +55,15 @@ void AssetClient::init() {
|
||||||
// Setup disk cache if not already
|
// Setup disk cache if not already
|
||||||
auto& networkAccessManager = NetworkAccessManager::getInstance();
|
auto& networkAccessManager = NetworkAccessManager::getInstance();
|
||||||
if (!networkAccessManager.cache()) {
|
if (!networkAccessManager.cache()) {
|
||||||
QString cachePath = QStandardPaths::writableLocation(QStandardPaths::DataLocation);
|
if (_cacheDir.isEmpty()) {
|
||||||
cachePath = !cachePath.isEmpty() ? cachePath : "interfaceCache";
|
QString cachePath = QStandardPaths::writableLocation(QStandardPaths::DataLocation);
|
||||||
|
_cacheDir = !cachePath.isEmpty() ? cachePath : "interfaceCache";
|
||||||
|
}
|
||||||
QNetworkDiskCache* cache = new QNetworkDiskCache();
|
QNetworkDiskCache* cache = new QNetworkDiskCache();
|
||||||
cache->setMaximumCacheSize(MAXIMUM_CACHE_SIZE);
|
cache->setMaximumCacheSize(MAXIMUM_CACHE_SIZE);
|
||||||
cache->setCacheDirectory(cachePath);
|
cache->setCacheDirectory(_cacheDir);
|
||||||
networkAccessManager.setCache(cache);
|
networkAccessManager.setCache(cache);
|
||||||
qInfo() << "ResourceManager disk cache setup at" << cachePath
|
qInfo() << "ResourceManager disk cache setup at" << _cacheDir
|
||||||
<< "(size:" << MAXIMUM_CACHE_SIZE / BYTES_PER_GIGABYTES << "GB)";
|
<< "(size:" << MAXIMUM_CACHE_SIZE / BYTES_PER_GIGABYTES << "GB)";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -49,7 +49,7 @@ using ProgressCallback = std::function<void(qint64 totalReceived, qint64 total)>
|
||||||
class AssetClient : public QObject, public Dependency {
|
class AssetClient : public QObject, public Dependency {
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
public:
|
public:
|
||||||
AssetClient();
|
AssetClient(const QString& cacheDir="");
|
||||||
|
|
||||||
Q_INVOKABLE GetMappingRequest* createGetMappingRequest(const AssetPath& path);
|
Q_INVOKABLE GetMappingRequest* createGetMappingRequest(const AssetPath& path);
|
||||||
Q_INVOKABLE GetAllMappingsRequest* createGetAllMappingsRequest();
|
Q_INVOKABLE GetAllMappingsRequest* createGetAllMappingsRequest();
|
||||||
|
@ -109,6 +109,8 @@ private:
|
||||||
std::unordered_map<SharedNodePointer, std::unordered_map<MessageID, GetInfoCallback>> _pendingInfoRequests;
|
std::unordered_map<SharedNodePointer, std::unordered_map<MessageID, GetInfoCallback>> _pendingInfoRequests;
|
||||||
std::unordered_map<SharedNodePointer, std::unordered_map<MessageID, UploadResultCallback>> _pendingUploads;
|
std::unordered_map<SharedNodePointer, std::unordered_map<MessageID, UploadResultCallback>> _pendingUploads;
|
||||||
|
|
||||||
|
QString _cacheDir;
|
||||||
|
|
||||||
friend class AssetRequest;
|
friend class AssetRequest;
|
||||||
friend class AssetUpload;
|
friend class AssetUpload;
|
||||||
friend class MappingRequest;
|
friend class MappingRequest;
|
||||||
|
|
|
@ -27,7 +27,7 @@
|
||||||
QThread ResourceManager::_thread;
|
QThread ResourceManager::_thread;
|
||||||
ResourceManager::PrefixMap ResourceManager::_prefixMap;
|
ResourceManager::PrefixMap ResourceManager::_prefixMap;
|
||||||
QMutex ResourceManager::_prefixMapLock;
|
QMutex ResourceManager::_prefixMapLock;
|
||||||
|
QString ResourceManager::_cacheDir;
|
||||||
|
|
||||||
void ResourceManager::setUrlPrefixOverride(const QString& prefix, const QString& replacement) {
|
void ResourceManager::setUrlPrefixOverride(const QString& prefix, const QString& replacement) {
|
||||||
QMutexLocker locker(&_prefixMapLock);
|
QMutexLocker locker(&_prefixMapLock);
|
||||||
|
@ -78,7 +78,7 @@ QUrl ResourceManager::normalizeURL(const QUrl& originalUrl) {
|
||||||
void ResourceManager::init() {
|
void ResourceManager::init() {
|
||||||
_thread.setObjectName("Resource Manager Thread");
|
_thread.setObjectName("Resource Manager Thread");
|
||||||
|
|
||||||
auto assetClient = DependencyManager::set<AssetClient>();
|
auto assetClient = DependencyManager::set<AssetClient>(_cacheDir);
|
||||||
assetClient->moveToThread(&_thread);
|
assetClient->moveToThread(&_thread);
|
||||||
QObject::connect(&_thread, &QThread::started, assetClient.data(), &AssetClient::init);
|
QObject::connect(&_thread, &QThread::started, assetClient.data(), &AssetClient::init);
|
||||||
|
|
||||||
|
@ -164,3 +164,7 @@ bool ResourceManager::resourceExists(const QUrl& url) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void ResourceManager::setCacheDir(const QString& cacheDir) {
|
||||||
|
// TODO: check for existence?
|
||||||
|
_cacheDir = cacheDir;
|
||||||
|
}
|
||||||
|
|
|
@ -40,6 +40,9 @@ public:
|
||||||
// to return to the calling thread so that events can still be processed.
|
// to return to the calling thread so that events can still be processed.
|
||||||
static bool resourceExists(const QUrl& url);
|
static bool resourceExists(const QUrl& url);
|
||||||
|
|
||||||
|
// adjust where we persist the cache
|
||||||
|
static void setCacheDir(const QString& cacheDir);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
static QThread _thread;
|
static QThread _thread;
|
||||||
|
|
||||||
|
@ -47,6 +50,8 @@ private:
|
||||||
|
|
||||||
static PrefixMap _prefixMap;
|
static PrefixMap _prefixMap;
|
||||||
static QMutex _prefixMapLock;
|
static QMutex _prefixMapLock;
|
||||||
|
|
||||||
|
static QString _cacheDir;
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
|
@ -69,7 +69,7 @@ PacketVersion versionForPacketType(PacketType packetType) {
|
||||||
case PacketType::AvatarData:
|
case PacketType::AvatarData:
|
||||||
case PacketType::BulkAvatarData:
|
case PacketType::BulkAvatarData:
|
||||||
case PacketType::KillAvatar:
|
case PacketType::KillAvatar:
|
||||||
return static_cast<PacketVersion>(AvatarMixerPacketVersion::AvatarIdentitySequenceFront);
|
return static_cast<PacketVersion>(AvatarMixerPacketVersion::IsReplicatedInAvatarIdentity);
|
||||||
case PacketType::MessagesData:
|
case PacketType::MessagesData:
|
||||||
return static_cast<PacketVersion>(MessageDataVersion::TextOrBinaryData);
|
return static_cast<PacketVersion>(MessageDataVersion::TextOrBinaryData);
|
||||||
case PacketType::ICEServerHeartbeat:
|
case PacketType::ICEServerHeartbeat:
|
||||||
|
|
|
@ -247,7 +247,8 @@ enum class AvatarMixerPacketVersion : PacketVersion {
|
||||||
IdentityPacketsIncludeUpdateTime,
|
IdentityPacketsIncludeUpdateTime,
|
||||||
AvatarIdentitySequenceId,
|
AvatarIdentitySequenceId,
|
||||||
MannequinDefaultAvatar,
|
MannequinDefaultAvatar,
|
||||||
AvatarIdentitySequenceFront
|
AvatarIdentitySequenceFront,
|
||||||
|
IsReplicatedInAvatarIdentity
|
||||||
};
|
};
|
||||||
|
|
||||||
enum class DomainConnectRequestVersion : PacketVersion {
|
enum class DomainConnectRequestVersion : PacketVersion {
|
||||||
|
|
|
@ -1309,6 +1309,7 @@ QObject* ScriptEngine::setupTimerWithInterval(const QScriptValue& function, int
|
||||||
// make sure the timer stops when the script does
|
// make sure the timer stops when the script does
|
||||||
connect(this, &ScriptEngine::scriptEnding, newTimer, &QTimer::stop);
|
connect(this, &ScriptEngine::scriptEnding, newTimer, &QTimer::stop);
|
||||||
|
|
||||||
|
|
||||||
CallbackData timerData = { function, currentEntityIdentifier, currentSandboxURL };
|
CallbackData timerData = { function, currentEntityIdentifier, currentSandboxURL };
|
||||||
_timerFunctionMap.insert(newTimer, timerData);
|
_timerFunctionMap.insert(newTimer, timerData);
|
||||||
|
|
||||||
|
@ -1392,6 +1393,15 @@ void ScriptEngine::print(const QString& message) {
|
||||||
emit printedMessage(message, getFilename());
|
emit printedMessage(message, getFilename());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void ScriptEngine::beginProfileRange(const QString& label) const {
|
||||||
|
PROFILE_SYNC_BEGIN(script, label.toStdString().c_str(), label.toStdString().c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
void ScriptEngine::endProfileRange(const QString& label) const {
|
||||||
|
PROFILE_SYNC_END(script, label.toStdString().c_str(), label.toStdString().c_str());
|
||||||
|
}
|
||||||
|
|
||||||
// Script.require.resolve -- like resolvePath, but performs more validation and throws exceptions on invalid module identifiers (for consistency with Node.js)
|
// Script.require.resolve -- like resolvePath, but performs more validation and throws exceptions on invalid module identifiers (for consistency with Node.js)
|
||||||
QString ScriptEngine::_requireResolve(const QString& moduleId, const QString& relativeTo) {
|
QString ScriptEngine::_requireResolve(const QString& moduleId, const QString& relativeTo) {
|
||||||
if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) {
|
if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) {
|
||||||
|
|
|
@ -43,6 +43,7 @@
|
||||||
#include "Vec3.h"
|
#include "Vec3.h"
|
||||||
#include "ConsoleScriptingInterface.h"
|
#include "ConsoleScriptingInterface.h"
|
||||||
#include "SettingHandle.h"
|
#include "SettingHandle.h"
|
||||||
|
#include "Profile.h"
|
||||||
|
|
||||||
class QScriptEngineDebugger;
|
class QScriptEngineDebugger;
|
||||||
|
|
||||||
|
@ -182,6 +183,8 @@ public:
|
||||||
Q_INVOKABLE void print(const QString& message);
|
Q_INVOKABLE void print(const QString& message);
|
||||||
Q_INVOKABLE QUrl resolvePath(const QString& path) const;
|
Q_INVOKABLE QUrl resolvePath(const QString& path) const;
|
||||||
Q_INVOKABLE QUrl resourcesPath() const;
|
Q_INVOKABLE QUrl resourcesPath() const;
|
||||||
|
Q_INVOKABLE void beginProfileRange(const QString& label) const;
|
||||||
|
Q_INVOKABLE void endProfileRange(const QString& label) const;
|
||||||
|
|
||||||
// Entity Script Related methods
|
// Entity Script Related methods
|
||||||
Q_INVOKABLE bool isEntityScriptRunning(const EntityItemID& entityID) {
|
Q_INVOKABLE bool isEntityScriptRunning(const EntityItemID& entityID) {
|
||||||
|
|
|
@ -34,7 +34,18 @@ QString PathUtils::getAppDataPath() {
|
||||||
return QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + "/";
|
return QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + "/";
|
||||||
}
|
}
|
||||||
|
|
||||||
QString PathUtils::getAppLocalDataPath() {
|
QString PathUtils::getAppLocalDataPath(const QString& overridePath /* = "" */) {
|
||||||
|
static QString overriddenPath = "";
|
||||||
|
// set the overridden path if one was passed in
|
||||||
|
if (!overridePath.isEmpty()) {
|
||||||
|
overriddenPath = overridePath;
|
||||||
|
}
|
||||||
|
// return overridden path if set
|
||||||
|
if (!overriddenPath.isEmpty()) {
|
||||||
|
return overriddenPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
// otherwise return standard path
|
||||||
return QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation) + "/";
|
return QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation) + "/";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -28,7 +28,7 @@ public:
|
||||||
static const QString& resourcesPath();
|
static const QString& resourcesPath();
|
||||||
|
|
||||||
static QString getAppDataPath();
|
static QString getAppDataPath();
|
||||||
static QString getAppLocalDataPath();
|
static QString getAppLocalDataPath(const QString& overridePath = "");
|
||||||
|
|
||||||
static QString getAppDataFilePath(const QString& filename);
|
static QString getAppDataFilePath(const QString& filename);
|
||||||
static QString getAppLocalDataFilePath(const QString& filename);
|
static QString getAppLocalDataFilePath(const QString& filename);
|
||||||
|
|
|
@ -46,6 +46,20 @@ private:
|
||||||
const QLoggingCategory& _category;
|
const QLoggingCategory& _category;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
inline void syncBegin(const QLoggingCategory& category, const QString& name, const QString& id, const QVariantMap& args = QVariantMap(), const QVariantMap& extra = QVariantMap()) {
|
||||||
|
if (category.isDebugEnabled()) {
|
||||||
|
tracing::traceEvent(category, name, tracing::DurationBegin, id, args, extra);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
inline void syncEnd(const QLoggingCategory& category, const QString& name, const QString& id, const QVariantMap& args = QVariantMap(), const QVariantMap& extra = QVariantMap()) {
|
||||||
|
if (category.isDebugEnabled()) {
|
||||||
|
tracing::traceEvent(category, name, tracing::DurationEnd, id, args, extra);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
inline void asyncBegin(const QLoggingCategory& category, const QString& name, const QString& id, const QVariantMap& args = QVariantMap(), const QVariantMap& extra = QVariantMap()) {
|
inline void asyncBegin(const QLoggingCategory& category, const QString& name, const QString& id, const QVariantMap& args = QVariantMap(), const QVariantMap& extra = QVariantMap()) {
|
||||||
if (category.isDebugEnabled()) {
|
if (category.isDebugEnabled()) {
|
||||||
tracing::traceEvent(category, name, tracing::AsyncNestableStart, id, args, extra);
|
tracing::traceEvent(category, name, tracing::AsyncNestableStart, id, args, extra);
|
||||||
|
@ -80,6 +94,8 @@ inline void metadata(const QString& metadataType, const QVariantMap& args) {
|
||||||
#define PROFILE_RANGE_EX(category, name, argbColor, payload, ...) Duration profileRangeThis(trace_##category(), name, argbColor, (uint64_t)payload, ##__VA_ARGS__);
|
#define PROFILE_RANGE_EX(category, name, argbColor, payload, ...) Duration profileRangeThis(trace_##category(), name, argbColor, (uint64_t)payload, ##__VA_ARGS__);
|
||||||
#define PROFILE_RANGE_BEGIN(category, rangeId, name, argbColor) rangeId = Duration::beginRange(trace_##category(), name, argbColor)
|
#define PROFILE_RANGE_BEGIN(category, rangeId, name, argbColor) rangeId = Duration::beginRange(trace_##category(), name, argbColor)
|
||||||
#define PROFILE_RANGE_END(category, rangeId) Duration::endRange(trace_##category(), rangeId)
|
#define PROFILE_RANGE_END(category, rangeId) Duration::endRange(trace_##category(), rangeId)
|
||||||
|
#define PROFILE_SYNC_BEGIN(category, name, id, ...) syncBegin(trace_##category(), name, id, ##__VA_ARGS__);
|
||||||
|
#define PROFILE_SYNC_END(category, name, id, ...) syncEnd(trace_##category(), name, id, ##__VA_ARGS__);
|
||||||
#define PROFILE_ASYNC_BEGIN(category, name, id, ...) asyncBegin(trace_##category(), name, id, ##__VA_ARGS__);
|
#define PROFILE_ASYNC_BEGIN(category, name, id, ...) asyncBegin(trace_##category(), name, id, ##__VA_ARGS__);
|
||||||
#define PROFILE_ASYNC_END(category, name, id, ...) asyncEnd(trace_##category(), name, id, ##__VA_ARGS__);
|
#define PROFILE_ASYNC_END(category, name, id, ...) asyncEnd(trace_##category(), name, id, ##__VA_ARGS__);
|
||||||
#define PROFILE_COUNTER_IF_CHANGED(category, name, type, value) { static type lastValue = 0; type newValue = value; if (newValue != lastValue) { counter(trace_##category(), name, { { name, newValue }}); lastValue = newValue; } }
|
#define PROFILE_COUNTER_IF_CHANGED(category, name, type, value) { static type lastValue = 0; type newValue = value; if (newValue != lastValue) { counter(trace_##category(), name, { { name, newValue }}); lastValue = newValue; } }
|
||||||
|
|
|
@ -959,15 +959,33 @@ void ViveControllerManager::InputDevice::calibrateFeet(glm::mat4& defaultToRefer
|
||||||
controller::Pose& secondFootPose = secondFoot.second;
|
controller::Pose& secondFootPose = secondFoot.second;
|
||||||
|
|
||||||
if (determineLimbOrdering(firstFootPose, secondFootPose, headXAxis, headPosition)) {
|
if (determineLimbOrdering(firstFootPose, secondFootPose, headXAxis, headPosition)) {
|
||||||
_jointToPuckMap[controller::LEFT_FOOT] = firstFoot.first;
|
calibrateFoot(defaultToReferenceMat, inputCalibration, firstFoot, true);
|
||||||
_pucksOffset[firstFoot.first] = computeOffset(defaultToReferenceMat, inputCalibration.defaultLeftFoot, firstFootPose);
|
calibrateFoot(defaultToReferenceMat, inputCalibration, secondFoot, false);
|
||||||
_jointToPuckMap[controller::RIGHT_FOOT] = secondFoot.first;
|
|
||||||
_pucksOffset[secondFoot.first] = computeOffset(defaultToReferenceMat, inputCalibration.defaultRightFoot, secondFootPose);
|
|
||||||
} else {
|
} else {
|
||||||
_jointToPuckMap[controller::LEFT_FOOT] = secondFoot.first;
|
calibrateFoot(defaultToReferenceMat, inputCalibration, secondFoot, true);
|
||||||
_pucksOffset[secondFoot.first] = computeOffset(defaultToReferenceMat, inputCalibration.defaultLeftFoot, secondFootPose);
|
calibrateFoot(defaultToReferenceMat, inputCalibration, firstFoot, false);
|
||||||
_jointToPuckMap[controller::RIGHT_FOOT] = firstFoot.first;
|
}
|
||||||
_pucksOffset[firstFoot.first] = computeOffset(defaultToReferenceMat, inputCalibration.defaultRightFoot, firstFootPose);
|
}
|
||||||
|
|
||||||
|
void ViveControllerManager::InputDevice::calibrateFoot(glm::mat4& defaultToReferenceMat, const controller::InputCalibrationData& inputCalibration, PuckPosePair& footPair, bool isLeftFoot){
|
||||||
|
controller::Pose footPose = footPair.second;
|
||||||
|
glm::mat4 puckPoseAvatarMat = createMatFromQuatAndPos(footPose.getRotation(), footPose.getTranslation());
|
||||||
|
glm::mat4 defaultFoot = isLeftFoot ? inputCalibration.defaultLeftFoot : inputCalibration.defaultRightFoot;
|
||||||
|
glm::mat4 footOffset = computeOffset(defaultToReferenceMat, defaultFoot, footPose);
|
||||||
|
|
||||||
|
glm::quat rotationOffset = glmExtractRotation(footOffset);
|
||||||
|
glm::vec3 translationOffset = extractTranslation(footOffset);
|
||||||
|
glm::vec3 avatarXAxisInPuckFrame = glm::normalize(transformVectorFast(glm::inverse(puckPoseAvatarMat), glm::vec3(-1.0f, 0.0f, 0.0f)));
|
||||||
|
float distance = glm::dot(translationOffset, avatarXAxisInPuckFrame);
|
||||||
|
glm::vec3 finalTranslation = translationOffset - (distance * avatarXAxisInPuckFrame);
|
||||||
|
glm::mat4 finalOffset = createMatFromQuatAndPos(rotationOffset, finalTranslation);
|
||||||
|
|
||||||
|
if (isLeftFoot) {
|
||||||
|
_jointToPuckMap[controller::LEFT_FOOT] = footPair.first;
|
||||||
|
_pucksOffset[footPair.first] = finalOffset;
|
||||||
|
} else {
|
||||||
|
_jointToPuckMap[controller::RIGHT_FOOT] = footPair.first;
|
||||||
|
_pucksOffset[footPair.first] = finalOffset;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -98,6 +98,7 @@ private:
|
||||||
void calibrateLeftHand(glm::mat4& defaultToReferenceMat, const controller::InputCalibrationData& inputCalibration, PuckPosePair& handPair);
|
void calibrateLeftHand(glm::mat4& defaultToReferenceMat, const controller::InputCalibrationData& inputCalibration, PuckPosePair& handPair);
|
||||||
void calibrateRightHand(glm::mat4& defaultToReferenceMat, const controller::InputCalibrationData& inputCalibration, PuckPosePair& handPair);
|
void calibrateRightHand(glm::mat4& defaultToReferenceMat, const controller::InputCalibrationData& inputCalibration, PuckPosePair& handPair);
|
||||||
void calibrateFeet(glm::mat4& defaultToReferenceMat, const controller::InputCalibrationData& inputCalibration);
|
void calibrateFeet(glm::mat4& defaultToReferenceMat, const controller::InputCalibrationData& inputCalibration);
|
||||||
|
void calibrateFoot(glm::mat4& defaultToReferenceMat, const controller::InputCalibrationData& inputCalibration, PuckPosePair& footPair, bool isLeftFoot);
|
||||||
void calibrateHips(glm::mat4& defaultToReferenceMat, const controller::InputCalibrationData& inputCalibration);
|
void calibrateHips(glm::mat4& defaultToReferenceMat, const controller::InputCalibrationData& inputCalibration);
|
||||||
void calibrateChest(glm::mat4& defaultToReferenceMat, const controller::InputCalibrationData& inputCalibration);
|
void calibrateChest(glm::mat4& defaultToReferenceMat, const controller::InputCalibrationData& inputCalibration);
|
||||||
void calibrateShoulders(glm::mat4& defaultToReferenceMat, const controller::InputCalibrationData& inputCalibration,
|
void calibrateShoulders(glm::mat4& defaultToReferenceMat, const controller::InputCalibrationData& inputCalibration,
|
||||||
|
|
|
@ -35,8 +35,8 @@ var audioOptions = new AudioEffectOptions({
|
||||||
wetDryMix: 50,
|
wetDryMix: 50,
|
||||||
});
|
});
|
||||||
|
|
||||||
AudioDevice.setReverbOptions(audioOptions);
|
Audio.setReverbOptions(audioOptions);
|
||||||
AudioDevice.setReverb(true);
|
Audio.setReverb(true);
|
||||||
print("Reverb is ON.");
|
print("Reverb is ON.");
|
||||||
|
|
||||||
var panel = new Panel(10, 160);
|
var panel = new Panel(10, 160);
|
||||||
|
@ -66,7 +66,7 @@ var parameters = [
|
||||||
]
|
]
|
||||||
|
|
||||||
function setter(name) {
|
function setter(name) {
|
||||||
return function(value) { audioOptions[name] = value; AudioDevice.setReverbOptions(audioOptions); }
|
return function(value) { audioOptions[name] = value; Audio.setReverbOptions(audioOptions); }
|
||||||
}
|
}
|
||||||
|
|
||||||
function getter(name) {
|
function getter(name) {
|
||||||
|
@ -89,7 +89,7 @@ Controller.mouseReleaseEvent.connect(function(event) { return panel.mouseRelease
|
||||||
|
|
||||||
function scriptEnding() {
|
function scriptEnding() {
|
||||||
panel.destroy();
|
panel.destroy();
|
||||||
AudioDevice.setReverb(false);
|
Audio.setReverb(false);
|
||||||
print("Reverb is OFF.");
|
print("Reverb is OFF.");
|
||||||
}
|
}
|
||||||
Script.scriptEnding.connect(scriptEnding);
|
Script.scriptEnding.connect(scriptEnding);
|
||||||
|
|
|
@ -15,6 +15,7 @@
|
||||||
|
|
||||||
var TABLET_BUTTON_NAME = "AUDIO";
|
var TABLET_BUTTON_NAME = "AUDIO";
|
||||||
var HOME_BUTTON_TEXTURE = "http://hifi-content.s3.amazonaws.com/alan/dev/tablet-with-home-button.fbx/tablet-with-home-button.fbm/button-root.png";
|
var HOME_BUTTON_TEXTURE = "http://hifi-content.s3.amazonaws.com/alan/dev/tablet-with-home-button.fbx/tablet-with-home-button.fbm/button-root.png";
|
||||||
|
var AUDIO_QML_SOURCE = "../audio/Audio.qml";
|
||||||
|
|
||||||
var MUTE_ICONS = {
|
var MUTE_ICONS = {
|
||||||
icon: "icons/tablet-icons/mic-mute-i.svg",
|
icon: "icons/tablet-icons/mic-mute-i.svg",
|
||||||
|
@ -34,7 +35,6 @@ function onMuteToggled() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var shouldActivateButton = false;
|
|
||||||
var onAudioScreen = false;
|
var onAudioScreen = false;
|
||||||
|
|
||||||
function onClicked() {
|
function onClicked() {
|
||||||
|
@ -44,18 +44,14 @@ function onClicked() {
|
||||||
} else {
|
} else {
|
||||||
var entity = HMD.tabletID;
|
var entity = HMD.tabletID;
|
||||||
Entities.editEntity(entity, { textures: JSON.stringify({ "tex.close": HOME_BUTTON_TEXTURE }) });
|
Entities.editEntity(entity, { textures: JSON.stringify({ "tex.close": HOME_BUTTON_TEXTURE }) });
|
||||||
shouldActivateButton = true;
|
tablet.loadQMLSource(AUDIO_QML_SOURCE);
|
||||||
shouldActivateButton = true;
|
|
||||||
tablet.loadQMLSource("../audio/Audio.qml");
|
|
||||||
onAudioScreen = true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function onScreenChanged(type, url) {
|
function onScreenChanged(type, url) {
|
||||||
|
onAudioScreen = (type === "QML" && url === AUDIO_QML_SOURCE);
|
||||||
// for toolbar mode: change button to active when window is first openend, false otherwise.
|
// for toolbar mode: change button to active when window is first openend, false otherwise.
|
||||||
button.editProperties({isActive: shouldActivateButton});
|
button.editProperties({isActive: onAudioScreen});
|
||||||
shouldActivateButton = false;
|
|
||||||
onAudioScreen = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system");
|
var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system");
|
||||||
|
|
|
@ -40,7 +40,8 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
function onScreenChanged(type, url) {
|
function onScreenChanged(type, url) {
|
||||||
onHelpScreen = false;
|
onHelpScreen = type === "Web" && url.startsWith("../../../html/tabletHelp.html");
|
||||||
|
button.editProperties({ isActive: onHelpScreen });
|
||||||
}
|
}
|
||||||
|
|
||||||
button.clicked.connect(onClicked);
|
button.clicked.connect(onClicked);
|
||||||
|
|
|
@ -52,17 +52,11 @@ function onMessageBoxClosed(id, button) {
|
||||||
|
|
||||||
Window.messageBoxClosed.connect(onMessageBoxClosed);
|
Window.messageBoxClosed.connect(onMessageBoxClosed);
|
||||||
|
|
||||||
var shouldActivateButton = false;
|
|
||||||
var onMarketplaceScreen = false;
|
var onMarketplaceScreen = false;
|
||||||
|
|
||||||
function showMarketplace() {
|
function showMarketplace() {
|
||||||
UserActivityLogger.openedMarketplace();
|
UserActivityLogger.openedMarketplace();
|
||||||
|
|
||||||
shouldActivateButton = true;
|
|
||||||
|
|
||||||
tablet.gotoWebScreen(MARKETPLACE_URL_INITIAL, MARKETPLACES_INJECT_SCRIPT_URL);
|
tablet.gotoWebScreen(MARKETPLACE_URL_INITIAL, MARKETPLACES_INJECT_SCRIPT_URL);
|
||||||
onMarketplaceScreen = true;
|
|
||||||
|
|
||||||
tablet.webEventReceived.connect(function (message) {
|
tablet.webEventReceived.connect(function (message) {
|
||||||
|
|
||||||
if (message === GOTO_DIRECTORY) {
|
if (message === GOTO_DIRECTORY) {
|
||||||
|
@ -122,7 +116,6 @@ function onClick() {
|
||||||
if (onMarketplaceScreen) {
|
if (onMarketplaceScreen) {
|
||||||
// for toolbar-mode: go back to home screen, this will close the window.
|
// for toolbar-mode: go back to home screen, this will close the window.
|
||||||
tablet.gotoHomeScreen();
|
tablet.gotoHomeScreen();
|
||||||
onMarketplaceScreen = false;
|
|
||||||
} else {
|
} else {
|
||||||
var entity = HMD.tabletID;
|
var entity = HMD.tabletID;
|
||||||
Entities.editEntity(entity, {textures: JSON.stringify({"tex.close": HOME_BUTTON_TEXTURE})});
|
Entities.editEntity(entity, {textures: JSON.stringify({"tex.close": HOME_BUTTON_TEXTURE})});
|
||||||
|
@ -131,10 +124,9 @@ function onClick() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function onScreenChanged(type, url) {
|
function onScreenChanged(type, url) {
|
||||||
|
onMarketplaceScreen = type === "Web" && url === MARKETPLACE_URL_INITIAL
|
||||||
// for toolbar mode: change button to active when window is first openend, false otherwise.
|
// for toolbar mode: change button to active when window is first openend, false otherwise.
|
||||||
marketplaceButton.editProperties({isActive: shouldActivateButton});
|
marketplaceButton.editProperties({isActive: onMarketplaceScreen});
|
||||||
shouldActivateButton = false;
|
|
||||||
onMarketplaceScreen = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
marketplaceButton.clicked.connect(onClick);
|
marketplaceButton.clicked.connect(onClick);
|
||||||
|
|
|
@ -21,7 +21,6 @@ var HOME_BUTTON_TEXTURE = "http://hifi-content.s3.amazonaws.com/alan/dev/tablet-
|
||||||
sortOrder: 3
|
sortOrder: 3
|
||||||
});
|
});
|
||||||
|
|
||||||
var shouldActivateButton = false;
|
|
||||||
var onMenuScreen = false;
|
var onMenuScreen = false;
|
||||||
|
|
||||||
function onClicked() {
|
function onClicked() {
|
||||||
|
@ -31,17 +30,13 @@ var HOME_BUTTON_TEXTURE = "http://hifi-content.s3.amazonaws.com/alan/dev/tablet-
|
||||||
} else {
|
} else {
|
||||||
var entity = HMD.tabletID;
|
var entity = HMD.tabletID;
|
||||||
Entities.editEntity(entity, {textures: JSON.stringify({"tex.close": HOME_BUTTON_TEXTURE})});
|
Entities.editEntity(entity, {textures: JSON.stringify({"tex.close": HOME_BUTTON_TEXTURE})});
|
||||||
shouldActivateButton = true;
|
|
||||||
tablet.gotoMenuScreen();
|
tablet.gotoMenuScreen();
|
||||||
onMenuScreen = true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function onScreenChanged(type, url) {
|
function onScreenChanged(type, url) {
|
||||||
// for toolbar mode: change button to active when window is first openend, false otherwise.
|
onMenuScreen = type === "Menu";
|
||||||
button.editProperties({isActive: shouldActivateButton});
|
button.editProperties({isActive: onMenuScreen});
|
||||||
shouldActivateButton = false;
|
|
||||||
onMenuScreen = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
button.clicked.connect(onClicked);
|
button.clicked.connect(onClicked);
|
||||||
|
|
|
@ -40,7 +40,7 @@ var HOVER_TEXTURES = {
|
||||||
var UNSELECTED_COLOR = { red: 0x1F, green: 0xC6, blue: 0xA6};
|
var UNSELECTED_COLOR = { red: 0x1F, green: 0xC6, blue: 0xA6};
|
||||||
var SELECTED_COLOR = {red: 0xF3, green: 0x91, blue: 0x29};
|
var SELECTED_COLOR = {red: 0xF3, green: 0x91, blue: 0x29};
|
||||||
var HOVER_COLOR = {red: 0xD0, green: 0xD0, blue: 0xD0}; // almost white for now
|
var HOVER_COLOR = {red: 0xD0, green: 0xD0, blue: 0xD0}; // almost white for now
|
||||||
|
var PAL_QML_SOURCE = "../Pal.qml";
|
||||||
var conserveResources = true;
|
var conserveResources = true;
|
||||||
|
|
||||||
Script.include("/~/system/libraries/controllers.js");
|
Script.include("/~/system/libraries/controllers.js");
|
||||||
|
@ -479,7 +479,8 @@ function populateNearbyUserList(selectData, oldAudioData) {
|
||||||
admin: false,
|
admin: false,
|
||||||
personalMute: !!id && Users.getPersonalMuteStatus(id), // expects proper boolean, not null
|
personalMute: !!id && Users.getPersonalMuteStatus(id), // expects proper boolean, not null
|
||||||
ignore: !!id && Users.getIgnoreStatus(id), // ditto
|
ignore: !!id && Users.getIgnoreStatus(id), // ditto
|
||||||
isPresent: true
|
isPresent: true,
|
||||||
|
isReplicated: avatar.isReplicated
|
||||||
};
|
};
|
||||||
if (id) {
|
if (id) {
|
||||||
addAvatarNode(id); // No overlay for ourselves
|
addAvatarNode(id); // No overlay for ourselves
|
||||||
|
@ -726,17 +727,14 @@ function tabletVisibilityChanged() {
|
||||||
}
|
}
|
||||||
|
|
||||||
var onPalScreen = false;
|
var onPalScreen = false;
|
||||||
var shouldActivateButton = false;
|
|
||||||
|
|
||||||
function onTabletButtonClicked() {
|
function onTabletButtonClicked() {
|
||||||
if (onPalScreen) {
|
if (onPalScreen) {
|
||||||
// for toolbar-mode: go back to home screen, this will close the window.
|
// for toolbar-mode: go back to home screen, this will close the window.
|
||||||
tablet.gotoHomeScreen();
|
tablet.gotoHomeScreen();
|
||||||
} else {
|
} else {
|
||||||
shouldActivateButton = true;
|
tablet.loadQMLSource(PAL_QML_SOURCE);
|
||||||
tablet.loadQMLSource("../Pal.qml");
|
|
||||||
tablet.tabletShownChanged.connect(tabletVisibilityChanged);
|
tablet.tabletShownChanged.connect(tabletVisibilityChanged);
|
||||||
onPalScreen = true;
|
|
||||||
Users.requestsDomainListData = true;
|
Users.requestsDomainListData = true;
|
||||||
populateNearbyUserList();
|
populateNearbyUserList();
|
||||||
isWired = true;
|
isWired = true;
|
||||||
|
@ -764,14 +762,13 @@ function wireEventBridge(on) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function onTabletScreenChanged(type, url) {
|
function onTabletScreenChanged(type, url) {
|
||||||
wireEventBridge(shouldActivateButton);
|
onPalScreen = (type === "QML" && url === PAL_QML_SOURCE);
|
||||||
|
wireEventBridge(onPalScreen);
|
||||||
// for toolbar mode: change button to active when window is first openend, false otherwise.
|
// for toolbar mode: change button to active when window is first openend, false otherwise.
|
||||||
button.editProperties({isActive: shouldActivateButton});
|
button.editProperties({isActive: onPalScreen});
|
||||||
shouldActivateButton = false;
|
|
||||||
onPalScreen = false;
|
|
||||||
|
|
||||||
// disable sphere overlays when not on pal screen.
|
// disable sphere overlays when not on pal screen.
|
||||||
if (type !== "QML" || url !== "../Pal.qml") {
|
if (!onPalScreen) {
|
||||||
off();
|
off();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -377,18 +377,15 @@ function fillImageDataFromPrevious() {
|
||||||
|
|
||||||
var SNAPSHOT_REVIEW_URL = Script.resolvePath("html/SnapshotReview.html");
|
var SNAPSHOT_REVIEW_URL = Script.resolvePath("html/SnapshotReview.html");
|
||||||
var isInSnapshotReview = false;
|
var isInSnapshotReview = false;
|
||||||
var shouldActivateButton = false;
|
|
||||||
function onButtonClicked() {
|
function onButtonClicked() {
|
||||||
if (isInSnapshotReview){
|
if (isInSnapshotReview){
|
||||||
// for toolbar-mode: go back to home screen, this will close the window.
|
// for toolbar-mode: go back to home screen, this will close the window.
|
||||||
tablet.gotoHomeScreen();
|
tablet.gotoHomeScreen();
|
||||||
} else {
|
} else {
|
||||||
shouldActivateButton = true;
|
|
||||||
fillImageDataFromPrevious();
|
fillImageDataFromPrevious();
|
||||||
tablet.gotoWebScreen(SNAPSHOT_REVIEW_URL);
|
tablet.gotoWebScreen(SNAPSHOT_REVIEW_URL);
|
||||||
tablet.webEventReceived.connect(onMessage);
|
tablet.webEventReceived.connect(onMessage);
|
||||||
HMD.openTablet();
|
HMD.openTablet();
|
||||||
isInSnapshotReview = true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -662,11 +659,10 @@ function maybeDeleteSnapshotStories() {
|
||||||
storyIDsToMaybeDelete = [];
|
storyIDsToMaybeDelete = [];
|
||||||
}
|
}
|
||||||
function onTabletScreenChanged(type, url) {
|
function onTabletScreenChanged(type, url) {
|
||||||
button.editProperties({ isActive: shouldActivateButton });
|
isInSnapshotReview = (type === "Web" && url === SNAPSHOT_REVIEW_URL);
|
||||||
shouldActivateButton = false;
|
button.editProperties({ isActive: isInSnapshotReview });
|
||||||
if (isInSnapshotReview) {
|
if (!isInSnapshotReview) {
|
||||||
tablet.webEventReceived.disconnect(onMessage);
|
tablet.webEventReceived.disconnect(onMessage);
|
||||||
isInSnapshotReview = false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function onUsernameChanged() {
|
function onUsernameChanged() {
|
||||||
|
|
|
@ -20,7 +20,7 @@
|
||||||
var averageLoudness = 0.0;
|
var averageLoudness = 0.0;
|
||||||
var AVERAGING_TIME = 0.9;
|
var AVERAGING_TIME = 0.9;
|
||||||
var LOUDNESS_THRESHOLD = 100;
|
var LOUDNESS_THRESHOLD = 100;
|
||||||
var HYSTERESIS_GAP = 1.41; // 3db gap
|
var HYSTERESIS_GAP = 1.41; // 3dB gap
|
||||||
var MICROPHONE_DISPLAY_NAME = "Microphone";
|
var MICROPHONE_DISPLAY_NAME = "Microphone";
|
||||||
|
|
||||||
var debug = false;
|
var debug = false;
|
||||||
|
@ -54,17 +54,13 @@ Script.update.connect(function () {
|
||||||
print("Muted!");
|
print("Muted!");
|
||||||
}
|
}
|
||||||
isMuted = true;
|
isMuted = true;
|
||||||
if (!AudioDevice.getMuted()) {
|
Audio.muted = true;
|
||||||
AudioDevice.toggleMute();
|
|
||||||
}
|
|
||||||
} else if (isMuted && (averageLoudness < LOUDNESS_THRESHOLD)) {
|
} else if (isMuted && (averageLoudness < LOUDNESS_THRESHOLD)) {
|
||||||
if (debug) {
|
if (debug) {
|
||||||
print("UnMuted!");
|
print("UnMuted!");
|
||||||
}
|
}
|
||||||
isMuted = false;
|
isMuted = false;
|
||||||
if (AudioDevice.getMuted()) {
|
Audio.muted = false;
|
||||||
AudioDevice.toggleMute();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,134 @@
|
||||||
|
// 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 require = Script.require;
|
||||||
|
|
||||||
|
var WANT_DEBUG = false;
|
||||||
|
|
||||||
|
var DopplegangerClass = require('./doppleganger.js'),
|
||||||
|
DopplegangerAttachments = require('./doppleganger-attachments.js'),
|
||||||
|
modelHelper = require('./model-helper.js').modelHelper;
|
||||||
|
|
||||||
|
var tablet = Tablet.getTablet('com.highfidelity.interface.tablet.system'),
|
||||||
|
button = tablet.addButton({
|
||||||
|
icon: Script.resolvePath('./doppleganger-i.svg'),
|
||||||
|
activeIcon: Script.resolvePath('./doppleganger-a.svg'),
|
||||||
|
text: 'MIRROR'
|
||||||
|
});
|
||||||
|
|
||||||
|
Script.scriptEnding.connect(function() {
|
||||||
|
tablet.removeButton(button);
|
||||||
|
button = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
var doppleganger = new DopplegangerClass({
|
||||||
|
avatar: MyAvatar,
|
||||||
|
mirrored: false,
|
||||||
|
autoUpdate: true,
|
||||||
|
type: 'overlay'
|
||||||
|
});
|
||||||
|
|
||||||
|
// add support for displaying regular (non-soft) attachments on the doppleganger
|
||||||
|
{
|
||||||
|
var RECHECK_ATTACHMENT_MS = 1000;
|
||||||
|
var dopplegangerAttachments = new DopplegangerAttachments(doppleganger),
|
||||||
|
attachmentInterval = null,
|
||||||
|
lastHash = dopplegangerAttachments.getAttachmentsHash();
|
||||||
|
|
||||||
|
// monitor for attachment changes, but only when the doppleganger is active
|
||||||
|
doppleganger.activeChanged.connect(function(active, reason) {
|
||||||
|
if (attachmentInterval) {
|
||||||
|
Script.clearInterval(attachmentInterval);
|
||||||
|
}
|
||||||
|
if (active) {
|
||||||
|
attachmentInterval = Script.setInterval(checkAttachmentsHash, RECHECK_ATTACHMENT_MS);
|
||||||
|
} else {
|
||||||
|
attachmentInterval = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
function checkAttachmentsHash() {
|
||||||
|
var currentHash = dopplegangerAttachments.getAttachmentsHash();
|
||||||
|
if (currentHash !== lastHash) {
|
||||||
|
lastHash = currentHash;
|
||||||
|
debugPrint('app-doppleganger | detect attachment change');
|
||||||
|
dopplegangerAttachments.refreshAttachments();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 });
|
||||||
|
debugPrint('doppleganger.activeChanged', active, reason);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// alert the user if there was an error applying their skeletonModelURL
|
||||||
|
doppleganger.modelLoaded.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)) {
|
||||||
|
WANT_DEBUG = true;
|
||||||
|
DopplegangerClass.addDebugControls(doppleganger);
|
||||||
|
}
|
||||||
|
|
||||||
|
function debugPrint() {
|
||||||
|
if (WANT_DEBUG) {
|
||||||
|
print('app-doppleganger | ' + [].slice.call(arguments).join(' '));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
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 {
|
||||||
|
debugPrint('doppleganger stopped:', reason);
|
||||||
|
UserActivityLogger.logAction('doppleganger_autodisable', { reason: reason });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
dopplegangerAttachments.attachmentsUpdated.connect(function(attachments) {
|
||||||
|
UserActivityLogger.logAction('doppleganger_attachments', { count: attachments.length });
|
||||||
|
});
|
||||||
|
|
Before Width: | Height: | Size: 5.8 KiB After Width: | Height: | Size: 5.8 KiB |
|
@ -0,0 +1,238 @@
|
||||||
|
"use strict";
|
||||||
|
/* eslint-env commonjs */
|
||||||
|
/* eslint-disable comma-dangle */
|
||||||
|
|
||||||
|
module.exports = DopplegangerAttachments;
|
||||||
|
|
||||||
|
DopplegangerAttachments.version = '0.0.0';
|
||||||
|
DopplegangerAttachments.WANT_DEBUG = false;
|
||||||
|
|
||||||
|
var _modelHelper = require('./model-helper.js'),
|
||||||
|
modelHelper = _modelHelper.modelHelper,
|
||||||
|
ModelReadyWatcher = _modelHelper.ModelReadyWatcher,
|
||||||
|
utils = require('./utils.js');
|
||||||
|
|
||||||
|
function log() {
|
||||||
|
print('doppleganger-attachments | ' + [].slice.call(arguments).join(' '));
|
||||||
|
}
|
||||||
|
|
||||||
|
function debugPrint() {
|
||||||
|
DopplegangerAttachments.WANT_DEBUG && log.apply(this, arguments);
|
||||||
|
}
|
||||||
|
|
||||||
|
function DopplegangerAttachments(doppleganger, options) {
|
||||||
|
utils.assign(this, {
|
||||||
|
_options: options,
|
||||||
|
doppleganger: doppleganger,
|
||||||
|
attachments: undefined,
|
||||||
|
manualJointSync: true,
|
||||||
|
attachmentsUpdated: utils.signal(function attachmentsUpdated(currentAttachments, previousAttachments){}),
|
||||||
|
});
|
||||||
|
this._initialize();
|
||||||
|
debugPrint('DopplegangerAttachments...', JSON.stringify(options));
|
||||||
|
}
|
||||||
|
DopplegangerAttachments.prototype = {
|
||||||
|
// "hash" the current attachments (so that changes can be detected)
|
||||||
|
getAttachmentsHash: function() {
|
||||||
|
return JSON.stringify(this.doppleganger.avatar.getAttachmentsVariant());
|
||||||
|
},
|
||||||
|
// create a pure Object copy of the current native attachments variant
|
||||||
|
_cloneAttachmentsVariant: function() {
|
||||||
|
return JSON.parse(JSON.stringify(this.doppleganger.avatar.getAttachmentsVariant()));
|
||||||
|
},
|
||||||
|
// fetch and resolve attachments to include jointIndex and other relevant $metadata
|
||||||
|
_getResolvedAttachments: function() {
|
||||||
|
var attachments = this._cloneAttachmentsVariant(),
|
||||||
|
objectID = this.doppleganger.objectID;
|
||||||
|
function toString() {
|
||||||
|
return '[attachment #' + this.$index + ' ' + this.$path + ' @ ' + this.jointName + '{' + this.$jointIndex + '}]';
|
||||||
|
}
|
||||||
|
return attachments.map(function(attachment, i) {
|
||||||
|
var jointIndex = modelHelper.getJointIndex(objectID, attachment.jointName),
|
||||||
|
path = (attachment.modelUrl+'').split(/[?#]/)[0].split('/').slice(-3).join('/');
|
||||||
|
return Object.defineProperties(attachment, {
|
||||||
|
$hash: { value: JSON.stringify(attachment) },
|
||||||
|
$index: { value: i },
|
||||||
|
$jointIndex: { value: jointIndex },
|
||||||
|
$path: { value: path },
|
||||||
|
toString: { value: toString },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
// compare before / after attachment sets to determine which ones need to be (re)created
|
||||||
|
refreshAttachments: function() {
|
||||||
|
var before = this.attachments || [],
|
||||||
|
beforeIndex = before.reduce(function(out, att, index) {
|
||||||
|
out[att.$hash] = index; return out;
|
||||||
|
}, {});
|
||||||
|
var after = this._getResolvedAttachments(),
|
||||||
|
afterIndex = after.reduce(function(out, att, index) {
|
||||||
|
out[att.$hash] = index; return out;
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
Object.keys(beforeIndex).concat(Object.keys(afterIndex)).forEach(function(hash) {
|
||||||
|
if (hash in beforeIndex && hash in afterIndex) {
|
||||||
|
// print('reusing previous attachment', hash);
|
||||||
|
after[afterIndex[hash]] = before[beforeIndex[hash]];
|
||||||
|
} else if (!(hash in afterIndex)) {
|
||||||
|
var attachment = before[beforeIndex[hash]];
|
||||||
|
attachment.properties && attachment.properties.objectID &&
|
||||||
|
modelHelper.deleteObject(attachment.properties.objectID);
|
||||||
|
delete attachment.properties;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.attachments = after;
|
||||||
|
this._createAttachmentObjects();
|
||||||
|
this.attachmentsUpdated(after, before);
|
||||||
|
},
|
||||||
|
_createAttachmentObjects: function() {
|
||||||
|
try {
|
||||||
|
var attachments = this.attachments,
|
||||||
|
parentID = this.doppleganger.objectID,
|
||||||
|
jointNames = this.doppleganger.jointNames,
|
||||||
|
properties = modelHelper.getProperties(this.doppleganger.objectID);
|
||||||
|
|
||||||
|
debugPrint('DopplegangerAttachments..._createAttachmentObjects', JSON.stringify({
|
||||||
|
type: properties.type,
|
||||||
|
attachments: attachments.length,
|
||||||
|
parentID: parentID,
|
||||||
|
jointNames: jointNames.join(' | '),
|
||||||
|
},0,2));
|
||||||
|
return attachments.map(utils.bind(this, function(attachment, i) {
|
||||||
|
var type = modelHelper.type(attachment.properties && attachment.properties.objectID);
|
||||||
|
if (type === 'overlay') {
|
||||||
|
debugPrint('skipping already-provisioned attachment object', type, attachment.properties && attachment.properties.name);
|
||||||
|
return attachment;
|
||||||
|
}
|
||||||
|
var jointIndex = attachment.$jointIndex, // jointNames.indexOf(attachment.jointName),
|
||||||
|
scale = this.doppleganger.avatar.scale * (attachment.scale||1.0);
|
||||||
|
|
||||||
|
attachment.properties = utils.assign({
|
||||||
|
name: attachment.toString(),
|
||||||
|
type: properties.type,
|
||||||
|
modelURL: attachment.modelUrl,
|
||||||
|
scale: scale,
|
||||||
|
dimensions: modelHelper.type(parentID) === 'entity' ?
|
||||||
|
Vec3.multiply(attachment.scale||1.0, Vec3.ONE) : undefined,
|
||||||
|
visible: false,
|
||||||
|
collisionless: true,
|
||||||
|
dynamic: false,
|
||||||
|
shapeType: 'none',
|
||||||
|
lifetime: 60,
|
||||||
|
grabbable: true,
|
||||||
|
}, !this.manualJointSync && {
|
||||||
|
parentID: parentID,
|
||||||
|
parentJointIndex: jointIndex,
|
||||||
|
});
|
||||||
|
var objectID = attachment.properties.objectID = modelHelper.addObject(attachment.properties);
|
||||||
|
attachment._resource = ModelCache.prefetch(attachment.properties.modelURL);
|
||||||
|
attachment._modelReadier = new ModelReadyWatcher( {
|
||||||
|
resource: attachment._resource,
|
||||||
|
objectID: objectID,
|
||||||
|
});
|
||||||
|
this.doppleganger.activeChanged.connect(attachment._modelReadier, '_stop');
|
||||||
|
|
||||||
|
attachment._modelReadier.modelReady.connect(this, function(err, result) {
|
||||||
|
if (err) {
|
||||||
|
log('>>>>> modelReady ERROR <<<<<: ' + err, attachment.modelUrl);
|
||||||
|
modelHelper.deleteObject(objectID);
|
||||||
|
return objectID = null;
|
||||||
|
}
|
||||||
|
debugPrint('attachment model ('+modelHelper.type(result.objectID)+') is ready; # joints ==',
|
||||||
|
result.jointNames && result.jointNames.length, result.naturalDimensions, result.objectID);
|
||||||
|
var properties = modelHelper.getProperties(result.objectID),
|
||||||
|
naturalDimensions = attachment.properties.naturalDimensions = properties.naturalDimensions;
|
||||||
|
modelHelper.editObject(result.objectID, {
|
||||||
|
dimensions: naturalDimensions ? Vec3.multiply(attachment.scale, naturalDimensions) : undefined,
|
||||||
|
});
|
||||||
|
this.onJointsUpdated(parentID); // trigger once to initialize position/rotation
|
||||||
|
// give time for model overlay to "settle", then make it visible
|
||||||
|
Script.setTimeout(utils.bind(this, function() {
|
||||||
|
modelHelper.editObject(result.objectID, {
|
||||||
|
visible: true,
|
||||||
|
});
|
||||||
|
attachment._loaded = true;
|
||||||
|
}), 100);
|
||||||
|
});
|
||||||
|
return attachment;
|
||||||
|
}));
|
||||||
|
} catch (e) {
|
||||||
|
log('_createAttachmentObjects ERROR:', e.stack || e, e.fileName, e.lineNumber);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
_removeAttachmentObjects: function() {
|
||||||
|
if (this.attachments) {
|
||||||
|
this.attachments.forEach(function(attachment) {
|
||||||
|
if (attachment.properties) {
|
||||||
|
if (attachment.properties.objectID) {
|
||||||
|
modelHelper.deleteObject(attachment.properties.objectID);
|
||||||
|
}
|
||||||
|
delete attachment.properties.objectID;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
delete this.attachments;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
onJointsUpdated: function onJointsUpdated(objectID) {
|
||||||
|
var jointOrientations = modelHelper.getJointOrientations(objectID),
|
||||||
|
jointPositions = modelHelper.getJointPositions(objectID),
|
||||||
|
parentID = objectID,
|
||||||
|
avatarScale = this.doppleganger.scale,
|
||||||
|
manualJointSync = this.manualJointSync;
|
||||||
|
|
||||||
|
if (!this.attachments) {
|
||||||
|
this.refreshAttachments();
|
||||||
|
}
|
||||||
|
var updatedObjects = this.attachments.reduce(function(updates, attachment, i) {
|
||||||
|
if (!attachment.properties || !attachment._loaded) {
|
||||||
|
return updates;
|
||||||
|
}
|
||||||
|
var objectID = attachment.properties.objectID,
|
||||||
|
jointIndex = attachment.$jointIndex,
|
||||||
|
jointOrientation = jointOrientations[jointIndex],
|
||||||
|
jointPosition = jointPositions[jointIndex];
|
||||||
|
|
||||||
|
var translation = Vec3.multiply(avatarScale, attachment.translation),
|
||||||
|
rotation = Quat.fromVec3Degrees(attachment.rotation),
|
||||||
|
localPosition = Vec3.multiplyQbyV(jointOrientation, translation),
|
||||||
|
localRotation = rotation;
|
||||||
|
|
||||||
|
updates[objectID] = manualJointSync ? {
|
||||||
|
visible: true,
|
||||||
|
position: Vec3.sum(jointPosition, localPosition),
|
||||||
|
rotation: Quat.multiply(jointOrientation, localRotation),
|
||||||
|
scale: avatarScale * attachment.scale,
|
||||||
|
} : {
|
||||||
|
visible: true,
|
||||||
|
parentID: parentID,
|
||||||
|
parentJointIndex: jointIndex,
|
||||||
|
localRotation: localRotation,
|
||||||
|
localPosition: localPosition,
|
||||||
|
scale: attachment.scale,
|
||||||
|
};
|
||||||
|
onJointsUpdated[objectID] = updates[objectID];
|
||||||
|
return updates;
|
||||||
|
}, {});
|
||||||
|
modelHelper.editObjects(updatedObjects);
|
||||||
|
},
|
||||||
|
|
||||||
|
_initialize: function() {
|
||||||
|
var doppleganger = this.doppleganger;
|
||||||
|
if ('$attachmentControls' in doppleganger) {
|
||||||
|
throw new Error('only one set of debug controls can be added per doppleganger');
|
||||||
|
}
|
||||||
|
doppleganger.$attachmentControls = this;
|
||||||
|
doppleganger.activeChanged.connect(this, function(active) {
|
||||||
|
if (active) {
|
||||||
|
doppleganger.jointsUpdated.connect(this, 'onJointsUpdated');
|
||||||
|
} else {
|
||||||
|
doppleganger.jointsUpdated.disconnect(this, 'onJointsUpdated');
|
||||||
|
this._removeAttachmentObjects();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Script.scriptEnding.connect(this, '_removeAttachmentObjects');
|
||||||
|
},
|
||||||
|
};
|
Before Width: | Height: | Size: 5.9 KiB After Width: | Height: | Size: 5.9 KiB |
|
@ -0,0 +1,515 @@
|
||||||
|
"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
|
||||||
|
//
|
||||||
|
|
||||||
|
/* eslint-env commonjs */
|
||||||
|
// @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;
|
||||||
|
|
||||||
|
var _modelHelper = require('./model-helper.js'),
|
||||||
|
modelHelper = _modelHelper.modelHelper,
|
||||||
|
ModelReadyWatcher = _modelHelper.ModelReadyWatcher;
|
||||||
|
|
||||||
|
// @property {bool} - toggle verbose debug logging on/off
|
||||||
|
Doppleganger.WANT_DEBUG = false;
|
||||||
|
|
||||||
|
// @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;
|
||||||
|
|
||||||
|
// @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) {
|
||||||
|
this.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.objectID = null; // current doppleganger's Overlay or Entity id
|
||||||
|
this.frame = 0; // current joint update frame
|
||||||
|
|
||||||
|
// @signal - emitted when .active state changes
|
||||||
|
this.activeChanged = signal(function(active, reason) {});
|
||||||
|
// @signal - emitted once model is either loaded or errors out
|
||||||
|
this.modelLoaded = signal(function(error, result){});
|
||||||
|
// @signal - emitted each time the model's joint data has been synchronized
|
||||||
|
this.jointsUpdated = signal(function(objectID){});
|
||||||
|
}
|
||||||
|
|
||||||
|
Doppleganger.prototype = {
|
||||||
|
// @public @method - toggles doppleganger on/off
|
||||||
|
toggle: function() {
|
||||||
|
if (this.active) {
|
||||||
|
debugPrint('toggling off');
|
||||||
|
this.stop();
|
||||||
|
} else {
|
||||||
|
debugPrint('toggling on');
|
||||||
|
this.start();
|
||||||
|
}
|
||||||
|
return this.active;
|
||||||
|
},
|
||||||
|
|
||||||
|
// @public @method - synchronize the joint data between Avatar / doppleganger
|
||||||
|
update: function() {
|
||||||
|
this.frame++;
|
||||||
|
try {
|
||||||
|
if (!this.objectID) {
|
||||||
|
throw new Error('!this.objectID');
|
||||||
|
}
|
||||||
|
|
||||||
|
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)) {
|
||||||
|
debugPrint('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;
|
||||||
|
}
|
||||||
|
modelHelper.editObject(this.objectID, {
|
||||||
|
jointRotations: rotations,
|
||||||
|
jointTranslations: translations
|
||||||
|
});
|
||||||
|
this.jointsUpdated(this.objectID);
|
||||||
|
} catch (e) {
|
||||||
|
log('.update error: '+ e, index, e.stack);
|
||||||
|
this.stop('update_error');
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// @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 = assign(this.options, options);
|
||||||
|
if (this.objectID) {
|
||||||
|
log('start() called but object model already exists', this.objectID);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var avatar = this.avatar;
|
||||||
|
if (!avatar.jointNames.length) {
|
||||||
|
return this.stop('joints_unavailable');
|
||||||
|
}
|
||||||
|
|
||||||
|
this.frame = 0;
|
||||||
|
var localPosition = Vec3.multiply(2, Quat.getForward(avatar.orientation));
|
||||||
|
this.position = options.position || Vec3.sum(avatar.position, localPosition);
|
||||||
|
this.orientation = options.orientation || avatar.orientation;
|
||||||
|
this.skeletonModelURL = avatar.skeletonModelURL;
|
||||||
|
this.scale = avatar.scale || 1.0;
|
||||||
|
this.jointStateCount = 0;
|
||||||
|
this.jointNames = avatar.jointNames;
|
||||||
|
this.type = options.type || 'overlay';
|
||||||
|
this.mirroredNames = modelHelper.deriveMirroredJointNames(this.jointNames);
|
||||||
|
this.mirroredIndexes = this.mirroredNames.map(function(name) {
|
||||||
|
return name ? avatar.getJointIndex(name) : false;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.objectID = modelHelper.addObject({
|
||||||
|
type: this.type === 'overlay' ? 'model' : 'Model',
|
||||||
|
modelURL: this.skeletonModelURL,
|
||||||
|
position: this.position,
|
||||||
|
rotation: this.orientation,
|
||||||
|
scale: this.scale
|
||||||
|
});
|
||||||
|
Script.scriptEnding.connect(this, function() {
|
||||||
|
modelHelper.deleteObject(this.objectID);
|
||||||
|
});
|
||||||
|
debugPrint('doppleganger created; objectID =', this.objectID);
|
||||||
|
|
||||||
|
// trigger clean up (and stop updates) if the object gets deleted
|
||||||
|
this.onObjectDeleted = function(uuid) {
|
||||||
|
if (uuid === this.objectID) {
|
||||||
|
log('onObjectDeleted', uuid);
|
||||||
|
this.stop('object_deleted');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
modelHelper.objectDeleted.connect(this, 'onObjectDeleted');
|
||||||
|
|
||||||
|
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.onModelLoaded = function(error, result) {
|
||||||
|
if (error) {
|
||||||
|
return this.stop(error);
|
||||||
|
}
|
||||||
|
debugPrint('model ('+modelHelper.type(this.objectID)+')' + ' is ready; # joints == ' + result.jointNames.length);
|
||||||
|
var naturalDimensions = modelHelper.getProperties(this.objectID, ['naturalDimensions']).naturalDimensions;
|
||||||
|
debugPrint('naturalDimensions:', JSON.stringify(naturalDimensions));
|
||||||
|
var props = { visible: true };
|
||||||
|
if (naturalDimensions) {
|
||||||
|
props.dimensions = Vec3.multiply(this.scale, naturalDimensions);
|
||||||
|
}
|
||||||
|
debugPrint('scaledDimensions:', this.scale, JSON.stringify(props.dimensions));
|
||||||
|
modelHelper.editObject(this.objectID, props);
|
||||||
|
if (!options.position) {
|
||||||
|
this.syncVerticalPosition();
|
||||||
|
}
|
||||||
|
if (this.autoUpdate) {
|
||||||
|
this._createUpdateThread();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
this._resource = ModelCache.prefetch(this.skeletonModelURL);
|
||||||
|
this._modelReadier = new ModelReadyWatcher({
|
||||||
|
resource: this._resource,
|
||||||
|
objectID: this.objectID
|
||||||
|
});
|
||||||
|
this._modelReadier.modelReady.connect(this, 'onModelLoaded');
|
||||||
|
this.activeChanged(this.active = true, 'start');
|
||||||
|
},
|
||||||
|
|
||||||
|
// @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.onObjectDeleted) {
|
||||||
|
modelHelper.objectDeleted.disconnect(this, 'onObjectDeleted');
|
||||||
|
delete this.onObjectDeleted;
|
||||||
|
}
|
||||||
|
if (this.onLoadComplete) {
|
||||||
|
this.avatar.onLoadComplete.disconnect(this, 'onLoadComplete');
|
||||||
|
delete this.onLoadComplete;
|
||||||
|
}
|
||||||
|
if (this.onModelLoaded) {
|
||||||
|
this._modelReadier && this._modelReadier.modelReady.disconnect(this, 'onModelLoaded');
|
||||||
|
this._modelReadier = this.onModelLoaded = undefined;
|
||||||
|
}
|
||||||
|
if (this.objectID) {
|
||||||
|
modelHelper.deleteObject(this.objectID);
|
||||||
|
this.objectID = undefined;
|
||||||
|
}
|
||||||
|
if (this.active) {
|
||||||
|
this.activeChanged(this.active = false, reason);
|
||||||
|
} else if (reason) {
|
||||||
|
debugPrint('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 positions = modelHelper.getJointPositions(this.objectID),
|
||||||
|
properties = modelHelper.getProperties(this.objectID),
|
||||||
|
dopplePosition = properties.position,
|
||||||
|
doppleJointIndex = modelHelper.getJointIndex(this.objectID, byJointName),// names.indexOf(byJointName),
|
||||||
|
doppleJointPosition = positions[doppleJointIndex];
|
||||||
|
|
||||||
|
debugPrint('........... doppleJointPosition', JSON.stringify({
|
||||||
|
byJointName: byJointName,
|
||||||
|
dopplePosition: dopplePosition,
|
||||||
|
doppleJointIndex: doppleJointIndex,
|
||||||
|
doppleJointPosition: doppleJointPosition,
|
||||||
|
properties: properties.type,
|
||||||
|
positions: positions[0]
|
||||||
|
},0,2));
|
||||||
|
var avatarPosition = this.avatar.position,
|
||||||
|
avatarJointIndex = this.avatar.getJointIndex(byJointName),
|
||||||
|
avatarJointPosition = this.avatar.getJointPosition(avatarJointIndex);
|
||||||
|
|
||||||
|
var offset = (avatarJointPosition.y - doppleJointPosition.y);
|
||||||
|
debugPrint('adjusting for offset', offset);
|
||||||
|
if (properties.type === 'model') {
|
||||||
|
dopplePosition.y = avatarPosition.y + offset;
|
||||||
|
} else {
|
||||||
|
dopplePosition.y = avatarPosition.y - offset;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.position = dopplePosition;
|
||||||
|
modelHelper.editObject(this.objectID, { position: this.position });
|
||||||
|
},
|
||||||
|
|
||||||
|
// @private @method - creates the update thread to synchronize joint data
|
||||||
|
_createUpdateThread: function() {
|
||||||
|
if (Doppleganger.USE_SCRIPT_UPDATE) {
|
||||||
|
debugPrint('creating Script.update thread');
|
||||||
|
this.onUpdate = this.update;
|
||||||
|
Script.update.connect(this, 'onUpdate');
|
||||||
|
} else {
|
||||||
|
debugPrint('creating Script.setInterval thread @ ~', Doppleganger.TARGET_FPS +'fps');
|
||||||
|
var timeout = 1000 / Doppleganger.TARGET_FPS;
|
||||||
|
this._interval = Script.setInterval(bind(this, 'update'), timeout);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
// @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) {
|
||||||
|
var callback = {scope: scope, handler: scope[handler] || handler || scope};
|
||||||
|
if (!callback.handler || !callback.handler.apply) {
|
||||||
|
throw new Error('invalid arguments to connect:' + [template, 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);
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign#Polyfill
|
||||||
|
/* eslint-disable */
|
||||||
|
function assign(target, varArgs) { // .length of function is 2
|
||||||
|
'use strict';
|
||||||
|
if (target == null) { // TypeError if undefined or null
|
||||||
|
throw new TypeError('Cannot convert undefined or null to object');
|
||||||
|
}
|
||||||
|
|
||||||
|
var to = Object(target);
|
||||||
|
|
||||||
|
for (var index = 1; index < arguments.length; index++) {
|
||||||
|
var nextSource = arguments[index];
|
||||||
|
|
||||||
|
if (nextSource != null) { // Skip over if undefined or null
|
||||||
|
for (var nextKey in nextSource) {
|
||||||
|
// Avoid bugs when hasOwnProperty is shadowed
|
||||||
|
if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) {
|
||||||
|
to[nextKey] = nextSource[nextKey];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return to;
|
||||||
|
}
|
||||||
|
/* eslint-enable */
|
||||||
|
// //https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign#Polyfill
|
||||||
|
|
||||||
|
// @function - debug logging
|
||||||
|
function log() {
|
||||||
|
print('doppleganger | ' + [].slice.call(arguments).join(' '));
|
||||||
|
}
|
||||||
|
|
||||||
|
function debugPrint() {
|
||||||
|
Doppleganger.WANT_DEBUG && log.apply(this, arguments);
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- 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 = modelHelper.deriveMirroredJointNames([hit.jointName])[0];
|
||||||
|
log('selected joint:', JSON.stringify(hit, 0, 2));
|
||||||
|
});
|
||||||
|
|
||||||
|
Script.scriptEnding.connect(debugControls, 'removeIndicators');
|
||||||
|
|
||||||
|
return doppleganger;
|
||||||
|
};
|
|
@ -0,0 +1,325 @@
|
||||||
|
// model-helper.js
|
||||||
|
//
|
||||||
|
// Created by Timothy Dedischew on 06/01/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
|
||||||
|
//
|
||||||
|
|
||||||
|
/* eslint-env commonjs */
|
||||||
|
// @module model-helper
|
||||||
|
//
|
||||||
|
// This module provides ModelReadyWatcher (a helper class for knowing when a model becomes usable inworld) and
|
||||||
|
// also initial plumbing helpers to eliminate unnecessary API differences when working with Model Overlays and
|
||||||
|
// Model Entities at a high-level from scripting.
|
||||||
|
|
||||||
|
var utils = require('./utils.js'),
|
||||||
|
assert = utils.assert;
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
version: '0.0.0',
|
||||||
|
ModelReadyWatcher: ModelReadyWatcher
|
||||||
|
};
|
||||||
|
|
||||||
|
var _objectDeleted = utils.signal(function objectDeleted(objectID){});
|
||||||
|
// proxy for _objectDeleted that only binds deletion tracking if script actually connects to the unified signal
|
||||||
|
var objectDeleted = utils.assign(function objectDeleted(objectID){}, {
|
||||||
|
connect: function() {
|
||||||
|
Overlays.overlayDeleted.connect(_objectDeleted);
|
||||||
|
// Entities.deletingEntity.connect(objectDeleted);
|
||||||
|
Script.scriptEnding.connect(function() {
|
||||||
|
Overlays.overlayDeleted.disconnect(_objectDeleted);
|
||||||
|
// Entities.deletingEntity.disconnect(objectDeleted);
|
||||||
|
});
|
||||||
|
// hereafter _objectDeleted.connect will be used instead
|
||||||
|
this.connect = utils.bind(_objectDeleted, 'connect');
|
||||||
|
return this.connect.apply(this, arguments);
|
||||||
|
},
|
||||||
|
disconnect: utils.bind(_objectDeleted, 'disconnect')
|
||||||
|
});
|
||||||
|
|
||||||
|
var modelHelper = module.exports.modelHelper = {
|
||||||
|
// Entity <-> Overlay property translations
|
||||||
|
_entityFromOverlay: {
|
||||||
|
modelURL: function url() {
|
||||||
|
return this.url;
|
||||||
|
},
|
||||||
|
dimensions: function dimensions() {
|
||||||
|
return Vec3.multiply(this.scale, this.naturalDimensions);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
_overlayFromEntity: {
|
||||||
|
url: function modelURL() {
|
||||||
|
return this.modelURL;
|
||||||
|
},
|
||||||
|
scale: function scale() {
|
||||||
|
return this.dimensions && this.naturalDimensions && {
|
||||||
|
x: this.dimensions.x / this.naturalDimensions.x,
|
||||||
|
y: this.dimensions.y / this.naturalDimensions.y,
|
||||||
|
z: this.dimensions.z / this.naturalDimensions.z
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
objectDeleted: objectDeleted,
|
||||||
|
type: function(objectID) {
|
||||||
|
// TODO: support Model Entities (by detecting type from objectID, which is already possible)
|
||||||
|
return !Uuid.isNull(objectID) ? 'overlay' : null;
|
||||||
|
},
|
||||||
|
addObject: function(properties) {
|
||||||
|
var type = 'overlay'; // this.resolveType(properties)
|
||||||
|
switch (type) {
|
||||||
|
case 'overlay': return Overlays.addOverlay(properties.type, this.toOverlayProps(properties));
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
editObject: function(objectID, properties) {
|
||||||
|
switch (this.type(objectID)) {
|
||||||
|
case 'overlay': return Overlays.editOverlay(objectID, this.toOverlayProps(properties));
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
deleteObject: function(objectID) {
|
||||||
|
return this.type(objectID) === 'overlay' && Overlays.deleteOverlay(objectID);
|
||||||
|
},
|
||||||
|
getProperty: function(objectID, propertyName) {
|
||||||
|
return this.type(objectID) === 'overlay' && Overlays.getProperty(objectID, propertyName);
|
||||||
|
},
|
||||||
|
getProperties: function(objectID, filter) {
|
||||||
|
switch (this.type(objectID)) {
|
||||||
|
case 'overlay':
|
||||||
|
filter = Array.isArray(filter) ? filter : [
|
||||||
|
'position', 'rotation', 'localPosition', 'localRotation', 'parentID',
|
||||||
|
'parentJointIndex', 'scale', 'name', 'visible', 'type', 'url',
|
||||||
|
'dimensions', 'naturalDimensions', 'grabbable'
|
||||||
|
];
|
||||||
|
var properties = filter.reduce(function(out, propertyName) {
|
||||||
|
out[propertyName] = Overlays.getProperty(objectID, propertyName);
|
||||||
|
return out;
|
||||||
|
}, {});
|
||||||
|
return this.toEntityProps(properties);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
// adapt Entity conventions to Overlay (eg: { modelURL: ... } -> { url: ... })
|
||||||
|
toOverlayProps: function(properties) {
|
||||||
|
var result = {};
|
||||||
|
for (var from in this._overlayFromEntity) {
|
||||||
|
var adapter = this._overlayFromEntity[from];
|
||||||
|
result[from] = adapter.call(properties, from, adapter.name);
|
||||||
|
}
|
||||||
|
return utils.assign(result, properties);
|
||||||
|
},
|
||||||
|
// adapt Overlay conventions to Entity (eg: { url: ... } -> { modelURL: ... })
|
||||||
|
toEntityProps: function(properties) {
|
||||||
|
var result = {};
|
||||||
|
for (var from in this._entityToOverlay) {
|
||||||
|
var adapter = this._entityToOverlay[from];
|
||||||
|
result[from] = adapter.call(properties, from, adapter.name);
|
||||||
|
}
|
||||||
|
return utils.assign(result, properties);
|
||||||
|
},
|
||||||
|
editObjects: function(updatedObjects) {
|
||||||
|
var objectIDs = Object.keys(updatedObjects),
|
||||||
|
type = this.type(objectIDs[0]);
|
||||||
|
switch (type) {
|
||||||
|
case 'overlay':
|
||||||
|
var translated = {};
|
||||||
|
for (var objectID in updatedObjects) {
|
||||||
|
translated[objectID] = this.toOverlayProps(updatedObjects[objectID]);
|
||||||
|
}
|
||||||
|
return Overlays.editOverlays(translated);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
getJointIndex: function(objectID, name) {
|
||||||
|
switch (this.type(objectID)) {
|
||||||
|
case 'overlay': return Overlays.getProperty(objectID, 'jointNames').indexOf(name);
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
},
|
||||||
|
getJointNames: function(objectID) {
|
||||||
|
switch (this.type(objectID)) {
|
||||||
|
case 'overlay': return Overlays.getProperty(objectID, 'jointNames');
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
},
|
||||||
|
// @function - derives 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`)
|
||||||
|
deriveMirroredJointNames: 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;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
getJointPosition: function(objectID, index) {
|
||||||
|
switch (this.type(objectID)) {
|
||||||
|
case 'overlay': return Overlays.getProperty(objectID, 'jointPositions')[index];
|
||||||
|
}
|
||||||
|
return Vec3.ZERO;
|
||||||
|
},
|
||||||
|
getJointPositions: function(objectID) {
|
||||||
|
switch (this.type(objectID)) {
|
||||||
|
case 'overlay': return Overlays.getProperty(objectID, 'jointPositions');
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
},
|
||||||
|
getJointOrientation: function(objectID, index) {
|
||||||
|
switch (this.type(objectID)) {
|
||||||
|
case 'overlay': return Overlays.getProperty(objectID, 'jointOrientations')[index];
|
||||||
|
}
|
||||||
|
return Quat.normalize({});
|
||||||
|
},
|
||||||
|
getJointOrientations: function(objectID) {
|
||||||
|
switch (this.type(objectID)) {
|
||||||
|
case 'overlay': return Overlays.getProperty(objectID, 'jointOrientations');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getJointTranslation: function(objectID, index) {
|
||||||
|
switch (this.type(objectID)) {
|
||||||
|
case 'overlay': return Overlays.getProperty(objectID, 'jointTranslations')[index];
|
||||||
|
}
|
||||||
|
return Vec3.ZERO;
|
||||||
|
},
|
||||||
|
getJointTranslations: function(objectID) {
|
||||||
|
switch (this.type(objectID)) {
|
||||||
|
case 'overlay': return Overlays.getProperty(objectID, 'jointTranslations');
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
},
|
||||||
|
getJointRotation: function(objectID, index) {
|
||||||
|
switch (this.type(objectID)) {
|
||||||
|
case 'overlay': return Overlays.getProperty(objectID, 'jointRotations')[index];
|
||||||
|
}
|
||||||
|
return Quat.normalize({});
|
||||||
|
},
|
||||||
|
getJointRotations: function(objectID) {
|
||||||
|
switch (this.type(objectID)) {
|
||||||
|
case 'overlay': return Overlays.getProperty(objectID, 'jointRotations');
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}; // modelHelper
|
||||||
|
|
||||||
|
|
||||||
|
// @property {PreconditionFunction} - indicates when the model's jointNames have become available
|
||||||
|
ModelReadyWatcher.JOINTS = function(state) {
|
||||||
|
return Array.isArray(state.jointNames);
|
||||||
|
};
|
||||||
|
// @property {PreconditionFunction} - indicates when a model's naturalDimensions have become available
|
||||||
|
ModelReadyWatcher.DIMENSIONS = function(state) {
|
||||||
|
return Vec3.length(state.naturalDimensions) > 0;
|
||||||
|
};
|
||||||
|
// @property {PreconditionFunction} - indicates when both a model's naturalDimensions and jointNames have become available
|
||||||
|
ModelReadyWatcher.JOINTS_AND_DIMENSIONS = function(state) {
|
||||||
|
// eslint-disable-next-line new-cap
|
||||||
|
return ModelReadyWatcher.JOINTS(state) && ModelReadyWatcher.DIMENSIONS(state);
|
||||||
|
};
|
||||||
|
// @property {int} - interval used for continually rechecking model readiness, until ready or a timeout occurs
|
||||||
|
ModelReadyWatcher.RECHECK_MS = 50;
|
||||||
|
// @property {int} - default wait time before considering a model unready-able.
|
||||||
|
ModelReadyWatcher.DEFAULT_TIMEOUT_SECS = 10;
|
||||||
|
|
||||||
|
// @private @class - waits for model to become usable inworld and tracks errors/timeouts
|
||||||
|
// @param [Object} options -- key/value config options:
|
||||||
|
// @param {ModelResource} options.resource - a ModelCache prefetched resource to observe for determining load state
|
||||||
|
// @param {Uuid} options.objectID - an inworld object to observe for determining readiness states
|
||||||
|
// @param {Function} [options.precondition=ModelReadyWatcher.JOINTS] - the precondition used to determine if the model is usable
|
||||||
|
// @param {int} [options.maxWaitSeconds=10] - max seconds to wait for the model to become usable, after which a timeout error is emitted
|
||||||
|
// @return {ModelReadyWatcher}
|
||||||
|
function ModelReadyWatcher(options) {
|
||||||
|
options = utils.assign({
|
||||||
|
precondition: ModelReadyWatcher.JOINTS,
|
||||||
|
maxWaitSeconds: ModelReadyWatcher.DEFAULT_TIMEOUT_SECS
|
||||||
|
}, options);
|
||||||
|
|
||||||
|
assert(!Uuid.isNull(options.objectID), 'Error: invalid options.objectID');
|
||||||
|
assert(options.resource && 'state' in options.resource, 'Error: invalid options.resource');
|
||||||
|
assert(typeof options.precondition === 'function', 'Error: invalid options.precondition');
|
||||||
|
|
||||||
|
utils.assign(this, {
|
||||||
|
resource: options.resource,
|
||||||
|
objectID: options.objectID,
|
||||||
|
precondition: options.precondition,
|
||||||
|
|
||||||
|
// @signal - emitted when the model becomes ready, or with the error that prevented it
|
||||||
|
modelReady: utils.signal(function modelReady(error, result){}),
|
||||||
|
|
||||||
|
// @public
|
||||||
|
ready: false, // tracks readiness state
|
||||||
|
jointNames: null, // populated with detected jointNames
|
||||||
|
naturalDimensions: null, // populated with detected naturalDimensions
|
||||||
|
|
||||||
|
_startTime: new Date,
|
||||||
|
_watchdogTimer: Script.setTimeout(utils.bind(this, function() {
|
||||||
|
this._watchdogTimer = null;
|
||||||
|
}), options.maxWaitSeconds * 1000),
|
||||||
|
_interval: Script.setInterval(utils.bind(this, '_waitUntilReady'), ModelReadyWatcher.RECHECK_MS)
|
||||||
|
});
|
||||||
|
|
||||||
|
this.modelReady.connect(this, function(error, result) {
|
||||||
|
this.ready = !error && result;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
ModelReadyWatcher.prototype = {
|
||||||
|
contructor: ModelReadyWatcher,
|
||||||
|
// @public method -- cancels monitoring and (if model was not yet ready) emits an error
|
||||||
|
cancel: function() {
|
||||||
|
return this._stop() && !this.ready && this.modelReady('cancelled', null);
|
||||||
|
},
|
||||||
|
// stop pending timers
|
||||||
|
_stop: function() {
|
||||||
|
var stopped = 0;
|
||||||
|
if (this._watchdogTimer) {
|
||||||
|
Script.clearTimeout(this._watchdogTimer);
|
||||||
|
this._watchdogTimer = null;
|
||||||
|
stopped++;
|
||||||
|
}
|
||||||
|
if (this._interval) {
|
||||||
|
Script.clearInterval(this._interval);
|
||||||
|
this._interval = null;
|
||||||
|
stopped++;
|
||||||
|
}
|
||||||
|
return stopped;
|
||||||
|
},
|
||||||
|
// the monitoring thread func
|
||||||
|
_waitUntilReady: function() {
|
||||||
|
var error = null, result = null;
|
||||||
|
if (!this._watchdogTimer) {
|
||||||
|
error = this.precondition.name || 'timeout';
|
||||||
|
} else if (this.resource.state === Resource.State.FAILED) {
|
||||||
|
error = 'prefetch_failed';
|
||||||
|
} else if (this.resource.state === Resource.State.FINISHED) {
|
||||||
|
// in theory there will always be at least one "joint name" that represents the main submesh
|
||||||
|
var names = modelHelper.getJointNames(this.objectID);
|
||||||
|
if (Array.isArray(names) && names.length) {
|
||||||
|
this.jointNames = names;
|
||||||
|
}
|
||||||
|
var props = modelHelper.getProperties(this.objectID, ['naturalDimensions']);
|
||||||
|
if (props && props.naturalDimensions && Vec3.length(props.naturalDimensions)) {
|
||||||
|
this.naturalDimensions = props.naturalDimensions;
|
||||||
|
}
|
||||||
|
var state = {
|
||||||
|
resource: this.resource,
|
||||||
|
objectID: this.objectID,
|
||||||
|
waitTime: (new Date - this._startTime) / 1000,
|
||||||
|
jointNames: this.jointNames,
|
||||||
|
naturalDimensions: this.naturalDimensions
|
||||||
|
};
|
||||||
|
if (this.precondition(state)) {
|
||||||
|
result = state;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (error || result !== null) {
|
||||||
|
this._stop();
|
||||||
|
this.modelReady(error, result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}; // ModelReadyWatcher.prototype
|
|
@ -0,0 +1,99 @@
|
||||||
|
/* eslint-env commonjs */
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
bind: bind,
|
||||||
|
signal: signal,
|
||||||
|
assign: assign,
|
||||||
|
assert: assert
|
||||||
|
};
|
||||||
|
|
||||||
|
// @function - bind a function to a `this` context
|
||||||
|
// @param {Object} - the `this` context
|
||||||
|
// @param {Function|String} - function or method name
|
||||||
|
// @param {value} varargs... - optional curry-right arguments (passed to method after any explicit arguments)
|
||||||
|
function bind(thiz, method, varargs) {
|
||||||
|
method = thiz[method] || method;
|
||||||
|
varargs = [].slice.call(arguments, 2);
|
||||||
|
return function() {
|
||||||
|
var args = [].slice.call(arguments).concat(varargs);
|
||||||
|
return method.apply(thiz, args);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// @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) {
|
||||||
|
var callback = {scope: scope, handler: scope[handler] || handler || scope};
|
||||||
|
if (!callback.handler || !callback.handler.apply) {
|
||||||
|
throw new Error('invalid arguments to connect:' + [template, 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);
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign#Polyfill
|
||||||
|
/* eslint-disable */
|
||||||
|
function assign(target, varArgs) { // .length of function is 2
|
||||||
|
'use strict';
|
||||||
|
if (target == null) { // TypeError if undefined or null
|
||||||
|
throw new TypeError('Cannot convert undefined or null to object');
|
||||||
|
}
|
||||||
|
|
||||||
|
var to = Object(target);
|
||||||
|
|
||||||
|
for (var index = 1; index < arguments.length; index++) {
|
||||||
|
var nextSource = arguments[index];
|
||||||
|
|
||||||
|
if (nextSource != null) { // Skip over if undefined or null
|
||||||
|
for (var nextKey in nextSource) {
|
||||||
|
// Avoid bugs when hasOwnProperty is shadowed
|
||||||
|
if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) {
|
||||||
|
to[nextKey] = nextSource[nextKey];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return to;
|
||||||
|
}
|
||||||
|
/* eslint-enable */
|
||||||
|
// //https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign#Polyfill
|
||||||
|
|
||||||
|
// examples:
|
||||||
|
// assert(function assertion() { return (conditions === true) }, 'assertion failed!')
|
||||||
|
// var neededValue = assert(idString, 'idString not specified!');
|
||||||
|
// assert(false, 'unexpected state');
|
||||||
|
function assert(truthy, message) {
|
||||||
|
message = message || 'Assertion Failed:';
|
||||||
|
|
||||||
|
if (typeof truthy === 'function' && truthy.name === 'assertion') {
|
||||||
|
// extract function body to display with the assertion message
|
||||||
|
var assertion = (truthy+'').replace(/[\r\n]/g, ' ')
|
||||||
|
.replace(/^[^{]+\{|\}$|^\s*|\s*$/g, '').trim()
|
||||||
|
.replace(/^return /,'').replace(/\s[\r\n\t\s]+/g, ' ');
|
||||||
|
message += ' ' + JSON.stringify(assertion);
|
||||||
|
try {
|
||||||
|
truthy = truthy();
|
||||||
|
} catch (e) {
|
||||||
|
message += '(exception: ' + e +')';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!truthy) {
|
||||||
|
message += ' ('+truthy+')';
|
||||||
|
throw new Error(message);
|
||||||
|
}
|
||||||
|
return truthy;
|
||||||
|
}
|
|
@ -14,8 +14,8 @@ var DopplegangerClass = Script.require('./doppleganger.js');
|
||||||
|
|
||||||
var tablet = Tablet.getTablet('com.highfidelity.interface.tablet.system'),
|
var tablet = Tablet.getTablet('com.highfidelity.interface.tablet.system'),
|
||||||
button = tablet.addButton({
|
button = tablet.addButton({
|
||||||
icon: "icons/tablet-icons/doppleganger-i.svg",
|
icon: Script.resolvePath('./doppleganger-i.svg'),
|
||||||
activeIcon: "icons/tablet-icons/doppleganger-a.svg",
|
activeIcon: Script.resolvePath('./doppleganger-a.svg'),
|
||||||
text: 'MIRROR'
|
text: 'MIRROR'
|
||||||
});
|
});
|
||||||
|
|
|
@ -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 |
|
@ -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 |
Loading…
Reference in a new issue