Merge pull request #9600 from highfidelity/RC-33
Beta Release 33 - Includes up to Developer Release 5990
|
@ -58,9 +58,9 @@ In a new Terminal window, run:
|
|||
|
||||
Any target can be terminated with Ctrl-C (SIGINT) in the associated Terminal window.
|
||||
|
||||
This assignment-client will grab one assignment from the domain-server. You can tell the assignment-client what type you want it to be with the `-t` option. You can also run an assignment-client that forks off *n* assignment-clients with the `-n` option.
|
||||
This assignment-client will grab one assignment from the domain-server. You can tell the assignment-client what type you want it to be with the `-t` option. You can also run an assignment-client that forks off *n* assignment-clients with the `-n` option. The `-min` and `-max` options allow you to set a range of required assignment-clients, this allows you to have flexibility in the number of assignment-clients that are running. See `--help` for more options.
|
||||
|
||||
./assignment-client -n 4
|
||||
./assignment-client --min 6 --max 20
|
||||
|
||||
To test things out you'll want to run the Interface client.
|
||||
|
||||
|
|
|
@ -9,6 +9,8 @@
|
|||
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
|
||||
//
|
||||
|
||||
#include "Agent.h"
|
||||
|
||||
#include <QtCore/QCoreApplication>
|
||||
#include <QtCore/QEventLoop>
|
||||
#include <QtCore/QStandardPaths>
|
||||
|
@ -46,14 +48,12 @@
|
|||
#include "RecordingScriptingInterface.h"
|
||||
#include "AbstractAudioInterface.h"
|
||||
|
||||
#include "Agent.h"
|
||||
#include "AvatarAudioTimer.h"
|
||||
|
||||
static const int RECEIVED_AUDIO_STREAM_CAPACITY_FRAMES = 10;
|
||||
|
||||
Agent::Agent(ReceivedMessage& message) :
|
||||
ThreadedAssignment(message),
|
||||
_entityEditSender(),
|
||||
_receivedAudioStream(RECEIVED_AUDIO_STREAM_CAPACITY_FRAMES, RECEIVED_AUDIO_STREAM_CAPACITY_FRAMES) {
|
||||
DependencyManager::get<EntityScriptingInterface>()->setPacketSender(&_entityEditSender);
|
||||
|
||||
|
@ -68,7 +68,7 @@ Agent::Agent(ReceivedMessage& message) :
|
|||
DependencyManager::set<recording::Recorder>();
|
||||
DependencyManager::set<RecordingScriptingInterface>();
|
||||
DependencyManager::set<ScriptCache>();
|
||||
DependencyManager::set<ScriptEngines>();
|
||||
DependencyManager::set<ScriptEngines>(ScriptEngine::AGENT_SCRIPT);
|
||||
|
||||
auto& packetReceiver = DependencyManager::get<NodeList>()->getPacketReceiver();
|
||||
|
||||
|
@ -143,7 +143,7 @@ void Agent::handleAudioPacket(QSharedPointer<ReceivedMessage> message) {
|
|||
_receivedAudioStream.clearBuffer();
|
||||
}
|
||||
|
||||
const QString AGENT_LOGGING_NAME = "agent";
|
||||
static const QString AGENT_LOGGING_NAME = "agent";
|
||||
|
||||
void Agent::run() {
|
||||
|
||||
|
@ -266,6 +266,9 @@ void Agent::handleSelectedAudioFormat(QSharedPointer<ReceivedMessage> message) {
|
|||
}
|
||||
|
||||
void Agent::selectAudioFormat(const QString& selectedCodecName) {
|
||||
if (_selectedCodecName == selectedCodecName) {
|
||||
return;
|
||||
}
|
||||
_selectedCodecName = selectedCodecName;
|
||||
|
||||
qDebug() << "Selected Codec:" << _selectedCodecName;
|
||||
|
@ -321,7 +324,7 @@ void Agent::scriptRequestFinished() {
|
|||
}
|
||||
|
||||
void Agent::executeScript() {
|
||||
_scriptEngine = std::unique_ptr<ScriptEngine>(new ScriptEngine(_scriptContents, _payload));
|
||||
_scriptEngine = std::unique_ptr<ScriptEngine>(new ScriptEngine(ScriptEngine::AGENT_SCRIPT, _scriptContents, _payload));
|
||||
_scriptEngine->setParent(this); // be the parent of the script engine so it gets moved when we do
|
||||
|
||||
// setup an Avatar for the script to use
|
||||
|
@ -351,9 +354,15 @@ void Agent::executeScript() {
|
|||
Transform audioTransform;
|
||||
audioTransform.setTranslation(scriptedAvatar->getPosition());
|
||||
audioTransform.setRotation(scriptedAvatar->getOrientation());
|
||||
AbstractAudioInterface::emitAudioPacket(audio.data(), audio.size(), audioSequenceNumber,
|
||||
QByteArray encodedBuffer;
|
||||
if (_encoder) {
|
||||
_encoder->encode(audio, encodedBuffer);
|
||||
} else {
|
||||
encodedBuffer = audio;
|
||||
}
|
||||
AbstractAudioInterface::emitAudioPacket(encodedBuffer.data(), encodedBuffer.size(), audioSequenceNumber,
|
||||
audioTransform, scriptedAvatar->getPosition(), glm::vec3(0),
|
||||
PacketType::MicrophoneAudioNoEcho);
|
||||
PacketType::MicrophoneAudioNoEcho, _selectedCodecName);
|
||||
});
|
||||
|
||||
auto avatarHashMap = DependencyManager::set<AvatarHashMap>();
|
||||
|
@ -376,6 +385,9 @@ void Agent::executeScript() {
|
|||
|
||||
_scriptEngine->registerGlobalObject("EntityViewer", &_entityViewer);
|
||||
|
||||
auto recordingInterface = DependencyManager::get<RecordingScriptingInterface>();
|
||||
_scriptEngine->registerGlobalObject("Recording", recordingInterface.data());
|
||||
|
||||
// we need to make sure that init has been called for our EntityScriptingInterface
|
||||
// so that it actually has a jurisdiction listener when we ask it for it next
|
||||
entityScriptingInterface->init();
|
||||
|
@ -499,8 +511,8 @@ void Agent::processAgentAvatar() {
|
|||
if (!_scriptEngine->isFinished() && _isAvatar) {
|
||||
auto scriptedAvatar = DependencyManager::get<ScriptableAvatar>();
|
||||
|
||||
QByteArray avatarByteArray = scriptedAvatar->toByteArray((randFloat() < AVATAR_SEND_FULL_UPDATE_RATIO)
|
||||
? AvatarData::SendAllData : AvatarData::CullSmallData);
|
||||
AvatarData::AvatarDataDetail dataDetail = (randFloat() < AVATAR_SEND_FULL_UPDATE_RATIO) ? AvatarData::SendAllData : AvatarData::CullSmallData;
|
||||
QByteArray avatarByteArray = scriptedAvatar->toByteArray(dataDetail, 0, scriptedAvatar->getLastSentJointData());
|
||||
scriptedAvatar->doneEncoding(true);
|
||||
|
||||
static AvatarDataSequenceNumber sequenceNumber = 0;
|
||||
|
|
|
@ -12,12 +12,13 @@
|
|||
#include <udt/PacketHeaders.h>
|
||||
|
||||
#include "Agent.h"
|
||||
#include "assets/AssetServer.h"
|
||||
#include "AssignmentFactory.h"
|
||||
#include "audio/AudioMixer.h"
|
||||
#include "avatars/AvatarMixer.h"
|
||||
#include "entities/EntityServer.h"
|
||||
#include "assets/AssetServer.h"
|
||||
#include "messages/MessagesMixer.h"
|
||||
#include "scripts/EntityScriptServer.h"
|
||||
|
||||
ThreadedAssignment* AssignmentFactory::unpackAssignment(ReceivedMessage& message) {
|
||||
|
||||
|
@ -39,7 +40,9 @@ ThreadedAssignment* AssignmentFactory::unpackAssignment(ReceivedMessage& message
|
|||
return new AssetServer(message);
|
||||
case Assignment::MessagesMixerType:
|
||||
return new MessagesMixer(message);
|
||||
case Assignment::EntityScriptServerType:
|
||||
return new EntityScriptServer(message);
|
||||
default:
|
||||
return NULL;
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,6 +12,8 @@
|
|||
|
||||
#include "AssetServer.h"
|
||||
|
||||
#include <thread>
|
||||
|
||||
#include <QtCore/QCoreApplication>
|
||||
#include <QtCore/QCryptographicHash>
|
||||
#include <QtCore/QDateTime>
|
||||
|
@ -21,15 +23,55 @@
|
|||
#include <QtCore/QJsonDocument>
|
||||
#include <QtCore/QString>
|
||||
|
||||
#include <SharedUtil.h>
|
||||
#include <ServerPathUtils.h>
|
||||
|
||||
#include "NetworkLogging.h"
|
||||
#include "NodeType.h"
|
||||
#include "SendAssetTask.h"
|
||||
#include "UploadAssetTask.h"
|
||||
#include <ClientServerUtils.h>
|
||||
|
||||
static const uint8_t MIN_CORES_FOR_MULTICORE = 4;
|
||||
static const uint8_t CPU_AFFINITY_COUNT_HIGH = 2;
|
||||
static const uint8_t CPU_AFFINITY_COUNT_LOW = 1;
|
||||
#ifdef Q_OS_WIN
|
||||
static const int INTERFACE_RUNNING_CHECK_FREQUENCY_MS = 1000;
|
||||
#endif
|
||||
|
||||
const QString ASSET_SERVER_LOGGING_TARGET_NAME = "asset-server";
|
||||
|
||||
bool interfaceRunning() {
|
||||
bool result = false;
|
||||
|
||||
#ifdef Q_OS_WIN
|
||||
QSharedMemory sharedMemory { getInterfaceSharedMemoryName() };
|
||||
result = sharedMemory.attach(QSharedMemory::ReadOnly);
|
||||
if (result) {
|
||||
sharedMemory.detach();
|
||||
}
|
||||
#endif
|
||||
return result;
|
||||
}
|
||||
|
||||
void updateConsumedCores() {
|
||||
static bool wasInterfaceRunning = false;
|
||||
bool isInterfaceRunning = interfaceRunning();
|
||||
// If state is unchanged, return early
|
||||
if (isInterfaceRunning == wasInterfaceRunning) {
|
||||
return;
|
||||
}
|
||||
|
||||
wasInterfaceRunning = isInterfaceRunning;
|
||||
auto coreCount = std::thread::hardware_concurrency();
|
||||
if (isInterfaceRunning) {
|
||||
coreCount = coreCount > MIN_CORES_FOR_MULTICORE ? CPU_AFFINITY_COUNT_HIGH : CPU_AFFINITY_COUNT_LOW;
|
||||
}
|
||||
qDebug() << "Setting max consumed cores to " << coreCount;
|
||||
setMaxCores(coreCount);
|
||||
}
|
||||
|
||||
|
||||
AssetServer::AssetServer(ReceivedMessage& message) :
|
||||
ThreadedAssignment(message),
|
||||
_taskPool(this)
|
||||
|
@ -45,6 +87,20 @@ AssetServer::AssetServer(ReceivedMessage& message) :
|
|||
packetReceiver.registerListener(PacketType::AssetGetInfo, this, "handleAssetGetInfo");
|
||||
packetReceiver.registerListener(PacketType::AssetUpload, this, "handleAssetUpload");
|
||||
packetReceiver.registerListener(PacketType::AssetMappingOperation, this, "handleAssetMappingOperation");
|
||||
|
||||
#ifdef Q_OS_WIN
|
||||
updateConsumedCores();
|
||||
QTimer* timer = new QTimer(this);
|
||||
auto timerConnection = connect(timer, &QTimer::timeout, [] {
|
||||
updateConsumedCores();
|
||||
});
|
||||
connect(qApp, &QCoreApplication::aboutToQuit, [this, timerConnection] {
|
||||
disconnect(timerConnection);
|
||||
});
|
||||
timer->setInterval(INTERFACE_RUNNING_CHECK_FREQUENCY_MS);
|
||||
timer->setTimerType(Qt::CoarseTimer);
|
||||
timer->start();
|
||||
#endif
|
||||
}
|
||||
|
||||
void AssetServer::run() {
|
||||
|
@ -137,7 +193,7 @@ void AssetServer::completeSetup() {
|
|||
cleanupUnmappedFiles();
|
||||
}
|
||||
|
||||
nodeList->addNodeTypeToInterestSet(NodeType::Agent);
|
||||
nodeList->addSetOfNodeTypesToNodeInterestSet({ NodeType::Agent, NodeType::EntityScriptServer });
|
||||
} else {
|
||||
qCritical() << "Asset Server assignment will not continue because mapping file could not be loaded.";
|
||||
setFinished(true);
|
||||
|
|
|
@ -21,6 +21,7 @@
|
|||
#include <udt/Packet.h>
|
||||
|
||||
#include "AssetUtils.h"
|
||||
#include "ClientServerUtils.h"
|
||||
|
||||
SendAssetTask::SendAssetTask(QSharedPointer<ReceivedMessage> message, const SharedNodePointer& sendToNode, const QDir& resourcesDir) :
|
||||
QRunnable(),
|
||||
|
|
|
@ -18,6 +18,8 @@
|
|||
#include <NodeList.h>
|
||||
#include <NLPacketList.h>
|
||||
|
||||
#include "ClientServerUtils.h"
|
||||
|
||||
|
||||
UploadAssetTask::UploadAssetTask(QSharedPointer<ReceivedMessage> receivedMessage, SharedNodePointer senderNode,
|
||||
const QDir& resourcesDir) :
|
||||
|
|
|
@ -28,6 +28,7 @@
|
|||
#include <StDev.h>
|
||||
#include <UUID.h>
|
||||
|
||||
#include "AudioHelpers.h"
|
||||
#include "AudioRingBuffer.h"
|
||||
#include "AudioMixerClientData.h"
|
||||
#include "AvatarAudioStream.h"
|
||||
|
@ -35,9 +36,8 @@
|
|||
|
||||
#include "AudioMixer.h"
|
||||
|
||||
static const float LOUDNESS_TO_DISTANCE_RATIO = 0.00001f;
|
||||
static const float DEFAULT_ATTENUATION_PER_DOUBLING_IN_DISTANCE = 0.5f; // attenuation = -6dB * log2(distance)
|
||||
static const float DEFAULT_NOISE_MUTING_THRESHOLD = 0.003f;
|
||||
static const float DEFAULT_NOISE_MUTING_THRESHOLD = 1.0f;
|
||||
static const QString AUDIO_MIXER_LOGGING_TARGET_NAME = "audio-mixer";
|
||||
static const QString AUDIO_ENV_GROUP_KEY = "audio_env";
|
||||
static const QString AUDIO_BUFFER_GROUP_KEY = "audio_buffer";
|
||||
|
@ -46,9 +46,6 @@ static const QString AUDIO_THREADING_GROUP_KEY = "audio_threading";
|
|||
int AudioMixer::_numStaticJitterFrames{ -1 };
|
||||
float AudioMixer::_noiseMutingThreshold{ DEFAULT_NOISE_MUTING_THRESHOLD };
|
||||
float AudioMixer::_attenuationPerDoublingInDistance{ DEFAULT_ATTENUATION_PER_DOUBLING_IN_DISTANCE };
|
||||
float AudioMixer::_trailingSleepRatio{ 1.0f };
|
||||
float AudioMixer::_performanceThrottlingRatio{ 0.0f };
|
||||
float AudioMixer::_minAudibilityThreshold{ LOUDNESS_TO_DISTANCE_RATIO / 2.0f };
|
||||
QHash<QString, AABox> AudioMixer::_audioZones;
|
||||
QVector<AudioMixer::ZoneSettings> AudioMixer::_zoneSettings;
|
||||
QVector<AudioMixer::ReverbSettings> AudioMixer::_zoneReverbSettings;
|
||||
|
@ -69,6 +66,7 @@ AudioMixer::AudioMixer(ReceivedMessage& message) :
|
|||
packetReceiver.registerListener(PacketType::NodeMuteRequest, this, "handleNodeMuteRequestPacket");
|
||||
packetReceiver.registerListener(PacketType::RadiusIgnoreRequest, this, "handleRadiusIgnoreRequestPacket");
|
||||
packetReceiver.registerListener(PacketType::RequestsDomainListData, this, "handleRequestsDomainListDataPacket");
|
||||
packetReceiver.registerListener(PacketType::PerAvatarGainSet, this, "handlePerAvatarGainSetDataPacket");
|
||||
|
||||
connect(nodeList.data(), &NodeList::nodeKilled, this, &AudioMixer::handleNodeKilled);
|
||||
}
|
||||
|
@ -186,7 +184,8 @@ void AudioMixer::handleNodeKilled(SharedNodePointer killedNode) {
|
|||
nodeList->eachNode([&killedNode](const SharedNodePointer& node) {
|
||||
auto clientData = dynamic_cast<AudioMixerClientData*>(node->getLinkedData());
|
||||
if (clientData) {
|
||||
clientData->removeHRTFsForNode(killedNode->getUUID());
|
||||
QUuid killedUUID = killedNode->getUUID();
|
||||
clientData->removeHRTFsForNode(killedUUID);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -240,6 +239,20 @@ void AudioMixer::handleNodeIgnoreRequestPacket(QSharedPointer<ReceivedMessage> p
|
|||
sendingNode->parseIgnoreRequestMessage(packet);
|
||||
}
|
||||
|
||||
void AudioMixer::handlePerAvatarGainSetDataPacket(QSharedPointer<ReceivedMessage> packet, SharedNodePointer sendingNode) {
|
||||
auto clientData = dynamic_cast<AudioMixerClientData*>(sendingNode->getLinkedData());
|
||||
if (clientData) {
|
||||
QUuid listeningNodeUUID = sendingNode->getUUID();
|
||||
// parse the UUID from the packet
|
||||
QUuid audioSourceUUID = QUuid::fromRfc4122(packet->readWithoutCopy(NUM_BYTES_RFC4122_UUID));
|
||||
uint8_t packedGain;
|
||||
packet->readPrimitive(&packedGain);
|
||||
float gain = unpackFloatGainFromByte(packedGain);
|
||||
clientData->hrtfForStream(audioSourceUUID, QUuid()).setGainAdjustment(gain);
|
||||
qDebug() << "Setting gain adjustment for hrtf[" << listeningNodeUUID << "][" << audioSourceUUID << "] to " << gain;
|
||||
}
|
||||
}
|
||||
|
||||
void AudioMixer::handleRadiusIgnoreRequestPacket(QSharedPointer<ReceivedMessage> packet, SharedNodePointer sendingNode) {
|
||||
sendingNode->parseIgnoreRadiusRequestMessage(packet);
|
||||
}
|
||||
|
@ -277,35 +290,31 @@ void AudioMixer::sendStatsPacket() {
|
|||
|
||||
// general stats
|
||||
statsObject["useDynamicJitterBuffers"] = _numStaticJitterFrames == -1;
|
||||
statsObject["trailing_sleep_percentage"] = _trailingSleepRatio * 100.0f;
|
||||
statsObject["performance_throttling_ratio"] = _performanceThrottlingRatio;
|
||||
|
||||
statsObject["threads"] = _slavePool.numThreads();
|
||||
|
||||
statsObject["trailing_mix_ratio"] = _trailingMixRatio;
|
||||
statsObject["throttling_ratio"] = _throttlingRatio;
|
||||
|
||||
statsObject["avg_streams_per_frame"] = (float)_stats.sumStreams / (float)_numStatFrames;
|
||||
statsObject["avg_listeners_per_frame"] = (float)_stats.sumListeners / (float)_numStatFrames;
|
||||
|
||||
// timing stats
|
||||
QJsonObject timingStats;
|
||||
uint64_t timing, trailing;
|
||||
|
||||
_sleepTiming.get(timing, trailing);
|
||||
timingStats["us_per_sleep"] = (qint64)(timing / _numStatFrames);
|
||||
timingStats["us_per_sleep_trailing"] = (qint64)(trailing / _numStatFrames);
|
||||
auto addTiming = [&](Timer& timer, std::string name) {
|
||||
uint64_t timing, trailing;
|
||||
timer.get(timing, trailing);
|
||||
timingStats[("us_per_" + name).c_str()] = (qint64)(timing / _numStatFrames);
|
||||
timingStats[("us_per_" + name + "_trailing").c_str()] = (qint64)(trailing / _numStatFrames);
|
||||
};
|
||||
|
||||
_frameTiming.get(timing, trailing);
|
||||
timingStats["us_per_frame"] = (qint64)(timing / _numStatFrames);
|
||||
timingStats["us_per_frame_trailing"] = (qint64)(trailing / _numStatFrames);
|
||||
|
||||
_prepareTiming.get(timing, trailing);
|
||||
timingStats["us_per_prepare"] = (qint64)(timing / _numStatFrames);
|
||||
timingStats["us_per_prepare_trailing"] = (qint64)(trailing / _numStatFrames);
|
||||
|
||||
_mixTiming.get(timing, trailing);
|
||||
timingStats["us_per_mix"] = (qint64)(timing / _numStatFrames);
|
||||
timingStats["us_per_mix_trailing"] = (qint64)(trailing / _numStatFrames);
|
||||
|
||||
_eventsTiming.get(timing, trailing);
|
||||
timingStats["us_per_events"] = (qint64)(timing / _numStatFrames);
|
||||
timingStats["us_per_events_trailing"] = (qint64)(trailing / _numStatFrames);
|
||||
addTiming(_ticTiming, "tic");
|
||||
addTiming(_sleepTiming, "sleep");
|
||||
addTiming(_frameTiming, "frame");
|
||||
addTiming(_prepareTiming, "prepare");
|
||||
addTiming(_mixTiming, "mix");
|
||||
addTiming(_eventsTiming, "events");
|
||||
|
||||
// call it "avg_..." to keep it higher in the display, sorted alphabetically
|
||||
statsObject["avg_timing_stats"] = timingStats;
|
||||
|
@ -315,7 +324,7 @@ void AudioMixer::sendStatsPacket() {
|
|||
|
||||
mixStats["%_hrtf_mixes"] = percentageForMixStats(_stats.hrtfRenders);
|
||||
mixStats["%_hrtf_silent_mixes"] = percentageForMixStats(_stats.hrtfSilentRenders);
|
||||
mixStats["%_hrtf_struggle_mixes"] = percentageForMixStats(_stats.hrtfStruggleRenders);
|
||||
mixStats["%_hrtf_throttle_mixes"] = percentageForMixStats(_stats.hrtfThrottleRenders);
|
||||
mixStats["%_manual_stereo_mixes"] = percentageForMixStats(_stats.manualStereoMixes);
|
||||
mixStats["%_manual_echo_mixes"] = percentageForMixStats(_stats.manualEchoMixes);
|
||||
|
||||
|
@ -381,7 +390,7 @@ void AudioMixer::start() {
|
|||
auto nodeList = DependencyManager::get<NodeList>();
|
||||
|
||||
// prepare the NodeList
|
||||
nodeList->addNodeTypeToInterestSet(NodeType::Agent);
|
||||
nodeList->addSetOfNodeTypesToNodeInterestSet({ NodeType::Agent, NodeType::EntityScriptServer });
|
||||
nodeList->linkedDataCreateCallback = [&](Node* node) { getOrCreateClientData(node); };
|
||||
|
||||
// parse out any AudioMixer settings
|
||||
|
@ -391,25 +400,25 @@ void AudioMixer::start() {
|
|||
parseSettingsObject(settingsObject);
|
||||
}
|
||||
|
||||
// manageLoad state
|
||||
auto frameTimestamp = p_high_resolution_clock::time_point::min();
|
||||
unsigned int framesSinceManagement = std::numeric_limits<int>::max();
|
||||
|
||||
// mix state
|
||||
unsigned int frame = 1;
|
||||
auto frameTimestamp = p_high_resolution_clock::now();
|
||||
|
||||
while (!_isFinished) {
|
||||
auto ticTimer = _ticTiming.timer();
|
||||
|
||||
{
|
||||
auto timer = _sleepTiming.timer();
|
||||
manageLoad(frameTimestamp, framesSinceManagement);
|
||||
auto frameDuration = timeFrame(frameTimestamp);
|
||||
throttle(frameDuration, frame);
|
||||
}
|
||||
|
||||
auto timer = _frameTiming.timer();
|
||||
auto frameTimer = _frameTiming.timer();
|
||||
|
||||
nodeList->nestedEach([&](NodeList::const_iterator cbegin, NodeList::const_iterator cend) {
|
||||
// prepare frames; pop off any new audio from their streams
|
||||
{
|
||||
auto timer = _prepareTiming.timer();
|
||||
auto prepareTimer = _prepareTiming.timer();
|
||||
std::for_each(cbegin, cend, [&](const SharedNodePointer& node) {
|
||||
_stats.sumStreams += prepareFrame(node, frame);
|
||||
});
|
||||
|
@ -417,8 +426,8 @@ void AudioMixer::start() {
|
|||
|
||||
// mix across slave threads
|
||||
{
|
||||
auto timer = _mixTiming.timer();
|
||||
_slavePool.mix(cbegin, cend, frame);
|
||||
auto mixTimer = _mixTiming.timer();
|
||||
_slavePool.mix(cbegin, cend, frame, _throttlingRatio);
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -433,7 +442,7 @@ void AudioMixer::start() {
|
|||
|
||||
// play nice with qt event-looping
|
||||
{
|
||||
auto timer = _eventsTiming.timer();
|
||||
auto eventsTimer = _eventsTiming.timer();
|
||||
|
||||
// since we're a while loop we need to yield to qt's event processing
|
||||
QCoreApplication::processEvents();
|
||||
|
@ -447,67 +456,68 @@ void AudioMixer::start() {
|
|||
}
|
||||
}
|
||||
|
||||
void AudioMixer::manageLoad(p_high_resolution_clock::time_point& frameTimestamp, unsigned int& framesSinceCutoffEvent) {
|
||||
auto timeToSleep = std::chrono::microseconds(0);
|
||||
std::chrono::microseconds AudioMixer::timeFrame(p_high_resolution_clock::time_point& timestamp) {
|
||||
// advance the next frame
|
||||
auto nextTimestamp = timestamp + std::chrono::microseconds(AudioConstants::NETWORK_FRAME_USECS);
|
||||
auto now = p_high_resolution_clock::now();
|
||||
|
||||
// sleep until the next frame, if necessary
|
||||
{
|
||||
// advance the next frame
|
||||
frameTimestamp += std::chrono::microseconds(AudioConstants::NETWORK_FRAME_USECS);
|
||||
auto now = p_high_resolution_clock::now();
|
||||
// compute how long the last frame took
|
||||
auto duration = std::chrono::duration_cast<std::chrono::microseconds>(now - timestamp);
|
||||
|
||||
// calculate sleep
|
||||
if (frameTimestamp < now) {
|
||||
frameTimestamp = now;
|
||||
} else {
|
||||
timeToSleep = std::chrono::duration_cast<std::chrono::microseconds>(frameTimestamp - now);
|
||||
std::this_thread::sleep_for(timeToSleep);
|
||||
}
|
||||
}
|
||||
// set the new frame timestamp
|
||||
timestamp = std::max(now, nextTimestamp);
|
||||
|
||||
// manage mixer load
|
||||
{
|
||||
const int TRAILING_AVERAGE_FRAMES = 100;
|
||||
const float CURRENT_FRAME_RATIO = 1.0f / TRAILING_AVERAGE_FRAMES;
|
||||
const float PREVIOUS_FRAMES_RATIO = 1.0f - CURRENT_FRAME_RATIO;
|
||||
// sleep until the next frame should start
|
||||
// WIN32 sleep_until is broken until VS2015 Update 2
|
||||
// instead, std::max (above) guarantees that timestamp >= now, so we can sleep_for
|
||||
std::this_thread::sleep_for(timestamp - now);
|
||||
|
||||
const float STRUGGLE_TRIGGER_SLEEP_PERCENTAGE_THRESHOLD = 0.10f;
|
||||
const float BACK_OFF_TRIGGER_SLEEP_PERCENTAGE_THRESHOLD = 0.20f;
|
||||
return duration;
|
||||
}
|
||||
|
||||
const float RATIO_BACK_OFF = 0.02f;
|
||||
void AudioMixer::throttle(std::chrono::microseconds duration, int frame) {
|
||||
// throttle using a modified proportional-integral controller
|
||||
const float FRAME_TIME = 10000.0f;
|
||||
float mixRatio = duration.count() / FRAME_TIME;
|
||||
|
||||
_trailingSleepRatio = (PREVIOUS_FRAMES_RATIO * _trailingSleepRatio) +
|
||||
// ratio of frame spent sleeping / total frame time
|
||||
((CURRENT_FRAME_RATIO * timeToSleep.count()) / (float) AudioConstants::NETWORK_FRAME_USECS);
|
||||
// constants are determined based on a "regular" 16-CPU EC2 server
|
||||
|
||||
bool hasRatioChanged = false;
|
||||
// target different mix and backoff ratios (they also have different backoff rates)
|
||||
// this is to prevent oscillation, and encourage throttling to find a steady state
|
||||
const float TARGET = 0.9f;
|
||||
// on a "regular" machine with 100 avatars, this is the largest value where
|
||||
// - overthrottling can be recovered
|
||||
// - oscillations will not occur after the recovery
|
||||
const float BACKOFF_TARGET = 0.44f;
|
||||
|
||||
if (framesSinceCutoffEvent >= TRAILING_AVERAGE_FRAMES) {
|
||||
if (_trailingSleepRatio <= STRUGGLE_TRIGGER_SLEEP_PERCENTAGE_THRESHOLD) {
|
||||
qDebug() << "Mixer is struggling";
|
||||
// change our min required loudness to reduce some load
|
||||
_performanceThrottlingRatio = _performanceThrottlingRatio + (0.5f * (1.0f - _performanceThrottlingRatio));
|
||||
hasRatioChanged = true;
|
||||
} else if (_trailingSleepRatio >= BACK_OFF_TRIGGER_SLEEP_PERCENTAGE_THRESHOLD && _performanceThrottlingRatio != 0) {
|
||||
qDebug() << "Mixer is recovering";
|
||||
// back off the required loudness
|
||||
_performanceThrottlingRatio = std::max(0.0f, _performanceThrottlingRatio - RATIO_BACK_OFF);
|
||||
hasRatioChanged = true;
|
||||
}
|
||||
// the mixer is known to struggle at about 80 on a "regular" machine
|
||||
// so throttle 2/80 the streams to ensure smooth audio (throttling is linear)
|
||||
const float THROTTLE_RATE = 2 / 80.0f;
|
||||
const float BACKOFF_RATE = THROTTLE_RATE / 4;
|
||||
|
||||
if (hasRatioChanged) {
|
||||
// set out min audability threshold from the new ratio
|
||||
_minAudibilityThreshold = LOUDNESS_TO_DISTANCE_RATIO / (2.0f * (1.0f - _performanceThrottlingRatio));
|
||||
framesSinceCutoffEvent = 0;
|
||||
// recovery should be bounded so that large changes in user count is a tolerable experience
|
||||
// throttling is linear, so most cases will not need a full recovery
|
||||
const int RECOVERY_TIME = 180;
|
||||
|
||||
qDebug() << "Sleeping" << _trailingSleepRatio << "of frame";
|
||||
qDebug() << "Cutoff is" << _performanceThrottlingRatio;
|
||||
qDebug() << "Minimum audibility to be mixed is" << _minAudibilityThreshold;
|
||||
}
|
||||
}
|
||||
// weight more recent frames to determine if throttling is necessary,
|
||||
const int TRAILING_FRAMES = (int)(100 * RECOVERY_TIME * BACKOFF_RATE);
|
||||
const float CURRENT_FRAME_RATIO = 1.0f / TRAILING_FRAMES;
|
||||
const float PREVIOUS_FRAMES_RATIO = 1.0f - CURRENT_FRAME_RATIO;
|
||||
_trailingMixRatio = PREVIOUS_FRAMES_RATIO * _trailingMixRatio + CURRENT_FRAME_RATIO * mixRatio;
|
||||
|
||||
if (!hasRatioChanged) {
|
||||
++framesSinceCutoffEvent;
|
||||
if (frame % TRAILING_FRAMES == 0) {
|
||||
if (_trailingMixRatio > TARGET) {
|
||||
int proportionalTerm = 1 + (_trailingMixRatio - TARGET) / 0.1f;
|
||||
_throttlingRatio += THROTTLE_RATE * proportionalTerm;
|
||||
_throttlingRatio = std::min(_throttlingRatio, 1.0f);
|
||||
qDebug("audio-mixer is struggling (%f mix/sleep) - throttling %f of streams",
|
||||
(double)_trailingMixRatio, (double)_throttlingRatio);
|
||||
} else if (_throttlingRatio > 0.0f && _trailingMixRatio <= BACKOFF_TARGET) {
|
||||
int proportionalTerm = 1 + (TARGET - _trailingMixRatio) / 0.2f;
|
||||
_throttlingRatio -= BACKOFF_RATE * proportionalTerm;
|
||||
_throttlingRatio = std::max(_throttlingRatio, 0.0f);
|
||||
qDebug("audio-mixer is recovering (%f mix/sleep) - throttling %f of streams",
|
||||
(double)_trailingMixRatio, (double)_throttlingRatio);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -46,7 +46,6 @@ public:
|
|||
static int getStaticJitterFrames() { return _numStaticJitterFrames; }
|
||||
static bool shouldMute(float quietestFrame) { return quietestFrame > _noiseMutingThreshold; }
|
||||
static float getAttenuationPerDoublingInDistance() { return _attenuationPerDoublingInDistance; }
|
||||
static float getMinimumAudibilityThreshold() { return _performanceThrottlingRatio > 0.0f ? _minAudibilityThreshold : 0.0f; }
|
||||
static const QHash<QString, AABox>& getAudioZones() { return _audioZones; }
|
||||
static const QVector<ZoneSettings>& getZoneSettings() { return _zoneSettings; }
|
||||
static const QVector<ReverbSettings>& getReverbSettings() { return _zoneReverbSettings; }
|
||||
|
@ -66,14 +65,15 @@ private slots:
|
|||
void handleRadiusIgnoreRequestPacket(QSharedPointer<ReceivedMessage> packet, SharedNodePointer sendingNode);
|
||||
void handleKillAvatarPacket(QSharedPointer<ReceivedMessage> packet, SharedNodePointer sendingNode);
|
||||
void handleNodeMuteRequestPacket(QSharedPointer<ReceivedMessage> packet, SharedNodePointer sendingNode);
|
||||
void handlePerAvatarGainSetDataPacket(QSharedPointer<ReceivedMessage> packet, SharedNodePointer sendingNode);
|
||||
|
||||
void start();
|
||||
void removeHRTFsForFinishedInjector(const QUuid& streamID);
|
||||
|
||||
private:
|
||||
// mixing helpers
|
||||
// check and maybe throttle mixer load by changing audibility threshold
|
||||
void manageLoad(p_high_resolution_clock::time_point& frameTimestamp, unsigned int& framesSinceManagement);
|
||||
std::chrono::microseconds timeFrame(p_high_resolution_clock::time_point& timestamp);
|
||||
void throttle(std::chrono::microseconds frameDuration, int frame);
|
||||
// pop a frame from any streams on the node
|
||||
// returns the number of available streams
|
||||
int prepareFrame(const SharedNodePointer& node, unsigned int frame);
|
||||
|
@ -84,6 +84,9 @@ private:
|
|||
|
||||
void parseSettingsObject(const QJsonObject& settingsObject);
|
||||
|
||||
float _trailingMixRatio { 0.0f };
|
||||
float _throttlingRatio { 0.0f };
|
||||
|
||||
int _numStatFrames { 0 };
|
||||
AudioMixerStats _stats;
|
||||
|
||||
|
@ -112,6 +115,7 @@ private:
|
|||
uint64_t _history[TIMER_TRAILING_SECONDS] {};
|
||||
int _index { 0 };
|
||||
};
|
||||
Timer _ticTiming;
|
||||
Timer _sleepTiming;
|
||||
Timer _frameTiming;
|
||||
Timer _prepareTiming;
|
||||
|
@ -121,9 +125,6 @@ private:
|
|||
static int _numStaticJitterFrames; // -1 denotes dynamic jitter buffering
|
||||
static float _noiseMutingThreshold;
|
||||
static float _attenuationPerDoublingInDistance;
|
||||
static float _trailingSleepRatio;
|
||||
static float _performanceThrottlingRatio;
|
||||
static float _minAudibilityThreshold;
|
||||
static QHash<QString, AABox> _audioZones;
|
||||
static QVector<ZoneSettings> _zoneSettings;
|
||||
static QVector<ReverbSettings> _zoneReverbSettings;
|
||||
|
|
|
@ -36,6 +36,292 @@
|
|||
|
||||
#include "AudioMixerSlave.h"
|
||||
|
||||
using AudioStreamMap = AudioMixerClientData::AudioStreamMap;
|
||||
|
||||
// packet helpers
|
||||
std::unique_ptr<NLPacket> createAudioPacket(PacketType type, int size, quint16 sequence, QString codec);
|
||||
void sendMixPacket(const SharedNodePointer& node, AudioMixerClientData& data, QByteArray& buffer);
|
||||
void sendSilentPacket(const SharedNodePointer& node, AudioMixerClientData& data);
|
||||
void sendMutePacket(const SharedNodePointer& node, AudioMixerClientData&);
|
||||
void sendEnvironmentPacket(const SharedNodePointer& node, AudioMixerClientData& data);
|
||||
|
||||
// mix helpers
|
||||
bool shouldIgnoreNode(const SharedNodePointer& listener, const SharedNodePointer& node);
|
||||
float gainForSource(const AvatarAudioStream& listeningNodeStream, const PositionalAudioStream& streamToAdd,
|
||||
const glm::vec3& relativePosition, bool isEcho);
|
||||
float azimuthForSource(const AvatarAudioStream& listeningNodeStream, const PositionalAudioStream& streamToAdd,
|
||||
const glm::vec3& relativePosition);
|
||||
|
||||
void AudioMixerSlave::configure(ConstIter begin, ConstIter end, unsigned int frame, float throttlingRatio) {
|
||||
_begin = begin;
|
||||
_end = end;
|
||||
_frame = frame;
|
||||
_throttlingRatio = throttlingRatio;
|
||||
}
|
||||
|
||||
void AudioMixerSlave::mix(const SharedNodePointer& node) {
|
||||
// check that the node is valid
|
||||
AudioMixerClientData* data = (AudioMixerClientData*)node->getLinkedData();
|
||||
if (data == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
// check that the stream is valid
|
||||
auto avatarStream = data->getAvatarAudioStream();
|
||||
if (avatarStream == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
// send mute packet, if necessary
|
||||
if (AudioMixer::shouldMute(avatarStream->getQuietestFrameLoudness()) || data->shouldMuteClient()) {
|
||||
sendMutePacket(node, *data);
|
||||
}
|
||||
|
||||
// send audio packets, if necessary
|
||||
if (node->getType() == NodeType::Agent && node->getActiveSocket()) {
|
||||
++stats.sumListeners;
|
||||
|
||||
// mix the audio
|
||||
bool mixHasAudio = prepareMix(node);
|
||||
|
||||
// send audio packet
|
||||
if (mixHasAudio || data->shouldFlushEncoder()) {
|
||||
QByteArray encodedBuffer;
|
||||
if (mixHasAudio) {
|
||||
// encode the audio
|
||||
QByteArray decodedBuffer(reinterpret_cast<char*>(_bufferSamples), AudioConstants::NETWORK_FRAME_BYTES_STEREO);
|
||||
data->encode(decodedBuffer, encodedBuffer);
|
||||
} else {
|
||||
// time to flush (resets shouldFlush until the next encode)
|
||||
data->encodeFrameOfZeros(encodedBuffer);
|
||||
}
|
||||
|
||||
sendMixPacket(node, *data, encodedBuffer);
|
||||
} else {
|
||||
sendSilentPacket(node, *data);
|
||||
}
|
||||
|
||||
// send environment packet
|
||||
sendEnvironmentPacket(node, *data);
|
||||
|
||||
// send stats packet (about every second)
|
||||
const unsigned int NUM_FRAMES_PER_SEC = (int)ceil(AudioConstants::NETWORK_FRAMES_PER_SEC);
|
||||
if (data->shouldSendStats(_frame % NUM_FRAMES_PER_SEC)) {
|
||||
data->sendAudioStreamStatsPackets(node);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool AudioMixerSlave::prepareMix(const SharedNodePointer& listener) {
|
||||
AvatarAudioStream* listenerAudioStream = static_cast<AudioMixerClientData*>(listener->getLinkedData())->getAvatarAudioStream();
|
||||
AudioMixerClientData* listenerData = static_cast<AudioMixerClientData*>(listener->getLinkedData());
|
||||
|
||||
// zero out the mix for this listener
|
||||
memset(_mixSamples, 0, sizeof(_mixSamples));
|
||||
|
||||
bool isThrottling = _throttlingRatio > 0.0f;
|
||||
std::vector<std::pair<float, SharedNodePointer>> throttledNodes;
|
||||
|
||||
typedef void (AudioMixerSlave::*MixFunctor)(
|
||||
AudioMixerClientData&, const QUuid&, const AvatarAudioStream&, const PositionalAudioStream&);
|
||||
auto allStreams = [&](const SharedNodePointer& node, MixFunctor mixFunctor) {
|
||||
AudioMixerClientData* nodeData = static_cast<AudioMixerClientData*>(node->getLinkedData());
|
||||
for (auto& streamPair : nodeData->getAudioStreams()) {
|
||||
auto nodeStream = streamPair.second;
|
||||
(this->*mixFunctor)(*listenerData, node->getUUID(), *listenerAudioStream, *nodeStream);
|
||||
}
|
||||
};
|
||||
|
||||
std::for_each(_begin, _end, [&](const SharedNodePointer& node) {
|
||||
if (*node == *listener) {
|
||||
AudioMixerClientData* nodeData = static_cast<AudioMixerClientData*>(node->getLinkedData());
|
||||
|
||||
// only mix the echo, if requested
|
||||
for (auto& streamPair : nodeData->getAudioStreams()) {
|
||||
auto nodeStream = streamPair.second;
|
||||
if (nodeStream->shouldLoopbackForNode()) {
|
||||
mixStream(*listenerData, node->getUUID(), *listenerAudioStream, *nodeStream);
|
||||
}
|
||||
}
|
||||
} else if (!shouldIgnoreNode(listener, node)) {
|
||||
if (!isThrottling) {
|
||||
allStreams(node, &AudioMixerSlave::mixStream);
|
||||
} else {
|
||||
AudioMixerClientData* nodeData = static_cast<AudioMixerClientData*>(node->getLinkedData());
|
||||
|
||||
// compute the node's max relative volume
|
||||
float nodeVolume;
|
||||
for (auto& streamPair : nodeData->getAudioStreams()) {
|
||||
auto nodeStream = streamPair.second;
|
||||
float distance = glm::length(nodeStream->getPosition() - listenerAudioStream->getPosition());
|
||||
nodeVolume = std::max(nodeStream->getLastPopOutputTrailingLoudness() / distance, nodeVolume);
|
||||
}
|
||||
|
||||
// max-heapify the nodes by relative volume
|
||||
throttledNodes.push_back(std::make_pair(nodeVolume, node));
|
||||
if (!throttledNodes.empty()) {
|
||||
std::push_heap(throttledNodes.begin(), throttledNodes.end());
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (isThrottling) {
|
||||
// pop the loudest nodes off the heap and mix their streams
|
||||
int numToRetain = (int)(std::distance(_begin, _end) * (1 - _throttlingRatio));
|
||||
for (int i = 0; i < numToRetain; i++) {
|
||||
if (throttledNodes.empty()) {
|
||||
break;
|
||||
}
|
||||
|
||||
std::pop_heap(throttledNodes.begin(), throttledNodes.end());
|
||||
|
||||
auto& node = throttledNodes.back().second;
|
||||
allStreams(node, &AudioMixerSlave::mixStream);
|
||||
|
||||
throttledNodes.pop_back();
|
||||
}
|
||||
|
||||
// throttle the remaining nodes' streams
|
||||
for (const std::pair<float, SharedNodePointer>& nodePair : throttledNodes) {
|
||||
auto& node = nodePair.second;
|
||||
allStreams(node, &AudioMixerSlave::throttleStream);
|
||||
}
|
||||
}
|
||||
|
||||
// use the per listener AudioLimiter to render the mixed data...
|
||||
listenerData->audioLimiter.render(_mixSamples, _bufferSamples, AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL);
|
||||
|
||||
// check for silent audio after the peak limiter has converted the samples
|
||||
bool hasAudio = false;
|
||||
for (int i = 0; i < AudioConstants::NETWORK_FRAME_SAMPLES_STEREO; ++i) {
|
||||
if (_bufferSamples[i] != 0) {
|
||||
hasAudio = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return hasAudio;
|
||||
}
|
||||
|
||||
void AudioMixerSlave::throttleStream(AudioMixerClientData& listenerNodeData, const QUuid& sourceNodeID,
|
||||
const AvatarAudioStream& listeningNodeStream, const PositionalAudioStream& streamToAdd) {
|
||||
addStream(listenerNodeData, sourceNodeID, listeningNodeStream, streamToAdd, true);
|
||||
}
|
||||
|
||||
void AudioMixerSlave::mixStream(AudioMixerClientData& listenerNodeData, const QUuid& sourceNodeID,
|
||||
const AvatarAudioStream& listeningNodeStream, const PositionalAudioStream& streamToAdd) {
|
||||
addStream(listenerNodeData, sourceNodeID, listeningNodeStream, streamToAdd, false);
|
||||
}
|
||||
|
||||
void AudioMixerSlave::addStream(AudioMixerClientData& listenerNodeData, const QUuid& sourceNodeID,
|
||||
const AvatarAudioStream& listeningNodeStream, const PositionalAudioStream& streamToAdd,
|
||||
bool throttle) {
|
||||
++stats.totalMixes;
|
||||
|
||||
// to reduce artifacts we call the HRTF functor for every source, even if throttled or silent
|
||||
// this ensures the correct tail from last mixed block and the correct spatialization of next first block
|
||||
|
||||
// check if this is a server echo of a source back to itself
|
||||
bool isEcho = (&streamToAdd == &listeningNodeStream);
|
||||
|
||||
glm::vec3 relativePosition = streamToAdd.getPosition() - listeningNodeStream.getPosition();
|
||||
|
||||
float distance = glm::max(glm::length(relativePosition), EPSILON);
|
||||
float gain = gainForSource(listeningNodeStream, streamToAdd, relativePosition, isEcho);
|
||||
float azimuth = isEcho ? 0.0f : azimuthForSource(listeningNodeStream, listeningNodeStream, relativePosition);
|
||||
static const int HRTF_DATASET_INDEX = 1;
|
||||
|
||||
if (!streamToAdd.lastPopSucceeded()) {
|
||||
bool forceSilentBlock = true;
|
||||
|
||||
if (!streamToAdd.getLastPopOutput().isNull()) {
|
||||
bool isInjector = dynamic_cast<const InjectedAudioStream*>(&streamToAdd);
|
||||
|
||||
// in an injector, just go silent - the injector has likely ended
|
||||
// in other inputs (microphone, &c.), repeat with fade to avoid the harsh jump to silence
|
||||
if (!isInjector) {
|
||||
// calculate its fade factor, which depends on how many times it's already been repeated.
|
||||
float fadeFactor = calculateRepeatedFrameFadeFactor(streamToAdd.getConsecutiveNotMixedCount() - 1);
|
||||
if (fadeFactor > 0.0f) {
|
||||
// apply the fadeFactor to the gain
|
||||
gain *= fadeFactor;
|
||||
forceSilentBlock = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (forceSilentBlock) {
|
||||
// call renderSilent with a forced silent block to reduce artifacts
|
||||
// (this is not done for stereo streams since they do not go through the HRTF)
|
||||
if (!streamToAdd.isStereo() && !isEcho) {
|
||||
// get the existing listener-source HRTF object, or create a new one
|
||||
auto& hrtf = listenerNodeData.hrtfForStream(sourceNodeID, streamToAdd.getStreamIdentifier());
|
||||
|
||||
static int16_t silentMonoBlock[AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL] = {};
|
||||
hrtf.renderSilent(silentMonoBlock, _mixSamples, HRTF_DATASET_INDEX, azimuth, distance, gain,
|
||||
AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL);
|
||||
|
||||
++stats.hrtfSilentRenders;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// grab the stream from the ring buffer
|
||||
AudioRingBuffer::ConstIterator streamPopOutput = streamToAdd.getLastPopOutput();
|
||||
|
||||
// stereo sources are not passed through HRTF
|
||||
if (streamToAdd.isStereo()) {
|
||||
for (int i = 0; i < AudioConstants::NETWORK_FRAME_SAMPLES_STEREO; ++i) {
|
||||
_mixSamples[i] += float(streamPopOutput[i] * gain / AudioConstants::MAX_SAMPLE_VALUE);
|
||||
}
|
||||
|
||||
++stats.manualStereoMixes;
|
||||
return;
|
||||
}
|
||||
|
||||
// echo sources are not passed through HRTF
|
||||
if (isEcho) {
|
||||
for (int i = 0; i < AudioConstants::NETWORK_FRAME_SAMPLES_STEREO; i += 2) {
|
||||
auto monoSample = float(streamPopOutput[i / 2] * gain / AudioConstants::MAX_SAMPLE_VALUE);
|
||||
_mixSamples[i] += monoSample;
|
||||
_mixSamples[i + 1] += monoSample;
|
||||
}
|
||||
|
||||
++stats.manualEchoMixes;
|
||||
return;
|
||||
}
|
||||
|
||||
// get the existing listener-source HRTF object, or create a new one
|
||||
auto& hrtf = listenerNodeData.hrtfForStream(sourceNodeID, streamToAdd.getStreamIdentifier());
|
||||
|
||||
streamPopOutput.readSamples(_bufferSamples, AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL);
|
||||
|
||||
if (streamToAdd.getLastPopOutputLoudness() == 0.0f) {
|
||||
// call renderSilent to reduce artifacts
|
||||
hrtf.renderSilent(_bufferSamples, _mixSamples, HRTF_DATASET_INDEX, azimuth, distance, gain,
|
||||
AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL);
|
||||
|
||||
++stats.hrtfSilentRenders;
|
||||
return;
|
||||
}
|
||||
|
||||
if (throttle) {
|
||||
// call renderSilent with actual frame data and a gain of 0.0f to reduce artifacts
|
||||
hrtf.renderSilent(_bufferSamples, _mixSamples, HRTF_DATASET_INDEX, azimuth, distance, 0.0f,
|
||||
AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL);
|
||||
|
||||
++stats.hrtfThrottleRenders;
|
||||
return;
|
||||
}
|
||||
|
||||
hrtf.render(_bufferSamples, _mixSamples, HRTF_DATASET_INDEX, azimuth, distance, gain,
|
||||
AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL);
|
||||
|
||||
++stats.hrtfRenders;
|
||||
}
|
||||
|
||||
std::unique_ptr<NLPacket> createAudioPacket(PacketType type, int size, quint16 sequence, QString codec) {
|
||||
auto audioPacket = NLPacket::create(type, size);
|
||||
audioPacket->writePrimitive(sequence);
|
||||
|
@ -73,6 +359,14 @@ void sendSilentPacket(const SharedNodePointer& node, AudioMixerClientData& data)
|
|||
data.incrementOutgoingMixedAudioSequenceNumber();
|
||||
}
|
||||
|
||||
void sendMutePacket(const SharedNodePointer& node, AudioMixerClientData& data) {
|
||||
auto mutePacket = NLPacket::create(PacketType::NoisyMute, 0);
|
||||
DependencyManager::get<NodeList>()->sendPacket(std::move(mutePacket), *node);
|
||||
|
||||
// probably now we just reset the flag, once should do it (?)
|
||||
data.setShouldMuteClient(false);
|
||||
}
|
||||
|
||||
void sendEnvironmentPacket(const SharedNodePointer& node, AudioMixerClientData& data) {
|
||||
bool hasReverb = false;
|
||||
float reverbTime, wetLevel;
|
||||
|
@ -134,285 +428,54 @@ void sendEnvironmentPacket(const SharedNodePointer& node, AudioMixerClientData&
|
|||
}
|
||||
}
|
||||
|
||||
void AudioMixerSlave::configure(ConstIter begin, ConstIter end, unsigned int frame) {
|
||||
_begin = begin;
|
||||
_end = end;
|
||||
_frame = frame;
|
||||
}
|
||||
|
||||
void AudioMixerSlave::mix(const SharedNodePointer& node) {
|
||||
// check that the node is valid
|
||||
AudioMixerClientData* data = (AudioMixerClientData*)node->getLinkedData();
|
||||
if (data == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto avatarStream = data->getAvatarAudioStream();
|
||||
if (avatarStream == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
// send mute packet, if necessary
|
||||
if (AudioMixer::shouldMute(avatarStream->getQuietestFrameLoudness()) || data->shouldMuteClient()) {
|
||||
auto mutePacket = NLPacket::create(PacketType::NoisyMute, 0);
|
||||
DependencyManager::get<NodeList>()->sendPacket(std::move(mutePacket), *node);
|
||||
|
||||
// probably now we just reset the flag, once should do it (?)
|
||||
data->setShouldMuteClient(false);
|
||||
}
|
||||
|
||||
// send audio packets, if necessary
|
||||
if (node->getType() == NodeType::Agent && node->getActiveSocket()) {
|
||||
++stats.sumListeners;
|
||||
|
||||
// mix the audio
|
||||
bool mixHasAudio = prepareMix(node);
|
||||
|
||||
// send audio packet
|
||||
if (mixHasAudio || data->shouldFlushEncoder()) {
|
||||
// encode the audio
|
||||
QByteArray encodedBuffer;
|
||||
if (mixHasAudio) {
|
||||
QByteArray decodedBuffer(reinterpret_cast<char*>(_bufferSamples), AudioConstants::NETWORK_FRAME_BYTES_STEREO);
|
||||
data->encode(decodedBuffer, encodedBuffer);
|
||||
} else {
|
||||
// time to flush, which resets the shouldFlush until next time we encode something
|
||||
data->encodeFrameOfZeros(encodedBuffer);
|
||||
}
|
||||
|
||||
sendMixPacket(node, *data, encodedBuffer);
|
||||
} else {
|
||||
sendSilentPacket(node, *data);
|
||||
}
|
||||
|
||||
// send environment packet
|
||||
sendEnvironmentPacket(node, *data);
|
||||
|
||||
// send stats packet (about every second)
|
||||
static const unsigned int NUM_FRAMES_PER_SEC = (int) ceil(AudioConstants::NETWORK_FRAMES_PER_SEC);
|
||||
if (data->shouldSendStats(_frame % NUM_FRAMES_PER_SEC)) {
|
||||
data->sendAudioStreamStatsPackets(node);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool AudioMixerSlave::prepareMix(const SharedNodePointer& node) {
|
||||
AvatarAudioStream* nodeAudioStream = static_cast<AudioMixerClientData*>(node->getLinkedData())->getAvatarAudioStream();
|
||||
bool shouldIgnoreNode(const SharedNodePointer& listener, const SharedNodePointer& node) {
|
||||
AudioMixerClientData* listenerData = static_cast<AudioMixerClientData*>(listener->getLinkedData());
|
||||
AudioMixerClientData* nodeData = static_cast<AudioMixerClientData*>(node->getLinkedData());
|
||||
|
||||
// zero out the client mix for this node
|
||||
memset(_mixSamples, 0, sizeof(_mixSamples));
|
||||
// when this is true, the AudioMixer will send Audio data to a client about avatars that have ignored them
|
||||
bool getsAnyIgnored = listenerData->getRequestsDomainListData() && listener->getCanKick();
|
||||
|
||||
// loop through all other nodes that have sufficient audio to mix
|
||||
std::for_each(_begin, _end, [&](const SharedNodePointer& otherNode){
|
||||
// make sure that we have audio data for this other node
|
||||
// and that it isn't being ignored by our listening node
|
||||
// and that it isn't ignoring our listening node
|
||||
AudioMixerClientData* otherData = static_cast<AudioMixerClientData*>(otherNode->getLinkedData());
|
||||
bool ignore = true;
|
||||
|
||||
// When this is true, the AudioMixer will send Audio data to a client about avatars that have ignored them
|
||||
bool getsAnyIgnored = nodeData->getRequestsDomainListData() && node->getCanKick();
|
||||
if (nodeData &&
|
||||
// make sure that it isn't being ignored by our listening node
|
||||
(!listener->isIgnoringNodeWithID(node->getUUID()) || (nodeData->getRequestsDomainListData() && node->getCanKick())) &&
|
||||
// and that it isn't ignoring our listening node
|
||||
(!node->isIgnoringNodeWithID(listener->getUUID()) || getsAnyIgnored)) {
|
||||
|
||||
if (otherData
|
||||
&& (!node->isIgnoringNodeWithID(otherNode->getUUID()) || (otherData->getRequestsDomainListData() && otherNode->getCanKick()))
|
||||
&& (!otherNode->isIgnoringNodeWithID(node->getUUID()) || getsAnyIgnored)) {
|
||||
// is either node enabling the space bubble / ignore radius?
|
||||
if ((listener->isIgnoreRadiusEnabled() || node->isIgnoreRadiusEnabled())) {
|
||||
// define the minimum bubble size
|
||||
static const glm::vec3 minBubbleSize = glm::vec3(0.3f, 1.3f, 0.3f);
|
||||
|
||||
// check to see if we're ignoring in radius
|
||||
bool insideIgnoreRadius = false;
|
||||
// If the otherNode equals the node, we're doing a comparison on ourselves
|
||||
if (*otherNode == *node) {
|
||||
// We'll always be inside the radius in that case.
|
||||
insideIgnoreRadius = true;
|
||||
// Check to see if the space bubble is enabled
|
||||
} else if ((node->isIgnoreRadiusEnabled() || otherNode->isIgnoreRadiusEnabled())) {
|
||||
// Define the minimum bubble size
|
||||
static const glm::vec3 minBubbleSize = glm::vec3(0.3f, 1.3f, 0.3f);
|
||||
AudioMixerClientData* nodeData = reinterpret_cast<AudioMixerClientData*>(node->getLinkedData());
|
||||
// Set up the bounding box for the current node
|
||||
AABox nodeBox(nodeData->getAvatarBoundingBoxCorner(), nodeData->getAvatarBoundingBoxScale());
|
||||
// Clamp the size of the bounding box to a minimum scale
|
||||
if (glm::any(glm::lessThan(nodeData->getAvatarBoundingBoxScale(), minBubbleSize))) {
|
||||
nodeBox.setScaleStayCentered(minBubbleSize);
|
||||
}
|
||||
// Set up the bounding box for the current other node
|
||||
AABox otherNodeBox(otherData->getAvatarBoundingBoxCorner(), otherData->getAvatarBoundingBoxScale());
|
||||
// Clamp the size of the bounding box to a minimum scale
|
||||
if (glm::any(glm::lessThan(otherData->getAvatarBoundingBoxScale(), minBubbleSize))) {
|
||||
otherNodeBox.setScaleStayCentered(minBubbleSize);
|
||||
}
|
||||
// Quadruple the scale of both bounding boxes
|
||||
nodeBox.embiggen(4.0f);
|
||||
otherNodeBox.embiggen(4.0f);
|
||||
|
||||
// Perform the collision check between the two bounding boxes
|
||||
if (nodeBox.touches(otherNodeBox)) {
|
||||
insideIgnoreRadius = true;
|
||||
}
|
||||
// set up the bounding box for the listener
|
||||
AABox listenerBox(listenerData->getAvatarBoundingBoxCorner(), listenerData->getAvatarBoundingBoxScale());
|
||||
if (glm::any(glm::lessThan(listenerData->getAvatarBoundingBoxScale(), minBubbleSize))) {
|
||||
listenerBox.setScaleStayCentered(minBubbleSize);
|
||||
}
|
||||
|
||||
// Enumerate the audio streams attached to the otherNode
|
||||
auto streamsCopy = otherData->getAudioStreams();
|
||||
for (auto& streamPair : streamsCopy) {
|
||||
auto otherNodeStream = streamPair.second;
|
||||
bool isSelfWithEcho = ((*otherNode == *node) && (otherNodeStream->shouldLoopbackForNode()));
|
||||
// Add all audio streams that should be added to the mix
|
||||
if (isSelfWithEcho || (!isSelfWithEcho && !insideIgnoreRadius)) {
|
||||
addStreamToMix(*nodeData, otherNode->getUUID(), *nodeAudioStream, *otherNodeStream);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// use the per listener AudioLimiter to render the mixed data...
|
||||
nodeData->audioLimiter.render(_mixSamples, _bufferSamples, AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL);
|
||||
|
||||
// check for silent audio after the peak limiter has converted the samples
|
||||
bool hasAudio = false;
|
||||
for (int i = 0; i < AudioConstants::NETWORK_FRAME_SAMPLES_STEREO; ++i) {
|
||||
if (_bufferSamples[i] != 0) {
|
||||
hasAudio = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return hasAudio;
|
||||
}
|
||||
|
||||
void AudioMixerSlave::addStreamToMix(AudioMixerClientData& listenerNodeData, const QUuid& sourceNodeID,
|
||||
const AvatarAudioStream& listeningNodeStream, const PositionalAudioStream& streamToAdd) {
|
||||
// to reduce artifacts we calculate the gain and azimuth for every source for this listener
|
||||
// even if we are not going to end up mixing in this source
|
||||
|
||||
++stats.totalMixes;
|
||||
|
||||
// this ensures that the tail of any previously mixed audio or the first block of new audio sounds correct
|
||||
|
||||
// check if this is a server echo of a source back to itself
|
||||
bool isEcho = (&streamToAdd == &listeningNodeStream);
|
||||
|
||||
glm::vec3 relativePosition = streamToAdd.getPosition() - listeningNodeStream.getPosition();
|
||||
|
||||
// figure out the distance between source and listener
|
||||
float distance = glm::max(glm::length(relativePosition), EPSILON);
|
||||
|
||||
// figure out the gain for this source at the listener
|
||||
float gain = gainForSource(listeningNodeStream, streamToAdd, relativePosition, isEcho);
|
||||
|
||||
// figure out the azimuth to this source at the listener
|
||||
float azimuth = isEcho ? 0.0f : azimuthForSource(listeningNodeStream, listeningNodeStream, relativePosition);
|
||||
|
||||
float repeatedFrameFadeFactor = 1.0f;
|
||||
|
||||
static const int HRTF_DATASET_INDEX = 1;
|
||||
|
||||
if (!streamToAdd.lastPopSucceeded()) {
|
||||
bool forceSilentBlock = true;
|
||||
|
||||
if (!streamToAdd.getLastPopOutput().isNull()) {
|
||||
bool isInjector = dynamic_cast<const InjectedAudioStream*>(&streamToAdd);
|
||||
|
||||
// in an injector, just go silent - the injector has likely ended
|
||||
// in other inputs (microphone, &c.), repeat with fade to avoid the harsh jump to silence
|
||||
|
||||
// we'll repeat the last block until it has a block to mix
|
||||
// and we'll gradually fade that repeated block into silence.
|
||||
|
||||
// calculate its fade factor, which depends on how many times it's already been repeated.
|
||||
repeatedFrameFadeFactor = calculateRepeatedFrameFadeFactor(streamToAdd.getConsecutiveNotMixedCount() - 1);
|
||||
if (!isInjector && repeatedFrameFadeFactor > 0.0f) {
|
||||
// apply the repeatedFrameFadeFactor to the gain
|
||||
gain *= repeatedFrameFadeFactor;
|
||||
|
||||
forceSilentBlock = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (forceSilentBlock) {
|
||||
// we're deciding not to repeat either since we've already done it enough times or repetition with fade is disabled
|
||||
// in this case we will call renderSilent with a forced silent block
|
||||
// this ensures the correct tail from the previously mixed block and the correct spatialization of first block
|
||||
// of any upcoming audio
|
||||
|
||||
if (!streamToAdd.isStereo() && !isEcho) {
|
||||
// get the existing listener-source HRTF object, or create a new one
|
||||
auto& hrtf = listenerNodeData.hrtfForStream(sourceNodeID, streamToAdd.getStreamIdentifier());
|
||||
|
||||
// this is not done for stereo streams since they do not go through the HRTF
|
||||
static int16_t silentMonoBlock[AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL] = {};
|
||||
hrtf.renderSilent(silentMonoBlock, _mixSamples, HRTF_DATASET_INDEX, azimuth, distance, gain,
|
||||
AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL);
|
||||
|
||||
++stats.hrtfSilentRenders;
|
||||
// set up the bounding box for the node
|
||||
AABox nodeBox(nodeData->getAvatarBoundingBoxCorner(), nodeData->getAvatarBoundingBoxScale());
|
||||
// Clamp the size of the bounding box to a minimum scale
|
||||
if (glm::any(glm::lessThan(nodeData->getAvatarBoundingBoxScale(), minBubbleSize))) {
|
||||
nodeBox.setScaleStayCentered(minBubbleSize);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
// quadruple the scale of both bounding boxes
|
||||
listenerBox.embiggen(4.0f);
|
||||
nodeBox.embiggen(4.0f);
|
||||
|
||||
// grab the stream from the ring buffer
|
||||
AudioRingBuffer::ConstIterator streamPopOutput = streamToAdd.getLastPopOutput();
|
||||
|
||||
if (streamToAdd.isStereo() || isEcho) {
|
||||
// this is a stereo source or server echo so we do not pass it through the HRTF
|
||||
// simply apply our calculated gain to each sample
|
||||
if (streamToAdd.isStereo()) {
|
||||
for (int i = 0; i < AudioConstants::NETWORK_FRAME_SAMPLES_STEREO; ++i) {
|
||||
_mixSamples[i] += float(streamPopOutput[i] * gain / AudioConstants::MAX_SAMPLE_VALUE);
|
||||
}
|
||||
|
||||
++stats.manualStereoMixes;
|
||||
// perform the collision check between the two bounding boxes
|
||||
ignore = listenerBox.touches(nodeBox);
|
||||
} else {
|
||||
for (int i = 0; i < AudioConstants::NETWORK_FRAME_SAMPLES_STEREO; i += 2) {
|
||||
auto monoSample = float(streamPopOutput[i / 2] * gain / AudioConstants::MAX_SAMPLE_VALUE);
|
||||
_mixSamples[i] += monoSample;
|
||||
_mixSamples[i + 1] += monoSample;
|
||||
}
|
||||
|
||||
++stats.manualEchoMixes;
|
||||
ignore = false;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// get the existing listener-source HRTF object, or create a new one
|
||||
auto& hrtf = listenerNodeData.hrtfForStream(sourceNodeID, streamToAdd.getStreamIdentifier());
|
||||
|
||||
streamPopOutput.readSamples(_bufferSamples, AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL);
|
||||
|
||||
// if the frame we're about to mix is silent, simply call render silent and move on
|
||||
if (streamToAdd.getLastPopOutputLoudness() == 0.0f) {
|
||||
// silent frame from source
|
||||
|
||||
// we still need to call renderSilent via the HRTF for mono source
|
||||
hrtf.renderSilent(_bufferSamples, _mixSamples, HRTF_DATASET_INDEX, azimuth, distance, gain,
|
||||
AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL);
|
||||
|
||||
++stats.hrtfSilentRenders;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
float audibilityThreshold = AudioMixer::getMinimumAudibilityThreshold();
|
||||
if (audibilityThreshold > 0.0f &&
|
||||
streamToAdd.getLastPopOutputTrailingLoudness() / glm::length(relativePosition) <= audibilityThreshold) {
|
||||
// the mixer is struggling so we're going to drop off some streams
|
||||
|
||||
// we call renderSilent via the HRTF with the actual frame data and a gain of 0.0
|
||||
hrtf.renderSilent(_bufferSamples, _mixSamples, HRTF_DATASET_INDEX, azimuth, distance, 0.0f,
|
||||
AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL);
|
||||
|
||||
++stats.hrtfStruggleRenders;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
++stats.hrtfRenders;
|
||||
|
||||
// mono stream, call the HRTF with our block and calculated azimuth and gain
|
||||
hrtf.render(_bufferSamples, _mixSamples, HRTF_DATASET_INDEX, azimuth, distance, gain,
|
||||
AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL);
|
||||
return ignore;
|
||||
}
|
||||
|
||||
float AudioMixerSlave::gainForSource(const AvatarAudioStream& listeningNodeStream, const PositionalAudioStream& streamToAdd,
|
||||
float gainForSource(const AvatarAudioStream& listeningNodeStream, const PositionalAudioStream& streamToAdd,
|
||||
const glm::vec3& relativePosition, bool isEcho) {
|
||||
float gain = 1.0f;
|
||||
|
||||
|
@ -472,7 +535,7 @@ float AudioMixerSlave::gainForSource(const AvatarAudioStream& listeningNodeStrea
|
|||
return gain;
|
||||
}
|
||||
|
||||
float AudioMixerSlave::azimuthForSource(const AvatarAudioStream& listeningNodeStream, const PositionalAudioStream& streamToAdd,
|
||||
float azimuthForSource(const AvatarAudioStream& listeningNodeStream, const PositionalAudioStream& streamToAdd,
|
||||
const glm::vec3& relativePosition) {
|
||||
glm::quat inverseOrientation = glm::inverse(listeningNodeStream.getOrientation());
|
||||
|
||||
|
@ -482,7 +545,7 @@ float AudioMixerSlave::azimuthForSource(const AvatarAudioStream& listeningNodeSt
|
|||
// project the rotated source position vector onto the XZ plane
|
||||
rotatedSourcePosition.y = 0.0f;
|
||||
|
||||
static const float SOURCE_DISTANCE_THRESHOLD = 1e-30f;
|
||||
const float SOURCE_DISTANCE_THRESHOLD = 1e-30f;
|
||||
|
||||
if (glm::length2(rotatedSourcePosition) > SOURCE_DISTANCE_THRESHOLD) {
|
||||
// produce an oriented angle about the y-axis
|
||||
|
|
|
@ -30,7 +30,7 @@ class AudioMixerSlave {
|
|||
public:
|
||||
using ConstIter = NodeList::const_iterator;
|
||||
|
||||
void configure(ConstIter begin, ConstIter end, unsigned int frame);
|
||||
void configure(ConstIter begin, ConstIter end, unsigned int frame, float throttlingRatio);
|
||||
|
||||
// mix and broadcast non-ignored streams to the node
|
||||
// returns true if a mixed packet was sent to the node
|
||||
|
@ -40,15 +40,14 @@ public:
|
|||
|
||||
private:
|
||||
// create mix, returns true if mix has audio
|
||||
bool prepareMix(const SharedNodePointer& node);
|
||||
// add a stream to the mix
|
||||
void addStreamToMix(AudioMixerClientData& listenerData, const QUuid& streamerID,
|
||||
bool prepareMix(const SharedNodePointer& listener);
|
||||
void throttleStream(AudioMixerClientData& listenerData, const QUuid& streamerID,
|
||||
const AvatarAudioStream& listenerStream, const PositionalAudioStream& streamer);
|
||||
|
||||
float gainForSource(const AvatarAudioStream& listener, const PositionalAudioStream& streamer,
|
||||
const glm::vec3& relativePosition, bool isEcho);
|
||||
float azimuthForSource(const AvatarAudioStream& listener, const PositionalAudioStream& streamer,
|
||||
const glm::vec3& relativePosition);
|
||||
void mixStream(AudioMixerClientData& listenerData, const QUuid& streamerID,
|
||||
const AvatarAudioStream& listenerStream, const PositionalAudioStream& streamer);
|
||||
void addStream(AudioMixerClientData& listenerData, const QUuid& streamerID,
|
||||
const AvatarAudioStream& listenerStream, const PositionalAudioStream& streamer,
|
||||
bool throttle);
|
||||
|
||||
// mixing buffers
|
||||
float _mixSamples[AudioConstants::NETWORK_FRAME_SAMPLES_STEREO];
|
||||
|
@ -58,6 +57,7 @@ private:
|
|||
ConstIter _begin;
|
||||
ConstIter _end;
|
||||
unsigned int _frame { 0 };
|
||||
float _throttlingRatio { 0.0f };
|
||||
};
|
||||
|
||||
#endif // hifi_AudioMixerSlave_h
|
||||
|
|
|
@ -41,7 +41,7 @@ void AudioMixerSlaveThread::wait() {
|
|||
});
|
||||
++_pool._numStarted;
|
||||
}
|
||||
configure(_pool._begin, _pool._end, _pool._frame);
|
||||
configure(_pool._begin, _pool._end, _pool._frame, _pool._throttlingRatio);
|
||||
}
|
||||
|
||||
void AudioMixerSlaveThread::notify(bool stopping) {
|
||||
|
@ -64,13 +64,14 @@ bool AudioMixerSlaveThread::try_pop(SharedNodePointer& node) {
|
|||
static AudioMixerSlave slave;
|
||||
#endif
|
||||
|
||||
void AudioMixerSlavePool::mix(ConstIter begin, ConstIter end, unsigned int frame) {
|
||||
void AudioMixerSlavePool::mix(ConstIter begin, ConstIter end, unsigned int frame, float throttlingRatio) {
|
||||
_begin = begin;
|
||||
_end = end;
|
||||
_frame = frame;
|
||||
_throttlingRatio = throttlingRatio;
|
||||
|
||||
#ifdef AUDIO_SINGLE_THREADED
|
||||
slave.configure(_begin, _end, frame);
|
||||
slave.configure(_begin, _end, frame, throttlingRatio);
|
||||
std::for_each(begin, end, [&](const SharedNodePointer& node) {
|
||||
slave.mix(node);
|
||||
});
|
||||
|
@ -131,7 +132,7 @@ void AudioMixerSlavePool::setNumThreads(int numThreads) {
|
|||
}
|
||||
|
||||
void AudioMixerSlavePool::resize(int numThreads) {
|
||||
assert(_numThreads == _slaves.size());
|
||||
assert(_numThreads == (int)_slaves.size());
|
||||
|
||||
#ifdef AUDIO_SINGLE_THREADED
|
||||
qDebug("%s: running single threaded", __FUNCTION__, numThreads);
|
||||
|
@ -182,6 +183,6 @@ void AudioMixerSlavePool::resize(int numThreads) {
|
|||
}
|
||||
|
||||
_numThreads = _numStarted = _numFinished = numThreads;
|
||||
assert(_numThreads == _slaves.size());
|
||||
assert(_numThreads == (int)_slaves.size());
|
||||
#endif
|
||||
}
|
||||
|
|
|
@ -61,7 +61,7 @@ public:
|
|||
~AudioMixerSlavePool() { resize(0); }
|
||||
|
||||
// mix on slave threads
|
||||
void mix(ConstIter begin, ConstIter end, unsigned int frame);
|
||||
void mix(ConstIter begin, ConstIter end, unsigned int frame, float throttlingRatio);
|
||||
|
||||
// iterate over all slaves
|
||||
void each(std::function<void(AudioMixerSlave& slave)> functor);
|
||||
|
@ -90,6 +90,7 @@ private:
|
|||
// frame state
|
||||
Queue _queue;
|
||||
unsigned int _frame { 0 };
|
||||
float _throttlingRatio { 0.0f };
|
||||
ConstIter _begin;
|
||||
ConstIter _end;
|
||||
};
|
||||
|
|
|
@ -17,7 +17,7 @@ void AudioMixerStats::reset() {
|
|||
totalMixes = 0;
|
||||
hrtfRenders = 0;
|
||||
hrtfSilentRenders = 0;
|
||||
hrtfStruggleRenders = 0;
|
||||
hrtfThrottleRenders = 0;
|
||||
manualStereoMixes = 0;
|
||||
manualEchoMixes = 0;
|
||||
}
|
||||
|
@ -28,7 +28,7 @@ void AudioMixerStats::accumulate(const AudioMixerStats& otherStats) {
|
|||
totalMixes += otherStats.totalMixes;
|
||||
hrtfRenders += otherStats.hrtfRenders;
|
||||
hrtfSilentRenders += otherStats.hrtfSilentRenders;
|
||||
hrtfStruggleRenders += otherStats.hrtfStruggleRenders;
|
||||
hrtfThrottleRenders += otherStats.hrtfThrottleRenders;
|
||||
manualStereoMixes += otherStats.manualStereoMixes;
|
||||
manualEchoMixes += otherStats.manualEchoMixes;
|
||||
}
|
||||
|
|
|
@ -20,7 +20,7 @@ struct AudioMixerStats {
|
|||
|
||||
int hrtfRenders { 0 };
|
||||
int hrtfSilentRenders { 0 };
|
||||
int hrtfStruggleRenders { 0 };
|
||||
int hrtfThrottleRenders { 0 };
|
||||
|
||||
int manualStereoMixes { 0 };
|
||||
int manualEchoMixes { 0 };
|
||||
|
|
|
@ -262,8 +262,12 @@ void AvatarMixer::broadcastAvatarData() {
|
|||
// setup a PacketList for the avatarPackets
|
||||
auto avatarPacketList = NLPacketList::create(PacketType::BulkAvatarData);
|
||||
|
||||
if (avatar.getSessionDisplayName().isEmpty() && // We haven't set it yet...
|
||||
nodeData->getReceivedIdentity()) { // ... but we have processed identity (with possible displayName).
|
||||
if (nodeData->getAvatarSessionDisplayNameMustChange()) {
|
||||
const QString& existingBaseDisplayName = nodeData->getBaseDisplayName();
|
||||
if (--_sessionDisplayNames[existingBaseDisplayName].second <= 0) {
|
||||
_sessionDisplayNames.remove(existingBaseDisplayName);
|
||||
}
|
||||
|
||||
QString baseName = avatar.getDisplayName().trimmed();
|
||||
const QRegularExpression curses{ "fuck|shit|damn|cock|cunt" }; // POC. We may eventually want something much more elaborate (subscription?).
|
||||
baseName = baseName.replace(curses, "*"); // Replace rather than remove, so that people have a clue that the person's a jerk.
|
||||
|
@ -276,11 +280,14 @@ void AvatarMixer::broadcastAvatarData() {
|
|||
QPair<int, int>& soFar = _sessionDisplayNames[baseName]; // Inserts and answers 0, 0 if not already present, which is what we want.
|
||||
int& highWater = soFar.first;
|
||||
nodeData->setBaseDisplayName(baseName);
|
||||
avatar.setSessionDisplayName((highWater > 0) ? baseName + "_" + QString::number(highWater) : baseName);
|
||||
QString sessionDisplayName = (highWater > 0) ? baseName + "_" + QString::number(highWater) : baseName;
|
||||
avatar.setSessionDisplayName(sessionDisplayName);
|
||||
highWater++;
|
||||
soFar.second++; // refcount
|
||||
nodeData->flagIdentityChange();
|
||||
sendIdentityPacket(nodeData, node); // Tell new node about its sessionUUID. Others will find out below.
|
||||
nodeData->setAvatarSessionDisplayNameMustChange(false);
|
||||
sendIdentityPacket(nodeData, node); // Tell node whose name changed about its new session display name. Others will find out below.
|
||||
qDebug() << "Giving session display name" << sessionDisplayName << "to node with ID" << node->getUUID();
|
||||
}
|
||||
|
||||
// this is an AGENT we have received head data from
|
||||
|
@ -416,12 +423,17 @@ void AvatarMixer::broadcastAvatarData() {
|
|||
nodeData->incrementAvatarOutOfView();
|
||||
} else {
|
||||
detail = distribution(generator) < AVATAR_SEND_FULL_UPDATE_RATIO
|
||||
? AvatarData::SendAllData : AvatarData::IncludeSmallData;
|
||||
? AvatarData::SendAllData : AvatarData::CullSmallData;
|
||||
nodeData->incrementAvatarInView();
|
||||
}
|
||||
|
||||
numAvatarDataBytes += avatarPacketList->write(otherNode->getUUID().toRfc4122());
|
||||
numAvatarDataBytes += avatarPacketList->write(otherAvatar.toByteArray(detail));
|
||||
auto lastEncodeForOther = nodeData->getLastOtherAvatarEncodeTime(otherNode->getUUID());
|
||||
QVector<JointData>& lastSentJointsForOther = nodeData->getLastOtherAvatarSentJoints(otherNode->getUUID());
|
||||
bool distanceAdjust = true;
|
||||
glm::vec3 viewerPosition = nodeData->getPosition();
|
||||
auto bytes = otherAvatar.toByteArray(detail, lastEncodeForOther, lastSentJointsForOther, distanceAdjust, viewerPosition, &lastSentJointsForOther);
|
||||
numAvatarDataBytes += avatarPacketList->write(bytes);
|
||||
|
||||
avatarPacketList->endSegment();
|
||||
});
|
||||
|
@ -581,10 +593,15 @@ void AvatarMixer::handleAvatarIdentityPacket(QSharedPointer<ReceivedMessage> mes
|
|||
// parse the identity packet and update the change timestamp if appropriate
|
||||
AvatarData::Identity identity;
|
||||
AvatarData::parseAvatarIdentityPacket(message->getMessage(), identity);
|
||||
if (avatar.processAvatarIdentity(identity)) {
|
||||
bool identityChanged = false;
|
||||
bool displayNameChanged = false;
|
||||
avatar.processAvatarIdentity(identity, identityChanged, displayNameChanged);
|
||||
if (identityChanged) {
|
||||
QMutexLocker nodeDataLocker(&nodeData->getMutex());
|
||||
nodeData->flagIdentityChange();
|
||||
nodeData->setReceivedIdentity();
|
||||
if (displayNameChanged) {
|
||||
nodeData->setAvatarSessionDisplayNameMustChange(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -674,8 +691,8 @@ void AvatarMixer::run() {
|
|||
|
||||
void AvatarMixer::domainSettingsRequestComplete() {
|
||||
auto nodeList = DependencyManager::get<NodeList>();
|
||||
nodeList->addNodeTypeToInterestSet(NodeType::Agent);
|
||||
|
||||
nodeList->addSetOfNodeTypesToNodeInterestSet({ NodeType::Agent, NodeType::EntityScriptServer });
|
||||
|
||||
// parse the settings to pull out the values we need
|
||||
parseDomainServerSettings(nodeList->getDomainHandler().getSettingsObject());
|
||||
|
||||
|
|
|
@ -53,8 +53,8 @@ public:
|
|||
|
||||
HRCTime getIdentityChangeTimestamp() const { return _identityChangeTimestamp; }
|
||||
void flagIdentityChange() { _identityChangeTimestamp = p_high_resolution_clock::now(); }
|
||||
bool getReceivedIdentity() const { return _gotIdentity; }
|
||||
void setReceivedIdentity() { _gotIdentity = true; }
|
||||
bool getAvatarSessionDisplayNameMustChange() const { return _avatarSessionDisplayNameMustChange; }
|
||||
void setAvatarSessionDisplayNameMustChange(bool set = true) { _avatarSessionDisplayNameMustChange = set; }
|
||||
|
||||
void setFullRateDistance(float fullRateDistance) { _fullRateDistance = fullRateDistance; }
|
||||
float getFullRateDistance() const { return _fullRateDistance; }
|
||||
|
@ -104,6 +104,22 @@ public:
|
|||
bool getRequestsDomainListData() { return _requestsDomainListData; }
|
||||
void setRequestsDomainListData(bool requesting) { _requestsDomainListData = requesting; }
|
||||
|
||||
quint64 getLastOtherAvatarEncodeTime(QUuid otherAvatar) {
|
||||
quint64 result = 0;
|
||||
if (_lastOtherAvatarEncodeTime.find(otherAvatar) != _lastOtherAvatarEncodeTime.end()) {
|
||||
result = _lastOtherAvatarEncodeTime[otherAvatar];
|
||||
}
|
||||
_lastOtherAvatarEncodeTime[otherAvatar] = usecTimestampNow();
|
||||
return result;
|
||||
}
|
||||
|
||||
QVector<JointData>& getLastOtherAvatarSentJoints(QUuid otherAvatar) {
|
||||
_lastOtherAvatarSentJoints[otherAvatar].resize(_avatar->getJointCount());
|
||||
return _lastOtherAvatarSentJoints[otherAvatar];
|
||||
}
|
||||
|
||||
|
||||
|
||||
private:
|
||||
AvatarSharedPointer _avatar { new AvatarData() };
|
||||
|
||||
|
@ -111,8 +127,13 @@ private:
|
|||
std::unordered_map<QUuid, uint16_t> _lastBroadcastSequenceNumbers;
|
||||
std::unordered_set<QUuid> _hasReceivedFirstPacketsFrom;
|
||||
|
||||
// this is a map of the last time we encoded an "other" avatar for
|
||||
// sending to "this" node
|
||||
std::unordered_map<QUuid, quint64> _lastOtherAvatarEncodeTime;
|
||||
std::unordered_map<QUuid, QVector<JointData>> _lastOtherAvatarSentJoints;
|
||||
|
||||
HRCTime _identityChangeTimestamp;
|
||||
bool _gotIdentity { false };
|
||||
bool _avatarSessionDisplayNameMustChange{ false };
|
||||
|
||||
float _fullRateDistance = FLT_MAX;
|
||||
float _maxAvatarDistance = FLT_MAX;
|
||||
|
|
|
@ -14,6 +14,13 @@
|
|||
#include <GLMHelpers.h>
|
||||
#include "ScriptableAvatar.h"
|
||||
|
||||
QByteArray ScriptableAvatar::toByteArray(AvatarDataDetail dataDetail, quint64 lastSentTime, const QVector<JointData>& lastSentJointData,
|
||||
bool distanceAdjust, glm::vec3 viewerPosition, QVector<JointData>* sentJointDataOut) {
|
||||
_globalPosition = getPosition();
|
||||
return AvatarData::toByteArray(dataDetail, lastSentTime, lastSentJointData, distanceAdjust, viewerPosition, sentJointDataOut);
|
||||
}
|
||||
|
||||
|
||||
// hold and priority unused but kept so that client side JS can run.
|
||||
void ScriptableAvatar::startAnimation(const QString& url, float fps, float priority,
|
||||
bool loop, bool hold, float firstFrame, float lastFrame, const QStringList& maskedJoints) {
|
||||
|
|
|
@ -27,6 +27,10 @@ public:
|
|||
Q_INVOKABLE void stopAnimation();
|
||||
Q_INVOKABLE AnimationDetails getAnimationDetails();
|
||||
virtual void setSkeletonModelURL(const QUrl& skeletonModelURL) override;
|
||||
|
||||
virtual QByteArray toByteArray(AvatarDataDetail dataDetail, quint64 lastSentTime, const QVector<JointData>& lastSentJointData,
|
||||
bool distanceAdjust = false, glm::vec3 viewerPosition = glm::vec3(0), QVector<JointData>* sentJointDataOut = nullptr) override;
|
||||
|
||||
|
||||
private slots:
|
||||
void update(float deltatime);
|
||||
|
|
|
@ -11,6 +11,8 @@
|
|||
|
||||
#include "AssignmentParentFinder.h"
|
||||
|
||||
#include <AvatarHashMap.h>
|
||||
|
||||
SpatiallyNestableWeakPointer AssignmentParentFinder::find(QUuid parentID, bool& success, SpatialParentTree* entityTree) const {
|
||||
SpatiallyNestableWeakPointer parent;
|
||||
|
||||
|
@ -25,10 +27,21 @@ SpatiallyNestableWeakPointer AssignmentParentFinder::find(QUuid parentID, bool&
|
|||
} else {
|
||||
parent = _tree->findEntityByEntityItemID(parentID);
|
||||
}
|
||||
if (parent.expired()) {
|
||||
success = false;
|
||||
} else {
|
||||
if (!parent.expired()) {
|
||||
success = true;
|
||||
return parent;
|
||||
}
|
||||
|
||||
// search avatars
|
||||
if (DependencyManager::isSet<AvatarHashMap>()) {
|
||||
auto avatarHashMap = DependencyManager::get<AvatarHashMap>();
|
||||
parent = avatarHashMap->getAvatarBySessionID(parentID);
|
||||
if (!parent.expired()) {
|
||||
success = true;
|
||||
return parent;
|
||||
}
|
||||
}
|
||||
|
||||
success = false;
|
||||
return parent;
|
||||
}
|
||||
|
|
|
@ -9,9 +9,12 @@
|
|||
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
|
||||
//
|
||||
|
||||
#include <QtCore/QEventLoop>
|
||||
#include <QTimer>
|
||||
#include <EntityTree.h>
|
||||
#include <SimpleEntitySimulation.h>
|
||||
#include <ResourceCache.h>
|
||||
#include <ScriptCache.h>
|
||||
|
||||
#include "EntityServer.h"
|
||||
#include "EntityServerConsts.h"
|
||||
|
@ -26,6 +29,10 @@ EntityServer::EntityServer(ReceivedMessage& message) :
|
|||
OctreeServer(message),
|
||||
_entitySimulation(NULL)
|
||||
{
|
||||
ResourceManager::init();
|
||||
DependencyManager::set<ResourceCacheSharedItems>();
|
||||
DependencyManager::set<ScriptCache>();
|
||||
|
||||
auto& packetReceiver = DependencyManager::get<NodeList>()->getPacketReceiver();
|
||||
packetReceiver.registerListenerForTypes({ PacketType::EntityAdd, PacketType::EntityEdit, PacketType::EntityErase },
|
||||
this, "handleEntityPacket");
|
||||
|
@ -285,6 +292,96 @@ void EntityServer::readAdditionalConfiguration(const QJsonObject& settingsSectio
|
|||
} else {
|
||||
tree->setEntityScriptSourceWhitelist("");
|
||||
}
|
||||
|
||||
if (readOptionString("entityEditFilter", settingsSectionObject, _entityEditFilter) && !_entityEditFilter.isEmpty()) {
|
||||
// Tell the tree that we have a filter, so that it doesn't accept edits until we have a filter function set up.
|
||||
std::static_pointer_cast<EntityTree>(_tree)->setHasEntityFilter(true);
|
||||
// Now fetch script from file asynchronously.
|
||||
QUrl scriptURL(_entityEditFilter);
|
||||
|
||||
// The following should be abstracted out for use in Agent.cpp (and maybe later AvatarMixer.cpp)
|
||||
if (scriptURL.scheme().isEmpty() || (scriptURL.scheme() == URL_SCHEME_FILE)) {
|
||||
qWarning() << "Cannot load script from local filesystem, because assignment may be on a different computer.";
|
||||
scriptRequestFinished();
|
||||
return;
|
||||
}
|
||||
auto scriptRequest = ResourceManager::createResourceRequest(this, scriptURL);
|
||||
if (!scriptRequest) {
|
||||
qWarning() << "Could not create ResourceRequest for Agent script at" << scriptURL.toString();
|
||||
scriptRequestFinished();
|
||||
return;
|
||||
}
|
||||
// Agent.cpp sets up a timeout here, but that is unnecessary, as ResourceRequest has its own.
|
||||
connect(scriptRequest, &ResourceRequest::finished, this, &EntityServer::scriptRequestFinished);
|
||||
// FIXME: handle atp rquests setup here. See Agent::requestScript()
|
||||
qInfo() << "Requesting script at URL" << qPrintable(scriptRequest->getUrl().toString());
|
||||
scriptRequest->send();
|
||||
qDebug() << "script request sent";
|
||||
}
|
||||
}
|
||||
|
||||
// Copied from ScriptEngine.cpp. We should make this a class method for reuse.
|
||||
// Note: I've deliberately stopped short of using ScriptEngine instead of QScriptEngine, as that is out of project scope at this point.
|
||||
static bool hasCorrectSyntax(const QScriptProgram& program) {
|
||||
const auto syntaxCheck = QScriptEngine::checkSyntax(program.sourceCode());
|
||||
if (syntaxCheck.state() != QScriptSyntaxCheckResult::Valid) {
|
||||
const auto error = syntaxCheck.errorMessage();
|
||||
const auto line = QString::number(syntaxCheck.errorLineNumber());
|
||||
const auto column = QString::number(syntaxCheck.errorColumnNumber());
|
||||
const auto message = QString("[SyntaxError] %1 in %2:%3(%4)").arg(error, program.fileName(), line, column);
|
||||
qCritical() << qPrintable(message);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
static bool hadUncaughtExceptions(QScriptEngine& engine, const QString& fileName) {
|
||||
if (engine.hasUncaughtException()) {
|
||||
const auto backtrace = engine.uncaughtExceptionBacktrace();
|
||||
const auto exception = engine.uncaughtException().toString();
|
||||
const auto line = QString::number(engine.uncaughtExceptionLineNumber());
|
||||
engine.clearExceptions();
|
||||
|
||||
static const QString SCRIPT_EXCEPTION_FORMAT = "[UncaughtException] %1 in %2:%3";
|
||||
auto message = QString(SCRIPT_EXCEPTION_FORMAT).arg(exception, fileName, line);
|
||||
if (!backtrace.empty()) {
|
||||
static const auto lineSeparator = "\n ";
|
||||
message += QString("\n[Backtrace]%1%2").arg(lineSeparator, backtrace.join(lineSeparator));
|
||||
}
|
||||
qCritical() << qPrintable(message);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
void EntityServer::scriptRequestFinished() {
|
||||
qDebug() << "script request completed";
|
||||
auto scriptRequest = qobject_cast<ResourceRequest*>(sender());
|
||||
const QString urlString = scriptRequest->getUrl().toString();
|
||||
if (scriptRequest && scriptRequest->getResult() == ResourceRequest::Success) {
|
||||
auto scriptContents = scriptRequest->getData();
|
||||
qInfo() << "Downloaded script:" << scriptContents;
|
||||
QScriptProgram program(scriptContents, urlString);
|
||||
if (hasCorrectSyntax(program)) {
|
||||
_entityEditFilterEngine.evaluate(scriptContents);
|
||||
if (!hadUncaughtExceptions(_entityEditFilterEngine, urlString)) {
|
||||
std::static_pointer_cast<EntityTree>(_tree)->initEntityEditFilterEngine(&_entityEditFilterEngine, [this]() {
|
||||
return hadUncaughtExceptions(_entityEditFilterEngine, _entityEditFilter);
|
||||
});
|
||||
scriptRequest->deleteLater();
|
||||
qDebug() << "script request filter processed";
|
||||
return;
|
||||
}
|
||||
}
|
||||
} else if (scriptRequest) {
|
||||
qCritical() << "Failed to download script at" << urlString;
|
||||
// See HTTPResourceRequest::onRequestFinished for interpretation of codes. For example, a 404 is code 6 and 403 is 3. A timeout is 2. Go figure.
|
||||
qCritical() << "ResourceRequest error was" << scriptRequest->getResult();
|
||||
} else {
|
||||
qCritical() << "Failed to create script request.";
|
||||
}
|
||||
// Hard stop of the assignment client on failure. We don't want anyone to think they have a filter in place when they don't.
|
||||
// Alas, only indications will be the above logging with assignment client restarting repeatedly, and clients will not see any entities.
|
||||
qDebug() << "script request failure causing stop";
|
||||
stop();
|
||||
}
|
||||
|
||||
void EntityServer::nodeAdded(SharedNodePointer node) {
|
||||
|
|
|
@ -69,6 +69,7 @@ protected:
|
|||
|
||||
private slots:
|
||||
void handleEntityPacket(QSharedPointer<ReceivedMessage> message, SharedNodePointer senderNode);
|
||||
void scriptRequestFinished();
|
||||
|
||||
private:
|
||||
SimpleEntitySimulationPointer _entitySimulation;
|
||||
|
@ -76,6 +77,9 @@ private:
|
|||
|
||||
QReadWriteLock _viewerSendingStatsLock;
|
||||
QMap<QUuid, QMap<QUuid, ViewerSendingStats>> _viewerSendingStats;
|
||||
|
||||
QString _entityEditFilter{};
|
||||
QScriptEngine _entityEditFilterEngine{};
|
||||
};
|
||||
|
||||
#endif // hifi_EntityServer_h
|
||||
|
|
|
@ -44,8 +44,7 @@ void MessagesMixer::handleMessages(QSharedPointer<ReceivedMessage> receivedMessa
|
|||
|
||||
nodeList->eachMatchingNode(
|
||||
[&](const SharedNodePointer& node)->bool {
|
||||
return node->getType() == NodeType::Agent && node->getActiveSocket() &&
|
||||
_channelSubscribers[channel].contains(node->getUUID());
|
||||
return node->getActiveSocket() && _channelSubscribers[channel].contains(node->getUUID());
|
||||
},
|
||||
[&](const SharedNodePointer& node) {
|
||||
auto packetList = MessagesClient::encodeMessagesPacket(channel, message, senderID);
|
||||
|
@ -83,5 +82,6 @@ void MessagesMixer::sendStatsPacket() {
|
|||
|
||||
void MessagesMixer::run() {
|
||||
ThreadedAssignment::commonInit(MESSAGES_MIXER_LOGGING_NAME, NodeType::MessagesMixer);
|
||||
DependencyManager::get<NodeList>()->addNodeTypeToInterestSet(NodeType::Agent);
|
||||
}
|
||||
auto nodeList = DependencyManager::get<NodeList>();
|
||||
nodeList->addSetOfNodeTypesToNodeInterestSet({ NodeType::Agent, NodeType::EntityScriptServer });
|
||||
}
|
||||
|
|
|
@ -316,8 +316,9 @@ int OctreeSendThread::packetDistributor(SharedNodePointer node, OctreeQueryNode*
|
|||
int truePacketsSent = 0;
|
||||
int trueBytesSent = 0;
|
||||
int packetsSentThisInterval = 0;
|
||||
bool isFullScene = ((!viewFrustumChanged) && nodeData->getViewFrustumJustStoppedChanging())
|
||||
|| nodeData->hasLodChanged();
|
||||
bool isFullScene = nodeData->haveJSONParametersChanged() ||
|
||||
(nodeData->getUsesFrustum()
|
||||
&& ((!viewFrustumChanged && nodeData->getViewFrustumJustStoppedChanging()) || nodeData->hasLodChanged()));
|
||||
|
||||
bool somethingToSend = true; // assume we have something
|
||||
|
||||
|
@ -432,7 +433,9 @@ int OctreeSendThread::packetDistributor(SharedNodePointer node, OctreeQueryNode*
|
|||
boundaryLevelAdjust, octreeSizeScale,
|
||||
nodeData->getLastTimeBagEmpty(),
|
||||
isFullScene, &nodeData->stats, _myServer->getJurisdiction(),
|
||||
&nodeData->extraEncodeData);
|
||||
&nodeData->extraEncodeData,
|
||||
nodeData->getUsesFrustum(),
|
||||
nodeData);
|
||||
nodeData->copyCurrentViewFrustum(params.viewFrustum);
|
||||
if (viewFrustumChanged) {
|
||||
nodeData->copyLastKnownViewFrustum(params.lastViewFrustum);
|
||||
|
|
|
@ -660,6 +660,7 @@ bool OctreeServer::handleHTTPRequest(HTTPConnection* connection, const QUrl& url
|
|||
quint64 averageUpdateTime = _tree->getAverageUpdateTime();
|
||||
quint64 averageCreateTime = _tree->getAverageCreateTime();
|
||||
quint64 averageLoggingTime = _tree->getAverageLoggingTime();
|
||||
quint64 averageFilterTime = _tree->getAverageFilterTime();
|
||||
|
||||
int FLOAT_PRECISION = 3;
|
||||
|
||||
|
@ -699,6 +700,8 @@ bool OctreeServer::handleHTTPRequest(HTTPConnection* connection, const QUrl& url
|
|||
.arg(locale.toString((uint)averageCreateTime).rightJustified(COLUMN_WIDTH, ' '));
|
||||
statsString += QString(" Average Logging Time: %1 usecs\r\n")
|
||||
.arg(locale.toString((uint)averageLoggingTime).rightJustified(COLUMN_WIDTH, ' '));
|
||||
statsString += QString(" Average Filter Time: %1 usecs\r\n")
|
||||
.arg(locale.toString((uint)averageFilterTime).rightJustified(COLUMN_WIDTH, ' '));
|
||||
|
||||
|
||||
int senderNumber = 0;
|
||||
|
@ -1136,8 +1139,8 @@ void OctreeServer::domainSettingsRequestComplete() {
|
|||
auto nodeList = DependencyManager::get<NodeList>();
|
||||
|
||||
// we need to ask the DS about agents so we can ping/reply with them
|
||||
nodeList->addNodeTypeToInterestSet(NodeType::Agent);
|
||||
|
||||
nodeList->addSetOfNodeTypesToNodeInterestSet({ NodeType::Agent, NodeType::EntityScriptServer });
|
||||
|
||||
auto& packetReceiver = DependencyManager::get<NodeList>()->getPacketReceiver();
|
||||
packetReceiver.registerListener(getMyQueryMessageType(), this, "handleOctreeQueryPacket");
|
||||
packetReceiver.registerListener(PacketType::OctreeDataNack, this, "handleOctreeDataNackPacket");
|
||||
|
|
372
assignment-client/src/scripts/EntityScriptServer.cpp
Normal file
|
@ -0,0 +1,372 @@
|
|||
//
|
||||
// EntityScriptServer.cpp
|
||||
// assignment-client/src/scripts
|
||||
//
|
||||
// Created by Clément Brisset on 1/5/17.
|
||||
// Copyright 2013 High Fidelity, Inc.
|
||||
//
|
||||
// Distributed under the Apache License, Version 2.0.
|
||||
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
|
||||
//
|
||||
|
||||
#include "EntityScriptServer.h"
|
||||
|
||||
#include <AudioConstants.h>
|
||||
#include <AudioInjectorManager.h>
|
||||
#include <EntityScriptingInterface.h>
|
||||
#include <MessagesClient.h>
|
||||
#include <plugins/CodecPlugin.h>
|
||||
#include <plugins/PluginManager.h>
|
||||
#include <ResourceManager.h>
|
||||
#include <ScriptCache.h>
|
||||
#include <ScriptEngines.h>
|
||||
#include <SoundCache.h>
|
||||
#include <UUID.h>
|
||||
#include <WebSocketServerClass.h>
|
||||
|
||||
#include "ClientServerUtils.h"
|
||||
#include "../entities/AssignmentParentFinder.h"
|
||||
|
||||
int EntityScriptServer::_entitiesScriptEngineCount = 0;
|
||||
|
||||
EntityScriptServer::EntityScriptServer(ReceivedMessage& message) : ThreadedAssignment(message) {
|
||||
DependencyManager::get<EntityScriptingInterface>()->setPacketSender(&_entityEditSender);
|
||||
|
||||
ResourceManager::init();
|
||||
|
||||
DependencyManager::registerInheritance<SpatialParentFinder, AssignmentParentFinder>();
|
||||
|
||||
DependencyManager::set<ResourceCacheSharedItems>();
|
||||
DependencyManager::set<SoundCache>();
|
||||
DependencyManager::set<AudioInjectorManager>();
|
||||
|
||||
DependencyManager::set<ScriptCache>();
|
||||
DependencyManager::set<ScriptEngines>(ScriptEngine::ENTITY_SERVER_SCRIPT);
|
||||
|
||||
|
||||
auto& packetReceiver = DependencyManager::get<NodeList>()->getPacketReceiver();
|
||||
packetReceiver.registerListenerForTypes({ PacketType::OctreeStats, PacketType::EntityData, PacketType::EntityErase },
|
||||
this, "handleOctreePacket");
|
||||
packetReceiver.registerListener(PacketType::Jurisdiction, this, "handleJurisdictionPacket");
|
||||
packetReceiver.registerListener(PacketType::SelectedAudioFormat, this, "handleSelectedAudioFormat");
|
||||
|
||||
auto avatarHashMap = DependencyManager::set<AvatarHashMap>();
|
||||
packetReceiver.registerListener(PacketType::BulkAvatarData, avatarHashMap.data(), "processAvatarDataPacket");
|
||||
packetReceiver.registerListener(PacketType::KillAvatar, avatarHashMap.data(), "processKillAvatar");
|
||||
packetReceiver.registerListener(PacketType::AvatarIdentity, avatarHashMap.data(), "processAvatarIdentityPacket");
|
||||
|
||||
packetReceiver.registerListener(PacketType::ReloadEntityServerScript, this, "handleReloadEntityServerScriptPacket");
|
||||
packetReceiver.registerListener(PacketType::EntityScriptGetStatus, this, "handleEntityScriptGetStatusPacket");
|
||||
}
|
||||
|
||||
static const QString ENTITY_SCRIPT_SERVER_LOGGING_NAME = "entity-script-server";
|
||||
|
||||
void EntityScriptServer::handleReloadEntityServerScriptPacket(QSharedPointer<ReceivedMessage> message, SharedNodePointer senderNode) {
|
||||
// These are temporary checks until we can ensure that nodes eventually disconnect if the Domain Server stops telling them
|
||||
// about each other.
|
||||
if (senderNode->getCanRez() || senderNode->getCanRezTmp()) {
|
||||
auto entityID = QUuid::fromRfc4122(message->read(NUM_BYTES_RFC4122_UUID));
|
||||
|
||||
if (_entityViewer.getTree() && !_shuttingDown) {
|
||||
qDebug() << "Reloading: " << entityID;
|
||||
_entitiesScriptEngine->unloadEntityScript(entityID);
|
||||
checkAndCallPreload(entityID, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void EntityScriptServer::handleEntityScriptGetStatusPacket(QSharedPointer<ReceivedMessage> message, SharedNodePointer senderNode) {
|
||||
// These are temporary checks until we can ensure that nodes eventually disconnect if the Domain Server stops telling them
|
||||
// about each other.
|
||||
if (senderNode->getCanRez() || senderNode->getCanRezTmp()) {
|
||||
MessageID messageID;
|
||||
message->readPrimitive(&messageID);
|
||||
auto entityID = QUuid::fromRfc4122(message->read(NUM_BYTES_RFC4122_UUID));
|
||||
|
||||
auto replyPacketList = NLPacketList::create(PacketType::EntityScriptGetStatusReply, QByteArray(), true, true);
|
||||
replyPacketList->writePrimitive(messageID);
|
||||
|
||||
EntityScriptDetails details;
|
||||
if (_entitiesScriptEngine->getEntityScriptDetails(entityID, details)) {
|
||||
replyPacketList->writePrimitive(true);
|
||||
replyPacketList->writePrimitive(details.status);
|
||||
replyPacketList->writeString(details.errorInfo);
|
||||
} else {
|
||||
replyPacketList->writePrimitive(false);
|
||||
}
|
||||
|
||||
auto nodeList = DependencyManager::get<NodeList>();
|
||||
nodeList->sendPacketList(std::move(replyPacketList), *senderNode);
|
||||
}
|
||||
}
|
||||
|
||||
void EntityScriptServer::run() {
|
||||
// make sure we request our script once the agent connects to the domain
|
||||
auto nodeList = DependencyManager::get<NodeList>();
|
||||
|
||||
ThreadedAssignment::commonInit(ENTITY_SCRIPT_SERVER_LOGGING_NAME, NodeType::EntityScriptServer);
|
||||
|
||||
// Setup MessagesClient
|
||||
auto messagesClient = DependencyManager::set<MessagesClient>();
|
||||
QThread* messagesThread = new QThread;
|
||||
messagesThread->setObjectName("Messages Client Thread");
|
||||
messagesClient->moveToThread(messagesThread);
|
||||
connect(messagesThread, &QThread::started, messagesClient.data(), &MessagesClient::init);
|
||||
messagesThread->start();
|
||||
|
||||
// make sure we hear about connected nodes so we can grab an ATP script if a request is pending
|
||||
connect(nodeList.data(), &LimitedNodeList::nodeActivated, this, &EntityScriptServer::nodeActivated);
|
||||
connect(nodeList.data(), &LimitedNodeList::nodeKilled, this, &EntityScriptServer::nodeKilled);
|
||||
|
||||
nodeList->addSetOfNodeTypesToNodeInterestSet({
|
||||
NodeType::Agent, NodeType::AudioMixer, NodeType::AvatarMixer,
|
||||
NodeType::EntityServer, NodeType::MessagesMixer, NodeType::AssetServer
|
||||
});
|
||||
|
||||
// Setup Script Engine
|
||||
resetEntitiesScriptEngine();
|
||||
|
||||
// we need to make sure that init has been called for our EntityScriptingInterface
|
||||
// so that it actually has a jurisdiction listener when we ask it for it next
|
||||
auto entityScriptingInterface = DependencyManager::get<EntityScriptingInterface>();
|
||||
entityScriptingInterface->init();
|
||||
_entityViewer.setJurisdictionListener(entityScriptingInterface->getJurisdictionListener());
|
||||
|
||||
_entityViewer.init();
|
||||
|
||||
// setup the JSON filter that asks for entities with a non-default serverScripts property
|
||||
QJsonObject queryJSONParameters;
|
||||
static const QString SERVER_SCRIPTS_PROPERTY = "serverScripts";
|
||||
queryJSONParameters[SERVER_SCRIPTS_PROPERTY] = EntityQueryFilterSymbol::NonDefault;
|
||||
|
||||
// setup the JSON parameters so that OctreeQuery does not use a frustum and uses our JSON filter
|
||||
_entityViewer.getOctreeQuery().setUsesFrustum(false);
|
||||
_entityViewer.getOctreeQuery().setJSONParameters(queryJSONParameters);
|
||||
|
||||
entityScriptingInterface->setEntityTree(_entityViewer.getTree());
|
||||
|
||||
DependencyManager::set<AssignmentParentFinder>(_entityViewer.getTree());
|
||||
|
||||
|
||||
auto tree = _entityViewer.getTree().get();
|
||||
connect(tree, &EntityTree::deletingEntity, this, &EntityScriptServer::deletingEntity, Qt::QueuedConnection);
|
||||
connect(tree, &EntityTree::addingEntity, this, &EntityScriptServer::addingEntity, Qt::QueuedConnection);
|
||||
connect(tree, &EntityTree::entityServerScriptChanging, this, &EntityScriptServer::entityServerScriptChanging, Qt::QueuedConnection);
|
||||
}
|
||||
|
||||
void EntityScriptServer::nodeActivated(SharedNodePointer activatedNode) {
|
||||
if (activatedNode->getType() == NodeType::AudioMixer) {
|
||||
negotiateAudioFormat();
|
||||
}
|
||||
}
|
||||
|
||||
void EntityScriptServer::negotiateAudioFormat() {
|
||||
auto nodeList = DependencyManager::get<NodeList>();
|
||||
auto negotiateFormatPacket = NLPacket::create(PacketType::NegotiateAudioFormat);
|
||||
auto codecPlugins = PluginManager::getInstance()->getCodecPlugins();
|
||||
quint8 numberOfCodecs = (quint8)codecPlugins.size();
|
||||
negotiateFormatPacket->writePrimitive(numberOfCodecs);
|
||||
for (auto& plugin : codecPlugins) {
|
||||
auto codecName = plugin->getName();
|
||||
negotiateFormatPacket->writeString(codecName);
|
||||
}
|
||||
|
||||
// grab our audio mixer from the NodeList, if it exists
|
||||
SharedNodePointer audioMixer = nodeList->soloNodeOfType(NodeType::AudioMixer);
|
||||
|
||||
if (audioMixer) {
|
||||
// send off this mute packet
|
||||
nodeList->sendPacket(std::move(negotiateFormatPacket), *audioMixer);
|
||||
}
|
||||
}
|
||||
|
||||
void EntityScriptServer::handleSelectedAudioFormat(QSharedPointer<ReceivedMessage> message) {
|
||||
QString selectedCodecName = message->readString();
|
||||
selectAudioFormat(selectedCodecName);
|
||||
}
|
||||
|
||||
void EntityScriptServer::selectAudioFormat(const QString& selectedCodecName) {
|
||||
_selectedCodecName = selectedCodecName;
|
||||
|
||||
qDebug() << "Selected Codec:" << _selectedCodecName;
|
||||
|
||||
// release any old codec encoder/decoder first...
|
||||
if (_codec && _encoder) {
|
||||
_codec->releaseEncoder(_encoder);
|
||||
_encoder = nullptr;
|
||||
_codec = nullptr;
|
||||
}
|
||||
|
||||
auto codecPlugins = PluginManager::getInstance()->getCodecPlugins();
|
||||
for (auto& plugin : codecPlugins) {
|
||||
if (_selectedCodecName == plugin->getName()) {
|
||||
_codec = plugin;
|
||||
_encoder = plugin->createEncoder(AudioConstants::SAMPLE_RATE, AudioConstants::MONO);
|
||||
qDebug() << "Selected Codec Plugin:" << _codec.get();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void EntityScriptServer::resetEntitiesScriptEngine() {
|
||||
auto engineName = QString("Entities %1").arg(++_entitiesScriptEngineCount);
|
||||
auto newEngine = QSharedPointer<ScriptEngine>(new ScriptEngine(ScriptEngine::ENTITY_SERVER_SCRIPT, NO_SCRIPT, engineName));
|
||||
|
||||
auto webSocketServerConstructorValue = newEngine->newFunction(WebSocketServerClass::constructor);
|
||||
newEngine->globalObject().setProperty("WebSocketServer", webSocketServerConstructorValue);
|
||||
|
||||
newEngine->registerGlobalObject("SoundCache", DependencyManager::get<SoundCache>().data());
|
||||
|
||||
// connect this script engines printedMessage signal to the global ScriptEngines these various messages
|
||||
auto scriptEngines = DependencyManager::get<ScriptEngines>().data();
|
||||
connect(newEngine.data(), &ScriptEngine::printedMessage, scriptEngines, &ScriptEngines::onPrintedMessage);
|
||||
connect(newEngine.data(), &ScriptEngine::errorMessage, scriptEngines, &ScriptEngines::onErrorMessage);
|
||||
connect(newEngine.data(), &ScriptEngine::warningMessage, scriptEngines, &ScriptEngines::onWarningMessage);
|
||||
connect(newEngine.data(), &ScriptEngine::infoMessage, scriptEngines, &ScriptEngines::onInfoMessage);
|
||||
|
||||
connect(newEngine.data(), &ScriptEngine::update, this, [this] {
|
||||
_entityViewer.queryOctree();
|
||||
});
|
||||
|
||||
|
||||
newEngine->runInThread();
|
||||
DependencyManager::get<EntityScriptingInterface>()->setEntitiesScriptEngine(newEngine.data());
|
||||
|
||||
_entitiesScriptEngine.swap(newEngine);
|
||||
}
|
||||
|
||||
|
||||
void EntityScriptServer::clear() {
|
||||
// unload and stop the engine
|
||||
if (_entitiesScriptEngine) {
|
||||
// do this here (instead of in deleter) to avoid marshalling unload signals back to this thread
|
||||
_entitiesScriptEngine->unloadAllEntityScripts();
|
||||
_entitiesScriptEngine->stop();
|
||||
}
|
||||
|
||||
// reset the engine
|
||||
if (!_shuttingDown) {
|
||||
resetEntitiesScriptEngine();
|
||||
}
|
||||
|
||||
_entityViewer.clear();
|
||||
}
|
||||
|
||||
void EntityScriptServer::shutdownScriptEngine() {
|
||||
if (_entitiesScriptEngine) {
|
||||
_entitiesScriptEngine->disconnectNonEssentialSignals(); // disconnect all slots/signals from the script engine, except essential
|
||||
}
|
||||
_shuttingDown = true;
|
||||
|
||||
clear(); // always clear() on shutdown
|
||||
}
|
||||
|
||||
void EntityScriptServer::addingEntity(const EntityItemID& entityID) {
|
||||
checkAndCallPreload(entityID);
|
||||
}
|
||||
|
||||
void EntityScriptServer::deletingEntity(const EntityItemID& entityID) {
|
||||
if (_entityViewer.getTree() && !_shuttingDown && _entitiesScriptEngine) {
|
||||
_entitiesScriptEngine->unloadEntityScript(entityID);
|
||||
}
|
||||
}
|
||||
|
||||
void EntityScriptServer::entityServerScriptChanging(const EntityItemID& entityID, const bool reload) {
|
||||
if (_entityViewer.getTree() && !_shuttingDown) {
|
||||
_entitiesScriptEngine->unloadEntityScript(entityID);
|
||||
checkAndCallPreload(entityID, reload);
|
||||
}
|
||||
}
|
||||
|
||||
void EntityScriptServer::checkAndCallPreload(const EntityItemID& entityID, const bool reload) {
|
||||
if (_entityViewer.getTree() && !_shuttingDown && _entitiesScriptEngine) {
|
||||
|
||||
EntityItemPointer entity = _entityViewer.getTree()->findEntityByEntityItemID(entityID);
|
||||
EntityScriptDetails details;
|
||||
bool notRunning = !_entitiesScriptEngine->getEntityScriptDetails(entityID, details);
|
||||
if (entity && (reload || notRunning || details.scriptText != entity->getServerScripts())) {
|
||||
QString scriptUrl = entity->getServerScripts();
|
||||
if (!scriptUrl.isEmpty()) {
|
||||
scriptUrl = ResourceManager::normalizeURL(scriptUrl);
|
||||
qDebug() << "Loading entity server script" << scriptUrl << "for" << entityID;
|
||||
ScriptEngine::loadEntityScript(_entitiesScriptEngine, entityID, scriptUrl, reload);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void EntityScriptServer::nodeKilled(SharedNodePointer killedNode) {
|
||||
if (!_shuttingDown && killedNode->getType() == NodeType::EntityServer) {
|
||||
if (_entitiesScriptEngine) {
|
||||
_entitiesScriptEngine->unloadAllEntityScripts();
|
||||
_entitiesScriptEngine->stop();
|
||||
}
|
||||
|
||||
resetEntitiesScriptEngine();
|
||||
|
||||
_entityViewer.clear();
|
||||
}
|
||||
}
|
||||
|
||||
void EntityScriptServer::sendStatsPacket() {
|
||||
|
||||
}
|
||||
|
||||
void EntityScriptServer::handleOctreePacket(QSharedPointer<ReceivedMessage> message, SharedNodePointer senderNode) {
|
||||
auto packetType = message->getType();
|
||||
|
||||
if (packetType == PacketType::OctreeStats) {
|
||||
|
||||
int statsMessageLength = OctreeHeadlessViewer::parseOctreeStats(message, senderNode);
|
||||
if (message->getSize() > statsMessageLength) {
|
||||
// pull out the piggybacked packet and create a new QSharedPointer<NLPacket> for it
|
||||
int piggyBackedSizeWithHeader = message->getSize() - statsMessageLength;
|
||||
|
||||
auto buffer = std::unique_ptr<char[]>(new char[piggyBackedSizeWithHeader]);
|
||||
memcpy(buffer.get(), message->getRawMessage() + statsMessageLength, piggyBackedSizeWithHeader);
|
||||
|
||||
auto newPacket = NLPacket::fromReceivedPacket(std::move(buffer), piggyBackedSizeWithHeader, message->getSenderSockAddr());
|
||||
message = QSharedPointer<ReceivedMessage>::create(*newPacket);
|
||||
} else {
|
||||
return; // bail since no piggyback data
|
||||
}
|
||||
|
||||
packetType = message->getType();
|
||||
} // fall through to piggyback message
|
||||
|
||||
if (packetType == PacketType::EntityData) {
|
||||
_entityViewer.processDatagram(*message, senderNode);
|
||||
} else if (packetType == PacketType::EntityErase) {
|
||||
_entityViewer.processEraseMessage(*message, senderNode);
|
||||
}
|
||||
}
|
||||
|
||||
void EntityScriptServer::handleJurisdictionPacket(QSharedPointer<ReceivedMessage> message, SharedNodePointer senderNode) {
|
||||
NodeType_t nodeType;
|
||||
message->peekPrimitive(&nodeType);
|
||||
|
||||
// PacketType_JURISDICTION, first byte is the node type...
|
||||
if (nodeType == NodeType::EntityServer) {
|
||||
DependencyManager::get<EntityScriptingInterface>()->getJurisdictionListener()->
|
||||
queueReceivedPacket(message, senderNode);
|
||||
}
|
||||
}
|
||||
|
||||
void EntityScriptServer::aboutToFinish() {
|
||||
shutdownScriptEngine();
|
||||
|
||||
// our entity tree is going to go away so tell that to the EntityScriptingInterface
|
||||
DependencyManager::get<EntityScriptingInterface>()->setEntityTree(nullptr);
|
||||
|
||||
ResourceManager::cleanup();
|
||||
|
||||
// cleanup the AudioInjectorManager (and any still running injectors)
|
||||
DependencyManager::destroy<AudioInjectorManager>();
|
||||
DependencyManager::destroy<ScriptEngines>();
|
||||
|
||||
// cleanup codec & encoder
|
||||
if (_codec && _encoder) {
|
||||
_codec->releaseEncoder(_encoder);
|
||||
_encoder = nullptr;
|
||||
}
|
||||
}
|
70
assignment-client/src/scripts/EntityScriptServer.h
Normal file
|
@ -0,0 +1,70 @@
|
|||
//
|
||||
// EntityScriptServer.h
|
||||
// assignment-client/src/scripts
|
||||
//
|
||||
// Created by Clément Brisset on 1/5/17.
|
||||
// Copyright 2013 High Fidelity, Inc.
|
||||
//
|
||||
// Distributed under the Apache License, Version 2.0.
|
||||
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
|
||||
//
|
||||
|
||||
#ifndef hifi_EntityScriptServer_h
|
||||
#define hifi_EntityScriptServer_h
|
||||
|
||||
#include <QtCore/QObject>
|
||||
|
||||
#include <EntityEditPacketSender.h>
|
||||
#include <EntityTreeHeadlessViewer.h>
|
||||
#include <plugins/CodecPlugin.h>
|
||||
#include <ScriptEngine.h>
|
||||
#include <ThreadedAssignment.h>
|
||||
|
||||
class EntityScriptServer : public ThreadedAssignment {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
EntityScriptServer(ReceivedMessage& message);
|
||||
|
||||
virtual void aboutToFinish() override;
|
||||
|
||||
public slots:
|
||||
void run() override;
|
||||
void nodeActivated(SharedNodePointer activatedNode);
|
||||
void nodeKilled(SharedNodePointer killedNode);
|
||||
void sendStatsPacket() override;
|
||||
|
||||
private slots:
|
||||
void handleOctreePacket(QSharedPointer<ReceivedMessage> message, SharedNodePointer senderNode);
|
||||
void handleJurisdictionPacket(QSharedPointer<ReceivedMessage> message, SharedNodePointer senderNode);
|
||||
void handleSelectedAudioFormat(QSharedPointer<ReceivedMessage> message);
|
||||
|
||||
void handleReloadEntityServerScriptPacket(QSharedPointer<ReceivedMessage> message, SharedNodePointer senderNode);
|
||||
void handleEntityScriptGetStatusPacket(QSharedPointer<ReceivedMessage> message, SharedNodePointer senderNode);
|
||||
|
||||
private:
|
||||
void negotiateAudioFormat();
|
||||
void selectAudioFormat(const QString& selectedCodecName);
|
||||
|
||||
void resetEntitiesScriptEngine();
|
||||
void clear();
|
||||
void shutdownScriptEngine();
|
||||
|
||||
void addingEntity(const EntityItemID& entityID);
|
||||
void deletingEntity(const EntityItemID& entityID);
|
||||
void entityServerScriptChanging(const EntityItemID& entityID, const bool reload);
|
||||
void checkAndCallPreload(const EntityItemID& entityID, const bool reload = false);
|
||||
|
||||
bool _shuttingDown { false };
|
||||
|
||||
static int _entitiesScriptEngineCount;
|
||||
QSharedPointer<ScriptEngine> _entitiesScriptEngine;
|
||||
EntityEditPacketSender _entityEditSender;
|
||||
EntityTreeHeadlessViewer _entityViewer;
|
||||
|
||||
QString _selectedCodecName;
|
||||
CodecPluginPointer _codec;
|
||||
Encoder* _encoder { nullptr };
|
||||
};
|
||||
|
||||
#endif // hifi_EntityScriptServer_h
|
32
cmake/externals/LibOVRPlatform/CMakeLists.txt
vendored
Normal file
|
@ -0,0 +1,32 @@
|
|||
include(ExternalProject)
|
||||
include(SelectLibraryConfigurations)
|
||||
|
||||
set(EXTERNAL_NAME LibOVRPlatform)
|
||||
|
||||
string(TOUPPER ${EXTERNAL_NAME} EXTERNAL_NAME_UPPER)
|
||||
|
||||
if (WIN32)
|
||||
|
||||
ExternalProject_Add(
|
||||
${EXTERNAL_NAME}
|
||||
URL http://hifi-public.s3.amazonaws.com/dependencies/OVRPlatformSDK_v1.10.0.zip
|
||||
URL_MD5 e6c8264af16d904e6506acd5172fa0a9
|
||||
CONFIGURE_COMMAND ""
|
||||
BUILD_COMMAND ""
|
||||
INSTALL_COMMAND ""
|
||||
LOG_DOWNLOAD 1
|
||||
)
|
||||
|
||||
ExternalProject_Get_Property(${EXTERNAL_NAME} SOURCE_DIR)
|
||||
|
||||
if ("${CMAKE_SIZEOF_VOID_P}" EQUAL "8")
|
||||
set(${EXTERNAL_NAME_UPPER}_LIBRARY_RELEASE ${SOURCE_DIR}/Windows/LibOVRPlatform64_1.lib CACHE TYPE INTERNAL)
|
||||
else()
|
||||
set(${EXTERNAL_NAME_UPPER}_LIBRARY_RELEASE ${SOURCE_DIR}/Windows/LibOVRPlatform32_1.lib CACHE TYPE INTERNAL)
|
||||
endif()
|
||||
|
||||
set(${EXTERNAL_NAME_UPPER}_INCLUDE_DIRS ${SOURCE_DIR}/Include CACHE TYPE INTERNAL)
|
||||
endif ()
|
||||
|
||||
# Hide this external target (for ide users)
|
||||
set_target_properties(${EXTERNAL_NAME} PROPERTIES FOLDER "hidden/externals")
|
4
cmake/externals/quazip/CMakeLists.txt
vendored
|
@ -38,10 +38,10 @@ set(${EXTERNAL_NAME_UPPER}_DLL_PATH ${INSTALL_DIR}/lib CACHE FILEPATH "Location
|
|||
|
||||
if (APPLE)
|
||||
set(${EXTERNAL_NAME_UPPER}_LIBRARY_RELEASE ${INSTALL_DIR}/lib/libquazip5.1.0.0.dylib CACHE FILEPATH "Location of QuaZip release library")
|
||||
set(${EXTERNAL_NAME_UPPER}_LIBRARY_DEBUG ${INSTALL_DIR}/lib/libquazip5.1.0.0.dylib CACHE FILEPATH "Location of QuaZip release library")
|
||||
set(${EXTERNAL_NAME_UPPER}_LIBRARY_DEBUG ${INSTALL_DIR}/lib/libquazip5d.1.0.0.dylib CACHE FILEPATH "Location of QuaZip release library")
|
||||
elseif (WIN32)
|
||||
set(${EXTERNAL_NAME_UPPER}_LIBRARY_RELEASE ${INSTALL_DIR}/lib/quazip5.lib CACHE FILEPATH "Location of QuaZip release library")
|
||||
set(${EXTERNAL_NAME_UPPER}_LIBRARY_DEBUG ${INSTALL_DIR}/lib/quazip5.lib CACHE FILEPATH "Location of QuaZip release library")
|
||||
set(${EXTERNAL_NAME_UPPER}_LIBRARY_DEBUG ${INSTALL_DIR}/lib/quazip5d.lib CACHE FILEPATH "Location of QuaZip release library")
|
||||
else ()
|
||||
set(${EXTERNAL_NAME_UPPER}_LIBRARY_RELEASE ${INSTALL_DIR}/lib/libquazip5.so CACHE FILEPATH "Location of QuaZip release library")
|
||||
set(${EXTERNAL_NAME_UPPER}_LIBRARY_DEBUG ${INSTALL_DIR}/lib/libquazip5.so CACHE FILEPATH "Location of QuaZip release library")
|
||||
|
|
4
cmake/externals/wasapi/CMakeLists.txt
vendored
|
@ -6,8 +6,8 @@ if (WIN32)
|
|||
include(ExternalProject)
|
||||
ExternalProject_Add(
|
||||
${EXTERNAL_NAME}
|
||||
URL http://hifi-public.s3.amazonaws.com/dependencies/qtaudio_wasapi5.zip
|
||||
URL_MD5 0530753e855ffc00232cc969bf1c84a8
|
||||
URL http://hifi-public.s3.amazonaws.com/dependencies/qtaudio_wasapi7.zip
|
||||
URL_MD5 bc2861e50852dd590cdc773a14a041a7
|
||||
CONFIGURE_COMMAND ""
|
||||
BUILD_COMMAND ""
|
||||
INSTALL_COMMAND ""
|
||||
|
|
44
cmake/modules/FindLibOVRPlatform.cmake
Normal file
|
@ -0,0 +1,44 @@
|
|||
#
|
||||
# FindLibOVRPlatform.cmake
|
||||
#
|
||||
# Try to find the LibOVRPlatform library to use the Oculus Platform SDK
|
||||
#
|
||||
# You must provide a LIBOVRPLATFORM_ROOT_DIR which contains Windows and Include directories
|
||||
#
|
||||
# Once done this will define
|
||||
#
|
||||
# LIBOVRPLATFORM_FOUND - system found Oculus Platform SDK
|
||||
# LIBOVRPLATFORM_INCLUDE_DIRS - the Oculus Platform include directory
|
||||
# LIBOVRPLATFORM_LIBRARIES - Link this to use Oculus Platform
|
||||
#
|
||||
# Created on December 16, 2016 by Stephen Birarda
|
||||
# Copyright 2016 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
|
||||
#
|
||||
|
||||
|
||||
if (WIN32)
|
||||
# setup hints for LIBOVRPLATFORM search
|
||||
include("${MACRO_DIR}/HifiLibrarySearchHints.cmake")
|
||||
hifi_library_search_hints("LibOVRPlatform")
|
||||
|
||||
find_path(LIBOVRPLATFORM_INCLUDE_DIRS OVR_Platform.h PATH_SUFFIXES Include HINTS ${LIBOVRPLATFORM_SEARCH_DIRS})
|
||||
|
||||
if ("${CMAKE_SIZEOF_VOID_P}" EQUAL "8")
|
||||
set(_LIB_NAME LibOVRPlatform64_1.lib)
|
||||
else()
|
||||
set(_LIB_NAME LibOVRPlatform32_1.lib)
|
||||
endif()
|
||||
|
||||
find_library(LIBOVRPLATFORM_LIBRARY_RELEASE NAMES ${_LIB_NAME} PATH_SUFFIXES Windows HINTS ${LIBOVRPLATFORM_SEARCH_DIRS})
|
||||
|
||||
include(SelectLibraryConfigurations)
|
||||
select_library_configurations(LIBOVRPLATFORM)
|
||||
|
||||
include(FindPackageHandleStandardArgs)
|
||||
find_package_handle_standard_args(LIBOVRPLATFORM DEFAULT_MSG LIBOVRPLATFORM_INCLUDE_DIRS LIBOVRPLATFORM_LIBRARIES)
|
||||
|
||||
mark_as_advanced(LIBOVRPLATFORM_INCLUDE_DIRS LIBOVRPLATFORM_LIBRARIES LIBOVRPLATFORM_SEARCH_DIRS)
|
||||
endif ()
|
|
@ -40,7 +40,7 @@
|
|||
{
|
||||
"name": "local_port",
|
||||
"label": "Local UDP Port",
|
||||
"help": "This is the local port your domain-server binds to for UDP connections.<br/>Depending on your router, this may need to be changed to run multiple full automatic networking domain-servers in the same network.",
|
||||
"help": "This is the local port your domain-server binds to for UDP connections.<br/>Depending on your router, this may need to be changed to unique values for each domain-server in order to run multiple full automatic networking domain-servers in the same network. You can use the value 0 to have the domain-server select a random port, which will help in preventing port collisions.",
|
||||
"default": "40102",
|
||||
"type": "int",
|
||||
"advanced": true
|
||||
|
@ -372,6 +372,13 @@
|
|||
"help": "Password used for basic HTTP authentication. Leave this blank if you do not want to change it.",
|
||||
"value-hidden": true
|
||||
},
|
||||
{
|
||||
"name": "verify_http_password",
|
||||
"label": "Verify HTTP Password",
|
||||
"type": "password",
|
||||
"help": "Must match the password entered above for change to be saved.",
|
||||
"value-hidden": true
|
||||
},
|
||||
{
|
||||
"name": "maximum_user_capacity",
|
||||
"label": "Maximum User Capacity",
|
||||
|
@ -1089,9 +1096,9 @@
|
|||
{
|
||||
"name": "noise_muting_threshold",
|
||||
"label": "Noise Muting Threshold",
|
||||
"help": "Loudness value for noise background between 0 and 1.0 (0: mute everyone, 1.0: never mute)",
|
||||
"placeholder": "0.003",
|
||||
"default": "0.003",
|
||||
"help": "Loudness value for noise background between 0 and 1.0 (0: mute everyone, 1.0: never mute). 0.003 is a typical setting to mute loud people.",
|
||||
"placeholder": "1.0",
|
||||
"default": "1.0",
|
||||
"advanced": false
|
||||
},
|
||||
{
|
||||
|
@ -1285,11 +1292,19 @@
|
|||
{
|
||||
"name": "entityScriptSourceWhitelist",
|
||||
"label": "Entity Scripts Allowed from:",
|
||||
"help": "The domains that entity scripts are allowed from. A comma separated list of domains that entity scripts are allowed from, if someone attempts to create and entity or edit an entity to have a different domain, it will be rejected. If left blank, any domain is allowed.",
|
||||
"help": "Comma separated list of URLs (with optional paths) that entity scripts are allowed from. If someone attempts to create and entity or edit an entity to have a different domain, it will be rejected. If left blank, any domain is allowed.",
|
||||
"placeholder": "",
|
||||
"default": "",
|
||||
"advanced": true
|
||||
},
|
||||
{
|
||||
"name": "entityEditFilter",
|
||||
"label": "Filter Entity Edits",
|
||||
"help": "Check all entity edits against this filter function.",
|
||||
"placeholder": "url whose content is like: function filter(properties) { return properties; }",
|
||||
"default": "",
|
||||
"advanced": true
|
||||
},
|
||||
{
|
||||
"name": "persistFilePath",
|
||||
"label": "Entities File Path",
|
||||
|
|
|
@ -125,6 +125,10 @@ tr.new-row {
|
|||
background-color: #dff0d8;
|
||||
}
|
||||
|
||||
tr.invalid-input {
|
||||
background-color: #f2dede;
|
||||
}
|
||||
|
||||
.graphable-stat {
|
||||
text-align: center;
|
||||
color: #5286BC;
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
<div id="setup-sidebar" class="hidden-xs" data-spy="affix" data-offset-top="55" data-clampedwidth="#setup-sidebar-col">
|
||||
<script id="list-group-template" type="text/template">
|
||||
<% _.each(descriptions, function(group){ %>
|
||||
<% panelID = group.name ? group.name : group.label %>
|
||||
<% panelID = group.name ? group.name : group.html_id %>
|
||||
<li>
|
||||
<a href="#<%- panelID %>" class="list-group-item">
|
||||
<span class="badge"></span>
|
||||
|
|
|
@ -38,14 +38,15 @@ var Settings = {
|
|||
DOMAIN_ID_SELECTOR: '[name="metaverse.id"]',
|
||||
ACCESS_TOKEN_SELECTOR: '[name="metaverse.access_token"]',
|
||||
PLACES_TABLE_ID: 'places-table',
|
||||
FORM_ID: 'settings-form'
|
||||
FORM_ID: 'settings-form',
|
||||
INVALID_ROW_CLASS: 'invalid-input'
|
||||
};
|
||||
|
||||
var viewHelpers = {
|
||||
getFormGroup: function(keypath, setting, values, isAdvanced) {
|
||||
form_group = "<div class='form-group " +
|
||||
(isAdvanced ? Settings.ADVANCED_CLASS : "") + " " +
|
||||
(setting.deprecated ? Settings.DEPRECATED_CLASS : "" ) + "' " +
|
||||
(setting.deprecated ? Settings.DEPRECATED_CLASS : "" ) + "' " +
|
||||
"data-keypath='" + keypath + "'>";
|
||||
setting_value = _(values).valueForKeyPath(keypath);
|
||||
|
||||
|
@ -215,8 +216,8 @@ $(document).ready(function(){
|
|||
sibling = sibling.next();
|
||||
}
|
||||
|
||||
if (sibling.hasClass(Settings.ADD_DEL_BUTTONS_CLASS)) {
|
||||
sibling.find('.' + Settings.ADD_ROW_BUTTON_CLASS).click();
|
||||
// for tables with categories we add the entry and setup the new row on enter
|
||||
if (sibling.find("." + Settings.ADD_CATEGORY_BUTTON_CLASS).length) {
|
||||
sibling.find("." + Settings.ADD_CATEGORY_BUTTON_CLASS).click();
|
||||
|
||||
// set focus to the first input in the new row
|
||||
|
@ -891,39 +892,146 @@ function reloadSettings(callback) {
|
|||
});
|
||||
}
|
||||
|
||||
function validateInputs() {
|
||||
// check if any new values are bad
|
||||
var tables = $('table');
|
||||
|
||||
var inputsValid = true;
|
||||
|
||||
var tables = $('table');
|
||||
|
||||
// clear any current invalid rows
|
||||
$('tr.' + Settings.INVALID_ROW_CLASS).removeClass(Settings.INVALID_ROW_CLASS);
|
||||
|
||||
function markParentRowInvalid(rowChild) {
|
||||
$(rowChild).closest('tr').addClass(Settings.INVALID_ROW_CLASS);
|
||||
}
|
||||
|
||||
_.each(tables, function(table) {
|
||||
// validate keys specificially for spaces and equality to an existing key
|
||||
var newKeys = $(table).find('tr.' + Settings.NEW_ROW_CLASS + ' td.key');
|
||||
|
||||
var keyWithSpaces = false;
|
||||
var empty = false;
|
||||
var duplicateKey = false;
|
||||
|
||||
_.each(newKeys, function(keyCell) {
|
||||
var keyVal = $(keyCell).children('input').val();
|
||||
|
||||
if (keyVal.indexOf(' ') !== -1) {
|
||||
keyWithSpaces = true;
|
||||
markParentRowInvalid(keyCell);
|
||||
return;
|
||||
}
|
||||
|
||||
// make sure the key isn't empty
|
||||
if (keyVal.length === 0) {
|
||||
empty = true
|
||||
|
||||
markParentRowInvalid(input);
|
||||
return;
|
||||
}
|
||||
|
||||
// make sure we don't have duplicate keys in the table
|
||||
var otherKeys = $(table).find('td.key').not(keyCell);
|
||||
_.each(otherKeys, function(otherKeyCell) {
|
||||
var keyInput = $(otherKeyCell).children('input');
|
||||
|
||||
if (keyInput.length) {
|
||||
if ($(keyInput).val() == keyVal) {
|
||||
duplicateKey = true;
|
||||
}
|
||||
} else if ($(otherKeyCell).html() == keyVal) {
|
||||
duplicateKey = true;
|
||||
}
|
||||
|
||||
if (duplicateKey) {
|
||||
markParentRowInvalid(keyCell);
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
if (keyWithSpaces) {
|
||||
showErrorMessage("Error", "Key contains spaces");
|
||||
inputsValid = false;
|
||||
return
|
||||
}
|
||||
|
||||
if (empty) {
|
||||
showErrorMessage("Error", "Empty field(s)");
|
||||
inputsValid = false;
|
||||
return
|
||||
}
|
||||
|
||||
if (duplicateKey) {
|
||||
showErrorMessage("Error", "Two keys cannot be identical");
|
||||
inputsValid = false;
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
return inputsValid;
|
||||
}
|
||||
|
||||
var SETTINGS_ERROR_MESSAGE = "There was a problem saving domain settings. Please try again!";
|
||||
|
||||
function saveSettings() {
|
||||
// disable any inputs not changed
|
||||
$("input:not([data-changed])").each(function(){
|
||||
$(this).prop('disabled', true);
|
||||
});
|
||||
|
||||
// grab a JSON representation of the form via form2js
|
||||
var formJSON = form2js('settings-form', ".", false, cleanupFormValues, true);
|
||||
if (validateInputs()) {
|
||||
// POST the form JSON to the domain-server settings.json endpoint so the settings are saved
|
||||
|
||||
// check if we've set the basic http password - if so convert it to base64
|
||||
if (formJSON["security"]) {
|
||||
var password = formJSON["security"]["http_password"];
|
||||
if (password && password.length > 0) {
|
||||
formJSON["security"]["http_password"] = sha256_digest(password);
|
||||
// disable any inputs not changed
|
||||
$("input:not([data-changed])").each(function(){
|
||||
$(this).prop('disabled', true);
|
||||
});
|
||||
|
||||
// grab a JSON representation of the form via form2js
|
||||
var formJSON = form2js('settings-form', ".", false, cleanupFormValues, true);
|
||||
|
||||
// check if we've set the basic http password - if so convert it to base64
|
||||
if (formJSON["security"]) {
|
||||
var password = formJSON["security"]["http_password"];
|
||||
if (password && password.length > 0) {
|
||||
formJSON["security"]["http_password"] = sha256_digest(password);
|
||||
}
|
||||
}
|
||||
|
||||
// verify that the password and confirmation match before saving
|
||||
var canPost = true;
|
||||
|
||||
if (formJSON["security"]) {
|
||||
var password = formJSON["security"]["http_password"];
|
||||
var verify_password = formJSON["security"]["verify_http_password"];
|
||||
|
||||
if (password && password.length > 0) {
|
||||
if (password != verify_password) {
|
||||
bootbox.alert({"message": "Passwords must match!", "title":"Password Error"});
|
||||
canPost = false;
|
||||
} else {
|
||||
formJSON["security"]["http_password"] = sha256_digest(password);
|
||||
delete formJSON["security"]["verify_http_password"];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log("----- SAVING ------");
|
||||
console.log(formJSON);
|
||||
|
||||
// re-enable all inputs
|
||||
$("input").each(function(){
|
||||
$(this).prop('disabled', false);
|
||||
});
|
||||
|
||||
// remove focus from the button
|
||||
$(this).blur();
|
||||
|
||||
if (canPost) {
|
||||
// POST the form JSON to the domain-server settings.json endpoint so the settings are saved
|
||||
postSettings(formJSON);
|
||||
}
|
||||
}
|
||||
|
||||
console.log("----- SAVING ------");
|
||||
console.log(formJSON);
|
||||
|
||||
// re-enable all inputs
|
||||
$("input").each(function(){
|
||||
$(this).prop('disabled', false);
|
||||
});
|
||||
|
||||
// remove focus from the button
|
||||
$(this).blur();
|
||||
|
||||
// POST the form JSON to the domain-server settings.json endpoint so the settings are saved
|
||||
postSettings(formJSON);
|
||||
}
|
||||
|
||||
$('body').on('click', '.save-button', function(e){
|
||||
|
@ -1100,8 +1208,9 @@ function makeTable(setting, keypath, setting_value) {
|
|||
if (setting.can_add_new_categories) {
|
||||
html += makeTableCategoryInput(setting, numVisibleColumns);
|
||||
}
|
||||
|
||||
if (setting.can_add_new_rows || setting.can_add_new_categories) {
|
||||
html += makeTableInputs(setting, {}, "");
|
||||
html += makeTableHiddenInputs(setting, {}, "");
|
||||
}
|
||||
}
|
||||
html += "</table>"
|
||||
|
@ -1127,7 +1236,7 @@ function makeTableCategoryHeader(categoryKey, categoryValue, numVisibleColumns,
|
|||
return html;
|
||||
}
|
||||
|
||||
function makeTableInputs(setting, initialValues, categoryValue) {
|
||||
function makeTableHiddenInputs(setting, initialValues, categoryValue) {
|
||||
var html = "<tr class='inputs'" + (setting.can_add_new_categories && !categoryValue ? " hidden" : "") + " " +
|
||||
(categoryValue ? ("data-category='" + categoryValue + "'") : "") + " " +
|
||||
(setting.categorize_by_key ? ("data-keep-field='" + setting.categorize_by_key + "'") : "") + ">";
|
||||
|
@ -1138,7 +1247,7 @@ function makeTableInputs(setting, initialValues, categoryValue) {
|
|||
|
||||
if (setting.key) {
|
||||
html += "<td class='key' name='" + setting.key.name + "'>\
|
||||
<input type='text' class='form-control' placeholder='" + (_.has(setting.key, 'placeholder') ? setting.key.placeholder : "") + "' value=''>\
|
||||
<input type='text' style='display: none;' class='form-control' placeholder='" + (_.has(setting.key, 'placeholder') ? setting.key.placeholder : "") + "' value=''>\
|
||||
</td>"
|
||||
}
|
||||
|
||||
|
@ -1147,14 +1256,14 @@ function makeTableInputs(setting, initialValues, categoryValue) {
|
|||
if (col.type === "checkbox") {
|
||||
html +=
|
||||
"<td class='" + Settings.DATA_COL_CLASS + "'name='" + col.name + "'>" +
|
||||
"<input type='checkbox' class='form-control table-checkbox' " +
|
||||
"<input type='checkbox' style='display: none;' class='form-control table-checkbox' " +
|
||||
"name='" + col.name + "'" + (defaultValue ? " checked" : "") + "/>" +
|
||||
"</td>";
|
||||
} else {
|
||||
html +=
|
||||
"<td " + (col.hidden ? "style='display: none;'" : "") + " class='" + Settings.DATA_COL_CLASS + "' " +
|
||||
"name='" + col.name + "'>" +
|
||||
"<input type='text' class='form-control' placeholder='" + (col.placeholder ? col.placeholder : "") + "' " +
|
||||
"<input type='text' style='display: none;' class='form-control' placeholder='" + (col.placeholder ? col.placeholder : "") + "' " +
|
||||
"value='" + (defaultValue || "") + "' data-default='" + (defaultValue || "") + "'" +
|
||||
(col.readonly ? " readonly" : "") + ">" +
|
||||
"</td>";
|
||||
|
@ -1234,49 +1343,17 @@ function addTableRow(row) {
|
|||
|
||||
var columns = row.parent().children('.' + Settings.DATA_ROW_CLASS);
|
||||
|
||||
var input_clone = row.clone();
|
||||
|
||||
if (!isArray) {
|
||||
// Check key spaces
|
||||
var key = row.children(".key").children("input").val()
|
||||
if (key.indexOf(' ') !== -1) {
|
||||
showErrorMessage("Error", "Key contains spaces")
|
||||
return
|
||||
}
|
||||
// Check keys with the same name
|
||||
var equals = false;
|
||||
_.each(columns.children(".key"), function(element) {
|
||||
if ($(element).text() === key) {
|
||||
equals = true
|
||||
return
|
||||
}
|
||||
})
|
||||
if (equals) {
|
||||
showErrorMessage("Error", "Two keys cannot be identical")
|
||||
return
|
||||
}
|
||||
// show the key input
|
||||
var keyInput = row.children(".key").children("input");
|
||||
}
|
||||
|
||||
// Check empty fields
|
||||
var empty = false;
|
||||
_.each(row.children('.' + Settings.DATA_COL_CLASS + ' input'), function(element) {
|
||||
if ($(element).val().length === 0) {
|
||||
empty = true
|
||||
return
|
||||
}
|
||||
})
|
||||
|
||||
if (empty) {
|
||||
showErrorMessage("Error", "Empty field(s)")
|
||||
return
|
||||
}
|
||||
|
||||
var input_clone = row.clone()
|
||||
|
||||
// Change input row to data row
|
||||
var table = row.parents("table")
|
||||
var setting_name = table.attr("name")
|
||||
var full_name = setting_name + "." + key
|
||||
row.addClass(Settings.DATA_ROW_CLASS + " " + Settings.NEW_ROW_CLASS)
|
||||
row.removeClass("inputs")
|
||||
var table = row.parents("table");
|
||||
var setting_name = table.attr("name");
|
||||
row.addClass(Settings.DATA_ROW_CLASS + " " + Settings.NEW_ROW_CLASS);
|
||||
|
||||
_.each(row.children(), function(element) {
|
||||
if ($(element).hasClass("numbered")) {
|
||||
|
@ -1298,56 +1375,43 @@ function addTableRow(row) {
|
|||
anchor.addClass(Settings.DEL_ROW_SPAN_CLASSES)
|
||||
} else if ($(element).hasClass("key")) {
|
||||
var input = $(element).children("input")
|
||||
$(element).html(input.val())
|
||||
input.remove()
|
||||
input.show();
|
||||
} else if ($(element).hasClass(Settings.DATA_COL_CLASS)) {
|
||||
// Hide inputs
|
||||
var input = $(element).find("input")
|
||||
var isCheckbox = false;
|
||||
var isTime = false;
|
||||
if (input.hasClass("table-checkbox")) {
|
||||
input = $(input).parent();
|
||||
isCheckbox = true;
|
||||
} else if (input.hasClass("table-time")) {
|
||||
input = $(input).parent();
|
||||
isTime = true;
|
||||
}
|
||||
// show inputs
|
||||
var input = $(element).find("input");
|
||||
input.show();
|
||||
|
||||
var val = input.val();
|
||||
if (isCheckbox) {
|
||||
// don't hide the checkbox
|
||||
val = $(input).find("input").is(':checked');
|
||||
} else if (isTime) {
|
||||
// don't hide the time
|
||||
} else {
|
||||
input.attr("type", "hidden")
|
||||
}
|
||||
var isCheckbox = input.hasClass("table-checkbox");
|
||||
|
||||
if (isArray) {
|
||||
var row_index = row.siblings('.' + Settings.DATA_ROW_CLASS).length
|
||||
var key = $(element).attr('name')
|
||||
var key = $(element).attr('name');
|
||||
|
||||
// are there multiple columns or just one?
|
||||
// with multiple we have an array of Objects, with one we have an array of whatever the value type is
|
||||
var num_columns = row.children('.' + Settings.DATA_COL_CLASS).length
|
||||
|
||||
if (isCheckbox) {
|
||||
$(input).find("input").attr("name", setting_name + "[" + row_index + "]" + (num_columns > 1 ? "." + key : ""))
|
||||
input.attr("name", setting_name + "[" + row_index + "]" + (num_columns > 1 ? "." + key : ""))
|
||||
} else {
|
||||
input.attr("name", setting_name + "[" + row_index + "]" + (num_columns > 1 ? "." + key : ""))
|
||||
}
|
||||
} else {
|
||||
input.attr("name", full_name + "." + $(element).attr("name"))
|
||||
// because the name of the setting in question requires the key
|
||||
// setup a hook to change the HTML name of the element whenever the key changes
|
||||
var colName = $(element).attr("name");
|
||||
keyInput.on('change', function(){
|
||||
input.attr("name", setting_name + "." + $(this).val() + "." + colName);
|
||||
});
|
||||
}
|
||||
|
||||
if (isCheckbox) {
|
||||
$(input).find("input").attr("data-changed", "true");
|
||||
} else {
|
||||
input.attr("data-changed", "true");
|
||||
$(element).append(val);
|
||||
}
|
||||
} else {
|
||||
console.log("Unknown table element")
|
||||
console.log("Unknown table element");
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -1377,7 +1441,12 @@ function deleteTableRow($row) {
|
|||
$row.empty();
|
||||
|
||||
if (!isArray) {
|
||||
$row.html("<input type='hidden' class='form-control' name='" + $row.attr('name') + "' data-changed='true' value=''>");
|
||||
if ($row.attr('name')) {
|
||||
$row.html("<input type='hidden' class='form-control' name='" + $row.attr('name') + "' data-changed='true' value=''>");
|
||||
} else {
|
||||
// for rows that didn't have a key, simply remove the row
|
||||
$row.remove();
|
||||
}
|
||||
} else {
|
||||
if ($table.find('.' + Settings.DATA_ROW_CLASS + "[data-category='" + categoryName + "']").length <= 1) {
|
||||
// This is the last row of the category, so delete the header
|
||||
|
|
|
@ -46,10 +46,9 @@ QUuid DomainGatekeeper::assignmentUUIDForPendingAssignment(const QUuid& tempUUID
|
|||
}
|
||||
}
|
||||
|
||||
const NodeSet STATICALLY_ASSIGNED_NODES = NodeSet() << NodeType::AudioMixer
|
||||
<< NodeType::AvatarMixer << NodeType::EntityServer
|
||||
<< NodeType::AssetServer
|
||||
<< NodeType::MessagesMixer;
|
||||
const NodeSet STATICALLY_ASSIGNED_NODES = NodeSet() << NodeType::AudioMixer << NodeType::AvatarMixer
|
||||
<< NodeType::EntityServer << NodeType::AssetServer << NodeType::MessagesMixer
|
||||
<< NodeType::EntityScriptServer;
|
||||
|
||||
void DomainGatekeeper::processConnectRequestPacket(QSharedPointer<ReceivedMessage> message) {
|
||||
if (message->getSize() == 0) {
|
||||
|
@ -72,7 +71,7 @@ void DomainGatekeeper::processConnectRequestPacket(QSharedPointer<ReceivedMessag
|
|||
}
|
||||
|
||||
static const NodeSet VALID_NODE_TYPES {
|
||||
NodeType::AudioMixer, NodeType::AvatarMixer, NodeType::AssetServer, NodeType::EntityServer, NodeType::Agent, NodeType::MessagesMixer
|
||||
NodeType::AudioMixer, NodeType::AvatarMixer, NodeType::AssetServer, NodeType::EntityServer, NodeType::Agent, NodeType::MessagesMixer, NodeType::EntityScriptServer
|
||||
};
|
||||
|
||||
if (!VALID_NODE_TYPES.contains(nodeConnection.nodeType)) {
|
||||
|
@ -107,7 +106,7 @@ void DomainGatekeeper::processConnectRequestPacket(QSharedPointer<ReceivedMessag
|
|||
|
||||
if (node) {
|
||||
// set the sending sock addr and node interest set on this node
|
||||
DomainServerNodeData* nodeData = reinterpret_cast<DomainServerNodeData*>(node->getLinkedData());
|
||||
DomainServerNodeData* nodeData = static_cast<DomainServerNodeData*>(node->getLinkedData());
|
||||
nodeData->setSendingSockAddr(message->getSenderSockAddr());
|
||||
|
||||
// guard against patched agents asking to hear about other agents
|
||||
|
@ -128,12 +127,12 @@ void DomainGatekeeper::processConnectRequestPacket(QSharedPointer<ReceivedMessag
|
|||
emit connectedNode(node);
|
||||
} else {
|
||||
qDebug() << "Refusing connection from node at" << message->getSenderSockAddr()
|
||||
<< "with hardware address" << nodeConnection.hardwareAddress
|
||||
<< "with hardware address" << nodeConnection.hardwareAddress
|
||||
<< "and machine fingerprint" << nodeConnection.machineFingerprint;
|
||||
}
|
||||
}
|
||||
|
||||
NodePermissions DomainGatekeeper::setPermissionsForUser(bool isLocalUser, QString verifiedUsername, const QHostAddress& senderAddress,
|
||||
NodePermissions DomainGatekeeper::setPermissionsForUser(bool isLocalUser, QString verifiedUsername, const QHostAddress& senderAddress,
|
||||
const QString& hardwareAddress, const QUuid& machineFingerprint) {
|
||||
NodePermissions userPerms;
|
||||
|
||||
|
@ -283,7 +282,7 @@ void DomainGatekeeper::updateNodePermissions() {
|
|||
QString hardwareAddress;
|
||||
QUuid machineFingerprint;
|
||||
|
||||
DomainServerNodeData* nodeData = reinterpret_cast<DomainServerNodeData*>(node->getLinkedData());
|
||||
DomainServerNodeData* nodeData = static_cast<DomainServerNodeData*>(node->getLinkedData());
|
||||
if (nodeData) {
|
||||
hardwareAddress = nodeData->getHardwareAddress();
|
||||
machineFingerprint = nodeData->getMachineFingerprint();
|
||||
|
@ -336,7 +335,7 @@ SharedNodePointer DomainGatekeeper::processAssignmentConnectRequest(const NodeCo
|
|||
// add the new node
|
||||
SharedNodePointer newNode = addVerifiedNodeFromConnectRequest(nodeConnection);
|
||||
|
||||
DomainServerNodeData* nodeData = reinterpret_cast<DomainServerNodeData*>(newNode->getLinkedData());
|
||||
DomainServerNodeData* nodeData = static_cast<DomainServerNodeData*>(newNode->getLinkedData());
|
||||
|
||||
// set assignment related data on the linked data for this node
|
||||
nodeData->setAssignmentUUID(matchingQueuedAssignment->getUUID());
|
||||
|
@ -458,7 +457,7 @@ SharedNodePointer DomainGatekeeper::processAgentConnectRequest(const NodeConnect
|
|||
newNode->setPermissions(userPerms);
|
||||
|
||||
// grab the linked data for our new node so we can set the username
|
||||
DomainServerNodeData* nodeData = reinterpret_cast<DomainServerNodeData*>(newNode->getLinkedData());
|
||||
DomainServerNodeData* nodeData = static_cast<DomainServerNodeData*>(newNode->getLinkedData());
|
||||
|
||||
// if we have a username from the connect request, set it on the DomainServerNodeData
|
||||
nodeData->setUsername(username);
|
||||
|
|
|
@ -107,7 +107,7 @@ DomainServer::DomainServer(int argc, char* argv[]) :
|
|||
|
||||
qRegisterMetaType<DomainServerWebSessionData>("DomainServerWebSessionData");
|
||||
qRegisterMetaTypeStreamOperators<DomainServerWebSessionData>("DomainServerWebSessionData");
|
||||
|
||||
|
||||
// make sure we hear about newly connected nodes from our gatekeeper
|
||||
connect(&_gatekeeper, &DomainGatekeeper::connectedNode, this, &DomainServer::handleConnectedNode);
|
||||
|
||||
|
@ -281,7 +281,7 @@ bool DomainServer::optionallyReadX509KeyAndCertificate() {
|
|||
QString keyPassphraseString = QProcessEnvironment::systemEnvironment().value(X509_KEY_PASSPHRASE_ENV);
|
||||
|
||||
qDebug() << "Reading certificate file at" << certPath << "for HTTPS.";
|
||||
qDebug() << "Reading key file at" << keyPath << "for HTTPS.";
|
||||
qDebug() << "Reading key file at" << keyPath << "for HTTPS.";
|
||||
|
||||
QFile certFile(certPath);
|
||||
certFile.open(QIODevice::ReadOnly);
|
||||
|
@ -528,12 +528,12 @@ void DomainServer::setupNodeListAndAssignments() {
|
|||
packetReceiver.registerListener(PacketType::DomainServerPathQuery, this, "processPathQueryPacket");
|
||||
packetReceiver.registerListener(PacketType::NodeJsonStats, this, "processNodeJSONStatsPacket");
|
||||
packetReceiver.registerListener(PacketType::DomainDisconnectRequest, this, "processNodeDisconnectRequestPacket");
|
||||
|
||||
|
||||
// NodeList won't be available to the settings manager when it is created, so call registerListener here
|
||||
packetReceiver.registerListener(PacketType::DomainSettingsRequest, &_settingsManager, "processSettingsRequestPacket");
|
||||
packetReceiver.registerListener(PacketType::NodeKickRequest, &_settingsManager, "processNodeKickRequestPacket");
|
||||
packetReceiver.registerListener(PacketType::UsernameFromIDRequest, &_settingsManager, "processUsernameFromIDRequestPacket");
|
||||
|
||||
|
||||
// register the gatekeeper for the packets it needs to receive
|
||||
packetReceiver.registerListener(PacketType::DomainConnectRequest, &_gatekeeper, "processConnectRequestPacket");
|
||||
packetReceiver.registerListener(PacketType::ICEPing, &_gatekeeper, "processICEPingPacket");
|
||||
|
@ -542,7 +542,7 @@ void DomainServer::setupNodeListAndAssignments() {
|
|||
|
||||
packetReceiver.registerListener(PacketType::ICEServerHeartbeatDenied, this, "processICEServerHeartbeatDenialPacket");
|
||||
packetReceiver.registerListener(PacketType::ICEServerHeartbeatACK, this, "processICEServerHeartbeatACK");
|
||||
|
||||
|
||||
// add whatever static assignments that have been parsed to the queue
|
||||
addStaticAssignmentsToQueue();
|
||||
|
||||
|
@ -808,21 +808,19 @@ void DomainServer::populateDefaultStaticAssignmentsExcludingTypes(const QSet<Ass
|
|||
for (Assignment::Type defaultedType = Assignment::AudioMixerType;
|
||||
defaultedType != Assignment::AllTypes;
|
||||
defaultedType = static_cast<Assignment::Type>(static_cast<int>(defaultedType) + 1)) {
|
||||
if (!excludedTypes.contains(defaultedType)
|
||||
&& defaultedType != Assignment::UNUSED_1
|
||||
&& defaultedType != Assignment::AgentType) {
|
||||
|
||||
if (!excludedTypes.contains(defaultedType) && defaultedType != Assignment::AgentType) {
|
||||
|
||||
if (defaultedType == Assignment::AssetServerType) {
|
||||
// Make sure the asset-server is enabled before adding it here.
|
||||
// Initially we do not assign it by default so we can test it in HF domains first
|
||||
static const QString ASSET_SERVER_ENABLED_KEYPATH = "asset_server.enabled";
|
||||
|
||||
|
||||
if (!_settingsManager.valueOrDefaultValueForKeyPath(ASSET_SERVER_ENABLED_KEYPATH).toBool()) {
|
||||
// skip to the next iteration if asset-server isn't enabled
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// type has not been set from a command line or config file config, use the default
|
||||
// by clearing whatever exists and writing a single default assignment with no payload
|
||||
Assignment* newAssignment = new Assignment(Assignment::CreateCommand, (Assignment::Type) defaultedType);
|
||||
|
@ -839,9 +837,9 @@ void DomainServer::processListRequestPacket(QSharedPointer<ReceivedMessage> mess
|
|||
// update this node's sockets in case they have changed
|
||||
sendingNode->setPublicSocket(nodeRequestData.publicSockAddr);
|
||||
sendingNode->setLocalSocket(nodeRequestData.localSockAddr);
|
||||
|
||||
|
||||
// update the NodeInterestSet in case there have been any changes
|
||||
DomainServerNodeData* nodeData = reinterpret_cast<DomainServerNodeData*>(sendingNode->getLinkedData());
|
||||
DomainServerNodeData* nodeData = static_cast<DomainServerNodeData*>(sendingNode->getLinkedData());
|
||||
|
||||
// guard against patched agents asking to hear about other agents
|
||||
auto safeInterestSet = nodeRequestData.interestList.toSet();
|
||||
|
@ -857,6 +855,44 @@ void DomainServer::processListRequestPacket(QSharedPointer<ReceivedMessage> mess
|
|||
sendDomainListToNode(sendingNode, message->getSenderSockAddr());
|
||||
}
|
||||
|
||||
bool DomainServer::isInInterestSet(const SharedNodePointer& nodeA, const SharedNodePointer& nodeB) {
|
||||
auto nodeAData = static_cast<DomainServerNodeData*>(nodeA->getLinkedData());
|
||||
auto nodeBData = static_cast<DomainServerNodeData*>(nodeB->getLinkedData());
|
||||
|
||||
// if we have no linked data for node A then B can't possibly be in the interest set
|
||||
if (!nodeAData) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// first check if the general interest set A contains the type for B
|
||||
if (nodeAData->getNodeInterestSet().contains(nodeB->getType())) {
|
||||
// given that there is a match in the general interest set, do any special checks
|
||||
|
||||
// (1/19/17) Agents only need to connect to Entity Script Servers to perform administrative tasks
|
||||
// related to entity server scripts. Only agents with rez permissions should be doing that, so
|
||||
// if the agent does not have those permissions, we do not want them and the server to incur the
|
||||
// overhead of connecting to one another. Additionally we exclude agents that do not care about the
|
||||
// Entity Script Server and won't attempt to connect to it.
|
||||
|
||||
bool isAgentWithoutRights = nodeA->getType() == NodeType::Agent
|
||||
&& nodeB->getType() == NodeType::EntityScriptServer
|
||||
&& !nodeA->getCanRez() && !nodeA->getCanRezTmp();
|
||||
|
||||
if (isAgentWithoutRights) {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool isScriptServerForIneffectiveAgent =
|
||||
(nodeA->getType() == NodeType::EntityScriptServer && nodeB->getType() == NodeType::Agent)
|
||||
&& ((nodeBData && !nodeBData->getNodeInterestSet().contains(NodeType::EntityScriptServer))
|
||||
|| (!nodeB->getCanRez() && !nodeB->getCanRezTmp()));
|
||||
|
||||
return !isScriptServerForIneffectiveAgent;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
unsigned int DomainServer::countConnectedUsers() {
|
||||
unsigned int result = 0;
|
||||
auto nodeList = DependencyManager::get<LimitedNodeList>();
|
||||
|
@ -928,14 +964,14 @@ void DomainServer::handleConnectedNode(SharedNodePointer newNode) {
|
|||
|
||||
void DomainServer::sendDomainListToNode(const SharedNodePointer& node, const HifiSockAddr &senderSockAddr) {
|
||||
const int NUM_DOMAIN_LIST_EXTENDED_HEADER_BYTES = NUM_BYTES_RFC4122_UUID + NUM_BYTES_RFC4122_UUID + 2;
|
||||
|
||||
|
||||
// setup the extended header for the domain list packets
|
||||
// this data is at the beginning of each of the domain list packets
|
||||
QByteArray extendedHeader(NUM_DOMAIN_LIST_EXTENDED_HEADER_BYTES, 0);
|
||||
QDataStream extendedHeaderStream(&extendedHeader, QIODevice::WriteOnly);
|
||||
|
||||
|
||||
auto limitedNodeList = DependencyManager::get<LimitedNodeList>();
|
||||
|
||||
|
||||
extendedHeaderStream << limitedNodeList->getSessionUUID();
|
||||
extendedHeaderStream << node->getUUID();
|
||||
extendedHeaderStream << node->getPermissions();
|
||||
|
@ -945,7 +981,7 @@ void DomainServer::sendDomainListToNode(const SharedNodePointer& node, const Hif
|
|||
// always send the node their own UUID back
|
||||
QDataStream domainListStream(domainListPackets.get());
|
||||
|
||||
DomainServerNodeData* nodeData = reinterpret_cast<DomainServerNodeData*>(node->getLinkedData());
|
||||
DomainServerNodeData* nodeData = static_cast<DomainServerNodeData*>(node->getLinkedData());
|
||||
|
||||
// store the nodeInterestSet on this DomainServerNodeData, in case it has changed
|
||||
auto& nodeInterestSet = nodeData->getNodeInterestSet();
|
||||
|
@ -955,10 +991,8 @@ void DomainServer::sendDomainListToNode(const SharedNodePointer& node, const Hif
|
|||
// DTLSServerSession* dtlsSession = _isUsingDTLS ? _dtlsSessions[senderSockAddr] : NULL;
|
||||
if (nodeData->isAuthenticated()) {
|
||||
// if this authenticated node has any interest types, send back those nodes as well
|
||||
limitedNodeList->eachNode([&](const SharedNodePointer& otherNode){
|
||||
if (otherNode->getUUID() != node->getUUID()
|
||||
&& nodeInterestSet.contains(otherNode->getType())) {
|
||||
|
||||
limitedNodeList->eachNode([&](const SharedNodePointer& otherNode) {
|
||||
if (otherNode->getUUID() != node->getUUID() && isInInterestSet(node, otherNode)) {
|
||||
// since we're about to add a node to the packet we start a segment
|
||||
domainListPackets->startSegment();
|
||||
|
||||
|
@ -974,7 +1008,7 @@ void DomainServer::sendDomainListToNode(const SharedNodePointer& node, const Hif
|
|||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// send an empty list to the node, in case there were no other nodes
|
||||
domainListPackets->closeCurrentPacket(true);
|
||||
|
||||
|
@ -983,8 +1017,8 @@ void DomainServer::sendDomainListToNode(const SharedNodePointer& node, const Hif
|
|||
}
|
||||
|
||||
QUuid DomainServer::connectionSecretForNodes(const SharedNodePointer& nodeA, const SharedNodePointer& nodeB) {
|
||||
DomainServerNodeData* nodeAData = dynamic_cast<DomainServerNodeData*>(nodeA->getLinkedData());
|
||||
DomainServerNodeData* nodeBData = dynamic_cast<DomainServerNodeData*>(nodeB->getLinkedData());
|
||||
DomainServerNodeData* nodeAData = static_cast<DomainServerNodeData*>(nodeA->getLinkedData());
|
||||
DomainServerNodeData* nodeBData = static_cast<DomainServerNodeData*>(nodeB->getLinkedData());
|
||||
|
||||
if (nodeAData && nodeBData) {
|
||||
QUuid& secretUUID = nodeAData->getSessionSecretHash()[nodeB->getUUID()];
|
||||
|
@ -994,7 +1028,7 @@ QUuid DomainServer::connectionSecretForNodes(const SharedNodePointer& nodeA, con
|
|||
secretUUID = QUuid::createUuid();
|
||||
|
||||
// set it on the other Node's sessionSecretHash
|
||||
reinterpret_cast<DomainServerNodeData*>(nodeBData)->getSessionSecretHash().insert(nodeA->getUUID(), secretUUID);
|
||||
static_cast<DomainServerNodeData*>(nodeBData)->getSessionSecretHash().insert(nodeA->getUUID(), secretUUID);
|
||||
}
|
||||
|
||||
return secretUUID;
|
||||
|
@ -1020,8 +1054,7 @@ void DomainServer::broadcastNewNode(const SharedNodePointer& addedNode) {
|
|||
[&](const SharedNodePointer& node)->bool {
|
||||
if (node->getLinkedData() && node->getActiveSocket() && node != addedNode) {
|
||||
// is the added Node in this node's interest list?
|
||||
DomainServerNodeData* nodeData = dynamic_cast<DomainServerNodeData*>(node->getLinkedData());
|
||||
return nodeData->getNodeInterestSet().contains(addedNode->getType());
|
||||
return isInInterestSet(node, addedNode);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
|
@ -1124,7 +1157,7 @@ void DomainServer::processRequestAssignmentPacket(QSharedPointer<ReceivedMessage
|
|||
void DomainServer::setupPendingAssignmentCredits() {
|
||||
// enumerate the NodeList to find the assigned nodes
|
||||
DependencyManager::get<LimitedNodeList>()->eachNode([&](const SharedNodePointer& node){
|
||||
DomainServerNodeData* nodeData = reinterpret_cast<DomainServerNodeData*>(node->getLinkedData());
|
||||
DomainServerNodeData* nodeData = static_cast<DomainServerNodeData*>(node->getLinkedData());
|
||||
|
||||
if (!nodeData->getAssignmentUUID().isNull() && !nodeData->getWalletUUID().isNull()) {
|
||||
// check if we have a non-finalized transaction for this node to add this amount to
|
||||
|
@ -1510,7 +1543,7 @@ void DomainServer::sendHeartbeatToIceServer() {
|
|||
}
|
||||
|
||||
void DomainServer::processNodeJSONStatsPacket(QSharedPointer<ReceivedMessage> packetList, SharedNodePointer sendingNode) {
|
||||
auto nodeData = dynamic_cast<DomainServerNodeData*>(sendingNode->getLinkedData());
|
||||
auto nodeData = static_cast<DomainServerNodeData*>(sendingNode->getLinkedData());
|
||||
if (nodeData) {
|
||||
nodeData->updateJSONStats(packetList->getMessage());
|
||||
}
|
||||
|
@ -1556,7 +1589,7 @@ QJsonObject DomainServer::jsonObjectForNode(const SharedNodePointer& node) {
|
|||
nodeJson[JSON_KEY_UPTIME] = QString::number(double(QDateTime::currentMSecsSinceEpoch() - node->getWakeTimestamp()) / 1000.0);
|
||||
|
||||
// if the node has pool information, add it
|
||||
DomainServerNodeData* nodeData = reinterpret_cast<DomainServerNodeData*>(node->getLinkedData());
|
||||
DomainServerNodeData* nodeData = static_cast<DomainServerNodeData*>(node->getLinkedData());
|
||||
|
||||
// add the node username, if it exists
|
||||
nodeJson[JSON_KEY_USERNAME] = nodeData->getUsername();
|
||||
|
@ -1624,23 +1657,23 @@ bool DomainServer::handleHTTPRequest(HTTPConnection* connection, const QUrl& url
|
|||
if (connection->requestOperation() == QNetworkAccessManager::GetOperation
|
||||
&& assignmentRegex.indexIn(url.path()) != -1) {
|
||||
QUuid nodeUUID = QUuid(assignmentRegex.cap(1));
|
||||
|
||||
|
||||
auto matchingNode = nodeList->nodeWithUUID(nodeUUID);
|
||||
|
||||
|
||||
// don't handle if we don't have a matching node
|
||||
if (!matchingNode) {
|
||||
return false;
|
||||
}
|
||||
|
||||
auto nodeData = dynamic_cast<DomainServerNodeData*>(matchingNode->getLinkedData());
|
||||
|
||||
|
||||
auto nodeData = static_cast<DomainServerNodeData*>(matchingNode->getLinkedData());
|
||||
|
||||
// don't handle if we don't have node data for this node
|
||||
if (!nodeData) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
SharedAssignmentPointer matchingAssignment = _allAssignments.value(nodeData->getAssignmentUUID());
|
||||
|
||||
|
||||
// check if we have an assignment that matches this temp UUID, and it is a scripted assignment
|
||||
if (matchingAssignment && matchingAssignment->getType() == Assignment::AgentType) {
|
||||
// we have a matching assignment and it is for the right type, have the HTTP manager handle it
|
||||
|
@ -1655,7 +1688,7 @@ bool DomainServer::handleHTTPRequest(HTTPConnection* connection, const QUrl& url
|
|||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
// request not handled
|
||||
return false;
|
||||
}
|
||||
|
@ -1687,7 +1720,7 @@ bool DomainServer::handleHTTPRequest(HTTPConnection* connection, const QUrl& url
|
|||
|
||||
// enumerate the NodeList to find the assigned nodes
|
||||
nodeList->eachNode([this, &assignedNodesJSON](const SharedNodePointer& node){
|
||||
DomainServerNodeData* nodeData = reinterpret_cast<DomainServerNodeData*>(node->getLinkedData());
|
||||
DomainServerNodeData* nodeData = static_cast<DomainServerNodeData*>(node->getLinkedData());
|
||||
|
||||
if (!nodeData->getAssignmentUUID().isNull()) {
|
||||
// add the node using the UUID as the key
|
||||
|
@ -1775,7 +1808,7 @@ bool DomainServer::handleHTTPRequest(HTTPConnection* connection, const QUrl& url
|
|||
if (matchingNode) {
|
||||
// create a QJsonDocument with the stats QJsonObject
|
||||
QJsonObject statsObject =
|
||||
reinterpret_cast<DomainServerNodeData*>(matchingNode->getLinkedData())->getStatsJSONObject();
|
||||
static_cast<DomainServerNodeData*>(matchingNode->getLinkedData())->getStatsJSONObject();
|
||||
|
||||
// add the node type to the JSON data for output purposes
|
||||
statsObject["node_type"] = NodeType::getNodeTypeName(matchingNode->getType()).toLower().replace(' ', '-');
|
||||
|
@ -2247,7 +2280,7 @@ void DomainServer::addStaticAssignmentsToQueue() {
|
|||
// if the domain-server has just restarted,
|
||||
// check if there are static assignments that we need to throw into the assignment queue
|
||||
auto sharedAssignments = _allAssignments.values();
|
||||
|
||||
|
||||
// sort the assignments to put the server/mixer assignments first
|
||||
qSort(sharedAssignments.begin(), sharedAssignments.end(), [](SharedAssignmentPointer a, SharedAssignmentPointer b){
|
||||
if (a->getType() == b->getType()) {
|
||||
|
@ -2258,9 +2291,9 @@ void DomainServer::addStaticAssignmentsToQueue() {
|
|||
return a->getType() != Assignment::AgentType;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
auto staticAssignment = sharedAssignments.begin();
|
||||
|
||||
|
||||
while (staticAssignment != sharedAssignments.end()) {
|
||||
// add any of the un-matched static assignments to the queue
|
||||
|
||||
|
@ -2371,7 +2404,6 @@ void DomainServer::processNodeDisconnectRequestPacket(QSharedPointer<ReceivedMes
|
|||
}
|
||||
|
||||
void DomainServer::handleKillNode(SharedNodePointer nodeToKill) {
|
||||
auto nodeType = nodeToKill->getType();
|
||||
auto limitedNodeList = DependencyManager::get<LimitedNodeList>();
|
||||
const QUuid& nodeUUID = nodeToKill->getUUID();
|
||||
|
||||
|
@ -2383,10 +2415,9 @@ void DomainServer::handleKillNode(SharedNodePointer nodeToKill) {
|
|||
removedNodePacket->write(nodeUUID.toRfc4122());
|
||||
|
||||
// broadcast out the DomainServerRemovedNode message
|
||||
limitedNodeList->eachMatchingNode([&nodeType](const SharedNodePointer& otherNode) -> bool {
|
||||
limitedNodeList->eachMatchingNode([this, &nodeToKill](const SharedNodePointer& otherNode) -> bool {
|
||||
// only send the removed node packet to nodes that care about the type of node this was
|
||||
auto nodeLinkedData = dynamic_cast<DomainServerNodeData*>(otherNode->getLinkedData());
|
||||
return (nodeLinkedData != nullptr) && nodeLinkedData->getNodeInterestSet().contains(nodeType);
|
||||
return isInInterestSet(otherNode, nodeToKill);
|
||||
}, [&limitedNodeList](const SharedNodePointer& otherNode){
|
||||
limitedNodeList->sendUnreliablePacket(*removedNodePacket, *otherNode);
|
||||
});
|
||||
|
|
|
@ -132,6 +132,8 @@ private:
|
|||
|
||||
void sendDomainListToNode(const SharedNodePointer& node, const HifiSockAddr& senderSockAddr);
|
||||
|
||||
bool isInInterestSet(const SharedNodePointer& nodeA, const SharedNodePointer& nodeB);
|
||||
|
||||
QUuid connectionSecretForNodes(const SharedNodePointer& nodeA, const SharedNodePointer& nodeB);
|
||||
void broadcastNewNode(const SharedNodePointer& node);
|
||||
|
||||
|
|
|
@ -725,7 +725,7 @@ void DomainServerSettingsManager::processNodeKickRequestPacket(QSharedPointer<Re
|
|||
}
|
||||
|
||||
// potentially remove connect permissions for the MAC address and machine fingerprint
|
||||
DomainServerNodeData* nodeData = reinterpret_cast<DomainServerNodeData*>(matchingNode->getLinkedData());
|
||||
DomainServerNodeData* nodeData = static_cast<DomainServerNodeData*>(matchingNode->getLinkedData());
|
||||
if (nodeData) {
|
||||
// mac address first
|
||||
NodePermissionsKey macAddressKey(nodeData->getHardwareAddress(), 0);
|
||||
|
@ -780,45 +780,52 @@ void DomainServerSettingsManager::processNodeKickRequestPacket(QSharedPointer<Re
|
|||
|
||||
// This function processes the "Get Username from ID" request.
|
||||
void DomainServerSettingsManager::processUsernameFromIDRequestPacket(QSharedPointer<ReceivedMessage> message, SharedNodePointer sendingNode) {
|
||||
// Before we do any processing on this packet, make sure it comes from a node that is allowed to kick (is an admin)
|
||||
if (sendingNode->getCanKick()) {
|
||||
// From the packet, pull the UUID we're identifying
|
||||
QUuid nodeUUID = QUuid::fromRfc4122(message->readWithoutCopy(NUM_BYTES_RFC4122_UUID));
|
||||
// From the packet, pull the UUID we're identifying
|
||||
QUuid nodeUUID = QUuid::fromRfc4122(message->readWithoutCopy(NUM_BYTES_RFC4122_UUID));
|
||||
|
||||
if (!nodeUUID.isNull()) {
|
||||
// First, make sure we actually have a node with this UUID
|
||||
auto limitedNodeList = DependencyManager::get<LimitedNodeList>();
|
||||
auto matchingNode = limitedNodeList->nodeWithUUID(nodeUUID);
|
||||
if (!nodeUUID.isNull()) {
|
||||
// First, make sure we actually have a node with this UUID
|
||||
auto limitedNodeList = DependencyManager::get<LimitedNodeList>();
|
||||
auto matchingNode = limitedNodeList->nodeWithUUID(nodeUUID);
|
||||
|
||||
// If we do have a matching node...
|
||||
if (matchingNode) {
|
||||
// If we do have a matching node...
|
||||
if (matchingNode) {
|
||||
// Setup the packet
|
||||
auto usernameFromIDReplyPacket = NLPacket::create(PacketType::UsernameFromIDReply);
|
||||
|
||||
QString verifiedUsername;
|
||||
QUuid machineFingerprint;
|
||||
|
||||
// Write the UUID to the packet
|
||||
usernameFromIDReplyPacket->write(nodeUUID.toRfc4122());
|
||||
|
||||
// Check if the sending node has permission to kick (is an admin)
|
||||
// OR if the message is from a node whose UUID matches the one in the packet
|
||||
if (sendingNode->getCanKick() || nodeUUID == sendingNode->getUUID()) {
|
||||
// It's time to figure out the username
|
||||
QString verifiedUsername = matchingNode->getPermissions().getVerifiedUserName();
|
||||
|
||||
// Setup the packet
|
||||
auto usernameFromIDReplyPacket = NLPacket::create(PacketType::UsernameFromIDReply);
|
||||
usernameFromIDReplyPacket->write(nodeUUID.toRfc4122());
|
||||
verifiedUsername = matchingNode->getPermissions().getVerifiedUserName();
|
||||
usernameFromIDReplyPacket->writeString(verifiedUsername);
|
||||
|
||||
// now put in the machine fingerprint
|
||||
DomainServerNodeData* nodeData = reinterpret_cast<DomainServerNodeData*>(matchingNode->getLinkedData());
|
||||
QUuid machineFingerprint = nodeData ? nodeData->getMachineFingerprint() : QUuid();
|
||||
DomainServerNodeData* nodeData = static_cast<DomainServerNodeData*>(matchingNode->getLinkedData());
|
||||
machineFingerprint = nodeData ? nodeData->getMachineFingerprint() : QUuid();
|
||||
usernameFromIDReplyPacket->write(machineFingerprint.toRfc4122());
|
||||
|
||||
qDebug() << "Sending username" << verifiedUsername << "and machine fingerprint" << machineFingerprint << "associated with node" << nodeUUID;
|
||||
|
||||
// Ship it!
|
||||
limitedNodeList->sendPacket(std::move(usernameFromIDReplyPacket), *sendingNode);
|
||||
} else {
|
||||
qWarning() << "Node username request received for unknown node. Refusing to process.";
|
||||
usernameFromIDReplyPacket->writeString(verifiedUsername);
|
||||
usernameFromIDReplyPacket->write(machineFingerprint.toRfc4122());
|
||||
}
|
||||
} else {
|
||||
qWarning() << "Node username request received for invalid node ID. Refusing to process.";
|
||||
}
|
||||
// Write whether or not the user is an admin
|
||||
bool isAdmin = matchingNode->getCanKick();
|
||||
usernameFromIDReplyPacket->writePrimitive(isAdmin);
|
||||
|
||||
qDebug() << "Sending username" << verifiedUsername << "and machine fingerprint" << machineFingerprint << "associated with node" << nodeUUID << ". Node admin status: " << isAdmin;
|
||||
// Ship it!
|
||||
limitedNodeList->sendPacket(std::move(usernameFromIDReplyPacket), *sendingNode);
|
||||
} else {
|
||||
qWarning() << "Node username request received for unknown node. Refusing to process.";
|
||||
}
|
||||
} else {
|
||||
qWarning() << "Refusing to process a username request packet from node" << uuidStringWithoutCurlyBraces(sendingNode->getUUID())
|
||||
<< "that does not have kick permissions.";
|
||||
qWarning() << "Node username request received for invalid node ID. Refusing to process.";
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -33,6 +33,7 @@ Item {
|
|||
propagateComposedEvents: true
|
||||
acceptedButtons: "AllButtons"
|
||||
onClicked: {
|
||||
menu.visible = false;
|
||||
menu.done();
|
||||
mouse.accepted = false;
|
||||
}
|
||||
|
|
|
@ -32,6 +32,7 @@ Item {
|
|||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: {
|
||||
menu.visible = false;
|
||||
root.triggered();
|
||||
menu.done();
|
||||
}
|
||||
|
|
|
@ -1,50 +1,12 @@
|
|||
{
|
||||
"name": "Standard to Action",
|
||||
"when": "Application.NavigationFocused",
|
||||
"name": "Standard to UI Navigation Action",
|
||||
"channels": [
|
||||
{ "from": "Standard.DU", "to": "Actions.UiNavVertical" },
|
||||
{ "from": "Standard.DD", "to": "Actions.UiNavVertical", "filters": "invert" },
|
||||
{ "from": "Standard.DL", "to": "Actions.UiNavLateral", "filters": "invert" },
|
||||
{ "from": "Standard.DR", "to": "Actions.UiNavLateral" },
|
||||
{ "from": "Standard.LB", "to": "Actions.UiNavGroup","filters": "invert" },
|
||||
{ "from": "Standard.RB", "to": "Actions.UiNavGroup" },
|
||||
{ "from": [ "Standard.A", "Standard.X" ], "to": "Actions.UiNavSelect" },
|
||||
{ "from": [ "Standard.B", "Standard.Y" ], "to": "Actions.UiNavBack" },
|
||||
{ "from": [ "Standard.RTClick", "Standard.LTClick" ], "to": "Actions.UiNavSelect" },
|
||||
{
|
||||
"from": "Standard.LX", "to": "Actions.UiNavLateral",
|
||||
"filters": [
|
||||
{ "type": "deadZone", "min": 0.95 },
|
||||
"constrainToInteger",
|
||||
{ "type": "pulse", "interval": 0.4 }
|
||||
]
|
||||
},
|
||||
{
|
||||
"from": "Standard.LY", "to": "Actions.UiNavVertical",
|
||||
"filters": [
|
||||
"invert",
|
||||
{ "type": "deadZone", "min": 0.95 },
|
||||
"constrainToInteger",
|
||||
{ "type": "pulse", "interval": 0.4 }
|
||||
]
|
||||
},
|
||||
{
|
||||
"from": "Standard.RX", "to": "Actions.UiNavLateral",
|
||||
"filters": [
|
||||
{ "type": "deadZone", "min": 0.95 },
|
||||
"constrainToInteger",
|
||||
{ "type": "pulse", "interval": 0.4 }
|
||||
]
|
||||
},
|
||||
{
|
||||
"from": "Standard.RY", "to": "Actions.UiNavVertical",
|
||||
"filters": [
|
||||
"invert",
|
||||
{ "type": "deadZone", "min": 0.95 },
|
||||
"constrainToInteger",
|
||||
{ "type": "pulse", "interval": 0.4 }
|
||||
]
|
||||
}
|
||||
{ "from": "Standard.RB", "to": "Actions.UiNavGroup" }
|
||||
]
|
||||
}
|
||||
|
||||
|
|
|
@ -56,6 +56,9 @@
|
|||
{ "from": "GamePad.A", "to": "Standard.A" },
|
||||
{ "from": "GamePad.B", "to": "Standard.B" },
|
||||
{ "from": "GamePad.X", "to": "Standard.X" },
|
||||
{ "from": "GamePad.Y", "to": "Standard.Y" }
|
||||
{ "from": "GamePad.Y", "to": "Standard.Y" },
|
||||
|
||||
{ "from": [ "Standard.A", "Standard.X" ], "to": "Actions.UiNavSelect" },
|
||||
{ "from": [ "Standard.B", "Standard.Y" ], "to": "Actions.UiNavBack" }
|
||||
]
|
||||
}
|
||||
|
|
Before Width: | Height: | Size: 106 KiB After Width: | Height: | Size: 124 KiB |
63
interface/resources/icons/circle.svg
Normal file
|
@ -0,0 +1,63 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="70mm"
|
||||
height="70mm"
|
||||
viewBox="0 0 248.0315 248.03149"
|
||||
id="svg2"
|
||||
version="1.1"
|
||||
inkscape:version="0.91 r13725"
|
||||
sodipodi:docname="circle.svg">
|
||||
<defs
|
||||
id="defs4" />
|
||||
<sodipodi:namedview
|
||||
id="base"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="1.979899"
|
||||
inkscape:cx="79.162615"
|
||||
inkscape:cy="188.36943"
|
||||
inkscape:document-units="px"
|
||||
inkscape:current-layer="layer1"
|
||||
showgrid="false"
|
||||
inkscape:window-width="2560"
|
||||
inkscape:window-height="1377"
|
||||
inkscape:window-x="-8"
|
||||
inkscape:window-y="-8"
|
||||
inkscape:window-maximized="1" />
|
||||
<metadata
|
||||
id="metadata7">
|
||||
<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>
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"
|
||||
transform="translate(0,-804.33071)">
|
||||
<circle
|
||||
style="fill:#4b504f;fill-opacity:1;stroke:#feffff;stroke-width:0.22076035;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="path4140"
|
||||
cx="124.46446"
|
||||
cy="928.42853"
|
||||
r="121.41819" />
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 1.8 KiB |
68
interface/resources/icons/edit-icon.svg
Normal file
|
@ -0,0 +1,68 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Generator: Adobe Illustrator 19.2.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
version="1.1"
|
||||
x="0px"
|
||||
y="0px"
|
||||
viewBox="0 0 50 49.999998"
|
||||
xml:space="preserve"
|
||||
id="svg4239"
|
||||
inkscape:version="0.91 r13725"
|
||||
sodipodi:docname="edit-icon.svg"
|
||||
width="50"
|
||||
height="50"><metadata
|
||||
id="metadata4336"><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="defs4334" /><sodipodi:namedview
|
||||
pagecolor="#615f71"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1"
|
||||
objecttolerance="10"
|
||||
gridtolerance="10"
|
||||
guidetolerance="10"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:window-width="1663"
|
||||
inkscape:window-height="1023"
|
||||
id="namedview4332"
|
||||
showgrid="false"
|
||||
inkscape:zoom="4.717641"
|
||||
inkscape:cx="27.234437"
|
||||
inkscape:cy="40.361616"
|
||||
inkscape:window-x="412"
|
||||
inkscape:window-y="188"
|
||||
inkscape:window-maximized="0"
|
||||
inkscape:current-layer="svg4239" /><style
|
||||
type="text/css"
|
||||
id="style4241">
|
||||
.st0{fill:#414042;}
|
||||
.st1{fill:#FFFFFF;}
|
||||
.st2{fill:#1E1E1E;}
|
||||
.st3{fill:#333333;}
|
||||
</style><g
|
||||
id="Layer_3"
|
||||
transform="matrix(2.0055009,0,0,2.0055009,-26.481547,-324.23978)" /><path
|
||||
style="fill:#ffffff;fill-opacity:1"
|
||||
inkscape:connector-curvature="0"
|
||||
id="path4268"
|
||||
d="m 17.615466,29.206401 c -2.807702,2.807702 -5.615403,5.615403 -8.4231037,8.423104 -0.601651,-0.60165 -1.403851,-1.40385 -2.005501,-2.005501 2.807701,-2.807701 5.6154017,-5.615402 8.4231037,-8.423104 l -2.406601,-2.206051 c -3.2088017,3.208802 -6.4176027,6.417603 -9.8269547,9.626405 -0.60165,0.60165 -1.00275,1.40385 -1.2033,2.406601 -0.401101,2.607151 -0.802201,5.214302 -1.20330104,8.022004 0.20055004,0 0.40110004,0 0.60165004,0 2.607151,-0.601651 5.013753,-1.002751 7.620904,-1.604401 0.4011,-0.20055 1.0027497,-0.4011 1.4038497,-0.802201 3.409352,-3.409351 6.818703,-6.818703 10.228055,-10.228054 l -3.208801,-3.208802 z"
|
||||
class="st3" /><path
|
||||
style="fill:#ffffff;fill-opacity:1"
|
||||
inkscape:connector-curvature="0"
|
||||
id="path4270"
|
||||
d="m 30.851772,7.3464388 c 1.00275,-1.00275 2.206051,-2.206051 3.409351,-3.409351 0.4011,-0.4011 1.002751,-0.4011 1.403851,0 2.005501,2.005501 4.011002,4.011002 6.016502,6.016503 0.401101,0.4011002 0.401101,1.0027502 0,1.6044002 -1.2033,1.203303 -2.406601,2.406603 -3.409351,3.409354 -2.406601,-2.406603 -4.813202,-5.0137542 -7.420353,-7.6209062 z"
|
||||
class="st3" /><path
|
||||
style="fill:#ffffff;fill-opacity:1"
|
||||
inkscape:connector-curvature="0"
|
||||
id="path4272"
|
||||
d="m 33.659473,25.9976 c -0.60165,0.20055 -0.802201,0 -1.203301,-0.4011 -0.8022,-0.802201 -1.6044,-1.604401 -2.406601,-2.406601 2.005501,-2.005501 3.810452,-3.810452 5.815953,-5.815953 0.20055,-0.20055 0.4011,-0.4011 0.4011,-0.4011 -2.607151,-2.607151 -5.013752,-5.013754 -7.620903,-7.8214562 -0.200551,0.20055 -0.200551,0.4011 -0.401101,0.601651 -2.005501,2.0055002 -3.810451,3.8104532 -5.815952,5.8159542 -3.609902,-3.609903 -7.019253,-7.0192552 -10.629155,-10.6291572 -0.20055,-0.20055 -0.60165,-0.60165 -0.8022,-0.8022 -2.0055007,-1.403851 -4.4121017,-1.203301 -6.0165027,0.60165 -1.604401,1.804951 -1.403851,4.412102 0.20055,6.0165032 5.2143017,5.214304 10.6291547,10.629157 15.8434567,15.843459 1.804951,1.804951 3.409352,3.409352 5.214302,5.214303 0.200551,0.20055 0.401101,0.4011 0.200551,0.8022 -0.200551,0.8022 -0.401101,1.604401 -0.401101,2.406601 -0.4011,5.013752 2.807702,9.425854 7.821454,10.829705 2.005501,0.60165 4.011002,0.4011 6.217053,-0.4011 -0.20055,-0.20055 -0.4011,-0.4011 -0.601651,-0.4011 -1.40385,-1.403851 -3.008251,-3.008252 -4.412102,-4.412102 -1.00275,-1.002751 -1.2033,-2.807702 -0.20055,-3.810452 0.802201,-0.802201 1.604401,-1.604401 2.406601,-2.406601 1.203301,-1.002751 2.607152,-1.002751 3.609902,0 0.20055,0.20055 0.4011,0.4011 0.60165,0.60165 1.403851,1.403851 3.008252,3.008251 4.412102,4.412102 0,0 0.20055,0 0.20055,0 0.200551,-0.8022 0.401101,-1.604401 0.401101,-2.607151 C 47.697979,29.607502 40.678726,23.992099 33.659473,25.9976 Z M 8.1896113,9.1513898 c -0.8022,0 -1.40385,-0.60165 -1.40385,-1.40385 0,-0.802201 0.60165,-1.604401 1.40385,-1.604401 0.802201,0 1.604401,0.8022 1.604401,1.403851 0,0.8022 -0.8022,1.6044 -1.604401,1.6044 z M 29.046821,13.764044 c 0.60165,0.601651 1.2033,1.403851 2.005501,2.005501 -1.403851,1.403851 -2.807702,2.807702 -4.211552,4.211552 -0.601651,-0.60165 -1.403851,-1.40385 -2.005501,-2.005501 1.40385,-1.40385 2.807701,-2.807701 4.211552,-4.211552 z"
|
||||
class="st3" /></svg>
|
After Width: | Height: | Size: 5 KiB |
46
interface/resources/icons/tablet-icons/bubble-i.svg
Normal file
|
@ -0,0 +1,46 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 19.2.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 50 50" style="enable-background:new 0 0 50 50;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#FFFFFF;}
|
||||
</style>
|
||||
<g id="Layer_2">
|
||||
</g>
|
||||
<g id="Layer_1">
|
||||
<g>
|
||||
<path class="st0" d="M23.2,24.1c-0.8,0.9-1.5,1.8-2.2,2.6c-0.1,0.2-0.1,0.5-0.1,0.7c0.1,1.7,0.2,3.4,0.2,5.1
|
||||
c0,0.8-0.4,1.2-1.1,1.3c-0.7,0.1-1.3-0.4-1.4-1.1c-0.2-2.2-0.3-4.3-0.5-6.5c0-0.3,0.1-0.7,0.4-1c1.1-1.5,2.3-3,3.4-4.5
|
||||
c0.6-0.7,1.6-1.6,2.6-1.6c0.3,0,1.1,0,1.4,0c0.8-0.1,1.3,0.1,1.9,0.9c1,1.2,1.5,2.3,2.4,3.6c0.7,1.1,1.4,1.6,2.9,1.9
|
||||
c1.1,0.2,2.2,0.5,3.3,0.8c0.3,0.1,0.6,0.2,0.8,0.3c0.5,0.3,0.7,0.8,0.6,1.3c-0.1,0.5-0.5,0.7-1,0.8c-0.4,0-0.9,0-1.3-0.1
|
||||
c-1.4-0.3-2.7-0.6-4.1-0.9c-0.8-0.2-1.5-0.6-2.1-1.1c-0.3-0.3-0.6-0.5-0.9-0.8c0,0.3,0,0.5,0,0.7c0,1.2,0,2.4,0,3.6
|
||||
c0,0.4-0.3,12.6-0.1,16.8c0,0.5-0.1,1-0.2,1.5c-0.2,0.7-0.6,1-1.4,1.1c-0.8,0-1.4-0.3-1.7-1c-0.2-0.5-0.3-1.1-0.4-1.6
|
||||
c-0.4-4.6-0.9-12.9-1.1-13.8c-0.1-0.8-0.2-1.1-0.3-2.1c-0.1-0.5-0.1-0.9-0.1-1.3C23.3,27.9,23.2,26.1,23.2,24.1z"/>
|
||||
<path class="st0" d="M28.2,14.6c0,1.4-1.1,2.6-2.6,2.6l0,0c-1.4,0-2.6-1.1-2.6-2.6v-1.6c0-1.4,1.1-2.6,2.6-2.6l0,0
|
||||
c1.4,0,2.6,1.1,2.6,2.6V14.6z"/>
|
||||
</g>
|
||||
<path class="st0" d="M8.4,38.9c2.8,3.2,6.4,5.5,10.5,6.7c0.6,0.2,1.3,0.1,1.7-0.3c0.4-0.3,0.6-0.6,0.7-1c0.2-0.5,0.1-1.1-0.2-1.5
|
||||
c-0.3-0.5-0.7-0.8-1.2-1c-1.6-0.5-3.2-1.2-4.6-2.1c-1.5-0.9-2.8-2.1-4-3.4c-0.4-0.4-0.9-0.7-1.5-0.7c-0.5,0-1,0.2-1.3,0.5
|
||||
c-0.4,0.4-0.6,0.8-0.7,1.4C7.8,38,8,38.5,8.4,38.9z"/>
|
||||
<path class="st0" d="M43.7,36.8c0.3-0.4,0.4-1,0.3-1.5c-0.1-0.5-0.4-1-0.8-1.3c-0.3-0.2-0.7-0.3-1.1-0.3c-0.7,0-1.3,0.3-1.7,0.9
|
||||
c-1.1,1.6-2.4,3-4,4.2c-1.2,0.9-2.5,1.7-3.9,2.3c-0.5,0.2-0.9,0.6-1.1,1.1c-0.2,0.5-0.2,1,0,1.5c0.4,1,1.6,1.5,2.6,1
|
||||
c1.7-0.7,3.3-1.7,4.8-2.8C40.7,40.4,42.4,38.7,43.7,36.8z"/>
|
||||
<path class="st0" d="M5.1,33.2c0.5,0.4,1.2,0.4,1.8,0.2c0.5-0.2,0.9-0.6,1.1-1.1c0.2-0.5,0.2-1,0-1.5c-0.1-0.4-0.4-0.7-0.7-0.9
|
||||
c-0.3-0.2-0.7-0.3-1.1-0.3c-0.2,0-0.5,0-0.7,0.1c-1,0.4-1.5,1.6-1.1,2.6C4.5,32.7,4.7,33,5.1,33.2z"/>
|
||||
<path class="st0" d="M45.4,27.3c-0.2,0-0.3-0.1-0.5-0.1c-0.9,0-1.7,0.6-1.9,1.5c-0.1,0.5-0.1,1.1,0.2,1.5c0.3,0.5,0.7,0.8,1.2,0.9
|
||||
c0.2,0,0.3,0.1,0.5,0.1c0.9,0,1.7-0.6,1.9-1.5c0.1-0.5,0.1-1.1-0.2-1.5C46.4,27.8,45.9,27.5,45.4,27.3z"/>
|
||||
<path class="st0" d="M8.6,12c-0.3-0.2-0.7-0.3-1-0.3c-0.3,0-0.7,0.1-1,0.3c-0.3,0.2-0.6,0.4-0.7,0.7c-2,3.5-3.1,7.4-3.1,11.4
|
||||
c0,0.2,0,0.4,0,0.6c0,0.5,0.2,1,0.6,1.4c0.4,0.4,0.9,0.6,1.4,0.6v0.4l0.1-0.4c0.5,0,1-0.2,1.4-0.6c0.4-0.4,0.6-0.9,0.5-1.4
|
||||
c0-0.2,0-0.4,0-0.5c0-3.3,0.9-6.6,2.6-9.4C9.9,13.8,9.6,12.6,8.6,12z"/>
|
||||
<path class="st0" d="M39.3,11.4c-0.1,0.5,0.1,1.1,0.4,1.5c1.1,1.4,2,3,2.6,4.6c0.6,1.6,1,3.2,1.2,4.9c0,0.5,0.3,1,0.6,1.3
|
||||
c0.4,0.4,1,0.6,1.5,0.5c0.5,0,1-0.3,1.4-0.7c0.3-0.4,0.5-0.9,0.5-1.5c-0.4-4.2-2-8.2-4.6-11.6c-0.4-0.5-1-0.8-1.6-0.8
|
||||
c-0.4,0-0.9,0.1-1.2,0.4C39.7,10.4,39.4,10.8,39.3,11.4z"/>
|
||||
<path class="st0" d="M12.2,6.3c-0.5,0-0.9,0.2-1.3,0.5c-0.4,0.3-0.7,0.8-0.7,1.4c-0.1,0.5,0.1,1.1,0.4,1.5c0.7,0.8,2,1,2.8,0.3
|
||||
c0.4-0.3,0.7-0.8,0.7-1.3c0.1-0.5-0.1-1.1-0.4-1.5C13.4,6.6,12.8,6.3,12.2,6.3z"/>
|
||||
<path class="st0" d="M37.2,5.2c-0.3-0.2-0.7-0.3-1.1-0.3c-0.7,0-1.3,0.3-1.7,0.9c-0.3,0.4-0.4,1-0.3,1.5c0.1,0.5,0.4,1,0.9,1.3
|
||||
C36,9.2,37.3,8.9,37.9,8C38.4,7.1,38.2,5.8,37.2,5.2z"/>
|
||||
<path class="st0" d="M16.5,4c-0.2,0.5-0.3,1-0.1,1.5c0.4,1,1.5,1.6,2.6,1.2c3.3-1.2,6.8-1.4,10.2-0.6c0.6,0.1,1.2,0,1.7-0.4
|
||||
c0.4-0.3,0.6-0.7,0.7-1.1c0.1-0.5,0-1.1-0.3-1.5c-0.3-0.5-0.7-0.8-1.3-0.9c-1.6-0.4-3.3-0.5-4.9-0.5c-2.6,0-5.1,0.4-7.5,1.3
|
||||
C17.1,3.2,16.7,3.6,16.5,4z"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 3.7 KiB |
25
interface/resources/icons/tablet-icons/edit-i.svg
Normal file
|
@ -0,0 +1,25 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 19.2.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 50 50" style="enable-background:new 0 0 50 50;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#FFFFFF;}
|
||||
</style>
|
||||
<g id="Layer_2">
|
||||
</g>
|
||||
<g>
|
||||
<path class="st0" d="M20.7,29.7c-2.2,2.2-4.4,4.4-6.7,6.7c-0.5-0.5-1.1-1.1-1.6-1.6c2.2-2.2,4.4-4.4,6.7-6.7l-1.8-1.8
|
||||
c-2.6,2.5-5.1,5.1-7.7,7.6c-0.5,0.5-0.9,1.1-1,1.8C8.3,37.8,8,39.8,7.7,42c0.2,0,0.4,0,0.5,0c2-0.4,4-0.8,5.9-1.2
|
||||
c0.4-0.1,0.8-0.3,1.1-0.6c2.7-2.6,5.3-5.3,8-8L20.7,29.7z"/>
|
||||
<path class="st0" d="M31.1,11c0.8-0.8,1.8-1.8,2.7-2.7C34.2,8,34.6,8,34.9,8.4c1.6,1.6,3.1,3.1,4.7,4.7c0.4,0.4,0.4,0.8,0,1.2
|
||||
c-0.9,0.9-1.8,1.8-2.7,2.7C35,15,33.1,13,31.1,11z"/>
|
||||
<path class="st0" d="M33,25.9c-0.4,0.1-0.6,0-0.9-0.2c-0.6-0.6-1.3-1.3-1.9-1.9c1.5-1.5,3.1-3.1,4.6-4.6c0.1-0.1,0.3-0.3,0.3-0.3
|
||||
c-2-2-3.9-4-5.9-6.1c-0.1,0.2-0.2,0.3-0.4,0.5c-1.5,1.5-3,3-4.6,4.6c-2.8-2.8-5.6-5.6-8.4-8.4c-0.2-0.2-0.4-0.4-0.6-0.6
|
||||
c-1.5-1.2-3.5-1-4.8,0.4c-1.2,1.4-1.1,3.5,0.2,4.8c4.2,4.2,8.3,8.3,12.5,12.5c1.4,1.4,2.7,2.7,4.1,4.1c0.2,0.2,0.2,0.4,0.2,0.7
|
||||
c-0.2,0.6-0.3,1.2-0.3,1.9c-0.3,4,2.3,7.5,6.1,8.5c1.6,0.4,3.2,0.3,4.8-0.3c-0.1-0.2-0.3-0.2-0.4-0.4c-1.2-1.2-2.3-2.3-3.5-3.5
|
||||
c-0.8-0.9-0.9-2.1-0.1-3c0.6-0.7,1.3-1.3,2-2c0.9-0.8,2-0.8,2.9,0c0.2,0.2,0.3,0.3,0.5,0.5c1.2,1.2,2.3,2.3,3.5,3.5
|
||||
c0.1,0,0.1,0,0.2,0c0.1-0.7,0.3-1.3,0.3-2C43.9,28.7,38.5,24.3,33,25.9z M12.9,12.6c-0.6,0-1.2-0.5-1.2-1.2s0.5-1.2,1.2-1.2
|
||||
c0.6,0,1.2,0.6,1.2,1.2C14.1,12,13.6,12.6,12.9,12.6z M29.3,16.3c0.5,0.5,1,1.1,1.6,1.6c-1.1,1.1-2.2,2.2-3.3,3.3
|
||||
c-0.5-0.5-1.1-1.1-1.6-1.6C27.1,18.5,28.2,17.4,29.3,16.3z"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 1.8 KiB |
16
interface/resources/icons/tablet-icons/goto-i.svg
Normal file
|
@ -0,0 +1,16 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 19.2.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 50 50" style="enable-background:new 0 0 50 50;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#FFFFFF;}
|
||||
</style>
|
||||
<g id="Layer_2">
|
||||
</g>
|
||||
<g id="Layer_1">
|
||||
<path class="st0" d="M47.2,41.3l-9.1-9.1c-0.8-0.8-1.9-1.1-3-1l-2.4-2.4c1.8-2.6,2.8-5.7,2.8-9c0-8.9-7.2-16.1-16.1-16.1
|
||||
S3.3,11,3.3,19.8c0,8.9,7.2,16.1,16.1,16.1c4.1,0,7.8-1.5,10.6-4l2.2,2.2c-0.2,1.1,0.1,2.2,1,3l9.1,9.1c1.4,1.4,3.6,1.4,4.9,0
|
||||
C48.5,44.9,48.5,42.7,47.2,41.3z M19.4,32.2c-6.8,0-12.3-5.5-12.3-12.3c0-6.8,5.5-12.3,12.3-12.3s12.3,5.5,12.3,12.3
|
||||
C31.8,26.6,26.2,32.2,19.4,32.2z"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 832 B |
27
interface/resources/icons/tablet-icons/help-i.svg
Normal file
|
@ -0,0 +1,27 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 19.2.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 50 50" style="enable-background:new 0 0 50 50;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#FFFFFF;}
|
||||
.st1{fill:#FFFFFF;stroke:#FFFFFF;stroke-width:0.25;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
|
||||
</style>
|
||||
<g id="Layer_2">
|
||||
</g>
|
||||
<g id="Layer_1">
|
||||
<path class="st0" d="M11.3,36.7c-0.2-0.3-0.4-0.6-0.6-0.8C6.3,29,5.8,21.8,10,14.8c4.3-7.2,11-10.4,19.3-9.3
|
||||
c7.9,1.1,13.4,5.8,15.8,13.4c2.4,7.8,0.4,14.6-5.5,20.1c-3.8,3.6-8.4,5.1-13.5,5.1c-7.3,0-14.6-0.1-21.9-0.1c-0.2,0-0.5,0-1,0
|
||||
C6,41.5,8.6,39.1,11.3,36.7z M10.9,41c0.3,0,0.6,0.1,0.8,0.1c4.9,0,9.9,0,14.8,0C35.6,41,42.8,34,43,25.1
|
||||
c0.2-9.3-7.3-16.9-16.6-16.7c-6.9,0.1-12,3.5-14.9,9.7c-2.9,6.4-1.8,12.5,2.8,17.8c0.3,0.4,0.7,0.7,1,1.1
|
||||
C13.9,38.3,12.5,39.6,10.9,41z"/>
|
||||
<path class="st1" d="M22.5,20.9c0-0.7,0.1-1.4,0.3-2c0.2-0.6,0.6-1.1,1-1.6c0.4-0.4,0.9-0.8,1.6-1c0.6-0.2,1.3-0.4,2-0.4
|
||||
c0.6,0,1.2,0.1,1.7,0.3c0.5,0.2,1,0.4,1.4,0.8c0.4,0.3,0.7,0.8,1,1.3c0.2,0.5,0.3,1.1,0.3,1.8c0,0.5-0.1,0.9-0.2,1.2
|
||||
c-0.1,0.3-0.2,0.6-0.4,0.9c-0.2,0.3-0.4,0.5-0.6,0.7c-0.2,0.2-0.4,0.4-0.7,0.6c-0.3,0.2-0.5,0.4-0.7,0.6c-0.2,0.2-0.4,0.4-0.6,0.7
|
||||
c-0.2,0.3-0.3,0.5-0.4,0.9c-0.1,0.3-0.1,0.8-0.1,1.2H26c0-0.6,0-1.1,0.1-1.5c0.1-0.4,0.2-0.8,0.3-1.1c0.1-0.3,0.3-0.6,0.5-0.8
|
||||
c0.2-0.2,0.4-0.5,0.7-0.7c0.2-0.2,0.4-0.4,0.6-0.5c0.2-0.2,0.4-0.3,0.5-0.5c0.2-0.2,0.3-0.4,0.4-0.7c0.1-0.2,0.1-0.5,0.1-0.9
|
||||
c0-0.4-0.1-0.8-0.2-1.1c-0.1-0.3-0.3-0.5-0.5-0.7c-0.2-0.2-0.4-0.3-0.7-0.4c-0.2-0.1-0.4-0.1-0.6-0.1c-0.8,0-1.5,0.3-1.9,0.8
|
||||
c-0.4,0.6-0.6,1.3-0.6,2.2H22.5z"/>
|
||||
<path class="st0" d="M27.6,29.8c-0.1,0-0.3-0.1-0.4-0.1c-0.8,0-1.4,0.5-1.6,1.2c-0.1,0.4,0,0.9,0.2,1.3c0.2,0.4,0.6,0.7,1,0.8
|
||||
c0.1,0,0.3,0.1,0.4,0.1c0.8,0,1.4-0.5,1.6-1.2c0.1-0.4,0.1-0.9-0.2-1.3C28.4,30.1,28,29.9,27.6,29.8z"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 2 KiB |
28
interface/resources/icons/tablet-icons/ignore-i.svg
Normal file
|
@ -0,0 +1,28 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 19.2.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 50 50" style="enable-background:new 0 0 50 50;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#FFFFFF;}
|
||||
</style>
|
||||
<g id="Layer_2">
|
||||
</g>
|
||||
<g id="Layer_1">
|
||||
<path class="st0" d="M27.1,42.3c-1.4,0.3-2.7,0.5-4.1,0.5c-9.3,0-16.8-7.5-16.8-16.8S13.8,9.2,23,9.2S39.8,16.7,39.8,26
|
||||
c0,0.2,0,0.3,0,0.5c0.7-0.7,1.4-1,2.4-1.5C41.7,14.9,33.3,6.8,23,6.8C12.4,6.8,3.8,15.4,3.8,26S12.4,45.2,23,45.2
|
||||
c1.7,0,3.4-0.3,5.1-0.7C27.8,43.6,27.5,43,27.1,42.3z"/>
|
||||
<path class="st0" d="M32.6,20.7L32.6,20.7c-0.4-0.4-0.7-0.4-1.2-0.3c0,0-5.9,0.7-8.4,0.7h-0.1c-2.5,0-8.6-0.9-8.6-0.9
|
||||
c-0.4-0.1-0.9,0-1.2,0.4L13,21c-0.1,0.3-0.1,0.6-0.1,1c0.1,0.3,0.3,0.6,0.6,0.7c1,0.4,4.9,1.8,5.9,2.1c0.1,0,0.6,0.1,0.6,0.9
|
||||
c0,0.9-0.3,4.6-0.7,6.4c-0.4,1.8-1.2,4-1.2,4c-0.1,0.6,0.1,1.3,0.7,1.5l0.7,0.3c0.3,0.1,0.6,0.1,0.9,0c0.3-0.1,0.4-0.4,0.6-0.7
|
||||
l2.1-6.4l1.9,6.5c0.1,0.3,0.3,0.6,0.6,0.7c0.1,0.1,0.3,0.1,0.4,0.1c0.1,0,0.3,0,0.4-0.1l0.7-0.3c0.6-0.1,0.9-0.7,0.7-1.3
|
||||
c0,0-0.6-2.4-1-4.3c-0.3-1.2-0.4-3-0.4-4.3c0-0.9-0.1-1.6-0.1-2.2c0-0.3,0.1-0.6,0.6-0.7c0.1,0,5.5-1.9,5.5-1.9
|
||||
c0.4-0.1,0.6-0.4,0.7-0.9C32.9,21.5,32.9,21,32.6,20.7z"/>
|
||||
<circle class="st0" cx="23" cy="17" r="2.5"/>
|
||||
<g>
|
||||
<path class="st0" d="M43.1,43L43.1,43L43.1,43z"/>
|
||||
<path class="st0" d="M41,35.6l4.6-4.6c0.7-0.7,0.7-1.9,0-2.6c-0.7-0.7-1.9-0.7-2.6,0L38.5,33l-4.6-4.6c-0.7-0.7-1.9-0.7-2.6,0
|
||||
s-0.7,1.9,0,2.6l4.6,4.6l-4.6,4.6c-0.7,0.7-0.7,1.9,0,2.6s1.9,0.7,2.6,0l4.6-4.6l4.6,4.6c0.7,0.7,1.9,0.7,2.6,0
|
||||
c0.7-0.7,0.7-1.9,0-2.6L41,35.6z"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 1.7 KiB |
177
interface/resources/icons/tablet-icons/ignore.svg
Normal file
|
@ -0,0 +1,177 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 19.2.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 50 200.1" style="enable-background:new 0 0 50 200.1;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#414042;}
|
||||
.st1{fill:#FFFFFF;}
|
||||
.st2{fill:#1E1E1E;}
|
||||
.st3{fill:#333333;}
|
||||
</style>
|
||||
<g id="Layer_2">
|
||||
<g>
|
||||
<g>
|
||||
<path class="st0" d="M50.1,146.1c0,2.2-1.8,4-4,4h-42c-2.2,0-4-1.8-4-4v-42c0-2.2,1.8-4,4-4h42c2.2,0,4,1.8,4,4V146.1z"/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<path class="st0" d="M50,196.1c0,2.2-1.8,4-4,4H4c-2.2,0-4-1.8-4-4v-42c0-2.2,1.8-4,4-4h42c2.2,0,4,1.8,4,4V196.1z"/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<path class="st1" d="M50,46c0,2.2-1.8,4-4,4H4c-2.2,0-4-1.8-4-4V4c0-2.2,1.8-4,4-4h42c2.2,0,4,1.8,4,4V46z"/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<path class="st2" d="M50,96.1c0,2.2-1.8,4-4,4H4c-2.2,0-4-1.8-4-4v-42c0-2.2,1.8-4,4-4h42c2.2,0,4,1.8,4,4V96.1z"/>
|
||||
</g>
|
||||
</g>
|
||||
<path class="st1" d="M24.7,81.2c-6.2,0-11.2-5-11.2-11.2c0-6.2,5-11.2,11.2-11.2c6.2,0,11.2,5,11.2,11.2
|
||||
C35.9,76.2,30.9,81.2,24.7,81.2z M24.7,60.3c-5.4,0-9.8,4.4-9.8,9.8c0,5.4,4.4,9.8,9.8,9.8c5.4,0,9.8-4.4,9.8-9.8
|
||||
C34.5,64.6,30.1,60.3,24.7,60.3z"/>
|
||||
<g>
|
||||
<path class="st3" d="M7.7,42.5v-6.4h1.2v6.4H7.7z"/>
|
||||
<path class="st3" d="M14.7,41.8c-0.5,0.5-1.1,0.8-1.7,0.8c-0.4,0-0.8-0.1-1.2-0.3c-0.4-0.2-0.7-0.4-0.9-0.7c-0.3-0.3-0.5-0.7-0.6-1
|
||||
s-0.2-0.8-0.2-1.2c0-0.4,0.1-0.9,0.2-1.2c0.2-0.4,0.4-0.7,0.6-1c0.3-0.3,0.6-0.5,1-0.7c0.4-0.2,0.8-0.3,1.2-0.3
|
||||
c0.6,0,1.1,0.1,1.5,0.4s0.7,0.6,0.9,1l-0.9,0.7c-0.2-0.3-0.4-0.6-0.6-0.7s-0.6-0.3-0.9-0.3c-0.3,0-0.5,0.1-0.7,0.2
|
||||
c-0.2,0.1-0.4,0.3-0.5,0.5c-0.2,0.2-0.3,0.4-0.4,0.7c-0.1,0.3-0.1,0.5-0.1,0.8c0,0.3,0,0.6,0.1,0.8c0.1,0.3,0.2,0.5,0.4,0.7
|
||||
c0.2,0.2,0.4,0.3,0.6,0.5c0.2,0.1,0.5,0.2,0.7,0.2c0.6,0,1.1-0.3,1.6-0.9v-0.5h-1.3v-0.9h2.3v3.3h-1V41.8z"/>
|
||||
<path class="st3" d="M18.3,38.4v4.1h-1.2v-6.4h1l3.3,4.2v-4.2h1.2v6.4h-1L18.3,38.4z"/>
|
||||
<path class="st3" d="M26.8,42.5c-0.5,0-0.9-0.1-1.2-0.3s-0.7-0.4-1-0.7c-0.3-0.3-0.5-0.6-0.6-1c-0.1-0.4-0.2-0.8-0.2-1.2
|
||||
c0-0.4,0.1-0.8,0.2-1.2s0.4-0.7,0.6-1c0.3-0.3,0.6-0.5,1-0.7s0.8-0.3,1.2-0.3c0.5,0,0.9,0.1,1.2,0.3s0.7,0.4,1,0.7
|
||||
c0.3,0.3,0.5,0.7,0.6,1c0.1,0.4,0.2,0.8,0.2,1.2c0,0.4-0.1,0.8-0.2,1.2c-0.2,0.4-0.4,0.7-0.6,1c-0.3,0.3-0.6,0.5-1,0.7
|
||||
C27.7,42.4,27.2,42.5,26.8,42.5z M25,39.3c0,0.3,0,0.5,0.1,0.8c0.1,0.3,0.2,0.5,0.4,0.7c0.2,0.2,0.3,0.4,0.6,0.5s0.5,0.2,0.8,0.2
|
||||
c0.3,0,0.5-0.1,0.8-0.2s0.4-0.3,0.6-0.5c0.1-0.2,0.3-0.4,0.3-0.7s0.1-0.5,0.1-0.8c0-0.3,0-0.5-0.1-0.8c-0.1-0.3-0.2-0.5-0.4-0.7
|
||||
c-0.2-0.2-0.3-0.4-0.6-0.5c-0.2-0.1-0.5-0.2-0.7-0.2c-0.3,0-0.5,0.1-0.8,0.2s-0.4,0.3-0.6,0.5c-0.2,0.2-0.3,0.4-0.3,0.7
|
||||
C25.1,38.8,25,39,25,39.3z"/>
|
||||
<path class="st3" d="M31,42.5v-6.4h2.8c0.3,0,0.6,0.1,0.8,0.2s0.5,0.3,0.6,0.5c0.2,0.2,0.3,0.4,0.4,0.7c0.1,0.3,0.2,0.5,0.2,0.8
|
||||
c0,0.4-0.1,0.8-0.3,1.1c-0.2,0.3-0.5,0.6-0.8,0.7l1.5,2.4h-1.4l-1.3-2.1h-1.2v2.1H31z M32.3,39.3h1.6c0.1,0,0.2,0,0.3-0.1
|
||||
c0.1-0.1,0.2-0.1,0.3-0.2c0.1-0.1,0.1-0.2,0.2-0.3s0.1-0.3,0.1-0.4c0-0.1,0-0.3-0.1-0.4s-0.1-0.2-0.2-0.3c-0.1-0.1-0.2-0.2-0.3-0.2
|
||||
c-0.1-0.1-0.2-0.1-0.3-0.1h-1.5V39.3z"/>
|
||||
<path class="st3" d="M41.8,41.4v1.1h-4.4v-6.4h4.4v1.1h-3.1v1.5h2.7v1h-2.7v1.7H41.8z"/>
|
||||
</g>
|
||||
<path class="st1" d="M27.1,79.5c-0.8,0.2-1.6,0.3-2.4,0.3c-5.4,0-9.8-4.4-9.8-9.8c0-5.4,4.4-9.8,9.8-9.8c5.4,0,9.8,4.4,9.8,9.8
|
||||
c0,0.1,0,0.2,0,0.3c0.4-0.4,0.8-0.6,1.4-0.9c-0.3-5.9-5.2-10.6-11.2-10.6c-6.2,0-11.2,5-11.2,11.2c0,6.2,5,11.2,11.2,11.2
|
||||
c1,0,2-0.2,3-0.4C27.5,80.4,27.3,80,27.1,79.5z"/>
|
||||
<path class="st1" d="M31.2,66.5L31.2,66.5c-0.3-0.3-0.5-0.3-0.8-0.2c0,0-4,0.5-5.7,0.5c0,0-0.1,0-0.1,0c-1.7,0-5.8-0.6-5.8-0.6
|
||||
c-0.3-0.1-0.6,0-0.8,0.3l-0.1,0.2c-0.1,0.2-0.1,0.4-0.1,0.7c0.1,0.2,0.2,0.4,0.4,0.5c0.7,0.3,3.3,1.2,4,1.4c0.1,0,0.4,0.1,0.4,0.6
|
||||
c0,0.6-0.2,3.1-0.5,4.3c-0.3,1.2-0.8,2.7-0.8,2.7c-0.1,0.4,0.1,0.9,0.5,1l0.5,0.2c0.2,0.1,0.4,0.1,0.6,0c0.2-0.1,0.3-0.3,0.4-0.5
|
||||
l1.4-4.3l1.3,4.4c0.1,0.2,0.2,0.4,0.4,0.5c0.1,0.1,0.2,0.1,0.3,0.1c0.1,0,0.2,0,0.3-0.1l0.5-0.2c0.4-0.1,0.6-0.5,0.5-0.9
|
||||
c0,0-0.4-1.6-0.7-2.9c-0.2-0.8-0.3-2-0.3-2.9c0-0.6-0.1-1.1-0.1-1.5c0-0.2,0.1-0.4,0.4-0.5c0.1,0,3.7-1.3,3.7-1.3
|
||||
c0.3-0.1,0.4-0.3,0.5-0.6C31.4,66.9,31.4,66.7,31.2,66.5z"/>
|
||||
<circle class="st1" cx="24.7" cy="64" r="1.7"/>
|
||||
<g>
|
||||
<path class="st1" d="M7.7,92.3V86h1.2v6.4H7.7z"/>
|
||||
<path class="st1" d="M14.7,91.6c-0.5,0.5-1.1,0.8-1.7,0.8c-0.4,0-0.8-0.1-1.2-0.3c-0.4-0.2-0.7-0.4-0.9-0.7c-0.3-0.3-0.5-0.7-0.6-1
|
||||
s-0.2-0.8-0.2-1.2c0-0.4,0.1-0.9,0.2-1.2c0.2-0.4,0.4-0.7,0.6-1c0.3-0.3,0.6-0.5,1-0.7c0.4-0.2,0.8-0.3,1.2-0.3
|
||||
c0.6,0,1.1,0.1,1.5,0.4s0.7,0.6,0.9,1L14.6,88c-0.2-0.3-0.4-0.6-0.6-0.7S13.4,87,13.1,87c-0.3,0-0.5,0.1-0.7,0.2
|
||||
c-0.2,0.1-0.4,0.3-0.5,0.5c-0.2,0.2-0.3,0.4-0.4,0.7c-0.1,0.3-0.1,0.5-0.1,0.8c0,0.3,0,0.6,0.1,0.8c0.1,0.3,0.2,0.5,0.4,0.7
|
||||
c0.2,0.2,0.4,0.3,0.6,0.5c0.2,0.1,0.5,0.2,0.7,0.2c0.6,0,1.1-0.3,1.6-0.9V90h-1.3v-0.9h2.3v3.3h-1V91.6z"/>
|
||||
<path class="st1" d="M18.3,88.2v4.1h-1.2V86h1l3.3,4.2V86h1.2v6.4h-1L18.3,88.2z"/>
|
||||
<path class="st1" d="M26.8,92.4c-0.5,0-0.9-0.1-1.2-0.3s-0.7-0.4-1-0.7c-0.3-0.3-0.5-0.6-0.6-1c-0.1-0.4-0.2-0.8-0.2-1.2
|
||||
c0-0.4,0.1-0.8,0.2-1.2s0.4-0.7,0.6-1c0.3-0.3,0.6-0.5,1-0.7s0.8-0.3,1.2-0.3c0.5,0,0.9,0.1,1.2,0.3s0.7,0.4,1,0.7
|
||||
c0.3,0.3,0.5,0.7,0.6,1c0.1,0.4,0.2,0.8,0.2,1.2c0,0.4-0.1,0.8-0.2,1.2c-0.2,0.4-0.4,0.7-0.6,1c-0.3,0.3-0.6,0.5-1,0.7
|
||||
C27.7,92.3,27.2,92.4,26.8,92.4z M25,89.1c0,0.3,0,0.5,0.1,0.8c0.1,0.3,0.2,0.5,0.4,0.7c0.2,0.2,0.3,0.4,0.6,0.5s0.5,0.2,0.8,0.2
|
||||
c0.3,0,0.5-0.1,0.8-0.2s0.4-0.3,0.6-0.5c0.1-0.2,0.3-0.4,0.3-0.7s0.1-0.5,0.1-0.8c0-0.3,0-0.5-0.1-0.8c-0.1-0.3-0.2-0.5-0.4-0.7
|
||||
c-0.2-0.2-0.3-0.4-0.6-0.5c-0.2-0.1-0.5-0.2-0.7-0.2c-0.3,0-0.5,0.1-0.8,0.2s-0.4,0.3-0.6,0.5c-0.2,0.2-0.3,0.4-0.3,0.7
|
||||
C25.1,88.6,25,88.9,25,89.1z"/>
|
||||
<path class="st1" d="M31,92.3V86h2.8c0.3,0,0.6,0.1,0.8,0.2s0.5,0.3,0.6,0.5c0.2,0.2,0.3,0.4,0.4,0.7c0.1,0.3,0.2,0.5,0.2,0.8
|
||||
c0,0.4-0.1,0.8-0.3,1.1c-0.2,0.3-0.5,0.6-0.8,0.7l1.5,2.4h-1.4l-1.3-2.1h-1.2v2.1H31z M32.3,89.1h1.6c0.1,0,0.2,0,0.3-0.1
|
||||
c0.1-0.1,0.2-0.1,0.3-0.2c0.1-0.1,0.1-0.2,0.2-0.3s0.1-0.3,0.1-0.4c0-0.1,0-0.3-0.1-0.4s-0.1-0.2-0.2-0.3c-0.1-0.1-0.2-0.2-0.3-0.2
|
||||
C34,87.1,33.9,87,33.8,87h-1.5V89.1z"/>
|
||||
<path class="st1" d="M41.8,91.3v1.1h-4.4V86h4.4V87h-3.1v1.5h2.7v1h-2.7v1.7H41.8z"/>
|
||||
</g>
|
||||
<path class="st3" d="M27.1,29.7c-0.8,0.2-1.6,0.3-2.4,0.3c-5.4,0-9.8-4.4-9.8-9.8c0-5.4,4.4-9.8,9.8-9.8c5.4,0,9.8,4.4,9.8,9.8
|
||||
c0,0.1,0,0.2,0,0.3c0.4-0.4,0.8-0.6,1.4-0.9C35.6,13.7,30.7,9,24.7,9c-6.2,0-11.2,5-11.2,11.2c0,6.2,5,11.2,11.2,11.2
|
||||
c1,0,2-0.2,3-0.4C27.5,30.5,27.3,30.1,27.1,29.7z"/>
|
||||
<path class="st3" d="M31.2,16.6L31.2,16.6c-0.3-0.3-0.5-0.3-0.8-0.2c0,0-4,0.5-5.7,0.5c0,0-0.1,0-0.1,0c-1.7,0-5.8-0.6-5.8-0.6
|
||||
c-0.3-0.1-0.6,0-0.8,0.3l-0.1,0.2c-0.1,0.2-0.1,0.4-0.1,0.7c0.1,0.2,0.2,0.4,0.4,0.5c0.7,0.3,3.3,1.2,4,1.4c0.1,0,0.4,0.1,0.4,0.6
|
||||
c0,0.6-0.2,3.1-0.5,4.3c-0.3,1.2-0.8,2.7-0.8,2.7c-0.1,0.4,0.1,0.9,0.5,1l0.5,0.2c0.2,0.1,0.4,0.1,0.6,0c0.2-0.1,0.3-0.3,0.4-0.5
|
||||
l1.4-4.3l1.3,4.4c0.1,0.2,0.2,0.4,0.4,0.5c0.1,0.1,0.2,0.1,0.3,0.1c0.1,0,0.2,0,0.3-0.1l0.5-0.2c0.4-0.1,0.6-0.5,0.5-0.9
|
||||
c0,0-0.4-1.6-0.7-2.9c-0.2-0.8-0.3-2-0.3-2.9c0-0.6-0.1-1.1-0.1-1.5c0-0.2,0.1-0.4,0.4-0.5c0.1,0,3.7-1.3,3.7-1.3
|
||||
c0.3-0.1,0.4-0.3,0.5-0.6C31.4,17.1,31.4,16.8,31.2,16.6z"/>
|
||||
<circle class="st3" cx="24.7" cy="14.1" r="1.7"/>
|
||||
<g>
|
||||
<polygon class="st3" points="36.4,30.1 36.4,30.1 36.4,30.1 "/>
|
||||
<path class="st3" d="M35.2,25.8l2.7-2.7c0.4-0.4,0.4-1.1,0-1.5c-0.4-0.4-1.1-0.4-1.5,0l-2.7,2.7l-2.7-2.7c-0.4-0.4-1.1-0.4-1.5,0
|
||||
c-0.4,0.4-0.4,1.1,0,1.5l2.7,2.7l-2.7,2.7c-0.4,0.4-0.4,1.1,0,1.5c0.4,0.4,1.1,0.4,1.5,0l2.7-2.7l2.7,2.7c0.4,0.4,1.1,0.4,1.5,0
|
||||
c0.4-0.4,0.4-1.1,0-1.5L35.2,25.8z"/>
|
||||
</g>
|
||||
<path class="st1" d="M27.3,129.6c-0.8,0.2-1.6,0.3-2.4,0.3c-5.4,0-9.8-4.4-9.8-9.8c0-5.4,4.4-9.8,9.8-9.8c5.4,0,9.8,4.4,9.8,9.8
|
||||
c0,0.1,0,0.2,0,0.3c0.4-0.4,0.8-0.6,1.4-0.9c-0.3-5.9-5.2-10.6-11.2-10.6c-6.2,0-11.2,5-11.2,11.2c0,6.2,5,11.2,11.2,11.2
|
||||
c1,0,2-0.2,3-0.4C27.6,130.5,27.4,130.1,27.3,129.6z"/>
|
||||
<path class="st1" d="M31.3,116.6L31.3,116.6c-0.3-0.3-0.5-0.3-0.8-0.2c0,0-4,0.5-5.7,0.5c0,0-0.1,0-0.1,0c-1.7,0-5.8-0.6-5.8-0.6
|
||||
c-0.3-0.1-0.6,0-0.8,0.3l-0.1,0.2c-0.1,0.2-0.1,0.4-0.1,0.7c0.1,0.2,0.2,0.4,0.4,0.5c0.7,0.3,3.3,1.2,4,1.4c0.1,0,0.4,0.1,0.4,0.6
|
||||
c0,0.6-0.2,3.1-0.5,4.3c-0.3,1.2-0.8,2.7-0.8,2.7c-0.1,0.4,0.1,0.9,0.5,1l0.5,0.2c0.2,0.1,0.4,0.1,0.6,0c0.2-0.1,0.3-0.3,0.4-0.5
|
||||
l1.4-4.3l1.3,4.4c0.1,0.2,0.2,0.4,0.4,0.5c0.1,0.1,0.2,0.1,0.3,0.1c0.1,0,0.2,0,0.3-0.1l0.5-0.2c0.4-0.1,0.6-0.5,0.5-0.9
|
||||
c0,0-0.4-1.6-0.7-2.9c-0.2-0.8-0.3-2-0.3-2.9c0-0.6-0.1-1.1-0.1-1.5c0-0.2,0.1-0.4,0.4-0.5c0.1,0,3.7-1.3,3.7-1.3
|
||||
c0.3-0.1,0.4-0.3,0.5-0.6C31.6,117,31.5,116.8,31.3,116.6z"/>
|
||||
<circle class="st1" cx="24.9" cy="114.1" r="1.7"/>
|
||||
<g>
|
||||
<path class="st1" d="M7.8,142.5v-6.4H9v6.4H7.8z"/>
|
||||
<path class="st1" d="M14.9,141.7c-0.5,0.5-1.1,0.8-1.7,0.8c-0.4,0-0.8-0.1-1.2-0.3c-0.4-0.2-0.7-0.4-0.9-0.7
|
||||
c-0.3-0.3-0.5-0.7-0.6-1s-0.2-0.8-0.2-1.2c0-0.4,0.1-0.9,0.2-1.2c0.2-0.4,0.4-0.7,0.6-1c0.3-0.3,0.6-0.5,1-0.7
|
||||
c0.4-0.2,0.8-0.3,1.2-0.3c0.6,0,1.1,0.1,1.5,0.4s0.7,0.6,0.9,1l-0.9,0.7c-0.2-0.3-0.4-0.6-0.6-0.7s-0.6-0.3-0.9-0.3
|
||||
c-0.3,0-0.5,0.1-0.7,0.2c-0.2,0.1-0.4,0.3-0.5,0.5c-0.2,0.2-0.3,0.4-0.4,0.7c-0.1,0.3-0.1,0.5-0.1,0.8c0,0.3,0,0.6,0.1,0.8
|
||||
c0.1,0.3,0.2,0.5,0.4,0.7c0.2,0.2,0.4,0.3,0.6,0.5c0.2,0.1,0.5,0.2,0.7,0.2c0.6,0,1.1-0.3,1.6-0.9v-0.5h-1.3v-0.9h2.3v3.3h-1V141.7
|
||||
z"/>
|
||||
<path class="st1" d="M18.4,138.4v4.1h-1.2v-6.4h1l3.3,4.2v-4.2h1.2v6.4h-1L18.4,138.4z"/>
|
||||
<path class="st1" d="M26.9,142.5c-0.5,0-0.9-0.1-1.2-0.3s-0.7-0.4-1-0.7c-0.3-0.3-0.5-0.6-0.6-1c-0.1-0.4-0.2-0.8-0.2-1.2
|
||||
c0-0.4,0.1-0.8,0.2-1.2s0.4-0.7,0.6-1c0.3-0.3,0.6-0.5,1-0.7s0.8-0.3,1.2-0.3c0.5,0,0.9,0.1,1.2,0.3s0.7,0.4,1,0.7
|
||||
c0.3,0.3,0.5,0.7,0.6,1c0.1,0.4,0.2,0.8,0.2,1.2c0,0.4-0.1,0.8-0.2,1.2c-0.2,0.4-0.4,0.7-0.6,1c-0.3,0.3-0.6,0.5-1,0.7
|
||||
C27.8,142.4,27.4,142.5,26.9,142.5z M25.2,139.3c0,0.3,0,0.5,0.1,0.8c0.1,0.3,0.2,0.5,0.4,0.7c0.2,0.2,0.3,0.4,0.6,0.5
|
||||
s0.5,0.2,0.8,0.2c0.3,0,0.5-0.1,0.8-0.2s0.4-0.3,0.6-0.5c0.1-0.2,0.3-0.4,0.3-0.7s0.1-0.5,0.1-0.8c0-0.3,0-0.5-0.1-0.8
|
||||
c-0.1-0.3-0.2-0.5-0.4-0.7c-0.2-0.2-0.3-0.4-0.6-0.5c-0.2-0.1-0.5-0.2-0.7-0.2c-0.3,0-0.5,0.1-0.8,0.2s-0.4,0.3-0.6,0.5
|
||||
c-0.2,0.2-0.3,0.4-0.3,0.7C25.2,138.7,25.2,139,25.2,139.3z"/>
|
||||
<path class="st1" d="M31.2,142.5v-6.4H34c0.3,0,0.6,0.1,0.8,0.2s0.5,0.3,0.6,0.5c0.2,0.2,0.3,0.4,0.4,0.7c0.1,0.3,0.2,0.5,0.2,0.8
|
||||
c0,0.4-0.1,0.8-0.3,1.1c-0.2,0.3-0.5,0.6-0.8,0.7l1.5,2.4H35l-1.3-2.1h-1.2v2.1H31.2z M32.4,139.2H34c0.1,0,0.2,0,0.3-0.1
|
||||
c0.1-0.1,0.2-0.1,0.3-0.2c0.1-0.1,0.1-0.2,0.2-0.3s0.1-0.3,0.1-0.4c0-0.1,0-0.3-0.1-0.4s-0.1-0.2-0.2-0.3c-0.1-0.1-0.2-0.2-0.3-0.2
|
||||
c-0.1-0.1-0.2-0.1-0.3-0.1h-1.5V139.2z"/>
|
||||
<path class="st1" d="M41.9,141.4v1.1h-4.4v-6.4h4.4v1.1h-3.1v1.5h2.7v1h-2.7v1.7H41.9z"/>
|
||||
</g>
|
||||
<g>
|
||||
<polygon class="st1" points="36.5,130.1 36.5,130.1 36.5,130.1 "/>
|
||||
<path class="st1" d="M35.3,125.8l2.7-2.7c0.4-0.4,0.4-1.1,0-1.5c-0.4-0.4-1.1-0.4-1.5,0l-2.7,2.7l-2.7-2.7c-0.4-0.4-1.1-0.4-1.5,0
|
||||
c-0.4,0.4-0.4,1.1,0,1.5l2.7,2.7l-2.7,2.7c-0.4,0.4-0.4,1.1,0,1.5c0.4,0.4,1.1,0.4,1.5,0l2.7-2.7l2.7,2.7c0.4,0.4,1.1,0.4,1.5,0
|
||||
c0.4-0.4,0.4-1.1,0-1.5L35.3,125.8z"/>
|
||||
</g>
|
||||
<g>
|
||||
<path class="st1" d="M7.7,192.7v-6.4h1.2v6.4H7.7z"/>
|
||||
<path class="st1" d="M14.7,192c-0.5,0.5-1.1,0.8-1.7,0.8c-0.4,0-0.8-0.1-1.2-0.3c-0.4-0.2-0.7-0.4-0.9-0.7c-0.3-0.3-0.5-0.7-0.6-1
|
||||
s-0.2-0.8-0.2-1.2c0-0.4,0.1-0.9,0.2-1.2c0.2-0.4,0.4-0.7,0.6-1c0.3-0.3,0.6-0.5,1-0.7c0.4-0.2,0.8-0.3,1.2-0.3
|
||||
c0.6,0,1.1,0.1,1.5,0.4s0.7,0.6,0.9,1l-0.9,0.7c-0.2-0.3-0.4-0.6-0.6-0.7s-0.6-0.3-0.9-0.3c-0.3,0-0.5,0.1-0.7,0.2
|
||||
c-0.2,0.1-0.4,0.3-0.5,0.5c-0.2,0.2-0.3,0.4-0.4,0.7c-0.1,0.3-0.1,0.5-0.1,0.8c0,0.3,0,0.6,0.1,0.8c0.1,0.3,0.2,0.5,0.4,0.7
|
||||
c0.2,0.2,0.4,0.3,0.6,0.5c0.2,0.1,0.5,0.2,0.7,0.2c0.6,0,1.1-0.3,1.6-0.9v-0.5h-1.3v-0.9h2.3v3.3h-1V192z"/>
|
||||
<path class="st1" d="M18.3,188.6v4.1h-1.2v-6.4h1l3.3,4.2v-4.2h1.2v6.4h-1L18.3,188.6z"/>
|
||||
<path class="st1" d="M26.8,192.7c-0.5,0-0.9-0.1-1.2-0.3s-0.7-0.4-1-0.7c-0.3-0.3-0.5-0.6-0.6-1c-0.1-0.4-0.2-0.8-0.2-1.2
|
||||
c0-0.4,0.1-0.8,0.2-1.2s0.4-0.7,0.6-1c0.3-0.3,0.6-0.5,1-0.7s0.8-0.3,1.2-0.3c0.5,0,0.9,0.1,1.2,0.3s0.7,0.4,1,0.7
|
||||
c0.3,0.3,0.5,0.7,0.6,1c0.1,0.4,0.2,0.8,0.2,1.2c0,0.4-0.1,0.8-0.2,1.2c-0.2,0.4-0.4,0.7-0.6,1c-0.3,0.3-0.6,0.5-1,0.7
|
||||
C27.7,192.6,27.2,192.7,26.8,192.7z M25,189.5c0,0.3,0,0.5,0.1,0.8c0.1,0.3,0.2,0.5,0.4,0.7c0.2,0.2,0.3,0.4,0.6,0.5
|
||||
s0.5,0.2,0.8,0.2c0.3,0,0.5-0.1,0.8-0.2s0.4-0.3,0.6-0.5c0.1-0.2,0.3-0.4,0.3-0.7s0.1-0.5,0.1-0.8c0-0.3,0-0.5-0.1-0.8
|
||||
c-0.1-0.3-0.2-0.5-0.4-0.7c-0.2-0.2-0.3-0.4-0.6-0.5c-0.2-0.1-0.5-0.2-0.7-0.2c-0.3,0-0.5,0.1-0.8,0.2s-0.4,0.3-0.6,0.5
|
||||
c-0.2,0.2-0.3,0.4-0.3,0.7C25.1,189,25,189.2,25,189.5z"/>
|
||||
<path class="st1" d="M31,192.7v-6.4h2.8c0.3,0,0.6,0.1,0.8,0.2s0.5,0.3,0.6,0.5c0.2,0.2,0.3,0.4,0.4,0.7c0.1,0.3,0.2,0.5,0.2,0.8
|
||||
c0,0.4-0.1,0.8-0.3,1.1c-0.2,0.3-0.5,0.6-0.8,0.7l1.5,2.4h-1.4l-1.3-2.1h-1.2v2.1H31z M32.3,189.5h1.6c0.1,0,0.2,0,0.3-0.1
|
||||
c0.1-0.1,0.2-0.1,0.3-0.2c0.1-0.1,0.1-0.2,0.2-0.3s0.1-0.3,0.1-0.4c0-0.1,0-0.3-0.1-0.4s-0.1-0.2-0.2-0.3c-0.1-0.1-0.2-0.2-0.3-0.2
|
||||
c-0.1-0.1-0.2-0.1-0.3-0.1h-1.5V189.5z"/>
|
||||
<path class="st1" d="M41.8,191.6v1.1h-4.4v-6.4h4.4v1.1h-3.1v1.5h2.7v1h-2.7v1.7H41.8z"/>
|
||||
</g>
|
||||
<path class="st1" d="M27.1,179.9c-0.8,0.2-1.6,0.3-2.4,0.3c-5.4,0-9.8-4.4-9.8-9.8c0-5.4,4.4-9.8,9.8-9.8c5.4,0,9.8,4.4,9.8,9.8
|
||||
c0,0.1,0,0.2,0,0.3c0.4-0.4,0.8-0.6,1.4-0.9c-0.3-5.9-5.2-10.6-11.2-10.6c-6.2,0-11.2,5-11.2,11.2c0,6.2,5,11.2,11.2,11.2
|
||||
c1,0,2-0.2,3-0.4C27.5,180.7,27.3,180.3,27.1,179.9z"/>
|
||||
<path class="st1" d="M31.2,166.8L31.2,166.8c-0.3-0.3-0.5-0.3-0.8-0.2c0,0-4,0.5-5.7,0.5c0,0-0.1,0-0.1,0c-1.7,0-5.8-0.6-5.8-0.6
|
||||
c-0.3-0.1-0.6,0-0.8,0.3l-0.1,0.2c-0.1,0.2-0.1,0.4-0.1,0.7c0.1,0.2,0.2,0.4,0.4,0.5c0.7,0.3,3.3,1.2,4,1.4c0.1,0,0.4,0.1,0.4,0.6
|
||||
c0,0.6-0.2,3.1-0.5,4.3c-0.3,1.2-0.8,2.7-0.8,2.7c-0.1,0.4,0.1,0.9,0.5,1l0.5,0.2c0.2,0.1,0.4,0.1,0.6,0c0.2-0.1,0.3-0.3,0.4-0.5
|
||||
l1.4-4.3l1.3,4.4c0.1,0.2,0.2,0.4,0.4,0.5c0.1,0.1,0.2,0.1,0.3,0.1c0.1,0,0.2,0,0.3-0.1l0.5-0.2c0.4-0.1,0.6-0.5,0.5-0.9
|
||||
c0,0-0.4-1.6-0.7-2.9c-0.2-0.8-0.3-2-0.3-2.9c0-0.6-0.1-1.1-0.1-1.5c0-0.2,0.1-0.4,0.4-0.5c0.1,0,3.7-1.3,3.7-1.3
|
||||
c0.3-0.1,0.4-0.3,0.5-0.6C31.4,167.3,31.4,167,31.2,166.8z"/>
|
||||
<circle class="st1" cx="24.7" cy="164.3" r="1.7"/>
|
||||
<g>
|
||||
<polygon class="st1" points="36.4,180.3 36.4,180.3 36.4,180.3 "/>
|
||||
<path class="st1" d="M35.2,176l2.7-2.7c0.4-0.4,0.4-1.1,0-1.5c-0.4-0.4-1.1-0.4-1.5,0l-2.7,2.7l-2.7-2.7c-0.4-0.4-1.1-0.4-1.5,0
|
||||
c-0.4,0.4-0.4,1.1,0,1.5l2.7,2.7l-2.7,2.7c-0.4,0.4-0.4,1.1,0,1.5c0.4,0.4,1.1,0.4,1.5,0l2.7-2.7l2.7,2.7c0.4,0.4,1.1,0.4,1.5,0
|
||||
c0.4-0.4,0.4-1.1,0-1.5L35.2,176z"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 14 KiB |
140
interface/resources/icons/tablet-icons/kick.svg
Normal file
|
@ -0,0 +1,140 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 19.2.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 50 200.1" style="enable-background:new 0 0 50 200.1;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#414042;}
|
||||
.st1{fill:#FFFFFF;}
|
||||
.st2{fill:#1E1E1E;}
|
||||
.st3{fill:#333333;}
|
||||
</style>
|
||||
<g id="Layer_2">
|
||||
<g>
|
||||
<g>
|
||||
<path class="st0" d="M50.1,146.1c0,2.2-1.8,4-4,4h-42c-2.2,0-4-1.8-4-4v-42c0-2.2,1.8-4,4-4h42c2.2,0,4,1.8,4,4V146.1z"/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<path class="st0" d="M50,196.1c0,2.2-1.8,4-4,4H4c-2.2,0-4-1.8-4-4v-42c0-2.2,1.8-4,4-4h42c2.2,0,4,1.8,4,4V196.1z"/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<path class="st1" d="M50,46c0,2.2-1.8,4-4,4H4c-2.2,0-4-1.8-4-4V4c0-2.2,1.8-4,4-4h42c2.2,0,4,1.8,4,4V46z"/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<path class="st2" d="M50,96.1c0,2.2-1.8,4-4,4H4c-2.2,0-4-1.8-4-4v-42c0-2.2,1.8-4,4-4h42c2.2,0,4,1.8,4,4V96.1z"/>
|
||||
</g>
|
||||
</g>
|
||||
<path class="st1" d="M24.7,81.2c-6.2,0-11.2-5-11.2-11.2c0-6.2,5-11.2,11.2-11.2c6.2,0,11.2,5,11.2,11.2
|
||||
C35.9,76.2,30.9,81.2,24.7,81.2z M24.7,60.3c-5.4,0-9.8,4.4-9.8,9.8c0,5.4,4.4,9.8,9.8,9.8c5.4,0,9.8-4.4,9.8-9.8
|
||||
C34.5,64.6,30.1,60.3,24.7,60.3z"/>
|
||||
<path class="st1" d="M27.1,79.5c-0.8,0.2-1.6,0.3-2.4,0.3c-5.4,0-9.8-4.4-9.8-9.8c0-5.4,4.4-9.8,9.8-9.8c5.4,0,9.8,4.4,9.8,9.8
|
||||
c0,0.1,0,0.2,0,0.3c0.4-0.4,0.8-0.6,1.4-0.9c-0.3-5.9-5.2-10.6-11.2-10.6c-6.2,0-11.2,5-11.2,11.2c0,6.2,5,11.2,11.2,11.2
|
||||
c1,0,2-0.2,3-0.4C27.5,80.4,27.3,80,27.1,79.5z"/>
|
||||
<path class="st1" d="M31.2,66.5L31.2,66.5c-0.3-0.3-0.5-0.3-0.8-0.2c0,0-4,0.5-5.7,0.5c0,0-0.1,0-0.1,0c-1.7,0-5.8-0.6-5.8-0.6
|
||||
c-0.3-0.1-0.6,0-0.8,0.3l-0.1,0.2c-0.1,0.2-0.1,0.4-0.1,0.7c0.1,0.2,0.2,0.4,0.4,0.5c0.7,0.3,3.3,1.2,4,1.4c0.1,0,0.4,0.1,0.4,0.6
|
||||
c0,0.6-0.2,3.1-0.5,4.3c-0.3,1.2-0.8,2.7-0.8,2.7c-0.1,0.4,0.1,0.9,0.5,1l0.5,0.2c0.2,0.1,0.4,0.1,0.6,0c0.2-0.1,0.3-0.3,0.4-0.5
|
||||
l1.4-4.3l1.3,4.4c0.1,0.2,0.2,0.4,0.4,0.5c0.1,0.1,0.2,0.1,0.3,0.1c0.1,0,0.2,0,0.3-0.1l0.5-0.2c0.4-0.1,0.6-0.5,0.5-0.9
|
||||
c0,0-0.4-1.6-0.7-2.9c-0.2-0.8-0.3-2-0.3-2.9c0-0.6-0.1-1.1-0.1-1.5c0-0.2,0.1-0.4,0.4-0.5c0.1,0,3.7-1.3,3.7-1.3
|
||||
c0.3-0.1,0.4-0.3,0.5-0.6C31.4,66.9,31.4,66.7,31.2,66.5z"/>
|
||||
<circle class="st1" cx="24.7" cy="64" r="1.7"/>
|
||||
<path class="st3" d="M27.1,29.7c-0.8,0.2-1.6,0.3-2.4,0.3c-5.4,0-9.8-4.4-9.8-9.8c0-5.4,4.4-9.8,9.8-9.8c5.4,0,9.8,4.4,9.8,9.8
|
||||
c0,0.1,0,0.2,0,0.3c0.4-0.4,0.8-0.6,1.4-0.9C35.6,13.7,30.7,9,24.7,9c-6.2,0-11.2,5-11.2,11.2c0,6.2,5,11.2,11.2,11.2
|
||||
c1,0,2-0.2,3-0.4C27.5,30.5,27.3,30.1,27.1,29.7z"/>
|
||||
<path class="st3" d="M31.2,16.6L31.2,16.6c-0.3-0.3-0.5-0.3-0.8-0.2c0,0-4,0.5-5.7,0.5c0,0-0.1,0-0.1,0c-1.7,0-5.8-0.6-5.8-0.6
|
||||
c-0.3-0.1-0.6,0-0.8,0.3l-0.1,0.2c-0.1,0.2-0.1,0.4-0.1,0.7c0.1,0.2,0.2,0.4,0.4,0.5c0.7,0.3,3.3,1.2,4,1.4c0.1,0,0.4,0.1,0.4,0.6
|
||||
c0,0.6-0.2,3.1-0.5,4.3c-0.3,1.2-0.8,2.7-0.8,2.7c-0.1,0.4,0.1,0.9,0.5,1l0.5,0.2c0.2,0.1,0.4,0.1,0.6,0c0.2-0.1,0.3-0.3,0.4-0.5
|
||||
l1.4-4.3l1.3,4.4c0.1,0.2,0.2,0.4,0.4,0.5c0.1,0.1,0.2,0.1,0.3,0.1c0.1,0,0.2,0,0.3-0.1l0.5-0.2c0.4-0.1,0.6-0.5,0.5-0.9
|
||||
c0,0-0.4-1.6-0.7-2.9c-0.2-0.8-0.3-2-0.3-2.9c0-0.6-0.1-1.1-0.1-1.5c0-0.2,0.1-0.4,0.4-0.5c0.1,0,3.7-1.3,3.7-1.3
|
||||
c0.3-0.1,0.4-0.3,0.5-0.6C31.4,17.1,31.4,16.8,31.2,16.6z"/>
|
||||
<circle class="st3" cx="24.7" cy="14.1" r="1.7"/>
|
||||
<g>
|
||||
<polygon class="st3" points="36.4,30.1 36.4,30.1 36.4,30.1 "/>
|
||||
<path class="st3" d="M35.2,25.8l2.7-2.7c0.4-0.4,0.4-1.1,0-1.5c-0.4-0.4-1.1-0.4-1.5,0l-2.7,2.7l-2.7-2.7c-0.4-0.4-1.1-0.4-1.5,0
|
||||
c-0.4,0.4-0.4,1.1,0,1.5l2.7,2.7l-2.7,2.7c-0.4,0.4-0.4,1.1,0,1.5c0.4,0.4,1.1,0.4,1.5,0l2.7-2.7l2.7,2.7c0.4,0.4,1.1,0.4,1.5,0
|
||||
c0.4-0.4,0.4-1.1,0-1.5L35.2,25.8z"/>
|
||||
</g>
|
||||
<path class="st1" d="M27.3,129.6c-0.8,0.2-1.6,0.3-2.4,0.3c-5.4,0-9.8-4.4-9.8-9.8c0-5.4,4.4-9.8,9.8-9.8c5.4,0,9.8,4.4,9.8,9.8
|
||||
c0,0.1,0,0.2,0,0.3c0.4-0.4,0.8-0.6,1.4-0.9c-0.3-5.9-5.2-10.6-11.2-10.6c-6.2,0-11.2,5-11.2,11.2c0,6.2,5,11.2,11.2,11.2
|
||||
c1,0,2-0.2,3-0.4C27.6,130.5,27.4,130.1,27.3,129.6z"/>
|
||||
<path class="st1" d="M31.3,116.6L31.3,116.6c-0.3-0.3-0.5-0.3-0.8-0.2c0,0-4,0.5-5.7,0.5c0,0-0.1,0-0.1,0c-1.7,0-5.8-0.6-5.8-0.6
|
||||
c-0.3-0.1-0.6,0-0.8,0.3l-0.1,0.2c-0.1,0.2-0.1,0.4-0.1,0.7c0.1,0.2,0.2,0.4,0.4,0.5c0.7,0.3,3.3,1.2,4,1.4c0.1,0,0.4,0.1,0.4,0.6
|
||||
c0,0.6-0.2,3.1-0.5,4.3c-0.3,1.2-0.8,2.7-0.8,2.7c-0.1,0.4,0.1,0.9,0.5,1l0.5,0.2c0.2,0.1,0.4,0.1,0.6,0c0.2-0.1,0.3-0.3,0.4-0.5
|
||||
l1.4-4.3l1.3,4.4c0.1,0.2,0.2,0.4,0.4,0.5c0.1,0.1,0.2,0.1,0.3,0.1c0.1,0,0.2,0,0.3-0.1l0.5-0.2c0.4-0.1,0.6-0.5,0.5-0.9
|
||||
c0,0-0.4-1.6-0.7-2.9c-0.2-0.8-0.3-2-0.3-2.9c0-0.6-0.1-1.1-0.1-1.5c0-0.2,0.1-0.4,0.4-0.5c0.1,0,3.7-1.3,3.7-1.3
|
||||
c0.3-0.1,0.4-0.3,0.5-0.6C31.6,117,31.5,116.8,31.3,116.6z"/>
|
||||
<circle class="st1" cx="24.9" cy="114.1" r="1.7"/>
|
||||
<g>
|
||||
<polygon class="st1" points="36.5,130.1 36.5,130.1 36.5,130.1 "/>
|
||||
<path class="st1" d="M35.3,125.8l2.7-2.7c0.4-0.4,0.4-1.1,0-1.5c-0.4-0.4-1.1-0.4-1.5,0l-2.7,2.7l-2.7-2.7c-0.4-0.4-1.1-0.4-1.5,0
|
||||
c-0.4,0.4-0.4,1.1,0,1.5l2.7,2.7l-2.7,2.7c-0.4,0.4-0.4,1.1,0,1.5c0.4,0.4,1.1,0.4,1.5,0l2.7-2.7l2.7,2.7c0.4,0.4,1.1,0.4,1.5,0
|
||||
c0.4-0.4,0.4-1.1,0-1.5L35.3,125.8z"/>
|
||||
</g>
|
||||
<path class="st1" d="M27.1,179.9c-0.8,0.2-1.6,0.3-2.4,0.3c-5.4,0-9.8-4.4-9.8-9.8c0-5.4,4.4-9.8,9.8-9.8c5.4,0,9.8,4.4,9.8,9.8
|
||||
c0,0.1,0,0.2,0,0.3c0.4-0.4,0.8-0.6,1.4-0.9c-0.3-5.9-5.2-10.6-11.2-10.6c-6.2,0-11.2,5-11.2,11.2c0,6.2,5,11.2,11.2,11.2
|
||||
c1,0,2-0.2,3-0.4C27.5,180.7,27.3,180.3,27.1,179.9z"/>
|
||||
<path class="st1" d="M31.2,166.8L31.2,166.8c-0.3-0.3-0.5-0.3-0.8-0.2c0,0-4,0.5-5.7,0.5c0,0-0.1,0-0.1,0c-1.7,0-5.8-0.6-5.8-0.6
|
||||
c-0.3-0.1-0.6,0-0.8,0.3l-0.1,0.2c-0.1,0.2-0.1,0.4-0.1,0.7c0.1,0.2,0.2,0.4,0.4,0.5c0.7,0.3,3.3,1.2,4,1.4c0.1,0,0.4,0.1,0.4,0.6
|
||||
c0,0.6-0.2,3.1-0.5,4.3c-0.3,1.2-0.8,2.7-0.8,2.7c-0.1,0.4,0.1,0.9,0.5,1l0.5,0.2c0.2,0.1,0.4,0.1,0.6,0c0.2-0.1,0.3-0.3,0.4-0.5
|
||||
l1.4-4.3l1.3,4.4c0.1,0.2,0.2,0.4,0.4,0.5c0.1,0.1,0.2,0.1,0.3,0.1c0.1,0,0.2,0,0.3-0.1l0.5-0.2c0.4-0.1,0.6-0.5,0.5-0.9
|
||||
c0,0-0.4-1.6-0.7-2.9c-0.2-0.8-0.3-2-0.3-2.9c0-0.6-0.1-1.1-0.1-1.5c0-0.2,0.1-0.4,0.4-0.5c0.1,0,3.7-1.3,3.7-1.3
|
||||
c0.3-0.1,0.4-0.3,0.5-0.6C31.4,167.3,31.4,167,31.2,166.8z"/>
|
||||
<circle class="st1" cx="24.7" cy="164.3" r="1.7"/>
|
||||
<g>
|
||||
<polygon class="st1" points="36.4,180.3 36.4,180.3 36.4,180.3 "/>
|
||||
<path class="st1" d="M35.2,176l2.7-2.7c0.4-0.4,0.4-1.1,0-1.5c-0.4-0.4-1.1-0.4-1.5,0l-2.7,2.7l-2.7-2.7c-0.4-0.4-1.1-0.4-1.5,0
|
||||
c-0.4,0.4-0.4,1.1,0,1.5l2.7,2.7l-2.7,2.7c-0.4,0.4-0.4,1.1,0,1.5c0.4,0.4,1.1,0.4,1.5,0l2.7-2.7l2.7,2.7c0.4,0.4,1.1,0.4,1.5,0
|
||||
c0.4-0.4,0.4-1.1,0-1.5L35.2,176z"/>
|
||||
</g>
|
||||
<g>
|
||||
<path class="st1" d="M14.7,92.3v-6.4h1.2v3.1l2.8-3.1H20l-2.4,2.8l2.6,3.6h-1.3l-2-2.9l-0.9,0.9v1.9H14.7z"/>
|
||||
<path class="st1" d="M20.9,92.3v-6.4h1.2v6.4H20.9z"/>
|
||||
<path class="st1" d="M23.3,89.1c0-0.4,0.1-0.8,0.2-1.2c0.1-0.4,0.3-0.7,0.6-1c0.3-0.3,0.6-0.5,1-0.7s0.8-0.3,1.3-0.3
|
||||
c0.6,0,1.1,0.1,1.5,0.4c0.4,0.3,0.7,0.6,0.9,1l-1,0.7c-0.1-0.2-0.2-0.3-0.3-0.5c-0.1-0.1-0.2-0.2-0.4-0.3c-0.1-0.1-0.3-0.1-0.4-0.2
|
||||
c-0.1,0-0.3,0-0.4,0c-0.3,0-0.6,0.1-0.8,0.2s-0.4,0.3-0.6,0.5c-0.1,0.2-0.3,0.4-0.3,0.7s-0.1,0.5-0.1,0.8c0,0.3,0,0.6,0.1,0.8
|
||||
c0.1,0.3,0.2,0.5,0.4,0.7c0.2,0.2,0.3,0.4,0.6,0.5c0.2,0.1,0.5,0.2,0.7,0.2c0.1,0,0.3,0,0.4-0.1c0.1,0,0.3-0.1,0.4-0.2
|
||||
c0.1-0.1,0.3-0.2,0.4-0.3c0.1-0.1,0.2-0.3,0.3-0.4l1,0.6c-0.1,0.2-0.2,0.5-0.4,0.6c-0.2,0.2-0.4,0.3-0.6,0.5
|
||||
c-0.2,0.1-0.5,0.2-0.7,0.3c-0.3,0.1-0.5,0.1-0.8,0.1c-0.4,0-0.9-0.1-1.2-0.3c-0.4-0.2-0.7-0.4-1-0.8c-0.3-0.3-0.5-0.7-0.6-1.1
|
||||
C23.4,89.9,23.3,89.5,23.3,89.1z"/>
|
||||
<path class="st1" d="M30,92.3v-6.4h1.2v3.1l2.8-3.1h1.3l-2.4,2.8l2.6,3.6h-1.3l-2-2.9l-0.9,0.9v1.9H30z"/>
|
||||
</g>
|
||||
<g>
|
||||
<path class="st3" d="M14.7,42.6v-6.4h1.2v3.1l2.8-3.1H20l-2.4,2.8l2.6,3.6h-1.3l-2-2.9l-0.9,0.9v1.9H14.7z"/>
|
||||
<path class="st3" d="M20.9,42.6v-6.4h1.2v6.4H20.9z"/>
|
||||
<path class="st3" d="M23.3,39.4c0-0.4,0.1-0.8,0.2-1.2c0.1-0.4,0.3-0.7,0.6-1c0.3-0.3,0.6-0.5,1-0.7s0.8-0.3,1.3-0.3
|
||||
c0.6,0,1.1,0.1,1.5,0.4c0.4,0.3,0.7,0.6,0.9,1l-1,0.7c-0.1-0.2-0.2-0.3-0.3-0.5c-0.1-0.1-0.2-0.2-0.4-0.3c-0.1-0.1-0.3-0.1-0.4-0.2
|
||||
c-0.1,0-0.3,0-0.4,0c-0.3,0-0.6,0.1-0.8,0.2S25.2,37.8,25,38c-0.1,0.2-0.3,0.4-0.3,0.7s-0.1,0.5-0.1,0.8c0,0.3,0,0.6,0.1,0.8
|
||||
c0.1,0.3,0.2,0.5,0.4,0.7c0.2,0.2,0.3,0.4,0.6,0.5c0.2,0.1,0.5,0.2,0.7,0.2c0.1,0,0.3,0,0.4-0.1c0.1,0,0.3-0.1,0.4-0.2
|
||||
c0.1-0.1,0.3-0.2,0.4-0.3c0.1-0.1,0.2-0.3,0.3-0.4l1,0.6c-0.1,0.2-0.2,0.5-0.4,0.6c-0.2,0.2-0.4,0.3-0.6,0.5
|
||||
c-0.2,0.1-0.5,0.2-0.7,0.3c-0.3,0.1-0.5,0.1-0.8,0.1c-0.4,0-0.9-0.1-1.2-0.3c-0.4-0.2-0.7-0.4-1-0.8c-0.3-0.3-0.5-0.7-0.6-1.1
|
||||
C23.4,40.2,23.3,39.8,23.3,39.4z"/>
|
||||
<path class="st3" d="M30,42.6v-6.4h1.2v3.1l2.8-3.1h1.3l-2.4,2.8l2.6,3.6h-1.3l-2-2.9l-0.9,0.9v1.9H30z"/>
|
||||
</g>
|
||||
<g>
|
||||
<path class="st1" d="M14.8,142.4v-6.4H16v3.1l2.8-3.1h1.3l-2.4,2.8l2.6,3.6h-1.3l-2-2.9l-0.9,0.9v1.9H14.8z"/>
|
||||
<path class="st1" d="M21,142.4v-6.4h1.2v6.4H21z"/>
|
||||
<path class="st1" d="M23.4,139.2c0-0.4,0.1-0.8,0.2-1.2c0.1-0.4,0.3-0.7,0.6-1c0.3-0.3,0.6-0.5,1-0.7c0.4-0.2,0.8-0.3,1.3-0.3
|
||||
c0.6,0,1.1,0.1,1.5,0.4c0.4,0.3,0.7,0.6,0.9,1l-1,0.7c-0.1-0.2-0.2-0.3-0.3-0.5c-0.1-0.1-0.2-0.2-0.4-0.3c-0.1-0.1-0.3-0.1-0.4-0.2
|
||||
c-0.1,0-0.3,0-0.4,0c-0.3,0-0.6,0.1-0.8,0.2s-0.4,0.3-0.6,0.5c-0.1,0.2-0.3,0.4-0.3,0.7s-0.1,0.5-0.1,0.8c0,0.3,0,0.6,0.1,0.8
|
||||
c0.1,0.3,0.2,0.5,0.4,0.7c0.2,0.2,0.3,0.4,0.6,0.5c0.2,0.1,0.5,0.2,0.7,0.2c0.1,0,0.3,0,0.4-0.1c0.1,0,0.3-0.1,0.4-0.2
|
||||
s0.3-0.2,0.4-0.3c0.1-0.1,0.2-0.3,0.3-0.4l1,0.6c-0.1,0.2-0.2,0.5-0.4,0.6c-0.2,0.2-0.4,0.3-0.6,0.5c-0.2,0.1-0.5,0.2-0.7,0.3
|
||||
c-0.3,0.1-0.5,0.1-0.8,0.1c-0.4,0-0.9-0.1-1.2-0.3c-0.4-0.2-0.7-0.4-1-0.8c-0.3-0.3-0.5-0.7-0.6-1.1
|
||||
C23.5,140,23.4,139.6,23.4,139.2z"/>
|
||||
<path class="st1" d="M30.1,142.4v-6.4h1.2v3.1l2.8-3.1h1.3l-2.4,2.8l2.6,3.6h-1.3l-2-2.9l-0.9,0.9v1.9H30.1z"/>
|
||||
</g>
|
||||
<g>
|
||||
<path class="st1" d="M14.7,192.7v-6.4h1.2v3.1l2.8-3.1H20l-2.4,2.8l2.6,3.6h-1.3l-2-2.9l-0.9,0.9v1.9H14.7z"/>
|
||||
<path class="st1" d="M20.9,192.7v-6.4h1.2v6.4H20.9z"/>
|
||||
<path class="st1" d="M23.3,189.4c0-0.4,0.1-0.8,0.2-1.2c0.1-0.4,0.3-0.7,0.6-1c0.3-0.3,0.6-0.5,1-0.7c0.4-0.2,0.8-0.3,1.3-0.3
|
||||
c0.6,0,1.1,0.1,1.5,0.4s0.7,0.6,0.9,1l-1,0.7c-0.1-0.2-0.2-0.3-0.3-0.5c-0.1-0.1-0.2-0.2-0.4-0.3s-0.3-0.1-0.4-0.2
|
||||
c-0.1,0-0.3,0-0.4,0c-0.3,0-0.6,0.1-0.8,0.2s-0.4,0.3-0.6,0.5c-0.1,0.2-0.3,0.4-0.3,0.7s-0.1,0.5-0.1,0.8c0,0.3,0,0.6,0.1,0.8
|
||||
c0.1,0.3,0.2,0.5,0.4,0.7c0.2,0.2,0.3,0.4,0.6,0.5c0.2,0.1,0.5,0.2,0.7,0.2c0.1,0,0.3,0,0.4-0.1c0.1,0,0.3-0.1,0.4-0.2
|
||||
c0.1-0.1,0.3-0.2,0.4-0.3c0.1-0.1,0.2-0.3,0.3-0.4l1,0.6c-0.1,0.2-0.2,0.5-0.4,0.6c-0.2,0.2-0.4,0.3-0.6,0.5
|
||||
c-0.2,0.1-0.5,0.2-0.7,0.3c-0.3,0.1-0.5,0.1-0.8,0.1c-0.4,0-0.9-0.1-1.2-0.3c-0.4-0.2-0.7-0.4-1-0.8c-0.3-0.3-0.5-0.7-0.6-1.1
|
||||
C23.4,190.2,23.3,189.8,23.3,189.4z"/>
|
||||
<path class="st1" d="M29.9,192.7v-6.4h1.2v3.1l2.8-3.1h1.3l-2.4,2.8l2.6,3.6h-1.3l-2-2.9l-0.9,0.9v1.9H29.9z"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 10 KiB |
23
interface/resources/icons/tablet-icons/market-i.svg
Normal file
|
@ -0,0 +1,23 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 19.2.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 50 50" style="enable-background:new 0 0 50 50;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#FFFFFF;}
|
||||
</style>
|
||||
<g id="Layer_2">
|
||||
</g>
|
||||
<g id="Layer_1">
|
||||
<g>
|
||||
<path class="st0" d="M45.4,13.7c-1.6-0.1-3.2,0-4.8,0c-7.5,0-18,0-25.7,0c-0.6-1.8-1-3.5-1.7-5.2C13,7.9,12.2,7.1,11.5,7
|
||||
C9.4,6.7,7.3,6.7,5.2,6.7c-1.1,0-1.9,0.5-1.9,1.7c0,1.2,0.8,1.7,1.9,1.7c1.3,0,2.7,0,4,0.1c0.5,0.1,1.2,0.6,1.4,1.1
|
||||
c0.9,2.6,1.7,5.2,2.5,7.8c1.3,4.3,1.8,5.5,3.1,10.2c0.6,2.3,1.2,2.8,2.2,3.3c1.1,0.4,2.1,0.4,2.1,0.4h1.8c4.6,0,12.2,0,16.8,0
|
||||
c1.1,0,2.1-0.1,2.6-1.4c1.9-5.1,3.8-10.2,5.7-15.3C47.8,14.7,47.1,13.8,45.4,13.7z M38.9,28.7c-0.1,0.3-0.8,0.7-1.2,0.7
|
||||
c-4.6,0-12.2,0-16.8,0c-0.4,0-1.1-0.3-1.2-0.7c-1.3-3.8-2.4-7.6-3.7-11.5h27.1C41.8,21.2,40.4,24.9,38.9,28.7z"/>
|
||||
<path class="st0" d="M17.2,37.3L17.2,37.3c-1,0-1.7,0.2-2.2,0.7c-0.6,0.6-0.9,1.3-0.9,2.3c0,1,0.3,1.8,0.8,2.4
|
||||
c0.6,0.6,1.4,0.9,2.2,0.9c1.8,0,3.2-1.4,3.2-3.2c0-0.9-0.3-1.7-0.9-2.3C18.8,37.6,18,37.3,17.2,37.3z"/>
|
||||
<path class="st0" d="M35.1,37.3L35.1,37.3c-1,0-1.7,0.2-2.2,0.7c-0.6,0.6-0.9,1.4-0.9,2.4c0,1,0.3,1.8,0.8,2.4
|
||||
c0.6,0.6,1.3,0.9,2.2,0.9c1.8,0,3.2-1.5,3.2-3.3c0-0.9-0.3-1.6-0.9-2.2C36.8,37.6,36,37.3,35.1,37.3z"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 1.4 KiB |
17
interface/resources/icons/tablet-icons/menu-i.svg
Normal file
|
@ -0,0 +1,17 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 19.2.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 50 50" style="enable-background:new 0 0 50 50;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#FFFFFF;}
|
||||
</style>
|
||||
<g id="Layer_2">
|
||||
</g>
|
||||
<g id="Layer_1">
|
||||
<path class="st0" d="M37,18.5H14.1c-0.9,0-1.7-0.7-1.7-1.7c0-0.9,0.8-1.7,1.7-1.7H37c0.9,0,1.7,0.7,1.7,1.7
|
||||
C38.6,17.8,37.9,18.5,37,18.5z"/>
|
||||
<path class="st0" d="M37,28H14.1c-0.9,0-1.7-0.8-1.7-1.7c0-0.9,0.8-1.7,1.7-1.7H37c0.9,0,1.7,0.8,1.7,1.7C38.6,27.3,37.9,28,37,28z
|
||||
"/>
|
||||
<path class="st0" d="M37,37.4H14.1c-0.9,0-1.7-0.8-1.7-1.7s0.8-1.7,1.7-1.7H37c0.9,0,1.7,0.8,1.7,1.7S37.9,37.4,37,37.4z"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 835 B |
21
interface/resources/icons/tablet-icons/mic-a.svg
Normal file
|
@ -0,0 +1,21 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 19.2.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 50 50" style="enable-background:new 0 0 50 50;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#FFFFFF;}
|
||||
</style>
|
||||
<g id="Layer_2">
|
||||
</g>
|
||||
<g id="Layer_1">
|
||||
<path class="st0" d="M28.9,17.1v-0.5c0-2-1.7-3.6-3.7-3.6h0c-2,0-3.7,1.6-3.7,3.6v6.9L28.9,17.1z"/>
|
||||
<path class="st0" d="M21.5,29.2v0.2c0,2,1.6,3.6,3.7,3.6h0c2,0,3.7-1.6,3.7-3.6v-6.6L21.5,29.2z"/>
|
||||
<path class="st0" d="M39.1,16.8L13.6,39.1c-0.7,0.6-1.8,0.5-2.4-0.2l-0.2-0.2c-0.6-0.7-0.5-1.8,0.2-2.4l25.4-22.4
|
||||
c0.7-0.6,1.8-0.5,2.4,0.2l0.2,0.2C39.8,15.1,39.7,16.1,39.1,16.8z"/>
|
||||
<path class="st0" d="M23.4,40.2l0,3.4h-4.3c-1,0-1.8,0.8-1.8,1.8c0,1,0.8,1.8,1.8,1.8h12.3c1,0,1.8-0.8,1.8-1.8
|
||||
c0-1-0.8-1.8-1.8-1.8h-4.4l0-3.4c5.2-0.8,9.2-5,9.2-10.1c0-0.1,0-5.1,0-5.3c0-1-0.9-1.7-1.8-1.7c-1,0-1.7,0.8-1.7,1.8
|
||||
c0,0.3,0,4.8,0,5.2c0,3.7-3.4,6.7-7.5,6.7c-3.6,0-6.7-2.3-7.3-5.4L15,34C16.4,37.2,19.6,39.7,23.4,40.2z"/>
|
||||
<path class="st0" d="M17.7,24.9c0-1-0.7-1.8-1.6-1.8c-1-0.1-1.8,0.7-1.9,1.6c0,0.2,0,4.2,0,5.3l3.5-3.1
|
||||
C17.7,25.9,17.7,25,17.7,24.9z"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 1.3 KiB |
22
interface/resources/icons/tablet-icons/mic-i.svg
Normal file
|
@ -0,0 +1,22 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 19.2.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 50 50" style="enable-background:new 0 0 50 50;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#FFFFFF;}
|
||||
</style>
|
||||
<g id="Layer_2">
|
||||
</g>
|
||||
<g id="Layer_1">
|
||||
<path class="st0" d="M31.4,14.1l2.2-2.2c-2.1-2.5-5.3-4.1-8.8-4.1c-3.4,0-6.4,1.5-8.5,3.8c0.7,0.7,1.5,1.5,2.2,2.2
|
||||
c1.6-1.7,3.8-2.9,6.3-2.9C27.5,10.9,29.9,12.1,31.4,14.1z"/>
|
||||
<path class="st0" d="M36.5,9l2.2-2.2C35.3,3,30.3,0.6,24.8,0.6c-5.3,0-10.2,2.3-13.6,5.9c0.7,0.7,1.5,1.5,2.2,2.2
|
||||
c2.9-3,6.9-5,11.4-5C29.5,3.7,33.6,5.8,36.5,9z"/>
|
||||
<path class="st0" d="M28.5,22.7v-4.4c0-2-1.6-3.6-3.7-3.6h0c-2,0-3.7,1.6-3.7,3.6v4.4H28.5z"/>
|
||||
<path class="st0" d="M21.1,26.5v4.3c0,2,1.7,3.6,3.7,3.6h0c2,0,3.7-1.6,3.7-3.6v-4.3H21.1z"/>
|
||||
<path class="st0" d="M36,31.5c0-0.1,0-5.1,0-5.3c0-1-0.9-1.7-1.8-1.7c-1,0-1.7,0.8-1.7,1.8c0,0.3,0,4.8,0,5.2
|
||||
c0,3.7-3.4,6.7-7.5,6.7c-4.1,0-7.5-3-7.5-6.7c0-0.4,0-4.9,0-5.3c0-1-0.7-1.8-1.6-1.8C14.9,24.3,14,25,14,26c0,0.3,0,5.4,0,5.5
|
||||
c0,5.1,4,9.3,9.2,10.1l0,3.4h-4.3c-1,0-1.8,0.8-1.8,1.8c0,1,0.8,1.8,1.8,1.8h12.3c1,0,1.8-0.8,1.8-1.8c0-1-0.8-1.8-1.8-1.8h-4.4
|
||||
l0-3.4C32,40.7,36,36.6,36,31.5z"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 1.3 KiB |
22
interface/resources/icons/tablet-icons/people-i.svg
Normal file
|
@ -0,0 +1,22 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 19.2.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 50 50" style="enable-background:new 0 0 50 50;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#FFFFFF;}
|
||||
</style>
|
||||
<g id="Layer_2">
|
||||
</g>
|
||||
<g id="Layer_1">
|
||||
<circle class="st0" cx="25.6" cy="14.8" r="8.1"/>
|
||||
<path class="st0" d="M31.4,27h-11c-4.6,0-8.2,3.9-8.2,8.5v4.3c3.8,2.8,8.1,4.5,13.1,4.5c5.6,0,10.6-2.1,14.4-5.6v-3.2
|
||||
C39.6,30.9,35.9,27,31.4,27z"/>
|
||||
<circle class="st0" cx="41.6" cy="17.1" r="3.5"/>
|
||||
<path class="st0" d="M43.9,23.9h-4.1c-0.9,0-1.7,0.4-2.3,1c1.2,0.6,2.3,1.6,3.1,2.5c1,1.2,1.5,2.7,1.7,4.3c0.3,0.9,0.4,1.8,0.2,2.8
|
||||
v0.6c1.6-2.2,3.3-6,4-8.1v-0.3C46.5,25.7,45.3,23.9,43.9,23.9z"/>
|
||||
<circle class="st0" cx="9.4" cy="18" r="3.5"/>
|
||||
<path class="st0" d="M8.5,35.7c-0.1-0.7-0.1-1.4,0-2.1l0.1-0.2c0-0.2,0-0.4,0-0.6c0-0.2,0-0.3,0.1-0.5c0-0.2,0-0.3,0-0.5
|
||||
c0.2-2.1,1.6-4.4,3.2-5.7c0.4-0.4,0.9-0.6,1.4-0.9c-0.5-0.5-1.3-0.7-2-0.7H7c-1.4,0-2.6,1.8-2.4,2.7v0.3
|
||||
C5.1,29.8,6.8,33.5,8.5,35.7z"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 1.2 KiB |
21
interface/resources/icons/tablet-icons/scripts-i.svg
Normal file
|
@ -0,0 +1,21 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 19.2.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 50 50" style="enable-background:new 0 0 50 50;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#FFFFFF;}
|
||||
</style>
|
||||
<g id="Layer_2">
|
||||
</g>
|
||||
<g id="Layer_1">
|
||||
<g>
|
||||
<path class="st0" d="M29.1,45H12c-3.5,0-6.4-1.8-8.4-5.1c-1.4-2.4-1.9-4.7-1.9-4.8l-0.3-1.6h10.4V4.8h35.8v27
|
||||
c0.1,0.8,0.5,6.6-2.4,10c-1.4,1.6-3.3,2.4-5.6,2.4c-4.8,0-7.2-2.6-8.3-4.7c-0.6-1.2-1-2.3-1.1-3.2H5c0.3,0.7,0.6,1.5,1.1,2.3
|
||||
c1.5,2.4,3.5,3.6,5.9,3.6h17.1c0.8,0,1.4,0.6,1.4,1.4C30.5,44.3,29.9,45,29.1,45z M14.6,33.5h18l0.1,1.3c0,0,0.1,1.7,1,3.4
|
||||
c1.2,2.1,3.1,3.2,5.8,3.2c1.5,0,2.7-0.5,3.5-1.4c1.2-1.3,1.6-3.3,1.7-4.8c0.2-1.7,0-3.1,0-3.1l0-0.1l0-0.1V7.6H14.6V33.5z"/>
|
||||
<path class="st0" d="M39.6,14.3H19.7c-0.8,0-1.5-0.7-1.5-1.5s0.7-1.5,1.5-1.5h19.9c0.8,0,1.5,0.7,1.5,1.5S40.4,14.3,39.6,14.3z"/>
|
||||
<path class="st0" d="M39.6,21.3H19.7c-0.8,0-1.5-0.7-1.5-1.6s0.7-1.6,1.5-1.6h19.9c0.8,0,1.5,0.7,1.5,1.6S40.4,21.3,39.6,21.3z"/>
|
||||
<path class="st0" d="M39.6,28.1H19.7c-0.8,0-1.5-0.7-1.5-1.6s0.7-1.6,1.5-1.6h19.9c0.8,0,1.5,0.7,1.5,1.6S40.4,28.1,39.6,28.1z"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 1.3 KiB |
18
interface/resources/icons/tablet-icons/snap-i.svg
Normal file
|
@ -0,0 +1,18 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 19.2.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 50 50" style="enable-background:new 0 0 50 50;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#FFFFFF;}
|
||||
</style>
|
||||
<g id="Layer_2">
|
||||
</g>
|
||||
<g id="Layer_1">
|
||||
<g>
|
||||
<path class="st0" d="M38.5,16.3h-3.7v-1.6c0-2.4-1.6-3-4-3H18.7c-2.4,0-3.6,0.5-3.6,3v1.6h-3.6c-2.4,0-4.5,1-4.5,3.5V36
|
||||
c0,2.4,1.9,4.6,4.5,4.6h27.1c2.4,0,4.2-2.7,4.2-5.2V19.7C42.8,17.3,41,16.3,38.5,16.3z M25.2,37c-5.5,0-9.9-4.5-9.9-9.9
|
||||
c0-5.5,4.5-9.9,9.9-9.9c5.5,0,9.9,4.5,9.9,9.9S30.8,37,25.2,37z"/>
|
||||
<circle class="st0" cx="25.2" cy="27" r="5.9"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 804 B |
16
interface/resources/icons/tablet-icons/switch-a.svg
Normal file
|
@ -0,0 +1,16 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 19.2.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 50 50" style="enable-background:new 0 0 50 50;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#FFFFFF;}
|
||||
</style>
|
||||
<g id="Layer_2">
|
||||
</g>
|
||||
<g id="Layer_1">
|
||||
<path class="st0" d="M42.2,6.2H8.1c-3.5,0-6.4,3.1-6.4,6.8v18c0,3.8,2.9,6.8,6.4,6.8h15.3v3.5h-8.9c-0.8,0-1.5,0.9-1.5,1.8
|
||||
c0,0.9,0.7,1.8,1.5,1.8h21.9c0.8,0,1.5-0.9,1.5-1.8c0-0.9-0.7-1.8-1.5-1.8h-9.6v-3.4c-0.3,0-0.6,0-0.9,0h16.3
|
||||
c3.5,0,6.4-3.1,6.4-6.8V13C48.6,9.2,45.7,6.2,42.2,6.2z M45.2,31c0,1.7-1.3,3.1-2.9,3.1H8.1c-1.6,0-2.9-1.4-2.9-3.1V13
|
||||
c0-1.7,1.3-3.1,2.9-3.1h34.2c1.6,0,2.9,1.4,2.9,3.1V31z"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 842 B |
15
interface/resources/icons/tablet-icons/switch-i.svg
Normal file
|
@ -0,0 +1,15 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 19.2.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 50 50" style="enable-background:new 0 0 50 50;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#FFFFFF;}
|
||||
</style>
|
||||
<g id="Layer_2">
|
||||
</g>
|
||||
<g id="Layer_1">
|
||||
<path class="st0" d="M25.4,28.7c1.7,0,2.8,1.8,3.8,3.6c0.5,0.8,1.6,2.6,2,2.6h7.5c4.2,0,7.6-3.8,7.6-8.4v-5.4
|
||||
c0-4.6-3.4-8.4-7.6-8.4H11.9c-4.2,0-7.6,3.8-7.6,8.4v5.4c0,4.6,3.4,8.4,7.6,8.4h7.3c0.6,0,1.7-1.6,2.4-2.7
|
||||
C22.6,30.5,23.7,28.7,25.4,28.7C25.3,28.7,25.4,28.7,25.4,28.7z"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 717 B |
30
interface/resources/icons/tablet-icons/users-i.svg
Normal file
|
@ -0,0 +1,30 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 19.2.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 50 50" style="enable-background:new 0 0 50 50;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#FFFFFF;}
|
||||
</style>
|
||||
<g id="Layer_2">
|
||||
</g>
|
||||
<g id="Layer_1">
|
||||
<g>
|
||||
<path class="st0" d="M22.6,14.8c-0.1-0.9-0.1-1.8,0.1-2.6h-8.6c-0.9,0-1.7,0.8-1.7,1.8s0.8,1.8,1.7,1.8h8.7
|
||||
C22.7,15.4,22.7,15.1,22.6,14.8z"/>
|
||||
<path class="st0" d="M26.5,22.3c-0.6-0.5-1.1-1-1.6-1.6H14.1c-0.9,0-1.7,0.8-1.7,1.8c0,1,0.8,1.8,1.7,1.8h8.2
|
||||
c0.3-0.2,0.6-0.3,0.9-0.4c1.2-0.8,2.5-1.2,3.9-1.3C26.9,22.6,26.7,22.5,26.5,22.3z"/>
|
||||
<path class="st0" d="M17.9,30.3c0.1-0.3,0.1-0.5,0.2-0.7h-4c-0.9,0-1.7,0.8-1.7,1.8c0,1,0.8,1.8,1.7,1.8h3.2
|
||||
C17.3,32.2,17.5,31.2,17.9,30.3z"/>
|
||||
<path class="st0" d="M5.9,15.7H4.5c-0.9,0-1.7-0.8-1.7-1.8s0.8-1.8,1.7-1.8h1.4c0.9,0,1.7,0.8,1.7,1.8S6.8,15.7,5.9,15.7z"/>
|
||||
<path class="st0" d="M5.9,24.4H4.5c-0.9,0-1.7-0.8-1.7-1.8c0-1,0.8-1.8,1.7-1.8h1.4c0.9,0,1.7,0.8,1.7,1.8
|
||||
C7.6,23.6,6.8,24.4,5.9,24.4z"/>
|
||||
<path class="st0" d="M5.9,33.2H4.5c-0.9,0-1.7-0.8-1.7-1.8s0.8-1.8,1.7-1.8h1.4c0.9,0,1.7,0.8,1.7,1.8S6.8,33.2,5.9,33.2z"/>
|
||||
<path class="st0" d="M17.3,38.3h-3.2c-0.9,0-1.7,0.8-1.7,1.8s0.8,1.8,1.7,1.8h3.2V38.3z"/>
|
||||
<path class="st0" d="M5.9,42H4.5c-0.9,0-1.7-0.8-1.7-1.8c0-1,0.8-1.8,1.7-1.8h1.4c0.9,0,1.7,0.8,1.7,1.8C7.6,41.1,6.8,42,5.9,42z"
|
||||
/>
|
||||
<circle class="st0" cx="34.5" cy="14.3" r="7.8"/>
|
||||
<path class="st0" d="M23.9,44.1c10.4,0,19.5-4.4,23.3-13.4c-1.3-2.7-3.9-4.7-7.1-4.7H29.5c-4.4,0-8.2,3.8-8.2,8.3v9.7
|
||||
C22.2,44,23,44.1,23.9,44.1z"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 1.7 KiB |
18
interface/resources/icons/tablet-mute-icon.svg
Normal file
|
@ -0,0 +1,18 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 19.2.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 40 40" style="enable-background:new 0 0 40 40;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#FFFFFF;}
|
||||
</style>
|
||||
<path class="st0" d="M24.6,12.5l1.6-1.6C24.7,9.2,22.4,8,19.9,8c-2.4,0-4.6,1-6.1,2.7c0.5,0.5,1,1,1.6,1.6c1.1-1.2,2.7-2,4.5-2
|
||||
C21.8,10.2,23.5,11.1,24.6,12.5z"/>
|
||||
<path class="st0" d="M28.3,8.9l1.6-1.6c-2.4-2.7-6-4.4-9.9-4.4c-3.8,0-7.3,1.6-9.7,4.2c0.5,0.5,1,1,1.6,1.6c2-2.2,4.9-3.5,8.1-3.5
|
||||
C23.2,5.1,26.2,6.6,28.3,8.9z"/>
|
||||
<path class="st0" d="M22.5,18.7v-3.1c0-1.5-1.2-2.6-2.6-2.6h0c-1.5,0-2.6,1.1-2.6,2.6v3.1H22.5z"/>
|
||||
<path class="st0" d="M17.3,21.4v3c0,1.5,1.2,2.6,2.7,2.6h0c1.5,0,2.6-1.2,2.6-2.6v-3H17.3z"/>
|
||||
<path class="st0" d="M27.9,24.9c0,0,0-3.6,0-3.8c0-0.7-0.6-1.2-1.3-1.2c-0.7,0-1.2,0.6-1.2,1.3c0,0.2,0,3.4,0,3.7
|
||||
c0,2.6-2.4,4.8-5.3,4.8c-2.9,0-5.3-2.1-5.3-4.8c0-0.3,0-3.5,0-3.8c0-0.7-0.5-1.3-1.2-1.3c-0.7,0-1.3,0.5-1.3,1.2c0,0.2,0,3.9,0,3.9
|
||||
c0,3.6,2.9,6.6,6.6,7.2l0,2.4h-3.1c-0.7,0-1.3,0.6-1.3,1.3c0,0.7,0.6,1.3,1.3,1.3h8.8c0.7,0,1.3-0.6,1.3-1.3c0-0.7-0.6-1.3-1.3-1.3
|
||||
h-3.2l0-2.4C25,31.6,27.9,28.6,27.9,24.9z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.3 KiB |
19
interface/resources/icons/tablet-unmute-icon.svg
Normal file
|
@ -0,0 +1,19 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 19.2.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 40 40" style="enable-background:new 0 0 40 40;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#FFFFFF;}
|
||||
</style>
|
||||
<path class="st0" d="M17.3,18.4v6c0,1.5,1.2,2.6,2.7,2.6h0c1.5,0,2.6-1.2,2.6-2.6v-6H17.3z"/>
|
||||
<path class="st0" d="M27.9,24.9c0,0,0-3.6,0-3.8c0-0.7-0.6-1.2-1.3-1.2c-0.7,0-1.2,0.6-1.2,1.3c0,0.2,0,3.4,0,3.7
|
||||
c0,2.6-2.4,4.8-5.3,4.8c-2.9,0-5.3-2.1-5.3-4.8c0-0.3,0-3.5,0-3.8c0-0.7-0.5-1.3-1.2-1.3c-0.7,0-1.3,0.5-1.3,1.2c0,0.2,0,3.9,0,3.9
|
||||
c0,3.6,2.9,6.6,6.6,7.2l0,2.4h-3.1c-0.7,0-1.3,0.6-1.3,1.3c0,0.7,0.6,1.3,1.3,1.3h8.8c0.7,0,1.3-0.6,1.3-1.3c0-0.7-0.6-1.3-1.3-1.3
|
||||
h-3.2l0-2.4C25,31.6,27.9,28.6,27.9,24.9z"/>
|
||||
<g>
|
||||
<polygon class="st0" points="23.4,15.8 23.4,15.8 23.4,15.8 "/>
|
||||
<path class="st0" d="M21.9,10.5l3.4-3.4c0.5-0.5,0.5-1.4,0-1.9c-0.5-0.5-1.4-0.5-1.9,0L20,8.6l-3.4-3.4c-0.5-0.5-1.4-0.5-1.9,0
|
||||
s-0.5,1.4,0,1.9l3.4,3.4l-3.4,3.4c-0.5,0.5-0.5,1.4,0,1.9c0.5,0.5,1.4,0.5,1.9,0l3.4-3.4l3.4,3.4c0.5,0.5,1.4,0.5,1.9,0
|
||||
c0.5-0.5,0.5-1.4,0-1.9L21.9,10.5z"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 1.2 KiB |
BIN
interface/resources/meshes/tablet-home-button.fbx
Normal file
BIN
interface/resources/meshes/tablet-stylus-fat.fbx
Normal file
BIN
interface/resources/meshes/tablet-stylus-skinny.fbx
Normal file
BIN
interface/resources/meshes/tablet-with-home-button.fbx
Normal file
|
@ -187,9 +187,6 @@ Window {
|
|||
ToolbarButton {
|
||||
id: homeButton
|
||||
imageURL: "../images/home.svg"
|
||||
buttonState: 1
|
||||
defaultState: 1
|
||||
hoverState: 2
|
||||
onClicked: {
|
||||
addressBarDialog.loadHome();
|
||||
root.shown = false;
|
||||
|
@ -204,9 +201,6 @@ Window {
|
|||
ToolbarButton {
|
||||
id: backArrow;
|
||||
imageURL: "../images/backward.svg";
|
||||
hoverState: addressBarDialog.backEnabled ? 2 : 0;
|
||||
defaultState: addressBarDialog.backEnabled ? 1 : 0;
|
||||
buttonState: addressBarDialog.backEnabled ? 1 : 0;
|
||||
onClicked: addressBarDialog.loadBack();
|
||||
anchors {
|
||||
left: homeButton.right
|
||||
|
@ -216,9 +210,6 @@ Window {
|
|||
ToolbarButton {
|
||||
id: forwardArrow;
|
||||
imageURL: "../images/forward.svg";
|
||||
hoverState: addressBarDialog.forwardEnabled ? 2 : 0;
|
||||
defaultState: addressBarDialog.forwardEnabled ? 1 : 0;
|
||||
buttonState: addressBarDialog.forwardEnabled ? 1 : 0;
|
||||
onClicked: addressBarDialog.loadForward();
|
||||
anchors {
|
||||
left: backArrow.right
|
||||
|
|
|
@ -12,7 +12,6 @@ import QtQuick 2.5
|
|||
import QtQuick.Controls 1.4
|
||||
|
||||
import "../dialogs"
|
||||
import "../menus"
|
||||
import "../js/Utils.js" as Utils
|
||||
|
||||
// This is our primary 'desktop' object to which all VR dialogs and windows are childed.
|
||||
|
@ -465,32 +464,7 @@ FocusScope {
|
|||
Component { id: fileDialogBuilder; FileDialog { } }
|
||||
function fileDialog(properties) {
|
||||
return fileDialogBuilder.createObject(desktop, properties);
|
||||
}
|
||||
|
||||
MenuMouseHandler { id: menuPopperUpper }
|
||||
function popupMenu(point) {
|
||||
menuPopperUpper.popup(desktop, rootMenu.items, point);
|
||||
}
|
||||
|
||||
function toggleMenu(point) {
|
||||
menuPopperUpper.toggle(desktop, rootMenu.items, point);
|
||||
}
|
||||
|
||||
Keys.onEscapePressed: {
|
||||
if (menuPopperUpper.closeLastMenu()) {
|
||||
event.accepted = true;
|
||||
return;
|
||||
}
|
||||
event.accepted = false;
|
||||
}
|
||||
|
||||
Keys.onLeftPressed: {
|
||||
if (menuPopperUpper.closeLastMenu()) {
|
||||
event.accepted = true;
|
||||
return;
|
||||
}
|
||||
event.accepted = false;
|
||||
}
|
||||
}
|
||||
|
||||
function unfocusWindows() {
|
||||
// First find the active focus item, and unfocus it, all the way
|
||||
|
|
|
@ -48,14 +48,7 @@ OriginalDesktop.Desktop {
|
|||
// This used to create sysToolbar dynamically with a call to getToolbar() within onCompleted.
|
||||
// Beginning with QT 5.6, this stopped working, as anything added to toolbars too early got
|
||||
// wiped during startup.
|
||||
Toolbar {
|
||||
id: sysToolbar;
|
||||
objectName: "com.highfidelity.interface.toolbar.system";
|
||||
anchors.horizontalCenter: settings.constrainToolbarToCenterX ? desktop.horizontalCenter : undefined;
|
||||
// Literal 50 is overwritten by settings from previous session, and sysToolbar.x comes from settings when not constrained.
|
||||
x: sysToolbar.x
|
||||
y: 50
|
||||
}
|
||||
|
||||
Settings {
|
||||
id: settings;
|
||||
category: "toolbar";
|
||||
|
@ -65,7 +58,6 @@ OriginalDesktop.Desktop {
|
|||
settings.constrainToolbarToCenterX = constrain;
|
||||
}
|
||||
property var toolbars: (function (map) { // answer dictionary preloaded with sysToolbar
|
||||
map[sysToolbar.objectName] = sysToolbar;
|
||||
return map; })({});
|
||||
|
||||
|
||||
|
@ -74,27 +66,6 @@ OriginalDesktop.Desktop {
|
|||
WebEngine.settings.javascriptCanAccessClipboard = false;
|
||||
WebEngine.settings.spatialNavigationEnabled = false;
|
||||
WebEngine.settings.localContentCanAccessRemoteUrls = true;
|
||||
|
||||
[ // Allocate the standard buttons in the correct order. They will get images, etc., via scripts.
|
||||
"hmdToggle", "mute", "pal", "bubble", "help",
|
||||
"hudToggle",
|
||||
"com.highfidelity.interface.system.editButton", "marketplace", "snapshot", "goto"
|
||||
].forEach(function (name) {
|
||||
sysToolbar.addButton({objectName: name});
|
||||
});
|
||||
var toggleHudButton = sysToolbar.findButton("hudToggle");
|
||||
toggleHudButton.imageURL = "../../../icons/hud.svg";
|
||||
toggleHudButton.pinned = true;
|
||||
sysToolbar.updatePinned(); // automatic when adding buttons only IFF button is pinned at creation.
|
||||
|
||||
toggleHudButton.buttonState = Qt.binding(function(){
|
||||
return desktop.pinned ? 1 : 0
|
||||
});
|
||||
toggleHudButton.clicked.connect(function(){
|
||||
console.log("Clicked on hud button")
|
||||
var overlayMenuItem = "Overlays"
|
||||
MenuInterface.setIsOptionChecked(overlayMenuItem, !MenuInterface.isOptionChecked(overlayMenuItem));
|
||||
});
|
||||
}
|
||||
|
||||
// Accept a download through the webview
|
||||
|
|
|
@ -15,7 +15,13 @@ import "../styles-uit"
|
|||
|
||||
Item {
|
||||
property alias text: popupText.text
|
||||
property alias headerGlyph: headerGlyph.text
|
||||
property alias headerText: headerText.text
|
||||
property real popupRadius: hifi.dimensions.borderRadius
|
||||
property real headerTextPixelSize: 22
|
||||
property real popupTextPixelSize: 16
|
||||
FontLoader { id: ralewayRegular; source: "../../fonts/Raleway-Regular.ttf"; }
|
||||
FontLoader { id: ralewaySemiBold; source: "../../fonts/Raleway-SemiBold.ttf"; }
|
||||
visible: false
|
||||
id: letterbox
|
||||
anchors.fill: parent
|
||||
|
@ -27,19 +33,79 @@ Item {
|
|||
}
|
||||
Rectangle {
|
||||
width: Math.max(parent.width * 0.75, 400)
|
||||
height: popupText.contentHeight*1.5
|
||||
height: contentContainer.height + 50
|
||||
anchors.centerIn: parent
|
||||
radius: popupRadius
|
||||
color: "white"
|
||||
FiraSansSemiBold {
|
||||
id: popupText
|
||||
size: hifi.fontSizes.textFieldInput
|
||||
color: hifi.colors.darkGray
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: 15
|
||||
anchors.rightMargin: 15
|
||||
wrapMode: Text.WordWrap
|
||||
Item {
|
||||
id: contentContainer
|
||||
width: parent.width - 60
|
||||
height: childrenRect.height
|
||||
anchors.centerIn: parent
|
||||
Item {
|
||||
id: popupHeaderContainer
|
||||
visible: headerText.text !== "" || headerGlyph.text !== ""
|
||||
height: 30
|
||||
// Anchors
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
// Header Glyph
|
||||
HiFiGlyphs {
|
||||
id: headerGlyph
|
||||
visible: headerGlyph.text !== ""
|
||||
// Size
|
||||
height: parent.height
|
||||
// Anchors
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: -15
|
||||
// Text Size
|
||||
size: headerTextPixelSize*2.5
|
||||
// Style
|
||||
horizontalAlignment: Text.AlignHLeft
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
color: hifi.colors.darkGray
|
||||
}
|
||||
// Header Text
|
||||
Text {
|
||||
id: headerText
|
||||
visible: headerText.text !== ""
|
||||
// Size
|
||||
height: parent.height
|
||||
// Anchors
|
||||
anchors.left: headerGlyph.right
|
||||
anchors.leftMargin: -5
|
||||
// Text Size
|
||||
font.pixelSize: headerTextPixelSize
|
||||
// Style
|
||||
font.family: ralewaySemiBold.name
|
||||
color: hifi.colors.darkGray
|
||||
horizontalAlignment: Text.AlignHLeft
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
wrapMode: Text.WordWrap
|
||||
textFormat: Text.StyledText
|
||||
}
|
||||
}
|
||||
// Popup Text
|
||||
Text {
|
||||
id: popupText
|
||||
// Size
|
||||
width: parent.width
|
||||
// Anchors
|
||||
anchors.top: popupHeaderContainer.visible ? popupHeaderContainer.bottom : parent.top
|
||||
anchors.topMargin: popupHeaderContainer.visible ? 15 : 0
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
// Text alignment
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
horizontalAlignment: Text.AlignHLeft
|
||||
// Style
|
||||
font.pixelSize: popupTextPixelSize
|
||||
font.family: ralewayRegular.name
|
||||
color: hifi.colors.darkGray
|
||||
wrapMode: Text.WordWrap
|
||||
textFormat: Text.StyledText
|
||||
}
|
||||
}
|
||||
}
|
||||
MouseArea {
|
||||
|
|
|
@ -10,35 +10,37 @@
|
|||
//
|
||||
|
||||
import QtQuick 2.5
|
||||
import QtQuick.Controls 1.4
|
||||
import QtQuick.Controls.Styles 1.4
|
||||
import QtGraphicalEffects 1.0
|
||||
import "../styles-uit"
|
||||
|
||||
Row {
|
||||
Item {
|
||||
id: thisNameCard
|
||||
// Spacing
|
||||
spacing: 10
|
||||
// Anchors
|
||||
anchors.top: parent.top
|
||||
anchors {
|
||||
topMargin: (parent.height - contentHeight)/2
|
||||
bottomMargin: (parent.height - contentHeight)/2
|
||||
verticalCenter: parent.verticalCenter
|
||||
leftMargin: 10
|
||||
rightMargin: 10
|
||||
}
|
||||
|
||||
// Properties
|
||||
property int contentHeight: 50
|
||||
property string uuid: ""
|
||||
property string displayName: ""
|
||||
property string userName: ""
|
||||
property int displayTextHeight: 18
|
||||
property real displayNameTextPixelSize: 18
|
||||
property int usernameTextHeight: 12
|
||||
property real audioLevel: 0.0
|
||||
property bool isMyCard: false
|
||||
property bool selected: false
|
||||
property bool isAdmin: false
|
||||
property bool currentlyEditingDisplayName: false
|
||||
|
||||
/* User image commented out for now - will probably be re-introduced later.
|
||||
Column {
|
||||
id: avatarImage
|
||||
// Size
|
||||
height: contentHeight
|
||||
height: parent.height
|
||||
width: height
|
||||
Image {
|
||||
id: userImage
|
||||
|
@ -49,26 +51,187 @@ Row {
|
|||
}
|
||||
}
|
||||
*/
|
||||
Column {
|
||||
Item {
|
||||
id: textContainer
|
||||
// Size
|
||||
width: parent.width - /*avatarImage.width - */parent.anchors.leftMargin - parent.anchors.rightMargin - parent.spacing
|
||||
height: contentHeight
|
||||
width: parent.width - /*avatarImage.width - parent.spacing - */parent.anchors.leftMargin - parent.anchors.rightMargin
|
||||
height: childrenRect.height
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
// DisplayName Text
|
||||
FiraSansSemiBold {
|
||||
id: displayNameText
|
||||
// Properties
|
||||
text: thisNameCard.displayName
|
||||
elide: Text.ElideRight
|
||||
// DisplayName field for my card
|
||||
Rectangle {
|
||||
id: myDisplayName
|
||||
visible: isMyCard
|
||||
// Size
|
||||
width: parent.width + 70
|
||||
height: 35
|
||||
// Anchors
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: -10
|
||||
// Style
|
||||
color: hifi.colors.textFieldLightBackground
|
||||
border.color: hifi.colors.blueHighlight
|
||||
border.width: 0
|
||||
TextInput {
|
||||
id: myDisplayNameText
|
||||
// Properties
|
||||
text: thisNameCard.displayName
|
||||
maximumLength: 256
|
||||
clip: true
|
||||
// Size
|
||||
width: parent.width
|
||||
height: parent.height
|
||||
// Anchors
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: 10
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: editGlyph.width + editGlyph.anchors.rightMargin
|
||||
// Style
|
||||
color: hifi.colors.darkGray
|
||||
FontLoader { id: firaSansSemiBold; source: "../../fonts/FiraSans-SemiBold.ttf"; }
|
||||
font.family: firaSansSemiBold.name
|
||||
font.pixelSize: displayNameTextPixelSize
|
||||
selectionColor: hifi.colors.blueHighlight
|
||||
selectedTextColor: "black"
|
||||
// Text Positioning
|
||||
verticalAlignment: TextInput.AlignVCenter
|
||||
horizontalAlignment: TextInput.AlignLeft
|
||||
// Signals
|
||||
onEditingFinished: {
|
||||
pal.sendToScript({method: 'displayNameUpdate', params: text})
|
||||
cursorPosition = 0
|
||||
focus = false
|
||||
myDisplayName.border.width = 0
|
||||
color = hifi.colors.darkGray
|
||||
currentlyEditingDisplayName = false
|
||||
}
|
||||
}
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.LeftButton
|
||||
hoverEnabled: true
|
||||
onClicked: {
|
||||
myDisplayName.border.width = 1
|
||||
myDisplayNameText.focus ? myDisplayNameText.cursorPosition = myDisplayNameText.positionAt(mouseX, mouseY, TextInput.CursorOnCharacter) : myDisplayNameText.selectAll();
|
||||
myDisplayNameText.focus = true
|
||||
myDisplayNameText.color = "black"
|
||||
currentlyEditingDisplayName = true
|
||||
}
|
||||
onDoubleClicked: {
|
||||
myDisplayNameText.selectAll();
|
||||
myDisplayNameText.focus = true;
|
||||
currentlyEditingDisplayName = true
|
||||
}
|
||||
onEntered: myDisplayName.color = hifi.colors.lightGrayText
|
||||
onExited: myDisplayName.color = hifi.colors.textFieldLightBackground
|
||||
}
|
||||
// Edit pencil glyph
|
||||
HiFiGlyphs {
|
||||
id: editGlyph
|
||||
text: hifi.glyphs.editPencil
|
||||
// Text Size
|
||||
size: displayNameTextPixelSize*1.5
|
||||
// Anchors
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: 5
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
// Style
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
color: hifi.colors.baseGray
|
||||
}
|
||||
}
|
||||
// Spacer for DisplayName for my card
|
||||
Item {
|
||||
id: myDisplayNameSpacer
|
||||
width: 1
|
||||
height: 4
|
||||
// Anchors
|
||||
anchors.top: myDisplayName.bottom
|
||||
}
|
||||
// DisplayName container for others' cards
|
||||
Item {
|
||||
id: displayNameContainer
|
||||
visible: !isMyCard
|
||||
// Size
|
||||
width: parent.width
|
||||
// Text Size
|
||||
size: thisNameCard.displayTextHeight
|
||||
// Text Positioning
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
// Style
|
||||
color: hifi.colors.darkGray
|
||||
height: displayNameTextPixelSize + 4
|
||||
// Anchors
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
// DisplayName Text for others' cards
|
||||
FiraSansSemiBold {
|
||||
id: displayNameText
|
||||
// Properties
|
||||
text: thisNameCard.displayName
|
||||
elide: Text.ElideRight
|
||||
// Size
|
||||
width: isAdmin ? Math.min(displayNameTextMetrics.tightBoundingRect.width + 8, parent.width - adminLabelText.width - adminLabelQuestionMark.width + 8) : parent.width
|
||||
// Anchors
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
// Text Size
|
||||
size: displayNameTextPixelSize
|
||||
// Text Positioning
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
// Style
|
||||
color: hifi.colors.darkGray
|
||||
}
|
||||
TextMetrics {
|
||||
id: displayNameTextMetrics
|
||||
font: displayNameText.font
|
||||
text: displayNameText.text
|
||||
}
|
||||
// "ADMIN" label for other users' cards
|
||||
RalewaySemiBold {
|
||||
id: adminLabelText
|
||||
visible: isAdmin
|
||||
text: "ADMIN"
|
||||
// Text size
|
||||
size: displayNameText.size - 4
|
||||
// Anchors
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.left: displayNameText.right
|
||||
// Style
|
||||
font.capitalization: Font.AllUppercase
|
||||
color: hifi.colors.redHighlight
|
||||
// Alignment
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignTop
|
||||
}
|
||||
// This Rectangle refers to the [?] popup button next to "ADMIN"
|
||||
Item {
|
||||
id: adminLabelQuestionMark
|
||||
visible: isAdmin
|
||||
// Size
|
||||
width: 20
|
||||
height: displayNameText.height
|
||||
// Anchors
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.left: adminLabelText.right
|
||||
RalewayRegular {
|
||||
id: adminLabelQuestionMarkText
|
||||
text: "[?]"
|
||||
size: adminLabelText.size
|
||||
font.capitalization: Font.AllUppercase
|
||||
color: hifi.colors.redHighlight
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
anchors.fill: parent
|
||||
}
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.LeftButton
|
||||
hoverEnabled: true
|
||||
onClicked: letterbox(hifi.glyphs.question,
|
||||
"Domain Admin",
|
||||
"This user is an admin on this domain. Admins can <b>Silence</b> and <b>Ban</b> other users at their discretion - so be extra nice!")
|
||||
onEntered: adminLabelQuestionMarkText.color = "#94132e"
|
||||
onExited: adminLabelQuestionMarkText.color = hifi.colors.redHighlight
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// UserName Text
|
||||
|
@ -80,6 +243,8 @@ Row {
|
|||
visible: thisNameCard.displayName
|
||||
// Size
|
||||
width: parent.width
|
||||
// Anchors
|
||||
anchors.top: isMyCard ? myDisplayNameSpacer.bottom : displayNameContainer.bottom
|
||||
// Text Size
|
||||
size: thisNameCard.usernameTextHeight
|
||||
// Text Positioning
|
||||
|
@ -90,25 +255,56 @@ Row {
|
|||
|
||||
// Spacer
|
||||
Item {
|
||||
id: userNameSpacer
|
||||
height: 4
|
||||
width: parent.width
|
||||
// Anchors
|
||||
anchors.top: userNameText.bottom
|
||||
}
|
||||
|
||||
// VU Meter
|
||||
Rectangle { // CHANGEME to the appropriate type!
|
||||
Rectangle {
|
||||
id: nameCardVUMeter
|
||||
// Size
|
||||
width: parent.width
|
||||
width: isMyCard ? myDisplayName.width - 70 : ((gainSlider.value - gainSlider.minimumValue)/(gainSlider.maximumValue - gainSlider.minimumValue)) * parent.width
|
||||
height: 8
|
||||
// Anchors
|
||||
anchors.top: userNameSpacer.bottom
|
||||
// Style
|
||||
radius: 4
|
||||
color: "#c5c5c5"
|
||||
// Rectangle for the zero-gain point on the VU meter
|
||||
Rectangle {
|
||||
id: vuMeterZeroGain
|
||||
visible: gainSlider.visible
|
||||
// Size
|
||||
width: 4
|
||||
height: 18
|
||||
// Style
|
||||
color: hifi.colors.darkGray
|
||||
// Anchors
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: (-gainSlider.minimumValue)/(gainSlider.maximumValue - gainSlider.minimumValue) * gainSlider.width - 4
|
||||
}
|
||||
// Rectangle for the VU meter line
|
||||
Rectangle {
|
||||
id: vuMeterLine
|
||||
width: gainSlider.width
|
||||
visible: gainSlider.visible
|
||||
// Style
|
||||
color: vuMeterBase.color
|
||||
radius: nameCardVUMeter.radius
|
||||
height: nameCardVUMeter.height / 2
|
||||
anchors.verticalCenter: nameCardVUMeter.verticalCenter
|
||||
}
|
||||
// Rectangle for the VU meter base
|
||||
Rectangle {
|
||||
id: vuMeterBase
|
||||
// Anchors
|
||||
anchors.fill: parent
|
||||
// Style
|
||||
color: "#c5c5c5"
|
||||
color: parent.color
|
||||
radius: parent.radius
|
||||
}
|
||||
// Rectangle for the VU meter audio level
|
||||
|
@ -117,7 +313,7 @@ Row {
|
|||
// Size
|
||||
width: (thisNameCard.audioLevel) * parent.width
|
||||
// Style
|
||||
color: "#c5c5c5"
|
||||
color: parent.color
|
||||
radius: parent.radius
|
||||
// Anchors
|
||||
anchors.bottom: parent.bottom
|
||||
|
@ -138,5 +334,73 @@ Row {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Per-Avatar Gain Slider
|
||||
Slider {
|
||||
id: gainSlider
|
||||
// Size
|
||||
width: parent.width
|
||||
height: 14
|
||||
// Anchors
|
||||
anchors.verticalCenter: nameCardVUMeter.verticalCenter
|
||||
// Properties
|
||||
visible: !isMyCard && selected
|
||||
value: pal.gainSliderValueDB[uuid] ? pal.gainSliderValueDB[uuid] : 0.0
|
||||
minimumValue: -60.0
|
||||
maximumValue: 20.0
|
||||
stepSize: 5
|
||||
updateValueWhileDragging: true
|
||||
onValueChanged: updateGainFromQML(uuid, value, false)
|
||||
onPressedChanged: {
|
||||
if (!pressed) {
|
||||
updateGainFromQML(uuid, value, true)
|
||||
}
|
||||
}
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onWheel: {
|
||||
// Do nothing.
|
||||
}
|
||||
onDoubleClicked: {
|
||||
gainSlider.value = 0.0
|
||||
}
|
||||
onPressed: {
|
||||
// Pass through to Slider
|
||||
mouse.accepted = false
|
||||
}
|
||||
onReleased: {
|
||||
// the above mouse.accepted seems to make this
|
||||
// never get called, nonetheless...
|
||||
mouse.accepted = false
|
||||
}
|
||||
}
|
||||
style: SliderStyle {
|
||||
groove: Rectangle {
|
||||
color: "#c5c5c5"
|
||||
implicitWidth: gainSlider.width
|
||||
implicitHeight: 4
|
||||
radius: 2
|
||||
opacity: 0
|
||||
}
|
||||
handle: Rectangle {
|
||||
anchors.centerIn: parent
|
||||
color: (control.pressed || control.hovered) ? "#00b4ef" : "#8F8F8F"
|
||||
implicitWidth: 10
|
||||
implicitHeight: 16
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function updateGainFromQML(avatarUuid, sliderValue, isReleased) {
|
||||
if (isReleased || pal.gainSliderValueDB[avatarUuid] !== sliderValue) {
|
||||
pal.gainSliderValueDB[avatarUuid] = sliderValue;
|
||||
var data = {
|
||||
sessionId: avatarUuid,
|
||||
gain: sliderValue,
|
||||
isReleased: isReleased
|
||||
};
|
||||
pal.sendToScript({method: 'updateGain', params: data});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -24,17 +24,34 @@ Rectangle {
|
|||
// Style
|
||||
color: "#E3E3E3"
|
||||
// Properties
|
||||
property int myCardHeight: 70
|
||||
property int myCardHeight: 90
|
||||
property int rowHeight: 70
|
||||
property int actionButtonWidth: 75
|
||||
property int nameCardWidth: palContainer.width - actionButtonWidth*(iAmAdmin ? 4 : 2) - 4 - hifi.dimensions.scrollbarBackgroundWidth
|
||||
property var myData: ({displayName: "", userName: "", audioLevel: 0.0}) // valid dummy until set
|
||||
property var myData: ({displayName: "", userName: "", audioLevel: 0.0, admin: true}) // valid dummy until set
|
||||
property var ignored: ({}); // Keep a local list of ignored avatars & their data. Necessary because HashMap is slow to respond after ignoring.
|
||||
property var userModelData: [] // This simple list is essentially a mirror of the userModel listModel without all the extra complexities.
|
||||
property bool iAmAdmin: false
|
||||
// Keep a local list of per-avatar gainSliderValueDBs. Far faster than fetching this data from the server.
|
||||
// NOTE: if another script modifies the per-avatar gain, this value won't be accurate!
|
||||
property var gainSliderValueDB: ({});
|
||||
|
||||
// The letterbox used for popup messages
|
||||
LetterboxMessage {
|
||||
id: letterboxMessage
|
||||
z: 999 // Force the popup on top of everything else
|
||||
}
|
||||
function letterbox(headerGlyph, headerText, message) {
|
||||
letterboxMessage.headerGlyph = headerGlyph
|
||||
letterboxMessage.headerText = headerText
|
||||
letterboxMessage.text = message
|
||||
letterboxMessage.visible = true
|
||||
letterboxMessage.popupRadius = 0
|
||||
}
|
||||
|
||||
// This is the container for the PAL
|
||||
Rectangle {
|
||||
property bool punctuationMode: false
|
||||
id: palContainer
|
||||
// Size
|
||||
width: pal.width - 50
|
||||
|
@ -51,7 +68,7 @@ Rectangle {
|
|||
id: myInfo
|
||||
// Size
|
||||
width: palContainer.width
|
||||
height: myCardHeight + 20
|
||||
height: myCardHeight
|
||||
// Style
|
||||
color: pal.color
|
||||
// Anchors
|
||||
|
@ -65,6 +82,7 @@ Rectangle {
|
|||
displayName: myData.displayName
|
||||
userName: myData.userName
|
||||
audioLevel: myData.audioLevel
|
||||
isMyCard: true
|
||||
// Size
|
||||
width: nameCardWidth
|
||||
height: parent.height
|
||||
|
@ -141,8 +159,9 @@ Rectangle {
|
|||
onSortIndicatorOrderChanged: sortModel()
|
||||
|
||||
TableViewColumn {
|
||||
id: displayNameHeader
|
||||
role: "displayName"
|
||||
title: "NAMES"
|
||||
title: table.rowCount + (table.rowCount === 1 ? " NAME" : " NAMES")
|
||||
width: nameCardWidth
|
||||
movable: false
|
||||
resizable: false
|
||||
|
@ -172,8 +191,6 @@ Rectangle {
|
|||
TableViewColumn {
|
||||
visible: iAmAdmin
|
||||
role: "kick"
|
||||
// The hacky spaces used to center text over the button, since I don't know how to apply a margin
|
||||
// to column header text.
|
||||
title: "BAN"
|
||||
width: actionButtonWidth
|
||||
movable: false
|
||||
|
@ -203,9 +220,12 @@ Rectangle {
|
|||
id: nameCard
|
||||
// Properties
|
||||
displayName: styleData.value
|
||||
userName: model && model.userName
|
||||
audioLevel: model && model.audioLevel
|
||||
userName: model ? model.userName : ""
|
||||
audioLevel: model ? model.audioLevel : 0.0
|
||||
visible: !isCheckBox && !isButton
|
||||
uuid: model ? model.sessionId : ""
|
||||
selected: styleData.selected
|
||||
isAdmin: model && model.admin
|
||||
// Size
|
||||
width: nameCardWidth
|
||||
height: parent.height
|
||||
|
@ -223,15 +243,16 @@ Rectangle {
|
|||
id: actionCheckBox
|
||||
visible: isCheckBox
|
||||
anchors.centerIn: parent
|
||||
checked: model[styleData.role]
|
||||
checked: model ? model[styleData.role] : false
|
||||
// If this is a "Personal Mute" checkbox, disable the checkbox if the "Ignore" checkbox is checked.
|
||||
enabled: !(styleData.role === "personalMute" && model["ignore"])
|
||||
enabled: !(styleData.role === "personalMute" && (model ? model["ignore"] : true))
|
||||
boxSize: 24
|
||||
onClicked: {
|
||||
var newValue = !model[styleData.role]
|
||||
userModel.setProperty(model.userIndex, styleData.role, newValue)
|
||||
userModelData[model.userIndex][styleData.role] = newValue // Defensive programming
|
||||
Users[styleData.role](model.sessionId, newValue)
|
||||
UserActivityLogger["palAction"](newValue ? styleData.role : "un-" + styleData.role, model.sessionId)
|
||||
if (styleData.role === "ignore") {
|
||||
userModel.setProperty(model.userIndex, "personalMute", newValue)
|
||||
userModelData[model.userIndex]["personalMute"] = newValue // Defensive programming
|
||||
|
@ -258,6 +279,7 @@ Rectangle {
|
|||
height: 24
|
||||
onClicked: {
|
||||
Users[styleData.role](model.sessionId)
|
||||
UserActivityLogger["palAction"](styleData.role, model.sessionId)
|
||||
if (styleData.role === "kick") {
|
||||
// Just for now, while we cannot undo "Ban":
|
||||
userModel.remove(model.userIndex)
|
||||
|
@ -331,10 +353,10 @@ Rectangle {
|
|||
visible: iAmAdmin
|
||||
color: hifi.colors.lightGrayText
|
||||
}
|
||||
function letterbox(message) {
|
||||
letterboxMessage.text = message;
|
||||
letterboxMessage.visible = true
|
||||
|
||||
TextMetrics {
|
||||
id: displayNameHeaderMetrics
|
||||
text: displayNameHeader.title
|
||||
font: displayNameHeader.font
|
||||
}
|
||||
// This Rectangle refers to the [?] popup button next to "NAMES"
|
||||
Rectangle {
|
||||
|
@ -344,7 +366,7 @@ Rectangle {
|
|||
anchors.left: table.left
|
||||
anchors.top: table.top
|
||||
anchors.topMargin: 1
|
||||
anchors.leftMargin: nameCardWidth/2 + 24
|
||||
anchors.leftMargin: nameCardWidth/2 + displayNameHeaderMetrics.width/2 + 6
|
||||
RalewayRegular {
|
||||
id: helpText
|
||||
text: "[?]"
|
||||
|
@ -359,9 +381,11 @@ Rectangle {
|
|||
anchors.fill: parent
|
||||
acceptedButtons: Qt.LeftButton
|
||||
hoverEnabled: true
|
||||
onClicked: letterbox("Bold names in the list are Avatar Display Names.\n" +
|
||||
"If a Display Name isn't set, a unique Session Display Name is assigned." +
|
||||
"\n\nAdministrators of this domain can also see the Username or Machine ID associated with each avatar present.")
|
||||
onClicked: letterbox(hifi.glyphs.question,
|
||||
"Display Names",
|
||||
"Bold names in the list are <b>avatar display names</b>.<br>" +
|
||||
"If a display name isn't set, a unique <b>session display name</b> is assigned." +
|
||||
"<br><br>Administrators of this domain can also see the <b>username</b> or <b>machine ID</b> associated with each avatar present.")
|
||||
onEntered: helpText.color = hifi.colors.baseGrayHighlight
|
||||
onExited: helpText.color = hifi.colors.darkGray
|
||||
}
|
||||
|
@ -390,16 +414,41 @@ Rectangle {
|
|||
anchors.fill: parent
|
||||
acceptedButtons: Qt.LeftButton
|
||||
hoverEnabled: true
|
||||
onClicked: letterbox('Silencing a user mutes their microphone. Silenced users can unmute themselves by clicking the "UNMUTE" button on their HUD.\n\n' +
|
||||
"Banning a user will remove them from this domain and prevent them from returning. You can un-ban users from your domain's settings page.)")
|
||||
onClicked: letterbox(hifi.glyphs.question,
|
||||
"Admin Actions",
|
||||
"<b>Silence</b> mutes a user's microphone. Silenced users can unmute themselves by clicking "UNMUTE" on their toolbar.<br><br>" +
|
||||
"<b>Ban</b> removes a user from this domain and prevents them from returning. Admins can un-ban users from the Sandbox Domain Settings page.")
|
||||
onEntered: adminHelpText.color = "#94132e"
|
||||
onExited: adminHelpText.color = hifi.colors.redHighlight
|
||||
}
|
||||
}
|
||||
LetterboxMessage {
|
||||
id: letterboxMessage
|
||||
HifiControls.Keyboard {
|
||||
id: keyboard
|
||||
raised: myCard.currentlyEditingDisplayName && HMD.active
|
||||
numeric: parent.punctuationMode
|
||||
anchors {
|
||||
bottom: parent.bottom
|
||||
left: parent.left
|
||||
right: parent.right
|
||||
}
|
||||
}
|
||||
}
|
||||
// Timer used when selecting table rows that aren't yet present in the model
|
||||
// (i.e. when selecting avatars using edit.js or sphere overlays)
|
||||
Timer {
|
||||
property bool selected // Selected or deselected?
|
||||
property int userIndex // The userIndex of the avatar we want to select
|
||||
id: selectionTimer
|
||||
onTriggered: {
|
||||
if (selected) {
|
||||
table.selection.clear(); // for now, no multi-select
|
||||
table.selection.select(userIndex);
|
||||
table.positionViewAtRow(userIndex, ListView.Beginning);
|
||||
} else {
|
||||
table.selection.deselect(userIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function findSessionIndex(sessionId, optionalData) { // no findIndex in .qml
|
||||
var data = optionalData || userModelData, length = data.length;
|
||||
|
@ -437,19 +486,30 @@ Rectangle {
|
|||
case 'select':
|
||||
var sessionIds = message.params[0];
|
||||
var selected = message.params[1];
|
||||
var alreadyRefreshed = message.params[2];
|
||||
var userIndex = findSessionIndex(sessionIds[0]);
|
||||
if (sessionIds.length > 1) {
|
||||
letterbox('Only one user can be selected at a time.');
|
||||
letterbox("", "", 'Only one user can be selected at a time.');
|
||||
} else if (userIndex < 0) {
|
||||
letterbox('The last editor is not among this list of users.');
|
||||
} else {
|
||||
if (selected) {
|
||||
table.selection.clear(); // for now, no multi-select
|
||||
table.selection.select(userIndex);
|
||||
table.positionViewAtRow(userIndex, ListView.Visible);
|
||||
// If we've already refreshed the PAL and the avatar still isn't present in the model...
|
||||
if (alreadyRefreshed === true) {
|
||||
letterbox('', '', 'The last editor of this object is either you or not among this list of users.');
|
||||
} else {
|
||||
table.selection.deselect(userIndex);
|
||||
pal.sendToScript({method: 'refresh', params: message.params});
|
||||
}
|
||||
} else {
|
||||
// If we've already refreshed the PAL and found the avatar in the model
|
||||
if (alreadyRefreshed === true) {
|
||||
// Wait a little bit before trying to actually select the avatar in the table
|
||||
selectionTimer.interval = 250;
|
||||
} else {
|
||||
// If we've found the avatar in the model and didn't need to refresh,
|
||||
// select the avatar in the table immediately
|
||||
selectionTimer.interval = 0;
|
||||
}
|
||||
selectionTimer.selected = selected;
|
||||
selectionTimer.userIndex = userIndex;
|
||||
selectionTimer.start();
|
||||
}
|
||||
break;
|
||||
// Received an "updateUsername()" request from the JS
|
||||
|
@ -458,6 +518,7 @@ Rectangle {
|
|||
var userId = message.params[0];
|
||||
// The text that goes in the userName field is the second parameter in the message.
|
||||
var userName = message.params[1];
|
||||
var admin = message.params[2];
|
||||
// If the userId is empty, we're updating "myData".
|
||||
if (!userId) {
|
||||
myData.userName = userName;
|
||||
|
@ -469,8 +530,9 @@ Rectangle {
|
|||
// Set the userName appropriately
|
||||
userModel.setProperty(userIndex, "userName", userName);
|
||||
userModelData[userIndex].userName = userName; // Defensive programming
|
||||
} else {
|
||||
console.log("updateUsername() called with unknown UUID: ", userId);
|
||||
// Set the admin status appropriately
|
||||
userModel.setProperty(userIndex, "admin", admin);
|
||||
userModelData[userIndex].admin = admin; // Defensive programming
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
@ -486,14 +548,17 @@ Rectangle {
|
|||
if (userIndex != -1) {
|
||||
userModel.setProperty(userIndex, "audioLevel", audioLevel);
|
||||
userModelData[userIndex].audioLevel = audioLevel; // Defensive programming
|
||||
} else {
|
||||
console.log("updateUsername() called with unknown UUID: ", userId);
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'clearIgnored':
|
||||
case 'clearLocalQMLData':
|
||||
ignored = {};
|
||||
gainSliderValueDB = {};
|
||||
break;
|
||||
case 'avatarDisconnected':
|
||||
var sessionID = message.params[0];
|
||||
delete ignored[sessionID];
|
||||
break;
|
||||
default:
|
||||
console.log('Unrecognized message:', JSON.stringify(message));
|
||||
|
|
278
interface/resources/qml/hifi/tablet/Tablet.qml
Normal file
|
@ -0,0 +1,278 @@
|
|||
import QtQuick 2.0
|
||||
import QtGraphicalEffects 1.0
|
||||
import "../../styles-uit"
|
||||
|
||||
Item {
|
||||
id: tablet
|
||||
objectName: "tablet"
|
||||
property double micLevel: 0.8
|
||||
property int rowIndex: 0
|
||||
property int columnIndex: 0
|
||||
property int count: (flowMain.children.length - 1)
|
||||
|
||||
// called by C++ code to keep audio bar updated
|
||||
function setMicLevel(newMicLevel) {
|
||||
tablet.micLevel = newMicLevel;
|
||||
}
|
||||
|
||||
// used to look up a button by its uuid
|
||||
function findButtonIndex(uuid) {
|
||||
if (!uuid) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
for (var i in flowMain.children) {
|
||||
var child = flowMain.children[i];
|
||||
if (child.uuid === uuid) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
// called by C++ code when a button should be added to the tablet
|
||||
function addButtonProxy(properties) {
|
||||
var component = Qt.createComponent("TabletButton.qml");
|
||||
var button = component.createObject(flowMain);
|
||||
|
||||
// copy all properites to button
|
||||
var keys = Object.keys(properties).forEach(function (key) {
|
||||
button[key] = properties[key];
|
||||
});
|
||||
|
||||
// pass a reference to the tabletRoot object to the button.
|
||||
button.tabletRoot = parent.parent;
|
||||
return button;
|
||||
}
|
||||
|
||||
// called by C++ code when a button should be removed from the tablet
|
||||
function removeButtonProxy(properties) {
|
||||
var index = findButtonIndex(properties.uuid);
|
||||
if (index < 0) {
|
||||
console.log("Warning: Tablet.qml could not find button with uuid = " + properties.uuid);
|
||||
} else {
|
||||
flowMain.children[index].destroy();
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: bgTopBar
|
||||
height: 90
|
||||
gradient: Gradient {
|
||||
GradientStop {
|
||||
position: 0
|
||||
color: "#2b2b2b"
|
||||
}
|
||||
|
||||
GradientStop {
|
||||
position: 1
|
||||
color: "#1e1e1e"
|
||||
}
|
||||
}
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: 0
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: 0
|
||||
anchors.topMargin: 0
|
||||
anchors.top: parent.top
|
||||
|
||||
|
||||
Image {
|
||||
id: muteIcon
|
||||
width: 40
|
||||
height: 40
|
||||
source: "../../../icons/tablet-mute-icon.svg"
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Item {
|
||||
id: item1
|
||||
width: 170
|
||||
height: 10
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: 50
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
Rectangle {
|
||||
id: audioBarBase
|
||||
color: "#333333"
|
||||
radius: 5
|
||||
anchors.fill: parent
|
||||
}
|
||||
Rectangle {
|
||||
id: audioBarMask
|
||||
width: parent.width * tablet.micLevel
|
||||
color: "#333333"
|
||||
radius: 5
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.bottomMargin: 0
|
||||
anchors.top: parent.top
|
||||
anchors.topMargin: 0
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: 0
|
||||
}
|
||||
LinearGradient {
|
||||
anchors.fill: audioBarMask
|
||||
source: audioBarMask
|
||||
start: Qt.point(0, 0)
|
||||
end: Qt.point(170, 0)
|
||||
gradient: Gradient {
|
||||
GradientStop {
|
||||
position: 0
|
||||
color: "#2c8e72"
|
||||
}
|
||||
GradientStop {
|
||||
position: 0.8
|
||||
color: "#1fc6a6"
|
||||
}
|
||||
GradientStop {
|
||||
position: 0.81
|
||||
color: "#ea4c5f"
|
||||
}
|
||||
GradientStop {
|
||||
position: 1
|
||||
color: "#ea4c5f"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RalewaySemiBold {
|
||||
id: usernameText
|
||||
text: tablet.parent.parent.username
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: 20
|
||||
horizontalAlignment: Text.AlignRight
|
||||
font.pixelSize: 20
|
||||
color: "#afafaf"
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: bgMain
|
||||
gradient: Gradient {
|
||||
GradientStop {
|
||||
position: 0
|
||||
color: "#2b2b2b"
|
||||
|
||||
}
|
||||
|
||||
GradientStop {
|
||||
position: 1
|
||||
color: "#0f212e"
|
||||
}
|
||||
}
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.bottomMargin: 0
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: 0
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: 0
|
||||
anchors.top: bgTopBar.bottom
|
||||
anchors.topMargin: 0
|
||||
|
||||
Flickable {
|
||||
id: flickable
|
||||
width: parent.width
|
||||
height: parent.height
|
||||
contentWidth: parent.width
|
||||
contentHeight: flowMain.childrenRect.height + flowMain.anchors.topMargin + flowMain.anchors.bottomMargin + flowMain.spacing
|
||||
clip: true
|
||||
Flow {
|
||||
id: flowMain
|
||||
spacing: 16
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: 30
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: 30
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.bottomMargin: 30
|
||||
anchors.top: parent.top
|
||||
anchors.topMargin: 30
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
states: [
|
||||
State {
|
||||
name: "muted state"
|
||||
|
||||
PropertyChanges {
|
||||
target: muteText
|
||||
text: "UNMUTE"
|
||||
}
|
||||
|
||||
PropertyChanges {
|
||||
target: muteIcon
|
||||
source: "../../../icons/tablet-unmute-icon.svg"
|
||||
}
|
||||
|
||||
PropertyChanges {
|
||||
target: tablet
|
||||
micLevel: 0
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
function setCurrentItemState(state) {
|
||||
var index = rowIndex + columnIndex;
|
||||
|
||||
if (index >= 0 && index <= count ) {
|
||||
flowMain.children[index].state = state;
|
||||
}
|
||||
}
|
||||
function nextItem() {
|
||||
setCurrentItemState("base state");
|
||||
var nextColumnIndex = (columnIndex + 3 + 1) % 3;
|
||||
var nextIndex = rowIndex + nextColumnIndex;
|
||||
if(nextIndex <= count) {
|
||||
columnIndex = nextColumnIndex;
|
||||
};
|
||||
setCurrentItemState("hover state");
|
||||
}
|
||||
|
||||
function previousItem() {
|
||||
setCurrentItemState("base state");
|
||||
var prevIndex = (columnIndex + 3 - 1) % 3;
|
||||
if((rowIndex + prevIndex) <= count){
|
||||
columnIndex = prevIndex;
|
||||
}
|
||||
setCurrentItemState("hover state");
|
||||
}
|
||||
|
||||
function upItem() {
|
||||
setCurrentItemState("base state");
|
||||
rowIndex = rowIndex - 3;
|
||||
if (rowIndex < 0 ) {
|
||||
rowIndex = (count - (count % 3));
|
||||
var index = rowIndex + columnIndex;
|
||||
if(index > count) {
|
||||
rowIndex = rowIndex - 3;
|
||||
}
|
||||
}
|
||||
setCurrentItemState("hover state");
|
||||
}
|
||||
|
||||
function downItem() {
|
||||
setCurrentItemState("base state");
|
||||
rowIndex = rowIndex + 3;
|
||||
var index = rowIndex + columnIndex;
|
||||
if (index > count ) {
|
||||
rowIndex = 0;
|
||||
}
|
||||
setCurrentItemState("hover state");
|
||||
}
|
||||
|
||||
function selectItem() {
|
||||
flowMain.children[rowIndex + columnIndex].clicked();
|
||||
if (tabletRoot) {
|
||||
tabletRoot.playButtonClickSound();
|
||||
}
|
||||
}
|
||||
|
||||
Keys.onRightPressed: nextItem();
|
||||
Keys.onLeftPressed: previousItem();
|
||||
Keys.onDownPressed: downItem();
|
||||
Keys.onUpPressed: upItem();
|
||||
Keys.onReturnPressed: selectItem();
|
||||
}
|
233
interface/resources/qml/hifi/tablet/TabletButton.qml
Normal file
|
@ -0,0 +1,233 @@
|
|||
import QtQuick 2.0
|
||||
import QtGraphicalEffects 1.0
|
||||
|
||||
Item {
|
||||
id: tabletButton
|
||||
property var uuid;
|
||||
property string text: "EDIT"
|
||||
property string icon: "icons/edit-icon.svg"
|
||||
property string activeText: tabletButton.text
|
||||
property string activeIcon: tabletButton.icon
|
||||
property bool isActive: false
|
||||
property bool inDebugMode: false
|
||||
property bool isEntered: false
|
||||
property var tabletRoot;
|
||||
width: 129
|
||||
height: 129
|
||||
|
||||
signal clicked()
|
||||
|
||||
function changeProperty(key, value) {
|
||||
tabletButton[key] = value;
|
||||
}
|
||||
|
||||
onIsActiveChanged: {
|
||||
if (tabletButton.isEntered) {
|
||||
tabletButton.state = (tabletButton.isActive) ? "hover active state" : "hover sate";
|
||||
} else {
|
||||
tabletButton.state = (tabletButton.isActive) ? "active state" : "base sate";
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: buttonBg
|
||||
color: "#000000"
|
||||
opacity: 0.1
|
||||
radius: 8
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: 0
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: 0
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.bottomMargin: 0
|
||||
anchors.top: parent.top
|
||||
anchors.topMargin: 0
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: buttonOutline
|
||||
color: "#00000000"
|
||||
opacity: 0.2
|
||||
radius: 8
|
||||
z: 1
|
||||
border.width: 2
|
||||
border.color: "#ffffff"
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: 0
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: 0
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.bottomMargin: 0
|
||||
anchors.top: parent.top
|
||||
anchors.topMargin: 0
|
||||
}
|
||||
|
||||
DropShadow {
|
||||
id: glow
|
||||
visible: false
|
||||
anchors.fill: parent
|
||||
horizontalOffset: 0
|
||||
verticalOffset: 0
|
||||
color: "#ffffff"
|
||||
radius: 20
|
||||
z: -1
|
||||
samples: 41
|
||||
source: buttonOutline
|
||||
}
|
||||
|
||||
function urlHelper(src) {
|
||||
if (src.match(/\bhttp/)) {
|
||||
return src;
|
||||
} else {
|
||||
return "../../../" + src;
|
||||
}
|
||||
}
|
||||
|
||||
Image {
|
||||
id: icon
|
||||
width: 50
|
||||
height: 50
|
||||
visible: false
|
||||
anchors.bottom: text.top
|
||||
anchors.bottomMargin: 5
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
fillMode: Image.Stretch
|
||||
source: tabletButton.urlHelper(tabletButton.icon)
|
||||
}
|
||||
|
||||
ColorOverlay {
|
||||
id: iconColorOverlay
|
||||
anchors.fill: icon
|
||||
source: icon
|
||||
color: "#ffffff"
|
||||
}
|
||||
|
||||
Text {
|
||||
id: text
|
||||
color: "#ffffff"
|
||||
text: tabletButton.text
|
||||
font.bold: true
|
||||
font.pixelSize: 18
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.bottomMargin: 20
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
enabled: true
|
||||
onClicked: {
|
||||
console.log("Tablet Button Clicked!");
|
||||
if (tabletButton.inDebugMode) {
|
||||
if (tabletButton.isActive) {
|
||||
tabletButton.isActive = false;
|
||||
} else {
|
||||
tabletButton.isActive = true;
|
||||
}
|
||||
}
|
||||
tabletButton.clicked();
|
||||
if (tabletRoot) {
|
||||
tabletRoot.playButtonClickSound();
|
||||
}
|
||||
}
|
||||
onEntered: {
|
||||
tabletButton.isEntered = true;
|
||||
if (tabletButton.isActive) {
|
||||
tabletButton.state = "hover active state";
|
||||
} else {
|
||||
tabletButton.state = "hover state";
|
||||
}
|
||||
}
|
||||
onExited: {
|
||||
tabletButton.isEntered = false;
|
||||
if (tabletButton.isActive) {
|
||||
tabletButton.state = "active state";
|
||||
} else {
|
||||
tabletButton.state = "base state";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
states: [
|
||||
State {
|
||||
name: "hover state"
|
||||
|
||||
PropertyChanges {
|
||||
target: buttonOutline
|
||||
border.color: "#1fc6a6"
|
||||
opacity: 1
|
||||
}
|
||||
|
||||
PropertyChanges {
|
||||
target: glow
|
||||
visible: true
|
||||
}
|
||||
},
|
||||
State {
|
||||
name: "active state"
|
||||
|
||||
PropertyChanges {
|
||||
target: buttonOutline
|
||||
border.color: "#1fc6a6"
|
||||
opacity: 1
|
||||
}
|
||||
|
||||
PropertyChanges {
|
||||
target: buttonBg
|
||||
color: "#1fc6a6"
|
||||
opacity: 1
|
||||
}
|
||||
|
||||
PropertyChanges {
|
||||
target: text
|
||||
color: "#333333"
|
||||
text: tabletButton.activeText
|
||||
}
|
||||
|
||||
PropertyChanges {
|
||||
target: iconColorOverlay
|
||||
color: "#333333"
|
||||
}
|
||||
|
||||
PropertyChanges {
|
||||
target: icon
|
||||
source: tabletButton.urlHelper(tabletButton.activeIcon)
|
||||
}
|
||||
},
|
||||
State {
|
||||
name: "hover active state"
|
||||
|
||||
PropertyChanges {
|
||||
target: glow
|
||||
visible: true
|
||||
}
|
||||
|
||||
PropertyChanges {
|
||||
target: buttonOutline
|
||||
border.color: "#ffffff"
|
||||
opacity: 1
|
||||
}
|
||||
|
||||
PropertyChanges {
|
||||
target: buttonBg
|
||||
color: "#1fc6a6"
|
||||
opacity: 1
|
||||
}
|
||||
|
||||
PropertyChanges {
|
||||
target: text
|
||||
color: "#333333"
|
||||
}
|
||||
|
||||
PropertyChanges {
|
||||
target: iconColorOverlay
|
||||
color: "#333333"
|
||||
}
|
||||
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
106
interface/resources/qml/hifi/tablet/TabletMenu.qml
Normal file
|
@ -0,0 +1,106 @@
|
|||
import QtQuick 2.5
|
||||
import QtGraphicalEffects 1.0
|
||||
import QtQuick.Controls 1.4
|
||||
import QtQml 2.2
|
||||
import "."
|
||||
import "../../styles-uit"
|
||||
|
||||
FocusScope {
|
||||
id: tabletMenu
|
||||
objectName: "tabletMenu"
|
||||
|
||||
width: 480
|
||||
height: 720
|
||||
|
||||
property var rootMenu: Menu { objectName:"rootMenu" }
|
||||
property var point: Qt.point(50, 50)
|
||||
|
||||
TabletMouseHandler { id: menuPopperUpper }
|
||||
|
||||
Rectangle {
|
||||
id: bgNavBar
|
||||
height: 90
|
||||
z: 1
|
||||
gradient: Gradient {
|
||||
GradientStop {
|
||||
position: 0
|
||||
color: "#2b2b2b"
|
||||
}
|
||||
|
||||
GradientStop {
|
||||
position: 1
|
||||
color: "#1e1e1e"
|
||||
}
|
||||
}
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: 0
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: 0
|
||||
anchors.topMargin: 0
|
||||
anchors.top: parent.top
|
||||
|
||||
Image {
|
||||
id: menuRootIcon
|
||||
width: 40
|
||||
height: 40
|
||||
source: "../../../icons/tablet-icons/menu-i.svg"
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: 15
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
onEntered: iconColorOverlay.color = "#1fc6a6";
|
||||
onExited: iconColorOverlay.color = "#ffffff";
|
||||
// navigate back to root level menu
|
||||
onClicked: {
|
||||
buildMenu();
|
||||
tabletRoot.playButtonClickSound();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ColorOverlay {
|
||||
id: iconColorOverlay
|
||||
anchors.fill: menuRootIcon
|
||||
source: menuRootIcon
|
||||
color: "#34a2c7"
|
||||
}
|
||||
|
||||
RalewayBold {
|
||||
id: breadcrumbText
|
||||
text: "Menu"
|
||||
size: 26
|
||||
color: "#34a2c7"
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.left: menuRootIcon.right
|
||||
anchors.leftMargin: 15
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
onEntered: breadcrumbText.color = "#1fc6a6";
|
||||
onExited: breadcrumbText.color = "#34a2c7";
|
||||
// navigate back to parent level menu if there is one
|
||||
onClicked: {
|
||||
if (breadcrumbText.text !== "Menu") {
|
||||
menuPopperUpper.closeLastMenu();
|
||||
}
|
||||
tabletRoot.playButtonClickSound();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function pop() {
|
||||
menuPopperUpper.closeLastMenu();
|
||||
}
|
||||
|
||||
function setRootMenu(menu) {
|
||||
tabletMenu.rootMenu = menu
|
||||
buildMenu()
|
||||
}
|
||||
function buildMenu() {
|
||||
menuPopperUpper.popup(tabletMenu, rootMenu.items)
|
||||
}
|
||||
}
|
|
@ -12,8 +12,8 @@ import QtQuick 2.5
|
|||
import QtQuick.Controls 1.4
|
||||
import QtQuick.Controls.Styles 1.4
|
||||
|
||||
import "../controls-uit"
|
||||
import "../styles-uit"
|
||||
import "../../controls-uit"
|
||||
import "../../styles-uit"
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
@ -31,7 +31,7 @@ Item {
|
|||
// FIXME: Should use radio buttons if source.exclusiveGroup.
|
||||
anchors {
|
||||
left: parent.left
|
||||
leftMargin: hifi.dimensions.menuPadding.x
|
||||
leftMargin: hifi.dimensions.menuPadding.x + 15
|
||||
top: label.top
|
||||
topMargin: 0
|
||||
}
|
||||
|
@ -50,7 +50,7 @@ Item {
|
|||
|
||||
RalewaySemiBold {
|
||||
id: label
|
||||
size: hifi.fontSizes.rootMenu
|
||||
size: 20
|
||||
font.capitalization: isSubMenu ? Font.MixedCase : Font.AllUppercase
|
||||
anchors.left: check.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
@ -103,7 +103,7 @@ Item {
|
|||
HiFiGlyphs {
|
||||
text: hifi.glyphs.disclosureExpand
|
||||
color: source.enabled ? hifi.colors.baseGrayShadow : hifi.colors.baseGrayShadow25
|
||||
size: 2 * hifi.fontSizes.rootMenuDisclosure
|
||||
size: 70
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.right: parent.right
|
||||
horizontalAlignment: Text.AlignRight
|
|
@ -12,13 +12,13 @@ import QtQuick 2.5
|
|||
import QtQuick.Controls 1.4
|
||||
import QtQuick.Controls.Styles 1.4
|
||||
|
||||
import "../styles-uit"
|
||||
|
||||
import "../../styles-uit"
|
||||
import "."
|
||||
FocusScope {
|
||||
id: root
|
||||
implicitHeight: background.height
|
||||
implicitWidth: background.width
|
||||
|
||||
objectName: "root"
|
||||
property alias currentItem: listView.currentItem
|
||||
property alias model: listView.model
|
||||
property bool isSubMenu: false
|
||||
|
@ -32,15 +32,23 @@ FocusScope {
|
|||
radius: hifi.dimensions.borderRadius
|
||||
border.width: hifi.dimensions.borderWidth
|
||||
border.color: hifi.colors.lightGrayText80
|
||||
color: isSubMenu ? hifi.colors.faintGray : hifi.colors.faintGray80
|
||||
color: hifi.colors.faintGray
|
||||
//color: isSubMenu ? hifi.colors.faintGray : hifi.colors.faintGray80
|
||||
}
|
||||
|
||||
|
||||
ListView {
|
||||
id: listView
|
||||
x: 8; y: 8
|
||||
width: 128
|
||||
height: count * 32
|
||||
x: 0
|
||||
y: 0
|
||||
width: 480
|
||||
height: 720
|
||||
contentWidth: 480
|
||||
contentHeight: 720
|
||||
objectName: "menuList"
|
||||
|
||||
topMargin: hifi.dimensions.menuPadding.y
|
||||
bottomMargin: hifi.dimensions.menuPadding.y
|
||||
onEnabledChanged: recalcSize();
|
||||
onVisibleChanged: recalcSize();
|
||||
onCountChanged: recalcSize();
|
||||
|
@ -57,7 +65,7 @@ FocusScope {
|
|||
color: hifi.colors.white
|
||||
}
|
||||
|
||||
delegate: VrMenuItem {
|
||||
delegate: TabletMenuItem {
|
||||
text: name
|
||||
source: item
|
||||
onImplicitHeightChanged: listView.recalcSize()
|
||||
|
@ -67,7 +75,10 @@ FocusScope {
|
|||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
onEntered: listView.currentIndex = index
|
||||
onClicked: root.selected(item)
|
||||
onClicked: {
|
||||
root.selected(item)
|
||||
tabletRoot.playButtonClickSound();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -91,26 +102,30 @@ FocusScope {
|
|||
newHeight += currentItem.implicitHeight
|
||||
}
|
||||
}
|
||||
newHeight += 2 * hifi.dimensions.menuPadding.y; // White space at top and bottom.
|
||||
newHeight += hifi.dimensions.menuPadding.y * 2; // White space at top and bottom.
|
||||
if (maxWidth > width) {
|
||||
width = maxWidth;
|
||||
}
|
||||
if (newHeight > height) {
|
||||
height = newHeight
|
||||
if (newHeight > contentHeight) {
|
||||
contentHeight = newHeight;
|
||||
}
|
||||
currentIndex = originalIndex;
|
||||
}
|
||||
|
||||
function previousItem() { currentIndex = (currentIndex + count - 1) % count; }
|
||||
function nextItem() { currentIndex = (currentIndex + count + 1) % count; }
|
||||
function selectCurrentItem() { if (currentIndex != -1) root.selected(currentItem.source); }
|
||||
|
||||
|
||||
Keys.onUpPressed: previousItem();
|
||||
Keys.onDownPressed: nextItem();
|
||||
Keys.onSpacePressed: selectCurrentItem();
|
||||
Keys.onRightPressed: selectCurrentItem();
|
||||
Keys.onReturnPressed: selectCurrentItem();
|
||||
Keys.onLeftPressed: previousPage();
|
||||
}
|
||||
|
||||
function previousItem() { listView.currentIndex = (listView.currentIndex + listView.count - 1) % listView.count; }
|
||||
function nextItem() { listView.currentIndex = (listView.currentIndex + listView.count + 1) % listView.count; }
|
||||
function selectCurrentItem() { if (listView.currentIndex != -1) root.selected(currentItem.source); }
|
||||
function previousPage() { root.parent.pop(); }
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
|
@ -16,11 +16,11 @@ import "."
|
|||
Item {
|
||||
id: root
|
||||
anchors.fill: parent
|
||||
objectName: "MouseMenuHandlerItem"
|
||||
objectName: "tabletMenuHandlerItem"
|
||||
|
||||
MouseArea {
|
||||
id: menuRoot;
|
||||
objectName: "MouseMenuHandlerMouseArea"
|
||||
objectName: "tabletMenuHandlerMouseArea"
|
||||
anchors.fill: parent
|
||||
enabled: d.topMenu !== null
|
||||
onClicked: {
|
||||
|
@ -34,7 +34,7 @@ Item {
|
|||
property var topMenu: null;
|
||||
property var modelMaker: Component { ListModel { } }
|
||||
property var menuViewMaker: Component {
|
||||
VrMenuView {
|
||||
TabletMenuView {
|
||||
id: subMenu
|
||||
onSelected: d.handleSelection(subMenu, currentItem, item)
|
||||
}
|
||||
|
@ -54,7 +54,7 @@ Item {
|
|||
}
|
||||
|
||||
function toModel(items) {
|
||||
var result = modelMaker.createObject(desktop);
|
||||
var result = modelMaker.createObject(tabletMenu);
|
||||
for (var i = 0; i < items.length; ++i) {
|
||||
var item = items[i];
|
||||
if (!item.visible) continue;
|
||||
|
@ -63,7 +63,9 @@ Item {
|
|||
result.append({"name": item.title, "item": item})
|
||||
break;
|
||||
case MenuItemType.Item:
|
||||
result.append({"name": item.text, "item": item})
|
||||
if (item.text !== "Users Online") {
|
||||
result.append({"name": item.text, "item": item})
|
||||
}
|
||||
break;
|
||||
case MenuItemType.Separator:
|
||||
result.append({"name": "", "item": item})
|
||||
|
@ -80,10 +82,16 @@ Item {
|
|||
if (menuStack.length) {
|
||||
topMenu = menuStack[menuStack.length - 1];
|
||||
topMenu.focus = true;
|
||||
topMenu.forceActiveFocus();
|
||||
// show current menu level on nav bar
|
||||
if (topMenu.objectName === "") {
|
||||
breadcrumbText.text = "Menu";
|
||||
} else {
|
||||
breadcrumbText.text = topMenu.objectName;
|
||||
}
|
||||
} else {
|
||||
breadcrumbText.text = "Menu";
|
||||
topMenu = null;
|
||||
offscreenFlags.navigationFocused = false;
|
||||
menuRoot.enabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -91,7 +99,7 @@ Item {
|
|||
menuStack.push(newMenu);
|
||||
topMenu = newMenu;
|
||||
topMenu.focus = true;
|
||||
offscreenFlags.navigationFocused = true;
|
||||
topMenu.forceActiveFocus();
|
||||
}
|
||||
|
||||
function clearMenus() {
|
||||
|
@ -118,12 +126,7 @@ Item {
|
|||
function buildMenu(items, targetPosition) {
|
||||
var model = toModel(items);
|
||||
// Menus must be childed to desktop for Z-ordering
|
||||
var newMenu = menuViewMaker.createObject(desktop, { model: model, z: topMenu ? topMenu.z + 1 : desktop.zLevels.menu, isSubMenu: topMenu !== null });
|
||||
if (targetPosition) {
|
||||
newMenu.x = targetPosition.x
|
||||
newMenu.y = targetPosition.y - newMenu.height / 3 * 1
|
||||
}
|
||||
clampMenuPosition(newMenu);
|
||||
var newMenu = menuViewMaker.createObject(tabletMenu, { model: model, isSubMenu: topMenu !== null });
|
||||
pushMenu(newMenu);
|
||||
return newMenu;
|
||||
}
|
||||
|
@ -137,39 +140,36 @@ Item {
|
|||
case MenuItemType.Menu:
|
||||
var target = Qt.vector2d(topMenu.x, topMenu.y).plus(Qt.vector2d(selectedItem.x + 96, selectedItem.y));
|
||||
buildMenu(item.items, target).objectName = item.title;
|
||||
// show current menu level on nav bar
|
||||
breadcrumbText.text = item.title;
|
||||
break;
|
||||
|
||||
case MenuItemType.Item:
|
||||
console.log("Triggering " + item.text)
|
||||
// Don't block waiting for modal dialogs and such that the menu might open.
|
||||
delay.trigger(item);
|
||||
clearMenus();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function popup(parent, items, point) {
|
||||
function popup(parent, items) {
|
||||
d.clearMenus();
|
||||
menuRoot.enabled = true;
|
||||
d.buildMenu(items, point);
|
||||
}
|
||||
|
||||
function toggle(parent, items, point) {
|
||||
if (d.topMenu) {
|
||||
d.clearMenus();
|
||||
return;
|
||||
}
|
||||
popup(parent, items, point);
|
||||
}
|
||||
|
||||
function closeLastMenu() {
|
||||
if (d.menuStack.length) {
|
||||
if (d.menuStack.length > 1) {
|
||||
d.popMenu();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function previousItem() { d.topMenu.previousItem(); }
|
||||
function nextItem() { d.topMenu.nextItem(); }
|
||||
function selectCurrentItem() { d.topMenu.selectCurrentItem(); }
|
||||
function previousPage() { d.topMenu.previousPage(); }
|
||||
|
||||
}
|
64
interface/resources/qml/hifi/tablet/TabletRoot.qml
Normal file
|
@ -0,0 +1,64 @@
|
|||
import QtQuick 2.0
|
||||
import Hifi 1.0
|
||||
|
||||
Item {
|
||||
id: tabletRoot
|
||||
objectName: "tabletRoot"
|
||||
property string username: "Unknown user"
|
||||
property var eventBridge;
|
||||
|
||||
signal showDesktop();
|
||||
|
||||
function loadSource(url) {
|
||||
loader.source = url;
|
||||
}
|
||||
|
||||
function loadWebUrl(url, injectedJavaScriptUrl) {
|
||||
loader.item.url = url;
|
||||
loader.item.scriptURL = injectedJavaScriptUrl;
|
||||
}
|
||||
|
||||
SoundEffect {
|
||||
id: buttonClickSound
|
||||
volume: 0.1
|
||||
source: "../../../sounds/Gamemaster-Audio-button-click.wav"
|
||||
}
|
||||
|
||||
function playButtonClickSound() {
|
||||
// Because of the asynchronous nature of initalization, it is possible for this function to be
|
||||
// called before the C++ has set the globalPosition context variable.
|
||||
if (typeof globalPosition !== 'undefined') {
|
||||
buttonClickSound.play(globalPosition);
|
||||
}
|
||||
}
|
||||
|
||||
function setUsername(newUsername) {
|
||||
username = newUsername;
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: loader
|
||||
objectName: "loader"
|
||||
asynchronous: false
|
||||
|
||||
width: parent.width
|
||||
height: parent.height
|
||||
|
||||
onLoaded: {
|
||||
if (loader.item.hasOwnProperty("eventBridge")) {
|
||||
loader.item.eventBridge = eventBridge;
|
||||
|
||||
// Hook up callback for clara.io download from the marketplace.
|
||||
eventBridge.webEventReceived.connect(function (event) {
|
||||
if (event.slice(0, 17) === "CLARA.IO DOWNLOAD") {
|
||||
ApplicationInterface.addAssetToWorldFromURL(event.slice(18));
|
||||
}
|
||||
});
|
||||
}
|
||||
loader.item.forceActiveFocus();
|
||||
}
|
||||
}
|
||||
|
||||
width: 480
|
||||
height: 720
|
||||
}
|
10
interface/resources/qml/hifi/tablet/TabletWebView.qml
Normal file
|
@ -0,0 +1,10 @@
|
|||
import QtQuick 2.0
|
||||
import QtWebEngine 1.2
|
||||
|
||||
import "../../controls" as Controls
|
||||
|
||||
Controls.WebView {
|
||||
|
||||
}
|
||||
|
||||
|
|
@ -6,7 +6,7 @@ Item {
|
|||
property alias alpha: image.opacity
|
||||
property var subImage;
|
||||
property int yOffset: 0
|
||||
property int buttonState: 0
|
||||
property int buttonState: 1
|
||||
property real size: 50
|
||||
width: size; height: size
|
||||
property bool pinned: false
|
||||
|
|
|
@ -3,11 +3,34 @@ import QtQuick.Controls 1.4
|
|||
|
||||
StateImage {
|
||||
id: button
|
||||
property int hoverState: -1
|
||||
property int defaultState: -1
|
||||
property bool isActive: false
|
||||
property bool isEntered: false
|
||||
|
||||
property int imageOffOut: 1
|
||||
property int imageOffIn: 3
|
||||
property int imageOnOut: 0
|
||||
property int imageOnIn: 2
|
||||
|
||||
signal clicked()
|
||||
|
||||
function changeProperty(key, value) {
|
||||
button[key] = value;
|
||||
}
|
||||
|
||||
function updateState() {
|
||||
if (!button.isEntered && !button.isActive) {
|
||||
buttonState = imageOffOut;
|
||||
} else if (!button.isEntered && button.isActive) {
|
||||
buttonState = imageOnOut;
|
||||
} else if (button.isEntered && !button.isActive) {
|
||||
buttonState = imageOffIn;
|
||||
} else {
|
||||
buttonState = imageOnIn;
|
||||
}
|
||||
}
|
||||
|
||||
onIsActiveChanged: updateState();
|
||||
|
||||
Timer {
|
||||
id: asyncClickSender
|
||||
interval: 10
|
||||
|
@ -22,14 +45,12 @@ StateImage {
|
|||
anchors.fill: parent
|
||||
onClicked: asyncClickSender.start();
|
||||
onEntered: {
|
||||
if (hoverState >= 0) {
|
||||
buttonState = hoverState;
|
||||
}
|
||||
button.isEntered = true;
|
||||
updateState();
|
||||
}
|
||||
onExited: {
|
||||
if (defaultState >= 0) {
|
||||
buttonState = defaultState;
|
||||
}
|
||||
button.isEntered = false;
|
||||
updateState();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -156,7 +156,7 @@ Item {
|
|||
readonly property real modalDialogTitleHeight: 40
|
||||
readonly property real controlLineHeight: 28 // Height of spinbox control on 1920 x 1080 monitor
|
||||
readonly property real controlInterlineHeight: 21 // 75% of controlLineHeight
|
||||
readonly property vector2d menuPadding: Qt.vector2d(14, 12)
|
||||
readonly property vector2d menuPadding: Qt.vector2d(14, 102)
|
||||
readonly property real scrollbarBackgroundWidth: 18
|
||||
readonly property real scrollbarHandleWidth: scrollbarBackgroundWidth - 2
|
||||
}
|
||||
|
@ -312,10 +312,11 @@ Item {
|
|||
readonly property string error: "="
|
||||
readonly property string settings: "@"
|
||||
readonly property string trash: "{"
|
||||
readonly property string objectGroup: ""
|
||||
readonly property string objectGroup: "\ue000"
|
||||
readonly property string cm: "}"
|
||||
readonly property string msvg79: "~"
|
||||
readonly property string deg: "\\"
|
||||
readonly property string px: "|"
|
||||
readonly property string editPencil: "\ue00d"
|
||||
}
|
||||
}
|
||||
|
|
BIN
interface/resources/sounds/Gamemaster-Audio-button-click.wav
Normal file
|
@ -61,7 +61,7 @@
|
|||
#include <CursorManager.h>
|
||||
#include <DebugDraw.h>
|
||||
#include <DeferredLightingEffect.h>
|
||||
#include <display-plugins/DisplayPlugin.h>
|
||||
#include <EntityScriptClient.h>
|
||||
#include <EntityScriptingInterface.h>
|
||||
#include <ErrorDialog.h>
|
||||
#include <FileScriptingInterface.h>
|
||||
|
@ -111,6 +111,7 @@
|
|||
#include <ScriptEngines.h>
|
||||
#include <ScriptCache.h>
|
||||
#include <SoundCache.h>
|
||||
#include <TabletScriptingInterface.h>
|
||||
#include <Tooltip.h>
|
||||
#include <udt/PacketHeaders.h>
|
||||
#include <UserActivityLogger.h>
|
||||
|
@ -173,6 +174,7 @@
|
|||
#include "FrameTimingsScriptingInterface.h"
|
||||
#include <GPUIdent.h>
|
||||
#include <gl/GLHelpers.h>
|
||||
#include <EntityScriptClient.h>
|
||||
|
||||
// On Windows PC, NVidia Optimus laptop, we want to enable NVIDIA GPU
|
||||
// FIXME seems to be broken.
|
||||
|
@ -252,7 +254,7 @@ public:
|
|||
static const unsigned long MAX_HEARTBEAT_AGE_USECS = 30 * USECS_PER_SECOND;
|
||||
static const int WARNING_ELAPSED_HEARTBEAT = 500 * USECS_PER_MSEC; // warn if elapsed heartbeat average is large
|
||||
static const int HEARTBEAT_SAMPLES = 100000; // ~5 seconds worth of samples
|
||||
|
||||
|
||||
// Set the heartbeat on launch
|
||||
DeadlockWatchdogThread() {
|
||||
setObjectName("Deadlock Watchdog");
|
||||
|
@ -458,7 +460,7 @@ bool setupEssentials(int& argc, char** argv) {
|
|||
// Set dependencies
|
||||
DependencyManager::set<AccountManager>(std::bind(&Application::getUserAgent, qApp));
|
||||
DependencyManager::set<StatTracker>();
|
||||
DependencyManager::set<ScriptEngines>();
|
||||
DependencyManager::set<ScriptEngines>(ScriptEngine::CLIENT_SCRIPT);
|
||||
DependencyManager::set<Preferences>();
|
||||
DependencyManager::set<recording::Deck>();
|
||||
DependencyManager::set<recording::Recorder>();
|
||||
|
@ -491,6 +493,7 @@ bool setupEssentials(int& argc, char** argv) {
|
|||
DependencyManager::set<WindowScriptingInterface>();
|
||||
DependencyManager::set<HMDScriptingInterface>();
|
||||
DependencyManager::set<ResourceScriptingInterface>();
|
||||
DependencyManager::set<TabletScriptingInterface>();
|
||||
DependencyManager::set<ToolbarScriptingInterface>();
|
||||
DependencyManager::set<UserActivityLoggerScriptingInterface>();
|
||||
|
||||
|
@ -514,6 +517,7 @@ bool setupEssentials(int& argc, char** argv) {
|
|||
DependencyManager::set<EntityTreeRenderer>(true, qApp, qApp);
|
||||
DependencyManager::set<CompositorHelper>();
|
||||
DependencyManager::set<OffscreenQmlSurfaceCache>();
|
||||
DependencyManager::set<EntityScriptClient>();
|
||||
return previousSessionCrashed;
|
||||
}
|
||||
|
||||
|
@ -536,6 +540,9 @@ Q_GUI_EXPORT void qt_gl_set_global_share_context(QOpenGLContext *context);
|
|||
|
||||
Setting::Handle<int> sessionRunTime{ "sessionRunTime", 0 };
|
||||
|
||||
const float DEFAULT_HMD_TABLET_SCALE_PERCENT = 100.0f;
|
||||
const float DEFAULT_DESKTOP_TABLET_SCALE_PERCENT = 75.0f;
|
||||
|
||||
Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bool runServer, QString runServerPathOption) :
|
||||
QApplication(argc, argv),
|
||||
_shouldRunServer(runServer),
|
||||
|
@ -553,6 +560,8 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo
|
|||
_mirrorViewRect(QRect(MIRROR_VIEW_LEFT_PADDING, MIRROR_VIEW_TOP_PADDING, MIRROR_VIEW_WIDTH, MIRROR_VIEW_HEIGHT)),
|
||||
_previousScriptLocation("LastScriptLocation", DESKTOP_LOCATION),
|
||||
_fieldOfView("fieldOfView", DEFAULT_FIELD_OF_VIEW_DEGREES),
|
||||
_hmdTabletScale("hmdTabletScale", DEFAULT_HMD_TABLET_SCALE_PERCENT),
|
||||
_desktopTabletScale("desktopTabletScale", DEFAULT_DESKTOP_TABLET_SCALE_PERCENT),
|
||||
_constrainToolbarPosition("toolbar/constrainToolbarToCenterX", true),
|
||||
_scaleMirror(1.0f),
|
||||
_rotateMirror(0.0f),
|
||||
|
@ -616,7 +625,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo
|
|||
_window->setWindowTitle("Interface");
|
||||
|
||||
Model::setAbstractViewStateInterface(this); // The model class will sometimes need to know view state details from us
|
||||
|
||||
|
||||
auto nodeList = DependencyManager::get<NodeList>();
|
||||
|
||||
// Set up a watchdog thread to intentionally crash the application on deadlocks
|
||||
|
@ -637,6 +646,9 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo
|
|||
qCDebug(interfaceapp) << "[VERSION] We will use DEVELOPMENT global services.";
|
||||
#endif
|
||||
|
||||
// set the OCULUS_STORE property so the oculus plugin can know if we ran from the Oculus Store
|
||||
static const QString OCULUS_STORE_ARG = "--oculus-store";
|
||||
setProperty(hifi::properties::OCULUS_STORE, arguments().indexOf(OCULUS_STORE_ARG) != -1);
|
||||
|
||||
static const QString NO_UPDATER_ARG = "--no-updater";
|
||||
static const bool noUpdater = arguments().indexOf(NO_UPDATER_ARG) != -1;
|
||||
|
@ -697,7 +709,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo
|
|||
audioIO->setPositionGetter([]{
|
||||
auto avatarManager = DependencyManager::get<AvatarManager>();
|
||||
auto myAvatar = avatarManager ? avatarManager->getMyAvatar() : nullptr;
|
||||
|
||||
|
||||
return myAvatar ? myAvatar->getPositionForAudio() : Vectors::ZERO;
|
||||
});
|
||||
audioIO->setOrientationGetter([]{
|
||||
|
@ -849,7 +861,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo
|
|||
|
||||
// tell the NodeList instance who to tell the domain server we care about
|
||||
nodeList->addSetOfNodeTypesToNodeInterestSet(NodeSet() << NodeType::AudioMixer << NodeType::AvatarMixer
|
||||
<< NodeType::EntityServer << NodeType::AssetServer << NodeType::MessagesMixer);
|
||||
<< NodeType::EntityServer << NodeType::AssetServer << NodeType::MessagesMixer << NodeType::EntityScriptServer);
|
||||
|
||||
// connect to the packet sent signal of the _entityEditSender
|
||||
connect(&_entityEditSender, &EntityEditPacketSender::packetSent, this, &Application::packetSent);
|
||||
|
@ -880,7 +892,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo
|
|||
#ifdef Q_OS_MAC
|
||||
auto cursorTarget = _window; // OSX doesn't seem to provide for hiding the cursor only on the GL widget
|
||||
#else
|
||||
// On windows and linux, hiding the top level cursor also means it's invisible when hovering over the
|
||||
// On windows and linux, hiding the top level cursor also means it's invisible when hovering over the
|
||||
// window menu, which is a pain, so only hide it for the GL surface
|
||||
auto cursorTarget = _glWidget;
|
||||
#endif
|
||||
|
@ -972,7 +984,8 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo
|
|||
connect(userInputMapper.data(), &UserInputMapper::actionEvent, [this](int action, float state) {
|
||||
using namespace controller;
|
||||
auto offscreenUi = DependencyManager::get<OffscreenUi>();
|
||||
if (offscreenUi->navigationFocused()) {
|
||||
auto tabletScriptingInterface = DependencyManager::get<TabletScriptingInterface>();
|
||||
{
|
||||
auto actionEnum = static_cast<Action>(action);
|
||||
int key = Qt::Key_unknown;
|
||||
static int lastKey = Qt::Key_unknown;
|
||||
|
@ -1016,25 +1029,28 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo
|
|||
break;
|
||||
}
|
||||
|
||||
if (navAxis) {
|
||||
auto window = tabletScriptingInterface->getTabletWindow();
|
||||
if (navAxis && window) {
|
||||
if (lastKey != Qt::Key_unknown) {
|
||||
QKeyEvent event(QEvent::KeyRelease, lastKey, Qt::NoModifier);
|
||||
sendEvent(offscreenUi->getWindow(), &event);
|
||||
sendEvent(window, &event);
|
||||
lastKey = Qt::Key_unknown;
|
||||
}
|
||||
|
||||
if (key != Qt::Key_unknown) {
|
||||
QKeyEvent event(QEvent::KeyPress, key, Qt::NoModifier);
|
||||
sendEvent(offscreenUi->getWindow(), &event);
|
||||
sendEvent(window, &event);
|
||||
tabletScriptingInterface->processEvent(&event);
|
||||
lastKey = key;
|
||||
}
|
||||
} else if (key != Qt::Key_unknown) {
|
||||
} else if (key != Qt::Key_unknown && window) {
|
||||
if (state) {
|
||||
QKeyEvent event(QEvent::KeyPress, key, Qt::NoModifier);
|
||||
sendEvent(offscreenUi->getWindow(), &event);
|
||||
sendEvent(window, &event);
|
||||
tabletScriptingInterface->processEvent(&event);
|
||||
} else {
|
||||
QKeyEvent event(QEvent::KeyRelease, key, Qt::NoModifier);
|
||||
sendEvent(offscreenUi->getWindow(), &event);
|
||||
sendEvent(window, &event);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
@ -1060,12 +1076,8 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo
|
|||
DependencyManager::get<AudioClient>()->toggleMute();
|
||||
} else if (action == controller::toInt(controller::Action::CYCLE_CAMERA)) {
|
||||
cycleCamera();
|
||||
} else if (action == controller::toInt(controller::Action::UI_NAV_SELECT)) {
|
||||
if (!offscreenUi->navigationFocused()) {
|
||||
toggleMenuUnderReticle();
|
||||
}
|
||||
} else if (action == controller::toInt(controller::Action::CONTEXT_MENU)) {
|
||||
toggleMenuUnderReticle();
|
||||
toggleTabletUI();
|
||||
} else if (action == controller::toInt(controller::Action::RETICLE_X)) {
|
||||
auto oldPos = getApplicationCompositor().getReticlePosition();
|
||||
getApplicationCompositor().setReticlePosition({ oldPos.x + state, oldPos.y });
|
||||
|
@ -1125,7 +1137,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo
|
|||
loadSettings();
|
||||
|
||||
// Now that we've loaded the menu and thus switched to the previous display plugin
|
||||
// we can unlock the desktop repositioning code, since all the positions will be
|
||||
// we can unlock the desktop repositioning code, since all the positions will be
|
||||
// relative to the desktop size for this plugin
|
||||
auto offscreenUi = DependencyManager::get<OffscreenUi>();
|
||||
offscreenUi->getDesktop()->setProperty("repositionLocked", false);
|
||||
|
@ -1195,8 +1207,11 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo
|
|||
auto entityScriptingInterface = DependencyManager::get<EntityScriptingInterface>();
|
||||
connect(entityScriptingInterface.data(), &EntityScriptingInterface::clickDownOnEntity,
|
||||
[this](const EntityItemID& entityItemID, const PointerEvent& event) {
|
||||
setKeyboardFocusOverlay(UNKNOWN_OVERLAY_ID);
|
||||
setKeyboardFocusEntity(entityItemID);
|
||||
auto entity = getEntities()->getTree()->findEntityByID(entityItemID);
|
||||
if (entity && entity->wantsKeyboardFocus()) {
|
||||
setKeyboardFocusOverlay(UNKNOWN_OVERLAY_ID);
|
||||
setKeyboardFocusEntity(entityItemID);
|
||||
}
|
||||
});
|
||||
|
||||
connect(entityScriptingInterface.data(), &EntityScriptingInterface::deletingEntity, [=](const EntityItemID& entityItemID) {
|
||||
|
@ -1235,8 +1250,11 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo
|
|||
|
||||
// Add periodic checks to send user activity data
|
||||
static int CHECK_NEARBY_AVATARS_INTERVAL_MS = 10000;
|
||||
static int SEND_STATS_INTERVAL_MS = 10000;
|
||||
static int NEARBY_AVATAR_RADIUS_METERS = 10;
|
||||
|
||||
// setup the stats interval depending on if the 1s faster hearbeat was requested
|
||||
static const QString FAST_STATS_ARG = "--fast-heartbeat";
|
||||
static int SEND_STATS_INTERVAL_MS = arguments().indexOf(FAST_STATS_ARG) != -1 ? 1000 : 10000;
|
||||
|
||||
static glm::vec3 lastAvatarPosition = myAvatar->getPosition();
|
||||
static glm::mat4 lastHMDHeadPose = getHMDSensorPose();
|
||||
|
@ -1581,15 +1599,17 @@ QString Application::getUserAgent() {
|
|||
return userAgent;
|
||||
}
|
||||
|
||||
void Application::toggleMenuUnderReticle() const {
|
||||
// In HMD, if the menu is near the mouse but not under it, the reticle can be at a significantly
|
||||
// different depth. When you focus on the menu, the cursor can appear to your crossed eyes as both
|
||||
// on the menu and off.
|
||||
// Even in 2D, it is arguable whether the user would want the menu to be to the side.
|
||||
const float X_LEFT_SHIFT = 50.0;
|
||||
auto offscreenUi = DependencyManager::get<OffscreenUi>();
|
||||
auto reticlePosition = getApplicationCompositor().getReticlePosition();
|
||||
offscreenUi->toggleMenu(QPoint(reticlePosition.x - X_LEFT_SHIFT, reticlePosition.y));
|
||||
uint64_t lastTabletUIToggle { 0 };
|
||||
const uint64_t toggleTabletUILockout { 500000 };
|
||||
void Application::toggleTabletUI() const {
|
||||
uint64_t now = usecTimestampNow();
|
||||
if (now - lastTabletUIToggle < toggleTabletUILockout) {
|
||||
return;
|
||||
}
|
||||
lastTabletUIToggle = now;
|
||||
|
||||
auto HMD = DependencyManager::get<HMDScriptingInterface>();
|
||||
HMD->toggleShouldShowTablet();
|
||||
}
|
||||
|
||||
void Application::checkChangeCursor() {
|
||||
|
@ -1598,7 +1618,7 @@ void Application::checkChangeCursor() {
|
|||
#ifdef Q_OS_MAC
|
||||
auto cursorTarget = _window; // OSX doesn't seem to provide for hiding the cursor only on the GL widget
|
||||
#else
|
||||
// On windows and linux, hiding the top level cursor also means it's invisible when hovering over the
|
||||
// On windows and linux, hiding the top level cursor also means it's invisible when hovering over the
|
||||
// window menu, which is a pain, so only hide it for the GL surface
|
||||
auto cursorTarget = _glWidget;
|
||||
#endif
|
||||
|
@ -1785,7 +1805,7 @@ Application::~Application() {
|
|||
#endif
|
||||
// The window takes ownership of the menu, so this has the side effect of destroying it.
|
||||
_window->setMenuBar(nullptr);
|
||||
|
||||
|
||||
_window->deleteLater();
|
||||
|
||||
// Can't log to file passed this point, FileLogger about to be deleted
|
||||
|
@ -1811,10 +1831,10 @@ void Application::initializeGL() {
|
|||
|
||||
_glWidget->makeCurrent();
|
||||
gpu::Context::init<gpu::gl::GLBackend>();
|
||||
qApp->setProperty(hifi::properties::gl::MAKE_PROGRAM_CALLBACK,
|
||||
qApp->setProperty(hifi::properties::gl::MAKE_PROGRAM_CALLBACK,
|
||||
QVariant::fromValue((void*)(&gpu::gl::GLBackend::makeProgram)));
|
||||
_gpuContext = std::make_shared<gpu::Context>();
|
||||
// The gpu context can make child contexts for transfers, so
|
||||
// The gpu context can make child contexts for transfers, so
|
||||
// we need to restore primary rendering context
|
||||
_glWidget->makeCurrent();
|
||||
|
||||
|
@ -1828,7 +1848,7 @@ void Application::initializeGL() {
|
|||
assert(items.canCast<RenderFetchCullSortTask::Output>());
|
||||
static const QString RENDER_FORWARD = "HIFI_RENDER_FORWARD";
|
||||
if (QProcessEnvironment::systemEnvironment().contains(RENDER_FORWARD)) {
|
||||
_renderEngine->addJob<RenderForwardTask>("RenderForwardTask", items.get<RenderFetchCullSortTask::Output>());
|
||||
_renderEngine->addJob<RenderForwardTask>("Forward", items.get<RenderFetchCullSortTask::Output>());
|
||||
} else {
|
||||
_renderEngine->addJob<RenderDeferredTask>("RenderDeferredTask", items.get<RenderFetchCullSortTask::Output>());
|
||||
}
|
||||
|
@ -1931,6 +1951,8 @@ void Application::initializeUi() {
|
|||
rootContext->setContextProperty("AvatarList", DependencyManager::get<AvatarManager>().data());
|
||||
rootContext->setContextProperty("Users", DependencyManager::get<UsersScriptingInterface>().data());
|
||||
|
||||
rootContext->setContextProperty("UserActivityLogger", DependencyManager::get<UserActivityLoggerScriptingInterface>().data());
|
||||
|
||||
rootContext->setContextProperty("Camera", &_myCamera);
|
||||
|
||||
#if defined(Q_OS_MAC) || defined(Q_OS_WIN)
|
||||
|
@ -2034,7 +2056,7 @@ void Application::paintGL() {
|
|||
// FIXME not needed anymore?
|
||||
_offscreenContext->makeCurrent();
|
||||
|
||||
// If a display plugin loses it's underlying support, it
|
||||
// If a display plugin loses it's underlying support, it
|
||||
// needs to be able to signal us to not use it
|
||||
if (!displayPlugin->beginFrameRender(_frameCount)) {
|
||||
_inPaint = false;
|
||||
|
@ -2301,6 +2323,14 @@ void Application::setFieldOfView(float fov) {
|
|||
}
|
||||
}
|
||||
|
||||
void Application::setHMDTabletScale(float hmdTabletScale) {
|
||||
_hmdTabletScale.set(hmdTabletScale);
|
||||
}
|
||||
|
||||
void Application::setDesktopTabletScale(float desktopTabletScale) {
|
||||
_desktopTabletScale.set(desktopTabletScale);
|
||||
}
|
||||
|
||||
void Application::setSettingConstrainToolbarPosition(bool setting) {
|
||||
_constrainToolbarPosition.set(setting);
|
||||
DependencyManager::get<OffscreenUi>()->setConstrainToolbarToCenterX(setting);
|
||||
|
@ -2846,7 +2876,7 @@ void Application::keyPressEvent(QKeyEvent* event) {
|
|||
if (isMirrorChecked) {
|
||||
|
||||
// if we got here without coming in from a non-Full Screen mirror case, then our
|
||||
// _returnFromFullScreenMirrorTo is unknown. In that case we'll go to the old
|
||||
// _returnFromFullScreenMirrorTo is unknown. In that case we'll go to the old
|
||||
// behavior of returning to ThirdPerson
|
||||
if (_returnFromFullScreenMirrorTo.isEmpty()) {
|
||||
_returnFromFullScreenMirrorTo = MenuOption::ThirdPerson;
|
||||
|
@ -2923,10 +2953,6 @@ void Application::keyPressEvent(QKeyEvent* event) {
|
|||
|
||||
|
||||
void Application::keyReleaseEvent(QKeyEvent* event) {
|
||||
if (event->key() == Qt::Key_Alt && _altPressed && hasFocus()) {
|
||||
toggleMenuUnderReticle();
|
||||
}
|
||||
|
||||
_keysPressed.remove(event->key());
|
||||
|
||||
_controllerScriptingInterface->emitKeyReleaseEvent(event); // send events to any registered scripts
|
||||
|
@ -3020,7 +3046,7 @@ void Application::mouseMoveEvent(QMouseEvent* event) {
|
|||
maybeToggleMenuVisible(event);
|
||||
|
||||
auto& compositor = getApplicationCompositor();
|
||||
// if this is a real mouse event, and we're in HMD mode, then we should use it to move the
|
||||
// if this is a real mouse event, and we're in HMD mode, then we should use it to move the
|
||||
// compositor reticle
|
||||
// handleRealMouseMoveEvent() will return true, if we shouldn't process the event further
|
||||
if (!compositor.fakeEventActive() && compositor.handleRealMouseMoveEvent()) {
|
||||
|
@ -3392,6 +3418,8 @@ void Application::idle(float nsecsElapsed) {
|
|||
PROFILE_COUNTER(app, "cpuSystem", { { "system", kernelUserAndSystem.z } });
|
||||
#endif
|
||||
|
||||
|
||||
|
||||
auto displayPlugin = getActiveDisplayPlugin();
|
||||
if (displayPlugin) {
|
||||
PROFILE_COUNTER_IF_CHANGED(app, "present", float, displayPlugin->presentRate());
|
||||
|
@ -3401,9 +3429,15 @@ void Application::idle(float nsecsElapsed) {
|
|||
PROFILE_COUNTER_IF_CHANGED(app, "pendingDownloads", int, ResourceCache::getPendingRequestCount());
|
||||
PROFILE_COUNTER_IF_CHANGED(app, "currentProcessing", int, DependencyManager::get<StatTracker>()->getStat("Processing").toInt());
|
||||
PROFILE_COUNTER_IF_CHANGED(app, "pendingProcessing", int, DependencyManager::get<StatTracker>()->getStat("PendingProcessing").toInt());
|
||||
|
||||
|
||||
|
||||
auto renderConfig = _renderEngine->getConfiguration();
|
||||
PROFILE_COUNTER_IF_CHANGED(render, "gpuTime", float, (float)_gpuContext->getFrameTimerGPUAverage());
|
||||
PROFILE_COUNTER(render_detail, "gpuTimes", {
|
||||
{ "OpaqueRangeTimer", renderConfig->getConfig("OpaqueRangeTimer")->property("gpuRunTime") },
|
||||
{ "LinearDepth", renderConfig->getConfig("LinearDepth")->property("gpuRunTime") },
|
||||
{ "SurfaceGeometry", renderConfig->getConfig("SurfaceGeometry")->property("gpuRunTime") },
|
||||
{ "RenderDeferred", renderConfig->getConfig("RenderDeferred")->property("gpuRunTime") },
|
||||
{ "ToneAndPostRangeTimer", renderConfig->getConfig("ToneAndPostRangeTimer")->property("gpuRunTime") }
|
||||
});
|
||||
|
||||
PROFILE_RANGE(app, __FUNCTION__);
|
||||
|
||||
|
@ -4109,7 +4143,7 @@ void Application::setKeyboardFocusEntity(EntityItemID entityItemID) {
|
|||
}
|
||||
_lastAcceptedKeyPress = usecTimestampNow();
|
||||
|
||||
setKeyboardFocusHighlight(entity->getPosition(), entity->getRotation(),
|
||||
setKeyboardFocusHighlight(entity->getPosition(), entity->getRotation(),
|
||||
entity->getDimensions() * FOCUS_HIGHLIGHT_EXPANSION_FACTOR);
|
||||
}
|
||||
}
|
||||
|
@ -4143,9 +4177,13 @@ void Application::setKeyboardFocusOverlay(unsigned int overlayID) {
|
|||
}
|
||||
_lastAcceptedKeyPress = usecTimestampNow();
|
||||
|
||||
auto size = overlay->getSize() * FOCUS_HIGHLIGHT_EXPANSION_FACTOR;
|
||||
const float OVERLAY_DEPTH = 0.0105f;
|
||||
setKeyboardFocusHighlight(overlay->getPosition(), overlay->getRotation(), glm::vec3(size.x, size.y, OVERLAY_DEPTH));
|
||||
if (overlay->getProperty("showKeyboardFocusHighlight").toBool()) {
|
||||
auto size = overlay->getSize() * FOCUS_HIGHLIGHT_EXPANSION_FACTOR;
|
||||
const float OVERLAY_DEPTH = 0.0105f;
|
||||
setKeyboardFocusHighlight(overlay->getPosition(), overlay->getRotation(), glm::vec3(size.x, size.y, OVERLAY_DEPTH));
|
||||
} else if (_keyboardFocusHighlight) {
|
||||
_keyboardFocusHighlight->setVisible(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -4359,6 +4397,10 @@ void Application::update(float deltaTime) {
|
|||
PROFILE_RANGE_EX(simulation_physics, "HarvestChanges", 0xffffff00, (uint64_t)getActiveDisplayPlugin()->presentCount());
|
||||
PerformanceTimer perfTimer("harvestChanges");
|
||||
if (_physicsEngine->hasOutgoingChanges()) {
|
||||
// grab the collision events BEFORE handleOutgoingChanges() because at this point
|
||||
// we have a better idea of which objects we own or should own.
|
||||
auto& collisionEvents = _physicsEngine->getCollisionEvents();
|
||||
|
||||
getEntities()->getTree()->withWriteLock([&] {
|
||||
PerformanceTimer perfTimer("handleOutgoingChanges");
|
||||
const VectorOfMotionStates& outgoingChanges = _physicsEngine->getOutgoingChanges();
|
||||
|
@ -4366,11 +4408,10 @@ void Application::update(float deltaTime) {
|
|||
avatarManager->handleOutgoingChanges(outgoingChanges);
|
||||
});
|
||||
|
||||
auto collisionEvents = _physicsEngine->getCollisionEvents();
|
||||
avatarManager->handleCollisionEvents(collisionEvents);
|
||||
|
||||
if (!_aboutToQuit) {
|
||||
// handleCollisionEvents() AFTER handleOutgoinChanges()
|
||||
PerformanceTimer perfTimer("entities");
|
||||
avatarManager->handleCollisionEvents(collisionEvents);
|
||||
// Collision events (and their scripts) must not be handled when we're locked, above. (That would risk
|
||||
// deadlock.)
|
||||
_entitySimulation->handleCollisionEvents(collisionEvents);
|
||||
|
@ -4700,7 +4741,7 @@ void Application::queryOctree(NodeType_t serverType, PacketType packetType, Node
|
|||
_octreeQuery.setMaxQueryPacketsPerSecond(0);
|
||||
}
|
||||
|
||||
// if asked to forceResend, then set the query's position/orientation to be degenerate in a manner
|
||||
// if asked to forceResend, then set the query's position/orientation to be degenerate in a manner
|
||||
// that will cause our next query to be guarenteed to be different and the server will resend to us
|
||||
if (forceResend) {
|
||||
_octreeQuery.setCameraPosition(glm::vec3(-0.1, -0.1, -0.1));
|
||||
|
@ -5155,6 +5196,7 @@ void Application::nodeAdded(SharedNodePointer node) const {
|
|||
if (node->getType() == NodeType::AvatarMixer) {
|
||||
// new avatar mixer, send off our identity packet right away
|
||||
getMyAvatar()->sendIdentityPacket();
|
||||
getMyAvatar()->resetLastSent();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -5294,15 +5336,17 @@ bool Application::nearbyEntitiesAreReadyForPhysics() {
|
|||
if (_nearbyEntitiesStabilityCount >= MINIMUM_NEARBY_ENTITIES_STABILITY_COUNT) {
|
||||
// We've seen the same number of nearby entities for several stats packets in a row. assume we've got all
|
||||
// the local entities.
|
||||
bool result = true;
|
||||
foreach (EntityItemPointer entity, entities) {
|
||||
if (entity->shouldBePhysical() && !entity->isReadyToComputeShape()) {
|
||||
static QString repeatedMessage =
|
||||
LogHandler::getInstance().addRepeatedMessageRegex("Physics disabled until entity loads: .*");
|
||||
qCDebug(interfaceapp) << "Physics disabled until entity loads: " << entity->getID() << entity->getName();
|
||||
return false;
|
||||
// don't break here because we want all the relevant entities to start their downloads
|
||||
result = false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
return result;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
@ -5475,6 +5519,9 @@ void Application::registerScriptEngineWithApplicationServices(ScriptEngine* scri
|
|||
scriptEngine->registerGlobalObject("Controller", scriptingInterface.data());
|
||||
UserInputMapper::registerControllerTypes(scriptEngine);
|
||||
|
||||
auto recordingInterface = DependencyManager::get<RecordingScriptingInterface>();
|
||||
scriptEngine->registerGlobalObject("Recording", recordingInterface.data());
|
||||
|
||||
// connect this script engines printedMessage signal to the global ScriptEngines these various messages
|
||||
connect(scriptEngine, &ScriptEngine::printedMessage, DependencyManager::get<ScriptEngines>().data(), &ScriptEngines::onPrintedMessage);
|
||||
connect(scriptEngine, &ScriptEngine::errorMessage, DependencyManager::get<ScriptEngines>().data(), &ScriptEngines::onErrorMessage);
|
||||
|
@ -5833,7 +5880,7 @@ void Application::addAssetToWorldWithNewMapping(QString filePath, QString mappin
|
|||
mapping = mapping.insert(mapping.lastIndexOf("."), "-" + QString::number(copy));
|
||||
addAssetToWorldWithNewMapping(filePath, mapping, copy);
|
||||
} else {
|
||||
QString errorInfo = "Too many copies of asset name: "
|
||||
QString errorInfo = "Too many copies of asset name: "
|
||||
+ mapping.left(mapping.length() - QString::number(copy).length() - 1);
|
||||
qWarning(interfaceapp) << "Error downloading model: " + errorInfo;
|
||||
addAssetToWorldError(filenameFromPath(filePath), errorInfo);
|
||||
|
@ -5900,7 +5947,7 @@ void Application::addAssetToWorldAddEntity(QString filePath, QString mapping) {
|
|||
|
||||
// Note: Model dimensions are not available here; model is scaled per FBX mesh in RenderableModelEntityItem::update() later
|
||||
// on. But FBX dimensions may be in cm, so we monitor for the dimension change and rescale again if warranted.
|
||||
|
||||
|
||||
if (entityID == QUuid()) {
|
||||
QString errorInfo = "Could not add model " + mapping + " to world.";
|
||||
qWarning(interfaceapp) << "Could not add model to world: " + errorInfo;
|
||||
|
@ -6364,7 +6411,7 @@ glm::uvec2 Application::getCanvasSize() const {
|
|||
}
|
||||
|
||||
QRect Application::getRenderingGeometry() const {
|
||||
auto geometry = _glWidget->geometry();
|
||||
auto geometry = _glWidget->geometry();
|
||||
auto topLeft = geometry.topLeft();
|
||||
auto topLeftScreen = _glWidget->mapToGlobal(topLeft);
|
||||
geometry.moveTopLeft(topLeftScreen);
|
||||
|
@ -6727,8 +6774,8 @@ bool Application::makeRenderingContextCurrent() {
|
|||
return _offscreenContext->makeCurrent();
|
||||
}
|
||||
|
||||
bool Application::isForeground() const {
|
||||
return _isForeground && !_window->isMinimized();
|
||||
bool Application::isForeground() const {
|
||||
return _isForeground && !_window->isMinimized();
|
||||
}
|
||||
|
||||
void Application::sendMousePressOnEntity(QUuid id, PointerEvent event) {
|
||||
|
|
|
@ -208,6 +208,11 @@ public:
|
|||
float getFieldOfView() { return _fieldOfView.get(); }
|
||||
void setFieldOfView(float fov);
|
||||
|
||||
float getHMDTabletScale() { return _hmdTabletScale.get(); }
|
||||
void setHMDTabletScale(float hmdTabletScale);
|
||||
float getDesktopTabletScale() { return _desktopTabletScale.get(); }
|
||||
void setDesktopTabletScale(float desktopTabletScale);
|
||||
|
||||
float getSettingConstrainToolbarPosition() { return _constrainToolbarPosition.get(); }
|
||||
void setSettingConstrainToolbarPosition(bool setting);
|
||||
|
||||
|
@ -484,7 +489,7 @@ private:
|
|||
static void dragEnterEvent(QDragEnterEvent* event);
|
||||
|
||||
void maybeToggleMenuVisible(QMouseEvent* event) const;
|
||||
void toggleMenuUnderReticle() const;
|
||||
void toggleTabletUI() const;
|
||||
|
||||
MainWindow* _window;
|
||||
QElapsedTimer& _sessionRunTimer;
|
||||
|
@ -539,6 +544,8 @@ private:
|
|||
|
||||
Setting::Handle<QString> _previousScriptLocation;
|
||||
Setting::Handle<float> _fieldOfView;
|
||||
Setting::Handle<float> _hmdTabletScale;
|
||||
Setting::Handle<float> _desktopTabletScale;
|
||||
Setting::Handle<bool> _constrainToolbarPosition;
|
||||
|
||||
float _scaleMirror;
|
||||
|
|
|
@ -160,7 +160,7 @@ Menu::Menu() {
|
|||
audioIO.data(), SLOT(toggleMute()));
|
||||
|
||||
// Audio > Show Level Meter
|
||||
addCheckableActionToQMenuAndActionHash(audioMenu, MenuOption::AudioTools, 0, true);
|
||||
addCheckableActionToQMenuAndActionHash(audioMenu, MenuOption::AudioTools, 0, false);
|
||||
|
||||
|
||||
// Avatar menu ----------------------------------
|
||||
|
|
|
@ -62,7 +62,7 @@ const glm::vec3 HAND_TO_PALM_OFFSET(0.0f, 0.12f, 0.08f);
|
|||
|
||||
namespace render {
|
||||
template <> const ItemKey payloadGetKey(const AvatarSharedPointer& avatar) {
|
||||
return ItemKey::Builder::opaqueShape();
|
||||
return ItemKey::Builder::opaqueShape().withTypeMeta();
|
||||
}
|
||||
template <> const Item::Bound payloadGetBound(const AvatarSharedPointer& avatar) {
|
||||
return static_pointer_cast<Avatar>(avatar)->getBounds();
|
||||
|
@ -74,19 +74,15 @@ namespace render {
|
|||
avatarPtr->render(args, qApp->getCamera()->getPosition());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static uint64_t timeProcessingJoints = 0;
|
||||
static int32_t numJointsProcessed = 0;
|
||||
|
||||
float Avatar::getNumJointsProcessedPerSecond() {
|
||||
float rate = 0.0f;
|
||||
if (timeProcessingJoints > 0) {
|
||||
rate = (float)(numJointsProcessed * USECS_PER_SECOND) / (float)timeProcessingJoints;
|
||||
template <> uint32_t metaFetchMetaSubItems(const AvatarSharedPointer& avatar, ItemIDs& subItems) {
|
||||
auto avatarPtr = static_pointer_cast<Avatar>(avatar);
|
||||
if (avatarPtr->getSkeletonModel()) {
|
||||
auto metaSubItems = avatarPtr->getSkeletonModel()->fetchRenderItemIDs();
|
||||
subItems.insert(subItems.end(), metaSubItems.begin(), metaSubItems.end());
|
||||
return (uint32_t) metaSubItems.size();
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
timeProcessingJoints = 0;
|
||||
numJointsProcessed = 0;
|
||||
return rate;
|
||||
}
|
||||
|
||||
Avatar::Avatar(RigPointer rig) :
|
||||
|
@ -118,6 +114,7 @@ Avatar::Avatar(RigPointer rig) :
|
|||
_nameRectGeometryID = geometryCache->allocateID();
|
||||
_leftPointerGeometryID = geometryCache->allocateID();
|
||||
_rightPointerGeometryID = geometryCache->allocateID();
|
||||
_lastRenderUpdateTime = usecTimestampNow();
|
||||
}
|
||||
|
||||
Avatar::~Avatar() {
|
||||
|
@ -178,25 +175,35 @@ AABox Avatar::getBounds() const {
|
|||
}
|
||||
|
||||
void Avatar::animateScaleChanges(float deltaTime) {
|
||||
float currentScale = getUniformScale();
|
||||
auto desiredScale = getDomainLimitedScale();
|
||||
if (currentScale != desiredScale) {
|
||||
if (_isAnimatingScale) {
|
||||
float currentScale = getUniformScale();
|
||||
float desiredScale = getDomainLimitedScale();
|
||||
|
||||
// use exponential decay toward the domain limit clamped scale
|
||||
const float SCALE_ANIMATION_TIMESCALE = 0.5f;
|
||||
float blendFactor = glm::clamp(deltaTime / SCALE_ANIMATION_TIMESCALE, 0.0f, 1.0f);
|
||||
float animatedScale = (1.0f - blendFactor) * currentScale + blendFactor * desiredScale;
|
||||
|
||||
// snap to the end when we get close enough
|
||||
const float MIN_RELATIVE_SCALE_ERROR = 0.03f;
|
||||
if (fabsf(desiredScale - currentScale) / desiredScale < MIN_RELATIVE_SCALE_ERROR) {
|
||||
const float MIN_RELATIVE_ERROR = 0.03f;
|
||||
float relativeError = fabsf(desiredScale - currentScale) / desiredScale;
|
||||
if (relativeError < MIN_RELATIVE_ERROR) {
|
||||
animatedScale = desiredScale;
|
||||
_isAnimatingScale = false;
|
||||
}
|
||||
|
||||
setScale(glm::vec3(animatedScale)); // avatar scale is uniform
|
||||
|
||||
// TODO: rebuilding the shape constantly is somehwat expensive.
|
||||
// We should only rebuild after significant change.
|
||||
rebuildCollisionShape();
|
||||
}
|
||||
}
|
||||
|
||||
void Avatar::setTargetScale(float targetScale) {
|
||||
AvatarData::setTargetScale(targetScale);
|
||||
_isAnimatingScale = true;
|
||||
}
|
||||
|
||||
void Avatar::updateAvatarEntities() {
|
||||
PerformanceTimer perfTimer("attachments");
|
||||
// - if queueEditEntityMessage sees clientOnly flag it does _myAvatar->updateAvatarEntity()
|
||||
|
@ -293,63 +300,23 @@ void Avatar::updateAvatarEntities() {
|
|||
}
|
||||
}
|
||||
|
||||
void Avatar::setShouldDie() {
|
||||
// This will cause the avatar to be shrunk away and removed (the actual Avatar gets removed), but then it comes back.
|
||||
_owningAvatarMixer.clear();
|
||||
bool Avatar::shouldDie() const {
|
||||
const qint64 AVATAR_SILENCE_THRESHOLD_USECS = 5 * USECS_PER_SECOND;
|
||||
return _owningAvatarMixer.isNull() || getUsecsSinceLastUpdate() > AVATAR_SILENCE_THRESHOLD_USECS;
|
||||
}
|
||||
|
||||
void Avatar::simulate(float deltaTime) {
|
||||
void Avatar::simulate(float deltaTime, bool inView) {
|
||||
PROFILE_RANGE(simulation, "simulate");
|
||||
PerformanceTimer perfTimer("simulate");
|
||||
|
||||
if (!isDead() && !_motionState) {
|
||||
DependencyManager::get<AvatarManager>()->addAvatarToSimulation(this);
|
||||
}
|
||||
animateScaleChanges(deltaTime);
|
||||
|
||||
bool avatarInView = false;
|
||||
{ // update the shouldAnimate flag to match whether or not we will render the avatar.
|
||||
PerformanceTimer perfTimer("cull");
|
||||
{
|
||||
// simple frustum check
|
||||
PerformanceTimer perfTimer("inView");
|
||||
ViewFrustum viewFrustum;
|
||||
qApp->copyDisplayViewFrustum(viewFrustum);
|
||||
avatarInView = viewFrustum.sphereIntersectsFrustum(getPosition(), getBoundingRadius())
|
||||
|| viewFrustum.boxIntersectsFrustum(_skeletonModel->getRenderableMeshBound());
|
||||
}
|
||||
PerformanceTimer lodPerfTimer("LOD");
|
||||
if (avatarInView) {
|
||||
const float MINIMUM_VISIBILITY_FOR_ON = 0.4f;
|
||||
const float MAXIMUM_VISIBILITY_FOR_OFF = 0.6f;
|
||||
ViewFrustum viewFrustum;
|
||||
qApp->copyViewFrustum(viewFrustum);
|
||||
float visibility = calculateRenderAccuracy(viewFrustum.getPosition(),
|
||||
getBounds(), DependencyManager::get<LODManager>()->getOctreeSizeScale());
|
||||
if (!_shouldAnimate) {
|
||||
if (visibility > MINIMUM_VISIBILITY_FOR_ON) {
|
||||
_shouldAnimate = true;
|
||||
qCDebug(interfaceapp) << "Restoring" << (isMyAvatar() ? "myself" : getSessionUUID()) << "for visibility" << visibility;
|
||||
}
|
||||
} else if (visibility < MAXIMUM_VISIBILITY_FOR_OFF) {
|
||||
_shouldAnimate = false;
|
||||
qCDebug(interfaceapp) << "Optimizing" << (isMyAvatar() ? "myself" : getSessionUUID()) << "for visibility" << visibility;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
uint64_t start = usecTimestampNow();
|
||||
// CRUFT? _shouldSkipRender is never set 'true'
|
||||
if (_shouldAnimate && avatarInView && !_shouldSkipRender) {
|
||||
{
|
||||
PerformanceTimer perfTimer("skeleton");
|
||||
{
|
||||
PROFILE_RANGE(simulation, "updateJoints");
|
||||
if (inView && _hasNewJointData) {
|
||||
_skeletonModel->getRig()->copyJointsFromJointData(_jointData);
|
||||
_skeletonModel->simulate(deltaTime, _hasNewJointRotations || _hasNewJointTranslations);
|
||||
_skeletonModel->simulate(deltaTime, true);
|
||||
|
||||
locationChanged(); // joints changed, so if there are any children, update them.
|
||||
_hasNewJointRotations = false;
|
||||
_hasNewJointTranslations = false;
|
||||
}
|
||||
{
|
||||
PerformanceTimer perfTimer("head");
|
||||
_hasNewJointData = false;
|
||||
|
||||
glm::vec3 headPosition = getPosition();
|
||||
if (!_skeletonModel->getHeadPosition(headPosition)) {
|
||||
headPosition = getPosition();
|
||||
|
@ -357,16 +324,12 @@ void Avatar::simulate(float deltaTime) {
|
|||
Head* head = getHead();
|
||||
head->setPosition(headPosition);
|
||||
head->setScale(getUniformScale());
|
||||
head->simulate(deltaTime, false, !_shouldAnimate);
|
||||
head->simulate(deltaTime, false);
|
||||
} else {
|
||||
// a non-full update is still required so that the position, rotation, scale and bounds of the skeletonModel are updated.
|
||||
_skeletonModel->simulate(deltaTime, false);
|
||||
}
|
||||
} else {
|
||||
// a non-full update is still required so that the position, rotation, scale and bounds of the skeletonModel are updated.
|
||||
getHead()->setPosition(getPosition());
|
||||
PerformanceTimer perfTimer("skeleton");
|
||||
_skeletonModel->simulate(deltaTime, false);
|
||||
}
|
||||
timeProcessingJoints += usecTimestampNow() - start;
|
||||
numJointsProcessed += _jointData.size();
|
||||
|
||||
// update animation for display name fade in/out
|
||||
if ( _displayNameTargetAlpha != _displayNameAlpha) {
|
||||
|
@ -385,11 +348,13 @@ void Avatar::simulate(float deltaTime) {
|
|||
_displayNameAlpha = abs(_displayNameAlpha - _displayNameTargetAlpha) < 0.01f ? _displayNameTargetAlpha : _displayNameAlpha;
|
||||
}
|
||||
|
||||
measureMotionDerivatives(deltaTime);
|
||||
|
||||
simulateAttachments(deltaTime);
|
||||
updatePalms();
|
||||
updateAvatarEntities();
|
||||
{
|
||||
PROFILE_RANGE(simulation, "misc");
|
||||
measureMotionDerivatives(deltaTime);
|
||||
simulateAttachments(deltaTime);
|
||||
updatePalms();
|
||||
updateAvatarEntities();
|
||||
}
|
||||
}
|
||||
|
||||
bool Avatar::isLookingAtMe(AvatarSharedPointer avatar) const {
|
||||
|
@ -927,6 +892,16 @@ glm::quat Avatar::getAbsoluteJointRotationInObjectFrame(int index) const {
|
|||
Transform controllerRightHandTransform = Transform(getControllerRightHandMatrix());
|
||||
return controllerRightHandTransform.getRotation();
|
||||
}
|
||||
case CAMERA_MATRIX_INDEX: {
|
||||
glm::quat rotation;
|
||||
if (_skeletonModel && _skeletonModel->isActive()) {
|
||||
int headJointIndex = _skeletonModel->getFBXGeometry().headJointIndex;
|
||||
if (headJointIndex >= 0) {
|
||||
_skeletonModel->getAbsoluteJointRotationInRigFrame(headJointIndex, rotation);
|
||||
}
|
||||
}
|
||||
return Quaternions::Y_180 * rotation * Quaternions::Y_180;
|
||||
}
|
||||
default: {
|
||||
glm::quat rotation;
|
||||
_skeletonModel->getAbsoluteJointRotationInRigFrame(index, rotation);
|
||||
|
@ -953,6 +928,16 @@ glm::vec3 Avatar::getAbsoluteJointTranslationInObjectFrame(int index) const {
|
|||
Transform controllerRightHandTransform = Transform(getControllerRightHandMatrix());
|
||||
return controllerRightHandTransform.getTranslation();
|
||||
}
|
||||
case CAMERA_MATRIX_INDEX: {
|
||||
glm::vec3 translation;
|
||||
if (_skeletonModel && _skeletonModel->isActive()) {
|
||||
int headJointIndex = _skeletonModel->getFBXGeometry().headJointIndex;
|
||||
if (headJointIndex >= 0) {
|
||||
_skeletonModel->getAbsoluteJointTranslationInRigFrame(headJointIndex, translation);
|
||||
}
|
||||
}
|
||||
return Quaternions::Y_180 * translation * Quaternions::Y_180;
|
||||
}
|
||||
default: {
|
||||
glm::vec3 translation;
|
||||
_skeletonModel->getAbsoluteJointTranslationInRigFrame(index, translation);
|
||||
|
@ -1035,10 +1020,14 @@ void Avatar::setModelURLFinished(bool success) {
|
|||
|
||||
|
||||
// create new model, can return an instance of a SoftAttachmentModel rather then Model
|
||||
static std::shared_ptr<Model> allocateAttachmentModel(bool isSoft, RigPointer rigOverride) {
|
||||
static std::shared_ptr<Model> allocateAttachmentModel(bool isSoft, RigPointer rigOverride, bool isCauterized) {
|
||||
if (isSoft) {
|
||||
// cast to std::shared_ptr<Model>
|
||||
return std::dynamic_pointer_cast<Model>(std::make_shared<SoftAttachmentModel>(std::make_shared<Rig>(), nullptr, rigOverride));
|
||||
std::shared_ptr<SoftAttachmentModel> softModel = std::make_shared<SoftAttachmentModel>(std::make_shared<Rig>(), nullptr, rigOverride);
|
||||
if (isCauterized) {
|
||||
softModel->flagAsCauterized();
|
||||
}
|
||||
return std::dynamic_pointer_cast<Model>(softModel);
|
||||
} else {
|
||||
return std::make_shared<Model>(std::make_shared<Rig>());
|
||||
}
|
||||
|
@ -1064,12 +1053,12 @@ void Avatar::setAttachmentData(const QVector<AttachmentData>& attachmentData) {
|
|||
for (int i = 0; i < attachmentData.size(); i++) {
|
||||
if (i == (int)_attachmentModels.size()) {
|
||||
// if number of attachments has been increased, we need to allocate a new model
|
||||
_attachmentModels.push_back(allocateAttachmentModel(attachmentData[i].isSoft, _skeletonModel->getRig()));
|
||||
_attachmentModels.push_back(allocateAttachmentModel(attachmentData[i].isSoft, _skeletonModel->getRig(), isMyAvatar()));
|
||||
}
|
||||
else if (i < oldAttachmentData.size() && oldAttachmentData[i].isSoft != attachmentData[i].isSoft) {
|
||||
// if the attachment has changed type, we need to re-allocate a new one.
|
||||
_attachmentsToRemove.push_back(_attachmentModels[i]);
|
||||
_attachmentModels[i] = allocateAttachmentModel(attachmentData[i].isSoft, _skeletonModel->getRig());
|
||||
_attachmentModels[i] = allocateAttachmentModel(attachmentData[i].isSoft, _skeletonModel->getRig(), isMyAvatar());
|
||||
}
|
||||
_attachmentModels[i]->setURL(attachmentData[i].modelURL);
|
||||
}
|
||||
|
@ -1093,7 +1082,7 @@ int Avatar::parseDataFromBuffer(const QByteArray& buffer) {
|
|||
if (_moving && _motionState) {
|
||||
_motionState->addDirtyFlags(Simulation::DIRTY_POSITION);
|
||||
}
|
||||
if (_moving || _hasNewJointRotations || _hasNewJointTranslations) {
|
||||
if (_moving || _hasNewJointData) {
|
||||
locationChanged();
|
||||
}
|
||||
|
||||
|
@ -1354,4 +1343,4 @@ void Avatar::ensureInScene(AvatarSharedPointer self) {
|
|||
if (!_inScene) {
|
||||
addToScene(self);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -32,6 +32,7 @@ namespace render {
|
|||
template <> const ItemKey payloadGetKey(const AvatarSharedPointer& avatar);
|
||||
template <> const Item::Bound payloadGetBound(const AvatarSharedPointer& avatar);
|
||||
template <> void payloadRender(const AvatarSharedPointer& avatar, RenderArgs* args);
|
||||
template <> uint32_t metaFetchMetaSubItems(const AvatarSharedPointer& avatar, ItemIDs& subItems);
|
||||
}
|
||||
|
||||
static const float SCALING_RATIO = .05f;
|
||||
|
@ -57,8 +58,6 @@ class Avatar : public AvatarData {
|
|||
Q_PROPERTY(glm::vec3 skeletonOffset READ getSkeletonOffset WRITE setSkeletonOffset)
|
||||
|
||||
public:
|
||||
static float getNumJointsProcessedPerSecond();
|
||||
|
||||
explicit Avatar(RigPointer rig = nullptr);
|
||||
~Avatar();
|
||||
|
||||
|
@ -67,7 +66,7 @@ public:
|
|||
|
||||
void init();
|
||||
void updateAvatarEntities();
|
||||
void simulate(float deltaTime);
|
||||
void simulate(float deltaTime, bool inView);
|
||||
virtual void simulateAttachments(float deltaTime);
|
||||
|
||||
virtual void render(RenderArgs* renderArgs, const glm::vec3& cameraPosition);
|
||||
|
@ -140,8 +139,6 @@ public:
|
|||
|
||||
Q_INVOKABLE glm::vec3 getAcceleration() const { return _acceleration; }
|
||||
|
||||
Q_INVOKABLE bool getShouldRender() const { return !_shouldSkipRender; }
|
||||
|
||||
/// Scales a world space position vector relative to the avatar position and scale
|
||||
/// \param vector position to be scaled. Will store the result
|
||||
void scaleVectorRelativeToPosition(glm::vec3 &positionToScale) const;
|
||||
|
@ -178,7 +175,12 @@ public:
|
|||
glm::vec3 getUncachedRightPalmPosition() const;
|
||||
glm::quat getUncachedRightPalmRotation() const;
|
||||
|
||||
Q_INVOKABLE void setShouldDie();
|
||||
uint64_t getLastRenderUpdateTime() const { return _lastRenderUpdateTime; }
|
||||
void setLastRenderUpdateTime(uint64_t time) { _lastRenderUpdateTime = time; }
|
||||
|
||||
bool shouldDie() const;
|
||||
void animateScaleChanges(float deltaTime);
|
||||
void setTargetScale(float targetScale) override;
|
||||
|
||||
public slots:
|
||||
|
||||
|
@ -229,8 +231,6 @@ protected:
|
|||
// protected methods...
|
||||
bool isLookingAtMe(AvatarSharedPointer avatar) const;
|
||||
|
||||
virtual void animateScaleChanges(float deltaTime);
|
||||
|
||||
glm::vec3 getBodyRightDirection() const { return getOrientation() * IDENTITY_RIGHT; }
|
||||
glm::vec3 getBodyUpDirection() const { return getOrientation() * IDENTITY_UP; }
|
||||
glm::vec3 getBodyFrontDirection() const { return getOrientation() * IDENTITY_FRONT; }
|
||||
|
@ -260,14 +260,14 @@ protected:
|
|||
void ensureInScene(AvatarSharedPointer self);
|
||||
|
||||
private:
|
||||
uint64_t _lastRenderUpdateTime { 0 };
|
||||
int _leftPointerGeometryID { 0 };
|
||||
int _rightPointerGeometryID { 0 };
|
||||
int _nameRectGeometryID { 0 };
|
||||
bool _initialized;
|
||||
bool _shouldAnimate { true };
|
||||
bool _shouldSkipRender { false };
|
||||
bool _isLookAtTarget { false };
|
||||
bool _inScene { false };
|
||||
bool _isAnimatingScale { false };
|
||||
|
||||
float getBoundingRadius() const;
|
||||
|
||||
|
|
|
@ -128,32 +128,44 @@ bool AvatarActionHold::getTarget(float deltaTimeStep, glm::quat& rotation, glm::
|
|||
glm::quat palmRotation;
|
||||
|
||||
if (holdingAvatar->isMyAvatar()) {
|
||||
std::shared_ptr<MyAvatar> myAvatar = avatarManager->getMyAvatar();
|
||||
|
||||
// fetch the hand controller pose
|
||||
controller::Pose pose;
|
||||
if (isRightHand) {
|
||||
pose = avatarManager->getMyAvatar()->getRightHandControllerPoseInWorldFrame();
|
||||
pose = myAvatar->getRightHandControllerPoseInWorldFrame();
|
||||
} else {
|
||||
pose = avatarManager->getMyAvatar()->getLeftHandControllerPoseInWorldFrame();
|
||||
pose = myAvatar->getLeftHandControllerPoseInWorldFrame();
|
||||
}
|
||||
|
||||
if (pose.isValid()) {
|
||||
linearVelocity = pose.getVelocity();
|
||||
angularVelocity = pose.getAngularVelocity();
|
||||
|
||||
if (isRightHand) {
|
||||
pose = avatarManager->getMyAvatar()->getRightHandControllerPoseInAvatarFrame();
|
||||
} else {
|
||||
pose = avatarManager->getMyAvatar()->getLeftHandControllerPoseInAvatarFrame();
|
||||
}
|
||||
}
|
||||
|
||||
if (_ignoreIK && pose.isValid()) {
|
||||
|
||||
// this position/rotation should be the same as the one in scripts/system/libraries/controllers.js
|
||||
// otherwise things will do a little hop when you grab them.
|
||||
|
||||
// if (isRightHand) {
|
||||
// pose = myAvatar->getRightHandControllerPoseInAvatarFrame();
|
||||
// } else {
|
||||
// pose = myAvatar->getLeftHandControllerPoseInAvatarFrame();
|
||||
// }
|
||||
// glm::vec3 camRelPos = pose.getTranslation();
|
||||
// glm::quat camRelRot = pose.getRotation();
|
||||
|
||||
int camRelIndex = isRightHand ?
|
||||
CAMERA_RELATIVE_CONTROLLER_RIGHTHAND_INDEX :
|
||||
CAMERA_RELATIVE_CONTROLLER_LEFTHAND_INDEX;
|
||||
glm::vec3 camRelPos = myAvatar->getAbsoluteJointTranslationInObjectFrame(camRelIndex);
|
||||
glm::quat camRelRot = myAvatar->getAbsoluteJointRotationInObjectFrame(camRelIndex);
|
||||
|
||||
Transform avatarTransform;
|
||||
auto myAvatar = DependencyManager::get<AvatarManager>()->getMyAvatar();
|
||||
avatarTransform = myAvatar->getTransform();
|
||||
palmPosition = avatarTransform.transform(pose.getTranslation() / myAvatar->getDomainLimitedScale());
|
||||
palmRotation = avatarTransform.getRotation() * pose.getRotation();
|
||||
palmPosition = avatarTransform.transform(camRelPos / myAvatar->getDomainLimitedScale());
|
||||
palmRotation = avatarTransform.getRotation() * camRelRot;
|
||||
} else {
|
||||
glm::vec3 avatarRigidBodyPosition;
|
||||
glm::quat avatarRigidBodyRotation;
|
||||
|
@ -223,6 +235,11 @@ void AvatarActionHold::doKinematicUpdate(float deltaTimeStep) {
|
|||
qDebug() << "AvatarActionHold::doKinematicUpdate -- no owning entity";
|
||||
return;
|
||||
}
|
||||
if (ownerEntity->getParentID() != QUuid()) {
|
||||
// if the held entity has been given a parent, stop acting on it.
|
||||
return;
|
||||
}
|
||||
|
||||
void* physicsInfo = ownerEntity->getPhysicsInfo();
|
||||
if (!physicsInfo) {
|
||||
qDebug() << "AvatarActionHold::doKinematicUpdate -- no owning physics info";
|
||||
|
|
|
@ -132,53 +132,136 @@ void AvatarManager::updateMyAvatar(float deltaTime) {
|
|||
|
||||
Q_LOGGING_CATEGORY(trace_simulation_avatar, "trace.simulation.avatar");
|
||||
|
||||
float AvatarManager::getAvatarDataRate(const QUuid& sessionID, const QString& rateName) {
|
||||
auto avatar = getAvatarBySessionID(sessionID);
|
||||
return avatar->getDataRate(rateName);
|
||||
}
|
||||
|
||||
class AvatarPriority {
|
||||
public:
|
||||
AvatarPriority(AvatarSharedPointer a, float p) : avatar(a), priority(p) {}
|
||||
AvatarSharedPointer avatar;
|
||||
float priority;
|
||||
// NOTE: we invert the less-than operator to sort high priorities to front
|
||||
bool operator<(const AvatarPriority& other) const { return priority > other.priority; }
|
||||
};
|
||||
|
||||
void AvatarManager::updateOtherAvatars(float deltaTime) {
|
||||
// lock the hash for read to check the size
|
||||
QReadLocker lock(&_hashLock);
|
||||
|
||||
if (_avatarHash.size() < 2 && _avatarFades.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
lock.unlock();
|
||||
|
||||
bool showWarnings = Menu::getInstance()->isOptionChecked(MenuOption::PipelineWarnings);
|
||||
PerformanceWarning warn(showWarnings, "Application::updateAvatars()");
|
||||
|
||||
PerformanceTimer perfTimer("otherAvatars");
|
||||
render::PendingChanges pendingChanges;
|
||||
uint64_t startTime = usecTimestampNow();
|
||||
|
||||
// simulate avatars
|
||||
auto hashCopy = getHashCopy();
|
||||
auto avatarMap = getHashCopy();
|
||||
QList<AvatarSharedPointer> avatarList = avatarMap.values();
|
||||
ViewFrustum cameraView;
|
||||
qApp->copyDisplayViewFrustum(cameraView);
|
||||
glm::vec3 frustumCenter = cameraView.getPosition();
|
||||
|
||||
uint64_t start = usecTimestampNow();
|
||||
AvatarHash::iterator avatarIterator = hashCopy.begin();
|
||||
while (avatarIterator != hashCopy.end()) {
|
||||
auto avatar = std::static_pointer_cast<Avatar>(avatarIterator.value());
|
||||
const float OUT_OF_VIEW_PENALTY = -10.0;
|
||||
|
||||
if (avatar == _myAvatar || !avatar->isInitialized()) {
|
||||
// DO NOT update _myAvatar! Its update has already been done earlier in the main loop.
|
||||
// DO NOT update or fade out uninitialized Avatars
|
||||
++avatarIterator;
|
||||
} else if (avatar->shouldDie()) {
|
||||
removeAvatar(avatarIterator.key());
|
||||
++avatarIterator;
|
||||
} else {
|
||||
avatar->ensureInScene(avatar);
|
||||
avatar->simulate(deltaTime);
|
||||
++avatarIterator;
|
||||
std::priority_queue<AvatarPriority> sortedAvatars;
|
||||
{
|
||||
PROFILE_RANGE(simulation, "sort");
|
||||
for (int32_t i = 0; i < avatarList.size(); ++i) {
|
||||
const auto& avatar = std::static_pointer_cast<Avatar>(avatarList.at(i));
|
||||
if (avatar == _myAvatar || !avatar->isInitialized()) {
|
||||
// DO NOT update _myAvatar! Its update has already been done earlier in the main loop.
|
||||
// DO NOT update or fade out uninitialized Avatars
|
||||
continue;
|
||||
}
|
||||
if (avatar->shouldDie()) {
|
||||
removeAvatar(avatar->getID());
|
||||
continue;
|
||||
}
|
||||
if (avatar->isDead()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
avatar->updateRenderItem(pendingChanges);
|
||||
// priority = weighted linear combination of:
|
||||
// (a) apparentSize
|
||||
// (b) proximity to center of view
|
||||
// (c) time since last update
|
||||
// (d) TIME_PENALTY to help recently updated entries sort toward back
|
||||
glm::vec3 avatarPosition = avatar->getPosition();
|
||||
glm::vec3 offset = avatarPosition - frustumCenter;
|
||||
float distance = glm::length(offset) + 0.001f; // add 1mm to avoid divide by zero
|
||||
float radius = avatar->getBoundingRadius();
|
||||
const glm::vec3& forward = cameraView.getDirection();
|
||||
float apparentSize = radius / distance;
|
||||
float cosineAngle = glm::length(offset - glm::dot(offset, forward) * forward) / distance;
|
||||
const float TIME_PENALTY = 0.080f; // seconds
|
||||
float age = (float)(startTime - avatar->getLastRenderUpdateTime()) / (float)(USECS_PER_SECOND) - TIME_PENALTY;
|
||||
// NOTE: we are adding values of different units to get a single measure of "priority".
|
||||
// Thus we multiply each component by a conversion "weight" that scales its units
|
||||
// relative to the others. These weights are pure magic tuning and are hard coded in the
|
||||
// relation below: (hint: unitary weights are not explicityly shown)
|
||||
float priority = apparentSize + 0.25f * cosineAngle + age;
|
||||
|
||||
// decrement priority of avatars outside keyhole
|
||||
if (distance > cameraView.getCenterRadius()) {
|
||||
if (!cameraView.sphereIntersectsFrustum(avatarPosition, radius)) {
|
||||
priority += OUT_OF_VIEW_PENALTY;
|
||||
}
|
||||
}
|
||||
sortedAvatars.push(AvatarPriority(avatar, priority));
|
||||
}
|
||||
}
|
||||
|
||||
render::PendingChanges pendingChanges;
|
||||
const uint64_t RENDER_UPDATE_BUDGET = 1500; // usec
|
||||
const uint64_t MAX_UPDATE_BUDGET = 2000; // usec
|
||||
uint64_t renderExpiry = startTime + RENDER_UPDATE_BUDGET;
|
||||
uint64_t maxExpiry = startTime + MAX_UPDATE_BUDGET;
|
||||
while (!sortedAvatars.empty()) {
|
||||
const AvatarPriority& sortData = sortedAvatars.top();
|
||||
const auto& avatar = std::static_pointer_cast<Avatar>(sortData.avatar);
|
||||
|
||||
// for ALL avatars...
|
||||
avatar->ensureInScene(avatar);
|
||||
if (!avatar->getMotionState()) {
|
||||
ShapeInfo shapeInfo;
|
||||
avatar->computeShapeInfo(shapeInfo);
|
||||
btCollisionShape* shape = const_cast<btCollisionShape*>(ObjectMotionState::getShapeManager()->getShape(shapeInfo));
|
||||
if (shape) {
|
||||
// don't add to the simulation now, instead put it on a list to be added later
|
||||
AvatarMotionState* motionState = new AvatarMotionState(avatar.get(), shape);
|
||||
avatar->setMotionState(motionState);
|
||||
_motionStatesToAddToPhysics.insert(motionState);
|
||||
_motionStatesThatMightUpdate.insert(motionState);
|
||||
}
|
||||
}
|
||||
avatar->animateScaleChanges(deltaTime);
|
||||
|
||||
uint64_t now = usecTimestampNow();
|
||||
if (now < renderExpiry) {
|
||||
// we're within budget
|
||||
const float OUT_OF_VIEW_THRESHOLD = 0.5f * OUT_OF_VIEW_PENALTY;
|
||||
bool inView = sortData.priority > OUT_OF_VIEW_THRESHOLD;
|
||||
avatar->simulate(deltaTime, inView);
|
||||
avatar->updateRenderItem(pendingChanges);
|
||||
avatar->setLastRenderUpdateTime(startTime);
|
||||
} else if (now < maxExpiry) {
|
||||
// we've spent most of our time budget, but we still simulate() the avatar as it if were out of view
|
||||
// --> some avatars may freeze until their priority trickles up
|
||||
const bool inView = false;
|
||||
avatar->simulate(deltaTime, inView);
|
||||
} else {
|
||||
// we've spent ALL of our time budget --> bail on the rest of the avatar updates
|
||||
// --> some scale or fade animations may glitch
|
||||
// --> some avatar velocity measurements may be a little off
|
||||
break;
|
||||
}
|
||||
sortedAvatars.pop();
|
||||
}
|
||||
qApp->getMain3DScene()->enqueuePendingChanges(pendingChanges);
|
||||
|
||||
// simulate avatar fades
|
||||
simulateAvatarFades(deltaTime);
|
||||
|
||||
PROFILE_COUNTER(simulation_avatar, "NumAvatarsPerSec",
|
||||
{ { "NumAvatarsPerSec", (float)(size() * USECS_PER_SECOND) / (float)(usecTimestampNow() - start) } });
|
||||
PROFILE_COUNTER(simulation_avatar, "NumJointsPerSec", { { "NumJointsPerSec", Avatar::getNumJointsProcessedPerSecond() } });
|
||||
}
|
||||
|
||||
void AvatarManager::postUpdate(float deltaTime) {
|
||||
|
@ -201,6 +284,7 @@ void AvatarManager::simulateAvatarFades(float deltaTime) {
|
|||
while (fadingIterator != _avatarFades.end()) {
|
||||
auto avatar = std::static_pointer_cast<Avatar>(*fadingIterator);
|
||||
avatar->setTargetScale(avatar->getUniformScale() * SHRINK_RATE);
|
||||
avatar->animateScaleChanges(deltaTime);
|
||||
if (avatar->getTargetScale() <= MIN_FADE_SCALE) {
|
||||
avatar->removeFromScene(*fadingIterator, scene, pendingChanges);
|
||||
// only remove from _avatarFades if we're sure its motionState has been removed from PhysicsEngine
|
||||
|
@ -210,7 +294,8 @@ void AvatarManager::simulateAvatarFades(float deltaTime) {
|
|||
++fadingIterator;
|
||||
}
|
||||
} else {
|
||||
avatar->simulate(deltaTime);
|
||||
const bool inView = true; // HACK
|
||||
avatar->simulate(deltaTime, inView);
|
||||
++fadingIterator;
|
||||
}
|
||||
}
|
||||
|
@ -260,6 +345,10 @@ void AvatarManager::handleRemovedAvatar(const AvatarSharedPointer& removedAvatar
|
|||
}
|
||||
if (removalReason == KillAvatarReason::TheirAvatarEnteredYourBubble || removalReason == YourAvatarEnteredTheirBubble) {
|
||||
DependencyManager::get<NodeList>()->radiusIgnoreNodeBySessionID(avatar->getSessionUUID(), true);
|
||||
} else if (removalReason == KillAvatarReason::AvatarDisconnected) {
|
||||
// remove from node sets, if present
|
||||
DependencyManager::get<NodeList>()->removeFromIgnoreMuteSets(avatar->getSessionUUID());
|
||||
DependencyManager::get<UsersScriptingInterface>()->avatarDisconnected(avatar->getSessionUUID());
|
||||
}
|
||||
_avatarFades.push_back(removedAvatar);
|
||||
}
|
||||
|
@ -382,21 +471,6 @@ void AvatarManager::handleCollisionEvents(const CollisionEvents& collisionEvents
|
|||
}
|
||||
}
|
||||
|
||||
void AvatarManager::addAvatarToSimulation(Avatar* avatar) {
|
||||
assert(!avatar->getMotionState());
|
||||
|
||||
ShapeInfo shapeInfo;
|
||||
avatar->computeShapeInfo(shapeInfo);
|
||||
btCollisionShape* shape = const_cast<btCollisionShape*>(ObjectMotionState::getShapeManager()->getShape(shapeInfo));
|
||||
if (shape) {
|
||||
// we don't add to the simulation now, we put it on a list to be added later
|
||||
AvatarMotionState* motionState = new AvatarMotionState(avatar, shape);
|
||||
avatar->setMotionState(motionState);
|
||||
_motionStatesToAddToPhysics.insert(motionState);
|
||||
_motionStatesThatMightUpdate.insert(motionState);
|
||||
}
|
||||
}
|
||||
|
||||
void AvatarManager::updateAvatarRenderStatus(bool shouldRenderAvatars) {
|
||||
if (DependencyManager::get<SceneScriptingInterface>()->shouldRenderAvatars()) {
|
||||
for (auto avatarData : _avatarHash) {
|
||||
|
|
|
@ -69,8 +69,7 @@ public:
|
|||
void handleOutgoingChanges(const VectorOfMotionStates& motionStates);
|
||||
void handleCollisionEvents(const CollisionEvents& collisionEvents);
|
||||
|
||||
void addAvatarToSimulation(Avatar* avatar);
|
||||
|
||||
Q_INVOKABLE float getAvatarDataRate(const QUuid& sessionID, const QString& rateName = QString(""));
|
||||
Q_INVOKABLE RayToAvatarIntersectionResult findRayIntersection(const PickRay& ray,
|
||||
const QScriptValue& avatarIdsToInclude = QScriptValue(),
|
||||
const QScriptValue& avatarIdsToDiscard = QScriptValue());
|
||||
|
|
74
interface/src/avatar/CauterizedMeshPartPayload.cpp
Normal file
|
@ -0,0 +1,74 @@
|
|||
//
|
||||
// CauterizedMeshPartPayload.cpp
|
||||
// interface/src/renderer
|
||||
//
|
||||
// Created by Andrew Meadows 2017.01.17
|
||||
// Copyright 2017 High Fidelity, Inc.
|
||||
//
|
||||
// Distributed under the Apache License, Version 2.0.
|
||||
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
|
||||
//
|
||||
|
||||
#include "CauterizedMeshPartPayload.h"
|
||||
|
||||
#include <PerfStat.h>
|
||||
|
||||
#include "SkeletonModel.h"
|
||||
|
||||
using namespace render;
|
||||
|
||||
CauterizedMeshPartPayload::CauterizedMeshPartPayload(Model* model, int meshIndex, int partIndex, int shapeIndex, const Transform& transform, const Transform& offsetTransform)
|
||||
: ModelMeshPartPayload(model, meshIndex, partIndex, shapeIndex, transform, offsetTransform) {}
|
||||
|
||||
void CauterizedMeshPartPayload::updateTransformForSkinnedCauterizedMesh(const Transform& transform,
|
||||
const QVector<glm::mat4>& clusterMatrices,
|
||||
const QVector<glm::mat4>& cauterizedClusterMatrices) {
|
||||
_transform = transform;
|
||||
_cauterizedTransform = transform;
|
||||
|
||||
if (clusterMatrices.size() > 0) {
|
||||
_worldBound = AABox();
|
||||
for (auto& clusterMatrix : clusterMatrices) {
|
||||
AABox clusterBound = _localBound;
|
||||
clusterBound.transform(clusterMatrix);
|
||||
_worldBound += clusterBound;
|
||||
}
|
||||
|
||||
_worldBound.transform(transform);
|
||||
if (clusterMatrices.size() == 1) {
|
||||
_transform = _transform.worldTransform(Transform(clusterMatrices[0]));
|
||||
if (cauterizedClusterMatrices.size() != 0) {
|
||||
_cauterizedTransform = _cauterizedTransform.worldTransform(Transform(cauterizedClusterMatrices[0]));
|
||||
} else {
|
||||
_cauterizedTransform = _transform;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
_worldBound = _localBound;
|
||||
_worldBound.transform(_drawTransform);
|
||||
}
|
||||
}
|
||||
|
||||
void CauterizedMeshPartPayload::bindTransform(gpu::Batch& batch, const render::ShapePipeline::LocationsPointer locations, RenderArgs::RenderMode renderMode) const {
|
||||
// Still relying on the raw data from the model
|
||||
const Model::MeshState& state = _model->getMeshState(_meshIndex);
|
||||
SkeletonModel* skeleton = static_cast<SkeletonModel*>(_model);
|
||||
bool useCauterizedMesh = (renderMode != RenderArgs::RenderMode::SHADOW_RENDER_MODE) && skeleton->getEnableCauterization();
|
||||
|
||||
if (state.clusterBuffer) {
|
||||
if (useCauterizedMesh) {
|
||||
const Model::MeshState& cState = skeleton->getCauterizeMeshState(_meshIndex);
|
||||
batch.setUniformBuffer(ShapePipeline::Slot::BUFFER::SKINNING, cState.clusterBuffer);
|
||||
} else {
|
||||
batch.setUniformBuffer(ShapePipeline::Slot::BUFFER::SKINNING, state.clusterBuffer);
|
||||
}
|
||||
batch.setModelTransform(_transform);
|
||||
} else {
|
||||
if (useCauterizedMesh) {
|
||||
batch.setModelTransform(_cauterizedTransform);
|
||||
} else {
|
||||
batch.setModelTransform(_transform);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
29
interface/src/avatar/CauterizedMeshPartPayload.h
Normal file
|
@ -0,0 +1,29 @@
|
|||
//
|
||||
// CauterizedModelMeshPartPayload.h
|
||||
// interface/src/avatar
|
||||
//
|
||||
// Created by AndrewMeadows 2017.01.17
|
||||
// Copyright 2017 High Fidelity, Inc.
|
||||
//
|
||||
// Distributed under the Apache License, Version 2.0.
|
||||
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
|
||||
//
|
||||
|
||||
#ifndef hifi_CauterizedMeshPartPayload_h
|
||||
#define hifi_CauterizedMeshPartPayload_h
|
||||
|
||||
#include <MeshPartPayload.h>
|
||||
|
||||
class CauterizedMeshPartPayload : public ModelMeshPartPayload {
|
||||
public:
|
||||
CauterizedMeshPartPayload(Model* model, int meshIndex, int partIndex, int shapeIndex, const Transform& transform, const Transform& offsetTransform);
|
||||
void updateTransformForSkinnedCauterizedMesh(const Transform& transform,
|
||||
const QVector<glm::mat4>& clusterMatrices,
|
||||
const QVector<glm::mat4>& cauterizedClusterMatrices);
|
||||
|
||||
void bindTransform(gpu::Batch& batch, const render::ShapePipeline::LocationsPointer locations, RenderArgs::RenderMode renderMode) const override;
|
||||
private:
|
||||
Transform _cauterizedTransform;
|
||||
};
|
||||
|
||||
#endif // hifi_CauterizedMeshPartPayload_h
|
254
interface/src/avatar/CauterizedModel.cpp
Normal file
|
@ -0,0 +1,254 @@
|
|||
//
|
||||
// CauterizedModel.cpp
|
||||
// interface/src/avatar
|
||||
//
|
||||
// Created by Andrew Meadows 2017.01.17
|
||||
// Copyright 2017 High Fidelity, Inc.
|
||||
//
|
||||
// Distributed under the Apache License, Version 2.0.
|
||||
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
|
||||
//
|
||||
|
||||
#include "CauterizedModel.h"
|
||||
|
||||
#include <AbstractViewStateInterface.h>
|
||||
#include <MeshPartPayload.h>
|
||||
#include <PerfStat.h>
|
||||
|
||||
#include "CauterizedMeshPartPayload.h"
|
||||
|
||||
|
||||
CauterizedModel::CauterizedModel(RigPointer rig, QObject* parent) :
|
||||
Model(rig, parent) {
|
||||
}
|
||||
|
||||
CauterizedModel::~CauterizedModel() {
|
||||
}
|
||||
|
||||
void CauterizedModel::deleteGeometry() {
|
||||
Model::deleteGeometry();
|
||||
_cauterizeMeshStates.clear();
|
||||
}
|
||||
|
||||
bool CauterizedModel::updateGeometry() {
|
||||
bool needsFullUpdate = Model::updateGeometry();
|
||||
if (_isCauterized && needsFullUpdate) {
|
||||
assert(_cauterizeMeshStates.empty());
|
||||
const FBXGeometry& fbxGeometry = getFBXGeometry();
|
||||
foreach (const FBXMesh& mesh, fbxGeometry.meshes) {
|
||||
Model::MeshState state;
|
||||
state.clusterMatrices.resize(mesh.clusters.size());
|
||||
_cauterizeMeshStates.append(state);
|
||||
}
|
||||
}
|
||||
return needsFullUpdate;
|
||||
}
|
||||
|
||||
void CauterizedModel::createVisibleRenderItemSet() {
|
||||
if (_isCauterized) {
|
||||
assert(isLoaded());
|
||||
const auto& meshes = _renderGeometry->getMeshes();
|
||||
|
||||
// all of our mesh vectors must match in size
|
||||
if ((int)meshes.size() != _meshStates.size()) {
|
||||
qCDebug(renderlogging) << "WARNING!!!! Mesh Sizes don't match! We will not segregate mesh groups yet.";
|
||||
return;
|
||||
}
|
||||
|
||||
// We should not have any existing renderItems if we enter this section of code
|
||||
Q_ASSERT(_modelMeshRenderItemsSet.isEmpty());
|
||||
|
||||
_modelMeshRenderItemsSet.clear();
|
||||
|
||||
Transform transform;
|
||||
transform.setTranslation(_translation);
|
||||
transform.setRotation(_rotation);
|
||||
|
||||
Transform offset;
|
||||
offset.setScale(_scale);
|
||||
offset.postTranslate(_offset);
|
||||
|
||||
// Run through all of the meshes, and place them into their segregated, but unsorted buckets
|
||||
int shapeID = 0;
|
||||
uint32_t numMeshes = (uint32_t)meshes.size();
|
||||
for (uint32_t i = 0; i < numMeshes; i++) {
|
||||
const auto& mesh = meshes.at(i);
|
||||
if (!mesh) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Create the render payloads
|
||||
int numParts = (int)mesh->getNumParts();
|
||||
for (int partIndex = 0; partIndex < numParts; partIndex++) {
|
||||
auto ptr = std::make_shared<CauterizedMeshPartPayload>(this, i, partIndex, shapeID, transform, offset);
|
||||
_modelMeshRenderItemsSet << std::static_pointer_cast<ModelMeshPartPayload>(ptr);
|
||||
shapeID++;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Model::createVisibleRenderItemSet();
|
||||
}
|
||||
}
|
||||
|
||||
void CauterizedModel::createCollisionRenderItemSet() {
|
||||
// Temporary HACK: use base class method for now
|
||||
Model::createCollisionRenderItemSet();
|
||||
}
|
||||
|
||||
// Called within Model::simulate call, below.
|
||||
void CauterizedModel::updateRig(float deltaTime, glm::mat4 parentTransform) {
|
||||
Model::updateRig(deltaTime, parentTransform);
|
||||
_needsUpdateClusterMatrices = true;
|
||||
}
|
||||
|
||||
void CauterizedModel::updateClusterMatrices() {
|
||||
PerformanceTimer perfTimer("CauterizedModel::updateClusterMatrices");
|
||||
|
||||
if (!_needsUpdateClusterMatrices || !isLoaded()) {
|
||||
return;
|
||||
}
|
||||
_needsUpdateClusterMatrices = false;
|
||||
const FBXGeometry& geometry = getFBXGeometry();
|
||||
|
||||
for (int i = 0; i < _meshStates.size(); i++) {
|
||||
Model::MeshState& state = _meshStates[i];
|
||||
const FBXMesh& mesh = geometry.meshes.at(i);
|
||||
for (int j = 0; j < mesh.clusters.size(); j++) {
|
||||
const FBXCluster& cluster = mesh.clusters.at(j);
|
||||
auto jointMatrix = _rig->getJointTransform(cluster.jointIndex);
|
||||
#if GLM_ARCH & GLM_ARCH_SSE2
|
||||
glm::mat4 out, inverseBindMatrix = cluster.inverseBindMatrix;
|
||||
glm_mat4_mul((glm_vec4*)&jointMatrix, (glm_vec4*)&inverseBindMatrix, (glm_vec4*)&out);
|
||||
state.clusterMatrices[j] = out;
|
||||
#else
|
||||
state.clusterMatrices[j] = jointMatrix * cluster.inverseBindMatrix;
|
||||
#endif
|
||||
}
|
||||
|
||||
// Once computed the cluster matrices, update the buffer(s)
|
||||
if (mesh.clusters.size() > 1) {
|
||||
if (!state.clusterBuffer) {
|
||||
state.clusterBuffer = std::make_shared<gpu::Buffer>(state.clusterMatrices.size() * sizeof(glm::mat4),
|
||||
(const gpu::Byte*) state.clusterMatrices.constData());
|
||||
} else {
|
||||
state.clusterBuffer->setSubData(0, state.clusterMatrices.size() * sizeof(glm::mat4),
|
||||
(const gpu::Byte*) state.clusterMatrices.constData());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// as an optimization, don't build cautrizedClusterMatrices if the boneSet is empty.
|
||||
if (!_cauterizeBoneSet.empty()) {
|
||||
static const glm::mat4 zeroScale(
|
||||
glm::vec4(0.0f, 0.0f, 0.0f, 0.0f),
|
||||
glm::vec4(0.0f, 0.0f, 0.0f, 0.0f),
|
||||
glm::vec4(0.0f, 0.0f, 0.0f, 0.0f),
|
||||
glm::vec4(0.0f, 0.0f, 0.0f, 1.0f));
|
||||
auto cauterizeMatrix = _rig->getJointTransform(geometry.neckJointIndex) * zeroScale;
|
||||
|
||||
for (int i = 0; i < _cauterizeMeshStates.size(); i++) {
|
||||
Model::MeshState& state = _cauterizeMeshStates[i];
|
||||
const FBXMesh& mesh = geometry.meshes.at(i);
|
||||
for (int j = 0; j < mesh.clusters.size(); j++) {
|
||||
const FBXCluster& cluster = mesh.clusters.at(j);
|
||||
auto jointMatrix = _rig->getJointTransform(cluster.jointIndex);
|
||||
if (_cauterizeBoneSet.find(cluster.jointIndex) != _cauterizeBoneSet.end()) {
|
||||
jointMatrix = cauterizeMatrix;
|
||||
}
|
||||
#if GLM_ARCH & GLM_ARCH_SSE2
|
||||
glm::mat4 out, inverseBindMatrix = cluster.inverseBindMatrix;
|
||||
glm_mat4_mul((glm_vec4*)&jointMatrix, (glm_vec4*)&inverseBindMatrix, (glm_vec4*)&out);
|
||||
state.clusterMatrices[j] = out;
|
||||
#else
|
||||
state.clusterMatrices[j] = jointMatrix * cluster.inverseBindMatrix;
|
||||
#endif
|
||||
}
|
||||
|
||||
if (!_cauterizeBoneSet.empty() && (state.clusterMatrices.size() > 1)) {
|
||||
if (!state.clusterBuffer) {
|
||||
state.clusterBuffer =
|
||||
std::make_shared<gpu::Buffer>(state.clusterMatrices.size() * sizeof(glm::mat4),
|
||||
(const gpu::Byte*) state.clusterMatrices.constData());
|
||||
} else {
|
||||
state.clusterBuffer->setSubData(0, state.clusterMatrices.size() * sizeof(glm::mat4),
|
||||
(const gpu::Byte*) state.clusterMatrices.constData());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// post the blender if we're not currently waiting for one to finish
|
||||
if (geometry.hasBlendedMeshes() && _blendshapeCoefficients != _blendedBlendshapeCoefficients) {
|
||||
_blendedBlendshapeCoefficients = _blendshapeCoefficients;
|
||||
DependencyManager::get<ModelBlender>()->noteRequiresBlend(getThisPointer());
|
||||
}
|
||||
}
|
||||
|
||||
void CauterizedModel::updateRenderItems() {
|
||||
if (_isCauterized) {
|
||||
if (!_addedToScene) {
|
||||
return;
|
||||
}
|
||||
|
||||
glm::vec3 scale = getScale();
|
||||
if (_collisionGeometry) {
|
||||
// _collisionGeometry is already scaled
|
||||
scale = glm::vec3(1.0f);
|
||||
}
|
||||
_needsUpdateClusterMatrices = true;
|
||||
_renderItemsNeedUpdate = false;
|
||||
|
||||
// queue up this work for later processing, at the end of update and just before rendering.
|
||||
// the application will ensure only the last lambda is actually invoked.
|
||||
void* key = (void*)this;
|
||||
std::weak_ptr<Model> weakSelf = shared_from_this();
|
||||
AbstractViewStateInterface::instance()->pushPostUpdateLambda(key, [weakSelf, scale]() {
|
||||
// do nothing, if the model has already been destroyed.
|
||||
auto self = weakSelf.lock();
|
||||
if (!self) {
|
||||
return;
|
||||
}
|
||||
|
||||
render::ScenePointer scene = AbstractViewStateInterface::instance()->getMain3DScene();
|
||||
|
||||
Transform modelTransform;
|
||||
modelTransform.setTranslation(self->getTranslation());
|
||||
modelTransform.setRotation(self->getRotation());
|
||||
|
||||
Transform scaledModelTransform(modelTransform);
|
||||
scaledModelTransform.setScale(scale);
|
||||
|
||||
uint32_t deleteGeometryCounter = self->getGeometryCounter();
|
||||
|
||||
render::PendingChanges pendingChanges;
|
||||
QList<render::ItemID> keys = self->getRenderItems().keys();
|
||||
foreach (auto itemID, keys) {
|
||||
pendingChanges.updateItem<CauterizedMeshPartPayload>(itemID, [modelTransform, deleteGeometryCounter](CauterizedMeshPartPayload& data) {
|
||||
if (data._model && data._model->isLoaded()) {
|
||||
// Ensure the model geometry was not reset between frames
|
||||
if (deleteGeometryCounter == data._model->getGeometryCounter()) {
|
||||
// lazy update of cluster matrices used for rendering. We need to update them here, so we can correctly update the bounding box.
|
||||
data._model->updateClusterMatrices();
|
||||
|
||||
// update the model transform and bounding box for this render item.
|
||||
const Model::MeshState& state = data._model->getMeshState(data._meshIndex);
|
||||
CauterizedModel* cModel = static_cast<CauterizedModel*>(data._model);
|
||||
assert(data._meshIndex < cModel->_cauterizeMeshStates.size());
|
||||
const Model::MeshState& cState = cModel->_cauterizeMeshStates.at(data._meshIndex);
|
||||
data.updateTransformForSkinnedCauterizedMesh(modelTransform, state.clusterMatrices, cState.clusterMatrices);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
scene->enqueuePendingChanges(pendingChanges);
|
||||
});
|
||||
} else {
|
||||
Model::updateRenderItems();
|
||||
}
|
||||
}
|
||||
|
||||
const Model::MeshState& CauterizedModel::getCauterizeMeshState(int index) const {
|
||||
assert(index < _meshStates.size());
|
||||
return _cauterizeMeshStates.at(index);
|
||||
}
|
53
interface/src/avatar/CauterizedModel.h
Normal file
|
@ -0,0 +1,53 @@
|
|||
//
|
||||
// CauterizeableModel.h
|
||||
// interface/src/avatar
|
||||
//
|
||||
// Created by Andrew Meadows 2016.01.17
|
||||
// Copyright 2017 High Fidelity, Inc.
|
||||
//
|
||||
// Distributed under the Apache License, Version 2.0.
|
||||
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
|
||||
//
|
||||
|
||||
#ifndef hifi_CauterizedModel_h
|
||||
#define hifi_CauterizedModel_h
|
||||
|
||||
|
||||
#include <Model.h>
|
||||
|
||||
class CauterizedModel : public Model {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
CauterizedModel(RigPointer rig, QObject* parent);
|
||||
virtual ~CauterizedModel();
|
||||
|
||||
void flagAsCauterized() { _isCauterized = true; }
|
||||
bool getIsCauterized() const { return _isCauterized; }
|
||||
|
||||
void setEnableCauterization(bool flag) { _enableCauterization = flag; }
|
||||
bool getEnableCauterization() const { return _enableCauterization; }
|
||||
|
||||
const std::unordered_set<int>& getCauterizeBoneSet() const { return _cauterizeBoneSet; }
|
||||
void setCauterizeBoneSet(const std::unordered_set<int>& boneSet) { _cauterizeBoneSet = boneSet; }
|
||||
|
||||
void deleteGeometry() override;
|
||||
bool updateGeometry() override;
|
||||
|
||||
void createVisibleRenderItemSet() override;
|
||||
void createCollisionRenderItemSet() override;
|
||||
|
||||
virtual void updateRig(float deltaTime, glm::mat4 parentTransform) override;
|
||||
virtual void updateClusterMatrices() override;
|
||||
void updateRenderItems() override;
|
||||
|
||||
const Model::MeshState& getCauterizeMeshState(int index) const;
|
||||
|
||||
protected:
|
||||
std::unordered_set<int> _cauterizeBoneSet;
|
||||
QVector<Model::MeshState> _cauterizeMeshStates;
|
||||
bool _isCauterized { false };
|
||||
bool _enableCauterization { false };
|
||||
};
|
||||
|
||||
#endif // hifi_CauterizedModel_h
|
|
@ -70,7 +70,7 @@ void Head::reset() {
|
|||
_baseYaw = _basePitch = _baseRoll = 0.0f;
|
||||
}
|
||||
|
||||
void Head::simulate(float deltaTime, bool isMine, bool billboard) {
|
||||
void Head::simulate(float deltaTime, bool isMine) {
|
||||
// Update audio trailing average for rendering facial animations
|
||||
const float AUDIO_AVERAGING_SECS = 0.05f;
|
||||
const float AUDIO_LONG_TERM_AVERAGING_SECS = 30.0f;
|
||||
|
@ -117,7 +117,7 @@ void Head::simulate(float deltaTime, bool isMine, bool billboard) {
|
|||
}
|
||||
}
|
||||
|
||||
if (!(_isFaceTrackerConnected || billboard)) {
|
||||
if (!_isFaceTrackerConnected) {
|
||||
|
||||
if (!_isEyeTrackerConnected) {
|
||||
// Update eye saccades
|
||||
|
@ -220,7 +220,7 @@ void Head::simulate(float deltaTime, bool isMine, bool billboard) {
|
|||
_leftEyePosition = _rightEyePosition = getPosition();
|
||||
_eyePosition = getPosition();
|
||||
|
||||
if (!billboard && _owningAvatar) {
|
||||
if (_owningAvatar) {
|
||||
auto skeletonModel = static_cast<Avatar*>(_owningAvatar)->getSkeletonModel();
|
||||
if (skeletonModel) {
|
||||
skeletonModel->getEyePositions(_leftEyePosition, _rightEyePosition);
|
||||
|
@ -378,10 +378,6 @@ glm::quat Head::getEyeRotation(const glm::vec3& eyePosition) const {
|
|||
return rotationBetween(orientation * IDENTITY_FRONT, lookAtDelta + glm::length(lookAtDelta) * _saccade) * orientation;
|
||||
}
|
||||
|
||||
glm::vec3 Head::getScalePivot() const {
|
||||
return _position;
|
||||
}
|
||||
|
||||
void Head::setFinalPitch(float finalPitch) {
|
||||
_deltaPitch = glm::clamp(finalPitch, MIN_HEAD_PITCH, MAX_HEAD_PITCH) - _basePitch;
|
||||
}
|
||||
|
|