Merge branch 'master' into avatar-mixer-scaling

This commit is contained in:
Simon Walton 2018-08-23 13:58:38 -07:00
commit 9dac399c20
90 changed files with 1952 additions and 1608 deletions

View file

@ -25,16 +25,19 @@
#include <AudioInjectorManager.h>
#include <AssetClient.h>
#include <DebugDraw.h>
#include <EntityScriptingInterface.h>
#include <LocationScriptingInterface.h>
#include <MessagesClient.h>
#include <NetworkAccessManager.h>
#include <NodeList.h>
#include <udt/PacketHeaders.h>
#include <ResourceCache.h>
#include <ResourceScriptingInterface.h>
#include <ScriptCache.h>
#include <ScriptEngines.h>
#include <SoundCacheScriptingInterface.h>
#include <SoundCache.h>
#include <UserActivityLoggerScriptingInterface.h>
#include <UsersScriptingInterface.h>
#include <UUID.h>
@ -54,7 +57,6 @@
#include "AbstractAudioInterface.h"
#include "AgentScriptingInterface.h"
static const int RECEIVED_AUDIO_STREAM_CAPACITY_FRAMES = 10;
Agent::Agent(ReceivedMessage& message) :
@ -63,6 +65,15 @@ Agent::Agent(ReceivedMessage& message) :
_audioGate(AudioConstants::SAMPLE_RATE, AudioConstants::MONO),
_avatarAudioTimer(this)
{
DependencyManager::set<ScriptableAvatar>();
DependencyManager::set<AnimationCache>();
DependencyManager::set<AnimationCacheScriptingInterface>();
DependencyManager::set<EntityScriptingInterface>(false);
DependencyManager::set<ResourceScriptingInterface>();
DependencyManager::set<UserActivityLoggerScriptingInterface>();
_entityEditSender.setPacketsPerSecond(DEFAULT_ENTITY_PPS_PER_SCRIPT);
DependencyManager::get<EntityScriptingInterface>()->setPacketSender(&_entityEditSender);
@ -99,7 +110,6 @@ Agent::Agent(ReceivedMessage& message) :
this, "handleOctreePacket");
packetReceiver.registerListener(PacketType::SelectedAudioFormat, this, "handleSelectedAudioFormat");
// 100Hz timer for audio
const int TARGET_INTERVAL_MSEC = 10; // 10ms
connect(&_avatarAudioTimer, &QTimer::timeout, this, &Agent::processAgentAvatarAudio);
@ -351,6 +361,8 @@ void Agent::executeScript() {
// setup an Avatar for the script to use
auto scriptedAvatar = DependencyManager::get<ScriptableAvatar>();
scriptedAvatar->setID(getSessionUUID());
connect(_scriptEngine.data(), SIGNAL(update(float)),
scriptedAvatar.data(), SLOT(update(float)), Qt::ConnectionType::QueuedConnection);
scriptedAvatar->setForceFaceTrackerConnected(true);
@ -439,7 +451,7 @@ void Agent::executeScript() {
encodedBuffer = audio;
}
AbstractAudioInterface::emitAudioPacket(encodedBuffer.data(), encodedBuffer.size(), audioSequenceNumber, false,
AbstractAudioInterface::emitAudioPacket(encodedBuffer.data(), encodedBuffer.size(), audioSequenceNumber, false,
audioTransform, scriptedAvatar->getWorldPosition(), glm::vec3(0),
packetType, _selectedCodecName);
});
@ -447,11 +459,6 @@ void Agent::executeScript() {
auto avatarHashMap = DependencyManager::set<AvatarHashMap>();
_scriptEngine->registerGlobalObject("AvatarList", avatarHashMap.data());
auto& packetReceiver = DependencyManager::get<NodeList>()->getPacketReceiver();
packetReceiver.registerListener(PacketType::BulkAvatarData, avatarHashMap.data(), "processAvatarDataPacket");
packetReceiver.registerListener(PacketType::KillAvatar, avatarHashMap.data(), "processKillAvatar");
packetReceiver.registerListener(PacketType::AvatarIdentity, avatarHashMap.data(), "processAvatarIdentityPacket");
// register ourselves to the script engine
_scriptEngine->registerGlobalObject("Agent", new AgentScriptingInterface(this));
@ -597,6 +604,11 @@ void Agent::setIsAvatar(bool isAvatar) {
}
QMetaObject::invokeMethod(&_avatarAudioTimer, "stop");
_entityEditSender.setMyAvatar(nullptr);
} else {
auto scriptableAvatar = DependencyManager::get<ScriptableAvatar>();
_entityEditSender.setMyAvatar(scriptableAvatar.data());
}
}
@ -847,6 +859,9 @@ void Agent::aboutToFinish() {
DependencyManager::destroy<recording::Recorder>();
DependencyManager::destroy<recording::ClipCache>();
DependencyManager::destroy<ScriptEngine>();
DependencyManager::destroy<ScriptableAvatar>();
QMetaObject::invokeMethod(&_avatarAudioTimer, "stop");
// cleanup codec & encoder

View file

@ -21,6 +21,7 @@
#include <QtCore/QTimer>
#include <QUuid>
#include <ClientTraitsHandler.h>
#include <EntityEditPacketSender.h>
#include <EntityTree.h>
#include <ScriptEngine.h>

View file

@ -21,10 +21,7 @@
#include <shared/QtHelpers.h>
#include <AccountManager.h>
#include <AddressManager.h>
#include <AnimationCacheScriptingInterface.h>
#include <Assignment.h>
#include <AvatarHashMap.h>
#include <EntityScriptingInterface.h>
#include <LogHandler.h>
#include <LogUtils.h>
#include <LimitedNodeList.h>
@ -32,16 +29,12 @@
#include <udt/PacketHeaders.h>
#include <SharedUtil.h>
#include <ShutdownEventListener.h>
#include <SoundCache.h>
#include <ResourceScriptingInterface.h>
#include <UserActivityLoggerScriptingInterface.h>
#include <Trace.h>
#include <StatTracker.h>
#include "AssignmentClientLogging.h"
#include "AssignmentDynamicFactory.h"
#include "AssignmentFactory.h"
#include "avatars/ScriptableAvatar.h"
const QString ASSIGNMENT_CLIENT_TARGET_NAME = "assignment-client";
const long long ASSIGNMENT_REQUEST_INTERVAL_MSECS = 1 * 1000;
@ -57,21 +50,11 @@ AssignmentClient::AssignmentClient(Assignment::Type requestAssignmentType, QStri
DependencyManager::set<StatTracker>();
DependencyManager::set<AccountManager>();
auto scriptableAvatar = DependencyManager::set<ScriptableAvatar>();
auto addressManager = DependencyManager::set<AddressManager>();
// create a NodeList as an unassigned client, must be after addressManager
auto nodeList = DependencyManager::set<NodeList>(NodeType::Unassigned, listenPort);
auto animationCache = DependencyManager::set<AnimationCache>();
DependencyManager::set<AnimationCacheScriptingInterface>();
auto entityScriptingInterface = DependencyManager::set<EntityScriptingInterface>(false);
DependencyManager::registerInheritance<EntityDynamicFactoryInterface, AssignmentDynamicFactory>();
auto dynamicFactory = DependencyManager::set<AssignmentDynamicFactory>();
DependencyManager::set<ResourceScriptingInterface>();
DependencyManager::set<UserActivityLoggerScriptingInterface>();
nodeList->startThread();
// set the logging target to the the CHILD_TARGET_NAME
LogHandler::getInstance().setTargetName(ASSIGNMENT_CLIENT_TARGET_NAME);

View file

@ -366,7 +366,7 @@ AudioMixerClientData* AudioMixer::getOrCreateClientData(Node* node) {
auto clientData = dynamic_cast<AudioMixerClientData*>(node->getLinkedData());
if (!clientData) {
node->setLinkedData(std::unique_ptr<NodeData> { new AudioMixerClientData(node->getUUID()) });
node->setLinkedData(std::unique_ptr<NodeData> { new AudioMixerClientData(node->getUUID(), node->getLocalID()) });
clientData = dynamic_cast<AudioMixerClientData*>(node->getLinkedData());
connect(clientData, &AudioMixerClientData::injectorStreamFinished, this, &AudioMixer::removeHRTFsForFinishedInjector);
}

View file

@ -25,8 +25,8 @@
#include "AudioHelpers.h"
#include "AudioMixer.h"
AudioMixerClientData::AudioMixerClientData(const QUuid& nodeID) :
NodeData(nodeID),
AudioMixerClientData::AudioMixerClientData(const QUuid& nodeID, Node::LocalID nodeLocalID) :
NodeData(nodeID, nodeLocalID),
audioLimiter(AudioConstants::SAMPLE_RATE, AudioConstants::STEREO),
_ignoreZone(*this),
_outgoingMixedAudioSequenceNumber(0),

View file

@ -30,7 +30,7 @@
class AudioMixerClientData : public NodeData {
Q_OBJECT
public:
AudioMixerClientData(const QUuid& nodeID);
AudioMixerClientData(const QUuid& nodeID, Node::LocalID nodeLocalID);
~AudioMixerClientData();
using SharedStreamPointer = std::shared_ptr<PositionalAudioStream>;

View file

@ -39,7 +39,8 @@ const QString AVATAR_MIXER_LOGGING_NAME = "avatar-mixer";
const int AVATAR_MIXER_BROADCAST_FRAMES_PER_SECOND = 45;
AvatarMixer::AvatarMixer(ReceivedMessage& message) :
ThreadedAssignment(message)
ThreadedAssignment(message),
_slavePool(&_slaveSharedData)
{
// make sure we hear about node kills so we can tell the other nodes
connect(DependencyManager::get<NodeList>().data(), &NodeList::nodeKilled, this, &AvatarMixer::handleAvatarKilled);
@ -54,6 +55,7 @@ AvatarMixer::AvatarMixer(ReceivedMessage& message) :
packetReceiver.registerListener(PacketType::RadiusIgnoreRequest, this, "handleRadiusIgnoreRequestPacket");
packetReceiver.registerListener(PacketType::RequestsDomainListData, this, "handleRequestsDomainListDataPacket");
packetReceiver.registerListener(PacketType::AvatarIdentityRequest, this, "handleAvatarIdentityRequestPacket");
packetReceiver.registerListener(PacketType::SetAvatarTraits, this, "queueIncomingPacket");
packetReceiver.registerListenerForTypes({
PacketType::ReplicatedAvatarIdentity,
@ -337,17 +339,7 @@ void AvatarMixer::manageIdentityData(const SharedNodePointer& node) {
sendIdentity = true;
qCDebug(avatars) << "Giving session display name" << sessionDisplayName << "to node with ID" << node->getUUID();
}
if (nodeData && nodeData->getAvatarSkeletonModelUrlMustChange()) { // never true for an empty _avatarWhitelist
nodeData->setAvatarSkeletonModelUrlMustChange(false);
AvatarData& avatar = nodeData->getAvatar();
static const QUrl emptyURL("");
QUrl url = avatar.cannonicalSkeletonModelURL(emptyURL);
if (!isAvatarInWhitelist(url)) {
qCDebug(avatars) << "Forbidden avatar" << nodeData->getNodeID() << avatar.getSkeletonModelURL() << "replaced with" << (_replacementAvatar.isEmpty() ? "default" : _replacementAvatar);
avatar.setSkeletonModelURL(_replacementAvatar);
sendIdentity = true;
}
}
if (sendIdentity && !node->isUpstream()) {
sendIdentityPacket(nodeData, node); // Tell node whose name changed about its new session display name or avatar.
// since this packet includes a change to either the skeleton model URL or the display name
@ -359,22 +351,6 @@ void AvatarMixer::manageIdentityData(const SharedNodePointer& node) {
}
}
bool AvatarMixer::isAvatarInWhitelist(const QUrl& url) {
// The avatar is in the whitelist if:
// 1. The avatar's URL's host matches one of the hosts of the URLs in the whitelist AND
// 2. The avatar's URL's path starts with the path of that same URL in the whitelist
for (const auto& whiteListedPrefix : _avatarWhitelist) {
auto whiteListURL = QUrl::fromUserInput(whiteListedPrefix);
// check if this script URL matches the whitelist domain and, optionally, is beneath the path
if (url.host().compare(whiteListURL.host(), Qt::CaseInsensitive) == 0 &&
url.path().startsWith(whiteListURL.path(), Qt::CaseInsensitive)) {
return true;
}
}
return false;
}
void AvatarMixer::throttle(std::chrono::microseconds duration, int frame) {
// throttle using a modified proportional-integral controller
const float FRAME_TIME = USECS_PER_SECOND / AVATAR_MIXER_BROADCAST_FRAMES_PER_SECOND;
@ -494,7 +470,8 @@ void AvatarMixer::handleAvatarKilled(SharedNodePointer avatarNode) {
QMetaObject::invokeMethod(node->getLinkedData(),
"cleanupKilledNode",
Qt::AutoConnection,
Q_ARG(const QUuid&, QUuid(avatarNode->getUUID())));
Q_ARG(const QUuid&, QUuid(avatarNode->getUUID())),
Q_ARG(Node::LocalID, avatarNode->getLocalID()));
}
);
}
@ -587,8 +564,7 @@ void AvatarMixer::handleAvatarIdentityPacket(QSharedPointer<ReceivedMessage> mes
// parse the identity packet and update the change timestamp if appropriate
bool identityChanged = false;
bool displayNameChanged = false;
bool skeletonModelUrlChanged = false;
avatar.processAvatarIdentity(message->getMessage(), identityChanged, displayNameChanged, skeletonModelUrlChanged);
avatar.processAvatarIdentity(message->getMessage(), identityChanged, displayNameChanged);
if (identityChanged) {
QMutexLocker nodeDataLocker(&nodeData->getMutex());
@ -596,9 +572,6 @@ void AvatarMixer::handleAvatarIdentityPacket(QSharedPointer<ReceivedMessage> mes
if (displayNameChanged) {
nodeData->setAvatarSessionDisplayNameMustChange(true);
}
if (skeletonModelUrlChanged && !_avatarWhitelist.isEmpty()) {
nodeData->setAvatarSkeletonModelUrlMustChange(true);
}
}
}
}
@ -903,7 +876,7 @@ AvatarMixerClientData* AvatarMixer::getOrCreateClientData(SharedNodePointer node
auto clientData = dynamic_cast<AvatarMixerClientData*>(node->getLinkedData());
if (!clientData) {
node->setLinkedData(std::unique_ptr<NodeData> { new AvatarMixerClientData(node->getUUID()) });
node->setLinkedData(std::unique_ptr<NodeData> { new AvatarMixerClientData(node->getUUID(), node->getLocalID()) });
clientData = dynamic_cast<AvatarMixerClientData*>(node->getLinkedData());
auto& avatar = clientData->getAvatar();
avatar.setDomainMinimumHeight(_domainMinimumHeight);
@ -991,20 +964,22 @@ void AvatarMixer::parseDomainServerSettings(const QJsonObject& domainSettings) {
qCDebug(avatars) << "This domain requires a minimum avatar height of" << _domainMinimumHeight
<< "and a maximum avatar height of" << _domainMaximumHeight;
const QString AVATAR_WHITELIST_DEFAULT{ "" };
static const QString AVATAR_WHITELIST_OPTION = "avatar_whitelist";
_avatarWhitelist = domainSettings[AVATARS_SETTINGS_KEY].toObject()[AVATAR_WHITELIST_OPTION].toString(AVATAR_WHITELIST_DEFAULT).split(',', QString::KeepEmptyParts);
_slaveSharedData.skeletonURLWhitelist = domainSettings[AVATARS_SETTINGS_KEY].toObject()[AVATAR_WHITELIST_OPTION]
.toString().split(',', QString::KeepEmptyParts);
static const QString REPLACEMENT_AVATAR_OPTION = "replacement_avatar";
_replacementAvatar = domainSettings[AVATARS_SETTINGS_KEY].toObject()[REPLACEMENT_AVATAR_OPTION].toString(REPLACEMENT_AVATAR_DEFAULT);
_slaveSharedData.skeletonReplacementURL = domainSettings[AVATARS_SETTINGS_KEY].toObject()[REPLACEMENT_AVATAR_OPTION]
.toString();
if ((_avatarWhitelist.count() == 1) && _avatarWhitelist[0].isEmpty()) {
_avatarWhitelist.clear(); // KeepEmptyParts above will parse "," as ["", ""] (which is ok), but "" as [""] (which is not ok).
if (_slaveSharedData.skeletonURLWhitelist.count() == 1 && _slaveSharedData.skeletonURLWhitelist[0].isEmpty()) {
// KeepEmptyParts above will parse "," as ["", ""] (which is ok), but "" as [""] (which is not ok).
_slaveSharedData.skeletonURLWhitelist.clear();
}
if (_avatarWhitelist.isEmpty()) {
if (_slaveSharedData.skeletonURLWhitelist.isEmpty()) {
qCDebug(avatars) << "All avatars are allowed.";
} else {
qCDebug(avatars) << "Avatars other than" << _avatarWhitelist << "will be replaced by" << (_replacementAvatar.isEmpty() ? "default" : _replacementAvatar);
qCDebug(avatars) << "Avatars other than" << _slaveSharedData.skeletonURLWhitelist << "will be replaced by" << (_slaveSharedData.skeletonReplacementURL.isEmpty() ? "default" : _slaveSharedData.skeletonReplacementURL.toString());
}
}

View file

@ -59,7 +59,6 @@ private slots:
void handlePacketVersionMismatch(PacketType type, const HifiSockAddr& senderSockAddr, const QUuid& senderUUID);
void start();
private:
AvatarMixerClientData* getOrCreateClientData(SharedNodePointer node);
std::chrono::microseconds timeFrame(p_high_resolution_clock::time_point& timestamp);
@ -69,11 +68,6 @@ private:
void sendIdentityPacket(AvatarMixerClientData* nodeData, const SharedNodePointer& destinationNode);
void manageIdentityData(const SharedNodePointer& node);
bool isAvatarInWhitelist(const QUrl& url);
const QString REPLACEMENT_AVATAR_DEFAULT{ "" };
QStringList _avatarWhitelist { };
QString _replacementAvatar { REPLACEMENT_AVATAR_DEFAULT };
void optionallyReplicatePacket(ReceivedMessage& message, const Node& node);
@ -83,7 +77,6 @@ private:
float _trailingMixRatio { 0.0f };
float _throttlingRatio { 0.0f };
int _sumListeners { 0 };
int _numStatFrames { 0 };
int _numTightLoopFrames { 0 };
@ -126,9 +119,8 @@ private:
RateCounter<> _loopRate; // this is the rate that the main thread tight loop runs
AvatarMixerSlavePool _slavePool;
SlaveSharedData _slaveSharedData;
};
#endif // hifi_AvatarMixer_h

View file

@ -16,8 +16,10 @@
#include <DependencyManager.h>
#include <NodeList.h>
AvatarMixerClientData::AvatarMixerClientData(const QUuid& nodeID) :
NodeData(nodeID)
#include "AvatarMixerSlave.h"
AvatarMixerClientData::AvatarMixerClientData(const QUuid& nodeID, Node::LocalID nodeLocalID) :
NodeData(nodeID, nodeLocalID)
{
// in case somebody calls getSessionUUID on the AvatarData instance, make sure it has the right ID
_avatar->setID(nodeID);
@ -47,7 +49,7 @@ void AvatarMixerClientData::queuePacket(QSharedPointer<ReceivedMessage> message,
_packetQueue.push(message);
}
int AvatarMixerClientData::processPackets() {
int AvatarMixerClientData::processPackets(const SlaveSharedData& slaveSharedData) {
int packetsProcessed = 0;
SharedNodePointer node = _packetQueue.node;
assert(_packetQueue.empty() || node);
@ -62,6 +64,9 @@ int AvatarMixerClientData::processPackets() {
case PacketType::AvatarData:
parseData(*packet);
break;
case PacketType::SetAvatarTraits:
processSetTraitsMessage(*packet, slaveSharedData, *node);
break;
default:
Q_UNREACHABLE();
}
@ -87,6 +92,113 @@ int AvatarMixerClientData::parseData(ReceivedMessage& message) {
// compute the offset to the data payload
return _avatar->parseDataFromBuffer(message.readWithoutCopy(message.getBytesLeftToRead()));
}
void AvatarMixerClientData::processSetTraitsMessage(ReceivedMessage& message,
const SlaveSharedData& slaveSharedData, Node& sendingNode) {
// pull the trait version from the message
AvatarTraits::TraitVersion packetTraitVersion;
message.readPrimitive(&packetTraitVersion);
bool anyTraitsChanged = false;
while (message.getBytesLeftToRead() > 0) {
// for each trait in the packet, apply it if the trait version is newer than what we have
AvatarTraits::TraitType traitType;
message.readPrimitive(&traitType);
if (AvatarTraits::isSimpleTrait(traitType)) {
AvatarTraits::TraitWireSize traitSize;
message.readPrimitive(&traitSize);
if (packetTraitVersion > _lastReceivedTraitVersions[traitType]) {
_avatar->processTrait(traitType, message.read(traitSize));
_lastReceivedTraitVersions[traitType] = packetTraitVersion;
if (traitType == AvatarTraits::SkeletonModelURL) {
// special handling for skeleton model URL, since we need to make sure it is in the whitelist
checkSkeletonURLAgainstWhitelist(slaveSharedData, sendingNode, packetTraitVersion);
}
anyTraitsChanged = true;
} else {
message.seek(message.getPosition() + traitSize);
}
} else {
AvatarTraits::TraitInstanceID instanceID = QUuid::fromRfc4122(message.readWithoutCopy(NUM_BYTES_RFC4122_UUID));
AvatarTraits::TraitWireSize traitSize;
message.readPrimitive(&traitSize);
auto& instanceVersionRef = _lastReceivedTraitVersions.getInstanceValueRef(traitType, instanceID);
if (packetTraitVersion > instanceVersionRef) {
if (traitSize == AvatarTraits::DELETED_TRAIT_SIZE) {
_avatar->processDeletedTraitInstance(traitType, instanceID);
// to track a deleted instance but keep version information
// the avatar mixer uses the negative value of the sent version
instanceVersionRef = -packetTraitVersion;
} else {
_avatar->processTraitInstance(traitType, instanceID, message.read(traitSize));
instanceVersionRef = packetTraitVersion;
}
anyTraitsChanged = true;
} else {
message.seek(message.getPosition() + traitSize);
}
}
}
if (anyTraitsChanged) {
_lastReceivedTraitsChange = std::chrono::steady_clock::now();
}
}
void AvatarMixerClientData::checkSkeletonURLAgainstWhitelist(const SlaveSharedData &slaveSharedData, Node& sendingNode,
AvatarTraits::TraitVersion traitVersion) {
const auto& whitelist = slaveSharedData.skeletonURLWhitelist;
if (!whitelist.isEmpty()) {
bool inWhitelist = false;
auto avatarURL = _avatar->getSkeletonModelURL();
// The avatar is in the whitelist if:
// 1. The avatar's URL's host matches one of the hosts of the URLs in the whitelist AND
// 2. The avatar's URL's path starts with the path of that same URL in the whitelist
for (const auto& whiteListedPrefix : whitelist) {
auto whiteListURL = QUrl::fromUserInput(whiteListedPrefix);
// check if this script URL matches the whitelist domain and, optionally, is beneath the path
if (avatarURL.host().compare(whiteListURL.host(), Qt::CaseInsensitive) == 0 &&
avatarURL.path().startsWith(whiteListURL.path(), Qt::CaseInsensitive)) {
inWhitelist = true;
break;
}
}
if (!inWhitelist) {
// make sure we're not unecessarily overriding the default avatar with the default avatar
if (_avatar->getWireSafeSkeletonModelURL() != slaveSharedData.skeletonReplacementURL) {
// we need to change this avatar's skeleton URL, and send them a traits packet informing them of the change
qDebug() << "Overwriting avatar URL" << _avatar->getWireSafeSkeletonModelURL()
<< "to replacement" << slaveSharedData.skeletonReplacementURL << "for" << sendingNode.getUUID();
_avatar->setSkeletonModelURL(slaveSharedData.skeletonReplacementURL);
auto packet = NLPacket::create(PacketType::SetAvatarTraits, -1, true);
// the returned set traits packet uses the trait version from the incoming packet
// so the client knows they should not overwrite if they have since changed the trait
_avatar->packTrait(AvatarTraits::SkeletonModelURL, *packet, traitVersion);
auto nodeList = DependencyManager::get<NodeList>();
nodeList->sendPacket(std::move(packet), sendingNode);
}
}
}
}
uint64_t AvatarMixerClientData::getLastBroadcastTime(const QUuid& nodeUUID) const {
// return the matching PacketSequenceNumber, or the default if we don't have it
auto nodeMatch = _lastBroadcastTimes.find(nodeUUID);
@ -164,3 +276,20 @@ void AvatarMixerClientData::loadJSONStats(QJsonObject& jsonObject) const {
jsonObject["recent_other_av_in_view"] = _recentOtherAvatarsInView;
jsonObject["recent_other_av_out_of_view"] = _recentOtherAvatarsOutOfView;
}
AvatarMixerClientData::TraitsCheckTimestamp AvatarMixerClientData::getLastOtherAvatarTraitsSendPoint(Node::LocalID otherAvatar) const {
auto it = _lastSentTraitsTimestamps.find(otherAvatar);
if (it != _lastSentTraitsTimestamps.end()) {
return it->second;
} else {
return TraitsCheckTimestamp();
}
}
void AvatarMixerClientData::cleanupKilledNode(const QUuid& nodeUUID, Node::LocalID nodeLocalID) {
removeLastBroadcastSequenceNumber(nodeUUID);
removeLastBroadcastTime(nodeUUID);
_lastSentTraitsTimestamps.erase(nodeLocalID);
_sentTraitVersions.erase(nodeLocalID);
}

View file

@ -22,6 +22,7 @@
#include <QtCore/QUrl>
#include <AvatarData.h>
#include <AssociatedTraitValues.h>
#include <NodeData.h>
#include <NumericalConstants.h>
#include <udt/PacketHeaders.h>
@ -33,10 +34,12 @@
const QString OUTBOUND_AVATAR_DATA_STATS_KEY = "outbound_av_data_kbps";
const QString INBOUND_AVATAR_DATA_STATS_KEY = "inbound_av_data_kbps";
struct SlaveSharedData;
class AvatarMixerClientData : public NodeData {
Q_OBJECT
public:
AvatarMixerClientData(const QUuid& nodeID = QUuid());
AvatarMixerClientData(const QUuid& nodeID, Node::LocalID nodeLocalID);
virtual ~AvatarMixerClientData() {}
using HRCTime = p_high_resolution_clock::time_point;
@ -54,10 +57,7 @@ public:
void setLastBroadcastTime(const QUuid& nodeUUID, uint64_t broadcastTime) { _lastBroadcastTimes[nodeUUID] = broadcastTime; }
Q_INVOKABLE void removeLastBroadcastTime(const QUuid& nodeUUID) { _lastBroadcastTimes.erase(nodeUUID); }
Q_INVOKABLE void cleanupKilledNode(const QUuid& nodeUUID) {
removeLastBroadcastSequenceNumber(nodeUUID);
removeLastBroadcastTime(nodeUUID);
}
Q_INVOKABLE void cleanupKilledNode(const QUuid& nodeUUID, Node::LocalID nodeLocalID);
uint16_t getLastReceivedSequenceNumber() const { return _lastReceivedSequenceNumber; }
@ -65,8 +65,6 @@ public:
void flagIdentityChange() { _identityChangeTimestamp = usecTimestampNow(); }
bool getAvatarSessionDisplayNameMustChange() const { return _avatarSessionDisplayNameMustChange; }
void setAvatarSessionDisplayNameMustChange(bool set = true) { _avatarSessionDisplayNameMustChange = set; }
bool getAvatarSkeletonModelUrlMustChange() const { return _avatarSkeletonModelUrlMustChange; }
void setAvatarSkeletonModelUrlMustChange(bool set = true) { _avatarSkeletonModelUrlMustChange = set; }
void resetNumAvatarsSentLastFrame() { _numAvatarsSentLastFrame = 0; }
void incrementNumAvatarsSentLastFrame() { ++_numAvatarsSentLastFrame; }
@ -118,7 +116,24 @@ public:
QVector<JointData>& getLastOtherAvatarSentJoints(QUuid otherAvatar) { return _lastOtherAvatarSentJoints[otherAvatar]; }
void queuePacket(QSharedPointer<ReceivedMessage> message, SharedNodePointer node);
int processPackets(); // returns number of packets processed
int processPackets(const SlaveSharedData& slaveSharedData); // returns number of packets processed
void processSetTraitsMessage(ReceivedMessage& message, const SlaveSharedData& slaveSharedData, Node& sendingNode);
void checkSkeletonURLAgainstWhitelist(const SlaveSharedData& slaveSharedData, Node& sendingNode,
AvatarTraits::TraitVersion traitVersion);
using TraitsCheckTimestamp = std::chrono::steady_clock::time_point;
TraitsCheckTimestamp getLastReceivedTraitsChange() const { return _lastReceivedTraitsChange; }
AvatarTraits::TraitVersions& getLastReceivedTraitVersions() { return _lastReceivedTraitVersions; }
const AvatarTraits::TraitVersions& getLastReceivedTraitVersions() const { return _lastReceivedTraitVersions; }
TraitsCheckTimestamp getLastOtherAvatarTraitsSendPoint(Node::LocalID otherAvatar) const;
void setLastOtherAvatarTraitsSendPoint(Node::LocalID otherAvatar, TraitsCheckTimestamp sendPoint)
{ _lastSentTraitsTimestamps[otherAvatar] = sendPoint; }
AvatarTraits::TraitVersions& getLastSentTraitVersions(Node::LocalID otherAvatar) { return _sentTraitVersions[otherAvatar]; }
private:
struct PacketQueue : public std::queue<QSharedPointer<ReceivedMessage>> {
@ -156,6 +171,12 @@ private:
int _recentOtherAvatarsOutOfView { 0 };
QString _baseDisplayName{}; // The santized key used in determinging unique sessionDisplayName, so that we can remove from dictionary.
bool _requestsDomainListData { false };
AvatarTraits::TraitVersions _lastReceivedTraitVersions;
TraitsCheckTimestamp _lastReceivedTraitsChange;
std::unordered_map<Node::LocalID, TraitsCheckTimestamp> _lastSentTraitsTimestamps;
std::unordered_map<Node::LocalID, AvatarTraits::TraitVersions> _sentTraitVersions;
};
#endif // hifi_AvatarMixerClientData_h

View file

@ -59,7 +59,7 @@ void AvatarMixerSlave::processIncomingPackets(const SharedNodePointer& node) {
auto nodeData = dynamic_cast<AvatarMixerClientData*>(node->getLinkedData());
if (nodeData) {
_stats.nodesProcessed++;
_stats.packetsProcessed += nodeData->processPackets();
_stats.packetsProcessed += nodeData->processPackets(*_sharedData);
}
auto end = usecTimestampNow();
_stats.processIncomingPacketsElapsedTime += (end - start);
@ -79,6 +79,107 @@ int AvatarMixerSlave::sendIdentityPacket(const AvatarMixerClientData* nodeData,
}
}
qint64 AvatarMixerSlave::addChangedTraitsToBulkPacket(AvatarMixerClientData* listeningNodeData,
const AvatarMixerClientData* sendingNodeData,
NLPacketList& traitsPacketList) {
auto otherNodeLocalID = sendingNodeData->getNodeLocalID();
// Perform a simple check with two server clock time points
// to see if there is any new traits data for this avatar that we need to send
auto timeOfLastTraitsSent = listeningNodeData->getLastOtherAvatarTraitsSendPoint(otherNodeLocalID);
auto timeOfLastTraitsChange = sendingNodeData->getLastReceivedTraitsChange();
qint64 bytesWritten = 0;
if (timeOfLastTraitsChange > timeOfLastTraitsSent) {
// there is definitely new traits data to send
// add the avatar ID to mark the beginning of traits for this avatar
bytesWritten += traitsPacketList.write(sendingNodeData->getNodeID().toRfc4122());
auto sendingAvatar = sendingNodeData->getAvatarSharedPointer();
// compare trait versions so we can see what exactly needs to go out
auto& lastSentVersions = listeningNodeData->getLastSentTraitVersions(otherNodeLocalID);
const auto& lastReceivedVersions = sendingNodeData->getLastReceivedTraitVersions();
auto simpleReceivedIt = lastReceivedVersions.simpleCBegin();
while (simpleReceivedIt != lastReceivedVersions.simpleCEnd()) {
auto traitType = static_cast<AvatarTraits::TraitType>(std::distance(lastReceivedVersions.simpleCBegin(),
simpleReceivedIt));
auto lastReceivedVersion = *simpleReceivedIt;
auto& lastSentVersionRef = lastSentVersions[traitType];
if (lastReceivedVersions[traitType] > lastSentVersionRef) {
// there is an update to this trait, add it to the traits packet
bytesWritten += sendingAvatar->packTrait(traitType, traitsPacketList, lastReceivedVersion);
// update the last sent version
lastSentVersionRef = lastReceivedVersion;
}
++simpleReceivedIt;
}
// enumerate the received instanced trait versions
auto instancedReceivedIt = lastReceivedVersions.instancedCBegin();
while (instancedReceivedIt != lastReceivedVersions.instancedCEnd()) {
auto traitType = instancedReceivedIt->traitType;
// get or create the sent trait versions for this trait type
auto& sentIDValuePairs = lastSentVersions.getInstanceIDValuePairs(traitType);
// enumerate each received instance
for (auto& receivedInstance : instancedReceivedIt->instances) {
auto instanceID = receivedInstance.id;
const auto receivedVersion = receivedInstance.value;
// to track deletes and maintain version information for traits
// the mixer stores the negative value of the received version when a trait instance is deleted
bool isDeleted = receivedVersion < 0;
const auto absoluteReceivedVersion = std::abs(receivedVersion);
// look for existing sent version for this instance
auto sentInstanceIt = std::find_if(sentIDValuePairs.begin(), sentIDValuePairs.end(),
[instanceID](auto& sentInstance)
{
return sentInstance.id == instanceID;
});
if (!isDeleted && (sentInstanceIt == sentIDValuePairs.end() || receivedVersion > sentInstanceIt->value)) {
// this instance version exists and has never been sent or is newer so we need to send it
bytesWritten += sendingAvatar->packTraitInstance(traitType, instanceID, traitsPacketList, receivedVersion);
if (sentInstanceIt != sentIDValuePairs.end()) {
sentInstanceIt->value = receivedVersion;
} else {
sentIDValuePairs.emplace_back(instanceID, receivedVersion);
}
} else if (isDeleted && sentInstanceIt != sentIDValuePairs.end() && absoluteReceivedVersion > sentInstanceIt->value) {
// this instance version was deleted and we haven't sent the delete to this client yet
bytesWritten += AvatarTraits::packInstancedTraitDelete(traitType, instanceID, traitsPacketList, absoluteReceivedVersion);
// update the last sent version for this trait instance to the absolute value of the deleted version
sentInstanceIt->value = absoluteReceivedVersion;
}
}
++instancedReceivedIt;
}
// write a null trait type to mark the end of trait data for this avatar
bytesWritten += traitsPacketList.writePrimitive(AvatarTraits::NullTrait);
// since we send all traits for this other avatar, update the time of last traits sent
// to match the time of last traits change
listeningNodeData->setLastOtherAvatarTraitsSendPoint(otherNodeLocalID, timeOfLastTraitsChange);
}
return bytesWritten;
}
int AvatarMixerSlave::sendReplicatedIdentityPacket(const Node& agentNode, const AvatarMixerClientData* nodeData, const Node& destinationNode) {
if (AvatarMixer::shouldReplicateTo(agentNode, destinationNode)) {
QByteArray individualData = nodeData->getConstAvatarData()->identityByteArray(true);
@ -138,6 +239,7 @@ void AvatarMixerSlave::broadcastAvatarDataToAgent(const SharedNodePointer& node)
// keep track of outbound data rate specifically for avatar data
int numAvatarDataBytes = 0;
int identityBytesSent = 0;
int traitBytesSent = 0;
// max number of avatarBytes per frame
auto maxAvatarBytesPerFrame = (_maxKbpsPerNode * BYTES_PER_KILOBIT) / AVATAR_MIXER_BROADCAST_FRAMES_PER_SECOND;
@ -326,6 +428,7 @@ void AvatarMixerSlave::broadcastAvatarDataToAgent(const SharedNodePointer& node)
// loop through our sorted avatars and allocate our bandwidth to them accordingly
int remainingAvatars = (int)sortedAvatars.size();
auto traitsPacketList = NLPacketList::create(PacketType::BulkAvatarTraits, QByteArray(), true, true);
while (!sortedAvatars.empty()) {
const auto avatarData = sortedAvatars.top().getAvatar();
sortedAvatars.pop();
@ -392,11 +495,12 @@ void AvatarMixerSlave::broadcastAvatarDataToAgent(const SharedNodePointer& node)
quint64 start = usecTimestampNow();
QByteArray bytes = otherAvatar->toByteArray(detail, lastEncodeForOther, lastSentJointsForOther,
hasFlagsOut, dropFaceTracking, distanceAdjust, viewerPosition, &lastSentJointsForOther);
hasFlagsOut, dropFaceTracking, distanceAdjust, viewerPosition,
&lastSentJointsForOther);
quint64 end = usecTimestampNow();
_stats.toByteArrayElapsedTime += (end - start);
auto maxAvatarDataBytes = avatarPacketList->getMaxSegmentSize() - NUM_BYTES_RFC4122_UUID;
static auto maxAvatarDataBytes = avatarPacketList->getMaxSegmentSize() - NUM_BYTES_RFC4122_UUID;
if (bytes.size() > maxAvatarDataBytes) {
qCWarning(avatars) << "otherAvatar.toByteArray() for" << otherNode->getUUID()
<< "resulted in very large buffer of" << bytes.size() << "bytes - dropping facial data";
@ -445,6 +549,11 @@ void AvatarMixerSlave::broadcastAvatarDataToAgent(const SharedNodePointer& node)
quint64 endAvatarDataPacking = usecTimestampNow();
_stats.avatarDataPackingElapsedTime += (endAvatarDataPacking - startAvatarDataPacking);
// use helper to add any changed traits to our packet list
traitBytesSent += addChangedTraitsToBulkPacket(nodeData, otherNodeData, *traitsPacketList);
traitsPacketList->getDataSize();
}
quint64 startPacketSending = usecTimestampNow();
@ -461,6 +570,14 @@ void AvatarMixerSlave::broadcastAvatarDataToAgent(const SharedNodePointer& node)
// record the bytes sent for other avatar data in the AvatarMixerClientData
nodeData->recordSentAvatarData(numAvatarDataBytes);
// close the current traits packet list
traitsPacketList->closeCurrentPacket();
if (traitsPacketList->getNumPackets() >= 1) {
// send the traits packet list
nodeList->sendPacketList(std::move(traitsPacketList), *node);
}
// record the number of avatars held back this frame
nodeData->recordNumOtherAvatarStarves(numAvatarsHeldBack);
nodeData->recordNumOtherAvatarSkips(numAvatarsWithSkippedFrames);

View file

@ -78,11 +78,16 @@ public:
jobElapsedTime += rhs.jobElapsedTime;
return *this;
}
};
struct SlaveSharedData {
QStringList skeletonURLWhitelist;
QUrl skeletonReplacementURL;
};
class AvatarMixerSlave {
public:
AvatarMixerSlave(SlaveSharedData* sharedData) : _sharedData(sharedData) {};
using ConstIter = NodeList::const_iterator;
void configure(ConstIter begin, ConstIter end);
@ -99,6 +104,10 @@ private:
int sendIdentityPacket(const AvatarMixerClientData* nodeData, const SharedNodePointer& destinationNode);
int sendReplicatedIdentityPacket(const Node& agentNode, const AvatarMixerClientData* nodeData, const Node& destinationNode);
qint64 addChangedTraitsToBulkPacket(AvatarMixerClientData* listeningNodeData,
const AvatarMixerClientData* sendingNodeData,
NLPacketList& traitsPacketList);
void broadcastAvatarDataToAgent(const SharedNodePointer& node);
void broadcastAvatarDataToDownstreamMixer(const SharedNodePointer& node);
@ -111,6 +120,7 @@ private:
float _throttlingRatio { 0.0f };
AvatarMixerSlaveStats _stats;
SlaveSharedData* _sharedData;
};
#endif // hifi_AvatarMixerSlave_h

View file

@ -168,7 +168,7 @@ void AvatarMixerSlavePool::resize(int numThreads) {
if (numThreads > _numThreads) {
// start new slaves
for (int i = 0; i < numThreads - _numThreads; ++i) {
auto slave = new AvatarMixerSlaveThread(*this);
auto slave = new AvatarMixerSlaveThread(*this, _slaveSharedData);
slave->start();
_slaves.emplace_back(slave);
}

View file

@ -32,7 +32,8 @@ class AvatarMixerSlaveThread : public QThread, public AvatarMixerSlave {
using Lock = std::unique_lock<Mutex>;
public:
AvatarMixerSlaveThread(AvatarMixerSlavePool& pool) : _pool(pool) {}
AvatarMixerSlaveThread(AvatarMixerSlavePool& pool, SlaveSharedData* slaveSharedData) :
AvatarMixerSlave(slaveSharedData), _pool(pool) {};
void run() override final;
@ -59,7 +60,8 @@ class AvatarMixerSlavePool {
public:
using ConstIter = NodeList::const_iterator;
AvatarMixerSlavePool(int numThreads = QThread::idealThreadCount()) { setNumThreads(numThreads); }
AvatarMixerSlavePool(SlaveSharedData* slaveSharedData, int numThreads = QThread::idealThreadCount()) :
_slaveSharedData(slaveSharedData) { setNumThreads(numThreads); }
~AvatarMixerSlavePool() { resize(0); }
// Jobs the slave pool can do...
@ -98,6 +100,8 @@ private:
Queue _queue;
ConstIter _begin;
ConstIter _end;
SlaveSharedData* _slaveSharedData;
};
#endif // hifi_AvatarMixerSlavePool_h

View file

@ -1,6 +1,6 @@
//
// ScriptableAvatar.cpp
//
// assignment-client/src/avatars
//
// Created by Clement on 7/22/14.
// Copyright 2014 High Fidelity, Inc.
@ -16,9 +16,13 @@
#include <glm/gtx/transform.hpp>
#include <shared/QtHelpers.h>
#include <GLMHelpers.h>
#include <AnimUtil.h>
#include <ClientTraitsHandler.h>
#include <GLMHelpers.h>
ScriptableAvatar::ScriptableAvatar() {
_clientTraitsHandler = std::unique_ptr<ClientTraitsHandler>(new ClientTraitsHandler(this));
}
QByteArray ScriptableAvatar::toByteArrayStateful(AvatarDataDetail dataDetail, bool dropFaceTracking) {
_globalPosition = getWorldPosition();
@ -61,6 +65,7 @@ AnimationDetails ScriptableAvatar::getAnimationDetails() {
void ScriptableAvatar::setSkeletonModelURL(const QUrl& skeletonModelURL) {
_bind.reset();
_animSkeleton.reset();
AvatarData::setSkeletonModelURL(skeletonModelURL);
}
@ -137,4 +142,6 @@ void ScriptableAvatar::update(float deltatime) {
_animation.clear();
}
}
_clientTraitsHandler->sendChangedTraitsToMixer();
}

View file

@ -1,6 +1,6 @@
//
// ScriptableAvatar.h
//
// assignment-client/src/avatars
//
// Created by Clement on 7/22/14.
// Copyright 2014 High Fidelity, Inc.
@ -123,7 +123,9 @@
class ScriptableAvatar : public AvatarData, public Dependency {
Q_OBJECT
public:
ScriptableAvatar();
/**jsdoc
* @function Avatar.startAnimation
* @param {string} url

View file

@ -24,6 +24,7 @@
#include <NetworkingConstants.h>
#include <AddressManager.h>
#include "../AssignmentDynamicFactory.h"
#include "AssignmentParentFinder.h"
#include "EntityNodeData.h"
#include "EntityServerConsts.h"
@ -42,6 +43,9 @@ EntityServer::EntityServer(ReceivedMessage& message) :
DependencyManager::set<ResourceCacheSharedItems>();
DependencyManager::set<ScriptCache>();
DependencyManager::registerInheritance<EntityDynamicFactoryInterface, AssignmentDynamicFactory>();
DependencyManager::set<AssignmentDynamicFactory>();
auto& packetReceiver = DependencyManager::get<NodeList>()->getPacketReceiver();
packetReceiver.registerListenerForTypes({ PacketType::EntityAdd,
PacketType::EntityClone,
@ -71,6 +75,8 @@ EntityServer::~EntityServer() {
void EntityServer::aboutToFinish() {
DependencyManager::get<ResourceManager>()->cleanup();
DependencyManager::destroy<AssignmentDynamicFactory>();
OctreeServer::aboutToFinish();
}

View file

@ -164,7 +164,7 @@ bool EntityTreeSendThread::traverseTreeAndSendContents(SharedNodePointer node, O
// Send EntityQueryInitialResultsComplete reliable packet ...
auto initialCompletion = NLPacket::create(PacketType::EntityQueryInitialResultsComplete,
sizeof(OCTREE_PACKET_SEQUENCE), true);
initialCompletion->writePrimitive(OCTREE_PACKET_SEQUENCE(nodeData->getSequenceNumber() - 1U));
initialCompletion->writePrimitive(OCTREE_PACKET_SEQUENCE(nodeData->getSequenceNumber()));
DependencyManager::get<NodeList>()->sendPacket(std::move(initialCompletion), *node);
}

View file

@ -24,6 +24,7 @@
#include <plugins/CodecPlugin.h>
#include <plugins/PluginManager.h>
#include <ResourceManager.h>
#include <ResourceScriptingInterface.h>
#include <ScriptCache.h>
#include <ScriptEngines.h>
#include <SoundCacheScriptingInterface.h>
@ -55,7 +56,8 @@ int EntityScriptServer::_entitiesScriptEngineCount = 0;
EntityScriptServer::EntityScriptServer(ReceivedMessage& message) : ThreadedAssignment(message) {
qInstallMessageHandler(messageHandler);
DependencyManager::get<EntityScriptingInterface>()->setPacketSender(&_entityEditSender);
DependencyManager::set<EntityScriptingInterface>(false)->setPacketSender(&_entityEditSender);
DependencyManager::set<ResourceScriptingInterface>();
DependencyManager::set<ResourceManager>();
DependencyManager::set<PluginManager>();
@ -81,9 +83,6 @@ EntityScriptServer::EntityScriptServer(ReceivedMessage& message) : ThreadedAssig
packetReceiver.registerListener(PacketType::SelectedAudioFormat, this, "handleSelectedAudioFormat");
auto avatarHashMap = DependencyManager::set<AvatarHashMap>();
packetReceiver.registerListener(PacketType::BulkAvatarData, avatarHashMap.data(), "processAvatarDataPacket");
packetReceiver.registerListener(PacketType::KillAvatar, avatarHashMap.data(), "processKillAvatar");
packetReceiver.registerListener(PacketType::AvatarIdentity, avatarHashMap.data(), "processAvatarIdentityPacket");
packetReceiver.registerListener(PacketType::ReloadEntityServerScript, this, "handleReloadEntityServerScriptPacket");
packetReceiver.registerListener(PacketType::EntityScriptGetStatus, this, "handleEntityScriptGetStatusPacket");
@ -458,8 +457,11 @@ void EntityScriptServer::resetEntitiesScriptEngine() {
auto newEngineSP = qSharedPointerCast<EntitiesScriptEngineProvider>(newEngine);
DependencyManager::get<EntityScriptingInterface>()->setEntitiesScriptEngine(newEngineSP);
disconnect(_entitiesScriptEngine.data(), &ScriptEngine::entityScriptDetailsUpdated,
this, &EntityScriptServer::updateEntityPPS);
if (_entitiesScriptEngine) {
disconnect(_entitiesScriptEngine.data(), &ScriptEngine::entityScriptDetailsUpdated,
this, &EntityScriptServer::updateEntityPPS);
}
_entitiesScriptEngine.swap(newEngine);
connect(_entitiesScriptEngine.data(), &ScriptEngine::entityScriptDetailsUpdated,
this, &EntityScriptServer::updateEntityPPS);
@ -490,6 +492,21 @@ void EntityScriptServer::shutdownScriptEngine() {
_shuttingDown = true;
clear(); // always clear() on shutdown
auto scriptEngines = DependencyManager::get<ScriptEngines>();
scriptEngines->shutdownScripting();
_entitiesScriptEngine.clear();
auto entityScriptingInterface = DependencyManager::get<EntityScriptingInterface>();
// our entity tree is going to go away so tell that to the EntityScriptingInterface
entityScriptingInterface->setEntityTree(nullptr);
// Should always be true as they are singletons.
if (entityScriptingInterface->getPacketSender() == &_entityEditSender) {
// The packet sender is about to go away.
entityScriptingInterface->setPacketSender(nullptr);
}
}
void EntityScriptServer::addingEntity(const EntityItemID& entityID) {
@ -562,24 +579,18 @@ void EntityScriptServer::handleOctreePacket(QSharedPointer<ReceivedMessage> mess
void EntityScriptServer::aboutToFinish() {
shutdownScriptEngine();
auto entityScriptingInterface = DependencyManager::get<EntityScriptingInterface>();
// our entity tree is going to go away so tell that to the EntityScriptingInterface
entityScriptingInterface->setEntityTree(nullptr);
// Should always be true as they are singletons.
if (entityScriptingInterface->getPacketSender() == &_entityEditSender) {
// The packet sender is about to go away.
entityScriptingInterface->setPacketSender(nullptr);
}
DependencyManager::destroy<AssignmentParentFinder>();
DependencyManager::get<ResourceManager>()->cleanup();
DependencyManager::destroy<PluginManager>();
DependencyManager::destroy<ResourceScriptingInterface>();
DependencyManager::destroy<EntityScriptingInterface>();
// cleanup the AudioInjectorManager (and any still running injectors)
DependencyManager::destroy<AudioInjectorManager>();
DependencyManager::destroy<ScriptEngines>();
DependencyManager::destroy<EntityScriptServerServices>();

View file

@ -103,7 +103,7 @@ FocusScope {
verticalCenter: parent.verticalCenter
}
size: hifi.fontSizes.textFieldInput
text: comboBox.currentText
text: comboBox.displayText ? comboBox.displayText : comboBox.currentText
elide: Text.ElideRight
color: comboBox.hovered || comboBox.popup.visible ? hifi.colors.baseGray : (isLightColorScheme ? hifi.colors.lightGray : hifi.colors.lightGrayText )
}

View file

@ -21,6 +21,7 @@ SpinBox {
id: hifi
}
inputMethodHints: Qt.ImhFormattedNumbersOnly
property int colorScheme: hifi.colorSchemes.light
readonly property bool isLightColorScheme: colorScheme === hifi.colorSchemes.light
property string label: ""
@ -91,7 +92,6 @@ SpinBox {
}
valueFromText: function(text, locale) {
spinBox.value = 0; // Force valueChanged signal to be emitted so that validator fires.
return Number.fromLocaleString(locale, text)*factor;
}
@ -104,6 +104,8 @@ SpinBox {
selectedTextColor: hifi.colors.black
selectionColor: hifi.colors.primaryHighlight
text: spinBox.textFromValue(spinBox.value, spinBox.locale) + suffix
inputMethodHints: spinBox.inputMethodHints
validator: spinBox.validator
verticalAlignment: Qt.AlignVCenter
leftPadding: spinBoxLabelInside.visible ? 30 : hifi.dimensions.textPadding
//rightPadding: hifi.dimensions.spinnerSize

View file

@ -57,7 +57,7 @@ Rectangle {
try {
var marketResponse = JSON.parse(xmlhttp.responseText.trim())
if(marketResponse.status === 'success') {
if (marketResponse.status === 'success') {
avatar.modelName = marketResponse.data.title;
}
}
@ -72,14 +72,14 @@ Rectangle {
function getAvatarModelName() {
if(currentAvatar === null) {
if (currentAvatar === null) {
return '';
}
if(currentAvatar.modelName !== undefined) {
if (currentAvatar.modelName !== undefined) {
return currentAvatar.modelName;
} else {
var marketId = allAvatars.extractMarketId(currentAvatar.avatarUrl);
if(marketId !== '') {
if (marketId !== '') {
fetchAvatarModelName(marketId, currentAvatar);
}
}
@ -103,51 +103,51 @@ Rectangle {
property url externalAvatarThumbnailUrl: '../../images/avatarapp/guy-in-circle.svg'
function fromScript(message) {
if(message.method === 'initialize') {
if (message.method === 'initialize') {
jointNames = message.data.jointNames;
emitSendToScript({'method' : getAvatarsMethod});
} else if(message.method === 'wearableUpdated') {
} else if (message.method === 'wearableUpdated') {
adjustWearables.refreshWearable(message.entityID, message.wearableIndex, message.properties, message.updateUI);
} else if(message.method === 'wearablesUpdated') {
} else if (message.method === 'wearablesUpdated') {
var wearablesModel = currentAvatar.wearables;
wearablesModel.clear();
message.wearables.forEach(function(wearable) {
wearablesModel.append(wearable);
});
adjustWearables.refresh(currentAvatar);
} else if(message.method === 'scaleChanged') {
} else if (message.method === 'scaleChanged') {
currentAvatar.avatarScale = message.value;
updateCurrentAvatarInBookmarks(currentAvatar);
} else if(message.method === 'externalAvatarApplied') {
} else if (message.method === 'externalAvatarApplied') {
currentAvatar.avatarUrl = message.avatarURL;
currentAvatar.thumbnailUrl = allAvatars.makeThumbnailUrl(message.avatarURL);
currentAvatar.entry.avatarUrl = currentAvatar.avatarUrl;
currentAvatar.modelName = undefined;
updateCurrentAvatarInBookmarks(currentAvatar);
} else if(message.method === 'settingChanged') {
} else if (message.method === 'settingChanged') {
currentAvatarSettings[message.name] = message.value;
} else if(message.method === 'changeSettings') {
} else if (message.method === 'changeSettings') {
currentAvatarSettings = message.settings;
} else if(message.method === 'bookmarkLoaded') {
} else if (message.method === 'bookmarkLoaded') {
setCurrentAvatar(message.data.currentAvatar, message.data.name);
var avatarIndex = allAvatars.findAvatarIndex(currentAvatar.name);
allAvatars.move(avatarIndex, 0, 1);
view.setPage(0);
} else if(message.method === 'bookmarkAdded') {
} else if (message.method === 'bookmarkAdded') {
var avatar = allAvatars.findAvatar(message.bookmarkName);
if(avatar !== undefined) {
if (avatar !== undefined) {
var avatarObject = allAvatars.makeAvatarObject(message.bookmark, message.bookmarkName);
for(var prop in avatarObject) {
avatar[prop] = avatarObject[prop];
}
if(currentAvatar.name === message.bookmarkName) {
if (currentAvatar.name === message.bookmarkName) {
currentAvatar = currentAvatarModel.makeAvatarEntry(avatarObject);
}
} else {
allAvatars.addAvatarEntry(message.bookmark, message.bookmarkName);
}
updateCurrentAvatarInBookmarks(currentAvatar);
} else if(message.method === 'bookmarkDeleted') {
} else if (message.method === 'bookmarkDeleted') {
pageOfAvatars.isUpdating = true;
var index = pageOfAvatars.findAvatarIndex(message.name);
@ -159,15 +159,16 @@ Rectangle {
var itemsOnPage = pageOfAvatars.count;
var newItemIndex = view.currentPage * view.itemsPerPage + itemsOnPage;
if(newItemIndex <= (allAvatars.count - 1)) {
if (newItemIndex <= (allAvatars.count - 1)) {
pageOfAvatars.append(allAvatars.get(newItemIndex));
} else {
if(!pageOfAvatars.hasGetAvatars())
if (!pageOfAvatars.hasGetAvatars()) {
pageOfAvatars.appendGetAvatars();
}
}
pageOfAvatars.isUpdating = false;
} else if(message.method === getAvatarsMethod) {
} else if (message.method === getAvatarsMethod) {
var getAvatarsData = message.data;
allAvatars.populate(getAvatarsData.bookmarks);
setCurrentAvatar(getAvatarsData.currentAvatar, '');
@ -175,16 +176,16 @@ Rectangle {
currentAvatarSettings = getAvatarsData.currentAvatarSettings;
updateCurrentAvatarInBookmarks(currentAvatar);
} else if(message.method === 'updateAvatarInBookmarks') {
} else if (message.method === 'updateAvatarInBookmarks') {
updateCurrentAvatarInBookmarks(currentAvatar);
} else if(message.method === 'selectAvatarEntity') {
} else if (message.method === 'selectAvatarEntity') {
adjustWearables.selectWearableByID(message.entityID);
}
}
function updateCurrentAvatarInBookmarks(avatar) {
var bookmarkAvatarIndex = allAvatars.findAvatarIndexByValue(avatar);
if(bookmarkAvatarIndex === -1) {
if (bookmarkAvatarIndex === -1) {
avatar.name = '';
view.setPage(0);
} else {
@ -285,6 +286,9 @@ Rectangle {
onWearableSelected: {
emitSendToScript({'method' : 'selectWearable', 'entityID' : id});
}
onAddWearable: {
emitSendToScript({'method' : 'addWearable', 'avatarName' : avatarName, 'url' : url});
}
z: 3
}
@ -491,33 +495,10 @@ Rectangle {
anchors.verticalCenter: wearablesLabel.verticalCenter
glyphText: "\ue02e"
visible: avatarWearablesCount !== 0
onClicked: {
adjustWearables.open(currentAvatar);
}
}
// TextStyle3
RalewayRegular {
size: 22;
anchors.right: parent.right
anchors.verticalCenter: wearablesLabel.verticalCenter
font.underline: true
text: "Add"
color: 'black'
visible: avatarWearablesCount === 0
MouseArea {
anchors.fill: parent
onClicked: {
popup.showGetWearables(function() {
emitSendToScript({'method' : 'navigate', 'url' : 'hifi://AvatarIsland/11.5848,-8.10862,-2.80195'})
}, function(link) {
emitSendToScript({'method' : 'navigate', 'url' : link})
});
}
}
}
}
Rectangle {
@ -617,8 +598,9 @@ Rectangle {
pageOfAvatars.append(avatarItem);
}
if(pageOfAvatars.count !== itemsPerPage)
if (pageOfAvatars.count !== itemsPerPage) {
pageOfAvatars.appendGetAvatars();
}
currentPage = pageIndex;
pageOfAvatars.isUpdating = false;
@ -639,7 +621,7 @@ Rectangle {
}
function removeGetAvatars() {
if(hasGetAvatars()) {
if (hasGetAvatars()) {
remove(count - 1)
}
}
@ -707,13 +689,13 @@ Rectangle {
hoverEnabled: enabled
onClicked: {
if(isInManageState) {
if (isInManageState) {
var currentItem = delegateRoot.GridView.view.model.get(index);
popup.showDeleteFavorite(currentItem.name, function() {
view.deleteAvatar(currentItem);
});
} else {
if(delegateRoot.GridView.view.currentIndex !== index) {
if (delegateRoot.GridView.view.currentIndex !== index) {
var currentItem = delegateRoot.GridView.view.model.get(index);
popup.showLoadFavorite(currentItem.name, function() {
view.selectAvatar(currentItem);

View file

@ -1,5 +1,6 @@
import Hifi 1.0 as Hifi
import QtQuick 2.5
import QtQuick.Layouts 1.3
import "../../styles-uit"
import "../../controls-uit" as HifiControlsUit
import "../../controls" as HifiControls
@ -17,6 +18,7 @@ Rectangle {
signal adjustWearablesOpened(var avatarName);
signal adjustWearablesClosed(bool status, var avatarName);
signal addWearable(string avatarName, string url);
property bool modified: false;
Component.onCompleted: {
@ -30,6 +32,7 @@ Rectangle {
function open(avatar) {
adjustWearablesOpened(avatar.name);
modified = false;
visible = true;
avatarName = avatar.name;
wearablesModel = avatar.wearables;
@ -38,43 +41,47 @@ Rectangle {
function refresh(avatar) {
wearablesCombobox.model.clear();
for(var i = 0; i < avatar.wearables.count; ++i) {
wearablesCombobox.currentIndex = -1;
for (var i = 0; i < avatar.wearables.count; ++i) {
var wearable = avatar.wearables.get(i).properties;
for(var j = (wearable.modelURL.length - 1); j >= 0; --j) {
if(wearable.modelURL[j] === '/') {
wearable.text = wearable.modelURL.substring(j + 1) + ' [%jointIndex%]'.replace('%jointIndex%', jointNames[wearable.parentJointIndex]);
for (var j = (wearable.modelURL.length - 1); j >= 0; --j) {
if (wearable.modelURL[j] === '/') {
wearable.text = wearable.modelURL.substring(j + 1);
break;
}
}
wearablesCombobox.model.append(wearable);
}
wearablesCombobox.currentIndex = 0;
if (wearablesCombobox.model.count !== 0) {
wearablesCombobox.currentIndex = 0;
}
}
function refreshWearable(wearableID, wearableIndex, properties, updateUI) {
if(wearableIndex === -1) {
if (wearableIndex === -1) {
wearableIndex = wearablesCombobox.model.findIndexById(wearableID);
}
var wearable = wearablesCombobox.model.get(wearableIndex);
if(!wearable) {
if (!wearable) {
return;
}
var wearableModelItemProperties = wearablesModel.get(wearableIndex).properties;
for(var prop in properties) {
for (var prop in properties) {
wearable[prop] = properties[prop];
wearableModelItemProperties[prop] = wearable[prop];
if(updateUI) {
if(prop === 'localPosition') {
position.set(wearable[prop]);
} else if(prop === 'localRotationAngles') {
rotation.set(wearable[prop]);
} else if(prop === 'dimensions') {
if (updateUI) {
if (prop === 'localPosition') {
positionVector.set(wearable[prop]);
} else if (prop === 'localRotationAngles') {
rotationVector.set(wearable[prop]);
} else if (prop === 'dimensions') {
scalespinner.set(wearable[prop].x / wearable.naturalDimensions.x);
}
}
@ -84,13 +91,13 @@ Rectangle {
}
function getCurrentWearable() {
return wearablesCombobox.model.get(wearablesCombobox.currentIndex)
return wearablesCombobox.currentIndex !== -1 ? wearablesCombobox.model.get(wearablesCombobox.currentIndex) : null;
}
function selectWearableByID(entityID) {
for(var i = 0; i < wearablesCombobox.model.count; ++i) {
for (var i = 0; i < wearablesCombobox.model.count; ++i) {
var wearable = wearablesCombobox.model.get(i);
if(wearable.id === entityID) {
if (wearable.id === entityID) {
wearablesCombobox.currentIndex = i;
break;
}
@ -115,72 +122,216 @@ Rectangle {
Column {
anchors.top: parent.top
anchors.topMargin: 15
anchors.topMargin: 12
anchors.horizontalCenter: parent.horizontalCenter
spacing: 20
width: parent.width - 30 * 2
width: parent.width - 22 * 2
HifiControlsUit.ComboBox {
id: wearablesCombobox
anchors.left: parent.left
anchors.right: parent.right
comboBox.textRole: "text"
Column {
width: parent.width
model: ListModel {
function findIndexById(id) {
Rectangle {
color: hifi.colors.orangeHighlight
anchors.left: parent.left
anchors.right: parent.right
height: 70
visible: HMD.active
for(var i = 0; i < count; ++i) {
var wearable = get(i);
if(wearable.id === id) {
return i;
}
}
return -1;
RalewayRegular {
anchors.fill: parent
anchors.leftMargin: 18
anchors.rightMargin: 18
size: 20;
lineHeightMode: Text.FixedHeight
lineHeight: 23;
text: "Tip: You can use hand controllers to grab and adjust your wearables"
wrapMode: Text.WordWrap
}
}
comboBox.onCurrentIndexChanged: {
var currentWearable = getCurrentWearable();
Rectangle {
anchors.left: parent.left
anchors.right: parent.right
height: 12 // spacing
visible: HMD.active
}
if(currentWearable) {
position.set(currentWearable.localPosition);
rotation.set(currentWearable.localRotationAngles);
scalespinner.set(currentWearable.dimensions.x / currentWearable.naturalDimensions.x)
RowLayout {
anchors.left: parent.left
anchors.right: parent.right
wearableSelected(currentWearable.id);
RalewayBold {
size: 15;
lineHeightMode: Text.FixedHeight
lineHeight: 18;
text: "Wearable"
anchors.verticalCenter: parent.verticalCenter
}
spacing: 10
RalewayBold {
size: 15;
lineHeightMode: Text.FixedHeight
lineHeight: 18;
text: "<a href='#'>Get more</a>"
linkColor: hifi.colors.blueHighlight
anchors.verticalCenter: parent.verticalCenter
onLinkActivated: {
popup.showGetWearables(function() {
emitSendToScript({'method' : 'navigate', 'url' : 'hifi://AvatarIsland/11.5848,-8.10862,-2.80195'})
}, function(link) {
emitSendToScript({'method' : 'navigate', 'url' : link})
});
}
}
Rectangle {
Layout.fillWidth: true
Layout.fillHeight: true
RalewayBold {
size: 15;
lineHeightMode: Text.FixedHeight
lineHeight: 18;
text: "<a href='#'>Add custom</a>"
linkColor: hifi.colors.blueHighlight
anchors.verticalCenter: parent.verticalCenter
anchors.right: parent.right
onLinkActivated: {
popup.showSpecifyWearableUrl(function(url) {
console.debug('popup.showSpecifyWearableUrl: ', url);
addWearable(root.avatarName, url);
modified = true;
});
}
}
}
}
HifiControlsUit.ComboBox {
id: wearablesCombobox
anchors.left: parent.left
anchors.right: parent.right
enabled: getCurrentWearable() !== null
comboBox.textRole: "text"
model: ListModel {
function findIndexById(id) {
for (var i = 0; i < count; ++i) {
var wearable = get(i);
if (wearable.id === id) {
return i;
}
}
return -1;
}
}
comboBox.onCurrentIndexChanged: {
var currentWearable = getCurrentWearable();
var position = currentWearable ? currentWearable.localPosition : { x : 0, y : 0, z : 0 };
var rotation = currentWearable ? currentWearable.localRotationAngles : { x : 0, y : 0, z : 0 };
var scale = currentWearable ? currentWearable.dimensions.x / currentWearable.naturalDimensions.x : 1.0;
var joint = currentWearable ? currentWearable.parentJointIndex : -1;
var soft = currentWearable ? currentWearable.relayParentJoints : false;
positionVector.set(position);
rotationVector.set(rotation);
scalespinner.set(scale);
jointsCombobox.set(joint);
isSoft.set(soft);
if (currentWearable) {
wearableSelected(currentWearable.id);
}
}
}
}
Column {
width: parent.width
RalewayBold {
size: 15;
lineHeightMode: Text.FixedHeight
lineHeight: 18;
text: "Joint"
}
HifiControlsUit.ComboBox {
id: jointsCombobox
anchors.left: parent.left
anchors.right: parent.right
enabled: getCurrentWearable() !== null && !isSoft.checked
comboBox.displayText: isSoft.checked ? 'Hips' : comboBox.currentText
model: jointNames
property bool notify: false
function set(jointIndex) {
notify = false;
currentIndex = jointIndex;
notify = true;
}
function notifyJointChanged() {
modified = true;
var properties = {
parentJointIndex: currentIndex,
localPosition: {
x: positionVector.xvalue,
y: positionVector.yvalue,
z: positionVector.zvalue
},
localRotationAngles: {
x: rotationVector.xvalue,
y: rotationVector.yvalue,
z: rotationVector.zvalue,
}
};
wearableUpdated(getCurrentWearable().id, wearablesCombobox.currentIndex, properties);
}
onCurrentIndexChanged: {
if (notify) notifyJointChanged();
}
}
}
Column {
width: parent.width
spacing: 5
Row {
spacing: 20
// TextStyle5
FiraSansSemiBold {
RalewayBold {
id: positionLabel
size: 22;
size: 15;
lineHeightMode: Text.FixedHeight
lineHeight: 18;
text: "Position"
}
// TextStyle7
FiraSansRegular {
size: 18;
RalewayBold {
size: 15;
lineHeightMode: Text.FixedHeight
lineHeight: 16.9;
lineHeight: 18;
text: "m"
anchors.verticalCenter: positionLabel.verticalCenter
}
}
Vector3 {
id: position
id: positionVector
backgroundColor: "lightgray"
enabled: getCurrentWearable() !== null
function set(localPosition) {
notify = false;
@ -201,9 +352,9 @@ Rectangle {
property bool notify: false;
onXvalueChanged: if(notify) notifyPositionChanged();
onYvalueChanged: if(notify) notifyPositionChanged();
onZvalueChanged: if(notify) notifyPositionChanged();
onXvalueChanged: if (notify) notifyPositionChanged();
onYvalueChanged: if (notify) notifyPositionChanged();
onZvalueChanged: if (notify) notifyPositionChanged();
decimals: 2
realFrom: -10
@ -214,31 +365,33 @@ Rectangle {
Column {
width: parent.width
spacing: 5
Row {
spacing: 20
// TextStyle5
FiraSansSemiBold {
RalewayBold {
id: rotationLabel
size: 22;
size: 15;
lineHeightMode: Text.FixedHeight
lineHeight: 18;
text: "Rotation"
}
// TextStyle7
FiraSansRegular {
size: 18;
RalewayBold {
size: 15;
lineHeightMode: Text.FixedHeight
lineHeight: 16.9;
lineHeight: 18;
text: "deg"
anchors.verticalCenter: rotationLabel.verticalCenter
}
}
Vector3 {
id: rotation
id: rotationVector
backgroundColor: "lightgray"
enabled: getCurrentWearable() !== null
function set(localRotationAngles) {
notify = false;
@ -259,9 +412,9 @@ Rectangle {
property bool notify: false;
onXvalueChanged: if(notify) notifyRotationChanged();
onYvalueChanged: if(notify) notifyRotationChanged();
onZvalueChanged: if(notify) notifyRotationChanged();
onXvalueChanged: if (notify) notifyRotationChanged();
onYvalueChanged: if (notify) notifyRotationChanged();
onZvalueChanged: if (notify) notifyRotationChanged();
decimals: 0
realFrom: -180
@ -270,33 +423,66 @@ Rectangle {
}
}
Column {
Item {
width: parent.width
spacing: 5
height: childrenRect.height
// TextStyle5
FiraSansSemiBold {
size: 22;
text: "Scale"
HifiControlsUit.CheckBox {
id: isSoft
enabled: getCurrentWearable() !== null
text: "Is soft"
labelFontSize: 15
labelFontWeight: Font.Bold
color: Qt.black
y: scalespinner.y
function set(value) {
notify = false;
checked = value
notify = true;
}
function notifyIsSoftChanged() {
modified = true;
var properties = {
relayParentJoints: checked
};
wearableUpdated(getCurrentWearable().id, wearablesCombobox.currentIndex, properties);
}
property bool notify: false;
onCheckedChanged: if (notify) notifyIsSoftChanged();
}
Item {
width: parent.width
height: childrenRect.height
Column {
id: scalesColumn
anchors.right: parent.right
// TextStyle5
RalewayBold {
id: scaleLabel
size: 15;
lineHeightMode: Text.FixedHeight
lineHeight: 18;
text: "Scale"
}
HifiControlsUit.SpinBox {
id: scalespinner
enabled: getCurrentWearable() !== null
decimals: 2
realStepSize: 0.1
realFrom: 0.1
realTo: 3.0
realValue: 1.0
backgroundColor: "lightgray"
width: position.spinboxWidth
width: positionVector.spinboxWidth
colorScheme: hifi.colorSchemes.light
property bool notify: false;
onValueChanged: if(notify) notifyScaleChanged();
onRealValueChanged: if (notify) notifyScaleChanged();
function set(value) {
notify = false;
@ -320,26 +506,34 @@ Rectangle {
wearableUpdated(currentWearable.id, wearablesCombobox.currentIndex, properties);
}
}
HifiControlsUit.Button {
fontSize: 18
height: 40
anchors.right: parent.right
color: hifi.buttons.red;
colorScheme: hifi.colorSchemes.light;
text: "TAKE IT OFF"
onClicked: wearableDeleted(root.avatarName, getCurrentWearable().id);
enabled: wearablesCombobox.model.count !== 0
anchors.verticalCenter: scalespinner.verticalCenter
}
}
}
Column {
width: parent.width
HifiControlsUit.Button {
fontSize: 18
height: 40
width: scalespinner.width
anchors.right: parent.right
color: hifi.buttons.red;
colorScheme: hifi.colorSchemes.light;
text: "TAKE IT OFF"
onClicked: {
modified = true;
wearableDeleted(root.avatarName, getCurrentWearable().id);
}
enabled: wearablesCombobox.model.count !== 0
}
}
}
DialogButtons {
yesButton.enabled: modified
anchors.bottom: parent.bottom
anchors.bottomMargin: 30
anchors.bottomMargin: 57
anchors.left: parent.left
anchors.leftMargin: 30
anchors.right: parent.right

View file

@ -24,8 +24,9 @@ ListModel {
function makeThumbnailUrl(avatarUrl) {
var marketId = extractMarketId(avatarUrl);
if(marketId === '')
if (marketId === '') {
return '';
}
var avatarThumbnailUrl = "https://hifi-metaverse.s3-us-west-1.amazonaws.com/marketplace/previews/%marketId%/large/hifi-mp-%marketId%.jpg"
.split('%marketId%').join(marketId);
@ -42,7 +43,6 @@ ListModel {
'thumbnailUrl' : avatarThumbnailUrl,
'avatarUrl' : avatar.avatarUrl,
'wearables' : avatar.avatarEntites ? avatar.avatarEntites : [],
'attachments' : avatar.attachments ? avatar.attachments : [],
'entry' : avatar,
'getMoreAvatars' : false
};
@ -58,7 +58,7 @@ ListModel {
function populate(bookmarks) {
clear();
for(var avatarName in bookmarks) {
for (var avatarName in bookmarks) {
var avatar = bookmarks[avatarName];
var avatarEntry = makeAvatarObject(avatar, avatarName);
@ -67,19 +67,19 @@ ListModel {
}
function arraysAreEqual(a1, a2, comparer) {
if(Array.isArray(a1) && Array.isArray(a2)) {
if(a1.length !== a2.length) {
if (Array.isArray(a1) && Array.isArray(a2)) {
if (a1.length !== a2.length) {
return false;
}
for(var i = 0; i < a1.length; ++i) {
if(!comparer(a1[i], a2[i])) {
for (var i = 0; i < a1.length; ++i) {
if (!comparer(a1[i], a2[i])) {
return false;
}
}
} else if(Array.isArray(a1)) {
} else if (Array.isArray(a1)) {
return a1.length === 0;
} else if(Array.isArray(a2)) {
} else if (Array.isArray(a2)) {
return a2.length === 0;
}
@ -87,26 +87,26 @@ ListModel {
}
function modelsAreEqual(m1, m2, comparer) {
if(m1.count !== m2.count) {
if (m1.count !== m2.count) {
return false;
}
for(var i = 0; i < m1.count; ++i) {
for (var i = 0; i < m1.count; ++i) {
var e1 = m1.get(i);
var allDifferent = true;
// it turns out order of wearables can randomly change so make position-independent comparison here
for(var j = 0; j < m2.count; ++j) {
for (var j = 0; j < m2.count; ++j) {
var e2 = m2.get(j);
if(comparer(e1, e2)) {
if (comparer(e1, e2)) {
allDifferent = false;
break;
}
}
if(allDifferent) {
if (allDifferent) {
return false;
}
}
@ -115,18 +115,20 @@ ListModel {
}
function compareNumericObjects(o1, o2) {
if(o1 === undefined && o2 !== undefined)
if (o1 === undefined && o2 !== undefined) {
return false;
if(o1 !== undefined && o2 === undefined)
}
if (o1 !== undefined && o2 === undefined) {
return false;
}
for(var prop in o1) {
if(o1.hasOwnProperty(prop) && o2.hasOwnProperty(prop)) {
for (var prop in o1) {
if (o1.hasOwnProperty(prop) && o2.hasOwnProperty(prop)) {
var v1 = o1[prop];
var v2 = o2[prop];
if(v1 !== v2 && Math.round(v1 * 500) != Math.round(v2 * 500)) {
if (v1 !== v2 && Math.round(v1 * 500) != Math.round(v2 * 500)) {
return false;
}
}
@ -136,7 +138,7 @@ ListModel {
}
function compareObjects(o1, o2, props, arrayProp) {
for(var i = 0; i < props.length; ++i) {
for (var i = 0; i < props.length; ++i) {
var prop = props[i];
var propertyName = prop.propertyName;
var comparer = prop.comparer;
@ -144,12 +146,12 @@ ListModel {
var o1Value = arrayProp ? o1[arrayProp][propertyName] : o1[propertyName];
var o2Value = arrayProp ? o2[arrayProp][propertyName] : o2[propertyName];
if(comparer) {
if(comparer(o1Value, o2Value) === false) {
if (comparer) {
if (comparer(o1Value, o2Value) === false) {
return false;
}
} else {
if(JSON.stringify(o1Value) !== JSON.stringify(o2Value)) {
if (JSON.stringify(o1Value) !== JSON.stringify(o2Value)) {
return false;
}
}
@ -164,42 +166,34 @@ ListModel {
{'propertyName' : 'marketplaceID'},
{'propertyName' : 'itemName'},
{'propertyName' : 'script'},
{'propertyName' : 'relayParentJoints'},
{'propertyName' : 'localPosition', 'comparer' : compareNumericObjects},
{'propertyName' : 'localRotationAngles', 'comparer' : compareNumericObjects},
{'propertyName' : 'dimensions', 'comparer' : compareNumericObjects}], 'properties')
}
function compareAttachments(a1, a2) {
return compareObjects(a1, a2, [{'propertyName' : 'position', 'comparer' : compareNumericObjects},
{'propertyName' : 'orientation'},
{'propertyName' : 'parentJointIndex'},
{'propertyName' : 'modelurl'}])
}
function findAvatarIndexByValue(avatar) {
var index = -1;
// 2DO: find better way of determining selected avatar in bookmarks
for(var i = 0; i < allAvatars.count; ++i) {
for (var i = 0; i < allAvatars.count; ++i) {
var thesame = true;
var bookmarkedAvatar = allAvatars.get(i);
if(bookmarkedAvatar.avatarUrl !== avatar.avatarUrl)
continue;
if(bookmarkedAvatar.avatarScale !== avatar.avatarScale)
continue;
if(!modelsAreEqual(bookmarkedAvatar.attachments, avatar.attachments, compareAttachments)) {
if (bookmarkedAvatar.avatarUrl !== avatar.avatarUrl) {
continue;
}
if(!modelsAreEqual(bookmarkedAvatar.wearables, avatar.wearables, compareWearables)) {
if (bookmarkedAvatar.avatarScale !== avatar.avatarScale) {
continue;
}
if(thesame) {
if (!modelsAreEqual(bookmarkedAvatar.wearables, avatar.wearables, compareWearables)) {
continue;
}
if (thesame) {
index = i;
break;
}
@ -209,8 +203,8 @@ ListModel {
}
function findAvatarIndex(avatarName) {
for(var i = 0; i < count; ++i) {
if(get(i).name === avatarName) {
for (var i = 0; i < count; ++i) {
if (get(i).name === avatarName) {
return i;
}
}
@ -219,8 +213,9 @@ ListModel {
function findAvatar(avatarName) {
var avatarIndex = findAvatarIndex(avatarName);
if(avatarIndex === -1)
if (avatarIndex === -1) {
return undefined;
}
return get(avatarIndex);
}

View file

@ -125,9 +125,9 @@ Rectangle {
size: 15
color: 'red'
visible: {
for(var i = 0; i < avatars.count; ++i) {
for (var i = 0; i < avatars.count; ++i) {
var avatarName = avatars.get(i).name;
if(avatarName === favoriteName.text) {
if (avatarName === favoriteName.text) {
return true;
}
}
@ -165,7 +165,7 @@ Rectangle {
}
onYesClicked: function() {
if(onSaveClicked) {
if (onSaveClicked) {
onSaveClicked();
} else {
root.close();
@ -173,7 +173,7 @@ Rectangle {
}
onNoClicked: function() {
if(onCancelClicked) {
if (onCancelClicked) {
onCancelClicked();
} else {
root.close();

View file

@ -33,7 +33,7 @@ Row {
onClicked: {
console.debug('whitebutton.clicked', onNoClicked);
if(onNoClicked) {
if (onNoClicked) {
onNoClicked();
}
}

View file

@ -128,7 +128,7 @@ Rectangle {
wrapMode: Text.WordWrap;
onLinkActivated: {
if(onLinkClicked)
if (onLinkClicked)
onLinkClicked(link);
}
}
@ -166,7 +166,7 @@ Rectangle {
noText: root.button1text
onYesClicked: function() {
if(onButton2Clicked) {
if (onButton2Clicked) {
onButton2Clicked();
} else {
root.close();
@ -174,7 +174,7 @@ Rectangle {
}
onNoClicked: function() {
if(onButton1Clicked) {
if (onButton1Clicked) {
onButton1Clicked();
} else {
root.close();

View file

@ -18,15 +18,38 @@ MessageBox {
popup.button2text = 'CONFIRM';
popup.onButton2Clicked = function() {
if(callback)
if (callback) {
callback();
}
popup.close();
}
popup.onLinkClicked = function(link) {
if(linkCallback)
if (linkCallback) {
linkCallback(link);
}
}
popup.open();
popup.inputText.forceActiveFocus();
}
function showSpecifyWearableUrl(callback) {
popup.button2text = 'CONFIRM'
popup.button1text = 'CANCEL'
popup.titleText = 'Specify Wearable URL'
popup.bodyText = 'If you want to add a custom wearable, you can specify the URL of the wearable file here.'
popup.inputText.visible = true;
popup.inputText.placeholderText = 'Enter Wearable URL';
popup.onButton2Clicked = function() {
if (callback) {
callback(popup.inputText.text);
}
popup.close();
}
popup.open();
@ -41,22 +64,24 @@ MessageBox {
popup.button1text = 'CANCEL'
popup.titleText = 'Get Wearables'
popup.bodyText = 'Buy wearables from <b><a href="app://marketplace">Marketplace.</a></b>' + '<br/>' +
'Use wearables in <b><a href="app://purchases">My Purchases.</a></b>' + '<br/>' + '<br/>' +
'Visit “AvatarIsland” to get wearables.'
'Wear wearable from <b><a href="app://purchases">My Purchases.</a></b>' + '<br/>' + '<br/>' +
'Visit “AvatarIsland” to get wearables'
popup.imageSource = getWearablesUrl;
popup.onButton2Clicked = function() {
popup.close();
if(callback)
if (callback) {
callback();
}
}
popup.onLinkClicked = function(link) {
popup.close();
if(linkCallback)
if (linkCallback) {
linkCallback(link);
}
}
popup.open();
@ -72,8 +97,9 @@ MessageBox {
popup.onButton2Clicked = function() {
popup.close();
if(callback)
if (callback) {
callback();
}
}
popup.open();
}
@ -87,8 +113,9 @@ MessageBox {
popup.onButton2Clicked = function() {
popup.close();
if(callback)
if (callback) {
callback();
}
}
popup.open();
}
@ -109,15 +136,17 @@ MessageBox {
popup.onButton2Clicked = function() {
popup.close();
if(callback)
if (callback) {
callback();
}
}
popup.onLinkClicked = function(link) {
popup.close();
if(linkCallback)
if (linkCallback) {
linkCallback(link);
}
}
popup.open();

View file

@ -33,13 +33,13 @@ Rectangle {
scaleSlider.value = Math.round(avatarScale * 10);
scaleSlider.notify = true;;
if(settings.dominantHand === 'left') {
if (settings.dominantHand === 'left') {
leftHandRadioButton.checked = true;
} else {
rightHandRadioButton.checked = true;
}
if(settings.collisionsEnabled) {
if (settings.collisionsEnabled) {
collisionsEnabledRadiobutton.checked = true;
} else {
collisionsDisabledRadioButton.checked = true;
@ -113,7 +113,7 @@ Rectangle {
onValueChanged: {
console.debug('value changed: ', value);
if(notify) {
if (notify) {
console.debug('notifying.. ');
root.scaleChanged(value / 10);
}

View file

@ -32,7 +32,7 @@ Item {
highp vec4 maskColor = texture2D(mask, vec2(qt_TexCoord0.x, qt_TexCoord0.y));
highp vec4 sourceColor = texture2D(source, vec2(qt_TexCoord0.x, qt_TexCoord0.y));
if(maskColor.a > 0.0)
if (maskColor.a > 0.0)
gl_FragColor = sourceColor;
else
gl_FragColor = maskColor;

View file

@ -20,6 +20,7 @@ Row {
spacing: spinboxSpace
property bool enabled: false;
property alias xvalue: xspinner.realValue
property alias yvalue: yspinner.realValue
property alias zvalue: zspinner.realValue
@ -35,6 +36,7 @@ Row {
realFrom: root.realFrom
realTo: root.realTo
realStepSize: root.realStepSize
enabled: root.enabled
}
HifiControlsUit.SpinBox {
@ -48,6 +50,7 @@ Row {
realFrom: root.realFrom
realTo: root.realTo
realStepSize: root.realStepSize
enabled: root.enabled
}
HifiControlsUit.SpinBox {
@ -61,5 +64,6 @@ Row {
realFrom: root.realFrom
realTo: root.realTo
realStepSize: root.realStepSize
enabled: root.enabled
}
}

View file

@ -1,48 +0,0 @@
//
// AttachmentsDialog.qml
//
// Created by David Rowe on 9 Mar 2017.
// Copyright 2017 High Fidelity, Inc.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or https://www.apache.org/licenses/LICENSE-2.0.html
//
import QtQuick 2.5
import Qt.labs.settings 1.0
import "../../styles-uit"
import "../../windows"
import "content"
ScrollingWindow {
id: root
title: "Attachments"
objectName: "AttachmentsDialog"
width: 600
height: 600
resizable: true
destroyOnHidden: true
minSize: Qt.vector2d(400, 500)
HifiConstants { id: hifi }
// This is for JS/QML communication, which is unused in the AttachmentsDialog,
// but not having this here results in spurious warnings about a
// missing signal
signal sendToScript(var message);
property var settings: Settings {
category: "AttachmentsDialog"
property alias x: root.x
property alias y: root.y
property alias width: root.width
property alias height: root.height
}
function closeDialog() {
root.destroy();
}
AttachmentsContent { }
}

View file

@ -1,130 +0,0 @@
import QtQuick 2.7
import QtQuick.Controls 1.5
import QtQuick.XmlListModel 2.0
import QtQuick.Controls.Styles 1.4
import "../../windows"
import "../../js/Utils.js" as Utils
import "../models"
ModalWindow {
id: root
resizable: true
width: 640
height: 480
property var result;
signal selected(var modelUrl);
signal canceled();
Rectangle {
anchors.fill: parent
color: "white"
Item {
anchors { fill: parent; margins: 8 }
TextField {
id: filterEdit
anchors { left: parent.left; right: parent.right; top: parent.top }
style: TextFieldStyle { renderType: Text.QtRendering }
placeholderText: "filter"
onTextChanged: tableView.model.filter = text
}
TableView {
id: tableView
anchors { left: parent.left; right: parent.right; top: filterEdit.bottom; topMargin: 8; bottom: buttonRow.top; bottomMargin: 8 }
model: S3Model{}
onCurrentRowChanged: {
if (currentRow == -1) {
root.result = null;
return;
}
result = model.baseUrl + "/" + model.get(tableView.currentRow).key;
}
itemDelegate: Component {
Item {
clip: true
Text {
x: 3
anchors.verticalCenter: parent.verticalCenter
color: tableView.activeFocus && styleData.row === tableView.currentRow ? "yellow" : styleData.textColor
elide: styleData.elideMode
text: getText()
function getText() {
switch(styleData.column) {
case 1:
return Utils.formatSize(styleData.value)
default:
return styleData.value;
}
}
}
}
}
TableViewColumn {
role: "name"
title: "Name"
width: 200
}
TableViewColumn {
role: "size"
title: "Size"
width: 100
}
TableViewColumn {
role: "modified"
title: "Last Modified"
width: 200
}
Rectangle {
anchors.fill: parent
visible: tableView.model.status !== XmlListModel.Ready
color: "#7fffffff"
BusyIndicator {
anchors.centerIn: parent
width: 48; height: 48
running: true
}
}
}
Row {
id: buttonRow
anchors { right: parent.right; bottom: parent.bottom }
Button { action: acceptAction }
Button { action: cancelAction }
}
Action {
id: acceptAction
text: qsTr("OK")
enabled: root.result ? true : false
shortcut: "Return"
onTriggered: {
root.selected(root.result);
root.destroy();
}
}
Action {
id: cancelAction
text: qsTr("Cancel")
shortcut: "Esc"
onTriggered: {
root.canceled();
root.destroy();
}
}
}
}
}

View file

@ -1,230 +0,0 @@
import QtQuick 2.5
import "."
import ".."
import "../../tablet"
import "../../../styles-uit"
import "../../../controls-uit" as HifiControls
import "../../../windows"
Item {
height: column.height + 2 * 8
property var attachment;
HifiConstants { id: hifi }
signal selectAttachment();
signal deleteAttachment(var attachment);
signal updateAttachment();
property bool completed: false;
function doSelectAttachment(control, focus) {
if (focus) {
selectAttachment();
// Refocus control after possibly changing focus to attachment.
if (control.setControlFocus !== undefined) {
control.setControlFocus();
} else {
control.focus = true;
}
}
}
Rectangle { color: hifi.colors.baseGray; anchors.fill: parent; radius: 4 }
Component.onCompleted: {
jointChooser.model = MyAvatar.jointNames;
completed = true;
}
Column {
y: 8
id: column
anchors { left: parent.left; right: parent.right; margins: 20 }
spacing: 8
Item {
height: modelChooserButton.height + urlLabel.height + 4
anchors { left: parent.left; right: parent.right;}
HifiControls.Label { id: urlLabel; color: hifi.colors.lightGrayText; text: "Model URL"; anchors.top: parent.top;}
HifiControls.TextField {
id: modelUrl;
height: jointChooser.height;
colorScheme: hifi.colorSchemes.dark
anchors { bottom: parent.bottom; left: parent.left; rightMargin: 8; right: modelChooserButton.left }
text: attachment ? attachment.modelUrl : ""
onTextChanged: {
if (completed && attachment && attachment.modelUrl !== text) {
attachment.modelUrl = text;
updateAttachment();
}
}
onFocusChanged: doSelectAttachment(this, focus);
}
HifiControls.Button {
id: modelChooserButton;
text: "Choose";
color: hifi.buttons.black
colorScheme: hifi.colorSchemes.dark
anchors { right: parent.right; verticalCenter: modelUrl.verticalCenter }
Component {
id: modelBrowserBuilder;
ModelBrowserDialog {}
}
Component {
id: tabletModelBrowserBuilder;
TabletModelBrowserDialog {}
}
onClicked: {
var browser;
if (typeof desktop !== "undefined") {
browser = modelBrowserBuilder.createObject(desktop);
browser.selected.connect(function(newModelUrl){
modelUrl.text = newModelUrl;
});
} else {
browser = tabletModelBrowserBuilder.createObject(tabletRoot);
browser.selected.connect(function(newModelUrl){
modelUrl.text = newModelUrl;
tabletRoot.openModal = null;
});
browser.canceled.connect(function() {
tabletRoot.openModal = null;
});
// Make dialog modal.
tabletRoot.openModal = browser;
}
}
}
}
Item {
z: 1000
height: jointChooser.height + jointLabel.height + 4
anchors { left: parent.left; right: parent.right; }
HifiControls.Label {
id: jointLabel;
text: "Joint";
color: hifi.colors.lightGrayText;
anchors.top: parent.top
}
HifiControls.ComboBox {
id: jointChooser;
dropdownHeight: (typeof desktop !== "undefined") ? 480 : 206
anchors { bottom: parent.bottom; left: parent.left; right: parent.right }
colorScheme: hifi.colorSchemes.dark
currentIndex: attachment ? model.indexOf(attachment.jointName) : -1
onCurrentIndexChanged: {
if (completed && attachment && currentIndex != -1 && attachment.jointName !== model[currentIndex]) {
attachment.jointName = model[currentIndex];
updateAttachment();
}
}
onFocusChanged: doSelectAttachment(this, focus);
}
}
Item {
height: translation.height + translationLabel.height + 4
anchors { left: parent.left; right: parent.right; }
HifiControls.Label { id: translationLabel; color: hifi.colors.lightGrayText; text: "Translation"; anchors.top: parent.top; }
Translation {
id: translation;
anchors { left: parent.left; right: parent.right; bottom: parent.bottom}
vector: attachment ? attachment.translation : {x: 0, y: 0, z: 0};
onValueChanged: {
if (completed && attachment) {
attachment.translation = vector;
updateAttachment();
}
}
onControlFocusChanged: doSelectAttachment(this, controlFocus);
}
}
Item {
height: rotation.height + rotationLabel.height + 4
anchors { left: parent.left; right: parent.right; }
HifiControls.Label { id: rotationLabel; color: hifi.colors.lightGrayText; text: "Rotation"; anchors.top: parent.top; }
Rotation {
id: rotation;
anchors { left: parent.left; right: parent.right; bottom: parent.bottom; }
vector: attachment ? attachment.rotation : {x: 0, y: 0, z: 0};
onValueChanged: {
if (completed && attachment) {
attachment.rotation = vector;
updateAttachment();
}
}
onControlFocusChanged: doSelectAttachment(this, controlFocus);
}
}
Item {
height: scaleItem.height
anchors { left: parent.left; right: parent.right; }
Item {
id: scaleItem
height: scaleSpinner.height + scaleLabel.height + 4
width: parent.width / 3 - 8
anchors { right: parent.right; }
HifiControls.Label { id: scaleLabel; color: hifi.colors.lightGrayText; text: "Scale"; anchors.top: parent.top; }
HifiControls.SpinBox {
id: scaleSpinner;
anchors { left: parent.left; right: parent.right; bottom: parent.bottom; }
decimals: 2;
minimumValue: 0.01
maximumValue: 10
realStepSize: 0.05;
realValue: attachment ? attachment.scale : 1.0
colorScheme: hifi.colorSchemes.dark
onRealValueChanged: {
if (completed && attachment && attachment.scale !== realValue) {
attachment.scale = realValue;
updateAttachment();
}
}
onFocusChanged: doSelectAttachment(this, focus);
}
}
Item {
id: isSoftItem
height: scaleSpinner.height
anchors {
left: parent.left
bottom: parent.bottom
}
HifiControls.CheckBox {
id: soft
text: "Is soft"
anchors {
left: parent.left
verticalCenter: parent.verticalCenter
}
checked: attachment ? attachment.soft : false
colorScheme: hifi.colorSchemes.dark
onCheckedChanged: {
if (completed && attachment && attachment.soft !== checked) {
attachment.soft = checked;
updateAttachment();
}
}
onFocusChanged: doSelectAttachment(this, focus);
}
}
}
HifiControls.Button {
color: hifi.buttons.black
colorScheme: hifi.colorSchemes.dark
anchors { left: parent.left; right: parent.right; }
text: "Delete"
onClicked: deleteAttachment(root.attachment);
}
}
}

View file

@ -1,9 +0,0 @@
import "."
Vector3 {
decimals: 1;
stepSize: 1;
maximumValue: 180
minimumValue: -180
}

View file

@ -1,9 +0,0 @@
import "."
Vector3 {
decimals: 3;
stepSize: 0.01;
maximumValue: 10
minimumValue: -10
}

View file

@ -1,112 +0,0 @@
import QtQuick 2.5
import "../../../styles-uit"
import "../../../controls-uit" as HifiControls
import "../../../windows"
Item {
id: root
implicitHeight: xspinner.height
readonly property real spacing: 8
property real spinboxWidth: (width / 3) - spacing
property var vector;
property real decimals: 0
property real stepSize: 1
property real maximumValue: 99
property real minimumValue: 0
property bool controlFocus: false; // True if one of the ordinate controls has focus.
property var controlFocusControl: undefined
signal valueChanged();
function setControlFocus() {
if (controlFocusControl) {
controlFocusControl.focus = true;
// The controlFocus value is updated via onFocusChanged.
}
}
function setFocus(control, focus) {
if (focus) {
controlFocusControl = control;
setControlFocusTrue.start(); // After any subsequent false from previous control.
} else {
controlFocus = false;
}
}
Timer {
id: setControlFocusTrue
interval: 50
repeat: false
running: false
onTriggered: {
controlFocus = true;
}
}
HifiConstants { id: hifi }
HifiControls.SpinBox {
id: xspinner
width: root.spinboxWidth
anchors { left: parent.left }
realValue: root.vector.x
labelInside: "X:"
colorScheme: hifi.colorSchemes.dark
colorLabelInside: hifi.colors.redHighlight
decimals: root.decimals
realStepSize: root.stepSize
maximumValue: root.maximumValue
minimumValue: root.minimumValue
onRealValueChanged: {
if (realValue !== vector.x) {
vector.x = realValue
root.valueChanged();
}
}
onFocusChanged: setFocus(this, focus);
}
HifiControls.SpinBox {
id: yspinner
width: root.spinboxWidth
anchors { horizontalCenter: parent.horizontalCenter }
realValue: root.vector.y
labelInside: "Y:"
colorLabelInside: hifi.colors.greenHighlight
colorScheme: hifi.colorSchemes.dark
decimals: root.decimals
realStepSize: root.stepSize
maximumValue: root.maximumValue
minimumValue: root.minimumValue
onRealValueChanged: {
if (realValue !== vector.y) {
vector.y = realValue
root.valueChanged();
}
}
onFocusChanged: setFocus(this, focus);
}
HifiControls.SpinBox {
id: zspinner
width: root.spinboxWidth
anchors { right: parent.right; }
realValue: root.vector.z
labelInside: "Z:"
colorLabelInside: hifi.colors.primaryHighlight
colorScheme: hifi.colorSchemes.dark
decimals: root.decimals
realStepSize: root.stepSize
maximumValue: root.maximumValue
minimumValue: root.minimumValue
onRealValueChanged: {
if (realValue !== vector.z) {
vector.z = realValue
root.valueChanged();
}
}
onFocusChanged: setFocus(this, focus);
}
}

View file

@ -1,282 +0,0 @@
import QtQuick 2.7
import QtQuick.Controls 2.3
import QtQuick.Dialogs 1.2 as OriginalDialogs
import "../../../styles-uit"
import "../../../controls-uit" as HifiControls
import "../../../windows"
import "../attachments"
Item {
id: content
readonly property var originalAttachments: MyAvatar.getAttachmentsVariant();
property var attachments: [];
function reload(){
content.attachments = [];
var currentAttachments = MyAvatar.getAttachmentsVariant();
listView.model.clear();
for (var i = 0; i < currentAttachments.length; ++i) {
var attachment = currentAttachments[i];
content.attachments.push(attachment);
listView.model.append({});
}
}
Connections {
id: onAttachmentsChangedConnection
target: MyAvatar
onAttachmentsChanged: reload()
}
Component.onCompleted: {
reload()
}
function setAttachmentsVariant(attachments) {
onAttachmentsChangedConnection.enabled = false;
MyAvatar.setAttachmentsVariant(attachments);
onAttachmentsChangedConnection.enabled = true;
}
Column {
width: pane.width
Rectangle {
width: parent.width
height: root.height - (keyboardEnabled && keyboardRaised ? 200 : 0)
color: hifi.colors.baseGray
Rectangle {
id: attachmentsBackground
anchors {
left: parent.left; right: parent.right; top: parent.top; bottom: newAttachmentButton.top;
margins: hifi.dimensions.contentMargin.x
bottomMargin: hifi.dimensions.contentSpacing.y
}
color: hifi.colors.baseGrayShadow
radius: 4
ListView {
id: listView
anchors {
top: parent.top
left: parent.left
right: scrollBar.left
bottom: parent.bottom
margins: 4
}
clip: true
cacheBuffer: 4000
model: ListModel {}
delegate: Item {
id: attachmentDelegate
implicitHeight: attachmentView.height + 8;
implicitWidth: attachmentView.width
MouseArea {
// User can click on whitespace to select item.
anchors.fill: parent
propagateComposedEvents: true
onClicked: {
listView.currentIndex = index;
attachmentsBackground.forceActiveFocus(); // Unfocus from any control.
mouse.accepted = false;
}
}
Attachment {
id: attachmentView
width: listView.width
attachment: content.attachments[index]
onSelectAttachment: {
listView.currentIndex = index;
}
onDeleteAttachment: {
attachments.splice(index, 1);
listView.model.remove(index, 1);
}
onUpdateAttachment: {
setAttachmentsVariant(attachments);
}
}
}
onCountChanged: {
setAttachmentsVariant(attachments);
}
/*
// DEBUG
highlight: Rectangle { color: "#40ffff00" }
highlightFollowsCurrentItem: true
*/
onHeightChanged: {
// Keyboard has been raised / lowered.
positionViewAtIndex(listView.currentIndex, ListView.SnapPosition);
}
onCurrentIndexChanged: {
if (!yScrollTimer.running) {
scrollSlider.y = currentIndex * (scrollBar.height - scrollSlider.height) / (listView.count - 1);
}
}
onContentYChanged: {
// User may have dragged content up/down.
yScrollTimer.restart();
}
Timer {
id: yScrollTimer
interval: 200
repeat: false
running: false
onTriggered: {
var index = (listView.count - 1) * listView.contentY / (listView.contentHeight - scrollBar.height);
index = Math.round(index);
listView.currentIndex = index;
scrollSlider.y = index * (scrollBar.height - scrollSlider.height) / (listView.count - 1);
}
}
}
Rectangle {
id: scrollBar
property bool scrolling: listView.contentHeight > listView.height
anchors {
top: parent.top
right: parent.right
bottom: parent.bottom
topMargin: 4
bottomMargin: 4
}
width: scrolling ? 18 : 0
radius: attachmentsBackground.radius
color: hifi.colors.baseGrayShadow
MouseArea {
anchors.fill: parent
onClicked: {
var index = listView.currentIndex;
index = index + (mouse.y <= scrollSlider.y ? -1 : 1);
if (index < 0) {
index = 0;
}
if (index > listView.count - 1) {
index = listView.count - 1;
}
listView.currentIndex = index;
}
}
Rectangle {
id: scrollSlider
anchors {
right: parent.right
rightMargin: 3
}
width: 16
height: (listView.height / listView.contentHeight) * listView.height
radius: width / 2
color: hifi.colors.lightGray
visible: scrollBar.scrolling;
onYChanged: {
var index = y * (listView.count - 1) / (scrollBar.height - scrollSlider.height);
index = Math.round(index);
listView.currentIndex = index;
}
MouseArea {
anchors.fill: parent
drag.target: scrollSlider
drag.axis: Drag.YAxis
drag.minimumY: 0
drag.maximumY: scrollBar.height - scrollSlider.height
}
}
}
}
HifiControls.Button {
id: newAttachmentButton
anchors {
left: parent.left
right: parent.right
bottom: buttonRow.top
margins: hifi.dimensions.contentMargin.x;
topMargin: hifi.dimensions.contentSpacing.y
bottomMargin: hifi.dimensions.contentSpacing.y
}
text: "New Attachment"
color: hifi.buttons.black
colorScheme: hifi.colorSchemes.dark
onClicked: {
var template = {
modelUrl: "",
translation: { x: 0, y: 0, z: 0 },
rotation: { x: 0, y: 0, z: 0 },
scale: 1,
jointName: MyAvatar.jointNames[0],
soft: false
};
attachments.push(template);
listView.model.append({});
setAttachmentsVariant(attachments);
}
}
Row {
id: buttonRow
spacing: 8
anchors {
right: parent.right
bottom: parent.bottom
margins: hifi.dimensions.contentMargin.x
topMargin: hifi.dimensions.contentSpacing.y
bottomMargin: hifi.dimensions.contentSpacing.y
}
HifiControls.Button {
action: okAction
color: hifi.buttons.black
colorScheme: hifi.colorSchemes.dark
}
HifiControls.Button {
action: cancelAction
color: hifi.buttons.black
colorScheme: hifi.colorSchemes.dark
}
}
Action {
id: cancelAction
text: "Cancel"
onTriggered: {
setAttachmentsVariant(originalAttachments);
closeDialog();
}
}
Action {
id: okAction
text: "OK"
onTriggered: {
for (var i = 0; i < attachments.length; ++i) {
console.log("Attachment " + i + ": " + attachments[i]);
}
setAttachmentsVariant(attachments);
closeDialog();
}
}
}
}
}

View file

@ -1,103 +0,0 @@
//
// TabletAttachmentsDialog.qml
//
// Created by David Rowe on 9 Mar 2017.
// Copyright 2017 High Fidelity, Inc.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or https://www.apache.org/licenses/LICENSE-2.0.html
//
import QtQuick 2.5
import "../../controls-uit" as HifiControls
import "../../styles-uit"
import "../dialogs/content"
Item {
id: root
objectName: "AttachmentsDialog"
property string title: "Avatar Attachments"
property bool keyboardEnabled: false
property bool keyboardRaised: false
property bool punctuationMode: false
signal sendToScript(var message);
anchors.fill: parent
HifiConstants { id: hifi }
Rectangle {
id: pane // Surrogate for ScrollingWindow's pane.
anchors.fill: parent
}
function closeDialog() {
Tablet.getTablet("com.highfidelity.interface.tablet.system").gotoHomeScreen();
}
anchors.topMargin: hifi.dimensions.tabletMenuHeader // Space for header.
HifiControls.TabletHeader {
id: header
title: root.title
anchors {
left: parent.left
right: parent.right
bottom: parent.top
}
}
AttachmentsContent {
id: attachments
anchors {
top: header.bottom
left: parent.left
right: parent.right
bottom: keyboard.top
}
MouseArea {
// Defocuses any current control so that the keyboard gets hidden.
id: defocuser
anchors.fill: parent
propagateComposedEvents: true
acceptedButtons: Qt.AllButtons
onPressed: {
parent.forceActiveFocus();
mouse.accepted = false;
}
}
}
HifiControls.Keyboard {
id: keyboard
raised: parent.keyboardEnabled && parent.keyboardRaised
numeric: parent.punctuationMode
anchors {
left: parent.left
right: parent.right
bottom: parent.bottom
}
}
MouseArea {
id: activator
anchors.fill: parent
propagateComposedEvents: true
enabled: true
acceptedButtons: Qt.AllButtons
onPressed: {
mouse.accepted = false;
}
}
Component.onCompleted: {
keyboardEnabled = HMD.active;
}
}

View file

@ -1013,7 +1013,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo
// This is done so as not break previous command line scripts
if (testScriptPath.left(URL_SCHEME_HTTP.length()) == URL_SCHEME_HTTP ||
testScriptPath.left(URL_SCHEME_FTP.length()) == URL_SCHEME_FTP) {
setProperty(hifi::properties::TEST, QUrl::fromUserInput(testScriptPath));
} else if (QFileInfo(testScriptPath).exists()) {
setProperty(hifi::properties::TEST, QUrl::fromLocalFile(testScriptPath));
@ -1830,14 +1830,6 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo
}
});
connect(getEntities()->getTree().get(), &EntityTree::deletingEntity, [](const EntityItemID& entityItemID) {
auto avatarManager = DependencyManager::get<AvatarManager>();
auto myAvatar = avatarManager ? avatarManager->getMyAvatar() : nullptr;
if (myAvatar) {
myAvatar->clearAvatarEntity(entityItemID);
}
});
EntityTree::setAddMaterialToEntityOperator([this](const QUuid& entityID, graphics::MaterialLayer material, const std::string& parentMaterialName) {
// try to find the renderable
auto renderable = getEntities()->renderableForEntityId(entityID);
@ -2617,7 +2609,7 @@ Application::~Application() {
// Can't log to file passed this point, FileLogger about to be deleted
qInstallMessageHandler(LogHandler::verboseMessageHandler);
_renderEventHandler->deleteLater();
}
@ -3667,7 +3659,7 @@ bool Application::event(QEvent* event) {
bool Application::eventFilter(QObject* object, QEvent* event) {
if (_aboutToQuit) {
if (_aboutToQuit && event->type() != QEvent::DeferredDelete && event->type() != QEvent::Destroy) {
return true;
}
@ -5496,10 +5488,7 @@ void Application::update(float deltaTime) {
// we haven't yet enabled physics. we wait until we think we have all the collision information
// for nearby entities before starting bullet up.
quint64 now = usecTimestampNow();
// Check for flagged EntityData having arrived.
auto entityTreeRenderer = getEntities();
if (isServerlessMode() ||
(_octreeProcessor.isLoadSequenceComplete() )) {
if (isServerlessMode() || _octreeProcessor.isLoadSequenceComplete()) {
// we've received a new full-scene octree stats packet, or it's been long enough to try again anyway
_lastPhysicsCheckTime = now;
_fullSceneCounterAtLastPhysicsCheck = _fullSceneReceivedCounter;
@ -6393,8 +6382,8 @@ void Application::nodeActivated(SharedNodePointer node) {
if (_avatarOverrideUrl.isValid()) {
getMyAvatar()->useFullAvatarURL(_avatarOverrideUrl);
}
static const QUrl empty{};
if (getMyAvatar()->getFullAvatarURLFromPreferences() != getMyAvatar()->cannonicalSkeletonModelURL(empty)) {
if (getMyAvatar()->getFullAvatarURLFromPreferences() != getMyAvatar()->getSkeletonModelURL()) {
getMyAvatar()->resetFullAvatarURL();
}
getMyAvatar()->markIdentityDataChanged();

View file

@ -25,6 +25,7 @@
#include <avatar/AvatarManager.h>
#include <EntityItemID.h>
#include <EntityTree.h>
#include <ModelEntityItem.h>
#include <PhysicalEntitySimulation.h>
#include <EntityEditPacketSender.h>
#include <VariantMapToScriptValue.h>
@ -35,7 +36,7 @@
#include "QVariantGLM.h"
#include <QtQuick/QQuickWindow>
#include <memory>
void addAvatarEntities(const QVariantList& avatarEntities) {
auto nodeList = DependencyManager::get<NodeList>();
@ -145,8 +146,8 @@ void AvatarBookmarks::removeBookmark(const QString& bookmarkName) {
}
bool isWearableEntity(const EntityItemPointer& entity) {
return entity->isVisible() && entity->getParentJointIndex() != INVALID_JOINT_INDEX &&
(entity->getParentID() == DependencyManager::get<NodeList>()->getSessionUUID() || entity->getParentID() == DependencyManager::get<AvatarManager>()->getMyAvatar()->getSelfID());
return entity->isVisible() && (entity->getParentJointIndex() != INVALID_JOINT_INDEX || (entity->getType() == EntityTypes::Model && (std::static_pointer_cast<ModelEntityItem>(entity))->getRelayParentJoints()))
&& (entity->getParentID() == DependencyManager::get<NodeList>()->getSessionUUID() || entity->getParentID() == DependencyManager::get<AvatarManager>()->getMyAvatar()->getSelfID());
}
void AvatarBookmarks::updateAvatarEntities(const QVariantList &avatarEntities) {
@ -254,7 +255,6 @@ QVariantMap AvatarBookmarks::getAvatarDataToBookmark() {
bookmark.insert(ENTRY_VERSION, AVATAR_BOOKMARK_VERSION);
bookmark.insert(ENTRY_AVATAR_URL, avatarUrl);
bookmark.insert(ENTRY_AVATAR_SCALE, avatarScale);
bookmark.insert(ENTRY_AVATAR_ATTACHMENTS, myAvatar->getAttachmentsVariant());
QScriptEngine scriptEngine;
QVariantList wearableEntities;

View file

@ -277,13 +277,6 @@ Menu::Menu() {
QString("hifi/tablet/TabletGraphicsPreferences.qml"), "GraphicsPreferencesDialog");
});
// Settings > Attachments...
action = addActionToQMenuAndActionHash(settingsMenu, MenuOption::Attachments);
connect(action, &QAction::triggered, [] {
qApp->showDialog(QString("hifi/dialogs/AttachmentsDialog.qml"),
QString("hifi/tablet/TabletAttachmentsDialog.qml"), "AttachmentsDialog");
});
// Settings > Developer Menu
addCheckableActionToQMenuAndActionHash(settingsMenu, "Developer Menu", 0, false, this, SLOT(toggleDeveloperMenus()));

View file

@ -36,7 +36,6 @@ namespace MenuOption {
const QString AskToResetSettings = "Ask To Reset Settings on Start";
const QString AssetMigration = "ATP Asset Migration";
const QString AssetServer = "Asset Browser";
const QString Attachments = "Attachments...";
const QString AudioScope = "Show Scope";
const QString AudioScopeFiftyFrames = "Fifty";
const QString AudioScopeFiveFrames = "Five";

View file

@ -14,7 +14,6 @@
#include <string>
#include <QScriptEngine>
#include <QtCore/QJsonDocument>
#include "AvatarLogging.h"
@ -72,10 +71,6 @@ AvatarManager::AvatarManager(QObject* parent) :
qRegisterMetaType<QWeakPointer<Node> >("NodeWeakPointer");
auto nodeList = DependencyManager::get<NodeList>();
auto& packetReceiver = nodeList->getPacketReceiver();
packetReceiver.registerListener(PacketType::BulkAvatarData, this, "processAvatarDataPacket");
packetReceiver.registerListener(PacketType::KillAvatar, this, "processKillAvatar");
packetReceiver.registerListener(PacketType::AvatarIdentity, this, "processAvatarIdentityPacket");
// when we hear that the user has ignored an avatar by session UUID
// immediately remove that avatar instead of waiting for the absence of packets from avatar mixer

View file

@ -105,7 +105,7 @@ MyAvatar::MyAvatar(QThread* thread) :
_eyeContactTarget(LEFT_EYE),
_realWorldFieldOfView("realWorldFieldOfView",
DEFAULT_REAL_WORLD_FIELD_OF_VIEW_DEGREES),
_useAdvancedMovementControls("advancedMovementForHandControllersIsChecked", false),
_useAdvancedMovementControls("advancedMovementForHandControllersIsChecked", true),
_smoothOrientationTimer(std::numeric_limits<float>::max()),
_smoothOrientationInitial(),
_smoothOrientationTarget(),
@ -121,6 +121,7 @@ MyAvatar::MyAvatar(QThread* thread) :
_audioListenerMode(FROM_HEAD),
_hmdAtRestDetector(glm::vec3(0), glm::quat())
{
_clientTraitsHandler = std::unique_ptr<ClientTraitsHandler>(new ClientTraitsHandler(this));
// give the pointer to our head to inherited _headData variable from AvatarData
_headData = new MyHead(this);
@ -202,6 +203,7 @@ MyAvatar::MyAvatar(QThread* thread) :
connect(recorder.data(), &Recorder::recordingStateChanged, [=] {
if (recorder->isRecording()) {
createRecordingIDs();
setRecordingBasis();
} else {
clearRecordingBasis();
@ -443,7 +445,6 @@ void MyAvatar::reset(bool andRecenter, bool andReload, bool andHead) {
}
void MyAvatar::update(float deltaTime) {
// update moving average of HMD facing in xz plane.
const float HMD_FACING_TIMESCALE = getRotationRecenterFilterLength();
@ -513,6 +514,8 @@ void MyAvatar::update(float deltaTime) {
sendIdentityPacket();
}
_clientTraitsHandler->sendChangedTraitsToMixer();
simulate(deltaTime);
currentEnergy += energyChargeRate;
@ -1289,7 +1292,6 @@ void MyAvatar::loadData() {
// HACK: manually remove empty 'avatarEntityData' else legacy data may persist in settings file
settings.remove("avatarEntityData");
}
setAvatarEntityDataChanged(true);
// Flying preferences must be loaded before calling setFlyingEnabled()
Setting::Handle<bool> firstRunVal { Settings::firstRun, true };
@ -1666,7 +1668,10 @@ void MyAvatar::clearJointsData() {
void MyAvatar::setSkeletonModelURL(const QUrl& skeletonModelURL) {
_skeletonModelChangeCount++;
int skeletonModelChangeCount = _skeletonModelChangeCount;
auto previousSkeletonModelURL = _skeletonModelURL;
Avatar::setSkeletonModelURL(skeletonModelURL);
_skeletonModel->setTagMask(render::hifi::TAG_NONE);
_skeletonModel->setGroupCulled(true);
_skeletonModel->setVisibleInScene(true, qApp->getMain3DScene());
@ -1693,9 +1698,9 @@ void MyAvatar::setSkeletonModelURL(const QUrl& skeletonModelURL) {
}
QObject::disconnect(*skeletonConnection);
});
saveAvatarUrl();
emit skeletonChanged();
emit skeletonModelURLChanged();
}
void MyAvatar::removeAvatarEntities(const std::function<bool(const QUuid& entityID)>& condition) {
@ -1766,8 +1771,6 @@ void MyAvatar::useFullAvatarURL(const QUrl& fullAvatarURL, const QString& modelN
setSkeletonModelURL(fullAvatarURL);
UserActivityLogger::getInstance().changedModel("skeleton", urlString);
}
markIdentityDataChanged();
}
glm::vec3 MyAvatar::getSkeletonPosition() const {
@ -2111,7 +2114,10 @@ void MyAvatar::setAttachmentData(const QVector<AttachmentData>& attachmentData)
attachmentDataToEntityProperties(data, properties);
newEntitiesProperties.push_back(properties);
}
removeAvatarEntities();
// clear any existing avatar entities
setAvatarEntityData(AvatarEntityMap());
for (auto& properties : newEntitiesProperties) {
DependencyManager::get<EntityScriptingInterface>()->addEntity(properties, true);
}

View file

@ -18,22 +18,22 @@
#include <QUuid>
#include <SettingHandle.h>
#include <Rig.h>
#include <Sound.h>
#include <ScriptEngine.h>
#include <controllers/Pose.h>
#include <controllers/Actions.h>
#include <AvatarConstants.h>
#include <avatars-renderer/Avatar.h>
#include <avatars-renderer/ScriptAvatar.h>
#include <ClientTraitsHandler.h>
#include <controllers/Pose.h>
#include <controllers/Actions.h>
#include <EntityItem.h>
#include <ThreadSafeValueCache.h>
#include <Rig.h>
#include <ScriptEngine.h>
#include <SettingHandle.h>
#include <Sound.h>
#include "AtRestDetector.h"
#include "MyCharacterController.h"
#include "RingBufferHistory.h"
#include <ThreadSafeValueCache.h>
#include <EntityItem.h>
class AvatarActionHold;
class ModelItemID;
@ -1334,7 +1334,6 @@ public slots:
*/
void setAnimGraphUrl(const QUrl& url); // thread-safe
/**jsdoc
* @function MyAvatar.getPositionForAudio
* @returns {Vec3}
@ -1347,7 +1346,6 @@ public slots:
*/
glm::quat getOrientationForAudio();
/**jsdoc
* @function MyAvatar.setModelScale
* @param {number} scale

View file

@ -122,11 +122,11 @@ bool SafeLanding::isSequenceNumbersComplete() {
int sequenceSize = _initialStart <= _initialEnd ? _initialEnd - _initialStart:
_initialEnd + SEQUENCE_MODULO - _initialStart;
auto startIter = _sequenceNumbers.find(_initialStart);
auto endIter = _sequenceNumbers.find(_initialEnd);
auto endIter = _sequenceNumbers.find(_initialEnd - 1);
if (sequenceSize == 0 ||
(startIter != _sequenceNumbers.end()
&& endIter != _sequenceNumbers.end()
&& distance(startIter, endIter) == sequenceSize) ) {
&& distance(startIter, endIter) == sequenceSize - 1) ) {
_trackingEntities = false; // Don't track anything else that comes in.
return true;
}

View file

@ -26,7 +26,7 @@ class SafeLanding : public QObject {
public:
void startEntitySequence(QSharedPointer<EntityTreeRenderer> entityTreeRenderer);
void stopEntitySequence();
void setCompletionSequenceNumbers(int first, int last);
void setCompletionSequenceNumbers(int first, int last); // 'last' exclusive.
void noteReceivedsequenceNumber(int sequenceNumber);
bool isLoadSequenceComplete();

View file

@ -272,7 +272,7 @@ void setupPreferences() {
auto getter = [myAvatar]()->bool { return myAvatar->useAdvancedMovementControls(); };
auto setter = [myAvatar](bool value) { myAvatar->setUseAdvancedMovementControls(value); };
preferences->addPreference(new CheckPreference(VR_MOVEMENT,
QStringLiteral("Advanced movement for hand controllers"),
QStringLiteral("Advanced movement in VR (Teleport movement when unchecked)"),
getter, setter));
}
{

View file

@ -97,7 +97,8 @@ namespace render {
}
}
GameWorkloadRenderItem::GameWorkloadRenderItem() : _key(render::ItemKey::Builder::opaqueShape().withoutShadowCaster().withTagBits(render::ItemKey::TAG_BITS_0 | render::ItemKey::TAG_BITS_1)) {
GameWorkloadRenderItem::GameWorkloadRenderItem() : _key(render::ItemKey::Builder::opaqueShape().withShadowCaster().withTagBits(render::ItemKey::TAG_BITS_0 | render::ItemKey::TAG_BITS_1)) {
}
render::ItemKey GameWorkloadRenderItem::getKey() const {

View file

@ -224,14 +224,24 @@ void Avatar::setAvatarEntityDataChanged(bool value) {
void Avatar::updateAvatarEntities() {
PerformanceTimer perfTimer("attachments");
// AVATAR ENTITY UPDATE FLOW
// - if queueEditEntityMessage sees clientOnly flag it does _myAvatar->updateAvatarEntity()
// - updateAvatarEntity saves the bytes and sets _avatarEntityDataLocallyEdited
// - MyAvatar::update notices _avatarEntityDataLocallyEdited and calls sendIdentityPacket
// - sendIdentityPacket sends the entity bytes to the server which relays them to other interfaces
// - AvatarHashMap::processAvatarIdentityPacket on other interfaces call avatar->setAvatarEntityData()
// - setAvatarEntityData saves the bytes and sets _avatarEntityDataChanged = true
// - updateAvatarEntity saves the bytes and flags the trait instance for the entity as updated
// - ClientTraitsHandler::sendChangedTraitsToMixer sends the entity bytes to the mixer which relays them to other interfaces
// - AvatarHashMap::processBulkAvatarTraits on other interfaces calls avatar->processTraitInstace
// - AvatarData::processTraitInstance calls updateAvatarEntity, which sets _avatarEntityDataChanged = true
// - (My)Avatar::simulate notices _avatarEntityDataChanged and here we are...
// AVATAR ENTITY DELETE FLOW
// - EntityScriptingInterface::deleteEntity calls _myAvatar->clearAvatarEntity() for deleted avatar entities
// - clearAvatarEntity removes the avatar entity and flags the trait instance for the entity as deleted
// - ClientTraitsHandler::sendChangedTraitsToMixer sends a deletion to the mixer which relays to other interfaces
// - AvatarHashMap::processBulkAvatarTraits on other interfaces calls avatar->processDeletedTraitInstace
// - AvatarData::processDeletedTraitInstance calls clearAvatarEntity
// - AvatarData::clearAvatarEntity sets _avatarEntityDataChanged = true and adds the ID to the detached list
// - Avatar::simulate notices _avatarEntityDataChanged and here we are...
if (!_avatarEntityDataChanged) {
return;
}
@ -345,27 +355,30 @@ void Avatar::updateAvatarEntities() {
stateItr.value().success = success;
}
AvatarEntityIDs recentlyDettachedAvatarEntities = getAndClearRecentlyDetachedIDs();
if (!recentlyDettachedAvatarEntities.empty()) {
AvatarEntityIDs recentlyDetachedAvatarEntities = getAndClearRecentlyDetachedIDs();
if (!recentlyDetachedAvatarEntities.empty()) {
// only lock this thread when absolutely necessary
AvatarEntityMap avatarEntityData;
_avatarEntitiesLock.withReadLock([&] {
avatarEntityData = _avatarEntityData;
});
foreach (auto entityID, recentlyDettachedAvatarEntities) {
foreach (auto entityID, recentlyDetachedAvatarEntities) {
if (!avatarEntityData.contains(entityID)) {
entityTree->deleteEntity(entityID, true, true);
}
}
// remove stale data hashes
foreach (auto entityID, recentlyDettachedAvatarEntities) {
foreach (auto entityID, recentlyDetachedAvatarEntities) {
MapOfAvatarEntityDataHashes::iterator stateItr = _avatarEntityDataHashes.find(entityID);
if (stateItr != _avatarEntityDataHashes.end()) {
_avatarEntityDataHashes.erase(stateItr);
}
}
}
if (avatarEntities.size() != _avatarEntityForRecording.size()) {
createRecordingIDs();
}
});
setAvatarEntityDataChanged(false);

View file

@ -0,0 +1,158 @@
//
// AssociatedTraitValues.h
// libraries/avatars/src
//
// Created by Stephen Birarda on 8/8/18.
// Copyright 2018 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_AssociatedTraitValues_h
#define hifi_AssociatedTraitValues_h
#include "AvatarTraits.h"
namespace AvatarTraits {
template<typename T, T defaultValue>
class AssociatedTraitValues {
public:
AssociatedTraitValues() : _simpleTypes(FirstInstancedTrait, defaultValue) {}
void insert(TraitType type, T value) { _simpleTypes[type] = value; }
void erase(TraitType type) { _simpleTypes[type] = defaultValue; }
T& getInstanceValueRef(TraitType traitType, TraitInstanceID instanceID);
void instanceInsert(TraitType traitType, TraitInstanceID instanceID, T value);
struct InstanceIDValuePair {
TraitInstanceID id;
T value;
InstanceIDValuePair(TraitInstanceID id, T value) : id(id), value(value) {};
};
using InstanceIDValuePairs = std::vector<InstanceIDValuePair>;
InstanceIDValuePairs& getInstanceIDValuePairs(TraitType traitType);
void instanceErase(TraitType traitType, TraitInstanceID instanceID);
void eraseAllInstances(TraitType traitType);
// will return defaultValue for instanced traits
T operator[](TraitType traitType) const { return _simpleTypes[traitType]; }
T& operator[](TraitType traitType) { return _simpleTypes[traitType]; }
void reset() {
std::fill(_simpleTypes.begin(), _simpleTypes.end(), defaultValue);
_instancedTypes.clear();
}
typename std::vector<T>::const_iterator simpleCBegin() const { return _simpleTypes.cbegin(); }
typename std::vector<T>::const_iterator simpleCEnd() const { return _simpleTypes.cend(); }
typename std::vector<T>::iterator simpleBegin() { return _simpleTypes.begin(); }
typename std::vector<T>::iterator simpleEnd() { return _simpleTypes.end(); }
struct TraitWithInstances {
TraitType traitType;
InstanceIDValuePairs instances;
TraitWithInstances(TraitType traitType) : traitType(traitType) {};
TraitWithInstances(TraitType traitType, TraitInstanceID instanceID, T value) :
traitType(traitType), instances({{ instanceID, value }}) {};
};
typename std::vector<TraitWithInstances>::const_iterator instancedCBegin() const { return _instancedTypes.cbegin(); }
typename std::vector<TraitWithInstances>::const_iterator instancedCEnd() const { return _instancedTypes.cend(); }
typename std::vector<TraitWithInstances>::iterator instancedBegin() { return _instancedTypes.begin(); }
typename std::vector<TraitWithInstances>::iterator instancedEnd() { return _instancedTypes.end(); }
private:
std::vector<T> _simpleTypes;
typename std::vector<TraitWithInstances>::iterator instancesForTrait(TraitType traitType) {
return std::find_if(_instancedTypes.begin(), _instancedTypes.end(),
[traitType](TraitWithInstances& traitWithInstances){
return traitWithInstances.traitType == traitType;
});
}
std::vector<TraitWithInstances> _instancedTypes;
};
template <typename T, T defaultValue>
inline typename AssociatedTraitValues<T, defaultValue>::InstanceIDValuePairs&
AssociatedTraitValues<T, defaultValue>::getInstanceIDValuePairs(TraitType traitType) {
auto it = instancesForTrait(traitType);
if (it != _instancedTypes.end()) {
return it->instances;
} else {
_instancedTypes.emplace_back(traitType);
return _instancedTypes.back().instances;
}
}
template <typename T, T defaultValue>
inline T& AssociatedTraitValues<T, defaultValue>::getInstanceValueRef(TraitType traitType, TraitInstanceID instanceID) {
auto it = instancesForTrait(traitType);
if (it != _instancedTypes.end()) {
auto& instancesVector = it->instances;
auto instanceIt = std::find_if(instancesVector.begin(), instancesVector.end(),
[instanceID](InstanceIDValuePair& idValuePair){
return idValuePair.id == instanceID;
});
if (instanceIt != instancesVector.end()) {
return instanceIt->value;
} else {
instancesVector.emplace_back(instanceID, defaultValue);
return instancesVector.back().value;
}
} else {
_instancedTypes.emplace_back(traitType, instanceID, defaultValue);
return _instancedTypes.back().instances.back().value;
}
}
template <typename T, T defaultValue>
inline void AssociatedTraitValues<T, defaultValue>::instanceInsert(TraitType traitType, TraitInstanceID instanceID, T value) {
auto it = instancesForTrait(traitType);
if (it != _instancedTypes.end()) {
auto& instancesVector = it->instances;
auto instanceIt = std::find_if(instancesVector.begin(), instancesVector.end(),
[instanceID](InstanceIDValuePair& idValuePair){
return idValuePair.id == instanceID;
});
if (instanceIt != instancesVector.end()) {
instanceIt->value = value;
} else {
instancesVector.emplace_back(instanceID, value);
}
} else {
_instancedTypes.emplace_back(traitType, instanceID, value);
}
}
template <typename T, T defaultValue>
inline void AssociatedTraitValues<T, defaultValue>::instanceErase(TraitType traitType, TraitInstanceID instanceID) {
auto it = instancesForTrait(traitType);
if (it != _instancedTypes.end()) {
auto& instancesVector = it->instances;
instancesVector.erase(std::remove_if(instancesVector.begin(),
instancesVector.end(),
[&instanceID](InstanceIDValuePair& idValuePair){
return idValuePair.id == instanceID;
}));
}
}
using TraitVersions = AssociatedTraitValues<TraitVersion, DEFAULT_TRAIT_VERSION>;
};
#endif // hifi_AssociatedTraitValues_h

View file

@ -42,6 +42,8 @@
#include <BitVectorHelpers.h>
#include "AvatarLogging.h"
#include "AvatarTraits.h"
#include "ClientTraitsHandler.h"
//#define WANT_DEBUG
@ -1749,14 +1751,8 @@ glm::quat AvatarData::getOrientationOutbound() const {
return (getLocalOrientation());
}
static const QUrl emptyURL("");
QUrl AvatarData::cannonicalSkeletonModelURL(const QUrl& emptyURL) const {
// We don't put file urls on the wire, but instead convert to empty.
return _skeletonModelURL.scheme() == "file" ? emptyURL : _skeletonModelURL;
}
void AvatarData::processAvatarIdentity(const QByteArray& identityData, bool& identityChanged,
bool& displayNameChanged, bool& skeletonModelUrlChanged) {
bool& displayNameChanged) {
QDataStream packetStream(identityData);
@ -1777,28 +1773,17 @@ void AvatarData::processAvatarIdentity(const QByteArray& identityData, bool& ide
if (incomingSequenceNumber > _identitySequenceNumber) {
Identity identity;
packetStream >> identity.skeletonModelURL
packetStream
>> identity.attachmentData
>> identity.displayName
>> identity.sessionDisplayName
>> identity.isReplicated
>> identity.avatarEntityData
>> identity.lookAtSnappingEnabled
;
// set the store identity sequence number to match the incoming identity
_identitySequenceNumber = incomingSequenceNumber;
if (_firstSkeletonCheck || (identity.skeletonModelURL != cannonicalSkeletonModelURL(emptyURL))) {
setSkeletonModelURL(identity.skeletonModelURL);
identityChanged = true;
skeletonModelUrlChanged = true;
if (_firstSkeletonCheck) {
displayNameChanged = true;
}
_firstSkeletonCheck = false;
}
if (identity.displayName != _displayName) {
_displayName = identity.displayName;
identityChanged = true;
@ -1816,16 +1801,6 @@ void AvatarData::processAvatarIdentity(const QByteArray& identityData, bool& ide
identityChanged = true;
}
bool avatarEntityDataChanged = false;
_avatarEntitiesLock.withReadLock([&] {
avatarEntityDataChanged = (identity.avatarEntityData != _avatarEntityData);
});
if (avatarEntityDataChanged) {
setAvatarEntityData(identity.avatarEntityData);
identityChanged = true;
}
if (identity.lookAtSnappingEnabled != _lookAtSnappingEnabled) {
setProperty("lookAtSnappingEnabled", identity.lookAtSnappingEnabled);
identityChanged = true;
@ -1834,7 +1809,6 @@ void AvatarData::processAvatarIdentity(const QByteArray& identityData, bool& ide
#ifdef WANT_DEBUG
qCDebug(avatars) << __FUNCTION__
<< "identity.uuid:" << identity.uuid
<< "identity.skeletonModelURL:" << identity.skeletonModelURL
<< "identity.displayName:" << identity.displayName
<< "identity.sessionDisplayName:" << identity.sessionDisplayName;
} else {
@ -1846,26 +1820,105 @@ void AvatarData::processAvatarIdentity(const QByteArray& identityData, bool& ide
}
}
QUrl AvatarData::getWireSafeSkeletonModelURL() const {
if (_skeletonModelURL.scheme() != "file" && _skeletonModelURL.scheme() != "qrc") {
return _skeletonModelURL;
} else {
return QUrl();
}
}
qint64 AvatarData::packTrait(AvatarTraits::TraitType traitType, ExtendedIODevice& destination,
AvatarTraits::TraitVersion traitVersion) {
qint64 bytesWritten = 0;
bytesWritten += destination.writePrimitive(traitType);
if (traitVersion > AvatarTraits::DEFAULT_TRAIT_VERSION) {
bytesWritten += destination.writePrimitive(traitVersion);
}
if (traitType == AvatarTraits::SkeletonModelURL) {
QByteArray encodedSkeletonURL = getWireSafeSkeletonModelURL().toEncoded();
AvatarTraits::TraitWireSize encodedURLSize = encodedSkeletonURL.size();
bytesWritten += destination.writePrimitive(encodedURLSize);
bytesWritten += destination.write(encodedSkeletonURL);
}
return bytesWritten;
}
qint64 AvatarData::packTraitInstance(AvatarTraits::TraitType traitType, AvatarTraits::TraitInstanceID traitInstanceID,
ExtendedIODevice& destination, AvatarTraits::TraitVersion traitVersion) {
qint64 bytesWritten = 0;
bytesWritten += destination.writePrimitive(traitType);
if (traitVersion > AvatarTraits::DEFAULT_TRAIT_VERSION) {
bytesWritten += destination.writePrimitive(traitVersion);
}
bytesWritten += destination.write(traitInstanceID.toRfc4122());
if (traitType == AvatarTraits::AvatarEntity) {
// grab a read lock on the avatar entities and check for entity data for the given ID
QByteArray entityBinaryData;
_avatarEntitiesLock.withReadLock([this, &entityBinaryData, &traitInstanceID] {
if (_avatarEntityData.contains(traitInstanceID)) {
entityBinaryData = _avatarEntityData[traitInstanceID];
}
});
if (!entityBinaryData.isNull()) {
AvatarTraits::TraitWireSize entityBinarySize = entityBinaryData.size();
bytesWritten += destination.writePrimitive(entityBinarySize);
bytesWritten += destination.write(entityBinaryData);
} else {
bytesWritten += destination.writePrimitive(AvatarTraits::DELETED_TRAIT_SIZE);
}
}
return bytesWritten;
}
void AvatarData::processTrait(AvatarTraits::TraitType traitType, QByteArray traitBinaryData) {
if (traitType == AvatarTraits::SkeletonModelURL) {
// get the URL from the binary data
auto skeletonModelURL = QUrl::fromEncoded(traitBinaryData);
setSkeletonModelURL(skeletonModelURL);
}
}
void AvatarData::processTraitInstance(AvatarTraits::TraitType traitType,
AvatarTraits::TraitInstanceID instanceID, QByteArray traitBinaryData) {
if (traitType == AvatarTraits::AvatarEntity) {
updateAvatarEntity(instanceID, traitBinaryData);
}
}
void AvatarData::processDeletedTraitInstance(AvatarTraits::TraitType traitType, AvatarTraits::TraitInstanceID instanceID) {
if (traitType == AvatarTraits::AvatarEntity) {
clearAvatarEntity(instanceID);
}
}
QByteArray AvatarData::identityByteArray(bool setIsReplicated) const {
QByteArray identityData;
QDataStream identityStream(&identityData, QIODevice::Append);
const QUrl& urlToSend = cannonicalSkeletonModelURL(emptyURL); // depends on _skeletonModelURL
// when mixers send identity packets to agents, they simply forward along the last incoming sequence number they received
// whereas agents send a fresh outgoing sequence number when identity data has changed
_avatarEntitiesLock.withReadLock([&] {
identityStream << getSessionUUID()
<< (udt::SequenceNumber::Type) _identitySequenceNumber
<< urlToSend
<< _attachmentData
<< _displayName
<< getSessionDisplayNameForTransport() // depends on _sessionDisplayName
<< (_isReplicated || setIsReplicated)
<< _avatarEntityData
<< _lookAtSnappingEnabled
;
});
identityStream << getSessionUUID()
<< (udt::SequenceNumber::Type) _identitySequenceNumber
<< _attachmentData
<< _displayName
<< getSessionDisplayNameForTransport() // depends on _sessionDisplayName
<< (_isReplicated || setIsReplicated)
<< _lookAtSnappingEnabled;
return identityData;
}
@ -1879,11 +1932,17 @@ void AvatarData::setSkeletonModelURL(const QUrl& skeletonModelURL) {
if (expanded == _skeletonModelURL) {
return;
}
_skeletonModelURL = expanded;
qCDebug(avatars) << "Changing skeleton model for avatar" << getSessionUUID() << "to" << _skeletonModelURL.toString();
updateJointMappings();
markIdentityDataChanged();
if (_clientTraitsHandler) {
_clientTraitsHandler->markTraitUpdated(AvatarTraits::SkeletonModelURL);
}
emit skeletonModelURLChanged();
}
void AvatarData::setDisplayName(const QString& displayName) {
@ -1978,6 +2037,13 @@ void AvatarData::setJointMappingsFromNetworkReply() {
QNetworkReply* networkReply = static_cast<QNetworkReply*>(sender());
// before we process this update, make sure that the skeleton model URL hasn't changed
// since we made the FST request
if (networkReply->url() != _skeletonModelURL) {
qCDebug(avatars) << "Refusing to set joint mappings for FST URL that does not match the current URL";
return;
}
{
QWriteLocker writeLock(&_jointDataLock);
QByteArray line;
@ -2076,7 +2142,6 @@ void AvatarData::sendIdentityPacket() {
nodeList->sendPacketList(std::move(packetList), *node);
});
_avatarEntityDataLocallyEdited = false;
_identityDataChanged = false;
}
@ -2243,6 +2308,15 @@ void AvatarData::setRecordingBasis(std::shared_ptr<Transform> recordingBasis) {
_recordingBasis = recordingBasis;
}
void AvatarData::createRecordingIDs() {
_avatarEntitiesLock.withReadLock([&] {
_avatarEntityForRecording.clear();
for (int i = 0; i < _avatarEntityData.size(); i++) {
_avatarEntityForRecording.insert(QUuid::createUuid());
}
});
}
void AvatarData::clearRecordingBasis() {
_recordingBasis.reset();
}
@ -2303,21 +2377,15 @@ QJsonObject AvatarData::toJson() const {
if (!getDisplayName().isEmpty()) {
root[JSON_AVATAR_DISPLAY_NAME] = getDisplayName();
}
if (!getAttachmentData().isEmpty()) {
QJsonArray attachmentsJson;
for (auto attachment : getAttachmentData()) {
attachmentsJson.push_back(attachment.toJson());
}
root[JSON_AVATAR_ATTACHMENTS] = attachmentsJson;
}
_avatarEntitiesLock.withReadLock([&] {
if (!_avatarEntityData.empty()) {
QJsonArray avatarEntityJson;
int entityCount = 0;
for (auto entityID : _avatarEntityData.keys()) {
QVariantMap entityData;
entityData.insert("id", entityID);
entityData.insert("properties", _avatarEntityData.value(entityID));
QUuid newId = _avatarEntityForRecording.size() == _avatarEntityData.size() ? _avatarEntityForRecording.values()[entityCount++] : entityID;
entityData.insert("id", newId);
entityData.insert("properties", _avatarEntityData.value(entityID).toBase64());
avatarEntityJson.push_back(QVariant(entityData).toJsonObject());
}
root[JSON_AVATAR_ENTITIES] = avatarEntityJson;
@ -2439,12 +2507,17 @@ void AvatarData::fromJson(const QJsonObject& json, bool useFrameSkeleton) {
setAttachmentData(attachments);
}
// if (json.contains(JSON_AVATAR_ENTITIES) && json[JSON_AVATAR_ENTITIES].isArray()) {
// QJsonArray attachmentsJson = json[JSON_AVATAR_ATTACHMENTS].toArray();
// for (auto attachmentJson : attachmentsJson) {
// // TODO -- something
// }
// }
if (json.contains(JSON_AVATAR_ENTITIES) && json[JSON_AVATAR_ENTITIES].isArray()) {
QJsonArray attachmentsJson = json[JSON_AVATAR_ENTITIES].toArray();
for (auto attachmentJson : attachmentsJson) {
if (attachmentJson.isObject()) {
QVariantMap entityData = attachmentJson.toObject().toVariantMap();
QUuid entityID = entityData.value("id").toUuid();
QByteArray properties = QByteArray::fromBase64(entityData.value("properties").toByteArray());
updateAvatarEntity(entityID, properties);
}
}
}
if (json.contains(JSON_AVATAR_JOINT_ARRAY)) {
if (version == (int)JsonAvatarFrameVersion::JointRotationsInRelativeFrame) {
@ -2631,23 +2704,36 @@ void AvatarData::updateAvatarEntity(const QUuid& entityID, const QByteArray& ent
if (itr == _avatarEntityData.end()) {
if (_avatarEntityData.size() < MAX_NUM_AVATAR_ENTITIES) {
_avatarEntityData.insert(entityID, entityData);
_avatarEntityDataLocallyEdited = true;
markIdentityDataChanged();
}
} else {
itr.value() = entityData;
_avatarEntityDataLocallyEdited = true;
markIdentityDataChanged();
}
});
_avatarEntityDataChanged = true;
if (_clientTraitsHandler) {
// we have a client traits handler, so we need to mark this instanced trait as changed
// so that changes will be sent next frame
_clientTraitsHandler->markInstancedTraitUpdated(AvatarTraits::AvatarEntity, entityID);
}
}
void AvatarData::clearAvatarEntity(const QUuid& entityID) {
_avatarEntitiesLock.withWriteLock([&] {
_avatarEntityData.remove(entityID);
_avatarEntityDataLocallyEdited = true;
markIdentityDataChanged();
void AvatarData::clearAvatarEntity(const QUuid& entityID, bool requiresRemovalFromTree) {
bool removedEntity = false;
_avatarEntitiesLock.withWriteLock([this, &removedEntity, &entityID] {
removedEntity = _avatarEntityData.remove(entityID);
});
insertDetachedEntityID(entityID);
if (removedEntity && _clientTraitsHandler) {
// we have a client traits handler, so we need to mark this removed instance trait as deleted
// so that changes are sent next frame
_clientTraitsHandler->markInstancedTraitDeleted(AvatarTraits::AvatarEntity, entityID);
}
}
AvatarEntityMap AvatarData::getAvatarEntityData() const {
@ -2662,6 +2748,8 @@ void AvatarData::insertDetachedEntityID(const QUuid entityID) {
_avatarEntitiesLock.withWriteLock([&] {
_avatarEntityDetached.insert(entityID);
});
_avatarEntityDataChanged = true;
}
void AvatarData::setAvatarEntityData(const AvatarEntityMap& avatarEntityData) {
@ -2681,6 +2769,20 @@ void AvatarData::setAvatarEntityData(const AvatarEntityMap& avatarEntityData) {
foreach (auto entityID, previousAvatarEntityIDs) {
if (!_avatarEntityData.contains(entityID)) {
_avatarEntityDetached.insert(entityID);
if (_clientTraitsHandler) {
// we have a client traits handler, so we flag this removed entity as deleted
// so that changes are sent next frame
_clientTraitsHandler->markInstancedTraitDeleted(AvatarTraits::AvatarEntity, entityID);
}
}
}
if (_clientTraitsHandler) {
// if we have a client traits handler, flag any updated or created entities
// so that we send changes for them next frame
foreach (auto entityID, _avatarEntityData) {
_clientTraitsHandler->markInstancedTraitUpdated(AvatarTraits::AvatarEntity, entityID);
}
}
}

View file

@ -51,6 +51,7 @@
#include <udt/SequenceNumber.h>
#include "AABox.h"
#include "AvatarTraits.h"
#include "HeadData.h"
#include "PathUtils.h"
@ -371,6 +372,8 @@ public:
bool operator<(const AvatarPriority& other) const { return priority < other.priority; }
};
class ClientTraitsHandler;
class AvatarData : public QObject, public SpatiallyNestable {
Q_OBJECT
@ -425,7 +428,6 @@ public:
virtual ~AvatarData();
static const QUrl& defaultFullAvatarModelUrl();
QUrl cannonicalSkeletonModelURL(const QUrl& empty) const;
virtual bool isMyAvatar() const { return false; }
@ -924,11 +926,12 @@ public:
* @param {string} entityData
*/
Q_INVOKABLE void updateAvatarEntity(const QUuid& entityID, const QByteArray& entityData);
/**jsdoc
* @function MyAvatar.clearAvatarEntity
* @param {Uuid} entityID
*/
Q_INVOKABLE void clearAvatarEntity(const QUuid& entityID);
Q_INVOKABLE void clearAvatarEntity(const QUuid& entityID, bool requiresRemovalFromTree = true);
/**jsdoc
@ -944,23 +947,32 @@ public:
const HeadData* getHeadData() const { return _headData; }
struct Identity {
QUrl skeletonModelURL;
QVector<AttachmentData> attachmentData;
QString displayName;
QString sessionDisplayName;
bool isReplicated;
AvatarEntityMap avatarEntityData;
bool lookAtSnappingEnabled;
};
// identityChanged returns true if identity has changed, false otherwise.
// identityChanged returns true if identity has changed, false otherwise. Similarly for displayNameChanged and skeletonModelUrlChange.
void processAvatarIdentity(const QByteArray& identityData, bool& identityChanged,
bool& displayNameChanged, bool& skeletonModelUrlChanged);
void processAvatarIdentity(const QByteArray& identityData, bool& identityChanged, bool& displayNameChanged);
qint64 packTrait(AvatarTraits::TraitType traitType, ExtendedIODevice& destination,
AvatarTraits::TraitVersion traitVersion = AvatarTraits::NULL_TRAIT_VERSION);
qint64 packTraitInstance(AvatarTraits::TraitType traitType, AvatarTraits::TraitInstanceID instanceID,
ExtendedIODevice& destination, AvatarTraits::TraitVersion traitVersion = AvatarTraits::NULL_TRAIT_VERSION);
void processTrait(AvatarTraits::TraitType traitType, QByteArray traitBinaryData);
void processTraitInstance(AvatarTraits::TraitType traitType,
AvatarTraits::TraitInstanceID instanceID, QByteArray traitBinaryData);
void processDeletedTraitInstance(AvatarTraits::TraitType traitType, AvatarTraits::TraitInstanceID instanceID);
QByteArray identityByteArray(bool setIsReplicated = false) const;
QUrl getWireSafeSkeletonModelURL() const;
const QUrl& getSkeletonModelURL() const { return _skeletonModelURL; }
const QString& getDisplayName() const { return _displayName; }
const QString& getSessionDisplayName() const { return _sessionDisplayName; }
bool getLookAtSnappingEnabled() const { return _lookAtSnappingEnabled; }
@ -1077,6 +1089,7 @@ public:
void clearRecordingBasis();
TransformPointer getRecordingBasis() const;
void setRecordingBasis(TransformPointer recordingBasis = TransformPointer());
void createRecordingIDs();
QJsonObject toJson() const;
void fromJson(const QJsonObject& json, bool useFrameSkeleton = true);
@ -1327,7 +1340,6 @@ protected:
mutable HeadData* _headData { nullptr };
QUrl _skeletonModelURL;
bool _firstSkeletonCheck { true };
QUrl _skeletonFBXURL;
QVector<AttachmentData> _attachmentData;
QVector<AttachmentData> _oldAttachmentData;
@ -1410,8 +1422,8 @@ protected:
mutable ReadWriteLockable _avatarEntitiesLock;
AvatarEntityIDs _avatarEntityDetached; // recently detached from this avatar
AvatarEntityIDs _avatarEntityForRecording; // create new entities id for avatar recording
AvatarEntityMap _avatarEntityData;
bool _avatarEntityDataLocallyEdited { false };
bool _avatarEntityDataChanged { false };
// used to transform any sensor into world space, including the _hmdSensorMat, or hand controllers.
@ -1434,6 +1446,9 @@ protected:
bool _hasProcessedFirstIdentity { false };
float _density;
// null unless MyAvatar or ScriptableAvatar sending traits data to mixer
std::unique_ptr<ClientTraitsHandler> _clientTraitsHandler;
template <typename T, typename F>
T readLockWithNamedJointIndex(const QString& name, const T& defaultValue, F f) const {
int index = getFauxJointIndex(name);

View file

@ -19,10 +19,17 @@
#include <SharedUtil.h>
#include "AvatarLogging.h"
#include "AvatarTraits.h"
AvatarHashMap::AvatarHashMap() {
auto nodeList = DependencyManager::get<NodeList>();
auto& packetReceiver = nodeList->getPacketReceiver();
packetReceiver.registerListener(PacketType::BulkAvatarData, this, "processAvatarDataPacket");
packetReceiver.registerListener(PacketType::KillAvatar, this, "processKillAvatar");
packetReceiver.registerListener(PacketType::AvatarIdentity, this, "processAvatarIdentityPacket");
packetReceiver.registerListener(PacketType::BulkAvatarTraits, this, "processBulkAvatarTraits");
connect(nodeList.data(), &NodeList::uuidChanged, this, &AvatarHashMap::sessionUUIDChanged);
}
@ -182,9 +189,72 @@ void AvatarHashMap::processAvatarIdentityPacket(QSharedPointer<ReceivedMessage>
auto avatar = newOrExistingAvatar(identityUUID, sendingNode, isNewAvatar);
bool identityChanged = false;
bool displayNameChanged = false;
bool skeletonModelUrlChanged = false;
// In this case, the "sendingNode" is the Avatar Mixer.
avatar->processAvatarIdentity(message->getMessage(), identityChanged, displayNameChanged, skeletonModelUrlChanged);
avatar->processAvatarIdentity(message->getMessage(), identityChanged, displayNameChanged);
}
}
void AvatarHashMap::processBulkAvatarTraits(QSharedPointer<ReceivedMessage> message, SharedNodePointer sendingNode) {
while (message->getBytesLeftToRead()) {
// read the avatar ID to figure out which avatar this is for
auto avatarID = QUuid::fromRfc4122(message->readWithoutCopy(NUM_BYTES_RFC4122_UUID));
// grab the avatar so we can ask it to process trait data
bool isNewAvatar;
auto avatar = newOrExistingAvatar(avatarID, sendingNode, isNewAvatar);
// read the first trait type for this avatar
AvatarTraits::TraitType traitType;
message->readPrimitive(&traitType);
// grab the last trait versions for this avatar
auto& lastProcessedVersions = _processedTraitVersions[avatarID];
while (traitType != AvatarTraits::NullTrait) {
AvatarTraits::TraitVersion packetTraitVersion;
message->readPrimitive(&packetTraitVersion);
AvatarTraits::TraitWireSize traitBinarySize;
bool skipBinaryTrait = false;
if (AvatarTraits::isSimpleTrait(traitType)) {
message->readPrimitive(&traitBinarySize);
// check if this trait version is newer than what we already have for this avatar
if (packetTraitVersion > lastProcessedVersions[traitType]) {
avatar->processTrait(traitType, message->read(traitBinarySize));
lastProcessedVersions[traitType] = packetTraitVersion;
} else {
skipBinaryTrait = true;
}
} else {
AvatarTraits::TraitInstanceID traitInstanceID =
QUuid::fromRfc4122(message->readWithoutCopy(NUM_BYTES_RFC4122_UUID));
message->readPrimitive(&traitBinarySize);
auto& processedInstanceVersion = lastProcessedVersions.getInstanceValueRef(traitType, traitInstanceID);
if (packetTraitVersion > processedInstanceVersion) {
if (traitBinarySize == AvatarTraits::DELETED_TRAIT_SIZE) {
avatar->processDeletedTraitInstance(traitType, traitInstanceID);
} else {
avatar->processTraitInstance(traitType, traitInstanceID, message->read(traitBinarySize));
}
processedInstanceVersion = packetTraitVersion;
} else {
skipBinaryTrait = true;
}
}
if (skipBinaryTrait) {
// we didn't read this trait because it was older or because we didn't have an avatar to process it for
message->seek(message->getPosition() + traitBinarySize);
}
// read the next trait type, which is null if there are no more traits for this avatar
message->readPrimitive(&traitType);
}
}
}
@ -209,6 +279,9 @@ void AvatarHashMap::removeAvatar(const QUuid& sessionUUID, KillAvatarReason remo
}
void AvatarHashMap::handleRemovedAvatar(const AvatarSharedPointer& removedAvatar, KillAvatarReason removalReason) {
// remove any information about processed traits for this avatar
_processedTraitVersions.erase(removedAvatar->getID());
qCDebug(avatars) << "Removed avatar with UUID" << uuidStringWithoutCurlyBraces(removedAvatar->getSessionUUID())
<< "from AvatarHashMap" << removalReason;
emit avatarRemovedEvent(removedAvatar->getSessionUUID());

View file

@ -30,6 +30,7 @@
#include "ScriptAvatarData.h"
#include "AvatarData.h"
#include "AssociatedTraitValues.h"
/**jsdoc
* <strong>Note:</strong> An <code>AvatarList</code> API is also provided for Interface and client entity scripts: it is a
@ -133,6 +134,8 @@ protected slots:
*/
void processAvatarIdentityPacket(QSharedPointer<ReceivedMessage> message, SharedNodePointer sendingNode);
void processBulkAvatarTraits(QSharedPointer<ReceivedMessage> message, SharedNodePointer sendingNode);
/**jsdoc
* @function AvatarList.processKillAvatar
* @param {} message
@ -152,7 +155,7 @@ protected:
virtual void removeAvatar(const QUuid& sessionUUID, KillAvatarReason removalReason = KillAvatarReason::NoReason);
virtual void handleRemovedAvatar(const AvatarSharedPointer& removedAvatar, KillAvatarReason removalReason = KillAvatarReason::NoReason);
AvatarHash _avatarHash;
struct PendingAvatar {
std::chrono::steady_clock::time_point creationTime;
@ -163,6 +166,7 @@ protected:
AvatarPendingHash _pendingAvatars;
mutable QReadWriteLock _hashLock;
std::unordered_map<QUuid, AvatarTraits::TraitVersions> _processedTraitVersions;
private:
QUuid _lastOwnerSessionUUID;
};

View file

@ -0,0 +1,61 @@
//
// AvatarTraits.h
// libraries/avatars/src
//
// Created by Stephen Birarda on 7/30/18.
// Copyright 2018 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_AvatarTraits_h
#define hifi_AvatarTraits_h
#include <algorithm>
#include <cstdint>
#include <vector>
#include <QtCore/QUuid>
namespace AvatarTraits {
enum TraitType : int8_t {
NullTrait = -1,
SkeletonModelURL,
FirstInstancedTrait,
AvatarEntity = FirstInstancedTrait,
TotalTraitTypes
};
using TraitInstanceID = QUuid;
inline bool isSimpleTrait(TraitType traitType) {
return traitType > NullTrait && traitType < FirstInstancedTrait;
}
using TraitVersion = int32_t;
const TraitVersion DEFAULT_TRAIT_VERSION = 0;
const TraitVersion NULL_TRAIT_VERSION = -1;
using TraitWireSize = int16_t;
const TraitWireSize DELETED_TRAIT_SIZE = -1;
inline qint64 packInstancedTraitDelete(TraitType traitType, TraitInstanceID instanceID, ExtendedIODevice& destination,
TraitVersion traitVersion = NULL_TRAIT_VERSION) {
qint64 bytesWritten = 0;
bytesWritten += destination.writePrimitive(traitType);
if (traitVersion > DEFAULT_TRAIT_VERSION) {
bytesWritten += destination.writePrimitive(traitVersion);
}
bytesWritten += destination.write(instanceID.toRfc4122());
bytesWritten += destination.writePrimitive(DELETED_TRAIT_SIZE);
return bytesWritten;
}
};
#endif // hifi_AvatarTraits_h

View file

@ -0,0 +1,143 @@
//
// ClientTraitsHandler.cpp
// libraries/avatars/src
//
// Created by Stephen Birarda on 7/30/18.
// Copyright 2018 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 "ClientTraitsHandler.h"
#include <QtCore/QObject>
#include <NodeList.h>
#include <NLPacketList.h>
#include "AvatarData.h"
ClientTraitsHandler::ClientTraitsHandler(AvatarData* owningAvatar) :
_owningAvatar(owningAvatar)
{
auto nodeList = DependencyManager::get<NodeList>();
QObject::connect(nodeList.data(), &NodeList::nodeAdded, [this](SharedNodePointer addedNode){
if (addedNode->getType() == NodeType::AvatarMixer) {
resetForNewMixer();
}
});
nodeList->getPacketReceiver().registerListener(PacketType::SetAvatarTraits, this, "processTraitOverride");
}
void ClientTraitsHandler::resetForNewMixer() {
// re-set the current version to 0
_currentTraitVersion = AvatarTraits::DEFAULT_TRAIT_VERSION;
// mark that all traits should be sent next time
_shouldPerformInitialSend = true;
}
void ClientTraitsHandler::sendChangedTraitsToMixer() {
if (hasChangedTraits() || _shouldPerformInitialSend) {
// we have at least one changed trait to send
auto nodeList = DependencyManager::get<NodeList>();
auto avatarMixer = nodeList->soloNodeOfType(NodeType::AvatarMixer);
if (!avatarMixer || !avatarMixer->getActiveSocket()) {
// we don't have an avatar mixer with an active socket, we can't send changed traits at this time
return;
}
// we have a mixer to send to, setup our set traits packet
auto traitsPacketList = NLPacketList::create(PacketType::SetAvatarTraits, QByteArray(), true, true);
// bump and write the current trait version to an extended header
// the trait version is the same for all traits in this packet list
traitsPacketList->writePrimitive(++_currentTraitVersion);
// take a copy of the set of changed traits and clear the stored set
auto traitStatusesCopy { _traitStatuses };
_traitStatuses.reset();
_hasChangedTraits = false;
auto simpleIt = traitStatusesCopy.simpleCBegin();
while (simpleIt != traitStatusesCopy.simpleCEnd()) {
// because the vector contains all trait types (for access using trait type as index)
// we double check that it is a simple iterator here
auto traitType = static_cast<AvatarTraits::TraitType>(std::distance(traitStatusesCopy.simpleCBegin(), simpleIt));
if (_shouldPerformInitialSend || *simpleIt == Updated) {
if (traitType == AvatarTraits::SkeletonModelURL) {
_owningAvatar->packTrait(traitType, *traitsPacketList);
// keep track of our skeleton version in case we get an override back
_currentSkeletonVersion = _currentTraitVersion;
}
}
++simpleIt;
}
auto instancedIt = traitStatusesCopy.instancedCBegin();
while (instancedIt != traitStatusesCopy.instancedCEnd()) {
for (auto& instanceIDValuePair : instancedIt->instances) {
if ((_shouldPerformInitialSend && instanceIDValuePair.value != Deleted)
|| instanceIDValuePair.value == Updated) {
// this is a changed trait we need to send or we haven't send out trait information yet
// ask the owning avatar to pack it
_owningAvatar->packTraitInstance(instancedIt->traitType, instanceIDValuePair.id, *traitsPacketList);
} else if (!_shouldPerformInitialSend && instanceIDValuePair.value == Deleted) {
// pack delete for this trait instance
AvatarTraits::packInstancedTraitDelete(instancedIt->traitType, instanceIDValuePair.id,
*traitsPacketList);
}
}
++instancedIt;
}
nodeList->sendPacketList(std::move(traitsPacketList), *avatarMixer);
// if this was an initial send of all traits, consider it completed
_shouldPerformInitialSend = false;
}
}
void ClientTraitsHandler::processTraitOverride(QSharedPointer<ReceivedMessage> message, SharedNodePointer sendingNode) {
if (sendingNode->getType() == NodeType::AvatarMixer) {
while (message->getBytesLeftToRead()) {
AvatarTraits::TraitType traitType;
message->readPrimitive(&traitType);
AvatarTraits::TraitVersion traitVersion;
message->readPrimitive(&traitVersion);
AvatarTraits::TraitWireSize traitBinarySize;
message->readPrimitive(&traitBinarySize);
// only accept an override if this is for a trait type we override
// and the version matches what we last sent for skeleton
if (traitType == AvatarTraits::SkeletonModelURL
&& traitVersion == _currentSkeletonVersion
&& _traitStatuses[AvatarTraits::SkeletonModelURL] != Updated) {
// override the skeleton URL but do not mark the trait as having changed
// so that we don't unecessarily send a new trait packet to the mixer with the overriden URL
auto encodedSkeletonURL = QUrl::fromEncoded(message->readWithoutCopy(traitBinarySize));
auto hasChangesBefore = _hasChangedTraits;
_owningAvatar->setSkeletonModelURL(encodedSkeletonURL);
// setSkeletonModelURL will flag us for changes to the SkeletonModelURL so we reset some state here to
// avoid unnecessarily sending the overriden skeleton model URL back to the mixer
_traitStatuses.erase(AvatarTraits::SkeletonModelURL);
_hasChangedTraits = hasChangesBefore;
} else {
message->seek(message->getPosition() + traitBinarySize);
}
}
}
}

View file

@ -0,0 +1,61 @@
//
// ClientTraitsHandler.h
// libraries/avatars/src
//
// Created by Stephen Birarda on 7/30/18.
// Copyright 2018 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_ClientTraitsHandler_h
#define hifi_ClientTraitsHandler_h
#include <ReceivedMessage.h>
#include "AssociatedTraitValues.h"
#include "Node.h"
class AvatarData;
class ClientTraitsHandler : public QObject {
Q_OBJECT
public:
ClientTraitsHandler(AvatarData* owningAvatar);
void sendChangedTraitsToMixer();
bool hasChangedTraits() { return _hasChangedTraits; }
void markTraitUpdated(AvatarTraits::TraitType updatedTrait)
{ _traitStatuses[updatedTrait] = Updated; _hasChangedTraits = true; }
void markInstancedTraitUpdated(AvatarTraits::TraitType traitType, QUuid updatedInstanceID)
{ _traitStatuses.instanceInsert(traitType, updatedInstanceID, Updated); _hasChangedTraits = true; }
void markInstancedTraitDeleted(AvatarTraits::TraitType traitType, QUuid deleteInstanceID)
{ _traitStatuses.instanceInsert(traitType, deleteInstanceID, Deleted); _hasChangedTraits = true; }
void resetForNewMixer();
public slots:
void processTraitOverride(QSharedPointer<ReceivedMessage> message, SharedNodePointer sendingNode);
private:
enum ClientTraitStatus {
Unchanged,
Updated,
Deleted
};
AvatarData* _owningAvatar;
AvatarTraits::AssociatedTraitValues<ClientTraitStatus, Unchanged> _traitStatuses;
AvatarTraits::TraitVersion _currentTraitVersion { AvatarTraits::DEFAULT_TRAIT_VERSION };
AvatarTraits::TraitVersion _currentSkeletonVersion { AvatarTraits::NULL_TRAIT_VERSION };
bool _shouldPerformInitialSend { false };
bool _hasChangedTraits { false };
};
#endif // hifi_ClientTraitsHandler_h

View file

@ -269,6 +269,26 @@ QVector<AttachmentData> ScriptAvatarData::getAttachmentData() const {
// END
//
#if PR_BUILD || DEV_BUILD
//
// ENTITY PROPERTIES
// START
//
AvatarEntityMap ScriptAvatarData::getAvatarEntities() const {
AvatarEntityMap scriptEntityData;
if (AvatarSharedPointer sharedAvatarData = _avatarData.lock()) {
return sharedAvatarData->getAvatarEntityData();
}
return scriptEntityData;
}
//
// ENTITY PROPERTIES
// END
//
#endif
//
// AUDIO PROPERTIES

View file

@ -116,6 +116,10 @@ public:
Q_INVOKABLE QStringList getJointNames() const;
Q_INVOKABLE QVector<AttachmentData> getAttachmentData() const;
#if DEV_BUILD || PR_BUILD
Q_INVOKABLE AvatarEntityMap getAvatarEntities() const;
#endif
//
// AUDIO PROPERTIES
//

View file

@ -149,11 +149,6 @@ void EntityEditPacketSender::queueEditEntityMessage(PacketType type,
void EntityEditPacketSender::queueEraseEntityMessage(const EntityItemID& entityItemID) {
// in case this was a clientOnly entity:
if(_myAvatar) {
_myAvatar->clearAvatarEntity(entityItemID);
}
QByteArray bufferOut(NLPacket::maxPayloadSize(PacketType::EntityErase), 0);
if (EntityItemProperties::encodeEraseEntityMessage(entityItemID, bufferOut)) {

View file

@ -27,7 +27,6 @@ public:
void setMyAvatar(AvatarData* myAvatar) { _myAvatar = myAvatar; }
AvatarData* getMyAvatar() { return _myAvatar; }
void clearAvatarEntity(QUuid entityID) { assert(_myAvatar); _myAvatar->clearAvatarEntity(entityID); }
/// Queues an array of several voxel edit messages. Will potentially send a pending multi-command packet. Determines
/// which voxel-server node or nodes the packet should be sent to. Can be called even before voxel servers are known, in

View file

@ -574,7 +574,7 @@ void EntityScriptingInterface::deleteEntity(QUuid id) {
_activityTracking.deletedEntityCount++;
EntityItemID entityID(id);
bool shouldDelete = true;
bool shouldSendDeleteToServer = true;
// If we have a local entity tree set, then also update it.
if (_entityTree) {
@ -591,16 +591,21 @@ void EntityScriptingInterface::deleteEntity(QUuid id) {
auto avatarHashMap = DependencyManager::get<AvatarHashMap>();
AvatarSharedPointer myAvatar = avatarHashMap->getAvatarBySessionID(myNodeID);
myAvatar->insertDetachedEntityID(id);
shouldDelete = false;
shouldSendDeleteToServer = false;
return;
}
if (entity->getLocked()) {
shouldDelete = false;
shouldSendDeleteToServer = false;
} else {
// only delete local entities, server entities will round trip through the server filters
if (entity->getClientOnly() || _entityTree->isServerlessMode()) {
shouldSendDeleteToServer = false;
_entityTree->deleteEntity(entityID);
if (entity->getClientOnly() && getEntityPacketSender()->getMyAvatar()) {
getEntityPacketSender()->getMyAvatar()->clearAvatarEntity(entityID, false);
}
}
}
}
@ -608,7 +613,7 @@ void EntityScriptingInterface::deleteEntity(QUuid id) {
}
// if at this point, we know the id, and we should still delete the entity, send the update to the entity server
if (shouldDelete) {
if (shouldSendDeleteToServer) {
getEntityPacketSender()->queueEraseEntityMessage(entityID);
}
}

View file

@ -212,6 +212,8 @@ GLShader* GLBackend::compileBackendProgram(const Shader& program, const Shader::
glprogram = ::gl::buildProgram(shaderGLObjects);
if (!::gl::linkProgram(glprogram, compilationLogs[version].message)) {
qCWarning(gpugllogging) << "GLBackend::compileBackendProgram - Program didn't link:\n" << compilationLogs[version].message.c_str();
compilationLogs[version].compiled = false;
glDeleteProgram(glprogram);
glprogram = 0;
return nullptr;
@ -254,7 +256,7 @@ GLint GLBackend::getRealUniformLocation(GLint location) const {
// uniforms. If someone is requesting a uniform that isn't in the remapping structure
// that's a bug from the calling code, because it means that location wasn't in the
// reflection
qWarning() << "Unexpected location requested for shader";
qWarning() << "Unexpected location requested for shader: #" << location;
return INVALID_UNIFORM_INDEX;
}
return itr->second;

View file

@ -0,0 +1,39 @@
//
// ExtendedIODevice.h
// libraries/networking/src
//
// Created by Stephen Birarda on 8/7/18.
// Copyright 2018 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_ExtendedIODevice_h
#define hifi_ExtendedIODevice_h
#include <QtCore/QIODevice>
class ExtendedIODevice : public QIODevice {
public:
ExtendedIODevice(QObject* parent = nullptr) : QIODevice(parent) {};
template<typename T> qint64 peekPrimitive(T* data);
template<typename T> qint64 readPrimitive(T* data);
template<typename T> qint64 writePrimitive(const T& data);
};
template<typename T> qint64 ExtendedIODevice::peekPrimitive(T* data) {
return peek(reinterpret_cast<char*>(data), sizeof(T));
}
template<typename T> qint64 ExtendedIODevice::readPrimitive(T* data) {
return read(reinterpret_cast<char*>(data), sizeof(T));
}
template<typename T> qint64 ExtendedIODevice::writePrimitive(const T& data) {
static_assert(!std::is_pointer<T>::value, "T must not be a pointer");
return write(reinterpret_cast<const char*>(&data), sizeof(T));
}
#endif // hifi_ExtendedIODevice_h

View file

@ -11,13 +11,10 @@
#include "NodeData.h"
NodeData::NodeData(const QUuid& nodeID) :
NodeData::NodeData(const QUuid& nodeID, NetworkPeer::LocalID nodeLocalID) :
_mutex(),
_nodeID(nodeID)
_nodeID(nodeID),
_nodeLocalID(nodeLocalID)
{
}
NodeData::~NodeData() {
}

View file

@ -16,6 +16,7 @@
#include <QtCore/QObject>
#include <QtCore/QSharedPointer>
#include "NetworkPeer.h"
#include "NLPacket.h"
#include "ReceivedMessage.h"
@ -24,17 +25,19 @@ class Node;
class NodeData : public QObject {
Q_OBJECT
public:
NodeData(const QUuid& nodeID = QUuid());
virtual ~NodeData() = 0;
NodeData(const QUuid& nodeID = QUuid(), NetworkPeer::LocalID localID = NetworkPeer::NULL_LOCAL_ID);
virtual ~NodeData() = default;
virtual int parseData(ReceivedMessage& message) { return 0; }
const QUuid& getNodeID() const { return _nodeID; }
NetworkPeer::LocalID getNodeLocalID() const { return _nodeLocalID; }
QMutex& getMutex() { return _mutex; }
private:
QMutex _mutex;
QUuid _nodeID;
NetworkPeer::LocalID _nodeLocalID;
};
#endif // hifi_NodeData_h

View file

@ -77,12 +77,6 @@ BasePacket::BasePacket(std::unique_ptr<char[]> data, qint64 size, const HifiSock
}
BasePacket::BasePacket(const BasePacket& other) :
QIODevice()
{
*this = other;
}
BasePacket& BasePacket::operator=(const BasePacket& other) {
_packetSize = other._packetSize;
_packet = std::unique_ptr<char[]>(new char[_packetSize]);

View file

@ -16,16 +16,15 @@
#include <memory>
#include <QtCore/QIODevice>
#include <PortableHighResolutionClock.h>
#include "../HifiSockAddr.h"
#include "Constants.h"
#include "../ExtendedIODevice.h"
namespace udt {
class BasePacket : public QIODevice {
class BasePacket : public ExtendedIODevice {
Q_OBJECT
public:
static const qint64 PACKET_WRITE_ERROR;
@ -85,15 +84,11 @@ public:
void setReceiveTime(p_high_resolution_clock::time_point receiveTime) { _receiveTime = receiveTime; }
p_high_resolution_clock::time_point getReceiveTime() const { return _receiveTime; }
template<typename T> qint64 peekPrimitive(T* data);
template<typename T> qint64 readPrimitive(T* data);
template<typename T> qint64 writePrimitive(const T& data);
protected:
BasePacket(qint64 size);
BasePacket(std::unique_ptr<char[]> data, qint64 size, const HifiSockAddr& senderSockAddr);
BasePacket(const BasePacket& other);
BasePacket(const BasePacket& other) : ExtendedIODevice() { *this = other; }
BasePacket& operator=(const BasePacket& other);
BasePacket(BasePacket&& other);
BasePacket& operator=(BasePacket&& other);
@ -116,19 +111,6 @@ protected:
p_high_resolution_clock::time_point _receiveTime; // captures the time the packet received (only used on receiving end)
};
template<typename T> qint64 BasePacket::peekPrimitive(T* data) {
return peek(reinterpret_cast<char*>(data), sizeof(T));
}
template<typename T> qint64 BasePacket::readPrimitive(T* data) {
return read(reinterpret_cast<char*>(data), sizeof(T));
}
template<typename T> qint64 BasePacket::writePrimitive(const T& data) {
static_assert(!std::is_pointer<T>::value, "T must not be a pointer");
return write(reinterpret_cast<const char*>(&data), sizeof(T));
}
} // namespace udt

View file

@ -40,7 +40,7 @@ PacketVersion versionForPacketType(PacketType packetType) {
case PacketType::AvatarData:
case PacketType::BulkAvatarData:
case PacketType::KillAvatar:
return static_cast<PacketVersion>(AvatarMixerPacketVersion::FarGrabJoints);
return static_cast<PacketVersion>(AvatarMixerPacketVersion::MigrateAvatarEntitiesToTraits);
case PacketType::MessagesData:
return static_cast<PacketVersion>(MessageDataVersion::TextOrBinaryData);
// ICE packets
@ -94,6 +94,8 @@ PacketVersion versionForPacketType(PacketType packetType) {
return static_cast<PacketVersion>(AvatarQueryVersion::ConicalFrustums);
case PacketType::AvatarIdentityRequest:
return 22;
case PacketType::EntityQueryInitialResultsComplete:
return static_cast<PacketVersion>(EntityVersion::ParticleSpin);
default:
return 22;
}

View file

@ -56,7 +56,7 @@ public:
ICEServerPeerInformation,
ICEServerQuery,
OctreeStats,
UNUSED_PACKET_TYPE_1,
SetAvatarTraits,
AvatarIdentityRequest,
AssignmentClientStatus,
NoisyMute,
@ -133,6 +133,7 @@ public:
EntityClone,
EntityQueryInitialResultsComplete,
BulkAvatarTraits,
NUM_PACKET_TYPE
};
@ -289,7 +290,9 @@ enum class AvatarMixerPacketVersion : PacketVersion {
FBXReaderNodeReparenting,
FixMannequinDefaultAvatarFeet,
ProceduralFaceMovementFlagsAndBlendshapes,
FarGrabJoints
FarGrabJoints,
MigrateSkeletonURLToTraits,
MigrateAvatarEntitiesToTraits
};
enum class DomainConnectRequestVersion : PacketVersion {

View file

@ -14,8 +14,7 @@
#include <memory>
#include <QtCore/QIODevice>
#include "../ExtendedIODevice.h"
#include "Packet.h"
#include "PacketHeaders.h"
@ -25,7 +24,7 @@ namespace udt {
class Packet;
class PacketList : public QIODevice {
class PacketList : public ExtendedIODevice {
Q_OBJECT
public:
using MessageNumber = uint32_t;
@ -59,9 +58,6 @@ public:
virtual bool isSequential() const override { return false; }
virtual qint64 size() const override { return getDataSize(); }
template<typename T> qint64 readPrimitive(T* data);
template<typename T> qint64 writePrimitive(const T& data);
qint64 writeString(const QString& string);
protected:
@ -105,16 +101,6 @@ private:
QByteArray _extendedHeader;
};
template <typename T> qint64 PacketList::readPrimitive(T* data) {
static_assert(!std::is_pointer<T>::value, "T must not be a pointer");
return read(reinterpret_cast<char*>(data), sizeof(T));
}
template <typename T> qint64 PacketList::writePrimitive(const T& data) {
static_assert(!std::is_pointer<T>::value, "T must not be a pointer");
return write(reinterpret_cast<const char*>(&data), sizeof(T));
}
template<typename T> std::unique_ptr<T> PacketList::takeFront() {
static_assert(std::is_base_of<Packet, T>::value, "T must derive from Packet.");

View file

@ -496,10 +496,6 @@ void RenderDeferredSetup::run(const render::RenderContextPointer& renderContext,
batch.setPipeline(program);
}
// Adjust the texcoordTransform in the case we are rendeirng a sub region(mini mirror)
auto textureFrameTransform = gpu::Framebuffer::evalSubregionTexcoordTransformCoefficients(deferredFramebuffer->getFrameSize(), args->_viewport);
batch._glUniform4fv(ru::Uniform::TexcoordTransform, 1, reinterpret_cast< const float* >(&textureFrameTransform));
// Setup the global lighting
deferredLightingEffect->setupKeyLightBatch(args, batch);
@ -560,23 +556,18 @@ void RenderDeferredLocals::run(const render::RenderContextPointer& renderContext
batch.setViewportTransform(viewport);
batch.setStateScissorRect(viewport);
auto textureFrameTransform = gpu::Framebuffer::evalSubregionTexcoordTransformCoefficients(deferredFramebuffer->getFrameSize(), viewport);
auto& lightIndices = lightClusters->_visibleLightIndices;
if (!lightIndices.empty() && lightIndices[0] > 0) {
deferredLightingEffect->setupLocalLightsBatch(batch, lightClusters);
// Local light pipeline
batch.setPipeline(deferredLightingEffect->_localLight);
batch._glUniform4fv(ru::Uniform::TexcoordTransform, 1, reinterpret_cast<const float*>(&textureFrameTransform));
batch.draw(gpu::TRIANGLE_STRIP, 4);
// Draw outline as well ?
if (lightingModel->isShowLightContourEnabled()) {
batch.setPipeline(deferredLightingEffect->_localLightOutline);
batch._glUniform4fv(ru::Uniform::TexcoordTransform, 1, reinterpret_cast<const float*>(&textureFrameTransform));
batch.draw(gpu::TRIANGLE_STRIP, 4);
}

View file

@ -282,6 +282,7 @@ void Model::reset() {
const FBXGeometry& geometry = getFBXGeometry();
_rig.reset(geometry);
emit rigReset();
emit rigReady();
}
}

View file

@ -0,0 +1,82 @@
//
// Created by Sam Gateau on 7/31/2018.
// Copyright 2018 High Fidelity, Inc.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
<@if not WORKLOAD_RESOURCE_SLH@>
<@def WORKLOAD_RESOURCE_SLH@>
<@include gpu/Color.slh@>
<$declareColorWheel()$>
const vec4 REGION_COLOR[4] = vec4[4](
vec4(0.0, 1.0, 0.0, 1.0),
vec4(1.0, 0.6, 0.0, 1.0),
vec4(1.0, 0.0, 0.0, 1.0),
vec4(0.3, 0.0, 0.8, 1.0)
);
<@func declareWorkloadProxies() @>
struct WorkloadProxy {
vec4 sphere;
vec4 region;
};
#if defined(GPU_GL410)
layout(binding=0) uniform samplerBuffer workloadProxiesBuffer;
WorkloadProxy getWorkloadProxy(int i) {
int offset = 2 * i;
WorkloadProxy proxy;
proxy.sphere = texelFetch(workloadProxiesBuffer, offset);
proxy.region = texelFetch(workloadProxiesBuffer, offset + 1);
return proxy;
}
#else
layout(std140, binding=0) buffer workloadProxiesBuffer {
WorkloadProxy _proxies[];
};
WorkloadProxy getWorkloadProxy(int i) {
WorkloadProxy proxy = _proxies[i];
return proxy;
}
#endif
<@endfunc@>
<@func declareWorkloadViews() @>
struct WorkloadView {
vec4 direction_far;
vec4 fov;
vec4 origin;
vec4 backFront[2];
vec4 regions[3];
};
#if defined(GPU_GL410)
layout(binding=1) uniform samplerBuffer workloadViewsBuffer;
WorkloadView getWorkloadView(int i) {
int offset = 2 * i;
WorkloadView view;
view.origin = texelFetch(workloadViewsBuffer, offset);
view.radiuses = texelFetch(workloadViewsBuffer, offset + 1);
return view;
}
#else
layout(std140, binding=1) buffer workloadViewsBuffer {
WorkloadView _views[];
};
WorkloadView getWorkloadView(int i) {
WorkloadView view = _views[i];
return view;
}
#endif
<@endfunc@>
<@endif@>

View file

@ -16,8 +16,6 @@
layout(location=RENDER_UTILS_ATTR_TEXCOORD01) out vec4 _texCoord01;
layout(location=RENDER_UTILS_UNIFORM_LIGHT_TEXCOORD_TRANSFORM) uniform vec4 texcoordFrameTransform;
void main(void) {
const float depth = 1.0;
const vec4 UNIT_QUAD[4] = vec4[4](
@ -30,8 +28,5 @@ void main(void) {
_texCoord01.xy = (pos.xy + 1.0) * 0.5;
_texCoord01.xy *= texcoordFrameTransform.zw;
_texCoord01.xy += texcoordFrameTransform.xy;
gl_Position = pos;
}

View file

@ -15,11 +15,13 @@
in vec4 varColor;
in vec3 varTexcoord;
in vec3 varEyePos;
void main(void) {
if (varColor.w > 0.0) {
float r = sqrt(dot(varTexcoord.xyz,varTexcoord.xyz));
float a = paintStripe(r * varColor.w, 0.0, 1.0 / varColor.w, 0.05 / varColor.w);
float d = varColor.w / abs(varEyePos.z);
float a = paintStripe(r * d, 0.0, 1.0 / d, 0.002 / d);
if (a <= 0.1 || r > 1.1) {
discard;
}

View file

@ -15,40 +15,13 @@
<@include gpu/Transform.slh@>
<$declareStandardTransform()$>
<@include gpu/Color.slh@>
<$declareColorWheel()$>
layout(location=0) uniform vec4 inColor;
struct WorkloadProxy {
vec4 sphere;
vec4 region;
};
#if defined(GPU_GL410)
layout(binding=0) uniform samplerBuffer workloadProxiesBuffer;
WorkloadProxy getWorkloadProxy(int i) {
int offset = 2 * i;
WorkloadProxy proxy;
proxy.sphere = texelFetch(workloadProxiesBuffer, offset);
proxy.region = texelFetch(workloadProxiesBuffer, offset + 1);
return proxy;
}
#else
layout(std140, binding=0) buffer workloadProxiesBuffer {
WorkloadProxy _proxies[];
};
WorkloadProxy getWorkloadProxy(int i) {
WorkloadProxy proxy = _proxies[i];
return proxy;
}
#endif
<@include WorkloadResource.slh@>
<$declareWorkloadProxies()$>
out vec4 varColor;
out vec3 varTexcoord;
out vec3 varEyePos;
void main(void) {
const vec4 UNIT_SPRITE[3] = vec4[3](
@ -79,6 +52,7 @@ void main(void) {
vec3 dirY = vec3(0.0, 1.0, 0.0);
vec4 pos = vec4(proxyPosEye.xyz + proxy.sphere.w * ( dirX * spriteVert.x + dirY * spriteVert.y /* + dirZ * spriteVert.z*/), 1.0);
varEyePos = pos.xyz;
varTexcoord = spriteVert.xyz;
<$transformEyeToClipPos(cam, pos, gl_Position)$>
@ -86,7 +60,7 @@ void main(void) {
int region = floatBitsToInt(proxy.region.x);
region = (0x000000FF & region);
varColor = vec4(colorWheel(float(region) / 4.0), proxy.sphere.w);
varColor = vec4(REGION_COLOR[region].xyz, proxy.sphere.w);
if (region == 4) {
gl_Position = vec4(0.0);

View file

@ -16,11 +16,13 @@
in vec4 varColor;
in vec3 varTexcoord;
in vec3 varEyePos;
void main(void) {
if (varColor.w > 0.0) {
float r = sqrt(dot(varTexcoord.xyz,varTexcoord.xyz));
float a = paintStripe(r * varColor.w, 0.0, 1.0 / varColor.w, 0.05 / varColor.w);
float d = varColor.w / abs(varEyePos.z);
float a = paintStripe(r * d, 0.0, 1.0 / d, 0.005 / d);
if (a <= 0.1 || r > 1.1) {
discard;
}

View file

@ -15,45 +15,12 @@
<@include gpu/Transform.slh@>
<$declareStandardTransform()$>
<@include gpu/Color.slh@>
<$declareColorWheel()$>
layout(location=0) uniform vec4 inColor;
struct WorkloadView {
vec4 direction_far;
vec4 fov;
vec4 origin;
vec4 backFront[2];
vec4 regions[3];
};
#if defined(GPU_GL410)
layout(binding=1) uniform samplerBuffer workloadViewsBuffer;
WorkloadView getWorkloadView(int i) {
int offset = 2 * i;
WorkloadView view;
view.origin = texelFetch(workloadViewsBuffer, offset);
//view.radiuses = texelFetch(workloadViewsBuffer, offset + 1);
return view;
}
#else
layout(std140,binding=1) buffer workloadViewsBuffer {
WorkloadView _views[];
};
WorkloadView getWorkloadView(int i) {
WorkloadView view = _views[i];
return view;
}
#endif
<@include WorkloadResource.slh@>
<$declareWorkloadViews()$>
out vec4 varColor;
out vec3 varTexcoord;
out vec3 varEyePos;
const int NUM_VERTICES_PER_SEGMENT = 2;
const int NUM_SEGMENT_PER_VIEW_REGION = 65;
@ -112,12 +79,13 @@ void main(void) {
<$transformModelToEyeDir(cam, obj, originSpaceTan, tanEye)$>
lateralDir = normalize(cross(vec3(0.0, 0.0, 1.0), normalize(tanEye)));
posEye.xyz += (0.05 * (regionID + 1)) * (-1.0 + 2.0 * float(segmentVertexID)) * lateralDir;
posEye.xyz += (0.005 * abs(posEye.z) * (regionID + 1)) * (-1.0 + 2.0 * float(segmentVertexID)) * lateralDir;
varEyePos = posEye.xyz;
<$transformEyeToClipPos(cam, posEye, gl_Position)$>
varTexcoord = spriteVert.xyz;
// Convert region to color
varColor = vec4(colorWheel(float(regionID) / 4.0), -1.0);
varColor = vec4(REGION_COLOR[regionID].xyz, -1.0);
}

View file

@ -9,10 +9,10 @@
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
<@include DeferredBufferWrite.slh@>
layout(location=0) in vec4 _color;
layout(location=0) out vec4 _fragColor;
void main(void) {
_fragColor = _color;
packDeferredFragmentUnlit(vec3(1.0, 0.0, 0.0), 1.0, _color.rgb);
}

View file

@ -176,9 +176,7 @@ ScriptEngine::ScriptEngine(Context context, const QString& scriptContents, const
_timerFunctionMap(),
_fileNameString(fileNameString),
_arrayBufferClass(new ArrayBufferClass(this)),
_assetScriptingInterface(new AssetScriptingInterface(this)),
// don't delete `ScriptEngines` until all `ScriptEngine`s are gone
_scriptEngines(DependencyManager::get<ScriptEngines>())
_assetScriptingInterface(new AssetScriptingInterface(this))
{
switch (_context) {
case Context::CLIENT_SCRIPT:

View file

@ -806,8 +806,6 @@ protected:
static const QString _SETTINGS_ENABLE_EXTENDED_EXCEPTIONS;
Setting::Handle<bool> _enableExtendedJSExceptions { _SETTINGS_ENABLE_EXTENDED_EXCEPTIONS, true };
QSharedPointer<ScriptEngines> _scriptEngines;
};
ScriptEnginePointer scriptEngineFactory(ScriptEngine::Context context,

View file

@ -91,12 +91,24 @@ void ControlViews::run(const workload::WorkloadContextPointer& runContext, const
regulateViews(outViews, inTimings);
}
// Export the timings for debuging
// Export the ranges for debuging
bool doExport = false;
if (outViews.size()) {
_dataExport.ranges[workload::Region::R1] = outViews[0].regionBackFronts[workload::Region::R1];
_dataExport.ranges[workload::Region::R2] = outViews[0].regionBackFronts[workload::Region::R2];
_dataExport.ranges[workload::Region::R3] = outViews[0].regionBackFronts[workload::Region::R3];
doExport = true;
}
// Export the ranges and timings for debuging
if (inTimings.size()) {
_dataExport.timings[workload::Region::R1] = std::chrono::duration<float, std::milli>(inTimings[0]).count();
_dataExport.timings[workload::Region::R2] = _dataExport.timings[workload::Region::R1];
_dataExport.timings[workload::Region::R3] = std::chrono::duration<float, std::milli>(inTimings[1]).count();
doExport = true;
}
if (doExport) {
auto config = std::static_pointer_cast<Config>(runContext->jobConfig);
config->dataExport = _dataExport;
config->emitDirty();
@ -129,11 +141,6 @@ void ControlViews::regulateViews(workload::Views& outViews, const workload::Timi
regionBackFronts[workload::Region::R3] = regionRegulators[workload::Region::R3].run(loopDuration, timings[1], regionBackFronts[workload::Region::R3]);
enforceRegionContainment();
_dataExport.ranges[workload::Region::R1] = regionBackFronts[workload::Region::R1];
_dataExport.ranges[workload::Region::R2] = regionBackFronts[workload::Region::R2];
_dataExport.ranges[workload::Region::R3] = regionBackFronts[workload::Region::R3];
for (auto& outView : outViews) {
outView.regionBackFronts[workload::Region::R1] = regionBackFronts[workload::Region::R1];
outView.regionBackFronts[workload::Region::R2] = regionBackFronts[workload::Region::R2];

View file

@ -32,9 +32,9 @@ namespace workload {
};
const std::vector<glm::vec2> MAX_VIEW_BACK_FRONTS = {
{ 100.0f, 200.0f },
{ 150.0f, 300.0f },
{ 250.0f, 500.0f }
{ 100.0f, 1600.0f },
{ 150.0f, 10000.0f },
{ 250.0f, 16000.0f }
};
const float RELATIVE_STEP_DOWN = 0.05f;
@ -192,7 +192,7 @@ namespace workload {
struct Data {
bool regulateViewRanges{ false };
bool regulateViewRanges{ false }; // regulation is OFF by default
} data;
struct DataExport {

View file

@ -16,16 +16,9 @@
var ICON_URL = Script.resolvePath("../../../system/assets/images/luci-i.svg");
var ACTIVE_ICON_URL = Script.resolvePath("../../../system/assets/images/luci-a.svg");
var onAppScreen = false;
var onTablet = false; // set this to true to use the tablet, false use a floating window
function onClicked() {
if (onAppScreen) {
tablet.gotoHomeScreen();
} else {
tablet.loadQMLSource(QMLAPP_URL);
}
}
var onAppScreen = false;
var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system");
var button = tablet.addButton({
@ -36,42 +29,90 @@
var hasEventBridge = false;
function wireEventBridge(on) {
if (!tablet) {
print("Warning in wireEventBridge(): 'tablet' undefined!");
return;
}
if (on) {
if (!hasEventBridge) {
tablet.fromQml.connect(fromQml);
hasEventBridge = true;
var onScreen = false;
var window;
function onClicked() {
if (onTablet) {
if (onAppScreen) {
tablet.gotoHomeScreen();
} else {
tablet.loadQMLSource(QMLAPP_URL);
}
} else {
if (hasEventBridge) {
tablet.fromQml.disconnect(fromQml);
hasEventBridge = false;
if (onScreen) {
killWindow()
} else {
createWindow()
}
}
}
function createWindow() {
var qml = Script.resolvePath(QMLAPP_URL);
window = Desktop.createWindow(Script.resolvePath(QMLAPP_URL), {
title: 'Workload Inspector',
flags: Desktop.ALWAYS_ON_TOP,
presentationMode: Desktop.PresentationMode.NATIVE,
size: {x: 400, y: 600}
});
// window.setPosition(200, 50);
window.closed.connect(killWindow);
window.fromQml.connect(fromQml);
onScreen = true
button.editProperties({isActive: true});
}
function killWindow() {
if (window !== undefined) {
window.closed.disconnect(killWindow);
window.fromQml.disconnect(fromQml);
window.close()
window = undefined
}
onScreen = false
button.editProperties({isActive: false})
}
function wireEventBridge(on) {
if (onTablet) {
if (!tablet) {
print("Warning in wireEventBridge(): 'tablet' undefined!");
return;
}
if (on) {
if (!hasEventBridge) {
tablet.fromQml.connect(fromQml);
hasEventBridge = true;
}
} else {
if (hasEventBridge) {
tablet.fromQml.disconnect(fromQml);
hasEventBridge = false;
}
}
}
}
function onScreenChanged(type, url) {
if (url === QMLAPP_URL) {
onAppScreen = true;
} else {
onAppScreen = false;
if (onTablet) {
if (url === QMLAPP_URL) {
onAppScreen = true;
} else {
onAppScreen = false;
}
button.editProperties({isActive: onAppScreen});
wireEventBridge(onAppScreen);
}
button.editProperties({isActive: onAppScreen});
wireEventBridge(onAppScreen);
}
function fromQml(message) {
}
button.clicked.connect(onClicked);
tablet.screenChanged.connect(onScreenChanged);
Script.scriptEnding.connect(function () {
killWindow()
if (onAppScreen) {
tablet.gotoHomeScreen();
}
@ -84,6 +125,7 @@
Script.include("./test_physics_scene.js")
function fromQml(message) {
print("fromQml: " + JSON.stringify(message))
switch (message.method) {
case "createScene":
createScene();
@ -113,8 +155,14 @@
}
function sendToQml(message) {
tablet.sendToQml(message);
if (onTablet) {
tablet.sendToQml(message);
} else {
if (window) {
window.sendToQml(message);
}
}
}
updateGridInQML()
}());
}());

View file

@ -160,9 +160,9 @@ Rectangle {
}
Repeater {
model: [
"r1Front:500:1.0",
"r2Front:500:1.0",
"r3Front:500:1.0"
"r1Front:16000:1.0",
"r2Front:16000:1.0",
"r3Front:16000:1.0"
]
ConfigSlider {
showLabel: false
@ -259,18 +259,71 @@ Rectangle {
}
]
}
/* PlotPerf {
title: "Ranges"
height: 100
object: stats.controlViews
valueScale: 1.0
valueUnit: "m"
plots: [
{
prop: "r3RangeFront",
label: "R3 F",
color: "#FF0000"
},
{
prop: "r3RangeBack",
label: "R3 B",
color: "#EF0000"
},
{
prop: "r2RangeFront",
label: "R2 F",
color: "orange"
},
{
prop: "r2RangeBack",
label: "R2 B",
color: "magenta"
},
{
prop: "r1RangeFront",
label: "R1 F",
color: "#00FF00"
},
{
prop: "r1RangeBack",
label: "R1 B",
color: "#00EF00"
},
]
}*/
Separator {}
HifiControls.Label {
text: "Numbers:";
text: "Ranges & Numbers:";
}
HifiControls.Label {
text: "R1= " + Workload.getConfig("regionState")["numR1"];
}
HifiControls.Label {
text: "R2= " + Workload.getConfig("regionState")["numR2"];
}
HifiControls.Label {
text: "R3= " + Workload.getConfig("regionState")["numR3"];
Repeater {
model: [
"green:R1:numR1:r1RangeBack:r1RangeFront",
"orange:R2:numR2:r2RangeBack:r2RangeFront",
"red:R3:numR3:r3RangeBack:r3RangeFront"
]
RowLayout {
anchors.left: parent.left
anchors.right: parent.right
HifiControls.Label {
text: modelData.split(":")[1] + " : " + Workload.getConfig("regionState")[modelData.split(":")[2]] ;
color: modelData.split(":")[0]
}
HifiControls.Label {
text: Workload.getConfig("controlViews")[modelData.split(":")[3]].toFixed(0) ;
color: modelData.split(":")[0]
}
HifiControls.Label {
text: Workload.getConfig("controlViews")[modelData.split(":")[4]].toFixed(0) ;
color: modelData.split(":")[0]
}
}
}
Separator {}

View file

@ -20,7 +20,6 @@ Script.include("/~/system/libraries/controllers.js");
// constants from AvatarBookmarks.h
var ENTRY_AVATAR_URL = "avatarUrl";
var ENTRY_AVATAR_ATTACHMENTS = "attachments";
var ENTRY_AVATAR_ENTITIES = "avatarEntites";
var ENTRY_AVATAR_SCALE = "avatarScale";
var ENTRY_VERSION = "version";
@ -31,7 +30,7 @@ function executeLater(callback) {
var INVALID_JOINT_INDEX = -1
function isWearable(avatarEntity) {
return avatarEntity.properties.visible === true && avatarEntity.properties.parentJointIndex !== INVALID_JOINT_INDEX &&
return avatarEntity.properties.visible === true && (avatarEntity.properties.parentJointIndex !== INVALID_JOINT_INDEX || avatarEntity.properties.relayParentJoints === true) &&
(avatarEntity.properties.parentID === MyAvatar.sessionUUID || avatarEntity.properties.parentID === MyAvatar.SELF_ID);
}
@ -57,7 +56,6 @@ function getMyAvatar() {
var avatar = {}
avatar[ENTRY_AVATAR_URL] = MyAvatar.skeletonModelURL;
avatar[ENTRY_AVATAR_SCALE] = MyAvatar.getAvatarScale();
avatar[ENTRY_AVATAR_ATTACHMENTS] = MyAvatar.getAttachmentsVariant();
avatar[ENTRY_AVATAR_ENTITIES] = getMyAvatarWearables();
return avatar;
}
@ -72,12 +70,15 @@ function getMyAvatarSettings() {
}
}
function updateAvatarWearables(avatar, bookmarkAvatarName) {
function updateAvatarWearables(avatar, bookmarkAvatarName, callback) {
executeLater(function() {
var wearables = getMyAvatarWearables();
avatar[ENTRY_AVATAR_ENTITIES] = wearables;
sendToQml({'method' : 'wearablesUpdated', 'wearables' : wearables, 'avatarName' : bookmarkAvatarName})
if(callback)
callback();
});
}
@ -235,6 +236,32 @@ function fromQml(message) { // messages are {method, params}, like json-rpc. See
Messages.messageReceived.disconnect(handleWearableMessages);
Messages.unsubscribe('Hifi-Object-Manipulation');
break;
case 'addWearable':
var joints = MyAvatar.getJointNames();
var hipsIndex = -1;
for(var i = 0; i < joints.length; ++i) {
if(joints[i] === 'Hips') {
hipsIndex = i;
break;
}
}
var properties = {
name: "Custom wearable",
type: "Model",
modelURL: message.url,
parentID: MyAvatar.sessionUUID,
relayParentJoints: false,
parentJointIndex: hipsIndex
};
var entityID = Entities.addEntity(properties, true);
updateAvatarWearables(currentAvatar, message.avatarName, function() {
onSelectedEntity(entityID);
});
break;
case 'selectWearable':
ensureWearableSelected(message.entityID);
break;

View file

@ -110,7 +110,7 @@ Script.include("/~/system/libraries/controllers.js");
SEAT: 'seat' // The current target is a seat
};
var speed = 12.0;
var speed = 9.3;
var accelerationAxis = {x: 0.0, y: -5.0, z: 0.0};
function Teleporter(hand) {