Merge branch 'master' of https://github.com/highfidelity/hifi into hdr

This commit is contained in:
samcake 2016-11-21 11:30:26 -08:00
commit 31232a1078
57 changed files with 1441 additions and 370 deletions

View file

@ -15,7 +15,6 @@
// this should send a signal every 10ms, with pretty good precision. Hardcoding
// to 10ms since that's what you'd want for audio.
void AvatarAudioTimer::start() {
qDebug() << __FUNCTION__;
auto startTime = usecTimestampNow();
quint64 frameCounter = 0;
const int TARGET_INTERVAL_USEC = 10000; // 10ms

View file

@ -95,7 +95,7 @@ AudioMixer::AudioMixer(ReceivedMessage& message) :
packetReceiver.registerListener(PacketType::NodeIgnoreRequest, this, "handleNodeIgnoreRequestPacket");
packetReceiver.registerListener(PacketType::KillAvatar, this, "handleKillAvatarPacket");
packetReceiver.registerListener(PacketType::NodeMuteRequest, this, "handleNodeMuteRequestPacket");
packetReceiver.registerListener(PacketType::RadiusIgnoreRequest, this, "handleRadiusIgnoreRequestPacket");
connect(nodeList.data(), &NodeList::nodeKilled, this, &AudioMixer::handleNodeKilled);
}
@ -393,16 +393,26 @@ bool AudioMixer::prepareMixForListeningNode(Node* node) {
&& !node->isIgnoringNodeWithID(otherNode->getUUID()) && !otherNode->isIgnoringNodeWithID(node->getUUID())) {
AudioMixerClientData* otherNodeClientData = (AudioMixerClientData*) otherNode->getLinkedData();
// enumerate the ARBs attached to the otherNode and add all that should be added to mix
auto streamsCopy = otherNodeClientData->getAudioStreams();
// check to see if we're ignoring in radius
bool insideIgnoreRadius = false;
if (node->isIgnoreRadiusEnabled() || otherNode->isIgnoreRadiusEnabled()) {
AudioMixerClientData* otherData = reinterpret_cast<AudioMixerClientData*>(otherNode->getLinkedData());
AudioMixerClientData* nodeData = reinterpret_cast<AudioMixerClientData*>(node->getLinkedData());
float ignoreRadius = glm::min(node->getIgnoreRadius(), otherNode->getIgnoreRadius());
if (glm::distance(nodeData->getPosition(), otherData->getPosition()) < ignoreRadius) {
insideIgnoreRadius = true;
}
}
for (auto& streamPair : streamsCopy) {
auto otherNodeStream = streamPair.second;
if (*otherNode != *node || otherNodeStream->shouldLoopbackForNode()) {
addStreamToMixForListeningNodeWithStream(*listenerNodeData, *otherNodeStream, otherNode->getUUID(),
*nodeAudioStream);
if (!insideIgnoreRadius) {
// enumerate the ARBs attached to the otherNode and add all that should be added to mix
auto streamsCopy = otherNodeClientData->getAudioStreams();
for (auto& streamPair : streamsCopy) {
auto otherNodeStream = streamPair.second;
if (*otherNode != *node || otherNodeStream->shouldLoopbackForNode()) {
addStreamToMixForListeningNodeWithStream(*listenerNodeData, *otherNodeStream, otherNode->getUUID(),
*nodeAudioStream);
}
}
}
}
@ -634,11 +644,14 @@ void AudioMixer::handleKillAvatarPacket(QSharedPointer<ReceivedMessage> packet,
}
}
void AudioMixer::handleNodeIgnoreRequestPacket(QSharedPointer<ReceivedMessage> packet, SharedNodePointer sendingNode) {
sendingNode->parseIgnoreRequestMessage(packet);
}
void AudioMixer::handleRadiusIgnoreRequestPacket(QSharedPointer<ReceivedMessage> packet, SharedNodePointer sendingNode) {
sendingNode->parseIgnoreRadiusRequestMessage(packet);
}
void AudioMixer::removeHRTFsForFinishedInjector(const QUuid& streamID) {
auto injectorClientData = qobject_cast<AudioMixerClientData*>(sender());
if (injectorClientData) {

View file

@ -48,6 +48,7 @@ private slots:
void handleNegotiateAudioFormat(QSharedPointer<ReceivedMessage> message, SharedNodePointer sendingNode);
void handleNodeKilled(SharedNodePointer killedNode);
void handleNodeIgnoreRequestPacket(QSharedPointer<ReceivedMessage> packet, SharedNodePointer sendingNode);
void handleRadiusIgnoreRequestPacket(QSharedPointer<ReceivedMessage> packet, SharedNodePointer sendingNode);
void handleKillAvatarPacket(QSharedPointer<ReceivedMessage> packet, SharedNodePointer sendingNode);
void handleNodeMuteRequestPacket(QSharedPointer<ReceivedMessage> packet, SharedNodePointer sendingNode);

View file

@ -365,10 +365,6 @@ QJsonObject AudioMixerClientData::getAudioStreamStats() {
}
void AudioMixerClientData::handleMismatchAudioFormat(SharedNodePointer node, const QString& currentCodec, const QString& recievedCodec) {
qDebug() << __FUNCTION__ <<
"sendingNode:" << *node <<
"currentCodec:" << currentCodec <<
"receivedCodec:" << recievedCodec;
sendSelectAudioFormat(node, currentCodec);
}

View file

@ -89,6 +89,7 @@ public:
bool shouldMuteClient() { return _shouldMuteClient; }
void setShouldMuteClient(bool shouldMuteClient) { _shouldMuteClient = shouldMuteClient; }
glm::vec3 getPosition() { return getAvatarAudioStream() ? getAvatarAudioStream()->getPosition() : glm::vec3(0); }
signals:
void injectorStreamFinished(const QUuid& streamIdentifier);

View file

@ -46,6 +46,7 @@ AvatarMixer::AvatarMixer(ReceivedMessage& message) :
packetReceiver.registerListener(PacketType::AvatarIdentity, this, "handleAvatarIdentityPacket");
packetReceiver.registerListener(PacketType::KillAvatar, this, "handleKillAvatarPacket");
packetReceiver.registerListener(PacketType::NodeIgnoreRequest, this, "handleNodeIgnoreRequestPacket");
packetReceiver.registerListener(PacketType::RadiusIgnoreRequest, this, "handleRadiusIgnoreRequestPacket");
auto nodeList = DependencyManager::get<NodeList>();
connect(nodeList.data(), &NodeList::packetVersionMismatch, this, &AvatarMixer::handlePacketVersionMismatch);
@ -237,6 +238,20 @@ void AvatarMixer::broadcastAvatarData() {
|| otherNode->isIgnoringNodeWithID(node->getUUID())) {
return false;
} else {
AvatarMixerClientData* otherData = reinterpret_cast<AvatarMixerClientData*>(otherNode->getLinkedData());
AvatarMixerClientData* nodeData = reinterpret_cast<AvatarMixerClientData*>(node->getLinkedData());
// check to see if we're ignoring in radius
if (node->isIgnoreRadiusEnabled() || otherNode->isIgnoreRadiusEnabled()) {
float ignoreRadius = glm::min(node->getIgnoreRadius(), otherNode->getIgnoreRadius());
if (glm::distance(nodeData->getPosition(), otherData->getPosition()) < ignoreRadius) {
nodeData->ignoreOther(node, otherNode);
otherData->ignoreOther(otherNode, node);
return false;
}
}
// not close enough to ignore
nodeData->removeFromRadiusIgnoringSet(otherNode->getUUID());
otherData->removeFromRadiusIgnoringSet(node->getUUID());
return true;
}
},
@ -442,6 +457,10 @@ void AvatarMixer::handleNodeIgnoreRequestPacket(QSharedPointer<ReceivedMessage>
senderNode->parseIgnoreRequestMessage(message);
}
void AvatarMixer::handleRadiusIgnoreRequestPacket(QSharedPointer<ReceivedMessage> packet, SharedNodePointer sendingNode) {
sendingNode->parseIgnoreRadiusRequestMessage(packet);
}
void AvatarMixer::sendStatsPacket() {
QJsonObject statsObject;
statsObject["average_listeners_last_second"] = (float) _sumListeners / (float) _numStatFrames;

View file

@ -38,6 +38,7 @@ private slots:
void handleAvatarIdentityPacket(QSharedPointer<ReceivedMessage> message, SharedNodePointer senderNode);
void handleKillAvatarPacket(QSharedPointer<ReceivedMessage> message);
void handleNodeIgnoreRequestPacket(QSharedPointer<ReceivedMessage> message, SharedNodePointer senderNode);
void handleRadiusIgnoreRequestPacket(QSharedPointer<ReceivedMessage> packet, SharedNodePointer sendingNode);
void domainSettingsRequestComplete();
void handlePacketVersionMismatch(PacketType type, const HifiSockAddr& senderSockAddr, const QUuid& senderUUID);

View file

@ -11,6 +11,9 @@
#include <udt/PacketHeaders.h>
#include <DependencyManager.h>
#include <NodeList.h>
#include "AvatarMixerClientData.h"
int AvatarMixerClientData::parseData(ReceivedMessage& message) {
@ -39,6 +42,16 @@ uint16_t AvatarMixerClientData::getLastBroadcastSequenceNumber(const QUuid& node
}
}
void AvatarMixerClientData::ignoreOther(SharedNodePointer self, SharedNodePointer other) {
if (!isRadiusIgnoring(other->getUUID())) {
addToRadiusIgnoringSet(other->getUUID());
auto killPacket = NLPacket::create(PacketType::KillAvatar, NUM_BYTES_RFC4122_UUID);
killPacket->write(other->getUUID().toRfc4122());
DependencyManager::get<NodeList>()->sendUnreliablePacket(*killPacket, *self);
_hasReceivedFirstPacketsFrom.erase(other->getUUID());
}
}
void AvatarMixerClientData::loadJSONStats(QJsonObject& jsonObject) const {
jsonObject["display_name"] = _avatar->getDisplayName();
jsonObject["full_rate_distance"] = _fullRateDistance;

View file

@ -79,6 +79,13 @@ public:
{ return _avgOtherAvatarDataRate.getAverageSampleValuePerSecond() / (float) BYTES_PER_KILOBIT; }
void loadJSONStats(QJsonObject& jsonObject) const;
glm::vec3 getPosition() { return _avatar ? _avatar->getPosition() : glm::vec3(0); }
bool isRadiusIgnoring(const QUuid& other) { return _radiusIgnoredOthers.find(other) != _radiusIgnoredOthers.end(); }
void addToRadiusIgnoringSet(const QUuid& other) { _radiusIgnoredOthers.insert(other); }
void removeFromRadiusIgnoringSet(const QUuid& other) { _radiusIgnoredOthers.erase(other); }
void ignoreOther(SharedNodePointer self, SharedNodePointer other);
private:
AvatarSharedPointer _avatar { new AvatarData() };
@ -99,6 +106,7 @@ private:
int _numOutOfOrderSends = 0;
SimpleMovingAverage _avgOtherAvatarDataRate;
std::unordered_set<QUuid> _radiusIgnoredOthers;
};
#endif // hifi_AvatarMixerClientData_h

View file

@ -0,0 +1,20 @@
set(EXTERNAL_NAME GifCreator)
include(ExternalProject)
ExternalProject_Add(
${EXTERNAL_NAME}
URL https://hifi-public.s3.amazonaws.com/dependencies/GifCreator.zip
URL_MD5 8ac8ef5196f47c658dce784df5ecdb70
CONFIGURE_COMMAND ""
BUILD_COMMAND ""
INSTALL_COMMAND ""
LOG_DOWNLOAD 1
)
# Hide this external target (for ide users)
set_target_properties(${EXTERNAL_NAME} PROPERTIES FOLDER "hidden/externals")
ExternalProject_Get_Property(${EXTERNAL_NAME} INSTALL_DIR)
string(TOUPPER ${EXTERNAL_NAME} EXTERNAL_NAME_UPPER)
set(${EXTERNAL_NAME_UPPER}_INCLUDE_DIRS ${INSTALL_DIR}/src/${EXTERNAL_NAME} CACHE PATH "List of GifCreator include directories")

View file

@ -6,8 +6,8 @@ if (WIN32)
include(ExternalProject)
ExternalProject_Add(
${EXTERNAL_NAME}
URL http://hifi-public.s3.amazonaws.com/dependencies/qtaudio_wasapi3.zip
URL_MD5 1a2433f80a788a54c70f505ff4f43ac1
URL http://hifi-public.s3.amazonaws.com/dependencies/qtaudio_wasapi4.zip
URL_MD5 2abde5340a64d387848f12b9536a7e85
CONFIGURE_COMMAND ""
BUILD_COMMAND ""
INSTALL_COMMAND ""

View file

@ -0,0 +1,26 @@
#
# FindGifCreator.cmake
#
# Try to find GifCreator include path.
# Once done this will define
#
# GIFCREATOR_INCLUDE_DIRS
#
# Created on 11/15/2016 by Zach Fox
# 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
#
# setup hints for GifCreator search
include("${MACRO_DIR}/HifiLibrarySearchHints.cmake")
hifi_library_search_hints("GIFCREATOR")
# locate header
find_path(GIFCREATOR_INCLUDE_DIRS "GifCreator/GifCreator.h" HINTS ${GIFCREATOR_SEARCH_DIRS})
include(FindPackageHandleStandardArgs)
find_package_handle_standard_args(GIFCREATOR DEFAULT_MSG GIFCREATOR_INCLUDE_DIRS)
mark_as_advanced(GIFCREATOR_INCLUDE_DIRS GIFCREATOR_SEARCH_DIRS)

View file

@ -684,6 +684,79 @@
}
]
},
{
"name": "permissions",
"type": "table",
"caption": "Permissions for Specific Users",
"can_add_new_rows": true,
"groups": [
{
"label": "User",
"span": 1
},
{
"label": "Permissions <a data-toggle='tooltip' data-html='true' title='<p><strong>Domain-Wide User Permissions</strong></p><ul><li><strong>Connect</strong><br />Sets whether a user can connect to the domain.</li><li><strong>Lock / Unlock</strong><br />Sets whether a user change the &ldquo;locked&rdquo; property of an entity (either from on to off or off to on).</li><li><strong>Rez</strong><br />Sets whether a user can create new entities.</li><li><strong>Rez Temporary</strong><br />Sets whether a user can create new entities with a finite lifetime.</li><li><strong>Write Assets</strong><br />Sets whether a user can make changes to the domain&rsquo;s asset-server assets.</li><li><strong>Ignore Max Capacity</strong><br />Sets whether a user can connect even if the domain has reached or exceeded its maximum allowed agents.</li></ul><p>Note that permissions assigned to a specific user will supersede any parameter-level or group permissions that might otherwise apply to that user.</p>'>?</a>",
"span": 7
}
],
"columns": [
{
"name": "permissions_id",
"label": ""
},
{
"name": "id_can_connect",
"label": "Connect",
"type": "checkbox",
"editable": true,
"default": false
},
{
"name": "id_can_adjust_locks",
"label": "Lock / Unlock",
"type": "checkbox",
"editable": true,
"default": false
},
{
"name": "id_can_rez",
"label": "Rez",
"type": "checkbox",
"editable": true,
"default": false
},
{
"name": "id_can_rez_tmp",
"label": "Rez Temporary",
"type": "checkbox",
"editable": true,
"default": false
},
{
"name": "id_can_write_to_asset_server",
"label": "Write Assets",
"type": "checkbox",
"editable": true,
"default": false
},
{
"name": "id_can_connect_past_max_capacity",
"label": "Ignore Max Capacity",
"type": "checkbox",
"editable": true,
"default": false
},
{
"name": "id_can_kick",
"label": "Kick Users",
"type": "checkbox",
"editable": true,
"default": false
}
]
},
{
"name": "ip_permissions",
"type": "table",
@ -757,18 +830,17 @@
]
},
{
"name": "permissions",
"name": "mac_permissions",
"type": "table",
"caption": "Permissions for Specific Users",
"caption": "Permissions for Users with MAC Addresses",
"can_add_new_rows": true,
"groups": [
{
"label": "User",
"label": "MAC Address",
"span": 1
},
{
"label": "Permissions <a data-toggle='tooltip' data-html='true' title='<p><strong>Domain-Wide User Permissions</strong></p><ul><li><strong>Connect</strong><br />Sets whether a user can connect to the domain.</li><li><strong>Lock / Unlock</strong><br />Sets whether a user change the &ldquo;locked&rdquo; property of an entity (either from on to off or off to on).</li><li><strong>Rez</strong><br />Sets whether a user can create new entities.</li><li><strong>Rez Temporary</strong><br />Sets whether a user can create new entities with a finite lifetime.</li><li><strong>Write Assets</strong><br />Sets whether a user can make changes to the domain&rsquo;s asset-server assets.</li><li><strong>Ignore Max Capacity</strong><br />Sets whether a user can connect even if the domain has reached or exceeded its maximum allowed agents.</li></ul><p>Note that permissions assigned to a specific user will supersede any parameter-level or group permissions that might otherwise apply to that user.</p>'>?</a>",
"label": "Permissions <a data-toggle='tooltip' data-html='true' title='<p><strong>Domain-Wide MAC Permissions</strong></p><ul><li><strong>Connect</strong><br />Sets whether users with specific MACs can connect to the domain.</li><li><strong>Lock / Unlock</strong><br />Sets whether users from specific MACs can change the &ldquo;locked&rdquo; property of an entity (either from on to off or off to on).</li><li><strong>Rez</strong><br />Sets whether users with specific MACs can create new entities.</li><li><strong>Rez Temporary</strong><br />Sets whether users with specific MACs can create new entities with a finite lifetime.</li><li><strong>Write Assets</strong><br />Sets whether users with specific MACs can make changes to the domain&rsquo;s asset-server assets.</li><li><strong>Ignore Max Capacity</strong><br />Sets whether users with specific MACs can connect even if the domain has reached or exceeded its maximum allowed agents.</li></ul><p>Note that permissions assigned to a specific MAC will supersede any parameter-level permissions that might otherwise apply to that user (from groups or standard permissions above). MAC address permissions are overriden if the user has their own row in the users section.</p>'>?</a>",
"span": 7
}
],

View file

@ -119,15 +119,20 @@ void DomainGatekeeper::processConnectRequestPacket(QSharedPointer<ReceivedMessag
nodeData->setNodeInterestSet(safeInterestSet);
nodeData->setPlaceName(nodeConnection.placeName);
qDebug() << "Allowed connection from node" << uuidStringWithoutCurlyBraces(node->getUUID())
<< "on" << message->getSenderSockAddr() << "with MAC" << nodeConnection.hardwareAddress;
// signal that we just connected a node so the DomainServer can get it a list
// and broadcast its presence right away
emit connectedNode(node);
} else {
qDebug() << "Refusing connection from node at" << message->getSenderSockAddr();
qDebug() << "Refusing connection from node at" << message->getSenderSockAddr()
<< "with hardware address" << nodeConnection.hardwareAddress;
}
}
NodePermissions DomainGatekeeper::setPermissionsForUser(bool isLocalUser, QString verifiedUsername, const QHostAddress& senderAddress) {
NodePermissions DomainGatekeeper::setPermissionsForUser(bool isLocalUser, QString verifiedUsername,
const QHostAddress& senderAddress, const QString& hardwareAddress) {
NodePermissions userPerms;
userPerms.setAll(false);
@ -144,8 +149,14 @@ NodePermissions DomainGatekeeper::setPermissionsForUser(bool isLocalUser, QStrin
#ifdef WANT_DEBUG
qDebug() << "| user-permissions: unverified or no username for" << userPerms.getID() << ", so:" << userPerms;
#endif
if (!hardwareAddress.isEmpty() && _server->_settingsManager.hasPermissionsForMAC(hardwareAddress)) {
// this user comes from a MAC we have in our permissions table, apply those permissions
userPerms = _server->_settingsManager.getPermissionsForMAC(hardwareAddress);
if (_server->_settingsManager.hasPermissionsForIP(senderAddress)) {
#ifdef WANT_DEBUG
qDebug() << "| user-permissions: specific MAC matches, so:" << userPerms;
#endif
} else if (_server->_settingsManager.hasPermissionsForIP(senderAddress)) {
// this user comes from an IP we have in our permissions table, apply those permissions
userPerms = _server->_settingsManager.getPermissionsForIP(senderAddress);
@ -158,6 +169,13 @@ NodePermissions DomainGatekeeper::setPermissionsForUser(bool isLocalUser, QStrin
userPerms = _server->_settingsManager.getPermissionsForName(verifiedUsername);
#ifdef WANT_DEBUG
qDebug() << "| user-permissions: specific user matches, so:" << userPerms;
#endif
} else if (!hardwareAddress.isEmpty() && _server->_settingsManager.hasPermissionsForMAC(hardwareAddress)) {
// this user comes from a MAC we have in our permissions table, apply those permissions
userPerms = _server->_settingsManager.getPermissionsForMAC(hardwareAddress);
#ifdef WANT_DEBUG
qDebug() << "| user-permissions: specific MAC matches, so:" << userPerms;
#endif
} else if (_server->_settingsManager.hasPermissionsForIP(senderAddress)) {
// this user comes from an IP we have in our permissions table, apply those permissions
@ -255,7 +273,14 @@ void DomainGatekeeper::updateNodePermissions() {
// or the public socket if we haven't activated a socket for the node yet
HifiSockAddr connectingAddr = node->getActiveSocket() ? *node->getActiveSocket() : node->getPublicSocket();
userPerms = setPermissionsForUser(isLocalUser, verifiedUsername, connectingAddr.getAddress());
QString hardwareAddress;
DomainServerNodeData* nodeData = reinterpret_cast<DomainServerNodeData*>(node->getLinkedData());
if (nodeData) {
hardwareAddress = nodeData->getHardwareAddress();
}
userPerms = setPermissionsForUser(isLocalUser, verifiedUsername, connectingAddr.getAddress(), hardwareAddress);
}
node->setPermissions(userPerms);
@ -308,6 +333,7 @@ SharedNodePointer DomainGatekeeper::processAssignmentConnectRequest(const NodeCo
nodeData->setAssignmentUUID(matchingQueuedAssignment->getUUID());
nodeData->setWalletUUID(it->second.getWalletUUID());
nodeData->setNodeVersion(it->second.getNodeVersion());
nodeData->setHardwareAddress(nodeConnection.hardwareAddress);
nodeData->setWasAssigned(true);
// cleanup the PendingAssignedNodeData for this assignment now that it's connecting
@ -369,7 +395,8 @@ SharedNodePointer DomainGatekeeper::processAgentConnectRequest(const NodeConnect
}
}
userPerms = setPermissionsForUser(isLocalUser, verifiedUsername, nodeConnection.senderSockAddr.getAddress());
userPerms = setPermissionsForUser(isLocalUser, verifiedUsername, nodeConnection.senderSockAddr.getAddress(),
nodeConnection.hardwareAddress);
if (!userPerms.can(NodePermissions::Permission::canConnectToDomain)) {
sendConnectionDeniedPacket("You lack the required permissions to connect to this domain.",
@ -425,6 +452,9 @@ SharedNodePointer DomainGatekeeper::processAgentConnectRequest(const NodeConnect
// if we have a username from the connect request, set it on the DomainServerNodeData
nodeData->setUsername(username);
// set the hardware address passed in the connect request
nodeData->setHardwareAddress(nodeConnection.hardwareAddress);
// also add an interpolation to DomainServerNodeData so that servers can get username in stats
nodeData->addOverrideForKey(USERNAME_UUID_REPLACEMENT_STATS_KEY,
uuidStringWithoutCurlyBraces(newNode->getUUID()), username);

View file

@ -107,7 +107,8 @@ private:
QSet<QString> _domainOwnerFriends; // keep track of friends of the domain owner
QSet<QString> _inFlightGroupMembershipsRequests; // keep track of which we've already asked for
NodePermissions setPermissionsForUser(bool isLocalUser, QString verifiedUsername, const QHostAddress& senderAddress);
NodePermissions setPermissionsForUser(bool isLocalUser, QString verifiedUsername,
const QHostAddress& senderAddress, const QString& hardwareAddress);
void getGroupMemberships(const QString& username);
// void getIsGroupMember(const QString& username, const QUuid groupID);

View file

@ -53,6 +53,9 @@ public:
void setNodeVersion(const QString& nodeVersion) { _nodeVersion = nodeVersion; }
const QString& getNodeVersion() { return _nodeVersion; }
void setHardwareAddress(const QString& hardwareAddress) { _hardwareAddress = hardwareAddress; }
const QString& getHardwareAddress() { return _hardwareAddress; }
void addOverrideForKey(const QString& key, const QString& value, const QString& overrideValue);
void removeOverrideForKey(const QString& key, const QString& value);
@ -81,6 +84,7 @@ private:
bool _isAuthenticated = true;
NodeSet _nodeInterestSet;
QString _nodeVersion;
QString _hardwareAddress;
QString _placeName;

View file

@ -29,6 +29,8 @@
#include <NLPacketList.h>
#include <NumericalConstants.h>
#include "DomainServerNodeData.h"
#include "DomainServerSettingsManager.h"
const QString SETTINGS_DESCRIPTION_RELATIVE_PATH = "/resources/describe-settings.json";
@ -439,6 +441,9 @@ void DomainServerSettingsManager::packPermissions() {
// save settings for IP addresses
packPermissionsForMap("permissions", _ipPermissions, IP_PERMISSIONS_KEYPATH);
// save settings for MAC addresses
packPermissionsForMap("permissions", _macPermissions, MAC_PERMISSIONS_KEYPATH);
// save settings for groups
packPermissionsForMap("permissions", _groupPermissions, GROUP_PERMISSIONS_KEYPATH);
@ -506,6 +511,17 @@ void DomainServerSettingsManager::unpackPermissions() {
}
});
needPack |= unpackPermissionsForKeypath(MAC_PERMISSIONS_KEYPATH, &_macPermissions,
[&](NodePermissionsPointer perms){
// make sure that this permission row is for a non-empty hardware
if (perms->getKey().first.isEmpty()) {
_macPermissions.remove(perms->getKey());
// we removed a row from the MAC permissions, we'll need a re-pack
needPack = true;
}
});
needPack |= unpackPermissionsForKeypath(GROUP_PERMISSIONS_KEYPATH, &_groupPermissions,
[&](NodePermissionsPointer perms){
@ -558,7 +574,8 @@ void DomainServerSettingsManager::unpackPermissions() {
qDebug() << "--------------- permissions ---------------------";
QList<QHash<NodePermissionsKey, NodePermissionsPointer>> permissionsSets;
permissionsSets << _standardAgentPermissions.get() << _agentPermissions.get()
<< _groupPermissions.get() << _groupForbiddens.get() << _ipPermissions.get();
<< _groupPermissions.get() << _groupForbiddens.get()
<< _ipPermissions.get() << _macPermissions.get();
foreach (auto permissionSet, permissionsSets) {
QHashIterator<NodePermissionsKey, NodePermissionsPointer> i(permissionSet);
while (i.hasNext()) {
@ -653,19 +670,25 @@ void DomainServerSettingsManager::processNodeKickRequestPacket(QSharedPointer<Re
auto verifiedUsername = matchingNode->getPermissions().getVerifiedUserName();
bool hadExistingPermissions = false;
bool newPermissions = false;
if (!verifiedUsername.isEmpty()) {
// if we have a verified user name for this user, we apply the kick to the username
// check if there were already permissions
hadExistingPermissions = havePermissionsForName(verifiedUsername);
bool hadPermissions = havePermissionsForName(verifiedUsername);
// grab or create permissions for the given username
destinationPermissions = _agentPermissions[matchingNode->getPermissions().getKey()];
auto userPermissions = _agentPermissions[matchingNode->getPermissions().getKey()];
newPermissions = !hadPermissions || userPermissions->can(NodePermissions::Permission::canConnectToDomain);
// ensure that the connect permission is clear
userPermissions->clear(NodePermissions::Permission::canConnectToDomain);
} else {
// otherwise we apply the kick to the IP from active socket for this node
// (falling back to the public socket if not yet active)
// otherwise we apply the kick to the IP from active socket for this node and the MAC address
// remove connect permissions for the IP (falling back to the public socket if not yet active)
auto& kickAddress = matchingNode->getActiveSocket()
? matchingNode->getActiveSocket()->getAddress()
: matchingNode->getPublicSocket().getAddress();
@ -673,32 +696,41 @@ void DomainServerSettingsManager::processNodeKickRequestPacket(QSharedPointer<Re
NodePermissionsKey ipAddressKey(kickAddress.toString(), QUuid());
// check if there were already permissions for the IP
hadExistingPermissions = hasPermissionsForIP(kickAddress);
bool hadIPPermissions = hasPermissionsForIP(kickAddress);
// grab or create permissions for the given IP address
destinationPermissions = _ipPermissions[ipAddressKey];
auto ipPermissions = _ipPermissions[ipAddressKey];
if (!hadIPPermissions || ipPermissions->can(NodePermissions::Permission::canConnectToDomain)) {
newPermissions = true;
ipPermissions->clear(NodePermissions::Permission::canConnectToDomain);
}
// potentially remove connect permissions for the MAC address
DomainServerNodeData* nodeData = reinterpret_cast<DomainServerNodeData*>(matchingNode->getLinkedData());
if (nodeData) {
NodePermissionsKey macAddressKey(nodeData->getHardwareAddress(), 0);
bool hadMACPermissions = hasPermissionsForMAC(nodeData->getHardwareAddress());
auto macPermissions = _macPermissions[macAddressKey];
if (!hadMACPermissions || macPermissions->can(NodePermissions::Permission::canConnectToDomain)) {
newPermissions = true;
macPermissions->clear(NodePermissions::Permission::canConnectToDomain);
}
}
}
// make sure we didn't already have existing permissions that disallowed connect
if (!hadExistingPermissions
|| destinationPermissions->can(NodePermissions::Permission::canConnectToDomain)) {
if (newPermissions) {
qDebug() << "Removing connect permission for node" << uuidStringWithoutCurlyBraces(matchingNode->getUUID())
<< "after kick request";
// ensure that the connect permission is clear
destinationPermissions->clear(NodePermissions::Permission::canConnectToDomain);
<< "after kick request from" << uuidStringWithoutCurlyBraces(sendingNode->getUUID());
// we've changed permissions, time to store them to disk and emit our signal to say they have changed
packPermissions();
emit updateNodePermissions();
} else {
qWarning() << "Received kick request for node" << uuidStringWithoutCurlyBraces(matchingNode->getUUID())
<< "that already did not have permission to connect";
// in this case, though we don't expect the node to be connected to the domain, it is
// emit updateNodePermissions so that the DomainGatekeeper kicks it out
emit updateNodePermissions();
}
@ -753,6 +785,16 @@ NodePermissions DomainServerSettingsManager::getPermissionsForIP(const QHostAddr
return nullPermissions;
}
NodePermissions DomainServerSettingsManager::getPermissionsForMAC(const QString& macAddress) const {
NodePermissionsKey macKey = NodePermissionsKey(macAddress, 0);
if (_macPermissions.contains(macKey)) {
return *(_macPermissions[macKey].get());
}
NodePermissions nullPermissions;
nullPermissions.setAll(false);
return nullPermissions;
}
NodePermissions DomainServerSettingsManager::getPermissionsForGroup(const QString& groupName, QUuid rankID) const {
NodePermissionsKey groupRankKey = NodePermissionsKey(groupName, rankID);
if (_groupPermissions.contains(groupRankKey)) {

View file

@ -28,6 +28,7 @@ const QString SETTINGS_PATH_JSON = SETTINGS_PATH + ".json";
const QString AGENT_STANDARD_PERMISSIONS_KEYPATH = "security.standard_permissions";
const QString AGENT_PERMISSIONS_KEYPATH = "security.permissions";
const QString IP_PERMISSIONS_KEYPATH = "security.ip_permissions";
const QString MAC_PERMISSIONS_KEYPATH = "security.mac_permissions";
const QString GROUP_PERMISSIONS_KEYPATH = "security.group_permissions";
const QString GROUP_FORBIDDENS_KEYPATH = "security.group_forbiddens";
@ -62,6 +63,10 @@ public:
bool hasPermissionsForIP(const QHostAddress& address) const { return _ipPermissions.contains(address.toString(), 0); }
NodePermissions getPermissionsForIP(const QHostAddress& address) const;
// these give access to permissions for specific MACs from the domain-server settings page
bool hasPermissionsForMAC(const QString& macAddress) const { return _macPermissions.contains(macAddress, 0); }
NodePermissions getPermissionsForMAC(const QString& macAddress) const;
// these give access to permissions for specific groups from the domain-server settings page
bool havePermissionsForGroup(const QString& groupName, QUuid rankID) const {
return _groupPermissions.contains(groupName, rankID);
@ -142,6 +147,7 @@ private:
NodePermissionsMap _agentPermissions; // specific account-names
NodePermissionsMap _ipPermissions; // permissions granted by node IP address
NodePermissionsMap _macPermissions; // permissions granted by node MAC address
NodePermissionsMap _groupPermissions; // permissions granted by membership to specific groups
NodePermissionsMap _groupForbiddens; // permissions denied due to membership in a specific group

View file

@ -29,6 +29,9 @@ NodeConnectionData NodeConnectionData::fromDataStream(QDataStream& dataStream, c
// NOTE: QDataStream::readBytes() - The buffer is allocated using new []. Destroy it with the delete [] operator.
delete[] rawBytes;
// read the hardware address sent by the client
dataStream >> newHeader.hardwareAddress;
}
dataStream >> newHeader.nodeType

View file

@ -28,6 +28,7 @@ public:
HifiSockAddr senderSockAddr;
QList<NodeType_t> interestList;
QString placeName;
QString hardwareAddress;
QByteArray protocolVersion;
};

View file

@ -351,3 +351,7 @@ if (ANDROID)
qt_create_apk()
endif ()
add_dependency_external_projects(GifCreator)
find_package(GifCreator REQUIRED)
target_include_directories(${TARGET_NAME} PUBLIC ${GIFCREATOR_INCLUDE_DIRS})

View file

@ -152,6 +152,7 @@
#include "ui/LoginDialog.h"
#include "ui/overlays/Cube3DOverlay.h"
#include "ui/Snapshot.h"
#include "ui/SnapshotAnimated.h"
#include "ui/StandAloneJSConsole.h"
#include "ui/Stats.h"
#include "ui/UpdateDialog.h"
@ -5428,19 +5429,27 @@ void Application::toggleLogDialog() {
}
}
void Application::takeSnapshot(bool notify, float aspectRatio) {
postLambdaEvent([notify, aspectRatio, this] {
void Application::takeSnapshot(bool notify, bool includeAnimated, float aspectRatio) {
postLambdaEvent([notify, includeAnimated, aspectRatio, this] {
QMediaPlayer* player = new QMediaPlayer();
QFileInfo inf = QFileInfo(PathUtils::resourcesPath() + "sounds/snap.wav");
player->setMedia(QUrl::fromLocalFile(inf.absoluteFilePath()));
player->play();
// Get a screenshot and save it
QString path = Snapshot::saveSnapshot(getActiveDisplayPlugin()->getScreenshot(aspectRatio));
emit DependencyManager::get<WindowScriptingInterface>()->snapshotTaken(path, notify);
// If we're not doing an animated snapshot as well...
if (!includeAnimated || !(SnapshotAnimated::alsoTakeAnimatedSnapshot.get())) {
// Tell the dependency manager that the capture of the still snapshot has taken place.
emit DependencyManager::get<WindowScriptingInterface>()->snapshotTaken(path, "", notify);
} else {
// Get an animated GIF snapshot and save it
SnapshotAnimated::saveSnapshotAnimated(path, aspectRatio, qApp, DependencyManager::get<WindowScriptingInterface>());
}
});
}
void Application::shareSnapshot(const QString& path) {
postLambdaEvent([path] {
// not much to do here, everything is done in snapshot code...

View file

@ -266,7 +266,7 @@ public:
float getAvatarSimrate() const { return _avatarSimCounter.rate(); }
float getAverageSimsPerSecond() const { return _simCounter.rate(); }
void takeSnapshot(bool notify, float aspectRatio = 0.0f);
void takeSnapshot(bool notify, bool includeAnimated = false, float aspectRatio = 0.0f);
void shareSnapshot(const QString& filename);
model::SkyboxPointer getDefaultSkybox() const { return _defaultSkybox; }

View file

@ -199,8 +199,8 @@ void WindowScriptingInterface::copyToClipboard(const QString& text) {
QApplication::clipboard()->setText(text);
}
void WindowScriptingInterface::takeSnapshot(bool notify, float aspectRatio) {
qApp->takeSnapshot(notify, aspectRatio);
void WindowScriptingInterface::takeSnapshot(bool notify, bool includeAnimated, float aspectRatio) {
qApp->takeSnapshot(notify, includeAnimated, aspectRatio);
}
void WindowScriptingInterface::shareSnapshot(const QString& path) {

View file

@ -52,7 +52,7 @@ public slots:
QScriptValue save(const QString& title = "", const QString& directory = "", const QString& nameFilter = "");
void showAssetServer(const QString& upload = "");
void copyToClipboard(const QString& text);
void takeSnapshot(bool notify = true, float aspectRatio = 0.0f);
void takeSnapshot(bool notify = true, bool includeAnimated = false, float aspectRatio = 0.0f);
void shareSnapshot(const QString& path);
bool isPhysicsEnabled();
@ -60,7 +60,7 @@ signals:
void domainChanged(const QString& domainHostname);
void svoImportRequested(const QString& url);
void domainConnectionRefused(const QString& reasonMessage, int reasonCode, const QString& extraInfo);
void snapshotTaken(const QString& path, bool notify);
void snapshotTaken(const QString& pathStillSnapshot, const QString& pathAnimatedSnapshot, bool notify);
void snapshotShared(const QString& error);
private:

View file

@ -23,6 +23,7 @@
#include "LODManager.h"
#include "Menu.h"
#include "Snapshot.h"
#include "SnapshotAnimated.h"
#include "UserActivityLogger.h"
#include "AmbientOcclusionEffect.h"
@ -31,7 +32,7 @@
void setupPreferences() {
auto preferences = DependencyManager::get<Preferences>();
auto nodeList = DependencyManager::get<NodeList>();
auto myAvatar = DependencyManager::get<AvatarManager>()->getMyAvatar();
static const QString AVATAR_BASICS { "Avatar Basics" };
{
@ -67,6 +68,18 @@ void setupPreferences() {
auto setter = [=](bool value) { myAvatar->setClearOverlayWhenMoving(value); };
preferences->addPreference(new CheckPreference(AVATAR_BASICS, "Clear overlays when moving", getter, setter));
}
{
auto getter = [=]()->float { return nodeList->getIgnoreRadius(); };
auto setter = [=](float value) {
nodeList->ignoreNodesInRadius(value, nodeList->getIgnoreRadiusEnabled());
};
auto preference = new SpinnerPreference(AVATAR_BASICS, "Personal space bubble radius (default is 1m)", getter, setter);
preference->setMin(0.01f);
preference->setMax(99.9f);
preference->setDecimals(2);
preference->setStep(0.25);
preferences->addPreference(preference);
}
// UI
{
@ -83,6 +96,20 @@ void setupPreferences() {
auto preference = new BrowsePreference(SNAPSHOTS, "Put my snapshots here", getter, setter);
preferences->addPreference(preference);
}
{
auto getter = []()->bool { return SnapshotAnimated::alsoTakeAnimatedSnapshot.get(); };
auto setter = [](bool value) { SnapshotAnimated::alsoTakeAnimatedSnapshot.set(value); };
preferences->addPreference(new CheckPreference(SNAPSHOTS, "Take Animated GIF Snapshot with HUD Button", getter, setter));
}
{
auto getter = []()->float { return SnapshotAnimated::snapshotAnimatedDuration.get(); };
auto setter = [](float value) { SnapshotAnimated::snapshotAnimatedDuration.set(value); };
auto preference = new SpinnerPreference(SNAPSHOTS, "Animated Snapshot Duration", getter, setter);
preference->setMin(3);
preference->setMax(10);
preference->setStep(1);
preferences->addPreference(preference);
}
// Scripts
{

View file

@ -51,16 +51,24 @@ SnapshotMetaData* Snapshot::parseSnapshotData(QString snapshotPath) {
return NULL;
}
QImage shot(snapshotPath);
QUrl url;
// no location data stored
if (shot.text(URL).isEmpty()) {
if (snapshotPath.right(3) == "jpg") {
QImage shot(snapshotPath);
// no location data stored
if (shot.text(URL).isEmpty()) {
return NULL;
}
// parsing URL
url = QUrl(shot.text(URL), QUrl::ParsingMode::StrictMode);
} else if (snapshotPath.right(3) == "gif") {
url = QUrl(DependencyManager::get<AddressManager>()->currentShareableAddress());
} else {
return NULL;
}
// parsing URL
QUrl url = QUrl(shot.text(URL), QUrl::ParsingMode::StrictMode);
SnapshotMetaData* data = new SnapshotMetaData();
data->setURL(url);
@ -156,7 +164,11 @@ void Snapshot::uploadSnapshot(const QString& filename) {
file->open(QIODevice::ReadOnly);
QHttpPart imagePart;
imagePart.setHeader(QNetworkRequest::ContentTypeHeader, QVariant("image/jpeg"));
if (filename.right(3) == "gif") {
imagePart.setHeader(QNetworkRequest::ContentTypeHeader, QVariant("image/gif"));
} else {
imagePart.setHeader(QNetworkRequest::ContentTypeHeader, QVariant("image/jpeg"));
}
imagePart.setHeader(QNetworkRequest::ContentDispositionHeader,
QVariant("form-data; name=\"image\"; filename=\"" + file->fileName() + "\""));
imagePart.setBodyDevice(file);

View file

@ -0,0 +1,139 @@
//
// SnapshotAnimated.cpp
// interface/src/ui
//
// Created by Zach Fox on 11/14/16.
// 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
//
#include <QtCore/QDateTime>
#include <QtCore/QObject>
#include <QtCore/QString>
#include <QtGui/QImage>
#include <QtConcurrent/qtconcurrentrun.h>
#include "SnapshotAnimated.h"
QTimer* SnapshotAnimated::snapshotAnimatedTimer = NULL;
qint64 SnapshotAnimated::snapshotAnimatedTimestamp = 0;
qint64 SnapshotAnimated::snapshotAnimatedFirstFrameTimestamp = 0;
bool SnapshotAnimated::snapshotAnimatedTimerRunning = false;
QString SnapshotAnimated::snapshotAnimatedPath;
QString SnapshotAnimated::snapshotStillPath;
QVector<QImage> SnapshotAnimated::snapshotAnimatedFrameVector;
QVector<qint64> SnapshotAnimated::snapshotAnimatedFrameDelayVector;
Application* SnapshotAnimated::app;
float SnapshotAnimated::aspectRatio;
QSharedPointer<WindowScriptingInterface> SnapshotAnimated::snapshotAnimatedDM;
GifWriter SnapshotAnimated::snapshotAnimatedGifWriter;
Setting::Handle<bool> SnapshotAnimated::alsoTakeAnimatedSnapshot("alsoTakeAnimatedSnapshot", true);
Setting::Handle<float> SnapshotAnimated::snapshotAnimatedDuration("snapshotAnimatedDuration", SNAPSNOT_ANIMATED_DURATION_SECS);
void SnapshotAnimated::saveSnapshotAnimated(QString pathStill, float aspectRatio, Application* app, QSharedPointer<WindowScriptingInterface> dm) {
// If we're not in the middle of capturing an animated snapshot...
if (SnapshotAnimated::snapshotAnimatedFirstFrameTimestamp == 0) {
SnapshotAnimated::snapshotAnimatedTimer = new QTimer();
SnapshotAnimated::aspectRatio = aspectRatio;
SnapshotAnimated::app = app;
SnapshotAnimated::snapshotAnimatedDM = dm;
// Define the output location of the still and animated snapshots.
SnapshotAnimated::snapshotStillPath = pathStill;
SnapshotAnimated::snapshotAnimatedPath = pathStill;
SnapshotAnimated::snapshotAnimatedPath.replace("jpg", "gif");
// Ensure the snapshot timer is Precise (attempted millisecond precision)
SnapshotAnimated::snapshotAnimatedTimer->setTimerType(Qt::PreciseTimer);
// Connect the snapshotAnimatedTimer QTimer to the lambda slot function
QObject::connect((SnapshotAnimated::snapshotAnimatedTimer), &QTimer::timeout, captureFrames);
// Start the snapshotAnimatedTimer QTimer - argument for this is in milliseconds
SnapshotAnimated::snapshotAnimatedTimerRunning = true;
SnapshotAnimated::snapshotAnimatedTimer->start(SNAPSNOT_ANIMATED_FRAME_DELAY_MSEC);
// If we're already in the middle of capturing an animated snapshot...
} else {
// Just tell the dependency manager that the capture of the still snapshot has taken place.
emit dm->snapshotTaken(pathStill, "", false);
}
}
void SnapshotAnimated::captureFrames() {
if (SnapshotAnimated::snapshotAnimatedTimerRunning) {
// Get a screenshot from the display, then scale the screenshot down,
// then convert it to the image format the GIF library needs,
// then save all that to the QImage named "frame"
QImage frame(SnapshotAnimated::app->getActiveDisplayPlugin()->getScreenshot(SnapshotAnimated::aspectRatio));
frame = frame.scaledToWidth(SNAPSNOT_ANIMATED_WIDTH);
SnapshotAnimated::snapshotAnimatedFrameVector.append(frame);
// If that was the first frame...
if (SnapshotAnimated::snapshotAnimatedFirstFrameTimestamp == 0) {
// Record the current frame timestamp
SnapshotAnimated::snapshotAnimatedTimestamp = QDateTime::currentMSecsSinceEpoch();
// Record the first frame timestamp
SnapshotAnimated::snapshotAnimatedFirstFrameTimestamp = SnapshotAnimated::snapshotAnimatedTimestamp;
SnapshotAnimated::snapshotAnimatedFrameDelayVector.append(SNAPSNOT_ANIMATED_FRAME_DELAY_MSEC / 10);
// If this is an intermediate or the final frame...
} else {
// Push the current frame delay onto the vector
SnapshotAnimated::snapshotAnimatedFrameDelayVector.append(round(((float)(QDateTime::currentMSecsSinceEpoch() - SnapshotAnimated::snapshotAnimatedTimestamp)) / 10));
// Record the current frame timestamp
SnapshotAnimated::snapshotAnimatedTimestamp = QDateTime::currentMSecsSinceEpoch();
// If that was the last frame...
if ((SnapshotAnimated::snapshotAnimatedTimestamp - SnapshotAnimated::snapshotAnimatedFirstFrameTimestamp) >= (SnapshotAnimated::snapshotAnimatedDuration.get() * MSECS_PER_SECOND)) {
SnapshotAnimated::snapshotAnimatedTimerRunning = false;
// Reset the current frame timestamp
SnapshotAnimated::snapshotAnimatedTimestamp = 0;
SnapshotAnimated::snapshotAnimatedFirstFrameTimestamp = 0;
// Kick off the thread that'll pack the frames into the GIF
QtConcurrent::run(processFrames);
// Stop the snapshot QTimer. This action by itself DOES NOT GUARANTEE
// that the slot will not be called again in the future.
// See: http://lists.qt-project.org/pipermail/qt-interest-old/2009-October/013926.html
SnapshotAnimated::snapshotAnimatedTimer->stop();
delete SnapshotAnimated::snapshotAnimatedTimer;
}
}
}
}
void SnapshotAnimated::processFrames() {
uint32_t width = SnapshotAnimated::snapshotAnimatedFrameVector[0].width();
uint32_t height = SnapshotAnimated::snapshotAnimatedFrameVector[0].height();
// Create the GIF from the temporary files
// Write out the header and beginning of the GIF file
GifBegin(
&(SnapshotAnimated::snapshotAnimatedGifWriter),
qPrintable(SnapshotAnimated::snapshotAnimatedPath),
width,
height,
1); // "1" means "yes there is a delay" with this GifCreator library.
for (int itr = 0; itr < SnapshotAnimated::snapshotAnimatedFrameVector.size(); itr++) {
// Write each frame to the GIF
GifWriteFrame(&(SnapshotAnimated::snapshotAnimatedGifWriter),
(uint8_t*)SnapshotAnimated::snapshotAnimatedFrameVector[itr].convertToFormat(QImage::Format_RGBA8888).bits(),
width,
height,
SnapshotAnimated::snapshotAnimatedFrameDelayVector[itr]);
}
// Write out the end of the GIF
GifEnd(&(SnapshotAnimated::snapshotAnimatedGifWriter));
// Clear out the frame and frame delay vectors.
// Also release the memory not required to store the items.
SnapshotAnimated::snapshotAnimatedFrameVector.clear();
SnapshotAnimated::snapshotAnimatedFrameVector.squeeze();
SnapshotAnimated::snapshotAnimatedFrameDelayVector.clear();
SnapshotAnimated::snapshotAnimatedFrameDelayVector.squeeze();
// Let the dependency manager know that the snapshots have been taken.
emit SnapshotAnimated::snapshotAnimatedDM->snapshotTaken(SnapshotAnimated::snapshotStillPath, SnapshotAnimated::snapshotAnimatedPath, false);
}

View file

@ -0,0 +1,58 @@
//
// SnapshotAnimated.h
// interface/src/ui
//
// Created by Zach Fox on 11/14/16.
// 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
//
#ifndef hifi_SnapshotAnimated_h
#define hifi_SnapshotAnimated_h
#include <QtCore/QVector>
#include <Application.h>
#include <DependencyManager.h>
#include <GifCreator.h>
#include <qtimer.h>
#include <SettingHandle.h>
#include "scripting/WindowScriptingInterface.h"
// If the snapshot width or the framerate are too high for the
// application to handle, the framerate of the output GIF will drop.
#define SNAPSNOT_ANIMATED_WIDTH (480)
// This value should divide evenly into 100. Snapshot framerate is NOT guaranteed.
#define SNAPSNOT_ANIMATED_TARGET_FRAMERATE (25)
#define SNAPSNOT_ANIMATED_DURATION_SECS (3)
#define SNAPSNOT_ANIMATED_DURATION_MSEC (SNAPSNOT_ANIMATED_DURATION_SECS*1000)
#define SNAPSNOT_ANIMATED_FRAME_DELAY_MSEC (1000/SNAPSNOT_ANIMATED_TARGET_FRAMERATE)
class SnapshotAnimated {
private:
static QTimer* snapshotAnimatedTimer;
static qint64 snapshotAnimatedTimestamp;
static qint64 snapshotAnimatedFirstFrameTimestamp;
static bool snapshotAnimatedTimerRunning;
static QString snapshotStillPath;
static QString snapshotAnimatedPath;
static QVector<QImage> snapshotAnimatedFrameVector;
static QVector<qint64> snapshotAnimatedFrameDelayVector;
static QSharedPointer<WindowScriptingInterface> snapshotAnimatedDM;
static Application* app;
static float aspectRatio;
static GifWriter snapshotAnimatedGifWriter;
static void captureFrames();
static void processFrames();
public:
static void saveSnapshotAnimated(QString pathStill, float aspectRatio, Application* app, QSharedPointer<WindowScriptingInterface> dm);
static Setting::Handle<bool> alsoTakeAnimatedSnapshot;
static Setting::Handle<float> snapshotAnimatedDuration;
};
#endif // hifi_SnapshotAnimated_h

View file

@ -61,6 +61,10 @@ static const auto DEFAULT_ORIENTATION_GETTER = [] { return Quaternions::IDENTITY
static const int DEFAULT_BUFFER_FRAMES = 1;
// OUTPUT_CHANNEL_COUNT is audio pipeline output format, which is always 2 channel.
// _outputFormat.channelCount() is device output format, which may be 1 or multichannel.
static const int OUTPUT_CHANNEL_COUNT = 2;
static const bool DEFAULT_STARVE_DETECTION_ENABLED = true;
static const int STARVE_DETECTION_THRESHOLD = 3;
static const int STARVE_DETECTION_PERIOD = 10 * 1000; // 10 Seconds
@ -140,7 +144,7 @@ AudioClient::AudioClient() :
_reverbOptions(&_scriptReverbOptions),
_inputToNetworkResampler(NULL),
_networkToOutputResampler(NULL),
_audioLimiter(AudioConstants::SAMPLE_RATE, AudioConstants::STEREO),
_audioLimiter(AudioConstants::SAMPLE_RATE, OUTPUT_CHANNEL_COUNT),
_outgoingAvatarAudioSequenceNumber(0),
_audioOutputIODevice(_receivedAudioStream, this),
_stats(&_receivedAudioStream),
@ -237,14 +241,6 @@ QAudioDeviceInfo getNamedAudioDeviceForMode(QAudio::Mode mode, const QString& de
return result;
}
int numDestinationSamplesRequired(const QAudioFormat& sourceFormat, const QAudioFormat& destinationFormat,
int numSourceSamples) {
float ratio = (float) destinationFormat.channelCount() / sourceFormat.channelCount();
ratio *= (float) destinationFormat.sampleRate() / sourceFormat.sampleRate();
return (numSourceSamples * ratio) + 0.5f;
}
#ifdef Q_OS_WIN
QString friendlyNameForAudioDevice(IMMDevice* pEndpoint) {
QString deviceName;
@ -387,14 +383,36 @@ bool adjustedFormatForAudioDevice(const QAudioDeviceInfo& audioDevice,
adjustedAudioFormat = desiredAudioFormat;
#ifdef Q_OS_ANDROID
#if defined(Q_OS_WIN)
// On Windows, using WASAPI shared mode, the sample rate and channel count must
// exactly match the internal mix format. Any other format will fail to open.
adjustedAudioFormat = audioDevice.preferredFormat(); // returns mixFormat
adjustedAudioFormat.setCodec("audio/pcm");
adjustedAudioFormat.setSampleSize(16);
adjustedAudioFormat.setSampleType(QAudioFormat::SignedInt);
adjustedAudioFormat.setByteOrder(QAudioFormat::LittleEndian);
if (!audioDevice.isFormatSupported(adjustedAudioFormat)) {
qCDebug(audioclient) << "WARNING: The mix format is" << adjustedAudioFormat << "but isFormatSupported() failed.";
return false;
}
// converting to/from this rate must produce an integral number of samples
if (adjustedAudioFormat.sampleRate() * AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL % AudioConstants::SAMPLE_RATE != 0) {
qCDebug(audioclient) << "WARNING: The current sample rate [" << adjustedAudioFormat.sampleRate() << "] is not supported.";
return false;
}
return true;
#elif defined(Q_OS_ANDROID)
// FIXME: query the native sample rate of the device?
adjustedAudioFormat.setSampleRate(48000);
#else
//
// Attempt the device sample rate in decreasing order of preference.
// On Windows, using WASAPI shared mode, only a match with the hardware sample rate will succeed.
//
if (audioDevice.supportedSampleRates().contains(48000)) {
adjustedAudioFormat.setSampleRate(48000);
@ -427,15 +445,15 @@ bool adjustedFormatForAudioDevice(const QAudioDeviceInfo& audioDevice,
}
bool sampleChannelConversion(const int16_t* sourceSamples, int16_t* destinationSamples, unsigned int numSourceSamples,
const QAudioFormat& sourceAudioFormat, const QAudioFormat& destinationAudioFormat) {
if (sourceAudioFormat.channelCount() == 2 && destinationAudioFormat.channelCount() == 1) {
const int sourceChannelCount, const int destinationChannelCount) {
if (sourceChannelCount == 2 && destinationChannelCount == 1) {
// loop through the stereo input audio samples and average every two samples
for (uint i = 0; i < numSourceSamples; i += 2) {
destinationSamples[i / 2] = (sourceSamples[i] / 2) + (sourceSamples[i + 1] / 2);
}
return true;
} else if (sourceAudioFormat.channelCount() == 1 && destinationAudioFormat.channelCount() == 2) {
} else if (sourceChannelCount == 1 && destinationChannelCount == 2) {
// loop through the mono input audio and repeat each sample twice
for (uint i = 0; i < numSourceSamples; ++i) {
@ -451,26 +469,24 @@ bool sampleChannelConversion(const int16_t* sourceSamples, int16_t* destinationS
void possibleResampling(AudioSRC* resampler,
const int16_t* sourceSamples, int16_t* destinationSamples,
unsigned int numSourceSamples, unsigned int numDestinationSamples,
const QAudioFormat& sourceAudioFormat, const QAudioFormat& destinationAudioFormat) {
const int sourceChannelCount, const int destinationChannelCount) {
if (numSourceSamples > 0) {
if (!resampler) {
if (!sampleChannelConversion(sourceSamples, destinationSamples, numSourceSamples,
sourceAudioFormat, destinationAudioFormat)) {
sourceChannelCount, destinationChannelCount)) {
// no conversion, we can copy the samples directly across
memcpy(destinationSamples, sourceSamples, numSourceSamples * AudioConstants::SAMPLE_SIZE);
}
} else {
if (sourceAudioFormat.channelCount() != destinationAudioFormat.channelCount()) {
float channelCountRatio = (float)destinationAudioFormat.channelCount() / sourceAudioFormat.channelCount();
if (sourceChannelCount != destinationChannelCount) {
int numChannelCoversionSamples = (int)(numSourceSamples * channelCountRatio);
int numChannelCoversionSamples = (numSourceSamples * destinationChannelCount) / sourceChannelCount;
int16_t* channelConversionSamples = new int16_t[numChannelCoversionSamples];
sampleChannelConversion(sourceSamples, channelConversionSamples,
numSourceSamples,
sourceAudioFormat, destinationAudioFormat);
sampleChannelConversion(sourceSamples, channelConversionSamples, numSourceSamples,
sourceChannelCount, destinationChannelCount);
resampler->render(channelConversionSamples, destinationSamples, numChannelCoversionSamples);
@ -480,7 +496,7 @@ void possibleResampling(AudioSRC* resampler,
unsigned int numAdjustedSourceSamples = numSourceSamples;
unsigned int numAdjustedDestinationSamples = numDestinationSamples;
if (sourceAudioFormat.channelCount() == 2 && destinationAudioFormat.channelCount() == 2) {
if (sourceChannelCount == 2 && destinationChannelCount == 2) {
numAdjustedSourceSamples /= 2;
numAdjustedDestinationSamples /= 2;
}
@ -502,7 +518,7 @@ void AudioClient::start() {
_desiredInputFormat.setChannelCount(1);
_desiredOutputFormat = _desiredInputFormat;
_desiredOutputFormat.setChannelCount(2);
_desiredOutputFormat.setChannelCount(OUTPUT_CHANNEL_COUNT);
QAudioDeviceInfo inputDeviceInfo = defaultAudioDeviceForMode(QAudio::AudioInput);
qCDebug(audioclient) << "The default audio input device is" << inputDeviceInfo.deviceName();
@ -824,6 +840,36 @@ void AudioClient::setReverbOptions(const AudioEffectOptions* options) {
}
}
static void channelUpmix(int16_t* source, int16_t* dest, int numSamples, int numExtraChannels) {
for (int i = 0; i < numSamples/2; i++) {
// read 2 samples
int16_t left = *source++;
int16_t right = *source++;
// write 2 + N samples
*dest++ = left;
*dest++ = right;
for (int n = 0; n < numExtraChannels; n++) {
*dest++ = 0;
}
}
}
static void channelDownmix(int16_t* source, int16_t* dest, int numSamples) {
for (int i = 0; i < numSamples/2; i++) {
// read 2 samples
int16_t left = *source++;
int16_t right = *source++;
// write 1 sample
*dest++ = (int16_t)((left + right) / 2);
}
}
void AudioClient::handleLocalEchoAndReverb(QByteArray& inputByteArray) {
// If there is server echo, reverb will be applied to the recieved audio stream so no need to have it here.
bool hasReverb = _reverb || _receivedAudioStream.hasReverb();
@ -857,7 +903,7 @@ void AudioClient::handleLocalEchoAndReverb(QByteArray& inputByteArray) {
static QByteArray loopBackByteArray;
int numInputSamples = inputByteArray.size() / AudioConstants::SAMPLE_SIZE;
int numLoopbackSamples = numDestinationSamplesRequired(_inputFormat, _outputFormat, numInputSamples);
int numLoopbackSamples = (numInputSamples * OUTPUT_CHANNEL_COUNT) / _inputFormat.channelCount();
loopBackByteArray.resize(numLoopbackSamples * AudioConstants::SAMPLE_SIZE);
@ -865,7 +911,7 @@ void AudioClient::handleLocalEchoAndReverb(QByteArray& inputByteArray) {
int16_t* loopbackSamples = reinterpret_cast<int16_t*>(loopBackByteArray.data());
// upmix mono to stereo
if (!sampleChannelConversion(inputSamples, loopbackSamples, numInputSamples, _inputFormat, _outputFormat)) {
if (!sampleChannelConversion(inputSamples, loopbackSamples, numInputSamples, _inputFormat.channelCount(), OUTPUT_CHANNEL_COUNT)) {
// no conversion, just copy the samples
memcpy(loopbackSamples, inputSamples, numInputSamples * AudioConstants::SAMPLE_SIZE);
}
@ -876,7 +922,29 @@ void AudioClient::handleLocalEchoAndReverb(QByteArray& inputByteArray) {
_sourceReverb.render(loopbackSamples, loopbackSamples, numLoopbackSamples/2);
}
_loopbackOutputDevice->write(loopBackByteArray);
// if required, upmix or downmix to deviceChannelCount
int deviceChannelCount = _outputFormat.channelCount();
if (deviceChannelCount == OUTPUT_CHANNEL_COUNT) {
_loopbackOutputDevice->write(loopBackByteArray);
} else {
static QByteArray deviceByteArray;
int numDeviceSamples = (numLoopbackSamples * deviceChannelCount) / OUTPUT_CHANNEL_COUNT;
deviceByteArray.resize(numDeviceSamples * AudioConstants::SAMPLE_SIZE);
int16_t* deviceSamples = reinterpret_cast<int16_t*>(deviceByteArray.data());
if (deviceChannelCount > OUTPUT_CHANNEL_COUNT) {
channelUpmix(loopbackSamples, deviceSamples, numLoopbackSamples, deviceChannelCount - OUTPUT_CHANNEL_COUNT);
} else {
channelDownmix(loopbackSamples, deviceSamples, numLoopbackSamples);
}
_loopbackOutputDevice->write(deviceByteArray);
}
}
void AudioClient::handleAudioInput() {
@ -923,7 +991,7 @@ void AudioClient::handleAudioInput() {
possibleResampling(_inputToNetworkResampler,
inputAudioSamples.get(), networkAudioSamples,
inputSamplesRequired, numNetworkSamples,
_inputFormat, _desiredInputFormat);
_inputFormat.channelCount(), _desiredInputFormat.channelCount());
// Remove DC offset
if (!_isStereoInput) {
@ -1170,9 +1238,9 @@ bool AudioClient::outputLocalInjector(bool isStereo, AudioInjector* injector) {
}
void AudioClient::outputFormatChanged() {
_outputFrameSize = (AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL * _outputFormat.channelCount() * _outputFormat.sampleRate()) /
_outputFrameSize = (AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL * OUTPUT_CHANNEL_COUNT * _outputFormat.sampleRate()) /
_desiredOutputFormat.sampleRate();
_receivedAudioStream.outputFormatChanged(_outputFormat.sampleRate(), _outputFormat.channelCount());
_receivedAudioStream.outputFormatChanged(_outputFormat.sampleRate(), OUTPUT_CHANNEL_COUNT);
}
bool AudioClient::switchInputToAudioDevice(const QAudioDeviceInfo& inputDeviceInfo) {
@ -1316,9 +1384,8 @@ bool AudioClient::switchOutputToAudioDevice(const QAudioDeviceInfo& outputDevice
assert(_desiredOutputFormat.sampleSize() == 16);
assert(_outputFormat.sampleSize() == 16);
int channelCount = (_desiredOutputFormat.channelCount() == 2 && _outputFormat.channelCount() == 2) ? 2 : 1;
_networkToOutputResampler = new AudioSRC(_desiredOutputFormat.sampleRate(), _outputFormat.sampleRate(), channelCount);
_networkToOutputResampler = new AudioSRC(_desiredOutputFormat.sampleRate(), _outputFormat.sampleRate(), OUTPUT_CHANNEL_COUNT);
} else {
qCDebug(audioclient) << "No resampling required for network output to match actual output format.";
@ -1328,8 +1395,11 @@ bool AudioClient::switchOutputToAudioDevice(const QAudioDeviceInfo& outputDevice
// setup our general output device for audio-mixer audio
_audioOutput = new QAudioOutput(outputDeviceInfo, _outputFormat, this);
int osDefaultBufferSize = _audioOutput->bufferSize();
int requestedSize = _sessionOutputBufferSizeFrames *_outputFrameSize * AudioConstants::SAMPLE_SIZE;
int deviceChannelCount = _outputFormat.channelCount();
int deviceFrameSize = (AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL * deviceChannelCount * _outputFormat.sampleRate()) / _desiredOutputFormat.sampleRate();
int requestedSize = _sessionOutputBufferSizeFrames * deviceFrameSize * AudioConstants::SAMPLE_SIZE;
_audioOutput->setBufferSize(requestedSize);
connect(_audioOutput, &QAudioOutput::notify, this, &AudioClient::outputNotify);
@ -1341,14 +1411,13 @@ bool AudioClient::switchOutputToAudioDevice(const QAudioDeviceInfo& outputDevice
_audioOutput->start(&_audioOutputIODevice);
lock.unlock();
qCDebug(audioclient) << "Output Buffer capacity in frames: " << _audioOutput->bufferSize() / AudioConstants::SAMPLE_SIZE / (float)_outputFrameSize <<
qCDebug(audioclient) << "Output Buffer capacity in frames: " << _audioOutput->bufferSize() / AudioConstants::SAMPLE_SIZE / (float)deviceFrameSize <<
"requested bytes:" << requestedSize << "actual bytes:" << _audioOutput->bufferSize() <<
"os default:" << osDefaultBufferSize << "period size:" << _audioOutput->periodSize();
// setup a loopback audio output device
_loopbackAudioOutput = new QAudioOutput(outputDeviceInfo, _outputFormat, this);
_timeSinceLastReceived.start();
supportedFormat = true;
@ -1447,15 +1516,27 @@ float AudioClient::gainForSource(float distance, float volume) {
}
qint64 AudioClient::AudioOutputIODevice::readData(char * data, qint64 maxSize) {
auto samplesRequested = maxSize / AudioConstants::SAMPLE_SIZE;
// samples requested from OUTPUT_CHANNEL_COUNT
int deviceChannelCount = _audio->_outputFormat.channelCount();
int samplesRequested = (int)(maxSize / AudioConstants::SAMPLE_SIZE) * OUTPUT_CHANNEL_COUNT / deviceChannelCount;
int samplesPopped;
int bytesWritten;
if ((samplesPopped = _receivedAudioStream.popSamples((int)samplesRequested, false)) > 0) {
if ((samplesPopped = _receivedAudioStream.popSamples(samplesRequested, false)) > 0) {
qCDebug(audiostream, "Read %d samples from buffer (%d available)", samplesPopped, _receivedAudioStream.getSamplesAvailable());
AudioRingBuffer::ConstIterator lastPopOutput = _receivedAudioStream.getLastPopOutput();
lastPopOutput.readSamples((int16_t*)data, samplesPopped);
bytesWritten = samplesPopped * AudioConstants::SAMPLE_SIZE;
// if required, upmix or downmix to deviceChannelCount
if (deviceChannelCount == OUTPUT_CHANNEL_COUNT) {
lastPopOutput.readSamples((int16_t*)data, samplesPopped);
} else if (deviceChannelCount > OUTPUT_CHANNEL_COUNT) {
lastPopOutput.readSamplesWithUpmix((int16_t*)data, samplesPopped, deviceChannelCount - OUTPUT_CHANNEL_COUNT);
} else {
lastPopOutput.readSamplesWithDownmix((int16_t*)data, samplesPopped);
}
bytesWritten = (samplesPopped * AudioConstants::SAMPLE_SIZE) * deviceChannelCount / OUTPUT_CHANNEL_COUNT;
} else {
// nothing on network, don't grab anything from injectors, and just return 0s
// this will flood the log: qCDebug(audioclient, "empty/partial network buffer");

View file

@ -105,6 +105,8 @@ public:
void readSamples(int16_t* dest, int numSamples);
void readSamplesWithFade(int16_t* dest, int numSamples, float fade);
void readSamplesWithUpmix(int16_t* dest, int numSamples, int numExtraChannels);
void readSamplesWithDownmix(int16_t* dest, int numSamples);
private:
int16_t* atShiftedBy(int i);
@ -225,6 +227,40 @@ inline void AudioRingBuffer::ConstIterator::readSamplesWithFade(int16_t* dest, i
}
}
inline void AudioRingBuffer::ConstIterator::readSamplesWithUpmix(int16_t* dest, int numSamples, int numExtraChannels) {
int16_t* at = _at;
for (int i = 0; i < numSamples/2; i++) {
// read 2 samples
int16_t left = *at;
at = (at == _bufferLast) ? _bufferFirst : at + 1;
int16_t right = *at;
at = (at == _bufferLast) ? _bufferFirst : at + 1;
// write 2 + N samples
*dest++ = left;
*dest++ = right;
for (int n = 0; n < numExtraChannels; n++) {
*dest++ = 0;
}
}
}
inline void AudioRingBuffer::ConstIterator::readSamplesWithDownmix(int16_t* dest, int numSamples) {
int16_t* at = _at;
for (int i = 0; i < numSamples/2; i++) {
// read 2 samples
int16_t left = *at;
at = (at == _bufferLast) ? _bufferFirst : at + 1;
int16_t right = *at;
at = (at == _bufferLast) ? _bufferFirst : at + 1;
// write 1 sample
*dest++ = (int16_t)((left + right) / 2);
}
}
inline AudioRingBuffer::ConstIterator AudioRingBuffer::nextOutput() const {
return ConstIterator(_buffer, _bufferLength, _nextOutput);
}

View file

@ -39,11 +39,11 @@ bool DebugHmdDisplayPlugin::beginFrameRender(uint32_t frameIndex) {
_uiModelTransform = DependencyManager::get<CompositorHelper>()->getModelTransform();
_frameInfos[frameIndex] = _currentRenderFrameInfo;
_handPoses[0] = glm::translate(mat4(), vec3(-0.3f, 0.0f, 0.0f));
_handPoses[0] = glm::translate(mat4(), vec3(0.3f * cosf(secTimestampNow() * 3.0f), -0.3f * sinf(secTimestampNow() * 5.0f), 0.0f));
_handLasers[0].color = vec4(1, 0, 0, 1);
_handLasers[0].mode = HandLaserMode::Overlay;
_handPoses[1] = glm::translate(mat4(), vec3(0.3f, 0.0f, 0.0f));
_handPoses[1] = glm::translate(mat4(), vec3(0.3f * sinf(secTimestampNow() * 3.0f), -0.3f * cosf(secTimestampNow() * 5.0f), 0.0f));
_handLasers[1].color = vec4(0, 1, 1, 1);
_handLasers[1].mode = HandLaserMode::Overlay;
});

View file

@ -23,7 +23,6 @@
#include <CursorManager.h>
#include <gl/GLWidget.h>
#include <shared/NsightHelpers.h>
#include <GeometryCache.h>
#include <gpu/Context.h>
#include <gpu/StandardShaderLib.h>
#include <gpu/gl/GLBackend.h>
@ -32,6 +31,9 @@
#include "../Logging.h"
#include "../CompositorHelper.h"
#include <../render-utils/shaders/render-utils/glowLine_vert.h>
#include <../render-utils/shaders/render-utils/glowLine_frag.h>
static const QString MONO_PREVIEW = "Mono Preview";
static const QString DISABLE_PREVIEW = "Disable Preview";
@ -47,6 +49,12 @@ static const size_t NUMBER_OF_HANDS = 2;
//#define LIVE_SHADER_RELOAD 1
extern glm::vec3 getPoint(float yaw, float pitch);
struct HandLaserData {
vec4 p1;
vec4 p2;
vec4 color;
};
static QString readFile(const QString& filename) {
QFile file(filename);
file.open(QFile::Text | QFile::ReadOnly);
@ -112,11 +120,28 @@ void HmdDisplayPlugin::internalDeactivate() {
void HmdDisplayPlugin::customizeContext() {
Parent::customizeContext();
_overlayRenderer.build();
auto geometryCache = DependencyManager::get<GeometryCache>();
for (size_t i = 0; i < _geometryIds.size(); ++i) {
_geometryIds[i] = geometryCache->allocateID();
}
_extraLaserID = geometryCache->allocateID();
{
auto state = std::make_shared<gpu::State>();
auto VS = gpu::Shader::createVertex(std::string(glowLine_vert));
auto PS = gpu::Shader::createPixel(std::string(glowLine_frag));
auto program = gpu::Shader::createProgram(VS, PS);
state->setCullMode(gpu::State::CULL_NONE);
state->setDepthTest(true, false, gpu::LESS_EQUAL);
state->setBlendFunction(true,
gpu::State::SRC_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::INV_SRC_ALPHA,
gpu::State::FACTOR_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::ONE);
gpu::Shader::makeProgram(*program, gpu::Shader::BindingSet());
_glowLinePipeline = gpu::Pipeline::create(program, state);
for (const auto& buffer : program->getBuffers()) {
if (buffer._name == "lineData") {
_handLaserUniformSlot = buffer._location;
}
}
_handLaserUniforms = std::array<gpu::BufferPointer, 2>{ { std::make_shared<gpu::Buffer>(), std::make_shared<gpu::Buffer>() } };
_extraLaserUniforms = std::make_shared<gpu::Buffer>();
};
}
void HmdDisplayPlugin::uncustomizeContext() {
@ -131,12 +156,10 @@ void HmdDisplayPlugin::uncustomizeContext() {
});
_overlayRenderer = OverlayRenderer();
_previewTexture.reset();
auto geometryCache = DependencyManager::get<GeometryCache>();
for (size_t i = 0; i < _geometryIds.size(); ++i) {
geometryCache->releaseID(_geometryIds[i]);
}
geometryCache->releaseID(_extraLaserID);
_handLaserUniforms[0].reset();
_handLaserUniforms[1].reset();
_extraLaserUniforms.reset();
_glowLinePipeline.reset();
Parent::uncustomizeContext();
}
@ -682,12 +705,16 @@ void HmdDisplayPlugin::compositeExtra() {
if (_presentHandPoses[0] == IDENTITY_MATRIX && _presentHandPoses[1] == IDENTITY_MATRIX && !_presentExtraLaser.valid()) {
return;
}
auto geometryCache = DependencyManager::get<GeometryCache>();
render([&](gpu::Batch& batch) {
batch.setFramebuffer(_compositeFramebuffer);
batch.setModelTransform(Transform());
batch.setViewportTransform(ivec4(uvec2(0), _renderTargetSize));
batch.setViewTransform(_currentPresentFrameInfo.presentPose, false);
// Compile the shaders
batch.setPipeline(_glowLinePipeline);
bilateral::for_each_side([&](bilateral::Side side){
auto index = bilateral::index(side);
if (_presentHandPoses[index] == IDENTITY_MATRIX) {
@ -696,13 +723,19 @@ void HmdDisplayPlugin::compositeExtra() {
const auto& laser = _presentHandLasers[index];
if (laser.valid()) {
const auto& points = _presentHandLaserPoints[index];
geometryCache->renderGlowLine(batch, points.first, points.second, laser.color, _geometryIds[index]);
_handLaserUniforms[index]->resize(sizeof(HandLaserData));
_handLaserUniforms[index]->setSubData(0, HandLaserData { vec4(points.first, 1.0f), vec4(points.second, 1.0f), _handLasers[index].color });
batch.setUniformBuffer(_handLaserUniformSlot, _handLaserUniforms[index]);
batch.draw(gpu::TRIANGLE_STRIP, 4, 0);
}
});
if (_presentExtraLaser.valid()) {
const auto& points = _presentExtraLaserPoints;
geometryCache->renderGlowLine(batch, points.first, points.second, _presentExtraLaser.color, _extraLaserID);
_extraLaserUniforms->resize(sizeof(HandLaserData));
_extraLaserUniforms->setSubData(0, HandLaserData { vec4(points.first, 1.0f), vec4(points.second, 1.0f), _presentExtraLaser.color });
batch.setUniformBuffer(_handLaserUniformSlot, _extraLaserUniforms);
batch.draw(gpu::TRIANGLE_STRIP, 4, 0);
}
});
}

View file

@ -80,8 +80,6 @@ protected:
Transform _presentUiModelTransform;
std::array<HandLaserInfo, 2> _presentHandLasers;
std::array<int, 2> _geometryIds;
int _extraLaserID;
std::array<mat4, 2> _presentHandPoses;
std::array<std::pair<vec3, vec3>, 2> _presentHandLaserPoints;
@ -120,6 +118,10 @@ private:
bool _disablePreviewItemAdded { false };
bool _monoPreview { true };
bool _clearPreviewFlag { false };
std::array<gpu::BufferPointer, 2> _handLaserUniforms;
uint32_t _handLaserUniformSlot { 0 };
gpu::BufferPointer _extraLaserUniforms;
gpu::PipelinePointer _glowLinePipeline;
gpu::TexturePointer _previewTexture;
glm::vec2 _lastWindowSize;

View file

@ -580,6 +580,11 @@ void RenderablePolyVoxEntityItem::render(RenderArgs* args) {
voxelVolumeSize = _voxelVolumeSize;
});
if (!mesh ||
!mesh->getIndexBuffer()._buffer) {
return;
}
if (!_pipeline) {
gpu::ShaderPointer vertexShader = gpu::Shader::createVertex(std::string(polyvox_vert));
gpu::ShaderPointer pixelShader = gpu::Shader::createPixel(std::string(polyvox_frag));
@ -600,17 +605,23 @@ void RenderablePolyVoxEntityItem::render(RenderArgs* args) {
_pipeline = gpu::Pipeline::create(program, state);
}
if (!_vertexFormat) {
auto vf = std::make_shared<gpu::Stream::Format>();
vf->setAttribute(gpu::Stream::POSITION, 0, gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::XYZ), 0);
vf->setAttribute(gpu::Stream::NORMAL, 0, gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::XYZ), 12);
_vertexFormat = vf;
}
gpu::Batch& batch = *args->_batch;
batch.setPipeline(_pipeline);
Transform transform(voxelToWorldMatrix());
batch.setModelTransform(transform);
batch.setInputFormat(mesh->getVertexFormat());
batch.setInputBuffer(gpu::Stream::POSITION, mesh->getVertexBuffer());
batch.setInputBuffer(gpu::Stream::NORMAL,
mesh->getVertexBuffer()._buffer,
sizeof(float) * 3,
mesh->getVertexBuffer()._stride);
batch.setInputFormat(_vertexFormat);
batch.setInputBuffer(gpu::Stream::POSITION, mesh->getVertexBuffer()._buffer,
0,
sizeof(PolyVox::PositionMaterialNormal));
batch.setIndexBuffer(gpu::UINT32, mesh->getIndexBuffer()._buffer, 0);
if (!_xTextureURL.isEmpty() && !_xTexture) {
@ -1097,7 +1108,6 @@ void RenderablePolyVoxEntityItem::getMesh() {
auto entity = std::static_pointer_cast<RenderablePolyVoxEntityItem>(getThisPointer());
QtConcurrent::run([entity, voxelSurfaceStyle] {
model::MeshPointer mesh(new model::Mesh());
@ -1146,18 +1156,14 @@ void RenderablePolyVoxEntityItem::getMesh() {
auto vertexBuffer = std::make_shared<gpu::Buffer>(vecVertices.size() * sizeof(PolyVox::PositionMaterialNormal),
(gpu::Byte*)vecVertices.data());
auto vertexBufferPtr = gpu::BufferPointer(vertexBuffer);
gpu::Resource::Size vertexBufferSize = 0;
if (vertexBufferPtr->getSize() > sizeof(float) * 3) {
vertexBufferSize = vertexBufferPtr->getSize() - sizeof(float) * 3;
}
gpu::BufferView vertexBufferView(vertexBufferPtr, 0, vertexBufferSize,
gpu::BufferView vertexBufferView(vertexBufferPtr, 0,
vertexBufferPtr->getSize(),
sizeof(PolyVox::PositionMaterialNormal),
gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::RAW));
mesh->setVertexBuffer(vertexBufferView);
mesh->addAttribute(gpu::Stream::NORMAL,
gpu::BufferView(vertexBufferPtr,
sizeof(float) * 3,
vertexBufferPtr->getSize() - sizeof(float) * 3,
gpu::BufferView(vertexBufferPtr, sizeof(float) * 3,
vertexBufferPtr->getSize() ,
sizeof(PolyVox::PositionMaterialNormal),
gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::RAW)));
entity->setMesh(mesh);

View file

@ -149,6 +149,7 @@ private:
// may not match _voxelVolumeSize.
model::MeshPointer _mesh;
gpu::Stream::FormatPointer _vertexFormat;
bool _meshDirty { true }; // does collision-shape need to be recomputed?
bool _meshInitialized { false };

View file

@ -64,6 +64,7 @@ Node::Node(const QUuid& uuid, NodeType_t type, const HifiSockAddr& publicSocket,
{
// Update socket's object name
setType(_type);
_ignoreRadiusEnabled = false;
}
void Node::setType(char type) {
@ -101,6 +102,15 @@ void Node::addIgnoredNode(const QUuid& otherNodeID) {
}
}
void Node::parseIgnoreRadiusRequestMessage(QSharedPointer<ReceivedMessage> message) {
bool enabled;
float radius;
message->readPrimitive(&enabled);
message->readPrimitive(&radius);
_ignoreRadiusEnabled = enabled;
_ignoreRadius = radius;
}
QDataStream& operator<<(QDataStream& out, const Node& node) {
out << node._type;
out << node._uuid;

View file

@ -74,10 +74,14 @@ public:
void parseIgnoreRequestMessage(QSharedPointer<ReceivedMessage> message);
void addIgnoredNode(const QUuid& otherNodeID);
bool isIgnoringNodeWithID(const QUuid& nodeID) const { return _ignoredNodeIDSet.find(nodeID) != _ignoredNodeIDSet.cend(); }
void parseIgnoreRadiusRequestMessage(QSharedPointer<ReceivedMessage> message);
friend QDataStream& operator<<(QDataStream& out, const Node& node);
friend QDataStream& operator>>(QDataStream& in, Node& node);
bool isIgnoreRadiusEnabled() const { return _ignoreRadiusEnabled; }
float getIgnoreRadius() { return _ignoreRadiusEnabled ? _ignoreRadius.load() : std::numeric_limits<float>::max(); }
private:
// privatize copy and assignment operator to disallow Node copying
Node(const Node &otherNode);
@ -94,6 +98,9 @@ private:
MovingPercentile _clockSkewMovingPercentile;
NodePermissions _permissions;
tbb::concurrent_unordered_set<QUuid, UUIDHasher> _ignoredNodeIDSet;
std::atomic_bool _ignoreRadiusEnabled;
std::atomic<float> _ignoreRadius { 0.0f };
};
Q_DECLARE_METATYPE(Node*)

View file

@ -18,6 +18,7 @@
#include <QtCore/QUrl>
#include <QtCore/QThread>
#include <QtNetwork/QHostInfo>
#include <QtNetwork/QNetworkInterface>
#include <LogHandler.h>
#include <UUID.h>
@ -346,6 +347,28 @@ void NodeList::sendDomainServerCheckIn() {
// include the protocol version signature in our connect request
QByteArray protocolVersionSig = protocolVersionsSignature();
packetStream.writeBytes(protocolVersionSig.constData(), protocolVersionSig.size());
// if possible, include the MAC address for the current interface in our connect request
QString hardwareAddress;
for (auto networkInterface : QNetworkInterface::allInterfaces()) {
for (auto interfaceAddress : networkInterface.addressEntries()) {
if (interfaceAddress.ip() == _localSockAddr.getAddress()) {
// this is the interface whose local IP matches what we've detected the current IP to be
hardwareAddress = networkInterface.hardwareAddress();
// stop checking interfaces and addresses
break;
}
}
// stop looping if this was the current interface
if (!hardwareAddress.isEmpty()) {
break;
}
}
packetStream << hardwareAddress;
}
// pack our data to send to the domain-server including
@ -727,9 +750,26 @@ bool NodeList::sockAddrBelongsToDomainOrNode(const HifiSockAddr& sockAddr) {
return _domainHandler.getSockAddr() == sockAddr || LimitedNodeList::sockAddrBelongsToNode(sockAddr);
}
void NodeList::ignoreNodesInRadius(float radiusToIgnore, bool enabled) {
_ignoreRadiusEnabled.set(enabled);
_ignoreRadius.set(radiusToIgnore);
eachMatchingNode([](const SharedNodePointer& node)->bool {
return (node->getType() == NodeType::AudioMixer || node->getType() == NodeType::AvatarMixer);
}, [this](const SharedNodePointer& destinationNode) {
sendIgnoreRadiusStateToNode(destinationNode);
});
}
void NodeList::sendIgnoreRadiusStateToNode(const SharedNodePointer& destinationNode) {
auto ignorePacket = NLPacket::create(PacketType::RadiusIgnoreRequest, sizeof(bool) + sizeof(float), true);
ignorePacket->writePrimitive(_ignoreRadiusEnabled.get());
ignorePacket->writePrimitive(_ignoreRadius.get());
sendPacket(std::move(ignorePacket), *destinationNode);
}
void NodeList::ignoreNodeBySessionID(const QUuid& nodeID) {
// enumerate the nodes to send a reliable ignore packet to each that can leverage it
if (!nodeID.isNull() && _sessionUUID != nodeID) {
eachMatchingNode([&nodeID](const SharedNodePointer& node)->bool {
if (node->getType() == NodeType::AudioMixer || node->getType() == NodeType::AvatarMixer) {
@ -788,6 +828,9 @@ void NodeList::maybeSendIgnoreSetToNode(SharedNodePointer newNode) {
// send this NLPacketList to the new node
sendPacketList(std::move(ignorePacketList), *newNode);
}
// also send them the current ignore radius state.
sendIgnoreRadiusStateToNode(newNode);
}
}

View file

@ -30,6 +30,7 @@
#include <QtNetwork/QUdpSocket>
#include <DependencyManager.h>
#include <SettingHandle.h>
#include "DomainHandler.h"
#include "LimitedNodeList.h"
@ -70,6 +71,12 @@ public:
void setIsShuttingDown(bool isShuttingDown) { _isShuttingDown = isShuttingDown; }
void ignoreNodesInRadius(float radiusToIgnore, bool enabled = true);
float getIgnoreRadius() const { return _ignoreRadius.get(); }
bool getIgnoreRadiusEnabled() const { return _ignoreRadiusEnabled.get(); }
void toggleIgnoreRadius() { ignoreNodesInRadius(getIgnoreRadius(), !getIgnoreRadiusEnabled()); }
void enableIgnoreRadius() { ignoreNodesInRadius(getIgnoreRadius(), true); }
void disableIgnoreRadius() { ignoreNodesInRadius(getIgnoreRadius(), false); }
void ignoreNodeBySessionID(const QUuid& nodeID);
bool isIgnoringNode(const QUuid& nodeID) const;
@ -101,7 +108,7 @@ signals:
void limitOfSilentDomainCheckInsReached();
void receivedDomainServerList();
void ignoredNode(const QUuid& nodeID);
private slots:
void stopKeepalivePingTimer();
void sendPendingDSPathQuery();
@ -146,6 +153,10 @@ private:
mutable QReadWriteLock _ignoredSetLock;
tbb::concurrent_unordered_set<QUuid, UUIDHasher> _ignoredNodeIDs;
void sendIgnoreRadiusStateToNode(const SharedNodePointer& destinationNode);
Setting::Handle<bool> _ignoreRadiusEnabled { "IgnoreRadiusEnabled", false };
Setting::Handle<float> _ignoreRadius { "IgnoreRadius", 1.0f };
#if (PR_BUILD || DEV_BUILD)
bool _shouldSendNewerVersion { false };
#endif

View file

@ -67,7 +67,7 @@ PacketVersion versionForPacketType(PacketType packetType) {
return static_cast<PacketVersion>(DomainConnectionDeniedVersion::IncludesExtraInfo);
case PacketType::DomainConnectRequest:
return static_cast<PacketVersion>(DomainConnectRequestVersion::HasProtocolVersions);
return static_cast<PacketVersion>(DomainConnectRequestVersion::HasMACAddress);
case PacketType::DomainServerAddedNode:
return static_cast<PacketVersion>(DomainServerAddedNodeVersion::PermissionsGrid);

View file

@ -100,7 +100,8 @@ public:
MoreEntityShapes,
NodeKickRequest,
NodeMuteRequest,
LAST_PACKET_TYPE = NodeMuteRequest
RadiusIgnoreRequest,
LAST_PACKET_TYPE = RadiusIgnoreRequest
};
};
@ -207,7 +208,8 @@ enum class AvatarMixerPacketVersion : PacketVersion {
enum class DomainConnectRequestVersion : PacketVersion {
NoHostname = 17,
HasHostname,
HasProtocolVersions
HasProtocolVersions,
HasMACAddress
};
enum class DomainConnectionDeniedVersion : PacketVersion {

View file

@ -38,7 +38,6 @@
#include "simple_opaque_web_browser_frag.h"
#include "simple_transparent_web_browser_frag.h"
#include "glowLine_vert.h"
#include "glowLine_geom.h"
#include "glowLine_frag.h"
#include "grid_frag.h"
@ -1405,6 +1404,7 @@ GeometryCache::BatchItemDetails::~BatchItemDetails() {
void GeometryCache::BatchItemDetails::clear() {
isCreated = false;
uniformBuffer.reset();
verticesBuffer.reset();
colorBuffer.reset();
streamFormat.reset();
@ -1593,8 +1593,6 @@ void GeometryCache::renderGlowLine(gpu::Batch& batch, const glm::vec3& p1, const
glowIntensity = 0.0f;
#endif
glowIntensity = 0.0f;
if (glowIntensity <= 0) {
bindSimpleProgram(batch, false, false, false, true, false);
renderLine(batch, p1, p2, color, id);
@ -1602,20 +1600,20 @@ void GeometryCache::renderGlowLine(gpu::Batch& batch, const glm::vec3& p1, const
}
// Compile the shaders
static const uint32_t LINE_DATA_SLOT = 1;
static std::once_flag once;
std::call_once(once, [&] {
auto state = std::make_shared<gpu::State>();
auto VS = gpu::Shader::createVertex(std::string(glowLine_vert));
auto GS = gpu::Shader::createGeometry(std::string(glowLine_geom));
auto PS = gpu::Shader::createPixel(std::string(glowLine_frag));
auto program = gpu::Shader::createProgram(VS, GS, PS);
auto program = gpu::Shader::createProgram(VS, PS);
state->setCullMode(gpu::State::CULL_NONE);
state->setDepthTest(true, false, gpu::LESS_EQUAL);
state->setBlendFunction(true,
gpu::State::SRC_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::INV_SRC_ALPHA,
gpu::State::FACTOR_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::ONE);
gpu::Shader::BindingSet slotBindings;
slotBindings.insert(gpu::Shader::Binding(std::string("normalFittingMap"), render::ShapePipeline::Slot::MAP::NORMAL_FITTING));
slotBindings.insert(gpu::Shader::Binding(std::string("lineData"), LINE_DATA_SLOT));
gpu::Shader::makeProgram(*program, slotBindings);
_glowLinePipeline = gpu::Pipeline::create(program, state);
});
@ -1626,11 +1624,6 @@ void GeometryCache::renderGlowLine(gpu::Batch& batch, const glm::vec3& p1, const
bool registered = (id != UNKNOWN_ID);
BatchItemDetails& details = _registeredLine3DVBOs[id];
int compactColor = ((int(color.x * 255.0f) & 0xFF)) |
((int(color.y * 255.0f) & 0xFF) << 8) |
((int(color.z * 255.0f) & 0xFF) << 16) |
((int(color.w * 255.0f) & 0xFF) << 24);
// if this is a registered quad, and we have buffers, then check to see if the geometry changed and rebuild if needed
if (registered && details.isCreated) {
Vec3Pair& lastKey = _lastRegisteredLine3D[id];
@ -1640,47 +1633,25 @@ void GeometryCache::renderGlowLine(gpu::Batch& batch, const glm::vec3& p1, const
}
}
const int FLOATS_PER_VERTEX = 3 + 3; // vertices + normals
const int NUM_POS_COORDS = 3;
const int VERTEX_NORMAL_OFFSET = NUM_POS_COORDS * sizeof(float);
const int vertices = 2;
const int NUM_VERTICES = 4;
if (!details.isCreated) {
details.isCreated = true;
details.vertices = vertices;
details.vertexSize = FLOATS_PER_VERTEX;
details.uniformBuffer = std::make_shared<gpu::Buffer>();
auto verticesBuffer = std::make_shared<gpu::Buffer>();
auto colorBuffer = std::make_shared<gpu::Buffer>();
auto streamFormat = std::make_shared<gpu::Stream::Format>();
auto stream = std::make_shared<gpu::BufferStream>();
struct LineData {
vec4 p1;
vec4 p2;
vec4 color;
};
details.verticesBuffer = verticesBuffer;
details.colorBuffer = colorBuffer;
details.streamFormat = streamFormat;
details.stream = stream;
details.streamFormat->setAttribute(gpu::Stream::POSITION, 0, gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::XYZ), 0);
details.streamFormat->setAttribute(gpu::Stream::NORMAL, 0, gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::XYZ), VERTEX_NORMAL_OFFSET);
details.streamFormat->setAttribute(gpu::Stream::COLOR, 1, gpu::Element(gpu::VEC4, gpu::NUINT8, gpu::RGBA));
details.stream->addBuffer(details.verticesBuffer, 0, details.streamFormat->getChannels().at(0)._stride);
details.stream->addBuffer(details.colorBuffer, 0, details.streamFormat->getChannels().at(1)._stride);
const glm::vec3 NORMAL(1.0f, 0.0f, 0.0f);
float vertexBuffer[vertices * FLOATS_PER_VERTEX] = {
p1.x, p1.y, p1.z, NORMAL.x, NORMAL.y, NORMAL.z,
p2.x, p2.y, p2.z, NORMAL.x, NORMAL.y, NORMAL.z };
const int NUM_COLOR_SCALARS = 2;
int colors[NUM_COLOR_SCALARS] = { compactColor, compactColor };
details.verticesBuffer->append(sizeof(vertexBuffer), (gpu::Byte*) vertexBuffer);
details.colorBuffer->append(sizeof(colors), (gpu::Byte*) colors);
LineData lineData { vec4(p1, 1.0f), vec4(p2, 1.0f), color };
details.uniformBuffer->resize(sizeof(LineData));
details.uniformBuffer->setSubData(0, lineData);
}
// this is what it takes to render a quad
batch.setInputFormat(details.streamFormat);
batch.setInputStream(0, *details.stream);
batch.draw(gpu::LINES, 2, 0);
// The shader requires no vertices, only uniforms.
batch.setUniformBuffer(LINE_DATA_SLOT, details.uniformBuffer);
batch.draw(gpu::TRIANGLE_STRIP, NUM_VERTICES, 0);
}
void GeometryCache::useSimpleDrawPipeline(gpu::Batch& batch, bool noBlend) {

View file

@ -369,6 +369,7 @@ private:
static int population;
gpu::BufferPointer verticesBuffer;
gpu::BufferPointer colorBuffer;
gpu::BufferPointer uniformBuffer;
gpu::Stream::FormatPointer streamFormat;
gpu::BufferStreamPointer stream;

View file

@ -10,26 +10,24 @@
//
layout(location = 0) in vec4 inColor;
layout(location = 1) in vec3 inLineDistance;
out vec4 _fragColor;
void main(void) {
vec2 d = inLineDistance.xy;
d.y = abs(d.y);
d.x = abs(d.x);
if (d.x > 1.0) {
d.x = (d.x - 1.0) / 0.02;
} else {
d.x = 0.0;
}
float alpha = 1.0 - length(d);
if (alpha <= 0.0) {
discard;
}
alpha = pow(alpha, 10.0);
if (alpha < 0.05) {
// The incoming value actually ranges from -1 to 1, so modify it
// so that it goes from 0 -> 1 -> 0 with the solid alpha being at
// the center of the line
float alpha = 1.0 - abs(inColor.a);
// Convert from a linear alpha curve to a sharp peaked one
alpha = pow(alpha, 10);
// Drop everything where the curve falls off to nearly nothing
if (alpha <= 0.05) {
discard;
}
// Emit the color
_fragColor = vec4(inColor.rgb, alpha);
return;
}

View file

@ -1,102 +0,0 @@
<@include gpu/Config.slh@>
<$VERSION_HEADER$>
// Generated on <$_SCRIBE_DATE$>
//
// Created by Bradley Austin Davis on 2016/07/05
// Copyright 2013-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
//
#extension GL_EXT_geometry_shader4 : enable
<@include gpu/Transform.slh@>
<$declareStandardCameraTransform()$>
layout(location = 0) in vec4 inColor[];
layout(location = 0) out vec4 outColor;
layout(location = 1) out vec3 outLineDistance;
layout(lines) in;
layout(triangle_strip, max_vertices = 24) out;
vec3 ndcToEyeSpace(in vec4 v) {
TransformCamera cam = getTransformCamera();
vec4 u = cam._projectionInverse * v;
return u.xyz / u.w;
}
vec2 toScreenSpace(in vec4 v)
{
TransformCamera cam = getTransformCamera();
vec4 u = cam._projection * cam._view * v;
return u.xy / u.w;
}
vec3[2] getOrthogonals(in vec3 n, float scale) {
float yDot = abs(dot(n, vec3(0, 1, 0)));
vec3 result[2];
if (yDot < 0.9) {
result[0] = normalize(cross(n, vec3(0, 1, 0)));
} else {
result[0] = normalize(cross(n, vec3(1, 0, 0)));
}
// The cross of result[0] and n is orthogonal to both, which are orthogonal to each other
result[1] = cross(result[0], n);
result[0] *= scale;
result[1] *= scale;
return result;
}
vec2 orthogonal(vec2 v) {
vec2 result = v.yx;
result.y *= -1.0;
return result;
}
void main() {
vec2 endpoints[2];
vec3 eyeSpace[2];
TransformCamera cam = getTransformCamera();
for (int i = 0; i < 2; ++i) {
eyeSpace[i] = ndcToEyeSpace(gl_PositionIn[i]);
endpoints[i] = gl_PositionIn[i].xy / gl_PositionIn[i].w;
}
vec2 lineNormal = normalize(endpoints[1] - endpoints[0]);
vec2 lineOrthogonal = orthogonal(lineNormal);
lineNormal *= 0.02;
lineOrthogonal *= 0.02;
gl_Position = gl_PositionIn[0];
gl_Position.xy -= lineOrthogonal;
outColor = inColor[0];
outLineDistance = vec3(-1.02, -1, gl_Position.z);
EmitVertex();
gl_Position = gl_PositionIn[0];
gl_Position.xy += lineOrthogonal;
outColor = inColor[0];
outLineDistance = vec3(-1.02, 1, gl_Position.z);
EmitVertex();
gl_Position = gl_PositionIn[1];
gl_Position.xy -= lineOrthogonal;
outColor = inColor[1];
outLineDistance = vec3(1.02, -1, gl_Position.z);
EmitVertex();
gl_Position = gl_PositionIn[1];
gl_Position.xy += lineOrthogonal;
outColor = inColor[1];
outLineDistance = vec3(1.02, 1, gl_Position.z);
EmitVertex();
EndPrimitive();
}

View file

@ -9,18 +9,50 @@
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
<@include gpu/Inputs.slh@>
<@include gpu/Color.slh@>
<@include gpu/Transform.slh@>
<$declareStandardTransform()$>
layout(std140) uniform lineData {
vec4 p1;
vec4 p2;
vec4 color;
};
layout(location = 0) out vec4 _color;
void main(void) {
_color = inColor;
_color = color;
// standard transform
TransformCamera cam = getTransformCamera();
TransformObject obj = getTransformObject();
<$transformModelToClipPos(cam, obj, inPosition, gl_Position)$>
vec4 p1eye, p2eye;
<$transformModelToEyePos(cam, obj, p1, p1eye)$>
<$transformModelToEyePos(cam, obj, p2, p2eye)$>
p1eye /= p1eye.w;
p2eye /= p2eye.w;
// Find the line direction
vec3 v1 = normalize(p1eye.xyz - p2eye.xyz);
// Find the vector from the eye to one of the points
vec3 v2 = normalize(p1eye.xyz);
// The orthogonal vector is the cross product of these two
vec3 orthogonal = cross(v1, v2) * 0.02;
// Deteremine which end to emit based on the vertex id (even / odd)
vec4 eye = (0 == gl_VertexID % 2) ? p1eye : p2eye;
// Add or subtract the orthogonal vector based on a different vertex ID
// calculation
if (gl_VertexID < 2) {
// Use the alpha channel to store the distance from the center in 'quad space'
_color.a = -1.0;
eye.xyz -= orthogonal;
} else {
_color.a = 1.0;
eye.xyz += orthogonal;
}
// Finally, put the eyespace vertex into clip space
<$transformEyeToClipPos(cam, eye, gl_Position)$>
}

View file

@ -38,3 +38,27 @@ bool UsersScriptingInterface::getCanKick() {
// ask the NodeList to return our ability to kick
return DependencyManager::get<NodeList>()->getThisNodeCanKick();
}
void UsersScriptingInterface::toggleIgnoreRadius() {
DependencyManager::get<NodeList>()->toggleIgnoreRadius();
}
void UsersScriptingInterface::enableIgnoreRadius() {
DependencyManager::get<NodeList>()->enableIgnoreRadius();
}
void UsersScriptingInterface::disableIgnoreRadius() {
DependencyManager::get<NodeList>()->disableIgnoreRadius();
}
void UsersScriptingInterface::setIgnoreRadius(float radius, bool enabled) {
DependencyManager::get<NodeList>()->ignoreNodesInRadius(radius, enabled);
}
float UsersScriptingInterface::getIgnoreRadius() {
return DependencyManager::get<NodeList>()->getIgnoreRadius();
}
bool UsersScriptingInterface::getIgnoreRadiusEnabled() {
return DependencyManager::get<NodeList>()->getIgnoreRadiusEnabled();
}

View file

@ -16,6 +16,9 @@
#include <DependencyManager.h>
/**jsdoc
* @namespace Users
*/
class UsersScriptingInterface : public QObject, public Dependency {
Q_OBJECT
SINGLETON_DEPENDENCY
@ -26,12 +29,75 @@ public:
UsersScriptingInterface();
public slots:
/**jsdoc
* Ignore another user.
* @function Users.ignore
* @param {nodeID} nodeID The node or session ID of the user you want to ignore.
*/
void ignore(const QUuid& nodeID);
/**jsdoc
* Kick another user.
* @function Users.kick
* @param {nodeID} nodeID The node or session ID of the user you want to kick.
*/
void kick(const QUuid& nodeID);
/**jsdoc
* Mute another user.
* @function Users.mute
* @param {nodeID} nodeID The node or session ID of the user you want to mute.
*/
void mute(const QUuid& nodeID);
/**jsdoc
* Returns `true` if the DomainServer will allow this Node/Avatar to make kick
* @function Users.getCanKick
* @return {bool} `true` if the client can kick other users, `false` if not.
*/
bool getCanKick();
/**jsdoc
* Toggle the state of the ignore in radius feature
* @function Users.toggleIgnoreRadius
*/
void toggleIgnoreRadius();
/**jsdoc
* Enables the ignore radius feature.
* @function Users.enableIgnoreRadius
*/
void enableIgnoreRadius();
/**jsdoc
* Disables the ignore radius feature.
* @function Users.disableIgnoreRadius
*/
void disableIgnoreRadius();
/**jsdoc
* sets the parameters for the ignore radius feature.
* @function Users.setIgnoreRadius
* @param {number} radius The radius for the auto ignore in radius feature
* @param {bool} [enabled=true] Whether the ignore in radius feature should be enabled
*/
void setIgnoreRadius(float radius, bool enabled = true);
/**jsdoc
* Returns the effective radius of the ingore radius feature if it is enabled.
* @function Users.getIgnoreRadius
* @return {number} radius of the ignore feature
*/
float getIgnoreRadius();
/**jsdoc
* Returns `true` if the ignore in radius feature is enabled
* @function Users.getIgnoreRadiusEnabled
* @return {bool} `true` if the ignore in radius feature is enabled, `false` if not.
*/
bool getIgnoreRadiusEnabled();
signals:
void canKickChanged(bool canKick);
};

View file

@ -33,7 +33,8 @@ var DEFAULT_SCRIPTS = [
"system/dialTone.js",
"system/firstPersonHMD.js",
"system/snapshot.js",
"system/help.js"
"system/help.js",
"system/bubble.js"
];
// add a menu item for debugging

View file

@ -0,0 +1,275 @@
<?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>
<g id="Layer_3">
</g>
<g>
<path class="st3" d="M24.3,19.6c-0.5,0.5-0.9,1-1.3,1.5c-0.1,0.1-0.1,0.3-0.1,0.4c0,1,0,2,0.1,3c0,0.4-0.2,0.7-0.7,0.8
c-0.4,0-0.8-0.2-0.8-0.7c-0.1-1.3-0.1-2.5-0.2-3.8c0-0.2,0.1-0.4,0.2-0.6c0.7-0.9,1.4-1.7,2-2.6c0.3-0.4,1-0.9,1.5-0.9
c0.2,0,0.6,0,0.8,0c0.5,0,0.8,0.1,1.1,0.6c0.5,0.7,0.9,1.4,1.4,2.1c0.4,0.7,0.8,1,1.7,1.2c0.6,0.1,1.3,0.3,1.9,0.5
c0.2,0,0.3,0.1,0.5,0.2c0.3,0.2,0.4,0.5,0.3,0.8c-0.1,0.3-0.3,0.4-0.6,0.4c-0.2,0-0.5,0-0.7-0.1c-0.8-0.2-1.6-0.4-2.4-0.6
c-0.5-0.1-0.8-0.4-1.2-0.7c-0.2-0.2-0.3-0.3-0.5-0.5c0,0.2,0,0.3,0,0.4c0,0.7,0,1.4-0.1,2.1c0,0.2-0.2,6.4-0.3,8.9
c0,0.3-0.1,0.6-0.2,0.9c-0.1,0.4-0.4,0.6-0.9,0.6c-0.4,0-0.8-0.3-0.9-0.7c-0.1-0.3-0.1-0.6-0.1-1c0-2.7-0.4-6.5-0.5-7
c0-0.5-0.1-0.6-0.1-1.2c-0.1-0.3-0.1-0.5-0.1-0.7C24.2,21.8,24.3,20.8,24.3,19.6z"/>
<path class="st3" d="M27.1,14.7c0,0.7-0.6,1.3-1.4,1.3l-0.3,0c-0.7,0-1.3-0.6-1.3-1.4l0-1.3c0-0.7,0.6-1.3,1.4-1.3l0.3,0
c0.7,0,1.3,0.6,1.3,1.4L27.1,14.7z"/>
</g>
<path class="st3" d="M22,31.5L22,31.5L22,31.5c-0.1,0-0.2,0-0.4-0.1c-2.2-0.7-4.2-2.1-5.7-3.9c-0.5-0.5-0.4-1.4,0.2-1.8
c0.2-0.2,0.5-0.3,0.9-0.3c0.4,0,0.7,0.2,1,0.5c0.6,0.7,1.3,1.4,2.1,1.9c0.8,0.5,1.6,0.9,2.5,1.2l0,0c0.3,0.1,0.6,0.3,0.7,0.6
c0.2,0.3,0.2,0.7,0.1,1c-0.1,0.3-0.3,0.5-0.5,0.7C22.5,31.5,22.2,31.6,22,31.5z"/>
<path class="st3" d="M29.5,31.3c-0.5,0-1-0.3-1.2-0.8c-0.1-0.3-0.1-0.7,0-1s0.4-0.6,0.7-0.7c0.8-0.3,1.5-0.7,2.1-1.2
c0.8-0.6,1.6-1.4,2.2-2.2l0,0c0.2-0.3,0.7-0.5,1.1-0.5c0.3,0,0.5,0.1,0.7,0.2c0.3,0.2,0.5,0.5,0.5,0.8c0.1,0.3,0,0.7-0.2,1
c-0.8,1.1-1.7,2-2.8,2.8c-0.8,0.6-1.7,1.1-2.7,1.5C29.9,31.3,29.7,31.3,29.5,31.3z"/>
<path class="st3" d="M14.9,24.7c-0.3,0-0.5-0.1-0.7-0.2c-0.2-0.2-0.4-0.4-0.5-0.6c-0.2-0.7,0.1-1.4,0.8-1.7c0.2-0.1,0.3-0.1,0.5-0.1
c0.3,0,0.5,0.1,0.7,0.2c0.2,0.2,0.4,0.4,0.5,0.6c0.1,0.3,0.1,0.7,0,1c-0.1,0.3-0.4,0.5-0.7,0.7C15.2,24.7,15,24.7,14.9,24.7z"/>
<path class="st3" d="M36.1,24L36.1,24L36.1,24c-0.1,0-0.2,0-0.3-0.1c-0.3-0.1-0.6-0.3-0.8-0.6c-0.2-0.3-0.2-0.6-0.1-1
c0.2-0.6,0.7-0.9,1.3-0.9c0.1,0,0.2,0,0.3,0.1c0.3,0.1,0.6,0.3,0.8,0.6c0.2,0.3,0.2,0.6,0.1,1C37.2,23.6,36.7,24,36.1,24z"/>
<path class="st3" d="M14.2,21c-0.3,0-0.7-0.1-0.9-0.4c-0.2-0.2-0.4-0.6-0.4-0.9c0-0.1,0-0.2,0-0.4c0.1-2.2,0.7-4.4,1.9-6.3
c0.1-0.2,0.3-0.4,0.5-0.5c0.2-0.1,0.4-0.2,0.6-0.1c0.2,0,0.5,0.1,0.6,0.2c0.6,0.4,0.8,1.2,0.4,1.8c-0.9,1.5-1.5,3.2-1.5,5
c0,0.1,0,0.2,0,0.3c0,0.3-0.1,0.7-0.4,0.9C14.9,20.8,14.6,21,14.2,21C14.2,21,14.2,21,14.2,21z"/>
<path class="st3" d="M36.5,20.2c-0.3,0-0.6-0.1-0.9-0.4c-0.2-0.2-0.4-0.5-0.4-0.8c-0.1-0.9-0.2-1.8-0.6-2.7
c-0.3-0.9-0.8-1.7-1.3-2.5l0,0c-0.2-0.3-0.3-0.6-0.2-1c0.1-0.3,0.2-0.6,0.5-0.8c0.2-0.2,0.5-0.3,0.8-0.2c0.4,0,0.8,0.2,1,0.5
c0,0,0,0,0,0l0,0c1.4,1.9,2.2,4.2,2.4,6.5c0,0.3-0.1,0.7-0.3,0.9c-0.2,0.3-0.5,0.4-0.9,0.4C36.6,20.2,36.6,20.2,36.5,20.2z"/>
<path class="st3" d="M18.5,12.1c-0.4,0-0.7-0.2-1-0.5c-0.2-0.3-0.3-0.6-0.3-1c0-0.3,0.2-0.6,0.5-0.9c0.2-0.2,0.5-0.3,0.8-0.3
c0.4,0,0.7,0.2,1,0.5c0.2,0.3,0.3,0.6,0.3,1c0,0.3-0.2,0.6-0.5,0.9C19.1,12,18.8,12.1,18.5,12.1z"/>
<path class="st3" d="M31.6,11.7c-0.2,0-0.5-0.1-0.7-0.2c-0.3-0.2-0.5-0.5-0.5-0.8c-0.1-0.3,0-0.7,0.2-1c0.2-0.4,0.7-0.6,1.1-0.6
c0.2,0,0.5,0.1,0.7,0.2c0.6,0.4,0.7,1.2,0.3,1.8C32.5,11.5,32.1,11.7,31.6,11.7z"/>
<path class="st3" d="M21.9,10.3c-0.5,0-1-0.4-1.2-0.9c-0.1-0.3-0.1-0.7,0.1-1c0.2-0.3,0.4-0.5,0.7-0.6c1.4-0.4,2.8-0.7,4.2-0.6
c0.9,0,1.8,0.1,2.7,0.4c0.3,0.1,0.6,0.3,0.8,0.6c0.2,0.3,0.2,0.6,0.1,1c-0.1,0.3-0.2,0.5-0.5,0.7c-0.3,0.2-0.7,0.3-1.1,0.2
c-0.7-0.2-1.4-0.3-2.2-0.3c-1.1,0-2.3,0.1-3.3,0.5C22.2,10.3,22,10.3,21.9,10.3z"/>
<g>
<path class="st3" d="M13.8,40.8c0,0.3-0.1,0.5-0.2,0.7c-0.1,0.2-0.3,0.4-0.4,0.5c-0.2,0.1-0.4,0.2-0.7,0.3
c-0.2,0.1-0.5,0.1-0.8,0.1H8.6v-6.4h3.4c0.2,0,0.4,0,0.6,0.1c0.2,0.1,0.3,0.2,0.5,0.4s0.2,0.3,0.3,0.5c0.1,0.2,0.1,0.4,0.1,0.6
c0,0.3-0.1,0.6-0.2,0.9c-0.2,0.3-0.4,0.5-0.7,0.6c0.4,0.1,0.7,0.3,0.9,0.6C13.7,40.1,13.8,40.4,13.8,40.8z M9.9,37.1v1.6h1.7
c0.2,0,0.4-0.1,0.5-0.2c0.1-0.1,0.2-0.3,0.2-0.6c0-0.2-0.1-0.4-0.2-0.6c-0.1-0.1-0.3-0.2-0.5-0.2H9.9z M12.5,40.6
c0-0.1,0-0.2-0.1-0.3c0-0.1-0.1-0.2-0.2-0.3c-0.1-0.1-0.1-0.1-0.2-0.2s-0.2-0.1-0.3-0.1H9.9v1.7h1.8c0.1,0,0.2,0,0.3-0.1
c0.1,0,0.2-0.1,0.3-0.2c0.1-0.1,0.1-0.2,0.2-0.3S12.5,40.7,12.5,40.6z"/>
<path class="st3" d="M17.7,41.4c0.3,0,0.5-0.1,0.7-0.2s0.4-0.3,0.5-0.5c0.1-0.2,0.2-0.4,0.3-0.7s0.1-0.5,0.1-0.8v-3.3h1.2v3.3
c0,0.4-0.1,0.8-0.2,1.2c-0.1,0.4-0.3,0.7-0.5,1c-0.2,0.3-0.5,0.5-0.9,0.7c-0.4,0.2-0.8,0.3-1.3,0.3c-0.5,0-0.9-0.1-1.3-0.3
c-0.4-0.2-0.6-0.4-0.9-0.7c-0.2-0.3-0.4-0.6-0.5-1c-0.1-0.4-0.1-0.8-0.1-1.2v-3.3h1.2v3.3c0,0.3,0,0.5,0.1,0.8
c0.1,0.2,0.1,0.5,0.3,0.7s0.3,0.3,0.5,0.5S17.4,41.4,17.7,41.4z"/>
<path class="st3" d="M27.1,40.8c0,0.3-0.1,0.5-0.2,0.7c-0.1,0.2-0.3,0.4-0.4,0.5c-0.2,0.1-0.4,0.2-0.7,0.3
c-0.2,0.1-0.5,0.1-0.8,0.1h-3.1v-6.4h3.4c0.2,0,0.4,0,0.6,0.1c0.2,0.1,0.3,0.2,0.5,0.4s0.2,0.3,0.3,0.5c0.1,0.2,0.1,0.4,0.1,0.6
c0,0.3-0.1,0.6-0.2,0.9c-0.2,0.3-0.4,0.5-0.7,0.6c0.4,0.1,0.7,0.3,0.9,0.6C27,40.1,27.1,40.4,27.1,40.8z M23.2,37.1v1.6h1.7
c0.2,0,0.4-0.1,0.5-0.2c0.1-0.1,0.2-0.3,0.2-0.6c0-0.2-0.1-0.4-0.2-0.6c-0.1-0.1-0.3-0.2-0.5-0.2H23.2z M25.8,40.6
c0-0.1,0-0.2-0.1-0.3c0-0.1-0.1-0.2-0.2-0.3c-0.1-0.1-0.1-0.1-0.2-0.2s-0.2-0.1-0.3-0.1h-1.9v1.7H25c0.1,0,0.2,0,0.3-0.1
c0.1,0,0.2-0.1,0.3-0.2c0.1-0.1,0.1-0.2,0.2-0.3S25.8,40.7,25.8,40.6z"/>
<path class="st3" d="M33.4,40.8c0,0.3-0.1,0.5-0.2,0.7c-0.1,0.2-0.3,0.4-0.4,0.5c-0.2,0.1-0.4,0.2-0.7,0.3
c-0.2,0.1-0.5,0.1-0.8,0.1h-3.1v-6.4h3.4c0.2,0,0.4,0,0.6,0.1c0.2,0.1,0.3,0.2,0.5,0.4s0.2,0.3,0.3,0.5c0.1,0.2,0.1,0.4,0.1,0.6
c0,0.3-0.1,0.6-0.2,0.9c-0.2,0.3-0.4,0.5-0.7,0.6c0.4,0.1,0.7,0.3,0.9,0.6C33.3,40.1,33.4,40.4,33.4,40.8z M29.5,37.1v1.6h1.7
c0.2,0,0.4-0.1,0.5-0.2c0.1-0.1,0.2-0.3,0.2-0.6c0-0.2-0.1-0.4-0.2-0.6c-0.1-0.1-0.3-0.2-0.5-0.2H29.5z M32.2,40.6
c0-0.1,0-0.2-0.1-0.3c0-0.1-0.1-0.2-0.2-0.3c-0.1-0.1-0.1-0.1-0.2-0.2s-0.2-0.1-0.3-0.1h-1.9v1.7h1.8c0.1,0,0.2,0,0.3-0.1
c0.1,0,0.2-0.1,0.3-0.2c0.1-0.1,0.1-0.2,0.2-0.3S32.2,40.7,32.2,40.6z"/>
<path class="st3" d="M34.6,42.5v-6.4h1.2v5.3h3.3v1.1H34.6z"/>
<path class="st3" d="M44.6,41.4v1.1h-4.4v-6.4h4.4v1.1h-3.1v1.5h2.7v1h-2.7v1.7H44.6z"/>
</g>
<g>
<path class="st1" d="M24.3,69.6c-0.5,0.5-0.9,1-1.3,1.5c-0.1,0.1-0.1,0.3-0.1,0.4c0,1,0,2,0.1,3c0,0.4-0.2,0.7-0.7,0.8
c-0.4,0-0.8-0.2-0.8-0.7c-0.1-1.3-0.1-2.5-0.2-3.8c0-0.2,0.1-0.4,0.2-0.6c0.7-0.9,1.4-1.7,2-2.6c0.3-0.4,1-0.9,1.5-0.9
c0.2,0,0.6,0,0.8,0c0.5,0,0.8,0.1,1.1,0.6c0.5,0.7,0.9,1.4,1.4,2.1c0.4,0.7,0.8,1,1.7,1.2c0.6,0.1,1.3,0.3,1.9,0.5
c0.2,0,0.3,0.1,0.5,0.2c0.3,0.2,0.4,0.5,0.3,0.8c-0.1,0.3-0.3,0.4-0.6,0.4c-0.2,0-0.5,0-0.7-0.1c-0.8-0.2-1.6-0.4-2.4-0.6
c-0.5-0.1-0.8-0.4-1.2-0.7c-0.2-0.2-0.3-0.3-0.5-0.5c0,0.2,0,0.3,0,0.4c0,0.7,0,1.4-0.1,2.1c0,0.2-0.2,6.4-0.3,8.9
c0,0.3-0.1,0.6-0.2,0.9c-0.1,0.4-0.4,0.6-0.9,0.6c-0.4,0-0.8-0.3-0.9-0.7c-0.1-0.3-0.1-0.6-0.1-1c0-2.7-0.4-6.5-0.5-7
c0-0.5-0.1-0.6-0.1-1.2c-0.1-0.3-0.1-0.5-0.1-0.7C24.2,71.8,24.3,70.8,24.3,69.6z"/>
<path class="st1" d="M27.1,64.7c0,0.7-0.6,1.3-1.4,1.3l-0.3,0c-0.7,0-1.3-0.6-1.3-1.4l0-1.3c0-0.7,0.6-1.3,1.4-1.3l0.3,0
c0.7,0,1.3,0.6,1.3,1.4L27.1,64.7z"/>
</g>
<path class="st1" d="M22,81.5L22,81.5L22,81.5c-0.1,0-0.2,0-0.4-0.1c-2.2-0.7-4.2-2.1-5.7-3.9c-0.5-0.5-0.4-1.4,0.2-1.8
c0.2-0.2,0.5-0.3,0.9-0.3c0.4,0,0.7,0.2,1,0.5c0.6,0.7,1.3,1.4,2.1,1.9c0.8,0.5,1.6,0.9,2.5,1.2l0,0c0.3,0.1,0.6,0.3,0.7,0.6
c0.2,0.3,0.2,0.7,0.1,1c-0.1,0.3-0.3,0.5-0.5,0.7C22.5,81.5,22.2,81.6,22,81.5z"/>
<path class="st1" d="M29.5,81.3c-0.5,0-1-0.3-1.2-0.8c-0.1-0.3-0.1-0.7,0-1s0.4-0.6,0.7-0.7c0.8-0.3,1.5-0.7,2.1-1.2
c0.8-0.6,1.6-1.4,2.2-2.2l0,0c0.2-0.3,0.7-0.5,1.1-0.5c0.3,0,0.5,0.1,0.7,0.2c0.3,0.2,0.5,0.5,0.5,0.8c0.1,0.3,0,0.7-0.2,1
c-0.8,1.1-1.7,2-2.8,2.8c-0.8,0.6-1.7,1.1-2.7,1.5C29.9,81.3,29.7,81.3,29.5,81.3z"/>
<path class="st1" d="M14.9,74.7c-0.3,0-0.5-0.1-0.7-0.2c-0.2-0.2-0.4-0.4-0.5-0.6c-0.2-0.7,0.1-1.4,0.8-1.7c0.2-0.1,0.3-0.1,0.5-0.1
c0.3,0,0.5,0.1,0.7,0.2c0.2,0.2,0.4,0.4,0.5,0.6c0.1,0.3,0.1,0.7,0,1c-0.1,0.3-0.4,0.5-0.7,0.7C15.2,74.7,15,74.7,14.9,74.7z"/>
<path class="st1" d="M36.1,74L36.1,74L36.1,74c-0.1,0-0.2,0-0.3-0.1c-0.3-0.1-0.6-0.3-0.8-0.6c-0.2-0.3-0.2-0.6-0.1-1
c0.2-0.6,0.7-0.9,1.3-0.9c0.1,0,0.2,0,0.3,0.1c0.3,0.1,0.6,0.3,0.8,0.6c0.2,0.3,0.2,0.6,0.1,1C37.2,73.6,36.7,74,36.1,74z"/>
<path class="st1" d="M14.2,71c-0.3,0-0.7-0.1-0.9-0.4c-0.2-0.2-0.4-0.6-0.4-0.9c0-0.1,0-0.2,0-0.4c0.1-2.2,0.7-4.4,1.9-6.3
c0.1-0.2,0.3-0.4,0.5-0.5c0.2-0.1,0.4-0.2,0.6-0.1c0.2,0,0.5,0.1,0.6,0.2c0.6,0.4,0.8,1.2,0.4,1.8c-0.9,1.5-1.5,3.2-1.5,5
c0,0.1,0,0.2,0,0.3c0,0.3-0.1,0.7-0.4,0.9C14.9,70.8,14.6,71,14.2,71C14.2,71,14.2,71,14.2,71z"/>
<path class="st1" d="M36.5,70.2c-0.3,0-0.6-0.1-0.9-0.4c-0.2-0.2-0.4-0.5-0.4-0.8c-0.1-0.9-0.2-1.8-0.6-2.7
c-0.3-0.9-0.8-1.7-1.3-2.5l0,0c-0.2-0.3-0.3-0.6-0.2-1c0.1-0.3,0.2-0.6,0.5-0.8c0.2-0.2,0.5-0.3,0.8-0.2c0.4,0,0.8,0.2,1,0.5
c0,0,0,0,0,0l0,0c1.4,1.9,2.2,4.2,2.4,6.5c0,0.3-0.1,0.7-0.3,0.9c-0.2,0.3-0.5,0.4-0.9,0.4C36.6,70.2,36.6,70.2,36.5,70.2z"/>
<path class="st1" d="M18.5,62.1c-0.4,0-0.7-0.2-1-0.5c-0.2-0.3-0.3-0.6-0.3-1c0-0.3,0.2-0.6,0.5-0.9c0.2-0.2,0.5-0.3,0.8-0.3
c0.4,0,0.7,0.2,1,0.5c0.2,0.3,0.3,0.6,0.3,1c0,0.3-0.2,0.6-0.5,0.9C19.1,62,18.8,62.1,18.5,62.1z"/>
<path class="st1" d="M31.6,61.7c-0.2,0-0.5-0.1-0.7-0.2c-0.3-0.2-0.5-0.5-0.5-0.8s0-0.7,0.2-1c0.2-0.4,0.7-0.6,1.1-0.6
c0.2,0,0.5,0.1,0.7,0.2c0.6,0.4,0.7,1.2,0.3,1.8C32.5,61.5,32.1,61.7,31.6,61.7z"/>
<path class="st1" d="M21.9,60.3c-0.5,0-1-0.4-1.2-0.9c-0.1-0.3-0.1-0.7,0.1-1c0.2-0.3,0.4-0.5,0.7-0.6c1.4-0.4,2.8-0.7,4.2-0.6
c0.9,0,1.8,0.1,2.7,0.4c0.3,0.1,0.6,0.3,0.8,0.6c0.2,0.3,0.2,0.6,0.1,1c-0.1,0.3-0.2,0.5-0.5,0.7c-0.3,0.2-0.7,0.3-1.1,0.2
c-0.7-0.2-1.4-0.3-2.2-0.3c-1.1,0-2.3,0.1-3.3,0.5C22.2,60.3,22,60.3,21.9,60.3z"/>
<g>
<path class="st1" d="M13.8,90.8c0,0.3-0.1,0.5-0.2,0.7c-0.1,0.2-0.3,0.4-0.4,0.5c-0.2,0.1-0.4,0.2-0.7,0.3
c-0.2,0.1-0.5,0.1-0.8,0.1H8.6v-6.4h3.4c0.2,0,0.4,0,0.6,0.1c0.2,0.1,0.3,0.2,0.5,0.4s0.2,0.3,0.3,0.5c0.1,0.2,0.1,0.4,0.1,0.6
c0,0.3-0.1,0.6-0.2,0.9c-0.2,0.3-0.4,0.5-0.7,0.6c0.4,0.1,0.7,0.3,0.9,0.6C13.7,90.1,13.8,90.4,13.8,90.8z M9.9,87.1v1.6h1.7
c0.2,0,0.4-0.1,0.5-0.2c0.1-0.1,0.2-0.3,0.2-0.6c0-0.2-0.1-0.4-0.2-0.6c-0.1-0.1-0.3-0.2-0.5-0.2H9.9z M12.5,90.6
c0-0.1,0-0.2-0.1-0.3c0-0.1-0.1-0.2-0.2-0.3c-0.1-0.1-0.1-0.1-0.2-0.2s-0.2-0.1-0.3-0.1H9.9v1.7h1.8c0.1,0,0.2,0,0.3-0.1
c0.1,0,0.2-0.1,0.3-0.2c0.1-0.1,0.1-0.2,0.2-0.3S12.5,90.7,12.5,90.6z"/>
<path class="st1" d="M17.7,91.4c0.3,0,0.5-0.1,0.7-0.2s0.4-0.3,0.5-0.5c0.1-0.2,0.2-0.4,0.3-0.7s0.1-0.5,0.1-0.8v-3.3h1.2v3.3
c0,0.4-0.1,0.8-0.2,1.2c-0.1,0.4-0.3,0.7-0.5,1c-0.2,0.3-0.5,0.5-0.9,0.7c-0.4,0.2-0.8,0.3-1.3,0.3c-0.5,0-0.9-0.1-1.3-0.3
c-0.4-0.2-0.6-0.4-0.9-0.7c-0.2-0.3-0.4-0.6-0.5-1c-0.1-0.4-0.1-0.8-0.1-1.2v-3.3h1.2v3.3c0,0.3,0,0.5,0.1,0.8
c0.1,0.2,0.1,0.5,0.3,0.7s0.3,0.3,0.5,0.5S17.4,91.4,17.7,91.4z"/>
<path class="st1" d="M27.1,90.8c0,0.3-0.1,0.5-0.2,0.7c-0.1,0.2-0.3,0.4-0.4,0.5c-0.2,0.1-0.4,0.2-0.7,0.3
c-0.2,0.1-0.5,0.1-0.8,0.1h-3.1v-6.4h3.4c0.2,0,0.4,0,0.6,0.1c0.2,0.1,0.3,0.2,0.5,0.4s0.2,0.3,0.3,0.5c0.1,0.2,0.1,0.4,0.1,0.6
c0,0.3-0.1,0.6-0.2,0.9c-0.2,0.3-0.4,0.5-0.7,0.6c0.4,0.1,0.7,0.3,0.9,0.6C27,90.1,27.1,90.4,27.1,90.8z M23.2,87.1v1.6h1.7
c0.2,0,0.4-0.1,0.5-0.2c0.1-0.1,0.2-0.3,0.2-0.6c0-0.2-0.1-0.4-0.2-0.6c-0.1-0.1-0.3-0.2-0.5-0.2H23.2z M25.8,90.6
c0-0.1,0-0.2-0.1-0.3c0-0.1-0.1-0.2-0.2-0.3c-0.1-0.1-0.1-0.1-0.2-0.2s-0.2-0.1-0.3-0.1h-1.9v1.7H25c0.1,0,0.2,0,0.3-0.1
c0.1,0,0.2-0.1,0.3-0.2c0.1-0.1,0.1-0.2,0.2-0.3S25.8,90.7,25.8,90.6z"/>
<path class="st1" d="M33.4,90.8c0,0.3-0.1,0.5-0.2,0.7c-0.1,0.2-0.3,0.4-0.4,0.5c-0.2,0.1-0.4,0.2-0.7,0.3
c-0.2,0.1-0.5,0.1-0.8,0.1h-3.1v-6.4h3.4c0.2,0,0.4,0,0.6,0.1c0.2,0.1,0.3,0.2,0.5,0.4s0.2,0.3,0.3,0.5c0.1,0.2,0.1,0.4,0.1,0.6
c0,0.3-0.1,0.6-0.2,0.9c-0.2,0.3-0.4,0.5-0.7,0.6c0.4,0.1,0.7,0.3,0.9,0.6C33.3,90.1,33.4,90.4,33.4,90.8z M29.5,87.1v1.6h1.7
c0.2,0,0.4-0.1,0.5-0.2c0.1-0.1,0.2-0.3,0.2-0.6c0-0.2-0.1-0.4-0.2-0.6c-0.1-0.1-0.3-0.2-0.5-0.2H29.5z M32.2,90.6
c0-0.1,0-0.2-0.1-0.3c0-0.1-0.1-0.2-0.2-0.3c-0.1-0.1-0.1-0.1-0.2-0.2s-0.2-0.1-0.3-0.1h-1.9v1.7h1.8c0.1,0,0.2,0,0.3-0.1
c0.1,0,0.2-0.1,0.3-0.2c0.1-0.1,0.1-0.2,0.2-0.3S32.2,90.7,32.2,90.6z"/>
<path class="st1" d="M34.6,92.5v-6.4h1.2v5.3h3.3v1.1H34.6z"/>
<path class="st1" d="M44.6,91.4v1.1h-4.4v-6.4h4.4v1.1h-3.1v1.5h2.7v1h-2.7v1.7H44.6z"/>
</g>
<g>
<path class="st1" d="M24.3,119.6c-0.5,0.5-0.9,1-1.3,1.5c-0.1,0.1-0.1,0.3-0.1,0.4c0,1,0,2,0.1,3c0,0.4-0.2,0.7-0.7,0.8
c-0.4,0-0.8-0.2-0.8-0.7c-0.1-1.3-0.1-2.5-0.2-3.8c0-0.2,0.1-0.4,0.2-0.6c0.7-0.9,1.4-1.7,2-2.6c0.3-0.4,1-0.9,1.5-0.9
c0.2,0,0.6,0,0.8,0c0.5,0,0.8,0.1,1.1,0.6c0.5,0.7,0.9,1.4,1.4,2.1c0.4,0.7,0.8,1,1.7,1.2c0.6,0.1,1.3,0.3,1.9,0.5
c0.2,0,0.3,0.1,0.5,0.2c0.3,0.2,0.4,0.5,0.3,0.8c-0.1,0.3-0.3,0.4-0.6,0.4c-0.2,0-0.5,0-0.7-0.1c-0.8-0.2-1.6-0.4-2.4-0.6
c-0.5-0.1-0.8-0.4-1.2-0.7c-0.2-0.2-0.3-0.3-0.5-0.5c0,0.2,0,0.3,0,0.4c0,0.7,0,1.4-0.1,2.1c0,0.2-0.2,6.4-0.3,8.9
c0,0.3-0.1,0.6-0.2,0.9c-0.1,0.4-0.4,0.6-0.9,0.6c-0.4,0-0.8-0.3-0.9-0.7c-0.1-0.3-0.1-0.6-0.1-1c0-2.7-0.4-6.5-0.5-7
c0-0.5-0.1-0.6-0.1-1.2c-0.1-0.3-0.1-0.5-0.1-0.7C24.2,121.8,24.3,120.8,24.3,119.6z"/>
<path class="st1" d="M27.1,114.7c0,0.7-0.6,1.3-1.4,1.3l-0.3,0c-0.7,0-1.3-0.6-1.3-1.4l0-1.3c0-0.7,0.6-1.3,1.4-1.3l0.3,0
c0.7,0,1.3,0.6,1.3,1.4L27.1,114.7z"/>
</g>
<path class="st1" d="M22,131.5L22,131.5L22,131.5c-0.1,0-0.2,0-0.4-0.1c-2.2-0.7-4.2-2.1-5.7-3.9c-0.5-0.5-0.4-1.4,0.2-1.8
c0.2-0.2,0.5-0.3,0.9-0.3c0.4,0,0.7,0.2,1,0.5c0.6,0.7,1.3,1.4,2.1,1.9c0.8,0.5,1.6,0.9,2.5,1.2l0,0c0.3,0.1,0.6,0.3,0.7,0.6
c0.2,0.3,0.2,0.7,0.1,1c-0.1,0.3-0.3,0.5-0.5,0.7C22.5,131.5,22.2,131.6,22,131.5z"/>
<path class="st1" d="M29.5,131.3c-0.5,0-1-0.3-1.2-0.8c-0.1-0.3-0.1-0.7,0-1c0.1-0.3,0.4-0.6,0.7-0.7c0.8-0.3,1.5-0.7,2.1-1.2
c0.8-0.6,1.6-1.4,2.2-2.2l0,0c0.2-0.3,0.7-0.5,1.1-0.5c0.3,0,0.5,0.1,0.7,0.2c0.3,0.2,0.5,0.5,0.5,0.8c0.1,0.3,0,0.7-0.2,1
c-0.8,1.1-1.7,2-2.8,2.8c-0.8,0.6-1.7,1.1-2.7,1.5C29.9,131.3,29.7,131.3,29.5,131.3z"/>
<path class="st1" d="M14.9,124.7c-0.3,0-0.5-0.1-0.7-0.2c-0.2-0.2-0.4-0.4-0.5-0.6c-0.2-0.7,0.1-1.4,0.8-1.7
c0.2-0.1,0.3-0.1,0.5-0.1c0.3,0,0.5,0.1,0.7,0.2c0.2,0.2,0.4,0.4,0.5,0.6c0.1,0.3,0.1,0.7,0,1c-0.1,0.3-0.4,0.5-0.7,0.7
C15.2,124.7,15,124.7,14.9,124.7z"/>
<path class="st1" d="M36.1,124L36.1,124L36.1,124c-0.1,0-0.2,0-0.3-0.1c-0.3-0.1-0.6-0.3-0.8-0.6c-0.2-0.3-0.2-0.6-0.1-1
c0.2-0.6,0.7-0.9,1.3-0.9c0.1,0,0.2,0,0.3,0.1c0.3,0.1,0.6,0.3,0.8,0.6c0.2,0.3,0.2,0.6,0.1,1C37.2,123.6,36.7,124,36.1,124z"/>
<path class="st1" d="M14.2,121c-0.3,0-0.7-0.1-0.9-0.4c-0.2-0.2-0.4-0.6-0.4-0.9c0-0.1,0-0.2,0-0.4c0.1-2.2,0.7-4.4,1.9-6.3
c0.1-0.2,0.3-0.4,0.5-0.5c0.2-0.1,0.4-0.2,0.6-0.1c0.2,0,0.5,0.1,0.6,0.2c0.6,0.4,0.8,1.2,0.4,1.8c-0.9,1.5-1.5,3.2-1.5,5
c0,0.1,0,0.2,0,0.3c0,0.3-0.1,0.7-0.4,0.9C14.9,120.8,14.6,121,14.2,121C14.2,121,14.2,121,14.2,121z"/>
<path class="st1" d="M36.5,120.2c-0.3,0-0.6-0.1-0.9-0.4c-0.2-0.2-0.4-0.5-0.4-0.8c-0.1-0.9-0.2-1.8-0.6-2.7
c-0.3-0.9-0.8-1.7-1.3-2.5l0,0c-0.2-0.3-0.3-0.6-0.2-1c0.1-0.3,0.2-0.6,0.5-0.8c0.2-0.2,0.5-0.3,0.8-0.2c0.4,0,0.8,0.2,1,0.5
c0,0,0,0,0,0l0,0c1.4,1.9,2.2,4.2,2.4,6.5c0,0.3-0.1,0.7-0.3,0.9c-0.2,0.3-0.5,0.4-0.9,0.4C36.6,120.2,36.6,120.2,36.5,120.2z"/>
<path class="st1" d="M18.5,112.1c-0.4,0-0.7-0.2-1-0.5c-0.2-0.3-0.3-0.6-0.3-1c0-0.3,0.2-0.6,0.5-0.9c0.2-0.2,0.5-0.3,0.8-0.3
c0.4,0,0.7,0.2,1,0.5c0.2,0.3,0.3,0.6,0.3,1c0,0.3-0.2,0.6-0.5,0.9C19.1,112,18.8,112.1,18.5,112.1z"/>
<path class="st1" d="M31.6,111.7c-0.2,0-0.5-0.1-0.7-0.2c-0.3-0.2-0.5-0.5-0.5-0.8s0-0.7,0.2-1c0.2-0.4,0.7-0.6,1.1-0.6
c0.2,0,0.5,0.1,0.7,0.2c0.6,0.4,0.7,1.2,0.3,1.8C32.5,111.5,32.1,111.7,31.6,111.7z"/>
<path class="st1" d="M21.9,110.3c-0.5,0-1-0.4-1.2-0.9c-0.1-0.3-0.1-0.7,0.1-1c0.2-0.3,0.4-0.5,0.7-0.6c1.4-0.4,2.8-0.7,4.2-0.6
c0.9,0,1.8,0.1,2.7,0.4c0.3,0.1,0.6,0.3,0.8,0.6c0.2,0.3,0.2,0.6,0.1,1c-0.1,0.3-0.2,0.5-0.5,0.7c-0.3,0.2-0.7,0.3-1.1,0.2
c-0.7-0.2-1.4-0.3-2.2-0.3c-1.1,0-2.3,0.1-3.3,0.5C22.2,110.3,22,110.3,21.9,110.3z"/>
<g>
<path class="st1" d="M13.8,140.8c0,0.3-0.1,0.5-0.2,0.7c-0.1,0.2-0.3,0.4-0.4,0.5c-0.2,0.1-0.4,0.2-0.7,0.3
c-0.2,0.1-0.5,0.1-0.8,0.1H8.6v-6.4h3.4c0.2,0,0.4,0,0.6,0.1c0.2,0.1,0.3,0.2,0.5,0.4s0.2,0.3,0.3,0.5c0.1,0.2,0.1,0.4,0.1,0.6
c0,0.3-0.1,0.6-0.2,0.9c-0.2,0.3-0.4,0.5-0.7,0.6c0.4,0.1,0.7,0.3,0.9,0.6C13.7,140.1,13.8,140.4,13.8,140.8z M9.9,137.1v1.6h1.7
c0.2,0,0.4-0.1,0.5-0.2c0.1-0.1,0.2-0.3,0.2-0.6c0-0.2-0.1-0.4-0.2-0.6c-0.1-0.1-0.3-0.2-0.5-0.2H9.9z M12.5,140.6
c0-0.1,0-0.2-0.1-0.3c0-0.1-0.1-0.2-0.2-0.3c-0.1-0.1-0.1-0.1-0.2-0.2s-0.2-0.1-0.3-0.1H9.9v1.7h1.8c0.1,0,0.2,0,0.3-0.1
c0.1,0,0.2-0.1,0.3-0.2c0.1-0.1,0.1-0.2,0.2-0.3S12.5,140.7,12.5,140.6z"/>
<path class="st1" d="M17.7,141.4c0.3,0,0.5-0.1,0.7-0.2s0.4-0.3,0.5-0.5c0.1-0.2,0.2-0.4,0.3-0.7s0.1-0.5,0.1-0.8v-3.3h1.2v3.3
c0,0.4-0.1,0.8-0.2,1.2c-0.1,0.4-0.3,0.7-0.5,1c-0.2,0.3-0.5,0.5-0.9,0.7c-0.4,0.2-0.8,0.3-1.3,0.3c-0.5,0-0.9-0.1-1.3-0.3
c-0.4-0.2-0.6-0.4-0.9-0.7c-0.2-0.3-0.4-0.6-0.5-1c-0.1-0.4-0.1-0.8-0.1-1.2v-3.3h1.2v3.3c0,0.3,0,0.5,0.1,0.8
c0.1,0.2,0.1,0.5,0.3,0.7s0.3,0.3,0.5,0.5S17.4,141.4,17.7,141.4z"/>
<path class="st1" d="M27.1,140.8c0,0.3-0.1,0.5-0.2,0.7c-0.1,0.2-0.3,0.4-0.4,0.5c-0.2,0.1-0.4,0.2-0.7,0.3
c-0.2,0.1-0.5,0.1-0.8,0.1h-3.1v-6.4h3.4c0.2,0,0.4,0,0.6,0.1c0.2,0.1,0.3,0.2,0.5,0.4s0.2,0.3,0.3,0.5c0.1,0.2,0.1,0.4,0.1,0.6
c0,0.3-0.1,0.6-0.2,0.9c-0.2,0.3-0.4,0.5-0.7,0.6c0.4,0.1,0.7,0.3,0.9,0.6C27,140.1,27.1,140.4,27.1,140.8z M23.2,137.1v1.6h1.7
c0.2,0,0.4-0.1,0.5-0.2c0.1-0.1,0.2-0.3,0.2-0.6c0-0.2-0.1-0.4-0.2-0.6c-0.1-0.1-0.3-0.2-0.5-0.2H23.2z M25.8,140.6
c0-0.1,0-0.2-0.1-0.3c0-0.1-0.1-0.2-0.2-0.3c-0.1-0.1-0.1-0.1-0.2-0.2s-0.2-0.1-0.3-0.1h-1.9v1.7H25c0.1,0,0.2,0,0.3-0.1
c0.1,0,0.2-0.1,0.3-0.2c0.1-0.1,0.1-0.2,0.2-0.3S25.8,140.7,25.8,140.6z"/>
<path class="st1" d="M33.4,140.8c0,0.3-0.1,0.5-0.2,0.7c-0.1,0.2-0.3,0.4-0.4,0.5c-0.2,0.1-0.4,0.2-0.7,0.3
c-0.2,0.1-0.5,0.1-0.8,0.1h-3.1v-6.4h3.4c0.2,0,0.4,0,0.6,0.1c0.2,0.1,0.3,0.2,0.5,0.4s0.2,0.3,0.3,0.5c0.1,0.2,0.1,0.4,0.1,0.6
c0,0.3-0.1,0.6-0.2,0.9c-0.2,0.3-0.4,0.5-0.7,0.6c0.4,0.1,0.7,0.3,0.9,0.6C33.3,140.1,33.4,140.4,33.4,140.8z M29.5,137.1v1.6h1.7
c0.2,0,0.4-0.1,0.5-0.2c0.1-0.1,0.2-0.3,0.2-0.6c0-0.2-0.1-0.4-0.2-0.6c-0.1-0.1-0.3-0.2-0.5-0.2H29.5z M32.2,140.6
c0-0.1,0-0.2-0.1-0.3c0-0.1-0.1-0.2-0.2-0.3c-0.1-0.1-0.1-0.1-0.2-0.2s-0.2-0.1-0.3-0.1h-1.9v1.7h1.8c0.1,0,0.2,0,0.3-0.1
c0.1,0,0.2-0.1,0.3-0.2c0.1-0.1,0.1-0.2,0.2-0.3S32.2,140.7,32.2,140.6z"/>
<path class="st1" d="M34.6,142.5v-6.4h1.2v5.3h3.3v1.1H34.6z"/>
<path class="st1" d="M44.6,141.4v1.1h-4.4v-6.4h4.4v1.1h-3.1v1.5h2.7v1h-2.7v1.7H44.6z"/>
</g>
<g>
<path class="st1" d="M24.3,169.6c-0.5,0.5-0.9,1-1.3,1.5c-0.1,0.1-0.1,0.3-0.1,0.4c0,1,0,2,0.1,3c0,0.4-0.2,0.7-0.7,0.8
c-0.4,0-0.8-0.2-0.8-0.7c-0.1-1.3-0.1-2.5-0.2-3.8c0-0.2,0.1-0.4,0.2-0.6c0.7-0.9,1.4-1.7,2-2.6c0.3-0.4,1-0.9,1.5-0.9
c0.2,0,0.6,0,0.8,0c0.5,0,0.8,0.1,1.1,0.6c0.5,0.7,0.9,1.4,1.4,2.1c0.4,0.7,0.8,1,1.7,1.2c0.6,0.1,1.3,0.3,1.9,0.5
c0.2,0,0.3,0.1,0.5,0.2c0.3,0.2,0.4,0.5,0.3,0.8c-0.1,0.3-0.3,0.4-0.6,0.4c-0.2,0-0.5,0-0.7-0.1c-0.8-0.2-1.6-0.4-2.4-0.6
c-0.5-0.1-0.8-0.4-1.2-0.7c-0.2-0.2-0.3-0.3-0.5-0.5c0,0.2,0,0.3,0,0.4c0,0.7,0,1.4-0.1,2.1c0,0.2-0.2,6.4-0.3,8.9
c0,0.3-0.1,0.6-0.2,0.9c-0.1,0.4-0.4,0.6-0.9,0.6c-0.4,0-0.8-0.3-0.9-0.7c-0.1-0.3-0.1-0.6-0.1-1c0-2.7-0.4-6.5-0.5-7
c0-0.5-0.1-0.6-0.1-1.2c-0.1-0.3-0.1-0.5-0.1-0.7C24.2,171.8,24.3,170.8,24.3,169.6z"/>
<path class="st1" d="M27.1,164.7c0,0.7-0.6,1.3-1.4,1.3l-0.3,0c-0.7,0-1.3-0.6-1.3-1.4l0-1.3c0-0.7,0.6-1.3,1.4-1.3l0.3,0
c0.7,0,1.3,0.6,1.3,1.4L27.1,164.7z"/>
</g>
<path class="st1" d="M22,181.5L22,181.5L22,181.5c-0.1,0-0.2,0-0.4-0.1c-2.2-0.7-4.2-2.1-5.7-3.9c-0.5-0.5-0.4-1.4,0.2-1.8
c0.2-0.2,0.5-0.3,0.9-0.3c0.4,0,0.7,0.2,1,0.5c0.6,0.7,1.3,1.4,2.1,1.9c0.8,0.5,1.6,0.9,2.5,1.2l0,0c0.3,0.1,0.6,0.3,0.7,0.6
c0.2,0.3,0.2,0.7,0.1,1c-0.1,0.3-0.3,0.5-0.5,0.7C22.5,181.5,22.2,181.6,22,181.5z"/>
<path class="st1" d="M29.5,181.3c-0.5,0-1-0.3-1.2-0.8c-0.1-0.3-0.1-0.7,0-1c0.1-0.3,0.4-0.6,0.7-0.7c0.8-0.3,1.5-0.7,2.1-1.2
c0.8-0.6,1.6-1.4,2.2-2.2l0,0c0.2-0.3,0.7-0.5,1.1-0.5c0.3,0,0.5,0.1,0.7,0.2c0.3,0.2,0.5,0.5,0.5,0.8c0.1,0.3,0,0.7-0.2,1
c-0.8,1.1-1.7,2-2.8,2.8c-0.8,0.6-1.7,1.1-2.7,1.5C29.9,181.3,29.7,181.3,29.5,181.3z"/>
<path class="st1" d="M14.9,174.7c-0.3,0-0.5-0.1-0.7-0.2c-0.2-0.2-0.4-0.4-0.5-0.6c-0.2-0.7,0.1-1.4,0.8-1.7
c0.2-0.1,0.3-0.1,0.5-0.1c0.3,0,0.5,0.1,0.7,0.2c0.2,0.2,0.4,0.4,0.5,0.6c0.1,0.3,0.1,0.7,0,1c-0.1,0.3-0.4,0.5-0.7,0.7
C15.2,174.7,15,174.7,14.9,174.7z"/>
<path class="st1" d="M36.1,174L36.1,174L36.1,174c-0.1,0-0.2,0-0.3-0.1c-0.3-0.1-0.6-0.3-0.8-0.6c-0.2-0.3-0.2-0.6-0.1-1
c0.2-0.6,0.7-0.9,1.3-0.9c0.1,0,0.2,0,0.3,0.1c0.3,0.1,0.6,0.3,0.8,0.6c0.2,0.3,0.2,0.6,0.1,1C37.2,173.6,36.7,174,36.1,174z"/>
<path class="st1" d="M14.2,171c-0.3,0-0.7-0.1-0.9-0.4c-0.2-0.2-0.4-0.6-0.4-0.9c0-0.1,0-0.2,0-0.4c0.1-2.2,0.7-4.4,1.9-6.3
c0.1-0.2,0.3-0.4,0.5-0.5c0.2-0.1,0.4-0.2,0.6-0.1c0.2,0,0.5,0.1,0.6,0.2c0.6,0.4,0.8,1.2,0.4,1.8c-0.9,1.5-1.5,3.2-1.5,5
c0,0.1,0,0.2,0,0.3c0,0.3-0.1,0.7-0.4,0.9C14.9,170.8,14.6,171,14.2,171C14.2,171,14.2,171,14.2,171z"/>
<path class="st1" d="M36.5,170.2c-0.3,0-0.6-0.1-0.9-0.4c-0.2-0.2-0.4-0.5-0.4-0.8c-0.1-0.9-0.2-1.8-0.6-2.7
c-0.3-0.9-0.8-1.7-1.3-2.5l0,0c-0.2-0.3-0.3-0.6-0.2-1c0.1-0.3,0.2-0.6,0.5-0.8c0.2-0.2,0.5-0.3,0.8-0.2c0.4,0,0.8,0.2,1,0.5
c0,0,0,0,0,0l0,0c1.4,1.9,2.2,4.2,2.4,6.5c0,0.3-0.1,0.7-0.3,0.9c-0.2,0.3-0.5,0.4-0.9,0.4C36.6,170.2,36.6,170.2,36.5,170.2z"/>
<path class="st1" d="M18.5,162.1c-0.4,0-0.7-0.2-1-0.5c-0.2-0.3-0.3-0.6-0.3-1c0-0.3,0.2-0.6,0.5-0.9c0.2-0.2,0.5-0.3,0.8-0.3
c0.4,0,0.7,0.2,1,0.5c0.2,0.3,0.3,0.6,0.3,1c0,0.3-0.2,0.6-0.5,0.9C19.1,162,18.8,162.1,18.5,162.1z"/>
<path class="st1" d="M31.6,161.7c-0.2,0-0.5-0.1-0.7-0.2c-0.3-0.2-0.5-0.5-0.5-0.8s0-0.7,0.2-1c0.2-0.4,0.7-0.6,1.1-0.6
c0.2,0,0.5,0.1,0.7,0.2c0.6,0.4,0.7,1.2,0.3,1.8C32.5,161.5,32.1,161.7,31.6,161.7z"/>
<path class="st1" d="M21.9,160.3c-0.5,0-1-0.4-1.2-0.9c-0.1-0.3-0.1-0.7,0.1-1c0.2-0.3,0.4-0.5,0.7-0.6c1.4-0.4,2.8-0.7,4.2-0.6
c0.9,0,1.8,0.1,2.7,0.4c0.3,0.1,0.6,0.3,0.8,0.6c0.2,0.3,0.2,0.6,0.1,1c-0.1,0.3-0.2,0.5-0.5,0.7c-0.3,0.2-0.7,0.3-1.1,0.2
c-0.7-0.2-1.4-0.3-2.2-0.3c-1.1,0-2.3,0.1-3.3,0.5C22.2,160.3,22,160.3,21.9,160.3z"/>
<g>
<path class="st1" d="M13.8,190.8c0,0.3-0.1,0.5-0.2,0.7c-0.1,0.2-0.3,0.4-0.4,0.5c-0.2,0.1-0.4,0.2-0.7,0.3
c-0.2,0.1-0.5,0.1-0.8,0.1H8.6v-6.4h3.4c0.2,0,0.4,0,0.6,0.1c0.2,0.1,0.3,0.2,0.5,0.4s0.2,0.3,0.3,0.5c0.1,0.2,0.1,0.4,0.1,0.6
c0,0.3-0.1,0.6-0.2,0.9c-0.2,0.3-0.4,0.5-0.7,0.6c0.4,0.1,0.7,0.3,0.9,0.6C13.7,190.1,13.8,190.4,13.8,190.8z M9.9,187.1v1.6h1.7
c0.2,0,0.4-0.1,0.5-0.2c0.1-0.1,0.2-0.3,0.2-0.6c0-0.2-0.1-0.4-0.2-0.6c-0.1-0.1-0.3-0.2-0.5-0.2H9.9z M12.5,190.6
c0-0.1,0-0.2-0.1-0.3c0-0.1-0.1-0.2-0.2-0.3c-0.1-0.1-0.1-0.1-0.2-0.2s-0.2-0.1-0.3-0.1H9.9v1.7h1.8c0.1,0,0.2,0,0.3-0.1
c0.1,0,0.2-0.1,0.3-0.2c0.1-0.1,0.1-0.2,0.2-0.3S12.5,190.7,12.5,190.6z"/>
<path class="st1" d="M17.7,191.4c0.3,0,0.5-0.1,0.7-0.2s0.4-0.3,0.5-0.5c0.1-0.2,0.2-0.4,0.3-0.7s0.1-0.5,0.1-0.8v-3.3h1.2v3.3
c0,0.4-0.1,0.8-0.2,1.2c-0.1,0.4-0.3,0.7-0.5,1c-0.2,0.3-0.5,0.5-0.9,0.7c-0.4,0.2-0.8,0.3-1.3,0.3c-0.5,0-0.9-0.1-1.3-0.3
c-0.4-0.2-0.6-0.4-0.9-0.7c-0.2-0.3-0.4-0.6-0.5-1c-0.1-0.4-0.1-0.8-0.1-1.2v-3.3h1.2v3.3c0,0.3,0,0.5,0.1,0.8
c0.1,0.2,0.1,0.5,0.3,0.7s0.3,0.3,0.5,0.5S17.4,191.4,17.7,191.4z"/>
<path class="st1" d="M27.1,190.8c0,0.3-0.1,0.5-0.2,0.7c-0.1,0.2-0.3,0.4-0.4,0.5c-0.2,0.1-0.4,0.2-0.7,0.3
c-0.2,0.1-0.5,0.1-0.8,0.1h-3.1v-6.4h3.4c0.2,0,0.4,0,0.6,0.1c0.2,0.1,0.3,0.2,0.5,0.4s0.2,0.3,0.3,0.5c0.1,0.2,0.1,0.4,0.1,0.6
c0,0.3-0.1,0.6-0.2,0.9c-0.2,0.3-0.4,0.5-0.7,0.6c0.4,0.1,0.7,0.3,0.9,0.6C27,190.1,27.1,190.4,27.1,190.8z M23.2,187.1v1.6h1.7
c0.2,0,0.4-0.1,0.5-0.2c0.1-0.1,0.2-0.3,0.2-0.6c0-0.2-0.1-0.4-0.2-0.6c-0.1-0.1-0.3-0.2-0.5-0.2H23.2z M25.8,190.6
c0-0.1,0-0.2-0.1-0.3c0-0.1-0.1-0.2-0.2-0.3c-0.1-0.1-0.1-0.1-0.2-0.2s-0.2-0.1-0.3-0.1h-1.9v1.7H25c0.1,0,0.2,0,0.3-0.1
c0.1,0,0.2-0.1,0.3-0.2c0.1-0.1,0.1-0.2,0.2-0.3S25.8,190.7,25.8,190.6z"/>
<path class="st1" d="M33.4,190.8c0,0.3-0.1,0.5-0.2,0.7c-0.1,0.2-0.3,0.4-0.4,0.5c-0.2,0.1-0.4,0.2-0.7,0.3
c-0.2,0.1-0.5,0.1-0.8,0.1h-3.1v-6.4h3.4c0.2,0,0.4,0,0.6,0.1c0.2,0.1,0.3,0.2,0.5,0.4s0.2,0.3,0.3,0.5c0.1,0.2,0.1,0.4,0.1,0.6
c0,0.3-0.1,0.6-0.2,0.9c-0.2,0.3-0.4,0.5-0.7,0.6c0.4,0.1,0.7,0.3,0.9,0.6C33.3,190.1,33.4,190.4,33.4,190.8z M29.5,187.1v1.6h1.7
c0.2,0,0.4-0.1,0.5-0.2c0.1-0.1,0.2-0.3,0.2-0.6c0-0.2-0.1-0.4-0.2-0.6c-0.1-0.1-0.3-0.2-0.5-0.2H29.5z M32.2,190.6
c0-0.1,0-0.2-0.1-0.3c0-0.1-0.1-0.2-0.2-0.3c-0.1-0.1-0.1-0.1-0.2-0.2s-0.2-0.1-0.3-0.1h-1.9v1.7h1.8c0.1,0,0.2,0,0.3-0.1
c0.1,0,0.2-0.1,0.3-0.2c0.1-0.1,0.1-0.2,0.2-0.3S32.2,190.7,32.2,190.6z"/>
<path class="st1" d="M34.6,192.5v-6.4h1.2v5.3h3.3v1.1H34.6z"/>
<path class="st1" d="M44.6,191.4v1.1h-4.4v-6.4h4.4v1.1h-3.1v1.5h2.7v1h-2.7v1.7H44.6z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 25 KiB

58
scripts/system/bubble.js Normal file
View file

@ -0,0 +1,58 @@
"use strict";
//
// bubble.js
// scripts/system/
//
// Created by Brad Hefta-Gaub on 11/18/2016
// 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
//
/* global Toolbars, Script, Users, Overlays, AvatarList, Controller, Camera, getControllerWorldLocation */
(function() { // BEGIN LOCAL_SCOPE
// grab the toolbar
var toolbar = Toolbars.getToolbar("com.highfidelity.interface.toolbar.system");
var ASSETS_PATH = Script.resolvePath("assets");
var TOOLS_PATH = Script.resolvePath("assets/images/tools/");
function buttonImageURL() {
return TOOLS_PATH + 'bubble.svg';
}
var bubbleActive = Users.getIgnoreRadiusEnabled();
// setup the mod button and add it to the toolbar
var button = toolbar.addButton({
objectName: 'bubble',
imageURL: buttonImageURL(),
visible: true,
buttonState: bubbleActive ? 0 : 1,
defaultState: bubbleActive ? 0 : 1,
hoverState: bubbleActive ? 2 : 3,
alpha: 0.9
});
// handle clicks on the toolbar button
function buttonClicked(){
Users.toggleIgnoreRadius();
bubbleActive = Users.getIgnoreRadiusEnabled();
button.writeProperty('buttonState', bubbleActive ? 0 : 1);
button.writeProperty('defaultState', bubbleActive ? 0 : 1);
button.writeProperty('hoverState', bubbleActive ? 2 : 3);
}
button.clicked.connect(buttonClicked);
// cleanup the toolbar button and overlays when script is stopped
Script.scriptEnding.connect(function() {
toolbar.removeButton('bubble');
});
}()); // END LOCAL_SCOPE

View file

@ -240,11 +240,8 @@ var toolBar = (function () {
hoverState: 3,
defaultState: 1
});
activeButton.clicked.connect(function () {
that.setActive(!isActive);
activeButton.writeProperty("buttonState", isActive ? 0 : 1);
activeButton.writeProperty("defaultState", isActive ? 0 : 1);
activeButton.writeProperty("hoverState", isActive ? 2 : 3);
activeButton.clicked.connect(function() {
that.toggle();
});
toolBar = Toolbars.getToolbar(EDIT_TOOLBAR);
@ -440,6 +437,14 @@ var toolBar = (function () {
entityListTool.clearEntityList();
};
that.toggle = function () {
that.setActive(!isActive);
activeButton.writeProperty("buttonState", isActive ? 0 : 1);
activeButton.writeProperty("defaultState", isActive ? 0 : 1);
activeButton.writeProperty("hoverState", isActive ? 2 : 3);
};
that.setActive = function (active) {
if (active === isActive) {
return;
@ -1093,7 +1098,6 @@ function handeMenuEvent(menuItem) {
}
}
} else if (menuItem === "Import Entities" || menuItem === "Import Entities from URL") {
var importURL = null;
if (menuItem === "Import Entities") {
var fullPath = Window.browse("Select Model to Import", "", "*.json");
@ -1105,6 +1109,9 @@ function handeMenuEvent(menuItem) {
}
if (importURL) {
if (!isActive && (Entities.canRez() && Entities.canRezTmp())) {
toolBar.toggle();
}
importSVO(importURL);
}
} else if (menuItem === "Entity List...") {
@ -1185,8 +1192,6 @@ function importSVO(importURL) {
if (isActive) {
selectionManager.setSelections(pastedEntityIDs);
}
Window.raiseMainWindow();
} else {
Window.notifyEditError("Can't import objects: objects would be out of bounds.");
}

View file

@ -1,48 +1,48 @@
<html>
<head>
<head>
<title>Share</title>
<link rel="stylesheet" type="text/css" href="css/edit-style.css">
<link rel="stylesheet" type="text/css" href="css/SnapshotReview.css">
<script type="text/javascript" src="qrc:///qtwebchannel/qwebchannel.js"></script>
<script type="text/javascript" src="js/eventBridgeLoader.js"></script>
<script type="text/javascript" src="js/SnapshotReview.js"></script>
</head>
</head>
<body>
<body>
<div class="snapshot-container">
<div class="snapshot-column-left">
<div class="snapsection">
<label class="title">Snapshot successfully saved!</label>
</div>
<hr />
<div class="snapsection">
<div id="sharing">
<div class="prompt">Would you like to share your pic in the Snapshots feed?</div>
<div class="button">
<span class="compound-button">
<input type="button" class="blue" id="share" value="Share in Feed" onclick="shareSelected()"/>
<span class="glyph"></span>
</span>
<div class="snapshot-column-left">
<div class="snapsection">
<label class="title">Snapshot successfully saved!</label>
</div>
<hr />
<div class="snapsection">
<div id="sharing">
<div class="prompt">Would you like to share your pics in the Snapshots feed?</div>
<div class="button">
<span class="compound-button">
<input type="button" class="blue" id="share" value="Share in Feed" onclick="shareSelected()" />
<span class="glyph"></span>
</span>
</div>
</div>
<div class="button">
<input type="button" class="black" id="close" value="Don't Share" onclick="doNotShare()" />
</div>
</div>
<hr />
<div class="snapsection">
<span class="setting">
<input type="button" class="glyph naked" id="snapshotSettings" value="@" onclick="snapshotSettings()" />
<label for="snapshotSettings">Snapshot settings</label>
</span>
<span class="setting checkbox">
<input id="openFeed" type="checkbox" checked />
<label for="openFeed">Open feed after</label>
</span>
</div>
</div>
<div class="button">
<input type="button" class="black" id="close" value="Don't Share" onclick="doNotShare()"/>
</div>
</div>
<hr />
<div class="snapsection">
<span class="setting">
<input type="button" class="glyph naked" id="snapshotSettings" value="@" onclick="snapshotSettings()" />
<label for="snapshotSettings">Snapshot settings</label>
</span>
<span class="setting checkbox">
<input id="openFeed" type="checkbox" checked/>
<label for="openFeed">Open feed after</label>
</span>
<div id="snapshot-images" class="snapshot-column-right">
</div>
</div>
<div id="snapshot-images" class="snapshot-column-right"/>
</div>
</div>
</body>
</body>
</html>

View file

@ -12,6 +12,9 @@
var paths = [], idCounter = 0, useCheckboxes;
function addImage(data) {
if (!data.localPath) {
return;
}
var div = document.createElement("DIV"),
input = document.createElement("INPUT"),
label = document.createElement("LABEL"),
@ -20,21 +23,22 @@ function addImage(data) {
function toggle() { data.share = input.checked; }
img.src = data.localPath;
div.appendChild(img);
data.share = true;
if (useCheckboxes) { // I'd rather use css, but the included stylesheet is quite particular.
// Our stylesheet(?) requires input.id to match label.for. Otherwise input doesn't display the check state.
label.setAttribute('for', id); // cannot do label.for =
input.id = id;
input.type = "checkbox";
input.checked = true;
input.checked = (id === "p0");
data.share = input.checked;
input.addEventListener('change', toggle);
div.class = "property checkbox";
div.appendChild(input);
div.appendChild(label);
} else {
data.share = true;
}
document.getElementById("snapshot-images").appendChild(div);
paths.push(data);
}
function handleShareButtons(shareMsg) {
var openFeed = document.getElementById('openFeed');
@ -49,7 +53,7 @@ function handleShareButtons(shareMsg) {
window.onload = function () {
// Something like the following will allow testing in a browser.
//addImage({localPath: 'c:/Users/howar/OneDrive/Pictures/hifi-snap-by--on-2016-07-27_12-58-43.jpg'});
//addImage({localPath: 'http://lorempixel.com/1512/1680'});
//addImage({ localPath: 'http://lorempixel.com/1512/1680' });
openEventBridge(function () {
// Set up a handler for receiving the data, and tell the .js we are ready to receive it.
EventBridge.scriptEventReceived.connect(function (message) {

View file

@ -522,13 +522,13 @@ function onEditError(msg) {
}
function onSnapshotTaken(path, notify) {
function onSnapshotTaken(pathStillSnapshot, pathAnimatedSnapshot, notify) {
if (notify) {
var imageProperties = {
path: "file:///" + path,
path: "file:///" + pathStillSnapshot,
aspectRatio: Window.innerWidth / Window.innerHeight
};
createNotification(wordWrap("Snapshot saved to " + path), NotificationType.SNAPSHOT, imageProperties);
createNotification(wordWrap("Snapshot saved to " + pathStillSnapshot), NotificationType.SNAPSHOT, imageProperties);
}
}

View file

@ -36,7 +36,7 @@ var SNAPSHOT_REVIEW_URL = Script.resolvePath("html/SnapshotReview.html");
var outstanding;
function confirmShare(data) {
var dialog = new OverlayWebWindow('Snapshot Review', SNAPSHOT_REVIEW_URL, 800, 320);
var dialog = new OverlayWebWindow('Snapshot Review', SNAPSHOT_REVIEW_URL, 800, 520);
function onMessage(message) {
// Receives message from the html dialog via the qwebchannel EventBridge. This is complicated by the following:
// 1. Although we can send POJOs, we cannot receive a toplevel object. (Arrays of POJOs are fine, though.)
@ -120,11 +120,11 @@ function onClicked() {
// take snapshot (with no notification)
Script.setTimeout(function () {
Window.takeSnapshot(false, 1.91);
Window.takeSnapshot(false, true, 1.91);
}, SNAPSHOT_DELAY);
}
function resetButtons(path, notify) {
function resetButtons(pathStillSnapshot, pathAnimatedSnapshot, notify) {
// show overlays if they were on
if (resetOverlays) {
Menu.setIsOptionChecked("Overlays", true);
@ -141,7 +141,8 @@ function resetButtons(path, notify) {
// last element in data array tells dialog whether we can share or not
confirmShare([
{ localPath: path },
{ localPath: pathAnimatedSnapshot },
{ localPath: pathStillSnapshot },
{
canShare: !!location.placename,
openFeedAfterShare: shouldOpenFeedAfterShare()